@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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/SKILL.md +3 -2
- package/claude/skills/deprecation-migration/REQUIREMENT.md +62 -0
- package/claude/skills/deprecation-migration/SKILL.md +115 -0
- package/claude/skills/deprecation-migration/references/adopted.md +50 -0
- package/claude/skills/git-followup/REQUIREMENT.md +1 -0
- package/claude/skills/git-followup/SKILL.md +3 -0
- package/claude/skills/git-pr/REQUIREMENT.md +1 -0
- package/claude/skills/git-pr/SKILL.md +1 -1
- package/claude/skills/git-pr/references/evidence.md +2 -2
- package/claude/skills/git-ship/REQUIREMENT.md +3 -0
- package/claude/skills/git-ship/SKILL.md +12 -1
- package/claude/skills/review-pr/SKILL.md +1 -1
- package/claude/skills/review-pr/references/close-out.md +1 -1
- package/claude/skills/review-pr/references/post.md +1 -1
- package/claude/skills/review-ui/REQUIREMENT.md +5 -2
- package/claude/skills/review-ui/SKILL.md +12 -4
- package/claude/skills/review-ui/references/driving.md +5 -3
- package/claude/skills/review-ui/references/post.md +3 -2
- package/claude/skills/role-orchestrator/SKILL.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +10 -10
- package/claude/skills/role-orchestrator/scripts/poll.ts +628 -0
- package/claude/skills/role-orchestrator/scripts/repo.ts +69 -0
- package/claude/skills/role-orchestrator/scripts/watch.ts +267 -0
- package/docs/agents/commands.md +1 -1
- package/docs/agents/index.md +1 -0
- package/docs/agents/markdown-audit.md +1 -1
- package/docs/agents/pr-evidence.md +5 -0
- package/docs/agents/pr-frames.md +112 -0
- package/docs/agents/pr-reads.md +1 -1
- package/docs/agents/sandbox.md +3 -3
- package/docs/agents/state-scoped-risk.md +2 -2
- package/docs/workflow/skill-map.md +15 -14
- package/package.json +4 -5
- package/scripts/core/check-color-source.sh +11 -1
- package/scripts/core/install-check.sh +13 -4
- package/scripts/lib/sandbox-dispatch.sh +1 -1
- package/scripts/lib/sandbox-fixtures.sh +2 -2
- package/scripts/lib/sandbox-path.sh +1 -1
- package/scripts/manage-sandbox.sh +2 -2
- package/src/claude/cases/misc.ts +5 -0
- package/src/claude/skills-audit.ts +1 -0
- package/src/commands/pr.ts +452 -0
- package/src/commands/sandbox.ts +2 -2
- package/src/context/architecture.ts +5 -2
- package/src/context/citations.ts +1 -1
- package/src/gate/boundaries.ts +509 -0
- package/src/gate/measures.ts +8 -19
- package/src/gate/stages.ts +12 -28
- package/src/pr/frames.ts +323 -0
- package/src/sandbox/census.ts +1 -1
- package/src/sandbox/coverage.ts +1 -1
- package/src/sandbox/expect.ts +0 -1
- package/src/shipped/references.ts +8 -10
- package/src/targets/pulls.ts +2 -2
- package/src/tooling/manifest.ts +2 -3
- package/src/tooling/seeds.ts +37 -0
- package/tooling/claude/manifest.toml +3 -3
- package/tsconfig.json +1 -1
- package/claude/skills/role-orchestrator/scripts/poll.sh +0 -504
- package/claude/skills/role-orchestrator/scripts/watch.sh +0 -181
- package/scripts/core/check-capability-seeding.sh +0 -134
- package/scripts/core/check-ignore-parity.sh +0 -141
- package/scripts/core/check-plugin-boundary.sh +0 -45
- package/scripts/core/check-seed-independence.sh +0 -62
- package/scripts/core/check-skill-paths.sh +0 -24
- package/scripts/core/list-seed-roots.sh +0 -18
- package/scripts/lib/tooling.sh +0 -37
package/src/pr/frames.ts
ADDED
|
@@ -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
|
+
}
|
package/src/sandbox/census.ts
CHANGED
|
@@ -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, '
|
|
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'))
|
package/src/sandbox/coverage.ts
CHANGED
|
@@ -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, '
|
|
91
|
+
const sandboxDir = join(root, 'sandbox')
|
|
92
92
|
const fixturesDir = join(sandboxDir, FIXTURES_DIR)
|
|
93
93
|
|
|
94
94
|
const scenarios = listScenarios(sandboxDir).map((scenario) => ({
|
package/src/sandbox/expect.ts
CHANGED
|
@@ -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
|
|
40
|
-
*
|
|
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
|
|
101
|
-
* twelve such runs
|
|
102
|
-
* `scripts
|
|
103
|
-
*
|
|
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
|
*/
|
package/src/targets/pulls.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
package/src/tooling/manifest.ts
CHANGED
|
@@ -111,9 +111,8 @@ export function isStackExcluded(stack: string): boolean {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
|
-
* Lists installable stack names, filtering the
|
|
115
|
-
*
|
|
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
|
|
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
|
-
#
|
|
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