@erclx/canon 4.118.0 → 4.119.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 (59) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/canon-rollout/SKILL.md +2 -2
  3. package/claude/skills/design-extract/SKILL.md +1 -1
  4. package/claude/skills/docs-fold/SKILL.md +2 -2
  5. package/claude/skills/draft-ready/REQUIREMENT.md +1 -1
  6. package/claude/skills/draft-ready/SKILL.md +1 -1
  7. package/claude/skills/draft-screencast/SKILL.md +1 -1
  8. package/claude/skills/git-pr/SKILL.md +5 -3
  9. package/claude/skills/memory-capture/SKILL.md +3 -3
  10. package/claude/skills/memory-review/SKILL.md +3 -3
  11. package/claude/skills/plan-feature/SKILL.md +1 -1
  12. package/claude/skills/review-branch/SKILL.md +1 -1
  13. package/claude/skills/review-pr/SKILL.md +1 -1
  14. package/claude/skills/role-planner/SKILL.md +1 -1
  15. package/claude/skills/role-worker/REQUIREMENT.md +1 -1
  16. package/claude/skills/role-worker/SKILL.md +10 -41
  17. package/claude/skills/seed-sync/SKILL.md +1 -1
  18. package/claude/skills/session-compact/SKILL.md +1 -1
  19. package/claude/skills/session-map/SKILL.md +1 -1
  20. package/claude/skills/session-relay/REQUIREMENT.md +7 -1
  21. package/claude/skills/session-relay/SKILL.md +4 -7
  22. package/claude/skills/session-worktree/REQUIREMENT.md +5 -1
  23. package/claude/skills/session-worktree/SKILL.md +7 -1
  24. package/claude/skills/session-worktree/references/main-root-writes.md +24 -0
  25. package/claude/skills/task-board/SKILL.md +4 -3
  26. package/claude/skills/teach-workspace/SKILL.md +4 -4
  27. package/claude/skills/ui-checklist/SKILL.md +1 -1
  28. package/claude/skills/ux-audit/SKILL.md +1 -1
  29. package/claude/skills/ux-measure/SKILL.md +1 -1
  30. package/claude/skills/ux-walkthrough/references/relay.md +1 -1
  31. package/docs/agents/commands.md +2 -2
  32. package/docs/agents/install-and-sync.md +11 -6
  33. package/docs/agents/tasks.md +4 -4
  34. package/docs/target-projects.md +1 -1
  35. package/docs/workflow/ai-workflow.md +9 -9
  36. package/docs/workflow/visual-design-workflow.md +1 -1
  37. package/governance/renames.toml +13 -0
  38. package/governance/rules/core/085-worktrees.md +2 -8
  39. package/package.json +1 -1
  40. package/src/claude/cases/workflow.ts +10 -0
  41. package/src/commands/sync.ts +11 -3
  42. package/src/commands/tasks.ts +9 -8
  43. package/src/design/base.css +19 -19
  44. package/src/design/board.ts +18 -7
  45. package/src/design/neutral.ts +127 -0
  46. package/src/design/regen.ts +6 -4
  47. package/src/design/tokens.ts +5 -2
  48. package/src/gate/stages.ts +4 -4
  49. package/src/gov/adapter.ts +51 -1
  50. package/src/gov/install.ts +13 -3
  51. package/src/gov/renames.ts +76 -0
  52. package/src/gov/stacks.ts +5 -1
  53. package/src/sync/check.ts +19 -4
  54. package/src/sync/engine.ts +67 -3
  55. package/src/tasks/archive.ts +28 -7
  56. package/src/tasks/record.ts +16 -9
  57. package/src/tasks/validate.ts +3 -1
  58. package/standards/tasks.md +5 -3
  59. package/governance/rules/core/091-channel.md +0 -13
@@ -4,7 +4,7 @@ Read when the operator calls a batch, and at the start of the walkthrough when a
4
4
 
5
5
  ## Where the batch goes
6
6
 
7
- - Send it to the controller where the session was dispatched by one, resolved per the Channel rule, matched by name through `canon gov list --rules --json` rather than a path a target may not have installed.
7
+ - Send it to the controller where the session was dispatched by one, through `canon:session-relay`, which resolves the address and carries the send.
8
8
  - Give it to the operator as one list in the conversation where no controller exists. The walkthrough file is the handoff either way.
9
9
 
10
10
  ## When
@@ -23,10 +23,10 @@ Full help: `canon <command> --help`. Bare `canon --help` lists every top-level c
23
23
  | `canon indexes list [path]` | Flatten every folder index under a path into one `{path, title, description}` catalog (`--json`) |
