@erclx/canon 4.126.0 → 5.1.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 (107) hide show
  1. package/README.md +5 -7
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/auto-ship/references/verb-absent.md +0 -1
  4. package/claude/skills/canon-feedback/REQUIREMENT.md +1 -1
  5. package/claude/skills/canon-feedback/SKILL.md +2 -2
  6. package/claude/skills/canon-operator/REQUIREMENT.md +2 -2
  7. package/claude/skills/canon-operator/SKILL.md +3 -3
  8. package/claude/skills/codebase-layout/REQUIREMENT.md +1 -0
  9. package/claude/skills/codebase-layout/SKILL.md +6 -0
  10. package/claude/skills/create-rule/REQUIREMENT.md +1 -1
  11. package/claude/skills/create-standard/REQUIREMENT.md +0 -2
  12. package/claude/skills/decision-escalate/REQUIREMENT.md +1 -2
  13. package/claude/skills/draft-context/REQUIREMENT.md +1 -1
  14. package/claude/skills/draft-docs/REQUIREMENT.md +1 -1
  15. package/claude/skills/draft-wireframes/REQUIREMENT.md +1 -1
  16. package/claude/skills/git-pr/references/task-number.md +2 -0
  17. package/claude/skills/plan-intake/SKILL.md +1 -1
  18. package/claude/skills/target-setup/REQUIREMENT.md +2 -2
  19. package/claude/skills/target-setup/SKILL.md +1 -1
  20. package/claude/skills/target-setup/references/detect.md +1 -1
  21. package/claude/skills/target-setup/references/indexes.md +1 -5
  22. package/claude/skills/task-board/references/archive.md +1 -0
  23. package/docs/agents/commands.md +1 -3
  24. package/docs/agents/counts.md +1 -2
  25. package/docs/agents/docs.md +1 -1
  26. package/docs/agents/drift-surfaces.md +6 -4
  27. package/docs/agents/index.md +1 -1
  28. package/docs/agents/indexes.md +1 -1
  29. package/docs/agents/install-and-sync.md +1 -8
  30. package/docs/agents/markdown-audit.md +2 -2
  31. package/docs/agents/restated.md +7 -5
  32. package/docs/agents/review-classification.md +1 -2
  33. package/docs/agents/scripting.md +2 -3
  34. package/docs/agents/skills-reach.md +3 -3
  35. package/docs/agents/state-scoped-risk.md +1 -1
  36. package/docs/agents/tasks-archive.md +1 -1
  37. package/docs/agents/tasks.md +4 -0
  38. package/docs/target/index.md +4 -4
  39. package/docs/target/{target-migrations.md → migrations.md} +17 -1
  40. package/docs/target/{target-projects.md → projects.md} +5 -7
  41. package/docs/target/{target-scenarios.md → scenarios.md} +1 -1
  42. package/docs/target/{target-sync.md → sync.md} +4 -2
  43. package/docs/workflow/ai-workflow.md +3 -7
  44. package/docs/workflow/operating-model.md +4 -4
  45. package/docs/workflow/parallel-features.md +1 -1
  46. package/docs/workflow/skill-map.md +1 -2
  47. package/docs/workflow/visual-design-workflow.md +5 -5
  48. package/docs/workflow/zshrc-aliases.md +1 -1
  49. package/package.json +1 -2
  50. package/scripts/core/check-plugin-boundary.sh +1 -1
  51. package/scripts/core/install-check.sh +3 -4
  52. package/scripts/core/regen-hero.sh +2 -9
  53. package/src/autoship/paths.ts +3 -5
  54. package/src/claude/cases/authoring.ts +1 -5
  55. package/src/claude/skills-reach.ts +1 -2
  56. package/src/cli.ts +0 -2
  57. package/src/commands/feedback-format.ts +0 -1
  58. package/src/commands/gov.ts +8 -7
  59. package/src/commands/labels.ts +3 -3
  60. package/src/commands/markdown.ts +2 -2
  61. package/src/commands/tasks.ts +82 -2
  62. package/src/counts/catalogs.ts +0 -16
  63. package/src/counts/scan.ts +1 -1
  64. package/src/docs/read.ts +25 -25
  65. package/src/gate/measures.ts +52 -0
  66. package/src/gate/stages.ts +9 -0
  67. package/src/gov/restated.ts +60 -22
  68. package/src/help.ts +0 -2
  69. package/src/init/steps.ts +1 -1
  70. package/src/labels/format.ts +2 -2
  71. package/src/migrate/skill-names.ts +4 -5
  72. package/src/secrets/shipped.ts +2 -2
  73. package/src/serve/static.ts +1 -2
  74. package/src/shipped/references.ts +7 -8
  75. package/src/sync/engine.ts +2 -2
  76. package/src/tasks/archive.ts +30 -0
  77. package/src/tasks/record.ts +106 -9
  78. package/standards/commit.md +1 -1
  79. package/standards/context.md +2 -0
  80. package/standards/docs.md +1 -0
  81. package/standards/index.md +0 -1
  82. package/standards/pr.md +1 -1
  83. package/standards/skill.md +1 -2
  84. package/standards/tasks.md +2 -2
  85. package/standards/wiki.md +2 -1
  86. package/tooling/base/reference.md +2 -2
  87. package/tooling/claude/reference.md +1 -1
  88. package/claude/skills/create-snippet/REQUIREMENT.md +0 -39
  89. package/claude/skills/create-snippet/SKILL.md +0 -30
  90. package/scripts/snippets/create.sh +0 -156
  91. package/snippets/align.md +0 -12
  92. package/snippets/claude/decision-memo.md +0 -39
  93. package/snippets/claude/feature-recap.md +0 -19
  94. package/snippets/claude/figma-steps.md +0 -24
  95. package/snippets/compact-summary.md +0 -5
  96. package/snippets/decision-help.md +0 -6
  97. package/snippets/meta-prompt.md +0 -14
  98. package/snippets/research-prompt.md +0 -7
  99. package/snippets/session-notes.md +0 -11
  100. package/snippets/snippets.toml +0 -5
  101. package/snippets/step-by-step.md +0 -10
  102. package/snippets/web-research.md +0 -21
  103. package/src/commands/snippets.ts +0 -90
  104. package/src/snippets/categories.ts +0 -66
  105. package/src/snippets/list.ts +0 -32
  106. package/src/snippets/presets.ts +0 -50
  107. package/standards/snippets.md +0 -76
