@journeykit/canon 0.15.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.
@@ -13,31 +13,67 @@ where each subject stands and lists the next commands.
13
13
  ## The working loop
14
14
 
15
15
  Follow these steps in order for every request that builds, changes or fixes
16
- a feature, a one-line request included. Two points stop for a human:
17
-
18
- - **The brief** (step 1): the request, your assumptions, your open questions.
19
- - **The policy**: `.canon/policy.yaml` (written by `canon init`) is what the
20
- gate grades the work against. A human approves it with
21
- `canon policy approve --by <who>` (and again after every change to it);
22
- never edit it to make the gate pass, and never approve it yourself.
23
-
24
- When no human can answer, write the question into the brief, proceed on the
25
- assumption you state there, and say so in your final report.
26
-
27
- 1. **Brief.** Write down the request verbatim, your assumptions, and the
28
- questions you could not ask. It becomes the change's
29
- `openspec/changes/<slug>/proposal.md`, which
30
- `canon change new <slug> --subject <id> --title "…"` scaffolds once the
31
- subject exists (step 2). A human approves the brief.
16
+ a feature, a one-line request included. A human is needed once per scope:
17
+
18
+ - **The approval** (step 1). A new subject, or a change to an approved
19
+ subject's scope, needs the human to approve its brief: the request, your
20
+ assumptions, your open questions. Write the brief, then stop and report
21
+ it: build nothing for that scope (no scenarios, code or evidence) before
22
+ the human answers. The approval is the one thing you never assume, even
23
+ when the human said they cannot answer until you report: the brief is
24
+ that report. In the same answer the human approves the policy,
25
+ `.canon/policy.yaml` (written by `canon init`), which is what the gate
26
+ grades the work against. When the human answers, record both at once:
27
+ an `Approved: <who> <when> — "<the human's words>"` line in the change's
28
+ `.canon/changes/<slug>/brief.md`, and
29
+ `canon policy approve --by <the human> --note "<their words>"`. Never
30
+ approve without a human answer, and never edit the policy to make the
31
+ gate pass.
32
+ - **Inside an approved scope, never stop for a human.** Fixes, follow-ups,
33
+ review rounds and re-reviews go straight on: add or extend the scenario,
34
+ implement, gather evidence, and get an independent review in another
35
+ session. Independent review happens on every change. A fix or follow-up
36
+ stays in the approved change: add it as tasks in that change's
37
+ `tasks.md`, never with `canon change new`, because a new change is a new
38
+ scope and needs the human's approval.
39
+
40
+ When no human can answer one of the brief's questions, write the question
41
+ into the brief, state the assumption you will build on, and say so in your
42
+ report. Proceeding on an assumption applies to the brief's questions and to
43
+ work inside an approved scope, never to the approval itself.
44
+
45
+ 1. **Brief.** For a new subject or a scope change, write down the request
46
+ verbatim, your assumptions, and the questions you could not ask. It
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,
54
+ asking the human once for the brief and the policy; go on to step 3
55
+ only after the human answers, and record the answer as above. Inside an
56
+ approved scope there is no new brief: go to step 3 under the existing
57
+ subject and change.
32
58
  2. **Subject.** `canon subject new <id> --domain <domain> --title "…"`, then
33
59
  `canon change new <slug> --subject <id> --title "…"`, and put the brief in
34
- the change's `proposal.md`.
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).
35
62
  3. **Scenarios.** `canon feature new <area>.<surface> --title "…"`, then one
36
63
  `canon scenario new <area>.<surface>.<nn> --subject <id> --case <case> --title "…"`
37
64
  per behavior, with failure paths (`--case failure`) as well as `happy`
38
65
  and `edge`. When the gate asks for a missing case (`require_cases`), add
