@aiwg/cli 2026.10.0 → 2026.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) 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 +297 -2
  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 +80 -28
  38. package/dist/src/decision/ask.js +352 -0
  39. package/dist/src/decision/driver.js +4 -0
  40. package/dist/src/decision/entry.js +38 -6
  41. package/dist/src/decision/heldout/collector.js +10 -1
  42. package/dist/src/decision/index.js +3 -0
  43. package/dist/src/decision/interop.js +60 -8
  44. package/dist/src/decision/jev-credentials.js +121 -0
  45. package/dist/src/decision/ledger.js +839 -0
  46. package/dist/src/decision/validate.js +6 -3
  47. package/dist/src/effects/dispatch-verifier.js +252 -0
  48. package/dist/src/effects/index.js +1 -0
  49. package/dist/src/features/installer.js +2 -2
  50. package/dist/src/flow/graph-metadata.js +5 -0
  51. package/dist/src/gates/providers/loader.js +3 -1
  52. package/dist/src/installation/manager-command.mjs +1 -1
  53. package/dist/src/installation/manager.mjs +17 -2
  54. package/dist/src/lint/runner.js +2 -2
  55. package/dist/src/mcp/adapters/codex-runtime.js +360 -56
  56. package/dist/src/mcp/cli.mjs +587 -77
  57. package/dist/src/mcp/config-file.mjs +279 -0
  58. package/dist/src/mcp/config-layers.mjs +348 -0
  59. package/dist/src/mcp/credentials.mjs +484 -0
  60. package/dist/src/mcp/grok-build-config.mjs +16 -26
  61. package/dist/src/mcp/helpers.mjs +106 -2
  62. package/dist/src/mcp/omp-config.mjs +50 -29
  63. package/dist/src/mcp/profiles.js +97 -23
  64. package/dist/src/mcp/profiles.mjs +99 -27
  65. package/dist/src/mcp/registry.js +264 -57
  66. package/dist/src/mcp/registry.mjs +198 -45
  67. package/dist/src/mcp/server.mjs +403 -20
  68. package/dist/src/mcp/toml-string.mjs +9 -0
  69. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  70. package/dist/src/mcp/tool-filters.mjs +357 -0
  71. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  72. package/dist/src/mcp/tools/decision.mjs +58 -0
  73. package/dist/src/mcp/tools/orchestration.mjs +38 -0
  74. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  75. package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
  76. package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
  77. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  78. package/dist/src/memory/project-registry.js +2 -2
  79. package/dist/src/mission-protocol/codecs.js +639 -1
  80. package/dist/src/mission-protocol/types.js +110 -0
  81. package/dist/src/mission-protocol/worker-status.js +2 -0
  82. package/dist/src/models/config-loader.js +70 -13
  83. package/dist/src/models/grok-build-models.js +93 -41
  84. package/dist/src/models/model-capabilities.v1.json +3091 -24
  85. package/dist/src/models/model-discovery.js +862 -63
  86. package/dist/src/models/provider-policy.js +625 -11
  87. package/dist/src/models/qualification-digest.mjs +17 -0
  88. package/dist/src/models/resolver.js +259 -0
  89. package/dist/src/models/worker-direct-readiness.js +529 -0
  90. package/dist/src/models/worker-dispatch-offer.js +169 -0
  91. package/dist/src/models/worker-health-readiness.js +131 -0
  92. package/dist/src/models/worker-installation.mjs +879 -0
  93. package/dist/src/models/wrapper-route.js +18 -0
  94. package/dist/src/ops/registry.js +9 -14
  95. package/dist/src/plugins/standalone-packager.js +4 -3
  96. package/dist/src/process/bounded-child-process.mjs +387 -0
  97. package/dist/src/process/cli-command.mjs +75 -0
  98. package/dist/src/providers/capability-matrix.js +8 -0
  99. package/dist/src/providers/capability-matrix.yaml +270 -0
  100. package/dist/src/providers/grok-build-headless.js +43 -21
  101. package/dist/src/providers/provider-definitions.js +9 -1
  102. package/dist/src/providers/provider-definitions.mjs +9 -2
  103. package/dist/src/serve/executor-registry.js +16 -24
  104. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  105. package/dist/src/serve/mission-conductor.js +9 -0
  106. package/dist/src/serve/shared-host-scheduler.js +2 -0
  107. package/dist/src/serve/worker-invocation.js +538 -0
  108. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  109. package/dist/src/sessions/adapter-registry.js +45 -0
  110. package/dist/src/sessions/adapters/generic.js +37 -3
  111. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  112. package/dist/src/sessions/adapters/grokbot.js +423 -24
  113. package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
  114. package/dist/src/sessions/adapters/muse.js +68 -29
  115. package/dist/src/sessions/adapters/pi.js +1 -0
  116. package/dist/src/sessions/batch-contracts.js +13 -1
  117. package/dist/src/sessions/batch-import.js +23 -42
  118. package/dist/src/sessions/candidates.js +47 -11
  119. package/dist/src/sessions/contracts.js +35 -0
  120. package/dist/src/sessions/coverage-preflight.js +134 -0
  121. package/dist/src/sessions/discovery.js +3 -0
  122. package/dist/src/sessions/grok-build-cli.js +67 -5
  123. package/dist/src/sessions/importer.js +24 -8
  124. package/dist/src/sessions/index.js +2 -0
  125. package/dist/src/sessions/repository.js +54 -13
  126. package/dist/src/sessions/workspace-discovery.js +99 -37
  127. package/dist/src/skills/adapters/clawhub.js +10 -8
  128. package/dist/src/skills/adapters/openclaw.js +10 -7
  129. package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
  130. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  131. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  132. package/dist/src/smiths/mcpsmith/example.js +1 -1
  133. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
  134. package/dist/src/storage/backends/logseq.js +68 -7
  135. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  136. package/dist/src/testing/generators/test-code-generator.js +57 -30
  137. package/dist/src/uhp/client.js +11 -1
  138. package/dist/src/uhp/mission.js +5 -0
  139. package/dist/src/update/checker.mjs +5 -5
  140. package/dist/src/update/notifier.mjs +54 -2
  141. package/dist/src/writing/fidelity.js +18 -1
  142. package/package.json +18 -1
  143. package/schemas/mission-v1.schema.json +2429 -0
  144. package/schemas/models/model-catalog.v1.schema.json +301 -0
  145. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  146. package/schemas/models/model-policy.v1.schema.json +14 -0
  147. package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
  148. package/tools/_resolve-impl.mjs +39 -0
  149. package/tools/agents/deploy-agents.mjs +15 -2
  150. package/tools/agents/providers/base.mjs +63 -43
  151. package/tools/agents/providers/grokbot.mjs +11 -12
  152. package/tools/agents/providers/hermes.mjs +6 -3
  153. package/tools/agents/providers/omp.mjs +5 -2
  154. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  155. package/tools/providers/deepseek-harness-transport.mjs +358 -0
  156. package/tools/providers/omp-transport.mjs +192 -0
  157. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  158. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  159. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  160. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  161. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  162. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  163. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  164. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  165. package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
  166. package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
  167. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  168. package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
