@journeykit/canon 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,11 +20,13 @@ authored in one place:
20
20
  - **Changes**: `change_ids` on the Subject, appended by `canon subject
21
21
  adopt`.
22
22
  - **Scenarios**: the `@subject:<id>` tag on each Gherkin scenario,
23
- indexed onto `Scenario.subject_id` by `canon inventory sync`. The
24
- Subject record carries no scenario list — the spec corpus is the only
25
- source, and the Subjects report panel, `spec_coverage.scope`, and the
26
- shipped gate all read it from there. To link a scenario, tag it and
27
- re-sync; there is no subject-side command for it.
23
+ indexed onto `Scenario.subject_id` by the scenario index (rebuilt by
24
+ `canon inventory sync`, and refreshed by every command that reads it).
25
+ The Subject record carries no scenario list — the spec corpus is the
26
+ only source, and the Subjects report panel, `spec_coverage.scope`, and
27
+ the shipped gate all read it from there. To link a scenario, tag it;
28
+ the next `canon status` or `canon gate check` indexes the tag. There is
29
+ no subject-side command for it.
28
30
 
29
31
  ## The 4-step loop
30
32
 
@@ -81,9 +83,10 @@ canon retrieve --role dev --domain dev --subject subject-domain-loop --k 5
81
83
  ### 3. Work → pin every artifact to `subject_id`
82
84
 
83
85
  - **Scenarios**: `canon scenario new <tag> --subject <subject-id>` emits
