forge-orkes 0.44.0 → 0.46.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 +162 -0
- package/template/.claude/hooks/tests/forge-reserve.test.sh +121 -0
- package/template/.claude/skills/architecting/SKILL.md +1 -1
- package/template/.claude/skills/planning/SKILL.md +2 -2
- package/template/.forge/FORGE.md +16 -12
- package/template/.forge/migrations/0.46.0-id-reservation-ledger.md +44 -0
package/package.json
CHANGED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# forge-reserve — allocate a collision-safe sequential id (ADR/DEF/FR/NFR).
|
|
3
|
+
#
|
|
4
|
+
# Implements ADR-021: the coordination point is a machine-local ledger in the
|
|
5
|
+
# git COMMON dir (.git/forge/id-reservations), shared instantly across every
|
|
6
|
+
# sibling worktree of the repo on this machine — the input ADR-016's
|
|
7
|
+
# branch-tracked reservations.yml could not see. Same primitive as the
|
|
8
|
+
# integration flag (.git/forge/integration-pending).
|
|
9
|
+
#
|
|
10
|
+
# forge-reserve <kind> --milestone <id> [--summary <text>]
|
|
11
|
+
# kind ∈ {adr|def|fr|nfr}
|
|
12
|
+
#
|
|
13
|
+
# stdout: the allocated id and NOTHING else (e.g. ADR-022). All logs → stderr.
|
|
14
|
+
# exit: 0 success; 2 bad/missing kind or args; 3 lock timeout. On non-zero
|
|
15
|
+
# exit nothing is written.
|
|
16
|
+
#
|
|
17
|
+
# Side effects (both UNcommitted — the caller commits reservations.yml with its
|
|
18
|
+
# work): appends one TSV line to the common-dir ledger and one YAML block to the
|
|
19
|
+
# worktree's .forge/reservations.yml, both under a portable mkdir lock.
|
|
20
|
+
#
|
|
21
|
+
# Pure POSIX sh — no bashisms, no flock (absent on macOS, ADR-021 point 3),
|
|
22
|
+
# no node/python. Only git/mkdir/awk/grep/sed/date/stat.
|
|
23
|
+
set -eu
|
|
24
|
+
|
|
25
|
+
WAIT="${FORGE_RESERVE_WAIT:-5}" # seconds: bounded lock-acquire budget
|
|
26
|
+
STALE="${FORGE_RESERVE_STALE:-15}" # seconds: a lock older than this is broken
|
|
27
|
+
SLEEP="0.1" # retry interval while the lock is held
|
|
28
|
+
|
|
29
|
+
# --- 1. parse args ----------------------------------------------------------
|
|
30
|
+
kind=""
|
|
31
|
+
milestone=""
|
|
32
|
+
summary=""
|
|
33
|
+
while [ $# -gt 0 ]; do
|
|
34
|
+
case "$1" in
|
|
35
|
+
--milestone) shift; milestone="${1:-}"; [ $# -gt 0 ] && shift || true ;;
|
|
36
|
+
--milestone=*) milestone="${1#*=}"; shift ;;
|
|
37
|
+
--summary) shift; summary="${1:-}"; [ $# -gt 0 ] && shift || true ;;
|
|
38
|
+
--summary=*) summary="${1#*=}"; shift ;;
|
|
39
|
+
-*) printf 'forge-reserve: unknown option: %s\n' "$1" >&2; exit 2 ;;
|
|
40
|
+
*)
|
|
41
|
+
if [ -z "$kind" ]; then kind="$1"; shift
|
|
42
|
+
else printf 'forge-reserve: unexpected argument: %s\n' "$1" >&2; exit 2; fi
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
done
|
|
46
|
+
|
|
47
|
+
# Validate kind BEFORE touching git, the lock, or any file — a bad kind must
|
|
48
|
+
# exit non-zero having written nothing (must_have truth #3).
|
|
49
|
+
case "$kind" in
|
|
50
|
+
adr) prefix="ADR" ;;
|
|
51
|
+
def) prefix="DEF" ;;
|
|
52
|
+
fr) prefix="FR" ;;
|
|
53
|
+
nfr) prefix="NFR" ;;
|
|
54
|
+
"") printf 'forge-reserve: missing kind (adr|def|fr|nfr)\n' >&2; exit 2 ;;
|
|
55
|
+
*) printf 'forge-reserve: unknown kind: %s (expected adr|def|fr|nfr)\n' "$kind" >&2; exit 2 ;;
|
|
56
|
+
esac
|
|
57
|
+
[ -n "$milestone" ] || { printf 'forge-reserve: --milestone <id> is required\n' >&2; exit 2; }
|
|
58
|
+
|
|
59
|
+
# --- 2. resolve paths -------------------------------------------------------
|
|
60
|
+
common="$(git rev-parse --git-common-dir)"
|
|
61
|
+
top="$(git rev-parse --show-toplevel)"
|
|
62
|
+
mkdir -p "$common/forge"
|
|
63
|
+
ledger="$common/forge/id-reservations"
|
|
64
|
+
lock="$common/forge/id-reservations.lock"
|
|
65
|
+
reservations="$top/.forge/reservations.yml"
|
|
66
|
+
|
|
67
|
+
# Portable mtime → epoch: BSD/macOS `stat -f %m`, GNU `stat -c %Y`. This is the
|
|
68
|
+
# one place a platform branch is warranted (ADR-021 point 3: macOS is a
|
|
69
|
+
# first-class platform and its stat flags differ from GNU's).
|
|
70
|
+
get_mtime() {
|
|
71
|
+
stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null || true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# --- 3. acquire lock (atomic mkdir, bounded retry, staleness-break) ----------
|
|
75
|
+
# mkdir is POSIX-atomic: it succeeds for exactly one racer and fails if the dir
|
|
76
|
+
# already exists — a lock without flock. A crashed helper's lock is broken after
|
|
77
|
+
# STALE seconds so it can't wedge the namespace forever.
|
|
78
|
+
max_iters="$(awk -v w="$WAIT" -v s="$SLEEP" 'BEGIN{print int(w/s)}')"
|
|
79
|
+
iters=0
|
|
80
|
+
while ! mkdir "$lock" 2>/dev/null; do
|
|
81
|
+
mt="$(get_mtime "$lock")"
|
|
82
|
+
now_epoch="$(date +%s)"
|
|
83
|
+
if [ -n "$mt" ] && [ "$((now_epoch - mt))" -ge "$STALE" ]; then
|
|
84
|
+
printf 'forge-reserve: breaking stale lock (age %ss): %s\n' "$((now_epoch - mt))" "$lock" >&2
|
|
85
|
+
rmdir "$lock" 2>/dev/null || true
|
|
86
|
+
continue
|
|
87
|
+
fi
|
|
88
|
+
iters="$((iters + 1))"
|
|
89
|
+
if [ "$iters" -ge "$max_iters" ]; then
|
|
90
|
+
printf 'forge-reserve: lock timeout after ~%ss: %s\n' "$WAIT" "$lock" >&2
|
|
91
|
+
exit 3
|
|
92
|
+
fi
|
|
93
|
+
sleep "$SLEEP"
|
|
94
|
+
done
|
|
95
|
+
# Only now that the lock is ours do we install the release trap.
|
|
96
|
+
trap 'rmdir "$lock" 2>/dev/null || true' EXIT INT TERM
|
|
97
|
+
|
|
98
|
+
# --- 4/5. three-way max(ledger ∪ in-tree ∪ main:reservations.yml) + 1 --------
|
|
99
|
+
# Everything numeric goes through awk (n+0) so leading-zero ids like 021 are read
|
|
100
|
+
# as decimal 21, never octal — no shell $(( )) octal trap.
|
|
101
|
+
|
|
102
|
+
# (a) ledger — same-machine siblings, INCLUDING uncommitted (the ADR-016-blind input)
|
|
103
|
+
ledger_max=0
|
|
104
|
+
if [ -f "$ledger" ]; then
|
|
105
|
+
ledger_max="$(awk -F'\t' -v k="$kind" \
|
|
106
|
+
'$1==k { split($2,a,"-"); n=a[2]+0; if(n>m)m=n } END{print m+0}' "$ledger")"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# (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
|
+
if [ "$kind" = "adr" ]; then
|
|
112
|
+
intree_max="$(ls "$top/docs/decisions" 2>/dev/null \
|
|
113
|
+
| awk -F- '/^ADR-[0-9]/ { n=$2+0; if(n>m)m=n } END{print m+0}')"
|
|
114
|
+
else
|
|
115
|
+
intree_max="$(grep -rhoE '[A-Z]+-[0-9]+' "$top/.forge/requirements/" 2>/dev/null \
|
|
116
|
+
| awk -F- -v p="$prefix" '$1==p { n=$2+0; if(n>m)m=n } END{print m+0}')"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# (c) main:reservations.yml — cross-machine / cold-start floor (survives a
|
|
120
|
+
# .git/forge wipe; carries another machine's MERGED reservations). Absent → 0.
|
|
121
|
+
main_max="$(git show main:.forge/reservations.yml 2>/dev/null \
|
|
122
|
+
| grep -oE '[A-Z]+-[0-9]+' \
|
|
123
|
+
| awk -F- -v p="$prefix" '$1==p { n=$2+0; if(n>m)m=n } END{print m+0}')"
|
|
124
|
+
[ -n "$main_max" ] || main_max=0
|
|
125
|
+
|
|
126
|
+
next="$(awk -v a="$ledger_max" -v b="$intree_max" -v c="$main_max" \
|
|
127
|
+
'BEGIN{m=a; if(b>m)m=b; if(c>m)m=c; print m+1}')"
|
|
128
|
+
id="$(printf '%s-%03d' "$prefix" "$next")"
|
|
129
|
+
|
|
130
|
+
# --- 6. dual-write (still under the lock) -----------------------------------
|
|
131
|
+
now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
132
|
+
printf '%s\t%s\t%s\t%s\n' "$kind" "$id" "$milestone" "$now" >> "$ledger"
|
|
133
|
+
|
|
134
|
+
# reservations.yml missing → seed the 0.42.0 starter HEADER, but with a bare
|
|
135
|
+
# `reservations:` key (not the guide's `reservations: []`) so the block items we
|
|
136
|
+
# append below form a valid sequence — appending `- kind:` under a `[]` flow list
|
|
137
|
+
# is malformed YAML. Existing repo files already use the bare-key block form.
|
|
138
|
+
if [ ! -f "$reservations" ]; then
|
|
139
|
+
mkdir -p "$(dirname "$reservations")"
|
|
140
|
+
cat > "$reservations" <<'YML'
|
|
141
|
+
# Forge ID Reservations — cross-worktree coordination for ADR / DEF / FR / NFR numbers
|
|
142
|
+
# Written by .claude/hooks/forge-reserve.sh (ADR-021). The machine-local ledger
|
|
143
|
+
# (.git/forge/id-reservations) is the same-machine coordination point; this file
|
|
144
|
+
# is the durable committed audit trail + cross-machine floor. Append-only; never
|
|
145
|
+
# edit prior entries. Next number for a kind = max(ledger, in-tree, main) + 1.
|
|
146
|
+
# kind ∈ {adr,def,fr,nfr}. See FORGE.md → ID Reservation Protocol + ADR-021.
|
|
147
|
+
reservations:
|
|
148
|
+
YML
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# YAML double-quoted scalar: escape backslash then double-quote.
|
|
152
|
+
esc_summary="$(printf '%s' "$summary" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
|
153
|
+
{
|
|
154
|
+
printf ' - kind: %s\n' "$kind"
|
|
155
|
+
printf ' id: %s\n' "$id"
|
|
156
|
+
printf ' milestone: %s\n' "$milestone"
|
|
157
|
+
printf ' reserved_at: "%s"\n' "$now"
|
|
158
|
+
printf ' summary: "%s"\n' "$esc_summary"
|
|
159
|
+
} >> "$reservations"
|
|
160
|
+
|
|
161
|
+
# --- 7. emit the id (stdout only); trap releases the lock on exit ------------
|
|
162
|
+
printf '%s\n' "$id"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Self-contained unit test for .claude/hooks/forge-reserve.sh (ADR-021).
|
|
3
|
+
#
|
|
4
|
+
# Needs no network and no fixtures beyond a temp dir. Each case builds a throwaway
|
|
5
|
+
# git repo, seeds ids, and asserts the helper's stdout + file side effects. Run:
|
|
6
|
+
# ./.claude/hooks/tests/forge-reserve.test.sh
|
|
7
|
+
# Exits 0 iff every case passes; non-zero on any failure. Self-cleans on exit.
|
|
8
|
+
set -u
|
|
9
|
+
|
|
10
|
+
# Resolve the helper from THIS script's own location so the test works from any cwd.
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
HELPER="$SCRIPT_DIR/../forge-reserve.sh"
|
|
13
|
+
|
|
14
|
+
[ -x "$HELPER" ] || { printf 'FATAL: helper not executable: %s\n' "$HELPER" >&2; exit 1; }
|
|
15
|
+
|
|
16
|
+
# One temp root; every throwaway repo lives under it; single cleanup.
|
|
17
|
+
ROOT="$(mktemp -d)"
|
|
18
|
+
trap 'rm -rf "$ROOT"' EXIT INT TERM
|
|
19
|
+
|
|
20
|
+
PASSED=0
|
|
21
|
+
FAILED=0
|
|
22
|
+
pass() { PASSED=$((PASSED + 1)); printf 'PASS: %s\n' "$1"; }
|
|
23
|
+
fail() { FAILED=$((FAILED + 1)); printf 'FAIL: %s\n' "$1" >&2; }
|
|
24
|
+
check() { # desc, actual, expected
|
|
25
|
+
if [ "$2" = "$3" ]; then pass "$1"; else fail "$1 (got '$2', want '$3')"; fi
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Fresh repo on branch `main` (symbolic-ref sets the unborn branch name portably,
|
|
29
|
+
# no git ≥2.28 -b flag needed). Prints its path.
|
|
30
|
+
new_repo() {
|
|
31
|
+
d="$ROOT/repo.$1"
|
|
32
|
+
mkdir -p "$d"
|
|
33
|
+
git -C "$d" init -q
|
|
34
|
+
git -C "$d" symbolic-ref HEAD refs/heads/main
|
|
35
|
+
git -C "$d" config user.email t@example.com
|
|
36
|
+
git -C "$d" config user.name tester
|
|
37
|
+
printf '%s\n' "$d"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# --- Case 1: allocation from in-tree ADR ------------------------------------
|
|
41
|
+
d="$(new_repo case1)"; cd "$d"
|
|
42
|
+
mkdir -p docs/decisions
|
|
43
|
+
: > docs/decisions/ADR-005-something.md
|
|
44
|
+
git add -A && git commit -qm init
|
|
45
|
+
got="$("$HELPER" adr --milestone m-x)"
|
|
46
|
+
check "case1 in-tree ADR-005 -> ADR-006" "$got" "ADR-006"
|
|
47
|
+
|
|
48
|
+
# --- Case 2: ledger beats tree (uncommitted sibling — the ADR-016-blind case) -
|
|
49
|
+
d="$(new_repo case2)"; cd "$d"
|
|
50
|
+
mkdir -p docs/decisions
|
|
51
|
+
: > docs/decisions/ADR-005-something.md
|
|
52
|
+
git add -A && git commit -qm init
|
|
53
|
+
mkdir -p .git/forge
|
|
54
|
+
printf 'adr\tADR-050\tm-x\t2026-01-01T00:00:00Z\n' > .git/forge/id-reservations
|
|
55
|
+
got="$("$HELPER" adr --milestone m-x)"
|
|
56
|
+
check "case2 ledger(ADR-050) beats tree(ADR-005) -> ADR-051" "$got" "ADR-051"
|
|
57
|
+
|
|
58
|
+
# --- Case 3: main:reservations.yml floor (committed, then removed from tree) --
|
|
59
|
+
d="$(new_repo case3)"; cd "$d"
|
|
60
|
+
mkdir -p .forge
|
|
61
|
+
cat > .forge/reservations.yml <<'YML'
|
|
62
|
+
reservations:
|
|
63
|
+
- kind: fr
|
|
64
|
+
id: FR-200
|
|
65
|
+
milestone: m-x
|
|
66
|
+
reserved_at: "2026-01-01"
|
|
67
|
+
summary: "seed"
|
|
68
|
+
YML
|
|
69
|
+
git add -A && git commit -qm init
|
|
70
|
+
rm .forge/reservations.yml # gone from the working tree; survives only on main
|
|
71
|
+
got="$("$HELPER" fr --milestone m-x)"
|
|
72
|
+
check "case3 main floor FR-200 -> FR-201" "$got" "FR-201"
|
|
73
|
+
|
|
74
|
+
# --- Case 4: dual-write side effects + no commit made by the helper ----------
|
|
75
|
+
d="$(new_repo case4)"; cd "$d"
|
|
76
|
+
git commit --allow-empty -qm init
|
|
77
|
+
ledger=".git/forge/id-reservations"
|
|
78
|
+
before=0; [ -f "$ledger" ] && before="$(wc -l < "$ledger" | tr -d ' ')"
|
|
79
|
+
got="$("$HELPER" nfr --milestone m-x --summary "dual write check")"
|
|
80
|
+
after="$(wc -l < "$ledger" | tr -d ' ')"
|
|
81
|
+
check "case4 ledger grew by exactly 1" "$((after - before))" "1"
|
|
82
|
+
if grep -q "id: $got" .forge/reservations.yml; then
|
|
83
|
+
pass "case4 reservations.yml has a $got block"
|
|
84
|
+
else
|
|
85
|
+
fail "case4 reservations.yml missing $got block"
|
|
86
|
+
fi
|
|
87
|
+
check "case4 no new commit (HEAD count still 1)" "$(git rev-list --count HEAD)" "1"
|
|
88
|
+
if [ -n "$(git status --porcelain .forge/reservations.yml)" ]; then
|
|
89
|
+
pass "case4 reservations.yml is uncommitted (dirty)"
|
|
90
|
+
else
|
|
91
|
+
fail "case4 reservations.yml unexpectedly clean"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# --- Case 5: unknown kind — non-zero exit, empty stdout, nothing written -----
|
|
95
|
+
d="$(new_repo case5)"; cd "$d"
|
|
96
|
+
git commit --allow-empty -qm init
|
|
97
|
+
out="$("$HELPER" zzz --milestone m-x 2>/dev/null)"; rc=$?
|
|
98
|
+
check "case5 unknown kind exits non-zero" "$( [ "$rc" -ne 0 ] && echo yes || echo no )" "yes"
|
|
99
|
+
check "case5 unknown kind prints nothing on stdout" "$out" ""
|
|
100
|
+
[ ! -f .git/forge/id-reservations ] && pass "case5 ledger not created" || fail "case5 ledger was created"
|
|
101
|
+
[ ! -f .forge/reservations.yml ] && pass "case5 reservations.yml not created" || fail "case5 reservations.yml was created"
|
|
102
|
+
|
|
103
|
+
# --- Case 6: N-way concurrency yields N distinct ids, N ledger lines ---------
|
|
104
|
+
d="$(new_repo case6)"; cd "$d"
|
|
105
|
+
git commit --allow-empty -qm init
|
|
106
|
+
outdir="$d/outs"; mkdir -p "$outdir"
|
|
107
|
+
N=8
|
|
108
|
+
i=1
|
|
109
|
+
while [ "$i" -le "$N" ]; do
|
|
110
|
+
( "$HELPER" fr --milestone m-x > "$outdir/out.$i" 2>/dev/null ) &
|
|
111
|
+
i=$((i + 1))
|
|
112
|
+
done
|
|
113
|
+
wait
|
|
114
|
+
distinct="$(cat "$outdir"/out.* | sort -u | grep -c .)"
|
|
115
|
+
check "case6 $N concurrent runs -> $N distinct ids" "$distinct" "$N"
|
|
116
|
+
ledger_lines="$(grep -c '^fr ' .git/forge/id-reservations)"
|
|
117
|
+
check "case6 $N distinct ledger fr lines" "$ledger_lines" "$N"
|
|
118
|
+
|
|
119
|
+
# --- Report -----------------------------------------------------------------
|
|
120
|
+
printf '\n%d passed, %d failed\n' "$PASSED" "$FAILED"
|
|
121
|
+
[ "$FAILED" -eq 0 ]
|
|
@@ -28,7 +28,7 @@ When an architectural decision conflicts with vertical slicing (e.g., a framewor
|
|
|
28
28
|
|
|
29
29
|
## Architecture Decision Record (ADR)
|
|
30
30
|
|
|
31
|
-
**Reserve the ADR number first (cross-worktree safety — blocking gate).** Before authoring a new ADR, allocate its number
|
|
31
|
+
**Reserve the ADR number first (cross-worktree safety — blocking gate).** Before authoring a new ADR, allocate its number with the `forge-reserve` helper (ADR-021, supersedes ADR-016): run `id=$(.claude/hooks/forge-reserve.sh adr --milestone <id> --summary "<one-line>")`, then author `docs/decisions/$id-*.md` and commit `.forge/reservations.yml` **with** the ADR (unchanged handoff — the helper writes `reservations.yml` uncommitted; the caller commits it alongside the work). **This reserve step is mandatory, not advisory: do not create the ADR file until `forge-reserve` has printed its id.** WHY: the number is allocated against a machine-local ledger (`.git/forge/id-reservations`) that every sibling worktree of the repo shares the moment it is written, so two worktrees architecting in parallel can no longer pick the same `ADR-NNN` and collide silently until merge (a multi-file renumber) — the exact failure ADR-016's "commit + push before allocating" could not prevent, because reservations sat on unmerged branches nothing else reads. The helper self-heals a missing ledger (lazy migration). See FORGE.md → **ID Reservation Protocol** and ADR-021 for the mechanism.
|
|
32
32
|
|
|
33
33
|
Record every significant decision in `.forge/decisions/`:
|
|
34
34
|
|
|
@@ -76,11 +76,11 @@ Resolve current milestone ID from `.forge/state/index.yml` (active milestone) or
|
|
|
76
76
|
|
|
77
77
|
If missing, create from `.forge/templates/requirements.yml`:
|
|
78
78
|
1. Extract from user description + research
|
|
79
|
-
2. IDs: FR-001, FR-002... **Globally unique across milestones.** Allocate
|
|
79
|
+
2. IDs: FR-001, FR-002... **Globally unique across milestones.** Allocate with the `forge-reserve` helper (ADR-021, supersedes ADR-016). **This is a blocking gate, not advisory: do not write an `FR-`/`NFR-`/`DEF-` ID into any artifact until `forge-reserve` has printed it.** Per ID: run `id=$(.claude/hooks/forge-reserve.sh fr --milestone <id> --summary "<one-line>")` and use its stdout, *then* write the FR; `.forge/reservations.yml` commits with the plan (the helper writes it uncommitted — the caller commits). WHY: the id is allocated against a machine-local ledger (`.git/forge/id-reservations`) every sibling worktree shares the moment it is written, so two worktrees branched from one baseline can no longer claim the same number and collide silently until merge (a multi-file renumber) — the exact failure ADR-016's "commit + push" could not prevent, because reservations sat on unmerged branches nothing else reads. The helper self-heals a missing ledger (lazy migration).
|
|
80
80
|
3. Acceptance: Given/When/Then
|
|
81
81
|
4. Uncertain: `[NEEDS CLARIFICATION]`
|
|
82
82
|
5. P1 (must) / P2 (should) / P3 (nice)
|
|
83
|
-
6. Deferred: DEF-001..., and NFR-001... — **also globally unique; reserve the same way**
|
|
83
|
+
6. Deferred: DEF-001..., and NFR-001... — **also globally unique; reserve the same way** via `forge-reserve def` / `forge-reserve nfr` before writing.
|
|
84
84
|
|
|
85
85
|
**E2E gate (M9):** For each functional requirement being added or refined:
|
|
86
86
|
1. Decide `e2e: true|false` -- does this story need a post-validation e2e test?
|
package/template/.forge/FORGE.md
CHANGED
|
@@ -106,6 +106,7 @@ Auto-detects complexity. Override: "Use Quick/Standard/Full tier."
|
|
|
106
106
|
| Systematic debugging | `debugging` | When stuck |
|
|
107
107
|
| Upgrade Forge files | `upgrading` | On-demand |
|
|
108
108
|
| Cross-session memory | `beads-integration` | When Beads installed |
|
|
109
|
+
| Project/read Forge work in Notion | `notion-integration` | When Notion configured |
|
|
109
110
|
| Multi-agent backend (experimental) | `orchestrating` | Optional, usually selected by Chief/Streams |
|
|
110
111
|
|
|
111
112
|
> Experimental skills require opt-in install — see `packages/create-forge/experimental/m10/README.md`.
|
|
@@ -136,8 +137,9 @@ Every new accumulating artifact needs: owner, size gate, archive/compaction rule
|
|
|
136
137
|
source-of-truth; load scoped slices, not whole history.
|
|
137
138
|
- `phases/`, `audits/`, `research/`, `archive/`, and `context-archive.md` are
|
|
138
139
|
historical; load only by milestone/stream/package/version.
|
|
139
|
-
- Runtime/generated files (`.mcp-server/*.db*`, logs, spike DBs
|
|
140
|
-
|
|
140
|
+
- Runtime/generated files (`.mcp-server/*.db*`, logs, spike DBs, and the
|
|
141
|
+
`.git/forge/*` machine-local files — the id-reservation ledger + integration
|
|
142
|
+
flag) are excluded from framework context and packages unless debugging them.
|
|
141
143
|
- Template copies sync mechanically; do not treat them as separate concepts.
|
|
142
144
|
|
|
143
145
|
### Fresh Agent Pattern
|
|
@@ -211,7 +213,7 @@ State lives in `.forge/`:
|
|
|
211
213
|
- `project.yml` — Vision, stack, design system, verification, constraints (<5KB)
|
|
212
214
|
- `constitution.md` — Active architectural gates
|
|
213
215
|
- `design-system.md` — Component mapping table
|
|
214
|
-
- `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** (
|
|
216
|
+
- `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`.
|
|
215
217
|
- `roadmap.yml` — Phases, milestones, dependencies
|
|
216
218
|
- `state/index.yml` — DERIVED registry rolled up from milestone files (id, name, status, last_updated). Never hand-edited; never written by worktree agents.
|
|
217
219
|
- `state/milestone-{id}.yml` — Per-milestone cursor (single source of truth): position, progress, decisions, blockers. One owner at a time.
|
|
@@ -227,7 +229,7 @@ State lives in `.forge/`:
|
|
|
227
229
|
- `refactor-backlog.yml` — Refactoring catalog (actionable items only), worked via quick-tasking. Canonical status vocab: `pending | in_progress | done | dismissed | deferred`. Terminal items auto-archive on the next reviewing/quick-tasking write (compaction-on-write).
|
|
228
230
|
- `refactor-backlog-archive.yml` — Append-only terminal-item archive (`done`/`dismissed`); keeps the audit trail out of the working backlog.
|
|
229
231
|
- `releases.yml` — Append-only version-reservation registry (cross-session coordination point; see Version Reservation Protocol)
|
|
230
|
-
- `reservations.yml` — Append-only ID-reservation
|
|
232
|
+
- `reservations.yml` — Append-only ID-reservation audit trail + cross-machine floor for ADR/DEF/FR/NFR numbers, written by `forge-reserve` (ADR-021). The same-machine cross-worktree coordination point is the machine-local ledger `.git/forge/id-reservations`; see ID Reservation Protocol.
|
|
231
233
|
- `archive/milestone-{id}/` — Archived milestone artifacts (archive-delete)
|
|
232
234
|
|
|
233
235
|
**Milestones** group phases into concurrent streams. Own state file — no conflicts across sessions. Can be deferred (frozen in place) or archive-deleted.
|
|
@@ -265,7 +267,8 @@ with the work it describes. Every artifact has a sharing class:
|
|
|
265
267
|
| **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. |
|
|
266
268
|
| **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. |
|
|
267
269
|
| **Derived** | `state/index.yml`, `streams/active.yml` | Never hand-edited. 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). Only the main/orchestrator session (and `chief-of-staff` for `active.yml`) runs rollup. |
|
|
268
|
-
| **Append-only shared** | `releases.yml` (version reservations), `reservations.yml` (ADR/DEF/FR/NFR id
|
|
270
|
+
| **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. |
|
|
271
|
+
| **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). |
|
|
269
272
|
| **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. |
|
|
270
273
|
| **Stable shared** | `project.yml`, `constitution.md`, `design-system.md`, `roadmap.yml`, `FORGE.md`, `.claude/skills/`, `.claude/agents/` | Rarely changes except via `/upgrading`. Canonical on main; worktrees lag until they rebase. An upgrade run touches **only the checkout it runs in**, so after upgrading in main, live worktrees keep their branch-point framework files — `upgrading` **Step 8** enumerates live `forge/m-*` worktrees and offers to rebase each (never auto), and the opt-in `forge.worktree_rebase_check` watches these files as a boot-time safety net. Gitignored carve-outs (`.mcp.json`, experimental hooks) don't rebase — re-run the experimental installer if the upgrade changed them. |
|
|
271
274
|
|
|
@@ -300,15 +303,16 @@ The project version + CHANGELOG slot are **shared resources** — when two miles
|
|
|
300
303
|
|
|
301
304
|
### ID Reservation Protocol
|
|
302
305
|
|
|
303
|
-
Sequential IDs — `ADR-NNN` (decision records), `DEF-NNN` (deferred issues), `FR-NNN`/`NFR-NNN` (requirements) — 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
|
|
306
|
+
Sequential IDs — `ADR-NNN` (decision records), `DEF-NNN` (deferred issues), `FR-NNN`/`NFR-NNN` (requirements) — 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.
|
|
304
307
|
|
|
305
|
-
- **Reserve
|
|
306
|
-
- **Next number = `max(
|
|
307
|
-
-
|
|
308
|
-
- **Reserve points.** `architecting` reserves an `adr` before authoring it; `planning` reserves `fr`/`nfr`/`def` before writing them into requirements
|
|
309
|
-
- **
|
|
308
|
+
- **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.
|
|
309
|
+
- **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` for fr/nfr/def), and **`main:reservations.yml`** (the cross-machine / cold-start floor). Taking all three keeps it backward-compatible (landed & pre-ledger IDs respected) while giving the ledger authority for the same-machine hot path.
|
|
310
|
+
- **`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`.
|
|
311
|
+
- **Reserve points.** `architecting` reserves an `adr` before authoring it; `planning` reserves `fr`/`nfr`/`def` before writing them into requirements — both via `forge-reserve`. (Out of scope: `executing`'s `DI-NNN` deferred-*issue* ids in `deferred-issues.md` are a separate, more local namespace — not reserved here.)
|
|
312
|
+
- **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.
|
|
313
|
+
- **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.
|
|
310
314
|
|
|
311
|
-
This is the cross-worktree mechanism the FR/DEF/NFR "globally unique" rule (State Management, above) previously lacked,
|
|
315
|
+
This is the cross-worktree mechanism the FR/DEF/NFR "globally unique" rule (State Management, above) previously lacked, now made collision-safe for same-machine worktrees. Versions stay in `releases.yml` (separate semver/CHANGELOG semantics); everything else reserves through `forge-reserve`.
|
|
312
316
|
|
|
313
317
|
## Deviation Rules
|
|
314
318
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Migration Guide: ID Reservation Ledger in the Git Common Dir (Forge 0.46.0)
|
|
2
|
+
|
|
3
|
+
Forge 0.46.0 moves the ID-reservation **coordination point** from the branch-tracked `.forge/reservations.yml` to a **machine-local ledger in the git common dir** — `.git/forge/id-reservations` — written by a new `forge-reserve` POSIX-sh helper. This fixes the cross-worktree collisions ADR-016 could not (a reservation on an unmerged branch is invisible to sibling worktrees, which share `.git`, not branches). See [ADR-021](../../docs/decisions/ADR-021-id-reservation-ledger-git-common-dir.md) (supersedes ADR-016) and [issue #19](https://github.com/Attuned-Media/forge/issues/19).
|
|
4
|
+
|
|
5
|
+
**It is backward-compatible and zero-touch.** The helper is installed + `chmod +x`'d by the upgrade's additive hooks sync. The ledger self-creates on the **first** `forge-reserve` run, seeded implicitly by the allocation `max()` (which reads in-tree IDs + `main:.forge/reservations.yml`). Your existing `reservations.yml` is untouched and still respected as a `max()` input. A project that never runs concurrent worktrees sees no behavior change and needs no action.
|
|
6
|
+
|
|
7
|
+
## What changed (no action needed)
|
|
8
|
+
|
|
9
|
+
- **`forge-reserve` helper** allocates the next `ADR/DEF/FR/NFR` number under a portable `mkdir` lock and prints it on stdout; `planning` (fr/nfr/def) and `architecting` (adr) now call it instead of hand-scanning.
|
|
10
|
+
- **Allocation is `max(ledger ∪ in-tree ∪ main:reservations.yml) + 1`** per kind — the ledger gives same-machine worktrees authority (including *uncommitted* siblings), while `main:reservations.yml` remains the durable cross-machine floor.
|
|
11
|
+
- **`reservations.yml` is reclassified** from the coordination mechanism to a **durable audit trail + cross-machine floor**, still committed with your work by the helper.
|
|
12
|
+
|
|
13
|
+
These take effect the moment the framework files are synced. No per-project state changes.
|
|
14
|
+
|
|
15
|
+
## Detection
|
|
16
|
+
|
|
17
|
+
Prints `MIGRATE` only if a project predates the helper (has `.forge/` but no `forge-reserve.sh`). Silent + exit 0 once the helper is present.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
[ -d .forge ] || exit 0 # not a Forge project
|
|
21
|
+
[ -f .claude/hooks/forge-reserve.sh ] && exit 0 # helper present → already migrated, silent
|
|
22
|
+
echo "MIGRATE — ID reservation now uses .git/forge/id-reservations via forge-reserve; run 'npx forge-orkes upgrade' to install the helper (ADR-021, issue #19)"
|
|
23
|
+
exit 0
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Migration steps (normally none)
|
|
27
|
+
|
|
28
|
+
The upgrade installs the helper; the ledger self-creates on first reserve. Two optional cleanups:
|
|
29
|
+
|
|
30
|
+
- **Backfill un-reserved IDs (recommended for concurrent-worktree projects).** `forge` boot's ID-reservation preflight lists landed `ADR-`/`FR-`/`NFR-`/`DEF-` IDs that have no `reservations.yml` entry. Appending them keeps `main:reservations.yml` a complete floor.
|
|
31
|
+
- **ADR location caveat.** `forge-reserve`'s in-tree ADR scan reads `docs/decisions/ADR-NNN*` (per ADR-021). Projects that keep ADRs under **`.forge/decisions/`** should ensure those ADRs are represented in `reservations.yml` (via the backfill above) so the `main:reservations.yml` floor covers them — the ledger + floor still prevent collisions, but the in-tree ADR *input* won't see a `.forge/decisions/` file directly.
|
|
32
|
+
|
|
33
|
+
## Cross-machine caveat (accepted)
|
|
34
|
+
|
|
35
|
+
The ledger is machine-local (like the integration flag). 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. Tracked as DEF-046 (land-time detector deferred).
|
|
36
|
+
|
|
37
|
+
## Validation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
[ -x .claude/hooks/forge-reserve.sh ] && echo "OK: forge-reserve present + executable" || echo "FAIL: helper missing/not executable"
|
|
41
|
+
id=$(.claude/hooks/forge-reserve.sh fr --milestone m-smoke --summary "migration smoke") && echo "OK: allocated $id" || echo "FAIL: forge-reserve did not run"
|
|
42
|
+
# revert the smoke reservation: drop the last reservations.yml block + the ledger line for $id
|
|
43
|
+
echo "validation complete"
|
|
44
|
+
```
|