@iceinvein/agent-skills 0.1.40 → 0.3.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 +18 -2
- package/dist/cli/index.js +115 -34
- package/package.json +1 -1
- package/skills/index.json +14 -2
- package/skills/magpie/SKILL.md +118 -40
- package/skills/magpie/bin/magpie.ts +43 -0
- package/skills/magpie/fixtures/fake-gh-nodiff.sh +38 -0
- package/skills/magpie/package.json +1 -1
- package/skills/magpie/references/peer-review.md +7 -2
- package/skills/magpie/references/specialists.md +38 -7
- package/skills/magpie/scripts/__tests__/cli.test.ts +101 -1
- package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +187 -0
- package/skills/magpie/scripts/__tests__/diff-chunks.test.ts +51 -0
- package/skills/magpie/scripts/__tests__/filter-diff-preservation.test.ts +54 -0
- package/skills/magpie/scripts/__tests__/findings-files.test.ts +35 -0
- package/skills/magpie/scripts/__tests__/gh.test.ts +69 -0
- package/skills/magpie/scripts/__tests__/git-diff.test.ts +83 -0
- package/skills/magpie/scripts/__tests__/helpers/git-fixture.ts +47 -0
- package/skills/magpie/scripts/__tests__/path-filter.test.ts +27 -0
- package/skills/magpie/scripts/__tests__/render-cmd.test.ts +95 -0
- package/skills/magpie/scripts/__tests__/render-findings.test.ts +33 -0
- package/skills/magpie/scripts/__tests__/render-progress.test.ts +42 -0
- package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +83 -1
- package/skills/magpie/scripts/__tests__/shard.test.ts +165 -0
- package/skills/magpie/scripts/__tests__/skill-lint.test.ts +96 -1
- package/skills/magpie/scripts/dedupe-cmd.ts +58 -3
- package/skills/magpie/scripts/diff-chunks.ts +28 -0
- package/skills/magpie/scripts/findings-files.ts +32 -0
- package/skills/magpie/scripts/gh.ts +64 -13
- package/skills/magpie/scripts/git-diff.ts +111 -0
- package/skills/magpie/scripts/path-filter.ts +9 -5
- package/skills/magpie/scripts/refresh.ts +8 -0
- package/skills/magpie/scripts/render-cmd.ts +28 -9
- package/skills/magpie/scripts/render-findings.ts +11 -1
- package/skills/magpie/scripts/render-progress.ts +6 -1
- package/skills/magpie/scripts/setup-cmd.ts +38 -1
- package/skills/magpie/scripts/shard.ts +171 -0
- package/skills/magpie/scripts/status-cmd.ts +4 -1
- package/skills/magpie/skill.json +2 -2
- package/skills/magpie/templates/styles.css +5 -0
- package/skills/migrate/README.md +194 -0
- package/skills/migrate/SKILL.md +197 -0
- package/skills/migrate/bin/migrate +15 -0
- package/skills/migrate/bin/migrate.ts +309 -0
- package/skills/migrate/biome.json +35 -0
- package/skills/migrate/bun.lock +24 -0
- package/skills/migrate/docs/architecture.md +294 -0
- package/skills/migrate/docs/reference.md +590 -0
- package/skills/migrate/fixtures/tiny-express/GROUND-TRUTH.md +39 -0
- package/skills/migrate/fixtures/tiny-express/app.js +29 -0
- package/skills/migrate/fixtures/tiny-express/cron.js +6 -0
- package/skills/migrate/fixtures/tiny-express/reports/daily-users.json +6 -0
- package/skills/migrate/fixtures/tiny-express/schema.sql +12 -0
- package/skills/migrate/fixtures/tiny-express/settings.json +4 -0
- package/skills/migrate/fixtures/tiny-express/views/users.html +9 -0
- package/skills/migrate/fixtures/tiny-webforms/Controllers/UsersController.cs +68 -0
- package/skills/migrate/fixtures/tiny-webforms/Default.aspx +7 -0
- package/skills/migrate/fixtures/tiny-webforms/Default.aspx.cs +14 -0
- package/skills/migrate/fixtures/tiny-webforms/GROUND-TRUTH.md +50 -0
- package/skills/migrate/fixtures/tiny-webforms/Integrations/BillingClient.cs +16 -0
- package/skills/migrate/fixtures/tiny-webforms/Jobs/NightlyDigestJob.cs +33 -0
- package/skills/migrate/fixtures/tiny-webforms/Reports/DailyUsers.rdl +11 -0
- package/skills/migrate/fixtures/tiny-webforms/Schema.sql +12 -0
- package/skills/migrate/fixtures/tiny-webforms/Site.master +16 -0
- package/skills/migrate/fixtures/tiny-webforms/Users.aspx +8 -0
- package/skills/migrate/fixtures/tiny-webforms/Users.aspx.cs +14 -0
- package/skills/migrate/fixtures/tiny-webforms/web.config +10 -0
- package/skills/migrate/install.sh +68 -0
- package/skills/migrate/package.json +17 -0
- package/skills/migrate/references/phases/enumerate.md +291 -0
- package/skills/migrate/references/phases/extract.md +652 -0
- package/skills/migrate/references/phases/parity.md +275 -0
- package/skills/migrate/references/phases/probe.md +135 -0
- package/skills/migrate/references/phases/queue.md +242 -0
- package/skills/migrate/references/phases/seam.md +416 -0
- package/skills/migrate/references/recipes/README.md +116 -0
- package/skills/migrate/references/recipes/aspnet.md +287 -0
- package/skills/migrate/references/run-ops.md +280 -0
- package/skills/migrate/scripts/__tests__/census.test.ts +775 -0
- package/skills/migrate/scripts/__tests__/check.test.ts +458 -0
- package/skills/migrate/scripts/__tests__/citations.test.ts +156 -0
- package/skills/migrate/scripts/__tests__/cli.test.ts +183 -0
- package/skills/migrate/scripts/__tests__/concurrency.test.ts +164 -0
- package/skills/migrate/scripts/__tests__/config.test.ts +112 -0
- package/skills/migrate/scripts/__tests__/e2e-express.test.ts +1093 -0
- package/skills/migrate/scripts/__tests__/e2e-webforms.test.ts +1276 -0
- package/skills/migrate/scripts/__tests__/e2e.test.ts +320 -0
- package/skills/migrate/scripts/__tests__/ids.test.ts +38 -0
- package/skills/migrate/scripts/__tests__/import.test.ts +155 -0
- package/skills/migrate/scripts/__tests__/init.test.ts +192 -0
- package/skills/migrate/scripts/__tests__/leaks.test.ts +176 -0
- package/skills/migrate/scripts/__tests__/lock.test.ts +183 -0
- package/skills/migrate/scripts/__tests__/paths.test.ts +129 -0
- package/skills/migrate/scripts/__tests__/phase-cmd.test.ts +151 -0
- package/skills/migrate/scripts/__tests__/phases.test.ts +70 -0
- package/skills/migrate/scripts/__tests__/queue.test.ts +475 -0
- package/skills/migrate/scripts/__tests__/report.test.ts +150 -0
- package/skills/migrate/scripts/__tests__/run-state.test.ts +136 -0
- package/skills/migrate/scripts/__tests__/status-reset.test.ts +318 -0
- package/skills/migrate/scripts/__tests__/store.test.ts +132 -0
- package/skills/migrate/scripts/__tests__/validate.test.ts +54 -0
- package/skills/migrate/scripts/census-cmd.ts +109 -0
- package/skills/migrate/scripts/census.ts +342 -0
- package/skills/migrate/scripts/check-cmd.ts +24 -0
- package/skills/migrate/scripts/check.ts +376 -0
- package/skills/migrate/scripts/citations.ts +92 -0
- package/skills/migrate/scripts/config.ts +237 -0
- package/skills/migrate/scripts/ids.ts +31 -0
- package/skills/migrate/scripts/import-cmd.ts +141 -0
- package/skills/migrate/scripts/init-cmd.ts +118 -0
- package/skills/migrate/scripts/leaks.ts +184 -0
- package/skills/migrate/scripts/lock.ts +188 -0
- package/skills/migrate/scripts/paths.ts +103 -0
- package/skills/migrate/scripts/phase-cmd.ts +63 -0
- package/skills/migrate/scripts/phases.ts +113 -0
- package/skills/migrate/scripts/queue-cmd.ts +98 -0
- package/skills/migrate/scripts/queue.ts +258 -0
- package/skills/migrate/scripts/report-cmd.ts +47 -0
- package/skills/migrate/scripts/report.ts +131 -0
- package/skills/migrate/scripts/reset-cmd.ts +120 -0
- package/skills/migrate/scripts/status-cmd.ts +52 -0
- package/skills/migrate/scripts/store.ts +159 -0
- package/skills/migrate/scripts/types.ts +137 -0
- package/skills/migrate/scripts/validate.ts +221 -0
- package/skills/migrate/skill.json +33 -0
- package/skills/migrate/templates/config.toml +27 -0
- package/skills/migrate/templates/queue-item.md +17 -0
- package/skills/migrate/tsconfig.json +18 -0
- package/skills/migrate/uninstall.sh +31 -0
- package/skills/sluice/SKILL.md +95 -0
- package/skills/sluice/references/deep-channel.md +114 -0
- package/skills/sluice/references/finish.md +37 -0
- package/skills/sluice/references/intent.md +29 -0
- package/skills/sluice/references/meter.md +38 -0
- package/skills/sluice/references/review.md +42 -0
- package/skills/sluice/references/root-cause.md +38 -0
- package/skills/sluice/references/show-or-say.md +36 -0
- package/skills/sluice/references/test-first.md +35 -0
- package/skills/sluice/references/verify.md +26 -0
- package/skills/sluice/scripts/run-stats.sh +236 -0
- package/skills/sluice/skill.json +33 -0
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { writeFile } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
+
import { localPrDiff } from './git-diff.ts'
|
|
3
4
|
|
|
4
5
|
export type FetchPrInput = {
|
|
5
6
|
ghBin: string
|
|
6
7
|
prNumber: number
|
|
7
8
|
runDir: string
|
|
8
|
-
/** Working directory for the gh
|
|
9
|
+
/** Working directory for the gh and git invocations. Defaults to process.cwd(). */
|
|
9
10
|
cwd?: string
|
|
11
|
+
/** git binary for the local-diff fallback. Defaults to `git` on PATH. */
|
|
12
|
+
gitBin?: string
|
|
13
|
+
/** Extra environment for the gh invocation. Test seam for the fake gh fixtures. */
|
|
14
|
+
env?: Record<string, string>
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
export type FetchPrResult =
|
|
13
|
-
| { ok: true; prJsonPath: string; diffPath: string }
|
|
18
|
+
| { ok: true; prJsonPath: string; diffPath: string; source: 'gh' | 'git'; mergeBase?: string }
|
|
14
19
|
| { ok: false; error: string }
|
|
15
20
|
|
|
16
21
|
export const PR_VIEW_FIELDS = [
|
|
@@ -31,9 +36,23 @@ export const PR_VIEW_FIELDS = [
|
|
|
31
36
|
'closingIssuesReferences',
|
|
32
37
|
].join(',')
|
|
33
38
|
|
|
39
|
+
function prFileCount(viewStdout: string): number {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(viewStdout) as { files?: unknown }
|
|
42
|
+
return Array.isArray(parsed.files) ? parsed.files.length : 0
|
|
43
|
+
} catch {
|
|
44
|
+
return 0
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
export async function fetchPr(input: FetchPrInput): Promise<FetchPrResult> {
|
|
35
49
|
const { ghBin, prNumber, runDir, cwd } = input
|
|
36
|
-
const spawnOpts = {
|
|
50
|
+
const spawnOpts = {
|
|
51
|
+
stdout: 'pipe' as const,
|
|
52
|
+
stderr: 'pipe' as const,
|
|
53
|
+
...(cwd ? { cwd } : {}),
|
|
54
|
+
...(input.env ? { env: { ...process.env, ...input.env } } : {}),
|
|
55
|
+
}
|
|
37
56
|
const view = Bun.spawn(
|
|
38
57
|
[ghBin, 'pr', 'view', String(prNumber), '--json', PR_VIEW_FIELDS],
|
|
39
58
|
spawnOpts,
|
|
@@ -48,20 +67,52 @@ export async function fetchPr(input: FetchPrInput): Promise<FetchPrResult> {
|
|
|
48
67
|
}
|
|
49
68
|
}
|
|
50
69
|
|
|
70
|
+
const prJsonPath = join(runDir, 'pr.json')
|
|
71
|
+
const diffPath = join(runDir, 'diff.patch')
|
|
72
|
+
await writeFile(prJsonPath, viewStdout)
|
|
73
|
+
|
|
51
74
|
const diff = Bun.spawn([ghBin, 'pr', 'diff', String(prNumber)], spawnOpts)
|
|
52
75
|
const diffStdout = await new Response(diff.stdout).text()
|
|
53
76
|
const diffStderr = await new Response(diff.stderr).text()
|
|
54
77
|
const diffExit = await diff.exited
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
78
|
+
|
|
79
|
+
// GitHub refuses the `.diff` media type above roughly 300 files (HTTP 406),
|
|
80
|
+
// and a truncating proxy can answer 200 with nothing at all. Reviewing an
|
|
81
|
+
// empty diff as though it were the PR is the worse failure of the two, so
|
|
82
|
+
// both conditions fall through to the local clone.
|
|
83
|
+
const emptyForNonEmptyPr = diffStdout.trim() === '' && prFileCount(viewStdout) > 0
|
|
84
|
+
if (diffExit === 0 && !emptyForNonEmptyPr) {
|
|
85
|
+
await writeFile(diffPath, diffStdout)
|
|
86
|
+
return { ok: true, prJsonPath, diffPath, source: 'gh' }
|
|
60
87
|
}
|
|
61
88
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
89
|
+
const ghError =
|
|
90
|
+
diffExit !== 0
|
|
91
|
+
? `gh pr diff exit ${diffExit}: ${diffStderr.trim()}`
|
|
92
|
+
: 'gh pr diff returned an empty diff for a PR with changed files'
|
|
93
|
+
|
|
94
|
+
let pr: { baseRefName?: unknown; headRefOid?: unknown }
|
|
95
|
+
try {
|
|
96
|
+
pr = JSON.parse(viewStdout) as { baseRefName?: unknown; headRefOid?: unknown }
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return { ok: false, error: `${ghError}; pr.json is unparseable: ${String(err)}` }
|
|
99
|
+
}
|
|
100
|
+
const baseRefName = typeof pr.baseRefName === 'string' ? pr.baseRefName : ''
|
|
101
|
+
const headRefOid = typeof pr.headRefOid === 'string' ? pr.headRefOid : ''
|
|
102
|
+
if (!baseRefName || !headRefOid) {
|
|
103
|
+
return { ok: false, error: `${ghError}; pr.json has no baseRefName/headRefOid to diff from` }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const local = await localPrDiff({
|
|
107
|
+
gitBin: input.gitBin ?? 'git',
|
|
108
|
+
repoPath: cwd ?? process.cwd(),
|
|
109
|
+
prNumber,
|
|
110
|
+
baseRefName,
|
|
111
|
+
headRefOid,
|
|
112
|
+
})
|
|
113
|
+
if (!local.ok) {
|
|
114
|
+
return { ok: false, error: `${ghError}; local fallback failed: ${local.error}` }
|
|
115
|
+
}
|
|
116
|
+
await writeFile(diffPath, local.diff)
|
|
117
|
+
return { ok: true, prJsonPath, diffPath, source: 'git', mergeBase: local.mergeBase }
|
|
67
118
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export type LocalDiffInput = {
|
|
2
|
+
gitBin: string
|
|
3
|
+
repoPath: string
|
|
4
|
+
prNumber: number
|
|
5
|
+
baseRefName: string
|
|
6
|
+
headRefOid: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type LocalDiffResult =
|
|
10
|
+
| { ok: true; diff: string; mergeBase: string }
|
|
11
|
+
| { ok: false; error: string }
|
|
12
|
+
|
|
13
|
+
async function git(
|
|
14
|
+
gitBin: string,
|
|
15
|
+
cwd: string,
|
|
16
|
+
args: string[],
|
|
17
|
+
): Promise<{ exit: number; stdout: string; stderr: string }> {
|
|
18
|
+
const proc = Bun.spawn([gitBin, ...args], { cwd, stdout: 'pipe', stderr: 'pipe' })
|
|
19
|
+
const stdout = await new Response(proc.stdout).text()
|
|
20
|
+
const stderr = await new Response(proc.stderr).text()
|
|
21
|
+
const exit = await proc.exited
|
|
22
|
+
return { exit, stdout, stderr }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function resolveCommit(gitBin: string, cwd: string, ref: string): Promise<string | null> {
|
|
26
|
+
const r = await git(gitBin, cwd, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`])
|
|
27
|
+
const sha = r.stdout.trim()
|
|
28
|
+
return r.exit === 0 && sha.length > 0 ? sha : null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Render a fetch's outcome for an error message, so a genuine failure (wrong
|
|
33
|
+
* remote, auth failure, rate limiting) can be told apart from a benign one
|
|
34
|
+
* (no origin, offline, non-GitHub remote) when resolution ends up failing.
|
|
35
|
+
* Empty when the fetch succeeded, since a successful fetch explains nothing.
|
|
36
|
+
*/
|
|
37
|
+
function fetchDiagnostic(result: { exit: number; stderr: string }): string {
|
|
38
|
+
if (result.exit === 0) return ''
|
|
39
|
+
const stderr = result.stderr.trim()
|
|
40
|
+
return stderr ? `; git fetch: ${stderr}` : `; git fetch exited ${result.exit}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reproduce `gh pr diff` from the local clone. GitHub refuses the `.diff` media
|
|
45
|
+
* type above roughly 300 files, and a review that cannot get a diff is worth
|
|
46
|
+
* less than one built from local objects.
|
|
47
|
+
*
|
|
48
|
+
* Diffs from the merge base rather than the base tip so the output matches
|
|
49
|
+
* `gh pr diff`'s three-dot semantics: the PR's own changes, not every commit
|
|
50
|
+
* the base branch gained since the branch point.
|
|
51
|
+
*/
|
|
52
|
+
export async function localPrDiff(input: LocalDiffInput): Promise<LocalDiffResult> {
|
|
53
|
+
const { gitBin, repoPath, prNumber, baseRefName, headRefOid } = input
|
|
54
|
+
const prRef = `refs/magpie/pr-${prNumber}`
|
|
55
|
+
|
|
56
|
+
// Best effort. `pull/<n>/head` is a GitHub convention and resolves fork PRs
|
|
57
|
+
// that `headRefName` cannot, but a repo with no `origin`, an offline run, or
|
|
58
|
+
// a non-GitHub remote must still work when the commits are already local.
|
|
59
|
+
// Each fetch's own result is kept (not discarded) so, if resolution below
|
|
60
|
+
// still fails, the error can say why the fetch didn't help.
|
|
61
|
+
const headFetch = await git(gitBin, repoPath, [
|
|
62
|
+
'fetch',
|
|
63
|
+
'origin',
|
|
64
|
+
`pull/${prNumber}/head:${prRef}`,
|
|
65
|
+
])
|
|
66
|
+
const baseFetch = await git(gitBin, repoPath, ['fetch', 'origin', baseRefName])
|
|
67
|
+
|
|
68
|
+
let head: string | null = null
|
|
69
|
+
for (const ref of [prRef, headRefOid]) {
|
|
70
|
+
head = await resolveCommit(gitBin, repoPath, ref)
|
|
71
|
+
if (head) break
|
|
72
|
+
}
|
|
73
|
+
if (!head) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
error: `cannot resolve PR head ${headRefOid} locally (tried ${prRef} and the SHA); fetch the PR branch and retry${fetchDiagnostic(headFetch)}`,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Reviewing a stale local ref and reporting it as a review of the PR is worse
|
|
80
|
+
// than failing, so a mismatch is fatal rather than a warning.
|
|
81
|
+
if (head !== headRefOid) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
error: `local PR head ${head} does not match the PR head ${headRefOid}; the local ref is stale`,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let base: string | null = null
|
|
89
|
+
for (const ref of [`origin/${baseRefName}`, baseRefName]) {
|
|
90
|
+
base = await resolveCommit(gitBin, repoPath, ref)
|
|
91
|
+
if (base) break
|
|
92
|
+
}
|
|
93
|
+
if (!base) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: `cannot resolve base branch ${baseRefName} locally (tried origin/${baseRefName} and ${baseRefName})${fetchDiagnostic(baseFetch)}`,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const mb = await git(gitBin, repoPath, ['merge-base', base, head])
|
|
101
|
+
if (mb.exit !== 0) {
|
|
102
|
+
return { ok: false, error: `git merge-base exit ${mb.exit}: ${mb.stderr.trim()}` }
|
|
103
|
+
}
|
|
104
|
+
const mergeBase = mb.stdout.trim()
|
|
105
|
+
|
|
106
|
+
const d = await git(gitBin, repoPath, ['diff', '--no-color', '--find-renames', mergeBase, head])
|
|
107
|
+
if (d.exit !== 0) {
|
|
108
|
+
return { ok: false, error: `git diff exit ${d.exit}: ${d.stderr.trim()}` }
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, diff: d.stdout, mergeBase }
|
|
111
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
|
+
import { chunkPath, splitRawChunks } from './diff-chunks.ts'
|
|
4
5
|
|
|
5
6
|
export type PathFilterConfig = {
|
|
6
7
|
exclude: string[]
|
|
@@ -43,6 +44,12 @@ export const DEFAULT_EXCLUDES: readonly string[] = [
|
|
|
43
44
|
'**/*.min.js',
|
|
44
45
|
'**/*.min.css',
|
|
45
46
|
'**/*.map',
|
|
47
|
+
// Generated .NET / C#
|
|
48
|
+
'**/*.Designer.cs',
|
|
49
|
+
'**/*ModelSnapshot.cs',
|
|
50
|
+
'**/*.g.cs',
|
|
51
|
+
'**/*.g.i.cs',
|
|
52
|
+
'**/*.designer.vb',
|
|
46
53
|
// Snapshot fixtures
|
|
47
54
|
'**/*.snap',
|
|
48
55
|
'**/__snapshots__/**',
|
|
@@ -113,11 +120,9 @@ export async function loadPathFilterConfig(cwd: string): Promise<PathFilterConfi
|
|
|
113
120
|
export type ExcludedFile = { path: string; pattern: string }
|
|
114
121
|
export type FilterDiffResult = { filtered: string; excluded: ExcludedFile[] }
|
|
115
122
|
|
|
116
|
-
const FILE_HEADER = /^diff --git a\/(.+?) b\/(.+?)$/m
|
|
117
|
-
|
|
118
123
|
export function filterDiff(rawDiff: string, config: PathFilterConfig): FilterDiffResult {
|
|
119
124
|
if (!rawDiff) return { filtered: '', excluded: [] }
|
|
120
|
-
const chunks = rawDiff
|
|
125
|
+
const chunks = splitRawChunks(rawDiff)
|
|
121
126
|
const kept: string[] = []
|
|
122
127
|
const excluded: ExcludedFile[] = []
|
|
123
128
|
for (const chunk of chunks) {
|
|
@@ -125,8 +130,7 @@ export function filterDiff(rawDiff: string, config: PathFilterConfig): FilterDif
|
|
|
125
130
|
if (chunk.trim()) kept.push(chunk)
|
|
126
131
|
continue
|
|
127
132
|
}
|
|
128
|
-
const
|
|
129
|
-
const path = m?.[2] ?? m?.[1]
|
|
133
|
+
const path = chunkPath(chunk)
|
|
130
134
|
if (!path) {
|
|
131
135
|
kept.push(chunk)
|
|
132
136
|
continue
|
|
@@ -98,6 +98,13 @@ export async function refreshFindings(runDir: string): Promise<RefreshResult> {
|
|
|
98
98
|
|
|
99
99
|
const diff = await readFile(join(runDir, 'diff.patch'), 'utf8').catch(() => '')
|
|
100
100
|
|
|
101
|
+
let diffSource: { source: 'gh' | 'git'; mergeBase: string | null } | undefined
|
|
102
|
+
try {
|
|
103
|
+
diffSource = (await Bun.file(join(runDir, 'diff-source.json')).json()) as typeof diffSource
|
|
104
|
+
} catch {
|
|
105
|
+
diffSource = undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
101
108
|
await renderFindingsToDisk(
|
|
102
109
|
{
|
|
103
110
|
findings,
|
|
@@ -108,6 +115,7 @@ export async function refreshFindings(runDir: string): Promise<RefreshResult> {
|
|
|
108
115
|
diff,
|
|
109
116
|
brief: brief ?? undefined,
|
|
110
117
|
issues,
|
|
118
|
+
diffSource,
|
|
111
119
|
},
|
|
112
120
|
join(screenDir, 'findings.html'),
|
|
113
121
|
)
|
|
@@ -48,12 +48,15 @@ function summarizeStages(log: Array<Record<string, unknown>>): {
|
|
|
48
48
|
report: 'pending',
|
|
49
49
|
post: 'pending',
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
// focus -> shard key -> count. Keyed by shard so a re-dispatched (focus, shard)
|
|
52
|
+
// pair replaces its own count rather than doubling it, while distinct shards
|
|
53
|
+
// of the same focus add up.
|
|
54
|
+
const perShard: Record<string, Record<string, number>> = {
|
|
55
|
+
security: {},
|
|
56
|
+
bugs: {},
|
|
57
|
+
performance: {},
|
|
58
|
+
'code-smells': {},
|
|
59
|
+
architecture: {},
|
|
57
60
|
}
|
|
58
61
|
for (const entry of log) {
|
|
59
62
|
const stage = entry.stage as string
|
|
@@ -63,11 +66,16 @@ function summarizeStages(log: Array<Record<string, unknown>>): {
|
|
|
63
66
|
if (stage in stages && status === 'error') stages[stage] = 'error'
|
|
64
67
|
if (stage in stages && status === 'skipped') stages[stage] = 'skipped'
|
|
65
68
|
if (stage === 'specialist' && typeof entry.focus === 'string') {
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
const bucket = perShard[entry.focus]
|
|
70
|
+
if (bucket && typeof entry.findings === 'number') {
|
|
71
|
+
bucket[String(entry.shard ?? 'all')] = entry.findings
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
}
|
|
75
|
+
const specialistCounts: Record<string, number> = {}
|
|
76
|
+
for (const [focus, bucket] of Object.entries(perShard)) {
|
|
77
|
+
specialistCounts[focus] = Object.values(bucket).reduce((a, b) => a + b, 0)
|
|
78
|
+
}
|
|
71
79
|
return { stages, specialistCounts }
|
|
72
80
|
}
|
|
73
81
|
|
|
@@ -81,6 +89,11 @@ export async function runRender(runDir: string, page: 'progress' | 'findings'):
|
|
|
81
89
|
})
|
|
82
90
|
const log = await readLog(runDir)
|
|
83
91
|
const summary = summarizeStages(log)
|
|
92
|
+
const manifest = await readJson<{ shards?: unknown[] }>(
|
|
93
|
+
join(runDir, 'shards', 'manifest.json'),
|
|
94
|
+
{},
|
|
95
|
+
)
|
|
96
|
+
const shardCount = Array.isArray(manifest.shards) ? manifest.shards.length : 1
|
|
84
97
|
const outPath = await nextVersionedPath(screenDir, 'progress')
|
|
85
98
|
await renderProgressToDisk(
|
|
86
99
|
{
|
|
@@ -89,6 +102,7 @@ export async function runRender(runDir: string, page: 'progress' | 'findings'):
|
|
|
89
102
|
branch: String(prJson.headRefName ?? '?'),
|
|
90
103
|
stages: summary.stages as never,
|
|
91
104
|
specialistCounts: summary.specialistCounts,
|
|
105
|
+
shardCount,
|
|
92
106
|
},
|
|
93
107
|
outPath,
|
|
94
108
|
)
|
|
@@ -124,9 +138,14 @@ export async function runRender(runDir: string, page: 'progress' | 'findings'):
|
|
|
124
138
|
// is simply omitted.
|
|
125
139
|
const brief = parseBrief(await readJson<unknown>(join(runDir, 'brief.json'), null)) ?? undefined
|
|
126
140
|
const issues = parseClosingIssues(prJson)
|
|
141
|
+
const diffSource =
|
|
142
|
+
(await readJson<{ source: 'gh' | 'git'; mergeBase: string | null } | null>(
|
|
143
|
+
join(runDir, 'diff-source.json'),
|
|
144
|
+
null,
|
|
145
|
+
)) ?? undefined
|
|
127
146
|
const outPath = await nextVersionedPath(screenDir, 'findings')
|
|
128
147
|
await renderFindingsToDisk(
|
|
129
|
-
{ findings, postStatus, runId: basename(runDir), pr, files, diff, brief, issues },
|
|
148
|
+
{ findings, postStatus, runId: basename(runDir), pr, files, diff, brief, issues, diffSource },
|
|
130
149
|
outPath,
|
|
131
150
|
)
|
|
132
151
|
return 0
|
|
@@ -59,6 +59,8 @@ export type RenderFindingsInput = {
|
|
|
59
59
|
issues?: BriefIssue[]
|
|
60
60
|
/** Shiki highlighter, prepared by the caller. */
|
|
61
61
|
highlighter: Highlighter
|
|
62
|
+
/** Where diff.patch came from. Absent means gh, which needs no note. */
|
|
63
|
+
diffSource?: { source: 'gh' | 'git'; mergeBase: string | null }
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
function esc(s: string): string {
|
|
@@ -86,13 +88,21 @@ function brandBlock(): string {
|
|
|
86
88
|
|
|
87
89
|
function prHeader(input: RenderFindingsInput): string {
|
|
88
90
|
const pr = input.pr
|
|
91
|
+
const src = input.diffSource
|
|
92
|
+
const diffNote =
|
|
93
|
+
src?.source === 'git'
|
|
94
|
+
? `<span class="diff-source" title="gh pr diff could not serve this PR, so the diff was rebuilt from the local clone">diff from local clone${
|
|
95
|
+
src.mergeBase ? ` at <code>${esc(src.mergeBase.slice(0, 12))}</code>` : ''
|
|
96
|
+
}</span>`
|
|
97
|
+
: ''
|
|
89
98
|
const prMeta = pr
|
|
90
99
|
? `<div class="pr-meta">
|
|
91
100
|
<span class="pr-number">PR #${pr.number}</span>
|
|
92
101
|
<span class="pr-branch">${esc(pr.branch)}</span>
|
|
93
102
|
<code>${esc(pr.headSha.slice(0, 12))}</code>
|
|
103
|
+
${diffNote}
|
|
94
104
|
</div>`
|
|
95
|
-
: `<div class="pr-meta"
|
|
105
|
+
: `<div class="pr-meta">${diffNote}</div>`
|
|
96
106
|
const total = input.findings.length
|
|
97
107
|
return `<header class="pr-header">
|
|
98
108
|
${brandBlock()}
|
|
@@ -41,6 +41,8 @@ export type RenderProgressInput = {
|
|
|
41
41
|
branch: string
|
|
42
42
|
stages: Record<StageId, StageStatus>
|
|
43
43
|
specialistCounts: Record<string, number>
|
|
44
|
+
/** Shard count from shards/manifest.json. Absent or 1 means unsharded. */
|
|
45
|
+
shardCount?: number
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
function esc(s: string): string {
|
|
@@ -82,7 +84,10 @@ function progressPane(input: RenderProgressInput): string {
|
|
|
82
84
|
const cur = current(input.stages)
|
|
83
85
|
let lead: string
|
|
84
86
|
if (cur.status === 'running') {
|
|
85
|
-
lead =
|
|
87
|
+
lead =
|
|
88
|
+
cur.id === 'specialists' && (input.shardCount ?? 1) > 1
|
|
89
|
+
? `Five reviewers across ${input.shardCount} shards`
|
|
90
|
+
: STAGE_NOW_DOING[cur.id]
|
|
86
91
|
} else if (cur.status === 'error') {
|
|
87
92
|
lead = `${STAGE_NOW_DOING[cur.id]} stalled`
|
|
88
93
|
} else if (cur.status === 'pending') {
|
|
@@ -4,6 +4,7 @@ import { fetchPr } from './gh.ts'
|
|
|
4
4
|
import { buildIncrementalContext, findPreviousRun } from './incremental.ts'
|
|
5
5
|
import { filterDiff, loadPathFilterConfig } from './path-filter.ts'
|
|
6
6
|
import { type Deps, defaultDeps, preflight, renderInstallHint } from './preflight.ts'
|
|
7
|
+
import { shardDiff } from './shard.ts'
|
|
7
8
|
import { detectMissingTests } from './tests-check.ts'
|
|
8
9
|
import { createWorktree } from './worktree.ts'
|
|
9
10
|
|
|
@@ -47,6 +48,7 @@ export async function runSetup(input: RunSetupInput): Promise<number> {
|
|
|
47
48
|
|
|
48
49
|
const fetched = await fetchPr({
|
|
49
50
|
ghBin: deps.gh,
|
|
51
|
+
gitBin: deps.git,
|
|
50
52
|
prNumber: input.prNumber,
|
|
51
53
|
runDir: input.runDir,
|
|
52
54
|
cwd: input.repoPath,
|
|
@@ -57,10 +59,31 @@ export async function runSetup(input: RunSetupInput): Promise<number> {
|
|
|
57
59
|
await cleanup(input.runDir)
|
|
58
60
|
return 4
|
|
59
61
|
}
|
|
60
|
-
await logLine(input.runDir, {
|
|
62
|
+
await logLine(input.runDir, {
|
|
63
|
+
stage: 'fetch-pr',
|
|
64
|
+
status: 'done',
|
|
65
|
+
source: fetched.source,
|
|
66
|
+
...(fetched.mergeBase ? { mergeBase: fetched.mergeBase } : {}),
|
|
67
|
+
})
|
|
68
|
+
// Sidecar so the report can say where the diff came from without re-parsing
|
|
69
|
+
// log.jsonl, the same way incremental.json carries the incremental context.
|
|
70
|
+
await writeFile(
|
|
71
|
+
join(input.runDir, 'diff-source.json'),
|
|
72
|
+
`${JSON.stringify({ source: fetched.source, mergeBase: fetched.mergeBase ?? null }, null, 2)}\n`,
|
|
73
|
+
)
|
|
61
74
|
|
|
62
75
|
await applyPathFilter(input.runDir, input.repoPath)
|
|
63
76
|
|
|
77
|
+
try {
|
|
78
|
+
await runShard(input.runDir)
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
81
|
+
await logLine(input.runDir, { stage: 'shard', status: 'error', error: message })
|
|
82
|
+
process.stderr.write(`magpie: ${message}\n`)
|
|
83
|
+
await cleanup(input.runDir)
|
|
84
|
+
return 6
|
|
85
|
+
}
|
|
86
|
+
|
|
64
87
|
await runTestsCheck(input.runDir)
|
|
65
88
|
|
|
66
89
|
await detectIncrementalReview(input.runDir, input.prNumber)
|
|
@@ -90,7 +113,9 @@ async function cleanup(runDir: string): Promise<void> {
|
|
|
90
113
|
'pr.json',
|
|
91
114
|
'diff.patch',
|
|
92
115
|
'diff.full.patch',
|
|
116
|
+
'diff-source.json',
|
|
93
117
|
'excluded-files.json',
|
|
118
|
+
'shards',
|
|
94
119
|
'incremental.json',
|
|
95
120
|
'findings',
|
|
96
121
|
'screen',
|
|
@@ -100,6 +125,18 @@ async function cleanup(runDir: string): Promise<void> {
|
|
|
100
125
|
}
|
|
101
126
|
}
|
|
102
127
|
|
|
128
|
+
async function runShard(runDir: string): Promise<void> {
|
|
129
|
+
const manifest = await shardDiff(runDir)
|
|
130
|
+
await logLine(runDir, {
|
|
131
|
+
stage: 'shard',
|
|
132
|
+
status: 'done',
|
|
133
|
+
shards: manifest.shards.length,
|
|
134
|
+
budget: manifest.budget,
|
|
135
|
+
files: manifest.totalFiles,
|
|
136
|
+
lines: manifest.totalLines,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
103
140
|
async function runTestsCheck(runDir: string): Promise<void> {
|
|
104
141
|
const diff = await Bun.file(join(runDir, 'diff.patch')).text()
|
|
105
142
|
const findings = detectMissingTests(diff)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { mkdir, readdir, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { type FileChunk, splitFileChunks } from './diff-chunks.ts'
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_SHARD_BUDGET = 6000
|
|
6
|
+
export const DEFAULT_SHARD_MAX_FILES = 80
|
|
7
|
+
|
|
8
|
+
export type ShardEntry = { id: number; path: string; files: string[]; lines: number }
|
|
9
|
+
export type ShardManifest = {
|
|
10
|
+
budget: number
|
|
11
|
+
maxFiles: number
|
|
12
|
+
totalFiles: number
|
|
13
|
+
totalLines: number
|
|
14
|
+
shards: ShardEntry[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The directory a file is grouped under, capped at two segments. Cohesion is
|
|
19
|
+
* not cosmetic: the architecture and code-smells focuses look for duplication,
|
|
20
|
+
* boundary violations, and shotgun surgery, and splitting a module across
|
|
21
|
+
* shards hides exactly those.
|
|
22
|
+
*/
|
|
23
|
+
export function groupKey(path: string): string {
|
|
24
|
+
const parts = path.split('/')
|
|
25
|
+
if (parts.length <= 1) return '.'
|
|
26
|
+
return parts.slice(0, Math.min(2, parts.length - 1)).join('/')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function planShards(chunks: FileChunk[], budget: number, maxFiles: number): FileChunk[][] {
|
|
30
|
+
const groups = new Map<string, FileChunk[]>()
|
|
31
|
+
for (const c of chunks) {
|
|
32
|
+
const key = groupKey(c.path)
|
|
33
|
+
const existing = groups.get(key)
|
|
34
|
+
if (existing) existing.push(c)
|
|
35
|
+
else groups.set(key, [c])
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const shards: FileChunk[][] = []
|
|
39
|
+
let current: FileChunk[] = []
|
|
40
|
+
let currentLines = 0
|
|
41
|
+
const flush = () => {
|
|
42
|
+
if (current.length > 0) {
|
|
43
|
+
shards.push(current)
|
|
44
|
+
current = []
|
|
45
|
+
currentLines = 0
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const members of groups.values()) {
|
|
50
|
+
const groupLines = members.reduce((n, c) => n + c.lines, 0)
|
|
51
|
+
// Start a fresh shard rather than straddle a group across a boundary, when
|
|
52
|
+
// the whole group could fit on its own.
|
|
53
|
+
if (
|
|
54
|
+
current.length > 0 &&
|
|
55
|
+
(currentLines + groupLines > budget || current.length + members.length > maxFiles)
|
|
56
|
+
) {
|
|
57
|
+
flush()
|
|
58
|
+
}
|
|
59
|
+
for (const c of members) {
|
|
60
|
+
if (
|
|
61
|
+
current.length > 0 &&
|
|
62
|
+
(currentLines + c.lines > budget || current.length + 1 > maxFiles)
|
|
63
|
+
) {
|
|
64
|
+
flush()
|
|
65
|
+
}
|
|
66
|
+
current.push(c)
|
|
67
|
+
currentLines += c.lines
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
flush()
|
|
71
|
+
return shards
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Read `$RUN_DIR/diff.patch`, tolerating only its absence. A missing
|
|
75
|
+
* diff.patch means "nothing to shard"; anything else (permission error,
|
|
76
|
+
* disk error, EISDIR, ...) is a genuine failure and must propagate rather
|
|
77
|
+
* than silently turn into an empty manifest. */
|
|
78
|
+
async function readDiffPatch(runDir: string): Promise<string> {
|
|
79
|
+
try {
|
|
80
|
+
return await Bun.file(join(runDir, 'diff.patch')).text()
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const code = err && typeof err === 'object' && 'code' in err ? err.code : undefined
|
|
83
|
+
if (code === 'ENOENT') return ''
|
|
84
|
+
throw err
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Delete `shard-<n>.patch` files left by an earlier split. Re-splitting 7 shards
|
|
89
|
+
* into 4 would otherwise leave `shard-5.patch` .. `shard-7.patch` on disk: the
|
|
90
|
+
* manifest is authoritative for dispatch, but a stale patch makes the directory
|
|
91
|
+
* claim coverage the manifest does not have, and a resume that reads shard ids
|
|
92
|
+
* off disk would review a file set that no longer matches its id. Only the shard
|
|
93
|
+
* patches are touched: `manifest.json` stays in place until it is overwritten at
|
|
94
|
+
* the end (so there is never a window with no manifest), and `diff.patch` lives
|
|
95
|
+
* outside this directory and is what a single-shard manifest points at. */
|
|
96
|
+
async function clearStaleShardPatches(shardsDir: string): Promise<void> {
|
|
97
|
+
let entries: string[]
|
|
98
|
+
try {
|
|
99
|
+
entries = await readdir(shardsDir)
|
|
100
|
+
} catch {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
for (const name of entries) {
|
|
104
|
+
if (/^shard-\d+\.patch$/.test(name)) await rm(join(shardsDir, name), { force: true })
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Split `$RUN_DIR/diff.patch` into budgeted shards. `diff.patch` is never
|
|
110
|
+
* modified: shards are views over it, which is what keeps the critic's diff
|
|
111
|
+
* excerpts, dedupe's evidence check, and the report's rendering working
|
|
112
|
+
* unchanged.
|
|
113
|
+
*
|
|
114
|
+
* Re-splitting an already-sharded run invalidates any
|
|
115
|
+
* `findings/<focus>.shard-<n>.json` from the previous split: the same shard id
|
|
116
|
+
* now names a different file set. Stage 4's resume rule keys on those files, so
|
|
117
|
+
* the caller must delete them before re-dispatching.
|
|
118
|
+
*/
|
|
119
|
+
export async function shardDiff(
|
|
120
|
+
runDir: string,
|
|
121
|
+
opts: { budget?: number; maxFiles?: number } = {},
|
|
122
|
+
): Promise<ShardManifest> {
|
|
123
|
+
const budget = opts.budget ?? DEFAULT_SHARD_BUDGET
|
|
124
|
+
const maxFiles = opts.maxFiles ?? DEFAULT_SHARD_MAX_FILES
|
|
125
|
+
const diff = await readDiffPatch(runDir)
|
|
126
|
+
const chunks = splitFileChunks(diff)
|
|
127
|
+
const totalLines = chunks.reduce((n, c) => n + c.lines, 0)
|
|
128
|
+
const shardsDir = join(runDir, 'shards')
|
|
129
|
+
await clearStaleShardPatches(shardsDir)
|
|
130
|
+
await mkdir(shardsDir, { recursive: true })
|
|
131
|
+
|
|
132
|
+
const planned = planShards(chunks, budget, maxFiles)
|
|
133
|
+
let shards: ShardEntry[]
|
|
134
|
+
if (planned.length === 0) {
|
|
135
|
+
shards = []
|
|
136
|
+
} else if (planned.length === 1) {
|
|
137
|
+
// Single-shard passthrough: stage 4 takes the same path it took on 0.9.0.
|
|
138
|
+
const only = planned[0] ?? []
|
|
139
|
+
shards = [
|
|
140
|
+
{
|
|
141
|
+
id: 1,
|
|
142
|
+
path: 'diff.patch',
|
|
143
|
+
files: only.map((c) => c.path),
|
|
144
|
+
lines: only.reduce((n, c) => n + c.lines, 0),
|
|
145
|
+
},
|
|
146
|
+
]
|
|
147
|
+
} else {
|
|
148
|
+
shards = []
|
|
149
|
+
for (const [i, members] of planned.entries()) {
|
|
150
|
+
const id = i + 1
|
|
151
|
+
const rel = `shards/shard-${id}.patch`
|
|
152
|
+
await writeFile(join(runDir, rel), members.map((c) => c.text).join(''))
|
|
153
|
+
shards.push({
|
|
154
|
+
id,
|
|
155
|
+
path: rel,
|
|
156
|
+
files: members.map((c) => c.path),
|
|
157
|
+
lines: members.reduce((n, c) => n + c.lines, 0),
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const manifest: ShardManifest = {
|
|
163
|
+
budget,
|
|
164
|
+
maxFiles,
|
|
165
|
+
totalFiles: chunks.length,
|
|
166
|
+
totalLines,
|
|
167
|
+
shards,
|
|
168
|
+
}
|
|
169
|
+
await writeFile(join(shardsDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
|
|
170
|
+
return manifest
|
|
171
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/** The resume ladder. Exported so `skill-lint` can assert that the diagnostic
|
|
5
|
+
* stage names SKILL.md logs (`filter`, `tests-check`, `shard-coverage`, ...) stay
|
|
6
|
+
* out of it: a diagnostic entry that lands on this list would move `next`. */
|
|
7
|
+
export const ORDER = [
|
|
5
8
|
'setup',
|
|
6
9
|
'context',
|
|
7
10
|
'specialists',
|