Skip to content

feat(claim): two-phase dispatch — claim before Phase -1, probe sees it (SHM S2) - #1467

Merged
artyhoo merged 2 commits into
stagingfrom
worktree-shm-s2-claim
Aug 18, 2026
Merged

feat(claim): two-phase dispatch — claim before Phase -1, probe sees it (SHM S2)#1467
artyhoo merged 2 commits into
stagingfrom
worktree-shm-s2-claim

Conversation

@artyhoo

@artyhoo artyhoo commented Aug 18, 2026

Copy link
Copy Markdown
Owner

What

S2 «claim» of the skill-harmonization-mechanisms umbrella — spec §5.3 (D-H5, premise P-5). Four parts, all four present; a prose-only diff would have been non-conformant by construction (kickoff trap T-SHM-B).

The race this closes was measured, not imagined: every historical double-dispatch materialised inside the Phase -1 cold-review window. Session A probes clean, opens a cold review; session B probes during that review, also sees clean, dispatches. Neither did anything wrong — there was simply no artefact representing «A intends to take this stage» until A actually dispatched. The claim moves that artefact to the front of the window.

1 — runtime-bridge split (packages/runtime-bridge/src/AifHandoffBackend.ts:183). dispatch() was create+unpause in one atomic call. Split into claim() (POST /tasks paused:true), release() (unpause) and cancelClaim() (DELETE), behind an optional ClaimCapableBackend + supportsClaims() guard (packages/runtime-bridge/src/backend.ts:46) so ManualBackend is not forced to fake a queue. dispatch() is now literally claim+release with the original rollback — all 227 pre-existing runtime-bridge tests pass untouched. New entrypoint packages/runtime-bridge/src/cli/claim.ts:1 exposes the three verbs; unlike dispatch.ts it exits non-zero and never degrades to ManualBackend, because a claim that silently failed is worse than no claim (the next session would probe a lane that looks clean).

2 — /pipeline §6 Step 3 reorder (.claude/skills/pipeline/SKILL.md:425). Claim-create before Phase -1, release on GO, cancel on RED. Detail routed to references/claim-machinery.md + references/stage-gates.md; SKILL.md stays at 599 lines (600 gate).

3 — probe widening (.claude/skills/dispatcher/helpers/probe-inflight.sh:126). Signal 5's jq filter selects only done|verified tasks carrying a branch, so a fresh paused claim was invisible to all five existing signals. Signal 6 matches a paused, unfinished task by slug in title+description — deliberately no claim marker field, because the task title is already the slug and inventing one would be the second status vocabulary P-5 forbids.

4 — orphan expiry. A claim past PROBE_CLAIM_TTL_MIN (default 120min) reads STALE-CLAIM rather than blocking the stage forever (starvation mode TD-F5). Surfaced, never auto-cancelled — an automatic sweep would race the sessions it protects. Unparseable/missing createdAt counts as live: the guard fails toward blocking.

Closes probe P4 (spec §6) — live proof, not the code merge

Kickoff trap T-SHM-A says the signal is not done at merge. Run against the live aif queue on localhost:3009:

1) baseline:      VERDICT: FRESH
2) claim-create:  SIGNAL claim 1 live=1 stale=0 ttl=120min -> VERDICT: CLAIMED
   GET /tasks/<id> -> {"status":"backlog","paused":true,"branchName":null}
3) orphan branch: clock +3h on the REAL createdAt
                  claim: 17d9e14a age=180m stale -> VERDICT: STALE-CLAIM
   (clock +1h -> age=61m live -> CLAIMED — the boundary is real, not asserted)
4) cancel (RED):  cancelled -> GET /tasks/<id> -> HTTP 404
5) after cancel:  VERDICT: FRESH
6) slug control:  a neighbouring slug sees claim 0 -> FRESH (no leakage)

The GO branch is proven live too — including the half that is easiest to hand-wave («and then the coordinator picks it up»). Held the claim for 6s as a control, then released it:

control  t=2,4,6s  {"status":"backlog","paused":true, "costUsd":0}   <- a claim is NOT touched
release            paused: true -> false
after    t=2s      {"status":"backlog","paused":false,"costUsd":0}
         t=4s      {"status":"backlog","paused":false,"costUsd":0}
         t=6s      {"status":"planning","paused":false,"costUsd":0}  <- picked up, for real
cleanup            DELETE -> HTTP 404, costUsd 0, no host branch

A claim therefore genuinely occupies no lane while paused (three coordinator polls ignored it), and release() genuinely hands it over (backlog -> planning within ~6s). Residue, stated because it is the honest cost of proving this: reaching planning made aif create a per-task worktree inside the container before the task was deleted. The worktree has been removed; one empty branch (feature/shm-release-probe-e0f6e8, 0 commits ahead of staging, 0 uncommitted changes) is still in the container — force-deleting a branch is agent-blocked machine-wide, so it needs one maintainer command.

