@aiwg/cli 2026.10.5 → 2026.10.6

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 (212) hide show
  1. package/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
  2. package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
  3. package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
  4. package/agentic/code/providers/model-capabilities.v1.json +74 -50
  5. package/bin/aiwg.mjs +3 -1
  6. package/dist/src/artifacts/browser-export.js +1 -0
  7. package/dist/src/artifacts/citation-parser.js +8 -1
  8. package/dist/src/artifacts/cli.js +57 -6
  9. package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
  10. package/dist/src/artifacts/fortemi-core-sync.js +26 -8
  11. package/dist/src/artifacts/graph-query.js +18 -8
  12. package/dist/src/artifacts/index-adapters.js +339 -0
  13. package/dist/src/artifacts/index-builder.js +143 -14
  14. package/dist/src/artifacts/index-files.js +15 -4
  15. package/dist/src/artifacts/index-reader.js +44 -0
  16. package/dist/src/artifacts/index-refresh.js +114 -55
  17. package/dist/src/artifacts/index-status.js +20 -5
  18. package/dist/src/artifacts/memory-maintenance.js +503 -0
  19. package/dist/src/artifacts/prebuilt-relations.js +3 -1
  20. package/dist/src/artifacts/query-engine.js +263 -108
  21. package/dist/src/artifacts/relations-builder.js +2 -2
  22. package/dist/src/artifacts/source-graph.js +2 -2
  23. package/dist/src/artifacts/types.js +9 -1
  24. package/dist/src/cli/handlers/decision.js +19 -1
  25. package/dist/src/cli/handlers/guided-setup.js +470 -0
  26. package/dist/src/cli/handlers/help.js +2 -2
  27. package/dist/src/cli/handlers/housekeeper.js +1110 -0
  28. package/dist/src/cli/handlers/index.js +4 -2
  29. package/dist/src/cli/handlers/init.js +41 -6
  30. package/dist/src/cli/handlers/local-executor.js +5 -3
  31. package/dist/src/cli/handlers/mc.js +509 -53
  32. package/dist/src/cli/handlers/models.js +32 -2
  33. package/dist/src/cli/handlers/ralph-launcher.js +76 -4
  34. package/dist/src/cli/handlers/rules.js +1 -1
  35. package/dist/src/cli/handlers/script-runner.js +73 -2
  36. package/dist/src/cli/handlers/serve.js +5 -3
  37. package/dist/src/cli/handlers/sessions.js +1 -0
  38. package/dist/src/cli/handlers/setup.js +428 -1
  39. package/dist/src/cli/handlers/skill-lint.js +1 -1
  40. package/dist/src/cli/handlers/subcommands.js +118 -8
  41. package/dist/src/cli/handlers/use.js +279 -93
  42. package/dist/src/cli/handlers/workspace.js +3 -1
  43. package/dist/src/cli/prompt-utils.js +8 -0
  44. package/dist/src/cli/router.js +16 -1
  45. package/dist/src/config/aiwg-config.js +42 -0
  46. package/dist/src/config/cli.js +7 -3
  47. package/dist/src/config/decision-config.js +14 -1
  48. package/dist/src/config/native-sandbox-warnings.js +25 -0
  49. package/dist/src/contributors/validation.js +6 -5
  50. package/dist/src/decision/admission.js +7 -3
  51. package/dist/src/decision/benchmark.js +469 -0
  52. package/dist/src/decision/ledger.js +22 -3
  53. package/dist/src/decision/pricing.js +147 -0
  54. package/dist/src/decision/qualification/gate-evidence.js +18 -15
  55. package/dist/src/decision/qualification/load-staged.js +539 -0
  56. package/dist/src/decision/qualification/load.js +109 -33
  57. package/dist/src/decision/qualification/runner.js +5 -2
  58. package/dist/src/decision/review/service.js +6 -4
  59. package/dist/src/decision/review/validate.js +23 -2
  60. package/dist/src/decision/savings.js +8 -3
  61. package/dist/src/discovery/index.js +492 -0
  62. package/dist/src/discovery/interview.js +233 -0
  63. package/dist/src/discovery/plan-apply.js +571 -0
  64. package/dist/src/discovery/plan-store.js +171 -0
  65. package/dist/src/discovery/plan-types.js +2 -0
  66. package/dist/src/discovery/plan-validation.js +139 -0
  67. package/dist/src/discovery/types.js +2 -0
  68. package/dist/src/extensions/commands/definitions.js +49 -9
  69. package/dist/src/extensions/manifest.js +71 -44
  70. package/dist/src/extensions/project-local-discovery.js +1 -1
  71. package/dist/src/extensions/project-quickref.js +18 -17
  72. package/dist/src/extensions/validation.js +32 -29
  73. package/dist/src/housekeeper/daemon-config.js +148 -0
  74. package/dist/src/housekeeper/ipc.js +84 -0
  75. package/dist/src/housekeeper/lifecycle.js +148 -0
  76. package/dist/src/housekeeper/platform.js +35 -0
  77. package/dist/src/housekeeper/status.js +70 -0
  78. package/dist/src/housekeeper/units.js +114 -0
  79. package/dist/src/jobs/flow.js +22 -21
  80. package/dist/src/lint/rule-activation.js +20 -5
  81. package/dist/src/lint/runner.js +1 -1
  82. package/dist/src/mcp/cli.mjs +5 -6
  83. package/dist/src/mcp/config-file.mjs +6 -2
  84. package/dist/src/mcp/config-layers.mjs +23 -7
  85. package/dist/src/mcp/profiles.js +18 -31
  86. package/dist/src/mcp/profiles.mjs +18 -32
  87. package/dist/src/mcp/registry.js +7 -27
  88. package/dist/src/mcp/registry.mjs +7 -28
  89. package/dist/src/mcp/server.mjs +34 -1
  90. package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
  91. package/dist/src/mcp/tools/command-run.mjs +3 -2
  92. package/dist/src/mcp/tools/decision.mjs +49 -48
  93. package/dist/src/mcp/tools/discovery.mjs +46 -21
  94. package/dist/src/mcp/tools/orchestration.mjs +22 -22
  95. package/dist/src/mcp/tools/subsystems.mjs +9 -8
  96. package/dist/src/mcp/tools/worker-invoke.mjs +17 -4
  97. package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
  98. package/dist/src/memory/audit.js +719 -0
  99. package/dist/src/memory/canonical-context.js +392 -13
  100. package/dist/src/memory/canonical-maintenance.js +95 -0
  101. package/dist/src/memory/cli-result.js +22 -0
  102. package/dist/src/memory/cli.js +176 -0
  103. package/dist/src/memory/context-pack.js +57 -6
  104. package/dist/src/memory/cost.js +512 -0
  105. package/dist/src/memory/firewall-classification.d.mts +7 -0
  106. package/dist/src/memory/firewall-classification.mjs +32 -0
  107. package/dist/src/memory/index.js +8 -0
  108. package/dist/src/memory/line-maintenance.js +68 -0
  109. package/dist/src/memory/maintenance-cli.js +286 -0
  110. package/dist/src/memory/maintenance-contract.js +136 -0
  111. package/dist/src/memory/maintenance-index.js +18 -0
  112. package/dist/src/memory/maintenance-owner.js +2 -0
  113. package/dist/src/memory/maintenance-record.js +138 -0
  114. package/dist/src/memory/maintenance.js +254 -0
  115. package/dist/src/memory/provider-memory-adapter.js +154 -0
  116. package/dist/src/memory/recommend-cli.js +83 -0
  117. package/dist/src/memory/recommend.js +269 -0
  118. package/dist/src/memory/scheduled.js +81 -0
  119. package/dist/src/memory/taxonomy.js +44 -0
  120. package/dist/src/mission-protocol/codecs.js +19 -9
  121. package/dist/src/models/config-loader.js +11 -1
  122. package/dist/src/models/model-capabilities.v1.json +74 -50
  123. package/dist/src/models/provider-policy.js +125 -62
  124. package/dist/src/models/resolver.js +45 -8
  125. package/dist/src/models/worker-installation.d.mts +7 -0
  126. package/dist/src/models/worker-installation.mjs +169 -37
  127. package/dist/src/models/wrapper-route.js +1 -1
  128. package/dist/src/output-modes/registry.js +8 -7
  129. package/dist/src/plugin/metadata-validator.js +1 -1
  130. package/dist/src/providers/deploy-version.d.mts +1 -0
  131. package/dist/src/providers/deploy-version.mjs +17 -0
  132. package/dist/src/providers/provider-definitions.js +19 -16
  133. package/dist/src/providers/skill-ownership.d.mts +11 -2
  134. package/dist/src/providers/skill-ownership.mjs +35 -17
  135. package/dist/src/rules/activation.d.mts +11 -2
  136. package/dist/src/rules/activation.mjs +82 -6
  137. package/dist/src/rules/inventory.js +1 -1
  138. package/dist/src/rules/rule-frontmatter.schema.json +2 -0
  139. package/dist/src/schema/zod-validation.d.mts +6 -0
  140. package/dist/src/schema/zod-validation.mjs +50 -0
  141. package/dist/src/serve/executor-registry.js +297 -20
  142. package/dist/src/serve/executor-token.d.mts +1 -0
  143. package/dist/src/serve/executor-token.mjs +8 -0
  144. package/dist/src/serve/shared-host-scheduler.js +1102 -31
  145. package/dist/src/serve/worker-invocation.js +114 -15
  146. package/dist/src/sessions/adapters/claude.js +19 -18
  147. package/dist/src/sessions/adapters/codex.js +10 -9
  148. package/dist/src/sessions/adapters/copilot.js +14 -13
  149. package/dist/src/sessions/adapters/cursor.js +19 -18
  150. package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
  151. package/dist/src/sessions/adapters/factory.js +11 -10
  152. package/dist/src/sessions/adapters/generic.js +32 -23
  153. package/dist/src/sessions/adapters/grokbot.js +35 -35
  154. package/dist/src/sessions/adapters/hermes.js +20 -19
  155. package/dist/src/sessions/adapters/muse.js +50 -49
  156. package/dist/src/sessions/adapters/omp.js +5 -4
  157. package/dist/src/sessions/adapters/openclaw.js +30 -29
  158. package/dist/src/sessions/adapters/opencode.js +26 -26
  159. package/dist/src/sessions/adapters/openhuman.js +24 -23
  160. package/dist/src/sessions/adapters/pi.js +5 -4
  161. package/dist/src/sessions/adapters/windsurf.js +8 -8
  162. package/dist/src/sessions/analytics.js +10 -9
  163. package/dist/src/sessions/candidates.js +7 -6
  164. package/dist/src/sessions/contracts.js +63 -53
  165. package/dist/src/sessions/output-registration.js +11 -10
  166. package/dist/src/sessions/workspace-discovery.js +3 -4
  167. package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
  168. package/dist/src/smiths/context-pipeline/claude-hook.js +42 -6
  169. package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
  170. package/dist/src/smiths/context-pipeline/workspace-context.js +106 -10
  171. package/dist/src/update/checker.d.mts +4 -0
  172. package/dist/src/update/checker.mjs +14 -0
  173. package/dist/src/writing/exemplar-selection.js +6 -6
  174. package/dist/src/writing/voice-evaluation.js +24 -23
  175. package/dist/src/writing/writer-migration.js +14 -13
  176. package/dist/src/writing/writer-profile.js +19 -18
  177. package/dist/src/writing/writing-brief.js +18 -17
  178. package/dist/src/writing/writing-receipt.js +47 -46
  179. package/package.json +5 -2
  180. package/schemas/decision/DecisionReview.schema.json +78 -1
  181. package/schemas/mission-v1.schema.json +3 -0
  182. package/tools/agents/deploy-agents.mjs +67 -39
  183. package/tools/agents/providers/antigravity.mjs +6 -1
  184. package/tools/agents/providers/base.mjs +19 -13
  185. package/tools/agents/providers/claude.mjs +1 -1
  186. package/tools/agents/providers/codex.mjs +50 -56
  187. package/tools/agents/providers/copilot.mjs +8 -2
  188. package/tools/agents/providers/cursor.mjs +1 -1
  189. package/tools/agents/providers/deepseek-harness.mjs +1 -1
  190. package/tools/agents/providers/factory.mjs +7 -1
  191. package/tools/agents/providers/grok-build.mjs +18 -2
  192. package/tools/agents/providers/grokbot.mjs +1 -1
  193. package/tools/agents/providers/hermes.mjs +7 -13
  194. package/tools/agents/providers/muse-hooks.mjs +21 -14
  195. package/tools/agents/providers/muse.mjs +7 -2
  196. package/tools/agents/providers/omp.mjs +28 -4
  197. package/tools/agents/providers/opencode.mjs +14 -5
  198. package/tools/agents/providers/openhuman.mjs +1 -1
  199. package/tools/agents/providers/pi.mjs +1 -1
  200. package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
  201. package/tools/agents/providers/rule-delivery.mjs +263 -71
  202. package/tools/agents/providers/windsurf.mjs +7 -0
  203. package/tools/cli/workspace-status.mjs +5 -0
  204. package/tools/commands/deploy-prompts-codex.mjs +11 -8
  205. package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
  206. package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
  207. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  208. package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
  209. package/tools/ralph-external/process-monitor.mjs +7 -4
  210. package/tools/ralph-external/session-launcher.mjs +280 -32
  211. package/tools/skills/deploy-skills-codex.mjs +11 -7
  212. package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
