@erclx/canon 5.26.0 → 5.27.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 (242) hide show
  1. package/README.md +2 -2
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/SKILL.md +2 -0
  4. package/claude/skills/auto-ship/SKILL.md +3 -11
  5. package/claude/skills/auto-ship/references/review-findings.md +20 -0
  6. package/claude/skills/backlog-triage/SKILL.md +2 -0
  7. package/claude/skills/bash-cli-script/SKILL.md +2 -0
  8. package/claude/skills/build-in-slices/SKILL.md +2 -0
  9. package/claude/skills/canon-cli/SKILL.md +2 -0
  10. package/claude/skills/canon-feedback/SKILL.md +2 -0
  11. package/claude/skills/canon-feedback-triage/SKILL.md +2 -0
  12. package/claude/skills/canon-operator/SKILL.md +2 -0
  13. package/claude/skills/canon-rollout/SKILL.md +3 -11
  14. package/claude/skills/canon-rollout/references/failure-recovery.md +20 -0
  15. package/claude/skills/canvas/SKILL.md +13 -0
  16. package/claude/skills/ci-workflow/SKILL.md +2 -0
  17. package/claude/skills/code-craft/SKILL.md +2 -0
  18. package/claude/skills/codebase-layout/SKILL.md +2 -0
  19. package/claude/skills/context-fold/SKILL.md +3 -17
  20. package/claude/skills/context-fold/references/completion-report.md +26 -0
  21. package/claude/skills/create-rule/SKILL.md +2 -0
  22. package/claude/skills/create-skill/SKILL.md +2 -0
  23. package/claude/skills/create-standard/SKILL.md +2 -0
  24. package/claude/skills/decision-escalate/SKILL.md +2 -0
  25. package/claude/skills/deploy-app/SKILL.md +2 -0
  26. package/claude/skills/deprecation-migration/SKILL.md +2 -0
  27. package/claude/skills/design-extract/SKILL.md +2 -0
  28. package/claude/skills/design-taste/SKILL.md +2 -0
  29. package/claude/skills/docs-sync/SKILL.md +2 -0
  30. package/claude/skills/document-health/SKILL.md +2 -0
  31. package/claude/skills/draft-and-pick/SKILL.md +2 -0
  32. package/claude/skills/draft-doc/SKILL.md +2 -0
  33. package/claude/skills/draft-doc/references/wiki.md +1 -1
  34. package/claude/skills/draft-figure/SKILL.md +2 -0
  35. package/claude/skills/draft-identity/SKILL.md +2 -0
  36. package/claude/skills/draft-ready/SKILL.md +2 -0
  37. package/claude/skills/draft-screencast/SKILL.md +2 -0
  38. package/claude/skills/draft-slides/SKILL.md +2 -0
  39. package/claude/skills/git-branch/SKILL.md +2 -0
  40. package/claude/skills/git-commit/SKILL.md +2 -0
  41. package/claude/skills/git-followup/SKILL.md +2 -0
  42. package/claude/skills/git-issue/SKILL.md +2 -0
  43. package/claude/skills/git-pr/SKILL.md +2 -0
  44. package/claude/skills/git-pr/references/evidence.md +6 -6
  45. package/claude/skills/git-ship/SKILL.md +2 -0
  46. package/claude/skills/git-split/SKILL.md +2 -0
  47. package/claude/skills/git-stage/SKILL.md +2 -0
  48. package/claude/skills/git-worktree/SKILL.md +2 -0
  49. package/claude/skills/index-lookup/SKILL.md +2 -0
  50. package/claude/skills/markdown-propose/SKILL.md +2 -0
  51. package/claude/skills/memory-capture/SKILL.md +2 -0
  52. package/claude/skills/memory-review/SKILL.md +2 -0
  53. package/claude/skills/plan-feature/SKILL.md +2 -0
  54. package/claude/skills/plan-groundwork/SKILL.md +2 -0
  55. package/claude/skills/plan-intake/SKILL.md +2 -0
  56. package/claude/skills/plan-intake-answer/SKILL.md +2 -0
  57. package/claude/skills/project-commands/SKILL.md +2 -0
  58. package/claude/skills/read-frames/SKILL.md +2 -0
  59. package/claude/skills/record-screencast/SKILL.md +2 -0
  60. package/claude/skills/repo-metadata/SKILL.md +2 -0
  61. package/claude/skills/restate-plainly/SKILL.md +2 -0
  62. package/claude/skills/review-address/SKILL.md +3 -7
  63. package/claude/skills/review-address/references/post-review-findings.md +16 -0
  64. package/claude/skills/review-branch/SKILL.md +2 -0
  65. package/claude/skills/review-craft/SKILL.md +2 -0
  66. package/claude/skills/review-craft/references/ui.md +2 -2
  67. package/claude/skills/review-pr/SKILL.md +4 -8
  68. package/claude/skills/review-pr/references/body-variants.md +52 -0
  69. package/claude/skills/review-pr/references/key-changes.md +16 -0
  70. package/claude/skills/review-pr/references/post.md +15 -43
  71. package/claude/skills/review-ui/SKILL.md +3 -1
  72. package/claude/skills/role-orchestrator/SKILL.md +5 -14
  73. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
  74. package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +23 -0
  75. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +5 -33
  76. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -21
  77. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +1 -1
  78. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +38 -0
  79. package/claude/skills/role-orchestrator/references/orchestrator-watch.md +26 -0
  80. package/claude/skills/role-planner/SKILL.md +2 -0
  81. package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
  82. package/claude/skills/role-reviewer/SKILL.md +2 -0
  83. package/claude/skills/role-worker/SKILL.md +2 -0
  84. package/claude/skills/search-craft/SKILL.md +2 -0
  85. package/claude/skills/seed-sync/SKILL.md +2 -0
  86. package/claude/skills/session-compact/SKILL.md +2 -0
  87. package/claude/skills/session-map/SKILL.md +2 -0
  88. package/claude/skills/session-relay/SKILL.md +2 -0
  89. package/claude/skills/session-resume/SKILL.md +2 -0
  90. package/claude/skills/session-worktree/SKILL.md +3 -11
  91. package/claude/skills/session-worktree/references/install-command.md +20 -0
  92. package/claude/skills/sketch-design/SKILL.md +2 -0
  93. package/claude/skills/systematic-debugging/SKILL.md +2 -0
  94. package/claude/skills/target-check/SKILL.md +2 -0
  95. package/claude/skills/target-setup/SKILL.md +2 -0
  96. package/claude/skills/task-board/SKILL.md +2 -0
  97. package/claude/skills/teach-workspace/SKILL.md +5 -15
  98. package/claude/skills/teach-workspace/references/lesson-build.md +75 -0
  99. package/claude/skills/teach-workspace/references/lesson-craft.md +13 -69
  100. package/claude/skills/teach-workspace/references/revisit.md +22 -0
  101. package/claude/skills/test-craft/SKILL.md +2 -0
  102. package/claude/skills/test-first/SKILL.md +2 -0
  103. package/claude/skills/ui-checklist/SKILL.md +2 -0
  104. package/claude/skills/ux-audit/SKILL.md +2 -0
  105. package/claude/skills/ux-measure/SKILL.md +2 -0
  106. package/claude/skills/ux-walkthrough/SKILL.md +2 -0
  107. package/claude/skills/video-craft/SKILL.md +2 -0
  108. package/claude/skills/write-human/SKILL.md +2 -0
  109. package/claude/skills/youtube-transcripts/SKILL.md +2 -0
  110. package/docs/agents/canvas.md +23 -7
  111. package/docs/agents/capture.md +5 -5
  112. package/docs/agents/commands.md +2 -2
  113. package/docs/agents/index.md +3 -3
  114. package/docs/agents/install-and-sync.md +7 -0
  115. package/docs/agents/key-changes.md +1 -1
  116. package/docs/agents/pr-evidence.md +38 -15
  117. package/docs/agents/pr-local.md +19 -13
  118. package/docs/agents/pr-preview.md +44 -21
  119. package/docs/agents/sandbox.md +28 -4
  120. package/docs/agents/scripting.md +8 -1
  121. package/docs/agents/skills-audit.md +2 -1
  122. package/docs/workflow/operating-model.md +5 -6
  123. package/docs/workflow/parallel-features.md +22 -1
  124. package/docs/workflow/skill-map.md +3 -1
  125. package/docs/workflow/zshrc-aliases.md +1 -1
  126. package/package.json +1 -1
  127. package/scripts/core/regen-hero.ts +1 -1
  128. package/scripts/lib/sandbox-path.sh +6 -162
  129. package/src/audits/run.ts +1 -1
  130. package/src/canvas/address.ts +12 -3
  131. package/src/canvas/client/inspector/align-grid.tsx +162 -0
  132. package/src/canvas/client/inspector/color-field.tsx +201 -171
  133. package/src/canvas/client/inspector/color-inputs.tsx +139 -0
  134. package/src/canvas/client/inspector/color-picker.tsx +668 -0
  135. package/src/canvas/client/inspector/color-space.ts +240 -0
  136. package/src/canvas/client/inspector/field.tsx +5 -1
  137. package/src/canvas/client/inspector/fields.css +395 -6
  138. package/src/canvas/client/inspector/size-field.tsx +69 -0
  139. package/src/canvas/client/inspector/values.ts +43 -1
  140. package/src/canvas/client/inspector.tsx +190 -4
  141. package/src/canvas/client/selection.tsx +197 -0
  142. package/src/canvas/client/shell.css +104 -12
  143. package/src/canvas/client/state.ts +69 -9
  144. package/src/canvas/client/surface.tsx +147 -22
  145. package/src/canvas/content.ts +50 -1
  146. package/src/canvas/edit.ts +37 -4
  147. package/src/canvas/editing.ts +203 -0
  148. package/src/canvas/global.d.ts +15 -0
  149. package/src/canvas/server.ts +149 -12
  150. package/src/canvas/tokens.ts +59 -17
  151. package/src/census/count.ts +1 -1
  152. package/src/claude/seeds.ts +1 -1
  153. package/src/claude/skills-audit.ts +28 -0
  154. package/src/claude/skills-drift.ts +1 -1
  155. package/src/claude/skills-families.ts +36 -0
  156. package/src/claude/skills-list.ts +76 -15
  157. package/src/commands/audits.ts +2 -2
  158. package/src/commands/canvas.ts +179 -2
  159. package/src/commands/claude.ts +67 -9
  160. package/src/commands/design.ts +142 -2
  161. package/src/commands/gov.ts +1 -1
  162. package/src/commands/hooks.ts +1 -1
  163. package/src/commands/indexes.ts +1 -1
  164. package/src/commands/init.ts +1 -1
  165. package/src/commands/intake.ts +1 -1
  166. package/src/commands/migrate.ts +1 -1
  167. package/src/commands/pr.ts +105 -58
  168. package/src/commands/records.ts +1 -1
  169. package/src/commands/repo.ts +1 -1
  170. package/src/commands/sandbox.ts +54 -2
  171. package/src/commands/sessions.ts +1 -1
  172. package/src/commands/sync.ts +1 -1
  173. package/src/commands/tasks.ts +2 -2
  174. package/src/commands/teach.ts +1 -1
  175. package/src/commands/transcripts.ts +1 -1
  176. package/src/commands/worktrees.ts +1 -1
  177. package/src/comments/scan.ts +1 -1
  178. package/src/comments/trend.ts +1 -1
  179. package/src/context/citations.ts +1 -1
  180. package/src/context/classify/extract.ts +2 -2
  181. package/src/design/project-fonts.ts +392 -0
  182. package/src/feedback/github.ts +1 -1
  183. package/src/gate/measures.ts +47 -9
  184. package/src/gate/sequencer.ts +1 -1
  185. package/src/gate/stages.ts +9 -0
  186. package/src/{git-files.ts → git/files.ts} +2 -2
  187. package/src/{git-ignore.ts → git/ignore.ts} +1 -1
  188. package/src/{worktree.ts → git/worktree.ts} +1 -1
  189. package/src/gov/citations.ts +1 -1
  190. package/src/gov/install.ts +1 -1
  191. package/src/gov/superseded.ts +1 -1
  192. package/src/gov/test-order.ts +2 -2
  193. package/src/help.ts +1 -1
  194. package/src/hooks/post-merge.ts +1 -1
  195. package/src/indexes/walk.ts +1 -1
  196. package/src/labels/audit.ts +1 -1
  197. package/src/markdown/files.ts +1 -1
  198. package/src/migrate/apply.ts +1 -1
  199. package/src/pr/bijection.ts +1 -1
  200. package/src/pr/evidence.ts +96 -43
  201. package/src/pr/local.ts +16 -10
  202. package/src/pr/preview.ts +78 -2
  203. package/src/records/backup.ts +1 -1
  204. package/src/records/stale-canonical.ts +1 -1
  205. package/src/sandbox/headless/dispatch.ts +264 -0
  206. package/src/sandbox/headless/run.ts +653 -0
  207. package/src/sandbox/headless/watch.ts +145 -0
  208. package/src/sandbox/provision.ts +12 -1
  209. package/src/sandbox/tree.ts +4 -8
  210. package/src/secrets/scan.ts +1 -1
  211. package/src/sessions/claim.ts +1 -1
  212. package/src/sessions/resolve.ts +1 -1
  213. package/src/sessions/transfer/bundle.ts +1 -1
  214. package/src/slides/package/fonts.ts +6 -2
  215. package/src/standards/audit.ts +2 -2
  216. package/src/sync/check.ts +2 -2
  217. package/src/sync/engine.ts +2 -2
  218. package/src/sync/git.ts +1 -1
  219. package/src/sync/history.ts +1 -1
  220. package/src/sync/reverse.ts +1 -1
  221. package/src/sync/target.ts +1 -1
  222. package/src/sync/workflow.ts +1 -1
  223. package/src/targets/pulls.ts +2 -2
  224. package/src/targets/sweep.ts +2 -2
  225. package/src/tasks/reach.ts +1 -1
  226. package/src/tasks/trunk.ts +1 -1
  227. package/src/tooling/inject.ts +1 -1
  228. package/src/tooling/subfolder.ts +1 -1
  229. package/src/web/readme-citations.ts +1 -1
  230. package/src/worktrees/bare-flag.ts +1 -1
  231. package/src/worktrees/reclaim.ts +2 -2
  232. package/src/worktrees/remove.ts +1 -1
  233. package/standards/canvas.md +2 -1
  234. package/standards/skill.md +1 -0
  235. package/standards/wiki.md +12 -3
  236. package/tooling/base/configs/.github/workflows/pr-local-preview.yml +2 -2
  237. package/scripts/lib/sandbox-dispatch.sh +0 -190
  238. /package/src/{client-commands.ts → gate/client-commands.ts} +0 -0
  239. /package/src/{git-base.ts → git/base.ts} +0 -0
  240. /package/src/{git-env.ts → git/env.ts} +0 -0
  241. /package/src/{copy.ts → sync/copy.ts} +0 -0
  242. /package/src/{target.ts → targets/validate.ts} +0 -0
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
3
  import { CORPORA } from '@/claude/skills-audit'