84
- the `@subject:<subject-id>` tag; `canon inventory sync` maps the tag
85
- onto the scenario's `subject_id`. A malformed/absent tag leaves it
86
- unset (fail-soft), never an error. See `canon-authoring`.
86
+ the `@subject:<subject-id>` tag; the scenario index maps the tag onto
87
+ the scenario's `subject_id` the next time a command reads it (no sync
88
+ step; see `canon-inventory`). A malformed/absent tag leaves it unset
89
+ (fail-soft), never an error. See `canon-authoring`.
87
90
  - **Changes**: adopt imported plan changes under the Subject (see
88
91
  [Adopt flow](#adopt-flow)).
89
92
  - **Reviews / evidence / divergences**: authored as usual (`canon review
@@ -145,6 +148,36 @@ empty set; so is one whose `scenario` records route away from the
145
148
  gate's rung. Each refusal prints by failure class (`uncovered-cell`),
146
149
  exits 1, and the status stays `verifying`. `retired` is not gated.
147
150
 
151
+ **The brief (0.16):** `verifying → shipped` also refuses where a brief
152
+ exists and is not approved, read exactly as `canon status` reads it (one
153
+ shared check, so status says `ship` only when this passes): the subject's
154
+ newest adopted change has a `proposal.md` under the openspec plan sources,
155
+ and it carries no `Approved:` line outside HTML comments and code fences.
156
+ It runs with or without a policy. The refusal is an `unreviewed-promotion`
157
+ naming the subject, and says what to do:
158
+
159
+ ```
160
+ canon subject status: unreviewed-promotion demo-subject — verifying → shipped: brief not approved — openspec/changes/c-demo/proposal.md has no `Approved:` line; a human must answer the brief, and the agent records `Approved: <who> <when> — "<their words>"` there only from that answer
161
+ canon subject status: refused — get the human's answer to the brief and record it as its `Approved:` line, or pass --override-reason <text> to move anyway; the waiver is recorded on the subject
162
+ ```
163
+
164
+ Never write the line without the human's answer. When there is no brief to
165
+ answer (no openspec plan source, no adopted change, or a change with no
166
+ `proposal.md`), the move is not refused: it prints
167
+ `canon subject status: advisory: <id>'s brief is not approved — <why>, so
168
+ there is no brief to answer and shipping is not refused`, and status shows
169
+ the brief as not approved, as an advisory. `subjects[].brief.enforced` in
170
+ `canon status --json` says which case applies. A brief that says the human
171
+ ships changes who runs the transition, not the check.
172
+
173
+ This refusal is a backstop, and so is the evidence one: shipping is not
174
+ the first thing an unapproved brief stops. The agent writes the brief,
175
+ then stops and reports it, and nothing is built or attested for the
176
+ subject until the human answers. `canon evidence add` refuses (exit 1,
177
+ nothing staged) a scenario whose `@subject` owner, or a task whose change
178
+ an adopting subject, has an enforced, unapproved brief, through the same
179
+ check; see `canon-gate`.
180
+
148
181
  ### The review guard (`spec_coverage.require_review`, opt-in)
149
182
 
150
183
  When `.canon/policy.yaml` sets `spec_coverage.require_review` (see
@@ -162,16 +195,21 @@ unreviewed-promotion — `specced` is not in require_review.scope`. A
162
195
  transition INTO an in-scope status with gaps — an owned scenario with no
163
196
  qualifying review (`unreviewed-promotion`), or an open blocker finding
164
197
  on an adopted change (`open-blocker`) — prints the violations, exits 1,
165
- leaves the record unchanged, and names `--override-reason`.
198
+ leaves the record unchanged, and names `--override-reason`. `shipped`
199
+ also refuses a `false-success` (a scenario whose latest audit is
200
+ `refuted`) and, when `require_audit.scope` covers `shipped`, an owed
201
+ sampled audit; see `canon-gate`.
166
202
 
167
203
  ```bash
168
204
  canon subject status demo-subject verifying \
169
205
  --override-reason "reviewer out until Monday" --actor-id lead
170
206
  ```
171
207
 
172
- `--override-reason "<one line>"` waives ONLY `unreviewed-promotion` and
173
- `open-blocker` — never a ship-gate verdict or case gap, never unreadable
174
- routing. It prints the `waived ...` lines and `override recorded by
208
+ `--override-reason "<one line>"` waives ONLY `unreviewed-promotion` (the
209
+ review gaps and the brief check) and `open-blocker` — never a ship-gate
210
+ verdict or case gap, never unreadable routing. A waived brief is recorded
211
+ as `{ class: unreviewed-promotion, subject: <subject id> }`. It prints
212
+ the `waived ...` lines and `override recorded by
175
213
  `lead` for the N violation(s) above: <reason>`, and records on the new
176
214
  Subject record exactly the violations it let through:
177
215
 
@@ -189,13 +227,14 @@ subject's next status write, so it never outlives the status it was
189
227
  granted for. While it stands, `canon gate check` lists the recorded gaps
190
228
  as advisories instead of failing; a gap it did not record (a blocker
191
229
  raised later, a scenario tagged later) still fails the gate. Without
192
- `require_review`, nothing is printed and nothing changes.
230
+ `require_review`, nothing is printed and nothing changes, except that
231
+ `shipped` still runs the brief check above.
193
232
 
194
233
  ## Adopt flow
195
234
 
196
- Planning docs become change/task records via `canon ingest plans` (see
197
- `canon-plan-import`), but that stops at import. `canon subject adopt`
198
- lifts an imported change into a managed Subject:
235
+ Planning docs written outside canon become change/task records via
236
+ `canon ingest plans` (see `canon-plan-import`), but that stops at import.
237
+ `canon subject adopt` lifts an imported change into a managed Subject:
199
238
 
200
239
  ```bash
201
240
  canon ingest plans --repo . # import → change/task rows
@@ -203,6 +242,13 @@ canon subject adopt subject-domain-loop-plan \
203
242
  --subject subject-domain-loop # link change → subject
204
243
  ```
205
244
 
245
+ A change made with `canon change new` (below) is recorded and adopted
246
+ already, and editing its `tasks.md` needs no import either:
247
+ `canon evidence add --task` and `canon gate task` read the file as it is,
248
+ and a flip re-imports that change itself. Run `canon ingest plans` for a
249
+ plan written outside canon, or when `canon query --kind task` must list
250
+ rows nobody has flipped yet.
251
+
206
252
  `adopt` stamps the change's `subject_id` and adds it to the Subject's
207
253
  `change_ids`, so `canon query --kind change --change-id …` and `canon
208
254
  query --kind subject` agree on the link from both ends. It writes only
@@ -248,9 +294,10 @@ canon change new add-login --subject auth --title "Add login"
248
294
  exist, when the slug already has a change dir (active or archived) or a
249
295
  `change` record, or when `canon.yaml` has no `openspec` plans source.
250
296
 
251
- Add task rows to `tasks.md` as `- [ ] <n> <title>`, rerun `canon ingest
252
- plans`, and flip each with `canon gate task <slug>#<n>` once evidence
253
- exists.
297
+ Add task rows to `tasks.md` as `- [ ] <n> <title>`, and flip each with
298
+ `canon gate task <slug>#<n>` once evidence exists. `canon evidence add
299
+ --task` and the flip read `tasks.md` as it is, so a new row needs no
300
+ `canon ingest plans` run.
254
301
 
255
302
  ## Where are we: `canon status`
256
303
 
@@ -274,29 +321,92 @@ uses, so status and the gate cannot disagree about a gap:
274
321
  scenarios it owns through `@subject:` (after `exclude_lanes`, which
275
322
  are counted separately), how many carry a ledger verdict (the
276
323
  `verifying → shipped` gate's reading; divergent ones are counted too),
277
- how many carry a review that satisfies `require_review` (any review
278
- when it is off; with `distinct_actor`, a self-review does not count),
279
- open blocker findings on its adopted changes, and each feature surface
280
- missing a `require_cases` case. `(N due)` marks reviews the next
281
- transition will require.
324
+ how many carry a review that satisfies `require_review` (any current
325
+ signed review when it is off; with `distinct_actor`, a self-review does
326
+ not count), open blocker findings on its adopted changes, and each
327
+ feature surface missing a `require_cases` case. `(N due)` marks reviews
328
+ the next transition will require; `N stale review(s)` and `N unsigned
329
+ review(s)` (printed when non-zero) count scenarios whose review went
330
+ stale or whose latest reviews are all `indeterminate`/`unavailable`
331
+ (see `canon-review`), and the `canon review add` step names both. With
332
+ `require_review` on, a `warning:` line counts the in-effect reviews that
333
+ predate review pinning (0.15), checked against the commit each pinned:
334
+ current, stale (named) and undecidable.
335
+ - **Brief** (0.16) — for each `building`, `verifying` or `shipped`
336
+ subject, and each `proposed` subject whose newest adopted change has a
337
+ `proposal.md`, a `brief:` line under it says whether the brief is approved:
338
+ `brief: approved in <proposal.md> — <the recorded words>`, or
339
+ `brief: not approved (…)`. Approval is an `Approved:` line in the
340
+ `proposal.md` of the subject's newest adopted change (the last one
341
+ adopted): a line that starts with `Approved:` (indent allowed), says
342
+ something after it, and sits outside HTML comments (`<!-- … -->`,
343
+ multi-line included) and fenced code blocks (```` ``` ```` or `~~~`),
344
+ written by the agent once the human answers, as
345
+ `Approved: <who> <when> — "<the human's words>"`. Fixes and follow-ups
346
+ live as tasks in the approved change, so a newly adopted change is a
347
+ new scope: an older change's approval does not cover it. Status reads
348
+ the line from the change dir under the repo's `openspec` plans sources,
349
+ in config order (the first source holding the change owns it, a live
350
+ dir before an archived one; with no `plans:` section, the `openspec`
351
+ source rooted at the repo that `canon gate task` also falls back to).
352
+ There is no record kind for it; the file is the record.
282
353
  - **Unowned** — scenarios with no `@subject:` tag.
283
- - **`next:`** — at most five commands, each with a one-line reason; the
354
+ - **Audits** — how many scenarios are sampled for
355
+ `spec_coverage.require_audit`, confirmed, owed (no audit or
356
+ `indeterminate`) and refuted (`false-success`). An owed or
357
+ refuted audit adds a `next:` step suggesting `canon evidence add
358
+ --kind audit …` (recorded from an external evaluator; canon never runs
359
+ one).
360
+ - **Dropped bindings** — `dropped bindings (N):` lists every recorded
361
+ `--drop-attachment` (scenario, paths, reason); `drops[]` in `--json`.
362
+ - **Brief overrides** — a `warning:` line per evidence record attested
363
+ with `canon evidence add --override-reason` while its subject's brief
364
+ was not approved: what it attests, who, when, the subject and the
365
+ reason (see `canon-gate`).
366
+ - **`next:`** — at most five steps, each with a one-line reason; the
284
367
  rest are counted. Rules, in order: no `canon.yaml` → `canon init`; an
285
368
  unreadable one → `canon init --check-config`; a scenario tagged with a
286
369
  subject that has no record → `canon subject new <that id>`; no subject
287
- at all → `canon subject new`; subjects but no scenario records →
288
- `canon inventory sync`. Then per subject, verifying first, then
289
- building, specced, proposed, shipped: no scenario → `canon scenario new
290
- … --subject <id>`; a missing case → `canon scenario new <surface>.<next
291
- nn> … --case <case>`; (building and later) an unevidenced or divergent
292
- scenario → `canon evidence add --scenario-id …`; an open blocker →
293
- `canon finding close …`; a due, unreviewed, evidenced scenario →
294
- `canon review add …` from another session; none of these →
295
- `canon subject status <id> <next status>`. Last, unowned scenarios →
296
- tag them and `canon inventory sync`. With more than one `specs.roots[]`
297
- entry, every suggested `canon scenario new` carries `--project`: the
298
- owning root for a missing case, `<root-id>` (the reason lists the
299
- roots) for a subject that owns no scenario yet.
370
+ at all → `canon subject new`; subjects and `.feature` scenarios but no
371
+ scenario records → `canon inventory sync` (status refreshes the index
372
+ itself first, so this means the refresh refused on a format violation
373
+ or a duplicate id, which the sync names). Then per subject, verifying
374
+ first, then building, specced, proposed, shipped: an unapproved brief
375
+ (building and later, and proposed once its change has a `proposal.md`)
376
+ → the human step below, which is then the subject's
377
+ only step; no scenario → `canon scenario new … --subject <id>` (and, when no
378
+ `.feature` file holds a scenario yet, `canon feature new` first, unless
379
+ every subject waits on its brief); a missing case → `canon scenario new
380
+ <surface>.<next nn> … --case <case>`; (building and later) an
381
+ unevidenced or divergent scenario → `canon evidence add --scenario-id
382
+ …` (its reason adds "then `canon gate promote`" only under
383
+ `write.promote: manual`); an open blocker → `canon finding close …`; a
384
+ due, unreviewed, evidenced scenario → `canon review add …` from another
385
+ session; none of these → `canon subject status <id> <next status>`.
386
+ Last, unowned scenarios → `canon status`, after tagging them in their
387
+ `.feature` files (status re-indexes the edit; no sync step). With more
388
+ than one `specs.roots[]` entry, every suggested `canon scenario new`
389
+ carries `--project`: the owning root for a missing case, `<root-id>`
390
+ (the reason lists the roots) for a subject that owns no scenario yet.
391
+ - **The one human step.** Every step is the agent's to run except one:
392
+ a subject whose brief is enforced (its newest change has a
393
+ `proposal.md` with no `Approved:` line) gets
394
+ `human: stop and report the brief in <proposal.md>; build and attest nothing for <id> until the human answers (one answer approves brief and policy)`,
395
+ whose reason names that `proposal.md`, says `canon evidence add`
396
+ refuses the subject's scenarios and tasks until the line is recorded,
397
+ and names `canon policy approve --by <the human> --note "<their words>"`.
398
+ It is the subject's only step, whatever its status: nothing suggests
399
+ `canon feature new`, `canon scenario new`, `canon evidence add`, a
400
+ review or a move-on for it before the human answers, and
401
+ `canon subject status <id> shipped` refuses on the same check. The
402
+ approval is never assumed. A subject with no brief to answer gets no human step;
403
+ its `brief:` line says
404
+ `not approved (no adopted change; advisory, nothing to approve yet)` or
405
+ ``not approved (change `<id>` has no proposal.md; advisory, nothing to
406
+ approve yet)`` and nothing is withheld (a `proposed` subject with no
407
+ `proposal.md`, and a `specced` subject, show no `brief:` line at all). Once nothing blocks, a
408
+ ship-ready verifying subject gets the agent's own
409
+ `canon subject status <id> shipped`; status never asks a human to ship.
300
410
 
301
411
  Status fails closed rather than suggest progress the gate would refuse:
302
412
 
@@ -314,19 +424,24 @@ Status fails closed rather than suggest progress the gate would refuse:
314
424
 
315
425
  ```json
316
426
  {
317
- "statusVersion": 1,
318
- "canonVersion": "0.14.0",
427
+ "statusVersion": 2,
428
+ "canonVersion": "0.16.0",
319
429
  "policy": { "present": true, "spec_coverage": { "require_evidence": true, "scope": [], "exclude_lanes": [], "require_cases": ["failure"], "require_review": { "scope": ["verifying", "shipped"], "distinct_actor": true, "block_on_findings": true } } },
320
430
  "warnings": [],
321
431
  "subjects": [
322
432
  { "id": "cart", "title": "Cart", "status": "building", "scenarios": 2, "excluded": 0, "evidenced": 2, "divergent": 0,
323
- "reviewed": 1, "reviewDue": true, "openBlockers": 1, "missingCases": [] }
433
+ "reviewed": 1, "staleReviews": 0, "unsignedReviews": 0, "reviewDue": true, "openBlockers": 1, "missingCases": [],
434
+ "brief": { "approved": true, "enforced": false, "change": "c-cart", "proposal": "openspec/changes/c-cart/proposal.md", "approval": "Journey 2026-10-11 — \"approved\"" } },
435
+ { "id": "promo", "title": "Promo", "status": "verifying", "scenarios": 1, "excluded": 0, "evidenced": 1, "divergent": 0,
436
+ "reviewed": 1, "staleReviews": 0, "unsignedReviews": 0, "reviewDue": true, "openBlockers": 0, "missingCases": [],
437
+ "brief": { "approved": false, "enforced": true, "change": "c-promo", "proposal": "openspec/changes/c-promo/proposal.md" } }
324
438
  ],
325
439
  "unowned": ["cart.promo.01"],
326
440
  "next": [
327
- { "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" },
328
- { "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; …" },
329
- { "command": "canon inventory sync", "why": "1 scenario(s) carry no @subject tag (first: cart.promo.01); …" }
441
+ { "command": "stop and report the brief in openspec/changes/c-promo/proposal.md; build and attest nothing for promo until the human answers (one answer approves brief and policy)", "why": "promo: its newest adopted change's proposal.md carries no `Approved:` line …", "actor": "human" },
442
+ { "command": "canon finding close --change-id c-cart --round 1 --seq 1 --disposition fixed --resolution-sha <sha> --actor-id <unit> --session-id <session>", "why": "cart: 1 open blocker finding(s) (first: …); fix it, commit, then close it", "actor": "agent" },
443
+ { "command": "canon review add --project-id root --scenario-id cart.add.02 --reviewer <reviewer> --actor-id <reviewer> --session-id <review-session> --role reviewer --pin <sha> --original-spec-ref \"<feature file>\"", "why": "cart: 1 of 2 scenario(s) lack a qualifying review; …", "actor": "agent" },
444
+ { "command": "canon status", "why": "1 scenario(s) carry no @subject tag (first: cart.promo.01); add `@subject:<id>` above each in its .feature file, then check again: …", "actor": "agent" }
330
445
  ],
331
446
  "nextOmitted": 0
332
447
  }
@@ -335,9 +450,22 @@ Status fails closed rather than suggest progress the gate would refuse:
335
450
  Each `missingCases` entry is `{ "projectId", "surface", "case" }`: the
336
451
  spec root, the `<area>.<surface>`, and the required case it lacks.
337
452
 
453
+ `brief` is present for `building`, `verifying` and `shipped` subjects,
454
+ and for a `proposed` subject whose newest adopted change has a
455
+ `proposal.md`. `change` is the newest adopted change (omitted when the
456
+ subject adopts none), `proposal` its `proposal.md` (omitted when no
457
+ `openspec` source holds one), `approval` the text after `Approved:`, and
458
+ `enforced` whether the missing approval blocks (a `proposal.md` exists to
459
+ carry it).
460
+
461
+ Each `next` entry's `actor` is `agent` (its `command` is a canon command
462
+ line) or `human` (its `command` says what to get from the human).
463
+
338
464
  `policy.spec_coverage` has the shape `canon context --json` prints
339
465
  (`null` when absent, `{"invalid": …}` when unusable). `statusVersion`
340
- changes only when a field changes meaning or is removed.
466
+ changes only when a field changes meaning or is removed. It is `2`
467
+ since 0.16, when `next[].command` stopped always being a command line
468
+ (`actor` tells which); `brief` was added alongside.
341
469
 
342
470
  `subject new`, `adopt` and `status` write their records directly; the
343
471
  human-readable success line ends `— written directly; nothing to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@journeykit/canon",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "canon — harness knowledge substrate. JS launcher: platform detection + native binary resolution.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "dependencies": {},
32
32
  "optionalDependencies": {
33
- "@journeykit/canon-core-darwin-arm64": "0.14.0",
34
- "@journeykit/canon-core-linux-x64": "0.14.0",
35
- "@journeykit/canon-core-win32-x64": "0.14.0"
33
+ "@journeykit/canon-core-darwin-arm64": "0.16.0",
34
+ "@journeykit/canon-core-linux-x64": "0.16.0",
35
+ "@journeykit/canon-core-win32-x64": "0.16.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^20.0.0",