@@ -47,6 +47,7 @@ export const ARCHIVE_REFUSALS = [
47
47
  'no-outcomes',
48
48
  'open-outcomes',
49
49
  'earlier-slice',
50
+ 'pending-branch',
50
51
  'bad-input',
51
52
  ] as const
52
53
 
@@ -251,6 +252,22 @@ export function readPullRequest(text: string): readonly number[] {
251
252
  return match[1].split(',').map((entry) => Number(entry.trim().slice(1)))
252
253
  }
253
254
 
255
+ /**
256
+ * Reads the `Pending branch:` line `canon tasks outcome` writes when a branch
257
+ * ticks an outcome ahead of its own pull request number reaching the task.
258
+ * `canon tasks pull-request` clears each branch as it records, so a name left
259
+ * here is a slice whose number the `Pull request:` line cannot list yet.
260
+ */
261
+ export function readPendingBranches(text: string): readonly string[] {
262
+ const match = /^Pending branch:(.*)$/m.exec(text)
263
+ if (!match) return []
264
+
265
+ return match[1]
266
+ .split(',')
267
+ .map((entry) => entry.trim())
268
+ .filter((entry) => entry.length > 0)
269
+ }
270
+
254
271
  /** A markdown link on a `Plan:` line, its target captured. */
255
272
  const PLAN_LINK_PATTERN = /\[[^\]]*\]\(([^)]+)\)/g
256
273
 
@@ -860,6 +877,19 @@ export async function archiveTask(
860
877
  )
861
878
  }
862
879
 
880
+ // A branch ticks its outcome before `git-pr` appends its number, so in that
881
+ // window the number above reads as last while it is not. The stem selector
882
+ // skips this check the way it skips the one above, since a hand archive is
883
+ // the route out of a pending branch that was abandoned.
884
+ const pending = readPendingBranches(text)
885
+ if (selector.kind === 'pull-request' && pending.length > 0) {
886
+ return refuse(
887
+ 'pending-branch',
888
+ `${stem} still waits on ${pending.join(', ')}, which closed an outcome before its own pull request was recorded, so the task closes when that pull request merges.`,
889
+ pending,
890
+ )
891
+ }
892
+
863
893
  const { open, closed, cut } = readOutcomes(text)
864
894
 
