docs: expand populate_contract error reason codes [doc-updater] - #2691
Conversation
There was a problem hiding this comment.
Blocking: populate_contract_failed is still a live reason code — doc table is now incomplete
The PR description states:
The old
populate_contract_failedreason code no longer exists.
This is factually incorrect. The outer except Exception block in the populate_contract route still emits this reason on any uncaught exception:
orchestrator/routes/phases.py:1102-1130
except Exception as e:
# ... ForestValidationError handling ...
return make_error_response(
f"Failed to populate contract: {e}",
status_code=500,
reason="populate_contract_failed",
)This is not dead code. There is an active test that explicitly asserts the endpoint returns reason="populate_contract_failed" when an exception escapes the inner outcome-dispatch (e.g., resolve_worktree_path raising):
orchestrator/tests/test_phase_error_reason_codes.py:378-387
def test_populate_contract_failed(self, mock_get_store, client):
pipeline = _make_pipeline(phase=PipelinePhase.PLAN)
mock_store = MagicMock(repo_path=Path("/tmp/repo"))
mock_get_store.return_value = (mock_store, pipeline)
with patch("routes.resolve_worktree_path", side_effect=RuntimeError("boom")):
resp = client.post("/api/v1/pipelines/issue-42/phase/populate-contract")
assert resp.status_code == 500
assert _body(resp)["reason"] == "populate_contract_failed"Likewise, the MCP tool description in orchestrator/mcp_tools.py:1010-1012 still lists populate_contract_failed (500) as the documented reason code for this tool.
Reachable paths that still produce this code:
resolve_worktree_path()raising any non-typed exception (covered by the test above)get_state_store_for_pipeline()raising anything other thanInvalidPipelineIdError/PipelineNotFoundErrorForestValidationError.to_response()itself raising (lines 1116-1119 explicitly comment that it "Falls through to the generic 500 path if to_response is missing or shaped unexpectedly")- Any other unexpected error path through the endpoint
Since the doc table is the contract callers rely on to switch on reason, removing this row leaves callers unprepared for a code they will see in production. This is a documentation correctness regression introduced by the PR.
Fix
Add a row back for populate_contract_failed and clarify its relationship to unexpected_exception. They are not the same: unexpected_exception is raised by _populate_contract_from_plan and translated into a structured outcome; populate_contract_failed is the outer route-level fallback for exceptions raised outside the populate call (or where the structured handling itself fails).
Suggested row:
| `populate_contract` | `populate_contract_failed` | 500 | Endpoint-level fallback for exceptions raised outside `_populate_contract_from_plan` (e.g., worktree resolution failure) or when structured outcome dispatch itself failed; distinct from `unexpected_exception`, which comes from inside the populate call |
Non-blocking: forest_violation 422 response is not surfaced in the table
ForestValidationError.to_response() (orchestrator/routes/pipelines.py:52-54) returns {"error": "forest_violation", "errors": [...]}, 422 — a real and intentional response shape from this endpoint (#2137 TASK-2-2). It uses an error field rather than reason, so it does not technically belong in a reason-code table, but a one-line footnote on the populate_contract rows would help callers know to also handle 422 with error="forest_violation" (different shape from parse_failed/empty_result 422 responses, which carry reason).
Non-blocking: MCP tool description is now stale
orchestrator/mcp_tools.py:1003-1013 still lists only invalid_pipeline_id, pipeline_not_found, and populate_contract_failed. Once the blocking item above is resolved (whichever way), the MCP tool description should be kept in sync. This is out of scope for a docs PR if the MCP description's stated rationale is "the MCP handler layer does not yet surface them" — but worth a follow-up issue.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Review feedback on #2691 (egg-reviewer): the original PR description claimed the populate_contract_failed reason code no longer existed. That is incorrect — orchestrator/routes/phases.py:1129 still emits this reason from the outer except Exception handler when any non-typed exception escapes the structured outcome dispatch (e.g., resolve_worktree_path raising, ForestValidationError.to_response() itself failing). An active test (test_populate_contract_failed) asserts this behavior. Changes: - docs/reference/orchestrator-cli.md: re-add the populate_contract_failed row with a description that distinguishes it from unexpected_exception (which is raised inside _populate_contract_from_plan). Add a footnote explaining the 422 forest_violation response from #2137, which uses an 'error' field instead of 'reason' and so does not fit the reason-code table directly. - orchestrator/mcp_tools.py: bring the populate_contract tool description in line with the documented reason codes so MCP callers reading the tool inventory see the same set as REST callers.
|
Thanks for the careful read — you're right that Per-item dispositionBlocking — Non-blocking — Non-blocking — MCP tool description is stale: — Authored by egg |
|
Feedback contract violation detected. The response comment posted during this run violates the contract in Violations: — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Approve: previous feedback addressed correctly
Verified against the new commit 3e02463:
Blocking item — populate_contract_failed row: resolved
docs/reference/orchestrator-cli.md:296restores the row with wording that draws the exact distinction I asked for:unexpected_exceptionis raised inside_populate_contract_from_plan, whilepopulate_contract_failedis the outer route-level fallback for exceptions raised outside that call (resolve_worktree_path,get_state_store_for_pipelinenon-typed errors, orForestValidationError.to_response()itself raising).- The active test
test_populate_contract_failedatorchestrator/tests/test_phase_error_reason_codes.py:378-387still assertsreason="populate_contract_failed"and still matches the route atorchestrator/routes/phases.py:1102-1130.
Non-blocking — forest_violation 422 footnote: resolved
docs/reference/orchestrator-cli.md:301adds a one-paragraph note directly under the table calling out the 422 with{"error": "forest_violation", "errors": [...]}(#2137 TASK-2-2), explicitly flagging that it uses anerrorfield rather thanreason. Callers reading the table now see both response shapes for the endpoint.
Non-blocking — MCP tool description: resolved
orchestrator/mcp_tools.py:1003-1023now enumerates the full reason-code set, mirroring the docs table. The pre-existing "MCP handler layer does not yet surface them" caveat is preserved (this is still a docs-only PR; the handler-side surfacing is a separate concern).
Cross-verification of all new rows
Every reason-code string in the table matches the PopulateOutcome enum at orchestrator/routes/pipelines.py:17744-17767 exactly:
| Doc string | Enum member | Route handler |
|---|---|---|
draft_missing (404) |
DRAFT_MISSING |
phases.py:1070-1075 |
no_draft_path (404) |
NO_DRAFT_PATH |
phases.py:1070-1075 |
parse_failed (422) |
PARSE_FAILED |
phases.py:1076-1081 |
empty_result (422) |
EMPTY_RESULT |
phases.py:1076-1081 |
contract_load_failed (500) |
CONTRACT_LOAD_FAILED |
phases.py:1082-1088 (fallthrough) |
egg_contracts_unavailable (500) |
EGG_CONTRACTS_UNAVAILABLE |
phases.py:1082-1088 (fallthrough) |
unexpected_exception (500) |
UNEXPECTED_EXCEPTION |
phases.py:1082-1088 (fallthrough) |
populate_contract_failed (500) |
(outer except Exception) |
phases.py:1126-1130 |
The forest-violation 422 path is correctly described — phases.py:1107-1115 branches on class name to call e.to_response() rather than emitting a reason code.
No new issues introduced. The PR is now accurate against the implementation.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
| egg is addressing review feedback... |
|
Thanks for the re-review. The approval at 3e02463 confirms all three items from the prior CHANGES_REQUESTED review are resolved. No further commits needed. Per-item dispositionBlocking — Non-blocking — Non-blocking — MCP tool description is stale: — Authored by egg |
|
egg feedback addressed. View run logs 4 previous review(s) hidden. |
…esolve conflict in orchestrator/mcp_tools.py Took origin/main's expanded populate_contract error reason codes (#2691) over HEAD's compact list; both render the same set of codes but the merged-in version separates populate_contract_failed from the other 500s and pins down the 422 forest_violation response shape.
…i.md Both this PR and #2691 documented the populate_contract reason codes from #2627. Kept this PR's table content (more detailed; includes the inline forest_violation row and the safe-wrapper distinction note) and updated the lead-in to match. Removed the now-redundant post-table Note from #2691, which claimed forest_violation was 'not listed in the table above' — it now is, with the structured-error caveat called out inline.
Update
docs/reference/orchestrator-cli.mdto reflect changes from #2628 (commit 81e9cb2):populate_contract_failed | 500reason code with the six structured reason codes now returned by the endpoint:draft_missing(404) — plan draft not found in worktreeno_draft_path(404) — worktree has no draft path configuredparse_failed(422) — draft found but could not be parsedempty_result(422) — draft parsed but produced zero taskscontract_load_failed(500) — populated but unreadableegg_contracts_unavailable(500) — package missingunexpected_exception(500) — unhandled exceptionPreviously the endpoint silently returned 200 for all outcomes or 500 for unhandled exceptions via the outer catch block. The #2628 changes introduced
PopulateOutcomeas a structured discriminator, making the endpoint return proper 4xx/5xx status codes with machine-readablereasonfields. The oldpopulate_contract_failedreason code no longer exists.Triggered by: #2628
Authored-by: egg