@aiwg/cli 2026.10.6 → 2026.10.7

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.
Files changed (84) hide show
  1. package/bin/aiwg.mjs +4 -2
  2. package/dist/src/a2a/agent-card.js +11 -0
  3. package/dist/src/api/index.d.ts +1 -0
  4. package/dist/src/api/index.js +1 -0
  5. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
  6. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
  7. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
  8. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
  9. package/dist/src/artifacts/fortemi-shard-export.js +284 -0
  10. package/dist/src/artifacts/index-preparation.js +2 -0
  11. package/dist/src/auth/client.js +5 -1
  12. package/dist/src/auth/credential-store.js +5 -1
  13. package/dist/src/auth/sensitive-input.js +151 -0
  14. package/dist/src/channel/manager.mjs +3 -0
  15. package/dist/src/cli/handlers/agentcard.js +47 -25
  16. package/dist/src/cli/handlers/auth.js +22 -2
  17. package/dist/src/cli/handlers/decision.js +3 -2
  18. package/dist/src/cli/handlers/effect.js +4 -84
  19. package/dist/src/cli/handlers/guided-setup.js +1 -1
  20. package/dist/src/cli/handlers/init.js +1 -1
  21. package/dist/src/cli/handlers/install.js +6 -0
  22. package/dist/src/cli/handlers/marketplace.js +15 -4
  23. package/dist/src/cli/handlers/setup.js +24 -20
  24. package/dist/src/cli/handlers/use.js +57 -15
  25. package/dist/src/cli/prompt-utils.js +73 -0
  26. package/dist/src/config/gitignore.js +4 -0
  27. package/dist/src/config/workspace.js +2 -1
  28. package/dist/src/decision/adapters/jev.js +20 -31
  29. package/dist/src/decision/ask.js +6 -1
  30. package/dist/src/decision/benchmark.js +19 -6
  31. package/dist/src/decision/qualification/gate-evidence.js +6 -4
  32. package/dist/src/decision/qualification/load-staged.js +47 -10
  33. package/dist/src/decision/qualification/runner.js +7 -3
  34. package/dist/src/discovery/interview.js +10 -5
  35. package/dist/src/discovery/plan-apply.js +26 -30
  36. package/dist/src/discovery/plan-validation.js +2 -2
  37. package/dist/src/effects/host.js +91 -0
  38. package/dist/src/extensions/commands/definitions.js +2 -2
  39. package/dist/src/extensions/project-local-gitignore.js +2 -0
  40. package/dist/src/issues/cli.js +16 -5
  41. package/dist/src/issues/live.js +44 -12
  42. package/dist/src/marketplace/exchange.js +18 -6
  43. package/dist/src/marketplace/provenance.js +3 -2
  44. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  45. package/dist/src/mcp/cli.mjs +17 -90
  46. package/dist/src/mcp/grok-build-config.mjs +2 -1
  47. package/dist/src/mcp/muse-native-config.mjs +2 -1
  48. package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
  49. package/dist/src/memory/project-registry.js +2 -1
  50. package/dist/src/mission-protocol/worker-status.js +91 -1
  51. package/dist/src/models/worker-installation.d.mts +1 -1
  52. package/dist/src/models/worker-installation.mjs +65 -4
  53. package/dist/src/packages/adapters/git.js +99 -23
  54. package/dist/src/packages/adapters/gitea.js +8 -27
  55. package/dist/src/packages/adapters/github.js +7 -25
  56. package/dist/src/packages/registry.js +4 -4
  57. package/dist/src/process/cli-command.d.mts +5 -0
  58. package/dist/src/process/cli-command.mjs +42 -0
  59. package/dist/src/serve/worker-invocation.js +10 -0
  60. package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
  61. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  62. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  63. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  64. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  65. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  66. package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
  67. package/dist/src/tracker/project-inputs.js +2 -0
  68. package/dist/src/update/notifier.mjs +2 -4
  69. package/package.json +1 -1
  70. package/tools/agents/providers/base.mjs +5 -1
  71. package/tools/agents/providers/claude.mjs +5 -2
  72. package/tools/agents/providers/codex.mjs +4 -1
  73. package/tools/agents/providers/context-write.mjs +28 -0
  74. package/tools/agents/providers/copilot.mjs +5 -2
  75. package/tools/agents/providers/hermes.mjs +5 -2
  76. package/tools/agents/providers/omp.mjs +3 -1
  77. package/tools/agents/providers/rule-delivery.mjs +29 -12
  78. package/tools/agents/providers/warp.mjs +4 -1
  79. package/tools/agents/providers/windsurf.mjs +6 -3
  80. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  81. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  82. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  83. package/tools/ralph-external/process-monitor.mjs +42 -4
  84. package/tools/ralph-external/session-launcher.mjs +125 -3
@@ -14,11 +14,13 @@
14
14
  *
