@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,8 @@
8
8
  * @implements #539
9
9
  * @see https://docs.openclaw.ai
10
10
  */
11
- import { execSync } from 'child_process';
11
+ import { boundedExecFileSync } from '../../process/bounded-child-process.mjs';
12
+ import { cliCommand } from '../../process/cli-command.mjs';
12
13
  import fs from 'fs';
13
14
  import path from 'path';
14
15
  import { getProviderDefinition, resolveProviderPathValue, } from '../../providers/provider-definitions.js';
@@ -26,7 +27,8 @@ function resolveSkillFallbackPath(target, projectDir, name) {
26
27
  */
27
28
  function runOpenClaw(args) {
28
29
  try {
29
- return execSync(`openclaw ${args}`, {
30
+ const [file, argv] = cliCommand('openclaw', args);
31
+ return boundedExecFileSync(file, argv, 'quick', {
30
32
  encoding: 'utf-8',
31
33
  timeout: 30000,
32
34
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -142,7 +144,8 @@ export class OpenClawAdapter {
142
144
  if (fs.existsSync(openclawDir))
143
145
  return true;
144
146
  try {
145
- execSync('openclaw --version 2>/dev/null', { encoding: 'utf-8' });
147
+ const [file, args] = cliCommand('openclaw', ['--version']);
148
+ boundedExecFileSync(file, args, 'quick', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
146
149
  return true;
147
150
  }
148
151
  catch {
@@ -153,7 +156,7 @@ export class OpenClawAdapter {
153
156
  if (this.cachedSkills)
154
157
  return this.cachedSkills;
155
158
  // Try openclaw CLI with JSON output first
156
- const jsonOutput = runOpenClaw('skills list --json 2>/dev/null');
159
+ const jsonOutput = runOpenClaw(['skills', 'list', '--json']);
157
160
  if (jsonOutput) {
158
161
  try {
159
162
  const parsed = JSON.parse(jsonOutput);
@@ -178,7 +181,7 @@ export class OpenClawAdapter {
178
181
  }
179
182
  async search(query) {
180
183
  // Try CLI delegation first
181
- const cliOutput = runOpenClaw(`skills search "${query}" --json 2>/dev/null`);
184
+ const cliOutput = runOpenClaw(['skills', 'search', query, '--json']);
182
185
  if (cliOutput) {
183
186
  try {
184
187
  const parsed = JSON.parse(cliOutput);
@@ -207,7 +210,7 @@ export class OpenClawAdapter {
207
210
  const skillMdPath = skillDir ? path.join(skillDir, 'SKILL.md') : '';
208
211
  if (!skillMdPath || !fs.existsSync(skillMdPath)) {
209
212
  // Try CLI
210
- const cliOutput = runOpenClaw(`skills info "${name}" --json 2>/dev/null`);
213
+ const cliOutput = runOpenClaw(['skills', 'info', name, '--json']);
211
214
  if (cliOutput) {
212
215
  try {
213
216
  const parsed = JSON.parse(cliOutput);
@@ -268,7 +271,7 @@ export class OpenClawAdapter {
268
271
  }
269
272
  async install(name, options) {
270
273
  // Try openclaw CLI install first
271
- const cliResult = runOpenClaw(`skills install "${name}" 2>/dev/null`);
274
+ const cliResult = runOpenClaw(['skills', 'install', name]);
272
275
  if (cliResult !== null) {
273
276
  // CLI handled it — if target is different from openclaw, copy to target
274
277
  if (options.target && options.target !== 'openclaw') {
@@ -13,9 +13,135 @@
13
13
  *
14
14
  * @implements #1362
15
15
  */
16
- import { readAiwgConfig, resolveParallelism, PROVIDER_PARALLELISM_DEFAULTS } from '../../config/aiwg-config.js';
16
+ import { dirname } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { findPackageRoot } from '../../cli/find-package-root.js';
19
+ import { readAiwgConfigCandidate, validateWorkerConfigCandidate, resolveParallelism, PROVIDER_PARALLELISM_DEFAULTS } from '../../config/aiwg-config.js';
17
20
  export const PARALLELISM_BLOCK_START = '<!-- AIWG-PARALLELISM-CAP:START -->';
18
21
  export const PARALLELISM_BLOCK_END = '<!-- AIWG-PARALLELISM-CAP:END -->';
22
+ /**
23
+ * One-line note rendered when the bundled model defaults cannot be loaded.
24
+ * The orchestration health block is omitted; the rest of the section renders
25
+ * unchanged so context regeneration never throws for a missing home install.
26
+ */
27
+ export const PARALLELISM_HEALTH_UNRESOLVED_NOTE = 'Health values could not be resolved from the bundled defaults, so the orchestration health block is omitted.';
28
+ /**
29
+ * Locate this package's bundled AIWG defaults (`models-v2.json` under
30
+ * `agentic/...`) from the running code, whether it runs from a source
31
+ * checkout or an installed package. Uses the canonical package-root walk
32
+ * shared with the rest of src/ (`findPackageRoot`), never a hand-rolled
33
+ * relative path and never the user's home install.
34
+ */
35
+ function resolveBundledAiwgHome() {
36
+ try {
37
+ return findPackageRoot(dirname(fileURLToPath(import.meta.url))) ?? undefined;
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ /**
44
+ * Resolve the AIWG home to pass to `ConfigLoader` for this generator's own
45
+ * read: an explicit, non-empty `AIWG_HOME` always wins; otherwise fall back
46
+ * to this package's own bundled defaults so regeneration does not depend on
47
+ * what (if anything) is installed at the user's home. This function only
48
+ * ever READS `process.env.AIWG_HOME` — it never sets or deletes it, so
49
+ * concurrent callers and any spawned children never observe a value this
50
+ * generator did not itself receive from the real environment.
51
+ */
52
+ function resolveAiwgHome() {
53
+ return process.env.AIWG_HOME || resolveBundledAiwgHome();
54
+ }
55
+ function isBundledDefaultsFailure(error) {
56
+ return error instanceof Error && error.message.includes('Failed to load AIWG default config');
57
+ }
58
+ function oneLine(value, maxLen) {
59
+ return value.replace(/[\r\n|]+/g, ' ').trim().slice(0, maxLen);
60
+ }
61
+ /** At most this many group summaries render; the rest collapse to "+N more". */
62
+ export const MAX_SHOWN_GROUPS = 8;
63
+ /**
64
+ * Config-level member exclusions for one group, mirroring the static gates
65
+ * in `resolveWorkerPlan()` (`src/models/resolver.ts`): a member or its CLI
66
+ * installation flagged `enabled: false` is statically excluded. This is
67
+ * NOT the live eligibility verdict — `resolveWorkerPlan()` resolves
68
+ * eligibility per (group, member, effort) tuple against the current
69
+ * catalog and request, so a member excluded nowhere here can still be
70
+ * excluded for a specific role/task/effort, and vice versa is never true
71
+ * (a statically excluded member is always excluded live too). Only the
72
+ * per-member reasons render; the group itself is never labeled
73
+ * "eligible"/"not eligible" — see the request-scoped resolve guidance
74
+ * below the group list.
75
+ *
76
+ * A `requiredRoles` entry with no enabled covering member is a config
77
+ * error `validateParallelismConfig()` (and `resolveParallelism()` /
78
+ * `ConfigLoader.load()`, both called earlier in `buildParallelismSection()`)
79
+ * already rejects outright — a config reaching this function can never
80
+ * have one, so that case is not re-rendered here.
81
+ */
82
+ function excludedMembers(parallelism, group) {
83
+ const excluded = [];
84
+ for (const member of group.members) {
85
+ if (member.enabled === false)
86
+ excluded.push(`${member.id} (member disabled)`);
87
+ else if (parallelism?.clis?.[member.cli]?.enabled === false)
88
+ excluded.push(`${member.id} (installation ${member.cli} disabled)`);
89
+ }
90
+ return excluded;
91
+ }
92
+ function groupSummaries(entries, parallelism) {
93
+ return entries.map(([id, group]) => {
94
+ const roles = [...new Set(group.members.flatMap(m => m.roles ?? []))].join('/');
95
+ const clis = [...new Set(group.members.map(m => m.cli))].join('/');
96
+ const parts = [`${oneLine(id, 64)} (members: ${group.members.length}`];
97
+ if (clis)
98
+ parts.push(`clis: ${oneLine(clis, 96)}`);
99
+ if (roles)
100
+ parts.push(`roles: ${oneLine(roles, 96)}`);
101
+ if (group.maxParallel !== undefined)
102
+ parts.push(`maxParallel: ${group.maxParallel}`);
103
+ if (group.requiredRoles?.length)
104
+ parts.push(`requiredRoles: ${group.requiredRoles.join('/')}`);
105
+ parts.push('configured');
106
+ const excluded = excludedMembers(parallelism, group);
107
+ if (excluded.length)
108
+ parts.push(`excluded members: ${oneLine(excluded.join(', '), 160)}`);
109
+ return parts.join('; ') + ')';
110
+ });
111
+ }
112
+ /**
113
+ * One configured health window with its origin. The origin mirrors
114
+ * `resolveWorkerEffectiveHealth()` (`src/serve/worker-invocation.ts`): a
115
+ * field the project config leaves unset resolves from the built-in defaults.
116
+ */
117
+ function healthValue(health, field, fallback) {
118
+ const value = health?.[field] ?? fallback;
119
+ return { value, origin: health?.[field] === undefined ? 'default' : 'project config' };
120
+ }
121
+ /**
122
+ * Plain-language dispatch state line (#3050 item 5). Agents must not assume
123
+ * other worker CLIs are unavailable just because dispatch is off — they
124
+ * should detect them and offer the opt-in, not silently work around it.
125
+ */
126
+ function workerDispatchStateLine(parallelism, setup) {
127
+ if (parallelism?.worker_dispatch === true) {
128
+ return '**Direct worker dispatch**: on (`parallelism.worker_dispatch`).';
129
+ }
130
+ // A recorded decline (#3050 review item 3) means a person already said no
131
+ // to this offer — render the plain off state without repeating the
132
+ // detect-and-offer instruction, so agents stop re-litigating a settled
133
+ // decision every session. The decline is a setup/onboarding preference,
134
+ // not worker policy, so it lives in `setup`, not in the frozen
135
+ // `parallelism` contract fragment (B-OG-29).
136
+ if (setup?.worker_dispatch_offer === 'declined') {
137
+ return '**Direct worker dispatch**: off (`parallelism.worker_dispatch`); a prior offer was declined '
138
+ + '(`setup.worker_dispatch_offer`). Do not re-offer it. It can still be turned on with '
139
+ + '`aiwg config set --project parallelism.worker_dispatch true` if the user asks for it directly.';
140
+ }
141
+ return '**Direct worker dispatch**: off (`parallelism.worker_dispatch`). Do not assume other worker CLIs '
142
+ + 'are unavailable — detect them yourself and offer `aiwg config set --project parallelism.worker_dispatch true` '
143
+ + 'to the user if it would help.';
144
+ }
19
145
  function delegationSupport(provider) {
20
146
  switch (provider) {
21
147
  case 'antigravity':
@@ -45,12 +171,34 @@ function delegationSupport(provider) {
45
171
  * - `AIWG_HIDE_PARALLELISM_IN_CONTEXT=1` is set
46
172
  * - No `.aiwg/aiwg.config` exists at the project path
47
173
  */
48
- export async function buildParallelismSection(projectPath) {
174
+ export async function buildParallelismSection(projectPath, deps = {}) {
49
175
  if (process.env.AIWG_HIDE_PARALLELISM_IN_CONTEXT === '1')
50
176
  return '';
51
- const cfg = await readAiwgConfig(projectPath);
177
+ // Step 1: the project config, independently validated except for the
178
+ // worker-model defaults (never throws solely because bundled defaults are
179
+ // unreachable — any error here is a genuine config problem and stays fatal).
180
+ const cfg = deps.readConfig
181
+ ? await deps.readConfig(projectPath)
182
+ : await readAiwgConfigCandidate(projectPath);
52
183
  if (!cfg)
53
184
  return '';
185
+ // Step 2: worker-model defaults validation, attempted only when dispatch
186
+ // is enabled. A bundled-defaults failure degrades (health/group details
187
+ // are omitted below with a one-line note); every other error stays fatal.
188
+ let healthUnresolved = false;
189
+ if (cfg.parallelism?.worker_dispatch === true) {
190
+ try {
191
+ if (deps.validateWorkerConfig)
192
+ await deps.validateWorkerConfig(projectPath, cfg);
193
+ else
194
+ await validateWorkerConfigCandidate(projectPath, cfg, { aiwgHome: resolveAiwgHome() });
195
+ }
196
+ catch (error) {
197
+ if (!isBundledDefaultsFailure(error))
198
+ throw error;
199
+ healthUnresolved = true;
200
+ }
201
+ }
54
202
  const primary = cfg.providers[0];
55
203
  const resolved = resolveParallelism(cfg.parallelism, primary);
56
204
  const providerDefaults = primary && PROVIDER_PARALLELISM_DEFAULTS[primary];
@@ -98,6 +246,47 @@ export async function buildParallelismSection(projectPath) {
98
246
  lines.push('');
99
247
  lines.push(delegationSupport(primary));
100
248
  lines.push('');
249
+ lines.push(workerDispatchStateLine(cfg.parallelism, cfg.setup));
250
+ lines.push('');
251
+ // Group dispatch disabled (worker_dispatch unset/false): the resolver reports
252
+ // `disabled`, so no group block renders — the native-delegation rubric above
253
+ // applies unchanged and the output stays byte-identical to the pre-dispatch
254
+ // rendering.
255
+ if (cfg.parallelism?.worker_dispatch === true && healthUnresolved) {
256
+ lines.push(PARALLELISM_HEALTH_UNRESOLVED_NOTE);
257
+ lines.push('');
258
+ }
259
+ else if (cfg.parallelism?.worker_dispatch === true) {
260
+ const health = cfg.parallelism?.health;
261
+ const check = healthValue(health, 'checkSeconds', 15);
262
+ const startup = healthValue(health, 'startupSeconds', 60);
263
+ const quiet = healthValue(health, 'quietSeconds', 90);
264
+ const stall = healthValue(health, 'stallSeconds', 300);
265
+ const hard = healthValue(health, 'hardTimeoutSeconds', 1800);
266
+ lines.push('### Orchestration groups (group dispatch enabled)');
267
+ lines.push('');
268
+ const entries = Object.entries(cfg.parallelism?.groups ?? {});
269
+ if (entries.length > 0) {
270
+ // Slice before summarizing so at most MAX_SHOWN_GROUPS summaries are built.
271
+ const shown = groupSummaries(entries.slice(0, MAX_SHOWN_GROUPS), cfg.parallelism).join(' | ');
272
+ const hidden = entries.length - MAX_SHOWN_GROUPS;
273
+ lines.push(`Configured groups: ${shown}${hidden > 0 ? ` (+${hidden} more)` : ''}`);
274
+ }
275
+ else {
276
+ lines.push('Configured groups: none — group dispatch is enabled but no groups are configured; the native delegation rubric above applies unchanged.');
277
+ }
278
+ lines.push('');
279
+ lines.push('A group above shown as configured, with excluded members or an uncovered required role listed, is a static config read — not a live eligibility verdict. Eligibility is resolved per (group, member, effort) tuple against the current catalog and request; see `aiwg models resolve` below to check a specific role/task/group.');
280
+ lines.push('');
281
+ lines.push('Tier 0 direct handoff is the default: use your own shell or a qualified MCP worker tool. No Mission/MC session, scheduler, daemon, or activation marker is needed. Self-count active native + CLI + MCP workers, take the minimum of the project, group, CLI, model, and context caps, and launch each writing task once.');
282
+ lines.push('');
283
+ lines.push('Resolve each bounded brief with `aiwg models resolve --role <role> --task <class> --group <group> --brief <file> --workdir <tree> --final-output <file> --transport shell --format shell`. Copy the exact rendered command or the MCP server/tool/arguments byte-for-byte; never assemble vendor flags from memory. Re-resolve after any brief, profile, model, or workspace change. `--format shell` prints only the launch command: to see the resolved health windows for an attempt, use `--format json` (the default) and read `invocation.health` with its per-field `origins`.');
284
+ lines.push('');
285
+ lines.push(`Configured health windows (each followed by its origin — \`default\` or \`project config\`): check every ${check.value}s (${check.origin}); confirm startup within ${startup.value}s (${startup.origin}); quiet limit ${quiet.value}s (${quiet.origin}); stall limit ${stall.value}s (${stall.origin}); hard stop ${hard.value}s (${hard.origin}). Per-attempt windows scale quiet and stall by the single largest applicable role/effort/model factor, never their product (for example, reasoning at \`xhigh\` effort resolves 180s/600s from the 90s/300s defaults; an effective 1.25 scale gives 112.5s/375s). Only the hard deadline and its diagnostic/termination grace periods are capped by the remaining wall-clock and transport ceilings; startup, quiet, and stall keep their configured or scaled values, and \`truncatedWindows\` instead records when one of them would exceed that capped deadline (\`resolveWorkerEffectiveHealth()\` in \`src/serve/worker-invocation.ts\`). Resolved values win over these configured values.`);
286
+ lines.push('');
287
+ lines.push('Supervise by observer state, never by suspicion. `quiet` (no observed activity for the quiet window, reported with confidence `suspected`) is not a stop: read the status (`aiwg ralph-external --status --workers --runs-root <dir> --json` is read-only and never kills or retries), check owned-worktree progress, and keep waiting — the observer’s `shouldStop()` stays false. `stalled` (no progress for the stall window) opens diagnosis through the diagnostic grace period; only `hung` (still no progress after grace, with the process live and connected) authorizes a no-progress stop — and only a worker this conductor owns. The launcher also stops independently of progress state on `failed-to-start`, the hard wall-clock deadline (`HARD_DEADLINE`), the step-count limit (`STEP_LIMIT`), and a fatal observer-record failure (`tools/ralph-external/session-launcher.mjs`). Confirm the stop (a sent signal, lease expiry, or final-file presence is not proof — only a confirmed exit is), preserve partial work for independent verification, then re-resolve within remaining attempts and budgets. Observer loss (`OBSERVER_GAP`, `DISCONNECTED`, degraded or unobserved coverage, opaque MCP output) is unknown, never worker failure: keep polling owned run files and processes from the shell at the same cadence. Heartbeats and log bytes are not progress. On correlated failure across workers sharing one account, quota, or host, pause further launches there and report cause and scope.');
288
+ lines.push('');
289
+ }
101
290
  lines.push(PARALLELISM_BLOCK_END);
102
291
  lines.push('');
103
292
  return lines.join('\n');
@@ -7,7 +7,8 @@
7
7
  * an operator runs the explicit migration workflow.
8
8
  */
9
9
  import { createHash } from 'node:crypto';
10
- import { promises as fs } from 'node:fs';
10
+ import { promises as fs, existsSync } from 'node:fs';
11
+ import { homedir } from 'node:os';
11
12
  import * as path from 'node:path';
12
13
  import { buildAiwgMdContent } from './aiwg-md.js';
13
14
  import { buildNormalizedAiwgMd } from './finalization.js';
@@ -592,11 +593,106 @@ function sensitiveEvidence(content) {
592
593
  }
593
594
  return findings;
594
595
  }
596
+ // Directory names the context-graph walk always skips, regardless of where
597
+ // the walk root is: VCS/build output that is never hand-authored project
598
+ // content (#277, pre-existing) plus two reserved, self-identifying names
599
+ // that are never legitimately a real project directory.
600
+ const CONTEXT_WALK_ALWAYS_IGNORED_DIR_NAMES = new Set([
601
+ '.git', 'node_modules', 'dist', 'build', 'coverage', '.aiwg',
602
+ ]);
603
+ // Cache/registry directory names that are only skipped when the walk root
604
+ // is the home directory itself (#277 fix-round). A project subtree that
605
+ // happens to be named `.cache`, `target`, `.gradle`, etc. is NOT generated
606
+ // data and must still be scanned — these names are only reliably "cache,
607
+ // not project" when they sit directly under $HOME. `Library/Caches` and
608
+ // `.local/share` are two-segment (parent + name) patterns, handled
609
+ // separately below.
610
+ const HOME_LEVEL_CACHE_DIR_NAMES = new Set([
611
+ '.cache', '.cargo', '.rustup', '.npm', '.nvm', '.pnpm-store',
612
+ '.gradle', '.m2', 'Trash',
613
+ ]);
614
+ /** True when `dirPath` contains `markerFile` — a cheap, synchronous, read-only check. */
615
+ function hasMarkerFile(dirPath, markerFile) {
616
+ return existsSync(path.join(dirPath, markerFile));
617
+ }
618
+ /** A `target` directory is Cargo/build output only when it carries the
619
+ * standard `CACHEDIR.TAG` marker, or sits next to a `Cargo.toml` — not
620
+ * merely because it is named `target` (#277 fix-round). */
621
+ function isGeneratedTargetDir(dirPath) {
622
+ if (hasMarkerFile(dirPath, 'CACHEDIR.TAG'))
623
+ return true;
624
+ return existsSync(path.join(path.dirname(dirPath), 'Cargo.toml'));
625
+ }
626
+ /** A `venv`/`.venv` directory is a Python virtualenv only when it carries
627
+ * `pyvenv.cfg` — not merely because of its name (#277 fix-round). */
628
+ function isPythonVenvDir(dirPath) {
629
+ return hasMarkerFile(dirPath, 'pyvenv.cfg');
630
+ }
631
+ /**
632
+ * Whether the context-graph walk should skip `entryName` (a directory
633
+ * directly under `directory`) without descending into it.
634
+ *
635
+ * `isHomeRoot` gates the home-level cache list and the `Library/Caches` /
636
+ * `.local/share` two-segment patterns — they apply only when the overall
637
+ * walk root is $HOME, never for an arbitrary project subtree that happens
638
+ * to share one of those names (#277 fix-round).
639
+ */
640
+ export function shouldSkipContextWalkDir(directory, entryName, isHomeRoot) {
641
+ if (CONTEXT_WALK_ALWAYS_IGNORED_DIR_NAMES.has(entryName))
642
+ return true;
643
+ if (entryName.startsWith('.context-migration-'))
644
+ return true;
645
+ const childPath = path.join(directory, entryName);
646
+ if (entryName === 'target' && isGeneratedTargetDir(childPath))
647
+ return true;
648
+ if ((entryName === 'venv' || entryName === '.venv') && isPythonVenvDir(childPath))
649
+ return true;
650
+ // Codex's own plugin/temp scratch — a known, specific noise source named
651
+ // in the original report, not a broad `.tmp` exclusion (#277 fix-round).
652
+ if (entryName === '.tmp' && path.basename(directory) === '.codex')
653
+ return true;
654
+ if (isHomeRoot) {
655
+ if (HOME_LEVEL_CACHE_DIR_NAMES.has(entryName))
656
+ return true;
657
+ if (entryName === 'Caches' && path.basename(directory) === 'Library')
658
+ return true;
659
+ if (entryName === 'share' && path.basename(directory) === '.local')
660
+ return true;
661
+ }
662
+ return false;
663
+ }
664
+ /** Walk depth/entry bounds for the context-graph walk (#277). A hit sets
665
+ * `truncated: true` on the result instead of silently stopping. Overridable
666
+ * via environment for tests that need to exercise the bound without
667
+ * building a correspondingly large fixture tree. */
668
+ function readPositiveIntEnv(name, fallback) {
669
+ const raw = process.env[name];
670
+ if (!raw)
671
+ return fallback;
672
+ const parsed = Number.parseInt(raw, 10);
673
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
674
+ }
675
+ const CONTEXT_WALK_MAX_DEPTH = readPositiveIntEnv('AIWG_CONTEXT_WALK_MAX_DEPTH', 10);
676
+ const CONTEXT_WALK_MAX_DIRECTORIES = readPositiveIntEnv('AIWG_CONTEXT_WALK_MAX_DIRECTORIES', 20_000);
595
677
  async function nestedInstructionFiles(projectPath) {
596
678
  const found = [];
597
- const ignored = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.aiwg']);
598
679
  const artifactRoot = path.resolve(resolveProjectAiwgDir(projectPath));
599
- async function walk(directory) {
680
+ // Running from $HOME is the exact shape of #277: bound the walk to the
681
+ // top level and apply the home-level cache exclusions instead of
682
+ // recursing into every cache/registry/venv underneath it. A markerless
683
+ // but otherwise ordinary (non-home) project root keeps the full bounded
684
+ // walk — "no project marker" is not evidence the directory is generated
685
+ // data (#277 fix-round).
686
+ const isHomeDirectory = path.resolve(projectPath) === path.resolve(homedir());
687
+ const maxDepth = isHomeDirectory ? 1 : CONTEXT_WALK_MAX_DEPTH;
688
+ let visitedDirectories = 0;
689
+ let truncated = false;
690
+ async function walk(directory, depth) {
691
+ if (depth > maxDepth || visitedDirectories >= CONTEXT_WALK_MAX_DIRECTORIES) {
692
+ truncated = true;
693
+ return;
694
+ }
695
+ visitedDirectories += 1;
600
696
  let entries;
601
697
  try {
602
698
  entries = await fs.readdir(directory, { withFileTypes: true });
@@ -605,12 +701,14 @@ async function nestedInstructionFiles(projectPath) {
605
701
  return;
606
702
  }
607
703
  for (const entry of entries) {
704
+ // Dirent.isDirectory() is false for symlinks (it reflects the entry
705
+ // itself, not its target), so symlinked directories are never
706
+ // followed out of the root by this walk.
608
707
  if (entry.isDirectory()) {
609
708
  const child = path.join(directory, entry.name);
610
- if (!ignored.has(entry.name)
611
- && !entry.name.startsWith('.context-migration-')
709
+ if (!shouldSkipContextWalkDir(directory, entry.name, isHomeDirectory)
612
710
  && path.resolve(child) !== artifactRoot)
613
- await walk(child);
711
+ await walk(child, depth + 1);
614
712
  }
615
713
  else if (['AGENTS.md', 'CLAUDE.md', 'WARP.md'].includes(entry.name)) {
616
714
  const relative = path.relative(projectPath, path.join(directory, entry.name)).replace(/\\/g, '/');
@@ -621,8 +719,8 @@ async function nestedInstructionFiles(projectPath) {
621
719
  }
622
720
  }
623
721
  }
624
- await walk(projectPath);
625
- return found.sort((a, b) => a.localeCompare(b));
722
+ await walk(projectPath, 1);
723
+ return { files: found.sort((a, b) => a.localeCompare(b)), truncated };
626
724
  }
627
725
  async function migratedProviderFiles(projectPath) {
628
726
  const directory = projectAiwgPath(projectPath, 'context', 'providers');
@@ -638,10 +736,11 @@ async function migratedProviderFiles(projectPath) {
638
736
  }
639
737
  export async function auditWorkspaceContext(projectPath) {
640
738
  const extractedProject = await extractExistingProjectContext(projectPath);
739
+ const nested = await nestedInstructionFiles(projectPath);
641
740
  const candidates = new Set([
642
741
  'WORKSPACE.md',
643
742
  ...LEGACY_ROOT_FILES,
644
- ...await nestedInstructionFiles(projectPath),
743
+ ...nested.files,
645
744
  ...await migratedProviderFiles(projectPath),
646
745
  ]);
647
746
  const sources = [];
@@ -740,6 +839,7 @@ export async function auditWorkspaceContext(projectPath) {
740
839
  overlaps,
741
840
  conflicts,
742
841
  sensitiveFindings,
842
+ contextWalkTruncated: nested.truncated,
743
843
  plan: {
744
844
  routing,
745
845
  scopeReview,
@@ -1039,6 +1139,13 @@ export async function workspaceLinkedFiles(projectPath) {
1039
1139
  export async function diagnoseWorkspaceContext(projectPath) {
1040
1140
  const diagnostics = [];
1041
1141
  const audit = await auditWorkspaceContext(projectPath);
1142
+ if (audit.contextWalkTruncated) {
1143
+ diagnostics.push({
1144
+ severity: 'info',
1145
+ code: 'context-walk-truncated',
1146
+ message: 'Nested-instruction-file walk stopped early (depth/entry bound reached, or the root looks like $HOME or has no project markers); the context graph may be incomplete.',
1147
+ });
1148
+ }
1042
1149
  const workspacePath = path.join(projectPath, 'WORKSPACE.md');
1043
1150
  const workspace = await readOptional(workspacePath);
1044
1151
  if (!workspace) {
@@ -13,6 +13,7 @@ import { promises as fs } from 'node:fs';
13
13
  import * as path from 'node:path';
14
14
  import { homedir } from 'node:os';
15
15
  import { substituteEnvVars } from './shim.js';
16
+ import { tomlString } from '../../mcp/toml-string.mjs';
16
17
  /** Codex-native event names (subset; extend when verified against codex-rs). */
17
18
  const CODEX_EVENT_MAP = {
18
19
  PreToolUse: 'before_tool',
@@ -44,16 +45,16 @@ export function renderCodexHookToml(source) {
44
45
  const subCommand = substituteEnvVars(source.command, 'codex');
45
46
  const subArgs = (source.args || []).map((a) => substituteEnvVars(a, 'codex'));
46
47
  lines.push(`[[hooks.${codexEvent}]]`);
47
- lines.push(`# AIWG-managed hook: ${source.id} — ${source.description}`);
48
- lines.push(`command = "${subCommand.replace(/"/g, '\\"')}"`);
48
+ lines.push(`# AIWG-managed hook: ${`${source.id} — ${source.description}`.replace(/[\x00-\x1f\x7f]/g, ' ')}`);
49
+ lines.push(`command = ${tomlString(subCommand)}`);
49
50
  if (subArgs.length > 0) {
50
- lines.push(`args = [${subArgs.map((a) => `"${a.replace(/"/g, '\\"')}"`).join(', ')}]`);
51
+ lines.push(`args = [${subArgs.map(tomlString).join(', ')}]`);
51
52
  }
52
53
  if (source.safetyCritical) {
53
54
  lines.push(`# safety-critical: cannot be auto-removed without --allow-unsafe-shadow`);
54
55
  }
55
56
  lines.push(`_aiwg_managed = true`);
56
- lines.push(`_aiwg_id = "${source.id}"`);
57
+ lines.push(`_aiwg_id = ${tomlString(source.id)}`);
57
58
  lines.push('');
58
59
  }
59
60
  return lines.join('\n');
@@ -95,7 +96,7 @@ export async function translateForCodex(source, options) {
95
96
  result.skipReason = 'degrade-on declared codex';
96
97
  return result;
97
98
  }
98
- const configPath = path.join(homedir(), '.codex', 'config.toml');
99
+ const configPath = path.join(process.env.CODEX_HOME || path.join(homedir(), '.codex'), 'config.toml');
99
100
  const block = renderCodexHookToml(source);
100
101
  if (block.trim().length === 0) {
101
102
  result.skipped = true;
@@ -99,7 +99,7 @@ async function main() {
99
99
  console.log(` node ${generatedServer.files.server}`);
100
100
  console.log();
101
101
  console.log(' To register with Claude Code:');
102
- console.log(' Add to .claude/settings.local.json:');
102
+ console.log(' Add to .mcp.json:');
103
103
  console.log(' {');
104
104
  console.log(' "mcpServers": {');
105
105
  console.log(' "git": {');
@@ -7,7 +7,7 @@
7
7
  * @architecture @.aiwg/architecture/toolsmith-implementation-spec.md
8
8
  */
9
9
 
10
- import { execSync } from 'child_process';
10
+ import { boundedExecFileSync, boundedExecSync } from '../../process/bounded-child-process.mjs';
11
11
  import { readFile, writeFile, mkdir, access } from 'fs/promises';
12
12
  import { existsSync, constants } from 'fs';
13
13
  import { resolve, join, dirname } from 'path';
@@ -140,10 +140,10 @@ export class RuntimeDiscovery {
140
140
  }
141
141
  }
142
142
  } else if (os === 'darwin') {
143
- const version = execSync('sw_vers -productVersion', { encoding: 'utf-8' }).trim();
143
+ const version = boundedExecFileSync('sw_vers', ['-productVersion'], 'quick', { encoding: 'utf-8' }).trim();
144
144
  osVersion = `macOS ${version}`;
145
145
  } else if (os === 'win32') {
146
- osVersion = execSync('ver', { encoding: 'utf-8' }).trim();
146
+ osVersion = boundedExecSync('ver', 'quick', { encoding: 'utf-8' }).trim();
147
147
  }
148
148
  } catch (error) {
149
149
  console.warn('Could not detect OS version:', error.message);
@@ -172,7 +172,7 @@ export class RuntimeDiscovery {
172
172
  // Disk space
173
173
  if (platform() === 'win32') {
174
174
  // Windows: use wmic
175
- const output = execSync('wmic logicaldisk get size,freespace', { encoding: 'utf-8' });
175
+ const output = boundedExecFileSync('wmic', ['logicaldisk', 'get', 'size,freespace'], 'quick', { encoding: 'utf-8' });
176
176
  const lines = output.trim().split('\n').slice(1);
177
177
  let totalFree = 0;
178
178
  for (const line of lines) {
@@ -184,7 +184,7 @@ export class RuntimeDiscovery {
184
184
  diskFreeGb = Math.round((totalFree / (1024 ** 3)) * 10) / 10;
185
185
  } else {
186
186
  // Unix: use df
187
- const output = execSync('df -k .', { encoding: 'utf-8' });
187
+ const output = boundedExecFileSync('df', ['-k', '.'], 'quick', { encoding: 'utf-8' });
188
188
  const lines = output.trim().split('\n');
189
189
  if (lines.length > 1) {
190
190
  const parts = lines[1].trim().split(/\s+/);
@@ -365,7 +365,7 @@ export class RuntimeDiscovery {
365
365
  async #findToolPath(toolName) {
366
366
  try {
367
367
  const cmd = platform() === 'win32' ? 'where' : 'which';
368
- const path = execSync(`${cmd} ${toolName}`, { encoding: 'utf-8' })
368
+ const path = boundedExecFileSync(cmd, [toolName], 'quick', { encoding: 'utf-8' })
369
369
  .trim()
370
370
  .split('\n')[0];
371
371
  return path;
@@ -393,15 +393,11 @@ export class RuntimeDiscovery {
393
393
  // Bare `version` (positional) is intentionally excluded: it caused
394
394
  // BPF tools to attach tracepoints, input-leapc to connect to
395
395
  // hostname "version", and cs2cs to open a file named "version".
396
- const versionCommands = [
397
- `${toolPath} --version`,
398
- `${toolPath} -v`,
399
- `${toolPath} -V`
400
- ];
396
+ const versionArgs = ['--version', '-v', '-V'];
401
397
 
402
- for (const cmd of versionCommands) {
398
+ for (const arg of versionArgs) {
403
399
  try {
404
- const output = execSync(cmd, {
400
+ const output = boundedExecFileSync(toolPath, [arg], 'quick', {
405
401
  encoding: 'utf-8',
406
402
  timeout: 3000,
407
403
  killSignal: 'SIGKILL',
@@ -503,7 +499,7 @@ export class RuntimeDiscovery {
503
499
  if (platform() === 'win32') return undefined;
504
500
 
505
501
  try {
506
- const manPath = execSync(`man -w ${toolName}`, {
502
+ const manPath = boundedExecFileSync('man', ['-w', toolName], 'quick', {
507
503
  encoding: 'utf-8',
508
504
  stdio: ['ignore', 'pipe', 'ignore']
509
505
  }).trim();