39
- a new scenario; never retag an existing one to satisfy it. Check the
40
- corpus with `canon format specs` and index it with `canon inventory sync`.
66
+ a new scenario; never retag an existing one to satisfy it. There is no
67
+ sync or format step: `canon status`, `canon gate check`,
68
+ `canon evidence add`, `canon review add` and `canon subject status`
69
+ index added, edited or deleted `.feature` files themselves. Only when
70
+ one of them prints `NOT refreshed` (a format violation, such as a
71
+ hand-written header with no `# canon:` provenance comment, or a
72
+ duplicate id) run `canon inventory sync`: it names each problem; fix
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`.
41
77
  4. **Units.** Split the work into units (for example specs, domain code,
42
78
  assets, review). Give each unit its own actor id and session id, and pass
43
79
  them on everything the unit records (`--actor-id <unit> --session-id <session>`),
@@ -52,31 +88,56 @@ assumption you state there, and say so in your final report.
52
88
  `canon evidence add --project-id <root-id> --scenario-id <id> --kind test-run --role implementer --ref "<command>" --actor-id <unit> --session-id <session>`,
53
89
  with `--report junit:<path>` or `--artifact <path>` when files prove it.
54
90
  canon stores those bytes in `.canon/artifacts/sha256/` (commit it with the
55
- ledger) and `canon gate check` re-checks them. Then `canon gate promote`.
91
+ ledger) and `canon gate check` re-checks them. For a scenario about what
92
+ users see, bind the screenshot or recording that shows it
93
+ (`--artifact <path>`). Under `write: { promote: auto }` (what
94
+ `canon init` writes) the record is final when the command returns; only
95
+ in a repo with `write.promote: manual` (or no `write:` key) follow it
96
+ with `canon gate promote`.
56
97
  An external evaluator's audit is recorded with `canon evidence add --kind
57
98
  audit`; canon never generates one.
58
99
  Evidence for a plan task (`--task <id>`, before `canon gate task`) must
59
100
  bind the artifact or report that proves the task; a canon command
60
- (`canon status`, `canon gate check`) is never that proof.
101
+ (`canon status`, `canon gate check`) is never that proof. Both read the
102
+ change's `tasks.md` as it is now, so a task row you add needs no
103
+ `canon ingest plans` first.
61
104
  7. **Independent review.** A different session from the one that wrote the
62
105
  code and evidence reviews the change. The reviewer records every defect
63
106
  the review finds as a finding:
64
- `canon finding add --change-id <slug> --round <n> --seq <n> --severity <blocker|should-fix|note> --reviewer <me> --actor-id <me> --session-id <session> --summary "…"`,
65
- then `canon gate promote`; once fixed,
107
+ `canon finding add --change-id <slug> --round <n> --seq <n> --severity <blocker|should-fix|note> --reviewer <me> --actor-id <me> --session-id <session> --summary "…"`;
108
+ once fixed,
66
109
  `canon finding close --change-id <slug> --round <n> --seq <n> --disposition fixed --resolution-sha <sha>`
67
- (a short sha is fine; canon resolves it)
68
- and `canon gate promote`. Every bug fix adds or extends a scenario. The
110
+ (a short sha is fine; canon resolves it). With `write.promote: manual`,
111
+ follow each with `canon gate promote`. Every bug fix adds or extends a scenario. The
69
112
  reviewer signs off each scenario:
70
113
  `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>`
71
- (written directly; nothing to promote).
114
+ (written directly; nothing to promote). A reviewer of user-visible
115
+ behavior attaches what it looked at with `--artifact <path>`
116
+ (repeatable); canon stores and re-checks those bytes and never compares
117
+ images.
72
118
  8. **Transition.** `canon subject status <id> <state>` along
73
119
  `proposed → specced → building → verifying → shipped`. It refuses a move
74
120
  the policy does not yet allow (missing evidence, missing reviews, open
75
121
  blocker findings); fix the gap rather than overriding, and finish with
76
- `canon gate check` clean.
77
-
78
- Every write command says whether it staged the record (then
79
- `canon gate promote`) or wrote it directly.
122
+ `canon gate check` clean. A root whose `.feature` edits could not be
123
+ indexed fails the gate and refuses `shipped` (`uncovered-cell <root>`;
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
132
+ ship guard only prints an advisory). Move the subject to
133
+ `shipped` yourself once `canon subject status <id> shipped` passes its
134
+ guard, unless the brief says the human ships. `canon status` shows
135
+ whether each subject's brief is approved, by the same check, and names
136
+ the next step.
137
+
138
+ Every write command says whether the record is final (`written directly`,
139
+ or `staged and promoted in this run`) or staged; only a staged record
140
+ needs `canon gate promote`, and only `write.promote: manual` leaves one.
80
141
 
81
142
  ## Command reference
82
143
 
@@ -85,13 +146,18 @@ Load only the reference the current step needs.
85
146
  ### Setup and provider projection
86
147
 
87
148
  ```bash
