@journeykit/canon 0.17.0 → 0.21.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.
@@ -28,7 +28,7 @@ Every violation carries one of these stable, grep-able strings:
28
28
  | `malformed-evidence` | A candidate record doesn't parse, is misfiled, or carries an unparseable interim tag. Malformed evidence is no evidence. |
29
29
  | `flagged` | The human-only `flagged` overlay is set — never green regardless of passing evidence. |
30
30
  | `unevidenced-flip` | `canon gate task <task_id>` was asked to flip a checkbox with no matching, non-`Divergent` evidence record. |
31
- | `fabricated-evidence` | An evidence note contains a blocklisted marker (`"would pass"`, `"TBD"`, `"n/a"`) or a bare `verified` claim with no attached command result. |
31
+ | `fabricated-evidence` | An evidence note contains a blocklisted marker (`"would pass"`, `"TBD"`, `"n/a"`) standing as a whole word — never inside a path, a file name, a URL or a longer word, so `pack.json/atlas` is not `n/a` — or a bare `verified` claim with no attached command result. `canon evidence add` refuses such a note before writing (`canon evidence add: refused — fabricated-evidence …`, exit 1). |
32
32
  | `open-blocker` | With `spec_coverage.require_review` set (and `block_on_findings`, the default), a Finding whose latest version is severity `blocker`, disposition `open`, on a change an in-scope subject adopted. Subject string `<change_id>#<round>.<seq>`. See `canon-review`. |
33
33
  | `false-success` | The latest audit record (`canon evidence add --kind audit`) of a scenario says `refuted`: an external evaluator found a counterexample to the claimed success. Always on, regardless of policy or sampling, and takes precedence over any declared faithful verdict; only a newer audit (`confirmed` or `indeterminate`) clears it. The detail names the evaluator, its version, its sha256 and the counterexample paths. |
34
34
 
@@ -142,7 +142,7 @@ A present section, even `require_evidence: false` or a malformed one
142
142
  case). This is the gap evidence presence cannot see: every scenario
143
143
  attested, none of them specifying a refusal. Untagged scenarios satisfy
144
144
  no case, so an unclassified corpus is reported, not passed. Works with
145
- `require_evidence: false` too. `canon subject status <id> shipped`
145
+ `require_evidence: false` too. `canon subject status <id> --to shipped`
146
146
  applies the same rule to the scenarios the subject owns.
147
147
  - **Independent review** (`require_review`) — scenarios whose subject is
148
148
  in `require_review.scope` (default `[verifying, shipped]`; `[]` = every
@@ -228,25 +228,38 @@ canon evidence add --kind audit --project-id root --scenario-id cart.add.01 \
228
228
  - Audits fold apart: per `(project_id, scenario_id)` there is a latest
229
229
  declared record and a latest audit. An audit never becomes the
230
230
  scenario's latest declared verdict.
231
- - `canon subject status <id> shipped` refuses `false-success`, and an
231
+ - `canon subject status <id> --to shipped` refuses `false-success`, and an
232
232
  owed sampled audit when `require_audit.scope` covers `shipped`.
233
233
  - `canon status` shows an `audits` section (sampled / confirmed / owed /
234
234
  refuted) and suggests `canon evidence add --kind audit …` in `next:`.
235
235
 
236
- ### Dropping a binding (`--drop-attachment`)
237
-
238
- `canon evidence add` for a scenario refuses (exit `2`) when the
239
- scenario's current latest record of the same fold (declared records, or
240
- audit records for an audit) — committed ledger plus `_staging` — binds
241
- files the new record does not. The refusal names the paths and two ways
242
- out: re-bind them (`--artifact`/`--report`; new bytes at the same path
243
- are fine), or drop them on purpose with `--drop-attachment <path>`
244
- (repeatable) and one single-line `--drop-reason <text>`. The drop is
245
- recorded as `attachment_drop: {paths, reason}`. Refused (`2`): a drop
246
- without a reason, a reason without a drop, a drop path that is not
247
- actually dropped, a multi-line reason, or a drop without
248
- `--scenario-id` (task-only evidence has no drop guard). `canon status`
249
- lists recorded drops (`dropped bindings (N):`; `drops[]` in `--json`).
236
+ ### Carrying and dropping bindings (`--drop-attachment`)
237
+
238
+ `canon evidence add` for a scenario carries forward what the scenario's
239
+ current latest record of the same fold (declared records, or audit
240
+ records for an audit; committed ledger plus `_staging`) bound: every
241
+ file the new write does not name again is bound into the new record
242
+ unchanged (same path, digest and stored blob), and `--json` lists it as
243
+ `carried: [paths]`. A file named again (`--artifact`/`--report`, new
244
+ bytes at the same path included) replaces its earlier binding. A suite
245
+ batch (`--all-scenarios`) therefore never costs a scenario its
246
+ screenshots. A carried file whose blob the artifact store does not
247
+ hold intact is refused (exit `2`, `EVIDENCE_CARRY_BLOB_MISSING`, nothing
248
+ written): re-bind it, run `canon evidence vault`, or drop it. The audit
249
+ fold (`--kind audit`) does not carry: an audit that leaves out a file
250
+ the latest audit bound is refused (exit `2`) unless it names the file
251
+ again or drops it with `--drop-attachment`, so a counterexample never
252
+ rides along into a confirming audit.
253
+
254
+ The only way to lose a binding is `--drop-attachment <path>`
255
+ (repeatable) with one single-line `--drop-reason <text>`, recorded as
256
+ `attachment_drop: {paths, reason}`. With `--all-scenarios` the drop
257
+ applies to each scenario that binds the path. Refused (`2`): a drop
258
+ without a reason, a reason without a drop, a drop path that no
259
+ scenario of the write leaves out, a multi-line reason, or a drop
260
+ without `--scenario-id`/`--all-scenarios` (task-only evidence has no
261
+ carry or drop). `canon status` lists recorded drops (`dropped bindings
262
+ (N):`; `drops[]` in `--json`).
250
263
 
