forge-orkes 0.18.1 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,29 @@ const FORGE_END = '<!-- forge:end -->';
18
18
  // Framework-owned: Forge controls these entirely
19
19
  const FRAMEWORK_OWNED_DIRS = ['.claude/agents', '.claude/skills'];
20
20
 
21
+ // Experimental / opt-in skills installed separately (e.g. from experimental/m10).
22
+ // They are NOT in the shipped base template, so the framework-owned auto-clean
23
+ // would otherwise delete them on upgrade. Preserve them instead.
24
+ const EXPERIMENTAL_SKILL_PATHS = ['.claude/skills/orchestrating'];
25
+
26
+ function isExperimentalSkillPath(displayPath) {
27
+ const norm = displayPath.split(path.sep).join('/');
28
+ return EXPERIMENTAL_SKILL_PATHS.some(
29
+ (p) => norm === p || norm.startsWith(p + '/')
30
+ );
31
+ }
32
+
33
+ // Compare dotted numeric versions (e.g. "0.19.1"). Returns 1 if a>b, -1 if a<b, 0 if equal.
34
+ function compareVersions(a, b) {
35
+ const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
36
+ const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0);
37
+ for (let i = 0; i < 3; i++) {
38
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
39
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
40
+ }
41
+ return 0;
42
+ }
43
+
21
44
  // Template-only: reference templates Forge controls
22
45
  const TEMPLATE_ONLY_DIRS = ['.forge/templates', '.forge/migrations'];
23
46
 
@@ -44,6 +67,21 @@ function copyDirRecursive(src, dest) {
44
67
  return count;
45
68
  }
46
69
 
