@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.
Files changed (100) hide show
  1. package/bin/aiwg.mjs +4 -2
  2. package/dist/src/a2a/agent-card.js +11 -0
  3. package/dist/src/api/index.d.ts +1 -0
  4. package/dist/src/api/index.js +1 -0
  5. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
  6. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
  7. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
  8. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
  9. package/dist/src/artifacts/fortemi-shard-export.js +284 -0
  10. package/dist/src/artifacts/index-preparation.js +2 -0
  11. package/dist/src/auth/client.js +5 -1
  12. package/dist/src/auth/credential-store.js +5 -1
  13. package/dist/src/auth/sensitive-input.js +151 -0
  14. package/dist/src/channel/manager.mjs +3 -0
  15. package/dist/src/cli/handlers/agentcard.js +47 -25
  16. package/dist/src/cli/handlers/auth.js +22 -2
  17. package/dist/src/cli/handlers/cost-report.js +7 -1
  18. package/dist/src/cli/handlers/decision.js +36 -10
  19. package/dist/src/cli/handlers/effect.js +4 -84
  20. package/dist/src/cli/handlers/guided-setup.js +1 -1
  21. package/dist/src/cli/handlers/init.js +1 -1
  22. package/dist/src/cli/handlers/install.js +6 -0
  23. package/dist/src/cli/handlers/local-executor.js +33 -6
  24. package/dist/src/cli/handlers/marketplace.js +15 -4
  25. package/dist/src/cli/handlers/setup.js +24 -20
  26. package/dist/src/cli/handlers/use.js +57 -15
  27. package/dist/src/cli/prompt-utils.js +73 -0
  28. package/dist/src/config/gitignore.js +4 -0
  29. package/dist/src/config/workspace.js +2 -1
  30. package/dist/src/cost/fleet-report.js +20 -22
  31. package/dist/src/decision/adapters/jev.js +20 -31
  32. package/dist/src/decision/ask.js +6 -1
  33. package/dist/src/decision/benchmark.js +19 -6
  34. package/dist/src/decision/driver.js +2 -2
  35. package/dist/src/decision/jev-credentials.js +18 -5
  36. package/dist/src/decision/ledger.js +230 -34
  37. package/dist/src/decision/qualification/gate-evidence.js +6 -4
  38. package/dist/src/decision/qualification/load-staged.js +47 -10
  39. package/dist/src/decision/qualification/runner.js +7 -3
  40. package/dist/src/discovery/interview.js +10 -5
  41. package/dist/src/discovery/plan-apply.js +26 -30
  42. package/dist/src/discovery/plan-validation.js +2 -2
  43. package/dist/src/effects/host.js +91 -0
  44. package/dist/src/extensions/commands/definitions.js +2 -2
  45. package/dist/src/extensions/project-local-gitignore.js +2 -0
  46. package/dist/src/issues/cli.js +16 -5
  47. package/dist/src/issues/live.js +44 -12
  48. package/dist/src/marketplace/exchange.js +18 -6
  49. package/dist/src/marketplace/provenance.js +3 -2
  50. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  51. package/dist/src/mcp/cli.mjs +122 -109
  52. package/dist/src/mcp/credentials.d.mts +4 -0
  53. package/dist/src/mcp/credentials.mjs +44 -3
  54. package/dist/src/mcp/grok-build-config.mjs +4 -1
  55. package/dist/src/mcp/muse-native-config.mjs +2 -1
  56. package/dist/src/mcp/omp-config.mjs +2 -0
  57. package/dist/src/mcp/registry.js +19 -15
  58. package/dist/src/mcp/registry.mjs +2 -18
  59. package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
  60. package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
  61. package/dist/src/memory/project-registry.js +2 -1
  62. package/dist/src/mission-protocol/worker-status.js +91 -1
  63. package/dist/src/models/grok-build-models.js +20 -0
  64. package/dist/src/models/worker-installation.d.mts +1 -1
  65. package/dist/src/models/worker-installation.mjs +65 -4
  66. package/dist/src/packages/adapters/git.js +99 -23
  67. package/dist/src/packages/adapters/gitea.js +8 -27
  68. package/dist/src/packages/adapters/github.js +7 -25
  69. package/dist/src/packages/registry.js +4 -4
  70. package/dist/src/process/cli-command.d.mts +5 -0
  71. package/dist/src/process/cli-command.mjs +42 -0
  72. package/dist/src/providers/provider-definitions.js +2 -2
  73. package/dist/src/serve/shared-host-scheduler.js +4 -1
  74. package/dist/src/serve/worker-invocation.js +10 -0
  75. package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
  76. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  77. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  78. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  79. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  80. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  81. package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
  82. package/dist/src/storage/backends/fortemi.js +107 -49
  83. package/dist/src/tracker/project-inputs.js +2 -0
  84. package/dist/src/update/notifier.mjs +2 -4
  85. package/package.json +1 -1
  86. package/tools/agents/providers/base.mjs +5 -1
  87. package/tools/agents/providers/claude.mjs +5 -2
  88. package/tools/agents/providers/codex.mjs +4 -1
  89. package/tools/agents/providers/context-write.mjs +28 -0
  90. package/tools/agents/providers/copilot.mjs +5 -2
  91. package/tools/agents/providers/hermes.mjs +5 -2
  92. package/tools/agents/providers/omp.mjs +3 -1
  93. package/tools/agents/providers/rule-delivery.mjs +29 -12
  94. package/tools/agents/providers/warp.mjs +4 -1
  95. package/tools/agents/providers/windsurf.mjs +6 -3
  96. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  97. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  98. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  99. package/tools/ralph-external/process-monitor.mjs +42 -4
  100. 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
- let transport;
385
- if (server.type === 'stdio') {
386
- const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
387
- transport = new StdioClientTransport({
388
- command: server.command ?? '',
389
- args: server.args ?? [],
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
- else {
394
- if (!server.url) {
395
- throw new Error(`storage(fortemi): MCP server "${serverName}" has no URL`);
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
- const url = validateRemoteMcpUrl(server.url);
398
- const headers = resolveMcpRequestHeaders(server, environment);
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
- else if (server.type === 'sse') {
406
- const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
407
- transport = new SSEClientTransport(url, {
408
- requestInit: { headers },
409
- eventSourceInit: {
410
- fetch: async (input, init) => {
411
- const merged = new Headers(init?.headers);
412
- for (const [name, value] of Object.entries(headers))
413
- merged.set(name, value);
414
- return fetch(input, { ...init, headers: merged });
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
- throw new Error(`storage(fortemi): unsupported MCP transport "${String(server.type)}"`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.10.6",
3
+ "version": "2026.10.8",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
- fs.writeFileSync(destPath, content, 'utf8');
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
- fs.writeFileSync(hookDest, content, 'utf8');
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
- fs.writeFileSync(claudeDest, lines.join('\n'), 'utf8');
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 fs.writeFileSync(destPath, out, 'utf8');
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
- fs.writeFileSync(destPath, combined, 'utf8');
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
- fs.writeFileSync(destPath, template, 'utf8');
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
- fs.writeFileSync(destPath, output, 'utf8');
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
- fs.writeFileSync(destPath, mergeHermesContext(destPath, body, 'routing'), 'utf8');
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) fs.writeFileSync(dest, content);
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
- return { ...entry, path: opts.mapInputPath ? opts.mapInputPath(resolvedPath) : resolvedPath };
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.resolve(root, entry.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
- put(file, block(content, body));
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) put(file, clean);
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) put(file, clean);
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)) put(former, block(read(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 plan = { version: 1, lease, temp, target, stage, rootNames, workspaceDirs, preimages, poolSeal: pool?.inventorySeal || null,
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
- fs.writeFileSync(path.join(from, rel), output);
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))) put(path.join(target, name), normalize(read(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) fs.writeFileSync(dest, output);
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
- fs.writeFileSync(destPath, output, 'utf8');
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
- fs.writeFileSync(orchestrationPath, output, 'utf8');
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
- fs.writeFileSync(stubPath, stubLines.join('\n'), 'utf8');
436
+ writeContextFileSync(stubPath, stubLines.join('\n'), 'utf8');
434
437
  console.log(`Created .windsurfrules (deprecated stub → .windsurf/rules/aiwg-orchestration.md)`);
435
438
  }
436
439
  }