24
24
  | `canon docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
25
25
  | `canon design render` | Render `canon/DESIGN.md` tokens to HTML and CSS, declaring each vendored face the typography table names with `--embed-fonts` |
26
- | `canon design regen` | Rewrite this repository's `canon/DESIGN.md` and `src/design/base.css` from `src/design/tokens.ts` |
26
+ | `canon design regen` | Rewrite this repository's `canon/DESIGN.md` from `src/design/tokens.ts` and `src/design/base.css` from `src/design/neutral.ts` |
27
27
  | `canon design board` | Generate a static page set indexing tokens, surfaces, wireframes, past candidates, components, and references against a caller-resolved `--root` (`--out`), defaulting to the main worktree |
28
28
  | `canon design css` | Emit the design tokens and components as CSS on stdout (`--no-components` for properties alone) |
29
- | `canon design install` | Install the base stylesheet into a project at `.claude/design/base.css` |
29
+ | `canon design install` | Install the neutral base stylesheet into a project at `.claude/design/base.css` |
30
30
  | `canon design sync` | Update a base stylesheet already installed under `.claude/design/` |
31
31
  | `canon slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck, reporting any unrecognized layout name on stderr |
32
32
  | `canon slides list` | List the available slide layouts (`--json` for the catalog) |
@@ -19,10 +19,13 @@ removes a stale `.claude/GOV.md` from the retired build. Use
19
19
 
20
20
  Everything under `.claude/rules/canon/` belongs to the toolkit, so a rule
21
21
  there that the toolkit no longer ships is deleted on sync, edited or not. That
22
- is how a retired or renamed rule stops loading in a target. The sync lists each
23
- deletion by path before it applies, and the target's git diff is the record
24
- afterwards. `canon sync --check` lists the same file as `retired` before any
25
- sync runs. A project's own rules belong under `.claude/rules/project/`, where
22
+ is how a retired rule stops loading in a target. A renamed or renumbered rule
23
+ the toolkit declares in `governance/renames.toml` moves instead: the sync
24
+ installs the rule under its new name and deletes the old file, whether or not
25
+ the target's stamp records a stack. Local edits to the old file are not
26
+ carried, and the sync says so. The sync lists each change by path before it
27
+ applies, and the target's git diff is the record afterwards. `canon sync
28
+ --check` lists the same file as `retired` or `renamed` before any sync runs. A project's own rules belong under `.claude/rules/project/`, where
26
29
  `canon standards rule` reserves `900-999` for them, and a rule placed under
27
30
  `canon/` instead is lost on the next sync.
28
31
 
@@ -164,9 +167,11 @@ matches what the toolkit installed, `customized` when the project edited it,
164
167
  `stranded` when it sits at a path the toolkit no longer installs to, `orphaned`
165
168
  when the project authored it, or `drifted` when no stamp covers it. Governance
166
169
  also reports `missing`, for a rule the target's recorded stack lists that its
167
- tree does not hold at all, and `retired`, for a rule under
170
+ tree does not hold at all, `retired`, for a rule under
168
171
  `.claude/rules/canon/` the toolkit no longer ships, which the next sync
169
- deletes. A `retired` file counts toward `--exit-code`, since one sync clears it.
172
+ deletes, and `renamed`, for one the toolkit ships under a new name, which the
173
+ next sync moves. A `retired` or `renamed` file counts toward `--exit-code`,
174
+ since one sync clears it.
170
175
 
171
176
  Use `--json` for the machine-readable report and `--exit-code` to fail a CI job. Orphaned
172
177
  and missing files are both excluded from that exit code: a project-authored
@@ -50,7 +50,7 @@ canon tasks archive --pull-request 673 --json
50
50
  | `--json` | Emit a machine-readable record on stdout |
51
51
  | `--root <path>` | Board root, defaulting to the main worktree |
52
52
 
53
- Exit codes: `0` archived, `1` refused. Every gate is a refusal rather than a warning, because `.husky/post-merge` calls this with nobody watching. The `reason` field carries which gate fired: `no-board`, `no-match`, `ambiguous`, `no-outcomes`, `open-outcomes`, or `bad-input`.
53
+ Exit codes: `0` archived, `1` refused. Every gate is a refusal rather than a warning, because `.husky/post-merge` calls this with nobody watching. The `reason` field carries which gate fired: `no-board`, `no-match`, `ambiguous`, `no-outcomes`, `open-outcomes`, `earlier-slice`, or `bad-input`. `earlier-slice` fires when `--pull-request` names a number that is not the last on the task's `Pull request:` line, since outcomes are ticked at ship time and only the last slice's merge closes the task.
54
54
 
55
55
  An outcome whose body is struck reads as cut rather than open or closed, whatever its checkbox holds: `- ~~<outcome>~~ <why>`. A task carrying only cut outcomes archives, since the gate refuses `no-outcomes` only when both the closed and the cut counts are zero. The success record carries `closed` and `cut` as counts, so a reader tells a shipped task from an abandoned one without opening the file.
56
56
 
@@ -245,13 +245,13 @@ canon tasks plan-link v28.1-trigger-escalation .canon/plans/feature-dispatch-ans
245
245
 
246
246
  The plan resolves the same way `canon tasks plan-answers` resolves one, against the project root first and `.canon/tasks/` second, so a bare slug and a board-relative path both work. A reference resolving to no file refuses as `no-plan`, naming every base it looked under.
247
247
 
248
- The write mirrors `canon tasks pull-request`'s add/correct/unchanged shape, anchored on the H1 rather than on the last origin line, since `Plan:` is the first origin line a task carries rather than the last. The `action` field reports `added`, `corrected`, or `unchanged`, which makes a rerun against the same plan safe.
248
+ The write adds the line when it is absent and corrects the target in place when it exists, anchored on the H1 rather than on the last origin line, since `Plan:` is the first origin line a task carries rather than the last. The `action` field reports `added`, `corrected`, or `unchanged`, which makes a rerun against the same plan safe.
249
249
 
250
250
  Exit codes: `0` recorded, `1` refused. The `reason` field carries `no-board`, `no-match`, `no-plan`, or `bad-input`.
251
251
 
252
252
  ## Pull request
253
253
 
254
- `canon tasks pull-request` records the number a branch's pull request carries onto the task that branch closes. It adds `Pull request: #NNN` under the `Plan:`, `Groundwork:`, `Intake:`, or `Issue:` lines the task already holds, and corrects the number in place when the line exists.
254
+ `canon tasks pull-request` records the number a branch's pull request carries onto the task that branch closes. It adds `Pull request: #NNN` under the `Plan:`, `Groundwork:`, `Intake:`, or `Issue:` lines the task already holds. When the line exists it appends `, #NNN` rather than replacing it, so a task shipped in slices lists every pull request oldest first, and a number already listed leaves the line alone. A line that does not read as a comma-separated list of `#NNN` entries is replaced whole.
255
255
 
