@erclx/canon 5.30.0 → 5.32.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 (343) hide show
  1. package/README.md +1 -1
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/references/adopted.md +1 -1
  4. package/claude/skills/auto-ship/REQUIREMENT.md +4 -16
  5. package/claude/skills/auto-ship/SKILL.md +13 -58
  6. package/claude/skills/auto-ship/references/draft-mark.md +2 -2
  7. package/claude/skills/auto-ship/references/failure-recovery.md +1 -5
  8. package/claude/skills/auto-ship/references/tier-failures.md +1 -1
  9. package/claude/skills/auto-ship/references/ui-checklist.md +2 -2
  10. package/claude/skills/auto-ship/references/verb-absent.md +3 -27
  11. package/claude/skills/backlog-triage/SKILL.md +1 -1
  12. package/claude/skills/build-in-slices/REQUIREMENT.md +1 -1
  13. package/claude/skills/build-in-slices/references/adopted.md +1 -1
  14. package/claude/skills/canon-cli/REQUIREMENT.md +4 -6
  15. package/claude/skills/canon-cli/SKILL.md +2 -2
  16. package/claude/skills/canon-feedback/REQUIREMENT.md +1 -1
  17. package/claude/skills/canon-feedback/SKILL.md +10 -13
  18. package/claude/skills/canon-feedback-triage/REQUIREMENT.md +13 -14
  19. package/claude/skills/canon-feedback-triage/SKILL.md +50 -29
  20. package/claude/skills/canvas/REQUIREMENT.md +3 -4
  21. package/claude/skills/canvas/SKILL.md +5 -5
  22. package/{standards → claude/skills/canvas/references}/canvas.md +12 -1
  23. package/claude/skills/ci-workflow/REQUIREMENT.md +0 -1
  24. package/claude/skills/context-fold/REQUIREMENT.md +8 -20
  25. package/claude/skills/context-fold/SKILL.md +21 -45
  26. package/claude/skills/context-fold/references/classify.md +7 -8
  27. package/claude/skills/context-fold/references/completion-report.md +2 -6
  28. package/claude/skills/context-fold/references/context-refresh.md +5 -15
  29. package/claude/skills/context-fold/references/promoted-pages.md +3 -3
  30. package/claude/skills/context-fold/references/receipt-sweep.md +6 -17
  31. package/claude/skills/context-fold/references/unusable-baseline.md +4 -6
  32. package/claude/skills/create-rule/REQUIREMENT.md +0 -1
  33. package/claude/skills/create-skill/REQUIREMENT.md +6 -4
  34. package/claude/skills/create-skill/SKILL.md +9 -8
  35. package/{standards → claude/skills/create-skill/references}/skill-paths.md +1 -0
  36. package/claude/skills/decision-escalate/REQUIREMENT.md +1 -1
  37. package/claude/skills/decision-escalate/SKILL.md +1 -1
  38. package/claude/skills/design-taste/REQUIREMENT.md +0 -2
  39. package/claude/skills/design-taste/SKILL.md +2 -4
  40. package/claude/skills/design-taste/references/vocabulary.md +1 -1
  41. package/claude/skills/docs-sync/REQUIREMENT.md +3 -3
  42. package/claude/skills/docs-sync/SKILL.md +1 -1
  43. package/claude/skills/document-health/REQUIREMENT.md +2 -2
  44. package/claude/skills/document-health/SKILL.md +4 -5
  45. package/claude/skills/document-health/references/axes.md +4 -6
  46. package/claude/skills/draft-and-pick/REQUIREMENT.md +16 -14
  47. package/claude/skills/draft-and-pick/SKILL.md +40 -32
  48. package/claude/skills/draft-and-pick/references/live-arms.md +9 -9
  49. package/claude/skills/draft-doc/REQUIREMENT.md +5 -5
  50. package/claude/skills/draft-doc/SKILL.md +10 -11
  51. package/claude/skills/draft-doc/references/requirements.md +61 -0
  52. package/claude/skills/draft-doc/references/wiki-kind.md +53 -0
  53. package/claude/skills/draft-doc/references/wiki.md +50 -30
  54. package/claude/skills/draft-figure/REQUIREMENT.md +2 -2
  55. package/claude/skills/draft-figure/SKILL.md +5 -5
  56. package/claude/skills/draft-figure/references/architecture-set.md +4 -4
  57. package/claude/skills/draft-figure/references/hand-drawn.md +1 -1
  58. package/claude/skills/draft-figure/references/mermaid-path.md +55 -0
  59. package/claude/skills/draft-figure/references/mermaid.md +43 -34
  60. package/claude/skills/draft-identity/REQUIREMENT.md +1 -1
  61. package/claude/skills/draft-identity/SKILL.md +9 -7
  62. package/claude/skills/draft-ready/REQUIREMENT.md +1 -1
  63. package/claude/skills/draft-ready/SKILL.md +6 -6
  64. package/claude/skills/draft-ready/references/assembly.md +1 -1
  65. package/{standards → claude/skills/draft-ready/references}/ready.md +13 -5
  66. package/claude/skills/git-issue/SKILL.md +1 -1
  67. package/claude/skills/git-issue/references/issue.md +3 -3
  68. package/claude/skills/git-pr/SKILL.md +1 -1
  69. package/claude/skills/git-ship/REQUIREMENT.md +0 -2
  70. package/claude/skills/git-ship/SKILL.md +16 -24
  71. package/claude/skills/git-stage/SKILL.md +1 -1
  72. package/claude/skills/markdown-craft/REQUIREMENT.md +23 -0
  73. package/claude/skills/markdown-craft/SKILL.md +17 -0
  74. package/{standards → claude/skills/markdown-craft/references}/markdown.md +14 -7
  75. package/claude/skills/plan-feature/REQUIREMENT.md +2 -1
  76. package/claude/skills/plan-feature/SKILL.md +14 -8
  77. package/{standards → claude/skills/plan-feature/references}/plan.md +33 -21
  78. package/claude/skills/plan-groundwork/SKILL.md +5 -6
  79. package/{standards → claude/skills/plan-groundwork/references}/groundwork.md +17 -4
  80. package/claude/skills/plan-intake/SKILL.md +2 -3
  81. package/{standards → claude/skills/plan-intake/references}/intake.md +17 -2
  82. package/claude/skills/plan-intake-answer/REQUIREMENT.md +1 -1
  83. package/claude/skills/plan-intake-answer/SKILL.md +1 -1
  84. package/claude/skills/review-address/SKILL.md +2 -2
  85. package/claude/skills/review-address/references/post-review-findings.md +1 -1
  86. package/claude/skills/review-craft/REQUIREMENT.md +3 -3
  87. package/claude/skills/review-craft/SKILL.md +2 -2
  88. package/claude/skills/review-craft/references/ui.md +4 -4
  89. package/claude/skills/review-pr/REQUIREMENT.md +1 -1
  90. package/claude/skills/review-pr/SKILL.md +5 -7
  91. package/claude/skills/review-pr/references/post.md +2 -2
  92. package/claude/skills/review-ui/REQUIREMENT.md +0 -1
  93. package/claude/skills/review-ui/SKILL.md +1 -1
  94. package/claude/skills/review-ui/references/post.md +1 -1
  95. package/claude/skills/role-orchestrator/REQUIREMENT.md +0 -3
  96. package/claude/skills/role-orchestrator/SKILL.md +8 -8
  97. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +4 -3
  98. package/claude/skills/role-orchestrator/references/orchestrator-handoff.md +7 -10
  99. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -2
  100. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +0 -3
  101. package/claude/skills/role-orchestrator/references/orchestrator-resume.md +1 -1
  102. package/claude/skills/role-planner/SKILL.md +2 -2
  103. package/claude/skills/role-reviewer/SKILL.md +1 -1
  104. package/claude/skills/role-worker/SKILL.md +2 -2
  105. package/claude/skills/session-compact/REQUIREMENT.md +1 -4
  106. package/claude/skills/session-compact/SKILL.md +3 -13
  107. package/claude/skills/session-compact/references/handoff-note.md +2 -2
  108. package/claude/skills/session-map/REQUIREMENT.md +1 -9
  109. package/claude/skills/session-map/SKILL.md +8 -16
  110. package/{standards → claude/skills/session-map/references}/session.md +17 -11
  111. package/claude/skills/session-relay/REQUIREMENT.md +1 -1
  112. package/claude/skills/session-resume/REQUIREMENT.md +4 -6
  113. package/claude/skills/session-resume/SKILL.md +3 -9
  114. package/claude/skills/session-worktree/SKILL.md +1 -1
  115. package/claude/skills/session-worktree/references/main-root-writes.md +2 -2
  116. package/claude/skills/systematic-debugging/REQUIREMENT.md +0 -1
  117. package/claude/skills/target-check/REQUIREMENT.md +3 -3
  118. package/claude/skills/target-check/SKILL.md +1 -1
  119. package/claude/skills/target-check/references/domains.md +1 -1
  120. package/claude/skills/target-setup/REQUIREMENT.md +4 -4
  121. package/claude/skills/target-setup/SKILL.md +3 -3
  122. package/claude/skills/target-setup/references/detect.md +1 -1
  123. package/claude/skills/target-setup/references/indexes.md +1 -1
  124. package/claude/skills/task-board/SKILL.md +4 -4
  125. package/claude/skills/task-board/references/archive.md +1 -1
  126. package/{standards → claude/skills/task-board/references}/board.md +9 -0
  127. package/claude/skills/task-board/references/decline.md +1 -1
  128. package/{standards → claude/skills/task-board/references}/tasks.md +13 -8
  129. package/claude/skills/teach-workspace/SKILL.md +1 -1
  130. package/claude/skills/teach-workspace/references/revisit.md +1 -1
  131. package/{standards → claude/skills/teach-workspace/references}/teach.md +17 -5
  132. package/claude/skills/test-first/REQUIREMENT.md +1 -1
  133. package/claude/skills/ux-audit/REQUIREMENT.md +2 -2
  134. package/claude/skills/ux-audit/SKILL.md +2 -3
  135. package/claude/skills/write-human/REQUIREMENT.md +0 -1
  136. package/claude/skills/write-human/SKILL.md +2 -2
  137. package/docs/agents/audits.md +1 -1
  138. package/docs/agents/canvas.md +1 -1
  139. package/docs/agents/commands.md +6 -10
  140. package/docs/agents/context-audit-checks.md +10 -24
  141. package/docs/agents/context-audit.md +7 -15
  142. package/docs/agents/context-classify.md +5 -5
  143. package/docs/agents/counts.md +5 -5
  144. package/docs/agents/drift-surfaces.md +2 -2
  145. package/docs/agents/index.md +3 -4
  146. package/docs/agents/install-and-sync.md +6 -6
  147. package/docs/agents/intake.md +1 -1
  148. package/docs/agents/markdown-audit.md +1 -1
  149. package/docs/agents/records-stale.md +5 -5
  150. package/docs/agents/records-validate.md +1 -1
  151. package/docs/agents/records.md +2 -2
  152. package/docs/agents/restated.md +1 -1
  153. package/docs/agents/routing.md +1 -1
  154. package/docs/agents/rule-citations.md +1 -1
  155. package/docs/agents/skills-audit.md +2 -2
  156. package/docs/agents/state-scoped-risk.md +2 -2
  157. package/docs/agents/superseded.md +2 -2
  158. package/docs/agents/tasks-plans.md +2 -2
  159. package/docs/agents/tasks-validate.md +1 -1
  160. package/docs/agents/tasks.md +1 -1
  161. package/docs/agents/teach.md +1 -1
  162. package/docs/agents/test-order.md +1 -1
  163. package/docs/agents/upstream.md +94 -0
  164. package/docs/target/migrations.md +24 -0
  165. package/docs/target/projects.md +5 -7
  166. package/docs/target/scenarios.md +2 -2
  167. package/docs/target/sync.md +3 -3
  168. package/docs/workflow/ai-workflow.md +7 -38
  169. package/docs/workflow/index.md +1 -1
  170. package/docs/workflow/operating-model.md +8 -8
  171. package/docs/workflow/parallel-features.md +7 -7
  172. package/docs/workflow/skill-map-on-request.md +13 -16
  173. package/docs/workflow/skill-map.md +20 -32
  174. package/docs/workflow/visual-design-workflow.md +11 -14
  175. package/governance/renames.toml +0 -23
  176. package/governance/rules/canon/605-worktrees.md +1 -1
  177. package/governance/rules/{standards → canon}/plan.md +5 -5
  178. package/governance/rules/canon/tasks.md +18 -0
  179. package/governance/rules/writing/markdown.md +23 -0
  180. package/governance/stacks/base.toml +1 -1
  181. package/package.json +1 -1
  182. package/scripts/core/install-check.sh +2 -2
  183. package/scripts/core/regen-web-previews.ts +1 -1
  184. package/src/audits/catalog.ts +5 -97
  185. package/src/claude/cases/authoring.ts +5 -18
  186. package/src/claude/cases/misc.ts +0 -17
  187. package/src/claude/cases/setup.ts +3 -20
  188. package/src/claude/cases/workflow.ts +0 -33
  189. package/src/claude/seeds.ts +1 -7
  190. package/src/claude/skills-audit.ts +1 -1
  191. package/src/claude/skills-reach.ts +1 -1
  192. package/src/cli/help.ts +3 -4
  193. package/src/cli.ts +2 -4
  194. package/src/commands/claude.ts +9 -15
  195. package/src/commands/context.ts +18 -280
  196. package/src/commands/feedback.ts +22 -91
  197. package/src/commands/gov.ts +2 -2
  198. package/src/commands/records.ts +24 -6
  199. package/src/commands/sync.ts +5 -3
  200. package/src/commands/upstream.ts +430 -0
  201. package/src/context/audit.ts +6 -15
  202. package/src/context/classify/extract.ts +6 -13
  203. package/src/context/classify/patterns.ts +2 -41
  204. package/src/context/classify/prompts.ts +2 -6
  205. package/src/context/classify/run.ts +3 -3
  206. package/src/context/folders.ts +7 -12
  207. package/src/context/gate.ts +11 -91
  208. package/src/context/requirements.ts +13 -4
  209. package/src/counts/scan.ts +3 -3
  210. package/src/design/components.ts +2 -2
  211. package/src/feedback/body.ts +0 -12
  212. package/src/gate/measures.ts +6 -57
  213. package/src/gate/stages.ts +0 -1
  214. package/src/gov/citations.ts +9 -3
  215. package/src/gov/restated.ts +3 -3
  216. package/src/markdown/structure.ts +3 -3
  217. package/src/migrate/record-layout.ts +3 -3
  218. package/src/records/backup.ts +1 -0
  219. package/src/records/ordinal.ts +1 -1
  220. package/src/records/prune.ts +2 -1
  221. package/src/records/stale-canonical.ts +2 -2
  222. package/src/records/validate.ts +55 -0
  223. package/src/roots/record.ts +2 -1
  224. package/src/sandbox/scenario.ts +115 -11
  225. package/src/shipped/references.ts +3 -3
  226. package/src/standards/read.ts +52 -1
  227. package/src/sync/seeds-report.ts +1 -1
  228. package/src/tasks/answers.ts +14 -1
  229. package/src/tasks/reach.ts +2 -2
  230. package/src/tasks/stack.ts +176 -0
  231. package/src/tasks/validate.ts +1 -1
  232. package/src/teach/workspace.ts +1 -1
  233. package/src/upstream/catalog.ts +112 -0
  234. package/src/upstream/cursor.ts +120 -0
  235. package/src/upstream/digest.ts +75 -0
  236. package/src/upstream/due.ts +128 -0
  237. package/src/upstream/releases.ts +170 -0
  238. package/standards/context.md +3 -5
  239. package/standards/design.md +2 -3
  240. package/standards/index.md +0 -23
  241. package/standards/publish.md +1 -1
  242. package/standards/rule.md +1 -1
  243. package/standards/skill.md +1 -1
  244. package/tooling/base/reference.md +1 -1
  245. package/tooling/claude/reference.md +6 -20
  246. package/tooling/claude/seeds/.claude/hooks/index-reminder.sh +1 -1
  247. package/tooling/claude/seeds/.claude/hooks/records-index.sh +8 -20
  248. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +3 -3
  249. package/tooling/claude/seeds/CLAUDE.md +1 -3
  250. package/tooling/claude/seeds/canon/REQUIREMENTS.md +2 -9
  251. package/tooling/web/configs/e2e/screenshot.ts +0 -1
  252. package/claude/skills/auto-ship/references/review-findings.md +0 -20
  253. package/claude/skills/bash-cli-script/REQUIREMENT.md +0 -43
  254. package/claude/skills/bash-cli-script/SKILL.md +0 -53
  255. package/claude/skills/bash-cli-script/references/template.md +0 -19
  256. package/claude/skills/canon-operator/REQUIREMENT.md +0 -62
  257. package/claude/skills/canon-operator/SKILL.md +0 -113
  258. package/claude/skills/canon-rollout/REQUIREMENT.md +0 -59
  259. package/claude/skills/canon-rollout/SKILL.md +0 -140
  260. package/claude/skills/canon-rollout/references/failure-recovery.md +0 -20
  261. package/claude/skills/context-fold/references/anchor-sweep.md +0 -58
  262. package/claude/skills/context-fold/references/wireframe-sweep.md +0 -45
  263. package/claude/skills/create-standard/REQUIREMENT.md +0 -33
  264. package/claude/skills/create-standard/SKILL.md +0 -31
  265. package/claude/skills/design-extract/REQUIREMENT.md +0 -42
  266. package/claude/skills/design-extract/SKILL.md +0 -108
  267. package/claude/skills/draft-and-pick/references/frame.html +0 -118
  268. package/claude/skills/draft-doc/references/wireframes.md +0 -48
  269. package/claude/skills/index-lookup/REQUIREMENT.md +0 -35
  270. package/claude/skills/index-lookup/SKILL.md +0 -44
  271. package/claude/skills/markdown-propose/REQUIREMENT.md +0 -47
  272. package/claude/skills/markdown-propose/SKILL.md +0 -122
  273. package/claude/skills/markdown-propose/references/format.md +0 -118
  274. package/claude/skills/memory-capture/REQUIREMENT.md +0 -50
  275. package/claude/skills/memory-capture/SKILL.md +0 -117
  276. package/claude/skills/memory-review/REQUIREMENT.md +0 -52
  277. package/claude/skills/memory-review/SKILL.md +0 -129
  278. package/claude/skills/memory-review/references/later-phases.md +0 -118
  279. package/claude/skills/memory-review/references/receipt-format.md +0 -64
  280. package/claude/skills/project-commands/REQUIREMENT.md +0 -42
  281. package/claude/skills/project-commands/SKILL.md +0 -87
  282. package/claude/skills/repo-metadata/REQUIREMENT.md +0 -37
  283. package/claude/skills/repo-metadata/SKILL.md +0 -54
  284. package/claude/skills/restate-plainly/REQUIREMENT.md +0 -41
  285. package/claude/skills/restate-plainly/SKILL.md +0 -41
  286. package/claude/skills/review-branch/REQUIREMENT.md +0 -40
  287. package/claude/skills/review-branch/SKILL.md +0 -137
  288. package/claude/skills/seed-sync/REQUIREMENT.md +0 -45
  289. package/claude/skills/seed-sync/SKILL.md +0 -158
  290. package/claude/skills/sketch-design/REQUIREMENT.md +0 -38
  291. package/claude/skills/sketch-design/SKILL.md +0 -94
  292. package/claude/skills/ux-walkthrough/REQUIREMENT.md +0 -56
  293. package/claude/skills/ux-walkthrough/SKILL.md +0 -53
  294. package/claude/skills/ux-walkthrough/references/builds.md +0 -15
  295. package/claude/skills/ux-walkthrough/references/candidate-pages.md +0 -11
  296. package/claude/skills/ux-walkthrough/references/measuring.md +0 -22
  297. package/claude/skills/ux-walkthrough/references/record.md +0 -36
  298. package/claude/skills/ux-walkthrough/references/relay.md +0 -25
  299. package/docs/agents/architecture-record.md +0 -36
  300. package/docs/agents/review-classification.md +0 -76
  301. package/governance/rules/canon/601-indexes.md +0 -9
  302. package/governance/rules/canon/603-memory.md +0 -11
  303. package/governance/rules/claude/565-behavior.md +0 -27
  304. package/governance/rules/claude/567-planning.md +0 -16
  305. package/governance/rules/claude/575-hooks.md +0 -16
  306. package/governance/rules/claude/576-settings.md +0 -13
  307. package/governance/rules/claude/592-claude-md.md +0 -19
  308. package/governance/rules/lang/120-bash.md +0 -18
  309. package/governance/rules/standards/architecture.md +0 -13
  310. package/governance/rules/standards/groundwork.md +0 -13
  311. package/governance/rules/standards/intake.md +0 -12
  312. package/governance/rules/standards/markdown.md +0 -16
  313. package/governance/rules/standards/memory.md +0 -12
  314. package/governance/rules/standards/ready.md +0 -13
  315. package/governance/rules/standards/requirements.md +0 -12
  316. package/governance/rules/standards/session.md +0 -13
  317. package/governance/rules/standards/tasks.md +0 -15
  318. package/governance/rules/standards/teach.md +0 -14
  319. package/governance/rules/standards/wiki.md +0 -13
  320. package/governance/rules/standards/wireframes.md +0 -13
  321. package/governance/rules/writing/800-prose.md +0 -15
  322. package/src/autoship/classify.ts +0 -75
  323. package/src/autoship/paths.ts +0 -50
  324. package/src/commands/autoship.ts +0 -129
  325. package/src/commands/repo.ts +0 -393
  326. package/src/context/architecture.ts +0 -654
  327. package/src/context/wireframe-states.ts +0 -238
  328. package/src/repo/metadata.ts +0 -206
  329. package/standards/architecture.md +0 -127
  330. package/standards/memory.md +0 -158
  331. package/standards/mermaid.md +0 -64
  332. package/standards/requirements.md +0 -79
  333. package/standards/wiki.md +0 -77
  334. package/standards/wireframes.md +0 -143
  335. package/tooling/claude/seeds/.claude/memory/index.md +0 -8
  336. package/tooling/claude/seeds/canon/ARCHITECTURE.md +0 -23
  337. package/tooling/claude/seeds/canon/wireframes/index.md +0 -8
  338. /package/{standards → claude/skills/create-skill/references}/skill-practice.md +0 -0
  339. /package/{standards → claude/skills/create-skill/references}/skill-requirement.md +0 -0
  340. /package/{standards → claude/skills/plan-feature/references}/plan-lifecycle.md +0 -0
  341. /package/{standards → claude/skills/plan-groundwork/references}/groundwork-spikes.md +0 -0
  342. /package/{standards → claude/skills/task-board/references}/tasks-decline.md +0 -0
  343. /package/{standards → claude/skills/teach-workspace/references}/glossary.md +0 -0
