forge-orkes 0.19.0 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/create-forge.js
CHANGED
|
@@ -18,6 +18,29 @@ const FORGE_END = '<!-- forge:end -->';
|
|
|
18
18
|
// Framework-owned: Forge controls these entirely
|
|
19
19
|
const FRAMEWORK_OWNED_DIRS = ['.claude/agents', '.claude/skills'];
|
|
20
20
|
|
|
21
|
+
// Experimental / opt-in skills installed separately (e.g. from experimental/m10).
|
|
22
|
+
// They are NOT in the shipped base template, so the framework-owned auto-clean
|
|
23
|
+
// would otherwise delete them on upgrade. Preserve them instead.
|
|
24
|
+
const EXPERIMENTAL_SKILL_PATHS = ['.claude/skills/orchestrating'];
|
|
25
|
+
|
|
26
|
+
function isExperimentalSkillPath(displayPath) {
|
|
27
|
+
const norm = displayPath.split(path.sep).join('/');
|
|
28
|
+
return EXPERIMENTAL_SKILL_PATHS.some(
|
|
29
|
+
(p) => norm === p || norm.startsWith(p + '/')
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Compare dotted numeric versions (e.g. "0.19.1"). Returns 1 if a>b, -1 if a<b, 0 if equal.
|
|
34
|
+
function compareVersions(a, b) {
|
|
35
|
+
const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
|
|
36
|
+
const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0);
|
|
37
|
+
for (let i = 0; i < 3; i++) {
|
|
38
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
39
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
40
|
+
}
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
// Template-only: reference templates Forge controls
|
|
22
45
|
const TEMPLATE_ONLY_DIRS = ['.forge/templates', '.forge/migrations'];
|
|
23
46
|
|
|
@@ -106,7 +129,7 @@ function upgradeDir(relDir, { autoClean = false } = {}) {
|
|
|
106
129
|
const srcDir = path.join(templateDir, relDir);
|
|
107
130
|
const destDir = path.join(targetDir, relDir);
|
|
108
131
|
|
|
109
|
-
const result = { updated: [], added: [], unchanged: [], removed: [] };
|
|
132
|
+
const result = { updated: [], added: [], unchanged: [], removed: [], preserved: [] };
|
|
110
133
|
|
|
111
134
|
if (!fs.existsSync(srcDir)) return result;
|
|
112
135
|
|
|
@@ -138,6 +161,12 @@ function upgradeDir(relDir, { autoClean = false } = {}) {
|
|
|
138
161
|
for (const rel of destFiles) {
|
|
139
162
|
const srcPath = path.join(srcDir, rel);
|
|
140
163
|
if (!fs.existsSync(srcPath)) {
|
|
164
|
+
const displayPath = path.join(relDir, rel);
|
|
165
|
+
// Never auto-clean opt-in experimental skills — they live outside the base template.
|
|
166
|
+
if (autoClean && isExperimentalSkillPath(displayPath)) {
|
|
167
|
+
result.preserved.push(displayPath);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
141
170
|
const destPath = path.join(destDir, rel);
|
|
142
171
|
if (autoClean) {
|
|
143
172
|
fs.unlinkSync(destPath);
|
|
@@ -381,7 +410,7 @@ function detectLegacyStateIndex() {
|
|
|
381
410
|
}
|
|
382
411
|
|
|
383
412
|
// Legacy markers: shared accumulators or per-milestone narrative drifted in.
|
|
384
|
-
if (/^\s*desire_paths
|
|
413
|
+
if (/^\s*(desire_paths|metrics|current_status):/m.test(content)) return true;
|
|
385
414
|
// A slim registry is small; KBs of index.yml means narrative crept in.
|
|
386
415
|
if (Buffer.byteLength(content, 'utf-8') > 4096) return true;
|
|
387
416
|
|
|
@@ -468,11 +497,28 @@ async function upgrade() {
|
|
|
468
497
|
console.log(` Installed: v${installedVersion}`);
|
|
469
498
|
console.log(` Available: v${pkgVersion}\n`);
|
|
470
499
|
|
|
500
|
+
// Downgrade guard: refuse to roll backward (overwrites newer framework files
|
|
501
|
+
// with older ones and deletes files newer versions added). Almost always a
|
|
502
|
+
// stale npx cache serving an old forge-orkes. Override with --force.
|
|
503
|
+
const force = process.argv.includes('--force');
|
|
504
|
+
if (
|
|
505
|
+
installedVersion !== 'unknown' &&
|
|
506
|
+
compareVersions(pkgVersion, installedVersion) < 0 &&
|
|
507
|
+
!force
|
|
508
|
+
) {
|
|
509
|
+
console.error(` ✖ Refusing to downgrade: installed v${installedVersion} is newer than available v${pkgVersion}.`);
|
|
510
|
+
console.error(` This usually means a stale npx cache served an old forge-orkes.`);
|
|
511
|
+
console.error(` Fix: npx forge-orkes@latest upgrade (or clear the cache: rm -rf ~/.npm/_npx)`);
|
|
512
|
+
console.error(` To downgrade intentionally: forge-orkes upgrade --force\n`);
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
|
|
471
516
|
const results = {
|
|
472
517
|
updated: [],
|
|
473
518
|
added: [],
|
|
474
519
|
unchanged: [],
|
|
475
520
|
removed: [],
|
|
521
|
+
preserved: [],
|
|
476
522
|
};
|
|
477
523
|
|
|
478
524
|
// 1. Process framework-owned directories (auto-clean stale files)
|
|
@@ -482,6 +528,7 @@ async function upgrade() {
|
|
|
482
528
|
results.added.push(...dirResult.added);
|
|
483
529
|
results.unchanged.push(...dirResult.unchanged);
|
|
484
530
|
results.removed.push(...dirResult.removed);
|
|
531
|
+
results.preserved.push(...dirResult.preserved);
|
|
485
532
|
}
|
|
486
533
|
|
|
487
534
|
// 2. Process template-only directories
|
|
@@ -491,6 +538,7 @@ async function upgrade() {
|
|
|
491
538
|
results.added.push(...dirResult.added);
|
|
492
539
|
results.unchanged.push(...dirResult.unchanged);
|
|
493
540
|
results.removed.push(...dirResult.removed);
|
|
541
|
+
results.preserved.push(...dirResult.preserved);
|
|
494
542
|
}
|
|
495
543
|
|
|
496
544
|
// 2b. Sync the forge .gitignore. Shipped as `gitignore` (npm strips dotfiles
|
|
@@ -560,6 +608,14 @@ async function upgrade() {
|
|
|
560
608
|
console.log();
|
|
561
609
|
}
|
|
562
610
|
|
|
611
|
+
if (results.preserved.length > 0) {
|
|
612
|
+
console.log(` Preserved (${results.preserved.length}):`);
|
|
613
|
+
for (const f of results.preserved) {
|
|
614
|
+
console.log(` ${f} (kept — opt-in experimental skill)`);
|
|
615
|
+
}
|
|
616
|
+
console.log();
|
|
617
|
+
}
|
|
618
|
+
|
|
563
619
|
console.log(` Upgraded to v${pkgVersion}\n`);
|
|
564
620
|
|
|
565
621
|
runPostUpgradeMigrationChecks();
|
package/package.json
CHANGED
|
@@ -15,13 +15,13 @@ Entry point. Detect tier, route skills, manage transitions. New projects → ini
|
|
|
15
15
|
|
|
16
16
|
Rollup procedure (deterministic + idempotent):
|
|
17
17
|
1. Glob `.forge/state/milestone-*.yml`.
|
|
18
|
-
2. For each, read `milestone.id`, `milestone.name`, `current.status`, `lifecycle.{deferred_at, resumed_at}`, `current.last_updated
|
|
18
|
+
2. For each, read `milestone.id`, `milestone.name`, `current.status`, `lifecycle.{deferred_at, resumed_at}`, and the last-touched date as `current.last_updated` → else legacy `progress.last_update` → else null. (The fallback keeps pre-0.19.0 milestone files self-sourcing without editing them; active milestones gain `current.last_updated` on their next transition.)
|
|
19
19
|
3. Derive the registry `status`:
|
|
20
20
|
- **deferred** — `lifecycle.deferred_at` set and not superseded by a later `resumed_at`
|
|
21
21
|
- **complete** — `current.status == complete`
|
|
22
22
|
- **not_started** — `current.status == not_started`
|
|
23
23
|
- **active** — otherwise
|
|
24
|
-
4. Rewrite `index.yml` `milestones:` (sorted by id) as `{id, name, status, last_updated
|
|
24
|
+
4. Rewrite `index.yml` `milestones:` (sorted by id) as `{id, name, status, last_updated}` (date from step 2's fallback). No other keys.
|
|
25
25
|
|
|
26
26
|
Output is a pure function of the milestone files, so two sessions regenerating it produce identical bytes — it never needs a hand-merge. **Only the main/orchestrator session runs rollup; worktree agents never write `index.yml`** (they edit only their own `milestone-{id}.yml`).
|
|
27
27
|
|
|
@@ -18,6 +18,10 @@ Check if `.forge/dev-source` exists in the project root.
|
|
|
18
18
|
|
|
19
19
|
Template directory: `{source}/packages/create-forge/template/`.
|
|
20
20
|
|
|
21
|
+
### Downgrade guard
|
|
22
|
+
|
|
23
|
+
Read the source version (`{source}/packages/create-forge/package.json` `version`) and the installed version (`.claude/settings.json` `forge.version`). **If source < installed, STOP** — do not sync. Report: *"Refusing to downgrade: installed v{installed} is newer than source v{source}. Point dev-source at a newer checkout, or confirm an intentional downgrade."* Only proceed on explicit user override. (Rolling backward overwrites newer framework files and deletes files newer versions added — and with `.claude/` often gitignored, it is unrecoverable.)
|
|
24
|
+
|
|
21
25
|
## Step 2: File Classification
|
|
22
26
|
|
|
23
27
|
| Category | Paths | Behavior |
|
|
@@ -28,6 +32,8 @@ Template directory: `{source}/packages/create-forge/template/`.
|
|
|
28
32
|
|
|
29
33
|
**Never touch** user-generated files: `.forge/project.yml`, `.forge/state/`, `.forge/constitution.md`, `.forge/context.md`, `.forge/requirements/`, `.forge/roadmap.yml`, `.forge/design-system.md`, `.forge/refactor-backlog.yml`.
|
|
30
34
|
|
|
35
|
+
**Preserve experimental skills.** Opt-in skills installed separately (e.g. `.claude/skills/orchestrating/` from `experimental/m10/`) are not in the base template. Never flag them as "removed from template" or delete them — leave them untouched.
|
|
36
|
+
|
|
31
37
|
## Step 3: Sync Framework-Owned Files
|
|
32
38
|
|
|
33
39
|
For each framework-owned file in the source template:
|
|
@@ -152,7 +158,7 @@ Add the layers field now? (yes/no/show guide)
|
|
|
152
158
|
Run from project root:
|
|
153
159
|
|
|
154
160
|
```bash
|
|
155
|
-
grep -qE 'desire_paths
|
|
161
|
+
grep -qE '^[[:space:]]*(desire_paths|metrics|current_status):' .forge/state/index.yml && echo "legacy index"
|
|
156
162
|
wc -c .forge/state/index.yml # slim registry is a few hundred bytes; KBs = narrative drifted in
|
|
157
163
|
```
|
|
158
164
|
|
|
@@ -20,9 +20,10 @@ Two problems this release fixes:
|
|
|
20
20
|
## Detection
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
# Bloated/legacy index.yml — large, or still carrying
|
|
23
|
+
# Bloated/legacy index.yml — large, or still carrying the metrics/desire-paths/narrative blocks
|
|
24
24
|
wc -c .forge/state/index.yml # » a few hundred bytes once migrated; KBs means legacy
|
|
25
|
-
|
|
25
|
+
# Anchored so the file's own explanatory comments don't false-positive:
|
|
26
|
+
grep -nE '^[[:space:]]*(desire_paths|metrics|current_status):' .forge/state/index.yml # any hit → migrate
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
The installer also prints a notice after `upgrade` when it detects a legacy `index.yml`.
|
|
@@ -60,7 +61,13 @@ For each entry under the old `desire_paths:` lists, create one file under `.forg
|
|
|
60
61
|
.forge/state/desire-paths/{YYYY-MM-DD}-{type}-{milestone}-{slug}.yml
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
Map the old list name to `type` (`deviation_patterns→deviation_pattern`, `tier_overrides→tier_override`, etc.).
|
|
64
|
+
Map the old list name to `type` (`deviation_patterns→deviation_pattern`, `tier_overrides→tier_override`, etc.). Migrate **one file per entry** — do *not* fabricate one-file-per-occurrence. The old `occurrences: N` is only an aggregate; collapse it to a single file and keep the count in a field (e.g. `detail.historical_occurrences: N`) if you want it. Forward recurrence is counted from *new* files.
|
|
65
|
+
|
|
66
|
+
**Resolved vs open:**
|
|
67
|
+
- **Still-open / could-recur** → file under `.forge/state/desire-paths/` (counts toward the future 3+ detection).
|
|
68
|
+
- **Already resolved** (pattern fixed / framework already evolved) → file under `.forge/state/desire-paths/resolved/`. The active check globs `desire-paths/*.yml` (top level only), so `resolved/` is preserved for the audit trail but excluded from the count — matching how the `forge` skill archives resolved groups.
|
|
69
|
+
|
|
70
|
+
Then delete the `desire_paths:` block from `index.yml`.
|
|
64
71
|
|
|
65
72
|
### 4. Drop `metrics:`
|
|
66
73
|
|
|
@@ -78,14 +85,18 @@ git commit -m "chore(forge): migrate to worktree-safe state (0.19.0)"
|
|
|
78
85
|
## Post-migration verification
|
|
79
86
|
|
|
80
87
|
```bash
|
|
81
|
-
# index.yml is
|
|
82
|
-
|
|
88
|
+
# index.yml is registry-only — parse the YAML and check actual keys (a text grep
|
|
89
|
+
# false-positives on the file's explanatory comments). Uses ruby (no pip yaml needed):
|
|
90
|
+
ruby -ryaml -e 'd=YAML.load_file(".forge/state/index.yml"); bad=d.key?("metrics")||d.key?("desire_paths")||(d["milestones"]||[]).any?{|m| m.is_a?(Hash) && (m.key?("current_status")||m.key?("overall_percent"))}; puts bad ? "STILL LEGACY" : "OK: registry-only"'
|
|
91
|
+
|
|
92
|
+
# (grep alternative — anchored + comment-stripped so it doesn't match the header)
|
|
93
|
+
grep -vE '^[[:space:]]*#' .forge/state/index.yml | grep -qE '^[[:space:]]*(desire_paths|metrics|current_status):' && echo "STILL LEGACY" || echo "OK: registry-only"
|
|
83
94
|
|
|
84
95
|
# rollup is idempotent — running /forge twice produces no diff to index.yml
|
|
85
96
|
git diff --quiet .forge/state/index.yml && echo "OK: stable" || echo "rollup changed index — commit it"
|
|
86
97
|
|
|
87
|
-
# desire-paths now live as files
|
|
88
|
-
ls .forge/state/desire-paths/ 2>/dev/null
|
|
98
|
+
# desire-paths now live as files (active at top level, resolved/ archived separately)
|
|
99
|
+
ls .forge/state/desire-paths/ .forge/state/desire-paths/resolved/ 2>/dev/null
|
|
89
100
|
```
|
|
90
101
|
|
|
91
102
|
## What changes downstream
|
|
@@ -17,8 +17,9 @@ milestones: # Registry rolled up from state/milestone
|
|
|
17
17
|
status: not_started # mirrors current.status: not_started | active | deferred | complete
|
|
18
18
|
last_updated: null # mirrors current.last_updated — used for resume default selection
|
|
19
19
|
|
|
20
|
-
# NOTE — removed from this file by design (M11)
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
20
|
+
# NOTE — removed from this file by design (M11). (Tokens below are spaced to
|
|
21
|
+
# avoid tripping naive legacy-detection greps that scan for "<key>:".)
|
|
22
|
+
# metrics — had zero writers; derive from `git log` if ever needed.
|
|
23
|
+
# desire-paths — now append-only files under state/desire-paths/ (one per
|
|
24
|
+
# observation) so concurrent agents never collide. Occurrence
|
|
25
|
+
# counts are derived by globbing that directory, not mutated here.
|