@erclx/canon 5.25.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 (315) hide show
  1. package/README.md +4 -3
  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 +14 -1
  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/REQUIREMENT.md +6 -2
  20. package/claude/skills/context-fold/SKILL.md +9 -21
  21. package/claude/skills/context-fold/references/anchor-sweep.md +2 -2
  22. package/claude/skills/context-fold/references/completion-report.md +26 -0
  23. package/claude/skills/create-rule/SKILL.md +2 -0
  24. package/claude/skills/create-skill/SKILL.md +2 -0
  25. package/claude/skills/create-standard/SKILL.md +2 -0
  26. package/claude/skills/decision-escalate/SKILL.md +2 -0
  27. package/claude/skills/deploy-app/SKILL.md +2 -0
  28. package/claude/skills/deprecation-migration/SKILL.md +2 -0
  29. package/claude/skills/design-extract/SKILL.md +2 -0
  30. package/claude/skills/design-taste/REQUIREMENT.md +1 -1
  31. package/claude/skills/design-taste/SKILL.md +6 -2
  32. package/claude/skills/design-taste/references/adopted.md +40 -0
  33. package/claude/skills/design-taste/references/kinds.md +1 -1
  34. package/claude/skills/design-taste/references/systems.md +1 -7
  35. package/claude/skills/design-taste/references/tells.md +2 -24
  36. package/claude/skills/docs-sync/REQUIREMENT.md +2 -1
  37. package/claude/skills/docs-sync/SKILL.md +2 -0
  38. package/claude/skills/document-health/REQUIREMENT.md +2 -0
  39. package/claude/skills/document-health/SKILL.md +23 -0
  40. package/claude/skills/document-health/references/axes.md +12 -0
  41. package/claude/skills/draft-and-pick/SKILL.md +3 -1
  42. package/claude/skills/draft-doc/SKILL.md +2 -0
  43. package/claude/skills/draft-doc/references/wiki.md +1 -1
  44. package/claude/skills/draft-figure/SKILL.md +2 -0
  45. package/claude/skills/draft-identity/SKILL.md +3 -1
  46. package/claude/skills/draft-ready/SKILL.md +2 -0
  47. package/claude/skills/draft-screencast/SKILL.md +11 -7
  48. package/claude/skills/draft-slides/REQUIREMENT.md +17 -12
  49. package/claude/skills/draft-slides/SKILL.md +33 -20
  50. package/claude/skills/git-branch/SKILL.md +2 -0
  51. package/claude/skills/git-commit/SKILL.md +2 -0
  52. package/claude/skills/git-followup/SKILL.md +2 -0
  53. package/claude/skills/git-issue/SKILL.md +2 -0
  54. package/claude/skills/git-issue/references/issue.md +10 -0
  55. package/claude/skills/git-pr/SKILL.md +2 -0
  56. package/claude/skills/git-pr/references/evidence.md +14 -6
  57. package/claude/skills/git-pr/references/labels.md +12 -0
  58. package/claude/skills/git-ship/SKILL.md +2 -0
  59. package/claude/skills/git-split/SKILL.md +21 -8
  60. package/claude/skills/git-stage/SKILL.md +2 -0
  61. package/claude/skills/git-worktree/SKILL.md +2 -0
  62. package/claude/skills/index-lookup/SKILL.md +2 -0
  63. package/claude/skills/markdown-propose/SKILL.md +2 -0
  64. package/claude/skills/markdown-propose/references/format.md +9 -0
  65. package/claude/skills/memory-capture/SKILL.md +2 -0
  66. package/claude/skills/memory-review/SKILL.md +2 -0
  67. package/claude/skills/memory-review/references/later-phases.md +7 -0
  68. package/claude/skills/plan-feature/SKILL.md +2 -0
  69. package/claude/skills/plan-groundwork/SKILL.md +2 -0
  70. package/claude/skills/plan-intake/SKILL.md +2 -0
  71. package/claude/skills/plan-intake-answer/SKILL.md +2 -0
  72. package/claude/skills/project-commands/SKILL.md +2 -0
  73. package/claude/skills/read-frames/SKILL.md +2 -0
  74. package/claude/skills/record-screencast/SKILL.md +2 -0
  75. package/claude/skills/repo-metadata/SKILL.md +2 -0
  76. package/claude/skills/restate-plainly/SKILL.md +2 -0
  77. package/claude/skills/review-address/SKILL.md +3 -7
  78. package/claude/skills/review-address/references/post-review-findings.md +16 -0
  79. package/claude/skills/review-branch/SKILL.md +2 -0
  80. package/claude/skills/review-craft/SKILL.md +2 -0
  81. package/claude/skills/review-craft/references/ui.md +2 -2
  82. package/claude/skills/review-pr/SKILL.md +4 -8
  83. package/claude/skills/review-pr/references/body-variants.md +52 -0
  84. package/claude/skills/review-pr/references/key-changes.md +16 -0
  85. package/claude/skills/review-pr/references/post.md +15 -43
  86. package/claude/skills/review-ui/SKILL.md +3 -1
  87. package/claude/skills/review-ui/references/post.md +4 -2
  88. package/claude/skills/role-orchestrator/SKILL.md +5 -14
  89. package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +8 -0
  90. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +13 -0
  91. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
  92. package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +23 -0
  93. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +5 -33
  94. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +1 -21
  95. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +1 -1
  96. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +38 -0
  97. package/claude/skills/role-orchestrator/references/orchestrator-watch.md +26 -0
  98. package/claude/skills/role-planner/SKILL.md +2 -0
  99. package/claude/skills/role-reviewer/REQUIREMENT.md +1 -1
  100. package/claude/skills/role-reviewer/SKILL.md +2 -0
  101. package/claude/skills/role-worker/SKILL.md +2 -0
  102. package/claude/skills/search-craft/REQUIREMENT.md +1 -1
  103. package/claude/skills/search-craft/SKILL.md +5 -1
  104. package/claude/skills/search-craft/references/adopted.md +30 -0
  105. package/claude/skills/seed-sync/SKILL.md +2 -0
  106. package/claude/skills/session-compact/SKILL.md +2 -0
  107. package/claude/skills/session-map/SKILL.md +2 -0
  108. package/claude/skills/session-relay/SKILL.md +2 -0
  109. package/claude/skills/session-resume/SKILL.md +2 -0
  110. package/claude/skills/session-worktree/SKILL.md +3 -11
  111. package/claude/skills/session-worktree/references/install-command.md +20 -0
  112. package/claude/skills/sketch-design/SKILL.md +2 -0
  113. package/claude/skills/systematic-debugging/SKILL.md +2 -0
  114. package/claude/skills/target-check/SKILL.md +2 -0
  115. package/claude/skills/target-setup/SKILL.md +2 -0
  116. package/claude/skills/target-setup/references/detect.md +8 -0
  117. package/claude/skills/target-setup/references/indexes.md +13 -0
  118. package/claude/skills/target-setup/references/verify.md +8 -0
  119. package/claude/skills/task-board/SKILL.md +2 -0
  120. package/claude/skills/teach-workspace/SKILL.md +5 -15
  121. package/claude/skills/teach-workspace/references/lesson-build.md +75 -0
  122. package/claude/skills/teach-workspace/references/lesson-craft.md +13 -69
  123. package/claude/skills/teach-workspace/references/revisit.md +22 -0
  124. package/claude/skills/test-craft/SKILL.md +2 -0
  125. package/claude/skills/test-first/SKILL.md +4 -0
  126. package/claude/skills/test-first/references/adopted.md +36 -0
  127. package/claude/skills/ui-checklist/SKILL.md +2 -0
  128. package/claude/skills/ux-audit/SKILL.md +2 -0
  129. package/claude/skills/ux-measure/SKILL.md +2 -0
  130. package/claude/skills/ux-walkthrough/SKILL.md +2 -0
  131. package/claude/skills/video-craft/SKILL.md +2 -0
  132. package/claude/skills/write-human/SKILL.md +3 -1
  133. package/claude/skills/write-human/references/machine-tells.md +13 -0
  134. package/claude/skills/youtube-transcripts/SKILL.md +2 -0
  135. package/docs/agents/canvas.md +24 -6
  136. package/docs/agents/capture.md +6 -6
  137. package/docs/agents/commands.md +5 -5
  138. package/docs/agents/index.md +5 -5
  139. package/docs/agents/install-and-sync.md +7 -0
  140. package/docs/agents/key-changes.md +1 -1
  141. package/docs/agents/pr-evidence.md +38 -15
  142. package/docs/agents/pr-local.md +19 -13
  143. package/docs/agents/pr-preview.md +44 -21
  144. package/docs/agents/records-stale.md +22 -9
  145. package/docs/agents/records.md +1 -1
  146. package/docs/agents/sandbox.md +29 -5
  147. package/docs/agents/scripting.md +8 -1
  148. package/docs/agents/skills-audit.md +11 -3
  149. package/docs/agents/slides.md +15 -3
  150. package/docs/workflow/ai-workflow.md +1 -1
  151. package/docs/workflow/index.md +1 -0
  152. package/docs/workflow/operating-model.md +5 -6
  153. package/docs/workflow/parallel-features.md +23 -2
  154. package/docs/workflow/skill-map-on-request.md +42 -0
  155. package/docs/workflow/skill-map.md +3 -34
  156. package/docs/workflow/zshrc-aliases.md +1 -1
  157. package/package.json +1 -1
  158. package/scripts/core/regen-hero.ts +1 -1
  159. package/scripts/lib/sandbox-git.sh +1 -9
  160. package/scripts/lib/sandbox-path.sh +6 -162
  161. package/scripts/sandbox-hook.sh +99 -0
  162. package/src/audits/run.ts +1 -1
  163. package/src/canvas/address.ts +12 -3
  164. package/src/canvas/capture.ts +1 -9
  165. package/src/canvas/client/inspector/align-grid.tsx +162 -0
  166. package/src/canvas/client/inspector/color-field.tsx +398 -0
  167. package/src/canvas/client/inspector/color-inputs.tsx +139 -0
  168. package/src/canvas/client/inspector/color-picker.tsx +668 -0
  169. package/src/canvas/client/inspector/color-space.ts +240 -0
  170. package/src/canvas/client/inspector/field.tsx +5 -1
  171. package/src/canvas/client/inspector/fields.css +547 -17
  172. package/src/canvas/client/inspector/size-field.tsx +69 -0
  173. package/src/canvas/client/inspector/values.ts +166 -1
  174. package/src/canvas/client/inspector.tsx +236 -73
  175. package/src/canvas/client/selection.tsx +197 -0
  176. package/src/canvas/client/shell.css +104 -12
  177. package/src/canvas/client/state.ts +69 -9
  178. package/src/canvas/client/surface.tsx +147 -22
  179. package/src/canvas/content.ts +50 -1
  180. package/src/canvas/edit.ts +37 -4
  181. package/src/canvas/editing.ts +203 -0
  182. package/src/canvas/global.d.ts +15 -0
  183. package/src/canvas/server.ts +149 -12
  184. package/src/canvas/tokens.ts +59 -17
  185. package/src/capture/render.ts +45 -7
  186. package/src/capture/sources.ts +43 -0
  187. package/src/census/count.ts +2 -2
  188. package/src/claude/cases/workflow.ts +5 -0
  189. package/src/claude/seeds.ts +2 -2
  190. package/src/claude/skills-audit.ts +96 -14
  191. package/src/claude/skills-drift.ts +1 -1
  192. package/src/claude/skills-families.ts +36 -0
  193. package/src/claude/skills-list.ts +76 -15
  194. package/src/commands/audits.ts +2 -2
  195. package/src/commands/canvas.ts +179 -2
  196. package/src/commands/claude.ts +94 -11
  197. package/src/commands/design.ts +142 -2
  198. package/src/commands/feedback.ts +3 -3
  199. package/src/commands/gov.ts +1 -1
  200. package/src/commands/hooks.ts +1 -1
  201. package/src/commands/indexes.ts +1 -1
  202. package/src/commands/init.ts +1 -1
  203. package/src/commands/intake.ts +1 -1
  204. package/src/commands/migrate.ts +1 -1
  205. package/src/commands/pr.ts +105 -58
  206. package/src/commands/records.ts +89 -7
  207. package/src/commands/repo.ts +1 -1
  208. package/src/commands/sandbox.ts +103 -11
  209. package/src/commands/sessions.ts +1 -1
  210. package/src/commands/slides.ts +31 -77
  211. package/src/commands/sync.ts +1 -1
  212. package/src/commands/tasks.ts +2 -2
  213. package/src/commands/teach.ts +1 -1
  214. package/src/commands/transcripts.ts +1 -1
  215. package/src/commands/worktrees.ts +1 -1
  216. package/src/comments/scan.ts +1 -1
  217. package/src/comments/trend.ts +1 -1
  218. package/src/context/audit.ts +1 -1
  219. package/src/context/citations.ts +1 -1
  220. package/src/context/classify/extract.ts +2 -2
  221. package/src/design/project-fonts.ts +392 -0
  222. package/src/design/tokens.ts +2 -18
  223. package/src/docs/read.ts +1 -1
  224. package/src/{github.ts → feedback/github.ts} +2 -2
  225. package/src/{client-commands.ts → gate/client-commands.ts} +1 -1
  226. package/src/gate/measures.ts +53 -13
  227. package/src/gate/sequencer.ts +1 -1
  228. package/src/gate/stages.ts +9 -0
  229. package/src/{git-files.ts → git/files.ts} +2 -2
  230. package/src/{git-ignore.ts → git/ignore.ts} +1 -1
  231. package/src/{worktree.ts → git/worktree.ts} +1 -1
  232. package/src/gov/citations.ts +2 -2
  233. package/src/gov/install.ts +1 -1
  234. package/src/gov/payload.ts +1 -1
  235. package/src/gov/superseded.ts +3 -3
  236. package/src/gov/test-order.ts +2 -2
  237. package/src/help.ts +1 -1
  238. package/src/hooks/post-merge.ts +1 -1
  239. package/src/indexes/walk.ts +1 -1
  240. package/src/labels/audit.ts +1 -1
  241. package/src/markdown/ceiling.ts +1 -1
  242. package/src/markdown/files.ts +1 -1
  243. package/src/migrate/apply.ts +1 -1
  244. package/src/pr/bijection.ts +1 -1
  245. package/src/pr/evidence.ts +96 -43
  246. package/src/pr/local.ts +16 -10
  247. package/src/pr/preview.ts +78 -2
  248. package/src/record-root.ts +1 -0
  249. package/src/records/backup.ts +2 -1
  250. package/src/records/push-guard.ts +1 -1
  251. package/src/records/stale-canonical.ts +146 -0
  252. package/src/records/stale.ts +1 -1
  253. package/src/sandbox/headless/dispatch.ts +264 -0
  254. package/src/sandbox/headless/run.ts +653 -0
  255. package/src/sandbox/headless/watch.ts +145 -0
  256. package/src/sandbox/hooks.ts +128 -0
  257. package/src/sandbox/provision.ts +607 -0
  258. package/src/sandbox/tree.ts +99 -4
  259. package/src/secrets/marker.ts +2 -2
  260. package/src/secrets/scan.ts +2 -2
  261. package/src/sessions/claim.ts +1 -1
  262. package/src/sessions/resolve.ts +1 -1
  263. package/src/sessions/transfer/bundle.ts +1 -1
  264. package/src/shipped/references.ts +2 -2
  265. package/src/slides/convert/export.ts +2 -16
  266. package/src/slides/decks.ts +110 -0
  267. package/src/slides/package/fonts.ts +6 -2
  268. package/src/standards/audit.ts +2 -2
  269. package/src/standards/read.ts +1 -1
  270. package/src/sync/check.ts +2 -2
  271. package/src/sync/engine.ts +2 -2
  272. package/src/sync/git.ts +1 -1
  273. package/src/sync/history.ts +1 -1
  274. package/src/sync/reverse.ts +1 -1
  275. package/src/sync/seeds-report.ts +1 -1
  276. package/src/sync/target.ts +1 -1
  277. package/src/sync/workflow.ts +1 -1
  278. package/src/targets/pulls.ts +2 -2
  279. package/src/targets/sweep.ts +2 -2
  280. package/src/tasks/answers.ts +1 -1
  281. package/src/tasks/archive.ts +1 -1
  282. package/src/tasks/reach.ts +1 -1
  283. package/src/tasks/trunk.ts +1 -1
  284. package/src/teach/nav.ts +1 -1
  285. package/src/tooling/inject.ts +2 -2
  286. package/src/tooling/subfolder.ts +1 -1
  287. package/src/web/readme-citations.ts +2 -2
  288. package/src/worktrees/bare-flag.ts +1 -1
  289. package/src/worktrees/reclaim.ts +2 -2
  290. package/src/worktrees/remove.ts +1 -1
  291. package/standards/architecture.md +1 -0
  292. package/standards/canvas.md +3 -2
  293. package/standards/requirements.md +2 -0
  294. package/standards/skill-practice.md +2 -1
  295. package/standards/skill.md +2 -0
  296. package/standards/wiki.md +12 -3
  297. package/tooling/base/configs/.github/workflows/pr-local-preview.yml +2 -2
  298. package/scripts/lib/sandbox-dispatch.sh +0 -190
  299. package/scripts/manage-sandbox.sh +0 -472
  300. package/src/slides/layouts.ts +0 -478
  301. package/src/slides/parse.ts +0 -84
  302. package/src/slides/render.ts +0 -110
  303. package/src/slides/styles.ts +0 -96
  304. /package/claude/skills/write-human/references/{source-material.md → adopted.md} +0 -0
  305. /package/src/{commands/feedback-format.ts → feedback/body.ts} +0 -0
  306. /package/src/{github-format.ts → feedback/github-format.ts} +0 -0
  307. /package/src/{git-base.ts → git/base.ts} +0 -0
  308. /package/src/{git-env.ts → git/env.ts} +0 -0
  309. /package/src/{frontmatter.ts → markdown/frontmatter.ts} +0 -0
  310. /package/src/{binary.ts → sweep/binary.ts} +0 -0
  311. /package/src/{exempt-marker.ts → sweep/exempt-marker.ts} +0 -0
  312. /package/src/{copy.ts → sync/copy.ts} +0 -0
  313. /package/src/{target.ts → targets/validate.ts} +0 -0
  314. /package/src/{paths.ts → tasks/paths.ts} +0 -0
  315. /package/src/{seed-marker.ts → tooling/seed-marker.ts} +0 -0
