@erclx/canon 5.37.0 → 5.38.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 (62) 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/backlog-triage/SKILL.md +1 -1
  4. package/claude/skills/build-in-slices/SKILL.md +1 -1
  5. package/claude/skills/git-issue/references/issue.md +11 -10
  6. package/claude/skills/markdown-craft/references/markdown.md +3 -2
  7. package/claude/skills/plan-groundwork/references/groundwork.md +53 -24
  8. package/claude/skills/review-pr/references/post.md +1 -1
  9. package/claude/skills/role-orchestrator/SKILL.md +1 -1
  10. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +1 -1
  11. package/claude/skills/role-planner/SKILL.md +1 -1
  12. package/claude/skills/role-worker/SKILL.md +1 -1
  13. package/claude/skills/target-setup/SKILL.md +3 -4
  14. package/claude/skills/target-setup/references/verify.md +6 -5
  15. package/claude/skills/task-board/references/tasks.md +3 -1
  16. package/docs/target/projects.md +1 -1
  17. package/docs/target/scenarios.md +1 -1
  18. package/governance/detect.toml +6 -1
  19. package/package.json +6 -3
  20. package/scripts/core/install-check.sh +1 -1
  21. package/src/commands/claude.ts +0 -1
  22. package/src/commands/pr.ts +1 -1
  23. package/src/commands/records.ts +0 -1
  24. package/src/docs/read.ts +15 -21
  25. package/src/gate/stages.ts +14 -0
  26. package/src/markdown/structure.ts +1 -1
  27. package/src/sandbox/equivalence.ts +7 -9
  28. package/src/teach/browser/head.ts +2 -2
  29. package/src/teach/browser/sidebar.ts +3 -3
  30. package/src/teach/browser/theme.ts +2 -2
  31. package/src/tooling/inject.ts +2 -2
  32. package/src/tooling/package.ts +15 -18
  33. package/src/tooling/verify-stack.ts +6 -3
  34. package/tooling/astro/configs/.oxfmtrc.json +8 -0
  35. package/tooling/astro/configs/.prettierrc +13 -0
  36. package/tooling/astro/manifest.toml +13 -0
  37. package/tooling/astro/reference.md +15 -5
  38. package/tooling/base/configs/.oxfmtrc.json +7 -0
  39. package/tooling/base/configs/scripts/verify.ts +1 -1
  40. package/tooling/base/manifest.toml +6 -6
  41. package/tooling/base/reference.md +12 -9
  42. package/tooling/base/seeds/.lintstagedrc +1 -3
  43. package/tooling/base/seeds/canon/context/ci.md +3 -3
  44. package/tooling/base/seeds/canon/context/development.md +2 -2
  45. package/tooling/claude/manifest.toml +5 -1
  46. package/tooling/go/reference.md +2 -2
  47. package/tooling/nextjs/configs/.oxlintrc.json +223 -0
  48. package/tooling/nextjs/manifest.toml +2 -5
  49. package/tooling/nextjs/reference.md +4 -4
  50. package/tooling/php/manifest.toml +6 -1
  51. package/tooling/php/reference.md +2 -2
  52. package/tooling/python/manifest.toml +6 -1
  53. package/tooling/python/reference.md +4 -4
  54. package/tooling/vite-react/manifest.toml +1 -4
  55. package/tooling/vite-react/reference.md +3 -3
  56. package/tooling/web/configs/.oxlintrc.json +213 -0
  57. package/tooling/web/manifest.toml +9 -13
  58. package/tooling/web/reference.md +14 -14
  59. package/tooling/base/configs/.prettierrc +0 -12
  60. package/tooling/base/seeds/.prettierignore +0 -0
  61. package/tooling/nextjs/configs/eslint.config.js +0 -91
  62. package/tooling/web/configs/eslint.config.js +0 -82
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
4
- "version": "5.37.0",
4
+ "version": "5.38.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -50,7 +50,7 @@ Check for a `**Files to touch:**` or `## Files to touch` marker per `${CLAUDE_SK
50
50
 
51
51
  Read `<plan>` at the main worktree root. This file is the scope for this run.
52
52
 
53
- Its sections and its answer contract are fixed by `${CLAUDE_SKILL_DIR}/../plan-feature/references/plan.md`. A blank `- Answer:` accepts the `- Suggested:` line above it, so an unanswered question is a decision this run executes rather than a reason to stop. When this run decides against that suggestion, rewrite the `- Suggested:` line as `overridden at execution to <pick>,` plus the measurement, leaving the `- Answer:` slot blank, and put the same deviation in one line under the open task's `## Findings`, per `${CLAUDE_SKILL_DIR}/../plan-feature/references/plan.md`.
53
+ Its sections and its answer contract are fixed by `${CLAUDE_SKILL_DIR}/../plan-feature/references/plan.md`. A blank `- Answer:` accepts the `- Suggested:` line above it, so an unanswered question is a decision this run executes rather than a reason to stop. When this run decides against that suggestion, rewrite the `- Suggested:` line as `overridden at execution to <pick>,` plus the measurement, leaving the `- Answer:` slot blank, and put the same deviation in one line under the open task's `## Findings`, per `${CLAUDE_SKILL_DIR}/../plan-feature/references/plan.md`, opening with the bold source-and-date label that `${CLAUDE_SKILL_DIR}/../task-board/references/tasks.md` fixes.
54
54
 
55
55
  ## Step 2: implement
56
56
 
@@ -112,7 +112,7 @@ Take the task stem from the item heading. Every verdict leaves the folder slug o
112
112
  - **keep**: append the `## Findings` line naming what reproduced. The backlog line stays.
113
113
  - **promote**: move the backlog line to the bottom of `## Needs a plan` in `.canon/tasks/priority.md`, with a `Waiting on` cell stating what cleared and ending in `last`, then append the `## Findings` line.
114
114
 
