faberun 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/LICENSE +21 -0
- package/README.md +131 -0
- package/bin/faberun.mjs +25 -0
- package/integrations/claude-code/statusline-bench.sh +42 -0
- package/integrations/claude-code/statusline.sh +80 -0
- package/package.json +33 -0
- package/skills/faberun/SKILL.md +24 -0
- package/skills/faberun/references/contract.md +380 -0
- package/skills/faberun/references/engineering.md +29 -0
- package/skills/faberun/references/handoffs.md +26 -0
- package/skills/faberun/references/operations.md +184 -0
- package/skills/faberun/references/rules.md +35 -0
- package/skills/faberun/references/workflow.md +23 -0
- package/skills/init-agentkit/SKILL.md +108 -0
- package/skills/init-agentkit/scripts/install-agentkit.sh +127 -0
- package/skills/init-agentkit/templates/.claude/commands/create-adr.md +44 -0
- package/skills/init-agentkit/templates/.github/workflows/quality.yml +43 -0
- package/skills/init-agentkit/templates/.sentrux/baseline.json +9 -0
- package/skills/init-agentkit/templates/.sentrux/rules.toml +21 -0
- package/skills/init-agentkit/templates/AGENTS.md +110 -0
- package/skills/init-agentkit/templates/docs/ABSTRACTIONS.md +30 -0
- package/skills/init-agentkit/templates/docs/ARCHITECTURE.md +31 -0
- package/skills/init-agentkit/templates/docs/GETTING-STARTED.md +44 -0
- package/skills/init-agentkit/templates/docs/VISION.md +33 -0
- package/skills/init-agentkit/templates/docs/adr/0001-record-architecture-decisions.md +36 -0
- package/skills/init-agentkit/templates/docs/adr/0002-root-managed-ai-guidance.md +37 -0
- package/skills/init-agentkit/templates/docs/adr/0003-sentrux-structural-quality-gates.md +49 -0
- package/skills/init-agentkit/templates/docs/adr/README.md +52 -0
- package/skills/init-agentkit/templates/docs/sentrux.md +66 -0
- package/skills/init-agentkit/templates/githooks/commit-msg +22 -0
- package/skills/init-agentkit/templates/githooks/pre-commit +32 -0
- package/src/campaign/brief.mjs +394 -0
- package/src/campaign/chain.mjs +555 -0
- package/src/campaign/handoff.mjs +516 -0
- package/src/campaign/index.mjs +300 -0
- package/src/campaign/journal.mjs +347 -0
- package/src/campaign/layout.mjs +51 -0
- package/src/campaign/metrics-evals.mjs +25 -0
- package/src/campaign/metrics.mjs +517 -0
- package/src/campaign/projection.mjs +250 -0
- package/src/campaign/record.mjs +102 -0
- package/src/campaign/unpark.mjs +56 -0
- package/src/cli/brand.mjs +205 -0
- package/src/cli/campaign.mjs +730 -0
- package/src/cli/contract.mjs +67 -0
- package/src/cli/init.mjs +170 -0
- package/src/cli/launch.mjs +239 -0
- package/src/cli/seat.mjs +139 -0
- package/src/cli/setup.mjs +294 -0
- package/src/cli/skills.mjs +105 -0
- package/src/cli/update.mjs +216 -0
- package/src/cli.mjs +525 -0
- package/src/contract/articles.mjs +12 -0
- package/src/contract/assert.mjs +162 -0
- package/src/contract/definition-of-done.mjs +97 -0
- package/src/contract/final-verification.mjs +96 -0
- package/src/contract/index.mjs +641 -0
- package/src/contract/judge-envelope.mjs +25 -0
- package/src/contract/review-modes.mjs +151 -0
- package/src/contract/runtime.mjs +204 -0
- package/src/contract/schema-version.mjs +25 -0
- package/src/contract/scope-findings.mjs +77 -0
- package/src/contract/snapshot.mjs +639 -0
- package/src/contract/task-packet.mjs +495 -0
- package/src/contract/untrusted.mjs +75 -0
- package/src/contract/verification.mjs +185 -0
- package/src/contract/worker-result.mjs +138 -0
- package/src/engine/assignment.mjs +63 -0
- package/src/engine/backoff.mjs +492 -0
- package/src/engine/bulk-read.mjs +361 -0
- package/src/engine/cancel.mjs +177 -0
- package/src/engine/detach.mjs +101 -0
- package/src/engine/dispatch.mjs +752 -0
- package/src/engine/failover.mjs +192 -0
- package/src/engine/gate.mjs +183 -0
- package/src/engine/judge-gate.mjs +517 -0
- package/src/engine/lifecycle.mjs +772 -0
- package/src/engine/live-preflight.mjs +299 -0
- package/src/engine/mutation.mjs +146 -0
- package/src/engine/notify-queue.mjs +327 -0
- package/src/engine/process-identity.mjs +72 -0
- package/src/engine/process.mjs +774 -0
- package/src/engine/prompts.mjs +289 -0
- package/src/engine/recover.mjs +300 -0
- package/src/engine/result-file.mjs +222 -0
- package/src/engine/resume.mjs +635 -0
- package/src/engine/retry.mjs +334 -0
- package/src/engine/review.mjs +228 -0
- package/src/engine/run-command.mjs +287 -0
- package/src/engine/run-identity.mjs +411 -0
- package/src/engine/runtime-discovery.mjs +235 -0
- package/src/engine/scheduler.mjs +526 -0
- package/src/engine/scope.mjs +378 -0
- package/src/engine/settle.mjs +207 -0
- package/src/engine/state.mjs +148 -0
- package/src/engine/supervise.mjs +713 -0
- package/src/engine/verify.mjs +167 -0
- package/src/harnesses/agy/index.mjs +62 -0
- package/src/harnesses/catalogue.mjs +509 -0
- package/src/harnesses/claude/index.mjs +90 -0
- package/src/harnesses/codex/index.mjs +87 -0
- package/src/harnesses/dsh/closed-packet.patch.yml +42 -0
- package/src/harnesses/dsh/index.mjs +210 -0
- package/src/harnesses/dsh/runner.mjs +259 -0
- package/src/harnesses/exec-jsonl/index.mjs +788 -0
- package/src/harnesses/index.mjs +508 -0
- package/src/harnesses/protocol.mjs +531 -0
- package/src/harnesses/replay/bin.mjs +386 -0
- package/src/harnesses/replay/index.mjs +238 -0
- package/src/harnesses/zcode/index.mjs +276 -0
- package/src/host/config.mjs +87 -0
- package/src/host/home.mjs +149 -0
- package/src/host/package.mjs +23 -0
- package/src/host/preflight.mjs +520 -0
- package/src/host/tool-policy-decisions.mjs +341 -0
- package/src/host/tool-policy-hook.mjs +270 -0
- package/src/notify/index.mjs +359 -0
- package/src/notify/os-macos.mjs +81 -0
- package/src/repo/declared-paths.mjs +220 -0
- package/src/repo/integrate.mjs +546 -0
- package/src/repo/scope-closure.mjs +665 -0
- package/src/repo/signal-block.mjs +16 -0
- package/src/repo/signal.mjs +222 -0
- package/src/repo/source-identity.mjs +295 -0
- package/src/repo/workspace.mjs +557 -0
- package/src/repo/worktree.mjs +352 -0
- package/src/report/final.mjs +200 -0
- package/src/report/metrics-report.mjs +99 -0
- package/src/report/next.mjs +383 -0
- package/src/report/render.mjs +716 -0
- package/src/run/disk-gc.mjs +251 -0
- package/src/run/lock.mjs +329 -0
- package/src/run/node-store.mjs +62 -0
- package/src/run/operations.mjs +286 -0
- package/src/run/store.mjs +187 -0
- package/src/run/usage.mjs +337 -0
- package/src/seat/harnesses.mjs +83 -0
- package/src/seat/index.mjs +239 -0
- package/src/seat/tmux.mjs +208 -0
- package/src/util.mjs +0 -0
- package/src/web/api.mjs +371 -0
- package/src/web/boundary.mjs +88 -0
- package/src/web/index.html +299 -0
- package/src/web/server.mjs +552 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running a verification command and bounding what comes back.
|
|
3
|
+
*
|
|
4
|
+
* The controller runs each command itself, in the attempt workspace, with a
|
|
5
|
+
* deliberately narrow environment (`VERIFICATION_ENV_BASE_NAMES`): a worker must
|
|
6
|
+
* not be able to make a suite pass by exporting something. Output is captured
|
|
7
|
+
* head-and-tail rather than whole, because a fuzz log will happily fill a disk,
|
|
8
|
+
* and the process is killed by group so a test runner's children die with it.
|
|
9
|
+
*
|
|
10
|
+
* The schema for what may be run lives in `contract/verification.mjs`; this is
|
|
11
|
+
* only the doing.
|
|
12
|
+
*/
|
|
13
|
+
import { Buffer } from "node:buffer";
|
|
14
|
+
import { VERIFICATION_LIMITS, compactVerification, resolveVerificationCwd, validateVerificationCommands } from "../contract/verification.mjs";
|
|
15
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { processStartToken } from "../run/lock.mjs";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { runMutation } from "./mutation.mjs";
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
/** @typedef {import("../contract/verification.mjs").VerificationOptions} VerificationOptions */
|
|
21
|
+
|
|
22
|
+
/** @typedef {import("node:child_process").ChildProcess} ChildProcess */
|
|
23
|
+
/** @typedef {import("../contract/verification.mjs").VerificationAttempt} VerificationAttempt */
|
|
24
|
+
/** @typedef {import("../contract/verification.mjs").VerificationAttemptResult} VerificationAttemptResult */
|
|
25
|
+
/** @typedef {import("../contract/verification.mjs").VerificationCommand} VerificationCommand */
|
|
26
|
+
/** @typedef {import("../contract/verification.mjs").VerificationCommandResult} VerificationCommandResult */
|
|
27
|
+
/** @typedef {import("../contract/verification.mjs").VerificationResult} VerificationResult */
|
|
28
|
+
|
|
29
|
+
// Verification children receive only the declared environment-variable names
|
|
30
|
+
// plus a minimal base set needed to spawn a process. Ambient controller
|
|
31
|
+
// variables (including secrets) must never leak into verification commands.
|
|
32
|
+
const VERIFICATION_ENV_BASE_NAMES = Object.freeze([
|
|
33
|
+
"PATH",
|
|
34
|
+
"HOME",
|
|
35
|
+
"USER",
|
|
36
|
+
"LOGNAME",
|
|
37
|
+
"SHELL",
|
|
38
|
+
"TMPDIR",
|
|
39
|
+
"TEMP",
|
|
40
|
+
"TMP",
|
|
41
|
+
"SYSTEMROOT",
|
|
42
|
+
"WINDIR",
|
|
43
|
+
"COMSPEC",
|
|
44
|
+
"PATHEXT",
|
|
45
|
+
"LANG",
|
|
46
|
+
"LC_ALL",
|
|
47
|
+
"LC_CTYPE",
|
|
48
|
+
"TERM",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {VerificationCommand} command
|
|
53
|
+
* @returns {Record<string, string|undefined>}
|
|
54
|
+
*/
|
|
55
|
+
function verificationEnv(command) {
|
|
56
|
+
const names = new Set([...(command.env ?? []), ...VERIFICATION_ENV_BASE_NAMES]);
|
|
57
|
+
/** @type {Record<string, string|undefined>} */
|
|
58
|
+
const env = {};
|
|
59
|
+
for (const name of names) {
|
|
60
|
+
if (process.env[name] !== undefined) env[name] = process.env[name];
|
|
61
|
+
}
|
|
62
|
+
return env;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Run every declared command `repeat` times inside the workspace.
|
|
66
|
+
*
|
|
67
|
+
* @param {unknown} commands
|
|
68
|
+
* @param {string} baseCwd
|
|
69
|
+
* @param {VerificationOptions} options
|
|
70
|
+
* @returns {Promise<VerificationResult>}
|
|
71
|
+
*/
|
|
72
|
+
export async function runVerification(commands, baseCwd, options = {}) {
|
|
73
|
+
const validated = validateVerificationCommands(commands);
|
|
74
|
+
/** @type {VerificationCommandResult[]} */
|
|
75
|
+
const results = [];
|
|
76
|
+
for (const [commandIndex, command] of validated.entries()) {
|
|
77
|
+
const result = command.mutation
|
|
78
|
+
? await runMutationCommand(command, baseCwd, commandIndex, options)
|
|
79
|
+
: await runRepeatedCommand(command, baseCwd, commandIndex, options);
|
|
80
|
+
results.push(result);
|
|
81
|
+
if (options.logDir) {
|
|
82
|
+
mkdirSync(options.logDir, { recursive: true });
|
|
83
|
+
writeFileSync(`${options.logDir}/verification-${results.length}.json`, `${JSON.stringify(compactVerification({ passed: result.passed, commands: [result] }))}\n`, { mode: 0o600 });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { passed: results.every((result) => result.passed), commands: results };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The ordinary case: run the argv `repeat` times and pass when every attempt does.
|
|
90
|
+
*
|
|
91
|
+
* @param {VerificationCommand} command
|
|
92
|
+
* @param {string} baseCwd
|
|
93
|
+
* @param {number} commandIndex
|
|
94
|
+
* @param {VerificationOptions} options
|
|
95
|
+
* @returns {Promise<VerificationCommandResult>}
|
|
96
|
+
*/
|
|
97
|
+
async function runRepeatedCommand(command, baseCwd, commandIndex, options) {
|
|
98
|
+
/** @type {VerificationAttemptResult[]} */
|
|
99
|
+
const attempts = [];
|
|
100
|
+
const repeat = command.repeat ?? 1;
|
|
101
|
+
for (let attempt = 1; attempt <= repeat; attempt += 1) {
|
|
102
|
+
attempts.push(await runCommand(command, baseCwd, command.cwd ?? ".", attempt, options.signal, options, commandIndex));
|
|
103
|
+
}
|
|
104
|
+
return { ...command, cwd: resolveVerificationCwd(baseCwd, command.cwd ?? "."), passed: attempts.every((item) => item.passed), attempts };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The mutation case: the entry passes on the mutant-kill fraction, and every
|
|
108
|
+
* attempt is a real run of the same argv against one broken file.
|
|
109
|
+
*
|
|
110
|
+
* @param {VerificationCommand} command
|
|
111
|
+
* @param {string} baseCwd
|
|
112
|
+
* @param {number} commandIndex
|
|
113
|
+
* @param {VerificationOptions} options
|
|
114
|
+
* @returns {Promise<VerificationCommandResult>}
|
|
115
|
+
*/
|
|
116
|
+
async function runMutationCommand(command, baseCwd, commandIndex, options) {
|
|
117
|
+
const commandCwd = command.cwd ?? ".";
|
|
118
|
+
const outcome = await runMutation(command, baseCwd, {
|
|
119
|
+
writeFiles: options.writeFiles ?? [],
|
|
120
|
+
run: (attempt) => runCommand(command, baseCwd, commandCwd, attempt, options.signal, options, commandIndex),
|
|
121
|
+
});
|
|
122
|
+
return { ...command, cwd: resolveVerificationCwd(baseCwd, commandCwd), passed: outcome.passed, attempts: outcome.attempts };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* @param {VerificationCommand} command
|
|
126
|
+
* @param {string} baseCwd
|
|
127
|
+
* @param {string} commandCwd
|
|
128
|
+
* @param {number} attempt
|
|
129
|
+
* @param {AbortSignal|undefined} signal
|
|
130
|
+
* @param {VerificationOptions} options
|
|
131
|
+
* @param {number} commandIndex
|
|
132
|
+
* @returns {Promise<VerificationAttemptResult>}
|
|
133
|
+
*/
|
|
134
|
+
function runCommand(command, baseCwd, commandCwd, attempt, signal, options, commandIndex) {
|
|
135
|
+
return new Promise((resolveResult) => {
|
|
136
|
+
const started = process.hrtime.bigint();
|
|
137
|
+
const stdout = boundedTail(VERIFICATION_LIMITS.stdoutBytes);
|
|
138
|
+
const stderr = boundedTail(VERIFICATION_LIMITS.stderrBytes);
|
|
139
|
+
let settled = false;
|
|
140
|
+
let timedOut = false;
|
|
141
|
+
/** @type {import("node:child_process").ChildProcess|null} */
|
|
142
|
+
let child = null;
|
|
143
|
+
/** @type {ReturnType<typeof setTimeout>|null} */
|
|
144
|
+
let timer = null;
|
|
145
|
+
/** @type {(() => void)|null} */
|
|
146
|
+
let abortHandler = null;
|
|
147
|
+
/** @type {VerificationAttempt|null} */
|
|
148
|
+
let identity = null;
|
|
149
|
+
let completionReported = false;
|
|
150
|
+
/**
|
|
151
|
+
* @param {number|null} exitCode
|
|
152
|
+
* @param {string|null} signalName
|
|
153
|
+
* @param {Error|null} error
|
|
154
|
+
*/
|
|
155
|
+
const finish = (exitCode, signalName, error = null) => {
|
|
156
|
+
if (settled) return;
|
|
157
|
+
settled = true;
|
|
158
|
+
if (timer) clearTimeout(timer);
|
|
159
|
+
if (abortHandler) signal?.removeEventListener("abort", abortHandler);
|
|
160
|
+
if (child?.pid && !error && !signalName && !timedOut) terminateGroup(child);
|
|
161
|
+
const durationMs = Number(process.hrtime.bigint() - started) / 1e6;
|
|
162
|
+
const result = {
|
|
163
|
+
attempt,
|
|
164
|
+
stdout: stdout.value(),
|
|
165
|
+
stderr: stderr.value(),
|
|
166
|
+
durationMs: Math.round(durationMs * 100) / 100,
|
|
167
|
+
exitCode: Number.isInteger(exitCode) ? exitCode : null,
|
|
168
|
+
signal: signalName ?? null,
|
|
169
|
+
timedOut,
|
|
170
|
+
error: error ? String(error.message ?? error) : null,
|
|
171
|
+
passed: !error && !timedOut && exitCode === 0 && !signalName,
|
|
172
|
+
};
|
|
173
|
+
if (!completionReported && identity) {
|
|
174
|
+
completionReported = true;
|
|
175
|
+
try { options?.onAttemptComplete?.({ ...identity, status: result.passed ? "closed" : "failed", completedAt: new Date().toISOString(), result }); } catch {
|
|
176
|
+
// Notification callback: any error it throws must not change the recorded result.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
resolveResult(result);
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* The timeout and abort paths settle here, never on `close`: a grandchild
|
|
183
|
+
* that escaped the process group can hold the stdout pipe open forever, so
|
|
184
|
+
* waiting for `close` would park the loop's critical path on a process that
|
|
185
|
+
* will never report. Kill the group, destroy the pipes, and settle now.
|
|
186
|
+
*
|
|
187
|
+
* @param {"timeout"|"abort"} reason
|
|
188
|
+
*/
|
|
189
|
+
const settleFromTimer = (reason) => {
|
|
190
|
+
if (settled) return;
|
|
191
|
+
timedOut = reason === "timeout";
|
|
192
|
+
if (child?.pid) terminateGroup(child);
|
|
193
|
+
try { child?.stdout?.destroy(); } catch {
|
|
194
|
+
// The stream already closed; destroying it again is a no-op.
|
|
195
|
+
}
|
|
196
|
+
try { child?.stderr?.destroy(); } catch {
|
|
197
|
+
// The stream already closed; destroying it again is a no-op.
|
|
198
|
+
}
|
|
199
|
+
finish(null, null, reason === "abort" ? new Error("verification command aborted") : null);
|
|
200
|
+
};
|
|
201
|
+
try {
|
|
202
|
+
const cwd = resolveVerificationCwd(baseCwd, commandCwd);
|
|
203
|
+
const startedAt = new Date().toISOString();
|
|
204
|
+
identity = {
|
|
205
|
+
invocationId: randomUUID(), commandIndex, attempt, pid: null, processStartToken: null, processGroupId: null,
|
|
206
|
+
startedAt, deadlineAt: new Date(Date.parse(startedAt) + (command.timeoutSec ?? 120) * 1_000).toISOString(), status: "active",
|
|
207
|
+
};
|
|
208
|
+
options?.onAttemptStart?.({ ...identity });
|
|
209
|
+
const env = verificationEnv(command);
|
|
210
|
+
child = spawn(command.argv[0], command.argv.slice(1), { cwd, env, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
|
|
211
|
+
const pid = child.pid ?? null;
|
|
212
|
+
let paused = false;
|
|
213
|
+
if (process.platform !== "win32" && pid) {
|
|
214
|
+
try { process.kill(-pid, "SIGSTOP"); paused = true; } catch {
|
|
215
|
+
// ESRCH: the child may have exited between spawn and the stop; not pausing is safe.
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
Object.assign(identity, { pid, processStartToken: processStartToken(pid), processGroupId: process.platform === "win32" ? null : pid });
|
|
219
|
+
options?.onAttemptSpawn?.({ ...identity });
|
|
220
|
+
if (paused && pid) {
|
|
221
|
+
try { process.kill(-pid, "SIGCONT"); } catch {
|
|
222
|
+
// ESRCH: the child is already gone, so there is nothing to resume.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const childStdout = /** @type {import("node:stream").Readable} */ (child.stdout);
|
|
226
|
+
const childStderr = /** @type {import("node:stream").Readable} */ (child.stderr);
|
|
227
|
+
childStdout.on("data", (chunk) => stdout.add(chunk));
|
|
228
|
+
childStderr.on("data", (chunk) => stderr.add(chunk));
|
|
229
|
+
child.once("error", (error) => finish(null, null, error));
|
|
230
|
+
child.once("close", (code, signalName) => finish(code, signalName));
|
|
231
|
+
abortHandler = () => settleFromTimer("abort");
|
|
232
|
+
if (signal?.aborted) abortHandler();
|
|
233
|
+
else signal?.addEventListener("abort", abortHandler, { once: true });
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (child?.pid && process.platform !== "win32") {
|
|
236
|
+
try { process.kill(-child.pid, "SIGCONT"); } catch {
|
|
237
|
+
// ESRCH: best-effort resume of a paused child that may already be gone.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
finish(null, null, error instanceof Error ? error : new Error(String(error)));
|
|
241
|
+
}
|
|
242
|
+
if (child && !settled) timer = setTimeout(() => settleFromTimer("timeout"), (command.timeoutSec ?? 120) * 1_000);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* @param {import("node:child_process").ChildProcess} child
|
|
247
|
+
*/
|
|
248
|
+
function terminateGroup(child) {
|
|
249
|
+
try {
|
|
250
|
+
if (process.platform !== "win32") process.kill(-/** @type {number} */ (child.pid), "SIGTERM");
|
|
251
|
+
else child.kill("SIGTERM");
|
|
252
|
+
} catch {
|
|
253
|
+
try { child.kill("SIGTERM"); } catch {
|
|
254
|
+
// ESRCH: the group kill failed and the leader was already gone.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
setTimeout(() => {
|
|
258
|
+
try {
|
|
259
|
+
if (process.platform !== "win32") process.kill(-/** @type {number} */ (child.pid), "SIGKILL");
|
|
260
|
+
else child.kill("SIGKILL");
|
|
261
|
+
} catch {
|
|
262
|
+
try { child.kill("SIGKILL"); } catch {
|
|
263
|
+
// ESRCH: the SIGKILL fallback found no leader left to kill.
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}, 100).unref();
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* @param {number} maxBytes
|
|
270
|
+
*/
|
|
271
|
+
function boundedTail(maxBytes) {
|
|
272
|
+
let value = Buffer.alloc(0);
|
|
273
|
+
return {
|
|
274
|
+
/**
|
|
275
|
+
* @param {string|Buffer} chunk
|
|
276
|
+
*/
|
|
277
|
+
add(chunk) {
|
|
278
|
+
value = Buffer.concat([value, Buffer.from(chunk)]);
|
|
279
|
+
if (value.length > maxBytes) {
|
|
280
|
+
let start = value.length - maxBytes;
|
|
281
|
+
while (start < value.length && (value[start] & 0xc0) === 0x80) start += 1;
|
|
282
|
+
value = value.subarray(start);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
value: () => value.toString("utf8"),
|
|
286
|
+
};
|
|
287
|
+
}
|