forge-orkes 0.19.0 → 0.21.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 +151 -57
- package/package.json +1 -1
- package/template/.claude/agents/executor.md +2 -2
- package/template/.claude/agents/planner.md +1 -1
- package/template/.claude/agents/verifier.md +2 -2
- package/template/.claude/skills/architecting/SKILL.md +3 -3
- package/template/.claude/skills/discussing/SKILL.md +1 -1
- package/template/.claude/skills/executing/SKILL.md +4 -4
- package/template/.claude/skills/forge/SKILL.md +4 -4
- package/template/.claude/skills/planning/SKILL.md +7 -7
- package/template/.claude/skills/reviewing/SKILL.md +1 -1
- package/template/.claude/skills/upgrading/SKILL.md +61 -10
- package/template/.claude/skills/verifying/SKILL.md +1 -1
- package/template/{CLAUDE.md → .forge/FORGE.md} +1 -3
- package/template/.forge/gitignore +1 -1
- package/template/.forge/migrations/0.19.0-worktree-safe-state.md +18 -7
- package/template/.forge/migrations/0.20.0-nested-phase-layout.md +168 -0
- package/template/.forge/templates/contract.md +1 -1
- package/template/.forge/templates/framework-absorption/gsd.md +1 -1
- package/template/.forge/templates/plan.md +2 -2
- package/template/.forge/templates/roadmap.yml +4 -3
- package/template/.forge/templates/state/index.yml +6 -5
package/bin/create-forge.js
CHANGED
|
@@ -8,8 +8,23 @@ const templateDir = path.join(__dirname, '..', 'template');
|
|
|
8
8
|
const targetDir = process.cwd();
|
|
9
9
|
const pkgVersion = require('../package.json').version;
|
|
10
10
|
|
|
11
|
-
// ---
|
|
12
|
-
|
|
11
|
+
// --- CLAUDE.md import management ---
|
|
12
|
+
|
|
13
|
+
// The framework prose lives in .forge/FORGE.md (a framework-owned file). CLAUDE.md
|
|
14
|
+
// itself only carries a single native Claude Code memory import line pointing at it,
|
|
15
|
+
// so CLAUDE.md belongs entirely to the user and upgrades never rewrite their content.
|
|
16
|
+
const FORGE_IMPORT_LINE = '@.forge/FORGE.md';
|
|
17
|
+
// Tolerate trailing whitespace / CRLF on the import line so we never duplicate it.
|
|
18
|
+
const FORGE_IMPORT_RE = /^@\.forge\/FORGE\.md\s*$/m;
|
|
19
|
+
// Stub written when no CLAUDE.md exists yet: one explanatory comment + the import line.
|
|
20
|
+
const CLAUDE_STUB =
|
|
21
|
+
'<!-- Forge loads its workflow context from the import below. ' +
|
|
22
|
+
'The rest of this file is yours — add project instructions freely. -->\n' +
|
|
23
|
+
FORGE_IMPORT_LINE + '\n';
|
|
24
|
+
|
|
25
|
+
// Legacy section markers. These survive SOLELY as migration detection for pre-0.20
|
|
26
|
+
// installs whose CLAUDE.md still embeds the framework prose between these markers.
|
|
27
|
+
// Forge no longer writes markers — new installs get the @import line instead.
|
|
13
28
|
const FORGE_START = '<!-- forge:start -->';
|
|
14
29
|
const FORGE_END = '<!-- forge:end -->';
|
|
15
30
|
|
|
@@ -18,6 +33,29 @@ const FORGE_END = '<!-- forge:end -->';
|
|
|
18
33
|
// Framework-owned: Forge controls these entirely
|
|
19
34
|
const FRAMEWORK_OWNED_DIRS = ['.claude/agents', '.claude/skills'];
|
|
20
35
|
|
|
36
|
+
// Experimental / opt-in skills installed separately (e.g. from experimental/m10).
|
|
37
|
+
// They are NOT in the shipped base template, so the framework-owned auto-clean
|
|
38
|
+
// would otherwise delete them on upgrade. Preserve them instead.
|
|
39
|
+
const EXPERIMENTAL_SKILL_PATHS = ['.claude/skills/orchestrating'];
|
|
40
|
+
|
|
41
|
+
function isExperimentalSkillPath(displayPath) {
|
|
42
|
+
const norm = displayPath.split(path.sep).join('/');
|
|
43
|
+
return EXPERIMENTAL_SKILL_PATHS.some(
|
|
44
|
+
(p) => norm === p || norm.startsWith(p + '/')
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Compare dotted numeric versions (e.g. "0.19.1"). Returns 1 if a>b, -1 if a<b, 0 if equal.
|
|
49
|
+
function compareVersions(a, b) {
|
|
50
|
+
const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
|
|
51
|
+
const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0);
|
|
52
|
+
for (let i = 0; i < 3; i++) {
|
|
53
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
54
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
21
59
|
// Template-only: reference templates Forge controls
|
|
22
60
|
const TEMPLATE_ONLY_DIRS = ['.forge/templates', '.forge/migrations'];
|
|
23
61
|
|
|
@@ -106,7 +144,7 @@ function upgradeDir(relDir, { autoClean = false } = {}) {
|
|
|
106
144
|
const srcDir = path.join(templateDir, relDir);
|
|
107
145
|
const destDir = path.join(targetDir, relDir);
|
|
108
146
|
|
|
109
|
-
const result = { updated: [], added: [], unchanged: [], removed: [] };
|
|
147
|
+
const result = { updated: [], added: [], unchanged: [], removed: [], preserved: [] };
|
|
110
148
|
|
|
111
149
|
if (!fs.existsSync(srcDir)) return result;
|
|
112
150
|
|
|
@@ -138,6 +176,12 @@ function upgradeDir(relDir, { autoClean = false } = {}) {
|
|
|
138
176
|
for (const rel of destFiles) {
|
|
139
177
|
const srcPath = path.join(srcDir, rel);
|
|
140
178
|
if (!fs.existsSync(srcPath)) {
|
|
179
|
+
const displayPath = path.join(relDir, rel);
|
|
180
|
+
// Never auto-clean opt-in experimental skills — they live outside the base template.
|
|
181
|
+
if (autoClean && isExperimentalSkillPath(displayPath)) {
|
|
182
|
+
result.preserved.push(displayPath);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
141
185
|
const destPath = path.join(destDir, rel);
|
|
142
186
|
if (autoClean) {
|
|
143
187
|
fs.unlinkSync(destPath);
|
|
@@ -156,60 +200,61 @@ function upgradeDir(relDir, { autoClean = false } = {}) {
|
|
|
156
200
|
}
|
|
157
201
|
|
|
158
202
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
203
|
+
* Guarantee CLAUDE.md carries the single `@.forge/FORGE.md` import line, migrating
|
|
204
|
+
* any legacy embedded forge section out in one pass. The framework prose itself lives
|
|
205
|
+
* in .forge/FORGE.md (synced separately) — this function never writes prose into
|
|
206
|
+
* CLAUDE.md and never touches user content outside the legacy forge section.
|
|
207
|
+
*
|
|
208
|
+
* no CLAUDE.md → write the stub (comment + import) → 'created'
|
|
209
|
+
* marker section present → replace section in place with the import line → 'migrated'
|
|
210
|
+
* legacy `# Forge` header (no markers) → strip header-to-EOF, append import line → 'migrated'
|
|
211
|
+
* import line already present → 'unchanged'
|
|
212
|
+
* none of the above → append comment + import line at EOF → 'appended'
|
|
213
|
+
*
|
|
214
|
+
* Newline cleanup is confined to the seam of the replaced region only — a global
|
|
215
|
+
* collapse would mutate user content, which upgrades must never do.
|
|
164
216
|
*/
|
|
165
|
-
function
|
|
166
|
-
const srcPath = path.join(templateDir, 'CLAUDE.md');
|
|
217
|
+
function ensureClaudeMdImport() {
|
|
167
218
|
const destPath = path.join(targetDir, 'CLAUDE.md');
|
|
168
219
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const forgeContent = fs.readFileSync(srcPath, 'utf-8');
|
|
172
|
-
|
|
173
|
-
// No existing CLAUDE.md — just copy
|
|
220
|
+
// No existing CLAUDE.md — write the stub.
|
|
174
221
|
if (!fs.existsSync(destPath)) {
|
|
175
|
-
fs.writeFileSync(destPath,
|
|
222
|
+
fs.writeFileSync(destPath, CLAUDE_STUB);
|
|
176
223
|
return 'created';
|
|
177
224
|
}
|
|
178
225
|
|
|
179
226
|
const existing = fs.readFileSync(destPath, 'utf-8');
|
|
180
227
|
|
|
181
|
-
// Check if content is already identical
|
|
182
|
-
if (existing === forgeContent) return 'unchanged';
|
|
183
|
-
|
|
184
228
|
const startIdx = existing.indexOf(FORGE_START);
|
|
185
229
|
const endIdx = existing.indexOf(FORGE_END);
|
|
186
230
|
|
|
187
231
|
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
188
|
-
//
|
|
232
|
+
// Legacy marker section — replace it in place with the import line (position preserved).
|
|
189
233
|
const before = existing.substring(0, startIdx);
|
|
190
234
|
const after = existing.substring(endIdx + FORGE_END.length);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
fs.writeFileSync(destPath,
|
|
196
|
-
return '
|
|
235
|
+
// Collapse blank-line runs only at the two seams we just created, never globally.
|
|
236
|
+
const merged = (before + FORGE_IMPORT_LINE + after)
|
|
237
|
+
.replace(/\n{3,}(@\.forge\/FORGE\.md)/, '\n\n$1')
|
|
238
|
+
.replace(/(@\.forge\/FORGE\.md)\n{3,}/, '$1\n\n');
|
|
239
|
+
fs.writeFileSync(destPath, merged);
|
|
240
|
+
return 'migrated';
|
|
197
241
|
}
|
|
198
242
|
|
|
199
|
-
// No markers
|
|
243
|
+
// No markers, but a legacy `# Forge` header — old installs appended prose to EOF.
|
|
200
244
|
const forgeHeaderIdx = existing.indexOf('# Forge\n');
|
|
201
245
|
if (forgeHeaderIdx !== -1) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const cleaned = merged.replace(/\n{3,}/g, '\n\n');
|
|
207
|
-
fs.writeFileSync(destPath, cleaned);
|
|
208
|
-
return 'replaced';
|
|
246
|
+
const before = existing.substring(0, forgeHeaderIdx).trimEnd();
|
|
247
|
+
const merged = (before ? before + '\n\n' : '') + FORGE_IMPORT_LINE + '\n';
|
|
248
|
+
fs.writeFileSync(destPath, merged);
|
|
249
|
+
return 'migrated';
|
|
209
250
|
}
|
|
210
251
|
|
|
211
|
-
//
|
|
212
|
-
|
|
252
|
+
// Import line already present (tolerating trailing whitespace/CRLF) — nothing to do.
|
|
253
|
+
if (FORGE_IMPORT_RE.test(existing)) return 'unchanged';
|
|
254
|
+
|
|
255
|
+
// User-authored CLAUDE.md with no forge content and no import line — append it.
|
|
256
|
+
const merged =
|
|
257
|
+
existing.trimEnd() + '\n\n' + CLAUDE_STUB;
|
|
213
258
|
fs.writeFileSync(destPath, merged);
|
|
214
259
|
return 'appended';
|
|
215
260
|
}
|
|
@@ -261,23 +306,6 @@ function isForgeInstalled() {
|
|
|
261
306
|
async function install() {
|
|
262
307
|
console.log('\n Forge - Meta-prompting framework for Claude Code\n');
|
|
263
308
|
|
|
264
|
-
// Handle CLAUDE.md — use smart merge
|
|
265
|
-
const claudeStatus = mergeClaudeMd();
|
|
266
|
-
switch (claudeStatus) {
|
|
267
|
-
case 'created':
|
|
268
|
-
console.log(' Created CLAUDE.md');
|
|
269
|
-
break;
|
|
270
|
-
case 'replaced':
|
|
271
|
-
console.log(' Updated Forge section in CLAUDE.md (user content preserved)');
|
|
272
|
-
break;
|
|
273
|
-
case 'appended':
|
|
274
|
-
console.log(' Appended Forge config to existing CLAUDE.md');
|
|
275
|
-
break;
|
|
276
|
-
case 'unchanged':
|
|
277
|
-
console.log(' CLAUDE.md already up to date');
|
|
278
|
-
break;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
309
|
// Copy .claude/ directory
|
|
282
310
|
const srcClaude = path.join(templateDir, '.claude');
|
|
283
311
|
const destClaude = path.join(targetDir, '.claude');
|
|
@@ -294,6 +322,23 @@ async function install() {
|
|
|
294
322
|
// the template ships it as `gitignore`. Materialize the real dotfile here.
|
|
295
323
|
materializeForgeGitignore(destForge);
|
|
296
324
|
|
|
325
|
+
// Guarantee CLAUDE.md imports the framework prose (now on disk at .forge/FORGE.md).
|
|
326
|
+
const claudeStatus = ensureClaudeMdImport();
|
|
327
|
+
switch (claudeStatus) {
|
|
328
|
+
case 'created':
|
|
329
|
+
console.log(' Created CLAUDE.md (@.forge/FORGE.md import stub)');
|
|
330
|
+
break;
|
|
331
|
+
case 'migrated':
|
|
332
|
+
console.log(' Migrated CLAUDE.md: forge section extracted to .forge/FORGE.md (@import)');
|
|
333
|
+
break;
|
|
334
|
+
case 'appended':
|
|
335
|
+
console.log(' Added @.forge/FORGE.md import line to existing CLAUDE.md');
|
|
336
|
+
break;
|
|
337
|
+
case 'unchanged':
|
|
338
|
+
console.log(' CLAUDE.md import line already present');
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
|
|
297
342
|
// Stamp version from package.json into settings.json
|
|
298
343
|
const settingsPath = path.join(targetDir, SETTINGS_FILE);
|
|
299
344
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -381,7 +426,7 @@ function detectLegacyStateIndex() {
|
|
|
381
426
|
}
|
|
382
427
|
|
|
383
428
|
// Legacy markers: shared accumulators or per-milestone narrative drifted in.
|
|
384
|
-
if (/^\s*desire_paths
|
|
429
|
+
if (/^\s*(desire_paths|metrics|current_status):/m.test(content)) return true;
|
|
385
430
|
// A slim registry is small; KBs of index.yml means narrative crept in.
|
|
386
431
|
if (Buffer.byteLength(content, 'utf-8') > 4096) return true;
|
|
387
432
|
|
|
@@ -468,11 +513,28 @@ async function upgrade() {
|
|
|
468
513
|
console.log(` Installed: v${installedVersion}`);
|
|
469
514
|
console.log(` Available: v${pkgVersion}\n`);
|
|
470
515
|
|
|
516
|
+
// Downgrade guard: refuse to roll backward (overwrites newer framework files
|
|
517
|
+
// with older ones and deletes files newer versions added). Almost always a
|
|
518
|
+
// stale npx cache serving an old forge-orkes. Override with --force.
|
|
519
|
+
const force = process.argv.includes('--force');
|
|
520
|
+
if (
|
|
521
|
+
installedVersion !== 'unknown' &&
|
|
522
|
+
compareVersions(pkgVersion, installedVersion) < 0 &&
|
|
523
|
+
!force
|
|
524
|
+
) {
|
|
525
|
+
console.error(` ✖ Refusing to downgrade: installed v${installedVersion} is newer than available v${pkgVersion}.`);
|
|
526
|
+
console.error(` This usually means a stale npx cache served an old forge-orkes.`);
|
|
527
|
+
console.error(` Fix: npx forge-orkes@latest upgrade (or clear the cache: rm -rf ~/.npm/_npx)`);
|
|
528
|
+
console.error(` To downgrade intentionally: forge-orkes upgrade --force\n`);
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
|
|
471
532
|
const results = {
|
|
472
533
|
updated: [],
|
|
473
534
|
added: [],
|
|
474
535
|
unchanged: [],
|
|
475
536
|
removed: [],
|
|
537
|
+
preserved: [],
|
|
476
538
|
};
|
|
477
539
|
|
|
478
540
|
// 1. Process framework-owned directories (auto-clean stale files)
|
|
@@ -482,6 +544,7 @@ async function upgrade() {
|
|
|
482
544
|
results.added.push(...dirResult.added);
|
|
483
545
|
results.unchanged.push(...dirResult.unchanged);
|
|
484
546
|
results.removed.push(...dirResult.removed);
|
|
547
|
+
results.preserved.push(...dirResult.preserved);
|
|
485
548
|
}
|
|
486
549
|
|
|
487
550
|
// 2. Process template-only directories
|
|
@@ -491,6 +554,7 @@ async function upgrade() {
|
|
|
491
554
|
results.added.push(...dirResult.added);
|
|
492
555
|
results.unchanged.push(...dirResult.unchanged);
|
|
493
556
|
results.removed.push(...dirResult.removed);
|
|
557
|
+
results.preserved.push(...dirResult.preserved);
|
|
494
558
|
}
|
|
495
559
|
|
|
496
560
|
// 2b. Sync the forge .gitignore. Shipped as `gitignore` (npm strips dotfiles
|
|
@@ -510,10 +574,32 @@ async function upgrade() {
|
|
|
510
574
|
}
|
|
511
575
|
}
|
|
512
576
|
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
577
|
+
// 2c. Sync the framework prose file .forge/FORGE.md (framework-owned single file,
|
|
578
|
+
// same overwrite-when-different pattern as 2b). MUST run before the CLAUDE.md pass
|
|
579
|
+
// so a migrated project's import line never points at a missing file.
|
|
580
|
+
const fmSrc = path.join(templateDir, '.forge', 'FORGE.md');
|
|
581
|
+
const fmDest = path.join(targetDir, '.forge', 'FORGE.md');
|
|
582
|
+
if (fs.existsSync(fmSrc)) {
|
|
583
|
+
const newContent = fs.readFileSync(fmSrc, 'utf-8');
|
|
584
|
+
const existed = fs.existsSync(fmDest);
|
|
585
|
+
const oldContent = existed ? fs.readFileSync(fmDest, 'utf-8') : null;
|
|
586
|
+
if (oldContent !== newContent) {
|
|
587
|
+
fs.mkdirSync(path.dirname(fmDest), { recursive: true });
|
|
588
|
+
fs.writeFileSync(fmDest, newContent);
|
|
589
|
+
results[existed ? 'updated' : 'added'].push('.forge/FORGE.md');
|
|
590
|
+
} else {
|
|
591
|
+
results.unchanged.push('.forge/FORGE.md');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// 3. Guarantee the CLAUDE.md import line; auto-migrate any legacy embedded section.
|
|
596
|
+
const claudeStatus = ensureClaudeMdImport();
|
|
597
|
+
if (claudeStatus === 'migrated') {
|
|
516
598
|
results.updated.push('CLAUDE.md');
|
|
599
|
+
console.log(' CLAUDE.md migrated: forge section extracted to .forge/FORGE.md (@import)');
|
|
600
|
+
} else if (claudeStatus === 'appended') {
|
|
601
|
+
results.updated.push('CLAUDE.md');
|
|
602
|
+
console.log(' CLAUDE.md: @.forge/FORGE.md import line restored');
|
|
517
603
|
} else if (claudeStatus === 'unchanged') {
|
|
518
604
|
results.unchanged.push('CLAUDE.md');
|
|
519
605
|
} else if (claudeStatus === 'created') {
|
|
@@ -560,6 +646,14 @@ async function upgrade() {
|
|
|
560
646
|
console.log();
|
|
561
647
|
}
|
|
562
648
|
|
|
649
|
+
if (results.preserved.length > 0) {
|
|
650
|
+
console.log(` Preserved (${results.preserved.length}):`);
|
|
651
|
+
for (const f of results.preserved) {
|
|
652
|
+
console.log(` ${f} (kept — opt-in experimental skill)`);
|
|
653
|
+
}
|
|
654
|
+
console.log();
|
|
655
|
+
}
|
|
656
|
+
|
|
563
657
|
console.log(` Upgraded to v${pkgVersion}\n`);
|
|
564
658
|
|
|
565
659
|
runPostUpgradeMigrationChecks();
|
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@ Execute plan tasks. Full dev tools, strict deviation rules. Plan says X, build X
|
|
|
15
15
|
| Task (sub-executors) | `git add .` or `git add -A` |
|
|
16
16
|
|
|
17
17
|
## Input
|
|
18
|
-
Plan: `.forge/phases/
|
|
18
|
+
Plan: `.forge/phases/milestone-{id}/{phase}-{name}/plan.md`, context: `.forge/context.md`, state: `.forge/state/milestone-{id}.yml`.
|
|
19
19
|
|
|
20
20
|
## Output
|
|
21
21
|
Committed code, updated `milestone-{id}.yml` (progress/deviations), execution summary. **Never write `index.yml`** — it is a derived registry; `milestone-{id}.yml` is the single source of truth, regenerated into the registry by the `forge` rollup. The phase-boundary state-sync commit (State Commit Protocol, CLAUDE.md) is run by the `executing` skill at handoff — you do per-task code commits and update the milestone file.
|
|
@@ -41,7 +41,7 @@ Need to deviate from plan?
|
|
|
41
41
|
|
|
42
42
|
### 1. Read Plan
|
|
43
43
|
```
|
|
44
|
-
Read: .forge/phases/
|
|
44
|
+
Read: .forge/phases/milestone-{id}/{phase}-{name}/plan.md
|
|
45
45
|
Read: .forge/context.md
|
|
46
46
|
Read: .forge/state/milestone-{id}.yml
|
|
47
47
|
```
|
|
@@ -18,7 +18,7 @@ Research -> actionable plans. Every plan passes constitutional gates with verifi
|
|
|
18
18
|
Research findings, `.forge/templates/project.yml`, `constitution.md`, `context.md`, `state/milestone-{id}.yml` (if resuming).
|
|
19
19
|
|
|
20
20
|
## Output
|
|
21
|
-
`.forge/` files: `phases/
|
|
21
|
+
`.forge/` files: `phases/milestone-{id}/{phase}-{name}/plan.md` (XML tasks), `specs/`, `requirements/m{N}.yml`, `context.md`, `state/milestone-{id}.yml`. **Never write `index.yml`** — it is a derived registry; `milestone-{id}.yml` is the single source of truth. The `planning` skill runs the state-sync commit at handoff (State Commit Protocol, CLAUDE.md).
|
|
22
22
|
|
|
23
23
|
## Process
|
|
24
24
|
|
|
@@ -14,7 +14,7 @@ Goal-backward: start from must_haves, confirm existence, substance, wiring. Repo
|
|
|
14
14
|
| Task (parallel sub-verifiers) | Fixing code (report — Executor fixes) |
|
|
15
15
|
|
|
16
16
|
## Input
|
|
17
|
-
Plan+must_haves (`.forge/phases/
|
|
17
|
+
Plan+must_haves (`.forge/phases/milestone-{id}/{phase}-{name}/plan.md`), execution summary, source (read-only), milestone state.
|
|
18
18
|
|
|
19
19
|
## Output
|
|
20
20
|
|
|
@@ -57,7 +57,7 @@ Run: {n} | Passed: {n} | Failed: {n} | Coverage: {if available}
|
|
|
57
57
|
|
|
58
58
|
### 1. Load Criteria
|
|
59
59
|
```
|
|
60
|
-
Read: .forge/phases/
|
|
60
|
+
Read: .forge/phases/milestone-{id}/{phase}-{name}/plan.md → extract must_haves
|
|
61
61
|
Read: .forge/state/milestone-{id}.yml → reported progress (the source of truth; index.yml is a derived registry — read the milestone file, not index)
|
|
62
62
|
Read: .forge/context.md → locked decisions
|
|
63
63
|
Read: .forge/deferred-issues.md → known pre-existing failures (if exists; treat as advisory)
|
|
@@ -101,7 +101,7 @@ Common conflicts:
|
|
|
101
101
|
4. Consider indexing for frequent queries
|
|
102
102
|
5. Plan for evolution — prefer nullable fields over rigid schemas
|
|
103
103
|
|
|
104
|
-
Document in `.forge/phases/
|
|
104
|
+
Document in `.forge/phases/milestone-{id}/{phase}-{name}/data-model.md`.
|
|
105
105
|
|
|
106
106
|
## API Contract Design
|
|
107
107
|
|
|
@@ -111,7 +111,7 @@ Document in `.forge/phases/m{M}-{N}-{name}/data-model.md`.
|
|
|
111
111
|
4. Add pagination for list endpoints
|
|
112
112
|
5. Plan per-endpoint auth requirements
|
|
113
113
|
|
|
114
|
-
Document in `.forge/phases/
|
|
114
|
+
Document in `.forge/phases/milestone-{id}/{phase}-{name}/contracts/`.
|
|
115
115
|
|
|
116
116
|
## Output
|
|
117
117
|
|
|
@@ -123,7 +123,7 @@ Document in `.forge/phases/m{M}-{N}-{name}/contracts/`.
|
|
|
123
123
|
|
|
124
124
|
## Phase Handoff
|
|
125
125
|
|
|
126
|
-
1. **Persist** — Confirm ADRs in `.forge/decisions/`, models and contracts in `.forge/phases/
|
|
126
|
+
1. **Persist** — Confirm ADRs in `.forge/decisions/`, models and contracts in `.forge/phases/milestone-{id}/{phase}-{name}/`
|
|
127
127
|
2. **Update state** — Set `current.status` to `planning` in `.forge/state/milestone-{id}.yml`
|
|
128
128
|
3. **State-sync commit** (State Commit Protocol): `git add .forge/` then `git commit -m "chore(forge): sync state after architecting — m{N} {phase-name}"` (scoped; never `git add .`).
|
|
129
129
|
4. **Recommend context clear:**
|
|
@@ -198,7 +198,7 @@ options:
|
|
|
198
198
|
### Step 1: Load Context
|
|
199
199
|
|
|
200
200
|
```
|
|
201
|
-
Read: .forge/phases/
|
|
201
|
+
Read: .forge/phases/milestone-{id}/{phase}-{name}/plan-{NN}.md → the plan
|
|
202
202
|
Read: .forge/requirements/m{N}.yml → source requirements (current milestone)
|
|
203
203
|
Read: .forge/context.md → locked decisions
|
|
204
204
|
Read: .forge/constitution.md → active gates
|
|
@@ -6,7 +6,7 @@ description: "Build to plan with atomic commits, deviation rules, and context en
|
|
|
6
6
|
# Executing
|
|
7
7
|
|
|
8
8
|
## Pre-Execution Checklist
|
|
9
|
-
- [ ] Plan exists (`.forge/phases/
|
|
9
|
+
- [ ] Plan exists (`.forge/phases/milestone-{id}/{phase}-{name}/plan-{NN}.md`)
|
|
10
10
|
- [ ] Context.md locked decisions noted
|
|
11
11
|
- [ ] Constitution.md gates satisfied
|
|
12
12
|
- [ ] Milestone state updated to `status: executing`
|
|
@@ -78,7 +78,7 @@ For each task in the plan:
|
|
|
78
78
|
## TDD Flow (task type="tdd")
|
|
79
79
|
|
|
80
80
|
**With Test Spec:**
|
|
81
|
-
1. Copy spec from `.forge/phases/
|
|
81
|
+
1. Copy spec from `.forge/phases/milestone-{id}/{phase}-{name}/specs/` to test directory
|
|
82
82
|
2. **RED:** Remove `skip` markers. Confirm failure. Commit: `test({scope}): activate spec tests for {feature}`
|
|
83
83
|
3. **GREEN:** Minimal code to pass. Repeat per test.
|
|
84
84
|
4. **REFACTOR:** Clean up. Commit: `feat({scope}): implement {feature}`
|
|
@@ -116,7 +116,7 @@ forge_claim_files {
|
|
|
116
116
|
session_id: lifecycle.session_id,
|
|
117
117
|
files: [<absolute paths from task <files> manifest>],
|
|
118
118
|
ttl_seconds: 1800,
|
|
119
|
-
reason: "executing m{
|
|
119
|
+
reason: "executing m{id} phase {phase} task <name>"
|
|
120
120
|
}
|
|
121
121
|
```
|
|
122
122
|
|
|
@@ -221,7 +221,7 @@ Run all non-advisory verification commands once after commit. 1 retry max.
|
|
|
221
221
|
After completing all tasks in a plan:
|
|
222
222
|
|
|
223
223
|
```markdown
|
|
224
|
-
# Execution Summary:
|
|
224
|
+
# Execution Summary: milestone-{id}/{phase}-{name}, Plan {NN}
|
|
225
225
|
|
|
226
226
|
## Completed Tasks
|
|
227
227
|
1. [Task name] — [one-line result]
|
|
@@ -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
|
|
|
@@ -144,7 +144,7 @@ No match → fall through to Step 2B (tier detection).
|
|
|
144
144
|
1. Validate: milestone in `index.yml` (any status — active, deferred, complete, not_started)
|
|
145
145
|
2. Build manifest — scan for associated files:
|
|
146
146
|
- `.forge/state/milestone-{id}.yml`
|
|
147
|
-
- `.forge/phases/
|
|
147
|
+
- `.forge/phases/milestone-{id}/` (the milestone's phase dir — holds all `{phase}-{name}/` subdirs)
|
|
148
148
|
- `.forge/research/milestone-{id}.md` (if exists)
|
|
149
149
|
- `.forge/audits/milestone-{id}-*` (if exists)
|
|
150
150
|
- Note: `.forge/context.md` is shared — do NOT archive or modify. M{id} decisions stay as history.
|
|
@@ -306,7 +306,7 @@ Phase transitions = clear boundaries. **Recommend `/clear`** after writing state
|
|
|
306
306
|
| researching | `.forge/research/milestone-{id}.md` | discussing |
|
|
307
307
|
| discussing | `.forge/context.md` (locked decisions, deferrals, discretion) | planning |
|
|
308
308
|
| architecting | ADRs, data models, API contracts | planning |
|
|
309
|
-
| planning | `.forge/phases/
|
|
309
|
+
| planning | `.forge/phases/milestone-{id}/{phase}-{name}/`, `.forge/requirements/m{N}.yml`, roadmap.yml | executing |
|
|
310
310
|
| executing | Committed code, execution summary, state | verifying |
|
|
311
311
|
| verifying | Verification report, desire-path files (`state/desire-paths/`) | reviewing |
|
|
312
312
|
|
|
@@ -141,7 +141,7 @@ Before decomposing, classify whether this phase crosses a layer boundary with a
|
|
|
141
141
|
|
|
142
142
|
**Two contract tiers:**
|
|
143
143
|
- **Durable** = the standing layer API. Lives in ADRs (`.forge/decisions/`) + constitution, indexed in `.forge/contracts/index.yml` (integration-point -> governing ADR). Stable + unchanged -> agents read the ADR; no per-phase artifact.
|
|
144
|
-
- **Per-phase delta** = the specific new/changed shape THIS phase introduces. Pinned in `.forge/phases/
|
|
144
|
+
- **Per-phase delta** = the specific new/changed shape THIS phase introduces. Pinned in `.forge/phases/milestone-{id}/{phase}-{name}/contract.md` (from `.forge/templates/contract.md`); references its governing ADR; folded back into that ADR on landing.
|
|
145
145
|
|
|
146
146
|
**Classify:**
|
|
147
147
|
|
|
@@ -176,9 +176,9 @@ Plan naming reflects the slice: `plan-01-user-signs-up.md`, not `plan-01-models.
|
|
|
176
176
|
|
|
177
177
|
#### File Layout
|
|
178
178
|
|
|
179
|
-
1. `.forge/templates/plan.md` -> `.forge/phases/
|
|
180
|
-
- `{
|
|
181
|
-
- Ex: `.forge/phases/
|
|
179
|
+
1. `.forge/templates/plan.md` -> `.forge/phases/milestone-{id}/{phase}-{name}/plan-{NN}.md`
|
|
180
|
+
- `{id}`=milestone, `{phase}`=phase# (preserved verbatim, NOT renumbered), `{name}`=kebab, `{NN}`=seq
|
|
181
|
+
- Ex: `.forge/phases/milestone-3/2-providers/plan-01.md`
|
|
182
182
|
2. Frontmatter: phase, plan#, wave, deps, `slice_exception:` (optional, see Core Principle)
|
|
183
183
|
3. must_haves:
|
|
184
184
|
- **Truths:** User-observable outcomes (3-7). MUST be phrased as something the user can see, click, or receive -- not "model X exists" or "table Y created".
|
|
@@ -270,7 +270,7 @@ None -> skip to Step 8.
|
|
|
270
270
|
|
|
271
271
|
### Generate Specs
|
|
272
272
|
|
|
273
|
-
**Location:** `.forge/phases/
|
|
273
|
+
**Location:** `.forge/phases/milestone-{id}/{phase}-{name}/specs/`
|
|
274
274
|
Test files (`.test.ts`, `_test.go`, `test_*.py`):
|
|
275
275
|
1. Import from `<files>` path
|
|
276
276
|
2. Behavior from requirements (happy, edge, error)
|
|
@@ -278,7 +278,7 @@ Test files (`.test.ts`, `_test.go`, `test_*.py`):
|
|
|
278
278
|
4. Mark pending (`it.skip()`, `@pytest.mark.skip`, `t.Skip()`)
|
|
279
279
|
|
|
280
280
|
```typescript
|
|
281
|
-
// Example: .forge/phases/
|
|
281
|
+
// Example: .forge/phases/milestone-1/1-auth/specs/login-endpoint.test.ts
|
|
282
282
|
describe('POST /api/auth/login', () => {
|
|
283
283
|
it.skip('returns 200 with valid token for correct credentials', () => {
|
|
284
284
|
// From FR-001: "User can log in with email and password"
|
|
@@ -306,7 +306,7 @@ Specs -> type becomes `tdd`:
|
|
|
306
306
|
<task type="tdd">
|
|
307
307
|
<name>Implement login endpoint</name>
|
|
308
308
|
<files>src/api/auth/login.ts</files>
|
|
309
|
-
<spec>.forge/phases/
|
|
309
|
+
<spec>.forge/phases/milestone-1/1-auth/specs/login-endpoint.test.ts</spec>
|
|
310
310
|
<action>Make all tests in the spec file pass. Remove skip markers as you implement.</action>
|
|
311
311
|
<verify>All spec tests pass: npm test -- login-endpoint</verify>
|
|
312
312
|
<done>All skip markers removed, all tests green</done>
|
|
@@ -375,7 +375,7 @@ If the milestone being completed has `milestone.origin: {R-id}` set (promoted fr
|
|
|
375
375
|
|
|
376
376
|
If the milestone's phases produced `contract.md` files (planning Step 6.1 Tier 1/2), close their lifecycle before completing the milestone. The durable contract is the ADR; the per-phase `contract.md` is a working delta that must be folded back in.
|
|
377
377
|
|
|
378
|
-
1. Glob `.forge/phases/
|
|
378
|
+
1. Glob `.forge/phases/milestone-{id}/*/contract.md`.
|
|
379
379
|
2. For each contract not yet `absorbed` (Tier-2 lands at `ratified` after the executing seam check; Tier-1 lands at `proposed` — both fold the same way now that the phase is verified):
|
|
380
380
|
- Fold `delta` into its `governing_adr` — amend the ADR in `.forge/decisions/`, or supersede it (`Status: Superseded by ADR-{NNN}`) if the shape changed materially.
|
|
381
381
|
- If a **new** integration point was introduced, add it to `.forge/contracts/index.yml` `integration_points:` (id, produces, consumes, governing_adr, summary).
|
|
@@ -18,16 +18,23 @@ 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 |
|
|
24
28
|
|----------|-------|----------|
|
|
25
|
-
| **Framework-owned** | `.claude/agents/*.md`, `.claude/skills/*/SKILL.md` | Overwrite |
|
|
26
|
-
| **
|
|
29
|
+
| **Framework-owned** | `.claude/agents/*.md`, `.claude/skills/*/SKILL.md`, `.forge/FORGE.md` | Overwrite |
|
|
30
|
+
| **Import-managed** | `CLAUDE.md` | Never overwrite content; migrate legacy forge sections + guarantee the `@.forge/FORGE.md` import line (Step 5) |
|
|
31
|
+
| **Merge-owned** | `.claude/settings.json` | Smart-merge `forge.*` keys only (Step 5) |
|
|
27
32
|
| **Template-only** | `.forge/templates/**`, `.forge/migrations/**`, `.forge/gitignore` → `.forge/.gitignore` | Overwrite |
|
|
28
33
|
|
|
29
34
|
**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
35
|
|
|
36
|
+
**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.
|
|
37
|
+
|
|
31
38
|
## Step 3: Sync Framework-Owned Files
|
|
32
39
|
|
|
33
40
|
For each framework-owned file in the source template:
|
|
@@ -44,11 +51,19 @@ Same process as Step 3 for `.forge/templates/**` and `.forge/migrations/**`. (Ma
|
|
|
44
51
|
|
|
45
52
|
**Also sync the forge gitignore:** the source ships it as `.forge/gitignore` (npm strips files literally named `.gitignore` from a published tarball). Copy/refresh the source's `.forge/gitignore` into the project as `.forge/.gitignore` (overwrite — it is framework-owned). Report added/updated/unchanged like any template-only file.
|
|
46
53
|
|
|
47
|
-
|
|
54
|
+
**Sync the framework prose file `.forge/FORGE.md`** (framework-owned single file — it holds the `# Forge` prose, formerly embedded in CLAUDE.md). Copy/refresh `{source}/packages/create-forge/template/.forge/FORGE.md` → `.forge/FORGE.md` (overwrite when different; report added/updated/unchanged). **Run this BEFORE the Step 5 CLAUDE.md pass** so a migrated import line never points at a missing file.
|
|
48
55
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
## Step 5: Handle Import-Managed + Merge-Owned Files
|
|
57
|
+
|
|
58
|
+
**`CLAUDE.md` (import-managed — one-pass auto-migration, no prompt):** mirror the npm bin's `ensureClaudeMdImport()` case-for-case. The framework prose lives in `.forge/FORGE.md` (synced in Step 4) — never write prose into CLAUDE.md, and never modify user content outside a legacy forge section.
|
|
59
|
+
|
|
60
|
+
1. **No `CLAUDE.md`** → write the stub: one explanatory comment + the `@.forge/FORGE.md` import line. Report **created**.
|
|
61
|
+
2. **Legacy `<!-- forge:start -->` / `<!-- forge:end -->` markers present** → replace the marker section *in place* with the `@.forge/FORGE.md` import line (position preserved), collapsing blank-line runs only at the two seams — never globally. Report **migrated**.
|
|
62
|
+
3. **No markers, but a legacy `# Forge` header** (old installs appended prose to EOF) → strip from the header to end-of-file, append the import line. Report **migrated**.
|
|
63
|
+
4. **Import line already present** (tolerating trailing whitespace / CRLF) → leave untouched. Report **unchanged**.
|
|
64
|
+
5. **User-authored CLAUDE.md, no forge content, no import line** → append the comment + import line at EOF. Report **restored** (the import-line guarantee — Forge always loads its context).
|
|
65
|
+
|
|
66
|
+
No manual-merge prompt for forge sections; migration is silent except the report line. User content outside the forge section is never modified, and the import line is never duplicated.
|
|
52
67
|
|
|
53
68
|
**`.claude/settings.json`:**
|
|
54
69
|
1. Read source and local versions
|
|
@@ -76,13 +91,17 @@ Removed from template: {N} files
|
|
|
76
91
|
- .claude/agents/old-agent.md (still in your project)
|
|
77
92
|
- ...
|
|
78
93
|
|
|
79
|
-
|
|
80
|
-
- CLAUDE.md (
|
|
81
|
-
|
|
94
|
+
Migrated: {N} files
|
|
95
|
+
- CLAUDE.md (forge section extracted to .forge/FORGE.md — @import)
|
|
96
|
+
|
|
97
|
+
Restored: {N} files
|
|
98
|
+
- CLAUDE.md (@.forge/FORGE.md import line re-appended)
|
|
82
99
|
|
|
83
100
|
Unchanged: {N} files
|
|
84
101
|
```
|
|
85
102
|
|
|
103
|
+
The `Migrated:` line appears only when a legacy marker/`# Forge` section was extracted this run; `Restored:` only when a missing import line was re-appended. A steady-state upgrade lists `CLAUDE.md` under Unchanged.
|
|
104
|
+
|
|
86
105
|
## Step 7: Post-Upgrade Migration Checks
|
|
87
106
|
|
|
88
107
|
After sync completes, detect legacy file layouts that the new framework version no longer writes but may still read in compatibility mode. For each match, surface a migration prompt — do not auto-migrate.
|
|
@@ -152,7 +171,7 @@ Add the layers field now? (yes/no/show guide)
|
|
|
152
171
|
Run from project root:
|
|
153
172
|
|
|
154
173
|
```bash
|
|
155
|
-
grep -qE 'desire_paths
|
|
174
|
+
grep -qE '^[[:space:]]*(desire_paths|metrics|current_status):' .forge/state/index.yml && echo "legacy index"
|
|
156
175
|
wc -c .forge/state/index.yml # slim registry is a few hundred bytes; KBs = narrative drifted in
|
|
157
176
|
```
|
|
158
177
|
|
|
@@ -176,6 +195,38 @@ Migrate now? (yes/no/show guide)
|
|
|
176
195
|
|
|
177
196
|
`upgrading` never edits `.forge/state/` directly — migration runs via `quick-tasking`.
|
|
178
197
|
|
|
198
|
+
### Pre-0.20.0 flat phase dirs
|
|
199
|
+
|
|
200
|
+
Run from project root:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
ls .forge/phases/ | grep -E '^m[0-9]+-[0-9]+-' && echo "flat layout"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
If any dir matching `m{id}-{phase}-{name}` sits directly under `.forge/phases/`, surface:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
Nested phase layout: phase dirs are flat under .forge/phases/ (Forge 0.20.0)
|
|
210
|
+
──────────────────────────────────────────────────────────────────────────
|
|
211
|
+
Forge 0.20.0 nests phase dirs one level per milestone:
|
|
212
|
+
m{id}-{phase}-{name}/ → milestone-{id}/{phase}-{name}/
|
|
213
|
+
This keeps `ls .forge/phases/` scoped per-milestone instead of dumping every
|
|
214
|
+
phase across all milestones into context. Phase numbers are preserved verbatim
|
|
215
|
+
(nothing is renumbered); the state cursor stays valid.
|
|
216
|
+
|
|
217
|
+
{N} flat phase dir(s) detected.
|
|
218
|
+
A migration guide is available at: .forge/migrations/0.20.0-nested-phase-layout.md
|
|
219
|
+
(canonical: https://github.com/Attuned-Media/forge/blob/main/docs/migrations/0.20.0-nested-phase-layout.md)
|
|
220
|
+
|
|
221
|
+
Migrate now? (yes/no/show guide)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
- **yes** → invoke `quick-tasking` skill, hand it the migration guide as the task definition (`git mv` each flat dir to `milestone-{id}/{phase}-{name}/`, preserving phase numbers; rewrite any `milestone_dir:` in roadmap.yml; verify plan-file count unchanged).
|
|
225
|
+
- **show guide** → read and display the file, then re-ask
|
|
226
|
+
- **no** → note in upgrade report. Skills compose the new nested path on write while flat dirs still exist on disk, so new and old plans split across two layouts (split-brain) until migrated. Recommend running migration before the next planning cycle.
|
|
227
|
+
|
|
228
|
+
`upgrading` never moves phase dirs directly — migration runs via `quick-tasking`.
|
|
229
|
+
|
|
179
230
|
### Future migrations
|
|
180
231
|
|
|
181
232
|
Add new detection blocks here for each Forge version that changes file layout. Pattern:
|
|
@@ -18,7 +18,7 @@ After `/clear`, load with fresh eyes — don't carry the executor's assumptions:
|
|
|
18
18
|
```
|
|
19
19
|
Read: .forge/state/milestone-{id}.yml → current phase, plans completed
|
|
20
20
|
Read: .forge/project.yml → tech stack (for running tests)
|
|
21
|
-
Read: .forge/phases/
|
|
21
|
+
Read: .forge/phases/milestone-{id}/{phase}-{name}/plan-{NN}.md → must_haves (truths, artifacts, key_links)
|
|
22
22
|
Read: .forge/context.md → locked decisions
|
|
23
23
|
Read: .forge/requirements/m{N}.yml → requirement IDs for coverage check (current milestone, resolved from state)
|
|
24
24
|
Read: .forge/deferred-issues.md → known pre-existing failures (if exists; treat as advisory)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
<!-- forge:start -->
|
|
2
1
|
# Forge
|
|
3
2
|
|
|
4
3
|
Lean meta-prompting framework for Claude Code. Context engineering + constitutional governance on native primitives.
|
|
@@ -135,7 +134,7 @@ State lives in `.forge/`:
|
|
|
135
134
|
- `state/desire-paths/` — Append-only framework-usage observations, one file per observation. Occurrence counts derived by globbing (no mutable counter).
|
|
136
135
|
- `context.md` — Locked decisions + deferred ideas (discuss phase)
|
|
137
136
|
- `research/milestone-{id}.md` — Research findings snapshot (dated, immutable)
|
|
138
|
-
- `phases/
|
|
137
|
+
- `phases/milestone-{id}/{phase}-{name}/plan-{NN}.md` — Task plans with must_haves frontmatter (`{id}`=milestone, `{phase}`=phase# preserved verbatim, `{name}`=kebab, `{NN}`=seq)
|
|
139
138
|
- `refactor-backlog.yml` — Refactoring catalog, worked via quick-tasking
|
|
140
139
|
- `archive/milestone-{id}/` — Archived milestone artifacts (archive-delete)
|
|
141
140
|
|
|
@@ -207,4 +206,3 @@ One commit per task. Format: `{type}({scope}): {description}`
|
|
|
207
206
|
Types: `feat`, `fix`, `test`, `refactor`, `chore`, `docs`
|
|
208
207
|
Never `git add .` or `git add -A` — stage individually.
|
|
209
208
|
Phase handoffs additionally emit one scoped `chore(forge): sync state …` commit (see State Commit Protocol) so `.forge/` state never drifts out of git.
|
|
210
|
-
<!-- forge:end -->
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# Everything under .forge/ IS durable and committed to version control, so
|
|
4
4
|
# project state survives machine loss and can be pulled from any clone:
|
|
5
|
-
# project.yml constitution.md design-system.md context.md roadmap.yml
|
|
5
|
+
# FORGE.md project.yml constitution.md design-system.md context.md roadmap.yml
|
|
6
6
|
# requirements/ phases/ research/ decisions/ contracts/ audits/
|
|
7
7
|
# refactor-backlog.yml state/ templates/ migrations/
|
|
8
8
|
#
|
|
@@ -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
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Migration Guide: Nested Phase Layout (Forge 0.20.0)
|
|
2
|
+
|
|
3
|
+
Applies to projects initialized before 0.20.0 that have one or more flat phase
|
|
4
|
+
directories under `.forge/phases/`.
|
|
5
|
+
|
|
6
|
+
## Why
|
|
7
|
+
|
|
8
|
+
Phase directories used to live **flat** under `.forge/phases/`, one dir per
|
|
9
|
+
phase, named `m{milestone}-{phase}-{name}/`:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
.forge/phases/
|
|
13
|
+
m1-1-schema-and-templates/
|
|
14
|
+
m1-2-skill-and-agent-updates/
|
|
15
|
+
m9-13-schema-and-templates/
|
|
16
|
+
m10-01-orchestration/
|
|
17
|
+
...
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
At a handful of milestones this is fine. At ~100 milestones the flat directory
|
|
21
|
+
becomes a context tax: any `ls .forge/phases/` or glob over it dumps hundreds of
|
|
22
|
+
sibling entries into an agent's window, most of them irrelevant to the milestone
|
|
23
|
+
being worked. Per Article XI (context is sacred), listings should scope to the
|
|
24
|
+
milestone in hand.
|
|
25
|
+
|
|
26
|
+
0.20.0 **nests** phase dirs one level under a per-milestone parent:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
.forge/phases/
|
|
30
|
+
milestone-1/
|
|
31
|
+
1-schema-and-templates/
|
|
32
|
+
2-skill-and-agent-updates/
|
|
33
|
+
milestone-9/
|
|
34
|
+
13-schema-and-templates/
|
|
35
|
+
milestone-10/
|
|
36
|
+
01-orchestration/
|
|
37
|
+
...
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Now `ls .forge/phases/milestone-9/` shows only milestone 9's phases.
|
|
41
|
+
|
|
42
|
+
## What does NOT change
|
|
43
|
+
|
|
44
|
+
- **Phase numbers are preserved verbatim — nothing is renumbered.**
|
|
45
|
+
`m9-13-schema-and-templates` → `milestone-9/13-schema-and-templates`. The
|
|
46
|
+
phase number `13` stays `13`. (Phase numbers in this framework are globally
|
|
47
|
+
sequential, not per-milestone — that is intentional and untouched.)
|
|
48
|
+
- **State cursor and roadmap need no edits to phase numbers.** The cursor stores
|
|
49
|
+
`current.phase` (int) + `current.phase_name` (string), never a path. Skills
|
|
50
|
+
compose the path at runtime. Because the phase number and name are unchanged,
|
|
51
|
+
every existing cursor stays valid — only the folder territory moves.
|
|
52
|
+
- **Plan file contents are untouched.** Each `plan-{NN}.md` is byte-identical
|
|
53
|
+
after migration; only its parent path changes.
|
|
54
|
+
|
|
55
|
+
## Detection
|
|
56
|
+
|
|
57
|
+
A flat (un-migrated) layout has directories matching `m{id}-{phase}-{name}`
|
|
58
|
+
**directly** under `.forge/phases/`:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
ls .forge/phases/ | grep -E '^m[0-9]+-[0-9]+-' && echo "FLAT — migrate" || echo "nested — no-op"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Already-nested projects have only `milestone-{id}/` dirs at that level and the
|
|
65
|
+
check prints `nested — no-op`.
|
|
66
|
+
|
|
67
|
+
## Defensive parse
|
|
68
|
+
|
|
69
|
+
The migration must tolerate **variable-width** milestone IDs **and**
|
|
70
|
+
**variable-width** phase numbers. Real-world dirs include single-digit
|
|
71
|
+
(`m1-1-x`), zero-padded (`m10-01-x`), and double-digit phase numbers
|
|
72
|
+
(`m7-11-x`, `m9-13-x`). Use this regex, anchored to the dir basename:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
^m([0-9]+)-([0-9]+)-(.+)$
|
|
76
|
+
│ │ └─ capture 3: kebab-case phase name
|
|
77
|
+
│ └─ capture 2: phase number (preserve verbatim, including any zero-pad)
|
|
78
|
+
└─ capture 1: milestone id
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Capture group 2 is copied through **as written** — do not strip a leading zero,
|
|
82
|
+
do not renumber.
|
|
83
|
+
|
|
84
|
+
## Mapping
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
m{ID}-{PHASE}-{name}/ → milestone-{ID}/{PHASE}-{name}/
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
|
|
92
|
+
| Flat | Nested |
|
|
93
|
+
|------|--------|
|
|
94
|
+
| `m1-1-schema-and-templates/` | `milestone-1/1-schema-and-templates/` |
|
|
95
|
+
| `m9-13-schema-and-templates/` | `milestone-9/13-schema-and-templates/` |
|
|
96
|
+
| `m10-01-orchestration/` | `milestone-10/01-orchestration/` |
|
|
97
|
+
| `m7-11-refactor-promotion-and-audit/` | `milestone-7/11-refactor-promotion-and-audit/` |
|
|
98
|
+
|
|
99
|
+
One milestone parent may receive multiple phase subdirs (e.g. `milestone-1/`
|
|
100
|
+
gets `1-…`, `2-…`, `3-…`).
|
|
101
|
+
|
|
102
|
+
## Migration steps
|
|
103
|
+
|
|
104
|
+
### 1. Secure current state first
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
git add .forge/
|
|
108
|
+
git commit -m "chore(forge): secure .forge state before 0.20.0 migration"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 2. Move each flat dir to its nested home
|
|
112
|
+
|
|
113
|
+
Use `git mv` so history follows the files and `git status` records a rename
|
|
114
|
+
(not delete+add):
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
cd .forge/phases
|
|
118
|
+
for d in m[0-9]*-[0-9]*-*/; do
|
|
119
|
+
d=${d%/}
|
|
120
|
+
[[ $d =~ ^m([0-9]+)-([0-9]+)-(.+)$ ]] || continue
|
|
121
|
+
id=${BASH_REMATCH[1]}; phase=${BASH_REMATCH[2]}; name=${BASH_REMATCH[3]}
|
|
122
|
+
mkdir -p "milestone-$id"
|
|
123
|
+
git mv "$d" "milestone-$id/$phase-$name"
|
|
124
|
+
done
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
(Run from the repo so `git mv` resolves; adjust the `cd` to your `.forge/phases`
|
|
128
|
+
path. The loop preserves `phase` verbatim — no renumbering.)
|
|
129
|
+
|
|
130
|
+
### 3. Update roadmap `milestone_dir` references (if present)
|
|
131
|
+
|
|
132
|
+
Some roadmaps pin an explicit `milestone_dir:` per phase. Rewrite any flat value
|
|
133
|
+
to the nested form, e.g.:
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
milestone_dir: "m10-01-orchestration" # before
|
|
137
|
+
milestone_dir: "milestone-10/01-orchestration" # after
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Phase numbers in the roadmap are **not** edited — only the path string.
|
|
141
|
+
|
|
142
|
+
### 4. Verify
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# No flat dirs remain directly under phases/:
|
|
146
|
+
ls .forge/phases/ | grep -E '^m[0-9]+-[0-9]+-' || echo CLEAN
|
|
147
|
+
|
|
148
|
+
# Plan-file count unchanged (compare to your pre-migration count):
|
|
149
|
+
find .forge/phases -name 'plan*.md' | wc -l
|
|
150
|
+
|
|
151
|
+
# git tracks the moves as renames, not delete+add:
|
|
152
|
+
git status --short # expect R lines, no D + ?? pairs
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 5. Commit
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
git add .forge/
|
|
159
|
+
git commit -m "chore(forge): migrate phase dirs to nested layout (0.20.0)"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Notes
|
|
163
|
+
|
|
164
|
+
- Idempotent: re-running detection on a nested project is a no-op (`CLEAN`).
|
|
165
|
+
- No data loss: this is a pure rename. If `find … plan*.md | wc -l` differs
|
|
166
|
+
before vs. after, **stop and investigate** — a dir failed to parse.
|
|
167
|
+
- The `upgrading` skill (Step 7) detects the flat layout after an upgrade and
|
|
168
|
+
offers to run this migration via `quick-tasking`. It never auto-migrates.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Phase Contract: {integration point}
|
|
2
2
|
|
|
3
|
-
Copy to `.forge/phases/
|
|
3
|
+
Copy to `.forge/phases/milestone-{id}/{phase}-{name}/contract.md` when planning **Step 6.1** detects a cross-layer delta (Tier 1 or 2). Pins the NEW or CHANGED interface shape the producing and consuming layers must agree on for this phase, so an agent building one layer in isolation does not have to guess the other's shape.
|
|
4
4
|
|
|
5
5
|
Lifecycle: pinned by the producer plan (NNa) BEFORE the consumer plan (NNb) builds against it -> ratified at the Tier-2 gate -> folded into the governing ADR when the phase lands (`status: absorbed`). The durable contract is the ADR; this file is the working delta on top of it.
|
|
6
6
|
|
|
@@ -22,7 +22,7 @@ CONTEXT.md with "NON-NEGOTIABLE" or "DEFERRED" sections
|
|
|
22
22
|
| `ROADMAP.md` | `.forge/roadmap.yml` | Extract phases, milestones. Add dependency references between phases. Convert to YAML. |
|
|
23
23
|
| `STATE.md` | `.forge/state/index.yml` + `.forge/state/milestone-{id}.yml` | Extract current phase number, progress, active blockers, recent decisions. Split into global index and per-milestone state. |
|
|
24
24
|
| `CONTEXT.md` | `.forge/context.md` | NON-NEGOTIABLE → Locked Decisions. DEFERRED → Deferred Ideas. DISCRETION → Discretion Areas. Minimal format change needed. |
|
|
25
|
-
| `PLAN.md` | `.forge/phases/
|
|
25
|
+
| `PLAN.md` | `.forge/phases/milestone-{id}/{phase}-{name}/plan.md` | Keep XML task format. Add `must_haves` YAML frontmatter. Split per-phase if combined. |
|
|
26
26
|
| `references/ui-brand.md` | `.forge/design-system.md` | Extract component rules, brand guidelines. Convert to component mapping table format. |
|
|
27
27
|
| `references/tdd.md` | `.forge/constitution.md` Article II | Inform Test-First article gates with project-specific testing approach. |
|
|
28
28
|
| `references/verification-patterns.md` | Informs `verifying` skill | Extract project-specific verification patterns. Add to constitution or context. |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Phase Plan Template
|
|
2
2
|
|
|
3
|
-
Copy to `.forge/phases/
|
|
4
|
-
`{
|
|
3
|
+
Copy to `.forge/phases/milestone-{id}/{phase}-{name}/plan-{NN}.md` for each plan in a phase.
|
|
4
|
+
`{id}` = milestone ID, `{phase}` = phase number (preserved verbatim, never renumbered), `{name}` = kebab-case phase name, `{NN}` = plan sequence within the phase.
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -21,9 +21,10 @@ roadmap:
|
|
|
21
21
|
name: "" # e.g., "MVP", "Admin Dashboard"
|
|
22
22
|
phases: [] # List of phase numbers belonging to this milestone
|
|
23
23
|
|
|
24
|
-
# Phases
|
|
25
|
-
#
|
|
26
|
-
#
|
|
24
|
+
# Phases belong to milestones (see each milestone's `phases:` list). Phase numbers
|
|
25
|
+
# are preserved verbatim and never renumbered.
|
|
26
|
+
# Directory naming: .forge/phases/milestone-{milestone_id}/{phase_num}-{name}/
|
|
27
|
+
# Example: milestone-1/1-foundation/, milestone-1/2-auth/, milestone-2/3-dashboard/
|
|
27
28
|
phases:
|
|
28
29
|
- id: 1
|
|
29
30
|
name: "" # Vertical slice name, e.g., "User can sign up"
|
|
@@ -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.
|