115
- Write the `## Findings` line as `- <YYYY-MM-DD>, backlog triage <slug>: <verdict>, <evidence>`. It is the last write on every verdict with a task file behind it, since Step 1 reads it as applied and a line written ahead of a refused move would mark a row done that never moved.
115
+ Write the `## Findings` line as `- **<YYYY-MM-DD>, backlog triage <slug>:** <verdict>, <evidence>`, the bold label form `${CLAUDE_SKILL_DIR}/../task-board/references/tasks.md` fixes. It is the last write on every verdict with a task file behind it, since Step 1 reads it as applied and a line written ahead of a refused move would mark a row done that never moved.
116
116
 
117
117
  Before any promote, check the roster the way `task-board` step 4 does: a live session in this repository whose name starts with `orchestrator-` and whose `sessionId` is not this one. When one is found, write the `## Findings` line but not `priority.md` or `backlog.md`, and message that session with the row and its cell so it places the row itself.
118
118
 
@@ -40,7 +40,7 @@ Leave the build passing at every commit. A slice that cannot pass on its own is
40
40
 
41
41
  A defect outside the plan that turns up mid-build is a finding, not a slice. Fixing it on this branch widens a diff the plan scoped and hides the fix inside an unrelated review.
42
42
 
43
- - Write it in one line under the open task's `## Findings`, naming the file and what is wrong, when the build is closing a task.
43
+ - Write it in one line under the open task's `## Findings`, naming the file and what is wrong, when the build is closing a task. Open the line with the bold source-and-date label from `${CLAUDE_SKILL_DIR}/../task-board/references/tasks.md`.
44
44
  - Carry it in the handover message instead when no task exists, and never create a file to hold it.
45
45
  - Leave the file as it is, even when the fix is one line.
46
46
  - Fix it here only when the plan's own change cannot pass without it, and then say so in the commit that does.
@@ -53,17 +53,18 @@ Does not govern:
53
53
  ### Feedback
54
54
 
55
55
  - One `## Toolkit feedback` wrapper holds every field as a nested `###` heading. A flat `##` field is not recognized.
56
- | Field | Holds |
57
- | --- | --- |
58
- | `### From project` (optional) | the reporting project's name or kind. |
59
- | `### Surface` (required) | the toolkit surface and its type, and the specific file or name when known. |
60
- | `### Observed` (required) | the observed behavior. |
61
- | `### Expected` (optional) | the expected behavior, or `unclear`. |
62
- | `### Repro` (optional) | repro details, or `none`. |
63
- | `### Proposed fix` (required) | a proposed fix, or `open` when the session settled on no direction. |
56
+
57
+ | Field | Holds |
58
+ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
59
+ | `### From project` (optional) | the reporting project's name or kind. |
60
+ | `### Surface` (required) | the toolkit surface and its type, and the specific file or name when known. |
61
+ | `### Observed` (required) | the observed behavior. |
62
+ | `### Expected` (optional) | the expected behavior, or `unclear`. |
63
+ | `### Repro` (optional) | repro details, or `none`. |
64
+ | `### Proposed fix` (required) | a proposed fix, or `open` when the session settled on no direction. |
64
65
  | `### Suggested shape` (optional) | the template, output, or field list the change should take, inside a `<details>` block. Omit the heading when there is none. |
65
- | `### Why` (optional) | the reasons for the fix, one bullet each. Omit the heading when there are none. |
66
- | `### Links` (optional) | the issues, pull requests, or documents the report cites, one per line. Omit the heading when there are none. |
66
+ | `### Why` (optional) | the reasons for the fix, one bullet each. Omit the heading when there are none. |
67
+ | `### Links` (optional) | the issues, pull requests, or documents the report cites, one per line. Omit the heading when there are none. |
67
68
 
68
69
  - A required field is refused when present but empty. Write the fallback literal for From project, Expected, and Repro rather than leaving one blank.
69
70
 
@@ -42,6 +42,7 @@ Does not govern:
42
42
  - H1 for document title, H2 for main sections, H3 for subsections
43
43
  - Use sentence case for all headings (H1, H2, H3). The audit reports a heading or a frontmatter `title` with a capitalized word after the first, and a proper noun it reports stays as it is.
44
44
  - Proper nouns and product names retain their casing in headings
45
+ - Shape a record as H2 per section and H3 per item a reader looks up by name, with a few bullets or prose under each H3. Use a table only for a fixed-shape set.
45
46
 
46
47
  ### Seams
47
48
 
@@ -67,11 +68,11 @@ Does not govern:
67
68
 
68
69
  - Use prose by default. Reserve bullets for discrete, unrelated items.
69
70
  - Keep paragraphs to four sentences or fewer. Split longer blocks at the next logical boundary.
70
- - Past roughly 700 characters in one paragraph, folding in the lines that wrap it, split at the next logical boundary as well. A paragraph written as two long sentences satisfies the sentence cap above and still asks the reader to hold too much at once. This number sits well above the bullet checkpoint because a paragraph is read straight through and a bullet is scanned. A paragraph a branch adds or grows past it fails the push in a context entry through the `paragraph-growth` gate stage, and an unchanged long paragraph passes. Elsewhere it is the audit's warning and no more.
71
+ - Past roughly 700 characters in one paragraph, folding in the lines that wrap it, split at the next logical boundary as well. Two long sentences satisfy the sentence cap and still overload the reader. This number sits well above the bullet checkpoint because a paragraph is read straight through and a bullet is scanned. A paragraph a branch adds or grows past it fails the push in a context entry through the `paragraph-growth` gate stage, and an unchanged long paragraph passes. Elsewhere it is the audit's warning and no more.
71
72
 
72
73
  ### Bullets
73
74
 
