forge-orkes 0.79.0 → 0.80.1
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 +6 -0
- package/package.json +1 -1
- package/template/.claude/hooks/forge-context-migrate.sh +317 -0
- package/template/.claude/hooks/forge-release-fold.sh +35 -2
- package/template/.claude/hooks/forge-size-gate.sh +175 -0
- package/template/.claude/hooks/forge-state-rollup.sh +135 -2
- package/template/.claude/hooks/skill-gate-tiers.txt +6 -0
- package/template/.claude/hooks/tests/forge-context-migrate.test.sh +248 -0
- package/template/.claude/hooks/tests/forge-release-fold.test.sh +53 -2
- package/template/.claude/hooks/tests/forge-size-gate.test.sh +132 -0
- package/template/.claude/settings.json +5 -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/planning/SKILL.md +11 -9
- package/template/.claude/skills/researching/SKILL.md +2 -0
- package/template/.claude/skills/verifying/SKILL.md +2 -2
- package/template/.forge/FORGE.md +79 -91
- 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/migrations/0.80.0-context-md-sharding.md +175 -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
|
@@ -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,175 @@
|
|
|
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
|
+
### 0. Dry-run first and check the processed count
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
.claude/hooks/forge-context-migrate.sh --dry-run
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Confirm the `forge-context-migrate: N milestone id(s) processed` line names a
|
|
86
|
+
plausible number of ids for your `context.md`'s actual Locked Decisions
|
|
87
|
+
headings. The tool exits non-zero and prints an `ERROR` line instead of a
|
|
88
|
+
silent `0 milestone id(s) processed` when it finds Locked Decisions headings
|
|
89
|
+
it can't resolve to any milestone id (issue #42) — but a dry-run check is
|
|
90
|
+
still worth doing before the destructive untrack step below, since a partial
|
|
91
|
+
match (some headings resolve, some don't) still exits 0. Milestone ids must
|
|
92
|
+
be `m-<number>` (`M53`, `m-53`) or a labelled `m-<label>` form (`M-EVT01`,
|
|
93
|
+
`m-SEQPRIM01`); anything else won't shard.
|
|
94
|
+
|
|
95
|
+
### 1. Run the migration tool for real (not dry-run)
|
|
96
|
+
|
|
97
|
+
From the repo root, with no flags — the tool resolves `--source` and `--dest`
|
|
98
|
+
from the current repo via `git rev-parse --show-toplevel` when they're
|
|
99
|
+
omitted, defaulting to `<repo-root>/.forge/context.md` and `<repo-root>/.forge`:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
.claude/hooks/forge-context-migrate.sh
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This reads `.forge/context.md` and writes `.forge/context/{id}.md` (one file
|
|
106
|
+
per resolvable milestone heading, numeric ids normalized to `m-{N}` and
|
|
107
|
+
labelled ids lowercased and preserved, e.g. `m-evt01`),
|
|
108
|
+
`.forge/context-log.md` (Amendment Log, Needs Resolution, and any undated
|
|
109
|
+
drafting block that has no milestone id to shard by), and
|
|
110
|
+
`.forge/context/_shared.md` if any Deferred Ideas / Discretion Areas entry
|
|
111
|
+
has no milestone-id prefix. It does **not** touch or delete the source
|
|
112
|
+
`context.md` — that's step 2, done separately on purpose so the tool itself
|
|
113
|
+
stays a pure read-old/write-new operation.
|
|
114
|
+
|
|
115
|
+
The tool is idempotent per milestone id: if `.forge/context/{id}.md` already
|
|
116
|
+
exists, that id is skipped (never overwritten, never duplicated) and one
|
|
117
|
+
skip line is printed. Re-running this command after the fact is always safe.
|
|
118
|
+
What is **not** repeatable is the untrack step below — do that once.
|
|
119
|
+
|
|
120
|
+
### 2. Untrack context.md and gitignore it
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
git rm --cached .forge/context.md
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Add `.forge/context.md` to whichever gitignore file already lists
|
|
127
|
+
`.forge/state/index.yml` and `.forge/streams/active.yml` — grep for
|
|
128
|
+
`index.yml` to find the right file (in this repo it's the root
|
|
129
|
+
`.gitignore`, under the "Render-on-read derived registries" comment block;
|
|
130
|
+
your project's may differ if it customized gitignore layout):
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
grep -rn "index.yml" .gitignore .forge/.gitignore 2>/dev/null
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Add the line next to the other two derived-registry entries, matching their
|
|
137
|
+
existing comment block rather than starting a new one.
|
|
138
|
+
|
|
139
|
+
### 3. Commit the migration output
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
git add .forge/context/ .forge/context-log.md .gitignore
|
|
143
|
+
git commit -m "chore(forge): shard context.md into per-milestone files (0.80.0)"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Do **not** `git add .forge/context.md` — it is now untracked and gitignored;
|
|
147
|
+
the working copy on disk will be overwritten by the next boot's render into
|
|
148
|
+
a derived index, which is expected.
|
|
149
|
+
|
|
150
|
+
**This is a one-time, per-repo operation.** The `git rm --cached` + gitignore
|
|
151
|
+
step only needs doing once — running `forge-context-migrate.sh` again later
|
|
152
|
+
(e.g. if a new milestone's context needs re-splitting for some reason) is
|
|
153
|
+
safe and idempotent per the tool's own design, but do not repeat the untrack
|
|
154
|
+
step; `.forge/context.md` is already gitignored after step 2 and a second
|
|
155
|
+
`git rm --cached` on an already-untracked path is a no-op error, not harmful
|
|
156
|
+
but not necessary.
|
|
157
|
+
|
|
158
|
+
## Validation
|
|
159
|
+
|
|
160
|
+
- `.forge/context/` contains one file per milestone heading that existed in
|
|
161
|
+
the old monolithic `context.md` (spot-check the count against what you
|
|
162
|
+
read in Prerequisites step 3).
|
|
163
|
+
- `git status --short .forge/context.md` shows nothing (untracked +
|
|
164
|
+
gitignored paths don't appear in default `git status` output) — confirming
|
|
165
|
+
the untrack + gitignore took effect. `git check-ignore .forge/context.md`
|
|
166
|
+
exits 0.
|
|
167
|
+
- Start a new session, or run `/clear` — the boot summary line now includes
|
|
168
|
+
a `+ context.md (N context rows)` clause, and `.forge/context.md` on disk
|
|
169
|
+
is the rendered index (one row per milestone: id, name, status,
|
|
170
|
+
release_state, path), not the old monolithic prose.
|
|
171
|
+
- Re-running the Detection block above now prints nothing (exits silently)
|
|
172
|
+
— the gap is closed.
|
|
173
|
+
- Spot-check any pre-existing citation into `context.md` (e.g. "LOCKED,
|
|
174
|
+
context.md § M24") still resolves: the rendered index has a row for that
|
|
175
|
+
milestone id, and `.forge/context/{id}.md` contains the cited content.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Migration Guide: Size gate wired to boot (Forge 0.80.0)
|
|
2
|
+
|
|
3
|
+
Applies to projects upgrading to 0.80.0 or later. The size-gate check
|
|
4
|
+
(`.claude/hooks/forge-size-gate.sh`, issue #37) — which warns when a
|
|
5
|
+
context-engineering artifact (FORGE.md, context.md, a plan, a skill file, …)
|
|
6
|
+
crosses its declared size gate — is now invoked mechanically by the same
|
|
7
|
+
`SessionStart` hook that runs `forge-state-rollup.sh`. A boot session can no
|
|
8
|
+
longer skip the check and let an artifact silently grow past its gate until
|
|
9
|
+
someone notices by hand.
|
|
10
|
+
|
|
11
|
+
`/upgrading` ships the new hook file (`.claude/hooks/forge-size-gate.sh`) like
|
|
12
|
+
any other framework file. It does **not** wire the `SessionStart` invocation,
|
|
13
|
+
because `.claude/settings.json` is a **user-owned file** — upgrades never
|
|
14
|
+
overwrite it (FORGE.md → State Ownership, "Stable shared" class notes
|
|
15
|
+
settings.json is propagated only via explicit installer steps, never blanket
|
|
16
|
+
overwrite). Without this migration's one manual step, the hook ships but is
|
|
17
|
+
never invoked: the executable exists, nothing mechanically triggers it.
|
|
18
|
+
|
|
19
|
+
Projects that ran `initializing` fresh on 0.80.0+ (or already carry the
|
|
20
|
+
`forge-size-gate` reference in their `SessionStart` block from an earlier
|
|
21
|
+
partial adoption) need nothing further.
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
1. On the new version's framework files (`npx forge-orkes upgrade`, or the
|
|
26
|
+
`upgrading` skill) — so `.claude/hooks/forge-size-gate.sh` is present and
|
|
27
|
+
executable.
|
|
28
|
+
2. Already migrated to 0.77.0's `forge-state-rollup.sh` SessionStart wiring
|
|
29
|
+
(`.forge/migrations/0.77.0-state-rollup-executable.md`) — this guide
|
|
30
|
+
assumes that hook block already exists in `.claude/settings.json` and
|
|
31
|
+
extends it, rather than creating a `SessionStart` block from scratch. If
|
|
32
|
+
that migration hasn't run yet, do it first.
|
|
33
|
+
3. Working tree clean or changes committed — this migration edits
|
|
34
|
+
`.claude/settings.json`, a tracked file.
|
|
35
|
+
|
|
36
|
+
## Detection
|
|
37
|
+
|
|
38
|
+
Keyed on **the executable existing while the SessionStart wiring is
|
|
39
|
+
absent** — the shipped-but-unwired gap. Uses `jq` to inspect
|
|
40
|
+
`.claude/settings.json`'s `hooks.SessionStart` array for an entry referencing
|
|
41
|
+
`forge-size-gate`; degrades to a plain-text grep if `jq` is not installed.
|
|
42
|
+
Silent in a non-Forge / non-git tree, silent once the hook is wired:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
|
46
|
+
[ -x .claude/hooks/forge-size-gate.sh ] || exit 0
|
|
47
|
+
|
|
48
|
+
settings=".claude/settings.json"
|
|
49
|
+
[ -f "$settings" ] || {
|
|
50
|
+
echo "MIGRATE — forge-size-gate.sh is present but $settings does not exist; add it with the SessionStart hook below"
|
|
51
|
+
exit 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if command -v jq >/dev/null 2>&1; then
|
|
55
|
+
wired="$(jq -r '
|
|
56
|
+
(.hooks.SessionStart // []) as $entries
|
|
57
|
+
| [$entries[].hooks[]?.command // "" | select(contains("forge-size-gate"))]
|
|
58
|
+
| length > 0
|
|
59
|
+
' "$settings" 2>/dev/null)"
|
|
60
|
+
if [ "$wired" != "true" ]; then
|
|
61
|
+
echo "MIGRATE — $settings has no SessionStart hook referencing forge-size-gate.sh; the executable shipped but nothing mechanically triggers it"
|
|
62
|
+
fi
|
|
63
|
+
else
|
|
64
|
+
if ! grep -q "forge-size-gate" "$settings" 2>/dev/null; then
|
|
65
|
+
echo "MIGRATE — $settings has no reference to forge-size-gate.sh (jq unavailable, ran plain-text check); add the SessionStart hook below"
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Migration steps
|
|
71
|
+
|
|
72
|
+
### 1. Add the size-gate command to the SessionStart hook block
|
|
73
|
+
|
|
74
|
+
Open `.claude/settings.json` and find the `hooks.SessionStart` array entry
|
|
75
|
+
(added by the 0.77.0 migration). Append a second command object to that
|
|
76
|
+
entry's `hooks` array, alongside the existing `forge-state-rollup.sh` command
|
|
77
|
+
— **not** a second `SessionStart` array entry:
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"hooks": {
|
|
82
|
+
"SessionStart": [
|
|
83
|
+
{
|
|
84
|
+
"matcher": "startup|resume|clear",
|
|
85
|
+
"hooks": [
|
|
86
|
+
{
|
|
87
|
+
"type": "command",
|
|
88
|
+
"command": "[ -x \"$CLAUDE_PROJECT_DIR/.claude/hooks/forge-state-rollup.sh\" ] && \"$CLAUDE_PROJECT_DIR/.claude/hooks/forge-state-rollup.sh\" || true"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"type": "command",
|
|
92
|
+
"command": "[ -x \"$CLAUDE_PROJECT_DIR/.claude/hooks/forge-size-gate.sh\" ] && \"$CLAUDE_PROJECT_DIR/.claude/hooks/forge-size-gate.sh\" || true"
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Both commands run on the same `startup|resume|clear` matcher, in order —
|
|
102
|
+
`forge-state-rollup.sh` first (so caches are fresh), then
|
|
103
|
+
`forge-size-gate.sh` (which only reads files, order doesn't matter
|
|
104
|
+
functionally, but mirroring this repo's own `.claude/settings.json` keeps the
|
|
105
|
+
two guides consistent).
|
|
106
|
+
|
|
107
|
+
If `hooks.SessionStart` already has other entries (e.g. from a project-local
|
|
108
|
+
customization) beyond the one 0.77.0 added, append the `forge-size-gate.sh`
|
|
109
|
+
command object to whichever entry's `hooks` array already contains
|
|
110
|
+
`forge-state-rollup.sh`, rather than creating a new array entry. If no
|
|
111
|
+
`SessionStart` block exists yet at all (0.77.0's migration was skipped), run
|
|
112
|
+
that migration first — this guide only adds a second command, it does not
|
|
113
|
+
construct the block from scratch.
|
|
114
|
+
|
|
115
|
+
The `[ -x ... ] && ... || true` guard means the hook is a no-op — never a
|
|
116
|
+
boot failure — on any checkout where the script is absent or not executable
|
|
117
|
+
(SessionStart hooks are non-blocking; `forge-size-gate.sh` itself also always
|
|
118
|
+
exits 0 — it's an advisory, not a gate that can fail a boot).
|
|
119
|
+
|
|
120
|
+
### 2. Commit the settings change
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
git add .claude/settings.json
|
|
124
|
+
git commit -m "chore(forge): wire forge-size-gate SessionStart hook (0.80.0)"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Validation
|
|
128
|
+
|
|
129
|
+
- Start a new session, or run `/clear` — the boot summary line should include
|
|
130
|
+
a `[forge-size-gate] N artifact(s) over their declared gate: …` line if any
|
|
131
|
+
tracked artifact is currently over gate, or no `[forge-size-gate]` line at
|
|
132
|
+
all if everything is within gate (the hook is silent when there's nothing
|
|
133
|
+
to report — it does not print an "all clear" line). Either outcome means
|
|
134
|
+
the hook ran; the absence of a `[forge-size-gate]` clause is not the same
|
|
135
|
+
as the hook slot producing no output at all (compare to before this
|
|
136
|
+
migration, where the SessionStart summary had no size-gate clause because
|
|
137
|
+
nothing invoked the script).
|
|
138
|
+
- `jq '.hooks.SessionStart' .claude/settings.json` prints the array with both
|
|
139
|
+
the `forge-state-rollup` and `forge-size-gate` commands present in the same
|
|
140
|
+
entry's `hooks` list.
|
|
141
|
+
- Re-running the Detection block above now prints nothing (exits silently) —
|
|
142
|
+
the gap is closed.
|
|
143
|
+
- Manually run `.claude/hooks/forge-size-gate.sh` from the repo root — it
|
|
144
|
+
should produce the same output the boot's SessionStart line showed,
|
|
145
|
+
confirming the wired invocation matches a direct run.
|
|
@@ -130,4 +130,4 @@ Minimize framework token footprint. Compress prose, route models by task complex
|
|
|
130
130
|
|
|
131
131
|
## Amending the Constitution
|
|
132
132
|
|
|
133
|
-
Articles can be amended only by explicit user decision during a discuss phase. Amendments are recorded in `.forge/context.md` with rationale. No agent may unilaterally override a constitutional article.
|
|
133
|
+
Articles can be amended only by explicit user decision during a discuss phase. Amendments are recorded in the active milestone's `.forge/context/{id}.md` with rationale (`.forge/context.md` is a derived index, regenerated every boot — never a write target). No agent may unilaterally override a constitutional article.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Per-Milestone Locked Context — `.forge/context/{id}.md`
|
|
2
|
+
|
|
3
|
+
One file per milestone (`m-{N}.md`), holding exactly that milestone's slice
|
|
4
|
+
of what the old monolithic `.forge/context.md` used to carry inline. Same
|
|
5
|
+
append-only convention as before — strike through instead of rewriting when a
|
|
6
|
+
decision changes; add an Amendment Log entry in `.forge/context-log.md`.
|
|
7
|
+
|
|
8
|
+
The rendered `.forge/context.md` (derived index, `forge-state-rollup.sh`)
|
|
9
|
+
prefers a `<!-- summary: ... -->` marker on the line right after the heading
|
|
10
|
+
over the first bullet, when a hand-written one-liner is worth pinning.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
### m-{N} — {Topic} (locked {date})
|
|
15
|
+
|
|
16
|
+
<!-- summary: one-line hand-written summary for the derived index (optional) -->
|
|
17
|
+
|
|
18
|
+
Context: {why this milestone exists / what triggered it}.
|
|
19
|
+
|
|
20
|
+
- **[Topic]**: [Decision]. Reason: [Why this was chosen].
|
|
21
|
+
|
|
22
|
+
## Deferred
|
|
23
|
+
|
|
24
|
+
<!-- Deferred Ideas entries whose text starts with this milestone's id prefix
|
|
25
|
+
(e.g. "m-{N}: ...") land here during migration or ongoing work. -->
|
|
26
|
+
|
|
27
|
+
## Discretion
|
|
28
|
+
|
|
29
|
+
<!-- Discretion Areas entries whose text starts with this milestone's id
|
|
30
|
+
prefix land here during migration or ongoing work. -->
|
|
@@ -15,7 +15,13 @@ per_slice_budget_usd: 25
|
|
|
15
15
|
|
|
16
16
|
# Max agent turns per headless phase (passed to `claude -p --max-turns`). Bounds a
|
|
17
17
|
# single phase; cross-platform (unlike a `timeout` wall-clock, which macOS lacks).
|
|
18
|
-
|
|
18
|
+
# Raised 60 -> 120 (m-54): three consecutive error_max_turns halts on large
|
|
19
|
+
# content-editing phases (context.md shard/migration, FORGE.md trim), each attempt
|
|
20
|
+
# landing real incremental progress before hitting the wall — the phases themselves
|
|
21
|
+
# weren't stuck, 60 was just too tight for multi-file, iteratively-tested work.
|
|
22
|
+
# Budget (per_slice_budget_usd) already gates cost independently, so this has
|
|
23
|
+
# headroom: each halted attempt spent $2-6 of the $25 slice cap, nowhere near it.
|
|
24
|
+
max_turns_per_phase: 120
|
|
19
25
|
|
|
20
26
|
# RESERVED — not yet enforced. Intended as an optional per-phase wall-clock cap in
|
|
21
27
|
# seconds (0 = off); the runner does not read this key yet (macOS has no stock
|