@erclx/canon 4.69.0 → 4.70.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.
Files changed (37) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/auto-ship/SKILL.md +1 -1
  3. package/claude/skills/draft-and-pick/REQUIREMENT.md +1 -1
  4. package/claude/skills/draft-and-pick/SKILL.md +1 -1
  5. package/claude/skills/{canon-screencast → draft-screencast}/REQUIREMENT.md +4 -4
  6. package/claude/skills/{canon-screencast → draft-screencast}/SKILL.md +4 -4
  7. package/claude/skills/{canon-slides-draft → draft-slides}/REQUIREMENT.md +3 -3
  8. package/claude/skills/{canon-slides-draft → draft-slides}/SKILL.md +2 -2
  9. package/claude/skills/{canon-frames-read → read-frames}/REQUIREMENT.md +2 -2
  10. package/claude/skills/{canon-frames-read → read-frames}/SKILL.md +3 -3
  11. package/claude/skills/{canon-record → record-screencast}/REQUIREMENT.md +5 -5
  12. package/claude/skills/{canon-record → record-screencast}/SKILL.md +4 -4
  13. package/claude/skills/review-pr/SKILL.md +55 -7
  14. package/claude/skills/role-orchestrator/SKILL.md +2 -1
  15. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +7 -3
  16. package/claude/skills/role-orchestrator/scripts/poll.sh +79 -34
  17. package/claude/skills/role-worker/SKILL.md +2 -1
  18. package/docs/agents/commands.md +2 -0
  19. package/docs/agents/demo.md +3 -3
  20. package/docs/agents/index.md +1 -1
  21. package/docs/agents/pr-reads.md +47 -12
  22. package/docs/agents/tasks.md +46 -3
  23. package/docs/workflow/ai-workflow.md +19 -19
  24. package/package.json +3 -2
  25. package/scripts/core/regen-web-previews.ts +94 -0
  26. package/src/claude/cases/workflow.ts +4 -4
  27. package/src/commands/demo.ts +1 -1
  28. package/src/commands/pr.ts +130 -1
  29. package/src/commands/tasks.ts +178 -1
  30. package/src/demo/beats.ts +1 -1
  31. package/src/migrate/skill-names.ts +15 -1
  32. package/src/pr/review-scope.ts +177 -0
  33. package/src/tasks/archive.ts +206 -3
  34. package/src/tasks/label.ts +14 -6
  35. package/src/tasks/validate.ts +22 -0
  36. package/standards/plan.md +1 -1
  37. package/standards/tasks.md +15 -1
@@ -32,6 +32,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
32
32
  | `canon feedback` | Write toolkit feedback from stdin to `.canon/review/feedback/`, or open a GitHub issue with `--github`, refusing either when a required field is absent |
33
33
  | `canon transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
34
34
  | `canon tasks archive` | Move a shipped task and its plan off the board, clear its ordering row, and regenerate the index |
35
+ | `canon tasks decline` | Move a decided-against task and its plan into `.canon/tasks/declined/`, recording who decided and why (`--reason`, `--by`, `--json`) |
35
36
  | `canon tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
36
37
  | `canon tasks plan-link` | Write or correct a task's `Plan:` line to point at a plan, by stem and plan path or slug (`--json`) |
37
38
  | `canon tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
@@ -81,6 +82,7 @@ Full help: `canon <command> --help`. Behavior notes for the install and sync ver
81
82
  | `canon pr key-changes` | Compare the files a pull request body's Key Changes names against its own diff, in both directions (`--body`, `--base`, `--json`) |
82
83
  | `canon pr head` | Compare the head a pull request object reports against the branch tip the remote carries, naming both shas (`--root`, `--json`) |
83
84
  | `canon pr checks` | Report the check runs belonging to the branch tip, reading pending for a tip carrying none rather than clean (`--root`, `--json`) |
85
+ | `canon pr review-state` | Report the commit and instant the last review pass covered, off the pass's own marker rather than the fields GitHub stamps at submission (`--root`, `--json`) |
84
86
  | `canon repo metadata propose` | Compare a description, homepage, and topic set computed from the README and `package.json` against what the remote carries, writing nothing (`--root`, `--json`) |
85
87
  | `canon repo metadata apply` | Write an explicitly supplied description, homepage, or topic set to the remote through `gh repo edit` (`--description`, `--homepage`, `--topics`, `--root`, `--json`) |
86
88
  | `canon census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
@@ -18,13 +18,13 @@ canon demo frames demos/inline-edit.webm --fps 2
18
18
 
19
19
  ## The draft and the plan are different files
20
20
 
21
- `canon-screencast` drafts beats for a person. A beat carries what is on screen, one verb, what to watch for, an emphasis, and a caption, and none of that names a selector, a URL, a wait condition, or a timing. Those four are what an executor needs and what would ruin the draft, since the format is pre-seeded so the operator edits down rather than fills blanks.
21
+ `draft-screencast` drafts beats for a person. A beat carries what is on screen, one verb, what to watch for, an emphasis, and a caption, and none of that names a selector, a URL, a wait condition, or a timing. Those four are what an executor needs and what would ruin the draft, since the format is pre-seeded so the operator edits down rather than fills blanks.
22
22
 
