@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
|
@@ -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** —
|
|
@@ -26,21 +26,45 @@ diagnostic and reports `clean: false`.
|
|
|
26
26
|
## The starter policy `canon init` writes
|
|
27
27
|
|
|
28
28
|
A fresh `canon init` writes `.canon/policy.yaml` (skip it with
|
|
29
|
-
`--no-policy`; an existing file is never overwritten)
|
|
29
|
+
`--no-policy`; an existing file is never overwritten). `--policy
|
|
30
|
+
light|standard|strict` picks how much it asks for; the default is
|
|
31
|
+
`strict`:
|
|
32
|
+
|
|
33
|
+
| Level | `spec_coverage` |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `light` | `require_evidence: true` — every scenario needs evidence |
|
|
36
|
+
| `standard` | light, plus `require_cases: [failure]` — every feature surface needs a `@case:failure` scenario |
|
|
37
|
+
| `strict` | standard, plus `require_review: {}` — verifying/shipped subjects need an independent review |
|
|
30
38
|
|
|
31
39
|
```yaml
|
|
40
|
+
# canon policy level: strict
|
|
32
41
|
spec_coverage:
|
|
33
42
|
require_evidence: true # every scenario needs evidence
|
|
34
43
|
require_cases: [failure] # every feature surface needs a @case:failure scenario
|
|
35
44
|
require_review: {} # verifying/shipped subjects need an independent review
|
|
36
45
|
```
|
|
37
46
|
|
|
38
|
-
The
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
policy
|
|
43
|
-
|
|
47
|
+
The first line names the level. The file carries comments explaining
|
|
48
|
+
each field, and a `light` or `standard` starter carries the next levels'
|
|
49
|
+
keys as commented lines: to raise the level, uncomment them and change
|
|
50
|
+
the level line, and a human approves the edit (next section). An
|
|
51
|
+
unknown `--policy` value exits 2, and so does `--policy` with `--no-policy`.
|
|
52
|
+
|
|
53
|
+
`canon status` prints the level under the policy settings
|
|
54
|
+
(`level: strict`, or `level: light — raise it by …` with how to raise
|
|
55
|
+
it) and in `--json` as `policy.level`. It reads the level line when the
|
|
56
|
+
file's leading comment block has one naming a known level; otherwise it
|
|
57
|
+
infers the level from the `spec_coverage` section, which must match a
|
|
58
|
+
level exactly (no `scope`, `exclude_lanes`, `require_audit`, or
|
|
59
|
+
non-default `require_review` settings); anything else is `custom`.
|
|
60
|
+
`policy.level` is `null` when there is no policy.
|
|
61
|
+
|
|
62
|
+
Every level loads with zero diagnostics. The policy is what `canon gate
|
|
63
|
+
check` grades the work against, so a human should review and approve
|
|
64
|
+
it, and every later change to it (next section). The agent doing the
|
|
65
|
+
work should not write its own grading policy. Narrow it with `scope:
|
|
66
|
+
[building, verifying]` to check evidence only for subjects in those
|
|
67
|
+
statuses (see `canon-gate`).
|
|
44
68
|
|
|
45
69
|
## Approving the policy
|
|
46
70
|
|
|
@@ -48,8 +72,11 @@ only for subjects in those statuses (see `canon-gate`).
|
|
|
48
72
|
canon policy approve --by <who> [--note <text>] [--repo <dir>]
|
|
49
73
|
```
|
|
50
74
|
|
|
51
|
-
A human
|
|
52
|
-
|
|
75
|
+
A human approves the policy after reviewing `.canon/policy.yaml`, in the
|
|
76
|
+
same answer that approves a subject's brief (0.16; see the skill's working
|
|
77
|
+
loop). The human runs this, or the agent runs it right after that answer
|
|
78
|
+
as `canon policy approve --by <the human> --note "<their words>"`. It
|
|
79
|
+
writes `.canon/policy.approval.json` directly (nothing to promote):
|
|
53
80
|
|
|
54
81
|
```json
|
|
55
82
|
{
|
|
@@ -85,8 +112,8 @@ An approval file that does not parse reads
|
|
|
85
112
|
`--json` `warnings`). The advisory never changes an exit code: canon
|
|
86
113
|
cannot tell whether a human or an agent ran the command, so the record
|
|
87
114
|
names who approved which bytes, and the advisory says when that no
|
|
88
|
-
longer holds. An agent
|
|
89
|
-
behalf.
|
|
115
|
+
longer holds. An agent runs `canon policy approve` only to record a
|
|
116
|
+
human's answer, never without one and never on its own behalf.
|
|
90
117
|
|
|
91
118
|
## The closed CEL profile
|
|
92
119
|
|
|
@@ -17,8 +17,10 @@ state they are in.
|
|
|
17
17
|
The order an agent follows to claim a subject done:
|
|
18
18
|
|
|
19
19
|
1. **Test-run evidence** per owned scenario (`canon evidence add
|
|
20
|
-
--scenario-id <id> --project-id <root-id> ... --session-id <session
|
|
21
|
-
|
|
20
|
+
--scenario-id <id> --project-id <root-id> ... --session-id <session>`;
|
|
21
|
+
final when it returns under `write: { promote: auto }`, which
|
|
22
|
+
`canon init` writes, and followed by `canon gate promote` only under
|
|
23
|
+
`write.promote: manual`). See `canon-gate`.
|
|
22
24
|
2. **Independent review** of the change(s) the subject adopted. Record
|
|
23
25
|
each issue the reviewer raises as a Finding, fix it, then close it:
|
|
24
26
|
|
|
@@ -26,13 +28,14 @@ The order an agent follows to claim a subject done:
|
|
|
26
28
|
canon finding add --change-id c-demo --round 1 --seq 1 \
|
|
27
29
|
--severity blocker --disposition open --reviewer reviewer-2 \
|
|
28
30
|
--summary "unbounded subprocess wait" --session-id review-session-1
|
|
29
|
-
canon gate promote
|
|
30
31
|
# ... fix, commit ...
|
|
31
32
|
canon finding close --change-id c-demo --round 1 --seq 1 \
|
|
32
33
|
--disposition fixed --resolution-sha "$(git rev-parse HEAD)"
|
|
33
|
-
canon gate promote
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
Under `write.promote: manual` (or no `write:` key), run
|
|
37
|
+
`canon gate promote` after each of the two.
|
|
38
|
+
|
|
36
39
|
3. **One Review per scenario**, by someone other than the evidence actor,
|
|
37
40
|
from a session other than the one that attested the evidence, after
|
|
38
41
|
the evidence is in (the review pins what it saw):
|
|
@@ -48,7 +51,9 @@ The order an agent follows to claim a subject done:
|
|
|
48
51
|
reviewer could not reach a verdict, record that instead of signing:
|
|
49
52
|
`--outcome indeterminate` (or `unavailable` when it could not run).
|
|
50
53
|
Changing the scenario or re-attesting it afterwards makes the review
|
|
51
|
-
stale; review it again.
|
|
54
|
+
stale; review it again. A reviewer of user-visible behavior attaches
|
|
55
|
+
the screenshots or recordings it looked at with `--artifact <path>`
|
|
56
|
+
(below).
|
|
52
57
|
|
|
53
58
|
4. **Move the subject**: `canon subject status <id> verifying`, later
|
|
54
59
|
`shipped`. See `canon-subject`.
|
|
@@ -70,8 +75,12 @@ canon finding close --change-id <c> --round <n> --seq <n> \
|
|
|
70
75
|
--disposition fixed --resolution-sha <sha> [--session-id <id>] # or rejected / deferred / open
|
|
71
76
|
```
|
|
72
77
|
|
|
73
|
-
Both stage a candidate
|
|
74
|
-
|
|
78
|
+
Both stage a candidate. Under `write: { promote: auto }` (what
|
|
79
|
+
`canon init` writes) the same run promotes it and the success line names
|
|
80
|
+
the committed record (``wrote kind=finding/… — staged and promoted in this
|
|
81
|
+
run; nothing to promote``); under `write.promote: manual`
|
|
82
|
+
`canon gate promote` commits it, and the line says so (``staged … — run
|
|
83
|
+
`canon gate promote` to commit it``). `finding close`
|
|
75
84
|
changes only the disposition (and `resolution_sha` with it); severity,
|
|
76
85
|
reviewer, summary, and refs are copied from the committed record. It
|
|
77
86
|
appends a second version at the same natural key — the first stays. Its
|
|
@@ -130,7 +139,8 @@ canon review add --project-id <root-id> --scenario-id <id> \
|
|
|
130
139
|
--reviewer <who> --pin <sha-or-ref> --role <role> \
|
|
131
140
|
(--upstream-ref <ref> | --original-spec-ref <ref>) [--actor-id <id>] [--session-id <id>] \
|
|
132
141
|
[--outcome signed|indeterminate|unavailable] \
|
|
133
|
-
[--reviewer-provider <p>] [--reviewer-model <m>] [--reviewer-harness <h>]
|
|
142
|
+
[--reviewer-provider <p>] [--reviewer-model <m>] [--reviewer-harness <h>] \
|
|
143
|
+
[--artifact <path>]... [--max-artifact-mib <MiB>]
|
|
134
144
|
```
|
|
135
145
|
|
|
136
146
|
One attributed Review record for one scenario, joined on
|
|
@@ -143,6 +153,40 @@ own. It is written directly to the ledger, with no staging step, and says
|
|
|
143
153
|
so: `canon review add: wrote <path> (new) — written directly; nothing to
|
|
144
154
|
promote`.
|
|
145
155
|
|
|
156
|
+
### What the reviewer looked at (artifacts)
|
|
157
|
+
|
|
158
|
+
`--artifact <path>` (repeatable) binds a file the reviewer looked at — a
|
|
159
|
+
screenshot, a recording, a trace — exactly as `canon evidence add
|
|
160
|
+
--artifact` binds one: the path must resolve to a file inside the
|
|
161
|
+
repository, a file over 25 MiB is refused unless `--max-artifact-mib <N>`
|
|
162
|
+
raises the limit, and the bytes are copied into
|
|
163
|
+
`.canon/artifacts/sha256/<hex>` (commit it with the ledger) before the
|
|
164
|
+
review is written. Any refusal exits 2 and writes and stores nothing. The
|
|
165
|
+
review records each distinct file once, in the order given:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
"artifacts": [{"path": "reports/title.png", "sha256": "<64 hex>"}]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The field is absent when no `--artifact` is given. `review add` prints
|
|
172
|
+
`canon review add: artifact store holds <n> bound file(s) under
|
|
173
|
+
.canon/artifacts/sha256/ (<k> newly stored) — commit them with the
|
|
174
|
+
ledger`.
|
|
175
|
+
|
|
176
|
+
`canon gate check` re-hashes the stored blob of every file bound to an
|
|
177
|
+
in-effect review (the latest per `(project_id, scenario_id, reviewer)`).
|
|
178
|
+
A missing or altered blob is `stale-evidence` on the review's scenario,
|
|
179
|
+
naming the review, even when the working-tree file still matches (see
|
|
180
|
+
`canon-gate`, "Bound evidence stays provable"):
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
stale-evidence world.demo.01 — review by `reviewer-2` (actor `reviewer-2`, 2026-10-11T09:00:00Z) bound file `reports/title.png` recorded sha256 <hex>; stored blob .canon/artifacts/sha256/<hex>: missing; working tree: <hex> — the bound bytes are gone, look again and re-record the review with `canon review add --artifact <path>`
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The reviewer's later review replaces the earlier one, so its files are
|
|
187
|
+
the ones checked. Canon binds and re-checks bytes only; it never compares
|
|
188
|
+
images or judges what a screenshot shows.
|
|
189
|
+
|
|
146
190
|
### What the review saw (pins)
|
|
147
191
|
|
|
148
192
|
`review add` reads the corpus and records `pins` on the review; the
|
|
@@ -245,20 +289,43 @@ The scenario is reviewed when at least one of those latest reviews:
|
|
|
245
289
|
1. is `signed`;
|
|
246
290
|
2. still matches its pins — the scenario record's current
|
|
247
291
|
`source_digest`, and its current latest evidence record per role —
|
|
248
|
-
when it has pins;
|
|
292
|
+
when it has pins; or, written before 0.15 without pins, still
|
|
293
|
+
describes its scenario at the commit it pinned (below);
|
|
249
294
|
3. passes `distinct_actor`, when that is on.
|
|
250
295
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
296
|
+
### Reviews written before pinning
|
|
297
|
+
|
|
298
|
+
A review written before 0.15 has no pins, only `pin`: the commit it
|
|
299
|
+
reviewed. The gate reads the scenario's `.feature` file at that commit
|
|
300
|
+
(`git`, with the same per-scenario normalization `canon inventory sync`
|
|
301
|
+
uses for `scenario_digest`; if the scenario was in another file then, the
|
|
302
|
+
spec root's files at that commit are searched for its id) and decides:
|
|
303
|
+
|
|
304
|
+
- **current** when that digest equals the scenario's current one and no
|
|
305
|
+
declared evidence record of the scenario is newer than the review: it
|
|
306
|
+
counts under rules 1 and 3;
|
|
307
|
+
- **stale** when the scenario changed since the commit (`its spec changed
|
|
308
|
+
since <pin>`) or has evidence newer than the review (`it has declared
|
|
309
|
+
evidence newer than the review`): it no longer counts, exactly like a
|
|
310
|
+
stale pinned review;
|
|
311
|
+
- **undecidable** when the pin is not a commit in this clone (shallow
|
|
312
|
+
clone, rewritten history) or the scenario id is absent at it: it keeps
|
|
313
|
+
counting, as in 0.15. A missing commit never becomes a violation.
|
|
314
|
+
|
|
315
|
+
`canon gate check` prints one advisory line with the split (exit code
|
|
316
|
+
unchanged by the line itself). A stale one whose scenario already has
|
|
317
|
+
another qualifying review is counted as superseded and not named:
|
|
254
318
|
|
|
255
319
|
```
|
|
256
|
-
advisory:
|
|
320
|
+
advisory: 338 review(s) predate review pinning (0.15) and were checked against the commit each one pinned: 318 current, 12 stale, 8 undecidable; 7 stale one(s) superseded by a current review of the same scenario, so nothing is owed for them; a stale one with no current review in its place no longer counts — review its scenario again (`canon review add`): platformer.movement.01, platformer.movement.02, …; an undecidable one still counts unchecked, because its pinned commit or the scenario at it is not in this clone — check out full history (`fetch-depth: 0` in CI) to check it
|
|
257
321
|
```
|
|
258
322
|
|
|
259
323
|
`canon status` prints the same line as a warning, and `canon subject
|
|
260
|
-
status` prints it on stderr for the subject's own scenarios
|
|
261
|
-
the
|
|
324
|
+
status` prints it on stderr for the subject's own scenarios; all three
|
|
325
|
+
make the same decision. Re-review exactly the scenarios the line names:
|
|
326
|
+
the new review pins what it saw and replaces the reviewer's legacy one.
|
|
327
|
+
In CI, check out full history (`fetch-depth: 0`) for the job that runs
|
|
328
|
+
the gate, or every legacy review is undecidable.
|
|
262
329
|
|
|
263
330
|
### `unreviewed-promotion`
|
|
264
331
|
|
|
@@ -149,7 +149,150 @@ Every query, export, and purge appends an actor-stamped record to
|
|
|
149
149
|
`.canon/audit/access.jsonl` (mode `0600` on creation) in the repository
|
|
150
150
|
whose `canon.yaml` it read (through `--repo` or `--canon-yaml`), never in
|
|
151
151
|
the current directory; with no `canon.yaml` there is no audit and nothing
|
|
152
|
-
is written. `
|
|
152
|
+
is written. The log is local operator state: `canon init` keeps
|
|
153
|
+
`.canon/audit/` out of git (below). `CANON_ACTOR` takes
|
|
153
154
|
precedence over the OS username. Audit rows contain operation, kind, scope,
|
|
154
155
|
sensitive flag, counts, and timestamp only—never query text, secrets, or
|
|
155
156
|
record bodies.
|
|
157
|
+
|
|
158
|
+
## Staged writes and promotion
|
|
159
|
+
|
|
160
|
+
Four writes stage their record instead of committing it: `canon evidence
|
|
161
|
+
add`, `canon finding add` and `canon finding close` (under
|
|
162
|
+
`.canon/ledger/_staging/`, committed by `canon gate promote`) and `canon
|
|
163
|
+
divergence stage` (committed by `canon divergence promote`). `canon.yaml`'s
|
|
164
|
+
`write:` section decides whether they promote in the same run:
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
write:
|
|
168
|
+
promote: auto # or manual
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- `auto` — the command stages, then promotes that one record through the
|
|
172
|
+
same validation and assignment `canon gate promote` (or `canon
|
|
173
|
+
divergence promote`) applies. Records other runs staged stay staged;
|
|
174
|
+
only `canon gate promote` / `canon divergence promote` commit
|
|
175
|
+
everything staged. The one success line names the committed record
|
|
176
|
+
(`wrote kind=…/….json`; promotion renames it, so the staged file is not
|
|
177
|
+
named) and ends `— staged and promoted in this run; nothing to
|
|
178
|
+
promote`. `canon init` writes `auto`.
|
|
179
|
+
- `manual` — the record stays staged and the line ends ``— run `canon
|
|
180
|
+
gate promote` to commit it``. An absent `write:` key (or `promote:`
|
|
181
|
+
field) is `manual`, so a repo that never set it is unchanged.
|
|
182
|
+
|
|
183
|
+
`--promote` / `--no-promote` decide for one write and win over the config
|
|
184
|
+
without reading it; both at once is a usage error (exit `2`). If the
|
|
185
|
+
promotion refuses the command's own record, the record stays staged, the
|
|
186
|
+
line ends ``— run `canon gate promote` to commit it`` (`canon divergence
|
|
187
|
+
promote` for a divergence), and the command exits `1`. With neither flag,
|
|
188
|
+
a malformed `write:` section (anything but `promote: auto | manual`)
|
|
189
|
+
refuses the write, exit `2`, before anything is staged. `canon init
|
|
190
|
+
--check-config` reports `[PASS] write (promote: auto)`, `[not configured]
|
|
191
|
+
write (promote: manual)`, or `[FAIL] write: …`. `canon gate promote` still
|
|
192
|
+
works under `auto` and is idempotent: with nothing staged it prints
|
|
193
|
+
`nothing staged` and exits `0`.
|
|
194
|
+
|
|
195
|
+
## Harnesses: the `skills:` section
|
|
196
|
+
|
|
197
|
+
`canon.yaml`'s `skills:` section names the harnesses the canon skill is
|
|
198
|
+
installed for (0.17):
|
|
199
|
+
|
|
200
|
+
```yaml
|
|
201
|
+
skills:
|
|
202
|
+
providers: [claude, codex] # any of claude, codex, omp, pi; [] for none
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`canon init` writes it: on a terminal it asks, with the harnesses whose
|
|
206
|
+
`.claude`, `.codex`, `.omp` or `.pi` directory exists checked;
|
|
207
|
+
`--providers a,b` (or `none`) answers without asking; with neither it takes
|
|
208
|
+
the detected ones, and with none detected it writes only a commented example
|
|
209
|
+
and installs nothing. `.agents` alone never selects Codex. `canon skills
|
|
210
|
+
install|check|doctor` follow the section; `canon skills install
|
|
211
|
+
--providers=<list>` rewrites it in place. Without the section they use the
|
|
212
|
+
install lock's providers, then the detected ones; nothing undetected is ever
|
|
213
|
+
added. A present section that is not this shape, or names another harness,
|
|
214
|
+
fails those commands (exit `1`), and `canon init --check-config` reports
|
|
215
|
+
`[PASS] skills (providers: …)`, `[not configured] skills (…)`, or
|
|
216
|
+
`[FAIL] skills: …`.
|
|
217
|
+
|
|
218
|
+
## The scenario index
|
|
219
|
+
|
|
220
|
+
`canon inventory sync` materializes one `Scenario` record per
|
|
221
|
+
`(project_id, scenario_id)` under `.canon/ledger/kind=scenario/`. The
|
|
222
|
+
index is derived — a pure function of the `.feature` files — so the
|
|
223
|
+
commands that read it rebuild it first: `canon gate check`, `canon
|
|
224
|
+
status`, `canon subject status`, `canon review add` and `canon evidence
|
|
225
|
+
add`. The index is stale when a `.feature` file holds a scenario with no
|
|
226
|
+
live index record, or one whose recorded source digest differs from the
|
|
227
|
+
file's current bytes, or when the index holds a scenario of a spec root
|
|
228
|
+
that no `.feature` file of it declares any more. The reader then runs
|
|
229
|
+
`canon inventory sync`'s code on the changed files (validation still
|
|
230
|
+
covers the whole root) and prints one stderr line:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
canon status: refreshed the scenario index from 1 changed feature file(s) (.canon/specs/kind=feature/area=cart/add.feature): 1 record(s) written
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
A deleted scenario is not erased: `canon inventory sync` — and a reader's
|
|
237
|
+
refresh, identically — appends a tombstone, a new generation of its
|
|
238
|
+
record with `removed: true`. The gate, `canon status`, `canon query` and
|
|
239
|
+
the report panels then treat it as absent; re-adding the scenario writes
|
|
240
|
+
a live generation that supersedes the tombstone. A root that fails to
|
|
241
|
+
sync writes no tombstone either.
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
canon status: refreshed the scenario index from 1 scenario(s) no longer in any feature file (cart.pay.01): 1 record(s) written
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`canon inventory sync` and every refresh hold an OS file lock on
|
|
248
|
+
`.canon/scenario-index.lock` while they write, so concurrent runs never
|
|
249
|
+
index one change twice; a refresh that waited checks staleness again and
|
|
250
|
+
is silent if another run already did the work. The lock is released when
|
|
251
|
+
its holder exits, however it exits, so it never goes stale; a writer
|
|
252
|
+
gives up after 30 s with a message naming the file (`canon inventory
|
|
253
|
+
sync` exits `2`; a reader skips the refresh and still runs).
|
|
254
|
+
|
|
255
|
+
A fresh index prints nothing. A root that fails to sync (format
|
|
256
|
+
violations, a duplicate id) is named and left as it was, and the command
|
|
257
|
+
still runs; `canon gate check` then fails with `uncovered-cell <root>`,
|
|
258
|
+
the `verifying → shipped` guard refuses, and `canon status` lists only
|
|
259
|
+
`canon inventory sync`, while the reporting readers (`query`, `context`)
|
|
260
|
+
keep only this line:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
canon gate check: found the scenario index stale for 1 changed feature file(s) (…): 0 record(s) written; NOT refreshed: root `root` (2 format violation(s), 0 sync error(s)) — `canon inventory sync` lists them
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Nothing is written in a directory without `canon.yaml`. `canon inventory
|
|
267
|
+
sync` stays the explicit command, and the one that lists violations and
|
|
268
|
+
tag diagnostics.
|
|
269
|
+
|
|
270
|
+
## The artifact store lock
|
|
271
|
+
|
|
272
|
+
Every write to the evidence artifact store (`.canon/artifacts/sha256/`:
|
|
273
|
+
`canon evidence add`, `canon review add`, `canon evidence vault`) holds an
|
|
274
|
+
OS file lock on `.canon/artifact-store.lock` from its "already stored?"
|
|
275
|
+
check through its last rename, so a write that fails and rolls back never
|
|
276
|
+
removes a blob another writer published or counted as stored. A rollback
|
|
277
|
+
removes only the blobs its own write renamed into place; a blob already
|
|
278
|
+
stored intact counts as stored and is not written again. The lock is
|
|
279
|
+
released when its holder exits, however it exits, so it never goes stale;
|
|
280
|
+
a writer gives up after 30 s with a message naming the file and saying to
|
|
281
|
+
retry (the command exits `2` and writes no record).
|
|
282
|
+
|
|
283
|
+
## What `canon init` keeps out of git
|
|
284
|
+
|
|
285
|
+
A fresh `canon init` writes these lines to `.gitignore` (each under a
|
|
286
|
+
comment, only the missing ones):
|
|
287
|
+
|
|
288
|
+
- `.canon/hot.db*` — the sqlite hot tier and its WAL/SHM sidecars.
|
|
289
|
+
- `.canon/audit/` — the query/export/purge access log.
|
|
290
|
+
- `.canon/ledger/kind=scenario/` — the scenario index; every reader
|
|
291
|
+
rebuilds it.
|
|
292
|
+
- `.canon/scenario-index.lock` — the lock serializing index rebuilds.
|
|
293
|
+
- `.canon/artifact-store.lock` — the lock serializing artifact store
|
|
294
|
+
writes.
|
|
295
|
+
|
|
296
|
+
A rerun on a repo that already has `canon.yaml` only refreshes
|
|
297
|
+
`AGENTS.md`, so an existing repo's `.gitignore` is unchanged; add the
|
|
298
|
+
lines by hand to adopt them.
|