@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,90 +1,60 @@
1
- import { copyFileSync, existsSync, mkdirSync } from 'node:fs'
2
- import { basename, join, relative, resolve } from 'node:path'
1
+ import { existsSync } from 'node:fs'
2
+ import { join, relative } from 'node:path'
3
3
  import type { Command } from 'commander'
4
- import {
5
- createDesignAdapter,
6
- DESIGN_INSTALL_DIR,
7
- DESIGN_PROJECT_SUBDIR,
8
- } from '@/design/adapter'
9
4
  import { buildDesignCss } from '@/design/css'
10
5
  import { HAND_DRAWN_FONT_FACES } from '@/design/fonts'
11
- import { addProjectFace, listProjectFaces } from '@/design/project-fonts'
12
- import { renderDesignDoc } from '@/design/render'
13
- import { DESIGN_BASE_CSS, DESIGN_DOCUMENT, regenDesign } from '@/design/regen'
6
+ import { DESIGN_DOCUMENT, regenDesign } from '@/design/regen'
14
7
  import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
15
- import { creationRel, SCRATCH } from '@/roots/record'
16
- import { surfaceDir } from '@/roots/surface'
17
- import { recordStamp, runDomainSync } from '@/sync/engine'
18
- import { isDirectory, resolveTarget } from '@/targets/validate'
19
- import {
20
- intro,
21
- logAdd,
22
- logError,
23
- logInfo,
24
- logWarn,
25
- outro,
26
- palette,
27
- } from '@/cli/ui'
8
+ import { intro, logError, logInfo, logWarn, outro } from '@/cli/ui'
28
9
 
29
10
  export function register(program: Command): void {
30
11
  const design = program
31
12
  .command('design')
32
13
  .helpOption('-h, --help', 'Show this help message')
33
- .description(
34
- 'Design system commands (regen, css, render, install, sync, fonts)',
35
- )
14
+ .description('Design system commands (regen, css)')
36
15
 
37
16
  design
38
17
  .command('regen')
39
18
  .helpOption('-h, --help', 'Show this help message')
40
- .description(
41
- 'Rewrite canon/DESIGN.md and the base stylesheet from src/design/tokens.ts',
42
- )
19
+ .description('Rewrite canon/DESIGN.md from src/design/tokens.ts')
43
20
  .addHelpText(
44
21
  'after',
45
22
  [
46
23
  '',
47
- 'Unlike install and sync, this runs against the toolkit checkout rather',
48
- 'than a target. It rewrites the record and the base stylesheet from the',
49
- 'token module and refuses where the record is absent, which is what the',
50
- 'CLI installed into a project as a dependency looks like.',
24
+ 'This runs against the toolkit checkout rather than a target. It',
25
+ 'rewrites the record from the token module and refuses where the record',
26
+ 'is absent, which is what the CLI installed into a project as a',
27
+ 'dependency looks like.',
51
28
  '',
52
29
  ].join('\n'),
53
30
  )
54
31
  .action(() => {
55
- const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
56
-
57
32
  // The guard below catches a `PROJECT_ROOT` with no record at all, which
58
33
  // is an installed package. It cannot catch a second real checkout that
59
34
  // has one, so both paths carry the warning. It is a frame-interior line,
60
35
  // so each emits it after its own opener rather than ahead of the branch.
61
36
  const mismatch = checkoutMismatchWarning(process.cwd())
62
37
 
63
- // Both outputs resolve from `PROJECT_ROOT`, which is the installed
38
+ // The output resolves from `PROJECT_ROOT`, which is the installed
64
39
  // package directory when the CLI runs out of a target's `node_modules`.
65
- // The record is the one output that is already committed here and ships
66
- // with no package, so its absence is what separates the two.
40
+ // The record is already committed here and ships with no package, so
41
+ // its absence is what separates the two.
67
42
  if (!existsSync(join(PROJECT_ROOT, DESIGN_DOCUMENT))) {
68
- process.stderr.write(`${GREY}┌${NC}\n`)
43
+ intro('Regenerate design source')
69
44
  if (mismatch !== undefined) logWarn(mismatch)
70
- process.stderr.write(
71
- `${GREY}│${NC} ${RED}✗${NC} No ${DESIGN_DOCUMENT} at ${PROJECT_ROOT}. Regen runs in the toolkit checkout, not against a target.\n${GREY}└${NC}\n`,
45
+ logError(
46
+ `No ${DESIGN_DOCUMENT} at ${PROJECT_ROOT}. Regen runs in the toolkit checkout, not against a target.`,
72
47
  )
48
+ outro()
73
49
  process.exitCode = 1
74
50
  return
75
51
  }
76
52
 
77
- process.stderr.write(
78
- `${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}Regenerate design source${NC}\n`,
79
- )
53
+ intro('Regenerate design source')
80
54
  if (mismatch !== undefined) logWarn(mismatch)
81
55
  const result = regenDesign(PROJECT_ROOT)
82
- for (const path of [result.documentPath, result.cssPath]) {
83
- process.stderr.write(
84
- `${GREY}│${NC} ${GREEN}✓${NC} ${relative(PROJECT_ROOT, path)}\n`,
85
- )
86
- }
87
- process.stderr.write(`${GREY}└${NC}\n`)
56
+ logInfo(relative(PROJECT_ROOT, result.documentPath))
57
+ outro()
88
58
  })
89
59
 
90
60
  design
@@ -107,278 +77,4 @@ export function register(program: Command): void {
107
77
  }),
108
78
  )
