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.
- package/bin/create-forge.js +59 -17
- package/package.json +1 -1
- package/template/.claude/skills/executing/SKILL.md +1 -1
- package/template/.claude/skills/forge/SKILL.md +12 -6
- package/template/.claude/skills/quick-tasking/SKILL.md +2 -2
- package/template/.claude/skills/upgrading/SKILL.md +31 -2
- package/template/.forge/migrations/0.17.0-cross-layer-contracts.md +110 -0
- package/template/.forge/templates/state/milestone.yml +8 -11
package/bin/create-forge.js
CHANGED
|
@@ -318,28 +318,70 @@ function detectLegacyRequirementsLayout() {
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
/**
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
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
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
console.log(`
|
|
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
|
@@ -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
|
|
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}, {
|
|
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}, {
|
|
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 `
|
|
30
|
-
8. Report + **immediately route** (Step 3). Show: `current.status`, phase labels (Executed/Verified/Pending/In progress -- **never "Complete" for unverified**), `
|
|
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}) | {
|
|
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 —
|
|
74
|
-
4. Report: fix description, files changed,
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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: ""
|