@erclx/canon 4.121.0 → 4.122.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 (96) 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/backlog-triage/REQUIREMENT.md +47 -0
  4. package/claude/skills/backlog-triage/SKILL.md +139 -0
  5. package/claude/skills/canon-rollout/SKILL.md +6 -6
  6. package/claude/skills/context-fold/REQUIREMENT.md +75 -0
  7. package/claude/skills/context-fold/SKILL.md +162 -0
  8. package/claude/skills/{docs-fold → context-fold}/references/anchor-sweep.md +1 -1
  9. package/claude/skills/{docs-fold → context-fold}/references/classify.md +4 -2
  10. package/claude/skills/context-fold/references/context-refresh.md +48 -0
  11. package/claude/skills/context-fold/references/promoted-pages.md +29 -0
  12. package/claude/skills/context-fold/references/receipt-sweep.md +43 -0
  13. package/claude/skills/context-fold/references/unusable-baseline.md +25 -0
  14. package/claude/skills/{docs-fold → context-fold}/references/wireframe-sweep.md +1 -1
  15. package/claude/skills/docs-fold/REQUIREMENT.md +8 -58
  16. package/claude/skills/docs-fold/SKILL.md +3 -278
  17. package/claude/skills/docs-sync/REQUIREMENT.md +1 -1
  18. package/claude/skills/docs-sync/SKILL.md +1 -1
  19. package/claude/skills/draft-context/REQUIREMENT.md +2 -2
  20. package/claude/skills/draft-context/SKILL.md +3 -3
  21. package/claude/skills/draft-docs/REQUIREMENT.md +1 -1
  22. package/claude/skills/draft-wireframes/REQUIREMENT.md +1 -1
  23. package/claude/skills/draft-wireframes/SKILL.md +2 -2
  24. package/claude/skills/git-pr/SKILL.md +8 -146
  25. package/claude/skills/git-pr/references/evidence.md +114 -0
  26. package/claude/skills/git-pr/references/labels.md +20 -0
  27. package/claude/skills/git-pr/references/task-number.md +26 -0
  28. package/claude/skills/git-ship/SKILL.md +1 -1
  29. package/claude/skills/memory-capture/REQUIREMENT.md +2 -2
  30. package/claude/skills/memory-capture/SKILL.md +5 -5
  31. package/claude/skills/memory-review/REQUIREMENT.md +1 -1
  32. package/claude/skills/memory-review/SKILL.md +5 -101
  33. package/claude/skills/memory-review/references/later-phases.md +108 -0
  34. package/claude/skills/plan-feature/REQUIREMENT.md +1 -1
  35. package/claude/skills/review-address/REQUIREMENT.md +1 -1
  36. package/claude/skills/review-address/SKILL.md +1 -1
  37. package/claude/skills/review-branch/SKILL.md +1 -1
  38. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +1 -1
  39. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -0
  40. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +2 -2
  41. package/claude/skills/session-resume/REQUIREMENT.md +1 -1
  42. package/claude/skills/session-worktree/SKILL.md +4 -10
  43. package/claude/skills/task-board/REQUIREMENT.md +1 -1
  44. package/claude/skills/task-board/SKILL.md +6 -80
  45. package/claude/skills/task-board/references/archive.md +54 -0
  46. package/claude/skills/task-board/references/decline.md +40 -0
  47. package/claude/skills/teach-workspace/SKILL.md +8 -105
  48. package/claude/skills/teach-workspace/references/lesson-craft.md +51 -1
  49. package/claude/skills/teach-workspace/references/promotion.md +41 -0
  50. package/docs/agents/architecture-record.md +26 -0
  51. package/docs/agents/commands.md +87 -154
  52. package/docs/agents/context-audit-checks.md +2 -20
  53. package/docs/agents/context-audit.md +1 -1
  54. package/docs/agents/context-classify.md +1 -1
  55. package/docs/agents/drift-surfaces.md +192 -0
  56. package/docs/agents/index.md +12 -4
  57. package/docs/agents/install-and-sync.md +2 -181
  58. package/docs/agents/markdown-audit-weight.md +73 -0
  59. package/docs/agents/markdown-audit.md +3 -90
  60. package/docs/agents/migrate.md +34 -0
  61. package/docs/agents/records-stale.md +62 -0
  62. package/docs/agents/records.md +5 -1
  63. package/docs/agents/sandbox.md +1 -1
  64. package/docs/agents/tasks-archive.md +81 -0
  65. package/docs/agents/tasks-plans.md +147 -0
  66. package/docs/agents/tasks-validate.md +131 -0
  67. package/docs/agents/tasks.md +6 -333
  68. package/docs/target-projects.md +3 -3
  69. package/docs/workflow/ai-workflow.md +32 -27
  70. package/governance/rules/core/045-memory.md +1 -1
  71. package/package.json +1 -1
  72. package/src/claude/cases/setup.ts +1 -1
  73. package/src/claude/cases/workflow.ts +22 -1
  74. package/src/commands/records.ts +146 -0
  75. package/src/commands/sandbox.ts +6 -4
  76. package/src/context/classify/extract.ts +1 -1
  77. package/src/context/classify/ollama.ts +1 -1
  78. package/src/context/classify/run.ts +1 -1
  79. package/src/gate/measures.ts +1 -1
  80. package/src/migrate/skill-names.ts +15 -2
  81. package/src/records/stale.ts +243 -0
  82. package/src/sandbox/expect.ts +1 -1
  83. package/src/tasks/archive.ts +28 -4
  84. package/src/tasks/record.ts +1 -1
  85. package/src/tasks/validate.ts +36 -3
  86. package/standards/memory.md +13 -1
  87. package/standards/tasks.md +1 -1
  88. package/tooling/astro/reference.md +1 -1
  89. package/tooling/base/configs/.husky/post-merge +1 -1
  90. package/tooling/claude/reference.md +1 -1
  91. package/tooling/go/reference.md +1 -1
  92. package/tooling/nextjs/reference.md +1 -1
  93. package/tooling/php/reference.md +1 -1
  94. package/tooling/python/reference.md +1 -1
  95. package/tooling/vite-react/reference.md +1 -1
  96. package/tooling/web/reference.md +1 -1
