forge-orkes 0.17.0 → 0.18.1

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.
@@ -318,28 +318,70 @@ function detectLegacyRequirementsLayout() {
318
318
  }
319
319
 
320
320
  /**
321
- * After upgrade, surface any detected legacy file layouts that the new framework
322
- * version no longer writes. Non-interactive prints a warning with a pointer to
323
- * the migration guide. Add new detection blocks here as future versions change layout.
321
+ * Detect a pre-0.17.0 project.yml that has no `layers:` field. Cross-layer
322
+ * contract detection (planning Step 6.1) reads `layers:`; without it the planner
323
+ * falls back to a coarse top-level-directory heuristic. Returns true if the field
324
+ * is absent and no contract index has been seeded. False if project.yml is missing
325
+ * (not yet initialized) or already migrated.
326
+ */
327
+ function detectMissingLayersConfig() {
328
+ const projectYml = path.join(targetDir, '.forge', 'project.yml');
329
+ if (!fs.existsSync(projectYml)) return false;
330
+
331
+ let content;
332
+ try {
333
+ content = fs.readFileSync(projectYml, 'utf-8');
334
+ } catch {
335
+ return false;
336
+ }
337
+
338
+ // Already declared a layers: key (any value, including []) → migrated.
339
+ if (/^layers:/m.test(content)) return false;
340
+ // A seeded contract index implies layers were declared at init → migrated.
341
+ if (fs.existsSync(path.join(targetDir, '.forge', 'contracts', 'index.yml'))) return false;
342
+
343
+ return true;
344
+ }
345
+
346
+ /**
347
+ * After upgrade, surface any detected legacy file layouts the new framework
348
+ * version no longer writes, or new config fields older projects lack.
349
+ * Non-interactive — prints a warning with a pointer to the migration guide.
350
+ * Add new detection blocks here as future versions change layout.
324
351
  */