@@ -0,0 +1,258 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { setTimeout as delay } from 'node:timers/promises';
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { getPackageRoot } from '../../channel/manager.mjs';
7
+
8
+ const execFileAsync = promisify(execFile);
9
+ const uncertain = () => ({ termination: 'uncertain', exitCode: null });
10
+ const unitProperties = ['LoadState', 'ActiveState', 'Result', 'ExecMainStatus', 'ExecMainCode', 'MainPID'];
11
+
12
+ // tools/ stays at the package root in both source and dist layouts.
13
+ function loadSystemdClientModule() {
14
+ return import(pathToFileURL(path.join(getPackageRoot(),
15
+ 'tools/ralph-external/lib/systemd-client.mjs')).href);
16
+ }
17
+
18
+ /** Run systemctl through the canonical hardened boundary: the absolute client
19
+ * resolved from the conductor PATH with a loader-filtered environment
20
+ * (manager connection preserved, nothing else inherited). Throws when the
21
+ * client is unavailable so pollers retry until deadline/cancellation and
22
+ * stop/kill report `{ ok: false }` — a bare inherited-env `systemctl` is
23
+ * never executed. */
24
+ async function execCanonicalSystemctl(exec, args, options) {
25
+ const { resolveSystemdClient, buildSystemdClientEnv } = await loadSystemdClientModule();
26
+ return exec(resolveSystemdClient('systemctl'), args,
27
+ { ...options, env: buildSystemdClientEnv({ envNames: [] }) });
28
+ }
29
+
30
+ /** Parse only explicit stop proof. Missing/unknown properties are observation
31
+ * failures, never evidence of termination. A collected unit has no exit result. */
32
+ export function parseShellUnitStatus(stdout) {
33
+ const fields = Object.fromEntries(String(stdout).split('\n').filter(line => line.includes('='))
34
+ .map(line => { const at = line.indexOf('='); return [line.slice(0, at), line.slice(at + 1)]; }));
35
+ if (fields.LoadState === 'not-found' && fields.ActiveState === 'inactive' && fields.MainPID === '0') {
36
+ return { termination: 'confirmed', exitCode: null };
37
+ }
38
+ if (!['loaded', 'error', 'masked'].includes(fields.LoadState)
39
+ || !['active', 'activating', 'deactivating', 'reloading', 'inactive', 'failed'].includes(fields.ActiveState)
40
+ || !fields.Result || !/^\d+$/.test(fields.ExecMainStatus ?? '') || !/^\d+$/.test(fields.ExecMainCode ?? '')
41
+ || !/^\d+$/.test(fields.MainPID ?? '')) {
42
+ throw new Error('worker status unavailable: missing or invalid systemd properties');
43
+ }
44
+ if (!['inactive', 'failed'].includes(fields.ActiveState) || fields.MainPID !== '0') return uncertain();
45
+ const signaled = ['2', '3'].includes(fields.ExecMainCode);
46
+ return { termination: 'confirmed', exitCode: fields.Result === 'success' && !signaled
47
+ ? Number(fields.ExecMainStatus) : Number(fields.ExecMainStatus) || 1,
48
+ ...(signaled ? { signalCode: Number(fields.ExecMainStatus) } : {}) };
49
+ }
50
+
51
+ export async function waitForUnitTermination(unit, deadlineMs, { signal, pollIntervalMs = 2000,
52
+ exec = execFileAsync, sleep = (ms, abortSignal) => delay(ms, undefined, { signal: abortSignal }),
53
+ clock = Date.now } = {}) {
54
+ if (typeof unit !== 'string' || !/^[A-Za-z0-9:_.@-]+$/.test(unit)) throw new Error('Worker isolation requires a unit-safe unit name');
55
+ const deadline = clock() + deadlineMs;
56
+ while (!signal?.aborted && clock() < deadline) {
57
+ try {
58
+ const { stdout } = await execCanonicalSystemctl(exec,
59
+ ['--user', 'show', unit, ...unitProperties.flatMap(name => ['-p', name])],
60
+ // Probes have a usable floor; the caller's AbortSignal bounds the
61
+ // overall grace/deadline even when a probe is still in flight.
62
+ { timeout: Math.max(1000, Math.min(10000, deadline - clock())), signal });
63
+ if (signal?.aborted) return uncertain();
64
+ const outcome = parseShellUnitStatus(stdout);
65
+ if (outcome.termination === 'confirmed') return outcome;
66
+ } catch (error) {
67
+ // systemctl may return nonzero for a collected unit while still showing
68
+ // its complete not-found/inactive properties. Never infer from stderr.
69
+ if (!signal?.aborted && Number.isInteger(error.code) && /^LoadState=not-found$/m.test(error.stdout ?? '')) {
70
+ try {
71
+ const outcome = parseShellUnitStatus(error.stdout);
72
+ if (outcome.termination === 'confirmed') return outcome;
73
+ } catch { /* incomplete proof; retry below */ }
74
+ }
75
+ // Retry transient status failures until deadline/cancellation.
76
+ }
77
+ if (signal?.aborted) return uncertain();
78
+ try { await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - clock())), signal); }
79
+ catch { return uncertain(); }
80
+ }
81
+ return { ...uncertain(), timedOut: !signal?.aborted };
82
+ }
83
+
84
+ /** Retain uncertain workers with bounded polling. POSIX groups cannot contain
85
+ * setsid/setpgid escapees; Windows proves only direct-child close. Sweep once
86
+ * after leader reap to limit (not eliminate) the PGID reuse race. */
87
+ export function buildShellStopControl(launch, { waitForUnit = waitForUnitTermination,
88
+ stopUnit = defaultStopShellUnit, killUnit = defaultKillShellUnit, graceMs,
89
+ onConfirmed, platform = process.platform, recheckMaxAttempts = 6, recheckDelayMs = 250 }) {
90
+ let closed = false;
91
+ let exitCode = null;
92
+ let signalCode = null;
93
+ let confirmed;
94
+ let stopTask;
95
+ let delivery;
96
+ let stopped = false;
97
+ let recheckTimer;
98
+ let rechecks = 0;
99
+ let disposed = false;
100
+ let groupSwept = false;
101
+ let resolveClose;
102
+ const probes = new Set();
103
+ const child = launch.child;
104
+ const ownedGroup = platform !== 'win32' && launch.ownedGroup && child?.pid;
105
+ const closeProof = new Promise(resolve => { resolveClose = resolve; });
106
+
107
+ function groupGone() {
108
+ if (!ownedGroup) return true;
109
+ try { process.kill(-child.pid, 0); return false; } catch (error) { return error.code === 'ESRCH'; }
110
+ }
111
+ function confirm(outcome) {
112
+ if (confirmed) return confirmed;
113
+ confirmed = { ...outcome, stopped };
114
+ clearTimeout(recheckTimer);
115
+ for (const probe of probes) probe.abort();
116
+ child?.removeListener('close', onClose);
117
+ child?.removeListener('error', onError);
118
+ resolveClose(confirmed);
119
+ // Registry/cleanup extension failures must not escape an event handler.
120
+ try { Promise.resolve(onConfirmed?.(confirmed)).catch(() => {}); } catch { /* confirmed proof remains valid */ }
121
+ return confirmed;
122
+ }
123
+ function onClose(code, signal) {
124
+ closed = true;
125
+ exitCode = code;
126
+ signalCode = signal ?? child?.signalCode ?? null;
127
+ if (launch.mode === 'systemd-unit') return; // systemd-run close is not cgroup stop proof
128
+ if (groupGone()) confirm({ termination: 'confirmed', exitCode, signalCode });
129
+ else void control.stop(); // descendants outlived their leader
130
+ }
131
+ function onError() {
132
+ // A pid-bearing child error is an observation failure, not a deadline.
133
+ // ENOENT has no pid and cannot have started a worker.
134
+ if (!child?.pid && launch.mode !== 'systemd-unit') confirm({ termination: 'confirmed', exitCode: null });
135
+ }
136
+ child?.once('close', onClose);
137
+ child?.on('error', onError);
138
+
139
+ async function bounded(operation, ms) {
140
+ const abort = new AbortController();
141
+ probes.add(abort);
142
+ let timer;
143
+ let onAbort;
144
+ try {
145
+ return await Promise.race([Promise.resolve().then(() => operation(abort.signal)), new Promise(resolve => {
146
+ onAbort = () => resolve(confirmed ?? uncertain());
147
+ abort.signal.addEventListener('abort', onAbort, { once: true });
148
+ timer = setTimeout(() => resolve({ ...uncertain(), timedOut: true }), Math.max(0, ms));
149
+ })]);
150
+ } catch { return uncertain(); }
151
+ finally {
152
+ clearTimeout(timer);
153
+ abort.signal.removeEventListener('abort', onAbort);
154
+ probes.delete(abort);
155
+ abort.abort(); // cancel a losing poll or execFile, including its sleep
156
+ }
157
+ }
158
+ async function waitStopped(ms) {
159
+ if (confirmed) return confirmed;
160
+ if (launch.mode === 'systemd-unit') {
161
+ const outcome = await bounded(signal => waitForUnit(launch.unit, ms, { signal }), ms);
162
+ return outcome.termination === 'confirmed' ? confirm(outcome) : outcome;
163
+ }
164
+ if (closed && groupGone()) return confirm({ termination: 'confirmed', exitCode, signalCode });
165
+ return bounded(() => closeProof, ms);
166
+ }
167
+ function recheck() {
168
+ if (confirmed || disposed || recheckTimer || rechecks >= recheckMaxAttempts
169
+ || (launch.mode !== 'systemd-unit' && !ownedGroup)) return;
170
+ recheckTimer = setTimeout(async () => {
171
+ recheckTimer = undefined;
172
+ rechecks++;
173
+ await waitStopped(launch.mode === 'systemd-unit' ? Math.max(1000, graceMs) : Math.max(25, graceMs));
174
+ recheck();
175
+ }, Math.min(8000, recheckDelayMs * 2 ** rechecks));
176
+ recheckTimer.unref?.();
177
+ }
178
+ async function signal(signalName, budgetMs) {
179
+ if (confirmed) return true;
180
+ try {
181
+ if (launch.mode === 'systemd-unit') {
182
+ const result = await bounded(abortSignal => (signalName === 'SIGTERM' ? stopUnit : killUnit)(launch.unit,
183
+ { signal: abortSignal, timeout: Math.max(1, budgetMs) }), budgetMs);
184
+ return !!result.ok;
185
+ }
186
+ if (ownedGroup) {
187
+ if ((closed || child.exitCode != null || child.signalCode != null) && groupSwept) return false;
188
+ if (signalName === 'SIGKILL') groupSwept = true;
189
+ process.kill(-child.pid, signalName);
190
+ return true;
191
+ }
192
+ return !closed && !!child?.kill(signalName);
193
+ } catch { return false; }
194
+ }
195
+ const control = {
196
+ dispose: () => {
197
+ disposed = true;
198
+ clearTimeout(recheckTimer);
199
+ for (const probe of probes) probe.abort();
200
+ // Keep close/error listeners for late proof and EventEmitter safety.
201
+ },
202
+ get pollingStopped() { return disposed || !!confirmed || rechecks >= recheckMaxAttempts; },
203
+ stop: async () => {
204
+ if (confirmed) return true;
205
+ if (stopTask) return delivery;
206
+ stopped = true;
207
+ const phaseMs = launch.mode === 'systemd-unit' ? Math.max(1000, graceMs) : graceMs;
208
+ let phaseEnd = Date.now() + phaseMs;
209
+ delivery = signal('SIGTERM', phaseMs);
210
+ stopTask = (async () => {
211
+ await delivery;
212
+ let outcome = await waitStopped(Math.max(0, phaseEnd - Date.now()));
213
+ if (outcome.termination !== 'confirmed') {
214
+ phaseEnd = Date.now() + phaseMs;
215
+ await signal('SIGKILL', phaseMs);
216
+ outcome = await waitStopped(Math.max(0, phaseEnd - Date.now()));
217
+ }
218
+ if (outcome.termination !== 'confirmed') recheck();
219
+ return outcome;
220
+ })().finally(() => { stopTask = undefined; });
221
+ return delivery;
222
+ },
223
+ stopAndConfirm: async () => {
224
+ await control.stop();
225
+ return confirmed ?? (stopTask ? await stopTask : uncertain());
226
+ },
227
+ wait: async deadlineMs => {
228
+ const deadline = Date.now() + deadlineMs;
229
+ let outcome;
230
+ do {
231
+ outcome = await waitStopped(Math.max(0, deadline - Date.now()));
232
+ if (confirmed) return confirmed;
233
+ // Observation failure alone never mandates a stop. Avoid a hot loop
234
+ // when an injected/failed observer returns immediately.
235
+ if (!outcome.timedOut) await Promise.race([closeProof, delay(Math.min(50, Math.max(0, deadline - Date.now())))]);
236
+ } while (!disposed && !outcome.timedOut && Date.now() < deadline);
237
+ if (disposed) return uncertain();
238
+ return { ...await control.stopAndConfirm(), timedOut: true };
239
+ },
240
+ };
241
+ // Only a recorded real close can be replayed; exitCode precedes stdio close.
242
+ if (launch.spawnObservation?.closed) onClose(launch.spawnObservation.exitCode, launch.spawnObservation.signalCode);
243
+ if (!child?.pid && !launch.unit && !launch.launched) confirm({ termination: 'confirmed', exitCode: null });
244
+ return control;
245
+ }
246
+
247
+ export async function defaultStopShellUnit(unit, { signal, timeout = 1000, exec = execFileAsync } = {}) {
248
+ try {
249
+ await execCanonicalSystemctl(exec, ['--user', '--no-block', 'stop', unit], { timeout, signal });
250
+ return { ok: true };
251
+ } catch { return { ok: false }; }
252
+ }
253
+ export async function defaultKillShellUnit(unit, { signal, timeout = 1000, exec = execFileAsync } = {}) {
254
+ try {
255
+ await execCanonicalSystemctl(exec, ['--user', 'kill', '--kill-who=all', '--signal=SIGKILL', unit], { timeout, signal });
256
+ return { ok: true };
257
+ } catch { return { ok: false }; }
258
+ }
@@ -0,0 +1,79 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdtemp, mkdir, writeFile, rm, readdir, lstat } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ /** Sweep old private directories only for dead conductors; PID reuse retains
7
+ * them. Runtime files keep prompts out of git, but cannot isolate same-user workers. */
8
+ export async function sweepWorkerStdin(root = tmpdir(), { staleMs = 86400000, now = Date.now(),
9
+ isAlive = pid => { try { process.kill(pid, 0); return true; } catch (error) { return error.code !== 'ESRCH'; } } } = {}) {
10
+ try {
11
+ for (const name of await readdir(root)) {
12
+ const match = /^aiwg-worker-stdin-(\d+)-[A-Za-z0-9]+$/.exec(name);
13
+ if (!match || isAlive(Number(match[1]))) continue;
14
+ const directory = path.join(root, name);
15
+ try {
16
+ const stat = await lstat(directory);
17
+ if (!stat.isDirectory() || stat.isSymbolicLink() || now - stat.mtimeMs < staleMs
18
+ || (process.getuid && (stat.uid !== process.getuid() || (stat.mode & 0o777) !== 0o700))) continue;
19
+ await rm(directory, { recursive: true, force: true });
20
+ } catch { /* best-effort stale cleanup must not prevent registration */ }
21
+ }
22
+ } catch { /* missing/inaccessible runtime root */ }
23
+ }
24
+
25
+ export async function prepareWorkerStdin(bytes, root = tmpdir()) {
26
+ await mkdir(root, { recursive: true, mode: 0o700 });
27
+ const directory = await mkdtemp(path.join(root, `aiwg-worker-stdin-${process.pid}-`));
28
+ const stdinPath = path.join(directory, 'stdin');
29
+ let cleanupTask;
30
+ const cleanup = () => cleanupTask ??= rm(directory, { recursive: true, force: true }).catch(() => {
31
+ console.error('[worker-invoke] private stdin cleanup failed; retained for stale sweep');
32
+ });
33
+ try {
34
+ await writeFile(stdinPath, bytes, { flag: 'wx', mode: 0o600 });
35
+ return { stdinPath, cleanup };
36
+ } catch (error) {
37
+ await cleanup();
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ export async function launchPreparedShellWorker(invocation, { allowDegraded, config, name, live, readers, thresholdOverlay,
43
+ memoryMaxGb, driver, servedRoot, isolation, spawn, spawnAsync, stdinRuntimeRoot },
44
+ { loadRalphExternal, readBoundedFile, importImpl, describeGateFailures }) {
45
+ let attempt;
46
+ let cleanup = () => Promise.resolve();
47
+ try {
48
+ const { planDirectWorkerLaunch } = await loadRalphExternal('session-launcher.mjs');
49
+ if (!Number.isFinite(memoryMaxGb) || memoryMaxGb <= 0) throw new Error('Worker isolation requires a positive memoryMaxGb ceiling');
50
+ const brief = await readBoundedFile(invocation.briefPath, 262144, servedRoot);
51
+ if (`sha256:${createHash('sha256').update(brief).digest('hex')}` !== invocation.briefDigest) {
52
+ throw new Error('Worker prompt digest changed');
53
+ }
54
+ const { workerStdinBytes } = await importImpl('serve/worker-stdin-frame.mjs');
55
+ let stdinPath;
56
+ const bytes = workerStdinBytes(invocation, brief, driver);
57
+ if (bytes !== null) {
58
+ ({ stdinPath, cleanup } = await prepareWorkerStdin(bytes, stdinRuntimeRoot));
59
+ }
60
+ const launch = await planDirectWorkerLaunch({ invocation, config, allowDegraded, name, live, readers,
61
+ thresholdOverlay, memoryMaxGb, stdinPath, ownedGroup: true, isolation,
62
+ spawn, spawnAsync, onSpawnAttempt: value => {
63
+ attempt = value;
64
+ value.child?.once('close', (exitCode, signalCode) => { Object.assign(value, { closed: true, exitCode, signalCode }); });
65
+ },
66
+ });
67
+ const launchAttempted = !!(attempt?.unit || attempt?.pid);
68
+ if (!launch.launched && !launchAttempted) await cleanup();
69
+ return { ...launch, launchAttempted, spawnObservation: attempt, ownedGroup: launch.mode === 'degraded' && !!launch.pid, cleanup,
70
+ diagnostic: launch.diagnostic ?? describeGateFailures(launch.failures) };
71
+ } catch (error) {
72
+ if (attempt?.unit || attempt?.pid) {
73
+ return { ...attempt, spawnObservation: attempt, ownedGroup: attempt.mode === 'degraded', launched: false, launchAttempted: true, cleanup, diagnostic: error.message };
74
+ }
75
+ error.workerNotLaunched = true;
76
+ await cleanup();
77
+ throw error;
78
+ }
79
+ }
@@ -9,7 +9,7 @@ import { existsSync } from 'node:fs';
9
9
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
10
10
  import { homedir } from 'node:os';
11
11
  import path from 'node:path';
12
- import { execFileSync } from 'node:child_process';
12
+ import { boundedExecFileSync } from '../process/bounded-child-process.mjs';
13
13
  import { projectAiwgPath, resolveProjectAiwgDir } from '../config/project-artifacts.js';
14
14
  export function projectMemoryHome() {
15
15
  const override = process.env.AIWG_PROJECT_MEMORY_HOME;
@@ -248,7 +248,7 @@ function sanitizeProjectId(id) {
248
248
  }
249
249
  function discoverGitRemotes(cwd) {
250
250
  try {
251
- const stdout = execFileSync('git', ['remote', '-v'], { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
251
+ const stdout = boundedExecFileSync('git', ['remote', '-v'], 'quick', { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
252
252
  const remotes = stdout
253
253
  .split('\n')
254
254
  .map((line) => line.trim().split(/\s+/)[1])