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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-orkes",
3
- "version": "0.44.0",
3
+ "version": "0.46.0",
4
4
  "description": "Set up the Forge meta-prompting framework for Claude Code in your project",
5
5
  "bin": {
6
6
  "create-forge": "./bin/create-forge.js"
@@ -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 via the **ID Reservation Protocol** (FORGE.md). **Do not create the ADR file until its reservation is appended and committed** the reserve step is mandatory, not advisory. The next number is `max(highest `kind: adr` in `.forge/reservations.yml`, highest `ADR-NNN` in `.forge/decisions/`) + 1; append one `{kind: adr, id, milestone, reserved_at, summary}` entry to `.forge/reservations.yml`, **commit + push it**, *then* write the ADR file. Bare "scan `decisions/` for the highest + increment" only sees committed state, so two worktrees architecting in parallel both pick the same `ADR-NNN` and collide silently until merge (a multi-file renumber). See FORGE.md **ID Reservation Protocol** for the full rule. No `reservations.yml` yet scan as before and the first reservation creates it (lazy migration).
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 via the **ID Reservation Protocol** (FORGE.md). **This is a blocking gate, not advisory: do not write an `FR-`/`NFR-`/`DEF-` ID into any artifact until its reservation is appended and committed.** Per ID: next = `max(highest `kind: fr` in `.forge/reservations.yml`, highest FR in `.forge/requirements/*.yml`) + 1; append `{kind: fr, id, milestone, reserved_at, summary}` to `.forge/reservations.yml`, **commit + push**, *then* write the FR. Skipping the reserve step is the exact failure ADR-016 exists to prevent bare scan-and-increment sees only committed state, so two worktrees branched from one baseline claim the same number and collide silently until merge (a multi-file renumber). No `reservations.yml` yet scan as before; the first reservation creates it (lazy migration), but once it exists the gate is mandatory.
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** (`kind: def` / `kind: nfr`) before writing.
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?
@@ -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) are excluded
140
- from framework context and packages unless debugging them.
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** (append to `.forge/reservations.yml`, commit+push, then write) the next number is `max(highest reserved, highest in `.forge/requirements/*.yml`) + 1`. That reservation is what makes the shared ID space safe across **concurrent worktrees**: bare scan-and-increment sees only committed state, so two worktrees branched from one baseline claim the same number and collide silently until merge (see 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`.
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 registry for ADR/DEF/FR/NFR numbers (cross-worktree coordination point; see ID Reservation Protocol)
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 reservations), `state/desire-paths/*` (one file per observation) | Many writers OK; structure prevents collision. `releases.yml` + `reservations.yml` use the reservation protocols (append + commit + push before depending on the number); desire-paths use distinct filenames so two writers never touch the same file. |
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 streams branched from the same baseline each claim the same next number and collide **silently until merge** — a costly multi-file renumber pass (see [ADR-016](../docs/decisions/ADR-016-id-reservation-protocol.md)). `.forge/reservations.yml` removes the number from per-stream scope, exactly as `releases.yml` does for versions.
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 before allocating.** Before authoring a new ADR or writing a new DEF/FR/NFR ID, append ONE entry to `.forge/reservations.yml` `{kind, id, milestone, reserved_at, summary}`, `kind {adr, def, fr, nfr}`then **commit + push it BEFORE creating the artifact**. A parallel worktree pulls and sees the number taken.
306
- - **Next number = `max(reserved, in-tree) + 1` per kind.** The higher of (a) the highest reservation for that kind in `reservations.yml` and (b) the highest ID of that kind actually landed in the tree. Taking both keeps it backward-compatible landed IDs that were never reserved are still respected.
307
- - **Append-only never edit prior entries.** Distinct lines = no contention. Race resolution: the second of two near-simultaneous reservations rebases onto the first (append-only → clean), re-reads `max()`, takes the next number.
308
- - **Reserve points.** `architecting` reserves an `adr` before authoring it; `planning` reserves `fr`/`nfr`/`def` before writing them into requirements. (Out of scope: `executing`'s `DI-NNN` deferred-*issue* ids in `deferred-issues.md` are a separate, more local namespace — not reserved here.)
309
- - **Lazy migration.** No `reservations.yml` allocate as before (scan in-tree max); the first reservation creates the file. Existing projects are unaffected until they next allocate an ID.
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, and the first reservation rule ADR numbers have ever had. Versions stay in `releases.yml` (separate semver/CHANGELOG semantics); everything else reserves here.
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
+ ```