@journeykit/canon 0.16.0 → 0.18.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.
@@ -25,7 +25,7 @@ a feature, a one-line request included. A human is needed once per scope:
25
25
  `.canon/policy.yaml` (written by `canon init`), which is what the gate
26
26
  grades the work against. When the human answers, record both at once:
27
27
  an `Approved: <who> <when> — "<the human's words>"` line in the change's
28
- `proposal.md`, and
28
+ `.canon/changes/<slug>/brief.md`, and
29
29
  `canon policy approve --by <the human> --note "<their words>"`. Never
30
30
  approve without a human answer, and never edit the policy to make the
31
31
  gate pass.
@@ -36,6 +36,14 @@ a feature, a one-line request included. A human is needed once per scope:
36
36
  stays in the approved change: add it as tasks in that change's
37
37
  `tasks.md`, never with `canon change new`, because a new change is a new
38
38
  scope and needs the human's approval.
39
+ - **A follow-up on a shipped subject reopens it.** Reopen it with
40
+ `canon subject status <id> verifying --reason "<why>" --actor-id <unit>`
41
+ (no human inside the approved scope; the reason and your id are
42
+ recorded), add the follow-up as tasks in the approved change's
43
+ `tasks.md`, then its scenario, evidence and an independent review, and
44
+ ship again with `canon subject status <id> shipped`, which runs every
45
+ guard again. `canon status` flags a shipped subject whose gate went red
46
+ and names the reopen.
39
47
 
40
48
  When no human can answer one of the brief's questions, write the question
41
49
  into the brief, state the assumption you will build on, and say so in your
@@ -44,17 +52,24 @@ work inside an approved scope, never to the approval itself.
44
52
 
45
53
  1. **Brief.** For a new subject or a scope change, write down the request
46
54
  verbatim, your assumptions, and the questions you could not ask. It
47
- becomes the change's `openspec/changes/<slug>/proposal.md`, which
48
- `canon change new <slug> --subject <id> --title "…"` scaffolds once the
49
- subject exists (step 2). Once it is there, stop and report the brief,
55
+ becomes the change's `.canon/changes/<slug>/brief.md`, which
56
+ `canon change new <slug> --subject <id> --title "…"` writes once the
57
+ subject exists (step 2). A change that comes from an external plan the
58
+ human prepared (an openspec `proposal.md` read through
59
+ `plans.sources`) gets its brief there too: a `brief.md` whose
60
+ `Source: <path>` line points at the plan; canon never edits the plan.
61
+ Once the brief is there, stop and report it,
50
62
  asking the human once for the brief and the policy; go on to step 3
51
63
  only after the human answers, and record the answer as above. Inside an
52
64
  approved scope there is no new brief: go to step 3 under the existing
53
65
  subject and change.
54
66
  2. **Subject.** `canon subject new <id> --domain <domain> --title "…"`, then
55
67
  `canon change new <slug> --subject <id> --title "…"`, and put the brief in
56
- the change's `proposal.md`. For a new subject or a scope change, this is
57
- where you stop until the human answers (step 1).
68
+ the `.canon/changes/<slug>/brief.md` it writes. For a new subject or a
69
+ scope change, this is where you stop until the human answers (step 1).
70
+ A change that also serves another subject is adopted there too,
71
+ `canon subject adopt <slug> --subject <other>`: it then belongs to both,
72
+ and its open blockers gate both.
58
73
  3. **Scenarios.** `canon feature new <area>.<surface> --title "…"`, then one
59
74
  `canon scenario new <area>.<surface>.<nn> --subject <id> --case <case> --title "…"`
60
75
  per behavior, with failure paths (`--case failure`) as well as `happy`
@@ -66,7 +81,10 @@ work inside an approved scope, never to the approval itself.
66
81
  one of them prints `NOT refreshed` (a format violation, such as a
67
82
  hand-written header with no `# canon:` provenance comment, or a
68
83
  duplicate id) run `canon inventory sync`: it names each problem; fix
69
- them, and the next command indexes the files.
84
+ them, and the next command indexes the files. Specs live at
85
+ `<root>/kind=feature/area=<area>/<surface>.feature` under a
86
+ `specs.roots[]` root (default `.canon/specs`); a root still on
87
+ `features/kind=feature/` is refused until you run `canon migrate`.
70
88
  4. **Units.** Split the work into units (for example specs, domain code,
71
89
  assets, review). Give each unit its own actor id and session id, and pass
72
90
  them on everything the unit records (`--actor-id <unit> --session-id <session>`),
@@ -81,12 +99,20 @@ work inside an approved scope, never to the approval itself.
81
99
  `canon evidence add --project-id <root-id> --scenario-id <id> --kind test-run --role implementer --ref "<command>" --actor-id <unit> --session-id <session>`,
82
100
  with `--report junit:<path>` or `--artifact <path>` when files prove it.
83
101
  canon stores those bytes in `.canon/artifacts/sha256/` (commit it with the
84
- ledger) and `canon gate check` re-checks them. For a scenario about what
102
+ ledger) and `canon gate check` re-checks them; the bound copy is the one
103
+ kept, so the working-tree report itself may be gitignored. For a scenario about what
85
104
  users see, bind the screenshot or recording that shows it
86
105
  (`--artifact <path>`). Under `write: { promote: auto }` (what
87
106
  `canon init` writes) the record is final when the command returns; only
88
107
  in a repo with `write.promote: manual` (or no `write:` key) follow it
89
108
  with `canon gate promote`.
109
+ One test run that proves several scenarios is bound once:
110
+ `canon evidence add --project-id <root-id> --all-scenarios --report <junit|cucumber|swift-testing>:<path> --kind test-run --role implementer --ref "<command>" --actor-id <unit> --session-id <session>`
111
+ writes one record per indexed scenario owning a case, each with the
112
+ verdict its own cases show; any refusal writes none. When a new record
113
+ makes a signed review stale, the add prints the reviewer and the
114
+ `canon review add … --pin <sha>` that re-pins it; the reviewer looks
115
+ again before running it.
90
116
  An external evaluator's audit is recorded with `canon evidence add --kind
91
117
  audit`; canon never generates one.
92
118
  Evidence for a plan task (`--task <id>`, before `canon gate task`) must
@@ -95,16 +121,24 @@ work inside an approved scope, never to the approval itself.
95
121
  change's `tasks.md` as it is now, so a task row you add needs no
96
122
  `canon ingest plans` first.
97
123
  7. **Independent review.** A different session from the one that wrote the
98
- code and evidence reviews the change. The reviewer records every defect
124
+ code and evidence reviews the change. The review session must be able
125
+ to run the checks it signs off (the build, the tests, the app); a
126
+ reviewer that cannot run the tests says so and does not sign off. The
127
+ reviewer records every defect
99
128
  the review finds as a finding:
100
129
  `canon finding add --change-id <slug> --round <n> --seq <n> --severity <blocker|should-fix|note> --reviewer <me> --actor-id <me> --session-id <session> --summary "…"`;
130
+ `canon finding list --change-id <slug> [--open] [--json]` shows the
131
+ change's findings (round, seq, severity, disposition, reviewer, summary),
132
+ so the next seq and the open blockers are one command away;
101
133
  once fixed,
102
134
  `canon finding close --change-id <slug> --round <n> --seq <n> --disposition fixed --resolution-sha <sha>`
103
135
  (a short sha is fine; canon resolves it). With `write.promote: manual`,
104
136
  follow each with `canon gate promote`. Every bug fix adds or extends a scenario. The
105
137
  reviewer signs off each scenario:
106
138
  `canon review add --project-id <root-id> --scenario-id <id> --reviewer <who> --actor-id <who> --role reviewer --session-id <review-session> --pin <sha> --upstream-ref <ref>`
107
- (written directly; nothing to promote). A reviewer of user-visible
139
+ (written directly; nothing to promote). Repeat `--scenario-id`, or pass
140
+ `--scenario-ids-from <file>`, to sign off several scenarios in one
141
+ call, all or nothing. A reviewer of user-visible
108
142
  behavior attaches what it looked at with `--artifact <path>`
109
143
  (repeatable); canon stores and re-checks those bytes and never compares
110
144
  images.
@@ -114,17 +148,22 @@ work inside an approved scope, never to the approval itself.
114
148
  blocker findings); fix the gap rather than overriding, and finish with
115
149
  `canon gate check` clean. A root whose `.feature` edits could not be
116
150
  indexed fails the gate and refuses `shipped` (`uncovered-cell <root>`;
117
- see step 3). `shipped` also refuses while the change's `proposal.md`
118
- has no `Approved:` line, and `canon evidence add` refuses the subject's
119
- scenarios and tasks the same way. Both are backstops: the stop itself
120
- is at step 1, so you never reach them for a brief the human has not
121
- answered. You record the line only from the human's answer (with no
122
- `proposal.md` there is no brief to answer: neither refuses, and the
151
+ see step 3). `shipped` also refuses while the change's
152
+ `.canon/changes/<slug>/brief.md` has no `Approved:` line (for a change
153
+ from an external plan with no canon brief yet, it names the `brief.md`
154
+ to create with its `Source:` line), and `canon evidence add` refuses the
155
+ subject's scenarios and tasks the same way. Both are backstops: the stop
156
+ itself is at step 1, so you never reach them for a brief the human has
157
+ not answered. You record the line only from the human's answer (with no
158
+ brief to answer there is nothing to refuse, and the
123
159
  ship guard only prints an advisory). Move the subject to
124
160
  `shipped` yourself once `canon subject status <id> shipped` passes its
125
161
  guard, unless the brief says the human ships. `canon status` shows
126
162
  whether each subject's brief is approved, by the same check, and names
127
- the next step.
163
+ the next step. `shipped → verifying` reopens a shipped subject for a
164
+ follow-up (see the working loop above): it needs `--reason`, and it is
165
+ refused, naming the brief, when the subject's newest change is not
166
+ approved.
128
167
 
129
168
  Every write command says whether the record is final (`written directly`,
130
169
  or `staged and promoted in this run`) or staged; only a staged record
@@ -137,13 +176,26 @@ Load only the reference the current step needs.
137
176
  ### Setup and provider projection
138
177
 
139
178
  ```bash