256
256
  Name the task by its filename stem, or by the plan its `Plan:` line points at:
257
257
 
@@ -266,7 +266,7 @@ canon tasks pull-request 673 --plan worktree-scratch-routing --json
266
266
  | `--json` | Emit a machine-readable record on stdout |
267
267
  | `--root <path>` | Board root, defaulting to the main worktree |
268
268
 
269
- A plan is matched on the token both spellings share, so `worktree-scratch-routing`, `feature-worktree-scratch-routing`, and `.canon/plans/feature-worktree-scratch-routing.md` all select the same task. The `action` field reports `added`, `corrected`, or `unchanged`, which makes a rerun against the same number safe.
269
+ A plan is matched on the token both spellings share, so `worktree-scratch-routing`, `feature-worktree-scratch-routing`, and `.canon/plans/feature-worktree-scratch-routing.md` all select the same task. The `action` field reports `added`, `appended`, `corrected`, or `unchanged`, which makes a rerun against the same number safe.
270
270
 
271
271
  Exit codes: `0` recorded, `1` refused. The `reason` field carries `no-board`, `no-match`, or `ambiguous`. `git-pr` skips silently on those three, because each is a case where a guessed write would archive the wrong task once the branch merges.
272
272
 
@@ -225,7 +225,7 @@ The report opens by naming the binary running it. The installed version reads ag
225
225
 
226
226
  #### Then the causes
227
227
 
228
- A `stale` file still matches what the toolkit installed, so the update is mechanical. A `customized` file carries local edits, so taking the upstream version is a decision and `canon:seed-sync` is the tool for it. A `stranded` file sits where an older toolkit installed it and the toolkit has since moved, which is a relocation the report names but no command runs. A `retired` rule is one under `.claude/rules/canon/` the toolkit no longer ships, and the next `canon gov sync` deletes it.
228
+ A `stale` file still matches what the toolkit installed, so the update is mechanical. A `customized` file carries local edits, so taking the upstream version is a decision and `canon:seed-sync` is the tool for it. A `stranded` file sits where an older toolkit installed it and the toolkit has since moved, which is a relocation the report names but no command runs. A `retired` rule is one under `.claude/rules/canon/` the toolkit no longer ships, and the next `canon gov sync` deletes it. A `renamed` rule is one the toolkit now ships under a new name, and the next sync installs the new name and deletes the old file.
229
229
 
230
230
  That attribution comes from `canon/config/config.json`, which every install and sync now writes, falling back to `.claude/canon/config.json` for a target stamped before that move. A target stamped before that path shipped is read from the retired `.claude/canon.json` instead, reported rather than migrated. Governance records a hash per installed file, plus the stack `canon gov install` was given, and tooling records the stack chain it resolved instead of any file hash, since its install runs no per-file walk to attribute.
231
231
 