@@ -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,
@@ -24,8 +24,11 @@ import { listSeeds, readSeedContents } from '@/claude/seeds-list'
24
24
  import {
25
25
  auditExitCode,
26
26
  auditSkills,
27
+ CONTENTS_SECTION,
28
+ CONTENTS_THRESHOLD,
27
29
  CORPORA,
28
30
  DESCRIPTION_LIMIT,
31
+ LEDGER_SKILLS,
29
32
  PRACTICE_LEDGER,
30
33
  PRACTICE_SECTIONS,
31
34
  PRACTICE_SKILLS,
@@ -41,7 +44,12 @@ import {
41
44
  scanRouting,
42
45
  } from '@/claude/routing'
43
46
  import { type DriftReport, readDrift } from '@/claude/skills-drift'
44
- 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'
45
53
  import {
46
54
  type ReachRefusal,
47
55
  type ReachReport,
@@ -61,12 +69,12 @@ import {
61
69
  serializeSettings,
62
70
  writeSettings,
63
71
  } from '@/claude/settings'
64
- import { copyPreservingMode } from '@/copy'
72
+ import { copyPreservingMode } from '@/sync/copy'
65
73
  import { execScript } from '@/exec'
66
74
  import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project-root'
67
75
  import { recordDir } from '@/record-root'
68
76
  import { SURFACE_ENTRIES, surfaceDir } from '@/surface-root'
69
- import { isDirectory, resolveTarget } from '@/target'
77
+ import { isDirectory, resolveTarget } from '@/targets/validate'
70
78
  import { injectGitignore, pruneGitignore } from '@/tooling/inject'
71
79
  import {
72
80
  frameError,
@@ -93,6 +101,7 @@ interface SeedsListOptions {
93
101
  interface SkillsListOptions {
94
102
  readonly json?: boolean
95
103
  readonly names?: boolean
104
+ readonly byFamily?: boolean
96
105
  }
97
106
 
98
107
  interface SkillsAuditOptions {
@@ -356,8 +365,12 @@ export function register(program: Command): void {
356
365
  .command('list')
357
366
  .description('List the plugin skills shipped under claude/skills/')
358
367
  .helpOption('-h, --help', 'Show this help message')
359
- .option('--json', 'Emit JSON with name and description')
368
+ .option(
369
+ '--json',
370
+ 'Emit JSON with each skill and the ordered family vocabulary',
371
+ )
360
372
  .option('--names', 'Only list skill names, one per line')
373
+ .option('--by-family', 'Group the listing under each family')
361
374
  .addHelpText(
362
375
  'after',
363
376
  [
@@ -365,6 +378,8 @@ export function register(program: Command): void {
365
378
  'Notes:',
366
379
  ' Internal skills under .claude/skills/ are excluded, since they',
367
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.',
368
383
  '',
369
384
  ].join('\n'),
370
385
  )
@@ -767,7 +782,9 @@ function runSkillsList(opts: SkillsListOptions): number {
767
782
 
768
783
  if (opts.json) {
769
784
  if (mismatch !== undefined) logWarn(mismatch)
770
- process.stdout.write(`${JSON.stringify({ skills: listings })}\n`)
785
+ process.stdout.write(
786
+ `${JSON.stringify({ skills: listings, families: SKILL_FAMILIES })}\n`,
787
+ )
771
788
  return 0
772
789
  }
773
790
 
@@ -781,14 +798,34 @@ function runSkillsList(opts: SkillsListOptions): number {
781
798
 
782
799
  intro('canon claude')
783
800
  if (mismatch !== undefined) logWarn(mismatch)
784
- logStep('Plugin skills')
785
- for (const listing of listings) {
786
- 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
+ }
787
808
  }
788
809
  outro()
789
810
  return 0
790
811
  }
791
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
+
792
829
  /**
793
830
  * Measures the cwd for the same reason the audit does, and takes the ref as a
794
831
  * required argument with no default. `HEAD` would be the only defensible one and
@@ -1398,6 +1435,8 @@ async function runSkillsAudit(
1398
1435
  reportRequirementShape(report)
1399
1436
  reportProvenance(report)
1400
1437
  reportPracticeShape(report)
1438
+ reportReferenceContents(report)
1439
+ reportFamily(report)
1401
1440
  reportUnmeasured()
1402
1441
  outro()
1403
1442
  }
@@ -1421,13 +1460,18 @@ async function runSkillsAudit(
1421
1460
  requirementSections: report.requirementSections,
1422
1461
  datedProvenance: report.datedProvenance,
1423
1462
  practiceShape: report.practiceShape,
1463
+ referenceContents: report.referenceContents,
1464
+ family: report.family,
1424
1465
  },
1425
1466
  checkpoints: {
1426
1467
  descriptionLimit: DESCRIPTION_LIMIT,
1427
1468
  requirementSections: REQUIREMENT_SECTIONS,
1428
1469
  corpora: CORPORA,
1429
1470
  practiceSkills: PRACTICE_SKILLS,
1471
+ ledgerSkills: LEDGER_SKILLS,
1430
1472
  practiceSections: PRACTICE_SECTIONS,
1473
+ contentsThreshold: CONTENTS_THRESHOLD,
1474
+ families: familyKeys(),
1431
1475
  },
1432
1476
  })}\n`,
1433
1477
  )
@@ -1626,10 +1670,10 @@ function reportProvenance(report: SkillsAudit): void {
1626
1670
  function reportPracticeShape(report: SkillsAudit): void {
1627
1671
  logStep('Practice shape')
1628
1672
  logInfo(
1629
- `A listed practice skill closes with ${PRACTICE_SECTIONS.map((section) => `## ${section}`).join(', ')}, and carries ${PRACTICE_LEDGER}.`,
1673
+ `A listed practice skill closes with ${PRACTICE_SECTIONS.map((section) => `## ${section}`).join(', ')}, and every skill on the wider ledger list carries ${PRACTICE_LEDGER}.`,
1630
1674
  )
1631
1675
  logInfo(
1632
- `The list is kept here and names ${plural(PRACTICE_SKILLS.length, 'shipped skill')}, so a skill outside ${SHIPPED_CORPUS} is never read.`,
1676
+ `Both lists are kept here and name ${plural(PRACTICE_SKILLS.length, 'practice skill')} and ${plural(LEDGER_SKILLS.length, 'ledger skill')}, so a skill outside ${SHIPPED_CORPUS} is never read.`,
1633
1677
  )
1634
1678
 
1635
1679
  if (report.practiceShape.length === 0) {
@@ -1643,6 +1687,45 @@ function reportPracticeShape(report: SkillsAudit): void {
1643
1687
  reportFindings(report.practiceShape)
1644
1688
  }
1645
1689
 
1690
+ /** Reports without failing the run, the same judgment the practice shape is. */
1691
+ function reportReferenceContents(report: SkillsAudit): void {
1692
+ logStep('Reference contents')
1693
+ logInfo(
1694
+ `A reference over ${CONTENTS_THRESHOLD} lines opens with a ## ${CONTENTS_SECTION} list, matched exactly outside fences.`,
1695
+ )
1696
+
1697
+ if (report.referenceContents.length === 0) {
1698
+ logInfo('Every long reference opens with a contents list.')
1699
+ return
1700
+ }
1701
+
1702
+ logWarn(
1703
+ `${plural(report.referenceContents.length, 'long reference')} with no contents list`,
1704
+ )
1705
+ reportFindings(report.referenceContents)
1706
+ }
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
+
1646
1729
  /**
1647
1730
  * Stated on every run, including the run where everything above passed. A
1648
1731
  * report that lists only what it measured reads as a verdict on the standard
@@ -8,20 +8,23 @@ import {
8
8
  } from '@/design/adapter'
9
9
  import { buildDesignCss } from '@/design/css'
10
10
  import { HAND_DRAWN_FONT_FACES } from '@/design/fonts'
11
+ import { addProjectFace, listProjectFaces } from '@/design/project-fonts'
11
12
  import { renderDesignDoc } from '@/design/render'
12
13
  import { DESIGN_BASE_CSS, DESIGN_DOCUMENT, regenDesign } from '@/design/regen'
13
14
  import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project-root'
14
15
  import { creationRel, SCRATCH } from '@/record-root'
15
16
  import { surfaceDir } from '@/surface-root'
16
17
  import { recordStamp, runDomainSync } from '@/sync/engine'
17
- import { resolveTarget } from '@/target'
18
+ import { isDirectory, resolveTarget } from '@/targets/validate'
18
19
  import { intro, logAdd, logError, logInfo, logWarn, outro, palette } from '@/ui'
19
20
 
20
21
  export function register(program: Command): void {
21
22
  const design = program
22
23
  .command('design')
23
24
  .helpOption('-h, --help', 'Show this help message')
24
- .description('Design system commands (regen, css, render, install, sync)')
25
+ .description(
26
+ 'Design system commands (regen, css, render, install, sync, fonts)',
27
+ )
25
28
 
26
29
  design
27
30
  .command('regen')
@@ -172,6 +175,143 @@ export function register(program: Command): void {
172
175
  { protectedRoot: PROJECT_ROOT },
173
176
  )
174
177
  })
178
+
179
+ registerFonts(design)
180
+ }
181
+
182
+ const FONTS_DIR = join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR, 'fonts')
183
+
184
+ interface FontsRefused {
185
+ readonly ok: false
186
+ readonly reason: string
187
+ readonly detail: string
188
+ }
189
+
190
+ function writeJson(record: unknown): void {
191
+ process.stdout.write(`${JSON.stringify(record)}\n`)
192
+ }
193
+
194
+ function refuseFonts(
195
+ title: string,
196
+ refusal: FontsRefused,
197
+ emitJson: boolean,
198
+ ): number {
199
+ if (emitJson) writeJson(refusal)
200
+ intro(title)
201
+ logError(refusal.detail)
202
+ outro()
203
+ return 1
204
+ }
205
+
206
+ /** A target that is a directory, or the refusal naming the path given. */
207
+ function fontsTarget(target: string): string | FontsRefused {
208
+ const resolved = resolve(target)
209
+ if (!isDirectory(resolved)) {
210
+ return {
211
+ ok: false,
212
+ reason: 'no-target',
213
+ detail: `Target directory not found: ${target}`,
214
+ }
215
+ }
216
+ return resolved
217
+ }
218
+
219
+ function registerFonts(design: Command): void {
220
+ const fonts = design
221
+ .command('fonts')
222
+ .helpOption('-h, --help', 'Show this help message')
223
+ .description(`Read and add the project's own faces under ${FONTS_DIR}/`)
224
+ .addHelpText(
225
+ 'after',
226
+ [
227
+ '',
228
+ `Faces are declared as @font-face rules in ${join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR)}/fonts.css,`,
229
+ 'which the canvas embeds into every frame. Sync never writes the folder.',
230
+ '',
231
+ ].join('\n'),
232
+ )
233
+
234
+ fonts
235
+ .command('list')
236
+ .helpOption('-h, --help', 'Show this help message')
237
+ .description('List each face the project override sheets declare')
238
+ .argument('[target]', 'Target directory', '.')
239
+ .option('--json', 'Emit the record on stdout')
240
+ .action((target: string, opts: { json?: boolean }) => {
241
+ const title = 'canon design fonts list'
242
+ const resolved = fontsTarget(target)
243
+ if (typeof resolved !== 'string') {
244
+ process.exitCode = refuseFonts(title, resolved, Boolean(opts.json))
245
+ return
246
+ }
247
+ const faces = listProjectFaces(resolved)
248
+ if (opts.json) writeJson({ ok: true, faces })
249
+ intro(title)
250
+ if (faces.length === 0) logInfo(`No faces declared under ${FONTS_DIR}/`)
251
+ for (const face of faces) {
252
+ const line = `${face.family} ${face.weight} ${face.style}: ${face.file} in ${face.sheet}`
253
+ if (face.present) logInfo(line)
254
+ else logWarn(`${line} (${face.problem ?? 'missing'})`)
255
+ }
256
+ outro()
257
+ })
258
+
259
+ fonts
260
+ .command('add')
261
+ .helpOption('-h, --help', 'Show this help message')
262
+ .description(
263
+ `Copy a woff2, woff, ttf, or otf face into ${FONTS_DIR}/ and declare it`,
264
+ )
265
+ .argument('<file>', 'Font file to copy')
266
+ .argument('[target]', 'Target directory', '.')
267
+ .requiredOption('--family <name>', 'Family name the face declares')
268
+ .option('--weight <n>', "Weight, or a range such as '100 900'", '400')
269
+ .option('--style <style>', 'normal or italic', 'normal')
270
+ .option('--json', 'Emit the record on stdout')
271
+ .action(
272
+ (
273
+ file: string,
274
+ target: string,
275
+ opts: { family: string; weight: string; style: string; json?: boolean },
276
+ ) => {
277
+ const title = 'canon design fonts add'
278
+ const emitJson = Boolean(opts.json)
279
+ const resolved = fontsTarget(target)
280
+ if (typeof resolved !== 'string') {
281
+ process.exitCode = refuseFonts(title, resolved, emitJson)
282
+ return
283
+ }
284
+ if (resolved === PROJECT_ROOT) {
285
+ process.exitCode = refuseFonts(
286
+ title,
287
+ {
288
+ ok: false,
289
+ reason: 'toolkit-root',
290
+ detail:
291
+ 'Cannot run against toolkit root. Files here are the source of truth.',
292
+ },
293
+ emitJson,
294
+ )
295
+ return
296
+ }
297
+ const outcome = addProjectFace(resolved, resolve(file), {
298
+ family: opts.family,
299
+ weight: opts.weight,
300
+ style: opts.style,
301
+ })
302
+ if (!outcome.ok) {
303
+ process.exitCode = refuseFonts(title, outcome, emitJson)
304
+ return
305
+ }
306
+ if (emitJson) writeJson(outcome)
307
+ intro(title)
308
+ logAdd(join(DESIGN_INSTALL_DIR, DESIGN_PROJECT_SUBDIR, outcome.file))
309
+ logInfo(
310
+ `Declared ${outcome.family} ${outcome.weight} ${outcome.style} in ${outcome.sheet}`,
311
+ )
312
+ outro()
313
+ },
314
+ )
175
315
  }
176
316
 
177
317
  /**
@@ -7,11 +7,11 @@ import {
7
7
  deriveTitle,
8
8
  missingField,
9
9
  missingFieldMessage,
10
- } from '@/commands/feedback-format'
10
+ } from '@/feedback/body'
11
11
  import { checkoutMismatchWarning, PROJECT_ROOT } from '@/project-root'
12
12
  import { creationRel } from '@/record-root'
13
- import { createGithubIssue } from '@/github'
14
- import { issueFailureMessage } from '@/github-format'
13
+ import { createGithubIssue } from '@/feedback/github'
14
+ import { issueFailureMessage } from '@/feedback/github-format'
15
15
  import { frameError, frameSuccess, logWarn, palette } from '@/ui'
16
16
 
17
17
  function readStdin(): Promise<string> {
@@ -53,7 +53,7 @@ import {
53
53
  } from '@/gov/test-order'
54
54
  import { recordStamp, runDomainSync } from '@/sync/engine'
55
55
  import { writeChainStamp } from '@/sync/stamp'
56
- import { resolveTarget } from '@/target'
56
+ import { resolveTarget } from '@/targets/validate'
57
57
  import {
58
58
  intro,
59
59
  isNonInteractive,
@@ -5,7 +5,7 @@ import {
5
5
  runPostMerge,
6
6
  STEP_VERBS,
7
7
  } from '@/hooks/post-merge'
8
- import { mainWorktreeRoot } from '@/worktree'
8
+ import { mainWorktreeRoot } from '@/git/worktree'
9
9
 
10
10
  interface PostMergeOptions {
11
11
  readonly root?: string
@@ -4,7 +4,7 @@ import { $ } from 'bun'
4
4
  import type { Command } from 'commander'
5
5
  import { buildIndexCatalog } from '@/indexes/list'
6
6
  import { exitCodeFor, type RegenResult, regenOne } from '@/indexes/regen'
7
- import { gitEnv } from '@/git-env'
7
+ import { gitEnv } from '@/git/env'
8
8
  import { findIndexedAncestor, isIgnored, listIndexes } from '@/indexes/walk'
9
9
  import { intro, logAdd, logInfo, logStep, logWarn, outro } from '@/ui'
10
10
 
@@ -5,7 +5,7 @@ import { applyInitOptions, flagsProvided } from '@/init/flags'
5
5
  import { type InitFlags, parseSkip, planInit } from '@/init/plan'
6
6
  import { runDomains } from '@/init/run'
7
7
  import { buildSteps } from '@/init/steps'
8
- import { resolveTarget } from '@/target'
8
+ import { resolveTarget } from '@/targets/validate'
9
9
  import { intro, logInfo, logStep, logWarn, outro, palette, select } from '@/ui'
10
10
 
11
11
  interface InitOptions {
@@ -21,7 +21,7 @@ import {
21
21
  outro,
22
22
  pipeOutput,
23
23
  } from '@/ui'
24
- import { mainWorktreeRoot } from '@/worktree'
24
+ import { mainWorktreeRoot } from '@/git/worktree'
25
25
 
26
26
  /**
27
27
  * A selection as the command line spells it, splitting on the first `=`. The