15
15
  * @implements #892
16
16
  */
17
+ import { readPrivateValueFile } from '../../auth/sensitive-input.js';
17
18
  import { signalChild } from '../../process/signal-child.mjs';
18
- import { readFile, mkdir, symlink, access, readdir, chmod, lstat, open } from 'fs/promises';
19
+ import { helperEnvironment, HELPER_ENV_ALLOWLIST, DESKTOP_HELPER_ENV_ALLOWLIST } from '../../process/cli-command.mjs';
20
+ import { readFile, mkdir, symlink, access, readdir, chmod } from 'fs/promises';
19
21
  import { join } from 'path';
20
22
  import { homedir } from 'os';
21
- import { existsSync, constants } from 'fs';
23
+ import { existsSync } from 'fs';
22
24
  import { spawn, spawnSync } from 'child_process';
23
25
  import { boundedSpawnSync, resolveSyncProcessTimeoutMs } from '../../process/bounded-child-process.mjs';
24
26
  import { isInteractive } from '../../cli/env.js';
@@ -228,73 +230,9 @@ function envWithoutSecretValue(base, envVarName, secretValue) {
228
230
  }
229
231
  return result;
230
232
  }
231
- /**
232
- * Read an API key from a private file (#3062 go/no-go review, blocker 1C):
233
- * `--api-key-file <path>` reads the key from disk instead of an
234
- * environment variable, so a file that does not meet the same bar the
235
- * decision ledger's own file writer enforces
236
- * (`src/decision/ledger.ts`'s `openLedgerFileForAppend`) is refused rather
237
- * than silently trusted. This mirrors that check for a read instead of a
238
- * write — there is no already-exported helper at this layer to reuse (the
239
- * ledger's checks are private to that module and tied to its own
240
- * create-if-missing write path), so this re-implements the same checks for
241
- * a read: never follow a symlink; the open file descriptor must resolve to
242
- * a regular file with exactly one hard link; and, on POSIX, must be owned
243
- * by the current user with no group/other permission bits. The file's
244
- * contents are returned trimmed; no error message here ever includes them.
245
- */
233
+ /** Backward-compatible MCP export; the validation is shared by all input commands. */
246
234
  async function readApiKeyFromFile(filePath) {
247
- try {
248
- const pre = await lstat(filePath);
249
- if (pre.isSymbolicLink()) {
250
- throw new Error(`--api-key-file: "${filePath}" is a symlink; refusing to follow it.`);
251
- }
252
- }
253
- catch (error) {
254
- if (error.code === 'ENOENT') {
255
- throw new Error(`--api-key-file: "${filePath}" does not exist.`);
256
- }
257
- if (error instanceof Error && error.message.startsWith('--api-key-file:'))
258
- throw error;
259
- throw new Error(`--api-key-file: cannot stat "${filePath}": ${error.message}`);
260
- }
261
- const hasNoFollow = typeof constants.O_NOFOLLOW === 'number';
262
- const flags = constants.O_RDONLY | (hasNoFollow ? constants.O_NOFOLLOW : 0);
263
- let handle;
264
- try {
265
- handle = await open(filePath, flags);
266
- }
267
- catch (error) {
268
- const code = error.code;
269
- if (code === 'ELOOP')
270
- throw new Error(`--api-key-file: "${filePath}" is a symlink; refusing to follow it.`);
271
- if (code === 'ENOENT')
272
- throw new Error(`--api-key-file: "${filePath}" does not exist.`);
273
- throw new Error(`--api-key-file: cannot open "${filePath}": ${error.message}`);
274
- }
275
- try {
276
- const stat = await handle.stat();
277
- if (!stat.isFile()) {
278
- throw new Error(`--api-key-file: "${filePath}" is not a regular file.`);
279
- }
280
- if (stat.nlink !== 1) {
281
- throw new Error(`--api-key-file: "${filePath}" has more than one hard link; refusing to read it.`);
282
- }
283
- if (process.platform !== 'win32') {
284
- const uid = typeof process.getuid === 'function' ? process.getuid() : null;
285
- if (uid !== null && stat.uid !== uid) {
286
- throw new Error(`--api-key-file: "${filePath}" is not owned by the current user.`);
287
- }
288
- if ((stat.mode & 0o077) !== 0) {
289
- throw new Error(`--api-key-file: "${filePath}" must not be readable or writable by group or other ` +
290
- '(chmod 600 it).');
291
- }
292
- }
293
- return (await handle.readFile({ encoding: 'utf-8' })).trim();
294
- }
295
- finally {
296
- await handle.close();
297
- }
235
+ return readPrivateValueFile(filePath, '--api-key-file');
298
236
  }
