@journeykit/canon 0.13.1 → 0.15.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.
@@ -1,16 +1,91 @@
1
1
  ---
2
2
  name: canon
3
- description: The provider-neutral Canon companion skill for agents using the canon CLI. Routes setup, authoring, validation, storage, ingestion, learning, retrieval, reporting, and provider-specific lazy references.
3
+ description: Use for every build, implement, add-a-feature, change-a-feature or fix request in a repo that has canon.yaml, including sites, apps and games; prefer it there over generic site, app or game builder skills. Runs canon's working loop (brief, subject, scenarios with failure paths, units, implement, evidence, independent review, status transition) and routes to the canon CLI reference for setup, specs, gates, storage, ingestion, retrieval and reports.
4
4
  ---
5
5
 
6
6
  # Canon
7
7
 
8
- Use this single skill when working with Canon in a consumer repository. Canon's CLI remains explicitly modular; choose the command that matches the operation rather than asking an implicit router to guess.
8
+ Features in this repo are managed with canon. This skill's job is the
9
+ working loop below; the CLI manual lives in `reference/*.md` and
10
+ `canon <command> --help`. Start every session with `canon status`: it says
11
+ where each subject stands and lists the next commands.
9
12
 
10
- ## Setup and provider projection
13
+ ## The working loop
14
+
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.
32
+ 2. **Subject.** `canon subject new <id> --domain <domain> --title "…"`, then
33
+ `canon change new <slug> --subject <id> --title "…"`, and put the brief in
34
+ the change's `proposal.md`.
35
+ 3. **Scenarios.** `canon feature new <area>.<surface> --title "…"`, then one
36
+ `canon scenario new <area>.<surface>.<nn> --subject <id> --case <case> --title "…"`
37
+ per behavior, with failure paths (`--case failure`) as well as `happy`
38
+ 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`.
41
+ 4. **Units.** Split the work into units (for example specs, domain code,
42
+ assets, review). Give each unit its own actor id and session id, and pass
43
+ them on everything the unit records (`--actor-id <unit> --session-id <session>`),
44
+ so the ledger shows which unit did what. A delegated unit attests its own
45
+ work; never attest another unit's work under your id.
46
+ 5. **Implement.** Write the code with tests whose names carry the scenario
47
+ id (`game.run.01: …`), so a test report binds to its scenario. Record a
48
+ defect you find in your own work as a finding the moment you find it,
49
+ before review:
50
+ `canon finding add --change-id <slug> --round <n> --seq <n> --severity <s> --reviewer <me> --actor-id <me> --session-id <session> --summary "…"`.
51
+ 6. **Evidence.** Per scenario:
52
+ `canon evidence add --project-id <root-id> --scenario-id <id> --kind test-run --role implementer --ref "<command>" --actor-id <unit> --session-id <session>`,
53
+ with `--report junit:<path>` or `--artifact <path>` when files prove it.
54
+ 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`.
56
+ An external evaluator's audit is recorded with `canon evidence add --kind
57
+ audit`; canon never generates one.
58
+ Evidence for a plan task (`--task <id>`, before `canon gate task`) must
59
+ bind the artifact or report that proves the task; a canon command
60
+ (`canon status`, `canon gate check`) is never that proof.
61
+ 7. **Independent review.** A different session from the one that wrote the
62
+ code and evidence reviews the change. The reviewer records every defect
63
+ 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,
66
+ `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
69
+ reviewer signs off each scenario:
70
+ `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).
72
+ 8. **Transition.** `canon subject status <id> <state>` along
73
+ `proposed → specced → building → verifying → shipped`. It refuses a move
74
+ the policy does not yet allow (missing evidence, missing reviews, open
75
+ 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.
80
+
81
+ ## Command reference
82
+
83
+ Load only the reference the current step needs.
84
+
85
+ ### Setup and provider projection
11
86
 
12
87
  ```bash
13
- canon init
88
+ canon init # canon.yaml, starter policy, AGENTS.md block, plans home
14
89
  canon skills install # detects .claude/.agents|.codex/.omp/.pi; if none exists, installs Claude+Codex
15
90
  canon skills install --providers=claude,codex,omp,pi
16
91
  canon skills check # read-only drift check
@@ -26,24 +101,23 @@ legacy `.codex/skills/canon*` projection from canon 0.13.0 or earlier is
26
101
  migrated by `canon skills install` and reported by `check`/`doctor`. OMP and Pi
27
102
  projections are project-local passive skill bundles: the
28
103
  `scripts/canon-retrieve-pre-dispatch.sh` file is available as a sidecar, not
29
- installed as a native hook. Load only the reference needed for the current
30
- task. `canon/skills-dev` is a separate developer-only legacy source and is
31
- installed explicitly with `--source canon/skills-dev`.
104
+ installed as a native hook. `canon/skills-dev` is a separate developer-only
105
+ legacy source and is installed explicitly with `--source canon/skills-dev`.
32
106
 
33
- ## Route by task
107
+ ### Route by task
34
108
 
35
- - **Start product work / pin a durable unit:** read `reference/canon-subject.md`, then run `canon context` and `canon subject`.
36
- - **Write or reorganize `.feature` specs:** read `reference/canon-authoring.md` before editing; use the scaffold.
109
+ - **Where the repo stands, subjects and changes (loop steps 1, 2, 8):** read `reference/canon-subject.md`; it covers `canon status`, `canon subject new|adopt|status` and `canon change new`.
110
+ - **Write or reorganize `.feature` specs (step 3):** read `reference/canon-authoring.md` before editing; use the scaffold.
37
111
  - **Understand available fields/enums/policy bindings:** read `reference/canon-context.md` and `reference/canon-vocab.md`.
38
112
  - **Capture replayable run inputs / version prompts:** read `reference/canon-context.md`, then use `canon context-pack create|show|verify` and `canon prompt register|show` with explicit repository-relative JSON manifests.
39
113
  - **Validate or index specs:** read `reference/canon-fmt.md` and `reference/canon-inventory.md`.
40
- - **Run evidence gates, flip tasks, install hooks:** read `reference/canon-gate.md`.
41
- - **Finish a subject (claim work done):** read `reference/canon-review.md`. Order: test-run evidence (`canon evidence add`) → independent review of the adopted change (`canon finding add` per issue, fix, `canon finding close --disposition fixed --resolution-sha <sha>`) → `canon review add` per scenario by a reviewer other than the evidence actor → `canon subject status`. Commit each record with `canon gate promote`.
42
- - **Ingest artifacts or sessions:** read `reference/canon-artifact-ingest.md`, `reference/canon-session-ingest.md`, or `reference/canon-plan-import.md`.
114
+ - **Evidence, gates, task checkboxes, hooks (step 6):** read `reference/canon-gate.md`.
115
+ - **Findings and reviews (step 7):** read `reference/canon-review.md`.
116
+ - **Ingest artifacts, sessions or plans:** read `reference/canon-artifact-ingest.md`, `reference/canon-session-ingest.md`, or `reference/canon-plan-import.md`.
43
117
  - **Configure storage, policy, plugins, or strategy learning:** read `reference/canon-storage.md`, `reference/canon-policy.md`, `reference/canon-plugins.md`, or `reference/canon-learn.md`.
44
118
  - **Retrieve dispatch guidance:** read `reference/canon-retrieve.md` and load `scripts/canon-retrieve-pre-dispatch.sh` when wiring hooks.
45
119
  - **Read generated reports and dashboards:** read `reference/canon-report-dashboard.md`.
46
120
 
47
- ## Safety contract
121
+ ### Safety contract
48
122
 
49
- `canon format --check`, `canon skills check`, and `canon gate check` are read-only. Do not hand-edit generated reports, provenance lines, locks, or task evidence. Inspect `canon context` before authoring typed artifacts, and treat every failure class as actionable evidence rather than bypassing the gate.
123
+ `canon status`, `canon format <root>`, `canon skills check`, and `canon gate check` are read-only. Do not hand-edit generated reports, provenance lines, locks, or task evidence. Inspect `canon context` before authoring typed artifacts, and treat every failure class as actionable evidence rather than bypassing the gate.
@@ -18,6 +18,23 @@ canon context --json # the full machine-readable surface
18
18
  canon context --repo ../other # resolve a specific repo's surface
