@aiwg/cli 2026.10.3 → 2026.10.5

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 (279) hide show
  1. package/README.md +2 -2
  2. package/agentic/code/providers/model-capabilities.v1.json +96 -4
  3. package/bin/aiwg.mjs +50 -27
  4. package/dist/src/agents/grounding/knowledge-base.js +54 -6
  5. package/dist/src/artifacts/browser-export.js +6 -0
  6. package/dist/src/artifacts/checksum-manifest.js +1 -0
  7. package/dist/src/artifacts/citation-parser.js +173 -92
  8. package/dist/src/artifacts/citation-table-schema.js +59 -0
  9. package/dist/src/artifacts/cli.js +69 -18
  10. package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
  11. package/dist/src/artifacts/corpus-tools/vision-extract.js +2 -0
  12. package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
  13. package/dist/src/artifacts/corpus-views/renderers.js +3 -3
  14. package/dist/src/artifacts/discovery-eval.js +6 -4
  15. package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
  16. package/dist/src/artifacts/fortemi-core-sync.js +166 -40
  17. package/dist/src/artifacts/framework-capabilities.js +171 -0
  18. package/dist/src/artifacts/framework-index-paths.js +73 -0
  19. package/dist/src/artifacts/framework-source-digest.js +75 -0
  20. package/dist/src/artifacts/index-builder.js +286 -131
  21. package/dist/src/artifacts/index-files.js +132 -18
  22. package/dist/src/artifacts/index-preparation.js +130 -0
  23. package/dist/src/artifacts/index-refresh.js +591 -0
  24. package/dist/src/artifacts/index-worker.mjs +11 -0
  25. package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
  26. package/dist/src/artifacts/prebuilt-deployment.js +46 -0
  27. package/dist/src/artifacts/prebuilt-relations.js +80 -0
  28. package/dist/src/artifacts/query-engine.js +410 -88
  29. package/dist/src/artifacts/relations-builder.js +168 -0
  30. package/dist/src/artifacts/relations.js +148 -0
  31. package/dist/src/artifacts/stats.js +5 -2
  32. package/dist/src/artifacts/types.js +30 -10
  33. package/dist/src/artifacts/watcher.js +1 -1
  34. package/dist/src/auth/credential-store.js +2 -1
  35. package/dist/src/catalog/cli.d.mts +5 -0
  36. package/dist/src/channel/manager.d.mts +20 -0
  37. package/dist/src/channel/manager.mjs +19 -2
  38. package/dist/src/cli/command-log.js +2 -3
  39. package/dist/src/cli/handlers/artifacts.js +3 -0
  40. package/dist/src/cli/handlers/auth.js +3 -0
  41. package/dist/src/cli/handlers/decision.js +19 -12
  42. package/dist/src/cli/handlers/diagnose.js +4 -3
  43. package/dist/src/cli/handlers/effect.js +4 -2
  44. package/dist/src/cli/handlers/evidence.js +3 -0
  45. package/dist/src/cli/handlers/help.js +1 -0
  46. package/dist/src/cli/handlers/index.js +3 -1
  47. package/dist/src/cli/handlers/init.js +4 -1
  48. package/dist/src/cli/handlers/lint.js +5 -0
  49. package/dist/src/cli/handlers/models.js +104 -8
  50. package/dist/src/cli/handlers/ralph-launcher.js +2 -0
  51. package/dist/src/cli/handlers/refresh.js +12 -0
  52. package/dist/src/cli/handlers/resource-versions.js +3 -0
  53. package/dist/src/cli/handlers/rules.js +195 -0
  54. package/dist/src/cli/handlers/run.js +4 -0
  55. package/dist/src/cli/handlers/runtime-info.js +1 -2
  56. package/dist/src/cli/handlers/script-runner.js +3 -2
  57. package/dist/src/cli/handlers/session.js +19 -9
  58. package/dist/src/cli/handlers/sessions.js +137 -43
  59. package/dist/src/cli/handlers/setup-manifest.js +39 -44
  60. package/dist/src/cli/handlers/setup.js +41 -0
  61. package/dist/src/cli/handlers/steward.js +11 -0
  62. package/dist/src/cli/handlers/subcommands.js +57 -2
  63. package/dist/src/cli/handlers/uhp.js +3 -0
  64. package/dist/src/cli/handlers/use.js +294 -55
  65. package/dist/src/cli/handlers/utilities.js +26 -10
  66. package/dist/src/cli/handlers/version.js +8 -0
  67. package/dist/src/cli/router.js +15 -1
  68. package/dist/src/cli/services/deployment-verification.js +19 -9
  69. package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
  70. package/dist/src/cli/skill-usage.js +2 -3
  71. package/dist/src/config/aiwg-config.js +87 -37
  72. package/dist/src/config/cli.js +111 -5
  73. package/dist/src/config/decision-config.js +106 -0
  74. package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
  75. package/dist/src/config/gitignore.js +50 -0
  76. package/dist/src/config/index-graph-overrides.js +36 -0
  77. package/dist/src/config/project-artifacts-health.d.mts +37 -0
  78. package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
  79. package/dist/src/config/user-config-dir.d.mts +9 -0
  80. package/dist/src/config/user-config.js +2 -1
  81. package/dist/src/config/user-registry.js +23 -15
  82. package/dist/src/config/user-state.js +7 -0
  83. package/dist/src/decision/ask.js +203 -43
  84. package/dist/src/decision/context-live-qualification.js +2 -2
  85. package/dist/src/decision/driver.js +13 -8
  86. package/dist/src/decision/ensemble/aggregate.js +23 -10
  87. package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
  88. package/dist/src/decision/entry.js +2 -15
  89. package/dist/src/decision/graph-live-qualification.js +1 -1
  90. package/dist/src/decision/graph-run.js +9 -0
  91. package/dist/src/decision/heldout/collector.js +5 -13
  92. package/dist/src/decision/heldout/contract.js +30 -7
  93. package/dist/src/decision/jev-credentials.js +15 -1
  94. package/dist/src/decision/ledger.js +131 -28
  95. package/dist/src/decision/offload-activation.js +137 -0
  96. package/dist/src/decision/offload.js +550 -0
  97. package/dist/src/decision/qualification/load.js +4 -6
  98. package/dist/src/decision/savings.js +28 -0
  99. package/dist/src/effects/types.js +5 -1
  100. package/dist/src/effects/verifiers/git.js +2 -1
  101. package/dist/src/effects/verifiers/tracker.js +26 -14
  102. package/dist/src/extensions/commands/definitions.js +19 -0
  103. package/dist/src/extensions/project-local-remove.js +36 -1
  104. package/dist/src/gates/providers/loader.js +61 -17
  105. package/dist/src/installation/manager-command.d.mts +18 -0
  106. package/dist/src/installation/manager-command.mjs +14 -2
  107. package/dist/src/installation/manager.d.mts +49 -0
  108. package/dist/src/installation/manager.mjs +78 -3
  109. package/dist/src/issues/cli.js +4 -4
  110. package/dist/src/issues/local.js +24 -5
  111. package/dist/src/jobs/runner.js +1 -1
  112. package/dist/src/lint/cli.js +27 -1
  113. package/dist/src/lint/loader.js +2 -1
  114. package/dist/src/lint/reporters.js +25 -1
  115. package/dist/src/lint/rule-activation.js +43 -0
  116. package/dist/src/lint/runner.js +137 -62
  117. package/dist/src/mcp/adapters/codex-runtime.js +7 -1
  118. package/dist/src/mcp/claude-install.d.mts +4 -0
  119. package/dist/src/mcp/claude-install.mjs +41 -0
  120. package/dist/src/mcp/cli.d.mts +1 -0
  121. package/dist/src/mcp/cli.mjs +36 -11
  122. package/dist/src/mcp/config-file.d.mts +28 -0
  123. package/dist/src/mcp/config-layers.d.mts +36 -0
  124. package/dist/src/mcp/credentials.d.mts +51 -0
  125. package/dist/src/mcp/grok-build-config.d.mts +6 -0
  126. package/dist/src/mcp/grok-build-config.mjs +1 -1
  127. package/dist/src/mcp/helpers.mjs +3 -2
  128. package/dist/src/mcp/muse-native-config.d.mts +13 -0
  129. package/dist/src/mcp/muse-native-config.mjs +1 -0
  130. package/dist/src/mcp/omp-config.d.mts +4 -0
  131. package/dist/src/mcp/registry.js +3 -93
  132. package/dist/src/mcp/registry.mjs +3 -87
  133. package/dist/src/mcp/server-config.d.mts +3 -0
  134. package/dist/src/mcp/server-config.mjs +89 -0
  135. package/dist/src/mcp/server.mjs +37 -375
  136. package/dist/src/mcp/test-simple.mjs +2 -1
  137. package/dist/src/mcp/toml-editor.d.mts +4 -0
  138. package/dist/src/mcp/toml-string.d.mts +1 -0
  139. package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
  140. package/dist/src/mcp/tool-filters.d.mts +46 -0
  141. package/dist/src/mcp/tools/decision.mjs +12 -11
  142. package/dist/src/mcp/tools/discovery.mjs +62 -55
  143. package/dist/src/mcp/tools/orchestration.mjs +12 -2
  144. package/dist/src/mcp/tools/worker-invoke.mjs +1340 -23
  145. package/dist/src/mcp/tools/worker-shell-control.mjs +52 -21
  146. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +3 -3
  147. package/dist/src/mission-protocol/codecs.js +8 -0
  148. package/dist/src/models/config-loader.js +7 -2
  149. package/dist/src/models/model-capabilities.v1.json +96 -4
  150. package/dist/src/models/model-discovery.js +76 -11
  151. package/dist/src/models/provider-policy.js +101 -7
  152. package/dist/src/models/qualification-digest.d.mts +2 -0
  153. package/dist/src/models/resolver.js +12 -10
  154. package/dist/src/models/worker-direct-readiness.js +65 -44
  155. package/dist/src/models/worker-dispatch-offer.js +3 -3
  156. package/dist/src/models/worker-health-readiness.js +16 -3
  157. package/dist/src/models/worker-installation.d.mts +39 -0
  158. package/dist/src/models/worker-installation.mjs +135 -28
  159. package/dist/src/process/bounded-child-process.d.mts +84 -0
  160. package/dist/src/process/bounded-child-process.mjs +43 -0
  161. package/dist/src/process/cli-command.d.mts +16 -0
  162. package/dist/src/process/signal-child.d.mts +6 -0
  163. package/dist/src/process/signal-child.mjs +7 -0
  164. package/dist/src/providers/deployed-skill-roots.js +20 -0
  165. package/dist/src/providers/grok-build-acp.js +3 -2
  166. package/dist/src/providers/grok-build-headless.js +12 -4
  167. package/dist/src/providers/grok-build-inspect.js +1 -0
  168. package/dist/src/providers/omp-agent.d.mts +8 -0
  169. package/dist/src/providers/omp-diagnostics.d.mts +1 -0
  170. package/dist/src/providers/omp-paths.d.mts +13 -0
  171. package/dist/src/providers/skill-ownership.d.mts +9 -0
  172. package/dist/src/providers/skill-ownership.mjs +88 -0
  173. package/dist/src/rules/activation.d.mts +21 -0
  174. package/dist/src/rules/activation.mjs +62 -0
  175. package/dist/src/rules/inline-budget.d.mts +26 -0
  176. package/dist/src/rules/inline-budget.mjs +66 -0
  177. package/dist/src/rules/inventory.js +129 -0
  178. package/dist/src/rules/pin-state.js +73 -0
  179. package/dist/src/rules/pins.d.mts +14 -0
  180. package/dist/src/rules/pins.mjs +112 -0
  181. package/dist/src/rules/rule-frontmatter.schema.json +197 -0
  182. package/dist/src/serve/mission-conductor.js +38 -12
  183. package/dist/src/serve/worker-invocation.js +30 -10
  184. package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
  185. package/dist/src/sessions/adapter-registry.js +23 -19
  186. package/dist/src/sessions/adapters/claude.js +39 -54
  187. package/dist/src/sessions/adapters/codex.js +95 -67
  188. package/dist/src/sessions/adapters/copilot.js +4 -4
  189. package/dist/src/sessions/adapters/cursor.js +40 -57
  190. package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
  191. package/dist/src/sessions/adapters/factory.js +31 -27
  192. package/dist/src/sessions/adapters/generic.js +10 -10
  193. package/dist/src/sessions/adapters/grok-build.js +11 -11
  194. package/dist/src/sessions/adapters/grokbot.js +24 -13
  195. package/dist/src/sessions/adapters/hermes.js +27 -29
  196. package/dist/src/sessions/adapters/muse-workspace-scan.js +31 -11
  197. package/dist/src/sessions/adapters/muse.js +150 -93
  198. package/dist/src/sessions/adapters/omp.js +28 -14
  199. package/dist/src/sessions/adapters/openclaw.js +89 -79
  200. package/dist/src/sessions/adapters/opencode.js +27 -24
  201. package/dist/src/sessions/adapters/openhuman.js +18 -60
  202. package/dist/src/sessions/adapters/pi.js +29 -14
  203. package/dist/src/sessions/adapters/warp.js +3 -3
  204. package/dist/src/sessions/adapters/windsurf.js +19 -48
  205. package/dist/src/sessions/batch-contracts.js +16 -8
  206. package/dist/src/sessions/batch-import.js +252 -12
  207. package/dist/src/sessions/contracts.js +50 -3
  208. package/dist/src/sessions/coverage-preflight.js +27 -2
  209. package/dist/src/sessions/export-plan.js +4 -3
  210. package/dist/src/sessions/import-exclusions.js +72 -0
  211. package/dist/src/sessions/import-lease.js +34 -18
  212. package/dist/src/sessions/import-limits.js +37 -0
  213. package/dist/src/sessions/importer.js +60 -38
  214. package/dist/src/sessions/lifecycle.js +45 -0
  215. package/dist/src/sessions/policy.js +145 -26
  216. package/dist/src/sessions/readers.js +106 -53
  217. package/dist/src/sessions/refresh.js +105 -0
  218. package/dist/src/sessions/repository.js +151 -54
  219. package/dist/src/sessions/workspace-discovery.js +292 -60
  220. package/dist/src/sessions/workspace-matching.js +107 -0
  221. package/dist/src/skills/importer.js +1 -0
  222. package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
  223. package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
  224. package/dist/src/smiths/context-pipeline/finalization.js +6 -5
  225. package/dist/src/smiths/context-pipeline/generator.js +21 -4
  226. package/dist/src/smiths/context-pipeline/parallelism-section.js +7 -15
  227. package/dist/src/smiths/mcpsmith/generator.js +1 -1
  228. package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
  229. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +76 -18
  230. package/dist/src/storage/backends/fs.js +18 -76
  231. package/dist/src/storage/backends/obsidian.js +1 -0
  232. package/dist/src/tracker/forge-http.js +2 -1
  233. package/dist/src/update/checker.d.mts +2 -0
  234. package/dist/src/update/service.d.mts +22 -0
  235. package/dist/src/utils/contained-filesystem.d.mts +37 -0
  236. package/dist/src/utils/contained-filesystem.mjs +683 -0
  237. package/dist/src/utils/temporary-directory.js +36 -0
  238. package/dist/src/writing/example-generator.js +1 -1
  239. package/package.json +15 -2
  240. package/schemas/models/model-catalog.v1.schema.json +1 -1
  241. package/schemas/models/provider-model-capabilities.v1.schema.json +6 -0
  242. package/tools/_resolve-impl.mjs +13 -15
  243. package/tools/agents/deploy-agents.mjs +49 -21
  244. package/tools/agents/providers/base.mjs +104 -134
  245. package/tools/agents/providers/claude.mjs +8 -24
  246. package/tools/agents/providers/codex.mjs +18 -11
  247. package/tools/agents/providers/cursor.mjs +19 -18
  248. package/tools/agents/providers/grok-build.mjs +3 -1
  249. package/tools/agents/providers/hermes.mjs +57 -47
  250. package/tools/agents/providers/openclaw.mjs +9 -4
  251. package/tools/agents/providers/pi.mjs +5 -0
  252. package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
  253. package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
  254. package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
  255. package/tools/agents/providers/rule-delivery.mjs +1270 -0
  256. package/tools/agents/providers/warp.mjs +20 -26
  257. package/tools/cli/decision-offload-report.mjs +40 -0
  258. package/tools/cli/workspace-status.mjs +862 -0
  259. package/tools/plugin/package-plugins.mjs +186 -8
  260. package/tools/plugin/plugin-links.mjs +114 -0
  261. package/tools/providers/antigravity-transport.mjs +3 -1
  262. package/tools/providers/deepseek-harness-transport.mjs +6 -4
  263. package/tools/providers/omp-transport.mjs +4 -2
  264. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
  265. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
  266. package/tools/ralph-external/lib/omp-adapter.mjs +1 -1
  267. package/tools/ralph-external/lib/pi-adapter.mjs +0 -4
  268. package/tools/ralph-external/lib/provider-adapter.mjs +59 -1
  269. package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
  270. package/tools/ralph-external/lib/session-environment.mjs +28 -0
  271. package/tools/ralph-external/lib/systemd-client.mjs +49 -0
  272. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  273. package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
  274. package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
  275. package/tools/ralph-external/process-monitor.mjs +1017 -0
  276. package/tools/ralph-external/session-launcher.mjs +5057 -0
  277. package/tools/scripts/resource-preflight.mjs +541 -0
  278. package/tools/skills/deploy-skills-codex.mjs +2 -1
  279. package/vscode-extension/schemas/aiwg.config.v1.json +1194 -231
