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