74
- - Keep bullets tight. Past roughly 250 characters in one top-level bullet, counting the lines that continue it and excluding any bullet nested under it, the overflow belongs in prose. A bullet a branch adds or grows past it fails the push in a context entry through the `paragraph-growth` gate stage, and an unchanged one passes until edited. Elsewhere it is the audit's warning and no more. The number is a checkpoint rather than a cap, and a bullet reading well past it means the number is wrong rather than the rule.
75
+ - Keep bullets tight. Past roughly 250 characters in one top-level bullet, counting the lines that continue it and excluding any bullet nested under it, the overflow belongs in prose. A bullet a branch adds or grows past it fails the push in a context entry through the `paragraph-growth` gate stage, and an unchanged one passes until edited. Elsewhere it is the audit's warning and no more. The number is a checkpoint rather than a cap.
75
76
  - Keep a run of consecutive top-level bullets to 5 or fewer under one heading, and fold a longer one into prose, an H3 per item, or a split at the seam. A blank line, a continuation, and a nested item do not break the run, and a paragraph or heading does. The audit reports a longer run and the `bullet-stacks` gate stage fails it.
76
77
  - Leave a run whose every item opens with a bold label or a link as it is, since each item names its own subject. A task box (`[ ]`) is not a link, so a checklist counts as plain bullets. Numbered items do not count toward the cap. Governance rules and `REQUIREMENT.md` sit outside it, since a rule is one imperative per bullet.
77
78
  - Collapse a stack of bullets narrating one subsystem into a single `###` subsection carrying one narrative. Splitting a heavy bullet into three light ones satisfies the checkpoint above and leaves the reader no better off, and subdividing a block does not lighten the bullets inside it, so the two rules answer different defects.
@@ -33,8 +33,8 @@ Governs a groundwork track under `.canon/groundwork/<nn>-<slug>/`: folder layout
33
33
  Does not govern:
34
34
 
35
35
  - **`groundwork-spikes.md`**: `08-spikes.md`, what each spike records, and the folders holding what a spike produced
36
- - **`intake.md`**: A dump of many findings filed by domain, each carrying its own verdict
37
- - **`plan.md`**: the feature plan a closed track feeds, and the contract its answer slots keep
36
+ - **`intake.md`**: A dump of many findings filed by domain, each with a verdict
37
+ - **`plan.md`**: the feature plan a closed track feeds, and its answer contract
38
38
  - **`tasks.md`**: the task file a closing track writes, and the origin line pointing back at the folder
39
39
  - **the `write-human` skill**: voice, rhythm, and sentence construction
40
40
  - **`markdown.md`**: headings, punctuation, word choice, and file references
@@ -42,9 +42,9 @@ Does not govern:
42
42
 
43
43
  ## Folder name
44
44
 
45
- - Name the folder `<nn>-<slug>`, a two-digit zero-padded ordinal followed by a kebab-case slug.
46
- - The ordinal marks a folder per track, opened once and worked over time, distinct from a file addressed by name, one already carrying its own sorting prefix, or one deliberately overwritten.
47
- - Claim the ordinal through `canon records ordinal groundwork <slug> --claim`, which reads the highest one across both `.canon/groundwork/` and `.canon/intake/` and creates the folder in one atomic act, so two sessions opening at once never share a number. The two kinds share one creation-order line.
45
+ - Name the folder `<nn>-<slug>`, a two-digit ordinal and a kebab-case slug.
46
+ - The ordinal marks a folder per track, opened once and worked over time, unlike a file addressed by name, one with its own sorting prefix, or one overwritten on purpose.
47
+ - Claim the ordinal through `canon records ordinal groundwork <slug> --claim`, which reads the highest one across both `.canon/groundwork/` and `.canon/intake/` and creates the folder in one atomic act, so two sessions opening at once never share a number. The two kinds share one order.
48
48
  - With neither folder holding an entry, the first one opened takes `01`, whatever number a track's own files start at.
49
49
  - Never renumber an existing folder. The ordinal is the order it opened, and a later reader cites it by that name.
50
50
 
@@ -53,17 +53,17 @@ Does not govern:
53
53
  A track works when a session that has never seen it re-enters from the folder alone and can answer each of these:
54
54
 
55
55
  - Which single question is being measured, and why is it running now?
56
- - What is the current state, measured during this pass rather than carried in from an earlier one?
57
- - Which questions are still open, where does the evidence point, and what would overturn that?
56
+ - What is the current state, measured this pass rather than carried in?
57
+ - Which questions are open, where is the evidence, and what overturns it?
58
58
  - What was decided, and what was considered and dropped?
59
59
 
60
- A track failing these is non-conforming even when it satisfies every shape rule below.
60
+ A track failing these is non-conforming even when it meets every shape rule.
61
61
 
62
62
  ## Frontmatter and dating
63
63
 
64
64
  - `title` (required): the track subject in sentence case
65
65
  - `description` (required): one line naming what the track measures
66
- - `date` (required, `README.md` only): the day the folder opened, as `YYYY-MM-DD`
66
+ - `date` (required, `README.md` only): the day it opened, as `YYYY-MM-DD`
67
67
 
68
68
  Carry the opening date as a frontmatter field rather than a sentence in the body, since nothing that walks the folder reads prose. Date the folder once rather than every file, because a per-file date goes stale on the first edit. The checkable half is the commit each measurement was taken against, which the file holding that measurement names.
69
69
 
@@ -90,13 +90,13 @@ Orients. Holds no findings.
90
90
 
91
91
  - A one-line definition of the investigation
92
92
  - A `## Why` section stating why the track is running now
93
- - A file-map table of filename and what it holds, kept current as files are added or retired
93
+ - A file-map table of filename and what it holds, kept current
94
94
  - A `## Method` section splitting internal sources from external ones, naming which were used and which were not yet done, and listing under a leads heading any external source found but not opened
95
95
 
96
96
  ### Closing sections and phase
97
97
 
98
98
  - A `## Prior art` section
99
- - A `## Source citation` section stating the rule below, so a returning session picks it up from the folder
99
+ - A `## Source citation` section stating the rule below, so a returning session finds it in the folder
100
100
  - The phase stated out loud in the first three lines, in the form `Groundwork phase. Nothing here is a feature plan.`
