@erclx/canon 5.13.0 → 5.14.1

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 (68) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/api-design/SKILL.md +3 -2
  3. package/claude/skills/deprecation-migration/REQUIREMENT.md +62 -0
  4. package/claude/skills/deprecation-migration/SKILL.md +115 -0
  5. package/claude/skills/deprecation-migration/references/adopted.md +50 -0
  6. package/claude/skills/git-followup/REQUIREMENT.md +1 -0
  7. package/claude/skills/git-followup/SKILL.md +3 -0
  8. package/claude/skills/git-pr/REQUIREMENT.md +1 -0
  9. package/claude/skills/git-pr/SKILL.md +1 -1
  10. package/claude/skills/git-pr/references/evidence.md +2 -2
  11. package/claude/skills/git-ship/REQUIREMENT.md +3 -0
  12. package/claude/skills/git-ship/SKILL.md +12 -1
  13. package/claude/skills/review-pr/SKILL.md +1 -1
  14. package/claude/skills/review-pr/references/close-out.md +1 -1
  15. package/claude/skills/review-pr/references/post.md +1 -1
  16. package/claude/skills/review-ui/REQUIREMENT.md +5 -2
  17. package/claude/skills/review-ui/SKILL.md +12 -4
  18. package/claude/skills/review-ui/references/driving.md +5 -3
  19. package/claude/skills/review-ui/references/post.md +3 -2
  20. package/claude/skills/role-orchestrator/SKILL.md +1 -1
  21. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +10 -10
  22. package/claude/skills/role-orchestrator/scripts/poll.ts +628 -0
  23. package/claude/skills/role-orchestrator/scripts/repo.ts +69 -0
  24. package/claude/skills/role-orchestrator/scripts/watch.ts +267 -0
  25. package/docs/agents/commands.md +1 -1
  26. package/docs/agents/index.md +1 -0
  27. package/docs/agents/markdown-audit.md +1 -1
  28. package/docs/agents/pr-evidence.md +5 -0
  29. package/docs/agents/pr-frames.md +112 -0
  30. package/docs/agents/pr-reads.md +1 -1
  31. package/docs/agents/sandbox.md +3 -3
  32. package/docs/agents/state-scoped-risk.md +2 -2
  33. package/docs/workflow/skill-map.md +15 -14
  34. package/package.json +4 -5
  35. package/scripts/core/check-color-source.sh +11 -1
  36. package/scripts/core/install-check.sh +13 -4
  37. package/scripts/lib/sandbox-dispatch.sh +1 -1
  38. package/scripts/lib/sandbox-fixtures.sh +2 -2
  39. package/scripts/lib/sandbox-path.sh +1 -1
  40. package/scripts/manage-sandbox.sh +2 -2
  41. package/src/claude/cases/misc.ts +5 -0
  42. package/src/claude/skills-audit.ts +1 -0
  43. package/src/commands/pr.ts +452 -0
  44. package/src/commands/sandbox.ts +2 -2
  45. package/src/context/architecture.ts +5 -2
  46. package/src/context/citations.ts +1 -1
  47. package/src/gate/boundaries.ts +509 -0
  48. package/src/gate/measures.ts +8 -19
  49. package/src/gate/stages.ts +12 -28
  50. package/src/pr/frames.ts +323 -0
  51. package/src/sandbox/census.ts +1 -1
  52. package/src/sandbox/coverage.ts +1 -1
  53. package/src/sandbox/expect.ts +0 -1
  54. package/src/shipped/references.ts +8 -10
  55. package/src/targets/pulls.ts +2 -2
  56. package/src/tooling/manifest.ts +2 -3
  57. package/src/tooling/seeds.ts +37 -0
  58. package/tooling/claude/manifest.toml +3 -3
  59. package/tsconfig.json +1 -1
  60. package/claude/skills/role-orchestrator/scripts/poll.sh +0 -504
  61. package/claude/skills/role-orchestrator/scripts/watch.sh +0 -181
  62. package/scripts/core/check-capability-seeding.sh +0 -134
  63. package/scripts/core/check-ignore-parity.sh +0 -141
  64. package/scripts/core/check-plugin-boundary.sh +0 -45
  65. package/scripts/core/check-seed-independence.sh +0 -62
  66. package/scripts/core/check-skill-paths.sh +0 -24
  67. package/scripts/core/list-seed-roots.sh +0 -18
  68. package/scripts/lib/tooling.sh +0 -37