140
- canon init # canon.yaml, starter policy (--policy light|standard|strict, default strict; see canon-policy), AGENTS.md block, plans home
141
- canon skills install # detects .claude/.agents|.codex/.omp/.pi; if none exists, installs Claude+Codex
142
- canon skills install --providers=claude,codex,omp,pi
179
+ canon init # canon.yaml, starter policy (--policy light|standard|strict, default strict; see canon-policy), AGENTS.md block, commented plans examples
180
+ canon init --providers=claude,codex # harnesses for the canon skill; without it init asks on a terminal, else takes the detected .claude/.codex/.omp/.pi
181
+ canon skills install # follows canon.yaml skills.providers (else the install lock's, else the detected ones; none detected installs nothing)
182
+ canon skills install --providers=claude,codex,omp,pi # any subset or none; rewrites skills.providers
143
183
  canon skills check # read-only drift check
144
- canon skills doctor # diagnostics; never deletes user files
184
+ canon skills doctor # diagnostics, incl. a stray canon projection for an unselected provider and a projection a formatter rewrote; never deletes user files
145
185
  ```
146
186
 
187
+ `install` and `doctor` end with `owned-paths:`, the globs of every path
188
+ canon writes. Add them to the repo's formatter and linter ignore lists; a
189
+ formatter that rewrites a projection or a ledger record makes canon read
190
+ drift. Specs, `policy.yaml` and the changes are written by hand and are not
191
+ on the list. `doctor` reports a projection whose bytes changed
192
+ but whose content did not as `formatter-rewritten` and names the ignore
193
+ entry to add; a changed file stays `stale`.
194
+
195
+ The harness list lives in `canon.yaml` as `skills: { providers: [...] }`.
196
+ `.agents` alone never selects Codex; `.codex` does. Change the list with
197
+ `canon skills install --providers=<list>`, never by assuming a default.
198
+
147
199
  The canonical bundle is read-only. Claude receives
148
200
  `.claude/skills/canon/SKILL.md`; Codex receives
149
201
  `.agents/skills/canon/SKILL.md`; OMP receives `.omp/skills/canon/SKILL.md`; and
@@ -105,7 +105,7 @@ format-clean scenario, and `canon status`, `canon gate check`,
105
105
  index added, edited and deleted `.feature` files before they read. When
106
106
  one of them prints `NOT refreshed`, the corpus has a format violation
107
107
  (for example a hand-written header with no `# canon:` line) or a
108
- duplicate id; `canon inventory sync` (or `canon format specs`) names it.
108
+ duplicate id; `canon inventory sync` (or `canon format <root>`) names it.
109
109
  Fix it and the next command indexes the files (see `canon-inventory`).
110
110
 
111
111
  - `--actor <id>` (or `CANON_ACTOR` in the environment) puts YOUR id on the
@@ -158,7 +158,7 @@ Fix it and the next command indexes the files (see `canon-inventory`).
158
158
  ## Worked example
159
159
 
160
160
  ```
161
- specs/features/kind=feature/area=checkout/
161
+ .canon/specs/kind=feature/area=checkout/
162
162
  cart.feature # surface: the cart screen
163
163
  payment.feature # surface: the payment step
164
164
  ```
@@ -3,7 +3,7 @@
3
3
  `canon format --check` is canon's read-only format validator over a
4
4
  consumer-repo artifact corpus: ledger records (run/drill/review/clear/
5
5
  code-review/design-review), divergence events (manifest/review/
6
- remediation), `features/`, `inventory/`, and `policy.yaml`. It never
6
+ remediation), `kind=feature/` spec files, `inventory/`, and `policy.yaml`. It never
7
7
  rewrites or mutates the corpus — it only validates and reports.
8
8
 
9
9
  ## Running
@@ -17,7 +17,7 @@ a report grouped by failure class in this fixed order:
17
17
 
18
18
  | Failure class | Meaning | Fix |
19
19
  |---|---|---|
20
- | `layout-grammar` | Wrong Hive path shape — a `features/`/`inventory/` file not under a `kind=<kind>/area=<area>/` prefix, a wrong leaf filename, or `assets.lock`'s ad-hoc format. | Move the file under the correct `kind=…/area=…/` prefix with the expected leaf name. |
20
+ | `layout-grammar` | Wrong Hive path shape — a spec or `inventory/` file not under a `kind=<kind>/area=<area>/` prefix, a wrong leaf filename, or `assets.lock`'s ad-hoc format. A spec root still on the pre-0.17 layout (any `.feature` file under `<root>/features/`) is refused with one violation at path `features`. | Move the file under the correct `kind=…/area=…/` prefix with the expected leaf name. For a pre-0.17 root, run `canon migrate`, which moves `features/kind=feature/` to `<root>/kind=feature/`. |
21
21
  | `missing-envelope` | A YAML file (`inventory/`, `policy.yaml`) has no top-level `schema`/`kind`/`at`/`actor` keys. | Add the four envelope keys at the top level. |
22
22
  | `missing-provenance` | A `.feature` file's `Feature:`/`Scenario:` header has no `# canon: {...}` comment either directly above it (with its tags, no blank line) or directly below it. | Use `canon scenario new` / `canon feature new`, which stamp it; never hand-type the line. See `canon-authoring`. |
23
23
  | `missing-actor` | No structured `actor` object — a bare `by` string, or absent. | Replace `by: <name>` with a structured `actor` object. |
@@ -28,7 +28,7 @@ Every violation carries one of these stable, grep-able strings:
28
28
  | `malformed-evidence` | A candidate record doesn't parse, is misfiled, or carries an unparseable interim tag. Malformed evidence is no evidence. |
29
29
  | `flagged` | The human-only `flagged` overlay is set — never green regardless of passing evidence. |
30
30
  | `unevidenced-flip` | `canon gate task <task_id>` was asked to flip a checkbox with no matching, non-`Divergent` evidence record. |
31
- | `fabricated-evidence` | An evidence note contains a blocklisted marker (`"would pass"`, `"TBD"`, `"n/a"`) or a bare `verified` claim with no attached command result. |
31
+ | `fabricated-evidence` | An evidence note contains a blocklisted marker (`"would pass"`, `"TBD"`, `"n/a"`) standing as a whole word — never inside a path, a file name, a URL or a longer word, so `pack.json/atlas` is not `n/a` — or a bare `verified` claim with no attached command result. `canon evidence add` refuses such a note before writing (`canon evidence add: refused — fabricated-evidence …`, exit 1). |
32
32
  | `open-blocker` | With `spec_coverage.require_review` set (and `block_on_findings`, the default), a Finding whose latest version is severity `blocker`, disposition `open`, on a change an in-scope subject adopted. Subject string `<change_id>#<round>.<seq>`. See `canon-review`. |
33
33
  | `false-success` | The latest audit record (`canon evidence add --kind audit`) of a scenario says `refuted`: an external evaluator found a counterexample to the claimed success. Always on, regardless of policy or sampling, and takes precedence over any declared faithful verdict; only a newer audit (`confirmed` or `indeterminate`) clears it. The detail names the evaluator, its version, its sha256 and the counterexample paths. |
34
34
 
@@ -63,7 +63,7 @@ sync` runs) and says so in one stderr line, silent when the index is
63
63
  fresh:
64
64
 
65
65
  ```
66
- canon gate check: refreshed the scenario index from 1 changed feature file(s) (specs/features/kind=feature/area=cart/add.feature): 1 record(s) written
66
+ canon gate check: refreshed the scenario index from 1 changed feature file(s) (.canon/specs/kind=feature/area=cart/add.feature): 1 record(s) written
67
67
  ```
68
68
 
69
69
  A root that fails to sync (format violations, a duplicate id) is named
@@ -77,6 +77,15 @@ the same violation, and `canon status` lists the sync as its only step.
77
77
  Fix what the sync lists and run the gate again. See `canon-storage`
78
78
  (The scenario index).
79
79
 
80
+ A spec root still on the pre-0.17 layout (a `.feature` file under
81
+ `<root>/features/`) fails closed the same way, whether or not the index
82
+ already holds its scenarios: the gate adds `uncovered-cell <root> — spec
83
+ root `<root>` (`<path>`) is on the pre-0.17 layout (…)…; until then its
84
+ scenarios are not graded` and exits 1, the ship guard refuses with the
85
+ same violation, and `canon status` names `canon migrate` as its only
86
+ step. `canon migrate` moves `features/kind=feature/` to
87
+ `<root>/kind=feature/`; run it, then the gate again.
88
+
80
89
  ### The spec-derived worklist (`spec_coverage`, opt-in)
81
90
 
82
91
  Coverage groups the EVIDENCE corpus, so it can only ever enumerate
@@ -246,19 +255,32 @@ reports it, and builds nothing for that scope until the human answers.
246
255
  `canon evidence add` is the backstop for that stop (0.16 M7). It
247
256
  refuses (exit `1`, nothing staged or stored) while a subject the record
248
257
  attests for has an enforced, unapproved brief: the newest adopted
249
- change has a `proposal.md` under the openspec plan sources with no
250
- `Approved:` line (the check `canon status` and the ship guard use; see
258
+ change's canon `.canon/changes/<change>/brief.md` has no `Approved:`
259
+ line, or there is no canon `brief.md` yet but the change comes from an
260
+ external openspec plan with a `proposal.md` under the plan sources (the
261
+ check `canon status` and the ship guard use; see
251
262
  `canon-subject`). The subjects are the scenario's `@subject` owner for
252
263
  `--scenario-id`, and every subject adopting the task's change for
253
264
  `--task`:
254
265
 
255
266
  ```
