docs: context PR hook wiring and observability events [doc-updater] - #2610
Conversation
Co-authored-by: doc-updater
There was a problem hiding this comment.
This is a doc-only PR documenting #2599. The new sentence about advance_phase triggering the context-PR hook is accurate (verified at orchestrator/routes/phases.py:468-485 — _maybe_open_base_pr_for_plan_to_implement is called on plan→implement transitions).
The new Observability paragraph, however, has two serious accuracy problems. The first is a clear documentation bug; the second is an empirically-falsifiable claim about how the orchestrator surfaces these events.
Blocking — Documentation example contradicts the code
docs/reference/orchestrator-cli.md:462:
context_pr.skippedmeans the hook ran without error but found no PR to open (e.g. the pipeline has norepoorbase_branch)
The example is exactly the case that does not emit a context_pr.skipped event. The wrapper guards the emission on if pipeline.repo and pipeline.base_branch: (orchestrator/routes/pipelines.py:10589). When either is falsy, nothing is emitted — this is pinned by test_does_not_emit_for_local_mode_pipeline in orchestrator/tests/test_context_pr_transition_paths.py:384:
issue_pipeline.repo = None
...
assert reports == [], "must not emit context_pr.* on local-mode pipelines (no remote)"The actual trigger for context_pr.skipped is: pipeline has both repo and base_branch, the inner hook returned without raising, and contract.pr.context_pr_number is still None afterwards — i.e., one of the inner short-circuits inside _open_context_pr_for_pipeline fired (no pr block on the contract, ContractNotFoundError, lookup error, etc.). Pick one of those for the example, or drop the parenthetical entirely.
Blocking — recent_messages / wait-status claim is empirically false
docs/reference/orchestrator-cli.md:462:
These appear in
recent_messagesinget_status/wait-statusresponses.
This is not how report_pipeline_status works on the current implementation. I verified empirically by running the function and observing the message store and event bus:
StatusReporter handlers count: 0
Messages before report: 0
Messages after report: 0 # message_store unchanged
Events received before: 0
Events received after: 0 # event_bus unchanged
Walking the code:
_maybe_open_base_pr_for_plan_to_implementcallsreport_pipeline_status(pipeline, event_type="context_pr.skipped"|"context_pr.failed", message=...)(orchestrator/routes/pipelines.py:10621).report_pipeline_status(orchestrator/status_reporter.py:425) callsget_status_reporter().report_status(...)→_dispatch_update(update)._dispatch_updateiteratesself._handlers. No production code callsStatusReporter.add_handler—grep -rn "add_handler" orchestrator/*.py orchestrator/routes/*.pyoutside tests returns onlycontainer_monitor.add_handler(different class) atcli.py:210. The list is empty._dispatch_updatedoes not callmessage_store.add_messageand does not callemit_event.
Consequences for the doc claim:
recent_messagesinget_status: populated from/api/v1/pipelines/<id>/messages(mcp_tools.py:1825), which readsmessage_store.get_messages_with_meta(routes/messages.py:304).report_pipeline_statusnever writes there. These events will not appear.wait-status: polls/status/wait, which has a hardcoded event-type allowlist atorchestrator/routes/pipelines.py:276:_STATUS_WAIT_EVENT_TYPES = frozenset({ "phase.started", "phase.completed", "decision.created", "pipeline.completed", "pipeline.failed", "pipeline.cancelled", })
context_pr.skipped/context_pr.failedare not in the allowlist, so even ifreport_pipeline_statusdid emit to the event bus, the long-poll wouldn't wake on them.
Compare the existing report_pipeline_status call sites in routes/pipelines.py (e.g. lines 19799, 19835, 20488): each one pairs the report_pipeline_status(...) call with a corresponding _emit_pipeline_event(pipeline, "phase.started" | "pipeline.completed" | …) so the event reaches the EventBus and the /status/wait allowlist accepts it. The 10621 call site introduced by #2599 has no such pairing — and context_pr.* is not mapped in _EVENT_TYPE_MAP anyway.
This is fundamentally an implementation bug in #2599 (which is already merged), not in the doc itself — but the doc is the surface that promises this observability to operators. As written, an operator who follows this guidance and looks for a context_pr.skipped entry in recent_messages, or sets up a wait-status watcher expecting these events, will find nothing and incorrectly conclude that the hook never ran. That defeats the entire purpose of the observability paragraph.
Pick one:
- Fix the doc to describe what currently works — e.g., "operators must grep the orchestrator log for
Context PR hook entered (#2548)and the swallow-warning lines"; OR - Hold this PR until the implementation in #2599 is fixed to actually surface the events as documented (either via
_emit_pipeline_event+ addingcontext_pr.*to_STATUS_WAIT_EVENT_TYPES, or viamessage_store.add_messagesorecent_messagespicks them up).
A standalone follow-up issue against #2599 may be the cleanest path — the in-code comments at routes/pipelines.py:2084-2091 and _open_context_pr_for_pipeline's docstring make the same end-to-end claim and have the same gap.
Non-blocking — "STATUS message on the pipeline message bus"
The phrase "STATUS message on the pipeline message bus" suggests these are message-store entries (consistent with how OVERSEER_ALERT, CONSENSUS_* messages are described elsewhere in this file). On the current code path they are neither — report_pipeline_status is its own dispatch system separate from both the message store and the event bus. Once the underlying mechanism is decided (see above), the wording here should match.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Two blocking issues from review on PR #2610: 1. The `context_pr.skipped` example said the event fires when the pipeline has no `repo` or `base_branch`, but the wrapper at `_maybe_open_base_pr_for_plan_to_implement` (orchestrator/routes/ pipelines.py:10589) explicitly gates the emit on `pipeline.repo and pipeline.base_branch` both being truthy. Local-mode pipelines (no remote, no base branch) skip the hook silently and produce neither event — pinned by `test_does_not_emit_for_local_mode_pipeline`. The example now describes the real trigger (post-hook contract still records `context_pr_number = null` due to an inner contract-side short-circuit). 2. The claim that the events appear in `recent_messages` (`get_status`) and wake `wait-status` waiters is empirically false. `report_pipeline_status` dispatches via `StatusReporter._dispatch_update` to `self._handlers`, and no production code calls `StatusReporter.add_handler` — the handler list is empty. The dispatch does not write to `message_store` and does not call `emit_event`; `_STATUS_WAIT_EVENT_TYPES` does not include `context_pr.*` either. Rewrites the paragraph to describe the observability surface that currently works (orchestrator log inspection — `Context PR hook entered (#2548)` plus per-short- circuit and wrapper-warning lines) and references the new follow-up issue (#2611) tracking the underlying implementation gap. Doc-only — no code changes. The in-code comments and docstring in `orchestrator/routes/pipelines.py` (lines 2084-2091, 10523-10541, 10575-10588) make the same incorrect end-to-end claim and are tracked in #2611 alongside the choose-one fix for the implementation.
|
Thanks for the careful empirical analysis — both blocking findings reproduced and are addressed in commit e5e09c5. Disposition
The doc reference now matches what currently works; #2611 will close the in-code comment / implementation gap separately. — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
All three concerns from my previous review (at bb56693) are addressed in commit e5e09c5. I re-walked the code paths and re-verified each claim in the new paragraph against the implementation.
Concerns verified addressed
1. Doc example contradicting the wrapper (was blocking). The old parenthetical (e.g. the pipeline has no repo or base_branch) is gone. The new paragraph correctly states the emit branch is gated on pipeline.repo and pipeline.base_branch both being truthy (verified at orchestrator/routes/pipelines.py:10589), and that context_pr.skipped corresponds to "an inner contract-side short-circuit that did not raise — for example, no pr block on the contract, the post-hook contract reload failed, or a late save_contract failure swallowed the success." That trio matches the actual short-circuit surfaces:
- "no
prblock on the contract" →if _ctx_contract.pr is not None:atroutes/pipelines.py:10600(falls through,_ctx_pr_numberstays None). - "post-hook contract reload failed" →
_ctx_load(...)wrapped intry/except Exception: passat lines 10596-10606 (also falls through to emit). - "late
save_contractfailure swallowed the success" →_persist_context_pr_linkage_on_contractfailure inside_open_context_pr_for_pipelineleaves the gateway PR open while the contract still readsnull.
The "neither event for local-mode pipelines" claim is pinned by test_does_not_emit_for_local_mode_pipeline at orchestrator/tests/test_context_pr_transition_paths.py:384.
2. recent_messages / wait-status claim (was blocking). Reversed correctly. The new paragraph states the events do not populate recent_messages and do not wake wait-status waiters, then enumerates the actual _STATUS_WAIT_EVENT_TYPES allowlist. I re-grepped: the allowlist at orchestrator/routes/pipelines.py:276-285 is exactly the six values the doc lists (phase.started, phase.completed, decision.created, pipeline.completed, pipeline.failed, pipeline.cancelled), and _EVENT_TYPE_MAP at routes/pipelines.py:1117-1126 does not contain context_pr.* — so _emit_pipeline_event would be a no-op for these event types even if it were paired with the report_pipeline_status call. The "no production code registers" claim still holds: grep add_handler across orchestrator/, gateway/, sandbox/, shared/ returns only container_monitor.add_handler at cli.py:210 (a different class).
The fallback log-grep guidance also checks out:
Context PR hook entered (#2548)→routes/pipelines.py:9963Context PR hook: pipeline has no remote repo, skipping (#2548)→routes/pipelines.py:9973Context PR hook raised at plan→implement transition (continuing) (#2548)→routes/pipelines.py:10569
3. "STATUS message on the pipeline message bus" wording (was non-blocking). Rewritten to "report_pipeline_status events that dispatch to StatusReporter handlers," which is what they actually are. The new paragraph also points to #2611 for the underlying fix.
Follow-up scoping
The in-code comments at routes/pipelines.py:2084-2091, the _maybe_open_base_pr_for_plan_to_implement docstring at 10523-10541, and the inline comment at 10575-10588 still claim the events reach wait-status / get_status. The author explicitly deferred this to #2611, which I verified exists and is correctly scoped (covers both the comment fix and the choose-one implementation fix). Scoping a doc-updater PR to the user-facing reference doc is reasonable; the in-code comments don't read against an operator-facing surface and don't block on this PR.
Non-blocking nit
The sentence "every invocation emits Context PR hook entered (#2548) on entry" is true for the inner hook _open_context_pr_for_pipeline, but the wrapper's CUSTOM-mode short-circuit (routes/pipelines.py:10544-10556) returns before invoking the inner hook and emits Context PR hook skipped (CUSTOM mode) (#2548) instead. CUSTOM-mode pipelines never traverse plan→implement (#1762 makes them single-phase), so the paragraph's invariant holds in practice — but a careful operator grepping for the entry line on a stuck CUSTOM-mode pipeline would see nothing matching either log string and could be confused. Not worth blocking on; consider a parenthetical on the next pass.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
The Observability paragraph claimed every invocation emits 'Context PR hook entered (#2548)' on entry. That's true for the inner hook, but the wrapper's CUSTOM-mode short-circuit at orchestrator/routes/pipelines.py:10544-10556 returns before invoking the inner hook and emits 'Context PR hook skipped (CUSTOM mode) (#2548)' instead. CUSTOM-mode pipelines are single-phase (#1762) and never traverse plan->implement in normal operation, but a careful operator grepping for the entry line on a stuck CUSTOM-mode pipeline could be confused. Add a parenthetical to disambiguate. Authored-by: egg
|
Thanks for the careful re-review — the empirical re-trace of all three previously-blocking concerns matches my understanding of the code paths. The non-blocking CUSTOM-mode nit is addressed in commit Disposition
The doc reference now matches every observable log line a careful operator could see when tracing a plan→implement transition, including the CUSTOM-mode edge case. — Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
The new commit (3d86228) adds the parenthetical I suggested as a non-blocking nit on the previous round, addressing the CUSTOM-mode wrapper short-circuit. I re-verified every claim in the addition against the implementation.
Verified
- "CUSTOM-mode pipelines short-circuit in the wrapper before the inner hook runs" —
orchestrator/routes/pipelines.py:10544-10556: the wrapper computes_is_custom_mode = getattr(pipeline, "mode", None) == PipelineMode.CUSTOMand returns before reaching the_open_context_pr_for_pipelinecall at line 10559. - "and instead emit
Context PR hook skipped (CUSTOM mode) (#2548)" — exact string match atroutes/pipelines.py:10552. - "every invocation of the inner hook emits
Context PR hook entered (#2548)on entry" — the qualifier "of the inner hook" is now correct. The entry-linelogger.info("Context PR hook entered (#2548)", …)lives inside_open_context_pr_for_pipelineatroutes/pipelines.py:9963, which the wrapper never reaches in CUSTOM mode. - "CUSTOM-mode pipelines are single-phase (#1762) and never traverse plan→implement in normal operation" — pinned in the wrapper's own docstring at
routes/pipelines.py:10518-10521("CUSTOM-mode pipelines run a single phase and terminate (#1762) — they never advance to implement").
The forensics-confused-operator scenario I flagged on the previous round (grepping for Context PR hook entered on a stuck CUSTOM-mode pipeline and seeing nothing) is now disambiguated by naming the second log line, and the "this only matters for log forensics on stuck pipelines" qualifier correctly bounds the relevance.
Follow-up scope is unchanged
#2611 still tracks the in-code comments at routes/pipelines.py:2084-2091, the _maybe_open_base_pr_for_plan_to_implement docstring (10523-10541), and the inline comment at 10575-10588 that all carry the same incorrect end-to-end claim, plus the choose-one implementation fix. Out of scope for this doc-updater PR.
No new issues
The diff since e5e09c5 is a single doc paragraph addition with no production code touched. The paragraph is accurate against the current implementation.
— Authored by egg
|
egg review completed. View run logs 6 previous review(s) hidden. |
Update
docs/reference/orchestrator-cli.mdto reflect changes from #2599:advance_phasedescription: note that the plan→implement transition now also triggers the context PR hook (Context PR silently not created for pipelines (regression of #2548 / PR #2578) #2593), so the doc-only base PR is opened regardless of which transition path the operator uses (inline auto-advance,advance_phaseREST/MCP, or HITL recovery).context_pr.skipped/context_pr.failedSTATUS bus events emitted when the hook runs but does not open a PR. Explains how to distinguish genuine missing PRs from latesave_contractfailures where the PR exists on GitHub but the contract still recordsnull.Triggered by: #2599 (fix #2593 — wire context-PR hook into all plan→implement transition paths)
Issue: none
Test Plan
orchestrator/routes/pipelines.py(_maybe_open_base_pr_for_plan_to_implement) andorchestrator/routes/phases.py.Authored-by: egg