101
101
 
102
102
  Every claim about a source outside the project carries a link to it, wherever the claim appears in the folder. Without one, a later reader cannot tell a fetched page from recall. A source found and not read is listed as a lead and is never cited, since a link to a page nobody opened is worse than no link. Listing it stops a later pass re-searching for it.
@@ -109,23 +109,19 @@ Facts before opinion. Verified measurement only, taken during this pass.
109
109
 
110
110
  - Never carry a figure from a previous session without re-measuring, since every number built on a stale one is quietly wrong.
111
111
  - Mark an inference as an inference where one is unavoidable.
112
- - Measure only what an open question needs. A number with no question attached is the mechanism by which the groundwork becomes the work.
112
+ - Measure only what an open question needs. A number with no question attached is how the groundwork becomes the work.
113
113
 
114
114
  ## 00-scope.md
115
115
 
116
- Written when the subject is large enough to run away. Holds constraints, risks, the open question list, and the downstream surfaces a decision would touch. A small track skips it.
116
+ Written when the subject is large enough to run away. Holds constraints, risks, the open questions, and what a decision would touch. A small track skips it.
117
117
 
118
118
  ## 06-decision.md
119
119
 
120
- Closes the folder. Everything above it is input.
120
+ Closes the folder. Everything above it is input. Write it from the skeleton under `## Template`, with an H3 per dropped alternative.
121
121
 
122
- - The problem stated once
123
- - The goal
124
- - The items to do
125
- - What was considered and dropped
126
- - `## Revisit when`: the "Overturned by" line of each lean that became the decision, carried over as written. A lean the decision did not take carries nothing, and a track whose deciding leans carried no overturn line omits the section.
122
+ `## Revisit when` carries the "Overturned by" line of each lean that became the decision, as written. A lean the decision did not take carries nothing, and a track whose deciding leans carried no overturn line omits the section.
127
123
 
128
- The dropped list is what stops a future session re-proposing something already rejected.
124
+ The dropped list stops a future session re-proposing a rejected idea.
129
125
 
130
126
  ## 07-next-session.md
131
127
 
@@ -141,8 +137,8 @@ Every open question carries a lean, wherever it appears. A bare numbered list ha
141
137
  - Overturned by: <the finding that would change it>
142
138
  ```
143
139
 
144
- - Pair every lean with what would overturn it. A lean with no falsifier is an opinion.
145
- - On a measurement rather than a judgment, write `- Leaning: none, needs measuring` and drop the overturn line. A guess at a number is worse than an admission.
140
+ - Pair every lean with what would overturn it. A lean with no falsifier is opinion.
141
+ - On a measurement rather than a judgment, write `- Leaning: none, needs measuring` and drop the overturn line. A guess at a number is worse.
146
142
  - Mark each question open or answered, and repeat the open ones at the end of the file they belong to. That gives the decision file its agenda for free.
147
143
 
148
144
  A lean is weaker than a plan's suggestion: it records the current read on an open question, not a decision to accept at execution time.
@@ -155,13 +151,15 @@ A lean is weaker than a plan's suggestion: it records the current read on an ope
155
151
 
156
152
  ## Anti-patterns
157
153
 
158
- - **The groundwork becomes the work.** Gathering expands until the measuring costs more than the change it justifies. Cap it, and drop any thread with no question attached.
154
+ - **The groundwork becomes the work.** Gathering expands until the measuring costs more than the change it justifies. Cap it, and drop any thread with no question.
159
155
  - **Deciding by omission.** Closing a track while an unresolved question quietly fails an outcome. Resolve it or record it as knowingly accepted.
160
156
  - **Recording a constraint discovered while defending a decision.** Check it against the alternative design first, or a fact about the current shape reads as inherent to the problem.
161
157
  - **A plan written before the groundwork.** Every track that has done this had to supersede the plan it wrote.
162
158
 
163
159
  ## Template
164
160
 
161
+ ### README.md
162
+
165
163
  ```markdown
166
164
  ---
167
165
  title: <Track subject>
@@ -195,10 +193,41 @@ Groundwork phase. Nothing here is a feature plan.
195
193
 
196
194
  ## Prior art
197
195
 
198
- <Earlier plans, folders, or decisions this track supersedes or builds on.>
196
+ ### <earlier plan or folder>
197
+
198
+ <What it decided and how this track supersedes or builds on it. Say not to go looking for it again.>
199
199
 
200
200
  ## Source citation
201
201
 
202
202
  Every claim about a source outside the project carries a link. A source found
203
203
  and not read is listed as a lead and is never cited.
204
204
  ```
205
+
206
+ ### 06-decision.md
207
+
208
+ ```markdown
209
+ ---
210
+ title: <Track subject> decision
211
+ description: <one line naming what the track decided>
212
+ ---
213
+
214
+ # <Track subject> decision
215
+
216
+ ## Problem and goal
217
+
218
+ <The problem stated once, then what done looks like.>
219
+
220
+ ## What to do
221
+
222
+ 1. <item>
223
+
224
+ ## Considered and dropped
225
+
226
+ ### <alternative>
227
+
228
+ <Why it was dropped, and what would revive it.>
229
+
230
+ ## Revisit when
231
+
232
+ - <an "Overturned by" line, as written>
233
+ ```
@@ -80,7 +80,7 @@ Keying either half of the opening's threshold on the grade was measured wrong: a
80
80
 
81
81
  The cost is that the merge decision no longer reads off the heading alone, since an open heading covers a minor as well as a critical. Take it from the counts on the summary line, which is where they already sit. No summary line reports the merge as unblocked under either heading, since a thread reading open cannot also report that nothing blocks it.
82
82
 
83
- A minor the dispatched worker declines is what needs a surface that survives the merge, rather than every minor, since one that gets fixed on the branch needs no durable record. Write a declined minor into the `## Findings` section of the task the branch closes, which is where the queue-refill sweep already routes a finding that changes another task. A declined finding left on the thread alone is lost the moment the pull request merges.
83
+ A minor the dispatched worker declines is what needs a surface that survives the merge, rather than every minor, since one that gets fixed on the branch needs no durable record. Write a declined minor into the `## Findings` section of the task the branch closes, as a line opening with the bold source-and-date label from `${CLAUDE_SKILL_DIR}/../task-board/references/tasks.md`, which is where the queue-refill sweep already routes a finding that changes another task. A declined finding left on the thread alone is lost the moment the pull request merges.
84
84
 
