@aiwg/cli 2026.10.6 → 2026.10.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.
Files changed (84) 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/decision.js +3 -2
  18. package/dist/src/cli/handlers/effect.js +4 -84
  19. package/dist/src/cli/handlers/guided-setup.js +1 -1
  20. package/dist/src/cli/handlers/init.js +1 -1
  21. package/dist/src/cli/handlers/install.js +6 -0
  22. package/dist/src/cli/handlers/marketplace.js +15 -4
  23. package/dist/src/cli/handlers/setup.js +24 -20
  24. package/dist/src/cli/handlers/use.js +57 -15
  25. package/dist/src/cli/prompt-utils.js +73 -0
  26. package/dist/src/config/gitignore.js +4 -0
  27. package/dist/src/config/workspace.js +2 -1
  28. package/dist/src/decision/adapters/jev.js +20 -31
  29. package/dist/src/decision/ask.js +6 -1
  30. package/dist/src/decision/benchmark.js +19 -6
  31. package/dist/src/decision/qualification/gate-evidence.js +6 -4
  32. package/dist/src/decision/qualification/load-staged.js +47 -10
  33. package/dist/src/decision/qualification/runner.js +7 -3
  34. package/dist/src/discovery/interview.js +10 -5
  35. package/dist/src/discovery/plan-apply.js +26 -30
  36. package/dist/src/discovery/plan-validation.js +2 -2
  37. package/dist/src/effects/host.js +91 -0
  38. package/dist/src/extensions/commands/definitions.js +2 -2
  39. package/dist/src/extensions/project-local-gitignore.js +2 -0
  40. package/dist/src/issues/cli.js +16 -5
  41. package/dist/src/issues/live.js +44 -12
  42. package/dist/src/marketplace/exchange.js +18 -6
  43. package/dist/src/marketplace/provenance.js +3 -2
  44. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  45. package/dist/src/mcp/cli.mjs +17 -90
  46. package/dist/src/mcp/grok-build-config.mjs +2 -1
  47. package/dist/src/mcp/muse-native-config.mjs +2 -1
  48. package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
  49. package/dist/src/memory/project-registry.js +2 -1
  50. package/dist/src/mission-protocol/worker-status.js +91 -1
  51. package/dist/src/models/worker-installation.d.mts +1 -1
  52. package/dist/src/models/worker-installation.mjs +65 -4
  53. package/dist/src/packages/adapters/git.js +99 -23
  54. package/dist/src/packages/adapters/gitea.js +8 -27
  55. package/dist/src/packages/adapters/github.js +7 -25
  56. package/dist/src/packages/registry.js +4 -4
  57. package/dist/src/process/cli-command.d.mts +5 -0
  58. package/dist/src/process/cli-command.mjs +42 -0
  59. package/dist/src/serve/worker-invocation.js +10 -0
  60. package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
  61. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  62. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  63. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  64. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  65. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  66. package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
  67. package/dist/src/tracker/project-inputs.js +2 -0
  68. package/dist/src/update/notifier.mjs +2 -4
  69. package/package.json +1 -1
  70. package/tools/agents/providers/base.mjs +5 -1
  71. package/tools/agents/providers/claude.mjs +5 -2
  72. package/tools/agents/providers/codex.mjs +4 -1
  73. package/tools/agents/providers/context-write.mjs +28 -0
  74. package/tools/agents/providers/copilot.mjs +5 -2
  75. package/tools/agents/providers/hermes.mjs +5 -2
  76. package/tools/agents/providers/omp.mjs +3 -1
  77. package/tools/agents/providers/rule-delivery.mjs +29 -12
  78. package/tools/agents/providers/warp.mjs +4 -1
  79. package/tools/agents/providers/windsurf.mjs +6 -3
  80. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  81. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  82. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  83. package/tools/ralph-external/process-monitor.mjs +42 -4
  84. package/tools/ralph-external/session-launcher.mjs +125 -3
@@ -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
  }
@@ -308,6 +308,9 @@ const HOST_CODES = new Set(['RESOURCE', 'NOT_FOUND', 'SANDBOX']);
308
308
  const PROVIDER_CODES = new Set(['NETWORK']);
309
309
  const INSTALLATION_CODES = new Set(['AUTH']);
310
310
  const MODEL_CODES = new Set(['MODEL']);
311
+ const STATUS_REASON_CODES = new Set(['NOT_FOUND', 'SPAWN_REFUSED', 'RATE_LIMIT', 'RESOURCE', 'SANDBOX',
312
+ 'NETWORK', 'AUTH', 'MODEL', 'UNKNOWN']);
313
+ export const RECOVERY_STATUS_LIMIT = 32;
311
314
 
312
315
  export const scopeKeyFor = Object.freeze({
313
316
  invocation: (id) => `invocation:${id ?? 'unknown'}`,
@@ -511,6 +514,7 @@ export class ScopedFailureBreaker extends EventEmitter {
511
514
  this._scopes = new Map();
512
515
  this._outstanding = new Map();
513
516
  this._hostFailureFacts = [];
517
+ this._transitions = [];
514
518
  this._canarySeq = 0;
515
519
  this._observedInvocations = new Set();
516
520
  this.canariesObserved = 0;
@@ -1022,6 +1026,10 @@ export class ScopedFailureBreaker extends EventEmitter {
1022
1026
  const from = rec.status;
1023
1027
  if (from === to) return;
1024
1028
  rec.status = to;
1029
+ if (rec.scopeKey.length <= 256) this._transitions.push({ scopeKey: rec.scopeKey, scope: rec.scope, from, to,
1030
+ reasons: [...rec.evidence.keys()].filter(code => STATUS_REASON_CODES.has(code)).slice(0, 8),
1031
+ atMs: this.nowMs(), nextEligibleAtMs: to === 'closed' ? null : rec.nextProbeAtMs });
1032
+ if (this._transitions.length > RECOVERY_STATUS_LIMIT) this._transitions.shift();
1025
1033
  this.emit('transition', { scopeKey: rec.scopeKey, from, to, action,
1026
1034
  distinctFailures: new Set(rec.failures.map(failure => failure.invocationId)).size, reasons: [...rec.evidence.keys()],
1027
1035
  nextEligibleAtMs: rec.nextProbeAtMs, resetAtMs: rec.resetAtMs,
@@ -1454,6 +1462,23 @@ export class ScopedFailureBreaker extends EventEmitter {
1454
1462
  };
1455
1463
  }
1456
1464
 
1465
+ /** Pure status projection: never sweep reservations, probe, or advance state. */
1466
+ recoveryStatus() {
1467
+ const recentKeys = [...new Set(this._transitions.map(event => event.scopeKey).reverse())];
1468
+ const active = [...this._scopes.values()].filter(rec => rec.status !== 'closed');
1469
+ const keys = [...new Set([...active.map(rec => rec.scopeKey), ...recentKeys])];
1470
+ const scopes = keys.filter(key => key.length <= 256).slice(0, RECOVERY_STATUS_LIMIT).map(scopeKey => {
1471
+ const rec = this._scopes.get(scopeKey);
1472
+ const last = this._transitions.findLast(event => event.scopeKey === scopeKey);
1473
+ return { scopeKey, scope: rec?.scope ?? last.scope, state: rec?.status ?? last.to,
1474
+ reasons: rec ? [...rec.evidence.keys()].filter(code => STATUS_REASON_CODES.has(code)).slice(0, 8) : [...last.reasons],
1475
+ nextEligibleAtMs: !rec || rec.status === 'closed' ? null : rec.nextProbeAtMs,
1476
+ canaryInFlight: rec?.activeCanary != null };
1477
+ });
1478
+ return { observedAtMs: this.nowMs(), scopes, omittedScopeCount: Math.max(0, keys.length - scopes.length),
1479
+ transitions: this._transitions.map(event => ({ ...event, reasons: [...event.reasons] })) };
1480
+ }
1481
+
1457
1482
  /** Correlate launcher-observed host faults across resolved inference backends.
1458
1483
  * Replay the first fault once corroborated, preserving the three-distinct
1459
1484
  * threshold even when the first backend alone could not establish scope.
@@ -1550,6 +1575,7 @@ export class ScopedFailureBreaker extends EventEmitter {
1550
1575
 
1551
1576
  destroy() {
1552
1577
  this._hostFailureFacts = [];
1578
+ this._transitions = [];
1553
1579
  for (const rec of this._scopes.values()) rec.breaker.destroy();
1554
1580
  this._scopes.clear();
1555
1581
  this._outstanding.clear();
@@ -3,6 +3,7 @@ import { runRecoveryProbe } from './recovery-probe.mjs';
3
3
  /** Recovery evidence observed outside worker output by the launch owner. */
4
4
  const observations = new WeakMap();
5
5
  const failureHints = new WeakMap();
6
+ const assessed = new WeakSet();
6
7
 
7
8
  export function recordLauncherRecoveryHint(result, reasonCode) {
8
9
  failureHints.set(result, reasonCode);
@@ -23,9 +24,10 @@ export function launcherRecoveryEvidence(result) {
23
24
  */
24
25
  export async function observeLauncherRecoveryOutcome({ invocation, result, installation, breaker,
25
26
  failureHint = '', beforeRequest, onDispatch, fetchImpl = globalThis.fetch, wallNowMs = Date.now, timeoutMs = 10_000 } = {}) {
26
- if (!result || result.attemptOutcome !== 'failed' || result.cancelled) return;
27
+ if (!result || result.attemptOutcome !== 'failed' || result.cancelled || assessed.has(result)) return;
27
28
  const backend = invocation?.selected?.inferenceProvider;
28
29
  if (HOST_ERRORS.has(result.errorCode) || HOST_RESULTS.has(result.nativeResult)) {
30
+ assessed.add(result);
29
31
  const corroboratedBackends = breaker?.observeHostFailure({ invocationId: result.invocationId,
30
32
  inferenceProvider: backend, reasonCode: 'RESOURCE' }) ?? [];
31
33
  if (corroboratedBackends.length >= 2) observations.set(result, {
@@ -43,6 +45,10 @@ export async function observeLauncherRecoveryOutcome({ invocation, result, insta
43
45
  if (!await beforeRequest()) return;
44
46
  const requestTimeoutMs = remaining();
45
47
  if (!Number.isFinite(requestTimeoutMs) || requestTimeoutMs <= 0) return;
48
+ // One diagnostic request per attempt, including unsuccessful diagnostics.
49
+ // Re-reading a result cannot consume allowance or create fresh evidence.
50
+ if (assessed.has(result)) return;
51
+ assessed.add(result);
46
52
  const observed = await runRecoveryProbe({ driver: { qualification: { installation } }, invocation: invocation ?? { transport: 'shell' },
47
53
  timeoutMs: Math.min(10_000, requestTimeoutMs), maxOutputBytes: 65_536, onDispatch, fetchImpl, wallNowMs });
48
54
  if (observed.reason === 'probe-rate-limited') {
@@ -1,6 +1,8 @@
1
+ import { RecoveryPolicy } from './consecutive-failure-circuit-breaker.mjs';
2
+
1
3
  /** A recovery probe is a conductor-owned request, never an uncontained worker. */
2
4
  export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000, maxOutputBytes = 65_536,
3
- onDispatch, fetchImpl = globalThis.fetch, wallNowMs = Date.now }) {
5
+ onDispatch, fetchImpl = globalThis.fetch, wallNowMs = Date.now, nowMs = () => performance.now() }) {
4
6
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 10_000
5
7
  || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > 65_536) {
6
8
  throw new RangeError('Recovery probe exceeds the 10s/64KiB bounds');
@@ -19,6 +21,7 @@ export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000,
19
21
  return failed('probe-endpoint-unavailable', { operatorIntervention: true });
20
22
  }
21
23
  const controller = new AbortController();
24
+ const deadlineMs = nowMs() + timeoutMs;
22
25
  const timer = setTimeout(() => controller.abort(), Math.max(1, Math.floor(timeoutMs)));
23
26
  let aborted;
24
27
  const timeout = new Promise((_, reject) => {
@@ -30,14 +33,19 @@ export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000,
30
33
  timeout.catch(() => {});
31
34
  try {
32
35
  await onDispatch?.();
36
+ if (nowMs() >= deadlineMs) controller.abort();
33
37
  if (controller.signal.aborted) return failed('probe-timeout');
34
38
  try {
35
39
  // HEAD has no task, credentials, inference body or worker filesystem.
36
40
  // A successful endpoint check permits only one subsequent task canary.
37
41
  const response = await Promise.race([fetchImpl(endpoint, { method: 'HEAD', redirect: 'error', signal: controller.signal }), timeout]);
38
42
  let headerBytes = 16 + Buffer.byteLength(response.statusText ?? '');
39
- for (const [name, value] of response.headers) headerBytes += Buffer.byteLength(name) + Buffer.byteLength(value) + 4;
40
43
  try {
44
+ if (nowMs() >= deadlineMs) { controller.abort(); return failed('probe-timeout'); }
45
+ for (const [name, value] of response.headers) {
46
+ headerBytes += Buffer.byteLength(name) + Buffer.byteLength(value) + 4;
47
+ if (headerBytes > maxOutputBytes) return failed('probe-output-limit');
48
+ }
41
49
  if (headerBytes > maxOutputBytes) return failed('probe-output-limit');
42
50
  if (response.body) return failed('probe-body-unexpected');
43
51
  const success = response.status >= 200 && response.status <= 299;
@@ -45,7 +53,8 @@ export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000,
45
53
  if (response.status === 429) {
46
54
  const after = response.headers.get('retry-after');
47
55
  const seconds = after != null && /^\d+(?:\.\d+)?$/.test(after.trim()) ? Number(after) : null;
48
- const date = seconds === null && after ? Date.parse(after) : NaN;
56
+ const date = seconds === null && /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(after ?? '')
57
+ ? Date.parse(after) : NaN;
49
58
  const reset = response.headers.get('x-ratelimit-reset');
50
59
  const resetEpoch = reset != null && /^\d+(?:\.\d+)?$/.test(reset.trim()) ? Number(reset) * 1000 : NaN;
51
60
  const resetDelay = response.headers.get('ratelimit-reset');
@@ -53,7 +62,8 @@ export async function runRecoveryProbe({ driver, invocation, timeoutMs = 10_000,
53
62
  Number.isFinite(resetEpoch) ? (resetEpoch - wallNowMs()) / 1000 : null,
54
63
  resetDelay != null && /^\d+(?:\.\d+)?$/.test(resetDelay.trim()) ? Number(resetDelay) : null]
55
64
  .filter(value => Number.isFinite(value) && value >= 0);
56
- return failed('probe-rate-limited', { retryAfterSeconds: delays.length ? Math.max(...delays) : null });
65
+ return failed('probe-rate-limited', { retryAfterSeconds: delays.length
66
+ ? Math.min(RecoveryPolicy.maxRetryAfterSeconds, Math.max(...delays)) : null });
57
67
  }
58
68
  return failed('probe-failed', { status: response.status });
59
69
  } finally {
@@ -15,7 +15,7 @@ import { hostname } from 'node:os';
15
15
  import { join, dirname } from 'path';
16
16
  import { constants } from 'node:fs';
17
17
  import { open, opendir, rename, unlink } from 'node:fs/promises';
18
- import { randomUUID } from 'node:crypto';
18
+ import { createHash, randomUUID } from 'node:crypto';
19
19
  import path from 'node:path';
20
20
  import { createWorkerHealthObserver } from './lib/worker-health.mjs';
21
21
 
@@ -38,6 +38,9 @@ import { createWorkerHealthObserver } from './lib/worker-health.mjs';
38
38
  // error, a thrown assertion, ...) and silently report every live-process
39
39
  // health query as unavailable instead of surfacing the actual failure.
40
40
  let boundedExecFileSync = null;
41
+ let projectRecoveryStatus = null;
42
+ let mergeRecoveryStatuses = null;
43
+ let projectStartupCorrection = null;
41
44
  let processHealthUnavailableReason = null;
42
45
  let importImpl;
43
46
  try {
@@ -58,6 +61,11 @@ if (importImpl) {
58
61
  if (!['AIWG_IMPLEMENTATION_MISSING', 'AIWG_PACKAGE_ROOT_MISSING'].includes(error?.code)) throw error;
59
62
  processHealthUnavailableReason = `bounded-child-process helper unavailable: implementation missing: ${error.message}`;
60
63
  }
64
+ try {
65
+ ({ projectRecoveryStatus, mergeRecoveryStatuses, projectStartupCorrection } = await importImpl(import.meta.url, 'mission-protocol/worker-status.js'));
66
+ } catch (error) {
67
+ if (!['AIWG_IMPLEMENTATION_MISSING', 'AIWG_PACKAGE_ROOT_MISSING'].includes(error?.code)) throw error;
68
+ }
61
69
  }
62
70
 
63
71
  /** Side-effect-free Linux sample. Unknown identity never grants signal authority. */
@@ -848,8 +856,10 @@ export async function sampleWorkerStatus(options, memory = new Map()) {
848
856
  const monotonicNow = options.monotonicNow ?? monotonicMs;
849
857
  const began = monotonicNow();
850
858
  const report = { version: 1, sampledAt, scope: 'direct-conductor', observerCoverage: 'unobserved',
851
- workers: [], breakers: [], nextCheckAt: null, complete: true, findings: [], exitCode: 0 };
859
+ workers: [], breakers: [], recovery: { source: 'unavailable', observedAt: null, observedAtMs: null,
860
+ scopes: [], transitions: [], omittedScopeCount: 0, truncated: false }, nextCheckAt: null, complete: true, findings: [], exitCode: 0 };
852
861
  let root;
862
+ const recoveryOwners = new Map();
853
863
  try {
854
864
  root = await openStatusDirectory(options.runDir ?? options.runsRoot);
855
865
  const listing = options.runDir ? { entries: [{ name: null }], complete: true }
@@ -870,6 +880,11 @@ export async function sampleWorkerStatus(options, memory = new Map()) {
870
880
  process: await statusRecord(directory, 'process.json', true), health: await statusRecord(directory, 'health.json'),
871
881
  exit: await statusRecord(directory, 'exit.json'), scratch: await statusRecord(directory, 'last-observed.json') };
872
882
  const ownerRecord = await statusRecord(directory, '.status-observer');
883
+ const supervisor = records.process.supervisor;
884
+ const ownerHost = safeLabel(supervisor?.host), ownerBoot = safeLabel(supervisor?.bootId), ownerStart = safeLabel(supervisor?.startTicks);
885
+ const launcherOwnerRecorded = records.process.origin === 'launcher'
886
+ && supervisor?.uid === process.getuid() && Number.isSafeInteger(supervisor.pid) && supervisor.pid > 0
887
+ && ownerHost && ownerBoot && ownerStart;
873
888
  const expected = records.process.worker;
874
889
  // classifyWorkerStatus() also ignores an unverified worker for this
875
890
  // comparison; skip the resample too so this never samples an
@@ -884,6 +899,22 @@ export async function sampleWorkerStatus(options, memory = new Map()) {
884
899
  ? (options.sampleIdentity ?? sampleWorkerIdentity)(ownerIdentity.pid) : null;
885
900
  const row = classifyWorkerStatus(records, { now, monotonicNow: sampleTime, bootId, actual, counters,
886
901
  previous: memory.get(key), ownerRecord, actualObserver });
902
+ if (projectRecoveryStatus && launcherOwnerRecorded && records.process.recovery) {
903
+ const recovery = projectRecoveryStatus(records.process.recovery, 'recorded');
904
+ const ownerKey = JSON.stringify([ownerHost, ownerBoot, supervisor.pid, ownerStart]);
905
+ const previous = recoveryOwners.get(ownerKey);
906
+ if (recovery.observedAt && (!previous || Date.parse(recovery.observedAt) > Date.parse(previous.observedAt)
907
+ || recovery.observedAt === previous.observedAt && recovery.transitions.length >= previous.transitions.length)) {
908
+ const digest = createHash('sha256').update(ownerKey).digest('hex').slice(0, 12);
909
+ const owner = `${supervisor.pid}@${ownerHost.slice(0, 80)}:${ownerStart.slice(0, 32)}:${digest}`;
910
+ for (const scope of recovery.scopes) scope.owner = owner;
911
+ for (const transition of recovery.transitions) transition.owner = owner;
912
+ recoveryOwners.set(ownerKey, recovery);
913
+ report.recovery = mergeRecoveryStatuses([...recoveryOwners.values()]);
914
+ }
915
+ }
916
+ const correction = launcherOwnerRecorded && projectStartupCorrection?.(records.process.startupCorrection);
917
+ if (correction) row.startupCorrection = correction;
887
918
  report.findings.push(...row.findings.map(finding => `${row.invocationId}: ${finding}`));
888
919
  if (row.observerOwnership === 'unavailable') report.findings.push('OBSERVER_OWNER_UNAVAILABLE: reconcile reservation before observing');
889
920
  memory.set(key, { cpuTicks: actual?.cpuTicks ?? null, counters, health: row.health });
@@ -975,12 +1006,19 @@ export function createStatusObservationOwner() {
975
1006
  export function formatWorkerStatus(report) {
976
1007
  const label = value => value == null ? '?' : String(value);
977
1008
  const seconds = value => value == null ? '?' : `${Math.round(value * 1000) / 1000}s`;
1009
+ const recovery = report.recovery;
1010
+ const recoveryLines = !recovery || recovery.source === 'unavailable'
1011
+ ? ['Recovery scope status unavailable: no recorded recovery owner observation']
1012
+ : [`Recovery scopes (${recovery.source}) observed=${label(recovery.observedAt)} omitted=${recovery.omittedScopeCount} truncated=${recovery.truncated}`,
1013
+ ...recovery.scopes.map(scope => ` ${scope.scopeKey} state=${scope.state} reasons=${scope.reasons.join(',') || 'UNKNOWN'} next eligible=${label(scope.nextEligibleAt ?? scope.nextEligibleAtMs)} canary in flight=${scope.canaryInFlight}${scope.owner ? ` owner=${scope.owner}` : ''}`),
1014
+ ...recovery.transitions.map(transition => ` ${transition.scopeKey}: ${transition.from} -> ${transition.to} at=${label(transition.at ?? transition.atMs)} reasons=${transition.reasons.join(',') || 'UNKNOWN'} next eligible=${label(transition.nextEligibleAt ?? transition.nextEligibleAtMs)}${transition.owner ? ` owner=${transition.owner}` : ''}`)];
978
1015
  return [`Workers (${report.scope}) coverage=${report.observerCoverage} complete=${report.complete} breakers=${report.breakers.length} (no shared Tier 0 breaker authority)`,
979
1016
  'invocation | member | state | coverage | age | last progress | remaining | cause | next action',
980
1017
  ...report.workers.map(row => row.error ? `? | ? | unreadable | ? | ? | ? | ? | ${row.error.code} | ${row.nextAction}`
981
1018
  : `${row.invocationId} | ${label(row.member)} | ${row.health.state} | ${row.health.coverage} | ${seconds(row.elapsedSeconds)} | ${seconds(row.ages.progressSeconds)} | ${seconds(row.remainingSeconds)} | ${row.health.reasonCode} | ${row.nextAction}\n`
982
- + ` thresholds=${JSON.stringify(row.thresholds)} termination=${row.termination} verification=${label(row.verification)} observer-gap=${row.health.maxCheckGapSeconds}s`),
983
- ...report.findings, `next check=${label(report.nextCheckAt)}`].join('\n');
1019
+ + ` thresholds=${JSON.stringify(row.thresholds)} termination=${row.termination} verification=${label(row.verification)} observer-gap=${row.health.maxCheckGapSeconds}s`
1020
+ + (row.startupCorrection ? `\n startup correction=${row.startupCorrection.action} state=${row.startupCorrection.state} reason=${row.startupCorrection.reasonCode} stop=${label(row.startupCorrection.stopCode)}` : '')),
1021
+ ...recoveryLines, ...report.findings, `next check=${label(report.nextCheckAt)}`].join('\n');
984
1022
  }
985
1023
 
986
1024
  /** One bounded foreground loop. Timeout only stops watching, never the worker. */