19
19
  ```
20
20
 
21
+ The outline opens with a short header, then a blank line, then the
22
+ schema outline starting at `capabilityVersion:`:
23
+
24
+ ```text
25
+ status: run `canon status` for this repo's subjects, gaps and next commands
26
+ warning: no policy: .canon/policy.yaml not found, so `canon gate check` requires no evidence, failure cases or review here
27
+ ```
28
+
29
+ The `warning:` line appears when the policy file is absent or unreadable
30
+ or has no `spec_coverage` section (the gate then requires no evidence,
31
+ cases or review), or when that section is invalid (the gate refuses until
32
+ it is fixed); it is omitted when coverage is in force. `context` describes what CAN be
33
+ authored; `canon status` (see `canon-subject`) says where the repo stands
34
+ and what to run next. `--json` is unchanged: the header adds no field,
35
+ and `policy.spec_coverage`/`policy.diagnostics` already carry the same
36
+ fact.
37
+
21
38
  ## Reproducible context packs and prompt bundles
22
39
 
23
40
  Use an explicit, repository-relative JSON manifest when a run needs a
@@ -15,26 +15,27 @@ release-scoped check (below) enforces it. A human-only `flagged` overlay
15
15
  overrides the ladder — a flagged artifact is never green regardless of
16
16
  evidence. (Author `policy.yaml`'s trust/staleness fields via `canon-policy`.)
17
17
 
18
- ## The nine failure classes
18
+ ## The ten failure classes
19
19
 
20
20
  Every violation carries one of these stable, grep-able strings:
21
21
 
22
22
  | Class | Meaning |
23
23
  |---|---|
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
- | `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, or only reviews by its own evidence actor). See `canon-review`. |
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. |
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). |
28
28
  | `malformed-evidence` | A candidate record doesn't parse, is misfiled, or carries an unparseable interim tag. Malformed evidence is no evidence. |
29
29
  | `flagged` | The human-only `flagged` overlay is set — never green regardless of passing evidence. |
30
30
  | `unevidenced-flip` | `canon gate task <task_id>` was asked to flip a checkbox with no matching, non-`Divergent` evidence record. |
31
31
  | `fabricated-evidence` | An evidence note contains a blocklisted marker (`"would pass"`, `"TBD"`, `"n/a"`) or a bare `verified` claim with no attached command result. |
32
32
  | `open-blocker` | With `spec_coverage.require_review` set (and `block_on_findings`, the default), a Finding whose latest version is severity `blocker`, disposition `open`, on a change an in-scope subject adopted. Subject string `<change_id>#<round>.<seq>`. See `canon-review`. |
33
+ | `false-success` | The latest audit record (`canon evidence add --kind audit`) of a scenario says `refuted`: an external evaluator found a counterexample to the claimed success. Always on, regardless of policy or sampling, and takes precedence over any declared faithful verdict; only a newer audit (`confirmed` or `indeterminate`) clears it. The detail names the evaluator, its version, its sha256 and the counterexample paths. |
33
34
 
34
35
  ## `canon gate check [--repo <dir>] [--release]`
35
36
 
36
37
  Assembles coverage + ledger + staleness + the always-on trust-ladder
37
- check, plus the opt-in spec-coverage check, over the resolved repo's
38
+ check + the bound-artifact check, plus the opt-in spec-coverage check, over the resolved repo's
38
39
  corpus and runs them, printing every violation grouped by failure class.
39
40
  `--release` additionally engages the release-scoped
40
41
  `trust-below-required` check; the trust-ladder check is never dropped
@@ -50,6 +51,12 @@ Exit `0` clean, `1` gate-red (any violation), `2` usage/load failure
50
51
  resolves through the nearest-ancestor `canon.yaml` walk, so it reads the
51
52
  repo ROOT's `.canon/policy.yaml` and `.canon/ledger` from any subdirectory.
52
53
 
54
+ When `.canon/policy.yaml` exists but has no approval, or its bytes no
55
+ longer match the approved digest, the gate prints `advisory: policy not
56
+ approved: …` or `advisory: policy changed since approval: …` after its
57
+ result, naming `canon policy approve --by <who>`; the exit code is
58
+ unchanged (see `canon-policy`).
59
+
53
60
  ### The spec-derived worklist (`spec_coverage`, opt-in)
54
61
 
55
62
  Coverage groups the EVIDENCE corpus, so it can only ever enumerate
@@ -60,7 +67,7 @@ adds the other direction: it starts from the Scenario corpus
60
67
  an unimplemented or mismatched spec becomes a reported violation.
61
68
 
62
69
  ```yaml
63
- # .canon/policy.yaml — ABSENT by default; absent means zero violations
70
+ # .canon/policy.yaml — absent means zero violations; `canon init` writes a starter
64
71
  spec_coverage:
65
72
  require_evidence: true
66
73
  scope: [building, verifying] # optional; omit for the whole corpus
@@ -69,11 +76,28 @@ spec_coverage:
69
76
  require_review: {} # optional; see Independent review below
70
77
  ```
71
78
 
79
+ `canon init` writes a starter `.canon/policy.yaml` with this section on
80
+ (`require_evidence: true`, `require_cases: [failure]`,
81
+ `require_review: {}`; see `canon-policy`). When the section is absent and
82
+ the indexed corpus has scenarios, the gate says so after its result, on
83
+ stdout like its other advisories, and the exit code is unchanged:
84
+
85
+ ```
86
+ advisory: spec_coverage is off — 12 scenario(s) are not checked for evidence; see .canon/policy.yaml
87
+ ```
88
+
89
+ A present section, even `require_evidence: false` or a malformed one
90
+ (already a violation), and an empty corpus print no advisory.
91
+
72
92
  - **Unimplemented** — no `EvidenceRecord` carries the scenario's
73
93
  `(project_id, scenario_id)`. Author one with
74
94
  `canon evidence add --scenario-id <id> --project-id <root-id>`; both
75
95
  flags are required together, because two spec roots may carry the same