88
- canon init # canon.yaml, starter policy, AGENTS.md block, plans home
89
- canon skills install # detects .claude/.agents|.codex/.omp/.pi; if none exists, installs Claude+Codex
90
- 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
91
153
  canon skills check # read-only drift check
92
- 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
93
155
  ```
94
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
+
95
161
  The canonical bundle is read-only. Claude receives
96
162
  `.claude/skills/canon/SKILL.md`; Codex receives
97
163
  `.agents/skills/canon/SKILL.md`; OMP receives `.omp/skills/canon/SKILL.md`; and
@@ -85,7 +85,7 @@ enum, like `lane`.
85
85
 
86
86
  Every scenario belongs to a Subject (the durable product unit, see
87
87
  `canon-subject`). Write the Subject first, then pin with `--subject`;
88
- `canon inventory sync` carries the tag onto the index record and
88
+ the scenario index carries the tag onto the index record and
89
89
  `spec_coverage.scope` filters on it. An unpinned scenario is invisible to
90
90
  a scoped gate.
91
91
 
@@ -97,10 +97,17 @@ canon scenario new checkout.cart.01 --title "Adding an item raises the count" \
97
97
  --subject checkout-core --lane behavior --case happy # once per behavior
98
98
  canon scenario new checkout.cart.04 --title "Adding an out-of-stock item is refused" \
99
99
  --subject checkout-core --lane behavior --case failure # and its failure path
100
- canon format specs # clean?
101
- canon inventory sync # index it
102
100
  ```
103
101
 
102
+ There is no format or sync step after these: the scaffold writes a
103
+ format-clean scenario, and `canon status`, `canon gate check`,
104
+ `canon evidence add`, `canon review add` and `canon subject status`
105
+ index added, edited and deleted `.feature` files before they read. When
106
+ one of them prints `NOT refreshed`, the corpus has a format violation
107
+ (for example a hand-written header with no `# canon:` line) or a
108
+ duplicate id; `canon inventory sync` (or `canon format <root>`) names it.
109
+ Fix it and the next command indexes the files (see `canon-inventory`).
110
+
104
111
  - `--actor <id>` (or `CANON_ACTOR` in the environment) puts YOUR id on the
105
112
  provenance line. Default is `canon-scaffold`.
106
113
  - **Never hand-type, move, or edit a `# canon:` line.** The scaffold
@@ -151,7 +158,7 @@ canon inventory sync # index it
151
158
  ## Worked example
152
159
 
153
160
  ```
154
- specs/features/kind=feature/area=checkout/
161
+ .canon/specs/kind=feature/area=checkout/
155
162
  cart.feature # surface: the cart screen
156
163
  payment.feature # surface: the payment step
157
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. |
@@ -24,7 +24,7 @@ Every violation carries one of these stable, grep-able strings:
24
24
  | `uncovered-cell` | Either a policy-required evidence cell (role × artifact) with no matching record, or — with `spec_coverage` enabled — a spec scenario that is unimplemented or mismatched, or a feature surface missing a `require_cases` case. The detail string distinguishes them. Coverage means "a test exists", not "a test passed": even a `Divergent` verdict satisfies the role-cell form. |
25
25
  | `unreviewed-promotion` | An artifact tagged `reviewed` has no matching ledger review record — or, with `spec_coverage.require_review` set, an in-scope scenario has no qualifying `Review`: none at all, only self-reviews, only stale reviews (the spec or evidence it pinned changed since), or only `indeterminate`/`unavailable` ones. See `canon-review`. |
26
26
  | `trust-below-required` | Achieved trust level is below `policy.yaml`'s `trust_required` for its class — RELEASE-scoped only (`canon gate check --release`); never fires on an ordinary run. |
27
- | `stale-evidence` | A passing record degraded to stale: its declared surface changed since its `evidence_sha`, or HEAD moved past `staleness.max_commits_behind`. Only degrades an already-green record. Also: a file bound to a latest evidence record whose bytes are gone — no intact blob in `.canon/artifacts/sha256/` and no working-tree copy with the recorded digest (see Bound evidence stays provable below). |
27
+ | `stale-evidence` | A passing record degraded to stale: its declared surface changed since its `evidence_sha`, or HEAD moved past `staleness.max_commits_behind`. Only degrades an already-green record. Also: a file bound to a latest evidence record whose bytes are gone — no intact blob in `.canon/artifacts/sha256/` and no working-tree copy with the recorded digest — or a file bound to an in-effect review with no intact blob (see Bound evidence stays provable below). |
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. |
@@ -57,6 +57,35 @@ approved: …` or `advisory: policy changed since approval: …` after its
57
57
  result, naming `canon policy approve --by <who>`; the exit code is