256
- canon evidence add: unreviewed-promotion demo-subject — evidence for world.demo.01: brief not approved — openspec/changes/c-demo/proposal.md has no `Approved:` line, so the human has not answered it
257
- canon evidence add: refused — stop, report the brief to the human and wait for their answer: nothing is built or attested for a subject before its brief is approved. Record their words as `Approved: <who> <when> — "<their words>"` in that proposal.md only from that answer; to attest anyway, pass --override-reason <text>, which is recorded on the evidence record
267
+ canon evidence add: unreviewed-promotion demo-subject — evidence for world.demo.01: brief not approved — .canon/changes/c-demo/brief.md has no `Approved:` line, so the human has not answered it; once they do, record their words as `Approved: <who> <when> — "<their words>"` in .canon/changes/c-demo/brief.md
268
+ canon evidence add: refused — stop, report the brief to the human and wait for their answer: nothing is built or attested for a subject before its brief is approved. Record their words as `Approved: <who> <when> — "<their words>"` in that brief.md only from that answer; to attest anyway, pass --override-reason <text>, which is recorded on the evidence record
258
269
  ```
259
270
 
260
- A subject with no brief to answer (no openspec source, no adopted change,
261
- no `proposal.md`) and a scenario no subject owns are unaffected.
271
+ For a change from an external plan with no canon brief yet, the first
272
+ line says instead ``.canon/changes/c-ext/brief.md does not exist yet:
273
+ change `c-ext` comes from the external plan
274
+ openspec/changes/c-ext/proposal.md, which canon reads and never edits, so
275
+ its approval lives in that canon file, so the human has not answered it;
276
+ once they do, create .canon/changes/c-ext/brief.md holding a `Source:
277
+ openspec/changes/c-ext/proposal.md` line and record their words there as
278
+ `Approved: <who> <when> — "<their words>"` ``. An `Approved:` line in the
279
+ external `proposal.md` itself is never read.
280
+
281
+ A subject with no brief to answer (no adopted change, or a newest change
282
+ with neither a canon `brief.md` nor an external `proposal.md`) and a
283
+ scenario no subject owns are unaffected.
262
284
  `canon review add` and `canon finding add` are never refused.
263
285
  `--override-reason "<one line>"` attests anyway, prints the `waived …`
264
286
  lines and `override recorded by `<actor>` for the N brief refusal(s)
@@ -298,8 +320,9 @@ review waivers: 1 advisory(ies) — not failing the gate:
298
320
  The waiver lasts only until the subject's next status write.
299
321
 
300
322
  `canon subject status <id> shipped` also refuses where a brief exists and
301
- has no `Approved:` line (the subject's newest adopted change has a
302
- `proposal.md`; see `canon-subject`), as an `unreviewed-promotion` naming
323
+ has no `Approved:` line (the subject's newest adopted change has a canon
324
+ `brief.md`, or an external `proposal.md` and no canon `brief.md` yet;
325
+ see `canon-subject`), as an `unreviewed-promotion` naming
303
326
  the subject. With no brief to answer it prints an advisory and does not
304
327
  refuse. Like the evidence refusal above, it is a backstop: the stop is at
305
328
  the brief. `canon gate check` does not grade briefs, so a waived ship
@@ -317,7 +340,10 @@ scenario) that carry no pins were written before 0.15. Each still names
317
340
  `scenario_digest`) equals its current digest, and no declared evidence
318
341
  record of the scenario is newer than the review. It counts. When the
319
342
  scenario lived in another `.feature` file at that commit, the spec
320
- root's files at that commit are searched for its id.
343
+ root's files at that commit are searched for its id, then the files
344
+ under each of the root's `former_roots` (where `canon migrate` moved
345
+ it from, e.g. `specs` for a root now at `.canon/specs`). Only the
346
+ root's own directories are searched, never another root's.
321
347
  - **stale** — the scenario changed since the commit, or it has newer
322
348
  evidence. It no longer counts, and an in-scope scenario left with no
323
349
  other qualifying review is `unreviewed-promotion`:
@@ -365,6 +391,8 @@ canon evidence add --scenario-id cart.add.04 --project-id root \
365
391
  # a JUnit XML or Cucumber JSON report: also records the matched case + outcome
366
392
  canon evidence add ... --report junit:target/nextest/default/junit.xml
367
393
  canon evidence add ... --report cucumber:reports/cucumber.json
394
+ # a Swift Testing event stream (swift test --experimental-event-stream-output <path>)
395
+ canon evidence add ... --report swift-testing:.build/events.jsonl
368
396
  ```
369
397
 
370
398
  A case is bound when the scenario id is its OWN: the first
@@ -382,6 +410,12 @@ verdict when ANY bound case failed or was skipped refuses (exit 1). Files
382
410
  must be inside the repository, are recorded by relative path, and are
383
411
  stored (see Bound evidence stays provable).
384
412
 
413
+ A Swift Testing case is named by its display name (`@Test("cart.add.04
414
+ refuses an out-of-stock item")`), else its function name
415
+ (`cart_add_04_refuses()`). A test with a failing issue failed, a skipped
416
+ test was skipped, a test that started and ended without one passed, and
417
+ a test that started and never ended failed.
418
+
385
419
  Strength, weakest first: `attested` (no attachment) < `artifact` (any
386
420
  bound file) < `report` (a parsed report whose case passed). The policy
387
421
  decides whether the gate cares:
@@ -405,6 +439,55 @@ violation in every mode. Canon still cannot know whether the bound test
405
439
  actually exercises the scenario — binding narrows the trust gap, it does
406
440
  not close it.
407
441
 
442
+ ### One report, many scenarios (0.18)
443
+
444
+ One test run proves many scenarios. Bind it once:
445
+
446
+ ```bash
447
+ canon evidence add --project-id root --all-scenarios \
448
+ --report junit:target/nextest/default/junit.xml \
449
+ --kind test-run --ref "cargo nextest run" --role implementer
450
+ ```
451
+
452
+ Each indexed scenario of `--project-id` that owns a case (the rule above)
453
+ gets its own record, with only its own cases. Its verdict comes from
454
+ them: `faithful` when every case passed, `divergent` when one failed,
455
+ `not-applicable` when one was skipped and none failed. `--verdict <v>`
456
+ claims the same verdict for every record instead. A case whose id is not
457
+ indexed is named and skipped (`skipped <id> — not an indexed scenario`);
458
+ a report no indexed scenario owns is refused (exit 2). The records share
459
+ the stored report and every `--artifact`, and the output lists each
460
+ scenario with its verdict on its own line.
461
+
462
+ It is all or nothing: a claimed `faithful` verdict over a failed case,
463
+ a fabrication marker in `--summary`, an unapproved brief for any
464
+ scenario's subject, or a scenario whose latest record binds a file the
465
+ batch does not refuses the whole batch, and nothing is written.
466
+ `--scenario-id`, `--task`, `--report-case`, `--kind audit`,
467
+ `--drop-attachment` and the approval flags name one subject and are
468
+ refused with `--all-scenarios`.
469
+
470
+ A task's evidence with `--report` and no `--scenario-id` binds EVERY case
471
+ in the report, so a `faithful` verdict needs all of them to pass.
472
+
473
+ ### A write that makes a review stale says so (0.18)
474
+
475
+ When new evidence (one record or a batch) supersedes the evidence a
476
+ current pinned review saw, `canon evidence add` prints one line per
477
+ review it makes stale, with the command that re-pins it:
478
+
479
+ ```
480
+ canon evidence add: review by `reviewer-2` of cart.add.01 is now stale (it pinned the evidence this record supersedes); re-pin it: canon review add --project-id root --scenario-id cart.add.01 --reviewer reviewer-2 --actor-id reviewer-2 --role reviewer --pin <HEAD sha> --original-spec-ref specs/kind=feature/area=cart/add.feature
481
+ ```
482
+
483
+ The decision is the gate's own (the same stale-review rule `canon gate
484
+ check` reports). The command repeats the review's reviewer, actor, role,
485
+ session, provenance ref, runtime labels and artifacts, pinned at `HEAD` (`<sha>`
486
+ when the repository has no commit). A record that is only staged says
487
+ the review "goes stale once this evidence is promoted": promote first,
488
+ then re-pin. The reviewer re-reviews before re-pinning; several
489
+ scenarios re-pin in one `canon review add` (see `canon-review`).
490
+
408
491
  ## Bound evidence stays provable (the artifact store)
409
492
 
410
493
  `canon evidence add --artifact/--report` copies each bound file into
@@ -442,20 +525,29 @@ have its bindings checked. A superseded record is not checked —
442
525
  re-attesting is the fix.
443
526
 
444
527
  Files bound to a review (`canon review add --artifact`, see
445
- `canon-review`) are checked the same way for every in-effect review (the
446
- latest per `(project_id, scenario_id, reviewer)`), with one difference:
447
- they were stored when the review was written, so only an intact blob is
448
- clean. A missing or altered blob is `stale-evidence` on the review's
449
- scenario, and the detail names the review: ``review by `<reviewer>`
450
- (actor `<id>`, <time>)``. The working-tree file is reported but never
451
- vouches for it.
528
+ `canon-review`) are checked by the same rule for every CURRENT in-effect
529
+ review: the latest per `(project_id, scenario_id, reviewer)`, less any
530
+ that went stale against its scenario (its spec or evidence pin moved, or
531
+ a review from before pinning is stale at its commit). A stale review is
532
+ superseded — the gate reads the newer review, or reports the missing
533
+ one as `unreviewed-promotion` — so its files are not re-checked, as a
534
+ superseded evidence record's are not. A missing blob whose working-tree
535
+ file still matches is an advisory, listed with the reviewer:
536
+
537
+ ```
538
+ unstored cart.add.01 reports/cart-badge.png (sha256 9848…, review by `reviewer-2`) is verified against the working tree only
539
+ ```
540
+
541
+ Anything else is `stale-evidence` on the review's scenario, and the
542
+ detail names the review: ``review by `<reviewer>` (actor `<id>`,
543
+ <time>)``.
452
544
 
453
545
  `canon evidence vault [--max-artifact-mib <N>]` stores the bytes for
454
- existing records: every attachment whose working-tree file still matches
455
- its digest is copied in, and everything else is listed as not stored,
456
- with both digests. It exits `0` whenever it ran (the gate decides whether
457
- an unstored file matters), `2` when the ledger cannot be read or a write
458
- fails.
546
+ existing evidence records and current reviews (a superseded review's files are not vaulted): every attachment whose
547
+ working-tree file still matches its digest is copied in, and everything
548
+ else is listed as not stored, with both digests. It exits `0` whenever it
549
+ ran (the gate decides whether an unstored file matters), `2` when the
550
+ ledger cannot be read or a write fails.
459
551
 
460
552
  ## Effect-aware risk approvals (`risk_tiers`)
461
553
 
@@ -489,12 +581,19 @@ empty clean map.
489
581
  ## `canon gate task <task_id> [--repo <dir>]`
490
582
 
491
583
  The evidence-gated task checkbox flip. Resolves `<task_id>`
492
- (`<change_id>#<n>`) through `canon.yaml`'s `plans:` sources — the first
493
- configured dialect whose `PlanWriteBack` locates the task's document
494
- wins, so the flip is plan-dialect agnostic (`openspec` change dirs,
495
- `superpowers` plan docs). A repo with no `plans:` section falls back to
496
- the compat default `[{ dialect: openspec, root: <repo> }]`; no source
497
- locating the task at all is a loud failure naming every source consulted.
584
+ (`<change_id>#<n>`) through `.canon/changes/` (canon's own changes, read
585
+ first whenever the directory exists, with no `plans:` entry) and then
586
+ `canon.yaml`'s `plans.sources` — the first dialect whose `PlanWriteBack`
587
+ locates the task's document wins, so the flip is plan-dialect agnostic
588
+ (canon's own `tasks.md`, external `openspec` change dirs, `superpowers`
589
+ plan docs). A repo with no `plans:` section falls back to the compat
590
+ default `[{ dialect: openspec, root: <repo> }]`; no source locating the
591
+ task at all is a loud failure naming every source consulted. A repo with
592
+ no `.canon/changes/` and `sources: []` is refused: "this repo configures
593
+ no plan sources (no `.canon/changes` and no `plans.sources` entries in
594
+ canon.yaml) … write the change with `canon change new`, or list your
595
+ external plans under `plans.sources`" (`canon dispatch --task` says the
596
+ same).
498
597
  It requires
