@erclx/canon 5.32.0 → 5.33.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 (210) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/auto-ship/SKILL.md +1 -1
  3. package/claude/skills/auto-ship/references/worktree-name.md +1 -1
  4. package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
  5. package/claude/skills/backlog-triage/SKILL.md +1 -1
  6. package/claude/skills/canon-cli/REQUIREMENT.md +16 -13
  7. package/claude/skills/canon-cli/SKILL.md +75 -32
  8. package/claude/skills/canon-feedback/SKILL.md +1 -1
  9. package/claude/skills/{canvas → canvas-board}/REQUIREMENT.md +2 -2
  10. package/claude/skills/{canvas → canvas-board}/SKILL.md +2 -2
  11. package/{standards → claude/skills/canvas-board/references}/design.md +11 -2
  12. package/claude/skills/context-fold/REQUIREMENT.md +25 -33
  13. package/claude/skills/context-fold/SKILL.md +25 -70
  14. package/claude/skills/context-fold/references/classify.md +3 -3
  15. package/claude/skills/context-fold/references/completion-report.md +4 -4
  16. package/claude/skills/context-fold/references/context-refresh.md +5 -5
  17. package/{standards → claude/skills/context-fold/references}/context.md +16 -5
  18. package/claude/skills/context-fold/references/unusable-baseline.md +3 -5
  19. package/claude/skills/create-rule/SKILL.md +2 -2
  20. package/{standards → claude/skills/create-rule/references}/rule.md +12 -5
  21. package/claude/skills/create-skill/SKILL.md +2 -2
  22. package/{standards → claude/skills/create-skill/references}/skill.md +17 -14
  23. package/claude/skills/design-taste/REQUIREMENT.md +3 -3
  24. package/claude/skills/design-taste/SKILL.md +1 -1
  25. package/claude/skills/docs-sync/REQUIREMENT.md +1 -1
  26. package/claude/skills/docs-sync/SKILL.md +3 -3
  27. package/{standards → claude/skills/docs-sync/references}/docs.md +11 -0
  28. package/{standards → claude/skills/docs-sync/references}/readme.md +9 -2
  29. package/claude/skills/draft-and-pick/SKILL.md +5 -5
  30. package/claude/skills/draft-doc/SKILL.md +6 -6
  31. package/claude/skills/draft-figure/REQUIREMENT.md +2 -2
  32. package/claude/skills/draft-figure/SKILL.md +2 -2
  33. package/claude/skills/draft-figure/references/hand-drawn.md +1 -1
  34. package/claude/skills/draft-figure/references/mermaid-path.md +1 -1
  35. package/claude/skills/draft-identity/REQUIREMENT.md +2 -2
  36. package/claude/skills/draft-identity/SKILL.md +2 -2
  37. package/claude/skills/draft-ready/SKILL.md +1 -1
  38. package/claude/skills/git-branch/SKILL.md +1 -1
  39. package/{standards → claude/skills/git-branch/references}/branch.md +1 -1
  40. package/claude/skills/git-commit/SKILL.md +2 -2
  41. package/claude/skills/git-followup/REQUIREMENT.md +1 -1
  42. package/claude/skills/git-followup/SKILL.md +1 -1
  43. package/claude/skills/git-issue/SKILL.md +1 -1
  44. package/claude/skills/git-pr/SKILL.md +13 -13
  45. package/claude/skills/git-pr/references/evidence.md +2 -2
  46. package/claude/skills/git-pr/references/final-command.md +38 -14
  47. package/claude/skills/git-pr/references/labels.md +5 -1
  48. package/{standards → claude/skills/git-pr/references}/pr.md +19 -9
  49. package/{standards → claude/skills/git-pr/references}/publish.md +4 -2
  50. package/claude/skills/git-pr/references/task-number.md +1 -1
  51. package/claude/skills/git-ship/SKILL.md +15 -14
  52. package/claude/skills/git-split/SKILL.md +3 -3
  53. package/claude/skills/git-stage/SKILL.md +1 -1
  54. package/claude/skills/plan-feature/REQUIREMENT.md +1 -1
  55. package/claude/skills/review-address/SKILL.md +1 -1
  56. package/claude/skills/review-address/references/post-review-findings.md +1 -1
  57. package/claude/skills/review-pr/REQUIREMENT.md +1 -1
  58. package/claude/skills/review-pr/SKILL.md +3 -3
  59. package/claude/skills/review-pr/references/body-variants.md +2 -2
  60. package/claude/skills/review-pr/references/key-changes.md +3 -3
  61. package/claude/skills/review-pr/references/post.md +4 -2
  62. package/claude/skills/review-ui/REQUIREMENT.md +1 -1
  63. package/claude/skills/review-ui/SKILL.md +1 -1
  64. package/claude/skills/review-ui/references/post.md +1 -1
  65. package/claude/skills/role-orchestrator/REQUIREMENT.md +3 -0
  66. package/claude/skills/role-orchestrator/SKILL.md +2 -2
  67. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +2 -2
  68. package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +8 -5
  69. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +23 -1
  70. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +2 -2
  71. package/claude/skills/role-orchestrator/references/orchestrator-resume.md +3 -0
  72. package/claude/skills/role-planner/REQUIREMENT.md +2 -1
  73. package/claude/skills/role-planner/SKILL.md +13 -2
  74. package/claude/skills/role-reviewer/SKILL.md +1 -1
  75. package/claude/skills/role-worker/REQUIREMENT.md +1 -0
  76. package/claude/skills/role-worker/SKILL.md +9 -1
  77. package/claude/skills/session-map/SKILL.md +1 -1
  78. package/claude/skills/session-resume/REQUIREMENT.md +2 -2
  79. package/claude/skills/session-worktree/SKILL.md +5 -5
  80. package/claude/skills/target-setup/references/detect.md +1 -1
  81. package/claude/skills/task-board/REQUIREMENT.md +7 -12
  82. package/claude/skills/task-board/SKILL.md +5 -17
  83. package/claude/skills/task-board/references/tasks.md +2 -2
  84. package/claude/skills/task-complete/REQUIREMENT.md +47 -0
  85. package/claude/skills/task-complete/SKILL.md +86 -0
  86. package/claude/skills/{task-board → task-complete}/references/archive.md +4 -4
  87. package/claude/skills/{context-fold → task-complete}/references/receipt-sweep.md +5 -5
  88. package/claude/skills/task-complete/references/unusable-baseline.md +19 -0
  89. package/claude/skills/teach-workspace/REQUIREMENT.md +6 -2
  90. package/claude/skills/teach-workspace/SKILL.md +8 -4
  91. package/claude/skills/teach-workspace/references/lesson-craft.md +1 -1
  92. package/claude/skills/{context-fold/references/promoted-pages.md → teach-workspace/references/promotion-fold.md} +3 -3
  93. package/claude/skills/teach-workspace/references/promotion.md +3 -3
  94. package/claude/skills/ui-checklist/SKILL.md +1 -1
  95. package/claude/skills/ux-audit/SKILL.md +1 -1
  96. package/claude/skills/ux-measure/SKILL.md +1 -1
  97. package/docs/agents/canvas.md +1 -1
  98. package/docs/agents/commands.md +3 -2
  99. package/docs/agents/context-audit-checks.md +4 -4
  100. package/docs/agents/index.md +3 -3
  101. package/docs/agents/indexes.md +1 -1
  102. package/docs/agents/install-and-sync.md +6 -11
  103. package/docs/agents/key-changes.md +5 -5
  104. package/docs/agents/label-coverage.md +11 -0
  105. package/docs/agents/migrate.md +1 -1
  106. package/docs/agents/records-push.md +2 -2
  107. package/docs/agents/records-stale.md +12 -47
  108. package/docs/agents/records-validate.md +3 -7
  109. package/docs/agents/records.md +5 -27
  110. package/docs/agents/rule-citations.md +1 -1
  111. package/docs/agents/sandbox.md +4 -4
  112. package/docs/agents/skills-audit.md +1 -1
  113. package/docs/agents/tasks-plans.md +2 -2
  114. package/docs/agents/tasks.md +2 -2
  115. package/docs/target/migrations.md +10 -2
  116. package/docs/target/projects.md +1 -1
  117. package/docs/target/sync.md +1 -1
  118. package/docs/workflow/operating-model.md +2 -2
  119. package/docs/workflow/parallel-features.md +7 -7
  120. package/docs/workflow/skill-map-on-request.md +9 -9
  121. package/docs/workflow/skill-map.md +12 -11
  122. package/docs/workflow/visual-design-workflow.md +2 -2
  123. package/governance/renames.toml +0 -11
  124. package/governance/rules/canon/context.md +13 -0
  125. package/governance/rules/claude/skill.md +17 -0
  126. package/governance/stacks/base.toml +2 -4
  127. package/package.json +4 -4
  128. package/scripts/core/install-check.sh +3 -3
  129. package/scripts/lib/ui.sh +0 -159
  130. package/src/audits/catalog.ts +0 -1
  131. package/src/claude/cases/misc.ts +1 -1
  132. package/src/claude/cases/setup.ts +4 -0
  133. package/src/claude/cases/workflow.ts +6 -1
  134. package/src/claude/seeds.ts +1 -1
  135. package/src/claude/skills-audit.ts +2 -2
  136. package/src/cli/help.ts +3 -1
  137. package/src/cli/ui.ts +2 -3
  138. package/src/commands/claude.ts +1 -1
  139. package/src/commands/gov.ts +2 -15
  140. package/src/commands/labels.ts +326 -7
  141. package/src/commands/migrate.ts +6 -1
  142. package/src/commands/pr.ts +1 -1
  143. package/src/commands/records.ts +13 -399
  144. package/src/commands/sandbox.ts +3 -6
  145. package/src/commands/tasks.ts +3 -3
  146. package/src/context/audit.ts +5 -5
  147. package/src/context/folders.ts +1 -1
  148. package/src/design/budget.ts +1 -1
  149. package/src/design/components.ts +1 -1
  150. package/src/design/document.ts +1 -1
  151. package/src/design/regen.ts +1 -1
  152. package/src/design/render.ts +1 -1
  153. package/src/gate/measures.ts +1 -1
  154. package/src/gate/sequencer.ts +56 -1
  155. package/src/gate/stages.ts +1 -7
  156. package/src/gov/install.ts +2 -3
  157. package/src/gov/stacks.ts +1 -1
  158. package/src/hooks/post-merge.ts +1 -1
  159. package/src/labels/apply.ts +39 -0
  160. package/src/labels/format.ts +3 -3
  161. package/src/labels/phase.ts +3 -3
  162. package/src/labels/sections.ts +43 -0
  163. package/src/migrate/skill-names.ts +20 -1
  164. package/src/pr/paths.ts +4 -4
  165. package/src/records/backup.ts +16 -6
  166. package/src/records/stale-canonical.ts +31 -1
  167. package/src/records/validate.ts +2 -188
  168. package/src/sandbox/census.ts +1 -1
  169. package/src/sandbox/coverage.ts +2 -6
  170. package/src/sandbox/equivalence.ts +5 -44
  171. package/src/sandbox/expect.ts +4 -4
  172. package/src/sandbox/provision.ts +13 -36
  173. package/src/sandbox/scenario.ts +26 -48
  174. package/src/sandbox/tree.ts +2 -6
  175. package/src/standards/read.ts +14 -1
  176. package/src/sync/git.ts +1 -1
  177. package/src/tasks/archive.ts +5 -5
  178. package/src/tasks/branch.ts +3 -3
  179. package/src/tasks/label.ts +1 -1
  180. package/src/tasks/record.ts +2 -2
  181. package/src/tasks/validate.ts +1 -1
  182. package/standards/index.md +0 -13
  183. package/standards/standard.md +1 -7
  184. package/tooling/base/reference.md +1 -1
  185. package/tooling/base/seeds/.github/pull_request_template.md +2 -2
  186. package/tooling/claude/reference.md +1 -1
  187. package/tooling/claude/seeds/.claude/hooks/pr-create-log.sh +9 -1
  188. package/tooling/claude/seeds/.claude/hooks/review-post-scan.sh +171 -0
  189. package/tooling/claude/seeds/.claude/settings.json +9 -0
  190. package/governance/rules/standards/context.md +0 -14
  191. package/governance/rules/standards/design.md +0 -12
  192. package/governance/rules/standards/readme.md +0 -12
  193. package/governance/rules/standards/rule.md +0 -13
  194. package/governance/rules/standards/skill.md +0 -21
  195. package/governance/rules/standards/standard.md +0 -13
  196. package/scripts/config.sh +0 -11
  197. package/scripts/lib/gov.sh +0 -14
  198. package/scripts/lib/sandbox-fixtures.sh +0 -191
  199. package/scripts/lib/sandbox-git.sh +0 -125
  200. package/scripts/lib/sandbox-path.sh +0 -50
  201. package/scripts/sandbox-hook.sh +0 -99
  202. package/src/gov/standard-rules.ts +0 -179
  203. package/src/records/migrate.ts +0 -78
  204. package/src/records/stale.ts +0 -243
  205. package/src/sandbox/hooks.ts +0 -128
  206. /package/claude/skills/{canvas → canvas-board}/references/canvas.md +0 -0
  207. /package/{standards → claude/skills/draft-figure/references}/figures.md +0 -0
  208. /package/{standards → claude/skills/git-branch/references}/slug.md +0 -0
  209. /package/{standards → claude/skills/git-commit/references}/commit.md +0 -0
  210. /package/{standards → claude/skills/git-commit/references}/versioning.md +0 -0
