@journeykit/canon 0.16.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 +31 -17
- 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 +56 -16
- package/dist/skills/reference/canon-inventory.md +42 -4
- package/dist/skills/reference/canon-plan-import.md +56 -17
- package/dist/skills/reference/canon-storage.md +24 -1
- package/dist/skills/reference/canon-subject.md +117 -58
- 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.
|
|
@@ -44,17 +44,21 @@ work inside an approved scope, never to the approval itself.
|
|
|
44
44
|
|
|
45
45
|
1. **Brief.** For a new subject or a scope change, write down the request
|
|
46
46
|
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).
|
|
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,
|
|
50
54
|
asking the human once for the brief and the policy; go on to step 3
|
|
51
55
|
only after the human answers, and record the answer as above. Inside an
|
|
52
56
|
approved scope there is no new brief: go to step 3 under the existing
|
|
53
57
|
subject and change.
|
|
54
58
|
2. **Subject.** `canon subject new <id> --domain <domain> --title "…"`, then
|
|
55
59
|
`canon change new <slug> --subject <id> --title "…"`, and put the brief in
|
|
56
|
-
the
|
|
57
|
-
where you stop until the human answers (step 1).
|
|
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).
|
|
58
62
|
3. **Scenarios.** `canon feature new <area>.<surface> --title "…"`, then one
|
|
59
63
|
`canon scenario new <area>.<surface>.<nn> --subject <id> --case <case> --title "…"`
|
|
60
64
|
per behavior, with failure paths (`--case failure`) as well as `happy`
|
|
@@ -66,7 +70,10 @@ work inside an approved scope, never to the approval itself.
|
|
|
66
70
|
one of them prints `NOT refreshed` (a format violation, such as a
|
|
67
71
|
hand-written header with no `# canon:` provenance comment, or a
|
|
68
72
|
duplicate id) run `canon inventory sync`: it names each problem; fix
|
|
69
|
-
them, and the next command indexes the files.
|
|
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`.
|
|
70
77
|
4. **Units.** Split the work into units (for example specs, domain code,
|
|
71
78
|
assets, review). Give each unit its own actor id and session id, and pass
|
|
72
79
|
them on everything the unit records (`--actor-id <unit> --session-id <session>`),
|
|
@@ -114,12 +121,14 @@ work inside an approved scope, never to the approval itself.
|
|
|
114
121
|
blocker findings); fix the gap rather than overriding, and finish with
|
|
115
122
|
`canon gate check` clean. A root whose `.feature` edits could not be
|
|
116
123
|
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
|
-
|
|
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
|
|
123
132
|
ship guard only prints an advisory). Move the subject to
|
|
124
133
|
`shipped` yourself once `canon subject status <id> shipped` passes its
|
|
125
134
|
guard, unless the brief says the human ships. `canon status` shows
|
|
@@ -137,13 +146,18 @@ Load only the reference the current step needs.
|
|
|
137
146
|
### Setup and provider projection
|
|
138
147
|
|
|
139
148
|
```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
|
|
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
|
|
143
153
|
canon skills check # read-only drift check
|
|
144
|
-
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
|
|
145
155
|
```
|
|
146
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
|
+
|
|
147
161
|
The canonical bundle is read-only. Claude receives
|
|
148
162
|
`.claude/skills/canon/SKILL.md`; Codex receives
|
|
149
163
|
`.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. |
|
|
@@ -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`:
|
|
@@ -489,12 +515,19 @@ empty clean map.
|
|
|
489
515
|
## `canon gate task <task_id> [--repo <dir>]`
|
|
490
516
|
|
|
491
517
|
The evidence-gated task checkbox flip. Resolves `<task_id>`
|
|
492
|
-
(`<change_id>#<n>`) through
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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).
|
|
498
531
|
It requires
|
|
499
532
|
a matching non-`Divergent` evidence record, and flips `- [ ]` → `- [x]`
|
|
500
533
|
with an appended evidence note ONLY on a clean check. The note
|
|
@@ -526,6 +559,13 @@ canon gate task my-change#5.2 --repo .
|
|
|
526
559
|
This is canon's own authority for the checkbox grammar — always flip
|
|
527
560
|
through `canon gate task`, never by hand.
|
|
528
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
|
+
|
|
529
569
|
## `canon gate promote [--repo <dir>] [--dry-run]`
|
|
530
570
|
|
|
531
571
|
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** —
|
|
@@ -192,6 +192,29 @@ write (promote: manual)`, or `[FAIL] write: …`. `canon gate promote` still
|
|
|
192
192
|
works under `auto` and is idempotent: with nothing staged it prints
|
|
193
193
|
`nothing staged` and exits `0`.
|
|
194
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
|
+
|
|
195
218
|
## The scenario index
|
|
196
219
|
|
|
197
220
|
`canon inventory sync` materializes one `Scenario` record per
|
|
@@ -207,7 +230,7 @@ that no `.feature` file of it declares any more. The reader then runs
|
|
|
207
230
|
covers the whole root) and prints one stderr line:
|
|
208
231
|
|
|
209
232
|
```
|
|
210
|
-
canon status: refreshed the scenario index from 1 changed feature file(s) (specs/
|
|
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
|
|
211
234
|
```
|
|
212
235
|
|
|
213
236
|
A deleted scenario is not erased: `canon inventory sync` — and a reader's
|
|
@@ -148,27 +148,39 @@ empty set; so is one whose `scenario` records route away from the
|
|
|
148
148
|
gate's rung. Each refusal prints by failure class (`uncovered-cell`),
|
|
149
149
|
exits 1, and the status stays `verifying`. `retired` is not gated.
|
|
150
150
|
|
|
151
|
-
**The brief (0.16):** `verifying → shipped` also refuses where a brief
|
|
151
|
+
**The brief (0.16, 0.17):** `verifying → shipped` also refuses where a brief
|
|
152
152
|
exists and is not approved, read exactly as `canon status` reads it (one
|
|
153
153
|
shared check, so status says `ship` only when this passes): the subject's
|
|
154
|
-
newest adopted change has a
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
newest adopted change has a canon `.canon/changes/<change>/brief.md`
|
|
155
|
+
with no `Approved:` line outside HTML comments and code fences, or has no
|
|
156
|
+
canon `brief.md` yet but comes from an external openspec plan with a
|
|
157
|
+
`proposal.md` under the plan sources. It runs with or without a policy.
|
|
158
|
+
The refusal is an `unreviewed-promotion` naming the subject, and says
|
|
159
|
+
what to do:
|
|
158
160
|
|
|
159
161
|
```
|
|
160
|
-
canon subject status: unreviewed-promotion demo-subject — verifying → shipped: brief not approved —
|
|
162
|
+
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
163
|
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
164
|
```
|
|
163
165
|
|
|
166
|
+
For an external change with no canon brief yet, the reason reads
|
|
167
|
+
``.canon/changes/<c>/brief.md does not exist yet: change `<c>` comes from
|
|
168
|
+
the external plan <proposal>, which canon reads and never edits, so its
|
|
169
|
+
approval lives in that canon file`` and the agent will ``create
|
|
170
|
+
.canon/changes/<c>/brief.md holding a `Source: <proposal>` line and
|
|
171
|
+
record their words there as `Approved: <who> <when> — "<their words>"` ``.
|
|
172
|
+
|
|
164
173
|
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
|
-
|
|
174
|
+
answer (no adopted change, or a newest change with neither a canon
|
|
175
|
+
`brief.md` nor an external `proposal.md`), the move is not refused: it
|
|
176
|
+
prints ``canon subject status: advisory: <id>'s brief is not approved —
|
|
177
|
+
its newest adopted change `<c>` has no brief (no
|
|
178
|
+
.canon/changes/<c>/brief.md, no openspec proposal.md), so there is no
|
|
179
|
+
brief to answer and shipping is not refused`` (or `— <id> adopts no
|
|
180
|
+
change, so …`), and status shows the brief as not approved, as an
|
|
181
|
+
advisory. `subjects[].brief.enforced` in `canon status --json` says
|
|
182
|
+
which case applies. A brief that says the human ships changes who runs
|
|
183
|
+
the transition, not the check.
|
|
172
184
|
|
|
173
185
|
This refusal is a backstop, and so is the evidence one: shipping is not
|
|
174
186
|
the first thing an unapproved brief stops. The agent writes the brief,
|
|
@@ -265,21 +277,33 @@ it.
|
|
|
265
277
|
|
|
266
278
|
### Starting a new change: `canon change new`
|
|
267
279
|
|
|
268
|
-
For a change that does not exist yet, one command
|
|
269
|
-
|
|
280
|
+
For a change that does not exist yet, one command writes it and records
|
|
281
|
+
it adopted:
|
|
270
282
|
|
|
271
283
|
```bash
|
|
272
284
|
canon change new add-login --subject auth --title "Add login"
|
|
273
285
|
```
|
|
274
286
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
287
|
+
```
|
|
288
|
+
canon change new: wrote .canon/changes/add-login/brief.md
|
|
289
|
+
canon change new: wrote .canon/changes/add-login/tasks.md
|
|
290
|
+
canon change new: recorded change `add-login` adopted into subject `auth` — written directly; nothing to promote
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
- **Writes** canon's own change dir, `.canon/changes/<slug>/`:
|
|
294
|
+
`brief.md` (`# <title>`, `## Why` holding the title, which imports as
|
|
295
|
+
the change summary, `## What Changes` with a comment saying what goes
|
|
296
|
+
there, and a comment naming where the human's `Approved:` line goes,
|
|
297
|
+
so the file starts unapproved) and `tasks.md` (no rows yet, so the
|
|
298
|
+
change imports as `proposed`; the openspec checkbox row grammar).
|
|
299
|
+
`.canon/changes/` is canon's native plan source: every plan reader
|
|
300
|
+
reads it with no `plans:` entry, so `canon.yaml` needs no edit and no
|
|
301
|
+
external plans source is needed (see `canon-plan-import`).
|
|
302
|
+
- **Reads** the files with the native `canon` plan adapter `canon ingest
|
|
281
303
|
plans` uses, then **records** the change already adopted, through the
|
|
282
|
-
same write as `canon subject adopt`.
|
|
304
|
+
same write as `canon subject adopt`. Its records carry the actor
|
|
305
|
+
`canon-plan-import-canon` (canon-written); an external openspec
|
|
306
|
+
change's carry `canon-plan-import-openspec`.
|
|
283
307
|
- **What a failure leaves behind.** The files are staged under `.canon/`
|
|
284
308
|
and the change dir is moved into place only once they parse. A failure
|
|
285
309
|
before any record is written (staging, publishing the dir, or the
|
|
@@ -291,14 +315,23 @@ canon change new add-login --subject auth --title "Add login"
|
|
|
291
315
|
plans && canon subject adopt <slug> --subject <id>`, which completes
|
|
292
316
|
the link and is safe to rerun.
|
|
293
317
|
- **Refuses** with exit `2`, writing nothing, when the subject does not
|
|
294
|
-
exist, when the slug already has a
|
|
295
|
-
|
|
318
|
+
exist, or when the slug already has a `.canon/changes/<slug>` dir
|
|
319
|
+
(active or under `.canon/changes/archive/`) or a `change` record.
|
|
296
320
|
|
|
297
321
|
Add task rows to `tasks.md` as `- [ ] <n> <title>`, and flip each with
|
|
298
322
|
`canon gate task <slug>#<n>` once evidence exists. `canon evidence add
|
|
299
323
|
--task` and the flip read `tasks.md` as it is, so a new row needs no
|
|
300
324
|
`canon ingest plans` run.
|
|
301
325
|
|
|
326
|
+
A change from an external plan (an openspec change listed under
|
|
327
|
+
`plans.sources`, imported by `canon ingest plans` and adopted with
|
|
328
|
+
`canon subject adopt`) gets its brief as a canon file too:
|
|
329
|
+
`.canon/changes/<change>/brief.md` holding a `Source: <path>` line that
|
|
330
|
+
points at the external document and, once the human answers, the
|
|
331
|
+
`Approved:` line. Canon never edits the external document. Such a
|
|
332
|
+
pointer `brief.md` is not a change of its own: the native reader skips
|
|
333
|
+
it, so the external source keeps the change, its summary and its tasks.
|
|
334
|
+
|
|
302
335
|
## Where are we: `canon status`
|
|
303
336
|
|
|
304
337
|
```bash
|
|
@@ -332,23 +365,33 @@ uses, so status and the gate cannot disagree about a gap:
|
|
|
332
365
|
`require_review` on, a `warning:` line counts the in-effect reviews that
|
|
333
366
|
predate review pinning (0.15), checked against the commit each pinned:
|
|
334
367
|
current, stale (named) and undecidable.
|
|
335
|
-
- **Brief** (0.16) — for each `building`, `verifying` or `shipped`
|
|
368
|
+
- **Brief** (0.16, 0.17) — for each `building`, `verifying` or `shipped`
|
|
336
369
|
subject, and each `proposed` subject whose newest adopted change has a
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
`brief:
|
|
340
|
-
`
|
|
341
|
-
|
|
370
|
+
brief (a canon `brief.md`, or an external `proposal.md`), a `brief:`
|
|
371
|
+
line under it says whether the brief is approved:
|
|
372
|
+
`brief: approved in <brief> — <the recorded words>`, or
|
|
373
|
+
`brief: not approved (…)`. Approval is an `Approved:` line in
|
|
374
|
+
`.canon/changes/<change>/brief.md` for the subject's newest adopted
|
|
375
|
+
change (the last one adopted; a live dir before an archived one), and
|
|
376
|
+
only there: a line that starts with `Approved:` (indent allowed), says
|
|
342
377
|
something after it, and sits outside HTML comments (`<!-- … -->`,
|
|
343
378
|
multi-line included) and fenced code blocks (```` ``` ```` or `~~~`),
|
|
344
379
|
written by the agent once the human answers, as
|
|
345
380
|
`Approved: <who> <when> — "<the human's words>"`. Fixes and follow-ups
|
|
346
381
|
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
|
-
|
|
382
|
+
new scope: an older change's approval does not cover it. A change from
|
|
383
|
+
an external plan has its brief in the same canon file, with a
|
|
384
|
+
`Source: <path>` line pointing at the external document; an `Approved:`
|
|
385
|
+
line inside an external `proposal.md` is not read. Before that canon
|
|
386
|
+
file exists, an external openspec `proposal.md` under the plan sources
|
|
387
|
+
(the first source holding the change; with no `plans:` section, the
|
|
388
|
+
`openspec` source rooted at the repo that `canon gate task` also falls
|
|
389
|
+
back to) still makes the brief one to answer, and the line reads
|
|
390
|
+
``brief: not approved (.canon/changes/<c>/brief.md does not exist yet:
|
|
391
|
+
change `<c>` comes from the external plan <proposal>, which canon reads
|
|
392
|
+
and never edits, so its approval lives in that canon file)``. With a
|
|
393
|
+
canon brief and no line it reads
|
|
394
|
+
``brief: not approved (<brief> has no `Approved:` line)``.
|
|
352
395
|
There is no record kind for it; the file is the record.
|
|
353
396
|
- **Unowned** — scenarios with no `@subject:` tag.
|
|
354
397
|
- **Audits** — how many scenarios are sampled for
|
|
@@ -372,7 +415,7 @@ uses, so status and the gate cannot disagree about a gap:
|
|
|
372
415
|
itself first, so this means the refresh refused on a format violation
|
|
373
416
|
or a duplicate id, which the sync names). Then per subject, verifying
|
|
374
417
|
first, then building, specced, proposed, shipped: an unapproved brief
|
|
375
|
-
(building and later, and proposed once its change has a
|
|
418
|
+
(building and later, and proposed once its change has a brief)
|
|
376
419
|
→ the human step below, which is then the subject's
|
|
377
420
|
only step; no scenario → `canon scenario new … --subject <id>` (and, when no
|
|
378
421
|
`.feature` file holds a scenario yet, `canon feature new` first, unless
|
|
@@ -389,12 +432,20 @@ uses, so status and the gate cannot disagree about a gap:
|
|
|
389
432
|
carries `--project`: the owning root for a missing case, `<root-id>`
|
|
390
433
|
(the reason lists the roots) for a subject that owns no scenario yet.
|
|
391
434
|
- **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
|
-
|
|
435
|
+
a subject whose brief is enforced (its newest change's canon
|
|
436
|
+
`brief.md` has no `Approved:` line, or it has no canon `brief.md` yet
|
|
437
|
+
and comes from an external openspec `proposal.md`) gets
|
|
438
|
+
`human: stop and report the brief in <brief>; build and attest nothing for <id> until the human answers (one answer approves brief and policy)`,
|
|
439
|
+
where `<brief>` is the canon `brief.md`, or the external `proposal.md`
|
|
440
|
+
when no canon brief exists yet. Its reason reads ``<id>: <why
|
|
441
|
+
unanswered> (an older change's approval does not cover a new scope),
|
|
442
|
+
and the approval is never assumed: `canon evidence add` refuses <id>'s
|
|
443
|
+
scenarios and tasks until it is recorded; once the human answers,
|
|
444
|
+
<record> and run `canon policy approve --by <the human> --note "<their
|
|
445
|
+
words>"` ``, where `<record>` is ``record their words as `Approved: <who>
|
|
446
|
+
<when> — "<their words>"` in <brief>``, or, for an external change,
|
|
447
|
+
``create <brief> holding a `Source: <proposal>` line and record their
|
|
448
|
+
words there as `Approved: <who> <when> — "<their words>"` ``.
|
|
398
449
|
It is the subject's only step, whatever its status: nothing suggests
|
|
399
450
|
`canon feature new`, `canon scenario new`, `canon evidence add`, a
|
|
400
451
|
review or a move-on for it before the human answers, and
|
|
@@ -402,9 +453,9 @@ uses, so status and the gate cannot disagree about a gap:
|
|
|
402
453
|
approval is never assumed. A subject with no brief to answer gets no human step;
|
|
403
454
|
its `brief:` line says
|
|
404
455
|
`not approved (no adopted change; advisory, nothing to approve yet)` or
|
|
405
|
-
``not approved (change `<id>` has no
|
|
456
|
+
``not approved (change `<id>` has no brief; advisory, nothing to
|
|
406
457
|
approve yet)`` and nothing is withheld (a `proposed` subject with no
|
|
407
|
-
|
|
458
|
+
brief, and a `specced` subject, show no `brief:` line at all). Once nothing blocks, a
|
|
408
459
|
ship-ready verifying subject gets the agent's own
|
|
409
460
|
`canon subject status <id> shipped`; status never asks a human to ship.
|
|
410
461
|
|
|
@@ -424,21 +475,21 @@ Status fails closed rather than suggest progress the gate would refuse:
|
|
|
424
475
|
|
|
425
476
|
```json
|
|
426
477
|
{
|
|
427
|
-
"statusVersion":
|
|
428
|
-
"canonVersion": "0.
|
|
478
|
+
"statusVersion": 3,
|
|
479
|
+
"canonVersion": "0.17.0",
|
|
429
480
|
"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
481
|
"warnings": [],
|
|
431
482
|
"subjects": [
|
|
432
483
|
{ "id": "cart", "title": "Cart", "status": "building", "scenarios": 2, "excluded": 0, "evidenced": 2, "divergent": 0,
|
|
433
484
|
"reviewed": 1, "staleReviews": 0, "unsignedReviews": 0, "reviewDue": true, "openBlockers": 1, "missingCases": [],
|
|
434
|
-
"brief": { "approved": true, "enforced": false, "change": "c-cart", "
|
|
485
|
+
"brief": { "approved": true, "enforced": false, "change": "c-cart", "brief": ".canon/changes/c-cart/brief.md", "briefExists": true, "approval": "Journey 2026-10-11 — \"approved\"" } },
|
|
435
486
|
{ "id": "promo", "title": "Promo", "status": "verifying", "scenarios": 1, "excluded": 0, "evidenced": 1, "divergent": 0,
|
|
436
487
|
"reviewed": 1, "staleReviews": 0, "unsignedReviews": 0, "reviewDue": true, "openBlockers": 0, "missingCases": [],
|
|
437
|
-
"brief": { "approved": false, "enforced": true, "change": "c-promo", "
|
|
488
|
+
"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
489
|
],
|
|
439
490
|
"unowned": ["cart.promo.01"],
|
|
440
491
|
"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
|
|
492
|
+
{ "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
493
|
{ "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
494
|
{ "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
495
|
{ "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 +502,29 @@ Each `missingCases` entry is `{ "projectId", "surface", "case" }`: the
|
|
|
451
502
|
spec root, the `<area>.<surface>`, and the required case it lacks.
|
|
452
503
|
|
|
453
504
|
`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
|
-
|
|
505
|
+
and for a `proposed` subject whose newest adopted change has a brief (a
|
|
506
|
+
canon `brief.md` or an external `proposal.md`). `change` is the newest
|
|
507
|
+
adopted change (omitted when the subject adopts none); `brief` its canon
|
|
508
|
+
`.canon/changes/<change>/brief.md`, the one file the approval is read
|
|
509
|
+
from (omitted when the change has no brief to answer); `briefExists`
|
|
510
|
+
(always present) whether that file exists (`false` for an external
|
|
511
|
+
change whose canon brief is still to be created); `source` the external
|
|
512
|
+
plan document the change comes from (the canon brief's `Source:` line,
|
|
513
|
+
or, before that file exists, the external `proposal.md`; omitted for a
|
|
514
|
+
canon-written change); `approval` the text after `Approved:`; and
|
|
515
|
+
`enforced` whether the missing approval blocks (a brief exists to
|
|
516
|
+
answer). The 0.16 `proposal` field is gone.
|
|
460
517
|
|
|
461
518
|
Each `next` entry's `actor` is `agent` (its `command` is a canon command
|
|
462
519
|
line) or `human` (its `command` says what to get from the human).
|
|
463
520
|
|
|
464
521
|
`policy.spec_coverage` has the shape `canon context --json` prints
|
|
465
522
|
(`null` when absent, `{"invalid": …}` when unusable). `statusVersion`
|
|
466
|
-
changes only when a field changes meaning or is removed. It
|
|
467
|
-
|
|
468
|
-
(`actor` tells which)
|
|
523
|
+
changes only when a field changes meaning or is removed. It became `2`
|
|
524
|
+
in 0.16, when `next[].command` stopped always being a command line
|
|
525
|
+
(`actor` tells which), and `brief` was added alongside. It is `3` since
|
|
526
|
+
0.17, when `brief.proposal` was removed: the canon `brief.md` is
|
|
527
|
+
`brief.brief`, with `briefExists` and `source`.
|
|
469
528
|
|
|
470
529
|
`subject new`, `adopt` and `status` write their records directly; the
|
|
471
530
|
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.17.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.17.0",
|
|
34
|
+
"@journeykit/canon-core-linux-x64": "0.17.0",
|
|
35
|
+
"@journeykit/canon-core-win32-x64": "0.17.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^20.0.0",
|