@aiwg/cli 2026.9.5 → 2026.9.7
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/dist/src/cli/handlers/help.js +7 -1
- package/dist/src/cli/handlers/installation.js +4 -0
- package/dist/src/cli/handlers/mc.js +13 -20
- package/dist/src/cli/handlers/ralph.js +14 -4
- package/dist/src/cli/handlers/refresh.js +298 -30
- package/dist/src/cli/handlers/runtime-info.js +3 -0
- package/dist/src/cli/handlers/serve.js +21 -3
- package/dist/src/cli/handlers/use.js +114 -8
- package/dist/src/cli/handlers/utilities.js +26 -10
- package/dist/src/cli/services/deployment-verification.js +117 -1
- package/dist/src/cli/watch-service.js +47 -4
- package/dist/src/config/project-artifacts-health.mjs +15 -2
- package/dist/src/cost/fleet-report.js +19 -5
- package/dist/src/extensions/project-local-doctor.js +40 -2
- package/dist/src/extensions/project-quickref.js +4 -0
- package/dist/src/installation/manager.mjs +38 -3
- package/dist/src/mcp/helpers.mjs +56 -22
- package/dist/src/mcp/registry.js +32 -22
- package/dist/src/mcp/registry.mjs +31 -26
- package/dist/src/mcp/toml-editor.mjs +117 -0
- package/dist/src/mcp/tools/orchestration.mjs +7 -7
- package/dist/src/mcp/tools/subsystems.mjs +7 -7
- package/dist/src/memory/context-pack.js +5 -1
- package/dist/src/plugin/skill-command-translator.js +70 -1
- package/dist/src/serve/a2a-terminal-observer.js +19 -1
- package/dist/src/serve/mission-hitl.js +91 -0
- package/dist/src/sessions/import-lease.js +5 -1
- package/dist/src/smiths/context-pipeline/workspace-context.js +81 -5
- package/dist/src/testing/fixtures/test-data-factory.js +3 -3
- package/dist/src/writing/pattern-library.js +29 -6
- package/package.json +3 -1
- package/tools/agents/deploy-agents.mjs +87 -5
- package/tools/agents/providers/base.mjs +61 -2
package/dist/src/mcp/helpers.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { spawn } from 'node:child_process';
|
|
9
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
9
10
|
import fs from 'node:fs/promises';
|
|
10
11
|
import path from 'node:path';
|
|
11
12
|
|
|
@@ -72,7 +73,7 @@ export async function resolveProjectAiwgDir(projectDir) {
|
|
|
72
73
|
*/
|
|
73
74
|
export async function findProjectRoot(startDir = process.cwd()) {
|
|
74
75
|
let currentDir = startDir;
|
|
75
|
-
while (
|
|
76
|
+
while (true) {
|
|
76
77
|
const aiwgPath = path.join(currentDir, '.aiwg');
|
|
77
78
|
const pointerPath = path.join(currentDir, PROJECT_AIWG_LOCATION_FILE);
|
|
78
79
|
try {
|
|
@@ -87,7 +88,10 @@ export async function findProjectRoot(startDir = process.cwd()) {
|
|
|
87
88
|
} catch {
|
|
88
89
|
// continue up
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
+
// Inspect the root candidate too, then stop instead of revisiting it.
|
|
92
|
+
const parentDir = path.dirname(currentDir);
|
|
93
|
+
if (parentDir === currentDir) break;
|
|
94
|
+
currentDir = parentDir;
|
|
91
95
|
}
|
|
92
96
|
throw new Error('No .aiwg directory or .aiwg-location pointer found. Run from an AIWG project or `aiwg new` first.');
|
|
93
97
|
}
|
|
@@ -167,33 +171,56 @@ export function runAiwgCli(args, { cwd, env, timeoutMs = 120_000, input } = {})
|
|
|
167
171
|
});
|
|
168
172
|
let stdout = '';
|
|
169
173
|
let stderr = '';
|
|
170
|
-
|
|
174
|
+
const stdoutDecoder = new StringDecoder('utf8');
|
|
175
|
+
const stderrDecoder = new StringDecoder('utf8');
|
|
176
|
+
let settled = false;
|
|
177
|
+
let killTimer;
|
|
178
|
+
const rejectAndTerminate = (err) => {
|
|
179
|
+
if (settled) return;
|
|
180
|
+
settled = true;
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
// Settlement must not depend on a cooperative close event. Give the
|
|
183
|
+
// owned child one second to exit gracefully, then escalate cleanup.
|
|
184
|
+
reject(err);
|
|
185
|
+
killTimer = setTimeout(() => {
|
|
186
|
+
try { proc.kill('SIGKILL'); } catch { /* child may already have exited */ }
|
|
187
|
+
}, 1000);
|
|
188
|
+
killTimer.unref?.();
|
|
189
|
+
// Install cleanup first: kill() can synchronously trigger close in an adapter.
|
|
190
|
+
try { proc.kill('SIGTERM'); } catch { /* retain the original failure */ }
|
|
191
|
+
};
|
|
171
192
|
const timer = setTimeout(() => {
|
|
172
|
-
|
|
173
|
-
proc.kill('SIGTERM');
|
|
193
|
+
rejectAndTerminate(new Error(`aiwg ${args[0] || ''} timed out after ${timeoutMs}ms`));
|
|
174
194
|
}, timeoutMs);
|
|
175
195
|
|
|
176
|
-
proc.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
177
|
-
proc.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
196
|
+
proc.stdout.on('data', (chunk) => { stdout += stdoutDecoder.write(chunk); });
|
|
197
|
+
proc.stderr.on('data', (chunk) => { stderr += stderrDecoder.write(chunk); });
|
|
178
198
|
|
|
179
199
|
proc.on('close', (code) => {
|
|
180
200
|
clearTimeout(timer);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
201
|
+
clearTimeout(killTimer);
|
|
202
|
+
if (settled) return;
|
|
203
|
+
settled = true;
|
|
204
|
+
stdout += stdoutDecoder.end();
|
|
205
|
+
stderr += stderrDecoder.end();
|
|
185
206
|
resolve({ stdout, stderr, code: code ?? -1 });
|
|
186
207
|
});
|
|
187
208
|
proc.on('error', (err) => {
|
|
188
209
|
clearTimeout(timer);
|
|
210
|
+
// A late error must not cancel cleanup of a failed, still-live child.
|
|
211
|
+
if (settled) return;
|
|
212
|
+
settled = true;
|
|
189
213
|
reject(err);
|
|
190
214
|
});
|
|
191
215
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
proc.stdin.
|
|
216
|
+
// Pipe errors are emitted on stdin, not on the ChildProcess. Register
|
|
217
|
+
// before writing so synchronous adapter events and late EPIPE are handled.
|
|
218
|
+
proc.stdin.on('error', rejectAndTerminate);
|
|
219
|
+
try {
|
|
220
|
+
if (input !== undefined) proc.stdin.write(input);
|
|
221
|
+
if (!settled) proc.stdin.end();
|
|
222
|
+
} catch (err) {
|
|
223
|
+
rejectAndTerminate(err);
|
|
197
224
|
}
|
|
198
225
|
});
|
|
199
226
|
}
|
|
@@ -244,13 +271,20 @@ export async function loadCommandAllowList() {
|
|
|
244
271
|
}
|
|
245
272
|
}
|
|
246
273
|
if (!text) {
|
|
247
|
-
//
|
|
274
|
+
// Installed packages need not contain TypeScript sources. Ask the CLI for
|
|
275
|
+
// its versioned canonical registry; human help is incomplete and contains examples.
|
|
248
276
|
try {
|
|
249
|
-
const { stdout } = await runAiwgCli(['help'], { timeoutMs: 30_000 });
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
277
|
+
const { stdout, code } = await runAiwgCli(['help', '--json'], { timeoutMs: 30_000 });
|
|
278
|
+
if (code !== 0) throw new Error('Command registry subprocess failed');
|
|
279
|
+
const registry = JSON.parse(stdout);
|
|
280
|
+
const ids = registry?.commandIds;
|
|
281
|
+
if (registry?.schema !== 'aiwg.command-registry.v1'
|
|
282
|
+
|| !Array.isArray(ids) || ids.length === 0
|
|
283
|
+
|| ids.some(id => typeof id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(id))
|
|
284
|
+
|| new Set(ids).size !== ids.length) {
|
|
285
|
+
throw new Error('Invalid command registry response');
|
|
286
|
+
}
|
|
287
|
+
_commandIds = new Set(ids);
|
|
254
288
|
return _commandIds;
|
|
255
289
|
} catch (e) {
|
|
256
290
|
_commandIds = new Set();
|
package/dist/src/mcp/registry.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { manageOmpMcp } from './omp-config.mjs';
|
|
2
|
+
import { replaceServer } from './toml-editor.mjs';
|
|
2
3
|
import { resolveOmpPaths } from '../providers/omp-paths.mjs';
|
|
3
4
|
/**
|
|
4
5
|
* MCP Server Registry
|
|
@@ -246,18 +247,30 @@ function buildServerConfig(server, provider) {
|
|
|
246
247
|
/**
|
|
247
248
|
* Build a TOML section for a server (Codex/OpenAI provider).
|
|
248
249
|
*/
|
|
250
|
+
function tomlString(value) {
|
|
251
|
+
if (typeof value !== 'string' || [...value].some(char => {
|
|
252
|
+
const point = char.codePointAt(0);
|
|
253
|
+
return point >= 0xd800 && point <= 0xdfff;
|
|
254
|
+
}))
|
|
255
|
+
throw new Error('TOML values must be strings containing valid Unicode scalar values');
|
|
256
|
+
// JSON escapes align with TOML basic strings except DEL must also be escaped.
|
|
257
|
+
return JSON.stringify(value).replace(/\u007f/g, '\\u007f');
|
|
258
|
+
}
|
|
259
|
+
function tomlKey(value) {
|
|
260
|
+
return typeof value === 'string' && /^[A-Za-z0-9_-]+$/.test(value) ? value : tomlString(value);
|
|
261
|
+
}
|
|
249
262
|
function buildServerToml(server) {
|
|
250
263
|
const lines = [];
|
|
251
|
-
lines.push(`[mcp_servers.${server.name}]`);
|
|
264
|
+
lines.push(`[mcp_servers.${tomlKey(server.name)}]`);
|
|
252
265
|
if (server.type === 'stdio') {
|
|
253
|
-
lines.push(`command =
|
|
266
|
+
lines.push(`command = ${tomlString(server.command)}`);
|
|
254
267
|
if (server.args && server.args.length > 0) {
|
|
255
|
-
const argsStr = server.args.map(a =>
|
|
268
|
+
const argsStr = server.args.map(a => tomlString(a)).join(', ');
|
|
256
269
|
lines.push(`args = [${argsStr}]`);
|
|
257
270
|
}
|
|
258
271
|
}
|
|
259
272
|
else {
|
|
260
|
-
lines.push(`url =
|
|
273
|
+
lines.push(`url = ${tomlString(server.url)}`);
|
|
261
274
|
}
|
|
262
275
|
lines.push(`startup_timeout_sec = 10.0`);
|
|
263
276
|
lines.push(`tool_timeout_sec = 60.0`);
|
|
@@ -344,12 +357,21 @@ async function injectJson(registry, servers, configPath, provider, dryRun, resul
|
|
|
344
357
|
existing = JSON.parse(content);
|
|
345
358
|
}
|
|
346
359
|
catch (error) {
|
|
360
|
+
if (error instanceof SyntaxError) {
|
|
361
|
+
throw new Error(`Refusing to overwrite malformed MCP config ${configPath}: invalid JSON`);
|
|
362
|
+
}
|
|
347
363
|
if ((provider === 'antigravity' || provider === 'agy') && error?.code !== 'ENOENT') {
|
|
348
364
|
throw new Error(`Refusing to overwrite malformed MCP config ${configPath}: ${error.message}`);
|
|
349
365
|
}
|
|
366
|
+
if (error?.code !== 'ENOENT')
|
|
367
|
+
throw error;
|
|
350
368
|
}
|
|
351
369
|
// Determine the MCP servers key for this provider
|
|
352
370
|
const mcpKey = provider === 'opencode' ? 'mcp' : 'mcpServers';
|
|
371
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
372
|
+
if (!isObject(existing) || (Object.hasOwn(existing, mcpKey) && !isObject(existing[mcpKey]))) {
|
|
373
|
+
throw new Error('MCP configuration must contain an object root and an object server map');
|
|
374
|
+
}
|
|
353
375
|
const existingServers = existing[mcpKey] || {};
|
|
354
376
|
// Build new server entries
|
|
355
377
|
const newServers = { ...existingServers };
|
|
@@ -382,26 +404,17 @@ async function injectToml(registry, servers, configPath, provider, dryRun, resul
|
|
|
382
404
|
try {
|
|
383
405
|
existing = await readFile(configPath, 'utf-8');
|
|
384
406
|
}
|
|
385
|
-
catch {
|
|
386
|
-
|
|
407
|
+
catch (error) {
|
|
408
|
+
if (error.code !== 'ENOENT')
|
|
409
|
+
throw error;
|
|
387
410
|
}
|
|
388
|
-
const sectionsToAdd = [];
|
|
389
411
|
for (const server of servers) {
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const sectionRegex = new RegExp(`\\[mcp_servers\\.${escapeRegex(server.name)}\\][\\s\\S]*?(?=\\n\\[|$)`);
|
|
394
|
-
existing = existing.replace(sectionRegex, buildServerToml(server));
|
|
412
|
+
const edited = replaceServer(existing, server.name, buildServerToml(server));
|
|
413
|
+
existing = edited.text;
|
|
414
|
+
if (edited.alreadyPresent)
|
|
395
415
|
result.alreadyPresent.push(server.name);
|
|
396
|
-
}
|
|
397
|
-
else {
|
|
398
|
-
sectionsToAdd.push(buildServerToml(server));
|
|
399
|
-
}
|
|
400
416
|
result.serversInjected.push(server.name);
|
|
401
417
|
}
|
|
402
|
-
if (sectionsToAdd.length > 0) {
|
|
403
|
-
existing = existing.trimEnd() + '\n\n' + sectionsToAdd.join('\n\n') + '\n';
|
|
404
|
-
}
|
|
405
418
|
if (!dryRun) {
|
|
406
419
|
await mkdir(resolve(configPath, '..'), { recursive: true });
|
|
407
420
|
await writeFile(configPath, existing, 'utf-8');
|
|
@@ -411,9 +424,6 @@ async function injectToml(registry, servers, configPath, provider, dryRun, resul
|
|
|
411
424
|
}
|
|
412
425
|
return result;
|
|
413
426
|
}
|
|
414
|
-
function escapeRegex(str) {
|
|
415
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
416
|
-
}
|
|
417
427
|
/** All supported provider names for injection */
|
|
418
428
|
export const SUPPORTED_PROVIDERS = [
|
|
419
429
|
'antigravity',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { manageOmpMcp } from './omp-config.mjs';
|
|
2
|
+
import { replaceServer } from './toml-editor.mjs';
|
|
2
3
|
import { resolveOmpPaths } from '../providers/omp-paths.mjs';
|
|
3
4
|
/**
|
|
4
5
|
* MCP Server Registry (Runtime ESM)
|
|
@@ -269,18 +270,31 @@ function buildServerConfig(server, provider) {
|
|
|
269
270
|
}
|
|
270
271
|
}
|
|
271
272
|
|
|
273
|
+
function tomlString(value) {
|
|
274
|
+
if (typeof value !== 'string' || [...value].some(char => {
|
|
275
|
+
const point = char.codePointAt(0);
|
|
276
|
+
return point >= 0xd800 && point <= 0xdfff;
|
|
277
|
+
})) throw new Error('TOML values must be strings containing valid Unicode scalar values');
|
|
278
|
+
// JSON escapes align with TOML basic strings except DEL must also be escaped.
|
|
279
|
+
return JSON.stringify(value).replace(/\u007f/g, '\\u007f');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function tomlKey(value) {
|
|
283
|
+
return typeof value === 'string' && /^[A-Za-z0-9_-]+$/.test(value) ? value : tomlString(value);
|
|
284
|
+
}
|
|
285
|
+
|
|
272
286
|
function buildServerToml(server) {
|
|
273
287
|
const lines = [];
|
|
274
|
-
lines.push(`[mcp_servers.${server.name}]`);
|
|
288
|
+
lines.push(`[mcp_servers.${tomlKey(server.name)}]`);
|
|
275
289
|
|
|
276
290
|
if (server.type === 'stdio') {
|
|
277
|
-
lines.push(`command =
|
|
291
|
+
lines.push(`command = ${tomlString(server.command)}`);
|
|
278
292
|
if (server.args && server.args.length > 0) {
|
|
279
|
-
const argsStr = server.args.map(a =>
|
|
293
|
+
const argsStr = server.args.map(a => tomlString(a)).join(', ');
|
|
280
294
|
lines.push(`args = [${argsStr}]`);
|
|
281
295
|
}
|
|
282
296
|
} else {
|
|
283
|
-
lines.push(`url =
|
|
297
|
+
lines.push(`url = ${tomlString(server.url)}`);
|
|
284
298
|
}
|
|
285
299
|
|
|
286
300
|
lines.push(`startup_timeout_sec = 10.0`);
|
|
@@ -344,12 +358,20 @@ async function injectJson(registry, servers, configPath, provider, dryRun, resul
|
|
|
344
358
|
const content = await readFile(configPath, 'utf-8');
|
|
345
359
|
existing = JSON.parse(content);
|
|
346
360
|
} catch (error) {
|
|
361
|
+
if (error instanceof SyntaxError) {
|
|
362
|
+
throw new Error(`Refusing to overwrite malformed MCP config ${configPath}: invalid JSON`);
|
|
363
|
+
}
|
|
347
364
|
if (normalizeRuntimeProviderId(provider) === 'antigravity' && error?.code !== 'ENOENT') {
|
|
348
365
|
throw new Error(`Refusing to overwrite malformed MCP config ${configPath}: ${error.message}`);
|
|
349
366
|
}
|
|
367
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
350
368
|
}
|
|
351
369
|
|
|
352
370
|
const mcpKey = getMcpInjectionDefinition(provider)?.serversKey || 'mcpServers';
|
|
371
|
+
const isObject = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
372
|
+
if (!isObject(existing) || (Object.hasOwn(existing, mcpKey) && !isObject(existing[mcpKey]))) {
|
|
373
|
+
throw new Error('MCP configuration must contain an object root and an object server map');
|
|
374
|
+
}
|
|
353
375
|
const existingServers = existing[mcpKey] || {};
|
|
354
376
|
const newServers = { ...existingServers };
|
|
355
377
|
|
|
@@ -381,30 +403,17 @@ async function injectToml(registry, servers, configPath, provider, dryRun, resul
|
|
|
381
403
|
let existing = '';
|
|
382
404
|
try {
|
|
383
405
|
existing = await readFile(configPath, 'utf-8');
|
|
384
|
-
} catch {
|
|
385
|
-
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if (error.code !== 'ENOENT') throw error;
|
|
386
408
|
}
|
|
387
409
|
|
|
388
|
-
const sectionsToAdd = [];
|
|
389
|
-
|
|
390
410
|
for (const server of servers) {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
`\\[mcp_servers\\.${escapeRegex(server.name)}\\][\\s\\S]*?(?=\\n\\[|$)`,
|
|
395
|
-
);
|
|
396
|
-
existing = existing.replace(sectionRegex, buildServerToml(server));
|
|
397
|
-
result.alreadyPresent.push(server.name);
|
|
398
|
-
} else {
|
|
399
|
-
sectionsToAdd.push(buildServerToml(server));
|
|
400
|
-
}
|
|
411
|
+
const edited = replaceServer(existing, server.name, buildServerToml(server));
|
|
412
|
+
existing = edited.text;
|
|
413
|
+
if (edited.alreadyPresent) result.alreadyPresent.push(server.name);
|
|
401
414
|
result.serversInjected.push(server.name);
|
|
402
415
|
}
|
|
403
416
|
|
|
404
|
-
if (sectionsToAdd.length > 0) {
|
|
405
|
-
existing = existing.trimEnd() + '\n\n' + sectionsToAdd.join('\n\n') + '\n';
|
|
406
|
-
}
|
|
407
|
-
|
|
408
417
|
if (!dryRun) {
|
|
409
418
|
await mkdir(resolve(configPath, '..'), { recursive: true });
|
|
410
419
|
await writeFile(configPath, existing, 'utf-8');
|
|
@@ -417,8 +426,4 @@ async function injectToml(registry, servers, configPath, provider, dryRun, resul
|
|
|
417
426
|
return result;
|
|
418
427
|
}
|
|
419
428
|
|
|
420
|
-
function escapeRegex(str) {
|
|
421
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
422
|
-
}
|
|
423
|
-
|
|
424
429
|
export const SUPPORTED_PROVIDERS = listMcpInjectProviderIds();
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Pure source-range editing; provider filesystem access belongs to the caller.
|
|
2
|
+
import { parseTOML } from 'toml-eslint-parser';
|
|
3
|
+
|
|
4
|
+
const keys = node => node.key.keys.map(key => key.type === 'TOMLBare' ? key.name : key.value);
|
|
5
|
+
const starts = (path, prefix) => prefix.every((key, index) => path[index] === key);
|
|
6
|
+
function parse(text) {
|
|
7
|
+
try { return parseTOML(text, { tomlVersion: '1.0.0' }); }
|
|
8
|
+
catch { throw new Error('Invalid TOML configuration; no changes made'); }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function replaceServer(text, name, section) {
|
|
12
|
+
const ast = parse(text);
|
|
13
|
+
const target = ['mcp_servers', name];
|
|
14
|
+
const replacement = parse(section).body[0].body;
|
|
15
|
+
if (replacement.length !== 1 || replacement[0].type !== 'TOMLTable' ||
|
|
16
|
+
replacement[0].resolvedKey.length !== 2 || !starts(replacement[0].resolvedKey, target)) {
|
|
17
|
+
throw new Error('Invalid replacement server definition');
|
|
18
|
+
}
|
|
19
|
+
const inline = '{ ' + replacement[0].body.map(node => section.slice(...node.range)).join(', ') + ' }';
|
|
20
|
+
const encodedName = section.slice(...replacement[0].key.keys[1].range);
|
|
21
|
+
const edits = [];
|
|
22
|
+
let present = false;
|
|
23
|
+
let placed = false;
|
|
24
|
+
const edit = (range, value = '') => edits.push({ start: range[0], end: range[1], value });
|
|
25
|
+
|
|
26
|
+
function inspectValue(node, path) {
|
|
27
|
+
if (path[0] === 'mcp_servers') {
|
|
28
|
+
if (path.length <= 2 && node.type !== 'TOMLInlineTable') {
|
|
29
|
+
throw new Error('MCP configuration and server entries must be TOML tables');
|
|
30
|
+
}
|
|
31
|
+
if (starts(path, target)) present = true;
|
|
32
|
+
}
|
|
33
|
+
if (node.type === 'TOMLInlineTable') {
|
|
34
|
+
for (const entry of node.body) inspectValue(entry.value, [...path, ...keys(entry)]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const node of ast.body[0].body) {
|
|
38
|
+
if (node.type === 'TOMLTable') {
|
|
39
|
+
const path = node.resolvedKey;
|
|
40
|
+
if (path[0] === 'mcp_servers' &&
|
|
41
|
+
(typeof path[1] === 'number' || typeof path[2] === 'number')) {
|
|
42
|
+
throw new Error('MCP configuration and server entries must not be TOML arrays of tables');
|
|
43
|
+
}
|
|
44
|
+
if (starts(path, target)) present = true;
|
|
45
|
+
for (const entry of node.body) inspectValue(entry.value, [...path, ...keys(entry)]);
|
|
46
|
+
} else inspectValue(node.value, keys(node));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const selectedTables = ast.body[0].body.filter(node => node.type === 'TOMLTable' && starts(node.resolvedKey, target));
|
|
50
|
+
if (selectedTables.length === 1 && text.slice(...selectedTables[0].range) === section) {
|
|
51
|
+
return { text, alreadyPresent: true };
|
|
52
|
+
}
|
|
53
|
+
if (selectedTables.length === 1 && selectedTables[0].resolvedKey.length === 2) {
|
|
54
|
+
const [start, end] = selectedTables[0].range;
|
|
55
|
+
if (!ast.comments.some(comment => comment.range[0] >= start && comment.range[0] < end)) {
|
|
56
|
+
const output = text.slice(0, start) + section + text.slice(end);
|
|
57
|
+
parse(output);
|
|
58
|
+
return { text: output, alreadyPresent: true };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function editInlineMap(node) {
|
|
63
|
+
const entries = node.body;
|
|
64
|
+
const selected = entries.map((entry, index) => keys(entry)[0] === name ? index : -1).filter(index => index >= 0);
|
|
65
|
+
if (selected.length === 0) {
|
|
66
|
+
edit([node.range[1] - 1, node.range[1] - 1], `${entries.length ? ', ' : ''}${encodedName} = ${inline}`);
|
|
67
|
+
} else {
|
|
68
|
+
const first = selected[0];
|
|
69
|
+
if (keys(entries[first]).length === 1) edit(entries[first].value.range, inline);
|
|
70
|
+
else edit(entries[first].range, `${encodedName} = ${inline}`);
|
|
71
|
+
// Keep the first selected entry as the replacement anchor. Remove each
|
|
72
|
+
// subsequent contiguous run together with one separator, never a neighbor.
|
|
73
|
+
for (let cursor = 1; cursor < selected.length;) {
|
|
74
|
+
const start = selected[cursor];
|
|
75
|
+
let end = start;
|
|
76
|
+
while (cursor + 1 < selected.length && selected[cursor + 1] === end + 1) {
|
|
77
|
+
cursor++;
|
|
78
|
+
end++;
|
|
79
|
+
}
|
|
80
|
+
if (end + 1 < entries.length) edit([entries[start].range[0], entries[end + 1].range[0]]);
|
|
81
|
+
else edit([entries[start - 1].range[1], entries[end].range[1]]);
|
|
82
|
+
cursor++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
placed = true;
|
|
86
|
+
}
|
|
87
|
+
function planEntry(entry, base) {
|
|
88
|
+
const path = [...base, ...keys(entry)];
|
|
89
|
+
if (path.length === 1 && path[0] === 'mcp_servers') {
|
|
90
|
+
editInlineMap(entry.value);
|
|
91
|
+
} else if (starts(path, target)) {
|
|
92
|
+
if (path.length === 2) {
|
|
93
|
+
edit(entry.value.range, inline);
|
|
94
|
+
placed = true;
|
|
95
|
+
} else edit(entry.range);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const node of ast.body[0].body) {
|
|
99
|
+
if (node.type !== 'TOMLTable') { planEntry(node, []); continue; }
|
|
100
|
+
if (starts(node.resolvedKey, target)) {
|
|
101
|
+
const closing = ast.tokens.filter(token => token.range[0] >= node.key.range[1] && token.value === ']');
|
|
102
|
+
const last = closing[node.kind === 'array' ? 1 : 0];
|
|
103
|
+
if (!last) throw new Error('Invalid TOML table range');
|
|
104
|
+
edit([node.range[0], last.range[1]]);
|
|
105
|
+
for (const entry of node.body) edit(entry.range);
|
|
106
|
+
} else for (const entry of node.body) planEntry(entry, node.resolvedKey);
|
|
107
|
+
}
|
|
108
|
+
edits.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
109
|
+
for (let index = 1; index < edits.length; index++) {
|
|
110
|
+
if (edits[index].start < edits[index - 1].end) throw new Error('Overlapping TOML edit ranges');
|
|
111
|
+
}
|
|
112
|
+
let output = text;
|
|
113
|
+
for (const change of edits.reverse()) output = output.slice(0, change.start) + change.value + output.slice(change.end);
|
|
114
|
+
if (!placed) output += `${output.endsWith('\n') ? '' : '\n'}\n${section}\n`;
|
|
115
|
+
parse(output);
|
|
116
|
+
return { text: output, alreadyPresent: present };
|
|
117
|
+
}
|
|
@@ -235,13 +235,13 @@ export function registerMissionToolset(server) {
|
|
|
235
235
|
session_id: z.string().describe('Existing Mission Control session id from mc-start / mc-list'),
|
|
236
236
|
objective: z.string().describe('Mission objective'),
|
|
237
237
|
completion: z.string().describe('Measurable completion criterion'),
|
|
238
|
-
max_iterations: z.number().int().positive().optional().describe('Ralph iteration cap'),
|
|
239
|
-
max_total_tokens: z.number().int().positive().optional().describe('Hard cumulative token ceiling'),
|
|
240
|
-
max_output_tokens: z.number().int().positive().optional().describe('Hard cumulative output-token ceiling'),
|
|
241
|
-
max_tool_calls: z.number().int().positive().optional().describe('Hard cumulative tool-call ceiling'),
|
|
242
|
-
max_total_cost: z.number().positive().optional().describe('Hard cumulative provider-reported spend ceiling'),
|
|
243
|
-
max_wall_clock_minutes: z.number().positive().optional().describe('Hard cumulative runtime ceiling'),
|
|
244
|
-
exploration_quota: z.number().int().positive().optional().describe('Require structural variant after this many flat cycles (off unless declared; no default K)'),
|
|
238
|
+
max_iterations: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional().describe('Ralph iteration cap'),
|
|
239
|
+
max_total_tokens: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional().describe('Hard cumulative token ceiling'),
|
|
240
|
+
max_output_tokens: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional().describe('Hard cumulative output-token ceiling'),
|
|
241
|
+
max_tool_calls: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional().describe('Hard cumulative tool-call ceiling'),
|
|
242
|
+
max_total_cost: z.number().positive().finite().optional().describe('Hard cumulative provider-reported spend ceiling'),
|
|
243
|
+
max_wall_clock_minutes: z.number().positive().finite().optional().describe('Hard cumulative runtime ceiling'),
|
|
244
|
+
exploration_quota: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional().describe('Require structural variant after this many flat cycles (off unless declared; no default K)'),
|
|
245
245
|
budget_stop_policy: z.enum(['completion-wins', 'budget-wins']).optional().describe('Stop semantics when the completing iteration crosses a ceiling (default: completion-wins)'),
|
|
246
246
|
project_dir: z.string().optional().describe('Project directory for CLI dispatch'),
|
|
247
247
|
confirmed: z.boolean().default(false).describe('Required for durable/long-running mission dispatch'),
|
|
@@ -413,13 +413,13 @@ function registerMcToolset(server) {
|
|
|
413
413
|
session_id: z.string().describe('Session id'),
|
|
414
414
|
objective: z.string().describe('Mission objective'),
|
|
415
415
|
completion: z.string().optional().describe('Completion criteria'),
|
|
416
|
-
max_iterations: z.number().int().positive().optional().describe('Ralph iteration cap'),
|
|
417
|
-
max_total_tokens: z.number().int().positive().optional().describe('Hard cumulative token ceiling'),
|
|
418
|
-
max_output_tokens: z.number().int().positive().optional().describe('Hard cumulative output-token ceiling'),
|
|
419
|
-
max_tool_calls: z.number().int().positive().optional().describe('Hard cumulative tool-call ceiling'),
|
|
420
|
-
max_total_cost: z.number().positive().optional().describe('Hard cumulative provider-reported spend ceiling'),
|
|
421
|
-
max_wall_clock_minutes: z.number().positive().optional().describe('Hard cumulative runtime ceiling'),
|
|
422
|
-
exploration_quota: z.number().int().positive().optional().describe('Require structural variant after this many flat cycles (off unless declared; no default K)'),
|
|
416
|
+
max_iterations: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional().describe('Ralph iteration cap'),
|
|
417
|
+
max_total_tokens: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional().describe('Hard cumulative token ceiling'),
|
|
418
|
+
max_output_tokens: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional().describe('Hard cumulative output-token ceiling'),
|
|
419
|
+
max_tool_calls: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional().describe('Hard cumulative tool-call ceiling'),
|
|
420
|
+
max_total_cost: z.number().positive().finite().optional().describe('Hard cumulative provider-reported spend ceiling'),
|
|
421
|
+
max_wall_clock_minutes: z.number().positive().finite().optional().describe('Hard cumulative runtime ceiling'),
|
|
422
|
+
exploration_quota: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional().describe('Require structural variant after this many flat cycles (off unless declared; no default K)'),
|
|
423
423
|
budget_stop_policy: z.enum(['completion-wins', 'budget-wins']).optional().describe('Stop semantics when the completing iteration crosses a ceiling (default: completion-wins)'),
|
|
424
424
|
},
|
|
425
425
|
buildArgs: ({
|
|
@@ -265,6 +265,7 @@ export function buildContextPack(task, candidates, options = {}) {
|
|
|
265
265
|
};
|
|
266
266
|
}
|
|
267
267
|
export function buildWorkspaceContextPack(projectRoot, task, options = {}) {
|
|
268
|
+
const started = performance.now();
|
|
268
269
|
if (!task.trim())
|
|
269
270
|
throw new Error('context task must be nonblank');
|
|
270
271
|
const root = realpathSync(projectRoot);
|
|
@@ -277,6 +278,9 @@ export function buildWorkspaceContextPack(projectRoot, task, options = {}) {
|
|
|
277
278
|
return indexed.length > 0 ? indexed : wikiCandidates(root, taskTerms, maxFiles);
|
|
278
279
|
})(),
|
|
279
280
|
];
|
|
280
|
-
|
|
281
|
+
const pack = buildContextPack(task, candidates, { ...options, maxFiles });
|
|
282
|
+
// Workspace callers need retrieval plus assembly latency, not assembly alone.
|
|
283
|
+
pack.metrics.elapsedMs = Number((performance.now() - started).toFixed(3));
|
|
284
|
+
return pack;
|
|
281
285
|
}
|
|
282
286
|
//# sourceMappingURL=context-pack.js.map
|
|
@@ -11,8 +11,63 @@
|
|
|
11
11
|
* @implements .aiwg/architecture/adr-skills-canonical-extension-type.md
|
|
12
12
|
* @issue #550
|
|
13
13
|
*/
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
14
15
|
import fs from 'fs/promises';
|
|
15
16
|
import path from 'path';
|
|
17
|
+
/**
|
|
18
|
+
* Ownership signal for generated command files (#2507).
|
|
19
|
+
*
|
|
20
|
+
* Command wrappers were written as bare files: no `aiwg:managed` marker and no
|
|
21
|
+
* `.aiwg-manifest.json` entry. AIWG could neither count them as deployed nor
|
|
22
|
+
* recognise them as its own, so a later run reported the wrappers it had just
|
|
23
|
+
* written as unmanaged artifacts the operator should delete. They now carry the
|
|
24
|
+
* same signals as any other deployed artifact, tagged `skill-command` so the
|
|
25
|
+
* flat-command prune leaves them to the skills prune that governs their source.
|
|
26
|
+
*/
|
|
27
|
+
const MANAGED_SIDECAR = '.aiwg-manifest.json';
|
|
28
|
+
const MANAGED_MARKER_PATTERN = /^(?:<!--\s*aiwg:managed\s|#\s*aiwg:managed\s)/m;
|
|
29
|
+
function addManagedMarker(content, version, source) {
|
|
30
|
+
if (MANAGED_MARKER_PATTERN.test(content))
|
|
31
|
+
return content;
|
|
32
|
+
if (content.startsWith('---\n')) {
|
|
33
|
+
return content.replace(/^---\n/, `---\n# aiwg:managed v${version} ${source}\n`);
|
|
34
|
+
}
|
|
35
|
+
return `<!-- aiwg:managed v${version} ${source} -->\n${content}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Merge generated command entries into a directory's managed sidecar.
|
|
39
|
+
*
|
|
40
|
+
* Best-effort: a translation that cannot record ownership still produced a
|
|
41
|
+
* usable command file, so a sidecar failure must not fail the deploy.
|
|
42
|
+
*/
|
|
43
|
+
async function recordManagedCommands(targetDir, entries, version, source) {
|
|
44
|
+
if (entries.length === 0)
|
|
45
|
+
return;
|
|
46
|
+
const sidecarPath = path.join(targetDir, MANAGED_SIDECAR);
|
|
47
|
+
let sidecar = { managed: {} };
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(await fs.readFile(sidecarPath, 'utf-8'));
|
|
50
|
+
if (parsed && typeof parsed === 'object' && parsed.managed)
|
|
51
|
+
sidecar = parsed;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// No sidecar yet, or unreadable — start a fresh managed map.
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
sidecar.managed[entry.filename] = {
|
|
58
|
+
hash: `sha256:${createHash('sha256').update(entry.content).digest('hex')}`,
|
|
59
|
+
source,
|
|
60
|
+
version,
|
|
61
|
+
kind: 'skill-command',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await fs.writeFile(sidecarPath, `${JSON.stringify(sidecar, null, 2)}\n`, 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Non-fatal — the command files themselves are already written.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
16
71
|
// ============================================
|
|
17
72
|
// Provider Configuration
|
|
18
73
|
// ============================================
|
|
@@ -280,6 +335,9 @@ export async function translateSkillsToCommands(skillsDir, options) {
|
|
|
280
335
|
errors: [],
|
|
281
336
|
totalProcessed: 0,
|
|
282
337
|
};
|
|
338
|
+
// Ownership records for the commands this call writes (#2507).
|
|
339
|
+
const managedEntries = [];
|
|
340
|
+
const promptEntries = [];
|
|
283
341
|
// Check if this provider needs commands. nameFilter overrides the
|
|
284
342
|
// provider gating: when an operator passes an explicit filter (e.g. Claude
|
|
285
343
|
// flow→command emission per PUW-015 #1116), they're opting in to selective
|
|
@@ -328,7 +386,7 @@ export async function translateSkillsToCommands(skillsDir, options) {
|
|
|
328
386
|
continue;
|
|
329
387
|
}
|
|
330
388
|
// Generate command content
|
|
331
|
-
const commandContent = generateCommandContent(skillName, frontmatter, body, options.provider);
|
|
389
|
+
const commandContent = addManagedMarker(generateCommandContent(skillName, frontmatter, body, options.provider), options.deployVersion ?? 'unknown', 'bundled');
|
|
332
390
|
const commandFilename = `${skillName}.md`;
|
|
333
391
|
const translated = {
|
|
334
392
|
sourcePath: skillMdPath,
|
|
@@ -353,7 +411,9 @@ export async function translateSkillsToCommands(skillsDir, options) {
|
|
|
353
411
|
const promptPath = path.join(promptsDir, `${skillName}.prompt.md`);
|
|
354
412
|
await fs.mkdir(promptsDir, { recursive: true });
|
|
355
413
|
await fs.writeFile(promptPath, commandContent, 'utf-8');
|
|
414
|
+
promptEntries.push({ filename: `${skillName}.prompt.md`, content: commandContent });
|
|
356
415
|
}
|
|
416
|
+
managedEntries.push({ filename: commandFilename, content: commandContent });
|
|
357
417
|
}
|
|
358
418
|
result.translated.push(translated);
|
|
359
419
|
if (options.verbose) {
|
|
@@ -373,6 +433,15 @@ export async function translateSkillsToCommands(skillsDir, options) {
|
|
|
373
433
|
}
|
|
374
434
|
}
|
|
375
435
|
}
|
|
436
|
+
if (!options.dryRun) {
|
|
437
|
+
const version = options.deployVersion ?? 'unknown';
|
|
438
|
+
await recordManagedCommands(options.targetDir, managedEntries, version, 'bundled');
|
|
439
|
+
if (promptEntries.length > 0) {
|
|
440
|
+
const projectRoot = options.projectPath
|
|
441
|
+
?? path.dirname(path.dirname(options.targetDir));
|
|
442
|
+
await recordManagedCommands(path.join(projectRoot, '.github', 'prompts'), promptEntries, version, 'bundled');
|
|
443
|
+
}
|
|
444
|
+
}
|
|
376
445
|
return result;
|
|
377
446
|
}
|
|
378
447
|
/**
|