@journeykit/canon 0.15.0 → 0.16.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.
- package/dist/skills/SKILL.src.md +82 -30
- package/dist/skills/reference/canon-authoring.md +10 -3
- package/dist/skills/reference/canon-gate.md +135 -10
- package/dist/skills/reference/canon-inventory.md +35 -8
- package/dist/skills/reference/canon-policy.md +38 -11
- package/dist/skills/reference/canon-review.md +82 -15
- package/dist/skills/reference/canon-storage.md +121 -1
- package/dist/skills/reference/canon-subject.md +152 -40
- package/package.json +4 -4
package/dist/skills/SKILL.src.md
CHANGED
|
@@ -13,31 +13,60 @@ where each subject stands and lists the next commands.
|
|
|
13
13
|
## The working loop
|
|
14
14
|
|
|
15
15
|
Follow these steps in order for every request that builds, changes or fixes
|
|
16
|
-
a feature, a one-line request included.
|
|
17
|
-
|
|
18
|
-
- **The
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
a feature, a one-line request included. A human is needed once per scope:
|
|
17
|
+
|
|
18
|
+
- **The approval** (step 1). A new subject, or a change to an approved
|
|
19
|
+
subject's scope, needs the human to approve its brief: the request, your
|
|
20
|
+
assumptions, your open questions. Write the brief, then stop and report
|
|
21
|
+
it: build nothing for that scope (no scenarios, code or evidence) before
|
|
22
|
+
the human answers. The approval is the one thing you never assume, even
|
|
23
|
+
when the human said they cannot answer until you report: the brief is
|
|
24
|
+
that report. In the same answer the human approves the policy,
|
|
25
|
+
`.canon/policy.yaml` (written by `canon init`), which is what the gate
|
|
26
|
+
grades the work against. When the human answers, record both at once:
|
|
27
|
+
an `Approved: <who> <when> — "<the human's words>"` line in the change's
|
|
28
|
+
`proposal.md`, and
|
|
29
|
+
`canon policy approve --by <the human> --note "<their words>"`. Never
|
|
30
|
+
approve without a human answer, and never edit the policy to make the
|
|
31
|
+
gate pass.
|
|
32
|
+
- **Inside an approved scope, never stop for a human.** Fixes, follow-ups,
|
|
33
|
+
review rounds and re-reviews go straight on: add or extend the scenario,
|
|
34
|
+
implement, gather evidence, and get an independent review in another
|
|
35
|
+
session. Independent review happens on every change. A fix or follow-up
|
|
36
|
+
stays in the approved change: add it as tasks in that change's
|
|
37
|
+
`tasks.md`, never with `canon change new`, because a new change is a new
|
|
38
|
+
scope and needs the human's approval.
|
|
39
|
+
|
|
40
|
+
When no human can answer one of the brief's questions, write the question
|
|
41
|
+
into the brief, state the assumption you will build on, and say so in your
|
|
42
|
+
report. Proceeding on an assumption applies to the brief's questions and to
|
|
43
|
+
work inside an approved scope, never to the approval itself.
|
|
44
|
+
|
|
45
|
+
1. **Brief.** For a new subject or a scope change, write down the request
|
|
46
|
+
verbatim, your assumptions, and the questions you could not ask. It
|
|
47
|
+
becomes the change's `openspec/changes/<slug>/proposal.md`, which
|
|
30
48
|
`canon change new <slug> --subject <id> --title "…"` scaffolds once the
|
|
31
|
-
subject exists (step 2).
|
|
49
|
+
subject exists (step 2). Once it is there, stop and report the brief,
|
|
50
|
+
asking the human once for the brief and the policy; go on to step 3
|
|
51
|
+
only after the human answers, and record the answer as above. Inside an
|
|
52
|
+
approved scope there is no new brief: go to step 3 under the existing
|
|
53
|
+
subject and change.
|
|
32
54
|
2. **Subject.** `canon subject new <id> --domain <domain> --title "…"`, then
|
|
33
55
|
`canon change new <slug> --subject <id> --title "…"`, and put the brief in
|
|
34
|
-
the change's `proposal.md`.
|
|
56
|
+
the change's `proposal.md`. For a new subject or a scope change, this is
|
|
57
|
+
where you stop until the human answers (step 1).
|
|
35
58
|
3. **Scenarios.** `canon feature new <area>.<surface> --title "…"`, then one
|
|
36
59
|
`canon scenario new <area>.<surface>.<nn> --subject <id> --case <case> --title "…"`
|
|
37
60
|
per behavior, with failure paths (`--case failure`) as well as `happy`
|
|
38
61
|
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.
|
|
40
|
-
|
|
62
|
+
a new scenario; never retag an existing one to satisfy it. There is no
|
|
63
|
+
sync or format step: `canon status`, `canon gate check`,
|
|
64
|
+
`canon evidence add`, `canon review add` and `canon subject status`
|
|
65
|
+
index added, edited or deleted `.feature` files themselves. Only when
|
|
66
|
+
one of them prints `NOT refreshed` (a format violation, such as a
|
|
67
|
+
hand-written header with no `# canon:` provenance comment, or a
|
|
68
|
+
duplicate id) run `canon inventory sync`: it names each problem; fix
|
|
69
|
+
them, and the next command indexes the files.
|
|
41
70
|
4. **Units.** Split the work into units (for example specs, domain code,
|
|
42
71
|
assets, review). Give each unit its own actor id and session id, and pass
|
|
43
72
|
them on everything the unit records (`--actor-id <unit> --session-id <session>`),
|
|
@@ -52,31 +81,54 @@ assumption you state there, and say so in your final report.
|
|
|
52
81
|
`canon evidence add --project-id <root-id> --scenario-id <id> --kind test-run --role implementer --ref "<command>" --actor-id <unit> --session-id <session>`,
|
|
53
82
|
with `--report junit:<path>` or `--artifact <path>` when files prove it.
|
|
54
83
|
canon stores those bytes in `.canon/artifacts/sha256/` (commit it with the
|
|
55
|
-
ledger) and `canon gate check` re-checks them.
|
|
84
|
+
ledger) and `canon gate check` re-checks them. For a scenario about what
|
|
85
|
+
users see, bind the screenshot or recording that shows it
|
|
86
|
+
(`--artifact <path>`). Under `write: { promote: auto }` (what
|
|
87
|
+
`canon init` writes) the record is final when the command returns; only
|
|
88
|
+
in a repo with `write.promote: manual` (or no `write:` key) follow it
|
|
89
|
+
with `canon gate promote`.
|
|
56
90
|
An external evaluator's audit is recorded with `canon evidence add --kind
|
|
57
91
|
audit`; canon never generates one.
|
|
58
92
|
Evidence for a plan task (`--task <id>`, before `canon gate task`) must
|
|
59
93
|
bind the artifact or report that proves the task; a canon command
|
|
60
|
-
(`canon status`, `canon gate check`) is never that proof.
|
|
94
|
+
(`canon status`, `canon gate check`) is never that proof. Both read the
|
|
95
|
+
change's `tasks.md` as it is now, so a task row you add needs no
|
|
96
|
+
`canon ingest plans` first.
|
|
61
97
|
7. **Independent review.** A different session from the one that wrote the
|
|
62
98
|
code and evidence reviews the change. The reviewer records every defect
|
|
63
99
|
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
|
-
|
|
100
|
+
`canon finding add --change-id <slug> --round <n> --seq <n> --severity <blocker|should-fix|note> --reviewer <me> --actor-id <me> --session-id <session> --summary "…"`;
|
|
101
|
+
once fixed,
|
|
66
102
|
`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
|
-
|
|
103
|
+
(a short sha is fine; canon resolves it). With `write.promote: manual`,
|
|
104
|
+
follow each with `canon gate promote`. Every bug fix adds or extends a scenario. The
|
|
69
105
|
reviewer signs off each scenario:
|
|
70
106
|
`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).
|
|
107
|
+
(written directly; nothing to promote). A reviewer of user-visible
|
|
108
|
+
behavior attaches what it looked at with `--artifact <path>`
|
|
109
|
+
(repeatable); canon stores and re-checks those bytes and never compares
|
|
110
|
+
images.
|
|
72
111
|
8. **Transition.** `canon subject status <id> <state>` along
|
|
73
112
|
`proposed → specced → building → verifying → shipped`. It refuses a move
|
|
74
113
|
the policy does not yet allow (missing evidence, missing reviews, open
|
|
75
114
|
blocker findings); fix the gap rather than overriding, and finish with
|
|
76
|
-
`canon gate check` clean.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
`canon
|
|
115
|
+
`canon gate check` clean. A root whose `.feature` edits could not be
|
|
116
|
+
indexed fails the gate and refuses `shipped` (`uncovered-cell <root>`;
|
|
117
|
+
see step 3). `shipped` also refuses while the change's `proposal.md`
|
|
118
|
+
has no `Approved:` line, and `canon evidence add` refuses the subject's
|
|
119
|
+
scenarios and tasks the same way. Both are backstops: the stop itself
|
|
120
|
+
is at step 1, so you never reach them for a brief the human has not
|
|
121
|
+
answered. You record the line only from the human's answer (with no
|
|
122
|
+
`proposal.md` there is no brief to answer: neither refuses, and the
|
|
123
|
+
ship guard only prints an advisory). Move the subject to
|
|
124
|
+
`shipped` yourself once `canon subject status <id> shipped` passes its
|
|
125
|
+
guard, unless the brief says the human ships. `canon status` shows
|
|
126
|
+
whether each subject's brief is approved, by the same check, and names
|
|
127
|
+
the next step.
|
|
128
|
+
|
|
129
|
+
Every write command says whether the record is final (`written directly`,
|
|
130
|
+
or `staged and promoted in this run`) or staged; only a staged record
|
|
131
|
+
needs `canon gate promote`, and only `write.promote: manual` leaves one.
|
|
80
132
|
|
|
81
133
|
## Command reference
|
|
82
134
|
|
|
@@ -85,7 +137,7 @@ Load only the reference the current step needs.
|
|
|
85
137
|
### Setup and provider projection
|
|
86
138
|
|
|
87
139
|
```bash
|
|
88
|
-
canon init # canon.yaml, starter policy, AGENTS.md block, plans home
|
|
140
|
+
canon init # canon.yaml, starter policy (--policy light|standard|strict, default strict; see canon-policy), AGENTS.md block, plans home
|
|
89
141
|
canon skills install # detects .claude/.agents|.codex/.omp/.pi; if none exists, installs Claude+Codex
|
|
90
142
|
canon skills install --providers=claude,codex,omp,pi
|
|
91
143
|
canon skills check # read-only drift check
|
|
@@ -85,7 +85,7 @@ enum, like `lane`.
|
|
|
85
85
|
|
|
86
86
|
Every scenario belongs to a Subject (the durable product unit, see
|
|
87
87
|
`canon-subject`). Write the Subject first, then pin with `--subject`;
|
|
88
|
-
|
|
88
|
+
the scenario index carries the tag onto the index record and
|
|
89
89
|
`spec_coverage.scope` filters on it. An unpinned scenario is invisible to
|
|
90
90
|
a scoped gate.
|
|
91
91
|
|
|
@@ -97,10 +97,17 @@ canon scenario new checkout.cart.01 --title "Adding an item raises the count" \
|
|
|
97
97
|
--subject checkout-core --lane behavior --case happy # once per behavior
|
|
98
98
|
canon scenario new checkout.cart.04 --title "Adding an out-of-stock item is refused" \
|
|
99
99
|
--subject checkout-core --lane behavior --case failure # and its failure path
|
|
100
|
-
canon format specs # clean?
|
|
101
|
-
canon inventory sync # index it
|
|
102
100
|
```
|
|
103
101
|
|
|
102
|
+
There is no format or sync step after these: the scaffold writes a
|
|
103
|
+
format-clean scenario, and `canon status`, `canon gate check`,
|
|
104
|
+
`canon evidence add`, `canon review add` and `canon subject status`
|
|
105
|
+
index added, edited and deleted `.feature` files before they read. When
|
|
106
|
+
one of them prints `NOT refreshed`, the corpus has a format violation
|
|
107
|
+
(for example a hand-written header with no `# canon:` line) or a
|
|
108
|
+
duplicate id; `canon inventory sync` (or `canon format specs`) names it.
|
|
109
|
+
Fix it and the next command indexes the files (see `canon-inventory`).
|
|
110
|
+
|
|
104
111
|
- `--actor <id>` (or `CANON_ACTOR` in the environment) puts YOUR id on the
|
|
105
112
|
provenance line. Default is `canon-scaffold`.
|
|
106
113
|
- **Never hand-type, move, or edit a `# canon:` line.** The scaffold
|
|
@@ -24,7 +24,7 @@ Every violation carries one of these stable, grep-able strings:
|
|
|
24
24
|
| `uncovered-cell` | Either a policy-required evidence cell (role × artifact) with no matching record, or — with `spec_coverage` enabled — a spec scenario that is unimplemented or mismatched, or a feature surface missing a `require_cases` case. The detail string distinguishes them. Coverage means "a test exists", not "a test passed": even a `Divergent` verdict satisfies the role-cell form. |
|
|
25
25
|
| `unreviewed-promotion` | An artifact tagged `reviewed` has no matching ledger review record — or, with `spec_coverage.require_review` set, an in-scope scenario has no qualifying `Review`: none at all, only self-reviews, only stale reviews (the spec or evidence it pinned changed since), or only `indeterminate`/`unavailable` ones. See `canon-review`. |
|
|
26
26
|
| `trust-below-required` | Achieved trust level is below `policy.yaml`'s `trust_required` for its class — RELEASE-scoped only (`canon gate check --release`); never fires on an ordinary run. |
|
|
27
|
-
| `stale-evidence` | A passing record degraded to stale: its declared surface changed since its `evidence_sha`, or HEAD moved past `staleness.max_commits_behind`. Only degrades an already-green record. Also: a file bound to a latest evidence record whose bytes are gone — no intact blob in `.canon/artifacts/sha256/` and no working-tree copy with the recorded digest (see Bound evidence stays provable below). |
|
|
27
|
+
| `stale-evidence` | A passing record degraded to stale: its declared surface changed since its `evidence_sha`, or HEAD moved past `staleness.max_commits_behind`. Only degrades an already-green record. Also: a file bound to a latest evidence record whose bytes are gone — no intact blob in `.canon/artifacts/sha256/` and no working-tree copy with the recorded digest — or a file bound to an in-effect review with no intact blob (see Bound evidence stays provable below). |
|
|
28
28
|
| `malformed-evidence` | A candidate record doesn't parse, is misfiled, or carries an unparseable interim tag. Malformed evidence is no evidence. |
|
|
29
29
|
| `flagged` | The human-only `flagged` overlay is set — never green regardless of passing evidence. |
|
|
30
30
|
| `unevidenced-flip` | `canon gate task <task_id>` was asked to flip a checkbox with no matching, non-`Divergent` evidence record. |
|
|
@@ -57,6 +57,26 @@ approved: …` or `advisory: policy changed since approval: …` after its
|
|
|
57
57
|
result, naming `canon policy approve --by <who>`; the exit code is
|
|
58
58
|
unchanged (see `canon-policy`).
|
|
59
59
|
|
|
60
|
+
Before it reads the scenario index, the gate rebuilds any stale part of
|
|
61
|
+
it from the changed `.feature` files (the same code `canon inventory
|
|
62
|
+
sync` runs) and says so in one stderr line, silent when the index is
|
|
63
|
+
fresh:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
canon gate check: refreshed the scenario index from 1 changed feature file(s) (specs/features/kind=feature/area=cart/add.feature): 1 record(s) written
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A root that fails to sync (format violations, a duplicate id) is named
|
|
70
|
+
instead, ending ``NOT refreshed: root `x` (N format violation(s), 0 sync
|
|
71
|
+
error(s)) — `canon inventory sync` lists them``, and the gate fails
|
|
72
|
+
closed: it adds `uncovered-cell <root> — the scenario index for spec root
|
|
73
|
+
`<root>` is stale and could not be refreshed (…); `canon inventory sync`
|
|
74
|
+
lists the problems to fix` and exits 1, since the index it would grade
|
|
75
|
+
does not hold the edits. The `verifying → shipped` guard refuses with
|
|
76
|
+
the same violation, and `canon status` lists the sync as its only step.
|
|
77
|
+
Fix what the sync lists and run the gate again. See `canon-storage`
|
|
78
|
+
(The scenario index).
|
|
79
|
+
|
|
60
80
|
### The spec-derived worklist (`spec_coverage`, opt-in)
|
|
61
81
|
|
|
62
82
|
Coverage groups the EVIDENCE corpus, so it can only ever enumerate
|
|
@@ -219,6 +239,48 @@ actually dropped, a multi-line reason, or a drop without
|
|
|
219
239
|
`--scenario-id` (task-only evidence has no drop guard). `canon status`
|
|
220
240
|
lists recorded drops (`dropped bindings (N):`; `drops[]` in `--json`).
|
|
221
241
|
|
|
242
|
+
### Nothing is attested before the brief is approved (`--override-reason`)
|
|
243
|
+
|
|
244
|
+
The approval is never assumed: the agent writes the brief, stops and
|
|
245
|
+
reports it, and builds nothing for that scope until the human answers.
|
|
246
|
+
`canon evidence add` is the backstop for that stop (0.16 M7). It
|
|
247
|
+
refuses (exit `1`, nothing staged or stored) while a subject the record
|
|
248
|
+
attests for has an enforced, unapproved brief: the newest adopted
|
|
249
|
+
change has a `proposal.md` under the openspec plan sources with no
|
|
250
|
+
`Approved:` line (the check `canon status` and the ship guard use; see
|
|
251
|
+
`canon-subject`). The subjects are the scenario's `@subject` owner for
|
|
252
|
+
`--scenario-id`, and every subject adopting the task's change for
|
|
253
|
+
`--task`:
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
canon evidence add: unreviewed-promotion demo-subject — evidence for world.demo.01: brief not approved — openspec/changes/c-demo/proposal.md has no `Approved:` line, so the human has not answered it
|
|
257
|
+
canon evidence add: refused — stop, report the brief to the human and wait for their answer: nothing is built or attested for a subject before its brief is approved. Record their words as `Approved: <who> <when> — "<their words>"` in that proposal.md only from that answer; to attest anyway, pass --override-reason <text>, which is recorded on the evidence record
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
A subject with no brief to answer (no openspec source, no adopted change,
|
|
261
|
+
no `proposal.md`) and a scenario no subject owns are unaffected.
|
|
262
|
+
`canon review add` and `canon finding add` are never refused.
|
|
263
|
+
`--override-reason "<one line>"` attests anyway, prints the `waived …`
|
|
264
|
+
lines and `override recorded by `<actor>` for the N brief refusal(s)
|
|
265
|
+
above: <reason>`, and records on the evidence record:
|
|
266
|
+
|
|
267
|
+
```yaml
|
|
268
|
+
brief_override:
|
|
269
|
+
reason: the human approved in chat; the line follows
|
|
270
|
+
waived:
|
|
271
|
+
- { class: unreviewed-promotion, subject: demo-subject }
|
|
272
|
+
actor: { agent_id: lead } # from --actor-id
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
A blank or multi-line reason exits 2; an override when nothing refused is
|
|
276
|
+
not recorded. `canon status` lists each such record as a `warning:`, and
|
|
277
|
+
`canon gate check` prints them after the result without failing:
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
brief overrides: 1 evidence record(s) attested before their brief was approved — not failing the gate:
|
|
281
|
+
evidence for world.demo.01 was attested by `lead` at 2026-10-12T07:00:00Z while the brief of demo-subject was not approved (override: the human approved in chat; the line follows)
|
|
282
|
+
```
|
|
283
|
+
|
|
222
284
|
### Review-waiver advisories
|
|
223
285
|
|
|
224
286
|
A subject moved into a review-scoped status with `canon subject status
|
|
@@ -235,17 +297,58 @@ review waivers: 1 advisory(ies) — not failing the gate:
|
|
|
235
297
|
|
|
236
298
|
The waiver lasts only until the subject's next status write.
|
|
237
299
|
|
|
238
|
-
|
|
300
|
+
`canon subject status <id> shipped` also refuses where a brief exists and
|
|
301
|
+
has no `Approved:` line (the subject's newest adopted change has a
|
|
302
|
+
`proposal.md`; see `canon-subject`), as an `unreviewed-promotion` naming
|
|
303
|
+
the subject. With no brief to answer it prints an advisory and does not
|
|
304
|
+
refuse. Like the evidence refusal above, it is a backstop: the stop is at
|
|
305
|
+
the brief. `canon gate check` does not grade briefs, so a waived ship
|
|
306
|
+
brief prints no advisory here; `canon status` keeps showing the brief as
|
|
307
|
+
not approved.
|
|
308
|
+
|
|
309
|
+
### Reviews that predate pinning (checked against their pin)
|
|
239
310
|
|
|
240
311
|
With `require_review` on, the reviews in effect (latest per reviewer and
|
|
241
|
-
scenario) that carry no pins were written before 0.15.
|
|
242
|
-
|
|
243
|
-
|
|
312
|
+
scenario) that carry no pins were written before 0.15. Each still names
|
|
313
|
+
`pin`, the commit it reviewed, and the gate checks it there (0.16):
|
|
314
|
+
|
|
315
|
+
- **current** — the scenario's digest at that commit (read with `git`,
|
|
316
|
+
normalized exactly as `canon inventory sync` computes
|
|
317
|
+
`scenario_digest`) equals its current digest, and no declared evidence
|
|
318
|
+
record of the scenario is newer than the review. It counts. When the
|
|
319
|
+
scenario lived in another `.feature` file at that commit, the spec
|
|
320
|
+
root's files at that commit are searched for its id.
|
|
321
|
+
- **stale** — the scenario changed since the commit, or it has newer
|
|
322
|
+
evidence. It no longer counts, and an in-scope scenario left with no
|
|
323
|
+
other qualifying review is `unreviewed-promotion`:
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
unreviewed-promotion gate.check.15 — stale review by `canon-reviewer-g1` (written before review pinning; checked against commit b556ab7529a332aa0b28d0a0f98bdd62fcbb9a63): it has declared evidence newer than the review; a review counts only while what it saw is current — review it again (`canon review add`)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
A spec change reads `its spec changed since <pin> (the scenario's
|
|
330
|
+
digest at that commit is not its current one)`.
|
|
331
|
+
- **undecidable** — the pin is not a commit in this clone (a shallow
|
|
332
|
+
clone, rewritten history), or no feature file of the scenario's root
|
|
333
|
+
holds its id at that commit. It keeps counting, as in 0.15: a missing
|
|
334
|
+
commit never becomes a violation.
|
|
335
|
+
|
|
336
|
+
One advisory line after the violations counts the three. A stale one
|
|
337
|
+
whose scenario already has another qualifying review (current, signed,
|
|
338
|
+
distinct) is counted as superseded, since nothing is owed for it; only
|
|
339
|
+
the scenarios a stale one leaves without a qualifying review are named
|
|
340
|
+
for re-review. The exit code is unchanged by the line itself:
|
|
244
341
|
|
|
245
342
|
```
|
|
246
|
-
advisory:
|
|
343
|
+
advisory: 338 review(s) predate review pinning (0.15) and were checked against the commit each one pinned: 318 current, 12 stale, 8 undecidable; 7 stale one(s) superseded by a current review of the same scenario, so nothing is owed for them; a stale one with no current review in its place no longer counts — review its scenario again (`canon review add`): platformer.movement.01, platformer.movement.02, …; an undecidable one still counts unchecked, because its pinned commit or the scenario at it is not in this clone — check out full history (`fetch-depth: 0` in CI) to check it
|
|
247
344
|
```
|
|
248
345
|
|
|
346
|
+
A review of a scenario no feature file declares any more is not counted.
|
|
347
|
+
In CI, check out full history (`actions/checkout` with `fetch-depth: 0`)
|
|
348
|
+
for the job that runs `canon gate check`, or every legacy review is
|
|
349
|
+
undecidable. Each pinned commit is read once per run through one `git
|
|
350
|
+
cat-file --batch` process, and never fetched over the network.
|
|
351
|
+
|
|
249
352
|
## Experimental: evidence binding (`experimental.evidence_binding`, off by default)
|
|
250
353
|
|
|
251
354
|
An evidence record is an attestation: canon never runs `--ref`, and it
|
|
@@ -338,6 +441,15 @@ subject's status, and a repo with no `spec_coverage` would otherwise never
|
|
|
338
441
|
have its bindings checked. A superseded record is not checked —
|
|
339
442
|
re-attesting is the fix.
|
|
340
443
|
|
|
444
|
+
Files bound to a review (`canon review add --artifact`, see
|
|
445
|
+
`canon-review`) are checked the same way for every in-effect review (the
|
|
446
|
+
latest per `(project_id, scenario_id, reviewer)`), with one difference:
|
|
447
|
+
they were stored when the review was written, so only an intact blob is
|
|
448
|
+
clean. A missing or altered blob is `stale-evidence` on the review's
|
|
449
|
+
scenario, and the detail names the review: ``review by `<reviewer>`
|
|
450
|
+
(actor `<id>`, <time>)``. The working-tree file is reported but never
|
|
451
|
+
vouches for it.
|
|
452
|
+
|
|
341
453
|
`canon evidence vault [--max-artifact-mib <N>]` stores the bytes for
|
|
342
454
|
existing records: every attachment whose working-tree file still matches
|
|
343
455
|
its digest is copied in, and everything else is listed as not stored,
|
|
@@ -431,13 +543,26 @@ canon gate promote --repo . --dry-run # preview only
|
|
|
431
543
|
```
|
|
432
544
|
|
|
433
545
|
Which writes need this step: `evidence add`, `finding add` and `finding
|
|
434
|
-
close` stage, and
|
|
435
|
-
|
|
436
|
-
|
|
546
|
+
close` stage, and under `write: { promote: manual }` (or an absent
|
|
547
|
+
`write:` key) their success line ends ``— run `canon gate promote` to
|
|
548
|
+
commit it``. Under `write: { promote: auto }` (what `canon init` writes)
|
|
549
|
+
or with `--promote`, they promote the one record they just staged —
|
|
550
|
+
same validation and `run_seq` assignment as this command — and print
|
|
551
|
+
one success line naming the committed record (`wrote kind=…/….json`), not
|
|
552
|
+
the staged file, ending `— staged and promoted in this run; nothing to
|
|
553
|
+
promote`, so no separate
|
|
554
|
+
`canon gate promote` is needed for it; `--no-promote` keeps one write
|
|
555
|
+
staged. A record another run staged is not touched: only `gate promote`
|
|
556
|
+
commits everything staged. It stays safe to run anyway: with nothing
|
|
557
|
+
staged it prints
|
|
558
|
+
`nothing staged` and exits `0`. `divergence stage` stages for `canon
|
|
559
|
+
divergence promote` and names it, under the same `write:` rule. Every
|
|
560
|
+
other write — `review add`, `divergence
|
|
437
561
|
resolve|defer`, `subject new|adopt|status`, `change new`, `scenario new`, `feature new`,
|
|
438
562
|
`gate task` (it edits the plan document and re-ingests it)
|
|
439
563
|
— is final when it returns, and its success line ends `— written
|
|
440
|
-
directly; nothing to promote`.
|
|
564
|
+
directly; nothing to promote`. Full promote-on-write contract:
|
|
565
|
+
`canon-storage` (Staged writes and promotion).
|
|
441
566
|
|
|
442
567
|
## `canon gate install-hooks [--repo] [--event] [--matcher] [--command] [--timeout]`
|
|
443
568
|
|
|
@@ -1,16 +1,43 @@
|
|
|
1
1
|
# canon-inventory
|
|
2
2
|
|
|
3
|
-
`canon inventory sync`
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
`canon inventory sync` builds canon's scenario ledger-INDEX: it turns a
|
|
4
|
+
hand-authored, `canon format`-validated `.feature` corpus into the scenario
|
|
5
|
+
records every downstream stage joins against.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## You rarely run it yourself
|
|
8
|
+
|
|
9
|
+
Since 0.16 the commands that read the index refresh it first:
|
|
10
|
+
`canon status`, `canon gate check`, `canon evidence add`,
|
|
11
|
+
`canon review add` and `canon subject status` index added, edited and
|
|
12
|
+
deleted `.feature` files themselves, with the same code this command runs,
|
|
13
|
+
scoped to the changed files, and say so in one stderr line:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
canon status: refreshed the scenario index from 1 changed feature file(s) (specs/features/…): 1 record(s) written
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
So the working loop has no sync step and no `canon format` step: write the
|
|
20
|
+
scenario (`canon scenario new` writes it format-clean) and run the next
|
|
21
|
+
command. Run `canon inventory sync` when:
|
|
22
|
+
|
|
23
|
+
- a reader prints `NOT refreshed` — a root has a format violation (a
|
|
24
|
+
hand-written `Feature:`/`Scenario:` header with no `# canon:` provenance
|
|
25
|
+
comment, an emptied file, …) or a duplicate id, so its files were not
|
|
26
|
+
indexed. `canon gate check` then fails (`uncovered-cell <root>`),
|
|
27
|
+
`shipped` is refused and `canon status` lists only the sync. `canon
|
|
28
|
+
inventory sync` names each problem (`canon format specs` names the format ones);
|
|
29
|
+
fix them and the next command indexes the files;
|
|
30
|
+
- `canon status` says the ledger holds no scenario records while the
|
|
31
|
+
`.feature` files hold some (the same refusal, on an index never built);
|
|
32
|
+
- you want the whole index rebuilt from every root at once.
|
|
33
|
+
|
|
34
|
+
## Where the index sits in the loop
|
|
8
35
|
|
|
9
36
|
```
|
|
10
|
-
author .feature corpus
|
|
37
|
+
author .feature corpus (canon scenario new)
|
|
11
38
|
│
|
|
12
39
|
▼
|
|
13
|
-
|
|
40
|
+
scenario index # refreshed by every reader; `canon inventory sync` rebuilds it
|
|
14
41
|
│
|
|
15
42
|
▼
|
|
16
43
|
canon review add # attests a review — see canon-inventory downstream / canon-gate
|
|
@@ -23,8 +50,8 @@ canon ingest artifacts # with artifacts.native_records: true in canon.y
|
|
|
23
50
|
# (see canon-artifact-ingest)
|
|
24
51
|
```
|
|
25
52
|
|
|
26
|
-
Every stage after
|
|
27
|
-
pair
|
|
53
|
+
Every stage after the index joins on the SAME `(project_id, scenario_id)`
|
|
54
|
+
pair it materializes — one identity scheme, no second one.
|
|
28
55
|
|
|
29
56
|
## `canon inventory sync [--repo <dir>] [--spec-root <dir>]`
|
|
30
57
|
|
|
@@ -26,21 +26,45 @@ diagnostic and reports `clean: false`.
|
|
|
26
26
|
## The starter policy `canon init` writes
|
|
27
27
|
|
|
28
28
|
A fresh `canon init` writes `.canon/policy.yaml` (skip it with
|
|
29
|
-
`--no-policy`; an existing file is never overwritten)
|
|
29
|
+
`--no-policy`; an existing file is never overwritten). `--policy
|
|
30
|
+
light|standard|strict` picks how much it asks for; the default is
|
|
31
|
+
`strict`:
|
|
32
|
+
|
|
33
|
+
| Level | `spec_coverage` |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `light` | `require_evidence: true` — every scenario needs evidence |
|
|
36
|
+
| `standard` | light, plus `require_cases: [failure]` — every feature surface needs a `@case:failure` scenario |
|
|
37
|
+
| `strict` | standard, plus `require_review: {}` — verifying/shipped subjects need an independent review |
|
|
30
38
|
|
|
31
39
|
```yaml
|
|
40
|
+
# canon policy level: strict
|
|
32
41
|
spec_coverage:
|
|
33
42
|
require_evidence: true # every scenario needs evidence
|
|
34
43
|
require_cases: [failure] # every feature surface needs a @case:failure scenario
|
|
35
44
|
require_review: {} # verifying/shipped subjects need an independent review
|
|
36
45
|
```
|
|
37
46
|
|
|
38
|
-
The
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
policy
|
|
43
|
-
|
|
47
|
+
The first line names the level. The file carries comments explaining
|
|
48
|
+
each field, and a `light` or `standard` starter carries the next levels'
|
|
49
|
+
keys as commented lines: to raise the level, uncomment them and change
|
|
50
|
+
the level line, and a human approves the edit (next section). An
|
|
51
|
+
unknown `--policy` value exits 2, and so does `--policy` with `--no-policy`.
|
|
52
|
+
|
|
53
|
+
`canon status` prints the level under the policy settings
|
|
54
|
+
(`level: strict`, or `level: light — raise it by …` with how to raise
|
|
55
|
+
it) and in `--json` as `policy.level`. It reads the level line when the
|
|
56
|
+
file's leading comment block has one naming a known level; otherwise it
|
|
57
|
+
infers the level from the `spec_coverage` section, which must match a
|
|
58
|
+
level exactly (no `scope`, `exclude_lanes`, `require_audit`, or
|
|
59
|
+
non-default `require_review` settings); anything else is `custom`.
|
|
60
|
+
`policy.level` is `null` when there is no policy.
|
|
61
|
+
|
|
62
|
+
Every level loads with zero diagnostics. The policy is what `canon gate
|
|
63
|
+
check` grades the work against, so a human should review and approve
|
|
64
|
+
it, and every later change to it (next section). The agent doing the
|
|
65
|
+
work should not write its own grading policy. Narrow it with `scope:
|
|
66
|
+
[building, verifying]` to check evidence only for subjects in those
|
|
67
|
+
statuses (see `canon-gate`).
|
|
44
68
|
|
|
45
69
|
## Approving the policy
|
|
46
70
|
|
|
@@ -48,8 +72,11 @@ only for subjects in those statuses (see `canon-gate`).
|
|
|
48
72
|
canon policy approve --by <who> [--note <text>] [--repo <dir>]
|
|
49
73
|
```
|
|
50
74
|
|
|
51
|
-
A human
|
|
52
|
-
|
|
75
|
+
A human approves the policy after reviewing `.canon/policy.yaml`, in the
|
|
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
|
|
79
|
+
writes `.canon/policy.approval.json` directly (nothing to promote):
|
|
53
80
|
|
|
54
81
|
```json
|
|
55
82
|
{
|
|
@@ -85,8 +112,8 @@ An approval file that does not parse reads
|
|
|
85
112
|
`--json` `warnings`). The advisory never changes an exit code: canon
|
|
86
113
|
cannot tell whether a human or an agent ran the command, so the record
|
|
87
114
|
names who approved which bytes, and the advisory says when that no
|
|
88
|
-
longer holds. An agent
|
|
89
|
-
behalf.
|
|
115
|
+
longer holds. An agent runs `canon policy approve` only to record a
|
|
116
|
+
human's answer, never without one and never on its own behalf.
|
|
90
117
|
|
|
91
118
|
## The closed CEL profile
|
|
92
119
|
|
|
@@ -17,8 +17,10 @@ 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> ... --session-id <session
|
|
21
|
-
|
|
20
|
+
--scenario-id <id> --project-id <root-id> ... --session-id <session>`;
|
|
21
|
+
final when it returns under `write: { promote: auto }`, which
|
|
22
|
+
`canon init` writes, and followed by `canon gate promote` only under
|
|
23
|
+
`write.promote: manual`). See `canon-gate`.
|
|
22
24
|
2. **Independent review** of the change(s) the subject adopted. Record
|
|
23
25
|
each issue the reviewer raises as a Finding, fix it, then close it:
|
|
24
26
|
|
|
@@ -26,13 +28,14 @@ The order an agent follows to claim a subject done:
|
|
|
26
28
|
canon finding add --change-id c-demo --round 1 --seq 1 \
|
|
27
29
|
--severity blocker --disposition open --reviewer reviewer-2 \
|
|
28
30
|
--summary "unbounded subprocess wait" --session-id review-session-1
|
|
29
|
-
canon gate promote
|
|
30
31
|
# ... fix, commit ...
|
|
31
32
|
canon finding close --change-id c-demo --round 1 --seq 1 \
|
|
32
33
|
--disposition fixed --resolution-sha "$(git rev-parse HEAD)"
|
|
33
|
-
canon gate promote
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
Under `write.promote: manual` (or no `write:` key), run
|
|
37
|
+
`canon gate promote` after each of the two.
|
|
38
|
+
|
|
36
39
|
3. **One Review per scenario**, by someone other than the evidence actor,
|
|
37
40
|
from a session other than the one that attested the evidence, after
|
|
38
41
|
the evidence is in (the review pins what it saw):
|
|
@@ -48,7 +51,9 @@ The order an agent follows to claim a subject done:
|
|
|
48
51
|
reviewer could not reach a verdict, record that instead of signing:
|
|
49
52
|
`--outcome indeterminate` (or `unavailable` when it could not run).
|
|
50
53
|
Changing the scenario or re-attesting it afterwards makes the review
|
|
51
|
-
stale; review it again.
|
|
54
|
+
stale; review it again. A reviewer of user-visible behavior attaches
|
|
55
|
+
the screenshots or recordings it looked at with `--artifact <path>`
|
|
56
|
+
(below).
|
|
52
57
|
|
|
53
58
|
4. **Move the subject**: `canon subject status <id> verifying`, later
|
|
54
59
|
`shipped`. See `canon-subject`.
|
|
@@ -70,8 +75,12 @@ canon finding close --change-id <c> --round <n> --seq <n> \
|
|
|
70
75
|
--disposition fixed --resolution-sha <sha> [--session-id <id>] # or rejected / deferred / open
|
|
71
76
|
```
|
|
72
77
|
|
|
73
|
-
Both stage a candidate
|
|
74
|
-
|
|
78
|
+
Both stage a candidate. Under `write: { promote: auto }` (what
|
|
79
|
+
`canon init` writes) the same run promotes it and the success line names
|
|
80
|
+
the committed record (``wrote kind=finding/… — staged and promoted in this
|
|
81
|
+
run; nothing to promote``); under `write.promote: manual`
|
|
82
|
+
`canon gate promote` commits it, and the line says so (``staged … — run
|
|
83
|
+
`canon gate promote` to commit it``). `finding close`
|
|
75
84
|
changes only the disposition (and `resolution_sha` with it); severity,
|
|
76
85
|
reviewer, summary, and refs are copied from the committed record. It
|
|
77
86
|
appends a second version at the same natural key — the first stays. Its
|
|
@@ -130,7 +139,8 @@ canon review add --project-id <root-id> --scenario-id <id> \
|
|
|
130
139
|
--reviewer <who> --pin <sha-or-ref> --role <role> \
|
|
131
140
|
(--upstream-ref <ref> | --original-spec-ref <ref>) [--actor-id <id>] [--session-id <id>] \
|
|
132
141
|
[--outcome signed|indeterminate|unavailable] \
|
|
133
|
-
[--reviewer-provider <p>] [--reviewer-model <m>] [--reviewer-harness <h>]
|
|
142
|
+
[--reviewer-provider <p>] [--reviewer-model <m>] [--reviewer-harness <h>] \
|
|
143
|
+
[--artifact <path>]... [--max-artifact-mib <MiB>]
|
|
134
144
|
```
|
|
135
145
|
|
|
136
146
|
One attributed Review record for one scenario, joined on
|
|
@@ -143,6 +153,40 @@ own. It is written directly to the ledger, with no staging step, and says
|
|
|
143
153
|
so: `canon review add: wrote <path> (new) — written directly; nothing to
|
|
144
154
|
promote`.
|
|
145
155
|
|
|
156
|
+
### What the reviewer looked at (artifacts)
|
|
157
|
+
|
|
158
|
+
`--artifact <path>` (repeatable) binds a file the reviewer looked at — a
|
|
159
|
+
screenshot, a recording, a trace — exactly as `canon evidence add
|
|
160
|
+
--artifact` binds one: the path must resolve to a file inside the
|
|
161
|
+
repository, a file over 25 MiB is refused unless `--max-artifact-mib <N>`
|
|
162
|
+
raises the limit, and the bytes are copied into
|
|
163
|
+
`.canon/artifacts/sha256/<hex>` (commit it with the ledger) before the
|
|
164
|
+
review is written. Any refusal exits 2 and writes and stores nothing. The
|
|
165
|
+
review records each distinct file once, in the order given:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
"artifacts": [{"path": "reports/title.png", "sha256": "<64 hex>"}]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The field is absent when no `--artifact` is given. `review add` prints
|
|
172
|
+
`canon review add: artifact store holds <n> bound file(s) under
|
|
173
|
+
.canon/artifacts/sha256/ (<k> newly stored) — commit them with the
|
|
174
|
+
ledger`.
|
|
175
|
+
|
|
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"):
|
|
181
|
+
|
|
182
|
+
```
|
|
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>`
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
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.
|
|
189
|
+
|
|
146
190
|
### What the review saw (pins)
|
|
147
191
|
|
|
148
192
|
`review add` reads the corpus and records `pins` on the review; the
|
|
@@ -245,20 +289,43 @@ The scenario is reviewed when at least one of those latest reviews:
|
|
|
245
289
|
1. is `signed`;
|
|
246
290
|
2. still matches its pins — the scenario record's current
|
|
247
291
|
`source_digest`, and its current latest evidence record per role —
|
|
248
|
-
when it has pins;
|
|
292
|
+
when it has pins; or, written before 0.15 without pins, still
|
|
293
|
+
describes its scenario at the commit it pinned (below);
|
|
249
294
|
3. passes `distinct_actor`, when that is on.
|
|
250
295
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
296
|
+
### Reviews written before pinning
|
|
297
|
+
|
|
298
|
+
A review written before 0.15 has no pins, only `pin`: the commit it
|
|
299
|
+
reviewed. The gate reads the scenario's `.feature` file at that commit
|
|
300
|
+
(`git`, with the same per-scenario normalization `canon inventory sync`
|
|
301
|
+
uses for `scenario_digest`; if the scenario was in another file then, the
|
|
302
|
+
spec root's files at that commit are searched for its id) and decides:
|
|
303
|
+
|
|
304
|
+
- **current** when that digest equals the scenario's current one and no
|
|
305
|
+
declared evidence record of the scenario is newer than the review: it
|
|
306
|
+
counts under rules 1 and 3;
|
|
307
|
+
- **stale** when the scenario changed since the commit (`its spec changed
|
|
308
|
+
since <pin>`) or has evidence newer than the review (`it has declared
|
|
309
|
+
evidence newer than the review`): it no longer counts, exactly like a
|
|
310
|
+
stale pinned review;
|
|
311
|
+
- **undecidable** when the pin is not a commit in this clone (shallow
|
|
312
|
+
clone, rewritten history) or the scenario id is absent at it: it keeps
|
|
313
|
+
counting, as in 0.15. A missing commit never becomes a violation.
|
|
314
|
+
|
|
315
|
+
`canon gate check` prints one advisory line with the split (exit code
|
|
316
|
+
unchanged by the line itself). A stale one whose scenario already has
|
|
317
|
+
another qualifying review is counted as superseded and not named:
|
|
254
318
|
|
|
255
319
|
```
|
|
256
|
-
advisory:
|
|
320
|
+
advisory: 338 review(s) predate review pinning (0.15) and were checked against the commit each one pinned: 318 current, 12 stale, 8 undecidable; 7 stale one(s) superseded by a current review of the same scenario, so nothing is owed for them; a stale one with no current review in its place no longer counts — review its scenario again (`canon review add`): platformer.movement.01, platformer.movement.02, …; an undecidable one still counts unchecked, because its pinned commit or the scenario at it is not in this clone — check out full history (`fetch-depth: 0` in CI) to check it
|
|
257
321
|
```
|
|
258
322
|
|
|
259
323
|
`canon status` prints the same line as a warning, and `canon subject
|
|
260
|
-
status` prints it on stderr for the subject's own scenarios
|
|
261
|
-
the
|
|
324
|
+
status` prints it on stderr for the subject's own scenarios; all three
|
|
325
|
+
make the same decision. Re-review exactly the scenarios the line names:
|
|
326
|
+
the new review pins what it saw and replaces the reviewer's legacy one.
|
|
327
|
+
In CI, check out full history (`fetch-depth: 0`) for the job that runs
|
|
328
|
+
the gate, or every legacy review is undecidable.
|
|
262
329
|
|
|
263
330
|
### `unreviewed-promotion`
|
|
264
331
|
|
|
@@ -149,7 +149,127 @@ Every query, export, and purge appends an actor-stamped record to
|
|
|
149
149
|
`.canon/audit/access.jsonl` (mode `0600` on creation) in the repository
|
|
150
150
|
whose `canon.yaml` it read (through `--repo` or `--canon-yaml`), never in
|
|
151
151
|
the current directory; with no `canon.yaml` there is no audit and nothing
|
|
152
|
-
is written. `
|
|
152
|
+
is written. The log is local operator state: `canon init` keeps
|
|
153
|
+
`.canon/audit/` out of git (below). `CANON_ACTOR` takes
|
|
153
154
|
precedence over the OS username. Audit rows contain operation, kind, scope,
|
|
154
155
|
sensitive flag, counts, and timestamp only—never query text, secrets, or
|
|
155
156
|
record bodies.
|
|
157
|
+
|
|
158
|
+
## Staged writes and promotion
|
|
159
|
+
|
|
160
|
+
Four writes stage their record instead of committing it: `canon evidence
|
|
161
|
+
add`, `canon finding add` and `canon finding close` (under
|
|
162
|
+
`.canon/ledger/_staging/`, committed by `canon gate promote`) and `canon
|
|
163
|
+
divergence stage` (committed by `canon divergence promote`). `canon.yaml`'s
|
|
164
|
+
`write:` section decides whether they promote in the same run:
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
write:
|
|
168
|
+
promote: auto # or manual
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- `auto` — the command stages, then promotes that one record through the
|
|
172
|
+
same validation and assignment `canon gate promote` (or `canon
|
|
173
|
+
divergence promote`) applies. Records other runs staged stay staged;
|
|
174
|
+
only `canon gate promote` / `canon divergence promote` commit
|
|
175
|
+
everything staged. The one success line names the committed record
|
|
176
|
+
(`wrote kind=…/….json`; promotion renames it, so the staged file is not
|
|
177
|
+
named) and ends `— staged and promoted in this run; nothing to
|
|
178
|
+
promote`. `canon init` writes `auto`.
|
|
179
|
+
- `manual` — the record stays staged and the line ends ``— run `canon
|
|
180
|
+
gate promote` to commit it``. An absent `write:` key (or `promote:`
|
|
181
|
+
field) is `manual`, so a repo that never set it is unchanged.
|
|
182
|
+
|
|
183
|
+
`--promote` / `--no-promote` decide for one write and win over the config
|
|
184
|
+
without reading it; both at once is a usage error (exit `2`). If the
|
|
185
|
+
promotion refuses the command's own record, the record stays staged, the
|
|
186
|
+
line ends ``— run `canon gate promote` to commit it`` (`canon divergence
|
|
187
|
+
promote` for a divergence), and the command exits `1`. With neither flag,
|
|
188
|
+
a malformed `write:` section (anything but `promote: auto | manual`)
|
|
189
|
+
refuses the write, exit `2`, before anything is staged. `canon init
|
|
190
|
+
--check-config` reports `[PASS] write (promote: auto)`, `[not configured]
|
|
191
|
+
write (promote: manual)`, or `[FAIL] write: …`. `canon gate promote` still
|
|
192
|
+
works under `auto` and is idempotent: with nothing staged it prints
|
|
193
|
+
`nothing staged` and exits `0`.
|
|
194
|
+
|
|
195
|
+
## The scenario index
|
|
196
|
+
|
|
197
|
+
`canon inventory sync` materializes one `Scenario` record per
|
|
198
|
+
`(project_id, scenario_id)` under `.canon/ledger/kind=scenario/`. The
|
|
199
|
+
index is derived — a pure function of the `.feature` files — so the
|
|
200
|
+
commands that read it rebuild it first: `canon gate check`, `canon
|
|
201
|
+
status`, `canon subject status`, `canon review add` and `canon evidence
|
|
202
|
+
add`. The index is stale when a `.feature` file holds a scenario with no
|
|
203
|
+
live index record, or one whose recorded source digest differs from the
|
|
204
|
+
file's current bytes, or when the index holds a scenario of a spec root
|
|
205
|
+
that no `.feature` file of it declares any more. The reader then runs
|
|
206
|
+
`canon inventory sync`'s code on the changed files (validation still
|
|
207
|
+
covers the whole root) and prints one stderr line:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
canon status: refreshed the scenario index from 1 changed feature file(s) (specs/features/kind=feature/area=cart/add.feature): 1 record(s) written
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A deleted scenario is not erased: `canon inventory sync` — and a reader's
|
|
214
|
+
refresh, identically — appends a tombstone, a new generation of its
|
|
215
|
+
record with `removed: true`. The gate, `canon status`, `canon query` and
|
|
216
|
+
the report panels then treat it as absent; re-adding the scenario writes
|
|
217
|
+
a live generation that supersedes the tombstone. A root that fails to
|
|
218
|
+
sync writes no tombstone either.
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
canon status: refreshed the scenario index from 1 scenario(s) no longer in any feature file (cart.pay.01): 1 record(s) written
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`canon inventory sync` and every refresh hold an OS file lock on
|
|
225
|
+
`.canon/scenario-index.lock` while they write, so concurrent runs never
|
|
226
|
+
index one change twice; a refresh that waited checks staleness again and
|
|
227
|
+
is silent if another run already did the work. The lock is released when
|
|
228
|
+
its holder exits, however it exits, so it never goes stale; a writer
|
|
229
|
+
gives up after 30 s with a message naming the file (`canon inventory
|
|
230
|
+
sync` exits `2`; a reader skips the refresh and still runs).
|
|
231
|
+
|
|
232
|
+
A fresh index prints nothing. A root that fails to sync (format
|
|
233
|
+
violations, a duplicate id) is named and left as it was, and the command
|
|
234
|
+
still runs; `canon gate check` then fails with `uncovered-cell <root>`,
|
|
235
|
+
the `verifying → shipped` guard refuses, and `canon status` lists only
|
|
236
|
+
`canon inventory sync`, while the reporting readers (`query`, `context`)
|
|
237
|
+
keep only this line:
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
canon gate check: found the scenario index stale for 1 changed feature file(s) (…): 0 record(s) written; NOT refreshed: root `root` (2 format violation(s), 0 sync error(s)) — `canon inventory sync` lists them
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Nothing is written in a directory without `canon.yaml`. `canon inventory
|
|
244
|
+
sync` stays the explicit command, and the one that lists violations and
|
|
245
|
+
tag diagnostics.
|
|
246
|
+
|
|
247
|
+
## The artifact store lock
|
|
248
|
+
|
|
249
|
+
Every write to the evidence artifact store (`.canon/artifacts/sha256/`:
|
|
250
|
+
`canon evidence add`, `canon review add`, `canon evidence vault`) holds an
|
|
251
|
+
OS file lock on `.canon/artifact-store.lock` from its "already stored?"
|
|
252
|
+
check through its last rename, so a write that fails and rolls back never
|
|
253
|
+
removes a blob another writer published or counted as stored. A rollback
|
|
254
|
+
removes only the blobs its own write renamed into place; a blob already
|
|
255
|
+
stored intact counts as stored and is not written again. The lock is
|
|
256
|
+
released when its holder exits, however it exits, so it never goes stale;
|
|
257
|
+
a writer gives up after 30 s with a message naming the file and saying to
|
|
258
|
+
retry (the command exits `2` and writes no record).
|
|
259
|
+
|
|
260
|
+
## What `canon init` keeps out of git
|
|
261
|
+
|
|
262
|
+
A fresh `canon init` writes these lines to `.gitignore` (each under a
|
|
263
|
+
comment, only the missing ones):
|
|
264
|
+
|
|
265
|
+
- `.canon/hot.db*` — the sqlite hot tier and its WAL/SHM sidecars.
|
|
266
|
+
- `.canon/audit/` — the query/export/purge access log.
|
|
267
|
+
- `.canon/ledger/kind=scenario/` — the scenario index; every reader
|
|
268
|
+
rebuilds it.
|
|
269
|
+
- `.canon/scenario-index.lock` — the lock serializing index rebuilds.
|
|
270
|
+
- `.canon/artifact-store.lock` — the lock serializing artifact store
|
|
271
|
+
writes.
|
|
272
|
+
|
|
273
|
+
A rerun on a repo that already has `canon.yaml` only refreshes
|
|
274
|
+
`AGENTS.md`, so an existing repo's `.gitignore` is unchanged; add the
|
|
275
|
+
lines by hand to adopt them.
|
|
@@ -20,11 +20,13 @@ authored in one place:
|
|
|
20
20
|
- **Changes**: `change_ids` on the Subject, appended by `canon subject
|
|
21
21
|
adopt`.
|
|
22
22
|
- **Scenarios**: the `@subject:<id>` tag on each Gherkin scenario,
|
|
23
|
-
indexed onto `Scenario.subject_id` by
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
indexed onto `Scenario.subject_id` by the scenario index (rebuilt by
|
|
24
|
+
`canon inventory sync`, and refreshed by every command that reads it).
|
|
25
|
+
The Subject record carries no scenario list — the spec corpus is the
|
|
26
|
+
only source, and the Subjects report panel, `spec_coverage.scope`, and
|
|
27
|
+
the shipped gate all read it from there. To link a scenario, tag it;
|
|
28
|
+
the next `canon status` or `canon gate check` indexes the tag. There is
|
|
29
|
+
no subject-side command for it.
|
|
28
30
|
|
|
29
31
|
## The 4-step loop
|
|
30
32
|
|
|
@@ -81,9 +83,10 @@ canon retrieve --role dev --domain dev --subject subject-domain-loop --k 5
|
|
|
81
83
|
### 3. Work → pin every artifact to `subject_id`
|
|
82
84
|
|
|
83
85
|
- **Scenarios**: `canon scenario new <tag> --subject <subject-id>` emits
|
|
84
|
-
the `@subject:<subject-id>` tag;
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
the `@subject:<subject-id>` tag; the scenario index maps the tag onto
|
|
87
|
+
the scenario's `subject_id` the next time a command reads it (no sync
|
|
88
|
+
step; see `canon-inventory`). A malformed/absent tag leaves it unset
|
|
89
|
+
(fail-soft), never an error. See `canon-authoring`.
|
|
87
90
|
- **Changes**: adopt imported plan changes under the Subject (see
|
|
88
91
|
[Adopt flow](#adopt-flow)).
|
|
89
92
|
- **Reviews / evidence / divergences**: authored as usual (`canon review
|
|
@@ -145,6 +148,36 @@ empty set; so is one whose `scenario` records route away from the
|
|
|
145
148
|
gate's rung. Each refusal prints by failure class (`uncovered-cell`),
|
|
146
149
|
exits 1, and the status stays `verifying`. `retired` is not gated.
|
|
147
150
|
|
|
151
|
+
**The brief (0.16):** `verifying → shipped` also refuses where a brief
|
|
152
|
+
exists and is not approved, read exactly as `canon status` reads it (one
|
|
153
|
+
shared check, so status says `ship` only when this passes): the subject's
|
|
154
|
+
newest adopted change has a `proposal.md` under the openspec plan sources,
|
|
155
|
+
and it carries no `Approved:` line outside HTML comments and code fences.
|
|
156
|
+
It runs with or without a policy. The refusal is an `unreviewed-promotion`
|
|
157
|
+
naming the subject, and says what to do:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
canon subject status: unreviewed-promotion demo-subject — verifying → shipped: brief not approved — openspec/changes/c-demo/proposal.md has no `Approved:` line; a human must answer the brief, and the agent records `Approved: <who> <when> — "<their words>"` there only from that answer
|
|
161
|
+
canon subject status: refused — get the human's answer to the brief and record it as its `Approved:` line, or pass --override-reason <text> to move anyway; the waiver is recorded on the subject
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Never write the line without the human's answer. When there is no brief to
|
|
165
|
+
answer (no openspec plan source, no adopted change, or a change with no
|
|
166
|
+
`proposal.md`), the move is not refused: it prints
|
|
167
|
+
`canon subject status: advisory: <id>'s brief is not approved — <why>, so
|
|
168
|
+
there is no brief to answer and shipping is not refused`, and status shows
|
|
169
|
+
the brief as not approved, as an advisory. `subjects[].brief.enforced` in
|
|
170
|
+
`canon status --json` says which case applies. A brief that says the human
|
|
171
|
+
ships changes who runs the transition, not the check.
|
|
172
|
+
|
|
173
|
+
This refusal is a backstop, and so is the evidence one: shipping is not
|
|
174
|
+
the first thing an unapproved brief stops. The agent writes the brief,
|
|
175
|
+
then stops and reports it, and nothing is built or attested for the
|
|
176
|
+
subject until the human answers. `canon evidence add` refuses (exit 1,
|
|
177
|
+
nothing staged) a scenario whose `@subject` owner, or a task whose change
|
|
178
|
+
an adopting subject, has an enforced, unapproved brief, through the same
|
|
179
|
+
check; see `canon-gate`.
|
|
180
|
+
|
|
148
181
|
### The review guard (`spec_coverage.require_review`, opt-in)
|
|
149
182
|
|
|
150
183
|
When `.canon/policy.yaml` sets `spec_coverage.require_review` (see
|
|
@@ -172,9 +205,11 @@ canon subject status demo-subject verifying \
|
|
|
172
205
|
--override-reason "reviewer out until Monday" --actor-id lead
|
|
173
206
|
```
|
|
174
207
|
|
|
175
|
-
`--override-reason "<one line>"` waives ONLY `unreviewed-promotion`
|
|
176
|
-
`open-blocker` — never a ship-gate
|
|
177
|
-
|
|
208
|
+
`--override-reason "<one line>"` waives ONLY `unreviewed-promotion` (the
|
|
209
|
+
review gaps and the brief check) and `open-blocker` — never a ship-gate
|
|
210
|
+
verdict or case gap, never unreadable routing. A waived brief is recorded
|
|
211
|
+
as `{ class: unreviewed-promotion, subject: <subject id> }`. It prints
|
|
212
|
+
the `waived ...` lines and `override recorded by
|
|
178
213
|
`lead` for the N violation(s) above: <reason>`, and records on the new
|
|
179
214
|
Subject record exactly the violations it let through:
|
|
180
215
|
|
|
@@ -192,13 +227,14 @@ subject's next status write, so it never outlives the status it was
|
|
|
192
227
|
granted for. While it stands, `canon gate check` lists the recorded gaps
|
|
193
228
|
as advisories instead of failing; a gap it did not record (a blocker
|
|
194
229
|
raised later, a scenario tagged later) still fails the gate. Without
|
|
195
|
-
`require_review`, nothing is printed and nothing changes
|
|
230
|
+
`require_review`, nothing is printed and nothing changes, except that
|
|
231
|
+
`shipped` still runs the brief check above.
|
|
196
232
|
|
|
197
233
|
## Adopt flow
|
|
198
234
|
|
|
199
|
-
Planning docs become change/task records via
|
|
200
|
-
`canon-plan-import`), but that stops at import.
|
|
201
|
-
lifts an imported change into a managed Subject:
|
|
235
|
+
Planning docs written outside canon become change/task records via
|
|
236
|
+
`canon ingest plans` (see `canon-plan-import`), but that stops at import.
|
|
237
|
+
`canon subject adopt` lifts an imported change into a managed Subject:
|
|
202
238
|
|
|
203
239
|
```bash
|
|
204
240
|
canon ingest plans --repo . # import → change/task rows
|
|
@@ -206,6 +242,13 @@ canon subject adopt subject-domain-loop-plan \
|
|
|
206
242
|
--subject subject-domain-loop # link change → subject
|
|
207
243
|
```
|
|
208
244
|
|
|
245
|
+
A change made with `canon change new` (below) is recorded and adopted
|
|
246
|
+
already, and editing its `tasks.md` needs no import either:
|
|
247
|
+
`canon evidence add --task` and `canon gate task` read the file as it is,
|
|
248
|
+
and a flip re-imports that change itself. Run `canon ingest plans` for a
|
|
249
|
+
plan written outside canon, or when `canon query --kind task` must list
|
|
250
|
+
rows nobody has flipped yet.
|
|
251
|
+
|
|
209
252
|
`adopt` stamps the change's `subject_id` and adds it to the Subject's
|
|
210
253
|
`change_ids`, so `canon query --kind change --change-id …` and `canon
|
|
211
254
|
query --kind subject` agree on the link from both ends. It writes only
|
|
@@ -251,9 +294,10 @@ canon change new add-login --subject auth --title "Add login"
|
|
|
251
294
|
exist, when the slug already has a change dir (active or archived) or a
|
|
252
295
|
`change` record, or when `canon.yaml` has no `openspec` plans source.
|
|
253
296
|
|
|
254
|
-
Add task rows to `tasks.md` as `- [ ] <n> <title>`,
|
|
255
|
-
|
|
256
|
-
|
|
297
|
+
Add task rows to `tasks.md` as `- [ ] <n> <title>`, and flip each with
|
|
298
|
+
`canon gate task <slug>#<n>` once evidence exists. `canon evidence add
|
|
299
|
+
--task` and the flip read `tasks.md` as it is, so a new row needs no
|
|
300
|
+
`canon ingest plans` run.
|
|
257
301
|
|
|
258
302
|
## Where are we: `canon status`
|
|
259
303
|
|
|
@@ -286,7 +330,26 @@ uses, so status and the gate cannot disagree about a gap:
|
|
|
286
330
|
stale or whose latest reviews are all `indeterminate`/`unavailable`
|
|
287
331
|
(see `canon-review`), and the `canon review add` step names both. With
|
|
288
332
|
`require_review` on, a `warning:` line counts the in-effect reviews that
|
|
289
|
-
predate review pinning (0.15)
|
|
333
|
+
predate review pinning (0.15), checked against the commit each pinned:
|
|
334
|
+
current, stale (named) and undecidable.
|
|
335
|
+
- **Brief** (0.16) — for each `building`, `verifying` or `shipped`
|
|
336
|
+
subject, and each `proposed` subject whose newest adopted change has a
|
|
337
|
+
`proposal.md`, a `brief:` line under it says whether the brief is approved:
|
|
338
|
+
`brief: approved in <proposal.md> — <the recorded words>`, or
|
|
339
|
+
`brief: not approved (…)`. Approval is an `Approved:` line in the
|
|
340
|
+
`proposal.md` of the subject's newest adopted change (the last one
|
|
341
|
+
adopted): a line that starts with `Approved:` (indent allowed), says
|
|
342
|
+
something after it, and sits outside HTML comments (`<!-- … -->`,
|
|
343
|
+
multi-line included) and fenced code blocks (```` ``` ```` or `~~~`),
|
|
344
|
+
written by the agent once the human answers, as
|
|
345
|
+
`Approved: <who> <when> — "<the human's words>"`. Fixes and follow-ups
|
|
346
|
+
live as tasks in the approved change, so a newly adopted change is a
|
|
347
|
+
new scope: an older change's approval does not cover it. Status reads
|
|
348
|
+
the line from the change dir under the repo's `openspec` plans sources,
|
|
349
|
+
in config order (the first source holding the change owns it, a live
|
|
350
|
+
dir before an archived one; with no `plans:` section, the `openspec`
|
|
351
|
+
source rooted at the repo that `canon gate task` also falls back to).
|
|
352
|
+
There is no record kind for it; the file is the record.
|
|
290
353
|
- **Unowned** — scenarios with no `@subject:` tag.
|
|
291
354
|
- **Audits** — how many scenarios are sampled for
|
|
292
355
|
`spec_coverage.require_audit`, confirmed, owed (no audit or
|
|
@@ -296,23 +359,54 @@ uses, so status and the gate cannot disagree about a gap:
|
|
|
296
359
|
one).
|
|
297
360
|
- **Dropped bindings** — `dropped bindings (N):` lists every recorded
|
|
298
361
|
`--drop-attachment` (scenario, paths, reason); `drops[]` in `--json`.
|
|
299
|
-
-
|
|
362
|
+
- **Brief overrides** — a `warning:` line per evidence record attested
|
|
363
|
+
with `canon evidence add --override-reason` while its subject's brief
|
|
364
|
+
was not approved: what it attests, who, when, the subject and the
|
|
365
|
+
reason (see `canon-gate`).
|
|
366
|
+
- **`next:`** — at most five steps, each with a one-line reason; the
|
|
300
367
|
rest are counted. Rules, in order: no `canon.yaml` → `canon init`; an
|
|
301
368
|
unreadable one → `canon init --check-config`; a scenario tagged with a
|
|
302
369
|
subject that has no record → `canon subject new <that id>`; no subject
|
|
303
|
-
at all → `canon subject new`; subjects
|
|
304
|
-
`canon inventory sync
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
`canon
|
|
311
|
-
`
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
370
|
+
at all → `canon subject new`; subjects and `.feature` scenarios but no
|
|
371
|
+
scenario records → `canon inventory sync` (status refreshes the index
|
|
372
|
+
itself first, so this means the refresh refused on a format violation
|
|
373
|
+
or a duplicate id, which the sync names). Then per subject, verifying
|
|
374
|
+
first, then building, specced, proposed, shipped: an unapproved brief
|
|
375
|
+
(building and later, and proposed once its change has a `proposal.md`)
|
|
376
|
+
→ the human step below, which is then the subject's
|
|
377
|
+
only step; no scenario → `canon scenario new … --subject <id>` (and, when no
|
|
378
|
+
`.feature` file holds a scenario yet, `canon feature new` first, unless
|
|
379
|
+
every subject waits on its brief); a missing case → `canon scenario new
|
|
380
|
+
<surface>.<next nn> … --case <case>`; (building and later) an
|
|
381
|
+
unevidenced or divergent scenario → `canon evidence add --scenario-id
|
|
382
|
+
…` (its reason adds "then `canon gate promote`" only under
|
|
383
|
+
`write.promote: manual`); an open blocker → `canon finding close …`; a
|
|
384
|
+
due, unreviewed, evidenced scenario → `canon review add …` from another
|
|
385
|
+
session; none of these → `canon subject status <id> <next status>`.
|
|
386
|
+
Last, unowned scenarios → `canon status`, after tagging them in their
|
|
387
|
+
`.feature` files (status re-indexes the edit; no sync step). With more
|
|
388
|
+
than one `specs.roots[]` entry, every suggested `canon scenario new`
|
|
389
|
+
carries `--project`: the owning root for a missing case, `<root-id>`
|
|
390
|
+
(the reason lists the roots) for a subject that owns no scenario yet.
|
|
391
|
+
- **The one human step.** Every step is the agent's to run except one:
|
|
392
|
+
a subject whose brief is enforced (its newest change has a
|
|
393
|
+
`proposal.md` with no `Approved:` line) gets
|
|
394
|
+
`human: stop and report the brief in <proposal.md>; build and attest nothing for <id> until the human answers (one answer approves brief and policy)`,
|
|
395
|
+
whose reason names that `proposal.md`, says `canon evidence add`
|
|
396
|
+
refuses the subject's scenarios and tasks until the line is recorded,
|
|
397
|
+
and names `canon policy approve --by <the human> --note "<their words>"`.
|
|
398
|
+
It is the subject's only step, whatever its status: nothing suggests
|
|
399
|
+
`canon feature new`, `canon scenario new`, `canon evidence add`, a
|
|
400
|
+
review or a move-on for it before the human answers, and
|
|
401
|
+
`canon subject status <id> shipped` refuses on the same check. The
|
|
402
|
+
approval is never assumed. A subject with no brief to answer gets no human step;
|
|
403
|
+
its `brief:` line says
|
|
404
|
+
`not approved (no adopted change; advisory, nothing to approve yet)` or
|
|
405
|
+
``not approved (change `<id>` has no proposal.md; advisory, nothing to
|
|
406
|
+
approve yet)`` and nothing is withheld (a `proposed` subject with no
|
|
407
|
+
`proposal.md`, and a `specced` subject, show no `brief:` line at all). Once nothing blocks, a
|
|
408
|
+
ship-ready verifying subject gets the agent's own
|
|
409
|
+
`canon subject status <id> shipped`; status never asks a human to ship.
|
|
316
410
|
|
|
317
411
|
Status fails closed rather than suggest progress the gate would refuse:
|
|
318
412
|
|
|
@@ -330,19 +424,24 @@ Status fails closed rather than suggest progress the gate would refuse:
|
|
|
330
424
|
|
|
331
425
|
```json
|
|
332
426
|
{
|
|
333
|
-
"statusVersion":
|
|
334
|
-
"canonVersion": "0.
|
|
427
|
+
"statusVersion": 2,
|
|
428
|
+
"canonVersion": "0.16.0",
|
|
335
429
|
"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
430
|
"warnings": [],
|
|
337
431
|
"subjects": [
|
|
338
432
|
{ "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": []
|
|
433
|
+
"reviewed": 1, "staleReviews": 0, "unsignedReviews": 0, "reviewDue": true, "openBlockers": 1, "missingCases": [],
|
|
434
|
+
"brief": { "approved": true, "enforced": false, "change": "c-cart", "proposal": "openspec/changes/c-cart/proposal.md", "approval": "Journey 2026-10-11 — \"approved\"" } },
|
|
435
|
+
{ "id": "promo", "title": "Promo", "status": "verifying", "scenarios": 1, "excluded": 0, "evidenced": 1, "divergent": 0,
|
|
436
|
+
"reviewed": 1, "staleReviews": 0, "unsignedReviews": 0, "reviewDue": true, "openBlockers": 0, "missingCases": [],
|
|
437
|
+
"brief": { "approved": false, "enforced": true, "change": "c-promo", "proposal": "openspec/changes/c-promo/proposal.md" } }
|
|
340
438
|
],
|
|
341
439
|
"unowned": ["cart.promo.01"],
|
|
342
440
|
"next": [
|
|
343
|
-
{ "command": "
|
|
344
|
-
{ "command": "canon
|
|
345
|
-
{ "command": "canon
|
|
441
|
+
{ "command": "stop and report the brief in openspec/changes/c-promo/proposal.md; build and attest nothing for promo until the human answers (one answer approves brief and policy)", "why": "promo: its newest adopted change's proposal.md carries no `Approved:` line …", "actor": "human" },
|
|
442
|
+
{ "command": "canon finding close --change-id c-cart --round 1 --seq 1 --disposition fixed --resolution-sha <sha> --actor-id <unit> --session-id <session>", "why": "cart: 1 open blocker finding(s) (first: …); fix it, commit, then close it", "actor": "agent" },
|
|
443
|
+
{ "command": "canon review add --project-id root --scenario-id cart.add.02 --reviewer <reviewer> --actor-id <reviewer> --session-id <review-session> --role reviewer --pin <sha> --original-spec-ref \"<feature file>\"", "why": "cart: 1 of 2 scenario(s) lack a qualifying review; …", "actor": "agent" },
|
|
444
|
+
{ "command": "canon status", "why": "1 scenario(s) carry no @subject tag (first: cart.promo.01); add `@subject:<id>` above each in its .feature file, then check again: …", "actor": "agent" }
|
|
346
445
|
],
|
|
347
446
|
"nextOmitted": 0
|
|
348
447
|
}
|
|
@@ -351,9 +450,22 @@ Status fails closed rather than suggest progress the gate would refuse:
|
|
|
351
450
|
Each `missingCases` entry is `{ "projectId", "surface", "case" }`: the
|
|
352
451
|
spec root, the `<area>.<surface>`, and the required case it lacks.
|
|
353
452
|
|
|
453
|
+
`brief` is present for `building`, `verifying` and `shipped` subjects,
|
|
454
|
+
and for a `proposed` subject whose newest adopted change has a
|
|
455
|
+
`proposal.md`. `change` is the newest adopted change (omitted when the
|
|
456
|
+
subject adopts none), `proposal` its `proposal.md` (omitted when no
|
|
457
|
+
`openspec` source holds one), `approval` the text after `Approved:`, and
|
|
458
|
+
`enforced` whether the missing approval blocks (a `proposal.md` exists to
|
|
459
|
+
carry it).
|
|
460
|
+
|
|
461
|
+
Each `next` entry's `actor` is `agent` (its `command` is a canon command
|
|
462
|
+
line) or `human` (its `command` says what to get from the human).
|
|
463
|
+
|
|
354
464
|
`policy.spec_coverage` has the shape `canon context --json` prints
|
|
355
465
|
(`null` when absent, `{"invalid": …}` when unusable). `statusVersion`
|
|
356
|
-
changes only when a field changes meaning or is removed.
|
|
466
|
+
changes only when a field changes meaning or is removed. It is `2`
|
|
467
|
+
since 0.16, when `next[].command` stopped always being a command line
|
|
468
|
+
(`actor` tells which); `brief` was added alongside.
|
|
357
469
|
|
|
358
470
|
`subject new`, `adopt` and `status` write their records directly; the
|
|
359
471
|
human-readable success line ends `— written directly; nothing to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@journeykit/canon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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.
|
|
34
|
-
"@journeykit/canon-core-linux-x64": "0.
|
|
35
|
-
"@journeykit/canon-core-win32-x64": "0.
|
|
33
|
+
"@journeykit/canon-core-darwin-arm64": "0.16.0",
|
|
34
|
+
"@journeykit/canon-core-linux-x64": "0.16.0",
|
|
35
|
+
"@journeykit/canon-core-win32-x64": "0.16.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^20.0.0",
|