forge-orkes 0.39.0 → 0.42.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.
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const fs = require('fs');
4
+ const os = require('os');
4
5
  const path = require('path');
5
6
  const readline = require('readline');
6
7
  const { execSync } = require('child_process');
@@ -68,7 +69,7 @@ function compareVersions(a, b) {
68
69
  }
69
70
 
70
71
  // Template-only: reference templates Forge controls
71
- const TEMPLATE_ONLY_DIRS = ['.forge/templates', '.forge/migrations'];
72
+ const TEMPLATE_ONLY_DIRS = ['.forge/templates', '.forge/migrations', '.forge/adapters'];
72
73
 
73
74
  // Settings file gets smart-merge (overwrite forge.* keys, preserve user hooks)
74
75
  const SETTINGS_FILE = '.claude/settings.json';
@@ -155,7 +156,7 @@ function upgradeDir(relDir, { autoClean = false } = {}) {
155
156
  const srcDir = path.join(templateDir, relDir);
156
157
  const destDir = path.join(targetDir, relDir);
157
158
 
158
- const result = { updated: [], added: [], unchanged: [], removed: [], preserved: [] };
159
+ const result = { updated: [], added: [], unchanged: [], removed: [], preserved: [], removeCandidates: [] };
159
160
 
160
161
  if (!fs.existsSync(srcDir)) return result;
161
162
 
@@ -195,15 +196,13 @@ function upgradeDir(relDir, { autoClean = false } = {}) {
195
196
  }
196
197
  const destPath = path.join(destDir, rel);
197
198
  if (autoClean) {
198
- fs.unlinkSync(destPath);
199
- // Remove empty parent dirs up to relDir
200
- let dir = path.dirname(destPath);
201
- while (dir !== destDir && fs.existsSync(dir) && fs.readdirSync(dir).length === 0) {
202
- fs.rmdirSync(dir);
203
- dir = path.dirname(dir);
204
- }
199
+ // Defer deletion — collect the candidate; upgrade() confirms before
200
+ // removing (R066a). Empty-parent cleanup needs destDir, carried along.
201
+ result.removeCandidates.push({ destPath, destDir, displayPath });
202
+ } else {
203
+ // Template-only dirs: report-only, no deletion here (existing behavior).
204
+ result.removed.push(displayPath);
205
205
  }
206
- result.removed.push(path.join(relDir, rel));
207
206
  }
208
207
  }
209
208
 
@@ -357,6 +356,11 @@ async function install() {
357
356
  }
358
357
 
359
358
  console.log(`\n Forge v${pkgVersion} is ready. Start with: /forge\n`);
359
+
360
+ // Presence-gated: a fresh install normally has no Codex adapter, so this is a
361
+ // no-op — but if the target repo already carries a .agents/ / .codex/ tree
362
+ // (e.g. migrating in from another setup), note it may need regeneration.
363
+ noticeCodexAdapter();
360
364
  }
361
365
 
