forge-orkes 0.19.2 → 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.
@@ -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
- // --- Section markers for CLAUDE.md ---
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
 
@@ -185,60 +200,61 @@ function upgradeDir(relDir, { autoClean = false } = {}) {
185
200
  }
186
201
 
187
202
  /**
188
- * Smart-merge CLAUDE.md using section markers.
189
- * - If markers exist: replace the forge section, preserve everything else
190
- * - If no markers but forge content exists: replace it and add markers
191
- * - If no forge content: append with markers
192
- * Returns 'replaced' | 'appended' | 'unchanged' | 'created'
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.
193
216
  */
194
- function mergeClaudeMd() {
195
- const srcPath = path.join(templateDir, 'CLAUDE.md');
217
+ function ensureClaudeMdImport() {
196
218
  const destPath = path.join(targetDir, 'CLAUDE.md');
197
219
 
198
- if (!fs.existsSync(srcPath)) return null;
199
-
200
- const forgeContent = fs.readFileSync(srcPath, 'utf-8');
201
-
202
- // No existing CLAUDE.md — just copy
220
+ // No existing CLAUDE.md write the stub.
203
221
  if (!fs.existsSync(destPath)) {
204
- fs.writeFileSync(destPath, forgeContent);
222
+ fs.writeFileSync(destPath, CLAUDE_STUB);
205
223
  return 'created';
206
224
  }
207
225
 
208
226
  const existing = fs.readFileSync(destPath, 'utf-8');
209
227
 
210
- // Check if content is already identical
211
- if (existing === forgeContent) return 'unchanged';
212
-
213
228
  const startIdx = existing.indexOf(FORGE_START);
214
229
  const endIdx = existing.indexOf(FORGE_END);
215
230
 
216
231
  if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
217
- // Markers found — replace the section between them (inclusive)
232
+ // Legacy marker section — replace it in place with the import line (position preserved).
218
233
  const before = existing.substring(0, startIdx);
219
234
  const after = existing.substring(endIdx + FORGE_END.length);
220
- const merged = before + forgeContent + after;
221
-
222
- // Clean up any double newlines at the seams
223
- const cleaned = merged.replace(/\n{3,}/g, '\n\n');
224
- fs.writeFileSync(destPath, cleaned);
225
- return 'replaced';
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';
226
241
  }
227
242
 
228
- // No markers check if there's an old forge section (starts with "# Forge")
243
+ // No markers, but a legacy `# Forge` header — old installs appended prose to EOF.
229
244
  const forgeHeaderIdx = existing.indexOf('# Forge\n');
230
245
  if (forgeHeaderIdx !== -1) {
231
- // Old install without markers — replace from "# Forge" to end of file
232
- // (Forge content is always appended at the end in old installs)
233
- const before = existing.substring(0, forgeHeaderIdx);
234
- const merged = before + forgeContent;
235
- const cleaned = merged.replace(/\n{3,}/g, '\n\n');
236
- fs.writeFileSync(destPath, cleaned);
237
- 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';
238
250
  }
239
251
 
240
- // No forge content at allappend with markers
241
- const merged = existing.trimEnd() + '\n\n' + forgeContent + '\n';
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;
242
258
  fs.writeFileSync(destPath, merged);
243
259
  return 'appended';
244
260
  }