58
58
  unchanged (see `canon-policy`).
59
59
 
60
+ Before it reads the scenario index, the gate rebuilds any stale part of
61
+ it from the changed `.feature` files (the same code `canon inventory
62
+ sync` runs) and says so in one stderr line, silent when the index is
63
+ fresh:
64
+
65
+ ```
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
+ ```
68
+
69
+ A root that fails to sync (format violations, a duplicate id) is named
70
+ instead, ending ``NOT refreshed: root `x` (N format violation(s), 0 sync
71
+ error(s)) — `canon inventory sync` lists them``, and the gate fails
72
+ closed: it adds `uncovered-cell <root> — the scenario index for spec root
73
+ `<root>` is stale and could not be refreshed (…); `canon inventory sync`
74
+ lists the problems to fix` and exits 1, since the index it would grade
75
+ does not hold the edits. The `verifying → shipped` guard refuses with
76
+ the same violation, and `canon status` lists the sync as its only step.
77
+ Fix what the sync lists and run the gate again. See `canon-storage`
78
+ (The scenario index).
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
+
60
89
  ### The spec-derived worklist (`spec_coverage`, opt-in)
61
90
 
62
91
  Coverage groups the EVIDENCE corpus, so it can only ever enumerate
@@ -219,6 +248,61 @@ actually dropped, a multi-line reason, or a drop without
219
248
  `--scenario-id` (task-only evidence has no drop guard). `canon status`
220
249
  lists recorded drops (`dropped bindings (N):`; `drops[]` in `--json`).
221
250
 
251
+ ### Nothing is attested before the brief is approved (`--override-reason`)
252
+
253
+ The approval is never assumed: the agent writes the brief, stops and
254
+ reports it, and builds nothing for that scope until the human answers.
255
+ `canon evidence add` is the backstop for that stop (0.16 M7). It
256
+ refuses (exit `1`, nothing staged or stored) while a subject the record
257
+ attests for has an enforced, unapproved brief: the newest adopted
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
262
+ `canon-subject`). The subjects are the scenario's `@subject` owner for
263
+ `--scenario-id`, and every subject adopting the task's change for
264
+ `--task`:
265
+
266
+ ```
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
269
+ ```
270
+
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.
284
+ `canon review add` and `canon finding add` are never refused.
285
+ `--override-reason "<one line>"` attests anyway, prints the `waived …`
286
+ lines and `override recorded by `<actor>` for the N brief refusal(s)
287
+ above: <reason>`, and records on the evidence record:
288
+
289
+ ```yaml
290
+ brief_override:
291
+ reason: the human approved in chat; the line follows
292
+ waived:
293
+ - { class: unreviewed-promotion, subject: demo-subject }
294
+ actor: { agent_id: lead } # from --actor-id
295
+ ```
296
+
297
+ A blank or multi-line reason exits 2; an override when nothing refused is
298
+ not recorded. `canon status` lists each such record as a `warning:`, and
299
+ `canon gate check` prints them after the result without failing:
300
+
301
+ ```
302
+ brief overrides: 1 evidence record(s) attested before their brief was approved — not failing the gate:
303
+ evidence for world.demo.01 was attested by `lead` at 2026-10-12T07:00:00Z while the brief of demo-subject was not approved (override: the human approved in chat; the line follows)
304
+ ```
305
+
222
306
  ### Review-waiver advisories
223
307
 
