@iceinvein/agent-skills 0.23.9 → 0.23.13
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/package.json +1 -1
- package/skills/index.json +2 -2
- package/skills/magpie/SKILL.md +21 -20
- package/skills/magpie/bin/magpie.ts +63 -2
- package/skills/magpie/evals/quality/README.md +74 -1
- package/skills/magpie/evals/quality/__tests__/probe-specialist.test.ts +498 -0
- package/skills/magpie/evals/quality/__tests__/replay-full.test.ts +9 -0
- package/skills/magpie/evals/quality/probe-specialist.ts +672 -0
- package/skills/magpie/evals/quality/replay-full.ts +6 -2
- package/skills/magpie/fixtures/fake-gh-with-lockfile.sh +37 -2
- package/skills/magpie/fixtures/fake-gh.sh +49 -2
- package/skills/magpie/references/critic.md +20 -4
- package/skills/magpie/references/proof.md +102 -0
- package/skills/magpie/references/scout.md +6 -1
- package/skills/magpie/references/specialists.md +15 -9
- package/skills/magpie/scripts/__tests__/cleanup-cmd.test.ts +103 -7
- package/skills/magpie/scripts/__tests__/cli.test.ts +69 -0
- package/skills/magpie/scripts/__tests__/critic-cmd.test.ts +46 -0
- package/skills/magpie/scripts/__tests__/critic.test.ts +11 -1
- package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +20 -1
- package/skills/magpie/scripts/__tests__/git-diff.test.ts +23 -4
- package/skills/magpie/scripts/__tests__/handoff-cmd.test.ts +68 -0
- package/skills/magpie/scripts/__tests__/history.test.ts +232 -0
- package/skills/magpie/scripts/__tests__/impact-cmd.test.ts +477 -0
- package/skills/magpie/scripts/__tests__/impact-render.test.ts +365 -0
- package/skills/magpie/scripts/__tests__/impact.test.ts +915 -0
- package/skills/magpie/scripts/__tests__/incremental.test.ts +59 -8
- package/skills/magpie/scripts/__tests__/pipeline-e2e.test.ts +1 -1
- package/skills/magpie/scripts/__tests__/post-cmd.test.ts +58 -0
- package/skills/magpie/scripts/__tests__/pr-activity.test.ts +281 -0
- package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +4 -4
- package/skills/magpie/scripts/__tests__/previous-findings.test.ts +267 -0
- package/skills/magpie/scripts/__tests__/proof-cmd.test.ts +260 -0
- package/skills/magpie/scripts/__tests__/proof.test.ts +226 -0
- package/skills/magpie/scripts/__tests__/refresh.test.ts +71 -0
- package/skills/magpie/scripts/__tests__/render-annotation.test.ts +34 -0
- package/skills/magpie/scripts/__tests__/render-findings.test.ts +75 -0
- package/skills/magpie/scripts/__tests__/render-progress.test.ts +9 -0
- package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +376 -1
- package/skills/magpie/scripts/__tests__/shard-coverage.test.ts +79 -0
- package/skills/magpie/scripts/__tests__/skill-lint.test.ts +48 -2
- package/skills/magpie/scripts/__tests__/status-cmd.test.ts +20 -8
- package/skills/magpie/scripts/__tests__/types.test.ts +46 -0
- package/skills/magpie/scripts/cleanup-cmd.ts +88 -0
- package/skills/magpie/scripts/critic-cmd.ts +2 -0
- package/skills/magpie/scripts/critic.ts +4 -0
- package/skills/magpie/scripts/dedupe-cmd.ts +4 -0
- package/skills/magpie/scripts/evidence-filter.ts +10 -0
- package/skills/magpie/scripts/git-diff.ts +50 -19
- package/skills/magpie/scripts/handoff-cmd.ts +42 -3
- package/skills/magpie/scripts/history.ts +143 -0
- package/skills/magpie/scripts/impact-cmd.ts +202 -0
- package/skills/magpie/scripts/impact-render.ts +158 -0
- package/skills/magpie/scripts/impact.ts +622 -0
- package/skills/magpie/scripts/incremental.ts +44 -5
- package/skills/magpie/scripts/labels-cmd.ts +7 -3
- package/skills/magpie/scripts/post-cmd.ts +46 -0
- package/skills/magpie/scripts/pr-activity.ts +243 -0
- package/skills/magpie/scripts/preview-cmd.ts +3 -0
- package/skills/magpie/scripts/previous-findings.ts +181 -0
- package/skills/magpie/scripts/proof-cmd.ts +226 -0
- package/skills/magpie/scripts/proof.ts +150 -0
- package/skills/magpie/scripts/refresh.ts +11 -0
- package/skills/magpie/scripts/render-annotation.ts +17 -0
- package/skills/magpie/scripts/render-cmd.ts +12 -0
- package/skills/magpie/scripts/render-findings.ts +37 -1
- package/skills/magpie/scripts/render-progress.ts +3 -0
- package/skills/magpie/scripts/setup-cmd.ts +142 -11
- package/skills/magpie/scripts/shard-coverage.ts +53 -0
- package/skills/magpie/scripts/status-cmd.ts +2 -0
- package/skills/magpie/scripts/types.ts +39 -0
- package/skills/magpie/skill.json +1 -1
- package/skills/magpie/templates/styles.css +30 -0
- package/skills/sluice/scripts/status.sh +32 -13
- package/skills/sluice/skill.json +1 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { appendFile, mkdir, realpath, rm, stat, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
3
3
|
import { fetchPr } from './gh.ts'
|
|
4
|
-
import {
|
|
4
|
+
import { resolvePrHead } from './git-diff.ts'
|
|
5
|
+
import { buildIncrementalContext, changedFilesSince, findPreviousRun } from './incremental.ts'
|
|
5
6
|
import { diffSha256, localBranchDiff, localBranchMeta } from './local-diff.ts'
|
|
6
7
|
import { filterDiff, loadPathFilterConfig } from './path-filter.ts'
|
|
8
|
+
import { fetchPrActivity } from './pr-activity.ts'
|
|
7
9
|
import { type Deps, defaultDeps, preflight, renderInstallHint } from './preflight.ts'
|
|
10
|
+
import { readPreviousFindings, writePreviousFindings } from './previous-findings.ts'
|
|
8
11
|
import { shardDiff } from './shard.ts'
|
|
9
12
|
import { writeTarget } from './target.ts'
|
|
10
13
|
import { detectMissingTests } from './tests-check.ts'
|
|
@@ -93,6 +96,13 @@ export async function runSetup(input: RunSetupInput): Promise<number> {
|
|
|
93
96
|
await cleanup(runDir)
|
|
94
97
|
return diffExit
|
|
95
98
|
}
|
|
99
|
+
if (input.mode === 'pr') {
|
|
100
|
+
const activityExit = await writePrActivity(runDir, deps, repoPath, input.prNumber)
|
|
101
|
+
if (activityExit !== 0) {
|
|
102
|
+
await cleanup(runDir)
|
|
103
|
+
return activityExit
|
|
104
|
+
}
|
|
105
|
+
}
|
|
96
106
|
|
|
97
107
|
await applyPathFilter(runDir, repoPath)
|
|
98
108
|
|
|
@@ -113,15 +123,35 @@ export async function runSetup(input: RunSetupInput): Promise<number> {
|
|
|
113
123
|
return 0
|
|
114
124
|
}
|
|
115
125
|
|
|
116
|
-
await
|
|
126
|
+
const prJson = (await Bun.file(join(runDir, 'pr.json')).json()) as { headRefOid: string }
|
|
127
|
+
const head = await resolvePrHead({
|
|
128
|
+
gitBin: deps.git,
|
|
129
|
+
repoPath,
|
|
130
|
+
prNumber: input.prNumber,
|
|
131
|
+
headRefOid: prJson.headRefOid,
|
|
132
|
+
})
|
|
133
|
+
if (!head.ok) {
|
|
134
|
+
await logLine(runDir, { stage: 'worktree', status: 'error', error: head.error })
|
|
135
|
+
process.stderr.write(`magpie: ${head.error}\n`)
|
|
136
|
+
await cleanup(runDir)
|
|
137
|
+
return 5
|
|
138
|
+
}
|
|
117
139
|
|
|
118
|
-
|
|
119
|
-
|
|
140
|
+
let previous: { runId: string; runDir: string } | null
|
|
141
|
+
try {
|
|
142
|
+
previous = await detectIncrementalReview(runDir, deps.git, repoPath, input.prNumber)
|
|
143
|
+
} catch (err) {
|
|
144
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
145
|
+
await logLine(runDir, { stage: 'incremental', status: 'error', error: message })
|
|
146
|
+
process.stderr.write(`magpie: ${message}\n`)
|
|
147
|
+
await cleanup(runDir)
|
|
148
|
+
return 4
|
|
149
|
+
}
|
|
120
150
|
|
|
121
151
|
const wt = await createWorktree({
|
|
122
152
|
gitBin: deps.git,
|
|
123
153
|
repoPath,
|
|
124
|
-
branch,
|
|
154
|
+
branch: head.sha,
|
|
125
155
|
runDir: runDir,
|
|
126
156
|
})
|
|
127
157
|
if (!wt.ok) {
|
|
@@ -146,10 +176,90 @@ export async function runSetup(input: RunSetupInput): Promise<number> {
|
|
|
146
176
|
await cleanup(runDir)
|
|
147
177
|
return 5
|
|
148
178
|
}
|
|
179
|
+
if (previous) {
|
|
180
|
+
try {
|
|
181
|
+
await carryPreviousFindings(runDir, previous, wt.worktreePath)
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
184
|
+
await logLine(runDir, { stage: 'previous-findings', status: 'error', error: message })
|
|
185
|
+
process.stderr.write(`magpie: ${message}\n`)
|
|
186
|
+
const removed = await removeWorktree({
|
|
187
|
+
gitBin: deps.git,
|
|
188
|
+
repoPath,
|
|
189
|
+
worktreePath: wt.worktreePath,
|
|
190
|
+
})
|
|
191
|
+
if (!removed.ok) process.stderr.write(`magpie: ${removed.error}\n`)
|
|
192
|
+
await cleanup(runDir)
|
|
193
|
+
return 5
|
|
194
|
+
}
|
|
195
|
+
}
|
|
149
196
|
await logLine(runDir, { stage: 'setup', status: 'done', worktree: wt.worktreePath })
|
|
150
197
|
return 0
|
|
151
198
|
}
|
|
152
199
|
|
|
200
|
+
async function writePrActivity(
|
|
201
|
+
runDir: string,
|
|
202
|
+
deps: Deps,
|
|
203
|
+
repoPath: string,
|
|
204
|
+
prNumber: number,
|
|
205
|
+
): Promise<number> {
|
|
206
|
+
const fail = async (error: string): Promise<number> => {
|
|
207
|
+
await logLine(runDir, { stage: 'pr-activity', status: 'error', error })
|
|
208
|
+
process.stderr.write(`magpie: ${error}\n`)
|
|
209
|
+
return 4
|
|
210
|
+
}
|
|
211
|
+
const prJson = (await Bun.file(join(runDir, 'pr.json')).json()) as { url?: unknown }
|
|
212
|
+
if (typeof prJson.url !== 'string') return fail('pr.json has no url')
|
|
213
|
+
let result: Awaited<ReturnType<typeof fetchPrActivity>>
|
|
214
|
+
try {
|
|
215
|
+
result = await fetchPrActivity({
|
|
216
|
+
ghBin: deps.gh,
|
|
217
|
+
prNumber,
|
|
218
|
+
prUrl: prJson.url,
|
|
219
|
+
runDir,
|
|
220
|
+
cwd: repoPath,
|
|
221
|
+
})
|
|
222
|
+
} catch (err) {
|
|
223
|
+
return fail(err instanceof Error ? err.message : String(err))
|
|
224
|
+
}
|
|
225
|
+
if (!result.ok) return fail(result.error)
|
|
226
|
+
await logLine(runDir, {
|
|
227
|
+
stage: 'pr-activity',
|
|
228
|
+
status: 'done',
|
|
229
|
+
checks: result.activity.checks.length,
|
|
230
|
+
threads: result.activity.threads.length,
|
|
231
|
+
comments: result.activity.comments.length,
|
|
232
|
+
})
|
|
233
|
+
return 0
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function carryPreviousFindings(
|
|
237
|
+
runDir: string,
|
|
238
|
+
previous: { runId: string; runDir: string },
|
|
239
|
+
worktree: string,
|
|
240
|
+
): Promise<void> {
|
|
241
|
+
const carried = await readPreviousFindings({
|
|
242
|
+
runDir: previous.runDir,
|
|
243
|
+
runId: previous.runId,
|
|
244
|
+
worktree,
|
|
245
|
+
})
|
|
246
|
+
if (!carried) {
|
|
247
|
+
await logLine(runDir, {
|
|
248
|
+
stage: 'previous-findings',
|
|
249
|
+
status: 'none',
|
|
250
|
+
previousRunId: previous.runId,
|
|
251
|
+
})
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
await writePreviousFindings(runDir, carried)
|
|
255
|
+
await logLine(runDir, {
|
|
256
|
+
stage: 'previous-findings',
|
|
257
|
+
status: 'done',
|
|
258
|
+
previousRunId: previous.runId,
|
|
259
|
+
findings: carried.findings.length,
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
153
263
|
/** realpath of the nearest existing ancestor, with the not-yet-created tail re-appended. */
|
|
154
264
|
async function realpathAllowingMissing(path: string): Promise<string> {
|
|
155
265
|
const absolute = resolve(path)
|
|
@@ -309,6 +419,8 @@ async function cleanup(runDir: string): Promise<void> {
|
|
|
309
419
|
'excluded-files.json',
|
|
310
420
|
'shards',
|
|
311
421
|
'incremental.json',
|
|
422
|
+
'pr-activity.json',
|
|
423
|
+
'previous-findings.json',
|
|
312
424
|
'findings',
|
|
313
425
|
'screen',
|
|
314
426
|
'state',
|
|
@@ -340,25 +452,44 @@ async function runTestsCheck(runDir: string): Promise<void> {
|
|
|
340
452
|
await logLine(runDir, { stage: 'tests-check', status: 'done', findings: findings.length })
|
|
341
453
|
}
|
|
342
454
|
|
|
343
|
-
|
|
344
|
-
|
|
455
|
+
/** The previous run, for setup to carry its findings forward once the worktree exists. */
|
|
456
|
+
async function detectIncrementalReview(
|
|
457
|
+
runDir: string,
|
|
458
|
+
gitBin: string,
|
|
459
|
+
repoPath: string,
|
|
460
|
+
prNumber: number,
|
|
461
|
+
): Promise<{ runId: string; runDir: string } | null> {
|
|
462
|
+
const currentPr = (await Bun.file(join(runDir, 'pr.json')).json()) as Record<string, unknown>
|
|
463
|
+
if (typeof currentPr.url !== 'string') throw new Error('pr.json has no url')
|
|
464
|
+
const previous = await findPreviousRun(prNumber, currentPr.url, runDir)
|
|
345
465
|
if (!previous) {
|
|
346
466
|
await logLine(runDir, { stage: 'incremental', status: 'first-run' })
|
|
347
|
-
return
|
|
467
|
+
return null
|
|
348
468
|
}
|
|
349
|
-
const currentPr = (await Bun.file(join(runDir, 'pr.json')).json()) as Record<string, unknown>
|
|
350
469
|
const context = buildIncrementalContext(currentPr, previous)
|
|
351
470
|
if (!context) {
|
|
352
471
|
await logLine(runDir, { stage: 'incremental', status: 'missing-sha' })
|
|
353
|
-
return
|
|
472
|
+
return previous
|
|
354
473
|
}
|
|
355
|
-
|
|
474
|
+
const changedSincePrevious = context.sameSha
|
|
475
|
+
? []
|
|
476
|
+
: await changedFilesSince({
|
|
477
|
+
gitBin,
|
|
478
|
+
repoPath,
|
|
479
|
+
fromSha: context.previousSha,
|
|
480
|
+
toSha: context.currentSha,
|
|
481
|
+
})
|
|
482
|
+
await writeFile(
|
|
483
|
+
join(runDir, 'incremental.json'),
|
|
484
|
+
`${JSON.stringify({ ...context, changedSincePrevious }, null, 2)}\n`,
|
|
485
|
+
)
|
|
356
486
|
await logLine(runDir, {
|
|
357
487
|
stage: 'incremental',
|
|
358
488
|
status: 'done',
|
|
359
489
|
previousRunId: context.previousRunId,
|
|
360
490
|
sameSha: context.sameSha,
|
|
361
491
|
})
|
|
492
|
+
return previous
|
|
362
493
|
}
|
|
363
494
|
|
|
364
495
|
async function applyPathFilter(runDir: string, repoPath: string): Promise<void> {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import type { ShardManifest } from './shard.ts'
|
|
4
|
+
|
|
5
|
+
/** The shards stage 4 skipped at the gate, from the run's last `shard-coverage` entry. */
|
|
6
|
+
export type ShardCoverage = { skipped: Array<{ id: number; files: string[] }> }
|
|
7
|
+
|
|
8
|
+
function isMissingFile(err: unknown): boolean {
|
|
9
|
+
return (err as NodeJS.ErrnoException)?.code === 'ENOENT'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Null when no shard was skipped, which is every run that did not answer the gate that way. */
|
|
13
|
+
export async function readShardCoverage(runDir: string): Promise<ShardCoverage | null> {
|
|
14
|
+
let log: string
|
|
15
|
+
try {
|
|
16
|
+
log = await readFile(join(runDir, 'log.jsonl'), 'utf8')
|
|
17
|
+
} catch (err) {
|
|
18
|
+
if (isMissingFile(err)) return null
|
|
19
|
+
throw err
|
|
20
|
+
}
|
|
21
|
+
let skippedIds: number[] | null = null
|
|
22
|
+
for (const line of log.split('\n').filter(Boolean)) {
|
|
23
|
+
let entry: Record<string, unknown>
|
|
24
|
+
try {
|
|
25
|
+
entry = JSON.parse(line)
|
|
26
|
+
} catch {
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
if (entry.stage !== 'shard-coverage' || entry.status !== 'partial') continue
|
|
30
|
+
if (!Array.isArray(entry.skipped) || !entry.skipped.every((id) => Number.isInteger(id))) {
|
|
31
|
+
throw new Error(`${join(runDir, 'log.jsonl')}: shard-coverage skipped must be shard ids`)
|
|
32
|
+
}
|
|
33
|
+
skippedIds = entry.skipped as number[]
|
|
34
|
+
}
|
|
35
|
+
if (skippedIds === null || skippedIds.length === 0) return null
|
|
36
|
+
const manifestPath = join(runDir, 'shards', 'manifest.json')
|
|
37
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as ShardManifest
|
|
38
|
+
return {
|
|
39
|
+
skipped: skippedIds.map((id) => {
|
|
40
|
+
const shard = manifest.shards.find((s) => s.id === id)
|
|
41
|
+
if (!shard) {
|
|
42
|
+
throw new Error(`${manifestPath}: shard ${id} was skipped but is not in the manifest`)
|
|
43
|
+
}
|
|
44
|
+
return { id, files: shard.files }
|
|
45
|
+
}),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function describeUnreviewed(coverage: ShardCoverage): string {
|
|
50
|
+
const ids = coverage.skipped.map((s) => s.id)
|
|
51
|
+
const files = coverage.skipped.reduce((n, s) => n + s.files.length, 0)
|
|
52
|
+
return `Not reviewed: ${ids.length === 1 ? 'shard' : 'shards'} ${ids.join(', ')} (${files} ${files === 1 ? 'file' : 'files'})`
|
|
53
|
+
}
|
|
@@ -12,6 +12,7 @@ export const ORDER = [
|
|
|
12
12
|
'dedupe',
|
|
13
13
|
'critic',
|
|
14
14
|
'peer-review',
|
|
15
|
+
'proof',
|
|
15
16
|
'report',
|
|
16
17
|
'post',
|
|
17
18
|
] as const
|
|
@@ -24,6 +25,7 @@ export const LOCAL_ORDER = [
|
|
|
24
25
|
'dedupe',
|
|
25
26
|
'critic',
|
|
26
27
|
'peer-review',
|
|
28
|
+
'proof',
|
|
27
29
|
'handoff',
|
|
28
30
|
] as const
|
|
29
31
|
|
|
@@ -42,6 +42,19 @@ export type MergedFromEntry = {
|
|
|
42
42
|
title: string
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export type Repro = {
|
|
46
|
+
/** Where the prover wrote the repro, relative to the worktree. It is deleted after the run. */
|
|
47
|
+
path: string
|
|
48
|
+
command: string
|
|
49
|
+
source: string
|
|
50
|
+
output: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A disproved finding is dropped, so a kept finding carries only these two. */
|
|
54
|
+
export type Proof =
|
|
55
|
+
| { status: 'reproduced'; reason: string; repro: Repro }
|
|
56
|
+
| { status: 'not-proven'; reason: string }
|
|
57
|
+
|
|
45
58
|
export type ReviewFinding = {
|
|
46
59
|
id: string
|
|
47
60
|
file: string
|
|
@@ -62,6 +75,8 @@ export type ReviewFinding = {
|
|
|
62
75
|
onChangedLine?: boolean | null
|
|
63
76
|
/** Verbatim code the reviewer quoted as grounds for the finding. */
|
|
64
77
|
evidence?: string
|
|
78
|
+
/** Set by the proof stage on the findings it tried to reproduce. */
|
|
79
|
+
proof?: Proof
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
const IMPACT_SYNONYMS: Record<string, Impact> = {
|
|
@@ -262,6 +277,29 @@ export function looksLikeProse(body: string): boolean {
|
|
|
262
277
|
return false
|
|
263
278
|
}
|
|
264
279
|
|
|
280
|
+
export function isRepro(raw: unknown): raw is Repro {
|
|
281
|
+
if (!raw || typeof raw !== 'object') return false
|
|
282
|
+
const r = raw as Record<string, unknown>
|
|
283
|
+
return [r.path, r.command, r.source, r.output].every((v) => typeof v === 'string' && v !== '')
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function parseProof(raw: unknown): Proof {
|
|
287
|
+
if (!raw || typeof raw !== 'object') throw new Error('Finding.proof must be an object')
|
|
288
|
+
const p = raw as Record<string, unknown>
|
|
289
|
+
if (typeof p.reason !== 'string' || p.reason === '') {
|
|
290
|
+
throw new Error('Finding.proof.reason must be a non-empty string')
|
|
291
|
+
}
|
|
292
|
+
if (p.status === 'not-proven') return { status: 'not-proven', reason: p.reason }
|
|
293
|
+
if (p.status !== 'reproduced') {
|
|
294
|
+
throw new Error('Finding.proof.status must be reproduced or not-proven')
|
|
295
|
+
}
|
|
296
|
+
if (!isRepro(p.repro)) {
|
|
297
|
+
throw new Error('Finding.proof.repro needs path, command, source and output')
|
|
298
|
+
}
|
|
299
|
+
const { path, command, source, output } = p.repro
|
|
300
|
+
return { status: 'reproduced', reason: p.reason, repro: { path, command, source, output } }
|
|
301
|
+
}
|
|
302
|
+
|
|
265
303
|
export function parseFinding(raw: unknown): ReviewFinding {
|
|
266
304
|
if (!raw || typeof raw !== 'object') {
|
|
267
305
|
throw new Error(`Finding must be an object, got ${typeof raw}`)
|
|
@@ -306,6 +344,7 @@ export function parseFinding(raw: unknown): ReviewFinding {
|
|
|
306
344
|
? { onChangedLine: r.onChangedLine as boolean | null }
|
|
307
345
|
: {}),
|
|
308
346
|
...(typeof r.evidence === 'string' && r.evidence.length > 0 ? { evidence: r.evidence } : {}),
|
|
347
|
+
...(r.proof !== undefined ? { proof: parseProof(r.proof) } : {}),
|
|
309
348
|
}
|
|
310
349
|
}
|
|
311
350
|
|
package/skills/magpie/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magpie",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Interactive PR review pipeline. Runs five parallel specialist subagents (security, bugs, performance, code-smells, architecture) plus a once-per-PR checks pass that finds tests and controls able to pass without proving their purpose, drops findings whose quoted evidence is not in the code, dedupes, runs critic subagents that re-check each finding against the worktree and set its risk label, peer-reviews via codex exec (falling back to a Claude second opinion when codex is unavailable), and serves an interactive HTML report for selecting or dismissing findings to post via gh, recording the outcome per finding as labels. Bundles a Bun CLI put on PATH by the skill's postinstall step under a bunx install, or by the plugin's bin/ under a Claude Code plugin install. Use when the user asks to review a GitHub pull request. Local mode (setup --local) runs the same pipeline on the current branch's diff from its merge base, uncommitted and untracked work included, against the caller's live tree, with no server or posting, and hands the ranked findings back in the terminal for an implementing or orchestrating agent to address. Splits oversized diffs into budgeted shards and rebuilds the diff from the local clone when gh pr diff refuses it.",
|
|
5
5
|
"author": "iceinvein",
|
|
6
6
|
"type": "prompt",
|
|
@@ -1785,3 +1785,33 @@ code.inline-code {
|
|
|
1785
1785
|
color: var(--accent);
|
|
1786
1786
|
text-decoration: none;
|
|
1787
1787
|
}
|
|
1788
|
+
|
|
1789
|
+
.proof-chip {
|
|
1790
|
+
font-family: var(--font-mono);
|
|
1791
|
+
font-size: 10px;
|
|
1792
|
+
font-weight: 700;
|
|
1793
|
+
padding: 2px 6px;
|
|
1794
|
+
border-radius: 999px;
|
|
1795
|
+
flex-shrink: 0;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
.proof-chip.reproduced {
|
|
1799
|
+
background: var(--badge-posted-bg);
|
|
1800
|
+
color: var(--badge-posted-ink);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
.proof-chip.not-proven {
|
|
1804
|
+
background: var(--card-inset);
|
|
1805
|
+
color: var(--ink-muted);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
pre.repro {
|
|
1809
|
+
font-family: var(--font-mono);
|
|
1810
|
+
font-size: 12px;
|
|
1811
|
+
background: var(--card-inset);
|
|
1812
|
+
border-radius: var(--radius-sm);
|
|
1813
|
+
padding: 6px 8px;
|
|
1814
|
+
margin: 4px 0 0;
|
|
1815
|
+
white-space: pre-wrap;
|
|
1816
|
+
overflow-x: auto;
|
|
1817
|
+
}
|
|
@@ -862,11 +862,13 @@ write_state() {
|
|
|
862
862
|
# their own task at the same moment. A flip reads the whole file, edits it with
|
|
863
863
|
# jq and writes it back, so unserialised the later write is built on a snapshot
|
|
864
864
|
# taken before the earlier one landed and drops that row without saying so.
|
|
865
|
-
#
|
|
866
|
-
#
|
|
867
|
-
#
|
|
865
|
+
# The lock is a file created with bash's noclobber, which bash opens itself with
|
|
866
|
+
# O_CREAT|O_EXCL, so exactly one racer gets it on every platform. Not mkdir:
|
|
867
|
+
# Ubuntu 26.04's uutils mkdir reports success to several callers racing on one
|
|
868
|
+
# path. flock is Linux only. Reads do not take the lock: write_state installs
|
|
869
|
+
# through a rename, so a reader sees either the whole old file or the whole new one.
|
|
868
870
|
#
|
|
869
|
-
# The holder's pid
|
|
871
|
+
# The holder's pid is the file's content so a killed run cannot wedge every
|
|
870
872
|
# later one. A lock whose holder is gone is broken rather than waited out, and
|
|
871
873
|
# one whose holder is alive is waited on for a bounded time and then reported,
|
|
872
874
|
# because a command that hangs in a status bar is worse than one that fails.
|
|
@@ -874,18 +876,36 @@ write_state() {
|
|
|
874
876
|
LOCKS_TAKEN=()
|
|
875
877
|
release_lock() {
|
|
876
878
|
local l
|
|
877
|
-
for l in "${LOCKS_TAKEN[@]+"${LOCKS_TAKEN[@]}"}"; do rm -
|
|
879
|
+
for l in "${LOCKS_TAKEN[@]+"${LOCKS_TAKEN[@]}"}"; do rm -f "$l"; done
|
|
878
880
|
LOCKS_TAKEN=()
|
|
879
881
|
}
|
|
880
882
|
|
|
883
|
+
# Every waiter reads the holder's pid while it holds, so when it exits they all
|
|
884
|
+
# see it dead at once. Each removing the lock on that stale read would remove
|
|
885
|
+
# the lock the next command had just taken, leaving two writers. Breaking
|
|
886
|
+
# is serialised instead, and the pid is read again once its process is known
|
|
887
|
+
# dead: the holder may have released and a new command taken the lock between
|
|
888
|
+
# the first read and the check. A dead holder cannot remove its own lock,
|
|
889
|
+
# so the same pid on the second read means the lock is still the stale one.
|
|
890
|
+
# A file read empty is a holder between creating it and writing its pid, alive.
|
|
891
|
+
break_stale_lock() { # <lock path>
|
|
892
|
+
local lock="$1" holder broke=1
|
|
893
|
+
(set -o noclobber; : >"$lock.break") 2>/dev/null || return 1
|
|
894
|
+
holder="$(cat "$lock" 2>/dev/null)"
|
|
895
|
+
if [ -n "$holder" ] && ! kill -0 "$holder" 2>/dev/null \
|
|
896
|
+
&& [ "$(cat "$lock" 2>/dev/null)" = "$holder" ]; then
|
|
897
|
+
rm -f "$lock"
|
|
898
|
+
broke=0
|
|
899
|
+
fi
|
|
900
|
+
rm -f "$lock.break"
|
|
901
|
+
return "$broke"
|
|
902
|
+
}
|
|
903
|
+
|
|
881
904
|
take_lock() { # [<lock path>, default the run's own]
|
|
882
|
-
local lock="${1:-$LOCK}" waited=0
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
rm -rf "$lock"
|
|
887
|
-
continue
|
|
888
|
-
fi
|
|
905
|
+
local lock="${1:-$LOCK}" waited=0
|
|
906
|
+
# $$ in the subshell is still this command's pid.
|
|
907
|
+
while ! (set -o noclobber; printf '%s\n' "$$" >"$lock") 2>/dev/null; do
|
|
908
|
+
break_stale_lock "$lock" && continue
|
|
889
909
|
if [ "$waited" -ge 100 ]; then
|
|
890
910
|
err "another sluice command has held $lock for 10s; remove it if nothing is running"
|
|
891
911
|
exit 1
|
|
@@ -893,7 +913,6 @@ take_lock() { # [<lock path>, default the run's own]
|
|
|
893
913
|
sleep 0.1
|
|
894
914
|
waited=$((waited + 1))
|
|
895
915
|
done
|
|
896
|
-
printf '%s\n' "$$" >"$lock/pid" 2>/dev/null || true
|
|
897
916
|
LOCKS_TAKEN+=("$lock")
|
|
898
917
|
trap release_lock EXIT INT TERM
|
|
899
918
|
}
|
package/skills/sluice/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sluice",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.2",
|
|
4
4
|
"description": "Routes work by change shape into four channels (bypass, fast, main, deep) and applies only the rules each channel needs, so a one-line fix does not pay the cost of a multi-subsystem build. Carries nine rules as one-liners in the router and the full treatment in references read only on friction. Deep runs read the repo before designing, map the spec's intent to its decisions, and check spec against intent and plan against spec with a fresh reader, with plan.sh validate enforcing that every decision is covered. Checks the finished plan with plan.sh validate rather than trusting it to memory, seeds the run state from it, keeps a deep run's task breakdown in .sluice/run.json so a statusline segment, one status command and a SessionStart hook can answer where the run is (the hook prints a live run at every session start, compaction included), lists every live deep run and routed fast or main session on the machine in one board.sh view (drawn once, watched, or as JSON), and closes each run with a ledger read out of the session transcript: elapsed, tools, tokens, and what each dispatched agent cost where the transcript recorded it. Two Stop hooks hold a handback: one refuses a tree that moved since its last recorded check, the other refuses a changed existing test not proven to fail on the session's starting code (or recorded with a reason). Claude Code only; stands down where the superpowers pipeline governs the repo.",
|
|
5
5
|
"author": "iceinvein",
|
|
6
6
|
"type": "prompt",
|