@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
@@ -8,7 +8,7 @@
8
8
  * validate — Validate all config files
9
9
  * reset [<key>] — Reset key or all config to defaults
10
10
  * path — Print the active config directory path
11
- * edit — Open config in $EDITOR
11
+ * edit [--print-path] — Open config in $EDITOR/$VISUAL, or print its path headlessly
12
12
  * gitignore — Show/check/fix .gitignore for AIWG runtime dirs
13
13
  *
14
14
  * Global flags:
@@ -21,6 +21,8 @@ import { fileURLToPath } from 'url';
21
21
  import path from 'path';
22
22
  import { UserConfig } from './user-config.js';
23
23
  import { AiwgError, EXIT_CODES } from '../cli/errors.js';
24
+ import { isInteractive } from '../cli/env.js';
25
+ import { exitCodeForSpawnResult } from '../cli/child-process-result.js';
24
26
  import { projectControlPath, resolveProjectAiwgDir } from './project-artifacts.js';
25
27
  const _scriptDir = path.dirname(fileURLToPath(import.meta.url));
26
28
  /**
@@ -63,7 +65,7 @@ export async function main(args) {
63
65
  handlePath(config);
64
66
  break;
65
67
  case 'edit':
66
- await handleEdit(config);
68
+ await handleEdit(config, subArgs);
67
69
  break;
68
70
  case 'gitignore':
69
71
  await handleGitignore(subArgs);
@@ -86,7 +88,7 @@ export async function main(args) {
86
88
  }
87
89
  async function handleGet(config, args) {
88
90
  const isProject = args.includes('--project');
89
- const positional = args.filter(a => a !== '--project' && a !== '--user');
91
+ const positional = configPositionals(args);
90
92
  const key = positional[0];
91
93
  if (!key) {
92
94
  throw new AiwgError({
@@ -113,7 +115,7 @@ async function handleGet(config, args) {
113
115
  }
114
116
  async function handleSet(config, args) {
115
117
  const isProject = args.includes('--project');
116
- const positional = args.filter(a => a !== '--project' && a !== '--user');
118
+ const positional = configPositionals(args);
117
119
  const key = positional[0];
118
120
  const value = positional[1];
119
121
  if (!key || value === undefined) {
@@ -167,6 +169,11 @@ const ENUM_RULES = {
167
169
  'security.threatAssessment.mode': ['off', 'audit', 'enforce'],
168
170
  'artifact_outputs.canonical': ['aiwg'],
169
171
  'artifact_outputs.provider_native': ['disabled', 'explicit-only', 'project-default'],
172
+ // #3050: a setup/onboarding preference, not worker policy — kept out of
173
+ // the frozen `parallelism` contract fragment (B-OG-29). String enum (not
174
+ // boolean) so a future state (e.g. "snoozed") can be added without a
175
+ // breaking schema change.
176
+ 'setup.worker_dispatch_offer': ['declined'],
170
177
  };
171
178
  const BOOLEAN_FIELDS = new Set([
172
179
  'delivery.delete_branch_on_merge',
@@ -178,6 +185,7 @@ const BOOLEAN_FIELDS = new Set([
178
185
  'telemetry.skill_usage.enabled',
179
186
  ]);
180
187
  const STRING_ARRAY_FIELDS = new Set([
188
+ 'providers',
181
189
  'remotes.tracker_actor.forbid_actors',
182
190
  'remotes.customer_tracker_actor.forbid_actors',
183
191
  'command_log.scopes',
@@ -193,6 +201,7 @@ const INTEGER_FIELDS = {
193
201
  'telemetry.skill_usage.max_bytes': { min: 1024, max: 104_857_600 },
194
202
  };
195
203
  async function projectConfigGet(key, args) {
204
+ assertSafeConfigPath(key);
196
205
  const { readAiwgConfig, getProjectDir } = await import('./aiwg-config.js');
197
206
  const projectDir = getProjectDir(undefined, args);
198
207
  const cfg = await readAiwgConfig(projectDir);
@@ -204,6 +213,21 @@ async function projectConfigGet(key, args) {
204
213
  exitCode: EXIT_CODES.CONFIG,
205
214
  });
206
215
  }
216
+ // Model preferences keep their existing file owner. Inspection uses the same
217
+ // merged policy and pre-merge origins as worker resolution, never aiwg.config.
218
+ if (key === 'defaults.workerSelection' || key.startsWith('defaults.workerSelection.')) {
219
+ const { ConfigLoader } = await import('../models/config-loader.js');
220
+ const loader = new ConfigLoader(projectDir);
221
+ const models = await loader.load(cfg.parallelism);
222
+ const value = getDottedPath(models, key);
223
+ const pointer = `/${key.split('.').join('/')}`;
224
+ const origins = Object.fromEntries(Object.entries(models.workerPolicyOrigins ?? {})
225
+ .filter(([field]) => field === pointer || field.startsWith(`${pointer}/`)));
226
+ console.log(args.includes('--origins')
227
+ ? JSON.stringify({ value: value ?? null, origins, digests: loader.getInputDigests() }, null, 2)
228
+ : value === undefined ? '(not set)' : JSON.stringify(value, null, 2));
229
+ return;
230
+ }
207
231
  const value = getDottedPath(cfg, key);
208
232
  if (value === undefined) {
209
233
  console.log('(not set)');
@@ -216,6 +240,15 @@ async function projectConfigGet(key, args) {
216
240
  }
217
241
  }
218
242
  async function projectConfigSet(key, raw, args) {
243
+ assertSafeConfigPath(key);
244
+ if (key === 'defaults' || key.startsWith('defaults.')) {
245
+ throw new AiwgError({
246
+ code: 'ERR_INVALID_KEY',
247
+ message: `Project model preferences '${key}' belong in models.json, not aiwg.config.`,
248
+ hint: 'Edit project-root models.json to change defaults, including defaults.workerSelection.',
249
+ exitCode: EXIT_CODES.USAGE,
250
+ });
251
+ }
219
252
  const { readAiwgConfig, writeAiwgConfig, getProjectDir, emptyConfig, validateExternalLinks, } = await import('./aiwg-config.js');
220
253
  const projectDir = getProjectDir(undefined, args);
221
254
  // Validate enum fields before writing. Deprecated spellings normalize forward with a
@@ -237,6 +270,12 @@ async function projectConfigSet(key, raw, args) {
237
270
  }
238
271
  // Coerce booleans for known boolean fields
239
272
  let value = raw;
273
+ if (key === 'parallelism' || /^parallelism\.(worker_dispatch|worker_mode|groups|clis|admission|budgets|health|retry|activation)(\.|$)/.test(key)) {
274
+ try {
275
+ value = JSON.parse(raw);
276
+ }
277
+ catch { /* String leaves are validated by the whole-object writer. */ }
278
+ }
240
279
  if (/^externalLinks\.[^.]+$/.test(key) || key === 'security.threatAssessment') {
241
280
  try {
242
281
  value = JSON.parse(raw);
@@ -267,10 +306,24 @@ async function projectConfigSet(key, raw, args) {
267
306
  }
268
307
  }
269
308
  if (STRING_ARRAY_FIELDS.has(key)) {
270
- value = raw
271
- .split(',')
272
- .map(item => item.trim())
273
- .filter(Boolean);
309
+ let items = raw.split(',');
310
+ if (raw.trimStart().startsWith('[')) {
311
+ try {
312
+ items = JSON.parse(raw);
313
+ }
314
+ catch {
315
+ items = undefined;
316
+ }
317
+ if (!Array.isArray(items) || items.some(item => typeof item !== 'string')) {
318
+ throw new AiwgError({
319
+ code: 'ERR_INVALID_VALUE',
320
+ message: `${key} must be a comma-separated list or a JSON array of strings`,
321
+ hint: `Try: aiwg config set --project ${key} '["a","b"]' (or a,b)`,
322
+ exitCode: EXIT_CODES.USAGE,
323
+ });
324
+ }
325
+ }
326
+ value = items.map(item => item.trim()).filter(Boolean);
274
327
  if (key === 'command_log.scopes' || key === 'telemetry.skill_usage.scopes') {
275
328
  const invalid = value.filter(item => item !== 'project' && item !== 'global');
276
329
  if (invalid.length > 0) {
@@ -336,6 +389,7 @@ function getDottedPath(obj, key) {
336
389
  }
337
390
  /** Write a dotted path into a nested record, creating intermediate objects. */
338
391
  function setDottedPath(obj, key, value) {
392
+ assertSafeConfigPath(key);
339
393
  const parts = key.split('.');
340
394
  const last = parts.pop();
341
395
  let cur = obj;
@@ -348,6 +402,25 @@ function setDottedPath(obj, key, value) {
348
402
  }
349
403
  cur[last] = value;
350
404
  }
405
+ function assertSafeConfigPath(key) {
406
+ if (key.split('.').some(part => !part || ['__proto__', 'prototype', 'constructor'].includes(part))) {
407
+ throw new AiwgError({ code: 'ERR_INVALID_KEY', message: 'Unsafe configuration path', exitCode: EXIT_CODES.USAGE });
408
+ }
409
+ }
410
+ /** Scope/inspection flags are not dotted keys, irrespective of flag order. */
411
+ function configPositionals(args) {
412
+ const result = [];
413
+ for (let i = 0; i < args.length; i++) {
414
+ if (['--target', '--prefix'].includes(args[i])) {
415
+ i++;
416
+ continue;
417
+ }
418
+ if (['--project', '--user', '--origins'].includes(args[i]))
419
+ continue;
420
+ result.push(args[i]);
421
+ }
422
+ return result;
423
+ }
351
424
  async function handleList(config) {
352
425
  const allConfig = await config.list();
353
426
  console.log(`Config directory: ${config.getPath()}\n`);
@@ -392,7 +465,7 @@ async function handleValidate(config, args) {
392
465
  }
393
466
  }
394
467
  async function handleProjectValidate(args) {
395
- const { getProjectDir, readAiwgConfig, resolveIssueLabels, validateExternalLinks, validateIndexConfig, validateIssueLabels, } = await import('./aiwg-config.js');
468
+ const { getProjectDir, readAiwgConfig, resolveIssueLabels, validateExternalLinks, validateIndexConfig, validateIssueLabels, validateWorkerConfigCandidate, } = await import('./aiwg-config.js');
396
469
  const projectDir = getProjectDir(undefined, args);
397
470
  const cfg = await readAiwgConfig(projectDir);
398
471
  if (!cfg) {
@@ -403,6 +476,7 @@ async function handleProjectValidate(args) {
403
476
  exitCode: EXIT_CODES.CONFIG,
404
477
  });
405
478
  }
479
+ await validateWorkerConfigCandidate(projectDir, cfg);
406
480
  const providerIndex = args.indexOf('--provider');
407
481
  const providerArg = providerIndex >= 0 ? args[providerIndex + 1] : undefined;
408
482
  const allowedProviders = ['gitea', 'github', 'local'];
@@ -463,7 +537,7 @@ async function handleProjectValidate(args) {
463
537
  }
464
538
  async function handleReset(config, args) {
465
539
  const isProject = args.includes('--project');
466
- const positional = args.filter(a => a !== '--project');
540
+ const positional = configPositionals(args);
467
541
  const key = positional[0];
468
542
  if (isProject) {
469
543
  await projectConfigReset(key, args);
@@ -496,6 +570,7 @@ async function projectConfigReset(key, args) {
496
570
  exitCode: EXIT_CODES.USAGE,
497
571
  });
498
572
  }
573
+ assertSafeConfigPath(key);
499
574
  const projectDir = getProjectDir(undefined, args);
500
575
  const cfg = await readAiwgConfig(projectDir);
501
576
  if (!cfg) {
@@ -520,6 +595,19 @@ async function projectConfigReset(key, args) {
520
595
  return;
521
596
  }
522
597
  if (key.startsWith('parallelism.')) {
598
+ const workerField = key.split('.')[1];
599
+ if (['worker_dispatch', 'worker_mode', 'clis', 'groups', 'admission', 'budgets', 'retry', 'health', 'activation'].includes(workerField)) {
600
+ const parts = key.split('.').slice(1);
601
+ const last = parts.pop();
602
+ let current = cfg.parallelism;
603
+ for (const part of parts)
604
+ current = current?.[part];
605
+ if (current)
606
+ delete current[last];
607
+ await writeAiwgConfig(projectDir, cfg);
608
+ console.log(`Reset --project ${key}`);
609
+ return;
610
+ }
523
611
  const field = key.slice('parallelism.'.length);
524
612
  if (field in defaults) {
525
613
  cfg.parallelism = cfg.parallelism ?? {};
@@ -539,13 +627,40 @@ async function projectConfigReset(key, args) {
539
627
  function handlePath(config) {
540
628
  console.log(config.getPath());
541
629
  }
542
- async function handleEdit(config) {
543
- const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
630
+ /**
631
+ * `aiwg config edit [--print-path]` (#3062). `--print-path` is the headless
632
+ * equivalent: it prints the resolved config file path and exits, so scripts
633
+ * and agents can read/edit the file themselves (or compose with
634
+ * `aiwg config get`/`set`) instead of needing $EDITOR. Without a terminal
635
+ * and without `--print-path`, this refuses instead of spawning an editor
636
+ * that would hang waiting for input nobody can supply.
637
+ */
638
+ async function handleEdit(config, args = []) {
544
639
  const configPath = `${config.getPath()}/config.yaml`;
640
+ if (args.includes('--print-path')) {
641
+ // Ensure the config file exists before pointing a caller at it.
642
+ await config.ensureDir();
643
+ console.log(configPath);
644
+ return;
645
+ }
646
+ if (!isInteractive()) {
647
+ throw new AiwgError({
648
+ code: 'ERR_NON_INTERACTIVE_EDIT',
649
+ message: 'aiwg config edit requires an interactive terminal to open $EDITOR/$VISUAL.',
650
+ hint: 'Use `aiwg config set <key> <value>` to change a value headlessly, or `aiwg config edit --print-path` to get the file path and edit it yourself.',
651
+ exitCode: EXIT_CODES.USAGE,
652
+ });
653
+ }
654
+ const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
545
655
  // Ensure the config file exists before opening
546
656
  await config.ensureDir();
547
657
  const { execSync } = await import('child_process');
548
658
  try {
659
+ // sync-spawn: interactive editor-launch — launches $EDITOR/$VISUAL on the
660
+ // config file; the user interacts with it directly (see tools/testing/
661
+ // sync-spawn-allowlist.json). Reached only when isInteractive() is true
662
+ // (#3062) — the no-terminal path above refuses before spawning anything.
663
+ // Not a candidate for a bounded timeout.
549
664
  execSync(`${editor} "${configPath}"`, { stdio: 'inherit' });
550
665
  }
551
666
  catch (err) {
@@ -584,7 +699,7 @@ For project-level config: aiwg config show --project [--json]
584
699
  }
585
700
  // Lazy-import to keep startup cost low for unrelated subcommands.
586
701
  const { readAiwgConfig, resolveRemotes, getProjectDir } = await import('./aiwg-config.js');
587
- const { spawnSync } = await import('child_process');
702
+ const { boundedSpawnSync } = await import('../process/bounded-child-process.mjs');
588
703
  const projectDir = getProjectDir(undefined, args);
589
704
  const cfg = await readAiwgConfig(projectDir);
590
705
  if (!cfg) {
@@ -598,7 +713,7 @@ For project-level config: aiwg config show --project [--json]
598
713
  const resolvedRemotes = resolveRemotes(cfg.remotes);
599
714
  // Resolve each declared remote name to its URL via git. Best-effort.
600
715
  function getUrl(name) {
601
- const r = spawnSync('git', ['-C', projectDir, 'remote', 'get-url', name], {
716
+ const r = boundedSpawnSync('git', ['-C', projectDir, 'remote', 'get-url', name], 'quick', {
602
717
  encoding: 'utf-8',
603
718
  stdio: ['ignore', 'pipe', 'ignore'],
604
719
  });
@@ -691,14 +806,24 @@ For project-level config: aiwg config show --project [--json]
691
806
  console.log('');
692
807
  }
693
808
  async function handleGitignore(args) {
694
- const { spawnSync } = await import('child_process');
809
+ const { boundedSpawnSync } = await import('../process/bounded-child-process.mjs');
695
810
  // Locate the gitignore CLI script relative to this compiled module
696
811
  // (#1228). At runtime this module lives at `dist/src/config/cli.js`,
697
812
  // so we need three `..` segments to reach the package root where
698
813
  // `tools/cli/` is shipped (see package.json `files`).
699
814
  const scriptPath = path.resolve(_scriptDir, '../../../tools/cli/config-gitignore.mjs');
700
- const result = spawnSync(process.execPath, [scriptPath, ...args], { stdio: 'inherit' });
815
+ // Local script editing .gitignore blocks; quick tier (#3062).
816
+ const result = boundedSpawnSync(process.execPath, [scriptPath, ...args], 'quick', { stdio: 'inherit' });
701
817
  if (result.status !== 0) {
818
+ // #3062 review item 7: already correctly treats a null status
819
+ // (timeout/signal) as failure (`process.exit(result.status ?? 1)`
820
+ // below); surface the bounded helper's diagnostic too — `stdio:
821
+ // 'inherit'` means a normal script failure already printed its own
822
+ // output, but a bounded-helper timeout/kill would otherwise never be
823
+ // reported anywhere.
824
+ const { message } = exitCodeForSpawnResult(result, 'aiwg config gitignore');
825
+ if (message)
826
+ console.error(message);
702
827
  process.exit(result.status ?? 1);
703
828
  }
704
829
  }
@@ -707,8 +832,8 @@ function printUsage() {
707
832
 
708
833
  Subcommands:
709
834
  get <key> Read a user config value
710
- get --project <key> Read a project config value (default .aiwg/aiwg.config;
711
- AIWG_ARTIFACTS_PATH may override the artifact root)
835
+ get --project <key> Read a project config value or merged defaults.workerSelection
836
+ [--origins] Include model policy sources, pointers and input digests
712
837
  set <key> <value> Write a user config value
713
838
  set --project <key> <value> Write a project config value (validates enums)
714
839
  list Show all user config
@@ -718,7 +843,8 @@ Subcommands:
718
843
  [--provider gitea|github|local] [--available-label NAME ...]
719
844
  reset [<key>] Reset key or all config to defaults
720
845
  path Print config directory path
721
- edit Open config in $EDITOR
846
+ edit Open config in $EDITOR/$VISUAL (requires a terminal)
847
+ edit --print-path Print the config file path instead of opening an editor
722
848
  gitignore Show/check/fix .gitignore AIWG entries
723
849
 
724
850
  Global flags:
@@ -735,6 +861,7 @@ Examples:
735
861
  aiwg config set --project delivery.mode pr-required
736
862
  aiwg config set --project delivery.merge_style squash
737
863
  aiwg config get --project parallelism
864
+ aiwg config get --project defaults.workerSelection --origins
738
865
  aiwg config set --project parallelism.max_parallel_subagents 6
739
866
  aiwg config reset --project parallelism
740
867
  aiwg config validate
@@ -0,0 +1,38 @@
1
+ /** Shared runtime resolver for config consumers and direct preflight tools. */
2
+ export const DEFAULT_DISPATCH_HOST_THRESHOLDS = Object.freeze({
3
+ min_avail_mem_gb: 8,
4
+ max_load_ratio: 1.0,
5
+ max_mem_psi_avg10: 30,
6
+ max_mem_psi_avg60: 10,
7
+ // 08 brief/evidence retention: stop new admissions when less than 2 GiB free remains.
8
+ min_free_disk_gb: 2,
9
+ });
10
+
11
+ /**
12
+ * Configured layers can only tighten one another and the conservative defaults;
13
+ * minimums take MAX and maximums take MIN across build, admission and task.
14
+ * Validate each layer before merging so an invalid value cannot be hidden.
15
+ */
16
+ export function resolveDispatchHostThresholds(buildDispatch, admissionHostResources, overlay) {
17
+ const configured = { ...DEFAULT_DISPATCH_HOST_THRESHOLDS };
18
+ const keys = [...Object.keys(DEFAULT_DISPATCH_HOST_THRESHOLDS), 'max_workers'];
19
+ for (const layer of [buildDispatch, admissionHostResources, overlay]) {
20
+ if (layer === undefined) continue;
21
+ if (layer === null || typeof layer !== 'object' || Array.isArray(layer)) {
22
+ throw new Error('OG_CONFIG_REFERENCE dispatch thresholds: must be an object');
23
+ }
24
+ for (const key of keys) {
25
+ const value = layer[key];
26
+ if (value === undefined) continue;
27
+ if (!Number.isFinite(value) || value < 0) {
28
+ throw new Error(`OG_CONFIG_REFERENCE dispatch threshold ${key}: must be a non-negative number`);
29
+ }
30
+ if (key === 'max_workers' && (!Number.isInteger(value) || value < 1)) {
31
+ throw new Error('OG_CONFIG_REFERENCE dispatch threshold max_workers: must be an integer >= 1 when set');
32
+ }
33
+ configured[key] = configured[key] === undefined ? value
34
+ : key.startsWith('min_') ? Math.max(configured[key], value) : Math.min(configured[key], value);
35
+ }
36
+ }
37
+ return { ...DEFAULT_DISPATCH_HOST_THRESHOLDS, ...configured };
38
+ }
@@ -10,6 +10,7 @@ import { join, relative, resolve, sep } from 'node:path';
10
10
  import {
11
11
  DEFAULT_PROJECT_AIWG_DIR,
12
12
  PROJECT_AIWG_LOCATION_FILE,
13
+ isProjectArtifactRootExternal,
13
14
  readProjectArtifactLocation,
14
15
  resolveProjectAiwgDir,
15
16
  } from './project-artifacts-runtime.mjs';
@@ -57,8 +58,16 @@ export function auditProjectArtifactHealth(projectDir, env = process.env) {
57
58
  const localRoot = join(root, DEFAULT_PROJECT_AIWG_DIR);
58
59
  const artifactRoot = resolveProjectAiwgDir(root, env);
59
60
  const source = configuredSource(root, env);
60
- const externalConfigured = resolve(localRoot) !== resolve(artifactRoot);
61
- const externalReachable = existsSync(artifactRoot) && statSync(artifactRoot).isDirectory();
61
+ // Cheap, read-only check for whether an external root is configured at all
62
+ // — reused so repair's early refusal (src/artifacts/repair.ts) and this
63
+ // audit agree on the same definition of "local mode" (#274).
64
+ const externalConfigured = isProjectArtifactRootExternal(root, env);
65
+ // In local mode (no external root configured) artifactRoot === localRoot:
66
+ // there is nothing external to reach or compare against. Skip the
67
+ // reachability stat and the full-corpus listing/byte-compare entirely —
68
+ // previously this read and byte-compared every local payload file with
69
+ // itself on every `aiwg status`/`doctor`/`repair` call (#274).
70
+ const externalReachable = externalConfigured && existsSync(artifactRoot) && statSync(artifactRoot).isDirectory();
62
71
 
63
72
  const controls = PROJECT_CONTROL_PLANE_FILES.map((file) => {
64
73
  const localPath = join(localRoot, file);
@@ -69,7 +78,9 @@ export function auditProjectArtifactHealth(projectDir, env = process.env) {
69
78
  });
70
79
  const missingLocal = controls.filter((item) => !item.local).map((item) => item.file);
71
80
  const divergentControl = controls.filter((item) => item.identical === false).map((item) => item.file);
72
- const localPayload = listFiles(localRoot).filter((file) => !PROJECT_CONTROL_PLANE_FILES.includes(file));
81
+ const localPayload = externalConfigured
82
+ ? listFiles(localRoot).filter((file) => !PROJECT_CONTROL_PLANE_FILES.includes(file))
83
+ : [];
73
84
  const divergentPayload = externalReachable
74
85
  ? localPayload.filter((file) => !sameFile(join(localRoot, file), join(artifactRoot, file)))
75
86
  : [];
@@ -245,10 +245,8 @@ export class UserConfig {
245
245
  if (this.configCache) {
246
246
  return this.configCache;
247
247
  }
248
- const configPath = resolve(this.configDir, 'config.yaml');
249
- try {
250
- const content = await readFile(configPath, 'utf-8');
251
- const parsed = parseYamlSimple(content);
248
+ const parsed = await this.readConfig();
249
+ if (parsed) {
252
250
  this.configCache = {
253
251
  ...DEFAULT_USER_CONFIG,
254
252
  ...parsed,
@@ -259,7 +257,7 @@ export class UserConfig {
259
257
  };
260
258
  return this.configCache;
261
259
  }
262
- catch {
260
+ else {
263
261
  // File doesn't exist — return defaults (don't create file yet)
264
262
  this.configCache = {
265
263
  ...DEFAULT_USER_CONFIG,
@@ -271,10 +269,24 @@ export class UserConfig {
271
269
  return this.configCache;
272
270
  }
273
271
  }
272
+ async readConfig() {
273
+ const configPath = resolve(this.configDir, 'config.yaml');
274
+ try {
275
+ const content = await readFile(configPath, 'utf-8');
276
+ return parseYamlSimple(content);
277
+ }
278
+ catch (err) {
279
+ if (err.code === 'ENOENT')
280
+ return null;
281
+ throw new Error(`Failed to load ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
282
+ }
283
+ }
274
284
  /**
275
285
  * Save config.yaml
276
286
  */
277
287
  async saveConfig(config) {
288
+ // Recheck the existing file even when defaults or cached values are being saved.
289
+ await this.readConfig();
278
290
  await this.ensureDir();
279
291
  const configPath = resolve(this.configDir, 'config.yaml');
280
292
  const yaml = serializeYamlSimple(config);
@@ -304,11 +316,15 @@ export function parseYamlSimple(content) {
304
316
  continue;
305
317
  const key = match[1].trim();
306
318
  const rawValue = match[2].trim();
319
+ // Config keys have no legitimate use for prototype-related names at any level.
320
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
321
+ throw new Error('Unsafe config key: prototype-related names are not allowed');
322
+ }
307
323
  if (indent === 0) {
308
324
  if (rawValue === '' || rawValue === undefined) {
309
325
  // Section header
310
326
  currentSection = key;
311
- if (!(key in result)) {
327
+ if (!Object.hasOwn(result, key)) {
312
328
  result[key] = {};
313
329
  }
314
330
  }
@@ -375,7 +391,7 @@ function getNestedValue(obj, path) {
375
391
  const parts = path.split('.');
376
392
  let current = obj;
377
393
  for (const part of parts) {
378
- if (current === null || current === undefined || typeof current !== 'object') {
394
+ if (current === null || current === undefined || typeof current !== 'object' || !Object.hasOwn(current, part)) {
379
395
  return undefined;
380
396
  }
381
397
  current = current[part];
@@ -384,15 +400,25 @@ function getNestedValue(obj, path) {
384
400
  }
385
401
  function setNestedValue(obj, path, value) {
386
402
  const parts = path.split('.');
403
+ // Reject the entire path before mutating config; key uses below also guard each segment.
404
+ if (parts.some(part => part === '__proto__' || part === 'constructor' || part === 'prototype')) {
405
+ throw new Error('Unsafe config key path: prototype-related segments are not allowed');
406
+ }
387
407
  let current = obj;
388
- for (let i = 0; i < parts.length - 1; i++) {
408
+ for (let i = 0; i < parts.length; i++) {
389
409
  const part = parts[i];
390
- if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
410
+ if (part === '__proto__' || part === 'constructor' || part === 'prototype') {
411
+ throw new Error('Unsafe config key path: prototype-related segments are not allowed');
412
+ }
413
+ if (i === parts.length - 1) {
414
+ current[part] = value;
415
+ return;
416
+ }
417
+ if (!Object.hasOwn(current, part) || typeof current[part] !== 'object' || current[part] === null) {
391
418
  current[part] = {};
392
419
  }
393
420
  current = current[part];
394
421
  }
395
- current[parts[parts.length - 1]] = value;
396
422
  }
397
423
  function parseValue(value) {
398
424
  if (value === 'true')