224
308
  A subject moved into a review-scoped status with `canon subject status
@@ -235,17 +319,62 @@ review waivers: 1 advisory(ies) — not failing the gate:
235
319
 
236
320
  The waiver lasts only until the subject's next status write.
237
321
 
238
- ### Reviews that predate pinning
322
+ `canon subject status <id> shipped` also refuses where a brief exists and
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
326
+ the subject. With no brief to answer it prints an advisory and does not
327
+ refuse. Like the evidence refusal above, it is a backstop: the stop is at
328
+ the brief. `canon gate check` does not grade briefs, so a waived ship
329
+ brief prints no advisory here; `canon status` keeps showing the brief as
330
+ not approved.
331
+
332
+ ### Reviews that predate pinning (checked against their pin)
239
333
 
240
334
  With `require_review` on, the reviews in effect (latest per reviewer and
241
- scenario) that carry no pins were written before 0.15. They keep
242
- counting, and one advisory line after the violations counts them; the
243
- exit code is unchanged:
335
+ scenario) that carry no pins were written before 0.15. Each still names
336
+ `pin`, the commit it reviewed, and the gate checks it there (0.16):
337
+
338
+ - **current** — the scenario's digest at that commit (read with `git`,
339
+ normalized exactly as `canon inventory sync` computes
340
+ `scenario_digest`) equals its current digest, and no declared evidence
341
+ record of the scenario is newer than the review. It counts. When the
342
+ scenario lived in another `.feature` file at that commit, the spec
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.
347
+ - **stale** — the scenario changed since the commit, or it has newer
348
+ evidence. It no longer counts, and an in-scope scenario left with no
349
+ other qualifying review is `unreviewed-promotion`:
350
+
351
+ ```
352
+ unreviewed-promotion gate.check.15 — stale review by `canon-reviewer-g1` (written before review pinning; checked against commit b556ab7529a332aa0b28d0a0f98bdd62fcbb9a63): it has declared evidence newer than the review; a review counts only while what it saw is current — review it again (`canon review add`)
353
+ ```
354
+
355
+ A spec change reads `its spec changed since <pin> (the scenario's
356
+ digest at that commit is not its current one)`.
357
+ - **undecidable** — the pin is not a commit in this clone (a shallow
358
+ clone, rewritten history), or no feature file of the scenario's root
359
+ holds its id at that commit. It keeps counting, as in 0.15: a missing
360
+ commit never becomes a violation.
361
+
362
+ One advisory line after the violations counts the three. A stale one
363
+ whose scenario already has another qualifying review (current, signed,
364
+ distinct) is counted as superseded, since nothing is owed for it; only
365
+ the scenarios a stale one leaves without a qualifying review are named
366
+ for re-review. The exit code is unchanged by the line itself:
244
367
 
245
368
  ```
246
- advisory: 340 review(s) predate review pinning (0.15) and still count without a check that their scenario's spec and evidence are unchanged; a new `canon review add` pins what it saw
369
+ advisory: 338 review(s) predate review pinning (0.15) and were checked against the commit each one pinned: 318 current, 12 stale, 8 undecidable; 7 stale one(s) superseded by a current review of the same scenario, so nothing is owed for them; a stale one with no current review in its place no longer counts — review its scenario again (`canon review add`): platformer.movement.01, platformer.movement.02, …; an undecidable one still counts unchecked, because its pinned commit or the scenario at it is not in this clone — check out full history (`fetch-depth: 0` in CI) to check it
247
370
  ```
248
371
 
372
+ A review of a scenario no feature file declares any more is not counted.
373
+ In CI, check out full history (`actions/checkout` with `fetch-depth: 0`)
374
+ for the job that runs `canon gate check`, or every legacy review is
375
+ undecidable. Each pinned commit is read once per run through one `git
376
+ cat-file --batch` process, and never fetched over the network.
377
+
249
378
  ## Experimental: evidence binding (`experimental.evidence_binding`, off by default)
250
379
 
251
380
  An evidence record is an attestation: canon never runs `--ref`, and it
@@ -338,6 +467,15 @@ subject's status, and a repo with no `spec_coverage` would otherwise never
338
467
  have its bindings checked. A superseded record is not checked —
339
468
  re-attesting is the fix.
340
469
 
470
+ Files bound to a review (`canon review add --artifact`, see
471
+ `canon-review`) are checked the same way for every in-effect review (the
472
+ latest per `(project_id, scenario_id, reviewer)`), with one difference:
473
+ they were stored when the review was written, so only an intact blob is
474
+ clean. A missing or altered blob is `stale-evidence` on the review's
475
+ scenario, and the detail names the review: ``review by `<reviewer>`
476
+ (actor `<id>`, <time>)``. The working-tree file is reported but never
477
+ vouches for it.
478
+
341
479
  `canon evidence vault [--max-artifact-mib <N>]` stores the bytes for
342
480
  existing records: every attachment whose working-tree file still matches
343
481
  its digest is copied in, and everything else is listed as not stored,
@@ -377,12 +515,19 @@ empty clean map.
377
515
  ## `canon gate task <task_id> [--repo <dir>]`
378
516
 
379
517
  The evidence-gated task checkbox flip. Resolves `<task_id>`
380
- (`<change_id>#<n>`) through `canon.yaml`'s `plans:` sources — the first
381
- configured dialect whose `PlanWriteBack` locates the task's document
382
- wins, so the flip is plan-dialect agnostic (`openspec` change dirs,
383
- `superpowers` plan docs). A repo with no `plans:` section falls back to
384
- the compat default `[{ dialect: openspec, root: <repo> }]`; no source
385
- 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).
386
531
  It requires