85
85
  Read the state off the most recent review comment rather than off the presence of a closed one. A close-out does not close the pull request, so a commit pushed after it gets its own pass, and that pass reopens the review under `## Review` when it raises a finding of any grade.
86
86
 
@@ -43,7 +43,7 @@ No surface carries cross-version sequencing, so report none. A row's `Waiting on
43
43
 
44
44
  A compaction is a moment this skill cannot detect, so the human asks for each side of it and this skill reads the matching runbook when they do.
45
45
 
46
- On a request to write the handoff or save the session, read `${CLAUDE_SKILL_DIR}/references/orchestrator-handoff.md` and follow it. It sends the generic half to `canon:session-map`, which writes a session map per `${CLAUDE_SKILL_DIR}/../session-map/references/session.md`, then adds the decisions taken under delegated authority as this role's extension. A lesson about how to work goes to the owning task's Findings. Write nothing to the handoff that the board, a task file, or a groundwork folder already carries.
46
+ On a request to write the handoff or save the session, read `${CLAUDE_SKILL_DIR}/references/orchestrator-handoff.md` and follow it. It sends the generic half to `canon:session-map`, which writes a session map per `${CLAUDE_SKILL_DIR}/../session-map/references/session.md`, then adds the decisions taken under delegated authority as this role's extension. A lesson about how to work goes to the owning task's Findings, as a bold-labeled line per `${CLAUDE_SKILL_DIR}/../task-board/references/tasks.md`. Write nothing to the handoff that the board, a task file, or a groundwork folder already carries.
47
47
 
48
48
  On a request to resume after a compaction, read `${CLAUDE_SKILL_DIR}/references/orchestrator-resume.md`, which reads that file back with the board and the groundwork behind the live work.
49
49
 
@@ -31,7 +31,7 @@ The blocker cell states what the row waits on, and each kind is tested different
31
31
 
32
32
  Every measurement this pass takes is a blocker re-test on a row already filed, and it decides whether that row can start rather than how big it is. Sizing a row is the filing boundary `role-orchestrator` states under `## Boundaries`, which leaves the extent and the cause to whoever plans the row.
33
33
 
34
- Write the result into the row. A re-test reported in chat is lost at the next compaction and the next pass measures the same thing again. Rewrite a blocker cell whose test no longer holds, move the row to the group its new state puts it in, and record the measurement in that task's `## Findings` with the date it was taken. `### Writing the board` in `role-orchestrator` owns the method, and `canon tasks validate` runs once the board is rewritten and before the report.
34
+ Write the result into the row. A re-test reported in chat is lost at the next compaction and the next pass measures the same thing again. Rewrite a blocker cell whose test no longer holds, move the row to the group its new state puts it in, and record the measurement in that task's `## Findings` as a line labeled with this session and the date it was taken, per `${CLAUDE_SKILL_DIR}/../task-board/references/tasks.md`. `### Writing the board` in `role-orchestrator` owns the method, and `canon tasks validate` runs once the board is rewritten and before the report.
35
35
 
36
36
  ## Re-testing a backlog row
37
37
 
@@ -69,7 +69,7 @@ it.
69
69
 
70
70
  - Never write `.canon/tasks/priority.md` or `.canon/tasks/backlog.md`. Both are gitignored, so an overwrite drops a row with no history to recover it from.
71
71
  - Never write the task file. The record is the whole output, and a row edited from here changes what the controller reads back as the state of the board.
72
- - One exception, and it belongs to `plan-groundwork` alone: that skill writes a single task file at close, recording what the track concluded. Its own `## Write scope` states the exception, and this line exists so the rule above does not forbid what that skill requires.
72
+ - One exception, and it belongs to `plan-groundwork` alone: that skill writes a single task file at close, recording what the track concluded. Each finding in it opens with the bold source-and-date label that `${CLAUDE_SKILL_DIR}/../task-board/references/tasks.md` fixes. Its own `## Write scope` states the exception, and this line exists so the rule above does not forbid what that skill requires.
73
73
  - Report what the row got wrong rather than repairing it. A stale count, a moved line, or a path that no longer resolves goes into the record and into the handback, and the controller decides which of the two carries the correction.
74
74
  - Leave the cross-feature call alone. Which rows collide, what merges before what, and whether a row should run at all stay with the controller. `priority.md` shows blockers and file sets and is enough to write a confident merge order off a partial picture, which is the failure this boundary exists to prevent.
75
75
 
@@ -37,7 +37,7 @@ which `auto-ship` Step 2 loads.
37
37
  - Copy a ready folder's files verbatim to the paths its plan's constraints name, when the plan's `**Constraints:**` block names one, and edit only what the gate or the overview's own list requires. A rewrite discards the text the handoff exists to carry.
38
38
  - Report a draft flag reading ready once, and leave it cleared. The ship chain marks the pull request a draft, and only `canon pr lift` or the operator un-marks it, so a flag reading ready afterwards passed every gate the verb reads or was readied by a person, which GitHub requires before a merge. Re-drafting fights them, and no surface in the tree states a reason to.
39
39
  - Leave the lift to the reviewers, who run `canon pr lift` on posting their close, and to the controlling session as the fallback. Refuse an instruction to ready the pull request directly, whoever sends it, since a direct ready skips every gate the verb reads.