23
23
  So `canon demo compile` writes a second artifact rather than adding fields to a beat. It maps each beat's verb to a step, seeds the timing, marks which beat the still comes from, and leaves the target and the URL empty for a person to fill. The report names every field it could not supply.
24
24
 
25
25
  The plan is committed, not scratch. Its timing is a starting point tuned by watching a recording, and the draft cannot reproduce a tuned value, so a recompile over an existing plan refuses and names `--force`.
26
26
 
27
- `canon-record` is the routed way to run both verbs from a draft path: it compiles only when no plan exists yet, then runs, stopping to report any field still unresolved rather than guessing one.
27
+ `record-screencast` is the routed way to run both verbs from a draft path: it compiles only when no plan exists yet, then runs, stopping to report any field still unresolved rather than guessing one.
28
28
 
29
29
  | Option | Behavior |
30
30
  | --------------- | ----------------------------------------------------------------- |
@@ -64,7 +64,7 @@ A step waits on its `waitFor` selector becoming visible and then holds for its o
64
64
  | `--fps <n>` | Frames extracted per second of video, default `1` |
65
65
  | `--json` | Add a record on stdout carrying every frame path written |
66
66
 
67
- `canon-frames-read` is the routed way to call this verb and read the frames back: it runs the verb, reads each returned frame with the Read tool, and reports one plain description per frame. It never renders a verdict, since a frame read is evidence a person weighs rather than a pass or fail this toolkit states on their behalf.
67
+ `read-frames` is the routed way to call this verb and read the frames back: it runs the verb, reads each returned frame with the Read tool, and reports one plain description per frame. It never renders a verdict, since a frame read is evidence a person weighs rather than a pass or fail this toolkit states on their behalf.
68
68
 
69
69
  ## The pointer is painted inside the page
70
70
 
@@ -28,7 +28,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
28
28
  - [Markdown audit](markdown-audit.md): Running the audit over any markdown path, where its bans and checkpoints are read from, what each check reports, and why the ban half gates while the structural half reports
29
29
  - [Output shape](output-shape.md): Two framed shapes every command renders into, how JSON and --names modes keep stdout clean, and the exit discipline that lets piped output drain
30
30
  - [Overview](overview.md): What this folder covers, the invocation rules every command inherits, and where domain behavior is documented instead
31
- - [Head-sensitive pull request reads](pr-reads.md): Resolving a branch tip from the remote rather than from the pull request object, reading check runs keyed on that tip, why an empty run list is not a pass, the refusal reasons each verb names, and what the remote read costs
31
+ - [Head-sensitive pull request reads](pr-reads.md): Resolving a branch tip from the remote rather than from the pull request object, reading what a review pass covered off its own marker rather than off GitHub's submission stamps, why an empty run list is not a pass, the refusal reasons each verb names, and what the remote read costs
32
32
  - [Records](records.md): The two roots a record folder resolves at, validating the session records and the standards corpus, the per-kind checks, the refusal reasons, migrating a record a frontmatter change orphaned, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
33
33
  - [Restated instructions](restated.md): Counting the instructions the always-loaded file and every path-scoped rule share with the seed, the shipped skill bodies, and each other, how a match is decided, the three classes, which surface a later edit starts from, and why the sweep reports rather than gates
34
34
  - [Review classification](review-classification.md): Deciding whether a changed set needs the review pass, the two tests it runs, why an empty set refuses rather than skipping, and the written fallback a target on an older binary falls back to
@@ -1,17 +1,19 @@
1
1
  ---
2
2
  title: Head-sensitive pull request reads
3
- description: Resolving a branch tip from the remote rather than from the pull request object, reading check runs keyed on that tip, why an empty run list is not a pass, the refusal reasons each verb names, and what the remote read costs
3
+ description: Resolving a branch tip from the remote rather than from the pull request object, reading what a review pass covered off its own marker rather than off GitHub's submission stamps, why an empty run list is not a pass, the refusal reasons each verb names, and what the remote read costs
4
4
  ---
5
5
 
6
6
  # Head-sensitive pull request reads
7
7
 
8
- `canon pr head` and `canon pr checks` answer about a commit. Both resolve the branch tip from the remote with `git ls-remote` and report what they found for that sha, rather than taking the pull request object's word for which commit the branch is on.
8
+ `canon pr head`, `canon pr checks`, and `canon pr review-state` each answer about a commit, and each exists because the field GitHub offers answers a different question. The first two resolve the branch tip from the remote with `git ls-remote` rather than taking the pull request object's word for which commit the branch is on. The third reads what a review pass covered off a marker the pass wrote, rather than off the head GitHub stamped when the review was submitted.
9
9
 
