@erclx/canon 4.84.0 → 4.86.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 (35) 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/docs-fold/SKILL.md +1 -1
  4. package/claude/skills/draft-and-pick/REQUIREMENT.md +2 -1
  5. package/claude/skills/draft-and-pick/SKILL.md +3 -2
  6. package/claude/skills/draft-and-pick/references/live-arms.md +19 -0
  7. package/claude/skills/git-ship/SKILL.md +2 -2
  8. package/claude/skills/memory-capture/SKILL.md +1 -1
  9. package/claude/skills/memory-review/REQUIREMENT.md +1 -1
  10. package/claude/skills/memory-review/SKILL.md +25 -22
  11. package/claude/skills/memory-review/references/receipt-format.md +2 -2
  12. package/claude/skills/role-worker/SKILL.md +2 -1
  13. package/claude/skills/ux-walkthrough/SKILL.md +2 -2
  14. package/claude/skills/ux-walkthrough/references/candidate-pages.md +1 -16
  15. package/docs/agents/commands.md +4 -1
  16. package/docs/agents/records.md +6 -6
  17. package/docs/workflow/ai-workflow.md +2 -2
  18. package/governance/rules/claude/563-ready.md +11 -0
  19. package/governance/rules/core/045-memory.md +1 -1
  20. package/package.json +1 -1
  21. package/src/commands/migrate.ts +142 -0
  22. package/src/commands/records.ts +12 -6
  23. package/src/design/tokens.ts +7 -4
  24. package/src/migrate/record-layout.ts +346 -0
  25. package/src/migrate/record-tree.ts +1 -1
  26. package/src/migrate/scratch-evidence.ts +3 -3
  27. package/src/record-root.ts +2 -0
  28. package/src/records/backup.ts +132 -37
  29. package/src/records/size.ts +16 -12
  30. package/standards/index.md +1 -0
  31. package/standards/memory.md +1 -1
  32. package/standards/plan.md +2 -0
  33. package/standards/ready.md +105 -0
  34. package/standards/skill.md +1 -1
  35. package/tooling/claude/seeds/.claude/hooks/memory-index.sh +10 -0
@@ -1,27 +1,25 @@
1
- import { existsSync } from 'node:fs'
1
+ import { existsSync, readdirSync } from 'node:fs'
2
2
  import { basename, join, relative, resolve } from 'node:path'
3
3
  import { $ } from 'bun'
4
4
  import { gitEnv } from '@/git-env'
5
5
  import { RECORD_ROOTS, recordRoot } from '@/record-root'
6
6
 
7
7
  /**
8
- * The folders a backup carries, relative to the record root `workTree` resolves
9
- * rather than to either root specifically, since the same eleven names sit
10
- * under whichever one a tree holds.
8
+ * The legacy `.claude`-root allowlist, read only when a project has not moved
9
+ * to `.canon/` yet.
11
10
  *
12
- * Nothing bounds this list any more, and the move is what took the bound away.
13
- * The claude manifest used to ship a folder apiece, so the `# Claude` group
14
- * named a superset this could be read against; it ships one `.canon/` root
15
- * entry now and names no folder at all. Spelling the list out is therefore the
16
- * whole of the protection rather than half of it, since a record folder added
17
- * under `.canon/` is ignored the moment it exists and enters no payload until
18
- * a name is written here.
11
+ * That root still tracks `skills/`, `rules/`, and `hooks/` a backup must never
12
+ * carry, so it cannot take the exclusion-based reading `foldersAt` applies at
13
+ * `.canon/`: every top-level entry there is fair game and an allowlist is the
14
+ * only thing separating a record folder from the vendor's own. A `.canon/` root
15
+ * carries no such mix, since nothing else lives there, which is what let the
16
+ * bound move from this list to `EXCLUDED_ENTRIES`.
19
17
  *
20
- * Three counts describe this surface and each is right about a different
21
- * question, so they are stated apart rather than reconciled. Eleven is what a
22
- * disk loss would take, which is this list. Twelve is what sat under `.claude/`
23
- * as an ignored folder before the move, which adds the scratch folder that is
24
- * deletable without loss and `worktrees/`, whose contents belong to the
18
+ * Three counts describe what this list once bounded and each is right about a
19
+ * different question, so they are stated apart rather than reconciled. Eleven
20
+ * is what a disk loss would take, which is this list. Twelve is what sat under
21
+ * `.claude/` as an ignored folder before the move, which adds the scratch folder
22
+ * that is deletable without loss and `worktrees/`, whose contents belong to the
25
23
  * enclosing repository already. Thirteen is what the move relocated, which counts
26
24
  * ignore entries rather than folders: the twelve less `worktrees/`, which stayed,
27
25
  * plus `.records.git/` and the `README.md` a records pull writes back.
@@ -73,6 +71,53 @@ const RETIRED_FOLDERS = [
73
71
  /** The history directory's own name, which keeps its dot at either record root. */