499
598
  a matching non-`Divergent` evidence record, and flips `- [ ]` → `- [x]`
500
599
  with an appended evidence note ONLY on a clean check. The note
@@ -526,6 +625,13 @@ canon gate task my-change#5.2 --repo .
526
625
  This is canon's own authority for the checkbox grammar — always flip
527
626
  through `canon gate task`, never by hand.
528
627
 
628
+ Canon only reads external plans, with this one exception: for a task
629
+ from an external plan source, `canon gate task` flips the checkbox in
630
+ that plan's own `tasks.md` (or plan document). It is the one place canon
631
+ writes into an external plan; their briefs and approvals still live in
632
+ `.canon/changes/<change>/brief.md`. (Open for the maintainer: record
633
+ external flips only in the ledger instead.)
634
+
529
635
  ## `canon gate promote [--repo <dir>] [--dry-run]`
530
636
 
531
637
  Staging → committed: every well-formed record under
@@ -13,7 +13,7 @@ deleted `.feature` files themselves, with the same code this command runs,
13
13
  scoped to the changed files, and say so in one stderr line:
14
14
 
15
15
  ```
16
- canon status: refreshed the scenario index from 1 changed feature file(s) (specs/features/…): 1 record(s) written
16
+ canon status: refreshed the scenario index from 1 changed feature file(s) (.canon/specs/kind=feature/area=cart/add.feature): 1 record(s) written
17
17
  ```
18
18
 
19
19
  So the working loop has no sync step and no `canon format` step: write the
@@ -25,12 +25,43 @@ command. Run `canon inventory sync` when:
25
25
  comment, an emptied file, …) or a duplicate id, so its files were not
26
26
  indexed. `canon gate check` then fails (`uncovered-cell <root>`),
27
27
  `shipped` is refused and `canon status` lists only the sync. `canon
28
- inventory sync` names each problem (`canon format specs` names the format ones);
28
+ inventory sync` names each problem (`canon format <root>` names the format ones);
29
29
  fix them and the next command indexes the files;
30
30
  - `canon status` says the ledger holds no scenario records while the
31
31
  `.feature` files hold some (the same refusal, on an index never built);
32
32
  - you want the whole index rebuilt from every root at once.
33
33
 
34
+ A spec root still on the pre-0.17 layout (a `.feature` file under
35
+ `<root>/features/`) is not refreshed and fails closed the same way: the
36
+ reader names the root and `canon migrate` on stderr and writes no
37
+ tombstones, `canon gate check` fails (`uncovered-cell <root>`) whether or
38
+ not the index already holds its scenarios, `shipped` is refused, and
39
+ `canon status` lists only `canon migrate`. `canon inventory sync` does not
40
+ fix it; run `canon migrate`, which moves `features/kind=feature/` to
41
+ `<root>/kind=feature/`.
42
+
43
+ `canon migrate --dry-run` prints the plan without writing anything, and
44
+ `canon migrate` applies it. Commit first: it refuses while a path it
45
+ moves has uncommitted changes, and when a destination already exists.
46
+ A root at the repo-level `specs` moves to `.canon/specs`, and
47
+ `canon.yaml` records `former_roots: [specs]`, so reviews pinned before
48
+ the move still find their scenarios. Changes `canon change new` wrote
49
+ under `openspec/changes/` move to `.canon/changes/<slug>/`
50
+ (`proposal.md` becomes `brief.md`). An `Approved:` line in an external
51
+ `proposal.md` is copied into a canon pointer `brief.md`. The command
52
+ also writes `skills.providers` from the install lock, adds the missing
53
+ canon `.gitignore` lines, and ends with a full index rebuild. It is
54
+ idempotent, and a run that stopped partway is finished by the next run:
55
+ `canon.yaml` is written atomically, a half-moved change is completed,
56
+ and the openspec source `canon init` wrote is removed only after every
57
+ pointer brief is written and nothing unread is left under
58
+ `openspec/changes/` (each unread directory is named).
59
+
60
+ The reader's refresh treats a file as stale when its latest index
61
+ generation has another `source_digest` or `scenario_digest`. A
62
+ generation written before 0.15 has no `scenario_digest`, so a repo that
63
+ kept an old index gets it rewritten on the first read.
64
+
34
65
  ## Where the index sits in the loop
35
66
 
36
67
  ```
@@ -66,7 +97,7 @@ Per configured root, in order:
66
97
  1. **Validate** — runs `canon format --check` over the root. ANY violation
67
98
  (missing provenance, layout-grammar, …) ABORTS THE WHOLE ROOT: zero
68
99
  records written for it, the violation(s) reported. Never a partial sync.
69
- 2. **Scan** — walks `<root>/features/**/*.feature`, pairing each
100
+ 2. **Scan** — walks `<root>/kind=feature/area=<area>/<surface>.feature`, pairing each
70
101
  `@<area>.<surface>.<nn>` tag with its header label as `title` and a
71
102
  `source_digest` (sha256 over the raw `.feature` bytes). `@subject:<id>`
