@erclx/canon 5.32.0 → 5.34.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 (250) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/api-design/REQUIREMENT.md +1 -2
  3. package/claude/skills/api-design/SKILL.md +2 -3
  4. package/claude/skills/api-design/references/adopted.md +1 -1
  5. package/claude/skills/auto-ship/SKILL.md +1 -1
  6. package/claude/skills/auto-ship/references/worktree-name.md +1 -1
  7. package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
  8. package/claude/skills/backlog-triage/SKILL.md +1 -1
  9. package/claude/skills/canon-cli/REQUIREMENT.md +16 -13
  10. package/claude/skills/canon-cli/SKILL.md +75 -32
  11. package/claude/skills/canon-feedback/REQUIREMENT.md +1 -0
  12. package/claude/skills/canon-feedback/SKILL.md +11 -29
  13. package/claude/skills/canon-feedback/references/template.md +53 -0
  14. package/claude/skills/{canvas → canvas-board}/REQUIREMENT.md +2 -2
  15. package/claude/skills/{canvas → canvas-board}/SKILL.md +6 -3
  16. package/claude/skills/{canvas → canvas-board}/references/canvas.md +26 -3
  17. package/{standards → claude/skills/canvas-board/references}/design.md +11 -2
  18. package/claude/skills/code-craft/REQUIREMENT.md +1 -1
  19. package/claude/skills/code-craft/SKILL.md +0 -1
  20. package/claude/skills/context-fold/REQUIREMENT.md +25 -33
  21. package/claude/skills/context-fold/SKILL.md +25 -70
  22. package/claude/skills/context-fold/references/classify.md +3 -3
  23. package/claude/skills/context-fold/references/completion-report.md +4 -4
  24. package/claude/skills/context-fold/references/context-refresh.md +5 -5
  25. package/{standards → claude/skills/context-fold/references}/context.md +16 -5
  26. package/claude/skills/context-fold/references/unusable-baseline.md +3 -5
  27. package/claude/skills/create-rule/SKILL.md +3 -3
  28. package/{standards → claude/skills/create-rule/references}/rule.md +15 -6
  29. package/claude/skills/create-skill/SKILL.md +2 -2
  30. package/{standards → claude/skills/create-skill/references}/skill.md +18 -15
  31. package/claude/skills/design-taste/REQUIREMENT.md +3 -3
  32. package/claude/skills/design-taste/SKILL.md +1 -1
  33. package/claude/skills/docs-sync/REQUIREMENT.md +1 -1
  34. package/claude/skills/docs-sync/SKILL.md +3 -3
  35. package/{standards → claude/skills/docs-sync/references}/docs.md +11 -0
  36. package/{standards → claude/skills/docs-sync/references}/readme.md +9 -2
  37. package/claude/skills/draft-and-pick/SKILL.md +5 -5
  38. package/claude/skills/draft-doc/SKILL.md +6 -6
  39. package/claude/skills/draft-figure/REQUIREMENT.md +2 -2
  40. package/claude/skills/draft-figure/SKILL.md +2 -2
  41. package/claude/skills/draft-figure/references/hand-drawn.md +1 -1
  42. package/claude/skills/draft-figure/references/mermaid-path.md +1 -1
  43. package/claude/skills/draft-identity/REQUIREMENT.md +2 -2
  44. package/claude/skills/draft-identity/SKILL.md +2 -2
  45. package/claude/skills/draft-ready/SKILL.md +1 -1
  46. package/claude/skills/git-branch/SKILL.md +1 -1
  47. package/{standards → claude/skills/git-branch/references}/branch.md +1 -1
  48. package/claude/skills/git-commit/SKILL.md +2 -2
  49. package/claude/skills/git-followup/REQUIREMENT.md +1 -1
  50. package/claude/skills/git-followup/SKILL.md +1 -1
  51. package/claude/skills/git-issue/SKILL.md +1 -1
  52. package/claude/skills/git-issue/references/issue.md +4 -1
  53. package/claude/skills/git-pr/SKILL.md +13 -13
  54. package/claude/skills/git-pr/references/evidence.md +2 -2
  55. package/claude/skills/git-pr/references/final-command.md +38 -14
  56. package/claude/skills/git-pr/references/labels.md +5 -1
  57. package/{standards → claude/skills/git-pr/references}/pr.md +19 -9
  58. package/{standards → claude/skills/git-pr/references}/publish.md +4 -2
  59. package/claude/skills/git-pr/references/task-number.md +1 -1
  60. package/claude/skills/git-ship/SKILL.md +15 -14
  61. package/claude/skills/git-split/SKILL.md +3 -3
  62. package/claude/skills/git-stage/SKILL.md +1 -1
  63. package/claude/skills/plan-feature/REQUIREMENT.md +1 -1
  64. package/claude/skills/review-address/SKILL.md +1 -1
  65. package/claude/skills/review-address/references/post-review-findings.md +1 -1
  66. package/claude/skills/review-craft/references/ui.md +1 -1
  67. package/claude/skills/review-pr/REQUIREMENT.md +1 -1
  68. package/claude/skills/review-pr/SKILL.md +3 -3
  69. package/claude/skills/review-pr/references/body-variants.md +2 -2
  70. package/claude/skills/review-pr/references/key-changes.md +3 -3
  71. package/claude/skills/review-pr/references/post.md +4 -2
  72. package/claude/skills/review-ui/REQUIREMENT.md +1 -1
  73. package/claude/skills/review-ui/SKILL.md +8 -6
  74. package/claude/skills/review-ui/references/driving.md +8 -8
  75. package/claude/skills/review-ui/references/post.md +53 -17
  76. package/claude/skills/role-orchestrator/REQUIREMENT.md +3 -0
  77. package/claude/skills/role-orchestrator/SKILL.md +2 -2
  78. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +2 -2
  79. package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +8 -5
  80. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +32 -1
  81. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +2 -2
  82. package/claude/skills/role-orchestrator/references/orchestrator-resume.md +3 -0
  83. package/claude/skills/role-planner/REQUIREMENT.md +2 -1
  84. package/claude/skills/role-planner/SKILL.md +13 -2
  85. package/claude/skills/role-reviewer/SKILL.md +1 -1
  86. package/claude/skills/role-worker/REQUIREMENT.md +1 -0
  87. package/claude/skills/role-worker/SKILL.md +9 -1
  88. package/claude/skills/session-map/SKILL.md +1 -1
  89. package/claude/skills/session-resume/REQUIREMENT.md +2 -2
  90. package/claude/skills/session-worktree/SKILL.md +5 -5
  91. package/claude/skills/target-setup/SKILL.md +10 -4
  92. package/claude/skills/target-setup/references/detect.md +20 -3
  93. package/claude/skills/task-board/REQUIREMENT.md +7 -12
  94. package/claude/skills/task-board/SKILL.md +5 -17
  95. package/claude/skills/task-board/references/tasks.md +2 -2
  96. package/claude/skills/task-complete/REQUIREMENT.md +47 -0
  97. package/claude/skills/task-complete/SKILL.md +86 -0
  98. package/claude/skills/{task-board → task-complete}/references/archive.md +4 -4
  99. package/claude/skills/{context-fold → task-complete}/references/receipt-sweep.md +5 -5
  100. package/claude/skills/task-complete/references/unusable-baseline.md +19 -0
  101. package/claude/skills/teach-workspace/REQUIREMENT.md +6 -2
  102. package/claude/skills/teach-workspace/SKILL.md +8 -4
  103. package/claude/skills/teach-workspace/references/lesson-craft.md +1 -1
  104. package/claude/skills/{context-fold/references/promoted-pages.md → teach-workspace/references/promotion-fold.md} +3 -3
  105. package/claude/skills/teach-workspace/references/promotion.md +3 -3
  106. package/claude/skills/test-craft/REQUIREMENT.md +2 -3
  107. package/claude/skills/test-craft/SKILL.md +15 -3
  108. package/claude/skills/ui-checklist/SKILL.md +1 -1
  109. package/claude/skills/ux-audit/SKILL.md +1 -1
  110. package/claude/skills/ux-measure/SKILL.md +1 -1
  111. package/docs/agents/canvas.md +6 -2
  112. package/docs/agents/commands.md +4 -3
  113. package/docs/agents/context-audit-checks.md +4 -4
  114. package/docs/agents/gov-detect.md +47 -0
  115. package/docs/agents/index.md +7 -4
  116. package/docs/agents/indexes.md +1 -1
  117. package/docs/agents/install-and-sync.md +10 -11
  118. package/docs/agents/key-changes.md +5 -5
  119. package/docs/agents/label-coverage.md +11 -0
  120. package/docs/agents/migrate.md +1 -1
  121. package/docs/agents/pr-evidence-layout.md +70 -0
  122. package/docs/agents/pr-evidence.md +8 -9
  123. package/docs/agents/pr-frames.md +7 -2
  124. package/docs/agents/pr-lift.md +132 -0
  125. package/docs/agents/records-push.md +2 -2
  126. package/docs/agents/records-stale.md +12 -47
  127. package/docs/agents/records-validate.md +3 -7
  128. package/docs/agents/records.md +5 -27
  129. package/docs/agents/rule-citations.md +1 -1
  130. package/docs/agents/sandbox.md +4 -4
  131. package/docs/agents/scripting.md +2 -1
  132. package/docs/agents/skills-audit.md +1 -1
  133. package/docs/agents/tasks-plans.md +2 -2
  134. package/docs/agents/tasks.md +2 -2
  135. package/docs/target/migrations.md +10 -2
  136. package/docs/target/projects.md +4 -4
  137. package/docs/target/sync.md +1 -1
  138. package/docs/workflow/operating-model.md +2 -2
  139. package/docs/workflow/parallel-features.md +7 -7
  140. package/docs/workflow/skill-map-on-request.md +9 -9
  141. package/docs/workflow/skill-map.md +12 -11
  142. package/docs/workflow/visual-design-workflow.md +2 -2
  143. package/governance/detect.toml +201 -0
  144. package/governance/renames.toml +0 -11
  145. package/governance/rules/canon/context.md +13 -0
  146. package/governance/rules/claude/skill.md +17 -0
  147. package/governance/rules/code/000-code.md +1 -13
  148. package/governance/rules/code/010-testing.md +2 -30
  149. package/governance/rules/ui/400-ui.md +1 -1
  150. package/governance/rules/ui/480-canvas.md +13 -0
  151. package/governance/stacks/base.toml +2 -4
  152. package/package.json +4 -4
  153. package/scripts/core/install-check.sh +3 -3
  154. package/scripts/core/regen-hero.ts +0 -6
  155. package/scripts/lib/ui.sh +0 -168
  156. package/src/audits/catalog.ts +0 -1
  157. package/src/canvas/arrange.ts +66 -0
  158. package/src/canvas/capture.ts +20 -6
  159. package/src/canvas/content.ts +39 -0
  160. package/src/canvas/fit.ts +238 -0
  161. package/src/canvas/tokens.ts +28 -10
  162. package/src/capture/frame-values.ts +2 -32
  163. package/src/claude/cases/misc.ts +1 -1
  164. package/src/claude/cases/setup.ts +4 -0
  165. package/src/claude/cases/workflow.ts +6 -1
  166. package/src/claude/seeds.ts +1 -1
  167. package/src/claude/skills-audit.ts +2 -2
  168. package/src/cli/help.ts +3 -1
  169. package/src/cli/ui.ts +2 -3
  170. package/src/commands/canvas.ts +179 -1
  171. package/src/commands/claude.ts +1 -1
  172. package/src/commands/gov.ts +146 -16
  173. package/src/commands/labels.ts +326 -7
  174. package/src/commands/migrate.ts +6 -1
  175. package/src/commands/pr.ts +377 -2
  176. package/src/commands/records.ts +13 -399
  177. package/src/commands/sandbox.ts +3 -6
  178. package/src/commands/tasks.ts +3 -3
  179. package/src/context/audit.ts +5 -5
  180. package/src/context/folders.ts +1 -1
  181. package/src/design/budget.ts +1 -1
  182. package/src/design/components.ts +1 -1
  183. package/src/design/document.ts +1 -1
  184. package/src/design/regen.ts +1 -1
  185. package/src/design/render.ts +1 -1
  186. package/src/feedback/body.ts +26 -5
  187. package/src/gate/measures.ts +59 -45
  188. package/src/gate/sequencer.ts +56 -1
  189. package/src/gate/stages.ts +4 -11
  190. package/src/gov/detect.ts +301 -0
  191. package/src/gov/install.ts +2 -3
  192. package/src/gov/list.ts +23 -2
  193. package/src/gov/stacks.ts +10 -1
  194. package/src/hooks/post-merge.ts +1 -1
  195. package/src/labels/apply.ts +39 -0
  196. package/src/labels/format.ts +3 -3
  197. package/src/labels/phase.ts +3 -3
  198. package/src/labels/sections.ts +43 -0
  199. package/src/migrate/skill-names.ts +20 -1
  200. package/src/pr/evidence.ts +218 -45
  201. package/src/pr/frames.ts +23 -3
  202. package/src/pr/lift.ts +418 -0
  203. package/src/pr/paths.ts +4 -4
  204. package/src/pr/rest.ts +3 -0
  205. package/src/pr/review-scope.ts +76 -0
  206. package/src/records/backup.ts +16 -6
  207. package/src/records/stale-canonical.ts +31 -1
  208. package/src/records/validate.ts +2 -188
  209. package/src/sandbox/census.ts +1 -1
  210. package/src/sandbox/coverage.ts +2 -6
  211. package/src/sandbox/equivalence.ts +19 -51
  212. package/src/sandbox/expect.ts +4 -4
  213. package/src/sandbox/provision.ts +13 -36
  214. package/src/sandbox/scenario.ts +26 -48
  215. package/src/sandbox/tree.ts +2 -6
  216. package/src/standards/read.ts +14 -1
  217. package/src/sync/git.ts +1 -1
  218. package/src/tasks/archive.ts +5 -5
  219. package/src/tasks/branch.ts +3 -3
  220. package/src/tasks/label.ts +1 -1
  221. package/src/tasks/record.ts +2 -2
  222. package/src/tasks/validate.ts +1 -1
  223. package/standards/index.md +0 -13
  224. package/standards/standard.md +1 -7
  225. package/tooling/base/reference.md +1 -1
  226. package/tooling/base/seeds/.github/pull_request_template.md +2 -2
  227. package/tooling/claude/reference.md +1 -1
  228. package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +9 -1
  229. package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +171 -0
  230. package/tooling/claude/seeds/.claude/settings.json +9 -0
  231. package/governance/rules/standards/context.md +0 -14
  232. package/governance/rules/standards/design.md +0 -12
  233. package/governance/rules/standards/readme.md +0 -12
  234. package/governance/rules/standards/rule.md +0 -13
  235. package/governance/rules/standards/skill.md +0 -21
  236. package/governance/rules/standards/standard.md +0 -13
  237. package/scripts/config.sh +0 -11
  238. package/scripts/lib/gov.sh +0 -14
  239. package/scripts/lib/sandbox-fixtures.sh +0 -191
  240. package/scripts/lib/sandbox-git.sh +0 -125
  241. package/scripts/lib/sandbox-path.sh +0 -50
  242. package/scripts/sandbox-hook.sh +0 -99
  243. package/src/gov/standard-rules.ts +0 -179
  244. package/src/records/migrate.ts +0 -78
  245. package/src/records/stale.ts +0 -243
  246. package/src/sandbox/hooks.ts +0 -128
  247. /package/{standards → claude/skills/draft-figure/references}/figures.md +0 -0
  248. /package/{standards → claude/skills/git-branch/references}/slug.md +0 -0
  249. /package/{standards → claude/skills/git-commit/references}/commit.md +0 -0
  250. /package/{standards → claude/skills/git-commit/references}/versioning.md +0 -0