10
10
  ```bash
11
11
  canon pr head
12
12
  canon pr head 1341 --json
13
13
  canon pr checks
14
14
  canon pr checks 1341 --json
15
+ canon pr review-state
16
+ canon pr review-state 1341 --json
15
17
  ```
16
18
 
17
19
  ## The pull request object is not the authority for a head
@@ -30,8 +32,40 @@ The argument is stated here once. The shipped skill bodies call the verb rather
30
32
 
31
33
  `canon pr checks` reads `repos/{owner}/{repo}/commits/<tip>/check-runs` and collapses it to `passing`, `failing`, or `pending`. A failure outranks a run still going, since a run in flight cannot clear a job that already failed.
32
34
 
35
+ `canon pr review-state` reports `heading` and `state` for the newest pass in the review family, `commit` and `readAt` for what it covered, `submittedAt` for when GitHub recorded it, and `source` for which of those readings answered. A thread carrying no pass reports `state` and `source` both as `none` with nothing else on the record.
36
+
33
37
  `gh pr checks` cannot be made to answer this question at all, which is the argument for the move rather than a preference between two working commands. Its `--json` field set is `bucket, completedAt, description, event, link, name, startedAt, state, workflow`, with no sha among them, so a caller cannot even learn which commit its answer describes.
34
38
 
39
+ ## A review's stamps describe the submission, not the read
40
+
41
+ `commit.oid` on a review names whatever the pull request head was at the instant the review was submitted. `submittedAt` names that instant. Neither names the commit the reviewing session actually opened, and the two are only the same when nothing was pushed while the comment was being written.
42
+
43
+ A push landing in that window moves both onto a commit nobody reviewed. The next pass then scopes its delta past that commit and reports it covered, which is the failure direction that loses work: the commit is skipped rather than re-read. It fired on a pull request in this toolkit on 2026-09-07, where a pass read one commit, the branch pushed another during the compose, and the poll reported the second as already seen. What it skipped was a genuine fix, and a reader comparing the sha in the report against the sha in the pass caught it.
44
+
45
+ `review-pr` therefore ends every body it posts with a marker naming what it read:
46
+
47
+ ```markdown
48
+ <!-- review-pr: commit=<sha> read-at=<iso8601> -->
49
+ ```
50
+
51
+ Both values are taken at the top of the pass rather than at the post, so the compose window falls outside what the marker claims. An HTML comment renders as nothing on GitHub, so a reader of the thread never meets it.
52
+
53
+ The last non-empty line is where it has to sit, and the reader looks nowhere else. Searching a body for the last match instead reads a marker the body was displaying rather than claiming, since a fenced block showing the format on its own line trims to exactly the pattern, and a pass carrying no marker of its own would then hand the next reader a commit taken from an illustration. That is the same defect through a second door, so position rather than shape is what separates a claim from a quotation.
54
+
55
+ `canon pr review-state` is the only parser of that format. Two parsers was the alternative, one in the shipped skill body and one in the orchestrator poll's jq, and those two ship on different cadences, so a format change to either would leave the other reading a reviewed commit as unreviewed.
56
+
57
+ `source` on the record says which reading answered, and a caller checks it before trusting the rest:
58
+
59
+ | Source | What it means |
60
+ | ---------- | ------------------------------------------------------------------------------------------- |
61
+ | `marker` | The pass wrote its own read-time record, which is the authority |
62
+ | `fallback` | A pass posted before this shipped, read off the submission stamps and carrying their defect |
63
+ | `none` | The thread carries no pass, so the next one is a first pass |
64
+
65
+ The record carries `submittedAt` beside `readAt` rather than in place of it, because each is right about a different question. What a pass had read is a fact about the read, and how long a posted comment has waited on a human is a fact about the submission. The orchestrator poll reads the first for its coverage test and the second for its age test.
66
+
67
+ A `PUT` rewrite of a standing close-out inherits the fix without a mechanism of its own. That request replaces the body and the marker rides in it, so a rewritten comment names the commit the rewriting pass read. `submittedAt` stays pinned to the original submission, which is the reading the age test wants anyway.
68
+
35
69
  ## An empty run list is not a pass
36
70
 
37
71
  Keying the query on a sha is necessary and not sufficient. The endpoint answered with `total_count` 2 and an empty row list during a measured window on 2026-09-02, so a reader that finds no run for the tip and reports `passing` reproduces the false green behind a better query.
@@ -50,15 +84,16 @@ Branch on the record rather than on the exit. Both verbs exit 0 whenever they re
50
84
 
51
85
  Each refusal names a different repair:
52
86
 