76
- scenario id.
96
+ scenario id. `--summary` is allowed here too (scanned for fabrication
97
+ markers like a task's); it is a row suffix only when the record also
98
+ names a `--task`. A record carrying both `--task` and `--scenario-id`
99
+ is one attestation answering both joins: it counts for the task cell
100
+ and for the scenario cell (verdicts, the shipped gate, the review gate).
77
101
  - **Mismatched** — the folded divergence state is `open`,
78
102
  `still-divergent`, or `resolved-invalid` (a resolution whose app sha
79
103
  has moved), or the latest ledger verdict is `divergent`.
@@ -94,12 +118,18 @@ spec_coverage:
94
118
  - **Independent review** (`require_review`) — scenarios whose subject is
95
119
  in `require_review.scope` (default `[verifying, shipped]`; `[]` = every
96
120
  scenario; independent of `scope` above) need a `Review` record
97
- (`canon review add`), by a reviewer and actor other than the
98
- scenario's evidence actor when `distinct_actor` (default `true`);
99
- gaps are `unreviewed-promotion`. With `block_on_findings` (default
100
- `true`), an open `blocker` Finding on a change an in-scope subject
101
- adopted is `open-blocker`. `exclude_lanes` also exempts scenarios
102
- from review. Full contract: `canon-review`.
121
+ (`canon review add`) whose latest version per reviewer is `signed`,
122
+ still matches the scenario digest and latest evidence it pinned
123
+ (a review written before 0.15 has no pins and skips this check), and,
124
+ when `distinct_actor` (default `true`), is by a reviewer and actor
125
+ other than the scenario's evidence actor and from a session other than
126
+ any evidence session when both carry one (`--session-id`);
127
+ gaps are `unreviewed-promotion`, and the detail says whether the
128
+ review is missing, a self-review, stale (naming whether the spec, the
129
+ evidence or both changed), or not signed. With `block_on_findings`
130
+ (default `true`), an open `blocker` Finding on a change an in-scope
131
+ subject adopted is `open-blocker`. `exclude_lanes` also exempts
132
+ scenarios from review. Full contract: `canon-review`.
103
133
 
104
134
  ```yaml
105
135
  spec_coverage:
@@ -110,12 +140,28 @@ spec_coverage:
110
140
  block_on_findings: true # default
111
141
  ```
112
142
 
143
+ - **Sampled audits** (`require_audit`) — a deterministic sample of
144
+ in-scope scenarios (`require_audit.scope`, default `[verifying,
145
+ shipped]`; `exclude_lanes` applies) must carry an external audit
146
+ (`canon evidence add --kind audit`, see Audit records below). A sampled
147
+ scenario whose latest audit is missing or `indeterminate` is
148
+ `uncovered-cell`:
149
+
150
+ ```
151
+ uncovered-cell cart.add.01 — sampled for audit by spec_coverage.require_audit (rate 0.1, seed `s1`) and has no audit record — …
152
+ uncovered-cell cart.add.01 — sampled for audit by spec_coverage.require_audit (rate 0.1, seed `s1`), but its latest audit is indeterminate (evaluator `judge` 1.2) — …
153
+ ```
154
+
155
+ Keys, defaults and the exact sampling formula: `canon-policy`.
156
+
113
157
  Two refusals rather than a silent pass, both surfacing as
114
158
  `uncovered-cell` on the subject `spec_coverage`:
115
159
 
116
160
  - A **malformed section** (unknown key, unknown `scope` status, or a
117
- malformed `require_cases`/`require_review` — unknown key, bad status,
118
- `null`, non-mapping) refuses at check time. It is never treated as
161
+ malformed `require_cases`/`require_review`/`require_audit` — unknown
162
+ key, bad status, `null`, non-mapping, a missing or out-of-range
163
+ `rate`) refuses at check time (`uncovered-cell spec_coverage — ...
164
+ unusable`). It is never treated as
119
165
  absent — a typo must not be silently equivalent to not opting in.
120
166
  - A **corpus kind routed off the gate's rung** (`scenario`,
121
167
  `divergence`, or `subject` sent anywhere but `local`) refuses, because
@@ -125,6 +171,54 @@ Malformed rows in those three kinds do NOT surface as
125
171
  `malformed-evidence`; they are kept off the evidence violation set on
126
172
  purpose, so enabling nothing changes no existing verdict.
127
173
 
174
+ ### Audit records (`--kind audit`)
175
+
176
+ canon never generates or runs an audit. An external evaluator does, and
177
+ canon records its result:
178
+
179
+ ```bash
180
+ canon evidence add --kind audit --project-id root --scenario-id cart.add.01 \
181
+ --ref "judge run 42" --role reviewer \
182
+ --evaluator-id judge --evaluator-version 1.2 \
183
+ --evaluator-digest <64 lowercase hex sha256> \
184
+ --audit-result refuted --report junit:audit/counterexample.xml
185
+ ```
186
+
187
+ - `--kind audit` requires `--evaluator-id`, `--evaluator-version`,
188
+ `--evaluator-digest` (sha256, 64 lowercase hex), `--audit-result
189
+ confirmed|refuted|indeterminate`, and `--scenario-id` with
190
+ `--project-id`. It refuses `--task`, `--verdict`, `--surface-ref` and
191
+ `--approval-*`; the four evaluator flags are refused on any other kind.
192
+ All refusals exit `2`.
193
+ - The record carries `audit: {evaluator_id, evaluator_version,
194
+ evaluator_digest, result}`; its verdict is derived (`confirmed` →
195
+ faithful, `refuted` → divergent, `indeterminate` → not-applicable).
196
+ - Optional `--report`/`--artifact` counterexamples are bound and stored
197
+ like any report; the latest audit's bindings are re-hashed by the
198
+ artifact check.
199
+ - Audits fold apart: per `(project_id, scenario_id)` there is a latest
200
+ declared record and a latest audit. An audit never becomes the
201
+ scenario's latest declared verdict.
202
+ - `canon subject status <id> shipped` refuses `false-success`, and an
203
+ owed sampled audit when `require_audit.scope` covers `shipped`.
204
+ - `canon status` shows an `audits` section (sampled / confirmed / owed /
205
+ refuted) and suggests `canon evidence add --kind audit …` in `next:`.
206
+
207
+ ### Dropping a binding (`--drop-attachment`)
208
+
209
+ `canon evidence add` for a scenario refuses (exit `2`) when the
210
+ scenario's current latest record of the same fold (declared records, or
211
+ audit records for an audit) — committed ledger plus `_staging` — binds
212
+ files the new record does not. The refusal names the paths and two ways
213
+ out: re-bind them (`--artifact`/`--report`; new bytes at the same path
214
+ are fine), or drop them on purpose with `--drop-attachment <path>`
215
+ (repeatable) and one single-line `--drop-reason <text>`. The drop is
216
+ recorded as `attachment_drop: {paths, reason}`. Refused (`2`): a drop
217
+ without a reason, a reason without a drop, a drop path that is not
218
+ actually dropped, a multi-line reason, or a drop without
219
+ `--scenario-id` (task-only evidence has no drop guard). `canon status`
220
+ lists recorded drops (`dropped bindings (N):`; `drops[]` in `--json`).
221
+
128
222
  ### Review-waiver advisories
129
223
 
130
224
  A subject moved into a review-scoped status with `canon subject status
@@ -141,6 +235,17 @@ review waivers: 1 advisory(ies) — not failing the gate:
141
235
 
142
236
  The waiver lasts only until the subject's next status write.
143
237
 
238
+ ### Reviews that predate pinning
239
+
240
+ 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:
244
+
245
+ ```
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
247
+ ```
248
+
144
249
  ## Experimental: evidence binding (`experimental.evidence_binding`, off by default)
145
250
 
146
251
  An evidence record is an attestation: canon never runs `--ref`, and it
@@ -159,12 +264,20 @@ canon evidence add ... --report junit:target/nextest/default/junit.xml
159
264
  canon evidence add ... --report cucumber:reports/cucumber.json
160
265
  ```
161
266
 
162
- The case is found by the scenario id in its name, classname, or Cucumber
163
- tag — dotted (`cart.add.04`) or underscored (`cart_add_04`, the form a
164
- test function carries) — or named exactly with `--report-case <name>`
165
- (a Rust test by its function name). No match refuses (exit 2); a
166
- `faithful` verdict over a failed or skipped case refuses (exit 1). Files
167
- must be inside the repository and are recorded by relative path.
267
+ A case is bound when the scenario id is its OWN: the first
268
+ scenario-id-shaped token (`<area>.<surface>.<nn>`) in its full name —
269
+ classname (a describe path) first, then name — dotted (`cart.add.04`) or
270
+ underscored (`cart_add_04`, the form a test function carries), equals
271
+ the scenario id; a Cucumber tag equal to it binds too. Start each test
272
+ name with its scenario id: `game.session.04: restarting as in
273
+ game.session.01` binds to `game.session.04` only, never to the id it
274
+ mentions. Each bound case is its own attachment with its name and
275
+ outcome. `--report-case <name>` (repeatable; a Rust test by its
276
+ function name) ADDS cases; it never hides the scenario's own. No match,
277
+ or a `--report-case` naming no case, refuses (exit 2); a `faithful`
278
+ verdict when ANY bound case failed or was skipped refuses (exit 1). Files
279
+ must be inside the repository, are recorded by relative path, and are
280
+ stored (see Bound evidence stays provable).
168
281
 
169
282
  Strength, weakest first: `attested` (no attachment) < `artifact` (any
170
283
  bound file) < `report` (a parsed report whose case passed). The policy
