@erclx/canon 5.39.0 → 5.41.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 (162) hide show
  1. package/README.md +39 -22
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/auto-ship/REQUIREMENT.md +1 -0
  4. package/claude/skills/auto-ship/SKILL.md +6 -0
  5. package/claude/skills/auto-ship/references/fan-in.md +59 -0
  6. package/claude/skills/canvas-board/SKILL.md +1 -1
  7. package/claude/skills/create-skill/references/skill.md +1 -1
  8. package/claude/skills/decision-escalate/REQUIREMENT.md +1 -1
  9. package/claude/skills/decision-escalate/SKILL.md +1 -2
  10. package/claude/skills/draft-slides/SKILL.md +2 -3
  11. package/claude/skills/draft-slides/references/deck.md +183 -0
  12. package/claude/skills/git-branch/REQUIREMENT.md +17 -6
  13. package/claude/skills/git-branch/SKILL.md +63 -7
  14. package/claude/skills/git-pr/references/evidence.md +2 -2
  15. package/claude/skills/git-pr/references/final-command.md +1 -1
  16. package/claude/skills/git-pr/references/labels.md +1 -1
  17. package/claude/skills/git-pr/references/publish.md +1 -1
  18. package/claude/skills/plan-feature/references/batches.md +48 -1
  19. package/claude/skills/plan-feature/references/plan.md +2 -0
  20. package/claude/skills/plan-intake-answer/REQUIREMENT.md +1 -1
  21. package/claude/skills/plan-intake-answer/SKILL.md +1 -2
  22. package/claude/skills/review-address/references/rebase-conflicts.md +7 -0
  23. package/claude/skills/review-ui/SKILL.md +3 -0
  24. package/claude/skills/review-ui/references/close-out.md +47 -0
  25. package/claude/skills/review-ui/references/post.md +3 -1
  26. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +1 -0
  27. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
  28. package/claude/skills/role-worker/REQUIREMENT.md +1 -0
  29. package/claude/skills/role-worker/SKILL.md +9 -0
  30. package/claude/skills/task-board/references/board.md +1 -1
  31. package/claude/skills/task-board/references/tasks.md +1 -1
  32. package/claude/skills/teach-workspace/REQUIREMENT.md +1 -1
  33. package/claude/skills/teach-workspace/SKILL.md +1 -2
  34. package/claude/skills/ux-measure/SKILL.md +1 -1
  35. package/docs/agents/audits.md +61 -60
  36. package/docs/agents/context-audit.md +40 -58
  37. package/docs/agents/index.md +5 -51
  38. package/docs/agents/install-and-sync.md +1 -0
  39. package/docs/agents/markdown-audit.md +53 -99
  40. package/docs/agents/records.md +37 -79
  41. package/docs/agents/sessions-transfer.md +26 -70
  42. package/docs/target/index.md +0 -1
  43. package/docs/target/migrations.md +57 -66
  44. package/docs/target/projects.md +36 -78
  45. package/docs/target/scenarios.md +9 -19
  46. package/docs/target/sync.md +27 -68
  47. package/docs/workflow/ai-workflow.md +22 -68
  48. package/docs/workflow/operating-model.md +32 -186
  49. package/docs/workflow/parallel-features.md +30 -66
  50. package/docs/workflow/skill-map-on-request.md +22 -24
  51. package/docs/workflow/skill-map.md +72 -76
  52. package/docs/workflow/visual-design-workflow.md +25 -118
  53. package/docs/workflow/zshrc-aliases.md +18 -37
  54. package/package.json +1 -1
  55. package/src/canvas/client/inspector/fields.css +4 -0
  56. package/src/canvas/index.html +6 -0
  57. package/src/claude/cases/git.ts +4 -0
  58. package/src/claude/skills-reach.ts +2 -2
  59. package/src/cli/ui.ts +3 -1
  60. package/src/commands/canvas.ts +23 -0
  61. package/src/commands/capture.ts +14 -0
  62. package/src/commands/claude.ts +13 -0
  63. package/src/commands/demo.ts +22 -0
  64. package/src/commands/deps.ts +6 -0
  65. package/src/commands/driver.ts +28 -2
  66. package/src/commands/gov.ts +52 -18
  67. package/src/commands/intake.ts +17 -0
  68. package/src/commands/labels.ts +38 -5
  69. package/src/commands/pr.ts +192 -52
  70. package/src/commands/records.ts +19 -3
  71. package/src/commands/secrets.ts +6 -0
  72. package/src/commands/sessions.ts +8 -0
  73. package/src/commands/tasks.ts +52 -0
  74. package/src/commands/teach.ts +13 -0
  75. package/src/commands/upstream.ts +25 -1
  76. package/src/commands/worktrees.ts +5 -0
  77. package/src/context/audit.ts +22 -4
  78. package/src/context/folders.ts +1 -1
  79. package/src/context/shape-gated.ts +5 -2
  80. package/src/counts/scan.ts +1 -1
  81. package/src/design/components.ts +1 -1
  82. package/src/design/document.ts +1 -1
  83. package/src/design/tokens.ts +1 -1
  84. package/src/gate/boundaries.ts +4 -1
  85. package/src/gate/measures.ts +1 -1
  86. package/src/gate/stages.ts +7 -7
  87. package/src/git/env.ts +6 -0
  88. package/src/markdown/scope.ts +11 -19
  89. package/src/pr/frames.ts +53 -9
  90. package/src/pr/lift.ts +153 -19
  91. package/src/pr/painted.ts +87 -0
  92. package/src/records/validate.ts +1 -0
  93. package/src/roots/record.ts +18 -1
  94. package/src/sandbox/census.ts +1 -1
  95. package/src/sandbox/headless/run.ts +3 -2
  96. package/src/sandbox/headless/watch.ts +3 -4
  97. package/src/slides/convert/master.ts +3 -0
  98. package/src/sync/format.ts +46 -0
  99. package/src/sync/stamp.ts +6 -0
  100. package/src/tasks/answers.ts +79 -2
  101. package/src/tasks/fan-in.ts +176 -0
  102. package/src/tasks/stack.ts +1 -1
  103. package/tooling/astro/reference.md +80 -39
  104. package/tooling/base/reference.md +63 -32
  105. package/tooling/base/seeds/canon/context/ci.md +7 -15
  106. package/tooling/base/seeds/canon/context/development.md +14 -14
  107. package/tooling/claude/reference.md +40 -22
  108. package/tooling/cloudflare/reference.md +31 -12
  109. package/tooling/go/reference.md +47 -23
  110. package/tooling/nextjs/reference.md +52 -19
  111. package/tooling/php/reference.md +35 -16
  112. package/tooling/python/reference.md +47 -30
  113. package/tooling/shell/reference.md +6 -5
  114. package/tooling/vite-react/reference.md +49 -21
  115. package/tooling/web/reference.md +131 -71
  116. package/docs/agents/canvas.md +0 -86
  117. package/docs/agents/capture.md +0 -65
  118. package/docs/agents/census.md +0 -23
  119. package/docs/agents/comments.md +0 -34
  120. package/docs/agents/context-audit-checks.md +0 -108
  121. package/docs/agents/context-classify.md +0 -101
  122. package/docs/agents/counts.md +0 -74
  123. package/docs/agents/demo.md +0 -140
  124. package/docs/agents/driver.md +0 -103
  125. package/docs/agents/gate.md +0 -90
  126. package/docs/agents/hooks.md +0 -78
  127. package/docs/agents/intake.md +0 -81
  128. package/docs/agents/key-changes.md +0 -116
  129. package/docs/agents/label-coverage.md +0 -86
  130. package/docs/agents/markdown-audit-density.md +0 -39
  131. package/docs/agents/markdown-audit-weight.md +0 -83
  132. package/docs/agents/pr-evidence-layout.md +0 -70
  133. package/docs/agents/pr-evidence.md +0 -273
  134. package/docs/agents/pr-frames.md +0 -131
  135. package/docs/agents/pr-lift.md +0 -132
  136. package/docs/agents/pr-local.md +0 -111
  137. package/docs/agents/pr-preview.md +0 -155
  138. package/docs/agents/pr-reads.md +0 -111
  139. package/docs/agents/pr-tick.md +0 -72
  140. package/docs/agents/records-push.md +0 -61
  141. package/docs/agents/records-stale.md +0 -40
  142. package/docs/agents/records-validate.md +0 -57
  143. package/docs/agents/restated.md +0 -83
  144. package/docs/agents/routing.md +0 -61
  145. package/docs/agents/rule-citations.md +0 -90
  146. package/docs/agents/sandbox.md +0 -126
  147. package/docs/agents/sessions.md +0 -128
  148. package/docs/agents/skills-audit.md +0 -120
  149. package/docs/agents/skills-reach.md +0 -64
  150. package/docs/agents/slides.md +0 -176
  151. package/docs/agents/state-scoped-risk.md +0 -105
  152. package/docs/agents/superseded.md +0 -85
  153. package/docs/agents/tasks-archive.md +0 -83
  154. package/docs/agents/tasks-plans.md +0 -152
  155. package/docs/agents/tasks-validate.md +0 -133
  156. package/docs/agents/tasks.md +0 -123
  157. package/docs/agents/teach-authoring.md +0 -118
  158. package/docs/agents/teach.md +0 -157
  159. package/docs/agents/test-order.md +0 -58
  160. package/docs/agents/upstream.md +0 -94
  161. package/docs/agents/worktrees.md +0 -118
  162. package/docs/target/standards-moved.md +0 -34