@@ -224,15 +224,15 @@ This section is the corpus the coverage claim is measured against: every name `c
224
224
 
225
225
  ### Build the feature
226
226
 
227
- | Skill | When to use |
228
- | ---------------------------- | ------------------------------------------------------------------------------- |
229
- | `canon:session-worktree` | At the plan-to-execute boundary, to get an isolated tree and branch |
230
- | `canon:auto-ship` | After plan approval, to chain implement, verify, review, draft PR |
231
- | `canon:project-commands` | When the project's own command needs running |
232
- | `canon:test-first` | Before implementing a planned change, to run its test red, green, then refactor |
233
- | `canon:test-craft` | When writing or changing any test, to pick its layer and filter what it asserts |
234
- | `canon:systematic-debugging` | When a test fails or a bug surfaces, to force root cause first |
235
- | `canon:ui-checklist` | After a UI change, to write what to look at and name what ships untested |
227
+ | Skill | When to use |
228
+ | ---------------------------- | ---------------------------------------------------------------------------------------------------------- |
229
+ | `canon:session-worktree` | At the plan-to-execute boundary, to get an isolated tree and branch, or to route a refused main-root write |
230
+ | `canon:auto-ship` | After plan approval, to chain implement, verify, review, draft PR |
231
+ | `canon:project-commands` | When the project's own command needs running |
232
+ | `canon:test-first` | Before implementing a planned change, to run its test red, green, then refactor |
233
+ | `canon:test-craft` | When writing or changing any test, to pick its layer and filter what it asserts |
234
+ | `canon:systematic-debugging` | When a test fails or a bug surfaces, to force root cause first |
235
+ | `canon:ui-checklist` | After a UI change, to write what to look at and name what ships untested |
236
236
 
237
237
  ### Check the work before it leaves the branch
238
238
 
@@ -24,7 +24,7 @@ The toolkit seed in `tooling/claude/seeds/canon/DESIGN.md` ships a token-table t
24
24
 
25
25
  The `canon:design-extract` skill drafts the file, sourcing tokens from a project's existing prose and CLI UI surfaces, or proposing them from `canon/REQUIREMENTS.md` and a `## Personality` paragraph when no UI code exists yet. `canon design render` writes an HTML plus CSS preview to `.canon/tmp/render/design/` for eyeballing the current system without leaving Claude Code. See `canon/context/design.md`.
26
26
 
27
- A project wanting the toolkit's own values rather than its own runs `canon design install`, which copies one stylesheet to `.claude/design/base.css` carrying the token set as custom properties and two components built on them. That file is toolkit-owned and `canon design sync` refreshes it, so a project overrides a value in `.claude/design/project/` instead, which sync never touches. Nothing arrives without that install, and the two channels are independent: a record drafted by the extract skill is the project's own, and the installed stylesheet is the toolkit's.
27
+ A project wanting a neutral starting point to override runs `canon design install`, which copies one stylesheet to `.claude/design/base.css` carrying an achromatic token set as custom properties and two components built on them. Greys run from near-black to near-white and the accent is monochrome, so the only palette the project renders is one it writes itself. That file is toolkit-owned and `canon design sync` refreshes it, so a project overrides a value in `.claude/design/project/` instead, which sync never touches. Nothing arrives without that install, and the two channels are independent: a record drafted by the extract skill is the project's own, and the installed stylesheet is the toolkit's.
28
28
 
29
29
  A cell no source anchors ends in `? verify`, and the preview shows that marker beside the value rather than folding it in, so a swatch and a font sample stay built from the value alone. A confidence line above the sections names how many cells are anchored against how many are tagged, which is what tells a reader whether they are looking at a record of the code or a proposal about it. It reads the columns a source could anchor and leaves out the row names, so the ratio is not diluted by cells no tag could ever reach. The proposal path tags nearly all of them, so that count reads low on day one by design.
30
30
 
@@ -0,0 +1,13 @@
1
+ # Rule renames a sync carries a target across. Each key under [renamed] is an
2
+ # old rule basename and its value the basename the same rule ships under now,
3
+ # so `canon gov sync` replaces a held old file with the rule under its new name
4
+ # rather than only deleting it.
5
+ #
6
+ # An entry declares one-to-one identity and nothing else. A fold of several
7
+ # rules into one, or a split of one into several, is a retirement plus an
8
+ # addition and takes no entry here. Add the entry in the same change as the
9
+ # rename or renumber, and never prune one: a target can sit any number of
10
+ # releases behind, and a chain A to B to C resolves A only while both links
11
+ # stay. A key must name no live rule and every chain must end on one, which
12
+ # `src/gov/renames.test.ts` checks against this file.
13
+ [renamed]
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Route tracked-file writes and shared session scratch correctly from a linked worktree
2
+ description: Enter a linked worktree for implementation, resolve shared session scratch at the main root, and route a refused main-root write through session-worktree
3
3
  ---
4
4
 
5
5
  # Worktrees standards
@@ -11,10 +11,4 @@ description: Route tracked-file writes and shared session scratch correctly from
11
11
  ## Shared session scratch
12
12
 
