ruvnet-brain 4.5.16 → 4.6.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 +15 -19
- package/bin/developer-update.mjs +3 -0
- package/bin/install.mjs +15 -3
- package/bin/nightly-refresh.mjs +17 -1
- package/config/practical-rule-catalog.json +111 -0
- package/console/CONTRACT.md +47 -3
- package/console/app.js +112 -20
- package/package.json +5 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/brain-console.md +13 -1
- package/plugin/commands/configure.md +2 -2
- package/plugin/commands/rnb.md +13 -1
- package/plugin/commands/rnbc.md +13 -1
- package/plugin/commands/rvbc.md +13 -1
- package/plugin/commands/rvcb.md +13 -1
- package/plugin/hooks/codex-hooks.json +13 -1
- package/plugin/hooks/hook-contracts.json +20 -4
- package/plugin/scripts/adr-currency-gate.mjs +75 -31
- package/plugin/scripts/advocacy-outcomes.mjs +18 -4
- package/plugin/scripts/agentdb-recall.mjs +161 -26
- package/plugin/scripts/automatic-update.mjs +11 -2
- package/plugin/scripts/capability-inventory-receipt.mjs +16 -7
- package/plugin/scripts/capability-registry.mjs +55 -114
- package/plugin/scripts/capacity-aware-parallel-work.mjs +1 -1
- package/plugin/scripts/codex-hook-adapter.mjs +33 -6
- package/plugin/scripts/codex-hook-wrapper.mjs +23 -3
- package/plugin/scripts/continuation-gate.mjs +2 -1
- package/plugin/scripts/continuity-brief.mjs +55 -22
- package/plugin/scripts/continuity-events.mjs +93 -17
- package/plugin/scripts/continuity-hook-policy.mjs +1 -1
- package/plugin/scripts/continuity-journal.mjs +77 -29
- package/plugin/scripts/decision-gate.mjs +62 -37
- package/plugin/scripts/decision-outcomes.mjs +192 -199
- package/plugin/scripts/detach.mjs +53 -13
- package/plugin/scripts/developer-update-cleanup.mjs +38 -0
- package/plugin/scripts/developer-update-lock.mjs +30 -0
- package/plugin/scripts/developer-update-maintenance.mjs +178 -0
- package/plugin/scripts/developer-update-owner.mjs +24 -0
- package/plugin/scripts/developer-update-policy.mjs +48 -0
- package/plugin/scripts/developer-update.mjs +435 -0
- package/plugin/scripts/doc-currency.mjs +993 -0
- package/plugin/scripts/ground-ruvnet.sh +16 -3
- package/plugin/scripts/grounding-answer.mjs +18 -16
- package/plugin/scripts/grounding-stamp.sh +3 -0
- package/plugin/scripts/grounding-turn-evidence.mjs +60 -65
- package/plugin/scripts/grounding-turn-gate.mjs +34 -29
- package/plugin/scripts/grounding-turn-mark.mjs +101 -17
- package/plugin/scripts/hook-context-budget.mjs +90 -0
- package/plugin/scripts/hook-registry.mjs +129 -1
- package/plugin/scripts/hook-shim.mjs +20 -2
- package/plugin/scripts/host-update.mjs +6 -3
- package/plugin/scripts/lesson-gate.mjs +1 -1
- package/plugin/scripts/nightly-scheduler.mjs +55 -16
- package/plugin/scripts/plugin-artifact-proof.mjs +135 -0
- package/plugin/scripts/project-capture-queue.mjs +22 -15
- package/plugin/scripts/project-progression-producer.mjs +15 -8
- package/plugin/scripts/project-progression-reader.mjs +14 -4
- package/plugin/scripts/project-progression-session-start.mjs +29 -17
- package/plugin/scripts/project-progression-sources.mjs +24 -10
- package/plugin/scripts/project-progression-store.mjs +7 -2
- package/plugin/scripts/project-store-resolver.mjs +10 -7
- package/plugin/scripts/project-transition-hook.mjs +20 -12
- package/plugin/scripts/session-snapshot-hook.mjs +20 -9
- package/plugin/scripts/session-start-budget.mjs +18 -0
- package/plugin/scripts/session-start-core.mjs +32 -6
- package/plugin/scripts/session-start-trace.mjs +2 -1
- package/plugin/scripts/turn-outcome-capture.mjs +7 -5
- package/plugin/scripts/unprompted-runtime.mjs +36 -10
- package/plugin/scripts/user-settings.mjs +7 -1
- package/plugin/skills/ruvnet-brain/SKILL.md +2 -2
- package/scripts/codex-managed-terminal.mjs +1 -1
- package/scripts/console-runtime-identity.mjs +2 -0
- package/scripts/console-suite-update.mjs +117 -0
- package/scripts/doc-currency.mjs +6 -990
- package/scripts/hook-qualify-core.mjs +30 -1
- package/scripts/model-managed-prompt.mjs +1 -1
- package/scripts/model-managed-workflow-service.mjs +113 -14
- package/scripts/model-routing-controller.mjs +112 -21
- package/scripts/onboarding-console.mjs +41 -12
- package/scripts/practical-rule-selector.mjs +51 -0
- package/scripts/product-integrity-contract.mjs +11 -4
- package/scripts/release-qualification-contract.mjs +14 -0
- package/scripts/remedy-registry.mjs +1 -0
- package/scripts/source-scope-receipt.mjs +9 -4
- package/scripts/stack-sync.mjs +19 -132
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import fs from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import { spawn } from 'node:child_process';
|
|
21
|
+
import { contextBlock, contextFrame, readContextFrame, emitOwnedContext } from './hook-context-budget.mjs';
|
|
21
22
|
import { fileURLToPath } from 'node:url';
|
|
22
23
|
// resolveBash ONLY. `skipNoBash` is not a predicate — it is a one-time notice emitter that returns 0
|
|
23
24
|
// and WRITES TO STDERR, which on this hot path is the refusal channel: calling it would have injected
|
|
@@ -240,6 +241,36 @@ export function decide(verdicts) {
|
|
|
240
241
|
return { allow: false, refusals: refusals.map((v) => v.id), reason };
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
/** Policy advice is context, separate from the winning refusal and the speech channel. */
|
|
245
|
+
export function policyContext(results, speechOutput = '') {
|
|
246
|
+
const contexts = [];
|
|
247
|
+
for (const result of results) {
|
|
248
|
+
if (result.skipped) contexts.push(`Policy ${result.id} did not obtain a verdict (${result.skipped}${result.reason ? `: ${String(result.reason).slice(0, 4096)}` : ''}).`);
|
|
249
|
+
if (result.code !== ALLOW || !result.stdout?.trim()) continue;
|
|
250
|
+
let parsed; try { parsed = JSON.parse(result.stdout); } catch {}
|
|
251
|
+
const text = typeof parsed?.hookSpecificOutput?.additionalContext === 'string'
|
|
252
|
+
? parsed.hookSpecificOutput.additionalContext : result.stdout.trim();
|
|
253
|
+
if (text) contexts.push(text.slice(0, 4096));
|
|
254
|
+
}
|
|
255
|
+
if (process.env.RUVNET_HOOK_CONTEXT_BUDGET === '1') {
|
|
256
|
+
const speechFrame = readContextFrame(speechOutput);
|
|
257
|
+
const blocks = contexts.map((text) => contextBlock(text, { id: 'policy-diagnostic', critical: true }));
|
|
258
|
+
if (speechFrame) blocks.push(...speechFrame.blocks);
|
|
259
|
+
else if (speechOutput.trim()) { let value; try { value = JSON.parse(speechOutput); } catch {}
|
|
260
|
+
const text = value?.hookSpecificOutput?.additionalContext || speechOutput.trim();
|
|
261
|
+
blocks.push(contextBlock(text, { id: 'unframed-speech-preserved', critical: true })); }
|
|
262
|
+
if (!blocks.length) return '';
|
|
263
|
+
return emitOwnedContext(contextFrame('decision-gate', 'PreToolUse', blocks));
|
|
264
|
+
}
|
|
265
|
+
if (!contexts.length) return speechOutput;
|
|
266
|
+
let speech; try { speech = JSON.parse(speechOutput); } catch {}
|
|
267
|
+
const prior = typeof speech?.hookSpecificOutput?.additionalContext === 'string'
|
|
268
|
+
? speech.hookSpecificOutput.additionalContext : speechOutput.trim();
|
|
269
|
+
return JSON.stringify({ ...(speech && typeof speech === 'object' && !Array.isArray(speech) ? speech : {}),
|
|
270
|
+
hookSpecificOutput: { ...(speech?.hookSpecificOutput || {}), hookEventName: 'PreToolUse',
|
|
271
|
+
additionalContext: [...new Set([prior, ...contexts].filter(Boolean))].join('\n') } });
|
|
272
|
+
}
|
|
273
|
+
|
|
243
274
|
const firstLine = (s) => String(s || '').trim().split('\n').map((l) => l.trim()).filter(Boolean)[0] || '';
|
|
244
275
|
|
|
245
276
|
/** Payload accessors — tolerant, because a malformed payload must degrade to "no measurement". */
|
|
@@ -258,23 +289,21 @@ function payloadInput(payload) { return parsed(payload).tool_input || {}; }
|
|
|
258
289
|
function speechEventFor(event) { return event === 'bash' ? 'PreToolUse-bash' : 'PreToolUse-write'; }
|
|
259
290
|
|
|
260
291
|
// ── Runtime ──────────────────────────────────────────────────────────────────────────────────────
|
|
261
|
-
|
|
262
|
-
|
|
263
292
|
if (isMain()) {
|
|
264
293
|
const started = Date.now();
|
|
265
294
|
const payload = normalizePayloadText(readPayload());
|
|
295
|
+
const shape = classifyPayload(payload);
|
|
296
|
+
const session = sessionOf(payload) || (shape.truncated ? shape.session : '');
|
|
266
297
|
const selected = policiesFor(EVENT);
|
|
267
298
|
// An unknown event is not an occasion to refuse anything. Same rule as unprompted-runtime's
|
|
268
299
|
// "never speak on a guess", pointed at the other decision. 'write' is the only registered route
|
|
269
300
|
// (H5 removed the dead 'bash' one — see the REGISTRY comment above), so it is the only exception.
|
|
270
301
|
if (!selected.length && EVENT !== 'write') process.exit(ALLOW);
|
|
271
|
-
|
|
272
302
|
const budgetMs = Number(process.env.RUVNET_DECISION_BUDGET_MS) || DEFAULT_BUDGET_MS;
|
|
273
|
-
const deadline = started + budgetMs;
|
|
303
|
+
const deadline = Math.min(started + budgetMs, Number(process.env.RUVNET_DECISION_DEADLINE) || Infinity);
|
|
274
304
|
// Resolved ONCE. On win32 resolveBash() can shell out to `where.exe`; four bash policies meant up
|
|
275
305
|
// to four of those per tool call, for an answer that cannot change mid-invocation.
|
|
276
306
|
BASH = resolveBash();
|
|
277
|
-
|
|
278
307
|
const trace = []; // one row per policy — surfaced by RUVNET_DECISION_TRACE=1
|
|
279
308
|
const unconsulted = []; // policies the budget cost us. NEVER silent; see reportBudget().
|
|
280
309
|
const toolInput = payloadInput(payload);
|
|
@@ -284,7 +313,6 @@ if (isMain()) {
|
|
|
284
313
|
if (why) trace.push({ id: p.id, ms: 0, skipped: why });
|
|
285
314
|
else consulted.push(p);
|
|
286
315
|
}
|
|
287
|
-
|
|
288
316
|
// ── PARALLEL, and the reason is arithmetic ─────────────────────────────────────────────────────
|
|
289
317
|
// Sequentially the gate's wall time was SUM(policies); run together it is MAX(policies). Measured
|
|
290
318
|
// on a `git push` payload by the 2026-08-13 audit: 182 + 345 + 2145..3990 + 629 + 743 ≈ 4.0-5.9s
|
|
@@ -299,41 +327,36 @@ if (isMain()) {
|
|
|
299
327
|
// A policy that exits SKIPPED (3) did not vote and said why (duplicate-gate under overload). Same
|
|
300
328
|
// two channels as a blown budget, below — never silence standing in for a verdict.
|
|
301
329
|
const selfSkipped = results.filter((r) => r.skipped === 'self');
|
|
302
|
-
|
|
303
330
|
const decision = decide(verdicts);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// hook: resolve first (did this call retry something we refused?), then open a new debt if we are
|
|
308
|
-
// about to refuse. Order matters — resolving after recording would close the debt we just opened.
|
|
309
|
-
// Entirely best-effort: measurement may never affect the verdict, so it runs after `decide`.
|
|
310
|
-
// An oversize payload is cut mid-JSON (see TRANSPORT_CAP_BYTES): recover session/tool/path from its
|
|
311
|
-
// prefix so its refusals and allows are counted like any other.
|
|
312
|
-
const shape = classifyPayload(payload);
|
|
313
|
-
const session = sessionOf(payload) || (shape.truncated ? shape.session : '');
|
|
331
|
+
// Measurement follows the FINAL gate verdict; it must never delay a blocking producer.
|
|
332
|
+
const measureFinalDecision = (allowed, policies = decision.refusals) => {
|
|
333
|
+
const metricDeadline = Math.min(deadline, Date.now() + 25);
|
|
314
334
|
try {
|
|
315
335
|
const key = shape.truncated
|
|
316
336
|
? actionKey(shape.tool, { file_path: shape.filePath })
|
|
317
337
|
: actionKey(payloadTool(payload), toolInput);
|
|
318
338
|
const ts = Date.now();
|
|
319
339
|
if (session && key) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
340
|
+
let observed = {}; try { observed = JSON.parse(payload); } catch { /* truncated payload: adapter/process scope */ }
|
|
341
|
+
const attribution = { host: observed.host || process.env.RUVNET_HOOK_HOST || 'claude',
|
|
342
|
+
project: observed.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd() };
|
|
343
|
+
sweepStale({ session, ts, deadlineAt: metricDeadline, nonBlocking: true }); // only proven observation age expires debt; foreign sessions are not abandoned
|
|
344
|
+
resolveOutcome({ session, key, allowed: allowed, ts, ...attribution, deadlineAt: metricDeadline, nonBlocking: true });
|
|
345
|
+
if (!allowed) recordRefusal({ session, key, policies: policies, ts, ...attribution, deadlineAt: metricDeadline, nonBlocking: true });
|
|
323
346
|
}
|
|
324
347
|
} catch { /* a ledger must never break a tool call */ }
|
|
325
|
-
|
|
348
|
+
};
|
|
326
349
|
reportSelfSkipped({ session, selfSkipped });
|
|
327
350
|
if (decision.allow && shape.truncated) {
|
|
328
351
|
// OVERSIZE POLICY (header): allowed on a check of the first TRANSPORT_CAP_BYTES only. Recorded, never silent.
|
|
329
352
|
try { appendOutcome({ kind: 'payload-oversize', event: EVENT, session, bytesSeen: shape.bytes, capBytes: TRANSPORT_CAP_BYTES, tool: shape.tool, filePath: shape.filePath, ts: Date.now() }); } catch { /* a ledger must never break a tool call */ }
|
|
330
353
|
}
|
|
331
354
|
if (!decision.allow) {
|
|
332
|
-
|
|
355
|
+
measureFinalDecision(false);
|
|
356
|
+
reportBudget({ session, unconsulted, trace, started, budgetMs, refused: true });
|
|
333
357
|
process.stderr.write(`${decision.reason}\n`);
|
|
334
358
|
process.exit(REFUSE);
|
|
335
359
|
}
|
|
336
|
-
|
|
337
360
|
// Nothing refused: run the speech chokepoint and forward its envelope verbatim. It owns its own
|
|
338
361
|
// per-channel policy; this gate does not inspect or re-decide anything it says.
|
|
339
362
|
//
|
|
@@ -347,18 +370,23 @@ if (isMain()) {
|
|
|
347
370
|
const speech = await runPolicy(SPEECH, payload, deadline, speechEventFor(EVENT), trace);
|
|
348
371
|
if (speech.skipped === 'budget') unconsulted.push(SPEECH.id);
|
|
349
372
|
if (speech.code === REFUSE) {
|
|
350
|
-
|
|
373
|
+
measureFinalDecision(false, [...decision.refusals, SPEECH.id]);
|
|
374
|
+
reportBudget({ session, unconsulted, trace, started, budgetMs, refused: true });
|
|
351
375
|
process.stderr.write(`${(speech.stderr || '').trim()}\n`);
|
|
352
376
|
process.exit(REFUSE);
|
|
353
377
|
}
|
|
354
378
|
reportBudget({ session, unconsulted, trace, started, budgetMs });
|
|
355
|
-
|
|
379
|
+
measureFinalDecision(true);
|
|
380
|
+
const context = policyContext(results, speech.stdout || '');
|
|
381
|
+
if (context) process.stdout.write(context);
|
|
356
382
|
process.exit(ALLOW);
|
|
357
383
|
}
|
|
358
384
|
reportBudget({ session, unconsulted, trace, started, budgetMs });
|
|
385
|
+
measureFinalDecision(true);
|
|
386
|
+
const context = policyContext([...results, ...unconsulted.filter((id) => !results.some((r) => r.id === id && r.skipped)).map((id) => ({ id, skipped: 'budget' }))]);
|
|
387
|
+
if (context) process.stdout.write(context);
|
|
359
388
|
process.exit(ALLOW);
|
|
360
389
|
}
|
|
361
|
-
|
|
362
390
|
/**
|
|
363
391
|
* ── A BUDGET THAT CAN BE EXCEEDED SILENTLY FAILS OPEN WITHOUT SAYING SO ──────────────────────────
|
|
364
392
|
*
|
|
@@ -378,7 +406,7 @@ if (isMain()) {
|
|
|
378
406
|
* the applicability skip, an ordinary write measures ~360ms against a 4000ms budget, so a trip
|
|
379
407
|
* there is not noise to be tolerated — it is the defect, and the suite should go red for it.
|
|
380
408
|
*/
|
|
381
|
-
function reportBudget({ session, unconsulted, trace, started, budgetMs }) {
|
|
409
|
+
function reportBudget({ session, unconsulted, trace, started, budgetMs, refused = false }) {
|
|
382
410
|
const elapsed = Date.now() - started;
|
|
383
411
|
if (process.env.RUVNET_DECISION_TRACE === '1') {
|
|
384
412
|
process.stderr.write(`[decision-gate] ${EVENT} ${elapsed}ms budget=${budgetMs}ms ${JSON.stringify(trace)}\n`);
|
|
@@ -388,19 +416,17 @@ function reportBudget({ session, unconsulted, trace, started, budgetMs }) {
|
|
|
388
416
|
appendOutcome({ kind: 'budget-exceeded', event: EVENT, session, unconsulted, elapsedMs: elapsed, budgetMs, ts: Date.now() });
|
|
389
417
|
} catch { /* a ledger must never break a tool call */ }
|
|
390
418
|
process.stderr.write(
|
|
391
|
-
`[decision-gate] ${budgetMs}ms budget exhausted after ${elapsed}ms — ALLOWED WITHOUT CONSULTING: `
|
|
392
|
-
+ `${unconsulted.join(', ')}. These policies did not vote; this allow is a timeout, not a verdict
|
|
419
|
+
`[decision-gate] ${budgetMs}ms budget exhausted after ${elapsed}ms — ${refused ? 'REFUSED; UNCONSULTED' : 'ALLOWED WITHOUT CONSULTING'}: `
|
|
420
|
+
+ `${unconsulted.join(', ')}. These policies did not vote; ${refused ? 'another policy refused the action.' : 'this allow is a timeout, not a verdict.'}\n`,
|
|
393
421
|
);
|
|
394
422
|
}
|
|
395
|
-
|
|
396
423
|
/** Record and print each policy that skipped itself (exit 3), with its own one-line reason. */
|
|
397
424
|
function reportSelfSkipped({ session, selfSkipped }) {
|
|
398
425
|
for (const r of selfSkipped) {
|
|
399
426
|
try { appendOutcome({ kind: 'policy-skipped', event: EVENT, session, policy: r.id, reason: r.reason, ts: Date.now() }); } catch { /* a ledger must never break a tool call */ }
|
|
400
|
-
process.stderr.write(`[decision-gate] ${r.reason || `${r.id} skipped`} — ${r.id} did not vote;
|
|
427
|
+
process.stderr.write(`[decision-gate] ${r.reason || `${r.id} skipped`} — ${r.id} did not vote; it supplied no authorization verdict.\n`);
|
|
401
428
|
}
|
|
402
429
|
}
|
|
403
|
-
|
|
404
430
|
/**
|
|
405
431
|
* Run one policy as a CAPTURED child. Never lets its bytes touch the real streams.
|
|
406
432
|
*
|
|
@@ -427,19 +453,19 @@ function runPolicy(p, payload, deadline, extraArg, trace) {
|
|
|
427
453
|
if (extraArg) args.push(extraArg);
|
|
428
454
|
const left = deadline - Date.now();
|
|
429
455
|
if (left <= 0) return Promise.resolve(done({ id: p.id, skipped: 'budget' }));
|
|
430
|
-
|
|
431
456
|
return new Promise((resolve) => {
|
|
432
457
|
let settled = false;
|
|
433
458
|
let timer = null;
|
|
434
459
|
const finish = (r) => { if (settled) return; settled = true; clearTimeout(timer); resolve(done(r)); };
|
|
435
460
|
let child;
|
|
436
461
|
try {
|
|
437
|
-
child = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, RUVNET_DECISION_GATE: '1', RUVNET_DECISION_DEADLINE: String(deadline) } });
|
|
462
|
+
child = spawn(cmd, args, { detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, RUVNET_DECISION_GATE: '1', RUVNET_DECISION_DEADLINE: String(deadline) } });
|
|
438
463
|
} catch { return finish({ id: p.id, skipped: 'spawn' }); }
|
|
439
464
|
// SIGKILL, not SIGTERM: a bash policy that has spawned its own child (jq, node, ruflo) can sit in
|
|
440
465
|
// a TERM handler, and the host's own kill is what we are racing. The whole batch shares ONE
|
|
441
466
|
// deadline, so a single slow policy cancels only the time it actually consumed.
|
|
442
|
-
|
|
467
|
+
const retireTree = () => { try { if (process.platform !== 'win32') process.kill(-child.pid, 'SIGKILL'); else child.kill('SIGKILL'); } catch { /* already gone */ } };
|
|
468
|
+
timer = setTimeout(() => { retireTree(); finish({ id: p.id, skipped: 'budget' }); }, Math.max(1, left - 100));
|
|
443
469
|
let stdout = ''; let stderr = ''; let bytes = 0;
|
|
444
470
|
const MAX = 1 << 20; // same ceiling spawnSync's maxBuffer enforced; a policy is not a data source
|
|
445
471
|
child.stdout.on('data', (d) => { if (bytes < MAX) { stdout += d; bytes += d.length; } });
|
|
@@ -449,6 +475,7 @@ function runPolicy(p, payload, deadline, extraArg, trace) {
|
|
|
449
475
|
// Unhandled, that error event would take the whole gate down and turn an allow into a hook error.
|
|
450
476
|
child.stdin.on('error', () => { /* the child did not want the payload; that is not a failure */ });
|
|
451
477
|
child.on('close', (code) => {
|
|
478
|
+
retireTree();
|
|
452
479
|
// A spawn failure, a timeout, or any code other than 0/2 is an ERROR — and an error here must
|
|
453
480
|
// never be mistaken for a refusal. That distinction is the one lesson-gate.mjs had to learn twice.
|
|
454
481
|
if (code === SKIPPED_SELF) return finish({ id: p.id, skipped: 'self', reason: firstLine(stderr) });
|
|
@@ -458,12 +485,10 @@ function runPolicy(p, payload, deadline, extraArg, trace) {
|
|
|
458
485
|
try { child.stdin.end(payload); } catch { /* handled by the stdin error listener above */ }
|
|
459
486
|
});
|
|
460
487
|
}
|
|
461
|
-
|
|
462
488
|
function readPayload() {
|
|
463
489
|
if (process.stdin.isTTY) return '';
|
|
464
490
|
try { return fs.readFileSync(0, 'utf8'); } catch { return ''; }
|
|
465
491
|
}
|
|
466
|
-
|
|
467
492
|
/** Never able to crash a caller that merely imported this (see tests/unit/entrypoint-guard-safety). */
|
|
468
493
|
function isMain() {
|
|
469
494
|
try {
|