forge-orkes 0.71.0 → 0.72.2
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-model-outcome.sh +5 -3
- package/template/.claude/hooks/forge-reserve.sh +127 -15
- package/template/.claude/hooks/forge-slice-notify.sh +12 -3
- package/template/.claude/hooks/forge-slice-runner.sh +40 -19
- package/template/.claude/skills/notion-integration/SKILL.md +1 -0
- package/template/.claude/skills/planning/SKILL.md +3 -3
- package/template/.forge/FORGE.md +2 -2
- package/template/.forge/checks/forge-board-render.sh +21 -18
- package/template/.forge/checks/forge-check-lint.sh +36 -2
- package/template/.forge/checks/forge-hold-check.sh +121 -2
- package/template/.forge/checks/forge-jarvis-dispatch.sh +63 -7
- package/template/.forge/checks/forge-jarvis.sh +28 -0
- package/template/.forge/checks/forge-roadmap-check.sh +106 -22
- package/template/.forge/checks/tests/forge-check-lint.test.sh +126 -4
- package/template/.forge/checks/tests/forge-hold-check.test.sh +166 -1
- package/template/.forge/checks/tests/forge-jarvis-dispatch.test.sh +64 -0
- package/template/.forge/checks/tests/forge-jarvis.test.sh +78 -4
- package/template/.forge/checks/tests/forge-reserve.test.sh +235 -0
- package/template/.forge/checks/tests/forge-roadmap-check.test.sh +487 -0
- package/template/.forge/templates/hold-config.yml +15 -0
- package/template/.forge/templates/project.yml +10 -0
- package/template/.forge/templates/waive-hold.yml +63 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Fixture-repo test suite for .claude/hooks/forge-reserve.sh (m-41, plan 81-01).
|
|
3
|
+
#
|
|
4
|
+
# Contract under test (ADR-021/023/029 + FR-236/237/238, NFR-045):
|
|
5
|
+
# phase — computed P-0NN; in-tree scan = live roadmap `- id: N` ints ∪ archived
|
|
6
|
+
# phase dirs .forge/archive/milestone-*/phases/<N>-* (burned ids never
|
|
7
|
+
# reallocated); ledger + main:reservations.yml floors as for all kinds.
|
|
8
|
+
# wave — computed W-0NN; in-tree scan = live waves-map KEYS only (never the
|
|
9
|
+
# phase ids inside the brackets).
|
|
10
|
+
# version — RECORD-ONLY (--value): clean claim dual-writes ledger +
|
|
11
|
+
# reservations.yml and echoes the value; the SAME value claimed by a
|
|
12
|
+
# DIFFERENT milestone → exit 1, stderr names the holder, nothing
|
|
13
|
+
# written; re-claim by the SAME milestone → exit 0, idempotent, no
|
|
14
|
+
# duplicate line; missing --value → usage exit 2, nothing written.
|
|
15
|
+
# regression (NFR-045) — the pre-existing kinds' ids and file shapes are
|
|
16
|
+
# unchanged; the lock releases so back-to-back calls never time out.
|
|
17
|
+
#
|
|
18
|
+
# Every case builds its own mktemp fixture repo (real `git init`) — the suite
|
|
19
|
+
# never touches the enclosing dev repo's state. Output: one line per case,
|
|
20
|
+
# `case N <tag> PASS` or `case N <tag> FAIL: <why>`; non-zero exit if any FAIL.
|
|
21
|
+
#
|
|
22
|
+
# Usage: sh .forge/checks/tests/forge-reserve.test.sh
|
|
23
|
+
set -u
|
|
24
|
+
|
|
25
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
26
|
+
RESERVE="$SCRIPT_DIR/../../../.claude/hooks/forge-reserve.sh"
|
|
27
|
+
[ -f "$RESERVE" ] || { printf 'FATAL: %s missing\n' "$RESERVE" >&2; exit 1; }
|
|
28
|
+
|
|
29
|
+
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
|
|
30
|
+
export GIT_CONFIG_GLOBAL GIT_CONFIG_SYSTEM
|
|
31
|
+
|
|
32
|
+
ROOT="$(mktemp -d)"
|
|
33
|
+
trap 'rm -rf "$ROOT"' EXIT INT TERM
|
|
34
|
+
|
|
35
|
+
fails=0
|
|
36
|
+
n=0
|
|
37
|
+
|
|
38
|
+
# new_fixture <name> → prints the fixture repo path
|
|
39
|
+
new_fixture() {
|
|
40
|
+
fx="$ROOT/$1"
|
|
41
|
+
mkdir -p "$fx"
|
|
42
|
+
git -C "$fx" init -q
|
|
43
|
+
printf '%s' "$fx"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
report() { # report <tag> <pass|fail> [why]
|
|
47
|
+
n=$((n + 1))
|
|
48
|
+
if [ "$2" = pass ]; then
|
|
49
|
+
printf 'case %d %s PASS\n' "$n" "$1"
|
|
50
|
+
else
|
|
51
|
+
printf 'case %d %s FAIL: %s\n' "$n" "$1" "${3:-unspecified}"
|
|
52
|
+
fails=$((fails + 1))
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# ── FR-236: phase kind ────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
# case 1 phase-archive-aware — roadmap max 12, archived phase dir 14 → P-015
|
|
59
|
+
fx=$(new_fixture c1)
|
|
60
|
+
mkdir -p "$fx/.forge/archive/milestone-9/phases/14-old"
|
|
61
|
+
printf 'roadmap:\n phases:\n - id: 11\n - id: 12\n' > "$fx/.forge/roadmap.yml"
|
|
62
|
+
out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
|
|
63
|
+
if [ "$out" = "P-015" ]; then report phase-archive-aware pass
|
|
64
|
+
else report phase-archive-aware fail "expected P-015 (archived 14 must stay burned), got: $out"; fi
|
|
65
|
+
|
|
66
|
+
# case 2 phase-dual-write — case 1's claim landed in BOTH registries
|
|
67
|
+
if grep -q "phase P-015 m-t" "$fx/.git/forge/id-reservations" 2>/dev/null \
|
|
68
|
+
&& grep -q "id: P-015" "$fx/.forge/reservations.yml" 2>/dev/null; then
|
|
69
|
+
report phase-dual-write pass
|
|
70
|
+
else report phase-dual-write fail "P-015 missing from ledger or reservations.yml"; fi
|
|
71
|
+
|
|
72
|
+
# case 3 phase-sibling-ledger — uncommitted sibling ledger claim is visible
|
|
73
|
+
fx=$(new_fixture c3)
|
|
74
|
+
mkdir -p "$fx/.forge" "$fx/.git/forge"
|
|
75
|
+
printf 'roadmap:\n phases:\n - id: 12\n' > "$fx/.forge/roadmap.yml"
|
|
76
|
+
printf 'phase\tP-015\tm-sib\t2026-07-23T00:00:00Z\n' >> "$fx/.git/forge/id-reservations"
|
|
77
|
+
out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
|
|
78
|
+
if [ "$out" = "P-016" ]; then report phase-sibling-ledger pass
|
|
79
|
+
else report phase-sibling-ledger fail "sibling claim P-015 ignored — expected P-016, got: $out"; fi
|
|
80
|
+
|
|
81
|
+
# case 4 phase-fresh — no roadmap, no archive → P-001
|
|
82
|
+
fx=$(new_fixture c4)
|
|
83
|
+
out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
|
|
84
|
+
if [ "$out" = "P-001" ]; then report phase-fresh pass
|
|
85
|
+
else report phase-fresh fail "fresh repo expected P-001, got: $out"; fi
|
|
86
|
+
|
|
87
|
+
# case 5 phase-wave-keys-not-counted — waves KEYS and bracket ids must not leak
|
|
88
|
+
# into the phase max: roadmap phase max 3, waves key 9 pointing at [3] → P-004.
|
|
89
|
+
fx=$(new_fixture c5)
|
|
90
|
+
mkdir -p "$fx/.forge"
|
|
91
|
+
printf 'roadmap:\n phases:\n - id: 3\n waves:\n 9: [3]\n' > "$fx/.forge/roadmap.yml"
|
|
92
|
+
out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
|
|
93
|
+
if [ "$out" = "P-004" ]; then report phase-wave-keys-not-counted pass
|
|
94
|
+
else report phase-wave-keys-not-counted fail "expected P-004 (wave key 9 is not a phase id), got: $out"; fi
|
|
95
|
+
|
|
96
|
+
# ── FR-237: wave kind ────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
# case 6 wave-max-key — waves keys 1,7 (bracket ids larger) → W-008
|
|
99
|
+
fx=$(new_fixture c6)
|
|
100
|
+
mkdir -p "$fx/.forge"
|
|
101
|
+
printf 'roadmap:\n phases:\n - id: 1\n waves:\n 1: [40]\n 7: [41]\n' > "$fx/.forge/roadmap.yml"
|
|
102
|
+
out=$(cd "$fx" && "$RESERVE" wave --milestone m-t 2>/dev/null)
|
|
103
|
+
if [ "$out" = "W-008" ]; then report wave-max-key pass
|
|
104
|
+
else report wave-max-key fail "expected W-008 (max KEY 7, ids in brackets ignored), got: $out"; fi
|
|
105
|
+
|
|
106
|
+
# case 7 wave-fresh — no waves block anywhere → W-001
|
|
107
|
+
fx=$(new_fixture c7)
|
|
108
|
+
out=$(cd "$fx" && "$RESERVE" wave --milestone m-t 2>/dev/null)
|
|
109
|
+
if [ "$out" = "W-001" ]; then report wave-fresh pass
|
|
110
|
+
else report wave-fresh fail "expected W-001, got: $out"; fi
|
|
111
|
+
|
|
112
|
+
# ── FR-238: version kind (record-only) ───────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
# case 8 version-clean-claim — records in BOTH registries, echoes the value
|
|
115
|
+
fx=$(new_fixture c8)
|
|
116
|
+
out=$(cd "$fx" && "$RESERVE" version --value 0.9.0 --milestone m-a 2>/dev/null)
|
|
117
|
+
if [ "$out" = "0.9.0" ] \
|
|
118
|
+
&& grep -q "version 0.9.0 m-a" "$fx/.git/forge/id-reservations" 2>/dev/null \
|
|
119
|
+
&& grep -q 'id: 0.9.0' "$fx/.forge/reservations.yml" 2>/dev/null; then
|
|
120
|
+
report version-clean-claim pass
|
|
121
|
+
else report version-clean-claim fail "claim not recorded in both registries (stdout: $out)"; fi
|
|
122
|
+
|
|
123
|
+
# case 9 version-conflict-refused — different milestone, same value → exit 1,
|
|
124
|
+
# stderr names the holder, NOTHING written
|
|
125
|
+
err=$(cd "$fx" && "$RESERVE" version --value 0.9.0 --milestone m-b 2>&1 >/dev/null)
|
|
126
|
+
rc=$?
|
|
127
|
+
lines=$(grep -c '^version' "$fx/.git/forge/id-reservations" 2>/dev/null)
|
|
128
|
+
case "$err" in
|
|
129
|
+
*m-a*) named=yes ;;
|
|
130
|
+
*) named=no ;;
|
|
131
|
+
esac
|
|
132
|
+
if [ "$rc" -ne 0 ] && [ "$named" = yes ] && [ "$lines" = "1" ]; then
|
|
133
|
+
report version-conflict-refused pass
|
|
134
|
+
else report version-conflict-refused fail "rc=$rc named-holder=$named ledger-version-lines=$lines (want rc!=0, holder m-a named, 1 line)"; fi
|
|
135
|
+
|
|
136
|
+
# case 10 version-idempotent-reclaim — same milestone re-claims → exit 0, no dup
|
|
137
|
+
out=$(cd "$fx" && "$RESERVE" version --value 0.9.0 --milestone m-a 2>/dev/null)
|
|
138
|
+
rc=$?
|
|
139
|
+
lines=$(grep -c '^version' "$fx/.git/forge/id-reservations" 2>/dev/null)
|
|
140
|
+
if [ "$rc" -eq 0 ] && [ "$out" = "0.9.0" ] && [ "$lines" = "1" ]; then
|
|
141
|
+
report version-idempotent-reclaim pass
|
|
142
|
+
else report version-idempotent-reclaim fail "rc=$rc stdout=$out ledger-version-lines=$lines (want 0 / 0.9.0 / 1)"; fi
|
|
143
|
+
|
|
144
|
+
# case 11 version-missing-value — usage exit 2, nothing written
|
|
145
|
+
fx=$(new_fixture c11)
|
|
146
|
+
err=$(cd "$fx" && "$RESERVE" version --milestone m-a 2>&1 >/dev/null)
|
|
147
|
+
rc=$?
|
|
148
|
+
if [ "$rc" -eq 2 ] && [ ! -f "$fx/.git/forge/id-reservations" ] && [ ! -f "$fx/.forge/reservations.yml" ]; then
|
|
149
|
+
report version-missing-value pass
|
|
150
|
+
else report version-missing-value fail "rc=$rc (want 2) or files written on usage error"; fi
|
|
151
|
+
|
|
152
|
+
# ── NFR-045: existing kinds regression + lock release ────────────────────────
|
|
153
|
+
|
|
154
|
+
# case 12 regression-existing-kinds — fr x2 / milestone / refactor sequence and
|
|
155
|
+
# reservations.yml shape identical to pre-m-41 behavior
|
|
156
|
+
fx=$(new_fixture c12)
|
|
157
|
+
a=$(cd "$fx" && "$RESERVE" fr --milestone m-t 2>/dev/null)
|
|
158
|
+
b=$(cd "$fx" && "$RESERVE" fr --milestone m-t 2>/dev/null)
|
|
159
|
+
m=$(cd "$fx" && "$RESERVE" milestone --milestone m-new 2>/dev/null)
|
|
160
|
+
r=$(cd "$fx" && "$RESERVE" refactor --milestone m-t 2>/dev/null)
|
|
161
|
+
if [ "$a $b $m $r" = "FR-001 FR-002 M-1 R001" ] \
|
|
162
|
+
&& grep -q '^reservations:' "$fx/.forge/reservations.yml" 2>/dev/null \
|
|
163
|
+
&& grep -q 'kind: refactor' "$fx/.forge/reservations.yml" 2>/dev/null; then
|
|
164
|
+
report regression-existing-kinds pass
|
|
165
|
+
else report regression-existing-kinds fail "expected FR-001 FR-002 M-1 R001 + seeded header, got: $a $b $m $r"; fi
|
|
166
|
+
|
|
167
|
+
# case 13 regression-lock-release — back-to-back calls in one fixture must not
|
|
168
|
+
# time out (a leaked lock would make the second call exit 3)
|
|
169
|
+
fx=$(new_fixture c13)
|
|
170
|
+
(cd "$fx" && "$RESERVE" adr --milestone m-t >/dev/null 2>&1)
|
|
171
|
+
out=$(cd "$fx" && FORGE_RESERVE_WAIT=2 "$RESERVE" adr --milestone m-t 2>/dev/null)
|
|
172
|
+
rc=$?
|
|
173
|
+
if [ "$rc" -eq 0 ] && [ "$out" = "ADR-002" ]; then
|
|
174
|
+
report regression-lock-release pass
|
|
175
|
+
else report regression-lock-release fail "second call rc=$rc out=$out (leaked lock?)"; fi
|
|
176
|
+
|
|
177
|
+
# ── input hardening (review, m-41): validation before the lock, nothing written ─
|
|
178
|
+
|
|
179
|
+
# case 14 version-value-rejects-non-semver — --value must be X.Y.Z; a non-semver
|
|
180
|
+
# (here a YAML-injection payload) → exit 2, NOTHING written
|
|
181
|
+
fx=$(new_fixture c14)
|
|
182
|
+
err=$(cd "$fx" && "$RESERVE" version --value '9.9.9
|
|
183
|
+
injected: pwned' --milestone m-a 2>&1 >/dev/null)
|
|
184
|
+
rc=$?
|
|
185
|
+
if [ "$rc" -eq 2 ] && [ ! -f "$fx/.git/forge/id-reservations" ] && [ ! -f "$fx/.forge/reservations.yml" ]; then
|
|
186
|
+
report version-value-rejects-non-semver pass
|
|
187
|
+
else report version-value-rejects-non-semver fail "rc=$rc (want 2) or files written on bad --value"; fi
|
|
188
|
+
|
|
189
|
+
# case 15 version-value-accepts-prerelease — X.Y.Z-pre is valid semver → recorded
|
|
190
|
+
fx=$(new_fixture c15)
|
|
191
|
+
out=$(cd "$fx" && "$RESERVE" version --value 1.2.0-rc.1 --milestone m-a 2>/dev/null)
|
|
192
|
+
if [ "$out" = "1.2.0-rc.1" ]; then report version-value-accepts-prerelease pass
|
|
193
|
+
else report version-value-accepts-prerelease fail "expected 1.2.0-rc.1 recorded, got: $out"; fi
|
|
194
|
+
|
|
195
|
+
# case 16 milestone-rejects-control-char — a newline in --milestone would split
|
|
196
|
+
# the ledger row / inject a sibling YAML key → exit 2, NOTHING written
|
|
197
|
+
fx=$(new_fixture c16)
|
|
198
|
+
err=$(cd "$fx" && "$RESERVE" fr --milestone 'm-1
|
|
199
|
+
injected: pwned' 2>&1 >/dev/null)
|
|
200
|
+
rc=$?
|
|
201
|
+
if [ "$rc" -eq 2 ] && [ ! -f "$fx/.git/forge/id-reservations" ] && [ ! -f "$fx/.forge/reservations.yml" ]; then
|
|
202
|
+
report milestone-rejects-control-char pass
|
|
203
|
+
else report milestone-rejects-control-char fail "rc=$rc (want 2) or files written on control-char milestone"; fi
|
|
204
|
+
|
|
205
|
+
# case 17 symlink-target-refused — reservations.yml is a symlink → refuse to
|
|
206
|
+
# write through it, exit 2, and the symlink target stays untouched
|
|
207
|
+
fx=$(new_fixture c17)
|
|
208
|
+
mkdir -p "$fx/.forge"
|
|
209
|
+
: > "$ROOT/c17-victim.txt"
|
|
210
|
+
ln -s "$ROOT/c17-victim.txt" "$fx/.forge/reservations.yml"
|
|
211
|
+
err=$(cd "$fx" && "$RESERVE" fr --milestone m-t 2>&1 >/dev/null)
|
|
212
|
+
rc=$?
|
|
213
|
+
if [ "$rc" -eq 2 ] && [ ! -s "$ROOT/c17-victim.txt" ]; then
|
|
214
|
+
report symlink-target-refused pass
|
|
215
|
+
else report symlink-target-refused fail "rc=$rc (want 2) or victim written through symlink"; fi
|
|
216
|
+
|
|
217
|
+
# case 18 main-floor — cross-machine/cold-start floor: main:reservations.yml
|
|
218
|
+
# carries a merged P-020, empty ledger + roadmap → next phase is P-021
|
|
219
|
+
fx=$(new_fixture c18)
|
|
220
|
+
mkdir -p "$fx/.forge"
|
|
221
|
+
printf 'reservations:\n - kind: phase\n id: P-020\n milestone: m-old\n' > "$fx/.forge/reservations.yml"
|
|
222
|
+
git -C "$fx" add .forge/reservations.yml >/dev/null 2>&1
|
|
223
|
+
git -C "$fx" -c user.email=t@t -c user.name=t commit -qm seed >/dev/null 2>&1
|
|
224
|
+
git -C "$fx" branch -M main >/dev/null 2>&1
|
|
225
|
+
out=$(cd "$fx" && "$RESERVE" phase --milestone m-t 2>/dev/null)
|
|
226
|
+
if [ "$out" = "P-021" ]; then report main-floor pass
|
|
227
|
+
else report main-floor fail "main floor P-020 ignored — expected P-021, got: $out"; fi
|
|
228
|
+
|
|
229
|
+
# ── summary ──────────────────────────────────────────────────────────────────
|
|
230
|
+
if [ "$fails" -eq 0 ]; then
|
|
231
|
+
printf 'forge-reserve.test.sh: all %d cases PASS\n' "$n"
|
|
232
|
+
exit 0
|
|
233
|
+
fi
|
|
234
|
+
printf 'forge-reserve.test.sh: %d of %d cases FAILED\n' "$fails" "$n" >&2
|
|
235
|
+
exit 1
|