53
- | Reason | What it means |
54
- | ------------------ | ------------------------------------------------------------------------------------------ |
55
- | `gh-missing` | `gh` is not on the path, so no pull request resolved |
56
- | `gh-failed` | `gh` could not answer for this branch, so name a number instead |
57
- | `no-branch` | The pull request carries no head branch name, so no ref could be read |
58
- | `unresolvable-ref` | The remote read failed, which is not the same answer as an absent branch |
59
- | `no-remote-branch` | The remote carries no branch by that name, so there is no tip to compare against |
60
- | `no-object-head` | The pull request object reported no head, so `head` has nothing to compare the tip against |
61
- | `runs-unreadable` | The check runs for the tip could not be read, which is unread rather than none |
87
+ | Reason | What it means |
88
+ | -------------------- | ------------------------------------------------------------------------------------------ |
89
+ | `gh-missing` | `gh` is not on the path, so no pull request resolved |
90
+ | `gh-failed` | `gh` could not answer for this branch, so name a number instead |
91
+ | `no-branch` | The pull request carries no head branch name, so no ref could be read |
92
+ | `unresolvable-ref` | The remote read failed, which is not the same answer as an absent branch |
93
+ | `no-remote-branch` | The remote carries no branch by that name, so there is no tip to compare against |
94
+ | `no-object-head` | The pull request object reported no head, so `head` has nothing to compare the tip against |
95
+ | `runs-unreadable` | The check runs for the tip could not be read, which is unread rather than none |
96
+ | `reviews-unreadable` | The reviews on the pull request could not be parsed, which is unread rather than none |
62
97
 
63
98
  A failed remote read and a branch the remote does not carry stay apart rather than collapsing into one reason. Reading the first as the second would report a network refusal as a deleted branch, and the repairs have nothing in common.
64
99
 
@@ -68,6 +103,6 @@ One remote round trip per call, sampled here at 0.41s, 0.55s, and 0.67s on 2026-
68
103
 
69
104
  One head-sensitive read is left on the object deliberately. `canon targets pulls` reads `statusCheckRollup` for every open pull request across every target, where resolving a tip per row would cost one remote read per pull request across a dozen projects, against a surface that reports a listing rather than gating a push.
70
105
 
71
- The orchestrator poll spends that same read per open pull request and repeats it on a three-minute timer, which makes it the heaviest caller here rather than an exception to the paragraph above. Six open pull requests is around 120 remote reads an hour. What separates the two cases is what each reading decides rather than what it costs. The poll's head fires the review trigger, so a stale one sends a pass at a commit nobody read or withholds one that is owed. The listing decides nothing, so the same spend buys a fresher column in a report and no correctness at all.
106
+ The orchestrator poll spends that same read per open pull request and repeats it on a three-minute timer, which makes it the heaviest caller here rather than an exception to the paragraph above. Six open pull requests is around 120 remote reads an hour. It now spends a third read per pull request per tick besides, since `canon pr review-state` makes its own `gh pr view` call beside the payload the poll already fetched. That one is an API round trip rather than a remote ref read, and it buys a coverage test that answers about what a session read. What separates the two cases is what each reading decides rather than what it costs. The poll's head fires the review trigger, so a stale one sends a pass at a commit nobody read or withholds one that is owed. The listing decides nothing, so the same spend buys a fresher column in a report and no correctness at all.
72
107
 
73
108
  `canon pr key-changes` also stays on the object, for a different reason. It reads the body, the file list, and `headRefOid` in one call on purpose, so the three describe one commit. That is a consistency requirement rather than a freshness one, and keying its head elsewhere would break it.
@@ -72,6 +72,36 @@ Skills branch on the reason rather than on the exit code:
72
72
  canon tasks archive --pull-request 673 --json | jq -r 'if .ok then .task else .reason end'
73
73
  ```
74
74
 
75
+ ## Decline
76
+
77
+ `canon tasks decline <task>` moves a task decided against from `.canon/tasks/` into `.canon/tasks/declined/`, clears whichever of `priority.md` or `backlog.md` holds its row, and regenerates the board index. Unlike `canon tasks archive`, it carries no outcome-state gate: a task can be decided against at any outcome state.
78
+
79
+ ```bash
80
+ canon tasks decline v28.1-trigger-escalation --reason "superseded by v30.2" # canon-allow-reference: illustrates the stem-selection form, not a citation of a real task
81
+ canon tasks decline v28.1-trigger-escalation --reason "no longer needed" --by Alex --json
82
+ ```
83
+
84
+ | Option | Behavior |
85
+ | ----------------- | ------------------------------------------------- |
86
+ | `--reason <text>` | Why the task was decided against, required |
87
+ | `--by <name>` | Who decided, defaulting to `git config user.name` |
88
+ | `--json` | Emit a machine-readable record on stdout |
89
+ | `--root <path>` | Board root, defaulting to the main worktree |
90
+
91
+ Exit codes: `0` declined, `1` refused. The `reason` field carries which gate fired: `no-board`, `no-match`, `ambiguous`, or `bad-input`. `bad-input` covers a missing `--reason` and a `--by` that resolves to nobody, git config included.
92
+
93
+ `DECLINE_REFUSALS` is kept apart from archive's own refusal set on purpose. Archive and decline answer different questions, shipped versus decided-against, and a shared gate would let one archive a task that cannot yet ship or decline one that already has.
94
+
95
+ The decision is written onto the task as a `Declined: <reason>, <who> on <YYYY-MM-DD>` line, anchored the same way `Pull request:` is, after the last origin line the task carries.
96
+
97
+ The task carries its plan with it the same way archive does, when the declining task is the last live citation. A declined task's plan lands in `.canon/plans/archive/`, indistinguishable from a shipped one by folder alone. The task file under `.canon/tasks/declined/` is what records which it was.
98
+
99
+ Skills branch on the reason rather than on the exit code, the same rule `canon tasks archive` states:
100
+
101
+ ```bash
102
+ canon tasks decline v28.1-trigger-escalation --reason "superseded by v30.2" --json | jq -r 'if .ok then .task else .reason end' # canon-allow-reference: illustrates the stem-selection form, not a citation of a real task
103
+ ```
104
+
75
105
  ## Plan citations
76
106
 
77
107
  `canon tasks plan-citations <stem>` answers where a task's plan sits and which other live tasks hold it. It reports and never writes.
@@ -244,7 +274,7 @@ Seven checks run. Plan and Collisions reach one half each of the `## Run now` te
244
274
  | Grouping | A task carrying a row in more than one readiness group, or on both surfaces |