@@ -24,6 +24,7 @@ import path from 'path';
24
24
  import os from 'os';
25
25
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
26
26
  import {
27
+ withRuleDeployment,
27
28
  ensureDir,
28
29
  listMdFiles,
29
30
  deploySkillDir,
@@ -192,8 +193,8 @@ export function mapModel(shorthand, modelCfg, modelsConfig) {
192
193
  /**
193
194
  * Generate a lean AGENTS.md for Hermes
194
195
  *
195
- * Hermes loads AGENTS.md on every turn — keep it under 1,000 characters
196
- * to preserve context budget on 12GB VRAM setups.
196
+ * Hermes reaches AGENTS.md through its startup pointer on every task. Reserve
197
+ * the 19,000-character budget for CRITICAL kernel bodies and the kernel index.
197
198
  * See: docs/integrations/hermes-quickstart.md (Part 3)
198
199
  */
199
200
  // Hermes context cap is 20K chars (head-tail truncated above). Hard limit
@@ -201,6 +202,23 @@ export function mapModel(shorthand, modelCfg, modelsConfig) {
201
202
  const HERMES_AGENTS_MD_HARD_CAP = 19_000;
202
203
  const HERMES_AGENTS_MD_SOFT_WARN = 15_000;
203
204
 
205
+ /** Never truncate CRITICAL bodies. Report each contribution before promotion. */
206
+ export function assertContextBudget(content, name = 'AGENTS.md', rules = []) {
207
+ if (content.length <= HERMES_AGENTS_MD_HARD_CAP) return;
208
+ const critical = rules.filter(rule => rule.tagged && rule.activation === 'kernel'
209
+ && String(rule.enforcement || '').trim().toLowerCase() === 'critical');
210
+ const sizes = critical.map(rule => ({ name: rule.name,
211
+ chars: `## Rule: ${rule.name}\n\n${rule.body.trim()}`.length }));
212
+ const total = sizes.reduce((sum, rule) => sum + rule.chars, 0) + Math.max(0, sizes.length - 1) * 2;
213
+ throw new Error(
214
+ `Hermes ${name} (${content.length} chars) exceeds hard cap of ${HERMES_AGENTS_MD_HARD_CAP}. ` +
215
+ `Hermes truncates above 20K. ` +
216
+ (total > HERMES_AGENTS_MD_HARD_CAP ? 'CRITICAL kernel set alone exceeds the cap. ' : 'Assembled context exceeds the cap; review wrappers and operator additions. ') +
217
+ `CRITICAL kernel total: ${total} chars (bodies and rule headings).\n` +
218
+ sizes.map(rule => ` ${rule.name}: ${rule.chars} chars`).join('\n')
219
+ );
220
+ }
221
+
204
222
  /**
205
223
  * Top-7 CRITICAL rule directives, inlined into AGENTS.md for guaranteed
206
224
  * priming on every Hermes turn (#1318 / S7; skill-discovery added #1347).
@@ -297,41 +315,32 @@ surface as provider-agnostic tools when configured.
297
315
  **Artifacts** (project-required, .aiwg/ directory):
298
316
  - \`mcp_aiwg_artifact_read\` / \`mcp_aiwg_artifact_write\``;
299
317
 
318
+ function mergeHermesContext(file, generated, kind) {
319
+ let existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
320
+ existing = existing.replace(/\n?<!-- AIWG:rule-startup:start -->[\s\S]*?<!-- AIWG:rule-startup:end -->\n?/g, '');
321
+ const start = `<!-- AIWG:hermes-${kind}:start -->`;
322
+ const end = `<!-- AIWG:hermes-${kind}:end -->`;
323
+ const managed = `${start}\n${generated}\n${end}`;
324
+ if (existing.includes(start)) return existing.slice(0, existing.indexOf(start)) + managed + existing.slice(existing.indexOf(end) + end.length);
325
+ if (kind === 'context' && existing.startsWith('# AIWG Integration') && existing.includes(HERMES_AGENTS_MD_FOOTER)) {
326
+ existing = existing.slice(existing.indexOf(HERMES_AGENTS_MD_FOOTER) + HERMES_AGENTS_MD_FOOTER.length);
327
+ } else if (kind === 'routing' && existing.includes(generated)) {
328
+ existing = existing.replace(generated, '');
329
+ }
330
+ return existing ? `${existing.trimEnd()}\n\n${managed}\n` : `<!-- aiwg-managed -->\n${managed}\n`;
331
+ }
332
+
300
333
  export function generateAgentsMd(agentCount, skillCount, targetDir, opts) {
301
334
  const { dryRun } = opts;
302
335
 
336
+ // Startup is reserved for rule contracts. Workflow routing, artifact catalogs,
337
+ // and the optional MCP tool inventory duplicate discover/show and consume
338
+ // Hermes's finite per-turn context before the kernel is appended.
303
339
  const header = `# AIWG Integration
304
340
 
305
- AIWG connected through file-based deployment. Native Hermes skills are available
306
- at \`$HERMES_HOME/skills/\` (kernel) and \`$HERMES_HOME/skills/.aiwg/\` (standard).
307
- When unset, \`HERMES_HOME\` defaults to the platform-native Hermes home.
308
- Use \`aiwg discover\` and \`aiwg show <type> <name>\` for the on-demand catalog.
309
- The MCP sidecar (\`aiwg mcp serve\`) is optional.
310
-
311
- ## Route to AIWG When
312
-
313
- - Structured artifacts needed (requirements, architecture, test plans, risk registers)
314
- - Multi-step workflows with phase gates or checkpoints
315
- - Template-driven output that persists across sessions
316
-
317
- Handle in Hermes directly: one-off questions, short tasks, conversation.
318
-
319
- ## Memory Boundary
320
-
321
- When AIWG returns an artifact: store path + one-sentence summary in MEMORY.md.
322
- Do NOT copy artifact body text into memory. Reference, don't replicate.
323
-
324
- Use \`delegate_task(goal="...", context="...")\` for AIWG workflows.
325
- Child agents automatically exclude context files and memory.
326
-
327
- ## Artifact Store (.aiwg/)
328
-
329
- Fetch on demand via \`mcp_aiwg_artifact_read\`:
330
- - \`requirements/\` — use cases, user stories
331
- - \`architecture/\` — SAD, ADRs
332
- - \`planning/\` — phase plans
333
- - \`testing/\` — test strategy
334
- - \`security/\` — threat models
341
+ Skills: \`$HERMES_HOME/skills/\` (kernel), \`$HERMES_HOME/skills/.aiwg/\` (standard).
342
+ Find: \`aiwg discover "rule <topic>" --type rule\`. Fetch: \`aiwg show rule <name>\`.
343
+ MCP is optional; \`mcp_aiwg_rule_show\` exposes the same rule catalog.
335
344
  `;
336
345
 
337
346
  // On-demand tier (#1675): the MEDIUM/LOW rules not inlined above. Hermes has
@@ -339,18 +348,15 @@ Fetch on demand via \`mcp_aiwg_artifact_read\`:
339
348
  const onDemandSection = opts.srcRoot
340
349
  ? renderOnDemandRuleSection(listOnDemandRuleFiles(opts.srcRoot))
341
350
  : '';
342
- const sections = [header, CRITICAL_RULE_DIRECTIVES];
351
+ const migrated = new Set((opts.ruleDelivery?.rules || []).filter(rule => rule.tagged).map(rule => rule.name));
352
+ const directives = CRITICAL_RULE_DIRECTIVES.split(/(?=^### Rule: )/m).filter(section => section.startsWith('### Rule: ') && !migrated.has(section.match(/^### Rule: ([^ \n]+)/)?.[1])).join('');
353
+ const sections = [header, directives];
343
354
  if (onDemandSection) sections.push(onDemandSection);
344
- sections.push(HERMES_AGENTS_MD_FOOTER);
345
- const output = sections.join('\n\n');
355
+ // The optional tool inventory is available on demand through the CLI.
346
356
  const destPath = path.join(targetDir, 'AGENTS.md');
357
+ const output = mergeHermesContext(destPath, sections.join('\n\n'), 'context');
347
358
 
348
- if (output.length > HERMES_AGENTS_MD_HARD_CAP) {
349
- throw new Error(
350
- `Hermes AGENTS.md (${output.length} chars) exceeds hard cap of ${HERMES_AGENTS_MD_HARD_CAP}. ` +
351
- `Hermes truncates above 20K. Trim the priming block or split rule bodies further.`
352
- );
353
- }
359
+ assertContextBudget(output, 'AGENTS.md', opts.ruleDelivery?.rules);
354
360
 
355
361
  if (dryRun) {
356
362
  console.log(`[dry-run] Would write AGENTS.md (${output.length} chars, ${Math.round(output.length / 4)} tokens estimated)`);
@@ -397,7 +403,7 @@ when the full AIWG project context is needed.
397
403
  if (dryRun) {
398
404
  console.log(`[dry-run] Would write .hermes.md (${body.length} chars)`);
399
405
  } else {
400
- fs.writeFileSync(destPath, body, 'utf8');
406
+ fs.writeFileSync(destPath, mergeHermesContext(destPath, body, 'routing'), 'utf8');
401
407
  console.log(` Created .hermes.md (${body.length} chars)`);
402
408
  }
403
409
  return 1;
@@ -417,7 +423,7 @@ when the full AIWG project context is needed.
417
423
  */
418
424
  export function deploySkills(skillDirs, opts) {
419
425
  const standardDestDir = paths.skills;
420
- const kernelDestDir = kernelSkillsPath;
426
+ const kernelDestDir = opts.kernelSkillsPath || kernelSkillsPath;
421
427
 
422
428
  if (!opts.dryRun) {
423
429
  console.log(` Deploying ${skillDirs.length} skills (kernel + standard split)...`);
@@ -431,6 +437,10 @@ export function deploySkills(skillDirs, opts) {
431
437
  // ============================================================================
432
438
 
433
439
  export async function deploy(opts) {
440
+ return withRuleDeployment(opts, { name, paths, kernelSkillsPath, assertContextBudget }, deploySelected);
441
+ }
442
+
443
+ async function deploySelected(opts) {
434
444
  const {
435
445
  srcRoot,
436
446
  target,
@@ -509,7 +519,7 @@ export async function deploy(opts) {
509
519
  const kernelNames = computeAllKernelNames(srcRoot);
510
520
  if (kernelNames != null) {
511
521
  const desiredKernel = [...kernelNames, 'aiwg-orchestrate'];
512
- pruneStaleAiwgSkills(kernelSkillsPath, desiredKernel, opts);
522
+ pruneStaleAiwgSkills(opts.kernelSkillsPath || kernelSkillsPath, desiredKernel, opts);
513
523
 
514
524
  // Register kernel skills in Hermes's bundled manifest so the Curator
515
525
  // (v0.12.0+, 7-day archival cycle) does not archive them. Standard
@@ -603,7 +613,7 @@ export async function deploy(opts) {
603
613
  */
604
614
  export function updateBundledManifest(kernelSkillNames, opts) {
605
615
  const { dryRun, quiet } = opts;
606
- const manifestPath = path.join(kernelSkillsPath, '.bundled_manifest');
616
+ const manifestPath = path.join(opts.kernelSkillsPath || kernelSkillsPath, '.bundled_manifest');
607
617
  const aiwgTag = 'aiwg-managed';
608
618
 
609
619
  // Read existing manifest (if any) and split into "ours" vs "theirs"
@@ -668,7 +678,7 @@ export function migrateLegacySkillPath(opts) {
668
678
  // this provider (env var first, platform default otherwise): with
669
679
  // HERMES_HOME set, the legacy tree lives under that root — never under
670
680
  // the process home directory.
671
- const legacyPath = path.join(getHermesHome(), '.aiwg', 'skills');
681
+ const legacyPath = opts.hermesLegacySkills || path.join(getHermesHome(), '.aiwg', 'skills');
672
682
  const newPath = paths.skills;
673
683
 
674
684
  let legacyExists = false;
@@ -754,7 +764,7 @@ function deployAiwgOrchestrateSkill(srcRoot, opts) {
754
764
  return;
755
765
  }
756
766
 
757
- const destDir = path.join(kernelSkillsPath, 'aiwg-orchestrate');
767
+ const destDir = path.join(opts.kernelSkillsPath || kernelSkillsPath, 'aiwg-orchestrate');
758
768
  const destPath = path.join(destDir, 'SKILL.md');
759
769
 
760
770
  if (fs.existsSync(destPath)) {
@@ -26,6 +26,7 @@ try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs;
26
26
  import path from 'path';
27
27
  import os from 'os';
28
28
  import {
29
+ withRuleDeployment,
29
30
  ensureDir,
30
31
  listMdFiles,
31
32
  listMdFilesRecursive,
@@ -191,7 +192,7 @@ function deploySkills(skillDirs, opts) {
191
192
  // Standard: ~/.openclaw/.aiwg/skills/ (no extra aiwg/ — the .aiwg/ segment IS the namespace)
192
193
  const standardDestDir = paths.skills;
193
194
  // Kernel: ~/.openclaw/skills/aiwg/ (preserves legacy 2-level namespacing)
194
- const kernelDestDir = path.join(kernelSkillsPath, 'aiwg');
195
+ const kernelDestDir = path.join(opts.kernelSkillsPath || kernelSkillsPath, 'aiwg');
195
196
 
196
197
  deploySkillsWithKernelRouting(skillDirs, standardDestDir, kernelDestDir, opts);
197
198
 
@@ -396,6 +397,10 @@ export function getFileExtension(type) {
396
397
  // ============================================================================
397
398
 
398
399
  export async function deploy(opts) {
400
+ return withRuleDeployment(opts, { name, paths, kernelSkillsPath, resolveOpenClawWorkspace }, deploySelected);
401
+ }
402
+
403
+ async function deploySelected(opts) {
399
404
  const {
400
405
  srcRoot,
401
406
  mode,
@@ -485,9 +490,9 @@ export async function deploy(opts) {
485
490
  // (`aiwg use` invokes deploy-agents.mjs once per framework + addon
486
491
  // batch).
487
492
  {
488
- const _kernelDestDir = path.isAbsolute(kernelSkillsPath)
493
+ const _kernelDestDir = opts.kernelSkillsPath || (path.isAbsolute(kernelSkillsPath)
489
494
  ? kernelSkillsPath
490
- : path.join(target, kernelSkillsPath);
495
+ : path.join(target, kernelSkillsPath));
491
496
  pruneStaleAiwgSkills(_kernelDestDir, computeAllKernelNames(srcRoot), opts);
492
497
  }
493
498
  }
@@ -549,7 +554,7 @@ export async function deploy(opts) {
549
554
  srcRoot, 'agentic', 'code', 'frameworks', 'sdlc-complete', 'templates', 'openclaw', 'AGENTS.md.aiwg-template',
550
555
  );
551
556
  if (fs.existsSync(templatePath)) {
552
- const workspace = resolveOpenClawWorkspace();
557
+ const workspace = opts.activeWorkspace || resolveOpenClawWorkspace();
553
558
  ensureDir(workspace, dryRun);
554
559
  if (verbose || !opts.quiet) {
555
560
  console.log(`\nBridging AIWG context into ${path.join(workspace, 'AGENTS.md')} + SOUL.md (#1562/#1572)...`);
@@ -11,6 +11,7 @@ import path from 'path';
11
11
  import fs from 'fs';
12
12
  import {
13
13
  collectFrameworkArtifacts,
14
+ withRuleDeployment,
14
15
  createAgentsMdFromTemplate,
15
16
  deployFiles,
16
17
  deploySkillsWithKernelRouting,
@@ -139,6 +140,10 @@ export function getFileExtension() {
139
140
  }
140
141
 
141
142
  export async function deploy(opts) {
143
+ return withRuleDeployment(opts, { name, paths, kernelSkillsPath }, deploySelected);
144
+ }
145
+
146
+ async function deploySelected(opts) {
142
147
  const normalizedMode = normalizeDeploymentMode(opts.mode);
143
148
  const agentFiles = [];
144
149
  const commandFiles = [];
@@ -0,0 +1,5 @@
1
+ // A once-only IPC listener is removed before its async work finishes. Keep the
2
+ // channel referenced until the worker disconnects after publishing its response.
3
+ export function retainPreparationChannel() {
4
+ process.channel?.ref();
5
+ }
@@ -0,0 +1,247 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fork } from 'node:child_process';
5
+ import { createHash, randomBytes, randomUUID } from 'node:crypto';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { importImpl } from '../../_resolve-impl.mjs';
8
+ import {
9
+ createRuleDeploymentInputPool,
10
+ describeRuleDeploymentInputPool,
11
+ disposeRuleDeploymentInputPool,
12
+ adoptRuleDeploymentPlan,
13
+ commitRuleDeploymentPlan,
14
+ disposeRuleDeploymentPlan,
15
+ } from './rule-delivery.mjs';
16
+
17
+ const { trackTemporaryDirectory } = await importImpl(import.meta.url, 'utils/temporary-directory.js');
18
+ const { signalChild } = await importImpl(import.meta.url, 'process/signal-child.mjs');
19
+ const DEFAULT_WORKER = fileURLToPath(new URL('./provider-prepare-worker.mjs', import.meta.url));
20
+ const CHECKOUT_NEEDS_TS_TRANSFORM = !fs.existsSync(fileURLToPath(new URL('../../../dist/src', import.meta.url)));
21
+ const PLAN_LIMIT = 256 * 1024 * 1024;
22
+
23
+ const defaultTransaction = {
24
+ createPool: createRuleDeploymentInputPool,
25
+ describePool: describeRuleDeploymentInputPool,
26
+ disposePool: disposeRuleDeploymentInputPool,
27
+ adopt: adoptRuleDeploymentPlan,
28
+ commit: commitRuleDeploymentPlan,
29
+ disposePlan: disposeRuleDeploymentPlan,
30
+ isStale: error => error?.code === 'AIWG_RULE_DEPLOYMENT_STALE',
31
+ };
32
+
33
+ function exactKeys(value, keys) {
34
+ return value && typeof value === 'object' && !Array.isArray(value) &&
35
+ JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort());
36
+ }
37
+
38
+ function validateJob(job, expectedOrder) {
39
+ if (!job || job.order !== expectedOrder || !Number.isSafeInteger(job.order) ||
40
+ typeof job.provider !== 'string' || !job.provider || typeof job.target !== 'string' || !path.isAbsolute(job.target) ||
41
+ !Array.isArray(job.args) || job.args.some(value => typeof value !== 'string')) {
42
+ throw new Error(`Invalid provider preparation job at order ${expectedOrder}`);
43
+ }
44
+ }
45
+
46
+ function readPlan(response, expectedPath) {
47
+ if (!exactKeys(response.plan, ['path', 'size', 'sha256']) || response.plan.path !== expectedPath ||
48
+ !Number.isSafeInteger(response.plan.size) || response.plan.size < 2 || response.plan.size > PLAN_LIMIT ||
49
+ !/^[a-f0-9]{64}$/.test(response.plan.sha256)) throw new Error('Invalid provider preparation plan descriptor');
50
+ const stat = fs.lstatSync(expectedPath);
51
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== response.plan.size) throw new Error('Invalid provider preparation plan file');
52
+ const body = fs.readFileSync(expectedPath);
53
+ if (body.length !== response.plan.size || createHash('sha256').update(body).digest('hex') !== response.plan.sha256) {
54
+ throw new Error('Provider preparation plan digest mismatch');
55
+ }
56
+ return JSON.parse(body.toString('utf8'));
57
+ }
58
+
59
+ function prepareInChild({ job, attempt, sessionRoot, poolDescriptor, workerPath, workerEnv, activeChildren, timeoutMs }) {
60
+ const scratch = path.join(sessionRoot, `${job.order}-${attempt}-${randomUUID()}`);
61
+ fs.mkdirSync(scratch, { mode: 0o700 });
62
+ const capability = randomBytes(32).toString('hex');
63
+ const jobId = randomUUID();
64
+ const expectedPlan = path.join(scratch, 'plan.json');
65
+ return new Promise((resolve, reject) => {
66
+ const child = fork(workerPath, [], {
67
+ cwd: process.cwd(),
68
+ env: { ...process.env, ...workerEnv },
69
+ stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
70
+ serialization: 'json',
71
+ execArgv: CHECKOUT_NEEDS_TS_TRANSFORM ? ['--import', 'tsx'] : [],
72
+ });
73
+ activeChildren.add(child);
74
+ const stderr = [];
75
+ let stderrBytes = 0;
76
+ child.stderr?.on('data', chunk => {
77
+ const retained = chunk.subarray(0, Math.max(0, 1024 * 1024 - stderrBytes));
78
+ if (retained.length) { stderr.push(Buffer.from(retained)); stderrBytes += retained.length; }
79
+ });
80
+ let settled = false;
81
+ let outcome;
82
+ let killTimer;
83
+ let timedOut = false;
84
+ const finish = () => {
85
+ if (settled) return;
86
+ settled = true;
87
+ clearTimeout(timer);
88
+ clearTimeout(killTimer);
89
+ activeChildren.delete(child);
90
+ if (child.connected) { try { child.disconnect(); } catch {} }
91
+ if (outcome?.error) reject(outcome.error); else if (outcome) resolve(outcome.value);
92
+ else reject(new Error(`Provider preparation worker exited ${child.exitCode ?? 'without status'} for ${job.provider}${stderrBytes ? `: ${Buffer.concat(stderr).toString('utf8').trim()}` : ''}`));
93
+ };
94
+ const stop = error => {
95
+ if (settled) return;
96
+ outcome = { error };
97
+ try { signalChild(child, 'SIGTERM'); } catch {}
98
+ if (!killTimer) {
99
+ killTimer = setTimeout(() => { try { signalChild(child, 'SIGKILL'); } catch {} }, 2_000);
100
+ killTimer.unref?.();
101
+ }
102
+ };
103
+ const timer = setTimeout(() => {
104
+ timedOut = true;
105
+ stop(new Error(`Provider preparation timed out for ${job.provider}`));
106
+ }, timeoutMs);
107
+ timer.unref?.();
108
+ child.once('error', error => {
109
+ // 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.
111
+ if (!child.pid) { outcome = { error }; finish(); }
112
+ else stop(error);
113
+ });
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}`) };
120
+ }
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()}` : ''}`) };
122
+ finish();
123
+ });
124
+ child.once('message', response => {
125
+ try {
126
+ if (timedOut) throw new Error(`Provider preparation timed out for ${job.provider}`);
127
+ if (!exactKeys(response, response?.type === 'error'
128
+ ? ['version', 'type', 'capability', 'jobId', 'error']
129
+ : ['version', 'type', 'capability', 'jobId', 'plan']) ||
130
+ response.version !== 1 || response.capability !== capability || response.jobId !== jobId) {
131
+ throw new Error('Invalid provider preparation worker response');
132
+ }
133
+ if (response.type === 'error') {
134
+ if (!exactKeys(response.error, ['message']) || typeof response.error.message !== 'string') throw new Error('Invalid provider preparation worker error');
135
+ throw new Error(`${job.provider}: ${response.error.message}`);
136
+ }
137
+ if (response.type !== 'prepared') throw new Error('Invalid provider preparation worker response type');
138
+ outcome = { value: { detached: readPlan(response, expectedPlan), scratch, attempt } };
139
+ // Closing the IPC channel lets the single-request worker exit normally;
140
+ // the exit handler above confirms termination before exposing its plan.
141
+ if (child.connected) { try { child.disconnect(); } catch {} }
142
+ } catch (error) { stop(error); }
143
+ });
144
+ child.send({ version: 1, type: 'prepare', capability, jobId, order: job.order, attempt, provider: job.provider,
145
+ target: job.target, args: job.args, scratch, inputPool: poolDescriptor }, error => { if (error) stop(error); });
146
+ });
147
+ }
148
+
149
+ /** Prepare provider projections in at most two subprocesses. The returned
150
+ * session owns every stage and promotes them only through ordered commit(). */
151
+ export async function prepareProviderDeployments({
152
+ srcRoot,
153
+ jobs,
154
+ maxConcurrency = 2,
155
+ workerPath = DEFAULT_WORKER,
156
+ workerEnv = {},
157
+ timeoutMs = 120_000,
158
+ transaction = defaultTransaction,
159
+ onSessionRoot,
160
+ signal = signalChild,
161
+ }) {
162
+ if (typeof srcRoot !== 'string' || !path.isAbsolute(srcRoot) || !Array.isArray(jobs) || !jobs.length) throw new Error('Invalid provider preparation request');
163
+ jobs.forEach(validateJob);
164
+ const concurrency = Math.max(1, Math.min(2, Number.isSafeInteger(maxConcurrency) ? maxConcurrency : 2, jobs.length));
165
+ const sessionRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'aiwg-provider-prepare-'));
166
+ const removeSession = trackTemporaryDirectory(sessionRoot);
167
+ onSessionRoot?.(sessionRoot);
168
+ const activeChildren = new Set();
169
+ let pool;
170
+ let poolDescriptor;
171
+ try {
172
+ pool = transaction.createPool(srcRoot);
173
+ poolDescriptor = transaction.describePool(pool);
174
+ } catch (error) {
175
+ if (pool) transaction.disposePool(pool);
176
+ removeSession();
177
+ throw error;
178
+ }
179
+ const prepared = new Array(jobs.length);
180
+ let next = 0;
181
+ let closed = false;
182
+ let nextCommit = 0;
183
+ const stopChildren = async () => {
184
+ const children = [...activeChildren];
185
+ await Promise.all(children.map(child => new Promise(resolve => {
186
+ if (child.exitCode !== null || child.signalCode !== null) return resolve();
187
+ if (!Number.isSafeInteger(child.pid) || child.pid <= 0) return resolve();
188
+ const hardKill = setTimeout(() => { try { signal(child, 'SIGKILL'); } catch {} }, 2_000);
189
+ hardKill.unref?.();
190
+ child.once('exit', () => { clearTimeout(hardKill); resolve(); });
191
+ try { signal(child, 'SIGTERM'); } catch { /* escalation and exit confirmation remain required */ }
192
+ })));
193
+ };
194
+ const cleanup = async () => {
195
+ if (closed) return;
196
+ closed = true;
197
+ await stopChildren();
198
+ for (const item of prepared) if (item?.plan && !item.committed) transaction.disposePlan(item.plan);
199
+ transaction.disposePool(pool);
200
+ removeSession();
201
+ };
202
+ const prepareOne = async (index, attempt = 0) => {
203
+ const raw = await prepareInChild({ job: jobs[index], attempt, sessionRoot, poolDescriptor, workerPath, workerEnv, activeChildren, timeoutMs });
204
+ const plan = transaction.adopt(raw.detached, {
205
+ scratch: raw.scratch,
206
+ target: jobs[index].target,
207
+ inputPool: pool,
208
+ cleanup: () => fs.rmSync(raw.scratch, { recursive: true, force: true }),
209
+ });
210
+ return { ...raw, plan, committed: false };
211
+ };
212
+ try {
213
+ await Promise.all(Array.from({ length: concurrency }, async () => {
214
+ for (;;) {
215
+ const index = next++;
216
+ if (index >= jobs.length) return;
217
+ prepared[index] = await prepareOne(index);
218
+ }
219
+ }));
220
+ } catch (error) {
221
+ await cleanup();
222
+ throw error;
223
+ }
224
+ return {
225
+ get providers() { return jobs.map(job => job.provider); },
226
+ async commit(index) {
227
+ if (closed) throw new Error('Provider preparation session is closed');
228
+ if (index !== nextCommit) throw new Error(`Provider plans must commit in order: expected ${nextCommit}, received ${index}`);
229
+ let item = prepared[index];
230
+ try {
231
+ let report;
232
+ try { report = transaction.commit(item.plan); }
233
+ catch (error) {
234
+ if (!transaction.isStale(error) || item.attempt >= 1) throw error;
235
+ item = prepared[index] = await prepareOne(index, item.attempt + 1);
236
+ report = transaction.commit(item.plan);
237
+ }
238
+ item.committed = true;
239
+ nextCommit++;
240
+ return report;
241
+ } catch (error) {
242
+ throw error;
243
+ }
244
+ },
245
+ async dispose() { await cleanup(); },
246
+ };
247
+ }
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { main as deployProvider, parseArgs } from '../deploy-agents.mjs';
6
+ import { retainPreparationChannel } from './provider-prepare-channel.mjs';
7
+ import {
8
+ importRuleDeploymentInputPool,
9
+ disposeRuleDeploymentInputPool,
10
+ installRuleDeploymentPreparationHook,
11
+ } from './rule-delivery.mjs';
12
+
13
+ function exactKeys(value, keys) {
14
+ return value && typeof value === 'object' && !Array.isArray(value) &&
15
+ JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...keys].sort());
16
+ }
17
+
18
+ function validRequest(request) {
19
+ return exactKeys(request, ['version', 'type', 'capability', 'jobId', 'order', 'attempt', 'provider', 'target', 'args', 'scratch', 'inputPool']) &&
20
+ request.version === 1 && request.type === 'prepare' && /^[a-f0-9]{64}$/.test(request.capability) &&
21
+ typeof request.jobId === 'string' && Number.isSafeInteger(request.order) && request.order >= 0 &&
22
+ (request.attempt === 0 || request.attempt === 1) && typeof request.provider === 'string' && request.provider &&
23
+ typeof request.target === 'string' && path.isAbsolute(request.target) && typeof request.scratch === 'string' && path.isAbsolute(request.scratch) &&
24
+ Array.isArray(request.args) && request.args.every(value => typeof value === 'string');
25
+ }
26
+
27
+ function publish(message) {
28
+ return new Promise(resolve => {
29
+ if (!process.connected) return resolve();
30
+ process.send(message, () => resolve());
31
+ });
32
+ }
33
+
34
+ process.once('message', async request => {
35
+ retainPreparationChannel();
36
+ if (!validRequest(request)) {
37
+ await publish({ version: 1, type: 'error', capability: request?.capability || '', jobId: request?.jobId || '', error: { message: 'Invalid provider preparation request' } });
38
+ try { process.disconnect(); } catch {}
39
+ return;
40
+ }
41
+ let pool;
42
+ let clearHook = () => {};
43
+ let response;
44
+ try {
45
+ const stat = fs.lstatSync(request.scratch);
46
+ if (!stat.isDirectory() || stat.isSymbolicLink() || fs.readdirSync(request.scratch).length) throw new Error('Invalid provider preparation scratch directory');
47
+ const parsed = parseArgs(request.args);
48
+ if (parsed.provider !== request.provider || path.resolve(parsed.target) !== path.resolve(request.target)) {
49
+ throw new Error('Provider preparation request does not match its deployment arguments');
50
+ }
51
+ pool = importRuleDeploymentInputPool(request.inputPool);
52
+ let detached;
53
+ clearHook = installRuleDeploymentPreparationHook({
54
+ scratch: request.scratch,
55
+ inputPool: pool,
56
+ accept: plan => {
57
+ if (detached) throw new Error('Provider attempted more than one transactional preparation');
58
+ detached = plan;
59
+ },
60
+ });
61
+ await deployProvider(request.args);
62
+ if (!detached) throw new Error('Provider did not produce a transactional preparation plan');
63
+ if (detached.target !== path.resolve(request.target)) throw new Error('Provider preparation target mismatch');
64
+ const body = Buffer.from(JSON.stringify(detached));
65
+ const planPath = path.join(request.scratch, 'plan.json');
66
+ const fd = fs.openSync(planPath, 'wx', 0o600);
67
+ try { fs.writeFileSync(fd, body); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
68
+ response = {
69
+ version: 1,
70
+ type: 'prepared',
71
+ capability: request.capability,
72
+ jobId: request.jobId,
73
+ plan: { path: planPath, size: body.length, sha256: createHash('sha256').update(body).digest('hex') },
74
+ };
75
+ } catch (error) {
76
+ response = { version: 1, type: 'error', capability: request.capability, jobId: request.jobId,
77
+ error: { message: error instanceof Error ? error.message : String(error) } };
78
+ } finally {
79
+ clearHook();
80
+ if (pool) disposeRuleDeploymentInputPool(pool);
81
+ }
82
+ await publish(response);
83
+ try { process.disconnect(); } catch {}
84
+ });