72
103
  → `subject_id`, `@lane:<v>` → `lane`, `@case:<v>` → `case` (first tag
@@ -100,7 +131,10 @@ specs:
100
131
  root: apps/b/specs
101
132
  ```
102
133
 
103
- - **Absent `specs:` key** → the single default root `{id: root, root: specs}`.
134
+ - **Absent `specs:` key** → the single default root
135
+ `{id: root, root: .canon/specs}`, the one `canon init` writes. An entry
136
+ with no `root:` also resolves to `.canon/specs`. The default root is
137
+ authored and git-tracked; no canon gitignore line covers it.
104
138
  - **Present `specs:` with an empty/missing `roots[]`** → fails LOUD; a
105
139
  present-but-incomplete config never silently resolves to zero roots.
106
140
  - **A malformed entry** (missing `id`, `roots` not a list, `id` not a
@@ -113,6 +147,10 @@ specs:
113
147
 
114
148
  - **A `canon format --check` validation violation** in the root's corpus →
115
149
  whole root aborts, 0 writes, violations reported.
150
+ - **A root on the pre-0.17 layout** (a `.feature` file under
151
+ `<root>/features/`) → a `layout-grammar` violation at path `features`;
152
+ the root aborts, 0 writes, exit 1. Run `canon migrate`. An empty
153
+ leftover `features/` directory is not a refusal.
116
154
  - **A duplicate `scenario_id`** within one root → that root aborts, 0
117
155
  writes, reported as a sync error (a distinct lane from fmt violations).
118
156
 
@@ -1,13 +1,34 @@
1
1
  # canon-plan-import
2
2
 
3
- `canon ingest plans` imports a FOREIGN plan dialect's on-disk state into
4
- canon's `change`/`task` records, through the same validated path every
5
- other ingest family uses. Imported rows carry no special marker — once
3
+ `canon ingest plans` imports plan state into canon's `change`/`task`
4
+ records, through the same validated path every other ingest family uses:
5
+ canon's own changes under `.canon/changes/`, and any FOREIGN plan dialect
6
+ listed in `canon.yaml`. Imported rows carry no special marker — once
6
7
  imported, `canon query` cannot tell an imported row from a hand-authored
7
8
  one, and neither can `canon gate`.
8
9
 
9
- ## 1. Configure a `plans:` source
10
-
10
+ ## 1. Canon's own changes and external `plans:` sources
11
+
12
+ `.canon/changes/` is canon's native plan source (dialect id `canon`):
13
+ one `<slug>/` directory per change holding `brief.md` and `tasks.md`,
14
+ written by `canon change new` (see `canon-subject`). Every plan reader —
15
+ `canon ingest plans`, `canon gate task`, `canon evidence add --task`,
16
+ `canon dispatch --task`, `canon status` and the brief check — reads it
17
+ first whenever the directory exists, with no `plans:` entry. Being first,
18
+ it owns a change id an external source also carries (the external copy
19
+ is the later duplicate, skipped and counted under `duplicate_change_id`).
20
+ Its records carry the actor `canon-plan-import-canon`; an external
21
+ openspec change's carry `canon-plan-import-openspec`. A `brief.md` with a
22
+ `Source: <path>` line is not a change of its own: it is the canon brief
23
+ of a change imported from an external plan (it points at the external
24
+ document and holds the human's `Approved:` line), so the native reader
25
+ skips that directory and the external source keeps the change id.
26
+
27
+ `plans.sources` lists only EXTERNAL plans — documents you prepare outside
28
+ canon, which canon only reads. The one exception: `canon gate task`
29
+ flips the task's checkbox in an external plan's `tasks.md` (see
30
+ `canon-gate`). Their briefs and approvals live in
31
+ `.canon/changes/<change>/brief.md`, never in the external document.
11
32
  `canon.yaml`:
12
33
 
13
34
  ```yaml
@@ -17,17 +38,22 @@ plans:
17
38
  root: . # resolved relative to canon.yaml's directory
18
39
  ```
19
40
 
20
- - `canon init` writes exactly this source, plus an empty
21
- `openspec/changes/` (so the adapter never scans the repo root itself).
22
- - An absent `plans:` section = zero sources (a clean no-op).
41
+ - `canon init` writes `sources: []`, preceded by comments explaining
42
+ `.canon/changes/` and commented examples for `openspec` (`root: .`) and
43
+ `superpowers` (`root: docs/superpowers/plans`). It creates no
44
+ `openspec/changes/` directory.
45
+ - An absent `plans:` section = zero external sources (a clean no-op;
46
+ `.canon/changes/` is still read).
23
47
  - A present section parses STRICTLY: a typo'd key, an unregistered
24
48
  `dialect`, or a nonexistent `root` fails the command loud, naming the
25
- offender.
49
+ offender. `canon` is not a registered dialect: a source naming it is
50
+ refused, since `.canon/changes/` needs no entry.
26
51
  - `root` is whatever the dialect's adapter scans — for `openspec`, a
27
52
  repo root containing `openspec/changes/`, or that changes dir directly.
28
53
 
29
- One-shot override (bypasses `canon.yaml` for a single ad hoc import;
30
- either flag alone fails loud):
54
+ One-shot override (bypasses `canon.yaml` and `.canon/changes/` for a
55
+ single ad hoc import, reading only the source it names; either flag alone
56
+ fails loud):
31
57
 
32
58
  ```bash
33
59
  canon ingest plans --dialect openspec --source ../other-repo
@@ -36,7 +62,7 @@ canon ingest plans --dialect openspec --source ../other-repo
36
62
  ## 2. Run the import
37
63
 
38
64
  ```bash
39
- canon ingest plans # canon.yaml-driven, every configured source
65
+ canon ingest plans # .canon/changes/ + every configured source
40
66
  canon ingest plans --json # machine-readable outcome
41
67
  canon ingest plans --repo ../svc # a specific repo root
42
68
  ```
@@ -60,9 +86,19 @@ canon query --kind task [--json]
60
86
  No dialect/plugin filter exists or is needed — an imported `change`/
61
87
  `task` is an ordinary core record.
62
88
 
89
+ ## The `canon` dialect (`.canon/changes/`)
90
+
91
+ Uses the `openspec` change-dir grammar below, with three differences:
92
+ the brief is `brief.md` (its `## Why` first paragraph is the summary; a
93
+ missing one is a `missing-brief-md` malformed dir), the source root is
94
+ always `.canon/changes/` itself (archived changes under
95
+ `.canon/changes/archive/`), and a `brief.md` carrying a `Source:` line is
96
+ skipped (a pointer brief, above). `tasks.md` uses the same checkbox row
97
+ grammar.
98
+
63
99
  ## The `openspec` dialect
64
100
 
65
- Maps `openspec/changes/**` onto core kinds:
101
+ Maps an external `openspec/changes/**` onto core kinds:
66
102
 
67
103
  | openspec construct | Core mapping |
68
104
  | --- | --- |
@@ -76,7 +112,9 @@ Maps `openspec/changes/**` onto core kinds:
76
112
  A missing/unreadable `proposal.md`, or a basename that isn't a valid
77
113
  `change_id`, skips the WHOLE dir (counted `malformed`); siblings
78
114
  unaffected. A proposal-only dir imports as `Change { status: proposed }`
79
- with zero tasks and zero diagnostics.
115
+ with zero tasks and zero diagnostics. An `Approved:` line in an external
116
+ `proposal.md` is not read: the change's approval lives in
117
+ `.canon/changes/<change>/brief.md` (see `canon-subject`).
80
118
 
81
119
  ## The `superpowers` dialect
82
120
 
@@ -136,9 +174,10 @@ downstream reader's job.
136
174
 
137
175
  ## What this skill does NOT cover
138
176
 
139
- - **Authoring an openspec change dir** (`proposal.md`/`tasks.md`
140
- conventions) — `canon change new` scaffolds one and adopts it into a
141
- subject (see `canon-subject`); this skill covers IMPORTING that shape.
177
+ - **Authoring a change** — `canon change new` writes
178
+ `.canon/changes/<slug>/brief.md` and `tasks.md` and adopts the change
179
+ into a subject (see `canon-subject`); it never scaffolds an openspec
180
+ change dir. This skill covers IMPORTING plans.
142
181
  - **`canon gate check`/`canon gate task`** (the hand-authored one-task-at-
143
182
  a-time path this complements at bulk) — see `canon-gate`.
144
183
  - **Wiring an imported `Task`/`Change` into a `canon gate` decision** —
@@ -135,7 +135,8 @@ fixed finding reopened with `--disposition open` counts as open again.
135
135
  ## `canon review add`
136
136
 
137
137
  ```bash
138
- canon review add --project-id <root-id> --scenario-id <id> \
138
+ canon review add --project-id <root-id> --scenario-id <id> [--scenario-id <id>]... \
139
+ [--scenario-ids-from <file|->] \
139
140
  --reviewer <who> --pin <sha-or-ref> --role <role> \
140
141
  (--upstream-ref <ref> | --original-spec-ref <ref>) [--actor-id <id>] [--session-id <id>] \
141
142
  [--outcome signed|indeterminate|unavailable] \
@@ -143,7 +144,7 @@ canon review add --project-id <root-id> --scenario-id <id> \
143
144
  [--artifact <path>]... [--max-artifact-mib <MiB>]
144
145
  ```
145
146
 
146
- One attributed Review record for one scenario, joined on
147
+ One attributed Review record per scenario, joined on
147
148
  `(project_id, scenario_id)` — both are required because two spec roots
148
149
  may carry the same scenario id. Exactly one provenance ref is required.
149
150
  `--actor-id` defaults to `canon`; `--session-id` fills `actor.session_id`.
@@ -153,6 +154,18 @@ own. It is written directly to the ledger, with no staging step, and says
153
154
  so: `canon review add: wrote <path> (new) — written directly; nothing to
154
155
  promote`.
155
156
 
157
+ ### Several scenarios in one call (0.18)
158
+
159
+ `--scenario-id` is repeatable, and `--scenario-ids-from <file>` (`-` reads
160
+ stdin) adds one id per line; `#` starts a comment and blank lines are
161
+ skipped. One reviewer, pin, provenance ref and set of artifacts cover
162
+ every review; each review pins its own scenario and evidence, and each
163
+ gets its own `pinned` and `wrote` line. The artifacts are stored once.
164
+ It is all or nothing: a scenario with no record, or a line in the file
165
+ that is not a scenario id, refuses the call (exit 2) and writes no review.
166
+ This is also the batch re-pin after `canon evidence add` reports reviews
167
+ it made stale (see `canon-gate`).
168
+
156
169
  ### What the reviewer looked at (artifacts)
157
170
 
158
171
  `--artifact <path>` (repeatable) binds a file the reviewer looked at — a
@@ -173,19 +186,25 @@ The field is absent when no `--artifact` is given. `review add` prints
173
186
  .canon/artifacts/sha256/ (<k> newly stored) — commit them with the
174
187
  ledger`.
175
188
 
176
- `canon gate check` re-hashes the stored blob of every file bound to an
177
- in-effect review (the latest per `(project_id, scenario_id, reviewer)`).
178
- A missing or altered blob is `stale-evidence` on the review's scenario,
179
- naming the review, even when the working-tree file still matches (see
180
- `canon-gate`, "Bound evidence stays provable"):
189
+ `canon gate check` re-checks every file bound to a current in-effect
190
+ review (the latest per `(project_id, scenario_id, reviewer)`, unless it
191
+ went stale against its scenario) by the evidence rule (see `canon-gate`,
192
+ "Bound evidence stays provable"). A missing blob whose working-tree file
193
+ still matches is an advisory naming `canon evidence vault`, which stores
194
+ it back. A missing blob with a changed or missing working-tree file, or
195
+ an altered blob, is `stale-evidence` on the review's scenario, naming
196
+ the review:
181
197
 
182
198
  ```
183
- stale-evidence world.demo.01 — review by `reviewer-2` (actor `reviewer-2`, 2026-10-11T09:00:00Z) bound file `reports/title.png` recorded sha256 <hex>; stored blob .canon/artifacts/sha256/<hex>: missing; working tree: <hex> — the bound bytes are gone, look again and re-record the review with `canon review add --artifact <path>`
199
+ stale-evidence world.demo.01 — review by `reviewer-2` (actor `reviewer-2`, 2026-10-11T09:00:00Z) bound file `reports/title.png` recorded sha256 <hex>; stored blob .canon/artifacts/sha256/<hex>: missing; working tree: <other hex> — the bound bytes are gone, look again and re-record the review with `canon review add --artifact <path>`
184
200
  ```
185
201
 
186
202
  The reviewer's later review replaces the earlier one, so its files are
187
- the ones checked. Canon binds and re-checks bytes only; it never compares
188
- images or judges what a screenshot shows.
203
+ the ones checked. A review that went stale (its scenario's spec or
204
+ evidence moved since) is not checked either: it no longer counts, and the
205
+ scenario's newer review is the one whose files are checked. Canon binds
206
+ and re-checks bytes only; it never compares images or judges what a
207
+ screenshot shows.
189
208
 
190
209
  ### What the review saw (pins)
191
210
 
@@ -104,11 +104,12 @@ canon query --kind trajectory --json
104
104
 
105
105
  canon exposes layered `stg_`/`int_`/`mart_` query views; `canon report`
106
106
  builds its numbers from the `mart_` views. Those read ONLY local roots
107
- — the git ledger and a LOCAL `.canon/r2` parquet mirror. `canon report`
107
+ — the git ledger, the sqlite `hot` db file (local, no credential; read
108
+ since 0.18) and a LOCAL `.canon/r2` parquet mirror. `canon report`
108
109
  never opens a live DB connection or the live bucket, so:
109
110
 
110
- - Kinds routed to `hot` (sqlite/postgres) do NOT appear in `canon
111
- report`.
111
+ - Kinds routed to a postgres `hot` rung do NOT appear in `canon
112
+ report`; kinds routed to `canon init`'s default sqlite `hot` rung do.
112
113
  - Kinds routed to `cold` (s3) appear only if a local `.canon/r2` mirror
113
114
  has been separately materialized — there is no automatic sync.
114
115
 
@@ -192,6 +193,29 @@ write (promote: manual)`, or `[FAIL] write: …`. `canon gate promote` still
192
193
  works under `auto` and is idempotent: with nothing staged it prints
193
194
  `nothing staged` and exits `0`.
194
195
 
196
+ ## Harnesses: the `skills:` section
197
+
198
+ `canon.yaml`'s `skills:` section names the harnesses the canon skill is
199
+ installed for (0.17):
200
+
201
+ ```yaml
202
+ skills:
203
+ providers: [claude, codex] # any of claude, codex, omp, pi; [] for none
204
+ ```
205
+
206
+ `canon init` writes it: on a terminal it asks, with the harnesses whose
207
+ `.claude`, `.codex`, `.omp` or `.pi` directory exists checked;
208
+ `--providers a,b` (or `none`) answers without asking; with neither it takes
209
+ the detected ones, and with none detected it writes only a commented example
210
+ and installs nothing. `.agents` alone never selects Codex. `canon skills
211
+ install|check|doctor` follow the section; `canon skills install
212
+ --providers=<list>` rewrites it in place. Without the section they use the
213
+ install lock's providers, then the detected ones; nothing undetected is ever
214
+ added. A present section that is not this shape, or names another harness,
215
+ fails those commands (exit `1`), and `canon init --check-config` reports
216
+ `[PASS] skills (providers: …)`, `[not configured] skills (…)`, or
217
+ `[FAIL] skills: …`.
218
+
195
219
  ## The scenario index
196
220
 
197
221
  `canon inventory sync` materializes one `Scenario` record per
@@ -207,7 +231,7 @@ that no `.feature` file of it declares any more. The reader then runs
207
231
  covers the whole root) and prints one stderr line:
