Skip to content

feat: recover interrupted dispatches in pickup and handoff - #20

Merged
vunm-dev merged 4 commits into
release/v0.5.0from
feat/pdn-0004-s5-pickup-handoff
Sep 25, 2026
Merged

vunm-dev merged 4 commits into
release/v0.5.0from
feat/pdn-0004-s5-pickup-handoff

Conversation

@vunm-dev

Copy link
Copy Markdown
Member

PDN-0004, slice S5 (pickup and handoff recovery). Design: docs/design/PDN-0004-v05-acceptance-recovery.md §13, §21–§23, §26.2 S5. Issue #12. It builds on S4 (#18): classes are carried out through dispatch Reconcile.

What changes

  • passdown-pickup:
    • Reads the attempt store read-only in every plan repository: list --unresolved, claims, and probe for C2/C3/C8.
    • New Recovery classes table (C1–C11) with ownership risk and one proposed action each. C9 is computed from session, age and the handoff's open_attempts.
    • Reports a handoff attempt that is missing from the store.
    • The completion check follows the S4 dispatch rule: an unresolved attempt means not accepted, and the latest Dispatched: line decides as a v0.5, host or legacy line (legacy only without receipts).
    • Mutating helper commands are forbidden by name, and pickup never proposes a retry.
  • passdown-handoff:
    • Optional open_attempts frontmatter key, and every open attempt named in Caveats / traps with class, location and ownership risk.
    • Never resolves an attempt, and never ticks a task that has one.
    • Applies the same accepted-line rule.
  • Both skills say "attempt store not readable: jq missing" instead of guessing.
  • Example workspace: a second handoff log with one open attempt. The example ships no store, so pickup there shows the missing-store case.
  • Docs: docs/SMOKE_TEST.md gains items for pickup and handoff against a live attempt. README, CHANGELOG and the S5 implementation notes are updated.

Acceptance check (§26.2: "pickup classification uses the same class names the skill defines")

New harness scenario tests/interrupt.sh PICKUP:

  1. The class names in the helper's CLASS_JQ plus the reference pickup's C9 must equal the rows of the pickup skill's table and of dispatch Reconcile.
  2. Every verdict word the reference pickup prints (accepted, unaccepted, unconfirmed, inconsistent) must be defined in the skill.
  3. With a live writer (C3) and a handoff naming an attempt that is not in the store, pickup must leave every file under docs/, src/ and the attempt store byte-identical.

Mutation-checked by hand: dropping | C7 from the pickup skill, dropping | C10 from Reconcile, or letting pickup append to the plan each fails the scenario.

ref-host pickup now reads open_attempts from a real handoff log's frontmatter (F16 was updated to write one).