@@ -0,0 +1,176 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { readdir, readFile } from 'node:fs/promises'
3
+ import { basename, join } from 'node:path'
4
+ import { recordDir } from '@/roots/record'
5
+ import type { Finding } from '@/records/validate'
6
+ import { splitPlanSections } from '@/records/validate'
7
+ import { readDeclarations } from '@/tasks/reach'
8
+
9
+ /**
10
+ * The fold thresholds, calibrated over 1,043 live and archived plans in which
11
+ * 87 stack pairs name a parent. At these values 15 report: the inspector,
12
+ * canvas, slides, and evidence chains, plus one sandbox contract split that
13
+ * carries a `Judged apart from` line. The ceiling drops one pair of 24 files.
14
+ * The per-PR cost was never measured as a number, so these are calibrated
15
+ * against the corpus and not derived.
16
+ */
17
+ export const MIN_SHARED = 3
18
+ export const MIN_OVERLAP = 0.6
19
+ export const MAX_UNION = 20
20
+
21
+ const STACK_PARENT =
22
+ /(?:stacks? on|builds? on|branch(?:es)? from)\s+(?:(?:the|batch|plan|slice)\s+)*`(feature-[a-z0-9-]+)`/gi
23
+
24
+ const JUDGED_APART = /^judged apart from\s+`(feature-[a-z0-9-]+)`:\s*\S/i
25
+
26
+ const BULLET = /^[-*]\s+/
27
+
28
+ export interface StackPlan {
29
+ readonly stem: string
30
+ readonly text: string
31
+ }
32
+
33
+ export interface StackPair {
34
+ readonly child: string
35
+ readonly parent: string
36
+ readonly shared: number
37
+ readonly union: number
38
+ }
39
+
40
+ /**
41
+ * One string per `**Constraints:**` bullet, with a wrapped continuation line
42
+ * joined to the bullet it belongs to.
43
+ */
44
+ function constraintBullets(text: string): string[] {
45
+ const bullets: string[] = []
46
+
47
+ for (const line of splitPlanSections(text).get('Constraints') ?? []) {
48
+ const trimmed = line.trim()
49
+ if (trimmed.length === 0) continue
50
+
51
+ if (BULLET.test(trimmed)) bullets.push(trimmed.replace(BULLET, ''))
52
+ else if (bullets.length > 0) bullets[bullets.length - 1] += ` ${trimmed}`
53
+ }
54
+
55
+ return bullets
56
+ }
57
+
58
+ /**
59
+ * The plan stems a plan says it stacks on, in the order written.
60
+ *
61
+ * Only the slug standing directly after the phrase counts, with at most a
62
+ * filler word between. A line like "`feature-x` stacks on this slice, not on
63
+ * `feature-y`" names a parent for `feature-y`'s plan and none for this one, and
64
+ * reading every slug on a stack-worded line would hand it the wrong parent.
65
+ */
66
+ export function readStackParents(text: string): string[] {
67
+ const parents: string[] = []
68
+
69
+ for (const bullet of constraintBullets(text)) {
70
+ for (const match of bullet.matchAll(STACK_PARENT)) {
71
+ const parent = match[1]
72
+ if (parent !== undefined) parents.push(parent.toLowerCase())
73
+ }
74
+ }
75
+
76
+ return [...new Set(parents)]
77
+ }
78
+
79
+ function isJudgedApart(text: string, parent: string): boolean {
80
+ return constraintBullets(text).some(
81
+ (bullet) => JUDGED_APART.exec(bullet)?.[1]?.toLowerCase() === parent,
82
+ )
83
+ }
84
+
85
+ /** Whether two declarations name the same file or one folder holding the other. */
86
+ function overlaps(a: string, b: string): boolean {
87
+ return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`)
88
+ }
89
+
90
+ function countShared(from: readonly string[], to: readonly string[]): number {
91
+ return from.filter((entry) => to.some((other) => overlaps(entry, other)))
92
+ .length
93
+ }
94
+
95
+ /**
96
+ * Every live plan that stacks on a live sibling it mostly shares files with and
97
+ * has not written down why the two are judged apart.
98
+ *
99
+ * A parent absent from `plans` is never reported, which is how an archived
100
+ * parent stays clear: the caller hands over the live folder only, so a merged
101
+ * parent cannot be folded into. When two plans each name the other, the pair is
102
+ * reported once, on the child sorting second.
103
+ *
104
+ * A folder declaration counts as sharing the files under it, taking the larger
105
+ * of the two directions and capping at the smaller set, so a folder and the
106
+ * files beneath it are not undercounted as distinct strings.
107
+ */
108
+ export function stackPairs(plans: readonly StackPlan[]): StackPair[] {
109
+ const byStem = new Map(plans.map((plan) => [plan.stem, plan]))
110
+ const pairs: StackPair[] = []
111
+
112
+ for (const child of plans) {
113
+ for (const parentStem of readStackParents(child.text)) {
114
+ const parent = byStem.get(parentStem)
115
+ if (parent === undefined || parent.stem === child.stem) continue
116
+
117
+ const mutual = readStackParents(parent.text).includes(child.stem)
118
+ if (mutual && child.stem < parent.stem) continue
119
+
120
+ if (isJudgedApart(child.text, parentStem)) continue
121
+
122
+ const own = readDeclarations(child.text)
123
+ const theirs = readDeclarations(parent.text)
124
+ const smaller = Math.min(own.length, theirs.length)
125
+ if (smaller === 0) continue
126
+
127
+ const shared = Math.min(
128
+ smaller,
129
+ Math.max(countShared(own, theirs), countShared(theirs, own)),
130
+ )
131
+ const union = new Set([...own, ...theirs]).size
132
+
133
+ if (
134
+ shared >= MIN_SHARED &&
135
+ shared / smaller >= MIN_OVERLAP &&
136
+ union <= MAX_UNION
137
+ ) {
138
+ pairs.push({ child: child.stem, parent: parentStem, shared, union })
139
+ }
140
+ }
141
+ }
142
+
143
+ return pairs
144
+ }
145
+
146
+ /** The `Judged apart from` remedy, worded once for the finding and the gate. */
147
+ export const JUDGED_APART_REMEDY =
148
+ 'Fold the batch into its parent as contiguous commits on one branch, or write `Judged apart from `feature-<parent>`: <reason>` in its constraints.'
149
+
150
+ export function stackFindings(plans: readonly StackPlan[]): Finding[] {
151
+ return stackPairs(plans).map(({ child, parent, shared, union }) => ({
152
+ kind: 'stack-foldable',
153
+ record: `${child}.md`,
154
+ subject: parent,
155
+ message: `stacks on \`${parent}\` and shares ${shared} declared files with it, ${union} between the two. ${JUDGED_APART_REMEDY}`,
156
+ }))
157
+ }
158
+
159
+ /**
160
+ * The live plans folder as stem and text pairs. The archive is a folder below
161
+ * this one and is never walked, so a merged parent is absent and stays clear.
162
+ */
163
+ export async function readLivePlans(root: string): Promise<StackPlan[]> {
164
+ const dir = recordDir(root, 'plans')
165
+ if (!existsSync(dir)) return []
166
+
167
+ const names = (await readdir(dir)).filter((name) => name.endsWith('.md'))
168
+ names.sort()
169
+
170
+ return Promise.all(
171
+ names.map(async (name) => ({
172
+ stem: basename(name, '.md'),
173
+ text: await readFile(join(dir, name), 'utf8'),
174
+ })),
175
+ )
176
+ }
@@ -18,7 +18,7 @@ const ORDERING_FILE = 'priority.md'
18
18
  const BACKLOG_FILE = 'backlog.md'