208
232
 
209
233
  ```
210
- canon status: refreshed the scenario index from 1 changed feature file(s) (specs/features/kind=feature/area=cart/add.feature): 1 record(s) written
234
+ canon status: refreshed the scenario index from 1 changed feature file(s) (.canon/specs/kind=feature/area=cart/add.feature): 1 record(s) written
211
235
  ```
212
236
 
213
237
  A deleted scenario is not erased: `canon inventory sync` — and a reader's
@@ -103,7 +103,7 @@ this subject and domain — the exact memory step 2 retrieves next time.
103
103
 
104
104
  ## Status lifecycle + the shipped evidence gate
105
105
 
106
- `canon subject status <id> <state> [--override-reason <text>] [--actor-id <id>]`
106
+ `canon subject status <id> <state> [--override-reason <text>] [--reason <text>] [--actor-id <id>]`
107
107
  performs a policy-gated transition.
108
108
  The legal chain:
109
109
 
@@ -111,9 +111,10 @@ The legal chain:
111
111
  proposed → specced → building → verifying → shipped
112
112
  ```
113
113
 
114
- plus **any state → retired** (a subject can be retired from anywhere).
115
- Any other transition is rejected by failure class, exits 1, and leaves
116
- the record UNCHANGED (fail closed).
114
+ plus **shipped → verifying** to reopen a shipped subject (with
115
+ `--reason`, below) and **any state → retired** (a subject can be retired
116
+ from anywhere). Any other transition is rejected by failure class, exits
117
+ 1, and leaves the record UNCHANGED (fail closed).
117
118
 
118
119
  ```bash
119
120
  canon subject status subject-domain-loop specced
@@ -148,27 +149,39 @@ empty set; so is one whose `scenario` records route away from the
148
149
  gate's rung. Each refusal prints by failure class (`uncovered-cell`),
149
150
  exits 1, and the status stays `verifying`. `retired` is not gated.
150
151
 
151
- **The brief (0.16):** `verifying → shipped` also refuses where a brief
152
+ **The brief (0.16, 0.17):** `verifying → shipped` also refuses where a brief
152
153
  exists and is not approved, read exactly as `canon status` reads it (one
153
154
  shared check, so status says `ship` only when this passes): the subject's
154
- newest adopted change has a `proposal.md` under the openspec plan sources,
155
- and it carries no `Approved:` line outside HTML comments and code fences.
156
- It runs with or without a policy. The refusal is an `unreviewed-promotion`
157
- naming the subject, and says what to do:
155
+ newest adopted change has a canon `.canon/changes/<change>/brief.md`
156
+ with no `Approved:` line outside HTML comments and code fences, or has no
157
+ canon `brief.md` yet but comes from an external openspec plan with a
158
+ `proposal.md` under the plan sources. It runs with or without a policy.
159
+ The refusal is an `unreviewed-promotion` naming the subject, and says
160
+ what to do:
158
161
 
159
162
  ```
160
- canon subject status: unreviewed-promotion demo-subject — verifying → shipped: brief not approved — openspec/changes/c-demo/proposal.md has no `Approved:` line; a human must answer the brief, and the agent records `Approved: <who> <when> — "<their words>"` there only from that answer
163
+ canon subject status: unreviewed-promotion demo-subject — verifying → shipped: brief not approved — .canon/changes/c-demo/brief.md has no `Approved:` line; a human must answer the brief, and only from that answer the agent will record their words as `Approved: <who> <when> — "<their words>"` in .canon/changes/c-demo/brief.md
161
164
  canon subject status: refused — get the human's answer to the brief and record it as its `Approved:` line, or pass --override-reason <text> to move anyway; the waiver is recorded on the subject
162
165
  ```
163
166
 
167
+ For an external change with no canon brief yet, the reason reads
168
+ ``.canon/changes/<c>/brief.md does not exist yet: change `<c>` comes from
169
+ the external plan <proposal>, which canon reads and never edits, so its
170
+ approval lives in that canon file`` and the agent will ``create
171
+ .canon/changes/<c>/brief.md holding a `Source: <proposal>` line and
172
+ record their words there as `Approved: <who> <when> — "<their words>"` ``.
173
+
164
174
  Never write the line without the human's answer. When there is no brief to
165
- answer (no openspec plan source, no adopted change, or a change with no
166
- `proposal.md`), the move is not refused: it prints
167
- `canon subject status: advisory: <id>'s brief is not approved — <why>, so
168
- there is no brief to answer and shipping is not refused`, and status shows
169
- the brief as not approved, as an advisory. `subjects[].brief.enforced` in
170
- `canon status --json` says which case applies. A brief that says the human
171
- ships changes who runs the transition, not the check.
175
+ answer (no adopted change, or a newest change with neither a canon
176
+ `brief.md` nor an external `proposal.md`), the move is not refused: it
177
+ prints ``canon subject status: advisory: <id>'s brief is not approved —
178
+ its newest adopted change `<c>` has no brief (no
179
+ .canon/changes/<c>/brief.md, no openspec proposal.md), so there is no
180
+ brief to answer and shipping is not refused`` (or `— <id> adopts no
181
+ change, so …`), and status shows the brief as not approved, as an
182
+ advisory. `subjects[].brief.enforced` in `canon status --json` says
183
+ which case applies. A brief that says the human ships changes who runs
184
+ the transition, not the check.
172
185
 
173
186
  This refusal is a backstop, and so is the evidence one: shipping is not
174
187
  the first thing an unapproved brief stops. The agent writes the brief,
@@ -230,6 +243,39 @@ raised later, a scenario tagged later) still fails the gate. Without
230
243
  `require_review`, nothing is printed and nothing changes, except that
231
244
  `shipped` still runs the brief check above.
232
245
 
246
+ ### Reopening a shipped subject (0.18)
247
+
248
+ A follow-up on a shipped subject (a fix, a review round, a scenario its
249
+ gate now misses) reopens it: reopen, add tasks to the approved change,
250
+ scenario, evidence, review, ship.
251
+
252
+ ```bash
253
+ canon subject status demo-subject verifying \
254
+ --reason "follow-up: the retry path leaks a file handle" --actor-id impl
255
+ ```
256
+
257
+ ```
258
+ canon subject status: reopened `demo-subject` from shipped, recorded by `impl`: follow-up: the retry path leaks a file handle; add the follow-up as tasks in its approved change `c-demo`, then scenario, evidence and review; shipping again runs every guard
259
+ canon subject status: demo-subject → verifying — written directly; nothing to promote
260
+ ```
261
+
262
+ - `--reason "<one line>"` is required for `shipped → verifying` and
263
+ refused on any other transition (exit 2, as is a blank one). The new
264
+ record carries it with the actor and the change the work goes into:
265
+ `reopen: { reason: …, change: c-demo, actor: { agent_id: impl } }`. The
266
+ next status write clears it, as it clears a waiver.
267
+ - Inside the approved scope it needs no human: the subject's newest
268
+ adopted change's brief is approved, or there is no brief to answer.
269
+ Otherwise it refuses, exit 1, naming the brief to approve, and
270
+ `--override-reason` does not waive it:
271
+ ``unreviewed-promotion demo-subject — shipped → verifying: reopening needs the approved scope, and the brief is not approved — .canon/changes/c-next/brief.md has no `Approved:` line; …``.
272
+ - The review guard does not run on a reopen (it prints `skipped … — a
273
+ reopen moves the subject back to fix its gaps; shipping again runs
274
+ it`). `verifying → shipped` afterwards runs every guard above again.
275
+ - `canon status` flags a shipped subject whose gate went red (whatever
276
+ `verifying → shipped` would now refuse it on) and its only step is
277
+ this reopen.
278
+
233
279
  ## Adopt flow
234
280
 
235
281
  Planning docs written outside canon become change/task records via
@@ -249,37 +295,63 @@ and a flip re-imports that change itself. Run `canon ingest plans` for a
249
295
  plan written outside canon, or when `canon query --kind task` must list
250
296
  rows nobody has flipped yet.
