@aiwg/cli 2026.10.1 → 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.
Files changed (166) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +270 -0
  2. package/agentic/code/providers/model-capabilities.v1.json +3091 -24
  3. package/bin/aiwg.mjs +86 -21
  4. package/dist/src/a2a/codecs.js +9 -0
  5. package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
  6. package/dist/src/artifacts/repair.js +9 -4
  7. package/dist/src/channel/manager.mjs +59 -10
  8. package/dist/src/cli/child-process-result.js +40 -0
  9. package/dist/src/cli/env.js +38 -16
  10. package/dist/src/cli/flag-parsing.js +236 -0
  11. package/dist/src/cli/handlers/decision.js +210 -4
  12. package/dist/src/cli/handlers/diagnose.js +9 -2
  13. package/dist/src/cli/handlers/feedback.js +14 -5
  14. package/dist/src/cli/handlers/init.js +245 -30
  15. package/dist/src/cli/handlers/mc.js +23 -0
  16. package/dist/src/cli/handlers/models.js +69 -6
  17. package/dist/src/cli/handlers/ralph.js +3 -0
  18. package/dist/src/cli/handlers/refresh.js +16 -0
  19. package/dist/src/cli/handlers/regenerate.js +14 -0
  20. package/dist/src/cli/handlers/runtime-info.js +24 -9
  21. package/dist/src/cli/handlers/script-runner.js +16 -6
  22. package/dist/src/cli/handlers/session.js +396 -62
  23. package/dist/src/cli/handlers/sessions.js +146 -40
  24. package/dist/src/cli/handlers/setup-manifest.js +439 -39
  25. package/dist/src/cli/handlers/setup.js +139 -39
  26. package/dist/src/cli/handlers/subcommands.js +3 -1
  27. package/dist/src/cli/handlers/team.js +18 -8
  28. package/dist/src/cli/handlers/use.js +13 -0
  29. package/dist/src/cli/handlers/utilities.js +106 -23
  30. package/dist/src/cli/prompt-utils.js +72 -0
  31. package/dist/src/cli/provider-resolution.js +11 -0
  32. package/dist/src/config/aiwg-config.js +268 -4
  33. package/dist/src/config/cli.js +146 -19
  34. package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
  35. package/dist/src/config/project-artifacts-health.mjs +14 -3
  36. package/dist/src/config/user-config.js +36 -10
  37. package/dist/src/decision/adapters/jev.js +75 -27
  38. package/dist/src/decision/driver.js +3 -1
  39. package/dist/src/decision/entry.js +38 -6
  40. package/dist/src/decision/heldout/collector.js +10 -1
  41. package/dist/src/decision/index.js +1 -0
  42. package/dist/src/decision/interop.js +60 -8
  43. package/dist/src/decision/ledger.js +839 -0
  44. package/dist/src/decision/validate.js +6 -3
  45. package/dist/src/effects/dispatch-verifier.js +252 -0
  46. package/dist/src/effects/index.js +1 -0
  47. package/dist/src/features/installer.js +2 -2
  48. package/dist/src/flow/graph-metadata.js +5 -0
  49. package/dist/src/gates/providers/loader.js +3 -1
  50. package/dist/src/installation/manager-command.mjs +1 -1
  51. package/dist/src/installation/manager.mjs +17 -2
  52. package/dist/src/lint/runner.js +2 -2
  53. package/dist/src/mcp/adapters/codex-runtime.js +360 -56
  54. package/dist/src/mcp/cli.mjs +587 -77
  55. package/dist/src/mcp/config-file.mjs +279 -0
  56. package/dist/src/mcp/config-layers.mjs +348 -0
  57. package/dist/src/mcp/credentials.mjs +484 -0
  58. package/dist/src/mcp/grok-build-config.mjs +16 -26
  59. package/dist/src/mcp/helpers.mjs +106 -2
  60. package/dist/src/mcp/omp-config.mjs +50 -29
  61. package/dist/src/mcp/profiles.js +97 -23
  62. package/dist/src/mcp/profiles.mjs +99 -27
  63. package/dist/src/mcp/registry.js +264 -57
  64. package/dist/src/mcp/registry.mjs +198 -45
  65. package/dist/src/mcp/server.mjs +403 -20
  66. package/dist/src/mcp/toml-string.mjs +9 -0
  67. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  68. package/dist/src/mcp/tool-filters.mjs +357 -0
  69. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  70. package/dist/src/mcp/tools/decision.mjs +21 -3
  71. package/dist/src/mcp/tools/orchestration.mjs +38 -0
  72. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  73. package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
  74. package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
  75. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  76. package/dist/src/memory/project-registry.js +2 -2
  77. package/dist/src/mission-protocol/codecs.js +639 -1
  78. package/dist/src/mission-protocol/types.js +110 -0
  79. package/dist/src/mission-protocol/worker-status.js +2 -0
  80. package/dist/src/models/config-loader.js +70 -13
  81. package/dist/src/models/grok-build-models.js +93 -41
  82. package/dist/src/models/model-capabilities.v1.json +3091 -24
  83. package/dist/src/models/model-discovery.js +862 -63
  84. package/dist/src/models/provider-policy.js +625 -11
  85. package/dist/src/models/qualification-digest.mjs +17 -0
  86. package/dist/src/models/resolver.js +259 -0
  87. package/dist/src/models/worker-direct-readiness.js +529 -0
  88. package/dist/src/models/worker-dispatch-offer.js +169 -0
  89. package/dist/src/models/worker-health-readiness.js +131 -0
  90. package/dist/src/models/worker-installation.mjs +879 -0
  91. package/dist/src/models/wrapper-route.js +18 -0
  92. package/dist/src/ops/registry.js +9 -14
  93. package/dist/src/plugins/standalone-packager.js +4 -3
  94. package/dist/src/process/bounded-child-process.mjs +387 -0
  95. package/dist/src/process/cli-command.mjs +75 -0
  96. package/dist/src/providers/capability-matrix.js +8 -0
  97. package/dist/src/providers/capability-matrix.yaml +270 -0
  98. package/dist/src/providers/grok-build-headless.js +43 -21
  99. package/dist/src/providers/provider-definitions.js +9 -1
  100. package/dist/src/providers/provider-definitions.mjs +9 -2
  101. package/dist/src/serve/executor-registry.js +16 -24
  102. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  103. package/dist/src/serve/mission-conductor.js +9 -0
  104. package/dist/src/serve/shared-host-scheduler.js +2 -0
  105. package/dist/src/serve/worker-invocation.js +538 -0
  106. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  107. package/dist/src/sessions/adapter-registry.js +45 -0
  108. package/dist/src/sessions/adapters/generic.js +37 -3
  109. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  110. package/dist/src/sessions/adapters/grokbot.js +423 -24
  111. package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
  112. package/dist/src/sessions/adapters/muse.js +68 -29
  113. package/dist/src/sessions/adapters/pi.js +1 -0
  114. package/dist/src/sessions/batch-contracts.js +13 -1
  115. package/dist/src/sessions/batch-import.js +23 -42
  116. package/dist/src/sessions/candidates.js +47 -11
  117. package/dist/src/sessions/contracts.js +35 -0
  118. package/dist/src/sessions/coverage-preflight.js +134 -0
  119. package/dist/src/sessions/discovery.js +3 -0
  120. package/dist/src/sessions/grok-build-cli.js +67 -5
  121. package/dist/src/sessions/importer.js +24 -8
  122. package/dist/src/sessions/index.js +2 -0
  123. package/dist/src/sessions/repository.js +54 -13
  124. package/dist/src/sessions/workspace-discovery.js +99 -37
  125. package/dist/src/skills/adapters/clawhub.js +10 -8
  126. package/dist/src/skills/adapters/openclaw.js +10 -7
  127. package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
  128. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  129. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  130. package/dist/src/smiths/mcpsmith/example.js +1 -1
  131. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
  132. package/dist/src/storage/backends/logseq.js +68 -7
  133. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  134. package/dist/src/testing/generators/test-code-generator.js +57 -30
  135. package/dist/src/uhp/client.js +11 -1
  136. package/dist/src/uhp/mission.js +5 -0
  137. package/dist/src/update/checker.mjs +5 -5
  138. package/dist/src/update/notifier.mjs +54 -2
  139. package/dist/src/writing/fidelity.js +18 -1
  140. package/package.json +18 -1
  141. package/schemas/mission-v1.schema.json +2429 -0
  142. package/schemas/models/model-catalog.v1.schema.json +301 -0
  143. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  144. package/schemas/models/model-policy.v1.schema.json +14 -0
  145. package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
  146. package/tools/_resolve-impl.mjs +39 -0
  147. package/tools/agents/deploy-agents.mjs +15 -2
  148. package/tools/agents/providers/base.mjs +49 -32
  149. package/tools/agents/providers/grokbot.mjs +11 -12
  150. package/tools/agents/providers/hermes.mjs +6 -3
  151. package/tools/agents/providers/omp.mjs +5 -2
  152. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  153. package/tools/providers/deepseek-harness-transport.mjs +358 -0
  154. package/tools/providers/omp-transport.mjs +192 -0
  155. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  156. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  157. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  158. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  159. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  160. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  161. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  162. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  163. package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
  164. package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
  165. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  166. package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
