@erclx/canon 5.39.0 → 5.40.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 (131) hide show
  1. package/README.md +5 -5
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/canvas-board/SKILL.md +1 -1
  4. package/claude/skills/draft-slides/SKILL.md +2 -3
  5. package/claude/skills/draft-slides/references/deck.md +183 -0
  6. package/claude/skills/git-pr/references/evidence.md +2 -2
  7. package/claude/skills/git-pr/references/final-command.md +1 -1
  8. package/claude/skills/git-pr/references/labels.md +1 -1
  9. package/claude/skills/git-pr/references/publish.md +1 -1
  10. package/claude/skills/review-ui/SKILL.md +1 -0
  11. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
  12. package/claude/skills/task-board/references/board.md +1 -1
  13. package/claude/skills/task-board/references/tasks.md +1 -1
  14. package/claude/skills/ux-measure/SKILL.md +1 -1
  15. package/docs/agents/audits.md +61 -60
  16. package/docs/agents/context-audit.md +40 -58
  17. package/docs/agents/index.md +5 -51
  18. package/docs/agents/install-and-sync.md +1 -0
  19. package/docs/agents/markdown-audit.md +53 -99
  20. package/docs/agents/records.md +37 -79
  21. package/docs/agents/sessions-transfer.md +26 -70
  22. package/docs/target/index.md +0 -1
  23. package/docs/target/migrations.md +57 -66
  24. package/docs/target/projects.md +36 -78
  25. package/docs/target/scenarios.md +9 -19
  26. package/docs/target/sync.md +27 -68
  27. package/docs/workflow/ai-workflow.md +22 -68
  28. package/docs/workflow/operating-model.md +32 -186
  29. package/docs/workflow/parallel-features.md +29 -66
  30. package/docs/workflow/skill-map-on-request.md +22 -24
  31. package/docs/workflow/skill-map.md +72 -76
  32. package/docs/workflow/visual-design-workflow.md +25 -118
  33. package/docs/workflow/zshrc-aliases.md +18 -37
  34. package/package.json +1 -1
  35. package/src/canvas/client/inspector/fields.css +4 -0
  36. package/src/canvas/index.html +6 -0
  37. package/src/claude/skills-reach.ts +2 -2
  38. package/src/cli/ui.ts +3 -1
  39. package/src/commands/canvas.ts +23 -0
  40. package/src/commands/capture.ts +14 -0
  41. package/src/commands/claude.ts +13 -0
  42. package/src/commands/demo.ts +22 -0
  43. package/src/commands/deps.ts +6 -0
  44. package/src/commands/driver.ts +28 -2
  45. package/src/commands/gov.ts +52 -18
  46. package/src/commands/intake.ts +17 -0
  47. package/src/commands/labels.ts +38 -5
  48. package/src/commands/pr.ts +160 -43
  49. package/src/commands/records.ts +8 -0
  50. package/src/commands/secrets.ts +6 -0
  51. package/src/commands/sessions.ts +8 -0
  52. package/src/commands/tasks.ts +52 -0
  53. package/src/commands/teach.ts +13 -0
  54. package/src/commands/upstream.ts +25 -1
  55. package/src/commands/worktrees.ts +5 -0
  56. package/src/context/audit.ts +22 -4
  57. package/src/context/folders.ts +1 -1
  58. package/src/context/shape-gated.ts +4 -2
  59. package/src/counts/scan.ts +1 -1
  60. package/src/design/components.ts +1 -1
  61. package/src/design/document.ts +1 -1
  62. package/src/design/tokens.ts +1 -1
  63. package/src/gate/boundaries.ts +4 -1
  64. package/src/gate/measures.ts +1 -1
  65. package/src/gate/stages.ts +7 -7
  66. package/src/git/env.ts +6 -0
  67. package/src/markdown/scope.ts +5 -17
  68. package/src/pr/lift.ts +153 -19
  69. package/src/pr/painted.ts +87 -0
  70. package/src/roots/record.ts +18 -1
  71. package/src/sandbox/census.ts +1 -1
  72. package/src/sandbox/headless/run.ts +3 -2
  73. package/src/sandbox/headless/watch.ts +3 -4
  74. package/src/slides/convert/master.ts +3 -0
  75. package/src/sync/format.ts +46 -0
  76. package/src/sync/stamp.ts +6 -0
  77. package/tooling/astro/reference.md +2 -2
  78. package/tooling/base/reference.md +1 -1
  79. package/tooling/go/reference.md +1 -1
  80. package/tooling/nextjs/reference.md +1 -1
  81. package/tooling/php/reference.md +1 -1
  82. package/tooling/python/reference.md +1 -1
  83. package/tooling/vite-react/reference.md +1 -1
  84. package/tooling/web/reference.md +1 -1
  85. package/docs/agents/canvas.md +0 -86
  86. package/docs/agents/capture.md +0 -65
  87. package/docs/agents/census.md +0 -23
  88. package/docs/agents/comments.md +0 -34
  89. package/docs/agents/context-audit-checks.md +0 -108
  90. package/docs/agents/context-classify.md +0 -101
  91. package/docs/agents/counts.md +0 -74
  92. package/docs/agents/demo.md +0 -140
  93. package/docs/agents/driver.md +0 -103
  94. package/docs/agents/gate.md +0 -90
  95. package/docs/agents/hooks.md +0 -78
  96. package/docs/agents/intake.md +0 -81
  97. package/docs/agents/key-changes.md +0 -116
  98. package/docs/agents/label-coverage.md +0 -86
  99. package/docs/agents/markdown-audit-density.md +0 -39
  100. package/docs/agents/markdown-audit-weight.md +0 -83
  101. package/docs/agents/pr-evidence-layout.md +0 -70
  102. package/docs/agents/pr-evidence.md +0 -273
  103. package/docs/agents/pr-frames.md +0 -131
  104. package/docs/agents/pr-lift.md +0 -132
  105. package/docs/agents/pr-local.md +0 -111
  106. package/docs/agents/pr-preview.md +0 -155
  107. package/docs/agents/pr-reads.md +0 -111
  108. package/docs/agents/pr-tick.md +0 -72
  109. package/docs/agents/records-push.md +0 -61
  110. package/docs/agents/records-stale.md +0 -40
  111. package/docs/agents/records-validate.md +0 -57
  112. package/docs/agents/restated.md +0 -83
  113. package/docs/agents/routing.md +0 -61
  114. package/docs/agents/rule-citations.md +0 -90
  115. package/docs/agents/sandbox.md +0 -126
  116. package/docs/agents/sessions.md +0 -128
  117. package/docs/agents/skills-audit.md +0 -120
  118. package/docs/agents/skills-reach.md +0 -64
  119. package/docs/agents/slides.md +0 -176
  120. package/docs/agents/state-scoped-risk.md +0 -105
  121. package/docs/agents/superseded.md +0 -85
  122. package/docs/agents/tasks-archive.md +0 -83
  123. package/docs/agents/tasks-plans.md +0 -152
  124. package/docs/agents/tasks-validate.md +0 -133
  125. package/docs/agents/tasks.md +0 -123
  126. package/docs/agents/teach-authoring.md +0 -118
  127. package/docs/agents/teach.md +0 -157
  128. package/docs/agents/test-order.md +0 -58
  129. package/docs/agents/upstream.md +0 -94
  130. package/docs/agents/worktrees.md +0 -118
  131. package/docs/target/standards-moved.md +0 -34