@@ -189,6 +302,49 @@ violation in every mode. Canon still cannot know whether the bound test
189
302
  actually exercises the scenario — binding narrows the trust gap, it does
190
303
  not close it.
191
304
 
305
+ ## Bound evidence stays provable (the artifact store)
306
+
307
+ `canon evidence add --artifact/--report` copies each bound file into
308
+ `.canon/artifacts/sha256/<hex>`, named by its sha256. Writes go through a
309
+ temp file and a rename, and a digest already stored intact is not written
310
+ again. The directory is tracked in git (`canon init` never ignores it):
311
+ commit it with the ledger. A file larger than 25 MiB is refused (exit 2);
312
+ raise the limit with `--max-artifact-mib <N>`.
313
+
314
+ `canon gate check` re-checks every file bound to a LATEST evidence record
315
+ (the latest per task and per `(project_id, scenario_id)`):
316
+
317
+ - the stored blob exists and hashes to the recorded digest → clean, even
318
+ after the working-tree file is rewritten (a smoke script that rewrites
319
+ `reports/smoke.json` on every run no longer orphans the record);
320
+ - no blob, but the working-tree path still hashes to the recorded digest
321
+ → clean, with an advisory naming `canon evidence vault` (records from
322
+ before the store existed):
323
+
324
+ ```
325
+ evidence artifacts: 1 bound file(s) not in the artifact store — not failing the gate; run `canon evidence vault` to store them before the working tree changes:
326
+ unstored cart.add.01 reports/junit.xml (sha256 b7cb…) is verified against the working tree only
327
+ ```
328
+ - anything else → `stale-evidence` naming the path, the recorded digest,
329
+ and what the store and the working tree hold now (`missing` or a
330
+ digest), ending with why: a stored blob that no longer hashes to its
331
+ name "does not match its name, so its bytes were altered"; a missing
332
+ one means "the bound bytes are gone". Re-run the test and re-attest
333
+ with `canon evidence add`.
334
+
335
+ The check applies to every latest record, not only subjects in
336
+ `spec_coverage.scope`: a binding is a claim about bytes whatever the
337
+ subject's status, and a repo with no `spec_coverage` would otherwise never
338
+ have its bindings checked. A superseded record is not checked —
339
+ re-attesting is the fix.
340
+
341
+ `canon evidence vault [--max-artifact-mib <N>]` stores the bytes for
342
+ existing records: every attachment whose working-tree file still matches
343
+ its digest is copied in, and everything else is listed as not stored,
344
+ with both digests. It exits `0` whenever it ran (the gate decides whether
345
+ an unstored file matters), `2` when the ledger cannot be read or a write
346
+ fails.
347
+
192
348
  ## Effect-aware risk approvals (`risk_tiers`)
193
349
 
194
350
  `risk_tiers` is absent by default and remains a no-op when absent. When it
@@ -229,12 +385,28 @@ the compat default `[{ dialect: openspec, root: <repo> }]`; no source
229
385
  locating the task at all is a loud failure naming every source consulted.
230
386
  It requires
231
387
  a matching non-`Divergent` evidence record, and flips `- [ ]` → `- [x]`
232
- with an appended evidence note ONLY on a clean check. Every other path —
233
- missing evidence, a `Divergent` verdict, a fabricated note — leaves the
234
- row byte-unchanged and exits `1` with the blocking violation on stderr.
235
- An already-`[x]` row is an idempotent no-op (exit `0`). An unknown
388
+ with an appended evidence note ONLY on a clean check. The note
389
+ aggregates every record bound to the task, on one line:
390
+ `— ✅ 3 evidence records (2 faithful, 1 not-applicable); latest: <summary>`
391
+ (the latest `--summary`, or a default naming the latest passing record).
392
+ Every other path — missing evidence, a `Divergent` verdict, a fabricated
393
+ note on any of the task's records — leaves the row byte-unchanged and
394
+ exits `1` with the blocking violation on stderr. After a flip, the plan
395
+ source is re-ingested so the record store's task status agrees with the
396
+ checkbox (`canon query --kind task` reads `done`); a failed re-ingest is
397
+ a stderr warning naming `canon ingest plans`, never a failed flip.
398
+ The success line is `canon gate task: <task_id> flipped — written
399
+ directly; nothing to promote`. An already-`[x]` row is an idempotent no-op (exit `0`). An unknown
236
400
  `task_id` is reported (exit `1`).
237
401
 
402
+ A flip on evidence that cannot prove the task still succeeds, but warns
403
+ on stderr: when none of the task's evidence records binds a file and one
404
+ of them cites a canon command as its `--ref` (`canon gate check`,
405
+ `canon status`), the warning says to bind the artifact or report that
406
+ proves the task (`canon evidence add --task <id> --artifact <path>` or
407
+ `--report <format>:<path>`). A canon command reports on canon's own
408
+ records, so it is never the proof of a task.
409
+
238
410
  ```bash
239
411
  canon gate task my-change#5.2 --repo .
240
412
  ```
@@ -258,6 +430,15 @@ canon gate promote --repo . # land every staged candidate
258
430
  canon gate promote --repo . --dry-run # preview only
259
431
  ```
260
432
 
433
+ 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
437
+ resolve|defer`, `subject new|adopt|status`, `change new`, `scenario new`, `feature new`,
438
+ `gate task` (it edits the plan document and re-ingests it)
439
+ — is final when it returns, and its success line ends `— written
440
+ directly; nothing to promote`.
441
+
261
442
  ## `canon gate install-hooks [--repo] [--event] [--matcher] [--command] [--timeout]`
262
443
 
263
444
  Idempotent, diff-only hook-seam installation: merges one
@@ -280,8 +461,8 @@ Prefer this over hand-editing `settings.json`/`hooks.json`.
280
461
 
281
462
  ## `canon gate selftest`
282
463
 
283
- Runs the shipped fixture corpus — one fixture per failure class (nine,
284
- including `open-blocker`), each a
464
+ Runs the shipped fixture corpus — one fixture per failure class (ten,
465
+ including `false-success`), each a
285
466
  deliberately broken corpus proving that class fires and ONLY that class
286
467
  (both under-detection and over-triggering fail the run). Takes no
287
468
  `--repo`; self-contained. Run it before trusting any other `canon gate
@@ -17,6 +17,8 @@ plans:
17
17
  root: . # resolved relative to canon.yaml's directory
18
18
  ```
19
19
 
20
+ - `canon init` writes exactly this source, plus an empty
21
+ `openspec/changes/` (so the adapter never scans the repo root itself).
20
22
  - An absent `plans:` section = zero sources (a clean no-op).
21
23
  - A present section parses STRICTLY: a typo'd key, an unregistered
22
24
  `dialect`, or a nonexistent `root` fails the command loud, naming the
@@ -135,8 +137,8 @@ downstream reader's job.
135
137
  ## What this skill does NOT cover
136
138
 
137
139
  - **Authoring an openspec change dir** (`proposal.md`/`tasks.md`
138
- conventions) — see the openspec CLI's own docs; this skill covers
139
- IMPORTING that shape, not producing it.
140
+ conventions) — `canon change new` scaffolds one and adopts it into a
141
+ subject (see `canon-subject`); this skill covers IMPORTING that shape.
140
142
  - **`canon gate check`/`canon gate task`** (the hand-authored one-task-at-
141
143
  a-time path this complements at bulk) — see `canon-gate`.
142
144
  - **Wiring an imported `Task`/`Change` into a `canon gate` decision** —
@@ -23,6 +23,71 @@ uncovered-cell spec_coverag — `spec_coverag` is not a known top-level policy.y
23
23
  The known keys still resolve normally, and `canon context` lists the
24
24
  diagnostic and reports `clean: false`.
25
25
 
