chore/governs-event-contract-extension — Two New Event Types for the Autonomous Ops Release Gates
115: Two New Event Types for the Autonomous Ops Release Gates
Broader goal (previous)
By the landing of the Road to Autonomous Operations strategy and its program-design spec (PR #915, 2026-08-12), the Autonomous Ops Lite milestone (target 2026-08-16) had two of its five Week-1 items done — cc-videoreframing's 8-role identity provisioning, and the roadmap-governance strategy resolution. Three remained: choosing a real synlynk goal and running the autopilot-lite loop manually, shipping v0.14.0, and measuring attribution friction from that loop run. The program-design spec named two concrete release gates the October 1 preview depends on: "100% of terminal jobs have cost data or explicit cost_missing status" and "no unreviewed autonomous merge." Verifying either meant hand-querying state.db — there was no event-observable surface for job settlement outcomes or PR review activity.
Why this PR
The GOVERNS event bus (synlynk/events.py, shipped via PR #817) already wired 4 event types — pr_merged, story_done, spec_or_plan_committed, cron_heartbeat — but none of them touched job dispatch outcomes or review submissions, the two things the release gates actually need to watch. This chore closes that gap with a small, deliberately scoped design: two new event types plus a read-only CLI to inspect them, built on the existing events table with zero schema changes.
job_terminal had to be synchronous — _reconcile_daemon_jobs() (the GTV reconcile loop) already determines a job's true terminal status and calls _ensure_daemon_job_cost_entry() at two settlement paths, so the event rides that existing hook rather than introducing a new poll. review_submitted couldn't be — gh pr review is a raw shell command invoked directly by dispatched agents, with no synlynk wrapper to hook into — so it had to be scan-detected, extending the existing pr_merged scan pattern in scan_local_events() with a gh pr view --json reviews call per merged PR, deduplicated by (reviewer_login, submitted_at) since reviews have no monotonic checkpoint the way the merged-PR list does.
One correction surfaced between design and plan: the approved spec's stated dedup key referenced submitted_at, a field its own payload example omitted. Rather than reopening the already-approved spec, the plan added submitted_at to the payload with an explicit rationale note — the dedup key needed a field that existed to compute against.
What shipped
Execution followed brainstorming → writing-plans → subagent-driven-development, with all three tasks dispatched to Codex (synlynk dispatch codex) per this repo's Claude=PM/review-only role split, one at a time in strict order since Tasks 2 and 3 both touch synlynk/events.py.
synlynk/jobs.py—_reconcile_daemon_jobs()'sSELECTand row-unpacking now carrydispatch_context; both settlement paths (preferred-summaryandguaranteed) capture_ensure_daemon_job_cost_entry()'s own return value ascost_recordedand emitjob_terminalwith{job_id, status, cost_recorded, dispatch_context}immediately after. Theexcept-block's defensive cost-entry call was deliberately left unmodified — the guaranteed call always re-runs right after it, so emitting there too would double-fire the event.synlynk/events.py— three new helpers:_reviewer_role_from_login()(regex-matches thesynlynk-<repo-slug>-<role>[bot]GitHub App login pattern,Nonefor non-matching logins),_existing_review_submitted_keys()(payload-content dedup query, since reviews have no id-based checkpoint), and_scan_pr_reviews()(thegh pr view --json reviewscall plus emission), wired into the existingpr_mergedloop inscan_local_events().synlynk events tail [--type TYPE] [--limit N]— new read-only CLI command,cmd_events_tail()inevents.pyplus a subparser/dispatch block incli.pymirroring the existingidentitycommand exactly. Sincesynlynk.eventsisn't re-exported throughsynlynk/__init__.py(unlike most other command modules), the CLI wiring uses a directfrom synlynk.events import cmd_events_tailimport rather than the giant re-export block.- Tests — 7 new tests across
tests/test_jobs.py(3, covering bothcost_recordedbranches plus the preferred-summary path) andtests/test_events.py(6, covering role derivation,Nonefor non-matching logins, no-duplicate-on-rescan, and--type/--limitfiltering), plus one existing test (test_scan_local_events_emits_pr_merged_from_gh_output) updated from a single sharedmock_run.return_valueto aside_effectlist, since the newgh pr viewcall per merged PR broke the old single-mock assumption.
Task 3's implementer also caught a real regression the plan hadn't anticipated: registering events tail in synlynk/taxonomy.py's COMMAND_TAXONOMY (required — every leaf CLI command needs an entry) left docs/reference/commands.md stale against its own generator-parity test. That got fixed as a small follow-up commit, running the existing scripts/generate_command_docs.py rather than hand-editing the generated file. The same regeneration also touched GEMINI.md as an unrelated side effect — the known, still-open stale-revert bug (#884/#899) — which was discarded rather than committed.
All three tasks passed two-stage review (spec compliance, then code quality) with no fix loops needed; diffs matched the plan's given code exactly. Final suite: 1879 passed, 2 skipped.
New goalpost
With job_terminal and review_submitted now event-observable, synlynk events tail gives this week's manual autopilot-lite loop run (Week-1 item 3) something concrete to watch without querying state.db directly. No consumer subscribes to either new event type yet — that stays Week 2's TPM/session MVP scope, per the program-design spec. The next steps on the Autonomous Ops Lite critical path are unchanged by this PR: pick a real synlynk goal and run the loop manually across synlynk + cc-videoreframing, ship v0.14.0 (this event-contract extension is one of its named line items), and measure the friction that run surfaces.