forge-orkes 0.50.1 → 0.53.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.
- package/package.json +1 -1
- package/template/.claude/hooks/forge-reserve.sh +51 -15
- package/template/.claude/hooks/tests/verify-signoff.test.sh +122 -0
- package/template/.claude/hooks/tests/wu-done.test.sh +68 -0
- package/template/.claude/hooks/verify-signoff.sh +139 -0
- package/template/.claude/hooks/wu-done.sh +48 -0
- package/template/.claude/skills/chief-of-staff/SKILL.md +11 -3
- package/template/.claude/skills/deferred/SKILL.md +8 -4
- package/template/.claude/skills/discussing/SKILL.md +4 -2
- package/template/.claude/skills/executing/SKILL.md +21 -17
- package/template/.claude/skills/forge/SKILL.md +13 -10
- package/template/.claude/skills/planning/SKILL.md +3 -1
- package/template/.claude/skills/prototyping/SKILL.md +10 -7
- package/template/.claude/skills/quick-tasking/SKILL.md +3 -1
- package/template/.claude/skills/researching/SKILL.md +2 -0
- package/template/.claude/skills/reviewing/SKILL.md +7 -7
- package/template/.claude/skills/verifying/SKILL.md +8 -7
- package/template/.forge/FORGE.md +14 -7
- package/template/.forge/gitignore +8 -0
- package/template/.forge/migrations/0.52.0-id-reservation-milestone-refactor.md +45 -0
- package/template/.forge/templates/deferred-issue.md +22 -0
package/package.json
CHANGED
|
@@ -51,8 +51,10 @@ case "$kind" in
|
|
|
51
51
|
def) prefix="DEF" ;;
|
|
52
52
|
fr) prefix="FR" ;;
|
|
53
53
|
nfr) prefix="NFR" ;;
|
|
54
|
-
""
|
|
55
|
-
|
|
54
|
+
milestone) prefix="M" ;; # ADR-023 — unpadded (M-26); ids used bare across the framework
|
|
55
|
+
refactor) prefix="R" ;; # ADR-023 — zero-padded R0NN, matching the backlog format
|
|
56
|
+
"") printf 'forge-reserve: missing kind (adr|def|fr|nfr|milestone|refactor)\n' >&2; exit 2 ;;
|
|
57
|
+
*) printf 'forge-reserve: unknown kind: %s (expected adr|def|fr|nfr|milestone|refactor)\n' "$kind" >&2; exit 2 ;;
|
|
56
58
|
esac
|
|
57
59
|
[ -n "$milestone" ] || { printf 'forge-reserve: --milestone <id> is required\n' >&2; exit 2; }
|
|
58
60
|
|
|
@@ -99,33 +101,67 @@ trap 'rmdir "$lock" 2>/dev/null || true' EXIT INT TERM
|
|
|
99
101
|
# Everything numeric goes through awk (n+0) so leading-zero ids like 021 are read
|
|
100
102
|
# as decimal 21, never octal — no shell $(( )) octal trap.
|
|
101
103
|
|
|
102
|
-
# (a) ledger — same-machine siblings, INCLUDING uncommitted (the ADR-016-blind input)
|
|
104
|
+
# (a) ledger — same-machine siblings, INCLUDING uncommitted (the ADR-016-blind input).
|
|
105
|
+
# Extract the trailing number by stripping non-digits, NOT by split on "-": refactor
|
|
106
|
+
# ids (R100) carry no hyphen (ADR-023), so a hyphen-split would read them as 0 and
|
|
107
|
+
# reallocate a live id. gsub handles ADR-014, M-26, R100, FR-023 uniformly.
|
|
103
108
|
ledger_max=0
|
|
104
109
|
if [ -f "$ledger" ]; then
|
|
105
110
|
ledger_max="$(awk -F'\t' -v k="$kind" \
|
|
106
|
-
'$1==k {
|
|
111
|
+
'$1==k { x=$2; gsub(/[^0-9]/,"",x); n=x+0; if(n>m)m=n } END{print m+0}' "$ledger")"
|
|
107
112
|
fi
|
|
108
113
|
|
|
109
114
|
# (b) in-tree — landed ids in THIS worktree. Exact-prefix match in awk ($1==p) so
|
|
110
|
-
# FR does not swallow NFR (FR is a substring of NFR).
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
# FR does not swallow NFR (FR is a substring of NFR). milestone + refactor (ADR-023)
|
|
116
|
+
# each scan BOTH live and archive locations so a burned id is never reallocated.
|
|
117
|
+
case "$kind" in
|
|
118
|
+
adr)
|
|
119
|
+
intree_max="$(ls "$top/docs/decisions" 2>/dev/null \
|
|
120
|
+
| awk -F- '/^ADR-[0-9]/ { n=$2+0; if(n>m)m=n } END{print m+0}')"
|
|
121
|
+
;;
|
|
122
|
+
milestone)
|
|
123
|
+
# live state/milestone-<N>.yml ∪ archived .forge/archive/milestone-<N>/
|
|
124
|
+
intree_max="$( { ls "$top/.forge/state"/milestone-*.yml 2>/dev/null; \
|
|
125
|
+
ls -d "$top/.forge/archive"/milestone-*/ 2>/dev/null; } \
|
|
126
|
+
| grep -oE 'milestone-[0-9]+' \
|
|
127
|
+
| awk -F- '{ n=$2+0; if(n>m)m=n } END{print m+0}')"
|
|
128
|
+
;;
|
|
129
|
+
refactor)
|
|
130
|
+
# live refactor-backlog.yml ∪ refactor-backlog-archive.yml
|
|
131
|
+
intree_max="$(grep -hoE 'R[0-9]+' \
|
|
132
|
+
"$top/.forge/refactor-backlog.yml" \
|
|
133
|
+
"$top/.forge/refactor-backlog-archive.yml" 2>/dev/null \
|
|
134
|
+
| awk '{ n=substr($0,2)+0; if(n>m)m=n } END{print m+0}')"
|
|
135
|
+
;;
|
|
136
|
+
*)
|
|
137
|
+
intree_max="$(grep -rhoE '[A-Z]+-[0-9]+' "$top/.forge/requirements/" 2>/dev/null \
|
|
138
|
+
| awk -F- -v p="$prefix" '$1==p { n=$2+0; if(n>m)m=n } END{print m+0}')"
|
|
139
|
+
;;
|
|
140
|
+
esac
|
|
118
141
|
|
|
119
142
|
# (c) main:reservations.yml — cross-machine / cold-start floor (survives a
|
|
120
143
|
# .git/forge wipe; carries another machine's MERGED reservations). Absent → 0.
|
|
144
|
+
# Hyphen is OPTIONAL in the token ([A-Z]+-?[0-9]+): refactor ids (R100) carry none
|
|
145
|
+
# (ADR-023), so requiring a hyphen would hide them and drop the floor to 0. Split the
|
|
146
|
+
# alpha prefix from the digits in awk so R does not match e.g. FR (anchor ^prefix$).
|
|
121
147
|
main_max="$(git show main:.forge/reservations.yml 2>/dev/null \
|
|
122
|
-
| grep -oE '[A-Z]
|
|
123
|
-
| awk -
|
|
148
|
+
| grep -oE '[A-Z]+-?[0-9]+' \
|
|
149
|
+
| awk -v p="$prefix" '{ a=$0; sub(/[-0-9].*$/,"",a); d=$0; gsub(/[^0-9]/,"",d);
|
|
150
|
+
if(a==p){ n=d+0; if(n>m)m=n } } END{print m+0}')"
|
|
124
151
|
[ -n "$main_max" ] || main_max=0
|
|
125
152
|
|
|
126
153
|
next="$(awk -v a="$ledger_max" -v b="$intree_max" -v c="$main_max" \
|
|
127
154
|
'BEGIN{m=a; if(b>m)m=b; if(c>m)m=c; print m+1}')"
|
|
128
|
-
id
|
|
155
|
+
# Per-kind id format (ADR-023). The prefix-hyphen-paddednumber form (ADR-021's
|
|
156
|
+
# ADR-014, FR-023) is NOT universal:
|
|
157
|
+
# milestone → M-26 : hyphen, UNPADDED — ids are used bare (milestone-26.yml, m-26)
|
|
158
|
+
# refactor → R064 : NO hyphen, zero-padded 3-wide — matches the backlog convention
|
|
159
|
+
# adr/def/fr/nfr → ADR-014 : hyphen, zero-padded 3-wide (unchanged)
|
|
160
|
+
case "$kind" in
|
|
161
|
+
milestone) id="$(printf '%s-%d' "$prefix" "$next")" ;;
|
|
162
|
+
refactor) id="$(printf '%s%03d' "$prefix" "$next")" ;;
|
|
163
|
+
*) id="$(printf '%s-%03d' "$prefix" "$next")" ;;
|
|
164
|
+
esac
|
|
129
165
|
|
|
130
166
|
# --- 6. dual-write (still under the lock) -----------------------------------
|
|
131
167
|
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
@@ -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
|
-
- **
|
|
90
|
-
|
|
91
|
-
|
|
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`
|
|
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
|
-
**
|
|
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
|
-
-
|
|
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/
|
|
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
|
-
-
|
|
311
|
-
-
|
|
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 →
|
|
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
|
|
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;
|
|
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
|
-
→
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
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` |
|
|
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 |
|
|
@@ -7,6 +7,8 @@ description: "Break work into executable tasks with verification gates. Enforces
|
|
|
7
7
|
|
|
8
8
|
> **Do NOT use `EnterPlanMode`.** Output -> `.forge/phases/`.
|
|
9
9
|
|
|
10
|
+
> 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.
|
|
11
|
+
|
|
10
12
|
## Core Principle: Vertical Slicing
|
|
11
13
|
|
|
12
14
|
**Every phase and every plan MUST deliver a thin vertical slice -- a user-observable behavior reachable end-to-end (UI -> API -> data, or CLI -> core -> output).** Never decompose by horizontal layer (all models, then all APIs, then all UI). Horizontal slicing defers user-testable behavior until the last phase and amplifies integration risk.
|
|
@@ -72,7 +74,7 @@ or active context summary; do not copy the historical block back into
|
|
|
72
74
|
|
|
73
75
|
## Step 4: Structure Requirements
|
|
74
76
|
|
|
75
|
-
Resolve current milestone ID from `.forge/state/index.yml` (active milestone) or
|
|
77
|
+
Resolve the current milestone ID from the invocation context, or from the active milestone. Since 0.53.0 `.forge/state/index.yml` is a **git-ignored render-on-read cache** — legitimately absent on a fresh clone until the first `/forge` boot renders it, so do not depend on it being present and never treat its absence as "no project" (`.forge/project.yml` is the project sentinel). If the cache is present and fresh, read the active milestone from it; otherwise derive it from `.forge/state/milestone-*.yml` directly (the rollup's own source), or run a `/forge` boot first to render the cache. Target file: `.forge/requirements/m{N}.yml`.
|
|
76
78
|
|
|
77
79
|
If missing, create from `.forge/templates/requirements.yml`:
|
|
78
80
|
1. Extract from user description + research
|
|
@@ -15,6 +15,8 @@ Fast visual exploration inside the real app. You are drawing iterations of how s
|
|
|
15
15
|
2. **No verify/review gate.** There is no `verifying`, no `reviewing`, no health gate. The gate is the operator's eyes.
|
|
16
16
|
3. **Iterate, don't finish.** The loop is: render → operator reacts → adjust → render. It ends when the operator graduates, parks, or discards — never at "100% of tasks done".
|
|
17
17
|
|
|
18
|
+
> **Enter a worktree first (R10, `forge/SKILL.md` Step 3).** A mockup is disposable, but it is not weightless: it writes flag-gated code into the real app, a `.forge/prototypes/{slug}.md` state file, and iteration commits — artifacts the team discusses, shares, and may fold into a feature-toggled section of the app. That is exactly the write-before-worktree hazard R10 closes, so prototyping's **initial entry** enters its worktree before Step 1, the same as researching/discussing/planning. If `forge` boot already entered one, this is simply where you work; if this skill was invoked directly on the main checkout, `EnterWorktree` now before establishing the flag or writing the state file. (Resuming an existing prototype: work in the worktree it already lives in.)
|
|
19
|
+
|
|
18
20
|
## Step 1: Establish the prototype
|
|
19
21
|
|
|
20
22
|
Give the exploration a short kebab `{slug}` (e.g. `dashboard-redesign`, `onboarding-v2`).
|
|
@@ -58,13 +60,14 @@ The loop ends when the operator picks an outcome. Each has a clean terminal path
|
|
|
58
60
|
### Graduate → promote to a real milestone
|
|
59
61
|
The mockup proved the idea; now build it for real. This mirrors the refactor-backlog promotion procedure.
|
|
60
62
|
|
|
61
|
-
1. Synthesize milestone id `m-proto-{slug}` (or a numeric id
|
|
62
|
-
2.
|
|
63
|
-
3.
|
|
64
|
-
4.
|
|
65
|
-
5.
|
|
66
|
-
6.
|
|
67
|
-
7.
|
|
63
|
+
1. Synthesize milestone id `m-proto-{slug}` (or a numeric id via `.claude/hooks/forge-reserve.sh milestone --milestone m-new` — ADR-023, collision-safe across worktrees; never scan `milestone-*.yml` for max+1 inline).
|
|
64
|
+
2. **Write the declaration (R10 already put you in a worktree at initial entry):** the prototype has run in its worktree since Step 1, so you are already there — no re-entry. (Defensive: if this is somehow reached on the main checkout, `git rev-parse --git-dir` == `--git-common-dir`, `EnterWorktree` now first.) Write the declaration (`.forge/templates/streams/stream.yml` → `.forge/streams/{stream}.yml` — `stream.goal` = the mockup's title/one-line intent, `stream.milestone: m-{id}`, `stream.status: draft`, `runtime.branch`/`runtime.worktree` from the worktree in use, best-known `ownership.owned_surfaces`/`shared_surfaces`, context noting `milestone.origin: prototype/{slug}`) as the worktree's first *milestone* commit and fast-forward-push it, per FORGE.md's Stream Integration Checkpoints intake-declaration trigger.
|
|
65
|
+
3. Copy `.forge/templates/state/milestone.yml` → `.forge/state/milestone-{id}.yml`. Populate: `milestone.id`, `milestone.name: "Graduated from prototype: {title}"`, `milestone.origin: prototype/{slug}`, `current.tier: standard` (or `full` if the operator says so), `current.status: researching`, `current.last_updated: {ISO}`.
|
|
66
|
+
4. Append to `.forge/roadmap.yml`: one milestone entry + a first phase (`dependencies: []`). Note the prototype as the design reference so `researching`/`designing` can look at the alpha code.
|
|
67
|
+
5. Run the **State Rollup (forge 1.0)** so `index.yml` picks up the new milestone (do **not** hand-edit `index.yml`).
|
|
68
|
+
6. In the state file: set `status: graduated`, add `graduated_to: m-{id}`, `resolved: {ISO}`. **Archive** it: move `.forge/prototypes/{slug}.md` → `.forge/prototypes/archive/{slug}.md`.
|
|
69
|
+
7. **Leave the alpha flag in place** — the graduated milestone builds the real thing and removes/keeps the flag as its own work decides. Say so.
|
|
70
|
+
8. Confirm: *"Graduated prototype {slug} → milestone m-{id}. State + roadmap created, prototype archived. The alpha mockup stays as the design reference. Routing to researching."* Then fall through to normal Standard/Full routing via `forge`.
|
|
68
71
|
|
|
69
72
|
### Discard → the idea's out, clean up
|
|
70
73
|
1. **Remove the alpha-flagged code** you added for this prototype (the render, the flag branch if nothing else uses it, stub data). Grep first; only remove what this prototype introduced — never touch code the flag gated before you arrived.
|
|
@@ -7,6 +7,8 @@ description: "Small, scoped changes: typo fixes, config updates, minor bugs, dep
|
|
|
7
7
|
|
|
8
8
|
Small change? Skip ceremony. Do it right, do it fast.
|
|
9
9
|
|
|
10
|
+
> Entered via a worktree already, for new work (R10, `forge/SKILL.md` Step 3) — a quick fix still gets a worktree (just no milestone/stream declaration). If this skill is somehow invoked directly on the main checkout for new work, enter one now before writing anything.
|
|
11
|
+
|
|
10
12
|
## Qualifies as Quick
|
|
11
13
|
|
|
12
14
|
- Single file change, under 50 lines
|
|
@@ -88,5 +90,5 @@ Standalone quick fix (no forge state exists or no milestone context provided):
|
|
|
88
90
|
|
|
89
91
|
During execution, if you discover:
|
|
90
92
|
- Fix is bigger than expected → **STOP**. Escalate to Standard.
|
|
91
|
-
- Related issues that should be fixed → **LOG**
|
|
93
|
+
- Related issues that should be fixed → **LOG** a file under `.forge/deferred-issues/` (copy `.forge/templates/deferred-issue.md`, slug the summary; ADR-023 — legacy `deferred-issues.md` still read if the dir is absent). Don't fix now.
|
|
92
94
|
- An architectural question → **STOP**. Escalate to Standard with a note on what triggered it.
|
|
@@ -7,6 +7,8 @@ description: "Use when you need to investigate before building: understand the c
|
|
|
7
7
|
|
|
8
8
|
Gather context before planning. Bad research leads to bad plans.
|
|
9
9
|
|
|
10
|
+
> Entered via a worktree already, for new work (R10, `forge/SKILL.md` Step 3) — research writes `.forge/research/` state, so if this skill is somehow invoked directly on the main checkout for new work, enter one now before writing anything.
|
|
11
|
+
|
|
10
12
|
## Research Types
|
|
11
13
|
|
|
12
14
|
### 1. Codebase Research
|
|
@@ -29,7 +29,7 @@ Read: .forge/project.yml → tech stack, framework, database, dependencies
|
|
|
29
29
|
Read: .forge/state/milestone-{id}.yml → milestone ID and name
|
|
30
30
|
Read: .forge/constitution.md → active architectural gates (if exists)
|
|
31
31
|
Read: .forge/refactor-backlog.yml → existing backlog items (if any)
|
|
32
|
-
Read: .forge/deferred-issues
|
|
32
|
+
Read: .forge/deferred-issues/*.md → pre-existing failures logged during execution (glob the dir; else legacy .forge/deferred-issues.md)
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
Skip by stack: no DB->SQL/NoSQL N/A, no frontend->XSS N/A, no CI/CD->Pipeline N/A.
|
|
@@ -97,7 +97,7 @@ security_audit:
|
|
|
97
97
|
|-----------|--------|
|
|
98
98
|
| **Scalability** | Synchronous blocking, missing pagination, unbounded queries, N+1, missing caching, SPOFs, hardcoded limits |
|
|
99
99
|
| **Maintainability** | Files >300 lines, nesting >4, god components/classes, circular deps, dup logic |
|
|
100
|
-
| **Code Health** | Dead code/unused exports, TODO/FIXME inventory, untested critical paths, stale deps, deferred issues in `.forge/deferred-issues.md` |
|
|
100
|
+
| **Code Health** | Dead code/unused exports, TODO/FIXME inventory, untested critical paths, stale deps, deferred issues in `.forge/deferred-issues/` (else legacy `.forge/deferred-issues.md`) |
|
|
101
101
|
| **Structural Quality** | Biz logic in UI, inconsistent patterns, missing error boundaries, API contract drift |
|
|
102
102
|
|
|
103
103
|
**Rules:** Actual code, not theory. Specific files + evidence. `critical`=prod issues/blocking, `warning`=quality, `info`=improvement. Respect ADRs + constitution.
|
|
@@ -303,12 +303,12 @@ Refactoring triage (max 10): *"{N} opportunities:"*
|
|
|
303
303
|
|
|
304
304
|
**Accept**->backlog | **Dismiss**->skip | **Accept all** | **Dismiss all**
|
|
305
305
|
|
|
306
|
-
Deferred issues triage:
|
|
306
|
+
Deferred issues triage: Glob `.forge/deferred-issues/*.md` (else legacy `.forge/deferred-issues.md`) for `status: pending` items, surface them:
|
|
307
307
|
*"**Test debt** ({N} pre-existing failures): 1. `{summary}` -- first seen {date}. [Accept/Dismiss/Fix-now]*"*
|
|
308
308
|
|
|
309
|
-
- **Accept** → add to refactor-backlog.yml as `category: test-debt`,
|
|
309
|
+
- **Accept** → add to refactor-backlog.yml as `category: test-debt`, set `status: triaged` in the issue's file under `.forge/deferred-issues/` (or the legacy file if that is where it lives)
|
|
310
310
|
- **Fix-now** → route to `planning` fix mode before completing milestone
|
|
311
|
-
- **Dismiss** →
|
|
311
|
+
- **Dismiss** → set `status: dismissed` + `dismissed_reason` in the issue's file (or the legacy file)
|
|
312
312
|
|
|
313
313
|
## Step 7: Backlog + Route
|
|
314
314
|
|
|
@@ -320,7 +320,7 @@ Deferred issues triage: If `.forge/deferred-issues.md` has `status: pending` ite
|
|
|
320
320
|
|
|
321
321
|
**Cross-tree liveness check (informational).** Before writing, run `git worktree list --porcelain | awk '/^branch / && $2 ~ /refs\/heads\/forge\/m-/ {print}'`. Any non-empty output means another milestone is live in a worktree — surface one line: *"{N} other Forge milestone(s) live in worktrees ({list}). Backlog appends here land cleanly on main but won't be visible to those worktrees until they rebase."* Proceed with the write — reviewing on main is the canonical author. (Two reviewing sessions writing in parallel from different worktrees rely on the milestone-scoped append rule above, not on this check.)
|
|
322
322
|
|
|
323
|
-
Read `.forge/refactor-backlog.yml`.
|
|
323
|
+
Read `.forge/refactor-backlog.yml`. Reserve the next id via `id=$(.claude/hooks/forge-reserve.sh refactor --milestone m-{ctx})` (ADR-023 — collision-safe across worktrees; scans live + archived backlog, so a compacted R-id is never reused). Append:
|
|
324
324
|
|
|
325
325
|
```yaml
|
|
326
326
|
items:
|
|
@@ -408,7 +408,7 @@ If the milestone's phases produced `contract.md` files (planning Step 6.1 Tier 1
|
|
|
408
408
|
## Phase Handoff
|
|
409
409
|
|
|
410
410
|
1. Confirm report + backlog
|
|
411
|
-
2. **Human Verification Gate (hard block)** — Read `current.human_verified` from `milestone-{id}.yml`. If **absent/null** → **do not complete the milestone.** Code-clean + review-clean is not enough to close (FORGE.md → Human Verification Gate). The milestone needs an explicit human sign-off, normally captured by `verifying`. Either route the user back to verify (then re-review) or capture the sign-off / recorded override here and write `current.human_verified` before proceeding. A recorded override (`method: override`) satisfies this check but **must** be surfaced in the health report. Never silently complete past an unset gate.
|
|
411
|
+
2. **Human Verification Gate (hard block)** — Read `current.human_verified` from `milestone-{id}.yml`. If **absent/null** → **do not complete the milestone.** Code-clean + review-clean is not enough to close (FORGE.md → Human Verification Gate). The milestone needs an explicit human sign-off, normally captured by `verifying`. Either route the user back to verify (then re-review) or capture the sign-off / recorded override here and write `current.human_verified` before proceeding. A recorded override (`method: override`) satisfies this check but **must** be surfaced in the health report. Never silently complete past an unset gate. *(Attended lane: the recorded `current.human_verified` — or a recorded override — **is** the gate, exactly as today. The M0 `verify-signoff` **forward-gate is not wired here** — that is autonomous-lane. Where an M0 `signoff/<wu-id>` tag does exist, treat `human_verified` as a **rendered echo** of that signature and surface the tag in the report; where none exists it is the stored attestation. A pre-existing tagless `complete` milestone reads complete — grandfathered, never re-gated.)*
|
|
412
412
|
3. **Run promoted-milestone completion hook** (above) if `milestone.origin` set
|
|
413
413
|
4. **Run Contract Landing** (above) for any cross-layer phases — fold ratified contracts into their ADRs
|
|
414
414
|
5. Set `current.status: complete` and `current.completed_at: "<ISO 8601 timestamp>"` in `milestone-{id}.yml`, then regenerate `index.yml` via the `forge` **Rollup**
|
|
@@ -21,17 +21,18 @@ Read: .forge/project.yml → tech stack (for running tests)
|
|
|
21
21
|
Read: .forge/phases/milestone-{id}/{phase}-{name}/plan-{NN}.md → must_haves (truths, artifacts, key_links)
|
|
22
22
|
Read: .forge/context.md → locked decisions
|
|
23
23
|
Read: .forge/requirements/m{N}.yml → requirement IDs for coverage check (current milestone, resolved from state)
|
|
24
|
-
Read: .forge/deferred-issues
|
|
24
|
+
Read: .forge/deferred-issues/*.md → known pre-existing failures (glob the dir; else legacy .forge/deferred-issues.md; advisory)
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
## Deferred Issues
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Glob `.forge/deferred-issues/*.md` before running any tests (ADR-023 — one file per issue). If the
|
|
30
|
+
directory is absent, read the legacy single-file `.forge/deferred-issues.md` instead.
|
|
30
31
|
|
|
31
|
-
When test results come in, cross-reference failures against known deferred
|
|
32
|
+
When test results come in, cross-reference failures against the known deferred issues:
|
|
32
33
|
|
|
33
|
-
- **Failure matches a deferred
|
|
34
|
-
- **Failure not in deferred-issues
|
|
34
|
+
- **Failure matches a deferred issue** → advisory only — note as "Pre-existing (deferred: {slug})", do NOT fail verification for this
|
|
35
|
+
- **Failure not matched by any file in `.forge/deferred-issues/` (nor the legacy file)** → regression introduced after the baseline — **FAIL** and include in gaps
|
|
35
36
|
|
|
36
37
|
In the Test Results section of the report, split accordingly:
|
|
37
38
|
|
|
@@ -43,7 +44,7 @@ Run: {n} | Passed: {n} | Failed: {n}
|
|
|
43
44
|
- {test name}: {error}
|
|
44
45
|
|
|
45
46
|
### Pre-existing / Deferred (advisory)
|
|
46
|
-
-
|
|
47
|
+
- {slug} ({file}): {test name} — {summary}
|
|
47
48
|
```
|
|
48
49
|
|
|
49
50
|
Verdict: FAIL only triggers for regressions. Known deferred failures do not block a PASSED verdict — they are tracked separately and must be worked off via the refactor backlog or a dedicated fix phase.
|
|
@@ -252,7 +253,7 @@ Run on a PASSED verdict (and after any **HUMAN VERIFICATION NEEDED** items are r
|
|
|
252
253
|
1. Surface what the human should confirm — the Observable Truths, any "Human Verification Items", and any milestone-specific manual check (device session, visual pass, real-device / e2e walk).
|
|
253
254
|
2. Prompt explicitly: *"Code verification passed. Have you verified the work yourself (device session / visual / e2e walk)? This is required before the milestone can close. [verified | not yet | close without verifying]"*
|
|
254
255
|
3. Record the response in `current.human_verified` in `.forge/state/milestone-{id}.yml`:
|
|
255
|
-
- **verified** → `{at: "<ISO 8601>", method: "<how they verified>", notes: "<optional>"}`
|
|
256
|
+
- **verified** → `{at: "<ISO 8601>", method: "<how they verified>", notes: "<optional>"}`. *(Where an M0 `signoff/<wu-id>` tag exists for this milestone's wu, `human_verified` is a **rendered echo** of that signature — a human-readable cache of the sign-off — rather than the source of truth; surface the tag. Attended lane: the `verify-signoff` predicate is **not** wired into this gate's hard block — that forward-gate is autonomous-lane. See FORGE.md → Human Verification Gate.)*
|
|
256
257
|
- **not yet** → leave `human_verified` unset. **Do NOT route to reviewing** — the milestone cannot close. Tell the human what's outstanding and stop. Re-run this gate after they've done it.
|
|
257
258
|
- **close without verifying** → recorded override: `{at: "<ISO 8601>", method: "override", override: true, reason: "<required free text>"}`. Note the override in the verification report. Satisfies the gate but is flagged downstream.
|
|
258
259
|
|
package/template/.forge/FORGE.md
CHANGED
|
@@ -45,6 +45,7 @@ Integration is **event-driven, not polled**: a stream publishes its work to main
|
|
|
45
45
|
|
|
46
46
|
- **Checkpoints (plan-marked).** `planning` marks selected phases as integration checkpoints (`integration_checkpoint: true` in the plan frontmatter) — the points where a verified slice should land on main. Default heuristic: the last phase, plus any phase another stream/phase depends on. Advisory — planning proposes, the operator confirms. **The mark is the opt-in:** a plan with no marked checkpoint never auto-publishes, so existing/unmarked work is unaffected.
|
|
47
47
|
- **Producer — publish on verified checkpoint (fast-forward only, strand-guarded).** When `verifying` returns PASS for a checkpoint phase *and the session is a worktree*, it integrates that phase to main with a **fast-forward-only** push: `git push origin HEAD:main`. Git rejects a non-ff push (vs `origin/main`) — that surfaces a diverged *origin*, never force-pushed. But ff-vs-origin is not enough on its own: `origin/main` is not the operator's canonical checkout, and the local `main` ref can carry commits origin never saw (another stream's merge, a framework bump, state-sync commits). Pushing past a *behind* origin then leaves **local main diverged, not behind** — and the naive "you're behind, `git pull --ff-only`" advice *fails* on divergence (forge#13). So the producer **strand-guards** first: it publishes only when local `main` is an **ancestor of the worktree HEAD** (no local commits this checkpoint omits). Stranded (local main has such commits) → it does **not** push; it surfaces "integrate through local main first" and leaves the work on the branch. On a strand-safe success it sets the **integration flag**. (No remote → fast-forward local `main` where possible, else record ready-to-ff for the primary checkout.)
|
|
48
|
+
- **Producer — publish at intake (declaration split, second trigger for the same mechanic).** Milestone/stream intake (discussing's Milestone Gate, `forge` Step 1.2 backlog promotion, `prototyping` graduation) reuses this exact strand-guard + fast-forward + integration-flag mechanic, triggered at a different moment: not a `verifying` PASS, but the worktree's first commit. Before any content (the full `milestone-{id}.yml`, research, discussion, plans, code) is written, the worktree commits a **declaration** — milestone ID (+ reservation), one-line intent, stream registration, and coordination/touches claims, i.e. `.forge/streams/{stream}.yml` with `ownership.owned_surfaces`/`shared_surfaces` populated — and fast-forward-pushes it to `origin/main` immediately. Content stays worktree-only until the milestone's own checkpoint (above). This keeps `chief-of-staff`'s cross-stream conflict detection — which globs `.forge/streams/*.yml` from the current checkout only — sighted on in-flight work without teaching it to read across worktrees (rejected: this project spans machines, so cross-worktree reading needs the early-push step *plus* multi-branch readers — strictly more machinery). One-breath rule: main knows THAT work exists; the worktree knows WHAT it is.
|
|
48
49
|
- **Flag (cheap, set by the producer).** A marker in the shared git common dir — `.git/forge/integration-pending` — records the new main sha, the publishing stream, and the shared surfaces that changed. It is shared across all of a repo's worktrees on a machine, so any boot reads it for free; absent or already-seen → skip. The `main` ref is the source of truth the marker points at. Cross-machine (the marker is machine-local) falls back to the opt-in `forge.worktree_rebase_check` / a periodic fetch — eventual, not instant.
|
|
49
50
|
- **Consumer — auto fast-forward, else prompt (worktrees *and* the primary main checkout).** `forge` boot checks the flag in **both** checkout kinds. In a **worktree**: flag set (main advanced past this worktree's merge-base) and no divergent local commits → **fast-forward automatically** (`git merge --ff-only main`); diverged → **surface a rebase prompt** (never auto-rebase). In the **primary main checkout**: the strand guard guarantees local main is a clean ff of the new `origin/main`, so boot **fast-forwards local main automatically** (`git merge --ff-only origin/main`) — this is what keeps the operator's canonical checkout in lockstep with origin after every checkpoint, instead of drifting behind until the next local commit diverges it. Flag not set → no fetch, no scan, continue boot.
|
|
50
51
|
|
|
@@ -231,12 +232,12 @@ State lives in `.forge/`:
|
|
|
231
232
|
- `design-system.md` — Component mapping table
|
|
232
233
|
- `requirements/m{N}.yml` — Per-milestone structured requirements with `[NEEDS CLARIFICATION]` markers. **FR-IDs, DEF-IDs, and NFR-IDs are globally unique across all milestone files** — `FR-001` may exist in exactly one `m{N}.yml`. Before adding a new ID, **reserve it via the ID Reservation Protocol** — run `forge-reserve fr|nfr|def --milestone <id>` and use the id it prints (the helper dual-writes the machine-local ledger + `.forge/reservations.yml`; you commit `reservations.yml` with the work). The next number is `max(ledger ∪ in-tree ∪ main:reservations.yml) + 1`. The machine-local ledger is what makes the shared ID space safe across **concurrent worktrees**: bare scan-and-increment (or a branch-local `reservations.yml`) sees only committed/merged state, so two worktrees branched from one baseline claim the same number and collide silently until merge (see ADR-021, supersedes ADR-016). On a residual collision (legacy/un-reserved, e.g. during a migration), keep the older milestone's ID and renumber the newer. Concurrent milestones each own their file — no cross-stream contention on file writes, but ID space is shared. Functional requirements may carry M9 e2e gate fields (`e2e`, `observable_outcome`, `observable_outcome_hash`, `validated`) — lazy migration, absent fields default to `e2e:false`/`validated:false`.
|
|
233
234
|
- `roadmap.yml` — Phases, milestones, dependencies
|
|
234
|
-
- `state/index.yml` — DERIVED registry rolled up from milestone files (id, name, status, last_updated). Never hand-edited; never written by worktree agents.
|
|
235
|
+
- `state/index.yml` — DERIVED registry rolled up from milestone files (id, name, status, last_updated). Never hand-edited; never written by worktree agents. **Git-ignored render-on-read cache (0.53.0): regenerated at every boot from the milestone files, never committed** — so a committed projection can never drift stale from its source, and any session can render it locally without touching `main`. Absent on a fresh clone until the first boot renders it (the project sentinel is `project.yml`, not this file).
|
|
235
236
|
- `state/milestone-{id}.yml` — Per-milestone cursor (single source of truth): position, progress, decisions, blockers. One owner at a time.
|
|
236
237
|
- `state/desire-paths/` — Append-only framework-usage observations, one file per observation. Occurrence counts derived by globbing (no mutable counter). Each carries `scope: project | framework` (set at capture; absent ⇒ project). **`forge` boot is the single review owner**: at 3+ occurrences it surfaces a concrete fix from the co-located suggestion table, persists declines to `declined/` (no re-nag), and for `scope: framework` signals routes them upstream — a portable block to `upstream/` plus a `gh issue` offer against `forge.upstream_repo`. This closes Principle 7's capture→act loop; `verifying` captures but no longer surfaces. See [ADR-012](../docs/decisions/ADR-012-upstream-desire-path-feedback-channel.md).
|
|
237
238
|
- `context.md` — Active locked decisions + deferred ideas (target <=12KB)
|
|
238
239
|
- `context-archive.md` — Historical locked decisions; load only when relevant
|
|
239
|
-
- `streams/active.yml` — **DERIVED** Project Chief traffic map. Regenerated by the Stream Rollup from the per-stream files + active milestone files (see Stream Rollup below). **Never hand-edited**, like `index.yml
|
|
240
|
+
- `streams/active.yml` — **DERIVED** Project Chief traffic map. Regenerated by the Stream Rollup from the per-stream files + active milestone files (see Stream Rollup below). **Never hand-edited**, like `index.yml`, and — since 0.53.0 — a **git-ignored render-on-read cache** regenerated at every boot, never committed (this is the durable kill for the drift where a committed `active.yml` phase went stale against its milestone). Each row carries the two orthogonal dimensions side by side: `phase` (from the milestone, for milestone-backed streams) and `coordination` (from the stream file). See [ADR-015](../docs/decisions/ADR-015-derived-stream-registry.md).
|
|
240
241
|
- `streams/{stream}.yml` — Per-stream **source of truth**: coordination lifecycle, ownership, shared surfaces, blockers, merge readiness, and an optional `stream.milestone: m-{id}` link. Standalone (quick-fix) streams own their `status` here; milestone-backed streams omit workflow phase (the rollup reads it from the milestone).
|
|
241
242
|
- `streams/{stream}/brief.md` — Human-readable stream context packet loaded when resuming that stream.
|
|
242
243
|
- `streams/{stream}/packages/{id}.yml` — Work package contract for delegated workers or manual sessions.
|
|
@@ -253,6 +254,7 @@ State lives in `.forge/`:
|
|
|
253
254
|
`index.yml status` gates routing: `not_started | active | deferred | complete`.
|
|
254
255
|
|
|
255
256
|
**Stream Rollup (`active.yml` is derived).** Mirrors the milestone Rollup: `active.yml` is a pure projection, regenerated — never hand-edited — by a deterministic, idempotent **join** of two single-sourced inputs (ADR-015). Run by the main/orchestrator session and at `chief-of-staff` Show/Sync; worktrees never write it. Procedure:
|
|
257
|
+
0. **Completed-stream auto-close sweep (source write, main checkout only — runs *before* the join).** A milestone-backed stream whose work has completed and landed on main should not linger as a `ready`/`merge_queue` row — that stale readiness is a **false** "ready to merge" nudge (the work is already on main; the stream is *closable, not mergeable*). Before the pure join, glob the stream files and for each with `stream.milestone: m-{id}` where the milestone's derived status (from the freshly-rolled `index.yml`) is `complete`, its stream `status` is **not** already `closed`, **and** it has no live worktree (`runtime.worktree` empty, or the path no longer resolves to a live `git worktree`) → **auto-close it**: set the stream file's `status: closed` and `merge.readiness: merged`, append a `merge.notes` line (*"auto-closed: milestone m-{id} complete + on main"*), refresh `updated_at`. Emit one line — *"auto-closed stale stream {id} — milestone m-{id} complete, already on main."* This is a legitimate source write (stream file = single-owner mutable; the milestone is done, so no live driver contends). **Guards:** a `deferred` linked milestone is **not** swept (its stream should pause, not close — Chief offers that interactively); a `complete` milestone **with a live worktree** is **not** swept here either — the forge-boot "Completed milestone + live worktree → finalize" path (SKILL Step 1) owns teardown, and closes the stream as part of it. Idempotent: an already-`closed` stream is skipped, so re-running the sweep is a no-op. No `streams/` dir, or nothing complete-and-open → the sweep is silent and the join runs unchanged.
|
|
256
258
|
1. Glob `streams/{stream}.yml`. Each → a row carrying its coordination state (`coordination` = the file's `status`), goal, runtime, ownership summary, `merge_readiness` (derived from the stream file's `merge.readiness`), `blocked_by`, `next_action` — all from the stream file.
|
|
257
259
|
2. For a stream with `stream.milestone: m-{id}`, read `phase`/activity from `milestone-{id}.yml` (`current.status`, `last_updated`) — **not** stored in the stream file. A linked milestone that is `deferred`/`complete`/absent → the row's `phase` reflects that.
|
|
258
260
|
3. Glob active milestones (from `index.yml`). Any active milestone with **no** stream file → emit a derived `stream: implicit` row (phase from the milestone, no coordination state) so coverage is guaranteed in the artifact — an active milestone can never be silently missing.
|
|
@@ -283,7 +285,7 @@ with the work it describes. Every artifact has a sharing class:
|
|
|
283
285
|
|---|---|---|
|
|
284
286
|
| **Single-owner mutable** | `state/milestone-{id}.yml`, `phases/milestone-{id}/`, `research/milestone-{id}.md` | Exactly one writer at a time — the worktree (or main session) currently driving that milestone. **Other worktrees never touch it**, not even to read-then-write — they pull from main if they need it. |
|
|
285
287
|
| **Single-owner mutable (per stream)** | `streams/{stream}.yml`, `streams/{stream}/brief.md`, `streams/{stream}/packages/*.yml` | The stream's **current driver** writes them — one writer at a time, like the milestone file. Distinct streams own distinct files (no cross-stream contention). `active.yml` is **derived** from them: a worktree driving a stream may write its own stream file but **never** `active.yml` or a peer stream's file. |
|
|
286
|
-
| **Derived** | `state/index.yml`, `streams/active.yml` | Never hand-edited.
|
|
288
|
+
| **Derived (git-ignored render-on-read cache)** | `state/index.yml`, `streams/active.yml` | Never hand-edited, **never committed** — git-ignored local caches (0.53.0), regenerated by rollup from their sources (`index.yml` ← `milestone-*.yml` via Rollup 1.0; `active.yml` ← per-stream files + active milestones via the Stream Rollup) at every boot. Because the value is computed on read and never stored in git, a committed projection cannot drift stale from its source, and **any** session (not just the orchestrator) may render them locally without a `main` write. Absent on a fresh clone until the first boot; readers must render-then-read and must **not** use their absence as a "not a Forge project" sentinel (that is `project.yml`). |
|
|
287
289
|
| **Append-only shared** | `releases.yml` (version reservations), `reservations.yml` (ADR/DEF/FR/NFR id **audit trail + cross-machine floor**), `state/desire-paths/*` (one file per observation) | Many writers OK; structure prevents collision. `releases.yml` uses the Version Reservation Protocol (append + commit + push). `reservations.yml` is written by `forge-reserve` (ADR-021) and committed with the caller's work — it is no longer the id *coordination* point (the machine-local ledger is), just the durable committed shadow. Desire-paths use distinct filenames so two writers never touch the same file. |
|
|
288
290
|
| **Machine-local runtime** | `.git/forge/id-reservations` (id-reservation ledger), `.git/forge/integration-pending` (integration flag) | In the git common dir, shared across a repo's worktrees on one machine, **not** git-tracked. `forge-reserve` writes the ledger under a `mkdir` lock; excluded from framework context. Machine-local by design (cross-machine falls back to the `main:reservations.yml` floor). |
|
|
289
291
|
| **Shared mutable** | `context.md`, `refactor-backlog.yml`, `requirements/m{N}.yml` (ID space `FR-`/`DEF-`/`NFR-` is globally shared even though each milestone owns its file) | Write only to your milestone/stream block. Within a block, append-only; strike through instead of rewriting. |
|
|
@@ -320,12 +322,13 @@ The project version + CHANGELOG slot are **shared resources** — when two miles
|
|
|
320
322
|
|
|
321
323
|
### ID Reservation Protocol
|
|
322
324
|
|
|
323
|
-
Sequential IDs — `ADR-NNN` (decision records), `DEF-NNN` (deferred
|
|
325
|
+
Sequential IDs — `ADR-NNN` (decision records), `DEF-NNN` (deferred requirements), `FR-NNN`/`NFR-NNN` (requirements), plus milestone ids (`M-NN`) and refactor-backlog ids (`R0NN`) — are **shared cross-worktree resources** the same way the version number is. "Scan the tree for the highest and increment" only sees *committed* state, so two worktrees branched from the same baseline each claim the same next number and collide **silently until merge** — a costly multi-file renumber pass. [ADR-016](../docs/decisions/ADR-016-id-reservation-protocol.md) tried to fix this by reserving in the branch-tracked `reservations.yml` and *committing + pushing before allocating*, but a reservation on an unmerged branch is invisible to every sibling worktree (they share `.git`, not branches) — so collisions continued. [ADR-021](../docs/decisions/ADR-021-id-reservation-ledger-git-common-dir.md) (**supersedes ADR-016**) moves the coordination point to a **machine-local ledger in the git common dir**, the same primitive as the integration flag: every sibling worktree of the repo on a machine sees it the moment it is written — no push, no pull, no merge.
|
|
324
326
|
|
|
325
|
-
- **Reserve with the helper.** `forge-reserve <kind> --milestone <id> [--summary <text>]` (`.claude/hooks/forge-reserve.sh`, `kind ∈ {adr, def, fr, nfr}`) allocates the next number under a portable `mkdir` lock and prints it on stdout. It **dual-writes**: one TSV line to `.git/forge/id-reservations` (the machine-local coordination ledger) and one `{kind, id, milestone, reserved_at, summary}` block to the tracked `.forge/reservations.yml`. Neither is committed by the helper — **the caller commits `reservations.yml` with its work** (unchanged handoff). Reserve *before* creating the artifact; the printed id is the one to use.
|
|
326
|
-
- **Next number = `max(ledger ∪ in-tree ∪ main:reservations.yml) + 1` per kind.** Three inputs, unioned: the **ledger** (same-machine siblings, *including uncommitted* — the input ADR-016 could not see), the **in-tree** landed ids (`docs/decisions/ADR-NNN*`; `.forge/requirements/*.yml`
|
|
327
|
+
- **Reserve with the helper.** `forge-reserve <kind> --milestone <id> [--summary <text>]` (`.claude/hooks/forge-reserve.sh`, `kind ∈ {adr, def, fr, nfr, milestone, refactor}`) allocates the next number under a portable `mkdir` lock and prints it on stdout. Id formats differ per kind (ADR-023): `adr/def/fr/nfr` → `PREFIX-0NN` (hyphen, zero-padded); `milestone` → `M-NN` (hyphen, **unpadded** — ids are used bare, `milestone-26.yml`/`m-26`); `refactor` → `R0NN` (**no hyphen**, zero-padded — matches the backlog convention). It **dual-writes**: one TSV line to `.git/forge/id-reservations` (the machine-local coordination ledger) and one `{kind, id, milestone, reserved_at, summary}` block to the tracked `.forge/reservations.yml`. Neither is committed by the helper — **the caller commits `reservations.yml` with its work** (unchanged handoff). Reserve *before* creating the artifact; the printed id is the one to use.
|
|
328
|
+
- **Next number = `max(ledger ∪ in-tree ∪ main:reservations.yml) + 1` per kind.** Three inputs, unioned: the **ledger** (same-machine siblings, *including uncommitted* — the input ADR-016 could not see), the **in-tree** landed ids, and **`main:reservations.yml`** (the cross-machine / cold-start floor). The in-tree scan is per-kind: `adr` → `docs/decisions/ADR-NNN*`; `fr/nfr/def` → `.forge/requirements/*.yml`; `milestone` → live `state/milestone-*.yml` **∪ archived** `.forge/archive/milestone-*/`; `refactor` → `refactor-backlog.yml` **∪** `refactor-backlog-archive.yml` (ADR-023 — reading archives means a compacted/archived id is never reallocated). Taking all three inputs keeps it backward-compatible (landed & pre-ledger IDs respected) while giving the ledger authority for the same-machine hot path.
|
|
327
329
|
- **`reservations.yml` is now the durable audit trail + cross-machine floor**, not the coordination mechanism. Append-only, distinct lines → it unions cleanly at merge (it stops being a merge-conflict source). An optional `forge-reserve --verify` can diff ledger vs. `reservations.yml`.
|
|
328
|
-
- **Reserve points.** `architecting` reserves an `adr` before authoring it; `planning` reserves `fr`/`nfr`/`def` before writing them into requirements
|
|
330
|
+
- **Reserve points.** `architecting` reserves an `adr` before authoring it; `planning` reserves `fr`/`nfr`/`def` before writing them into requirements; the three milestone-id sites (`forge` Rollup, `discussing`, `prototyping`) reserve `milestone`; `reviewing` reserves `refactor` for a new backlog id — all via `forge-reserve` (ADR-023).
|
|
331
|
+
- **DI-NNN eliminated, not reserved (ADR-023).** `executing`'s deferred-*issue* ids used to be a scan-and-increment `DI-NNN` counter in a single `deferred-issues.md` — the same cross-worktree collision in kind. Rather than reserve them, deferred issues are now **counter-free per-issue files** under `.forge/deferred-issues/` (`{date}-{milestone}-{slug}.md`, the desire-path pattern): distinct filenames, no shared counter, nothing to collide on. Nothing cross-references a deferred issue by number, so the id density reservation buys is worthless here. Legacy `deferred-issues.md` is still read until migrated.
|
|
329
332
|
- **Cross-machine gap (accepted).** The ledger is machine-local (like `integration-pending`). Two *concurrent* worktrees on *different* laptops, both reserving before either merges, still fall back to merge-time renumber — bounded by the `main:reservations.yml` floor, so no worse than ADR-016.
|
|
330
333
|
- **Lazy migration.** No ledger ⇒ the helper creates it on first reserve, seeded by the `max()` (which reads in-tree + `main:reservations.yml`). Existing `reservations.yml` is untouched and still respected. Projects that never run concurrent worktrees see no behavior change.
|
|
331
334
|
|
|
@@ -380,6 +383,10 @@ Plans MAY declare a per-truth `check:` shell command in `must_haves.truths` (exi
|
|
|
380
383
|
|
|
381
384
|
Lazy migration: milestones with no `human_verified` field (pre-0.27.0) simply hit the prompt on their next close transition; already-`complete` milestones are never re-gated.
|
|
382
385
|
|
|
386
|
+
**Grandfather (legacy-complete marker).** A milestone already stored `current.status: complete` with **no** `signoff/<wu-id>` tag (the pre-M0 history — 26 milestones as of 0.53.0) is **grandfathered**: readers honor it as complete and it is **never re-gated** by any later sign-off convention. "Complete with no signoff tag" **is** the legacy-complete marker — no field is stamped onto those files. Only *new* completions going forward can be asked to carry a signature (that forward-gate is autonomous-lane and is **not** wired in the attended lane — see below).
|
|
387
|
+
|
|
388
|
+
**Where the Contract M0 sign-off convention is adopted, `human_verified` is a derived echo, not the source of truth.** The authoritative predicate becomes `verify-signoff <wu-id>` exiting 0 — an SSH-signed `signoff/<wu-id>` tag an agent cannot forge (see `docs/sign-off-convention.md` and `.claude/hooks/{verify-signoff,wu-done}.sh`). `current.human_verified` may still be written for human-readable display, but treat it as a cache of the signature check, never the gate itself. Core does not yet wire this predicate into `verifying`/`reviewing`'s own hard-block logic automatically — that remains a flagged follow-up (Contract M0 §D2 scoped to a minimal fold stub, not the full skill wiring).
|
|
389
|
+
|
|
383
390
|
## Beads Integration (Optional)
|
|
384
391
|
|
|
385
392
|
With Beads installed: `bd prime` (session context), `bd ready` (unblocked tasks), `bd complete` (update deps), `bd compact` (summarize old). Without Beads, `.forge/state/` + Session Memory suffice.
|
|
@@ -28,3 +28,11 @@ dev-source
|
|
|
28
28
|
|
|
29
29
|
# Orchestration locks
|
|
30
30
|
*.lock
|
|
31
|
+
|
|
32
|
+
# Render-on-read derived registries (0.53.0). Regenerated at every `forge` boot
|
|
33
|
+
# from their sources (state/index.yml ← state/milestone-*.yml; streams/active.yml
|
|
34
|
+
# ← streams/*.yml + active milestones). Local cache only — never committed, so a
|
|
35
|
+
# committed projection can never drift stale from its source. Without these
|
|
36
|
+
# entries a fresh install would re-commit them.
|
|
37
|
+
state/index.yml
|
|
38
|
+
streams/active.yml
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Migration Guide: forge-reserve covers milestone + refactor ids; DI-NNN → counter-free files (Forge 0.52.0)
|
|
2
|
+
|
|
3
|
+
Forge 0.52.0 closes the last scan-and-increment ID collisions across concurrent worktrees ([ADR-023](../../docs/decisions/ADR-023-extend-id-reservation-to-milestone-refactor-ids.md), extends [ADR-021](../../docs/decisions/ADR-021-id-reservation-ledger-git-common-dir.md)):
|
|
4
|
+
|
|
5
|
+
- **`forge-reserve` gains two kinds** — `milestone` (emits `M-NN`, unpadded) and `refactor` (emits `R0NN`, no hyphen). Both scan live **and archived** locations as the cold-ledger floor, so a compacted or archived id is never reallocated. The milestone-id sites (`forge` Rollup, `discussing`, `prototyping`) and the refactor-backlog site (`reviewing`) now call the helper instead of hand-scanning `max + 1`.
|
|
6
|
+
- **DI-NNN deferred-issue ids are eliminated.** Deferred issues become **counter-free per-issue files** under `.forge/deferred-issues/` named `{YYYY-MM-DD}-{milestone}-{slug}.md` — the same anti-collision pattern as `.forge/state/desire-paths/`. No shared counter means concurrent worktrees never collide.
|
|
7
|
+
|
|
8
|
+
**The `forge-reserve` change is zero-touch** — it takes effect the moment the framework files sync; the next milestone/refactor allocation just works. **The deferred-issues change is backward-compatible** — the reader skills still read a legacy single-file `.forge/deferred-issues.md` when the new directory is absent, so nothing breaks before you migrate.
|
|
9
|
+
|
|
10
|
+
## What changed (no action needed for the helper)
|
|
11
|
+
|
|
12
|
+
- `forge-reserve <milestone|refactor> --milestone <id>` allocates through the same machine-local ledger + `reservations.yml` audit trail as the existing kinds. Id formats: `milestone` → `M-26` (hyphen, unpadded — ids are used bare); `refactor` → `R067` (no hyphen, zero-padded — matches the backlog).
|
|
13
|
+
- The in-tree scan for these kinds reads archives too: `milestone` = live `state/milestone-*.yml` ∪ `.forge/archive/milestone-*/`; `refactor` = `refactor-backlog.yml` ∪ `refactor-backlog-archive.yml`.
|
|
14
|
+
- `executing`/`quick-tasking` (writers) log a new per-issue file; `verifying`/`reviewing` (readers) glob `.forge/deferred-issues/*.md`, with the legacy single file as fallback.
|
|
15
|
+
|
|
16
|
+
## Detection
|
|
17
|
+
|
|
18
|
+
Prints `MIGRATE` only if a legacy single-file `.forge/deferred-issues.md` is present (the DI-NNN layout the new skills supersede). Silent + exit 0 otherwise — including projects that never had deferred issues.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
[ -d .forge ] || exit 0 # not a Forge project
|
|
22
|
+
[ -f .forge/deferred-issues.md ] || exit 0 # no legacy file → nothing to migrate, silent
|
|
23
|
+
echo "MIGRATE — legacy .forge/deferred-issues.md present; split each issue into a counter-free file under .forge/deferred-issues/ ({date}-{milestone}-{slug}.md) and remove the old file (ADR-023). Safe to defer — the skills still read the legacy file until you do."
|
|
24
|
+
exit 0
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Migration steps (deferred issues only)
|
|
28
|
+
|
|
29
|
+
The `forge-reserve` kinds need no migration. For deferred issues, if Detection prints `MIGRATE`:
|
|
30
|
+
|
|
31
|
+
1. For each entry in `.forge/deferred-issues.md` `issues:`, create `.forge/deferred-issues/{first_seen}-{milestone}-{slug}.md` by copying `.forge/templates/deferred-issue.md` and filling the frontmatter (`type`, `command`, `summary`, `first_seen`, `milestone`, `status`). `{slug}` is a short kebab of the summary. **Drop the `id: DI-NNN` field** — the filename is the id now.
|
|
32
|
+
2. Delete `.forge/deferred-issues.md` once every entry has a file.
|
|
33
|
+
3. Commit the split with your next state-sync.
|
|
34
|
+
|
|
35
|
+
This is **safe to defer**: until you split the file, the reader skills (`verifying`, `reviewing`) still load the legacy `deferred-issues.md`, so no deferred failure is lost or silently un-tracked.
|
|
36
|
+
|
|
37
|
+
## Validation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
[ -x .claude/hooks/forge-reserve.sh ] && echo "OK: forge-reserve present" || echo "FAIL: helper missing"
|
|
41
|
+
m=$(.claude/hooks/forge-reserve.sh milestone --milestone m-smoke) && echo "OK: milestone $m" || echo "FAIL: milestone kind"
|
|
42
|
+
r=$(.claude/hooks/forge-reserve.sh refactor --milestone m-smoke) && echo "OK: refactor $r" || echo "FAIL: refactor kind"
|
|
43
|
+
# revert the smoke reservations: drop the last two reservations.yml blocks + their ledger lines
|
|
44
|
+
echo "validation complete"
|
|
45
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Deferred-issue file template (ADR-023). Copy to:
|
|
3
|
+
.forge/deferred-issues/{YYYY-MM-DD}-{milestone}-{slug}.md
|
|
4
|
+
ONE file per issue — the filename slug replaces the old DI-NNN counter. Because
|
|
5
|
+
each writer picks a distinct {date}-{milestone}-{slug} name, concurrent worktrees
|
|
6
|
+
never touch the same file, so there is no shared counter to collide on at merge
|
|
7
|
+
(the same anti-collision property as .forge/state/desire-paths/).
|
|
8
|
+
|
|
9
|
+
Legacy .forge/deferred-issues.md (single file, DI-NNN entries) is still READ by the
|
|
10
|
+
reader skills when this directory is absent — lazy migration, no data loss.
|
|
11
|
+
|
|
12
|
+
Frontmatter below is the per-issue record. Keep it terse; this is state, not a doc.
|
|
13
|
+
-->
|
|
14
|
+
type: test_failure # test_failure | lint_failure | build_failure
|
|
15
|
+
command: "" # the command that surfaced it, e.g. "npm test"
|
|
16
|
+
summary: "" # one line — what fails and why (feeds the verify report)
|
|
17
|
+
first_seen: "" # YYYY-MM-DD — also the filename date
|
|
18
|
+
milestone: "" # milestone id that logged it, e.g. m26
|
|
19
|
+
status: pending # pending | triaged | dismissed
|
|
20
|
+
# --- set by reviewing triage (optional; omit until triaged) ---
|
|
21
|
+
# triaged_at: "" # YYYY-MM-DD when accepted into the refactor backlog
|
|
22
|
+
# dismissed_reason: "" # why it was dismissed instead of tracked
|