forge-orkes 0.29.0 → 0.31.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 +94 -118
- package/package.json +1 -1
- package/template/.claude/skills/executing/SKILL.md +16 -0
- package/template/.claude/skills/forge/SKILL.md +27 -3
- package/template/.claude/skills/planning/SKILL.md +10 -1
- package/template/.claude/skills/quick-tasking/SKILL.md +2 -0
- package/template/.claude/skills/upgrading/SKILL.md +31 -198
- package/template/.claude/skills/verifying/SKILL.md +3 -17
- package/template/.forge/FORGE.md +1 -1
- package/template/.forge/migrations/0.10.0-per-milestone-requirements.md +10 -6
- package/template/.forge/migrations/0.17.0-cross-layer-contracts.md +7 -7
- package/template/.forge/migrations/0.19.0-worktree-safe-state.md +9 -4
- package/template/.forge/migrations/0.20.0-nested-phase-layout.md +8 -4
- package/template/.forge/migrations/0.22.0-backlog-compaction.md +12 -8
- package/template/.forge/migrations/0.28.0-worktree-root.md +5 -5
- package/template/.forge/migrations/0.29.0-shared-state-taxonomy.md +7 -2
- package/template/.forge/templates/migration-guide.md +110 -0
- package/template/.forge/templates/project.yml +8 -0
- package/template/.forge/templates/state/desire-path.yml +18 -3
package/bin/create-forge.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const readline = require('readline');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
6
7
|
|
|
7
8
|
const templateDir = path.join(__dirname, '..', 'template');
|
|
8
9
|
const targetDir = process.cwd();
|
|
@@ -349,144 +350,117 @@ async function install() {
|
|
|
349
350
|
}
|
|
350
351
|
|
|
351
352
|
/**
|
|
352
|
-
*
|
|
353
|
-
*
|
|
353
|
+
* Extract the first fenced ```bash block under a guide's `## Detection` heading.
|
|
354
|
+
* Returns the script string, or null if the guide has no such block (e.g. a
|
|
355
|
+
* non-conformant guide). State-machine parse — no markdown dependency.
|
|
354
356
|
*/
|
|
355
|
-
function
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
legacy.push(path.join('.forge', entry.name));
|
|
365
|
-
} else if (/^requirements-m\d+\.yml$/.test(entry.name)) {
|
|
366
|
-
legacy.push(path.join('.forge', entry.name));
|
|
357
|
+
function extractDetectionBlock(guideContent) {
|
|
358
|
+
const lines = guideContent.split('\n');
|
|
359
|
+
let inDetection = false;
|
|
360
|
+
let inCode = false;
|
|
361
|
+
const code = [];
|
|
362
|
+
for (const line of lines) {
|
|
363
|
+
if (!inDetection) {
|
|
364
|
+
if (/^##\s+Detection\b/.test(line)) inDetection = true;
|
|
365
|
+
continue;
|
|
367
366
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (!phase.isDirectory()) continue;
|
|
374
|
-
const reqPath = path.join(phasesDir, phase.name, 'requirements.yml');
|
|
375
|
-
if (fs.existsSync(reqPath)) {
|
|
376
|
-
legacy.push(path.join('.forge', 'phases', phase.name, 'requirements.yml'));
|
|
377
|
-
}
|
|
367
|
+
// Stop scanning if we hit the next top-level (## ...) heading before a block.
|
|
368
|
+
if (!inCode && /^##\s/.test(line) && !/^##\s+Detection\b/.test(line)) break;
|
|
369
|
+
if (!inCode) {
|
|
370
|
+
if (/^```bash\s*$/.test(line)) inCode = true;
|
|
371
|
+
continue;
|
|
378
372
|
}
|
|
373
|
+
if (/^```/.test(line)) break; // closing fence
|
|
374
|
+
code.push(line);
|
|
379
375
|
}
|
|
380
|
-
|
|
381
|
-
return legacy;
|
|
376
|
+
return code.length ? code.join('\n') : null;
|
|
382
377
|
}
|
|
383
378
|
|
|
384
379
|
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
380
|
+
* Data-driven post-upgrade migration check. Loops over the migration guides
|
|
381
|
+
* synced into the project this run (.forge/migrations/{v}-*.md), runs each
|
|
382
|
+
* guide's `## Detection` block, and on a `MIGRATE` stdout hit prints a warning
|
|
383
|
+
* with the exact guide pointer. There are NO per-version code branches: a new
|
|
384
|
+
* guide that ships with a conformant Detection block is auto-covered.
|
|
385
|
+
*
|
|
386
|
+
* Range: installed < v <= source. `installedVersion` is the project's
|
|
387
|
+
* settings.json forge.version captured BEFORE the version stamp; `source` is
|
|
388
|
+
* pkgVersion (this package's package.json) — never the template settings.json
|
|
389
|
+
* literal, which is a placeholder the installer stamps at write time. Non-
|
|
390
|
+
* interactive: the bin only warns and points at the guide; it never applies.
|
|
390
391
|
*/
|
|
391
|
-
function
|
|
392
|
-
const
|
|
393
|
-
if (!fs.existsSync(
|
|
394
|
-
|
|
395
|
-
let content;
|
|
396
|
-
try {
|
|
397
|
-
content = fs.readFileSync(projectYml, 'utf-8');
|
|
398
|
-
} catch {
|
|
399
|
-
return false;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Already declared a layers: key (any value, including []) → migrated.
|
|
403
|
-
if (/^layers:/m.test(content)) return false;
|
|
404
|
-
// A seeded contract index implies layers were declared at init → migrated.
|
|
405
|
-
if (fs.existsSync(path.join(targetDir, '.forge', 'contracts', 'index.yml'))) return false;
|
|
392
|
+
function runPostUpgradeMigrationChecks(installedVersion) {
|
|
393
|
+
const migrationsDir = path.join(targetDir, '.forge', 'migrations');
|
|
394
|
+
if (!fs.existsSync(migrationsDir)) return;
|
|
406
395
|
|
|
407
|
-
|
|
408
|
-
|
|
396
|
+
const installed =
|
|
397
|
+
installedVersion && installedVersion !== 'unknown' ? installedVersion : '0.0.0';
|
|
398
|
+
const source = pkgVersion;
|
|
409
399
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
* registry (regenerated by rollup) and moves desire_paths to append-only files.
|
|
413
|
-
* A legacy index.yml still carries desire_paths:/metrics:/embedded narrative, or
|
|
414
|
-
* is large. Returns true if index.yml exists and looks legacy; false if missing
|
|
415
|
-
* (not yet initialized) or already a slim registry.
|
|
416
|
-
*/
|
|
417
|
-
function detectLegacyStateIndex() {
|
|
418
|
-
const indexYml = path.join(targetDir, '.forge', 'state', 'index.yml');
|
|
419
|
-
if (!fs.existsSync(indexYml)) return false;
|
|
420
|
-
|
|
421
|
-
let content;
|
|
400
|
+
// Select in-range guides: installed < v <= source. Parse {v} from the filename.
|
|
401
|
+
let guides;
|
|
422
402
|
try {
|
|
423
|
-
|
|
403
|
+
guides = fs
|
|
404
|
+
.readdirSync(migrationsDir)
|
|
405
|
+
.map((name) => {
|
|
406
|
+
const m = name.match(/^(\d+\.\d+\.\d+)-.*\.md$/);
|
|
407
|
+
return m ? { name, version: m[1] } : null;
|
|
408
|
+
})
|
|
409
|
+
.filter(Boolean)
|
|
410
|
+
.filter(
|
|
411
|
+
(g) =>
|
|
412
|
+
compareVersions(installed, g.version) < 0 &&
|
|
413
|
+
compareVersions(g.version, source) <= 0
|
|
414
|
+
)
|
|
415
|
+
.sort((a, b) => compareVersions(a.version, b.version));
|
|
424
416
|
} catch {
|
|
425
|
-
return
|
|
417
|
+
return;
|
|
426
418
|
}
|
|
427
419
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
420
|
+
for (const guide of guides) {
|
|
421
|
+
const guidePath = path.join(migrationsDir, guide.name);
|
|
422
|
+
let script;
|
|
423
|
+
try {
|
|
424
|
+
script = extractDetectionBlock(fs.readFileSync(guidePath, 'utf-8'));
|
|
425
|
+
} catch {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (!script) continue; // no runnable Detection block → treat as no-op
|
|
429
|
+
|
|
430
|
+
let stdout = '';
|
|
431
|
+
try {
|
|
432
|
+
stdout = execSync(script, {
|
|
433
|
+
cwd: targetDir,
|
|
434
|
+
shell: '/bin/bash',
|
|
435
|
+
encoding: 'utf-8',
|
|
436
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
437
|
+
});
|
|
438
|
+
} catch {
|
|
439
|
+
// A non-zero exit or detection error is a no-op, not a failure — keep going.
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!/MIGRATE/.test(stdout)) continue; // no-op
|
|
432
444
|
|
|
433
|
-
|
|
434
|
-
|
|
445
|
+
const reasonLine = stdout
|
|
446
|
+
.split('\n')
|
|
447
|
+
.find((l) => l.includes('MIGRATE'));
|
|
448
|
+
const reason = reasonLine
|
|
449
|
+
? reasonLine.replace(/^.*MIGRATE\s*[—-]?\s*/, '').trim()
|
|
450
|
+
: '';
|
|
435
451
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
function runPostUpgradeMigrationChecks() {
|
|
443
|
-
const legacyReqs = detectLegacyRequirementsLayout();
|
|
444
|
-
if (legacyReqs.length > 0) {
|
|
445
|
-
console.log(' ⚠ Pre-0.10.0 requirements layout detected');
|
|
446
|
-
console.log(' ─────────────────────────────────────────');
|
|
447
|
-
console.log(' Found:');
|
|
448
|
-
for (const p of legacyReqs) {
|
|
449
|
-
console.log(` ${p}`);
|
|
450
|
-
}
|
|
451
|
-
console.log();
|
|
452
|
-
console.log(' Forge 0.10.0+ uses per-milestone files at .forge/requirements/m{N}.yml.');
|
|
453
|
-
console.log(' Migration guide: .forge/migrations/0.10.0-per-milestone-requirements.md');
|
|
452
|
+
console.log(` ⚠ ${guide.version} migration available${reason ? ` — ${reason}` : ''}`);
|
|
453
|
+
console.log(' ─────────────────────────────────────────────────────────────');
|
|
454
|
+
console.log(` Guide: .forge/migrations/${guide.name}`);
|
|
455
|
+
console.log(
|
|
456
|
+
` (canonical: https://github.com/Attuned-Media/forge/blob/main/docs/migrations/${guide.name})`
|
|
457
|
+
);
|
|
454
458
|
console.log();
|
|
455
|
-
console.log('
|
|
459
|
+
console.log(' Migrations are never applied automatically. In Claude Code:');
|
|
456
460
|
console.log(' /forge then hand the guide to quick-tasking, or invoke Skill(quick-tasking)');
|
|
457
461
|
console.log(' with the guide path as the task definition.');
|
|
458
462
|
console.log();
|
|
459
463
|
}
|
|
460
|
-
|
|
461
|
-
if (detectMissingLayersConfig()) {
|
|
462
|
-
console.log(' ⚠ Cross-layer contracts: project.yml has no `layers:` field (Forge 0.17.0)');
|
|
463
|
-
console.log(' ──────────────────────────────────────────────────────────────────────');
|
|
464
|
-
console.log(' Forge 0.17.0 added cross-layer contract detection (planning Step 6.1),');
|
|
465
|
-
console.log(' which reads `layers:` from .forge/project.yml. Your project.yml predates');
|
|
466
|
-
console.log(' this field, so planning falls back to a coarse top-level-directory heuristic.');
|
|
467
|
-
console.log();
|
|
468
|
-
console.log(' Migration guide: .forge/migrations/0.17.0-cross-layer-contracts.md');
|
|
469
|
-
console.log();
|
|
470
|
-
console.log(' Quick fix: add a `layers:` list to .forge/project.yml — or `layers: []`');
|
|
471
|
-
console.log(' for a single-layer project to silence this notice. In Claude Code:');
|
|
472
|
-
console.log(' /forge then hand the guide to quick-tasking.');
|
|
473
|
-
console.log();
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (detectLegacyStateIndex()) {
|
|
477
|
-
console.log(' ⚠ Worktree-safe state: legacy state/index.yml detected (Forge 0.19.0)');
|
|
478
|
-
console.log(' ────────────────────────────────────────────────────────────────────');
|
|
479
|
-
console.log(' Forge 0.19.0 makes index.yml a derived registry (regenerated by rollup),');
|
|
480
|
-
console.log(' moves desire_paths to append-only files, and commits .forge/ state at every');
|
|
481
|
-
console.log(' phase handoff. Your index.yml predates this — it still carries desire_paths/');
|
|
482
|
-
console.log(' metrics or embedded narrative, which conflicts across git worktrees.');
|
|
483
|
-
console.log();
|
|
484
|
-
console.log(' Migration guide: .forge/migrations/0.19.0-worktree-safe-state.md');
|
|
485
|
-
console.log();
|
|
486
|
-
console.log(' This migration edits user-owned state, so it is guided — never automatic.');
|
|
487
|
-
console.log(' In Claude Code: /forge then hand the guide to quick-tasking.');
|
|
488
|
-
console.log();
|
|
489
|
-
}
|
|
490
464
|
}
|
|
491
465
|
|
|
492
466
|
async function upgrade() {
|
|
@@ -656,7 +630,9 @@ async function upgrade() {
|
|
|
656
630
|
|
|
657
631
|
console.log(` Upgraded to v${pkgVersion}\n`);
|
|
658
632
|
|
|
659
|
-
|
|
633
|
+
// Pass the version captured BEFORE upgradeSettings() stamped the new one, so the
|
|
634
|
+
// migration range is (installed, source] against the freshly-synced guides.
|
|
635
|
+
runPostUpgradeMigrationChecks(installedVersion);
|
|
660
636
|
}
|
|
661
637
|
|
|
662
638
|
// --- Entry point ---
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ description: "Build to plan with atomic commits, deviation rules, and context en
|
|
|
11
11
|
- [ ] Constitution.md gates satisfied
|
|
12
12
|
- [ ] Milestone state updated to `status: executing`
|
|
13
13
|
- [ ] Baseline snapshot captured (see below)
|
|
14
|
+
- [ ] Plan anchors re-derived against HEAD (see below)
|
|
14
15
|
|
|
15
16
|
## Baseline Snapshot
|
|
16
17
|
|
|
@@ -23,6 +24,19 @@ Run **before the first task begins**. Makes failure causality mechanical — no
|
|
|
23
24
|
|
|
24
25
|
Skip only if re-entering an in-progress execution and `deferred-issues.md` already documents all current failures.
|
|
25
26
|
|
|
27
|
+
## Plan Anchor Re-Derivation
|
|
28
|
+
|
|
29
|
+
Run **once, before the first task** (alongside the baseline snapshot). Plans snapshot concrete codebase literals — symbol names, `file:line` references, contract/type shapes, dependency versions — at planning time. Between planning and execution the codebase moves, and a stale literal silently misdirects the task ("plan said v5, HEAD is v6").
|
|
30
|
+
|
|
31
|
+
Re-derive each anchor the plan names against current HEAD:
|
|
32
|
+
|
|
33
|
+
1. Scan plan `<files>`, `<action>`, `must_haves`, and any `contract.md` for concrete literals: symbol/function names, `path:line` references, contract shapes, pinned versions.
|
|
34
|
+
2. Resolve each against HEAD — grep the symbol, open the file, read the installed version.
|
|
35
|
+
3. **Drifted** (symbol moved/renamed, line shifted, version bumped, shape changed) → reconcile the plan text to the live value *before* acting. Reconciled here, not discovered mid-task.
|
|
36
|
+
4. **Gone** (symbol/file deleted, contract removed) → not a drift but a broken planning assumption → **STOP under Rule 4**; the slice may no longer hold.
|
|
37
|
+
|
|
38
|
+
Cheap pass, high leverage. Skip only when re-entering an in-progress execution already past its first task.
|
|
39
|
+
|
|
26
40
|
## Deviation Rules
|
|
27
41
|
|
|
28
42
|
**Full definitions:** `.claude/agents/executor.md`. Decision order: **Rule 4 first** (architectural → STOP, ask user), then Rule 1 (bugs), Rule 2 (critical gaps), Rule 3 (infra blockers). Uncertain → Rule 4.
|
|
@@ -265,6 +279,8 @@ Append **one file per observation** to `.forge/state/desire-paths/` — copy `.f
|
|
|
265
279
|
- **User corrections**: Repeated correction matching a prior one → `type: user_correction`
|
|
266
280
|
- **Agent struggles**: Multiple attempts or user guidance needed → `type: agent_struggle`
|
|
267
281
|
|
|
282
|
+
**Set `scope` at capture** (see ADR-012): `framework` when the fix this signal implies targets Forge's own skills/templates (a skill's check, a plan/tier heuristic, a template); `project` when it targets user-owned files (`constitution.md`, `design-system.md`, `context.md`, project setup). Record `suggestion_target` when you know the file/skill it touches. `scope: framework` is what later lets `forge` review route the signal upstream instead of trapping it in this project.
|
|
283
|
+
|
|
268
284
|
## Cross-Layer Seam Check
|
|
269
285
|
|
|
270
286
|
**Trigger:** the phase was split by planning Step 6.1 into a **Tier-2** producer plan-NNa + consumer plan-NNb (both carry a `contract:` frontmatter path pointing at the same `contract.md`). Single-plan / Tier-1 (`layer:` tag, no split, contract honored inline) → skip; no seam check needed.
|
|
@@ -122,11 +122,35 @@ backlog pickup → effort: standard
|
|
|
122
122
|
|
|
123
123
|
Downstream skills (researching, discussing, planning, executing, verifying, reviewing) see a normal milestone — no special branching.
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
**Desire-path review — `forge` boot is the SINGLE review owner** (verifying *captures* but does not surface; see ADR-012). Glob `.forge/state/desire-paths/*.yml`, group by `type` + normalized `note`, count each group. Read each file's `scope` (absent ⇒ `project`).
|
|
126
|
+
|
|
127
|
+
**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.
|
|
128
|
+
|
|
129
|
+
**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):
|
|
130
|
+
|
|
131
|
+
| Type | Suggested evolution |
|
|
132
|
+
|------|---------------------|
|
|
133
|
+
| `deviation_pattern` | Add a pre-check to planning, or a new constitutional article |
|
|
134
|
+
| `tier_override` | Adjust tier-detection heuristics in the forge skill |
|
|
135
|
+
| `skipped_step` | Make the step optional, or merge it into another |
|
|
136
|
+
| `recurring_friction` | Add guidance to the relevant skill, or create a template |
|
|
137
|
+
| `agent_struggle` | Add examples or anti-patterns to the relevant skill |
|
|
138
|
+
| `user_correction` | Add a rule to constitution / context.md / the relevant skill |
|
|
139
|
+
|
|
140
|
+
Prompt: *"Recurring: [{note}] ({N}x, scope:{scope}). Fix: [concrete suggestion from table]. Apply / decline?"*
|
|
141
|
+
|
|
142
|
+
- **Agree** → apply the fix; archive the group's files to `.forge/state/desire-paths/resolved/`.
|
|
143
|
+
- **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.)
|
|
128
144
|
- (Occurrence count is derived from file count — there is no counter to reset.)
|
|
129
145
|
|
|
146
|
+
**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:
|
|
147
|
+
|
|
148
|
+
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.
|
|
149
|
+
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.
|
|
150
|
+
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/`.
|
|
151
|
+
|
|
152
|
+
`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.
|
|
153
|
+
|
|
130
154
|
### 1.3 Interface Check
|
|
131
155
|
|
|
132
156
|
Check `interface` in `project.yml`:
|
|
@@ -156,7 +156,7 @@ Before decomposing, classify whether this phase crosses a layer boundary with a
|
|
|
156
156
|
**Tier-2 ratify gate** (the ONLY interruption; frame as contract-correctness, not "parallelize y/n"):
|
|
157
157
|
> *"This phase changes the {integration point} contract ({governing ADR}). Delta: [summary]. plan-NNa ({producer}) pins it; plan-NNb ({consumer}) builds against it in parallel. Is this contract shape correct?"*
|
|
158
158
|
|
|
159
|
-
Block the split until confirmed. Override ("keep it one plan") -> append a `type: tier_override` file to `.forge/state/desire-paths/` (
|
|
159
|
+
Block the split until confirmed. Override ("keep it one plan") -> append a `type: tier_override` file to `.forge/state/desire-paths/` with `scope: framework` (the fix would tune Forge's own Tier-2 threshold/heuristics — see ADR-012; recurring overrides route upstream), fall back to Tier 1.
|
|
160
160
|
|
|
161
161
|
**Integration (Tier 2):** layer plans build isolated (per-layer worktrees). The phase's final task is a **seam check** owned by the executing flow (NOT a standing agent): merge the layer branches, verify the shape the producer emits matches what the consumer built against, per `contract.md`.
|
|
162
162
|
|
|
@@ -196,6 +196,15 @@ Plan naming reflects the slice: `plan-01-user-signs-up.md`, not `plan-01-models.
|
|
|
196
196
|
</task>
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
+
#### Plan Anchors (drift-resistant references)
|
|
200
|
+
|
|
201
|
+
Tasks reference concrete code — symbols, paths, line numbers, contract shapes, versions. These are **anchors**: snapshotted at planning time, relied on at execution time, *after* the codebase has moved. Make them re-derivable instead of fragile:
|
|
202
|
+
|
|
203
|
+
- Reference by **symbol/intent** (`the signup handler in src/api/signup.ts`), not by bare line number (`src/api/signup.ts:42`). Line numbers drift on the next edit; symbols survive refactors.
|
|
204
|
+
- For a version or contract shape that *must* be pinned, state the literal **and** its source (`react 18 — per package.json at plan time`) so executing can re-derive it against HEAD.
|
|
205
|
+
|
|
206
|
+
Executing re-derives all anchors against HEAD before the first task (see executing → Plan Anchor Re-Derivation). Planning's job is to make that possible: name the symbol, cite the source.
|
|
207
|
+
|
|
199
208
|
### Task Sizing
|
|
200
209
|
|
|
201
210
|
| Duration | Action |
|
|
@@ -60,6 +60,8 @@ Example: `fix(docs): correct typo in API reference`
|
|
|
60
60
|
### 6. Done
|
|
61
61
|
No verification ceremony. Move on.
|
|
62
62
|
|
|
63
|
+
**Desire-path check (only when `.forge/state/desire-paths/` exists).** A Quick-tier streak (`quick-tasking → commit → done`) never boots `forge` or runs `verifying`, so recurring signals would pile up unreviewed. Cheap catch: glob `.forge/state/desire-paths/*.yml`, group by `type` + normalized `note`, and if any group hits 3+ (and has no decline file under `declined/`), surface it using **forge Step 1.2's review procedure** (the canonical table + Apply/decline + decline-ledger live there — do not duplicate them). One glob, surface if 3+, done. Standalone (no forge state) → skip.
|
|
64
|
+
|
|
63
65
|
## Milestone Context
|
|
64
66
|
|
|
65
67
|
Quick-tasking handles two contexts. Existing workflow steps (Identify → Validate → Execute → Test → Commit → Done) apply in both — this section adds state management around them.
|
|
@@ -18,6 +18,16 @@ Check if `.forge/dev-source` exists in the project root.
|
|
|
18
18
|
|
|
19
19
|
Template directory: `{source}/packages/create-forge/template/`.
|
|
20
20
|
|
|
21
|
+
### Harvest upstream desire paths (advisory)
|
|
22
|
+
|
|
23
|
+
`upgrading` is the one skill that bridges this project and a local Forge clone (`{source}`), so it harvests framework-scope desire paths the project has queued (ADR-012). After resolving `{source}`:
|
|
24
|
+
|
|
25
|
+
1. Glob this project's `.forge/state/desire-paths/upstream/*.md`. None → skip silently.
|
|
26
|
+
2. Any present → list them and offer to copy each into `{source}/docs/desire-paths-inbox/` (create the dir if missing) — a **tracked** location in the Forge repo where it won't be lost on machine swap.
|
|
27
|
+
3. On copy → move the project-side block to `.forge/state/desire-paths/upstream/harvested/` so it isn't re-offered. Report counts like other steps.
|
|
28
|
+
|
|
29
|
+
Advisory — never blocks the sync. This is the **convenience tier**: the primary close is the GH issue offered by `forge` review at capture time. Harvest just catches anything written when `gh`/the repo URL wasn't available.
|
|
30
|
+
|
|
21
31
|
### Downgrade guard
|
|
22
32
|
|
|
23
33
|
Read the source version (`{source}/packages/create-forge/package.json` `version`) and the installed version (`.claude/settings.json` `forge.version`). **If source < installed, STOP** — do not sync. Report: *"Refusing to downgrade: installed v{installed} is newer than source v{source}. Point dev-source at a newer checkout, or confirm an intentional downgrade."* Only proceed on explicit user override. (Rolling backward overwrites newer framework files and deletes files newer versions added — and with `.claude/` often gitignored, it is unrecoverable.)
|
|
@@ -102,215 +112,38 @@ Unchanged: {N} files
|
|
|
102
112
|
|
|
103
113
|
The `Migrated:` line appears only when a legacy marker/`# Forge` section was extracted this run; `Restored:` only when a missing import line was re-appended. A steady-state upgrade lists `CLAUDE.md` under Unchanged.
|
|
104
114
|
|
|
105
|
-
## Step 7: Post-Upgrade Migration Checks
|
|
106
|
-
|
|
107
|
-
After sync completes, detect legacy file layouts that the new framework version no longer writes but may still read in compatibility mode. For each match, surface a migration prompt — do not auto-migrate.
|
|
108
|
-
|
|
109
|
-
### Pre-0.10.0 requirements layout
|
|
110
|
-
|
|
111
|
-
Run from project root:
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
find .forge -maxdepth 2 -type f \( -name "requirements.yml" -o -name "requirements-m*.yml" \) \
|
|
115
|
-
| grep -v "^.forge/templates/" \
|
|
116
|
-
| grep -v "^.forge/requirements/"
|
|
117
|
-
|
|
118
|
-
find .forge/phases -type f -name "requirements.yml" 2>/dev/null
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
If either command returns matches, surface:
|
|
122
|
-
|
|
123
|
-
```
|
|
124
|
-
Pre-0.10.0 requirements layout detected
|
|
125
|
-
───────────────────────────────────────
|
|
126
|
-
Found: {list of legacy paths}
|
|
127
|
-
|
|
128
|
-
Forge 0.10.0 uses per-milestone files at .forge/requirements/m{N}.yml.
|
|
129
|
-
A migration guide is available at: .forge/migrations/0.10.0-per-milestone-requirements.md
|
|
130
|
-
(installed alongside Forge; canonical copy at
|
|
131
|
-
https://github.com/Attuned-Media/forge/blob/main/docs/migrations/0.10.0-per-milestone-requirements.md)
|
|
132
|
-
|
|
133
|
-
Run the migration now? (yes/no/show guide)
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
- **yes** → invoke `quick-tasking` skill, hand it the migration guide as the task definition
|
|
137
|
-
- **show guide** → read and display the file, then re-ask
|
|
138
|
-
- **no** → note in upgrade report. Skills will continue reading the legacy path as deprecated; new writes still go to the new path, causing split-brain state. Recommend running migration before next planning cycle.
|
|
139
|
-
|
|
140
|
-
### Pre-0.17.0 missing `layers:` field
|
|
141
|
-
|
|
142
|
-
Run from project root:
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
grep -q '^layers:' .forge/project.yml || echo "no layers field"
|
|
146
|
-
ls .forge/contracts/index.yml 2>/dev/null
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
If `project.yml` has no `layers:` key AND `.forge/contracts/index.yml` is absent, surface:
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
Cross-layer contracts: project.yml predates the `layers:` field (Forge 0.17.0)
|
|
153
|
-
─────────────────────────────────────────────────────────────────────────────
|
|
154
|
-
Forge 0.17.0 added cross-layer contract detection (planning Step 6.1), which
|
|
155
|
-
reads `layers:` from .forge/project.yml. Without it, planning falls back to a
|
|
156
|
-
coarse top-level-directory heuristic.
|
|
157
|
-
|
|
158
|
-
A migration guide is available at: .forge/migrations/0.17.0-cross-layer-contracts.md
|
|
159
|
-
|
|
160
|
-
Add the layers field now? (yes/no/show guide)
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
- **yes** → invoke `quick-tasking` skill, hand it the migration guide as the task definition (identify layers, write `layers:` to project.yml, seed `.forge/contracts/index.yml` for multi-layer projects)
|
|
164
|
-
- **show guide** → read and display the file, then re-ask
|
|
165
|
-
- **no** → note in upgrade report. Planning still works on the fallback heuristic; cross-layer detection is just imprecise until `layers:` is declared. A single-layer project can set `layers: []` to opt out and clear the notice.
|
|
166
|
-
|
|
167
|
-
`upgrading` never edits `.forge/project.yml` directly — this is the only path that adds the field, via `quick-tasking`.
|
|
168
|
-
|
|
169
|
-
### Pre-0.19.0 legacy `state/index.yml`
|
|
170
|
-
|
|
171
|
-
Run from project root:
|
|
172
|
-
|
|
173
|
-
```bash
|
|
174
|
-
grep -qE '^[[:space:]]*(desire_paths|metrics|current_status):' .forge/state/index.yml && echo "legacy index"
|
|
175
|
-
wc -c .forge/state/index.yml # slim registry is a few hundred bytes; KBs = narrative drifted in
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
If `state/index.yml` carries `desire_paths:`/`metrics:`/embedded narrative or is large, surface:
|
|
179
|
-
|
|
180
|
-
```
|
|
181
|
-
Worktree-safe state: state/index.yml predates the derived registry (Forge 0.19.0)
|
|
182
|
-
─────────────────────────────────────────────────────────────────────────────────
|
|
183
|
-
Forge 0.19.0 makes index.yml a derived registry (regenerated by rollup), moves
|
|
184
|
-
desire_paths to append-only files, and commits .forge/ at every phase handoff.
|
|
185
|
-
A legacy index.yml conflicts across git worktrees.
|
|
115
|
+
## Step 7: Post-Upgrade Migration Checks (data-driven)
|
|
186
116
|
|
|
187
|
-
|
|
117
|
+
After sync completes, detect legacy file layouts the new framework version no longer writes but may still read in compatibility mode. This is **data-driven** — it loops over the migration guides synced in Step 4 and runs each guide's `## Detection` block. There are **no per-version blocks here**: a new guide that ships in the template (with a conformant Detection block — see `.forge/templates/migration-guide.md`) is auto-covered on the next upgrade with no edit to this skill.
|
|
188
118
|
|
|
189
|
-
|
|
190
|
-
```
|
|
119
|
+
For each guide that signals it applies, surface a migration prompt — **never auto-migrate**.
|
|
191
120
|
|
|
192
|
-
|
|
193
|
-
- **show guide** → read and display the file, then re-ask
|
|
194
|
-
- **no** → note in upgrade report. Skills still run against a legacy index.yml; it just won't gain worktree conflict-safety until migrated.
|
|
121
|
+
### The loop
|
|
195
122
|
|
|
196
|
-
`
|
|
123
|
+
1. **`installed`** = the project's `.claude/settings.json` `forge.version`, **captured at the START of this upgrade run, BEFORE Step 5 stamps the new version**. If absent/unknown, treat `installed` as `0.0.0` (all guides in range). Do **not** read the *source template's* `settings.json` literal — it carries a placeholder the installer stamps at write time, not the real version.
|
|
124
|
+
2. **`source`** = `{source}/packages/create-forge/package.json` `version`. This is the authoritative new version. (Again: never the template `settings.json` literal.)
|
|
125
|
+
3. Glob the just-synced guides: `.forge/migrations/{v}-*.md`. Parse `{v}` (the leading `MAJOR.MINOR.PATCH`) from each filename. Select guides where **`installed < v <= source`** (semver compare on dotted numerics).
|
|
126
|
+
4. For each selected guide, ascending by version: run its `## Detection` fenced bash block from the project root (via the Bash tool, under `bash`).
|
|
127
|
+
- stdout contains **`MIGRATE`** → the migration applies; surface the prompt (below). The text after `MIGRATE —` is the reason; show it.
|
|
128
|
+
- prints nothing / no `MIGRATE` → **no-op**; report `{v}: no action` and move on.
|
|
129
|
+
- the block errors → report `{v}: detection error (skipped)`; do **not** abort the loop.
|
|
197
130
|
|
|
198
|
-
###
|
|
131
|
+
### The prompt (uniform across guides)
|
|
199
132
|
|
|
200
|
-
|
|
133
|
+
For each applying guide:
|
|
201
134
|
|
|
202
|
-
```bash
|
|
203
|
-
ls .forge/phases/ | grep -E '^m[0-9]+-[0-9]+-' && echo "flat layout"
|
|
204
135
|
```
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
```
|
|
209
|
-
Nested phase layout: phase dirs are flat under .forge/phases/ (Forge 0.20.0)
|
|
210
|
-
──────────────────────────────────────────────────────────────────────────
|
|
211
|
-
Forge 0.20.0 nests phase dirs one level per milestone:
|
|
212
|
-
m{id}-{phase}-{name}/ → milestone-{id}/{phase}-{name}/
|
|
213
|
-
This keeps `ls .forge/phases/` scoped per-milestone instead of dumping every
|
|
214
|
-
phase across all milestones into context. Phase numbers are preserved verbatim
|
|
215
|
-
(nothing is renumbered); the state cursor stays valid.
|
|
216
|
-
|
|
217
|
-
{N} flat phase dir(s) detected.
|
|
218
|
-
A migration guide is available at: .forge/migrations/0.20.0-nested-phase-layout.md
|
|
219
|
-
(canonical: https://github.com/Attuned-Media/forge/blob/main/docs/migrations/0.20.0-nested-phase-layout.md)
|
|
136
|
+
{v} migration available — {reason from the MIGRATE line, or the guide title}
|
|
137
|
+
Guide: .forge/migrations/{v}-{slug}.md
|
|
138
|
+
(canonical: https://github.com/Attuned-Media/forge/blob/main/docs/migrations/{v}-{slug}.md)
|
|
220
139
|
|
|
221
140
|
Migrate now? (yes/no/show guide)
|
|
222
141
|
```
|
|
223
142
|
|
|
224
|
-
- **yes** → invoke `quick-tasking` skill,
|
|
225
|
-
- **show guide** → read and display the file, then re-ask
|
|
226
|
-
- **no** → note in upgrade report.
|
|
227
|
-
|
|
228
|
-
`upgrading` never moves phase dirs directly — migration runs via `quick-tasking`.
|
|
229
|
-
|
|
230
|
-
### Pre-0.22.0 unbounded refactor-backlog
|
|
231
|
-
|
|
232
|
-
Run from project root (backlog present only):
|
|
233
|
-
|
|
234
|
-
```bash
|
|
235
|
-
F=.forge/refactor-backlog.yml
|
|
236
|
-
[ -f "$F" ] || exit 0
|
|
237
|
-
kb=$(( $(wc -c < "$F") / 1024 ))
|
|
238
|
-
terminal=$(grep -cE 'status:[[:space:]]*(done|dismissed|completed|complete|closed|wont_fix|stale)' "$F")
|
|
239
|
-
noncanon=$(grep -oE 'status:[[:space:]]*[A-Za-z_]+' "$F" \
|
|
240
|
-
| grep -vE 'status:[[:space:]]*(pending|in_progress|done|dismissed|deferred)$' | wc -l)
|
|
241
|
-
# bloat if: kb > 150 OR terminal > 0 OR noncanon > 0
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
Surface the prompt when `kb > 150` OR `terminal > 0` OR `noncanon > 0`, printing the three detected numbers:
|
|
245
|
-
|
|
246
|
-
```
|
|
247
|
-
Refactor-backlog compaction available (Forge 0.22.0)
|
|
248
|
-
────────────────────────────────────────────────────
|
|
249
|
-
Pre-0.22.0 the refactor backlog was append-only — resolved items were never
|
|
250
|
-
pruned and statuses drifted (completed/complete/closed/wont_fix/stale + junk).
|
|
251
|
-
0.22.0 archives terminal items to .forge/refactor-backlog-archive.yml and
|
|
252
|
-
enforces the canonical vocab (pending|in_progress|done|dismissed|deferred).
|
|
253
|
-
|
|
254
|
-
Detected: {kb} KB · {terminal} terminal item(s) · {noncanon} non-canonical status(es).
|
|
255
|
-
A migration guide is available at: .forge/migrations/0.22.0-backlog-compaction.md
|
|
256
|
-
(canonical: https://github.com/Attuned-Media/forge/blob/main/docs/migrations/0.22.0-backlog-compaction.md)
|
|
257
|
-
|
|
258
|
-
Migrate now? (yes/no/show guide)
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
- **yes** → invoke `quick-tasking` skill, hand it the migration guide as the task definition (snapshot item count, normalize statuses, split actionable/terminal/triage, archive terminal items to `.forge/refactor-backlog-archive.yml`, rewrite the working file, verify item-count conservation before committing).
|
|
262
|
-
- **show guide** → read and display the file, then re-ask
|
|
263
|
-
- **no** → note in upgrade report. The backlog keeps growing and statuses stay drifted until migrated; reviewing/quick-tasking will compact on their next write regardless, but a bloated file lingers until then. Recommend running before the next review cycle.
|
|
264
|
-
|
|
265
|
-
No-op guarantee: detection exits clean (no prompt) when the backlog is absent, under the gate, has zero terminal items, and all statuses are canonical. (Pending-item count is **not** a trigger — a busy-but-clean backlog is left alone.)
|
|
266
|
-
|
|
267
|
-
`upgrading` never edits the backlog directly — migration runs via `quick-tasking`.
|
|
268
|
-
|
|
269
|
-
### Pre-0.28.0 fully-ignored `.claude/`
|
|
270
|
-
|
|
271
|
-
Run from project root:
|
|
272
|
-
|
|
273
|
-
```bash
|
|
274
|
-
F=.gitignore
|
|
275
|
-
[ -f "$F" ] || exit 0
|
|
276
|
-
# bare ".claude/" or ".claude" rule (no negation lines un-ignoring product files)
|
|
277
|
-
if grep -qE '^[[:space:]]*\.claude/?[[:space:]]*$' "$F" \
|
|
278
|
-
&& ! grep -qE '^![[:space:]]*\.claude/(skills|agents|hooks|settings\.json)' "$F"; then
|
|
279
|
-
echo "broad-ignore"
|
|
280
|
-
fi
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
If the script prints `broad-ignore`, surface:
|
|
284
|
-
|
|
285
|
-
```
|
|
286
|
-
.claude/ fully ignored — Forge worktrees + hooks silently break (Forge 0.28.0)
|
|
287
|
-
─────────────────────────────────────────────────────────────────────────────
|
|
288
|
-
Your .gitignore ignores all of `.claude/` without a carve-out. That means:
|
|
289
|
-
• git worktrees (Forge or the app's) have no `.claude/skills/`, so `/forge`
|
|
290
|
-
is missing — typing `/forge` in a worktree prints "Unknown command".
|
|
291
|
-
• M10 worktrees come up with no hooks (forge-claim-check, branch-guard,
|
|
292
|
-
session-id) — coordination + ADR-008 protection are silently disabled.
|
|
293
|
-
|
|
294
|
-
These are PRODUCT files, not per-machine config. The fix is a narrow
|
|
295
|
-
carve-out: track skills/agents/hooks/settings.json; keep settings.local.json,
|
|
296
|
-
projects/, worktrees/ ignored.
|
|
297
|
-
|
|
298
|
-
A migration guide is available at: .forge/migrations/0.28.0-worktree-root.md
|
|
299
|
-
(canonical: https://github.com/Attuned-Media/forge/blob/main/docs/migrations/0.28.0-worktree-root.md)
|
|
300
|
-
|
|
301
|
-
Apply the carve-out now? (yes/no/show guide)
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
- **yes** → invoke `quick-tasking` skill with the migration guide as the task (edit `.gitignore` in place, `git rm -r --cached .claude/` to clear the ignore-cache, stage `.gitignore` + the four product trees, commit `chore(forge): track .claude/ product files`). The skill MUST confirm with the user before staging anything outside the four product trees.
|
|
305
|
-
- **show guide** → read and display the file, then re-ask
|
|
306
|
-
- **no** → note in upgrade report. The framework keeps working in the main checkout but worktree-mode sessions (M10 / Claude Desktop folder-open) will be missing `/forge` and Forge hooks until migrated.
|
|
307
|
-
|
|
308
|
-
This is a Forge-product-tracking concern, not a state migration — `upgrading` never edits user `.gitignore` directly. Migration runs via `quick-tasking`.
|
|
143
|
+
- **yes** → invoke the `quick-tasking` skill, handing it the guide path as the task definition. `upgrading` never edits user state / project files / `.gitignore` directly — every migration runs via `quick-tasking`, and any lossy step (e.g. narrative extraction, staging files) is confirmed with the user first.
|
|
144
|
+
- **show guide** → read and display the guide file, then re-ask.
|
|
145
|
+
- **no** → note in the upgrade report. The new skills compose new-shape paths on write while legacy files persist on disk (split-brain) until migrated; recommend running it before the next planning/review cycle.
|
|
309
146
|
|
|
310
|
-
###
|
|
147
|
+
### Why there is no bookkeeping
|
|
311
148
|
|
|
312
|
-
|
|
313
|
-
1. Detection command(s)
|
|
314
|
-
2. Single-line description of the drift
|
|
315
|
-
3. Pointer to `.forge/migrations/{version}-{slug}.md` (installed copy; canonical at `docs/migrations/...` in the Forge source repo)
|
|
316
|
-
4. Three-way prompt (yes/no/show guide)
|
|
149
|
+
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.
|
|
@@ -278,25 +278,11 @@ After verification completes (PASSED or GAPS FOUND), run a quick retrospective o
|
|
|
278
278
|
|
|
279
279
|
**6. User corrections**: User correct the same thing multiple times? Implicit rules that should become explicit.
|
|
280
280
|
|
|
281
|
-
|
|
281
|
+
**Set `scope` on each observation** (see ADR-012): `framework` when the fix targets Forge's own skills/templates (tier heuristics, a skill's step, a template); `project` when it targets user-owned files (`constitution.md`, `design-system.md`, `context.md`). Record `suggestion_target` when known. This classifies the signal at write time so review can route `framework` signals upstream. Tier-override signals (#2) and the "constitution needs a new article" deviation case (#1) are usually `framework` and `project` respectively — judge by the file the fix touches, not the type.
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
**Surfacing is owned by `forge` boot — not here** (single review owner, ADR-012). `verifying` only *records* observations; it does not surface recommendations, prompt for fixes, or archive. The 3+ aggregation, the type→evolution suggestion table, the decline-ledger, and upstream routing all live in `forge` Step 1.2. Write the observation files (with `scope`) and move on — the next `forge` boot reviews them.
|
|
284
284
|
|
|
285
|
-
|
|
286
|
-
|-------------|-------------------|
|
|
287
|
-
| Repeated deviations | Add pre-check to planning, or new constitutional article |
|
|
288
|
-
| Tier overrides | Adjust detection heuristics in forge skill |
|
|
289
|
-
| Skipped steps | Make step optional, or merge into another step |
|
|
290
|
-
| Recurring friction | Add guidance to relevant skill, or create a template |
|
|
291
|
-
| Agent struggles | Add examples or anti-patterns to the relevant skill |
|
|
292
|
-
| User corrections | Add rule to constitution, context.md, or relevant skill |
|
|
293
|
-
|
|
294
|
-
Propose concrete actions:
|
|
295
|
-
- *"Add 'always use Card component for content containers' to design-system.md?"*
|
|
296
|
-
- *"Add null-check verification step to planning template?"*
|
|
297
|
-
- *"Make research phase optional for Standard tier in this project?"*
|
|
298
|
-
|
|
299
|
-
Only suggest at 3+ occurrences. One-off issues are noise.
|
|
285
|
+
> Why retired: two surfacers (`forge` + `verifying`) disagreed, and `verifying` proposed without archiving, so its patterns re-surfaced forever. One owner fixes both.
|
|
300
286
|
|
|
301
287
|
## Phase Handoff
|
|
302
288
|
|
package/template/.forge/FORGE.md
CHANGED
|
@@ -135,7 +135,7 @@ State lives in `.forge/`:
|
|
|
135
135
|
- `roadmap.yml` — Phases, milestones, dependencies
|
|
136
136
|
- `state/index.yml` — DERIVED registry rolled up from milestone files (id, name, status, last_updated). Never hand-edited; never written by worktree agents.
|
|
137
137
|
- `state/milestone-{id}.yml` — Per-milestone cursor (single source of truth): position, progress, decisions, blockers. One owner at a time.
|
|
138
|
-
- `state/desire-paths/` — Append-only framework-usage observations, one file per observation. Occurrence counts derived by globbing (no mutable counter).
|
|
138
|
+
- `state/desire-paths/` — Append-only framework-usage observations, one file per observation. Occurrence counts derived by globbing (no mutable counter). Each carries `scope: project | framework` (set at capture; absent ⇒ project). **`forge` boot is the single review owner**: at 3+ occurrences it surfaces a concrete fix from the co-located suggestion table, persists declines to `declined/` (no re-nag), and for `scope: framework` signals routes them upstream — a portable block to `upstream/` plus a `gh issue` offer against `forge.upstream_repo`. This closes Principle 7's capture→act loop; `verifying` captures but no longer surfaces. See [ADR-012](../docs/decisions/ADR-012-upstream-desire-path-feedback-channel.md).
|
|
139
139
|
- `context.md` — Locked decisions + deferred ideas (discuss phase)
|
|
140
140
|
- `research/milestone-{id}.md` — Research findings snapshot (dated, immutable)
|
|
141
141
|
- `phases/milestone-{id}/{phase}-{name}/plan-{NN}.md` — Task plans with must_haves frontmatter (`{id}`=milestone, `{phase}`=phase# preserved verbatim, `{name}`=kebab, `{NN}`=seq)
|
|
@@ -15,16 +15,20 @@ The pre-0.10.0 spec had a single top-level `.forge/requirements.yml` "refreshed
|
|
|
15
15
|
|
|
16
16
|
## Detection
|
|
17
17
|
|
|
18
|
-
Run from project root.
|
|
18
|
+
Run from project root. Prints `MIGRATE` on stdout if a legacy requirements layout is present; silent + exit 0 when already on the per-milestone layout (the data-driven `upgrading` loop keys off the `MIGRATE` sentinel).
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
|
|
22
|
-
ls .forge/requirements.yml 2>/dev/null # legacy single file
|
|
23
|
-
ls .forge/requirements-m*.yml 2>/dev/null # suffixed top-level
|
|
24
|
-
find .forge/phases -name "requirements.yml" 2>/dev/null # per-phase files
|
|
21
|
+
legacy=$(
|
|
22
|
+
ls .forge/requirements.yml 2>/dev/null # legacy single file
|
|
23
|
+
ls .forge/requirements-m*.yml 2>/dev/null # suffixed top-level
|
|
24
|
+
find .forge/phases -name "requirements.yml" 2>/dev/null # per-phase files
|
|
25
|
+
)
|
|
26
|
+
if [ -n "$legacy" ]; then
|
|
27
|
+
echo "MIGRATE — legacy requirements layout: $(echo "$legacy" | tr '\n' ' ')"
|
|
28
|
+
fi
|
|
25
29
|
```
|
|
26
30
|
|
|
27
|
-
If `.forge/requirements/` already exists as a directory and the legacy paths above are absent, migration is complete —
|
|
31
|
+
If `.forge/requirements/` already exists as a directory and the legacy paths above are absent, migration is complete — the block prints nothing and exits 0.
|
|
28
32
|
|
|
29
33
|
## Migration steps
|
|
30
34
|
|
|
@@ -17,17 +17,17 @@ Adding `layers:` once makes the detection deterministic. A single-layer project
|
|
|
17
17
|
|
|
18
18
|
## Detection
|
|
19
19
|
|
|
20
|
-
Run from project root.
|
|
20
|
+
Run from project root. Prints `MIGRATE` if `project.yml` predates the `layers:` field and no contract index has been seeded; silent + exit 0 once `layers:` is declared (the data-driven `upgrading` loop keys off the `MIGRATE` sentinel).
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
|
|
24
|
-
grep -q '^layers:' .forge/project.yml
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
if [ -f .forge/project.yml ] \
|
|
24
|
+
&& ! grep -q '^layers:' .forge/project.yml \
|
|
25
|
+
&& [ ! -f .forge/contracts/index.yml ]; then
|
|
26
|
+
echo "MIGRATE — project.yml has no layers: field"
|
|
27
|
+
fi
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
If `project.yml` already contains a `layers:` key (any value, including `[]`), migration is complete —
|
|
30
|
+
If `project.yml` already contains a `layers:` key (any value, including `[]`), or a `.forge/contracts/index.yml` exists (layers were declared at init), migration is complete — the block prints nothing and exits 0.
|
|
31
31
|
|
|
32
32
|
## Migration steps
|
|
33
33
|
|
|
@@ -19,14 +19,19 @@ Two problems this release fixes:
|
|
|
19
19
|
|
|
20
20
|
## Detection
|
|
21
21
|
|
|
22
|
+
Run from project root. Prints `MIGRATE` if `state/index.yml` is a legacy file — still carrying the metrics/desire-paths/narrative blocks, or grown to KBs; silent + exit 0 once it is the slim derived registry (the data-driven `upgrading` loop keys off the `MIGRATE` sentinel).
|
|
23
|
+
|
|
22
24
|
```bash
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
F=.forge/state/index.yml
|
|
26
|
+
[ -f "$F" ] || exit 0
|
|
25
27
|
# Anchored so the file's own explanatory comments don't false-positive:
|
|
26
|
-
grep -
|
|
28
|
+
if grep -qE '^[[:space:]]*(desire_paths|metrics|current_status):' "$F" \
|
|
29
|
+
|| [ "$(wc -c < "$F")" -gt 4096 ]; then
|
|
30
|
+
echo "MIGRATE — legacy state/index.yml ($(wc -c < "$F") bytes)"
|
|
31
|
+
fi
|
|
27
32
|
```
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
A slim derived registry is a few hundred bytes and carries none of those keys → the block prints nothing and exits 0. The installer (`npx forge-orkes upgrade`) runs the same check after sync.
|
|
30
35
|
|
|
31
36
|
## Migration steps
|
|
32
37
|
|
|
@@ -55,14 +55,18 @@ Now `ls .forge/phases/milestone-9/` shows only milestone 9's phases.
|
|
|
55
55
|
## Detection
|
|
56
56
|
|
|
57
57
|
A flat (un-migrated) layout has directories matching `m{id}-{phase}-{name}`
|
|
58
|
-
**directly** under `.forge/phases
|
|
58
|
+
**directly** under `.forge/phases/`. Run from project root — prints `MIGRATE`
|
|
59
|
+
if any flat phase dir is present; silent + exit 0 when already nested (the
|
|
60
|
+
data-driven `upgrading` loop keys off the `MIGRATE` sentinel):
|
|
59
61
|
|
|
60
62
|
```bash
|
|
61
|
-
ls .forge/phases/ | grep -
|
|
63
|
+
if ls .forge/phases/ 2>/dev/null | grep -qE '^m[0-9]+-[0-9]+-'; then
|
|
64
|
+
echo "MIGRATE — flat phase layout under .forge/phases/"
|
|
65
|
+
fi
|
|
62
66
|
```
|
|
63
67
|
|
|
64
|
-
Already-nested projects have only `milestone-{id}/` dirs at that level
|
|
65
|
-
|
|
68
|
+
Already-nested projects have only `milestone-{id}/` dirs at that level → the
|
|
69
|
+
block prints nothing and exits 0.
|
|
66
70
|
|
|
67
71
|
## Defensive parse
|
|
68
72
|
|
|
@@ -59,23 +59,27 @@ triage."* Silently dropping or guessing is forbidden.
|
|
|
59
59
|
## Detection
|
|
60
60
|
|
|
61
61
|
Bloat/drift exists if the file is over the 150 KB size gate, OR any terminal
|
|
62
|
-
item is present, OR any non-canonical status appears
|
|
62
|
+
item is present, OR any non-canonical status appears. Run from project root —
|
|
63
|
+
prints `MIGRATE` when bloat is detected; silent + exit 0 when the backlog is
|
|
64
|
+
absent or clean (the data-driven `upgrading` loop keys off the `MIGRATE`
|
|
65
|
+
sentinel):
|
|
63
66
|
|
|
64
67
|
```bash
|
|
65
68
|
F=.forge/refactor-backlog.yml
|
|
66
|
-
[ -f "$F" ] ||
|
|
69
|
+
[ -f "$F" ] || exit 0
|
|
67
70
|
kb=$(( $(wc -c < "$F") / 1024 ))
|
|
68
71
|
terminal=$(grep -cE 'status:[[:space:]]*(done|dismissed|completed|complete|closed|wont_fix|stale)' "$F")
|
|
69
72
|
noncanon=$(grep -oE 'status:[[:space:]]*[A-Za-z_]+' "$F" \
|
|
70
73
|
| grep -vE 'status:[[:space:]]*(pending|in_progress|done|dismissed|deferred)$' | wc -l)
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
if [ "$kb" -gt 150 ] || [ "$terminal" -gt 0 ] || [ "$noncanon" -gt 0 ]; then
|
|
75
|
+
echo "MIGRATE — backlog bloat: size=${kb}KB terminal=$terminal noncanon=$noncanon"
|
|
76
|
+
fi
|
|
73
77
|
```
|
|
74
78
|
|
|
75
|
-
A clean backlog (under the gate, zero terminal, all-canonical) prints
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
A clean backlog (under the gate, zero terminal, all-canonical) prints nothing
|
|
80
|
+
and exits 0 → **no-op**. The detector keys off bloat that compaction actually
|
|
81
|
+
fixes, **not** the count of pending items — a busy-but-clean backlog is left
|
|
82
|
+
alone.
|
|
79
83
|
|
|
80
84
|
## ⚠️ Environment gotcha (read before scripting)
|
|
81
85
|
|
|
@@ -27,14 +27,14 @@ git worktree add ${wt_root}/${anchor} main
|
|
|
27
27
|
|
|
28
28
|
The resolved absolute path is recorded in `lifecycle.worktree_path` and read verbatim by teardown + crash-recovery — so the config knob is forward-looking, not retroactive.
|
|
29
29
|
|
|
30
|
-
###
|
|
30
|
+
### Inventory (informational — not a migration trigger)
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
# Are any of your active milestones pointing at the old shared dir?
|
|
34
34
|
grep -rE '^\s*worktree_path:.*forge-worktrees' .forge/state/ 2>/dev/null | head
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Matches = pre-0.28.0 worktrees still in flight. **Leave them alone** — they keep working off their recorded path. Only **new** worktrees pick up the new default.
|
|
37
|
+
Matches = pre-0.28.0 worktrees still in flight. **Leave them alone** — they keep working off their recorded path. Only **new** worktrees pick up the new default. This is informational: it does **not** trigger a migration (the actionable trigger for this guide is the `.gitignore` carve-out — see `## Detection` below).
|
|
38
38
|
|
|
39
39
|
### Migration
|
|
40
40
|
|
|
@@ -86,16 +86,16 @@ Forge installs into `.claude/` (skills, agents, hooks, `settings.json`). Many Cl
|
|
|
86
86
|
|
|
87
87
|
The fix: track product files, keep per-machine/scratch files ignored.
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
## Detection
|
|
90
90
|
|
|
91
|
-
`upgrading` runs this automatically; you can run it by hand too:
|
|
91
|
+
This is the guide's single migration trigger — the `.gitignore` carve-out. (Part 1's worktree-path inventory above is informational only.) `upgrading` runs this automatically; you can run it by hand too. Prints `MIGRATE` when `.claude/` is broadly ignored with no product-file carve-out; silent + exit 0 otherwise (the data-driven `upgrading` loop keys off the `MIGRATE` sentinel):
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
94
|
F=.gitignore
|
|
95
95
|
[ -f "$F" ] || exit 0
|
|
96
96
|
if grep -qE '^[[:space:]]*\.claude/?[[:space:]]*$' "$F" \
|
|
97
97
|
&& ! grep -qE '^![[:space:]]*\.claude/(skills|agents|hooks|settings\.json)' "$F"; then
|
|
98
|
-
echo "
|
|
98
|
+
echo "MIGRATE — .claude/ broadly ignored with no product-file carve-out"
|
|
99
99
|
fi
|
|
100
100
|
```
|
|
101
101
|
|
|
@@ -50,9 +50,14 @@ The taxonomy is descriptive: every existing decision in `context.md` already fit
|
|
|
50
50
|
|
|
51
51
|
If a past edit broke the rule (decision rewritten in place), don't go back and reconstruct strike-throughs — the history is in git. Just follow the rule going forward.
|
|
52
52
|
|
|
53
|
-
## Detection
|
|
53
|
+
## Detection
|
|
54
54
|
|
|
55
|
-
There's nothing to detect on disk
|
|
55
|
+
Intentional no-op. There's nothing to detect on disk — the 0.29.0 changes are in skills and `FORGE.md`, so upgrading the framework files is the whole migration. The block below is the uniform-contract placeholder: it prints no `MIGRATE` sentinel and exits 0, so the data-driven `upgrading` loop correctly treats this in-range guide as "no action".
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# 0.29.0 is documentation + skill-behavior only — nothing on disk to migrate.
|
|
59
|
+
exit 0
|
|
60
|
+
```
|
|
56
61
|
|
|
57
62
|
## Validation
|
|
58
63
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Migration Guide Template
|
|
2
|
+
|
|
3
|
+
Copy to `.forge/migrations/{version}-{slug}.md` (e.g. `0.31.0-foo.md`) when a Forge
|
|
4
|
+
version changes an on-disk layout, a config field, or a state shape that older
|
|
5
|
+
projects need to migrate. Also mirror the file to `docs/migrations/{version}-{slug}.md`
|
|
6
|
+
(canonical) — the two copies must be byte-identical.
|
|
7
|
+
|
|
8
|
+
A guide written to this contract is **auto-covered** by the post-upgrade migration
|
|
9
|
+
check in BOTH routes (`upgrading` SKILL Step 7 and `npx forge-orkes upgrade`) the
|
|
10
|
+
moment it ships in the template. **You do not edit `upgrading/SKILL.md` or
|
|
11
|
+
`create-forge.js`** — they loop over the guides, so a conformant new guide just works.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The Detection-block contract (load-bearing — read before writing the guide)
|
|
16
|
+
|
|
17
|
+
The data-driven migration loop in both routes finds each guide, decides whether it is
|
|
18
|
+
in range, and runs its Detection block. For that to work, every guide MUST follow this
|
|
19
|
+
contract:
|
|
20
|
+
|
|
21
|
+
1. **Exactly one `## Detection` heading per guide.** Not `### Detection`, not two of
|
|
22
|
+
them. If a guide has informational sub-checks (e.g. an inventory the user should see
|
|
23
|
+
but that does NOT trigger a migration), give them a different heading — only the one
|
|
24
|
+
`## Detection` block is run.
|
|
25
|
+
2. **Under it, exactly one fenced ` ```bash ` block.** It is run from the project root,
|
|
26
|
+
must tolerate absent files (`[ -f … ] || exit 0`, `2>/dev/null`), and must not depend
|
|
27
|
+
on the user's shell config. The loop runs it under `bash` explicitly — do not rely on
|
|
28
|
+
zsh-only or interactive features. (The Bash tool runs zsh; `$BASH_REMATCH` / `=~`
|
|
29
|
+
capture groups silently fail — avoid them.)
|
|
30
|
+
3. **Sentinel = `MIGRATE`.** The block echoes the literal token `MIGRATE` on stdout when
|
|
31
|
+
the migration applies. A trailing `— reason` after it is encouraged — it surfaces in
|
|
32
|
+
the prompt the user sees. When there is nothing to migrate (no-op guides included),
|
|
33
|
+
the block prints **no** `MIGRATE` token and exits 0.
|
|
34
|
+
4. **No-op guides still get the heading.** A documentation/skill-behavior-only change
|
|
35
|
+
(nothing on disk to migrate) keeps a `## Detection` heading with a bash block that
|
|
36
|
+
simply `exit 0`s, so the loop's heading-find stays uniform across all guides.
|
|
37
|
+
|
|
38
|
+
### Range gating (why you never touch the loop code)
|
|
39
|
+
|
|
40
|
+
The loop reads the project's **installed** version (`settings.json forge.version`, captured
|
|
41
|
+
BEFORE the upgrade stamps the new value) and the **source** version
|
|
42
|
+
(`packages/create-forge/package.json`). It runs only the guides where
|
|
43
|
+
`installed < {guide version} <= source`. So a guide ships, the next upgrade that crosses
|
|
44
|
+
its version runs it once, and after the version is stamped it falls out of range — no
|
|
45
|
+
ledger, no per-version code, no staleness. (It also closes the install-run blind spot:
|
|
46
|
+
detection keys off the freshly-synced guides on disk, not the skill's own pre-sync code.)
|
|
47
|
+
|
|
48
|
+
### Detection block skeleton (applies)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Detect the legacy shape; tolerate absent files; emit MIGRATE iff it applies.
|
|
52
|
+
F=.forge/some-file.yml
|
|
53
|
+
[ -f "$F" ] || exit 0
|
|
54
|
+
if grep -q 'legacy-marker' "$F"; then
|
|
55
|
+
echo "MIGRATE — <one-line reason the user will see>"
|
|
56
|
+
fi
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Detection block skeleton (no-op guide)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Documentation / skill-behavior change only — nothing on disk to migrate.
|
|
63
|
+
exit 0
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Guide skeleton
|
|
69
|
+
|
|
70
|
+
```markdown
|
|
71
|
+
# Migration Guide: <Title> (Forge <version>)
|
|
72
|
+
|
|
73
|
+
Applies to <which projects>. <One-paragraph what-changed-and-why.>
|
|
74
|
+
|
|
75
|
+
## Prerequisites
|
|
76
|
+
|
|
77
|
+
1. <e.g. on the new version's framework files (run `npx forge-orkes upgrade` first)>
|
|
78
|
+
2. <e.g. working tree clean or changes committed — migration touches N files>
|
|
79
|
+
|
|
80
|
+
## Detection
|
|
81
|
+
|
|
82
|
+
<One sentence: prints `MIGRATE` when it applies; silent + exit 0 otherwise.>
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
<the single detection block per the contract above>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Migration steps
|
|
89
|
+
|
|
90
|
+
### 1. <step>
|
|
91
|
+
### 2. <step>
|
|
92
|
+
...
|
|
93
|
+
<The migration is NEVER auto-applied. In Claude Code it runs via the `quick-tasking`
|
|
94
|
+
skill, which the `upgrading` Step-7 prompt hands the guide to on "yes". Lossy steps
|
|
95
|
+
must be confirmed with the user.>
|
|
96
|
+
|
|
97
|
+
## Validation
|
|
98
|
+
|
|
99
|
+
<How to confirm the migration succeeded — greps, counts, conservation checks.>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Acceptance self-check before you ship the guide
|
|
105
|
+
|
|
106
|
+
- [ ] Exactly one `## Detection` heading; exactly one ` ```bash ` block under it.
|
|
107
|
+
- [ ] Block emits `MIGRATE` on stdout iff the migration applies; silent + exit 0 otherwise.
|
|
108
|
+
- [ ] Block is safe on a clean/empty project (no errors, exit 0).
|
|
109
|
+
- [ ] Mirrored byte-identical to `docs/migrations/{same-name}.md`.
|
|
110
|
+
- [ ] No edit to `upgrading/SKILL.md` or `create-forge.js` was needed (the loop covers it).
|
|
@@ -101,6 +101,14 @@ orchestration: # M10 multi-agent orchestration (experiment
|
|
|
101
101
|
# ../wt/<repo> — shared dir with per-repo subfolder
|
|
102
102
|
# Do NOT point this inside the repo working tree (file watchers + indexers will recurse into the worktree).
|
|
103
103
|
|
|
104
|
+
forge: # Forge framework-level settings (all optional)
|
|
105
|
+
# upstream_repo: "https://github.com/zayneupton/forge"
|
|
106
|
+
# Where framework-scope desire paths (ADR-012) file issues when `gh`
|
|
107
|
+
# is available. Default if absent: the canonical Forge repo, so this
|
|
108
|
+
# works configless. Uncomment only to retarget a fork/mirror.
|
|
109
|
+
# worktree_rebase_check: true # Opt-in: when forge boots inside a worktree, diff .forge/ vs main and
|
|
110
|
+
# offer to rebase/restore shared-mutable files (FORGE.md State Ownership).
|
|
111
|
+
|
|
104
112
|
risks: # What could go wrong?
|
|
105
113
|
- risk: ""
|
|
106
114
|
mitigation: ""
|
|
@@ -9,12 +9,27 @@
|
|
|
9
9
|
# The date+type+milestone+slug make the name unique, so concurrent agents in
|
|
10
10
|
# different worktrees only ever ADD files — git never has to merge content.
|
|
11
11
|
#
|
|
12
|
-
# Occurrence counts are DERIVED: the `forge` skill
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# Occurrence counts are DERIVED: the `forge` skill globs this directory and
|
|
13
|
+
# groups by (type + normalized note). A pattern appearing in 3+ files is a
|
|
14
|
+
# candidate for framework evolution, surfaced at `forge` boot — the single
|
|
15
|
+
# review owner (see ADR-012).
|
|
16
|
+
#
|
|
17
|
+
# SCOPE decides where a 3+ pattern's fix lands:
|
|
18
|
+
# project = fix targets user-owned files (constitution.md, design-system.md,
|
|
19
|
+
# context.md). Stays in-project.
|
|
20
|
+
# framework = fix targets Forge's own skills/templates. Eligible for UPSTREAM
|
|
21
|
+
# transport — forge review writes a portable block to
|
|
22
|
+
# state/desire-paths/upstream/ and (when gh + forge.upstream_repo
|
|
23
|
+
# are available) offers a GH issue against the Forge repo.
|
|
24
|
+
# Lazy migration: a file with no `scope` is read as `project`.
|
|
15
25
|
|
|
16
26
|
type: "" # deviation_pattern | tier_override | skipped_step |
|
|
17
27
|
# recurring_friction | agent_struggle | user_correction
|
|
28
|
+
scope: "" # project | framework — set AT CAPTURE by the writing
|
|
29
|
+
# skill, from the file its suggested fix targets.
|
|
30
|
+
# Absent ⇒ project (lazy migration).
|
|
31
|
+
suggestion_target: "" # optional: the skill/file the fix would touch
|
|
32
|
+
# (e.g. "planning/executing skills", "constitution.md")
|
|
18
33
|
milestone: "" # e.g. "m7" (or "global" if not milestone-scoped)
|
|
19
34
|
skill: "" # skill that observed it (executing, planning, verifying, ...)
|
|
20
35
|
first_seen: null # ISO 8601 date this observation was recorded
|