forge-orkes 0.41.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.
- package/bin/create-forge.js +107 -42
- package/package.json +5 -1
- package/template/.claude/agents/tester.md +3 -5
- package/template/.claude/skills/architecting/SKILL.md +2 -0
- package/template/.claude/skills/chief-of-staff/SKILL.md +23 -26
- package/template/.claude/skills/forge/SKILL.md +3 -26
- package/template/.claude/skills/forge/desire-paths-review.md +37 -0
- package/template/.claude/skills/planning/SKILL.md +5 -3
- package/template/.claude/skills/testing/SKILL.md +3 -3
- package/template/.claude/skills/verifying/SKILL.md +1 -1
- package/template/.forge/FORGE.md +20 -5
- package/template/.forge/migrations/0.20.0-nested-phase-layout.md +10 -2
- package/template/.forge/migrations/0.42.0-id-reservation.md +48 -0
- package/template/.forge/reservations.yml +31 -0
package/bin/create-forge.js
CHANGED
|
@@ -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');
|
|
@@ -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
|
-
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
|
@@ -358,7 +357,9 @@ async function install() {
|
|
|
358
357
|
|
|
359
358
|
console.log(`\n Forge v${pkgVersion} is ready. Start with: /forge\n`);
|
|
360
359
|
|
|
361
|
-
// Presence-gated:
|
|
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.
|
|
362
363
|
noticeCodexAdapter();
|
|
363
364
|
}
|
|
364
365
|
|
|
@@ -442,14 +443,19 @@ function runPostUpgradeMigrationChecks(installedVersion) {
|
|
|
442
443
|
|
|
443
444
|
let stdout = '';
|
|
444
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`.
|
|
445
450
|
stdout = execSync(script, {
|
|
446
451
|
cwd: targetDir,
|
|
447
452
|
shell: '/bin/bash',
|
|
448
453
|
encoding: 'utf-8',
|
|
449
454
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
455
|
+
timeout: 10000, // 10s — a pathological guide throws (caught below) → no-op, loop continues
|
|
450
456
|
});
|
|
451
457
|
} catch {
|
|
452
|
-
// 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.
|
|
453
459
|
continue;
|
|
454
460
|
}
|
|
455
461
|
|
|
@@ -476,6 +482,26 @@ function runPostUpgradeMigrationChecks(installedVersion) {
|
|
|
476
482
|
}
|
|
477
483
|
}
|
|
478
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
|
+
|
|
479
505
|
async function upgrade() {
|
|
480
506
|
console.log('\n Forge Upgrade\n');
|
|
481
507
|
|
|
@@ -524,7 +550,9 @@ async function upgrade() {
|
|
|
524
550
|
preserved: [],
|
|
525
551
|
};
|
|
526
552
|
|
|
527
|
-
// 1. Process framework-owned directories (
|
|
553
|
+
// 1. Process framework-owned directories (stale files are collected for a
|
|
554
|
+
// confirmed removal pass below, not deleted inline — R066a).
|
|
555
|
+
const pendingRemovals = [];
|
|
528
556
|
for (const dir of FRAMEWORK_OWNED_DIRS) {
|
|
529
557
|
const dirResult = upgradeDir(dir, { autoClean: true });
|
|
530
558
|
results.updated.push(...dirResult.updated);
|
|
@@ -532,6 +560,7 @@ async function upgrade() {
|
|
|
532
560
|
results.unchanged.push(...dirResult.unchanged);
|
|
533
561
|
results.removed.push(...dirResult.removed);
|
|
534
562
|
results.preserved.push(...dirResult.preserved);
|
|
563
|
+
pendingRemovals.push(...dirResult.removeCandidates);
|
|
535
564
|
}
|
|
536
565
|
|
|
537
566
|
// 2. Process template-only directories
|
|
@@ -544,41 +573,59 @@ async function upgrade() {
|
|
|
544
573
|
results.preserved.push(...dirResult.preserved);
|
|
545
574
|
}
|
|
546
575
|
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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';
|
|
561
590
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
if (oldContent !== newContent) {
|
|
574
|
-
fs.mkdirSync(path.dirname(fmDest), { recursive: true });
|
|
575
|
-
fs.writeFileSync(fmDest, newContent);
|
|
576
|
-
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
|
+
}
|
|
577
602
|
} else {
|
|
578
|
-
|
|
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
|
+
}
|
|
579
607
|
}
|
|
608
|
+
console.log();
|
|
580
609
|
}
|
|
581
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
|
+
|
|
582
629
|
// 3. Guarantee the CLAUDE.md import line; auto-migrate any legacy embedded section.
|
|
583
630
|
const claudeStatus = ensureClaudeMdImport();
|
|
584
631
|
if (claudeStatus === 'migrated') {
|
|
@@ -721,6 +768,24 @@ function noticeWorktreeLag() {
|
|
|
721
768
|
|
|
722
769
|
// --- Entry point ---
|
|
723
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
|
+
|
|
724
789
|
const subcommand = process.argv[2];
|
|
725
790
|
|
|
726
791
|
if (subcommand === 'upgrade') {
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-orkes",
|
|
3
|
-
"version": "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 |
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
102
|
-
`/forge` pass if it looks off. The Chief writes `active.yml`
|
|
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
|
-
- `
|
|
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
|
-
`
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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.
|
|
@@ -54,7 +54,7 @@ Cross-machine note: the flag file is machine-local, so a worktree on another lap
|
|
|
54
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`.)"*
|
|
55
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.
|
|
56
56
|
|
|
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`. `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.
|
|
58
58
|
|
|
59
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.
|
|
60
60
|
|
|
@@ -125,7 +125,7 @@ Single-owner-mutable state must be written by exactly one checkout. When this se
|
|
|
125
125
|
Resolve the worktree path from the recorded `lifecycle.worktree_path` / `runtime.worktree`, or — if absent — from the base **Worktree Convention** (FORGE.md).
|
|
126
126
|
|
|
127
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]"*.
|
|
128
|
-
- **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.)
|
|
129
129
|
- **keep** → leave it; impose no write-block — a `complete` milestone has no live cursor left to fork.
|
|
130
130
|
|
|
131
131
|
The gate (below) and this finalize path are mutually exclusive on `current.status`: `complete` → finalize; any other status with a live worktree → gate.
|
|
@@ -185,30 +185,7 @@ Downstream skills (researching, discussing, planning, executing, verifying, revi
|
|
|
185
185
|
|
|
186
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.
|
|
187
187
|
|
|
188
|
-
**Surface at 3+.** For each remaining group with 3+ files,
|
|
189
|
-
|
|
190
|
-
| Type | Suggested evolution |
|
|
191
|
-
|------|---------------------|
|
|
192
|
-
| `deviation_pattern` | Add a pre-check to planning, or a new constitutional article |
|
|
193
|
-
| `tier_override` | Adjust tier-detection heuristics in the forge skill |
|
|
194
|
-
| `skipped_step` | Make the step optional, or merge it into another |
|
|
195
|
-
| `recurring_friction` | Add guidance to the relevant skill, or create a template |
|
|
196
|
-
| `agent_struggle` | Add examples or anti-patterns to the relevant skill |
|
|
197
|
-
| `user_correction` | Add a rule to constitution / context.md / the relevant skill |
|
|
198
|
-
|
|
199
|
-
Prompt: *"Recurring: [{note}] ({N}x, scope:{scope}). Fix: [concrete suggestion from table]. Apply / decline?"*
|
|
200
|
-
|
|
201
|
-
- **Agree** → apply the fix; archive the group's files to `.forge/state/desire-paths/resolved/`.
|
|
202
|
-
- **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.)
|
|
203
|
-
- (Occurrence count is derived from file count — there is no counter to reset.)
|
|
204
|
-
|
|
205
|
-
**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:
|
|
206
|
-
|
|
207
|
-
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.
|
|
208
|
-
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.
|
|
209
|
-
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/`.
|
|
210
|
-
|
|
211
|
-
`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.)
|
|
212
189
|
|
|
213
190
|
### 1.3 Interface Check
|
|
214
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**
|
|
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...
|
|
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** —
|
|
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
|
-
|
|
56
|
-
|
|
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.
|
|
@@ -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
|
|
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**:
|
package/template/.forge/FORGE.md
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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,8 +250,9 @@ 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`
|
|
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
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
|
|
|
@@ -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`.
|
|
@@ -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
|
-
|
|
125
|
-
id=${
|
|
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,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: []
|