@erclx/canon 5.53.0 → 5.55.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 (127) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +10 -9
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/canon-cli/SKILL.md +5 -5
  5. package/claude/skills/create-skill/SKILL.md +1 -1
  6. package/claude/skills/design-identity/REQUIREMENT.md +2 -2
  7. package/claude/skills/design-identity/SKILL.md +6 -6
  8. package/claude/skills/design-record/REQUIREMENT.md +2 -2
  9. package/claude/skills/design-record/SKILL.md +4 -4
  10. package/claude/skills/design-record/references/folder.md +1 -1
  11. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  12. package/claude/skills/document-health/SKILL.md +6 -6
  13. package/claude/skills/document-repair/SKILL.md +5 -5
  14. package/claude/skills/draw-figure/REQUIREMENT.md +1 -1
  15. package/claude/skills/draw-figure/SKILL.md +1 -1
  16. package/claude/skills/pick-by-eye/REQUIREMENT.md +4 -4
  17. package/claude/skills/pick-by-eye/SKILL.md +34 -41
  18. package/claude/skills/pick-by-eye/references/board.md +42 -0
  19. package/claude/skills/pick-by-eye/references/live-arms.md +3 -3
  20. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  21. package/claude/skills/target-check/SKILL.md +6 -8
  22. package/claude/skills/target-check/references/domains.md +1 -4
  23. package/claude/skills/target-setup/SKILL.md +8 -8
  24. package/claude/skills/target-update/SKILL.md +5 -5
  25. package/claude/skills/teach-craft/REQUIREMENT.md +1 -1
  26. package/claude/skills/teach-craft/SKILL.md +32 -1
  27. package/claude/skills/teach-craft/references/adopted.md +113 -0
  28. package/claude/skills/teach-craft/references/lesson-craft.md +10 -2
  29. package/claude/skills/teach-craft/references/pedagogy.md +29 -2
  30. package/docs/agents/install-and-sync.md +2 -10
  31. package/docs/agents/output-shape.md +11 -10
  32. package/docs/target/migrations.md +8 -3
  33. package/docs/target/projects.md +1 -1
  34. package/docs/target/sync.md +1 -1
  35. package/docs/workflow/skill-map.md +0 -1
  36. package/docs/workflow/visual-design-workflow.md +1 -4
  37. package/governance/detect.toml +1 -6
  38. package/governance/renames.toml +0 -1
  39. package/package.json +2 -5
  40. package/scripts/core/install-check.sh +1 -1
  41. package/scripts/lib/ui.sh +12 -65
  42. package/src/audits/run.ts +1 -1
  43. package/src/claude/cases/misc.ts +0 -5
  44. package/src/claude/routing.ts +1 -1
  45. package/src/claude/skills-audit.ts +1 -0
  46. package/src/cli/help.ts +0 -3
  47. package/src/cli/run.ts +5 -6
  48. package/src/cli/ui.ts +87 -28
  49. package/src/cli.ts +6 -2
  50. package/src/commands/claude.ts +2 -3
  51. package/src/commands/design.ts +20 -324
  52. package/src/commands/gate.ts +5 -3
  53. package/src/commands/gov.ts +11 -21
  54. package/src/commands/labels.ts +1 -1
  55. package/src/commands/pr.ts +1 -1
  56. package/src/commands/sync.ts +5 -6
  57. package/src/commands/tooling.ts +5 -8
  58. package/src/commands/transcripts.ts +6 -11
  59. package/src/design/regen.ts +5 -32
  60. package/src/design/render.ts +1 -1
  61. package/src/design/tokens.ts +3 -2
  62. package/src/gate/sequencer.ts +1 -1
  63. package/src/gate/stages.ts +4 -20
  64. package/src/gov/audit.ts +1 -6
  65. package/src/hooks/post-merge.ts +2 -5
  66. package/src/markdown/scope.ts +2 -4
  67. package/src/process/harness.ts +3 -4
  68. package/src/sandbox/equivalence.ts +0 -1
  69. package/src/sandbox/provision.ts +8 -13
  70. package/src/sandbox/scenario.ts +6 -12
  71. package/src/shipped/references.ts +1 -2
  72. package/src/sync/check.ts +4 -29
  73. package/src/sync/engine.ts +3 -8
  74. package/src/sync/stamp.ts +6 -6
  75. package/src/sync/target.ts +1 -4
  76. package/src/sync/workflow.ts +3 -5
  77. package/src/tooling/verify-stack.ts +1 -1
  78. package/tooling/base/configs/.husky/post-merge +6 -10
  79. package/tooling/claude/reference.md +1 -1
  80. package/tooling/claude/seeds/canon/DESIGN.md +1 -1
  81. package/claude/skills/canvas-board/REQUIREMENT.md +0 -35
  82. package/claude/skills/canvas-board/SKILL.md +0 -143
  83. package/claude/skills/canvas-board/references/canvas.md +0 -170
  84. package/governance/rules/ui/canvas.md +0 -13
  85. package/scripts/core/check-color-source.sh +0 -51
  86. package/src/canvas/address.ts +0 -227
  87. package/src/canvas/arrange.ts +0 -66
  88. package/src/canvas/capture.ts +0 -391
  89. package/src/canvas/client/app.tsx +0 -188
  90. package/src/canvas/client/inspector/align-grid.tsx +0 -162
  91. package/src/canvas/client/inspector/color-field.tsx +0 -398
  92. package/src/canvas/client/inspector/color-inputs.tsx +0 -139
  93. package/src/canvas/client/inspector/color-picker.tsx +0 -668
  94. package/src/canvas/client/inspector/color-space.ts +0 -240
  95. package/src/canvas/client/inspector/field.tsx +0 -263
  96. package/src/canvas/client/inspector/fields.css +0 -763
  97. package/src/canvas/client/inspector/section.tsx +0 -27
  98. package/src/canvas/client/inspector/size-field.tsx +0 -69
  99. package/src/canvas/client/inspector/token-select.tsx +0 -109
  100. package/src/canvas/client/inspector/values.ts +0 -297
  101. package/src/canvas/client/inspector.tsx +0 -692
  102. package/src/canvas/client/keys.ts +0 -71
  103. package/src/canvas/client/layers.tsx +0 -118
  104. package/src/canvas/client/main.tsx +0 -71
  105. package/src/canvas/client/pages-panel.tsx +0 -212
  106. package/src/canvas/client/panel-handle.tsx +0 -90
  107. package/src/canvas/client/selection.tsx +0 -199
  108. package/src/canvas/client/shell.css +0 -762
  109. package/src/canvas/client/state.ts +0 -792
  110. package/src/canvas/client/surface.tsx +0 -803
  111. package/src/canvas/client/theme-panel.tsx +0 -55
  112. package/src/canvas/client/tools.tsx +0 -153
  113. package/src/canvas/content.ts +0 -781
  114. package/src/canvas/edit.ts +0 -575
  115. package/src/canvas/editing.ts +0 -203
  116. package/src/canvas/fit.ts +0 -238
  117. package/src/canvas/global.d.ts +0 -15
  118. package/src/canvas/history.ts +0 -116
  119. package/src/canvas/index.html +0 -20
  120. package/src/canvas/server.ts +0 -766
  121. package/src/canvas/tokens.ts +0 -221
  122. package/src/commands/canvas.ts +0 -1097
  123. package/src/design/adapter.ts +0 -59
  124. package/src/design/base.css +0 -203
  125. package/src/design/brand-favicon.ts +0 -18
  126. package/src/design/neutral.ts +0 -127
  127. package/src/design/project-fonts.ts +0 -392