26
+ ## The starter policy `canon init` writes
27
+
28
+ A fresh `canon init` writes `.canon/policy.yaml` (skip it with
29
+ `--no-policy`; an existing file is never overwritten):
30
+
31
+ ```yaml
32
+ spec_coverage:
33
+ require_evidence: true # every scenario needs evidence
34
+ require_cases: [failure] # every feature surface needs a @case:failure scenario
35
+ require_review: {} # verifying/shipped subjects need an independent review
36
+ ```
37
+
38
+ The file carries comments explaining each field. It loads with zero
39
+ diagnostics. It is what `canon gate check` grades the work against, so a
40
+ human should review and approve it, and every later change to it (next
41
+ section). The agent doing the work should not write its own grading
42
+ policy. Narrow it with `scope: [building, verifying]` to check evidence
43
+ only for subjects in those statuses (see `canon-gate`).
44
+
45
+ ## Approving the policy
46
+
47
+ ```bash
48
+ canon policy approve --by <who> [--note <text>] [--repo <dir>]
49
+ ```
50
+
51
+ A human runs this after reviewing `.canon/policy.yaml`. It writes
52
+ `.canon/policy.approval.json` directly (nothing to promote):
53
+
54
+ ```json
55
+ {
56
+ "policy_digest": "sha256:<sha256 of the policy.yaml bytes>",
57
+ "approved_by": "<who>",
58
+ "approved_at": "2026-10-11T09:00:00Z",
59
+ "note": "<text, or null>"
60
+ }
61
+ ```
62
+
63
+ Commit it with the policy: the file is tracked, and neither canon's
64
+ `.gitignore` nor the one `canon init` writes ignores it. Running the
65
+ command again replaces the record. With no `.canon/policy.yaml` it is
66
+ refused (exit `2`), and so is an empty `--by`.
67
+
68
+ While a policy exists with no approval, `canon gate check` prints after
69
+ its result:
70
+
71
+ ```
72
+ advisory: policy not approved: .canon/policy.yaml has no approval record (.canon/policy.approval.json); a human reviews it and runs `canon policy approve --by <who>`
73
+ ```
74
+
75
+ Once the policy's bytes no longer match the approved digest, it prints
76
+ this instead:
77
+
78
+ ```
79
+ advisory: policy changed since approval: .canon/policy.yaml is sha256:<now>, but `<who>` approved sha256:<then> at <time>; a human reviews it and runs `canon policy approve --by <who>`
80
+ ```
81
+
82
+ An approval file that does not parse reads
83
+ `advisory: policy approval unreadable: …`.
84
+ `canon status` prints the same text as a `warning:` line (and in
85
+ `--json` `warnings`). The advisory never changes an exit code: canon
86
+ cannot tell whether a human or an agent ran the command, so the record
87
+ names who approved which bytes, and the advisory says when that no
88
+ longer holds. An agent never runs `canon policy approve` on its own
89
+ behalf.
90
+
26
91
  ## The closed CEL profile
27
92
 