19
19
 
20
20
  /**
21
- * The readiness headings `standards/board.md` fixes. The names are the
21
+ * The readiness headings `claude/skills/task-board/references/board.md` fixes. The names are the
22
22
  * contract rather than a suggestion, so a board grouping under names of its own
23
23
  * reads as carrying no group at all and the run refuses instead of reporting a
24
24
  * clean board it never parsed.
@@ -30,7 +30,7 @@ export interface TeachRefused {
30
30
  readonly detail: readonly string[]
31
31
  }
32
32
 
33
- /** The files and folders `standards/teach.md` fixes for a workspace. */
33
+ /** The files and folders `claude/skills/teach-workspace/references/teach.md` fixes for a workspace. */
34
34
  export const TEACH_MISSION = 'MISSION.md'
35
35
  export const TEACH_RESOURCES = 'RESOURCES.md'
36
36
  export const TEACH_GLOSSARY = 'GLOSSARY.md'
@@ -0,0 +1,112 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { COMMAND_GROUPS } from '@/cli/help'
4
+ import { listSkillsAt } from '@/claude/skills-list'
5
+ import { STAGES } from '@/gate/stages'
6
+
7
+ const MAX_TEXT = 140
8
+ const PLUGIN_SKILLS = 'claude/skills'
9
+ const INTERNAL_SKILLS = '.claude/skills'
10
+
11
+ export interface CatalogEntry {
12
+ readonly name: string
13
+ readonly text: string
14
+ }
15
+
16
+ export interface Catalog {
17
+ readonly skills: CatalogEntry[]
18
+ readonly internalSkills: CatalogEntry[]
19
+ readonly verbs: CatalogEntry[]
20
+ readonly hooks: CatalogEntry[]
21
+ readonly gate: CatalogEntry[]
22
+ /** Rows that exist but could not be described, so none drops out silently. */
23
+ readonly gaps: string[]
24
+ }
25
+
26
+ const firstSentence = (text: string): string =>
27
+ text.split(/\.\s/)[0]?.replace(/\.$/, '').slice(0, MAX_TEXT) ?? ''
28
+
29
+ function skillsAt(
30
+ root: string,
31
+ corpus: string,
32
+ gaps: string[],
33
+ ): CatalogEntry[] {
34
+ const dir = join(root, corpus)
35
+ if (!existsSync(dir)) return []
36
+
37
+ const listed = listSkillsAt(dir)
38
+ const named = new Set(listed.map((skill) => skill.name))
39
+ const folders = readdirSync(dir, { withFileTypes: true })
40
+ .filter((entry) => entry.isDirectory())
41
+ .map((entry) => entry.name)
42
+
43
+ for (const folder of folders) {
44
+ if (!named.has(folder)) gaps.push(`${corpus}/${folder}: no SKILL.md`)
45
+ }
46
+ for (const skill of listed) {
47
+ if (!skill.description) gaps.push(`${corpus}/${skill.name}: no description`)
48
+ }
49
+
50
+ return listed.map((skill) => ({
51
+ name: skill.name,
52
+ text: firstSentence(skill.description),
53
+ }))
54
+ }
55
+
56
+ function hooksAt(root: string): CatalogEntry[] {
57
+ const dir = join(root, '.claude', 'hooks')
58
+ if (!existsSync(dir)) return []
59
+
60
+ return readdirSync(dir)
61
+ .filter((file) => file.endsWith('.sh'))
62
+ .sort()
63
+ .map((file) => {
64
+ const comment = readFileSync(join(dir, file), 'utf8')
65
+ .split('\n')
66
+ .find((line) => /^#\s*\S/.test(line) && !line.startsWith('#!'))
67
+
68
+ return {
69
+ name: file,
70
+ text: (comment ?? '').replace(/^#\s*/, '').slice(0, MAX_TEXT),
71
+ }
72
+ })
73
+ }
74
+
75
+ export function buildCatalog(root: string): Catalog {
76
+ const gaps: string[] = []
77
+
78
+ return {
79
+ skills: skillsAt(root, PLUGIN_SKILLS, gaps),
80
+ internalSkills: skillsAt(root, INTERNAL_SKILLS, gaps),
81
+ verbs: COMMAND_GROUPS.flatMap((group) => group.rows).map(
82
+ ([signature, description]) => ({
83
+ name: `canon ${signature}`,
84
+ text: description,
85
+ }),
86
+ ),
87
+ hooks: hooksAt(root),
88
+ gate: STAGES.map((stage) => ({ name: stage.id, text: stage.label })),
89
+ gaps,
90
+ }
91
+ }
92
+
93
+ const section = (title: string, entries: readonly CatalogEntry[]): string[] => [
94
+ `### ${title}`,
95
+ ...entries.map((entry) =>
96
+ entry.text ? `- ${entry.name}: ${entry.text}` : `- ${entry.name}`,
97
+ ),
98
+ '',
99
+ ]
100
+
101
+ export function renderCatalog(catalog: Catalog): string {
102
+ return [
103
+ ...section('Plugin skills (claude/skills/)', catalog.skills),
104
+ ...section('Internal skills (.claude/skills/)', catalog.internalSkills),
105
+ ...section('CLI verbs', catalog.verbs),
106
+ ...section('Hooks (.claude/hooks/)', catalog.hooks),
107
+ ...section('Gate stages (canon gate run)', catalog.gate),
108
+ ...(catalog.gaps.length > 0
109
+ ? ['### Gaps', ...catalog.gaps.map((gap) => `- ${gap}`), '']
110
+ : []),
111
+ ].join('\n')
112
+ }
@@ -0,0 +1,120 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ renameSync,
6
+ writeFileSync,
7
+ } from 'node:fs'
8
+ import { dirname } from 'node:path'
9
+ import { recordDir } from '@/roots/record'
10
+
11
+ const FOLDER = 'upstream'
12
+ const VERSION = /^\d+(\.\d+)*$/
13
+
14
+ export interface Cursor {
15
+ readonly version: string
16
+ readonly date: string
17
+ readonly intake: string
18
+ }
19
+
20
+ export interface AdvanceInput {
21
+ readonly version: string
22
+ readonly intake: string
23
+ readonly today: string
24
+ /** The current `llms.txt`, or null to keep the copy already stored. */
25
+ readonly llms: string | null
26
+ }
27
+
28
+ export type AdvanceOutcome =
29
+ | { readonly kind: 'advanced' }
30
+ | {
31
+ readonly kind: 'refused'
32
+ readonly reason: 'older-than-cursor' | 'invalid-version'
33
+ }
34
+
35
+ export interface LlmsDifference {
36
+ readonly added: string[]
37
+ readonly removed: string[]
38
+ }
39
+
40
+ const cursorPath = (root: string): string =>
41
+ recordDir(root, FOLDER, 'cursor.json')
42
+
43
+ const llmsPath = (root: string): string => recordDir(root, FOLDER, 'llms.txt')
44
+
45
+ export function readCursor(root: string): Cursor | null {
46
+ const path = cursorPath(root)
47
+ if (!existsSync(path)) return null
48
+
49
+ try {
50
+ return JSON.parse(readFileSync(path, 'utf8')) as Cursor
51
+ } catch {
52
+ return null
53
+ }
54
+ }
55
+
56
+ export function readLastLlms(root: string): string | null {
57
+ const path = llmsPath(root)
58
+
59
+ return existsSync(path) ? readFileSync(path, 'utf8') : null
60
+ }
61
+
62
+ export function compareVersions(a: string, b: string): number {
63
+ const left = a.split('.').map(Number)
64
+ const right = b.split('.').map(Number)
65
+
66
+ for (let i = 0; i < Math.max(left.length, right.length); i++) {
67
+ const diff = (left[i] ?? 0) - (right[i] ?? 0)
68
+ if (diff !== 0) return diff
69
+ }
70
+
71
+ return 0
72
+ }
73
+
74
+ // `fetch` reads this file, so a write that half-lands would leave a reader on a
75
+ // truncated cursor. A rename within one folder is atomic.
76
+ export async function writeAtomic(path: string, body: string): Promise<void> {
77
+ mkdirSync(dirname(path), { recursive: true })
78
+ const temp = `${path}.${process.pid}.tmp`
79
+ writeFileSync(temp, body)
80
+ renameSync(temp, path)
81
+ }
82
+
83
+ export async function advanceCursor(
84
+ root: string,
85
+ input: AdvanceInput,
86
+ ): Promise<AdvanceOutcome> {
87
+ if (!VERSION.test(input.version)) {
88
+ return { kind: 'refused', reason: 'invalid-version' }
89
+ }
90
+
91
+ const stored = readCursor(root)
92
+ if (stored && compareVersions(input.version, stored.version) < 0) {
93
+ return { kind: 'refused', reason: 'older-than-cursor' }
94
+ }
95
+
96
+ if (input.llms !== null) await writeAtomic(llmsPath(root), input.llms)
97
+ const next: Cursor = {
98
+ version: input.version,
99
+ date: input.today,
100
+ intake: input.intake,
101
+ }
102
+ await writeAtomic(cursorPath(root), `${JSON.stringify(next, null, 2)}\n`)
103
+
104
+ return { kind: 'advanced' }
105
+ }
106
+
107
+ const lines = (text: string): string[] => text.split('\n').filter(Boolean)
108
+
109
+ export function llmsDifference(
110
+ last: string | null,
111
+ current: string,
112
+ ): LlmsDifference {
113
+ const before = new Set(lines(last ?? ''))
114
+ const after = new Set(lines(current))
115
+
116
+ return {
117
+ added: [...after].filter((line) => !before.has(line)),
118
+ removed: [...before].filter((line) => !after.has(line)),
119
+ }
120
+ }
@@ -0,0 +1,75 @@
1
+ import {
2
+ extractIdentifiers,
3
+ keepLines,
4
+ versionOf,
5
+ walkReleases,
6
+ type FetchFailure,
7
+ type PageFetcher,
8
+ type ReleaseLine,
9
+ } from '@/upstream/releases'
10
+
11
+ const FILE_HINTS = 3
12
+
13
+ export interface NamedIdentifier {
14
+ readonly name: string
15
+ readonly files: string[]
16
+ }
17
+
18
+ export interface DigestLine extends ReleaseLine {
19
+ readonly identifiers: NamedIdentifier[]
20
+ }
21
+
22
+ export type Digest =
23
+ | {
24
+ readonly kind: 'digest'
25
+ readonly from: string
26
+ readonly to: string
27
+ readonly releases: number
28
+ readonly lines: DigestLine[]
29
+ }
30
+ | { readonly kind: 'cursor-missing'; readonly cursor: string }
31
+ | {
32
+ readonly kind: 'failed'
33
+ readonly reason: FetchFailure
34
+ readonly message: string
35
+ }
36
+
37
+ /** Files naming an identifier, supplied so a test needs no repository. */
38
+ export type Grep = (identifier: string) => Promise<string[]>
39
+
40
+ export async function buildDigest(
41
+ cursor: string,
42
+ fetchPage: PageFetcher,
43
+ grep: Grep,
44
+ ): Promise<Digest> {
45
+ const walk = await walkReleases(cursor, fetchPage)
46
+ if (walk.kind !== 'reached') return walk
47
+
48
+ const oldestFirst = [...walk.releases].reverse()
49
+ const lines = await Promise.all(
50
+ oldestFirst.flatMap(keepLines).map(async (line) => ({
51
+ ...line,
52
+ identifiers: await hintsFor(line.text, grep),
53
+ })),
54
+ )
55
+ const newest = walk.releases[0]
56
+
57
+ return {
58
+ kind: 'digest',
59
+ from: cursor,
60
+ to: newest ? versionOf(newest.tag_name) : cursor,
61
+ releases: walk.releases.length,
62
+ lines,
63
+ }
64
+ }
65
+
66
+ async function hintsFor(text: string, grep: Grep): Promise<NamedIdentifier[]> {
67
+ const found = await Promise.all(
68
+ extractIdentifiers(text).map(async (name) => ({
69
+ name,
70
+ files: (await grep(name)).slice(0, FILE_HINTS),
71
+ })),
72
+ )
73
+
74
+ return found.filter((entry) => entry.files.length > 0)
75
+ }
@@ -0,0 +1,128 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { recordDir, SCRATCH } from '@/roots/record'
3
+ import { compareVersions, writeAtomic, type Cursor } from '@/upstream/cursor'
4
+
5
+ const DAY_MS = 86_400_000
6
+ const DUE_AFTER_DAYS = 7
7
+
8
+ // Ported from the groundwork's pre-filter script. It names canon's own surfaces,
9
+ // so an Added line matching it is one a digest would act on.
10
+ const VOCABULARY =
11
+ /plugin|skill|hook|SendMessage|ListAgents|agent view|agents view|worktree|subagent|background|routine|schedule|Monitor|mod\b|mods\b|CLAUDE\.md|rules|memory|permission|auto mode|stream-json|--bg|headless|-p\b|marketplace|session/i
12
+ const ADDED = /^Added/
13
+ const CACHE_MAX_AGE_MS = DAY_MS
14
+
15
+ /** What the gap between the cursor and the installed version holds. */
16
+ export interface Gap {
17
+ readonly releases: number
18
+ readonly lines: readonly string[]
19
+ }
20
+
21
+ export interface DueInput {
22
+ /** The installed Claude Code version, or null when it could not be read. */
23
+ readonly installed: string | null
24
+ readonly cursor: Cursor | null
25
+ /** Null when the feed was not read, so only the age path can decide. */
26
+ readonly gap: Gap | null
27
+ readonly now: Date
28
+ }
29
+
30
+ export type DueReason =
31
+ | 'no-cursor'
32
+ | 'week'
33
+ | 'vocabulary'
34
+ | 'current'
35
+ | 'recent'
36
+ | 'unknown-version'
37
+
38
+ export interface DueResult {
39
+ readonly due: boolean
40
+ readonly reason: DueReason
41
+ /** Releases in the gap, or null when it was not read. */
42
+ readonly releases: number | null
43
+ }
44
+
45
+ export function passesVocabulary(line: string): boolean {
46
+ return ADDED.test(line) && VOCABULARY.test(line)
47
+ }
48
+
49
+ // A cursor date is a calendar day with no time, read at UTC midnight. An
50
+ // unreadable one gives NaN, which fails the age test and leaves the vocabulary
51
+ // path to decide.
52
+ function ageInDays(date: string, now: Date): number {
53
+ return (now.getTime() - Date.parse(`${date}T00:00:00Z`)) / DAY_MS
54
+ }
55
+
56
+ /**
57
+ * Decides whether a digest is due.
58
+ *
59
+ * The age boundary is inclusive: a cursor exactly seven days old is due, since
60
+ * a week has passed. An installed version at or below the cursor is never due,
61
+ * which covers a downgrade as well as a run straight after a digest.
62
+ */
63
+ export function decideDue(input: DueInput): DueResult {
64
+ const { installed, cursor, gap, now } = input
65
+ const releases = gap?.releases ?? null
66
+
67
+ if (cursor === null) return { due: true, reason: 'no-cursor', releases }
68
+ if (installed === null) {
69
+ return { due: false, reason: 'unknown-version', releases }
70
+ }
71
+ if (compareVersions(installed, cursor.version) <= 0) {
72
+ return { due: false, reason: 'current', releases }
73
+ }
74
+ if (ageInDays(cursor.date, now) >= DUE_AFTER_DAYS) {
75
+ return { due: true, reason: 'week', releases }
76
+ }
77
+ if (gap?.lines.some(passesVocabulary)) {
78
+ return { due: true, reason: 'vocabulary', releases }
79
+ }
80
+
81
+ return { due: false, reason: 'recent', releases }
82
+ }
83
+
84
+ /**
85
+ * One reading of the feed, kept so a wave of sessions starting together spends
86
+ * one request rather than one each.
87
+ */
88
+ export interface DueCache {
89
+ readonly checkedAt: string
90
+ readonly installed: string
91
+ readonly cursor: string
92
+ /** Null when the read failed, so a failure is held for the day too. */
93
+ readonly gap: Gap | null
94
+ }
95
+
96
+ // Scratch rather than the upstream record folder, which a records push backs
97
+ // whole. A file rewritten daily would add a commit to the history each time.
98
+ const cachePath = (root: string): string =>
99
+ recordDir(root, SCRATCH, 'upstream-due', 'check.json')
100
+
101
+ /** The stored reading when it is fresh and answers for this pair of versions. */
102
+ export function readDueCache(
103
+ root: string,
104
+ installed: string,
105
+ cursor: string,
106
+ now: Date,
107
+ ): DueCache | null {
108
+ const path = cachePath(root)
109
+ if (!existsSync(path)) return null
110
+
111
+ try {
112
+ const cache = JSON.parse(readFileSync(path, 'utf8')) as DueCache
113
+ const age = now.getTime() - Date.parse(cache.checkedAt)
114
+ const isFresh = age >= 0 && age < CACHE_MAX_AGE_MS
115
+ const isSamePair = cache.installed === installed && cache.cursor === cursor
116
+
117
+ return isFresh && isSamePair ? cache : null
118
+ } catch {
119
+ return null
120
+ }
121
+ }
122
+
123
+ export async function writeDueCache(
124
+ root: string,
125
+ cache: DueCache,
126
+ ): Promise<void> {
127
+ await writeAtomic(cachePath(root), `${JSON.stringify(cache)}\n`)
128
+ }