@@ -1,42 +1,20 @@
1
1
  import { mkdirSync, writeFileSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
- import { buildDesignCss } from '@/design/css'
4
3
  import { renderDesignDocument } from '@/design/document'
5
- import { NEUTRAL_TOKENS } from '@/design/neutral'
6
4
 
7
5
  /**
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`.
11
- *
12
- * The document is what a person reads and the stylesheet is what a target
13
- * installs, and neither is edited by hand. That is the cost the operator took
14
- * on when the tokens moved into a module: two artifacts where the record used
15
- * to be one, and a regen step that has to run or both go stale silently. The
16
- * gate stage is what keeps silently out of it.
6
+ * The record the token module renders, written by `canon design regen` and
7
+ * asserted for drift by the `design` stage of `bun run check`. It is never
8
+ * edited by hand. That is the cost the operator took on when the tokens moved
9
+ * into a module: a regen step that has to run or the record goes stale
10
+ * silently. The gate stage is what keeps silently out of it.
17
11
  */
18
12
 
19
13
  /** The rendered record, at the path `claude/skills/design-record/references/design.md` fixes for it. */
20
14
  export const DESIGN_DOCUMENT = join('canon', 'DESIGN.md')
21
15
 
22
- /**
23
- * The base stylesheet a target installs. It sits inside `src/` because that is
24
- * the folder the published package ships, and beside the module that writes it
25
- * because a generated file kept away from its generator is one nobody thinks to
26
- * regenerate.
27
- */
28
- export const DESIGN_BASE_CSS = join('src', 'design', 'base.css')
29
-
30
- const BASE_BANNER = [
31
- 'Generated by `canon design regen` from src/design/neutral.ts. Do not edit.',
32
- 'This is the base half of a design install. A target overrides a value in',
33
- '.claude/design/project/, which the three-way merge leaves alone, so this',
34
- 'file changes freely and nothing a project wrote lives in it.',
35
- ].join('\n ')
36
-
37
16
  export interface RegenResult {
38
17
  readonly documentPath: string
39
- readonly cssPath: string
40
18
  }
41
19
 
42
20
  function write(root: string, rel: string, body: string): string {
@@ -49,10 +27,5 @@ function write(root: string, rel: string, body: string): string {
49
27
  export function regenDesign(root: string): RegenResult {
50
28
  return {
51
29
  documentPath: write(root, DESIGN_DOCUMENT, renderDesignDocument()),
52
- cssPath: write(
53
- root,
54
- DESIGN_BASE_CSS,
55
- buildDesignCss(NEUTRAL_TOKENS, { banner: BASE_BANNER }),
56
- ),
57
30
  }
58
31
  }
@@ -259,7 +259,7 @@ ${previewChrome()}
259
259
  </head>
260
260
  <body>
261
261
  <h1>Design tokens</h1>
262
- <p class="note">Generated from <code>canon/DESIGN.md</code> by <code>canon design render</code>. Token preview only, not a screen mock.</p>${summary}
262
+ <p class="note">Generated from <code>canon/DESIGN.md</code> by <code>bun run web:previews</code>. Token preview only, not a screen mock.</p>${summary}
263
263
  ${sections.join('\n')}
264
264
  </body>
265
265
  </html>
@@ -84,8 +84,9 @@ const MONO = 'Noto Sans Mono, DejaVu Sans Mono, monospace'
84
84
  * The proportional stack every role but `code` declares. Geist is the face the
85
85
  * visual-direction track set throughout, embedded through `FONT_FACES`.
86
86
  *
87
- * The stack stops at two names and a generic because `src/design/base.css` is
88
- * written by `canon design regen` and formatted by prettier, and a declaration
87
+ * The stack stops at two names and a generic because
88
+ * `web/src/styles/tokens.css` is written by `bun run web:tokens` and formatted
89
+ * by oxfmt, and a declaration
89
90
  * past 80 columns is wrapped by the second and flattened by the first. Every
90
91
  * emitted line stays inside the width so the two writers never disagree.
91
92
  */
@@ -525,7 +525,7 @@ async function spawn(
525
525
  cwd,
526
526
  reject: false,
527
527
  all: true,
528
- env: { ...gitEnv(), CANON_NON_INTERACTIVE: '1', ...env },
528
+ env: { ...gitEnv(), ...env },
529
529
  extendEnv: false,
530
530
  })
531
531
 
@@ -119,12 +119,6 @@ const TESTS_SCOPE = new RegExp(
119
119
  ].join('|'),
120
120
  )
121
121
 
122
- const script = (name: string, failure: string): Check => ({
123
- kind: 'command',
124
- argv: ['bash', `scripts/core/${name}`],
125
- failure,
126
- })
127
-
128
122
  /**
129
123
  * Every stage the gate runs, in the order it runs them.
130
124
  *
@@ -212,10 +206,10 @@ export const STAGES: readonly Stage[] = [
212
206
  },
213
207
  {
214
208
  // `canon/DESIGN.md`, the web stylesheet, and the tab icon are written
215
- // from `src/design/tokens.ts`, the base stylesheet from
216
- // `src/design/neutral.ts`, and none is edited by hand. Four artifacts
217
- // 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
218
- // when it did not, which is this. The record and the worked example are
209
+ // from `src/design/tokens.ts`, and none is edited by hand. Three
210
+ // artifacts from one token source is the cost of the token move, and a
211
+ // render step that has to run is only safe while something fails when it
212
+ // did not, which is this. The record and the worked example are
219
213
  // then counted against the prose budget, which the drift asserts cannot
220
214
  // see since a regenerated paragraph agrees with its source.
221
215
  id: 'design',
@@ -232,12 +226,6 @@ export const STAGES: readonly Stage[] = [
232
226
  failure:
233
227
  'The design record drifted from the token source. Run bun run check and commit canon/DESIGN.md.',
234
228
  },
235
- {
236
- kind: 'drift',
237
- pathspec: 'src/design/base.css',
238
- failure:
239
- 'The base stylesheet drifted from the token source. Run bun run check and commit src/design/base.css.',
240
- },
241
229
  {
242
230
  kind: 'command',
243
231
  argv: ['bun', 'run', 'web:tokens'],
@@ -536,10 +524,6 @@ export const STAGES: readonly Stage[] = [
536
524
  argv: ['bun', 'run', 'check:shell'],
537
525
  failure: 'Shell check failed',
538
526
  },
539
- script(
540
- 'check-color-source.sh',
541
- 'A color escape is defined outside scripts/lib/ui.sh.',
542
- ),
543
527
  ],
544
528
  success: 'Shell check passed',
545
529
  },
package/src/gov/audit.ts CHANGED
@@ -45,13 +45,8 @@ export const RULE_GATED_FOLDERS: readonly string[] = [
45
45
  /**
46
46
  * Rule files in a gated folder whose findings `--gate` still passes, one path
47
47
  * each and never a folder, so a new rule beside one is gated the day it lands.
48
- *
49
- * `ui/canvas.md` carries a shape finding and no ledger, and v149.2 removes the
50
- * rule, so this entry goes with it.
51
48
  */
52
- export const RULE_GATE_EXEMPT: readonly string[] = [
53
- join('governance', 'rules', 'ui', 'canvas.md'),
54
- ]
49
+ export const RULE_GATE_EXEMPT: readonly string[] = []
55
50
 
56
51
  /** The folders whose rules claim outside practice, so each carries a ledger. */
57
52
  export const PRACTICE_FOLDERS: readonly string[] = [
@@ -359,11 +359,8 @@ export async function runPostMerge(input: PostMergeInput): Promise<void> {
359
359
  }
360
360
 
361
361
  if (!isSkipped('CANON_SKIP_UPGRADE')) {
362
- await step(
363
- input,
364
- at([...STEP_VERBS.upgrade, '--json'], { CANON_NON_INTERACTIVE: '1' }),
365
- (result, fields) =>
366
- moveLines(result, fields, '⬆️ ', 'CLI not upgraded', []),
362
+ await step(input, at([...STEP_VERBS.upgrade, '--json']), (result, fields) =>
363
+ moveLines(result, fields, '⬆️ ', 'CLI not upgraded', []),
367
364
  )
368
365
  }
369
366
 
@@ -136,9 +136,6 @@ export const GATED_PATHS: readonly string[] = [
136
136
  'claude/skills/canon-feedback/REQUIREMENT.md',
137
137
  'claude/skills/canon-feedback/SKILL.md',
138
138
  'claude/skills/canon-feedback/references/template.md',
139
- 'claude/skills/canvas-board/REQUIREMENT.md',
140
- 'claude/skills/canvas-board/SKILL.md',
141
- 'claude/skills/canvas-board/references/canvas.md',
142
139
  'claude/skills/ci-workflow/REQUIREMENT.md',
143
140
  'claude/skills/ci-workflow/SKILL.md',
144
141
  'claude/skills/ci-workflow/references/workflows.md',
@@ -203,6 +200,7 @@ export const GATED_PATHS: readonly string[] = [
203
200
  'claude/skills/document-repair/references/batches.md',
204
201
  'claude/skills/pick-by-eye/REQUIREMENT.md',
205
202
  'claude/skills/pick-by-eye/SKILL.md',
203
+ 'claude/skills/pick-by-eye/references/board.md',
206
204
  'claude/skills/pick-by-eye/references/live-arms.md',
207
205
  'claude/skills/write-doc/REQUIREMENT.md',
208
206
  'claude/skills/write-doc/SKILL.md',
@@ -375,6 +373,7 @@ export const GATED_PATHS: readonly string[] = [
375
373
  'claude/skills/task-complete/references/unusable-baseline.md',
376
374
  'claude/skills/teach-craft/REQUIREMENT.md',
377
375
  'claude/skills/teach-craft/SKILL.md',
376
+ 'claude/skills/teach-craft/references/adopted.md',
378
377
  'claude/skills/teach-craft/references/lesson-craft.md',
379
378
  'claude/skills/teach-craft/references/pedagogy.md',
380
379
  'claude/skills/test-craft/REQUIREMENT.md',
@@ -399,7 +398,6 @@ export const GATED_PATHS: readonly string[] = [
399
398
  'claude/skills/write-human/references/machine-tells.md',
400
399
  'claude/skills/youtube-transcripts/REQUIREMENT.md',
401
400
  'claude/skills/youtube-transcripts/SKILL.md',
402
- '.claude/skills/internal-canvas/REQUIREMENT.md',
403
401
  '.claude/skills/internal-claude/REQUIREMENT.md',
404
402
  '.claude/skills/internal-docs/REQUIREMENT.md',
405
403
  '.claude/skills/internal-governance/REQUIREMENT.md',
@@ -90,8 +90,7 @@ export function snapshotStateDir(): string {
90
90
  *
91
91
  * A git hook exports `GIT_DIR`, which would resolve a fixture's git-aware
92
92
  * reads against this checkout instead of the temporary directory a case
93
- * builds, so every spawn drops the `GIT_` prefix before adding the headless
94
- * flag every case needs to avoid a picker blocking on stdin.
93
+ * builds, so every spawn drops the `GIT_` prefix.
95
94
  *
96
95
  * `CANON_STATE_DIR` and `CANON_SANDBOX_DIR` get the same treatment as `GIT_DIR`,
97
96
  * each pointed at a folder under the case's own `cwd` rather than dropped,
@@ -99,7 +98,8 @@ export function snapshotStateDir(): string {
99
98
  * `HOME` to this machine's real `~/.local/state/canon`. `stateDir()` and
100
99
  * `sandboxTree()` resolve the same three ways and share that parent, so both
101
100
  * overrides move together. A case explicitly passing its own value through
102
- * `options.env` still wins, matching `CANON_NON_INTERACTIVE` below.
101
+ * `options.env` still wins. The child's stdin is a pipe rather than a
102
+ * terminal, so every case runs headless.
103
103
  *
104
104
  * The `stateDir()` snapshot before and after the spawn is what actually
105
105
  * catches an escape past that redirection, since a default can be wrong in a
@@ -125,7 +125,6 @@ export function runCli(
125
125
  timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
126
126
  env: {
127
127
  ...inherited,
128
- CANON_NON_INTERACTIVE: '1',
129
128
  CANON_STATE_DIR: join(options.cwd, '.canon-state'),
130
129
  CANON_SANDBOX_DIR: join(options.cwd, '.canon-state', 'sandbox'),
131
130
  ...options.env,
@@ -517,7 +517,6 @@ function provisionEnv(
517
517
  ): NodeJS.ProcessEnv {
518
518
  const env: NodeJS.ProcessEnv = {
519
519
  ...process.env,
520
- CANON_NON_INTERACTIVE: '1',
521
520
  CANON_SANDBOX_DIR: dir,
522
521
  CANON_SANDBOX_RUN_ID: PINNED_RUN_ID,
523
522
  CLAUDE_CONFIG_DIR: configDirFor(dir),
@@ -27,7 +27,7 @@ import {
27
27
  logStep,
28
28
  logWarn,
29
29
  outro,
30
- palette,
30
+ outroSuccess,
31
31
  select,
32
32
  } from '@/cli/ui'
33
33
 
@@ -99,9 +99,7 @@ function readGit(cwd: string, args: string[]): string | undefined {
99
99
  }
100
100
 
101
101
  function finish(message: string): void {
102
- const { GREEN, NC } = palette(process.stderr)
103
- outro()
104
- process.stderr.write(`\n${GREEN}${message}${NC}\n`)
102
+ outroSuccess(message)
105
103
  }
106
104
 
107
105
  function stageAnchorTree(
@@ -210,7 +208,7 @@ function install(
210
208
  env: NodeJS.ProcessEnv,
211
209
  ): void {
212
210
  const result = spawnSync('bun', [join(root, 'src', 'cli.ts'), ...args], {
213
- env: { ...env, CANON_NON_INTERACTIVE: '1' },
211
+ env,
214
212
  encoding: 'utf8',
215
213
  stdio: ['ignore', 'ignore', 'pipe'],
216
214
  })
@@ -432,10 +430,7 @@ async function provisionScenario(
432
430
  baseEnv: NodeJS.ProcessEnv,
433
431
  ): Promise<void> {
434
432
  const env: NodeJS.ProcessEnv = { ...baseEnv }
435
- if (scenario.arm !== undefined) {
436
- env.SANDBOX_SCENARIO = scenario.arm
437
- env.CANON_NON_INTERACTIVE = '1'
438
- }
433
+ if (scenario.arm !== undefined) env.SANDBOX_SCENARIO = scenario.arm
439
434
 
440
435
  const sandboxDir = join(root, 'sandbox')
441
436
  const declared = declareScenario(root, scenario, env)
@@ -475,14 +470,14 @@ async function provisionScenario(
475
470
  must(commitScenarioChanges(sandbox, isSkipAutoCommit, env))
476
471
  tagBaseline(sandbox, env)
477
472
 
478
- finish('✓ Sandbox Ready')
473
+ finish('Sandbox Ready')
479
474
  }
480
475
 
481
476
  function cleanSandbox(sandbox: string): void {
482
477
  logStep('Removing sandbox')
483
478
  rmSync(sandbox, { recursive: true, force: true })
484
479
  logRemove(sandbox)
485
- finish('✓ Sandbox clean')
480
+ finish('Sandbox clean')
486
481
  }
487
482
 
488
483
  function isAtBaseline(sandbox: string): boolean {
@@ -520,7 +515,7 @@ async function resetSandbox(sandbox: string): Promise<void> {
520
515
 
521
516
  if (isAtBaseline(sandbox)) {
522
517
  logInfo('At baseline')
523
- finish('✓ Sandbox at baseline')
518
+ finish('Sandbox at baseline')
524
519
  return
525
520
  }
526
521
 
@@ -556,7 +551,7 @@ async function resetSandbox(sandbox: string): Promise<void> {
556
551
  must(run('git', ['checkout-index', '-a', '-f'], quiet))
557
552
  }
558
553
  logInfo('Sandbox reset to baseline')
559
- finish('✓ Sandbox reset complete')
554
+ finish('Sandbox reset complete')
560
555
  }
561
556
 
562
557
  /**
@@ -13,6 +13,7 @@ import {
13
13
  import { createRequire } from 'node:module'
14
14
  import { dirname, join, relative } from 'node:path'
15
15
  import {
16
+ isNonInteractive,
16
17
  logError,
17
18
  logInfo,
18
19
  logStep,
@@ -580,8 +581,9 @@ export function createStageContext(options: ContextOptions): StageContext {
580
581
  }
581
582
 
582
583
  /**
583
- * Picks the arm: a routed name first,
584
- * echoed on the frame, then the first arm for a headless caller, then a prompt.
584
+ * Picks the arm: a routed name first, echoed on the frame, then a prompt. A
585
+ * headless caller naming no arm is refused rather than handed the first one,
586
+ * since a verdict on an arm nobody chose reads as a verdict on the scenario.
585
587
  */
586
588
  async function routeArm(
587
589
  names: readonly string[],
@@ -600,17 +602,9 @@ async function routeArm(
600
602
  const routed = env.SANDBOX_SCENARIO ?? ''
601
603
  if (routed !== '') return echo(routed)
602
604
 
603
- // Read off the scenario's environment rather than this process's, as
604
- // `is_non_interactive` read the shell it ran in.
605
- const isHeadless =
606
- env.CANON_NON_INTERACTIVE === '1' ||
607
- // canon-keep-retired
608
- env.AITK_NON_INTERACTIVE === '1'
609
- if (isHeadless) return echo(names[0] ?? DEFAULT_ARM_NAME)
610
-
611
- if (!process.stdin.isTTY) {
605
+ if (isNonInteractive()) {
612
606
  logError(
613
- `${prompt} requires a TTY. Pass an argument or set CANON_NON_INTERACTIVE=1.`,
607
+ `${prompt} requires a TTY. Pass the arm as the scenario argument, one of: ${names.join(', ')}.`,
614
608
  )
615
609
  throw new ScenarioStop(1, 'exited')
616
610
  }
@@ -15,8 +15,7 @@ import { isMarked } from '@/sweep/exempt-marker'
15
15
  * number resolves.
16
16
  *
17
17
  * That is also what keeps the check a prose pattern rather than a parser.
18
- * `src/design/base.css` and `src/design/tokens.ts` carry sixteen values shaped
19
- * `#191512`, which no width or boundary rule separates from a pull request
18
+ * `src/design/tokens.ts` carries twenty values shaped `#191512`, which no width or boundary rule separates from a pull request
20
19
  * number, so telling them apart would need the line's syntactic position.
21
20
  */
22
21
  export const SHIPPED_CORPORA = [
package/src/sync/check.ts CHANGED
@@ -2,7 +2,6 @@ import { existsSync } from 'node:fs'
2
2
  import { basename, join, sep } from 'node:path'
3
3
  import { execa } from 'execa'
4
4
  import { gitEnv } from '@/git/env'
5
- import { createDesignAdapter, DESIGN_INSTALL_DIR } from '@/design/adapter'
6
5
  import { createGovAdapter } from '@/gov/adapter'
7
6
  import { successorResolver } from '@/gov/renames'
8
7
  import { lookupRules } from '@/gov/install'
@@ -46,17 +45,11 @@ import { readSkew, type SkewReport } from '@/version/skew'
46
45
  * being one of these, because `src/tooling/` never calls `planSync`, so the
47
46
  * three lookups below have no entry to offer it. Standards and snippets left
48
47
  * the list with their install channels: nothing writes either corpus into a
49
- * target, so there is no installed copy to attribute.
50
- *
51
- * Design joins as a scanned domain rather than a stamp-only one because its
52
- * base file is attributed the same way a rule is, and its install marker is
53
- * what keeps it off a target that never asked for it: a project with no
54
- * `.claude/design/` is never scanned, so design values arrive on an install
55
- * rather than on the next sync.
48
+ * target, so there is no installed copy to attribute. Design left with its
49
+ * install verb for the same reason.
56
50
  */
57
51
  export const SCANNED_DOMAINS = [
58
52
  'governance',
59
- 'design',
60
53
  ] as const satisfies readonly StampDomain[]
61
54
 
62
55
  export type ScannedDomain = (typeof SCANNED_DOMAINS)[number]
@@ -68,17 +61,14 @@ export type ScannedDomain = (typeof SCANNED_DOMAINS)[number]
68
61
  */
69
62
  const SYNCED_SOURCES: Record<ScannedDomain, string> = {
70
63
  governance: 'governance/rules/',
71
- design: 'src/design/',
72
64
  }
73
65
 
74
66
  const ADAPTERS: Record<ScannedDomain, (root: string) => SyncAdapter> = {
75
67
  governance: createGovAdapter,
76
- design: createDesignAdapter,
77
68
  }
78
69
 
79
70
  const INSTALL_MARKERS: Record<ScannedDomain, readonly string[]> = {
80
71
  governance: ['.claude', 'rules', 'canon'],
81
- design: DESIGN_INSTALL_DIR.split(sep),
82
72
  }
83
73
 
84
74
  export interface StateCounts {
@@ -215,33 +205,18 @@ export interface CheckReport {
215
205
  readonly skew: SkewReport
216
206
  }
217
207
 
218
- /**
219
- * Scanned domains a target takes deliberately rather than by being managed.
220
- *
221
- * An absent one is a choice, so it is never reported as unstamped. Governance
222
- * wants the opposite reading, since a managed target without it has yet to
223
- * install what every project is expected to carry, and naming it is the only
224
- * place that shows up.
225
- */
226
- export const OPT_IN_DOMAINS: readonly ScannedDomain[] = ['design']
227
-
228
208
  /**
229
209
  * Scanned domains this target should have stamped and has not, which is the
230
210
  * one line a domain nobody installed ever appears on.
231
211
  *
232
212
  * An unmigrated domain is excluded because the relocation is its remedy rather
233
- * than a sync, and an opt-in domain the target does not hold is excluded
234
- * because there is nothing there to stamp and the sync it would name refuses.
213
+ * than a sync.
235
214
  */
236
215
  export function uncoveredDomains(report: CheckReport): ScannedDomain[] {
237
216
  const unmigrated = new Set(report.unmigrated.map((entry) => entry.domain))
238
- const installed = new Set(report.domains.map((entry) => entry.domain))
239
217
 
240
218
  return SCANNED_DOMAINS.filter(
241
- (domain) =>
242
- !report.covers.includes(domain) &&
243
- !unmigrated.has(domain) &&
244
- (!OPT_IN_DOMAINS.includes(domain) || installed.has(domain)),
219
+ (domain) => !report.covers.includes(domain) && !unmigrated.has(domain),
245
220
  )
246
221
  }
247
222
 
@@ -26,7 +26,7 @@ import {
26
26
  logStep,
27
27
  logWarn,
28
28
  outro,
29
- palette,
29
+ outroSuccess,
30
30
  select,
31
31
  } from '@/cli/ui'
32
32
  import { compareVersions, parseVersion } from '@/version/compare'
@@ -436,7 +436,6 @@ export async function runDomainSync(
436
436
  }
437
437
 
438
438
  const plan = planSync(adapter, resolved)
439
- const { GREEN, GREY, NC } = palette(process.stderr)
440
439
 
441
440
  if (
442
441
  !existsSync(adapter.installedRoot(resolved)) &&
@@ -453,8 +452,7 @@ export async function runDomainSync(
453
452
  if (count === 0) {
454
453
  await adapter.onComplete?.(resolved)
455
454
  await recordStamp(adapter, resolved, new Date())
456
- outro()
457
- process.stderr.write(`${GREEN}✓ Everything up to date${NC}\n`)
455
+ outroSuccess('Everything up to date')
458
456
  return 0
459
457
  }
460
458
 
@@ -489,10 +487,7 @@ export async function runDomainSync(
489
487
  await adapter.onComplete?.(resolved)
490
488
  await recordStamp(adapter, resolved, new Date())
491
489
 
492
- outro()
493
- process.stderr.write(
494
- `${GREEN}✓ Sync complete${NC} ${GREY}(${count} ${adapter.unit})${NC}\n`,
495
- )
490
+ outroSuccess('Sync complete', `(${count} ${adapter.unit})`)
496
491
  return 0
497
492
  }
498
493
 
package/src/sync/stamp.ts CHANGED
@@ -10,17 +10,17 @@ import { compareVersions, parseVersion } from '@/version/compare'
10
10
  import { readInstalled } from '@/version/installed'
11
11
 
12
12
  /**
13
- * Domains the stamp can record. Governance and design both attribute file by
14
- * file through the sync engine. Tooling runs its own inject and manifest
15
- * machinery, so it records the stack chain it resolved instead and carries no
16
- * file hashes.
13
+ * Domains the stamp can record. Governance attributes file by file through
14
+ * the sync engine. Tooling runs its own inject and manifest machinery, so it
15
+ * records the stack chain it resolved instead and carries no file hashes.
17
16
  *
18
17
  * A stamp written before the standards or snippets install channel closed
19
- * still carries a `standards` or `snippets` record. `isStamp` ignores the key
18
+ * still carries a `standards` or `snippets` record, and one written before the
19
+ * design install retired carries a `design` record. `isStamp` ignores the key
20
20
  * and `sortDomains` drops it on the next write, so the target loses a domain
21
21
  * nothing can refresh rather than losing the whole file.
22
22
  */
23
- export const STAMP_DOMAINS = ['governance', 'design', 'tooling'] as const
23
+ export const STAMP_DOMAINS = ['governance', 'tooling'] as const
24
24
 
25
25
  export type StampDomain = (typeof STAMP_DOMAINS)[number]
26
26
 
@@ -1,9 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
- import { DESIGN_INSTALL_DIR } from '@/design/adapter'
4
3
  import { isDirectory } from '@/targets/validate'
5
4
 
6
- export const SYNC_DOMAINS = ['governance', 'design', 'claude'] as const
5
+ export const SYNC_DOMAINS = ['governance', 'claude'] as const
7
6
 
8
7
  export type SyncDomain = (typeof SYNC_DOMAINS)[number]
9
8
 
@@ -14,7 +13,6 @@ export interface DomainState {
14
13
 
15
14
  const DOMAIN_MARKERS: Record<SyncDomain, string> = {
16
15
  governance: join('.claude', 'rules'),
17
- design: DESIGN_INSTALL_DIR,
18
16
  claude: '.claude',
19
17
  }
20
18
 
@@ -26,7 +24,6 @@ const DOMAIN_MARKERS: Record<SyncDomain, string> = {
26
24
  */
27
25
  const DOMAIN_PATHS: Record<SyncDomain, readonly string[]> = {
28
26
  governance: ['.claude/rules/', '.claude/GOV.md'],
29
- design: ['.claude/design/'],
30
27
  claude: ['.gitignore'],
31
28
  }
32
29
 
@@ -19,7 +19,7 @@ import {
19
19
  logStep,
20
20
  logWarn,
21
21
  outro,
22
- palette,
22
+ outroSuccess,
23
23
  pipeOutput,
24
24
  select,
25
25
  } from '@/cli/ui'
@@ -75,7 +75,7 @@ export async function collectChanges(git: GitRunner): Promise<DomainChange[]> {
75
75
  *
76
76
  * This is the only toolkit code that writes to a git remote, which is why a
77
77
  * headless run refuses it outright. `select_option` resolved to its first
78
- * option under `CANON_NON_INTERACTIVE=1`, and the first option here was
78
+ * option with no terminal to answer it, and the first option here was
79
79
  * `Commit and open PR` whenever `gh` was installed, so an agent following the
80
80
  * documented non-interactive path pushed a branch and opened a pull request on
81
81
  * someone's repository without being asked.
@@ -193,8 +193,6 @@ function promptChoice(canOpenPullRequest: boolean): Promise<WorkflowChoice> {
193
193
  }
194
194
 
195
195
  function succeed(message: string): number {
196
- const { GREEN, NC } = palette(process.stderr)
197
- outro()
198
- process.stderr.write(`\n${GREEN}✓ ${message}${NC}\n`)
196
+ outroSuccess(message)
199
197
  return 0
200
198
  }
@@ -124,7 +124,7 @@ async function run({
124
124
  await phase(state, 'Sync', () =>
125
125
  execa('bun', [cli, 'tooling', 'sync', stack, '.', '--write'], {
126
126
  ...captured(state.dir),
127
- env: { CI: '1', CANON_NON_INTERACTIVE: '1' },
127
+ env: { CI: '1' },
128
128
  }),
129
129
  )
130
130
 
@@ -27,20 +27,16 @@ if ! command -v canon >/dev/null 2>&1; then
27
27
  exit 0
28
28
  fi
29
29
 
30
- # A global binary older than the verb cannot run it, and the verb carries the
31
- # only step that would update it, so reinstall here and probe again. The probe
32
- # reads the verb's own usage line rather than the exit, since an older binary
33
- # answers an unknown command's `--help` with its root help and exits zero.
30
+ # A global binary older than the verb cannot run it, and its owner upgrades it
31
+ # by hand. The probe reads the verb's own usage line rather than the exit,
32
+ # since an older binary answers an unknown command's `--help` with its root
33
+ # help and exits zero.
34
34
  has_verb() {
35
35
  canon hooks post-merge --help 2>/dev/null | grep -q '^Usage: canon hooks post-merge'
36
36
  }
37
37
 
38
- if ! has_verb; then
39
- if [ -z "$CANON_SKIP_UPGRADE" ]; then
40
- CANON_NON_INTERACTIVE=1 canon upgrade >/dev/null 2>&1 || true
41
- fi
42
- fi
43
-
44
38
  if has_verb; then
45
39
  canon hooks post-merge --root "$root" || true
40
+ else
41
+ echo 'canon is too old to run the post-merge hook. Run `canon upgrade`.' >&2
46
42
  fi
@@ -16,7 +16,7 @@ The claude stack installs the Claude Code workflow into a project through `canon
16
16
 
17
17
  - `.canon/tasks/` holds one file per task plus a generated `index.md`, local scratch only, with `archive/` holding the tasks that shipped
18
18
  - `.canon/plans/` holds execution detail for multi-step tasks, and `canon tasks archive` moves a `feature-*.md` plan into `archive/` when the `post-merge` hook archives its task
19
- - `.canon/review/` holds one subfolder per producer, with `design/` for `canon design render`
19
+ - `.canon/review/` holds the reports a review or measurement skill writes, such as `ux-measure-<slug>.md`
20
20
  - `.canon/tmp/` is ephemeral scratch, safe to delete without loss
21
21
  - `.claude/worktrees/` is where `EnterWorktree` creates a linked worktree, which is why it stays under `.claude/`
22
22
 
@@ -1,6 +1,6 @@
1
1
  # Design
2
2
 
3
- Authoring guidance: the design standard. Values live in `<your stylesheet>`, such as `src/styles/tokens.css` or a Tailwind `@theme` block. A project with none writes them under `.claude/design/project/`, beside the installed base stylesheet.
3
+ Authoring guidance: the design standard. Values live in `<your stylesheet>`, such as `src/styles/tokens.css` or a Tailwind `@theme` block. A project with none writes a stylesheet of its own and names it here.
4
4
 
5
5
  ## Overview
6
6