4
+ import { isFamilyKey, SKILL_FAMILIES } from '@/claude/skills-families'
4
5
 
5
6
  const FRONTMATTER = /^---\n([\s\S]*?)\n---/
6
7
 
@@ -8,6 +9,11 @@ export interface SkillListing {
8
9
  readonly name: string
9
10
  readonly description: string
10
11
  readonly requirement: boolean
12
+ /**
13
+ * The `metadata.family` the frontmatter declares, read as written. Whether it
14
+ * names a vocabulary key is the audit's finding, not the listing's.
15
+ */
16
+ readonly family: string | null
11
17
  }
12
18
 
13
19
  export interface SkillsCorpus {
@@ -72,28 +78,83 @@ export function listSkillsAt(skillsRoot: string): SkillListing[] {
72
78
  }),
73
79
  ].sort()
74
80
 
75
- return paths.map((path) => ({
76
- name: dirname(path),
77
- description: readDescription(join(skillsRoot, path)),
78
- requirement: existsSync(join(skillsRoot, dirname(path), 'REQUIREMENT.md')),
79
- }))
81
+ return paths.map((path) => {
82
+ const { description, family } = readListedFields(join(skillsRoot, path))
83
+ return {
84
+ name: dirname(path),
85
+ description,
86
+ requirement: existsSync(
87
+ join(skillsRoot, dirname(path), 'REQUIREMENT.md'),
88
+ ),
89
+ family,
90
+ }
91
+ })
92
+ }
93
+
94
+ export interface FamilyGroup {
95
+ readonly key: string
96
+ readonly group: string
97
+ readonly skills: readonly string[]
98
+ }
99
+
100
+ export interface FamilyGrouping {
101
+ /** Vocabulary order, holding only the families some listing declares. */
102
+ readonly groups: readonly FamilyGroup[]
103
+ /** Listings whose family is missing or names no vocabulary key. */
104
+ readonly unassigned: readonly string[]
105
+ }
106
+
107
+ export function groupByFamily(
108
+ listings: readonly SkillListing[],
109
+ ): FamilyGrouping {
110
+ const groups = SKILL_FAMILIES.map((family) => ({
111
+ ...family,
112
+ skills: listings
113
+ .filter((listing) => listing.family === family.key)
114
+ .map((listing) => listing.name),
115
+ })).filter((group) => group.skills.length > 0)
116
+
117
+ return {
118
+ groups,
119
+ unassigned: listings
120
+ .filter(
121
+ (listing) => listing.family === null || !isFamilyKey(listing.family),
122
+ )
123
+ .map((listing) => listing.name),
124
+ }
125
+ }
126
+
127
+ interface ListedFields {
128
+ readonly description: string
129
+ readonly family: string | null
80
130
  }
81
131
 
82
132
  /**
83
- * Returns an empty description rather than throwing on a skill whose
84
- * frontmatter is missing or unparseable, so one malformed file does not hide
85
- * the rest of the catalog from a caller counting it.
133
+ * Returns an empty description and a null family rather than throwing on a
134
+ * skill whose frontmatter is missing or unparseable, so one malformed file
135
+ * does not hide the rest of the catalog from a caller counting it.
86
136
  */
87
- function readDescription(path: string): string {
137
+ function readListedFields(path: string): ListedFields {
138
+ const empty = { description: '', family: null }
88
139
  const match = FRONTMATTER.exec(readFileSync(path, 'utf8'))
89
- if (!match) return ''
140
+ if (!match) return empty
90
141
 
142
+ let parsed: unknown
91
143
  try {
92
- const parsed = Bun.YAML.parse(match[1])
93
- if (typeof parsed !== 'object' || parsed === null) return ''
94
- const { description } = parsed as Record<string, unknown>
95
- return typeof description === 'string' ? description : ''
144
+ parsed = Bun.YAML.parse(match[1])
96
145
  } catch {
97
- return ''
146
+ return empty
98
147
  }
148
+ if (!isRecord(parsed)) return empty
149
+
150
+ const { description, metadata } = parsed
151
+ const family = isRecord(metadata) ? metadata.family : undefined
152
+ return {
153
+ description: typeof description === 'string' ? description : '',
154
+ family: typeof family === 'string' ? family : null,
155
+ }
156
+ }
157
+
158
+ function isRecord(value: unknown): value is Record<string, unknown> {
159
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
99
160
  }
@@ -19,9 +19,9 @@ import {
19
19
  type Summary,
20
20
  summarize,
21
21
  } from '@/audits/run'
22
- import { gitEnv } from '@/git-env'
22
+ import { gitEnv } from '@/git/env'
23
23
  import { intro, logError, logInfo, logStep, logWarn, outro, plural } from '@/ui'
24
- import { currentWorktreeRoot } from '@/worktree'
24
+ import { currentWorktreeRoot } from '@/git/worktree'
25
25
 
26
26
  interface RunCommandOptions {
27
27
  readonly json?: boolean
@@ -12,6 +12,7 @@ import {
12
12
  readPage,
13
13
  readSelection,
14
14
  renamePage,
15
+ resizeFrame,
15
16
  } from '@/canvas/content'
16
17
  import { captureCanvas, captureComposite } from '@/canvas/capture'
17
18
  import {
@@ -19,10 +20,22 @@ import {
19
20
  editFrameAtIndex,
20
21
  STYLE_PROPERTIES,
21
22
  } from '@/canvas/edit'
23
+ import {
24
+ clearEditing,
25
+ EDITING_TTL_MS,
26
+ markEditing,
27
+ readEditing,
28
+ } from '@/canvas/editing'
22
29
  import { CANVAS_PORT, startCanvas } from '@/canvas/server'
23
30
  import { PROJECT_ROOT } from '@/project-root'
24
31
  import { resolveFrameTokens } from '@/canvas/tokens'
25
32
  import { displayPath, parsePort, waitForInterrupt } from '@/serve/report'
33
+ import {
34
+ callerIdentity,
35
+ type Located,
36
+ resolveSessions,
37
+ selfOf,
38
+ } from '@/sessions/resolve'
26
39
  import {
27
40
  intro,
28
41
  logAdd,
@@ -33,7 +46,7 @@ import {
33
46
  outro,
34
47
  plural,
35
48
  } from '@/ui'
36
- import { mainWorktreeRoot } from '@/worktree'
49
+ import { mainWorktreeRoot } from '@/git/worktree'
37
50
 
38
51
  const BANNER = 'canon canvas'
39
52
 
@@ -135,7 +148,7 @@ export function register(program: Command): void {
135
148
  .command('canvas')
136
149
  .helpOption('-h, --help', 'Show this help message')
137
150
  .description(
138
- 'Local canvas of pages and HTML frames (serve, list, page, frame, selection, edit, capture)',
151
+ 'Local canvas of pages and HTML frames (serve, list, page, frame, selection, edit, editing, capture)',
139
152
  )
140
153
  .addHelpText(
141
154
  'after',
@@ -152,6 +165,7 @@ export function register(program: Command): void {
152
165
  ' canon canvas frame move drafts hero --x 200 --y 120',
153
166
  ' canon canvas selection --json',
154
167
  " canon canvas edit drafts/hero --element 4 --set 'color=var(--color-accent)'",
168
+ ' canon canvas editing drafts/hero',
155
169
  ' canon canvas capture drafts/hero',
156
170
  ' canon canvas list --json',
157
171
  '',
@@ -341,6 +355,56 @@ export function register(program: Command): void {
341
355
  },
342
356
  )
343
357
 
358
+ withRoot(
359
+ frame
360
+ .command('resize')
361
+ .description('Resize a frame, and move it when given a position')
362
+ .argument('<page>', 'Page the frame is on')
363
+ .argument('<name>', 'Frame name')
364
+ .option('--width <px>', 'New width, defaulting to the frame width')
365
+ .option('--height <px>', 'New height, defaulting to the frame height')
366
+ .option('--x <px>', 'New x, defaulting to where the frame is')
367
+ .option('--y <px>', 'New y, defaulting to where the frame is'),
368
+ ).action(
369
+ async (
370
+ pageName: string,
371
+ name: string,
372
+ opts: RootOptions & {
373
+ width?: string
374
+ height?: string
375
+ x?: string
376
+ y?: string
377
+ },
378
+ ) => {
379
+ const resolved = await resolveRoot(opts)
380
+ if (!resolved.ok) {
381
+ process.exitCode = refuse(resolved, opts.json ?? false)
382
+ return
383
+ }
384
+ const { root } = resolved
385
+ const current = readPage(root, pageName)?.frames.find(
386
+ (candidate) => candidate.name === name,
387
+ )
388
+ const outcome = resizeFrame(root, pageName, name, {
389
+ x: parsePosition(opts.x, current?.x ?? 0),
390
+ y: parsePosition(opts.y, current?.y ?? 0),
391
+ width: parseSize(opts.width, current?.width ?? DEFAULT_FRAME.width),
392
+ height: parseSize(opts.height, current?.height ?? DEFAULT_FRAME.height),
393
+ })
394
+ if (!outcome.ok) {
395
+ process.exitCode = refuse(outcome, opts.json ?? false)
396
+ return
397
+ }
398
+ if (opts.json) writeJson(outcome)
399
+ intro(BANNER)
400
+ logAdd(`${outcome.page}/${outcome.file}`)
401
+ logInfo(
402
+ `${outcome.box.width} × ${outcome.box.height} at ${outcome.box.x}, ${outcome.box.y}`,
403
+ )
404
+ outro()
405
+ },
406
+ )
407
+
344
408
  withRoot(
345
409
  canvas
346
410
  .command('selection')
@@ -487,6 +551,91 @@ export function register(program: Command): void {
487
551
  },
488
552
  )
489
553
 
554
+ withRoot(
555
+ canvas
556
+ .command('editing')
557
+ .description('Mark a frame as being edited, clear it, or list the marks')
558
+ .argument('[target]', '<page>/<frame>, or none to list the marks')
559
+ .option('--done', 'Clear the mark once the edits are written')
560
+ .option('--by <name>', 'The label the mark shows, over the session name'),
561
+ )
562
+ .addHelpText(
563
+ 'after',
564
+ [
565
+ '',
566
+ 'Mark a frame before writing it and clear it with --done after, so an',
567
+ `open canvas shows who is editing it. A mark lapses after ${EDITING_TTL_MS / 60_000} minutes`,
568
+ 'unless marked again, so re-mark during a long edit. A mark gates no',
569
+ 'write, and --done on a frame holding no mark succeeds.',
570
+ '',
571
+ ].join('\n'),
572
+ )
573
+ .action(
574
+ async (
575
+ target: string | undefined,
576
+ opts: RootOptions & { done?: boolean; by?: string },
577
+ ) => {
578
+ const emitJson = opts.json ?? false
579
+ const resolved = await resolveRoot(opts)
580
+ if (!resolved.ok) {
581
+ process.exitCode = refuse(resolved, emitJson)
582
+ return
583
+ }
584
+ const { root } = resolved
585
+ const now = new Date()
586
+
587
+ if (target !== undefined) {
588
+ const [pageName, frameName, ...rest] = target.split('/')
589
+ if (!pageName || !frameName || rest.length > 0) {
590
+ process.exitCode = refuse(
591
+ {
592
+ ok: false,
593
+ reason: 'invalid-name',
594
+ detail: `${target} is not <page>/<frame>`,
595
+ },
596
+ emitJson,
597
+ )
598
+ return
599
+ }
600
+ const outcome = opts.done
601
+ ? clearEditing(root, pageName, frameName)
602
+ : markEditing(
603
+ root,
604
+ pageName,
605
+ frameName,
606
+ opts.by ?? (await sessionLabel()),
607
+ now,
608
+ )
609
+ if (!outcome.ok) {
610
+ process.exitCode = refuse(outcome, emitJson)
611
+ return
612
+ }
613
+ } else if (opts.done) {
614
+ process.exitCode = refuse(
615
+ {
616
+ ok: false,
617
+ reason: 'invalid-name',
618
+ detail: '--done needs the <page>/<frame> to clear',
619
+ },
620
+ emitJson,
621
+ )
622
+ return
623
+ }
624
+
625
+ const marks = readEditing(root, now)
626
+ if (emitJson) writeJson({ ok: true, editing: marks })
627
+ intro(BANNER)
628
+ if (target !== undefined) {
629
+ logStep(`${target} ${opts.done ? 'cleared' : 'marked'}`)
630
+ }
631
+ if (marks.length === 0) logInfo('No frame is marked as being edited')
632
+ for (const mark of marks) {
633
+ logInfo(`${mark.page}/${mark.frame} ${mark.by} until ${mark.until}`)
634
+ }
635
+ outro()
636
+ },
637
+ )
638
+
490
639
  withRoot(
491
640
  canvas
492
641
  .command('capture')
@@ -644,6 +793,29 @@ async function runComposite(
644
793
  return 0
645
794
  }
646
795
 
796
+ async function unlocated(): Promise<Located> {
797
+ return {
798
+ repository: null,
799
+ worktree: null,
800
+ branch: null,
801
+ unresolved: 'git-unavailable',
802
+ }
803
+ }
804
+
805
+ /**
806
+ * The caller's roster name, or a generic label when the environment names no
807
+ * session or the roster holds no row for it, as in a sandbox or a CI run.
808
+ */
809
+ async function sessionLabel(): Promise<string> {
810
+ const identity = callerIdentity()
811
+ if (identity.sessionId === null && identity.pid === null) return 'a session'
812
+ /* The label needs the name alone, so skip the git probe each row's branch costs. */
813
+ const report = await resolveSessions({ locate: unlocated })
814
+ if (report.kind === 'absent') return 'a session'
815
+ const self = selfOf(report.sessions, identity)
816
+ return self.kind === 'self' ? self.session.name : 'a session'
817
+ }
818
+
647
819
  /** NaN for a value that is not a number, which the content module refuses. */
648
820
  function parsePosition(raw: string | undefined, fallback: number): number {
649
821
  if (raw === undefined) return fallback
@@ -698,6 +870,11 @@ async function runServe(
698
870
  if (tokens.source === 'none') logWarn(tokens.notice)
699
871
  else if (tokens.source === 'installed') {
700
872
  for (const file of tokens.files) logInfo(file)
873
+ for (const face of tokens.dropped ?? []) {
874
+ logWarn(
875
+ `${face.sheet}: dropped the face at ${face.file} (${face.reason})`,
876
+ )
877
+ }
701
878
  } else logInfo('this toolkit’s own token module')
702
879
  logStep('Stop')
703
880
  logInfo('Ctrl-C')
@@ -5,7 +5,7 @@ import { join, resolve } from 'node:path'
5
5
  import type { Command } from 'commander'
6
6
  import { execa } from 'execa'
7
7
  import { singleLine } from '@/commands/upgrade'
8
- import { gitEnv } from '@/git-env'
8
+ import { gitEnv } from '@/git/env'
9
9
  import { claudeChain, pendingEntries, planGitignore } from '@/claude/gitignore'
10
10
  import {
11
11
  matchInstall,
@@ -44,7 +44,12 @@ import {
44
44
  scanRouting,
45
45
  } from '@/claude/routing'
46
46
  import { type DriftReport, readDrift } from '@/claude/skills-drift'
47
- import { listSkills } from '@/claude/skills-list'
47
+ import { familyKeys, SKILL_FAMILIES } from '@/claude/skills-families'
48
+ import {
49
+ type FamilyGrouping,
50
+ groupByFamily,
51
+ listSkills,
52
+ } from '@/claude/skills-list'
48
53
  import {
49
54
  type ReachRefusal,
50
55
  type ReachReport,
@@ -64,12 +69,12 @@ import {
64
69
  serializeSettings,
65
70
  writeSettings,
66
71
  } from '@/claude/settings'
67
- import { copyPreservingMode } from '@/copy'
72
+ import { copyPreservingMode } from '@/sync/copy'
68
73
  import { execScript } from '@/exec'
69
74
  import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project-root'
70
75
  import { recordDir } from '@/record-root'
71
76
  import { SURFACE_ENTRIES, surfaceDir } from '@/surface-root'
72
- import { isDirectory, resolveTarget } from '@/target'
77
+ import { isDirectory, resolveTarget } from '@/targets/validate'
73
78
  import { injectGitignore, pruneGitignore } from '@/tooling/inject'
74
79
  import {
75
80
  frameError,
@@ -96,6 +101,7 @@ interface SeedsListOptions {
96
101
  interface SkillsListOptions {
97
102
  readonly json?: boolean
98
103
  readonly names?: boolean
104
+ readonly byFamily?: boolean
99
105
  }
100
106
 
101
107
  interface SkillsAuditOptions {
@@ -359,8 +365,12 @@ export function register(program: Command): void {
359
365
  .command('list')
360
366
  .description('List the plugin skills shipped under claude/skills/')
361
367
  .helpOption('-h, --help', 'Show this help message')
362
- .option('--json', 'Emit JSON with name and description')
368
+ .option(
369
+ '--json',
370
+ 'Emit JSON with each skill and the ordered family vocabulary',
371
+ )
363
372
  .option('--names', 'Only list skill names, one per line')
373
+ .option('--by-family', 'Group the listing under each family')
364
374
  .addHelpText(
365
375
  'after',
366
376
  [
@@ -368,6 +378,8 @@ export function register(program: Command): void {
368
378
  'Notes:',
369
379
  ' Internal skills under .claude/skills/ are excluded, since they',
370
380
  ' never install into a target project.',
381
+ ' Each skill carries the family its frontmatter declares under',
382
+ ' metadata.family, and --json lists the vocabulary as families.',
371
383
  '',
372
384
  ].join('\n'),
373
385
  )
@@ -770,7 +782,9 @@ function runSkillsList(opts: SkillsListOptions): number {
770
782
 
771
783
  if (opts.json) {
772
784
  if (mismatch !== undefined) logWarn(mismatch)
773
- process.stdout.write(`${JSON.stringify({ skills: listings })}\n`)
785
+ process.stdout.write(
786
+ `${JSON.stringify({ skills: listings, families: SKILL_FAMILIES })}\n`,
787
+ )
774
788
  return 0
775
789
  }
776
790
 
@@ -784,14 +798,34 @@ function runSkillsList(opts: SkillsListOptions): number {
784
798
 
785
799
  intro('canon claude')
786
800
  if (mismatch !== undefined) logWarn(mismatch)
787
- logStep('Plugin skills')
788
- for (const listing of listings) {
789
- logInfo(listing.name)
801
+ if (opts.byFamily) {
802
+ reportByFamily(groupByFamily(listings))
803
+ } else {
804
+ logStep('Plugin skills')
805
+ for (const listing of listings) {
806
+ logInfo(listing.name)
807
+ }
790
808
  }
791
809
  outro()
792
810
  return 0
793
811
  }
794
812
 
813
+ /**
814
+ * One step per family in vocabulary order. A skill with a missing or unknown
815
+ * family takes a trailing step rather than vanishing, so the view still
816
+ * accounts for every listed skill.
817
+ */
818
+ function reportByFamily(grouping: FamilyGrouping): void {
819
+ for (const group of grouping.groups) {
820
+ logStep(`${group.group} (${group.key})`)
821
+ for (const name of group.skills) logInfo(name)
822
+ }
823
+
824
+ if (grouping.unassigned.length === 0) return
825
+ logStep('No known family')
826
+ for (const name of grouping.unassigned) logWarn(name)
827
+ }
828
+
795
829
  /**
796
830
  * Measures the cwd for the same reason the audit does, and takes the ref as a
797
831
  * required argument with no default. `HEAD` would be the only defensible one and
@@ -1402,6 +1436,7 @@ async function runSkillsAudit(
1402
1436
  reportProvenance(report)
1403
1437
  reportPracticeShape(report)
1404
1438
  reportReferenceContents(report)
1439
+ reportFamily(report)
1405
1440
  reportUnmeasured()
1406
1441
  outro()
1407
1442
  }
@@ -1426,6 +1461,7 @@ async function runSkillsAudit(
1426
1461
  datedProvenance: report.datedProvenance,
1427
1462
  practiceShape: report.practiceShape,
1428
1463
  referenceContents: report.referenceContents,
1464
+ family: report.family,
1429
1465
  },
1430
1466
  checkpoints: {
1431
1467
  descriptionLimit: DESCRIPTION_LIMIT,
@@ -1435,6 +1471,7 @@ async function runSkillsAudit(
1435
1471
  ledgerSkills: LEDGER_SKILLS,
1436
1472
  practiceSections: PRACTICE_SECTIONS,
1437
1473
  contentsThreshold: CONTENTS_THRESHOLD,
1474
+ families: familyKeys(),
1438
1475
  },
1439
1476
  })}\n`,
1440
1477
  )
@@ -1668,6 +1705,27 @@ function reportReferenceContents(report: SkillsAudit): void {
1668
1705
  reportFindings(report.referenceContents)
1669
1706
  }
1670
1707
 
1708
+ /**
1709
+ * Reports without failing the verb, the split the practice shape takes. The
1710
+ * Skill family stage of `canon gate run` is what fails a push on it.
1711
+ */
1712
+ function reportFamily(report: SkillsAudit): void {
1713
+ logStep('Family')
1714
+ logInfo(
1715
+ `A skill under ${SHIPPED_CORPUS} declares metadata.family, one of ${familyKeys().join(', ')}.`,
1716
+ )
1717
+
1718
+ if (report.family.length === 0) {
1719
+ logInfo('Every shipped skill declares a known family.')
1720
+ return
1721
+ }
1722
+
1723
+ logWarn(
1724
+ `${plural(report.family.length, 'skill')} with a missing or unknown family`,
1725
+ )
1726
+ reportFindings(report.family)
1727
+ }
1728
+
1671
1729
  /**
1672
1730
  * Stated on every run, including the run where everything above passed. A
1673
1731
  * report that lists only what it measured reads as a verdict on the standard