fix: Dockerfile never copied docs/, breaking every stage on real deploys - #31
Merged
psi16181918161phi merged 1 commit intoAug 7, 2026
Conversation
Root cause of the first live Railway deployment failing on every single run: task_specs.py reads docs/task-specs.md at runtime to load each stage's system prompt, but the Dockerfile only ever copied src/ and frontend/ — never docs/. Every stage failed immediately with FileNotFoundError the moment it tried to load its own prompt. Found by actually testing the deployed app end to end (not just inspecting code) and tracing the failure from the live Railway logs back through runs.py and orchestrator.py to this exact line. While tracing it, found and fixed two more real bugs in the same failure path: 1. orchestrator.py: get_task_spec() was called in _run_stage() outside its own try/except block, so when it failed, the exception propagated uncaught past the stage's "running" event with no matching "error" event ever emitted for that specific stage — leaving it stuck at "running" forever in the UI even though the overall run correctly showed "error". Moved the call inside the try block so any failure there is now reported the same way as any other stage failure. 2. runs.py: RunState never persisted the run-level error detail message anywhere — _finish() only set run.status, discarding the exception text after one transient SSE publish. If a client's connection wasn't live at that exact moment (e.g. a dropped/reconnecting SSE stream, which is what actually happened during testing), the real error message was gone forever with no way to retrieve it via the API afterward. Added a `detail` field to RunState, persisted it in _finish() and load_from_disk(), and included it in to_dict() — the frontend already had full support for displaying it, it just never had real data to show. Verified the actual root-cause fix by rebuilding the Docker image and confirming inside the running container: docs/ is present, and get_task_spec() successfully loads real Task Specification text (it previously would have raised FileNotFoundError here). 142 tests passing (8 new), 100% coverage, ruff/mypy/bandit clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why this is urgent
This is the actual bug behind the failed test run on the live Railway demo just now. Every single run failed immediately at the Research stage.
Root cause
task_specs.pyreadsdocs/task-specs.mdat runtime to load each stage's system prompt — but the Dockerfile only ever copiedsrc/andfrontend/, neverdocs/. Every stage failed instantly withFileNotFoundErrorthe moment it tried to load its own prompt.Found this by actually testing the deployed app end to end and tracing the failure from the live Railway logs, through
runs.py, intoorchestrator.py, down to this exact line.Two more bugs found in the same failure path
orchestrator.py:get_task_spec()was called outside_run_stage()'s own try/except, so this failure propagated uncaught past the "running" event with no matching "error" event — the stage stayed stuck at "running" in the UI forever, even though the overall run correctly showed "error". Fixed by moving the call inside the try block.runs.py: the run-level error message was never actually persisted anywhere — only published once over SSE and then discarded. If a client's connection dropped at that exact moment (which is what happened during testing — the SSE stream was reconnecting repeatedly), the real error message was gone forever, unrecoverable via the API. Added adetailfield toRunState, persisted through_finish()andload_from_disk(), included into_dict(). The frontend already fully supports displaying it — it just never had real data to show.Verification
docs/is present,get_task_spec()successfully loads real spec text (previously would have raisedFileNotFoundErrorhere)