245
275
  | Ordering | A `## Needs a plan` row whose stated position disagrees with where it actually sits, or which states no position at all |
246
276
  | Collisions | Two `## Run now` rows whose Touches columns name a path in common |
247
- | Blockers | A parked row whose blocker has stopped holding, or whose cited task resolves nowhere |
277
+ | Blockers | A parked row whose blocker has stopped holding, whose cited task resolves nowhere, or whose cited task was declined |
248
278
 
249
279
  Shape runs before any other check reads a row, since a row failing it carries no dependable fields for the rest to check. A blank or prose line closes the table above it, so the walk treats the next pipe line as a fresh header candidate rather than as a continuation. That candidate counts as a header only when the line behind it is a separator carrying the same cell count, and one that fails is `row-untabled`, stranded behind a table that already closed. Cell count still has to match the header on every row that clears that test, and a row whose count disagrees is `row-misshapen`, the shape a dropped pipe or a merged column produces.
250
280
 
@@ -264,7 +294,20 @@ The collision check is the one a person cannot run by eye. Paths come from the b
264
294
 
265
295
  Where a directory holds the other row's file, the finding names the row that claimed it, reading `both touch src/tasks, which v2.0-second claims as a folder.` The shared strings alone leave an over-broad cell and a genuine overlap identical, which is how a correct report was once read as the verb comparing folders rather than files. <!-- canon-allow-reference: illustrates the finding's own sentence shape, not a citation of a real task -->
266
296
 
267
- The blocker check re-takes a measurement the board records once and never repeats. Two of the five blocker kinds put a fact on disk: a dependency is settled by the cited task being archived or by its work reaching the trunk, and a collision is settled by nothing under `## Run now` still holding the file the cell cites.
297
+ The blocker check re-takes a measurement the board records once and never repeats. Two of the five blocker kinds put a fact on disk: a dependency is settled by the cited task being archived or by its work reaching the trunk, and a collision is settled by nothing under `## Run now` still holding the file the cell cites. A cited task resolving under `.canon/tasks/declined/` instead reports separately as `blocker-declined`, since a decided-against task is neither the shipped work `blocker-settled` reports nor the dangling pointer `blocker-unresolved` reports.
298
+
299
+ ```json
300
+ {
301
+ "findings": [
302
+ {
303
+ "kind": "blocker-declined",
304
+ "group": "Up next",
305
+ "subject": "v50.6-a-standard-no-skill-reads", // canon-allow-reference: shows the subject field's real vXX.Y-slug shape, not a citation of a real task
306
+ "message": "waits on v9.0-superseded, which was declined." // canon-allow-reference: illustrates the finding's own sentence shape, not a citation of a real task
307
+ }
308
+ ]
309
+ }
310
+ ```
268
311
 
269
312
  A closed outcome is not the fact the dependency half needs. The ship chain marks outcomes as its first step and opens the pull request several steps later, so a check reading the checkbox reports the row settled while the branch is still in review. A live task therefore settles the row only once it closed every outcome and carries a `Pull request:` line the trunk holds. One that names no pull request, and one whose number no trunk ref could answer for, land in the untested array below rather than being settled or left silent.
270
313
 