Review focus

  1. Pickup's class table vs §13.2 and vs dispatch Reconcile: is any proposed action stronger than "propose" (pickup must stay read-only), or weaker than Reconcile?
  2. Completion-authority wording in pickup and handoff vs the S4 dispatch rule (feat: run delegated dispatch through the v0.5 attempt protocol #18 round 1, finding chore: prepare passdown for public marketplace #1).
  3. The C9 threshold: cards have no budget field (§18), so the skill falls back to one hour when none is recorded. Is that acceptable, or should the budget be recorded in the receipt?
  4. The example log: showing the missing-store case in examples/basic-workspace versus keeping the example attempt-free.

Tests (local, macOS, bash 3.2)

  • tests/interrupt.sh: 35/35 (PICKUP added).
  • tests/skills.sh: 142. tests/docs.sh: 32. tests/openspec-apply.sh: 6. tests/install.sh: 7. tests/version.sh: 4.
  • shellcheck, sync-bundled --check and validate-plugin are clean.

🤖 Generated with Claude Code

vunm-dev and others added 2 commits September 25, 2026 10:41
PDN-0004 slice S5:

- passdown-pickup reads the attempt store read-only (list --unresolved,
  claims, probe) in every plan repository. It classifies each unresolved
  attempt C1-C11 with its ownership risk and one proposed action, to be
  carried out later through dispatch Reconcile, and reports a handoff
  attempt missing from the store. It never writes a receipt, claim, plan
  or log, and never proposes retrying an uncertain attempt.
- Its completion check follows the dispatch rule: a task with an
  unresolved attempt is never accepted, the latest Dispatched: line
  decides, and a legacy line counts only for a task with no receipts.
- passdown-handoff lists unresolved attempts in an optional open_attempts
  frontmatter key and names them in Caveats / traps. It never resolves an
  attempt and never ticks a task that has one.
- Both skills report a missing jq as "attempt store not readable" instead
  of guessing.

The example workspace adds a handoff with an open attempt, SMOKE_TEST
covers both skills against a live attempt, and tests/skills.sh and
tests/docs.sh lint the new rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reference pickup reads open_attempts from a handoff log's frontmatter
(F16 now writes a real log) and reports an open attempt that has no
receipt as inconsistent.

New scenario PICKUP, the S5 acceptance check:
- the class names used by the helper's CLASS_JQ and the reference pickup
  must equal the rows of the pickup skill's class table and of dispatch
  Reconcile;
- every verdict the reference pickup prints is a term the skill defines;
- with a live writer and a handoff naming a missing attempt, pickup
  leaves every file under docs/, src/ and the attempt store byte-identical.
Dropping a class row from either skill, or letting pickup write, fails it.

The design records the S5 implementation notes; CHANGELOG the slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@vunm-dev vunm-dev left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two recovery issues need adjustment before merging. Validation: 142 skill checks, 32 documentation checks, and interruption scenarios PICKUP and F16 passed. The interruption scenarios required execution outside the sandbox to inspect processes correctly.

written: run `passdown-attempt --json list --unresolved` (the helper in
this skill's `scripts/` directory, with `--store <attempt_dir>` when that
key is set) in each repository that holds a plan this session touched,
and list every ID it prints. Omit the key when there are none. The next

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve unresolved handoff IDs when their store is missing

The helper returns an empty array successfully when the store does not exist, so rebuilding open_attempts solely from list --unresolved and omitting the key on an empty result loses previously known attempts. For example, pickup reports an ID from the previous handoff as missing with ownership risk, but the next handoff drops that ID even though nobody has confirmed the worker stopped. Subsequent pickup uses the latest handoff and loses that recovery signal. Carry forward previously recorded IDs whose ownership remains unresolved, including missing receipts, and distinguish an absent/unreadable store from a verified empty store. Please cover pickup followed by handoff with a missing store.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3029311 (skill and helper) and 32b0ff6 (harness).

  • passdown-attempt list now warns "absent, not empty" when the store directory does not exist (it still prints []), so the two cases can be told apart.
  • Handoff builds open_attempts from two sources: the store's unresolved attempts, plus every ID from the previous handoff whose receipt does not show it resolved. An ID with no receipt stays listed as # receipt missing until the owner confirms no worker remains. An absent store is never evidence that nothing is open. When jq is missing, the previous IDs are carried forward unchanged.
  • Pickup treats the handoff's IDs as missing when the store is absent.
  • Covered by the new scenario HANDOFF:
    • With a store: the live attempt is listed, the receipt-less ID is carried forward, and the resolved ID is dropped.
    • With no store at all, pickup and then handoff: pickup reports the missing attempt and sees the absent store, handoff keeps the ID, and neither one creates the store.
  • tests/attempt.sh checks the warning. HANDOFF fails when the carry-forward of receipt-less IDs is removed.

| C6 verdict, no projection | `accepted`, not projected | No | Re-check the task digest, the checks and the artifact digest; then finish the projection. |
| C7 stale | the task changed since `new` | Per observation | Resolve ownership risk first, then reject `stale`; new work is a new attempt. |
| C8 cancel unconfirmed | cancel requested, `running`/`unknown` | **Yes** | `probe`; escalate the cancel per the card; stop only with safe evidence. |
| C9 orphaned | another session's attempt, older than its dispatch budget (use one hour when none is recorded), not named in the latest handoff's `open_attempts` | Per observation | Surface it prominently with age and location, then classify it as C1–C8. |

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Base C9 on inactivity rather than creation age

Section 13.2 requires no update for longer than the budget, but this rule uses the attempt's age, and the helper's age_seconds is measured from created_at. An attempt created two hours ago and updated moments ago by another session is therefore classified as orphaned under the one-hour fallback if it is absent from the latest handoff. Use the last recorded update (for example, the latest receipt transition) for the inactivity threshold and add a case with an old creation time and a recent transition; the existing F16 only covers an old attempt without a subsequent update.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3029311 / 32b0ff6.

  • list now reports idle_seconds, measured from the receipt's last transition, next to age_seconds. The pickup C9 row and the reference pickup use it.
  • tests/attempt.sh creates an attempt, waits 2 s, arms it, and requires age_seconds >= 2 and idle_seconds <= 1.
  • New scenario F16b: another session's attempt is created, and 3 s later that session updates it (observe unknown). With budget 2 it is not C9. After 3 more idle seconds it is C9. F16b fails with the old age-based rule.

vunm-dev and others added 2 commits September 25, 2026 11:15
…vity

Review round 1 on #20:

- An absent attempt store is not an empty one. `passdown-attempt list`
  warns "absent, not empty" when the store directory does not exist, and
  still prints []. Handoff builds open_attempts from the store's unresolved
  attempts plus every ID from the previous handoff whose receipt does not
  show it resolved. An ID with no receipt stays listed, marked `# receipt
  missing`, until the owner confirms no worker remains. Pickup treats the
  handoff's IDs as missing when the store is absent.
- C9 follows inactivity, as §13.2 says ("no update for longer than the
  budget"). `list` reports idle_seconds, measured from the receipt's last
  transition, next to age_seconds, and pickup uses it.

tests/attempt.sh covers both helper changes; tests/skills.sh lints the
skill rules; the design records them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ref-host gains `handoff`, the reference for the open_attempts rule, and
  measures C9 by idle_seconds. Its pickup and handoff no longer create the
  store.
- HANDOFF: with a store, a live attempt is listed, a receipt-less ID is
  carried forward and a resolved one dropped. With no store at all,
  pickup reports the missing attempt and sees the absent store, handoff
  keeps the ID, and neither creates the store.
- F16b: an attempt created long ago but updated recently by its own
  session is not orphaned; once idle past the budget it is C9.

Both scenarios fail against the old rules (age-based C9; dropping
receipt-less IDs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vunm-dev
vunm-dev merged commit 8971930 into release/v0.5.0 Sep 25, 2026
7 checks passed
vunm-dev added a commit that referenced this pull request Sep 25, 2026
…vity

Review round 1 on #20:

- An absent attempt store is not an empty one. `passdown-attempt list`
  warns "absent, not empty" when the store directory does not exist, and
  still prints []. Handoff builds open_attempts from the store's unresolved
  attempts plus every ID from the previous handoff whose receipt does not
  show it resolved. An ID with no receipt stays listed, marked `# receipt
  missing`, until the owner confirms no worker remains. Pickup treats the
  handoff's IDs as missing when the store is absent.
- C9 follows inactivity, as §13.2 says ("no update for longer than the
  budget"). `list` reports idle_seconds, measured from the receipt's last
  transition, next to age_seconds, and pickup uses it.

tests/attempt.sh covers both helper changes; tests/skills.sh lints the
skill rules; the design records them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vunm-dev
vunm-dev deleted the feat/pdn-0004-s5-pickup-handoff branch September 25, 2026 04:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant