@journeykit/canon 0.16.0 → 0.17.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.
@@ -44,17 +44,21 @@ work inside an approved scope, never to the approval itself.
44
44
 
45
45
  1. **Brief.** For a new subject or a scope change, write down the request
46
46
  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,
47
+ becomes the change's `.canon/changes/<slug>/brief.md`, which
48
+ `canon change new <slug> --subject <id> --title "…"` writes once the
49
+ subject exists (step 2). A change that comes from an external plan the
50
+ human prepared (an openspec `proposal.md` read through
51
+ `plans.sources`) gets its brief there too: a `brief.md` whose
52
+ `Source: <path>` line points at the plan; canon never edits the plan.
53
+ Once the brief is there, stop and report it,
50
54
  asking the human once for the brief and the policy; go on to step 3
51
55
  only after the human answers, and record the answer as above. Inside an
52
56
  approved scope there is no new brief: go to step 3 under the existing
53
57
  subject and change.
54
58
  2. **Subject.** `canon subject new <id> --domain <domain> --title "…"`, then
55
59
  `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).
60
+ the `.canon/changes/<slug>/brief.md` it writes. For a new subject or a
61
+ scope change, this is where you stop until the human answers (step 1).
58
62
  3. **Scenarios.** `canon feature new <area>.<surface> --title "…"`, then one
59
63
  `canon scenario new <area>.<surface>.<nn> --subject <id> --case <case> --title "…"`
60
64
  per behavior, with failure paths (`--case failure`) as well as `happy`
@@ -66,7 +70,10 @@ work inside an approved scope, never to the approval itself.
66
70
  one of them prints `NOT refreshed` (a format violation, such as a
67
71
  hand-written header with no `# canon:` provenance comment, or a
68
72
  duplicate id) run `canon inventory sync`: it names each problem; fix
69
- them, and the next command indexes the files.
73
+ them, and the next command indexes the files. Specs live at
74
+ `<root>/kind=feature/area=<area>/<surface>.feature` under a
75
+ `specs.roots[]` root (default `.canon/specs`); a root still on
76
+ `features/kind=feature/` is refused until you run `canon migrate`.
70
77
  4. **Units.** Split the work into units (for example specs, domain code,
71
78
  assets, review). Give each unit its own actor id and session id, and pass
72
79
  them on everything the unit records (`--actor-id <unit> --session-id <session>`),
@@ -114,12 +121,14 @@ work inside an approved scope, never to the approval itself.
114
121
  blocker findings); fix the gap rather than overriding, and finish with
115
122
  `canon gate check` clean. A root whose `.feature` edits could not be
116
123
  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
124
+ see step 3). `shipped` also refuses while the change's
125
+ `.canon/changes/<slug>/brief.md` has no `Approved:` line (for a change
126
+ from an external plan with no canon brief yet, it names the `brief.md`
127
+ to create with its `Source:` line), and `canon evidence add` refuses the
128
+ subject's scenarios and tasks the same way. Both are backstops: the stop
129
+ itself is at step 1, so you never reach them for a brief the human has
130
+ not answered. You record the line only from the human's answer (with no
131
+ brief to answer there is nothing to refuse, and the
123
132
  ship guard only prints an advisory). Move the subject to
124
133
  `shipped` yourself once `canon subject status <id> shipped` passes its
125
134
  guard, unless the brief says the human ships. `canon status` shows
@@ -137,13 +146,18 @@ Load only the reference the current step needs.
137
146
  ### Setup and provider projection
138
147
 
139
148
  ```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
149
+ canon init # canon.yaml, starter policy (--policy light|standard|strict, default strict; see canon-policy), AGENTS.md block, commented plans examples
150
+ 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
151
+ canon skills install # follows canon.yaml skills.providers (else the install lock's, else the detected ones; none detected installs nothing)
152
+ canon skills install --providers=claude,codex,omp,pi # any subset or none; rewrites skills.providers
143
153
  canon skills check # read-only drift check
144
- canon skills doctor # diagnostics; never deletes user files
154
+ canon skills doctor # diagnostics, incl. a stray canon projection for an unselected provider; never deletes user files
145
155
  ```
146
156
 
157
+ The harness list lives in `canon.yaml` as `skills: { providers: [...] }`.
158
+ `.agents` alone never selects Codex; `.codex` does. Change the list with
159
+ `canon skills install --providers=<list>`, never by assuming a default.
160
+
147
161
  The canonical bundle is read-only. Claude receives
148
162
  `.claude/skills/canon/SKILL.md`; Codex receives
149
163
  `.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. |
@@ -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`:
@@ -489,12 +515,19 @@ empty clean map.
489
515
  ## `canon gate task <task_id> [--repo <dir>]`
490
516
 
491
517
  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.
518
+ (`<change_id>#<n>`) through `.canon/changes/` (canon's own changes, read
519
+ first whenever the directory exists, with no `plans:` entry) and then
520
+ `canon.yaml`'s `plans.sources` — the first dialect whose `PlanWriteBack`
521
+ locates the task's document wins, so the flip is plan-dialect agnostic
522
+ (canon's own `tasks.md`, external `openspec` change dirs, `superpowers`
523
+ plan docs). A repo with no `plans:` section falls back to the compat
524
+ default `[{ dialect: openspec, root: <repo> }]`; no source locating the
525
+ task at all is a loud failure naming every source consulted. A repo with
526
+ no `.canon/changes/` and `sources: []` is refused: "this repo configures
527
+ no plan sources (no `.canon/changes` and no `plans.sources` entries in
528
+ canon.yaml) … write the change with `canon change new`, or list your
529
+ external plans under `plans.sources`" (`canon dispatch --task` says the
530
+ same).
498
531
  It requires
499
532
  a matching non-`Divergent` evidence record, and flips `- [ ]` → `- [x]`
500
533
  with an appended evidence note ONLY on a clean check. The note
@@ -526,6 +559,13 @@ canon gate task my-change#5.2 --repo .
526
559
  This is canon's own authority for the checkbox grammar — always flip
527
560
  through `canon gate task`, never by hand.
528
561
 
562
+ Canon only reads external plans, with this one exception: for a task
563
+ from an external plan source, `canon gate task` flips the checkbox in
564
+ that plan's own `tasks.md` (or plan document). It is the one place canon
565
+ writes into an external plan; their briefs and approvals still live in
566
+ `.canon/changes/<change>/brief.md`. (Open for the maintainer: record
567
+ external flips only in the ledger instead.)
568
+
529
569
  ## `canon gate promote [--repo <dir>] [--dry-run]`
530
570
 
531
571
  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** —
@@ -192,6 +192,29 @@ write (promote: manual)`, or `[FAIL] write: …`. `canon gate promote` still
192
192
  works under `auto` and is idempotent: with nothing staged it prints
193
193
  `nothing staged` and exits `0`.
194
194
 
195
+ ## Harnesses: the `skills:` section
196
+
197
+ `canon.yaml`'s `skills:` section names the harnesses the canon skill is
198
+ installed for (0.17):
199
+
200
+ ```yaml
201
+ skills:
202
+ providers: [claude, codex] # any of claude, codex, omp, pi; [] for none
203
+ ```
204
+
205
+ `canon init` writes it: on a terminal it asks, with the harnesses whose
206
+ `.claude`, `.codex`, `.omp` or `.pi` directory exists checked;
207
+ `--providers a,b` (or `none`) answers without asking; with neither it takes
208
+ the detected ones, and with none detected it writes only a commented example
209
+ and installs nothing. `.agents` alone never selects Codex. `canon skills
210
+ install|check|doctor` follow the section; `canon skills install
211
+ --providers=<list>` rewrites it in place. Without the section they use the
212
+ install lock's providers, then the detected ones; nothing undetected is ever
213
+ added. A present section that is not this shape, or names another harness,
214
+ fails those commands (exit `1`), and `canon init --check-config` reports
215
+ `[PASS] skills (providers: …)`, `[not configured] skills (…)`, or
216
+ `[FAIL] skills: …`.
217
+
195
218
  ## The scenario index
196
219
 
197
220
  `canon inventory sync` materializes one `Scenario` record per
@@ -207,7 +230,7 @@ that no `.feature` file of it declares any more. The reader then runs
207
230
  covers the whole root) and prints one stderr line:
208
231
 
209
232
  ```
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
233
+ 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
234
  ```
212
235
 
213
236
  A deleted scenario is not erased: `canon inventory sync` — and a reader's
@@ -148,27 +148,39 @@ empty set; so is one whose `scenario` records route away from the
148
148
  gate's rung. Each refusal prints by failure class (`uncovered-cell`),
149
149
  exits 1, and the status stays `verifying`. `retired` is not gated.
150
150
 
151
- **The brief (0.16):** `verifying → shipped` also refuses where a brief
151
+ **The brief (0.16, 0.17):** `verifying → shipped` also refuses where a brief
152
152
  exists and is not approved, read exactly as `canon status` reads it (one
153
153
  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:
154
+ newest adopted change has a canon `.canon/changes/<change>/brief.md`
155
+ with no `Approved:` line outside HTML comments and code fences, or has no
156
+ canon `brief.md` yet but comes from an external openspec plan with a
157
+ `proposal.md` under the plan sources. It runs with or without a policy.
158
+ The refusal is an `unreviewed-promotion` naming the subject, and says
159
+ what to do:
158
160
 
159
161
  ```
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
162
+ 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
163
  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
164
  ```
163
165
 
166
+ For an external change with no canon brief yet, the reason reads
167
+ ``.canon/changes/<c>/brief.md does not exist yet: change `<c>` comes from
168
+ the external plan <proposal>, which canon reads and never edits, so its
169
+ approval lives in that canon file`` and the agent will ``create
170
+ .canon/changes/<c>/brief.md holding a `Source: <proposal>` line and
171
+ record their words there as `Approved: <who> <when> — "<their words>"` ``.
172
+
164
173
  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.
174
+ answer (no adopted change, or a newest change with neither a canon
175
+ `brief.md` nor an external `proposal.md`), the move is not refused: it
176
+ prints ``canon subject status: advisory: <id>'s brief is not approved —
177
+ its newest adopted change `<c>` has no brief (no
178
+ .canon/changes/<c>/brief.md, no openspec proposal.md), so there is no
179
+ brief to answer and shipping is not refused`` (or `— <id> adopts no
180
+ change, so …`), and status shows the brief as not approved, as an
181
+ advisory. `subjects[].brief.enforced` in `canon status --json` says
182
+ which case applies. A brief that says the human ships changes who runs
183
+ the transition, not the check.
172
184
 
173
185
  This refusal is a backstop, and so is the evidence one: shipping is not
174
186
  the first thing an unapproved brief stops. The agent writes the brief,
@@ -265,21 +277,33 @@ it.
265
277
 
266
278
  ### Starting a new change: `canon change new`
267
279
 
268
- For a change that does not exist yet, one command scaffolds it and
269
- records it adopted:
280
+ For a change that does not exist yet, one command writes it and records
281
+ it adopted:
270
282
 
271
283
  ```bash
272
284
  canon change new add-login --subject auth --title "Add login"
273
285
  ```
274
286
 
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
287
+ ```
288
+ canon change new: wrote .canon/changes/add-login/brief.md
289
+ canon change new: wrote .canon/changes/add-login/tasks.md
290
+ canon change new: recorded change `add-login` adopted into subject `auth` — written directly; nothing to promote
291
+ ```
292
+
293
+ - **Writes** canon's own change dir, `.canon/changes/<slug>/`:
294
+ `brief.md` (`# <title>`, `## Why` holding the title, which imports as
295
+ the change summary, `## What Changes` with a comment saying what goes
296
+ there, and a comment naming where the human's `Approved:` line goes,
297
+ so the file starts unapproved) and `tasks.md` (no rows yet, so the
298
+ change imports as `proposed`; the openspec checkbox row grammar).
299
+ `.canon/changes/` is canon's native plan source: every plan reader
300
+ reads it with no `plans:` entry, so `canon.yaml` needs no edit and no
301
+ external plans source is needed (see `canon-plan-import`).
302
+ - **Reads** the files with the native `canon` plan adapter `canon ingest
281
303
  plans` uses, then **records** the change already adopted, through the
282
- same write as `canon subject adopt`.
304
+ same write as `canon subject adopt`. Its records carry the actor
305
+ `canon-plan-import-canon` (canon-written); an external openspec
306
+ change's carry `canon-plan-import-openspec`.
283
307
  - **What a failure leaves behind.** The files are staged under `.canon/`
284
308
  and the change dir is moved into place only once they parse. A failure
285
309
  before any record is written (staging, publishing the dir, or the
@@ -291,14 +315,23 @@ canon change new add-login --subject auth --title "Add login"
291
315
  plans && canon subject adopt <slug> --subject <id>`, which completes
292
316
  the link and is safe to rerun.
293
317
  - **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.
318
+ exist, or when the slug already has a `.canon/changes/<slug>` dir
319
+ (active or under `.canon/changes/archive/`) or a `change` record.
296
320
 
297
321
  Add task rows to `tasks.md` as `- [ ] <n> <title>`, and flip each with
298
322
  `canon gate task <slug>#<n>` once evidence exists. `canon evidence add
299
323
  --task` and the flip read `tasks.md` as it is, so a new row needs no
300
324
  `canon ingest plans` run.
301
325
 
326
+ A change from an external plan (an openspec change listed under
327
+ `plans.sources`, imported by `canon ingest plans` and adopted with
328
+ `canon subject adopt`) gets its brief as a canon file too:
329
+ `.canon/changes/<change>/brief.md` holding a `Source: <path>` line that
330
+ points at the external document and, once the human answers, the
331
+ `Approved:` line. Canon never edits the external document. Such a
332
+ pointer `brief.md` is not a change of its own: the native reader skips
333
+ it, so the external source keeps the change, its summary and its tasks.
334
+
302
335
  ## Where are we: `canon status`
303
336
 
304
337
  ```bash
@@ -332,23 +365,33 @@ uses, so status and the gate cannot disagree about a gap:
332
365
  `require_review` on, a `warning:` line counts the in-effect reviews that
333
366
  predate review pinning (0.15), checked against the commit each pinned:
334
367
  current, stale (named) and undecidable.
335
- - **Brief** (0.16) — for each `building`, `verifying` or `shipped`
368
+ - **Brief** (0.16, 0.17) — for each `building`, `verifying` or `shipped`
336
369
  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
370
+ brief (a canon `brief.md`, or an external `proposal.md`), a `brief:`
371
+ line under it says whether the brief is approved:
372
+ `brief: approved in <brief> — <the recorded words>`, or
373
+ `brief: not approved (…)`. Approval is an `Approved:` line in
374
+ `.canon/changes/<change>/brief.md` for the subject's newest adopted
375
+ change (the last one adopted; a live dir before an archived one), and
376
+ only there: a line that starts with `Approved:` (indent allowed), says
342
377
  something after it, and sits outside HTML comments (`<!-- … -->`,
343
378
  multi-line included) and fenced code blocks (```` ``` ```` or `~~~`),
344
379
  written by the agent once the human answers, as
345
380
  `Approved: <who> <when> — "<the human's words>"`. Fixes and follow-ups
346
381
  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).
382
+ new scope: an older change's approval does not cover it. A change from
383
+ an external plan has its brief in the same canon file, with a
384
+ `Source: <path>` line pointing at the external document; an `Approved:`
385
+ line inside an external `proposal.md` is not read. Before that canon
386
+ file exists, an external openspec `proposal.md` under the plan sources
387
+ (the first source holding the change; with no `plans:` section, the
388
+ `openspec` source rooted at the repo that `canon gate task` also falls
389
+ back to) still makes the brief one to answer, and the line reads
390
+ ``brief: not approved (.canon/changes/<c>/brief.md does not exist yet:
391
+ change `<c>` comes from the external plan <proposal>, which canon reads
392
+ and never edits, so its approval lives in that canon file)``. With a
393
+ canon brief and no line it reads
394
+ ``brief: not approved (<brief> has no `Approved:` line)``.
352
395
  There is no record kind for it; the file is the record.
353
396
  - **Unowned** — scenarios with no `@subject:` tag.
354
397
  - **Audits** — how many scenarios are sampled for
@@ -372,7 +415,7 @@ uses, so status and the gate cannot disagree about a gap:
372
415
  itself first, so this means the refresh refused on a format violation
373
416
  or a duplicate id, which the sync names). Then per subject, verifying
374
417
  first, then building, specced, proposed, shipped: an unapproved brief
375
- (building and later, and proposed once its change has a `proposal.md`)
418
+ (building and later, and proposed once its change has a brief)
376
419
  → the human step below, which is then the subject's
377
420
  only step; no scenario → `canon scenario new … --subject <id>` (and, when no
378
421
  `.feature` file holds a scenario yet, `canon feature new` first, unless
@@ -389,12 +432,20 @@ uses, so status and the gate cannot disagree about a gap:
389
432
  carries `--project`: the owning root for a missing case, `<root-id>`
390
433
  (the reason lists the roots) for a subject that owns no scenario yet.
391
434
  - **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>"`.
435
+ a subject whose brief is enforced (its newest change's canon
436
+ `brief.md` has no `Approved:` line, or it has no canon `brief.md` yet
437
+ and comes from an external openspec `proposal.md`) gets
438
+ `human: stop and report the brief in <brief>; build and attest nothing for <id> until the human answers (one answer approves brief and policy)`,
439
+ where `<brief>` is the canon `brief.md`, or the external `proposal.md`
440
+ when no canon brief exists yet. Its reason reads ``<id>: <why
441
+ unanswered> (an older change's approval does not cover a new scope),
442
+ and the approval is never assumed: `canon evidence add` refuses <id>'s
443
+ scenarios and tasks until it is recorded; once the human answers,
444
+ <record> and run `canon policy approve --by <the human> --note "<their
445
+ words>"` ``, where `<record>` is ``record their words as `Approved: <who>
446
+ <when> — "<their words>"` in <brief>``, or, for an external change,
447
+ ``create <brief> holding a `Source: <proposal>` line and record their
448
+ words there as `Approved: <who> <when> — "<their words>"` ``.
398
449
  It is the subject's only step, whatever its status: nothing suggests
399
450
  `canon feature new`, `canon scenario new`, `canon evidence add`, a
400
451
  review or a move-on for it before the human answers, and
@@ -402,9 +453,9 @@ uses, so status and the gate cannot disagree about a gap:
402
453
  approval is never assumed. A subject with no brief to answer gets no human step;
403
454
  its `brief:` line says
404
455
  `not approved (no adopted change; advisory, nothing to approve yet)` or
405
- ``not approved (change `<id>` has no proposal.md; advisory, nothing to
456
+ ``not approved (change `<id>` has no brief; advisory, nothing to
406
457
  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
458
+ brief, and a `specced` subject, show no `brief:` line at all). Once nothing blocks, a
408
459
  ship-ready verifying subject gets the agent's own
409
460
  `canon subject status <id> shipped`; status never asks a human to ship.
410
461
 
@@ -424,21 +475,21 @@ Status fails closed rather than suggest progress the gate would refuse:
424
475
 
425
476
  ```json
426
477
  {
427
- "statusVersion": 2,
428
- "canonVersion": "0.16.0",
478
+ "statusVersion": 3,
479
+ "canonVersion": "0.17.0",
429
480
  "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
481
  "warnings": [],
431
482
  "subjects": [
432
483
  { "id": "cart", "title": "Cart", "status": "building", "scenarios": 2, "excluded": 0, "evidenced": 2, "divergent": 0,
433
484
  "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\"" } },
485
+ "brief": { "approved": true, "enforced": false, "change": "c-cart", "brief": ".canon/changes/c-cart/brief.md", "briefExists": true, "approval": "Journey 2026-10-11 — \"approved\"" } },
435
486
  { "id": "promo", "title": "Promo", "status": "verifying", "scenarios": 1, "excluded": 0, "evidenced": 1, "divergent": 0,
436
487
  "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" } }
488
+ "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
489
  ],
439
490
  "unowned": ["cart.promo.01"],
440
491
  "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" },
492
+ { "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
493
  { "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
494
  { "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
495
  { "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 +502,29 @@ Each `missingCases` entry is `{ "projectId", "surface", "case" }`: the
451
502
  spec root, the `<area>.<surface>`, and the required case it lacks.
452
503
 
453
504
  `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).
505
+ and for a `proposed` subject whose newest adopted change has a brief (a
506
+ canon `brief.md` or an external `proposal.md`). `change` is the newest
507
+ adopted change (omitted when the subject adopts none); `brief` its canon
508
+ `.canon/changes/<change>/brief.md`, the one file the approval is read
509
+ from (omitted when the change has no brief to answer); `briefExists`
510
+ (always present) whether that file exists (`false` for an external
511
+ change whose canon brief is still to be created); `source` the external
512
+ plan document the change comes from (the canon brief's `Source:` line,
513
+ or, before that file exists, the external `proposal.md`; omitted for a
514
+ canon-written change); `approval` the text after `Approved:`; and
515
+ `enforced` whether the missing approval blocks (a brief exists to
516
+ answer). The 0.16 `proposal` field is gone.
460
517
 
461
518
  Each `next` entry's `actor` is `agent` (its `command` is a canon command
462
519
  line) or `human` (its `command` says what to get from the human).
463
520
 
464
521
  `policy.spec_coverage` has the shape `canon context --json` prints
465
522
  (`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.
523
+ changes only when a field changes meaning or is removed. It became `2`
524
+ in 0.16, when `next[].command` stopped always being a command line
525
+ (`actor` tells which), and `brief` was added alongside. It is `3` since
526
+ 0.17, when `brief.proposal` was removed: the canon `brief.md` is
527
+ `brief.brief`, with `briefExists` and `source`.
469
528
 
470
529
  `subject new`, `adopt` and `status` write their records directly; the
471
530
  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.17.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.17.0",
34
+ "@journeykit/canon-core-linux-x64": "0.17.0",
35
+ "@journeykit/canon-core-win32-x64": "0.17.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^20.0.0",