109
79
  })
110
-
111
- design
112
- .command('render')
113
- .helpOption('-h, --help', 'Show this help message')
114
- .description('Render DESIGN.md tokens to HTML and CSS preview')
115
- .option(
116
- '-s, --source <path>',
117
- 'Source DESIGN.md path',
118
- relative(process.cwd(), surfaceDir(process.cwd(), 'DESIGN.md')),
119
- )
120
- .option(
121
- '-o, --out <path>',
122
- 'Output directory',
123
- creationRel(process.cwd(), SCRATCH, 'render', 'design'),
124
- )
125
- .option(
126
- '--embed-fonts',
127
- 'Declare each vendored face the typography table names, and set the page body in the body stack',
128
- )
129
- .action((opts: { source: string; out: string; embedFonts?: boolean }) => {
130
- const sourcePath = resolve(process.cwd(), opts.source)
131
- const outDir = resolve(process.cwd(), opts.out)
132
- const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
133
- if (!existsSync(sourcePath)) {
134
- process.stderr.write(
135
- `${GREY}┌${NC}\n${GREY}│${NC} ${RED}✗${NC} ${opts.source} not found\n${GREY}└${NC}\n`,
136
- )
137
- process.exitCode = 1
138
- return
139
- }
140
- process.stderr.write(
141
- `${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}Render design tokens${NC}\n`,
142
- )
143
- const result = renderDesignDoc(sourcePath, outDir, {
144
- embedFonts: opts.embedFonts,
145
- })
146
- process.stderr.write(
147
- `${GREY}│${NC} ${GREEN}✓${NC} ${result.htmlPath}\n${GREY}│${NC} ${GREEN}✓${NC} ${result.cssPath}\n${GREY}└${NC}\n`,
148
- )
149
- })
150
-
151
- design
152
- .command('install')
153
- .description('Install the base stylesheet into a project')
154
- .argument('[target]', 'Target directory', '.')
155
- .helpOption('-h, --help', 'Show this help message')
156
- .addHelpText(
157
- 'after',
158
- [
159
- '',
160
- 'Installs one toolkit-owned file and creates no override. A project',
161
- `overrides a value by writing ${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/`,
162
- 'itself, which sync never touches. Nothing arrives on a project that',
163
- 'has not run this, so a design value reaches a target on an install',
164
- 'rather than on the next sync. A base stylesheet already installed is',
165
- 'replaced.',
166
- '',
167
- 'Exit codes:',
168
- ' 0 the stylesheet was installed',
169
- ' 1 the target is missing or is the toolkit root, or the toolkit',
170
- ' source is absent',
171
- '',
172
- ].join('\n'),
173
- )
174
- .action(async (target: string) => {
175
- process.exitCode = await runInstall(target)
176
- })
177
-
178
- design
179
- .command('sync')
180
- .description(
181
- 'Update the base stylesheet already installed under .claude/design/',
182
- )
183
- .argument('[target]', 'Target directory', '.')
184
- .helpOption('-h, --help', 'Show this help message')
185
- .addHelpText(
186
- 'after',
187
- [
188
- '',
189
- 'Replaces the shipped design files under .claude/design/ with the',
190
- 'toolkit copy, edited or not, and leaves',
191
- `${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/ alone. It deletes nothing it finds no`,
192
- 'source for.',
193
- '',
194
- 'Exit codes:',
195
- ' 0 synced, already up to date, or the prompt was cancelled',
196
- ' 1 the target is missing or is the toolkit root',
197
- '',
198
- ].join('\n'),
199
- )
200
- .action(async (target: string) => {
201
- process.exitCode = await runDomainSync(
202
- createDesignAdapter(PROJECT_ROOT),
203
- target,
204
- { protectedRoot: PROJECT_ROOT },
205
- )
206
- })
207
-
208
- registerFonts(design)
209
- }
210
-
211
- const FONTS_DIR = join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR, 'fonts')
212
-
213
- interface FontsRefused {
214
- readonly ok: false
215
- readonly reason: string
216
- readonly detail: string
217
- }
218
-
219
- function writeJson(record: unknown): void {
220
- process.stdout.write(`${JSON.stringify(record)}\n`)
221
- }
222
-
223
- function refuseFonts(
224
- title: string,
225
- refusal: FontsRefused,
226
- emitJson: boolean,
227
- ): number {
228
- if (emitJson) writeJson(refusal)
229
- intro(title)
230
- logError(refusal.detail)
231
- outro()
232
- return 1
233
- }
234
-
235
- /** A target that is a directory, or the refusal naming the path given. */
236
- function fontsTarget(target: string): string | FontsRefused {
237
- const resolved = resolve(target)
238
- if (!isDirectory(resolved)) {
239
- return {
240
- ok: false,
241
- reason: 'no-target',
242
- detail: `Target directory not found: ${target}`,
243
- }
244
- }
245
- return resolved
246
- }
247
-
248
- function registerFonts(design: Command): void {
249
- const fonts = design
250
- .command('fonts')
251
- .helpOption('-h, --help', 'Show this help message')
252
- .description(`Read and add the project's own faces under ${FONTS_DIR}/`)
253
- .addHelpText(
254
- 'after',
255
- [
256
- '',
257
- `Faces are declared as @font-face rules in ${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/fonts.css,`,
258
- 'which the canvas embeds into every frame. Sync never writes the folder.',
259
- '',
260
- ].join('\n'),
261
- )
262
-
263
- fonts
264
- .command('list')
265
- .helpOption('-h, --help', 'Show this help message')
266
- .description('List each face the project override sheets declare')
267
- .argument('[target]', 'Target directory', '.')
268
- .option('--json', 'Emit the record on stdout')
269
- .action((target: string, opts: { json?: boolean }) => {
270
- const title = 'canon design fonts list'
271
- const resolved = fontsTarget(target)
272
- if (typeof resolved !== 'string') {
273
- process.exitCode = refuseFonts(title, resolved, Boolean(opts.json))
274
- return
275
- }
276
- const faces = listProjectFaces(resolved)
277
- if (opts.json) writeJson({ ok: true, faces })
278
- intro(title)
279
- if (faces.length === 0) logInfo(`No faces declared under ${FONTS_DIR}/`)
280
- for (const face of faces) {
281
- const line = `${face.family} ${face.weight} ${face.style}: ${face.file} in ${face.sheet}`
282
- if (face.present) logInfo(line)
283
- else logWarn(`${line} (${face.problem ?? 'missing'})`)
284
- }
285
- outro()
286
- })
287
-
288
- fonts
289
- .command('add')
290
- .helpOption('-h, --help', 'Show this help message')
291
- .description(
292
- `Copy a woff2, woff, ttf, or otf face into ${FONTS_DIR}/ and declare it`,
293
- )
294
- .argument('<file>', 'Font file to copy')
295
- .argument('[target]', 'Target directory', '.')
296
- .requiredOption('--family <name>', 'Family name the face declares')
297
- .option('--weight <n>', "Weight, or a range such as '100 900'", '400')
298
- .option('--style <style>', 'normal or italic', 'normal')
299
- .option('--json', 'Emit the record on stdout')
300
- .action(
301
- (
302
- file: string,
303
- target: string,
304
- opts: { family: string; weight: string; style: string; json?: boolean },
305
- ) => {
306
- const title = 'canon design fonts add'
307
- const emitJson = Boolean(opts.json)
308
- const resolved = fontsTarget(target)
309
- if (typeof resolved !== 'string') {
310
- process.exitCode = refuseFonts(title, resolved, emitJson)
311
- return
312
- }
313
- if (resolved === PROJECT_ROOT) {
314
- process.exitCode = refuseFonts(
315
- title,
316
- {
317
- ok: false,
318
- reason: 'toolkit-root',
319
- detail:
320
- 'Cannot run against toolkit root. Files here are the source of truth.',
321
- },
322
- emitJson,
323
- )
324
- return
325
- }
326
- const outcome = addProjectFace(resolved, resolve(file), {
327
- family: opts.family,
328
- weight: opts.weight,
329
- style: opts.style,
330
- })
331
- if (!outcome.ok) {
332
- process.exitCode = refuseFonts(title, outcome, emitJson)
333
- return
334
- }
335
- if (emitJson) writeJson(outcome)
336
- intro(title)
337
- logAdd(join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR, outcome.file))
338
- logInfo(
339
- `Declared ${outcome.family} ${outcome.weight} ${outcome.style} in ${outcome.sheet}`,
340
- )
341
- outro()
342
- },
343
- )
344
- }
345
-
346
- /**
347
- * Copies the base and stops. The override folder is deliberately not created:
348
- * an empty override is a file the project did not ask for and did not write,
349
- * the three-way merge already handles a missing side, and an empty file invites
350
- * a target to fill it before it has an opinion.
351
- */
352
- async function runInstall(target: string): Promise<number> {
353
- intro('canon design install')
354
-
355
- const resolved = resolveTarget(target, PROJECT_ROOT)
356
- if (typeof resolved === 'number') {
357
- outro()
358
- return resolved
359
- }
360
-
361
- const source = join(PROJECT_ROOT, DESIGN_BASE_CSS)
362
- if (!existsSync(source)) {
363
- logError(
364
- `No base stylesheet at ${DESIGN_BASE_CSS}. Run canon design regen.`,
365
- )
366
- outro()
367
- return 1
368
- }
369
-
370
- const dir = join(resolved, DESIGN_INSTALL_DIR)
371
- mkdirSync(dir, { recursive: true })
372
-
373
- const dest = join(dir, basename(source))
374
- copyFileSync(source, dest)
375
- logAdd(relative(resolved, dest))
376
-
377
- await recordStamp(createDesignAdapter(PROJECT_ROOT), resolved, new Date())
378
-
379
- logInfo(
380
- `Override a value in ${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/, which sync leaves alone.`,
381
- )
382
- outro()
383
- return 0
384
80
  }