@@ -22,7 +22,6 @@ export const RECORD_KINDS = [
22
22
  'plans',
23
23
  'groundwork',
24
24
  'intake',
25
- 'memory',
26
25
  'standards',
27
26
  'teach',
28
27
  ] as const
@@ -60,7 +59,6 @@ export type ValidateRefusal = (typeof VALIDATE_REFUSALS)[number]
60
59
  export const FINDING_KINDS = [
61
60
  'name-malformed',
62
61
  'title-missing',
63
- 'title-is-slug',
64
62
  'section-missing',
65
63
  'scope-unanchored',
66
64
  'entry-unreasoned',
@@ -72,7 +70,6 @@ export const FINDING_KINDS = [
72
70
  'state-missing',
73
71
  'closing-partial',
74
72
  'item-incomplete',
75
- 'category-mismatch',
76
73
  'operator-call-phrasing',
77
74
  'batch-unsplit',
78
75
  'source-unquoted',
@@ -81,22 +78,12 @@ export const FINDING_KINDS = [
81
78
 
82
79
  export type FindingKind = (typeof FINDING_KINDS)[number]
83
80
 
84
- /**
85
- * The transforms `migrate.ts` carries. Most finding kinds have none, since a
86
- * transform is only safe where the old shape is recoverable from the file
87
- * itself, so this stays optional on `Finding` rather than required.
88
- */
89
- export const FINDING_REMEDIES = ['category-from-name'] as const
90
-
91
- export type FindingRemedy = (typeof FINDING_REMEDIES)[number]
92
-
93
81
  export interface Finding {
94
82
  readonly kind: FindingKind
95
83
  /** The record the finding sits in, relative to the validated folder. */
96
84
  readonly record: string
97
85
  readonly subject: string
98
86
  readonly message: string
99
- readonly remedy?: FindingRemedy
100
87
  }
101
88
 
102
89
  export interface ValidateReport {
@@ -214,9 +201,8 @@ function finding(
214
201
  record: string,
215
202
  subject: string,
216
203
  message: string,
217
- remedy?: FindingRemedy,
218
204
  ): Finding {
219
- return { kind, record, subject, message, remedy }
205
+ return { kind, record, subject, message }
220
206
  }
221
207
 
222
208
  async function listMarkdown(dir: string): Promise<string[]> {
@@ -934,169 +920,6 @@ async function checkWorkspace(dir: string, slug: string): Promise<Finding[]> {
934
920
  ]
935
921
  }
936
922
 
937
- const MEMORY_INDEX = 'index.md'
938
- const MEMORY_FIELDS = ['title', 'description', 'category'] as const
939
-
940
- /**
941
- * The filename prefix and the `category` field are one fact in two spellings,
942
- * so the map is the whole type list and the comparison against it is what
943
- * catches a prefix outside the set, a field disagreeing with the prefix, and a
944
- * casing drift that would open a second group in the catalog.
945
- */
946
- export const CATEGORY_BY_TYPE = {
947
- feedback: 'Feedback',
948
- project: 'Project',
949
- user: 'User',
950
- reference: 'Reference',
951
- } as const
952
-
953
- export type MemoryType = keyof typeof CATEGORY_BY_TYPE
954
-
955
- const MEMORY_TYPES = Object.keys(CATEGORY_BY_TYPE) as readonly MemoryType[]
956
-
957
- export const MEMORY_NAME = /^([a-z]+)-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/
958
-
959
- /** The two markers a rule-bearing body carries, on top of the rule line itself. */
960
- const MEMORY_MARKERS = ['**Why:**', '**How to apply:**'] as const
961
-
962
- export function memoryType(value: string): MemoryType | undefined {
963
- return MEMORY_TYPES.find((type) => type === value)
964
- }
965
-
966
- /**
967
- * The one missing-field shape `migrate.ts` can repair: `category` alone,
968
- * recoverable from the same filename prefix `checkMemory` already read it
969
- * from. `title` and `description` are prose nobody wrote down, so a finding
970
- * naming either carries no remedy.
971
- */
972
- function memoryRemedy(
973
- missing: readonly string[],
974
- named: MemoryType | undefined,
975
- ): FindingRemedy | undefined {
976
- return named && missing.length === 1 && missing[0] === 'category'
977
- ? 'category-from-name'
978
- : undefined
979
- }
980
-
981
- export function checkMemory(name: string, text: string): Finding[] {
982
- const findings: Finding[] = []
983
- const match = MEMORY_NAME.exec(name)
984
- const named = match ? memoryType(match[1]) : undefined
985
-
986
- if (!named) {
987
- findings.push(
988
- finding(
989
- 'name-malformed',
990
- name,
991
- name,
992
- `is not named <type>-<slug>.md with a type of ${MEMORY_TYPES.join(', ')}.`,
993
- ),
994
- )
995
- }
996
-
997
- const frontmatter = parseFrontmatter(text)
998
- const missing = MEMORY_FIELDS.filter(
999
- (field) => !readField(frontmatter, field),
1000
- )
1001
-
1002
- if (missing.length > 0) {
1003
- findings.push(
1004
- finding(
1005
- 'frontmatter-incomplete',
1006
- name,
1007
- name,
1008
- `carries no ${missing.join(' and no ')}.`,
1009
- memoryRemedy(missing, named),
1010
- ),
1011
- )
1012
- }
1013
-
1014
- // Its own kind rather than `title-missing`, which means an absent heading on a
1015
- // plan. One kind covering both leaves a caller filtering the JSON unable to
1016
- // tell a record with no title from one whose title is its own slug.
1017
- if (readField(frontmatter, 'title') === name.replace(/\.md$/, '')) {
1018
- findings.push(
1019
- finding(
1020
- 'title-is-slug',
1021
- name,
1022
- name,
1023
- 'is titled with its own filename, so the catalog renders a slug where the rule belongs.',
1024
- ),
1025
- )
1026
- }
1027
-
1028
- const category = readField(frontmatter, 'category')
1029
-
1030
- // Reported against the prefix alone. A name the prefix rule already failed
1031
- // has no type to compare against, and reporting it twice names one defect as
1032
- // two.
1033
- if (named && category && category !== CATEGORY_BY_TYPE[named]) {
1034
- findings.push(
1035
- finding(
1036
- 'category-mismatch',
1037
- name,
1038
- category,
1039
- `is not ${CATEGORY_BY_TYPE[named]}, which the filename prefix declares.`,
1040
- ),
1041
- )
1042
- }
1043
-
1044
- return [
1045
- ...findings,
1046
- ...checkMemoryBody(
1047
- name,
1048
- text.slice(frontmatter?.raw.length ?? 0),
1049
- named ?? category,
1050
- ),
1051
- ]
1052
- }
1053
-
1054
- /**
1055
- * A `user` or `reference` entry is a single sentence by design, so the markers
1056
- * are checked only where a rule is being stated. The type is read off the
1057
- * prefix, falling back to the category so a misnamed file is still checked
1058
- * against the shape it claims.
1059
- */
1060
- function checkMemoryBody(
1061
- name: string,
1062
- text: string,
1063
- claimed: string | undefined,
1064
- ): Finding[] {
1065
- const type = claimed && memoryType(claimed.toLowerCase())
1066
- if (type !== 'feedback' && type !== 'project') return []
1067
-
1068
- const body = linesOutsideFences(text).filter((line) => line.trim().length > 0)
1069
-
1070
- const findings: Finding[] = []
1071
- const opening = body[0]
1072
-
1073
- if (!opening || MEMORY_MARKERS.some((marker) => opening.startsWith(marker))) {
1074
- findings.push(
1075
- finding(
1076
- 'section-missing',
1077
- name,
1078
- 'the rule line',
1079
- 'is absent, so the entry carries a rationale with no rule to apply.',
1080
- ),
1081
- )
1082
- }
1083
-
1084
- for (const marker of MEMORY_MARKERS) {
1085
- if (!body.some((line) => line.startsWith(marker))) {
1086
- findings.push(
1087
- finding(
1088
- 'section-missing',
1089
- name,
1090
- marker,
1091
- `is required on a ${type} entry and the body carries no such line.`,
1092
- ),
1093
- )
1094
- }
1095
- }
1096
-
1097
- return findings
1098
- }
1099
-
1100
923
  const STANDARD_INDEX = 'index.md'
1101
924
  const STANDARD_FIELDS = ['title', 'description'] as const
1102
925
 
@@ -1225,7 +1048,7 @@ export function checkStandard(name: string, text: string): Finding[] {
1225
1048
  */
1226
1049
  const FOLDER_CHECK: Readonly<
1227
1050
  Record<
1228
- Exclude<RecordKind, 'plans' | 'memory' | 'standards'>,
1051
+ Exclude<RecordKind, 'plans' | 'standards'>,
1229
1052
  (dir: string, slug: string) => Promise<Finding[]>
1230
1053
  >
1231
1054
  > = {
@@ -1279,15 +1102,6 @@ export async function validateRecords(
1279
1102
 
1280
1103
  if (kind === 'plans') return validateFiles(dir, kind, checkPlan)
1281
1104
 
1282
- if (kind === 'memory') {
1283
- return validateFiles(
1284
- dir,
1285
- kind,
1286
- checkMemory,
1287
- (file) => file === MEMORY_INDEX,
1288
- )
1289
- }
1290
-
1291
1105
  // The walk stays flat, matching install and the catalog.
1292
1106
  if (kind === 'standards') {
1293
1107
  return validateFiles(
@@ -71,7 +71,7 @@ export function listSkills(root: string): string[] {
71
71
  *
72
72
  * `<category>-<command>` is the rule `canon/context/sandbox/coverage/census.md` states, and it
73
73
  * alone pairs 29 of 54 skills. The bare `<command>` fallback is what reaches the
74
- * rest: `claude/target-setup.sh` drives the `target-setup` skill, not a `claude-target-setup` that
74
+ * rest: `claude/target-setup.ts` drives the `target-setup` skill, not a `claude-target-setup` that
75
75
  * does not exist. Stating one spelling and shipping two is what let the audit
76
76
  * report a paired skill as unpaired.
77
77
  *
@@ -44,13 +44,9 @@ function listScenarios(sandboxDir: string): ScenarioCoverage[] {
44
44
  const scenarios: ScenarioCoverage[] = []
45
45
 
46
46
  for (const category of listCategories(sandboxDir)) {
47
- // A stem in both forms counts once, and provisioning refuses it by name.
48
- const { scenarios: files, ambiguous } = listScenarioFiles(
49
- join(sandboxDir, category),
50
- )
51
- const commands = [...files.map((f) => f.command), ...ambiguous].sort()
47
+ const { scenarios: files } = listScenarioFiles(join(sandboxDir, category))
52
48
 
53
- for (const command of commands) {
49
+ for (const { command } of files) {
54
50
  scenarios.push({ category, command, armed: [] })
55
51
  }
56
52
  }
@@ -22,7 +22,6 @@ import {
22
22
  listCategories,
23
23
  listScenarioFiles,
24
24
  loadScenario,
25
- type ScenarioKind,
26
25
  } from '@/sandbox/scenario'
27
26
  import { createStubRemote, readStubManifest } from '@/sandbox/stub-remote'
28
27
 
@@ -153,28 +152,6 @@ export function armKey(arm: Arm): string {
153
152
  return arm.arm === undefined ? scenario : `${scenario}/${arm.arm}`
154
153
  }
155
154
 
156
- /**
157
- * Reads the quoted arguments of `select_or_route_scenario`, joining a call that
158
- * continues across backslash-newline pairs. The first argument is the prompt and
159
- * the rest are arm names. A call this cannot see returns no arms, which the
160
- * coverage cross-check in `enumerateArms` turns into an error rather than a
161
- * smaller list.
162
- */
163
- export function parseArmNames(source: string): string[] {
164
- const start = source.search(/select_or_route_scenario\b/)
165
- if (start === -1) return []
166
-
167
- const call = source
168
- .slice(start)
169
- .replace(/\\\n/g, ' ')
170
- .split('\n')[0]
171
- ?.replace(/^select_or_route_scenario/, '')
172
-
173
- return [...(call ?? '').matchAll(/"([^"]*)"/g)]
174
- .map((match) => match[1] ?? '')
175
- .slice(1)
176
- }
177
-
178
155
  function matchesTarget(
179
156
  category: string,
180
157
  command: string,
@@ -199,25 +176,14 @@ function matchesArm(arm: Arm, targets: readonly string[]): boolean {
199
176
  }
200
177
 
201
178
  /**
202
- * Reads a scenario's arm names and anchor flag: off a bash file's source, and
203
- * off a TypeScript module's own keys, which is what its stage routes on.
179
+ * Reads a scenario's arm names and anchor flag off its module's own keys, which
180
+ * is what its stage routes on.
204
181
  */
205
182
  function readArms(
206
183
  file: string,
207
- kind: ScenarioKind,
208
184
  ):
209
185
  | { ok: true; anchor: boolean; names: string[] }
210
186
  | { ok: false; error: string } {
211
- if (kind === 'sh') {
212
- const source = readFileSync(file, 'utf8')
213
-
214
- return {
215
- ok: true,
216
- anchor: /^use_anchor\s*\(\)/m.test(source),
217
- names: parseArmNames(source),
218
- }
219
- }
220
-
221
187
  try {
222
188
  const definition = loadScenario(file)
223
189
 
@@ -250,17 +216,12 @@ export function enumerateArms(
250
216
  const coverage = collectCoverage(root)
251
217
 
252
218
  for (const category of listCategories(sandboxDir)) {
253
- const { scenarios, ambiguous } = listScenarioFiles(
254
- join(sandboxDir, category),
255
- )
256
- for (const command of ambiguous)
257
- if (matchesTarget(category, command, targets))
258
- errors.push(`${category}:${command} exists as both .sh and .ts`)
219
+ const { scenarios } = listScenarioFiles(join(sandboxDir, category))
259
220
 
260
- for (const { command, file, kind } of scenarios) {
221
+ for (const { command, file } of scenarios) {
261
222
  if (!matchesTarget(category, command, targets)) continue
262
223
 
263
- const declared = readArms(file, kind)
224
+ const declared = readArms(file)
264
225
  if (!declared.ok) {
265
226
  errors.push(`${category}:${command} did not load: ${declared.error}`)
266
227
  continue
@@ -740,13 +701,16 @@ function canonVersion(): string {
740
701
  }
741
702
 
742
703
  /**
743
- * Adds the baseline as a detached worktree of the base ref, so it carries full
744
- * history for the scenarios that read it, and returns a remover that is safe to
745
- * call twice. `node_modules` is linked from the working checkout, since the
746
- * baseline runs the same CLI and carries no install of its own.
704
+ * Adds a detached worktree of `ref` at `path`, so it carries full history for
705
+ * the scenarios that read it, and returns a remover that is safe to call twice.
706
+ * `node_modules` is linked from the working checkout, since the copy runs the
707
+ * same CLI and carries no install of its own.
747
708
  */
748
- function addBaseline(root: string, workDir: string, ref: string): () => void {
749
- const path = join(workDir, 'base')
709
+ export function addDetachedWorktree(
710
+ root: string,
711
+ path: string,
712
+ ref: string,
713
+ ): () => void {
750
714
  execFileSync('git', ['worktree', 'add', '--detach', path, ref], {
751
715
  cwd: root,
752
716
  stdio: ['ignore', 'ignore', 'pipe'],
@@ -800,7 +764,11 @@ export async function runEquivalence(
800
764
 
801
765
  let removeBaseline: () => void
802
766
  try {
803
- removeBaseline = addBaseline(options.root, workDir, options.base)
767
+ removeBaseline = addDetachedWorktree(
768
+ options.root,
769
+ join(workDir, 'base'),
770
+ options.base,
771
+ )
804
772
  } catch (error) {
805
773
  // An unknown ref must not leave an empty scratch folder behind.
806
774
  if (options.out === undefined)
@@ -354,7 +354,7 @@ function checkWriteScope(
354
354
  // none, and without this the declaration vanishes from the verdict entirely:
355
355
  // no result, no skipped entry, and no contribution to the unchecked count that
356
356
  // exists to surface exactly this. The `undefined` branch above cannot stand in,
357
- // since `run.sh` always passes `--writes` and `readPathList` returns `[]` for an
357
+ // since `canon sandbox run` always passes `--writes` and `readPathList` returns `[]` for an
358
358
  // empty file. An arm whose output escaped the snapshot reads as a clean run,
359
359
  // which is the vacuous pass the harness exists to remove.
360
360
  if (writes.length === 0) {
@@ -377,7 +377,7 @@ function checkWriteScope(
377
377
  }
378
378
 
379
379
  /**
380
- * `run.sh` watches two toolkit roots for a write to shared session scratch
380
+ * `canon sandbox run` watches two toolkit roots for a write to shared session scratch
381
381
  * during a run, and reports every one it finds as an unattributed escape with
382
382
  * no arm able to fail on it. An arm whose skill legitimately reaches outside
383
383
  * the sandbox tree declares the destinations here, and the harness asserts
@@ -388,8 +388,8 @@ function checkWriteScope(
388
388
  * nothing skips rather than passing on a fabricated zero. An escape-scope
389
389
  * declaration exists to bound a side effect nothing requires, so zero escapes
390
390
  * is the outcome a correct run produces and reports as a pass outright,
391
- * provided a watched root held something to watch. `run.sh`'s `snapshot_root`
392
- * returns an empty manifest both when a watch ran clean and when none of the
391
+ * provided a watched root held something to watch. `snapshotRoot` in
392
+ * `src/sandbox/headless/watch.ts` returns an empty manifest both when a watch ran clean and when none of the
393
393
  * four watched directories existed under a root, and the two produce the same
394
394
  * empty `escapes` list. `watched` is what tells them apart: a run that had
395
395
  * nothing to watch reports unmeasured rather than passing on a diff it never
@@ -8,7 +8,6 @@ import {
8
8
  writeFileSync,
9
9
  } from 'node:fs'
10
10
  import { basename, dirname, join, relative } from 'node:path'
11
- import { probeScenario, stageScenario } from '@/sandbox/hooks'
12
11
  import {
13
12
  FIXTURE_SUFFIX,
14
13
  FIXTURES_DIR,
@@ -34,9 +33,8 @@ import {
34
33
 
35
34
  /**
36
35
  * Provisions one scenario into the sandbox tree, and resets or cleans that tree.
37
- * A bash scenario's hooks run through `sandbox-hook.sh` and a TypeScript one's
38
- * arm runs in process through `scenario.ts`, and everything between them runs
39
- * here, in the order the retired bash dispatcher held.
36
+ * A scenario's arm runs in process through `scenario.ts`, and everything
37
+ * around it runs here, in the order the retired bash dispatcher held.
40
38
  *
41
39
  * Narration is part of the contract. `canon sandbox equivalence` compares the
42
40
  * log line for line against the bash harness, so every message, its frame
@@ -403,36 +401,22 @@ interface Scenario {
403
401
 
404
402
  /**
405
403
  * Reads what a scenario declares ahead of provisioning: its config exports and
406
- * whether it stages from the anchor. A bash scenario answers through the probe,
407
- * and a TypeScript one from its module, which is also what its stage runs.
404
+ * whether it stages from the anchor, from its module, which is also what its
405
+ * stage runs.
408
406
  */
409
407
  function declareScenario(
410
408
  root: string,
411
409
  scenario: Scenario,
412
410
  env: NodeJS.ProcessEnv,
413
- ):
414
- | { kind: 'sh'; file: string; isAnchor: boolean }
415
- | { kind: 'ts'; definition: ScenarioDefinition; isAnchor: boolean } {
411
+ ): { definition: ScenarioDefinition; isAnchor: boolean } {
416
412
  const name = `${scenario.category}/${scenario.command}`
417
413
  const resolved = resolveScenarioFile(
418
414
  join(root, 'sandbox'),
419
415
  scenario.category,
420
416
  scenario.command,
421
417
  )
422
- if (!resolved.ok && resolved.reason === 'ambiguous')
423
- fail(
424
- `Sandbox script ${name} exists as both .sh and .ts. Delete the one the port replaced.`,
425
- )
426
418
  if (!resolved.ok) fail(`Sandbox script not found: ${name}`)
427
419
 
428
- if (resolved.kind === 'sh') {
429
- const probe = probeScenario(resolved.file, env)
430
- if (!probe.ok) throw new Halt(probe.status)
431
- Object.assign(env, probe.exports)
432
-
433
- return { kind: 'sh', file: resolved.file, isAnchor: probe.isAnchor }
434
- }
435
-
436
420
  let definition: ScenarioDefinition
437
421
  try {
438
422
  definition = loadScenario(resolved.file)
@@ -443,7 +427,7 @@ function declareScenario(
443
427
  }
444
428
  Object.assign(env, scenarioExports(definition, root, env))
445
429
 
446
- return { kind: 'ts', definition, isAnchor: definition.anchor === true }
430
+ return { definition, isAnchor: definition.anchor === true }
447
431
  }
448
432
 
449
433
  async function provisionScenario(
@@ -481,21 +465,14 @@ async function provisionScenario(
481
465
  // A scenario that execs a verb as its last step hands the run to that verb,
482
466
  // whose own frame is the last one written, so nothing follows it here. An
483
467
  // in-process arm writes its exports straight into `env`.
484
- const staged =
485
- declared.kind === 'sh'
486
- ? stageScenario(declared.file, env)
487
- : {
488
- ...(await stageScenarioInProcess(declared.definition, {
489
- root,
490
- dir: sandbox,
491
- env,
492
- arm: scenario.arm,
493
- })),
494
- exports: {},
495
- }
468
+ const staged = await stageScenarioInProcess(declared.definition, {
469
+ root,
470
+ dir: sandbox,
471
+ env,
472
+ arm: scenario.arm,
473
+ })
496
474
  if (staged.ending === 'replaced') throw new Halt(staged.status, false)
497
475
  if (staged.ending === 'exited') throw new Halt(staged.status)
498
- Object.assign(env, staged.exports)
499
476
 
500
477
  const isSkipAutoCommit = (env.SANDBOX_SKIP_AUTO_COMMIT ?? '') !== ''
501
478
  const isSkipInject = (env.SANDBOX_SKIP_SKILL_INJECT ?? '') !== ''
@@ -602,7 +579,7 @@ function parseTarget(target: string): { category: string; command: string } {
602
579
  }
603
580
 
604
581
  export interface SandboxRequest {
605
- /** False where a caller already opened the frame, as `run.sh` and the picker do. */
582
+ /** False where a caller already opened the frame, as `canon sandbox run` and the picker do. */
606
583
  readonly isHeader: boolean
607
584
  /** `<category>:<command>`, `reset`, or `clean`. */
608
585
  readonly target: string