251
264
  ### Nothing is attested before the brief is approved (`--override-reason`)
252
265
 
@@ -256,7 +269,8 @@ reports it, and builds nothing for that scope until the human answers.
256
269
  refuses (exit `1`, nothing staged or stored) while a subject the record
257
270
  attests for has an enforced, unapproved brief: the newest adopted
258
271
  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
272
+ line, was edited after its approval (`BRIEF_CHANGED_SINCE_APPROVAL`), or
273
+ there is no canon `brief.md` yet but the change comes from an
260
274
  external openspec plan with a `proposal.md` under the plan sources (the
261
275
  check `canon status` and the ship guard use; see
262
276
  `canon-subject`). The subjects are the scenario's `@subject` owner for
@@ -264,8 +278,8 @@ check `canon status` and the ship guard use; see
264
278
  `--task`:
265
279
 
266
280
  ```
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
281
+ 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 with `canon brief approve --change c-demo --by <the human> --note "<their words>"`
282
+ 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 answer with `canon brief approve --by <the human> --note "<their words>"` only from that answer; to attest anyway, pass --override-reason <text>, which is recorded on the evidence record
269
283
  ```
270
284
 
271
285
  For a change from an external plan with no canon brief yet, the first
@@ -274,8 +288,8 @@ change `c-ext` comes from the external plan
274
288
  openspec/changes/c-ext/proposal.md, which canon reads and never edits, so
275
289
  its approval lives in that canon file, so the human has not answered it;
276
290
  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
291
+ openspec/changes/c-ext/proposal.md` line, then record their words with
292
+ `canon brief approve --change c-ext --by <the human> --note "<their words>"` ``. An `Approved:` line in the
279
293
  external `proposal.md` itself is never read.
280
294
 
281
295
  A subject with no brief to answer (no adopted change, or a newest change
@@ -305,7 +319,7 @@ brief overrides: 1 evidence record(s) attested before their brief was approved
305
319
 
306
320
  ### Review-waiver advisories
307
321
 
308
- A subject moved into a review-scoped status with `canon subject status
322
+ A subject moved into a review-scoped status with `canon subject status <id> --to <state>
309
323
  --override-reason` (see `canon-subject`) keeps the `unreviewed-promotion`
310
324
  and `open-blocker` gaps its waiver recorded visible without failing the
311
325
  gate. Only those exact `(class, subject)` pairs are waived; a gap that
@@ -319,15 +333,18 @@ review waivers: 1 advisory(ies) — not failing the gate:
319
333
 
320
334
  The waiver lasts only until the subject's next status write.