13
13
  - Shared session scratch (`.canon/plans/`, `.canon/review/`, `.canon/memory/`, `.canon/tasks/`) lives at the main worktree root, not inside a linked worktree. From a linked worktree, resolve these paths against the main root via `git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`. Fall back to `pwd` if not a git repo.
14
- - `.canon/tmp/` is mixed rather than main-root by default: it stays worktree-local per `055-scratch.md`, and a skill that resolves a write there at the main root instead states so in its own body.
15
- - From a linked worktree, every `Edit` or `Write` to a tracked file (source, docs) must use a path starting with `pwd`.
16
- - From a linked worktree, `Edit` and `Write` are refused for every main-root path, session scratch included. The refusal names session isolation and points at the worktree copy, which is a second gitignored file no later session reads, so never take that redirect.
17
- - `Read` resolves against the main root normally from a linked worktree. A main-root write reaches it only through `Bash`, as one plain command rather than a compound one, which is refused for complexity.
18
- - Route a main-root write by what it does to the file. Creating a whole file goes out as one plain `Bash` command carrying a heredoc. Changing a line inside a file that already exists goes through a command that resolves the main root in-process, because the shell route for that case is the stream editor this file bans.
19
- - A background session that entered no worktree at all meets the identical refusal writing to the main root, since the guard gates on isolation rather than on sitting inside a linked worktree. A planner writing its plan file from no worktree takes the same heredoc route for that reason.
20
- - The heredoc route costs nothing on `.canon/plans/` and `.canon/review/`, which no `PostToolUse` hook watches, and costs the index on `.canon/tasks/` and `.canon/memory/`, since a hook matching `Write|Edit|MultiEdit` never fires on `Bash`. Regenerate positionally: in-process where a verb such as `canon tasks archive` already calls it, elsewhere by an explicit `canon indexes regen` call.
14
+ - From a linked worktree, or from a background session that entered none, `Edit` and `Write` refuse a main-root path and redirect to a worktree copy no later session reads. Never take that redirect. Route the write the way the `session-worktree` skill states, and report it rather than proceeding silently when that skill does not resolve. It ships with the plugin and this rule ships with the CLI, so a project that installed governance alone does not have it.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.118.0",
4
+ "version": "4.119.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -163,11 +163,21 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
163
163
  prompt: 'Get me set up in a fresh Claude Code worktree for this branch.',
164
164
  expect: 'session-worktree',
165
165
  },
166
+ {
167
+ prompt:
168
+ 'My Write to the plan file at the main root got refused for session isolation. How do I land it there?',
169
+ expect: 'session-worktree',
170
+ },
166
171
  {
167
172
  prompt:
168
173
  'Relay this over to the session that dispatched me. It gave me a sessionId rather than a name, so work out who that is and send it.',
169
174
  expect: 'session-relay',
170
175
  },
176
+ {
177
+ prompt:
178
+ 'I am stuck on a question only the operator can answer. Before I stop and ask it, tell the session that dispatched me, it gave me a sessionId.',
179
+ expect: 'session-relay',
180
+ },
171
181
 
172
182
  // Negatives: pairs whose bodies already carry an explicit Do NOT boundary.