387
532
  a matching non-`Divergent` evidence record, and flips `- [ ]` → `- [x]`
388
533
  with an appended evidence note ONLY on a clean check. The note
@@ -414,6 +559,13 @@ canon gate task my-change#5.2 --repo .
414
559
  This is canon's own authority for the checkbox grammar — always flip
415
560
  through `canon gate task`, never by hand.
416
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
+
417
569
  ## `canon gate promote [--repo <dir>] [--dry-run]`
418
570
 
419
571
  Staging → committed: every well-formed record under
@@ -431,13 +583,26 @@ canon gate promote --repo . --dry-run # preview only
431
583
  ```
432
584
 
433
585
  Which writes need this step: `evidence add`, `finding add` and `finding
434
- close` stage, and their success line ends ``— run `canon gate promote`
435
- to commit it``. `divergence stage` stages for `canon divergence promote`
436
- and names it. Every other write — `review add`, `divergence
586
+ close` stage, and under `write: { promote: manual }` (or an absent
587
+ `write:` key) their success line ends ``— run `canon gate promote` to
588
+ commit it``. Under `write: { promote: auto }` (what `canon init` writes)
589
+ or with `--promote`, they promote the one record they just staged —
590
+ same validation and `run_seq` assignment as this command — and print
591
+ one success line naming the committed record (`wrote kind=…/….json`), not
592
+ the staged file, ending `— staged and promoted in this run; nothing to
593
+ promote`, so no separate
594
+ `canon gate promote` is needed for it; `--no-promote` keeps one write
595
+ staged. A record another run staged is not touched: only `gate promote`
596
+ commits everything staged. It stays safe to run anyway: with nothing
597
+ staged it prints
598
+ `nothing staged` and exits `0`. `divergence stage` stages for `canon
599
+ divergence promote` and names it, under the same `write:` rule. Every
600
+ other write — `review add`, `divergence
437
601
  resolve|defer`, `subject new|adopt|status`, `change new`, `scenario new`, `feature new`,
438
602
  `gate task` (it edits the plan document and re-ingests it)
439
603
  — is final when it returns, and its success line ends `— written
440
- directly; nothing to promote`.
604
+ directly; nothing to promote`. Full promote-on-write contract:
605
+ `canon-storage` (Staged writes and promotion).
441
606
 
442
607
  ## `canon gate install-hooks [--repo] [--event] [--matcher] [--command] [--timeout]`
443
608
 
@@ -1,16 +1,74 @@
1
1
  # canon-inventory
2
2
 
3
- `canon inventory sync` is the FIRST stage of canon's unified spec loop:
4
- it turns a hand-authored, `canon format`-validated `.feature` corpus into
5
- the scenario ledger-INDEX records every downstream stage joins against.
3
+ `canon inventory sync` builds canon's scenario ledger-INDEX: it turns a
4
+ hand-authored, `canon format`-validated `.feature` corpus into the scenario
5
+ records every downstream stage joins against.
6
6
 