28
93
  Every expression is evaluated against exactly one variable, `record` (the
@@ -105,6 +170,40 @@ question rather than adding an expression field. Conditional logic about
105
170
  what a record *means* lives in `policy.yaml`/`applies_when:`, evaluated
106
171
  against the record's fields, never inside the record.
107
172
 
173
+ ## Sampled audits: `spec_coverage.require_audit`
174
+
175
+ Optional; absent means no audit is ever owed (a recorded `refuted` audit
176
+ is still `false-success` regardless — see `canon-gate`).
177
+
178
+ ```yaml
179
+ spec_coverage:
180
+ require_audit:
181
+ rate: 0.1 # required: a number in [0, 1]
182
+ seed: "2026-q4" # optional string; default ""
183
+ scope: [verifying, shipped] # optional subject statuses; this is the default; [] = every scenario
184
+ ```
185
+
186
+ `spec_coverage.exclude_lanes` applies. A malformed `require_audit` —
187
+ unknown key, missing `rate`, `rate` out of range or a string, bad
188
+ `scope`, `null` or a non-mapping — poisons the whole `spec_coverage`
189
+ section: the gate reports `uncovered-cell spec_coverage — ... unusable`
190
+ rather than checking anything under it.
191
+
192
+ Sampling is deterministic. A scenario is in the sample iff the first 8
193
+ bytes of
194
+
195
+ ```
196
+ sha256(seed || 0x00 || project_id || 0x00 || scenario_id)
197
+ ```
198
+
199
+ read as a big-endian u64, are less than `rate · 2^64` — computed exactly,
200
+ as a comparison against `ceil(rate · 2^64)`, so `rate: 0` samples nothing
201
+ and `rate: 1` samples everything. A sampled in-scope scenario whose
202
+ latest audit is missing or `indeterminate` is `uncovered-cell`; with
203
+ `shipped` in `scope`, `canon subject status <id> shipped` also refuses
204
+ it. `canon context` and the `canon status` policy line show
205
+ `require_audit=(rate=… seed=`…` scope=…)`.
206
+
108
207
  ## Effect-aware risk tiers
109
208
 
110
209
  The optional `risk_tiers` section is not CEL. Absent means no risk-approval
@@ -17,15 +17,15 @@ state they are in.
17
17
  The order an agent follows to claim a subject done:
18
18
 
19
19
  1. **Test-run evidence** per owned scenario (`canon evidence add
20
- --scenario-id <id> --project-id <root-id> ...`, then `canon gate
21
- promote`). See `canon-gate`.
20
+ --scenario-id <id> --project-id <root-id> ... --session-id <session>`,
21
+ then `canon gate promote`). See `canon-gate`.
22
22
  2. **Independent review** of the change(s) the subject adopted. Record
23
23
  each issue the reviewer raises as a Finding, fix it, then close it:
24
24
 
25
25
  ```bash
26
26
  canon finding add --change-id c-demo --round 1 --seq 1 \
27
27
  --severity blocker --disposition open --reviewer reviewer-2 \
28
- --summary "unbounded subprocess wait"
28
+ --summary "unbounded subprocess wait" --session-id review-session-1
29
29
  canon gate promote
30
30
  # ... fix, commit ...
31
31
  canon finding close --change-id c-demo --round 1 --seq 1 \
@@ -33,15 +33,23 @@ The order an agent follows to claim a subject done:
33
33
  canon gate promote
34
34
  ```
35
35
 
36
- 3. **One Review per scenario**, by someone other than the evidence actor:
36
+ 3. **One Review per scenario**, by someone other than the evidence actor,
37
+ from a session other than the one that attested the evidence, after
38
+ the evidence is in (the review pins what it saw):
37
39
 
38
40
  ```bash
39
41
  canon review add --project-id root --scenario-id world.demo.01 \
40
42
  --reviewer reviewer-2 --actor-id reviewer-2 --role reviewer \
43
+ --session-id review-session-1 \
41
44
  --pin "$(git rev-parse HEAD)" --upstream-ref "pr#41"
42
- canon gate promote
43
45
  ```
44
46
 
47
+ `review add` writes directly; there is nothing to promote. If the
48
+ reviewer could not reach a verdict, record that instead of signing:
49
+ `--outcome indeterminate` (or `unavailable` when it could not run).
50
+ Changing the scenario or re-attesting it afterwards makes the review
51
+ stale; review it again.
52
+
45
53
  4. **Move the subject**: `canon subject status <id> verifying`, later
46
54
  `shipped`. See `canon-subject`.
47
55
 
@@ -57,15 +65,37 @@ work collapsed into one identity.
57
65
  canon finding add --change-id <c> --round <n> --seq <n> \
58
66
  --severity blocker|should-fix|note --disposition open \
59
67
  --reviewer <who> --summary "<one line>" \
60
- [--reviewed-sha <sha>] [--file-ref path/to/file.rs:120-134] [--actor-id <id>]
68
+ [--reviewed-sha <sha>] [--file-ref path/to/file.rs:120-134] [--actor-id <id>] [--session-id <id>]
61
69
  canon finding close --change-id <c> --round <n> --seq <n> \
62
- --disposition fixed --resolution-sha <sha> # or rejected / deferred / open
70
+ --disposition fixed --resolution-sha <sha> [--session-id <id>] # or rejected / deferred / open
63
71
  ```
64
72
 
65
- Both stage a candidate; `canon gate promote` commits it. `finding close`
73
+ Both stage a candidate; `canon gate promote` commits it, and both say so
74
+ (``… — run `canon gate promote` to commit it``). `finding close`
66
75
  changes only the disposition (and `resolution_sha` with it); severity,
67
76
  reviewer, summary, and refs are copied from the committed record. It
68
- appends a second version at the same natural key — the first stays.
77
+ appends a second version at the same natural key — the first stays. Its
78
+ `--actor-id` and `--session-id` name who authored the transition; the
79
+ raiser's stay on the first version.
80
+
81
+ Every sha flag (`--reviewed-sha`, `--resolution-sha`, `--introduced-by`)
82
+ takes a full 40-character sha or a short one (4 or more lowercase hex
83
+ characters). A short sha is resolved with `git rev-parse --verify
84
+ <sha>^{commit}` in the repo, and the record stores the full sha. It is
85
+ refused (exit `2`) when it names no commit, when more than one commit
86
+ starts with it (pass more characters), when it only resolves by peeling a
87
+ tag, or outside a git work tree.
88
+
89
+ ### A defect the author found in its own work
90
+
91
+ Record it like any other finding, at the moment you find it and before
92
+ review, with `--reviewer` and `--actor-id` set to your own id; never ask
93
+ the reviewer to record it later under theirs. Nothing refuses a finding
94
+ whose reviewer is also the implementer, and an open `blocker` the author
95
+ raised blocks under `block_on_findings` exactly like a reviewer's.
96
+ `--introduced-by` names the commit that introduced the defect, not who
97
+ found it: `--introduced-by self` is a usage error (exit 2) that points at
98
+ `--reviewer`.
69
99
 
70
100
  ### Severities
71
101
 
@@ -98,15 +128,70 @@ fixed finding reopened with `--disposition open` counts as open again.
98
128
  ```bash
99
129
  canon review add --project-id <root-id> --scenario-id <id> \
100
130
  --reviewer <who> --pin <sha-or-ref> --role <role> \
101
- (--upstream-ref <ref> | --original-spec-ref <ref>) [--actor-id <id>]
131
+ (--upstream-ref <ref> | --original-spec-ref <ref>) [--actor-id <id>] [--session-id <id>] \
132
+ [--outcome signed|indeterminate|unavailable] \
133
+ [--reviewer-provider <p>] [--reviewer-model <m>] [--reviewer-harness <h>]
102
134
  ```
103
135
 
104
136
  One attributed Review record for one scenario, joined on
105
137
  `(project_id, scenario_id)` — both are required because two spec roots
106
138
  may carry the same scenario id. Exactly one provenance ref is required.
107
- `--actor-id` defaults to `canon`. A Review carries no verdict; the
108
- `verifying → shipped` ship gate still refuses missing or `Divergent`
109
- evidence verdicts on its own. Commit with `canon gate promote`.
139
+ `--actor-id` defaults to `canon`; `--session-id` fills `actor.session_id`.
140
+ A Review carries no verdict about the behavior; the `verifying → shipped`
141
+ ship gate still refuses missing or `Divergent` evidence verdicts on its
142
+ own. It is written directly to the ledger, with no staging step, and says
143
+ so: `canon review add: wrote <path> (new) — written directly; nothing to
144
+ promote`.
145
+
146
+ ### What the review saw (pins)
147
+
148
+ `review add` reads the corpus and records `pins` on the review; the
149
+ reviewer never types them:
150
+
151
+ - `scenario_digest` — the scenario record's `scenario_digest`: a sha256
152
+ over that scenario alone, as `canon inventory sync` stores it. It
153
+ covers the scenario's tags and block (header, steps, doc strings,
154
+ tables, `Examples:`) and the context it inherits: the Feature's tags,
155
+ the Feature `Background:`, and an enclosing `Rule:` with its tags and
156
+ `Background:`. Layout does not count: comments, blank lines, line
157
+ indentation, tag order, and table alignment (cells are compared
158
+ trimmed and unescaped). Values do: any step, tag or cell change, and
159
+ whitespace inside a doc string after the delimiter's own indentation
160
+ is removed. Other scenarios never count. A record synced before 0.15 has no
161
+ `scenario_digest`; then the review pins its `source_digest` (the
162
+ sha256 of the whole `.feature` file) and records
163
+ `scenario_digest_scope: file`;
164
+ - `evidence` — the scenario's latest evidence record per role, each as
165
+ `{key, digest}`: the ledger file `kind=evidence_record/<key>__<digest>.json`.
166
+ A scenario with no evidence yet is pinned with an empty list.
167
+
168
+ It prints `canon review add: pinned <id> at scenario digest <12 hex> and
169
+ <n> latest evidence record(s); …` (`at source digest` for a file pin). A
170
+ scenario with no record (never synced) is refused with exit 2 and nothing
171
+ is written: there is nothing to pin.
172
+
173
+ The gate compares each pin like for like: a scenario pin with the
174
+ scenario's current `scenario_digest`, a file pin with its current
175
+ `source_digest`. Adding or editing another scenario in the file leaves a
176
+ scenario-pinned review current; editing this scenario, or a Background
177
+ or Rule it inherits, makes it stale. A file-pinned review stays a file
178
+ pin until it is reviewed again. Review after the spec and evidence are
179
+ final.
180
+
181
+ ### The reviewer's outcome and runtime
182
+
183
+ `--outcome` records how the reviewer's run ended: `signed` (the default:
184
+ an approval), `indeterminate` (it ran and reached no verdict) or
185
+ `unavailable` (it could not run). Only `signed` counts; the other two
186
+ are recorded so a failed review is visible, never mistaken for a
187
+ sign-off or a rejection, and `review add` says so. `--reviewer-provider`,
188
+ `--reviewer-model` and `--reviewer-harness` record the runtime that
189
+ produced the review, as given. An empty or padded label is a usage error.
190
+
191
+ `--session-id` (on `evidence add`, `review add`, `finding add` and
192
+ `finding close`) takes one opaque token: non-empty, no surrounding
193
+ whitespace, no control characters. Anything else is a usage error
194
+ (exit 2) and nothing is written.
110
195
 
111
196
  ## The `require_review` policy (opt-in)
112
197
 
@@ -129,8 +214,11 @@ spec_coverage:
129
214
  scenarios from review.
130
215
  - **`distinct_actor`** — a review only counts when both its `reviewer`
131
216
  and its actor (`--actor-id`) differ from every actor on that
132
- scenario's evidence records. An agent cannot review its own attested
133
- work.
217
+ scenario's evidence records, and, when the review carries a session,
218
+ that session differs from every session on those evidence records. An
219
+ agent cannot review its own attested work, under another id or from
220
+ the same session. A record with no session never matches one, so
221
+ records written before sessions were recorded count by actor alone.
134
222
  - **`block_on_findings`** — fail on open blocker findings (below).
135
223
 
136
224
  A malformed `require_review` (an unknown key, an unknown status, `null`,
@@ -138,13 +226,46 @@ a non-mapping) poisons the whole `spec_coverage` section: `canon gate
138
226
  check` reports it invalid rather than treating it as off, the same as a
139
227
  malformed `require_cases`.
140
228
 
141
- Canon's own `.canon/policy.yaml` does not enable `require_review`.
229
+ Canon's own `.canon/policy.yaml` enables `require_review: {}`, and so does
230
+ the starter policy `canon init` writes.
142
231
 
143
232
  ## How it meets the gate
144
233
 