Entry-point coverage, honestly. The claim covers the aif-queue surface; host branches, container branches and PRs stay covered by signals 1/4/2 exactly as before. Verified cwd-independent — the same live claim reported CLAIMED from three unrelated working directories. Measured limit worth recording: each session runs the probe from its own checkout, so a sibling worktree on a pre-merge base reported FRESH against a live claim. Until this lands on staging the guard protects only sessions based on it. All of this is written down in references/claim-machinery.md, not just here.

Own cold review (T19) caught three defects before handoff

All three fixed here, each with a regression test:

  1. The stale count was a substring grep over the whole detail line — so a claim titled demo fix stale refs counted as expired one minute after creation, and the verdict told the operator to cancel a lane somebody was actively holding. The inverse of the starvation mode it was meant to prevent. Counting now reads the fixed field-3 state token; titles are squashed to one line so a multi-line title cannot inflate the count either (packages/core/skills/dispatcher/probe-inflight.test.ts:308).
  2. cancelClaim() returned void, so the CLI printed «lane is free» even when the DELETE failed. Best-effort must not mean unreported: it returns a boolean and the CLI exits non-zero with an honest message.
  3. The live proof then caught fix 2 misfiring on a repeat cancel — a 404 means the claim is already gone, i.e. success. 404 now counts as cancelled, so an idempotent retry stops raising a false alarm (verified live: two consecutive cancels, both exit 0).

Prior-art — the gate overruled the kickoff, and was right

Kickoff §3 sanctioned Prior-art: skipped. The pre-push gate rejected it: cli/claim.ts is 140 LOC under packages/, a capability commit by CLAUDE.md's own definition. The forced context7 pass (3 phrasings, /lee-to/aif-handoff, 2026-08-18) paid for itself twice — it confirmed the design premise («paused tasks are excluded from coordinator selection queries», so a claim genuinely occupies no lane), and it surfaced prior art the design had missed: upstream already claims tasks atomically via lockedBy/lockedUntil with automatic stale-claim release. T16 check: different problem class (mutual exclusion between poll cycles over an existing row vs. our intent registered before any row exists), so not the surface to reuse — but its stale-claim vocabulary is adopted for the orphan branch, with the auto-release half deliberately declined. Recorded as SSOT #258 (ADAPT) per the #125 precedent; #28 keeps its row and gets a Last reviewed bump.

Shipped-surface obligation

The shipped /pipeline now instructs a consumer to run claim.ts, so the vendor drop carries it (packages/runtime-bridge/vendor/src/cli/claim.ts) along with the split backend — the vendor README's own admitting criterion (PR #1432 precedent): vendor what a shipped skill instructs the consumer to run, or the promise is a documented lie. Install baselines regenerated with the diff reviewed first: exactly 4 lines per JS fixture — the SKILL.md hash, the two new reference docs, and .prettierignore (the installer auto-adds shipped docs to its managed ignore block; reproduced against a pristine staging checkout, which is 15/15 clean).

Fidelity verdict

FIDELITY: skipped — no cold fidelity-auditor seat was dispatched for this stage; the acceptance evidence is the author's own T19 cold review (three defects found and fixed, listed above) plus the live RED/GREEN proof against the running aif queue. Dispatching a cold seat before merge remains the operator's call.

Prior-art: prior-art-evaluations.md#258 (verdict ADAPT — the claim reuses the existing aif paused REST primitive rather than adding one; upstream's own lockedBy/lockedUntil claim is a T16 problem-class mismatch, vocabulary adopted for STALE-CLAIM, mechanism not).
Prior-art: prior-art-evaluations.md#28 (verdict DEFER — its «pipeline automation requires machine-readable state without human presence» trigger fired; the capability-specific re-evaluation is #258, per the #125 precedent).

Verification

runtime-bridge          15 files / 256 tests   PASS
packages/core/skills    23 files / 242 tests   PASS
principles              45 files               PASS
install snapshots       15 pass / 0 fail
format-shipped --check  clean     shellcheck   clean
typecheck               EXIT=0    markdownlint 0 errors
pipeline/SKILL.md       599 lines (600 gate)

§1.7 Forward-check applied

The claim protocol is gated, not trusted. Probe verdicts are asserted in packages/core/skills/dispatcher/probe-inflight.test.ts:1 — claim signal, orphan expiry, precedence, and both title-collision regressions — and the backend split in packages/runtime-bridge/test/aif-claim-split.test.ts:1, so the guard cannot regress to bare attention (.claude/rules/attention-is-not-a-mechanism.md:1 §1: a load-bearing check must be a deterministic gate, never «someone will notice»). The STALE-CLAIM branch is deliberately a surfaced state rather than an auto-sweep, because expiry is a decision, and gating a judgment is #gate-where-judgment-needed.

