@iceinvein/agent-skills 0.22.0 → 0.23.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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/skills/bug-hunter/SKILL.md +5 -0
- package/skills/bug-hunter/evals/README.md +6 -1
- package/skills/bug-hunter/evals/callers-tree-unchanged/graders/output-ignores-itself.md +11 -0
- package/skills/bug-hunter/evals/rules.json +8 -0
- package/skills/bug-hunter/skill.json +1 -1
- package/skills/index.json +4 -4
- package/skills/magpie/README.md +5 -3
- package/skills/magpie/SKILL.md +40 -15
- package/skills/magpie/bin/magpie.ts +83 -7
- package/skills/magpie/evals/README.md +7 -4
- package/skills/magpie/evals/local-mode-hands-back/case.yaml +4 -0
- package/skills/magpie/evals/local-mode-hands-back/fixture.sh +300 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/branch-was-reachable.md +5 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/cleanup-after-handoff.md +6 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/handoff-was-run.md +6 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/nothing-served-or-posted.md +7 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/reply-lists-both-findings.md +10 -0
- package/skills/magpie/evals/local-mode-hands-back/graders/skill-fired.md +5 -0
- package/skills/magpie/evals/local-mode-hands-back/prompt.md +11 -0
- package/skills/magpie/evals/quality/README.md +73 -2
- package/skills/magpie/evals/quality/__tests__/recall.test.ts +234 -0
- package/skills/magpie/evals/quality/__tests__/replay-full.test.ts +164 -0
- package/skills/magpie/evals/quality/corpus.ts +4 -1
- package/skills/magpie/evals/quality/recall.ts +249 -0
- package/skills/magpie/evals/quality/replay-critic.ts +3 -0
- package/skills/magpie/evals/quality/replay-full.ts +168 -6
- package/skills/magpie/evals/rules.json +9 -1
- package/skills/magpie/references/checks.md +78 -0
- package/skills/magpie/references/peer-review.md +7 -0
- package/skills/magpie/references/scout.md +12 -5
- package/skills/magpie/references/specialists.md +10 -8
- package/skills/magpie/scripts/__tests__/cleanup-cmd.test.ts +94 -0
- package/skills/magpie/scripts/__tests__/critic-cmd.test.ts +18 -0
- package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +80 -1
- package/skills/magpie/scripts/__tests__/handoff-cmd.test.ts +319 -0
- package/skills/magpie/scripts/__tests__/housekeeping-cmd.test.ts +14 -0
- package/skills/magpie/scripts/__tests__/local-diff.test.ts +275 -0
- package/skills/magpie/scripts/__tests__/pipeline-e2e.test.ts +1 -0
- package/skills/magpie/scripts/__tests__/resolve-run.test.ts +28 -0
- package/skills/magpie/scripts/__tests__/server.test.ts +30 -1
- package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +324 -1
- package/skills/magpie/scripts/__tests__/skill-lint.test.ts +8 -5
- package/skills/magpie/scripts/__tests__/status-cmd.test.ts +44 -0
- package/skills/magpie/scripts/__tests__/target.test.ts +84 -0
- package/skills/magpie/scripts/__tests__/types.test.ts +2 -1
- package/skills/magpie/scripts/cleanup-cmd.ts +5 -1
- package/skills/magpie/scripts/critic-cmd.ts +2 -1
- package/skills/magpie/scripts/dedupe-cmd.ts +15 -5
- package/skills/magpie/scripts/handoff-cmd.ts +132 -0
- package/skills/magpie/scripts/housekeeping-cmd.ts +7 -5
- package/skills/magpie/scripts/local-diff.ts +228 -0
- package/skills/magpie/scripts/post-cmd.ts +1 -0
- package/skills/magpie/scripts/render-annotation.ts +1 -0
- package/skills/magpie/scripts/resolve-run.ts +17 -3
- package/skills/magpie/scripts/server.ts +9 -1
- package/skills/magpie/scripts/setup-cmd.ts +241 -49
- package/skills/magpie/scripts/status-cmd.ts +22 -6
- package/skills/magpie/scripts/target.ts +56 -0
- package/skills/magpie/scripts/types.ts +1 -0
- package/skills/magpie/skill.json +2 -2
- package/skills/sluice/SKILL.md +3 -1
- package/skills/sluice/evals/README.md +10 -2
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/case.yaml +4 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/fixture.sh +108 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/deploy-names-its-steps.md +9 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/kept-status-by-changing-code.md +12 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/status-test-unchanged.md +6 -0
- package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/prompt.md +11 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/case.yaml +4 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/fixture.sh +73 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/proved-after-the-last-edit.md +18 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/sluice-fired.md +5 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/test-expects-the-new-line.md +5 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/updated-without-asking.md +11 -0
- package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/prompt.md +11 -0
- package/skills/sluice/evals/rules.json +11 -0
- package/skills/sluice/references/review.md +6 -4
- package/skills/sluice/references/test-first.md +24 -4
- package/skills/sluice/scripts/content-digest.sh +1 -1
- package/skills/sluice/scripts/run-stats.sh +14 -0
- package/skills/sluice/scripts/session-start.sh +44 -2
- package/skills/sluice/scripts/status.sh +293 -0
- package/skills/sluice/scripts/stop-guard.sh +80 -6
- package/skills/sluice/scripts/test-changes.sh +135 -0
- package/skills/sluice/skill.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, utimes, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { type ServerHandle, startServer } from '../server.ts'
|
|
@@ -37,6 +37,35 @@ test('serves the newest HTML file verbatim (HTML is self-contained at render tim
|
|
|
37
37
|
expect(html).toContain('inline-helper')
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
+
/** Writes a screen page with an explicit mtime, in seconds since the epoch. */
|
|
41
|
+
async function writeScreen(name: string, body: string, mtime: number): Promise<void> {
|
|
42
|
+
const path = join(runDir, 'screen', name)
|
|
43
|
+
await writeFile(path, body)
|
|
44
|
+
await utimes(path, mtime, mtime)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test('a progress page rendered after the findings page does not replace it', async () => {
|
|
48
|
+
await writeScreen('findings.html', '<h1>Findings</h1>', 1000)
|
|
49
|
+
await writeScreen('progress-v9.html', '<h1>Progress, report done</h1>', 2000)
|
|
50
|
+
server = await startServer({ runDir, idleMs: 60_000 })
|
|
51
|
+
expect(await fetchHtml(server)).toContain('<h1>Findings</h1>')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('before any findings page exists, the newest progress page is served', async () => {
|
|
55
|
+
await writeScreen('progress.html', '<h1>Progress one</h1>', 1000)
|
|
56
|
+
await writeScreen('progress-v2.html', '<h1>Progress two</h1>', 2000)
|
|
57
|
+
server = await startServer({ runDir, idleMs: 60_000 })
|
|
58
|
+
expect(await fetchHtml(server)).toContain('<h1>Progress two</h1>')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('the newest findings version is served when several exist', async () => {
|
|
62
|
+
await writeScreen('findings.html', '<h1>Findings one</h1>', 1000)
|
|
63
|
+
await writeScreen('findings-v2.html', '<h1>Findings two</h1>', 2000)
|
|
64
|
+
await writeScreen('progress-v5.html', '<h1>Progress</h1>', 3000)
|
|
65
|
+
server = await startServer({ runDir, idleMs: 60_000 })
|
|
66
|
+
expect(await fetchHtml(server)).toContain('<h1>Findings two</h1>')
|
|
67
|
+
})
|
|
68
|
+
|
|
40
69
|
test('POST /events appends a JSON line to state/events', async () => {
|
|
41
70
|
await writeFile(join(runDir, 'screen', 'a.html'), '<h1>X</h1>')
|
|
42
71
|
server = await startServer({ runDir, idleMs: 60_000 })
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
mkdir,
|
|
4
|
+
mkdtemp,
|
|
5
|
+
readdir,
|
|
6
|
+
readFile,
|
|
7
|
+
realpath,
|
|
8
|
+
rm,
|
|
9
|
+
symlink,
|
|
10
|
+
writeFile,
|
|
11
|
+
} from 'node:fs/promises'
|
|
3
12
|
import { tmpdir } from 'node:os'
|
|
4
13
|
import { join } from 'node:path'
|
|
14
|
+
import { diffSha256, localBranchDiff } from '../local-diff.ts'
|
|
5
15
|
import { runSetup } from '../setup-cmd.ts'
|
|
6
16
|
|
|
7
17
|
const FAKE_GH = new URL('../../fixtures/fake-gh.sh', import.meta.url).pathname
|
|
@@ -42,6 +52,7 @@ afterEach(async () => {
|
|
|
42
52
|
|
|
43
53
|
test('runSetup completes happy path: writes pr.json, diff.patch, worktree', async () => {
|
|
44
54
|
const exit = await runSetup({
|
|
55
|
+
mode: 'pr',
|
|
45
56
|
runDir,
|
|
46
57
|
prNumber: 1234,
|
|
47
58
|
repoPath: repo,
|
|
@@ -52,10 +63,13 @@ test('runSetup completes happy path: writes pr.json, diff.patch, worktree', asyn
|
|
|
52
63
|
expect(contents).toContain('pr.json')
|
|
53
64
|
expect(contents).toContain('diff.patch')
|
|
54
65
|
expect(contents).toContain('worktree')
|
|
66
|
+
const target = JSON.parse(await readFile(join(runDir, 'target.json'), 'utf8'))
|
|
67
|
+
expect(target).toEqual({ mode: 'pr', root: join(runDir, 'worktree') })
|
|
55
68
|
})
|
|
56
69
|
|
|
57
70
|
test('runSetup with missing dep returns non-zero and cleans up', async () => {
|
|
58
71
|
const exit = await runSetup({
|
|
72
|
+
mode: 'pr',
|
|
59
73
|
runDir,
|
|
60
74
|
prNumber: 1234,
|
|
61
75
|
repoPath: repo,
|
|
@@ -68,6 +82,7 @@ test('runSetup with missing dep returns non-zero and cleans up', async () => {
|
|
|
68
82
|
|
|
69
83
|
test('runSetup filters excluded files by default and preserves raw diff', async () => {
|
|
70
84
|
const exit = await runSetup({
|
|
85
|
+
mode: 'pr',
|
|
71
86
|
runDir,
|
|
72
87
|
prNumber: 1234,
|
|
73
88
|
repoPath: repo,
|
|
@@ -93,6 +108,7 @@ test('runSetup filters excluded files by default and preserves raw diff', async
|
|
|
93
108
|
|
|
94
109
|
test('runSetup with no excludable files does not write filter sidecars', async () => {
|
|
95
110
|
const exit = await runSetup({
|
|
111
|
+
mode: 'pr',
|
|
96
112
|
runDir,
|
|
97
113
|
prNumber: 1234,
|
|
98
114
|
repoPath: repo,
|
|
@@ -107,6 +123,7 @@ test('runSetup with no excludable files does not write filter sidecars', async (
|
|
|
107
123
|
test('runSetup honors .magpie.json useDefaults=false', async () => {
|
|
108
124
|
await writeFile(join(repo, '.magpie.json'), JSON.stringify({ useDefaults: false, exclude: [] }))
|
|
109
125
|
const exit = await runSetup({
|
|
126
|
+
mode: 'pr',
|
|
110
127
|
runDir,
|
|
111
128
|
prNumber: 1234,
|
|
112
129
|
repoPath: repo,
|
|
@@ -129,6 +146,7 @@ test('runSetup writes incremental.json when a prior run for the same PR exists',
|
|
|
129
146
|
process.env.MAGPIE_HOME = magpieHome
|
|
130
147
|
try {
|
|
131
148
|
const exit = await runSetup({
|
|
149
|
+
mode: 'pr',
|
|
132
150
|
runDir,
|
|
133
151
|
prNumber: 1234,
|
|
134
152
|
repoPath: repo,
|
|
@@ -152,6 +170,7 @@ test('runSetup omits incremental.json when no prior run exists', async () => {
|
|
|
152
170
|
process.env.MAGPIE_HOME = magpieHome
|
|
153
171
|
try {
|
|
154
172
|
const exit = await runSetup({
|
|
173
|
+
mode: 'pr',
|
|
155
174
|
runDir,
|
|
156
175
|
prNumber: 1234,
|
|
157
176
|
repoPath: repo,
|
|
@@ -169,6 +188,7 @@ test('runSetup omits incremental.json when no prior run exists', async () => {
|
|
|
169
188
|
|
|
170
189
|
test('runSetup with failing gh removes any partial state', async () => {
|
|
171
190
|
const exit = await runSetup({
|
|
191
|
+
mode: 'pr',
|
|
172
192
|
runDir,
|
|
173
193
|
prNumber: 1234,
|
|
174
194
|
repoPath: repo,
|
|
@@ -178,10 +198,12 @@ test('runSetup with failing gh removes any partial state', async () => {
|
|
|
178
198
|
const contents = await readdir(runDir).catch(() => [])
|
|
179
199
|
expect(contents).not.toContain('worktree')
|
|
180
200
|
expect(contents).not.toContain('pr.json')
|
|
201
|
+
expect(contents).not.toContain('target.json')
|
|
181
202
|
})
|
|
182
203
|
|
|
183
204
|
test('runSetup shards the filtered diff and logs the result', async () => {
|
|
184
205
|
const exit = await runSetup({
|
|
206
|
+
mode: 'pr',
|
|
185
207
|
runDir,
|
|
186
208
|
prNumber: 1234,
|
|
187
209
|
repoPath: repo,
|
|
@@ -210,6 +232,7 @@ test('runSetup cleans up a populated shards/ when worktree creation fails after
|
|
|
210
232
|
await writeFile(join(runDir, 'worktree', 'occupied'), 'x')
|
|
211
233
|
|
|
212
234
|
const exit = await runSetup({
|
|
235
|
+
mode: 'pr',
|
|
213
236
|
runDir,
|
|
214
237
|
prNumber: 1234,
|
|
215
238
|
repoPath: repo,
|
|
@@ -230,6 +253,7 @@ test('runSetup cleans up a populated shards/ when worktree creation fails after
|
|
|
230
253
|
const contents = await readdir(runDir).catch(() => [])
|
|
231
254
|
expect(contents).not.toContain('shards')
|
|
232
255
|
expect(contents).not.toContain('worktree')
|
|
256
|
+
expect(contents).not.toContain('target.json')
|
|
233
257
|
})
|
|
234
258
|
|
|
235
259
|
test('runSetup treats a shardDiff throw as a hard stop: logs, cleans up, exits 6', async () => {
|
|
@@ -242,6 +266,7 @@ test('runSetup treats a shardDiff throw as a hard stop: logs, cleans up, exits 6
|
|
|
242
266
|
await writeFile(join(runDir, 'shards'), 'not a directory')
|
|
243
267
|
|
|
244
268
|
const exit = await runSetup({
|
|
269
|
+
mode: 'pr',
|
|
245
270
|
runDir,
|
|
246
271
|
prNumber: 1234,
|
|
247
272
|
repoPath: repo,
|
|
@@ -261,3 +286,301 @@ test('runSetup treats a shardDiff throw as a hard stop: logs, cleans up, exits 6
|
|
|
261
286
|
const contents = await readdir(runDir).catch(() => [])
|
|
262
287
|
expect(contents.filter((c) => c !== 'log.jsonl')).toHaveLength(0)
|
|
263
288
|
})
|
|
289
|
+
|
|
290
|
+
const LOCAL_DEPS = { bun: 'bun', gh: FAKE_GH, codex: 'echo', git: 'git' }
|
|
291
|
+
|
|
292
|
+
async function prepareLocalBranch(): Promise<void> {
|
|
293
|
+
await sh(repo, 'git', 'checkout', '-q', 'feature-x')
|
|
294
|
+
await writeFile(join(repo, 'b.txt'), 'two\n')
|
|
295
|
+
await sh(repo, 'git', 'add', 'b.txt')
|
|
296
|
+
await sh(repo, 'git', 'commit', '-q', '-m', 'add b')
|
|
297
|
+
await writeFile(join(repo, 'a.txt'), 'one\nedited\n')
|
|
298
|
+
await writeFile(join(repo, 'new.txt'), 'untracked\n')
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function setupCapturingStderr(
|
|
302
|
+
input: Parameters<typeof runSetup>[0],
|
|
303
|
+
): Promise<{ exit: number; stderr: string }> {
|
|
304
|
+
const errs: string[] = []
|
|
305
|
+
const origWrite = process.stderr.write.bind(process.stderr)
|
|
306
|
+
process.stderr.write = ((s: string) => {
|
|
307
|
+
errs.push(s)
|
|
308
|
+
return true
|
|
309
|
+
}) as typeof process.stderr.write
|
|
310
|
+
try {
|
|
311
|
+
const exit = await runSetup(input)
|
|
312
|
+
return { exit, stderr: errs.join('') }
|
|
313
|
+
} finally {
|
|
314
|
+
process.stderr.write = origWrite
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function readLog(): Promise<Array<Record<string, unknown>>> {
|
|
319
|
+
const log = await readFile(join(runDir, 'log.jsonl'), 'utf8')
|
|
320
|
+
return log
|
|
321
|
+
.split('\n')
|
|
322
|
+
.filter(Boolean)
|
|
323
|
+
.map((l) => JSON.parse(l))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
test('local setup writes the diff, its sidecars and a target rooted at the repo', async () => {
|
|
327
|
+
await prepareLocalBranch()
|
|
328
|
+
const exit = await runSetup({
|
|
329
|
+
mode: 'local',
|
|
330
|
+
runDir,
|
|
331
|
+
repoPath: repo,
|
|
332
|
+
baseRef: 'main',
|
|
333
|
+
deps: LOCAL_DEPS,
|
|
334
|
+
})
|
|
335
|
+
expect(exit).toBe(0)
|
|
336
|
+
const contents = await readdir(runDir)
|
|
337
|
+
expect(contents).not.toContain('pr.json')
|
|
338
|
+
expect(contents).not.toContain('incremental.json')
|
|
339
|
+
expect(contents).not.toContain('worktree')
|
|
340
|
+
|
|
341
|
+
const diff = await readFile(join(runDir, 'diff.patch'), 'utf8')
|
|
342
|
+
expect(diff).toContain('+++ b/b.txt')
|
|
343
|
+
expect(diff).toContain('+edited')
|
|
344
|
+
expect(diff).toContain('+++ b/new.txt')
|
|
345
|
+
|
|
346
|
+
const source = JSON.parse(await readFile(join(runDir, 'diff-source.json'), 'utf8'))
|
|
347
|
+
expect(source.source).toBe('local')
|
|
348
|
+
expect(source.baseRef).toBe('main')
|
|
349
|
+
expect(source.mergeBase).toMatch(/^[0-9a-f]{40}$/)
|
|
350
|
+
expect(source.head).toMatch(/^[0-9a-f]{40}$/)
|
|
351
|
+
expect(source.diffSha256).toMatch(/^[0-9a-f]{64}$/)
|
|
352
|
+
|
|
353
|
+
const meta = JSON.parse(await readFile(join(runDir, 'local.json'), 'utf8'))
|
|
354
|
+
expect(meta.branch).toBe('feature-x')
|
|
355
|
+
expect(meta.baseRef).toBe('main')
|
|
356
|
+
expect(meta.dirty).toBe(true)
|
|
357
|
+
expect(meta.intentFile).toBeNull()
|
|
358
|
+
|
|
359
|
+
const target = JSON.parse(await readFile(join(runDir, 'target.json'), 'utf8'))
|
|
360
|
+
expect(target).toEqual({ mode: 'local', root: repo })
|
|
361
|
+
|
|
362
|
+
expect((await readLog()).find((e) => e.stage === 'setup')).toMatchObject({
|
|
363
|
+
status: 'done',
|
|
364
|
+
root: repo,
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
test('local setup hashes the raw diff, not the path-filtered patch', async () => {
|
|
369
|
+
await sh(repo, 'git', 'checkout', '-q', 'feature-x')
|
|
370
|
+
await writeFile(join(repo, 'bun.lock'), 'lock\n')
|
|
371
|
+
await writeFile(join(repo, 'src.ts'), 'export const x = 1\n')
|
|
372
|
+
await sh(repo, 'git', 'add', '.')
|
|
373
|
+
await sh(repo, 'git', 'commit', '-q', '-m', 'lock and src')
|
|
374
|
+
|
|
375
|
+
const exit = await runSetup({
|
|
376
|
+
mode: 'local',
|
|
377
|
+
runDir,
|
|
378
|
+
repoPath: repo,
|
|
379
|
+
baseRef: 'main',
|
|
380
|
+
deps: LOCAL_DEPS,
|
|
381
|
+
})
|
|
382
|
+
expect(exit).toBe(0)
|
|
383
|
+
const filtered = await readFile(join(runDir, 'diff.patch'), 'utf8')
|
|
384
|
+
expect(filtered).not.toContain('bun.lock')
|
|
385
|
+
|
|
386
|
+
const raw = await localBranchDiff({ gitBin: 'git', repoPath: repo, baseRef: 'main' })
|
|
387
|
+
if (!raw.ok) throw new Error(raw.error)
|
|
388
|
+
const source = JSON.parse(await readFile(join(runDir, 'diff-source.json'), 'utf8'))
|
|
389
|
+
expect(source.diffSha256).toBe(diffSha256(raw.diff))
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
test('local setup with an unresolvable base logs the error and keeps only the log', async () => {
|
|
393
|
+
await prepareLocalBranch()
|
|
394
|
+
const exit = await runSetup({
|
|
395
|
+
mode: 'local',
|
|
396
|
+
runDir,
|
|
397
|
+
repoPath: repo,
|
|
398
|
+
baseRef: 'nope',
|
|
399
|
+
deps: LOCAL_DEPS,
|
|
400
|
+
})
|
|
401
|
+
expect(exit).toBe(4)
|
|
402
|
+
expect((await readLog()).find((e) => e.stage === 'local-diff')).toMatchObject({
|
|
403
|
+
status: 'error',
|
|
404
|
+
})
|
|
405
|
+
expect(await readdir(runDir)).toEqual(['log.jsonl'])
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
test('local setup with a missing intent file fails naming the path', async () => {
|
|
409
|
+
await prepareLocalBranch()
|
|
410
|
+
const missing = join(runDir, 'no-such-intent.md')
|
|
411
|
+
const { exit, stderr } = await setupCapturingStderr({
|
|
412
|
+
mode: 'local',
|
|
413
|
+
runDir,
|
|
414
|
+
repoPath: repo,
|
|
415
|
+
baseRef: 'main',
|
|
416
|
+
intentFile: missing,
|
|
417
|
+
deps: LOCAL_DEPS,
|
|
418
|
+
})
|
|
419
|
+
expect(exit).not.toBe(0)
|
|
420
|
+
expect(stderr).toContain(`magpie: intent file ${missing} does not exist`)
|
|
421
|
+
expect((await readLog()).find((e) => e.stage === 'local-diff')).toMatchObject({
|
|
422
|
+
status: 'error',
|
|
423
|
+
})
|
|
424
|
+
expect(await readdir(runDir)).toEqual(['log.jsonl'])
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
test('local setup with an intent path that is a directory fails naming the path', async () => {
|
|
428
|
+
await prepareLocalBranch()
|
|
429
|
+
const intentDir = await mkdtemp(join(tmpdir(), 'magpie-intent-dir-'))
|
|
430
|
+
try {
|
|
431
|
+
const { exit, stderr } = await setupCapturingStderr({
|
|
432
|
+
mode: 'local',
|
|
433
|
+
runDir,
|
|
434
|
+
repoPath: repo,
|
|
435
|
+
baseRef: 'main',
|
|
436
|
+
intentFile: intentDir,
|
|
437
|
+
deps: LOCAL_DEPS,
|
|
438
|
+
})
|
|
439
|
+
expect(exit).not.toBe(0)
|
|
440
|
+
expect(stderr).toContain(`magpie: intent file ${intentDir} is not a file`)
|
|
441
|
+
expect((await readLog()).find((e) => e.stage === 'local-diff')).toMatchObject({
|
|
442
|
+
status: 'error',
|
|
443
|
+
})
|
|
444
|
+
expect(await readdir(runDir)).toEqual(['log.jsonl'])
|
|
445
|
+
} finally {
|
|
446
|
+
await rm(intentDir, { recursive: true, force: true })
|
|
447
|
+
}
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
test('local setup refuses a run dir that is the repository and leaves it untouched', async () => {
|
|
451
|
+
await prepareLocalBranch()
|
|
452
|
+
await mkdir(join(repo, 'findings'))
|
|
453
|
+
await writeFile(join(repo, 'findings', 'user.txt'), 'mine\n')
|
|
454
|
+
await sh(repo, 'git', 'add', 'findings/user.txt')
|
|
455
|
+
await sh(repo, 'git', 'commit', '-q', '-m', 'user findings')
|
|
456
|
+
const before = (await readdir(repo)).sort()
|
|
457
|
+
|
|
458
|
+
const { exit, stderr } = await setupCapturingStderr({
|
|
459
|
+
mode: 'local',
|
|
460
|
+
runDir: repo,
|
|
461
|
+
repoPath: repo,
|
|
462
|
+
baseRef: 'nope',
|
|
463
|
+
deps: LOCAL_DEPS,
|
|
464
|
+
})
|
|
465
|
+
expect(exit).toBe(2)
|
|
466
|
+
expect(stderr).toContain(
|
|
467
|
+
`magpie: run dir ${repo} is inside the repository under review ${await realpath(repo)}\n`,
|
|
468
|
+
)
|
|
469
|
+
expect(await readFile(join(repo, 'findings', 'user.txt'), 'utf8')).toBe('mine\n')
|
|
470
|
+
expect((await readdir(repo)).sort()).toEqual(before)
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
test('local setup refuses a not-yet-created run dir under the repository and creates nothing', async () => {
|
|
474
|
+
await prepareLocalBranch()
|
|
475
|
+
const before = (await readdir(repo)).sort()
|
|
476
|
+
const nested = join(repo, 'runs', 'r1')
|
|
477
|
+
|
|
478
|
+
const { exit, stderr } = await setupCapturingStderr({
|
|
479
|
+
mode: 'local',
|
|
480
|
+
runDir: nested,
|
|
481
|
+
repoPath: repo,
|
|
482
|
+
baseRef: 'main',
|
|
483
|
+
deps: LOCAL_DEPS,
|
|
484
|
+
})
|
|
485
|
+
expect(exit).toBe(2)
|
|
486
|
+
expect(stderr).toContain(`magpie: run dir ${nested} is inside the repository under review`)
|
|
487
|
+
expect((await readdir(repo)).sort()).toEqual(before)
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
async function gitStatus(cwd: string): Promise<string> {
|
|
491
|
+
const proc = Bun.spawn(['git', 'status', '--porcelain'], { cwd, stdout: 'pipe', stderr: 'pipe' })
|
|
492
|
+
const out = await new Response(proc.stdout).text()
|
|
493
|
+
expect(await proc.exited).toBe(0)
|
|
494
|
+
return out
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
test('local setup with --repo at a subdirectory fails before writing a run dir into the repository', async () => {
|
|
498
|
+
await mkdir(join(repo, 'sub'))
|
|
499
|
+
await writeFile(join(repo, 'sub', 's.txt'), 's\n')
|
|
500
|
+
await sh(repo, 'git', 'add', '.')
|
|
501
|
+
await sh(repo, 'git', 'commit', '-q', '-m', 'sub')
|
|
502
|
+
const before = (await readdir(repo)).sort()
|
|
503
|
+
const statusBefore = await gitStatus(repo)
|
|
504
|
+
const sub = join(repo, 'sub')
|
|
505
|
+
|
|
506
|
+
const { exit, stderr } = await setupCapturingStderr({
|
|
507
|
+
mode: 'local',
|
|
508
|
+
runDir: join(repo, 'run'),
|
|
509
|
+
repoPath: sub,
|
|
510
|
+
baseRef: 'main',
|
|
511
|
+
deps: LOCAL_DEPS,
|
|
512
|
+
})
|
|
513
|
+
expect(exit).toBe(2)
|
|
514
|
+
expect(stderr).toContain(`magpie: --repo ${sub} is not the repository root`)
|
|
515
|
+
expect((await readdir(repo)).sort()).toEqual(before)
|
|
516
|
+
expect(await gitStatus(repo)).toBe(statusBefore)
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
test('local setup with a run dir that climbs out of a symlink into the repository leaves the repository unchanged', async () => {
|
|
520
|
+
await mkdir(join(repo, 'sub'))
|
|
521
|
+
await writeFile(join(repo, 'sub', 's.txt'), 's\n')
|
|
522
|
+
await sh(repo, 'git', 'add', '.')
|
|
523
|
+
await sh(repo, 'git', 'commit', '-q', '-m', 'sub')
|
|
524
|
+
await symlink(join(repo, 'sub'), join(runDir, 'L'))
|
|
525
|
+
const before = (await readdir(repo)).sort()
|
|
526
|
+
const statusBefore = await gitStatus(repo)
|
|
527
|
+
|
|
528
|
+
await setupCapturingStderr({
|
|
529
|
+
mode: 'local',
|
|
530
|
+
runDir: `${join(runDir, 'L')}/../run5`,
|
|
531
|
+
repoPath: repo,
|
|
532
|
+
baseRef: 'nope',
|
|
533
|
+
deps: LOCAL_DEPS,
|
|
534
|
+
})
|
|
535
|
+
expect((await readdir(repo)).sort()).toEqual(before)
|
|
536
|
+
expect(await gitStatus(repo)).toBe(statusBefore)
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
test('local setup with a --repo that does not exist fails with exit 2 and creates no run dir', async () => {
|
|
540
|
+
const missing = join(runDir, 'no-such-repo')
|
|
541
|
+
const nested = join(runDir, 'r1')
|
|
542
|
+
const { exit, stderr } = await setupCapturingStderr({
|
|
543
|
+
mode: 'local',
|
|
544
|
+
runDir: nested,
|
|
545
|
+
repoPath: missing,
|
|
546
|
+
baseRef: 'main',
|
|
547
|
+
deps: LOCAL_DEPS,
|
|
548
|
+
})
|
|
549
|
+
expect(exit).toBe(2)
|
|
550
|
+
expect(stderr).toBe(`magpie: --repo ${missing} does not exist\n`)
|
|
551
|
+
expect(await readdir(runDir)).toEqual([])
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
test('local setup with a --repo that is a file fails with exit 2 and creates no run dir', async () => {
|
|
555
|
+
const file = join(repo, 'a.txt')
|
|
556
|
+
const nested = join(runDir, 'r1')
|
|
557
|
+
const { exit, stderr } = await setupCapturingStderr({
|
|
558
|
+
mode: 'local',
|
|
559
|
+
runDir: nested,
|
|
560
|
+
repoPath: file,
|
|
561
|
+
baseRef: 'main',
|
|
562
|
+
deps: LOCAL_DEPS,
|
|
563
|
+
})
|
|
564
|
+
expect(exit).toBe(2)
|
|
565
|
+
expect(stderr).toBe(`magpie: --repo ${file} is not a directory\n`)
|
|
566
|
+
expect(await readdir(runDir)).toEqual([])
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
test('local setup with --repo outside any git repository fails with exit 2 and creates no run dir', async () => {
|
|
570
|
+
const notRepo = await mkdtemp(join(tmpdir(), 'magpie-setup-notrepo-'))
|
|
571
|
+
const nested = join(runDir, 'r1')
|
|
572
|
+
try {
|
|
573
|
+
const { exit, stderr } = await setupCapturingStderr({
|
|
574
|
+
mode: 'local',
|
|
575
|
+
runDir: nested,
|
|
576
|
+
repoPath: notRepo,
|
|
577
|
+
baseRef: 'main',
|
|
578
|
+
deps: LOCAL_DEPS,
|
|
579
|
+
})
|
|
580
|
+
expect(exit).toBe(2)
|
|
581
|
+
expect(stderr).toContain(`magpie: --repo ${notRepo} is not inside a git work tree: `)
|
|
582
|
+
expect(await readdir(runDir)).toEqual([])
|
|
583
|
+
} finally {
|
|
584
|
+
await rm(notRepo, { recursive: true, force: true })
|
|
585
|
+
}
|
|
586
|
+
})
|
|
@@ -180,7 +180,7 @@ test('references/scout.md holds the scout prompt and the brief contract', async
|
|
|
180
180
|
}
|
|
181
181
|
expect(text).toContain('brief: <N> changes, <K> watch items, <R> review rules')
|
|
182
182
|
expect(text).not.toContain('"subsystems"')
|
|
183
|
-
for (const ph of ['<<RUN_DIR>>', '<<
|
|
183
|
+
for (const ph of ['<<RUN_DIR>>', '<<WORKTREE>>', '<<TARGET>>']) {
|
|
184
184
|
expect(text).toContain(ph)
|
|
185
185
|
}
|
|
186
186
|
// watchItems are context for specialists, not findings in their own right.
|
|
@@ -219,8 +219,9 @@ test('SKILL.md no longer inlines the prompt bodies it moved out', async () => {
|
|
|
219
219
|
}
|
|
220
220
|
// The walkthrough is the always-read part; keep it small enough to be cheap.
|
|
221
221
|
// Raised from 2600 when the sharded-dispatch and fallback-diff prose was added:
|
|
222
|
-
// that's real, load-bearing procedure, not bloat.
|
|
223
|
-
|
|
222
|
+
// that's real, load-bearing procedure, not bloat. Raised from 3000 for the
|
|
223
|
+
// local-mode section, which the walkthrough already sat 3 words under.
|
|
224
|
+
expect(text.split(/\s+/).length).toBeLessThan(3500)
|
|
224
225
|
})
|
|
225
226
|
|
|
226
227
|
test('styles.css declares a prefers-color-scheme:dark block that overrides core tokens', async () => {
|
|
@@ -413,11 +414,13 @@ test('the stage-4 gate offers only options the pipeline can carry out', async ()
|
|
|
413
414
|
expect(gate).toContain('--max-files')
|
|
414
415
|
// Option 3 must name a mechanism that exists. There is no unreviewed marker in
|
|
415
416
|
// the report, so the record is a diagnostic log entry, on a stage outside
|
|
416
|
-
// status-cmd's
|
|
417
|
-
|
|
417
|
+
// status-cmd's ladders (a local run can hit the same gate) and never
|
|
418
|
+
// `status: error`.
|
|
419
|
+
const { ORDER, LOCAL_ORDER } = await import('../status-cmd.ts')
|
|
418
420
|
const stage = gate.match(/\{stage: ([a-z-]+), status: ([a-z-]+)/)
|
|
419
421
|
expect(stage?.[1]).toBeDefined()
|
|
420
422
|
expect(ORDER as readonly string[]).not.toContain(stage?.[1])
|
|
423
|
+
expect(LOCAL_ORDER as readonly string[]).not.toContain(stage?.[1])
|
|
421
424
|
expect(stage?.[2]).not.toBe('error')
|
|
422
425
|
expect(gate).toMatch(/skipped/)
|
|
423
426
|
})
|
|
@@ -53,3 +53,47 @@ test('error stage halts progression', async () => {
|
|
|
53
53
|
expect(result.lastCompleted).toBe('setup')
|
|
54
54
|
expect(result.error).toBe('context')
|
|
55
55
|
})
|
|
56
|
+
|
|
57
|
+
function logLines(entries: [string, string][]): string {
|
|
58
|
+
return entries.map(([stage, status]) => `${JSON.stringify({ stage, status })}\n`).join('')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const throughPeerReview: [string, string][] = [
|
|
62
|
+
['setup', 'done'],
|
|
63
|
+
['context', 'done'],
|
|
64
|
+
['specialists', 'done'],
|
|
65
|
+
['dedupe', 'done'],
|
|
66
|
+
['critic', 'done'],
|
|
67
|
+
['peer-review', 'done'],
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
test('a local run after peer-review resumes at handoff', async () => {
|
|
71
|
+
await writeFile(join(runDir, 'target.json'), JSON.stringify({ mode: 'local', root: runDir }))
|
|
72
|
+
await writeFile(join(runDir, 'log.jsonl'), logLines(throughPeerReview))
|
|
73
|
+
const result = await runStatus(runDir)
|
|
74
|
+
expect(result.lastCompleted).toBe('peer-review')
|
|
75
|
+
expect(result.next).toBe('handoff')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('a local run with handoff done goes to cleanup', async () => {
|
|
79
|
+
await writeFile(join(runDir, 'target.json'), JSON.stringify({ mode: 'local', root: runDir }))
|
|
80
|
+
await writeFile(join(runDir, 'log.jsonl'), logLines([...throughPeerReview, ['handoff', 'done']]))
|
|
81
|
+
const result = await runStatus(runDir)
|
|
82
|
+
expect(result.lastCompleted).toBe('handoff')
|
|
83
|
+
expect(result.next).toBe('cleanup')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('a PR or archived run with no target.json resumes at report after peer-review', async () => {
|
|
87
|
+
await writeFile(join(runDir, 'log.jsonl'), logLines(throughPeerReview))
|
|
88
|
+
const result = await runStatus(runDir)
|
|
89
|
+
expect(result.lastCompleted).toBe('peer-review')
|
|
90
|
+
expect(result.next).toBe('report')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('a PR run with target.json resumes at report after peer-review', async () => {
|
|
94
|
+
await writeFile(join(runDir, 'target.json'), JSON.stringify({ mode: 'pr', root: runDir }))
|
|
95
|
+
await writeFile(join(runDir, 'log.jsonl'), logLines(throughPeerReview))
|
|
96
|
+
const result = await runStatus(runDir)
|
|
97
|
+
expect(result.lastCompleted).toBe('peer-review')
|
|
98
|
+
expect(result.next).toBe('report')
|
|
99
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { readTarget, reviewRoot, writeTarget } from '../target.ts'
|
|
6
|
+
|
|
7
|
+
let runDir: string
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
runDir = await mkdtemp(join(tmpdir(), 'magpie-target-'))
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await rm(runDir, { recursive: true, force: true })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('a run without target.json reads as PR mode rooted at its worktree', async () => {
|
|
18
|
+
await writeFile(join(runDir, 'log.jsonl'), '')
|
|
19
|
+
expect(await readTarget(runDir)).toEqual({ mode: 'pr', root: join(runDir, 'worktree') })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('a directory with neither target.json nor log.jsonl is not a run', async () => {
|
|
23
|
+
await expect(readTarget(runDir)).rejects.toThrow(
|
|
24
|
+
`${runDir}: not a magpie run (no target.json or log.jsonl)`,
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('a missing directory is not a run', async () => {
|
|
29
|
+
const missing = join(runDir, 'mistyped')
|
|
30
|
+
await expect(reviewRoot(missing)).rejects.toThrow(
|
|
31
|
+
`${missing}: not a magpie run (no target.json or log.jsonl)`,
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('a written local target reads back', async () => {
|
|
36
|
+
await writeTarget(runDir, { mode: 'local', root: '/x' })
|
|
37
|
+
expect(await readTarget(runDir)).toEqual({ mode: 'local', root: '/x' })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('writeTarget writes two-space JSON with a trailing newline', async () => {
|
|
41
|
+
await writeTarget(runDir, { mode: 'local', root: '/x' })
|
|
42
|
+
expect(await readFile(join(runDir, 'target.json'), 'utf8')).toBe(
|
|
43
|
+
'{\n "mode": "local",\n "root": "/x"\n}\n',
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('reviewRoot returns the root named in target.json', async () => {
|
|
48
|
+
await writeTarget(runDir, { mode: 'local', root: '/repo' })
|
|
49
|
+
expect(await reviewRoot(runDir)).toBe('/repo')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('an unknown mode throws naming target.json', async () => {
|
|
53
|
+
await writeFile(join(runDir, 'target.json'), JSON.stringify({ mode: 'other', root: '/x' }))
|
|
54
|
+
await expect(readTarget(runDir)).rejects.toThrow(/target\.json.*mode/)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('a non-string root throws naming target.json', async () => {
|
|
58
|
+
await writeFile(join(runDir, 'target.json'), JSON.stringify({ mode: 'local', root: 7 }))
|
|
59
|
+
await expect(readTarget(runDir)).rejects.toThrow(/target\.json.*root/)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('an empty root throws as not absolute', async () => {
|
|
63
|
+
await writeFile(join(runDir, 'target.json'), JSON.stringify({ mode: 'local', root: '' }))
|
|
64
|
+
await expect(readTarget(runDir)).rejects.toThrow(
|
|
65
|
+
`${join(runDir, 'target.json')}: root must be an absolute path`,
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('a relative root throws as not absolute', async () => {
|
|
70
|
+
await writeFile(join(runDir, 'target.json'), JSON.stringify({ mode: 'local', root: 'repo' }))
|
|
71
|
+
await expect(readTarget(runDir)).rejects.toThrow(
|
|
72
|
+
`${join(runDir, 'target.json')}: root must be an absolute path`,
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('a target.json that cannot be read for a reason other than absence rejects', async () => {
|
|
77
|
+
await mkdir(join(runDir, 'target.json'))
|
|
78
|
+
await expect(readTarget(runDir)).rejects.toThrow(/EISDIR/)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('unparseable target.json throws naming the file', async () => {
|
|
82
|
+
await writeFile(join(runDir, 'target.json'), 'not json')
|
|
83
|
+
await expect(readTarget(runDir)).rejects.toThrow(/target\.json/)
|
|
84
|
+
})
|
|
@@ -10,13 +10,14 @@ import {
|
|
|
10
10
|
parseFinding,
|
|
11
11
|
} from '../types.ts'
|
|
12
12
|
|
|
13
|
-
test('FOCUS_IDS contains the LLM focuses
|
|
13
|
+
test('FOCUS_IDS contains the LLM focuses, the checks pass and the deterministic tests domain', () => {
|
|
14
14
|
expect(FOCUS_IDS).toEqual([
|
|
15
15
|
'security',
|
|
16
16
|
'bugs',
|
|
17
17
|
'performance',
|
|
18
18
|
'code-smells',
|
|
19
19
|
'architecture',
|
|
20
|
+
'checks',
|
|
20
21
|
'tests',
|
|
21
22
|
])
|
|
22
23
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { appendFile, readFile, rename, stat } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { writeLabels } from './labels-cmd.ts'
|
|
4
|
+
import { readTarget } from './target.ts'
|
|
4
5
|
import { removeWorktree } from './worktree.ts'
|
|
5
6
|
|
|
6
7
|
export type RunCleanupInput = {
|
|
@@ -67,6 +68,9 @@ export async function killServer(
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
export async function runCleanup(input: RunCleanupInput): Promise<number> {
|
|
71
|
+
// Read before any side effect, so a malformed target.json leaves the run untouched.
|
|
72
|
+
// Local mode promises the caller no report, so its cleanup does not point at one.
|
|
73
|
+
const { mode } = await readTarget(input.runDir)
|
|
70
74
|
const worktreePath = join(input.runDir, 'worktree')
|
|
71
75
|
const worktreeExists = await stat(worktreePath)
|
|
72
76
|
.then(() => true)
|
|
@@ -95,7 +99,7 @@ export async function runCleanup(input: RunCleanupInput): Promise<number> {
|
|
|
95
99
|
await rename(input.runDir, target)
|
|
96
100
|
process.stdout.write(`archived to ${target}\n`)
|
|
97
101
|
const archivedId = target.split('/').filter(Boolean).pop()
|
|
98
|
-
if (archivedId) {
|
|
102
|
+
if (archivedId && mode !== 'local') {
|
|
99
103
|
process.stdout.write(`view later: magpie open ${archivedId}\n`)
|
|
100
104
|
}
|
|
101
105
|
if (kill.outcome === 'failed') {
|