@@ -55,11 +55,12 @@ One session works for most features. Prefer splitting across two sessions only w
55
55
  Work in Claude Code directly. It reads `CLAUDE.md` automatically and has full file access, no pasting needed.
56
56
 
57
57
  - When the input is a pile of findings rather than one feature, invoke `canon:plan-intake` first. It files the dump into `.canon/intake/<nn>-<slug>/`, one item per finding carrying a problem measured against the tree, a proposed fix, and a verdict, then names which items are plan-ready, which need measuring, and which are already settled.
58
+ - When the backlog has grown past what anyone rereads, invoke `canon:backlog-triage`. It files one item per backlog row into an intake folder, suggesting decline, archive, keep, or promote and leaning toward decline, and once `canon:plan-intake-answer` has taken your answers, a second run applies the approved verdicts through the task verbs.
58
59
  - When the current state is unmeasured and more than one approach is live, invoke `canon:plan-groundwork` first. It opens a track folder under `.canon/groundwork/<nn>-<slug>/` and ends in a decision, which may be to do nothing. Skip it when the approach is already settled.
59
60
  - Invoke `canon:plan-feature` to scan for code-level conflicts and ambiguities, confirm approach before proceeding
60
61
  - Implement the feature, then Claude Code runs the commands defined in `CLAUDE.md`, fixes failures, and iterates until all pass
61
62
  - For UI changes, invoke `canon:ui-checklist` to write what a reviewer has to look at and name any behavior shipping without a test
62
- End the session once the feature works and tests pass. Invoke `canon:docs-fold` to capture any decisions made during implementation before closing.
63
+ End the session once the feature works and tests pass. Invoke `canon:context-fold` to capture any decisions made during implementation before closing.
63
64
 
64
65
  The routing test is whether the repository can answer an item today. A session grepping handles the yes, and a groundwork track handles the no.
65
66
 
@@ -115,11 +116,13 @@ A person points it at a private repository once and both verbs refuse until they
115
116
 
116
117
  `canon records prune-tmp` reports scratch under `.canon/tmp` nobody has touched inside an age window, and deletes it only with `--write`. A `tmp/handoff/` folder and the live `tmp/pr/poll/` baseline are never offered, since a reader deletes a handoff once it is read and a poll baseline is state a session still needs. See [records](../agents/records.md) for the candidate shapes and the exit codes.
117
118
 
119
+ `canon records stale memory` reads the memory pen as a review queue. It names each entry due for review, meaning one carrying no `reviewed` date or one older than 30 days, and every backticked path an entry cites that the tree no longer holds. Due entries come first, with the ones citing a missing path ahead of the rest, so a review takes the first batch rather than the whole pen. It writes nothing. See [records stale](../agents/records-stale.md) for how a path is read and the refusals.
120
+
118
121
  A plan that ships is archived, never deleted. `canon tasks archive` moves it to `.canon/plans/archive/` alongside the task it belonged to and retargets that task's `Plan:` line at the new location, so a completed task still leads to the reasoning behind it. An archive sits inside the record folder it archives rather than beside it, so one ignore entry and one backed-folder entry cover a record and everything it has retired. The folder is gitignored, which is why a deleted plan had no recovery path. A plan cited by more than one task stays put until the last of them closes, since moving it early would strand every other pointer.
