forge-orkes 0.74.0 → 0.75.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/experimental/m10/source/skills/orchestrating/SKILL.md +31 -2
- package/package.json +1 -1
- package/template/.claude/hooks/forge-reserve.sh +21 -4
- package/template/.claude/hooks/tests/forge-reserve.test.sh +92 -0
- package/template/.claude/skills/executing/SKILL.md +23 -0
- package/template/.forge/FORGE.md +2 -0
- package/template/.forge/templates/project.yml +14 -0
|
@@ -80,16 +80,45 @@ mkdir -p "$wt_root"
|
|
|
80
80
|
git worktree prune
|
|
81
81
|
git worktree add -b forge/${anchor} --lock --reason "forge session m-${milestone_id}" "$wt_root/${anchor}" main
|
|
82
82
|
wt="$wt_root/${anchor}"
|
|
83
|
-
|
|
83
|
+
|
|
84
|
+
# --- Hydrate the worktree ---
|
|
85
|
+
# A git worktree is a clean checkout of *tracked* files only. Everything required but
|
|
86
|
+
# untracked — submodule working trees, gitignored config/secrets — is absent until we
|
|
87
|
+
# put it there. Hydration is that ordered step; submodule init is one instance of it.
|
|
88
|
+
|
|
89
|
+
# (1) Submodules don't auto-populate in a new worktree — init them if present. Reuses the
|
|
84
90
|
# superproject's shared .git/modules object store (no re-clone, just a working-tree
|
|
85
91
|
# checkout); ccache absorbs the subsequent rebuild cost across worktrees.
|
|
86
92
|
if [ -n "$(git -C "$wt" submodule status 2>/dev/null)" ]; then
|
|
87
93
|
( cd "$wt" && git submodule update --init --recursive )
|
|
88
94
|
fi
|
|
95
|
+
|
|
96
|
+
# (2) Required-but-gitignored artifacts (e.g. .mcp.json — MCP config, often secret-bearing;
|
|
97
|
+
# .env.local; experimental hook configs) named in orchestration.worktree_hydrate. A clean
|
|
98
|
+
# checkout lacks them, and the failure is SILENT: MCP servers that never appear, an env-
|
|
99
|
+
# dependent command that misbehaves. Copy each from the superproject, guarded source-exists
|
|
100
|
+
# && dest-absent → never overwrites, and we NEVER `git add` a hydrated path: it stays
|
|
101
|
+
# gitignored/untracked in both trees, so a secret like .mcp.json never enters git history.
|
|
102
|
+
sed -n '/^orchestration:/,/^[^[:space:]#]/p' "$repo_root/.forge/project.yml" 2>/dev/null \
|
|
103
|
+
| sed -n '/^[[:space:]]*worktree_hydrate:/,/^[[:space:]]*[a-z_]*:/p' \
|
|
104
|
+
| sed -n 's/^[[:space:]]*-[[:space:]]*//p' | tr -d "\"'" \
|
|
105
|
+
| while IFS= read -r rel; do # line-based: a path may contain spaces; robust across sh/bash/zsh
|
|
106
|
+
[ -n "$rel" ] || continue
|
|
107
|
+
[ -e "$repo_root/$rel" ] || continue # source-exists guard (no-op when nothing applies)
|
|
108
|
+
[ -e "$wt/$rel" ] && continue # dest-absent guard (idempotent — never clobbers)
|
|
109
|
+
mkdir -p "$(dirname "$wt/$rel")"
|
|
110
|
+
cp -p "$repo_root/$rel" "$wt/$rel"
|
|
111
|
+
done
|
|
112
|
+
|
|
113
|
+
# (3) Dependency install (node_modules etc.) is DEFERRED to the executing/verifying preflight
|
|
114
|
+
# on purpose — Forge core owns no package-manager concept, and the node_modules desire path is
|
|
115
|
+
# already handled there. That is where the false-green `tsc` class (a no-op tsc against a missing
|
|
116
|
+
# node_modules reporting green) is closed — not here.
|
|
117
|
+
|
|
89
118
|
( cd "$wt" && git hook run pre-commit || true )
|
|
90
119
|
```
|
|
91
120
|
|
|
92
|
-
Verify worktree dir exists, branch locked, and (if applicable) submodule dirs are non-empty. On failure → cleanup partial state, refuse mode.
|
|
121
|
+
Verify worktree dir exists, branch locked, and (if applicable) submodule dirs are non-empty and each configured `worktree_hydrate` artifact that exists in the superproject is present in the worktree. On failure → cleanup partial state, refuse mode.
|
|
93
122
|
|
|
94
123
|
`anchor` (= `m-{milestone_id}-{session_id}`) names the worktree dir **and** branch from here on; every reference below that used to be `{session_id}` is now `{anchor}`. `session_id` (the bare uuid) is retained only as the collision key + audit id in state.
|
|
95
124
|
|
package/package.json
CHANGED
|
@@ -249,10 +249,27 @@ compute_id() {
|
|
|
249
249
|
# Hyphen is OPTIONAL in the token ([A-Z]+-?[0-9]+): refactor ids (R100) carry none
|
|
250
250
|
# (ADR-023), so requiring a hyphen would hide them and drop the floor to 0. Split the
|
|
251
251
|
# alpha prefix from the digits in awk so R does not match e.g. FR (anchor ^prefix$).
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
252
|
+
#
|
|
253
|
+
# READ BOTH main AND origin/main (#26): in the local-main-first workflow (ADR-010 —
|
|
254
|
+
# primary checkout parked on main, all work in worktrees reaching main via PRs), local
|
|
255
|
+
# `main` routinely lags origin/main by whatever merged since the last fast-forward, so a
|
|
256
|
+
# local-only floor hands out ids origin/main already claimed — #19's collision class,
|
|
257
|
+
# relocated to the one input that still isn't machine-shared. Best-effort-fetch origin
|
|
258
|
+
# first, then take the max across whichever of {main, origin/main} resolve. Every leg
|
|
259
|
+
# degrades to nothing offline / with no remote (the same posture the rest of the script
|
|
260
|
+
# takes) — a fetch failure or a missing ref is never fatal, it just contributes 0.
|
|
261
|
+
# FORGE_RESERVE_NO_FETCH=1 skips the fetch (fixtures with a local file:// origin want the
|
|
262
|
+
# deterministic ref state they set up, not a network round-trip).
|
|
263
|
+
ref_reservations_max() { # $1 = git ref → max reserved id for $prefix on that ref (0 if absent)
|
|
264
|
+
git show "$1:.forge/reservations.yml" 2>/dev/null \
|
|
265
|
+
| grep -oE '[A-Z]+-?[0-9]+' \
|
|
266
|
+
| awk -v p="$prefix" '{ a=$0; sub(/[-0-9].*$/,"",a); d=$0; gsub(/[^0-9]/,"",d);
|
|
267
|
+
if(a==p){ n=d+0; if(n>m)m=n } } END{print m+0}'
|
|
268
|
+
}
|
|
269
|
+
[ "${FORGE_RESERVE_NO_FETCH:-0}" = 1 ] || git fetch --quiet origin main 2>/dev/null || true
|
|
270
|
+
main_local_max="$(ref_reservations_max main)"; [ -n "$main_local_max" ] || main_local_max=0
|
|
271
|
+
main_origin_max="$(ref_reservations_max origin/main)"; [ -n "$main_origin_max" ] || main_origin_max=0
|
|
272
|
+
main_max="$(awk -v a="$main_local_max" -v b="$main_origin_max" 'BEGIN{print (a>b)?a:b}')"
|
|
256
273
|
[ -n "$main_max" ] || main_max=0
|
|
257
274
|
|
|
258
275
|
next="$(awk -v a="$ledger_max" -v b="$intree_max" -v c="$main_max" \
|
|
@@ -285,6 +285,98 @@ else
|
|
|
285
285
|
fail "case10b --days 1 dropped the kind's max row"
|
|
286
286
|
fi
|
|
287
287
|
|
|
288
|
+
# --- Case 11: origin/main floor beats a lagging local main (#26) -------------
|
|
289
|
+
# The local-main-first collision: origin/main carries a MERGED higher claim
|
|
290
|
+
# (FR-300) that local main has not fast-forwarded to (local main still at FR-200).
|
|
291
|
+
# A local-main-only floor would hand out FR-201 — an id origin/main already used.
|
|
292
|
+
# The fix reads across {main, origin/main}, so the floor is FR-300 → next FR-301.
|
|
293
|
+
origin_repo="$(new_repo case11-origin)"
|
|
294
|
+
( cd "$origin_repo"
|
|
295
|
+
mkdir -p .forge
|
|
296
|
+
cat > .forge/reservations.yml <<'YML'
|
|
297
|
+
reservations:
|
|
298
|
+
- kind: fr
|
|
299
|
+
id: FR-300
|
|
300
|
+
milestone: m-sibling
|
|
301
|
+
reserved_at: "2026-02-01"
|
|
302
|
+
summary: "merged on origin/main, ahead of local main"
|
|
303
|
+
YML
|
|
304
|
+
git add -A && git commit -qm "origin has FR-300" )
|
|
305
|
+
# Clone it (clone's origin → the seed repo), then rewind the clone's LOCAL main to
|
|
306
|
+
# an earlier commit carrying only FR-200 — simulating a primary checkout parked on a
|
|
307
|
+
# stale main while sibling work merged FR-300 to origin.
|
|
308
|
+
clone="$ROOT/repo.case11-clone"
|
|
309
|
+
git clone -q "$origin_repo" "$clone"
|
|
310
|
+
cd "$clone"
|
|
311
|
+
git config user.email t@example.com; git config user.name tester
|
|
312
|
+
# Build a divergent local main: reset to an orphan-free earlier state with FR-200.
|
|
313
|
+
cat > .forge/reservations.yml <<'YML'
|
|
314
|
+
reservations:
|
|
315
|
+
- kind: fr
|
|
316
|
+
id: FR-200
|
|
317
|
+
milestone: m-x
|
|
318
|
+
reserved_at: "2026-01-01"
|
|
319
|
+
summary: "local main lags"
|
|
320
|
+
YML
|
|
321
|
+
git add -A && git commit -qm "local main at FR-200 (behind origin/main FR-300)"
|
|
322
|
+
# origin/main (remote-tracking) still points at FR-300; local main is now at FR-200.
|
|
323
|
+
# Allow the helper to fetch the local file:// origin (deterministic, no network).
|
|
324
|
+
got="$(FORGE_RESERVE_NO_FETCH=1 "$HELPER" fr --milestone m-x)"
|
|
325
|
+
check "case11 origin/main floor (FR-300) beats lagging local main (FR-200) -> FR-301" "$got" "FR-301"
|
|
326
|
+
# Negative control: with the origin arm blind (simulate no remote-tracking ref by
|
|
327
|
+
# reading a repo that has NO origin at all), the floor falls back to local main only.
|
|
328
|
+
d="$(new_repo case11-noorigin)"; cd "$d"
|
|
329
|
+
mkdir -p .forge
|
|
330
|
+
cat > .forge/reservations.yml <<'YML'
|
|
331
|
+
reservations:
|
|
332
|
+
- kind: fr
|
|
333
|
+
id: FR-200
|
|
334
|
+
milestone: m-x
|
|
335
|
+
reserved_at: "2026-01-01"
|
|
336
|
+
summary: "no origin — local main is the only floor"
|
|
337
|
+
YML
|
|
338
|
+
git add -A && git commit -qm init
|
|
339
|
+
rm .forge/reservations.yml
|
|
340
|
+
got="$(FORGE_RESERVE_NO_FETCH=1 "$HELPER" fr --milestone m-x)"
|
|
341
|
+
check "case11 no-origin repo still floors on local main (FR-200) -> FR-201" "$got" "FR-201"
|
|
342
|
+
|
|
343
|
+
# --- Case 12: the fetch refreshes a STALE origin/main tracking ref (#26) ------
|
|
344
|
+
# The end-to-end proof: a sibling merges a higher claim to origin AFTER this clone
|
|
345
|
+
# was made, so the clone's origin/main remote-tracking ref is stale. Without the
|
|
346
|
+
# best-effort fetch the floor would miss it; with the fetch (default, NOT skipped)
|
|
347
|
+
# the helper refreshes origin/main and floors correctly. Uses a local file:// origin
|
|
348
|
+
# so the "fetch" is a deterministic local operation, no network.
|
|
349
|
+
seed="$(new_repo case12-seed)"
|
|
350
|
+
( cd "$seed"
|
|
351
|
+
mkdir -p .forge
|
|
352
|
+
cat > .forge/reservations.yml <<'YML'
|
|
353
|
+
reservations:
|
|
354
|
+
- kind: fr
|
|
355
|
+
id: FR-200
|
|
356
|
+
milestone: m-x
|
|
357
|
+
reserved_at: "2026-01-01"
|
|
358
|
+
summary: "seed FR-200"
|
|
359
|
+
YML
|
|
360
|
+
git add -A && git commit -qm "seed FR-200" )
|
|
361
|
+
clone2="$ROOT/repo.case12-clone"
|
|
362
|
+
git clone -q "$seed" "$clone2"
|
|
363
|
+
# Sibling advances origin to FR-400 AFTER the clone → clone's origin/main is now stale.
|
|
364
|
+
( cd "$seed"
|
|
365
|
+
cat > .forge/reservations.yml <<'YML'
|
|
366
|
+
reservations:
|
|
367
|
+
- kind: fr
|
|
368
|
+
id: FR-400
|
|
369
|
+
milestone: m-sibling
|
|
370
|
+
reserved_at: "2026-03-01"
|
|
371
|
+
summary: "merged to origin after the clone"
|
|
372
|
+
YML
|
|
373
|
+
git add -A && git commit -qm "origin advances to FR-400" )
|
|
374
|
+
cd "$clone2"
|
|
375
|
+
git config user.email t@example.com; git config user.name tester
|
|
376
|
+
# Do NOT set FORGE_RESERVE_NO_FETCH — the fetch is exactly what we are testing.
|
|
377
|
+
got="$("$HELPER" fr --milestone m-x)"
|
|
378
|
+
check "case12 fetch refreshes stale origin/main (FR-400) -> FR-401" "$got" "FR-401"
|
|
379
|
+
|
|
288
380
|
# --- Report -----------------------------------------------------------------
|
|
289
381
|
printf '\n%d passed, %d failed\n' "$PASSED" "$FAILED"
|
|
290
382
|
[ "$FAILED" -eq 0 ]
|
|
@@ -36,6 +36,29 @@ fi
|
|
|
36
36
|
|
|
37
37
|
Surface the init if it ran (*"populated N uninitialised submodule(s) before baseline"*). A `+`/`U` line (divergent/conflicted submodule) is **not** an init case — STOP under Rule 4; it signals real submodule movement. (Desire-path: canvaz m-TIDES01 — empty `external/JUCE` failed the first CMake configure mid-task.)
|
|
38
38
|
|
|
39
|
+
**Gitignored artifacts.** A fresh worktree is a clean checkout of *tracked* files only. Anything gitignored but required — `.mcp.json` (MCP server config, often secret-bearing so it MUST stay gitignored), `.env.local`, experimental hook configs — is simply **absent**, and the failure is silent: MCP servers that never appear, an env-dependent command that misbehaves — no clear "missing config" error. `orchestrating` hydrates these when *it* creates the worktree, but a worktree from any other origin (a manual `git worktree add`, a `chief-of-staff` stream, a fresh clone) does not. So assert it here, regardless of how the workspace was created. Advisory and guarded — a clean no-op when `orchestration.worktree_hydrate` is unset or nothing applies, so existing installs are unaffected:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Copy required-but-gitignored artifacts named in orchestration.worktree_hydrate from the
|
|
43
|
+
# superproject (the MAIN checkout, resolved from the shared git common dir — this assertion
|
|
44
|
+
# runs INSIDE the worktree), guarded source-exists && dest-absent. Never overwrites, never
|
|
45
|
+
# git-adds — the file stays gitignored/untracked in both trees (a secret like .mcp.json never
|
|
46
|
+
# enters git history). No-op when the key is absent or nothing applies.
|
|
47
|
+
repo_root=$(git rev-parse --show-toplevel)
|
|
48
|
+
super=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null | sed 's#/\.git/modules/.*##;s#/\.git/worktrees/.*##;s#/\.git$##')
|
|
49
|
+
sed -n '/^orchestration:/,/^[^[:space:]#]/p' "$repo_root/.forge/project.yml" 2>/dev/null \
|
|
50
|
+
| sed -n '/^[[:space:]]*worktree_hydrate:/,/^[[:space:]]*[a-z_]*:/p' \
|
|
51
|
+
| sed -n 's/^[[:space:]]*-[[:space:]]*//p' | tr -d "\"'" \
|
|
52
|
+
| while IFS= read -r rel; do # line-based: robust across sh/bash/zsh; a path may contain spaces
|
|
53
|
+
[ -n "$rel" ] && [ -n "$super" ] && [ "$super" != "$repo_root" ] || continue # no key / main checkout → no-op
|
|
54
|
+
[ -e "$super/$rel" ] && [ ! -e "$repo_root/$rel" ] || continue # source-exists && dest-absent
|
|
55
|
+
mkdir -p "$(dirname "$repo_root/$rel")"; cp -p "$super/$rel" "$repo_root/$rel"
|
|
56
|
+
echo "hydrated: $rel"
|
|
57
|
+
done
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The loop prints one `hydrated: {path}` line per artifact it copied — surface those before the baseline (*"hydrated N gitignored artifact(s) before baseline"*), mirroring the submodule surface line; no output means nothing needed hydrating. The `$super != $repo_root` guard makes this a natural no-op in a non-worktree (main) checkout — where superproject and checkout are the same tree and there is nothing to hydrate from.
|
|
61
|
+
|
|
39
62
|
## Baseline Snapshot
|
|
40
63
|
|
|
41
64
|
Run **before the first task begins**. Makes failure causality mechanical — no self-assessment.
|
package/template/.forge/FORGE.md
CHANGED
|
@@ -59,6 +59,8 @@ Where worktrees live and what they're named — single source of truth, base con
|
|
|
59
59
|
|
|
60
60
|
**Recorded path is authoritative once set** — `lifecycle.worktree_path` (milestone) / `runtime.worktree` (stream) is read verbatim, never re-derived; changing `worktree_root` affects only new worktrees; move a live worktree only with `git worktree move` + update the record. No recorded path → resolve from the convention. `forge` boot and `chief-of-staff` adoption **validate advisorily**: recorded path outside the resolved root → warn (never block, never move); an explicit `worktree_root` is honored.
|
|
61
61
|
|
|
62
|
+
**`orchestration.worktree_hydrate`** (optional, `project.yml`; forge#18) lists superproject-relative paths to required-but-gitignored artifacts (e.g. `.mcp.json`) a fresh worktree's tracked-only checkout lacks. Copy is guarded `source-exists && dest-absent` — never overwrites, never `git add`s, so the secret stays untracked in both trees. Applied by `orchestrating` at creation and re-asserted idempotently in `executing`'s Workspace Prerequisites, covering worktrees of any origin. Omit ⇒ no propagation. The per-worktree runtime counterpart of the install-time carve-out propagation below (State Ownership § Stable shared).
|
|
63
|
+
|
|
62
64
|
## Workflow Tiers
|
|
63
65
|
|
|
64
66
|
Auto-detects complexity. Override: "Use Quick/Standard/Full tier."
|
|
@@ -99,6 +99,20 @@ verification:
|
|
|
99
99
|
# # only — the private key lives host-side, in the deploy Environment).
|
|
100
100
|
# # Empty ⇒ git config gpg.ssh.allowedSignersFile.
|
|
101
101
|
|
|
102
|
+
# orchestration: # OPTIONAL — worktree behavior (M10 / streams). Absent → defaults.
|
|
103
|
+
# worktree_root: "" # Where Forge worktrees live. Default ../<repo-basename>-worktrees.
|
|
104
|
+
# # Relative → resolves against repo root; absolute / ~ honored verbatim.
|
|
105
|
+
# worktree_hydrate: [] # OPTIONAL list of superproject-relative paths — required-but-gitignored
|
|
106
|
+
# # artifacts copied into a fresh worktree so it can build and reach its
|
|
107
|
+
# # tools (a clean worktree checks out TRACKED files only). Guarded
|
|
108
|
+
# # source-exists && dest-absent: never overwrites, never git-adds, so a
|
|
109
|
+
# # secret-bearing file stays gitignored/untracked in both trees and never
|
|
110
|
+
# # enters git history (.mcp.json the canonical case). Applied by
|
|
111
|
+
# # orchestrating at creation AND asserted in executing's Workspace
|
|
112
|
+
# # Prerequisites → worktrees of ANY origin covered. Omit → no propagation.
|
|
113
|
+
# # e.g. [".mcp.json", ".env.local"]. Dependency install stays with the
|
|
114
|
+
# # executing/verifying preflight, not this key.
|
|
115
|
+
|
|
102
116
|
success_criteria: # How do we know we're done?
|
|
103
117
|
- "" # e.g., "User can create and edit posts"
|
|
104
118
|
- "" # e.g., "All tests pass with >80% coverage"
|