74
72
  const RECORDS_GIT_NAME = '.records.git'
75
73
 
74
+ /**
75
+ * Top-level `.canon/` entries a backup never carries, named rather than
76
+ * matched by pattern so a reader can see the whole exemption in one place.
77
+ *
78
+ * `tmp` is deletable without loss, per the scratch standard. `ordinal-locks`
79
+ * is transient per claim, and pushing one races the claim it guards.
80
+ * `.records.git` is the history itself, and reading directories rather than
81
+ * every entry already keeps a pull's `README.md` out, so this is the one name
82
+ * that still has to be said: without it a listing would stage the history
83
+ * into itself.
84
+ */
85
+ export const EXCLUDED_ENTRIES = [
86
+ 'tmp',
87
+ 'ordinal-locks',
88
+ RECORDS_GIT_NAME,
89
+ ] as const
90
+
91
+ /**
92
+ * The record folders sitting at `dir` right now, at either root spelling.
93
+ *
94
+ * A `.claude`-spelled directory reads the fixed allowlist, filtered to what
95
+ * exists, since that root also holds `skills/`, `rules/`, and `hooks/` no
96
+ * exclusion set names. A `.canon`-spelled directory reads its own top-level
97
+ * directories less `EXCLUDED_ENTRIES` instead, since nothing else shares that
98
+ * root and a folder added there is a record folder by construction.
99
+ *
100
+ * Takes a directory rather than a project root so `strandedFolders` can ask it
101
+ * about a candidate root other than the one `workTree` resolved.
102
+ */
103
+ function foldersAt(dir: string): string[] {
104
+ if (basename(dir) === '.claude') {
105
+ return BACKED_FOLDERS.filter((folder) => existsSync(join(dir, folder)))
106
+ }
107
+
108
+ const entries = existsSync(dir)
109
+ ? readdirSync(dir, { withFileTypes: true })
110
+ : []
111
+
112
+ return entries
113
+ .filter((entry) => entry.isDirectory())
114
+ .map((entry) => entry.name)
115
+ .filter(
116
+ (name) =>
117
+ !EXCLUDED_ENTRIES.includes(name as (typeof EXCLUDED_ENTRIES)[number]),
118
+ )
119
+ }
120
+
76
121
  /**
77
122
  * The tree a backup stages, which is the record root itself.
78
123
  *
@@ -167,6 +212,8 @@ export interface PushReport {
167
212
  readonly ok: true
168
213
  readonly root: string
169
214
  readonly folders: readonly string[]
215
+ /** A folder in scope this push found on disk but the records index had never tracked. */
216
+ readonly firstSeen: readonly string[]
170
217
  readonly changed: number
171
218
  readonly commit?: string
172
219
  readonly pushed: boolean
@@ -372,9 +419,10 @@ async function resolveRemote(
372
419
  * work tree hard and leaves the stranded copy beside it, which is not a loss but
373
420
  * is a tree where two roots disagree and neither is wrong.
374
421
  *
375
- * Only `BACKED_FOLDERS` is read. The scratch folder is deletable by definition
376
- * and `worktrees/` belongs to the enclosing repository, so neither stranded
377
- * anywhere costs a record.
422
+ * Reads `foldersAt` on each candidate, so a `.canon` candidate is checked
423
+ * against its own exclusion set rather than the `.claude` allowlist. The
424
+ * scratch folder is deletable by definition and `worktrees/` belongs to the
425
+ * enclosing repository, so neither stranded anywhere costs a record.
378
426
  */
379
427
  function strandedFolders(root: string): string[] {
380
428
  const resolved = resolve(workTree(root))
@@ -383,9 +431,7 @@ function strandedFolders(root: string): string[] {
383
431
  const dir = join(root, candidate)
384
432
  if (resolve(dir) === resolved) return []
385
433
 
386
- return BACKED_FOLDERS.filter((folder) => existsSync(join(dir, folder))).map(
387
- (folder) => join(candidate, folder),
388
- )
434
+ return foldersAt(dir).map((folder) => join(candidate, folder))
389
435
  })
390
436
  }