299
237
  /**
300
238
  * Async, credential-safe launch of `codex login --with-api-key` (#3062
@@ -320,17 +258,37 @@ function spawnCodexLoginWithApiKey(apiKey, env) {
320
258
  return new Promise((resolve) => {
321
259
  const child = spawn('codex', ['login', '--with-api-key'], {
322
260
  env,
323
- stdio: ['pipe', 'inherit', 'inherit'],
261
+ stdio: ['pipe', 'pipe', 'pipe'],
324
262
  });
325
263
  let settled = false;
326
264
  let timer;
265
+ const stdout = [];
266
+ const stderr = [];
267
+ let outputBytes = 0;
268
+ // Buffer through close so a credential split between chunks cannot escape
269
+ // redaction. The combined cap bounds both memory and forwarded diagnostics.
270
+ const collect = (target, chunk) => {
271
+ if (settled)
272
+ return;
273
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
274
+ outputBytes += buffer.length;
275
+ if (outputBytes > 1024 * 1024) {
276
+ settle({ status: null, signal: 'SIGKILL', error: new Error('codex login output exceeded the 1 MiB limit.') });
277
+ return;
278
+ }
279
+ target.push(buffer);
280
+ };
281
+ const onStdout = (chunk) => collect(stdout, chunk);
282
+ const onStderr = (chunk) => collect(stderr, chunk);
283
+ child.stdout?.on('data', onStdout);
284
+ child.stderr?.on('data', onStderr);
327
285
  // #3062 go/no-go review, blocker 2: stdin error, write completion,
328
- // child 'error', child 'exit', and the deadline all funnel through this
286
+ // child 'error', child 'close', and the deadline all funnel through this
329
287
  // one function, which settles exactly once, always clears the timer and
330
288
  // removes every listener this call added, and kills the child if it is
331
289
  // still alive on a failing settlement (a running process on the
332
290
  // timeout path; a process that may still be running on an unexpected
333
- // stdin-write error). The 'exit' path never reaches the kill — by the
291
+ // stdin-write error). The 'close' path never reaches the kill — by the
334
292
  // time that event fires the child has already exited.
335
293
  const settle = (result) => {
336
294
  if (settled)
@@ -340,13 +298,23 @@ function spawnCodexLoginWithApiKey(apiKey, env) {
340
298
  clearTimeout(timer);
341
299
  child.stdin?.removeListener('error', onStdinError);
342
300
  child.removeListener('error', onChildError);
343
- child.removeListener('exit', onExit);
301
+ child.removeListener('close', onClose);
302
+ child.stdout?.removeListener('data', onStdout);
303
+ child.stderr?.removeListener('data', onStderr);
304
+ child.stdout?.destroy();
305
+ child.stderr?.destroy();
344
306
  if (result.error && child.exitCode === null && child.signalCode === null) {
345
307
  try {
346
308
  signalChild(child, 'SIGKILL');
347
309
  }
348
310
  catch { /* already gone */ }
349
311
  }
312
+ // On a failed/oversized stream, suppress buffered fragments entirely:
313
+ // the missing remainder might contain the rest of a credential.
314
+ if (!result.error) {
315
+ process.stdout.write(Buffer.concat(stdout).toString('utf8').split(apiKey).join('[redacted]'));
316
+ process.stderr.write(Buffer.concat(stderr).toString('utf8').split(apiKey).join('[redacted]'));
317
+ }
350
318
  resolve(result);
351
319
  };