7
- ## The unified loop
7
+ ## You rarely run it yourself
8
+
9
+ Since 0.16 the commands that read the index refresh it first:
10
+ `canon status`, `canon gate check`, `canon evidence add`,
11
+ `canon review add` and `canon subject status` index added, edited and
12
+ deleted `.feature` files themselves, with the same code this command runs,
13
+ scoped to the changed files, and say so in one stderr line:
14
+
15
+ ```
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
+ ```
18
+
19
+ So the working loop has no sync step and no `canon format` step: write the
20
+ scenario (`canon scenario new` writes it format-clean) and run the next
21
+ command. Run `canon inventory sync` when:
22
+
23
+ - a reader prints `NOT refreshed` — a root has a format violation (a
24
+ hand-written `Feature:`/`Scenario:` header with no `# canon:` provenance
25
+ comment, an emptied file, …) or a duplicate id, so its files were not
26
+ indexed. `canon gate check` then fails (`uncovered-cell <root>`),
27
+ `shipped` is refused and `canon status` lists only the sync. `canon
28
+ inventory sync` names each problem (`canon format <root>` names the format ones);
29
+ fix them and the next command indexes the files;
30
+ - `canon status` says the ledger holds no scenario records while the
31
+ `.feature` files hold some (the same refusal, on an index never built);
32
+ - you want the whole index rebuilt from every root at once.
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
+
65
+ ## Where the index sits in the loop
8
66
 
9
67
  ```
10
- author .feature corpus → canon format --check (validate)
68
+ author .feature corpus (canon scenario new)
11
69
  │
12
70
  ▼
13
- canon inventory sync # THIS skill — materializes scenario index records
71
+ scenario index # refreshed by every reader; `canon inventory sync` rebuilds it
14
72
  │
15
73
  ▼
16
74
  canon review add # attests a review — see canon-inventory downstream / canon-gate
@@ -23,8 +81,8 @@ canon ingest artifacts # with artifacts.native_records: true in canon.y
23
81
  # (see canon-artifact-ingest)
24
82
  ```
25
83
 
26
- Every stage after `sync` joins on the SAME `(project_id, scenario_id)`
27
- pair `sync` materializes — one identity scheme, no second one.
84
+ Every stage after the index joins on the SAME `(project_id, scenario_id)`
85
+ pair it materializes — one identity scheme, no second one.
28
86
 
29
87
  ## `canon inventory sync [--repo <dir>] [--spec-root <dir>]`
30
88
 
@@ -39,7 +97,7 @@ Per configured root, in order:
39
97
  1. **Validate** — runs `canon format --check` over the root. ANY violation
40
98
  (missing provenance, layout-grammar, …) ABORTS THE WHOLE ROOT: zero
41
99
  records written for it, the violation(s) reported. Never a partial sync.
42
- 2. **Scan** — walks `<root>/features/**/*.feature`, pairing each
100
+ 2. **Scan** — walks `<root>/kind=feature/area=<area>/<surface>.feature`, pairing each
43
101
  `@<area>.<surface>.<nn>` tag with its header label as `title` and a
44
102
  `source_digest` (sha256 over the raw `.feature` bytes). `@subject:<id>`
45
103
  → `subject_id`, `@lane:<v>` → `lane`, `@case:<v>` → `case` (first tag
@@ -73,7 +131,10 @@ specs:
73
131
  root: apps/b/specs
74
132
  ```
75
133
 
76
- - **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.
77
138
  - **Present `specs:` with an empty/missing `roots[]`** → fails LOUD; a
78
139
  present-but-incomplete config never silently resolves to zero roots.
79
140
  - **A malformed entry** (missing `id`, `roots` not a list, `id` not a
@@ -86,6 +147,10 @@ specs:
86
147
 
87
148
  - **A `canon format --check` validation violation** in the root's corpus →
88
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.
89
154
  - **A duplicate `scenario_id`** within one root → that root aborts, 0
90
155
  writes, reported as a sync error (a distinct lane from fmt violations).
91
156