@@ -19,6 +19,7 @@ import {
19
19
  logStep,
20
20
  logWarn,
21
21
  outro,
22
+ outroSuccess,
22
23
  palette,
23
24
  pipeOutput,
24
25
  plural,
@@ -216,20 +217,21 @@ function report(result: StageResult): void {
216
217
  * against.
217
218
  */
218
219
  function close(summary: Summary, code: number): void {
219
- const { GREEN, NC, RED, YELLOW } = palette(process.stderr)
220
- outro()
220
+ const { NC, RED, YELLOW } = palette(process.stderr)
221
221
 
222
222
  if (code !== 0) {
223
+ outro()
223
224
  process.stderr.write(`${RED}✗ Verification failed${NC}\n\n`)
224
225
  return
225
226
  }
226
227
 
227
228
  if (summary.unmeasured > 0) {
229
+ outro()
228
230
  process.stderr.write(
229
231
  `${YELLOW}! Verification passed, ${plural(summary.unmeasured, 'stage')} measured nothing${NC}\n\n`,
230
232
  )
231
233
  return
232
234
  }
233
235
 
234
- process.stderr.write(`${GREEN}✓ Verification passed${NC}\n\n`)
236
+ outroSuccess('Verification passed')
235
237
  }
@@ -75,7 +75,7 @@ import {
75
75
  logStep,
76
76
  logWarn,
77
77
  outro,
78
- palette,
78
+ outroSuccess,
79
79
  select,
80
80
  } from '@/cli/ui'
81
81
 
@@ -184,9 +184,9 @@ export function register(program: Command): void {
184
184
  'Copies each rule into .claude/rules/canon/, replacing a copy already',
185
185
  'there. Rules under .claude/rules/project/ are never read.',
186
186
  '',
187
- 'The install confirm needs a TTY. --yes answers it, so a headless call',
188
- 'installs without setting CANON_NON_INTERACTIVE=1. It does not pick a',
189
- 'stack, so a headless call still names one.',
187
+ 'The install confirm needs a TTY. --yes answers it, and a call with no',
188
+ 'terminal on stdin takes Yes. It does not pick a stack, so a headless',
189
+ 'call still names one.',
190
190
  '',
191
191
  'Exit codes:',
192
192
  ' 0 installed, the stack held no rules, or the prompt was cancelled',
@@ -1160,7 +1160,7 @@ function runTestOrder(opts: TestOrderOptions): number {
1160
1160
  const report = readTestOrder(root, { base: opts.base })
1161
1161
  const emitJson = opts.json ?? false
1162
1162
 
1163
- // The frame renders on stderr in both modes and the record goes to stdout
1163
+ // The refusal reaches stderr in both modes and the record goes to stdout
1164
1164
  // alone, which is the split `docs/agents/output-shape.md` fixes. A consumer
1165
1165
  // reading stdout sees pure data either way, and an operator reading the
1166
1166
  // terminal sees the refusal rather than a command that appeared to do nothing.
@@ -1455,8 +1455,8 @@ function displayPath(target: string, rel: string): string {
1455
1455
  }
1456
1456
 
1457
1457
  /**
1458
- * Refuses rather than picking. `select_option` returned `options[0]` under
1459
- * `CANON_NON_INTERACTIVE=1`, so a headless `canon gov install` with no stack
1458
+ * Refuses rather than picking. `select_option` returned `options[0]` with no
1459
+ * terminal to answer it, so a headless `canon gov install` with no stack
1460
1460
  * installed whichever stack sorted first, measured as 26 astro rules into an
1461
1461
  * empty directory. Every documented agent path already passes the argument.
1462
1462
  */
@@ -1558,16 +1558,13 @@ async function runInstall(
1558
1558
  now,
1559
1559
  )
1560
1560
 
1561
- const { GREEN, NC } = palette(process.stderr)
1562
- outro()
1563
- process.stderr.write(`${GREEN}✓ Rules installed${NC}\n`)
1561
+ outroSuccess('Rules installed')
1564
1562
  return 0
1565
1563
  }
1566
1564
 
1567
1565
  /**
1568
1566
  * The install confirm. `--yes` answers it without a prompt, so the line it
1569
- * would have printed is skipped rather than duplicated under
1570
- * `CANON_NON_INTERACTIVE=1`.
1567
+ * would have printed is skipped rather than duplicated on a headless call.
1571
1568
  */
1572
1569
  async function confirmInstall(
1573
1570
  count: number,
@@ -1582,7 +1579,6 @@ async function confirmInstall(
1582
1579
  { value: false, label: 'No' },
1583
1580
  ],
1584
1581
  nonInteractiveDefault: true,
1585
- ttyHint: 'Pass --yes',
1586
1582
  })
1587
1583
  }
1588
1584
 
@@ -1638,9 +1634,7 @@ async function installExtras(
1638
1634
 
1639
1635
  await recordStamp(createGovAdapter(PROJECT_ROOT), resolved, new Date())
1640
1636
 
1641
- const { GREEN, NC } = palette(process.stderr)
1642
- outro()
1643
- process.stderr.write(`${GREEN}✓ Rules installed${NC}\n`)
1637
+ outroSuccess('Rules installed')
1644
1638
  return 0
1645
1639
  }
1646
1640
 
@@ -1689,10 +1683,6 @@ async function runBuild(target: string): Promise<number> {
1689
1683
  await writeFile(output, buildRulesPayload(files))
1690
1684
  logAdd(payload)
1691
1685
 
1692
- const { GREEN, NC } = palette(process.stderr)
1693
- outro()
1694
- process.stderr.write(
1695
- `${GREEN}✓ Rules built (${files.length} rules → ${payload})${NC}\n`,
1696
- )
1686
+ outroSuccess(`Rules built (${files.length} rules → ${payload})`)
1697
1687
  return 0
1698
1688
  }
@@ -311,7 +311,7 @@ async function runAudit(paths: string[], opts: AuditOptions): Promise<number> {
311
311
 
312
312
  intro('canon labels audit')
313
313
 
314
- // The frame renders on stderr in both modes and the record goes to stdout
314
+ // The refusal reaches stderr in both modes and the record goes to stdout
315
315
  // alone, so an operator reading the terminal sees the refusal rather than a
316
316
  // command that appeared to do nothing.
317
317
  if (report.kind === 'refused') {
@@ -3839,7 +3839,7 @@ async function readEvidenceGate(
3839
3839
  cwd: root,
3840
3840
  reject: false,
3841
3841
  timeout: GH_TIMEOUT_MS * 2,
3842
- env: { ...gitEnv(), CANON_NON_INTERACTIVE: '1' },
3842
+ env: gitEnv(),
3843
3843
  extendEnv: false,
3844
3844
  },
3845
3845
  )
@@ -35,7 +35,6 @@ import { describeSkew } from '@/version/skew'
35
35
 
36
36
  const SYNC_ARGS: Record<SyncDomain, readonly string[]> = {
37
37
  governance: ['gov', 'sync'],
38
- design: ['design', 'sync'],
39
38
  claude: ['claude', 'sync'],
40
39
  }
41
40
 
@@ -58,10 +57,10 @@ export function register(program: Command): void {
58
57
  'after',
59
58
  [
60
59
  '',
61
- 'Runs `canon gov sync`, `canon design sync`, and `canon claude sync`',
62
- 'on each domain installed, so each carries its own replace behavior:',
63
- 'governance replaces an edited rule and deletes a retired one, design',
64
- 'replaces its shipped files, and claude writes .gitignore alone.',
60
+ 'Runs `canon gov sync` and `canon claude sync` on each domain',
61
+ 'installed, so each carries its own replace behavior: governance',
62
+ 'replaces an edited rule and deletes a retired one, and claude writes',
63
+ '.gitignore alone.',
65
64
  'Tooling is not synced. Refuses a dirty working tree.',
66
65
  '',
67
66
  'Exit codes:',
@@ -424,7 +423,7 @@ async function runSync(target: string): Promise<number> {
424
423
  /**
425
424
  * Each domain sync runs as its own process. They open their own frames and
426
425
  * prompt for their own changes, so stdin stays inherited. Claude is the one
427
- * exception: it runs headless because the combined pull request preview is the
426
+ * exception: its stdin is closed because the combined pull request preview is the
428
427
  * single confirmation gate for the whole sync, and `canon claude sync` writes
429
428
  * only `.gitignore`.
430
429
  */
@@ -36,7 +36,7 @@ import {
36
36
  logStep,
37
37
  logWarn,
38
38
  outro,
39
- palette,
39
+ outroSuccess,
40
40
  select,
41
41
  } from '@/cli/ui'
42
42
 
@@ -101,7 +101,7 @@ export function register(program: Command): void {
101
101
  'Examples:',
102
102
  ' canon tooling sync base',
103
103
  ' canon tooling sync base --check',
104
- ' CANON_NON_INTERACTIVE=1 canon tooling sync base --write',
104
+ ' canon tooling sync base --write',
105
105
  ' canon tooling sync vite-react web --skip base',
106
106
  '',
107
107
  'A target below its git root syncs as a subfolder. It withholds',
@@ -412,7 +412,6 @@ async function runSync(
412
412
  }
413
413
 
414
414
  const mode = resolveWriteMode(opts)
415
- const { GREEN, NC } = palette(process.stderr)
416
415
 
417
416
  if (result.totalChanges === 0) {
418
417
  // The stamp is a write like any other, so a run with no authority to write
@@ -421,8 +420,7 @@ async function runSync(
421
420
  if (mode === 'apply' || mode === 'prompt') {
422
421
  await stampChain(prepared.chain, prepared.target)
423
422
  }
424
- outro()
425
- process.stderr.write(`${GREEN}✓ Everything up to date${NC}\n`)
423
+ outroSuccess('Everything up to date')
426
424
  return 0
427
425
  }
428
426
 
@@ -450,8 +448,7 @@ async function runSync(
450
448
 
451
449
  await stampChain(prepared.chain, prepared.target)
452
450
 
453
- outro()
454
- process.stderr.write(`${GREEN}✓ Tooling sync complete${NC}\n`)
451
+ outroSuccess('Tooling sync complete')
455
452
  return 0
456
453
  }
457
454
 
@@ -607,7 +604,7 @@ async function promptForStack(): Promise<string | undefined> {
607
604
  return select({
608
605
  message: 'Select tooling stack:',
609
606
  options: stacks.map((name) => ({ value: name, label: name })),
610
- nonInteractiveDefault: true,
607
+ ttyHint: `Pass a stack, one of: ${stacks.join(', ')}`,
611
608
  })
612
609
  }
613
610
 
@@ -2,7 +2,7 @@ import { resolve } from 'node:path'
2
2
  import type { Command } from 'commander'
3
3
  import { recordDir } from '@/roots/record'
4
4
  import { ensureYtDlp, fetchOne } from '@/transcripts/fetch'
5
- import { palette } from '@/cli/ui'
5
+ import { intro, logError, logInfo, outro } from '@/cli/ui'
6
6
  import { mainWorktreeRoot } from '@/git/worktree'
7
7
 
8
8
  interface TranscriptOptions {
@@ -37,10 +37,7 @@ export function register(program: Command): void {
37
37
  )
38
38
  .action(async (url: string, opts: TranscriptOptions) => {
39
39
  const outDir = await resolveOutDir(opts)
40
- const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
41
- process.stderr.write(
42
- `${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}canon transcripts${NC}\n`,
43
- )
40
+ intro('canon transcripts')
44
41
  try {
45
42
  ensureYtDlp()
46
43
  const target = await fetchOne(url, {
@@ -48,14 +45,12 @@ export function register(program: Command): void {
48
45
  keepTimestamps: opts.keepTimestamps ?? false,
49
46
  })
50
47
  process.stdout.write(`${target}\n`)
51
- process.stderr.write(
52
- `${GREY}│${NC} ${GREEN}✓${NC} ${target}\n${GREY}└${NC}\n`,
53
- )
48
+ logInfo(target)
49
+ outro()
54
50
  } catch (error) {
55
51
  const message = error instanceof Error ? error.message : String(error)
56
- process.stderr.write(
57
- `${GREY}│${NC} ${RED}✗${NC} ${message}\n${GREY}└${NC}\n`,
58
- )
52
+ logError(message)
53
+ outro()
59
54
  process.exitCode = 1
60
55
  }
61
56
  })