@worca/app 1.3.0 → 1.4.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 +85 -6
- package/agents/clarify.meta.json +1 -0
- package/agents/memoryDefragmenter.meta.json +2 -1
- package/agents/reviewer.meta.json +60 -0
- package/agents/worca-cc-code-reviewer.md +33 -0
- package/agents/worca-cc-memory-defragmenter.md +5 -3
- package/agents/workspaceScanner.meta.json +1 -0
- package/package.json +14 -10
- package/scripts/git-diff.mjs +25 -0
- package/scripts/gitDiff.meta.json +18 -0
- package/scripts/js-inline.mjs +11 -0
- package/scripts/js.meta.json +22 -0
- package/scripts/py-inline.py +27 -0
- package/scripts/py.meta.json +22 -0
- package/scripts/shell.meta.json +24 -0
- package/skills/worca/SKILL.md +3 -2
- package/src/cli/models.mjs +247 -0
- package/src/cli/render.mjs +72 -4
- package/src/cli/schedule.mjs +494 -0
- package/src/cli/worca-cc.mjs +1001 -22
- package/src/core/agent-registry.mjs +75 -23
- package/src/core/agent-store.mjs +51 -2
- package/src/core/artifacts.mjs +73 -10
- package/src/core/ask/events.mjs +119 -1
- package/src/core/ask/limits.mjs +32 -4
- package/src/core/ask/mcp-stdio.mjs +12 -0
- package/src/core/ask/model-deps.mjs +126 -0
- package/src/core/ask/model-proposal.mjs +370 -0
- package/src/core/ask/models.mjs +12 -0
- package/src/core/ask/policy-deps.mjs +124 -0
- package/src/core/ask/policy-proposal.mjs +363 -0
- package/src/core/ask/prompt.mjs +74 -9
- package/src/core/ask/proposal.mjs +54 -5
- package/src/core/ask/schedule-deps.mjs +83 -0
- package/src/core/ask/schedule-spec.mjs +310 -0
- package/src/core/ask/script-deps.mjs +357 -0
- package/src/core/ask/source-deps.mjs +52 -0
- package/src/core/ask/source-spec.mjs +157 -0
- package/src/core/ask/spawn.mjs +1 -0
- package/src/core/ask/store.mjs +6 -3
- package/src/core/ask/tool-deps.mjs +4 -0
- package/src/core/ask/tools.mjs +657 -37
- package/src/core/ask/turn.mjs +109 -2
- package/src/core/ask-files.mjs +406 -0
- package/src/core/ask-forms.mjs +195 -0
- package/src/core/ask-projection.mjs +72 -0
- package/src/core/bridge/errors.mjs +84 -0
- package/src/core/bridge/provider-ops.mjs +281 -0
- package/src/core/bridge/providers/copilot.mjs +269 -0
- package/src/core/bridge/providers/endpoint.mjs +257 -0
- package/src/core/bridge/registry.mjs +88 -0
- package/src/core/bridge/semaphore.mjs +73 -0
- package/src/core/bridge/server.mjs +184 -0
- package/src/core/bridge/telemetry.mjs +53 -0
- package/src/core/bridge/translate/request.mjs +252 -0
- package/src/core/bridge/translate/response.mjs +82 -0
- package/src/core/bridge/translate/stream.mjs +242 -0
- package/src/core/bridge/upstream.mjs +209 -0
- package/src/core/chat/command-router.mjs +58 -4
- package/src/core/chat/notifier.mjs +14 -1
- package/src/core/chat/renderers.mjs +35 -0
- package/src/core/claude-runner.mjs +126 -19
- package/src/core/config.mjs +212 -31
- package/src/core/cost-budget.mjs +3 -2
- package/src/core/db.mjs +169 -15
- package/src/core/failure-policy.mjs +10 -0
- package/src/core/fs-browse.mjs +16 -4
- package/src/core/git-info.mjs +22 -0
- package/src/core/graph/builtin-workflows.mjs +3 -1
- package/src/core/graph/exec-io.mjs +71 -0
- package/src/core/graph/executor.mjs +139 -70
- package/src/core/graph/human-evidence.mjs +131 -0
- package/src/core/graph/python-probe.mjs +172 -0
- package/src/core/graph/registry-ports.mjs +10 -6
- package/src/core/graph/scheduler.mjs +39 -24
- package/src/core/graph/script-child.mjs +81 -0
- package/src/core/graph/script-runner.mjs +597 -0
- package/src/core/graph/worca_script.py +207 -0
- package/src/core/guardrail-store.mjs +16 -0
- package/src/core/human-backfill.mjs +108 -0
- package/src/core/human-rate.mjs +17 -0
- package/src/core/index-html.mjs +6 -2
- package/src/core/memory-defrag-model.mjs +112 -0
- package/src/core/memory-store.mjs +70 -18
- package/src/core/memory-sync.mjs +22 -11
- package/src/core/metrics/read.mjs +4 -1
- package/src/core/metrics/record.mjs +50 -2
- package/src/core/metrics/sync.mjs +6 -4
- package/src/core/model-env.mjs +149 -0
- package/src/core/model-test.mjs +14 -1
- package/src/core/notifications.mjs +128 -0
- package/src/core/onboarding.mjs +8 -2
- package/src/core/orchestrator.mjs +357 -26
- package/src/core/phases.mjs +95 -6
- package/src/core/plugin-api.mjs +24 -7
- package/src/core/plugin-manifest.mjs +184 -18
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-script-cases.mjs +118 -0
- package/src/core/plugin-store.mjs +163 -17
- package/src/core/plugin-workflows.mjs +71 -17
- package/src/core/policy/cache.mjs +116 -0
- package/src/core/policy/effective.mjs +175 -0
- package/src/core/policy/gate.mjs +91 -0
- package/src/core/policy/local.mjs +145 -0
- package/src/core/policy/registry.mjs +330 -0
- package/src/core/policy/scope.mjs +61 -0
- package/src/core/policy/state.mjs +79 -0
- package/src/core/policy/sync.mjs +513 -0
- package/src/core/protocol.mjs +43 -0
- package/src/core/run-harness.mjs +421 -72
- package/src/core/scheduler.mjs +980 -0
- package/src/core/script-bench.mjs +628 -0
- package/src/core/script-registry.mjs +116 -0
- package/src/core/script-store.mjs +563 -0
- package/src/core/settings.mjs +489 -14
- package/src/core/stats.mjs +33 -2
- package/src/core/workflow-export.mjs +94 -3
- package/src/core/workflow-share.mjs +67 -18
- package/src/core/workflows.mjs +47 -11
- package/src/core/workspaces.mjs +18 -12
- package/src/shared/forms/answer.mjs +164 -0
- package/src/shared/forms/catalog.mjs +91 -0
- package/src/shared/forms/form-def.mjs +290 -0
- package/src/shared/forms/layout.mjs +67 -0
- package/src/shared/forms/paths.mjs +47 -0
- package/src/shared/forms/project.mjs +309 -0
- package/src/shared/forms/schema.mjs +205 -0
- package/src/shared/graph/agent-meta.mjs +55 -5
- package/src/shared/graph/constants.mjs +14 -2
- package/src/shared/graph/flow-layout.mjs +2 -1
- package/src/shared/graph/isomorphic.mjs +5 -3
- package/src/shared/graph/manifest.mjs +22 -13
- package/src/shared/graph/ports.mjs +45 -19
- package/src/shared/graph/script-cases.mjs +257 -0
- package/src/shared/graph/script-icons.mjs +46 -0
- package/src/shared/graph/script-infer.mjs +259 -0
- package/src/shared/graph/script-meta.mjs +408 -0
- package/src/shared/graph/script-templates.mjs +201 -0
- package/src/shared/graph/template.mjs +4 -4
- package/src/shared/graph/validate.mjs +89 -16
- package/src/shared/human-estimate.mjs +100 -0
- package/src/shared/schedule/recurrence.mjs +353 -0
- package/src/shared/team-metrics/aggregate.mjs +51 -11
- package/{scripts → tools}/install.mjs +3 -3
- package/ui/public/app.js +4390 -683
- package/ui/public/artifact-picker.mjs +189 -0
- package/ui/public/ask/dom.mjs +121 -0
- package/ui/public/ask/form-preview.mjs +55 -0
- package/ui/public/ask/form-renderer.mjs +250 -0
- package/ui/public/ask/registry.mjs +53 -0
- package/ui/public/ask/widgets-display.mjs +370 -0
- package/ui/public/ask/widgets-input.mjs +624 -0
- package/ui/public/ask/widgets-layout.mjs +90 -0
- package/ui/public/ask-panel.mjs +401 -27
- package/ui/public/ask-run-card.mjs +1 -1
- package/ui/public/bridge-view.mjs +694 -0
- package/ui/public/chat-settings-view.mjs +24 -0
- package/ui/public/code-editor.mjs +181 -0
- package/ui/public/getting-started.mjs +34 -7
- package/ui/public/graph/composer.mjs +138 -10
- package/ui/public/graph/inspector.mjs +61 -58
- package/ui/public/graph/palette.mjs +27 -9
- package/ui/public/graph/run-decor.mjs +34 -17
- package/ui/public/graph/run-hosts.mjs +7 -1
- package/ui/public/graph/save-dialog.mjs +3 -0
- package/ui/public/graph/view.mjs +23 -7
- package/ui/public/guardrails-view.mjs +15 -3
- package/ui/public/guide-spot.mjs +87 -9
- package/ui/public/index.html +480 -141
- package/ui/public/memory-view.mjs +22 -4
- package/ui/public/models-view.mjs +162 -17
- package/ui/public/node-tunables.mjs +33 -4
- package/ui/public/plugins-view.mjs +23 -1
- package/ui/public/results-view.mjs +4 -2
- package/ui/public/schedule-sheet.mjs +430 -0
- package/ui/public/schedules-view.mjs +432 -0
- package/ui/public/script-bench-view.mjs +1154 -0
- package/ui/public/script-forms.mjs +282 -0
- package/ui/public/script-wizard.mjs +529 -0
- package/ui/public/scripts-view.mjs +868 -0
- package/ui/public/stats-view.mjs +159 -52
- package/ui/public/style.css +1461 -44
- package/ui/public/team-metrics-surfaces.mjs +77 -16
- package/ui/public/team-metrics-view.mjs +68 -5
- package/ui/public/team-policy-view.mjs +1402 -0
- package/ui/public/ui-level.mjs +237 -0
- package/ui/server.mjs +1827 -73
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Shape of a pass: publishing routes tokens IMMEDIATELY (per-wire delivery counting
|
|
7
7
|
// and gate checks happen AT DELIVERY), but FIRING happens only in the drain loop. A
|
|
8
8
|
// pass walks `loops.launchOrder` ONCE; a node ready at its slot fires AT that slot —
|
|
9
|
-
//
|
|
9
|
+
// keyed nodes (agents, scripts) as an async `execute` under their pool's semaphore, flow nodes inline,
|
|
10
10
|
// whose publishes route immediately and may make LATER slots ready within the same
|
|
11
11
|
// pass. Earlier slots are never revisited mid-pass; passes repeat until a pass fires
|
|
12
12
|
// nothing. The execution sequence is therefore the order of `execute` CALLS, and it
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// `rerunPending` coalescing is structural: a node that is already running is skipped
|
|
20
20
|
// in the walk, and readiness is re-evaluated after every completion, so readiness
|
|
21
21
|
// reached while running re-fires exactly once and never queues.
|
|
22
|
-
import { FLOW_KINDS, DEFAULT_MAX_CYCLES, AWAIT_PORT } from '../../shared/graph/constants.mjs';
|
|
22
|
+
import { FLOW_KINDS, KEYED_KINDS, DEFAULT_MAX_CYCLES, AWAIT_PORT } from '../../shared/graph/constants.mjs';
|
|
23
23
|
import { classifyLoops } from '../../shared/graph/loops.mjs';
|
|
24
24
|
import { firedOutputs, resolveOrOutType } from '../../shared/graph/ports.mjs';
|
|
25
25
|
import { blockingIssues, hasBlocking } from '../../shared/graph/verdict.mjs';
|
|
@@ -46,6 +46,11 @@ function defaultMaxParallel() {
|
|
|
46
46
|
const n = Number(process.env.WORCA_MAX_PARALLEL);
|
|
47
47
|
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 4;
|
|
48
48
|
}
|
|
49
|
+
/** D7: scripts have their own, smaller pool — they burn CPU, not tokens. */
|
|
50
|
+
function defaultMaxParallelScripts() {
|
|
51
|
+
const n = Number(process.env.WORCA_MAX_PARALLEL_SCRIPTS);
|
|
52
|
+
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 2;
|
|
53
|
+
}
|
|
49
54
|
|
|
50
55
|
/** Non-interactive default: gates continue, clarify asks answer empty (v1 `_ask` auto). */
|
|
51
56
|
const defaultAsk = async (ask) => (ask?.kind === 'gate' ? 'continue' : []);
|
|
@@ -96,6 +101,7 @@ function raceAbort(promise, signal) {
|
|
|
96
101
|
* @param {(gate:{wireId,fromNode,toNode,askId}|null) => void} [opts.onGate]
|
|
97
102
|
* @param {(ask:object) => Promise<any>} [opts.onAsk]
|
|
98
103
|
* @param {number} [opts.maxParallel]
|
|
104
|
+
* @param {number} [opts.maxParallelScripts]
|
|
99
105
|
* @param {(line:string, attrs?:object) => void} [opts.log]
|
|
100
106
|
*/
|
|
101
107
|
export function createScheduler(opts) {
|
|
@@ -109,6 +115,7 @@ export function createScheduler(opts) {
|
|
|
109
115
|
onGate = () => {},
|
|
110
116
|
onAsk = defaultAsk,
|
|
111
117
|
maxParallel = defaultMaxParallel(),
|
|
118
|
+
maxParallelScripts = defaultMaxParallelScripts(),
|
|
112
119
|
log = () => {},
|
|
113
120
|
} = opts || {};
|
|
114
121
|
|
|
@@ -149,7 +156,6 @@ export function createScheduler(opts) {
|
|
|
149
156
|
const completions = [];
|
|
150
157
|
const warnings = [];
|
|
151
158
|
const controller = new AbortController();
|
|
152
|
-
let activeAgents = 0;
|
|
153
159
|
let ended = null;
|
|
154
160
|
let gate = null; // the CURRENT gate for state.gate (P4 stamps it)
|
|
155
161
|
let failure = null;
|
|
@@ -176,20 +182,27 @@ export function createScheduler(opts) {
|
|
|
176
182
|
signalled = false;
|
|
177
183
|
}
|
|
178
184
|
|
|
179
|
-
// --- the
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
// --- the two semaphores ---------------------------------------------------
|
|
186
|
+
// Agents and scripts are counted APART (D7): the agent pool caps LLM spend and
|
|
187
|
+
// rate-limit exposure, the script pool caps CPU. Each is a counting semaphore
|
|
188
|
+
// with a FIFO waiter queue. The drain walk POLLS a pool before launching a node;
|
|
189
|
+
// composite slices AWAIT the agent pool (they launch from inside a running
|
|
190
|
+
// execution). The composite SHELL holds no slot.
|
|
191
|
+
const pools = {
|
|
192
|
+
agent: { active: 0, max: maxParallel, queue: [] },
|
|
193
|
+
script: { active: 0, max: maxParallelScripts, queue: [] },
|
|
194
|
+
};
|
|
195
|
+
const poolOf = (node) => (node.kind === 'script' ? 'script' : 'agent');
|
|
196
|
+
function takeSlot(pool = 'agent') {
|
|
197
|
+
const p = pools[pool];
|
|
198
|
+
if (p.active < p.max) { p.active += 1; return Promise.resolve(); }
|
|
199
|
+
return new Promise((resolve) => { p.queue.push(resolve); });
|
|
188
200
|
}
|
|
189
|
-
function freeSlot() {
|
|
190
|
-
const
|
|
201
|
+
function freeSlot(pool = 'agent') {
|
|
202
|
+
const p = pools[pool];
|
|
203
|
+
const next = p.queue.shift();
|
|
191
204
|
if (next) { next(); return; } // handed straight over: the count is unchanged
|
|
192
|
-
|
|
205
|
+
p.active -= 1;
|
|
193
206
|
wake(); // a freed slot may unblock a queued launch
|
|
194
207
|
}
|
|
195
208
|
|
|
@@ -227,7 +240,8 @@ export function createScheduler(opts) {
|
|
|
227
240
|
kind: entry.kind,
|
|
228
241
|
ordinal: entry.ordinal,
|
|
229
242
|
status,
|
|
230
|
-
agentKey: node.kind === 'agent' ? (node.key ?? null) : null, //
|
|
243
|
+
agentKey: node.kind === 'agent' ? (node.key ?? null) : null, // agents only (D17)
|
|
244
|
+
key: KEYED_KINDS.includes(node.kind) ? (node.key ?? null) : null, // every keyed kind; flow rows carry none
|
|
231
245
|
trigger: entry.trigger,
|
|
232
246
|
// Composite sub-executions carry their slice identity; the UI collapses them
|
|
233
247
|
// under the node and labels them by title (A9: taskIndex/taskTotal ride along).
|
|
@@ -353,7 +367,7 @@ export function createScheduler(opts) {
|
|
|
353
367
|
/** Agent nodes take a semaphore slot; `execute` is called AT the slot. */
|
|
354
368
|
function fireAgent(node) {
|
|
355
369
|
const h = startExecution(node);
|
|
356
|
-
if (!h.composite)
|
|
370
|
+
if (!h.composite) pools[poolOf(node)].active += 1;
|
|
357
371
|
let p;
|
|
358
372
|
try { p = invoke(h); } catch (err) { p = Promise.reject(err); }
|
|
359
373
|
Promise.resolve(p).then(
|
|
@@ -435,7 +449,7 @@ export function createScheduler(opts) {
|
|
|
435
449
|
};
|
|
436
450
|
delete entry.expandsPort;
|
|
437
451
|
h.composite = false; // … so settle() frees the slot taken here
|
|
438
|
-
await takeSlot();
|
|
452
|
+
await takeSlot('agent');
|
|
439
453
|
h.args = argsFor(node, entry);
|
|
440
454
|
return await execute(h.args);
|
|
441
455
|
}
|
|
@@ -488,11 +502,11 @@ export function createScheduler(opts) {
|
|
|
488
502
|
async function runSlice(h, portId, ph, task, index, phaseAbort) {
|
|
489
503
|
const { node, entry } = h;
|
|
490
504
|
const signal = AbortSignal.any([controller.signal, phaseAbort.signal]);
|
|
491
|
-
await takeSlot();
|
|
505
|
+
await takeSlot('agent');
|
|
492
506
|
// A slot handed over AFTER the phase (or the run) aborted: never launch. The slice
|
|
493
507
|
// gets no ledger row (it never started) and rejects with the abort reason, so it
|
|
494
508
|
// is not counted as the phase's failure.
|
|
495
|
-
if (signal.aborted) { freeSlot(); throw signal.reason; }
|
|
509
|
+
if (signal.aborted) { freeSlot('agent'); throw signal.reason; }
|
|
496
510
|
const sub = {
|
|
497
511
|
executionId: sliceExecutionId(entry.executionId, task.id),
|
|
498
512
|
nodeId: node.id,
|
|
@@ -550,13 +564,13 @@ export function createScheduler(opts) {
|
|
|
550
564
|
if (!isAbortError(err)) phaseAbort.abort();
|
|
551
565
|
throw err;
|
|
552
566
|
} finally {
|
|
553
|
-
freeSlot();
|
|
567
|
+
freeSlot('agent');
|
|
554
568
|
}
|
|
555
569
|
}
|
|
556
570
|
|
|
557
571
|
function settle(h, res, err) {
|
|
558
572
|
running.delete(h.node.id);
|
|
559
|
-
if (!isFlow(h.node) && !h.composite) freeSlot();
|
|
573
|
+
if (!isFlow(h.node) && !h.composite) freeSlot(poolOf(h.node));
|
|
560
574
|
if (err || res?.error) failExecution(h, err || res.error);
|
|
561
575
|
else if (res?.paused === true) pausedExecution(h);
|
|
562
576
|
else if (res?.skipped === true) skippedExecution(h);
|
|
@@ -907,7 +921,7 @@ export function createScheduler(opts) {
|
|
|
907
921
|
if (!node) continue;
|
|
908
922
|
running.set(node.id, entry.executionId);
|
|
909
923
|
const h = { node, entry, args: argsFor(node, entry), composite: !!entry.expandsPort };
|
|
910
|
-
if (!isFlow(node) && !h.composite)
|
|
924
|
+
if (!isFlow(node) && !h.composite) pools[poolOf(node)].active += 1;
|
|
911
925
|
let p;
|
|
912
926
|
try { p = invoke(h); } catch (err) { p = Promise.reject(err); }
|
|
913
927
|
Promise.resolve(p).then(
|
|
@@ -998,7 +1012,8 @@ export function createScheduler(opts) {
|
|
|
998
1012
|
const node = nodeById.get(nodeId);
|
|
999
1013
|
if (!node || running.has(nodeId) || !isReady(node)) continue;
|
|
1000
1014
|
if (isFlow(node)) { await fireFlow(node); fired = true; continue; }
|
|
1001
|
-
|
|
1015
|
+
const pool = pools[poolOf(node)];
|
|
1016
|
+
if (pool.active >= pool.max) continue; // capped: retried once a slot frees
|
|
1002
1017
|
fireAgent(node);
|
|
1003
1018
|
fired = true;
|
|
1004
1019
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/core/graph/script-child.mjs
|
|
2
|
+
// The `node` runtime harness (spec §5.1, D24). Spawned as
|
|
3
|
+
// process.execPath script-child.mjs <program.mjs>
|
|
4
|
+
// with the envelope on stdin. Imports NOTHING from worca: it runs with
|
|
5
|
+
// WORCA_HOME stripped, so any store import would resolve the wrong home.
|
|
6
|
+
// Protocol: read ONE JSON envelope, import the program, call its default export
|
|
7
|
+
// with the api, write ONE JSON frame to stdout, exit 0. stdout is protocol-
|
|
8
|
+
// reserved, so console.* is routed to stderr (the run log). The child ALWAYS
|
|
9
|
+
// exits 0 after a frame; a non-zero exit means "crashed before the frame" and
|
|
10
|
+
// the parent reports "no result frame (exit N)".
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
|
|
13
|
+
// The REAL writers, bound before the program is imported: the exit flush below
|
|
14
|
+
// must not call a `process.stderr.write` the user's program replaced (muting a
|
|
15
|
+
// noisy dependency is a one-liner, and console.* rides stderr here) — that call
|
|
16
|
+
// waits for a callback the program controls, and the setInterval below holds the
|
|
17
|
+
// loop open, so the parent's timeout kill was the only way out.
|
|
18
|
+
const outWrite = process.stdout.write.bind(process.stdout);
|
|
19
|
+
const errWrite = process.stderr.write.bind(process.stderr);
|
|
20
|
+
const logs = [];
|
|
21
|
+
for (const m of ['log', 'info', 'warn', 'error', 'debug']) {
|
|
22
|
+
console[m] = (...a) => { process.stderr.write(a.map((x) => (typeof x === 'string' ? x : JSON.stringify(x))).join(' ') + '\n'); };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function main() {
|
|
26
|
+
process.stdin.setEncoding('utf8');
|
|
27
|
+
let raw = '';
|
|
28
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
29
|
+
const envelope = JSON.parse(raw);
|
|
30
|
+
const file = process.argv[2];
|
|
31
|
+
if (!file) throw new Error('script-child: no program file argument');
|
|
32
|
+
const mod = await import(pathToFileURL(file).href);
|
|
33
|
+
if (typeof mod.default !== 'function') throw new Error(`script module has no default-export function: ${file}`);
|
|
34
|
+
const api = {
|
|
35
|
+
inputs: envelope.inputs || {},
|
|
36
|
+
outputs: envelope.outputs || {},
|
|
37
|
+
params: envelope.params || {},
|
|
38
|
+
ctx: envelope.ctx || {},
|
|
39
|
+
verdictPath: envelope.verdictPath ?? null,
|
|
40
|
+
node: envelope.node || {},
|
|
41
|
+
execution: envelope.execution || {},
|
|
42
|
+
log: (level, msg) => { logs.push({ level: String(level), msg: String(msg) }); },
|
|
43
|
+
};
|
|
44
|
+
const result = await mod.default(api);
|
|
45
|
+
const r = result && typeof result === 'object' ? result : {};
|
|
46
|
+
const frame = { ok: true, logs };
|
|
47
|
+
if (r.outputs && typeof r.outputs === 'object') frame.outputs = r.outputs;
|
|
48
|
+
if (r.verdict !== undefined) frame.verdict = r.verdict;
|
|
49
|
+
if (typeof r.summary === 'string') frame.summary = r.summary;
|
|
50
|
+
return frame;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// A pending promise alone does not keep the event loop alive: a hung script with
|
|
54
|
+
// no live handles would exit 0 with NO frame. Hold the loop open so a hung script
|
|
55
|
+
// stays hung and the parent's timeout kill is the only way out.
|
|
56
|
+
setInterval(() => {}, 1 << 30);
|
|
57
|
+
|
|
58
|
+
main()
|
|
59
|
+
.catch((err) => ({ ok: false, error: { message: err?.message || String(err), stack: err?.stack || '' }, logs }))
|
|
60
|
+
.then((frame) => {
|
|
61
|
+
let text;
|
|
62
|
+
try { text = JSON.stringify(frame); } catch (err) {
|
|
63
|
+
text = JSON.stringify({ ok: false, error: { message: `frame is not serializable: ${err?.message || err}` }, logs });
|
|
64
|
+
}
|
|
65
|
+
// BOTH pipes before the exit. console.* rides STDERR, which is asynchronous on
|
|
66
|
+
// a macOS pipe: exiting on the stdout callback alone discarded every line still
|
|
67
|
+
// queued there, so a chatty script silently lost most of its live output
|
|
68
|
+
// whenever the host drained slower than the child wrote it.
|
|
69
|
+
// BOUNDED, and through the writers captured above: a program that muted or
|
|
70
|
+
// corked stderr never fires the flush callback, and the flush may never
|
|
71
|
+
// outlive the frame it is flushing.
|
|
72
|
+
// The bound is armed FROM the stdout callback, never beside the write: stdout
|
|
73
|
+
// is a pipe with a 64 KiB kernel buffer, so a frame bigger than that waits for
|
|
74
|
+
// the parent to read it — a timer racing the write exits mid-frame whenever the
|
|
75
|
+
// parent's loop is blocked longer than the bound, and the parent then reports
|
|
76
|
+
// `no result frame (exit 0)` for a run that produced one. The bound belongs to
|
|
77
|
+
// the STDERR flush, which is the only part the user's program can stall.
|
|
78
|
+
let exited = false;
|
|
79
|
+
const bye = () => { if (!exited) { exited = true; process.exit(0); } };
|
|
80
|
+
outWrite(text, () => { setTimeout(bye, 1000); errWrite('', bye); });
|
|
81
|
+
});
|