@erclx/canon 5.67.0 → 5.69.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 (105) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +1 -1
  3. package/claude/.claude-plugin/plugin.json +1 -1
  4. package/claude/skills/app-drive/REQUIREMENT.md +5 -5
  5. package/claude/skills/app-drive/SKILL.md +14 -4
  6. package/claude/skills/app-drive/references/drive-record.md +12 -1
  7. package/claude/skills/canon-cli/SKILL.md +2 -2
  8. package/claude/skills/canon-feedback/REQUIREMENT.md +1 -1
  9. package/claude/skills/canon-feedback/SKILL.md +1 -1
  10. package/claude/skills/design-identity/REQUIREMENT.md +4 -4
  11. package/claude/skills/design-identity/SKILL.md +7 -7
  12. package/claude/skills/{pick-by-eye → design-pick}/REQUIREMENT.md +6 -6
  13. package/claude/skills/{pick-by-eye → design-pick}/SKILL.md +18 -10
  14. package/claude/skills/{pick-by-eye → design-pick}/references/board.md +17 -11
  15. package/claude/skills/design-pick/references/design-config.md +33 -0
  16. package/claude/skills/design-record/REQUIREMENT.md +2 -2
  17. package/claude/skills/design-record/SKILL.md +1 -1
  18. package/claude/skills/design-taste/REQUIREMENT.md +2 -2
  19. package/claude/skills/design-taste/SKILL.md +3 -3
  20. package/claude/skills/design-taste/references/preflight.md +1 -1
  21. package/claude/skills/git-issue/REQUIREMENT.md +1 -1
  22. package/claude/skills/git-issue/references/issue.md +1 -1
  23. package/claude/skills/plan-feature/SKILL.md +1 -1
  24. package/claude/skills/plan-feature/references/plan.md +21 -6
  25. package/claude/skills/plan-groundwork/SKILL.md +1 -1
  26. package/claude/skills/pr-evidence/REQUIREMENT.md +1 -1
  27. package/claude/skills/pr-evidence/SKILL.md +7 -1
  28. package/claude/skills/pr-evidence/references/evidence.md +33 -3
  29. package/claude/skills/review-craft/references/ui.md +30 -10
  30. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +3 -3
  31. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +1 -1
  32. package/claude/skills/role-orchestrator/references/orchestrator-planning-wait.md +24 -0
  33. package/claude/skills/role-planner/SKILL.md +4 -2
  34. package/claude/skills/role-worker/SKILL.md +5 -6
  35. package/claude/skills/task-board/references/tasks.md +1 -1
  36. package/claude/skills/task-complete/references/receipt-sweep.md +1 -1
  37. package/claude/skills/{ux-audit → ui-audit}/REQUIREMENT.md +3 -3
  38. package/claude/skills/{ux-audit → ui-audit}/SKILL.md +7 -7
  39. package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
  40. package/claude/skills/ui-craft/SKILL.md +1 -1
  41. package/claude/skills/{ui-record → ui-film}/REQUIREMENT.md +7 -5
  42. package/claude/skills/{ui-record → ui-film}/SKILL.md +23 -12
  43. package/claude/skills/{ui-record → ui-film}/references/config.md +3 -3
  44. package/claude/skills/{ui-record → ui-film}/references/handoff.md +28 -9
  45. package/claude/skills/ui-film/references/pr-clip.md +84 -0
  46. package/claude/skills/{ui-record → ui-film}/references/take-craft.md +8 -1
  47. package/claude/skills/{ux-measure → ui-measure}/REQUIREMENT.md +3 -3
  48. package/claude/skills/{ux-measure → ui-measure}/SKILL.md +6 -6
  49. package/docs/agents/audits.md +1 -1
  50. package/docs/agents/migrate.md +13 -13
  51. package/docs/agents/records.md +1 -0
  52. package/docs/target/migrations.md +12 -10
  53. package/docs/target/projects.md +1 -1
  54. package/docs/target/sync.md +1 -1
  55. package/docs/workflow/skill-map-on-request.md +1 -2
  56. package/docs/workflow/skill-map.md +9 -10
  57. package/docs/workflow/visual-design-workflow.md +6 -1
  58. package/governance/ledger/claude/background.md +26 -0
  59. package/governance/rules/claude/background.md +19 -0
  60. package/governance/stacks/base.toml +1 -0
  61. package/package.json +8 -5
  62. package/scripts/ci/install-playwright-deps.sh +52 -0
  63. package/src/claude/cases/authoring.ts +2 -2
  64. package/src/claude/cases/misc.ts +1 -1
  65. package/src/claude/cases/setup.ts +0 -5
  66. package/src/claude/cases/workflow.ts +6 -6
  67. package/src/commands/fixtures/gh.sh +64 -0
  68. package/src/commands/gov.ts +33 -3
  69. package/src/commands/migrate.ts +12 -2
  70. package/src/commands/pr.ts +237 -79
  71. package/src/commands/take.ts +44 -8
  72. package/src/commands/tasks.ts +183 -0
  73. package/src/gate/stages.ts +2 -2
  74. package/src/gov/audit.ts +48 -11
  75. package/src/gov/detect.ts +1 -1
  76. package/src/markdown/scope.ts +9 -14
  77. package/src/migrate/record-layout.ts +5 -3
  78. package/src/migrate/skill-names.ts +21 -4
  79. package/src/pr/evidence.ts +208 -78
  80. package/src/pr/frames.ts +42 -4
  81. package/src/pr/pixels.ts +44 -0
  82. package/src/records/backup.ts +28 -4
  83. package/src/records/validate.ts +1 -0
  84. package/src/sandbox/headless/dispatch-shim.sh +15 -0
  85. package/src/sandbox/headless/dispatch.ts +12 -14
  86. package/src/sandbox/stub-gh.sh +26 -0
  87. package/src/sandbox/stub-remote.ts +8 -29
  88. package/src/sync/engine.ts +1 -1
  89. package/src/take/camera.ts +2 -2
  90. package/src/take/encode.ts +38 -1
  91. package/src/take/film.ts +26 -3
  92. package/src/take/pointer.ts +169 -0
  93. package/src/take/reel.ts +215 -0
  94. package/src/take/screencast.ts +5 -2
  95. package/src/take/stepper.ts +23 -6
  96. package/src/take/zoom/NOTICE.md +1 -1
  97. package/src/tasks/launch-wait.ts +160 -0
  98. package/tooling/claude/reference.md +1 -1
  99. package/claude/skills/canon-feedback-triage/REQUIREMENT.md +0 -34
  100. package/claude/skills/canon-feedback-triage/SKILL.md +0 -98
  101. package/claude/skills/plan-ready/REQUIREMENT.md +0 -35
  102. package/claude/skills/plan-ready/SKILL.md +0 -136
  103. package/claude/skills/plan-ready/references/assembly.md +0 -75
  104. package/claude/skills/plan-ready/references/ready.md +0 -128
  105. /package/claude/skills/{pick-by-eye → design-pick}/references/live-arms.md +0 -0