251
297
 
252
- `adopt` stamps the change's `subject_id` and adds it to the Subject's
253
- `change_ids`, so `canon query --kind change --change-id …` and `canon
254
- query --kind subject` agree on the link from both ends. It writes only
255
- the side that lacks the link, so rerunning it on a linked pair writes
256
- nothing and exits `0`.
298
+ `adopt` adds the subject to the change's `subject_ids` and the change to
299
+ the Subject's `change_ids`, so `canon query --kind change --change-id …`
300
+ and `canon query --kind subject` agree on the link from both ends. It
301
+ writes only the side that lacks the link, so rerunning it on a linked
302
+ pair writes nothing and exits `0`.
303
+
304
+ A change may belong to several subjects (0.18). Adopting it under a
305
+ second subject adds that subject, never re-points the change, and says
306
+ so:
307
+
308
+ ```
309
+ canon subject adopt: linked change `refactor-core` to subject `editing` as well; the change now belongs to `perception` and `editing` — written directly; nothing to promote
310
+ ```
311
+
312
+ Every reader joins a subject to its changes from both sides (the
313
+ subject's `change_ids`, and every change whose `subject_ids` names it):
314
+ the open-blocker rule, the status guard, `canon status`, the brief and
315
+ the evidence refusal. An open blocker on a shared change gates each
316
+ subject it belongs to. A change written before 0.18 names one subject as
317
+ `subject_id`; it reads, and `canon query --kind change --json` shows it,
318
+ as a one-element `subject_ids`.
257
319
 
258
320
  The two records cannot be written atomically, so the subject is written
259
- first: its `change_ids` is what the gate reads a subject's adopted
260
- changes from (`open-blocker`, the status guard), so a half-done adoption
261
- still has that change's open blockers counted. If the change write then
262
- fails, `adopt` exits `2`, names the subject that now lists the change,
263
- and prints `canon subject adopt <change_id> --subject <id>` to complete
264
- it.
321
+ first: a half-done adoption still has that change's open blockers
322
+ counted. If the change write then fails, `adopt` exits `2`, names the
323
+ subject that now lists the change, and prints `canon subject adopt
324
+ <change_id> --subject <id>` to complete it.
265
325
 
266
326
  ### Starting a new change: `canon change new`
267
327
 
268
- For a change that does not exist yet, one command scaffolds it and
269
- records it adopted:
328
+ For a change that does not exist yet, one command writes it and records
329
+ it adopted:
270
330
 
271
331
  ```bash
272
332
  canon change new add-login --subject auth --title "Add login"
273
333
  ```
274
334
 
275
- - **Writes** `openspec/changes/<slug>/proposal.md` (the title is its
276
- `## Why`, which imports as the change summary) and `tasks.md` (no rows
277
- yet, so the change imports as `proposed`), under the repo's first
278
- `openspec` plans source. `canon init` configures that source
279
- (`{dialect: openspec, root: .}`), so `canon.yaml` needs no edit.
280
- - **Reads** the files with the same `openspec` adapter `canon ingest
335
+ ```
336
+ canon change new: wrote .canon/changes/add-login/brief.md
337
+ canon change new: wrote .canon/changes/add-login/tasks.md
338
+ canon change new: recorded change `add-login` adopted into subject `auth` — written directly; nothing to promote
339
+ ```
340
+
341
+ - **Writes** canon's own change dir, `.canon/changes/<slug>/`:
342
+ `brief.md` (`# <title>`, `## Why` holding the title, which imports as
343
+ the change summary, `## What Changes` with a comment saying what goes
344
+ there, and a comment naming where the human's `Approved:` line goes,
345
+ so the file starts unapproved) and `tasks.md` (no rows yet, so the
346
+ change imports as `proposed`; the openspec checkbox row grammar).
347
+ `.canon/changes/` is canon's native plan source: every plan reader
348
+ reads it with no `plans:` entry, so `canon.yaml` needs no edit and no
349
+ external plans source is needed (see `canon-plan-import`).
350
+ - **Reads** the files with the native `canon` plan adapter `canon ingest
281
351
  plans` uses, then **records** the change already adopted, through the
282
- same write as `canon subject adopt`.
352
+ same write as `canon subject adopt`. Its records carry the actor
353
+ `canon-plan-import-canon` (canon-written); an external openspec
354
+ change's carry `canon-plan-import-openspec`.
283
355
  - **What a failure leaves behind.** The files are staged under `.canon/`
284
356
  and the change dir is moved into place only once they parse. A failure
285
357
  before any record is written (staging, publishing the dir, or the
@@ -291,14 +363,23 @@ canon change new add-login --subject auth --title "Add login"
291
363
  plans && canon subject adopt <slug> --subject <id>`, which completes
292
364
  the link and is safe to rerun.
293
365
  - **Refuses** with exit `2`, writing nothing, when the subject does not
294
- exist, when the slug already has a change dir (active or archived) or a
295
- `change` record, or when `canon.yaml` has no `openspec` plans source.
366
+ exist, or when the slug already has a `.canon/changes/<slug>` dir
367
+ (active or under `.canon/changes/archive/`) or a `change` record.
296
368
 
297
369
  Add task rows to `tasks.md` as `- [ ] <n> <title>`, and flip each with
298
370
  `canon gate task <slug>#<n>` once evidence exists. `canon evidence add
299
371
  --task` and the flip read `tasks.md` as it is, so a new row needs no
300
372
  `canon ingest plans` run.
301
373
 
374
+ A change from an external plan (an openspec change listed under
375
+ `plans.sources`, imported by `canon ingest plans` and adopted with
376
+ `canon subject adopt`) gets its brief as a canon file too:
377
+ `.canon/changes/<change>/brief.md` holding a `Source: <path>` line that
378
+ points at the external document and, once the human answers, the
379
+ `Approved:` line. Canon never edits the external document. Such a
380
+ pointer `brief.md` is not a change of its own: the native reader skips
381
+ it, so the external source keeps the change, its summary and its tasks.
382
+
302
383
  ## Where are we: `canon status`
303
384
 
304
385
  ```bash
@@ -332,24 +413,42 @@ uses, so status and the gate cannot disagree about a gap:
332
413
  `require_review` on, a `warning:` line counts the in-effect reviews that
333
414
  predate review pinning (0.15), checked against the commit each pinned:
334
415
  current, stale (named) and undecidable.
335
- - **Brief** (0.16) — for each `building`, `verifying` or `shipped`
416
+ - **Brief** (0.16, 0.17) — for each `building`, `verifying` or `shipped`
336
417
  subject, and each `proposed` subject whose newest adopted change has a
337
- `proposal.md`, a `brief:` line under it says whether the brief is approved:
338
- `brief: approved in <proposal.md> — <the recorded words>`, or
339
- `brief: not approved (…)`. Approval is an `Approved:` line in the
340
- `proposal.md` of the subject's newest adopted change (the last one
341
- adopted): a line that starts with `Approved:` (indent allowed), says
418
+ brief (a canon `brief.md`, or an external `proposal.md`), a `brief:`
419
+ line under it says whether the brief is approved:
420
+ `brief: approved in <brief> — <the recorded words>`, or
421
+ `brief: not approved (…)`. Approval is an `Approved:` line in
422
+ `.canon/changes/<change>/brief.md` for the subject's newest adopted
423
+ change (the last one adopted; a live dir before an archived one), and
424
+ only there: a line that starts with `Approved:` (indent allowed), says
342
425
  something after it, and sits outside HTML comments (`<!-- … -->`,
343
426
  multi-line included) and fenced code blocks (```` ``` ```` or `~~~`),
344
427
  written by the agent once the human answers, as
345
428
  `Approved: <who> <when> — "<the human's words>"`. Fixes and follow-ups
346
429
  live as tasks in the approved change, so a newly adopted change is a
347
- new scope: an older change's approval does not cover it. Status reads
348
- the line from the change dir under the repo's `openspec` plans sources,
349
- in config order (the first source holding the change owns it, a live
350
- dir before an archived one; with no `plans:` section, the `openspec`
351
- source rooted at the repo that `canon gate task` also falls back to).
430
+ new scope: an older change's approval does not cover it. A change from
431
+ an external plan has its brief in the same canon file, with a
432
+ `Source: <path>` line pointing at the external document; an `Approved:`
433
+ line inside an external `proposal.md` is not read. Before that canon
434
+ file exists, an external openspec `proposal.md` under the plan sources
435
+ (the first source holding the change; with no `plans:` section, the
436
+ `openspec` source rooted at the repo that `canon gate task` also falls
437
+ back to) still makes the brief one to answer, and the line reads
438
+ ``brief: not approved (.canon/changes/<c>/brief.md does not exist yet:
439
+ change `<c>` comes from the external plan <proposal>, which canon reads
440
+ and never edits, so its approval lives in that canon file)``. With a
441
+ canon brief and no line it reads
442
+ ``brief: not approved (<brief> has no `Approved:` line)``.
352
443
  There is no record kind for it; the file is the record.
444
+ - **Red shipped subject** (0.18) — a `shipped` subject that the
445
+ `verifying → shipped` guard would now refuse, by the guard's own checks
446
+ (owned scenarios, verdicts, required cases, audits, and, where
447
+ `require_review` covers `shipped`, reviews and open blockers under
448
+ `block_on_findings`; a gap its waiver recorded is not counted), prints
449
+ ``gate: red — <n> <class> (first: <subject>), …; reopen with `canon subject status <id> verifying --reason "<why it reopens>" --actor-id <unit>` ``,
450
+ carries `red: [...]` in `--json`, and that reopen is its only `next:`
451
+ step. Open blockers count against every subject a change belongs to.
353
452
  - **Unowned** — scenarios with no `@subject:` tag.
354
453
  - **Audits** — how many scenarios are sampled for
355
454
  `spec_coverage.require_audit`, confirmed, owed (no audit or
@@ -372,7 +471,7 @@ uses, so status and the gate cannot disagree about a gap:
372
471
  itself first, so this means the refresh refused on a format violation
373
472
  or a duplicate id, which the sync names). Then per subject, verifying
374
473
  first, then building, specced, proposed, shipped: an unapproved brief
375
- (building and later, and proposed once its change has a `proposal.md`)
474
+ (building and later, and proposed once its change has a brief)
376
475
  → the human step below, which is then the subject's
377
476
  only step; no scenario → `canon scenario new … --subject <id>` (and, when no
378
477
  `.feature` file holds a scenario yet, `canon feature new` first, unless
@@ -389,12 +488,20 @@ uses, so status and the gate cannot disagree about a gap:
389
488
  carries `--project`: the owning root for a missing case, `<root-id>`
390
489
  (the reason lists the roots) for a subject that owns no scenario yet.
391
490
  - **The one human step.** Every step is the agent's to run except one:
392
- a subject whose brief is enforced (its newest change has a
393
- `proposal.md` with no `Approved:` line) gets
394
- `human: stop and report the brief in <proposal.md>; build and attest nothing for <id> until the human answers (one answer approves brief and policy)`,
395
- whose reason names that `proposal.md`, says `canon evidence add`
396
- refuses the subject's scenarios and tasks until the line is recorded,
397
- and names `canon policy approve --by <the human> --note "<their words>"`.
491
+ a subject whose brief is enforced (its newest change's canon
492
+ `brief.md` has no `Approved:` line, or it has no canon `brief.md` yet
493
+ and comes from an external openspec `proposal.md`) gets
494
+ `human: stop and report the brief in <brief>; build and attest nothing for <id> until the human answers (one answer approves brief and policy)`,
495
+ where `<brief>` is the canon `brief.md`, or the external `proposal.md`
496
+ when no canon brief exists yet. Its reason reads ``<id>: <why
497
+ unanswered> (an older change's approval does not cover a new scope),
498
+ and the approval is never assumed: `canon evidence add` refuses <id>'s
499
+ scenarios and tasks until it is recorded; once the human answers,
500
+ <record> and run `canon policy approve --by <the human> --note "<their
501
+ words>"` ``, where `<record>` is ``record their words as `Approved: <who>
502
+ <when> — "<their words>"` in <brief>``, or, for an external change,
503
+ ``create <brief> holding a `Source: <proposal>` line and record their
504
+ words there as `Approved: <who> <when> — "<their words>"` ``.
398
505
  It is the subject's only step, whatever its status: nothing suggests
399
506
  `canon feature new`, `canon scenario new`, `canon evidence add`, a
400
507
  review or a move-on for it before the human answers, and
@@ -402,9 +509,9 @@ uses, so status and the gate cannot disagree about a gap:
402
509
  approval is never assumed. A subject with no brief to answer gets no human step;
403
510
  its `brief:` line says
404
511
  `not approved (no adopted change; advisory, nothing to approve yet)` or
405
- ``not approved (change `<id>` has no proposal.md; advisory, nothing to
512
+ ``not approved (change `<id>` has no brief; advisory, nothing to
406
513
  approve yet)`` and nothing is withheld (a `proposed` subject with no
407
- `proposal.md`, and a `specced` subject, show no `brief:` line at all). Once nothing blocks, a
514
+ brief, and a `specced` subject, show no `brief:` line at all). Once nothing blocks, a
408
515
  ship-ready verifying subject gets the agent's own
409
516
  `canon subject status <id> shipped`; status never asks a human to ship.
410
517
 
@@ -424,21 +531,21 @@ Status fails closed rather than suggest progress the gate would refuse:
424
531
 
425
532
  ```json
426
533
  {
427
- "statusVersion": 2,
428
- "canonVersion": "0.16.0",
534
+ "statusVersion": 3,
535
+ "canonVersion": "0.17.0",
429
536
  "policy": { "present": true, "spec_coverage": { "require_evidence": true, "scope": [], "exclude_lanes": [], "require_cases": ["failure"], "require_review": { "scope": ["verifying", "shipped"], "distinct_actor": true, "block_on_findings": true } } },
430
537
  "warnings": [],
431
538
  "subjects": [
432
539
  { "id": "cart", "title": "Cart", "status": "building", "scenarios": 2, "excluded": 0, "evidenced": 2, "divergent": 0,
433
540
  "reviewed": 1, "staleReviews": 0, "unsignedReviews": 0, "reviewDue": true, "openBlockers": 1, "missingCases": [],
434
- "brief": { "approved": true, "enforced": false, "change": "c-cart", "proposal": "openspec/changes/c-cart/proposal.md", "approval": "Journey 2026-10-11 — \"approved\"" } },
541
+ "brief": { "approved": true, "enforced": false, "change": "c-cart", "brief": ".canon/changes/c-cart/brief.md", "briefExists": true, "approval": "Journey 2026-10-11 — \"approved\"" } },
435
542
  { "id": "promo", "title": "Promo", "status": "verifying", "scenarios": 1, "excluded": 0, "evidenced": 1, "divergent": 0,
436
543
  "reviewed": 1, "staleReviews": 0, "unsignedReviews": 0, "reviewDue": true, "openBlockers": 0, "missingCases": [],
437
- "brief": { "approved": false, "enforced": true, "change": "c-promo", "proposal": "openspec/changes/c-promo/proposal.md" } }
544
+ "brief": { "approved": false, "enforced": true, "change": "c-promo", "brief": ".canon/changes/c-promo/brief.md", "briefExists": false, "source": "openspec/changes/c-promo/proposal.md" } }
438
545
  ],
