forge-orkes 0.67.0 → 0.68.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 +49 -27
- package/package.json +1 -1
- package/template/.claude/settings.json +6 -1
- package/template/.claude/skills/upgrading/SKILL.md +1 -1
- package/template/.forge/checks/forge-jarvis-dispatch.sh +16 -8
- package/template/.forge/checks/tests/forge-jarvis-dispatch.test.sh +35 -0
package/bin/create-forge.js
CHANGED
|
@@ -52,31 +52,22 @@ const FORGE_END = '<!-- forge:end -->';
|
|
|
52
52
|
|
|
53
53
|
// --- File classification for upgrades ---
|
|
54
54
|
|
|
55
|
-
// Framework-owned: Forge controls these entirely
|
|
55
|
+
// Framework-owned: Forge controls these entirely. Auto-clean removes files that
|
|
56
|
+
// dropped out of the template — but ownership is decided per top-level UNIT
|
|
57
|
+
// (a skill DIR under .claude/skills/, an agent FILE under .claude/agents/), and
|
|
58
|
+
// only the template defines which units Forge shipped. A unit the base template
|
|
59
|
+
// never shipped — user-authored, vendored-parity, or an opt-in experimental
|
|
60
|
+
// skill installed separately (e.g. m10's orchestrating, the convention packs) —
|
|
61
|
+
// is NOT framework content and is preserved, never swept, even though it lives
|
|
62
|
+
// under a framework-owned dir. Deleting it would destroy work Forge never owned,
|
|
63
|
+
// and .claude/ is frequently gitignored, so the loss is unrecoverable. Files
|
|
64
|
+
// removed from WITHIN a still-shipped unit (e.g. a renamed helper inside a
|
|
65
|
+
// template skill dir) stay cleanable — their unit is still present in the
|
|
66
|
+
// template. See upgradeDir(). Experimental skills are additionally diffed
|
|
67
|
+
// against their shipped sources by detectExperimentalStaleness() (report-only;
|
|
68
|
+
// re-sync consent lives in the /upgrading skill layer).
|
|
56
69
|
const FRAMEWORK_OWNED_DIRS = ['.claude/agents', '.claude/skills'];
|
|
57
70
|
|
|
58
|
-
// Experimental / opt-in skills installed separately (e.g. from experimental/m10).
|
|
59
|
-
// They are NOT in the shipped base template, so the framework-owned auto-clean
|
|
60
|
-
// would otherwise delete them on upgrade. Preserve them instead.
|
|
61
|
-
//
|
|
62
|
-
// The npm tarball ships experimental/ alongside bin/ + template/, so this route
|
|
63
|
-
// CAN diff an installed experimental skill against its same-tarball source:
|
|
64
|
-
// detectExperimentalStaleness() reports unchanged|stale with the differing files.
|
|
65
|
-
// (Blind preservation is what silently froze these pre-0.33.0 — issue #5: a base
|
|
66
|
-
// migration that changed experimental-skill behavior became a no-op.) Detection
|
|
67
|
-
// only — consent for a re-sync lives in the /upgrading skill layer, and
|
|
68
|
-
// load-bearing conventions are still promoted to base (FORGE.md) rather than
|
|
69
|
-
// left to a frozen skill. This preserve-list gates auto-clean only; staleness
|
|
70
|
-
// discovery is by directory scan of the shipped experimental/ tree.
|
|
71
|
-
const EXPERIMENTAL_SKILL_PATHS = ['.claude/skills/orchestrating'];
|
|
72
|
-
|
|
73
|
-
function isExperimentalSkillPath(displayPath) {
|
|
74
|
-
const norm = displayPath.split(path.sep).join('/');
|
|
75
|
-
return EXPERIMENTAL_SKILL_PATHS.some(
|
|
76
|
-
(p) => norm === p || norm.startsWith(p + '/')
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
71
|
// Compare dotted numeric versions (e.g. "0.19.1"). Returns 1 if a>b, -1 if a<b, 0 if equal.
|
|
81
72
|
function compareVersions(a, b) {
|
|
82
73
|
const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
|
|
@@ -259,9 +250,15 @@ function upgradeDir(relDir, { autoClean = false } = {}) {
|
|
|
259
250
|
const srcPath = path.join(srcDir, rel);
|
|
260
251
|
if (!fs.existsSync(srcPath)) {
|
|
261
252
|
const displayPath = path.join(relDir, rel);
|
|
262
|
-
//
|
|
263
|
-
|
|
264
|
-
|
|
253
|
+
// Ownership is per top-level unit: the first path segment of `rel` is the
|
|
254
|
+
// skill dir (or, for flat agents, the file itself). If the template no
|
|
255
|
+
// longer ships that unit at all, the file is user-authored, vendored, or an
|
|
256
|
+
// opt-in experimental skill — not Forge's to delete. Preserve it. Only
|
|
257
|
+
// files whose unit the template STILL ships (a stale file inside a shipped
|
|
258
|
+
// skill dir) remain removal candidates.
|
|
259
|
+
const topUnit = rel.split(path.sep)[0];
|
|
260
|
+
if (autoClean && !fs.existsSync(path.join(srcDir, topUnit))) {
|
|
261
|
+
result.preserved.push(`${displayPath} (kept — not shipped by the base template)`);
|
|
265
262
|
continue;
|
|
266
263
|
}
|
|
267
264
|
const destPath = path.join(destDir, rel);
|
|
@@ -374,6 +371,30 @@ function mergeManagedDeny(destSettings, srcSettings) {
|
|
|
374
371
|
}
|
|
375
372
|
}
|
|
376
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Union the template's managed allow rules into the project's permissions.allow.
|
|
376
|
+
* Additive — a user's own allow/deny entries are untouched (mirrors
|
|
377
|
+
* mergeManagedDeny). Forge automates a worktree-heavy workflow (streams, the
|
|
378
|
+
* slice runner, orchestration/human-verified teardown), but the harness auto-mode
|
|
379
|
+
* classifier soft-denies `git worktree remove` for a worktree the session didn't
|
|
380
|
+
* create in-session, and a Remote-Control / detached session cannot enter
|
|
381
|
+
* bypassPermissions after launch. A NARROW allow rule survives auto mode and runs
|
|
382
|
+
* before the classifier, so the automating session can clean up its own worktrees.
|
|
383
|
+
* These are read/teardown of Forge-managed worktrees only — `remove` is git-guarded
|
|
384
|
+
* (refuses on a dirty/untracked tree without --force) and, in the Forge flow, only
|
|
385
|
+
* ever runs on a merged, human-verified worktree. It never touches commits or
|
|
386
|
+
* branches — worktree removal drops a checkout, not history.
|
|
387
|
+
*/
|
|
388
|
+
function mergeManagedAllow(destSettings, srcSettings) {
|
|
389
|
+
const srcAllow = (srcSettings.permissions && srcSettings.permissions.allow) || [];
|
|
390
|
+
if (!srcAllow.length) return;
|
|
391
|
+
destSettings.permissions = destSettings.permissions || {};
|
|
392
|
+
const destAllow = destSettings.permissions.allow || (destSettings.permissions.allow = []);
|
|
393
|
+
for (const rule of srcAllow) {
|
|
394
|
+
if (!destAllow.includes(rule)) destAllow.push(rule);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
377
398
|
/**
|
|
378
399
|
* Strip the legacy *inline* active-skill guard from PreToolUse (forge#12). It was
|
|
379
400
|
* an inline `if [ ! -f .../.forge/.active-skill ]; then ... exit 2; fi` one-liner;
|
|
@@ -437,6 +458,7 @@ function upgradeSettings() {
|
|
|
437
458
|
// Additively install framework-managed safety guardrails (ADR-017)
|
|
438
459
|
mergeManagedHooks(destSettings, srcSettings);
|
|
439
460
|
mergeManagedDeny(destSettings, srcSettings);
|
|
461
|
+
mergeManagedAllow(destSettings, srcSettings);
|
|
440
462
|
|
|
441
463
|
const after = JSON.stringify(destSettings);
|
|
442
464
|
if (before === after) return 'unchanged';
|
|
@@ -949,7 +971,7 @@ async function upgrade() {
|
|
|
949
971
|
if (results.preserved.length > 0) {
|
|
950
972
|
say(` Preserved (${results.preserved.length}):`);
|
|
951
973
|
for (const f of results.preserved) {
|
|
952
|
-
say(` ${f}
|
|
974
|
+
say(` ${f}`);
|
|
953
975
|
}
|
|
954
976
|
for (const e of experimental) {
|
|
955
977
|
say(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"forge": {
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.68.0",
|
|
4
4
|
"default_tier": "standard",
|
|
5
5
|
"beads_integration": false,
|
|
6
6
|
"context_gates": {
|
|
@@ -48,6 +48,11 @@
|
|
|
48
48
|
"Edit(**/.secrets/**)",
|
|
49
49
|
"Edit(**/*.pem)",
|
|
50
50
|
"Edit(**/*.key)"
|
|
51
|
+
],
|
|
52
|
+
"allow": [
|
|
53
|
+
"Bash(git worktree remove *)",
|
|
54
|
+
"Bash(git worktree prune)",
|
|
55
|
+
"Bash(git worktree prune *)"
|
|
51
56
|
]
|
|
52
57
|
},
|
|
53
58
|
"hooks": {
|
|
@@ -139,7 +139,7 @@ Source version (`{source}/packages/create-forge/package.json`) **<** installed (
|
|
|
139
139
|
|
|
140
140
|
Dev mode syncs from a checkout, so it keeps file-classification semantics — **the npm bin's sync behavior is the spec**; do not re-derive it here. Categories:
|
|
141
141
|
|
|
142
|
-
- **Framework-owned** (`.claude/agents/*.md`, `.claude/skills/*/SKILL.md`, `.forge/FORGE.md`) → overwrite when different
|
|
142
|
+
- **Framework-owned** (`.claude/agents/*.md`, `.claude/skills/*/SKILL.md`, `.forge/FORGE.md`) → overwrite when different. Ownership is per top-level **unit** — a skill DIR under `.claude/skills/`, a flat agent FILE under `.claude/agents/`: a unit the template doesn't ship (user-authored, vendored-parity, or an opt-in experimental skill) is **preserved, never deleted**; only stale files *inside* a still-shipped skill dir are cleaned. Experimental skills additionally get the staleness pass below.
|
|
143
143
|
- **Template-only** (`.forge/templates/**`, `.forge/migrations/**`, `.forge/adapters/**`, `.forge/gitignore` → `.forge/.gitignore`) → overwrite; `.forge/FORGE.md` lands BEFORE the CLAUDE.md pass.
|
|
144
144
|
- **Additive** (`.claude/hooks/**` incl. `tests/`, `.claude/rules/**`, `.forge/checks/**`) → add + overwrite template-shipped files, never delete local extras, `chmod +x` synced `*.sh`.
|
|
145
145
|
- **Import-managed** `CLAUDE.md` → the bin's `ensureClaudeMdImport()` cases verbatim; user content outside a forge section is never modified.
|
|
@@ -91,17 +91,25 @@ materialize_work_order() {
|
|
|
91
91
|
# Prefer the worktree's OWN milestone — .forge/phases/ carries the repo's whole milestone
|
|
92
92
|
# history (32 dirs on the forge repo), so "the only milestone dir" is a fixture-world
|
|
93
93
|
# assumption. The Worktree Convention names the branch forge/{anchor} with anchor =
|
|
94
|
-
# {milestone-id}[-{session}]; derive the id and pick that milestone's dir.
|
|
94
|
+
# {milestone-id}[-{session}]; derive the id and pick that milestone's dir. The id's own
|
|
95
|
+
# m- may be a strippable prefix (m-40 → milestone-40) or PART of a labeled id kept
|
|
96
|
+
# verbatim (m-EVT01 → milestone-m-EVT01) — canvaz first run 2026-07-22: stripping it
|
|
97
|
+
# unconditionally missed milestone-m-EVT01 and fell through to a 38-dir refusal. Try the
|
|
98
|
+
# anchor verbatim before the m--stripped form, then both again minus one trailing
|
|
99
|
+
# -{session} token; first existing dir wins (exact/longer forms first).
|
|
95
100
|
_mdir=""
|
|
96
101
|
_branch="$(git -C "$WORKTREE" branch --show-current 2>/dev/null)"
|
|
97
102
|
case "$_branch" in
|
|
98
|
-
forge
|
|
99
|
-
_anchor="${_branch#forge/
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
forge/*)
|
|
104
|
+
_anchor="${_branch#forge/}" # m-40 | m-40-a1b2c3d4 | m-EVT01-e6063597
|
|
105
|
+
_short="${_anchor%-*}" # minus one trailing -token (session suffix)
|
|
106
|
+
for _cand in "$_anchor" "${_anchor#m-}" "$_short" "${_short#m-}"; do
|
|
107
|
+
[ -n "$_cand" ] || continue
|
|
108
|
+
if [ -d "$WORKTREE/.forge/phases/milestone-$_cand" ]; then
|
|
109
|
+
_mdir="$WORKTREE/.forge/phases/milestone-$_cand"
|
|
110
|
+
break
|
|
111
|
+
fi
|
|
112
|
+
done ;;
|
|
105
113
|
esac
|
|
106
114
|
# Fallback (non-git fixture dirs / unconventional branches): a SINGLE milestone dir is
|
|
107
115
|
# unambiguous; several without a branch-derived pick is a refusal, never a guess.
|
|
@@ -157,6 +157,41 @@ else
|
|
|
157
157
|
bad "dry-run side effects" "dry-run wrote work-order files"
|
|
158
158
|
fi
|
|
159
159
|
|
|
160
|
+
# --- 13-14. BRANCH-DERIVED MILESTONE PICK (canvaz first run 2026-07-22): with several
|
|
161
|
+
# milestone dirs, the forge/{anchor} branch names the one to build. The id's m- may be
|
|
162
|
+
# strippable (m-40 → milestone-40) or part of a labeled id (m-EVT01 → milestone-m-EVT01);
|
|
163
|
+
# unconditional stripping refused a clean labeled-id dispatch. These fixtures are real
|
|
164
|
+
# git repos — cases 9-12 (mktemp dirs) never reach the branch path.
|
|
165
|
+
mkbranch() { git -C "$1" init -q -b "$2" 2>/dev/null || { git -C "$1" init -q && git -C "$1" checkout -q -b "$2"; }; }
|
|
166
|
+
|
|
167
|
+
# 13. labeled id: forge/m-EVT01-<session> resolves milestone-m-EVT01 (m- is part of the id)
|
|
168
|
+
LSLICE="$ROOT/labeled-wt"
|
|
169
|
+
mkdir -p "$LSLICE/.forge/phases/milestone-m-EVT01/1-evt" "$LSLICE/.forge/phases/milestone-5/1-decoy"
|
|
170
|
+
printf 'evt plan\n' > "$LSLICE/.forge/phases/milestone-m-EVT01/1-evt/plan-01.md"
|
|
171
|
+
printf 'decoy\n' > "$LSLICE/.forge/phases/milestone-5/1-decoy/plan-01.md"
|
|
172
|
+
mkbranch "$LSLICE" forge/m-EVT01-e6063597
|
|
173
|
+
err13="$(sh "$DISPATCH" --slice "$LSLICE" --materialize-only 2>&1)"; rc=$?
|
|
174
|
+
if [ "$rc" = 0 ] && grep -q '^ - 1-evt$' "$LSLICE/slice.yml" 2>/dev/null \
|
|
175
|
+
&& grep -q 'evt plan' "$LSLICE/phases/1-evt/plan.md" 2>/dev/null && [ ! -d "$LSLICE/phases/1-decoy" ]; then
|
|
176
|
+
ok "labeled milestone id: branch picks milestone-m-EVT01 among several dirs (m- kept)"
|
|
177
|
+
else
|
|
178
|
+
bad "labeled-id branch pick" "rc=$rc err=$err13"
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# 14. numeric id + session suffix: forge/m-40-<session> still resolves milestone-40 (m- stripped)
|
|
182
|
+
QSLICE="$ROOT/numeric-wt"
|
|
183
|
+
mkdir -p "$QSLICE/.forge/phases/milestone-40/1-num" "$QSLICE/.forge/phases/milestone-39/1-decoy"
|
|
184
|
+
printf 'num plan\n' > "$QSLICE/.forge/phases/milestone-40/1-num/plan-01.md"
|
|
185
|
+
printf 'decoy\n' > "$QSLICE/.forge/phases/milestone-39/1-decoy/plan-01.md"
|
|
186
|
+
mkbranch "$QSLICE" forge/m-40-a1b2c3d4
|
|
187
|
+
err14="$(sh "$DISPATCH" --slice "$QSLICE" --materialize-only 2>&1)"; rc=$?
|
|
188
|
+
if [ "$rc" = 0 ] && grep -q '^ - 1-num$' "$QSLICE/slice.yml" 2>/dev/null \
|
|
189
|
+
&& grep -q 'num plan' "$QSLICE/phases/1-num/plan.md" 2>/dev/null && [ ! -d "$QSLICE/phases/1-decoy" ]; then
|
|
190
|
+
ok "numeric milestone id: branch picks milestone-40 among several dirs (m- stripped)"
|
|
191
|
+
else
|
|
192
|
+
bad "numeric-id branch pick" "rc=$rc err=$err14"
|
|
193
|
+
fi
|
|
194
|
+
|
|
160
195
|
# --- summary -----------------------------------------------------------------
|
|
161
196
|
printf '\n%s: %d passed, %d failed\n' "$(basename "$0")" "$PASSED" "$FAILED"
|
|
162
197
|
[ "$FAILED" = 0 ]
|