@aiwg/cli 2026.10.0 → 2026.10.3
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/agentic/code/providers/capability-matrix.yaml +270 -0
- package/agentic/code/providers/model-capabilities.v1.json +3091 -24
- package/bin/aiwg.mjs +86 -21
- package/dist/src/a2a/codecs.js +9 -0
- package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
- package/dist/src/artifacts/repair.js +9 -4
- package/dist/src/channel/manager.mjs +59 -10
- package/dist/src/cli/child-process-result.js +40 -0
- package/dist/src/cli/env.js +38 -16
- package/dist/src/cli/flag-parsing.js +236 -0
- package/dist/src/cli/handlers/decision.js +297 -2
- package/dist/src/cli/handlers/diagnose.js +9 -2
- package/dist/src/cli/handlers/feedback.js +14 -5
- package/dist/src/cli/handlers/init.js +245 -30
- package/dist/src/cli/handlers/mc.js +23 -0
- package/dist/src/cli/handlers/models.js +69 -6
- package/dist/src/cli/handlers/ralph.js +3 -0
- package/dist/src/cli/handlers/refresh.js +16 -0
- package/dist/src/cli/handlers/regenerate.js +14 -0
- package/dist/src/cli/handlers/runtime-info.js +24 -9
- package/dist/src/cli/handlers/script-runner.js +16 -6
- package/dist/src/cli/handlers/session.js +396 -62
- package/dist/src/cli/handlers/sessions.js +146 -40
- package/dist/src/cli/handlers/setup-manifest.js +439 -39
- package/dist/src/cli/handlers/setup.js +139 -39
- package/dist/src/cli/handlers/subcommands.js +3 -1
- package/dist/src/cli/handlers/team.js +18 -8
- package/dist/src/cli/handlers/use.js +13 -0
- package/dist/src/cli/handlers/utilities.js +106 -23
- package/dist/src/cli/prompt-utils.js +72 -0
- package/dist/src/cli/provider-resolution.js +11 -0
- package/dist/src/config/aiwg-config.js +268 -4
- package/dist/src/config/cli.js +146 -19
- package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
- package/dist/src/config/project-artifacts-health.mjs +14 -3
- package/dist/src/config/user-config.js +36 -10
- package/dist/src/decision/adapters/jev.js +80 -28
- package/dist/src/decision/ask.js +352 -0
- package/dist/src/decision/driver.js +4 -0
- package/dist/src/decision/entry.js +38 -6
- package/dist/src/decision/heldout/collector.js +10 -1
- package/dist/src/decision/index.js +3 -0
- package/dist/src/decision/interop.js +60 -8
- package/dist/src/decision/jev-credentials.js +121 -0
- package/dist/src/decision/ledger.js +839 -0
- package/dist/src/decision/validate.js +6 -3
- package/dist/src/effects/dispatch-verifier.js +252 -0
- package/dist/src/effects/index.js +1 -0
- package/dist/src/features/installer.js +2 -2
- package/dist/src/flow/graph-metadata.js +5 -0
- package/dist/src/gates/providers/loader.js +3 -1
- package/dist/src/installation/manager-command.mjs +1 -1
- package/dist/src/installation/manager.mjs +17 -2
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/adapters/codex-runtime.js +360 -56
- package/dist/src/mcp/cli.mjs +587 -77
- package/dist/src/mcp/config-file.mjs +279 -0
- package/dist/src/mcp/config-layers.mjs +348 -0
- package/dist/src/mcp/credentials.mjs +484 -0
- package/dist/src/mcp/grok-build-config.mjs +16 -26
- package/dist/src/mcp/helpers.mjs +106 -2
- package/dist/src/mcp/omp-config.mjs +50 -29
- package/dist/src/mcp/profiles.js +97 -23
- package/dist/src/mcp/profiles.mjs +99 -27
- package/dist/src/mcp/registry.js +264 -57
- package/dist/src/mcp/registry.mjs +198 -45
- package/dist/src/mcp/server.mjs +403 -20
- package/dist/src/mcp/toml-string.mjs +9 -0
- package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
- package/dist/src/mcp/tool-filters.mjs +357 -0
- package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
- package/dist/src/mcp/tools/decision.mjs +58 -0
- package/dist/src/mcp/tools/orchestration.mjs +38 -0
- package/dist/src/mcp/tools/subsystems.mjs +39 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
- package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
- package/dist/src/memory/project-registry.js +2 -2
- package/dist/src/mission-protocol/codecs.js +639 -1
- package/dist/src/mission-protocol/types.js +110 -0
- package/dist/src/mission-protocol/worker-status.js +2 -0
- package/dist/src/models/config-loader.js +70 -13
- package/dist/src/models/grok-build-models.js +93 -41
- package/dist/src/models/model-capabilities.v1.json +3091 -24
- package/dist/src/models/model-discovery.js +862 -63
- package/dist/src/models/provider-policy.js +625 -11
- package/dist/src/models/qualification-digest.mjs +17 -0
- package/dist/src/models/resolver.js +259 -0
- package/dist/src/models/worker-direct-readiness.js +529 -0
- package/dist/src/models/worker-dispatch-offer.js +169 -0
- package/dist/src/models/worker-health-readiness.js +131 -0
- package/dist/src/models/worker-installation.mjs +879 -0
- package/dist/src/models/wrapper-route.js +18 -0
- package/dist/src/ops/registry.js +9 -14
- package/dist/src/plugins/standalone-packager.js +4 -3
- package/dist/src/process/bounded-child-process.mjs +387 -0
- package/dist/src/process/cli-command.mjs +75 -0
- package/dist/src/providers/capability-matrix.js +8 -0
- package/dist/src/providers/capability-matrix.yaml +270 -0
- package/dist/src/providers/grok-build-headless.js +43 -21
- package/dist/src/providers/provider-definitions.js +9 -1
- package/dist/src/providers/provider-definitions.mjs +9 -2
- package/dist/src/serve/executor-registry.js +16 -24
- package/dist/src/serve/fleet-mission-conductor.js +9 -0
- package/dist/src/serve/mission-conductor.js +9 -0
- package/dist/src/serve/shared-host-scheduler.js +2 -0
- package/dist/src/serve/worker-invocation.js +538 -0
- package/dist/src/serve/worker-stdin-frame.mjs +107 -0
- package/dist/src/sessions/adapter-registry.js +45 -0
- package/dist/src/sessions/adapters/generic.js +37 -3
- package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
- package/dist/src/sessions/adapters/grokbot.js +423 -24
- package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
- package/dist/src/sessions/adapters/muse.js +68 -29
- package/dist/src/sessions/adapters/pi.js +1 -0
- package/dist/src/sessions/batch-contracts.js +13 -1
- package/dist/src/sessions/batch-import.js +23 -42
- package/dist/src/sessions/candidates.js +47 -11
- package/dist/src/sessions/contracts.js +35 -0
- package/dist/src/sessions/coverage-preflight.js +134 -0
- package/dist/src/sessions/discovery.js +3 -0
- package/dist/src/sessions/grok-build-cli.js +67 -5
- package/dist/src/sessions/importer.js +24 -8
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/repository.js +54 -13
- package/dist/src/sessions/workspace-discovery.js +99 -37
- package/dist/src/skills/adapters/clawhub.js +10 -8
- package/dist/src/skills/adapters/openclaw.js +10 -7
- package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
- package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
- package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
- package/dist/src/smiths/mcpsmith/example.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
- package/dist/src/storage/backends/logseq.js +68 -7
- package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
- package/dist/src/testing/generators/test-code-generator.js +57 -30
- package/dist/src/uhp/client.js +11 -1
- package/dist/src/uhp/mission.js +5 -0
- package/dist/src/update/checker.mjs +5 -5
- package/dist/src/update/notifier.mjs +54 -2
- package/dist/src/writing/fidelity.js +18 -1
- package/package.json +18 -1
- package/schemas/mission-v1.schema.json +2429 -0
- package/schemas/models/model-catalog.v1.schema.json +301 -0
- package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
- package/schemas/models/model-policy.v1.schema.json +14 -0
- package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
- package/tools/_resolve-impl.mjs +39 -0
- package/tools/agents/deploy-agents.mjs +15 -2
- package/tools/agents/providers/base.mjs +63 -43
- package/tools/agents/providers/grokbot.mjs +11 -12
- package/tools/agents/providers/hermes.mjs +6 -3
- package/tools/agents/providers/omp.mjs +5 -2
- package/tools/agents/providers/openclaw-translator.mjs +19 -5
- package/tools/providers/deepseek-harness-transport.mjs +358 -0
- package/tools/providers/omp-transport.mjs +192 -0
- package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
- package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
- package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
- package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
- package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
- package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
- package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
- package/tools/ralph-external/lib/worker-health.mjs +439 -0
- package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
package/bin/aiwg.mjs
CHANGED
|
@@ -244,10 +244,81 @@ if (process.argv.slice(2).includes('--dry-run')) {
|
|
|
244
244
|
process.env['AIWG_CLI_DRY_RUN'] = '1';
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
// Startup tracing — set AIWG_TRACE_STARTUP=1 to print per-phase timings to
|
|
248
|
+
// stderr. Useful for diagnosing cold-start regressions, and for a test to
|
|
249
|
+
// observe whether the credential-handling skip below actually fired,
|
|
250
|
+
// without needing the update notifier to otherwise be eligible to run (it
|
|
251
|
+
// also needs a real terminal — see `isCredentialHandlingCommand()`'s own
|
|
252
|
+
// tests). No-op by default so it doesn't cost anything on the hot path.
|
|
253
|
+
// Defined ahead of its first use below (moved up from just before
|
|
254
|
+
// `resolveRouterPath()`, where it used to sit) so the notifier gate can use
|
|
255
|
+
// it too.
|
|
256
|
+
const traceStartup = process.env['AIWG_TRACE_STARTUP'] === '1' ||
|
|
257
|
+
process.env['AIWG_TRACE_STARTUP']?.toLowerCase() === 'true';
|
|
258
|
+
const startHr = process.hrtime.bigint();
|
|
259
|
+
function trace(phase) {
|
|
260
|
+
if (!traceStartup) return;
|
|
261
|
+
const ms = Number(process.hrtime.bigint() - startHr) / 1_000_000;
|
|
262
|
+
process.stderr.write(`[trace] +${ms.toFixed(1)}ms ${phase}\n`);
|
|
263
|
+
}
|
|
264
|
+
trace('bin:entry');
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* True iff this invocation is a command that reads a secret directly from
|
|
268
|
+
* the environment or from an argument as part of its own documented
|
|
269
|
+
* contract (go/no-go review, blocker 1A). Checked from argv alone — no
|
|
270
|
+
* dist/ import, no router resolution — so it costs nothing on the
|
|
271
|
+
* `--version`/`--help` fast paths and can run before any bootstrap
|
|
272
|
+
* subprocess starts.
|
|
273
|
+
*
|
|
274
|
+
* No bootstrap subprocess may start before dispatch for one of these,
|
|
275
|
+
* because a bootstrap child that copies the parent's environment (or is
|
|
276
|
+
* merely spawned while `NODE_DEBUG=child_process` is set) would otherwise
|
|
277
|
+
* see whatever the command's own secret-bearing variable holds, before the
|
|
278
|
+
* command itself ever ran. Scrubbing a specific variable out of every
|
|
279
|
+
* possible bootstrap child is not attempted here — not starting the
|
|
280
|
+
* bootstrap at all for these commands is the smaller, certain fix. (The
|
|
281
|
+
* background update-check child also gets its own environment allow-list,
|
|
282
|
+
* independent of this list — see `notifierChildEnv()` in
|
|
283
|
+
* `src/update/notifier.mjs` — so a secret in the parent's environment that
|
|
284
|
+
* this list does not yet name is still not copied into that one child.)
|
|
285
|
+
*
|
|
286
|
+
* Qualifying commands, and why:
|
|
287
|
+
* - `mcp profile login` — reads an API key via `--api-key-env`,
|
|
288
|
+
* `--api-key-stdin`, or `--api-key-file`.
|
|
289
|
+
* - `decision setup jev` — without `--token-stdin`, reads the token
|
|
290
|
+
* directly from `JEV_API_KEY`/`AIWG_DECISION_JEV_API_KEY`.
|
|
291
|
+
*
|
|
292
|
+
* Checked and left out, with reasons (see the go/no-go review response):
|
|
293
|
+
* - `auth login` — an OAuth device/browser flow; it does not read a
|
|
294
|
+
* pre-existing secret from the environment or argv at invocation, it
|
|
295
|
+
* obtains one over the network after the user authorizes it.
|
|
296
|
+
* - `mcp inject` / `mcp add` / `mcp update` (including `--header-env`) —
|
|
297
|
+
* `headerEnv`/`envFrom` references only ever carry a variable *name*
|
|
298
|
+
* into a provider's config (resolved by that provider later, not by
|
|
299
|
+
* this process); a literal `--env NAME=value` on `mcp add` is stored
|
|
300
|
+
* to the registry file and never touches `process.env` here.
|
|
301
|
+
*/
|
|
302
|
+
function isCredentialHandlingCommand(args) {
|
|
303
|
+
const positionals = [];
|
|
304
|
+
for (const arg of args) {
|
|
305
|
+
if (arg === '--') break;
|
|
306
|
+
if (!arg.startsWith('-')) {
|
|
307
|
+
positionals.push(arg);
|
|
308
|
+
if (positionals.length >= 3) break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (positionals[0] === 'mcp' && positionals[1] === 'profile' && positionals[2] === 'login') return true;
|
|
312
|
+
if (positionals[0] === 'decision' && positionals[1] === 'setup' && positionals[2] === 'jev') return true;
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
247
316
|
// Display a cached notice and schedule its refresh before every eligible CLI
|
|
248
317
|
// path, including fast help/version, channel recovery, and later preflight
|
|
249
|
-
// failures
|
|
250
|
-
//
|
|
318
|
+
// failures — except a credential-handling command (above), which never gets
|
|
319
|
+
// this or any other bootstrap subprocess before dispatch. This local-only
|
|
320
|
+
// bootstrap never waits on the registry and failures are deliberately
|
|
321
|
+
// ignored so update advice cannot change command behavior.
|
|
251
322
|
async function runUpdateNotifierBootstrap() {
|
|
252
323
|
try {
|
|
253
324
|
const notifierPath = path.join(packageRoot, 'dist', 'src', 'update', 'notifier.mjs');
|
|
@@ -260,12 +331,18 @@ async function runUpdateNotifierBootstrap() {
|
|
|
260
331
|
}
|
|
261
332
|
}
|
|
262
333
|
|
|
263
|
-
|
|
334
|
+
const cliArgs = process.argv.slice(2);
|
|
335
|
+
if (isCredentialHandlingCommand(cliArgs)) {
|
|
336
|
+
trace('notifier:skip (credential-handling command)');
|
|
337
|
+
} else {
|
|
338
|
+
await runUpdateNotifierBootstrap();
|
|
339
|
+
trace('notifier:bootstrap-complete');
|
|
340
|
+
}
|
|
264
341
|
|
|
265
|
-
if (maybeHandleFastVersion(
|
|
342
|
+
if (maybeHandleFastVersion(cliArgs)) {
|
|
266
343
|
process.exit(0);
|
|
267
344
|
}
|
|
268
|
-
if (maybeHandleFastHelp(
|
|
345
|
+
if (maybeHandleFastHelp(cliArgs)) {
|
|
269
346
|
process.exit(0);
|
|
270
347
|
}
|
|
271
348
|
|
|
@@ -286,19 +363,6 @@ function ensureInvocationId() {
|
|
|
286
363
|
}
|
|
287
364
|
const invocationId = ensureInvocationId();
|
|
288
365
|
|
|
289
|
-
// Startup tracing — set AIWG_TRACE_STARTUP=1 to print per-phase timings to
|
|
290
|
-
// stderr. Useful for diagnosing cold-start regressions. No-op by default so
|
|
291
|
-
// it doesn't cost anything on the hot path.
|
|
292
|
-
const traceStartup = process.env['AIWG_TRACE_STARTUP'] === '1' ||
|
|
293
|
-
process.env['AIWG_TRACE_STARTUP']?.toLowerCase() === 'true';
|
|
294
|
-
const startHr = process.hrtime.bigint();
|
|
295
|
-
function trace(phase) {
|
|
296
|
-
if (!traceStartup) return;
|
|
297
|
-
const ms = Number(process.hrtime.bigint() - startHr) / 1_000_000;
|
|
298
|
-
process.stderr.write(`[trace] +${ms.toFixed(1)}ms ${phase}\n`);
|
|
299
|
-
}
|
|
300
|
-
trace('bin:entry');
|
|
301
|
-
|
|
302
366
|
/**
|
|
303
367
|
* Resolve the path to the compiled router.
|
|
304
368
|
*
|
|
@@ -309,10 +373,11 @@ trace('bin:entry');
|
|
|
309
373
|
* emit a clear error and exit rather than falling back to a tsx fork.
|
|
310
374
|
*/
|
|
311
375
|
async function resolveRouterPath() {
|
|
312
|
-
const { loadConfig } = await import('../dist/src/channel/manager.mjs');
|
|
376
|
+
const { loadConfig, resolveEffectiveInstallationRoot } = await import('../dist/src/channel/manager.mjs');
|
|
313
377
|
const config = await loadConfig({ createIfMissing: !process.argv.slice(2).includes('--dry-run') });
|
|
314
|
-
|
|
315
|
-
|
|
378
|
+
const effectiveRoot = resolveEffectiveInstallationRoot(packageRoot, config);
|
|
379
|
+
if (effectiveRoot !== packageRoot) {
|
|
380
|
+
const devRouter = path.join(effectiveRoot, 'dist', 'src', 'cli', 'router.js');
|
|
316
381
|
if (!existsSync(devRouter)) {
|
|
317
382
|
console.error(`Dev mode: compiled router not found at ${devRouter}`);
|
|
318
383
|
console.error(` Run: (cd ${config.edgePath} && npm run build)`);
|
package/dist/src/a2a/codecs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertMissionWorkerHandoff } from '../mission-protocol/codecs.js';
|
|
1
2
|
export function encodePushNotificationConfig(version, config) {
|
|
2
3
|
assertNonEmptyString(version, '$.url', config.url);
|
|
3
4
|
if (version === '0.3') {
|
|
@@ -81,6 +82,7 @@ const V1_TO_STATE = {
|
|
|
81
82
|
};
|
|
82
83
|
const STATE_TO_V1 = Object.fromEntries(Object.entries(V1_TO_STATE).map(([wire, normalized]) => [normalized, wire]));
|
|
83
84
|
export function encodeMessage(version, message) {
|
|
85
|
+
assertMissionWorkerHandoff(message, 'a2a');
|
|
84
86
|
assertNonEmptyString(version, '$.messageId', message.messageId);
|
|
85
87
|
if (message.role !== 'user' && message.role !== 'agent') {
|
|
86
88
|
fail(version, '$.role', 'must be user or agent');
|
|
@@ -101,6 +103,7 @@ export function encodeMessage(version, message) {
|
|
|
101
103
|
return out;
|
|
102
104
|
}
|
|
103
105
|
export function decodeMessage(version, input, path = '$') {
|
|
106
|
+
assertMissionWorkerHandoff(input, 'a2a');
|
|
104
107
|
const obj = objectAt(version, path, input);
|
|
105
108
|
const messageId = stringAt(version, `${path}.messageId`, obj.messageId);
|
|
106
109
|
const role = decodeRole(version, obj.role, `${path}.role`);
|
|
@@ -120,6 +123,7 @@ export function decodeMessage(version, input, path = '$') {
|
|
|
120
123
|
return message;
|
|
121
124
|
}
|
|
122
125
|
export function encodeTask(version, task) {
|
|
126
|
+
assertMissionWorkerHandoff(task, 'a2a');
|
|
123
127
|
assertNonEmptyString(version, '$.id', task.id);
|
|
124
128
|
const out = {
|
|
125
129
|
id: task.id,
|
|
@@ -137,6 +141,7 @@ export function encodeTask(version, task) {
|
|
|
137
141
|
return out;
|
|
138
142
|
}
|
|
139
143
|
export function decodeTask(version, input, path = '$') {
|
|
144
|
+
assertMissionWorkerHandoff(input, 'a2a');
|
|
140
145
|
const obj = objectAt(version, path, input);
|
|
141
146
|
if (version === '1.0' && typeof obj.kind === 'string') {
|
|
142
147
|
fail(version, `${path}.kind`, 'legacy kind discriminator is not valid in 1.0');
|
|
@@ -219,6 +224,7 @@ export function decodeStatus(version, input, path = '$') {
|
|
|
219
224
|
return status;
|
|
220
225
|
}
|
|
221
226
|
export function encodeArtifact(version, artifact, path = '$') {
|
|
227
|
+
assertMissionWorkerHandoff(artifact, 'a2a');
|
|
222
228
|
assertNonEmptyString(version, `${path}.artifactId`, artifact.artifactId);
|
|
223
229
|
if (!Array.isArray(artifact.parts) || artifact.parts.length === 0) {
|
|
224
230
|
fail(version, `${path}.parts`, 'must contain at least one part');
|
|
@@ -234,6 +240,7 @@ export function encodeArtifact(version, artifact, path = '$') {
|
|
|
234
240
|
return out;
|
|
235
241
|
}
|
|
236
242
|
export function decodeArtifact(version, input, path = '$') {
|
|
243
|
+
assertMissionWorkerHandoff(input, 'a2a');
|
|
237
244
|
const obj = objectAt(version, path, input);
|
|
238
245
|
const partsRaw = arrayAt(version, `${path}.parts`, obj.parts);
|
|
239
246
|
if (partsRaw.length === 0)
|
|
@@ -260,6 +267,7 @@ function encodeMessageAt(version, message, path) {
|
|
|
260
267
|
}
|
|
261
268
|
}
|
|
262
269
|
function encodePart(version, part, path) {
|
|
270
|
+
assertMissionWorkerHandoff(part, 'a2a');
|
|
263
271
|
const common = {};
|
|
264
272
|
copyJsonObject(version, common, 'metadata', part.metadata, `${path}.metadata`);
|
|
265
273
|
copyOptional(common, version === '0.3' ? 'mimeType' : 'mediaType', part.mediaType);
|
|
@@ -288,6 +296,7 @@ function encodePart(version, part, path) {
|
|
|
288
296
|
};
|
|
289
297
|
}
|
|
290
298
|
function decodePart(version, input, path) {
|
|
299
|
+
assertMissionWorkerHandoff(input, 'a2a');
|
|
291
300
|
const obj = objectAt(version, path, input);
|
|
292
301
|
if (version === '0.3')
|
|
293
302
|
return decodeLegacyPart(obj, path);
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import * as fs from 'fs';
|
|
22
22
|
import * as path from 'path';
|
|
23
23
|
import { spawnSync } from 'child_process';
|
|
24
|
+
import { boundedSpawnSync, positiveTimeoutMs } from '../../process/bounded-child-process.mjs';
|
|
24
25
|
/** Strict-transcription prompt. `title` parameterizes the corpus-specific intro. */
|
|
25
26
|
export function buildPrompt(page, title) {
|
|
26
27
|
const subject = title ? `from ${title}` : 'from a scanned document';
|
|
@@ -53,7 +54,11 @@ const codexAdapter = {
|
|
|
53
54
|
try {
|
|
54
55
|
const r = spawnSync(process.env.AIWG_CODEX_BIN || 'codex', args, {
|
|
55
56
|
input: prompt,
|
|
56
|
-
|
|
57
|
+
// ctx.timeoutSec is caller-controlled (a CLI flag); clamp it so a
|
|
58
|
+
// configured 0 (or anything else non-positive) can't disable the
|
|
59
|
+
// bound entirely (#3062 B/C finding 8). 240s matches this
|
|
60
|
+
// function's own documented default (see extractPages() below).
|
|
61
|
+
timeout: positiveTimeoutMs(ctx.timeoutSec * 1000, 240_000),
|
|
57
62
|
encoding: 'utf8',
|
|
58
63
|
});
|
|
59
64
|
if (r.status === 0 && fs.existsSync(tmp)) {
|
|
@@ -70,15 +75,93 @@ const codexAdapter = {
|
|
|
70
75
|
}
|
|
71
76
|
},
|
|
72
77
|
};
|
|
78
|
+
/** Paths enter the shell as positional arguments, never as shell source. */
|
|
79
|
+
const MAX_VISION_TEMPLATE_LENGTH = 4096;
|
|
80
|
+
function visionCommand(template) {
|
|
81
|
+
const guidance = 'Use "$1"/"$2"/"$3" as data operands or move complex logic to a wrapper script';
|
|
82
|
+
const reject = (construct) => {
|
|
83
|
+
throw new Error(`Vision command rejects ${construct}. ${guidance}`);
|
|
84
|
+
};
|
|
85
|
+
// Bound the checks below; a vision command template is a short command line.
|
|
86
|
+
if (template.length > MAX_VISION_TEMPLATE_LENGTH)
|
|
87
|
+
reject(`templates longer than ${MAX_VISION_TEMPLATE_LENGTH} characters`);
|
|
88
|
+
// A vision template is one command line; a backslash-newline continuation can
|
|
89
|
+
// join an allowed name with following text into a different variable at runtime.
|
|
90
|
+
if (/\\\r?\n|\\\r/.test(template))
|
|
91
|
+
reject('line continuations (backslash-newline)');
|
|
92
|
+
// Recognize literal command names even when quoted or backslash-escaped.
|
|
93
|
+
const syntax = template.replace(/\\([^\n])/g, '$1').replace(/["']/g, '');
|
|
94
|
+
if (syntax.includes('$('))
|
|
95
|
+
reject('command substitution ($(...))');
|
|
96
|
+
if (template.includes('`'))
|
|
97
|
+
reject('backticks');
|
|
98
|
+
if (/\beval\b/.test(syntax))
|
|
99
|
+
reject('eval');
|
|
100
|
+
if (syntax.includes('<<'))
|
|
101
|
+
reject('here-documents and here-strings (<<)');
|
|
102
|
+
// Conservatively reject a shell word together with any short -c option
|
|
103
|
+
// cluster, regardless of order, wrappers, redirections or command boundaries.
|
|
104
|
+
const words = syntax.split(/[\s;&|(){}<>]+/);
|
|
105
|
+
const shellWords = words.map(word => word.slice(word.lastIndexOf('/') + 1)
|
|
106
|
+
.match(/^(?:sh|bash|zsh|dash|ksh|ash|mksh|yash|fish|csh|tcsh|rbash|busybox|toybox)(?:$|(-[A-Za-z]*c))/));
|
|
107
|
+
const hasShell = shellWords.some(Boolean);
|
|
108
|
+
if (shellWords.some(word => word?.[1]) || (hasShell && words.some(word => /^-[A-Za-z]*c/.test(word))))
|
|
109
|
+
reject('nested shell invocation (-c)');
|
|
110
|
+
// Only contract-provided parameters may expand; match at each dollar without
|
|
111
|
+
// rescanning the remaining template. Braces must contain only the exact name.
|
|
112
|
+
const parameter = /\$(?:[123](?![0-9])|AIWG_VISION_(?:IMAGE|PROMPT_FILE|OUT)(?![A-Za-z0-9_])|\{(?:[123]|AIWG_VISION_(?:IMAGE|PROMPT_FILE|OUT))\})/y;
|
|
113
|
+
// Track quotes to reject quoted/escaped placeholders, expansions and comments.
|
|
114
|
+
let quote = '';
|
|
115
|
+
return template.replace(/\\?\{(image|prompt_file|out)\}|\\[\s\S]|\$'|[$"'#]/g, (token, name, offset) => {
|
|
116
|
+
if ((token === '#' || token === '\\#') && quote !== "'")
|
|
117
|
+
reject('comments (# outside single quotes)');
|
|
118
|
+
if (name) {
|
|
119
|
+
if (quote)
|
|
120
|
+
throw new Error(`Vision command placeholders must be outside shell quotes. ${guidance}`);
|
|
121
|
+
if (token.startsWith('\\'))
|
|
122
|
+
throw new Error(`Vision command placeholders must not be escaped. ${guidance}`);
|
|
123
|
+
return `"$${name === 'image' ? 1 : name === 'prompt_file' ? 2 : 3}"`;
|
|
124
|
+
}
|
|
125
|
+
if (token === '#')
|
|
126
|
+
return token;
|
|
127
|
+
// Backslashes are literal inside POSIX single quotes, but escape the next
|
|
128
|
+
// character in double quotes and ANSI-C ($'...') quotes.
|
|
129
|
+
if (token.startsWith('\\')) {
|
|
130
|
+
if (quote === "'" && token[1] === "'")
|
|
131
|
+
quote = '';
|
|
132
|
+
return token;
|
|
133
|
+
}
|
|
134
|
+
if (token.startsWith('$')) {
|
|
135
|
+
if (quote !== "'") {
|
|
136
|
+
parameter.lastIndex = offset;
|
|
137
|
+
if (!parameter.test(template))
|
|
138
|
+
reject('parameter expansions other than the provided vision variables');
|
|
139
|
+
}
|
|
140
|
+
if (token === '$')
|
|
141
|
+
return token;
|
|
142
|
+
}
|
|
143
|
+
if (!quote)
|
|
144
|
+
quote = token;
|
|
145
|
+
else if (quote === token || (quote === "$'" && token === "'") || (quote === "'" && token === "$'"))
|
|
146
|
+
quote = '';
|
|
147
|
+
return token;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
73
150
|
/**
|
|
74
151
|
* Generic command-template adapter (provider-neutral). Runs
|
|
75
152
|
* `AIWG_VISION_COMMAND` (or the `--command` flag) with placeholders:
|
|
76
|
-
* {image} absolute image path
|
|
77
|
-
* {prompt_file} path to a file containing the prompt
|
|
78
|
-
* {out} path the command must write the transcription to
|
|
79
|
-
*
|
|
153
|
+
* {image} "$1": absolute image path
|
|
154
|
+
* {prompt_file} "$2": path to a file containing the prompt
|
|
155
|
+
* {out} "$3": path the command must write the transcription to
|
|
156
|
+
* Placeholders must be unquoted and unescaped. AIWG_VISION_IMAGE /
|
|
157
|
+
* AIWG_VISION_PROMPT_FILE / AIWG_VISION_OUT are also exported.
|
|
158
|
+
* Only $1/$2/$3 and the exported vision variables (also braced) may expand.
|
|
159
|
+
* Other $ expansions, command substitution, backticks, eval, nested shells with -c, <<, and #
|
|
160
|
+
* outside single quotes are rejected; use a wrapper script for complex logic.
|
|
161
|
+
* e.g. vision-cli --prompt-file {prompt_file} --image {image} > {out}
|
|
80
162
|
*/
|
|
81
163
|
function commandAdapter(template) {
|
|
164
|
+
const cmd = visionCommand(template);
|
|
82
165
|
return {
|
|
83
166
|
name: 'command',
|
|
84
167
|
transcribe(imagePath, prompt, ctx) {
|
|
@@ -86,12 +169,12 @@ function commandAdapter(template) {
|
|
|
86
169
|
const promptFile = path.join(dir, `.vision-prompt-${ctx.page}.txt`);
|
|
87
170
|
const out = path.join(dir, `.vision-out-${ctx.page}.txt`);
|
|
88
171
|
fs.writeFileSync(promptFile, prompt);
|
|
89
|
-
const cmd = template
|
|
90
|
-
.replaceAll('{image}', JSON.stringify(imagePath))
|
|
91
|
-
.replaceAll('{prompt_file}', JSON.stringify(promptFile))
|
|
92
|
-
.replaceAll('{out}', JSON.stringify(out));
|
|
93
172
|
try {
|
|
94
|
-
|
|
173
|
+
// Same caller-controlled-timeout clamp as the codex adapter above.
|
|
174
|
+
const r = spawnSync('sh', ['-c', cmd, 'aiwg-vision', imagePath, promptFile, out], {
|
|
175
|
+
timeout: positiveTimeoutMs(ctx.timeoutSec * 1000, 240_000), encoding: 'utf8',
|
|
176
|
+
env: { ...process.env, AIWG_VISION_IMAGE: imagePath, AIWG_VISION_PROMPT_FILE: promptFile, AIWG_VISION_OUT: out },
|
|
177
|
+
});
|
|
95
178
|
let text;
|
|
96
179
|
if (fs.existsSync(out))
|
|
97
180
|
text = fs.readFileSync(out, 'utf8');
|
|
@@ -115,7 +198,7 @@ export function resolveAdapter(opts = {}) {
|
|
|
115
198
|
const provider = opts.provider ?? process.env.AIWG_VISION_PROVIDER ?? (command ? 'command' : 'codex');
|
|
116
199
|
if (provider === 'command') {
|
|
117
200
|
if (!command)
|
|
118
|
-
throw new Error("vision provider 'command' requires --command or AIWG_VISION_COMMAND (template with {image}/{prompt_file}/{out})");
|
|
201
|
+
throw new Error("vision provider 'command' requires --command or AIWG_VISION_COMMAND (template with unquoted, unescaped {image}/{prompt_file}/{out} placeholders)");
|
|
119
202
|
return commandAdapter(command);
|
|
120
203
|
}
|
|
121
204
|
if (provider === 'codex')
|
|
@@ -125,7 +208,8 @@ export function resolveAdapter(opts = {}) {
|
|
|
125
208
|
// ── PDF → PNG rasterization (poppler pdftoppm) ───────────────────────────────
|
|
126
209
|
export function pdftoppmAvailable() {
|
|
127
210
|
try {
|
|
128
|
-
return
|
|
211
|
+
return boundedSpawnSync('pdftoppm', ['-v'], 'quick', { encoding: 'utf8' }).status === 0
|
|
212
|
+
|| boundedSpawnSync('pdftoppm', ['-h'], 'quick', { encoding: 'utf8' }).status !== null;
|
|
129
213
|
}
|
|
130
214
|
catch {
|
|
131
215
|
return false;
|
|
@@ -144,9 +228,15 @@ export function rasterizePdf(pdfPath, outDir, dpi = 200) {
|
|
|
144
228
|
fs.mkdirSync(outDir, { recursive: true });
|
|
145
229
|
// pdftoppm writes <prefix>-<n>.png with its own zero-padding based on page count.
|
|
146
230
|
const prefix = path.join(outDir, 'page');
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
231
|
+
// Rasterizing a multi-page PDF can take a while; long tier (#3062).
|
|
232
|
+
const r = boundedSpawnSync('pdftoppm', ['-png', '-r', String(dpi), pdfPath, prefix], 'long', { encoding: 'utf8' });
|
|
233
|
+
if (r.status !== 0) {
|
|
234
|
+
// #3062 finding 14: r.error.message (when present) carries the
|
|
235
|
+
// enriched timeout explanation (tier/bound/override) that stderr/
|
|
236
|
+
// status alone do not — put it first, captured stderr after.
|
|
237
|
+
const detail = r.error ? r.error.message : `pdftoppm failed: ${r.stderr || r.status}`;
|
|
238
|
+
throw new Error(r.error && r.stderr ? `${detail} (stderr: ${r.stderr})` : detail);
|
|
239
|
+
}
|
|
150
240
|
// Normalize names to page-NNN.png (3-digit zero-pad).
|
|
151
241
|
let count = 0;
|
|
152
242
|
for (const f of fs.readdirSync(outDir)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { PROJECT_CONTROL_PLANE_FILES, auditProjectArtifactHealth, } from '../config/project-artifacts.js';
|
|
4
|
+
import { PROJECT_CONTROL_PLANE_FILES, auditProjectArtifactHealth, isProjectArtifactRootExternal, } from '../config/project-artifacts.js';
|
|
5
5
|
async function optionalBytes(filePath) {
|
|
6
6
|
try {
|
|
7
7
|
return await readFile(filePath);
|
|
@@ -21,14 +21,19 @@ async function writeAndVerify(destination, bytes) {
|
|
|
21
21
|
}
|
|
22
22
|
export async function repairProjectArtifacts(options) {
|
|
23
23
|
const projectDir = path.resolve(options.projectDir);
|
|
24
|
+
// Refuse local mode before auditing — the audit's own payload scan is a
|
|
25
|
+
// no-op in local mode now (#274), but the refusal should not depend on
|
|
26
|
+
// running the audit first either (repair.ts:45-51 previously audited
|
|
27
|
+
// before this check existed, so the refusal reason always paid for the
|
|
28
|
+
// scan it was about to reject).
|
|
29
|
+
if (!isProjectArtifactRootExternal(projectDir)) {
|
|
30
|
+
throw new Error('No external AIWG artifact corpus is configured for this project.');
|
|
31
|
+
}
|
|
24
32
|
const before = auditProjectArtifactHealth(projectDir);
|
|
25
33
|
const copied = [];
|
|
26
34
|
const migrated = [];
|
|
27
35
|
const archivedConflicts = [];
|
|
28
36
|
const removed = [];
|
|
29
|
-
if (!before.external_configured) {
|
|
30
|
-
throw new Error('No external AIWG artifact corpus is configured for this project.');
|
|
31
|
-
}
|
|
32
37
|
if (!before.external_reachable) {
|
|
33
38
|
throw new Error(`External AIWG artifact corpus is unavailable: ${before.artifact_root}`);
|
|
34
39
|
}
|
|
@@ -13,9 +13,10 @@ import fs from 'fs/promises';
|
|
|
13
13
|
import { readFileSync, existsSync } from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
|
-
import {
|
|
16
|
+
import { spawn } from 'child_process';
|
|
17
17
|
import os from 'os';
|
|
18
18
|
import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
|
|
19
|
+
import { boundedExecFileSync } from '../process/bounded-child-process.mjs';
|
|
19
20
|
import {
|
|
20
21
|
assertCanonicalInstallation,
|
|
21
22
|
createInstallationIdentity,
|
|
@@ -135,6 +136,50 @@ export function getPackageRoot() {
|
|
|
135
136
|
return path.resolve(__dirname, '..', '..');
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Resolve the installation root bin/aiwg.mjs actually dispatches to.
|
|
141
|
+
*
|
|
142
|
+
* This is the exact decision `resolveRouterPath()` in bin/aiwg.mjs makes
|
|
143
|
+
* when picking which `dist/` to load the compiled router (and therefore
|
|
144
|
+
* every compiled-dist module it imports, including the MCP server) from:
|
|
145
|
+
* the dev-mode edge checkout when configured and present and distinct from
|
|
146
|
+
* the package root, otherwise the package root itself. Shared so health
|
|
147
|
+
* checks (doctor) validate the layout the launcher will actually execute
|
|
148
|
+
* instead of re-deriving — and risking drifting from — the same decision
|
|
149
|
+
* (#276).
|
|
150
|
+
*
|
|
151
|
+
* @param {string} packageRoot - This install's own package root (`getPackageRoot()`).
|
|
152
|
+
* @param {{devMode?: boolean, edgePath?: string}} config - Channel config (`loadConfig()`).
|
|
153
|
+
* @returns {string} The effective root whose `dist/src/...` the launcher loads from.
|
|
154
|
+
*/
|
|
155
|
+
export function resolveEffectiveInstallationRoot(packageRoot, config) {
|
|
156
|
+
if (config?.devMode && config.edgePath && config.edgePath !== packageRoot) {
|
|
157
|
+
return config.edgePath;
|
|
158
|
+
}
|
|
159
|
+
return packageRoot;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolve the absolute path to the MCP server entrypoint the production
|
|
164
|
+
* launcher actually loads: `bin/aiwg.mjs` always dispatches to the
|
|
165
|
+
* *compiled* router (`resolveRouterPath()`, never an uncompiled `src/`
|
|
166
|
+
* tree), whose MCP handler imports `../../mcp/cli.mjs` relative to its own
|
|
167
|
+
* compiled location, which in turn imports `./server.mjs` relative to
|
|
168
|
+
* itself — i.e. always `<effective root>/dist/src/mcp/server.mjs`, in every
|
|
169
|
+
* layout (npm install, source checkout, dev-mode edge checkout). There is
|
|
170
|
+
* no runtime path that falls back to `src/mcp/server.mjs` directly, so a
|
|
171
|
+
* health check that also accepts that source path can report OK for a
|
|
172
|
+
* layout where `aiwg mcp serve` will actually fail (#276).
|
|
173
|
+
*
|
|
174
|
+
* @param {string} packageRoot - This install's own package root (`getPackageRoot()`).
|
|
175
|
+
* @param {{devMode?: boolean, edgePath?: string}} config - Channel config (`loadConfig()`).
|
|
176
|
+
* @returns {string} Absolute path to the compiled MCP server entrypoint.
|
|
177
|
+
*/
|
|
178
|
+
export function resolveMcpServerEntrypointPath(packageRoot, config) {
|
|
179
|
+
const root = resolveEffectiveInstallationRoot(packageRoot, config);
|
|
180
|
+
return path.join(root, 'dist', 'src', 'mcp', 'server.mjs');
|
|
181
|
+
}
|
|
182
|
+
|
|
138
183
|
/**
|
|
139
184
|
* Load channel configuration
|
|
140
185
|
* @returns {Promise<object>} Channel configuration
|
|
@@ -237,6 +282,9 @@ export async function getFrameworkRoot(options = {}) {
|
|
|
237
282
|
*/
|
|
238
283
|
export async function switchToEdge() {
|
|
239
284
|
const config = await loadConfig();
|
|
285
|
+
if (typeof config.edgePath !== 'string' || !path.isAbsolute(config.edgePath)) {
|
|
286
|
+
throw new Error('Edge installation path must be absolute');
|
|
287
|
+
}
|
|
240
288
|
|
|
241
289
|
console.log('Switching to edge channel (bleeding edge from main branch)...');
|
|
242
290
|
console.log('');
|
|
@@ -256,8 +304,8 @@ export async function switchToEdge() {
|
|
|
256
304
|
const sparseConfig = path.join(config.edgePath, '.git', 'info', 'sparse-checkout');
|
|
257
305
|
const hasSparse = await fs.access(sparseConfig).then(() => true).catch(() => false);
|
|
258
306
|
if (!hasSparse) {
|
|
259
|
-
|
|
260
|
-
|
|
307
|
+
boundedExecFileSync('git', ['sparse-checkout', 'init', '--cone'], 'quick', { cwd: config.edgePath, stdio: 'pipe' });
|
|
308
|
+
boundedExecFileSync('git', ['sparse-checkout', 'set', '--no-cone', '/*', '!/.aiwg'], 'quick', { cwd: config.edgePath, stdio: 'pipe' });
|
|
261
309
|
}
|
|
262
310
|
} catch {
|
|
263
311
|
// Sparse checkout is optional — older git may not support it
|
|
@@ -275,11 +323,12 @@ export async function switchToEdge() {
|
|
|
275
323
|
console.log(`Cloning repository to ${config.edgePath}...`);
|
|
276
324
|
await fs.mkdir(path.dirname(config.edgePath), { recursive: true });
|
|
277
325
|
try {
|
|
278
|
-
|
|
326
|
+
// Cloning the full repository; long tier (#3062).
|
|
327
|
+
boundedExecFileSync('git', ['clone', '--branch', 'main', '--', REPO_URL, config.edgePath], 'long', { stdio: 'inherit' });
|
|
279
328
|
// Exclude dogfooding artifacts from edge clones — users want framework source only
|
|
280
329
|
try {
|
|
281
|
-
|
|
282
|
-
|
|
330
|
+
boundedExecFileSync('git', ['sparse-checkout', 'init', '--cone'], 'quick', { cwd: config.edgePath, stdio: 'pipe' });
|
|
331
|
+
boundedExecFileSync('git', ['sparse-checkout', 'set', '--no-cone', '/*', '!/.aiwg'], 'quick', { cwd: config.edgePath, stdio: 'pipe' });
|
|
283
332
|
} catch {
|
|
284
333
|
// Sparse checkout is optional — older git versions may not support it
|
|
285
334
|
console.log('Note: sparse checkout not available, .aiwg/ will be present in edge install.');
|
|
@@ -375,7 +424,7 @@ export async function switchToNext() {
|
|
|
375
424
|
console.log('');
|
|
376
425
|
|
|
377
426
|
try {
|
|
378
|
-
|
|
427
|
+
boundedExecFileSync(status.identity.managerExecutable, ['install', '--global', 'aiwg@next'], 'long', { stdio: 'inherit' });
|
|
379
428
|
} catch (error) {
|
|
380
429
|
console.error('Failed to install aiwg@next:', error.message);
|
|
381
430
|
console.error('Check that npm is available and you have write access to the global prefix.');
|
|
@@ -409,7 +458,7 @@ export async function switchToNightly() {
|
|
|
409
458
|
console.log('');
|
|
410
459
|
|
|
411
460
|
try {
|
|
412
|
-
|
|
461
|
+
boundedExecFileSync(status.identity.managerExecutable, ['install', '--global', 'aiwg@nightly'], 'long', { stdio: 'inherit' });
|
|
413
462
|
} catch (error) {
|
|
414
463
|
console.error('Failed to install aiwg@nightly:', error.message);
|
|
415
464
|
console.error('Check that npm is available and you have write access to the global prefix.');
|
|
@@ -526,11 +575,11 @@ export async function getVersionInfo() {
|
|
|
526
575
|
if (config.channel === 'edge') {
|
|
527
576
|
// Get git info for edge channel
|
|
528
577
|
try {
|
|
529
|
-
const gitHash =
|
|
578
|
+
const gitHash = boundedExecFileSync('git', ['rev-parse', '--short', 'HEAD'], 'quick', {
|
|
530
579
|
cwd: config.edgePath,
|
|
531
580
|
encoding: 'utf8',
|
|
532
581
|
}).trim();
|
|
533
|
-
const gitBranch =
|
|
582
|
+
const gitBranch = boundedExecFileSync('git', ['branch', '--show-current'], 'quick', {
|
|
534
583
|
cwd: config.edgePath,
|
|
535
584
|
encoding: 'utf8',
|
|
536
585
|
}).trim();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared `spawnSync`/`boundedSpawnSync` result-to-exit-code mapping (#3062
|
|
3
|
+
* review item 7).
|
|
4
|
+
*
|
|
5
|
+
* `SpawnSyncReturns.status` is `null` on spawn failure, timeout, or signal
|
|
6
|
+
* termination — not `0`. Several call sites derived a CLI exit code with
|
|
7
|
+
* `result.status ?? 0`, which reports *success* for exactly those failure
|
|
8
|
+
* cases. This helper never does that: a `status` of `null` defaults to a
|
|
9
|
+
* non-zero exit, and when the bounded helper's `error`/`signal` fields are
|
|
10
|
+
* present, their message (already enriched with the tier/bound/override
|
|
11
|
+
* guidance for a timeout) is surfaced instead of being silently dropped.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately lives under `src/cli/` rather than `src/process/` so it
|
|
14
|
+
* composes with, but does not modify, the bounded helper itself.
|
|
15
|
+
*
|
|
16
|
+
* @module src/cli/child-process-result
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Map one spawn result to `{ exitCode, message? }`.
|
|
20
|
+
*
|
|
21
|
+
* - `result.error` set (spawn failure, or a bounded-helper timeout with its
|
|
22
|
+
* enriched message) → non-zero, `message` is `error.message` (optionally
|
|
23
|
+
* prefixed with `context`).
|
|
24
|
+
* - No `error`, but `result.signal` set (child killed by a signal, with no
|
|
25
|
+
* separate error object) → non-zero, `message` names the signal.
|
|
26
|
+
* - Otherwise → `result.status ?? 1` (a `null`/`undefined` status with
|
|
27
|
+
* neither `error` nor `signal` set is still treated as a failure, never
|
|
28
|
+
* as success).
|
|
29
|
+
*/
|
|
30
|
+
export function exitCodeForSpawnResult(result, context) {
|
|
31
|
+
const prefix = context ? `${context}: ` : '';
|
|
32
|
+
if (result.error) {
|
|
33
|
+
return { exitCode: 1, message: `${prefix}${result.error.message}` };
|
|
34
|
+
}
|
|
35
|
+
if (result.signal) {
|
|
36
|
+
return { exitCode: 1, message: `${prefix}terminated by signal ${result.signal}` };
|
|
37
|
+
}
|
|
38
|
+
return { exitCode: result.status ?? 1 };
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=child-process-result.js.map
|