391
437
 
@@ -404,9 +450,15 @@ function refuseSplitRoots(root: string): BackupRefused | undefined {
404
450
  )
405
451
  }
406
452
 
453
+ /** A pathspec-safe subset, plus which of it never entered the records index before. */
454
+ interface FolderScope {
455
+ readonly scope: readonly string[]
456
+ readonly firstSeen: readonly string[]
457
+ }
458
+
407
459
  /**
408
- * The subset of the backed and retired names a pathspec can name: on disk, or
409
- * already in the records index.
460
+ * The subset of the present, retired, and previously-tracked names a pathspec
461
+ * can name.
410
462
  *
411
463
  * A pathspec matching neither fails the whole `add`, which is why the subset
412
464
  * exists. The index half is what covers a folder deleted in full. Reading disk
@@ -415,29 +467,64 @@ function refuseSplitRoots(root: string): BackupRefused | undefined {
415
467
  * every other unpushed deletion.
416
468
  *
417
469
  * The retired names are the same case one level up, where the folder left the
418
- * backed list rather than the disk, and the index is the only side that still
419
- * knows it existed.
470
+ * disk before the index caught up, and the index is the only side that still
471
+ * knows it existed. At a `.canon` root a folder can leave the same way under a
472
+ * name `RETIRED_FOLDERS` never anticipated, which is what folding the index
473
+ * into the candidate set (rather than only using it as a filter) covers.
474
+ *
475
+ * `firstSeen` is a folder in scope that disk carries but the index has never
476
+ * tracked, which is the read a caller reports rather than acts on: a folder
477
+ * `.canon` picked up that should have been excluded is visible in the push
478
+ * report instead of entering the payload silently.
420
479
  */
