forge-orkes 0.77.0 → 0.80.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/bin/create-forge.js +27 -0
- package/experimental/m10/README.md +4 -4
- package/experimental/m10/install.sh +10 -9
- package/experimental/m10/source/hooks/protect-primary-checkout.sh +149 -0
- package/package.json +1 -1
- package/template/.claude/hooks/forge-context-migrate.sh +297 -0
- package/template/.claude/hooks/forge-size-gate.sh +175 -0
- package/template/.claude/hooks/forge-state-rollup.sh +135 -2
- package/template/.claude/hooks/protect-primary-checkout.sh +149 -0
- package/template/.claude/hooks/skill-gate-tiers.txt +6 -0
- package/template/.claude/hooks/tests/forge-context-migrate.test.sh +166 -0
- package/template/.claude/hooks/tests/forge-size-gate.test.sh +132 -0
- package/template/.claude/hooks/tests/protect-primary-checkout.test.sh +147 -0
- package/template/.claude/settings.json +14 -1
- package/template/.claude/skills/discussing/SKILL.md +30 -31
- package/template/.claude/skills/executing/SKILL.md +3 -3
- package/template/.claude/skills/forge/SKILL.md +112 -170
- package/template/.claude/skills/forge/references/manual-fallback-procedures.md +54 -0
- package/template/.claude/skills/initializing/SKILL.md +20 -1
- package/template/.claude/skills/planning/SKILL.md +11 -9
- package/template/.claude/skills/researching/SKILL.md +2 -0
- package/template/.claude/skills/reviewing/SKILL.md +17 -0
- package/template/.claude/skills/upgrading/SKILL.md +22 -2
- package/template/.claude/skills/verifying/SKILL.md +2 -2
- package/template/.forge/FORGE.md +81 -90
- package/template/.forge/bin/new-worktree.sh +61 -0
- package/template/.forge/bin/tests/new-worktree.test.sh +70 -0
- package/template/.forge/checks/forge-jarvis-awareness.sh +46 -2
- package/template/.forge/checks/forge-jarvis-relay.sh +64 -16
- package/template/.forge/checks/tests/forge-jarvis-awareness.test.sh +58 -0
- package/template/.forge/checks/tests/forge-jarvis-relay.test.sh +50 -0
- package/template/.forge/git-hooks/pre-commit +57 -0
- package/template/.forge/git-hooks/tests/pre-commit.test.sh +97 -0
- package/template/.forge/migrations/0.78.0-weld-primary-checkout.md +66 -0
- package/template/.forge/migrations/0.80.0-context-md-sharding.md +158 -0
- package/template/.forge/migrations/0.80.0-size-gate-wiring.md +145 -0
- package/template/.forge/templates/constitution.md +1 -1
- package/template/.forge/templates/context-milestone.md +30 -0
- package/template/.forge/templates/slice-runner/config.yml +7 -1
- package/template/.forge/templates/state/desire-path.yml +3 -0
- package/experimental/m10/source/hooks/forge-branch-guard.sh +0 -61
|
@@ -66,23 +66,71 @@ payload_claims_landed() { # payload_claims_landed <payload>
|
|
|
66
66
|
printf '%s' "${1:-}" | grep -qiE 'merged|landed|validated|promoted|on main|is live|shipped|deployed'
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
# ── milestone name lookup (issue #34 — an operator with 40+ milestones in flight can't tell
|
|
70
|
+
# which slice a bare buzz is about). The awk extraction is kept textually aligned with
|
|
71
|
+
# milestone_name in forge-jarvis-awareness.sh — same guard, same nested `milestone: / name:`
|
|
72
|
+
# parse, same degrade-to-empty on any miss. <forge-dir> here is the slice's `.forge` directory
|
|
73
|
+
# (NOT the worktree root — see the two-dirname derivation in build_relay_text below); the state
|
|
74
|
+
# file lives at <forge-dir>/state/milestone-<id>.yml. ────────────────────────────────────────
|
|
75
|
+
milestone_name() { # milestone_name <milestone-id> <forge-dir>
|
|
76
|
+
_mid="${1:-}"; _fd="${2:-}"
|
|
77
|
+
[ -n "$_mid" ] && [ -n "$_fd" ] || { printf ''; return 0; }
|
|
78
|
+
_id="${_mid#m-}"
|
|
79
|
+
_f="$_fd/state/milestone-$_id.yml"
|
|
80
|
+
[ -f "$_f" ] || { printf ''; return 0; }
|
|
81
|
+
awk '
|
|
82
|
+
/^milestone:/ { inp=1; next }
|
|
83
|
+
inp && /^[A-Za-z]/ { inp=0 }
|
|
84
|
+
inp && /^[[:space:]]+name:/ {
|
|
85
|
+
sub(/^[[:space:]]*[^:]+:[[:space:]]*/, "")
|
|
86
|
+
sub(/[[:space:]]*#.*/, "")
|
|
87
|
+
gsub(/^["'"'"']|["'"'"']$/, "")
|
|
88
|
+
print; exit
|
|
89
|
+
}
|
|
90
|
+
' "$_f"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# ── "m-<id> — <name>", truncated ~60 chars; degrades to the bare id (never a blank "m- — "
|
|
94
|
+
# prefix) when the name can't be resolved or the id itself is empty. ─────────────────────────
|
|
95
|
+
named_milestone() { # named_milestone <milestone-id> <forge-dir>
|
|
96
|
+
_mid="${1:-}"; _fd="${2:-}"
|
|
97
|
+
[ -n "$_mid" ] || { printf ''; return 0; }
|
|
98
|
+
_nm="$(milestone_name "$_mid" "$_fd")"
|
|
99
|
+
if [ -n "$_nm" ]; then
|
|
100
|
+
printf '%s' "$_mid — $_nm" | cut -c1-60
|
|
101
|
+
else
|
|
102
|
+
printf '%s' "$_mid"
|
|
103
|
+
fi
|
|
104
|
+
}
|
|
105
|
+
|
|
69
106
|
# ── build the relay text for ONE parsed ping (the pure host-truth text-builder) ───────────────
|
|
70
|
-
# Reads: $2=type $3=payload ; $MILESTONE (optional, for fold
|
|
71
|
-
|
|
72
|
-
|
|
107
|
+
# Reads: $2=type $3=payload $4=log-path (optional) ; $MILESTONE (optional, for fold + naming).
|
|
108
|
+
# Prints one line to stdout, prefixed "m-<id> — <name>: " when a name can be resolved (issue #34)
|
|
109
|
+
# — degrades to a bare "m-<id>: " prefix, or no prefix at all when $MILESTONE is also empty.
|
|
110
|
+
build_relay_text() { # build_relay_text <type> <payload> [<log-path>]
|
|
111
|
+
_type="${1:-note}"; _payload="${2:-}"; _LOG="${3:-}"
|
|
112
|
+
if [ -n "$_LOG" ]; then
|
|
113
|
+
# mode_scan's log path is always <worktree>/.forge/runner-work/notify.log — two dirnames
|
|
114
|
+
# land on <worktree>/.forge (the forge-dir milestone_name expects), not the worktree root.
|
|
115
|
+
_fd="$(dirname "$(dirname "$_LOG")")"
|
|
116
|
+
_named="$(named_milestone "${MILESTONE:-}" "$_fd")"
|
|
117
|
+
else
|
|
118
|
+
_named="${MILESTONE:-}"
|
|
119
|
+
fi
|
|
120
|
+
if [ -n "$_named" ]; then _prefix="$_named: "; else _prefix=""; fi
|
|
73
121
|
case "$_type" in
|
|
74
122
|
ambiguity)
|
|
75
123
|
# A QUESTION — relay verbatim, named. Legal: it is not a state claim (acceptance #2).
|
|
76
|
-
printf '❓ Needs you (ambiguity): %s' "$_payload" ;;
|
|
124
|
+
printf '%s❓ Needs you (ambiguity): %s' "$_prefix" "$_payload" ;;
|
|
77
125
|
fork)
|
|
78
126
|
# payload is a PATH to a sketch the operator LOOKS AT (§8) — name it.
|
|
79
|
-
printf '🎨 Design fork — review the sketch: %s' "$_payload" ;;
|
|
127
|
+
printf '%s🎨 Design fork — review the sketch: %s' "$_prefix" "$_payload" ;;
|
|
80
128
|
irreversible)
|
|
81
129
|
# a gate on an irreversible act — your call; relay verbatim, named.
|
|
82
|
-
printf '⚠️ Irreversible action — your call: %s' "$_payload" ;;
|
|
130
|
+
printf '%s⚠️ Irreversible action — your call: %s' "$_prefix" "$_payload" ;;
|
|
83
131
|
budget)
|
|
84
132
|
# the runner's OWN spend tally — no host source to recompute against; relay verbatim, named.
|
|
85
|
-
printf '💸 Budget cap reached: %s' "$_payload" ;;
|
|
133
|
+
printf '%s💸 Budget cap reached: %s' "$_prefix" "$_payload" ;;
|
|
86
134
|
done|halt)
|
|
87
135
|
# STATE-CLAIM territory. Recompute host truth FIRST; build the claim from it, not the payload.
|
|
88
136
|
_rs="$(host_release_state "${MILESTONE:-}")"
|
|
@@ -90,25 +138,25 @@ build_relay_text() { # build_relay_text <type> <payload>
|
|
|
90
138
|
if payload_claims_landed "$_payload"; then
|
|
91
139
|
if [ "$_rs" = unmerged ]; then
|
|
92
140
|
# The standing lesson: payload says merged, host says not. Relay the host, loudly.
|
|
93
|
-
printf '⚠️ DISCREPANCY — payload claims landed/merged, but host shows release_state=unmerged. %s (host truth wins). Payload said: %s' "$_base" "$_payload"
|
|
141
|
+
printf '%s⚠️ DISCREPANCY — payload claims landed/merged, but host shows release_state=unmerged. %s (host truth wins). Payload said: %s' "$_prefix" "$_base" "$_payload"
|
|
94
142
|
elif [ "$_rs" = unknown ]; then
|
|
95
|
-
printf '⚠️ %s — payload claims landed/merged but the host could NOT be verified (release_state=unknown). Payload said: %s' "$_base" "$_payload"
|
|
143
|
+
printf '%s⚠️ %s — payload claims landed/merged but the host could NOT be verified (release_state=unknown). Payload said: %s' "$_prefix" "$_base" "$_payload"
|
|
96
144
|
else
|
|
97
145
|
# payload claims landed AND host agrees (merged/validated) — no discrepancy.
|
|
98
|
-
printf '%s — host confirms: release_state=%s. %s' "$_base" "$_rs" "$_payload"
|
|
146
|
+
printf '%s%s — host confirms: release_state=%s. %s' "$_prefix" "$_base" "$_rs" "$_payload"
|
|
99
147
|
fi
|
|
100
148
|
else
|
|
101
|
-
printf '%s — host: release_state=%s. %s' "$_base" "$_rs" "$_payload"
|
|
149
|
+
printf '%s%s — host: release_state=%s. %s' "$_prefix" "$_base" "$_rs" "$_payload"
|
|
102
150
|
fi ;;
|
|
103
151
|
*)
|
|
104
152
|
# unknown type — relay raw, named (never silently drop).
|
|
105
|
-
printf '(%s): %s' "$_type" "$_payload" ;;
|
|
153
|
+
printf '%s(%s): %s' "$_prefix" "$_type" "$_payload" ;;
|
|
106
154
|
esac
|
|
107
155
|
}
|
|
108
156
|
|
|
109
157
|
# Parse a TSV log line (ts<TAB>type<TAB>payload) then build text. Prints one line.
|
|
110
|
-
relay_one_line() { # relay_one_line <tsv-line>
|
|
111
|
-
_line="$1"
|
|
158
|
+
relay_one_line() { # relay_one_line <tsv-line> [<log-path>]
|
|
159
|
+
_line="$1"; _LOG="${2:-}"
|
|
112
160
|
_type="$(printf '%s' "$_line" | cut -f2)"
|
|
113
161
|
_payload="$(printf '%s' "$_line" | cut -f3-)"
|
|
114
162
|
# A malformed line with no tabs → cut returns the whole line for f2/f3; treat as a raw note.
|
|
@@ -116,7 +164,7 @@ relay_one_line() { # relay_one_line <tsv-line>
|
|
|
116
164
|
*" "*) : ;; # has a tab — well-formed
|
|
117
165
|
*) _type='note'; _payload="$_line" ;;
|
|
118
166
|
esac
|
|
119
|
-
build_relay_text "$_type" "$_payload"
|
|
167
|
+
build_relay_text "$_type" "$_payload" "$_LOG"
|
|
120
168
|
}
|
|
121
169
|
|
|
122
170
|
# ── modes — one function per mode, each owning its own arg loop ───────────────────────────────
|
|
@@ -211,7 +259,7 @@ mode_scan() {
|
|
|
211
259
|
_n=$((_n + 1))
|
|
212
260
|
_marker="$MARKER_DIR/$_n.relayed"
|
|
213
261
|
[ -f "$_marker" ] && continue # at-most-once: already relayed, never re-buzz
|
|
214
|
-
_text="$(relay_one_line "$_l")"
|
|
262
|
+
_text="$(relay_one_line "$_l" "$LOG")"
|
|
215
263
|
# Write the marker BEFORE emitting — a crash after emit must not double-buzz; a crash
|
|
216
264
|
# before emit leaves it unmarked and it re-surfaces as while-you-were-away (no silent loss).
|
|
217
265
|
# A FAILED marker write must not emit either: emitting unmarked would re-buzz on every
|
|
@@ -167,5 +167,63 @@ else
|
|
|
167
167
|
bad 'guard: bare-number branch fix-issue-2 (no m- token) is not discovered' "$OUT"
|
|
168
168
|
fi
|
|
169
169
|
|
|
170
|
+
# --- 13-16. NAMED MILESTONES (issue #34): "m-<id> — <name>" at both print sites, degrading to
|
|
171
|
+
# the bare id when the name can't be resolved — no crash either way. ------------------------
|
|
172
|
+
WTN="$ROOT/worktrees-named"; mkdir -p "$WTN/m-50/.forge/state" "$WTN/m-51"
|
|
173
|
+
cat > "$WTN/m-50/.forge/state/milestone-50.yml" <<'EOF'
|
|
174
|
+
milestone:
|
|
175
|
+
id: m-50
|
|
176
|
+
name: "Named milestone fixture"
|
|
177
|
+
origin: null
|
|
178
|
+
|
|
179
|
+
current:
|
|
180
|
+
status: executing
|
|
181
|
+
EOF
|
|
182
|
+
# m-51 has NO .forge/state/milestone-51.yml — the no-file degrade case.
|
|
183
|
+
|
|
184
|
+
OUT="$(FORGE_JARVIS_FOLD="$FOLDSTUB" FORGE_JARVIS_BOARD="$BOARDSTUB" \
|
|
185
|
+
sh "$AWARE" --dry-run --worktree-root "$WTN" 2>&1)"
|
|
186
|
+
|
|
187
|
+
# 13. slice-list row WITH a resolvable milestone.name shows "m-<id> — <name>"
|
|
188
|
+
if printf '%s' "$OUT" | grep -q 'm-50 — Named milestone fixture'; then
|
|
189
|
+
ok 'slice-list: milestone with a resolvable name renders "m-<id> — <name>"'
|
|
190
|
+
else
|
|
191
|
+
bad 'slice-list: milestone with a resolvable name renders "m-<id> — <name>"' "$OUT"
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
# 14. slice-list row with NO milestone state file still renders — degrades to the bare id
|
|
195
|
+
if printf '%s' "$OUT" | grep -qE '(^|[[:space:]])m-51([[:space:]]|$)'; then
|
|
196
|
+
ok 'slice-list: no state file degrades to the bare id, no crash'
|
|
197
|
+
else
|
|
198
|
+
bad 'slice-list: no state file degrades to the bare id, no crash' "$OUT"
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
# stub board: one row WITH a "name" field, one row WITHOUT — pins the board-side degrade
|
|
202
|
+
BOARDSTUB_NAMED="$ROOT/board-named.sh"
|
|
203
|
+
cat > "$BOARDSTUB_NAMED" <<'EOF'
|
|
204
|
+
#!/bin/sh
|
|
205
|
+
[ "$1" = "--emit" ] || exit 2
|
|
206
|
+
printf '{"forge_id": "m-60", "status": "executing", "release_state": "unmerged", "name": "Board-named unit"}\n'
|
|
207
|
+
printf '{"forge_id": "m-61", "status": "executing", "release_state": "unmerged"}\n'
|
|
208
|
+
EOF
|
|
209
|
+
chmod +x "$BOARDSTUB_NAMED"
|
|
210
|
+
|
|
211
|
+
OUT="$(FORGE_JARVIS_FOLD="$FOLDSTUB" FORGE_JARVIS_BOARD="$BOARDSTUB_NAMED" \
|
|
212
|
+
sh "$AWARE" --worktree-root "$EMPTY" 2>&1)"
|
|
213
|
+
|
|
214
|
+
# 15. board-projection row WITH a "name" field shows "m-<id> — <name>"
|
|
215
|
+
if printf '%s' "$OUT" | grep -q 'm-60 — Board-named unit status=executing'; then
|
|
216
|
+
ok 'board projection: a row with a name field renders "m-<id> — <name>"'
|
|
217
|
+
else
|
|
218
|
+
bad 'board projection: a row with a name field renders "m-<id> — <name>"' "$OUT"
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
# 16. board-projection row missing the "name" field degrades to the bare id (no crash)
|
|
222
|
+
if printf '%s' "$OUT" | grep -q 'm-61 status=executing' && ! printf '%s' "$OUT" | grep -q 'm-61 — '; then
|
|
223
|
+
ok 'board projection: no name field degrades to the bare id, no crash'
|
|
224
|
+
else
|
|
225
|
+
bad 'board projection: no name field degrades to the bare id, no crash' "$OUT"
|
|
226
|
+
fi
|
|
227
|
+
|
|
170
228
|
printf '\nforge-jarvis-awareness.test.sh: %s passed, %s failed\n' "$PASSED" "$FAILED"
|
|
171
229
|
[ "$FAILED" -eq 0 ]
|
|
@@ -160,6 +160,56 @@ else
|
|
|
160
160
|
bad "guard: bare-number branch fix-issue-2 (no m- token) emits no log path" "$out"
|
|
161
161
|
fi
|
|
162
162
|
|
|
163
|
+
# --- 16-19. NAMED MILESTONES (issue #34): build_relay_text prefixes "m-<id> — <name>" onto
|
|
164
|
+
# every buzz type, resolving the name from $LOG's worktree root; degrades to the bare
|
|
165
|
+
# "m-<id>" prefix when the state file can't be resolved — never a crash either way. ----------
|
|
166
|
+
WTN="$ROOT/slice-wt-named"; mkdir -p "$WTN/.forge/state" "$WTN/.forge/runner-work"
|
|
167
|
+
cat > "$WTN/.forge/state/milestone-77.yml" <<'EOF'
|
|
168
|
+
milestone:
|
|
169
|
+
id: m-77
|
|
170
|
+
name: "Named relay fixture"
|
|
171
|
+
origin: null
|
|
172
|
+
|
|
173
|
+
current:
|
|
174
|
+
status: executing
|
|
175
|
+
EOF
|
|
176
|
+
LOGN="$WTN/.forge/runner-work/notify.log"
|
|
177
|
+
printf '2026-07-21T10:00:00Z\tambiguity\tJWT or sessions?\n' >> "$LOGN"
|
|
178
|
+
printf '2026-07-21T10:01:00Z\tdone\tPR opened\n' >> "$LOGN"
|
|
179
|
+
printf '2026-07-21T10:02:00Z\thalt\tstuck\n' >> "$LOGN"
|
|
180
|
+
|
|
181
|
+
scan_named() { FORGE_JARVIS_FOLD="$FOLD_UNMERGED" sh "$RELAY" --scan "$LOGN" --milestone m-77 2>/dev/null; }
|
|
182
|
+
out="$(scan_named)"
|
|
183
|
+
|
|
184
|
+
# 16. ambiguity buzz is prefixed "m-<id> — <name>"
|
|
185
|
+
case "$out" in
|
|
186
|
+
*"m-77 — Named relay fixture: "*"Needs you (ambiguity)"*) ok "ambiguity buzz prefixed with the resolved milestone name" ;;
|
|
187
|
+
*) bad "ambiguity buzz prefixed with the resolved milestone name" "$out" ;;
|
|
188
|
+
esac
|
|
189
|
+
|
|
190
|
+
# 17. done buzz is prefixed "m-<id> — <name>"
|
|
191
|
+
case "$out" in
|
|
192
|
+
*"m-77 — Named relay fixture: "*"Slice done"*) ok "done buzz prefixed with the resolved milestone name" ;;
|
|
193
|
+
*) bad "done buzz prefixed with the resolved milestone name" "$out" ;;
|
|
194
|
+
esac
|
|
195
|
+
|
|
196
|
+
# 18. halt buzz is prefixed "m-<id> — <name>"
|
|
197
|
+
case "$out" in
|
|
198
|
+
*"m-77 — Named relay fixture: "*"Slice HALTED"*) ok "halt buzz prefixed with the resolved milestone name" ;;
|
|
199
|
+
*) bad "halt buzz prefixed with the resolved milestone name" "$out" ;;
|
|
200
|
+
esac
|
|
201
|
+
|
|
202
|
+
# 19. a worktree whose milestone state can't be resolved still fires — degrades to the bare
|
|
203
|
+
# "m-<id>" prefix, no crash.
|
|
204
|
+
WTB="$ROOT/slice-wt-bare"; mkdir -p "$WTB/.forge/runner-work" # no .forge/state at all
|
|
205
|
+
LOGB="$WTB/.forge/runner-work/notify.log"
|
|
206
|
+
printf '2026-07-21T10:00:00Z\tbudget\tcap hit\n' >> "$LOGB"
|
|
207
|
+
out2="$(FORGE_JARVIS_FOLD="$FOLD_UNMERGED" sh "$RELAY" --scan "$LOGB" --milestone m-88 2>/dev/null)"
|
|
208
|
+
case "$out2" in
|
|
209
|
+
*"m-88: "*"Budget cap reached"*) ok "unresolvable milestone state degrades to the bare id prefix, no crash" ;;
|
|
210
|
+
*) bad "unresolvable milestone state degrades to the bare id prefix, no crash" "$out2" ;;
|
|
211
|
+
esac
|
|
212
|
+
|
|
163
213
|
# --- summary -----------------------------------------------------------------
|
|
164
214
|
printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
|
|
165
215
|
[ "$FAILED" = 0 ]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Backstop for the primary-checkout branch weld (issue #21, ADR-031).
|
|
3
|
+
#
|
|
4
|
+
# Refuses commits made in the PRIMARY checkout while HEAD is not the default branch
|
|
5
|
+
# (or is detached). The Claude PreToolUse hook (.claude/hooks/protect-primary-checkout.sh)
|
|
6
|
+
# blocks branch create/switch at the source; this git hook catches anything that slips
|
|
7
|
+
# past it — humans, non-agent tools, cd-inside-compound-command races — at the moment
|
|
8
|
+
# damage would occur: a commit landing on a branch some other concurrent session owns.
|
|
9
|
+
#
|
|
10
|
+
# Linked worktrees are unaffected (git-dir != git-common-dir there — their own HEAD is
|
|
11
|
+
# the whole point). Commits ON the default branch in the primary checkout remain allowed
|
|
12
|
+
# by design — Forge's own state-sync commits land there.
|
|
13
|
+
#
|
|
14
|
+
# Default branch is resolved dynamically (main/master + init.defaultBranch), never
|
|
15
|
+
# hardcoded. Fails OPEN on any git-probe error (a hook must not wedge commits on its own
|
|
16
|
+
# malfunction). Exit 0 = allow the commit; exit 1 = abort it.
|
|
17
|
+
#
|
|
18
|
+
# Activate once per clone (done by initializing / upgrading):
|
|
19
|
+
# git config core.hooksPath .forge/git-hooks
|
|
20
|
+
# Escape hatch (deliberate, human use): ALLOW_MAIN_BRANCHING=1 git commit ...
|
|
21
|
+
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
|
|
24
|
+
[ "${ALLOW_MAIN_BRANCHING:-0}" = "1" ] && exit 0
|
|
25
|
+
|
|
26
|
+
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) || exit 0
|
|
27
|
+
COMMON_DIR=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
|
|
28
|
+
GIT_DIR_ABS=$(cd "$GIT_DIR" 2>/dev/null && pwd) || exit 0
|
|
29
|
+
COMMON_DIR_ABS=$(cd "$COMMON_DIR" 2>/dev/null && pwd) || exit 0
|
|
30
|
+
|
|
31
|
+
# Linked worktree → its own HEAD; branch commits are the whole point. Allow.
|
|
32
|
+
[ "$GIT_DIR_ABS" != "$COMMON_DIR_ABS" ] && exit 0
|
|
33
|
+
|
|
34
|
+
# Default-branch set (dynamic — mirrors protect-primary-checkout.sh).
|
|
35
|
+
DEFAULT_BRANCHES="main master"
|
|
36
|
+
GIT_DEFAULT=$(git config --get init.defaultBranch 2>/dev/null || true)
|
|
37
|
+
[ -n "$GIT_DEFAULT" ] && DEFAULT_BRANCHES="$DEFAULT_BRANCHES $GIT_DEFAULT"
|
|
38
|
+
|
|
39
|
+
BRANCH=$(git branch --show-current 2>/dev/null || true)
|
|
40
|
+
|
|
41
|
+
if [ -z "$BRANCH" ]; then
|
|
42
|
+
echo "✋ Commit blocked: detached HEAD in the primary checkout." >&2
|
|
43
|
+
echo " Return to the default branch (git checkout main) or move this work to a worktree:" >&2
|
|
44
|
+
echo " bash .forge/bin/new-worktree.sh <slug>" >&2
|
|
45
|
+
echo " Human escape hatch: ALLOW_MAIN_BRANCHING=1 git commit ..." >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
for b in $DEFAULT_BRANCHES; do
|
|
50
|
+
[ "$BRANCH" = "$b" ] && exit 0 # on the default branch → allowed (state-sync commits land here)
|
|
51
|
+
done
|
|
52
|
+
|
|
53
|
+
echo "✋ Commit blocked: the primary checkout must stay on the default branch (you are on '$BRANCH')." >&2
|
|
54
|
+
echo " Another concurrent session may own this branch — committing here is how work gets clobbered." >&2
|
|
55
|
+
echo " Branch work belongs in a worktree: bash .forge/bin/new-worktree.sh <slug>" >&2
|
|
56
|
+
echo " Human escape hatch: ALLOW_MAIN_BRANCHING=1 git commit ..." >&2
|
|
57
|
+
exit 1
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Scratch-repo scenario tests for .forge/git-hooks/pre-commit (issue #21 backstop).
|
|
3
|
+
#
|
|
4
|
+
# Each scenario builds a throwaway repo (and, where needed, a linked worktree), then
|
|
5
|
+
# invokes the pre-commit hook directly from the target dir and asserts its exit code
|
|
6
|
+
# (0 = commit allowed, 1 = commit aborted). No real `git commit` is needed — invoking
|
|
7
|
+
# the hook is deterministic and fast. Self-cleans on exit. Run:
|
|
8
|
+
# ./.forge/git-hooks/tests/pre-commit.test.sh
|
|
9
|
+
# Prints `ALL PASS` and exits 0 iff every scenario passes; non-zero on any failure.
|
|
10
|
+
|
|
11
|
+
set -uo pipefail
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
14
|
+
HOOK="$SCRIPT_DIR/../pre-commit"
|
|
15
|
+
[ -x "$HOOK" ] || { printf 'FATAL: pre-commit not executable: %s\n' "$HOOK" >&2; exit 1; }
|
|
16
|
+
|
|
17
|
+
ROOT="$(mktemp -d)"
|
|
18
|
+
trap 'rm -rf "$ROOT"' EXIT INT TERM
|
|
19
|
+
|
|
20
|
+
PASSED=0
|
|
21
|
+
FAILED=0
|
|
22
|
+
|
|
23
|
+
new_repo() {
|
|
24
|
+
local d="$ROOT/$1" br="${2:-main}"
|
|
25
|
+
mkdir -p "$d"
|
|
26
|
+
git -C "$d" init -q
|
|
27
|
+
git -C "$d" symbolic-ref HEAD "refs/heads/$br"
|
|
28
|
+
[ "$br" != "main" ] && git -C "$d" config init.defaultBranch "$br"
|
|
29
|
+
git -C "$d" config user.email t@example.com
|
|
30
|
+
git -C "$d" config user.name tester
|
|
31
|
+
: > "$d/seed"
|
|
32
|
+
git -C "$d" add -A && git -C "$d" commit -qm init
|
|
33
|
+
printf '%s\n' "$d"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# run_hook <dir> [env-assignment] — run the pre-commit FROM <dir>, return its exit code.
|
|
37
|
+
run_hook() {
|
|
38
|
+
local dir="$1" envset="${2:-}"
|
|
39
|
+
if [ -n "$envset" ]; then
|
|
40
|
+
( cd "$dir" && env "$envset" "$HOOK" >/dev/null 2>&1 )
|
|
41
|
+
else
|
|
42
|
+
( cd "$dir" && "$HOOK" >/dev/null 2>&1 )
|
|
43
|
+
fi
|
|
44
|
+
return $?
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# assert <desc> <expected-exit> <dir> [env]
|
|
48
|
+
assert() {
|
|
49
|
+
local desc="$1" want="$2" dir="$3" envset="${4:-}"
|
|
50
|
+
local got
|
|
51
|
+
run_hook "$dir" "$envset"; got=$?
|
|
52
|
+
if [ "$got" -eq "$want" ]; then
|
|
53
|
+
PASSED=$((PASSED + 1))
|
|
54
|
+
else
|
|
55
|
+
FAILED=$((FAILED + 1))
|
|
56
|
+
printf ' FAIL: %s (got exit %s, want %s)\n' "$desc" "$got" "$want" >&2
|
|
57
|
+
fi
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ALLOW=0
|
|
61
|
+
BLOCK=1
|
|
62
|
+
|
|
63
|
+
# ── Scenario 1: primary checkout on the default branch → commit allowed ──────────
|
|
64
|
+
P="$(new_repo primary)"
|
|
65
|
+
assert "primary on main: commit allowed" "$ALLOW" "$P"
|
|
66
|
+
|
|
67
|
+
# ── Scenario 2: primary checkout on a feature branch → commit blocked ────────────
|
|
68
|
+
git -C "$P" checkout -q -b feature
|
|
69
|
+
assert "primary on feature branch: commit blocked" "$BLOCK" "$P"
|
|
70
|
+
|
|
71
|
+
# ── Scenario 5 (escape hatch, on the feature branch): allowed ───────────────────
|
|
72
|
+
assert "escape hatch on feature branch: commit allowed" "$ALLOW" "$P" "ALLOW_MAIN_BRANCHING=1"
|
|
73
|
+
git -C "$P" checkout -q main # back home for later worktree add
|
|
74
|
+
|
|
75
|
+
# ── Scenario 3: detached HEAD in the primary checkout → blocked ─────────────────
|
|
76
|
+
D="$(new_repo detached)"
|
|
77
|
+
git -C "$D" checkout -q --detach
|
|
78
|
+
assert "primary detached HEAD: commit blocked" "$BLOCK" "$D"
|
|
79
|
+
|
|
80
|
+
# ── Scenario 4: linked worktree on a feature branch → allowed ────────────────────
|
|
81
|
+
WT="$ROOT/primary-wt"
|
|
82
|
+
git -C "$P" worktree add -q -b wtbranch "$WT" >/dev/null 2>&1
|
|
83
|
+
assert "linked worktree on feature branch: commit allowed" "$ALLOW" "$WT"
|
|
84
|
+
|
|
85
|
+
# ── Bonus: master-default repo on master → allowed; on a branch → blocked ────────
|
|
86
|
+
M="$(new_repo master-primary master)"
|
|
87
|
+
assert "master-default on master: commit allowed" "$ALLOW" "$M"
|
|
88
|
+
git -C "$M" checkout -q -b feat
|
|
89
|
+
assert "master-default on feature branch: commit blocked" "$BLOCK" "$M"
|
|
90
|
+
|
|
91
|
+
# ── Summary ──────────────────────────────────────────────────────────────────────
|
|
92
|
+
printf '\n%d passed, %d failed\n' "$PASSED" "$FAILED"
|
|
93
|
+
if [ "$FAILED" -eq 0 ]; then
|
|
94
|
+
printf 'ALL PASS (incl. escape hatch)\n'
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
exit 1
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Migration Guide: Weld the primary checkout to the default branch (Forge 0.78.0)
|
|
2
|
+
|
|
3
|
+
Applies to projects installed before 0.78.0. This milestone (issue #21, ADR-031) welds the primary checkout to its default branch across ALL tiers: a PreToolUse guard denies branch create/switch there, a tracked git `pre-commit` backstop refuses off-default commits there, and a `new-worktree` helper teaches the right path. The `/upgrading` run that installed this guide already synced the hook bodies (`.claude/hooks/protect-primary-checkout.sh`, `.forge/git-hooks/pre-commit`, `.forge/bin/new-worktree.sh`) and merged the settings.json wiring; the one remaining per-clone step is the `core.hooksPath` wiring, which `/upgrading` applies with a detect+warn+never-clobber policy.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
1. Nothing to hand-edit — the upgrade sync + the `/upgrading` `core.hooksPath` layer ARE the migration.
|
|
8
|
+
2. If you use Husky / lefthook / a pre-commit framework (i.e. `core.hooksPath` is already set), the wiring will NOT overwrite it — you'll get a warning and can chain `.forge/git-hooks/pre-commit` into your existing hooks dir, or accept PreToolUse-only coverage.
|
|
9
|
+
|
|
10
|
+
## Detection
|
|
11
|
+
|
|
12
|
+
Prints `MIGRATE` when the primary-checkout weld is not fully wired: the PreToolUse guard is absent from `settings.json`, OR the tracked pre-commit backstop is missing, OR `core.hooksPath` is not pointing at `.forge/git-hooks`. Silent + exit 0 when fully wired.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
migrate=""
|
|
16
|
+
# (1) PreToolUse guard wired in settings.json?
|
|
17
|
+
if [ -f .claude/settings.json ]; then
|
|
18
|
+
if ! grep -q 'protect-primary-checkout' .claude/settings.json 2>/dev/null; then
|
|
19
|
+
migrate="guard not wired in settings.json"
|
|
20
|
+
fi
|
|
21
|
+
fi
|
|
22
|
+
# (2) tracked pre-commit backstop present?
|
|
23
|
+
if [ ! -f .forge/git-hooks/pre-commit ]; then
|
|
24
|
+
migrate="${migrate:+$migrate; }pre-commit backstop missing (.forge/git-hooks/pre-commit)"
|
|
25
|
+
fi
|
|
26
|
+
# (3) core.hooksPath wired to our dir? (advisory — /upgrading applies this)
|
|
27
|
+
cur=$(git config --get core.hooksPath 2>/dev/null || true)
|
|
28
|
+
if [ "$cur" != ".forge/git-hooks" ]; then
|
|
29
|
+
migrate="${migrate:+$migrate; }core.hooksPath not '.forge/git-hooks' (currently '${cur:-unset}')"
|
|
30
|
+
fi
|
|
31
|
+
if [ -n "$migrate" ]; then
|
|
32
|
+
echo "MIGRATE — primary-checkout weld incomplete: $migrate"
|
|
33
|
+
fi
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Migration steps
|
|
37
|
+
|
|
38
|
+
### 1. Re-run the upgrade / initialize
|
|
39
|
+
|
|
40
|
+
- `/upgrading` on 0.78.0+ framework files syncs the hooks, merges the settings.json guard group, and applies the `core.hooksPath` wiring layer (detect+warn+never-clobber). The run that installed this guide already did the file sync; the `core.hooksPath` layer is what completes the weld.
|
|
41
|
+
- On a brand-new project, `initializing` Finalize wires `core.hooksPath` as part of setup.
|
|
42
|
+
|
|
43
|
+
### 2. If `core.hooksPath` was already set (Husky/lefthook)
|
|
44
|
+
|
|
45
|
+
The wiring will not overwrite it. To keep the pre-commit weld active, chain the Forge hook into your existing hooks directory — e.g. add to your `pre-commit`:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# run the Forge primary-checkout backstop, if present
|
|
49
|
+
[ -x .forge/git-hooks/pre-commit ] && .forge/git-hooks/pre-commit || true
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or accept PreToolUse-only coverage: Claude sessions are still guarded by `protect-primary-checkout.sh`; only the human/CI backstop is inactive.
|
|
53
|
+
|
|
54
|
+
### 3. Verify
|
|
55
|
+
|
|
56
|
+
From the primary checkout on the default branch:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# guard denies branch create (exit 2) — run it directly against a payload:
|
|
60
|
+
printf '{"tool_input":{"command":"git checkout -b probe"},"cwd":"'"$(git rev-parse --show-toplevel)"'"}' \
|
|
61
|
+
| .claude/hooks/protect-primary-checkout.sh; echo "exit=$? (expect 2 in the PRIMARY checkout)"
|
|
62
|
+
# pre-commit allows on the default branch:
|
|
63
|
+
.forge/git-hooks/pre-commit; echo "exit=$? (expect 0 on the default branch)"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
All branch work now goes through `bash .forge/bin/new-worktree.sh <slug>`. Escape hatch for deliberate human branching: `ALLOW_MAIN_BRANCHING=1`.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Migration Guide: context.md sharded into per-milestone files (Forge 0.80.0)
|
|
2
|
+
|
|
3
|
+
Applies to projects upgrading to 0.80.0 or later. `.forge/context.md` — a
|
|
4
|
+
single monolithic, git-tracked file that accumulated one section per
|
|
5
|
+
milestone's locked decisions — is now a **derived, git-ignored index**
|
|
6
|
+
(ADR-033, issue #37), same class as `.forge/state/index.yml` and
|
|
7
|
+
`.forge/streams/active.yml`. The actual decision content lives in
|
|
8
|
+
`.forge/context/{id}.md`, one file per milestone, produced by a new one-time
|
|
9
|
+
tool: `.claude/hooks/forge-context-migrate.sh`.
|
|
10
|
+
|
|
11
|
+
`/upgrading` ships both new pieces — the migration tool
|
|
12
|
+
(`.claude/hooks/forge-context-migrate.sh`) and the extended
|
|
13
|
+
`.claude/hooks/forge-state-rollup.sh` that renders `context.md` as an index —
|
|
14
|
+
like any other framework file. It does **not** run the migration or touch
|
|
15
|
+
`.forge/context.md`, because both are **user-owned content**: `context.md`'s
|
|
16
|
+
existing prose is a specific repo's real decision history, not something an
|
|
17
|
+
upgrade can safely rewrite or delete on its own.
|
|
18
|
+
|
|
19
|
+
**What actually happens if this migration is skipped.** Read
|
|
20
|
+
`forge-state-rollup.sh`'s render logic directly: the third join
|
|
21
|
+
(`render_context_index`) only runs when `.forge/context/` exists as a
|
|
22
|
+
directory (`if [ -d "$root/.forge/context" ]; then render_context_index; fi`).
|
|
23
|
+
On an unmigrated repo — no `.forge/context/` directory yet — that guard is
|
|
24
|
+
false, so the rollup **never touches `.forge/context.md` at all**: it is not
|
|
25
|
+
overwritten, not cleared, not rendered as an empty index. The old monolithic
|
|
26
|
+
file sits there completely untouched, and the boot summary line simply omits
|
|
27
|
+
the `+ context.md (…)` clause it would otherwise show. There is no
|
|
28
|
+
data-loss risk from the render side; the actual gap is that the file quietly
|
|
29
|
+
stops being what the rest of the framework expects it to be once the repo
|
|
30
|
+
does start sharding (skills read/write `.forge/context/{id}.md` post-0.80.0,
|
|
31
|
+
not the monolithic file) — an unmigrated repo's decision history becomes
|
|
32
|
+
stale and invisible to that new convention until this migration runs.
|
|
33
|
+
|
|
34
|
+
Projects that ran `initializing` fresh on 0.80.0+ start with no
|
|
35
|
+
`.forge/context.md` at all (nothing to migrate). Projects that already ran
|
|
36
|
+
this migration (their own dogfood, or a prior partial adoption) have a
|
|
37
|
+
populated `.forge/context/` directory and need nothing further.
|
|
38
|
+
|
|
39
|
+
## Prerequisites
|
|
40
|
+
|
|
41
|
+
1. On the new version's framework files (`npx forge-orkes upgrade`, or the
|
|
42
|
+
`upgrading` skill) — so `.claude/hooks/forge-context-migrate.sh` and the
|
|
43
|
+
extended `.claude/hooks/forge-state-rollup.sh` are present and executable.
|
|
44
|
+
2. Working tree clean or changes committed — this migration creates new
|
|
45
|
+
tracked files (`context/{id}.md`, `context-log.md`), untracks
|
|
46
|
+
`.forge/context.md`, and edits a gitignore file.
|
|
47
|
+
3. Read `.forge/context.md` once before running anything, so you know what
|
|
48
|
+
the pre-migration content actually looks like in this repo (heading
|
|
49
|
+
shapes vary historically — `### m-52 — …`, `### M49 — …`, and undated
|
|
50
|
+
drafting blocks with no id all exist in the wild; the migration tool
|
|
51
|
+
handles all three, but it's worth knowing what to expect in the output).
|
|
52
|
+
|
|
53
|
+
## Detection
|
|
54
|
+
|
|
55
|
+
Keyed on **`.forge/context.md` being git-tracked, non-trivially sized, and
|
|
56
|
+
`.forge/context/` not existing yet** — the pre-migration gap. A byte-count
|
|
57
|
+
floor (500 bytes) guards against false-positiving on a repo that already
|
|
58
|
+
shrank `context.md` down to a near-empty derived index through some other
|
|
59
|
+
path. Silent in a non-Forge / non-git tree, silent on a fresh install (no
|
|
60
|
+
`context.md` at all), silent once `.forge/context/` exists (already
|
|
61
|
+
migrated), silent once `context.md` is untracked (already gitignored, i.e.
|
|
62
|
+
already migrated):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
|
66
|
+
[ -f .forge/context.md ] || exit 0
|
|
67
|
+
[ -d .forge/context ] && exit 0
|
|
68
|
+
|
|
69
|
+
git ls-files --error-unmatch .forge/context.md >/dev/null 2>&1 || exit 0
|
|
70
|
+
|
|
71
|
+
size="$(wc -c < .forge/context.md 2>/dev/null | tr -d ' ')"
|
|
72
|
+
[ -n "$size" ] && [ "$size" -gt 500 ] || exit 0
|
|
73
|
+
|
|
74
|
+
echo "MIGRATE — .forge/context.md is a tracked, ${size}-byte monolithic file with no .forge/context/ directory; run forge-context-migrate.sh to shard it before the derived-index render starts being relied on"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Migration steps
|
|
78
|
+
|
|
79
|
+
### 1. Run the migration tool for real (not dry-run)
|
|
80
|
+
|
|
81
|
+
From the repo root, with no flags — the tool resolves `--source` and `--dest`
|
|
82
|
+
from the current repo via `git rev-parse --show-toplevel` when they're
|
|
83
|
+
omitted, defaulting to `<repo-root>/.forge/context.md` and `<repo-root>/.forge`:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
.claude/hooks/forge-context-migrate.sh
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
This reads `.forge/context.md` and writes `.forge/context/{id}.md` (one file
|
|
90
|
+
per resolvable milestone heading, ids normalized to `m-{N}`),
|
|
91
|
+
`.forge/context-log.md` (Amendment Log, Needs Resolution, and any undated
|
|
92
|
+
drafting block that has no milestone id to shard by), and
|
|
93
|
+
`.forge/context/_shared.md` if any Deferred Ideas / Discretion Areas entry
|
|
94
|
+
has no milestone-id prefix. It does **not** touch or delete the source
|
|
95
|
+
`context.md` — that's step 2, done separately on purpose so the tool itself
|
|
96
|
+
stays a pure read-old/write-new operation.
|
|
97
|
+
|
|
98
|
+
The tool is idempotent per milestone id: if `.forge/context/{id}.md` already
|
|
99
|
+
exists, that id is skipped (never overwritten, never duplicated) and one
|
|
100
|
+
skip line is printed. Re-running this command after the fact is always safe.
|
|
101
|
+
What is **not** repeatable is the untrack step below — do that once.
|
|
102
|
+
|
|
103
|
+
### 2. Untrack context.md and gitignore it
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git rm --cached .forge/context.md
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Add `.forge/context.md` to whichever gitignore file already lists
|
|
110
|
+
`.forge/state/index.yml` and `.forge/streams/active.yml` — grep for
|
|
111
|
+
`index.yml` to find the right file (in this repo it's the root
|
|
112
|
+
`.gitignore`, under the "Render-on-read derived registries" comment block;
|
|
113
|
+
your project's may differ if it customized gitignore layout):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
grep -rn "index.yml" .gitignore .forge/.gitignore 2>/dev/null
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Add the line next to the other two derived-registry entries, matching their
|
|
120
|
+
existing comment block rather than starting a new one.
|
|
121
|
+
|
|
122
|
+
### 3. Commit the migration output
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
git add .forge/context/ .forge/context-log.md .gitignore
|
|
126
|
+
git commit -m "chore(forge): shard context.md into per-milestone files (0.80.0)"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Do **not** `git add .forge/context.md` — it is now untracked and gitignored;
|
|
130
|
+
the working copy on disk will be overwritten by the next boot's render into
|
|
131
|
+
a derived index, which is expected.
|
|
132
|
+
|
|
133
|
+
**This is a one-time, per-repo operation.** The `git rm --cached` + gitignore
|
|
134
|
+
step only needs doing once — running `forge-context-migrate.sh` again later
|
|
135
|
+
(e.g. if a new milestone's context needs re-splitting for some reason) is
|
|
136
|
+
safe and idempotent per the tool's own design, but do not repeat the untrack
|
|
137
|
+
step; `.forge/context.md` is already gitignored after step 2 and a second
|
|
138
|
+
`git rm --cached` on an already-untracked path is a no-op error, not harmful
|
|
139
|
+
but not necessary.
|
|
140
|
+
|
|
141
|
+
## Validation
|
|
142
|
+
|
|
143
|
+
- `.forge/context/` contains one file per milestone heading that existed in
|
|
144
|
+
the old monolithic `context.md` (spot-check the count against what you
|
|
145
|
+
read in Prerequisites step 3).
|
|
146
|
+
- `git status --short .forge/context.md` shows nothing (untracked +
|
|
147
|
+
gitignored paths don't appear in default `git status` output) — confirming
|
|
148
|
+
the untrack + gitignore took effect. `git check-ignore .forge/context.md`
|
|
149
|
+
exits 0.
|
|
150
|
+
- Start a new session, or run `/clear` — the boot summary line now includes
|
|
151
|
+
a `+ context.md (N context rows)` clause, and `.forge/context.md` on disk
|
|
152
|
+
is the rendered index (one row per milestone: id, name, status,
|
|
153
|
+
release_state, path), not the old monolithic prose.
|
|
154
|
+
- Re-running the Detection block above now prints nothing (exits silently)
|
|
155
|
+
— the gap is closed.
|
|
156
|
+
- Spot-check any pre-existing citation into `context.md` (e.g. "LOCKED,
|
|
157
|
+
context.md § M24") still resolves: the rendered index has a row for that
|
|
158
|
+
milestone id, and `.forge/context/{id}.md` contains the cited content.
|