119
122
 
120
- A branch review report takes the other route and is swept rather than archived. `review-branch` writes it flat into `.canon/review/` as `branch-<slug>.md`, the session addressing it reads it once, and the durable record of what a review found is the comment `review-pr` posts on the pull request, so `docs-fold` deletes any report whose branch is gone. The body that writes a report owns how long it lives, which leaves the shipping branch's own report on disk through the run that cites it and collects it a branch later. What that loses is a local-only review on a branch that never opened a pull request, which is why the report says so where a reader meets it.
123
+ A branch review report takes the other route and is swept rather than archived. `review-branch` writes it flat into `.canon/review/` as `branch-<slug>.md`, the session addressing it reads it once, and the durable record of what a review found is the comment `review-pr` posts on the pull request, so `context-fold` deletes any report whose branch is gone. The body that writes a report owns how long it lives, which leaves the shipping branch's own report on disk through the run that cites it and collects it a branch later. What that loses is a local-only review on a branch that never opened a pull request, which is why the report says so where a reader meets it.
121
124
 
122
- `canon:docs-fold` decides which task closed by reading the diff rather than the conversation. It resolves a merge base against `origin/main`, unions the committed diff with the working tree and untracked files, then matches unchecked outcomes on the board against what shipped. A task that shipped without ever being discussed still gets marked. Requirements, architecture, and design stay session-sourced, because a diff cannot carry a judgment.
125
+ `canon:context-fold` decides which task closed by reading the diff rather than the conversation. It resolves a merge base against `origin/main`, unions the committed diff with the working tree and untracked files, then matches unchecked outcomes on the board against what shipped. A task that shipped without ever being discussed still gets marked. Requirements, architecture, and design stay session-sourced, because a diff cannot carry a judgment.
123
126
 
124
127
  `.canon/tasks/` is gitignored and resolves at the main worktree root, so every session shares one board. One file per task is what keeps concurrent sessions from overwriting each other, since a gitignored board has no history to recover a lost write from. Its `index.md` is generated by a hook rather than by `bun run check`, because the whole-repo index walk skips gitignored folders.
125
128
 
@@ -164,7 +167,7 @@ The list stays written in the skill body as the fallback for a target whose inst
164
167
 
165
168
  #### Memory in the chain
166
169
 
167
- `git-ship` runs its verify gate and then opens on `memory-capture`, which sends what the session learned to the surface that owns it. `autoship` reaches the same step by invoking that skill at its Step 8 rather than restating the order. A fact about a domain carrying an entry in `canon/context/index.md` is routed to that entry, and `docs-fold` folds it in on the next step, so it ships in the same pull request. Anything no entry owns stays a file in `.canon/memory/`.
170
+ `git-ship` runs its verify gate and then opens on `memory-capture`, which sends what the session learned to the surface that owns it. `autoship` reaches the same step by invoking that skill at its Step 8 rather than restating the order. A fact about a domain carrying an entry in `canon/context/index.md` is routed to that entry, and `context-fold` folds it in on the next step, so it ships in the same pull request. Anything no entry owns stays a file in `.canon/memory/`.
168
171
 
169
172
  Capture leads rather than trails because a routed fact edits a tracked file, which has to reach the branch before the commit steps run.
170
173
 
@@ -172,7 +175,7 @@ The chain ends at capture, and review is run on its own.
172
175
 
173
176
  Run `memory-review` standalone to curate the whole pen. An entry it retires moves to `.canon/memory/archive/` rather than being deleted, since a bulk pass has no undo.
174
177
 
175
- The receipt is collected once every item on it has been decided, and it survives untouched while any item is still pending. Whichever runs first takes it: Apply collects the receipt it has resolved, and `docs-fold` scans the folder on every shipped branch for one an earlier session left behind. Before the file goes, each declined item is folded into the entry it was about, since a promotion survives in its target and in git while a decline is recorded nowhere else. `canon standards memory` states what a fold writes and which entry types take one.
178
+ The receipt is collected once every item on it has been decided, and it survives untouched while any item is still pending. Whichever runs first takes it: Apply collects the receipt it has resolved, and `context-fold` scans the folder on every shipped branch for one an earlier session left behind. Before the file goes, each declined item is folded into the entry it was about, since a promotion survives in its target and in git while a decline is recorded nowhere else. `canon standards memory` states what a fold writes and which entry types take one.
176
179
 
177
180
  ### UI polish
178
181
 
