severally-mcp 2.4.0 → 2.4.2
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.ja.md +44 -19
- package/README.md +44 -18
- package/package.json +3 -1
- package/plugins/severally/.claude-plugin/plugin.json +1 -1
- package/plugins/severally/.codex-plugin/plugin.json +1 -1
- package/plugins/severally/dist/severally-mcp.mjs +585 -255
- package/plugins/severally/skills/_template/reference.md.tmpl +2 -2
- package/plugins/severally/skills/antigravity/severally/reference.md +2 -2
- package/plugins/severally/skills/claude/severally/reference.md +2 -2
- package/plugins/severally/skills/codex/severally/reference.md +2 -2
- package/plugins/severally/skills/opencode/severally/reference.md +2 -2
- package/scripts/build.mjs +57 -54
- package/scripts/doctor.mjs +15 -4
- package/scripts/install.mjs +73 -9
- package/scripts/live-check.mjs +11 -5
- package/scripts/live-mcp-check.mjs +10 -4
- package/src/adapters/antigravity-sandbox.mjs +24 -6
- package/src/adapters/antigravity.mjs +80 -17
- package/src/adapters/claude-code.mjs +49 -8
- package/src/adapters/codex.mjs +56 -6
- package/src/adapters/opencode-sandbox.mjs +20 -2
- package/src/adapters/opencode.mjs +115 -19
- package/src/brief.mjs +84 -7
- package/src/consultants.mjs +184 -25
- package/src/cross-spawn-escape.d.ts +7 -0
- package/src/export.mjs +50 -6
- package/src/expose-paths.mjs +149 -7
- package/src/failures.mjs +8 -1
- package/src/jobs.mjs +578 -132
- package/src/jsonc.mjs +11 -3
- package/src/parse-result.mjs +91 -14
- package/src/platform.mjs +52 -2
- package/src/policy.mjs +151 -28
- package/src/redact.mjs +22 -3
- package/src/result-schema.mjs +13 -0
- package/src/run.mjs +158 -12
- package/src/schema.mjs +84 -11
- package/src/server.mjs +49 -14
- package/src/status.mjs +16 -4
- package/src/store.mjs +287 -6
package/src/expose-paths.mjs
CHANGED
|
@@ -22,7 +22,58 @@ import { spawnSync } from 'node:child_process';
|
|
|
22
22
|
import { POLICY, isProtectedPath } from './policy.mjs';
|
|
23
23
|
import { redact } from './redact.mjs';
|
|
24
24
|
|
|
25
|
+
/** @typedef {{ files: number, bytes: number }} ExposeTotals */
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A path left out of the copy, with why: 'ignored', 'symlink', 'protected',
|
|
29
|
+
* 'submodule', 'gone', 'masked' or 'replaced'. `exposed_as` names it where a
|
|
30
|
+
* follow-up carries over an earlier round's entry.
|
|
31
|
+
* @typedef {{ source: string, reason: string, exposed_as?: string | undefined }} SkippedPath
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One exposed entry as the walk and the copy report it. `kind` is 'file',
|
|
36
|
+
* 'directory', 'diff' or 'change_files'.
|
|
37
|
+
* @typedef {object} ExposeEntry
|
|
38
|
+
* @property {number} index
|
|
39
|
+
* @property {string} source
|
|
40
|
+
* @property {string} kind
|
|
41
|
+
* @property {string} exposedAs
|
|
42
|
+
* @property {number} files
|
|
43
|
+
* @property {number} bytes
|
|
44
|
+
* @property {string[]} paths
|
|
45
|
+
* @property {string | null} [sha256]
|
|
46
|
+
* @property {string | undefined} [base]
|
|
47
|
+
* @property {string[]} [changes]
|
|
48
|
+
* @property {boolean} [changed] a follow-up's copy of a parent path: whether its bytes changed since
|
|
49
|
+
* @property {boolean} [inherited] a follow-up copied it again from the parent round
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What an expose_paths request comes to: the entries, their totals, and what was left out.
|
|
54
|
+
* @typedef {{ entries: ExposeEntry[], totals: ExposeTotals, skipped: SkippedPath[] }} ExposeManifest
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/** A path as the lead names it, or as an earlier round numbered it. @typedef {string | { source: string, index: number }} ExposePathSpec */
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* An entry of an earlier round's brief record (briefRecord's `expose_paths.entries`).
|
|
61
|
+
* @typedef {object} ExposedEntryRecord
|
|
62
|
+
* @property {number} index
|
|
63
|
+
* @property {string} source
|
|
64
|
+
* @property {string | undefined} [exposed_as]
|
|
65
|
+
* @property {string | undefined} [kind]
|
|
66
|
+
* @property {string | undefined} [base]
|
|
67
|
+
* @property {string[] | undefined} [changes]
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/** @typedef {{ path: string, status: string }} RepoChange */
|
|
71
|
+
|
|
25
72
|
export class ExposeError extends Error {
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} message
|
|
75
|
+
* @param {string} code
|
|
76
|
+
*/
|
|
26
77
|
constructor(message, code) {
|
|
27
78
|
super(message);
|
|
28
79
|
this.name = 'ExposeError';
|
|
@@ -30,7 +81,13 @@ export class ExposeError extends Error {
|
|
|
30
81
|
}
|
|
31
82
|
}
|
|
32
83
|
|
|
33
|
-
/**
|
|
84
|
+
/** @param {unknown} err */
|
|
85
|
+
const messageOf = (err) => (err instanceof Error ? err.message : String(err));
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A size a reader can judge at a glance; the brief is read, not parsed.
|
|
89
|
+
* @param {number} bytes
|
|
90
|
+
*/
|
|
34
91
|
export function humanSize(bytes) {
|
|
35
92
|
if (bytes < 1024) return `${bytes} B`;
|
|
36
93
|
const kb = bytes / 1024;
|
|
@@ -42,6 +99,7 @@ export function humanSize(bytes) {
|
|
|
42
99
|
// Getting it wrong is survivable in one direction only -- a binary treated as
|
|
43
100
|
// text would be corrupted by redact()'s utf8 round-trip, so the test errs
|
|
44
101
|
// toward calling things binary.
|
|
102
|
+
/** @param {Uint8Array} buf */
|
|
45
103
|
function isBinary(buf) {
|
|
46
104
|
const len = Math.min(buf.length, 8_000);
|
|
47
105
|
for (let i = 0; i < len; i++) if (buf[i] === 0) return true;
|
|
@@ -50,10 +108,15 @@ function isBinary(buf) {
|
|
|
50
108
|
|
|
51
109
|
// Before a file is read: the caps are a few megabytes, and a 1 GB file must
|
|
52
110
|
// be refused by its size, not after it has been read into memory and written.
|
|
111
|
+
/**
|
|
112
|
+
* @param {ExposeTotals} totals
|
|
113
|
+
* @param {number} size
|
|
114
|
+
*/
|
|
53
115
|
function ensureRoom(totals, size) {
|
|
54
116
|
checkBudget({ files: totals.files + 1, bytes: totals.bytes + size });
|
|
55
117
|
}
|
|
56
118
|
|
|
119
|
+
/** @param {ExposeTotals} totals */
|
|
57
120
|
function checkBudget(totals) {
|
|
58
121
|
if (totals.files > POLICY.input.exposeFilesMax) {
|
|
59
122
|
throw new ExposeError(
|
|
@@ -74,12 +137,13 @@ function checkBudget(totals) {
|
|
|
74
137
|
// A path the lead named, as opposed to one found while walking: it is refused
|
|
75
138
|
// rather than skipped, because a lead who typed a symlink meant something by
|
|
76
139
|
// it and should hear which entry could not be used.
|
|
140
|
+
/** @param {string} source */
|
|
77
141
|
function statEntry(source) {
|
|
78
142
|
let stat;
|
|
79
143
|
try {
|
|
80
144
|
stat = fs.lstatSync(source);
|
|
81
145
|
} catch (err) {
|
|
82
|
-
throw new ExposeError(`expose_paths entry "${source}" cannot be read: ${err
|
|
146
|
+
throw new ExposeError(`expose_paths entry "${source}" cannot be read: ${messageOf(err)}`, 'not_found');
|
|
83
147
|
}
|
|
84
148
|
if (stat.isSymbolicLink()) {
|
|
85
149
|
throw new ExposeError(`expose_paths entry "${source}" is a symlink; name the path it points at instead`, 'invalid_type');
|
|
@@ -94,6 +158,13 @@ function statEntry(source) {
|
|
|
94
158
|
// accounts for the file (it owns the totals), and the budget is checked after
|
|
95
159
|
// every single one, so an oversized tree stops at the file that crossed the
|
|
96
160
|
// line rather than after the whole thing has been walked or copied.
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} root
|
|
163
|
+
* @param {string} dir
|
|
164
|
+
* @param {(abs: string, rel: string) => void} onFile
|
|
165
|
+
* @param {ExposeTotals} totals
|
|
166
|
+
* @param {SkippedPath[]} skipped
|
|
167
|
+
*/
|
|
97
168
|
function walkDir(root, dir, onFile, totals, skipped) {
|
|
98
169
|
let items;
|
|
99
170
|
try {
|
|
@@ -101,7 +172,7 @@ function walkDir(root, dir, onFile, totals, skipped) {
|
|
|
101
172
|
// entries were created.
|
|
102
173
|
items = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
103
174
|
} catch (err) {
|
|
104
|
-
throw new ExposeError(`expose_paths entry "${dir}" cannot be read: ${err
|
|
175
|
+
throw new ExposeError(`expose_paths entry "${dir}" cannot be read: ${messageOf(err)}`, 'not_found');
|
|
105
176
|
}
|
|
106
177
|
for (const item of items) {
|
|
107
178
|
const child = path.join(dir, item.name);
|
|
@@ -138,10 +209,18 @@ function walkDir(root, dir, onFile, totals, skipped) {
|
|
|
138
209
|
|
|
139
210
|
// walkDir's order: name by name at every level, not the whole path as one
|
|
140
211
|
// string ("a/x" comes before "a-b" there, after it as a string).
|
|
212
|
+
/**
|
|
213
|
+
* @param {string} a
|
|
214
|
+
* @param {string} b
|
|
215
|
+
*/
|
|
141
216
|
function byPathParts(a, b) {
|
|
142
217
|
const x = a.split('/');
|
|
143
218
|
const y = b.split('/');
|
|
144
|
-
for (
|
|
219
|
+
for (const [i, part] of x.entries()) {
|
|
220
|
+
const other = y[i];
|
|
221
|
+
if (other === undefined) break;
|
|
222
|
+
if (part !== other) return part < other ? -1 : 1;
|
|
223
|
+
}
|
|
145
224
|
return x.length - y.length;
|
|
146
225
|
}
|
|
147
226
|
|
|
@@ -152,6 +231,14 @@ function byPathParts(a, b) {
|
|
|
152
231
|
// submodule, or reach outside the directory. False when this is not a work
|
|
153
232
|
// tree, git is missing or lists nothing (an ignored directory the lead named
|
|
154
233
|
// on purpose): the caller walks it instead.
|
|
234
|
+
/**
|
|
235
|
+
* @param {string} dir
|
|
236
|
+
* @param {string} exposedAs
|
|
237
|
+
* @param {(abs: string, rel: string) => void} onFile
|
|
238
|
+
* @param {ExposeTotals} totals
|
|
239
|
+
* @param {SkippedPath[]} skipped
|
|
240
|
+
* @returns {boolean}
|
|
241
|
+
*/
|
|
155
242
|
function walkGit(dir, exposedAs, onFile, totals, skipped) {
|
|
156
243
|
let listed;
|
|
157
244
|
let ignored;
|
|
@@ -217,8 +304,14 @@ function walkGit(dir, exposedAs, onFile, totals, skipped) {
|
|
|
217
304
|
|
|
218
305
|
// Each path is a string, or { source, index } to keep the name an earlier
|
|
219
306
|
// round gave it.
|
|
307
|
+
/**
|
|
308
|
+
* @param {ExposePathSpec[]} exposePaths
|
|
309
|
+
* @param {(abs: string, rel: string, index: number, source: string, totals: ExposeTotals) => void} onFile
|
|
310
|
+
* @returns {ExposeManifest}
|
|
311
|
+
*/
|
|
220
312
|
function walkEntries(exposePaths, onFile) {
|
|
221
313
|
const totals = { files: 0, bytes: 0 };
|
|
314
|
+
/** @type {SkippedPath[]} */
|
|
222
315
|
const skipped = [];
|
|
223
316
|
const entries = exposePaths.map((p, i) => {
|
|
224
317
|
const { source, index } = typeof p === 'string' ? { source: p, index: i } : p;
|
|
@@ -229,6 +322,7 @@ function walkEntries(exposePaths, onFile) {
|
|
|
229
322
|
const before = { files: totals.files, bytes: totals.bytes };
|
|
230
323
|
// What a directory holds, for the brief: not every consultant has a tool
|
|
231
324
|
// that lists a directory, and a name it cannot see is a file it never reads.
|
|
325
|
+
/** @type {string[]} */
|
|
232
326
|
const paths = [];
|
|
233
327
|
if (isProtectedPath(source)) {
|
|
234
328
|
skipped.push({ source, reason: 'protected' });
|
|
@@ -236,6 +330,7 @@ function walkEntries(exposePaths, onFile) {
|
|
|
236
330
|
onFile(source, '', index, source, totals);
|
|
237
331
|
checkBudget(totals);
|
|
238
332
|
} else {
|
|
333
|
+
/** @type {(abs: string, rel: string) => void} */
|
|
239
334
|
const add = (abs, rel) => {
|
|
240
335
|
paths.push(rel.split(path.sep).join('/'));
|
|
241
336
|
onFile(abs, rel, index, source, totals);
|
|
@@ -261,6 +356,8 @@ function walkEntries(exposePaths, onFile) {
|
|
|
261
356
|
* real copy may write slightly fewer bytes once a text file's
|
|
262
357
|
* credential-shaped strings are masked, which only ever shrinks what was
|
|
263
358
|
* already reported.
|
|
359
|
+
* @param {ExposePathSpec[] | null | undefined} exposePaths
|
|
360
|
+
* @returns {ExposeManifest | null}
|
|
264
361
|
*/
|
|
265
362
|
export function previewExposePaths(exposePaths) {
|
|
266
363
|
if (!exposePaths || exposePaths.length === 0) return null;
|
|
@@ -277,11 +374,15 @@ export function previewExposePaths(exposePaths) {
|
|
|
277
374
|
* are copied unchanged, since a utf8 round-trip would corrupt them. Walks
|
|
278
375
|
* from scratch and re-checks the caps, so a tree that grew or vanished since
|
|
279
376
|
* the preview fails here instead of being copied in part.
|
|
377
|
+
* @param {string} workdir
|
|
378
|
+
* @param {ExposePathSpec[] | null | undefined} exposePaths
|
|
379
|
+
* @returns {ExposeManifest | null}
|
|
280
380
|
*/
|
|
281
381
|
export function materializeExposedPaths(workdir, exposePaths) {
|
|
282
382
|
if (!exposePaths || exposePaths.length === 0) return null;
|
|
283
383
|
const root = path.join(workdir, 'workspace');
|
|
284
384
|
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
385
|
+
/** @type {import('node:crypto').Hash[]} */
|
|
285
386
|
const hashes = [];
|
|
286
387
|
const manifest = walkEntries(exposePaths, (abs, rel, index, source, totals) => {
|
|
287
388
|
ensureRoom(totals, fs.statSync(abs).size);
|
|
@@ -297,6 +398,10 @@ export function materializeExposedPaths(workdir, exposePaths) {
|
|
|
297
398
|
|
|
298
399
|
// The bytes a consultant gets for one file, written to dest: valid-utf8 text
|
|
299
400
|
// through redact(), anything else unchanged.
|
|
401
|
+
/**
|
|
402
|
+
* @param {string} abs
|
|
403
|
+
* @param {string} dest
|
|
404
|
+
*/
|
|
300
405
|
function copyMasked(abs, dest) {
|
|
301
406
|
const buf = fs.readFileSync(abs);
|
|
302
407
|
let out = buf;
|
|
@@ -319,12 +424,21 @@ function copyMasked(abs, dest) {
|
|
|
319
424
|
// sha256 covers the copies in path order, so the same files give the same
|
|
320
425
|
// hash however they were found. `gone` is where a path that no longer exists
|
|
321
426
|
// goes; by default it is dropped, as a change listing a vanished path is.
|
|
427
|
+
/**
|
|
428
|
+
* @param {string} workdir
|
|
429
|
+
* @param {string} top
|
|
430
|
+
* @param {RepoChange[]} changes
|
|
431
|
+
* @param {ExposeManifest} manifest
|
|
432
|
+
* @param {(c: RepoChange) => void} shown
|
|
433
|
+
* @param {((c: RepoChange) => void) | null} [gone]
|
|
434
|
+
*/
|
|
322
435
|
function copyRepoFiles(workdir, top, changes, manifest, shown, gone = null) {
|
|
323
436
|
const root = path.join(workdir, 'workspace', 'repo');
|
|
324
437
|
// Made even when nothing is copied (a change that only deletes): each
|
|
325
438
|
// member's workspace is copied from here.
|
|
326
439
|
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
327
440
|
const realTop = fs.realpathSync(top);
|
|
441
|
+
/** @type {{ path: string, size: number }[]} */
|
|
328
442
|
const copied = [];
|
|
329
443
|
for (const c of changes) {
|
|
330
444
|
const abs = path.join(top, c.path);
|
|
@@ -388,11 +502,17 @@ function copyRepoFiles(workdir, top, changes, manifest, shown, gone = null) {
|
|
|
388
502
|
* ('masked'). The parent's diff is not taken again: the files it touched are
|
|
389
503
|
* re-copied as they are now (`diffs`, for reexposeChange), because a diff
|
|
390
504
|
* against a base that has since moved would be a different change.
|
|
505
|
+
* @param {ExposedEntryRecord[]} parentEntries
|
|
506
|
+
* @param {string[]} exposePaths
|
|
391
507
|
*/
|
|
392
508
|
export function planInheritance(parentEntries, exposePaths) {
|
|
509
|
+
/** @type {{ source: string, index: number }[]} */
|
|
393
510
|
const paths = [];
|
|
511
|
+
/** @type {SkippedPath[]} */
|
|
394
512
|
const skipped = [];
|
|
513
|
+
/** @type {ExposedEntryRecord[]} */
|
|
395
514
|
const diffs = [];
|
|
515
|
+
/** @param {string} p */
|
|
396
516
|
const exists = (p) => {
|
|
397
517
|
try {
|
|
398
518
|
fs.lstatSync(p);
|
|
@@ -433,12 +553,17 @@ export function planInheritance(parentEntries, exposePaths) {
|
|
|
433
553
|
* The files an earlier round's change touched, copied again under
|
|
434
554
|
* workspace/repo/ as they are now, added to `manifest` as a 'change_files'
|
|
435
555
|
* entry. Read from that round's record of the change, not from git.
|
|
556
|
+
* @param {string} workdir
|
|
557
|
+
* @param {ExposedEntryRecord} entry
|
|
558
|
+
* @param {ExposeManifest} manifest
|
|
559
|
+
* @returns {boolean}
|
|
436
560
|
*/
|
|
437
561
|
export function reexposeChange(workdir, entry, manifest) {
|
|
562
|
+
/** @type {RepoChange[]} */
|
|
438
563
|
const changes = [];
|
|
439
564
|
for (const c of entry.changes ?? []) {
|
|
440
565
|
const m = /^(.*) \(([^()]+)\)$/.exec(c);
|
|
441
|
-
const p = m
|
|
566
|
+
const p = m?.[1] ?? c;
|
|
442
567
|
if (p.includes('[REDACTED')) {
|
|
443
568
|
manifest.skipped.push({ source: path.join(entry.source, p), reason: 'masked', exposed_as: `workspace/repo/${p}` });
|
|
444
569
|
continue;
|
|
@@ -456,6 +581,7 @@ export function reexposeChange(workdir, entry, manifest) {
|
|
|
456
581
|
changes.push({ path: p, status: deleted ? (back ? 'restored' : 'D') : (m?.[2] ?? 'M') });
|
|
457
582
|
}
|
|
458
583
|
const before = { ...manifest.totals };
|
|
584
|
+
/** @type {RepoChange[]} */
|
|
459
585
|
const shown = [];
|
|
460
586
|
const { copied, sha256 } = copyRepoFiles(
|
|
461
587
|
workdir,
|
|
@@ -496,6 +622,10 @@ export function reexposeChange(workdir, entry, manifest) {
|
|
|
496
622
|
// switched off, pathspecs are literal, and no shell is involved.
|
|
497
623
|
// ponytail: a clean filter (filter.<driver>.clean in .git/config) still runs
|
|
498
624
|
// on `git diff` of the working tree; .git/config is local, not cloned.
|
|
625
|
+
/**
|
|
626
|
+
* @param {string} cwd
|
|
627
|
+
* @param {string[]} args
|
|
628
|
+
*/
|
|
499
629
|
function git(cwd, args) {
|
|
500
630
|
// Without the server's own GIT_* (GIT_DIR, GIT_INDEX_FILE ... would point
|
|
501
631
|
// at another repository), and without taking the lead's index lock.
|
|
@@ -508,7 +638,7 @@ function git(cwd, args) {
|
|
|
508
638
|
});
|
|
509
639
|
if (r.error || r.status !== 0) {
|
|
510
640
|
const why =
|
|
511
|
-
|
|
641
|
+
r.error && 'code' in r.error && r.error.code === 'ENOENT'
|
|
512
642
|
? 'git is not on PATH'
|
|
513
643
|
: String(r.stderr ?? r.error?.message ?? '').trim();
|
|
514
644
|
throw new ExposeError(`context.expose_diff: git ${args[0]} failed in "${cwd}": ${why}`, 'not_found');
|
|
@@ -516,6 +646,7 @@ function git(cwd, args) {
|
|
|
516
646
|
return r.stdout;
|
|
517
647
|
}
|
|
518
648
|
|
|
649
|
+
/** @param {Buffer} buf */
|
|
519
650
|
const nulSplit = (buf) => buf.toString('utf8').split('\0').filter(Boolean);
|
|
520
651
|
|
|
521
652
|
/**
|
|
@@ -525,6 +656,11 @@ const nulSplit = (buf) => buf.toString('utf8').split('\0').filter(Boolean);
|
|
|
525
656
|
* name the copies; deleted files are listed, not copied. Protected paths are
|
|
526
657
|
* left out of the copy and of the diff. Returns the diff text, masked and cut
|
|
527
658
|
* to maxChars, for the brief.
|
|
659
|
+
* @param {string} workdir
|
|
660
|
+
* @param {{ repo: string, base?: string | undefined }} source
|
|
661
|
+
* @param {ExposeManifest} manifest
|
|
662
|
+
* @param {number} maxChars
|
|
663
|
+
* @returns {string}
|
|
528
664
|
*/
|
|
529
665
|
export function exposeDiff(workdir, { repo, base = 'HEAD' }, manifest, maxChars) {
|
|
530
666
|
const top = git(repo, ['rev-parse', '--show-toplevel']).toString('utf8').trim();
|
|
@@ -532,12 +668,18 @@ export function exposeDiff(workdir, { repo, base = 'HEAD' }, manifest, maxChars)
|
|
|
532
668
|
const tokens = nulSplit(
|
|
533
669
|
git(top, ['diff', '--name-status', '-z', '--no-renames', '--no-relative', '--no-ext-diff', '--no-textconv', base, '--']),
|
|
534
670
|
);
|
|
671
|
+
/** @type {RepoChange[]} */
|
|
535
672
|
const changes = [];
|
|
536
|
-
for (let i = 0; i + 1 < tokens.length; i += 2)
|
|
673
|
+
for (let i = 0; i + 1 < tokens.length; i += 2) {
|
|
674
|
+
const status = tokens[i];
|
|
675
|
+
const file = tokens[i + 1];
|
|
676
|
+
if (status !== undefined && file !== undefined) changes.push({ status, path: file });
|
|
677
|
+
}
|
|
537
678
|
for (const p of nulSplit(git(top, ['ls-files', '--others', '--exclude-standard', '-z'])))
|
|
538
679
|
changes.push({ status: 'untracked', path: p });
|
|
539
680
|
|
|
540
681
|
const before = { ...manifest.totals };
|
|
682
|
+
/** @type {RepoChange[]} */
|
|
541
683
|
const shown = [];
|
|
542
684
|
const { copied, sha256 } = copyRepoFiles(workdir, top, changes, manifest, (c) => shown.push(c));
|
|
543
685
|
if (shown.length === 0) {
|
package/src/failures.mjs
CHANGED
|
@@ -36,13 +36,20 @@ const RULES = [
|
|
|
36
36
|
],
|
|
37
37
|
];
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* @param {string | null | undefined} text
|
|
41
|
+
* @param {string} [fallback]
|
|
42
|
+
*/
|
|
39
43
|
export function classifyMessage(text, fallback = 'cli_error') {
|
|
40
44
|
if (!text) return fallback;
|
|
41
45
|
for (const [re, kind] of RULES) if (re.test(text)) return kind;
|
|
42
46
|
return fallback;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Whether the lead may reasonably try again (with a longer budget, or later).
|
|
51
|
+
* @param {string} kind
|
|
52
|
+
*/
|
|
46
53
|
export function isRetriable(kind) {
|
|
47
54
|
return kind === 'timeout' || kind === 'cli_error' || kind === 'rate_limited' || kind === 'interrupted';
|
|
48
55
|
}
|