@aiwg/cli 2026.10.6 → 2026.10.8
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/bin/aiwg.mjs +4 -2
- package/dist/src/a2a/agent-card.js +11 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
- package/dist/src/artifacts/fortemi-shard-export.js +284 -0
- package/dist/src/artifacts/index-preparation.js +2 -0
- package/dist/src/auth/client.js +5 -1
- package/dist/src/auth/credential-store.js +5 -1
- package/dist/src/auth/sensitive-input.js +151 -0
- package/dist/src/channel/manager.mjs +3 -0
- package/dist/src/cli/handlers/agentcard.js +47 -25
- package/dist/src/cli/handlers/auth.js +22 -2
- package/dist/src/cli/handlers/cost-report.js +7 -1
- package/dist/src/cli/handlers/decision.js +36 -10
- package/dist/src/cli/handlers/effect.js +4 -84
- package/dist/src/cli/handlers/guided-setup.js +1 -1
- package/dist/src/cli/handlers/init.js +1 -1
- package/dist/src/cli/handlers/install.js +6 -0
- package/dist/src/cli/handlers/local-executor.js +33 -6
- package/dist/src/cli/handlers/marketplace.js +15 -4
- package/dist/src/cli/handlers/setup.js +24 -20
- package/dist/src/cli/handlers/use.js +57 -15
- package/dist/src/cli/prompt-utils.js +73 -0
- package/dist/src/config/gitignore.js +4 -0
- package/dist/src/config/workspace.js +2 -1
- package/dist/src/cost/fleet-report.js +20 -22
- package/dist/src/decision/adapters/jev.js +20 -31
- package/dist/src/decision/ask.js +6 -1
- package/dist/src/decision/benchmark.js +19 -6
- package/dist/src/decision/driver.js +2 -2
- package/dist/src/decision/jev-credentials.js +18 -5
- package/dist/src/decision/ledger.js +230 -34
- package/dist/src/decision/qualification/gate-evidence.js +6 -4
- package/dist/src/decision/qualification/load-staged.js +47 -10
- package/dist/src/decision/qualification/runner.js +7 -3
- package/dist/src/discovery/interview.js +10 -5
- package/dist/src/discovery/plan-apply.js +26 -30
- package/dist/src/discovery/plan-validation.js +2 -2
- package/dist/src/effects/host.js +91 -0
- package/dist/src/extensions/commands/definitions.js +2 -2
- package/dist/src/extensions/project-local-gitignore.js +2 -0
- package/dist/src/issues/cli.js +16 -5
- package/dist/src/issues/live.js +44 -12
- package/dist/src/marketplace/exchange.js +18 -6
- package/dist/src/marketplace/provenance.js +3 -2
- package/dist/src/mcp/adapters/codex-runtime.js +46 -76
- package/dist/src/mcp/cli.mjs +122 -109
- package/dist/src/mcp/credentials.d.mts +4 -0
- package/dist/src/mcp/credentials.mjs +44 -3
- package/dist/src/mcp/grok-build-config.mjs +4 -1
- package/dist/src/mcp/muse-native-config.mjs +2 -1
- package/dist/src/mcp/omp-config.mjs +2 -0
- package/dist/src/mcp/registry.js +19 -15
- package/dist/src/mcp/registry.mjs +2 -18
- package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
- package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
- package/dist/src/memory/project-registry.js +2 -1
- package/dist/src/mission-protocol/worker-status.js +91 -1
- package/dist/src/models/grok-build-models.js +20 -0
- package/dist/src/models/worker-installation.d.mts +1 -1
- package/dist/src/models/worker-installation.mjs +65 -4
- package/dist/src/packages/adapters/git.js +99 -23
- package/dist/src/packages/adapters/gitea.js +8 -27
- package/dist/src/packages/adapters/github.js +7 -25
- package/dist/src/packages/registry.js +4 -4
- package/dist/src/process/cli-command.d.mts +5 -0
- package/dist/src/process/cli-command.mjs +42 -0
- package/dist/src/providers/provider-definitions.js +2 -2
- package/dist/src/serve/shared-host-scheduler.js +4 -1
- package/dist/src/serve/worker-invocation.js +10 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
- package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
- package/dist/src/smiths/context-pipeline/finalization.js +3 -2
- package/dist/src/smiths/context-pipeline/generator.js +7 -19
- package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
- package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
- package/dist/src/storage/backends/fortemi.js +107 -49
- package/dist/src/tracker/project-inputs.js +2 -0
- package/dist/src/update/notifier.mjs +2 -4
- package/package.json +1 -1
- package/tools/agents/providers/base.mjs +5 -1
- package/tools/agents/providers/claude.mjs +5 -2
- package/tools/agents/providers/codex.mjs +4 -1
- package/tools/agents/providers/context-write.mjs +28 -0
- package/tools/agents/providers/copilot.mjs +5 -2
- package/tools/agents/providers/hermes.mjs +5 -2
- package/tools/agents/providers/omp.mjs +3 -1
- package/tools/agents/providers/rule-delivery.mjs +29 -12
- package/tools/agents/providers/warp.mjs +4 -1
- package/tools/agents/providers/windsurf.mjs +6 -3
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
- package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
- package/tools/ralph-external/process-monitor.mjs +42 -4
- package/tools/ralph-external/session-launcher.mjs +125 -3
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* @issue #972
|
|
38
38
|
*/
|
|
39
39
|
import { createHash } from 'node:crypto';
|
|
40
|
+
import { debuglog } from 'node:util';
|
|
40
41
|
const FORTEMI_QUERY_LIMIT = 50;
|
|
41
42
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
42
43
|
function schemaProperties(tool) {
|
|
@@ -371,8 +372,8 @@ export class FortemiAdapter {
|
|
|
371
372
|
* Implemented as a lazy import so tests that inject a stub never load
|
|
372
373
|
* the SDK or touch the registry.
|
|
373
374
|
*/
|
|
374
|
-
export const createDefaultMcpClient = async (serverName, registryOverride, environment = process.env) => {
|
|
375
|
-
const { McpServerRegistry } = await import('../../mcp/registry.js');
|
|
375
|
+
export const createDefaultMcpClient = async (serverName, registryOverride, environment = process.env, sensitiveInput = {}) => {
|
|
376
|
+
const { McpServerRegistry, resolveMcpCredentialMaps } = await import('../../mcp/registry.js');
|
|
376
377
|
const registry = registryOverride ?? new McpServerRegistry();
|
|
377
378
|
const server = await registry.get(serverName);
|
|
378
379
|
if (!server) {
|
|
@@ -381,60 +382,117 @@ export const createDefaultMcpClient = async (serverName, registryOverride, envir
|
|
|
381
382
|
}
|
|
382
383
|
// Lazy imports keep unit tests that inject a stub isolated from transports.
|
|
383
384
|
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
env: server.env,
|
|
391
|
-
});
|
|
385
|
+
const hasNewReferences = [...Object.values(server.headerEnv ?? {}), ...Object.values(server.envFrom ?? {})]
|
|
386
|
+
.some(reference => /^(?:file|store):/.test(reference));
|
|
387
|
+
// Keep legacy header/environment behavior unchanged. New locators are resolved
|
|
388
|
+
// only at connection time and never fed back to the registry or config writers.
|
|
389
|
+
if (hasNewReferences && server.type === 'stdio' && debuglog('child_process').enabled) {
|
|
390
|
+
throw new Error('MCP file/store stdio credentials require NODE_DEBUG child_process logging to be disabled.');
|
|
392
391
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
392
|
+
const resolved = hasNewReferences
|
|
393
|
+
? await resolveMcpCredentialMaps(server, { ...sensitiveInput, environment })
|
|
394
|
+
: undefined;
|
|
395
|
+
const secrets = resolved ? [
|
|
396
|
+
...Object.keys(server.envFrom ?? {}).map(key => resolved.env?.[key]),
|
|
397
|
+
...Object.keys(server.headerEnv ?? {}).map(key => resolved.headers?.[key]),
|
|
398
|
+
].filter((value) => Boolean(value)).sort((a, b) => b.length - a.length) : [];
|
|
399
|
+
const redact = (text) => secrets.reduce((text, secret) => text.split(secret).join('[REDACTED]'), text);
|
|
400
|
+
const protect = async (operation) => {
|
|
401
|
+
try {
|
|
402
|
+
return await operation();
|
|
396
403
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (server.type === 'http') {
|
|
400
|
-
const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
|
401
|
-
transport = new StreamableHTTPClientTransport(url, {
|
|
402
|
-
requestInit: { headers },
|
|
403
|
-
});
|
|
404
|
+
catch (error) {
|
|
405
|
+
throw new Error(redact(error instanceof Error ? error.message : 'MCP transport failed'));
|
|
404
406
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
407
|
+
};
|
|
408
|
+
const protectResult = (value) => {
|
|
409
|
+
if (typeof value === 'string')
|
|
410
|
+
return redact(value);
|
|
411
|
+
if (Array.isArray(value))
|
|
412
|
+
return value.map(protectResult);
|
|
413
|
+
if (value && typeof value === 'object')
|
|
414
|
+
return Object.fromEntries(Object.entries(value).map(([key, value]) => [redact(key), protectResult(value)]));
|
|
415
|
+
return value;
|
|
416
|
+
};
|
|
417
|
+
try {
|
|
418
|
+
let transport;
|
|
419
|
+
if (server.type === 'stdio') {
|
|
420
|
+
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
|
|
421
|
+
transport = new StdioClientTransport({
|
|
422
|
+
command: server.command ?? '',
|
|
423
|
+
args: server.args ?? [],
|
|
424
|
+
env: resolved?.env ?? server.env,
|
|
425
|
+
...(hasNewReferences ? { stderr: 'pipe' } : {}),
|
|
417
426
|
});
|
|
418
427
|
}
|
|
419
428
|
else {
|
|
420
|
-
|
|
429
|
+
if (!server.url) {
|
|
430
|
+
throw new Error(`storage(fortemi): MCP server "${serverName}" has no URL`);
|
|
431
|
+
}
|
|
432
|
+
const url = validateRemoteMcpUrl(server.url);
|
|
433
|
+
const headers = resolved?.headers ?? resolveMcpRequestHeaders(server, environment);
|
|
434
|
+
if (resolved) {
|
|
435
|
+
for (const header of Object.keys(server.headerEnv ?? {})) {
|
|
436
|
+
if (header.toLowerCase() === 'authorization')
|
|
437
|
+
headers[header] = `Bearer ${headers[header]}`;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (server.type === 'http') {
|
|
441
|
+
const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
|
442
|
+
transport = new StreamableHTTPClientTransport(url, {
|
|
443
|
+
requestInit: { headers },
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
else if (server.type === 'sse') {
|
|
447
|
+
const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
|
|
448
|
+
transport = new SSEClientTransport(url, {
|
|
449
|
+
requestInit: { headers },
|
|
450
|
+
eventSourceInit: {
|
|
451
|
+
fetch: async (input, init) => {
|
|
452
|
+
const merged = new Headers(init?.headers);
|
|
453
|
+
for (const [name, value] of Object.entries(headers))
|
|
454
|
+
merged.set(name, value);
|
|
455
|
+
return fetch(input, { ...init, headers: merged });
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
throw new Error(`storage(fortemi): unsupported MCP transport "${String(server.type)}"`);
|
|
462
|
+
}
|
|
421
463
|
}
|
|
464
|
+
const client = new Client({ name: 'aiwg-storage-fortemi-adapter', version: '1.0.0' }, { capabilities: {} });
|
|
465
|
+
if (hasNewReferences && server.type === 'stdio') {
|
|
466
|
+
// The recipient's stderr can contain credentials. Drain without forwarding.
|
|
467
|
+
transport.stderr?.on('data', () => { });
|
|
468
|
+
}
|
|
469
|
+
if (hasNewReferences)
|
|
470
|
+
await protect(() => client.connect(transport));
|
|
471
|
+
else
|
|
472
|
+
await client.connect(transport);
|
|
473
|
+
return {
|
|
474
|
+
async callTool(name, args) {
|
|
475
|
+
const call = async () => unwrapMcpToolResult(await client.callTool({ name, arguments: args }));
|
|
476
|
+
return hasNewReferences ? protectResult(await protect(call)) : call();
|
|
477
|
+
},
|
|
478
|
+
async listTools() {
|
|
479
|
+
return hasNewReferences ? protectResult(await protect(() => client.listTools())) : client.listTools();
|
|
480
|
+
},
|
|
481
|
+
serverVersion() {
|
|
482
|
+
return hasNewReferences ? protectResult(client.getServerVersion()) : client.getServerVersion();
|
|
483
|
+
},
|
|
484
|
+
async close() {
|
|
485
|
+
if (hasNewReferences)
|
|
486
|
+
await protect(() => client.close());
|
|
487
|
+
else
|
|
488
|
+
await client.close();
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
if (!hasNewReferences)
|
|
494
|
+
throw error;
|
|
495
|
+
throw new Error(redact(error instanceof Error ? error.message : 'MCP transport failed'));
|
|
422
496
|
}
|
|
423
|
-
const client = new Client({ name: 'aiwg-storage-fortemi-adapter', version: '1.0.0' }, { capabilities: {} });
|
|
424
|
-
await client.connect(transport);
|
|
425
|
-
return {
|
|
426
|
-
async callTool(name, args) {
|
|
427
|
-
return unwrapMcpToolResult(await client.callTool({ name, arguments: args }));
|
|
428
|
-
},
|
|
429
|
-
async listTools() {
|
|
430
|
-
return client.listTools();
|
|
431
|
-
},
|
|
432
|
-
serverVersion() {
|
|
433
|
-
return client.getServerVersion();
|
|
434
|
-
},
|
|
435
|
-
async close() {
|
|
436
|
-
await client.close();
|
|
437
|
-
},
|
|
438
|
-
};
|
|
439
497
|
};
|
|
440
498
|
//# sourceMappingURL=fortemi.js.map
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { execFile } from 'node:child_process';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
|
+
import { helperEnvironment } from '../process/cli-command.mjs';
|
|
8
9
|
import { readAiwgConfig } from '../config/aiwg-config.js';
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
/** The project config, or `null` when it is missing or unreadable. */
|
|
@@ -20,6 +21,7 @@ export async function readConfig(projectPath) {
|
|
|
20
21
|
export async function readGitRemoteUrls(projectPath) {
|
|
21
22
|
try {
|
|
22
23
|
const { stdout } = await execFileAsync('git', ['-C', projectPath, 'remote', '-v'], {
|
|
24
|
+
env: helperEnvironment(),
|
|
23
25
|
maxBuffer: 1024 * 1024,
|
|
24
26
|
timeout: 10_000,
|
|
25
27
|
});
|
|
@@ -36,6 +36,7 @@ import { spawn } from 'child_process';
|
|
|
36
36
|
import { fileURLToPath } from 'url';
|
|
37
37
|
import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
|
|
38
38
|
import { loadInstallationIdentity } from '../installation/manager.mjs';
|
|
39
|
+
import { helperEnvironment } from '../process/cli-command.mjs';
|
|
39
40
|
|
|
40
41
|
const CACHE_DIR = resolveUserConfigDir();
|
|
41
42
|
const CACHE_FILE = path.join(CACHE_DIR, 'update-notifier.json');
|
|
@@ -261,10 +262,7 @@ const NOTIFIER_CHILD_ENV_ALLOWLIST = Object.freeze([
|
|
|
261
262
|
* secret-shaped variable" (go/no-go review, blocker 1 tests).
|
|
262
263
|
*/
|
|
263
264
|
export function notifierChildEnv(parentEnv = process.env) {
|
|
264
|
-
const env =
|
|
265
|
-
for (const name of NOTIFIER_CHILD_ENV_ALLOWLIST) {
|
|
266
|
-
if (parentEnv[name] !== undefined) env[name] = parentEnv[name];
|
|
267
|
-
}
|
|
265
|
+
const env = helperEnvironment(parentEnv, NOTIFIER_CHILD_ENV_ALLOWLIST);
|
|
268
266
|
env.AIWG_UPDATE_NOTIFIER_CHILD = '1';
|
|
269
267
|
return env;
|
|
270
268
|
}
|
package/package.json
CHANGED
|
@@ -17,10 +17,13 @@ import { createHash } from 'crypto';
|
|
|
17
17
|
import { createRequire } from 'module';
|
|
18
18
|
import { importImpl } from '../../_resolve-impl.mjs';
|
|
19
19
|
|
|
20
|
+
import { writeContextFileSync } from './context-write.mjs';
|
|
21
|
+
|
|
20
22
|
const { parseRuleFrontmatter, inferRuleActivation } = await importImpl(import.meta.url, 'rules/activation.mjs');
|
|
21
23
|
const { resolveRulesInlineBudget, DEFAULT_RULES_INLINE_BUDGET_TOKENS } = await importImpl(import.meta.url, 'rules/inline-budget.mjs');
|
|
22
24
|
export { DEFAULT_RULES_INLINE_BUDGET_TOKENS, parseRuleFrontmatter as parseRuleActivation };
|
|
23
25
|
const { boundedExecFileSync } = await importImpl(import.meta.url, 'process/bounded-child-process.mjs');
|
|
26
|
+
const { helperEnvironment } = await importImpl(import.meta.url, 'process/cli-command.mjs');
|
|
24
27
|
const { extractFrameworkSlug, sameSkillOwner, canDeploySkill, pruneRecordedSkillFiles, releaseSkillOwnership } =
|
|
25
28
|
await importImpl(import.meta.url, 'providers/skill-ownership.mjs');
|
|
26
29
|
export { extractFrameworkSlug, sameSkillOwner, pruneRecordedSkillFiles, releaseSkillOwnership };
|
|
@@ -1840,6 +1843,7 @@ export function buildRemotesTopologyBlock(targetDir) {
|
|
|
1840
1843
|
function getUrl(remote) {
|
|
1841
1844
|
try {
|
|
1842
1845
|
return boundedExecFileSync('git', ['-C', targetDir, 'remote', 'get-url', '--', remote], 'quick', {
|
|
1846
|
+
env: helperEnvironment(),
|
|
1843
1847
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1844
1848
|
encoding: 'utf8',
|
|
1845
1849
|
}).trim();
|
|
@@ -1949,7 +1953,7 @@ export function createManagedMdFromTemplate(target, destFilename, srcRoot, templ
|
|
|
1949
1953
|
if (dryRun) {
|
|
1950
1954
|
console.log(`[dry-run] Would ${verb} ${destFilename} AIWG-managed section`);
|
|
1951
1955
|
} else {
|
|
1952
|
-
|
|
1956
|
+
writeContextFileSync(destPath, content, 'utf8');
|
|
1953
1957
|
console.log(`${verb[0].toUpperCase()}${verb.slice(1)} ${destFilename} AIWG-managed section`);
|
|
1954
1958
|
}
|
|
1955
1959
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* Claude Code Provider
|
|
3
4
|
*
|
|
@@ -61,6 +62,8 @@ import {
|
|
|
61
62
|
interpolateContextTokens
|
|
62
63
|
} from './base.mjs';
|
|
63
64
|
|
|
65
|
+
import { writeContextFileSync } from './context-write.mjs';
|
|
66
|
+
|
|
64
67
|
// ============================================================================
|
|
65
68
|
// Provider Configuration
|
|
66
69
|
// ============================================================================
|
|
@@ -472,7 +475,7 @@ function deployHookFile(targetDir, opts) {
|
|
|
472
475
|
} else {
|
|
473
476
|
let content = fs.readFileSync(templatePath, 'utf8');
|
|
474
477
|
content = interpolateHookTokens(content, effectiveCounts, targetDir);
|
|
475
|
-
|
|
478
|
+
writeContextFileSync(hookDest, content, 'utf8');
|
|
476
479
|
console.log('Created AIWG.md (hook file)');
|
|
477
480
|
}
|
|
478
481
|
|
|
@@ -496,7 +499,7 @@ function deployHookFile(targetDir, opts) {
|
|
|
496
499
|
}
|
|
497
500
|
}
|
|
498
501
|
lines.splice(insertAt, 0, '', directive, '');
|
|
499
|
-
|
|
502
|
+
writeContextFileSync(claudeDest, lines.join('\n'), 'utf8');
|
|
500
503
|
console.log('Added @AIWG.md directive to CLAUDE.md');
|
|
501
504
|
}
|
|
502
505
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* OpenAI Codex Provider
|
|
3
4
|
*
|
|
@@ -68,6 +69,8 @@ import {
|
|
|
68
69
|
resolveAiwgRoot
|
|
69
70
|
} from './base.mjs';
|
|
70
71
|
|
|
72
|
+
import { writeContextFileSync } from './context-write.mjs';
|
|
73
|
+
|
|
71
74
|
function resolveCodexHelperRoot(srcRoot) {
|
|
72
75
|
const resolved = resolveAiwgRoot(srcRoot);
|
|
73
76
|
if (resolved) return resolved;
|
|
@@ -403,7 +406,7 @@ export function aggregateToAgentsMd(agentFiles, destPath, opts) {
|
|
|
403
406
|
}
|
|
404
407
|
const out = `${lines.join('\n')}\n`;
|
|
405
408
|
if (opts.dryRun) console.log(`[dry-run] write ${destPath}`);
|
|
406
|
-
else
|
|
409
|
+
else writeContextFileSync(destPath, out, 'utf8');
|
|
407
410
|
|
|
408
411
|
}
|
|
409
412
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context-file writes for deployment providers (#3037).
|
|
3
|
+
*
|
|
4
|
+
* The journaled writer lives in compiled TypeScript. In source-resolution
|
|
5
|
+
* mode (AIWG_RESOLVE_IMPL_FROM=src without a TS loader) it cannot load, and
|
|
6
|
+
* read-only consumers of provider modules (for example the remotes topology
|
|
7
|
+
* block) must still import cleanly. Writes then fail closed with the original
|
|
8
|
+
* load error, so nothing ever writes context files without the safety layer.
|
|
9
|
+
*/
|
|
10
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
11
|
+
|
|
12
|
+
// Mirrors CONTEXT_STAGE_ENV in src/smiths/context-pipeline/context-file-safety.ts.
|
|
13
|
+
export const CONTEXT_STAGE_ENV = 'AIWG_CONTEXT_STAGE_LOG';
|
|
14
|
+
|
|
15
|
+
let loadError = null;
|
|
16
|
+
let writer = null;
|
|
17
|
+
try {
|
|
18
|
+
writer = await importImpl(import.meta.url, 'smiths/context-pipeline/context-file-safety.js');
|
|
19
|
+
} catch (error) {
|
|
20
|
+
loadError = error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function writeContextFileSync(file, content, encoding) {
|
|
24
|
+
if (!writer) {
|
|
25
|
+
throw new Error(`Context-file safety layer is unavailable; refusing to write ${file}`, { cause: loadError });
|
|
26
|
+
}
|
|
27
|
+
return writer.writeContextFileSync(file, content, encoding);
|
|
28
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* GitHub Copilot Provider
|
|
3
4
|
*
|
|
@@ -54,6 +55,8 @@ import {
|
|
|
54
55
|
deploySoulCompanions,
|
|
55
56
|
loadRuntimeModelCatalog
|
|
56
57
|
} from './base.mjs';
|
|
58
|
+
|
|
59
|
+
import { writeContextFileSync } from './context-write.mjs';
|
|
57
60
|
const modelCatalog = loadRuntimeModelCatalog(staticModelCatalog);
|
|
58
61
|
|
|
59
62
|
// ============================================================================
|
|
@@ -477,14 +480,14 @@ export function createCopilotInstructions(target, srcRoot, dryRun) {
|
|
|
477
480
|
if (dryRun) {
|
|
478
481
|
console.log(`[dry-run] Would update existing copilot-instructions.md with AIWG section`);
|
|
479
482
|
} else {
|
|
480
|
-
|
|
483
|
+
writeContextFileSync(destPath, combined, 'utf8');
|
|
481
484
|
console.log('Updated copilot-instructions.md with AIWG SDLC framework section');
|
|
482
485
|
}
|
|
483
486
|
} else {
|
|
484
487
|
if (dryRun) {
|
|
485
488
|
console.log(`[dry-run] Would create copilot-instructions.md from template`);
|
|
486
489
|
} else {
|
|
487
|
-
|
|
490
|
+
writeContextFileSync(destPath, template, 'utf8');
|
|
488
491
|
console.log('Created copilot-instructions.md from template');
|
|
489
492
|
}
|
|
490
493
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* Hermes Agent Provider
|
|
3
4
|
*
|
|
@@ -41,6 +42,8 @@ import {
|
|
|
41
42
|
stripPlatformsFromContent,
|
|
42
43
|
} from './base.mjs';
|
|
43
44
|
|
|
45
|
+
import { writeContextFileSync } from './context-write.mjs';
|
|
46
|
+
|
|
44
47
|
// ============================================================================
|
|
45
48
|
// Hermes home resolution (HERMES_HOME) — #2119
|
|
46
49
|
// ============================================================================
|
|
@@ -355,7 +358,7 @@ MCP is optional; \`mcp_aiwg_rule_show\` exposes the same rule catalog.
|
|
|
355
358
|
if (dryRun) {
|
|
356
359
|
console.log(`[dry-run] Would write AGENTS.md (${output.length} chars, ${Math.round(output.length / 4)} tokens estimated)`);
|
|
357
360
|
} else {
|
|
358
|
-
|
|
361
|
+
writeContextFileSync(destPath, output, 'utf8');
|
|
359
362
|
const charCount = output.length;
|
|
360
363
|
const tokenEstimate = Math.round(charCount / 4);
|
|
361
364
|
let budgetNote;
|
|
@@ -397,7 +400,7 @@ when the full AIWG project context is needed.
|
|
|
397
400
|
if (dryRun) {
|
|
398
401
|
console.log(`[dry-run] Would write .hermes.md (${body.length} chars)`);
|
|
399
402
|
} else {
|
|
400
|
-
|
|
403
|
+
writeContextFileSync(destPath, mergeHermesContext(destPath, body, 'routing'), 'utf8');
|
|
401
404
|
console.log(` Created .hermes.md (${body.length} chars)`);
|
|
402
405
|
}
|
|
403
406
|
return 1;
|
|
@@ -5,6 +5,8 @@ import { createHash } from 'node:crypto';
|
|
|
5
5
|
import YAML from 'yaml';
|
|
6
6
|
import { importImpl } from '../../_resolve-impl.mjs';
|
|
7
7
|
import { withRuleDeployment, collectFrameworkArtifacts, getAddonFiles, isKernelSkill, listMdFiles, listSkillDirs, normalizeDeploymentMode, resolveAiwgRoot } from './base.mjs';
|
|
8
|
+
|
|
9
|
+
import { writeContextFileSync } from './context-write.mjs';
|
|
8
10
|
export const name = 'omp';
|
|
9
11
|
export const aliases = ['oh-my-pi'];
|
|
10
12
|
export const paths = { agents: '.omp/agents', commands: '.omp/prompts', skills: '.agents/skills', rules: '.omp/rules' };
|
|
@@ -168,7 +170,7 @@ export function createAgentsMd(target, srcRoot, dryRun = false) {
|
|
|
168
170
|
const content = current.includes(start) && current.includes(end) ? current.slice(0, current.indexOf(start)) + block + current.slice(current.indexOf(end) + end.length) : `${current}${current ? '\n\n' : ''}${block}\n`;
|
|
169
171
|
if (!dryRun) {
|
|
170
172
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
171
|
-
if (content !== current)
|
|
173
|
+
if (content !== current) writeContextFileSync(dest, content);
|
|
172
174
|
const receiptPath = path.join(path.dirname(dest), '.aiwg-manifest.json');
|
|
173
175
|
// Block ownership must never claim the operator's complete context file.
|
|
174
176
|
receipt.managed['AGENTS.md'] = { blockHash: `sha256:${hash(block)}`, source: 'omp-bootstrap', provider: 'omp', transformation: 'managed-block' };
|
|
@@ -19,6 +19,7 @@ const { loadRulePinConfigs, installedRuleCapabilities, resolveRulePins } = await
|
|
|
19
19
|
const { guardRulesInlineBudget } = await importImpl(import.meta.url, 'rules/inline-budget.mjs');
|
|
20
20
|
const { userConfigFile } = await importImpl(import.meta.url, 'config/user-config-dir.mjs');
|
|
21
21
|
const { resolveProjectAiwgDir } = await importImpl(import.meta.url, 'config/project-artifacts-runtime.mjs');
|
|
22
|
+
import { writeContextFileSync, CONTEXT_STAGE_ENV } from './context-write.mjs';
|
|
22
23
|
const PREPARATION_HOOK = Symbol.for('aiwg.provider-preparation-hook.v1');
|
|
23
24
|
|
|
24
25
|
export function installRuleDeploymentPreparationHook(hook) {
|
|
@@ -190,14 +191,17 @@ export async function prepareRuleDelivery(opts) {
|
|
|
190
191
|
const key = `${anchor}\0${entry.path}`;
|
|
191
192
|
if (!entryPaths.has(key)) entryPaths.set(key, path.resolve(anchor, entry.path));
|
|
192
193
|
const resolvedPath = entryPaths.get(key);
|
|
193
|
-
|
|
194
|
+
// Canonical IDs hash the indexed package-relative path. Keep it through
|
|
195
|
+
// resolution; the selected target is anchored to root below.
|
|
196
|
+
return { ...entry, path: entry.path.startsWith('agentic/code/') ? entry.path
|
|
197
|
+
: opts.mapInputPath ? opts.mapInputPath(resolvedPath) : resolvedPath };
|
|
194
198
|
});
|
|
195
199
|
let rebuilt = false;
|
|
196
200
|
const cacheDir = process.env.AIWG_RULE_BINDING_INDEX_CACHE;
|
|
197
201
|
const cacheFile = cacheDir && path.join(cacheDir, `${contentHash(opts.logicalInputPath ? opts.logicalInputPath(root) : root)}.json`);
|
|
198
202
|
const buildSourceEntries = async (refresh = false) => {
|
|
199
203
|
if (!refresh && cacheFile) {
|
|
200
|
-
try { return JSON.parse(fs.readFileSync(cacheFile, 'utf8')).map(entry => ({ ...entry, path: opts.mapInputPath ? opts.mapInputPath(entry.path) : entry.path })); }
|
|
204
|
+
try { return JSON.parse(fs.readFileSync(cacheFile, 'utf8')).map(entry => ({ ...entry, path: path.isAbsolute(entry.path) && opts.mapInputPath ? opts.mapInputPath(entry.path) : entry.path })); }
|
|
201
205
|
catch (error) {
|
|
202
206
|
if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) throw error;
|
|
203
207
|
}
|
|
@@ -206,7 +210,8 @@ export async function prepareRuleDelivery(opts) {
|
|
|
206
210
|
const sourceEntries = await collectRuleBindingEntries(root,
|
|
207
211
|
root === opts.srcRoot && !fs.existsSync(path.join(root, 'agentic')) ? '.' : 'agentic/code');
|
|
208
212
|
rebuilt = true;
|
|
209
|
-
const entries = sourceEntries.map(entry => ({ ...entry, path: path.
|
|
213
|
+
const entries = sourceEntries.map(entry => ({ ...entry, path: entry.path.startsWith('agentic/code/')
|
|
214
|
+
? entry.path : path.resolve(root, entry.path) }));
|
|
210
215
|
if (cacheFile) {
|
|
211
216
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
212
217
|
// Workers may publish the same immutable source snapshot concurrently.
|
|
@@ -324,7 +329,7 @@ function block(content, body) {
|
|
|
324
329
|
|
|
325
330
|
function putStartup(file, body) {
|
|
326
331
|
const content = fs.existsSync(file) ? read(file) : '<!-- aiwg-managed -->\n';
|
|
327
|
-
|
|
332
|
+
writeContextFileSync(file, block(content, body));
|
|
328
333
|
}
|
|
329
334
|
|
|
330
335
|
/** Tagged rules bypass legacy aggregate writers. Untagged projections retain their tier policy. */
|
|
@@ -337,7 +342,7 @@ function projectRules(opts, provider) {
|
|
|
337
342
|
const content = read(file);
|
|
338
343
|
const clean = content.replace(/(<!-- AIWG:hermes-context:start -->)([\s\S]*?)(<!-- AIWG:hermes-context:end -->)/g,
|
|
339
344
|
(_, start, body, end) => start + body.replace(/^### Rule: [\s\S]*?(?=^#{1,3} |(?![\s\S]))/gm, '') + end);
|
|
340
|
-
if (clean !== content)
|
|
345
|
+
if (clean !== content) writeContextFileSync(file, clean);
|
|
341
346
|
}
|
|
342
347
|
const nativePaths = ['claude', 'cursor'].includes(provider.name);
|
|
343
348
|
const rulesDir = path.isAbsolute(provider.paths.rules || '') ? provider.paths.rules : path.join(opts.target, provider.paths.rules || `.${provider.name}/rules`);
|
|
@@ -487,14 +492,14 @@ function projectRules(opts, provider) {
|
|
|
487
492
|
if (!fs.existsSync(file)) continue;
|
|
488
493
|
const content = read(file);
|
|
489
494
|
const clean = block(content, '');
|
|
490
|
-
if (clean !== content)
|
|
495
|
+
if (clean !== content) writeContextFileSync(file, clean);
|
|
491
496
|
}
|
|
492
497
|
return;
|
|
493
498
|
}
|
|
494
499
|
if (inlineStartup[provider.name]) {
|
|
495
500
|
// Retire the B1 indirect startup block when migrating to the actual loader.
|
|
496
501
|
const former = path.join(opts.target, 'AIWG.md');
|
|
497
|
-
if (fs.existsSync(former))
|
|
502
|
+
if (fs.existsSync(former)) writeContextFileSync(former, block(read(former), ''));
|
|
498
503
|
putStartup(path.join(opts.target, inlineStartup[provider.name]), body);
|
|
499
504
|
} else if (provider.name === 'hermes') {
|
|
500
505
|
const dest = path.join(opts.target, 'AGENTS.md');
|
|
@@ -916,12 +921,12 @@ function expectedAnchorPaths(paths) {
|
|
|
916
921
|
/** Adopt a detached worker plan into the coordinator's process-local lease. */
|
|
917
922
|
export function adoptRuleDeploymentPlan(detached, { scratch, target, inputPool, cleanup }) {
|
|
918
923
|
const invalid = () => { throw new Error('Invalid transferred rule deployment plan'); };
|
|
919
|
-
if (!exactObjectKeys(detached, ['version', 'lease', 'temp', 'target', 'stage', 'rootNames', 'workspaceDirs', 'preimages', 'poolSeal', 'anchors', 'snapshotRoot', 'corpusRoot', 'sourceMappings', 'staged', 'report', 'mappings']) ||
|
|
924
|
+
if (!exactObjectKeys(detached, ['version', 'lease', 'temp', 'target', 'stage', 'rootNames', 'workspaceDirs', 'preimages', 'poolSeal', 'anchors', 'snapshotRoot', 'corpusRoot', 'sourceMappings', 'staged', 'report', 'mappings', 'contextWrites']) ||
|
|
920
925
|
detached.version !== 1 || typeof detached.lease !== 'string' || !(detached.poolSeal === null || typeof detached.poolSeal === 'string') ||
|
|
921
926
|
!detached.report || typeof detached.report !== 'object' || Array.isArray(detached.report) || typeof scratch !== 'string' || typeof target !== 'string' || typeof cleanup !== 'function') invalid();
|
|
922
927
|
const root = path.resolve(scratch), expectedTarget = path.resolve(target);
|
|
923
928
|
if (path.resolve(detached.temp || '') !== root || path.resolve(detached.stage || '') !== path.join(root, 'project') || path.resolve(detached.target || '') !== expectedTarget) invalid();
|
|
924
|
-
if (!Array.isArray(detached.staged) || !Array.isArray(detached.mappings) || !Array.isArray(detached.rootNames) || !Array.isArray(detached.preimages) ||
|
|
929
|
+
if (!Array.isArray(detached.contextWrites) || detached.contextWrites.some(file => typeof file !== 'string' || !within(root, file)) || !Array.isArray(detached.staged) || !Array.isArray(detached.mappings) || !Array.isArray(detached.rootNames) || !Array.isArray(detached.preimages) ||
|
|
925
930
|
!Array.isArray(detached.workspaceDirs) || !Array.isArray(detached.anchors) || !Array.isArray(detached.sourceMappings) ||
|
|
926
931
|
typeof detached.snapshotRoot !== 'string' || !path.isAbsolute(detached.snapshotRoot) || typeof detached.corpusRoot !== 'string' || !path.isAbsolute(detached.corpusRoot)) invalid();
|
|
927
932
|
if (detached.rootNames.some(name => !safeRelative(expectedTarget, name))) invalid();
|
|
@@ -1011,6 +1016,10 @@ export async function prepareRuleDeployment(original, provider, deploy) {
|
|
|
1011
1016
|
const removeTemp = () => { removeStage(); if (pool) releaseInputPool(pool); };
|
|
1012
1017
|
const lease = randomUUID();
|
|
1013
1018
|
stagedCleanups.set(lease, { temp, cleanup: removeTemp, detach: () => { if (pool) releaseInputPool(pool); }, pool, seal: null, transferable });
|
|
1019
|
+
const priorContextStage = process.env[CONTEXT_STAGE_ENV];
|
|
1020
|
+
const contextStageLog = path.join(temp, 'context-writes.jsonl');
|
|
1021
|
+
fs.writeFileSync(contextStageLog, '');
|
|
1022
|
+
process.env[CONTEXT_STAGE_ENV] = contextStageLog;
|
|
1014
1023
|
const mappings = [];
|
|
1015
1024
|
const preimages = [...initialPreimages];
|
|
1016
1025
|
let retained = false;
|
|
@@ -1338,7 +1347,8 @@ export async function prepareRuleDeployment(original, provider, deploy) {
|
|
|
1338
1347
|
}
|
|
1339
1348
|
};
|
|
1340
1349
|
collectDirs(path.join(stage, '.aiwg'), '.aiwg');
|
|
1341
|
-
const
|
|
1350
|
+
const contextWrites = [...new Set(fs.readFileSync(contextStageLog, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line)))];
|
|
1351
|
+
const plan = { version: 1, lease, temp, target, stage, contextWrites, rootNames, workspaceDirs, preimages, poolSeal: pool?.inventorySeal || null,
|
|
1342
1352
|
anchors: captureAnchors([target, ...mappings.map(mapping => mapping.from), ...workspaceDirs.map(dir => path.join(target, dir))]),
|
|
1343
1353
|
snapshotRoot, corpusRoot, sourceMappings, staged: snapshotPreimages([stage, ...mappings.map(mapping => mapping.to)]), report: guarded.report,
|
|
1344
1354
|
mappings: mappings.map(({ from, to, before }) => ({ from, to, before: [...before] })) };
|
|
@@ -1346,6 +1356,7 @@ export async function prepareRuleDeployment(original, provider, deploy) {
|
|
|
1346
1356
|
retained = true;
|
|
1347
1357
|
return plan;
|
|
1348
1358
|
} finally {
|
|
1359
|
+
if (priorContextStage === undefined) delete process.env[CONTEXT_STAGE_ENV]; else process.env[CONTEXT_STAGE_ENV] = priorContextStage;
|
|
1349
1360
|
Object.assign(provider.paths, originals);
|
|
1350
1361
|
if (priorAiwgRoot === undefined) delete process.env.AIWG_ROOT; else process.env.AIWG_ROOT = priorAiwgRoot;
|
|
1351
1362
|
if (priorRegistry === undefined) delete process.env.AIWG_USER_REGISTRY_PATH; else process.env.AIWG_USER_REGISTRY_PATH = priorRegistry;
|
|
@@ -1418,10 +1429,16 @@ export function commitRuleDeploymentPlan(plan) {
|
|
|
1418
1429
|
output = Buffer.from(`${JSON.stringify(receipt, null, 2)}\n`);
|
|
1419
1430
|
}
|
|
1420
1431
|
fs.mkdirSync(path.dirname(path.join(from, rel)), { recursive: true });
|
|
1421
|
-
|
|
1432
|
+
if (plan.contextWrites.includes(file)) writeContextFileSync(path.join(from, rel), output);
|
|
1433
|
+
else fs.writeFileSync(path.join(from, rel), output);
|
|
1422
1434
|
}
|
|
1423
1435
|
}
|
|
1424
|
-
for (const name of rootNames) if (fs.existsSync(path.join(stage, name)))
|
|
1436
|
+
for (const name of rootNames) if (fs.existsSync(path.join(stage, name))) {
|
|
1437
|
+
const output = normalize(read(path.join(stage, name)));
|
|
1438
|
+
if (output === read(path.join(target, name))) continue;
|
|
1439
|
+
const write = plan.contextWrites.includes(path.join(stage, name)) ? writeContextFileSync : put;
|
|
1440
|
+
write(path.join(target, name), output);
|
|
1441
|
+
}
|
|
1425
1442
|
return plan.report;
|
|
1426
1443
|
} finally {
|
|
1427
1444
|
disposeRuleDeploymentPlan(plan);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* Warp Terminal Provider
|
|
3
4
|
*
|
|
@@ -48,6 +49,8 @@ import {
|
|
|
48
49
|
collectFrameworkArtifacts
|
|
49
50
|
} from './base.mjs';
|
|
50
51
|
|
|
52
|
+
import { writeContextFileSync } from './context-write.mjs';
|
|
53
|
+
|
|
51
54
|
// ============================================================================
|
|
52
55
|
// Provider Configuration
|
|
53
56
|
// ============================================================================
|
|
@@ -206,7 +209,7 @@ export async function deployWarp(targetDir, srcRoot, opts) {
|
|
|
206
209
|
// Preserve operator sections even for a forced artifact refresh.
|
|
207
210
|
const output = mergeWarpMd(existing, content, false);
|
|
208
211
|
ensureDir(targetDir, opts.dryRun);
|
|
209
|
-
if (!opts.dryRun)
|
|
212
|
+
if (!opts.dryRun) writeContextFileSync(dest, output);
|
|
210
213
|
|
|
211
214
|
}
|
|
212
215
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* Devin Desktop Provider (Windsurf compatibility adapter)
|
|
3
4
|
*
|
|
@@ -55,6 +56,8 @@ import {
|
|
|
55
56
|
deploySoulCompanions
|
|
56
57
|
} from './base.mjs';
|
|
57
58
|
|
|
59
|
+
import { writeContextFileSync } from './context-write.mjs';
|
|
60
|
+
|
|
58
61
|
// ============================================================================
|
|
59
62
|
// Provider Configuration
|
|
60
63
|
// ============================================================================
|
|
@@ -285,7 +288,7 @@ export function generateAgentsMd(files, destPath, opts) {
|
|
|
285
288
|
if (dryRun) {
|
|
286
289
|
console.log(`[dry-run] Would write AGENTS.md with ${agents.length} agents`);
|
|
287
290
|
} else {
|
|
288
|
-
|
|
291
|
+
writeContextFileSync(destPath, output, 'utf8');
|
|
289
292
|
console.log(`Created AGENTS.md with ${agents.length} agents at ${path.relative(process.cwd(), destPath)}`);
|
|
290
293
|
}
|
|
291
294
|
|
|
@@ -409,7 +412,7 @@ export function generateWindsurfRules(srcRoot, target, opts) {
|
|
|
409
412
|
console.log(`[dry-run] Would write .windsurf/rules/aiwg-orchestration.md (trigger: always_on)`);
|
|
410
413
|
} else {
|
|
411
414
|
ensureDir(rulesDir);
|
|
412
|
-
|
|
415
|
+
writeContextFileSync(orchestrationPath, output, 'utf8');
|
|
413
416
|
console.log(`Created .windsurf/rules/aiwg-orchestration.md (trigger: always_on)`);
|
|
414
417
|
}
|
|
415
418
|
|
|
@@ -430,7 +433,7 @@ export function generateWindsurfRules(srcRoot, target, opts) {
|
|
|
430
433
|
if (dryRun) {
|
|
431
434
|
console.log(`[dry-run] Would write .windsurfrules (deprecated stub)`);
|
|
432
435
|
} else {
|
|
433
|
-
|
|
436
|
+
writeContextFileSync(stubPath, stubLines.join('\n'), 'utf8');
|
|
434
437
|
console.log(`Created .windsurfrules (deprecated stub → .windsurf/rules/aiwg-orchestration.md)`);
|
|
435
438
|
}
|
|
436
439
|
}
|