325
352
  function runPostUpgradeMigrationChecks() {
326
353
  const legacyReqs = detectLegacyRequirementsLayout();
327
- if (legacyReqs.length === 0) return;
354
+ if (legacyReqs.length > 0) {
355
+ console.log(' ⚠ Pre-0.10.0 requirements layout detected');
356
+ console.log(' ─────────────────────────────────────────');
357
+ console.log(' Found:');
358
+ for (const p of legacyReqs) {
359
+ console.log(` ${p}`);
360
+ }
361
+ console.log();
362
+ console.log(' Forge 0.10.0+ uses per-milestone files at .forge/requirements/m{N}.yml.');
363
+ console.log(' Migration guide: .forge/migrations/0.10.0-per-milestone-requirements.md');
364
+ console.log();
365
+ console.log(' Run the migration before your next planning cycle. In Claude Code:');
366
+ console.log(' /forge then hand the guide to quick-tasking, or invoke Skill(quick-tasking)');
367
+ console.log(' with the guide path as the task definition.');
368
+ console.log();
369
+ }
328
370
 
329
- console.log(' ⚠ Pre-0.10.0 requirements layout detected');
330
- console.log(' ─────────────────────────────────────────');
331
- console.log(' Found:');
332
- for (const p of legacyReqs) {
333
- console.log(` ${p}`);
371
+ if (detectMissingLayersConfig()) {
372
+ console.log(' ⚠ Cross-layer contracts: project.yml has no `layers:` field (Forge 0.17.0)');
373
+ console.log(' ──────────────────────────────────────────────────────────────────────');
374
+ console.log(' Forge 0.17.0 added cross-layer contract detection (planning Step 6.1),');
375
+ console.log(' which reads `layers:` from .forge/project.yml. Your project.yml predates');
376
+ console.log(' this field, so planning falls back to a coarse top-level-directory heuristic.');
377
+ console.log();
378
+ console.log(' Migration guide: .forge/migrations/0.17.0-cross-layer-contracts.md');
379
+ console.log();
380
+ console.log(' Quick fix: add a `layers:` list to .forge/project.yml — or `layers: []`');
381
+ console.log(' for a single-layer project to silence this notice. In Claude Code:');
382
+ console.log(' /forge then hand the guide to quick-tasking.');
383
+ console.log();
334
384
  }
335
- console.log();
336
- console.log(' Forge 0.10.0+ uses per-milestone files at .forge/requirements/m{N}.yml.');
337
- console.log(' Migration guide: .forge/migrations/0.10.0-per-milestone-requirements.md');
338
- console.log();
339
- console.log(' Run the migration before your next planning cycle. In Claude Code:');
340
- console.log(' /forge then hand the guide to quick-tasking, or invoke Skill(quick-tasking)');
341
- console.log(' with the guide path as the task definition.');
342
- console.log();
343
385
  }
344
386
 
345
387
  async function upgrade() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-orkes",
3
- "version": "0.17.0",
3
+ "version": "0.18.1",
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"
@@ -241,7 +241,7 @@ Do NOT list test failures here — pre-existing failures belong in deferred-issu
241
241
  ```
242
242
 
243
243
  ## State Updates
244
- 1. Update `.forge/state/milestone-{id}.yml` — advance plan counter, update progress
244
+ 1. Update `.forge/state/milestone-{id}.yml` — advance `current` cursor (`current.plan`/`current.task`, and `current.phase` when a phase completes). Do **not** write a progress percent — it is derived on read by the `forge` skill from `current.phase` vs the roadmap phase count.
245
245
  2. Record deviations in milestone state
246
246
  3. Update `.forge/state/index.yml` — set `last_updated` timestamp
247
247
  4. All plans in phase complete → transition to `verifying`
@@ -20,14 +20,21 @@ Check state files:
20
20
  1. Read for active milestones
21
21
  2. **Check arg first.** `/forge 2` or `/forge "Auth system"`:
22
22
  - Match IDs (exact) or names (case-insensitive substring)
23
- - Found → auto-select: *"Resuming {id}: [{name}] -- {current.status}, {overall_percent}%"*
23
+ - Found → auto-select: *"Resuming {id}: [{name}] -- {current.status}, {percent}%"*
24
24
  - No match → *"No match for '{arg}'. Active:"*
25
25
  3. **Multiple (no arg):** Show active + not_started milestones with status + `last_updated`. Default most recent. After main list, show **Deferred:** section (id, name, frozen status `was: {current.status}`, defer date, reason). *"{N} active. Most recent: [{name}] ({current.status}, {date}). This one, or switch?"*
26
- 4. **One:** Auto-select. *"Resuming: [{name}] -- {current.status}, {overall_percent}%"*
26
+ 4. **One:** Auto-select. *"Resuming: [{name}] -- {current.status}, {percent}%"*
27
27
  5. **None active:** If deferred exist, mention them: *"No active milestones. {N} deferred — 'resume milestone {id}' to reactivate."* Else → init or create.
28
28
  6. Load `milestone-{id}.yml`
29
- 7. **Route on `current.status`, NOT `overall_percent`.** Complete only at `complete`. 100% tasks ≠ done -- verifying + reviewing must run.
30
- 8. Report + **immediately route** (Step 3). Show: `current.status`, phase labels (Executed/Verified/Pending/In progress -- **never "Complete" for unverified**), `overall_percent`. **No menus.** Position → action → invoke.
29
+ 7. **Route on `current.status`, NOT `{percent}`.** Complete only at `complete`. 100% phases ≠ done -- verifying + reviewing must run.
30
+ 8. Report + **immediately route** (Step 3). Show: `current.status`, phase labels (Executed/Verified/Pending/In progress -- **never "Complete" for unverified**), `{percent}`. **No menus.** Position → action → invoke.
31
+
32
+ **`{percent}` is derived on read — never stored.** Compute it, don't look it up:
33
+ - `total` = number of phases listed for this milestone in `.forge/roadmap.yml` (the milestone's `phases:` list length).
34
+ - `done` = `current.status == complete` → `total`; else `max(0, current.phase - 1)` (phases before the active one count as done).
35
+ - `{percent}` = `total > 0` → `round(done / total * 100)`; `total` unknown/0 → omit the percent from the line.
36
+
37
+ Uses only signals that stay current (`current.phase`, `current.status`, the roadmap phase list). Old milestone files may still carry a vestigial `progress:` block — ignore it; derive anyway.
31
38
 
32
39
  Beads enabled (`forge.beads_integration: true`) → `bd prime`. Optional.
33
40
 
@@ -50,7 +57,6 @@ Check `.forge/refactor-backlog.yml`:
50
57
  - `milestone.name: "Promoted from {R-id}: {backlog item title}"`
51
58
  - `milestone.origin: {R-id}`
52
59
  - `current.tier: standard`, `current.status: researching`
53
- - `progress.last_update: {ISO date}`
54
60
  3. Append to `.forge/state/index.yml` `milestones:`:
55
61
  ```yaml
56
62
  - id: m-{R-id}
@@ -196,7 +202,7 @@ Tier + state → invoke via `Skill` tool. All phases use `Skill()`.
196
202
 
197
203
  1. Read `milestone-{id}.yml` → 2. Check advancement → 3. `current.status` → skill → 4. Brief + route:
198
204
 
199
- *"Milestone {id}: {name} | {current.status} → {next} ({model}) | {overall_percent}% | Routing..."*
205
+ *"Milestone {id}: {name} | {current.status} → {next} ({model}) | {percent}% | Routing..."*
200
206
 
201
207
  Briefing, not prompt -- default = forward.
202
208
 
@@ -70,8 +70,8 @@ Invoked via forge routing (mid-workflow or refactor-backlog item with milestone
70
70
 
71
71
  1. Read `.forge/state/milestone-{id}.yml` for current position
72
72
  2. Follow standard workflow (above)
73
- 3. After commit: update milestone state — increment `progress.overall_percent`, log deviations if any Rule 1-3 applied
74
- 4. Report: fix description, files changed, milestone progress update
73
+ 3. After commit: update milestone state — advance the `current` cursor if the task moved position, log deviations if any Rule 1-3 applied. Do **not** write a progress percent (derived on read by `forge`).
74
+ 4. Report: fix description, files changed, current position
75
75
 
76
76
  ### Without Milestone
77
77
 
@@ -24,7 +24,7 @@ Template directory: `{source}/packages/create-forge/template/`.
24
24
  |----------|-------|----------|
25
25
  | **Framework-owned** | `.claude/agents/*.md`, `.claude/skills/*/SKILL.md` | Overwrite |
26
26
  | **Merge-owned** | `CLAUDE.md`, `.claude/settings.json` | Never auto-overwrite |
27
- | **Template-only** | `.forge/templates/**` | Overwrite |
27
+ | **Template-only** | `.forge/templates/**`, `.forge/migrations/**` | Overwrite |
28
28
 
29
29
  **Never touch** user-generated files: `.forge/project.yml`, `.forge/state/`, `.forge/constitution.md`, `.forge/context.md`, `.forge/requirements/`, `.forge/roadmap.yml`, `.forge/design-system.md`, `.forge/refactor-backlog.yml`.
30
30
 
@@ -40,7 +40,7 @@ For each framework-owned file in the source template:
40
40
 
41
41
  ## Step 4: Sync Template-Only Files
42
42
 
43
- Same process as Step 3 for `.forge/templates/**`.
43
+ Same process as Step 3 for `.forge/templates/**` and `.forge/migrations/**`. (Matches the npm bin's template-only dirs — keeps migration guides installed so the Step 7 pointers resolve via the in-Claude sync route, not just `npx forge-orkes upgrade`.)
44
44
 
45
45
  ## Step 5: Handle Merge-Owned Files
46
46
 
@@ -116,6 +116,35 @@ Run the migration now? (yes/no/show guide)
116
116
  - **show guide** → read and display the file, then re-ask
117
117
  - **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.
118
118
 
119
+ ### Pre-0.17.0 missing `layers:` field
120
+
121
+ Run from project root:
122
+
123
+ ```bash
124
+ grep -q '^layers:' .forge/project.yml || echo "no layers field"
125
+ ls .forge/contracts/index.yml 2>/dev/null
126
+ ```
127
+
128
+ If `project.yml` has no `layers:` key AND `.forge/contracts/index.yml` is absent, surface:
129
+
130
+ ```
131
+ Cross-layer contracts: project.yml predates the `layers:` field (Forge 0.17.0)
132
+ ─────────────────────────────────────────────────────────────────────────────
133
+ Forge 0.17.0 added cross-layer contract detection (planning Step 6.1), which
134
+ reads `layers:` from .forge/project.yml. Without it, planning falls back to a
135
+ coarse top-level-directory heuristic.
136
+
137
+ A migration guide is available at: .forge/migrations/0.17.0-cross-layer-contracts.md
138
+
139
+ Add the layers field now? (yes/no/show guide)
140
+ ```
141
+
142
+ - **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)
143
+ - **show guide** → read and display the file, then re-ask
144
+ - **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.
145
+
146
+ `upgrading` never edits `.forge/project.yml` directly — this is the only path that adds the field, via `quick-tasking`.
147
+
119
148
  ### Future migrations
120
149
 
121
150
  Add new detection blocks here for each Forge version that changes file layout. Pattern:
@@ -0,0 +1,110 @@
1
+ # Migration Guide: Cross-Layer Contracts (Forge 0.17.0)
2
+
3
+ Forge 0.17.0 adds **cross-layer contract detection** to planning (Step 6.1). When a phase introduces or changes an interface that one architectural layer *produces* and another *consumes*, the planner pins the shape in a `contract.md` so an agent building one layer in isolation never has to guess the other's shape.
4
+
5
+ The detector reads a new `layers:` field from `.forge/project.yml`. Projects created before 0.17.0 have no `layers:` field, so the detector falls back to a coarse top-level-directory heuristic. This guide adds the field (and, for genuinely layered projects, a durable contract index) so detection runs precisely. Safe for an agent to run autonomously.
6
+
7
+ ## Why
8
+
9
+ `upgrading` syncs framework files and reference templates but, by design, **never edits your live `.forge/project.yml`** — that is user-owned state. So the new `layers:` field is not added automatically on upgrade. Until it exists, planning Step 6.1 cannot read declared layers and falls back to guessing from top-level source directories, which is noisy and imprecise.
10
+
11
+ Adding `layers:` once makes the detection deterministic. A single-layer project sets `layers: []` to opt out cleanly and silence the upgrade notice.
12
+
13
+ ## Prerequisites
14
+
15
+ 1. Forge framework files upgraded to ≥0.17.0 (run `npx forge-orkes upgrade`, or `Skill(upgrading)` for a local dev sync).
16
+ 2. Working tree clean or changes committed — this touches `project.yml` and optionally adds one new file.
17
+
18
+ ## Detection
19
+
20
+ Run from project root. Migration is suggested if `layers:` is absent:
21
+
22
+ ```bash
23
+ # No `layers:` key in project.yml → migration applies
24
+ grep -q '^layers:' .forge/project.yml || echo "no layers field — migrate"
25
+
26
+ # Already has a contract index? Then layers were declared at init — likely done.
27
+ ls .forge/contracts/index.yml 2>/dev/null
28
+ ```
29
+
30
+ If `project.yml` already contains a `layers:` key (any value, including `[]`), migration is complete — stop here.
31
+
32
+ ## Migration steps
33
+
34
+ ### 1. Identify the project's layers
35
+
36
+ A **layer** is a directory whose code is *produced for* or *consumed by* another across a typed boundary — `engine ↔ ui`, `core ↔ plugins`, `api ↔ web`, `native ↔ bindings`. A single cohesive codebase with no internal producer→consumer boundary is **not** layered.
37
+
38
+ ```bash
39
+ ls -d */ src/*/ 2>/dev/null # top-level + src subdirs as candidates
40
+ # Look for cross-boundary imports: ui importing engine types, generated bindings,
41
+ # ABI/descriptor/schema files shared between directories.
42
+ ```
43
+
44
+ Decisive litmus: *"Would an agent building one directory in isolation have to guess the shape another directory owns?"* If no — it is not a cross-layer boundary.
45
+
46
+ ### 2. Add the `layers:` field to project.yml
47
+
48
+ **Multi-layer project** — list each layer as `{name, path}`:
49
+
50
+ ```yaml
51
+ layers:
52
+ - name: engine
53
+ path: src/engine/
54
+ - name: ui
55
+ path: src/ui/
56
+ ```
57
+
58
+ **Single-layer project** — declare empty to opt out and silence the upgrade notice:
59
+
60
+ ```yaml
61
+ layers: []
62
+ ```
63
+
64
+ Place it near the top of `project.yml` (the template puts it just after `tech_stack`). Keep the file under the 5 KB size gate.
65
+
66
+ ### 3. (Multi-layer only) Seed the durable contract index
67
+
68
+ Standing integration points and their governing ADRs live in `.forge/contracts/index.yml`. Create it from the template so planning Step 6.1 can map an integration point to its authoritative ADR:
69
+
70
+ ```bash
71
+ mkdir -p .forge/contracts
72
+ cp .forge/templates/contracts-index.yml .forge/contracts/index.yml
73
+ ```
74
+
75
+ Then edit `.forge/contracts/index.yml`:
76
+ - Fill `layers:` to match what you wrote in `project.yml`.
77
+ - Add one `integration_points:` entry per standing boundary, each pointing at its governing ADR in `.forge/decisions/`. Leave `integration_points: []` if you have no ADRs yet — the first cross-layer phase will populate it.
78
+
79
+ Single-layer projects skip this step entirely.
80
+
81
+ ## Post-migration verification
82
+
83
+ ```bash
84
+ # Should print the layers key (non-empty list, or [])
85
+ grep -A4 '^layers:' .forge/project.yml
86
+
87
+ # Multi-layer projects: index exists and is not the unfilled template
88
+ test -f .forge/contracts/index.yml && echo "contract index present"
89
+ ```
90
+
91
+ Commit as a single change:
92
+
93
+ ```
94
+ chore(forge): declare layers for cross-layer contract detection (0.17.0)
95
+ ```
96
+
97
+ ## What changes downstream
98
+
99
+ After migration, planning Step 6.1 reads declared layers instead of guessing:
100
+
101
+ - **planning** classifies each phase: Tier 0 (no cross-layer delta), Tier 1 (pin `contract.md`, one plan), or Tier 2 (split into producer/consumer plans against a frozen contract).
102
+ - **executing** runs a seam check on Tier-2 phases — merges the layer plans and verifies the producer's emitted shape matches what the consumer built against.
103
+ - **reviewing** folds each phase `contract.md` delta into its governing ADR (`status: absorbed`) at milestone close.
104
+ - `upgrading` continues to protect `.forge/project.yml` and `.forge/contracts/` as user-owned — it will not overwrite your `layers:` field on future upgrades.
105
+
106
+ A project that stays `layers: []` keeps single-plan decomposition everywhere — the detector is a no-op, exactly as before 0.17.0.
107
+
108
+ ## Rollback
109
+
110
+ Reversible by `git revert` of the migration commit. Removing the `layers:` field returns planning to the top-level-directory fallback; no other state is affected.
@@ -17,17 +17,14 @@ current:
17
17
  status: not_started # not_started | researching | discussing | planning | executing | verifying | reviewing | complete
18
18
  completed_at: null # ISO 8601 timestamp — set when status transitions to complete
19
19
 
20
- progress:
21
- phases_complete: 0 # Number of roadmap phases with all tasks done
22
- phases_total: 0 # Total roadmap phases in this milestone
23
- current_phase_percent: 0 # Task completion % within the current phase (0-100)
24
- overall_percent: 0 # Task completion % across all phases (0-100)
25
- last_update: null # ISO 8601 timestamp
26
- # NOTE: overall_percent measures TASK completion, not WORKFLOW completion.
27
- # A milestone with 100% tasks done still needs verifying, auditing, and refactoring.
28
- # The authoritative workflow position is current.status — not this percentage.
29
- # Agents: do NOT set overall_percent above 100 or use it to determine if a milestone is "done".
30
- # A milestone is only done when current.status == "complete".
20
+ # NO stored progress block. Progress percent is DERIVED on read, never stored.
21
+ # The forge skill computes it from genuinely-maintained signals:
22
+ # total = number of phases listed for this milestone in roadmap.yml
23
+ # done = current.phase - 1 (phases before the active one), or total when current.status == complete
24
+ # percent = round(done / total * 100)
25
+ # Storing a percent here drifts — agents reliably advance current.phase/current.status
26
+ # but did not reliably recompute a stored percentage. current.status is authoritative
27
+ # for "is it done?" (only `complete` means done); the derived percent is display-only.
31
28
 
32
29
  decisions: # Locked decisions (synced from context.md)
33
30
  - decision: ""