@aiwg/cli 2026.10.3 → 2026.10.4

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 +57 -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 +285 -131
  21. package/dist/src/artifacts/index-files.js +132 -18
  22. package/dist/src/artifacts/index-preparation.js +65 -0
  23. package/dist/src/artifacts/index-refresh.js +576 -0
  24. package/dist/src/artifacts/index-worker.mjs +9 -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 +98 -37
  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 +82 -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 +51 -11
  207. package/dist/src/sessions/contracts.js +40 -2
  208. package/dist/src/sessions/coverage-preflight.js +1 -1
  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 +133 -25
  216. package/dist/src/sessions/readers.js +106 -53
  217. package/dist/src/sessions/refresh.js +106 -0
  218. package/dist/src/sessions/repository.js +147 -52
  219. package/dist/src/sessions/workspace-discovery.js +221 -56
  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
@@ -1,5 +1,6 @@
1
1
  /** Shared installed-worker qualification owner, also used by the Ralph compatibility facade. */
2
2
  import { spawn } from 'node:child_process';
3
+ import { constants } from 'node:fs';
3
4
  import { open, realpath, lstat, mkdir, rename, writeFile } from 'node:fs/promises';
4
5
  import path from 'node:path';
5
6
  import { homedir } from 'node:os';
@@ -8,6 +9,21 @@ import { createHash } from 'node:crypto';
8
9
  import { parse } from 'acorn';
9
10
  import { satisfies, valid, validRange } from 'semver';
10
11
 
12
+ // Recovery metadata belongs to the installed driver identity, never its shape.
13
+ const workerRecoveryCapabilities = new WeakMap();
14
+
15
+ /** Register capabilities for an installed or caller-supplied driver. Keep the
16
+ * original driver when passing it to readers; copies must be registered anew. */
17
+ export function registerWorkerRecoveryCapabilities(driver, capabilities) {
18
+ workerRecoveryCapabilities.set(driver, Object.freeze({ ...capabilities }));
19
+ return driver;
20
+ }
21
+
22
+ /** Gated readers use this single lookup; ordinary driver properties are ignored. */
23
+ export function getWorkerRecoveryCapabilities(driver) {
24
+ return workerRecoveryCapabilities.get(driver) ?? null;
25
+ }
26
+
11
27
  // Shipped negative-suite definition. Evidence producers hash this definition,
12
28
  // including case semantics and permission rows; arbitrary fixture labels cannot
13
29
  // qualify an installation. Passed observations still come from the trusted host.