@@ -5,6 +5,7 @@ import { INSTALL_BROWSER, isEngineMissing } from '@/browser/engine'
5
5
  import { readTakePlan } from '@/take/plan'
6
6
  import type { TakePlanRefusal } from '@/take/plan'
7
7
  import type { TakeRefusal } from '@/take/film'
8
+ import { renderReel } from '@/take/reel'
8
9
  import {
9
10
  intro,
10
11
  logError,
@@ -24,6 +25,7 @@ type Take = typeof import('@/take/film')
24
25
 
25
26
  interface RunOptions {
26
27
  readonly out: string
28
+ readonly reel?: string
27
29
  readonly json?: boolean
28
30
  }
29
31
 
@@ -60,14 +62,19 @@ export function register(program: Command): void {
60
62
  .argument('<plan>', 'JSON file naming the beats and their steps')
61
63
  .requiredOption('--out <dir>', 'Folder the clips, logs, and tracks land in')
62
64
  .helpOption('-h, --help', 'Show this help message')
65
+ .option(
66
+ '--reel <file>',
67
+ 'Also join the beats into one animated WebP with the pointer drawn',
68
+ )
63
69
  .option('--json', 'Add a machine-readable record on stdout')
64
70
  .addHelpText(
65
71
  'after',
66
72
  [
67
73
  '',
68
74
  'Films each beat from a fresh load of the page into its own folder under',
69
- '--out, numbered in plan order: clip.mp4, log.json, and camera.json.',
70
- 'take.json beside them lists every beat and the route that filmed it.',
75
+ '--out, numbered in plan order: clip.mp4, log.json, camera.json, and',
76
+ "still.jpg, the clip's closing frame. take.json beside them lists every",
77
+ 'beat and the route that filmed it.',
71
78
  '',
72
79
  'A step beat is filmed by frame stepping in chrome-headless-shell, at',
73
80
  '3840x2160 and exactly 60 fps, with the page clock advancing one frame',
@@ -77,7 +84,13 @@ export function register(program: Command): void {
77
84
  '',
78
85
  'No cursor is drawn in a clip. The edit draws it from the log at a',
79
86
  'constant size, and the camera track carries the cursor and click ring',
80
- 'per frame beside the view. Canon renders nothing past the clip.',
87
+ 'per frame beside the view. Canon renders nothing past the clip, save',
88
+ 'the reel below.',
89
+ '',
90
+ '--reel <file> joins every beat in plan order into one looping animated',
91
+ 'WebP at 960 wide and 20 fps, keeping every third frame, with the cursor',
92
+ 'and click ring drawn from each camera track. It carries no zoom, so a',
93
+ "reviewer sees the whole page. clip.mp4 stays each beat's raw take.",
81
94
  '',
82
95
  ' {',
83
96
  ' "beats": [',
@@ -118,14 +131,16 @@ export function register(program: Command): void {
118
131
  ' server-unreachable nothing answered at the URL',
119
132
  ' target-missing a step names a target the page does not carry',
120
133
  ' take-failed the take failed against a reachable page',
134
+ ' reel-failed every beat was filmed and the reel did not encode',
121
135
  '',
122
136
  'Exit codes:',
123
- ' 0 every beat was filmed',
137
+ ' 0 every beat was filmed, and the reel written when asked for',
124
138
  ' 1 refused, with the reason on stderr or in the JSON record',
125
139
  '',
126
140
  'Examples:',
127
141
  ' canon take http://localhost:4173 plan.json --out .canon/tmp/demo/takes',
128
142
  ' canon take http://localhost:4173 plan.json --out takes --json',
143
+ ' canon take http://localhost:4173 plan.json --out takes --reel takes/reel.webp',
129
144
  '',
130
145
  ].join('\n'),
131
146
  )
@@ -222,16 +237,37 @@ async function runTake(
222
237
  )
223
238
  if (beat.fallback) logWarn(` filmed by screencast: ${beat.fallback}`)
224
239
  }
225
- outro()
226
240
 
227
- emit(emitJson, {
241
+ const filmed = {
228
242
  url,
229
243
  plan: path,
230
244
  out: result.out,
231
245
  beats: result.beats,
232
246
  durationMs: result.durationMs,
233
- })
234
- return 0
247
+ }
248
+ if (!opts.reel) {
249
+ outro()
250
+ emit(emitJson, filmed)
251
+ return 0
252
+ }
253
+
254
+ logStep('Reel')
255
+ try {
256
+ const reel = await renderReel(result.out, result.beats, resolve(opts.reel))
257
+ logInfo(
258
+ `${reel.file} ${plural(reel.frames, 'frame')} ${reel.seconds} s ${(reel.bytes / 1e6).toFixed(1)} MB`,
259
+ )
260
+ outro()
261
+ emit(emitJson, { ...filmed, reel })
262
+ return 0
263
+ } catch (error) {
264
+ const message = error instanceof Error ? error.message : String(error)
265
+ logWarn('Every beat was filmed, and the reel did not encode.')
266
+ logWarn(message.split('\n')[0] ?? '')
267
+ outro()
268
+ emit(emitJson, { ...filmed, reason: 'reel-failed', message })
269
+ return 1
270
+ }
235
271
  }
236
272
 
237
273
  /**
@@ -3,6 +3,13 @@ import type { Command } from 'commander'
3
3
  import { execa } from 'execa'
4
4
  import { gitEnv } from '@/git/env'
5
5
  import { type AnswersOutcome, planAnswers } from '@/tasks/answers'
6
+ import {
7
+ DEFAULT_LAUNCH_WAIT_SECONDS,
8
+ type LaunchOutcome,
9
+ type LaunchWait,
10
+ readLaunchable,
11
+ waitForLaunchable,
12
+ } from '@/tasks/launch-wait'
6
13
  import {
7
14
  type BoardOutcome,
8
15
  type BoardRefused,
@@ -112,6 +119,12 @@ interface AnswersCommandOptions {
112
119
  readonly root?: string
113
120
  }
114
121
 
122
+ interface LaunchableCommandOptions {
123
+ readonly json?: boolean
124
+ readonly root?: string
125
+ readonly wait?: string | true
126
+ }
127
+
115
128
  interface BranchCommandOptions {
116
129
  readonly json?: boolean
117
130
  readonly root?: string
@@ -414,6 +427,72 @@ export function register(program: Command): void {
414
427
  process.exitCode = await runAnswers(plan, opts)
415
428
  })
416
429
 
430
+ tasks
431
+ .command('launchable')
432
+ .description(
433
+ 'Report whether a row has a linked plan the dispatch may launch',
434
+ )
435
+ .argument('<task>', 'Task filename stem, as in v28.1-trigger-escalation')
436
+ .helpOption('-h, --help', 'Show this help message')
437
+ .option('--json', 'Emit a machine-readable record on stdout')
438
+ .option('--root <path>', 'Board root, defaulting to the main worktree')
439
+ .option(
440
+ '--wait [seconds]',
441
+ `Hold until the row is launchable, default ${DEFAULT_LAUNCH_WAIT_SECONDS}`,
442
+ )
443
+ .addHelpText(
444
+ 'after',
445
+ [
446
+ '',
447
+ "Resolves the task's Plan: line and runs the plan-answers gate on the",
448
+ 'plan it names, in one read. It keys on the line rather than on the',
449
+ 'plan file, since a planner links a plan only after it validates, so a',
450
+ 'plan still being written never reads as landed. It answers planning',
451
+ 'alone: the branch and claim checks stay in the dispatch runbook.',
452
+ '',
453
+ 'The record carries task, plan, launchable, and open, the last three',
454
+ 'named as plan-answers names them. Read `reason`:',
455
+ ' unlinked the task carries no Plan: line yet',
456
+ ' open the plan still holds a question for the operator',
457
+ ' launchable the plan waits on nobody',
458
+ '',
459
+ '`--wait [seconds]` re-reads every 30 seconds until the row is',
460
+ 'launchable or the deadline passes. `reason` then reads `launchable`',
461
+ 'or `timeout`, the record carries the last reading, and',
462
+ '`waitedSeconds` is added. A refusal ends the wait at once. A wait',
463
+ 'past 600 seconds outlasts a foreground shell call, so run it in the',
464
+ 'background and act on the wake.',
465
+ '',
466
+ 'Refusals, read off `reason`:',
467
+ ' no-board the project carries no .canon/tasks/',
468
+ ' no-match no task carries that stem exactly',
469
+ ' ambiguous several tasks start with the name, so pass one in full',
470
+ ' several-plans the Plan: line links more than one plan',
471
+ ' no-plan the Plan: line names no file',
472
+ ' archived the Plan: line points into the plans archive',
473
+ ' bad-input the Plan: line carries an empty target',
474
+ ' bad-wait --wait takes a whole number of seconds above zero',
475
+ '',
476
+ 'Exit codes:',
477
+ ' 0 the row is launchable',
478
+ ' 1 refused, with the reason on stderr or in the JSON record',
479
+ ' 2 the row is unlinked or open, or the wait reached its deadline',
480
+ '',
481
+ 'It reports and never writes. Branch on `reason` rather than on the',
482
+ 'exit code, which a shell function wrapping canon can flatten to zero.',
483
+ '',
484
+ 'Examples:',
485
+ ' canon tasks launchable v28.1-trigger-escalation',
486
+ ' canon tasks launchable v28.1-trigger-escalation --json',
487
+ ' canon tasks launchable v28.1-trigger-escalation --wait --json',
488
+ ' canon tasks launchable v28.1-trigger-escalation --wait 900 --json',
489
+ '',
490
+ ].join('\n'),
491
+ )
492
+ .action(async (task: string, opts: LaunchableCommandOptions) => {
493
+ process.exitCode = await runLaunchable(task, opts)
494
+ })
495
+
417
496
  tasks
418
497
  .command('plan-branch')
419
498
  .description('Derive the branch name a dispatch and a worker both take')
@@ -1827,6 +1906,110 @@ function reportAnswers(
1827
1906
  return EXIT_FINDINGS
1828
1907
  }
1829
1908
 
1909
+ const BAD_WAIT = '--wait takes a whole number of seconds above zero.'
1910
+
1911
+ interface BadWait {
1912
+ readonly ok: false
1913
+ readonly reason: 'bad-wait'
1914
+ readonly message: string
1915
+ }
1916
+
1917
+ async function runLaunchable(
1918
+ task: string,
1919
+ opts: LaunchableCommandOptions,
1920
+ ): Promise<number> {
1921
+ const emitJson = opts.json ?? false
1922
+ const root = opts.root ?? (await mainWorktreeRoot())
1923
+
1924
+ let waitSeconds: number | undefined
1925
+ if (opts.wait === true) {
1926
+ waitSeconds = DEFAULT_LAUNCH_WAIT_SECONDS
1927
+ } else if (opts.wait !== undefined) {
1928
+ if (!/^\d+$/.test(opts.wait) || Number(opts.wait) === 0) {
1929
+ const refused: BadWait = {
1930
+ ok: false,
1931
+ reason: 'bad-wait',
1932
+ message: BAD_WAIT,
1933
+ }
1934
+ return reportLaunchable(refused, emitJson, root)
1935
+ }
1936
+ waitSeconds = Number(opts.wait)
1937
+ }
1938
+
1939
+ const outcome =
1940
+ waitSeconds === undefined
1941
+ ? await readLaunchable(root, task)
1942
+ : await waitForLaunchable(root, task, {
1943
+ waitSeconds,
1944
+ clock: {
1945
+ now: () => Date.now(),
1946
+ sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
1947
+ },
1948
+ })
1949
+
1950
+ return reportLaunchable(outcome, emitJson, root)
1951
+ }
1952
+
1953
+ function reportLaunchable(
1954
+ outcome: LaunchOutcome | LaunchWait | BadWait,
1955
+ emitJson: boolean,
1956
+ root: string,
1957
+ ): number {
1958
+ if (!outcome.ok) {
1959
+ intro('canon tasks launchable')
1960
+ logStep('Refused')
1961
+ logError(outcome.message)
1962
+ outro()
1963
+
1964
+ if (emitJson) {
1965
+ process.stdout.write(
1966
+ `${JSON.stringify({
1967
+ ok: false,
1968
+ reason: outcome.reason,
1969
+ message: outcome.message,
1970
+ ...('waitedSeconds' in outcome && {
1971
+ waitedSeconds: outcome.waitedSeconds,
1972
+ }),
1973
+ root,
1974
+ })}\n`,
1975
+ )
1976
+ }
1977
+ return 1
1978
+ }
1979
+
1980
+ const code = outcome.launchable ? 0 : EXIT_FINDINGS
1981
+
1982
+ if (emitJson) {
1983
+ process.stdout.write(`${JSON.stringify({ ...outcome, root })}\n`)
1984
+ return code
1985
+ }
1986
+
1987
+ intro('canon tasks launchable')
1988
+ logStep(outcome.task)
1989
+
1990
+ if (outcome.plan === undefined) {
1991
+ logWarn('carries no Plan: line yet, so no plan has landed.')
1992
+ } else if (outcome.launchable) {
1993
+ logInfo(`${outcome.plan} waits on nobody, so the dispatch may launch it.`)
1994
+ } else {
1995
+ for (const question of outcome.open) {
1996
+ logWarn(`${question.label} ${question.why}`)
1997
+ }
1998
+ logError(`${outcome.plan} still waits on the operator.`)
1999
+ }
2000
+
2001
+ if ('waitedSeconds' in outcome) {
2002
+ logInfo(
2003
+ outcome.reason === 'timeout'
2004
+ ? `not launchable within ${outcome.waitedSeconds}s`
2005
+ : `launchable after ${outcome.waitedSeconds}s`,
2006
+ )
2007
+ }
2008
+
2009
+ outro()
2010
+ return code
2011
+ }
2012
+
1830
2013
  async function runBranch(
1831
2014
  plan: string,
1832
2015
  opts: BranchCommandOptions,
@@ -402,9 +402,9 @@ export const STAGES: readonly Stage[] = [
402
402
  checks: [
403
403
  {
404
404
  kind: 'cli',
405
- argv: ['gov', 'audit', '--gate'],
405
+ argv: ['gov', 'audit', '--gate', '--touched'],
406
406
  failure:
407
- 'A rule in a gated folder is past a shape limit or short of its ledger. Run bun src/cli.ts gov audit.',
407
+ 'A rule in a gated folder is past a shape limit or short of its ledger. Run bun src/cli.ts gov audit --touched.',
408
408
  },
409
409
  ],
410
410
  },
package/src/gov/audit.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, statSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
- import { join, relative, resolve, sep } from 'node:path'
3
+ import { join, normalize, relative, resolve, sep } from 'node:path'
4
4
  import { listRuleFiles } from '@/gov/payload'
5
5
 
6
6
  /**
@@ -57,6 +57,19 @@ export const PRACTICE_FOLDERS: readonly string[] = [
57
57
  'tooling',
58
58
  ]
59
59
 
60
+ /**
61
+ * The folders whose rules record house decisions. Their ledger is checked
62
+ * wherever one exists and owed only by a rule the branch touched, since the
63
+ * reasoning behind an older bullet was never written down and no backfill
64
+ * recovers it.
65
+ */
66
+ export const CORE_FOLDERS: readonly string[] = [
67
+ 'canon',
68
+ 'claude',
69
+ 'code',
70
+ 'writing',
71
+ ]
72
+
60
73
  const SHIPPED_RULES = join('governance', 'rules')
61
74
  const LEDGERS = join('governance', 'ledger')
62
75
 
@@ -316,29 +329,41 @@ export function compareLedger(
316
329
  .filter((entry) => entry.bullets !== entry.rows)
317
330
  }
318
331
 
319
- /** Where a rule's ledger lives, or nothing when its folder carries none. */
320
- function ledgerPath(file: string): string | undefined {
332
+ /**
333
+ * Where a rule's ledger lives and whether its absence is a finding, or nothing
334
+ * when its folder carries none.
335
+ */
336
+ function ledgerFor(
337
+ file: string,
338
+ touched: ReadonlySet<string>,
339
+ ): { path: string; owed: boolean } | undefined {
321
340
  const prefix = `${SHIPPED_RULES}${sep}`
322
341
  if (!file.startsWith(prefix)) return undefined
323
342
  const rest = file.slice(prefix.length)
324
343
  const folder = rest.split(sep)[0]
325
- if (!PRACTICE_FOLDERS.includes(folder)) return undefined
326
- return join(LEDGERS, rest)
344
+ const path = join(LEDGERS, rest)
345
+ if (PRACTICE_FOLDERS.includes(folder)) return { path, owed: true }
346
+ if (CORE_FOLDERS.includes(folder)) return { path, owed: touched.has(file) }
347
+ return undefined
327
348
  }
328
349
 
329
350
  async function readLedger(
330
351
  root: string,
331
352
  file: string,
332
353
  groups: readonly HeadingCount[],
354
+ touched: ReadonlySet<string>,
333
355
  ): Promise<LedgerReading> {
334
- const path = ledgerPath(file)
335
- if (path === undefined) return { status: 'not-required' }
356
+ const ledger = ledgerFor(file, touched)
357
+ if (ledger === undefined) return { status: 'not-required' }
358
+ const { path } = ledger
336
359
 
337
360
  let text: string
338
361
  try {
339
362
  text = await readFile(resolve(root, path), 'utf8')
340
363
  } catch {
341
- return { status: 'missing', path }
364
+ return ledger.owed
365
+ ? { status: 'missing', path }
366
+ : { status: 'not-required' }
342
367
  }
343
368
 
344
369
  return {
@@ -352,14 +377,22 @@ async function readLedger(
352
377
  * Measures every rule this tree authors, or the files named.
353
378
  *
354
379
  * A tree holding `governance/rules/` is a canon checkout, read across both
355
- * authored corpora with a ledger asked of each outside-practice rule. Any
356
- * other tree is a target, read at `.claude/rules/project/` alone with no
380
+ * authored corpora with a ledger asked of each outside-practice rule and of
381
+ * each core rule in `touched`, and checked for any core rule carrying one.
382
+ * Any other tree is a target, read at `.claude/rules/project/` alone with no
357
383
  * ledger asked, since ledgers never ship.
384
+ *
385
+ * `touched` holds paths relative to the root, and leaving it out reads no core
386
+ * rule as touched.
358
387
  */
359
388
  export async function readRuleAudit(
360
389
  root: string,
361
390
  paths: readonly string[] = [],
391
+ options: { readonly touched?: readonly string[] } = {},
362
392
  ): Promise<RuleAuditReport> {
393
+ const touched = new Set(
394
+ (options.touched ?? []).map((path) => normalize(path)),
395
+ )
363
396
  const scope = existsSync(resolve(root, SHIPPED_RULES)) ? 'canon' : 'target'
364
397
  const corpus = scope === 'canon' ? CANON_CORPUS : TARGET_CORPUS
365
398
 
@@ -421,7 +454,11 @@ export async function readRuleAudit(
421
454
  for (const abs of files) {
422
455
  const file = relative(root, abs)
423
456
  const { groups, ...shape } = measureRule(await readFile(abs, 'utf8'))
424
- rules.push({ file, ...shape, ledger: await readLedger(root, file, groups) })
457
+ rules.push({
458
+ file,
459
+ ...shape,
460
+ ledger: await readLedger(root, file, groups, touched),
461
+ })
425
462
  }
426
463
 
427
464
  return { kind: 'measured', scope, corpus, rules }
package/src/gov/detect.ts CHANGED
@@ -253,7 +253,7 @@ function isLiteral(pattern: string): boolean {
253
253
 
254
254
  /**
255
255
  * A literal pattern is also tested on disk, since evidence such as
256
- * `.canon/canvas` sits in a folder a target's own `.gitignore` excludes, and
256
+ * `.canon/board` sits in a folder a target's own `.gitignore` excludes, and
257
257
  * the git listing never carries it.
258
258
  */
259
259
  function matchEntry(
@@ -136,8 +136,6 @@ export const GATED_PATHS: readonly string[] = [
136
136
  'claude/skills/build-in-slices/references/adopted.md',
137
137
  'claude/skills/canon-cli/REQUIREMENT.md',
138
138
  'claude/skills/canon-cli/SKILL.md',
139
- 'claude/skills/canon-feedback-triage/REQUIREMENT.md',
140
- 'claude/skills/canon-feedback-triage/SKILL.md',
141
139
  'claude/skills/canon-feedback/REQUIREMENT.md',
142
140
  'claude/skills/canon-feedback/SKILL.md',
143
141
  'claude/skills/canon-feedback/references/template.md',
@@ -202,10 +200,10 @@ export const GATED_PATHS: readonly string[] = [
202
200
  'claude/skills/document-repair/REQUIREMENT.md',
203
201
  'claude/skills/document-repair/SKILL.md',
204
202
  'claude/skills/document-repair/references/batches.md',
205
- 'claude/skills/pick-by-eye/REQUIREMENT.md',
206
- 'claude/skills/pick-by-eye/SKILL.md',
207
- 'claude/skills/pick-by-eye/references/board.md',
208
- 'claude/skills/pick-by-eye/references/live-arms.md',
203
+ 'claude/skills/design-pick/REQUIREMENT.md',
204
+ 'claude/skills/design-pick/SKILL.md',
205
+ 'claude/skills/design-pick/references/board.md',
206
+ 'claude/skills/design-pick/references/live-arms.md',
209
207
  'claude/skills/write-doc/REQUIREMENT.md',
210
208
  'claude/skills/write-doc/SKILL.md',
211
209
  'claude/skills/write-doc/references/context.md',
@@ -229,10 +227,6 @@ export const GATED_PATHS: readonly string[] = [
229
227
  'claude/skills/design-identity/REQUIREMENT.md',
230
228
  'claude/skills/design-identity/SKILL.md',
231
229
  'claude/skills/design-identity/references/card-route.md',
232
- 'claude/skills/plan-ready/REQUIREMENT.md',
233
- 'claude/skills/plan-ready/SKILL.md',
234
- 'claude/skills/plan-ready/references/assembly.md',
235
- 'claude/skills/plan-ready/references/ready.md',
236
230
  'claude/skills/git-branch/REQUIREMENT.md',
237
231
  'claude/skills/git-branch/SKILL.md',
238
232
  'claude/skills/git-branch/references/branch.md',
@@ -391,10 +385,10 @@ export const GATED_PATHS: readonly string[] = [
391
385
  'claude/skills/test-first/references/adopted.md',
392
386
  'claude/skills/ui-checklist/REQUIREMENT.md',
393
387
  'claude/skills/ui-checklist/SKILL.md',
394
- 'claude/skills/ux-audit/REQUIREMENT.md',
395
- 'claude/skills/ux-audit/SKILL.md',
396
- 'claude/skills/ux-measure/REQUIREMENT.md',
397
- 'claude/skills/ux-measure/SKILL.md',
388
+ 'claude/skills/ui-audit/REQUIREMENT.md',
389
+ 'claude/skills/ui-audit/SKILL.md',
390
+ 'claude/skills/ui-measure/REQUIREMENT.md',
391
+ 'claude/skills/ui-measure/SKILL.md',
398
392
  'claude/skills/write-human/REQUIREMENT.md',
399
393
  'claude/skills/write-human/SKILL.md',
400
394
  'claude/skills/write-human/references/adopted.md',
@@ -404,6 +398,7 @@ export const GATED_PATHS: readonly string[] = [
404
398
  'claude/skills/youtube-transcripts/SKILL.md',
405
399
  '.claude/skills/internal-claude/REQUIREMENT.md',
406
400
  '.claude/skills/internal-docs/REQUIREMENT.md',
401
+ '.claude/skills/internal-feedback-triage/REQUIREMENT.md',
407
402
  '.claude/skills/internal-governance/REQUIREMENT.md',
408
403
  '.claude/skills/internal-rule-audit/REQUIREMENT.md',
409
404
  '.claude/skills/internal-sandbox-check/REQUIREMENT.md',
@@ -88,8 +88,9 @@ const RENDER = [SCRATCH, 'render'] as const
88
88
  * Every move this migration makes, as record-relative paths.
89
89
  *
90
90
  * The first two rows are batch 2's. The rest are batch 3's, in the order the
91
- * intake's review-folder pass listed them. A later batch appends here rather
92
- * than adding a second table.
91
+ * intake's review-folder pass listed them, then the design board, moved to
92
+ * the `.canon/board` folder Frameboard names. A later batch appends here
93
+ * rather than adding a second table.
93
94
  */
94
95
  export const RECORD_LAYOUT_MOVES: readonly RecordLayoutMove[] = [
95
96
  { kind: 'folder', from: ['review', 'memory'], to: ['memory', 'review'] },
@@ -130,6 +131,7 @@ export const RECORD_LAYOUT_MOVES: readonly RecordLayoutMove[] = [
130
131
  picks: ['picks'],
131
132
  evidence: ['evidence'],
132
133
  },
134
+ { kind: 'folder', from: ['canvas'], to: ['board'] },
133
135
  ]
134
136
 
135
137
  function resolveRecord(root: string, segments: readonly string[]): string {
@@ -346,7 +348,7 @@ function entriesOf(dir: string, isDirectory: boolean): string[] {
346
348
  }
347
349
 
348
350
  /**
349
- * What `pick-by-eye` Step 6 writes into the folder it archives a choice to:
351
+ * What `design-pick` Step 6 writes into the folder it archives a choice to:
350
352
  * a capture per canvas frame, named `arm-<id>`. A design handoff marks a pick
351
353
  * folder an earlier skill wrote. A folder carrying neither is evidence.
352
354
  */
@@ -54,6 +54,15 @@ import { defineRenameRules, type RenameRules } from '@/migrate/rename'
54
54
  * target on the oldest spelling lands in one pass rather than two.
55
55
  * `draft-ready` alone keys on its namespaced forms below rather than here.
56
56
  *
57
+ * Four skills then moved into the family their job belongs to. `pick-by-eye`
58
+ * was the one step of the design flow without the `design-` prefix, so it
59
+ * became `design-pick`. `ui-record` filmed footage while "record" already
60
+ * named the design record, so it became `ui-film`. `ux-audit` and
61
+ * `ux-measure` were the only `ux-` skills beside the `ui-` ones, so they
62
+ * became `ui-audit` and `ui-measure`. `draft-and-pick`, `claude-ux-audit`,
63
+ * and `claude-ux-measure` are retargeted onto the new names for the same
64
+ * reason `claude-ui-test` was.
65
+ *
57
66
  * Every name takes two words. Ten of these would have landed as a bare single
58
67
  * word under a plain strip, and a bare word such as `review` or `docs` is a
59
68
  * substring of ordinary prose with no token left for a later sweep to find.
@@ -92,8 +101,10 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
92
101
  'claude-teach': 'teach-workspace',
93
102
  'claude-ui-test': 'ui-checklist',
94
103
  'ui-test': 'ui-checklist',
95
- 'claude-ux-audit': 'ux-audit',
96
- 'claude-ux-measure': 'ux-measure',
104
+ 'claude-ux-audit': 'ui-audit',
105
+ 'claude-ux-measure': 'ui-measure',
106
+ 'ux-audit': 'ui-audit',
107
+ 'ux-measure': 'ui-measure',
97
108
  'claude-worker': 'role-worker',
98
109
  'claude-worktree': 'session-worktree',
99
110
  'canon-screencast': 'script-screencast',
@@ -110,8 +121,10 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
110
121
  'draft-figure': 'draw-figure',
111
122
  'draft-identity': 'design-identity',
112
123
  'draft-slides': 'make-deck',
113
- 'draft-and-pick': 'pick-by-eye',
124
+ 'draft-and-pick': 'design-pick',
114
125
  'draft-screencast': 'script-screencast',
126
+ 'pick-by-eye': 'design-pick',
127
+ 'ui-record': 'ui-film',
115
128
  }
116
129
 
117
130
  /**
@@ -190,6 +203,10 @@ export const RENAMED_DRAFT_POINTERS: readonly string[] = [
190
203
  * what shipped or what a session ran under whatever name was current then, so
191
204
  * rewriting one makes it testify to a release or a run that never happened.
192
205
  *
206
+ * A replay fixture is excluded on the same argument. It records a real
207
+ * project's tree as that project held it, so a rewrite changes what the replay
208
+ * proves.
209
+ *
193
210
  * The record archives need no entry here. They are gitignored, so the tracked
194
211
  * listing every sweep reads never reaches them.
195
212
  */
@@ -197,7 +214,7 @@ export const SKILL_NAME_RULES: RenameRules = defineRenameRules({
197
214
  replacements: { ...SKILL_NAME_MAP, ...SKILL_CITATION_MAP },
198
215
  keepMarker: 'canon-keep-retired',
199
216
  wholeToken: true,
200
- excludedPrefixes: ['scripts/eval/result-'],
217
+ excludedPrefixes: ['scripts/eval/result-', 'sandbox/fixtures/replay/'],
201
218
  excludedPaths: [
202
219
  'CHANGELOG.md',
203
220
  'src/migrate/skill-names.ts',