@@ -0,0 +1,323 @@
1
+ import { readFile } from 'node:fs/promises'
2
+
3
+ /** The branch holding review frames. It never merges into a branch anyone checks out. */
4
+ export const FRAMES_BRANCH = 'canon-frames'
5
+
6
+ /** The eight bytes every PNG opens with. A frame that lacks them embeds as a broken image. */
7
+ const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]
8
+
9
+ const SHORT_HEAD_LENGTH = 7
10
+
11
+ const HEAD_PATTERN = /^[0-9a-f]{7,40}$/
12
+
13
+ /** A compact UTC instant such as `20261002T154450Z`, the form `date -u +%Y%m%dT%H%M%SZ` prints. */
14
+ const PASS_PATTERN = /^\d{8}T\d{6}Z$/
15
+
16
+ /** How many times an add rebuilds on a tip another writer moved. */
17
+ const ADD_ATTEMPTS = 3
18
+
19
+ const FOLDER_PATTERN = /^pr-(\d+)$/
20
+
21
+ const DAY_MS = 86_400_000
22
+
23
+ export interface TreeEntry {
24
+ readonly path: string
25
+ readonly mode: string
26
+ readonly type: 'blob' | 'tree'
27
+ readonly sha: string
28
+ }
29
+
30
+ export type Tip =
31
+ | { readonly kind: 'found'; readonly commit: string; readonly tree: string }
32
+ | { readonly kind: 'missing' }
33
+ | { readonly kind: 'unreadable' }
34
+
35
+ /** How a ref write landed. `conflict` means another writer holds the ref the call expected to own. */
36
+ export type RefWrite = 'ok' | 'conflict' | 'failed'
37
+
38
+ export interface PullClosure {
39
+ readonly state: 'open' | 'closed'
40
+ readonly closedAt?: string
41
+ }
42
+
43
+ /**
44
+ * The git data API calls a frame write needs, injected so the logic runs
45
+ * without a network. Every read that fails returns undefined rather than
46
+ * throwing, and each caller decides what an absent answer means.
47
+ */
48
+ export interface FramesApi {
49
+ readTip(): Promise<Tip>
50
+ listTree(tree: string): Promise<readonly TreeEntry[] | undefined>
51
+ createBlob(base64: string): Promise<string | undefined>
52
+ createTree(
53
+ entries: readonly TreeEntry[],
54
+ base?: string,
55
+ ): Promise<string | undefined>
56
+ createCommit(
57
+ message: string,
58
+ tree: string,
59
+ parents: readonly string[],
60
+ ): Promise<string | undefined>
61
+ createRef(commit: string): Promise<RefWrite>
62
+ moveRef(commit: string, force: boolean): Promise<RefWrite>
63
+ deleteRef(): Promise<boolean>
64
+ readPull(number: number): Promise<PullClosure | undefined>
65
+ }
66
+
67
+ export type FramesRefusal = 'unreadable-tip' | 'push-failed' | 'ref-conflict'
68
+
69
+ export interface FrameInput {
70
+ readonly number: number
71
+ readonly head: string
72
+ readonly pass: string
73
+ readonly box: number
74
+ readonly bytes: Uint8Array
75
+ }
76
+
77
+ export type PushResult =
78
+ | {
79
+ readonly kind: 'pushed'
80
+ readonly commit: string
81
+ readonly path: string
82
+ readonly created: boolean
83
+ }
84
+ | { readonly kind: 'refused'; readonly reason: FramesRefusal }
85
+
86
+ export type DropResult =
87
+ | {
88
+ readonly kind: 'dropped'
89
+ readonly removed: readonly number[]
90
+ readonly commit?: string
91
+ readonly deleted?: boolean
92
+ }
93
+ | { readonly kind: 'refused'; readonly reason: FramesRefusal }
94
+
95
+ export type PruneResult =
96
+ | {
97
+ readonly kind: 'dropped'
98
+ readonly removed: readonly number[]
99
+ readonly kept: readonly number[]
100
+ readonly unread: readonly number[]
101
+ readonly commit?: string
102
+ readonly deleted?: boolean
103
+ }
104
+ | { readonly kind: 'refused'; readonly reason: FramesRefusal }
105
+
106
+ /** Whether a value names a commit, so a branch name or a path never becomes a folder. */
107
+ export function isFrameHead(head: string): boolean {
108
+ return HEAD_PATTERN.test(head)
109
+ }
110
+
111
+ /** Whether a value is a compact UTC stamp, the only pass segment the path takes. */
112
+ export function isFramePass(pass: string): boolean {
113
+ return PASS_PATTERN.test(pass)
114
+ }
115
+
116
+ /**
117
+ * Where a frame lands. The pass segment keeps a second pass at the same head
118
+ * and box from replacing the image an earlier review comment shows, since a
119
+ * branch-path link reads whatever the path holds now.
120
+ */
121
+ export function framePath(
122
+ number: number,
123
+ head: string,
124
+ pass: string,
125
+ box: number,
126
+ ): string {
127
+ return `pr-${number}/${head.slice(0, SHORT_HEAD_LENGTH)}/${pass}/box-${box}.png`
128
+ }
129
+
130
+ /**
131
+ * The embed link, by branch path rather than by commit. Every drop rewrites
132
+ * the branch, so a commit-pinned link to another pull request's frame would
133
+ * stop resolving once GitHub collects the commit, while a path link lives
134
+ * exactly as long as its file. The blob form is the one `canon pr evidence`
135
+ * embeds, which a private repository's browser can open where the raw host
136
+ * serves nothing.
137
+ */
138
+ export function frameLink(repo: string, path: string): string {
139
+ return `https://github.com/${repo}/blob/${FRAMES_BRANCH}/${path}?raw=true`
140
+ }
141
+
142
+ /** The frame's bytes, or undefined when the file is absent, empty, or not a PNG. */
143
+ export async function readFrame(path: string): Promise<Uint8Array | undefined> {
144
+ const bytes = await readFile(path).catch(() => undefined)
145
+ if (bytes === undefined || bytes.length <= PNG_SIGNATURE.length) {
146
+ return undefined
147
+ }
148
+ return PNG_SIGNATURE.every((byte, index) => bytes[index] === byte)
149
+ ? new Uint8Array(bytes)
150
+ : undefined
151
+ }
152
+
153
+ /**
154
+ * Commits one frame onto the branch, creating the branch with a root
155
+ * commit when it is missing. The ref only fast-forwards here, so a drop or a
156
+ * second add landing between the read and the move fails the move, and the
157
+ * add rebuilds on the tip that writer left.
158
+ */
159
+ export async function pushFrame(
160
+ api: FramesApi,
161
+ input: FrameInput,
162
+ ): Promise<PushResult> {
163
+ const path = framePath(input.number, input.head, input.pass, input.box)
164
+ const blob = await api.createBlob(Buffer.from(input.bytes).toString('base64'))
165
+ if (blob === undefined) return { kind: 'refused', reason: 'push-failed' }
166
+ const entry: TreeEntry = { path, mode: '100644', type: 'blob', sha: blob }
167
+ const message = `chore(frames): add box ${input.box} of #${input.number}`
168
+
169
+ for (let attempt = 0; attempt < ADD_ATTEMPTS; attempt += 1) {
170
+ const tip = await api.readTip()
171
+ if (tip.kind === 'unreadable') {
172
+ return { kind: 'refused', reason: 'unreadable-tip' }
173
+ }
174
+
175
+ const base = tip.kind === 'found' ? tip.tree : undefined
176
+ const tree = await api.createTree([entry], base)
177
+ if (tree === undefined) return { kind: 'refused', reason: 'push-failed' }
178
+ const parents = tip.kind === 'found' ? [tip.commit] : []
179
+ const commit = await api.createCommit(message, tree, parents)
180
+ if (commit === undefined) return { kind: 'refused', reason: 'push-failed' }
181
+
182
+ const write =
183
+ tip.kind === 'found'
184
+ ? await api.moveRef(commit, false)
185
+ : await api.createRef(commit)
186
+ if (write === 'failed') return { kind: 'refused', reason: 'push-failed' }
187
+ if (write === 'ok') {
188
+ return { kind: 'pushed', commit, path, created: tip.kind === 'missing' }
189
+ }
190
+ }
191
+ return { kind: 'refused', reason: 'ref-conflict' }
192
+ }
193
+
194
+ function folderNumber(entry: TreeEntry): number | undefined {
195
+ if (entry.type !== 'tree') return undefined
196
+ const match = FOLDER_PATTERN.exec(entry.path)
197
+ return match?.[1] === undefined ? undefined : Number(match[1])
198
+ }
199
+
200
+ async function readFolders(
201
+ api: FramesApi,
202
+ ): Promise<
203
+ | { readonly kind: 'read'; readonly entries: readonly TreeEntry[] }
204
+ | { readonly kind: 'missing' }
205
+ | { readonly kind: 'refused'; readonly reason: FramesRefusal }
206
+ > {
207
+ const tip = await api.readTip()
208
+ if (tip.kind === 'missing') return { kind: 'missing' }
209
+ if (tip.kind === 'unreadable') {
210
+ return { kind: 'refused', reason: 'unreadable-tip' }
211
+ }
212
+ const entries = await api.listTree(tip.tree)
213
+ if (entries === undefined) {
214
+ return { kind: 'refused', reason: 'unreadable-tip' }
215
+ }
216
+ return { kind: 'read', entries }
217
+ }
218
+
219
+ /**
220
+ * Rewrites the branch without the named pull requests' folders, as one
221
+ * root commit the ref is force-moved to. A plain delete commit would
222
+ * keep every dropped image reachable through history. The rewrite builds from
223
+ * the tip it reads, and a branch left empty is deleted rather than holding an
224
+ * empty tree.
225
+ */
226
+ async function rewriteWithout(
227
+ api: FramesApi,
228
+ entries: readonly TreeEntry[],
229
+ drop: ReadonlySet<number>,
230
+ ): Promise<DropResult> {
231
+ const removed = entries
232
+ .map(folderNumber)
233
+ .filter((n): n is number => n !== undefined && drop.has(n))
234
+ .sort((a, b) => a - b)
235
+ if (removed.length === 0) return { kind: 'dropped', removed: [] }
236
+
237
+ const kept = entries.filter((entry) => {
238
+ const n = folderNumber(entry)
239
+ return n === undefined || !drop.has(n)
240
+ })
241
+ if (kept.length === 0) {
242
+ return (await api.deleteRef())
243
+ ? { kind: 'dropped', removed, deleted: true }
244
+ : { kind: 'refused', reason: 'push-failed' }
245
+ }
246
+
247
+ const tree = await api.createTree(kept)
248
+ if (tree === undefined) return { kind: 'refused', reason: 'push-failed' }
249
+ const message = `chore(frames): drop ${removed.map((n) => `#${n}`).join(', ')}`
250
+ const commit = await api.createCommit(message, tree, [])
251
+ if (commit === undefined) return { kind: 'refused', reason: 'push-failed' }
252
+ const write = await api.moveRef(commit, true)
253
+ return write === 'ok'
254
+ ? { kind: 'dropped', removed, commit }
255
+ : { kind: 'refused', reason: 'push-failed' }
256
+ }
257
+
258
+ export async function dropFrames(
259
+ api: FramesApi,
260
+ numbers: readonly number[],
261
+ ): Promise<DropResult> {
262
+ const folders = await readFolders(api)
263
+ if (folders.kind === 'missing') return { kind: 'dropped', removed: [] }
264
+ if (folders.kind === 'refused') return folders
265
+ return rewriteWithout(api, folders.entries, new Set(numbers))
266
+ }
267
+
268
+ /**
269
+ * Whether a pull request's frames are past keeping. Only a pull request
270
+ * reading closed, with a close date strictly older than the window, expires,
271
+ * so a reopened one, an unread one, and one at the boundary all stay.
272
+ */
273
+ export function isExpired(
274
+ closure: PullClosure | undefined,
275
+ days: number,
276
+ now: Date,
277
+ ): boolean {
278
+ if (closure?.state !== 'closed' || closure.closedAt === undefined) {
279
+ return false
280
+ }
281
+ const closed = Date.parse(closure.closedAt)
282
+ if (Number.isNaN(closed)) return false
283
+ return now.getTime() - closed > days * DAY_MS
284
+ }
285
+
286
+ /**
287
+ * Drops every pull request on the branch that closed more than `days` ago, in
288
+ * one rewrite. A pull request whose state could not be read is kept and
289
+ * listed under `unread`, so a failed read never deletes frames.
290
+ */
291
+ export async function pruneFrames(
292
+ api: FramesApi,
293
+ days: number,
294
+ now: Date,
295
+ ): Promise<PruneResult> {
296
+ const folders = await readFolders(api)
297
+ if (folders.kind === 'missing') {
298
+ return { kind: 'dropped', removed: [], kept: [], unread: [] }
299
+ }
300
+ if (folders.kind === 'refused') return folders
301
+
302
+ const numbers = folders.entries
303
+ .map(folderNumber)
304
+ .filter((n): n is number => n !== undefined)
305
+ const closures = await Promise.all(numbers.map((n) => api.readPull(n)))
306
+ const expired = new Set<number>()
307
+ const kept: number[] = []
308
+ const unread: number[] = []
309
+ numbers.forEach((n, index) => {
310
+ const closure = closures[index]
311
+ if (closure === undefined) unread.push(n)
312
+ if (isExpired(closure, days, now)) expired.add(n)
313
+ else kept.push(n)
314
+ })
315
+
316
+ const result = await rewriteWithout(api, folders.entries, expired)
317
+ if (result.kind === 'refused') return result
318
+ return {
319
+ ...result,
320
+ kept: kept.sort((a, b) => a - b),
321
+ unread: unread.sort((a, b) => a - b),
322
+ }
323
+ }
@@ -134,7 +134,7 @@ export function parseExemptions(source: string): Map<string, string> {
134
134
  }