@@ -84,7 +100,7 @@ function assertWorkerControlArgv(profile, argv) {
84
100
 
85
101
 
86
102
  async function assertWorkerInstallationEvidence(request, scopeDigest) {
87
- const { DEFAULT_WORKER_SELECTION } = await import('./provider-policy.js');
103
+ const { DEFAULT_WORKER_SELECTION, WORKER_OPERATOR_APPROVAL, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
88
104
  const { evidence, now } = request;
89
105
  if (!Number.isFinite(now) || !evidence || !/^sha256:[a-f0-9]{64}$/.test(scopeDigest) || evidence.scopeDigest !== scopeDigest
90
106
  || evidence.fixtureDigest !== await workerNegativeFixtureDigest()
@@ -95,17 +111,21 @@ async function assertWorkerInstallationEvidence(request, scopeDigest) {
95
111
  if (now - Date.parse(evidence.verifiedAt) >= (request.policy?.freshness?.policyDays ?? DEFAULT_WORKER_SELECTION.freshness.policyDays) * 86400000) {
96
112
  throw new Error('Installation evidence expired');
97
113
  }
114
+ // Decision 17's direct CLI baseline records operator approval, never claims
115
+ // that a version probe ran the negative suite. Stronger host evidence keeps
116
+ // its existing checks below.
117
+ if (evidence.evidenceRef === WORKER_OPERATOR_APPROVAL && isOperatorQualifiedWorkerAdapter(request.adapter)) return;
98
118
  const { cases: requiredCases } = await workerPermissionRequirements(request.adapter.provider, request.requiredControls);
99
119
  if (requiredCases.some(id => evidence.cases?.[id] !== 'passed')) {
100
120
  throw new Error('Installation negative evidence is incomplete');
101
121
  }
102
122
  }
103
123
 
104
- /** Read-only recheck: the version probe is bound to the unchanged binary digest. */
124
+ /** Read-only recheck of the approved installation bytes and bindings. */
105
125
  async function recheckWorkerInstallation(instance, options) {
106
126
  const request = structuredClone(options);
107
- const { requiredWorkerControls } = await import('./provider-policy.js');
108
- request.requiredControls = requiredWorkerControls(request);
127
+ const { requiredWorkerControls, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
128
+ if (!isOperatorQualifiedWorkerAdapter(request.adapter)) request.requiredControls = requiredWorkerControls(request);
109
129
  const scope = await inspectWorkerInstallation(instance, request);
110
130
  await assertWorkerInstallationEvidence(request, scope.scopeDigest);
111
131
  return { scope, request };
@@ -143,9 +163,9 @@ export async function qualifyWorkerInstallation(instance, options) {
143
163
  }
144
164
 
145
165
  export async function prepareInstalledWorkerInvocation(instance, invocation, qualification) {
146
- const { prepareWorkerInvocation } = await import('../serve/worker-invocation.js');
147
- const { requiredWorkerControls, WorkerPolicyError } = await import('./provider-policy.js');
148
- qualification = { ...qualification, requiredControls: requiredWorkerControls({
166
+ const { prepareWorkerInvocation, WorkerPathScopeError } = await import('../serve/worker-invocation.js');
167
+ const { requiredWorkerControls, WorkerPolicyError, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
168
+ if (!isOperatorQualifiedWorkerAdapter(qualification.adapter)) qualification = { ...qualification, requiredControls: requiredWorkerControls({
149
169
  ...qualification, effort: invocation.selected.effort.value }) };
150
170
  const result = await prepareWorkerInvocation(invocation, { ...qualification, adapterId: instance.getName() });
151
171
  const profile = qualification.adapter.invocationProfiles[result.selected.profileRef];
@@ -172,14 +192,16 @@ export async function prepareInstalledWorkerInvocation(instance, invocation, qua
172
192
  for (const key of ['briefPath', 'stdoutPath', 'stderrPath', 'finalPath']) {
173
193
  const target = await canonicalFuturePath(result[key]);
174
194
  const roots = key === 'briefPath' ? scope.inputRoots : scope.outputRoots;
175
- if (!roots.some(root => inside(root, target))) throw new WorkerPolicyError('OG_CONFIG_REFERENCE', 'Worker input/output path is outside the qualified scope');
195
+ if (!roots.some(root => inside(root, target))) throw new WorkerPathScopeError(key, result[key], roots);
176
196
  }
177
197
  if (result.transport === 'shell') {
198
+ const driverInput = isOperatorQualifiedWorkerAdapter(qualification.adapter)
199
+ ? { ...result, brief: (await readInstalledFile(result.briefPath)).toString('utf8') } : result;
178
200
  const expectedArtifacts = typeof instance.buildWorkerArtifacts === 'function'
179
- ? instance.buildWorkerArtifacts({ ...result, permissions: qualification.permissions }) : undefined;
201
+ ? instance.buildWorkerArtifacts({ ...driverInput, permissions: qualification.permissions }) : undefined;
180
202
  if (JSON.stringify(expectedArtifacts) !== JSON.stringify(result.artifacts)) throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Worker artifacts differ from authorized driver mapping');
181
- const workerArgs = instance.buildWorkerArgs(result, profile, qualification.permissions);
182
- assertWorkerControlArgv(profile, workerArgs);
203
+ const workerArgs = instance.buildWorkerArgs(driverInput, profile, qualification.permissions);
204
+ if (!isOperatorQualifiedWorkerAdapter(qualification.adapter)) assertWorkerControlArgv(profile, workerArgs);
183
205
  const expected = [scope.binary.realpath, ...workerArgs];
184
206
  if (JSON.stringify(expected) !== JSON.stringify(result.argv)
185
207
  || result.stdin !== qualification.adapter.invocation.stdin) {
@@ -215,6 +237,7 @@ async function binaryIdentity(filename, workerWritableRoots) {
215
237
  const stat = await handle.stat();
216
238
  if (!stat.isFile() || !(stat.mode & 0o111) || (stat.mode & 0o022)
217
239
  || (stat.nlink > 1 && (stat.mode & 0o222))) throw new Error('Untrusted binary permissions');
240
+ if (stat.size > 1024 * 1024 * 1024) throw new Error('Binary exceeds qualification byte bound (1 GiB)');
218
241
  const hash = createHash('sha256');
219
242
  // Incremental, bounded reads: never allocate the size reported by the binary.
220
243
  const buffer = Buffer.alloc(64 * 1024);
@@ -223,7 +246,7 @@ async function binaryIdentity(filename, workerWritableRoots) {
223
246
  const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
224
247
  if (!bytesRead) break;
225
248
  total += bytesRead;
226
- if (total > 256 * 1024 * 1024) throw new Error('Binary exceeds qualification byte bound');
249
+ if (total > 1024 * 1024 * 1024) throw new Error('Binary exceeds qualification byte bound');
227
250
  hash.update(buffer.subarray(0, bytesRead));
228
251
  }
229
252
  return { realpath: canonical, sha256: `sha256:${hash.digest('hex')}` };
@@ -245,11 +268,12 @@ async function canonicalFuturePath(filename) {
245
268
  }
246
269
 
247
270
  export async function inspectWorkerInstallation(instance, request) {
248
- const { inspectWorkerInvocationProfile, requiredWorkerControls } = await import('./provider-policy.js');
271
+ const { inspectWorkerInvocationProfile, requiredWorkerControls, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
249
272
  const { qualificationDigest } = await import('./qualification-digest.mjs');
250
273
  const { adapter, profile: profileId, cliVersion, workerWritableRoots,
251
274
  effectiveConfigDigest, accountScope, boundary, cwd } = request;
252
- const requiredControls = requiredWorkerControls(request);
275
+ const operatorApproved = isOperatorQualifiedWorkerAdapter(adapter);
276
+ const requiredControls = operatorApproved ? request.requiredControls ?? [] : requiredWorkerControls(request);
253
277
  const platform = request.platform ?? process.platform;
254
278
  if (platform !== 'linux' || adapter.id !== instance.getName()) throw new Error('Unsupported installation identity/platform');
255
279
  if (!Array.isArray(requiredControls) || !Array.isArray(workerWritableRoots)
@@ -269,11 +293,11 @@ export async function inspectWorkerInstallation(instance, request) {
269
293
  throw new Error('External control lacks an enforcing boundary');
270
294
  }
271
295
  const permissionRequirements = await workerPermissionRequirements(adapter.provider, requiredControls);
272
- if (permissionRequirements.externalBoundary && boundary.kind === 'native') {
296
+ if (!operatorApproved && permissionRequirements.externalBoundary && boundary.kind === 'native') {
273
297
  throw new Error('Profile requires an external enforcing boundary');
274
298
  }
275
299
  let controlArgv = null;
276
- if (Object.values(profile.controls).some(hasControlArgvMapping)) {
300
+ if (!operatorApproved && Object.values(profile.controls).some(hasControlArgvMapping)) {
277
301
  if (!request.invocation) throw new Error('Worker control qualification requires representative argv invocation');
278
302
  controlArgv = instance.buildWorkerArgs(structuredClone(request.invocation), profile, request.permissions);
279
303
  assertWorkerControlArgv(profile, controlArgv);
@@ -425,7 +449,7 @@ export function boundedVersionProbe(executable, argv, environment, cwd, timeoutM
425
449
  let size = 0;
426
450
  let failed = false;
427
451
  const stop = () => {
428
- if (child.pid) { try { process.kill(-child.pid, 'SIGKILL'); } catch { /* already stopped */ } }
452
+ if (Number.isSafeInteger(child.pid) && child.pid > 0) { try { process.kill(-child.pid, 'SIGKILL'); } catch { /* already stopped */ } }
429
453
  };
430
454
  // Never rely on close: an escaped descendant may retain the pipe descriptors.
431
455
  const abort = () => {
@@ -564,7 +588,8 @@ async function inspectInstalledModule(entry, workerWritableRoots, platform) {
564
588
  }
565
589
  for (const target of [root, path.join(root, 'package.json'), ...entry.modulePath.split('/').map((_, i, parts) => path.join(root, ...parts.slice(0, i + 1)))]) {
566
590
  const stat = await lstat(target);
567
- if (stat.isSymbolicLink() || (stat.mode & 0o022) !== 0) throw new Error('Symlink or untrusted writable installed module path');
591
+ if (stat.isSymbolicLink()) throw new Error(`Symlink in installed module path: ${target}`);
592
+ if ((stat.mode & 0o022) !== 0) throw new Error(`Installed module path is group/other-writable: ${target}. Run: chmod -R go-w ${JSON.stringify(root)}`);
568
593
  // Directory link counts reflect children, not file aliases. Hardlinked store
569
594
  // files qualify only when no permission class can write the shared inode.
570
595
  if (stat.isFile() && stat.nlink > 1 && (stat.mode & 0o222) !== 0) {
@@ -722,7 +747,29 @@ function installationStorePath() {
722
747
  return path.join(root, 'worker-installations.json');
723
748
  }
724
749
 
725
- async function assertTrustedStore(filename, writableRoots) {
750
+ // HOME is the operator's trust boundary; custom stores outside it are checked
751
+ // back to /. O_NOFOLLOW on the store itself does not protect its ancestors.
752
+ async function assertTrustedStoreAncestors(directory) {
753
+ const home = path.resolve(homedir());
754
+ const boundary = inside(home, directory) ? home : path.parse(directory).root;
755
+ // The store directory itself is checked by the caller.
756
+ if (directory === boundary) return;
757
+ const ancestors = [];
758
+ for (let target = path.dirname(directory); ; target = path.dirname(target)) {
759
+ ancestors.push(target);
760
+ if (target === boundary) break;
761
+ }
762
+ for (const target of ancestors.reverse()) {
763
+ const stat = await lstat(target);
764
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.mode & 0o022
765
+ || process.getuid && stat.uid !== 0 && stat.uid !== process.getuid()) {
766
+ throw new Error(`Untrusted installation store ancestor: ${target}`);
767
+ }
768
+ }
769
+ }
770
+
771
+ async function assertTrustedStore(filename, writableRoots, repairDirectory = false, apply = false) {
772
+ const changes = [];
726
773
  const canonical = await canonicalFuturePath(filename);
727
774
  if (canonical !== filename) throw new Error('Symlinked installation store');
728
775
  for (const root of writableRoots) {
@@ -731,10 +778,35 @@ async function assertTrustedStore(filename, writableRoots) {
731
778
  for (const target of [path.dirname(filename), filename]) {
732
779
  try {
733
780
  const stat = await lstat(target);
734
- if (stat.isSymbolicLink() || (stat.mode & 0o022) || stat.isFile() && stat.nlink !== 1
735
- || process.getuid && stat.uid !== process.getuid()) throw new Error('Untrusted installation store permissions');
781
+ if (process.getuid && stat.uid !== process.getuid()) throw new Error(`Installation store path is owned by another user: ${target}`);
782
+ if (stat.isSymbolicLink() || stat.isFile() && stat.nlink !== 1
783
+ || target === path.dirname(filename) && !stat.isDirectory()
784
+ || target === filename && !stat.isFile()) throw new Error(`Untrusted installation store permissions: ${target}`);
785
+ if (stat.mode & 0o022) {
786
+ if (!repairDirectory || target !== path.dirname(filename) || !process.getuid) {
787
+ throw new Error(`Untrusted installation store permissions: ${target}`);
788
+ }
789
+ await assertTrustedStoreAncestors(target);
790
+ changes.push({ path: target, action: 'chmod go-w' });
791
+ if (apply) {
792
+ const handle = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_DIRECTORY);
793
+ try {
794
+ const current = await handle.stat();
795
+ if (current.uid !== process.getuid() || current.ino !== stat.ino || current.dev !== stat.dev) {
796
+ throw new Error(`Installation store directory changed: ${target}`);
797
+ }
798
+ await handle.chmod(current.mode & ~0o022);
799
+ } finally { await handle.close(); }
800
+ }
801
+ }
736
802
  } catch (error) { if (error.code !== 'ENOENT') throw error; }
737
803
  }
804
+ return changes;
805
+ }
806
+
807
+ /** Qualification's confirmed write may secure its own directory; ordinary readers never repair. */
808
+ export async function prepareWorkerInstallationStore({ workerWritableRoots, apply = false }) {
809
+ return assertTrustedStore(installationStorePath(), workerWritableRoots, true, apply);
738
810
  }
739
811
 
740
812
  async function readInstallationStore(filename, expectedDigest) {
@@ -775,7 +847,7 @@ export async function recordWorkerInstallation(instance, options) {
775
847
  delete installation.environment;
776
848
  delete installation.adapter;
777
849
  delete installation.now;
778
- const record = { cli, binding: { ...structuredClone(binding), boundary: installation.boundary }, adapter,
850
+ const record = { cli, ...(options.modelObservations ? { modelObservations: structuredClone(options.modelObservations) } : {}), binding: { ...structuredClone(binding), boundary: installation.boundary }, adapter,
779
851
  installation: { ...installation, environmentNames, versionProbe: { rawBanner: result.rawVersionBanner, version: result.cliVersion } },
780
852
  ...(options.mcpSchemas ? { mcpSchemas: structuredClone(options.mcpSchemas) } : {}),
781
853
  ...(options.mcpProfile ? { mcpProfile: structuredClone(options.mcpProfile) } : {}) };
@@ -789,9 +861,9 @@ export async function recordWorkerInstallation(instance, options) {
789
861
  return result;
790
862
  }
791
863
 
792
- /** Read recorded host authority, validate installation bytes, then load the same
793
- * approved factory used by Ralph. Resolution never launches a version probe. */
794
- export async function loadRecordedWorkerInstallations({ registry, cwd, policy, storePath, storeDigest }) {
864
+ /** Read recorded host authority, validate installation bytes and the current CLI
865
+ * version, then load the same approved factory used by Ralph. */
866
+ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, parallelism, storePath, storeDigest }) {
795
867
  const { WorkerPolicyError, parseProviderModelCapabilities } = await import('./provider-policy.js');
796
868
  const { qualificationDigest } = await import('./qualification-digest.mjs');
797
869
  const fail = () => new WorkerPolicyError('OG_ADAPTER_UNQUALIFIED', 'Trusted installation qualification is required');
@@ -805,6 +877,7 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, s
805
877
  catch { throw fail(); }
806
878
  const installations = {};
807
879
  const drivers = {};
880
+ const observations = {};
808
881
  const resolvedRegistry = structuredClone(registry);
809
882
  const counts = new Map();
810
883
  for (const record of stored.registrations) counts.set(record?.cli, (counts.get(record?.cli) ?? 0) + 1);
@@ -825,8 +898,8 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, s
825
898
  const proof = adapter.invocationProfiles?.[binding.profile]?.installationQualification;
826
899
  if (!proof || ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef'].some(key => proof[key] !== installation.evidence?.[key])) throw fail();
827
900
  // Reject expired/unbound negative observations before any factory import.
828
- const { requiredWorkerControls } = await import('./provider-policy.js');
829
- installation.requiredControls = requiredWorkerControls(installation);
901
+ const { requiredWorkerControls, isOperatorQualifiedWorkerAdapter } = await import('./provider-policy.js');
902
+ if (!isOperatorQualifiedWorkerAdapter(adapter)) installation.requiredControls = requiredWorkerControls(installation);
830
903
  await assertWorkerInstallationEvidence(installation, proof.scopeDigest);
831
904
  await assertTrustedStore(filename, installation.workerWritableRoots);
832
905
  const candidateRegistry = structuredClone(resolvedRegistry);
@@ -842,7 +915,31 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, s
842
915
  workerWritableRoots: installation.workerWritableRoots, platform: process.platform });
843
916
  const instance = createProvider(adapter.id);
844
917
  const { scope } = await recheckWorkerInstallation(instance, installation);
918
+ if (parallelism && isOperatorQualifiedWorkerAdapter(adapter)) {
919
+ const { resolveWorkerGroupInstallation } = await import('../config/aiwg-config.js');
920
+ const configured = resolveWorkerGroupInstallation(parallelism, cli, candidateRegistry);
921
+ let executable = configured.executable;
922
+ if (!path.isAbsolute(executable)) {
923
+ if (executable.includes(path.sep)) executable = path.resolve(cwd, executable);
924
+ else {
925
+ const candidates = (process.env.PATH ?? '').split(path.delimiter).filter(Boolean).map(directory => path.resolve(directory, executable));
926
+ executable = null;
927
+ for (const candidate of candidates) {
928
+ try { await binaryIdentity(candidate, installation.workerWritableRoots); executable = candidate; break; } catch { /* Next PATH entry. */ }
929
+ }
930
+ }
931
+ }
932
+ if (!executable || await realpath(executable) !== scope.binary.realpath) throw fail();
933
+ }
845
934
  if (scope.scopeDigest !== proof.scopeDigest) throw fail();
935
+ // A launcher can keep the same bytes while its CLI payload is updated.
936
+ // Reuse the bounded, credential-free doctor probe before granting authority.
937
+ const { version } = await boundedVersionProbe(scope.binary.realpath,
938
+ adapter.invocation.versionArgv, workerVersionProbeEnvironment(), cwd,
939
+ workerVersionProbeDefaults.timeoutMs, workerVersionProbeDefaults.maxBytes,
940
+ adapter.invocation.versionPattern);
941
+ if (version !== binding.cliVersion
942
+ || (await inspectWorkerInstallation(instance, installation)).scopeDigest !== scope.scopeDigest) throw fail();
846
943
  const profile = adapter.invocationProfiles[binding.profile];
847
944
  let mcpRegistry;
848
945
  if (profile.transport === 'mcp') {
@@ -870,10 +967,20 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, s
870
967
  prepareWorkerInvocation: (invocation, qualification) => prepareInstalledWorkerInvocation(instance, invocation, qualification),
871
968
  ...(mcpRegistry ? { mcpRegistry, mcpProfile: record.mcpProfile } : {}),
872
969
  };
970
+ registerWorkerRecoveryCapabilities(drivers[cli], {
971
+ ...(typeof instance.parseWorkerAttemptFinal === 'function'
972
+ ? { parseWorkerAttemptFinal: (output, options) => instance.parseWorkerAttemptFinal(output, options) } : {}),
973
+ // Adapters with neither an explicit signal nor an attempt parser cannot
974
+ // verify success (e.g. OpenHuman); never admit an unreachable canary.
975
+ canVerifyAttempt: typeof instance.supportsAttemptVerification === 'function'
976
+ ? !!instance.supportsAttemptVerification()
977
+ : typeof instance.parseWorkerAttemptFinal === 'function',
978
+ });
873
979
  installations[cli] = structuredClone(binding);
980
+ if (record.modelObservations) observations[cli] = structuredClone(record.modelObservations);
874
981
  Object.assign(resolvedRegistry, candidateRegistry);
875
982
  } catch { /* One missing/changed installation cannot grant authority or hide qualified peers. */ }
876
983
  }
877
984
  if (!Object.keys(installations).length) throw fail();
878
- return { registry: resolvedRegistry, installations, drivers };
985
+ return { registry: resolvedRegistry, installations, drivers, observations };
879
986
  }
@@ -0,0 +1,84 @@
1
+ import type {
2
+ ExecFileSyncOptions,
3
+ ExecFileSyncOptionsWithBufferEncoding,
4
+ ExecFileSyncOptionsWithStringEncoding,
5
+ ExecSyncOptions,
6
+ ExecSyncOptionsWithBufferEncoding,
7
+ ExecSyncOptionsWithStringEncoding,
8
+ SpawnSyncOptions,
9
+ SpawnSyncOptionsWithBufferEncoding,
10
+ SpawnSyncOptionsWithStringEncoding,
11
+ SpawnSyncReturns,
12
+ } from 'node:child_process';
13
+
14
+ export type SyncProcessTier = 'quick' | 'network' | 'long';
15
+
16
+ export const AIWG_SYNC_LONG_TIMEOUT_MS_ENV: 'AIWG_SYNC_LONG_TIMEOUT_MS';
17
+
18
+ export const SYNC_PROCESS_TIER_BOUNDS_MS: Readonly<{
19
+ quick: number;
20
+ network: number;
21
+ long: number;
22
+ }>;
23
+
24
+ export function resolveSyncProcessTimeoutMs(tier: SyncProcessTier): number;
25
+
26
+ export function positiveTimeoutMs(value: unknown, fallbackMs: number): number;
27
+
28
+ // Overloads mirror Node's own execSync/execFileSync/spawnSync overload set
29
+ // (string vs Buffer return type keyed on `options.encoding`), with the
30
+ // required `tier` inserted right before `options` — same narrowing behavior
31
+ // callers get from the native APIs, e.g. `.trim()` on the result when
32
+ // `encoding: 'utf8'` is set.
33
+
34
+ export function boundedExecSync(command: string, tier: SyncProcessTier): Buffer;
35
+ export function boundedExecSync(command: string, tier: SyncProcessTier, options: ExecSyncOptionsWithStringEncoding): string;
36
+ export function boundedExecSync(command: string, tier: SyncProcessTier, options: ExecSyncOptionsWithBufferEncoding): Buffer;
37
+ export function boundedExecSync(command: string, tier: SyncProcessTier, options?: ExecSyncOptions): string | Buffer;
38
+
39
+ export function boundedExecFileSync(file: string, args: readonly string[], tier: SyncProcessTier): Buffer;
40
+ export function boundedExecFileSync(
41
+ file: string,
42
+ args: readonly string[],
43
+ tier: SyncProcessTier,
44
+ options: ExecFileSyncOptionsWithStringEncoding,
45
+ ): string;
46
+ export function boundedExecFileSync(
47
+ file: string,
48
+ args: readonly string[],
49
+ tier: SyncProcessTier,
50
+ options: ExecFileSyncOptionsWithBufferEncoding,
51
+ ): Buffer;
52
+ export function boundedExecFileSync(
53
+ file: string,
54
+ args: readonly string[],
55
+ tier: SyncProcessTier,
56
+ options?: ExecFileSyncOptions,
57
+ ): string | Buffer;
58
+
59
+ export function boundedSpawnSync(command: string, args: readonly string[], tier: SyncProcessTier): SpawnSyncReturns<Buffer>;
60
+ export function boundedSpawnSync(
61
+ command: string,
62
+ args: readonly string[],
63
+ tier: SyncProcessTier,
64
+ options: SpawnSyncOptionsWithStringEncoding,
65
+ ): SpawnSyncReturns<string>;
66
+ export function boundedSpawnSync(
67
+ command: string,
68
+ args: readonly string[],
69
+ tier: SyncProcessTier,
70
+ options: SpawnSyncOptionsWithBufferEncoding,
71
+ ): SpawnSyncReturns<Buffer>;
72
+ export function boundedSpawnSync(
73
+ command: string,
74
+ args: readonly string[],
75
+ tier: SyncProcessTier,
76
+ options?: SpawnSyncOptions,
77
+ ): SpawnSyncReturns<string | Buffer>;
78
+
79
+ /** Async launch with direct-child timeout and cleanup before termination. */
80
+ export function boundedSpawn(
81
+ command: string, args: readonly string[], tier: SyncProcessTier,
82
+ options: import('node:child_process').SpawnOptions,
83
+ onTermination?: () => void,
84
+ ): Promise<{ status: number | null; signal: NodeJS.Signals | null; error?: Error; pid?: number }>;
@@ -70,10 +70,12 @@
70
70
  */
71
71
 
72
72
  import {
73
+ spawn,
73
74
  execSync as nodeExecSync,
74
75
  execFileSync as nodeExecFileSync,
75
76
  spawnSync as nodeSpawnSync,
76
77
  } from 'node:child_process';
78
+ import { signalChild } from './signal-child.mjs';
77
79
 
78
80
  /** Env var that raises or disables the `long` tier bound. `0` disables it (no timeout). */
79
81
  export const AIWG_SYNC_LONG_TIMEOUT_MS_ENV = 'AIWG_SYNC_LONG_TIMEOUT_MS';
@@ -385,3 +387,44 @@ export function boundedSpawnSync(command, args, tier, options) {
385
387
  enrichTimeoutError(result.error, { description: describeCommand(command, args), tier, boundMs, signal: result.signal });
386
388
  return result;
387
389
  }
390
+
391
+ /** Async counterpart for launches that must clean owned files on termination.
392
+ * Like the sync wrappers, this supervises the direct child only. */
393
+ export async function boundedSpawn(command, args, tier, options, onTermination = () => {}) {
394
+ assertValidTier(tier);
395
+ const boundMs = effectiveTimeoutMs(tier, options);
396
+ const { timeout: _timeout, killSignal: _killSignal, ...nodeOptions } = toNodeOptions(options, boundMs);
397
+ let child;
398
+ let timer;
399
+ let timedOut = false;
400
+ const handlers = new Map();
401
+ try {
402
+ child = spawn(command, args, nodeOptions);
403
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
404
+ const handler = () => {
405
+ try { onTermination(); }
406
+ finally {
407
+ signalChild(child, 'SIGKILL');
408
+ // Restore the signal's normal exit semantics after cleanup.
409
+ process.removeListener(signal, handler);
410
+ process.kill(process.pid, signal);
411
+ }
412
+ };
413
+ handlers.set(signal, handler);
414
+ process.once(signal, handler);
415
+ }
416
+ if (boundMs > 0) timer = setTimeout(() => { timedOut = true; signalChild(child, 'SIGKILL'); }, boundMs);
417
+ return await new Promise(resolve => {
418
+ let spawnError;
419
+ child.once('error', error => { spawnError = error; });
420
+ child.once('close', (status, signal) => {
421
+ const error = timedOut ? Object.assign(new Error(`Command timed out after ${boundMs}ms`), { code: 'ETIMEDOUT' }) : spawnError;
422
+ enrichTimeoutError(error, { description: describeCommand(command, args), tier, boundMs, signal });
423
+ resolve({ status, signal, error, pid: child.pid, stdout: null, stderr: null, output: [] });
424
+ });
425
+ });
426
+ } finally {
427
+ clearTimeout(timer);
428
+ for (const [signal, handler] of handlers) process.removeListener(signal, handler);
429
+ }
430
+ }
@@ -0,0 +1,16 @@
1
+ export function resolveExecutable(
2
+ command: string,
3
+ platform?: NodeJS.Platform,
4
+ searchPath?: string,
5
+ pathExt?: string,
6
+ cwd?: string,
7
+ ): string;
8
+
9
+ export function cliCommand(
10
+ command: string,
11
+ args: string[],
12
+ platform?: NodeJS.Platform,
13
+ searchPath?: string,
14
+ pathExt?: string,
15
+ cwd?: string,
16
+ ): [string, string[]];
@@ -0,0 +1,6 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+
3
+ export function signalChild(
4
+ child: Pick<ChildProcess, 'pid' | 'kill'> | null | undefined,
5
+ signal?: NodeJS.Signals | number,
6
+ ): boolean;
@@ -0,0 +1,7 @@
1
+ /** Signal only a child that acquired a real PID. Failed spawns can retain a
2
+ * native handle with PID zero until their asynchronous error event arrives.
3
+ * Keep native return/throw behavior so callers retain their stop semantics. */
4
+ export function signalChild(child, signal) {
5
+ if (!Number.isSafeInteger(child?.pid) || child.pid <= 0) return false;
6
+ return !!child.kill(signal);
7
+ }
@@ -0,0 +1,20 @@
1
+ import path from 'node:path';
2
+ import { getProviderArtifactPathStrings, getProviderKernelSkillPath } from './provider-definitions.js';
3
+ import { resolveGrokbotSkillsDir } from './grokbot-paths.js';
4
+ /** All shared-copier tiers, including adapter mirrors. */
5
+ export function deployedSkillRoots(target, provider) {
6
+ const roots = [(getProviderArtifactPathStrings(provider) ?? getProviderArtifactPathStrings('claude'))?.skills,
7
+ getProviderKernelSkillPath(provider) || getProviderKernelSkillPath('claude')];
8
+ if (provider === 'muse')
9
+ roots.push('.agents/.aiwg/skills');
10
+ if (provider === 'grok-build')
11
+ roots.push('.grok/.aiwg/skills');
12
+ if (provider === 'grokbot') {
13
+ const root = resolveGrokbotSkillsDir();
14
+ if (root)
15
+ roots.push(root, path.join(root, '.aiwg', 'skills'));
16
+ }
17
+ return [...new Set(roots.filter((root) => Boolean(root))
18
+ .map(root => path.isAbsolute(root) ? root : path.join(target, root)))];
19
+ }
20
+ //# sourceMappingURL=deployed-skill-roots.js.map
@@ -1,4 +1,5 @@
1
1
  /** Grok Build ACP stdio client. Separate from one-shot headless execution. */
2
+ import { signalChild } from '../process/signal-child.mjs';
2
3
  import { spawn } from 'node:child_process';
3
4
  import { redactGrokOutput } from './grok-build-headless.js';
4
5
  export class GrokAcpClient {
@@ -182,9 +183,9 @@ export class GrokAcpClient {
182
183
  this.terminated = true;
183
184
  this.failAll(new Error('Grok ACP connection closed'));
184
185
  this.child.stdin.end();
185
- if (!this.child.kill('SIGTERM'))
186
+ if (!signalChild(this.child, 'SIGTERM'))
186
187
  return;
187
- const forceTimer = setTimeout(() => this.child.kill('SIGKILL'), 2_000);
188
+ const forceTimer = setTimeout(() => signalChild(this.child, 'SIGKILL'), 2_000);
188
189
  forceTimer.unref();
189
190
  this.child.once('close', () => clearTimeout(forceTimer));
190
191
  }
@@ -1,8 +1,13 @@
1
1
  /** Bounded Grok Build headless transport. The caller owns task authorization. */
2
+ import { signalChild } from '../process/signal-child.mjs';
2
3
  import { spawn } from 'node:child_process';
3
4
  const MAX_TIMEOUT_MS = 10 * 60_000;
4
5
  const DEFAULT_TIMEOUT_MS = 2 * 60_000;
5
6
  const DEFAULT_MAX_OUTPUT = 8 * 1024 * 1024;
7
+ // grok 1.0.50 --output-format streaming-json: ACP session updates.
8
+ // See docs/providers/grok-build-cli-evidence.md; json is a separate result object.
9
+ const STREAM_EVENT_TYPES = new Set(['text', 'thought', 'tool_call', 'tool_call_update',
10
+ 'usage', 'plan', 'available_commands', 'end', 'error']);
6
11
  export function redactGrokOutput(value, env = process.env) {
7
12
  let redacted = value
8
13
  .replace(/\b(Bearer\s+)[A-Za-z0-9._~+/-]+/gi, '$1[REDACTED]')
@@ -28,14 +33,14 @@ function isError(event) {
28
33
  || (['tool_call', 'tool_call_update'].includes(String(event.type)) && event.status === 'failed');
29
34
  }
30
35
  function stopProcess(child) {
31
- if (child.pid && process.platform !== 'win32') {
36
+ if (Number.isSafeInteger(child.pid) && child.pid > 0 && process.platform !== 'win32') {
32
37
  try {
33
38
  process.kill(-child.pid, 'SIGTERM');
34
39
  return;
35
40
  }
36
41
  catch { /* process already exited */ }
37
42
  }
38
- child.kill('SIGTERM');
43
+ signalChild(child, 'SIGTERM');
39
44
  }
40
45
  export async function runGrokHeadless(options) {
41
46
  if (!options.prompt.trim())
@@ -90,7 +95,9 @@ export async function runGrokHeadless(options) {
90
95
  if (format === 'json') {
91
96
  try {
92
97
  const parsed = JSON.parse(cleanStdout);
93
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || isError(parsed) || parsed.stopReason !== 'end_turn')
98
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)
99
+ || typeof parsed.text !== 'string' || isError(parsed)
100
+ || parsed.stopReason !== 'end_turn')
94
101
  throw new Error('Grok Build returned a JSON error result');
95
102
  events.push(parsed);
96
103
  }
@@ -110,7 +117,8 @@ export async function runGrokHeadless(options) {
110
117
  const acceptEvent = (event) => {
111
118
  if (settled)
112
119
  return;
113
- if (!event || typeof event !== 'object' || Array.isArray(event)) {
120
+ if (!event || typeof event !== 'object' || Array.isArray(event)
121
+ || typeof event.type !== 'string' || !STREAM_EVENT_TYPES.has(event.type)) {
114
122
  finish(new Error('Invalid Grok Build streaming-json event'));
115
123
  return;
116
124
  }
@@ -177,6 +177,7 @@ export function runGrokInspect(envOrOptions = process.env) {
177
177
  // npm exposes Grok through a .cmd shim on Windows; Node requires a shell
178
178
  // for .cmd/.bat launchers. Native .exe installs stay shell-free.
179
179
  shell: process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(binary),
180
+ killSignal: 'SIGKILL',
180
181
  });
181
182
  if (result.error && result.error.code === 'ENOENT') {
182
183
  return { status: 'absent', remediation: ABSENT_REMEDIATION };
@@ -0,0 +1,8 @@
1
+ export interface OmpAgentOptions {
2
+ diagnostics?: string[];
3
+ quiet?: boolean;
4
+ modelsConfig?: Record<string, any>;
5
+ modelConfig?: Record<string, any>;
6
+ }
7
+ export function transformAgent(src: string, content: string, opts?: OmpAgentOptions): string;
8
+ export function mapModel(model: string | undefined, config?: Record<string, any>): string | undefined;
@@ -0,0 +1 @@
1
+ export function diagnoseOmpRuntime(options?: {cwd?: string; env?: NodeJS.ProcessEnv; binary?: string; runner?: (...args: any[]) => Promise<{stdout: string}>}): Promise<{executable: string; available: boolean; version: string | null; paths: ReturnType<typeof import('./omp-paths.mjs').resolveOmpPaths>; reason: string | null}>;
@@ -0,0 +1,13 @@
1
+ export interface OmpPathOptions {
2
+ env?: NodeJS.ProcessEnv;
3
+ home?: string;
4
+ cwd?: string;
5
+ platform?: string;
6
+ exists?: (path: string) => boolean;
7
+ }
8
+ export function normalizeOmpProfile(value?: string): string | undefined;
9
+ export function resolveOmpPaths(options?: OmpPathOptions): {
10
+ profile?: string; configRoot: string; agentDir: string; projectDir: string;
11
+ resourceDirs: { agents: string; skills: string; commands: string; rules: string; behaviors: string };
12
+ dataDir: string; stateDir: string; cacheDir: string; sessionsDir: string;
13
+ };
@@ -0,0 +1,9 @@
1
+ export interface SkillReceipt {
2
+ skillSource?: string;
3
+ managed?: Record<string, { hash: string; frameworkSlug?: string; source?: string; version?: string }>;
4
+ }
5
+ export function extractFrameworkSlug(source: string): string | null;
6
+ export function sameSkillOwner(receipt: SkillReceipt | null, skillSource: string, frameworkSlug?: string | null): boolean;
7
+ export function canDeploySkill(receipt: SkillReceipt | null, skillSource: string, opts?: { force?: boolean }): boolean;
8
+ export function pruneRecordedSkillFiles(skillDir: string, managed: SkillReceipt['managed'], desired: Set<string>, opts?: { dryRun?: boolean }): void;
9
+ export function releaseSkillOwnership(skillDir: string, owner: { frameworkSlug?: string; skillSource?: string; sourceRoot?: string }, opts?: { dryRun?: boolean }): boolean | null;