@@ -105,7 +105,7 @@ interface Confidence {
105
105
  }
106
106
 
107
107
  /**
108
- * The columns the confidence ratio reads, fixed by `standards/design.md`. The
108
+ * The columns the confidence ratio reads, fixed by `claude/skills/canvas-board/references/design.md`. The
109
109
  * first column of each table names its row, and `Multiplier` and `When used`
110
110
  * restate what the row already carries, so none of them is something a source
111
111
  * could anchor and none belongs in the denominator.
@@ -317,7 +317,7 @@ export const designProse: Measure = async (ctx) => {
317
317
  if (failures.length > 0) {
318
318
  return {
319
319
  emissions: [],
320
- failure: `${failures.join(' ')} Cut to the budget in standards/design.md, moving the why to the context entry.`,
320
+ failure: `${failures.join(' ')} Cut to the budget in claude/skills/canvas-board/references/design.md, moving the why to the context entry.`,
321
321
  }
322
322
  }
323
323
  if (emissions.length === 0)
@@ -1,3 +1,5 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { availableParallelism } from 'node:os'
1
3
  import { join } from 'node:path'
2
4
  import { execa } from 'execa'
3
5
  import type {
@@ -44,6 +46,28 @@ export interface StageResult {
44
46
  readonly queuedMs?: number
45
47
  }
46
48
 
49
+ export interface LoadReading {
50
+ /** The 1-minute load average. */
51
+ readonly loadAverage: number
52
+ readonly cores: number
53
+ }
54
+
55
+ /**
56
+ * Reads the 1-minute load average from `/proc/loadavg`. A platform without
57
+ * it, as macOS, has no reading to give, so this answers `undefined` rather
58
+ * than throwing into a stage that has already failed.
59
+ */
60
+ export function readProcLoad(): LoadReading | undefined {
61
+ try {
62
+ const first = readFileSync('/proc/loadavg', 'utf8').split(' ')[0]
63
+ const loadAverage = Number.parseFloat(first)
64
+ if (!Number.isFinite(loadAverage)) return undefined
65
+ return { loadAverage, cores: availableParallelism() }
66
+ } catch {
67
+ return undefined
68
+ }
69
+ }
70
+
47
71
  export interface GateContext extends MeasureContext {
48
72
  /** Whether the format stage writes or checks. */
49
73
  readonly write: boolean
@@ -58,6 +82,11 @@ export interface GateContext extends MeasureContext {
58
82
  * wait and reads as a hung suite.
59
83
  */
60
84
  readonly onQueue?: (label: string, holder: LockHolder) => void
85
+ /**
86
+ * Reads the machine load a failed locked stage ran under. Defaults to
87
+ * `/proc/loadavg`, and answers `undefined` where there is none.
88
+ */
89
+ readonly readLoad?: () => LoadReading | undefined
61
90
  }
62
91
 
63
92
  export interface ChangedSet {
@@ -195,7 +224,11 @@ async function executeStage(
195
224
  ]
196
225
 
197
226
  try {
198
- const outcome = await runChecks(stage, ctx, queued)
227
+ const outcome = withLoadLine(
228
+ await runChecks(stage, ctx, queued),
229
+ stage,
230
+ ctx,
231
+ )
199
232
  return held.waitedOn === undefined
200
233
  ? outcome
201
234
  : { ...outcome, queuedMs: held.waitedMs }
@@ -204,6 +237,28 @@ async function executeStage(
204
237
  }
205
238
  }
206
239
 
240
+ /**
241
+ * The lock serializes suites and does nothing about the rest of the box, so a
242
+ * starved spawn test and a real failure read alike. The load beside the
243
+ * failure is what lets a report tell them apart.
244
+ */
245
+ function withLoadLine(
246
+ outcome: StageOutcome,
247
+ stage: Stage,
248
+ ctx: GateContext,
249
+ ): StageOutcome {
250
+ if (outcome.status !== 'failed') return outcome
251
+
252
+ const reading = (ctx.readLoad ?? readProcLoad)()
253
+ if (reading === undefined) return outcome
254
+
255
+ const text = `Load average ${reading.loadAverage} on ${reading.cores} cores when the ${stage.label} stage failed`
256
+ return {
257
+ ...outcome,
258
+ emissions: [...outcome.emissions, { kind: 'info', text }],
259
+ }
260
+ }
261
+
207
262
  async function runChecks(
208
263
  stage: Stage,
209
264
  ctx: GateContext,
@@ -198,17 +198,11 @@ export const STAGES: readonly Stage[] = [
198
198
  argv: ['gov', 'regen'],
199
199
  failure: 'Consumed-copy regen failed',
200
200
  },
201
- {
202
- kind: 'drift',
203
- pathspec: 'governance/rules/standards',
204
- failure:
205
- 'Standard-pointer rules drifted. Run bun run check and commit governance/rules/standards and .claude/rules.',
206
- },
207
201
  {
208
202
  kind: 'drift',
209
203
  pathspec: '.claude/rules',
210
204
  failure:
211
- 'Consumed copies drifted. Run bun run check and commit governance/rules/standards and .claude/rules.',
205
+ 'Consumed copies drifted. Run bun run check and commit .claude/rules.',
212
206
  },
213
207
  ],
214
208
  success: 'Consumed copies clean',
@@ -66,9 +66,8 @@ export function installedRuleNames(
66
66
  }
67
67
 
68
68
  /**
69
- * Mirrors `rule_subdir` in `scripts/lib/gov.sh`, which stays in bash for the
70
- * sandbox loops. A rule sitting directly under `governance/rules/` has no
71
- * subdirectory and installs flat.
69
+ * A rule sitting directly under `governance/rules/` has no subdirectory and
70
+ * installs flat.
72
71
  */
73
72
  export function ruleSubdir(src: string, rulesRoot: string): string {
74
73
  const subdir = dirname(relative(rulesRoot, src))
package/src/gov/stacks.ts CHANGED
@@ -79,7 +79,7 @@ export function loadGovStack(
79
79
  * shape rather than two the caller has to tell apart.
80
80
  *
81
81
  * The directory wins over a rule file of the same name. They cannot collide
82
- * while `standards/rule.md` requires a numeric prefix on a rule slug, since a
82
+ * while `claude/skills/create-rule/references/rule.md` requires a numeric prefix on a rule slug, since a
83
83
  * band folder carries none.
84
84
  */
85
85
  export function expandStackEntry(root: string, entry: string): string[] {
@@ -163,7 +163,7 @@ function archiveLines(
163
163
  }
164
164
  return (
165
165
  `\n📋 Task not archived for #${number}: ${text(fields, 'message') ?? reason}\n` +
166
- 'Run /task-board to resolve it by hand.\n\n'
166
+ `Run /task-complete archive #${number} to resolve it by hand.\n\n`
167
167
  )
168
168
  }
169
169
 
@@ -0,0 +1,39 @@
1
+ /** What a label apply decided to do, before any write. */
2
+ export interface LabelDiff {
3
+ /** Earned labels the pull request lacks and the remote carries, so addable. */
4
+ readonly toAdd: readonly string[]
5
+ /** Every earned label the pull request lacks, whether or not it is addable. */
6
+ readonly owed: readonly string[]
7
+ /** Owed labels the remote does not carry, which are reported and never created. */
8
+ readonly missingOnRemote: readonly string[]
9
+ }
10
+
11
+ /**
12
+ * Diffs what a branch earns against what its pull request already carries and
13
+ * what the remote can attach.
14
+ *
15
+ * The diff only adds. A label on the pull request that the map does not earn
16
+ * is a person's decision and never appears here, so nothing in the result can
17
+ * be turned into a removal. Order follows `earned`, which the map declares, so
18
+ * two runs over one branch produce one list.
19
+ *
20
+ * A label the remote lacks is separated from the addable set rather than
21
+ * dropped, because the REST add endpoint creates any label it is handed and
22
+ * that write to the repository settings is not the map's call.
23
+ */
24
+ export function diffLabels(
25
+ earned: readonly string[],
26
+ present: readonly string[],
27
+ remote: readonly string[],
28
+ ): LabelDiff {
29
+ const has = new Set(present)
30
+ const carried = new Set(remote)
31
+
32
+ const owed = earned.filter((label) => !has.has(label))
33
+
34
+ return {
35
+ owed,
36
+ toAdd: owed.filter((label) => carried.has(label)),
37
+ missingOnRemote: owed.filter((label) => !carried.has(label)),
38
+ }
39
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Which rule in `standards/pr.md`'s `## Title` section a title breaks.
2
+ * Which rule in `claude/skills/git-pr/references/pr.md`'s `## Title` section a title breaks.
3
3
  *
4
4
  * `structure` stands alone rather than beside the other three, because a
5
5
  * title failing the shape match has no parsed `<type>`, `<scope>`, or
@@ -28,9 +28,9 @@ const LEADING_LETTERS = /^[A-Za-z]+/
28
28
 
29
29
  /**
30
30
  * Grades a pull request title, or a commit subject sharing the same form,
31
- * against `standards/pr.md`'s structure, casing, and length rules.
31
+ * against `claude/skills/git-pr/references/pr.md`'s structure, casing, and length rules.
32
32
  *
33
- * Does not check `<type>` against `standards/commit.md`'s fixed enum.
33
+ * Does not check `<type>` against `claude/skills/git-commit/references/commit.md`'s fixed enum.
34
34
  * `pr.md`'s own `## Title` section states format, casing, and length only,
35
35
  * and names no type list of its own to check against.
36
36
  */
@@ -3,7 +3,7 @@ import { RECORD_ENTRIES, RECORD_ROOTS } from '@/roots/record'
3
3
  import type { RecordRoot } from '@/roots/record'
4
4
 
5
5
  /**
6
- * The two version namespaces `standards/versioning.md` keeps apart, and why a
6
+ * The two version namespaces `claude/skills/git-commit/references/versioning.md` keeps apart, and why a
7
7
  * pull request cannot be sorted between them by pattern alone.
8
8
  *
9
9
  * A phase label (`v68.5`) and a semver reference (`v3.44`) both match
@@ -186,7 +186,7 @@ const RELEASE_TITLE_PREFIX = 'chore(main): release '
186
186
  * phase label past this check. Together they name release-please's own act of
187
187
  * opening the pull request, which nothing else on the remote can perform.
188
188
  */
189
- function isReleasePullRequest(input: PhaseScanInput): boolean {
189
+ export function isReleasePullRequest(input: PhaseScanInput): boolean {
190
190
  return (
191
191
  input.headRefName.startsWith(RELEASE_HEAD_PREFIX) &&
192
192
  input.title.startsWith(RELEASE_TITLE_PREFIX)
@@ -265,7 +265,7 @@ function sessionLinks(text: string): string[] {
265
265
  * The split runs on the pull request rather than on each token. A release
266
266
  * pull request's tokens are read as the semver references its generated body
267
267
  * legitimately carries, and every other pull request's tokens are read as
268
- * leaked phase labels, which is what `standards/versioning.md` names the
268
+ * leaked phase labels, which is what `claude/skills/git-commit/references/versioning.md` names the
269
269
  * defect this exists to catch.
270
270
  *
271
271
  * A release pull request reports no board reference either, and the ground is
@@ -0,0 +1,43 @@
1
+ import { linesOutsideFences } from '@/markdown/scan'
2
+
3
+ /**
4
+ * The closed list of section headings a pull request body may carry, in the
5
+ * sentence case `claude/skills/git-pr/references/pr.md` states.
6
+ */
7
+ const ALLOWED_HEADINGS: readonly string[] = [
8
+ '## Summary',
9
+ '## Key changes',
10
+ '## Technical context',
11
+ '## Testing',
12
+ '## For the reviewer',
13
+ ]
14
+
15
+ const REQUIRED_HEADINGS: readonly string[] = ['## Summary', '## Key changes']
16
+
17
+ const H2 = /^##\s/
18
+
19
+ export interface SectionsCheck {
20
+ /** Level-two headings the closed list does not hold, as written. */
21
+ readonly outside: readonly string[]
22
+ /** Required headings the body never carries. */
23
+ readonly missing: readonly string[]
24
+ }
25
+
26
+ /**
27
+ * Grades the level-two headings of a pull request body against the closed list.
28
+ *
29
+ * The comparison is exact, so `## Key Changes` fails. `canon pr key-changes`
30
+ * reads its section case-insensitively, which keeps a body posted before the
31
+ * rename parsing while the scan holds new text to one spelling. Subheadings and
32
+ * fenced lines are not headings, so the list governs level two alone.
33
+ */
34
+ export function checkSections(body: string): SectionsCheck {
35
+ const headings = linesOutsideFences(body)
36
+ .map((line) => line.trim())
37
+ .filter((line) => H2.test(line))
38
+
39
+ return {
40
+ outside: headings.filter((heading) => !ALLOWED_HEADINGS.includes(heading)),
41
+ missing: REQUIRED_HEADINGS.filter((heading) => !headings.includes(heading)),
42
+ }
43
+ }
@@ -99,6 +99,25 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
99
99
  'draft-diagram': 'draft-figure',
100
100
  }
101
101
 
102
+ /**
103
+ * The skills that took a two-word name from a single word, which the map above
104
+ * may not carry as a bare key.
105
+ *
106
+ * `canvas` became `canvas-board`, and a bare `canvas` key would rewrite every
107
+ * unrelated use of the word: `canon canvas serve`, `src/canvas/`,
108
+ * `.canon/canvas/`, and the `internal-canvas` skill. So each row keys on a form
109
+ * only the skill spells, a plugin invocation or a skill folder path. The tail
110
+ * guard rejects a following `-`, so a rewritten `canon:canvas-board` is not
111
+ * matched again.
112
+ *
113
+ * What this leaves behind is a prose mention of the skill by its bare name,
114
+ * which has no token for a sweep to find and is fixed by hand.
115
+ */
116
+ export const SKILL_CITATION_MAP: Readonly<Record<string, string>> = {
117
+ 'canon:canvas': 'canon:canvas-board',
118
+ 'skills/canvas': 'skills/canvas-board',
119
+ }
120
+
102
121
  /**
103
122
  * The skill rename.
104
123
  *
@@ -132,7 +151,7 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
132
151
  * listing every sweep reads never reaches them.
133
152
  */
134
153
  export const SKILL_NAME_RULES: RenameRules = defineRenameRules({
135
- replacements: SKILL_NAME_MAP,
154
+ replacements: { ...SKILL_NAME_MAP, ...SKILL_CITATION_MAP },
136
155
  keepMarker: 'canon-keep-retired',
137
156
  wholeToken: true,
138
157
  excludedPrefixes: ['scripts/eval/result-'],
package/src/pr/paths.ts CHANGED
@@ -3,12 +3,12 @@ import { findBacktickSpans } from '@/markdown/backticks'
3
3
  /**
4
4
  * The heading whose bullets state what a branch changed.
5
5
  *
6
- * Read alone, and never widened to a sibling section. `## Technical Context`
6
+ * Read alone, and never widened to a sibling section. `## Technical context`
7
7
  * legitimately names files a branch never touched, such as an install stamp
8
8
  * inside a target, so a reader that took the whole body would manufacture a
9
9
  * finding out of every argument the author made for the change.
10
10
  */
11
- export const KEY_CHANGES = 'Key Changes'
11
+ export const KEY_CHANGES = 'Key changes'
12
12
 
13
13
  /** What one bullet claimed, kept with its bullet so a finding can quote it. */
14
14
  export interface PathClaim {
@@ -86,7 +86,7 @@ const BARE_LINE = /^:\d+(?:-\d+)?$/
86
86
  * `canon markdown audit .claude/rules --json` carries a slash and names no file.
87
87
  * Angle brackets answer a second, since `.canon/plans/feature-<slug>.md`
88
88
  * describes a shape rather than naming a file. A glob and a caret describe a
89
- * shape too, `^src/` being a grep pattern one body spelled in Key Changes, and
89
+ * shape too, `^src/` being a grep pattern one body spelled in Key changes, and
90
90
  * a leading anchor names something outside this repository.
91
91
  *
92
92
  * Deliberately not shared with `classifySpan` in `@/gov/citations`, which asks
@@ -307,7 +307,7 @@ function resolveSpan(span: string): ResolvedSpan | undefined {
307
307
  }
308
308
 
309
309
  /**
310
- * Every path the `## Key Changes` section claims a change to.
310
+ * Every path the `## Key changes` section claims a change to.
311
311
  *
312
312
  * `roots` names the entries the tree holds at its top level, which is what
313
313
  * separates a whole path from one written partially. It is passed in rather
@@ -30,7 +30,7 @@ import { type BlockedPath, guardPayload } from '@/records/push-guard'
30
30
  * rather than named here. That is what keeps this list at one line per surface
31
31
  * as archives spread, which a sibling-per-archive layout could not.
32
32
  *
33
- * `RECORD_KINDS` in `validate.ts` overlaps this on five names and carries one
33
+ * `RECORD_KINDS` in `validate.ts` overlaps this on four names and carries one
34
34
  * more that no backup reaches. The two lists differ on purpose: one is what a
35
35
  * standard governs, this is what a disk loss would take, and `standards` is
36
36
  * tracked so a backup would carry a second copy of committed files.
@@ -39,7 +39,6 @@ export const BACKED_FOLDERS = [
39
39
  'diagrams',
40
40
  'groundwork',
41
41
  'intake',
42
- 'memory',
43
42
  'plans',
44
43
  'proposals',
45
44
  'review',
@@ -51,6 +50,15 @@ export const BACKED_FOLDERS = [
51
50
  'walkthroughs',
52
51
  ] as const
53
52
 
53
+ /**
54
+ * Folders a retired kind left at a legacy `.claude` root that the allowlist
55
+ * must still read, so a half-finished move that strands one still trips
56
+ * `split-roots`. Absent from `BACKED_FOLDERS` because nothing writes them, and
57
+ * absent from `RETIRED_FOLDERS` because that list stages a deletion and this
58
+ * folder is an archive to keep.
59
+ */
60
+ const LEGACY_RECORD_FOLDERS = ['memory'] as const
61
+
54
62
  /**
55
63
  * Names that have left `BACKED_FOLDERS` and whose removal still has to reach a
56
64
  * records history once.
@@ -124,7 +132,9 @@ function isExcluded(name: string): boolean {
124
132
  */
125
133
  function foldersAt(dir: string): string[] {
126
134
  if (basename(dir) === '.claude') {
127
- return BACKED_FOLDERS.filter((folder) => existsSync(join(dir, folder)))
135
+ return [...BACKED_FOLDERS, ...LEGACY_RECORD_FOLDERS].filter((folder) =>
136
+ existsSync(join(dir, folder)),
137
+ )
128
138
  }
129
139
 
130
140
  const entries = existsSync(dir)
@@ -378,7 +388,7 @@ async function enclosingRemotes(
378
388
  * Clears the four gates both verbs share and returns the records remote URL.
379
389
  *
380
390
  * The last two are the ones the payload depends on. This repository is public,
381
- * so a records branch on any of its remotes serves the memory pen and the
391
+ * so a records branch on any of its remotes serves the plans and the
382
392
  * groundwork trails to anyone who fetches all refs. Comparing the configured
383
393
  * URL against every remote of the enclosing repository is what keeps a
384
394
  * misconfigured `origin` from publishing them, and refusing when that list
@@ -416,7 +426,7 @@ async function resolveRemote(
416
426
  if (!enclosing) {
417
427
  return refuse(
418
428
  'remote-unreadable',
419
- `Cannot read the remotes of the project at ${root}, so the records origin cannot be checked against them. Records carry the memory pen and the groundwork trails, and an unchecked origin risks publishing them.`,
429
+ `Cannot read the remotes of the project at ${root}, so the records origin cannot be checked against them. Records carry the plans and the groundwork trails, and an unchecked origin risks publishing them.`,
420
430
  )
421
431
  }
422
432
 
@@ -424,7 +434,7 @@ async function resolveRemote(
424
434
  if (enclosing.identities.includes(url)) {
425
435
  return refuse(
426
436
  'remote-shared',
427
- `The records origin ${remote.text} is a remote of this project. Records carry the memory pen and the groundwork trails, so they need a repository of their own.`,
437
+ `The records origin ${remote.text} is a remote of this project. Records carry the plans and the groundwork trails, so they need a repository of their own.`,
428
438
  )
429
439
  }
430
440
 
@@ -3,7 +3,6 @@ import { existsSync } from 'node:fs'
3
3
  import { readFile } from 'node:fs/promises'
4
4
  import { relative } from 'node:path'
5
5
  import { gitEnv } from '@/git/env'
6
- import { readReviewed } from '@/records/stale'
7
6
  import { surfaceDir } from '@/roots/surface'
8
7
 
9
8
  /** The record whose drift is identity rather than a stale path. */
@@ -22,6 +21,37 @@ export interface CanonicalDoc {
22
21
  readonly latestRelease: string | null
23
22
  }
24
23
 
24
+ const REVIEWED_FIELD = /^reviewed:[ \t]*(.*?)[ \t]*$/m
25
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/
26
+ const RAW_FRONTMATTER = /^---\n([\s\S]*?)\n---/
27
+
28
+ /**
29
+ * Reads `reviewed` off the raw block rather than the parsed fields, for the
30
+ * reason `hasOpeningDate` in the validator gives: a YAML parser resolves a
31
+ * bare `YYYY-MM-DD` to a date on one schema and a string on another.
32
+ *
33
+ * A value that is not a calendar date reads as never reviewed and is carried
34
+ * back so the review can see it, rather than throwing over one hand edit.
35
+ */
36
+ function readReviewed(
37
+ text: string,
38
+ ): Pick<CanonicalDoc, 'reviewed' | 'invalidReviewed'> {
39
+ const block = RAW_FRONTMATTER.exec(text)?.[1]
40
+ const match = block ? REVIEWED_FIELD.exec(block) : null
41
+ if (!match) return { reviewed: null }
42
+
43
+ const value = match[1].replace(/^(['"])(.*)\1$/, '$2')
44
+ const parsed = Date.parse(`${value}T00:00:00Z`)
45
+ const isDate =
46
+ ISO_DATE.test(value) &&
47
+ Number.isFinite(parsed) &&
48
+ new Date(parsed).toISOString().slice(0, 10) === value
49
+
50
+ return isDate
51
+ ? { reviewed: value }
52
+ : { reviewed: null, invalidReviewed: value }
53
+ }
54
+
25
55
  export interface CanonicalReport {
26
56
  readonly ok: true
27
57
  readonly root: string