@journeykit/canon 0.16.0 → 0.18.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 +73 -21
- package/dist/skills/reference/canon-authoring.md +2 -2
- package/dist/skills/reference/canon-fmt.md +2 -2
- package/dist/skills/reference/canon-gate.md +135 -29
- package/dist/skills/reference/canon-inventory.md +42 -4
- package/dist/skills/reference/canon-plan-import.md +56 -17
- package/dist/skills/reference/canon-review.md +29 -10
- package/dist/skills/reference/canon-storage.md +28 -4
- package/dist/skills/reference/canon-subject.md +188 -73
- package/package.json +4 -4
package/dist/skills/SKILL.src.md
CHANGED
|
@@ -25,7 +25,7 @@ a feature, a one-line request included. A human is needed once per scope:
|
|
|
25
25
|
`.canon/policy.yaml` (written by `canon init`), which is what the gate
|
|
26
26
|
grades the work against. When the human answers, record both at once:
|
|
27
27
|
an `Approved: <who> <when> — "<the human's words>"` line in the change's
|
|
28
|
-
|
|
28
|
+
`.canon/changes/<slug>/brief.md`, and
|
|
29
29
|
`canon policy approve --by <the human> --note "<their words>"`. Never
|
|
30
30
|
approve without a human answer, and never edit the policy to make the
|
|
31
31
|
gate pass.
|
|
@@ -36,6 +36,14 @@ a feature, a one-line request included. A human is needed once per scope:
|
|
|
36
36
|
stays in the approved change: add it as tasks in that change's
|
|
37
37
|
`tasks.md`, never with `canon change new`, because a new change is a new
|
|
38
38
|
scope and needs the human's approval.
|
|
39
|
+
- **A follow-up on a shipped subject reopens it.** Reopen it with
|
|
40
|
+
`canon subject status <id> verifying --reason "<why>" --actor-id <unit>`
|
|
41
|
+
(no human inside the approved scope; the reason and your id are
|
|
42
|
+
recorded), add the follow-up as tasks in the approved change's
|
|
43
|
+
`tasks.md`, then its scenario, evidence and an independent review, and
|
|
44
|
+
ship again with `canon subject status <id> shipped`, which runs every
|
|
45
|
+
guard again. `canon status` flags a shipped subject whose gate went red
|
|
46
|
+
and names the reopen.
|
|
39
47
|
|
|
40
48
|
When no human can answer one of the brief's questions, write the question
|
|
41
49
|
into the brief, state the assumption you will build on, and say so in your
|
|
@@ -44,17 +52,24 @@ work inside an approved scope, never to the approval itself.
|
|
|
44
52
|
|
|
45
53
|
1. **Brief.** For a new subject or a scope change, write down the request
|
|
46
54
|
verbatim, your assumptions, and the questions you could not ask. It
|
|
47
|
-
becomes the change's
|
|
48
|
-
`canon change new <slug> --subject <id> --title "…"`
|
|
49
|
-
subject exists (step 2).
|
|
55
|
+
becomes the change's `.canon/changes/<slug>/brief.md`, which
|
|
56
|
+
`canon change new <slug> --subject <id> --title "…"` writes once the
|
|
57
|
+
subject exists (step 2). A change that comes from an external plan the
|
|
58
|
+
human prepared (an openspec `proposal.md` read through
|
|
59
|
+
`plans.sources`) gets its brief there too: a `brief.md` whose
|
|
60
|
+
`Source: <path>` line points at the plan; canon never edits the plan.
|
|
61
|
+
Once the brief is there, stop and report it,
|
|
50
62
|
asking the human once for the brief and the policy; go on to step 3
|
|
51
63
|
only after the human answers, and record the answer as above. Inside an
|
|
52
64
|
approved scope there is no new brief: go to step 3 under the existing
|
|
53
65
|
subject and change.
|
|
54
66
|
2. **Subject.** `canon subject new <id> --domain <domain> --title "…"`, then
|
|
55
67
|
`canon change new <slug> --subject <id> --title "…"`, and put the brief in
|
|
56
|
-
the
|
|
57
|
-
where you stop until the human answers (step 1).
|
|
68
|
+
the `.canon/changes/<slug>/brief.md` it writes. For a new subject or a
|
|
69
|
+
scope change, this is where you stop until the human answers (step 1).
|
|
70
|
+
A change that also serves another subject is adopted there too,
|
|
71
|
+
`canon subject adopt <slug> --subject <other>`: it then belongs to both,
|
|
72
|
+
and its open blockers gate both.
|
|
58
73
|
3. **Scenarios.** `canon feature new <area>.<surface> --title "…"`, then one
|
|
59
74
|
`canon scenario new <area>.<surface>.<nn> --subject <id> --case <case> --title "…"`
|
|
60
75
|
per behavior, with failure paths (`--case failure`) as well as `happy`
|
|
@@ -66,7 +81,10 @@ work inside an approved scope, never to the approval itself.
|
|
|
66
81
|
one of them prints `NOT refreshed` (a format violation, such as a
|
|
67
82
|
hand-written header with no `# canon:` provenance comment, or a
|
|
68
83
|
duplicate id) run `canon inventory sync`: it names each problem; fix
|
|
69
|
-
them, and the next command indexes the files.
|
|
84
|
+
them, and the next command indexes the files. Specs live at
|
|
85
|
+
`<root>/kind=feature/area=<area>/<surface>.feature` under a
|
|
86
|
+
`specs.roots[]` root (default `.canon/specs`); a root still on
|
|
87
|
+
`features/kind=feature/` is refused until you run `canon migrate`.
|
|
70
88
|
4. **Units.** Split the work into units (for example specs, domain code,
|
|
71
89
|
assets, review). Give each unit its own actor id and session id, and pass
|
|
72
90
|
them on everything the unit records (`--actor-id <unit> --session-id <session>`),
|
|
@@ -81,12 +99,20 @@ work inside an approved scope, never to the approval itself.
|
|
|
81
99
|
`canon evidence add --project-id <root-id> --scenario-id <id> --kind test-run --role implementer --ref "<command>" --actor-id <unit> --session-id <session>`,
|
|
82
100
|
with `--report junit:<path>` or `--artifact <path>` when files prove it.
|
|
83
101
|
canon stores those bytes in `.canon/artifacts/sha256/` (commit it with the
|
|
84
|
-
ledger) and `canon gate check` re-checks them
|
|
102
|
+
ledger) and `canon gate check` re-checks them; the bound copy is the one
|
|
103
|
+
kept, so the working-tree report itself may be gitignored. For a scenario about what
|
|
85
104
|
users see, bind the screenshot or recording that shows it
|
|
86
105
|
(`--artifact <path>`). Under `write: { promote: auto }` (what
|
|
87
106
|
`canon init` writes) the record is final when the command returns; only
|
|
88
107
|
in a repo with `write.promote: manual` (or no `write:` key) follow it
|
|
89
108
|
with `canon gate promote`.
|
|
109
|
+
One test run that proves several scenarios is bound once:
|
|
110
|
+
`canon evidence add --project-id <root-id> --all-scenarios --report <junit|cucumber|swift-testing>:<path> --kind test-run --role implementer --ref "<command>" --actor-id <unit> --session-id <session>`
|
|
111
|
+
writes one record per indexed scenario owning a case, each with the
|
|
112
|
+
verdict its own cases show; any refusal writes none. When a new record
|
|
113
|
+
makes a signed review stale, the add prints the reviewer and the
|
|
114
|
+
`canon review add … --pin <sha>` that re-pins it; the reviewer looks
|
|
115
|
+
again before running it.
|
|
90
116
|
An external evaluator's audit is recorded with `canon evidence add --kind
|
|
91
117
|
audit`; canon never generates one.
|
|
92
118
|
Evidence for a plan task (`--task <id>`, before `canon gate task`) must
|
|
@@ -95,16 +121,24 @@ work inside an approved scope, never to the approval itself.
|
|
|
95
121
|
change's `tasks.md` as it is now, so a task row you add needs no
|
|
96
122
|
`canon ingest plans` first.
|
|
97
123
|
7. **Independent review.** A different session from the one that wrote the
|
|
98
|
-
code and evidence reviews the change. The
|
|
124
|
+
code and evidence reviews the change. The review session must be able
|
|
125
|
+
to run the checks it signs off (the build, the tests, the app); a
|
|
126
|
+
reviewer that cannot run the tests says so and does not sign off. The
|
|
127
|
+
reviewer records every defect
|
|
99
128
|
the review finds as a finding:
|
|
100
129
|
`canon finding add --change-id <slug> --round <n> --seq <n> --severity <blocker|should-fix|note> --reviewer <me> --actor-id <me> --session-id <session> --summary "…"`;
|
|
130
|
+
`canon finding list --change-id <slug> [--open] [--json]` shows the
|
|
131
|
+
change's findings (round, seq, severity, disposition, reviewer, summary),
|
|
132
|
+
so the next seq and the open blockers are one command away;
|
|
101
133
|
once fixed,
|
|
102
134
|
`canon finding close --change-id <slug> --round <n> --seq <n> --disposition fixed --resolution-sha <sha>`
|
|
103
135
|
(a short sha is fine; canon resolves it). With `write.promote: manual`,
|
|
104
136
|
follow each with `canon gate promote`. Every bug fix adds or extends a scenario. The
|
|
105
137
|
reviewer signs off each scenario:
|
|
106
138
|
`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>`
|
|
107
|
-
(written directly; nothing to promote).
|
|
139
|
+
(written directly; nothing to promote). Repeat `--scenario-id`, or pass
|
|
140
|
+
`--scenario-ids-from <file>`, to sign off several scenarios in one
|
|
141
|
+
call, all or nothing. A reviewer of user-visible
|
|
108
142
|
behavior attaches what it looked at with `--artifact <path>`
|
|
109
143
|
(repeatable); canon stores and re-checks those bytes and never compares
|
|
110
144
|
images.
|
|
@@ -114,17 +148,22 @@ work inside an approved scope, never to the approval itself.
|
|
|
114
148
|
blocker findings); fix the gap rather than overriding, and finish with
|
|
115
149
|
`canon gate check` clean. A root whose `.feature` edits could not be
|
|
116
150
|
indexed fails the gate and refuses `shipped` (`uncovered-cell <root>`;
|
|
117
|
-
see step 3). `shipped` also refuses while the change's
|
|
118
|
-
has no `Approved:` line
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
151
|
+
see step 3). `shipped` also refuses while the change's
|
|
152
|
+
`.canon/changes/<slug>/brief.md` has no `Approved:` line (for a change
|
|
153
|
+
from an external plan with no canon brief yet, it names the `brief.md`
|
|
154
|
+
to create with its `Source:` line), and `canon evidence add` refuses the
|
|
155
|
+
subject's scenarios and tasks the same way. Both are backstops: the stop
|
|
156
|
+
itself is at step 1, so you never reach them for a brief the human has
|
|
157
|
+
not answered. You record the line only from the human's answer (with no
|
|
158
|
+
brief to answer there is nothing to refuse, and the
|
|
123
159
|
ship guard only prints an advisory). Move the subject to
|
|
124
160
|
`shipped` yourself once `canon subject status <id> shipped` passes its
|
|
125
161
|
guard, unless the brief says the human ships. `canon status` shows
|
|
126
162
|
whether each subject's brief is approved, by the same check, and names
|
|
127
|
-
the next step.
|
|
163
|
+
the next step. `shipped → verifying` reopens a shipped subject for a
|
|
164
|
+
follow-up (see the working loop above): it needs `--reason`, and it is
|
|
165
|
+
refused, naming the brief, when the subject's newest change is not
|
|
166
|
+
approved.
|
|
128
167
|
|
|
129
168
|
Every write command says whether the record is final (`written directly`,
|
|
130
169
|
or `staged and promoted in this run`) or staged; only a staged record
|
|
@@ -137,13 +176,26 @@ Load only the reference the current step needs.
|
|
|
137
176
|
### Setup and provider projection
|
|
138
177
|
|
|
139
178
|
```bash
|
|
140
|
-
canon init # canon.yaml, starter policy (--policy light|standard|strict, default strict; see canon-policy), AGENTS.md block, plans
|
|
141
|
-
canon
|
|
142
|
-
canon skills install
|
|
179
|
+
canon init # canon.yaml, starter policy (--policy light|standard|strict, default strict; see canon-policy), AGENTS.md block, commented plans examples
|
|
180
|
+
canon init --providers=claude,codex # harnesses for the canon skill; without it init asks on a terminal, else takes the detected .claude/.codex/.omp/.pi
|
|
181
|
+
canon skills install # follows canon.yaml skills.providers (else the install lock's, else the detected ones; none detected installs nothing)
|
|
182
|
+
canon skills install --providers=claude,codex,omp,pi # any subset or none; rewrites skills.providers
|
|
143
183
|
canon skills check # read-only drift check
|
|
144
|
-
canon skills doctor # diagnostics; never deletes user files
|
|
184
|
+
canon skills doctor # diagnostics, incl. a stray canon projection for an unselected provider and a projection a formatter rewrote; never deletes user files
|
|
145
185
|
```
|
|
146
186
|
|
|
187
|
+
`install` and `doctor` end with `owned-paths:`, the globs of every path
|
|
188
|
+
canon writes. Add them to the repo's formatter and linter ignore lists; a
|
|
189
|
+
formatter that rewrites a projection or a ledger record makes canon read
|
|
190
|
+
drift. Specs, `policy.yaml` and the changes are written by hand and are not
|
|
191
|
+
on the list. `doctor` reports a projection whose bytes changed
|
|
192
|
+
but whose content did not as `formatter-rewritten` and names the ignore
|
|
193
|
+
entry to add; a changed file stays `stale`.
|
|
194
|
+
|
|
195
|
+
The harness list lives in `canon.yaml` as `skills: { providers: [...] }`.
|
|
196
|
+
`.agents` alone never selects Codex; `.codex` does. Change the list with
|
|
197
|
+
`canon skills install --providers=<list>`, never by assuming a default.
|
|
198
|
+
|
|
147
199
|
The canonical bundle is read-only. Claude receives
|
|
148
200
|
`.claude/skills/canon/SKILL.md`; Codex receives
|
|
149
201
|
`.agents/skills/canon/SKILL.md`; OMP receives `.omp/skills/canon/SKILL.md`; and
|
|
@@ -105,7 +105,7 @@ format-clean scenario, and `canon status`, `canon gate check`,
|
|
|
105
105
|
index added, edited and deleted `.feature` files before they read. When
|
|
106
106
|
one of them prints `NOT refreshed`, the corpus has a format violation
|
|
107
107
|
(for example a hand-written header with no `# canon:` line) or a
|
|
108
|
-
duplicate id; `canon inventory sync` (or `canon format
|
|
108
|
+
duplicate id; `canon inventory sync` (or `canon format <root>`) names it.
|
|
109
109
|
Fix it and the next command indexes the files (see `canon-inventory`).
|
|
110
110
|
|
|
111
111
|
- `--actor <id>` (or `CANON_ACTOR` in the environment) puts YOUR id on the
|
|
@@ -158,7 +158,7 @@ Fix it and the next command indexes the files (see `canon-inventory`).
|
|
|
158
158
|
## Worked example
|
|
159
159
|
|
|
160
160
|
```
|
|
161
|
-
specs/
|
|
161
|
+
.canon/specs/kind=feature/area=checkout/
|
|
162
162
|
cart.feature # surface: the cart screen
|
|
163
163
|
payment.feature # surface: the payment step
|
|
164
164
|
```
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
`canon format --check` is canon's read-only format validator over a
|
|
4
4
|
consumer-repo artifact corpus: ledger records (run/drill/review/clear/
|
|
5
5
|
code-review/design-review), divergence events (manifest/review/
|
|
6
|
-
remediation), `
|
|
6
|
+
remediation), `kind=feature/` spec files, `inventory/`, and `policy.yaml`. It never
|
|
7
7
|
rewrites or mutates the corpus — it only validates and reports.
|
|
8
8
|
|
|
9
9
|
## Running
|
|
@@ -17,7 +17,7 @@ a report grouped by failure class in this fixed order:
|
|
|
17
17
|
|
|
18
18
|
| Failure class | Meaning | Fix |
|
|
19
19
|
|---|---|---|
|
|
20
|
-
| `layout-grammar` | Wrong Hive path shape — a `
|
|
20
|
+
| `layout-grammar` | Wrong Hive path shape — a spec or `inventory/` file not under a `kind=<kind>/area=<area>/` prefix, a wrong leaf filename, or `assets.lock`'s ad-hoc format. A spec root still on the pre-0.17 layout (any `.feature` file under `<root>/features/`) is refused with one violation at path `features`. | Move the file under the correct `kind=…/area=…/` prefix with the expected leaf name. For a pre-0.17 root, run `canon migrate`, which moves `features/kind=feature/` to `<root>/kind=feature/`. |
|
|
21
21
|
| `missing-envelope` | A YAML file (`inventory/`, `policy.yaml`) has no top-level `schema`/`kind`/`at`/`actor` keys. | Add the four envelope keys at the top level. |
|
|
22
22
|
| `missing-provenance` | A `.feature` file's `Feature:`/`Scenario:` header has no `# canon: {...}` comment either directly above it (with its tags, no blank line) or directly below it. | Use `canon scenario new` / `canon feature new`, which stamp it; never hand-type the line. See `canon-authoring`. |
|
|
23
23
|
| `missing-actor` | No structured `actor` object — a bare `by` string, or absent. | Replace `by: <name>` with a structured `actor` object. |
|
|
@@ -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
|
|
|
@@ -63,7 +63,7 @@ sync` runs) and says so in one stderr line, silent when the index is
|
|
|
63
63
|
fresh:
|
|
64
64
|
|
|
65
65
|
```
|
|
66
|
-
canon gate check: refreshed the scenario index from 1 changed feature file(s) (specs/
|
|
66
|
+
canon gate check: refreshed the scenario index from 1 changed feature file(s) (.canon/specs/kind=feature/area=cart/add.feature): 1 record(s) written
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
A root that fails to sync (format violations, a duplicate id) is named
|
|
@@ -77,6 +77,15 @@ the same violation, and `canon status` lists the sync as its only step.
|
|
|
77
77
|
Fix what the sync lists and run the gate again. See `canon-storage`
|
|
78
78
|
(The scenario index).
|
|
79
79
|
|
|
80
|
+
A spec root still on the pre-0.17 layout (a `.feature` file under
|
|
81
|
+
`<root>/features/`) fails closed the same way, whether or not the index
|
|
82
|
+
already holds its scenarios: the gate adds `uncovered-cell <root> — spec
|
|
83
|
+
root `<root>` (`<path>`) is on the pre-0.17 layout (…)…; until then its
|
|
84
|
+
scenarios are not graded` and exits 1, the ship guard refuses with the
|
|
85
|
+
same violation, and `canon status` names `canon migrate` as its only
|
|
86
|
+
step. `canon migrate` moves `features/kind=feature/` to
|
|
87
|
+
`<root>/kind=feature/`; run it, then the gate again.
|
|
88
|
+
|
|
80
89
|
### The spec-derived worklist (`spec_coverage`, opt-in)
|
|
81
90
|
|
|
82
91
|
Coverage groups the EVIDENCE corpus, so it can only ever enumerate
|
|
@@ -246,19 +255,32 @@ reports it, and builds nothing for that scope until the human answers.
|
|
|
246
255
|
`canon evidence add` is the backstop for that stop (0.16 M7). It
|
|
247
256
|
refuses (exit `1`, nothing staged or stored) while a subject the record
|
|
248
257
|
attests for has an enforced, unapproved brief: the newest adopted
|
|
249
|
-
change
|
|
250
|
-
|
|
258
|
+
change's canon `.canon/changes/<change>/brief.md` has no `Approved:`
|
|
259
|
+
line, or there is no canon `brief.md` yet but the change comes from an
|
|
260
|
+
external openspec plan with a `proposal.md` under the plan sources (the
|
|
261
|
+
check `canon status` and the ship guard use; see
|
|
251
262
|
`canon-subject`). The subjects are the scenario's `@subject` owner for
|
|
252
263
|
`--scenario-id`, and every subject adopting the task's change for
|
|
253
264
|
`--task`:
|
|
254
265
|
|
|
255
266
|
```
|
|
256
|
-
canon evidence add: unreviewed-promotion demo-subject — evidence for world.demo.01: brief not approved —
|
|
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
|
|
267
|
+
canon evidence add: unreviewed-promotion demo-subject — evidence for world.demo.01: brief not approved — .canon/changes/c-demo/brief.md has no `Approved:` line, so the human has not answered it; once they do, record their words as `Approved: <who> <when> — "<their words>"` in .canon/changes/c-demo/brief.md
|
|
268
|
+
canon evidence add: refused — stop, report the brief to the human and wait for their answer: nothing is built or attested for a subject before its brief is approved. Record their words as `Approved: <who> <when> — "<their words>"` in that brief.md only from that answer; to attest anyway, pass --override-reason <text>, which is recorded on the evidence record
|
|
258
269
|
```
|
|
259
270
|
|
|
260
|
-
|
|
261
|
-
|
|
271
|
+
For a change from an external plan with no canon brief yet, the first
|
|
272
|
+
line says instead ``.canon/changes/c-ext/brief.md does not exist yet:
|
|
273
|
+
change `c-ext` comes from the external plan
|
|
274
|
+
openspec/changes/c-ext/proposal.md, which canon reads and never edits, so
|
|
275
|
+
its approval lives in that canon file, so the human has not answered it;
|
|
276
|
+
once they do, create .canon/changes/c-ext/brief.md holding a `Source:
|
|
277
|
+
openspec/changes/c-ext/proposal.md` line and record their words there as
|
|
278
|
+
`Approved: <who> <when> — "<their words>"` ``. An `Approved:` line in the
|
|
279
|
+
external `proposal.md` itself is never read.
|
|
280
|
+
|
|
281
|
+
A subject with no brief to answer (no adopted change, or a newest change
|
|
282
|
+
with neither a canon `brief.md` nor an external `proposal.md`) and a
|
|
283
|
+
scenario no subject owns are unaffected.
|
|
262
284
|
`canon review add` and `canon finding add` are never refused.
|
|
263
285
|
`--override-reason "<one line>"` attests anyway, prints the `waived …`
|
|
264
286
|
lines and `override recorded by `<actor>` for the N brief refusal(s)
|
|
@@ -298,8 +320,9 @@ review waivers: 1 advisory(ies) — not failing the gate:
|
|
|
298
320
|
The waiver lasts only until the subject's next status write.
|
|
299
321
|
|
|
300
322
|
`canon subject status <id> shipped` also refuses where a brief exists and
|
|
301
|
-
has no `Approved:` line (the subject's newest adopted change has a
|
|
302
|
-
`
|
|
323
|
+
has no `Approved:` line (the subject's newest adopted change has a canon
|
|
324
|
+
`brief.md`, or an external `proposal.md` and no canon `brief.md` yet;
|
|
325
|
+
see `canon-subject`), as an `unreviewed-promotion` naming
|
|
303
326
|
the subject. With no brief to answer it prints an advisory and does not
|
|
304
327
|
refuse. Like the evidence refusal above, it is a backstop: the stop is at
|
|
305
328
|
the brief. `canon gate check` does not grade briefs, so a waived ship
|
|
@@ -317,7 +340,10 @@ scenario) that carry no pins were written before 0.15. Each still names
|
|
|
317
340
|
`scenario_digest`) equals its current digest, and no declared evidence
|
|
318
341
|
record of the scenario is newer than the review. It counts. When the
|
|
319
342
|
scenario lived in another `.feature` file at that commit, the spec
|
|
320
|
-
root's files at that commit are searched for its id
|
|
343
|
+
root's files at that commit are searched for its id, then the files
|
|
344
|
+
under each of the root's `former_roots` (where `canon migrate` moved
|
|
345
|
+
it from, e.g. `specs` for a root now at `.canon/specs`). Only the
|
|
346
|
+
root's own directories are searched, never another root's.
|
|
321
347
|
- **stale** — the scenario changed since the commit, or it has newer
|
|
322
348
|
evidence. It no longer counts, and an in-scope scenario left with no
|
|
323
349
|
other qualifying review is `unreviewed-promotion`:
|
|
@@ -365,6 +391,8 @@ canon evidence add --scenario-id cart.add.04 --project-id root \
|
|
|
365
391
|
# a JUnit XML or Cucumber JSON report: also records the matched case + outcome
|
|
366
392
|
canon evidence add ... --report junit:target/nextest/default/junit.xml
|
|
367
393
|
canon evidence add ... --report cucumber:reports/cucumber.json
|
|
394
|
+
# a Swift Testing event stream (swift test --experimental-event-stream-output <path>)
|
|
395
|
+
canon evidence add ... --report swift-testing:.build/events.jsonl
|
|
368
396
|
```
|
|
369
397
|
|
|
370
398
|
A case is bound when the scenario id is its OWN: the first
|
|
@@ -382,6 +410,12 @@ verdict when ANY bound case failed or was skipped refuses (exit 1). Files
|
|
|
382
410
|
must be inside the repository, are recorded by relative path, and are
|
|
383
411
|
stored (see Bound evidence stays provable).
|
|
384
412
|
|
|
413
|
+
A Swift Testing case is named by its display name (`@Test("cart.add.04
|
|
414
|
+
refuses an out-of-stock item")`), else its function name
|
|
415
|
+
(`cart_add_04_refuses()`). A test with a failing issue failed, a skipped
|
|
416
|
+
test was skipped, a test that started and ended without one passed, and
|
|
417
|
+
a test that started and never ended failed.
|
|
418
|
+
|
|
385
419
|
Strength, weakest first: `attested` (no attachment) < `artifact` (any
|
|
386
420
|
bound file) < `report` (a parsed report whose case passed). The policy
|
|
387
421
|
decides whether the gate cares:
|
|
@@ -405,6 +439,55 @@ violation in every mode. Canon still cannot know whether the bound test
|
|
|
405
439
|
actually exercises the scenario — binding narrows the trust gap, it does
|
|
406
440
|
not close it.
|
|
407
441
|
|
|
442
|
+
### One report, many scenarios (0.18)
|
|
443
|
+
|
|
444
|
+
One test run proves many scenarios. Bind it once:
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
canon evidence add --project-id root --all-scenarios \
|
|
448
|
+
--report junit:target/nextest/default/junit.xml \
|
|
449
|
+
--kind test-run --ref "cargo nextest run" --role implementer
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Each indexed scenario of `--project-id` that owns a case (the rule above)
|
|
453
|
+
gets its own record, with only its own cases. Its verdict comes from
|
|
454
|
+
them: `faithful` when every case passed, `divergent` when one failed,
|
|
455
|
+
`not-applicable` when one was skipped and none failed. `--verdict <v>`
|
|
456
|
+
claims the same verdict for every record instead. A case whose id is not
|
|
457
|
+
indexed is named and skipped (`skipped <id> — not an indexed scenario`);
|
|
458
|
+
a report no indexed scenario owns is refused (exit 2). The records share
|
|
459
|
+
the stored report and every `--artifact`, and the output lists each
|
|
460
|
+
scenario with its verdict on its own line.
|
|
461
|
+
|
|
462
|
+
It is all or nothing: a claimed `faithful` verdict over a failed case,
|
|
463
|
+
a fabrication marker in `--summary`, an unapproved brief for any
|
|
464
|
+
scenario's subject, or a scenario whose latest record binds a file the
|
|
465
|
+
batch does not refuses the whole batch, and nothing is written.
|
|
466
|
+
`--scenario-id`, `--task`, `--report-case`, `--kind audit`,
|
|
467
|
+
`--drop-attachment` and the approval flags name one subject and are
|
|
468
|
+
refused with `--all-scenarios`.
|
|
469
|
+
|
|
470
|
+
A task's evidence with `--report` and no `--scenario-id` binds EVERY case
|
|
471
|
+
in the report, so a `faithful` verdict needs all of them to pass.
|
|
472
|
+
|
|
473
|
+
### A write that makes a review stale says so (0.18)
|
|
474
|
+
|
|
475
|
+
When new evidence (one record or a batch) supersedes the evidence a
|
|
476
|
+
current pinned review saw, `canon evidence add` prints one line per
|
|
477
|
+
review it makes stale, with the command that re-pins it:
|
|
478
|
+
|
|
479
|
+
```
|
|
480
|
+
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
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
The decision is the gate's own (the same stale-review rule `canon gate
|
|
484
|
+
check` reports). The command repeats the review's reviewer, actor, role,
|
|
485
|
+
session, provenance ref, runtime labels and artifacts, pinned at `HEAD` (`<sha>`
|
|
486
|
+
when the repository has no commit). A record that is only staged says
|
|
487
|
+
the review "goes stale once this evidence is promoted": promote first,
|
|
488
|
+
then re-pin. The reviewer re-reviews before re-pinning; several
|
|
489
|
+
scenarios re-pin in one `canon review add` (see `canon-review`).
|
|
490
|
+
|
|
408
491
|
## Bound evidence stays provable (the artifact store)
|
|
409
492
|
|
|
410
493
|
`canon evidence add --artifact/--report` copies each bound file into
|
|
@@ -442,20 +525,29 @@ have its bindings checked. A superseded record is not checked —
|
|
|
442
525
|
re-attesting is the fix.
|
|
443
526
|
|
|
444
527
|
Files bound to a review (`canon review add --artifact`, see
|
|
445
|
-
`canon-review`) are checked the same
|
|
446
|
-
latest per `(project_id, scenario_id, reviewer)
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
528
|
+
`canon-review`) are checked by the same rule for every CURRENT in-effect
|
|
529
|
+
review: the latest per `(project_id, scenario_id, reviewer)`, less any
|
|
530
|
+
that went stale against its scenario (its spec or evidence pin moved, or
|
|
531
|
+
a review from before pinning is stale at its commit). A stale review is
|
|
532
|
+
superseded — the gate reads the newer review, or reports the missing
|
|
533
|
+
one as `unreviewed-promotion` — so its files are not re-checked, as a
|
|
534
|
+
superseded evidence record's are not. A missing blob whose working-tree
|
|
535
|
+
file still matches is an advisory, listed with the reviewer:
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
unstored cart.add.01 reports/cart-badge.png (sha256 9848…, review by `reviewer-2`) is verified against the working tree only
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Anything else is `stale-evidence` on the review's scenario, and the
|
|
542
|
+
detail names the review: ``review by `<reviewer>` (actor `<id>`,
|
|
543
|
+
<time>)``.
|
|
452
544
|
|
|
453
545
|
`canon evidence vault [--max-artifact-mib <N>]` stores the bytes for
|
|
454
|
-
existing records: every attachment whose
|
|
455
|
-
its digest is copied in, and everything
|
|
456
|
-
with both digests. It exits `0` whenever it
|
|
457
|
-
an unstored file matters), `2` when the
|
|
458
|
-
fails.
|
|
546
|
+
existing evidence records and current reviews (a superseded review's files are not vaulted): every attachment whose
|
|
547
|
+
working-tree file still matches its digest is copied in, and everything
|
|
548
|
+
else is listed as not stored, with both digests. It exits `0` whenever it
|
|
549
|
+
ran (the gate decides whether an unstored file matters), `2` when the
|
|
550
|
+
ledger cannot be read or a write fails.
|
|
459
551
|
|
|
460
552
|
## Effect-aware risk approvals (`risk_tiers`)
|
|
461
553
|
|
|
@@ -489,12 +581,19 @@ empty clean map.
|
|
|
489
581
|
## `canon gate task <task_id> [--repo <dir>]`
|
|
490
582
|
|
|
491
583
|
The evidence-gated task checkbox flip. Resolves `<task_id>`
|
|
492
|
-
(`<change_id>#<n>`) through
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
584
|
+
(`<change_id>#<n>`) through `.canon/changes/` (canon's own changes, read
|
|
585
|
+
first whenever the directory exists, with no `plans:` entry) and then
|
|
586
|
+
`canon.yaml`'s `plans.sources` — the first dialect whose `PlanWriteBack`
|
|
587
|
+
locates the task's document wins, so the flip is plan-dialect agnostic
|
|
588
|
+
(canon's own `tasks.md`, external `openspec` change dirs, `superpowers`
|
|
589
|
+
plan docs). A repo with no `plans:` section falls back to the compat
|
|
590
|
+
default `[{ dialect: openspec, root: <repo> }]`; no source locating the
|
|
591
|
+
task at all is a loud failure naming every source consulted. A repo with
|
|
592
|
+
no `.canon/changes/` and `sources: []` is refused: "this repo configures
|
|
593
|
+
no plan sources (no `.canon/changes` and no `plans.sources` entries in
|
|
594
|
+
canon.yaml) … write the change with `canon change new`, or list your
|
|
595
|
+
external plans under `plans.sources`" (`canon dispatch --task` says the
|
|
596
|
+
same).
|
|
498
597
|
It requires
|
|
499
598
|
a matching non-`Divergent` evidence record, and flips `- [ ]` → `- [x]`
|
|
500
599
|
with an appended evidence note ONLY on a clean check. The note
|
|
@@ -526,6 +625,13 @@ canon gate task my-change#5.2 --repo .
|
|
|
526
625
|
This is canon's own authority for the checkbox grammar — always flip
|
|
527
626
|
through `canon gate task`, never by hand.
|
|
528
627
|
|
|
628
|
+
Canon only reads external plans, with this one exception: for a task
|
|
629
|
+
from an external plan source, `canon gate task` flips the checkbox in
|
|
630
|
+
that plan's own `tasks.md` (or plan document). It is the one place canon
|
|
631
|
+
writes into an external plan; their briefs and approvals still live in
|
|
632
|
+
`.canon/changes/<change>/brief.md`. (Open for the maintainer: record
|
|
633
|
+
external flips only in the ledger instead.)
|
|
634
|
+
|
|
529
635
|
## `canon gate promote [--repo <dir>] [--dry-run]`
|
|
530
636
|
|
|
531
637
|
Staging → committed: every well-formed record under
|
|
@@ -13,7 +13,7 @@ deleted `.feature` files themselves, with the same code this command runs,
|
|
|
13
13
|
scoped to the changed files, and say so in one stderr line:
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
canon status: refreshed the scenario index from 1 changed feature file(s) (specs/
|
|
16
|
+
canon status: refreshed the scenario index from 1 changed feature file(s) (.canon/specs/kind=feature/area=cart/add.feature): 1 record(s) written
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
So the working loop has no sync step and no `canon format` step: write the
|
|
@@ -25,12 +25,43 @@ command. Run `canon inventory sync` when:
|
|
|
25
25
|
comment, an emptied file, …) or a duplicate id, so its files were not
|
|
26
26
|
indexed. `canon gate check` then fails (`uncovered-cell <root>`),
|
|
27
27
|
`shipped` is refused and `canon status` lists only the sync. `canon
|
|
28
|
-
inventory sync` names each problem (`canon format
|
|
28
|
+
inventory sync` names each problem (`canon format <root>` names the format ones);
|
|
29
29
|
fix them and the next command indexes the files;
|
|
30
30
|
- `canon status` says the ledger holds no scenario records while the
|
|
31
31
|
`.feature` files hold some (the same refusal, on an index never built);
|
|
32
32
|
- you want the whole index rebuilt from every root at once.
|
|
33
33
|
|
|
34
|
+
A spec root still on the pre-0.17 layout (a `.feature` file under
|
|
35
|
+
`<root>/features/`) is not refreshed and fails closed the same way: the
|
|
36
|
+
reader names the root and `canon migrate` on stderr and writes no
|
|
37
|
+
tombstones, `canon gate check` fails (`uncovered-cell <root>`) whether or
|
|
38
|
+
not the index already holds its scenarios, `shipped` is refused, and
|
|
39
|
+
`canon status` lists only `canon migrate`. `canon inventory sync` does not
|
|
40
|
+
fix it; run `canon migrate`, which moves `features/kind=feature/` to
|
|
41
|
+
`<root>/kind=feature/`.
|
|
42
|
+
|
|
43
|
+
`canon migrate --dry-run` prints the plan without writing anything, and
|
|
44
|
+
`canon migrate` applies it. Commit first: it refuses while a path it
|
|
45
|
+
moves has uncommitted changes, and when a destination already exists.
|
|
46
|
+
A root at the repo-level `specs` moves to `.canon/specs`, and
|
|
47
|
+
`canon.yaml` records `former_roots: [specs]`, so reviews pinned before
|
|
48
|
+
the move still find their scenarios. Changes `canon change new` wrote
|
|
49
|
+
under `openspec/changes/` move to `.canon/changes/<slug>/`
|
|
50
|
+
(`proposal.md` becomes `brief.md`). An `Approved:` line in an external
|
|
51
|
+
`proposal.md` is copied into a canon pointer `brief.md`. The command
|
|
52
|
+
also writes `skills.providers` from the install lock, adds the missing
|
|
53
|
+
canon `.gitignore` lines, and ends with a full index rebuild. It is
|
|
54
|
+
idempotent, and a run that stopped partway is finished by the next run:
|
|
55
|
+
`canon.yaml` is written atomically, a half-moved change is completed,
|
|
56
|
+
and the openspec source `canon init` wrote is removed only after every
|
|
57
|
+
pointer brief is written and nothing unread is left under
|
|
58
|
+
`openspec/changes/` (each unread directory is named).
|
|
59
|
+
|
|
60
|
+
The reader's refresh treats a file as stale when its latest index
|
|
61
|
+
generation has another `source_digest` or `scenario_digest`. A
|
|
62
|
+
generation written before 0.15 has no `scenario_digest`, so a repo that
|
|
63
|
+
kept an old index gets it rewritten on the first read.
|
|
64
|
+
|
|
34
65
|
## Where the index sits in the loop
|
|
35
66
|
|
|
36
67
|
```
|
|
@@ -66,7 +97,7 @@ Per configured root, in order:
|
|
|
66
97
|
1. **Validate** — runs `canon format --check` over the root. ANY violation
|
|
67
98
|
(missing provenance, layout-grammar, …) ABORTS THE WHOLE ROOT: zero
|
|
68
99
|
records written for it, the violation(s) reported. Never a partial sync.
|
|
69
|
-
2. **Scan** — walks `<root>/
|
|
100
|
+
2. **Scan** — walks `<root>/kind=feature/area=<area>/<surface>.feature`, pairing each
|
|
70
101
|
`@<area>.<surface>.<nn>` tag with its header label as `title` and a
|
|
71
102
|
`source_digest` (sha256 over the raw `.feature` bytes). `@subject:<id>`
|
|
72
103
|
→ `subject_id`, `@lane:<v>` → `lane`, `@case:<v>` → `case` (first tag
|
|
@@ -100,7 +131,10 @@ specs:
|
|
|
100
131
|
root: apps/b/specs
|
|
101
132
|
```
|
|
102
133
|
|
|
103
|
-
- **Absent `specs:` key** → the single default root
|
|
134
|
+
- **Absent `specs:` key** → the single default root
|
|
135
|
+
`{id: root, root: .canon/specs}`, the one `canon init` writes. An entry
|
|
136
|
+
with no `root:` also resolves to `.canon/specs`. The default root is
|
|
137
|
+
authored and git-tracked; no canon gitignore line covers it.
|
|
104
138
|
- **Present `specs:` with an empty/missing `roots[]`** → fails LOUD; a
|
|
105
139
|
present-but-incomplete config never silently resolves to zero roots.
|
|
106
140
|
- **A malformed entry** (missing `id`, `roots` not a list, `id` not a
|
|
@@ -113,6 +147,10 @@ specs:
|
|
|
113
147
|
|
|
114
148
|
- **A `canon format --check` validation violation** in the root's corpus →
|
|
115
149
|
whole root aborts, 0 writes, violations reported.
|
|
150
|
+
- **A root on the pre-0.17 layout** (a `.feature` file under
|
|
151
|
+
`<root>/features/`) → a `layout-grammar` violation at path `features`;
|
|
152
|
+
the root aborts, 0 writes, exit 1. Run `canon migrate`. An empty
|
|
153
|
+
leftover `features/` directory is not a refusal.
|
|
116
154
|
- **A duplicate `scenario_id`** within one root → that root aborts, 0
|
|
117
155
|
writes, reported as a sync error (a distinct lane from fmt violations).
|
|
118
156
|
|
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
# canon-plan-import
|
|
2
2
|
|
|
3
|
-
`canon ingest plans` imports
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
`canon ingest plans` imports plan state into canon's `change`/`task`
|
|
4
|
+
records, through the same validated path every other ingest family uses:
|
|
5
|
+
canon's own changes under `.canon/changes/`, and any FOREIGN plan dialect
|
|
6
|
+
listed in `canon.yaml`. Imported rows carry no special marker — once
|
|
6
7
|
imported, `canon query` cannot tell an imported row from a hand-authored
|
|
7
8
|
one, and neither can `canon gate`.
|
|
8
9
|
|
|
9
|
-
## 1.
|
|
10
|
-
|
|
10
|
+
## 1. Canon's own changes and external `plans:` sources
|
|
11
|
+
|
|
12
|
+
`.canon/changes/` is canon's native plan source (dialect id `canon`):
|
|
13
|
+
one `<slug>/` directory per change holding `brief.md` and `tasks.md`,
|
|
14
|
+
written by `canon change new` (see `canon-subject`). Every plan reader —
|
|
15
|
+
`canon ingest plans`, `canon gate task`, `canon evidence add --task`,
|
|
16
|
+
`canon dispatch --task`, `canon status` and the brief check — reads it
|
|
17
|
+
first whenever the directory exists, with no `plans:` entry. Being first,
|
|
18
|
+
it owns a change id an external source also carries (the external copy
|
|
19
|
+
is the later duplicate, skipped and counted under `duplicate_change_id`).
|
|
20
|
+
Its records carry the actor `canon-plan-import-canon`; an external
|
|
21
|
+
openspec change's carry `canon-plan-import-openspec`. A `brief.md` with a
|
|
22
|
+
`Source: <path>` line is not a change of its own: it is the canon brief
|
|
23
|
+
of a change imported from an external plan (it points at the external
|
|
24
|
+
document and holds the human's `Approved:` line), so the native reader
|
|
25
|
+
skips that directory and the external source keeps the change id.
|
|
26
|
+
|
|
27
|
+
`plans.sources` lists only EXTERNAL plans — documents you prepare outside
|
|
28
|
+
canon, which canon only reads. The one exception: `canon gate task`
|
|
29
|
+
flips the task's checkbox in an external plan's `tasks.md` (see
|
|
30
|
+
`canon-gate`). Their briefs and approvals live in
|
|
31
|
+
`.canon/changes/<change>/brief.md`, never in the external document.
|
|
11
32
|
`canon.yaml`:
|
|
12
33
|
|
|
13
34
|
```yaml
|
|
@@ -17,17 +38,22 @@ plans:
|
|
|
17
38
|
root: . # resolved relative to canon.yaml's directory
|
|
18
39
|
```
|
|
19
40
|
|
|
20
|
-
- `canon init` writes
|
|
21
|
-
|
|
22
|
-
|
|
41
|
+
- `canon init` writes `sources: []`, preceded by comments explaining
|
|
42
|
+
`.canon/changes/` and commented examples for `openspec` (`root: .`) and
|
|
43
|
+
`superpowers` (`root: docs/superpowers/plans`). It creates no
|
|
44
|
+
`openspec/changes/` directory.
|
|
45
|
+
- An absent `plans:` section = zero external sources (a clean no-op;
|
|
46
|
+
`.canon/changes/` is still read).
|
|
23
47
|
- A present section parses STRICTLY: a typo'd key, an unregistered
|
|
24
48
|
`dialect`, or a nonexistent `root` fails the command loud, naming the
|
|
25
|
-
offender.
|
|
49
|
+
offender. `canon` is not a registered dialect: a source naming it is
|
|
50
|
+
refused, since `.canon/changes/` needs no entry.
|
|
26
51
|
- `root` is whatever the dialect's adapter scans — for `openspec`, a
|
|
27
52
|
repo root containing `openspec/changes/`, or that changes dir directly.
|
|
28
53
|
|
|
29
|
-
One-shot override (bypasses `canon.yaml` for a
|
|
30
|
-
either flag alone
|
|
54
|
+
One-shot override (bypasses `canon.yaml` and `.canon/changes/` for a
|
|
55
|
+
single ad hoc import, reading only the source it names; either flag alone
|
|
56
|
+
fails loud):
|
|
31
57
|
|
|
32
58
|
```bash
|
|
33
59
|
canon ingest plans --dialect openspec --source ../other-repo
|
|
@@ -36,7 +62,7 @@ canon ingest plans --dialect openspec --source ../other-repo
|
|
|
36
62
|
## 2. Run the import
|
|
37
63
|
|
|
38
64
|
```bash
|
|
39
|
-
canon ingest plans # canon
|
|
65
|
+
canon ingest plans # .canon/changes/ + every configured source
|
|
40
66
|
canon ingest plans --json # machine-readable outcome
|
|
41
67
|
canon ingest plans --repo ../svc # a specific repo root
|
|
42
68
|
```
|
|
@@ -60,9 +86,19 @@ canon query --kind task [--json]
|
|
|
60
86
|
No dialect/plugin filter exists or is needed — an imported `change`/
|
|
61
87
|
`task` is an ordinary core record.
|
|
62
88
|
|
|
89
|
+
## The `canon` dialect (`.canon/changes/`)
|
|
90
|
+
|
|
91
|
+
Uses the `openspec` change-dir grammar below, with three differences:
|
|
92
|
+
the brief is `brief.md` (its `## Why` first paragraph is the summary; a
|
|
93
|
+
missing one is a `missing-brief-md` malformed dir), the source root is
|
|
94
|
+
always `.canon/changes/` itself (archived changes under
|
|
95
|
+
`.canon/changes/archive/`), and a `brief.md` carrying a `Source:` line is
|
|
96
|
+
skipped (a pointer brief, above). `tasks.md` uses the same checkbox row
|
|
97
|
+
grammar.
|
|
98
|
+
|
|
63
99
|
## The `openspec` dialect
|
|
64
100
|
|
|
65
|
-
Maps `openspec/changes/**` onto core kinds:
|
|
101
|
+
Maps an external `openspec/changes/**` onto core kinds:
|
|
66
102
|
|
|
67
103
|
| openspec construct | Core mapping |
|
|
68
104
|
| --- | --- |
|
|
@@ -76,7 +112,9 @@ Maps `openspec/changes/**` onto core kinds:
|
|
|
76
112
|
A missing/unreadable `proposal.md`, or a basename that isn't a valid
|
|
77
113
|
`change_id`, skips the WHOLE dir (counted `malformed`); siblings
|
|
78
114
|
unaffected. A proposal-only dir imports as `Change { status: proposed }`
|
|
79
|
-
with zero tasks and zero diagnostics.
|
|
115
|
+
with zero tasks and zero diagnostics. An `Approved:` line in an external
|
|
116
|
+
`proposal.md` is not read: the change's approval lives in
|
|
117
|
+
`.canon/changes/<change>/brief.md` (see `canon-subject`).
|
|
80
118
|
|
|
81
119
|
## The `superpowers` dialect
|
|
82
120
|
|
|
@@ -136,9 +174,10 @@ downstream reader's job.
|
|
|
136
174
|
|
|
137
175
|
## What this skill does NOT cover
|
|
138
176
|
|
|
139
|
-
- **Authoring
|
|
140
|
-
|
|
141
|
-
subject (see `canon-subject`);
|
|
177
|
+
- **Authoring a change** — `canon change new` writes
|
|
178
|
+
`.canon/changes/<slug>/brief.md` and `tasks.md` and adopts the change
|
|
179
|
+
into a subject (see `canon-subject`); it never scaffolds an openspec
|
|
180
|
+
change dir. This skill covers IMPORTING plans.
|
|
142
181
|
- **`canon gate check`/`canon gate task`** (the hand-authored one-task-at-
|
|
143
182
|
a-time path this complements at bulk) — see `canon-gate`.
|
|
144
183
|
- **Wiring an imported `Task`/`Change` into a `canon gate` decision** —
|
|
@@ -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
|
|
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-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
`canon
|
|
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.
|
|
188
|
-
|
|
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
|
|
|
@@ -104,11 +104,12 @@ canon query --kind trajectory --json
|
|
|
104
104
|
|
|
105
105
|
canon exposes layered `stg_`/`int_`/`mart_` query views; `canon report`
|
|
106
106
|
builds its numbers from the `mart_` views. Those read ONLY local roots
|
|
107
|
-
— the git ledger
|
|
107
|
+
— the git ledger, the sqlite `hot` db file (local, no credential; read
|
|
108
|
+
since 0.18) and a LOCAL `.canon/r2` parquet mirror. `canon report`
|
|
108
109
|
never opens a live DB connection or the live bucket, so:
|
|
109
110
|
|
|
110
|
-
- Kinds routed to `hot`
|
|
111
|
-
report
|
|
111
|
+
- Kinds routed to a postgres `hot` rung do NOT appear in `canon
|
|
112
|
+
report`; kinds routed to `canon init`'s default sqlite `hot` rung do.
|
|
112
113
|
- Kinds routed to `cold` (s3) appear only if a local `.canon/r2` mirror
|
|
113
114
|
has been separately materialized — there is no automatic sync.
|
|
114
115
|
|
|
@@ -192,6 +193,29 @@ write (promote: manual)`, or `[FAIL] write: …`. `canon gate promote` still
|
|
|
192
193
|
works under `auto` and is idempotent: with nothing staged it prints
|
|
193
194
|
`nothing staged` and exits `0`.
|
|
194
195
|
|
|
196
|
+
## Harnesses: the `skills:` section
|
|
197
|
+
|
|
198
|
+
`canon.yaml`'s `skills:` section names the harnesses the canon skill is
|
|
199
|
+
installed for (0.17):
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
skills:
|
|
203
|
+
providers: [claude, codex] # any of claude, codex, omp, pi; [] for none
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`canon init` writes it: on a terminal it asks, with the harnesses whose
|
|
207
|
+
`.claude`, `.codex`, `.omp` or `.pi` directory exists checked;
|
|
208
|
+
`--providers a,b` (or `none`) answers without asking; with neither it takes
|
|
209
|
+
the detected ones, and with none detected it writes only a commented example
|
|
210
|
+
and installs nothing. `.agents` alone never selects Codex. `canon skills
|
|
211
|
+
install|check|doctor` follow the section; `canon skills install
|
|
212
|
+
--providers=<list>` rewrites it in place. Without the section they use the
|
|
213
|
+
install lock's providers, then the detected ones; nothing undetected is ever
|
|
214
|
+
added. A present section that is not this shape, or names another harness,
|
|
215
|
+
fails those commands (exit `1`), and `canon init --check-config` reports
|
|
216
|
+
`[PASS] skills (providers: …)`, `[not configured] skills (…)`, or
|
|
217
|
+
`[FAIL] skills: …`.
|
|
218
|
+
|
|
195
219
|
## The scenario index
|
|
196
220
|
|
|
197
221
|
`canon inventory sync` materializes one `Scenario` record per
|
|
@@ -207,7 +231,7 @@ that no `.feature` file of it declares any more. The reader then runs
|
|
|
207
231
|
covers the whole root) and prints one stderr line:
|
|
208
232
|
|
|
209
233
|
```
|
|
210
|
-
canon status: refreshed the scenario index from 1 changed feature file(s) (specs/
|
|
234
|
+
canon status: refreshed the scenario index from 1 changed feature file(s) (.canon/specs/kind=feature/area=cart/add.feature): 1 record(s) written
|
|
211
235
|
```
|
|
212
236
|
|
|
213
237
|
A deleted scenario is not erased: `canon inventory sync` — and a reader's
|
|
@@ -103,7 +103,7 @@ this subject and domain — the exact memory step 2 retrieves next time.
|
|
|
103
103
|
|
|
104
104
|
## Status lifecycle + the shipped evidence gate
|
|
105
105
|
|
|
106
|
-
`canon subject status <id> <state> [--override-reason <text>] [--actor-id <id>]`
|
|
106
|
+
`canon subject status <id> <state> [--override-reason <text>] [--reason <text>] [--actor-id <id>]`
|
|
107
107
|
performs a policy-gated transition.
|
|
108
108
|
The legal chain:
|
|
109
109
|
|
|
@@ -111,9 +111,10 @@ The legal chain:
|
|
|
111
111
|
proposed → specced → building → verifying → shipped
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
plus **
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
plus **shipped → verifying** to reopen a shipped subject (with
|
|
115
|
+
`--reason`, below) and **any state → retired** (a subject can be retired
|
|
116
|
+
from anywhere). Any other transition is rejected by failure class, exits
|
|
117
|
+
1, and leaves the record UNCHANGED (fail closed).
|
|
117
118
|
|
|
118
119
|
```bash
|
|
119
120
|
canon subject status subject-domain-loop specced
|
|
@@ -148,27 +149,39 @@ empty set; so is one whose `scenario` records route away from the
|
|
|
148
149
|
gate's rung. Each refusal prints by failure class (`uncovered-cell`),
|
|
149
150
|
exits 1, and the status stays `verifying`. `retired` is not gated.
|
|
150
151
|
|
|
151
|
-
**The brief (0.16):** `verifying → shipped` also refuses where a brief
|
|
152
|
+
**The brief (0.16, 0.17):** `verifying → shipped` also refuses where a brief
|
|
152
153
|
exists and is not approved, read exactly as `canon status` reads it (one
|
|
153
154
|
shared check, so status says `ship` only when this passes): the subject's
|
|
154
|
-
newest adopted change has a
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
newest adopted change has a canon `.canon/changes/<change>/brief.md`
|
|
156
|
+
with no `Approved:` line outside HTML comments and code fences, or has no
|
|
157
|
+
canon `brief.md` yet but comes from an external openspec plan with a
|
|
158
|
+
`proposal.md` under the plan sources. It runs with or without a policy.
|
|
159
|
+
The refusal is an `unreviewed-promotion` naming the subject, and says
|
|
160
|
+
what to do:
|
|
158
161
|
|
|
159
162
|
```
|
|
160
|
-
canon subject status: unreviewed-promotion demo-subject — verifying → shipped: brief not approved —
|
|
163
|
+
canon subject status: unreviewed-promotion demo-subject — verifying → shipped: brief not approved — .canon/changes/c-demo/brief.md has no `Approved:` line; a human must answer the brief, and only from that answer the agent will record their words as `Approved: <who> <when> — "<their words>"` in .canon/changes/c-demo/brief.md
|
|
161
164
|
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
165
|
```
|
|
163
166
|
|
|
167
|
+
For an external change with no canon brief yet, the reason reads
|
|
168
|
+
``.canon/changes/<c>/brief.md does not exist yet: change `<c>` comes from
|
|
169
|
+
the external plan <proposal>, which canon reads and never edits, so its
|
|
170
|
+
approval lives in that canon file`` and the agent will ``create
|
|
171
|
+
.canon/changes/<c>/brief.md holding a `Source: <proposal>` line and
|
|
172
|
+
record their words there as `Approved: <who> <when> — "<their words>"` ``.
|
|
173
|
+
|
|
164
174
|
Never write the line without the human's answer. When there is no brief to
|
|
165
|
-
answer (no
|
|
166
|
-
`proposal.md`), the move is not refused: it
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
175
|
+
answer (no adopted change, or a newest change with neither a canon
|
|
176
|
+
`brief.md` nor an external `proposal.md`), the move is not refused: it
|
|
177
|
+
prints ``canon subject status: advisory: <id>'s brief is not approved —
|
|
178
|
+
its newest adopted change `<c>` has no brief (no
|
|
179
|
+
.canon/changes/<c>/brief.md, no openspec proposal.md), so there is no
|
|
180
|
+
brief to answer and shipping is not refused`` (or `— <id> adopts no
|
|
181
|
+
change, so …`), and status shows the brief as not approved, as an
|
|
182
|
+
advisory. `subjects[].brief.enforced` in `canon status --json` says
|
|
183
|
+
which case applies. A brief that says the human ships changes who runs
|
|
184
|
+
the transition, not the check.
|
|
172
185
|
|
|
173
186
|
This refusal is a backstop, and so is the evidence one: shipping is not
|
|
174
187
|
the first thing an unapproved brief stops. The agent writes the brief,
|
|
@@ -230,6 +243,39 @@ raised later, a scenario tagged later) still fails the gate. Without
|
|
|
230
243
|
`require_review`, nothing is printed and nothing changes, except that
|
|
231
244
|
`shipped` still runs the brief check above.
|
|
232
245
|
|
|
246
|
+
### Reopening a shipped subject (0.18)
|
|
247
|
+
|
|
248
|
+
A follow-up on a shipped subject (a fix, a review round, a scenario its
|
|
249
|
+
gate now misses) reopens it: reopen, add tasks to the approved change,
|
|
250
|
+
scenario, evidence, review, ship.
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
canon subject status demo-subject verifying \
|
|
254
|
+
--reason "follow-up: the retry path leaks a file handle" --actor-id impl
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
canon subject status: reopened `demo-subject` from shipped, recorded by `impl`: follow-up: the retry path leaks a file handle; add the follow-up as tasks in its approved change `c-demo`, then scenario, evidence and review; shipping again runs every guard
|
|
259
|
+
canon subject status: demo-subject → verifying — written directly; nothing to promote
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
- `--reason "<one line>"` is required for `shipped → verifying` and
|
|
263
|
+
refused on any other transition (exit 2, as is a blank one). The new
|
|
264
|
+
record carries it with the actor and the change the work goes into:
|
|
265
|
+
`reopen: { reason: …, change: c-demo, actor: { agent_id: impl } }`. The
|
|
266
|
+
next status write clears it, as it clears a waiver.
|
|
267
|
+
- Inside the approved scope it needs no human: the subject's newest
|
|
268
|
+
adopted change's brief is approved, or there is no brief to answer.
|
|
269
|
+
Otherwise it refuses, exit 1, naming the brief to approve, and
|
|
270
|
+
`--override-reason` does not waive it:
|
|
271
|
+
``unreviewed-promotion demo-subject — shipped → verifying: reopening needs the approved scope, and the brief is not approved — .canon/changes/c-next/brief.md has no `Approved:` line; …``.
|
|
272
|
+
- The review guard does not run on a reopen (it prints `skipped … — a
|
|
273
|
+
reopen moves the subject back to fix its gaps; shipping again runs
|
|
274
|
+
it`). `verifying → shipped` afterwards runs every guard above again.
|
|
275
|
+
- `canon status` flags a shipped subject whose gate went red (whatever
|
|
276
|
+
`verifying → shipped` would now refuse it on) and its only step is
|
|
277
|
+
this reopen.
|
|
278
|
+
|
|
233
279
|
## Adopt flow
|
|
234
280
|
|
|
235
281
|
Planning docs written outside canon become change/task records via
|
|
@@ -249,37 +295,63 @@ and a flip re-imports that change itself. Run `canon ingest plans` for a
|
|
|
249
295
|
plan written outside canon, or when `canon query --kind task` must list
|
|
250
296
|
rows nobody has flipped yet.
|
|
251
297
|
|
|
252
|
-
`adopt`
|
|
253
|
-
`change_ids`, so `canon query --kind change --change-id …`
|
|
254
|
-
query --kind subject` agree on the link from both ends. It
|
|
255
|
-
the side that lacks the link, so rerunning it on a linked
|
|
256
|
-
nothing and exits `0`.
|
|
298
|
+
`adopt` adds the subject to the change's `subject_ids` and the change to
|
|
299
|
+
the Subject's `change_ids`, so `canon query --kind change --change-id …`
|
|
300
|
+
and `canon query --kind subject` agree on the link from both ends. It
|
|
301
|
+
writes only the side that lacks the link, so rerunning it on a linked
|
|
302
|
+
pair writes nothing and exits `0`.
|
|
303
|
+
|
|
304
|
+
A change may belong to several subjects (0.18). Adopting it under a
|
|
305
|
+
second subject adds that subject, never re-points the change, and says
|
|
306
|
+
so:
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
canon subject adopt: linked change `refactor-core` to subject `editing` as well; the change now belongs to `perception` and `editing` — written directly; nothing to promote
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Every reader joins a subject to its changes from both sides (the
|
|
313
|
+
subject's `change_ids`, and every change whose `subject_ids` names it):
|
|
314
|
+
the open-blocker rule, the status guard, `canon status`, the brief and
|
|
315
|
+
the evidence refusal. An open blocker on a shared change gates each
|
|
316
|
+
subject it belongs to. A change written before 0.18 names one subject as
|
|
317
|
+
`subject_id`; it reads, and `canon query --kind change --json` shows it,
|
|
318
|
+
as a one-element `subject_ids`.
|
|
257
319
|
|
|
258
320
|
The two records cannot be written atomically, so the subject is written
|
|
259
|
-
first:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
and prints `canon subject adopt <change_id> --subject <id>` to complete
|
|
264
|
-
it.
|
|
321
|
+
first: a half-done adoption still has that change's open blockers
|
|
322
|
+
counted. If the change write then fails, `adopt` exits `2`, names the
|
|
323
|
+
subject that now lists the change, and prints `canon subject adopt
|
|
324
|
+
<change_id> --subject <id>` to complete it.
|
|
265
325
|
|
|
266
326
|
### Starting a new change: `canon change new`
|
|
267
327
|
|
|
268
|
-
For a change that does not exist yet, one command
|
|
269
|
-
|
|
328
|
+
For a change that does not exist yet, one command writes it and records
|
|
329
|
+
it adopted:
|
|
270
330
|
|
|
271
331
|
```bash
|
|
272
332
|
canon change new add-login --subject auth --title "Add login"
|
|
273
333
|
```
|
|
274
334
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
335
|
+
```
|
|
336
|
+
canon change new: wrote .canon/changes/add-login/brief.md
|
|
337
|
+
canon change new: wrote .canon/changes/add-login/tasks.md
|
|
338
|
+
canon change new: recorded change `add-login` adopted into subject `auth` — written directly; nothing to promote
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
- **Writes** canon's own change dir, `.canon/changes/<slug>/`:
|
|
342
|
+
`brief.md` (`# <title>`, `## Why` holding the title, which imports as
|
|
343
|
+
the change summary, `## What Changes` with a comment saying what goes
|
|
344
|
+
there, and a comment naming where the human's `Approved:` line goes,
|
|
345
|
+
so the file starts unapproved) and `tasks.md` (no rows yet, so the
|
|
346
|
+
change imports as `proposed`; the openspec checkbox row grammar).
|
|
347
|
+
`.canon/changes/` is canon's native plan source: every plan reader
|
|
348
|
+
reads it with no `plans:` entry, so `canon.yaml` needs no edit and no
|
|
349
|
+
external plans source is needed (see `canon-plan-import`).
|
|
350
|
+
- **Reads** the files with the native `canon` plan adapter `canon ingest
|
|
281
351
|
plans` uses, then **records** the change already adopted, through the
|
|
282
|
-
same write as `canon subject adopt`.
|
|
352
|
+
same write as `canon subject adopt`. Its records carry the actor
|
|
353
|
+
`canon-plan-import-canon` (canon-written); an external openspec
|
|
354
|
+
change's carry `canon-plan-import-openspec`.
|
|
283
355
|
- **What a failure leaves behind.** The files are staged under `.canon/`
|
|
284
356
|
and the change dir is moved into place only once they parse. A failure
|
|
285
357
|
before any record is written (staging, publishing the dir, or the
|
|
@@ -291,14 +363,23 @@ canon change new add-login --subject auth --title "Add login"
|
|
|
291
363
|
plans && canon subject adopt <slug> --subject <id>`, which completes
|
|
292
364
|
the link and is safe to rerun.
|
|
293
365
|
- **Refuses** with exit `2`, writing nothing, when the subject does not
|
|
294
|
-
exist, when the slug already has a
|
|
295
|
-
|
|
366
|
+
exist, or when the slug already has a `.canon/changes/<slug>` dir
|
|
367
|
+
(active or under `.canon/changes/archive/`) or a `change` record.
|
|
296
368
|
|
|
297
369
|
Add task rows to `tasks.md` as `- [ ] <n> <title>`, and flip each with
|
|
298
370
|
`canon gate task <slug>#<n>` once evidence exists. `canon evidence add
|
|
299
371
|
--task` and the flip read `tasks.md` as it is, so a new row needs no
|
|
300
372
|
`canon ingest plans` run.
|
|
301
373
|
|
|
374
|
+
A change from an external plan (an openspec change listed under
|
|
375
|
+
`plans.sources`, imported by `canon ingest plans` and adopted with
|
|
376
|
+
`canon subject adopt`) gets its brief as a canon file too:
|
|
377
|
+
`.canon/changes/<change>/brief.md` holding a `Source: <path>` line that
|
|
378
|
+
points at the external document and, once the human answers, the
|
|
379
|
+
`Approved:` line. Canon never edits the external document. Such a
|
|
380
|
+
pointer `brief.md` is not a change of its own: the native reader skips
|
|
381
|
+
it, so the external source keeps the change, its summary and its tasks.
|
|
382
|
+
|
|
302
383
|
## Where are we: `canon status`
|
|
303
384
|
|
|
304
385
|
```bash
|
|
@@ -332,24 +413,42 @@ uses, so status and the gate cannot disagree about a gap:
|
|
|
332
413
|
`require_review` on, a `warning:` line counts the in-effect reviews that
|
|
333
414
|
predate review pinning (0.15), checked against the commit each pinned:
|
|
334
415
|
current, stale (named) and undecidable.
|
|
335
|
-
- **Brief** (0.16) — for each `building`, `verifying` or `shipped`
|
|
416
|
+
- **Brief** (0.16, 0.17) — for each `building`, `verifying` or `shipped`
|
|
336
417
|
subject, and each `proposed` subject whose newest adopted change has a
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
`brief:
|
|
340
|
-
`
|
|
341
|
-
|
|
418
|
+
brief (a canon `brief.md`, or an external `proposal.md`), a `brief:`
|
|
419
|
+
line under it says whether the brief is approved:
|
|
420
|
+
`brief: approved in <brief> — <the recorded words>`, or
|
|
421
|
+
`brief: not approved (…)`. Approval is an `Approved:` line in
|
|
422
|
+
`.canon/changes/<change>/brief.md` for the subject's newest adopted
|
|
423
|
+
change (the last one adopted; a live dir before an archived one), and
|
|
424
|
+
only there: a line that starts with `Approved:` (indent allowed), says
|
|
342
425
|
something after it, and sits outside HTML comments (`<!-- … -->`,
|
|
343
426
|
multi-line included) and fenced code blocks (```` ``` ```` or `~~~`),
|
|
344
427
|
written by the agent once the human answers, as
|
|
345
428
|
`Approved: <who> <when> — "<the human's words>"`. Fixes and follow-ups
|
|
346
429
|
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.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
430
|
+
new scope: an older change's approval does not cover it. A change from
|
|
431
|
+
an external plan has its brief in the same canon file, with a
|
|
432
|
+
`Source: <path>` line pointing at the external document; an `Approved:`
|
|
433
|
+
line inside an external `proposal.md` is not read. Before that canon
|
|
434
|
+
file exists, an external openspec `proposal.md` under the plan sources
|
|
435
|
+
(the first source holding the change; with no `plans:` section, the
|
|
436
|
+
`openspec` source rooted at the repo that `canon gate task` also falls
|
|
437
|
+
back to) still makes the brief one to answer, and the line reads
|
|
438
|
+
``brief: not approved (.canon/changes/<c>/brief.md does not exist yet:
|
|
439
|
+
change `<c>` comes from the external plan <proposal>, which canon reads
|
|
440
|
+
and never edits, so its approval lives in that canon file)``. With a
|
|
441
|
+
canon brief and no line it reads
|
|
442
|
+
``brief: not approved (<brief> has no `Approved:` line)``.
|
|
352
443
|
There is no record kind for it; the file is the record.
|
|
444
|
+
- **Red shipped subject** (0.18) — a `shipped` subject that the
|
|
445
|
+
`verifying → shipped` guard would now refuse, by the guard's own checks
|
|
446
|
+
(owned scenarios, verdicts, required cases, audits, and, where
|
|
447
|
+
`require_review` covers `shipped`, reviews and open blockers under
|
|
448
|
+
`block_on_findings`; a gap its waiver recorded is not counted), prints
|
|
449
|
+
``gate: red — <n> <class> (first: <subject>), …; reopen with `canon subject status <id> verifying --reason "<why it reopens>" --actor-id <unit>` ``,
|
|
450
|
+
carries `red: [...]` in `--json`, and that reopen is its only `next:`
|
|
451
|
+
step. Open blockers count against every subject a change belongs to.
|
|
353
452
|
- **Unowned** — scenarios with no `@subject:` tag.
|
|
354
453
|
- **Audits** — how many scenarios are sampled for
|
|
355
454
|
`spec_coverage.require_audit`, confirmed, owed (no audit or
|
|
@@ -372,7 +471,7 @@ uses, so status and the gate cannot disagree about a gap:
|
|
|
372
471
|
itself first, so this means the refresh refused on a format violation
|
|
373
472
|
or a duplicate id, which the sync names). Then per subject, verifying
|
|
374
473
|
first, then building, specced, proposed, shipped: an unapproved brief
|
|
375
|
-
(building and later, and proposed once its change has a
|
|
474
|
+
(building and later, and proposed once its change has a brief)
|
|
376
475
|
→ the human step below, which is then the subject's
|
|
377
476
|
only step; no scenario → `canon scenario new … --subject <id>` (and, when no
|
|
378
477
|
`.feature` file holds a scenario yet, `canon feature new` first, unless
|
|
@@ -389,12 +488,20 @@ uses, so status and the gate cannot disagree about a gap:
|
|
|
389
488
|
carries `--project`: the owning root for a missing case, `<root-id>`
|
|
390
489
|
(the reason lists the roots) for a subject that owns no scenario yet.
|
|
391
490
|
- **The one human step.** Every step is the agent's to run except one:
|
|
392
|
-
a subject whose brief is enforced (its newest change
|
|
393
|
-
`
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
491
|
+
a subject whose brief is enforced (its newest change's canon
|
|
492
|
+
`brief.md` has no `Approved:` line, or it has no canon `brief.md` yet
|
|
493
|
+
and comes from an external openspec `proposal.md`) gets
|
|
494
|
+
`human: stop and report the brief in <brief>; build and attest nothing for <id> until the human answers (one answer approves brief and policy)`,
|
|
495
|
+
where `<brief>` is the canon `brief.md`, or the external `proposal.md`
|
|
496
|
+
when no canon brief exists yet. Its reason reads ``<id>: <why
|
|
497
|
+
unanswered> (an older change's approval does not cover a new scope),
|
|
498
|
+
and the approval is never assumed: `canon evidence add` refuses <id>'s
|
|
499
|
+
scenarios and tasks until it is recorded; once the human answers,
|
|
500
|
+
<record> and run `canon policy approve --by <the human> --note "<their
|
|
501
|
+
words>"` ``, where `<record>` is ``record their words as `Approved: <who>
|
|
502
|
+
<when> — "<their words>"` in <brief>``, or, for an external change,
|
|
503
|
+
``create <brief> holding a `Source: <proposal>` line and record their
|
|
504
|
+
words there as `Approved: <who> <when> — "<their words>"` ``.
|
|
398
505
|
It is the subject's only step, whatever its status: nothing suggests
|
|
399
506
|
`canon feature new`, `canon scenario new`, `canon evidence add`, a
|
|
400
507
|
review or a move-on for it before the human answers, and
|
|
@@ -402,9 +509,9 @@ uses, so status and the gate cannot disagree about a gap:
|
|
|
402
509
|
approval is never assumed. A subject with no brief to answer gets no human step;
|
|
403
510
|
its `brief:` line says
|
|
404
511
|
`not approved (no adopted change; advisory, nothing to approve yet)` or
|
|
405
|
-
``not approved (change `<id>` has no
|
|
512
|
+
``not approved (change `<id>` has no brief; advisory, nothing to
|
|
406
513
|
approve yet)`` and nothing is withheld (a `proposed` subject with no
|
|
407
|
-
|
|
514
|
+
brief, and a `specced` subject, show no `brief:` line at all). Once nothing blocks, a
|
|
408
515
|
ship-ready verifying subject gets the agent's own
|
|
409
516
|
`canon subject status <id> shipped`; status never asks a human to ship.
|
|
410
517
|
|
|
@@ -424,21 +531,21 @@ Status fails closed rather than suggest progress the gate would refuse:
|
|
|
424
531
|
|
|
425
532
|
```json
|
|
426
533
|
{
|
|
427
|
-
"statusVersion":
|
|
428
|
-
"canonVersion": "0.
|
|
534
|
+
"statusVersion": 3,
|
|
535
|
+
"canonVersion": "0.17.0",
|
|
429
536
|
"policy": { "present": true, "spec_coverage": { "require_evidence": true, "scope": [], "exclude_lanes": [], "require_cases": ["failure"], "require_review": { "scope": ["verifying", "shipped"], "distinct_actor": true, "block_on_findings": true } } },
|
|
430
537
|
"warnings": [],
|
|
431
538
|
"subjects": [
|
|
432
539
|
{ "id": "cart", "title": "Cart", "status": "building", "scenarios": 2, "excluded": 0, "evidenced": 2, "divergent": 0,
|
|
433
540
|
"reviewed": 1, "staleReviews": 0, "unsignedReviews": 0, "reviewDue": true, "openBlockers": 1, "missingCases": [],
|
|
434
|
-
"brief": { "approved": true, "enforced": false, "change": "c-cart", "
|
|
541
|
+
"brief": { "approved": true, "enforced": false, "change": "c-cart", "brief": ".canon/changes/c-cart/brief.md", "briefExists": true, "approval": "Journey 2026-10-11 — \"approved\"" } },
|
|
435
542
|
{ "id": "promo", "title": "Promo", "status": "verifying", "scenarios": 1, "excluded": 0, "evidenced": 1, "divergent": 0,
|
|
436
543
|
"reviewed": 1, "staleReviews": 0, "unsignedReviews": 0, "reviewDue": true, "openBlockers": 0, "missingCases": [],
|
|
437
|
-
"brief": { "approved": false, "enforced": true, "change": "c-promo", "
|
|
544
|
+
"brief": { "approved": false, "enforced": true, "change": "c-promo", "brief": ".canon/changes/c-promo/brief.md", "briefExists": false, "source": "openspec/changes/c-promo/proposal.md" } }
|
|
438
545
|
],
|
|
439
546
|
"unowned": ["cart.promo.01"],
|
|
440
547
|
"next": [
|
|
441
|
-
{ "command": "stop and report the brief in openspec/changes/c-promo/proposal.md; build and attest nothing for promo until the human answers (one answer approves brief and policy)", "why": "promo: its
|
|
548
|
+
{ "command": "stop and report the brief in openspec/changes/c-promo/proposal.md; build and attest nothing for promo until the human answers (one answer approves brief and policy)", "why": "promo: .canon/changes/c-promo/brief.md does not exist yet: change `c-promo` comes from the external plan openspec/changes/c-promo/proposal.md, which canon reads and never edits, so its approval lives in that canon file (an older change's approval does not cover a new scope), and the approval is never assumed: … once the human answers, create .canon/changes/c-promo/brief.md holding a `Source: openspec/changes/c-promo/proposal.md` line and record their words there as `Approved: <who> <when> — \"<their words>\"` and run `canon policy approve --by <the human> --note \"<their words>\"`", "actor": "human" },
|
|
442
549
|
{ "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
550
|
{ "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
551
|
{ "command": "canon status", "why": "1 scenario(s) carry no @subject tag (first: cart.promo.01); add `@subject:<id>` above each in its .feature file, then check again: …", "actor": "agent" }
|
|
@@ -451,21 +558,29 @@ Each `missingCases` entry is `{ "projectId", "surface", "case" }`: the
|
|
|
451
558
|
spec root, the `<area>.<surface>`, and the required case it lacks.
|
|
452
559
|
|
|
453
560
|
`brief` is present for `building`, `verifying` and `shipped` subjects,
|
|
454
|
-
and for a `proposed` subject whose newest adopted change has a
|
|
455
|
-
`proposal.md
|
|
456
|
-
subject adopts none)
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
561
|
+
and for a `proposed` subject whose newest adopted change has a brief (a
|
|
562
|
+
canon `brief.md` or an external `proposal.md`). `change` is the newest
|
|
563
|
+
adopted change (omitted when the subject adopts none); `brief` its canon
|
|
564
|
+
`.canon/changes/<change>/brief.md`, the one file the approval is read
|
|
565
|
+
from (omitted when the change has no brief to answer); `briefExists`
|
|
566
|
+
(always present) whether that file exists (`false` for an external
|
|
567
|
+
change whose canon brief is still to be created); `source` the external
|
|
568
|
+
plan document the change comes from (the canon brief's `Source:` line,
|
|
569
|
+
or, before that file exists, the external `proposal.md`; omitted for a
|
|
570
|
+
canon-written change); `approval` the text after `Approved:`; and
|
|
571
|
+
`enforced` whether the missing approval blocks (a brief exists to
|
|
572
|
+
answer). The 0.16 `proposal` field is gone.
|
|
460
573
|
|
|
461
574
|
Each `next` entry's `actor` is `agent` (its `command` is a canon command
|
|
462
575
|
line) or `human` (its `command` says what to get from the human).
|
|
463
576
|
|
|
464
577
|
`policy.spec_coverage` has the shape `canon context --json` prints
|
|
465
578
|
(`null` when absent, `{"invalid": …}` when unusable). `statusVersion`
|
|
466
|
-
changes only when a field changes meaning or is removed. It
|
|
467
|
-
|
|
468
|
-
(`actor` tells which)
|
|
579
|
+
changes only when a field changes meaning or is removed. It became `2`
|
|
580
|
+
in 0.16, when `next[].command` stopped always being a command line
|
|
581
|
+
(`actor` tells which), and `brief` was added alongside. It is `3` since
|
|
582
|
+
0.17, when `brief.proposal` was removed: the canon `brief.md` is
|
|
583
|
+
`brief.brief`, with `briefExists` and `source`.
|
|
469
584
|
|
|
470
585
|
`subject new`, `adopt` and `status` write their records directly; the
|
|
471
586
|
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.18.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.18.0",
|
|
34
|
+
"@journeykit/canon-core-linux-x64": "0.18.0",
|
|
35
|
+
"@journeykit/canon-core-win32-x64": "0.18.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^20.0.0",
|