refactor(#313): remove PR-mutation executor, review-state veto, and worker respawn subsystem#320
Conversation
The PR merge/resolve-comments mutation surface added by #241 never had a MergeExecutor/ResolveExecutor implementation: every mutation path 501'd and the only wiring passed nil executors. Remove the whole dormant machinery, keeping the PR observation/persistence path (manager.go ApplyObservation/WritePR) intact — AO still OBSERVES PR state everywhere; only mutation dies. Removed: - service/pr: action_service.go, main_ci_gate.go, errors.go and their tests (LiveMainCIGate existed solely to gate the dormant executor; nothing else imported it). - httpd/controllers: prs.go + prs_test.go and the Merge/ResolveComments DTOs (MergePRResponse, ResolveCommentsRequest/Response, PRIDParam). - api.go PRs dep field, prs controller construction and its Register call. - daemon/pr_wiring.go (buildPRActionService) and its daemon.go call, plus the wiring_test.go tests for it. - OpenAPI: the two /prs/{id}/merge and /prs/{id}/resolve-comments routes, the now-empty "prs" tag, and the regenerated spec + frontend schema.ts. - Dead frontend/mobile callers of the removed endpoints: api-client route templates, the mobile mergePR action chain, and stale comments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1BedAdnAVFqVQTBoVyyp5
#311 (commit e6b1ade) taught the autonomous merge gate to fetch a PR's GitHub review states and refuse the merge on a current-head CHANGES_REQUESTED. Per the operator-confirmed #313 harmony plan, the autonomous check returns to exactly: a clean, SHA-current final-review status plus no current-head merge-park signal. AO's review authority does not live in GitHub review states. Removed evaluateBlockingReviews + VERDICT_REVIEW_STATES + selectHeadPullRequest + flattenReviewPages from final-review-status-core.mjs, and fetchHeadPullRequestReviews + the autonomous blocking-reviews veto from final-review-status.mjs. evaluateAutonomousMergeStatuses (final-review-clean + no merge-park) is unchanged; human mode is untouched; the review-passed status mirror, merge-park semantics, and the ao-native reviews API all remain. Tests pin the restored contract: a CLI integration test asserts autonomous check passes on a clean final-review with a single commit-statuses gh call and never reads /reviews or /pulls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1BedAdnAVFqVQTBoVyyp5
…ack, #313) Per the operator-approved #313 plan: remove the automatic worker respawn/retry subsystem; keep a clear terminal failure notification and require an explicit operator restart. Removed: - observe/trackerintake: the respawn engine — retryDecision and its retry-cap logic, orphaned-PR branch adoption (canAdoptOpenPR, spawnCfg.Branch, postSpawnIntent/AdoptsOpenPR plumbing), workerRetryExhaustedIntent, workerRetryBlockedIntent. The duplicate-PR guard and intake dedup (seenIssueIDs, live-driver semantics from #181/#230) stay intact; a live non-worker session still cannot silence a dead worker's escalation. - domain/tracker.go: DefaultWorkerRespawnMaxRetries and the WithDefaults/IsEnabled/EffectiveMaxRetries/EffectiveRespawnPolicy machinery plus respawn validation. The respawn config FIELD is retained as a tolerated-deprecated no-op (accepted, round-tripped, ignored) because #302's strict decode on project add/set-config would otherwise 400 every stored config that still carries a respawn stanza (including the committed ops/project-config spec). Test pins the tolerance. - ports/notifications.go: RetryCount/RetryLimit/AdoptsOpenPR intent fields (TerminalFailureReason stays and now rides worker_died_unfinished). - domain/notification.go: the worker_retry_exhausted type (const, Valid, WithInferredSubject) is gone. Reworked — worker_died_unfinished is now the terminal death escalation: - It carries the dead session's terminal failure reason (#318 provenance) and names an orphaned open PR as a fact; the body demands an explicit operator restart, with no respawn/retry language. - It takes worker_retry_exhausted's operator-attention slot: projection type list, attention item metadata, DTO kind enum, notifier MENTION_KINDS, icon and tone maps. It leaves the notifier's INFORMATIONAL stream set so it is never double-posted. - Store dedupe (subject+type+body, survives read) keeps the per-tick re-emission from spamming; a new real-store test pins one notification across polls and no resurrection after acknowledgement. Legacy data degrade (no migration needed): existing unread worker_retry_exhausted rows simply drop out of the type-filtered projection query and become invisible; the DB CHECK constraints in applied migrations keep tolerating the value; the Slack notifier keeps only the reconcile-away migration mapping for legacy needs-response records. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1BedAdnAVFqVQTBoVyyp5
There was a problem hiding this comment.
Pull request overview
Removes three dormant/accidental subsystems to simplify AO’s operational surface area: PR mutation endpoints that always returned 501, an autonomous merge veto based on GitHub review state, and the automatic worker respawn/retry machinery—while preserving PR observation and operator-attention escalation.
Changes:
- Removed the PR mutation API surface (backend routes/services + generated frontend/mobile clients).
- Simplified autonomous merge eligibility checks to rely only on SHA-current
final-reviewstatus plus absence ofmerge-park(no GitHub review-state reads). - Removed tracker-intake worker respawn/retry behavior and standardized on a single terminal
worker_died_unfinishedescalation with durable dedupe.
Reviewed changes
Copilot reviewed 48 out of 49 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/mobile/lib/store.tsx | Drops mobile merge action wiring now that PR mutation endpoints are removed. |
| packages/mobile/lib/api.ts | Removes mergePR API call targeting removed backend route. |
| ops/what-needs-me.mjs | Updates icon mapping to use worker_died_unfinished instead of retry exhaustion. |
| ops/final-review-status.test.mjs | Replaces blocking-review tests with a seam test ensuring autonomous mode only reads commit statuses. |
| ops/final-review-status.mjs | Removes review/veto fetch path from autonomous status check logic. |
| ops/final-review-status-core.mjs | Deletes GitHub review-state evaluator/helpers used for autonomous veto. |
| ops/attention-core.mjs | Updates attention-kind icons to match new terminal worker-death escalation. |
| ops/ao-slack-notifier.test.mjs | Aligns notifier behavior/tests with projection-driven worker_died_unfinished attention. |
| ops/ao-slack-notifier.mjs | Removes worker_died_unfinished from informational stream and treats it as projection attention/@mention kind. |
| frontend/src/renderer/lib/api-client.ts | Removes now-invalid PR mutation route templates. |
| frontend/src/renderer/lib/api-client.test.ts | Drops normalization expectation for removed PR mutation endpoint. |
| frontend/src/renderer/components/PullRequestsPage.tsx | Updates UI copy to reflect PR board is read-only and mutation endpoints are gone. |
| frontend/src/renderer/components/PullRequestsPage.test.tsx | Updates test commentary/assertions to match removed PR mutation API. |
| frontend/src/renderer/components/OperatorAttentionPage.tsx | Updates tone mapping for worker terminal escalation kind rename. |
| frontend/src/api/schema.ts | Regenerates API schema removing PR mutation ops and deprecating trackerIntake.respawn as ignored compatibility field. |
| docs/cli/README.md | Removes mention of PR actions that no longer exist. |
| docs/architecture.md | Updates intake/worker lifecycle description to reflect no automatic respawn. |
| backend/internal/storage/sqlite/store/notification_store.go | Restricts “dedupe survives read” behavior to worker_died_unfinished only. |
| backend/internal/storage/sqlite/store/notification_store_test.go | Updates terminal dedupe test cases to remove retry-exhausted type. |
| backend/internal/storage/sqlite/queries/notifications.sql | Updates worker-terminal dedupe query to only match worker_died_unfinished. |
| backend/internal/storage/sqlite/gen/notifications.sql.go | Regenerates sqlc output for updated notifications query. |
| backend/internal/service/pr/main_ci_gate.go | Deletes unused PR main-CI gate (part of removed PR mutation subsystem). |
| backend/internal/service/pr/main_ci_gate_test.go | Deletes tests for removed main-CI gate. |
| backend/internal/service/pr/errors.go | Deletes PR action sentinel errors (subsystem removed). |
| backend/internal/service/pr/action_service.go | Deletes PR action service/contracts (subsystem removed). |
| backend/internal/service/pr/action_service_test.go | Deletes tests for removed PR action service. |
| backend/internal/service/notification/service_test.go | Updates notification service test data to use worker_died_unfinished. |
| backend/internal/service/attention/service.go | Updates operator-attention mapping to treat worker_died_unfinished as the terminal operator-attention notification. |
| backend/internal/service/attention/attention_parity_test.go | Updates parity tests for terminal worker death attention semantics and IDs. |
| backend/internal/ports/notifications.go | Removes retry/respawn enrichment fields from NotificationIntent, keeps terminal failure provenance. |
| backend/internal/observe/trackerintake/observer.go | Removes respawn decision tree; emits one terminal escalation and never spawns replacement workers. |
| backend/internal/observe/trackerintake/observer_test.go | Replaces respawn tests with terminal escalation + dedupe behavior tests. |
| backend/internal/notify/manager_test.go | Updates notification copy/tests to demand explicit operator restart and to name orphaned PRs as facts. |
| backend/internal/notify/enrich.go | Removes retry-exhausted/blocked copy paths; standardizes worker death body text and failure-point suffix. |
| backend/internal/httpd/controllers/prs.go | Deletes PR action controller and routes. |
| backend/internal/httpd/controllers/prs_test.go | Deletes tests for removed PR action routes. |
| backend/internal/httpd/controllers/notifications_test.go | Updates stream notification test fixture to use worker_died_unfinished. |
| backend/internal/httpd/controllers/dto.go | Removes PR action DTOs; updates operator attention kind enum for worker_died_unfinished. |
| backend/internal/httpd/controllers/attention_test.go | Updates attention API tests to expect worker_died_unfinished and exclude informational merged notifications. |
| backend/internal/httpd/apispec/specgen/build.go | Removes prs OpenAPI tag and PR action operations from spec generation. |
| backend/internal/httpd/apispec/openapi.yaml | Regenerates OpenAPI removing PR mutation paths/schemas and marking respawn config deprecated/ignored. |
| backend/internal/httpd/api.go | Removes PR controller wiring from API deps/router registration. |
| backend/internal/domain/tracker.go | Deprecates/ignores trackerIntake.respawn policy while keeping strict decode compatibility. |
| backend/internal/domain/tracker_test.go | Adds strict-decode test ensuring legacy respawn payloads still validate/round-trip. |
| backend/internal/domain/notification.go | Removes worker_retry_exhausted notification type; reclassifies worker_died_unfinished as terminal escalation. |
| backend/internal/daemon/wiring_test.go | Removes PR action service wiring tests. |
| backend/internal/daemon/pr_wiring.go | Deletes PR action service wiring helper. |
| backend/internal/daemon/daemon.go | Removes PR action service from daemon API deps. |
| backend/internal/cli/project.go | Retains CLI config shape for respawn as compatibility-only/deprecated field. |
Files not reviewed (1)
- backend/internal/storage/sqlite/gen/notifications.sql.go: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…nd-trip test; PR-guard comment Review findings on PR #320 (head d5aa978): 1. [MEDIUM] The terminal-death suppression treated ANY historical merged PR as issue completion before considering the latest dead worker, so a reopened issue — or a later worker dying after an earlier worker's PR merged — had its death silenced. dispatchDecision now computes the latest terminated worker first, and inspectHandledPRs counts a merged PR as completion only when it belongs to that worker or postdates its chronology (mergedPRIsCurrent). Tests pin all three shapes: latest worker's own merged PR suppresses; an earlier worker's stale merged PR no longer silences a later death; a posthumous merge of an earlier PR still suppresses. 2. [LOW] The respawn compat test only strict-decoded into the struct. Added TestManager_SetConfigRoundTripsLegacyRespawnStanza: the exact legacy stanza strict-decodes (the #302 API write path), saves through Service.SetConfig into a real sqlite store, reads back, re-encodes byte-identically, and has zero behavioral effect on the effective intake config. 3. (Copilot thread) The Store.ListOpenPRs comment claimed intake skips issues with an open PR "even when the owning session row is gone", overstating seenIssueIDs, which pins only live-driven PRs. Comment now states the real contract: live driver pins; terminated/missing owner routes to the terminal death escalation (#230). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1BedAdnAVFqVQTBoVyyp5
1. mergedPRIsCurrent requires BOTH timestamps for the chronology comparison: a latest worker with no recorded chronology (zero UpdatedAt/CreatedAt) no longer lets an earlier merged PR's nonzero timestamp silence its death escalation. Ownership by the latest worker still counts independently. Unit test pins both sides. 2. The respawn round-trip test compares the re-encoded stanza structurally (unmarshal + reflect.DeepEqual against the decoded original fragment) instead of strings.Contains. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1BedAdnAVFqVQTBoVyyp5
|
Final review verdict for head |
Implements the operator-approved #313 harmony plan: three accidental subsystems removed, in three commits. Net -2,747 / +332 lines across the branch.
A. Dormant PR-mutation/merge executor (#241 rollback) —
cd89d199d, -1,348/+9POST /api/v1/prs/{id}/mergeand/prs/{id}/resolve-commentsnever worked: no MergeExecutor/ResolveExecutor implementation existed anywhere and the only wiring passed nil executors, so every mutation path answered 501. Removed the whole chain —service/praction service + LiveMainCIGate + error sentinels, theprscontroller and DTOs, api.go/daemon wiring, the two OpenAPI routes and the now-emptyprstag, plus dead frontend route templates and an unreachable mobilemergePRaction chain. AO still observes PR state everywhere (service/pr/manager.goApplyObservation/WritePR untouched); only mutation died.B. #311 review-state veto (review-authority collapse) —
fd5e9baf1, -381/+46Removed
evaluateBlockingReviews/selectHeadPullRequest/flattenReviewPages/VERDICT_REVIEW_STATESfromops/final-review-status-core.mjsandfetchHeadPullRequestReviews+ the autonomous blocking-reviews veto fromops/final-review-status.mjs.Review-authority statement: autonomous merge eligibility is exactly a clean, SHA-current
final-reviewcommit status plus the absence of a current-headmerge-park(human-only) signal.review-passedremains a derived mirror offinal-review; the AO-native/sessions/{id}/reviewsAPI remains visibility-only. Autonomous mode does not consult GitHub review states — a new CLI test pins that the autonomous check makes a single commit-statuses read and never touches/reviewsor/pulls. Human mode is untouched.C. Automatic worker respawn/retry subsystem (#210/#231/#243 rollback) —
d5aa978a3, -782/+277Tracker intake never launches replacement workers any more. A worker that dies with unfinished work emits one terminal
worker_died_unfinishedescalation carrying the session's terminal failure reason (#318 provenance) and, when one exists, the orphaned open PR as a fact — then waits for an explicit operator restart. Removed: retryDecision + retry caps, orphaned-PR branch adoption,worker_retry_exhausted(type, enrichment, intent fields RetryCount/RetryLimit/AdoptsOpenPR). Kept intact: the duplicate-PR guard and intake dedup (#181/#230 live-driver semantics — a dead worker's open PR still escalates instead of going silent; a live non-worker session cannot silence it).worker_died_unfinishedtakes the vacated operator-attention slot (projection type list, attention metadata, DTO kind enum, Slack MENTION_KINDS, icons/tones) and leaves the notifier's informational stream so it is never double-posted. Store-level dedupe (subject+type+body, survives read) keeps the once-per-tick re-emission from spamming — a real-store test pins one row across polls and no resurrection after acknowledgement.worker_retry_exhausteddegrade note (no migration needed)Existing unread
worker_retry_exhaustedrows in a live DB simply become invisible: the attention projection reads by an explicit type list, so removing the type filters the rows out at query time; nothing reads or validates them afterwards. The DB CHECK constraints in already-applied migrations still allow the value, so legacy rows keep round-tripping harmlessly. The Slack notifier keeps only its state-migration mapping for the kind, so any legacy needs-response record reconciles away on the next projection poll instead of lingering. Mark-all-read now clears such rows (the type left the exclusion list), which is the desired end state for a retired escalation.Config-compat decision
#302 added strict JSON decoding (
DisallowUnknownFields) on project add/set-config, and stored configs — including the committed specops/project-config/agent-orchestrator.json— carrytrackerIntake.respawn: {disabled, maxRetries}. Dropping the field would therefore 400 every re-apply of an existing config. Chosen path (the smaller one, matching the existing deprecatedlabels/excludeLabelspattern): keeprespawnas a tolerated-deprecated no-op field — decoded, round-tripped, markeddeprecatedin the OpenAPI schema, and completely ignored by intake. A domain test pins that a strict decode + Validate of a legacy respawn payload still succeeds. The spec file stays unchanged (soproject-config checkshows no drift); removing the stanza from spec + live config later is an optional operator cleanup, not required by this change.Gates
npm run ci:backend— exit 0 (go build, go vet,go test -race ./...all ok, golangci-lint 0 issues)npm run test:ops— 464/464 passnpm ciinstall, vitest 717/717 pass,tsc --noEmitclean,build:webok (schema.ts regenerated)BASE_REF=origin/main npm run format:check— cleango generate(specgen drift guardTestBuild_MatchesEmbeddedgreen),schema.tsregenerated vianpm run api:tsMergePR|ResolveComments|resolve-comments|evaluateBlockingReviews|fetchHeadPullRequestReviewsand for the retry machinery (retryDecision|workerRetryExhaustedIntent|EffectiveRespawnPolicy|AdoptsOpenPR|RetryCount|RetryLimit|WorkerRetryExhausted); remainingworker_retry_exhaustedstrings live only in immutable applied migrations, a migration-tolerance test fixture, and the notifier's reconcile-away mapping — all intentional.Refs #313
🤖 Generated with Claude Code
https://claude.ai/code/session_01K1BedAdnAVFqVQTBoVyyp5
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.