321
335
 
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;
336
+ `canon subject status <id> --to shipped` also refuses where a brief exists and
337
+ is not approved (the subject's newest adopted change has a canon
338
+ `brief.md` with no `Approved:` line or one the brief's text no longer
339
+ matches, or an external `proposal.md` and no canon `brief.md` yet;
325
340
  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.
341
+ the subject, and while `canon brief check` has structure blockers or a
342
+ flow step has no scenario or no failure scenario (the coverage rules gate
343
+ shipping, never approval). With no brief to answer it prints an advisory
344
+ and does not refuse. Like the evidence refusal above, it is a backstop:
345
+ the stop is at the brief. `canon gate check` does not grade briefs, so a
346
+ waived ship brief prints no advisory here; `canon status` keeps showing
347
+ the brief as not approved.
331
348
 
332
349
  ### Reviews that predate pinning (checked against their pin)
333
350
 
@@ -391,6 +408,8 @@ canon evidence add --scenario-id cart.add.04 --project-id root \
391
408
  # a JUnit XML or Cucumber JSON report: also records the matched case + outcome
392
409
  canon evidence add ... --report junit:target/nextest/default/junit.xml
393
410
  canon evidence add ... --report cucumber:reports/cucumber.json
411
+ # a Swift Testing event stream (swift test --experimental-event-stream-output <path>)
412
+ canon evidence add ... --report swift-testing:.build/events.jsonl
394
413
  ```
395
414
 
396
415
  A case is bound when the scenario id is its OWN: the first
@@ -408,6 +427,12 @@ verdict when ANY bound case failed or was skipped refuses (exit 1). Files
408
427
  must be inside the repository, are recorded by relative path, and are
409
428
  stored (see Bound evidence stays provable).
410
429
 
430
+ A Swift Testing case is named by its display name (`@Test("cart.add.04
431
+ refuses an out-of-stock item")`), else its function name
432
+ (`cart_add_04_refuses()`). A test with a failing issue failed, a skipped
433
+ test was skipped, a test that started and ended without one passed, and
434
+ a test that started and never ended failed.
435
+
411
436
  Strength, weakest first: `attested` (no attachment) < `artifact` (any
412
437
  bound file) < `report` (a parsed report whose case passed). The policy
413
438
  decides whether the gate cares:
@@ -431,6 +456,56 @@ violation in every mode. Canon still cannot know whether the bound test
431
456
  actually exercises the scenario — binding narrows the trust gap, it does
432
457
  not close it.
433
458
 
459
+ ### One report, many scenarios (0.18)
460
+
461
+ One test run proves many scenarios. Bind it once:
462
+
463
+ ```bash
464
+ canon evidence add --project-id root --all-scenarios \
465
+ --report junit:target/nextest/default/junit.xml \
466
+ --kind test-run --ref "cargo nextest run" --role implementer
467
+ ```
468
+
469
+ Each indexed scenario of `--project-id` that owns a case (the rule above)
470
+ gets its own record, with only its own cases. Its verdict comes from
471
+ them: `faithful` when every case passed, `divergent` when one failed,
472
+ `not-applicable` when one was skipped and none failed. `--verdict <v>`
473
+ claims the same verdict for every record instead. A case whose id is not
474
+ indexed is named and skipped (`skipped <id> — not an indexed scenario`);
475
+ a report no indexed scenario owns is refused (exit 2). The records share
476
+ the stored report and every `--artifact`, and the output lists each
477
+ scenario with its verdict on its own line.
478
+
479
+ It is all or nothing: a claimed `faithful` verdict over a failed case,
480
+ a fabrication marker in `--summary`, an unapproved brief for any
481
+ scenario's subject, or a carried attachment whose blob is missing from the
482
+ store refuses the whole batch, and nothing is written. Each scenario's
483
+ earlier attachments are carried into its new record (see above), and
484
+ `--drop-attachment` applies per scenario. `--scenario-id`, `--task`,
485
+ `--report-case`, `--kind audit` and the approval flags name one subject
486
+ and are refused with `--all-scenarios`.
487
+
488
+ A task's evidence with `--report` and no `--scenario-id` binds EVERY case
489
+ in the report, so a `faithful` verdict needs all of them to pass.
490
+
491
+ ### A write that makes a review stale says so (0.18)
492
+
493
+ When new evidence (one record or a batch) supersedes the evidence a
494
+ current pinned review saw, `canon evidence add` prints one line per
495
+ review it makes stale, with the command that re-pins it:
496
+
497
+ ```
498
+ canon evidence add: review by `reviewer-2` of cart.add.01 is now stale (it pinned the evidence this record supersedes); re-pin it: canon review add --project-id root --scenario-id cart.add.01 --reviewer reviewer-2 --actor-id reviewer-2 --role reviewer --pin <HEAD sha> --original-spec-ref specs/kind=feature/area=cart/add.feature
499
+ ```
500
+
501
+ The decision is the gate's own (the same stale-review rule `canon gate
502
+ check` reports). The command repeats the review's reviewer, actor, role,
503
+ session, provenance ref, runtime labels and artifacts, pinned at `HEAD` (`<sha>`
504
+ when the repository has no commit). A record that is only staged says
505
+ the review "goes stale once this evidence is promoted": promote first,
506
+ then re-pin. The reviewer re-reviews before re-pinning; several
507
+ scenarios re-pin in one `canon review add` (see `canon-review`).
508
+
434
509
  ## Bound evidence stays provable (the artifact store)
435
510
 
436
511
  `canon evidence add --artifact/--report` copies each bound file into
@@ -468,20 +543,29 @@ have its bindings checked. A superseded record is not checked —
468
543
  re-attesting is the fix.
469
544
 
470
545
  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.
546
+ `canon-review`) are checked by the same rule for every CURRENT in-effect
547
+ review: the latest per `(project_id, scenario_id, reviewer)`, less any
548
+ that went stale against its scenario (its spec or evidence pin moved, or
549
+ a review from before pinning is stale at its commit). A stale review is
550
+ superseded — the gate reads the newer review, or reports the missing
551
+ one as `unreviewed-promotion` — so its files are not re-checked, as a
552
+ superseded evidence record's are not. A missing blob whose working-tree
553
+ file still matches is an advisory, listed with the reviewer:
554
+
555
+ ```
556
+ unstored cart.add.01 reports/cart-badge.png (sha256 9848…, review by `reviewer-2`) is verified against the working tree only
557
+ ```
558
+
559
+ Anything else is `stale-evidence` on the review's scenario, and the
560
+ detail names the review: ``review by `<reviewer>` (actor `<id>`,
561
+ <time>)``.
478
562
 
479
563
  `canon evidence vault [--max-artifact-mib <N>]` stores the bytes for
480
- existing records: every attachment whose working-tree file still matches
481
- its digest is copied in, and everything else is listed as not stored,
482
- with both digests. It exits `0` whenever it ran (the gate decides whether
483
- an unstored file matters), `2` when the ledger cannot be read or a write
484
- fails.
564
+ existing evidence records and current reviews (a superseded review's files are not vaulted): every attachment whose
565
+ working-tree file still matches its digest is copied in, and everything
566
+ else is listed as not stored, with both digests. It exits `0` whenever it
567
+ ran (the gate decides whether an unstored file matters), `2` when the
568
+ ledger cannot be read or a write fails.
485
569
 
486
570
  ## Effect-aware risk approvals (`risk_tiers`)
487
571
 
@@ -514,6 +598,11 @@ empty clean map.
514
598
 
515
599
  ## `canon gate task <task_id> [--repo <dir>]`
516
600
 
601
+ For imported external plans, and for a `tasks.md` that already exists in
602
+ a canon change. `canon change new` writes no `tasks.md` (0.19), and
603
+ nothing gates on one: a canon-owned change is covered by its flow steps'
604
+ scenarios (`canon status`).
605
+
517
606
  The evidence-gated task checkbox flip. Resolves `<task_id>`
518
607
  (`<change_id>#<n>`) through `.canon/changes/` (canon's own changes, read
519
608
  first whenever the directory exists, with no `plans:` entry) and then
@@ -595,7 +684,7 @@ promote`, so no separate
595
684
  staged. A record another run staged is not touched: only `gate promote`
596
685
  commits everything staged. It stays safe to run anyway: with nothing
597
686
  staged it prints
598
- `nothing staged` and exits `0`. `divergence stage` stages for `canon
687
+ `nothing staged` and exits `0`. `divergence add` stages for `canon
599
688
  divergence promote` and names it, under the same `write:` rule. Every
600
689
  other write — `review add`, `divergence
601
690
  resolve|defer`, `subject new|adopt|status`, `change new`, `scenario new`, `feature new`,
@@ -72,7 +72,7 @@ scenario index # refreshed by every reader; `canon inventory sy
72
72
  │
73
73
  ▼
74
74
  canon review add # attests a review — see canon-inventory downstream / canon-gate
75
- canon divergence {stage,promote,resolve,defer,status} # tracks/resolves a divergence
75
+ canon divergence {add,promote,resolve,defer,status} # tracks/resolves a divergence
76
76
  canon gate check [--release] # trust-ladder / staleness / coverage — see canon-gate
77
77
  │
78
78
  ▼
@@ -158,7 +158,7 @@ specs:
158
158
 
159
159
  - **Review** — `canon review add` attests a verdict against a synced
160
160
  scenario. (See `canon-inventory` downstream commands and each command's `--help`.)
161
- - **Divergence** — `canon divergence {stage,promote,resolve,defer,status}`
161
+ - **Divergence** — `canon divergence {add,promote,resolve,defer,status}`
162
162
  tracks/resolves divergences against synced scenarios.
163
163
  - **Gate** — `canon gate check` enforces trust-ladder / staleness /
164
164
  coverage over the corpus (see `canon-gate`).
@@ -46,15 +46,13 @@ The production activation path is a signed, paired-evaluation workflow:
46
46
  ```bash
47
47
  canon learn approve <strategy_id> --evaluation evaluation.json \
48
48
  --principal alice --repo .
49
- # `request` is an exact alias:
50
- canon learn request <strategy_id> --evaluation evaluation.json \
51
- --principal alice --repo .
52
49
  ```
53
50
 
54
- The JSON output contains the evaluation digest, RFC3339 `approved_at`,
51
+ With `--json` the payload is `.data` of the `{ "data", "meta" }` envelope, and
52
+ `meta.next` names the human signing step. The `data` contains the evaluation digest, RFC3339 `approved_at`,
55
53
  subject `strategy:<strategy_id>`, namespace
56
- `canon-learning-approval-v1`, and `payload_hex`. `approve`/`request` never
57
- read a private key and never set `verified`.
54
+ `canon-learning-approval-v1`, and `payload_hex`. `approve` never
55
+ reads a private key and never sets `verified`.
58
56
  6. Decode `payload_hex` to the exact canonical JSON bytes. The signed object
59
57
  is:
60
58
 
@@ -10,8 +10,9 @@ one, and neither can `canon gate`.
10
10
  ## 1. Canon's own changes and external `plans:` sources
11
11
 
12
12
  `.canon/changes/` is canon's native plan source (dialect id `canon`):
13
- one `<slug>/` directory per change holding `brief.md` and `tasks.md`,
14
- written by `canon change new` (see `canon-subject`). Every plan reader —
13
+ one `<slug>/` directory per change holding `brief.md` (and a `tasks.md`
14
+ only when someone adds one: an optional work note), written by
15
+ `canon change new` (see `canon-subject`). Every plan reader —
15
16
  `canon ingest plans`, `canon gate task`, `canon evidence add --task`,
16
17
  `canon dispatch --task`, `canon status` and the brief check — reads it
17
18
  first whenever the directory exists, with no `plans:` entry. Being first,
@@ -21,7 +22,8 @@ Its records carry the actor `canon-plan-import-canon`; an external
21
22
  openspec change's carry `canon-plan-import-openspec`. A `brief.md` with a
22
23
  `Source: <path>` line is not a change of its own: it is the canon brief
23
24
  of a change imported from an external plan (it points at the external
24
- document and holds the human's `Approved:` line), so the native reader
25
+ document and holds the human's `Approved:` line, written by
26
+ `canon brief approve`), so the native reader
25
27
  skips that directory and the external source keeps the change id.
26
28
 
27
29
  `plans.sources` lists only EXTERNAL plans — documents you prepare outside
@@ -89,12 +91,13 @@ No dialect/plugin filter exists or is needed — an imported `change`/
89
91
  ## The `canon` dialect (`.canon/changes/`)
90
92
 
91
93
  Uses the `openspec` change-dir grammar below, with three differences:
92
- the brief is `brief.md` (its `## Why` first paragraph is the summary; a
94
+ the brief is `brief.md` (the first paragraph of its intent section, or of
95
+ `## Why` in a pre-0.19 brief, is the summary; a
93
96
  missing one is a `missing-brief-md` malformed dir), the source root is
94
97
  always `.canon/changes/` itself (archived changes under
95
98
  `.canon/changes/archive/`), and a `brief.md` carrying a `Source:` line is
96
- skipped (a pointer brief, above). `tasks.md` uses the same checkbox row
97
- grammar.
99
+ skipped (a pointer brief, above). An existing `tasks.md` uses the same
100
+ checkbox row grammar; a change with none imports with zero tasks.
98
101
 
99
102
  ## The `openspec` dialect
100
103
 
@@ -175,7 +178,7 @@ downstream reader's job.
175
178
  ## What this skill does NOT cover
176
179
 
177
180
  - **Authoring a change** — `canon change new` writes
178
- `.canon/changes/<slug>/brief.md` and `tasks.md` and adopts the change
181
+ `.canon/changes/<slug>/brief.md` (no `tasks.md`) and adopts the change
179
182
  into a subject (see `canon-subject`); it never scaffolds an openspec
180
183
  change dir. This skill covers IMPORTING plans.
181
184
  - **`canon gate check`/`canon gate task`** (the hand-authored one-task-at-
@@ -74,8 +74,10 @@ canon policy approve --by <who> [--note <text>] [--repo <dir>]
74
74
 
75
75
  A human approves the policy after reviewing `.canon/policy.yaml`, in the
76
76
  same answer that approves a subject's brief (0.16; see the skill's working
77
- loop). The human runs this, or the agent runs it right after that answer
78
- as `canon policy approve --by <the human> --note "<their words>"`. It
77
+ loop). `canon brief approve` records both in one call, so the agent does
78
+ not run this command next to it. Run `canon policy approve` alone for a
79
+ policy-only change, as the human, or as the agent right after that answer
80
+ (`canon policy approve --by <the human> --note "<their words>"`). It
79
81
  writes `.canon/policy.approval.json` directly (nothing to promote):
80
82
 
81
83
  ```json
@@ -227,7 +229,7 @@ read as a big-endian u64, are less than `rate · 2^64` — computed exactly,
227
229
  as a comparison against `ceil(rate · 2^64)`, so `rate: 0` samples nothing
228
230
  and `rate: 1` samples everything. A sampled in-scope scenario whose
229
231
  latest audit is missing or `indeterminate` is `uncovered-cell`; with
230
- `shipped` in `scope`, `canon subject status <id> shipped` also refuses
232
+ `shipped` in `scope`, `canon subject status <id> --to shipped` also refuses
231
233
  it. `canon context` and the `canon status` policy line show
232
234
  `require_audit=(rate=… seed=`…` scope=…)`.
233
235
 
@@ -55,7 +55,7 @@ The order an agent follows to claim a subject done:
55
55
  the screenshots or recordings it looked at with `--artifact <path>`
56
56
  (below).
57
57
 
58
- 4. **Move the subject**: `canon subject status <id> verifying`, later
58
+ 4. **Move the subject**: `canon subject status <id> --to verifying`, later
59
59
  `shipped`. See `canon-subject`.
60
60
 
61
61
  ## `canon finding add` / `canon finding close`
@@ -135,7 +135,8 @@ fixed finding reopened with `--disposition open` counts as open again.
135
135
  ## `canon review add`
136
136
 
137
137
  ```bash
138
- canon review add --project-id <root-id> --scenario-id <id> \
138
+ canon review add --project-id <root-id> --scenario-id <id> [--scenario-id <id>]... \
139
+ [--scenario-ids-from <file|->] \
139
140
  --reviewer <who> --pin <sha-or-ref> --role <role> \
140
141
  (--upstream-ref <ref> | --original-spec-ref <ref>) [--actor-id <id>] [--session-id <id>] \
141
142
  [--outcome signed|indeterminate|unavailable] \
@@ -143,7 +144,7 @@ canon review add --project-id <root-id> --scenario-id <id> \
143
144
  [--artifact <path>]... [--max-artifact-mib <MiB>]
144
145
  ```
145
146
 
146
- One attributed Review record for one scenario, joined on
147
+ One attributed Review record per scenario, joined on
147
148
  `(project_id, scenario_id)` — both are required because two spec roots
148
149
  may carry the same scenario id. Exactly one provenance ref is required.
149
150
  `--actor-id` defaults to `canon`; `--session-id` fills `actor.session_id`.
@@ -153,6 +154,18 @@ own. It is written directly to the ledger, with no staging step, and says
153
154
  so: `canon review add: wrote <path> (new) — written directly; nothing to
154
155
  promote`.
155
156
 
157
+ ### Several scenarios in one call (0.18)
158
+
159
+ `--scenario-id` is repeatable, and `--scenario-ids-from <file>` (`-` reads
160
+ stdin) adds one id per line; `#` starts a comment and blank lines are
161
+ skipped. One reviewer, pin, provenance ref and set of artifacts cover
162
+ every review; each review pins its own scenario and evidence, and each
163
+ gets its own `pinned` and `wrote` line. The artifacts are stored once.
164
+ It is all or nothing: a scenario with no record, or a line in the file
165
+ that is not a scenario id, refuses the call (exit 2) and writes no review.
166
+ This is also the batch re-pin after `canon evidence add` reports reviews
167
+ it made stale (see `canon-gate`).
168
+
156
169
  ### What the reviewer looked at (artifacts)
157
170
 
158
171
  `--artifact <path>` (repeatable) binds a file the reviewer looked at — a
@@ -173,19 +186,25 @@ The field is absent when no `--artifact` is given. `review add` prints
173
186
  .canon/artifacts/sha256/ (<k> newly stored) — commit them with the
174
187
  ledger`.
175
188
 
176
- `canon gate check` re-hashes the stored blob of every file bound to an
177
- in-effect review (the latest per `(project_id, scenario_id, reviewer)`).
178
- A missing or altered blob is `stale-evidence` on the review's scenario,
179
- naming the review, even when the working-tree file still matches (see
180
- `canon-gate`, "Bound evidence stays provable"):
189
+ `canon gate check` re-checks every file bound to a current in-effect
190
+ review (the latest per `(project_id, scenario_id, reviewer)`, unless it
191
+ went stale against its scenario) by the evidence rule (see `canon-gate`,
192
+ "Bound evidence stays provable"). A missing blob whose working-tree file
193
+ still matches is an advisory naming `canon evidence vault`, which stores
194
+ it back. A missing blob with a changed or missing working-tree file, or
195
+ an altered blob, is `stale-evidence` on the review's scenario, naming
196
+ the review:
181
197
 
182
198
  ```
183
- stale-evidence world.demo.01 — review by `reviewer-2` (actor `reviewer-2`, 2026-10-11T09:00:00Z) bound file `reports/title.png` recorded sha256 <hex>; stored blob .canon/artifacts/sha256/<hex>: missing; working tree: <hex> — the bound bytes are gone, look again and re-record the review with `canon review add --artifact <path>`
199
+ stale-evidence world.demo.01 — review by `reviewer-2` (actor `reviewer-2`, 2026-10-11T09:00:00Z) bound file `reports/title.png` recorded sha256 <hex>; stored blob .canon/artifacts/sha256/<hex>: missing; working tree: <other hex> — the bound bytes are gone, look again and re-record the review with `canon review add --artifact <path>`
184
200
  ```
185
201
 
186
202
  The reviewer's later review replaces the earlier one, so its files are
187
- the ones checked. Canon binds and re-checks bytes only; it never compares
188
- images or judges what a screenshot shows.
203
+ the ones checked. A review that went stale (its scenario's spec or
204
+ evidence moved since) is not checked either: it no longer counts, and the
205
+ scenario's newer review is the one whose files are checked. Canon binds
206
+ and re-checks bytes only; it never compares images or judges what a
207
+ screenshot shows.
189
208
 
190
209
  ### What the review saw (pins)
191
210
 
@@ -394,7 +413,7 @@ review waivers: 1 advisory(ies) — not failing the gate:
394
413
 
395
414
  ## The status guard
396
415
 
397
- With `require_review` present, `canon subject status <id> <state>`
416
+ With `require_review` present, `canon subject status <id> --to <state>`
398
417
  prints on stderr which review checks it ran or skipped:
399
418
 
400
419
  ```
@@ -98,17 +98,23 @@ canon query --kind handoff --since 2026-06-01T00:00:00Z
98
98
 
99
99
  # Machine-readable output instead of the human table.
100
100
  canon query --kind trajectory --json
101
+
102
+ # Page a long result and keep only the fields you need; `meta` carries
103
+ # returned, total, hasMore and nextCursor (`--all` returns everything).
104
+ canon query --kind trajectory --json --limit 50 --fields at,kind
105
+ canon query --kind trajectory --json --limit 50 --cursor <nextCursor>
101
106
  ```
102
107
 
103
108
  ## What `canon report` does and doesn't read
104
109
 
105
110
  canon exposes layered `stg_`/`int_`/`mart_` query views; `canon report`
106
111
  builds its numbers from the `mart_` views. Those read ONLY local roots
107
- — the git ledger and a LOCAL `.canon/r2` parquet mirror. `canon report`
112
+ — the git ledger, the sqlite `hot` db file (local, no credential; read
113
+ since 0.18) and a LOCAL `.canon/r2` parquet mirror. `canon report`
108
114
  never opens a live DB connection or the live bucket, so:
109
115
 
110
- - Kinds routed to `hot` (sqlite/postgres) do NOT appear in `canon
111
- report`.
116
+ - Kinds routed to a postgres `hot` rung do NOT appear in `canon
117
+ report`; kinds routed to `canon init`'s default sqlite `hot` rung do.
112
118
  - Kinds routed to `cold` (s3) appear only if a local `.canon/r2` mirror
113
119
  has been separately materialized — there is no automatic sync.
114
120
 
@@ -119,8 +125,11 @@ section listing them and emits a matching stderr `WARN`.
119
125
 
120
126
  ## Retention, export, and access audit
121
127
 
122
- `canon purge --kind <kind> --before <RFC3339> [--repo <dir>] [--dry-run] [--json]`
123
- is the canonical data-lifecycle boundary. The only purgeable kinds are
128
+ `canon purge --kind <kind> --before <RFC3339> (--plan | --confirm <planDigest>) [--repo <dir>] [--json]`
129
+ is the canonical data-lifecycle boundary: `--plan` prints the removal set and
130
+ its `planDigest` and deletes nothing; `--confirm <planDigest>` deletes only if
131
+ the plan recomputed now has the same digest (`PLAN_CHANGED`, exit 2, when the
132
+ data moved), and a bare `purge` is refused (`PURGE_PLAN_REQUIRED`). The only purgeable kinds are
124
133
  `session`, `run`, `event`, and `trajectory`; the cutoff applies to every
125
134
  routed rung and every configured aging destination, including retained
126
135
  historical versions. For `trajectory`, the command also purges the
@@ -133,8 +142,14 @@ an explicit `partial` result with matched/deleted counts and a nonzero
133
142
  status. Purge never rewrites Git history or deletes source transcript
134
143
  files.
135
144
 
145
+ With `--json`, `purge` and `export` answer `{ "data", "meta" }` like every other
146
+ command: `data` is the plan or manifest, `meta.next` the step after (a plan's is the
147
+ human's review and `--confirm`), and an export's page state (`total`, `returned`,
148
+ `nextCursor`) sits in `meta`. `export --json --out <file>` writes the manifest to the
149
+ file and answers with `data: { out, kind, recordCount }`.
150
+
136
151
  `canon export --kind <kind> [--before <RFC3339>] [--after <RFC3339>]
137
- [--out <file>] [--json]` emits a versioned manifest containing source scope,
152
+ [--out <file>] [--json] [--limit <n> | --all] [--cursor <c>]` emits a versioned manifest containing source scope,
138
153
  counts, digests, and redacted metadata. It does not authorize backend access;
139
154
  credentials remain an operator prerequisite. New output files are mode
140
155
  `0600`. Raw event detail text is excluded unless `--include-sensitive` is
@@ -160,7 +175,7 @@ record bodies.
160
175
  Four writes stage their record instead of committing it: `canon evidence
161
176
  add`, `canon finding add` and `canon finding close` (under
162
177
  `.canon/ledger/_staging/`, committed by `canon gate promote`) and `canon
163
- divergence stage` (committed by `canon divergence promote`). `canon.yaml`'s
178
+ divergence add` (committed by `canon divergence promote`). `canon.yaml`'s
164
179
  `write:` section decides whether they promote in the same run:
165
180
 
166
181
  ```yaml