@@ -1,74 +0,0 @@
1
- ---
2
- title: Self-stated counts
3
- description: Reading a sentence that asserts a closed catalog's size, how a match is decided, the plausibility filter that keeps a generic word from matching a subset, and why the sweep reports rather than gates
4
- ---
5
-
6
- # Self-stated counts
7
-
8
- `canon gov counts` reports a sentence stating how many members a closed catalog holds where the stated figure disagrees with what the tree actually counts. A document that reports its own catalog size carries a number nothing else compares against the tree, so it goes stale on the edit that moves the catalog and reads as current until a reviewer happens to recompute it.
9
-
10
- ```bash
11
- canon gov counts
12
- canon gov counts --json
13
- canon gov counts --root ../my-app
14
- ```
15
-
16
- | Option | Behavior |
17
- | --------------- | ---------------------------------------------------------- |
18
- | `--root <path>` | Tree to read, defaulting to the current directory |
19
- | `--json` | Add a machine-readable record on stdout, keeping the frame |
20
-
21
- Under `--json` the record holds stdout alone and the frame still renders on stderr, refusals included, which is the split `output-shape.md` fixes for every mode.
22
-
23
- ## The catalogs it reads
24
-
25
- A closed set of four, each with a filesystem reader behind it rather than every list command this CLI ships:
26
-
27
- - `skills`: shipped plugin skills, a folder per `claude/skills/*/SKILL.md`
28
- - `rules`: authored governance rules, a file per `governance/rules/**/*.md`
29
- - `commands`: top-level `canon` commands, read off `src/cli.ts`'s registration imports
30
- - `audits`: the registered set in `src/audits/catalog.ts`, this entry included
31
-
32
- The set is closed rather than derived, so widening it is a deliberate change to this file rather than a side effect of a new list command shipping elsewhere. `commands` is the one catalog with no meaning outside this repository: a project installing this CLI carries no `src/cli.ts` of its own, and that catalog reads as not applicable there rather than as zero, since zero would report a finding against any such project stating its command count as anything but zero, forever.
33
-
34
- ## How a match is decided
35
-
36
- A sentence has to carry an assertion verb (`loads`, `ships`, `carries`, `holds`, `counts`, `totals`, `documents`, `declares`, `installs`, `lists`, `contains`, `comprises`, `authors`), the quantifier `all`, or an article (`the`, `a`, `an`) immediately ahead of the number, which is immediately ahead of the catalog noun, with one optional qualifying word between the number and the noun (`sixty-one shipped skills`). The number reads either as digits or as a spelled-out cardinal through ninety-nine, since this corpus states a catalog size in words as often as in digits.
37
-
38
- Nothing stands between the trigger and the number, which is the rule an author writing a new count has to know. It separates `the toolkit authors 49 rules` and `took all 49 rules`, both read, from `a domain of 55 skills` and `denominator of sixty-one shipped skills`, neither of them read. A figure meant as a past state takes a date in the same sentence instead, which reads the sentence past whatever its shape.
39
-
40
- ## How the trigger set grew
41
-
42
- The verb gate is not the first design tried. A bare number next to a catalog noun anywhere in the tracked corpus returned 290 findings against a repository whose actionable instance count was one. Reading that run showed why: `18 rules citing a standard`, `21 skill bodies`, and `eight internal skills` all pair a number with a catalog noun while naming a subset, an example, or a different catalog than the one matched, and that shape dominates ordinary prose. Every instance this sweep was written against reads the number as the direct object of a verb asserting the catalog's own total, and gating on that verb is what took the false-positive count from 290 to five on the same tree.
43
-
44
- The article half was added after a first review of the shipped design found a live miss: the development domain's gating-stages entry stated a stale audit total while the tree held 20, and the verb gate never reached it, since that sentence puts its verb after the noun rather than ahead of the number. Widening the trigger set to admit an article ahead of the number closed that gap.
45
-
46
- The quantifier `all` and the verb `authors` joined on the same evidence, two more live misses failing only the trigger test. Allowing words to stand between the trigger and the number was the other candidate, measured and declined: at widths of one, two, and three it reached 77, 104, and 132 sentences against a baseline of 65, missed `took all 49 rules` at every width, and reached `the toolkit authors 49 rules` only at a width admitting the indirect-noun shape below. The two words reach both at 75 sentences and no false positive.
47
-
48
- ## The plausibility filter
49
-
50
- The verb gate alone still left four false positives standing: `carries two rules about a standard's own lifecycle`, `holds one rule or one fact` (twice, once per mirrored copy), and `documents two similar commands`. Each pairs an assertion verb with a catalog noun used for something the `rules` or `commands` catalog does not mean, and what tells those apart from the one live finding is magnitude: a catalog this sweep tracks drifts by a few members between the day a sentence was written and the day it is read, so a genuine staleness claim sits near the true count. `2` beside a true count of `59` is not a catalog that shrank, it is a different `rules` entirely.
51
-
52
- A stated figure is read only when it sits within a factor of two of the true count, in either direction. That bound is tuned against the run this sweep was written against rather than reasoned to from first principles, the way `restated.ts` tunes its own document-frequency ceiling, and it is a property of the scale every catalog here sits at (the tens) rather than a universal rule.
53
-
54
- The article gate is looser than the verb list and carries a real cost. Re-running against the tree once it widened caught the live miss above and also misread a passage in the standards destinations entry, since retired, naming a real subset, a standard outside the retired bundled fan-out, as a claim about the whole standards catalog. The qualifying adjective there fills the same optional-word slot `sixty-one shipped skills` needs to match at all, and no syntactic rule tells a qualifier that narrows a catalog from one that only restates it. The finding could not be closed by correcting a figure, since the sentence was true when the corpus held it, only unmarked as a past state, which is the class the date exclusion already reads past. Naming the moment, the same way a context entry dates its own figures, closed it with no change to the matcher.
55
-
56
- ## What it does not measure
57
-
58
- A delta phrased as a transition (`from fourteen to fifteen`), a fraction (`thirteen of sixteen`), and a total reached through an indirect noun (`denominator of sixty-one shipped skills`) are all catalog-size claims this corpus carries, and none matches the trigger shape this reads. Each stays a known gap. The false-positive rate that gated closing them has a measurement behind it now, and what it showed is that the widening these three need is the one that costs a false positive rather than the one that does not.
59
-
60
- A second figure in a sentence whose first figure already matched is a fourth gap and a structural one, since one match is taken per catalog per sentence. `authors 49 rules under governance/rules/ and consumes 31 into .claude/rules/` is read for its 49 alone, which is correct here because the two figures name different populations, and a sentence stating one catalog twice would go unread the same way.
61
-
62
- A calendar date (`2026-08-21`) or a backticked commit reference in the same sentence reads the whole sentence past, since that is how this corpus already marks a figure as a historical record rather than a live claim. The context entries carry a figure this way deliberately, and every one of them stays correct forever.
63
-
64
- The wider check over prose restatements is deliberately out of reach here. `canon gov restated` already records a parser over prose as rejected, and this sweep reaches a number with a command behind it rather than two sentences a reader has to judge as agreeing.
65
-
66
- ## Exit codes
67
-
68
- Exit codes are `0` when no stated figure disagrees with the tree, `1` for a refusal, and `2` for at least one disagreement.
69
-
70
- Nothing wires this into `bun run check` or into a hook. The false-positive rate is read off the first real run rather than assumed ahead of it, and gating a measure with an unmeasured rate is what teaches a contributor to route around the stage. `canon gov restated` and `canon gov test-order` are the siblings this reasoning already governs.
71
-
72
- Both refusals are breaks rather than absences. A tree with no git history or no markdown file at all is a broken checkout, matching what `canon markdown audit` already reads `no-git` and `no-markdown` as, since this sweep shares that corpus.
73
-
74
- An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the record's `findings` rather than the exit when a skill consumes this.
@@ -1,140 +0,0 @@
1
- ---
2
- title: Demo
3
- description: Compiling a screencast draft into a runnable plan, driving a served application to a recording and a still, reading numbered frames back out of a recording, the pointer the recording paints, and what each refusal reports
4
- ---
5
-
6
- # Demo
7
-
8
- `canon demo` drives a project's running application and writes a recording of what it did, plus a still frame of the same run. It exists so a project card and a short demo stop depending on someone sitting down to record one by hand.
9
-
10
- Two verbs, and they are separate because the artifact between them is edited.
11
-
12
- ```bash
13
- canon demo compile demos/inline-edit/beats.md --slug inline-edit
14
- canon demo run demos/inline-edit/plan.json
15
- canon demo run demos/inline-edit/plan.json --cursor ~/cursors/theme --out assets
16
- canon demo frames demos/inline-edit/take/inline-edit.webm --fps 2
17
- ```
18
-
19
- ## One folder per demo
20
-
21
- Each demo owns a folder, `demos/<slug>/`, and `compile` writes the plan into it.
22
-
23
- | Path | Committed | Holds |
24
- | ------------ | --------- | ----------------------------------------------------- |
25
- | `beats.md` | yes | The draft `draft-screencast` wrote |
26
- | `plan.json` | yes | The compiled plan, with its hand-tuned timing |
27
- | `index.html` | yes | The HyperFrames composition, when the demo is wrapped |
28
- | `demo.gif` | yes | The one deliverable a README embeds |
29
- | `take/` | no | The raw recording, its still, and its timeline |
30
- | `renders/` | no | The composed mp4, written by the render |
31
-
32
- A project carries `demos/*/take/` and `demos/*/renders/` in its `.gitignore`, since both are one command away and a webm is large. A plan compiled before this layout, at `demos/<slug>.json`, still runs by path, because only the default moved.
33
-
34
- ## The draft and the plan are different files
35
-
36
- `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.
37
-
38
- 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.
39
-
40
- 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`.
41
-
42
- A plan can carry an optional `colorScheme` of `light` or `dark`, which the recording context starts in so a page following the system preference paints that scheme with no click beat of its own. An absent field keeps the engine's default, which is light, and `compile` never seeds it. Any other value makes the plan unreadable. A page keyed on a stored theme rather than the preference ignores the field.
43
-
44
- A recording that has to match both themes takes one plan per scheme, committed side by side, rather than one plan run twice. The pair shares its beats and differs in `colorScheme`, `slug`, and the `output` paths. Neither plan clicks a theme toggle, since a stored theme overrides the preference. Nothing checks that the two plans keep the same beats, so an edit to one is made to both.
45
-
46
- `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.
47
-
48
- | Option | Behavior |
49
- | --------------- | ----------------------------------------------------------------- |
50
- | `--out <dir>` | Directory the plan and its output paths point at, default `demos` |
51
- | `--slug <slug>` | Demo folder name, defaulting to the draft filename |
52
- | `--force` | Overwrite an existing plan, losing any field set by hand |
53
- | `--json` | Add a record on stdout carrying the beats and what is unfilled |
54
-
55
- ## What a run does
56
-
57
- `canon demo run` reads the plan, refuses if a field is still empty, and drives the application the plan's URL names. It records the whole run to `webm` and writes the still from the beat the draft calls the hero, falling back to the last beat, since a demo's final state is the payoff and a cold open is usually an empty screen.
58
-
59
- A step's caption from the draft renders as an overlay while its hold plays, so the narration a person wrote is what shows on screen rather than the name of the action the engine performed.
60
-
61
- Beside the webm, the run writes `<video-basename>.timeline.json`, so a composition can aim a zoom at the moment and the place of an action rather than adding up holds by hand. It holds an `entries` array with one entry per step, each carrying the step's `index`, its `kind`, and `startMs` and `endMs` counted from the recording's first frame. A step ends once its hold has played. A click, fill, or hover entry also carries `box`, the target's `x`, `y`, `width`, and `height` in viewport pixels, which are the video frame's own coordinates. A navigate, scroll, wait, or hold entry carries no `box`, and the file is still written. A run under `--no-video` writes no timeline, since there is no video for it to sit beside. The `--json` record names the file under `timeline`, and `null` when none was written.
62
-
63
- When `ffmpeg` is on PATH, the run also writes an mp4 beside the webm, since webm plays in a `<video>` tag but nothing else accepts it. A target without `ffmpeg` still gets the webm and a line naming what to install, and the run does not fail over the missing converter.
64
-
65
- `--gif` adds a third file from the same converter, and it is opt-in where the mp4 is not. A gif runs several times the size of the webm it derives from, and the destination that needs one is a README on a host that strips `<video>`, which GitHub does. The filter generates a palette from the source and then applies it, rather than quantizing per frame, since a per-frame palette is what makes a recording of flat interface colors band and shimmer. It applies that palette without dithering, because the command records applications rather than photographs and the dither writes noise the encoder then stores. One recording measured 3,193,715 bytes dithered against 2,701,941 without, with the same text region cropped from both and read as identical. A missing converter is reported the same way it is for the mp4, so an absent `ffmpeg` costs the gif and not the run.
66
-
67
- | Option | Behavior |
68
- | ---------------- | ------------------------------------------------------------------- |
69
- | `--out <dir>` | Directory to write into, overriding what the plan names |
70
- | `--cursor <dir>` | Cursor theme folder to draw the pointer from |
71
- | `--no-video` | Write only the still |
72
- | `--no-still` | Write only the recording |
73
- | `--gif` | Also write a gif, for a host that strips video |
74
- | `--json` | Add a record on stdout carrying every path written and the duration |
75
-
76
- A step waits on its `waitFor` selector becoming visible and then holds for its own `holdMs`, which is what puts a finished state on screen long enough to read. `navigate` uses the plan's URL unless the step names its own.
77
-
78
- ## What frames does
79
-
80
- `canon demo frames` pulls numbered PNG stills back out of a recorded video through the same `ffmpeg` binary the mp4 and gif conversion already shells to. Nothing reads a recording back today, so a broken one ships until a person opens it, and this verb is what a skill calls to read one instead. It writes one frame a second by default, sampling at a rate matched to how long a tuned recording runs rather than at a rate tuned for any one clip. Frames land beside the video by default, named `<video-basename>-frame-<NNN>.png`, so they fall under the same `take/` ignore entry the still already uses.
81
-
82
- | Option | Behavior |
83
- | ----------------- | -------------------------------------------------------- |
84
- | `-o, --out <dir>` | Directory to write frames into, default beside the video |
85
- | `--fps <n>` | Frames extracted per second of video, default `1` |
86
- | `--json` | Add a record on stdout carrying every frame path written |
87
-
88
- `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.
89
-
90
- ## The pointer is painted inside the page
91
-
92
- The browser engine offers an annotation of its own that draws a dot on the interacted element and a title naming the API call it made, and this recorder does not turn it on. It paints no cursor, so a run relying on it looks like the pointer teleports between targets, and the two overlays below supersede it: a real cursor where the dot is a marker, and the beat's narration where the title reads `Mouse move`. Running both put four overlays on the frame, and the two the engine drew were the two a viewer reads as noise.
93
-
94
- The recorder injects a pointer element before navigation and moves it through the engine's pointer with interpolated steps rather than through the element-clicking helper, which resolves a target and jumps to it. The step count is the whole difference between a cursor that travels and one that appears, and how many steps a move takes is derived from the machine's own round-trip cost rather than fixed, so the same plan glides at roughly the same pace on a loaded machine as an idle one. It also reads the element under it on every move and switches between an arrow, a hand, and a text beam, so it reflects the page the way a real cursor does.
95
-
96
- `--cursor` points at a folder of Windows cursor resources and the browser decodes them directly, with no conversion step and no image tooling. Each resource carries a hotspot per size, and the largest entry's hotspot scaled to the drawn size is what puts the artwork's tip where the click lands. A theme contributes per state, so a folder holding an arrow and no hand still supplies its arrow and the bundled artwork covers the rest.
97
-
98
- Three of the nineteen states a theme carries are read. A drag, a resize, or a wait shows the arrow where a real session would show something else, and the two animated states have no still frame to draw.
99
-
100
- ## What the refusals report
101
-
102
- Every refusal exits 1 and names its reason in the `--json` record, so a skill branches on `reason` rather than on the exit code.
103
-
104
- | Reason | What happened |
105
- | --------------------- | ----------------------------------------------------------------------------------- |
106
- | `draft-missing` | No file at the path given to `compile` |
107
- | `draft-unreadable` | The draft carries no beat sheet, or a sheet with no beats |
108
- | `plan-exists` | A plan is already there and `--force` was not passed |
109
- | `plan-unresolved` | A URL or a target is still empty, named field by field |
110
- | `no-output-requested` | `--no-video` and `--no-still` together, which would drive the app and write nothing |
111
- | `plan-unreadable` | The plan is not JSON, or a step names a kind nothing drives |
112
- | `browser-missing` | The browser binary is not installed, with `install` carrying the command |
113
- | `engine-missing` | The browser package itself did not resolve |
114
- | `video-missing` | No file at the path given to `frames` |
115
- | `converter-missing` | `ffmpeg` is not on PATH, so `frames` extracted nothing |
116
- | `extraction-failed` | `ffmpeg` exited non-zero, with `message` carrying its stderr |
117
-
118
- ## The browser reaches every target
119
-
120
- This command was the first browser command to ship, and `canon capture` has since joined it. Capture was held back because its only caller was this repository regenerating its own committed images, which was a fact about that caller rather than about the render.
121
-
122
- The cost is stated rather than hidden: the browser binary installs separately, so a target runs `bunx playwright install chromium` once before a recording works at all. A run that cannot launch reports that command inside the frame and exits 1.
123
-
124
- ## Composing the take into a finished video
125
-
126
- The take is raw. When the draft's `## Wrap` section asks for an intro, an outro, or music, `record-screencast` composes the take into `demos/<slug>/index.html` through [HyperFrames](https://hyperframes.heygen.com) and renders `renders/<slug>.mp4`, the deliverable. The skill loads HyperFrames' own `hyperframes` skill for the composition rules and `canon:video-craft` for the captions, zoom, pointer, and audio judgment a video of software needs, aims each zoom from the take's timeline, and runs `bunx -y hyperframes check` and `render`, then hands the render to `read-frames`. A wrap section reading `none` throughout stops at the take. The webm is only the raw take, and a gif is for a host that strips video.
127
-
128
- Canon neither ships nor installs HyperFrames, and there is no `canon` verb for composing, since the render is one command already. The operator installs it once per machine:
129
-
130
- ```bash
131
- bunx -y hyperframes init
132
- ```
133
-
134
- `hyperframes init` installs its skills machine-wide without asking, so run it knowing that. A machine without it stops the skill with this command and nothing else.
135
-
136
- ## What it does not do
137
-
138
- Nothing in `canon demo` itself trims a take or scores it beyond the caption each beat already carries. Composing is the skill's step and HyperFrames' work.
139
-
140
- It also does not replace a narrated screencast. That has a hero moment, a cut list, and a voice, none of which survives being generated. This answers the case where the alternative is recording nothing.
@@ -1,103 +0,0 @@
1
- ---
2
- title: Driver
3
- description: Walking a page through named interactions, the probe catalog and the false finding each one carries, why viewport heights are never defaulted, and what each refusal reports
4
- ---
5
-
6
- # Driver
7
-
8
- `canon drive <url> <run>` walks a page through a sequence of interactions and measures every state it reaches. A render answers about a page as it loads, so a defect that exists only after a menu opens, an answer is chosen, or the page scrolls is invisible to one. That second axis is what this adds.
9
-
10
- ```bash
11
- canon drive http://localhost:4173 run.json
12
- canon drive http://localhost:4173 run.json --json
13
- ```
14
-
15
- | Option | Behavior |
16
- | -------- | --------------------------------------- |
17
- | `--json` | Add a machine-readable record on stdout |
18
-
19
- ## The run file
20
-
21
- The run is a JSON file rather than a key in a project config, because a route catalog is state a project holds and an interaction sequence is a script written for one question. It carries the viewport, the probes to run by default, and the steps.
22
-
23
- ```json
24
- {
25
- "viewport": { "width": 1440, "heights": [900, 1200, 1500] },
26
- "probes": ["focus", "details"],
27
- "steps": [
28
- { "name": "on load", "kind": "wait", "ms": 0 },
29
- { "name": "open the menu", "kind": "click", "target": "#menu summary" },
30
- {
31
- "name": "reach the diagram",
32
- "kind": "scroll",
33
- "target": "figure",
34
- "probes": ["diagram-geometry", "diagram-strokes"]
35
- }
36
- ]
37
- }
38
- ```
39
-
40
- Every step names what it did, and that name is carried onto each finding it produced, alongside the viewport. A step naming its own `probes` overrides the run-level list for that step alone. Omitting `probes` at the run level runs all four.
41
-
42
- | Step kind | Fields | What it does |
43
- | --------- | ------------------ | ---------------------------------------------- |
44
- | `click` | `target` | Presses the first element the selector matches |
45
- | `scroll` | `target` | Brings the first match into view |
46
- | `fill` | `target`, `text` | Types into the first match |
47
- | `tab` | `count` (optional) | Advances keyboard focus, once by default |
48
- | `wait` | `ms` | Holds, for a state the page reaches alone |
49
-
50
- Probes run after a step and never on arrival, so a run reaches the load state by opening with a `wait` step of its own, as the example above does. Nothing probes before the first step runs, which makes that leading step the only way to measure the page as it first painted, and naming it is what puts the load state on its own findings rather than under whatever ran next.
51
-
52
- Write one wherever no capture runs against the page. `canon capture` renders a single state from a source on disk or a URL, so where it runs it already answers about arrival and a leading `wait` duplicates it. Where it does not, this command is the only thing measuring the page at all, and a run without that step reports every driven state and nothing about the one a visitor sees first.
53
-
54
- Each height is driven in a context of its own from a fresh navigation, rather than by resizing the page the previous height already drove, since a sweep asks the same question of each height rather than a later question of an already-driven page.
55
-
56
- ## The probes
57
-
58
- Four probes, a fixed catalog rather than a subject system a project extends. Each carries a class of false finding that a throwaway version of this command produced before it produced true ones, and a probe a project authored for itself would re-pay those.
59
-
60
- | Probe | What it reports |
61
- | ------------------ | ------------------------------------------------------------------------------------------ |
62
- | `focus` | An element that takes keyboard focus and changes nothing visible |
63
- | `details` | A `details` panel off the viewport, or a row under the tap minimum, both read open |
64
- | `diagram-geometry` | An SVG label outside its frame, covered by a filled shape, or colliding with another label |
65
- | `diagram-strokes` | An SVG label a stroke crosses, sampled along the stroke's own geometry |
66
-
67
- `focus` presses Tab before it reads anything. `:focus-visible` does not match a scripted `.focus()` once the page has taken a pointer interaction, and a driver clicks by definition, so a reader without that press reports every correctly styled element as unstyled. It also ignores an outline width, color, or offset moving underneath an outline that resolves to `none`, which paints nothing.
68
-
69
- `details` discovers every `details` on the page rather than taking a selector, measures each shut and again open, and judges only the open reading. A shut menu still reports a layout box and the box describes the trigger rather than the panel, so the shut numbers travel in the record as context. Discovery is automatic because the one real menu defect in the originating run was missed by a hand-picked probe: nobody thought to name that menu.
70
-
71
- Both diagram probes read after `document.fonts.ready` and never compute from the markup. A generated page is routinely authored against one font and restyled to another, so a label that cleared a line at its authored width can overlap it once rendered. Every rect either probe compares is inset vertically and never horizontally, since a text rect is loose above and below the glyphs and tight to them left and right, and a horizontal inset passes a real overrun.
72
-
73
- `diagram-strokes` is separate from `diagram-geometry` because a bounding box cannot answer for a stroke. A probe comparing filled boxes filters out every `line` and every `fill="none"` panel border before it compares, and a diagonal connector's box covers most of a diagram while the stroke itself touches almost none of it. Both paint orders are findings: a stroke drawn after a label crosses the glyphs out, and one drawn before it shows through them, since SVG text carries no plate of its own.
74
-
75
- ## Viewport heights are never defaulted
76
-
77
- A run with no `viewport.heights` refuses. The heights that separate a passing render from a failing one belong to the layout being driven, and the only evidence behind any default this could ship is one fixture's failure range, too narrow a sample to hand every later caller a number they never chose. A scroll rail that skipped its middle sections passed at 900 and failed at 1200 and 1500, so one height reports clean over a live defect.
78
-
79
- ## What it reports
80
-
81
- It reports findings and never gates, and the exit code says only whether the drive completed. Every probe here carries a class of false finding already paid for once, so a command ending a build on its own reading makes a claim this catalog has not earned. A caller that wants a verdict reads `findings` off the `--json` record and decides for itself.
82
-
83
- | Reason | What it means |
84
- | -------------------- | ------------------------------------------------------ |
85
- | `no-run-file` | Nothing readable at the path given |
86
- | `unreadable-plan` | The run file is not valid JSON |
87
- | `no-steps` | The run names no step |
88
- | `no-probes` | The run names an empty probe list |
89
- | `unknown-probe` | The run names a probe this build does not ship |
90
- | `bad-step` | A step is missing what the driver needs to perform it |
91
- | `no-viewport` | The run declares no viewport |
92
- | `no-width` | The viewport has no width to wrap at |
93
- | `no-heights` | The viewport names no height, which is never defaulted |
94
- | `engine-missing` | The browser engine is not installed in this project |
95
- | `browser-missing` | The engine is installed and its browser binary is not |
96
- | `server-unreachable` | Nothing answered at the URL, so no state was reached |
97
- | `drive-failed` | The drive failed against a reachable page |
98
-
99
- An unreachable page refuses rather than returning an empty report, since nothing measured and nothing found read the same to anything counting findings.
100
-
101
- The browser binary installs separately from the package, once, with `bunx playwright install chromium`. The command ships to targets like `demo`, `inventory`, and `capture`, because its whole purpose is measuring a page inside someone else's project. It reaches nothing under `src/capture/`, so the two surfaces move independently.
102
-
103
- See `canon --help` for where this sits among the browser commands, and `capture.md` for the single-state render it adds an axis to.
@@ -1,90 +0,0 @@
1
- ---
2
- title: Merge gate
3
- description: Running the gate this repository verifies a branch with, what the stage table holds and what stays a script, how the changed set scopes three stages, and why a stage that cannot read its input reports rather than passing
4
- ---
5
-
6
- # Merge gate
7
-
8
- `canon gate run` runs every stage that guards a branch here, in order, stopping at the first stage that finds a fact. `bun run check` and `bun run check:ci` resolve to it, and the `update` package script runs `bun run check` after a dependency update.
9
-
10
- ```bash
11
- canon gate run
12
- canon gate run --all
13
- canon gate run --json
14
- ```
15
-
16
- | Option | Behavior |
17
- | ---------- | ----------------------------------------------------------------------------- |
18
- | `--all` | Run every stage instead of scoping shell, types, and tests to the changed set |
19
- | `--nested` | Suppress the outer frame when a calling script has already opened one |
20
- | `--json` | Add a machine-readable record on stdout |
21
-
22
- ## What the command owns and what it runs
23
-
24
- Three things sit in the command: the stage table in `src/gate/stages.ts`, the changed-file scoping and the run loop in `src/gate/sequencer.ts`, and every threshold comparison in `src/gate/measures.ts`. Each individual check is the script or the verb it already was, under `scripts/core/` or behind another `canon` command, and the move changed none of them. Sequencing, scoping, and comparison are where the recurring defects were, and a check whose behavior changed while its sequencing moved would make any regression impossible to attribute.
25
-
26
- A stage is a list of checks and a check is one of four kinds:
27
-
28
- | Kind | What it is |
29
- | --------- | ------------------------------------------------------------------------------- |
30
- | `command` | Any binary, run from the project root |
31
- | `cli` | This checkout's own `src/cli.ts`, never a globally installed `canon` |
32
- | `drift` | A regenerated pathspec asserted against the index and against the untracked set |
33
- | `measure` | A reading whose verdict is a comparison rather than an exit code |
34
-
35
- A `cli` check runs the source rather than the binary because a globally installed `canon` resolves to the main checkout no matter which worktree is running, so a gate reading through it would measure the wrong tree and pass a branch it never opened.
36
-
37
- Every check is an argument vector rather than a shell line, so no stage carries a quoting hazard and a `drift` pathspec reaches git exactly as the table spells it.
38
-
39
- ## Scoping
40
-
41
- Shell, types, and tests read the changed set. Everything else always runs, because its input is diffuse enough that no path predicts it.
42
-
43
- The changed set unions the branch diff against the merge base with `origin/main`, the working tree, and untracked files, which is what a pull request will contain. The baseline is the remote ref and not local `main`, since on `main` itself the local ref is HEAD and every unpushed commit would drop out. Every fallback widens rather than narrows: no merge base at all runs every stage, and a local baseline equal to HEAD does the same. `--all` turns scoping off outright, which is what `bun run check:ci` passes so CI stays the backstop for a wrong local scoping decision.
44
-
45
- ## A stage that cannot read its input
46
-
47
- A stage reports one of four states. It passed, it was scoped out, it found a fact, or it could not measure its input at all.
48
-
49
- The fourth is the one worth naming. An absent tool, a catalog that did not report, a corpus with nothing under it: each used to print a line that read like a pass. Now the run records it as unmeasured, the closing line says how many stages measured nothing, and the reader is not told a verdict nobody took.
50
-
51
- What happens next depends on where the run is. On a contributor's machine it warns and the run still exits 0, because an absent tool there is somebody mid-setup. Under CI, read off `CI=true`, it refuses, because the same absence on a runner is a broken workflow step.
52
-
53
- ## Exit codes
54
-
55
- | Code | Meaning |
56
- | ---- | ------------------------------------------------------------- |
57
- | `0` | every stage that ran reported, and none found a fact |
58
- | `1` | a stage found a fact, or could not measure its input under CI |
59
-
60
- One code for a failure, which is what a `bun run` caller and a git hook both read. An unmeasured stage takes no code of its own, since it has already refused under CI and reports on a contributor's machine, so a second code would name a state no caller branches on.
61
-
62
- ## The record
63
-
64
- `--json` puts one record on stdout and keeps every diagnostic on stderr:
65
-
66
- ```json
67
- {
68
- "ok": true,
69
- "root": "/path/to/checkout",
70
- "scoped": true,
71
- "changed": 12,
72
- "summary": {
73
- "ran": 23,
74
- "passed": 22,
75
- "skipped": 1,
76
- "unmeasured": 0,
77
- "failed": 0
78
- },
79
- "ms": 44453,
80
- "stages": [
81
- { "id": "indexes", "label": "Indexes", "status": "passed", "ms": 210 }
82
- ]
83
- }
84
- ```
85
-
86
- A failing stage carries its remedy in `failure`, and the same line goes to stderr so a caller reading neither the record nor the frame is still told what to fix.
87
-
88
- `ms` carries the wall time each stage took, including every process spawn, and the top-level `ms` sums them. Neither reading changes what a stage measures. Both exist so a slow run is attributable to a stage rather than read off a stopwatch held against the whole thing.
89
-
90
- The Tests stage holds a machine lock so one box runs one suite at a time, and its `ms` includes any time it queued behind another worktree's suite. A stage that queued carries `queuedMs`, the part of `ms` spent waiting, which is how to separate a queue from a slow suite. The field is absent on a stage that never waited. The wait is also announced on stderr when it starts, naming the holder's pid and worktree, in every mode.
@@ -1,78 +0,0 @@
1
- ---
2
- title: Hooks
3
- description: Running the post-merge steps through one verb, the order and the child call behind each, the root and working directory each step reads, the skip switches, the line each record maps to, and the reply link check
4
- ---
5
-
6
- # Hooks
7
-
8
- `canon hooks post-merge` runs the steps a git `post-merge` hook drives, so the hook itself stays a guard plus one call. It reads which pull requests the merge brought in, runs each step as a child `canon` call with `--json`, and prints one line per outcome worth reporting.
9
-
10
- ```bash
11
- canon hooks post-merge
12
- canon hooks post-merge --root "$root"
13
- ```
14
-
15
- | Option | Behavior |
16
- | --------------- | ---------------------------------------------------------------------------------------------------- |
17
- | `--root <path>` | Main worktree root holding the board and the records, resolved from `git worktree list` when omitted |
18
-
19
- The base stack ships the same call as its `.husky/post-merge`, behind the same board guard and upgrade bootstrap, so a target that syncs it runs these five steps.
20
-
21
- Exit codes: `0` on every path, since a hook failing aborts nothing useful. Lines go to stderr, and stdout stays empty. An ordinary merge prints nothing.
22
-
23
- ## The steps
24
-
25
- The steps run in this order, and the verb's `--help` lists the child calls from the same table it runs them from.
26
-
27
- | Step | Child call | Runs from |
28
- | ------------------------ | ------------------------------------------------------------- | ---------------- |
29
- | Archive the merged task | `canon tasks archive --pull-request <n> --root <root> --json` | the caller's cwd |
30
- | Push the records | `canon records push --root <root> --json` | the caller's cwd |
31
- | Reclaim merged worktrees | `canon worktrees reclaim --json` | the caller's cwd |
32
- | Reinstall the CLI | `canon upgrade --json`, under `CANON_NON_INTERACTIVE=1` | the caller's cwd |
33
- | Update the plugin cache | `canon claude plugin-update --json` | the caller's cwd |
34
-
35
- The archive runs once per pull request number, read as the trailing `(#NNN)` on every subject in `ORIG_HEAD..HEAD`, or on the tip alone when the merge left no `ORIG_HEAD`.
36
-
37
- The reclaim takes no `--root`. It refuses the worktree its own working directory names, so a root would let a pull inside a linked worktree remove the worktree it stands in. The archive and the push take the root because the board and the records live at the main worktree root.
38
-
39
- Each step is a separate process rather than an in-process call, so the plugin update runs the binary the upgrade just installed. Every child, and every `git` read, runs with the repository variables a hook exports stripped, since `GIT_DIR` beats `-C`.
40
-
41
- ## Reply links
42
-
43
- `canon hooks reply-links` checks that every markdown link in a reply names a file on disk. The `Stop` hook `reply-links.sh` runs it on the closing message of a turn, so a dead link is corrected in the same turn.
44
-
45
- ```bash
46
- printf '%s' "$reply" | canon hooks reply-links
47
- canon hooks reply-links --json < reply.md
48
- ```
49
-
50
- | Option | Behavior |
51
- | --------------- | ------------------------------------------------------------------------------------------------- |
52
- | `--root <path>` | Root a relative link resolves against, the main worktree root from `git worktree list` if omitted |
53
- | `--json` | Write `{ ok, missing, message }` to stdout. `message` is present only when `ok` is false |
54
-
55
- The reply comes on stdin, and a terminal on stdin is refused. A relative link resolves against the root, an absolute one is checked as written. The check skips placeholders such as `<path>`, globs, URLs, anchors, and any link inside a code span or fence. A `:42`, `:42-50`, or `#L42` tail is stripped before the check.
56
-
57
- Each entry in `missing` carries the `destination` as written and the `path` that was checked. A link into `.claude/worktrees/<name>/.canon/` also carries a `suggestion` with the main-root path when that file exists there.
58
-
59
- Exit codes: `0` when every link resolves or the reply holds none, `1` when at least one does not. The missing list goes to stderr. The hook turns a finding into exit 2 so the session continues, and exits 1 without blocking when it could not run the check, such as with no `jq` or no `canon` on `PATH`.
60
-
61
- ## Skip switches
62
-
63
- | Variable | Skips |
64
- | ---------------------------- | ----------------------- |
65
- | `CANON_SKIP_RECLAIM=1` | the worktree reclaim |
66
- | `CANON_SKIP_UPGRADE=1` | the CLI reinstall |
67
- | `CANON_SKIP_PLUGIN_UPDATE=1` | the plugin cache update |
68
-
69
- Any non-empty value skips the step, and each switch skips its own step only.
70
-
71
- ## What prints
72
-
73
- Each line is decided from the child's parsed record. A child producing no parseable record is an older binary carrying no such subcommand, and that step stays quiet.
74
-
75
- - Archive: the archived task and its number, plus a `Links not repointed in:` line naming the files in a non-empty `relinkFailed`, or the refusal's `message` with a pointer to the task board. A record carrying no `relinkFailed` reads as empty. `no-match` and `no-board` stay quiet.
76
- - Records push: the count of changed paths when above zero, or the refusal reason. `no-repository` stays quiet.
77
- - Reclaim: the removal count and the failure count, both read from the fields whatever the exit, or an unreadable reason. `gh-missing` stays quiet.
78
- - Upgrade and plugin update: the record's `message` on any state but `current`, or on a failure. The plugin update stays quiet on `no-claude` and `no-plugin`.
@@ -1,81 +0,0 @@
1
- ---
2
- title: Intake
3
- description: Reading intake folder counts and items, the three read states an item can be in, landing a batch of selections in one cluster, the refusal reasons, and why a call is scoped to one file
4
- ---
5
-
6
- # Intake
7
-
8
- ## List
9
-
10
- `canon intake list` reports the intake folders under `.canon/intake/`, or the items one folder holds. It reads and never writes, because an answer belongs to the operator and a verb that filled one would decide what the folder exists to ask.
11
-
12
- A folder carries a two-digit ordinal ahead of its slug, as in `21-toolkit-overview`, so a listing sorts by when each opened. A bare slug still resolves: passing `toolkit-overview` matches the one folder whose name is an ordinal ahead of it, and the folder's real name is what every command reports back.
13
-
14
- ```bash
15
- canon intake list
16
- canon intake list toolkit-overview --unread --json
17
- ```
18
-
19
- | Option | Behavior |
20
- | --------------- | ------------------------------------------------------------------------- |
21
- | `--unread` | Keep only what is unread: folders carrying one, or a folder's empty slots |
22
- | `--json` | Emit a machine-readable record on stdout |
23
- | `--root <path>` | Intake root, defaulting to the main worktree |
24
-
25
- With no slug it reports per-folder counts. With one it reports every item grouped by the cluster file holding it, each carrying its label, title, open question, suggestion, and answer.
26
-
27
- An item sits in one of three read states, and the counts keep them apart:
28
-
29
- | State | What it means |
30
- | --------- | ------------------------------------------------------- |
31
- | unread | The slot is present and empty, so nobody has reached it |
32
- | answered | The slot carries text, which is a decision already made |
33
- | malformed | The item carries no slot, so no verb here can answer it |
34
-
35
- The third is counted apart from both rather than folded into either. Counted as answered it hides a file that needs fixing behind a folder reading as fully worked through, and counted as unread it joins a list whose every entry `answer` then refuses.
36
-
37
- The index is skipped, since it points at items and answers nothing itself. So is any item a file displays inside a fence, which is how a folder copying the item format into its own overview is read as the sample it is rather than as an item offering a slot no reader owns.
38
-
39
- ## Answer
40
-
41
- `canon intake answer` writes selections into the answer slots of one cluster file.
42
-
43
- ```bash
44
- canon intake answer toolkit-overview --cluster 05-coverage.md --set 3=ok
45
- canon intake answer toolkit-overview --cluster 11-intake-skill.md --set 3d=ok --set 9="not worth it" --json
46
- ```
47
-
48
- | Option | Behavior |
49
- | --------------------- | ----------------------------------------------------- |
50
- | `--cluster <file>` | Cluster file the items live in, with or without `.md` |
51
- | `--set <item=answer>` | Answer to land on an item, repeatable |
52
- | `--json` | Emit a machine-readable record on stdout |
53
- | `--root <path>` | Intake root, defaulting to the main worktree |
54
-
55
- Items are labeled per cluster file, so a selection names the cluster and the label together. A label alone names an item in every cluster at once.
56
-
57
- A label may carry a letter suffix, as in `3a` beside `3`, which is how a pass records a finding split after the fact rather than renumbering every item below it. Pass it exactly as the heading spells it.
58
-
59
- One call writes one cluster. A call per selection is the alternative, and several of those against the same file read it before any of them writes, so every answer but the last is lost with nothing reporting it. The selections split on the first `=`, so an answer carrying its own survives intact.
60
-
61
- An item already carrying an answer refuses rather than being overwritten, and one filled item refuses the whole batch, so a partly applied write never lands. Drop the named item and send the rest.
62
-
63
- Exit codes: `0` every named item now carries its answer, `1` refused. The `reason` field carries `no-intake`, `no-folder`, `ambiguous-slug`, `no-cluster`, `no-item`, `answered`, or `bad-input`. A bare slug matching more than one ordinal-prefixed folder refuses as `ambiguous-slug` rather than `no-folder`, naming every match in `detail`.
64
-
65
- An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the record's `reason` rather than the exit when a skill consumes this, which matters most here because the verb writes.
66
-
67
- `bad-input` covers a malformed command line: no cluster, no selection, a selection that parses to no label and answer, an empty answer, an answer carrying a line break, or two answers for one item. It is separate from the reasons describing the folder, so a caller that mistyped a flag is not sent to repair a file that is fine.
68
-
69
- An empty answer refuses rather than writing an empty slot. The slot means unread while it is empty, so writing one back would report an item as answered that nobody decided.
70
-
71
- An answer occupies one line, and one carrying a line break refuses before anything is read. Writing it splices a bare continuation into the item that matches none of the patterns the reader tests, so the slot reads back as the text before the break while the item counts as answered, and the refusal on an already-answered item then leaves hand-editing the file as the only correction. The whole batch refuses, so a good selection beside a broken one never lands half applied.
72
-
73
- The folder is shared scratch at the main worktree root, so `--root` defaults to the first entry of `git worktree list` rather than the working directory. The write is an edit inside a file that already exists, which `Edit` and `Write` refuse from a linked worktree and a shell stream editor may not do, so the verb resolves the root in-process and rewrites whole lines.
74
-
75
- Skills branch on the reason rather than on the exit code:
76
-
77
- ```bash
78
- canon intake list toolkit-overview --unread --json | jq -r '.clusters[] | .cluster'
79
- ```
80
-
81
- For the folder layout, the item format, the answer contract, and retrieval, see `claude/skills/plan-intake/references/intake.md`.