234
+ ### Which review counts
235
+
236
+ The reviews of a scenario are folded to the **latest per `reviewer`**
237
+ first (by `at`, then schema, then content digest — the fold every reader
238
+ uses). A reviewer's later review replaces their earlier one: a reviewer
239
+ who signed and later recorded `indeterminate` no longer counts as
240
+ signing, and a later `signed` review counts again. Reviews by different
241
+ reviewers never replace each other.
242
+
243
+ The scenario is reviewed when at least one of those latest reviews:
244
+
245
+ 1. is `signed`;
246
+ 2. still matches its pins — the scenario record's current
247
+ `source_digest`, and its current latest evidence record per role —
248
+ when it has pins;
249
+ 3. passes `distinct_actor`, when that is on.
250
+
251
+ A review written before 0.15 has no pins. It keeps counting under rules
252
+ 1 and 3, without the freshness check, and `canon gate check` prints one
253
+ advisory line counting such in-effect reviews (exit code unchanged):
254
+
255
+ ```
256
+ 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
257
+ ```
258
+
259
+ `canon status` prints the same line as a warning, and `canon subject
260
+ status` prints it on stderr for the subject's own scenarios. Reviewing
261
+ the scenario again pins it and drops it from the count.
262
+
145
263
  ### `unreviewed-promotion`
146
264
 
147
- An in-scope scenario with no qualifying review:
265
+ An in-scope scenario with no qualifying review. When several latest
266
+ reviews fail for different reasons, the detail names the one closest to
267
+ counting: a current signed review failing only `distinct_actor`, then a
268
+ stale review, then non-signed outcomes.
148
269
 
149
270
  ```
150
271
  unreviewed-promotion world.demo.01 — no review record for this scenario; spec_coverage.require_review wants an independent review (`canon review add`) once its subject is claimed done
@@ -156,6 +277,29 @@ or, when the only reviews are self-reviews:
156
277
  unreviewed-promotion world.demo.01 — every review of this scenario is by an actor that also attested its evidence (`impl-agent`); spec_coverage.require_review.distinct_actor wants a reviewer other than the evidence actor
157
278
  ```
158
279
 
280
+ or, when another actor id reviewed from a session that attested the
281
+ evidence:
282
+
283
+ ```
284
+ unreviewed-promotion world.demo.01 — every review of this scenario is by an actor that also attested its evidence (`impl-agent`) or from a session that did (`sess-impl`); spec_coverage.require_review.distinct_actor wants a reviewer other than the evidence actor, in a session other than any evidence session
285
+ ```
286
+
287
+ or, when the scenario's spec, its evidence, or both changed after the
288
+ review (`its spec changed`, `its evidence changed`, `its spec and its
289
+ evidence changed`):
290
+
291
+ ```
292
+ unreviewed-promotion world.demo.01 — stale review by `reviewer-2`: its evidence changed (the latest evidence records are not the pinned ones) since the review; a pinned review counts only while what it saw is current — review it again (`canon review add`)
293
+ ```
294
+
295
+ or, when every latest review recorded no sign-off:
296
+
297
+ ```
298
+ unreviewed-promotion world.demo.01 — no signed review: the latest review recorded no sign-off (`reviewer-2` indeterminate); an indeterminate or unavailable review is a gap, never an approval — run the review again and record `--outcome signed`
299
+ ```
300
+
301
+ A non-signed review is a gap even without `distinct_actor`.
302
+
159
303
  ### `open-blocker`
160
304
 
161
305
  With `block_on_findings`, a Finding whose latest version is severity
@@ -188,7 +332,7 @@ prints on stderr which review checks it ran or skipped:
188
332
 
189
333
  ```
190
334
  canon subject status: review guard for building → verifying (spec_coverage.require_review, scope: verifying, shipped)
191
- ran unreviewed-promotion — every owned scenario needs a review by an actor other than its evidence actor
335
+ ran unreviewed-promotion — every owned scenario needs a signed review, current with the spec and evidence it pinned, by an actor, and from a session, other than its evidence's
192
336
  ran open-blocker — 1 adopted change(s) checked for open blocker findings
193
337
  ```
194
338
 
@@ -146,7 +146,10 @@ query:
146
146
  ```
147
147
 
148
148
  Every query, export, and purge appends an actor-stamped record to
149
- `.canon/audit/access.jsonl` (mode `0600` on creation). `CANON_ACTOR` takes
149
+ `.canon/audit/access.jsonl` (mode `0600` on creation) in the repository
150
+ whose `canon.yaml` it read (through `--repo` or `--canon-yaml`), never in
151
+ the current directory; with no `canon.yaml` there is no audit and nothing
152
+ is written. `CANON_ACTOR` takes
150
153
  precedence over the OS username. Audit rows contain operation, kind, scope,
151
154
  sensitive flag, counts, and timestamp only—never query text, secrets, or
152
155
  record bodies.
@@ -135,7 +135,8 @@ what brings a scenario into that scope. See `canon-gate`.
135
135
  requires that the Subject OWNS at least one scenario (one whose latest
136
136
  synced generation carries `@subject:<id>`), and that EVERY owned
137
137
  scenario carries a latest, non-`Divergent` verdict in the ledger (the
138
- same last-wins rule `canon gate check` uses). With
138
+ same last-wins rule `canon gate check` uses; a record keyed by both a
139
+ task and the scenario counts for the scenario). With
139
140
  `spec_coverage.require_cases` set, every feature surface the Subject
140
141
  owns must also carry a scenario of each required `@case:` (e.g. one
141
142
  `@case:failure`) — attested golden-path scenarios alone do not ship. A
@@ -152,7 +153,7 @@ prints on stderr which it ran or skipped:
152
153
 
153
154
  ```
154
155
  canon subject status: review guard for building → verifying (spec_coverage.require_review, scope: verifying, shipped)
155
- ran unreviewed-promotion — every owned scenario needs a review by an actor other than its evidence actor
156
+ ran unreviewed-promotion — every owned scenario needs a review by an actor, and from a session, other than its evidence's
156
157
  ran open-blocker — 1 adopted change(s) checked for open blocker findings
157
158
  ```
158
159
 
@@ -161,7 +162,10 @@ unreviewed-promotion — `specced` is not in require_review.scope`. A
161
162
  transition INTO an in-scope status with gaps — an owned scenario with no
162
163
  qualifying review (`unreviewed-promotion`), or an open blocker finding
163
164
  on an adopted change (`open-blocker`) — prints the violations, exits 1,
164
- leaves the record unchanged, and names `--override-reason`.
165
+ leaves the record unchanged, and names `--override-reason`. `shipped`
166
+ also refuses a `false-success` (a scenario whose latest audit is
167
+ `refuted`) and, when `require_audit.scope` covers `shipped`, an owed
168
+ sampled audit; see `canon-gate`.
165
169
 
166
170
  ```bash
167
171
  canon subject status demo-subject verifying \
@@ -204,7 +208,156 @@ canon subject adopt subject-domain-loop-plan \
204
208
 
205
209
  `adopt` stamps the change's `subject_id` and adds it to the Subject's
206
210
  `change_ids`, so `canon query --kind change --change-id …` and `canon
