@vibe-agent-toolkit/cli 0.2.0-rc.4 â 0.2.0-rc.6
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/dist/bin.js +35 -2
- package/dist/bin.js.map +1 -1
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +169 -17
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/claude/claude-config.d.ts.map +1 -1
- package/dist/commands/claude/claude-config.js +6 -0
- package/dist/commands/claude/claude-config.js.map +1 -1
- package/dist/commands/claude/org/helpers.d.ts +59 -1
- package/dist/commands/claude/org/helpers.d.ts.map +1 -1
- package/dist/commands/claude/org/helpers.js +58 -7
- package/dist/commands/claude/org/helpers.js.map +1 -1
- package/dist/commands/claude/org/index.d.ts.map +1 -1
- package/dist/commands/claude/org/index.js +24 -16
- package/dist/commands/claude/org/index.js.map +1 -1
- package/dist/commands/claude/org/skills.d.ts +428 -0
- package/dist/commands/claude/org/skills.d.ts.map +1 -1
- package/dist/commands/claude/org/skills.js +1189 -91
- package/dist/commands/claude/org/skills.js.map +1 -1
- package/dist/commands/claude/org/users.d.ts.map +1 -1
- package/dist/commands/claude/org/users.js +3 -1
- package/dist/commands/claude/org/users.js.map +1 -1
- package/dist/commands/claude/org/workspaces.d.ts.map +1 -1
- package/dist/commands/claude/org/workspaces.js +5 -2
- package/dist/commands/claude/org/workspaces.js.map +1 -1
- package/dist/commands/claude/plugin/install.js +0 -1
- package/dist/commands/claude/plugin/install.js.map +1 -1
- package/dist/commands/corpus/runner.d.ts.map +1 -1
- package/dist/commands/corpus/runner.js +1 -19
- package/dist/commands/corpus/runner.js.map +1 -1
- package/dist/commands/inventory.js +1 -1
- package/dist/commands/inventory.js.map +1 -1
- package/dist/commands/resources/check-progress.d.ts +145 -0
- package/dist/commands/resources/check-progress.d.ts.map +1 -0
- package/dist/commands/resources/check-progress.js +205 -0
- package/dist/commands/resources/check-progress.js.map +1 -0
- package/dist/commands/resources/check-supervisor.d.ts +465 -0
- package/dist/commands/resources/check-supervisor.d.ts.map +1 -0
- package/dist/commands/resources/check-supervisor.js +566 -0
- package/dist/commands/resources/check-supervisor.js.map +1 -0
- package/dist/commands/resources/check.d.ts +454 -0
- package/dist/commands/resources/check.d.ts.map +1 -0
- package/dist/commands/resources/check.js +1237 -0
- package/dist/commands/resources/check.js.map +1 -0
- package/dist/commands/resources/index.d.ts.map +1 -1
- package/dist/commands/resources/index.js +277 -6
- package/dist/commands/resources/index.js.map +1 -1
- package/dist/commands/resources/query.d.ts +89 -0
- package/dist/commands/resources/query.d.ts.map +1 -0
- package/dist/commands/resources/query.js +146 -0
- package/dist/commands/resources/query.js.map +1 -0
- package/dist/commands/resources/scan.js +1 -1
- package/dist/commands/resources/scan.js.map +1 -1
- package/dist/commands/skill/review-checklist.d.ts.map +1 -1
- package/dist/commands/skill/review-checklist.js +35 -0
- package/dist/commands/skill/review-checklist.js.map +1 -1
- package/dist/commands/skill/test/configure.d.ts +37 -0
- package/dist/commands/skill/test/configure.d.ts.map +1 -1
- package/dist/commands/skill/test/configure.js +57 -12
- package/dist/commands/skill/test/configure.js.map +1 -1
- package/dist/commands/skills/install.js +0 -1
- package/dist/commands/skills/install.js.map +1 -1
- package/dist/commands/skills/skill-discovery.d.ts.map +1 -1
- package/dist/commands/skills/skill-discovery.js +4 -2
- package/dist/commands/skills/skill-discovery.js.map +1 -1
- package/dist/utils/agent-runner.d.ts.map +1 -1
- package/dist/utils/agent-runner.js +7 -3
- package/dist/utils/agent-runner.js.map +1 -1
- package/dist/utils/command-error.d.ts +56 -1
- package/dist/utils/command-error.d.ts.map +1 -1
- package/dist/utils/command-error.js +66 -3
- package/dist/utils/command-error.js.map +1 -1
- package/dist/utils/config-loader.d.ts.map +1 -1
- package/dist/utils/config-loader.js +30 -7
- package/dist/utils/config-loader.js.map +1 -1
- package/dist/utils/issue-severity.d.ts +12 -5
- package/dist/utils/issue-severity.d.ts.map +1 -1
- package/dist/utils/issue-severity.js +1 -1
- package/dist/utils/issue-severity.js.map +1 -1
- package/dist/utils/projection-query.d.ts +263 -0
- package/dist/utils/projection-query.d.ts.map +1 -0
- package/dist/utils/projection-query.js +262 -0
- package/dist/utils/projection-query.js.map +1 -0
- package/dist/utils/projection-store.d.ts +104 -2
- package/dist/utils/projection-store.d.ts.map +1 -1
- package/dist/utils/projection-store.js +126 -4
- package/dist/utils/projection-store.js.map +1 -1
- package/dist/utils/vat-bin-path.d.ts +36 -0
- package/dist/utils/vat-bin-path.d.ts.map +1 -0
- package/dist/utils/vat-bin-path.js +49 -0
- package/dist/utils/vat-bin-path.js.map +1 -0
- package/package.json +25 -14
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The progress log a supervised `vat resources check` writes, and the reader
|
|
3
|
+
* that turns whatever survived into a report.
|
|
4
|
+
*
|
|
5
|
+
* ## ðĻ Why progress goes to a FILE, and never to stdout or IPC
|
|
6
|
+
*
|
|
7
|
+
* The run this log exists for is one that gets `SIGKILL`ed. That is not a design
|
|
8
|
+
* preference: a statement blocked inside synchronous `node:sqlite` never returns
|
|
9
|
+
* to the event loop, so nothing in-process can interrupt it â measured on Node
|
|
10
|
+
* 24.13.1, `worker.terminate()` never resolves against such a thread and the
|
|
11
|
+
* parent's own `process.exit()` does not exit either. An external `SIGKILL` is
|
|
12
|
+
* the only lever, and `SIGKILL` cannot be handled, deferred or flushed.
|
|
13
|
+
*
|
|
14
|
+
* So the child's report of what it has finished has to be DURABLE at every
|
|
15
|
+
* instant, not merely sent. stdout is buffered by the pipe and lost with the
|
|
16
|
+
* process; an IPC message needs a live event loop to be delivered, which is the
|
|
17
|
+
* exact resource the hung child does not have. An `appendFileSync` per unit is
|
|
18
|
+
* on disk before the next unit starts, which is the only property that matters.
|
|
19
|
+
*
|
|
20
|
+
* Keeping it off stdout has a second benefit worth stating: stdout stays the one
|
|
21
|
+
* parseable document this verb has always published, so `vat resources check |
|
|
22
|
+
* jq` is unaffected by a mechanism it should never have to know about.
|
|
23
|
+
*
|
|
24
|
+
* ## ðŠĪ The reader must expect DAMAGE, not merely tolerate it
|
|
25
|
+
*
|
|
26
|
+
* The signal lands wherever it lands, including part-way through a write, so the
|
|
27
|
+
* last line of a killed run's log is routinely half a JSON object. A reader that
|
|
28
|
+
* threw there would replace the bounded failure report with a stack trace at
|
|
29
|
+
* exactly the moment the report is the only thing the operator gets. A reader
|
|
30
|
+
* that GUESSED at a damaged line would be worse still: it would publish a cost
|
|
31
|
+
* record for a measurement nobody took, beside ones somebody did.
|
|
32
|
+
*
|
|
33
|
+
* Every line is therefore validated against a `.strict()` schema and silently
|
|
34
|
+
* dropped when it does not parse. That schema is also the whole of the format
|
|
35
|
+
* contract â there is no version integer here and there must never be one. This
|
|
36
|
+
* file is written and read by ONE process pair within ONE run of ONE build; the
|
|
37
|
+
* question a version integer pretends to answer ("can I read this?") is answered
|
|
38
|
+
* exactly and automatically by the schema the reader compiles with.
|
|
39
|
+
*/
|
|
40
|
+
import { appendFileSync } from 'node:fs';
|
|
41
|
+
import { z } from 'zod';
|
|
42
|
+
/**
|
|
43
|
+
* The population finished â how it was obtained, what it cost, and how much of
|
|
44
|
+
* the tree it covered.
|
|
45
|
+
*
|
|
46
|
+
* Written the instant the projection is ready, because a kill BEFORE this line
|
|
47
|
+
* means there is no projection at all and therefore no honest document to
|
|
48
|
+
* publish. The parent distinguishes those two endings by this line's presence
|
|
49
|
+
* and nothing else, so it must never be written early or optimistically.
|
|
50
|
+
*/
|
|
51
|
+
const PopulationEntrySchema = z.object({
|
|
52
|
+
kind: z.literal('population'),
|
|
53
|
+
population: z.union([z.literal('derived'), z.literal('store')]),
|
|
54
|
+
populationMs: z.number(),
|
|
55
|
+
membersEnumerated: z.number(),
|
|
56
|
+
}).strict();
|
|
57
|
+
/**
|
|
58
|
+
* A check's statement is ABOUT to run.
|
|
59
|
+
*
|
|
60
|
+
* The only record that can name the rule that hung: a check which never returns
|
|
61
|
+
* never files a cost, so without this line a killed run could say a check was in
|
|
62
|
+
* flight but not which one â and "one of your forty rules hangs" is not an
|
|
63
|
+
* actionable report.
|
|
64
|
+
*/
|
|
65
|
+
const StartEntrySchema = z.object({
|
|
66
|
+
kind: z.literal('start'),
|
|
67
|
+
name: z.string(),
|
|
68
|
+
}).strict();
|
|
69
|
+
/**
|
|
70
|
+
* A check completed â the same fields `CheckCost` publishes.
|
|
71
|
+
*
|
|
72
|
+
* ðŠĪ `rows` is optional and `broken` is `true`-or-absent, mirroring `CheckCost`
|
|
73
|
+
* exactly: a statement that threw has no row count, and `rows: 0` on it would
|
|
74
|
+
* read as "selected nothing and passed". Keeping the two shapes identical is
|
|
75
|
+
* what lets the parent hand recovered records to the same `buildCheckOutputData`
|
|
76
|
+
* a completed run uses, rather than growing a second payload builder that could
|
|
77
|
+
* drift from it.
|
|
78
|
+
*/
|
|
79
|
+
const CheckEntrySchema = z.object({
|
|
80
|
+
kind: z.literal('check'),
|
|
81
|
+
name: z.string(),
|
|
82
|
+
durationMs: z.number(),
|
|
83
|
+
rows: z.number().optional(),
|
|
84
|
+
broken: z.literal(true).optional(),
|
|
85
|
+
}).strict();
|
|
86
|
+
/**
|
|
87
|
+
* Every check has filed its cost; the document is about to be built.
|
|
88
|
+
*
|
|
89
|
+
* ðĻ **The defect this closes is a run killed AFTER it had its answer.** When
|
|
90
|
+
* the last check files its cost the child is not finished: it still resolves
|
|
91
|
+
* severities and serialises the document, and a document with 5,000 issues is
|
|
92
|
+
* 639 KB of YAML. That phase emitted nothing, so the watchdog's clock kept
|
|
93
|
+
* running from the final cost line and a budget that expired there SIGKILLed a
|
|
94
|
+
* run whose work was complete â a false failure that discards a correct result.
|
|
95
|
+
*
|
|
96
|
+
* One `appendFileSync` buys severity resolution and serialisation a fresh budget
|
|
97
|
+
* window. It carries no fields: the fact that it was written is the whole
|
|
98
|
+
* message.
|
|
99
|
+
*
|
|
100
|
+
* ðŠĪ **Emitted by `runDeclaredChecks`, before it resolves severities.** It first
|
|
101
|
+
* shipped a level up, from `runOutcome` after that function had already
|
|
102
|
+
* returned, which left `resolveIssueSeverity` charged to the last check's window
|
|
103
|
+
* while this comment claimed otherwise. What it still does NOT cover is the last
|
|
104
|
+
* check's row-to-issue conversion â `runChecks` files that check's cost before
|
|
105
|
+
* converting its rows â which is why the report's `idle` sentence hedges.
|
|
106
|
+
*/
|
|
107
|
+
const ChecksCompleteEntrySchema = z.object({
|
|
108
|
+
kind: z.literal('checks-complete'),
|
|
109
|
+
}).strict();
|
|
110
|
+
const ProgressEntrySchema = z.discriminatedUnion('kind', [
|
|
111
|
+
PopulationEntrySchema,
|
|
112
|
+
StartEntrySchema,
|
|
113
|
+
CheckEntrySchema,
|
|
114
|
+
ChecksCompleteEntrySchema,
|
|
115
|
+
]);
|
|
116
|
+
/**
|
|
117
|
+
* Read whatever of the log survived.
|
|
118
|
+
*
|
|
119
|
+
* @param text - The log's raw contents, tail damage and all
|
|
120
|
+
* @returns The lines that validate, in the order they were written
|
|
121
|
+
*/
|
|
122
|
+
export function parseProgressLog(text) {
|
|
123
|
+
const entries = [];
|
|
124
|
+
for (const line of text.split('\n')) {
|
|
125
|
+
if (line.trim() === '')
|
|
126
|
+
continue;
|
|
127
|
+
const entry = readLine(line);
|
|
128
|
+
if (entry !== undefined)
|
|
129
|
+
entries.push(entry);
|
|
130
|
+
}
|
|
131
|
+
return entries;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* One line, or nothing.
|
|
135
|
+
*
|
|
136
|
+
* ðŠĪ Two separate refusals, deliberately: `JSON.parse` throws on the truncated
|
|
137
|
+
* tail, and the schema refuses a well-formed object that is not an entry this
|
|
138
|
+
* build knows. Collapsing them into one `try` around a parse-and-validate would
|
|
139
|
+
* still work, but the two failures mean different things â damage versus drift â
|
|
140
|
+
* and only the second is worth a future reader's attention.
|
|
141
|
+
*
|
|
142
|
+
* @param line - One line of the log
|
|
143
|
+
* @returns The entry, or undefined when the line is damaged or unknown
|
|
144
|
+
*/
|
|
145
|
+
function readLine(line) {
|
|
146
|
+
let value;
|
|
147
|
+
try {
|
|
148
|
+
value = JSON.parse(line);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
const parsed = ProgressEntrySchema.safeParse(value);
|
|
154
|
+
return parsed.success ? parsed.data : undefined;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Which unit was running when the log stopped growing.
|
|
158
|
+
*
|
|
159
|
+
* ð The population sentinel is keyed on the population line's ABSENCE rather
|
|
160
|
+
* than on there being no `start` lines. Those are different states: a run killed
|
|
161
|
+
* during population has no projection and no honest document, while a run killed
|
|
162
|
+
* between the population and the first statement has both.
|
|
163
|
+
*
|
|
164
|
+
* @param entries - What {@link parseProgressLog} recovered
|
|
165
|
+
* @returns The unit in flight
|
|
166
|
+
*/
|
|
167
|
+
export function unitInFlight(entries) {
|
|
168
|
+
if (!entries.some((entry) => entry.kind === 'population'))
|
|
169
|
+
return { kind: 'population' };
|
|
170
|
+
const completed = new Set(entries.filter((entry) => entry.kind === 'check').map((entry) => entry.name));
|
|
171
|
+
// ðŠĪ The `completed` set is what makes this unique, NOT the scan direction.
|
|
172
|
+
// The runner is serial â one statement at a time, its cost filed before the
|
|
173
|
+
// next `start` â so at most one `start` can lack a cost, and a scan from
|
|
174
|
+
// either end reaches the same entry. An earlier version scanned backwards and
|
|
175
|
+
// justified it as "the LAST unfinished start"; reversing it left all 10 unit
|
|
176
|
+
// tests green, which is the honest tell that the direction was never the
|
|
177
|
+
// guard. Dropping the `completed` filter, by contrast, reds immediately.
|
|
178
|
+
const started = entries.find((entry) => entry.kind === 'start' && !completed.has(entry.name));
|
|
179
|
+
if (started !== undefined)
|
|
180
|
+
return { kind: 'check', name: started.name };
|
|
181
|
+
// ð Checked AFTER the in-flight statement, never before. The line means "no
|
|
182
|
+
// check is running any more"; a `start` without a cost means one still is, and
|
|
183
|
+
// a log holding both is damaged in a way that must still name the rule.
|
|
184
|
+
if (entries.some((entry) => entry.kind === 'checks-complete'))
|
|
185
|
+
return { kind: 'reporting' };
|
|
186
|
+
return { kind: 'idle' };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Open the log for append, and hand back the one-line-per-unit writer.
|
|
190
|
+
*
|
|
191
|
+
* â ïļ `appendFileSync`, and it must stay synchronous. The child that writes these
|
|
192
|
+
* lines is about to enter a blocking native call; an asynchronous write would be
|
|
193
|
+
* queued behind work the event loop never gets back to, so the line naming the
|
|
194
|
+
* rule that hangs would exist only in memory â where `SIGKILL` destroys it.
|
|
195
|
+
*
|
|
196
|
+
* @param path - Where the supervisor is watching
|
|
197
|
+
* @returns A writer that appends one entry and returns once it is on disk
|
|
198
|
+
*/
|
|
199
|
+
export function createProgressWriter(path) {
|
|
200
|
+
return (entry) => {
|
|
201
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- a path this process minted for this run
|
|
202
|
+
appendFileSync(path, `${JSON.stringify(entry)}\n`, 'utf-8');
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=check-progress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-progress.js","sourceRoot":"","sources":["../../../src/commands/resources/check-progress.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The time bound on `vat resources check`, enforced from OUTSIDE the process
|
|
3
|
+
* doing the work.
|
|
4
|
+
*
|
|
5
|
+
* ## ðĻ Why an external process, and not any of the cheaper things
|
|
6
|
+
*
|
|
7
|
+
* This verb runs adopter-authored SQL as an unattended CI gate. An accidental
|
|
8
|
+
* cross join, or a `WITH RECURSIVE` with no termination, runs forever â and
|
|
9
|
+
* every in-process lever was tried and MEASURED on Node 24.13.1 before this
|
|
10
|
+
* shape was chosen. Do not re-derive them:
|
|
11
|
+
*
|
|
12
|
+
* - **A worker thread does not work.** `worker.terminate()` never resolves
|
|
13
|
+
* against a thread blocked in native SQLite, and the parent's own
|
|
14
|
+
* `process.exit()` does not exit either. V8 can unwind a worker spinning in
|
|
15
|
+
* JS â which is why the parse pool's `terminate()` genuinely works â but a
|
|
16
|
+
* thread inside a synchronous native call is not spinning in JS. Do NOT
|
|
17
|
+
* "align" this design with the parse pool's.
|
|
18
|
+
* - **A signal handler is a REGRESSION.** A process blocked in synchronous
|
|
19
|
+
* `node:sqlite` dies INSTANTLY on SIGINT while no handler is installed, and
|
|
20
|
+
* SURVIVES both SIGINT and SIGTERM once one is â the handler is a JS callback,
|
|
21
|
+
* and the event loop that would schedule it is the resource that is blocked.
|
|
22
|
+
* Installing one removes the operator's Ctrl-C. There is no
|
|
23
|
+
* `process.on('SIGINT')` here and there must never be.
|
|
24
|
+
* - **The kill is `SIGKILL` because SIGKILL cannot be REGRESSED.** No code the
|
|
25
|
+
* child runs, now or after any future edit, can handle, ignore or block it.
|
|
26
|
+
*
|
|
27
|
+
* â ïļ This header used to claim, as measured fact, that "SIGTERM does not kill
|
|
28
|
+
* the child either, for the same reason". **That is false and was measured to
|
|
29
|
+
* be false**: `kill -TERM` on a child blocked inside synchronous `node:sqlite`
|
|
30
|
+
* `.all()` killed it instantly. The measurement above is about a process that
|
|
31
|
+
* HAS a JS handler installed â a handler is a JS callback and the blocked
|
|
32
|
+
* event loop is what would schedule it â and with no handler installed
|
|
33
|
+
* SIGTERM's default disposition is kernel-delivered termination that needs no
|
|
34
|
+
* cooperation at all. So SIGTERM works today only because nothing here
|
|
35
|
+
* installs a handler, which is a property one future edit could silently
|
|
36
|
+
* remove; SIGKILL is not a property anything can remove. The design did not
|
|
37
|
+
* change, only the reason for it â recorded because a false claim is at its
|
|
38
|
+
* worst in a section that tells the next reader not to re-derive it.
|
|
39
|
+
* - **The database cannot be handed over.** `DatabaseSync` has no `serialize()`
|
|
40
|
+
* and `:memory:` is per-CONNECTION, so whoever runs the SQL populates its own
|
|
41
|
+
* projection. That is why the child is a whole `vat resources check` run and
|
|
42
|
+
* not a query executor.
|
|
43
|
+
*
|
|
44
|
+
* ## ð The budget is ABSENCE OF PROGRESS, not total runtime
|
|
45
|
+
*
|
|
46
|
+
* A total-runtime bound has to be set above the slowest legitimate run on the
|
|
47
|
+
* largest adopter tree, which makes it useless as a hang detector, and it kills
|
|
48
|
+
* healthy runs that are merely large. So the parent watches the child's progress
|
|
49
|
+
* log and resets its clock on every new line â INCLUDING the population line,
|
|
50
|
+
* which is what bounds the population itself without charging it the first
|
|
51
|
+
* check's budget. A gate that can hang forever is the thing being fixed, and
|
|
52
|
+
* that includes hanging before the first statement.
|
|
53
|
+
*
|
|
54
|
+
* â ïļ **That property holds for the CHECKS. It does NOT hold for the population,
|
|
55
|
+
* and the difference is not a detail.** The population emits exactly ONE line,
|
|
56
|
+
* at its end, so for the single longest unit in a cold run the budget degrades
|
|
57
|
+
* to precisely the total-runtime bound this section argues against: a population
|
|
58
|
+
* that takes longer than the budget is killed however healthily it is working.
|
|
59
|
+
*
|
|
60
|
+
* That is not cheap to fix and a half-fix would be worse than saying so.
|
|
61
|
+
* `onContributorTiming` does fire per contributor, but ~88% of a cold population
|
|
62
|
+
* is the BLOB stage â one call, remark-parsing every document, 33-35 s of the
|
|
63
|
+
* 35 s â and it emits nothing. Per-contributor lines would therefore bound the
|
|
64
|
+
* cheap part, leave the expensive part exactly as unbounded as it is now, and
|
|
65
|
+
* make the whole thing LOOK instrumented. So the population stays one
|
|
66
|
+
* un-instrumented unit, the default budget is set well above a cold one, and the
|
|
67
|
+
* per-unit property is claimed only where it is true.
|
|
68
|
+
*
|
|
69
|
+
* ## Why the child is the CLI itself
|
|
70
|
+
*
|
|
71
|
+
* The child is `vat resources check` with a hidden `--cost-log` flag, running
|
|
72
|
+
* the ordinary in-process path. Forking the logic would give the supervised lane
|
|
73
|
+
* and the unsupervised one two implementations of the same gate â and the one
|
|
74
|
+
* nobody runs interactively is the one that would rot.
|
|
75
|
+
*/
|
|
76
|
+
/** What the watchdog remembers between polls. */
|
|
77
|
+
export interface WatchdogState {
|
|
78
|
+
/** How large the progress log was when it last changed. */
|
|
79
|
+
readonly bytesSeen: number;
|
|
80
|
+
/** When that was â the instant the budget is counted from. */
|
|
81
|
+
readonly quietSince: number;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Decide whether the child has gone quiet for longer than the budget.
|
|
85
|
+
*
|
|
86
|
+
* Pure, and separated from the timer for exactly that reason: the interesting
|
|
87
|
+
* behaviour is "a run that keeps making progress is never killed, however long
|
|
88
|
+
* it takes", and a test of that against a real timer would be a twenty-second
|
|
89
|
+
* test that flakes. Here it is twenty function calls.
|
|
90
|
+
*
|
|
91
|
+
* ðŠĪ Growth is measured in BYTES rather than parsed lines. A partially written
|
|
92
|
+
* line also grows the file, and treating that as progress is the safe direction:
|
|
93
|
+
* the child is demonstrably alive and writing. Counting lines instead would mean
|
|
94
|
+
* a very slow write could look like silence.
|
|
95
|
+
*
|
|
96
|
+
* @param state - What the previous poll saw
|
|
97
|
+
* @param observation - The run
|
|
98
|
+
* @param observation.bytes - The progress log's current size
|
|
99
|
+
* @param observation.now - The clock, in milliseconds
|
|
100
|
+
* @param observation.budgetMs - How long silence is allowed to last
|
|
101
|
+
* @returns The state to carry forward, and whether the budget was blown
|
|
102
|
+
*/
|
|
103
|
+
export declare function pollWatchdog(state: WatchdogState, observation: {
|
|
104
|
+
bytes: number;
|
|
105
|
+
now: number;
|
|
106
|
+
budgetMs: number;
|
|
107
|
+
}): {
|
|
108
|
+
state: WatchdogState;
|
|
109
|
+
breach: boolean;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Read `--budget`, or supply the default.
|
|
113
|
+
*
|
|
114
|
+
* ðŠĪ A typo is REFUSED rather than defaulted. `--budget 2O` (letter O) would
|
|
115
|
+
* otherwise run with a five-minute bound while the operator believed they had
|
|
116
|
+
* set two seconds â and they would only find out from a CI job that hung for
|
|
117
|
+
* five minutes instead of two. Thrown, so it exits 2 as an operator error, in
|
|
118
|
+
* line with this command's existing contract for an unknown `--check` name.
|
|
119
|
+
*
|
|
120
|
+
* ðĻ **A BLANK value is refused before `Number` ever sees it, and that is the
|
|
121
|
+
* more important half.** `Number('')`, `Number(' ')` and `Number('\n')` are all
|
|
122
|
+
* `0` â finite, non-negative, and therefore accepted by the range guard below â
|
|
123
|
+
* and `0` is this flag's documented escape hatch meaning *no bound at all*. So
|
|
124
|
+
* `--budget "$CHECK_BUDGET"` with the variable unset did not shorten the bound
|
|
125
|
+
* and did not complain: it silently REMOVED it, on the one flag whose entire
|
|
126
|
+
* purpose is to stop an unattended job hanging forever. An unset shell variable
|
|
127
|
+
* is a far more common CI accident than the typo the guard was written for.
|
|
128
|
+
*
|
|
129
|
+
* ðĻ **The blank string was only the visible half of the hazard, and the guard
|
|
130
|
+
* below it is the rest.** What actually removes the bound is `Number(raw) === 0`
|
|
131
|
+
* for a raw string nobody would recognise as a zero â `--budget 1e-400`
|
|
132
|
+
* underflows to `0`, and `--budget -0` is negative zero, which sails past the
|
|
133
|
+
* `seconds < 0` test because `-0 < 0` is false. Both were MEASURED to run
|
|
134
|
+
* completely unbounded, in process, exiting 0 with no supervision and no word
|
|
135
|
+
* said. So a value that PARSES to zero is refused unless the operator literally
|
|
136
|
+
* wrote a zero: `raw.trim() !== '0'` and it is an operator error. The escape
|
|
137
|
+
* hatch is still there, it just has to be spelled the one unambiguous way.
|
|
138
|
+
*
|
|
139
|
+
* â ïļ That deliberately refuses `0.0` and `0x0` as well. They are not accidents
|
|
140
|
+
* an operator can fall into, they cost one character to rewrite, and a guard
|
|
141
|
+
* with an exception list is a guard whose next exception nobody argues about.
|
|
142
|
+
*
|
|
143
|
+
* ð **Non-zero hex is deliberately KEPT.** `Number('0x10')` is 16 and
|
|
144
|
+
* `--budget 0x10` runs with a sixteen-second bound. That is unambiguous, it
|
|
145
|
+
* cannot arise by accident the way an empty variable can, and refusing it would
|
|
146
|
+
* buy nothing â recorded here so the next reader does not "fix" it into a
|
|
147
|
+
* refusal.
|
|
148
|
+
*
|
|
149
|
+
* @param raw - Whatever Commander parsed, or undefined
|
|
150
|
+
* @returns The budget in seconds; 0 means no bound
|
|
151
|
+
* @throws When the value is blank, means zero without saying so, or is not a
|
|
152
|
+
* non-negative number
|
|
153
|
+
*/
|
|
154
|
+
export declare function parseBudgetSeconds(raw: string | undefined): number;
|
|
155
|
+
/**
|
|
156
|
+
* Does this process do the work, or supervise a child that does?
|
|
157
|
+
*
|
|
158
|
+
* ðĻ **The recursion guard, and the reason it is a named function.** `--cost-log`
|
|
159
|
+
* is what a supervising parent hands its child, and it must WIN against the
|
|
160
|
+
* budget â a child that consulted the budget first would spawn a child of its
|
|
161
|
+
* own, which would spawn a child of its own. A condition inline in the command
|
|
162
|
+
* would be a fork bomb one refactor away with nothing asserting on it.
|
|
163
|
+
*
|
|
164
|
+
* `--budget 0` also runs here: there is nothing to supervise, and a spawn would
|
|
165
|
+
* cost a second process startup to buy no bound at all.
|
|
166
|
+
*
|
|
167
|
+
* @param options - The run
|
|
168
|
+
* @param options.costLog - The hidden `--cost-log` value, or undefined
|
|
169
|
+
* @param options.budgetSecs - The bound in seconds; 0 means none
|
|
170
|
+
* @returns True when the checks run in this process
|
|
171
|
+
*/
|
|
172
|
+
export declare function runsInThisProcess(options: {
|
|
173
|
+
costLog: string | undefined;
|
|
174
|
+
budgetSecs: number;
|
|
175
|
+
}): boolean;
|
|
176
|
+
/**
|
|
177
|
+
* Refuse a combination where one flag the operator passed would do nothing.
|
|
178
|
+
*
|
|
179
|
+
* ðĻ **The defect: `--cost-log` wins the fork above unconditionally**, so
|
|
180
|
+
* `vat resources check --budget 60 --cost-log /tmp/x` ran entirely UNBOUNDED and
|
|
181
|
+
* said nothing about it. That is harmless to the recursion guard â a child must
|
|
182
|
+
* keep winning â but an explicitly-passed, documented flag that is silently
|
|
183
|
+
* inert is exactly the shape an operator never discovers until the hang it was
|
|
184
|
+
* meant to prevent.
|
|
185
|
+
*
|
|
186
|
+
* â ïļ **The real spawn path must survive this.** {@link runsInThisProcess}'s
|
|
187
|
+
* caller builds the child's argv with `--cost-log` and does NOT forward
|
|
188
|
+
* `--budget` â which is why the RAW flag is the discriminator here rather than
|
|
189
|
+
* the parsed seconds: the child's `budgetSecs` is the 300 s default, not
|
|
190
|
+
* anything the operator typed.
|
|
191
|
+
*
|
|
192
|
+
* `--budget 0` alongside `--cost-log` is allowed: both say "nobody is
|
|
193
|
+
* supervising this", so there is no contradiction to report.
|
|
194
|
+
*
|
|
195
|
+
* @param options - The run
|
|
196
|
+
* @param options.costLog - The hidden `--cost-log` value, or undefined
|
|
197
|
+
* @param options.budgetRaw - What the operator actually typed for `--budget`
|
|
198
|
+
* @param options.budgetSecs - The parsed bound; 0 means none
|
|
199
|
+
* @throws When both flags were passed and the budget could not be honoured
|
|
200
|
+
*/
|
|
201
|
+
export declare function requireSupervisableFlags(options: {
|
|
202
|
+
costLog: string | undefined;
|
|
203
|
+
budgetRaw: string | undefined;
|
|
204
|
+
budgetSecs: number;
|
|
205
|
+
}): void;
|
|
206
|
+
/**
|
|
207
|
+
* Why a child ended without completing.
|
|
208
|
+
*
|
|
209
|
+
* Structured rather than a prose string because the two common signals point at
|
|
210
|
+
* DIFFERENT remedies â `SIGABRT` is Node's own heap abort and wants a narrower
|
|
211
|
+
* statement, `SIGKILL` from outside wants a bigger runner â and the message that
|
|
212
|
+
* says so belongs beside the other findings, not here.
|
|
213
|
+
*/
|
|
214
|
+
export type AbnormalDeath = {
|
|
215
|
+
readonly kind: 'signal';
|
|
216
|
+
readonly signal: string;
|
|
217
|
+
} | {
|
|
218
|
+
readonly kind: 'spawn-failed';
|
|
219
|
+
readonly binary: string;
|
|
220
|
+
readonly detail: string;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The watchdog fired and the KILL WAS REFUSED â so the child is, as far as
|
|
224
|
+
* this process can tell, still running.
|
|
225
|
+
*
|
|
226
|
+
* ðĻ Distinct from `spawn-failed` and that distinction is the whole point.
|
|
227
|
+
* Both arrive on `child.on('error')` and the handler used to read every one of
|
|
228
|
+
* them as a spawn failure, which reported the exact opposite of what happened:
|
|
229
|
+
* "could not be started at all", "nothing ran at all", "the watchdog never
|
|
230
|
+
* fired". It started, it ran past its budget, the watchdog fired, and it would
|
|
231
|
+
* not die.
|
|
232
|
+
*
|
|
233
|
+
* `pid` is carried because it is the only handle the operator has left on a
|
|
234
|
+
* process this command could not stop.
|
|
235
|
+
*/
|
|
236
|
+
| {
|
|
237
|
+
readonly kind: 'kill-failed';
|
|
238
|
+
readonly detail: string;
|
|
239
|
+
readonly pid: number | undefined;
|
|
240
|
+
} | {
|
|
241
|
+
readonly kind: 'no-status';
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The child exited with a CODE, and published nothing at all.
|
|
245
|
+
*
|
|
246
|
+
* ðĻ The Windows half of the signal-death defect, and it survived the fix for
|
|
247
|
+
* the POSIX half. A fatal abort â Node's own heap limit among them â has no
|
|
248
|
+
* signal to report on a platform that has none, so `close` fires with
|
|
249
|
+
* `(134, null)` where macOS and Linux give `(null, 'SIGABRT')`. That shape is
|
|
250
|
+
* indistinguishable from an ordinary nonzero exit to
|
|
251
|
+
* {@link resolveChildEnding}, which read it as a COMPLETED run and handed the
|
|
252
|
+
* parent an empty string to forward. Measured on `windows-latest`: exit 134,
|
|
253
|
+
* zero bytes of stdout, on precisely the runaway the bound exists to catch.
|
|
254
|
+
*
|
|
255
|
+
* It did not fail OPEN â 134 is not 0 â so a gate still went red. But the
|
|
256
|
+
* report was absent, which is the other half of what the original defect did.
|
|
257
|
+
*
|
|
258
|
+
* `code` is carried because it is the only thing left that says HOW the child
|
|
259
|
+
* died: 128 + n is the conventional encoding of a fatal signal, and
|
|
260
|
+
* `deathRemedy` reads the signal back out of it.
|
|
261
|
+
*/
|
|
262
|
+
| {
|
|
263
|
+
readonly kind: 'no-output';
|
|
264
|
+
readonly code: number;
|
|
265
|
+
};
|
|
266
|
+
/** What a child's ending MEANT, once the watchdog's belief is reconciled with it. */
|
|
267
|
+
export type RunResolution = {
|
|
268
|
+
readonly kind: 'completed';
|
|
269
|
+
readonly code: number;
|
|
270
|
+
} | {
|
|
271
|
+
readonly kind: 'killed';
|
|
272
|
+
} | {
|
|
273
|
+
readonly kind: 'abnormal';
|
|
274
|
+
readonly death: AbnormalDeath;
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* Read `close`'s two arguments, and the watchdog's belief, into one answer.
|
|
278
|
+
*
|
|
279
|
+
* ## ðĻ The defect this closes: a signal death READ AS SUCCESS
|
|
280
|
+
*
|
|
281
|
+
* `close` fires with `(code, signal)`, and a process that dies from a signal has
|
|
282
|
+
* `code === null` with `signal` set. This handler used to read only the code and
|
|
283
|
+
* coerce it â `resolve(exitCode ?? 0)` â so every signal death became a
|
|
284
|
+
* COMPLETED run with exit code 0. The parent then forwarded an empty stdout and
|
|
285
|
+
* exited 0, and a CI gate reads that as a clean pass.
|
|
286
|
+
*
|
|
287
|
+
* It needed no external actor. A check whose statement materialises an unbounded
|
|
288
|
+
* result set â `SELECT i FROM <infinite recursive CTE>`, the row-returning twin
|
|
289
|
+
* of the aggregate this whole module exists for â makes `.all()` fill the heap
|
|
290
|
+
* until V8 gives up and Node ABORTS. Measured: `PARENT_EXIT=0`, zero bytes of
|
|
291
|
+
* stdout, on precisely the input the bound exists to catch. Through the
|
|
292
|
+
* unsupervised lane the identical input exits 134.
|
|
293
|
+
*
|
|
294
|
+
* Every memory death arrives here: Node's own heap abort, a kernel OOM killer on
|
|
295
|
+
* a memory-capped runner (which picks the large CHILD, never the idle parent),
|
|
296
|
+
* and a container limit. `?? 0` turned all of them into a pass.
|
|
297
|
+
*
|
|
298
|
+
* ## ðŠĪ A kill only counts if it LANDED
|
|
299
|
+
*
|
|
300
|
+
* `killed` says the watchdog fired, not that the child died of it. libuv runs
|
|
301
|
+
* timers BEFORE the poll phase that reaps the child, so the timer can fire in
|
|
302
|
+
* the same loop turn as a child that has already exited cleanly. A NORMAL ending
|
|
303
|
+
* â a non-null code with no signal â is therefore honoured whatever `killed`
|
|
304
|
+
* says: that run finished on its own terms and its answer is real. Reporting it
|
|
305
|
+
* as killed fails closed, but it discards a correct result, which is its own
|
|
306
|
+
* kind of wrong.
|
|
307
|
+
*
|
|
308
|
+
* ## ðĻ The ORDER of the tests below is load-bearing
|
|
309
|
+
*
|
|
310
|
+
* `spawnError` used to be tested FIRST, so `killed` was never consulted on that
|
|
311
|
+
* branch â and `child.on('error')` is not only a spawn-failure listener. Node
|
|
312
|
+
* emits `error` on the ChildProcess when `subprocess.kill()` itself fails, so a
|
|
313
|
+
* refused SIGKILL landed on the spawn-failure branch and the report said the
|
|
314
|
+
* child "could not be started at all", that "nothing ran at all, so this is an
|
|
315
|
+
* installation or PATH problem", and that "the watchdog never fired". Three
|
|
316
|
+
* statements, all false, about a runaway that was still executing.
|
|
317
|
+
*
|
|
318
|
+
* So: a real ending first, then what the SUPERVISOR did (a refused kill, then a
|
|
319
|
+
* kill), and only then a failure to start. A run whose kill was refused must
|
|
320
|
+
* never claim the watchdog was uninvolved.
|
|
321
|
+
*
|
|
322
|
+
* @param ending - What `close` reported, plus what the supervisor did and saw
|
|
323
|
+
* @param ending.code - `close`'s first argument
|
|
324
|
+
* @param ending.signal - `close`'s second argument
|
|
325
|
+
* @param ending.killed - Whether the watchdog sent SIGKILL
|
|
326
|
+
* @param ending.spawnError - Set when the child never started at all
|
|
327
|
+
* @param ending.killFailure - Set when the watchdog's SIGKILL was REFUSED
|
|
328
|
+
* @returns What actually happened
|
|
329
|
+
*/
|
|
330
|
+
export declare function resolveChildEnding(ending: {
|
|
331
|
+
code: number | null;
|
|
332
|
+
signal: string | null;
|
|
333
|
+
killed: boolean;
|
|
334
|
+
spawnError?: {
|
|
335
|
+
binary: string;
|
|
336
|
+
detail: string;
|
|
337
|
+
} | undefined;
|
|
338
|
+
killFailure?: {
|
|
339
|
+
detail: string;
|
|
340
|
+
pid: number | undefined;
|
|
341
|
+
} | undefined;
|
|
342
|
+
}): RunResolution;
|
|
343
|
+
/**
|
|
344
|
+
* Reconcile "how the process ended" with "what it left behind".
|
|
345
|
+
*
|
|
346
|
+
* ## ðĻ A completion that PUBLISHED NOTHING did not complete
|
|
347
|
+
*
|
|
348
|
+
* {@link resolveChildEnding} answers a question about the process â what `close`
|
|
349
|
+
* reported, reconciled with what the supervisor did. This answers a DIFFERENT
|
|
350
|
+
* one, about the artifact, and the two disagree in exactly one place: a child
|
|
351
|
+
* that exited with a code and wrote no document.
|
|
352
|
+
*
|
|
353
|
+
* Every in-process path through `checkCommand` ends at `emitCheckDocument` or at
|
|
354
|
+
* `handleCommandError`, and both write to stdout â a clean pass, a violation, a
|
|
355
|
+
* bricked config, a mistyped `--check`. There is no exit this command can take
|
|
356
|
+
* that legitimately publishes nothing. So an empty stdout is not a quiet run, it
|
|
357
|
+
* is a run that was cut off before it could say anything, and forwarding its
|
|
358
|
+
* exit code verbatim tells the operator a number and nothing else.
|
|
359
|
+
*
|
|
360
|
+
* ## ðŠĪ Why this is not folded into {@link resolveChildEnding}
|
|
361
|
+
*
|
|
362
|
+
* That function's branch ORDER is load-bearing and reasoned entirely about
|
|
363
|
+
* process endings and supervisor beliefs. The emptiness of stdout belongs to
|
|
364
|
+
* neither, and threading it through would put an artifact test inside an
|
|
365
|
+
* ordering argument that has already been got wrong twice. It runs AFTER, on the
|
|
366
|
+
* one branch it can change, and it changes no other.
|
|
367
|
+
*
|
|
368
|
+
* â ïļ **Blank, not unparseable.** The parent forwards the child's document
|
|
369
|
+
* verbatim precisely so it stays `--format`-agnostic, so it cannot ask whether
|
|
370
|
+
* the bytes parse without learning the format. A truncated document â an abort
|
|
371
|
+
* that landed mid-write â therefore still forwards, and that is the honest
|
|
372
|
+
* limit of this test rather than an oversight.
|
|
373
|
+
*
|
|
374
|
+
* @param resolution - How the process ended
|
|
375
|
+
* @param stdout - Everything the child wrote to stdout
|
|
376
|
+
* @returns The resolution, with a silent completion re-read as abnormal
|
|
377
|
+
*/
|
|
378
|
+
export declare function resolveSilentCompletion(resolution: RunResolution, stdout: string): RunResolution;
|
|
379
|
+
/** How a supervised run ended. */
|
|
380
|
+
export type SupervisedRun = {
|
|
381
|
+
readonly outcome: 'completed';
|
|
382
|
+
readonly code: number;
|
|
383
|
+
readonly stdout: string;
|
|
384
|
+
} | {
|
|
385
|
+
readonly outcome: 'killed';
|
|
386
|
+
readonly log: string;
|
|
387
|
+
readonly elapsedMs: number;
|
|
388
|
+
} | {
|
|
389
|
+
readonly outcome: 'abnormal';
|
|
390
|
+
readonly death: AbnormalDeath;
|
|
391
|
+
readonly log: string;
|
|
392
|
+
readonly elapsedMs: number;
|
|
393
|
+
};
|
|
394
|
+
/**
|
|
395
|
+
* Run `vat resources check` as a child, bounded by the budget.
|
|
396
|
+
*
|
|
397
|
+
* `stdio` is `['ignore', 'pipe', 'inherit']`: the child's stdout is accumulated
|
|
398
|
+
* so the parent can forward the document verbatim, and its stderr is INHERITED
|
|
399
|
+
* so warnings and blob-stage refusals stream live exactly as they do today.
|
|
400
|
+
*
|
|
401
|
+
* â ïļ Asynchronous `spawn`, never `spawnSync`. The parent's event loop is the
|
|
402
|
+
* thing holding the watchdog timer; a synchronous spawn would block it for
|
|
403
|
+
* precisely as long as the run it is supposed to be bounding.
|
|
404
|
+
*
|
|
405
|
+
* ðĻ There is an `error` listener as well as a `close` one, and it is not
|
|
406
|
+
* optional politeness: without it an unspawnable binary is an unhandled `error`
|
|
407
|
+
* event, which Node turns into a stack dump instead of a report. It fails
|
|
408
|
+
* closed, so this is legibility â but a stack dump is not a report.
|
|
409
|
+
*
|
|
410
|
+
* ## ðĻ `error` is NOT only a spawn-failure listener, and reading it as one
|
|
411
|
+
* ABANDONED A LIVE CHILD
|
|
412
|
+
*
|
|
413
|
+
* Node emits `error` on the ChildProcess when `subprocess.kill()` itself fails.
|
|
414
|
+
* The shipped function ends `else { /* Other error, almost certainly EPERM. *\/
|
|
415
|
+
* this.emit('error', new ErrnoException(err, 'kill')); }`. So: the watchdog
|
|
416
|
+
* breached, `killed` was set, the SIGKILL was refused, `error` fired, the
|
|
417
|
+
* handler recorded a SPAWN failure and resolved immediately â which cleared the
|
|
418
|
+
* watchdog timer, so no further kill could ever be attempted â and the parent
|
|
419
|
+
* published a report saying the child "could not be started at all" and "the
|
|
420
|
+
* watchdog never fired" while the runaway was still executing. The bound this
|
|
421
|
+
* module exists to enforce became an orphaned unbounded hang, described as its
|
|
422
|
+
* own opposite.
|
|
423
|
+
*
|
|
424
|
+
* The handler therefore forks on `killed`, and the two arms are deliberately
|
|
425
|
+
* asymmetric:
|
|
426
|
+
*
|
|
427
|
+
* - **Before the watchdog fired**, an `error` means the child never started.
|
|
428
|
+
* Resolve at once: there is nothing alive to wait for.
|
|
429
|
+
* - **After it fired**, an `error` means the KILL failed and the child is,
|
|
430
|
+
* as far as this process can tell, still running. Do NOT resolve. Resolving
|
|
431
|
+
* is what clears the timer, and clearing the timer is what makes the failure
|
|
432
|
+
* permanent. The poll keeps breaching (the log is not growing and
|
|
433
|
+
* `quietSince` is not reset), so it keeps re-sending SIGKILL, and a transient
|
|
434
|
+
* refusal still lands. Only after {@link KILL_ATTEMPTS_BEFORE_REPORTING}
|
|
435
|
+
* refusals does it give up â and it gives up by REPORTING, naming the pid,
|
|
436
|
+
* never by claiming the child never started.
|
|
437
|
+
*
|
|
438
|
+
* â ïļ Reachability of `EPERM` on one's own child is REASONED rather than
|
|
439
|
+
* reproduced â it is rare on macOS and Linux. Nothing in production was
|
|
440
|
+
* contorted to make it testable; the branch ordering and the message
|
|
441
|
+
* composition are pure functions and are pinned as such.
|
|
442
|
+
*
|
|
443
|
+
* @param options - The run
|
|
444
|
+
* @param options.args - The child's argv after the node binary, `--cost-log` included
|
|
445
|
+
* @param options.logPath - The progress log both sides agreed on
|
|
446
|
+
* @param options.budgetMs - How long silence is allowed to last
|
|
447
|
+
* @returns How the child ended, and what it left behind
|
|
448
|
+
*/
|
|
449
|
+
export declare function superviseCheck(options: {
|
|
450
|
+
args: readonly string[];
|
|
451
|
+
logPath: string;
|
|
452
|
+
budgetMs: number;
|
|
453
|
+
}): Promise<SupervisedRun>;
|
|
454
|
+
/**
|
|
455
|
+
* Run `work` with a fresh progress log, and remove it however that ends.
|
|
456
|
+
*
|
|
457
|
+
* â ïļ Ephemeral, per run, and it carries NO format version â see
|
|
458
|
+
* `check-progress.ts`. It is written and read by one process pair within one run
|
|
459
|
+
* of one build, so the reader's own `.strict()` schema is the entire contract.
|
|
460
|
+
*
|
|
461
|
+
* @param work - Given the log's path
|
|
462
|
+
* @returns Whatever `work` returned
|
|
463
|
+
*/
|
|
464
|
+
export declare function withProgressLog<T>(work: (logPath: string) => Promise<T>): Promise<T>;
|
|
465
|
+
//# sourceMappingURL=check-supervisor.d.ts.map
|