@@ -272,7 +315,7 @@ The trunk is read as the clone already holds it, `origin/main` first and local `
272
315
 
273
316
  Both halves gate on a citation inside the `Waiting on` cell, never on the columns beside it. The board format gives a collision cell the file held by the running task, so a row whose cell names no file was parked by something else, and testing its Touches column instead reports a cleared collision on a row no collision ever parked while counting that row as re-tested. A cited task is a bare sibling link, the way the Task column spells one, so a pointer into another folder names a plan rather than a task and settles nothing. A cited task carrying no outcome box settles nothing either, since a file the check could not parse is not evidence of a finished one.
274
317
 
275
- A citation resolving in neither the board nor the archive is `blocker-unresolved` rather than a settled row. Reading an absent file as archived states a specific fact about a file nobody ever wrote, which is what a renamed task or a typo produces, and only a task that genuinely closed releases the row waiting on it.
318
+ A citation resolving in none of the board, the archive, or the declined folder is `blocker-unresolved` rather than a settled row. Reading an absent file as archived or declined states a specific fact about a file nobody ever wrote, which is what a renamed task or a typo produces, and only a task that genuinely closed or was genuinely declined releases the row waiting on it.
276
319
 
277
320
  The other three kinds rest on a person's judgment, so a row neither half reached lands in a second array rather than in the findings:
278
321
 
@@ -80,7 +80,7 @@ When features are independent, run them in parallel instead of sequentially. Use
80
80
  - Invoke `canon:plan-feature` in each session. Plans land at the main worktree root as `.canon/plans/feature-<slug>.md`, one per feature, no collisions. Small features stay in chat and skip the file.
81
81
  - Implement, verify, and review each feature independently. `review-branch` writes a per-branch report at the main worktree root (`review/branch/review-<slug>.md`), and `ui-test` writes a per-branch checklist handoff there too (`tmp/ui-checklist/<slug>.md`) that `git-pr` posts to the pull request and removes, so parallel sessions do not overwrite each other. The slug is the branch name with any leading type segment dropped, so `feat/jwt-expiration` and the plan at `feature-jwt-expiration.md` meet on one name
82
82
  - Ship each worktree separately with `canon:git-ship`
83
- - For full autonomy per worktree, invoke `canon:auto-ship` instead of the manual chain. Approve the plan, walk away, come back to a pull request the chain marked as a draft and then read the flag back on. The mark says the work has had no review yet, and it holds no window, since readying a pull request to merge lifts it and is the operator's act.
83
+ - For full autonomy per worktree, invoke `canon:auto-ship` instead of the manual chain. Approve the plan, walk away, come back to a pull request the chain marked as a draft and then read the flag back on. The mark says the work has had no review yet, and it holds no window, since readying a pull request to merge lifts it directly, an act reserved to the operator or to the controlling session that closed the review.
84
84
 
85
85
  To run several worktrees as a coordinated flow rather than ad hoc, assert the orchestrator role in one warm session with `canon:role-orchestrator`. It holds the cross-feature call, plans each feature itself or dispatches a cold planner under `canon:role-planner` to write the plan, refills the ready queue so a free worker never waits, and reviews each worker's PR with `canon:review-pr`, then tells the session holding that branch to run `canon:review-address` whenever the pass posted a finding at any severity, which is the same threshold `canon:review-pr` states and posts its open heading under. The human launches workers and merges. See [operating model](operating-model.md) for the full loop.
86
86
 
@@ -289,23 +289,23 @@ This section is the corpus the coverage claim is measured against: every name `c
289
289
 
290
290
  ### Generate an artifact on demand
291
291
 
292
- | Skill | When to use |
293
- | -------------------------- | ------------------------------------------------------------------------------------------------- |
294
- | `canon:create-rule` | For a project-specific governance rule the toolkit does not ship |
295
- | `canon:create-skill` | For a new `SKILL.md` |
296
- | `canon:create-snippet` | For a reusable prompt |
297
- | `canon:create-standard` | For a new authoring convention |
298
- | `canon:draft-docs` | For a brand-new `docs/*.md` page, drafted against `standards/docs.md` |
299
- | `canon:draft-context` | For a brand-new `.claude/context/<domain>.md` entry, drafted against `standards/context.md` |
300
- | `canon:draft-wireframes` | For a brand-new `.claude/wireframes/<surface>.md` file, drafted against `standards/wireframes.md` |
301
- | `canon:draft-readme` | For a project's `README.md`, drafted against `standards/readme.md` |
302
- | `canon:bash-script` | For an interactive, human-facing shell tool |
303
- | `canon:bash-cli-script` | For a non-interactive automation, CI, or pipeline script |
304
- | `canon:ci-workflow` | For a GitHub Actions workflow file |
305
- | `canon:canon-slides-draft` | For a deck, drafted as `.claude/SLIDES.md` and rendered to PowerPoint |
306
- | `canon:canon-screencast` | For a recording script with beats and defaults already seeded |
307
- | `canon:canon-record` | For compiling and running a screencast draft into a recording and a still |
308
- | `canon:identity` | For a project's logo mark and its social card, drafted through `draft-and-pick`'s pick loop |
292
+ | Skill | When to use |
293
+ | ------------------------- | ------------------------------------------------------------------------------------------------- |
294
+ | `canon:create-rule` | For a project-specific governance rule the toolkit does not ship |
295
+ | `canon:create-skill` | For a new `SKILL.md` |
296
+ | `canon:create-snippet` | For a reusable prompt |
297
+ | `canon:create-standard` | For a new authoring convention |
298
+ | `canon:draft-docs` | For a brand-new `docs/*.md` page, drafted against `standards/docs.md` |
299
+ | `canon:draft-context` | For a brand-new `.claude/context/<domain>.md` entry, drafted against `standards/context.md` |
300
+ | `canon:draft-wireframes` | For a brand-new `.claude/wireframes/<surface>.md` file, drafted against `standards/wireframes.md` |
301
+ | `canon:draft-readme` | For a project's `README.md`, drafted against `standards/readme.md` |
302
+ | `canon:bash-script` | For an interactive, human-facing shell tool |
303
+ | `canon:bash-cli-script` | For a non-interactive automation, CI, or pipeline script |
304
+ | `canon:ci-workflow` | For a GitHub Actions workflow file |
305
+ | `canon:draft-slides` | For a deck, drafted as `.claude/SLIDES.md` and rendered to PowerPoint |
306
+ | `canon:draft-screencast` | For a recording script with beats and defaults already seeded |
307
+ | `canon:record-screencast` | For compiling and running a screencast draft into a recording and a still |
308
+ | `canon:identity` | For a project's logo mark and its social card, drafted through `draft-and-pick`'s pick loop |
309
309
 