§1.7 Backward-check applied

Every probe consumer was enumerated by grep, not from memory. .claude/skills/dispatcher/SKILL.md:65 was the only surface documenting the signal table and verdict ladder — updated with signal 6 and both new verdicts, and the existing wiring test at packages/core/skills/dispatcher/probe-inflight.test.ts:394 went RED first for exactly that omission before the doc was written. The sibling CLI entrypoint guard at packages/runtime-bridge/test/dispatch-import-no-sideeffect.test.ts:69 was extended to claim.ts, which carries the identical isDirectCliInvocation shape and could regress identically. The shipped vendor drop was re-vendored so the promise made at .claude/skills/pipeline/SKILL.md:433 reaches a consumer. CLAUDE.md:1's pre-dispatch section names the script but enumerates no verdicts, so it needed no edit — surfaced here rather than silently skipped.

Out of scope (surfaced, not actioned)

  • CLAUDE.md's «Pre-dispatch in-flight probe» section could gain a sentence about claims. It enumerates no verdicts today so nothing there is stale, and CLAUDE.md is outside this stage's scope — flagging rather than drive-by editing (PR-strategy incident 2026-05-11, chore(pr-template): add §1.7 forward+backward stubs + Skipped guidance #33).
  • S3 also edits .claude/skills/pipeline/SKILL.md; scopes were kept disjoint (mine is §6 Step 3, theirs is the frontier derivation). No overlap in this diff.

Test added 2 commits August 18, 2026 12:05
S2 of the skill-harmonization-mechanisms umbrella (spec §5.3, D-H5/P-5).
Four parts, all four required — the diff is deliberately not prose-only.

1. Runtime-bridge split (`AifHandoffBackend`): `dispatch()` was create+unpause
   in one atomic call, so a lane became observable only AFTER the work started.
   Split into `claim()` (POST /tasks paused:true), `release()` (unpause) and
   `cancelClaim()` (DELETE), behind an optional `ClaimCapableBackend` interface
   + `supportsClaims()` guard. `dispatch()` is now literally claim+release with
   the original rollback, so the one-shot callers are behaviourally unchanged —
   all 227 pre-existing runtime-bridge tests pass untouched. New CLI entrypoint
   `cli/claim.ts` exposes the three verbs; unlike `dispatch.ts` it exits
   NON-zero and never falls back to ManualBackend, because a claim that
   silently failed is worse than no claim.

2. `/pipeline` §6 Step 3 reordered: claim-create fires BEFORE the Phase -1
   window, release on GO, cancel on RED. Detail routed to
   `references/claim-machinery.md` + `references/stage-gates.md` so SKILL.md
   stays at 599 lines (the 600 gate).

3. `probe-inflight.sh` signal 6 (`claim`): the jq filter for signal 5 selects
   only done/verified tasks carrying a branch, so a fresh paused claim was
   invisible to ALL five existing signals. Matching is by slug in
   title+description — no claim marker field, because a second status
   vocabulary is what P-5 forbids.

4. Orphan expiry: a claim past `PROBE_CLAIM_TTL_MIN` (default 120min) reads
   `STALE-CLAIM` instead of blocking the stage forever (starvation mode TD-F5).
   Surfaced, never auto-cancelled — an automatic sweep would race the sessions
   it protects. Unparseable/missing createdAt counts as live (fails toward
   blocking).

Also: `cli/claim.ts` + the split backend are added to the runtime-bridge vendor
drop. The shipped `/pipeline` now instructs a consumer to run it, and the
vendor README's own admitting criterion (PR #1432 precedent) is «vendor what a
shipped skill instructs the consumer to run» — otherwise the promise is a
documented lie. Install baselines regenerated (`SNAPSHOT_MODE=capture`); the
reviewed delta is exactly 4 lines per JS fixture: the pipeline SKILL.md hash,
the two new reference docs, and `.prettierignore` (the installer auto-adds
shipped docs to its managed ignore block — reproduced and confirmed against a
pristine staging checkout, which is 15/15 clean).

Live proof (probe P4, spec §6) against the running aif queue, not the diff:
FRESH → claim-create → CLAIMED (age=0m live) → cancel → 404 → FRESH; orphan
branch proven on the real task's own createdAt with the clock advanced 3h →
STALE-CLAIM; slug isolation and cwd-independence both confirmed.

Own cold review of the diff before handoff (T19) caught three defects, all fixed
here with regression tests:
  (a) the stale count was `grep -c ' stale '` over the whole detail line, so a
      claim titled "demo fix stale refs" counted as expired one minute after
      creation — the verdict then told the operator to cancel a lane somebody was
      actively holding. Counts now read the fixed field-3 state token, and titles
      are squashed to one line so they cannot inflate the count either.
  (b) `cancelClaim()` returned void, so the CLI printed "lane is free" even when
      the DELETE failed. It returns a boolean now and the CLI exits non-zero with
      an honest message — best-effort must not mean unreported.
  (c) the live proof then showed (b) firing on a REPEAT cancel: a 404 means the
      claim is already gone, i.e. success. 404 is now treated as cancelled, so an
      idempotent retry stops raising a false alarm.

The kickoff §3 sanctioned a `Prior-art: skipped` escape hatch here; the pre-push
gate rejected it, correctly — `cli/claim.ts` is 140 LOC under `packages/`, which
is a capability commit by CLAUDE.md's own definition. The gate won, and the
fresh context7 pass it forced (3 phrasings, `/lee-to/aif-handoff`, 2026-08-18)
paid for itself twice. It CONFIRMED the design premise — «paused tasks are
excluded from coordinator selection queries», so a claim really does occupy no
lane — and it surfaced prior art the design had not accounted for: upstream
already claims tasks atomically via `lockedBy`/`lockedUntil` with automatic
stale-claim release. T16 check says that is a different problem class (mutual
exclusion between poll cycles over an EXISTING row, vs. our intent registered
BEFORE any row exists), so it is not the surface to reuse; its stale-claim
vocabulary IS adopted for the orphan branch, with the auto-release half
deliberately declined — our claim's owner may still be alive. Recorded as SSOT
#258 (ADAPT) following the #125 precedent for re-evaluating a candidate against
a different capability; #28 keeps its row and gets a `Last reviewed` bump.

§1.7: forward-check applied — the new claim protocol is itself gated, not trusted: probe verdicts are asserted in packages/core/skills/dispatcher/probe-inflight.test.ts:1 (claim signal, orphan expiry, precedence) and the backend split in packages/runtime-bridge/test/aif-claim-split.test.ts:1, so the guard cannot silently regress to attention; backward-check sweep — every probe consumer enumerated by grep, not by memory: .claude/skills/dispatcher/SKILL.md:65 was the only surface documenting the signal table + verdict ladder and is updated (signal 6 + CLAIMED/STALE-CLAIM), the sibling CLI entrypoint guard in packages/runtime-bridge/test/dispatch-import-no-sideeffect.test.ts:69 was extended to claim.ts, the shipped vendor drop was re-vendored so the promise in .claude/skills/pipeline/SKILL.md:433 reaches a consumer, and CLAUDE.md's pre-dispatch section enumerates no verdicts so it needed no edit (surfaced, not silently skipped).

Prior-art: prior-art-evaluations.md#258 (verdict ADAPT — the claim reuses the
existing aif `paused` REST primitive rather than adding one; upstream's own
`lockedBy`/`lockedUntil` claim is a T16 problem-class mismatch, vocabulary
adopted for STALE-CLAIM, mechanism not).
Prior-art: prior-art-evaluations.md#28 (verdict DEFER — its «pipeline automation
requires machine-readable state without human presence» trigger fired; the
capability-specific re-evaluation is #258, per the #125 precedent).
@artyhoo
artyhoo merged commit 8b88460 into staging Aug 18, 2026
52 of 54 checks passed
@artyhoo
artyhoo deleted the worktree-shm-s2-claim branch August 18, 2026 09:18
artyhoo added a commit that referenced this pull request Aug 18, 2026
…inal PR #1468) (#1470)

