@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
@@ -0,0 +1,214 @@
1
+ import {
2
+ linkSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ renameSync,
6
+ unlinkSync,
7
+ writeFileSync,
8
+ } from 'node:fs'
9
+ import { dirname, join } from 'node:path'
10
+ import { liveness, type LivenessProbes, SYSTEM_PROBES } from '@/sessions/live'
11
+ import { stateDir } from '@/targets/registry'
12
+
13
+ /**
14
+ * Who holds a machine lock, as the file records it.
15
+ *
16
+ * The start time is what separates the holder from whatever inherits its pid
17
+ * after a crash, so a takeover never waits on a recycled number.
18
+ */
19
+ export interface LockHolder {
20
+ readonly pid: number
21
+ readonly procStart: string | undefined
22
+ /** The worktree whose gate holds the lock, which the wait line names. */
23
+ readonly root: string
24
+ }
25
+
26
+ export interface SuiteLockOptions {
27
+ readonly path: string
28
+ readonly root: string
29
+ readonly pid?: number
30
+ readonly probes?: LivenessProbes
31
+ readonly pollMs?: number
32
+ readonly sleep?: (ms: number) => Promise<void>
33
+ /** Called once, when the first poll finds a live holder, so a wait is never silent. */
34
+ readonly onWait?: (holder: LockHolder) => void
35
+ }
36
+
37
+ export interface HeldLock {
38
+ readonly release: () => void
39
+ /** The holder this acquirer queued behind, absent when it never waited. */
40
+ readonly waitedOn?: LockHolder
41
+ readonly waitedMs: number
42
+ }
43
+
44
+ export type AcquireOutcome =
45
+ | { readonly acquired: true; readonly release: () => void }
46
+ | { readonly acquired: false; readonly holder: LockHolder | undefined }
47
+
48
+ /**
49
+ * The file a named machine lock lives at, resolved at call time so a test run
50
+ * that redirects the state folder never touches the real one.
51
+ */
52
+ export function machineLockPath(name: string): string {
53
+ return join(stateDir(), 'locks', `${name}.lock`)
54
+ }
55
+
56
+ function isHolder(value: unknown): value is LockHolder {
57
+ if (typeof value !== 'object' || value === null) return false
58
+ const fields = value as Record<string, unknown>
59
+ return typeof fields.pid === 'number' && typeof fields.root === 'string'
60
+ }
61
+
62
+ function readText(path: string): string | undefined {
63
+ try {
64
+ return readFileSync(path, 'utf8')
65
+ } catch {
66
+ return undefined
67
+ }
68
+ }
69
+
70
+ function parseHolder(text: string): LockHolder | undefined {
71
+ try {
72
+ const value: unknown = JSON.parse(text)
73
+ return isHolder(value) ? value : undefined
74
+ } catch {
75
+ return undefined
76
+ }
77
+ }
78
+
79
+ function isCode(error: unknown, code: string): boolean {
80
+ return (error as NodeJS.ErrnoException).code === code
81
+ }
82
+
83
+ /**
84
+ * Publishes the holder's file whole or not at all.
85
+ *
86
+ * `link` refuses an existing name the way an exclusive open does, and the
87
+ * linked file is already written, so no reader meets an empty lock and mistakes
88
+ * a holder mid-write for a corrupt file it may take over.
89
+ */
90
+ function publish(path: string, text: string, pid: number): boolean {
91
+ const staged = `${path}.${pid}.tmp`
92
+ writeFileSync(staged, text)
93
+ try {
94
+ linkSync(staged, path)
95
+ return true
96
+ } catch (error) {
97
+ if (isCode(error, 'EEXIST')) return false
98
+ throw error
99
+ } finally {
100
+ unlinkSync(staged)
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Moves a dead holder's file aside, or puts back one that changed under it.
106
+ *
107
+ * Unlink then create is two calls, and two acquirers judging one stale file
108
+ * would both get through. A rename moves the file to a name only this acquirer
109
+ * uses, so only one of them gets it, and comparing what moved against what was
110
+ * judged catches the case where another acquirer already replaced it.
111
+ *
112
+ * One interleaving still gets through. A third acquirer publishing between the
113
+ * rename and the restore leaves the moved holder's file with nowhere to go, so
114
+ * two suites run at once. That costs one oversubscribed run and never a hang,
115
+ * and closing it needs a lock the kernel releases, which `node:fs` does not
116
+ * offer.
117
+ */
118
+ function displace(path: string, judged: string, pid: number): void {
119
+ const aside = `${path}.${pid}.stale`
120
+ try {
121
+ renameSync(path, aside)
122
+ } catch (error) {
123
+ if (isCode(error, 'ENOENT')) return
124
+ throw error
125
+ }
126
+
127
+ if (readText(aside) !== judged) {
128
+ try {
129
+ linkSync(aside, path)
130
+ } catch (error) {
131
+ if (!isCode(error, 'EEXIST')) throw error
132
+ }
133
+ }
134
+ unlinkSync(aside)
135
+ }
136
+
137
+ /**
138
+ * One attempt at the lock: take it free, take it over from a dead holder, or
139
+ * report who holds it.
140
+ */
141
+ export function tryAcquire(options: SuiteLockOptions): AcquireOutcome {
142
+ const pid = options.pid ?? process.pid
143
+ const probes = options.probes ?? SYSTEM_PROBES
144
+ const own: LockHolder = {
145
+ pid,
146
+ procStart: probes.procStartOf(pid) ?? undefined,
147
+ root: options.root,
148
+ }
149
+ const text = JSON.stringify(own)
150
+
151
+ mkdirSync(dirname(options.path), { recursive: true })
152
+
153
+ if (publish(options.path, text, pid)) {
154
+ return { acquired: true, release: () => release(options.path, text) }
155
+ }
156
+
157
+ const judged = readText(options.path)
158
+ if (judged === undefined) return { acquired: false, holder: undefined }
159
+
160
+ const holder = parseHolder(judged)
161
+ if (holder !== undefined && liveness(holder, probes).alive) {
162
+ return { acquired: false, holder }
163
+ }
164
+
165
+ displace(options.path, judged, pid)
166
+
167
+ if (publish(options.path, text, pid)) {
168
+ return { acquired: true, release: () => release(options.path, text) }
169
+ }
170
+ return { acquired: false, holder: parseHolder(readText(options.path) ?? '') }
171
+ }
172
+
173
+ /** Removes the file only while it is still this holder's own. */
174
+ function release(path: string, text: string): void {
175
+ if (readText(path) !== text) return
176
+ try {
177
+ unlinkSync(path)
178
+ } catch (error) {
179
+ if (!isCode(error, 'ENOENT')) throw error
180
+ }
181
+ }
182
+
183
+ const wait = (ms: number) =>
184
+ new Promise<void>((resolve) => setTimeout(resolve, ms))
185
+
186
+ /**
187
+ * Waits for the lock with no deadline while its holder lives. A deadline would
188
+ * turn a long queue back into the intermittent failure the lock removes, and a
189
+ * dead holder is taken over on the next poll rather than waited out.
190
+ */
191
+ export async function acquireSuiteLock(
192
+ options: SuiteLockOptions,
193
+ ): Promise<HeldLock> {
194
+ const sleep = options.sleep ?? wait
195
+ const pollMs = options.pollMs ?? 500
196
+ const startedAt = performance.now()
197
+ let waitedOn: LockHolder | undefined
198
+
199
+ for (;;) {
200
+ const outcome = tryAcquire(options)
201
+ if (outcome.acquired) {
202
+ return {
203
+ release: outcome.release,
204
+ waitedOn,
205
+ waitedMs: waitedOn === undefined ? 0 : performance.now() - startedAt,
206
+ }
207
+ }
208
+ if (waitedOn === undefined && outcome.holder !== undefined) {
209
+ options.onWait?.(outcome.holder)
210
+ }
211
+ waitedOn = outcome.holder ?? waitedOn
212
+ await sleep(pollMs)
213
+ }
214
+ }
@@ -1,6 +1,6 @@
1
1
  import { $ } from 'bun'
2
- import { baseCandidates, isMergeBase } from '@/git-base'
3
- import { gitEnv } from '@/git-env'
2
+ import { baseCandidates, isMergeBase } from '@/git/base'
3
+ import { gitEnv } from '@/git/env'
4
4
 
5
5
  /** Runs git under `root` with the resolution variables a hook exports stripped. */
6
6
  async function git(
@@ -1,6 +1,6 @@
1
1
  import { resolve } from 'node:path'
2
2
  import { $ } from 'bun'
3
- import { gitEnv } from '@/git-env'
3
+ import { gitEnv } from '@/git/env'
4
4
 
5
5
  /**
6
6
  * Reports which of `candidates` git ignores under `root`.
@@ -1,5 +1,5 @@
1
1
  import { $ } from 'bun'
2
- import { gitEnv } from '@/git-env'
2
+ import { gitEnv } from '@/git/env'
3
3
 
4
4
  /**
5
5
  * Resolves the root of the checkout the caller is standing in, which is the
@@ -3,7 +3,7 @@ import { readFile } from 'node:fs/promises'
3
3
  import { dirname, join, resolve } from 'node:path'
4
4
  import { $ } from 'bun'
5
5
  import { isMarked } from '@/sweep/exempt-marker'
6
- import { gitEnv } from '@/git-env'
6
+ import { gitEnv } from '@/git/env'
7
7
  import { listRuleFiles } from '@/gov/payload'
8
8
  import { parseFrontmatter } from '@/indexes/frontmatter'
9
9
  import { findBacktickSpans } from '@/markdown/backticks'
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { basename, dirname, join, relative } from 'node:path'
3
- import { copyPreservingMode } from '@/copy'
3
+ import { copyPreservingMode } from '@/sync/copy'
4
4
 
5
5
  export interface RuleSource {
6
6
  readonly rule: string
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
3
  import { isBinary } from '@/sweep/binary'
4
4
  import { isMarked } from '@/sweep/exempt-marker'
5
- import { listRepositoryFiles } from '@/git-files'
5
+ import { listRepositoryFiles } from '@/git/files'
6
6
 
7
7
  /**
8
8
  * The inline token exempting one line from this sweep, shaped on the
@@ -1,6 +1,6 @@
1
1
  import { execaSync } from 'execa'
2
- import { baseCandidates, isMergeBase } from '@/git-base'
3
- import { gitEnv } from '@/git-env'
2
+ import { baseCandidates, isMergeBase } from '@/git/base'
3
+ import { gitEnv } from '@/git/env'
4
4
 
5
5
  /**
6
6
  * The extensions this check can pair. A test sits beside its subject under one
package/src/help.ts CHANGED
@@ -105,7 +105,7 @@ const EXAMPLES = [
105
105
  'canon canvas list --json',
106
106
  'canon slides render',
107
107
  'canon slides list --json',
108
- 'canon capture assets/captures/install.html --selector .window --out assets/evidence',
108
+ 'canon capture assets/captures/install.html --selector .window --out assets/frames',
109
109
  'canon serve .canon/teach',
110
110
  'canon inventory focus --json',
111
111
  'canon drive http://localhost:4173 run.json --json',
@@ -1,5 +1,5 @@
1
1
  import { execa } from 'execa'
2
- import { gitEnv } from '@/git-env'
2
+ import { gitEnv } from '@/git/env'
3
3
 
4
4
  /**
5
5
  * One child `canon` call. Each step runs as its own process rather than
@@ -1,6 +1,6 @@
1
1
  import { existsSync, statSync } from 'node:fs'
2
2
  import { dirname, resolve } from 'node:path'
3
- import { listIgnored } from '@/git-ignore'
3
+ import { listIgnored } from '@/git/ignore'
4
4
 
5
5
  const INDEX_FILE = 'index.md'
6
6
 
@@ -9,7 +9,7 @@ import {
9
9
  readItems,
10
10
  writeAnswerLine,
11
11
  } from '@/intake/items'
12
- import { recordDir } from '@/record-root'
12
+ import { recordDir } from '@/roots/record'
13
13
 
14
14
  export const INTAKE_REFUSALS = [
15
15
  'no-intake',
@@ -1,4 +1,4 @@
1
- import { listChangedFiles, resolveBaseRef } from '@/git-files'
1
+ import { listChangedFiles, resolveBaseRef } from '@/git/files'
2
2
  import { type Coverage, resolveCoverage } from '@/labels/coverage'
3
3
  import { type MapRefusal, readLabelMap } from '@/labels/map'
4
4
 
package/src/labels/map.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
- import { resolveExisting } from '@/legacy-path'
3
+ import { resolveExisting } from '@/roots/legacy'
4
4
 
5
5
  /**
6
6
  * Where a project declares its pull request label map.
@@ -1,6 +1,6 @@
1
1
  import { linesOutsideFences, maskCodeSpans } from '@/markdown/scan'
2
- import { RECORD_ENTRIES, RECORD_ROOTS } from '@/record-root'
3
- import type { RecordRoot } from '@/record-root'
2
+ import { RECORD_ENTRIES, RECORD_ROOTS } from '@/roots/record'
3
+ import type { RecordRoot } from '@/roots/record'
4
4
 
5
5
  /**
6
6
  * The two version namespaces `standards/versioning.md` keeps apart, and why a
@@ -114,7 +114,7 @@ const ROOT_READING: Record<RecordRoot, 'whole' | 'entries'> = {
114
114
  * `.claude/` is tracked and holds `rules`, `skills`, `hooks`, and `context`,
115
115
  * so a rule path resolves in any clone and is not a board reference, and only
116
116
  * the entries the record move relocated are unreadable there. Reading the
117
- * roots and the relocated entries from `src/record-root.ts` is what makes a
117
+ * roots and the relocated entries from `src/roots/record.ts` is what makes a
118
118
  * folder added there matched here without an edit, and the list above is what
119
119
  * covers the one thing that module deliberately does not carry.
120
120
  *
@@ -1,6 +1,6 @@
1
1
  import { existsSync, statSync } from 'node:fs'
2
2
  import { relative, resolve } from 'node:path'
3
- import { listRepositoryFiles } from '@/git-files'
3
+ import { listRepositoryFiles } from '@/git/files'
4
4
 
5
5
  const MARKDOWN = /\.md$/
6
6
 
@@ -1,7 +1,7 @@
1
1
  import { $ } from 'bun'
2
2
  import { readFile, mkdir, rename, rmdir, writeFile } from 'node:fs/promises'
3
3
  import { dirname, join, relative } from 'node:path'
4
- import { gitEnv } from '@/git-env'
4
+ import { gitEnv } from '@/git/env'
5
5
  import type { RenamePlan, RenameSource } from '@/migrate/plan'
6
6
  import { FROM_ROOT as OLD_ROOT, type RecordsPlan } from '@/migrate/records'
7
7
 
@@ -40,7 +40,7 @@ import {
40
40
  slugOf,
41
41
  } from '@/migrate/evidence-ordinal'
42
42
  import { presentFolders } from '@/records/backup'
43
- import { recordDir, SCRATCH, spell, type RecordRoot } from '@/record-root'
43
+ import { recordDir, SCRATCH, spell, type RecordRoot } from '@/roots/record'
44
44
 
45
45
  const CANON: RecordRoot = '.canon'
46
46
  const CLAUDE: RecordRoot = '.claude'
@@ -15,7 +15,7 @@ import {
15
15
  RECORD_ENTRIES,
16
16
  RECORD_ROOTS,
17
17
  spell,
18
- } from '@/record-root'
18
+ } from '@/roots/record'
19
19
 
20
20
  /** The root the entries below leave, exported so the writer can prune it. */
21
21
  export const FROM_ROOT: RecordRoot = '.claude'
@@ -26,7 +26,7 @@ const TO_ROOT: RecordRoot = '.canon'
26
26
  /**
27
27
  * Every entry the move relocates, at the name `.claude/` gave it.
28
28
  *
29
- * The list is `RECORD_ENTRIES`, which `record-root.ts` owns because a seed
29
+ * The list is `RECORD_ENTRIES`, which `roots/record.ts` owns because a seed
30
30
  * destination and a superseded-layout report ask the same question. Restating
31
31
  * it here would let the sweep and the resolver disagree about what moved.
32
32
  */
@@ -76,12 +76,12 @@ function escape(value: string): string {
76
76
  * paths a session actually opened. Rewriting either makes it testify to
77
77
  * something that never happened.
78
78
  *
79
- * This module and `src/record-root.ts` are two of the sources that state the old
80
- * root on purpose. Sweeping them turns every citation this expression is built
81
- * from into its own replacement, leaving a rewriter that maps `.canon/` to
82
- * `.canon/` and matches nothing. `src/surface-root.ts` joins the list a release
83
- * early, for the same reason: it spells `.claude` as data the moment it
84
- * exists, and excluding it later would leave one release where a records
79
+ * This module and `src/roots/record.ts` are two of the sources that state the
80
+ * old root on purpose. Sweeping them turns every citation this expression is
81
+ * built from into its own replacement, leaving a rewriter that maps `.canon/`
82
+ * to `.canon/` and matches nothing. `src/roots/surface.ts` joins the list a
83
+ * release early, for the same reason: it spells `.claude` as data the moment
84
+ * it exists, and excluding it later would leave one release where a records
85
85
  * migration in a target could rewrite the resolver that migration itself
86
86
  * depends on.
87
87
  *
@@ -112,8 +112,8 @@ const EXCLUDED_PREFIXES: readonly string[] = [
112
112
 
113
113
  const EXCLUDED_PATHS: readonly string[] = [
114
114
  'CHANGELOG.md',
115
- 'src/record-root.ts',
116
- 'src/surface-root.ts',
115
+ 'src/roots/record.ts',
116
+ 'src/roots/surface.ts',
117
117
  ]
118
118
 
119
119
  const EXCLUDED_SUFFIXES: readonly string[] = ['.test.ts']
@@ -171,7 +171,7 @@ export const RECORD_ONLY_ROOTS: readonly RecordRoot[] = RECORD_ROOTS.filter(
171
171
  * under `.claude/`, and a target's installed `.claude/rules/canon/core/035-tasks.md`
172
172
  * is the file the sweep exists to repoint, so a bare `.claude/` prefix strands
173
173
  * it silently. The old root is therefore entry-scoped, through `spell` so the
174
- * one naming variant stays decided in `record-root.ts`.
174
+ * one naming variant stays decided in `roots/record.ts`.
175
175
  *
176
176
  * Joined with a literal separator rather than through `join`, the way
177
177
  * `EXCLUDED_PREFIXES` already is. These are matched against what `git ls-files`
@@ -34,7 +34,7 @@ import {
34
34
  slugOf,
35
35
  } from '@/migrate/evidence-ordinal'
36
36
  import { presentFolders } from '@/records/backup'
37
- import { recordDir, SCRATCH } from '@/record-root'
37
+ import { recordDir, SCRATCH } from '@/roots/record'
38
38
 
39
39
  /**
40
40
  * Every folder this promotion moves, at the name it carries under scratch.
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { join } from 'node:path'
12
- import { SURFACE_ENTRIES, spell, type SurfaceRoot } from '@/surface-root'
12
+ import { SURFACE_ENTRIES, spell, type SurfaceRoot } from '@/roots/surface'
13
13
 
14
14
  /** The root the entries below leave. */
15
15
  export const FROM_ROOT: SurfaceRoot = '.claude'
@@ -80,8 +80,8 @@ function escape(value: string): string {
80
80
  *
81
81
  * The changelog is release history, and an eval result is a transcript of
82
82
  * paths a session actually opened; rewriting either makes it testify to
83
- * something that never happened. `src/surface-root.ts` and
84
- * `src/record-root.ts` are the two sources that spell the old root as data on
83
+ * something that never happened. `src/roots/surface.ts` and
84
+ * `src/roots/record.ts` are the two sources that spell the old root as data on
85
85
  * purpose, so sweeping either would turn the very literals this verb reads
86
86
  * into their own replacement. A hook is the third such source, guarding on a
87
87
  * `case` carrying an arm per root, so `.claude/hooks/` and the seed's copy of
@@ -102,8 +102,8 @@ const EXCLUDED_PREFIXES: readonly string[] = [
102
102
 
103
103
  const EXCLUDED_PATHS: readonly string[] = [
104
104
  'CHANGELOG.md',
105
- 'src/surface-root.ts',
106
- 'src/record-root.ts',
105
+ 'src/roots/surface.ts',
106
+ 'src/roots/record.ts',
107
107
  ]
108
108
 
109
109
  export function isExcludedPath(path: string): boolean {
@@ -380,7 +380,7 @@ export interface SurfaceRootsPlan {
380
380
  * moved entry, and whether its content cites one. Either alone is enough to
381
381
  * produce an entry, and a file excluded from the citation rewrite still moves
382
382
  * when its path itself is inside a moved entry, since the two entries excluded
383
- * today, `src/surface-root.ts` and `src/record-root.ts`, both sit outside
383
+ * today, `src/roots/surface.ts` and `src/roots/record.ts`, both sit outside
384
384
  * every moved entry and never take that branch in practice.
385
385
  */
386
386
  export function planSurfaceRootsMove(
@@ -1,4 +1,4 @@
1
- import type { RenamePair } from '@/git-files'
1
+ import type { RenamePair } from '@/git/files'
2
2
  import { extractKeyChangePaths, KEY_CHANGES, type PathClaim } from '@/pr/paths'
3
3
 
4
4
  /**