docs: expand populate_contract error reason codes [doc-updater] - #2692
Conversation
There was a problem hiding this comment.
Doc-only PR, but several of the new descriptions don't match what the populate_contract endpoint at orchestrator/routes/phases.py:1037-1130 actually returns. The endpoint calls _populate_contract_from_plan directly (not the _safe wrapper), so the safe-wrapper's extra "check origin" / pre-raise behaviour does not apply to HTTP callers. The table is the source of truth for clients switching on reason, so the descriptions need to be accurate to the endpoint's real behaviour.
Blocking — factual inaccuracies in the new rows
1. draft_missing (404) — description is wrong for this endpoint (docs/reference/orchestrator-cli.md:289)
"Plan draft missing from both local worktree and origin; re-run the plan phase or restore the draft file"
The HTTP endpoint calls _populate_contract_from_plan (the unsafe variant) at phases.py:1049-1054. The DRAFT_MISSING outcome there is set purely by a local-only check — if not plan_path.exists(): at pipelines.py:18321-18329. The "local-and-origin" check only exists in _populate_contract_from_plan_safe and only for source="plan_complete" (pipelines.py:18186-18225), and even then it raises PlanDraftMissingOnLocalError / PlanDraftMissingOnLocalAndOriginError rather than returning DRAFT_MISSING. The endpoint never goes through that path, so this reason will fire whenever the draft is missing from the local worktree, regardless of origin state. The doc currently tells operators the wrong thing about when this code fires and what state they need to fix.
Suggested wording: Plan draft missing from the pipeline's local worktree at the configured draft path; re-run the plan phase or restore the file before retrying.
2. contract_load_failed (500) — temporal order is reversed (docs/reference/orchestrator-cli.md:293)
"Contract could not be loaded after population"
The load happens before populate writes anything, at pipelines.py:18331-18340:
try:
contract = load_contract(pipeline_id, repo_path)
except Exception as load_err:
...
return PopulateResult(PopulateOutcome.CONTRACT_LOAD_FAILED)If load fails, the populator never runs at all. "After population" misdirects an operator debugging this — they'll look for a corrupted post-populate contract when the actual failure is reading the existing contract first. Suggested wording: Existing contract on disk could not be loaded prior to population.
3. egg_contracts_unavailable (500) — wrong process (docs/reference/orchestrator-cli.md:294)
"
egg_contractspackage unavailable in the sandbox"
The populate_contract endpoint runs in the orchestrator process, not the sandbox. The import check at pipelines.py:18301-18309 happens inside the orchestrator's REST handler. "In the sandbox" sends operators looking at the wrong container. Suggested wording: egg_contracts package failed to import in the orchestrator process.
Blocking — omissions
4. populate_contract_failed was removed from the table but is still returned by the endpoint
The PR body and commit message both describe this as a replacement of the single 500 code. But the catch-all is still live at phases.py:1120-1130:
logger.error("contract_populate_endpoint_failed", ...)
return make_error_response(
f"Failed to populate contract: {e}",
status_code=500,
reason="populate_contract_failed",
)This fires when an exception escapes the inner function — e.g., get_state_store_for_pipeline / resolve_worktree_path raise, or the dynamic from routes.pipelines import … fails (phases.py:1039-1047). It's the residual generic-500 path; the PR removed its row from the doc anyway. Either keep the row (with a description scoped to "uncaught error outside the populator helper, normally pre-empted by one of the specific codes above") or remove the catch-all from phases.py so the source actually matches the doc.
5. forest_violation (422) is missing from the table entirely
This is the highest-value 422 a real client will hit — it fires when the planner emits a multi-parent slice DAG and is the documented (#2137) NACK path. The endpoint handles it in the outer except at phases.py:1107-1119 and returns the body returned by ForestValidationError.to_response() (pipelines.py:52-54):
return ({"error": "forest_violation", "errors": self.errors}, 422)Two things worth noting:
- The body key is
error, notreason. That already breaks the section's lead-in claim ("All four endpoints include a stable, machine-readablereasonfield in error responses"). Either document thatpopulate_contracthas aforest_violation422 with body shape{"error": "forest_violation", "errors": [...]}(i.e. it does not follow thereasonconvention for this case), or harmonize the response —to_response()could emitreasoninstead oferror. As-is, a client following the doc and switching only onreasonwill never recognise the forest_violation case. - Without this row, operators staring at a 422 with
errors=[...]and noreasonfield have nothing in the doc that explains what they're looking at.
Non-blocking — imprecisions
6. parse_failed (422) description is too narrow (docs/reference/orchestrator-cli.md:291)
"Plan draft found but could not be parsed (malformed
yaml-taskssection)"
parse_plan returns success=False for multiple distinct reasons, not all yaml-tasks-related — e.g., empty document (plan_parser.py:1081-1083) and "no tasks or phases found in plan document" (plan_parser.py:1198-1200). An operator whose plan is empty will read this row and look for a non-existent yaml-tasks block. Suggested wording: parse_plan returned success=False (e.g., empty plan document, missing or malformed yaml-tasks appendix).
7. empty_result (422) description is incomplete (docs/reference/orchestrator-cli.md:292)
"Plan draft parsed successfully but produced no tasks"
EMPTY_RESULT requires changed=False, which means no slices/tasks AND no PR metadata (pipelines.py:18365-18526). A draft that yielded a pr_title but no tasks would still come back as POPULATED. Suggested wording: parse succeeded but produced no slices/tasks and no PR metadata (changed=False).
Out of scope but related
The lead-in at line 273 says "All four endpoints include a stable, machine-readable reason field in error responses." The forest_violation 422 from this endpoint and the structured 400-list shape from earlier endpoints don't actually follow this. Worth a clarifying sentence either way once #5 is resolved.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Address reviewer feedback on PR #2692: - draft_missing: scope description to the local worktree check that fires in _populate_contract_from_plan (the HTTP endpoint does not use the _safe wrapper, so the origin lookup never runs here). - contract_load_failed: load happens before population, not after. - egg_contracts_unavailable: import runs in the orchestrator process, not the agent sandbox. - parse_failed: broaden to cover empty plan documents and other parser-rejected inputs, not just malformed yaml-tasks. - empty_result: require no slices/tasks AND no PR metadata (changed= False); a draft yielding only a pr_title still comes back POPULATED. - forest_violation: add the row that was missing entirely; flag that the body key is error rather than reason so reason-only clients don't silently miss the case. - populate_contract_failed: keep the residual catch-all row — it is still live in routes/phases.py:1126 and fires for exceptions that escape the populator helper. - Lead-in: note the two cases that break the reason-field convention.
|
Thanks for the careful read — the reviewer's verification against the HTTP endpoint caught real inaccuracies. All blocking and non-blocking items are addressed in 906f7a5. Disposition
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-reviewed against 906f7a5. All eight items from the prior review are addressed correctly — I verified each claim against the code:
| # | Item | Status |
|---|---|---|
| 1 | draft_missing description |
Fixed. New wording correctly scopes to the local-only check at _populate_contract_from_plan (pipelines.py:18321-18329) and explains why the safe-wrapper's PlanDraftMissingOnLocal{,AndOrigin}Error raises don't apply to the HTTP path. |
| 2 | contract_load_failed temporal order |
Fixed. New wording matches pipelines.py:18331-18340 (load happens before populate). |
| 3 | egg_contracts_unavailable process |
Fixed. Now correctly attributes the import to the orchestrator process. |
| 4 | populate_contract_failed row removed |
Fixed. Row restored with a description scoped to the residual catch-all at phases.py:1120-1130. |
| 5 | forest_violation row missing |
Fixed. Row added with the {"error": "forest_violation", "errors": [...]} body shape called out (matches pipelines.py:52-54 / to_response()), plus the contract.plan_review_feedback stash. |
| 6 | parse_failed too narrow |
Fixed. Broadened wording covers all parse_plan success=False paths (empty doc, missing/malformed yaml-tasks, etc. — verified in plan_parser.py:1081-1257). |
| 7 | empty_result incomplete |
Fixed. Now correctly notes changed=False requires no slices/tasks AND no PR metadata (matches pipelines.py:18365-18526). |
| 8 | Lead-in convention claim | Partially fixed. See below. |
Non-blocking — one new imprecision in the lead-in
The reworded lead-in (line 273) says:
Two cases break this convention and are noted inline in the table:
populate_contract'sforest_violation422 ships the structured errors under anerrorkey (noreason), and the earlier 400-list shape returns a list of structured entries rather than a top-levelreason.
The first half is well-supported — the new forest_violation row carries the inline note. The second half is a dangling reference: there is no row in the table (or anywhere else in the doc) that documents an "earlier 400-list shape." I grepped the four endpoints (start_pipeline at pipelines.py:21800, advance_phase at phases.py:224, complete_phase at phases.py:818, populate_contract at phases.py:1037) and every 400 response goes through make_error_response (phases.py:64-83), which emits the standard {success, message, reason, details} shape. There is no return jsonify([…]) or list-shaped 400 anywhere in orchestrator/routes/. A reader switching on reason will read this lead-in, look for the second exception in the table, and find nothing — that's worse than not mentioning it.
This appears to have come from my prior review's "out of scope but related" note about a "structured 400-list shape from earlier endpoints" — that was a speculative aside on my part that should have been verified before being lifted into the doc verbatim. Either:
- Drop the second half of the sentence, since the only convention-breaker actually documented is
forest_violation; or - Find the concrete endpoint that returns a 400-list shape (if one exists outside the four populate-related endpoints I checked) and add a row for it.
Suggested wording (option 1):
Two cases break this convention and are noted inline in the table:
populate_contract'sforest_violation422 ships the structured errors under anerrorkey (noreason).
Or, since there's only one case left, drop "Two cases" entirely:
One case breaks this convention and is noted inline in the table:
populate_contract'sforest_violation422 ships the structured errors under anerrorkey (noreason).
Approving — the new content is materially more accurate than what was on main, and this is a minor copy-edit on a single sentence in the lead-in.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
The lead-in claimed two convention-breakers (forest_violation's error
key and an earlier 400-list shape), but no row documented the
400-list case and no endpoint in orchestrator/routes/ returns one —
every 400 goes through make_error_response with the standard
{success, message, reason, details} shape. Reword to mention only
the real exception (forest_violation).
|
Thanks for the careful re-review — you're right that the second half of the lead-in was a phantom reference. I verified independently: every 400 in Disposition
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Check Failure: Integration Tests / Integration TestsWhat's failing: The sandbox image build ( Root cause: Transient deadsnakes PPA outage (Launchpad 503). Not caused by this PR — the PR only modifies What needs to be done:
Suggestion (out of scope for this PR): The two RUN blocks in — Authored by egg |
Autofix tracking{"Test/Integration Tests / Integration Tests": 4} |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…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.
Conflict Resolution SummaryResolved merge conflict with
DetailBoth this PR and #2691 expanded the
The auto-merged lead-in (line 273) already reflects this PR's wording: "One case breaks this convention and is noted inline in the table: Verification
Please review: the choice to remove #2691's post-table Note. If the reviewer prefers to keep both the inline row AND the post-table Note for redundancy, the Note can be re-added after the table — but it would need its "not listed in the table above" wording reworked. — Authored by egg |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Check Failure: Integration Tests / Integration TestsWhat's failing: The Root cause: Transient infrastructure outage on Launchpad's CDN — This PR only modifies What needs to be done:
Suggestion (longer-term): If deadsnakes 503s recur frequently, consider adding retries around — Authored by egg |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Check Failure: Integration Tests / Integration TestsWhat's failing: The test calls Root cause (unrelated to this PR):
What needs to be done:
Why I'm not committing a fix: — Authored by egg |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Check Failure: Integration TestsWhat's failing: Root cause: Flaky integration test, unrelated to this PR.
What needs to be done:
Suggestion: I'm not commiting an empty-commit retrigger from the autofixer because the failure is purely environmental and the safer signal is to let a human decide whether to rerun or investigate the underlying flake. — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-reviewed against 501b37d (the current head). Since my last review at 906f7a5, the only PR-authored change is commit 187b4fa — the rest is merge traffic from main (#2691 and #2693 landed in the meantime and were resolved at 579c694).
Prior-review item resolution
| # | Item | Status |
|---|---|---|
| 8 | Lead-in dangling reference to "earlier 400-list shape" | Fixed. Adopted the option-1 wording verbatim — Two cases → One case, dropped the second half of the sentence. The lead-in at docs/reference/orchestrator-cli.md:273 now references only the real exception (forest_violation's error key). |
I independently re-verified the deleted claim: there is no list-shaped 400 anywhere in orchestrator/routes/. Every 400 in phases.py (make_error_response at phases.py:75-94), pipelines.py, signals.py, etc. goes through the standard {success, message, reason, details} envelope. grep -n "return jsonify(\[" across orchestrator/routes/ returns no matches. The deleted text was indeed phantom.
Merge-traffic content (not PR-authored, but worth flagging)
The merge resolution at 579c694 pulled in two additions from #2693 (the WORKTREE_BASE_DIR notes in the Context PR and per-slice BRC sections at lines 472 and 481). These were merged from main and reviewed under #2693 — not in scope for this re-review, but they integrate cleanly with the surrounding sections and don't conflict with the populate_contract changes.
Table content (unchanged since 906f7a5)
No regressions in the table rows or the forest_violation callout. All eight prior items remain correctly resolved.
Approving. The PR is now narrowly scoped to a single sentence change in the lead-in, which directly addresses the only outstanding non-blocking note from the prior round.
— Authored by egg
|
egg review completed. View run logs 16 previous review(s) hidden. |
Update orchestrator CLI reference docs to reflect changes from 81e9cb2 (Fix #2627):
populate_contract_failed(500) reason code with the six specific codes introduced by the fix:draft_missing(404),no_draft_path(404),parse_failed(422),empty_result(422),contract_load_failed(500),egg_contracts_unavailable(500), andunexpected_exception(500)The
populate_contractendpoint now returns structured 4xx/5xx responses instead of always returning 200 when the draft is missing or the populate step fails silently.Triggered by: #2628
Authored-by: egg