362
366
  /**
@@ -439,14 +443,19 @@ function runPostUpgradeMigrationChecks(installedVersion) {
439
443
 
440
444
  let stdout = '';
441
445
  try {
446
+ // Trust model: guides were re-synced from the (trusted) npm package in
447
+ // step 2 above before this runs, so the on-disk content is not
448
+ // attacker-controlled here; the only residual risk is a runaway/buggy
449
+ // detection block, bounded by `timeout`.
442
450
  stdout = execSync(script, {
443
451
  cwd: targetDir,
444
452
  shell: '/bin/bash',
445
453
  encoding: 'utf-8',
446
454
  stdio: ['ignore', 'pipe', 'ignore'],
455
+ timeout: 10000, // 10s — a pathological guide throws (caught below) → no-op, loop continues
447
456
  });
448
457
  } catch {
449
- // A non-zero exit or detection error is a no-op, not a failure — keep going.
458
+ // A non-zero exit, timeout, or detection error is a no-op, not a failure — keep going.
450
459
  continue;
451
460
  }
452
461
 
@@ -473,6 +482,26 @@ function runPostUpgradeMigrationChecks(installedVersion) {
473
482
  }
474
483
  }
475
484
 
485
+ /**
486
+ * Sync a single standalone framework-owned file, overwriting when content
487
+ * differs. Records the outcome in `results` (added/updated/unchanged). Used for
488
+ * framework files that live outside the recursively-synced dirs (.forge/.gitignore,
489
+ * .forge/FORGE.md).
490
+ */
491
+ function syncFrameworkFile(srcPath, destPath, displayName, results) {
492
+ if (!fs.existsSync(srcPath)) return;
493
+ const newContent = fs.readFileSync(srcPath, 'utf-8');
494
+ const existed = fs.existsSync(destPath);
495
+ const oldContent = existed ? fs.readFileSync(destPath, 'utf-8') : null;
496
+ if (oldContent !== newContent) {
497
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
498
+ fs.writeFileSync(destPath, newContent);
499
+ results[existed ? 'updated' : 'added'].push(displayName);
500
+ } else {
501
+ results.unchanged.push(displayName);
502
+ }
503
+ }
504
+
476
505
  async function upgrade() {
477
506
  console.log('\n Forge Upgrade\n');
478
507
 
@@ -521,7 +550,9 @@ async function upgrade() {
521
550
  preserved: [],
522
551
  };
523
552
 
524
- // 1. Process framework-owned directories (auto-clean stale files)
553
+ // 1. Process framework-owned directories (stale files are collected for a
554
+ // confirmed removal pass below, not deleted inline — R066a).
555
+ const pendingRemovals = [];
525
556
  for (const dir of FRAMEWORK_OWNED_DIRS) {
526
557
  const dirResult = upgradeDir(dir, { autoClean: true });
527
558
  results.updated.push(...dirResult.updated);
@@ -529,6 +560,7 @@ async function upgrade() {
529
560
  results.unchanged.push(...dirResult.unchanged);
530
561
  results.removed.push(...dirResult.removed);
531
562
  results.preserved.push(...dirResult.preserved);
563
+ pendingRemovals.push(...dirResult.removeCandidates);
532
564
  }
533
565
 
534
566
  // 2. Process template-only directories
@@ -541,41 +573,59 @@ async function upgrade() {
541
573
  results.preserved.push(...dirResult.preserved);
542
574
  }
543
575
 
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');
576
+ // 2a. Confirm stale framework-file removals before deleting (R066a). Defaults
577
+ // to yes (cleanup is the normal case); auto-confirms when non-interactive
578
+ // (CI / piped stdin) or with --force/--yes, preserving prior behavior.
579
+ if (pendingRemovals.length > 0) {
580
+ const auto =
581
+ process.argv.includes('--force') ||
582
+ process.argv.includes('--yes') ||
583
+ !process.stdin.isTTY;
584
+ console.log(` ${pendingRemovals.length} stale framework file(s) no longer in the template:`);
585
+ for (const c of pendingRemovals) console.log(` ${c.displayPath}`);
586
+ let go = auto;
587
+ if (!auto) {
588
+ const ans = await prompt(' Remove these stale framework files? [Y/n] ');
589
+ go = ans === '' || ans === 'y' || ans === 'yes';
558
590
  }
559
- }
560
-
561
- // 2c. Sync the framework prose file .forge/FORGE.md (framework-owned single file,
562
- // same overwrite-when-different pattern as 2b). MUST run before the CLAUDE.md pass
563
- // so a migrated project's import line never points at a missing file.
564
- const fmSrc = path.join(templateDir, '.forge', 'FORGE.md');
565
- const fmDest = path.join(targetDir, '.forge', 'FORGE.md');
566
- if (fs.existsSync(fmSrc)) {
567
- const newContent = fs.readFileSync(fmSrc, 'utf-8');
568
- const existed = fs.existsSync(fmDest);
569
- const oldContent = existed ? fs.readFileSync(fmDest, 'utf-8') : null;
570
- if (oldContent !== newContent) {
571
- fs.mkdirSync(path.dirname(fmDest), { recursive: true });
572
- fs.writeFileSync(fmDest, newContent);
573
- results[existed ? 'updated' : 'added'].push('.forge/FORGE.md');
591
+ if (go) {
592
+ for (const c of pendingRemovals) {
593
+ if (fs.existsSync(c.destPath)) fs.unlinkSync(c.destPath);
594
+ // Remove now-empty parent dirs up to the synced root.
595
+ let dir = path.dirname(c.destPath);
596
+ while (dir !== c.destDir && fs.existsSync(dir) && fs.readdirSync(dir).length === 0) {
597
+ fs.rmdirSync(dir);
598
+ dir = path.dirname(dir);
599
+ }
600
+ results.removed.push(c.displayPath);
601
+ }
574
602
  } else {
575
- results.unchanged.push('.forge/FORGE.md');
603
+ console.log(' Keeping stale files (skipped removal).');
604
+ for (const c of pendingRemovals) {
605
+ results.preserved.push(`${c.displayPath} (stale — kept by choice)`);
606
+ }
576
607
  }
608
+ console.log();
577
609
  }
578
610
 
611
+ // 2b. Sync the forge .gitignore. Shipped as `gitignore` (npm strips dotfiles
612
+ // named .gitignore); materialize/refresh it as `.forge/.gitignore`.
613
+ syncFrameworkFile(
614
+ path.join(templateDir, '.forge', 'gitignore'),
615
+ path.join(targetDir, '.forge', '.gitignore'),
616
+ '.forge/.gitignore',
617
+ results
618
+ );
619
+
620
+ // 2c. Sync the framework prose file .forge/FORGE.md. MUST run before the
621
+ // CLAUDE.md pass so a migrated project's import line never points at a missing file.
622
+ syncFrameworkFile(
623
+ path.join(templateDir, '.forge', 'FORGE.md'),
624
+ path.join(targetDir, '.forge', 'FORGE.md'),
625
+ '.forge/FORGE.md',
626
+ results
627
+ );
628
+
579
629
  // 3. Guarantee the CLAUDE.md import line; auto-migrate any legacy embedded section.
580
630
  const claudeStatus = ensureClaudeMdImport();
581
631
  if (claudeStatus === 'migrated') {
@@ -652,10 +702,90 @@ async function upgrade() {
652
702
  // Pass the version captured BEFORE upgradeSettings() stamped the new one, so the
653
703
  // migration range is (installed, source] against the freshly-synced guides.
654
704
  runPostUpgradeMigrationChecks(installedVersion);
705
+
706
+ // Presence-gated advisories (no generation, no rebase — notices only).
707
+ noticeCodexAdapter();
708
+ noticeWorktreeLag();
709
+ }
710
+
711
+ /**
712
+ * Presence-gated Codex-adapter staleness notice. The bin has no LLM, so it can
713
+ * only NOTICE that a Codex adapter (.agents/ / .codex/) may be stale after a
714
+ * .claude/ sync and point to the regeneration route (/upgrading Step 4b). It
715
+ * never generates, never writes .agents/, never adds .agents to
716
+ * FRAMEWORK_OWNED_DIRS. Silent when no Codex adapter is present.
717
+ */
718
+ function noticeCodexAdapter() {
719
+ const hasCodex =
720
+ fs.existsSync(path.join(targetDir, '.agents')) ||
721
+ fs.existsSync(path.join(targetDir, '.codex'));
722
+ if (!hasCodex) return;
723
+ console.log(' Codex adapter detected (.agents/ / .codex/).');
724
+ console.log(' It may now be stale relative to the .claude/ source just synced.');
725
+ console.log(' This installer does not regenerate it (no LLM). To refresh it,');
726
+ console.log(' run the `/upgrading` skill — its Codex step offers regeneration');
727
+ console.log(' from .forge/adapters/codex-generation.md.\n');
728
+ }
729
+
730
+ /**
731
+ * Presence-gated worktree-lag notice (paired with /upgrading Step 8). An upgrade
732
+ * run touches only this checkout; live forge/m-* worktrees keep their
733
+ * branch-point framework files until rebased. The bin only NOTICES — it never
734
+ * rebases (a rebase can conflict). Silent when there are no live forge worktrees
735
+ * or this is not a git repo.
736
+ */
737
+ function noticeWorktreeLag() {
738
+ let out;
739
+ try {
740
+ out = execSync('git worktree list --porcelain', {
741
+ cwd: targetDir,
742
+ stdio: ['ignore', 'pipe', 'ignore'],
743
+ }).toString();
744
+ } catch {
745
+ return; // not a git repo / git unavailable
746
+ }
747
+ const lagging = [];
748
+ let curPath = null;
749
+ for (const line of out.split('\n')) {
750
+ if (line.startsWith('worktree ')) curPath = line.slice('worktree '.length).trim();
751
+ else if (line.startsWith('branch ')) {
752
+ const br = line.slice('branch '.length).trim();
753
+ if (/^refs\/heads\/forge\/m-/.test(br) && curPath) {
754
+ lagging.push({ path: curPath, branch: br.replace('refs/heads/', '') });
755
+ }
756
+ }
757
+ }
758
+ if (!lagging.length) return;
759
+ console.log(` ${lagging.length} live Forge worktree(s) lag this upgrade:`);
760
+ for (const w of lagging) {
761
+ console.log(` ${w.path} (${w.branch})`);
762
+ }
763
+ console.log(' They keep their branch-point framework files until rebased.');
764
+ console.log(' Pick up this upgrade in each: git -C <path> rebase main');
765
+ console.log(' (Gitignored carve-outs like .mcp.json / experimental hooks do not');
766
+ console.log(' rebase — re-run the experimental installer if the upgrade changed them.)\n');
655
767
  }
656
768
 
657
769
  // --- Entry point ---
658
770
 
771
+ // cwd sanity (R066c): Forge writes .claude/, .forge/, and CLAUDE.md into the
772
+ // current directory. Refuse the clearly-wrong locations (home dir, filesystem
773
+ // root) unless --force, so an accidental run from the wrong place can't litter
774
+ // framework files across $HOME or /.
775
+ function assertSaneTargetDir() {
776
+ if (process.argv.includes('--force')) return;
777
+ const root = path.parse(targetDir).root;
778
+ if (targetDir === root || targetDir === os.homedir()) {
779
+ const where = targetDir === os.homedir() ? 'your home directory' : 'the filesystem root';
780
+ console.error(` ✖ Refusing to run in ${where} (${targetDir}).`);
781
+ console.error(' Forge writes .claude/, .forge/, and CLAUDE.md into the current directory —');
782
+ console.error(' run it from a project root. To override: forge-orkes --force\n');
783
+ process.exit(1);
784
+ }
785
+ }
786
+
787
+ assertSaneTargetDir();
788
+
659
789
  const subcommand = process.argv[2];
660
790
 
661
791
  if (subcommand === 'upgrade') {
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "forge-orkes",
3
- "version": "0.39.0",
3
+ "version": "0.42.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"
7
7
  },
8
+ "scripts": {
9
+ "release": "node scripts/release.js",
10
+ "release:audit": "node scripts/release.js audit"
11
+ },
8
12
  "files": [
9
13
  "bin/",
10
14
  "template/"
@@ -66,7 +66,7 @@ suite_audit:
66
66
  e2e: N
67
67
  runners: ["playwright", "vitest"]
68
68
  findings:
69
- - category: flake | coverage_gap | anti_pattern | quarantine_candidate | ci_gap
69
+ - category: flake | coverage-gap | anti-pattern | quarantine-candidate | ci-gap
70
70
  file: "e2e/login.spec.ts"
71
71
  lines: "42-58"
72
72
  severity: critical | warning | info
@@ -141,12 +141,10 @@ Baked rules. Author mode follows all. Violations = rework.
141
141
  | Anti-Pattern | Mode | Why it's wrong |
142
142
  |--------------|------|----------------|
143
143
  | Writing source during audit | analyst | Read-only gate; fixes go through backlog + quick-tasking |
144
- | `sleep(1000)` or hardcoded waits | author | Masks race, creates flake |
145
144
  | Rubber-stamping suite as healthy | analyst | Defeats audit; real suites have findings |
146
145
  | Inventing YAML fields | analyst | Breaks testing skill aggregation |
147
146
  | Severity inflation / backlog spam | analyst | Degrades signal; reviewer bar applies |
148
147
  | Auto-fixing analyst findings | analyst | Violates deferred decision: no auto-queue analyst → author |
149
- | Shared page across tests | author | Leaks state; breaks test isolation |
150
- | CSS-selector locators over data-testid | author | Fragile; breaks on style refactor |
151
- | "Retry until pass" in CI | author | Hides flake; prefer quarantine |
152
148
  | Self-switching modes mid-run | either | Mode is set at spawn; exit cleanly and let skill re-spawn |
149
+
150
+ **Author-mode flake anti-patterns** (`sleep`/hardcoded waits, shared `page` across tests, CSS selectors over `data-testid`, "retry until pass") are the **Flake-Resistant Rules** above — violating any numbered rule is an anti-pattern. Not repeated here.
@@ -28,6 +28,8 @@ When an architectural decision conflicts with vertical slicing (e.g., a framewor
28
28
 
29
29
  ## Architecture Decision Record (ADR)
30
30
 
31
+ **Reserve the ADR number first (cross-worktree safety).** Before authoring a new ADR, allocate its number via the **ID Reservation Protocol** (FORGE.md): the next number is `max(highest `kind: adr` in `.forge/reservations.yml`, highest `ADR-NNN` in `.forge/decisions/`) + 1; append one `{kind: adr, id, milestone, reserved_at, summary}` entry to `.forge/reservations.yml`, **commit + push it**, *then* write the ADR file. Bare "scan `decisions/` for the highest + increment" only sees committed state, so two worktrees architecting in parallel both pick the same `ADR-NNN` and collide silently until merge (a multi-file renumber). See FORGE.md → **ID Reservation Protocol** for the full rule. No `reservations.yml` yet ⇒ scan as before and the first reservation creates it (lazy migration).
32
+
31
33
  Record every significant decision in `.forge/decisions/`:
32
34
 
33
35
  ```markdown
@@ -78,29 +78,25 @@ every field has one source and coverage is computed into the artifact. Run the
78
78
  rollup at the **start of Show and Sync**, and after any Start/Resume/Pause/Close
79
79
  edit to a stream file.
80
80
 
81
- Procedure (deterministic + idempotent never hand-edit `active.yml`):
82
-
83
- 1. Glob `.forge/streams/{stream}.yml`. Each → a row: `coordination` = the file's
84
- `status`; plus goal, runtime (branch/worktree), ownership summary,
85
- `merge_readiness` (from `merge.readiness`), `blocked_by`, `next_action`.
86
- 2. For a stream with `stream.milestone: m-{id}`, read `phase` + activity from
87
- `.forge/state/milestone-{id}.yml` (`current.status`, `last_updated`) do not
88
- store workflow phase in the stream file. Linked milestone `deferred`/`complete`/
89
- absent `phase` reflects that (and surface it: a deferred milestone's stream
90
- should likely pause; offer it).
91
- 3. Glob active milestones (`index.yml` `status: active`). Any with **no** stream
92
- file emit a derived `stream: implicit` row (phase from the milestone, no
93
- coordination) so coverage is guaranteed an active milestone is never silently
94
- missing. Offer to materialize a stream file if it needs coordination (never
95
- auto-create).
96
- 4. Derive `merge_queue:` — one entry per stream with `merge.readiness: ready`.
97
- 5. Write `active.yml` (`version`, `updated_at`, derived `streams:` sorted by id,
98
- derived `merge_queue:`). No hand-edited keys.
81
+ Procedure: run the **Stream Rollup** exactly as defined in FORGE.md → Stream
82
+ Rollup — the deterministic join + write (glob stream files, derive each row's
83
+ `coordination`/`merge_readiness`/`blocked_by`/`next_action`, read `phase` from the
84
+ linked milestone, emit `stream: implicit` coverage rows, derive `merge_queue` from
85
+ `merge.readiness: ready`, write `active.yml`) is **single-sourced there**. Never
86
+ hand-edit `active.yml`. Beyond that deterministic write, the Chief adds two
87
+ **interactive** steps the boot-time rollup omits:
88
+
89
+ - **Deferred/complete linked milestone** when a stream's `stream.milestone`
90
+ points at a `deferred`/`complete` milestone, surface it: the stream should
91
+ likely pause offer it.
92
+ - **Implicit row needs coordination** when an active milestone has no stream
93
+ file (the rollup emits a `stream: implicit` row for coverage), offer to
94
+ materialize a stream file for it. **Never auto-create.**
99
95
 
100
96
  Note: invoked directly, `/chief-of-staff` does not run `forge`'s milestone Rollup,
101
- so `index.yml` may be slightly stale; read it as-is for step 3 and suggest a
102
- `/forge` pass if it looks off. The Chief writes `active.yml` (derived class); it
103
- never writes `index.yml` or any milestone file.
97
+ so `index.yml` may be slightly stale; read it as-is when deriving implicit-row
98
+ coverage and suggest a `/forge` pass if it looks off. The Chief writes `active.yml`
99
+ (derived class); it never writes `index.yml` or any milestone file.
104
100
 
105
101
  ## Start Stream Flow
106
102
 
@@ -172,7 +168,7 @@ existing M10 state.
172
168
  - `ownership.owned_surfaces`, `shared_surfaces`, and `read_only_surfaces`
173
169
  as empty lists if unknown
174
170
  - `blockers` from milestone blockers or degraded/refused notes
175
- - `context.milestone` pointing at `.forge/state/milestone-{id}.yml`
171
+ - `stream.milestone: "m-{id}"` — the rollup join key (phase derives from the linked milestone's state); this is the same link step 8 confirms
176
172
  7. Populate the stream brief with:
177
173
  - current milestone/phase/status
178
174
  - worktree path and branch
@@ -282,10 +278,11 @@ merge/rebase), and **cross-stream merge order**. A checkpoint stream that is
282
278
  cleanly auto-publishing needs no nudge here.
283
279
 
284
280
  1. **Ready-to-merge surfacing.** From `active.yml`, collect streams with
285
- `status: ready` (and any `merge_queue` entries). For each, run the Merge Safe
286
- Flow conditions. Unblocked + no conflicts list under **Ready to merge** with
287
- a one-line *"ready integrate now"* and, if known, how long it has been ready
288
- (from the stream's `last_sync` / brief date). Add/refresh its `merge_queue`
281
+ `coordination: ready` (the derived-row coordination field) plus any
282
+ `merge_queue` entries (derived from `merge.readiness: ready`). For each, run the
283
+ Merge Safe Flow conditions. Unblocked + no conflicts list under **Ready to
284
+ merge** with a one-line *"ready integrate now"* and, if known, how long it has
285
+ been ready (from the stream's `stream.updated_at` / brief date). Add/refresh its `merge_queue`
289
286
  entry (`status: waiting`). Blocked → list the named reason instead.
290
287
  2. **Idle-ready nudge.** A stream that has sat `ready` across **multiple Show/Sync
291
288
  passes** or whose branch is mergeable but untouched is the cadence smell.
@@ -32,8 +32,10 @@ git worktree list --porcelain 2>/dev/null \
32
32
  - Surfacing continues to Rollup + selection — but it is **no longer purely informational**. Enforcement is deferred to **1.1a**, which fires only for the *selected* milestone (booting from main to work an unrelated milestone stays free). Record which worktrees were reported here so 1.1a can match the selection against them. (See FORGE.md State Ownership for the full taxonomy.)
33
33
 
34
34
  **If `in_main_checkout: false`** (this session is itself a worktree) AND `forge.worktree_rebase_check: true` in `.forge/project.yml` (default `false` — opt-in):
35
- - Diff `.forge/` between this worktree's `HEAD` and `origin/main` (or `main` if no remote), restricted to the **shared-mutable** + **stable-shared** files per the FORGE.md taxonomy (`context.md`, `refactor-backlog.yml`, `project.yml`, `constitution.md`, `design-system.md`, `roadmap.yml`, `requirements/m{N}.yml`).
36
- - Any differ surface: *"`{file}` has changed on main since this worktree branched. Pull just this file (`git restore --source main -- {file}`), rebase, or skip?"* — operator decides.
35
+ - Diff this worktree's `HEAD` against `origin/main` (or `main` if no remote) across **two** surfaces:
36
+ - **(a) shared `.forge/` state** the **shared-mutable** + **stable-shared** files per the FORGE.md taxonomy: `context.md`, `refactor-backlog.yml`, `project.yml`, `constitution.md`, `design-system.md`, `roadmap.yml`, `requirements/m{N}.yml`, `FORGE.md`.
37
+ - **(b) framework surface** — `.claude/skills/`, `.claude/agents/`, `.forge/FORGE.md` — the files a `/upgrading` run changes. This catches a **framework upgrade that landed on main after this worktree branched**: `upgrading` Step 8's producer-side offer is the primary path, but a worktree created (or forgotten) since the upgrade only finds out here. (Gitignored framework files — `.mcp.json`, experimental hooks — won't show in the diff; they propagate via the experimental installer, not git.)
38
+ - Any differ → surface: *"`{file}` has changed on main since this worktree branched. Pull just this file (`git restore --source main -- {file}`), rebase, or skip?"* — operator decides. For a framework-surface (b) hit, lead with rebase (a partial restore of skills/agents is rarely what you want).
37
39
  - Setting absent or `false` → no-op.
38
40
 
39
41
  **Integration flag check (consumer — worktree only).** When `in_main_checkout: false`, check whether a checkpoint publish has landed work on main that this worktree should pull (FORGE.md → Stream Integration Checkpoints). Event-driven, not polled — read the flag first; act only if it is set:
@@ -52,7 +54,7 @@ Cross-machine note: the flag file is machine-local, so a worktree on another lap
52
54
  - Mismatch → one advisory line: *"recorded worktree path `{path}` for m{id} is not under the convention root `{root}` — likely a manual `git worktree add` or pre-convention state. Fix: `git worktree move {path} {root}/{anchor}` then update the recorded path. (Advisory; honors `orchestration.worktree_root`.)"*
53
55
  - Parent dir under the root, or no recorded path → silent. An explicit `orchestration.worktree_root` is honored by construction (the override **is** the root compared against), so a deliberately relocated root never false-warns.
54
56
 
55
- **Stream Rollup (active.yml is derived).** If `.forge/streams/` has any stream files, regenerate `.forge/streams/active.yml` from them + active milestones per FORGE.md → Stream Rollup, the same way step 1.0 regenerates `index.yml`. `active.yml` is derived — never read it as authoritative without rolling up first, and never hand-edit it. Runs in the main checkout (worktrees never write `active.yml`). No `streams/` dir → skip.
57
+ **Stream Rollup (active.yml is derived).** If `.forge/streams/` has any stream files, regenerate `.forge/streams/active.yml` from them + active milestones per FORGE.md → Stream Rollup, the same way step 1.0 regenerates `index.yml`. **Run the 1.0 State Rollup (below) first** so `index.yml` is fresh — the Stream Rollup reads active milestones from it for implicit-row coverage, so on a boot where a just-merged milestone promotion lands, a stale `index.yml` would silently drop that milestone's coverage row. `active.yml` is derived — never read it as authoritative without rolling up first, and never hand-edit it. Runs in the main checkout (worktrees never write `active.yml`). No `streams/` dir → skip.
56
58
 
57
59
  **Ready-to-merge nudge (main checkout / streams without checkpoints).** From the just-regenerated `active.yml`, scan for streams with `coordination: ready` or a non-empty `merge_queue` **that are not auto-publishing via checkpoints**. Any present → one line: *"{N} stream(s) ready to merge — run `/chief-of-staff` (merge safe) to integrate."* Pointer only — the cadence logic lives in the Chief's Merge Cadence Check; `forge` never merges. Nothing pending → silent.
58
60
 
@@ -123,7 +125,7 @@ Single-owner-mutable state must be written by exactly one checkout. When this se
123
125
  Resolve the worktree path from the recorded `lifecycle.worktree_path` / `runtime.worktree`, or — if absent — from the base **Worktree Convention** (FORGE.md).
124
126
 
125
127
  **Completed milestone + live worktree → finalize, not gate (precedence).** The gate assumes the live worktree holds *in-progress, owned* work. A worktree whose milestone file is already `current.status: complete` — it set complete and merged to main, then a fresh main boot lands here — needs the **opposite** of a write-block: it needs wrapping up. So when the selected milestone is `complete` **and** still has a live worktree, do **not** raise the four-option ownership gate. Surface the finalize offer instead: *"m{id} is complete but its worktree `{path}` (`{branch}`) is still live — roll up `index.yml` and tear down the worktree + branch? [yes / keep]"*.
126
- - **yes** → confirm, then `git worktree remove {path}`, delete the branch, clear `lifecycle.worktree_*` on the milestone, and run **Rollup (1.0)**. (Never auto-remove without the explicit yes.)
128
+ - **yes** → confirm, then `git worktree remove {path}`, delete the branch, clear `lifecycle.worktree_*` on the milestone, and run **Rollup (1.0)**. If a stream file backs this milestone, also close it — clear `runtime.worktree`, set `stream.status: closed` (or prompt) — and run the **Stream Rollup** so `active.yml` drops the row pointing at the torn-down worktree. (Never auto-remove without the explicit yes.)
127
129
  - **keep** → leave it; impose no write-block — a `complete` milestone has no live cursor left to fork.
128
130
 
129
131
  The gate (below) and this finalize path are mutually exclusive on `current.status`: `complete` → finalize; any other status with a live worktree → gate.
@@ -183,30 +185,7 @@ Downstream skills (researching, discussing, planning, executing, verifying, revi
183
185
 
184
186
  **Skip declined groups first.** Glob `.forge/state/desire-paths/declined/*.yml`; skip any group whose key (`type` + normalized `note`) already has a decline file — it stays dismissed, no re-nag.
185
187
 
186
- **Surface at 3+.** For each remaining group with 3+ files, name a CONCRETE fix from this table (co-located here so the reader that acts carries the map):
187
-
188
- | Type | Suggested evolution |
189
- |------|---------------------|
190
- | `deviation_pattern` | Add a pre-check to planning, or a new constitutional article |
191
- | `tier_override` | Adjust tier-detection heuristics in the forge skill |
192
- | `skipped_step` | Make the step optional, or merge it into another |
193
- | `recurring_friction` | Add guidance to the relevant skill, or create a template |
194
- | `agent_struggle` | Add examples or anti-patterns to the relevant skill |
195
- | `user_correction` | Add a rule to constitution / context.md / the relevant skill |
196
-
197
- Prompt: *"Recurring: [{note}] ({N}x, scope:{scope}). Fix: [concrete suggestion from table]. Apply / decline?"*
198
-
199
- - **Agree** → apply the fix; archive the group's files to `.forge/state/desire-paths/resolved/`.
200
- - **Decline** → write ONE file `.forge/state/desire-paths/declined/{date}-{type}-{slug}.yml` with `{group_key, declined_at, reason}`, so it never re-prompts. (Replaces the old "note, don't nag" — which wrote nothing, so the 3+ files re-nagged every boot.)
201
- - (Occurrence count is derived from file count — there is no counter to reset.)
202
-
203
- **Upstream transport — for `scope: framework` groups only** (ADR-012). A framework-scope signal's fix targets Forge's own skills/templates, which live in the Forge repo, not this project. So in addition to the Apply/decline prompt above:
204
-
205
- 1. **Always** write a portable, self-contained block to `.forge/state/desire-paths/upstream/{date}-{type}-{slug}.md` — pattern, occurrence count, the sibling observation filenames, the suggested evolution (from the table), and `suggestion_target`. Formatted to paste cleanly into a GH issue. This is the durable floor; it survives even with no network/tooling.
206
- 2. **Resolve `forge.upstream_repo`** from `project.yml`; if absent, default to `https://github.com/zayneupton/forge`. If resolvable **and** `gh` is on PATH → offer (don't auto-run — it publishes externally): `gh issue create --repo {upstream_repo} --title "[desire-path] {note}" --body-file {block}`. **The GH issue is the real close** — tracked, central, survives machine loss.
207
- 3. If `gh`/url unavailable → note the block is on disk and harvestable by `upgrading` (which runs from a local Forge clone) into `{source}/docs/desire-paths-inbox/`.
208
-
209
- `project`-scope groups keep the in-project flow above — no upstream. Applying a framework fix locally (when this *is* the Forge repo) and routing it upstream are not mutually exclusive: in a user project you route; in the Forge repo you apply.
188
+ **Surface at 3+.** For each remaining group with 3+ files (declined groups already skipped), **load `desire-paths-review.md` in this skill dir and follow it** — it carries the concrete fix-suggestion map (per `type`), the Apply/decline mechanics (archive to `resolved/` on apply; write a `declined/` file on decline — no re-nag), and the upstream transport for `scope: framework` signals (portable block + optional `gh issue` against `forge.upstream_repo`). Groups under 3 (the common boot) nothing to surface; skip the load. (Occurrence count is derived from file count — no counter to reset.)
210
189
 
211
190
  ### 1.3 Interface Check
212
191
 
@@ -0,0 +1,37 @@
1
+ # Desire-Path Review — Surfacing & Fix Map
2
+
3
+ Loaded by `forge` boot (SKILL.md Step 1.2) **only when a desire-path group reaches
4
+ 3+ occurrences**. The cheap part of the review — glob, skip declined groups, count
5
+ per group — stays inline in SKILL.md; this file carries the fix-suggestion map,
6
+ the apply/decline mechanics, and the upstream transport for framework-scope
7
+ signals, so the common boot (no group at 3+) never loads it.
8
+
9
+ ## Surface at 3+ — concrete fix map
10
+
11
+ For each remaining group with 3+ files, name a CONCRETE fix from this table:
12
+
13
+ | Type | Suggested evolution |
14
+ |------|---------------------|
15
+ | `deviation_pattern` | Add a pre-check to planning, or a new constitutional article |
16
+ | `tier_override` | Adjust tier-detection heuristics in the forge skill |
17
+ | `skipped_step` | Make the step optional, or merge it into another |
18
+ | `recurring_friction` | Add guidance to the relevant skill, or create a template |
19
+ | `agent_struggle` | Add examples or anti-patterns to the relevant skill |
20
+ | `user_correction` | Add a rule to constitution / context.md / the relevant skill |
21
+
22
+ Prompt: *"Recurring: [{note}] ({N}x, scope:{scope}). Fix: [concrete suggestion from table]. Apply / decline?"*
23
+
24
+ - **Agree** → apply the fix; archive the group's files to `.forge/state/desire-paths/resolved/`.
25
+ - **Decline** → write ONE file `.forge/state/desire-paths/declined/{date}-{type}-{slug}.yml` with `{group_key, declined_at, reason}`, so it never re-prompts. (Replaces the old "note, don't nag" — which wrote nothing, so the 3+ files re-nagged every boot.)
26
+ - (Occurrence count is derived from file count — there is no counter to reset.)
27
+
28
+ ## Upstream transport — `scope: framework` groups only (ADR-012)
29
+
30
+ A framework-scope signal's fix targets Forge's own skills/templates, which live in
31
+ the Forge repo, not this project. So in addition to the Apply/decline prompt above:
32
+
33
+ 1. **Always** write a portable, self-contained block to `.forge/state/desire-paths/upstream/{date}-{type}-{slug}.md` — pattern, occurrence count, the sibling observation filenames, the suggested evolution (from the table), and `suggestion_target`. Formatted to paste cleanly into a GH issue. This is the durable floor; it survives even with no network/tooling.
34
+ 2. **Resolve `forge.upstream_repo`** from `project.yml`; if absent, default to `https://github.com/zayneupton/forge`. If resolvable **and** `gh` is on PATH → offer (don't auto-run — it publishes externally): `gh issue create --repo {upstream_repo} --title "[desire-path] {note}" --body-file {block}`. **The GH issue is the real close** — tracked, central, survives machine loss.
35
+ 3. If `gh`/url unavailable → note the block is on disk and harvestable by `upgrading` (which runs from a local Forge clone) into `{source}/docs/desire-paths-inbox/`.
36
+
37
+ `project`-scope groups keep the in-project flow above — no upstream. Applying a framework fix locally (when this *is* the Forge repo) and routing it upstream are not mutually exclusive: in a user project you route; in the Forge repo you apply.
@@ -76,11 +76,11 @@ Resolve current milestone ID from `.forge/state/index.yml` (active milestone) or
76
76
 
77
77
  If missing, create from `.forge/templates/requirements.yml`:
78
78
  1. Extract from user description + research
79
- 2. IDs: FR-001, FR-002... **Globally unique across milestones** -- check existing `.forge/requirements/*.yml` for highest FR-ID, continue sequence
79
+ 2. IDs: FR-001, FR-002... **Globally unique across milestones.** Allocate via the **ID Reservation Protocol** (FORGE.md): next = `max(highest `kind: fr` in `.forge/reservations.yml`, highest FR in `.forge/requirements/*.yml`) + 1; append `{kind: fr, id, milestone, reserved_at, summary}` to `.forge/reservations.yml`, **commit + push**, then write the FR. The reservation is what keeps concurrent worktrees from silently claiming the same FR number (bare scan-and-increment collides — see FORGE.md / ADR-016). No `reservations.yml` ⇒ scan as before; first reservation creates it.
80
80
  3. Acceptance: Given/When/Then
81
81
  4. Uncertain: `[NEEDS CLARIFICATION]`
82
82
  5. P1 (must) / P2 (should) / P3 (nice)
83
- 6. Deferred: DEF-001... (also globally unique)
83
+ 6. Deferred: DEF-001..., and NFR-001... — **also globally unique; reserve the same way** (`kind: def` / `kind: nfr`) before writing.
84
84
 
85
85
  **E2E gate (M9):** For each functional requirement being added or refined:
86
86
  1. Decide `e2e: true|false` -- does this story need a post-validation e2e test?
@@ -135,6 +135,8 @@ This is the common case when starting a new milestone alongside an existing one.
135
135
 
136
136
  If a sibling milestone (e.g. m50) has state + requirements but is missing from `roadmap.yml`, that's a pre-existing gap -- flag to user, don't silently backfill.
137
137
 
138
+ **Promoted-from-backlog milestone (`milestone.origin` set).** If `state/milestone-{N}.yml` carries `milestone.origin: {R-id}` (set by `forge` Step 1.2 when promoting a Standard-tier refactor item), no extra bookkeeping is needed here — the originating backlog item is flipped to `done` automatically when the milestone completes (see `reviewing` → **Promoted Milestone Completion Hook**). Just plan it as a normal milestone.
139
+
138
140
  ## Step 6: Decompose Tasks
139
141
 
140
142
  ### Step 6.1: Cross-Layer Contract Detection
@@ -364,7 +366,7 @@ Prompt pattern:
364
366
 
365
367
  - **yes** — defer to `testing` after planning completes; note in plan frontmatter
366
368
  - **no** — document rationale in `context.md` Discretion Areas
367
- - **later** — surface again at verifying gate if UI tests still missing
369
+ - **later** — rides on the verifying **Interface Testing Gate**: when `interface` is declared in `project.yml`, that gate already BLOCKS at verifying if UI tests are still missing (no separate planning hook needed)
368
370
 
369
371
  Decision captured once, pre-code. Does not block planning.
370
372
 
@@ -52,15 +52,15 @@ Read: .github/workflows/* → CI config (ci-check mode, analyst CI sub-check)
52
52
  3. Agent appends actionable items to `.forge/refactor-backlog.yml`:
53
53
  ```yaml
54
54
  - id: T-{NNN}
55
- source: testing-analyst
56
- date: YYYY-MM-DD
57
- category: flake | coverage-gap | anti-pattern | quarantine
55
+ milestone: {active milestone id}
56
+ category: flake | coverage-gap | anti-pattern | quarantine-candidate | ci-gap
58
57
  file: "path/to/test.spec.ts"
59
58
  lines: "NN-NN"
60
59
  description: "What's wrong"
61
60
  effort: quick | standard
62
61
  suggested_approach: "How to fix"
63
62
  status: pending
63
+ added: "YYYY-MM-DD"
64
64
  ```
65
65
  ID numbering: read existing backlog, find max T-NNN, increment. No gaps.
66
66
  4. Receive agent YAML output (`suite_audit:` block). Review for completeness.
@@ -81,12 +81,28 @@ For each skill dir present in the project's `.claude/skills/` but **absent from
81
81
 
82
82
  ## Step 4: Sync Template-Only Files
83
83
 
84
- 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`.)
84
+ Same process as Step 3 for `.forge/templates/**`, `.forge/migrations/**`, and `.forge/adapters/**`. (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`. `.forge/adapters/**` ships the platform-adapter generation protocols, e.g. `codex-generation.md`, so the Step 4b Codex step can hand it to `quick-tasking`.)
85
85
 
86
86
  **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.
87
87
 
88
88
  **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.
89
89
 
90
+ ## Step 4b: Codex Adapter (presence-gated — detect + offer, never auto)
91
+
92
+ Forge ships a **Codex/AGENTS adapter** (`.agents/skills/` + `.codex/` wiring) for projects run on Codex. It is a *derived translation* of the canonical `.claude/` skills (ADR-014) and drifts whenever core moves — but `upgrading` syncs only `.claude/`, so the Codex adapter would silently rot. This step makes that staleness visible and one-keystroke-fixable, **without** ever generating it here (only an agent-as-Codex can author it natively).
93
+
94
+ 1. **Presence gate.** Glob the project root for `.agents/` and `.codex/`. **Neither exists → skip silently** — no prose, no prompt (Claude-only / greenfield projects are unaffected).
95
+ 2. **Either exists → report + offer.** The Codex adapter is present and may be stale relative to the `.claude/` source just synced in Steps 3–4. Offer regeneration:
96
+ ```
97
+ Codex adapter detected (.agents/ / .codex/). It may be stale vs the .claude/
98
+ source just synced. Regenerate it now? (yes / no)
99
+ ```
100
+ - **yes** → invoke `quick-tasking`, handing it `.forge/adapters/codex-generation.md` as the task definition (the **same delegation pattern as Step 7 migrations**). The protocol instructs an agent-as-Codex to author `.agents/` + `.codex/` from source using native primitives; a human reviews the diff before commit. `upgrading` itself **never** writes `.agents/` and never performs the translation.
101
+ - **no** → record `Codex adapter: stale, declined` for the Step 6 report.
102
+ 3. **Report line.** Add a `Codex adapter:` line to the Step 6 report — one of `regenerated` / `stale, declined` / `not present`.
103
+
104
+ Presence-gated, never auto-runs, regeneration is agent-run via `quick-tasking`. The protocol doc itself reaches the project via the Step 4 `.forge/adapters/**` sync.
105
+
90
106
  ## Step 5: Handle Import-Managed + Merge-Owned Files
91
107
 
92
108
  **`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.
@@ -125,6 +141,8 @@ Experimental skills: {N}
125
141
  - orchestrating (from experimental/m10) — updated [or: unchanged / STALE, declined]
126
142
  - ...
127
143
 
144
+ Codex adapter: regenerated [or: stale, declined / not present]
145
+
128
146
  Removed from template: {N} files
129
147
  - .claude/agents/old-agent.md (still in your project)
130
148
  - ...
@@ -175,3 +193,21 @@ Migrate now? (yes/no/show guide)
175
193
  ### Why there is no bookkeeping
176
194
 
177
195
  Recording "crossed" guides is implicit in the range. Once Step 5 stamps `forge.version` to `source`, a later run's range `(new installed, source]` excludes every guide already crossed — so nothing re-detects, with **no ledger or marker file**. The range also closes the install-run blind spot: detection keys off the guides **freshly synced this run** (Step 4) plus the **pre-stamp installed version**, so the run that installs new detection behavior uses the new guides, not this skill's own pre-sync prose.
196
+
197
+ ## Step 8: Propagate to Live Worktrees (offer, never auto)
198
+
199
+ The sync lands framework files (`.claude/skills/`, `.claude/agents/`, `.forge/FORGE.md`, templates) in **this checkout only**. A live Forge worktree keeps running its branch-point copies until it pulls main in — and nothing else surfaces this: the integration flag (`.git/forge/integration-pending`) fires only on a verified **work** checkpoint, never on an upgrade, and `forge.worktree_rebase_check` is opt-in. So close the loop here, where the upgrade actually happened.
200
+
201
+ 1. **Main checkout only.** `git rev-parse --git-dir` == `--git-common-dir` → main; differ → this session **is** a worktree, so skip (run `/upgrading` from main to propagate). No remote/not a git repo → skip.
202
+ 2. **Commit the sync first** — a worktree can only rebase onto committed history, so uncommitted synced files won't propagate. If the sync is unstaged, prompt: *"Commit this upgrade on main before propagating? (e.g. `chore(forge): upgrade to v{new}`)"* and wait.
203
+ 3. **Enumerate live worktrees:** `git worktree list --porcelain`, branches matching `refs/heads/forge/m-*` (same scan as the `forge` boot preflight). None → done, silent.
204
+ 4. **Offer — never auto** (a rebase can conflict and needs human judgment):
205
+ ```
206
+ {N} live worktree(s) lag this upgrade (v{old} → v{new}):
207
+ - {path} ({branch})
208
+ Pick up v{new} in each: git -C {path} rebase main
209
+ Rebase now? (each / list-only / skip)
210
+ ```
211
+ - **each** → run `git -C {path} rebase main` per worktree; on conflict, **stop that one**, report it, and move on — never force, never auto-resolve.
212
+ - **list-only / skip** → print the commands; the operator runs them when each worktree is at a safe stopping point.
213
+ 5. **Gitignored carve-outs don't rebase.** `.mcp.json`, experimental hooks (e.g. M10 `forge-claim-check*.sh`), and anything else gitignored is **not** carried by a rebase. If the upgrade changed them (e.g. an experimental skill was re-synced in Step 3b), each worktree must re-run the experimental installer (`experimental/{pkg}/install.sh`) — a rebase alone leaves them stale. Surface this line whenever Step 3b reported an experimental re-sync.
@@ -302,7 +302,7 @@ If GAPS found, route back to planning in gap-closure mode. Context clear applies
302
302
  Realizes producer-side continuous integration (FORGE.md → Stream Integration Checkpoints). Runs at Phase Handoff step 5, **only when all hold**:
303
303
 
304
304
  - the phase's plan frontmatter has `integration_checkpoint: true`;
305
- - the verdict is PASSED **and** `current.human_verified` is set (where required)never publish unverified work;
305
+ - the verdict is PASSED never publish unverified work. (`current.human_verified` gates milestone **close**, *not* per-checkpoint publish: a non-final checkpoint phase publishes on PASS alone; the milestone's **final** checkpoint coincides with close, where the Human Verification Gate applies so in practice only the last checkpoint also requires `human_verified`.)
306
306
  - the session is a **worktree** (`git rev-parse --git-dir` ≠ `--git-common-dir`). In a single checkout, skip — there is no main to fast-forward into.
307
307
 
308
308
  Then publish **fast-forward only**:
@@ -125,14 +125,15 @@ Auto-detects complexity. Override: "Use Quick/Standard/Full tier."
125
125
  | `streams/{stream}.yml` | 15 KB | Stream state stays resumable without becoming history |
126
126
  | `streams/{stream}/brief.md` | 8 KB | Stream context packet must fit into active session context |
127
127
  | `streams/{stream}/packages/{id}.yml` | 10 KB | Delegated work contract stays bounded |
128
+ | `reservations.yml` | 50 KB | Append-only ID-reservation registry; one short line per ADR/DEF/FR/NFR reserved |
128
129
 
129
130
  ### Accumulating Artifacts
130
131
 
131
132
  Every new accumulating artifact needs: owner, size gate, archive/compaction rule,
132
133
  `derived` vs source-of-truth status, and migration behavior. Defaults:
133
134
 
134
- - `roadmap.yml`, `requirements/*.yml`, `releases.yml` are source-of-truth; load
135
- scoped slices, not whole history.
135
+ - `roadmap.yml`, `requirements/*.yml`, `releases.yml`, `reservations.yml` are
136
+ source-of-truth; load scoped slices, not whole history.
136
137
  - `phases/`, `audits/`, `research/`, `archive/`, and `context-archive.md` are
137
138
  historical; load only by milestone/stream/package/version.
138
139
  - Runtime/generated files (`.mcp-server/*.db*`, logs, spike DBs) are excluded
@@ -197,7 +198,7 @@ State lives in `.forge/`:
197
198
  - `project.yml` — Vision, stack, design system, verification, constraints (<5KB)
198
199
  - `constitution.md` — Active architectural gates
199
200
  - `design-system.md` — Component mapping table
200
- - `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`.
201
+ - `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, **reserve it via the ID Reservation Protocol** (append to `.forge/reservations.yml`, commit+push, then write) — the next number is `max(highest reserved, highest in `.forge/requirements/*.yml`) + 1`. That reservation is what makes the shared ID space safe across **concurrent worktrees**: bare scan-and-increment sees only committed state, so two worktrees branched from one baseline claim the same number and collide silently until merge (see ADR-016). On a residual collision (legacy/un-reserved, 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`.
201
202
  - `roadmap.yml` — Phases, milestones, dependencies
202
203
  - `state/index.yml` — DERIVED registry rolled up from milestone files (id, name, status, last_updated). Never hand-edited; never written by worktree agents.
203
204
  - `state/milestone-{id}.yml` — Per-milestone cursor (single source of truth): position, progress, decisions, blockers. One owner at a time.
@@ -213,13 +214,14 @@ State lives in `.forge/`:
213
214
  - `refactor-backlog.yml` — Refactoring catalog (actionable items only), worked via quick-tasking. Canonical status vocab: `pending | in_progress | done | dismissed | deferred`. Terminal items auto-archive on the next reviewing/quick-tasking write (compaction-on-write).
214
215
  - `refactor-backlog-archive.yml` — Append-only terminal-item archive (`done`/`dismissed`); keeps the audit trail out of the working backlog.
215
216
  - `releases.yml` — Append-only version-reservation registry (cross-session coordination point; see Version Reservation Protocol)
217
+ - `reservations.yml` — Append-only ID-reservation registry for ADR/DEF/FR/NFR numbers (cross-worktree coordination point; see ID Reservation Protocol)
216
218
  - `archive/milestone-{id}/` — Archived milestone artifacts (archive-delete)
217
219
 
218
220
  **Milestones** group phases into concurrent streams. Own state file — no conflicts across sessions. Can be deferred (frozen in place) or archive-deleted.
219
221
  `index.yml status` gates routing: `not_started | active | deferred | complete`.
220
222
 
221
223
  **Stream Rollup (`active.yml` is derived).** Mirrors the milestone Rollup: `active.yml` is a pure projection, regenerated — never hand-edited — by a deterministic, idempotent **join** of two single-sourced inputs (ADR-015). Run by the main/orchestrator session and at `chief-of-staff` Show/Sync; worktrees never write it. Procedure:
222
- 1. Glob `streams/{stream}.yml`. Each → a row carrying its coordination state (`coordination` = the file's `status`), goal, runtime, ownership summary, `merge.readiness`, blockers, next action — all from the stream file.
224
+ 1. Glob `streams/{stream}.yml`. Each → a row carrying its coordination state (`coordination` = the file's `status`), goal, runtime, ownership summary, `merge_readiness` (derived from the stream file's `merge.readiness`), `blocked_by`, `next_action` — all from the stream file.
223
225
  2. For a stream with `stream.milestone: m-{id}`, read `phase`/activity from `milestone-{id}.yml` (`current.status`, `last_updated`) — **not** stored in the stream file. A linked milestone that is `deferred`/`complete`/absent → the row's `phase` reflects that.
224
226
  3. Glob active milestones (from `index.yml`). Any active milestone with **no** stream file → emit a derived `stream: implicit` row (phase from the milestone, no coordination state) so coverage is guaranteed in the artifact — an active milestone can never be silently missing.
225
227
  4. Derive `merge_queue:` — one entry per stream with `merge.readiness: ready`.
@@ -248,10 +250,11 @@ with the work it describes. Every artifact has a sharing class:
248
250
  | Class | Files | Write rule |
249
251
  |---|---|---|
250
252
  | **Single-owner mutable** | `state/milestone-{id}.yml`, `phases/milestone-{id}/`, `research/milestone-{id}.md` | Exactly one writer at a time — the worktree (or main session) currently driving that milestone. **Other worktrees never touch it**, not even to read-then-write — they pull from main if they need it. |
253
+ | **Single-owner mutable (per stream)** | `streams/{stream}.yml`, `streams/{stream}/brief.md`, `streams/{stream}/packages/*.yml` | The stream's **current driver** writes them — one writer at a time, like the milestone file. Distinct streams own distinct files (no cross-stream contention). `active.yml` is **derived** from them: a worktree driving a stream may write its own stream file but **never** `active.yml` or a peer stream's file. |
251
254
  | **Derived** | `state/index.yml`, `streams/active.yml` | Never hand-edited. Regenerated by rollup from their sources (`index.yml` ← `milestone-*.yml` via Rollup 1.0; `active.yml` ← per-stream files + active milestones via the Stream Rollup). Only the main/orchestrator session (and `chief-of-staff` for `active.yml`) runs rollup. |
252
- | **Append-only shared** | `releases.yml` (version reservations), `state/desire-paths/*` (one file per observation) | Many writers OK; structure prevents collision. `releases.yml` uses the Version Reservation Protocol (append + commit + push before depending on the slot); desire-paths use distinct filenames so two writers never touch the same file. |
255
+ | **Append-only shared** | `releases.yml` (version reservations), `reservations.yml` (ADR/DEF/FR/NFR id reservations), `state/desire-paths/*` (one file per observation) | Many writers OK; structure prevents collision. `releases.yml` + `reservations.yml` use the reservation protocols (append + commit + push before depending on the number); desire-paths use distinct filenames so two writers never touch the same file. |
253
256
  | **Shared mutable** | `context.md`, `refactor-backlog.yml`, `requirements/m{N}.yml` (ID space `FR-`/`DEF-`/`NFR-` is globally shared even though each milestone owns its file) | Write only to your milestone/stream block. Within a block, append-only; strike through instead of rewriting. |
254
- | **Stable shared** | `project.yml`, `constitution.md`, `design-system.md`, `roadmap.yml`, `FORGE.md` | Rarely changes after init. Canonical on main. Worktrees lag and pull on rebase. No special protocolgit handles it because the conflict surface is tiny. |
257
+ | **Stable shared** | `project.yml`, `constitution.md`, `design-system.md`, `roadmap.yml`, `FORGE.md`, `.claude/skills/`, `.claude/agents/` | Rarely changes except via `/upgrading`. Canonical on main; worktrees lag until they rebase. An upgrade run touches **only the checkout it runs in**, so after upgrading in main, live worktrees keep their branch-point framework files — `upgrading` **Step 8** enumerates live `forge/m-*` worktrees and offers to rebase each (never auto), and the opt-in `forge.worktree_rebase_check` watches these files as a boot-time safety net. Gitignored carve-outs (`.mcp.json`, experimental hooks) don't rebase re-run the experimental installer if the upgrade changed them. |
255
258
 
256
259
  Cross-tree edits land on the current branch and are invisible to other worktrees
257
260
  until they pull/rebase. `forge` surfaces live worktrees at boot **and enforces
@@ -282,6 +285,18 @@ The project version + CHANGELOG slot are **shared resources** — when two miles
282
285
 
283
286
  **Version-bump criteria (0.x projects):** patch (`0.x.y`) = bug fix, doc tweak, sync-only, refinement. Minor (`0.x.0`) = new skill/agent/routing-row/auto-trigger/state-artifact/capability. Major (`x.0.0`) = post-1.0 breaking change. Default to minor when in doubt — a capability addition shipped as a patch drifts the version away from truth.
284
287
 
288
+ ### ID Reservation Protocol
289
+
290
+ Sequential IDs — `ADR-NNN` (decision records), `DEF-NNN` (deferred issues), `FR-NNN`/`NFR-NNN` (requirements) — are **shared cross-worktree resources** the same way the version number is. "Scan the tree for the highest and increment" only sees *committed* state, so two streams branched from the same baseline each claim the same next number and collide **silently until merge** — a costly multi-file renumber pass (see [ADR-016](../docs/decisions/ADR-016-id-reservation-protocol.md)). `.forge/reservations.yml` removes the number from per-stream scope, exactly as `releases.yml` does for versions.
291
+
292
+ - **Reserve before allocating.** Before authoring a new ADR or writing a new DEF/FR/NFR ID, append ONE entry to `.forge/reservations.yml` — `{kind, id, milestone, reserved_at, summary}`, `kind ∈ {adr, def, fr, nfr}` — then **commit + push it BEFORE creating the artifact**. A parallel worktree pulls and sees the number taken.
293
+ - **Next number = `max(reserved, in-tree) + 1` per kind.** The higher of (a) the highest reservation for that kind in `reservations.yml` and (b) the highest ID of that kind actually landed in the tree. Taking both keeps it backward-compatible — landed IDs that were never reserved are still respected.
294
+ - **Append-only — never edit prior entries.** Distinct lines = no contention. Race resolution: the second of two near-simultaneous reservations rebases onto the first (append-only → clean), re-reads `max()`, takes the next number.
295
+ - **Reserve points.** `architecting` reserves an `adr` before authoring it; `planning` reserves `fr`/`nfr`/`def` before writing them into requirements. (Out of scope: `executing`'s `DI-NNN` deferred-*issue* ids in `deferred-issues.md` are a separate, more local namespace — not reserved here.)
296
+ - **Lazy migration.** No `reservations.yml` ⇒ allocate as before (scan in-tree max); the first reservation creates the file. Existing projects are unaffected until they next allocate an ID.
297
+
298
+ This is the cross-worktree mechanism the FR/DEF/NFR "globally unique" rule (State Management, above) previously lacked, and the first reservation rule ADR numbers have ever had. Versions stay in `releases.yml` (separate semver/CHANGELOG semantics); everything else reserves here.
299
+
285
300
  ## Deviation Rules
286
301
 
287
302
  **Full definitions:** `.claude/agents/executor.md`.
@@ -0,0 +1,90 @@
1
+ # Codex/AGENTS Adapter — Generation Protocol
2
+
3
+ **You are an agent running AS the Codex runtime.** Your job: (re)generate Forge's
4
+ Codex/AGENTS adapter from the canonical source skills, using **Codex's own native
5
+ primitives**. This is a *native authoring* task, not a find-replace — a swapped-nouns
6
+ copy of the Claude adapter is wrong (FORGE.md Principle 2: adapter-native). Output is a
7
+ **derived product**; the source below is the spec.
8
+
9
+ Run via `quick-tasking` (handed this file by `/upgrading` Step 4b). A human reviews your
10
+ output before commit. Regenerating *this* Forge repo's own `.agents/` is **DEF-040** —
11
+ not your job unless explicitly asked.
12
+
13
+ ## 1. Source spec → output
14
+
15
+ | Source of truth (the spec) | You author (the derived adapter) |
16
+ |---|---|
17
+ | `.claude/skills/<name>/*.md` | `.agents/skills/<name>/*.md` |
18
+ | `.claude/agents/<name>.md` (if present) | `.agents/agents/<name>.md` |
19
+ | `.claude/hooks/*.sh` + settings hooks | `.codex/hooks/*.sh` + `.codex/hooks.json` |
20
+ | `CLAUDE.md` | `AGENTS.md` (keep the `@.forge/FORGE.md` import) |
21
+
22
+ Source = `.claude/` for now. The platform-neutral core (M16) is **DEF-041 deferred**; when
23
+ it lands, only this "source" pointer changes. `.forge/` protocol state is shared, **not**
24
+ translated — never fork it.
25
+
26
+ ## 2. Layout deltas — cosmetic, always-true (apply mechanically)
27
+
28
+ - skill dir `.claude/skills/` → `.agents/skills/`
29
+ - agent dir `.claude/agents/` → `.agents/agents/`
30
+ - hook dir `.claude/hooks/` → `.codex/hooks/`
31
+ - config file `CLAUDE.md` → `AGENTS.md`
32
+ - settings `.claude/settings.json` → Codex's settings equivalent
33
+ - `Reviewer: Claude` → `Reviewer: Codex`; `Claude Code <X>` → `Codex <X>`
34
+
35
+ **WARN — do NOT produce `.Codex` (capital C).** The real directories are lowercase
36
+ `.agents/` (skills/agents) and `.codex/` (hooks). A `.Codex` token is the exact bug this
37
+ protocol exists to kill — it must appear **nowhere** in your output.
38
+
39
+ **Do NOT rewrite Claude *model ids*** (`claude-opus-*`, `claude-sonnet-*`, `claude-haiku-*`,
40
+ `claude-fable-*`) — those are literal API identifiers, not branding. Leave them verbatim.
41
+
42
+ ## 3. Coupled mechanisms — handle with JUDGMENT, not substitution
43
+
44
+ These four skills (`executing`, `forge`, `orchestrating`, `planning`) reference Claude-only
45
+ runtime mechanics. A noun-swap corrupts them. For each, translate to Codex's real surface or
46
+ drop the moot part:
47
+
48
+ - **`EnterPlanMode`** (forge, planning). A Claude-native tool. → Map to Codex's plan/approval
49
+ convention if it has one; else **DROP** the "never use EnterPlanMode" prohibition (moot on
50
+ Codex) and keep the positive rule: *planning = invoke the planning skill*.
51
+ - **ADR-007 session-id / file-claim identity / `forge-session-id.sh` SessionStart hook**
52
+ (orchestrating). → Translate to Codex's actual session concept; if Codex has none, mark
53
+ **N/A** — the claim layer degrades to single-session, which `orchestrating` already
54
+ tolerates.
55
+ - **Native Task tools** (`TaskCreate`/`TaskUpdate`/`TaskList`, executing in-session UI). →
56
+ Map to Codex's equivalent; if none, note they were a Claude-only UI nicety and the
57
+ `.forge/state/` files remain the cross-session source of truth.
58
+
59
+ ## 4. `.codex/` wiring
60
+
61
+ Author the hook scripts + `.codex/hooks.json` for Codex's real hook surface (its
62
+ PreToolUse/PostToolUse analogs), preserving the **active-skill gate** behavior the
63
+ `.claude/hooks/` versions enforce. If Codex exposes no hook surface, say so explicitly and
64
+ have the skills self-enforce in prose instead of silently dropping the gate. Pre-existing
65
+ `.codex/hooks/README.md` cleanup (stale "Claude Code PreToolUse" wording) is **DEF-042** —
66
+ separate.
67
+
68
+ ## 5. Invariants — PRESERVE verbatim across adapters
69
+
70
+ Translate **mechanics** (tool names, hook surface, paths, config filenames), **never the
71
+ protocol**. These must read identically in `.agents/` and `.claude/`, so a regeneration is
72
+ diffable for fidelity:
73
+
74
+ - skill routing tables + workflow tiers (Quick / Standard / Full)
75
+ - gate semantics — verification gates, the **Human Verification Gate** (close precondition)
76
+ - state-ownership rules (single-owner / derived / append-only / shared classes)
77
+ - the **Version Reservation Protocol** and atomic-commit rules
78
+ - desire-path capture, milestone lifecycle, context size gates
79
+
80
+ If a translation forces a change to any of these, you have over-reached — stop and flag it;
81
+ the protocol does not move to fit a platform.
82
+
83
+ ## 6. How this runs
84
+
85
+ 1. `/upgrading` Step 4b detects `.agents/` or `.codex/` and offers regeneration; on **yes**
86
+ it hands you this file via `quick-tasking`.
87
+ 2. You author the adapter per §1–5 using native primitives.
88
+ 3. A human reviews the diff for fidelity (invariants intact, no `.Codex`, coupled mechanisms
89
+ handled) **before** anything is committed. `/upgrading` never writes `.agents/` itself.
90
+ 4. Output is the derived adapter; the source skills are untouched.
@@ -119,10 +119,18 @@ Use `git mv` so history follows the files and `git status` records a rename
119
119
 
120
120
  ```bash
121
121
  cd .forge/phases
122
+ # POSIX parameter expansion — bash- AND zsh-safe. Do NOT use [[ =~ ]] + $BASH_REMATCH
123
+ # here: under zsh (macOS default, and the shell quick-tasking runs this in) captures
124
+ # land in $match, not $BASH_REMATCH, so id/phase/name expand empty and git mv
125
+ # collapses every phase dir onto a malformed milestone-/- target.
122
126
  for d in m[0-9]*-[0-9]*-*/; do
123
127
  d=${d%/}
124
- [[ $d =~ ^m([0-9]+)-([0-9]+)-(.+)$ ]] || continue
125
- id=${BASH_REMATCH[1]}; phase=${BASH_REMATCH[2]}; name=${BASH_REMATCH[3]}
128
+ rest=${d#m} # strip leading 'm' → id-phase-name
129
+ id=${rest%%-*} # digits before first '-'
130
+ rest=${rest#*-} # drop 'id-' → phase-name
131
+ phase=${rest%%-*} # digits before next '-'
132
+ name=${rest#*-} # remainder (may contain '-')
133
+ case "$id$phase" in *[!0-9]*) continue;; esac # skip if id/phase non-numeric
126
134
  mkdir -p "milestone-$id"
127
135
  git mv "$d" "milestone-$id/$phase-$name"
128
136
  done
@@ -0,0 +1,81 @@
1
+ # Migration Guide: Agent-Regenerated Codex Adapter (Forge 0.41.0)
2
+
3
+ Applies to projects that have a **Codex/AGENTS adapter** (`.agents/` and/or `.codex/`).
4
+ Claude-only and greenfield projects are unaffected — this guide is silent for them.
5
+
6
+ Forge 0.41.0 (ADR-014) stops treating `.agents/` as a hand-maintained or find-replaced
7
+ second source. The Codex adapter is now a **derived translation** of the `.claude/` skills,
8
+ **regenerated by an agent acting as the Codex runtime** from a shipped protocol
9
+ (`.forge/adapters/codex-generation.md`), using Codex's native primitives. This kills the
10
+ `.Codex`-token corruption an earlier naive find-replace produced and fixes the silent drift
11
+ where `.agents/` rotted while `.claude/` advanced. Regeneration is **offered, never
12
+ auto-applied** — `/upgrading` Step 4b hands the protocol to `quick-tasking`, and a human
13
+ reviews the diff before commit.
14
+
15
+ ## Prerequisites
16
+
17
+ 1. On the 0.41.0 framework files — run `npx forge-orkes upgrade` (or the `/upgrading` skill)
18
+ first, so `.forge/adapters/codex-generation.md` is present.
19
+ 2. Working tree clean or changes committed — regeneration rewrites `.agents/` + `.codex/`.
20
+
21
+ ## Detection
22
+
23
+ Prints `MIGRATE` when a Codex adapter exists but is missing the protocol, carries the broken
24
+ `.Codex` token, or is older than the `.claude/` source. Silent + exit 0 otherwise (including
25
+ Claude-only projects).
26
+
27
+ ```bash
28
+ # Codex adapter present but stale/broken → regenerate via the protocol.
29
+ { [ -d .agents ] || [ -d .codex ]; } || exit 0 # Claude-only → nothing to do
30
+
31
+ # (a) generation protocol not installed yet
32
+ if [ ! -f .forge/adapters/codex-generation.md ]; then
33
+ echo "MIGRATE — Codex adapter present but the generation protocol is missing; sync framework files then regenerate"
34
+ exit 0
35
+ fi
36
+
37
+ # (b) broken: the .Codex corruption token anywhere in the adapter trees
38
+ if grep -rqF '.Codex' .agents .codex 2>/dev/null; then
39
+ echo "MIGRATE — Codex adapter contains the broken .Codex token; regenerate from the protocol"
40
+ exit 0
41
+ fi
42
+
43
+ # (c) stale: newest source skill newer than newest adapter skill
44
+ newest_src=$(ls -t .claude/skills/*/SKILL.md 2>/dev/null | head -1)
45
+ newest_adapter=$(ls -t .agents/skills/*/SKILL.md 2>/dev/null | head -1)
46
+ if [ -n "$newest_src" ] && [ -n "$newest_adapter" ] && [ "$newest_src" -nt "$newest_adapter" ]; then
47
+ echo "MIGRATE — Codex adapter is older than the .claude/ source; regenerate from the protocol"
48
+ exit 0
49
+ fi
50
+ exit 0
51
+ ```
52
+
53
+ ## Migration steps
54
+
55
+ The migration is **never auto-applied**. In Claude Code it runs via the `quick-tasking`
56
+ skill, which `/upgrading` Step 4b hands the protocol to on "yes".
57
+
58
+ ### 1. Ensure the protocol is present
59
+ `.forge/adapters/codex-generation.md` ships with 0.41.0 — confirm `npx forge-orkes upgrade`
60
+ (or `/upgrading`) has synced it.
61
+
62
+ ### 2. Regenerate (Codex-run, agent-authored)
63
+ Run the regeneration **as the Codex runtime**, following `.forge/adapters/codex-generation.md`:
64
+ author `.agents/skills/` + `.codex/` wiring from the `.claude/` source using native
65
+ primitives. Do **not** find-replace; handle the coupled mechanisms (EnterPlanMode, ADR-007
66
+ session-id/claim hook, native Task tools) with judgment per the protocol §3.
67
+
68
+ ### 3. Human review before commit
69
+ Review the diff for fidelity: invariants intact (routing tables, tiers, gate semantics,
70
+ state-ownership, version-reservation, atomic commits), **no `.Codex` token**, coupled
71
+ mechanisms handled. Commit only after review. Lossy steps are confirmed first.
72
+
73
+ ## Validation
74
+
75
+ ```bash
76
+ # No .Codex token survives anywhere in the regenerated adapter
77
+ grep -rqF '.Codex' .agents .codex 2>/dev/null && echo "FAIL: .Codex token still present" || echo "OK: no .Codex token"
78
+ # Every source skill has a corresponding adapter skill dir
79
+ for d in .claude/skills/*/; do n=$(basename "$d"); [ -d ".agents/skills/$n" ] || echo "MISSING adapter skill: $n"; done
80
+ echo "validation complete"
81
+ ```
@@ -0,0 +1,48 @@
1
+ # Migration Guide: ID Reservation Protocol (Forge 0.42.0)
2
+
3
+ Forge 0.42.0 ([ADR-016](https://github.com/Attuned-Media/forge/blob/main/docs/decisions/ADR-016-id-reservation-protocol.md), [forge#10](https://github.com/Attuned-Media/forge/issues/10)) adds a cross-worktree reservation mechanism for sequential IDs — `ADR-NNN`, `DEF-NNN`, `FR-NNN`, `NFR-NNN` — backed by a new append-only file `.forge/reservations.yml` (sibling to `releases.yml`). It closes the silent collision where two streams in parallel worktrees each "scan the tree for the highest + increment" and claim the same number, surfacing only at merge as a multi-file renumber.
4
+
5
+ **Fully backward-compatible / lazy.** Nothing breaks without acting: with no `reservations.yml`, allocation behaves exactly as before (scan the in-tree max), and the first reservation creates the file. This guide just materialises the file so the protocol is active and visible immediately.
6
+
7
+ ## Detection
8
+
9
+ Prints `MIGRATE` when the project has no `.forge/reservations.yml` yet. Silent + exit 0 once it exists.
10
+
11
+ ```bash
12
+ [ -d .forge ] || exit 0 # not a Forge project
13
+ [ -f .forge/reservations.yml ] && exit 0 # already migrated → silent
14
+ echo "MIGRATE — add .forge/reservations.yml to activate the ID Reservation Protocol (ADR/DEF/FR/NFR cross-worktree number reservation)"
15
+ exit 0
16
+ ```
17
+
18
+ ## Migration steps
19
+
20
+ One trivial, safe step — create the starter file. In Claude Code this runs via `quick-tasking`.
21
+
22
+ ### 1. Create `.forge/reservations.yml`
23
+
24
+ Write the append-only starter (header + empty list). It needs no back-fill — the `max(reserved, in-tree)` allocation rule already respects every ADR/DEF/FR/NFR already landed in the tree, so existing IDs stay valid without being listed.
25
+
26
+ ```bash
27
+ cat > .forge/reservations.yml <<'YML'
28
+ # Forge ID Reservations — cross-worktree coordination for ADR / DEF / FR / NFR numbers
29
+ # Reserve BEFORE allocating: append one entry, commit + push, THEN create the artifact.
30
+ # Next number for a kind = max(highest reserved here, highest in-tree) + 1. kind ∈ {adr,def,fr,nfr}.
31
+ # Append-only; never edit prior entries. See FORGE.md → ID Reservation Protocol + ADR-016.
32
+ reservations: []
33
+ YML
34
+ git add .forge/reservations.yml
35
+ git commit -m "chore(forge): add reservations.yml (ID Reservation Protocol, 0.42.0)"
36
+ ```
37
+
38
+ ### 2. From now on, reserve before allocating
39
+
40
+ When `architecting` files a new ADR or `planning` writes a new FR/NFR/DEF, append `{kind, id, milestone, reserved_at, summary}` to `.forge/reservations.yml`, commit + push it, *then* create the artifact. The skills (0.42.0) prompt this automatically.
41
+
42
+ ## Validation
43
+
44
+ ```bash
45
+ [ -f .forge/reservations.yml ] && echo "OK: reservations.yml present" || echo "FAIL: missing"
46
+ grep -q '^reservations:' .forge/reservations.yml && echo "OK: has reservations key" || echo "FAIL: malformed"
47
+ echo "validation complete"
48
+ ```
@@ -0,0 +1,31 @@
1
+ # Forge ID Reservations — cross-worktree coordination for ADR / DEF / FR / NFR numbers
2
+ #
3
+ # PURPOSE: remove sequential ID numbers from per-stream scope so two streams
4
+ # running in parallel worktrees never silently claim the same ADR/DEF/FR/NFR
5
+ # number (a collision that otherwise only surfaces at merge as a multi-file
6
+ # renumber). Sibling to releases.yml, which does the same for version numbers.
7
+ # See ADR-016 + FORGE.md → ID Reservation Protocol.
8
+ #
9
+ # COMMITTED + APPEND-ONLY. Reserve BEFORE allocating: append exactly one entry,
10
+ # commit + push it, THEN create the ADR file / write the DEF/FR/NFR. Appending —
11
+ # never editing prior entries — is what keeps parallel worktrees from conflicting
12
+ # (distinct lines, no contention). A worktree pulls and sees taken numbers.
13
+ #
14
+ # RULE: next free number for a kind = max(highest reserved here for that kind,
15
+ # highest actually in-tree for that kind) + 1. Taking the max of both keeps it
16
+ # backward-compatible — landed IDs that predate this file are still respected.
17
+ #
18
+ # RACE: if two worktrees reserve near-simultaneously, the second push rebases
19
+ # onto the first (append-only → clean), re-reads max(), takes the next number.
20
+ #
21
+ # LAZY MIGRATION: absent ⇒ allocate as before (scan in-tree max). This file
22
+ # materialises on the first reservation. kind ∈ {adr, def, fr, nfr}.
23
+ #
24
+ # reservations:
25
+ # - kind: adr
26
+ # id: ADR-036
27
+ # milestone: m-PLUG01
28
+ # reserved_at: "2026-06-26"
29
+ # summary: "Plugin-host scanner architecture"
30
+
31
+ reservations: []