40
- - Rewrite a plan question's `- Suggested:` line as `overridden at execution to <pick>,` plus the measurement when this build decides against an unanswered one, leaving the `- Answer:` slot blank, and put the same deviation in one line under the open task's `## Findings`, per `${CLAUDE_SKILL_DIR}/../plan-feature/references/plan.md`.
40
+ - Rewrite a plan question's `- Suggested:` line as `overridden at execution to <pick>,` plus the measurement when this build decides against an unanswered one, leaving the `- Answer:` slot blank, and put the same deviation in one line under the open task's `## Findings`, per `${CLAUDE_SKILL_DIR}/../plan-feature/references/plan.md`, opening with the bold source-and-date label that `${CLAUDE_SKILL_DIR}/../task-board/references/tasks.md` fixes.
41
41
 
42
42
  ## The board is read-only
43
43
 
@@ -99,7 +99,7 @@ Step 2: `canon tooling sync <tooling-stack> --write` installs stack deps, script
99
99
  CANON_NON_INTERACTIVE=1 canon tooling sync <tooling-stack> <target> --write
100
100
  ```
101
101
 
102
- Monorepo with multiple language roots: run `canon init` once at the repo root so `base` (husky, prettier, commitlint, CI) lands single, then sync each subtree with `--skip base` so the shared layer is not re-dropped. Take the subtree commands from the per-root rows the preview printed rather than picking folders by hand. Detection found each root and its stack, and the two commands below show the shape.
102
+ Monorepo with multiple language roots: run `canon init` once at the repo root so `base` (husky, oxfmt, commitlint, CI) lands single, then sync each subtree with `--skip base` so the shared layer is not re-dropped. Take the subtree commands from the per-root rows the preview printed rather than picking folders by hand. Detection found each root and its stack, and the two commands below show the shape.
103
103
 
104
104
  ```bash
105
105
  CANON_NON_INTERACTIVE=1 canon tooling sync vite-react ./frontend --skip base --write
@@ -112,11 +112,10 @@ A subtree sync withholds `.github/`, since GitHub reads workflows only at the re
112
112
 
113
113
  Step 3: post-sync fixups. Golden configs arrive from sync, so no config generation is required. But a few items may need a one-time touch:
114
114
 
115
- - **ESLint version pin.** If `bun create vite` installed `eslint@^10` and the manifest pins `eslint@^9`, sync does not override a present dep. Run `bun add -d eslint@^9` if `bun run lint:fix` fails with `Class extends value undefined`.
116
115
  - **File naming.** `bun create vite`'s `App.tsx` violates the `KEBAB_CASE` rule. Rename to `app.tsx` and update the import in `main.tsx`.
117
116
  - **Docs.** Run `canon tooling reference <tooling-stack>` and `canon tooling reference web` for any stack-specific follow-ups (Chrome extension overrides, setup script details).
118
117
 
119
- Do not generate ESLint, Vitest, or Playwright configs. They ship as golden files. Generating from prose duplicates what sync already installed.
118
+ Do not generate oxlint, ESLint, Vitest, or Playwright configs. They ship as golden files. Generating from prose duplicates what sync already installed.
120
119
 
121
120
  ## Phase: gov
122
121
 
@@ -161,7 +160,7 @@ After the chain, report:
161
160
 
162
161
  - Domains installed, with a check per domain
163
162
  - Tooling stack synced, or skipped. Name the layers pulled via the extends chain.
164
- - Any post-sync fixups applied (ESLint pin, filename renames)
163
+ - Any post-sync fixups applied (filename renames)
165
164
  - `verify` outcome, naming the depth it ran at
166
165
 
167
166
  ### Outcomes and gaps
@@ -58,10 +58,11 @@ exit code is the whole verdict.
58
58
  | Order | Script | Skip if |
59
59
  | ----- | ----------- | --------------------------- |
60
60
  | 1 | `lint:fix` | absent |
61
- | 2 | `typecheck` | absent |
62
- | 3 | `check` | absent |
63
- | 4 | `test:run` | absent, fall back to `test` |
64
- | 5 | `build` | absent |
61
+ | 2 | `format` | absent |
62
+ | 3 | `typecheck` | absent |
63
+ | 4 | `check` | absent |
64
+ | 5 | `test:run` | absent, fall back to `test` |
65
+ | 6 | `build` | absent |
65
66
 
66
67
  Do not run composite scripts like `check:full`. Do not run `dev`, `preview`,
67
68
  `test:e2e`, `test:ui`, or `screenshot` at this depth. They belong to `deep`
@@ -80,7 +81,7 @@ server, and whether the screenshot harness produces output.
80
81
  | 3 | `test:e2e` | absent | leaf script: exit code |
81
82
  | 4 | `screenshot` | absent | leaf script: exit code |
82
83
 
83
- `deep` runs the four above and not the five leaf scripts, which the default
84
+ `deep` runs the four above and not the six leaf scripts, which the default
84
85
  depth already covers. The two sets are disjoint, so running both depths runs
85
86
  every declared script exactly once.
86
87
 
@@ -82,6 +82,8 @@ Two headings, `## Outcomes` and `## Findings`. Outcomes are future and checkable
82
82
 
83
83
  Add no third heading. Status stays inline on an outcome rather than becoming an "In progress" section.
84
84
 
85
+ Open each finding with a bold label naming its source and date, as in `- **planner, 2026-10-11:** <finding>`, and keep it to a few sentences. Anything longer moves into the plan or the groundwork folder. Outcomes stay a checklist, since the board tools read them by their `- [ ]` line.
86
+
85
87
  Size the outcomes so one pull request closes all of them, and split the task before handing it off rather than after. A task closes whole or not at all, so outcomes spanning two pull requests leave the board showing in-progress work no branch carries.
86
88
 