207
- query --kind subject` agree on the link from both ends.
211
+ query --kind subject` agree on the link from both ends. It writes only
212
+ the side that lacks the link, so rerunning it on a linked pair writes
213
+ nothing and exits `0`.
214
+
215
+ The two records cannot be written atomically, so the subject is written
216
+ first: its `change_ids` is what the gate reads a subject's adopted
217
+ changes from (`open-blocker`, the status guard), so a half-done adoption
218
+ still has that change's open blockers counted. If the change write then
219
+ fails, `adopt` exits `2`, names the subject that now lists the change,
220
+ and prints `canon subject adopt <change_id> --subject <id>` to complete
221
+ it.
222
+
223
+ ### Starting a new change: `canon change new`
224
+
225
+ For a change that does not exist yet, one command scaffolds it and
226
+ records it adopted:
227
+
228
+ ```bash
229
+ canon change new add-login --subject auth --title "Add login"
230
+ ```
231
+
232
+ - **Writes** `openspec/changes/<slug>/proposal.md` (the title is its
233
+ `## Why`, which imports as the change summary) and `tasks.md` (no rows
234
+ yet, so the change imports as `proposed`), under the repo's first
235
+ `openspec` plans source. `canon init` configures that source
236
+ (`{dialect: openspec, root: .}`), so `canon.yaml` needs no edit.
237
+ - **Reads** the files with the same `openspec` adapter `canon ingest
238
+ plans` uses, then **records** the change already adopted, through the
239
+ same write as `canon subject adopt`.
240
+ - **What a failure leaves behind.** The files are staged under `.canon/`
241
+ and the change dir is moved into place only once they parse. A failure
242
+ before any record is written (staging, publishing the dir, or the
243
+ first record write) removes the change dir, the staged files and any
244
+ directory created for them: nothing is left behind. The two records
245
+ cannot be written atomically. If the change record fails after the
246
+ subject record was written, the command exits `2`, keeps the change
247
+ dir and that one subject record, and prints the repair, `canon ingest
248
+ plans && canon subject adopt <slug> --subject <id>`, which completes
249
+ the link and is safe to rerun.
250
+ - **Refuses** with exit `2`, writing nothing, when the subject does not
251
+ exist, when the slug already has a change dir (active or archived) or a
252
+ `change` record, or when `canon.yaml` has no `openspec` plans source.
253
+
254
+ Add task rows to `tasks.md` as `- [ ] <n> <title>`, rerun `canon ingest
255
+ plans`, and flip each with `canon gate task <slug>#<n>` once evidence
256
+ exists.
257
+
258
+ ## Where are we: `canon status`
259
+
260
+ ```bash
261
+ canon status # subjects by status, their gaps, next commands
262
+ canon status --json # the same report, stable shape
263
+ canon status --repo ../other
264
+ ```
265
+
266
+ A read: it always exits 0 and writes nothing (no ledger record, no
267
+ `.canon/audit` line). Every count comes from the joins the gate itself
268
+ uses, so status and the gate cannot disagree about a gap:
269
+
270
+ - **Header** — the canon version, and the policy in force:
271
+ `spec_coverage` (with `scope`/`exclude_lanes`), `require_cases` and
272
+ `require_review`. When `.canon/policy.yaml` is absent, unreadable, has
273
+ no `spec_coverage` section, or has an invalid one, a `warning:` line
274
+ says the gate requires no evidence, failure cases or review (or refuses
275
+ until it is fixed).
276
+ - **Subjects grouped by status** (lifecycle order). Per subject:
277
+ scenarios it owns through `@subject:` (after `exclude_lanes`, which
278
+ are counted separately), how many carry a ledger verdict (the
279
+ `verifying → shipped` gate's reading; divergent ones are counted too),
280
+ how many carry a review that satisfies `require_review` (any current
281
+ signed review when it is off; with `distinct_actor`, a self-review does
282
+ not count), open blocker findings on its adopted changes, and each
283
+ feature surface missing a `require_cases` case. `(N due)` marks reviews
284
+ the next transition will require; `N stale review(s)` and `N unsigned
285
+ review(s)` (printed when non-zero) count scenarios whose review went
286
+ stale or whose latest reviews are all `indeterminate`/`unavailable`
287
+ (see `canon-review`), and the `canon review add` step names both. With
288
+ `require_review` on, a `warning:` line counts the in-effect reviews that
289
+ predate review pinning (0.15).
290
+ - **Unowned** — scenarios with no `@subject:` tag.
291
+ - **Audits** — how many scenarios are sampled for
292
+ `spec_coverage.require_audit`, confirmed, owed (no audit or
293
+ `indeterminate`) and refuted (`false-success`). An owed or
294
+ refuted audit adds a `next:` step suggesting `canon evidence add
295
+ --kind audit …` (recorded from an external evaluator; canon never runs
296
+ one).
297
+ - **Dropped bindings** — `dropped bindings (N):` lists every recorded
298
+ `--drop-attachment` (scenario, paths, reason); `drops[]` in `--json`.
299
+ - **`next:`** — at most five commands, each with a one-line reason; the
300
+ rest are counted. Rules, in order: no `canon.yaml` → `canon init`; an
301
+ unreadable one → `canon init --check-config`; a scenario tagged with a
302
+ subject that has no record → `canon subject new <that id>`; no subject
303
+ at all → `canon subject new`; subjects but no scenario records →
304
+ `canon inventory sync`. Then per subject, verifying first, then
305
+ building, specced, proposed, shipped: no scenario → `canon scenario new
306
+ … --subject <id>`; a missing case → `canon scenario new <surface>.<next
307
+ nn> … --case <case>`; (building and later) an unevidenced or divergent
308
+ scenario → `canon evidence add --scenario-id …`; an open blocker →
309
+ `canon finding close …`; a due, unreviewed, evidenced scenario →
310
+ `canon review add …` from another session; none of these →
311
+ `canon subject status <id> <next status>`. Last, unowned scenarios →
312
+ tag them and `canon inventory sync`. With more than one `specs.roots[]`
313
+ entry, every suggested `canon scenario new` carries `--project`: the
314
+ owning root for a missing case, `<root-id>` (the reason lists the
315
+ roots) for a subject that owns no scenario yet.
316
+
317
+ Status fails closed rather than suggest progress the gate would refuse:
318
+
319
+ - When `scenario`, `subject`, `review` or `finding` routes away from the
320
+ `local` rung (the only rung status and the gate read), its records
321
+ would read as empty. The only `next:` step is
322
+ `canon init --check-config`, with the reason naming the
323
+ `routing.<kind>: local` setting to restore.
324
+ - When `spec_coverage` is present but unusable, `canon gate check`
325
+ refuses. The first `next:` step is `canon gate check`, with the reason
326
+ naming the section to fix, and no subject is suggested to move to its
327
+ next status until it parses.
328
+
329
+ `--json` prints one object:
330
+
331
+ ```json
332
+ {
333
+ "statusVersion": 1,
334
+ "canonVersion": "0.14.0",
335
+ "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 } } },
336
+ "warnings": [],
337
+ "subjects": [
338
+ { "id": "cart", "title": "Cart", "status": "building", "scenarios": 2, "excluded": 0, "evidenced": 2, "divergent": 0,
339
+ "reviewed": 1, "staleReviews": 0, "unsignedReviews": 0, "reviewDue": true, "openBlockers": 1, "missingCases": [] }
340
+ ],
341
+ "unowned": ["cart.promo.01"],
342
+ "next": [
343
+ { "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" },
344
+ { "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; …" },
345
+ { "command": "canon inventory sync", "why": "1 scenario(s) carry no @subject tag (first: cart.promo.01); …" }
346
+ ],
347
+ "nextOmitted": 0
348
+ }
349
+ ```
350
+
351
+ Each `missingCases` entry is `{ "projectId", "surface", "case" }`: the
352
+ spec root, the `<area>.<surface>`, and the required case it lacks.
353
+
354
+ `policy.spec_coverage` has the shape `canon context --json` prints
355
+ (`null` when absent, `{"invalid": …}` when unusable). `statusVersion`
356
+ changes only when a field changes meaning or is removed.
357
+
358
+ `subject new`, `adopt` and `status` write their records directly; the
359
+ human-readable success line ends `— written directly; nothing to
360
+ promote` (`--json` prints the record and nothing else).
208
361
 
209
362
  ## Reading the per-domain management view
210
363
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@journeykit/canon",
3
- "version": "0.13.1",
3
+ "version": "0.15.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.13.1",
34
- "@journeykit/canon-core-linux-x64": "0.13.1",
35
- "@journeykit/canon-core-win32-x64": "0.13.1"
33
+ "@journeykit/canon-core-darwin-arm64": "0.15.0",
34
+ "@journeykit/canon-core-linux-x64": "0.15.0",
35
+ "@journeykit/canon-core-win32-x64": "0.15.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^20.0.0",