fix(rungate): resume arm always binds a run epoch; unknown --run-epoch gets a named refusal (change 0463) - #345
Conversation
e7f9063 to
7007a61
Compare
Docket-Plan-Path: docs/superpowers/plans/2026-09-27-resume-gate-armed-line-is-ambiguous-when-no-epoch-exists-dis.md
…ch-context> (change 0463)
…lid-request (change 0463)
…wn-run-epoch (change 0463)
…launch (change 0463)
…cle test drives the real cancel path (change 0463, review fix) BLOCKER: the epoch an epochless `gate-before --resume` mints binds the change and stays active, but runCancel required a confirmed claim binding, which a resume arm never gets (change.claim requires proposed). The next resume refused resume-active-run with a `run cancel` remedy that always refused claim-unconfirmed: a permanent wedge. runCancel now accepts the resume-verified shape resolveGateOwnership already exempts (AttributedID set, no claim-binding file at all), still refusing claim-mismatch against the epoch's change and claim-unconfirmed when an unconfirmed reservation exists. This also makes resume replacement epochs cancellable. IMPORTANT: TestEpochlessResumeEpochJoinsCancelCycle no longer forces the epoch cancelled with a direct epochCAS; it cancels through runCancel with the arm's key and epoch, asserts `cancelled`, arms the replacement, and cancels that too. TestRunCancelResumeAuthorityFailsClosed pins the fail-closed edges.
…efore launch (change 0463, review fix) Review finding: the agent.enter epoch preflight ran only when both --run-gate-key and --run-epoch were given, but AGENTS.md threads only --run-epoch into agent.enter. A lone --run-epoch - including the 0382 misroute where the dispatch context is passed as the epoch - skipped CheckRunEpochLinkage and launched unlinked with no warning. A lone --run-epoch now runs the same resolvability check prepare-scope uses (runEpochLocator / findEpochDirByID) and refuses unknown-run-epoch before anything is spawned; an existing lone epoch still proceeds.
…inting (change 0463, review fix) Review finding: RunGateBefore step (6a) resume mint binds ChangeID+Worktree without checking whether another live epoch already owns the verified worktree. Two live owners make findEpochByWorktree return ErrEpochOwnerAmbiguous, so admitWorkflowMutation refuses every fenced mutation (PR publish, workspace publish) in that worktree. Step (4b) now resolves the worktree's current owner with findEpochByWorktree before any scope or record is minted: an active/completing owner naming no or another change refuses resume-active-run with its locator; an ambiguous/unreadable owner set refuses resume-epoch-unreadable; a fenced owner does not block. The decision-4 comment and TestConcurrentEpochlessResumeEpochsFailSafe now state the residual race window and the run.cancel recovery.
…t.TempDir fixture (change 0463, review fix)
…re reserving (change 0463, post-review) A claim under a resume-attributed gate context left an unconfirmed reservation (the resumed change is not proposed) or a confirmed binding for another change; either made run.cancel refuse the resume epoch (claim-unconfirmed or claim-mismatch), bringing back the wedge the resume-verified cancel authority removed. ChangeClaim now refuses gate-context-conflict before ReserveGateClaim. The resume-verified shape is one predicate, GateRecord.resumeAttributed, shared by ChangeClaim, runCancel, and resolveGateOwnership.
…-arm remedy (change 0463, post-review) Concurrent epochless resume arms of one change each passed the no-prior-epoch check and each minted and bound a live epoch (12 of 12 concurrent arms armed in test). A per-change resume lock, taken once the resume id is verified and held to the end of the arm, serializes check and bind; losing arms refuse resume-active-run. It lives under <git-common-dir>/docket/rungate-resume so the gate-key scanners never see it. A resume arm binds its epoch when armed, so an undispatched earlier arm refuses the next resume. Nothing records whether an agent uses an epoch, so the refusal now names both remedies explicitly (cancel if never dispatched or exited, gate-verdict if still running); edge-paths.md says the same.
…pus (change 0463, post-rebase repair) The rebase onto change 0465 landed 0463's tests as the pre-0465 layout left them: in the DEFAULT internal/app corpus, which 0465's no-real-git guard (internal/app/nogit_guard_test.go) forbids from starting real git, and under names with no TestIntegration prefix inside tagged files, which tests/test_go_integration_contract.sh checks (2)/(4) reject. Applies 0465's own partition treatment to each of 0463's additions: - TestCheckRunEpochLinkage (real git via newGateRepo, helper isEpochKind is integration-only) moves from rungate_epoch_refusal_test.go into rungate_epoch_integration_test.go as TestIntegrationGateEpochCheckRunEpochLinkage. The two pure classifier tests stay in the default corpus. - rungate_epochless_resume_e2e_test.go (real git via newWorkingRepo, helper resumeInspectService is integration-only) becomes rungate_epochless_resume_e2e_integration_test.go behind //go:build integration, its test renamed TestIntegrationGateArmEpochlessResumeEndToEnd0382. - The two gate-context claim tests move from change_claim_test.go into change_claim_integration_test.go under the TestIntegrationRecordOps prefix. - The ten tests 0463 added to the rungate_before* integration files gain the shard prefix their runner selects: TestIntegrationGateArm… for the resume-arm family, TestIntegrationGateCancel… for the runCancel authority test, and TestRaceIntegrationAppConcurrency… for the goroutine race over concurrent epochless resume arms. No assertion is weakened and no test is dropped; every moved body is byte-identical apart from its name. The results file's pointer follows the renamed test and file.
7007a61 to
ad25610
Compare
|
Finalize rebased PR #345 onto main baa0fb5 (change 0465). One conflict in internal/app/change_claim_test.go was resolved by docket-rebase-resolver. The rebased suite went red: change 0465 had moved the real-git internal/app tests behind //go:build integration with TestIntegration*/TestRaceIntegration* prefixes, so 0463s new tests did not compile (undefined isEpochKind, resumeInspectService) and broke the integration contract. docket-integration-repair added commit ad25610, which only moves and renames tests (8 files, +127/-127, test bodies unchanged). One judgment call: rungate_epoch_refusal_test.go was split, and its two pure tests stay in the default corpus. It also retargeted one test reference in the results doc. Suite at ad25610: SUITE files=66 passed=66 failed=0. The repaired head is published to the PR. |
Summary
Resuming a change whose earlier run was never armed printed
gate-armed <key> <dispatch-context>, a two-token line. Parents read the dispatch context as the epoch, the gate refused with a genericinvalid-request, and the resumed run finished with no epoch linkage (change 0382). This PR:run gate-before --resume, so the armed line is alwaysgate-armed <key> <epoch> <dispatch-context>--run-epochrefuse withunknown-run-epochongate drive start,gate drive prepare-scope(existence check only) andagent enter(checked before launch, including a lone--run-epoch)run cancelaccept the resume-verified owner (review blocker; without it the new epoch could never be cancelled and every later resume was stuck). Needs design sign-off against ADR-0111.resume-active-runwhen a live epoch already owns the worktreeResults: docs/results/2026-09-27-resume-gate-armed-line-is-ambiguous-when-no-epoch-exists-dis-results.md
Review
Rung:
docket-review-deep. Task 1 routed premium, and the diff is over 1500 lines.claim-unconfirmed), so later resumes wedgerun cancelagent enterskipped the preflight when--run-epochwas passed alonet.TempDir()calls for the repoguard fixture)Follow-up reported for human triage: a check-after-bind re-scan to close the remaining concurrent-resume race window.
command: go run ./cmd/docket development test
result: green
head_sha: ad25610
ran_at: 2026-09-28T12:32:23Z