87
89
  A sliced task is the one allowed exception: a task the operator decided to ship across several pull requests on purpose, one slice each. Its `Pull request:` line lists every slice, so the board records which merged work covered which outcomes, and it closes whole when the last slice's merge finds every outcome ticked and no slice left pending.
@@ -115,7 +117,7 @@ Why this task exists and what it depends on.
115
117
 
116
118
  ## Findings
117
119
 
118
- - What constrains the task, dated where it matters.
120
+ - **<source>, <YYYY-MM-DD>:** What constrains the task.
119
121
 
120
122
  > Test strategy: <unit | component | e2e | visual | manual>, what is being verified
121
123
  ```
@@ -57,7 +57,7 @@ The chain is:
57
57
 
58
58
  - `canon init` installs base tooling, Claude seeds, and governance rules into `.claude/rules/` in the same pass
59
59
  - `canon tooling sync <stack> --write` adds stack-specific deps, scripts, and gitignore entries
60
- - The agent reads `canon tooling reference <stack>` (plus parents) as its audit context, follows it to generate eslint, vitest, playwright configs and the stack's setup script, and extends `canon/context/ci.md` and `canon/context/development.md` per the reference's extend sections <!-- audit-ignore-citations: canon/context/ci.md, canon/context/development.md -->
60
+ - The agent reads `canon tooling reference <stack>` (plus parents) as its audit context, follows it to generate oxlint, vitest, playwright configs and the stack's setup script, and extends `canon/context/ci.md` and `canon/context/development.md` per the reference's extend sections <!-- audit-ignore-citations: canon/context/ci.md, canon/context/development.md -->
61
61
  - The `verify` phase runs the installed `package.json` scripts (lint, typecheck, check, test, build) and reports pass or fail
62
62
  - The `indexes` phase bootstraps the `index.md` system over the project's own documentation folders, confirming candidate folders with the operator rather than running unattended
63
63
 
@@ -46,7 +46,7 @@ canon tooling sync vite-react ./frontend --skip base --write
46
46
  canon tooling sync python ./backend --skip base --write
47
47
  ```
48
48
 
49
- `--skip base` drops the `base` layer from each subtree sync, so husky, prettier, commitlint, and CI stay single at the repo root. The one exception is base's verify runner, `scripts/verify.ts` and `scripts/verify.json`, which every subtree still receives so it can verify on its own. Without it, every subtree re-drops husky, and since git honors only one `core.hooksPath` the extra hook dirs silently break. Each subtree still gets its own framework configs (eslint, vitest, tsconfig, vite), and its own stack reference reads through `canon tooling reference <stack>`.
49
+ `--skip base` drops the `base` layer from each subtree sync, so husky, oxfmt, commitlint, and CI stay single at the repo root. The one exception is base's verify runner, `scripts/verify.ts` and `scripts/verify.json`, which every subtree still receives so it can verify on its own. Without it, every subtree re-drops husky, and since git honors only one `core.hooksPath` the extra hook dirs silently break. Each subtree still gets its own framework configs (oxlint, vitest, tsconfig, vite), and its own stack reference reads through `canon tooling reference <stack>`.
50
50
 
51
51
  A sync whose target sits below the git root knows it is writing into a subfolder:
52
52
 
@@ -27,7 +27,12 @@ files = [".github/workflows"]
27
27
 
28
28
  ["dependencies"]
29
29
  deps = []
30
- files = ["**/package.json", "**/pyproject.toml", "**/go.mod", "**/composer.json"]
30
+ files = [
31
+ "**/package.json",
32
+ "**/pyproject.toml",
33
+ "**/go.mod",
34
+ "**/composer.json",
35
+ ]
31
36
 
32
37
  ["config-comments"]
33
38
  deps = []
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.37.0",
4
+ "version": "5.38.0",
5
5
  "description": "Runs your agent's work and keeps its code good, on one setup for every repo.",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -24,9 +24,10 @@
24
24
  "access": "public"
25
25
  },