439
546
  "unowned": ["cart.promo.01"],
440
547
  "next": [
441
- { "command": "stop and report the brief in openspec/changes/c-promo/proposal.md; build and attest nothing for promo until the human answers (one answer approves brief and policy)", "why": "promo: its newest adopted change's proposal.md carries no `Approved:` line …", "actor": "human" },
548
+ { "command": "stop and report the brief in openspec/changes/c-promo/proposal.md; build and attest nothing for promo until the human answers (one answer approves brief and policy)", "why": "promo: .canon/changes/c-promo/brief.md does not exist yet: change `c-promo` comes from the external plan openspec/changes/c-promo/proposal.md, which canon reads and never edits, so its approval lives in that canon file (an older change's approval does not cover a new scope), and the approval is never assumed: … once the human answers, create .canon/changes/c-promo/brief.md holding a `Source: openspec/changes/c-promo/proposal.md` line and record their words there as `Approved: <who> <when> — \"<their words>\"` and run `canon policy approve --by <the human> --note \"<their words>\"`", "actor": "human" },
442
549
  { "command": "canon finding close --change-id c-cart --round 1 --seq 1 --disposition fixed --resolution-sha <sha> --actor-id <unit> --session-id <session>", "why": "cart: 1 open blocker finding(s) (first: …); fix it, commit, then close it", "actor": "agent" },
443
550
  { "command": "canon review add --project-id root --scenario-id cart.add.02 --reviewer <reviewer> --actor-id <reviewer> --session-id <review-session> --role reviewer --pin <sha> --original-spec-ref \"<feature file>\"", "why": "cart: 1 of 2 scenario(s) lack a qualifying review; …", "actor": "agent" },
444
551
  { "command": "canon status", "why": "1 scenario(s) carry no @subject tag (first: cart.promo.01); add `@subject:<id>` above each in its .feature file, then check again: …", "actor": "agent" }
@@ -451,21 +558,29 @@ Each `missingCases` entry is `{ "projectId", "surface", "case" }`: the
451
558
  spec root, the `<area>.<surface>`, and the required case it lacks.
452
559
 
453
560
  `brief` is present for `building`, `verifying` and `shipped` subjects,
454
- and for a `proposed` subject whose newest adopted change has a
455
- `proposal.md`. `change` is the newest adopted change (omitted when the
456
- subject adopts none), `proposal` its `proposal.md` (omitted when no
457
- `openspec` source holds one), `approval` the text after `Approved:`, and
458
- `enforced` whether the missing approval blocks (a `proposal.md` exists to
459
- carry it).
561
+ and for a `proposed` subject whose newest adopted change has a brief (a
562
+ canon `brief.md` or an external `proposal.md`). `change` is the newest
563
+ adopted change (omitted when the subject adopts none); `brief` its canon
564
+ `.canon/changes/<change>/brief.md`, the one file the approval is read
565
+ from (omitted when the change has no brief to answer); `briefExists`
566
+ (always present) whether that file exists (`false` for an external
567
+ change whose canon brief is still to be created); `source` the external
568
+ plan document the change comes from (the canon brief's `Source:` line,
569
+ or, before that file exists, the external `proposal.md`; omitted for a
570
+ canon-written change); `approval` the text after `Approved:`; and
571
+ `enforced` whether the missing approval blocks (a brief exists to
572
+ answer). The 0.16 `proposal` field is gone.
460
573
 
461
574
  Each `next` entry's `actor` is `agent` (its `command` is a canon command
462
575
  line) or `human` (its `command` says what to get from the human).
463
576
 
464
577
  `policy.spec_coverage` has the shape `canon context --json` prints
465
578
  (`null` when absent, `{"invalid": …}` when unusable). `statusVersion`
466
- changes only when a field changes meaning or is removed. It is `2`
467
- since 0.16, when `next[].command` stopped always being a command line
468
- (`actor` tells which); `brief` was added alongside.
579
+ changes only when a field changes meaning or is removed. It became `2`
580
+ in 0.16, when `next[].command` stopped always being a command line
581
+ (`actor` tells which), and `brief` was added alongside. It is `3` since
582
+ 0.17, when `brief.proposal` was removed: the canon `brief.md` is
583
+ `brief.brief`, with `briefExists` and `source`.
469
584
 
470
585
  `subject new`, `adopt` and `status` write their records directly; the
471
586
  human-readable success line ends `— written directly; nothing to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@journeykit/canon",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "canon — harness knowledge substrate. JS launcher: platform detection + native binary resolution.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "dependencies": {},
32
32
  "optionalDependencies": {
33
- "@journeykit/canon-core-darwin-arm64": "0.16.0",
34
- "@journeykit/canon-core-linux-x64": "0.16.0",
35
- "@journeykit/canon-core-win32-x64": "0.16.0"
33
+ "@journeykit/canon-core-darwin-arm64": "0.18.0",
34
+ "@journeykit/canon-core-linux-x64": "0.18.0",
35
+ "@journeykit/canon-core-win32-x64": "0.18.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^20.0.0",