421
- async function scopedFolders(root: string): Promise<string[]> {
480
+ async function scopedFolders(root: string): Promise<FolderScope> {
422
481
  const tracked = await records(root, ['ls-files'])
423
482
  const indexed = new Set(
424
483
  tracked.ok ? tracked.text.split('\n').filter(Boolean).map(topSegment) : [],
425
484
  )
426
485
 
427
- return [...BACKED_FOLDERS, ...RETIRED_FOLDERS].filter(
486
+ const present = new Set(foldersAt(workTree(root)))
487
+ const retired = RETIRED_FOLDERS.filter(
428
488
  (folder) => existsSync(join(workTree(root), folder)) || indexed.has(folder),
429
489
  )
490
+ const scope = [...new Set([...present, ...retired, ...indexed])].sort()
491
+ const firstSeen = scope.filter(
492
+ (folder) => present.has(folder) && !indexed.has(folder),
493
+ )
494
+
495
+ return { scope, firstSeen }
430
496
  }
431
497
 
432
498
  function topSegment(path: string): string {
433
499
  return path.split('/')[0]
434
500
  }
435
501
 
436
- /** What a report names, which is the folders a reader can go and open. */
437
- function presentFolders(root: string): string[] {
438
- return BACKED_FOLDERS.filter((folder) =>
439
- existsSync(join(workTree(root), folder)),
440
- )
502
+ /**
503
+ * What a report names, which is the folders a reader can go and open.
504
+ *
505
+ * Exported so the scratch-evidence walk and the push and pull help text read
506
+ * the same resolved set rather than each re-deriving it against
507
+ * `BACKED_FOLDERS`, which is silently wrong at a `.canon` root.
508
+ */
509
+ export function presentFolders(root: string): string[] {
510
+ return foldersAt(workTree(root))
511
+ }
512
+
513
+ /**
514
+ * The record folder names a project could ever carry, without regard to
515
+ * whether each currently exists: `BACKED_FOLDERS` at the legacy `.claude`
516
+ * root, since nothing else names a folder there, and the live directory
517
+ * listing at `.canon`, since nothing bounds that root's names from outside
518
+ * any more and a name nobody has created yet cannot be listed.
519
+ *
520
+ * Exported for `canon records size`, whose own contract reports every folder
521
+ * whether or not it exists so a caller reading the record gets a stable set
522
+ * of keys. `presentFolders` filters by existence instead, which is right for
523
+ * a push report naming what a reader can go and open and wrong here.
524
+ */
525
+ export function candidateFolders(root: string): string[] {
526
+ const tree = workTree(root)
527
+ return basename(tree) === '.claude' ? [...BACKED_FOLDERS] : foldersAt(tree)
441
528
  }
442
529
 
443
530
  function countLines(text: string): number {
@@ -459,7 +546,7 @@ export async function pushRecords(root: string): Promise<PushOutcome> {
459
546
  const remote = await resolveRemote(root, enclosing)
460
547
  if (typeof remote !== 'string') return remote
461
548
 
462
- const scope = await scopedFolders(root)
549
+ const { scope, firstSeen } = await scopedFolders(root)
463
550
 
464
551
  if (scope.length > 0) {
465
552
  // `-f` is what carries the payload: every backed folder is ignored by the
@@ -495,7 +582,7 @@ export async function pushRecords(root: string): Promise<PushOutcome> {
495
582
  const folders = presentFolders(root)
496
583
  const head = await records(root, ['rev-parse', '--short', 'HEAD'])
497
584
  if (!head.ok) {
498
- return { ok: true, root, folders, changed, pushed: false }
585
+ return { ok: true, root, folders, firstSeen, changed, pushed: false }
499
586
  }
500
587
 
501
588
  const branch = await projectBranch(root, enclosing)
@@ -506,7 +593,15 @@ export async function pushRecords(root: string): Promise<PushOutcome> {
506
593
  ])
507
594
  if (!pushed.ok) return failed('push', pushed)
508
595
 
509
- return { ok: true, root, folders, changed, commit: head.text, pushed: true }
596
+ return {
597
+ ok: true,
598
+ root,
599
+ folders,
600
+ firstSeen,
601
+ changed,
602
+ commit: head.text,
603
+ pushed: true,
604
+ }
510
605
  }
511
606
 
512
607
  /**
@@ -557,7 +652,7 @@ export async function pullRecords(root: string): Promise<PullOutcome> {
557
652
  const target = await records(root, ['rev-parse', 'FETCH_HEAD'])
558
653
  if (!target.ok) return failed('rev-parse', target)
559
654
 
560
- const scope = await scopedFolders(root)
655
+ const { scope } = await scopedFolders(root)
561
656
 
562
657
  if (scope.length > 0) {
563
658
  const dirty = await records(root, ['status', '--porcelain', '--', ...scope])
@@ -2,20 +2,24 @@ import { existsSync, type Stats } from 'node:fs'
2
2
  import { readdir, stat } from 'node:fs/promises'
3
3
  import { join } from 'node:path'
4
4
  import { RECORD_ROOTS, recordDir, SCRATCH } from '@/record-root'
5
- import { BACKED_FOLDERS } from '@/records/backup'
5
+ import { candidateFolders } from '@/records/backup'
6
6
 
7
7
  /**
8
- * The folders a size reading covers, named at the record root they sit under.
8
+ * The folders a size reading covers at `root`, resolved the same way a push
9
+ * resolves its scope rather than pinned to `BACKED_FOLDERS`, which is silently
10
+ * wrong at a `.canon` root.
9
11
  *
10
- * It is the backed set plus the scratch folder, which a backup skips because it is
11
- * deletable without loss and a reading covers because deletable is not the same
12
- * as empty: the routing handoffs and the memory archive both sit there and both
13
- * accumulate. `.records.git` stays out because it is the backup history rather
14
- * than a record, and `worktrees/` stays out because each entry there is a
15
- * checkout of the enclosing repository with its own removal verb, and one of
16
- * them outweighs every record folder combined.
12
+ * It is the present backed set plus the scratch folder, which a backup skips
13
+ * because it is deletable without loss and a reading covers because deletable
14
+ * is not the same as empty: the routing handoffs and the memory archive both
15
+ * sit there and both accumulate. `.records.git` stays out because it is the
16
+ * backup history rather than a record, and `worktrees/` stays out because each
17
+ * entry there is a checkout of the enclosing repository with its own removal
18
+ * verb, and one of them outweighs every record folder combined.
17
19
  */
18
- export const SIZED_FOLDERS = [...BACKED_FOLDERS, SCRATCH] as const
20
+ export function sizedFolders(root: string): string[] {
21
+ return [...candidateFolders(root), SCRATCH]
22
+ }
19
23
 
20
24
  /**
21
25
  * The windows a reading reports, in days.
@@ -227,9 +231,9 @@ export async function sizeRecords(
227
231
 
228
232
  // Each folder is walked independently, and the report is ordered by the
229
233
  // caller rather than by arrival, so `Promise.all` keeps the input order while
230
- // the ten walks overlap.
234
+ // the walks overlap.
231
235
  const folders = await Promise.all(
232
- SIZED_FOLDERS.map((folder) => measure(root, folder, now)),
236
+ sizedFolders(root).map((folder) => measure(root, folder, now)),
233
237
  )
234
238
 
235
239
  return {
@@ -27,6 +27,7 @@ Reference docs for consistent authoring across the toolkit and target projects.
27
27
  - [Pull request reference](pr.md): Pull request title and body conventions
28
28
  - [Publish reference](publish.md): Scan an author runs against finished text, the cross-reference form each destination takes, and the response to an unreadable source
29
29
  - [Readme reference](readme.md): Readme voice, structure, and content conventions
30
+ - [Ready reference](ready.md): Folder layout, ordinal naming, the overview frontmatter, the thin-plan contract, and the archive lifecycle for a finished-file handoff
30
31
  - [Requirements reference](requirements.md): Shape and content rules for canon/REQUIREMENTS.md
31
32
  - [Governance rule reference](rule.md): Rule frontmatter, body shape, and voice for .claude/rules files
32
33
  - [Session map reference](session.md): Filename and location, the sections a handoff carries, the write and read procedures, and how a role extends it
@@ -88,7 +88,7 @@ Capture the pattern rather than the recovery. What was tried, what failed, and w
88
88
 
89
89
  ## Links
90
90
 
91
- Link a related entry as `[[name]]`, where `name` is the target's filename stem without the extension. Link freely: the folder is flat and the links are the only structure it has.
91
+ Link a related entry as `[[name]]`, where `name` is the target's filename stem without the extension. Link freely: an entry sits flat at the top level and the links are the only structure among them. `review/` and `archive/` hold receipts and retirements rather than entries, so neither takes a link.
92
92
 
93
93
  - Place links inside the body part they support, not in a list of their own at the end.
94
94
  - A link naming an entry nobody has written yet is legal, and it marks a rule worth writing rather than a defect.
package/standards/plan.md CHANGED
@@ -85,6 +85,8 @@ A constraint measured against work in flight expires when that work merges, and
85
85
 
86
86
  A dead constraint fails silently in the expensive direction. A session honoring one ships the dangling citation the change created and reports success, where a session crossing a live constraint collides visibly and is caught.
87
87
 
88
+ A constraint naming a `.canon/ready/` folder is a third shape beside the two above. It makes that folder's files the verbatim source for the paths this plan's `**Files to touch:**` lists, per `ready.md`, so the executing session copies those paths rather than authoring them.
89
+
88
90
  ### Risks
89
91
 
90
92
  - Name the collision rather than the category. A risk a reader cannot act on is padding.
@@ -0,0 +1,105 @@
1
+ ---
2
+ title: Ready reference
3
+ description: Folder layout, ordinal naming, the overview frontmatter, the thin-plan contract, and the archive lifecycle for a finished-file handoff
4
+ ---
5
+
6
+ # Ready reference
7
+
8
+ Applies to a ready folder at `.canon/ready/<nn>-<slug>/`. A warm session that has already written a skill, a rule, or another finished file uses it to hand the exact text to the worker that ships it, since a plan only describes a change and a cold worker reading a description writes the file again from scratch. The folder holds the finished files themselves, laid out at their destination paths, so the worker's job is to copy rather than to author.
9
+
10
+ The folder is gitignored, and backed wherever a records remote is configured: `canon records push` and `canon records pull` protect it against the machine being lost there, refuse with `no-remote` where it is not, and protect nothing against a folder deleted before anyone has pushed. That is why the archive step below is a move rather than a cleanup.
11
+
12
+ ## Scope
13
+
14
+ Governs a ready folder under `.canon/ready/<nn>-<slug>/`: folder layout, ordinal naming, the overview's frontmatter, what the mirrored tree holds, the thin-plan contract that ships it, and the lifecycle from the live folder to the archive.
15
+
16
+ Does not govern:
17
+
18
+ - The thin plan itself, its filename, its sections, and its suggested-and-answer contract: `plan.md`
19
+ - The task file that reaches a ready folder through a plan, and the origin line pointing back at it: `tasks.md`
20
+ - Voice, rhythm, and sentence construction: the `write-human` skill
21
+ - Headings, punctuation, word choice, and file references: `markdown.md`
22
+ - Whether a change earns a ready folder over a plan a worker builds from scratch, which belongs to the warm session deciding how to hand off its own work
23
+
24
+ ## What a working ready folder looks like
25
+
26
+ A ready folder works when a worker that has never seen the warm session's conversation can copy from it alone:
27
+
28
+ - Which destination path does each file land at, and does the folder hold nothing else?
29
+ - What is the worker still responsible for that the files themselves do not carry, such as a docs sync, a sandbox scenario, or a test?
30
+ - What branch type does the change take?
31
+ - Is every path the folder mirrors also declared in the thin plan's `**Files to touch:**`?
32
+
33
+ A ready folder failing these is non-conforming even when it satisfies every shape rule below.
34
+
35
+ ## Folder name
36
+
37
+ - Name the folder `<nn>-<slug>`, a two-digit zero-padded ordinal followed by a kebab-case slug matching the plan's own slug.
38
+ - The ordinal marks a folder per handoff, opened once by the warm session that writes it. It runs on its own sequence, separate from groundwork and intake's shared one, since a ready folder is not a measurement track.
39
+ - With no folder holding an entry yet, the first one opened takes `01`. Read the highest existing `.canon/ready/<nn>-*/` folder, including the archive, and take the next integer.
40
+ - Never renumber an existing folder. The ordinal is the order it opened, and the pull request that shipped it cites the folder by that name.
41
+
42
+ ## 00-overview.md
43
+
44
+ Every ready folder carries `00-overview.md` at its root, beside the mirrored tree. It orients the worker and states what the files themselves cannot.
45
+
46
+ - `title` (required): the change in sentence case
47
+ - `description` (required): one line naming what the handoff carries
48
+ - `type` (required): the branch type the plan should take, one of the types `branch.md` fixes
49
+ - `destinations` (required): the list of destination paths the folder mirrors, matching the thin plan's `**Files to touch:**` exactly
50
+
51
+ ```yaml
52
+ ---
53
+ title: <Change in sentence case>
54
+ description: <one line naming what the handoff carries>
55
+ type: <feat | fix | chore | ...>
56
+ destinations:
57
+ - <path/to/file>
58
+ ---
59
+ ```
60
+
61
+ Below the frontmatter, state in prose what the worker still owns beyond copying the files: a docs sync, a sandbox scenario update, a test the files do not include, or "nothing further" where the files are the whole of the change.
62
+
63
+ ## The mirrored tree
64
+
65
+ - Every other file in the folder sits at the same relative path its destination has in the project, so `standards/ready.md` inside the destination tree sits at `<nn>-<slug>/standards/ready.md` inside the ready folder.
66
+ - Carry no file the destination tree would not carry. A ready folder is a source for `git mv`-shaped copies, not a scratch pad for the warm session's own notes. Anything else belongs in the plan or in the pull request body.
67
+ - Write each file exactly as it should land. The worker copies verbatim and edits only what the gate or the overview's own list requires, so a placeholder or a half-finished passage ships as written.
68
+
69
+ ## The thin-plan contract
70
+
71
+ - A ready folder ships through an ordinary task row and a plan at `.canon/plans/feature-<slug>.md`, per `plan.md`. No new plan shape exists for it.
72
+ - Name the ready folder in the plan's `**Constraints:**`, stating that the folder's files are the verbatim source for the paths the plan's `**Files to touch:**` lists.
73
+ - List every destination path in `**Files to touch:**`, matching `00-overview.md`'s `destinations` field. A path the plan omits is invisible to `plan-reach`'s collision check, so a mismatch between the two lists is a defect in the plan rather than a variant the standard permits.
74
+ - Keep the plan itself thin. Its `**Files to touch:**` entries may point at the ready folder's own copy for the reason behind each file rather than restating it, since the overview and the files already carry the detail a plan would otherwise duplicate.
75
+
76
+ ## Lifecycle
77
+
78
+ - Write the ready folder in the same session that writes the files it carries. A folder assembled later from memory is a plan with extra steps, not a handoff.
79
+ - Move the folder to `.canon/ready/archive/<nn>-<slug>/` by hand when the task that shipped it archives. No board verb currently automates this move. `canon tasks archive` moves the task and its plan and leaves the ready folder where it is.
80
+ - Never delete a ready folder. The archived copy sits beside the merged pull request as the exact text that shipped, the way an archived plan sits beside the reasoning that produced it.
81
+
82
+ ## Anti-patterns
83
+
84
+ - **The folder with an undeclared destination.** A file the plan's `**Files to touch:**` does not list passes the collision check unseen, and a second track can write the same path without either side finding out.
85
+ - **The rewritten copy.** A worker that reads the folder's files as inspiration and writes its own version loses the exact text the handoff exists to carry.
86
+ - **The folder as scratch.** Notes, alternates, or draft passages left in the folder beside the real files leave the worker guessing which is the source.
87
+ - **The folder left live after shipping.** A ready folder nobody moves to the archive reads as unshipped work to the next session that lists the live folder.
88
+
89
+ ## Template
90
+
91
+ ```markdown
92
+ .canon/ready/<nn>-<slug>/
93
+ ├── 00-overview.md
94
+ └── <path/to/file> # mirrors the destination tree, one entry per file
95
+ ```
96
+
97
+ ```yaml
98
+ ---
99
+ title: <Change in sentence case>
100
+ description: <one line naming what the handoff carries>
101
+ type: <feat | fix | chore | ...>
102
+ destinations:
103
+ - <path/to/file>
104
+ ---
105
+ ```
@@ -210,7 +210,7 @@ Without this skill, a session <observed failure>, <observed failure>.
210
210
  ### Output and tuning
211
211
 
212
212
  - Skill success lines emit the full relative path from the project root (`<dir>/<file>`) for any file written, updated, or deleted. A bare filename names a file the reader cannot open. The `## Output` section of the project's instruction file sets the form that path takes, so a skill body states which path is emitted and leaves the form to that section.
213
- - Before a skill writes anything, decide whether the output is a deliverable the project keeps or a toolkit session record. A deliverable lands among the project's own tracked files. A session record lands under `.canon/`, in the named subfolder for its kind (`tasks/`, `plans/`, `review/`, `memory/`, `groundwork/`, `intake/`, `proposals/`, `diagrams/`, `teach/`, or `walkthroughs/`, with `tmp/` for scratch nothing else claims), never in a folder the body invents. `canon/ARCHITECTURE.md`'s per-folder decisions are the precedent for which kind takes which folder.
213
+ - Before a skill writes anything, decide whether the output is a deliverable the project keeps or a toolkit session record. A deliverable lands among the project's own tracked files. A session record lands under `.canon/`, in the named subfolder for its kind (`tasks/`, `plans/`, `review/`, `memory/`, `groundwork/`, `intake/`, `proposals/`, `diagrams/`, `teach/`, `ready/`, or `walkthroughs/`, with `tmp/` for scratch nothing else claims), never in a folder the body invents. `canon/ARCHITECTURE.md`'s per-folder decisions are the precedent for which kind takes which folder.
214
214
  - Codify a skill's posted or generated output as a fenced template, and keep the body consistent with every capability the frontmatter description names.
215
215
  - When a skill gathers user input or pre-seeds a template, attach a concrete proposed default to every question, derived from project context. Accept "use defaults" as a bulk-confirm.
216
216
  - Separate correctness axes (routing, sourcing, escalation, decline) from shape axes (line count, formatting, variant sprawl) when tuning a skill. Tighten only on correctness regressions. Do not convert soft caps to hard caps for aesthetic drift when correctness passes.
@@ -33,6 +33,16 @@ case "$file_path" in
33
33
  *) exit 0 ;;
34
34
  esac
35
35
 
36
+ # A shell `case` `*` crosses `/`, so the match above also catches a receipt
37
+ # under memory/review/ or a retired entry under memory/archive/. Neither is
38
+ # a pen entry the index renders, so both exit here before the regen call.
39
+ case "$file_path" in
40
+ */.claude/memory/review/* | */.canon/memory/review/* | \
41
+ */.claude/memory/archive/* | */.canon/memory/archive/*)
42
+ exit 0
43
+ ;;
44
+ esac
45
+
36
46
  case "$file_path" in
37
47
  */.claude/memory/index.md | */.canon/memory/index.md) exit 0 ;;
38
48
  esac