@@ -30,8 +30,8 @@ export class DefaultScriptRunner {
30
30
  async run(scriptPath, args = [], options = {}) {
31
31
  const fullPath = path.join(this.frameworkRoot, scriptPath);
32
32
  return new Promise((resolve) => {
33
- // When capture mode is enabled, stdout/stderr are piped back to the
34
- // caller so that quiet-mode callers can suppress output. When not
33
+ // When capture mode is enabled, stdout/stderr are piped so that
34
+ // quiet-mode callers can suppress output. When not
35
35
  // capturing, stdio: 'inherit' lets subprocess output flow directly to
36
36
  // the terminal (verbose mode).
37
37
  const stdio = options.capture ? 'pipe' : 'inherit';
@@ -40,10 +40,20 @@ export class DefaultScriptRunner {
40
40
  cwd: options.cwd || process.cwd(),
41
41
  env: { ...process.env, ...options.env },
42
42
  });
43
- let capturedStderr = '';
43
+ // Always drain captured pipes, even after the diagnostic cap is reached.
44
+ // stdout is not part of HandlerResult; discard it. Retain only the first
45
+ // 1 MiB of stderr for failure messages, discarding any excess.
46
+ child.stdout?.resume();
47
+ const stderrLimit = 1024 * 1024;
48
+ const capturedStderr = [];
49
+ let capturedStderrBytes = 0;
44
50
  if (options.capture && child.stderr) {
45
51
  child.stderr.on('data', (data) => {
46
- capturedStderr += data.toString();
52
+ const retained = data.subarray(0, stderrLimit - capturedStderrBytes);
53
+ if (retained.length > 0) {
54
+ capturedStderr.push(Buffer.from(retained));
55
+ capturedStderrBytes += retained.length;
56
+ }
47
57
  });
48
58
  }
49
59
  const timeout = options.timeout;
@@ -86,10 +96,10 @@ export class DefaultScriptRunner {
86
96
  if (killEscalationId)
87
97
  clearTimeout(killEscalationId);
88
98
  // On failure in capture mode, surface the captured stderr
89
- if (options.capture && code !== 0 && capturedStderr) {
99
+ if (options.capture && code !== 0 && capturedStderrBytes > 0) {
90
100
  resolve({
91
101
  exitCode: code ?? 1,
92
- message: capturedStderr.trim(),
102
+ message: Buffer.concat(capturedStderr).toString().trim(),
93
103
  });
94
104
  }
95
105
  else {
@@ -14,19 +14,41 @@
14
14
  * aiwg session --provider codex # explicit provider
15
15
  * aiwg session mcp --provider cursor # MCP inject for cursor + start instructions
16
16
  * aiwg session --no-repair # skip auto-repair (still check and report)
17
+ * aiwg session --prompt "<text>" # headless: one-shot, bounded, exits (#3062)
18
+ * aiwg session --prompt-file <path> # headless from a file instead of argv
19
+ * aiwg session --non-interactive # force the non-interactive decision (#3062)
20
+ *
21
+ * Without a terminal attached, launching a spawnable provider with no
22
+ * `--prompt`/`--prompt-file` would hand over a terminal that does not exist
23
+ * and hang. `--prompt`/`--prompt-file` map to each provider's documented
24
+ * non-interactive CLI form, delivered off argv (see
25
+ * `HEADLESS_SESSION_TRANSPORTS` below); providers without an evidenced one
26
+ * refuse instead of guessing. A provider with no spawnable binary at all
27
+ * (IDE-integrated) refuses `--prompt`/`--prompt-file` outright rather than
28
+ * discarding it. `--non-interactive` forces the same refusal even with a
29
+ * terminal attached, taking priority over the environment-derived check.
17
30
  *
18
31
  * @issue #884
19
32
  */
20
33
  import { spawnSync } from 'child_process';
34
+ import { readFile } from 'node:fs/promises';
35
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
36
+ import { tmpdir } from 'node:os';
37
+ import { join } from 'node:path';
38
+ import { boundedSpawnSync } from '../../process/bounded-child-process.mjs';
39
+ import { existsSync } from 'node:fs';
21
40
  import { ensureRuntimeHome, writeProfileConfig, launchWithProfile } from '../../mcp/adapters/codex-runtime.js';
22
41
  import { getFrameworkRoot } from '../../channel/manager.mjs';
23
42
  import { forceUpdateCheck } from '../../update/checker.mjs';
24
43
  import { updateInstallation } from '../../update/service.mjs';
44
+ import { isInteractive } from '../env.js';
25
45
  import { readAiwgConfig, getDeploymentSummary, VALID_PROVIDERS, } from '../../config/aiwg-config.js';
26
46
  import { getProviderConfig, isSpawnableProvider, PROVIDER_CONFIGS, } from '../agent-spawn.js';
27
47
  import { useHandler as useFrameworkHandler } from './use.js';
28
48
  import { debug } from '../log.js';
29
49
  import { normalizeProviderDefinitionId } from '../../providers/provider-definitions.js';
50
+ import { requireFlagValue, MissingFlagValueError, hasPositionalOrSwitch } from '../flag-parsing.js';
51
+ import { exitCodeForSpawnResult } from '../child-process-result.js';
30
52
  // ── Provider resolution ──────────────────────────────────────────────────────
31
53
  /**
32
54
  * Resolve which provider to target.
@@ -46,31 +68,44 @@ async function resolveProvider(explicitProvider, cwd) {
46
68
  }
47
69
  return 'claude';
48
70
  }
71
+ /**
72
+ * Parse `aiwg session` flags.
73
+ *
74
+ * Every value-taking flag below is parsed through {@link requireFlagValue}
75
+ * (#3062 review item 11): a flag present with no value, or immediately
76
+ * followed by another flag, throws {@link MissingFlagValueError} rather
77
+ * than silently resolving to "not given" — callers MUST catch it before
78
+ * any preflight check, spawn, or write. `precedence: 'last'` matches this
79
+ * command's own pre-#3062 behavior (a hand-rolled loop that kept scanning
80
+ * and overwrote on every match, so a later `--provider`/`--profile` always
81
+ * won) — the first-round shared parser silently flipped that to
82
+ * first-wins; this restores it (#3062 second review, finding 4).
83
+ * `--prompt`/`--prompt-file` reject an empty string: an empty prompt can
84
+ * never be meaningful, and silently accepting one would let a mistake
85
+ * reach either the interactive launch path or a headless launch with
86
+ * nothing to say.
87
+ *
88
+ * The `mcp` positional and every boolean switch below go through
89
+ * {@link hasPositionalOrSwitch} rather than `args.includes(...)` (#3062
90
+ * go/no-go review, blocker 4): a plain `args.includes('mcp')` matches the
91
+ * literal word `mcp` *anywhere* in `args`, including inside a free-text
92
+ * `--prompt`/`--prompt-file` value (`aiwg session --prompt mcp` used to
93
+ * set `mcp: true` and reach MCP config injection) or after a bare `--`
94
+ * (`aiwg session -- --persist` used to still set `persist: true`).
95
+ * {@link hasPositionalOrSwitch} excludes both: anything consumed as the
96
+ * value of `VALUE_FLAGS` below, and anything at or after `--`.
97
+ */
98
+ const VALUE_FLAGS = ['--provider', '--profile', '--prompt', '--prompt-file'];
49
99
  function parseSessionArgs(args) {
50
- let mcp = false;
51
- let provider;
52
- let noRepair = false;
53
- let profile;
54
- let persist = false;
55
- for (let i = 0; i < args.length; i++) {
56
- const a = args[i];
57
- if (a === 'mcp') {
58
- mcp = true;
59
- }
60
- else if (a === '--provider' && args[i + 1]) {
61
- provider = args[++i];
62
- }
63
- else if (a === '--no-repair') {
64
- noRepair = true;
65
- }
66
- else if (a === '--profile' && args[i + 1]) {
67
- profile = args[++i];
68
- }
69
- else if (a === '--persist') {
70
- persist = true;
71
- }
72
- }
73
- return { mcp, provider, noRepair, profile, persist };
100
+ const mcp = hasPositionalOrSwitch(args, 'mcp', VALUE_FLAGS);
101
+ const noRepair = hasPositionalOrSwitch(args, '--no-repair', VALUE_FLAGS);
102
+ const persist = hasPositionalOrSwitch(args, '--persist', VALUE_FLAGS);
103
+ const nonInteractive = hasPositionalOrSwitch(args, '--non-interactive', VALUE_FLAGS);
104
+ const provider = requireFlagValue(args, { precedence: 'last' }, '--provider');
105
+ const profile = requireFlagValue(args, { precedence: 'last' }, '--profile');
106
+ const prompt = requireFlagValue(args, { precedence: 'last', rejectEmpty: true }, '--prompt');
107
+ const promptFile = requireFlagValue(args, { precedence: 'last', rejectEmpty: true }, '--prompt-file');
108
+ return { mcp, provider, noRepair, profile, persist, prompt, promptFile, nonInteractive };
74
109
  }
75
110
  // ── Pre-flight steps ─────────────────────────────────────────────────────────
76
111
  /** Run `npm view aiwg version` and compare to current. Returns true if up-to-date. */
@@ -103,8 +138,17 @@ async function checkAndUpdateVersion(noRepair) {
103
138
  * Uses the same script runner path as doctorHandler to avoid duplication.
104
139
  */
105
140
  function runDoctor(_frameworkRoot, cwd) {
106
- const result = spawnSync(process.execPath, [process.argv[1], 'doctor'], { stdio: 'inherit', cwd });
107
- return (result.status ?? 1) === 0;
141
+ // Self-invocation of the aiwg binary, not the harness launch below; bounded
142
+ // as a long-tier call (#3062) since `aiwg doctor` can touch the network.
143
+ const result = boundedSpawnSync(process.execPath, [process.argv[1], 'doctor'], 'long', { stdio: 'inherit', cwd });
144
+ // #3062 review item 7: a `null` status (killed/timed out/failed to spawn)
145
+ // already falls through to `false` below via `?? 1`; surface the bounded
146
+ // helper's diagnostic (e.g. the enriched timeout message) instead of
147
+ // silently dropping it.
148
+ const { exitCode, message } = exitCodeForSpawnResult(result, 'aiwg doctor');
149
+ if (message)
150
+ debug('cli:session:doctor', message);
151
+ return exitCode === 0;
108
152
  }
109
153
  /**
110
154
  * Attempt to repair a failed doctor result.
@@ -115,7 +159,15 @@ function runDoctor(_frameworkRoot, cwd) {
115
159
  function repairInstallation(frameworkRoot, cwd, provider) {
116
160
  // Strategy 1: sync (update + redeploy)
117
161
  console.log('\n Attempting auto-repair via `aiwg sync`...');
118
- const syncResult = spawnSync(process.execPath, [process.argv[1], 'sync'], { stdio: 'inherit', cwd });
162
+ // Self-invocation of the aiwg binary; bounded as a long-tier call (#3062)
163
+ // since `aiwg sync` updates and redeploys (network + install-shaped work).
164
+ const syncResult = boundedSpawnSync(process.execPath, [process.argv[1], 'sync'], 'long', { stdio: 'inherit', cwd });
165
+ // #3062 review item 7: `status === 0` already treats a `null` status
166
+ // (timeout/signal/spawn failure) as "not repaired", falling through to
167
+ // the manual-options message below; surface the diagnostic too.
168
+ const syncDiagnostic = exitCodeForSpawnResult(syncResult, 'aiwg sync').message;
169
+ if (syncDiagnostic)
170
+ debug('cli:session:repair', syncDiagnostic);
119
171
  if (syncResult.status === 0) {
120
172
  // Re-check doctor
121
173
  const doctorOk = runDoctor(frameworkRoot, cwd);
@@ -170,28 +222,32 @@ async function redeployFrameworks(ctx, frameworks, provider) {
170
222
  // ── MCP inject ────────────────────────────────────────────────────────────────
171
223
  function injectMcp(provider, cwd) {
172
224
  console.log(`\n Injecting configured MCP servers into ${provider}...`);
173
- const result = spawnSync(process.execPath, [process.argv[1], 'mcp', 'inject', '--provider', provider], { stdio: 'inherit', cwd });
225
+ // Self-invocation of the aiwg binary; bounded as a long-tier call (#3062).
226
+ const result = boundedSpawnSync(process.execPath, [process.argv[1], 'mcp', 'inject', '--provider', provider], 'long', { stdio: 'inherit', cwd });
174
227
  if (result.status !== 0) {
228
+ // #3062 review item 7: surface the bounded helper's diagnostic (if any)
229
+ // alongside the existing warning.
230
+ const { message } = exitCodeForSpawnResult(result, 'aiwg mcp inject');
231
+ if (message)
232
+ debug('cli:session:mcp-inject', message);
175
233
  console.warn(' WARN MCP inject reported issues — continuing. Run `aiwg mcp list` to check registered servers.');
176
234
  return false;
177
235
  }
178
236
  return true;
179
237
  }
180
- /**
181
- * Profile-aware inject (#891).
182
- * Default is ephemeral (no persistent config mutation).
183
- *
184
- * For Claude: returns the ephemeral config path (for claude --mcp-config).
185
- * For Codex: sets up the runtime home via the codex-runtime adapter (#892).
186
- * Returns null for persistent mode or on failure.
187
- */
188
- function injectMcpProfile(provider, profileName, cwd, persist) {
238
+ function injectMcpProfile(provider, profileName, cwd, persist, hasClaudePermissions = false) {
189
239
  console.log(`\n Resolving profile "${profileName}" for ${provider}...`);
190
240
  if (persist) {
191
241
  // Persistent: write to provider's default config
192
- const result = spawnSync(process.execPath, [process.argv[1], 'mcp', 'inject', '--provider', provider, '--profile', profileName], { stdio: 'inherit', cwd });
242
+ // Self-invocation of the aiwg binary; bounded as a long-tier call (#3062).
243
+ const result = boundedSpawnSync(process.execPath, [process.argv[1], 'mcp', 'inject', '--provider', provider, '--profile', profileName], 'long', { stdio: 'inherit', cwd });
193
244
  if (result.status !== 0) {
194
- console.warn(' WARN Profile inject failed — continuing without profile.');
245
+ // #3062 review item 7: surface the bounded helper's diagnostic.
246
+ const { message } = exitCodeForSpawnResult(result, 'aiwg mcp inject');
247
+ if (provider === 'codex' || provider === 'openai' || provider === 'claude' || provider === 'claude-code') {
248
+ throw new Error(`${provider} profile injection failed; refusing to launch.${message ? ` ${message}` : ''}`);
249
+ }
250
+ console.warn(` WARN Profile inject failed — continuing without profile.${message ? ` ${message}` : ''}`);
195
251
  }
196
252
  return null; // persistent mode, no ephemeral path
197
253
  }
@@ -202,28 +258,119 @@ function injectMcpProfile(provider, profileName, cwd, persist) {
202
258
  }
203
259
  // Claude/others: generate a temp config file
204
260
  const tmpPath = `${process.env.TMPDIR ?? '/tmp'}/aiwg-mcp-${profileName}-${provider}-${Date.now()}.json`;
205
- const result = spawnSync(process.execPath, [
261
+ // Self-invocation of the aiwg binary; bounded as a long-tier call (#3062).
262
+ const result = boundedSpawnSync(process.execPath, [
206
263
  process.argv[1], 'mcp', 'inject',
207
264
  '--provider', provider,
208
265
  '--profile', profileName,
209
266
  '--ephemeral',
210
267
  '--out', tmpPath,
211
- ], { stdio: 'inherit', cwd });
268
+ ], 'long', { stdio: ['inherit', 'pipe', 'inherit'], encoding: 'utf-8', cwd });
269
+ if (result.stdout)
270
+ process.stdout.write(result.stdout);
212
271
  if (result.status !== 0) {
213
- console.warn(' WARN Ephemeral profile inject failed — launching without profile MCP config.');
272
+ // #3062 review item 7: surface the bounded helper's diagnostic.
273
+ const { message } = exitCodeForSpawnResult(result, 'aiwg mcp inject');
274
+ if (provider === 'claude' || provider === 'claude-code') {
275
+ throw new Error(`Claude profile injection failed; refusing to launch.${message ? ` ${message}` : ''}`);
276
+ }
277
+ console.warn(` WARN Ephemeral profile inject failed — launching without profile MCP config.${message ? ` ${message}` : ''}`);
278
+ return null;
279
+ }
280
+ const reportedSettingsPath = result.stdout?.match(/^\s*Tool permissions: (.+)$/m)?.[1]?.trim();
281
+ const settingsPath = reportedSettingsPath && existsSync(reportedSettingsPath) ? reportedSettingsPath : null;
282
+ if (hasClaudePermissions && !settingsPath) {
283
+ throw new Error('Claude profile permission settings were not generated; refusing to launch.');
284
+ }
285
+ return {
286
+ mcpConfigPath: tmpPath,
287
+ settingsPath,
288
+ };
289
+ }
290
+ const HEADLESS_SESSION_TRANSPORTS = {
291
+ claude: { kind: 'stdin', flags: ['--print', '--input-format', 'text'] },
292
+ 'claude-code': { kind: 'stdin', flags: ['--print', '--input-format', 'text'] },
293
+ codex: { kind: 'stdin', flags: ['exec'], trailingArg: '-' },
294
+ openai: { kind: 'stdin', flags: ['exec'], trailingArg: '-' },
295
+ 'deepseek-harness': { kind: 'stdin', flags: ['--profile', 'headless'], trailingArg: '-' },
296
+ opencode: {
297
+ kind: 'file',
298
+ flags: ['run', '--format', 'json'],
299
+ fileFlag: '-f',
300
+ fixedPositional: 'Follow the instructions in the attached file exactly.',
301
+ },
302
+ };
303
+ /**
304
+ * Build the final argv (and stdin/temp-file payload) for a headless launch,
305
+ * or `null` if the provider has no evidenced non-interactive form.
306
+ */
307
+ function buildHeadlessLaunch(provider, binaryArgs, prompt) {
308
+ const transport = HEADLESS_SESSION_TRANSPORTS[provider];
309
+ if (!transport)
214
310
  return null;
311
+ if (transport.kind === 'stdin') {
312
+ const args = [...binaryArgs, ...transport.flags, ...(transport.trailingArg ? [transport.trailingArg] : [])];
313
+ return { args, input: prompt };
314
+ }
315
+ // File transport (opencode): the prompt never touches argv or stdin — it
316
+ // is written to a private temp file only this process can read, passed
317
+ // by path. `mkdtemp()` under the system temp directory (never a custom
318
+ // location — #3062 second review, finding 10) already gives the
319
+ // directory itself a private, unpredictable name owned by this process;
320
+ // the file within it additionally gets an explicit `0600` mode via an
321
+ // exclusive create (`flag: 'wx'` — fails rather than silently
322
+ // overwriting if a file with that exact name already existed, which it
323
+ // never should in a fresh `mkdtemp()` directory).
324
+ //
325
+ // Cleanup ownership starts the moment `mkdtemp()` returns, *before* the
326
+ // write: a failed write must not leave the directory behind any more
327
+ // than a failed launch would. The caller's own `finally` (in
328
+ // `launchProvider()`, keyed off `plan.tempDir`) handles the ordinary
329
+ // "launch ran, now clean up" path; this one exists only for the write
330
+ // itself failing before a `plan` is ever returned to that caller.
331
+ const tempDir = mkdtempSync(join(tmpdir(), 'aiwg-session-prompt-'));
332
+ try {
333
+ const promptPath = join(tempDir, 'prompt.txt');
334
+ writeFileSync(promptPath, prompt, { encoding: 'utf-8', mode: 0o600, flag: 'wx' });
335
+ const args = [...binaryArgs, ...transport.flags, transport.fileFlag, promptPath, transport.fixedPositional];
336
+ return { args, tempDir };
337
+ }
338
+ catch (err) {
339
+ try {
340
+ rmSync(tempDir, { recursive: true, force: true });
341
+ }
342
+ catch {
343
+ // Best-effort cleanup only; the original write failure is what matters.
344
+ }
345
+ throw err;
215
346
  }
216
- return tmpPath;
217
347
  }
218
- // ── Launch ────────────────────────────────────────────────────────────────────
219
348
  /**
220
349
  * Launch a spawnable provider binary, replacing the current process (exec-style).
221
350
  * For IDE-integrated providers, print guidance and exit.
351
+ *
352
+ * `prompt`, when given, selects the headless path (#3062): the provider is
353
+ * launched with its documented non-interactive form, bounded and with stdin
354
+ * ignored, regardless of whether a terminal is attached. Without `prompt`
355
+ * and without a terminal, this refuses before spawning anything that would
356
+ * hang.
222
357
  */
223
- function launchProvider(provider, mcpInjected, mcpConfigPath, profile) {
358
+ function launchProvider(provider, mcpInjected, mcpConfigPath, profile, settingsPath, prompt, interactive) {
224
359
  const cfg = getProviderConfig(provider);
225
360
  if (!isSpawnableProvider(provider)) {
226
- // Not spawnable — print context-aware guidance
361
+ // Not spawnable — a prompt can never be delivered to a binary that does
362
+ // not exist. Refuse explicitly instead of discarding it and printing
363
+ // success (#3062 review item 9). `execute()` already checks this before
364
+ // any pre-flight step runs; this is defense-in-depth for this function
365
+ // on its own.
366
+ if (prompt) {
367
+ return {
368
+ exitCode: 1,
369
+ message: `Provider '${cfg.name}' has no CLI binary to spawn; --prompt/--prompt-file requires a ` +
370
+ 'spawnable provider.',
371
+ };
372
+ }
373
+ // Print context-aware guidance
227
374
  const mcpNote = mcpInjected
228
375
  ? '\n MCP servers have been injected into your provider config.\n'
229
376
  : '';
@@ -237,25 +384,84 @@ ${cfg.guidanceMessage ?? `Open ${cfg.name} in your project directory to begin.`}
237
384
  `);
238
385
  return { exitCode: 0 };
239
386
  }
240
- // Spawnable — hand off the terminal
387
+ // Spawnable — hand off the terminal (or run headless, see below)
241
388
  console.log(`\n── Launching ${cfg.name} ──\n`);
242
389
  // Codex + profile: use runtime-home adapter (#892)
243
390
  if ((provider === 'codex' || provider === 'openai') && profile) {
244
391
  console.log(` Using runtime home for profile "${profile}"`);
245
- const result = launchWithProfile(profile);
246
- return { exitCode: result.status ?? 0 };
392
+ // Headless (#3062): the prompt travels on stdin via a trailing `-`
393
+ // positional (docs/providers/codex-direct.md: "`-` reads stdin"),
394
+ // never on argv (review item 8). `launchWithProfile` itself
395
+ // bounds/refuses based on whether `extraArgs` is a non-interactive form
396
+ // and whether a terminal is attached — see its own doc comment.
397
+ const extraArgs = prompt ? ['exec', '-'] : [];
398
+ const result = launchWithProfile(profile, extraArgs, prompt ? { input: prompt, interactive } : { interactive });
399
+ const { exitCode, message } = exitCodeForSpawnResult(result, 'codex');
400
+ return message ? { exitCode, message } : { exitCode };
247
401
  }
248
- // Claude: if we have an ephemeral MCP config, pass it via --mcp-config
402
+ // Claude: load both the ephemeral MCP config and any permission settings.
249
403
  const binaryArgs = [];
250
404
  if (mcpConfigPath && (provider === 'claude' || provider === 'claude-code')) {
251
405
  binaryArgs.push('--mcp-config', mcpConfigPath);
252
406
  console.log(` Using ephemeral MCP config: ${mcpConfigPath}`);
407
+ if (settingsPath) {
408
+ binaryArgs.push('--settings', settingsPath);
409
+ console.log(` Using ephemeral permission settings: ${settingsPath}`);
410
+ }
253
411
  }
412
+ if (prompt) {
413
+ const plan = buildHeadlessLaunch(provider, binaryArgs, prompt);
414
+ if (!plan) {
415
+ return {
416
+ exitCode: 1,
417
+ message: `Provider '${cfg.name}' has no evidenced non-interactive CLI form; --prompt/--prompt-file ` +
418
+ `is not supported for it. Run \`aiwg session --provider ${provider}\` from a terminal instead.`,
419
+ };
420
+ }
421
+ try {
422
+ // Bounded (#3062): a headless run never hands over the terminal, so
423
+ // it is safe to bound. The prompt never rides on argv (review item
424
+ // 8); stdin-transport providers get it via `input` (never inherited
425
+ // from the parent), file-transport providers get it via a private
426
+ // temp file path already baked into `plan.args`.
427
+ const result = boundedSpawnSync(cfg.binary, plan.args, 'long', {
428
+ stdio: plan.input !== undefined ? ['pipe', 'inherit', 'inherit'] : ['ignore', 'inherit', 'inherit'],
429
+ env: process.env,
430
+ ...(plan.input !== undefined ? { input: plan.input } : {}),
431
+ });
432
+ const { exitCode, message } = exitCodeForSpawnResult(result, cfg.binary);
433
+ return message ? { exitCode, message } : { exitCode };
434
+ }
435
+ finally {
436
+ if (plan.tempDir) {
437
+ try {
438
+ rmSync(plan.tempDir, { recursive: true, force: true });
439
+ }
440
+ catch {
441
+ // Best-effort cleanup only; a leftover temp file is not fatal.
442
+ }
443
+ }
444
+ }
445
+ }
446
+ if (!interactive) {
447
+ return {
448
+ exitCode: 1,
449
+ message: 'aiwg session requires an interactive terminal to launch a provider without a prompt. ' +
450
+ 'Pass --prompt <text> or --prompt-file <path> to run headless.',
451
+ };
452
+ }
453
+ // sync-spawn: interactive harness-launch — hands the terminal to the
454
+ // provider binary the user is meant to talk to, when no --prompt/
455
+ // --prompt-file was supplied (see tools/testing/sync-spawn-allowlist.json).
456
+ // Reached only when `interactive` is true (#3062) — the headless and
457
+ // no-terminal paths above are handled before this point. Not a candidate
458
+ // for a bounded timeout.
254
459
  const result = spawnSync(cfg.binary, binaryArgs, {
255
460
  stdio: 'inherit',
256
461
  env: process.env,
257
462
  });
258
- return { exitCode: result.status ?? 0 };
463
+ const { exitCode, message } = exitCodeForSpawnResult(result, cfg.binary);
464
+ return message ? { exitCode, message } : { exitCode };
259
465
  }
260
466
  // ── Handler ───────────────────────────────────────────────────────────────────
261
467
  export const sessionHandler = {
@@ -265,11 +471,93 @@ export const sessionHandler = {
265
471
  category: 'project',
266
472
  aliases: [],
267
473
  async execute(ctx) {
268
- const { mcp, provider: explicitProvider, noRepair, profile, persist } = parseSessionArgs(ctx.args);
474
+ let parsed;
475
+ try {
476
+ parsed = parseSessionArgs(ctx.args);
477
+ }
478
+ catch (err) {
479
+ // #3062 review item 11: a value-taking flag present without a usable
480
+ // value is a usage error, reported before any preflight/spawn/write.
481
+ if (err instanceof MissingFlagValueError) {
482
+ return { exitCode: 2, message: `aiwg session: ${err.message}.` };
483
+ }
484
+ throw err;
485
+ }
486
+ const { mcp, provider: explicitProvider, noRepair, profile, persist, prompt: promptArg, promptFile, nonInteractive, } = parsed;
269
487
  const cwd = ctx.cwd || process.cwd();
488
+ // #3062 review item 12: the one shared interactive decision, taking
489
+ // this command's own explicit `--non-interactive` flag as input instead
490
+ // of only ever checking the environment.
491
+ const interactive = isInteractive(nonInteractive);
492
+ // ── Resolve headless prompt (#3062) — before any pre-flight spawn ──────
493
+ if (promptArg && promptFile) {
494
+ return { exitCode: 2, message: 'aiwg session: pass only one of --prompt or --prompt-file.' };
495
+ }
496
+ let prompt = promptArg;
497
+ if (!prompt && promptFile) {
498
+ let fileText;
499
+ try {
500
+ fileText = await readFile(promptFile, 'utf-8');
501
+ }
502
+ catch (err) {
503
+ return {
504
+ exitCode: 1,
505
+ message: `aiwg session: could not read --prompt-file '${promptFile}': ${err instanceof Error ? err.message : String(err)}`,
506
+ };
507
+ }
508
+ prompt = fileText.trim();
509
+ if (!prompt) {
510
+ return { exitCode: 1, message: `aiwg session: --prompt-file '${promptFile}' is empty.` };
511
+ }
512
+ }
270
513
  // ── Resolve provider ─────────────────────────────────────────
271
514
  const provider = await resolveProvider(explicitProvider, cwd);
272
515
  const providerCfg = PROVIDER_CONFIGS[provider];
516
+ // An unsupported (non-spawnable) provider can never run a supplied
517
+ // prompt — it has no binary to hand it to. Validate this before any
518
+ // pre-flight step runs a child process (#3062 review item 9): the old
519
+ // behavior silently discarded the prompt and printed IDE guidance at
520
+ // exit 0.
521
+ if (prompt && !isSpawnableProvider(provider)) {
522
+ const unsupportedCfg = getProviderConfig(provider);
523
+ return {
524
+ exitCode: 1,
525
+ message: `Provider '${unsupportedCfg.name}' has no CLI binary to spawn; --prompt/--prompt-file ` +
526
+ 'requires a spawnable provider. Run `aiwg session --provider ' + provider +
527
+ '` from a terminal for IDE-integrated usage instead.',
528
+ };
529
+ }
530
+ // A spawnable provider with no evidenced non-interactive CLI form
531
+ // (e.g. antigravity) can never run a supplied prompt either — check
532
+ // this in the same early spot as the non-spawnable case above (#3062
533
+ // second review, finding 6): the old behavior deferred this to
534
+ // `buildHeadlessLaunch()` inside `launchProvider()`, reached only
535
+ // after the version-check/doctor/deployment/mcp-inject pre-flight
536
+ // already ran. The codex-profile launch path is exempt — it always
537
+ // uses `codex exec -`, never `HEADLESS_SESSION_TRANSPORTS`.
538
+ if (prompt && isSpawnableProvider(provider) && !HEADLESS_SESSION_TRANSPORTS[provider]
539
+ && !((provider === 'codex' || provider === 'openai') && profile)) {
540
+ const unsupportedTransportCfg = getProviderConfig(provider);
541
+ return {
542
+ exitCode: 1,
543
+ message: `Provider '${unsupportedTransportCfg.name}' has no evidenced non-interactive CLI form; ` +
544
+ `--prompt/--prompt-file is not supported for it. Run \`aiwg session --provider ${provider}\` ` +
545
+ 'from a terminal instead.',
546
+ };
547
+ }
548
+ // Without a terminal, a spawnable provider launched with no prompt would
549
+ // hand over a terminal that does not exist and hang. Fail fast, before
550
+ // any pre-flight step runs a child process (#3062). IDE-integrated
551
+ // providers only ever print guidance — never spawn — so they don't need
552
+ // a prompt to stay safe headless.
553
+ if (!prompt && isSpawnableProvider(provider) && !interactive) {
554
+ return {
555
+ exitCode: 1,
556
+ message: `aiwg session requires --prompt <text> or --prompt-file <path> when no interactive terminal is ` +
557
+ `attached (provider '${provider}' launches a binary that would otherwise hand over the terminal ` +
558
+ 'and hang).',
559
+ };
560
+ }
273
561
  const profileSuffix = profile ? ` · profile: ${profile}` : '';
274
562
  console.log(`\n── aiwg session ── provider: ${providerCfg?.name ?? provider}${profileSuffix} ──\n`);
275
563
  const frameworkRoot = await getFrameworkRoot();
@@ -298,38 +586,84 @@ export const sessionHandler = {
298
586
  // ── Step 4: MCP inject ────────────────────────────────────────
299
587
  let mcpInjected = false;
300
588
  let mcpConfigPath = null;
589
+ let settingsPath = null;
301
590
  if (profile) {
302
591
  // Profile-aware injection (#891) — ephemeral by default
303
- // For codex: set up the runtime home and write profile config (#892)
304
- if ((provider === 'codex' || provider === 'openai') && !persist) {
592
+ // Codex profiles must pass policy and setup before any launch (#892)
593
+ if (provider === 'codex' || provider === 'openai') {
305
594
  try {
306
- console.log(`\n Setting up Codex runtime home for profile "${profile}"...`);
595
+ console.log(`\n Setting up Codex ${persist ? 'persistent config' : 'runtime home'} for profile "${profile}"...`);
307
596
  // Import server list for this profile
308
597
  const { McpProfileRegistry } = await import('../../mcp/profiles.js');
309
598
  const { McpServerRegistry } = await import('../../mcp/registry.js');
310
599
  const profiles = new McpProfileRegistry();
311
600
  const registry = new McpServerRegistry();
312
601
  const resolvedServers = await profiles.resolveServers(profile, registry);
313
- await ensureRuntimeHome(profile);
314
- await writeProfileConfig(profile, resolvedServers);
315
- console.log(` Runtime home ready. Profile servers: ${resolvedServers.map((s) => s.name).join(', ') || '(none)'}`);
602
+ const { resolveToolFilters } = await import('../../mcp/tool-filters.mjs');
603
+ const { assertCredentialPolicy, resolveCredentialPolicy } = await import('../../mcp/credentials.mjs');
604
+ const registryPolicy = await registry.getCredentialPolicy();
605
+ const floorPolicy = await registry.getCredentialPolicyFloor();
606
+ const floorSource = await registry.getCredentialPolicyFloorSource();
607
+ let credentialPolicy;
608
+ try {
609
+ credentialPolicy = resolveCredentialPolicy({ registryPolicy, floorPolicy, floorSource });
610
+ assertCredentialPolicy(resolvedServers, credentialPolicy);
611
+ }
612
+ catch (err) {
613
+ console.error(` ${err instanceof Error ? err.message : String(err)}`);
614
+ return { exitCode: 1 };
615
+ }
616
+ if (persist) {
617
+ injectMcpProfile(provider, profile, cwd, true);
618
+ }
619
+ else {
620
+ await ensureRuntimeHome(profile);
621
+ // writeProfileConfig re-resolves and re-enforces the floor itself (so a
622
+ // direct API call cannot bypass it); passing it here too keeps this
623
+ // call's diagnostics consistent with the one already performed above.
624
+ const warnings = await writeProfileConfig(profile, resolvedServers, {
625
+ credentialPolicy, floorPolicy,
626
+ toolFilters: resolveToolFilters(await profiles.resolve(profile), 'codex'),
627
+ });
628
+ for (const warning of warnings)
629
+ console.warn(` WARN codex: ${warning}`);
630
+ console.log(` Runtime home ready. Profile servers: ${resolvedServers.map((s) => s.name).join(', ') || '(none)'}`);
631
+ }
316
632
  mcpInjected = true;
317
633
  }
318
634
  catch (err) {
319
635
  console.warn(` WARN Codex runtime home setup failed: ${err instanceof Error ? err.message : String(err)}`);
320
- console.warn(' Falling back to standard launch.');
636
+ return { exitCode: 1 };
321
637
  }
322
638
  }
323
639
  else {
324
- mcpConfigPath = injectMcpProfile(provider, profile, cwd, persist);
325
- mcpInjected = mcpConfigPath !== null || persist;
640
+ try {
641
+ let hasClaudePermissions = false;
642
+ if (provider === 'claude' || provider === 'claude-code') {
643
+ const { McpProfileRegistry } = await import('../../mcp/profiles.js');
644
+ const { McpServerRegistry } = await import('../../mcp/registry.js');
645
+ const { resolveToolFilters, planToolFilters } = await import('../../mcp/tool-filters.mjs');
646
+ const profiles = new McpProfileRegistry();
647
+ const servers = await profiles.resolveServers(profile, new McpServerRegistry());
648
+ const plan = planToolFilters(provider, servers.map(server => server.name), resolveToolFilters(await profiles.resolve(profile), provider));
649
+ hasClaudePermissions = plan.claudePermissions !== null;
650
+ }
651
+ const ephemeral = injectMcpProfile(provider, profile, cwd, persist, hasClaudePermissions);
652
+ mcpConfigPath = ephemeral?.mcpConfigPath ?? null;
653
+ settingsPath = ephemeral?.settingsPath ?? null;
654
+ mcpInjected = ephemeral !== null || persist;
655
+ }
656
+ catch (err) {
657
+ console.error(` ${err instanceof Error ? err.message : String(err)}`);
658
+ return { exitCode: 1 };
659
+ }
326
660
  }
327
661
  }
328
662
  else if (mcp) {
329
663
  mcpInjected = injectMcp(provider, cwd);
330
664
  }
331
665
  // ── Step 5: Launch ────────────────────────────────────────────
332
- return launchProvider(provider, mcpInjected, mcpConfigPath, profile);
666
+ return launchProvider(provider, mcpInjected, mcpConfigPath, persist && (provider === 'codex' || provider === 'openai') ? null : profile, settingsPath, prompt, interactive);
333
667
  },
334
668
  };
335
669
  //# sourceMappingURL=session.js.map