@@ -290,23 +306,6 @@ function isForgeInstalled() {
290
306
  async function install() {
291
307
  console.log('\n Forge - Meta-prompting framework for Claude Code\n');
292
308
 
293
- // Handle CLAUDE.md — use smart merge
294
- const claudeStatus = mergeClaudeMd();
295
- switch (claudeStatus) {
296
- case 'created':
297
- console.log(' Created CLAUDE.md');
298
- break;
299
- case 'replaced':
300
- console.log(' Updated Forge section in CLAUDE.md (user content preserved)');
301
- break;
302
- case 'appended':
303
- console.log(' Appended Forge config to existing CLAUDE.md');
304
- break;
305
- case 'unchanged':
306
- console.log(' CLAUDE.md already up to date');
307
- break;
308
- }
309
-
310
309
  // Copy .claude/ directory
311
310
  const srcClaude = path.join(templateDir, '.claude');
312
311
  const destClaude = path.join(targetDir, '.claude');
@@ -323,6 +322,23 @@ async function install() {
323
322
  // the template ships it as `gitignore`. Materialize the real dotfile here.
324
323
  materializeForgeGitignore(destForge);
325
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
+
326
342
  // Stamp version from package.json into settings.json
327
343
  const settingsPath = path.join(targetDir, SETTINGS_FILE);
328
344
  if (fs.existsSync(settingsPath)) {
@@ -558,10 +574,32 @@ async function upgrade() {
558
574
  }
559
575
  }
560
576
 
561
- // 3. Smart-merge CLAUDE.md using section markers
562
- const claudeStatus = mergeClaudeMd();
563
- if (claudeStatus === 'replaced' || claudeStatus === 'appended') {
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') {
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') {
564
601
  results.updated.push('CLAUDE.md');
602
+ console.log(' CLAUDE.md: @.forge/FORGE.md import line restored');
565
603
  } else if (claudeStatus === 'unchanged') {
566
604
  results.unchanged.push('CLAUDE.md');
567
605
  } else if (claudeStatus === 'created') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-orkes",
3
- "version": "0.19.2",
3
+ "version": "0.21.0",
4
4
  "description": "Set up the Forge meta-prompting framework for Claude Code in your project",
5
5
  "bin": {
6
6
  "create-forge": "./bin/create-forge.js"
@@ -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/m{M}-{N}-{name}/plan.md`, context: `.forge/context.md`, state: `.forge/state/milestone-{id}.yml`.
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/m{M}-{N}-{name}/plan.md
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/m{M}-{N}-{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).
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/m{M}-{N}-{name}/plan.md`), execution summary, source (read-only), milestone state.
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/m{M}-{N}-{name}/plan.md → extract must_haves
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/m{M}-{N}-{name}/data-model.md`.
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/m{M}-{N}-{name}/contracts/`.
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/m{M}-{N}-{name}/`
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/m{M}-{N}-{name}/plan-{NN}.md → the plan
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/m{M}-{N}-{name}/plan-{NN}.md`)
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/m{M}-{N}-{name}/specs/` to test directory
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{M}-{N} task <name>"
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: m{M}-{N}-{name}, Plan {NN}
224
+ # Execution Summary: milestone-{id}/{phase}-{name}, Plan {NN}
225
225
 
226
226
  ## Completed Tasks
227
227
  1. [Task name] — [one-line result]
@@ -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/m{id}-*/` (glob all phase dirs)
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/m{M}-{N}-{name}/`, `.forge/requirements/m{N}.yml`, roadmap.yml | executing |
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/m{M}-{N}-{name}/contract.md` (from `.forge/templates/contract.md`); references its governing ADR; folded back into that ADR on landing.
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/m{M}-{N}-{name}/plan-{NN}.md`
180
- - `{M}`=milestone, `{N}`=phase#, `{name}`=kebab, `{NN}`=seq
181
- - Ex: `.forge/phases/m3-2-providers/plan-01.md`
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/m{M}-{N}-{name}/specs/`
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/m1-1-auth/specs/login-endpoint.test.ts
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/m1-1-auth/specs/login-endpoint.test.ts</spec>
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/m{id}-*/contract.md`.
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).
@@ -26,8 +26,9 @@ Read the source version (`{source}/packages/create-forge/package.json` `version`
26
26
 
27
27
  | Category | Paths | Behavior |
28
28
  |----------|-------|----------|
29
- | **Framework-owned** | `.claude/agents/*.md`, `.claude/skills/*/SKILL.md` | Overwrite |
30
- | **Merge-owned** | `CLAUDE.md`, `.claude/settings.json` | Never auto-overwrite |
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) |
31
32
  | **Template-only** | `.forge/templates/**`, `.forge/migrations/**`, `.forge/gitignore` → `.forge/.gitignore` | Overwrite |
32
33
 
33
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`.
@@ -50,11 +51,19 @@ Same process as Step 3 for `.forge/templates/**` and `.forge/migrations/**`. (Ma
50
51
 
51
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.
52
53
 
53
- ## Step 5: Handle Merge-Owned Files
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.
54
55
 
55
- **`CLAUDE.md`:**
56
- 1. Read source and local versions
57
- 2. If different summarize changes (new/removed/modified sections) and let the user decide how to merge. Do not overwrite.
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.
58
67
 
59
68
  **`.claude/settings.json`:**
60
69
  1. Read source and local versions
@@ -82,13 +91,17 @@ Removed from template: {N} files
82
91
  - .claude/agents/old-agent.md (still in your project)
83
92
  - ...
84
93
 
85
- Needs manual review: {N} files
86
- - CLAUDE.md (new sections: "Upgrade Mechanism", modified: "Skill Routing")
87
- - ...
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)
88
99
 
89
100
  Unchanged: {N} files
90
101
  ```
91
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
+
92
105
  ## Step 7: Post-Upgrade Migration Checks
93
106
 
94
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.
@@ -182,6 +195,38 @@ Migrate now? (yes/no/show guide)
182
195
 
183
196
  `upgrading` never edits `.forge/state/` directly — migration runs via `quick-tasking`.
184
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
+
185
230
  ### Future migrations
186
231
 
187
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/m{M}-{N}-{name}/plan-{NN}.md → must_haves (truths, artifacts, key_links)
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/m{M}-{N}-{name}/plan-{NN}.md` — Task plans with must_haves frontmatter
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
  #
@@ -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/m{M}-{N}-{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.
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/m{M}-{N}-{name}/plan.md` | Keep XML task format. Add `must_haves` YAML frontmatter. Split per-phase if combined. |
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/m{M}-{N}-{name}/plan-{NN}.md` for each plan in a phase.
4
- `{M}` = milestone ID, `{N}` = phase number within milestone, `{name}` = kebab-case phase name.
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 are scoped within milestones. Each phase has a local number (1, 2, 3...)
25
- # within its milestone. Directory naming: .forge/phases/m{milestone_id}-{phase_num}-{name}/
26
- # Example: m1-1-foundation/, m1-2-auth/, m2-1-dashboard/
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"