@@ -211,16 +214,17 @@ This section is the corpus the coverage claim is measured against: every name `c
211
214
 
212
215
  ### Decide what to build
213
216
 
214
- | Skill | When to use |
215
- | -------------------------- | ------------------------------------------------------------------------------- |
216
- | `canon:plan-intake` | When the input is a pile of findings rather than one feature |
217
- | `canon:plan-intake-answer` | When an intake folder holds unread slots waiting on your decision |
218
- | `canon:plan-groundwork` | When the state is unmeasured and more than one approach is live |
219
- | `canon:decision-escalate` | When open decisions turn on your preference and want batching into one set |
220
- | `canon:draft-and-pick` | When the call is taste and wants several candidates rendered side by side |
221
- | `canon:task-board` | When a decided item needs a file on the board, or a shipped one needs archiving |
222
- | `canon:plan-feature` | When the approach is settled and the next step is a plan |
223
- | `canon:codebase-layout` | When a plan or change adds a file and you need to decide which folder holds it |
217
+ | Skill | When to use |
218
+ | -------------------------- | --------------------------------------------------------------------------------- |
219
+ | `canon:plan-intake` | When the input is a pile of findings rather than one feature |
220
+ | `canon:plan-intake-answer` | When an intake folder holds unread slots waiting on your decision |
221
+ | `canon:backlog-triage` | When the backlog needs pruning, to file a verdict per row and apply approved ones |
222
+ | `canon:plan-groundwork` | When the state is unmeasured and more than one approach is live |
223
+ | `canon:decision-escalate` | When open decisions turn on your preference and want batching into one set |
224
+ | `canon:draft-and-pick` | When the call is taste and wants several candidates rendered side by side |
225
+ | `canon:task-board` | When a decided item needs a file on the board, or a shipped one needs archiving |
226
+ | `canon:plan-feature` | When the approach is settled and the next step is a plan |
227
+ | `canon:codebase-layout` | When a plan or change adds a file and you need to decide which folder holds it |
224
228
 
225
229
  ### Build the feature
226
230
 
@@ -249,17 +253,18 @@ This section is the corpus the coverage claim is measured against: every name `c
249
253
 
250
254
  ### Ship it
251
255
 
252
- | Skill | When to use |
253
- | ---------------------- | ------------------------------------------------------------------------------------- |
254
- | `canon:git-ship` | To run the whole post-feature chain from the verify gate through open PR |
255
- | `canon:memory-capture` | First skill in that chain, to route what the session learned to the surface owning it |
256
- | `canon:docs-fold` | When decisions diverged from the plan, or a shipped task needs its outcomes marked |
257
- | `canon:docs-sync` | When a change since main left `README.md` or `docs/` stale |
258
- | `canon:git-stage` | When the staged set spans several concerns and wants one commit each |
259
- | `canon:git-commit` | When the staged set is one concern, or was staged hunk by hand |
260
- | `canon:git-branch` | When a branch name needs generating or renaming to conventional form |
261
- | `canon:git-pr` | When a pull request needs a title and body written from the diff |
262
- | `canon:memory-review` | When the pen has grown, to propose where each entry belongs |
256
+ | Skill | When to use |
257
+ | ---------------------- | ------------------------------------------------------------------------------------------------- |
258
+ | `canon:git-ship` | To run the whole post-feature chain from the verify gate through open PR |
259
+ | `canon:memory-capture` | First skill in that chain, to route what the session learned to the surface owning it |
260
+ | `canon:context-fold` | When decisions diverged from the plan, or a shipped task needs its outcomes marked |
261
+ | `canon:docs-fold` | Never on purpose. A pointer at the old name that names `context-fold` <!-- canon-keep-retired --> |
262
+ | `canon:docs-sync` | When a change since main left `README.md` or `docs/` stale |
263
+ | `canon:git-stage` | When the staged set spans several concerns and wants one commit each |
264
+ | `canon:git-commit` | When the staged set is one concern, or was staged hunk by hand |
265
+ | `canon:git-branch` | When a branch name needs generating or renaming to conventional form |
266
+ | `canon:git-pr` | When a pull request needs a title and body written from the diff |
267
+ | `canon:memory-review` | When the pen has grown, to propose where each entry belongs |
263
268
 
264
269
  ### After the pull request opens
265
270
 