@@ -103,6 +103,14 @@ export function register(program: Command): void {
103
103
  'Writes nothing. The cursor moves only through `upstream advance`, so a',
104
104
  'run that dies midway reads the same range next time.',
105
105
  '',
106
+ 'Reasons:',
107
+ ' no-cursor no cursor is stored and no --since was given',
108
+ ' bad-since --since is not a dotted version',
109
+ ' cursor-missing every release the feed returned is newer than the cursor',
110
+ ' rate-limited GitHub refused the request, so set GH_TOKEN or GITHUB_TOKEN',
111
+ ' network the feed could not be reached',
112
+ ' http GitHub answered with an error status',
113
+ '',
106
114
  'Exit codes:',
107
115
  ' 0 the range was read, possibly empty',
108
116
  ' 1 refused, with the reason on stderr or in the JSON record',
@@ -154,7 +162,19 @@ export function register(program: Command): void {
154
162
  'Due when the cursor is a week old and the installed version is ahead of',
155
163
  'it, or sooner when an Added line in the gap names a canon surface. The',
156
164
  'gap is read from GitHub at most once a day and cached under the scratch',
157
- 'folder, and a failed read is held for the day as well.',
165
+ 'folder, and a failed read is held for the day as well. The record',
166
+ 'carries due, reason, releases, installed, and cursor, and releases is',
167
+ 'null when the feed was not read.',
168
+ '',
169
+ 'Reasons:',
170
+ ' Due, by reason:',
171
+ ' no-cursor no digest has been run',
172
+ ' week the cursor is seven or more days old and the install is ahead',
173
+ ' vocabulary an Added line in the gap names a plugin, skill, hook, or worktree',
174
+ ' Not due, by reason:',
175
+ ' current the install is at or below the cursor, as after a downgrade',
176
+ ' recent the cursor is under a week old and no line in the gap matches',
177
+ ' unknown-version claude --version could not be read',
158
178
  '',
159
179
  'Exit codes:',
160
180
  ' 0 answered, whether or not a digest is due',
@@ -185,6 +205,10 @@ export function register(program: Command): void {
185
205
  'folder through a temp file and a rename. It refuses a version older than',
186
206
  'the stored cursor.',
187
207
  '',
208
+ 'Reasons:',
209
+ ' invalid-version the version is not a dotted number',
210
+ ' older-than-cursor the version is older than the stored cursor',
211
+ '',
188
212
  'Exit codes:',
189
213
  ' 0 the cursor moved',
190
214
  ' 1 refused, with the reason on stderr or in the JSON record',
@@ -119,6 +119,11 @@ const CONDITIONS = [
119
119
  'command is running in, since git removes the directory a caller stands',
120
120
  'in and every later call scoped to it then fails.',
121
121
  '',
122
+ 'A "refusals" entry is one of these reasons:',
123
+ ...Object.entries(REFUSALS).map(
124
+ ([reason, meaning]) => ` ${reason.padEnd(24)}${meaning}`,
125
+ ),
126
+ '',
122
127
  'An unreadable input refuses the whole reading rather than reporting',
123
128
  'every worktree as not reclaimable, since an absent merge state and a',
124
129
  'branch with no merged pull request produce the same empty answer.',
@@ -104,6 +104,19 @@ export const LAYOUT_PARAGRAPH_CAP = 1
104
104
  /** The one Layout H3 not titled with a path, holding the trivial folders. */
105
105
  export const OTHER_FOLDERS = 'Other folders'
106
106
 
107
+ /** The run-command table H3 only the development entry carries under `Layout`. */
108
+ export const SCRIPTS_HEADING = 'Scripts'
109
+
110
+ /**
111
+ * Whether an entry path is the development entry, being the flat entry, the
112
+ * overview of a split one a target may still carry, or this repository's
113
+ * `local-loop` entry. The standard exempts the
114
+ * `Scripts` H3 for that entry alone, so the path is what tells.
115
+ */
116
+ function isDevelopmentEntry(rel: string): boolean {
117
+ return /(?:^|\/)development(?:\/(?:overview|local-loop))?\.md$/.test(rel)
118
+ }
119
+
107
120
  /**
108
121
  * The context standard's list, kept as its own export because the audit record
109
122
  * has published it flat under `checkpoints.requiredSections` since before any
@@ -584,13 +597,18 @@ interface FolderHeading {
584
597
  * A cap reports once per section or folder heading however far past it the
585
598
  * count runs, since the fix is one cut either way. `Other folders` is exempt
586
599
  * from both folder caps, its one-line bullets being held by the bullet run
587
- * limit `canon markdown audit` already gates.
600
+ * limit `canon markdown audit` already gates. The development entry's `Scripts`
601
+ * H3 is exempt from the heading rule and both caps the same way, its table
602
+ * being a run-command surface rather than a folder.
588
603
  */
589
604
  function shapeIssues(
605
+ rel: string,
590
606
  lines: readonly BodyLine[],
591
607
  retired: readonly string[],
592
608
  ): ShapeIssue[] {
593
609
  const issues: ShapeIssue[] = []
610
+ const exempt = new Set([OTHER_FOLDERS])
611
+ if (isDevelopmentEntry(rel)) exempt.add(SCRIPTS_HEADING)
594
612
  let section: string | undefined
595
613
  let gotchas: { line: number; bullets: number } | undefined
596
614
  let folder: FolderHeading | undefined
@@ -599,7 +617,7 @@ function shapeIssues(
599
617
  let afterBlank = false
600
618
 
601
619
  const closeFolder = (): void => {
602
- if (folder && folder.text !== OTHER_FOLDERS) {
620
+ if (folder && !exempt.has(folder.text)) {
603
621
  if (folder.paragraphs > LAYOUT_PARAGRAPH_CAP) {
604
622
  issues.push({
605
623
  line: folder.line,
@@ -659,7 +677,7 @@ function shapeIssues(
659
677
  } else if (section === 'Layout' && level === 3) {
660
678
  closeFolder()
661
679
  folder = { line: line.number, text, bullets: 0, paragraphs: 0 }
662
- if (!FOLDER_TITLE.test(text) && text !== OTHER_FOLDERS) {
680
+ if (!FOLDER_TITLE.test(text) && !exempt.has(text)) {
663
681
  issues.push({ line: line.number, kind: 'layout-heading', text })
664
682
  }
665
683
  } else if (section === 'Layout') {
@@ -780,7 +798,7 @@ export function measureEntry(
780
798
  bareReferences: bareReferences(lines, siblings),
781
799
  sections: declaredSections(lines, required),
782
800
  otherSections: otherSections(lines, allowed, retired),
783
- shape: governsContent ? shapeIssues(lines, retired) : [],
801
+ shape: governsContent ? shapeIssues(rel, lines, retired) : [],
784
802
  stub: isStubSeed(source),
785
803
  governed: governsContent,
786
804
  }
@@ -56,7 +56,7 @@ export interface AuditedFolder {
56
56
  * The requested folder name this was resolved under, which is what says
57
57
  * which standard governs the entries. A nested split folder carries the name
58
58
  * of the folder it sits beneath rather than its own, so
59
- * `canon/context/claude-plugin` is governed as `context`.
59
+ * `canon/context/<domain>` is governed as `context`.
60
60
  */
61
61
  readonly name: string
62
62
  /**
@@ -15,8 +15,11 @@
15
15
  */
16
16
  export const SHAPE_GATED_FOLDERS: readonly string[] = [
17
17
  'canon/context/context-model.md',
18
- 'canon/context/design.md',
19
- 'canon/context/web.md',
18
+ 'canon/context/development/',
19
+ 'canon/context/features/',
20
+ 'canon/context/surfaces/',
21
+ 'canon/context/workflow/',
22
+ 'tooling/base/seeds/',
20
23
  ]
21
24
 
22
25
  /** Whether a repository-relative entry path sits under a gated prefix. */
@@ -80,7 +80,7 @@ function sentencesOf(line: string): string[] {
80
80
  * the 290 showed removing all of them while keeping that one.
81
81
  *
82
82
  * `authors` joined the list on a corpus measurement rather than on the shape
83
- * argument above. `canon/context/development/regeneration.md` states `the
83
+ * argument above. A context entry asserting `the
84
84
  * toolkit authors 69 rules under governance/rules/`, which is the assertion
85
85
  * shape this list was built for with a verb the list did not carry, and the
86
86
  * word costs one further sentence across the whole corpus and no finding.
@@ -250,7 +250,7 @@ const TEACH_CHROME: Component = {
250
250
  'navigation shared by every teach page. Recovered from two gitignored',
251
251
  'course.css files that predate a regression that dropped this layer from the',
252
252
  "generator, and rewritten onto this module's tokens rather than the",
253
- 'incompatible palette they carried. See canon/context/features/teach/chrome.md',
253
+ 'incompatible palette they carried. See canon/context/features/teach.md',
254
254
  'for the shape.',
255
255
  ].join('\n '),
256
256
  reads: [
@@ -95,7 +95,7 @@ function borderRows(tokens: readonly BorderToken[]): string[][] {
95
95
  */
96
96
  const LEAD = [
97
97
  'Authoring guidance: `claude/skills/canvas-board/references/design.md`. Rendered from `src/design/tokens.ts` by `canon design regen`, so edit the module and never this file.',
98
- 'Why each value was chosen lives in `canon/context/design.md`.',
98
+ 'Why each value was chosen lives in `canon/context/features/design.md`.',
99
99
  ].join(' ')
100
100
 
101
101
  function bullets(rules: readonly string[]): string {
@@ -59,7 +59,7 @@ export interface BorderToken {
59
59
  /**
60
60
  * Every prose slot is a list of one-line rules rather than a string, so a
61
61
  * bullet is the unit the renderer emits and the prose budget counts, and a
62
- * value needing a paragraph of why goes to `canon/context/design.md`.
62
+ * value needing a paragraph of why goes to `canon/context/features/design.md`.
63
63
  */
64
64
  export interface DesignTokens {
65
65
  readonly personality: string
@@ -211,7 +211,7 @@ function collectCommands(value: unknown, into: string[] = []): string[] {
211
211
  /**
212
212
  * A hook, a workflow, or a husky script reaching one side of the seed or config
213
213
  * boundary and not the other is a capability withheld with no recorded reason,
214
- * per the criterion in `canon/context/tooling/seeds.md`.
214
+ * per the criterion in `canon/context/surfaces/tooling.md`.
215
215
  *
216
216
  * Presence on either side clears a name, and a `canon-no-seed:` comment in the
217
217
  * file is the recorded reason. The same marker on a destination clears a
@@ -434,6 +434,9 @@ function grepLines(
434
434
  * seed itself installs, so every other spelling reports. No seed root, or roots
435
435
  * carrying no markdown, is unmeasured rather than a pass, which agrees with
436
436
  * `seedStandards` on the no-root case.
437
+ *
438
+ * The token matches as a substring, so `canonical` reports as `canon` does. The
439
+ * walk covers the seed roots alone, so a shipped rule citing the CLI passes.
437
440
  */
438
441
  export const seedIndependence: Measure = async (ctx) => {
439
442
  if (!isDirectory(join(ctx.root, 'tooling'))) {
@@ -1401,7 +1401,7 @@ export const pluginManifests: Measure = async (ctx) => {
1401
1401
  return {
1402
1402
  emissions: [],
1403
1403
  unmeasured:
1404
- '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.',
1404
+ '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, then test the raised pin as `canon/context/development/ci.md` states.',
1405
1405
  }
1406
1406
  }
1407
1407
 
@@ -92,12 +92,12 @@ export interface Stage {
92
92
  }
93
93
 
94
94
  /**
95
- * Corpora a `src/` test asserts over from outside `src/`, censused in
96
- * `canon/context/development/tests.md`. This list and that census are
97
- * two copies of one set with nothing comparing them, so a corpus joining the
98
- * census joins this list in the same change. The first four are directory
99
- * prefixes because their tests walk the tree whole, which is what reaches a
100
- * rule or a skill a branch adds rather than edits.
95
+ * Corpora a `src/` test asserts over from outside `src/`. Nothing compares
96
+ * this list to the tests reading those paths, so a test that starts reading a
97
+ * new corpus joins it here in the same change, or a branch touching only that
98
+ * corpus skips the Tests stage. The first four are directory prefixes because
99
+ * their tests walk the tree whole, which is what reaches a rule or a skill a
100
+ * branch adds rather than edits.
101
101
  */
102
102
  export const TEST_CORPORA_PATTERNS = [
103
103
  '^claude/skills/',
@@ -302,7 +302,7 @@ export const STAGES: readonly Stage[] = [
302
302
  {
303
303
  // A hook, a workflow, or a husky script reaching one side of the seed or
304
304
  // config boundary and not the other is a capability withheld with no
305
- // recorded reason, per the criterion in canon/context/tooling/seeds.md.
305
+ // recorded reason, per the criterion in canon/context/surfaces/tooling.md.
306
306
  id: 'capability-seeding',
307
307
  label: 'Capability seeding',
308
308
  checks: [{ kind: 'measure', measure: capabilitySeeding }],
package/src/git/env.ts CHANGED
@@ -26,6 +26,12 @@ const RESOLUTION_VARS = [
26
26
  * Read per call rather than captured at module load. A long-lived process can
27
27
  * have these set after import, and a snapshot would then hand git the very
28
28
  * variables this exists to strip.
29
+ *
30
+ * Hooks export `GIT_DIR`, `GIT_WORK_TREE`, `GIT_INDEX_FILE`, and `GIT_PREFIX`,
31
+ * which beat `-C`. Under pre-push an inherited `GIT_DIR` made a `-C src` read
32
+ * resolve against the whole repository, and in a fixture it let a config write
33
+ * overwrite the real repository's committer identity, so route every shelled
34
+ * call through this, fixtures included.
29
35
  */
30
36
  export function gitEnv(): Record<string, string> {
31
37
  return Object.fromEntries(
@@ -35,16 +35,11 @@ const DENSITY_SCOPE = [
35
35
  * corpus, which is why the audit reads it only inside a canon checkout.
36
36
  */
37
37
  export const GATED_PATHS: readonly string[] = [
38
- 'canon/context/claude-plugin/',
39
- 'canon/context/claude-internal/',
38
+ 'canon/context/surfaces/',
39
+ 'canon/context/workflow/',
40
+ 'canon/context/development/',
40
41
  'canon/context/features/',
41
- 'canon/context/cli/',
42
- 'canon/context/sandbox/',
43
- 'canon/context/governance/',
44
- 'canon/context/tooling/',
45
- 'canon/context/web.md',
46
42
  'canon/context/context-model.md',
47
- 'canon/context/design.md',
48
43
  'canon/context/index.md',
49
44
  'claude/skills/api-design/SKILL.md',
50
45
  'claude/skills/auto-ship/SKILL.md',
@@ -76,6 +71,7 @@ export const GATED_PATHS: readonly string[] = [
76
71
  'claude/skills/draft-screencast/SKILL.md',
77
72
  'claude/skills/draft-screencast/references/beats-template.md',
78
73
  'claude/skills/draft-slides/SKILL.md',
74
+ 'claude/skills/draft-slides/references/deck.md',
79
75
  'claude/skills/git-branch/SKILL.md',
80
76
  'claude/skills/git-commit/SKILL.md',
81
77
  'claude/skills/git-followup/SKILL.md',
@@ -130,16 +126,12 @@ export const GATED_PATHS: readonly string[] = [
130
126
  'claude/skills/video-craft/SKILL.md',
131
127
  'claude/skills/write-human/SKILL.md',
132
128
  'claude/skills/youtube-transcripts/SKILL.md',
133
- 'docs/agents/indexes.md',
134
- 'docs/agents/install-and-sync.md',
135
- 'docs/agents/migrate.md',
136
- 'docs/agents/output-shape.md',
137
- 'docs/agents/overview.md',
138
- 'docs/agents/targets.md',
139
- 'canon/context/development/',
140
- 'canon/context/ci/',
141
- 'canon/context/scripts/',
142
129
  '.claude/skills/internal-',
130
+ 'docs/',
131
+ 'tooling/',
132
+ 'wiki/',
133
+ 'README.md',
134
+ 'CONTRIBUTING.md',
143
135
  ]
144
136
 
145
137
  /**
@@ -148,8 +140,8 @@ export const GATED_PATHS: readonly string[] = [
148
140
  * entries, the docs pages, and the skill bodies and references through a stage
149
141
  * of its own before this measure existed, and waiting on `GATED_PATHS` would
150
142
  * reopen that hole. The seeds, the stack references, the wiki, and the root
151
- * guides were never held to it, and seven runs in the stack references would
152
- * fail the push on the day this lands, so those wait for their batch.
143
+ * guides were never held to it, so they joined `GATED_PATHS` only once their
144
+ * rewrite brought them to zero.
153
145
  */
154
146
  const ALWAYS_GATED: readonly DensityKind[] = ['bullet-run']
155
147
 
package/src/pr/frames.ts CHANGED
@@ -19,6 +19,9 @@ const PASS_PATTERN = /^\d{8}T\d{6}Z$/
19
19
  /** How many times an add rebuilds on a tip another writer moved. */
20
20
  const ADD_ATTEMPTS = 3
21
21
 
22
+ /** One half of a reference address. It opens on a letter or digit and holds no separator, so `..` and `/` cannot survive it. */
23
+ const REFERENCE_PART = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
24
+
22
25
  const FOLDER_PATTERN = /^pr-(\d+)$/
23
26
 
24
27
  const DAY_MS = 86_400_000
@@ -77,7 +80,8 @@ export interface FrameInput {
77
80
  readonly number: number
78
81
  readonly head: string
79
82
  readonly pass: string
80
- readonly box: number
83
+ readonly box?: number
84
+ readonly reference?: string
81
85
  readonly theme?: FrameTheme
82
86
  readonly bytes: Uint8Array
83
87
  }
@@ -158,6 +162,39 @@ export function frameLink(repo: string, path: string): string {
158
162
  return `https://github.com/${repo}/blob/${FRAMES_BRANCH}/${path}?raw=true`
159
163
  }
160
164
 
165
+ /**
166
+ * The file-name slug of a `<page>/<frame>` address, or undefined when the
167
+ * address would not round-trip. Each half must open on a letter or digit and
168
+ * hold only letters, digits, dots, underscores, and dashes, and neither may
169
+ * carry `--`, so the joined slug reads back as the same two names and a path
170
+ * never gains a directory or escapes the pass folder.
171
+ */
172
+ export function referenceSlug(address: string): string | undefined {
173
+ const parts = address.split('/')
174
+ if (parts.length !== 2) return undefined
175
+ const [page, frame] = parts as [string, string]
176
+ const valid = [page, frame].every(
177
+ (part) => REFERENCE_PART.test(part) && !part.includes('--'),
178
+ )
179
+ return valid ? `${page}--${frame}` : undefined
180
+ }
181
+
182
+ /**
183
+ * Where a design reference capture lands, beside the evidence frames of one
184
+ * pass. The slug repeats for one reference, so a second capture in a pass
185
+ * overwrites only itself. Undefined when the address has no valid slug.
186
+ */
187
+ export function referencePath(
188
+ number: number,
189
+ head: string,
190
+ pass: string,
191
+ address: string,
192
+ ): string | undefined {
193
+ const slug = referenceSlug(address)
194
+ if (slug === undefined) return undefined
195
+ return `pr-${number}/${head.slice(0, SHORT_HEAD_LENGTH)}/${pass}/ref-${slug}.png`
196
+ }
197
+
161
198
  /** The frame's bytes, or undefined when the file is absent, empty, or not a PNG. */
162
199
  export async function readFrame(path: string): Promise<Uint8Array | undefined> {
163
200
  const bytes = await readFile(path).catch(() => undefined)
@@ -179,17 +216,24 @@ export async function pushFrame(
179
216
  api: FramesApi,
180
217
  input: FrameInput,
181
218
  ): Promise<PushResult> {
182
- const path = framePath(
183
- input.number,
184
- input.head,
185
- input.pass,
186
- input.box,
187
- input.theme,
188
- )
219
+ const path =
220
+ input.reference === undefined
221
+ ? framePath(
222
+ input.number,
223
+ input.head,
224
+ input.pass,
225
+ input.box ?? 0,
226
+ input.theme,
227
+ )
228
+ : referencePath(input.number, input.head, input.pass, input.reference)
229
+ if (path === undefined) return { kind: 'refused', reason: 'push-failed' }
189
230
  const blob = await api.createBlob(Buffer.from(input.bytes).toString('base64'))
190
231
  if (blob === undefined) return { kind: 'refused', reason: 'push-failed' }
191
232
  const entry: TreeEntry = { path, mode: '100644', type: 'blob', sha: blob }
192
- const message = `chore(frames): add box ${input.box} of #${input.number}`
233
+ const message =
234
+ input.reference === undefined
235
+ ? `chore(frames): add box ${input.box} of #${input.number}`
236
+ : `chore(frames): add reference ${input.reference} of #${input.number}`
193
237
 
194
238
  for (let attempt = 0; attempt < ADD_ATTEMPTS; attempt += 1) {
195
239
  const tip = await api.readTip()