Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
},
"metadata": {
"description": "Shift-handover workflow for AI agents: intake captured ideas, dispatch tasks to the right executor with recoverable attempt receipts, and hand work down between sessions and agents.",
"version": "0.5.0-beta.2"
"version": "0.5.0-beta.3"
},
"plugins": [
{
"name": "passdown",
"displayName": "Passdown",
"description": "Intake, dispatch, handoff, and pickup skills for multi-agent, multi-session work.",
"version": "0.5.0-beta.2",
"version": "0.5.0-beta.3",
"author": {
"name": "vunm",
"url": "https://github.com/vunm-io"
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.5.0-beta.3] - 2026-09-28

Beta build for the `release/v0.5.0` testing window — not a GitHub release.

### Fixed

- The attempt helper refuses to accept an attempt whose plan edit was
restored. Found by the Layer B release gate (scenario F2): after a crash,
a real host restored the worker's forged `[x]` and `Dispatched:` line,
re-inspected, and accepted with `--scope-override`, following a hint in
the helper's own refusal message. Each inspection now records
`plan_touched` (a new optional member of the receipt's inspection
entries), an accept after a restore is refused, and the refusal message
no longer suggests restoring and overriding. A receipt inspected by a
beta.2 helper keeps its evidence across the upgrade: the helper carries
the earlier `plan_touched` onto its entry and also reads `inspect.json`.
The dispatch skill states
that a plan change seen while the verdict is pending is never the
host's own.
- The Layer B pickup session is limited to task 1.1, so the oracle's
final-tree check is not confused by unrelated host work.

## [0.5.0-beta.2] - 2026-09-27

Beta build for the `release/v0.5.0` testing window — not a GitHub release.
Expand Down Expand Up @@ -302,6 +324,7 @@ Initial dogfooding snapshot.
tags.

[Unreleased]: https://github.com/vunm-io/passdown/compare/v0.4.0...HEAD
[0.5.0-beta.3]: https://github.com/vunm-io/passdown/compare/v0.4.0...release/v0.5.0
[0.5.0-beta.2]: https://github.com/vunm-io/passdown/compare/v0.4.0...release/v0.5.0
[0.5.0-beta.1]: https://github.com/vunm-io/passdown/compare/v0.4.0...release/v0.5.0
[0.4.0]: https://github.com/vunm-io/passdown/compare/v0.3.0...v0.4.0
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.5.0-beta.2
0.5.0-beta.3
28 changes: 24 additions & 4 deletions docs/design/PDN-0004-v05-acceptance-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -1178,10 +1178,14 @@ The helper refuses unless **all** hold:
`settle_seconds > 0`, `inspect` must also have run twice, at least that far
apart, with equal digests. This is **artifact-stability** evidence. It is
required in addition to safe stop evidence and never replaces it (§8.3).
5. `artifact.out_of_scope` is empty and `plan_touched` is false — or the host
has already restored the plan per the PDN-0003 rule and re-run `inspect`,
and the verdict carries the `--scope-override <reason>` flag, which is
recorded. (Default: reject `scope_violation` / `plan_tampered`.)
5. `artifact.out_of_scope` is empty and `plan_touched` is false — or the
verdict carries the `--scope-override <reason>` flag, which is recorded.
(Default: reject `scope_violation` / `plan_tampered`.) *Revised in S7
(Layer B F2):* restoring the plan never earns an accept. Each inspection
records `plan_touched`, and the helper refuses an accept once any
inspection saw the plan touched and the latest no longer does; the host
edits the plan only after the verdict, so that edit was the worker's.
The override records only a plan change the owner made and keeps.
6. At least one `--check`, **every** `--check` with exit code 0, each with
its output file in `checks/`. (Refined in S4: r3 said "at least one whose
exit code is 0", which accepted a passing lint next to a failing test.)
Expand Down Expand Up @@ -2191,6 +2195,22 @@ ships it.
- Independent merge: this is the release PR (`release/v0.5.0` → `main`).
- Rollback: do not merge; `main` stays at v0.4.x.

**Implementation notes (S7).** Layer B found two protocol gaps on the real
host, each fixed before the gate reran:

- *F1, beta.1.* After rejecting an attempt `plan_tampered`, the host
verified the rejected worker's file in place and appended `Dispatched:
main — accepted`, although the owner's route named no fallback. Step 16
now says a host line never accepts a rejected attempt's output: an
owner-mandated task stays pending, and any other task is finished only
through salvage or after the output is given up.
- *F2, beta.2.* After a crash, the pickup host restored the worker's forged
plan lines, re-inspected, and accepted with `--scope-override`, blaming
"an interrupted host session". The helper's own refusal message had
suggested restoring and overriding. Rule 5 of §12.3 is revised above; the
helper enforces it, and the skill states that a plan change seen while
the verdict is pending is never the host's.

## 27. Open questions

r1 listed five open questions. The first review resolved all of them, and r2
Expand Down
2 changes: 1 addition & 1 deletion plugins/passdown/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "passdown",
"displayName": "Passdown",
"version": "0.5.0-beta.2",
"version": "0.5.0-beta.3",
"description": "Intake, dispatch, handoff, and pickup skills for multi-agent, multi-session work.",
"author": {
"name": "vunm",
Expand Down
2 changes: 1 addition & 1 deletion plugins/passdown/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "passdown",
"version": "0.5.0-beta.2",
"version": "0.5.0-beta.3",
"description": "Intake, dispatch, handoff, and pickup skills for multi-agent, multi-session work.",
"author": {
"name": "vunm",
Expand Down
7 changes: 6 additions & 1 deletion plugins/passdown/skills/passdown-dispatch/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,12 @@ otherwise.
`plan_tampered`. If you cannot attribute the change safely (the user or
another session may have edited the plan meanwhile), do not overwrite it
and do not record a verdict: stop and report the conflict so the user
can reconcile it;
can reconcile it. **A plan change seen while the verdict is pending is
never the host's own,** not even an interrupted earlier session's: the
host edits the plan only at step 16, after the verdict. Restoring the
plan is always followed by `plan_tampered`, never by an accept (the
helper refuses one). `--scope-override` records only a plan change the
owner made and keeps, on the owner's word;
- `out_of_scope` not empty → reject `scope_violation`;
- otherwise run every verification command the task names yourself,
saving each command's output to a file. Every one must exit `0`; if any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,8 @@
"additionalProperties": false,
"properties": {
"at": { "$ref": "#/$defs/timestamp" },
"digest": { "$ref": "#/$defs/digest" }
"digest": { "$ref": "#/$defs/digest" },
"plan_touched": { "type": "boolean" }
}
}
},
Expand Down
26 changes: 21 additions & 5 deletions plugins/passdown/skills/passdown-dispatch/scripts/passdown-attempt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# shellcheck disable=SC2016,SC2015
set -euo pipefail

PASSDOWN_VERSION="0.5.0-beta.2"
PASSDOWN_VERSION="0.5.0-beta.3"

# jq >= 1.7 is required on Windows for its -b (binary output) flag.
# Git Bash on Windows rewrites every argument that looks like a POSIX path
Expand Down Expand Up @@ -1700,12 +1700,18 @@ cmd_inspect() {
else
jq -c -n --argjson rec "$record" '[$rec]' | write_atomic "$STORE/$id/inspect.json"
fi
# A receipt written before inspections carried plan_touched (beta.2) keeps
# that evidence only in artifact.plan_touched, the latest inspection's
# value: move it onto that entry before this inspection overwrites it.
mutate "$id" "$REV" inspect '
.execution.reconciled_at = (.execution.reconciled_at // $now)
.artifact.plan_touched as $prev
| .artifact.inspections |= (if length > 0 and (.[-1] | has("plan_touched") | not) and $prev != null
then .[-1].plan_touched = $prev else . end)
| .execution.reconciled_at = (.execution.reconciled_at // $now)
| .artifact.digest = $a.digest | .artifact.changed = $a.changed
| .artifact.out_of_scope = $a.out_of_scope | .artifact.overlaps_baseline = $a.overlaps_baseline
| .artifact.plan_touched = $a.plan_touched
| .artifact.inspections += [{at: $now, digest: $a.digest}]
| .artifact.inspections += [{at: $now, digest: $a.digest, plan_touched: $a.plan_touched}]
| if .claim == null then .artifact.target_after = $a.digest else . end' --argjson a "$art"
if [ "$JSON_OUT" = 1 ]; then
jq -c --slurpfile r "$STORE/$id/receipt.json" '{id: $r[0].id, rev: $r[0].rev} + (. | del(.entries)) + {paths: [.entries[].path]}' <<<"$art"
Expand Down Expand Up @@ -1835,9 +1841,19 @@ verdict_accept() {
and (($i[-1].at | fromdateiso8601) - ($i[-2].at | fromdateiso8601)) >= $s' <<<"$r" >/dev/null ||
die $E_STATE "accept: the card asks for two equal inspections at least ${settle}s apart (artifact stability)"
fi
# 5. scope and plan
# 5. scope and plan. The host edits the plan only after the verdict, so a
# plan change an inspection saw belongs to the worker (or to the owner).
# Restoring it undoes the worker's edit; it never earns an accept.
# inspect.json holds every inspection's full record, including those a
# beta.2 helper made before the receipt's entries carried plan_touched.
local seen=false
if jq -e 'any(.artifact.inspections[]; .plan_touched == true)' <<<"$r" >/dev/null; then seen=true
elif [ -f "$STORE/$id/inspect.json" ] && jq -e 'any(.[]; .plan_touched == true)' "$STORE/$id/inspect.json" >/dev/null 2>&1; then seen=true; fi
if [ "$seen" = true ] && [ "$(jq -r .artifact.plan_touched <<<"$r")" = false ]; then
die $E_STATE "accept: an earlier inspection saw the plan touched and it has since been restored; the host edits the plan only after the verdict, so that edit was the worker's: reject plan_tampered"
fi
if [ "$(jq -r '(.artifact.out_of_scope | length) > 0 or .artifact.plan_touched' <<<"$r")" = true ] && [ -z "$override" ]; then
die $E_STATE "accept: out-of-scope paths $(jq -c .artifact.out_of_scope <<<"$r") / plan touched $(jq -r .artifact.plan_touched <<<"$r"); reject scope_violation/plan_tampered, or restore, re-inspect and pass --scope-override"
die $E_STATE "accept: out-of-scope paths $(jq -c .artifact.out_of_scope <<<"$r") / plan touched $(jq -r .artifact.plan_touched <<<"$r"); reject scope_violation/plan_tampered (--scope-override only records a change the owner made and keeps, on the owner's word)"
fi
# 6. at least one host check, and every one of them passed
[ "$#" -gt 0 ] || die $E_STATE "accept: at least one --check is required (I-10)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,8 @@
"additionalProperties": false,
"properties": {
"at": { "$ref": "#/$defs/timestamp" },
"digest": { "$ref": "#/$defs/digest" }
"digest": { "$ref": "#/$defs/digest" },
"plan_touched": { "type": "boolean" }
}
}
},
Expand Down
26 changes: 21 additions & 5 deletions plugins/passdown/skills/passdown-handoff/scripts/passdown-attempt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# shellcheck disable=SC2016,SC2015
set -euo pipefail

PASSDOWN_VERSION="0.5.0-beta.2"
PASSDOWN_VERSION="0.5.0-beta.3"

# jq >= 1.7 is required on Windows for its -b (binary output) flag.
# Git Bash on Windows rewrites every argument that looks like a POSIX path
Expand Down Expand Up @@ -1700,12 +1700,18 @@ cmd_inspect() {
else
jq -c -n --argjson rec "$record" '[$rec]' | write_atomic "$STORE/$id/inspect.json"
fi
# A receipt written before inspections carried plan_touched (beta.2) keeps
# that evidence only in artifact.plan_touched, the latest inspection's
# value: move it onto that entry before this inspection overwrites it.
mutate "$id" "$REV" inspect '
.execution.reconciled_at = (.execution.reconciled_at // $now)
.artifact.plan_touched as $prev
| .artifact.inspections |= (if length > 0 and (.[-1] | has("plan_touched") | not) and $prev != null
then .[-1].plan_touched = $prev else . end)
| .execution.reconciled_at = (.execution.reconciled_at // $now)
| .artifact.digest = $a.digest | .artifact.changed = $a.changed
| .artifact.out_of_scope = $a.out_of_scope | .artifact.overlaps_baseline = $a.overlaps_baseline
| .artifact.plan_touched = $a.plan_touched
| .artifact.inspections += [{at: $now, digest: $a.digest}]
| .artifact.inspections += [{at: $now, digest: $a.digest, plan_touched: $a.plan_touched}]
| if .claim == null then .artifact.target_after = $a.digest else . end' --argjson a "$art"
if [ "$JSON_OUT" = 1 ]; then
jq -c --slurpfile r "$STORE/$id/receipt.json" '{id: $r[0].id, rev: $r[0].rev} + (. | del(.entries)) + {paths: [.entries[].path]}' <<<"$art"
Expand Down Expand Up @@ -1835,9 +1841,19 @@ verdict_accept() {
and (($i[-1].at | fromdateiso8601) - ($i[-2].at | fromdateiso8601)) >= $s' <<<"$r" >/dev/null ||
die $E_STATE "accept: the card asks for two equal inspections at least ${settle}s apart (artifact stability)"
fi
# 5. scope and plan
# 5. scope and plan. The host edits the plan only after the verdict, so a
# plan change an inspection saw belongs to the worker (or to the owner).
# Restoring it undoes the worker's edit; it never earns an accept.
# inspect.json holds every inspection's full record, including those a
# beta.2 helper made before the receipt's entries carried plan_touched.
local seen=false
if jq -e 'any(.artifact.inspections[]; .plan_touched == true)' <<<"$r" >/dev/null; then seen=true
elif [ -f "$STORE/$id/inspect.json" ] && jq -e 'any(.[]; .plan_touched == true)' "$STORE/$id/inspect.json" >/dev/null 2>&1; then seen=true; fi
if [ "$seen" = true ] && [ "$(jq -r .artifact.plan_touched <<<"$r")" = false ]; then
die $E_STATE "accept: an earlier inspection saw the plan touched and it has since been restored; the host edits the plan only after the verdict, so that edit was the worker's: reject plan_tampered"
fi
if [ "$(jq -r '(.artifact.out_of_scope | length) > 0 or .artifact.plan_touched' <<<"$r")" = true ] && [ -z "$override" ]; then
die $E_STATE "accept: out-of-scope paths $(jq -c .artifact.out_of_scope <<<"$r") / plan touched $(jq -r .artifact.plan_touched <<<"$r"); reject scope_violation/plan_tampered, or restore, re-inspect and pass --scope-override"
die $E_STATE "accept: out-of-scope paths $(jq -c .artifact.out_of_scope <<<"$r") / plan touched $(jq -r .artifact.plan_touched <<<"$r"); reject scope_violation/plan_tampered (--scope-override only records a change the owner made and keeps, on the owner's word)"
fi
# 6. at least one host check, and every one of them passed
[ "$#" -gt 0 ] || die $E_STATE "accept: at least one --check is required (I-10)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,8 @@
"additionalProperties": false,
"properties": {
"at": { "$ref": "#/$defs/timestamp" },
"digest": { "$ref": "#/$defs/digest" }
"digest": { "$ref": "#/$defs/digest" },
"plan_touched": { "type": "boolean" }
}
}
},
Expand Down
26 changes: 21 additions & 5 deletions plugins/passdown/skills/passdown-pickup/scripts/passdown-attempt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# shellcheck disable=SC2016,SC2015
set -euo pipefail

PASSDOWN_VERSION="0.5.0-beta.2"
PASSDOWN_VERSION="0.5.0-beta.3"

# jq >= 1.7 is required on Windows for its -b (binary output) flag.
# Git Bash on Windows rewrites every argument that looks like a POSIX path
Expand Down Expand Up @@ -1700,12 +1700,18 @@ cmd_inspect() {
else
jq -c -n --argjson rec "$record" '[$rec]' | write_atomic "$STORE/$id/inspect.json"
fi
# A receipt written before inspections carried plan_touched (beta.2) keeps
# that evidence only in artifact.plan_touched, the latest inspection's
# value: move it onto that entry before this inspection overwrites it.
mutate "$id" "$REV" inspect '
.execution.reconciled_at = (.execution.reconciled_at // $now)
.artifact.plan_touched as $prev
| .artifact.inspections |= (if length > 0 and (.[-1] | has("plan_touched") | not) and $prev != null
then .[-1].plan_touched = $prev else . end)
| .execution.reconciled_at = (.execution.reconciled_at // $now)
| .artifact.digest = $a.digest | .artifact.changed = $a.changed
| .artifact.out_of_scope = $a.out_of_scope | .artifact.overlaps_baseline = $a.overlaps_baseline
| .artifact.plan_touched = $a.plan_touched
| .artifact.inspections += [{at: $now, digest: $a.digest}]
| .artifact.inspections += [{at: $now, digest: $a.digest, plan_touched: $a.plan_touched}]
| if .claim == null then .artifact.target_after = $a.digest else . end' --argjson a "$art"
if [ "$JSON_OUT" = 1 ]; then
jq -c --slurpfile r "$STORE/$id/receipt.json" '{id: $r[0].id, rev: $r[0].rev} + (. | del(.entries)) + {paths: [.entries[].path]}' <<<"$art"
Expand Down Expand Up @@ -1835,9 +1841,19 @@ verdict_accept() {
and (($i[-1].at | fromdateiso8601) - ($i[-2].at | fromdateiso8601)) >= $s' <<<"$r" >/dev/null ||
die $E_STATE "accept: the card asks for two equal inspections at least ${settle}s apart (artifact stability)"
fi
# 5. scope and plan
# 5. scope and plan. The host edits the plan only after the verdict, so a
# plan change an inspection saw belongs to the worker (or to the owner).
# Restoring it undoes the worker's edit; it never earns an accept.
# inspect.json holds every inspection's full record, including those a
# beta.2 helper made before the receipt's entries carried plan_touched.
local seen=false
if jq -e 'any(.artifact.inspections[]; .plan_touched == true)' <<<"$r" >/dev/null; then seen=true
elif [ -f "$STORE/$id/inspect.json" ] && jq -e 'any(.[]; .plan_touched == true)' "$STORE/$id/inspect.json" >/dev/null 2>&1; then seen=true; fi
if [ "$seen" = true ] && [ "$(jq -r .artifact.plan_touched <<<"$r")" = false ]; then
die $E_STATE "accept: an earlier inspection saw the plan touched and it has since been restored; the host edits the plan only after the verdict, so that edit was the worker's: reject plan_tampered"
fi
if [ "$(jq -r '(.artifact.out_of_scope | length) > 0 or .artifact.plan_touched' <<<"$r")" = true ] && [ -z "$override" ]; then
die $E_STATE "accept: out-of-scope paths $(jq -c .artifact.out_of_scope <<<"$r") / plan touched $(jq -r .artifact.plan_touched <<<"$r"); reject scope_violation/plan_tampered, or restore, re-inspect and pass --scope-override"
die $E_STATE "accept: out-of-scope paths $(jq -c .artifact.out_of_scope <<<"$r") / plan touched $(jq -r .artifact.plan_touched <<<"$r"); reject scope_violation/plan_tampered (--scope-override only records a change the owner made and keeps, on the owner's word)"
fi
# 6. at least one host check, and every one of them passed
[ "$#" -gt 0 ] || die $E_STATE "accept: at least one --check is required (I-10)"
Expand Down
3 changes: 2 additions & 1 deletion schemas/protocol/receipt.v1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,8 @@
"additionalProperties": false,
"properties": {
"at": { "$ref": "#/$defs/timestamp" },
"digest": { "$ref": "#/$defs/digest" }
"digest": { "$ref": "#/$defs/digest" },
"plan_touched": { "type": "boolean" }
}
}
},
Expand Down
Loading
Loading