173
183
  {
@@ -133,9 +133,17 @@ function renderCheck(report: CheckReport): void {
133
133
  logWarn(entry.notice ?? `${entry.rel} (${entry.state})`)
134
134
  }
135
135
 
136
- const { stale, customized, drifted, stranded, retired, orphaned, missing } =
137
- domain.counts
138
- if (stale + customized + drifted + stranded + retired === 0) {
136
+ const {
137
+ stale,
138
+ customized,
139
+ drifted,
140
+ stranded,
141
+ retired,
142
+ renamed,
143
+ orphaned,
144
+ missing,
145
+ } = domain.counts
146
+ if (stale + customized + drifted + stranded + retired + renamed === 0) {
139
147
  logInfo(orphaned === 0 ? 'up to date' : `up to date (${orphaned} local)`)
140
148
  }
141
149
  // A local file is not a deficiency and a missing rule is, so this never
@@ -409,12 +409,14 @@ export function register(program: Command): void {
409
409
  [
410
410
  '',
411
411
  'Exit codes:',
412
- ' 0 the line was added, corrected, or already correct',
412
+ ' 0 the line was added, appended to, corrected, or already correct',
413
413
  ' 1 refused, with the reason on stderr or in the JSON record',
414
414
  '',
415
415
  'It adds Pull request: #NNN under the origin lines the task carries,',
416
- 'and corrects the number in place when the line exists. The board is',
417
- 'shared scratch, so a linked worktree records against the same board.',
416
+ 'and appends the number to the line when one exists, so a task shipped',
417
+ 'in slices lists every pull request. A line that does not read as a',
418
+ 'list is replaced. The board is shared scratch, so a linked worktree',
419
+ 'records against the same board.',
418
420
  '',
419
421
  'Examples:',
420
422
  ' canon tasks pull-request 673 v28.1-trigger-escalation',
@@ -448,11 +450,10 @@ export function register(program: Command): void {
448
450
  ' 0 the line was added, corrected, or already correct',
449
451
  ' 1 refused, with the reason on stderr or in the JSON record',
450
452
  '',
451
- 'It writes Plan: [<label>](<target>) right after the H1, the same',
452
- 'add/correct/unchanged shape canon tasks pull-request writes with, and',
453
- 'resolves both a bare slug and a board-relative path the way',
454
- 'canon tasks plan-answers does. Safe from a linked worktree, since it',
455
- 'resolves the board root in-process.',
453
+ 'It writes Plan: [<label>](<target>) right after the H1, corrects the',
454
+ 'target in place when the line exists, and resolves both a bare slug',
455
+ 'and a board-relative path the way canon tasks plan-answers does. Safe',
456
+ 'from a linked worktree, since it resolves the board root in-process.',
456
457
  '',
457
458
  'Examples:',
458
459
  ' canon tasks plan-link v28.1-trigger-escalation dispatch-answer-gate',
@@ -1,29 +1,29 @@
1
- /* Generated by `canon design regen` from src/design/tokens.ts. Do not edit.
1
+ /* Generated by `canon design regen` from src/design/neutral.ts. Do not edit.
2
2
  This is the base half of a design install. A target overrides a value in
3
3
  .claude/design/project/, which the three-way merge leaves alone, so this
4
4
  file changes freely and nothing a project wrote lives in it. */
5
5
 
6
6
  :root {
7
- --color-background: #0f0e0c;
8
- --color-surface: #151412;
9
- --color-chrome: #1b1a18;
10
- --color-border: #2a2926;
11
- --color-text: #d9d7d4;
12
- --color-text-body: #aeada9;
13
- --color-text-secondary: #8c8b86;
14
- --color-muted: #7f7f7c;
15
- --color-accent: #c76b5f;
7
+ --color-background: #0e0e0e;
8
+ --color-surface: #141414;
9
+ --color-chrome: #1a1a1a;
10
+ --color-border: #292929;
11
+ --color-text: #dddddd;
12
+ --color-text-body: #b2b2b2;
13
+ --color-text-secondary: #8e8e8e;
14
+ --color-muted: #7f7f7f;
15
+ --color-accent: #ececec;
16
16
  --color-success: #61c454;
17
- --color-light-background: #fbfaf8;
18
- --color-light-surface: #f1f1ee;
19
- --color-light-chrome: #e9e8e5;
20
- --color-light-text: #2c2c29;
21
- --color-light-text-body: #4b4947;
22
- --color-light-text-secondary: #666561;
23
- --color-light-muted: #6e6d6c;
24
- --color-light-accent: #ad4a4b;
17
+ --color-light-background: #fafafa;
18
+ --color-light-surface: #f1f1f1;
19
+ --color-light-chrome: #e8e8e8;
20
+ --color-light-text: #252525;
21
+ --color-light-text-body: #444444;
22
+ --color-light-text-secondary: #5f5f5f;
23
+ --color-light-muted: #6c6c6c;
24
+ --color-light-accent: #191919;
25
25
  --color-light-success: #2d6b22;
26
- --color-light-border: #d5d4d1;
26
+ --color-light-border: #d4d4d4;
27
27
  --space-xs: 0.25rem;
28
28
  --space-sm: 0.5rem;
29
29
  --space-md: 0.875rem;
@@ -10,6 +10,7 @@ import {
10
10
  import { join, relative, sep } from 'node:path'
11
11
  import { DESIGN_DOCUMENT } from '@/design/regen'
12
12
  import { renderDesignDoc } from '@/design/render'
13
+ import { NEUTRAL_TOKENS } from '@/design/neutral'
13
14
  import { colorValue } from '@/design/tokens'
14
15
  import { parseFrontmatter, readField } from '@/indexes/frontmatter'
15
16
  import { recordDir } from '@/record-root'
@@ -131,6 +132,16 @@ const SCROLLSPY_SCRIPT = [
131
132
  '})();</script>',
132
133
  ].join('')
133
134
 
135
+ /**
136
+ * A role's toolkit value, falling back to the neutral install base's where the
137
+ * toolkit record drops the role, so the board never restates a hex of its own.
138
+ */
139
+ function roleColor(role: string): string {
140
+ const value = colorValue(role) ?? colorValue(role, NEUTRAL_TOKENS)
141
+ if (value === undefined) throw new Error(`No design token declares ${role}`)
142
+ return value
143
+ }
144
+
134
145
  /**
135
146
  * The same brand mark `src/design/render.ts` embeds in its own preview,
136
147
  * colored with the dark accent since the board's default chrome is dark
@@ -139,7 +150,7 @@ const SCROLLSPY_SCRIPT = [
139
150
  * copies rather than a shared icon helper for the first second caller.
140
151
  */
141
152
  function faviconLink(): string {
142
- const color = colorValue('accent') ?? '#e0724b'
153
+ const color = roleColor('accent')
143
154
  const href = `data:image/svg+xml,${encodeURIComponent(
144
155
  `<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80"><path d="M34,20 L15,28 L15,72 L34,80 Z M66,20 L85,28 L85,72 L66,80 Z" fill="${color}" /><rect x="44" y="15" width="12" height="70" rx="2" fill="${color}" /></svg>`,
145
156
  )}`
@@ -232,12 +243,12 @@ ${SCROLLSPY_SCRIPT}
232
243
  }
233
244
 
234
245
  function panelPage(title: string, body: string): string {
235
- const accent = colorValue('light-accent') ?? '#a4471c'
236
- const surface = colorValue('light-surface') ?? '#f4efe6'
237
- const border = colorValue('light-border') ?? '#e4dcd0'
238
- const muted = colorValue('light-muted') ?? '#726b62'
239
- const text = colorValue('light-text') ?? '#1a1815'
240
- const background = colorValue('light-background') ?? '#faf7f2'
246
+ const accent = roleColor('light-accent')
247
+ const surface = roleColor('light-surface')
248
+ const border = roleColor('light-border')
249
+ const muted = roleColor('light-muted')
250
+ const text = roleColor('light-text')
251
+ const background = roleColor('light-background')
241
252
 
242
253
  return `<!doctype html>
243
254
  <html lang="en">
@@ -0,0 +1,127 @@
1
+ import type { ColorToken, DesignTokens } from '@/design/tokens'
2
+ import { TOKENS } from '@/design/tokens'
3
+
4
+ /**
5
+ * The token set `canon design install` hands a target, achromatic so the only
6
+ * palette a target renders is one it wrote under `.claude/design/project/`.
7
+ * Type, spacing, and radii are this repository's by spread, and only the color
8
+ * list is a second copy, held to the same roles by `neutral.test.ts`.
9
+ *
10
+ * Grounds, surface, chrome, and border take the OKLCH lightness of the toolkit
11
+ * role they replace at chroma 0. Text roles are solved against `surface` for
12
+ * the contrast targets `TOKENS` records (13.5, 8.6, 5.6, 4.6), and the accent
13
+ * for 15.5, past `text` so it still reads apart from body copy. The state
14
+ * roles keep the toolkit's values, since a state color is a meaning rather
15
+ * than a palette.
16
+ */
17
+
18
+ const DARK_GROUNDS = ['background', 'surface'] as const
19
+ const LIGHT_GROUNDS = ['light-background', 'light-surface'] as const
20
+
21
+ function toolkitRole(role: string): ColorToken {
22
+ const token = TOKENS.color.find((candidate) => candidate.role === role)
23
+ if (token === undefined) throw new Error(`TOKENS declares no ${role} role`)
24
+ return token
25
+ }
26
+
27
+ const NEUTRAL_COLOR: readonly ColorToken[] = [
28
+ { role: 'background', intent: 'page canvas', value: '#0e0e0e' },
29
+ {
30
+ role: 'surface',
31
+ intent: 'cards, panels, raised blocks',
32
+ value: '#141414',
33
+ },
34
+ {
35
+ role: 'chrome',
36
+ intent: 'the window titlebar, one step above the canvas',
37
+ value: '#1a1a1a',
38
+ },
39
+ { role: 'border', intent: 'every rule and panel edge', value: '#292929' },
40
+ {
41
+ role: 'text',
42
+ intent: 'headings, counts, emphasized runs',
43
+ value: '#dddddd',
44
+ grounds: DARK_GROUNDS,
45
+ },
46
+ {
47
+ role: 'text-body',
48
+ intent: 'default body copy',
49
+ value: '#b2b2b2',
50
+ grounds: DARK_GROUNDS,
51
+ },
52
+ {
53
+ role: 'text-secondary',
54
+ intent: 'labels, captions, supporting copy',
55
+ value: '#8e8e8e',
56
+ grounds: DARK_GROUNDS,
57
+ },
58
+ {
59
+ role: 'muted',
60
+ intent: 'the faintest step, trailing notes',
61
+ value: '#7f7f7f',
62
+ grounds: DARK_GROUNDS,
63
+ },
64
+ {
65
+ role: 'accent',
66
+ intent: 'install command, mark, primary action',
67
+ value: '#ececec',
68
+ grounds: DARK_GROUNDS,
69
+ },
70
+ toolkitRole('success'),
71
+ toolkitRole('warning'),
72
+ toolkitRole('error'),
73
+ {
74
+ role: 'light-background',
75
+ intent: 'page canvas on a light ground',
76
+ value: '#fafafa',
77
+ },
78
+ {
79
+ role: 'light-surface',
80
+ intent: 'cards and panels on a light ground',
81
+ value: '#f1f1f1',
82
+ },
83
+ {
84
+ role: 'light-chrome',
85
+ intent: 'the window titlebar, one step above the canvas',
86
+ value: '#e8e8e8',
87
+ },
88
+ {
89
+ role: 'light-text',
90
+ intent: 'primary text on a light ground',
91
+ value: '#252525',
92
+ grounds: LIGHT_GROUNDS,
93
+ },
94
+ {
95
+ role: 'light-text-body',
96
+ intent: 'default body copy on a light ground',
97
+ value: '#444444',
98
+ grounds: LIGHT_GROUNDS,
99
+ },
100
+ {
101
+ role: 'light-text-secondary',
102
+ intent: 'labels, captions, supporting copy on a light ground',
103
+ value: '#5f5f5f',
104
+ grounds: LIGHT_GROUNDS,
105
+ },
106
+ {
107
+ role: 'light-muted',
108
+ intent: 'secondary text on a light ground',
109
+ value: '#6c6c6c',
110
+ grounds: LIGHT_GROUNDS,
111
+ },
112
+ {
113
+ role: 'light-accent',
114
+ intent: 'links and primary action on light',
115
+ value: '#191919',
116
+ grounds: LIGHT_GROUNDS,
117
+ },
118
+ toolkitRole('light-success'),
119
+ {
120
+ role: 'light-border',
121
+ intent: 'rules and panel edges on light',
122
+ value: '#d4d4d4',
123
+ verify: true,
124
+ },
125
+ ]
126
+
127
+ export const NEUTRAL_TOKENS: DesignTokens = { ...TOKENS, color: NEUTRAL_COLOR }
@@ -2,10 +2,12 @@ import { mkdirSync, writeFileSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
3
  import { buildDesignCss } from '@/design/css'
4
4
  import { renderDesignDocument } from '@/design/document'
5
+ import { NEUTRAL_TOKENS } from '@/design/neutral'
5
6
 
6
7
  /**
7
- * The two artifacts the token source produces, written by `canon design regen`
8
- * and asserted for drift by the `design` stage of `bun run check`.
8
+ * The two artifacts the token sources produce, written by `canon design regen`
9
+ * and asserted for drift by the `design` stage of `bun run check`. The
10
+ * document renders from `TOKENS` and the stylesheet from `NEUTRAL_TOKENS`.
9
11
  *
10
12
  * The document is what a person reads and the stylesheet is what a target
11
13
  * installs, and neither is edited by hand. That is the cost the operator took
@@ -26,7 +28,7 @@ export const DESIGN_DOCUMENT = join('canon', 'DESIGN.md')
26
28
  export const DESIGN_BASE_CSS = join('src', 'design', 'base.css')
27
29
 
28
30
  const BASE_BANNER = [
29
- 'Generated by `canon design regen` from src/design/tokens.ts. Do not edit.',
31
+ 'Generated by `canon design regen` from src/design/neutral.ts. Do not edit.',
30
32
  'This is the base half of a design install. A target overrides a value in',
31
33
  '.claude/design/project/, which the three-way merge leaves alone, so this',
32
34
  'file changes freely and nothing a project wrote lives in it.',
@@ -50,7 +52,7 @@ export function regenDesign(root: string): RegenResult {
50
52
  cssPath: write(
51
53
  root,
52
54
  DESIGN_BASE_CSS,
53
- buildDesignCss(undefined, { banner: BASE_BANNER }),
55
+ buildDesignCss(NEUTRAL_TOKENS, { banner: BASE_BANNER }),
54
56
  ),
55
57
  }
56
58
  }
@@ -386,8 +386,11 @@ export const TOKENS: DesignTokens = {
386
386
  }
387
387
 
388
388
  /** A role's value, or `undefined` where the record declares no such role. */
389
- export function colorValue(role: string): string | undefined {
390
- return TOKENS.color.find((token) => token.role === role)?.value
389
+ export function colorValue(
390
+ role: string,
391
+ tokens: DesignTokens = TOKENS,
392
+ ): string | undefined {
393
+ return tokens.color.find((token) => token.role === role)?.value
391
394
  }
392
395
 
393
396
  /**
@@ -209,10 +209,10 @@ export const STAGES: readonly Stage[] = [
209
209
  success: 'Hero clean',
210
210
  },
211
211
  {
212
- // `canon/DESIGN.md`, the base stylesheet, the web stylesheet, and the
213
- // tab icon are all written from `src/design/tokens.ts` and none is edited
214
- // by hand. Four artifacts from one source is the cost of the token move,
215
- // and a render step that has to run is only safe while something fails
212
+ // `canon/DESIGN.md`, the web stylesheet, and the tab icon are written
213
+ // from `src/design/tokens.ts`, the base stylesheet from
214
+ // `src/design/neutral.ts`, and none is edited by hand. Four artifacts
215
+ // from two token sources is the cost of the token move, and a render step that has to run is only safe while something fails
216
216
  // when it did not, which is this.
217
217
  id: 'design',
218
218
  label: 'Design',