310
310
  ### Answer a question at any point
311
311
 
@@ -314,7 +314,7 @@ This section is the corpus the coverage claim is measured against: every name `c
314
314
  | `canon:canon-cli` | Before running an unfamiliar verb, a sync, or an install, to learn which command to run, which reference doc covers it, or what it overwrites, merges, or leaves alone |
315
315
  | `canon:index-lookup` | To find where a topic is documented across the tracked `index.md` catalogs |
316
316
  | `canon:youtube-transcripts` | When a video transcript is wanted in the repo as context |
317
- | `canon:canon-frames-read` | To read a recorded demo back frame by frame and report what each one shows, with no verdict on whether the recording looks right |
317
+ | `canon:read-frames` | To read a recorded demo back frame by frame and report what each one shows, with no verdict on whether the recording looks right |
318
318
  | `canon:teach-workspace` | To learn a subject across sessions, in a workspace that holds the progress |
319
319
  | `canon:write-human` | Before drafting or revising prose, for voice, rhythm, and density |
320
320
  | `canon:restate-plainly` | When an answer or a document has to be read again in plain words |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.69.0",
4
+ "version": "4.70.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -44,7 +44,8 @@
44
44
  "web:tokens": "bun src/cli.ts design css --no-components > web/src/styles/tokens.css.new && (echo '/* Generated by `canon design css --no-components`. Regenerate with `bun run web:tokens`. Do not hand-edit. */'; cat web/src/styles/tokens.css.new) > web/src/styles/tokens.css && rm web/src/styles/tokens.css.new",
45
45
  "web:dev": "cd web && astro dev",
46
46
  "web:favicon": "bun scripts/core/regen-web-favicon.ts",
47
- "web:build": "bun run web:tokens && bun run web:favicon && cd web && astro check && astro build",
47
+ "web:previews": "bun scripts/core/regen-web-previews.ts",
48
+ "web:build": "bun run web:tokens && bun run web:favicon && bun run web:previews && cd web && astro check && astro build",
48
49
  "web:preview": "cd web && astro preview",
49
50
  "web:e2e": "cd web && playwright test"
50
51
  },
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Writes `web/public/previews/design-tokens/index.html` and `design.css` by
3
+ * calling `renderDesignDoc` directly, the in-process pattern
4
+ * `regen-web-favicon.ts` already takes rather than spawning a subprocess.
5
+ * Writes `web/public/previews/teach-workspace/` the same way, calling
6
+ * `generateNav` over an isolated copy of one workspace.
7
+ *
8
+ * Both are embedded live in a landing section as an `<iframe>` rather than a
9
+ * screenshot, since `web/e2e/home.spec.ts` asserts no `<img>` renders inside
10
+ * `<main>`.
11
+ */
12
+ import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
13
+ import { tmpdir } from 'node:os'
14
+ import { join } from 'node:path'
15
+ import { DESIGN_DOCUMENT } from '@/design/regen'
16
+ import { renderDesignDoc } from '@/design/render'
17
+ import { generateNav } from '@/teach/nav'
18
+
19
+ const root = new URL('../..', import.meta.url).pathname
20
+
21
+ const { cssPath } = renderDesignDoc(
22
+ `${root}${DESIGN_DOCUMENT}`,
23
+ `${root}web/public/previews/design-tokens`,
24
+ )
25
+
26
+ const css = readFileSync(cssPath, 'utf8')
27
+ if (!/--(color|space|type|radius)-/.test(css)) {
28
+ console.error(
29
+ 'regen-web-previews: design-tokens render carries no custom properties, refusing an empty preview',
30
+ )
31
+ process.exit(1)
32
+ }
33
+
34
+ console.log('regen-web-previews: wrote web/public/previews/design-tokens/')
35
+
36
+ /**
37
+ * The workspace ships alone, on its own isolated nav tree, rather than
38
+ * copied straight out of `.canon/teach/`. That folder cross-links every
39
+ * sibling workspace by relative path, including the operator's own personal
40
+ * study notes, so a copy carrying its live chrome would ship either dead
41
+ * links or a path into content never meant to be public. Regenerating the
42
+ * chrome over a scratch copy holding this one workspace alone is what keeps
43
+ * the jump menus from naming anything else.
44
+ *
45
+ * `.canon/teach/` is gitignored session scratch, per `standards/teach.md`, so
46
+ * it exists on the machine that authored the lesson and nowhere else,
47
+ * including CI. `web/public/previews/teach-workspace/` is therefore the
48
+ * durable copy: committed once the workspace exists, regenerated here as a
49
+ * local convenience when authoring continues, and left untouched by a build
50
+ * that cannot see the source, the same way a target without Playwright still
51
+ * builds against a committed capture.
52
+ */
53
+ const TEACH_SLUG = '04-governance-rule-loading'
54
+ const teachSource = `${root}.canon/teach/${TEACH_SLUG}`
55
+
56
+ if (!existsSync(teachSource)) {
57
+ console.log(
58
+ `regen-web-previews: no workspace at .canon/teach/${TEACH_SLUG} on this machine, leaving the committed web/public/previews/teach-workspace/ as is`,
59
+ )
60
+ } else {
61
+ const scratchRoot = mkdtempSync(join(tmpdir(), 'canon-teach-preview-'))
62
+ try {
63
+ cpSync(teachSource, join(scratchRoot, '.canon', 'teach', TEACH_SLUG), {
64
+ recursive: true,
65
+ })
66
+
67
+ const nav = await generateNav(scratchRoot)
68
+ if (!nav.ok) {
69
+ console.error(`regen-web-previews: ${nav.message}`)
70
+ process.exit(1)
71
+ }
72
+ if (nav.lessons === 0) {
73
+ console.error(
74
+ 'regen-web-previews: teach-workspace render carries no lesson, refusing an empty preview',
75
+ )
76
+ process.exit(1)
77
+ }
78
+
79
+ const dest = `${root}web/public/previews/teach-workspace`
80
+ rmSync(dest, { recursive: true, force: true })
81
+ cpSync(join(scratchRoot, '.canon', 'teach'), dest, {
82
+ recursive: true,
83
+ // Only the rendered chrome ships. MISSION.md, RESOURCES.md, and
84
+ // GLOSSARY.md carry links relative to `.canon/teach/`, which resolve to
85
+ // nothing once copied under `web/public/`, and nothing links to them
86
+ // from the rendered pages anyway.
87
+ filter: (path) => !path.endsWith('.md'),
88
+ })
89
+ } finally {
90
+ rmSync(scratchRoot, { recursive: true, force: true })
91
+ }
92
+
93
+ console.log('regen-web-previews: wrote web/public/previews/teach-workspace/')
94
+ }
@@ -41,7 +41,7 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
41
41
  {
42
42
  prompt:
43
43
  'The video just finished recording. Read it back and tell me what each part shows.',
44
- expect: 'canon-frames-read',
44
+ expect: 'read-frames',
45
45
  },
46
46
  {
47
47
  prompt:
@@ -87,7 +87,7 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
87
87
  {
88
88
  prompt:
89
89
  'The screencast draft is finished. Turn it into an actual video now.',
90
- expect: 'canon-record',
90
+ expect: 'record-screencast',
91
91
  },
92
92
  {
93
93
  prompt:
@@ -96,7 +96,7 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
96
96
  },
97
97
  {
98
98
  prompt: 'Draft me a script with beats for a screencast recording.',
99
- expect: 'canon-screencast',
99
+ expect: 'draft-screencast',
100
100
  },
101
101
  {
102
102
  prompt:
@@ -105,7 +105,7 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
105
105
  },
106
106
  {
107
107
  prompt: 'Turn this topic into a slide deck I can render.',
108
- expect: 'canon-slides-draft',
108
+ expect: 'draft-slides',
109
109
  },
110
110
  {
111
111
  prompt:
@@ -54,7 +54,7 @@ export function register(program: Command): void {
54
54
  demo
55
55
  .command('compile')
56
56
  .description('Turn a screencast draft into a plan a run can drive')
57
- .argument('<draft>', 'Screencast draft written by canon-screencast')
57
+ .argument('<draft>', 'Screencast draft written by draft-screencast')
58
58
  .helpOption('-h, --help', 'Show this help message')
59
59
  .option('-o, --out <dir>', 'Directory the plan is written to', DEFAULT_OUT)
60
60
  .option('-s, --slug <slug>', 'Plan name, defaulting to the draft filename')