@@ -27,6 +27,7 @@ let fs;
27
27
  try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; }
28
28
  import path from 'path';
29
29
  import {
30
+ withRuleDeployment,
30
31
  ensureDir,
31
32
  listMdFiles,
32
33
  listMdFilesRecursive,
@@ -429,6 +430,7 @@ export function deploySkills(skillDirs, targetDir, opts) {
429
430
  * Deploy rules to .github/instructions/ as .instructions.md files
430
431
  */
431
432
  export function deployRules(ruleFiles, targetDir, opts) {
433
+ if (opts.ruleDelivery) return [];
432
434
  const destDir = path.join(targetDir, paths.rules);
433
435
  ensureDir(destDir, opts.dryRun);
434
436
  cleanupOldRuleFiles(destDir, opts);
@@ -495,8 +497,8 @@ export function createCopilotInstructions(target, srcRoot, dryRun) {
495
497
  export async function postDeploy(targetDir, opts) {
496
498
  initializeFrameworkWorkspace(targetDir, opts.mode, opts.dryRun, opts.srcRoot);
497
499
 
498
- // Create copilot-instructions.md
499
- createCopilotInstructions(targetDir, opts.srcRoot, opts.dryRun);
500
+ // copilot-instructions.md is Copilot's context file: --no-context-files leaves it alone.
501
+ if (opts.contextFiles !== false) createCopilotInstructions(targetDir, opts.srcRoot, opts.dryRun);
500
502
  }
501
503
 
502
504
  // ============================================================================
@@ -517,6 +519,10 @@ export function getFileExtension(type) {
517
519
  // ============================================================================
518
520
 
519
521
  export async function deploy(opts) {
522
+ return withRuleDeployment(opts, { name, paths, kernelSkillsPath }, deploySelected);
523
+ }
524
+
525
+ async function deploySelected(opts) {
520
526
  const {
521
527
  srcRoot,
522
528
  target,
@@ -414,7 +414,7 @@ export async function postDeploy(targetDir, opts) {
414
414
  initializeFrameworkWorkspace(targetDir, opts.mode, opts.dryRun, opts.srcRoot);
415
415
 
416
416
  if (opts.createAgentsMd) {
417
- createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
417
+ if (opts.contextFiles !== false) createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
418
418
  }
419
419
 
420
420
  // PUW-019 (#1120): wire environment.json + worktrees.json templates.
@@ -46,7 +46,7 @@ export function deployCordisPatch(target, opts) {
46
46
  }
47
47
 
48
48
  export async function postDeploy(target, opts) {
49
- if (opts.createAgentsMd || (!opts.commandsOnly && !opts.skillsOnly && !opts.rulesOnly)) createAgentsMd(target, opts.srcRoot, opts.dryRun);
49
+ if (opts.contextFiles !== false && (opts.createAgentsMd || (!opts.commandsOnly && !opts.skillsOnly && !opts.rulesOnly))) createAgentsMd(target, opts.srcRoot, opts.dryRun);
50
50
  if (!opts.quiet) console.log('DeepSeek Harness: runtime credentials remain environment-only; use a reviewed ephemeral route patch.');
51
51
  }
52
52
 
@@ -25,6 +25,7 @@ let fs;
25
25
  try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; }
26
26
  import path from 'path';
27
27
  import {
28
+ withRuleDeployment,
28
29
  ensureDir,
29
30
  listMdFiles,
30
31
  listMdFilesRecursive,
@@ -576,6 +577,7 @@ export function deploySkills(skillDirs, targetDir, opts) {
576
577
  * Deploy rules to .factory/rules/
577
578
  */
578
579
  export function deployRules(ruleFiles, targetDir, opts) {
580
+ if (opts.ruleDelivery) return [];
579
581
  const destDir = path.join(targetDir, paths.rules);
580
582
  ensureDir(destDir, opts.dryRun);
581
583
  cleanupOldRuleFiles(destDir, opts);
@@ -816,7 +818,7 @@ export async function postDeploy(targetDir, opts) {
816
818
 
817
819
  // Create/update AGENTS.md if requested
818
820
  if (opts.createAgentsMd) {
819
- createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
821
+ if (opts.contextFiles !== false) createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
820
822
  }
821
823
  }
822
824
 
@@ -945,6 +947,10 @@ export function getFileExtension(type) {
945
947
  * Main deployment function for Factory provider
946
948
  */
947
949
  export async function deploy(opts) {
950
+ return withRuleDeployment(opts, { name, paths, kernelSkillsPath, transformSkillMd: transformSkillFrontmatter }, deploySelected);
951
+ }
952
+
953
+ async function deploySelected(opts) {
948
954
  const {
949
955
  srcRoot,
950
956
  target,
@@ -16,6 +16,8 @@ import fs from 'node:fs';
16
16
  import { createHash, randomUUID } from 'node:crypto';
17
17
  import yaml from 'js-yaml';
18
18
  import {
19
+ withRuleDeployment,
20
+ replaceBoundRulesBlock,
19
21
  createAgentsMdFromTemplate,
20
22
  deploySkillsWithKernelRouting,
21
23
  collectFrameworkArtifacts,
@@ -224,7 +226,17 @@ function matchesSkillSource(source, target) {
224
226
  if (relative.endsWith(path.sep)) return true;
225
227
  let content = fs.readFileSync(path.join(source, relative), 'utf8');
226
228
  if (relative === 'SKILL.md') content = injectPlatformInContent(content, 'grok-build');
227
- return content === fs.readFileSync(path.join(target, relative), 'utf8');
229
+ const deployed = fs.readFileSync(path.join(target, relative), 'utf8');
230
+ if (content === deployed) return true;
231
+ if (relative !== 'SKILL.md' || !deployed.includes('<!-- AIWG:bound-rules:start -->')) return false;
232
+ // Activation projection appends generated bindings. Verify the complete
233
+ // deployed bytes against the receipt before comparing the authored body;
234
+ // an operator edit (including inside the binding block) must survive.
235
+ const receipt = JSON.parse(fs.readFileSync(path.join(target, '.aiwg-manifest.json'), 'utf8'));
236
+ const digest = `sha256:${createHash('sha256').update(deployed).digest('hex')}`;
237
+ return receipt.skillSource === path.resolve(source)
238
+ && receipt.managed?.[relative]?.hash === digest
239
+ && replaceBoundRulesBlock(content, []).trimEnd() === replaceBoundRulesBlock(deployed, []).trimEnd();
228
240
  });
229
241
  } catch { return false; }
230
242
  }
@@ -326,7 +338,7 @@ export function uninstall(target, opts = {}) {
326
338
 
327
339
  export async function postDeploy(target, opts = {}) {
328
340
  if (opts.createAgentsMd || (!opts.commandsOnly && !opts.skillsOnly && !opts.rulesOnly)) {
329
- createAgentsMd(target, opts.srcRoot, opts.dryRun);
341
+ if (opts.contextFiles !== false) createAgentsMd(target, opts.srcRoot, opts.dryRun);
330
342
  }
331
343
  if (!opts.quiet) {
332
344
  const home = resolveGrokHome(opts.env || process.env);
@@ -344,6 +356,10 @@ export function getFileExtension() {
344
356
  }
345
357
 
346
358
  export async function deploy(opts) {
359
+ return withRuleDeployment(opts, { name, paths, kernelSkillsPath }, deploySelected);
360
+ }
361
+
362
+ async function deploySelected(opts) {
347
363
  const mode = normalizeDeploymentMode(opts.mode);
348
364
  if (opts.createAgentsMd || (!opts.commandsOnly && !opts.skillsOnly && !opts.rulesOnly)) {
349
365
  assertSafeAgentsMd(opts.target);
@@ -171,7 +171,7 @@ export async function postDeploy(targetDir, opts = {}) {
171
171
  opts.createAgentsMd ||
172
172
  (!opts.commandsOnly && !opts.skillsOnly && !opts.rulesOnly)
173
173
  ) {
174
- createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
174
+ if (opts.contextFiles !== false) createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
175
175
  }
176
176
  if (!opts.quiet) {
177
177
  const root = resolveGrokbotSkillsDir(opts.env || process.env);
@@ -224,9 +224,9 @@ export function assertContextBudget(content, name = 'AGENTS.md', rules = []) {
224
224
  * priming on every Hermes turn (#1318 / S7; skill-discovery added #1347).
225
225
  *
226
226
  * The full rule bodies are reachable without MCP via `aiwg discover` and
227
- * `aiwg show rule <name>`; MCP exposes the same path through `rule-list` and
227
+ * `aiwg show rule <name> --body`; MCP exposes the same path through `rule-list` and
228
228
  * `rule-show` when configured (#1320). The bodies exceed Hermes's 20K context
229
- * cap if inlined verbatim (anti-laziness alone is 32K chars), so we inline
229
+ * cap if inlined verbatim (anti-laziness alone was 32K chars; it now ships as a compact core body, #3450), so we inline
230
230
  * compressed directives only and point to CLI/MCP for the full text.
231
231
  *
232
232
  * Selection criteria: rules tagged CRITICAL/HIGH enforcement level whose
@@ -240,7 +240,7 @@ const CRITICAL_RULE_DIRECTIVES = `## CRITICAL Rules (always apply)
240
240
  These are the highest-enforcement AIWG rules. Full bodies are reachable without
241
241
  MCP:
242
242
  - Find rules: \`aiwg discover "rule <topic>" --type rule\`
243
- - Fetch a rule: \`aiwg show rule <name>\`
243
+ - Fetch a rule: \`aiwg show rule <name> --body\`
244
244
 
245
245
  If the optional MCP sidecar is configured, the same surface is available through
246
246
  \`mcp_aiwg_rule_list\` and \`mcp_aiwg_rule_show\`.
@@ -260,13 +260,6 @@ Never add AI-tool attribution to commits, PRs, code, or docs. No \`Co-Authored-B
260
260
  no "Generated with", no "Written by [AI tool]". The AI is a tool; tools don't sign
261
261
  their output. Applies to ALL platforms (Claude, Codex, Copilot, Cursor, etc.).
262
262
 
263
- ### Rule: anti-laziness
264
- Never delete tests to make them pass. Never skip/disable tests. Never remove
265
- features instead of fixing them. Never weaken assertions to be meaningless.
266
- Never suppress CI/pipeline signals (\`continue-on-error\`, \`|| true\`, \`set +e\`).
267
- If stuck after 3 honest attempts: escalate with full context, don't shortcut.
268
- Within scope: leave nothing half-done — code + tests + docs + verification.
269
-
270
263
  ### Rule: citation-policy
271
264
  Never fabricate citations, DOIs, URLs, or page numbers. Only cite sources that
272
265
  exist in the research corpus (.aiwg/research/sources/). Match claim strength
@@ -331,6 +324,7 @@ function mergeHermesContext(file, generated, kind) {
331
324
  }
332
325
 
333
326
  export function generateAgentsMd(agentCount, skillCount, targetDir, opts) {
327
+ if (opts.contextFiles === false) return;
334
328
  const { dryRun } = opts;
335
329
 
336
330
  // Startup is reserved for rule contracts. Workflow routing, artifact catalogs,
@@ -339,7 +333,7 @@ export function generateAgentsMd(agentCount, skillCount, targetDir, opts) {
339
333
  const header = `# AIWG Integration
340
334
 
341
335
  Skills: \`$HERMES_HOME/skills/\` (kernel), \`$HERMES_HOME/skills/.aiwg/\` (standard).
342
- Find: \`aiwg discover "rule <topic>" --type rule\`. Fetch: \`aiwg show rule <name>\`.
336
+ Find: \`aiwg discover "rule <topic>" --type rule\`. Fetch: \`aiwg show rule <name> --body\`.
343
337
  MCP is optional; \`mcp_aiwg_rule_show\` exposes the same rule catalog.
344
338
  `;
345
339
 
@@ -533,7 +527,7 @@ async function deploySelected(opts) {
533
527
  // Generate AGENTS.md (with inlined CRITICAL rule priming, #1318/#1532)
534
528
  // AND .hermes.md thin pointer (resolves #1319 doc-debt, was claimed in
535
529
  // CHANGELOG but not previously implemented).
536
- if (!skillsOnly && !opts.commandsOnly && !opts.rulesOnly) {
530
+ if (opts.contextFiles !== false && !skillsOnly && !opts.commandsOnly && !opts.rulesOnly) {
537
531
  const artifacts = collectFrameworkArtifacts(srcRoot, normalizedMode, {
538
532
  includeAgents: true,
539
533
  includeCommands: false,
@@ -560,7 +554,7 @@ async function deploySelected(opts) {
560
554
  if (!opts.quiet) {
561
555
  console.log('');
562
556
  console.log(`Skills root: ${kernelSkillsPath}`);
563
- console.log('Rules are in AGENTS.md as compressed directives; full bodies via `aiwg show rule <name>`.');
557
+ console.log('Rules are in AGENTS.md as compressed directives; full bodies via `aiwg show rule <name> --body`.');
564
558
  console.log('Optional: configure config.yaml to connect AIWG MCP server.');
565
559
  console.log('See: docs/integrations/hermes-quickstart.md (optional MCP setup)');
566
560
  }
@@ -43,9 +43,7 @@
43
43
  * Servers inherit the operator's process environment; pass `MUSE_SESSION_ID`
44
44
  * through at runtime instead of hardcoding a session id into `env`.
45
45
  *
46
- * This module is intentionally dependency-free (node builtins +
47
- * muse-paths.mjs only) so provider-policy-style import graphs never pull it
48
- * in transitively.
46
+ * MCP writes use the shared config writer, resolved for source and packaged installs.
49
47
  *
50
48
  * @issue #228
51
49
  */
@@ -53,9 +51,13 @@
53
51
  import fs from 'node:fs';
54
52
  import os from 'node:os';
55
53
  import path from 'node:path';
54
+ import { randomUUID } from 'node:crypto';
55
+ import { importImpl } from '../../_resolve-impl.mjs';
56
56
  import { stripJsonComments } from './base.mjs';
57
57
  import { resolveMuseXdgSkillsDir, museXdgSkillsDirRemediation } from './muse-paths.mjs';
58
58
 
59
+ const { assertConfigDestination, writeConfigAtomic, writeConfigTransaction } = await importImpl(import.meta.url, 'mcp/config-file.mjs');
60
+
59
61
  // ---------------------------------------------------------------------------
60
62
  // Shared JSON helpers
61
63
  // ---------------------------------------------------------------------------
@@ -631,7 +633,7 @@ export function mergeMcpProfile(existingDoc, opts = {}) {
631
633
  * preserving unknown keys, timestamped backup before writing, atomic write,
632
634
  * dry-run diff with zero writes, fail closed on unparseable settings.
633
635
  */
634
- export function deployMuseMcp(opts = {}) {
636
+ export async function deployMuseMcp(opts = {}) {
635
637
  const { dryRun = false, quiet = false, mcp = false, env = process.env, userHome = os.homedir() } = opts;
636
638
  const log = quiet ? () => {} : (msg) => console.log(msg);
637
639
 
@@ -642,6 +644,8 @@ export function deployMuseMcp(opts = {}) {
642
644
 
643
645
  const settingsPath = assertMuseUserSettingsPath(env, userHome);
644
646
  const configDir = path.dirname(settingsPath);
647
+ await assertConfigDestination(settingsPath);
648
+ await assertConfigDestination(path.join(configDir, MUSE_MCP_SIDECAR_NAME));
645
649
  const { doc: existingDoc, raw: existingRaw, existed } = readSettingsDoc(settingsPath);
646
650
  const sidecar = readMcpSidecar(configDir);
647
651
  const profile = aiwgMcpServerProfile();
@@ -680,14 +684,15 @@ export function deployMuseMcp(opts = {}) {
680
684
  return { settingsPath, changed: true, plan, wrote: false, warnings };
681
685
  }
682
686
 
683
- fs.mkdirSync(configDir, { recursive: true });
684
687
  let backupPath = null;
685
688
  if (existed) {
686
- backupPath = `${settingsPath}.aiwg-backup-${new Date().toISOString().replace(/[:.]/g, '-')}`;
687
- fs.copyFileSync(settingsPath, backupPath);
689
+ backupPath = `${settingsPath}.aiwg-backup-${new Date().toISOString().replace(/[:.]/g, '-')}-${randomUUID()}`;
690
+ await writeConfigAtomic(backupPath, existingRaw, { userScope: true });
688
691
  }
689
- atomicWrite(settingsPath, afterText);
690
- atomicWrite(sidecar.path, canonicalJson({ version: 1, servers: ['aiwg'] }));
692
+ await writeConfigTransaction([
693
+ { file: settingsPath, content: afterText, options: { userScope: true } },
694
+ { file: sidecar.path, content: canonicalJson({ version: 1, servers: ['aiwg'] }), options: { userScope: true } },
695
+ ]);
691
696
 
692
697
  log(`Muse MCP: merged AIWG stdio profile into ${settingsPath}`);
693
698
  for (const step of plan) log(` ${step}`);
@@ -700,11 +705,13 @@ export function deployMuseMcp(opts = {}) {
700
705
  * Removal path: drop the AIWG-managed `aiwg` server key (only when recorded
701
706
  * in the sidecar) and delete the sidecar. Operator servers are never touched.
702
707
  */
703
- export function pruneMuseMcp(opts = {}) {
708
+ export async function pruneMuseMcp(opts = {}) {
704
709
  const { dryRun = false, quiet = false, env = process.env, userHome = os.homedir() } = opts;
705
710
  const log = quiet ? () => {} : (msg) => console.log(msg);
706
711
  const settingsPath = assertMuseUserSettingsPath(env, userHome);
707
712
  const configDir = path.dirname(settingsPath);
713
+ await assertConfigDestination(settingsPath);
714
+ await assertConfigDestination(path.join(configDir, MUSE_MCP_SIDECAR_NAME));
708
715
 
709
716
  if (!fs.existsSync(settingsPath)) {
710
717
  log(`Muse MCP: no ${settingsPath}; nothing to prune.`);
@@ -731,10 +738,10 @@ export function pruneMuseMcp(opts = {}) {
731
738
  return { settingsPath, pruned: true, wrote: false };
732
739
  }
733
740
 
734
- const backupPath = `${settingsPath}.aiwg-backup-${new Date().toISOString().replace(/[:.]/g, '-')}`;
735
- fs.copyFileSync(settingsPath, backupPath);
736
- atomicWrite(settingsPath, canonicalJson(merged));
737
- if (fs.existsSync(sidecar.path)) fs.unlinkSync(sidecar.path);
741
+ const backupPath = `${settingsPath}.aiwg-backup-${new Date().toISOString().replace(/[:.]/g, '-')}-${randomUUID()}`;
742
+ await writeConfigAtomic(backupPath, existingRaw, { userScope: true });
743
+ await writeConfigAtomic(settingsPath, canonicalJson(merged), { userScope: true });
744
+ if (await assertConfigDestination(sidecar.path)) fs.unlinkSync(sidecar.path);
738
745
  log(`Muse MCP: removed mcp_servers.aiwg from ${settingsPath} (backup: ${backupPath}).`);
739
746
  return { settingsPath, pruned: true, wrote: true, backupPath };
740
747
  }
@@ -35,6 +35,7 @@ import fs from 'node:fs';
35
35
  import os from 'node:os';
36
36
  import path from 'node:path';
37
37
  import {
38
+ withRuleDeployment,
38
39
  collectFrameworkArtifacts,
39
40
  createAgentsMdFromTemplate,
40
41
  deploySkillsWithKernelRouting,
@@ -226,7 +227,7 @@ export async function postDeploy(targetDir, opts = {}) {
226
227
  // bridge, managed hooks, and an opted-in MCP profile must not be skipped.
227
228
  const fullDeploy = !opts.commandsOnly && !opts.rulesOnly && (!opts.skillsOnly || opts.kernelOnly === true);
228
229
  if (opts.createAgentsMd || fullDeploy) {
229
- createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
230
+ if (opts.contextFiles !== false) createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
230
231
  }
231
232
 
232
233
  // #228 — managed project hooks (.muse/hooks.json). Default on, mirroring
@@ -240,7 +241,7 @@ export async function postDeploy(targetDir, opts = {}) {
240
241
  // #228 — optional MCP settings profile. Explicit opt-in only (--mcp);
241
242
  // default `aiwg use --provider muse` never touches user settings.
242
243
  if (fullDeploy && opts.mcp === true) {
243
- deployMuseMcp(opts);
244
+ await deployMuseMcp(opts);
244
245
  }
245
246
 
246
247
  if (!opts.quiet) {
@@ -265,6 +266,10 @@ export async function postDeploy(targetDir, opts = {}) {
265
266
  }
266
267
 
267
268
  export async function deploy(opts = {}) {
269
+ return withRuleDeployment(opts, { name, paths, kernelSkillsPath }, deploySelected);
270
+ }
271
+
272
+ async function deploySelected(opts) {
268
273
  const {
269
274
  srcRoot,
270
275
  target,
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { createHash } from 'node:crypto';
5
5
  import YAML from 'yaml';
6
6
  import { importImpl } from '../../_resolve-impl.mjs';
7
- import { collectFrameworkArtifacts, getAddonFiles, isKernelSkill, listMdFiles, listSkillDirs, normalizeDeploymentMode, resolveAiwgRoot } from './base.mjs';
7
+ import { withRuleDeployment, collectFrameworkArtifacts, getAddonFiles, isKernelSkill, listMdFiles, listSkillDirs, normalizeDeploymentMode, resolveAiwgRoot } from './base.mjs';
8
8
  export const name = 'omp';
9
9
  export const aliases = ['oh-my-pi'];
10
10
  export const paths = { agents: '.omp/agents', commands: '.omp/prompts', skills: '.agents/skills', rules: '.omp/rules' };
@@ -131,7 +131,7 @@ function deployType(files, target, opts, type, transform) {
131
131
  }
132
132
  export const deployAgents = (files, target, opts = {}) => deployType(files, target, opts, 'agents', transformAgent);
133
133
  export const deployCommands = (files, target, opts = {}) => deployType(files, target, opts, 'prompts', transformCommand);
134
- export const deployRules = (files, target, opts = {}) => deployType(files, target, opts, 'rules', transformRule);
134
+ export const deployRules = (files, target, opts = {}) => opts.ruleDelivery ? 0 : deployType(files, target, opts, 'rules', transformRule);
135
135
  export function deploySkills(dirs, target, opts = {}) {
136
136
  let count = 0; const seen = new Map(); const root = roots(target, opts);
137
137
  for (const dir of [...new Set(dirs)]) {
@@ -223,10 +223,20 @@ export function deployExtensionBridge(target, opts = {}) {
223
223
  return writeOwned(path.join(roots(target, opts).native, 'extensions/aiwg-bridge.ts'), fs.readFileSync(src, 'utf8'), src, opts, 'omp-extension');
224
224
  }
225
225
  export async function postDeploy(target, opts) {
226
- if (opts.createAgentsMd || (!opts.commandsOnly && !opts.skillsOnly && !opts.rulesOnly)) createAgentsMd(target, opts.srcRoot, opts.dryRun);
226
+ if (opts.contextFiles !== false && (opts.createAgentsMd || (!opts.commandsOnly && !opts.skillsOnly && !opts.rulesOnly))) createAgentsMd(target, opts.srcRoot, opts.dryRun);
227
227
  }
228
228
  export const getFileExtension = () => '.md';
229
229
  export async function deploy(opts) {
230
+ return withRuleDeployment(opts, { name, paths, kernelSkillsPath }, async staged => {
231
+ let count = await deploySelected(staged);
232
+ for (const srcRoot of staged.additionalSources || []) {
233
+ count += await deploySelected({ ...staged, srcRoot });
234
+ }
235
+ return count;
236
+ });
237
+ }
238
+
239
+ async function deploySelected(opts) {
230
240
  const mode = normalizeDeploymentMode(opts.mode);
231
241
  const include = { includeAgents: true, includeCommands: opts.deployCommands || opts.commandsOnly, includeSkills: opts.deploySkills || opts.skillsOnly, includeRules: opts.deployRules || opts.rulesOnly };
232
242
  const directSource = ['agents', 'commands', 'skills', 'rules'].some(type => fs.existsSync(path.join(opts.srcRoot, type)));
@@ -251,7 +261,21 @@ export async function deploy(opts) {
251
261
  let count = 0;
252
262
  if (!opts.commandsOnly && !opts.skillsOnly && !opts.rulesOnly) count += deployAgents(selected('agents'), opts.target, opts);
253
263
  if (include.includeCommands && !opts.skillsOnly && !opts.rulesOnly) count += deployCommands(selected('commands'), opts.target, opts);
254
- if (include.includeSkills && !opts.commandsOnly && !opts.rulesOnly) count += deploySkills(selected('skills'), opts.target, opts);
264
+ if (include.includeSkills && !opts.commandsOnly && !opts.rulesOnly) {
265
+ // Different frameworks may index same-named standard skills. With copying
266
+ // disabled they share only a stale-copy cleanup destination, not a native
267
+ // definition slot. Keep one cleanup pass; retain collision checks for every
268
+ // kernel skill and for explicitly copied standard skills.
269
+ const lazyNames = new Set();
270
+ const skills = selected('skills').filter(dir => {
271
+ if (opts.copyStandardSkills || isKernelSkill(dir)) return true;
272
+ const basename = path.basename(dir);
273
+ if (lazyNames.has(basename)) return false;
274
+ lazyNames.add(basename);
275
+ return true;
276
+ });
277
+ count += deploySkills(skills, opts.target, opts);
278
+ }
255
279
  if (include.includeRules && !opts.commandsOnly && !opts.skillsOnly) count += deployRules(selected('rules'), opts.target, opts);
256
280
  if (!opts.commandsOnly && !opts.skillsOnly && !opts.rulesOnly) count += deployExtensionBridge(opts.target, opts);
257
281
  await postDeploy(opts.target, opts); return count;
@@ -23,7 +23,9 @@ const staticModelCatalog = _require('../../../agentic/code/providers/model-catal
23
23
  let fs;
24
24
  try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; }
25
25
  import path from 'path';
26
+ import { importImpl } from '../../_resolve-impl.mjs';
26
27
  import {
28
+ withRuleDeployment,
27
29
  ensureDir,
28
30
  listMdFiles,
29
31
  listMdFilesRecursive,
@@ -50,6 +52,7 @@ import {
50
52
  filterCommandsAgainstSkills,
51
53
  deploySoulCompanions
52
54
  } from './base.mjs';
55
+ const { assertConfigDestination, writeConfigAtomic } = await importImpl(import.meta.url, 'mcp/config-file.mjs');
53
56
  const modelCatalog = loadRuntimeModelCatalog(staticModelCatalog);
54
57
 
55
58
  // ============================================================================
@@ -375,6 +378,7 @@ export function deploySkills(skillDirs, targetDir, opts) {
375
378
  * Deploy rules to .opencode/rule/
376
379
  */
377
380
  export function deployRules(ruleFiles, targetDir, opts) {
381
+ if (opts.ruleDelivery) return [];
378
382
  const destDir = path.join(targetDir, paths.rules);
379
383
  ensureDir(destDir, opts.dryRun);
380
384
  cleanupOldRuleFiles(destDir, opts);
@@ -394,9 +398,9 @@ export function deployRules(ruleFiles, targetDir, opts) {
394
398
  * Non-destructive: a malformed existing opencode.json is left untouched (warn,
395
399
  * skip) rather than overwritten.
396
400
  *
397
- * @returns {boolean} true if the config was changed (or would be, in dry-run)
401
+ * @returns {Promise<boolean>} true if the config was changed (or would be, in dry-run)
398
402
  */
399
- export function mergeOpenCodeInstructions(targetDir, opts = {}) {
403
+ export async function mergeOpenCodeInstructions(targetDir, opts = {}) {
400
404
  const RULE_GLOB = `${paths.rules}*.md`; // .opencode/rule/*.md — opencode supports globs in instructions (verified in sst/opencode session/instruction.ts)
401
405
  // opencode auto-loads AGENTS.md/CLAUDE.md/CONTEXT.md but NOT AIWG.md, and the
402
406
  // generated AGENTS.md is a thin link-index that points to AIWG.md. The
@@ -407,6 +411,7 @@ export function mergeOpenCodeInstructions(targetDir, opts = {}) {
407
411
  const AIWG = 'AIWG.md';
408
412
  const wanted = [RULE_GLOB, AGENTS, AIWG];
409
413
  const configPath = path.join(targetDir, 'opencode.json');
414
+ await assertConfigDestination(configPath, targetDir);
410
415
 
411
416
  let config = {};
412
417
  if (fs.existsSync(configPath)) {
@@ -433,7 +438,7 @@ export function mergeOpenCodeInstructions(targetDir, opts = {}) {
433
438
 
434
439
  // Preserve operator-authored entries; append only what's missing, deduped.
435
440
  config.instructions = [...existing, ...missing];
436
- fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
441
+ await writeConfigAtomic(configPath, `${JSON.stringify(config, null, 2)}\n`, { projectRoot: targetDir });
437
442
  if (opts.verbose) {
438
443
  console.log(`Wired ${missing.length} entr${missing.length === 1 ? 'y' : 'ies'} into opencode.json instructions[]: ${missing.join(', ')}`);
439
444
  }
@@ -503,7 +508,7 @@ export async function postDeploy(targetDir, opts) {
503
508
  initializeFrameworkWorkspace(targetDir, opts.mode, opts.dryRun, opts.srcRoot);
504
509
 
505
510
  if (opts.createAgentsMd) {
506
- createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
511
+ if (opts.contextFiles !== false) createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
507
512
  }
508
513
 
509
514
  // PUW-035 (#1136): emit SDLC primary roles as TUI modes.
@@ -527,6 +532,10 @@ export function getFileExtension(type) {
527
532
  // ============================================================================
528
533
 
529
534
  export async function deploy(opts) {
535
+ return withRuleDeployment(opts, { name, paths, kernelSkillsPath }, deploySelected);
536
+ }
537
+
538
+ async function deploySelected(opts) {
530
539
  const {
531
540
  srcRoot,
532
541
  target,
@@ -637,7 +646,7 @@ export async function deploy(opts) {
637
646
  // #1548: rules on disk are inert unless referenced from opencode.json
638
647
  // instructions[]. Wire them so opencode actually loads them.
639
648
  try {
640
- mergeOpenCodeInstructions(target, opts);
649
+ await mergeOpenCodeInstructions(target, opts);
641
650
  } catch (err) {
642
651
  console.warn(`Warning: could not wire opencode.json instructions[]: ${err && err.message ? err.message : err}`);
643
652
  }
@@ -173,7 +173,7 @@ export async function postDeploy(targetDir, opts) {
173
173
  opts.createAgentsMd ||
174
174
  (!opts.commandsOnly && !opts.skillsOnly && !opts.rulesOnly)
175
175
  ) {
176
- createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
176
+ if (opts.contextFiles !== false) createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
177
177
  }
178
178
  }
179
179
 
@@ -128,7 +128,7 @@ export function createAgentsMd(target, srcRoot, dryRun) {
128
128
 
129
129
  export async function postDeploy(targetDir, opts) {
130
130
  if (opts.createAgentsMd || (!opts.commandsOnly && !opts.skillsOnly && !opts.rulesOnly)) {
131
- createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
131
+ if (opts.contextFiles !== false) createAgentsMd(targetDir, opts.srcRoot, opts.dryRun);
132
132
  }
133
133
  if (!opts.quiet && (opts.deployCommands || opts.deploySkills || opts.commandsOnly || opts.skillsOnly)) {
134
134
  console.log('Pi project resources are trust-gated; approve the project with `/trust` (restart required) or use `pi --approve` for a reviewed one-shot run.');
@@ -81,6 +81,10 @@ function prepareInChild({ job, attempt, sessionRoot, poolDescriptor, workerPath,
81
81
  let outcome;
82
82
  let killTimer;
83
83
  let timedOut = false;
84
+ let exited = false;
85
+ let disconnected = false;
86
+ let exitCode;
87
+ let exitSignal;
84
88
  const finish = () => {
85
89
  if (settled) return;
86
90
  settled = true;
@@ -107,19 +111,30 @@ function prepareInChild({ job, attempt, sessionRoot, poolDescriptor, workerPath,
107
111
  timer.unref?.();
108
112
  child.once('error', error => {
109
113
  // A spawn failure has no operating-system child to reap. Every error
110
- // after a pid is assigned still waits for the exit event below.
114
+ // after a pid is assigned still waits for exit and IPC disconnect below.
111
115
  if (!child.pid) { outcome = { error }; finish(); }
112
116
  else stop(error);
113
117
  });
114
- // Settle only after the operating system reports process exit. This keeps
115
- // every worker in activeChildren until it is confirmed gone, including workers
116
- // that send a response and retain an unexpected live handle.
117
- child.once('exit', (code, signal) => {
118
- if (outcome?.value && (code !== 0 || signal)) {
119
- outcome = { error: new Error(`Provider preparation worker exited ${signal || code || 'without status'} after responding for ${job.provider}`) };
118
+ // Exit confirms termination; disconnect confirms pending IPC messages have
119
+ // drained. Either event can arrive first. Keep workers active until both,
120
+ // including workers that respond and retain an unexpected live handle.
121
+ const finishAfterDisconnectAndExit = () => {
122
+ if (!exited || !disconnected) return;
123
+ if (outcome?.value && (exitCode !== 0 || exitSignal)) {
124
+ outcome = { error: new Error(`Provider preparation worker exited ${exitSignal || exitCode || 'without status'} after responding for ${job.provider}`) };
120
125
  }
121
- if (!outcome) outcome = { error: new Error(`${timedOut ? 'Provider preparation timed out' : `Provider preparation worker exited ${code ?? 'without status'}`} for ${job.provider}${stderrBytes ? `: ${Buffer.concat(stderr).toString('utf8').trim()}` : ''}`) };
126
+ if (!outcome) outcome = { error: new Error(`${timedOut ? 'Provider preparation timed out' : `Provider preparation worker exited ${exitCode ?? 'without status'}`} for ${job.provider}${stderrBytes ? `: ${Buffer.concat(stderr).toString('utf8').trim()}` : ''}`) };
122
127
  finish();
128
+ };
129
+ child.once('exit', (code, signal) => {
130
+ exited = true;
131
+ exitCode = code;
132
+ exitSignal = signal;
133
+ finishAfterDisconnectAndExit();
134
+ });
135
+ child.once('disconnect', () => {
136
+ disconnected = true;
137
+ finishAfterDisconnectAndExit();
123
138
  });
124
139
  child.once('message', response => {
125
140
  try {
@@ -137,7 +152,7 @@ function prepareInChild({ job, attempt, sessionRoot, poolDescriptor, workerPath,
137
152
  if (response.type !== 'prepared') throw new Error('Invalid provider preparation worker response type');
138
153
  outcome = { value: { detached: readPlan(response, expectedPlan), scratch, attempt } };
139
154
  // Closing the IPC channel lets the single-request worker exit normally;
140
- // the exit handler above confirms termination before exposing its plan.
155
+ // the exit/disconnect handlers confirm termination and IPC drainage.
141
156
  if (child.connected) { try { child.disconnect(); } catch {} }
142
157
  } catch (error) { stop(error); }
143
158
  });
@@ -200,6 +215,10 @@ export async function prepareProviderDeployments({
200
215
  removeSession();
201
216
  };
202
217
  const prepareOne = async (index, attempt = 0) => {
218
+ // Never start a worker once the session is closed: it would outlive
219
+ // cleanup, and anything it writes (e.g. the shared binding-index cache)
220
+ // would land after its owner removed it.
221
+ if (closed) throw new Error('Provider preparation session is closed');
203
222
  const raw = await prepareInChild({ job: jobs[index], attempt, sessionRoot, poolDescriptor, workerPath, workerEnv, activeChildren, timeoutMs });
204
223
  const plan = transaction.adopt(raw.detached, {
205
224
  scratch: raw.scratch,
@@ -212,6 +231,7 @@ export async function prepareProviderDeployments({
212
231
  try {
213
232
  await Promise.all(Array.from({ length: concurrency }, async () => {
214
233
  for (;;) {
234
+ if (closed) return;
215
235
  const index = next++;
216
236
  if (index >= jobs.length) return;
217
237
  prepared[index] = await prepareOne(index);