352
320
  timer = setTimeout(() => {
@@ -358,15 +326,15 @@ function spawnCodexLoginWithApiKey(apiKey, env) {
358
326
  }, boundMs);
359
327
  const onChildError = (err) => settle({ status: null, signal: null, error: err });
360
328
  child.on('error', onChildError);
361
- const onExit = (code, signal) => settle({ status: code, signal });
362
- child.on('exit', onExit);
329
+ const onClose = (code, signal) => settle({ status: code, signal });
330
+ child.on('close', onClose);
363
331
  // A child that exits (or closes its read end) before reading stdin
364
332
  // raises EPIPE/ECONNRESET on the *stdin stream*, not on the child
365
333
  // object — `child.on('error', ...)` above never sees it, so without
366
334
  // this listener it is an unhandled 'error' event, which Node turns into
367
335
  // an uncaught exception. Installed before the write/end below, per the
368
336
  // review. EPIPE/ECONNRESET specifically mean "the child already went
369
- // away"; the already-firing (or about-to-fire) 'exit' event carries the
337
+ // away"; the already-firing (or about-to-fire) 'close' event carries the
370
338
  // real, non-secret result, so this swallows exactly those two codes and
371
339
  // lets settle() happen there instead. Anything else is a genuine,
372
340
  // unexpected stdin failure and is itself the settling error.
@@ -413,7 +381,9 @@ export async function loginInProfile(profile, options = {}) {
413
381
  await ensureRuntimeHome(profile);
414
382
  const rtHome = runtimeHomePath(profile);
415
383
  const env = {
416
- ...process.env,
384
+ ...helperEnvironment(process.env, [
385
+ ...HELPER_ENV_ALLOWLIST, ...DESKTOP_HELPER_ENV_ALLOWLIST, 'NODE_EXTRA_CA_CERTS',
386
+ ]),
417
387
  HOME: rtHome,
418
388
  CODEX_HOME: rtHome,
419
389
  AIWG_CONFIG: resolveUserConfigDir(),
@@ -1124,7 +1124,7 @@ Usage:
1124
1124
  aiwg mcp profile import <file>
1125
1125
  aiwg mcp profile export <name> [--out <file>]
1126
1126
  aiwg mcp profile init-presets
1127
- aiwg mcp profile login <name> [--api-key-stdin | --api-key-file <path> | --api-key-env <ENV_VAR_NAME>]
1127
+ aiwg mcp profile login <name> [--api-key-interactive | --api-key-stdin | --api-key-file <path> | --api-key-store <name> | --api-key-env <ENV_VAR_NAME>]
1128
1128
 
1129
1129
  Profiles let you define named subsets of your registered MCP servers:
1130
1130
  aiwg mcp profile add dev --servers git-gitea,memory-fortemi --description "Dev work"
@@ -1162,10 +1162,10 @@ Logging in to a Codex profile's isolated runtime home (#3062):
1162
1162
  $OPENAI_KEY; visible to every other
1163
1163
  child process this shell starts
1164
1164
  In every case the key reaches codex over stdin only; it never appears on
1165
- argv, in logs, or in error messages. Exactly one of the three may be
1166
- given. Without a terminal and without any of the three, this errors
1167
- naming all three. See "Supplying credentials safely" in the docs:
1168
- docs/mcp/codex-profiles.md#supplying-credentials-safely
1165
+ argv, in logs, or in error messages. Use --api-key-store <name> for a named
1166
+ OS-store entry, or --api-key-interactive for hidden terminal entry. Give
1167
+ sources in preference order; a missing source without a terminal errors naming the options.
1168
+ See docs/cli/sensitive-values.md for exposure details and manager examples.
1169
1169
  `);
1170
1170
  }
1171
1171
 
@@ -1419,59 +1419,18 @@ async function handleProfileInitPresets() {
1419
1419
  }
1420
1420
  }
1421
1421
 
1422
- /**
1423
- * Read all of stdin and return it as one trimmed string (go/no-go review,
1424
- * blocker 1C's `--api-key-stdin`). Mirrors `src/cli/handlers/decision.ts`'s
1425
- * own private `readStdin()` + `.trim()` for `--token-stdin` — duplicated
1426
- * rather than imported for the same source/dist reason documented on
1427
- * `requireFlagValue` above (this file runs both compiled and directly from
1428
- * source). Input is expected to be a single line; a value containing
1429
- * additional lines is not specially rejected, matching that precedent.
1430
- */
1431
- async function readStdinAll() {
1432
- const chunks = [];
1433
- for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
1434
- return Buffer.concat(chunks).toString('utf8');
1435
- }
1436
-
1437
- /**
1438
- * Handle `aiwg mcp profile login <name> [--api-key-env <ENV_VAR_NAME> |
1439
- * --api-key-stdin | --api-key-file <path>]` (#3062, #3069, go/no-go review
1440
- * blocker 1C).
1441
- *
1442
- * Wires `loginInProfile()` (src/mcp/adapters/codex-runtime.ts), which
1443
- * already implements the full headless-safe behavior this command needs:
1444
- * with a terminal attached and none of the three given, it runs the
1445
- * interactive `codex login` OAuth flow. Exactly one credential source may
1446
- * be given; more than one is a usage error naming the flags given. In
1447
- * every case the key reaches `loginInProfile()` only as an in-memory value
1448
- * (or, for `--api-key-env`, a variable *name*) — never on argv, in logs, or
1449
- * in an error message — and travels to `codex` only on its stdin (see
1450
- * `spawnCodexLoginWithApiKey()`'s own doc comment). Without a terminal and
1451
- * without any of the three, `loginInProfile()` itself refuses, naming all
1452
- * three flags.
1453
- *
1454
- * `--api-key-env` reads the key from that named environment variable, as
1455
- * before. `--api-key-stdin` reads one line from this process's own stdin
1456
- * (refusing if stdin is a terminal, or if it is empty) — the least
1457
- * observable source, since the key then never exists in any environment or
1458
- * on any command line. `--api-key-file <path>` reads the key from a file
1459
- * that `readApiKeyFromFile()` requires to be a non-symlink, single-hard-
1460
- * link, regular file owned by the current user with no group/other
1461
- * permission bits on POSIX — refused otherwise, without ever printing the
1462
- * path's contents. `--api-key-env` remains the least preferred of the
1463
- * three (see "Supplying credentials safely" in the docs): the named
1464
- * variable is visible to every other child process this shell starts.
1465
- */
1422
+ /** Standard sensitive input; the Codex adapter still receives only an in-memory key. */
1466
1423
  async function handleProfileLogin(args) {
1467
1424
  let apiKeyEnv;
1468
1425
  let apiKeyFilePath;
1426
+ let apiKeyStore;
1469
1427
  try {
1470
1428
  // rejectEmpty: an explicitly-given-but-empty variable name/path can
1471
1429
  // never name a real environment variable or file (#3062 second review,
1472
1430
  // finding 4; go/no-go review blocker 1C).
1473
1431
  apiKeyEnv = requireFlagValue(args, { rejectEmpty: true }, '--api-key-env');
1474
1432
  apiKeyFilePath = requireFlagValue(args, { rejectEmpty: true }, '--api-key-file');
1433
+ apiKeyStore = requireFlagValue(args, { rejectEmpty: true }, '--api-key-store');
1475
1434
  } catch (err) {
1476
1435
  if (err instanceof MissingFlagValueError) {
1477
1436
  console.error(`${err.message}.`);
@@ -1481,16 +1440,6 @@ async function handleProfileLogin(args) {
1481
1440
  }
1482
1441
  const useStdin = args.includes('--api-key-stdin');
1483
1442
 
1484
- const sourcesGiven = [
1485
- apiKeyEnv !== undefined ? '--api-key-env' : null,
1486
- useStdin ? '--api-key-stdin' : null,
1487
- apiKeyFilePath !== undefined ? '--api-key-file' : null,
1488
- ].filter(Boolean);
1489
- if (sourcesGiven.length > 1) {
1490
- console.error(`Only one credential source may be given; got ${sourcesGiven.join(', ')}.`);
1491
- process.exit(2);
1492
- }
1493
-
1494
1443
  // Skip --api-key-env's/--api-key-file's own value when hunting for the
1495
1444
  // positional profile name, so e.g. `login --api-key-env FOO` (missing
1496
1445
  // name) never misreads FOO as the name (mirrors this file's own
@@ -1500,46 +1449,24 @@ async function handleProfileLogin(args) {
1500
1449
  for (let i = 0; i < args.length; i += 1) {
1501
1450
  const token = args[i];
1502
1451
  if (token === '--') break;
1503
- if (token === '--api-key-env' || token === '--api-key-file') { i += 1; continue; }
1452
+ if (token === '--api-key-env' || token === '--api-key-file' || token === '--api-key-store') { i += 1; continue; }
1504
1453
  if (!token.startsWith('--')) positional.push(token);
1505
1454
  }
1506
1455
  const name = positional[0];
1507
1456
  if (!name) {
1508
- console.error('Usage: aiwg mcp profile login <name> [--api-key-env <ENV_VAR_NAME> | --api-key-stdin | --api-key-file <path>]');
1457
+ console.error('Usage: aiwg mcp profile login <name> [--api-key-interactive | --api-key-stdin | --api-key-file <path> | --api-key-store <name> | --api-key-env <ENV_VAR_NAME>]');
1509
1458
  process.exit(1);
1510
1459
  }
1511
1460
 
1512
- let apiKey;
1513
- if (useStdin) {
1514
- if (process.stdin.isTTY) {
1515
- console.error(
1516
- '--api-key-stdin requires piped input, not a terminal; run ' +
1517
- `\`aiwg mcp profile login ${name}\` with no credential flag for the interactive login instead.`,
1518
- );
1519
- process.exit(2);
1520
- }
1521
- apiKey = (await readStdinAll()).trim();
1522
- if (!apiKey) {
1523
- console.error('--api-key-stdin received no key (stdin was empty).');
1524
- process.exit(2);
1525
- }
1526
- } else if (apiKeyFilePath !== undefined) {
1527
- const { readApiKeyFromFile } = await loadCodexRuntimeAdapter();
1528
- try {
1529
- apiKey = await readApiKeyFromFile(apiKeyFilePath);
1530
- } catch (err) {
1531
- console.error(err instanceof Error ? err.message : String(err));
1532
- process.exit(2);
1533
- }
1534
- if (!apiKey) {
1535
- console.error(`--api-key-file "${apiKeyFilePath}" is empty.`);
1536
- process.exit(2);
1537
- }
1538
- }
1539
-
1540
1461
  try {
1462
+ await loadCodexRuntimeAdapter();
1463
+ const { resolveSensitiveInput } = await import(pathToFileURL(path.join(findOwnPackageRoot(), 'dist', 'src', 'auth', 'sensitive-input.js')).href);
1464
+ const apiKey = await resolveSensitiveInput({
1465
+ prefix: 'api-key', stdin: useStdin, file: apiKeyFilePath, store: apiKeyStore,
1466
+ env: apiKeyEnv, interactive: args.includes('--api-key-interactive'),
1467
+ });
1541
1468
  const { loginInProfile } = await loadCodexRuntimeAdapter();
1542
- const loginOptions = apiKey !== undefined ? { apiKey } : apiKeyEnv ? { apiKeyEnv } : {};
1469
+ const loginOptions = { apiKey };
1543
1470
  await loginInProfile(name, loginOptions);
1544
1471
  console.log(`\nLogged in to profile "${name}".`);
1545
1472
  } catch (err) {
@@ -3,6 +3,7 @@ import { constants } from 'node:fs';
3
3
  import { access, lstat, readFile } from 'node:fs/promises';
4
4
  import { dirname, relative, resolve } from 'node:path';
5
5
  import { spawnSync } from 'node:child_process';
6
+ import { helperEnvironment } from '../process/cli-command.mjs';
6
7
  import { replaceServer } from './toml-editor.mjs';
7
8
  import { tomlString as string } from './toml-string.mjs';
8
9
 
@@ -144,7 +145,7 @@ export async function unmanageGrokBuildMcp(configPath, names, options = {}) {
144
145
  }
145
146
 
146
147
  function runJson(binary, args, cwd, env) {
147
- const run = spawnSync(binary, args, { cwd, env, encoding: 'utf8', timeout: 30_000, killSignal: 'SIGKILL' });
148
+ const run = spawnSync(binary, args, { cwd, env: helperEnvironment(env), encoding: 'utf8', timeout: 30_000, killSignal: 'SIGKILL' });
148
149
  if (run.error || run.status !== 0) return { ok: false, detail: run.error?.message || run.stderr || `exit ${run.status}` };
149
150
  try { return { ok: true, value: JSON.parse(run.stdout || '{}') }; }
150
151
  catch { return { ok: false, detail: 'Grok returned malformed JSON' }; }
@@ -4,6 +4,7 @@ import { spawnSync } from 'node:child_process';
4
4
  import { dirname, join } from 'node:path';
5
5
  import os from 'node:os';
6
6
  import { resolveMuseXdgSkillsDir } from '../providers/muse-paths.js';
7
+ import { helperEnvironment } from '../process/cli-command.mjs';
7
8
 
8
9
  // Mirrors tools/agents/providers/muse-hooks.mjs. src/ cannot import tools/:
9
10
  // tools/ is not part of the compiled dist/ tree.
@@ -47,7 +48,7 @@ function deepEqual(a, b) {
47
48
 
48
49
  function inspectMuseCli(env) {
49
50
  const result = spawnSync('muse', ['--version'], {
50
- env,
51
+ env: helperEnvironment(env),
51
52
  encoding: 'utf8',
52
53
  timeout: MUSE_VERSION_TIMEOUT_MS,
53
54
  killSignal: 'SIGKILL',
@@ -2707,6 +2707,9 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
2707
2707
  }
2708
2708
 
2709
2709
  async function status(input) {
2710
+ const { projectRecoveryStatus, mergeRecoveryStatuses } = await importImpl('mission-protocol/worker-status.js');
2711
+ const recoveryStatus = breaker => projectRecoveryStatus(typeof breaker?.recoveryStatus === 'function'
2712
+ ? { ...breaker.recoveryStatus(), observedAt: now().toISOString() } : null, 'live');
2710
2713
  if (!input.invocation_id) {
2711
2714
  // #2915 round 8 (D4): ordinary status, read-only, no id — lists every
2712
2715
  // invocation this server currently tracks as a `dispatched` reservation
@@ -2725,11 +2728,19 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
2725
2728
  .filter(scope => scope.state === 'operator-intervention')
2726
2729
  .map(scope => ({ scopeId: scope.scopeKey, reason: scope.operatorReason,
2727
2730
  resetInstruction: { operation: 'reset_recovery_scope', scope_id: scope.scopeKey } })));
2728
- return { unresolvedReservations, interventionScopes };
2731
+ const recovery = mergeRecoveryStatuses([...breakers].map((breaker, index) => {
2732
+ const status = recoveryStatus(breaker);
2733
+ if (breakers.size > 1) {
2734
+ for (const scope of status.scopes) scope.owner = `owner-${index + 1}`;
2735
+ for (const transition of status.transitions) transition.owner = `owner-${index + 1}`;
2736
+ }
2737
+ return status;
2738
+ }));
2739
+ return { unresolvedReservations, interventionScopes, recovery };
2729
2740
  }
2730
2741
  const entry = registry.get(input.invocation_id);
2731
2742
  if (!entry) throw new Error('Unknown invocation_id');
2732
- return publicStatus(entry, healthHook);
2743
+ return { ...publicStatus(entry, healthHook), recovery: recoveryStatus(entry.recoveryBreaker ?? registry.recoveryOwner) };
2733
2744
  }
2734
2745
 
2735
2746
  /**
@@ -10,6 +10,7 @@ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
10
10
  import { homedir } from 'node:os';
11
11
  import path from 'node:path';
12
12
  import { boundedExecFileSync } from '../process/bounded-child-process.mjs';
13
+ import { helperEnvironment } from '../process/cli-command.mjs';
13
14
  import { projectAiwgPath, resolveProjectAiwgDir } from '../config/project-artifacts.js';
14
15
  export function projectMemoryHome() {
15
16
  const override = process.env.AIWG_PROJECT_MEMORY_HOME;
@@ -248,7 +249,7 @@ function sanitizeProjectId(id) {
248
249
  }
249
250
  function discoverGitRemotes(cwd) {
250
251
  try {
251
- const stdout = boundedExecFileSync('git', ['remote', '-v'], 'quick', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
252
+ const stdout = boundedExecFileSync('git', ['remote', '-v'], 'quick', { cwd, env: helperEnvironment(), encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
252
253
  const remotes = stdout
253
254
  .split('\n')
254
255
  .map((line) => line.trim().split(/\s+/)[1])
@@ -1,2 +1,92 @@
1
- export {};
1
+ const recoveryStates = new Set(['closed', 'open', 'half-open', 'canary-ready', 'canary', 'operator-intervention']);
2
+ const recoveryScopes = new Set(['invocation', 'model', 'installation', 'quota', 'provider', 'host']);
3
+ const recoveryReasons = new Set(['NOT_FOUND', 'SPAWN_REFUSED', 'AUTH', 'RATE_LIMIT', 'NETWORK',
4
+ 'RESOURCE', 'SANDBOX', 'MODEL', 'UNKNOWN']);
5
+ const objectRecord = (value) => value !== null
6
+ && typeof value === 'object' && !Array.isArray(value) ? value : null;
7
+ const finiteClock = (value) => typeof value === 'number' && Number.isFinite(value)
8
+ && Math.abs(value) <= Number.MAX_SAFE_INTEGER ? value : null;
9
+ /** Read-only projection of the existing breaker owner. Only fixed reason codes
10
+ * and scope identities survive; worker captures, actions and diagnostics do not.
11
+ * Clock values are converted with the owner's persisted wall-clock anchor,
12
+ * never with the reader's clock (which may belong to another process/boot). */
13
+ export function projectRecoveryStatus(value, source = 'recorded') {
14
+ const record = objectRecord(value);
15
+ const observedAtMs = finiteClock(record?.observedAtMs);
16
+ const wall = typeof record?.observedAt === 'string' ? Date.parse(record.observedAt) : NaN;
17
+ const observedAt = Number.isFinite(wall) ? new Date(wall).toISOString() : null;
18
+ const at = (clock) => {
19
+ if (clock === null || observedAtMs === null || observedAt === null)
20
+ return null;
21
+ const mapped = wall + clock - observedAtMs;
22
+ return Number.isFinite(mapped) && Math.abs(mapped) <= 8.64e15 ? new Date(mapped).toISOString() : null;
23
+ };
24
+ const identity = (item) => {
25
+ if (!recoveryScopes.has(item.scope) || typeof item.scopeKey !== 'string'
26
+ || !/^[\p{L}\p{N}_.:/@+~%\-]{1,256}$/u.test(item.scopeKey)
27
+ || !item.scopeKey.startsWith(`${String(item.scope)}:`))
28
+ return null;
29
+ return { scopeKey: item.scopeKey, scope: item.scope };
30
+ };
31
+ const reasons = (value) => Array.isArray(value)
32
+ ? [...new Set(value.slice(0, 32).filter(reason => recoveryReasons.has(reason)))] : [];
33
+ const rawScopes = Array.isArray(record?.scopes) ? record.scopes : [];
34
+ const rawTransitions = Array.isArray(record?.transitions) ? record.transitions : [];
35
+ const scopes = [];
36
+ for (const raw of rawScopes.slice(0, 32)) {
37
+ const item = objectRecord(raw), id = item && identity(item);
38
+ if (!item || !id || !recoveryStates.has(item.state))
39
+ continue;
40
+ const nextEligibleAtMs = finiteClock(item.nextEligibleAtMs);
41
+ scopes.push({ ...id, state: item.state, reasons: reasons(item.reasons),
42
+ nextEligibleAtMs, nextEligibleAt: at(nextEligibleAtMs), canaryInFlight: item.canaryInFlight === true });
43
+ }
44
+ const transitions = [];
45
+ for (const raw of rawTransitions.slice(-32)) {
46
+ const item = objectRecord(raw), id = item && identity(item);
47
+ const clock = finiteClock(item?.atMs);
48
+ if (!item || !id || clock === null || !recoveryStates.has(item.from) || !recoveryStates.has(item.to))
49
+ continue;
50
+ const nextEligibleAtMs = finiteClock(item.nextEligibleAtMs);
51
+ transitions.push({ ...id, from: item.from, to: item.to,
52
+ reasons: reasons(item.reasons), atMs: clock, at: at(clock), nextEligibleAtMs, nextEligibleAt: at(nextEligibleAtMs) });
53
+ }
54
+ const omittedScopeCount = typeof record?.omittedScopeCount === 'number'
55
+ && Number.isSafeInteger(record.omittedScopeCount) && record.omittedScopeCount >= 0 ? record.omittedScopeCount : 0;
56
+ return { source: record ? source : 'unavailable', observedAt, observedAtMs, scopes, transitions,
57
+ omittedScopeCount: omittedScopeCount + Math.max(0, rawScopes.length - 32),
58
+ truncated: record?.truncated === true || omittedScopeCount > 0 || rawScopes.length > 32 || rawTransitions.length > 32 };
59
+ }
60
+ /** Merge independently owned observations without letting one owner's close
61
+ * hide another owner's active scope. Callers first select the latest record
62
+ * for each owner; owner labels are supplied from validated launcher identity. */
63
+ export function mergeRecoveryStatuses(statuses) {
64
+ const available = statuses.filter(status => status.source !== 'unavailable');
65
+ const latest = available.reduce((last, status) => !last
66
+ || Date.parse(status.observedAt ?? '') >= Date.parse(last.observedAt ?? '') ? status : last, null);
67
+ if (!latest)
68
+ return projectRecoveryStatus(null);
69
+ const allScopes = available.flatMap(status => status.scopes);
70
+ const scopes = allScopes.filter(scope => scope.state !== 'closed').concat(allScopes.filter(scope => scope.state === 'closed'));
71
+ const transitions = available.flatMap(status => status.transitions).sort((a, b) => (a.at ? Date.parse(a.at) : a.atMs) - (b.at ? Date.parse(b.at) : b.atMs));
72
+ const omittedScopeCount = available.reduce((count, status) => count + status.omittedScopeCount, 0) + Math.max(0, scopes.length - 32);
73
+ return { source: latest.source, observedAt: latest.observedAt, observedAtMs: latest.observedAtMs,
74
+ scopes: scopes.slice(0, 32), transitions: transitions.slice(-32), omittedScopeCount,
75
+ truncated: available.some(status => status.truncated) || omittedScopeCount > 0 || transitions.length > 32 };
76
+ }
77
+ /** Mechanical correction records carry fixed codes and invocation identities. */
78
+ export function projectStartupCorrection(value) {
79
+ const record = objectRecord(value);
80
+ const safeId = (value) => typeof value === 'string'
81
+ && /^[\p{L}\p{N}_.:/@+~%\-]{1,256}$/u.test(value);
82
+ if (!record || record.action !== 'refresh-executable-binding' || record.reasonCode !== 'NOT_FOUND'
83
+ || !['applied', 'stopped'].includes(String(record.state)) || !safeId(record.previousInvocationId))
84
+ return null;
85
+ const stopCodes = new Set(['ATTEMPT_LIMIT', 'CANCELLED', 'DEADLINE', 'UNCHANGED_EXECUTABLE', 'QUALIFICATION_REFUSED',
86
+ 'BINDING_CHANGED', 'RETRY_REFUSED', 'RETRY_FAILED']);
87
+ return { action: 'refresh-executable-binding', reasonCode: 'NOT_FOUND', state: record.state,
88
+ previousInvocationId: record.previousInvocationId,
89
+ ...(typeof record.stopCode === 'string' && stopCodes.has(record.stopCode) ? { stopCode: record.stopCode } : {}),
90
+ ...(safeId(record.retryInvocationId) ? { retryInvocationId: record.retryInvocationId } : {}) };
91
+ }
2
92
  //# sourceMappingURL=worker-status.js.map
@@ -11,7 +11,7 @@ export function loadRecordedWorkerInstallations(input: {
11
11
  storeDigest?: string;
12
12
  /** Request validation after store presence, before recorded policy/adapter checks. */
13
13
  precheckPolicy?: () => void;
14
- }): Promise<{ registry: ProviderModelCapabilityRegistry; installations: Record<string, WorkerResolutionBinding>; drivers: Record<string, WorkerInvocationDriver>; observations: Record<string, import('./model-discovery.js').ProviderModelDiscovery> }>;
14
+ }): Promise<{ registry: ProviderModelCapabilityRegistry; installations: Record<string, WorkerResolutionBinding>; drivers: Record<string, WorkerInvocationDriver>; observations: Record<string, import('./model-discovery.js').ProviderModelDiscovery>; qualificationFailures?: Record<string, string> }>;
15
15
 
16
16
  export function hasProvider(name: string): boolean;
17
17