@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.
- package/agentic/code/providers/capability-matrix.yaml +270 -0
- package/agentic/code/providers/model-capabilities.v1.json +3091 -24
- package/bin/aiwg.mjs +86 -21
- package/dist/src/a2a/codecs.js +9 -0
- package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
- package/dist/src/artifacts/repair.js +9 -4
- package/dist/src/channel/manager.mjs +59 -10
- package/dist/src/cli/child-process-result.js +40 -0
- package/dist/src/cli/env.js +38 -16
- package/dist/src/cli/flag-parsing.js +236 -0
- package/dist/src/cli/handlers/decision.js +297 -2
- package/dist/src/cli/handlers/diagnose.js +9 -2
- package/dist/src/cli/handlers/feedback.js +14 -5
- package/dist/src/cli/handlers/init.js +245 -30
- package/dist/src/cli/handlers/mc.js +23 -0
- package/dist/src/cli/handlers/models.js +69 -6
- package/dist/src/cli/handlers/ralph.js +3 -0
- package/dist/src/cli/handlers/refresh.js +16 -0
- package/dist/src/cli/handlers/regenerate.js +14 -0
- package/dist/src/cli/handlers/runtime-info.js +24 -9
- package/dist/src/cli/handlers/script-runner.js +16 -6
- package/dist/src/cli/handlers/session.js +396 -62
- package/dist/src/cli/handlers/sessions.js +146 -40
- package/dist/src/cli/handlers/setup-manifest.js +439 -39
- package/dist/src/cli/handlers/setup.js +139 -39
- package/dist/src/cli/handlers/subcommands.js +3 -1
- package/dist/src/cli/handlers/team.js +18 -8
- package/dist/src/cli/handlers/use.js +13 -0
- package/dist/src/cli/handlers/utilities.js +106 -23
- package/dist/src/cli/prompt-utils.js +72 -0
- package/dist/src/cli/provider-resolution.js +11 -0
- package/dist/src/config/aiwg-config.js +268 -4
- package/dist/src/config/cli.js +146 -19
- package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
- package/dist/src/config/project-artifacts-health.mjs +14 -3
- package/dist/src/config/user-config.js +36 -10
- package/dist/src/decision/adapters/jev.js +80 -28
- package/dist/src/decision/ask.js +352 -0
- package/dist/src/decision/driver.js +4 -0
- package/dist/src/decision/entry.js +38 -6
- package/dist/src/decision/heldout/collector.js +10 -1
- package/dist/src/decision/index.js +3 -0
- package/dist/src/decision/interop.js +60 -8
- package/dist/src/decision/jev-credentials.js +121 -0
- package/dist/src/decision/ledger.js +839 -0
- package/dist/src/decision/validate.js +6 -3
- package/dist/src/effects/dispatch-verifier.js +252 -0
- package/dist/src/effects/index.js +1 -0
- package/dist/src/features/installer.js +2 -2
- package/dist/src/flow/graph-metadata.js +5 -0
- package/dist/src/gates/providers/loader.js +3 -1
- package/dist/src/installation/manager-command.mjs +1 -1
- package/dist/src/installation/manager.mjs +17 -2
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/adapters/codex-runtime.js +360 -56
- package/dist/src/mcp/cli.mjs +587 -77
- package/dist/src/mcp/config-file.mjs +279 -0
- package/dist/src/mcp/config-layers.mjs +348 -0
- package/dist/src/mcp/credentials.mjs +484 -0
- package/dist/src/mcp/grok-build-config.mjs +16 -26
- package/dist/src/mcp/helpers.mjs +106 -2
- package/dist/src/mcp/omp-config.mjs +50 -29
- package/dist/src/mcp/profiles.js +97 -23
- package/dist/src/mcp/profiles.mjs +99 -27
- package/dist/src/mcp/registry.js +264 -57
- package/dist/src/mcp/registry.mjs +198 -45
- package/dist/src/mcp/server.mjs +403 -20
- package/dist/src/mcp/toml-string.mjs +9 -0
- package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
- package/dist/src/mcp/tool-filters.mjs +357 -0
- package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
- package/dist/src/mcp/tools/decision.mjs +58 -0
- package/dist/src/mcp/tools/orchestration.mjs +38 -0
- package/dist/src/mcp/tools/subsystems.mjs +39 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
- package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
- package/dist/src/memory/project-registry.js +2 -2
- package/dist/src/mission-protocol/codecs.js +639 -1
- package/dist/src/mission-protocol/types.js +110 -0
- package/dist/src/mission-protocol/worker-status.js +2 -0
- package/dist/src/models/config-loader.js +70 -13
- package/dist/src/models/grok-build-models.js +93 -41
- package/dist/src/models/model-capabilities.v1.json +3091 -24
- package/dist/src/models/model-discovery.js +862 -63
- package/dist/src/models/provider-policy.js +625 -11
- package/dist/src/models/qualification-digest.mjs +17 -0
- package/dist/src/models/resolver.js +259 -0
- package/dist/src/models/worker-direct-readiness.js +529 -0
- package/dist/src/models/worker-dispatch-offer.js +169 -0
- package/dist/src/models/worker-health-readiness.js +131 -0
- package/dist/src/models/worker-installation.mjs +879 -0
- package/dist/src/models/wrapper-route.js +18 -0
- package/dist/src/ops/registry.js +9 -14
- package/dist/src/plugins/standalone-packager.js +4 -3
- package/dist/src/process/bounded-child-process.mjs +387 -0
- package/dist/src/process/cli-command.mjs +75 -0
- package/dist/src/providers/capability-matrix.js +8 -0
- package/dist/src/providers/capability-matrix.yaml +270 -0
- package/dist/src/providers/grok-build-headless.js +43 -21
- package/dist/src/providers/provider-definitions.js +9 -1
- package/dist/src/providers/provider-definitions.mjs +9 -2
- package/dist/src/serve/executor-registry.js +16 -24
- package/dist/src/serve/fleet-mission-conductor.js +9 -0
- package/dist/src/serve/mission-conductor.js +9 -0
- package/dist/src/serve/shared-host-scheduler.js +2 -0
- package/dist/src/serve/worker-invocation.js +538 -0
- package/dist/src/serve/worker-stdin-frame.mjs +107 -0
- package/dist/src/sessions/adapter-registry.js +45 -0
- package/dist/src/sessions/adapters/generic.js +37 -3
- package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
- package/dist/src/sessions/adapters/grokbot.js +423 -24
- package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
- package/dist/src/sessions/adapters/muse.js +68 -29
- package/dist/src/sessions/adapters/pi.js +1 -0
- package/dist/src/sessions/batch-contracts.js +13 -1
- package/dist/src/sessions/batch-import.js +23 -42
- package/dist/src/sessions/candidates.js +47 -11
- package/dist/src/sessions/contracts.js +35 -0
- package/dist/src/sessions/coverage-preflight.js +134 -0
- package/dist/src/sessions/discovery.js +3 -0
- package/dist/src/sessions/grok-build-cli.js +67 -5
- package/dist/src/sessions/importer.js +24 -8
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/repository.js +54 -13
- package/dist/src/sessions/workspace-discovery.js +99 -37
- package/dist/src/skills/adapters/clawhub.js +10 -8
- package/dist/src/skills/adapters/openclaw.js +10 -7
- package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
- package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
- package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
- package/dist/src/smiths/mcpsmith/example.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
- package/dist/src/storage/backends/logseq.js +68 -7
- package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
- package/dist/src/testing/generators/test-code-generator.js +57 -30
- package/dist/src/uhp/client.js +11 -1
- package/dist/src/uhp/mission.js +5 -0
- package/dist/src/update/checker.mjs +5 -5
- package/dist/src/update/notifier.mjs +54 -2
- package/dist/src/writing/fidelity.js +18 -1
- package/package.json +18 -1
- package/schemas/mission-v1.schema.json +2429 -0
- package/schemas/models/model-catalog.v1.schema.json +301 -0
- package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
- package/schemas/models/model-policy.v1.schema.json +14 -0
- package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
- package/tools/_resolve-impl.mjs +39 -0
- package/tools/agents/deploy-agents.mjs +15 -2
- package/tools/agents/providers/base.mjs +63 -43
- package/tools/agents/providers/grokbot.mjs +11 -12
- package/tools/agents/providers/hermes.mjs +6 -3
- package/tools/agents/providers/omp.mjs +5 -2
- package/tools/agents/providers/openclaw-translator.mjs +19 -5
- package/tools/providers/deepseek-harness-transport.mjs +358 -0
- package/tools/providers/omp-transport.mjs +192 -0
- package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
- package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
- package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
- package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
- package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
- package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
- package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
- package/tools/ralph-external/lib/worker-health.mjs +439 -0
- 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 {
|
|
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 =
|
|
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])
|