135
135
 
136
136
  function readExemptions(root: string): Map<string, string> {
137
- const path = join(root, 'scripts', 'sandbox', EXEMPT_FILE)
137
+ const path = join(root, 'sandbox', EXEMPT_FILE)
138
138
  if (!existsSync(path)) return new Map()
139
139
 
140
140
  return parseExemptions(readFileSync(path, 'utf8'))
@@ -88,7 +88,7 @@ function armsFor(
88
88
  * a suite that proves something from one that runs.
89
89
  */
90
90
  export function collectCoverage(root: string): CoverageReport {
91
- const sandboxDir = join(root, 'scripts', 'sandbox')
91
+ const sandboxDir = join(root, 'sandbox')
92
92
  const fixturesDir = join(sandboxDir, FIXTURES_DIR)
93
93
 
94
94
  const scenarios = listScenarios(sandboxDir).map((scenario) => ({
@@ -138,7 +138,6 @@ export function expectFilePath(
138
138
  ): string {
139
139
  return join(
140
140
  root,
141
- 'scripts',
142
141
  'sandbox',
143
142
  'fixtures',
144
143
  category,
@@ -36,13 +36,10 @@ export const SHIPPED_CORPORA = [
36
36
 
37
37
  /**
38
38
  * Trees inside a corpus above that no target receives, matching the negations
39
- * the `files` field already makes. A number under either names something its
40
- * only reader can already resolve.
39
+ * the `files` field already makes. A number under one names something its only
40
+ * reader can already resolve.
41
41
  */
42
- export const SHIPPED_EXCLUSIONS = [
43
- /^scripts\/sandbox\//,
44
- /^scripts\/eval\//,
45
- ] as const
42
+ export const SHIPPED_EXCLUSIONS = [/^scripts\/eval\//] as const
46
43
 
47
44
  /** Whether a repository-relative path sits in the gated corpus. */
48
45
  export function isShippedCorpus(path: string): boolean {
@@ -97,10 +94,11 @@ const PULL_REQUEST = /(?<![0-9A-Za-z_])#([0-9]+)(?![0-9A-Za-z_])/g
97
94
  * The third is the one a later author meets. Admitting an all-digit sha admits
98
95
  * every run of seven or more decimal digits with it, so a date written without
99
96
  * separators, a large count, or a float artifact reads as a commit reference.
100
- * The corpus carries no instance and that is the exclusions rather than luck:
101
- * twelve such runs sit under the seven roots, eleven of them under
102
- * `scripts/sandbox/`, `scripts/eval/`, or a test file, and the twelfth is the
103
- * genuine sha in `orchestrator-poll.md`. Write a seven-digit measurement into a
97
+ * The corpus carries no instance and that is the corpus bounds rather than luck:
98
+ * twelve such runs were counted when the scenario tree still sat under
99
+ * `scripts/`, eleven of them under that tree, `scripts/eval/`, or a test file,
100
+ * all now outside the corpus, and the twelfth is the genuine sha in
101
+ * `orchestrator-poll.md`. Write a seven-digit measurement into a
104
102
  * shipped page and the push fails on it, which the marker answers and no
105
103
  * narrowing can, since requiring a letter loses the all-digit sha above.
106
104
  */
@@ -8,7 +8,7 @@ const GH_TIMEOUT_MS = 30_000
8
8
  * The two headings a review pass posts under.
9
9
  *
10
10
  * Owned by `review-pr`, which states the full set once, and pinned here
11
- * the way `role-orchestrator/scripts/poll.sh` pins them. All three surfaces
11
+ * the way `role-orchestrator/scripts/poll.ts` pins them. All three surfaces
12
12
  * ship separately, so a heading added in that skill goes stale here with
13
13
  * nothing comparing the copies.
14
14
  */
@@ -139,7 +139,7 @@ export function rollup(checks: readonly RawCheck[]): ChecksState | null {
139
139
 
140
140
  /**
141
141
  * Reads the heading of the newest pass carrying one, matching on the first line
142
- * alone the way `poll.sh` does.
142
+ * alone the way `poll.ts` does.
143
143
  *
144
144
  * The reviews arrive oldest first, so the last match is the current state of
145
145
  * the thread. A pass carrying neither heading is somebody reviewing by hand and
@@ -111,9 +111,8 @@ export function isStackExcluded(stack: string): boolean {
111
111
  }
112
112
 
113
113
  /**
114
- * Lists installable stack names, filtering the `claude` stack the way
115
- * `is_tooling_stack_excluded` in `scripts/lib/tooling.sh` did. Claude is
116
- * managed by `canon claude`, not `canon tooling`.
114
+ * Lists installable stack names, filtering the stacks in `EXCLUDED_STACKS`.
115
+ * Claude is managed by `canon claude`, not `canon tooling`.
117
116
  */
118
117
  export function listStacks(root: string): string[] {
119
118
  const dir = toolingDir(root)
@@ -0,0 +1,37 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ const TOOLING = 'tooling'
5
+ const SURFACE_ROOTS = ['.claude', 'canon']
6
+
7
+ /**
8
+ * Every `tooling/<stack>/seeds` holding a `.claude/` or a `canon/`, sorted and
9
+ * relative to the root.
10
+ *
11
+ * Both seed stages discover through this rather than naming a stack, so a stack
12
+ * seeding either root later reaches both and the two cannot disagree about which
13
+ * roots exist. A stack seeding only tracked surfaces carries `canon/` alone,
14
+ * which a `.claude/` test would drop from both stages. An absent `tooling/` is
15
+ * an empty answer, and each caller decides what an empty answer means.
16
+ */
17
+ export function seedRoots(root: string): string[] {
18
+ const tooling = join(root, TOOLING)
19
+ if (!existsSync(tooling)) return []
20
+
21
+ return readdirSync(tooling)
22
+ .filter((name) => !name.startsWith('.'))
23
+ .sort()
24
+ .map((name) => `${TOOLING}/${name}/seeds`)
25
+ .filter((seeds) => isDirectory(join(root, seeds)))
26
+ .filter((seeds) =>
27
+ SURFACE_ROOTS.some((surface) => isDirectory(join(root, seeds, surface))),
28
+ )
29
+ }
30
+
31
+ function isDirectory(path: string): boolean {
32
+ try {
33
+ return statSync(path).isDirectory()
34
+ } catch {
35
+ return false
36
+ }
37
+ }
@@ -1,5 +1,5 @@
1
1
  # Consumed only by `canon claude init`. Claude is not a discoverable tooling stack
2
- # and is excluded from `canon tooling` discovery via scripts/lib/tooling.sh.
2
+ # and is excluded from `canon tooling` discovery via `EXCLUDED_STACKS` in src/tooling/manifest.ts.
3
3
 
4
4
  [stack]
5
5
  name = "claude"
@@ -8,8 +8,8 @@ runtime = ""
8
8
  scaffold = ""
9
9
 
10
10
  # Checked against this repository's own `.gitignore` by
11
- # scripts/core/check-ignore-parity.sh, which carries no sanctioned divergence
12
- # now that both sides ship the same two entries.
11
+ # the Ignore parity stage of `bun run check`, which carries no sanctioned
12
+ # divergence now that both sides ship the same two entries.
13
13
  #
14
14
  # One root entry rather than a folder apiece, so a target that gains a record
15
15
  # folder later receives no manifest edit and no sync. `.claude/worktrees/` is
package/tsconfig.json CHANGED
@@ -11,5 +11,5 @@
11
11
  "@/*": ["./src/*"]
12
12
  }
13
13
  },
14
- "include": ["src"]
14
+ "include": ["src", "claude/skills/*/scripts/**/*.ts"]
15
15
  }