All three build stages of the operator-axis harmonization tail merged: S1 #1464 (§5.2
CONTEXT.md pointer-rule principle test), S2 #1467 (§5.3 two-phase claim machinery), S3
#1468 (§5.4 mechanical dependency frontier + slicing vocabulary). Written by the session
that merged the last stage, per the umbrella closure convention
(docs/meta-factory/operational-conventions.md §1) — `done.md` is the load-bearing
`priority-score.sh` Layer C3 fallback, so without it `/pipeline` keeps offering a closed
umbrella.

Verified live, both consumers:
- `priority-score.sh` → `status=DONE done_pr=1468 basis=done-md`
- `frontier.sh skill-harmonization-mechanisms` → `DONE: S1 S2 S3` / `FRONTIER: (none)`
  (layer 1 answering first — the stage rows themselves carry no merge markers)

Also records what this umbrella deliberately did NOT close: the seven observations S3's two
PR-blind cold seats surfaced outside the ratified stage scopes (the weightiest being that
`/dispatcher` §2.7 and `night-mode` still advance stages without reading the frontier), plus
the parent spec's own open residue (§8 item 1 attended CONTEXT.md run, D-H18 consumer-axis
contour, the ARMED D-H7 counter). The operator routed items 1-7 to a follow-up session on
2026-08-18.

Co-authored-by: Test <test@example.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant