@erclx/canon 5.26.0 → 5.28.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 (320) 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 +5 -1
  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 +7 -7
  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/REQUIREMENT.md +4 -2
  63. package/claude/skills/review-address/SKILL.md +19 -14
  64. package/claude/skills/review-address/references/post-review-findings.md +16 -0
  65. package/claude/skills/review-address/references/rebase-conflicts.md +4 -2
  66. package/claude/skills/review-branch/SKILL.md +2 -0
  67. package/claude/skills/review-craft/SKILL.md +2 -0
  68. package/claude/skills/review-craft/references/ui.md +6 -6
  69. package/claude/skills/review-pr/SKILL.md +4 -8
  70. package/claude/skills/review-pr/references/body-variants.md +52 -0
  71. package/claude/skills/review-pr/references/key-changes.md +16 -0
  72. package/claude/skills/review-pr/references/post.md +15 -43
  73. package/claude/skills/review-ui/REQUIREMENT.md +2 -2
  74. package/claude/skills/review-ui/SKILL.md +16 -12
  75. package/claude/skills/review-ui/references/driving.md +8 -8
  76. package/claude/skills/review-ui/references/post.md +13 -3
  77. package/claude/skills/review-ui/references/tick.md +22 -0
  78. package/claude/skills/role-orchestrator/REQUIREMENT.md +15 -15
  79. package/claude/skills/role-orchestrator/SKILL.md +31 -42
  80. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +18 -16
  81. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +13 -3
  82. package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +23 -0
  83. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +6 -34
  84. package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -4
  85. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +5 -5
  86. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +12 -23
  87. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +6 -6
  88. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +1 -1
  89. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +38 -0
  90. package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +2 -2
  91. package/claude/skills/role-orchestrator/references/orchestrator-watch.md +26 -0
  92. package/claude/skills/role-orchestrator/scripts/poll.ts +304 -10
  93. package/claude/skills/role-planner/REQUIREMENT.md +1 -1
  94. package/claude/skills/role-planner/SKILL.md +4 -2
  95. package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
  96. package/claude/skills/role-reviewer/SKILL.md +3 -1
  97. package/claude/skills/role-worker/SKILL.md +5 -2
  98. package/claude/skills/search-craft/SKILL.md +2 -0
  99. package/claude/skills/seed-sync/SKILL.md +2 -0
  100. package/claude/skills/session-compact/SKILL.md +2 -0
  101. package/claude/skills/session-map/SKILL.md +2 -0
  102. package/claude/skills/session-relay/SKILL.md +2 -0
  103. package/claude/skills/session-resume/SKILL.md +2 -0
  104. package/claude/skills/session-worktree/SKILL.md +3 -11
  105. package/claude/skills/session-worktree/references/install-command.md +20 -0
  106. package/claude/skills/sketch-design/SKILL.md +2 -0
  107. package/claude/skills/systematic-debugging/SKILL.md +2 -0
  108. package/claude/skills/target-check/SKILL.md +2 -0
  109. package/claude/skills/target-setup/SKILL.md +2 -0
  110. package/claude/skills/task-board/SKILL.md +2 -0
  111. package/claude/skills/teach-workspace/SKILL.md +5 -15
  112. package/claude/skills/teach-workspace/references/lesson-build.md +75 -0
  113. package/claude/skills/teach-workspace/references/lesson-craft.md +13 -69
  114. package/claude/skills/teach-workspace/references/revisit.md +22 -0
  115. package/claude/skills/test-craft/SKILL.md +2 -0
  116. package/claude/skills/test-first/SKILL.md +2 -0
  117. package/claude/skills/ui-checklist/SKILL.md +3 -1
  118. package/claude/skills/ux-audit/SKILL.md +2 -0
  119. package/claude/skills/ux-measure/SKILL.md +2 -0
  120. package/claude/skills/ux-walkthrough/SKILL.md +2 -0
  121. package/claude/skills/video-craft/SKILL.md +2 -0
  122. package/claude/skills/write-human/SKILL.md +2 -0
  123. package/claude/skills/youtube-transcripts/SKILL.md +2 -0
  124. package/docs/agents/canvas.md +24 -8
  125. package/docs/agents/capture.md +5 -5
  126. package/docs/agents/commands.md +2 -2
  127. package/docs/agents/gate.md +3 -1
  128. package/docs/agents/index.md +5 -4
  129. package/docs/agents/install-and-sync.md +7 -0
  130. package/docs/agents/key-changes.md +1 -1
  131. package/docs/agents/pr-evidence.md +47 -23
  132. package/docs/agents/pr-local.md +19 -13
  133. package/docs/agents/pr-preview.md +44 -21
  134. package/docs/agents/pr-tick.md +72 -0
  135. package/docs/agents/sandbox.md +30 -6
  136. package/docs/agents/scripting.md +8 -1
  137. package/docs/agents/skills-audit.md +2 -1
  138. package/docs/agents/tasks-plans.md +1 -1
  139. package/docs/agents/tasks-validate.md +18 -18
  140. package/docs/agents/tasks.md +1 -1
  141. package/docs/workflow/operating-model.md +15 -21
  142. package/docs/workflow/parallel-features.md +24 -3
  143. package/docs/workflow/skill-map.md +3 -1
  144. package/docs/workflow/zshrc-aliases.md +1 -1
  145. package/package.json +1 -1
  146. package/scripts/core/regen-hero.ts +1 -1
  147. package/scripts/lib/sandbox-path.sh +6 -162
  148. package/src/audits/baseline.ts +1 -1
  149. package/src/audits/run.ts +2 -2
  150. package/src/canvas/address.ts +12 -3
  151. package/src/canvas/capture.ts +1 -1
  152. package/src/canvas/client/inspector/align-grid.tsx +162 -0
  153. package/src/canvas/client/inspector/color-field.tsx +201 -171
  154. package/src/canvas/client/inspector/color-inputs.tsx +139 -0
  155. package/src/canvas/client/inspector/color-picker.tsx +668 -0
  156. package/src/canvas/client/inspector/color-space.ts +240 -0
  157. package/src/canvas/client/inspector/field.tsx +66 -3
  158. package/src/canvas/client/inspector/fields.css +473 -6
  159. package/src/canvas/client/inspector/size-field.tsx +69 -0
  160. package/src/canvas/client/inspector/token-select.tsx +109 -0
  161. package/src/canvas/client/inspector/values.ts +117 -3
  162. package/src/canvas/client/inspector.tsx +311 -15
  163. package/src/canvas/client/keys.ts +54 -0
  164. package/src/canvas/client/selection.tsx +199 -0
  165. package/src/canvas/client/shell.css +210 -12
  166. package/src/canvas/client/state.ts +165 -14
  167. package/src/canvas/client/surface.tsx +341 -39
  168. package/src/canvas/client/tools.tsx +64 -0
  169. package/src/canvas/content.ts +51 -2
  170. package/src/canvas/edit.ts +49 -6
  171. package/src/canvas/editing.ts +203 -0
  172. package/src/canvas/global.d.ts +15 -0
  173. package/src/canvas/server.ts +149 -12
  174. package/src/canvas/tokens.ts +60 -18
  175. package/src/census/count.ts +1 -1
  176. package/src/claude/seeds.ts +3 -3
  177. package/src/claude/skills-audit.ts +28 -0
  178. package/src/claude/skills-drift.ts +1 -1
  179. package/src/claude/skills-families.ts +36 -0
  180. package/src/claude/skills-list.ts +76 -15
  181. package/src/claude/skills-reach.ts +1 -1
  182. package/src/commands/audits.ts +2 -2
  183. package/src/commands/canvas.ts +180 -3
  184. package/src/commands/claude.ts +70 -12
  185. package/src/commands/design.ts +145 -5
  186. package/src/commands/docs.ts +1 -1
  187. package/src/commands/feedback.ts +2 -2
  188. package/src/commands/gate.ts +9 -2
  189. package/src/commands/gov.ts +3 -3
  190. package/src/commands/hooks.ts +1 -1
  191. package/src/commands/indexes.ts +1 -1
  192. package/src/commands/init.ts +2 -2
  193. package/src/commands/intake.ts +1 -1
  194. package/src/commands/migrate.ts +2 -2
  195. package/src/commands/pr.ts +288 -65
  196. package/src/commands/records.ts +1 -1
  197. package/src/commands/repo.ts +1 -1
  198. package/src/commands/sandbox.ts +64 -15
  199. package/src/commands/sessions.ts +1 -1
  200. package/src/commands/slides.ts +1 -1
  201. package/src/commands/sync.ts +2 -2
  202. package/src/commands/tasks.ts +21 -4
  203. package/src/commands/teach.ts +1 -1
  204. package/src/commands/tooling.ts +1 -1
  205. package/src/commands/transcripts.ts +2 -2
  206. package/src/commands/upgrade.ts +1 -1
  207. package/src/commands/wiki.ts +1 -1
  208. package/src/commands/worktrees.ts +1 -1
  209. package/src/comments/scan.ts +1 -1
  210. package/src/comments/trend.ts +1 -1
  211. package/src/context/architecture.ts +1 -1
  212. package/src/context/citations.ts +3 -3
  213. package/src/context/classify/extract.ts +3 -3
  214. package/src/context/folders.ts +2 -2
  215. package/src/context/requirements.ts +1 -1
  216. package/src/design/project-fonts.ts +392 -0
  217. package/src/docs/read.ts +1 -1
  218. package/src/exec.ts +1 -1
  219. package/src/feedback/github.ts +2 -2
  220. package/src/gate/measures.ts +48 -10
  221. package/src/gate/sequencer.ts +56 -4
  222. package/src/gate/stages.ts +17 -0
  223. package/src/gate/suite-lock.ts +214 -0
  224. package/src/{git-files.ts → git/files.ts} +2 -2
  225. package/src/{git-ignore.ts → git/ignore.ts} +1 -1
  226. package/src/{worktree.ts → git/worktree.ts} +1 -1
  227. package/src/gov/citations.ts +1 -1
  228. package/src/gov/install.ts +1 -1
  229. package/src/gov/superseded.ts +1 -1
  230. package/src/gov/test-order.ts +2 -2
  231. package/src/help.ts +1 -1
  232. package/src/hooks/post-merge.ts +1 -1
  233. package/src/indexes/walk.ts +1 -1
  234. package/src/intake/folder.ts +1 -1
  235. package/src/labels/audit.ts +1 -1
  236. package/src/labels/map.ts +1 -1
  237. package/src/labels/phase.ts +3 -3
  238. package/src/markdown/files.ts +1 -1
  239. package/src/migrate/apply.ts +1 -1
  240. package/src/migrate/record-layout.ts +1 -1
  241. package/src/migrate/records.ts +11 -11
  242. package/src/migrate/scratch-evidence.ts +1 -1
  243. package/src/migrate/surface-roots.ts +6 -6
  244. package/src/pr/bijection.ts +1 -1
  245. package/src/pr/evidence.ts +250 -59
  246. package/src/pr/local.ts +16 -10
  247. package/src/pr/preview.ts +78 -2
  248. package/src/pr/tick.ts +56 -0
  249. package/src/process/harness.ts +1 -1
  250. package/src/records/backup.ts +2 -2
  251. package/src/records/ordinal.ts +1 -1
  252. package/src/records/prune.ts +1 -1
  253. package/src/records/size.ts +1 -1
  254. package/src/records/stale-canonical.ts +2 -2
  255. package/src/records/stale.ts +1 -1
  256. package/src/records/validate.ts +2 -2
  257. package/src/{project-root.ts → roots/project.ts} +1 -1
  258. package/src/{record-root.ts → roots/record.ts} +1 -1
  259. package/src/sandbox/coverage.ts +11 -12
  260. package/src/sandbox/equivalence.ts +57 -26
  261. package/src/sandbox/headless/dispatch.ts +264 -0
  262. package/src/sandbox/headless/run.ts +653 -0
  263. package/src/sandbox/headless/watch.ts +145 -0
  264. package/src/sandbox/hooks.ts +1 -1
  265. package/src/sandbox/provision.ts +90 -29
  266. package/src/sandbox/scenario.ts +588 -0
  267. package/src/sandbox/tree.ts +4 -8
  268. package/src/secrets/scan.ts +1 -1
  269. package/src/sessions/claim.ts +1 -1
  270. package/src/sessions/live.ts +4 -1
  271. package/src/sessions/resolve.ts +1 -1
  272. package/src/sessions/transfer/bundle.ts +2 -2
  273. package/src/slides/decks.ts +1 -1
  274. package/src/slides/package/fonts.ts +6 -2
  275. package/src/standards/audit.ts +2 -2
  276. package/src/standards/read.ts +1 -1
  277. package/src/sync/check.ts +2 -2
  278. package/src/sync/engine.ts +3 -3
  279. package/src/sync/git.ts +1 -1
  280. package/src/sync/history.ts +1 -1
  281. package/src/sync/layout.ts +2 -2
  282. package/src/sync/reverse.ts +1 -1
  283. package/src/sync/stamp.ts +1 -1
  284. package/src/sync/target.ts +1 -1
  285. package/src/sync/workflow.ts +1 -1
  286. package/src/targets/pulls.ts +2 -2
  287. package/src/targets/sweep.ts +2 -2
  288. package/src/{target.ts → targets/validate.ts} +1 -1
  289. package/src/tasks/answers.ts +1 -1
  290. package/src/tasks/archive.ts +1 -1
  291. package/src/tasks/label.ts +1 -1
  292. package/src/tasks/reach.ts +2 -2
  293. package/src/tasks/trunk.ts +1 -1
  294. package/src/tasks/validate.ts +38 -14
  295. package/src/teach/nav.ts +1 -1
  296. package/src/teach/workspace.ts +1 -1
  297. package/src/tooling/inject.ts +2 -2
  298. package/src/tooling/read.ts +1 -1
  299. package/src/tooling/scan.ts +1 -1
  300. package/src/tooling/subfolder.ts +1 -1
  301. package/src/version/installed.ts +1 -1
  302. package/src/version/skew.ts +1 -1
  303. package/src/web/readme-citations.ts +1 -1
  304. package/src/worktrees/bare-flag.ts +1 -1
  305. package/src/worktrees/reclaim.ts +2 -2
  306. package/src/worktrees/remove.ts +1 -1
  307. package/standards/board.md +3 -3
  308. package/standards/canvas.md +2 -1
  309. package/standards/plan.md +13 -7
  310. package/standards/skill.md +1 -0
  311. package/standards/wiki.md +12 -3
  312. package/tooling/base/configs/.github/workflows/pr-local-preview.yml +2 -2
  313. package/tsconfig.json +1 -1
  314. package/scripts/lib/sandbox-dispatch.sh +0 -190
  315. /package/src/{client-commands.ts → gate/client-commands.ts} +0 -0
  316. /package/src/{git-base.ts → git/base.ts} +0 -0
  317. /package/src/{git-env.ts → git/env.ts} +0 -0
  318. /package/src/{legacy-path.ts → roots/legacy.ts} +0 -0
  319. /package/src/{surface-root.ts → roots/surface.ts} +0 -0
  320. /package/src/{copy.ts → sync/copy.ts} +0 -0
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
+ import { isFamilyKey } from '@/claude/skills-families'
3
4
  import { parseFrontmatter, readField } from '@/indexes/frontmatter'
4
5
  import { bodyLines, maskDisplayed } from '@/markdown/scan'
5
6
 
@@ -128,6 +129,7 @@ export interface SkillsAudit {
128
129
  readonly datedProvenance: readonly SkillFinding[]
129
130
  readonly practiceShape: readonly SkillFinding[]
130
131
  readonly referenceContents: readonly SkillFinding[]
132
+ readonly family: readonly SkillFinding[]
131
133
  }
132
134
 
133
135
  interface SkillSource {
@@ -136,6 +138,8 @@ interface SkillSource {
136
138
  readonly hasReadme: boolean
137
139
  readonly name: string | undefined
138
140
  readonly description: string | undefined
141
+ /** `metadata.family`, undefined when absent, blank, or not a string. */
142
+ readonly family: string | undefined
139
143
  /** Undefined when the folder carries no `REQUIREMENT.md` at all. */
140
144
  readonly requirementHeadings: readonly string[] | undefined
141
145
  /** Every H2 in `SKILL.md` outside a fence, for the practice shape. */
@@ -207,6 +211,7 @@ export async function auditSkills(root: string): Promise<SkillsAudit> {
207
211
  referenceContents: sources.flatMap((source) =>
208
212
  source.bodies.flatMap(contentsFindings),
209
213
  ),
214
+ family: sources.flatMap(familyFindings),
210
215
  }
211
216
  }
212
217
 
@@ -261,6 +266,7 @@ async function readSkill(
261
266
  hasReadme: existsSync(join(skillDir, 'README.md')),
262
267
  name: declared(readField(fields, 'name')),
263
268
  description: declared(readField(fields, 'description')),
269
+ family: declaredFamily(fields?.fields.metadata),
264
270
  requirementHeadings:
265
271
  requirement === undefined ? undefined : headings(requirement),
266
272
  sections: sectionsOf(body),
@@ -285,6 +291,13 @@ function declared(value: string | undefined): string | undefined {
285
291
  return trimmed === undefined || trimmed === '' ? undefined : trimmed
286
292
  }
287
293
 
294
+ function declaredFamily(metadata: unknown): string | undefined {
295
+ if (typeof metadata !== 'object' || metadata === null) return undefined
296
+ if (Array.isArray(metadata)) return undefined
297
+ const { family } = metadata as Record<string, unknown>
298
+ return typeof family === 'string' ? declared(family) : undefined
299
+ }
300
+
288
301
  /** Every H2 outside a fence, the one level both exact-section checks read. */
289
302
  function sectionsOf(source: string): string[] {
290
303
  return bodyLines(source)
@@ -375,6 +388,21 @@ function practiceFindings(source: SkillSource): SkillFinding[] {
375
388
  return [...sections, ...ledger]
376
389
  }
377
390
 
391
+ /**
392
+ * Asks only the shipped corpus for a family, since the family names the skill
393
+ * map group a skill's row sits under and an internal skill takes no row.
394
+ */
395
+ function familyFindings(source: SkillSource): SkillFinding[] {
396
+ if (dirname(source.rel) !== SHIPPED_CORPUS) return []
397
+ if (source.family === undefined) {
398
+ return [
399
+ { rel: source.rel, detail: 'frontmatter declares no metadata.family' },
400
+ ]
401
+ }
402
+ if (isFamilyKey(source.family)) return []
403
+ return [{ rel: source.rel, detail: `unknown family: ${source.family}` }]
404
+ }
405
+
378
406
  /**
379
407
  * Reads references alone, since `SKILL.md` carries its own length checkpoint
380
408
  * and no contents rule. Frontmatter counts toward the length, so the count a
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import { execaSync } from 'execa'
4
- import { gitEnv } from '@/git-env'
4
+ import { gitEnv } from '@/git/env'
5
5
 
6
6
  /**
7
7
  * The shipped corpus alone. A session loads a body from the plugin root, which
@@ -0,0 +1,36 @@
1
+ export interface SkillFamily {
2
+ /** The value a shipped `SKILL.md` declares as `metadata.family`. */
3
+ readonly key: string
4
+ /** The skill map heading the family's rows sit under, verbatim. */
5
+ readonly group: string
6
+ }
7
+
8
+ /**
9
+ * The one vocabulary a shipped skill's family is drawn from, in the order the
10
+ * skill map reads across its two pages. A heading is the map's prose and a key
11
+ * is what frontmatter carries, so renaming a heading touches this constant and
12
+ * no skill.
13
+ *
14
+ * Import-free on purpose: `skills-list.ts` and `skills-audit.ts` both read it,
15
+ * and the first already imports the second.
16
+ */
17
+ export const SKILL_FAMILIES: readonly SkillFamily[] = [
18
+ { key: 'setup', group: 'Set up a project' },
19
+ { key: 'decide', group: 'Decide what to build' },
20
+ { key: 'build', group: 'Build the feature' },
21
+ { key: 'check', group: 'Check the work before it leaves the branch' },
22
+ { key: 'ship', group: 'Ship it' },
23
+ { key: 'after-pr', group: 'After the pull request opens' },
24
+ { key: 'parallel', group: 'Run several tracks at once' },
25
+ { key: 'upkeep', group: 'Keep the project current with the toolkit' },
26
+ { key: 'generate', group: 'Generate an artifact on demand' },
27
+ { key: 'answer', group: 'Answer a question at any point' },
28
+ ]
29
+
30
+ export function familyKeys(): string[] {
31
+ return SKILL_FAMILIES.map((family) => family.key)
32
+ }
33
+
34
+ export function isFamilyKey(value: string): boolean {
35
+ return SKILL_FAMILIES.some((family) => family.key === value)
36
+ }
@@ -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
  }
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import { resolveSkillsCorpus } from '@/claude/skills-list'
4
- import { SURFACE_ROOTS } from '@/surface-root'
4
+ import { SURFACE_ROOTS } from '@/roots/surface'
5
5
 
6
6
  /**
7
7
  * The authoring roots this repository owns and no install channel delivers.
@@ -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
- import { PROJECT_ROOT } from '@/project-root'
30
+ import { PROJECT_ROOT } from '@/roots/project'
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
- import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project-root'
70
- import { recordDir } from '@/record-root'
71
- import { SURFACE_ENTRIES, surfaceDir } from '@/surface-root'
72
- import { isDirectory, resolveTarget } from '@/target'
74
+ import { checkoutMismatchWarning, PROJECT_ROOT } from '@/roots/project'
75
+ import { recordDir } from '@/roots/record'
76
+ import { SURFACE_ENTRIES, surfaceDir } from '@/roots/surface'
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