forge-orkes 0.50.1 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/template/.claude/hooks/forge-reserve.sh +51 -15
  3. package/template/.claude/hooks/forge-slice-notify.sh +66 -0
  4. package/template/.claude/hooks/forge-slice-runner.sh +593 -0
  5. package/template/.claude/hooks/tests/verify-signoff.test.sh +122 -0
  6. package/template/.claude/hooks/tests/wu-done.test.sh +68 -0
  7. package/template/.claude/hooks/verify-signoff.sh +139 -0
  8. package/template/.claude/hooks/wu-done.sh +48 -0
  9. package/template/.claude/skills/chief-of-staff/SKILL.md +11 -3
  10. package/template/.claude/skills/deferred/SKILL.md +8 -4
  11. package/template/.claude/skills/discussing/SKILL.md +4 -2
  12. package/template/.claude/skills/executing/SKILL.md +21 -17
  13. package/template/.claude/skills/forge/SKILL.md +13 -10
  14. package/template/.claude/skills/planning/SKILL.md +3 -1
  15. package/template/.claude/skills/prototyping/SKILL.md +10 -7
  16. package/template/.claude/skills/quick-tasking/SKILL.md +3 -1
  17. package/template/.claude/skills/researching/SKILL.md +2 -0
  18. package/template/.claude/skills/reviewing/SKILL.md +7 -7
  19. package/template/.claude/skills/verifying/SKILL.md +8 -7
  20. package/template/.forge/FORGE.md +14 -7
  21. package/template/.forge/gitignore +8 -0
  22. package/template/.forge/migrations/0.52.0-id-reservation-milestone-refactor.md +45 -0
  23. package/template/.forge/templates/deferred-issue.md +22 -0
  24. package/template/.forge/templates/slice-runner/config.yml +62 -0
  25. package/template/.forge/templates/slice-runner/phase-report.schema.json +51 -0
  26. package/template/.forge/templates/slice-runner/phase-report.yml +58 -0
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env sh
2
+ # Self-contained unit test for .claude/hooks/verify-signoff.sh (Contract M0 / D1).
3
+ #
4
+ # Builds throwaway git repos with real SSH-signed tags (a fresh keypair per test
5
+ # run, registered as an allowed signer) and asserts exit code + reason for the
6
+ # four negative cases plus the one positive case. Needs ssh-keygen + git built
7
+ # with SSH signing support (git >= 2.34). Run:
8
+ # ./.claude/hooks/tests/verify-signoff.test.sh
9
+ # Exits 0 iff every case passes; non-zero on any failure. Self-cleans on exit.
10
+ set -u
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
+ HELPER="$SCRIPT_DIR/../verify-signoff.sh"
14
+
15
+ [ -x "$HELPER" ] || { printf 'FATAL: helper not executable: %s\n' "$HELPER" >&2; exit 1; }
16
+ command -v ssh-keygen >/dev/null 2>&1 || { printf 'FATAL: ssh-keygen not found\n' >&2; exit 1; }
17
+
18
+ ROOT="$(mktemp -d)"
19
+ trap 'rm -rf "$ROOT"' EXIT INT TERM
20
+
21
+ PASSED=0
22
+ FAILED=0
23
+ pass() { PASSED=$((PASSED + 1)); printf 'PASS: %s\n' "$1"; }
24
+ fail() { FAILED=$((FAILED + 1)); printf 'FAIL: %s\n' "$1" >&2; }
25
+ check() { # desc, actual, expected
26
+ if [ "$2" = "$3" ]; then pass "$1"; else fail "$1 (got '$2', want '$3')"; fi
27
+ }
28
+
29
+ # One allowed-signer keypair (the "operator") + one non-allowed keypair (the
30
+ # "agent") shared across cases.
31
+ gen_key() { # path
32
+ ssh-keygen -t ed25519 -N "" -C "$2" -f "$1" -q
33
+ }
34
+ gen_key "$ROOT/operator_key" "operator@example.com"
35
+ gen_key "$ROOT/agent_key" "agent@example.com"
36
+
37
+ allowed_signers="$ROOT/allowed_signers"
38
+ printf 'operator@example.com namespaces="git" %s\n' "$(cat "$ROOT/operator_key.pub")" > "$allowed_signers"
39
+
40
+ new_repo() { # name
41
+ d="$ROOT/repo.$1"
42
+ mkdir -p "$d"
43
+ git -C "$d" init -q
44
+ git -C "$d" symbolic-ref HEAD refs/heads/main
45
+ git -C "$d" config user.email t@example.com
46
+ git -C "$d" config user.name tester
47
+ printf '%s\n' "$d"
48
+ }
49
+
50
+ # Mint a signoff tag with an arbitrary key + arbitrary head:/evidence: body.
51
+ mint_tag() { # repo, wu-id, signing-key, head, evidence
52
+ repo="$1"; wu="$2"; key="$3"; head="$4"; evidence="$5"
53
+ body="wu: $wu
54
+ head: $head
55
+ evidence: $evidence"
56
+ git -C "$repo" -c gpg.format=ssh -c "user.signingkey=$key" \
57
+ tag -s "signoff/$wu" -m "$body"
58
+ }
59
+
60
+ evidence_file="$ROOT/evidence.txt"
61
+ printf 'evidence payload\n' > evidence_tmp && mv evidence_tmp "$evidence_file" 2>/dev/null || printf 'evidence payload\n' > "$evidence_file"
62
+ evidence_hash="$(shasum -a 256 "$evidence_file" 2>/dev/null | awk '{print $1}')"
63
+ [ -n "$evidence_hash" ] || evidence_hash="$(sha256sum "$evidence_file" | awk '{print $1}')"
64
+
65
+ # --- Positive case: correctly signed tag at HEAD, matching evidence ---------
66
+ d="$(new_repo pos)"; cd "$d"
67
+ git commit --allow-empty -qm init
68
+ head_sha="$(git rev-parse HEAD)"
69
+ mint_tag "$d" "wu-pos" "$ROOT/operator_key" "$head_sha" "$evidence_hash"
70
+ out="$("$HELPER" wu-pos --head "$head_sha" --evidence "$evidence_hash" --allowed-signers "$allowed_signers")"; rc=$?
71
+ check "positive: exit 0" "$rc" "0"
72
+ check "positive: stdout ok" "$out" "ok"
73
+
74
+ # Same, using --evidence-file instead of a precomputed hash.
75
+ out2="$("$HELPER" wu-pos --head "$head_sha" --evidence-file "$evidence_file" --allowed-signers "$allowed_signers")"; rc2=$?
76
+ check "positive (evidence-file): exit 0" "$rc2" "0"
77
+
78
+ # --- Negative 1: no-signoff (tag absent) ------------------------------------
79
+ d="$(new_repo neg1)"; cd "$d"
80
+ git commit --allow-empty -qm init
81
+ head_sha="$(git rev-parse HEAD)"
82
+ out="$("$HELPER" wu-neg1 --head "$head_sha" --evidence "$evidence_hash" --allowed-signers "$allowed_signers" 2>/dev/null)"; rc=$?
83
+ check "no-signoff: exit 1" "$rc" "1"
84
+ check "no-signoff: reason" "$out" "reason=no-signoff"
85
+
86
+ # --- Negative 2: bad-signature (signed by a non-allowed key) ----------------
87
+ d="$(new_repo neg2)"; cd "$d"
88
+ git commit --allow-empty -qm init
89
+ head_sha="$(git rev-parse HEAD)"
90
+ mint_tag "$d" "wu-neg2" "$ROOT/agent_key" "$head_sha" "$evidence_hash"
91
+ out="$("$HELPER" wu-neg2 --head "$head_sha" --evidence "$evidence_hash" --allowed-signers "$allowed_signers" 2>/dev/null)"; rc=$?
92
+ check "bad-signature: exit 1" "$rc" "1"
93
+ check "bad-signature: reason" "$out" "reason=bad-signature"
94
+
95
+ # --- Negative 3: stale (head: does not match landed commit) -----------------
96
+ d="$(new_repo neg3)"; cd "$d"
97
+ git commit --allow-empty -qm init
98
+ head_sha="$(git rev-parse HEAD)"
99
+ mint_tag "$d" "wu-neg3" "$ROOT/operator_key" "0000000000000000000000000000000000000000" "$evidence_hash"
100
+ out="$("$HELPER" wu-neg3 --head "$head_sha" --evidence "$evidence_hash" --allowed-signers "$allowed_signers" 2>/dev/null)"; rc=$?
101
+ check "stale: exit 1" "$rc" "1"
102
+ check "stale: reason" "$out" "reason=stale"
103
+
104
+ # --- Negative 4: evidence-mismatch ------------------------------------------
105
+ d="$(new_repo neg4)"; cd "$d"
106
+ git commit --allow-empty -qm init
107
+ head_sha="$(git rev-parse HEAD)"
108
+ mint_tag "$d" "wu-neg4" "$ROOT/operator_key" "$head_sha" "deadbeef"
109
+ out="$("$HELPER" wu-neg4 --head "$head_sha" --evidence "$evidence_hash" --allowed-signers "$allowed_signers" 2>/dev/null)"; rc=$?
110
+ check "evidence-mismatch: exit 1" "$rc" "1"
111
+ check "evidence-mismatch: reason" "$out" "reason=evidence-mismatch"
112
+
113
+ # --- Bad invocation: missing wu-id / missing evidence exits 2 ---------------
114
+ out="$("$HELPER" 2>/dev/null)"; rc=$?
115
+ check "missing wu-id: exit 2" "$rc" "2"
116
+ d="$(new_repo badinvoke)"; cd "$d"
117
+ git commit --allow-empty -qm init
118
+ out="$("$HELPER" wu-x --allowed-signers "$allowed_signers" 2>/dev/null)"; rc=$?
119
+ check "missing evidence: exit 2" "$rc" "2"
120
+
121
+ printf '\n%d passed, %d failed\n' "$PASSED" "$FAILED"
122
+ [ "$FAILED" -eq 0 ]
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env sh
2
+ # Self-contained unit test for .claude/hooks/wu-done.sh (Contract M0 / D2).
3
+ #
4
+ # Asserts the fold stub reports done=true only for a valid signoff, and that
5
+ # replacing a good tag with a bad-signature one flips done back to false —
6
+ # the contract's verification #4. Run:
7
+ # ./.claude/hooks/tests/wu-done.test.sh
8
+ set -u
9
+
10
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
11
+ HELPER="$SCRIPT_DIR/../wu-done.sh"
12
+
13
+ [ -x "$HELPER" ] || { printf 'FATAL: helper not executable: %s\n' "$HELPER" >&2; exit 1; }
14
+ command -v ssh-keygen >/dev/null 2>&1 || { printf 'FATAL: ssh-keygen not found\n' >&2; exit 1; }
15
+
16
+ ROOT="$(mktemp -d)"
17
+ trap 'rm -rf "$ROOT"' EXIT INT TERM
18
+
19
+ PASSED=0
20
+ FAILED=0
21
+ pass() { PASSED=$((PASSED + 1)); printf 'PASS: %s\n' "$1"; }
22
+ fail() { FAILED=$((FAILED + 1)); printf 'FAIL: %s\n' "$1" >&2; }
23
+ check() { # desc, actual, expected
24
+ if [ "$2" = "$3" ]; then pass "$1"; else fail "$1 (got '$2', want '$3')"; fi
25
+ }
26
+
27
+ ssh-keygen -t ed25519 -N "" -C "operator@example.com" -f "$ROOT/operator_key" -q
28
+ ssh-keygen -t ed25519 -N "" -C "agent@example.com" -f "$ROOT/agent_key" -q
29
+ allowed_signers="$ROOT/allowed_signers"
30
+ printf 'operator@example.com namespaces="git" %s\n' "$(cat "$ROOT/operator_key.pub")" > "$allowed_signers"
31
+
32
+ d="$ROOT/repo"
33
+ mkdir -p "$d"
34
+ git -C "$d" init -q
35
+ git -C "$d" symbolic-ref HEAD refs/heads/main
36
+ git -C "$d" config user.email t@example.com
37
+ git -C "$d" config user.name tester
38
+ cd "$d"
39
+ git commit --allow-empty -qm init
40
+ head_sha="$(git rev-parse HEAD)"
41
+ evidence="deadbeefcafe"
42
+
43
+ mint() { # signing-key
44
+ git -c gpg.format=ssh -c "user.signingkey=$1" \
45
+ tag -sf "signoff/wu-flip" -m "wu: wu-flip
46
+ head: $head_sha
47
+ evidence: $evidence"
48
+ }
49
+
50
+ # --- Before any tag: done=false, reason=no-signoff --------------------------
51
+ out="$("$HELPER" wu-flip --head "$head_sha" --evidence "$evidence" --allowed-signers "$allowed_signers")"; rc=$?
52
+ check "no tag: exit 1" "$rc" "1"
53
+ check "no tag: reason" "$out" "done=false reason=no-signoff"
54
+
55
+ # --- Good signature: done=true ----------------------------------------------
56
+ mint "$ROOT/operator_key"
57
+ out="$("$HELPER" wu-flip --head "$head_sha" --evidence "$evidence" --allowed-signers "$allowed_signers")"; rc=$?
58
+ check "good signoff: exit 0" "$rc" "0"
59
+ check "good signoff: stdout" "$out" "done=true"
60
+
61
+ # --- Flip: re-tag the same wu-id with a non-allowed key — done flips false --
62
+ mint "$ROOT/agent_key"
63
+ out="$("$HELPER" wu-flip --head "$head_sha" --evidence "$evidence" --allowed-signers "$allowed_signers")"; rc=$?
64
+ check "flipped to bad signature: exit 1" "$rc" "1"
65
+ check "flipped to bad signature: reason" "$out" "done=false reason=bad-signature"
66
+
67
+ printf '\n%d passed, %d failed\n' "$PASSED" "$FAILED"
68
+ [ "$FAILED" -eq 0 ]
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env sh
2
+ # verify-signoff — check that a work unit's completion is backed by a valid,
3
+ # SSH-signed sign-off tag. Implements the Contract M0 sign-off convention
4
+ # (docs/sign-off-convention.md): "done" for a work unit is true iff this
5
+ # script exits 0 for that work unit's id. No YAML `human_verified` field is
6
+ # the source of truth — a stored copy of that value is a derived echo only.
7
+ #
8
+ # Convention: the sign-off is an SSH-signed annotated tag `signoff/<wu-id>` at
9
+ # the landed commit, message body:
10
+ # wu: <wu-id>
11
+ # head: <full-sha>
12
+ # evidence: <evidence-set-hash>
13
+ # Minted by the operator (or a process holding the operator's signing key),
14
+ # never from an agent context:
15
+ # git -c gpg.format=ssh -c user.signingkey=<operator key> \
16
+ # tag -s signoff/<wu-id> -m "wu: <wu-id>\nhead: <sha>\nevidence: <hash>"
17
+ #
18
+ # verify-signoff.sh <wu-id> [--head <sha>] [--evidence <hash>]
19
+ # [--evidence-file <path>] [--allowed-signers <path>]
20
+ #
21
+ # --head <sha> expected landed commit. Default: `git rev-parse HEAD`.
22
+ # --evidence <hash> expected evidence-set hash, compared verbatim.
23
+ # --evidence-file <path> compute the expected hash as `sha256(path)` instead
24
+ # of passing it directly. Exactly one of --evidence /
25
+ # --evidence-file is required.
26
+ # --allowed-signers <path> SSH allowed-signers file. Default:
27
+ # `git config --get gpg.ssh.allowedSignersFile`.
28
+ #
29
+ # stdout on success: `ok`. stdout on failure: `reason=<code>` (machine-parseable;
30
+ # nothing else on stdout). Human-readable detail always goes to stderr.
31
+ #
32
+ # Exit codes:
33
+ # 0 signed, verified, and matching — the work unit is done.
34
+ # 1 a verification check failed; stdout carries the reason code:
35
+ # no-signoff no signoff/<wu-id> tag exists
36
+ # bad-signature tag exists but signature is missing/unknown/invalid
37
+ # stale tag's `head:` does not match the expected commit
38
+ # evidence-mismatch tag's `evidence:` does not match the expected hash
39
+ # 2 bad invocation (missing wu-id, missing evidence input, bad flag) — no
40
+ # verification was attempted.
41
+ #
42
+ # Portable: no network calls; only git (must support `gpg.format=ssh`, i.e.
43
+ # git >= 2.34) plus standard POSIX utilities (awk/sed/grep) and shasum/sha256sum
44
+ # for --evidence-file. Works with no remote at all — the tag is local git state.
45
+ set -eu
46
+
47
+ usage() {
48
+ printf 'usage: verify-signoff.sh <wu-id> [--head <sha>] [--evidence <hash> | --evidence-file <path>] [--allowed-signers <path>]\n' >&2
49
+ exit 2
50
+ }
51
+
52
+ wu_id=""
53
+ head_arg=""
54
+ evidence_arg=""
55
+ evidence_file=""
56
+ allowed_signers=""
57
+
58
+ while [ $# -gt 0 ]; do
59
+ case "$1" in
60
+ --head) shift; head_arg="${1:-}"; [ $# -gt 0 ] && shift || usage ;;
61
+ --head=*) head_arg="${1#*=}"; shift ;;
62
+ --evidence) shift; evidence_arg="${1:-}"; [ $# -gt 0 ] && shift || usage ;;
63
+ --evidence=*) evidence_arg="${1#*=}"; shift ;;
64
+ --evidence-file) shift; evidence_file="${1:-}"; [ $# -gt 0 ] && shift || usage ;;
65
+ --evidence-file=*) evidence_file="${1#*=}"; shift ;;
66
+ --allowed-signers) shift; allowed_signers="${1:-}"; [ $# -gt 0 ] && shift || usage ;;
67
+ --allowed-signers=*) allowed_signers="${1#*=}"; shift ;;
68
+ -*) printf 'verify-signoff: unknown option: %s\n' "$1" >&2; usage ;;
69
+ *)
70
+ if [ -z "$wu_id" ]; then wu_id="$1"; shift
71
+ else printf 'verify-signoff: unexpected argument: %s\n' "$1" >&2; usage; fi
72
+ ;;
73
+ esac
74
+ done
75
+
76
+ [ -n "$wu_id" ] || usage
77
+ if [ -n "$evidence_arg" ] && [ -n "$evidence_file" ]; then
78
+ printf 'verify-signoff: pass exactly one of --evidence / --evidence-file, not both\n' >&2
79
+ exit 2
80
+ fi
81
+ if [ -z "$evidence_arg" ] && [ -z "$evidence_file" ]; then
82
+ printf 'verify-signoff: --evidence <hash> or --evidence-file <path> is required\n' >&2
83
+ exit 2
84
+ fi
85
+
86
+ [ -n "$head_arg" ] || head_arg="$(git rev-parse HEAD)"
87
+
88
+ if [ -n "$evidence_file" ]; then
89
+ [ -f "$evidence_file" ] || { printf 'verify-signoff: evidence file not found: %s\n' "$evidence_file" >&2; exit 2; }
90
+ if command -v shasum >/dev/null 2>&1; then
91
+ evidence_arg="$(shasum -a 256 "$evidence_file" | awk '{print $1}')"
92
+ elif command -v sha256sum >/dev/null 2>&1; then
93
+ evidence_arg="$(sha256sum "$evidence_file" | awk '{print $1}')"
94
+ else
95
+ printf 'verify-signoff: need shasum or sha256sum to hash --evidence-file\n' >&2
96
+ exit 2
97
+ fi
98
+ fi
99
+
100
+ if [ -z "$allowed_signers" ]; then
101
+ allowed_signers="$(git config --get gpg.ssh.allowedSignersFile || true)"
102
+ fi
103
+ [ -n "$allowed_signers" ] || { printf 'verify-signoff: no --allowed-signers given and gpg.ssh.allowedSignersFile is unset\n' >&2; exit 2; }
104
+ [ -f "$allowed_signers" ] || { printf 'verify-signoff: allowed-signers file not found: %s\n' "$allowed_signers" >&2; exit 2; }
105
+
106
+ tag_ref="refs/tags/signoff/$wu_id"
107
+
108
+ fail() { # reason
109
+ printf 'reason=%s\n' "$1"
110
+ printf 'verify-signoff: FAIL (%s) for %s\n' "$1" "$wu_id" >&2
111
+ exit 1
112
+ }
113
+
114
+ # --- 1. resolve the tag ------------------------------------------------------
115
+ git rev-parse -q --verify "$tag_ref" >/dev/null 2>&1 || fail no-signoff
116
+
117
+ # --- 2. verify the SSH signature (self-contained: pass format + signers file
118
+ # explicitly so this doesn't depend on the caller's git config, e.g. a scrubbed
119
+ # worker environment that has no signing key but must still be ABLE to verify).
120
+ if ! git -c gpg.format=ssh -c "gpg.ssh.allowedSignersFile=$allowed_signers" \
121
+ tag -v "signoff/$wu_id" >/dev/null 2>&1; then
122
+ fail bad-signature
123
+ fi
124
+
125
+ # --- 3. parse the message body and compare head:/evidence: ------------------
126
+ # Raw tag object: headers, blank line, message, then (for a signed tag) an
127
+ # embedded "-----BEGIN SSH SIGNATURE-----" block appended after the message.
128
+ # Strip headers up to the first blank line, then stop at the signature marker.
129
+ msg="$(git cat-file -p "$tag_ref" | awk 'f{ if ($0 ~ /-----BEGIN SSH SIGNATURE-----/) exit; print } /^$/{ f=1 }')"
130
+
131
+ tag_head="$(printf '%s\n' "$msg" | sed -n 's/^head: *//p' | head -1)"
132
+ tag_evidence="$(printf '%s\n' "$msg" | sed -n 's/^evidence: *//p' | head -1)"
133
+
134
+ [ "$tag_head" = "$head_arg" ] || fail stale
135
+ [ "$tag_evidence" = "$evidence_arg" ] || fail evidence-mismatch
136
+
137
+ # --- 4. all checks passed -----------------------------------------------------
138
+ printf 'ok\n'
139
+ exit 0
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env sh
2
+ # wu-done — minimal fold stub wiring the `done` predicate to verify-signoff.
3
+ #
4
+ # Contract M0 (D2): "Wherever the program's fold computes work-unit state,
5
+ # `done` is true iff `verify-signoff <wu-id>` exits 0." Forge's own repo has
6
+ # no executable fold (workflow state is derived by the verifying/reviewing
7
+ # SKILLs, not by a code module) — this script IS the fold, scoped to exactly
8
+ # this predicate. A project with a real fold (e.g. pi-forge's package/stream
9
+ # state) wires its own `done` computation to `verify-signoff.sh` the same way
10
+ # this script does; it does not need to shell out to this file specifically.
11
+ #
12
+ # See docs/sign-off-convention.md. `current.human_verified` in a milestone
13
+ # state file is a DERIVED ECHO of this predicate for human readability — it
14
+ # is never the source of truth, and this script never reads or writes it.
15
+ #
16
+ # wu-done.sh <wu-id> [same flags as verify-signoff.sh]
17
+ #
18
+ # stdout: `done=true` or `done=false reason=<code>`. Exit 0 iff done=true —
19
+ # same convention as verify-signoff.sh, so `wu-done.sh <id> && ...` composes.
20
+ set -eu
21
+
22
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
23
+ VERIFY="$SCRIPT_DIR/verify-signoff.sh"
24
+
25
+ [ $# -ge 1 ] || { printf 'usage: wu-done.sh <wu-id> [verify-signoff flags]\n' >&2; exit 2; }
26
+
27
+ # Only stdout is captured (the reason= line); verify-signoff's stderr detail
28
+ # flows straight through to our own stderr so a bad-invocation usage message
29
+ # is never swallowed. The `if` guards the assignment from `set -e` — under
30
+ # errexit a bare `out=$(...); rc=$?` would abort THIS script the instant
31
+ # verify-signoff exits non-zero, before rc=$? ever ran.
32
+ if out="$("$VERIFY" "$@")"; then rc=0; else rc=$?; fi
33
+
34
+ if [ "$rc" -eq 0 ]; then
35
+ printf 'done=true\n'
36
+ exit 0
37
+ fi
38
+
39
+ if [ "$rc" -eq 1 ]; then
40
+ printf 'done=false %s\n' "$out"
41
+ exit 1
42
+ fi
43
+
44
+ # rc 2 (or anything else): bad invocation, not a verification verdict — the
45
+ # fold could not even ask the question, so it must not report false as if it
46
+ # had. verify-signoff already printed the usage error to stderr; just
47
+ # propagate the same exit code.
48
+ exit "$rc"
@@ -86,9 +86,17 @@ linked milestone, emit `stream: implicit` coverage rows, derive `merge_queue` fr
86
86
  hand-edit `active.yml`. Beyond that deterministic write, the Chief adds two
87
87
  **interactive** steps the boot-time rollup omits:
88
88
 
89
- - **Deferred/complete linked milestone**when a stream's `stream.milestone`
90
- points at a `deferred`/`complete` milestone, surface it: the stream should
91
- likely pause offer it.
89
+ - **Complete linked milestone → auto-close (not a prompt).** The rollup's **step
90
+ 0 sweep** (FORGE.md Stream Rollup) already ran: a stream whose
91
+ `stream.milestone` is `complete`, that is not already `closed`, with no live
92
+ worktree, was auto-closed to `status: closed` / `merge.readiness: merged` before
93
+ the join — so it never reaches Merge Cadence as a false "ready to merge". Just
94
+ report it: *"auto-closed stale stream {id} — milestone m-{id} complete, on main."*
95
+ (A `complete` milestone that still has a live worktree is left for forge-boot's
96
+ finalize path.)
97
+ - **Deferred linked milestone** → when a stream's `stream.milestone` points at a
98
+ `deferred` milestone, surface it: the stream should likely **pause** (not close —
99
+ the milestone can resume) — offer it.
92
100
  - **Implicit row needs coordination** → when an active milestone has no stream
93
101
  file (the rollup emits a `stream: implicit` row for coverage), offer to
94
102
  materialize a stream file for it. **Never auto-create.**
@@ -10,19 +10,21 @@ Read-only. Surface all deferred work across three storage surfaces so user need
10
10
 
11
11
  Three surfaces:
12
12
 
13
- 1. **Milestones** — `.forge/state/index.yml` entries with `status: deferred`. Strategic punts (whole milestone frozen).
13
+ 1. **Milestones** — milestones whose derived `status` is `deferred` (from `.forge/state/milestone-*.yml`; `index.yml` is a render-on-read cache of the same). Strategic punts (whole milestone frozen).
14
14
  2. **Refactor backlog** — `.forge/refactor-backlog.yml` items with `status: deferred`. Code-quality punts.
15
15
  3. **Inside active milestones** — phase/task entries with `status: deferred` or `deferred: true` inside `.forge/state/milestone-*.yml`. Tactical punts within in-flight work.
16
16
 
17
17
  ## Step 1: Read three surfaces
18
18
 
19
- **Surface 1Deferred milestones.** Read `.forge/state/index.yml`. Collect every entry under `milestones:` where `status: deferred`. Pull `id`, `name`, `lifecycle.deferred_at` (if present at top level fall back to `last_updated`), `lifecycle.deferred_reason` (may live in the milestone state file read `.forge/state/milestone-{id}.yml → lifecycle.deferred_at / deferred_reason` for accuracy).
19
+ **Render-on-read (0.53.0).** `.forge/state/index.yml` is a **git-ignored derived cache**, not a committed file it may be **absent** (fresh clone, no boot yet) or **stale**. Never read it as authoritative and never treat its absence as "not a Forge project". Derive each milestone's `id`/`name`/`status` from its source, `.forge/state/milestone-{id}.yml` (the same inputs the `forge` Rollup uses): `status` = `deferred` if `lifecycle.deferred_at` is set and not superseded by a later `resumed_at`, else `complete`/`not_started` from `current.status`, else `active`. (If `index.yml` is present and you trust it fresh, it is a valid fast-path — but the milestone files are the source of truth here.) The project sentinel is `.forge/project.yml` (below).
20
+
21
+ **Surface 1 — Deferred milestones.** Glob `.forge/state/milestone-*.yml` (or read a fresh `.forge/state/index.yml` cache) and collect every milestone whose derived `status` is `deferred`. Pull `id`, `name`, `lifecycle.deferred_at` (fall back to `current.last_updated`), `lifecycle.deferred_reason` (read `.forge/state/milestone-{id}.yml → lifecycle.deferred_at / deferred_reason` for accuracy).
20
22
 
21
23
  **Surface 2 — Deferred refactor items.** Read `.forge/refactor-backlog.yml`. Collect every item with `status: deferred`. Pull `id` (e.g. R-007), `description` (first line, truncate ≤80 chars), `deferred_at`, `deferred_reason`.
22
24
 
23
25
  **Surface 3 — Deferred phases/tasks inside active milestones.** Glob `.forge/state/milestone-*.yml`. For each file:
24
26
 
25
- - Look up that milestone's status in `index.yml`.
27
+ - Determine that milestone's derived status from its `milestone-{id}.yml` (`current.status` / `lifecycle`), not a possibly-absent `index.yml`.
26
28
  - **Only process if `status: active`.** Skip `complete` and `deferred` milestones.
27
29
  - *Why:* a deferred milestone's own `lifecycle.deferred_at` block is already counted by Surface 1. Including it here double-counts. Skipping `complete` avoids historical noise.
28
30
  - Within the file, find phase or task entries with `status: deferred` or `deferred: true`. Pull milestone id, phase/task name, `deferred_at`, `deferred_reason`.
@@ -71,7 +73,9 @@ Then exit.
71
73
 
72
74
  ## Graceful degradation
73
75
 
74
- - Missing `.forge/state/index.yml` → cannot proceed. Print `No forge state found in this project.` exit.
76
+ - Missing `.forge/project.yml` → not a Forge project. Print `No forge state found in this project.` and exit. (Key the sentinel on `project.yml`, a committed file — **not** on `.forge/state/index.yml`, which since 0.53.0 is a git-ignored render-on-read cache and is legitimately absent on a fresh clone until the first `/forge` boot renders it.)
77
+ - Missing/stale `.forge/state/index.yml` → it is a derived cache, not a source. Derive milestone statuses from `.forge/state/milestone-*.yml` directly (or run a `/forge` boot to render the cache). Never error on its absence.
78
+ - No `.forge/state/milestone-*.yml` matches → Surface 1 renders `(none)`. Continue.
75
79
  - Missing `.forge/refactor-backlog.yml` → Surface 2 renders `(none)`. Continue.
76
80
  - No `milestone-*.yml` matches → Surface 3 renders `(none)`. Continue.
77
81
  - Malformed YAML in any source → skip that source, render `(unreadable)` beneath header, continue others.
@@ -20,6 +20,7 @@ Structured conversation: approach, trade-offs, decisions. Clarity, not artifacts
20
20
  - No plans or code
21
21
  - Writes `context.md` progressively (after each confirmed decision, not just at handoff)
22
22
  - No phase/plan required
23
+ - Entered via a worktree already, for new work (R10, `forge/SKILL.md` Step 3) — if this skill is somehow invoked directly on the main checkout for new work, enter one now before writing anything.
23
24
 
24
25
  ## Progressive Persistence
25
26
 
@@ -307,8 +308,9 @@ If no milestone exists (advisory discussion — forge routed here without tier/m
307
308
  - Multi-file, refactor, feature, service changes → **Standard**
308
309
  - Major architectural, multi-subsystem, multi-phase → **Full**
309
310
  3. **Create milestone.**
310
- - Next ID = (max id across existing `.forge/state/milestone-*.yml`) + 1 (or 1 if none)
311
- - Create `milestone-{id}.yml` from `.forge/templates/state/milestone.yml`:
311
+ - Allocate the milestone id via `id=$(.claude/hooks/forge-reserve.sh milestone --milestone m-new)` (ADR-023) — never scan `milestone-*.yml` for max+1 inline (collides across concurrent worktrees; the helper is ledger-backed and scans live + archived milestones).
312
+ - **Write the declaration** (R10/B3' — FORGE.md → Stream Integration Checkpoints, "Producer — publish at intake") — **Standard/Full only; SKIP for Quick** (a quick fix registers no stream, matching context.md M28; it routes to quick-tasking at step 5): copy `.forge/templates/streams/stream.yml` → `.forge/streams/{stream}.yml`, populate `stream.id`, `stream.goal` (one-line summary of the discussion topic), `stream.milestone: m-{id}`, `stream.status: draft`, `stream.tier` = detected tier, `runtime.branch`/`runtime.worktree` from the worktree in use, best-known `ownership.owned_surfaces`/`shared_surfaces` (coarse is fine — claims may go stale as scope evolves; amend later). Commit it — this should be the worktree's first commit if `forge` boot's R10 step already entered one; if this skill is somehow reached before that (direct invoke), enter the worktree now, then commit the declaration. Fast-forward-push it per the FORGE.md mechanic just referenced — do not re-explain the git plumbing here.
313
+ - Create `milestone-{id}.yml` from `.forge/templates/state/milestone.yml` — **worktree-only from here, landing at this milestone's own checkpoint, not pushed immediately**:
312
314
  - `milestone.id` = {id}, `milestone.name` = brief summary of discussion topic
313
315
  - `current.tier` = detected tier
314
316
  - `current.status` = `planning` (Standard) / `architecting` (Full) / `not_started` (Quick)
@@ -36,9 +36,9 @@ Run **before the first task begins**. Makes failure causality mechanical — no
36
36
  1. Run all non-advisory verification commands
37
37
  2. Record which tests/checks pass and which fail — this is the baseline
38
38
  3. After each commit: failures **not in baseline** = introduced by this task = **must fix under Rule 3**
39
- 4. Failures **present in baseline** = pre-existing → append to `.forge/deferred-issues.md`, mark `status: pending`
39
+ 4. Failures **present in baseline** = pre-existing → log a file under `.forge/deferred-issues/`, `status: pending` (see Deferred Issues Format)
40
40
 
41
- Skip only if re-entering an in-progress execution and `deferred-issues.md` already documents all current failures.
41
+ Skip only if re-entering an in-progress execution and `.forge/deferred-issues/` (or legacy `deferred-issues.md`) already documents all current failures.
42
42
 
43
43
  ## Plan Anchor Re-Derivation
44
44
 
@@ -63,7 +63,7 @@ Execution-phase operational guidance below supplements the rules — it does not
63
63
  3 auto-fix attempts on a single task → STOP. Document remaining issues. Move to next task.
64
64
 
65
65
  ### Scope Boundary
66
- Only fix issues DIRECTLY caused by the current task. Pre-existing warnings, tech debt, unrelated bugs → log to `.forge/deferred-issues.md`.
66
+ Only fix issues DIRECTLY caused by the current task. Pre-existing warnings, tech debt, unrelated bugs → log a file under `.forge/deferred-issues/`.
67
67
 
68
68
  ### Slice Integrity (Execution-Side)
69
69
 
@@ -203,14 +203,14 @@ For each command, in order:
203
203
  Attempt 1:
204
204
  1. Read error output
205
205
  2. In baseline snapshot?
206
- - YES (pre-existing) → mark advisory for this session; append to .forge/deferred-issues.md; continue
206
+ - YES (pre-existing) → mark advisory for this session; log a file under .forge/deferred-issues/; continue
207
207
  - NO (introduced by this task) → fix code, stage fixes, amend commit
208
208
  3. Re-run command
209
209
  4. Pass → next command
210
210
  5. Fail → next attempt (up to max_retries)
211
211
 
212
212
  After max_retries exhausted:
213
- Append to .forge/deferred-issues.md
213
+ Log a file under .forge/deferred-issues/
214
214
  → Log failure in execution summary
215
215
  → Continue to next task
216
216
  ```
@@ -219,26 +219,30 @@ After max_retries exhausted:
219
219
  Verification retries count toward the task's 3-strike limit. 2 strikes used = 1 verification retry max.
220
220
 
221
221
  ### Do NOT Fix
222
- - **Pre-existing failures** present in baseline snapshot → mark advisory; append to `.forge/deferred-issues.md`
222
+ - **Pre-existing failures** present in baseline snapshot → mark advisory; log a file under `.forge/deferred-issues/`
223
223
  - **Flaky tests** passing on re-run without changes → note in summary, no strike
224
224
  - **Unrelated warnings** (deprecation, non-blocking lint) → ignore
225
225
 
226
226
  ### Deferred Issues Format
227
227
 
228
- Append one entry per failure to `.forge/deferred-issues.md` (create if missing):
228
+ **One file per failure** (ADR-023 — no DI-NNN counter). Copy `.forge/templates/deferred-issue.md`
229
+ → `.forge/deferred-issues/{YYYY-MM-DD}-{milestone}-{slug}.md`, where `{slug}` is a short kebab of
230
+ the failure (e.g. `redis-unavailable`). Distinct filenames mean concurrent worktrees never collide —
231
+ the same anti-collision property as `.forge/state/desire-paths/`.
229
232
 
230
233
  ```yaml
231
- issues:
232
- - id: DI-001
233
- type: test_failure | lint_failure | build_failure
234
- command: "npm test"
235
- summary: "HealthEndpointTest — Redis unavailable in test env"
236
- first_seen: "2026-04-14"
237
- milestone: "m3"
238
- status: pending
234
+ # .forge/deferred-issues/2026-04-14-m3-redis-unavailable.md
235
+ type: test_failure # test_failure | lint_failure | build_failure
236
+ command: "npm test"
237
+ summary: "HealthEndpointTest — Redis unavailable in test env"
238
+ first_seen: "2026-04-14"
239
+ milestone: "m3"
240
+ status: pending
239
241
  ```
240
242
 
241
- ID = `DI-{N}` where N = next integer after existing max (or 001 if new file).
243
+ **Legacy read (lazy migration):** if `.forge/deferred-issues/` is absent, the old single-file
244
+ `.forge/deferred-issues.md` (DI-NNN `issues[]` entries) is still read — no data loss before migration.
245
+ Never allocate a DI-NNN number; the filename slug is the id.
242
246
 
243
247
  ### Quick Tier
244
248
  Run all non-advisory verification commands once after commit. 1 retry max.
@@ -307,7 +311,7 @@ After completing all tasks in a plan:
307
311
 
308
312
  ## Notes
309
313
  [Genuine handoff context only: environment requirements, seed data, external services needed (e.g. "Redis must be running for auth tests").
310
- Do NOT list test failures here — pre-existing failures belong in deferred-issues.md; task-introduced failures must be fixed before committing.]
314
+ Do NOT list test failures here — pre-existing failures belong in `.forge/deferred-issues/`; task-introduced failures must be fixed before committing.]
311
315
  ```
312
316
 
313
317
  ## State Updates
@@ -65,13 +65,13 @@ Cross-machine note: the flag file is machine-local, so a worktree (or the primar
65
65
  - **Absent registry, but IDs already exist.** `.forge/reservations.yml` missing *and* the tree already holds allocated IDs (`requirements/*.yml` has `FR-`/`NFR-`/`DEF-`, or `.forge/decisions/` has `ADR-NNN`) → one-time nudge: *"No `reservations.yml` yet, but this project already allocates IDs. Concurrent worktrees will collide on the next number. Create it by reserving your next ID via the protocol (`planning`/`architecting` do this), or backfill existing IDs now."* (No IDs anywhere → silent; the first reservation creates the file lazily.)
66
66
  - **Backfill scan (un-reserved landed IDs).** When `reservations.yml` exists, diff the IDs actually landed in the tree (`FR-`/`NFR-`/`DEF-` across `requirements/*.yml`; `ADR-NNN` across `.forge/decisions/`) against the ids recorded in `reservations.yml`. Any landed ID with **no** reservation entry → list them once: *"{N} ID(s) are in the tree but not in `reservations.yml` ({ids}) — landed before the protocol or via an un-reserved path. They are still respected (next = max(reserved, in-tree)+1), but appending backfill entries keeps the registry the single audit trail."* Advisory: surface, don't auto-write — backfilling is the operator's call.
67
67
 
68
- **Stream Rollup (active.yml is derived).** If `.forge/streams/` has any stream files, regenerate `.forge/streams/active.yml` from them + active milestones per FORGE.md → Stream Rollup, the same way step 1.0 regenerates `index.yml`. **Run the 1.0 State Rollup (below) first** so `index.yml` is fresh — the Stream Rollup reads active milestones from it for implicit-row coverage, so on a boot where a just-merged milestone promotion lands, a stale `index.yml` would silently drop that milestone's coverage row. `active.yml` is derived — never read it as authoritative without rolling up first, and never hand-edit it. Runs in the main checkout (worktrees never write `active.yml`). No `streams/` dir → skip.
68
+ **Stream Rollup (active.yml is derived).** If `.forge/streams/` has any stream files, regenerate `.forge/streams/active.yml` from them + active milestones per FORGE.md → Stream Rollup, the same way step 1.0 regenerates `index.yml`. **Run the 1.0 State Rollup (below) first** so `index.yml` is fresh — the Stream Rollup reads active milestones from it for implicit-row coverage, so on a boot where a just-merged milestone promotion lands, a stale `index.yml` would silently drop that milestone's coverage row. This includes the rollup's **step 0 completed-stream auto-close sweep** (FORGE.md → Stream Rollup): a milestone-backed stream whose milestone is now `complete`, that is not already `closed`, and has no live worktree, is auto-closed (`status: closed`, `merge.readiness: merged`) *before* the join — so a done-and-merged milestone can never leave a zombie `ready` row that re-nags every boot. Announce each: *"auto-closed stale stream {id} — milestone m-{id} complete, already on main."* (A `complete` milestone that still has a live worktree is left for the finalize path below, not swept.) `active.yml` is derived — never read it as authoritative without rolling up first, and never hand-edit it. Since 0.53.0 it is a **git-ignored render-on-read cache** (never committed), so rendering it is a local-cache write that never lands on `main`; the source writes it still performs (the step-0 auto-close sweep touches *stream files*, which remain committed) run in the main checkout as before. No `streams/` dir → skip.
69
69
 
70
- **Ready-to-merge nudge (main checkout / streams without checkpoints).** From the just-regenerated `active.yml`, scan for streams with `coordination: ready` or a non-empty `merge_queue` **that are not auto-publishing via checkpoints**. Any present one line: *"{N} stream(s) ready to merge — run `/chief-of-staff` (merge safe) to integrate."* Pointer only — the cadence logic lives in the Chief's Merge Cadence Check; `forge` never merges. Nothing pending → silent.
70
+ **Ready-to-merge nudge (main checkout / streams without checkpoints).** From the just-regenerated `active.yml` — already swept of completed-milestone zombies by the rollup's step 0 — scan for streams with `coordination: ready` or a non-empty `merge_queue` **that are not auto-publishing via checkpoints**. Because the sweep ran first, anything surfaced here is genuinely mergeable (open work, not yet on main), so the nudge is truthful: *"{N} stream(s) ready to merge — run `/chief-of-staff` (merge safe) to integrate."* Pointer only — the cadence logic lives in the Chief's Merge Cadence Check; `forge` never merges. Nothing pending → silent.
71
71
 
72
72
  ### 1.0 State Rollup (index.yml is derived)
73
73
 
74
- `index.yml` is a **derived registry** — regenerate it from the milestone files before reading, and after any milestone CRUD (promote/defer/resume/delete). **Never hand-edit `index.yml`.**
74
+ `index.yml` is a **derived registry** — regenerate it from the milestone files before reading, and after any milestone CRUD (promote/defer/resume/delete). **Never hand-edit `index.yml`.** Since 0.53.0 it is a **git-ignored render-on-read cache** (never committed): rendering it here is a local-cache write, so it never conflicts and never needs the orchestrator-only guard for *safety* — any session may render its own copy. The rollup below is unchanged; only the storage is (see FORGE.md → State Ownership, Derived class).
75
75
 
76
76
  Rollup procedure (deterministic + idempotent):
77
77
  1. Glob `.forge/state/milestone-*.yml`.
@@ -176,16 +176,17 @@ Check `.forge/refactor-backlog.yml`:
176
176
  **Promotion procedure (effort: standard only):**
177
177
 
178
178
  1. Read backlog item `id` (e.g. `R61-002`). Synthesize milestone id `m-{R-id}` (e.g. `m-R61-002`).
179
- 2. Copy `.forge/templates/state/milestone.yml` → `.forge/state/milestone-{m-R-id}.yml`. Populate:
179
+ 2. **Enter the worktree first (R10 — this runs in Step 1, *before* Step 3's routing gate fires, so do not assume a worktree already exists):** if this session is still on the main checkout (`git rev-parse --git-dir` == `--git-common-dir`), `EnterWorktree` now; if R10 already entered one, this is simply the first write in it. Then write the declaration (`.forge/templates/streams/stream.yml` → `.forge/streams/{stream}.yml` — `stream.goal`, `stream.milestone: m-{R-id}`, `stream.status: draft`, `stream.tier: standard`, `runtime.branch`/`runtime.worktree` from the worktree just entered, best-known `ownership.owned_surfaces`/`shared_surfaces`) as the worktree's first commit and fast-forward-push it, per FORGE.md's Stream Integration Checkpoints intake-declaration trigger.
180
+ 3. Copy `.forge/templates/state/milestone.yml` → `.forge/state/milestone-{m-R-id}.yml`. Populate:
180
181
  - `milestone.id: m-{R-id}`
181
182
  - `milestone.name: "Promoted from {R-id}: {backlog item title}"`
182
183
  - `milestone.origin: {R-id}`
183
184
  - `current.tier: standard`, `current.status: researching`
184
185
  - `current.last_updated: {ISO date}`
185
- 3. Run **Rollup (1.0)** — regenerates `index.yml` from the new milestone file (do not hand-append the registry entry). New milestone id is `m-{R-id}` from step 1; for numeric ids, allocate by scanning existing `milestone-*.yml` for the max id + 1.
186
- 4. Append to `.forge/roadmap.yml` — one milestone entry + one phase (`refactor-{R-id}`, `requirements_source: refactor-backlog.yml#{R-id}`, `dependencies: []`).
187
- 5. Update `.forge/refactor-backlog.yml` item: `status: in_progress`, add `promoted_to: m-{R-id}`.
188
- 6. Confirm: *"Promoted {R-id} → milestone m-{R-id}. State + roadmap created. Routing to researching."*
186
+ 4. Run **Rollup (1.0)** — regenerates `index.yml` from the new milestone file (do not hand-append the registry entry). New milestone id is `m-{R-id}` from step 1; for numeric ids, allocate via `id=$(.claude/hooks/forge-reserve.sh milestone --milestone m-new)` (ADR-023) — never scan `milestone-*.yml` for max+1 inline. The three milestone-id call sites (here, `discussing`, `prototyping`) share this ledger-backed allocator so they cannot disagree, and it scans live **+ archived** milestones so a burned id is never reused.
187
+ 5. Append to `.forge/roadmap.yml` — one milestone entry + one phase (`refactor-{R-id}`, `requirements_source: refactor-backlog.yml#{R-id}`, `dependencies: []`).
188
+ 6. Update `.forge/refactor-backlog.yml` item: `status: in_progress`, add `promoted_to: m-{R-id}`.
189
+ 7. Confirm: *"Promoted {R-id} → milestone m-{R-id}. State + roadmap created. Routing to researching."*
189
190
 
190
191
  ```text
191
192
  backlog pickup → effort: standard
@@ -357,6 +358,8 @@ Tier + state → invoke via `Skill` tool. All phases use `Skill()`.
357
358
 
358
359
  **CRITICAL: NEVER `EnterPlanMode`.** "Planning" = `Skill(planning)`. Native plan mode writes wrong format, bypasses gates + state.
359
360
 
361
+ **CRITICAL: New work enters its worktree before anything else (R10).** When routing genuinely NEW work — `current.status: not_started` about to detect tier and start, or any session about to invoke `researching`/`discussing`/`planning`/`quick-tasking`/`prototyping` for a request that has no milestone yet — call `EnterWorktree` (Worktree Convention, FORGE.md) BEFORE invoking that skill. The main checkout is for orchestration and reads. Applies to every tier, including Quick and Prototyping: one `EnterWorktree` call, no heavier ceremony — a Quick fix or a disposable mockup gets a worktree but no milestone/stream declaration (nothing is being registered until, for a prototype, it graduates). A mockup is disposable but not weightless — it writes flag-gated app code + a prototype state file the team shares and may fold into a feature-toggled section — so its initial entry is worktree-first like the rest. Earlier practice held off on worktree isolation until implementation began, on the theory that research/discussion/planning don't touch code — that theory doesn't hold: discussing/planning/researching write `.forge` state too (context.md, plans, research notes), and per P17 those writes left untracked droppings on main that broke other worktrees' landings. No phase may hold off on worktree entry for new work. Milestone/stream intake itself is a deliberate exception with its own mechanic — see FORGE.md → Stream Integration Checkpoints for the declaration/content split (B3'). Pure read-only sessions that start no work need no worktree. This governs the FIRST write of new work; resuming an in-flight milestone already routes through the Live-Worktree Ownership Gate (1.1a) / branch-anchor path (Step 1.1) instead.
362
+
360
363
  **Experimental — M10 orchestration backend.** Chief/Streams is the preferred
361
364
  user-facing orchestration model. M10 remains an optional backend for streams
362
365
  that need worktree isolation, merge-queue discipline, or experimental file
@@ -364,7 +367,7 @@ claims. Route through `Skill(orchestrating)` first when **all** hold:
364
367
  - **Selected:** Project Chief chooses M10 for a stream, user explicitly asks for multi-agent/worktree mode, or legacy `orchestration.auto` is enabled.
365
368
  - **Installed:** `.claude/skills/orchestrating/` present + `forge-orchestrator` in `.mcp.json` + `.claude/hooks/forge-claim-check.sh` present.
366
369
  - **Not opted out:** `orchestration.auto` in `project.yml` ≠ `false` for legacy auto-routing; explicit Project Chief selection can still use M10 when installed.
367
- - **Tier is Standard or Full.** Quick never auto-orchestrates (a worktree per typo is the wrong trade).
370
+ - **Tier is Standard or Full.** Quick never auto-orchestrates — the *orchestration backend* (MCP file-claims, merge queue, bootstrap) is the wrong trade for a typo. (The plain worktree is not the cost: per R10 a Quick fix already runs in one — `EnterWorktree` is ~instant. What Quick skips here is the M10 machinery on top, not the worktree.)
368
371
  - **Not already in a session:** active milestone has no `lifecycle.worktree_mode: active|degraded` (don't re-bootstrap inside a live session).
369
372
 
370
373
  `orchestrating` bootstrap stays the safety net — incompatible repo → it refuses, writes `worktree_mode: refused`, and `forge` continues single-agent. Absent install → standard routing. Manual `Skill(orchestrating)` still works for explicit backend control; set `orchestration.auto: false` in `project.yml` to opt out of legacy auto-routing.
@@ -411,7 +414,7 @@ Where `{source}` = `skills.{name}` | `models.default` | `parent session`. Suppre
411
414
 
412
415
  | `current.status` | Route To |
413
416
  |-------------------|----------|
414
- | `not_started` | Detect tier, start |
417
+ | `not_started` | EnterWorktree (R10), then detect tier, start |
415
418
  | `researching` | `Skill(researching)` → discussing |
416
419
  | `discussing` | `Skill(discussing)` → planning (or architecting if Full) |
417
420
  | `architecting` | `Skill(architecting)` → planning |