@@ -331,7 +336,7 @@ This section is the corpus the coverage claim is measured against: every name `c
331
336
 
332
337
  Every row answers a question rather than marking a point in a project's life, so a phase above would send a reader to the wrong group.
333
338
 
334
- A learning workspace produces two halves and only one of them leaves. A lesson is worked through once and stays in the workspace, and a reference page or a glossary carries no learner, so it belongs wherever the project already keeps prose on that subject. Asking `canon:teach-workspace` to promote sorts each durable page by who owns its subject, sending an Anthropic-owned subject to the wiki, an internal one to the matching context entry, and everything else, subject-neutral material included, to the public docs. It proposes and waits, because a promoted page is public prose that needs a line naming who owns the subject, and it writes nothing to a destination: each page the operator confirms goes to a handoff file that `canon:docs-fold` folds in from a branch. A project with no wiki folder gets a refusal naming `canon wiki init` rather than a folder it never asked for.
339
+ A learning workspace produces two halves and only one of them leaves. A lesson is worked through once and stays in the workspace, and a reference page or a glossary carries no learner, so it belongs wherever the project already keeps prose on that subject. Asking `canon:teach-workspace` to promote sorts each durable page by who owns its subject, sending an Anthropic-owned subject to the wiki, an internal one to the matching context entry, and everything else, subject-neutral material included, to the public docs. It proposes and waits, because a promoted page is public prose that needs a line naming who owns the subject, and it writes nothing to a destination: each page the operator confirms goes to a handoff file that `canon:context-fold` folds in from a branch. A project with no wiki folder gets a refusal naming `canon wiki init` rather than a folder it never asked for.
335
340
 
336
341
  ## Feedback routing
337
342
 
@@ -7,5 +7,5 @@ description: Keep memory writes scoped to .canon/memory/ and out of context-owne
7
7
  ## Writing memory
8
8
 
9
9
  - Write all memory files to `.canon/memory/`, not `~/.claude/projects/`
10
- - A fact about a domain goes to that domain's `canon/context/` entry, not to memory. `canon:memory-capture` routes it there and `canon:docs-fold` folds it in. Memory keeps only what no context entry owns. Report it rather than proceeding silently when either skill does not resolve. Both ship with the plugin and this rule ships with the CLI, so a project that installed governance alone does not have them.
10
+ - A fact about a domain goes to that domain's `canon/context/` entry, not to memory. `canon:memory-capture` routes it there and `canon:context-fold` folds it in. Memory keeps only what no context entry owns. Report it rather than proceeding silently when either skill does not resolve. Both ship with the plugin and this rule ships with the CLI, so a project that installed governance alone does not have them.
11
11
  - Never delete a memory entry. Retire one by moving it to `.canon/memory/archive/`, which `canon records push` backs. A bulk retire runs through the shell, where no file edit fires a path-scoped rule.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "4.121.0",
4
+ "version": "4.122.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -52,7 +52,7 @@ export const SETUP_CASES: readonly SkillCase[] = [
52
52
  },
53
53
  // The performing skill wins a phrase naming its operation over the front
54
54
  // door's own routing framing, even though canon-operator's description
55
- // quotes a phrase near this one. See canon/context/cli/audits.md.
55
+ // quotes a phrase near this one. See canon/context/cli/audits/routing.md.
56
56
  {
57
57
  prompt:
58
58
  "Whatever the right toolkit command is, get this project's rules installed.",
@@ -35,7 +35,13 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
35
35
  },
36
36
  {
37
37
  prompt:
38
- 'Bring the internal planning docs under .claude up to date with what we decided this session.',
38
+ 'Fold what we decided this session into the context entries and mark off what shipped on the task board.',
39
+ expect: 'context-fold',
40
+ },
41
+ {
42
+ prompt:
43
+ 'I typed the old fold skill name and it says it was renamed, where did it go?',
44
+ // The pointer answers under its old name on purpose. canon-keep-retired
39
45
  expect: 'docs-fold',
40
46
  },
41
47
  {
@@ -104,6 +110,16 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
104
110
  'What should a review of a change look for beyond bugs, and how much evidence does a finding need?',
105
111
  expect: 'review-craft',
106
112
  },
113
+ {
114
+ prompt:
115
+ 'Go through every task sitting in the backlog and tell me which ones are still worth doing.',
116
+ expect: 'backlog-triage',
117
+ },
118
+ {
119
+ prompt:
120
+ 'Our backlog only ever grows, measure each row and suggest which to decline.',
121
+ expect: 'backlog-triage',
122
+ },
107
123
  {
108
124
  prompt: 'Draft me a script with beats for a screencast recording.',
109
125
  expect: 'draft-screencast',
@@ -194,6 +210,11 @@ export const WORKFLOW_CASES: readonly SkillCase[] = [
194
210
  prompt: 'Sort through this brain dump and write it up as findings.',
195
211
  expect: 'plan-intake',
196
212
  },
213
+ {
214
+ prompt:
215
+ 'Here are my scattered notes from this week, file each one against the codebase with a verdict.',
216
+ expect: 'plan-intake',
217
+ },
197
218
  {
198
219
  prompt:
199
220
  "Go through what's still unanswered in the intake folder and decide.",
@@ -24,6 +24,12 @@ import {
24
24
  type SizeOutcome,
25
25
  sizeRecords,
26
26
  } from '@/records/size'
27
+ import {
28
+ DEFAULT_REVIEW_DAYS,
29
+ type StaleEntry,
30
+ type StaleReport,
31
+ staleMemory,
32
+ } from '@/records/stale'
27
33
  import {
28
34
  type Finding,
29
35
  type FindingRemedy,
@@ -75,6 +81,13 @@ interface PruneCommandOptions extends ValidateCommandOptions {
75
81
  readonly olderThan?: string
76
82
  }
77
83
 
84
+ interface StaleCommandOptions extends ValidateCommandOptions {
85
+ readonly days?: string
86
+ }
87
+
88
+ /** The one kind `stale` reads, since only a memory entry carries a review date. */
89
+ const STALE_KINDS = ['memory'] as const
90
+
78
91
  interface OrdinalCommandOptions extends ValidateCommandOptions {
79
92
  readonly claim?: boolean
80
93
  }
@@ -247,6 +260,52 @@ export function register(program: Command): void {
247
260
  process.exitCode = await runSize(opts)
248
261
  })
249
262
 
263
+ records
264
+ .command('stale')
265
+ .description(
266
+ 'Report which entries are due for review and which cite paths the tree no longer holds',
267
+ )
268
+ .argument('<kind>', `Record folder: ${STALE_KINDS.join(', ')}`)
269
+ .helpOption('-h, --help', 'Show this help message')
270
+ .option('--json', 'Add a machine-readable record on stdout')
271
+ .option(
272
+ '--days <n>',
273
+ `Days after a review before an entry is due again (default ${DEFAULT_REVIEW_DAYS})`,
274
+ )
275
+ .option('--root <path>', 'Project root, defaulting to the main worktree')
276
+ .addHelpText(
277
+ 'after',
278
+ [
279
+ '',
280
+ 'Reads the top-level entries of the memory folder, never review/, archive/,',
281
+ 'or index.md. An entry is due when it carries no reviewed date, a reviewed',
282
+ 'value that is not a date, or one older than --days. A path is a backticked',
283
+ 'token holding a / and a file extension, resolved against the project root',
284
+ 'after any :line or #heading anchor is stripped. Tokens under .canon/,',
285
+ 'scratch, or worktrees, and any holding a placeholder, glob, variable, or',
286
+ 'elided ... segment, are never reported.',
287
+ '',
288
+ 'Entries are ordered due first, then those citing a missing path, then the',
289
+ 'longest unreviewed, then by name, so a review takes the first N as a batch.',
290
+ '',
291
+ 'Exit codes:',
292
+ ' 0 the reading completed, whatever it found',
293
+ ' 1 refused, with the reason on stderr or in the JSON record',
294
+ '',
295
+ 'It gates nothing and writes nothing. A due entry is a queue position rather',
296
+ 'than a failure, and a missing path is a prompt for the review to judge.',
297
+ '',
298
+ 'Examples:',
299
+ ' canon records stale memory',
300
+ ' canon records stale memory --json',
301
+ ' canon records stale memory --days 60 --json',
302
+ '',
303
+ ].join('\n'),
304
+ )
305
+ .action(async (kind: string, opts: StaleCommandOptions) => {
306
+ process.exitCode = await runStale(kind, opts)
307
+ })
308
+
250
309
  records
251
310
  .command('prune-tmp')
252
311
  .description('Report scratch nobody has touched in a while, and delete it')
@@ -484,6 +543,93 @@ async function runPruneTmp(opts: PruneCommandOptions): Promise<number> {
484
543
  return reportPrune(outcome, emitJson)
485
544
  }
486
545
 
546
+ async function runStale(
547
+ kind: string,
548
+ opts: StaleCommandOptions,
549
+ ): Promise<number> {
550
+ const emitJson = opts.json ?? false
551
+
552
+ if (!(STALE_KINDS as readonly string[]).includes(kind)) {
553
+ return reportRefusal(
554
+ 'canon records stale',
555
+ {
556
+ reason: 'unknown-kind',
557
+ message: `Not a kind stale reads: ${kind}. Expected one of: ${STALE_KINDS.join(', ')}.`,
558
+ },
559
+ emitJson,
560
+ )
561
+ }
562
+
563
+ const days = Number(opts.days ?? DEFAULT_REVIEW_DAYS)
564
+
565
+ if (!Number.isInteger(days) || days <= 0) {
566
+ return reportRefusal(
567
+ 'canon records stale',
568
+ {
569
+ reason: 'bad-days',
570
+ message: `--days must be a positive whole number of days, got ${opts.days}.`,
571
+ },
572
+ emitJson,
573
+ )
574
+ }
575
+
576
+ const root = opts.root ?? (await mainWorktreeRoot())
577
+ const outcome = await staleMemory(root, days)
578
+
579
+ if (!outcome.ok)
580
+ return reportRefusal('canon records stale', outcome, emitJson)
581
+
582
+ if (emitJson) {
583
+ process.stdout.write(`${JSON.stringify(outcome)}\n`)
584
+ return 0
585
+ }
586
+
587
+ reportStale(outcome)
588
+ return 0
589
+ }
590
+
591
+ function staleRow(entry: StaleEntry): string[] {
592
+ return [
593
+ entry.name,
594
+ entry.reviewed ?? (entry.invalidReviewed ? 'invalid' : 'never'),
595
+ entry.unresolved.join(', '),
596
+ ]
597
+ }
598
+
599
+ function reportStale(outcome: StaleReport): void {
600
+ const due = outcome.entries.filter((entry) => entry.due)
601
+ const moved = outcome.entries.filter((entry) => entry.unresolved.length > 0)
602
+
603
+ intro('canon records stale')
604
+ logStep('Due')
605
+
606
+ if (due.length === 0) {
607
+ logInfo(`none, every entry reviewed within ${plural(outcome.days, 'day')}`)
608
+ } else {
609
+ const headers = ['entry', 'reviewed', 'unresolved']
610
+ const rows = due.map(staleRow)
611
+ const widths = headers.map((header, column) =>
612
+ columnWidth(
613
+ header,
614
+ rows.map((row) => row[column]),
615
+ ),
616
+ )
617
+ const render = (cells: readonly string[]): string =>
618
+ cells
619
+ .map((cell, column) => cell.padEnd(widths[column]))
620
+ .join(' ')
621
+ .trimEnd()
622
+
623
+ pipeOutput([render(headers), ...rows.map(render)].join('\n'))
624
+ }
625
+
626
+ logStep('Total')
627
+ logInfo(
628
+ `${outcome.due} of ${outcome.total} due, ${moved.length} citing a missing path, in ${outcome.folder}`,
629
+ )
630
+ outro()
631
+ }
632
+
487
633
  function pruneRow(entry: PruneUnit): string[] {
488
634
  return [
489
635
  entry.path,
@@ -350,7 +350,9 @@ function runCheck(
350
350
 
351
351
  const parsed = parseTarget(target)
352
352
  if (parsed === undefined) {
353
- logError('Invalid target. Use <category>:<command>, e.g. claude:docs-fold.')
353
+ logError(
354
+ 'Invalid target. Use <category>:<command>, e.g. claude:context-fold.',
355
+ )
354
356
  outro()
355
357
  process.exitCode = 1
356
358
  return
@@ -418,7 +420,7 @@ export function register(program: Command): void {
418
420
  .description('Check a provisioned sandbox against a scenario expectation')
419
421
  .argument(
420
422
  '<target>',
421
- 'Scenario as <category>:<command>, e.g. claude:docs-fold',
423
+ 'Scenario as <category>:<command>, e.g. claude:context-fold',
422
424
  )
423
425
  .argument('[arm]', 'Named scenario arm, e.g. drift')
424
426
  .helpOption('-h, --help', 'Show this help message')
@@ -443,8 +445,8 @@ export function register(program: Command): void {
443
445
  [
444
446
  '',
445
447
  'Examples:',
446
- ' canon sandbox check claude:docs-fold drift',
447
- ' canon sandbox check claude:docs-fold drift --envelope run.json --json',
448
+ ' canon sandbox check claude:context-fold drift',
449
+ ' canon sandbox check claude:context-fold drift --envelope run.json --json',
448
450
  '',
449
451
  'Exit codes: 0 on pass or unchecked, 1 on failure.',
450
452
  'With --strict, unchecked exits 1 as well.',
@@ -182,7 +182,7 @@ export function sectionAround(
182
182
  * requested canonical doc types.
183
183
  *
184
184
  * Reads the working tree rather than `HEAD`, matching `listChangedFiles`: a
185
- * `docs-fold` run checking a session's own edits sees them before they are
185
+ * `context-fold` run checking a session's own edits sees them before they are
186
186
  * committed. A file the range deleted contributes nothing, since there is no
187
187
  * current section left to read a hunk's chunk against.
188
188
  */
@@ -14,7 +14,7 @@ export const DEFAULT_OLLAMA_BASE_URL = 'http://localhost:11434'
14
14
  * 600s ceiling meant for a batch research run, not a per-call budget for an
15
15
  * interactive verb. 30s is chosen here as a deviation, wide enough that a
16
16
  * shared GPU under another local session's load (measured coexisting at 12GB)
17
- * still has room, while short enough that a `docs-fold` run does not hang
17
+ * still has room, while short enough that a `context-fold` run does not hang
18
18
  * indefinitely on a backend that stopped responding mid-call.
19
19
  */
20
20
  export const OLLAMA_TIMEOUT_MS = 30_000
@@ -165,7 +165,7 @@ interface ModelLayerPlan {
165
165
  * Decides whether the model layer runs, probing reachability before any real
166
166
  * call. A configured-but-unreachable backend is a plan the caller reports and
167
167
  * falls back from, never a refusal: the groundwork decision reads "warn and
168
- * continue," and a refused run here would fail `docs-fold` for a reason that
168
+ * continue," and a refused run here would fail `context-fold` for a reason that
169
169
  * has nothing to do with the branch it is checking.
170
170
  */
171
171
  async function planModelLayer(
@@ -1242,7 +1242,7 @@ export const pluginManifests: Measure = async (ctx) => {
1242
1242
  return {
1243
1243
  emissions: [],
1244
1244
  unmeasured:
1245
- 'claude is on PATH and claude --version fails, so the install brought down no platform-native binary and no manifest was read. Raise or lower the pinned version at the Install Plugin CLI step in .github/workflows/verify.yml, and record the move in canon/context/ci.md.',
1245
+ 'claude is on PATH and claude --version fails, so the install brought down no platform-native binary and no manifest was read. Raise or lower the pinned version at the Install Plugin CLI step in .github/workflows/verify.yml, and record the move in canon/context/ci/checks.md.',
1246
1246
  }
1247
1247
  }
1248
1248
 
@@ -21,13 +21,18 @@ import { defineRenameRules, type RenameRules } from '@/migrate/rename'
21
21
  * reading in sequence with `draft-screencast` as one three-step pipeline with
22
22
  * no prefix forcing that reading.
23
23
  *
24
- * One row renames a skill rather than retiring a prefix. `ui-test` became
24
+ * Two rows rename a skill rather than retiring a prefix. `ui-test` became
25
25
  * `ui-checklist` when the skill stopped writing tests and shrank to the
26
26
  * checklist a reviewer reads, so the old name described a job it no longer
27
27
  * does. `claude-ui-test` is retargeted onto the same destination rather than
28
28
  * left pointing at the retired name, which lands a target on the oldest
29
29
  * spelling in one pass instead of two.
30
30
  *
31
+ * `docs-fold` became `context-fold` because the skill folds a session into the
32
+ * context entries, the canonical records, and the task board, while "docs" in
33
+ * its name collided with `docs-sync`, which owns `docs/`. `claude-docs` is
34
+ * retargeted onto `context-fold` for the same reason `claude-ui-test` was.
35
+ *
31
36
  * Every name takes two words. Ten of these would have landed as a bare single
32
37
  * word under a plain strip, and a bare word such as `review` or `docs` is a
33
38
  * substring of ordinary prose with no token left for a later sweep to find.
@@ -47,7 +52,8 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
47
52
  'claude-autoship': 'auto-ship',
48
53
  'claude-design-extract': 'design-extract',
49
54
  'claude-diagram': 'draft-diagram',
50
- 'claude-docs': 'docs-fold',
55
+ 'claude-docs': 'context-fold',
56
+ 'docs-fold': 'context-fold',
51
57
  'claude-feature': 'plan-feature',
52
58
  'claude-groundwork': 'plan-groundwork',
53
59
  'claude-intake': 'plan-intake',
@@ -105,6 +111,11 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
105
111
  * what shipped or what a session ran under whatever name was current then, so
106
112
  * rewriting one makes it testify to a release or a run that never happened.
107
113
  *
114
+ * The pointer left at `docs-fold` is excluded because it exists to answer under
115
+ * the old name, so a later run would move it onto the skill it points at. The
116
+ * exclusion is only safe once the real body has moved, since excluding the path
117
+ * ahead of the first sweep leaves that body behind.
118
+ *
108
119
  * The record archives need no entry here. They are gitignored, so the tracked
109
120
  * listing every sweep reads never reaches them.
110
121
  */
@@ -117,5 +128,7 @@ export const SKILL_NAME_RULES: RenameRules = defineRenameRules({
117
128
  'CHANGELOG.md',
118
129
  'src/migrate/skill-names.ts',
119
130
  'src/migrate/skill-names.test.ts',
131
+ 'claude/skills/docs-fold/SKILL.md',
132
+ 'claude/skills/docs-fold/REQUIREMENT.md',
120
133
  ],
121
134
  })