865
895
  if (open.length > 0) {
@@ -10,6 +10,7 @@ import {
10
10
  OUTCOME_PATTERN,
11
11
  planLine,
12
12
  planLineIndex,
13
+ readPendingBranches,
13
14
  readPlanTargets,
14
15
  readPullRequest,
15
16
  tasksDir,
@@ -63,6 +64,8 @@ export interface PullRequestRecorded {
63
64
  readonly path: string
64
65
  readonly number: number
65
66
  readonly action: LineAction
67
+ /** Branches still on the `Pending branch:` line after this write. */
68
+ readonly pending: readonly string[]
66
69
  }
67
70
 
68
71
  export type PullRequestOutcome = PullRequestRecorded | RecordRefused
@@ -85,6 +88,8 @@ export interface OutcomesClosed {
85
88
  readonly path: string
86
89
  readonly closed: readonly string[]
87
90
  readonly alreadyClosed: readonly string[]
91
+ /** Branches on the `Pending branch:` line after this write. */
92
+ readonly pending: readonly string[]
88
93
  }
89
94
 
90
95
  export type CloseOutcome = OutcomesClosed | RecordRefused
@@ -146,16 +151,77 @@ export function writePullRequestLine(
146
151
  return { text: lines.join('\n'), action: 'added' }
147
152
  }
148
153
 
149
- function lastOriginLine(lines: readonly string[]): number | undefined {
154
+ function lastOriginLine(
155
+ lines: readonly string[],
156
+ prefixes: readonly string[] = ORIGIN_PREFIXES,
157
+ ): number | undefined {
150
158
  let found: number | undefined
151
159
 
152
160
  for (const [index, line] of lines.entries()) {
153
- if (ORIGIN_PREFIXES.some((prefix) => line.startsWith(prefix))) found = index
161
+ if (prefixes.some((prefix) => line.startsWith(prefix))) found = index
154
162
  }
155
163
 
156
164
  return found
157
165
  }
158
166
 
167
+ const PENDING_PREFIX = 'Pending branch:'
168
+
169
+ /**
170
+ * Names a branch on the `Pending branch:` line, which holds the task open
171
+ * against a merge-time archive until that branch's own number is recorded. The
172
+ * line sits under `Pull request:` when one exists, since a later slice ticks
173
+ * after an earlier slice's number is already listed.
174
+ */
175
+ export function writePendingBranch(
176
+ text: string,
177
+ branch: string,
178
+ ): { readonly text: string; readonly changed: boolean } {
179
+ const lines = text.split('\n')
180
+ const existing = lines.findIndex((entry) => entry.startsWith(PENDING_PREFIX))
181
+
182
+ if (existing !== -1) {
183
+ const pending = readPendingBranches(lines[existing])
184
+ if (pending.includes(branch)) return { text, changed: false }
185
+ lines[existing] = `${PENDING_PREFIX} ${[...pending, branch].join(', ')}`
186
+ return { text: lines.join('\n'), changed: true }
187
+ }
188
+
189
+ const line = `${PENDING_PREFIX} ${branch}`
190
+ const anchor = lastOriginLine(lines, [...ORIGIN_PREFIXES, 'Pull request:'])
191
+ if (anchor !== undefined) {
192
+ lines.splice(anchor + 1, 0, line)
193
+ return { text: lines.join('\n'), changed: true }
194
+ }
195
+
196
+ const heading = lines.findIndex((entry) => entry.startsWith('# '))
197
+ if (heading === -1) return { text: `${line}\n${text}`, changed: true }
198
+
199
+ lines.splice(heading + 1, 0, '', line)
200
+ return { text: lines.join('\n'), changed: true }
201
+ }
202
+
203
+ /** Drops a branch from the `Pending branch:` line, and the line once it is empty. */
204
+ export function clearPendingBranch(
205
+ text: string,
206
+ branch: string,
207
+ ): { readonly text: string; readonly changed: boolean } {
208
+ const lines = text.split('\n')
209
+ const existing = lines.findIndex((entry) => entry.startsWith(PENDING_PREFIX))
210
+ if (existing === -1) return { text, changed: false }
211
+
212
+ const pending = readPendingBranches(lines[existing])
213
+ if (!pending.includes(branch)) return { text, changed: false }
214
+
215
+ const rest = pending.filter((entry) => entry !== branch)
216
+ if (rest.length > 0) {
217
+ lines[existing] = `${PENDING_PREFIX} ${rest.join(', ')}`
218
+ } else {
219
+ lines.splice(existing, 1)
220
+ }
221
+
222
+ return { text: lines.join('\n'), changed: true }
223
+ }
224
+
159
225
  export type PlanLineWrite =
160
226
  | {
161
227
  readonly ok: true
@@ -330,24 +396,43 @@ async function openTask(
330
396
  * this from a linked worktree, where an in-place edit through the file-editing
331
397
  * tools is refused and a shell stream editor is banned, so the write has to
332
398
  * resolve the board root in-process.
399
+ *
400
+ * `branches` names the recording branch and every name it carried before a
401
+ * rename, since `git-ship` renames a branch after `context-fold` has already
402
+ * written the marker under the old name. Each comes off the `Pending branch:`
403
+ * line even when the number was already listed, so a rerun repairs a stale
404
+ * marker.
333
405
  */
334
406
  export async function recordPullRequest(
335
407
  root: string,
336
408
  selector: RecordSelector,
337
409
  number: number,
410
+ branches: readonly string[] = [],
338
411
  ): Promise<PullRequestOutcome> {
339
412
  const opened = await openTask(root, selector)
340
413
  if ('ok' in opened) return opened
341
414
 
342
415
  const { stem, path } = opened
343
- const { text, action } = writePullRequestLine(
344
- await readFile(path, 'utf8'),
345
- number,
346
- )
416
+ const numbered = writePullRequestLine(await readFile(path, 'utf8'), number)
417
+ let text = numbered.text
418
+ let isCleared = false
419
+
420
+ for (const branch of branches) {
421
+ const cleared = clearPendingBranch(text, branch)
422
+ text = cleared.text
423
+ isCleared ||= cleared.changed
424
+ }
347
425
 
348
- if (action !== 'unchanged') await writeFile(path, text)
426
+ if (numbered.action !== 'unchanged' || isCleared) await writeFile(path, text)
349
427
 
350
- return { ok: true, stem, path, number, action }
428
+ return {
429
+ ok: true,
430
+ stem,
431
+ path,
432
+ number,
433
+ action: numbered.action,
434
+ pending: readPendingBranches(text),
435
+ }
351
436
  }
352
437
 
353
438
  /**
@@ -399,11 +484,17 @@ export async function recordPlan(
399
484
  * Marks the named outcomes `[x]` in place. `context-fold` runs this against a
400
485
  * board it can read and cannot edit from a linked worktree, and the positions
401
486
  * come from the read it already made.
487
+ *
488
+ * A `branch` that ticks at least one new box goes on the `Pending branch:`
489
+ * line, so the merge of an earlier slice cannot archive the task in the window
490
+ * before this branch's own number is recorded. A rerun that ticks nothing adds
491
+ * no marker, since its branch has already recorded or is already listed.
402
492
  */
403
493
  export async function closeOutcomes(
404
494
  root: string,
405
495
  selector: RecordSelector,
406
496
  positions: readonly number[],
497
+ branch?: string,
407
498
  ): Promise<CloseOutcome> {
408
499
  const opened = await openTask(root, selector)
409
500
  if ('ok' in opened) return opened
@@ -427,7 +518,12 @@ export async function closeOutcomes(
427
518
  )
428
519
  }
429
520
 
430
- if (result.closed.length > 0) await writeFile(path, result.text)
521
+ const text =
522
+ result.closed.length > 0 && branch
523
+ ? writePendingBranch(result.text, branch).text
524
+ : result.text
525
+
526
+ if (result.closed.length > 0) await writeFile(path, text)
431
527
 
432
528
  return {
433
529
  ok: true,
@@ -435,5 +531,6 @@ export async function closeOutcomes(
435
531
  path,
436
532
  closed: result.closed,
437
533
  alreadyClosed: result.alreadyClosed,
534
+ pending: readPendingBranches(text),
438
535
  }
439
536
  }
@@ -17,7 +17,7 @@ Does not govern:
17
17
 
18
18
  ## Format
19
19
 
20
- - Structure: `<type>(<scope>): <subject>`
20
+ - Structure: `<type>(<scope>): <subject>`, with `!` before the colon for a breaking change
21
21
  - Casing: lowercase for `<type>`, `<scope>`, and first word of `<subject>`
22
22
  - Subject: 72 characters maximum, no trailing period
23
23
 
@@ -111,6 +111,8 @@ Only the `development` entry carries this section. It is not a general-purpose h
111
111
  - Retire a decision or gotcha once its subject is gone, rewriting the bullet to state the current design rather than leaving the narration of what it replaced beside it. A rejected alternative is not a retired one, so what was tried and why it lost stays whatever its age. The rule above protects content whose subject is live, and this one releases content whose subject is not.
112
112
  - Rewrite a decision a later one replaced rather than appending the replacement beside it. The subject is still live, so the rule above does not reach it, and two bullets on one subject leave a reader to work out which of them is current. State the design that stands and keep the superseded reasoning only where it is the alternative that lost.
113
113
  - Split into a folder (`canon/context/<domain>/<sub-area>.md`) when a domain has three or more sub-areas that do not fit cleanly in one file. That split is the natural ceiling.
114
+ - Group whole single-file entries of two or more small domains into one folder when they share a kind, such as small feature domains that each own a verb. Each entry still covers one domain, and the folder's generated `index.md` carries a `subtitle` naming the kind. It names an entry to start with only when that entry frames the others. A grouping folder is not a split, so no entry inside it is a sub-area of another.
115
+ - Name an entry inside a split or grouping folder per the `codebase-layout` skill, which never repeats the folder's name as the entry's stem or prefix.
114
116
  - Keep a split domain's `index.md` generated. The catalog body is rewritten on every regen, so the domain's own overview and layout belong in a sibling file rather than in it, and the `subtitle` is what names the file to start with.
115
117
  - Author a catalog that grows a row per shipped thing as a bullet list, never a markdown table. A table pads its columns to a shared width, so one row outgrowing its column reflows every row and turns a one-line edit into a whole-file rewrite that conflicts with any sibling branch. This is what forced the split above.
116
118
 
package/standards/docs.md CHANGED
@@ -72,6 +72,7 @@ A page failing these is non-conforming even when it satisfies every shape rule b
72
72
  - Move every page carrying that value. A shelf split across two depths is harder to scan than either arrangement taken whole.
73
73
  - Give the subfolder a catalog page of its own, so the parent lists one entry where it listed several. The parent lists a subfolder as a folder rather than as a member of the shelf it replaced, so nothing on that page restores the grouping.
74
74
  - Retarget every link into a moved page in the same change. A link resolving nowhere is what the move costs, and nothing else reports it.
75
+ - Name a moved page per the `codebase-layout` skill, which drops the prefix the subfolder now states.
75
76
 
76
77
  ## Template
77
78
 
@@ -34,7 +34,6 @@ Reference docs for consistent authoring across the toolkit and target projects.
34
34
  - [Skill requirement reference](skill-requirement.md): Shape and content rules for the REQUIREMENT.md beside a skill
35
35
  - [Claude skill reference](skill.md): Claude skill structure and authoring rules
36
36
  - [Slug reference](slug.md): Transform from a git branch name to a slug, and the three responses to an empty result
37
- - [Snippet reference](snippets.md): Snippet reference and authoring conventions
38
37
  - [Standard reference](standard.md): Shape and content rules for authoring a standard
39
38
  - [Tasks reference](tasks.md): Folder layout, filename convention, file format, origin lines, and archiving for task files under .canon/tasks/
40
39
  - [Teach reference](teach.md): Workspace layout, ordinal naming, frontmatter, and the mission and learning-record formats for a learning workspace
package/standards/pr.md CHANGED
@@ -20,7 +20,7 @@ Does not govern:
20
20
 
21
21
  ## Title
22
22
 
23
- - Format: `<type>(<scope>): <subject>`
23
+ - Format: `<type>(<scope>): <subject>`, with `!` before the colon for a breaking change
24
24
  - Casing: lowercase for `<type>`, `<scope>`, and first word of `<subject>`
25
25
  - Length: 72 characters maximum
26
26
 
@@ -17,7 +17,6 @@ Does not govern:
17
17
 
18
18
  - What a `REQUIREMENT.md` must answer, its sections, and its template: `skill-requirement.md`
19
19
  - Path-scoped coding rules, which load on a file match rather than on a request match: `rule.md`
20
- - Single-purpose chat prompts carrying no frontmatter, references, or scripts: `snippets.md`
21
20
  - Voice, rhythm, and sentence construction in a skill body: the `write-human` skill
22
21
  - Punctuation, formatting, and word choice in a skill body: `markdown.md`
23
22
  - The transform from a branch name to a slug a skill carries in a filename: `slug.md`
@@ -128,7 +127,7 @@ allowed-tools: <tools required>
128
127
 
129
128
  - Avoid flags that dispatch between alternate flows. The model misreads them and runs the vanilla path. Dry-run-style toggles are fine. For alternate flows, prefer a separate skill or manual invocation of two skills in sequence.
130
129
  - When a skill should fire from multiple callers, rely on description matching with strong trigger phrases. Do not hardcode `Skill` calls in sibling skills that could trigger it naturally.
131
- - Before collapsing a manual multi-step flow into a skill, ask what the manual pauses do. Where a pause carries external timing, error-surfacing, or judgment weight, prefer a snippet or explicit per-step confirmation.
130
+ - Before collapsing a manual multi-step flow into a skill, ask what the manual pauses do. Where a pause carries external timing, error-surfacing, or judgment weight, prefer explicit per-step confirmation.
132
131
 
133
132
  ### Output and tuning
134
133
 
@@ -72,7 +72,7 @@ Add no third heading. Status stays inline on an outcome rather than becoming an
72
72
 
73
73
  Size the outcomes so one pull request closes all of them, and split the task before handing it off rather than after. A task closes whole or not at all, so outcomes spanning two pull requests leave the board showing in-progress work no branch carries.
74
74
 
75
- A sliced task is the one allowed exception: a task the operator decided to ship across several pull requests on purpose, one slice each. Its `Pull request:` line lists every slice, so the board records which merged work covered which outcomes, and it closes whole when the last slice's merge finds every outcome ticked.
75
+ A sliced task is the one allowed exception: a task the operator decided to ship across several pull requests on purpose, one slice each. Its `Pull request:` line lists every slice, so the board records which merged work covered which outcomes, and it closes whole when the last slice's merge finds every outcome ticked and no slice left pending.
76
76
 
77
77
  Prefix the H1 with the `vX.Y:` phase label, then a short title whose form depends on the task type:
78
78
 
@@ -134,7 +134,7 @@ One plan per task. A plan cited by two tasks is a misfile rather than a shape to
134
134
 
135
135
  `Pull request:` records which pull requests carry the task's work, each as a bare `#NNN` the way `Issue:` does. It lists every pull request that shipped part of the task, oldest first and separated by commas, as in `Pull request: #NNN, #NNN`, and a task shipped whole lists one. It is not an origin, so a task without one is well-formed. `git-pr` appends to it when a pull request opens.
136
136
 
137
- The line is what lets a merge close its own task. `canon tasks archive --pull-request <n>` finds the task by any number the line lists and archives it only on the last. One pull request, one task: two tasks naming the same number refuse to archive rather than both moving.
137
+ The line is what lets a merge close its own task. `canon tasks archive --pull-request <n>` finds the task by any number the line lists and archives it only on the last. One pull request, one task: two tasks naming the same number refuse to archive rather than both moving. `Pending branch: <branch>` holds that merge back while a branch that ticked an outcome waits on its own number, since `canon tasks outcome` writes it and `canon tasks pull-request` clears it.
138
138
 
139
139
  ## What goes in
140
140
 
package/standards/wiki.md CHANGED
@@ -37,7 +37,8 @@ A page failing either is non-conforming even when it satisfies every shape rule
37
37
 
38
38
  ## Naming
39
39
 
40
- - Name a page with a `claude-` prefix before the kebab subject.
40
+ - Name a page by its kebab subject alone. The vendor folder it sits in already names the vendor, so a prefix repeats the folder.
41
+ - The `codebase-layout` skill carries the same rule for every folder in a project, beyond the wiki.
41
42
 
42
43
  ## Sourcing
43
44
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  The base layer covers every project the toolkit scaffolds, whatever language sits on top. It ships formatting, spelling, shell linting, conventional commits, git hooks, CI, and one maintenance script. Every other stack extends it, so a decision made here is one every stack inherits.
6
6
 
7
- A repository with several language roots layers this stack once at the root and skips it per subtree, since git honors only one `core.hooksPath` and a re-dropped husky silently breaks. `canon docs target-scenarios` carries the recipe.
7
+ A repository with several language roots layers this stack once at the root and skips it per subtree, since git honors only one `core.hooksPath` and a re-dropped husky silently breaks. `canon docs scenarios` carries the recipe.
8
8
 
9
9
  ## What ships as golden configs
10
10
 
@@ -97,4 +97,4 @@ Sticky negative knowledge. Do not relearn.
97
97
 
98
98
  `bun run check` repairs a checkout rather than gating one. It runs `format` first to auto-fix drifted code, then asserts only what the formatters could not fix. The pull request workflow calls `check:format`, `check:spell`, and `check:shell` directly, and those three are the gate.
99
99
 
100
- `scripts/verify.sh`'s `Markdown bans` stage is the exception: it gates rather than repairs. It runs `canon markdown audit` over every tracked markdown file except `CHANGELOG.md`, since a generated changelog carries commit subjects nobody wrote against the ban set, and exits `2` on a finding or `3` on a shipped-empty ban set, failing `bun run check` and the `pre-push` hook that calls it. A refusal to measure exits `1` and is logged as a skip rather than a failure, as does a corpus with no file left to check once `CHANGELOG.md` is set aside. The stage needs `canon` on PATH. `canon docs target-projects` names the install command. Without it, the stage is skipped and logged, and coverage falls back to the three checks above.
100
+ `scripts/verify.sh`'s `Markdown bans` stage is the exception: it gates rather than repairs. It runs `canon markdown audit` over every tracked markdown file except `CHANGELOG.md`, since a generated changelog carries commit subjects nobody wrote against the ban set, and exits `2` on a finding or `3` on a shipped-empty ban set, failing `bun run check` and the `pre-push` hook that calls it. A refusal to measure exits `1` and is logged as a skip rather than a failure, as does a corpus with no file left to check once `CHANGELOG.md` is set aside. The stage needs `canon` on PATH. `canon docs projects` names the install command. Without it, the stage is skipped and logged, and coverage falls back to the three checks above.
@@ -82,7 +82,7 @@ The first entry covers the whole record root, so a record folder added later nee
82
82
  ## CLAUDE.md
83
83
 
84
84
  - Ships as a seed with placeholder project name, description, and key paths. Fill these in after init.
85
- - Section layout: `Context`, `Indexes`, `Markdown`, `Key paths`, `Spelling`, `Snippets`, `Tasks`, `Memory`. Each section groups rules that share a surface so skills can detect, extend, or skip by heading.
85
+ - Section layout: `Context`, `Indexes`, `Markdown`, `Key paths`, `Spelling`, `Tasks`, `Memory`. Each section groups rules that share a surface so skills can detect, extend, or skip by heading.
86
86
  - The memory rules favor signal over volume. Saving only after a second occurrence or an explicit user correction keeps entries focused on patterns worth preventing, not first-occurrence slips. The 3-line cap stops the built-in auto-memory from drifting into narrative writeups that bury the rule underneath the recovery story.
87
87
  - The `Context` section points at `.claude/` state docs one line. `plan-feature` does the parallel read when invoked. The seed intentionally does not re-list every file.
88
88
 
@@ -1,39 +0,0 @@
1
- ---
2
- name: create-snippet
3
- description: Why a snippet needs its shape read from the reference and its write surface resolved before anything is drafted
4
- ---
5
-
6
- # Create snippet requirement
7
-
8
- ## Gap
9
-
10
- Without this skill, a snippet is drafted the way any other markdown file is drafted, so it arrives carrying frontmatter, headings, and a fill-in placeholder. None of the invocation channels strip those. Typing `>slug` in a chat inserts the scaffolding verbatim, and the file that was meant to be one instruction reads as a document.
11
-
12
- The write surface is the second failure. The toolkit authors at `snippets/` and a target project may still hold a stale `.claude/snippets/` copy from before that domain's install channel retired, so a session picks whichever folder it noticed first. A toolkit snippet written into the consumed copy fails the drift assertion that regenerates that folder. A snippet written there in a target project is correct and looks identical, which is why the surface has to be resolved from what the project has rather than guessed.
13
-
14
- Inside the project surface, a third failure sits one level deeper. A project-authored snippet sharing a name with a toolkit one used to be indistinguishable from it, back when `canon snippets sync` walked that folder and could not tell which side owned the file. That verb retired with the domain's install channel, so nothing reads ownership by location there any more, but the `project/` subfolder still separates the two by eye, which is worth keeping even with no sync left to protect from.
15
-
16
- ## Must
17
-
18
- - Read the bundled snippet reference before drafting, since the shape rules are what the invocation channels depend on
19
- - Resolve the write surface from which snippet folder the project has, rather than from whichever one the session noticed first
20
- - On the project surface, write under its `project/` subfolder, keeping a project-authored file visually apart from a toolkit-installed one
21
- - Confirm the slug and the full body with the user before writing
22
- - Emit the written path in full, so the terminal can resolve it
23
- - State what the resolved surface implies after writing, since a root file needs the consumed copy regenerated and a project-local one needs copying to the toolkit to ship
24
-
25
- ## Must not
26
-
27
- - Restate the snippet authoring conventions in the body, which the bundled reference owns and which would drift from it
28
- - Write a standard, which answers to a different authoring contract
29
-
30
- ## Guards
31
-
32
- - Neither snippet folder exists: stop, since there is nowhere for the snippet to live
33
-
34
- ## Out of scope
35
-
36
- - Editing a snippet that already exists
37
- - A standard, which `create-standard` owns
38
- - Installing and syncing the snippets the toolkit ships, which the `canon snippets` commands own
39
- - What a snippet is and how it is invoked, which the bundled snippet reference states
@@ -1,30 +0,0 @@
1
- ---
2
- name: create-snippet
3
- description: Creates a new snippet file in `snippets/` or `.claude/snippets/`. Use when asked to "create a snippet", add a snippet, write a reusable prompt, or make a new snippet. Do NOT use to edit an existing snippet.
4
- ---
5
-
6
- # Create snippet
7
-
8
- Creates one snippet file. Read these files in parallel:
9
-
10
- - `${CLAUDE_SKILL_DIR}/../../standards/markdown.md`: punctuation and formatting for all generated text
11
- - The `write-human` skill: voice, rhythm, and sentence construction for all generated text
12
- - `${CLAUDE_SKILL_DIR}/../../standards/snippets.md`: authoring conventions, invocation channels, use patterns
13
-
14
- ## Guards
15
-
16
- - If neither `snippets/` nor `.claude/snippets/` exists, stop: `❌ No snippets/ or .claude/snippets/ directory found.`
17
-
18
- ## Steps
19
-
20
- 1. Resolve the write surface: `snippets/` at the root if present, which is the toolkit's own authoring source. Otherwise `.claude/snippets/`, a stale copy a target project holds from before its install channel retired.
21
- 2. Draft the content from the user's description. The snippet reference governs structure, invocation, and authoring conventions.
22
- 3. Confirm the slug and full content with the user before writing
23
- 4. Write the file. On the root surface: `<surface>/<category>/<slug>.md`, or `<surface>/<slug>.md` when the snippet takes no category. On the `.claude/` surface: the same shape nested one level deeper under `project/`, a convention kept for its own sake now that nothing syncs that folder.
24
-
25
- ## After writing
26
-
27
- Emit the full path on its own line.
28
-
29
- - Root surface: this is the toolkit's authoring source. Remind the user to run `bun run check` to regenerate the consumed copy under `.claude/`.
30
- - `.claude/` surface: the file is project-local, under `.claude/snippets/project/`. Remind the user to copy it to the toolkit repo, under `snippets/<category>/<name>.md`, if it should ship to every project.
@@ -1,156 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -e
3
- set -o pipefail
4
-
5
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
- PROJECT_ROOT="${PROJECT_ROOT:-$(dirname "$(dirname "$SCRIPT_DIR")")}"
7
-
8
- source "$PROJECT_ROOT/scripts/lib/ui.sh"
9
- trap close_timeline EXIT
10
-
11
- SNIPPETS_SOURCE="$PROJECT_ROOT/snippets"
12
-
13
- show_help() {
14
- echo -e "${GREY}┌${NC}"
15
- echo -e "${GREY}├${NC} ${WHITE}Usage:${NC} canon snippets create"
16
- echo -e "${GREY}│${NC}"
17
- echo -e "${GREY}│${NC} Creates a new snippet: prompts for category and name, writes the file."
18
- echo -e "${GREY}│${NC}"
19
- echo -e "${GREY}│${NC} ${WHITE}Options:${NC}"
20
- echo -e "${GREY}│${NC} -h, --help ${GREY}# Show this help message${NC}"
21
- echo -e "${GREY}└${NC}"
22
- exit 0
23
- }
24
-
25
- list_categories() {
26
- echo "base (root, no prefix)"
27
- find "$SNIPPETS_SOURCE" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort
28
- echo "new category"
29
- }
30
-
31
- slug_error() {
32
- local slug="$1"
33
- local path="$2"
34
- if [ -z "$slug" ]; then
35
- echo "name cannot be empty"
36
- return
37
- fi
38
- if [[ ! "$slug" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
39
- echo "kebab-case only (e.g. my-snippet)"
40
- return
41
- fi
42
- if [ -f "$path" ]; then
43
- echo "file already exists: $path"
44
- return
45
- fi
46
- echo ""
47
- }
48
-
49
- ask_new_category() {
50
- while true; do
51
- ask "Category name?" "CATEGORY_INPUT"
52
- if [ -z "$CATEGORY_INPUT" ]; then
53
- log_warn "Category name cannot be empty"
54
- continue
55
- fi
56
- if [[ ! "$CATEGORY_INPUT" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
57
- log_warn "Invalid category: kebab-case only (e.g. my-category)"
58
- continue
59
- fi
60
- if [ -d "$SNIPPETS_SOURCE/$CATEGORY_INPUT" ]; then
61
- log_warn "Category '$CATEGORY_INPUT' already exists"
62
- continue
63
- fi
64
- CATEGORY_NAME="$CATEGORY_INPUT"
65
- break
66
- done
67
- }
68
-
69
- ask_name() {
70
- local category="$1"
71
- local is_base="$2"
72
- local label
73
- local error
74
-
75
- label="Snippet name?"
76
-
77
- while true; do
78
- ask "$label" "NAME_INPUT"
79
- local dest_path
80
- if [ "$is_base" = "true" ]; then
81
- dest_path="$SNIPPETS_SOURCE/$NAME_INPUT.md"
82
- else
83
- dest_path="$SNIPPETS_SOURCE/$category/$NAME_INPUT.md"
84
- fi
85
- error=$(slug_error "$NAME_INPUT" "$dest_path")
86
- if [ -z "$error" ]; then
87
- NAME="$NAME_INPUT"
88
- break
89
- fi
90
- log_warn "Invalid name: $error"
91
- done
92
- }
93
-
94
- cmd_create() {
95
- local categories=()
96
- mapfile -t categories < <(list_categories)
97
-
98
- select_option "Category?" "${categories[@]}"
99
- local selected="$SELECTED_OPTION"
100
-
101
- local category
102
- local is_base="false"
103
-
104
- if [ "$selected" = "base (root, no prefix)" ]; then
105
- is_base="true"
106
- category="base"
107
- elif [ "$selected" = "new category" ]; then
108
- ask_new_category
109
- category="$CATEGORY_NAME"
110
- else
111
- category="$selected"
112
- fi
113
-
114
- ask_name "$category" "$is_base"
115
- local name="$NAME"
116
-
117
- local dest_path
118
- local install_path
119
- if [ "$is_base" = "true" ]; then
120
- dest_path="$SNIPPETS_SOURCE/$name.md"
121
- install_path="snippets/$name"
122
- else
123
- dest_path="$SNIPPETS_SOURCE/$category/$name.md"
124
- install_path="snippets/$category/$name"
125
- fi
126
-
127
- log_info "Installs as: @${install_path}"
128
-
129
- log_step "Creating file"
130
-
131
- if [ "$is_base" = "false" ]; then
132
- mkdir -p "$SNIPPETS_SOURCE/$category"
133
- fi
134
-
135
- printf '<!-- TODO: write %s prompt -->\n' "$slug" >"$dest_path"
136
-
137
- if [ "$is_base" = "true" ]; then
138
- log_add "snippets/$name.md"
139
- else
140
- log_add "snippets/$category/$name.md"
141
- fi
142
- }
143
-
144
- main() {
145
- if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
146
- show_help
147
- fi
148
-
149
- cmd_create
150
-
151
- trap - EXIT
152
- echo -e "${GREY}└${NC}\n"
153
- echo -e "${GREEN}✓ Snippet created${NC}"
154
- }
155
-
156
- main "$@"
package/snippets/align.md DELETED
@@ -1,12 +0,0 @@
1
- Check alignment before acting, not after. State three lines, then proceed.
2
-
3
- - Outcome: what will be true when this is done
4
- - Scope: what this deliberately does not touch
5
- - Assumption: the one inference that would change the work if it is wrong
6
-
7
- Act without waiting, unless being wrong about the assumption wastes the work rather than costing an edit.
8
-
9
- - Three lines, not three paragraphs
10
- - Name the inference you are least sure of, not the safest one
11
- - Do not ask what the request already answers
12
- - Silence is agreement
@@ -1,39 +0,0 @@
1
- Produce a decision memo for a research or should-we question. This is the decision-heavy counterpart to `plan-feature`, which plans a build. Write one file per independent concern under `.canon/plans/feature-<slug>.md`, or answer inline when the plan is small. Use the same plans folder as `plan-feature`.
2
-
3
- Read the current state from the code and docs before recommending. Do not assume it.
4
-
5
- Output this shape per memo:
6
-
7
- ```markdown
8
- # <slug title>
9
-
10
- <the question in one or two lines>
11
-
12
- ## Summary
13
-
14
- - <goal in one line>
15
- - <deliverables in one line>
16
- - <key tradeoff in one line>
17
-
18
- ## Finding
19
-
20
- <what the current state actually is, from reading the code and docs>
21
-
22
- ## Item A: <decision>
23
-
24
- **Current state.** <what exists today>
25
-
26
- **Recommendation.** <the pick, with the tradeoff in one sentence>
27
-
28
- **Options.** <the real alternatives, only when the choice is the user's to make>
29
-
30
- **Feasibility.** <what is not buildable as asked and why, omit when all options are buildable>
31
-
32
- **Questions.**
33
-
34
- 1. <question>
35
- - Suggested: <best-practice default derived from context>
36
- - Answer:
37
- ```
38
-
39
- Repeat the `## Item` block per decision. Keep chat to the file pointer plus the questions. Recommend, do not enumerate, unless the user's preference is the deciding factor.