26
26
  "scripts": {
27
- "format": "prettier --write --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && shfmt --write --indent 2 scripts/ tooling/ claude/",
28
- "check:format": "prettier --check --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && shfmt --diff --indent 2 scripts/ tooling/ claude/",
27
+ "format": "oxfmt . && prettier --write --log-level warn 'web/**/*.astro' && shfmt --write --indent 2 scripts/ tooling/ claude/",
28
+ "check:format": "oxfmt --check . && prettier --check --log-level warn 'web/**/*.astro' && shfmt --diff --indent 2 scripts/ tooling/ claude/",
29
29
  "check:shell": "find scripts tooling claude .claude/hooks -name '*.sh' -exec shellcheck --severity=warning {} + && find .husky -maxdepth 1 -type f -exec shellcheck --severity=warning --shell=sh {} +",
30
+ "check:lint": "oxlint --disable-nested-config --max-warnings 0",
30
31
  "check:types": "tsc --noEmit",
31
32
  "check:install": "./scripts/core/install-check.sh",
32
33
  "test": "bun --bun vitest run",
@@ -72,6 +73,8 @@
72
73
  "happy-dom": "^20.14.5",
73
74
  "husky": "^9.1.7",
74
75
  "micromatch": "^4.0.8",
76
+ "oxfmt": "0.72.0",
77
+ "oxlint": "1.87.0",
75
78
  "prettier": "^3.8.1",
76
79
  "prettier-plugin-astro": "^0.14.1",
77
80
  "prettier-plugin-tailwindcss": "^0.7.2",
@@ -101,7 +101,7 @@ log_step "Assert scaffold"
101
101
  # skills rather than installing into a target. The two
102
102
  # rule paths stand for the `claude` and `canon` groups `base` takes whole.
103
103
  for path in "CLAUDE.md" ".claude/wiki/index.md" ".claude" "canon/context/index.md" \
104
- ".prettierrc" ".lintstagedrc" ".husky/pre-commit" ".github/workflows/verify.yml" "scripts/verify.ts" \
104
+ ".oxfmtrc.json" ".lintstagedrc" ".husky/pre-commit" ".github/workflows/verify.yml" "scripts/verify.ts" \
105
105
  ".claude/rules/canon/claude/output.md" ".claude/rules/canon/canon/scratch.md"; do
106
106
  if [ ! -e "$TARGET_DIR/$path" ]; then
107
107
  log_error "Missing after canon init: $path"
@@ -70,7 +70,6 @@ import {
70
70
  writeSettings,
71
71
  } from '@/claude/settings'
72
72
  import { copyPreservingMode } from '@/sync/copy'
73
- import { execScript } from '@/cli/exec'
74
73
  import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
75
74
  import { recordDir } from '@/roots/record'
76
75
  import { SURFACE_ENTRIES, surfaceDir } from '@/roots/surface'
@@ -1891,7 +1891,7 @@ async function mapPool<T, R>(
1891
1891
  limit: number,
1892
1892
  task: (input: T) => Promise<R>,
1893
1893
  ): Promise<R[]> {
1894
- const results: R[] = new Array(inputs.length)
1894
+ const results: R[] = Array.from({ length: inputs.length })
1895
1895
  let next = 0
1896
1896
  const lane = async (): Promise<void> => {
1897
1897
  while (next < inputs.length) {
@@ -17,7 +17,6 @@ import {
17
17
  type FolderSize,
18
18
  formatBytes,
19
19
  GROWTH_WINDOWS,
20
- sizedFolders,
21
20
  type SizeOutcome,
22
21
  sizeRecords,
23
22
  } from '@/records/size'
package/src/docs/read.ts CHANGED
@@ -149,29 +149,23 @@ export function walkEntries(root: string, dir: string): WalkedEntry[] {
149
149
  const cwd = join(root, dir)
150
150
  if (!existsSync(cwd)) return []
151
151
 
152
- const files = markdownNames(cwd).map(
153
- (name): WalkedEntry => ({
154
- name,
155
- kind: 'file',
156
- rel: join(dir, `${name}.md`),
157
- }),
158
- )
152
+ const files = markdownNames(cwd).map((name): WalkedEntry => ({
153
+ name,
154
+ kind: 'file',
155
+ rel: join(dir, `${name}.md`),
156
+ }))
159
157
  const folders = catalogedFolders(cwd)
160
- const split = folders.map(
161
- (folder): WalkedEntry => ({
162
- name: folder,
163
- kind: 'folder',
164
- rel: join(dir, folder, `${INDEX_TOPIC}.md`),
165
- }),
166
- )
158
+ const split = folders.map((folder): WalkedEntry => ({
159
+ name: folder,
160
+ kind: 'folder',
161
+ rel: join(dir, folder, `${INDEX_TOPIC}.md`),
162
+ }))
167
163
  const leaves = folders.flatMap((folder) =>
168
- markdownNames(join(cwd, folder)).map(
169
- (name): WalkedEntry => ({
170
- name,
171
- kind: 'leaf',
172
- rel: join(dir, folder, `${name}.md`),
173
- }),
174
- ),
164
+ markdownNames(join(cwd, folder)).map((name): WalkedEntry => ({
165
+ name,
166
+ kind: 'leaf',
167
+ rel: join(dir, folder, `${name}.md`),
168
+ })),
175
169
  )
176
170
 
177
171
  return [...files, ...split, ...leaves]
@@ -544,6 +544,20 @@ export const STAGES: readonly Stage[] = [
544
544
  ],
545
545
  success: 'Shell check passed',
546
546
  },
547
+ {
548
+ id: 'lint',
549
+ label: 'Lint',
550
+ // Unscoped, since a full run costs less than spawning the process and a
551
+ // scope is one more pattern to fall behind the extensions oxlint reads.
552
+ checks: [
553
+ {
554
+ kind: 'command',
555
+ argv: ['bun', 'run', 'check:lint'],
556
+ failure: 'Lint failed',
557
+ },
558
+ ],
559
+ success: 'Lint passed',
560
+ },
547
561
  {
548
562
  id: 'types',
549
563
  label: 'Types',
@@ -86,7 +86,7 @@ const BLOCKQUOTE = /^\s*>/
86
86
  * follows, and the price is that a span opening a fragment mid-paragraph reads
87
87
  * as a sentence start.
88
88
  */
89
- const SENTENCE_END = /[.!?]["'’”)\]]*(?=\s+(?:["'“(\[]*[A-Z]|`)|\s*$)/g
89
+ const SENTENCE_END = /[.!?]["'’”)\]]*(?=\s+(?:["'“([]*[A-Z]|`)|\s*$)/g
90
90
 
91
91
  /**
92
92
  * Checkpoints the structural measures run against.
@@ -674,7 +674,7 @@ async function pool<T, R>(
674
674
  size: number,
675
675
  work: (item: T, index: number) => Promise<R>,
676
676
  ): Promise<R[]> {
677
- const results: R[] = new Array(items.length)
677
+ const results: R[] = Array.from({ length: items.length })
678
678
  let next = 0
679
679
 
680
680
  async function lane(): Promise<void> {
@@ -827,14 +827,12 @@ export async function runEquivalence(
827
827
  const records: ArmRecord[] = [
828
828
  ...offline,
829
829
  ...anchored,
830
- ...skipped.map(
831
- (arm): ArmRecord => ({
832
- arm: armKey(arm),
833
- state: 'skipped-anchor',
834
- differs: [],
835
- masked: [],
836
- }),
837
- ),
830
+ ...skipped.map((arm): ArmRecord => ({
831
+ arm: armKey(arm),
832
+ state: 'skipped-anchor',
833
+ differs: [],
834
+ masked: [],
835
+ })),
838
836
  ].sort((x, y) => x.arm.localeCompare(y.arm))
839
837
 
840
838
  const counts: Record<ArmState, number> = {