70
+ /**
71
+ * npm strips files literally named `.gitignore` from a published tarball (it
72
+ * treats them as ignore-rules, not content), so the template ships its forge
73
+ * gitignore as `gitignore`. Rename it to the real `.forge/.gitignore` dotfile
74
+ * in the destination. Idempotent: if `gitignore` is absent it does nothing; if
75
+ * a `.gitignore` already exists it is overwritten with the shipped canonical one.
76
+ */
77
+ function materializeForgeGitignore(destForge) {
78
+ const shipped = path.join(destForge, 'gitignore');
79
+ const real = path.join(destForge, '.gitignore');
80
+ if (fs.existsSync(shipped)) {
81
+ fs.renameSync(shipped, real);
82
+ }
83
+ }
84
+
47
85
  function prompt(question) {
48
86
  const rl = readline.createInterface({
49
87
  input: process.stdin,
@@ -91,7 +129,7 @@ function upgradeDir(relDir, { autoClean = false } = {}) {
91
129
  const srcDir = path.join(templateDir, relDir);
92
130
  const destDir = path.join(targetDir, relDir);
93
131
 
94
- const result = { updated: [], added: [], unchanged: [], removed: [] };
132
+ const result = { updated: [], added: [], unchanged: [], removed: [], preserved: [] };
95
133
 
96
134
  if (!fs.existsSync(srcDir)) return result;
97
135
 
@@ -123,6 +161,12 @@ function upgradeDir(relDir, { autoClean = false } = {}) {
123
161
  for (const rel of destFiles) {
124
162
  const srcPath = path.join(srcDir, rel);
125
163
  if (!fs.existsSync(srcPath)) {
164
+ const displayPath = path.join(relDir, rel);
165
+ // Never auto-clean opt-in experimental skills — they live outside the base template.
166
+ if (autoClean && isExperimentalSkillPath(displayPath)) {
167
+ result.preserved.push(displayPath);
168
+ continue;
169
+ }
126
170
  const destPath = path.join(destDir, rel);
127
171
  if (autoClean) {
128
172
  fs.unlinkSync(destPath);
@@ -275,6 +319,10 @@ async function install() {
275
319
  const forgeCount = copyDirRecursive(srcForge, destForge);
276
320
  console.log(` Installed .forge/templates/ (${forgeCount} files)`);
277
321
 
322
+ // npm strips files literally named `.gitignore` from the published tarball, so
323
+ // the template ships it as `gitignore`. Materialize the real dotfile here.
324
+ materializeForgeGitignore(destForge);
325
+
278
326
  // Stamp version from package.json into settings.json
279
327
  const settingsPath = path.join(targetDir, SETTINGS_FILE);
280
328
  if (fs.existsSync(settingsPath)) {
@@ -343,6 +391,32 @@ function detectMissingLayersConfig() {
343
391
  return true;
344
392
  }
345
393
 
394
+ /**
395
+ * Detect a pre-0.19.0 state/index.yml: 0.19.0 makes index.yml a thin derived
396
+ * registry (regenerated by rollup) and moves desire_paths to append-only files.
397
+ * A legacy index.yml still carries desire_paths:/metrics:/embedded narrative, or
398
+ * is large. Returns true if index.yml exists and looks legacy; false if missing
399
+ * (not yet initialized) or already a slim registry.
400
+ */
401
+ function detectLegacyStateIndex() {
402
+ const indexYml = path.join(targetDir, '.forge', 'state', 'index.yml');
403
+ if (!fs.existsSync(indexYml)) return false;
404
+
405
+ let content;
406
+ try {
407
+ content = fs.readFileSync(indexYml, 'utf-8');
408
+ } catch {
409
+ return false;
410
+ }
411
+
412
+ // Legacy markers: shared accumulators or per-milestone narrative drifted in.
413
+ if (/^\s*(desire_paths|metrics|current_status):/m.test(content)) return true;
414
+ // A slim registry is small; KBs of index.yml means narrative crept in.
415
+ if (Buffer.byteLength(content, 'utf-8') > 4096) return true;
416
+
417
+ return false;
418
+ }
419
+
346
420
  /**
347
421
  * After upgrade, surface any detected legacy file layouts the new framework
348
422
  * version no longer writes, or new config fields older projects lack.
@@ -382,6 +456,21 @@ function runPostUpgradeMigrationChecks() {
382
456
  console.log(' /forge then hand the guide to quick-tasking.');
383
457
  console.log();
384
458
  }
459
+
460
+ if (detectLegacyStateIndex()) {
461
+ console.log(' ⚠ Worktree-safe state: legacy state/index.yml detected (Forge 0.19.0)');
462
+ console.log(' ────────────────────────────────────────────────────────────────────');
463
+ console.log(' Forge 0.19.0 makes index.yml a derived registry (regenerated by rollup),');
464
+ console.log(' moves desire_paths to append-only files, and commits .forge/ state at every');
465
+ console.log(' phase handoff. Your index.yml predates this — it still carries desire_paths/');
466
+ console.log(' metrics or embedded narrative, which conflicts across git worktrees.');
467
+ console.log();
468
+ console.log(' Migration guide: .forge/migrations/0.19.0-worktree-safe-state.md');
469
+ console.log();
470
+ console.log(' This migration edits user-owned state, so it is guided — never automatic.');
471
+ console.log(' In Claude Code: /forge then hand the guide to quick-tasking.');
472
+ console.log();
473
+ }
385
474
  }
386
475
 
387
476
  async function upgrade() {
@@ -408,11 +497,28 @@ async function upgrade() {
408
497
  console.log(` Installed: v${installedVersion}`);
409
498
  console.log(` Available: v${pkgVersion}\n`);
410
499
 
500
+ // Downgrade guard: refuse to roll backward (overwrites newer framework files
501
+ // with older ones and deletes files newer versions added). Almost always a
502
+ // stale npx cache serving an old forge-orkes. Override with --force.
503
+ const force = process.argv.includes('--force');
504
+ if (
505
+ installedVersion !== 'unknown' &&
506
+ compareVersions(pkgVersion, installedVersion) < 0 &&
507
+ !force
508
+ ) {
509
+ console.error(` ✖ Refusing to downgrade: installed v${installedVersion} is newer than available v${pkgVersion}.`);
510
+ console.error(` This usually means a stale npx cache served an old forge-orkes.`);
511
+ console.error(` Fix: npx forge-orkes@latest upgrade (or clear the cache: rm -rf ~/.npm/_npx)`);
512
+ console.error(` To downgrade intentionally: forge-orkes upgrade --force\n`);
513
+ process.exit(1);
514
+ }
515
+
411
516
  const results = {
412
517
  updated: [],
413
518
  added: [],
414
519
  unchanged: [],
415
520
  removed: [],
521
+ preserved: [],
416
522
  };
417
523
 
418
524
  // 1. Process framework-owned directories (auto-clean stale files)
@@ -422,6 +528,7 @@ async function upgrade() {
422
528
  results.added.push(...dirResult.added);
423
529
  results.unchanged.push(...dirResult.unchanged);
424
530
  results.removed.push(...dirResult.removed);
531
+ results.preserved.push(...dirResult.preserved);
425
532
  }
426
533
 
427
534
  // 2. Process template-only directories
@@ -431,6 +538,24 @@ async function upgrade() {
431
538
  results.added.push(...dirResult.added);
432
539
  results.unchanged.push(...dirResult.unchanged);
433
540
  results.removed.push(...dirResult.removed);
541
+ results.preserved.push(...dirResult.preserved);
542
+ }
543
+
544
+ // 2b. Sync the forge .gitignore. Shipped as `gitignore` (npm strips dotfiles
545
+ // named .gitignore); materialize/refresh it as `.forge/.gitignore`.
546
+ const giSrc = path.join(templateDir, '.forge', 'gitignore');
547
+ const giDest = path.join(targetDir, '.forge', '.gitignore');
548
+ if (fs.existsSync(giSrc)) {
549
+ const newContent = fs.readFileSync(giSrc, 'utf-8');
550
+ const existed = fs.existsSync(giDest);
551
+ const oldContent = existed ? fs.readFileSync(giDest, 'utf-8') : null;
552
+ if (oldContent !== newContent) {
553
+ fs.mkdirSync(path.dirname(giDest), { recursive: true });
554
+ fs.writeFileSync(giDest, newContent);
555
+ results[existed ? 'updated' : 'added'].push('.forge/.gitignore');
556
+ } else {
557
+ results.unchanged.push('.forge/.gitignore');
558
+ }
434
559
  }
435
560
 
436
561
  // 3. Smart-merge CLAUDE.md using section markers
@@ -483,6 +608,14 @@ async function upgrade() {
483
608
  console.log();
484
609
  }
485
610
 
611
+ if (results.preserved.length > 0) {
612
+ console.log(` Preserved (${results.preserved.length}):`);
613
+ for (const f of results.preserved) {
614
+ console.log(` ${f} (kept — opt-in experimental skill)`);
615
+ }
616
+ console.log();
617
+ }
618
+
486
619
  console.log(` Upgraded to v${pkgVersion}\n`);
487
620
 
488
621
  runPostUpgradeMigrationChecks();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-orkes",
3
- "version": "0.18.1",
3
+ "version": "0.19.2",
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"
@@ -18,7 +18,7 @@ Execute plan tasks. Full dev tools, strict deviation rules. Plan says X, build X
18
18
  Plan: `.forge/phases/m{M}-{N}-{name}/plan.md`, context: `.forge/context.md`, state: `.forge/state/milestone-{id}.yml`.
19
19
 
20
20
  ## Output
21
- Committed code, updated `milestone-{id}.yml` (progress/deviations), updated `index.yml` (timestamp), execution summary.
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.
22
22
 
23
23
  ## Deviation Rules
24
24
 
@@ -67,6 +67,7 @@ One change per commit. Stage specific files. Rules 1,3 get own commits. Test bef
67
67
  20+ files -> spawn fresh sub-agents via Task. Coordinate via milestone state. Approaching limits -> summarize, spawn fresh. State in `.forge/state/milestone-{id}.yml`.
68
68
 
69
69
  ### 6. Update State
70
+ Write to `.forge/state/milestone-{id}.yml` only (advance `current` cursor + set `current.last_updated`). Never `index.yml`.
70
71
  ```yaml
71
72
  progress:
72
73
  - task: "{task name}"
@@ -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`.
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).
22
22
 
23
23
  ## Process
24
24
 
@@ -27,7 +27,7 @@ Supplied by the reviewing skill at spawn:
27
27
 
28
28
  Additional context to read on start:
29
29
  - `.forge/project.yml` — stack, framework, database, dependencies
30
- - `.forge/state/milestone-{id}.yml` — milestone id + name
30
+ - `.forge/state/milestone-{id}.yml` — milestone id + name (source of truth; index.yml is a derived registry)
31
31
  - `.forge/constitution.md` — active gates (if present)
32
32
  - `.forge/deferred-issues.md` — pre-existing failures (architecture mode only)
33
33
 
@@ -58,7 +58,7 @@ Run: {n} | Passed: {n} | Failed: {n} | Coverage: {if available}
58
58
  ### 1. Load Criteria
59
59
  ```
60
60
  Read: .forge/phases/m{M}-{N}-{name}/plan.md → extract must_haves
61
- Read: .forge/state/milestone-{id}.yml → reported progress
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)
64
64
  ```
@@ -125,6 +125,7 @@ Document in `.forge/phases/m{M}-{N}-{name}/contracts/`.
125
125
 
126
126
  1. **Persist** — Confirm ADRs in `.forge/decisions/`, models and contracts in `.forge/phases/m{M}-{N}-{name}/`
127
127
  2. **Update state** — Set `current.status` to `planning` in `.forge/state/milestone-{id}.yml`
128
- 3. **Recommend context clear:**
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
+ 4. **Recommend context clear:**
129
130
 
130
- *"Architecture decisions written. `/clear` then `/forge` to continue with planning."*
131
+ *"Architecture decisions written and synced. `/clear` then `/forge` to continue with planning."*
@@ -294,14 +294,14 @@ If no milestone exists (advisory discussion — forge routed here without tier/m
294
294
  - Multi-file, refactor, feature, service changes → **Standard**
295
295
  - Major architectural, multi-subsystem, multi-phase → **Full**
296
296
  3. **Create milestone.**
297
- - Read `.forge/state/index.yml` (create from `.forge/templates/state/index.yml` if missing)
298
- - Next ID = max existing ID + 1 (or 1 if none)
297
+ - Next ID = (max id across existing `.forge/state/milestone-*.yml`) + 1 (or 1 if none)
299
298
  - Create `milestone-{id}.yml` from `.forge/templates/state/milestone.yml`:
300
299
  - `milestone.id` = {id}, `milestone.name` = brief summary of discussion topic
301
300
  - `current.tier` = detected tier
302
301
  - `current.status` = `planning` (Standard) / `architecting` (Full) / `not_started` (Quick)
302
+ - `current.last_updated` = now
303
303
  - `decisions[]` = locked decisions from `context.md`
304
- - Add entry to `index.yml`: `id`, `name`, `status: active`, `last_updated: now`
304
+ - Regenerate `.forge/state/index.yml` via the `forge` **Rollup** (read all `milestone-*.yml` → rewrite registry). Do not hand-add the entry. (Create `index.yml` from `.forge/templates/state/index.yml` first if missing.)
305
305
  4. **Update `context.md`** — scope decisions under `### M{id} — {name} (locked {date})`
306
306
  5. **Quick tier?** → *"Scope is Quick. `/clear` then `/forge {id}` for quick-tasking."* End.
307
307
  6. Proceed to Step B with new milestone.
@@ -323,4 +323,5 @@ If milestone exists → skip to Step B.
323
323
  - Add any unresolved items to `## Needs Resolution`
324
324
  - If `context.md` already exists (post-planning discussion), update relevant sections + log amendments
325
325
  2. **Update state** -- `current.status` = `planning` (`architecting` for Full) in milestone yml
326
- 3. **Recommend clear:** *"State written and verified ({N} decisions in context.md). `/clear` then `/forge` for {planning/architecting}."*
326
+ 3. **State-sync commit** (State Commit Protocol): `git add .forge/` then `git commit -m "chore(forge): sync state after discussing — m{N} {name}"` (scoped; never `git add .`).
327
+ 4. **Recommend clear:** *"State synced and verified ({N} decisions in context.md). `/clear` then `/forge` for {planning/architecting}."*
@@ -241,19 +241,19 @@ Do NOT list test failures here — pre-existing failures belong in deferred-issu
241
241
  ```
242
242
 
243
243
  ## State Updates
244
- 1. Update `.forge/state/milestone-{id}.yml` — advance `current` cursor (`current.plan`/`current.task`, and `current.phase` when a phase completes). Do **not** write a progress percent — it is derived on read by the `forge` skill from `current.phase` vs the roadmap phase count.
244
+ 1. Update `.forge/state/milestone-{id}.yml` — advance the `current` cursor (`current.plan`/`current.task`, and `current.phase` when a phase completes) and set `current.last_updated`. Do **not** write a progress percent — it is derived on read by the `forge` skill from `current.phase` vs the roadmap phase count.
245
245
  2. Record deviations in milestone state
246
- 3. Update `.forge/state/index.yml` — set `last_updated` timestamp
246
+ 3. Do **not** write `.forge/state/index.yml` — it is a derived registry; the `forge` rollup regenerates it. Worktree agents write only their own milestone file.
247
247
  4. All plans in phase complete → transition to `verifying`
248
248
 
249
249
  **When deferring an individual phase or task** (writing `status: deferred` or `deferred: true` to a phase/task entry inside the milestone state file — as opposed to milestone-wide defer which goes through the forge skill's lifecycle flow): write `deferred_at` (ISO date today) and `deferred_reason` (one-line) siblings. Old entries without these fields parse fine — lazy migration. These feed the `deferred` aggregator skill.
250
250
 
251
251
  ## Desire Path Signals
252
252
 
253
- Log to `.forge/state/index.yml desire_paths` (global, not per-milestone):
254
- - **Repeated deviations**: Same rule, same reason, more than once → `deviation_pattern`
255
- - **User corrections**: Repeated correction matching a prior one → `user_correction`, increment count
256
- - **Agent struggles**: Multiple attempts or user guidance needed → `agent_struggle`
253
+ Append **one file per observation** to `.forge/state/desire-paths/` — copy `.forge/templates/state/desire-path.yml`, name it `{YYYY-MM-DD}-{type}-{milestone}-{slug}.yml`. Never write `index.yml` and never keep a counter; recurrence is derived by globbing (so concurrent worktrees only ever add files).
254
+ - **Repeated deviations**: Same rule, same reason, more than once → `type: deviation_pattern`
255
+ - **User corrections**: Repeated correction matching a prior one → `type: user_correction`
256
+ - **Agent struggles**: Multiple attempts or user guidance needed → `type: agent_struggle`
257
257
 
258
258
  ## Cross-Layer Seam Check
259
259
 
@@ -269,7 +269,8 @@ After **both** layer plans are committed, the executing flow owns one final **se
269
269
  5. Leave the contract at `status: ratified` — the `reviewing` skill folds `delta` into the governing ADR (`status: absorbed`) at milestone landing. Do **not** absorb here.
270
270
 
271
271
  ## Phase Handoff
272
- 1. Confirm persistence — summary documented, commits made, state updated, desire paths logged
272
+ 1. Confirm persistence — summary documented, commits made, state updated, desire-path files written
273
273
  2. **Run the Cross-Layer Seam Check** (above) if this phase was a Tier-2 contract split
274
- 3. Set `current.status` to `verifying`
275
- 4. Recommend: *"Tasks committed, state updated. `/clear` then `/forge` to continue with verifying."*
274
+ 3. Set `current.status` to `verifying` in `.forge/state/milestone-{id}.yml`
275
+ 4. **State-sync commit** (State Commit Protocol): `git add .forge/` then `git commit -m "chore(forge): sync state after executing m{N} {phase-name}"`. Scoped — never `git add .`. Per-task code commits already landed; this captures the cursor + desire-path files at the phase boundary.
276
+ 5. Recommend: *"Tasks committed, state synced. `/clear` then `/forge` to continue with verifying."*
@@ -9,6 +9,22 @@ Entry point. Detect tier, route skills, manage transitions. New projects → ini
9
9
 
10
10
  ## Step 1: Read State
11
11
 
12
+ ### 1.0 State Rollup (index.yml is derived)
13
+
14
+ `index.yml` is a **derived registry** — regenerate it from the milestone files before reading, and after any milestone CRUD (promote/defer/resume/delete). **Never hand-edit `index.yml`.**
15
+
16
+ Rollup procedure (deterministic + idempotent):
17
+ 1. Glob `.forge/state/milestone-*.yml`.
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
+ 3. Derive the registry `status`:
20
+ - **deferred** — `lifecycle.deferred_at` set and not superseded by a later `resumed_at`
21
+ - **complete** — `current.status == complete`
22
+ - **not_started** — `current.status == not_started`
23
+ - **active** — otherwise
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
+
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
+
12
28
  ### 1.1 Milestone Selection
13
29
 
14
30
  Check state files:
@@ -17,7 +33,7 @@ Check state files:
17
33
  3. Neither → init/tier detection.
18
34
 
19
35
  **With `state/index.yml`:**
20
- 1. Read for active milestones
36
+ 1. Run **Rollup (1.0)** to regenerate `index.yml` from the milestone files, then read it for active milestones
21
37
  2. **Check arg first.** `/forge 2` or `/forge "Auth system"`:
22
38
  - Match IDs (exact) or names (case-insensitive substring)
23
39
  - Found → auto-select: *"Resuming {id}: [{name}] -- {current.status}, {percent}%"*
@@ -57,13 +73,8 @@ Check `.forge/refactor-backlog.yml`:
57
73
  - `milestone.name: "Promoted from {R-id}: {backlog item title}"`
58
74
  - `milestone.origin: {R-id}`
59
75
  - `current.tier: standard`, `current.status: researching`
60
- 3. Append to `.forge/state/index.yml` `milestones:`:
61
- ```yaml
62
- - id: m-{R-id}
63
- name: "Promoted from {R-id}: {title}"
64
- status: active
65
- last_updated: "{ISO date}"
66
- ```
76
+ - `current.last_updated: {ISO date}`
77
+ 3. Run **Rollup (1.0)** — regenerates `index.yml` from the new milestone file (do not hand-append the registry entry). New milestone id is `m-{R-id}` from step 1; for numeric ids, allocate by scanning existing `milestone-*.yml` for the max id + 1.
67
78
  4. Append to `.forge/roadmap.yml` — one milestone entry + one phase (`refactor-{R-id}`, `requirements_source: refactor-backlog.yml#{R-id}`, `dependencies: []`).
68
79
  5. Update `.forge/refactor-backlog.yml` item: `status: in_progress`, add `promoted_to: m-{R-id}`.
69
80
  6. Confirm: *"Promoted {R-id} → milestone m-{R-id}. State + roadmap created. Routing to researching."*
@@ -71,16 +82,17 @@ Check `.forge/refactor-backlog.yml`:
71
82
  ```text
72
83
  backlog pickup → effort: standard
73
84
  ├─ write milestone-{m-R-id}.yml (origin: {R-id})
74
- ├─ append index.yml + roadmap.yml
85
+ ├─ rollup index.yml + append roadmap.yml
75
86
  ├─ flip backlog item (in_progress + promoted_to)
76
87
  └─ fall through → Standard tier routing
77
88
  ```
78
89
 
79
90
  Downstream skills (researching, discussing, planning, executing, verifying, reviewing) see a normal milestone — no special branching.
80
91
 
81
- Check `desire_paths` 3+ occurrences:
82
- - *"Recurring: [{description}] ({N}x). Fix via [suggestion]?"*
83
- - Agree → apply, reset. Decline → note, don't nag.
92
+ Check desire paths for 3+ occurrences: glob `.forge/state/desire-paths/*.yml`, group by `type` + normalized `note`, count each group.
93
+ - 3+ in a group → *"Recurring: [{note}] ({N}x). Fix via [suggestion]?"*
94
+ - Agree → apply; archive the group's files to `.forge/state/desire-paths/resolved/`. Decline → note, don't nag.
95
+ - (Occurrence count is derived from file count — there is no counter to reset.)
84
96
 
85
97
  ### 1.3 Interface Check
86
98
 
@@ -111,19 +123,19 @@ No match → fall through to Step 2B (tier detection).
111
123
 
112
124
  1. Validate: milestone in `index.yml`, status not `deferred`
113
125
  2. Extract reason (text after "—"/"-" or second sentence). None → ask: *"Reason for deferring?"*
114
- 3. Update `index.yml`: `status: deferred`, `last_updated`
115
- 4. Update `milestone-{id}.yml`:
126
+ 3. Update `milestone-{id}.yml`:
116
127
  - `lifecycle.deferred_at` = ISO 8601 now
117
128
  - `lifecycle.deferred_reason` = reason
118
129
  - `lifecycle.status_before_defer` = copy of `current.status`
130
+ 4. Run **Rollup (1.0)** — `index.yml` now derives `status: deferred` from the milestone file (do not hand-edit it).
119
131
  5. Update `roadmap.yml`: all phases belonging to this milestone → `status: deferred`
120
132
  6. Confirm: *"Deferred milestone {id}: {name}. Was {status}, frozen at {current.status}. Reason: {reason}."*
121
133
 
122
134
  ### Resume Operation
123
135
 
124
136
  1. Validate: milestone in `index.yml`, status is `deferred`
125
- 2. Update `index.yml`: `status: active`, `last_updated`
126
- 3. Update `milestone-{id}.yml`: `lifecycle.resumed_at` = ISO 8601 now
137
+ 2. Update `milestone-{id}.yml`: `lifecycle.resumed_at` = ISO 8601 now
138
+ 3. Run **Rollup (1.0)** — `index.yml` now derives `status: active` from the milestone file.
127
139
  4. Confirm + route: *"Resumed milestone {id}: {name}. Picking up at {current.status}."*
128
140
  5. Fall through to normal routing (Step 3) — `current.status` already frozen at correct position
129
141
 
@@ -144,10 +156,9 @@ No match → fall through to Step 2B (tier detection).
144
156
  c. Copy phase dirs → archive `phases/`
145
157
  d. Copy research file → archive `research.md` (if exists)
146
158
  e. Copy audit files → archive `audits/` (if exists)
147
- f. Remove originals after successful copy
148
- g. Remove milestone entry from `index.yml`
149
- h. Remove milestone + its phases from `roadmap.yml`
150
- i. Update `index.yml` `last_updated`
159
+ f. Remove originals after successful copy (including `milestone-{id}.yml`)
160
+ g. Remove milestone + its phases from `roadmap.yml`
161
+ h. Run **Rollup (1.0)** `index.yml` regenerates without the removed milestone
151
162
  5. Confirm: *"Archived milestone {id}: {name} → .forge/archive/milestone-{id}/. Removed from active state."*
152
163
 
153
164
  **Safety:** Copy-then-delete, not move. If copy fails, originals intact.
@@ -297,7 +308,7 @@ Phase transitions = clear boundaries. **Recommend `/clear`** after writing state
297
308
  | architecting | ADRs, data models, API contracts | planning |
298
309
  | planning | `.forge/phases/m{M}-{N}-{name}/`, `.forge/requirements/m{N}.yml`, roadmap.yml | executing |
299
310
  | executing | Committed code, execution summary, state | verifying |
300
- | verifying | Verification report, desire paths | reviewing |
311
+ | verifying | Verification report, desire-path files (`state/desire-paths/`) | reviewing |
301
312
 
302
313
  ### Context Loading on Resume
303
314
 
@@ -314,4 +325,4 @@ Branches (any tier, on-demand): debugging (stuck) · designing (UI) · securing
314
325
  Phase boundaries: `[clear]` recommended between phases to reset context.
315
326
  ```
316
327
 
317
- Update `milestone-{id}.yml` + `index.yml` `last_updated` at each transition.
328
+ Update `milestone-{id}.yml` at each transition (set `current.last_updated`). `index.yml` is **not** hand-edited — it is regenerated by Rollup (1.0) on the next `forge` run.
@@ -338,15 +338,7 @@ User selects per stack.
338
338
  3. Write `.forge/design-system.md` (if configured)
339
339
  4. Write `.forge/contracts/index.yml` (only if `layers:` has 2+ entries) — copy `.forge/templates/contracts-index.yml`, fill `layers:` from the confirmed list, leave `integration_points:` empty (first cross-layer phase populates them via planning Step 6.1)
340
340
  5. Init state:
341
- - `.forge/state/index.yml`:
342
- ```yaml
343
- milestones:
344
- - id: 1
345
- name: "{project name}"
346
- status: active # not_started | active | deferred | complete
347
- last_updated: "{date}"
348
- ```
349
- - `.forge/state/milestone-1.yml`:
341
+ - `.forge/state/milestone-1.yml` (the source of truth):
350
342
  ```yaml
351
343
  milestone:
352
344
  id: 1
@@ -358,8 +350,12 @@ User selects per stack.
358
350
  plan: null
359
351
  task: null
360
352
  status: not_started
353
+ last_updated: "{date}"
361
354
  ```
355
+ - `.forge/state/index.yml` — regenerate via the `forge` **Rollup** from the milestone file (do not hand-write the registry). It will derive `status: not_started` for milestone 1.
356
+ - `.forge/state/desire-paths/` — create the (empty) directory for append-only observations.
362
357
  6. Templates as needed
358
+ 7. **State-sync commit**: `git add .forge/` then `git commit -m "chore(forge): initialize forge state"` (scoped; never `git add .`) — so the new project's state is in git from the start.
363
359
 
364
360
  *"Initialized. Ready?"*
365
361
 
@@ -156,7 +156,7 @@ Before decomposing, classify whether this phase crosses a layer boundary with a
156
156
  **Tier-2 ratify gate** (the ONLY interruption; frame as contract-correctness, not "parallelize y/n"):
157
157
  > *"This phase changes the {integration point} contract ({governing ADR}). Delta: [summary]. plan-NNa ({producer}) pins it; plan-NNb ({consumer}) builds against it in parallel. Is this contract shape correct?"*
158
158
 
159
- Block the split until confirmed. Override ("keep it one plan") -> log to `state/index.yml` `desire_paths` (recurring overrides tune the threshold), fall back to Tier 1.
159
+ Block the split until confirmed. Override ("keep it one plan") -> append a `type: tier_override` file to `.forge/state/desire-paths/` (recurring overrides tune the threshold), fall back to Tier 1.
160
160
 
161
161
  **Integration (Tier 2):** layer plans build isolated (per-layer worktrees). The phase's final task is a **seam check** owned by the executing flow (NOT a standing agent): merge the layer branches, verify the shape the producer emits matches what the consumer built against, per `contract.md`.
162
162
 
@@ -382,4 +382,5 @@ Done when approved.
382
382
 
383
383
  1. **Persist** -- plans `.forge/phases/`, reqs `.forge/requirements/m{N}.yml`, roadmap `.forge/roadmap.yml`, context `.forge/context.md`
384
384
  2. **State** -- `current.status` = `executing` in `.forge/state/milestone-{id}.yml`
385
- 3. *"Plan written. `/clear` then `/forge` to continue."*
385
+ 3. **State-sync commit** (State Commit Protocol): `git add .forge/` then `git commit -m "chore(forge): sync state after planning — m{N} {phase-name}"` (scoped; never `git add .`).
386
+ 4. *"Plan written and synced. `/clear` then `/forge` to continue."*
@@ -70,7 +70,7 @@ Invoked via forge routing (mid-workflow or refactor-backlog item with milestone
70
70
 
71
71
  1. Read `.forge/state/milestone-{id}.yml` for current position
72
72
  2. Follow standard workflow (above)
73
- 3. After commit: update milestone state — advance the `current` cursor if the task moved position, log deviations if any Rule 1-3 applied. Do **not** write a progress percent (derived on read by `forge`).
73
+ 3. After commit: update `milestone-{id}.yml` — advance the `current` cursor if the task moved position and set `current.last_updated`; log deviations if any Rule 1-3 applied. Do **not** write a progress percent (derived on read by `forge`) and do **not** write `index.yml` (derived). Then **state-sync commit**: `git add .forge/` && `git commit -m "chore(forge): sync state after quick-task — m{N}"` (scoped; never `git add .`).
74
74
  4. Report: fix description, files changed, current position
75
75
 
76
76
  ### Without Milestone
@@ -125,6 +125,7 @@ Artifact uses the Finding Format above, with two adjustments: prepend `Date: {YY
125
125
 
126
126
  1. **Write artifact** (see Research Artifact section above).
127
127
  2. **Update state** — Set `current.status` to `discussing` in `.forge/state/milestone-{id}.yml`
128
- 3. **Recommend context clear:**
128
+ 3. **State-sync commit** (State Commit Protocol): `git add .forge/` then `git commit -m "chore(forge): sync state after researching — m{N} {phase-name}"` (scoped; never `git add .`).
129
+ 4. **Recommend context clear:**
129
130
 
130
- *"Research complete. State written. `/clear` then `/forge` to continue with discussing."*
131
+ *"Research complete. State synced. `/clear` then `/forge` to continue with discussing."*
@@ -335,7 +335,7 @@ Missing? Create from `.forge/templates/refactor-backlog.yml`.
335
335
 
336
336
  ### Route
337
337
 
338
- **HEALTHY/WARNINGS (accepted):** `current.status: complete` in milestone + index. *"Milestone [{name}] complete. {N} backlog items."* Beads: `bd complete`.
338
+ **HEALTHY/WARNINGS (accepted):** set `current.status: complete` in `milestone-{id}.yml`, then regenerate `index.yml` via the `forge` **Rollup** (do not hand-edit index). *"Milestone [{name}] complete. {N} backlog items."* Beads: `bd complete`.
339
339
 
340
340
  **CRITICAL:** Don't complete. A) Fix->`planning` fix mode->re-verify->re-review. B) Accept risk->doc in report->complete.
341
341
 
@@ -387,5 +387,6 @@ If the milestone's phases produced `contract.md` files (planning Step 6.1 Tier 1
387
387
  1. Confirm report + backlog
388
388
  2. **Run promoted-milestone completion hook** (above) if `milestone.origin` set
389
389
  3. **Run Contract Landing** (above) for any cross-layer phases — fold ratified contracts into their ADRs
390
- 4. Set `current.status: complete` and `current.completed_at: "<ISO 8601 timestamp>"`
391
- 5. *"Milestone [{name}] complete. Report: `.forge/audits/milestone-{id}-health-report.md`. {N} backlog items. `/forge` or backlog."*
390
+ 4. Set `current.status: complete` and `current.completed_at: "<ISO 8601 timestamp>"` in `milestone-{id}.yml`, then regenerate `index.yml` via the `forge` **Rollup**
391
+ 5. **State-sync commit** (State Commit Protocol): `git add .forge/` then `git commit -m "chore(forge): sync state after reviewing — m{N} complete"` (scoped; never `git add .`).
392
+ 6. *"Milestone [{name}] complete. Report: `.forge/audits/milestone-{id}-health-report.md`. {N} backlog items. `/forge` or backlog."*
@@ -18,16 +18,22 @@ 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
29
  | **Framework-owned** | `.claude/agents/*.md`, `.claude/skills/*/SKILL.md` | Overwrite |
26
30
  | **Merge-owned** | `CLAUDE.md`, `.claude/settings.json` | Never auto-overwrite |
27
- | **Template-only** | `.forge/templates/**`, `.forge/migrations/**` | Overwrite |
31
+ | **Template-only** | `.forge/templates/**`, `.forge/migrations/**`, `.forge/gitignore` → `.forge/.gitignore` | Overwrite |
28
32
 
29
33
  **Never touch** user-generated files: `.forge/project.yml`, `.forge/state/`, `.forge/constitution.md`, `.forge/context.md`, `.forge/requirements/`, `.forge/roadmap.yml`, `.forge/design-system.md`, `.forge/refactor-backlog.yml`.
30
34
 
35
+ **Preserve experimental skills.** Opt-in skills installed separately (e.g. `.claude/skills/orchestrating/` from `experimental/m10/`) are not in the base template. Never flag them as "removed from template" or delete them — leave them untouched.
36
+
31
37
  ## Step 3: Sync Framework-Owned Files
32
38
 
33
39
  For each framework-owned file in the source template:
@@ -42,6 +48,8 @@ For each framework-owned file in the source template:
42
48
 
43
49
  Same process as Step 3 for `.forge/templates/**` and `.forge/migrations/**`. (Matches the npm bin's template-only dirs — keeps migration guides installed so the Step 7 pointers resolve via the in-Claude sync route, not just `npx forge-orkes upgrade`.)
44
50
 
51
+ **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
+
45
53
  ## Step 5: Handle Merge-Owned Files
46
54
 
47
55
  **`CLAUDE.md`:**
@@ -145,6 +153,35 @@ Add the layers field now? (yes/no/show guide)
145
153
 
146
154
  `upgrading` never edits `.forge/project.yml` directly — this is the only path that adds the field, via `quick-tasking`.
147
155
 
156
+ ### Pre-0.19.0 legacy `state/index.yml`
157
+
158
+ Run from project root:
159
+
160
+ ```bash
161
+ grep -qE '^[[:space:]]*(desire_paths|metrics|current_status):' .forge/state/index.yml && echo "legacy index"
162
+ wc -c .forge/state/index.yml # slim registry is a few hundred bytes; KBs = narrative drifted in
163
+ ```
164
+
165
+ If `state/index.yml` carries `desire_paths:`/`metrics:`/embedded narrative or is large, surface:
166
+
167
+ ```
168
+ Worktree-safe state: state/index.yml predates the derived registry (Forge 0.19.0)
169
+ ─────────────────────────────────────────────────────────────────────────────────
170
+ Forge 0.19.0 makes index.yml a derived registry (regenerated by rollup), moves
171
+ desire_paths to append-only files, and commits .forge/ at every phase handoff.
172
+ A legacy index.yml conflicts across git worktrees.
173
+
174
+ A migration guide is available at: .forge/migrations/0.19.0-worktree-safe-state.md
175
+
176
+ Migrate now? (yes/no/show guide)
177
+ ```
178
+
179
+ - **yes** → invoke `quick-tasking` skill, hand it the migration guide as the task definition (secure state, slim index.yml, split desire_paths into files, drop metrics, regenerate via rollup). This edits user-owned state — confirm each lossy step (the narrative extraction) with the user.
180
+ - **show guide** → read and display the file, then re-ask
181
+ - **no** → note in upgrade report. Skills still run against a legacy index.yml; it just won't gain worktree conflict-safety until migrated.
182
+
183
+ `upgrading` never edits `.forge/state/` directly — migration runs via `quick-tasking`.
184
+
148
185
  ### Future migrations
149
186
 
150
187
  Add new detection blocks here for each Forge version that changes file layout. Pattern:
@@ -246,7 +246,7 @@ After gap closure:
246
246
 
247
247
  ## Desire Paths Retrospective
248
248
 
249
- After verification completes (PASSED or GAPS FOUND), run a quick retrospective on framework usage patterns. Update `.forge/state/index.yml desire_paths` (global, not per-milestone).
249
+ After verification completes (PASSED or GAPS FOUND), run a quick retrospective on framework usage patterns. Record each signal as **one new file** under `.forge/state/desire-paths/` (copy `.forge/templates/state/desire-path.yml`, name `{YYYY-MM-DD}-{type}-{milestone}-{slug}.yml`). Never write `index.yml`; recurrence is derived by globbing.
250
250
 
251
251
  ### Collect Signals
252
252
 
@@ -259,7 +259,7 @@ After verification completes (PASSED or GAPS FOUND), run a quick retrospective o
259
259
 
260
260
  **3. Skipped steps**: User ask to skip workflow steps? Repeated skips = friction without value.
261
261
 
262
- **4. Recurring friction**: Same problem from previous sessions? Check prior `desire_paths`. Increment counts.
262
+ **4. Recurring friction**: Same problem from previous sessions? Glob `.forge/state/desire-paths/` for matching `type` + `note`; if it recurs, add another observation file (count is the file count — do not edit existing files).
263
263
 
264
264
  **5. Agent struggles**: Agent need multiple attempts or human intervention? Log task type and failure pattern.
265
265
 
@@ -289,8 +289,9 @@ Only suggest at 3+ occurrences. One-off issues are noise.
289
289
 
290
290
  After PASSED verdict:
291
291
 
292
- 1. **Persist** — Confirm verification results documented, desire paths logged to `.forge/state/index.yml`
292
+ 1. **Persist** — Confirm verification report documented, desire-path files written to `.forge/state/desire-paths/`
293
293
  2. **Update state** — Set `current.status` to `reviewing` in `.forge/state/milestone-{id}.yml`
294
- 3. **Recommend clear:** *"State written. `/clear` then `/forge` for reviewing."*
294
+ 3. **State-sync commit** (State Commit Protocol): `git add .forge/` then `git commit -m "chore(forge): sync state after verifying — m{N} {phase-name}"` (scoped; never `git add .`).
295
+ 4. **Recommend clear:** *"State synced. `/clear` then `/forge` for reviewing."*
295
296
 
296
297
  If GAPS found, route back to planning in gap-closure mode. Context clear applies after re-verified PASSED verdict.
@@ -0,0 +1,30 @@
1
+ # Forge state — durable vs ephemeral.
2
+ #
3
+ # Everything under .forge/ IS durable and committed to version control, so
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
6
+ # requirements/ phases/ research/ decisions/ contracts/ audits/
7
+ # refactor-backlog.yml state/ templates/ migrations/
8
+ #
9
+ # The entries below are the ONLY ephemeral exceptions — session- or
10
+ # machine-local artifacts that must never be committed.
11
+
12
+ # Active-skill marker (session-specific, created by hooks)
13
+ .active-skill
14
+
15
+ # Upgrade artifacts (generated by npx forge-orkes upgrade)
16
+ upgrade/
17
+
18
+ # Local dev source path (user-specific)
19
+ dev-source
20
+
21
+ # OS cruft
22
+ .DS_Store
23
+
24
+ # M10 orchestrator runtime (per-machine; not part of repo)
25
+ .mcp-server/claims.db
26
+ .mcp-server/*.pid
27
+ .mcp-server/node_modules/
28
+
29
+ # Orchestration locks
30
+ *.lock
@@ -0,0 +1,110 @@
1
+ # Migration Guide: Worktree-Safe Durable State (Forge 0.19.0)
2
+
3
+ Applies to projects initialized before 0.19.0 — especially any project run across multiple git worktrees.
4
+
5
+ ## Why
6
+
7
+ Two problems this release fixes:
8
+
9
+ 1. **State drifted out of git.** Skills wrote `.forge/` state to disk but never committed it on a cadence, so milestone progress could live only on your local machine — lost on hardware failure, unrecoverable from a fresh clone.
10
+ 2. **`index.yml` conflicted across worktrees.** The global `state/index.yml` was hand-edited on every cursor advance and accumulated per-milestone narrative + shared `desire_paths` lists, so parallel worktrees collided on merge.
11
+
12
+ 0.19.0 makes `index.yml` a **derived registry** (regenerated by rollup from the per-milestone files), moves desire-paths to **append-only files**, and adds a **State Commit Protocol** (every phase handoff commits `.forge/`).
13
+
14
+ ## Prerequisites
15
+
16
+ - Forge 0.19.0 installed (`npx forge-orkes upgrade`).
17
+ - A clean-ish working tree (commit or stash unrelated changes first).
18
+ - This migration edits **user-owned state** (`index.yml`, `desire_paths`), so it is **guided / human-confirmed** — never run blind. Narrative extraction in step 2 is a judgment call.
19
+
20
+ ## Detection
21
+
22
+ ```bash
23
+ # Bloated/legacy index.yml — large, or still carrying the metrics/desire-paths/narrative blocks
24
+ wc -c .forge/state/index.yml # » a few hundred bytes once migrated; KBs means legacy
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
27
+ ```
28
+
29
+ The installer also prints a notice after `upgrade` when it detects a legacy `index.yml`.
30
+
31
+ ## Migration steps
32
+
33
+ ### 1. Secure current state first
34
+
35
+ Before changing anything, get today's state into git so nothing is lost mid-migration:
36
+
37
+ ```bash
38
+ git add .forge/
39
+ git commit -m "chore(forge): secure .forge state before 0.19.0 migration"
40
+ ```
41
+
42
+ ### 2. Slim `index.yml` to the derived registry
43
+
44
+ Move any per-milestone narrative that drifted into `index.yml` (e.g. paragraph-long `current_status:` values) **out** to the relevant `state/milestone-{id}.yml` prose fields or the milestone's audit report. Reduce each `milestones:` entry to exactly:
45
+
46
+ ```yaml
47
+ milestones:
48
+ - id: 1
49
+ name: "…"
50
+ status: complete # not_started | active | deferred | complete
51
+ last_updated: "…"
52
+ ```
53
+
54
+ This is the lossy/judgment step — do it with the agent and confirm each move. The header in `.forge/templates/state/index.yml` shows the target shape.
55
+
56
+ ### 3. Split `desire_paths:` into append-only files
57
+
58
+ For each entry under the old `desire_paths:` lists, create one file under `.forge/state/desire-paths/` from `.forge/templates/state/desire-path.yml`:
59
+
60
+ ```
61
+ .forge/state/desire-paths/{YYYY-MM-DD}-{type}-{milestone}-{slug}.yml
62
+ ```
63
+
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`.
71
+
72
+ ### 4. Drop `metrics:`
73
+
74
+ Delete the `metrics:` block from `index.yml` (it had no writers). If you ever want the numbers, derive them from `git log`.
75
+
76
+ ### 5. Regenerate and commit
77
+
78
+ Run `/forge` once — its rollup step regenerates `index.yml` deterministically from the milestone files. Confirm it matches your slimmed version, then commit:
79
+
80
+ ```bash
81
+ git add .forge/
82
+ git commit -m "chore(forge): migrate to worktree-safe state (0.19.0)"
83
+ ```
84
+
85
+ ## Post-migration verification
86
+
87
+ ```bash
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"
94
+
95
+ # rollup is idempotent — running /forge twice produces no diff to index.yml
96
+ git diff --quiet .forge/state/index.yml && echo "OK: stable" || echo "rollup changed index — commit it"
97
+
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
100
+ ```
101
+
102
+ ## What changes downstream
103
+
104
+ - Every phase handoff now emits a `chore(forge): sync state …` commit — your history gains small, regular state commits separate from code commits.
105
+ - Worktree/parallel agents write only their own `milestone-{id}.yml` (+ append desire-path files); `index.yml` is regenerated by the orchestrator/`forge` rollup, so it no longer conflicts.
106
+ - One agent per milestone — same-milestone parallel work is out of scope (the M10 claim layer guards it).
107
+
108
+ ## Rollback
109
+
110
+ This migration only restructures state files; it does not touch code. To roll back, `git revert` the two migration commits (steps 1 and 5) — your pre-migration `index.yml` returns intact. The 0.19.0 skills will still run against a legacy `index.yml` (they just won't get the conflict-safety benefit until migrated).
@@ -0,0 +1,31 @@
1
+ # Forge Desire-Path Observation — APPEND-ONLY
2
+ #
3
+ # One observation = one new file under state/desire-paths/. NEVER edit an
4
+ # existing observation in place and NEVER keep a mutable occurrence counter —
5
+ # that is what used to make state/index.yml conflict across worktrees.
6
+ #
7
+ # Filename: state/desire-paths/{YYYY-MM-DD}-{type}-{milestone}-{slug}.yml
8
+ # e.g. state/desire-paths/2026-06-10-recurring_friction-m7-uncommitted-state.yml
9
+ # The date+type+milestone+slug make the name unique, so concurrent agents in
10
+ # different worktrees only ever ADD files — git never has to merge content.
11
+ #
12
+ # Occurrence counts are DERIVED: the `forge` skill and the `verifying`
13
+ # retrospective glob this directory and group by (type + normalized note).
14
+ # A pattern appearing in 3+ files is a candidate for framework evolution.
15
+
16
+ type: "" # deviation_pattern | tier_override | skipped_step |
17
+ # recurring_friction | agent_struggle | user_correction
18
+ milestone: "" # e.g. "m7" (or "global" if not milestone-scoped)
19
+ skill: "" # skill that observed it (executing, planning, verifying, ...)
20
+ first_seen: null # ISO 8601 date this observation was recorded
21
+ related_files: [] # source files involved, if any
22
+ note: "" # one-line description — the grouping key for occurrence counts
23
+
24
+ # Optional type-specific detail (fill what applies to `type`):
25
+ detail:
26
+ detected: "" # tier_override: what Forge detected
27
+ overridden_to: "" # tier_override: what the user chose
28
+ rule: null # deviation_pattern: 1 | 2 | 3
29
+ correction: "" # user_correction: the repeated correction
30
+ failure_pattern: "" # agent_struggle: how the task fails
31
+ step: "" # skipped_step: the step skipped
@@ -1,51 +1,25 @@
1
- # Forge Global State — Cross-Milestone Index
2
- # Auto-managed by agents. Do not edit manually unless recovering from errors.
3
- # This file tracks global concerns. Per-milestone state lives in state/milestone-{id}.yml.
4
-
5
- milestones: # Active milestones and their high-level status
1
+ # Forge Global State — Cross-Milestone Index (DERIVED)
2
+ #
3
+ # This file is REGENERATED by the `forge` skill's rollup step from
4
+ # state/milestone-*.yml. Do NOT hand-edit it, and do NOT add per-milestone
5
+ # cursor data or narrative here — that lives in state/milestone-{id}.yml.
6
+ #
7
+ # Worktree / parallel agent sessions MUST NOT write this file. Editing only the
8
+ # per-milestone files (different files = no git conflict) and regenerating this
9
+ # registry from them is what makes Forge state safe across many worktrees.
10
+ #
11
+ # Rollup is deterministic + idempotent: regenerating from the same milestone
12
+ # files yields identical bytes, so concurrent regenerations never conflict.
13
+
14
+ milestones: # Registry rolled up from state/milestone-{id}.yml
6
15
  - id: 1
7
- name: "" # Human-readable milestone name from roadmap
8
- status: not_started # not_started | active | deferred | complete
9
- last_updated: null # ISO 8601 timestamp — used for resume default selection
10
-
11
- metrics:
12
- total_commits: 0
13
- total_files_modified: 0
14
- verification_passes: 0
15
- verification_failures: 0
16
- checkpoints_hit: 0
17
-
18
- # Desire Paths — Patterns in how the framework is actually used
19
- # Collected automatically by agents. Reviewed during verification retrospective.
20
- # When a pattern appears 3+ times, it becomes a candidate for framework evolution.
21
- # Desire paths are GLOBAL — they track framework usage across all milestones.
22
- desire_paths:
23
- deviation_patterns: [] # Repeated Rule 1/2/3 deviations (same type, same area)
24
- # - pattern: "" # e.g., "Rule 2: missing null checks in API handlers"
25
- # occurrences: 0
26
- # first_seen: null
27
- # last_seen: null
28
-
29
- tier_overrides: [] # User overriding auto-detected tier
30
- # - detected: "" # What Forge detected
31
- # overridden_to: "" # What user chose instead
32
- # reason: "" # Why (if stated)
33
-
34
- skipped_steps: [] # Steps users consistently skip or rush through
35
- # - step: "" # e.g., "constitutional gate check"
36
- # skill: ""
37
- # times_skipped: 0
38
-
39
- recurring_friction: [] # Same problem appearing across sessions
40
- # - description: "" # e.g., "Design system violations in form components"
41
- # occurrences: 0
42
- # related_files: []
43
-
44
- agent_struggles: [] # Tasks where agents consistently fail or need retries
45
- # - task_type: "" # e.g., "Responsive layout implementation"
46
- # failure_pattern: ""
47
- # occurrences: 0
48
-
49
- user_corrections: [] # User correcting agent output in the same way repeatedly
50
- # - correction: "" # e.g., "Always adds 'use client' directive"
51
- # occurrences: 0
16
+ name: "" # mirrors milestone-{id}.yml milestone.name
17
+ status: not_started # mirrors current.status: not_started | active | deferred | complete
18
+ last_updated: null # mirrors current.last_updated — used for resume default selection
19
+
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.
@@ -1,7 +1,12 @@
1
1
  # Forge Milestone State — Per-Milestone Cursor
2
2
  # Auto-managed by agents. Do not edit manually unless recovering from errors.
3
3
  # Copy to .forge/state/milestone-{id}.yml for each active milestone.
4
- # This file tracks one milestone's position. Global state lives in state/index.yml.
4
+ #
5
+ # SINGLE SOURCE OF TRUTH for this milestone. Exactly one agent/session owns this
6
+ # file at a time (enforced by the M10 claim layer). state/index.yml is DERIVED
7
+ # FROM this file by the `forge` rollup step — never the other way around.
8
+ # Because each milestone has its own file, agents on different milestones never
9
+ # touch the same file, so parallel worktrees produce no git conflicts here.
5
10
 
6
11
  milestone:
7
12
  id: null # Milestone ID from roadmap
@@ -16,6 +21,7 @@ current:
16
21
  task: null # Current task number within plan
17
22
  status: not_started # not_started | researching | discussing | planning | executing | verifying | reviewing | complete
18
23
  completed_at: null # ISO 8601 timestamp — set when status transitions to complete
24
+ last_updated: null # ISO 8601 — set at each transition; rolled up into index.yml registry for resume ordering
19
25
 
20
26
  # NO stored progress block. Progress percent is DERIVED on read, never stored.
21
27
  # The forge skill computes it from genuinely-maintained signals:
@@ -130,8 +130,9 @@ State lives in `.forge/`:
130
130
  - `design-system.md` — Component mapping table
131
131
  - `requirements/m{N}.yml` — Per-milestone structured requirements with `[NEEDS CLARIFICATION]` markers. **FR-IDs, DEF-IDs, and NFR-IDs are globally unique across all milestone files** — `FR-001` may exist in exactly one `m{N}.yml`. Before adding a new ID, scan `.forge/requirements/*.yml` for the highest in-use number and continue the sequence. On collision (e.g. during a migration), keep the older milestone's ID and renumber the newer. Concurrent milestones each own their file — no cross-stream contention on file writes, but ID space is shared. Functional requirements may carry M9 e2e gate fields (`e2e`, `observable_outcome`, `observable_outcome_hash`, `validated`) — lazy migration, absent fields default to `e2e:false`/`validated:false`.
132
132
  - `roadmap.yml` — Phases, milestones, dependencies
133
- - `state/index.yml` — Global: active milestones, desire_paths, metrics
134
- - `state/milestone-{id}.yml` — Per-milestone cursor: position, progress, decisions, blockers
133
+ - `state/index.yml` — DERIVED registry rolled up from milestone files (id, name, status, last_updated). Never hand-edited; never written by worktree agents.
134
+ - `state/milestone-{id}.yml` — Per-milestone cursor (single source of truth): position, progress, decisions, blockers. One owner at a time.
135
+ - `state/desire-paths/` — Append-only framework-usage observations, one file per observation. Occurrence counts derived by globbing (no mutable counter).
135
136
  - `context.md` — Locked decisions + deferred ideas (discuss phase)
136
137
  - `research/milestone-{id}.md` — Research findings snapshot (dated, immutable)
137
138
  - `phases/m{M}-{N}-{name}/plan-{NN}.md` — Task plans with must_haves frontmatter
@@ -143,6 +144,24 @@ State lives in `.forge/`:
143
144
  **Format**: YAML for machine state, Markdown for human content.
144
145
  **`current.status` is authoritative.** Complete only at `current.status == complete`. 100% tasks ≠ done — still needs verifying + reviewing.
145
146
 
147
+ ### State Commit Protocol
148
+
149
+ `.forge/` durable state is **committed**, not just written to disk — so project state survives machine loss and is pullable from any clone. At every phase **Handoff**, after writing state, the completing skill runs one scoped state-sync commit:
150
+
151
+ ```
152
+ git add .forge/ # scoped — respects .gitignore; never `git add .`
153
+ git commit -m "chore(forge): sync state after {phase} — m{N} {phase-name}"
154
+ ```
155
+
156
+ State-sync commits are separate from per-task code commits (which stay atomic during executing).
157
+
158
+ ### State Ownership (multi-worktree safety)
159
+
160
+ - `state/milestone-{id}.yml` is the **single source of truth**; exactly one agent owns it at a time.
161
+ - Worktree / parallel agents write **only** their own milestone file and **append** desire-path files. They **never** write `index.yml`.
162
+ - `index.yml` is **regenerated by rollup** (read every `milestone-*.yml` → rewrite the registry) by the main/orchestrator session — on `forge` resume and at `orchestrating` teardown. The rollup is deterministic + idempotent, so it **is** the reconcile step — never a hand-merge.
163
+ - Same-milestone parallel work is out of scope, guarded by the M10 claim layer.
164
+
146
165
  ## Deviation Rules
147
166
 
148
167
  **Full definitions:** `.claude/agents/executor.md`.
@@ -187,4 +206,5 @@ With Beads installed: `bd prime` (session context), `bd ready` (unblocked tasks)
187
206
  One commit per task. Format: `{type}({scope}): {description}`
188
207
  Types: `feat`, `fix`, `test`, `refactor`, `chore`, `docs`
189
208
  Never `git add .` or `git add -A` — stage individually.
209
+ Phase handoffs additionally emit one scoped `chore(forge): sync state …` commit (see State Commit Protocol) so `.forge/` state never drifts out of git.
190
210
  <!-- forge:end -->