@aiwg/cli 2026.10.10 → 2026.10.11
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/model-capabilities.v1.json +78 -14
- package/dist/src/cli/handlers/sessions.js +198 -15
- package/dist/src/cli/handlers/use.js +12 -4
- package/dist/src/config/worker-context.js +337 -0
- package/dist/src/mcp/tools/worker-invoke.mjs +108 -10
- package/dist/src/models/model-capabilities.v1.json +78 -14
- package/dist/src/models/provider-policy.js +7 -0
- package/dist/src/policy/authorization.js +29 -1
- package/dist/src/process/aiwg-probe-command.d.mts +15 -0
- package/dist/src/process/aiwg-probe-command.mjs +37 -0
- package/dist/src/sessions/adapters/grokbot.js +7 -0
- package/dist/src/sessions/batch-import.js +34 -6
- package/dist/src/sessions/contracts.js +19 -0
- package/dist/src/sessions/coverage-preflight.js +7 -1
- package/dist/src/sessions/fortemi-export-mapping.js +33 -3
- package/dist/src/sessions/host-catalog.js +157 -0
- package/dist/src/sessions/image-blobs.js +141 -0
- package/dist/src/sessions/importer.js +23 -3
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/refresh.js +25 -2
- package/dist/src/sessions/repository.js +164 -22
- package/dist/src/sessions/workspace-discovery.js +5 -2
- package/package.json +7 -1
- package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
- package/tools/ralph-external/lib/worker-context.mjs +237 -0
- package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
- package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
- package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +17 -29
- package/tools/ralph-external/lib/worker-launch-cli.mjs +3 -1
- package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
- package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
- package/tools/ralph-external/lib/worker-termination.mjs +82 -0
- package/tools/ralph-external/session-launcher.mjs +51 -96
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker AIWG context providers and launch-time bootstrap ensure (#3685).
|
|
3
|
+
*
|
|
4
|
+
* Orchestration must automatically `aiwg use` the providers configured for
|
|
5
|
+
* it, so any new worker process starts with the AIWG bootstrap in place.
|
|
6
|
+
* This module owns the config-side half: resolving WHICH providers
|
|
7
|
+
* (config `providers` plus every CLI configured under
|
|
8
|
+
* `parallelism.clis` / `parallelism.groups.*.members`, mapped through the
|
|
9
|
+
* provider registry) and checking/ensuring that bootstrap inside exactly
|
|
10
|
+
* one worker workdir (never outside it).
|
|
11
|
+
*
|
|
12
|
+
* The tools/ half (`tools/ralph-external/lib/worker-context.mjs`) loads the
|
|
13
|
+
* config through `readAiwgConfig` and injects it here; direct callers may
|
|
14
|
+
* pass an already-loaded config object instead.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { getProviderArtifactPathStrings, getProviderDefinition, normalizeProviderDefinitionId, } from '../providers/provider-definitions.js';
|
|
20
|
+
import { emptyConfig, readAiwgConfig, writeAiwgConfig, } from './aiwg-config.js';
|
|
21
|
+
/**
|
|
22
|
+
* Typed refusal when a worker's AIWG bootstrap cannot be ensured.
|
|
23
|
+
* `remedy` names the operator action that unblocks the launch.
|
|
24
|
+
*/
|
|
25
|
+
export class WorkerContextBootstrapError extends Error {
|
|
26
|
+
code = 'OG_WORKER_CONTEXT_UNAVAILABLE';
|
|
27
|
+
remedy;
|
|
28
|
+
provider;
|
|
29
|
+
workdir;
|
|
30
|
+
constructor(message, remedy, opts) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'WorkerContextBootstrapError';
|
|
33
|
+
this.remedy = remedy;
|
|
34
|
+
if (opts?.provider !== undefined)
|
|
35
|
+
this.provider = opts.provider;
|
|
36
|
+
if (opts?.workdir !== undefined)
|
|
37
|
+
this.workdir = opts.workdir;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Explicit provider lists must be non-empty arrays of deployable ids. */
|
|
41
|
+
export function validateExplicitWorkerContextProviders(providers) {
|
|
42
|
+
if (!Array.isArray(providers) || providers.length === 0
|
|
43
|
+
|| providers.some(p => typeof p !== 'string' || p.length === 0 || p.includes('\0') || p.includes(','))) {
|
|
44
|
+
throw new WorkerContextBootstrapError('worker context providers must be a non-empty array of provider ids', 'pass an explicit non-empty provider list, or configure providers plus orchestration CLIs in .aiwg/aiwg.config');
|
|
45
|
+
}
|
|
46
|
+
return [...new Set(providers)];
|
|
47
|
+
}
|
|
48
|
+
const ADAPTER_REFERENCE = /^(providers|workerAdapters):([a-z][a-z0-9-]{0,62})$/;
|
|
49
|
+
/**
|
|
50
|
+
* Map one orchestration CLI id to its AIWG provider id.
|
|
51
|
+
* A CLI without an explicit `parallelism.clis` entry defaults to
|
|
52
|
+
* `providers:<cli>` (the same default `resolveWorkerGroupInstallation`
|
|
53
|
+
* applies). Non-provider adapters (e.g. `workerAdapters:local-http`) and
|
|
54
|
+
* ids the registry does not know are reported as skipped, never guessed.
|
|
55
|
+
*/
|
|
56
|
+
export function mapCliToProviderId(cli, parallelism) {
|
|
57
|
+
if (typeof cli !== 'string' || cli.length === 0) {
|
|
58
|
+
return { skipped: `empty CLI reference` };
|
|
59
|
+
}
|
|
60
|
+
const raw = typeof parallelism?.clis?.[cli]?.adapter === 'string'
|
|
61
|
+
&& parallelism.clis[cli].adapter.length > 0
|
|
62
|
+
? parallelism.clis[cli].adapter
|
|
63
|
+
: `providers:${cli}`;
|
|
64
|
+
const match = ADAPTER_REFERENCE.exec(raw);
|
|
65
|
+
if (!match || match[1] !== 'providers') {
|
|
66
|
+
return { skipped: `clis/${cli}: ${raw}` };
|
|
67
|
+
}
|
|
68
|
+
const provider = normalizeProviderDefinitionId(match[2]);
|
|
69
|
+
if (!provider)
|
|
70
|
+
return { skipped: `clis/${cli}: ${raw} (unknown provider)` };
|
|
71
|
+
return { provider };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the full worker-context provider set: the union of the config's
|
|
75
|
+
* own `providers` and every CLI configured for orchestration
|
|
76
|
+
* (`parallelism.clis` keys/adapters and the `cli` of every
|
|
77
|
+
* `parallelism.groups.*.members[]`), mapped to AIWG provider ids through
|
|
78
|
+
* the provider registry. Non-provider adapters are skipped and reported.
|
|
79
|
+
* Fails closed when the union is empty.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveWorkerContextProvidersFromConfig(config) {
|
|
82
|
+
const providers = [];
|
|
83
|
+
const skipped = [];
|
|
84
|
+
const add = (id) => {
|
|
85
|
+
if (!providers.includes(id))
|
|
86
|
+
providers.push(id);
|
|
87
|
+
};
|
|
88
|
+
const skip = (entry) => {
|
|
89
|
+
if (!skipped.includes(entry))
|
|
90
|
+
skipped.push(entry);
|
|
91
|
+
};
|
|
92
|
+
const configured = config?.providers;
|
|
93
|
+
if (Array.isArray(configured)) {
|
|
94
|
+
for (const entry of configured) {
|
|
95
|
+
if (typeof entry !== 'string' || entry.length === 0)
|
|
96
|
+
continue;
|
|
97
|
+
// Configured providers deploy verbatim when the registry does not
|
|
98
|
+
// know them: project-local provider adapters resolve at deploy time
|
|
99
|
+
// (see the `use` handler's project-local resolution), so an unknown
|
|
100
|
+
// id here is the deployer's decision, not this resolver's.
|
|
101
|
+
add(normalizeProviderDefinitionId(entry) ?? entry);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const parallelism = config?.parallelism;
|
|
105
|
+
for (const cli of Object.keys(parallelism?.clis ?? {})) {
|
|
106
|
+
const mapped = mapCliToProviderId(cli, parallelism);
|
|
107
|
+
if ('provider' in mapped)
|
|
108
|
+
add(mapped.provider);
|
|
109
|
+
else
|
|
110
|
+
skip(mapped.skipped);
|
|
111
|
+
}
|
|
112
|
+
for (const [groupId, group] of Object.entries(parallelism?.groups ?? {})) {
|
|
113
|
+
for (const member of group?.members ?? []) {
|
|
114
|
+
const cli = member?.cli;
|
|
115
|
+
const mapped = mapCliToProviderId(cli, parallelism);
|
|
116
|
+
if ('provider' in mapped)
|
|
117
|
+
add(mapped.provider);
|
|
118
|
+
else
|
|
119
|
+
skip(typeof cli === 'string' && cli ? `groups/${groupId}/members cli '${cli}': ${mapped.skipped}` : `groups/${groupId}/members: ${mapped.skipped}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (providers.length === 0) {
|
|
123
|
+
throw new WorkerContextBootstrapError('worker context deployment needs configured providers: the union of config providers and orchestration CLIs is empty'
|
|
124
|
+
+ (skipped.length > 0 ? ` (skipped: ${skipped.join('; ')})` : ''), 'configure providers and/or parallelism clis/groups in .aiwg/aiwg.config, or pass an explicit provider list');
|
|
125
|
+
}
|
|
126
|
+
return { providers, skipped };
|
|
127
|
+
}
|
|
128
|
+
/** AIWG-managed rule-section markers written into a deployed AGENTS.md. */
|
|
129
|
+
const WORKER_CONTEXT_RULE_MARKER = /<!--\s*AIWG:[A-Za-z0-9_-]+:(start|end)\s*-->/;
|
|
130
|
+
/**
|
|
131
|
+
* Check that a worker workdir already carries one provider's bootstrap:
|
|
132
|
+
* the kernel artifact dirs (`use all` writes kernel skills, rules,
|
|
133
|
+
* behaviors — never the full agent/command surface) and, for providers
|
|
134
|
+
* whose contract targets AGENTS.md, the AIWG rule sections.
|
|
135
|
+
*/
|
|
136
|
+
export function workerContextBootstrapStatus({ workdir, provider }) {
|
|
137
|
+
const missing = [];
|
|
138
|
+
const definition = getProviderDefinition(provider);
|
|
139
|
+
if (!definition) {
|
|
140
|
+
return { present: false, missing: [`unknown provider '${provider}'`] };
|
|
141
|
+
}
|
|
142
|
+
const paths = getProviderArtifactPathStrings(provider);
|
|
143
|
+
const kernelDirs = [
|
|
144
|
+
definition.paths.kernelSkills,
|
|
145
|
+
paths?.rules ?? null,
|
|
146
|
+
paths?.behaviors ?? null,
|
|
147
|
+
].filter((dir) => typeof dir === 'string' && dir.length > 0);
|
|
148
|
+
const requiredDirs = kernelDirs.length > 0
|
|
149
|
+
? kernelDirs
|
|
150
|
+
: Object.values(paths ?? {}).filter((dir) => typeof dir === 'string' && dir.length > 0);
|
|
151
|
+
for (const dir of [...new Set(requiredDirs)]) {
|
|
152
|
+
const absolute = path.isAbsolute(dir) ? dir : path.join(workdir, dir);
|
|
153
|
+
let entries = null;
|
|
154
|
+
try {
|
|
155
|
+
entries = readdirSync(absolute);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
entries = null;
|
|
159
|
+
}
|
|
160
|
+
if (!entries || entries.length === 0)
|
|
161
|
+
missing.push(`provider dir '${dir}'`);
|
|
162
|
+
}
|
|
163
|
+
if ((definition.context?.bootstrapTargets ?? []).includes('AGENTS.md')) {
|
|
164
|
+
let content = null;
|
|
165
|
+
try {
|
|
166
|
+
content = readFileSync(path.join(workdir, 'AGENTS.md'), 'utf8');
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
content = null;
|
|
170
|
+
}
|
|
171
|
+
if (!content || !WORKER_CONTEXT_RULE_MARKER.test(content)) {
|
|
172
|
+
missing.push('AGENTS.md rule sections');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { present: missing.length === 0, missing };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Materialize the union provider set into the worker workdir's own config
|
|
179
|
+
* through the existing loader/writer (never ad-hoc JSON). Existing entries
|
|
180
|
+
* keep their order; union members absent from the worktree base are
|
|
181
|
+
* appended. A missing config is created minimal. An already-current config
|
|
182
|
+
* is left untouched so the deploy below stays the only writer.
|
|
183
|
+
*/
|
|
184
|
+
export async function syncWorkerContextProviders(options) {
|
|
185
|
+
const { workdir } = options;
|
|
186
|
+
if (typeof workdir !== 'string' || !path.isAbsolute(workdir)) {
|
|
187
|
+
throw new WorkerContextBootstrapError(`worker context sync needs an absolute workdir, got ${JSON.stringify(workdir)}`, 'sync worker context providers only into the worker workdir', { workdir: typeof workdir === 'string' ? workdir : undefined });
|
|
188
|
+
}
|
|
189
|
+
const providers = validateExplicitWorkerContextProviders(options.providers);
|
|
190
|
+
const read = options.readConfig ?? ((dir) => readAiwgConfig(dir));
|
|
191
|
+
const existing = await read(workdir);
|
|
192
|
+
const current = Array.isArray(existing?.providers)
|
|
193
|
+
? existing.providers.filter((entry) => typeof entry === 'string')
|
|
194
|
+
: [];
|
|
195
|
+
const merged = [...current];
|
|
196
|
+
for (const provider of providers) {
|
|
197
|
+
if (!merged.includes(provider))
|
|
198
|
+
merged.push(provider);
|
|
199
|
+
}
|
|
200
|
+
const unchanged = merged.length === current.length && merged.every((entry, index) => entry === current[index]);
|
|
201
|
+
if (unchanged)
|
|
202
|
+
return { providers: merged, updated: false };
|
|
203
|
+
const base = existing !== null && typeof existing === 'object' && 'version' in existing
|
|
204
|
+
? existing
|
|
205
|
+
: {};
|
|
206
|
+
const next = { ...emptyConfig(merged), ...base, providers: merged };
|
|
207
|
+
const write = options.writeConfig ?? ((dir, config) => writeAiwgConfig(dir, config));
|
|
208
|
+
await write(workdir, next);
|
|
209
|
+
return { providers: merged, updated: true };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Canonical one-pass deploy argv: a single `aiwg use all` invocation
|
|
213
|
+
* covering every listed provider. Never one deploy per provider:
|
|
214
|
+
* per-provider invocations rewrite AGENTS.md piecemeal and clobber each
|
|
215
|
+
* other's rule sections, while one pass keeps them all. No `--providers`
|
|
216
|
+
* flag: it feeds only the global bootstrap and the orchestrated
|
|
217
|
+
* verification report — `executeCore` resolves project-scope providers
|
|
218
|
+
* from config (`src/cli/handlers/use.ts`), ignoring both the flag and any
|
|
219
|
+
* explicitly resolved list. The supported project-scope mechanism is the
|
|
220
|
+
* configured-provider path: `syncWorkerContextProviders` first, then this
|
|
221
|
+
* single pass reads the union back from the worktree config.
|
|
222
|
+
*/
|
|
223
|
+
export function buildWorkerContextDeployArgs({ providers, target }) {
|
|
224
|
+
// Fail fast on a hostile list; the synced worktree config carries the set.
|
|
225
|
+
validateExplicitWorkerContextProviders(providers);
|
|
226
|
+
return ['use', 'all', '--target', target, '--yes'];
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Ensure a worker workdir carries its CLI's AIWG bootstrap before launch.
|
|
230
|
+
* Present: no-op. Missing: sync the union set into the workdir config,
|
|
231
|
+
* then exactly one `aiwg use all` pass into the workdir (never anywhere
|
|
232
|
+
* else), then re-verify. Impossible (no binary, failed deploy, still
|
|
233
|
+
* missing afterwards): throw `WorkerContextBootstrapError` naming the
|
|
234
|
+
* remedy — the caller refuses the launch rather than starting a
|
|
235
|
+
* context-free worker.
|
|
236
|
+
*/
|
|
237
|
+
export async function ensureWorkerContextBootstrap(options) {
|
|
238
|
+
const { workdir, provider, aiwgBin = process.env.AIWG_BIN ?? 'aiwg', spawn = spawnSync, timeoutMs = 300000, statusFn = workerContextBootstrapStatus, } = options;
|
|
239
|
+
if (typeof workdir !== 'string' || !path.isAbsolute(workdir) || !existsSync(workdir)) {
|
|
240
|
+
throw new WorkerContextBootstrapError(`worker context bootstrap needs an existing absolute workdir, got ${JSON.stringify(workdir)}`, 'launch the worker with its resolved absolute workdir', { provider, workdir: typeof workdir === 'string' ? workdir : undefined });
|
|
241
|
+
}
|
|
242
|
+
const status = statusFn({ workdir, provider });
|
|
243
|
+
if (status.present)
|
|
244
|
+
return { ensured: false, providers: [provider], skippedBridges: [] };
|
|
245
|
+
const providers = options.providers && options.providers.length > 0
|
|
246
|
+
? validateExplicitWorkerContextProviders(options.providers)
|
|
247
|
+
: [provider];
|
|
248
|
+
// The configured-provider path: the single pass below resolves its set
|
|
249
|
+
// from the workdir config, so the union must be materialized there
|
|
250
|
+
// first — `--providers` never reaches project-scope deploys.
|
|
251
|
+
const sync = options.syncFn ?? syncWorkerContextProviders;
|
|
252
|
+
let synced;
|
|
253
|
+
try {
|
|
254
|
+
synced = await sync({ workdir, providers });
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (error instanceof WorkerContextBootstrapError)
|
|
258
|
+
throw error;
|
|
259
|
+
throw new WorkerContextBootstrapError(`worker context sync failed: ${error instanceof Error ? error.message : String(error)}`, `write the union providers into ${path.join(workdir, '.aiwg', 'aiwg.config')} (or run 'aiwg use all --target ${workdir} --yes' manually) before launch`, { provider, workdir });
|
|
260
|
+
}
|
|
261
|
+
const argv = buildWorkerContextDeployArgs({ providers: synced.providers, target: workdir });
|
|
262
|
+
const deploy = options.deployFn ?? (({ worktreePath, providers: deployProviders, aiwgBin: bin }) => {
|
|
263
|
+
// worktreePath IS the worker workdir by construction: the only
|
|
264
|
+
// `--target` ever passed is `workdir` above, and the spawn cwd is the
|
|
265
|
+
// same directory, so no deploy write can land outside it.
|
|
266
|
+
if (path.resolve(worktreePath) !== path.resolve(workdir)) {
|
|
267
|
+
throw new WorkerContextBootstrapError(`refusing worker context deploy outside the worker workdir (${worktreePath} !== ${workdir})`, 'deploy worker context only into the worker workdir', { provider, workdir });
|
|
268
|
+
}
|
|
269
|
+
const result = spawn(bin, argv, { cwd: workdir, encoding: 'utf8', timeout: timeoutMs });
|
|
270
|
+
if (result.error) {
|
|
271
|
+
throw new WorkerContextBootstrapError(`worker context deploy failed to run (${result.error?.message});`
|
|
272
|
+
+ ` missing in ${workdir}: ${status.missing.join('; ')}`, `install the aiwg CLI on PATH or set AIWG_BIN to its location (tried '${bin}'),`
|
|
273
|
+
+ ` or run 'aiwg ${argv.join(' ')}' manually before launch`, { provider, workdir });
|
|
274
|
+
}
|
|
275
|
+
if (result.status !== 0) {
|
|
276
|
+
throw new WorkerContextBootstrapError(`worker context deploy exited ${result.status}: ${String(result.stderr ?? '').trim().slice(0, 2000)};`
|
|
277
|
+
+ ` missing in ${workdir}: ${status.missing.join('; ')}`, `run 'aiwg ${argv.join(' ')}' manually and resolve the reported failure before launch`, { provider, workdir });
|
|
278
|
+
}
|
|
279
|
+
return { providers: [...deployProviders] };
|
|
280
|
+
});
|
|
281
|
+
let deployed;
|
|
282
|
+
try {
|
|
283
|
+
deployed = await deploy({ worktreePath: workdir, providers: synced.providers, aiwgBin });
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (error instanceof WorkerContextBootstrapError)
|
|
287
|
+
throw error;
|
|
288
|
+
throw new WorkerContextBootstrapError(`worker context deploy failed: ${error instanceof Error ? error.message : String(error)};`
|
|
289
|
+
+ ` missing in ${workdir}: ${status.missing.join('; ')}`, `run 'aiwg ${argv.join(' ')}' manually and resolve the reported failure before launch`, { provider, workdir });
|
|
290
|
+
}
|
|
291
|
+
const after = statusFn({ workdir, provider });
|
|
292
|
+
if (!after.present) {
|
|
293
|
+
throw new WorkerContextBootstrapError(`worker context deploy did not produce the bootstrap: still missing in ${workdir}: ${after.missing.join('; ')}`, `run 'aiwg ${argv.join(' ')}' manually and verify the provider bootstrap before launch`, { provider, workdir });
|
|
294
|
+
}
|
|
295
|
+
// Keep deploy-touched tracked bridges out of the worker diff. A workdir
|
|
296
|
+
// outside any git repository has no tracked files; skip silently there.
|
|
297
|
+
let skippedBridges = [];
|
|
298
|
+
if (options.skipWorktreeFn) {
|
|
299
|
+
skippedBridges = options.skipWorktreeFn({ workdir });
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
try {
|
|
303
|
+
skippedBridges = flagDeployDirtiedSkipWorktree({ workdir, spawn, timeoutMs });
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
skippedBridges = [];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { ensured: true, providers: [...deployed.providers], skippedBridges };
|
|
310
|
+
}
|
|
311
|
+
/** Tracked files the deploy modified, hidden from the worker diff. */
|
|
312
|
+
export function flagDeployDirtiedSkipWorktree({ workdir, spawn = spawnSync, timeoutMs = 30000 }) {
|
|
313
|
+
const status = spawn('git', ['status', '--porcelain=v1', '--untracked-files=no'], { cwd: workdir, encoding: 'utf8', timeout: timeoutMs });
|
|
314
|
+
if (status.error)
|
|
315
|
+
throw new Error(`git status failed: ${status.error?.message}`);
|
|
316
|
+
if (status.status !== 0)
|
|
317
|
+
throw new Error(`git status exited ${status.status}`);
|
|
318
|
+
const files = [];
|
|
319
|
+
for (const line of String(status.stdout ?? '').split('\n')) {
|
|
320
|
+
if (!line.trim())
|
|
321
|
+
continue;
|
|
322
|
+
const raw = line.slice(3);
|
|
323
|
+
const arrow = raw.indexOf(' -> ');
|
|
324
|
+
const rel = arrow >= 0 ? raw.slice(arrow + 4) : raw;
|
|
325
|
+
if (!rel || rel.startsWith('"'))
|
|
326
|
+
throw new Error(`worker context deploy dirtied an unflaggeable path: ${line}`);
|
|
327
|
+
files.push(rel);
|
|
328
|
+
}
|
|
329
|
+
if (files.length > 0) {
|
|
330
|
+
const flag = spawn('git', ['update-index', '--skip-worktree', '--', ...files], { cwd: workdir, encoding: 'utf8', timeout: timeoutMs });
|
|
331
|
+
if (flag.error || flag.status !== 0) {
|
|
332
|
+
throw new Error(`git update-index --skip-worktree failed: ${flag.error?.message ?? String(flag.stderr ?? '').trim()}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return files;
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=worker-context.js.map
|
|
@@ -178,6 +178,13 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
178
178
|
import { mcpError, mcpJson } from '../helpers.mjs';
|
|
179
179
|
import { buildShellStopControl, waitForUnitTermination } from './worker-shell-control.mjs';
|
|
180
180
|
import { launchPreparedShellWorker, sweepWorkerStdin } from './worker-stdin-runtime.mjs';
|
|
181
|
+
// NOTE: no static import of ../../config/worker-context.js here. That
|
|
182
|
+
// module is TypeScript: a literal relative import resolves under vitest
|
|
183
|
+
// but throws ERR_MODULE_NOT_FOUND under raw node (e.g. `node
|
|
184
|
+
// src/mcp/cli.mjs`, which test/unit/mcp exercises directly). The
|
|
185
|
+
// request-time loader below resolves it through importImpl (dist when
|
|
186
|
+
// built, source otherwise), so merely loading this bridge never touches
|
|
187
|
+
// it. See tools/_resolve-impl.mjs (#3062).
|
|
181
188
|
|
|
182
189
|
const execFileAsync = promisify(execFile);
|
|
183
190
|
|
|
@@ -642,6 +649,64 @@ function describeGateFailures(failures) {
|
|
|
642
649
|
return `worker dispatch gate blocked: ${failures.map(f => `${f.signal} (${f.reason})`).join('; ')}`;
|
|
643
650
|
}
|
|
644
651
|
|
|
652
|
+
/** Request-time loader for the TypeScript worker-context module (see the
|
|
653
|
+
* import note above): resolved through importImpl so production uses the
|
|
654
|
+
* compiled `dist` tree while a checkout falls back to source. Cached for
|
|
655
|
+
* the process lifetime; only request paths that actually ensure worker
|
|
656
|
+
* context ever trigger the load. */
|
|
657
|
+
let cachedWorkerContextModule = null;
|
|
658
|
+
async function loadWorkerContextModule() {
|
|
659
|
+
if (!cachedWorkerContextModule) cachedWorkerContextModule = await importImpl('config/worker-context.js');
|
|
660
|
+
return cachedWorkerContextModule;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** Map the worker CLI to its provider and ensure its bootstrap is present
|
|
664
|
+
* in the worker workdir (#3685 round 2). Deploys the resolved union set
|
|
665
|
+
* (config providers plus orchestration CLIs) in one pass when the worker
|
|
666
|
+
* provider's bootstrap is missing; an unmappable CLI refuses with the
|
|
667
|
+
* typed diagnostic, since no deploy could target it. */
|
|
668
|
+
async function ensureWorkerContextForInvocation({ invocation, config }) {
|
|
669
|
+
const { ensureWorkerContextBootstrap, mapCliToProviderId, resolveWorkerContextProvidersFromConfig, WorkerContextBootstrapError } =
|
|
670
|
+
await loadWorkerContextModule();
|
|
671
|
+
const cli = invocation?.selected?.cli;
|
|
672
|
+
const mapped = mapCliToProviderId(cli, config?.parallelism);
|
|
673
|
+
if (!('provider' in mapped) || !mapped.provider) {
|
|
674
|
+
throw new WorkerContextBootstrapError(
|
|
675
|
+
`cannot ensure worker context: CLI '${cli}' maps to no AIWG provider (${mapped.skipped ?? 'unmapped'})`,
|
|
676
|
+
`configure parallelism.clis.${cli} with a providers:<id> adapter, or dispatch a CLI whose adapter maps to a known provider`,
|
|
677
|
+
{ workdir: invocation?.cwd },
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
let providers = [mapped.provider];
|
|
681
|
+
try {
|
|
682
|
+
providers = [...new Set([...resolveWorkerContextProvidersFromConfig(config).providers, mapped.provider])];
|
|
683
|
+
} catch {
|
|
684
|
+
providers = [mapped.provider];
|
|
685
|
+
}
|
|
686
|
+
return ensureWorkerContextBootstrap({ workdir: invocation?.cwd, provider: mapped.provider, providers });
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** Refuse a failed bootstrap ensure as a typed blocked diagnostic that
|
|
690
|
+
* names the remedy — never a bare string the operator cannot act on.
|
|
691
|
+
* `code`/`remedy`/`provider`/`workdir` survive on any error object that
|
|
692
|
+
* carries them (the tools-side ensure throws a duck-typed refusal, not
|
|
693
|
+
* necessarily this module's class instance); only genuinely bare failures
|
|
694
|
+
* fall back to the generic code and remedy. */
|
|
695
|
+
function toWorkerContextDiagnostic(error) {
|
|
696
|
+
const fields = (error !== null && typeof error === 'object') ? error : {};
|
|
697
|
+
// Duck-typed on purpose: the worker-context module loads lazily (see
|
|
698
|
+
// above), so no class identity is available at module scope. Every
|
|
699
|
+
// refusal path sets `code`/`remedy` on the thrown object instead.
|
|
700
|
+
const code = (typeof fields.code === 'string' && fields.code ? fields.code : 'OG_WORKER_CONTEXT_UNAVAILABLE');
|
|
701
|
+
const remedy = (typeof fields.remedy === 'string' && fields.remedy ? fields.remedy
|
|
702
|
+
: 'ensure the worker CLI maps to a known AIWG provider and the aiwg CLI is installed (AIWG_BIN), then retry the run');
|
|
703
|
+
return { code,
|
|
704
|
+
message: error instanceof Error ? error.message : String(error?.message ?? error),
|
|
705
|
+
remedy,
|
|
706
|
+
...(fields.provider !== undefined ? { provider: fields.provider } : {}),
|
|
707
|
+
...(fields.workdir !== undefined ? { workdir: fields.workdir } : {}) };
|
|
708
|
+
}
|
|
709
|
+
|
|
645
710
|
// Audited observer; refresh the historical baseline graph after rebase.
|
|
646
711
|
const waitForUnitTerminationBaseline = waitForUnitTermination;
|
|
647
712
|
export { waitForUnitTerminationBaseline as __testOnlyWaitForUnitTerminationBaseline };
|
|
@@ -758,6 +823,7 @@ export class ChildProcessTransport {
|
|
|
758
823
|
// cap since a long-lived connection could otherwise grow it unbounded.
|
|
759
824
|
this._rawResponses = new Map();
|
|
760
825
|
this._buffer = undefined;
|
|
826
|
+
this._bufferBytes = 0;
|
|
761
827
|
}
|
|
762
828
|
|
|
763
829
|
async start() {
|
|
@@ -769,7 +835,7 @@ export class ChildProcessTransport {
|
|
|
769
835
|
try {
|
|
770
836
|
// Check each frame BEFORE concatenation or UTF-8 decoding.
|
|
771
837
|
let offset = 0;
|
|
772
|
-
let frameBytes = this.
|
|
838
|
+
let frameBytes = this._bufferBytes;
|
|
773
839
|
while (offset < chunk.length) {
|
|
774
840
|
const newline = chunk.indexOf(10, offset);
|
|
775
841
|
const end = newline === -1 ? chunk.length : newline;
|
|
@@ -782,8 +848,7 @@ export class ChildProcessTransport {
|
|
|
782
848
|
frameBytes = 0;
|
|
783
849
|
offset = newline + 1;
|
|
784
850
|
}
|
|
785
|
-
this.
|
|
786
|
-
this._drain();
|
|
851
|
+
this._drain(chunk);
|
|
787
852
|
} catch (error) { this.onerror?.(error); }
|
|
788
853
|
});
|
|
789
854
|
child.stdout?.on('error', error => this.onerror?.(error));
|
|
@@ -799,6 +864,7 @@ export class ChildProcessTransport {
|
|
|
799
864
|
if (this._closed) return;
|
|
800
865
|
this._closed = true;
|
|
801
866
|
this._buffer = undefined;
|
|
867
|
+
this._bufferBytes = 0;
|
|
802
868
|
this._rawResponses.clear();
|
|
803
869
|
this.onclose?.();
|
|
804
870
|
}
|
|
@@ -807,6 +873,7 @@ export class ChildProcessTransport {
|
|
|
807
873
|
if (this._failure) return;
|
|
808
874
|
this._failure = Object.assign(new Error(code), { code });
|
|
809
875
|
this._buffer = undefined;
|
|
876
|
+
this._bufferBytes = 0;
|
|
810
877
|
this._rawResponses.clear();
|
|
811
878
|
// Start stopping before pipe closure can exit the leader; onclose rejects
|
|
812
879
|
// pending SDK requests immediately while termination proof is awaited.
|
|
@@ -825,14 +892,26 @@ export class ChildProcessTransport {
|
|
|
825
892
|
// (own-property-preserving, retained in `_rawResponses` — see
|
|
826
893
|
// constructor doc) and once through the SDK's own `JSONRPCMessageSchema`
|
|
827
894
|
// (routed to `onmessage`, unchanged behavior for the Client/Protocol).
|
|
828
|
-
_drain() {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
895
|
+
_drain(chunk) {
|
|
896
|
+
// Retain fragments without copying or rescanning the growing partial
|
|
897
|
+
// frame on every pipe read. Join once, only when a newline arrives.
|
|
898
|
+
let offset = 0;
|
|
899
|
+
while (offset < chunk.length) {
|
|
900
|
+
const newline = chunk.indexOf(10, offset);
|
|
901
|
+
const end = newline === -1 ? chunk.length : newline;
|
|
902
|
+
if (end > offset) {
|
|
903
|
+
(this._buffer ??= []).push(chunk.subarray(offset, end));
|
|
904
|
+
this._bufferBytes += end - offset;
|
|
905
|
+
}
|
|
906
|
+
if (newline === -1) return;
|
|
907
|
+
const frame = this._buffer?.length === 1
|
|
908
|
+
? this._buffer[0] : Buffer.concat(this._buffer ?? [], this._bufferBytes);
|
|
909
|
+
const line = frame.toString('utf8').replace(/\r$/, '');
|
|
910
|
+
this._buffer = undefined;
|
|
911
|
+
this._bufferBytes = 0;
|
|
834
912
|
this._handleLine(line);
|
|
835
|
-
if (this.
|
|
913
|
+
if (this._closed || this._failure) return;
|
|
914
|
+
offset = newline + 1;
|
|
836
915
|
}
|
|
837
916
|
}
|
|
838
917
|
|
|
@@ -905,6 +984,7 @@ export class ChildProcessTransport {
|
|
|
905
984
|
try { signalChild(child, 'SIGKILL'); } catch { /* already gone */ }
|
|
906
985
|
}
|
|
907
986
|
this._buffer = undefined;
|
|
987
|
+
this._bufferBytes = 0;
|
|
908
988
|
}
|
|
909
989
|
}
|
|
910
990
|
|
|
@@ -1745,6 +1825,24 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
|
|
|
1745
1825
|
}
|
|
1746
1826
|
const { root, snapshot, drivers, invocation } = prefix;
|
|
1747
1827
|
|
|
1828
|
+
// #3685 round 2: the worker CLI's AIWG bootstrap must be present in
|
|
1829
|
+
// the worker workdir before dispatch — a context-free worker would run
|
|
1830
|
+
// with no project rules/skills/hooks. Present: no-op. Missing: exactly
|
|
1831
|
+
// one `aiwg use all` pass into the workdir (never elsewhere). A failed
|
|
1832
|
+
// ensure refuses as `blocked` with the typed diagnostic (never a
|
|
1833
|
+
// launch). Default on; `ensureWorkerContext: false` skips (synthetic
|
|
1834
|
+
// tests), a function value replaces the ensure.
|
|
1835
|
+
if (options.ensureWorkerContext !== false) {
|
|
1836
|
+
try {
|
|
1837
|
+
const ensure = typeof options.ensureWorkerContext === 'function'
|
|
1838
|
+
? options.ensureWorkerContext
|
|
1839
|
+
: ensureWorkerContextForInvocation;
|
|
1840
|
+
await ensure({ invocation, config: snapshot.config });
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
return markTerminal({ status: 'blocked', termination: 'confirmed', diagnostics: [toWorkerContextDiagnostic(error)] });
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1748
1846
|
// =========================================================================
|
|
1749
1847
|
// #2915 round 8 (D6): ONE decision, immediately after the prefix, from
|
|
1750
1848
|
// the launcher's own pure eligibility predicate,
|