@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
@@ -0,0 +1,541 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, readdirSync, statfsSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import os from 'node:os';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
7
+
8
+ // Installed packages ship dist/src, not src; resolve the runtime like workspace-status.mjs.
9
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
10
+ const runtimeRoot = existsSync(join(packageRoot, 'src')) ? 'src' : 'dist/src';
11
+ const [
12
+ { DEFAULT_DISPATCH_HOST_THRESHOLDS: DEFAULT_DISPATCH_THRESHOLDS, resolveDispatchHostThresholds },
13
+ { resolveProjectAiwgDir },
14
+ ] = await Promise.all([
15
+ import(pathToFileURL(join(packageRoot, runtimeRoot, 'config/dispatch-thresholds-runtime.mjs')).href),
16
+ import(pathToFileURL(join(packageRoot, runtimeRoot, 'config/project-artifacts-runtime.mjs')).href),
17
+ ]);
18
+
19
+ const BYTES_PER_GB = 1024 ** 3;
20
+
21
+ const DEFAULT_AUTO_REQUIREMENTS = {
22
+ min_memory_gb: 8,
23
+ min_free_disk_gb: 5,
24
+ min_cpus: 2,
25
+ min_swap_gb: 0,
26
+ };
27
+
28
+ const REQUIREMENT_LABELS = {
29
+ min_memory_gb: 'memory',
30
+ min_free_disk_gb: 'free disk',
31
+ min_cpus: 'CPU cores',
32
+ min_swap_gb: 'swap',
33
+ };
34
+
35
+ const HOST_KEYS = {
36
+ min_memory_gb: 'memory_gb',
37
+ min_free_disk_gb: 'free_disk_gb',
38
+ min_cpus: 'cpus',
39
+ min_swap_gb: 'swap_gb',
40
+ };
41
+
42
+ function parseArgs(argv) {
43
+ const args = { projectDir: process.cwd(), profile: 'default' };
44
+ for (let i = 0; i < argv.length; i += 1) {
45
+ const arg = argv[i];
46
+ if (arg === '--project') {
47
+ args.projectDir = argv[++i] ?? args.projectDir;
48
+ } else if (arg === '--profile') {
49
+ args.profile = argv[++i] ?? args.profile;
50
+ }
51
+ }
52
+ args.projectDir = resolve(args.projectDir);
53
+ return args;
54
+ }
55
+
56
+ function readJson(path) {
57
+ return JSON.parse(readFileSync(path, 'utf8'));
58
+ }
59
+
60
+ function readProjectConfig(projectDir) {
61
+ const configPath = join(resolveProjectAiwgDir(projectDir), 'aiwg.config');
62
+ try {
63
+ return { config: readJson(configPath), configPath };
64
+ } catch (error) {
65
+ if (error && error.code === 'ENOENT') return { config: null, configPath };
66
+ throw new Error(`Unable to read ${configPath}: ${error.message}`);
67
+ }
68
+ }
69
+
70
+ function readSwapGb() {
71
+ if (process.platform === 'linux') {
72
+ try {
73
+ const meminfo = readFileSync('/proc/meminfo', 'utf8');
74
+ const match = meminfo.match(/^SwapTotal:\s+(\d+)\s+kB$/m);
75
+ if (match) return Number(match[1]) / 1024 / 1024;
76
+ } catch {
77
+ return undefined;
78
+ }
79
+ }
80
+ return undefined;
81
+ }
82
+
83
+ function detectHostResources(projectDir) {
84
+ const disk = statfsSync(projectDir);
85
+ return {
86
+ memory_gb: os.totalmem() / BYTES_PER_GB,
87
+ free_disk_gb: (Number(disk.bavail) * Number(disk.bsize)) / BYTES_PER_GB,
88
+ cpus: os.cpus().length,
89
+ swap_gb: readSwapGb(),
90
+ };
91
+ }
92
+
93
+ function formatValue(value, unit) {
94
+ if (value === undefined || Number.isNaN(value)) return 'unknown';
95
+ if (unit === 'count') return String(Math.floor(value));
96
+ return `${value.toFixed(1)} GB`;
97
+ }
98
+
99
+ function normalizeMode(rawMode) {
100
+ if (rawMode === undefined || rawMode === null) return 'configured';
101
+ if (rawMode === 'configured' || rawMode === 'auto_detect') return rawMode;
102
+ throw new Error(`build.resource_preflight.mode must be "configured" or "auto_detect" (got ${JSON.stringify(rawMode)})`);
103
+ }
104
+
105
+ function normalizeRequirements(preflight) {
106
+ const mode = normalizeMode(preflight.mode);
107
+ const explicit = preflight.requirements ?? {};
108
+ if (typeof explicit !== 'object' || Array.isArray(explicit)) {
109
+ throw new Error('build.resource_preflight.requirements must be an object when provided');
110
+ }
111
+
112
+ const base = mode === 'auto_detect' ? DEFAULT_AUTO_REQUIREMENTS : {};
113
+ const requirements = { ...base };
114
+ for (const key of Object.keys(REQUIREMENT_LABELS)) {
115
+ if (explicit[key] === undefined) continue;
116
+ const value = Number(explicit[key]);
117
+ if (!Number.isFinite(value) || value < 0) {
118
+ throw new Error(`build.resource_preflight.requirements.${key} must be a non-negative number`);
119
+ }
120
+ requirements[key] = value;
121
+ }
122
+ return { mode, requirements };
123
+ }
124
+
125
+ function evaluatePreflight(config, hostResources) {
126
+ const preflight = config?.build?.resource_preflight;
127
+ if (!preflight || preflight.enabled !== true) {
128
+ return { enabled: false, ok: true, failures: [], mode: 'disabled', requirements: {} };
129
+ }
130
+
131
+ const { mode, requirements } = normalizeRequirements(preflight);
132
+ const failures = [];
133
+ for (const [requirementKey, expected] of Object.entries(requirements)) {
134
+ if (expected <= 0) continue;
135
+ const hostKey = HOST_KEYS[requirementKey];
136
+ const actual = hostResources[hostKey];
137
+ if (actual === undefined || Number.isNaN(actual)) {
138
+ failures.push({
139
+ resource: REQUIREMENT_LABELS[requirementKey],
140
+ expected,
141
+ actual,
142
+ unit: requirementKey === 'min_cpus' ? 'count' : 'gb',
143
+ reason: 'not detectable on this host',
144
+ });
145
+ continue;
146
+ }
147
+ if (actual < expected) {
148
+ failures.push({
149
+ resource: REQUIREMENT_LABELS[requirementKey],
150
+ expected,
151
+ actual,
152
+ unit: requirementKey === 'min_cpus' ? 'count' : 'gb',
153
+ reason: 'below threshold',
154
+ });
155
+ }
156
+ }
157
+
158
+ return {
159
+ enabled: true,
160
+ ok: failures.length === 0,
161
+ failures,
162
+ mode,
163
+ requirements,
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Parse MemAvailable (kB) from /proc/meminfo text. Returns GiB or undefined
169
+ * when the field is absent/unparsable. Pure: tests feed synthetic fixtures.
170
+ */
171
+ function parseMemAvailableGb(meminfoText) {
172
+ if (typeof meminfoText !== 'string') return undefined;
173
+ const match = meminfoText.match(/^MemAvailable:\s+(\d+)\s+kB$/m);
174
+ if (!match) return undefined;
175
+ const gib = Number(match[1]) / 1024 / 1024;
176
+ return Number.isFinite(gib) ? gib : undefined;
177
+ }
178
+
179
+ /**
180
+ * Parse memory pressure from /proc/pressure/memory text. Returns
181
+ * `{ avg10, avg60, avg300 }` from the `some` line, or null when no `some`
182
+ * line parses. Pure: tests feed synthetic fixtures.
183
+ */
184
+ function parseMemoryPsi(pressureText) {
185
+ if (typeof pressureText !== 'string') return null;
186
+ for (const line of pressureText.split('\n')) {
187
+ if (!line.startsWith('some')) continue;
188
+ const values = {};
189
+ for (const part of line.split(/\s+/)) {
190
+ const [key, raw] = part.split('=');
191
+ if ((key === 'avg10' || key === 'avg60' || key === 'avg300') && raw !== undefined) {
192
+ const value = Number(raw);
193
+ if (Number.isFinite(value)) values[key] = value;
194
+ }
195
+ }
196
+ if (values.avg10 !== undefined && values.avg60 !== undefined && values.avg300 !== undefined) return values;
197
+ return null;
198
+ }
199
+ return null;
200
+ }
201
+
202
+ /**
203
+ * Parse /proc/loadavg text. Returns `{ load1, load5, load15 }` or null.
204
+ * Pure: tests feed synthetic fixtures.
205
+ */
206
+ function parseLoadAverages(loadavgText) {
207
+ if (typeof loadavgText !== 'string') return null;
208
+ const parts = loadavgText.trim().split(/\s+/);
209
+ const loads = parts.slice(0, 3).map(Number);
210
+ if (loads.length !== 3 || loads.some(value => !Number.isFinite(value))) return null;
211
+ return { load1: loads[0], load5: loads[1], load15: loads[2] };
212
+ }
213
+
214
+ /** Count active current-user units with the explicit aiwg-worker- prefix. */
215
+ function countWorkerUnits({ spawn = spawnSync } = {}) {
216
+ try {
217
+ const result = spawn('systemctl', ['--user', 'list-units', '--type=service',
218
+ '--state=running,activating', '--no-legend', '--plain', 'aiwg-worker-*'],
219
+ { encoding: 'utf8', timeout: 1000 });
220
+ if (result.error || result.status !== 0) return undefined;
221
+ return String(result.stdout ?? '').split('\n')
222
+ .filter(line => /^aiwg-worker-[^\s]+\.service\s/.test(line.trim())).length;
223
+ } catch { return undefined; }
224
+ }
225
+
226
+ /**
227
+ * Current UID only. Explicit worker units are authoritative when available;
228
+ * otherwise count exact comm names, never shell text or arbitrary argv.
229
+ */
230
+ function defaultCountWorkers({
231
+ readDir = readdirSync,
232
+ readText = path => readFileSync(path, 'utf8'),
233
+ uid = process.getuid?.(),
234
+ countWorkerUnits: readUnits = countWorkerUnits,
235
+ } = {}) {
236
+ if (process.platform !== 'linux' || uid === undefined) return undefined;
237
+ // Units cover launched workers; process names cover degraded launches and other
238
+ // conductors. Take the larger so no worker class can hide from max_workers.
239
+ let units;
240
+ try {
241
+ const counted = readUnits();
242
+ if (Number.isInteger(counted) && counted >= 0) units = counted;
243
+ } catch { /* Fall back to current-user process names. */ }
244
+ let entries;
245
+ try { entries = readDir('/proc'); } catch { return units; }
246
+ let total = 0;
247
+ for (const entry of entries) {
248
+ if (!/^\d+$/.test(entry)) continue;
249
+ try {
250
+ const status = readText(`/proc/${entry}/status`);
251
+ const owner = status.match(/^Uid:\s+(\d+)/m);
252
+ if (!owner || Number(owner[1]) !== uid) continue;
253
+ const comm = readText(`/proc/${entry}/comm`).trim();
254
+ if (comm === 'codex' || comm === 'muse-bin') total += 1;
255
+ } catch { /* Processes may exit during the scan. */ }
256
+ }
257
+ return Math.max(units ?? 0, total);
258
+ }
259
+
260
+ /**
261
+ * Sample live host signals. All inputs are injectable so tests use synthetic
262
+ * meminfo/pressure/loadavg fixtures instead of the real /proc:
263
+ * `{ readText, coreCount, countWorkers }`. Non-Linux hosts report
264
+ * `{ supported: false }`; the gate turns that into an explicit diagnostic
265
+ * (MVP qualifies Linux per NFR-OG-008) rather than an implicit pass.
266
+ */
267
+ /**
268
+ * Convert one statfs result to GiB. Non-finite or negative samples are
269
+ * rejected (fail closed): a NaN/Infinity/-1 reading must never become a
270
+ * passing number downstream.
271
+ */
272
+ function toFreeDiskGb(disk) {
273
+ const gb = (Number(disk?.bavail) * Number(disk?.bsize)) / BYTES_PER_GB;
274
+ return Number.isFinite(gb) && gb >= 0 ? gb : undefined;
275
+ }
276
+
277
+ /**
278
+ * Sample free disk on the caller-supplied filesystem(s): `readers.paths`
279
+ * names the actual evidence root and worktree, and the gate takes the
280
+ * minimum across them. Any unreadable, non-finite or negative sample — or
281
+ * no supplied filesystem at all — yields undefined so the gate fails closed
282
+ * instead of admitting on the conductor's process-cwd filesystem.
283
+ * `readers.statfs` (zero-arg) and `readers.statfsPath` (per-path) stay as
284
+ * injectable test seams; there is no process.cwd() fallback.
285
+ */
286
+ function sampleFreeDiskGb(readers) {
287
+ if (Array.isArray(readers.paths)) {
288
+ if (readers.paths.length === 0) return undefined;
289
+ const statOne = typeof readers.statfsPath === 'function' ? readers.statfsPath : (path) => statfsSync(path);
290
+ let minimum;
291
+ for (const path of readers.paths) {
292
+ let sample;
293
+ try {
294
+ sample = toFreeDiskGb(statOne(path));
295
+ } catch {
296
+ return undefined;
297
+ }
298
+ if (sample === undefined) return undefined;
299
+ minimum = minimum === undefined ? sample : Math.min(minimum, sample);
300
+ }
301
+ return minimum;
302
+ }
303
+ if (typeof readers.statfs === 'function') {
304
+ try {
305
+ return toFreeDiskGb(readers.statfs());
306
+ } catch {
307
+ return undefined;
308
+ }
309
+ }
310
+ return undefined;
311
+ }
312
+
313
+ function readLiveHostResources(readers = {}) {
314
+ const readText = readers.readText ?? (path => readFileSync(path, 'utf8'));
315
+ const coreCount = readers.coreCount ?? (() => os.cpus().length);
316
+ const countWorkers = readers.countWorkers ?? (() => defaultCountWorkers());
317
+ // 08 disk-pressure admission: free disk sampled through the same injectable
318
+ // resolver path as every other live signal, so tests feed synthetic values.
319
+ // Callers supply the evidence/worktree filesystems via `readers.paths`
320
+ // (threaded through `checkWorkerDispatch({ paths })`); without them free
321
+ // disk is unknown and the gate fails closed.
322
+ if (process.platform !== 'linux' && readers.readText === undefined) {
323
+ return { supported: false, reason: `live dispatch signals require Linux /proc (this host: ${process.platform})` };
324
+ }
325
+ const readOptional = (path) => {
326
+ try {
327
+ return readText(path);
328
+ } catch {
329
+ return undefined;
330
+ }
331
+ };
332
+ const cores = coreCount();
333
+ const load = parseLoadAverages(readOptional('/proc/loadavg') ?? '');
334
+ const psi = parseMemoryPsi(readOptional('/proc/pressure/memory') ?? '');
335
+ let workers;
336
+ try {
337
+ workers = countWorkers();
338
+ } catch {
339
+ workers = undefined;
340
+ }
341
+ return {
342
+ supported: true,
343
+ availMemGb: parseMemAvailableGb(readOptional('/proc/meminfo') ?? ''),
344
+ load1: load?.load1,
345
+ cores: Number.isFinite(cores) && cores > 0 ? cores : undefined,
346
+ loadRatio: load && cores ? load.load1 / cores : undefined,
347
+ memPsiAvg10: psi?.avg10,
348
+ memPsiAvg60: psi?.avg60,
349
+ memPsiAvg300: psi?.avg300,
350
+ workers,
351
+ freeDiskGb: sampleFreeDiskGb(readers),
352
+ };
353
+ }
354
+
355
+ /** Adapt tool option names to the single source-owned config resolver. */
356
+ function resolveDispatchThresholds({ buildDispatch, admissionHostResources, overlay } = {}) {
357
+ return resolveDispatchHostThresholds(buildDispatch, admissionHostResources, overlay);
358
+ }
359
+
360
+ /**
361
+ * Evaluate sampled live signals against thresholds. Every breach becomes a
362
+ * BLOCK failure naming its signal. Unknown-but-required signals (threshold
363
+ * configured, signal undetectable) fail closed with reason 'not detectable'.
364
+ */
365
+ function evaluateDispatchGate(live, thresholds) {
366
+ const failures = [];
367
+ if (!live || live.supported === false) {
368
+ return {
369
+ ok: false,
370
+ failures: [{ signal: 'platform', reason: live?.reason ?? 'live host signals unavailable' }],
371
+ thresholds,
372
+ };
373
+ }
374
+ const breach = (signal, actual, threshold, reason) => failures.push({ signal, actual, threshold, reason });
375
+ if (live.availMemGb === undefined) breach('avail-mem', undefined, thresholds.min_avail_mem_gb, 'MemAvailable not detectable on this host');
376
+ else if (live.availMemGb < thresholds.min_avail_mem_gb) {
377
+ breach('avail-mem', live.availMemGb, thresholds.min_avail_mem_gb, `available ${live.availMemGb.toFixed(1)}GB < ${thresholds.min_avail_mem_gb}GB`);
378
+ }
379
+ if (live.loadRatio === undefined) breach('load', undefined, thresholds.max_load_ratio, 'load average not detectable on this host');
380
+ else if (live.loadRatio > thresholds.max_load_ratio) {
381
+ breach('load', live.loadRatio, thresholds.max_load_ratio, `load ratio ${live.loadRatio.toFixed(2)} > ${thresholds.max_load_ratio}`);
382
+ }
383
+ if (live.memPsiAvg10 === undefined) breach('mem-psi-avg10', undefined, thresholds.max_mem_psi_avg10, '/proc/pressure/memory not detectable on this host');
384
+ else if (live.memPsiAvg10 > thresholds.max_mem_psi_avg10) {
385
+ breach('mem-psi-avg10', live.memPsiAvg10, thresholds.max_mem_psi_avg10, `memory PSI avg10 ${live.memPsiAvg10} > ${thresholds.max_mem_psi_avg10}`);
386
+ }
387
+ if (live.memPsiAvg60 === undefined) breach('mem-psi-avg60', undefined, thresholds.max_mem_psi_avg60, '/proc/pressure/memory not detectable on this host');
388
+ else if (live.memPsiAvg60 > thresholds.max_mem_psi_avg60) {
389
+ breach('mem-psi-avg60', live.memPsiAvg60, thresholds.max_mem_psi_avg60, `memory PSI avg60 ${live.memPsiAvg60} > ${thresholds.max_mem_psi_avg60}`);
390
+ }
391
+ if (thresholds.max_workers !== undefined) {
392
+ if (live.workers === undefined) breach('workers', undefined, thresholds.max_workers, 'worker count not detectable on this host');
393
+ else if (live.workers >= thresholds.max_workers) {
394
+ breach('workers', live.workers, thresholds.max_workers, `${live.workers} running workers >= ${thresholds.max_workers}`);
395
+ }
396
+ }
397
+ // 08 disk-pressure admission: unknown-but-required free disk fails closed,
398
+ // like every other required signal; under-threshold free disk blocks.
399
+ // Pre-sampled NaN/Infinity/negative readings are rejected here too, so a
400
+ // forged or broken sample can never satisfy the gate.
401
+ if (thresholds.min_free_disk_gb !== undefined && thresholds.min_free_disk_gb > 0) {
402
+ const disk = live.freeDiskGb;
403
+ if (disk === undefined || !Number.isFinite(disk) || disk < 0) {
404
+ breach('free-disk', undefined, thresholds.min_free_disk_gb, 'free disk not detectable on this host');
405
+ } else if (disk < thresholds.min_free_disk_gb) {
406
+ breach('free-disk', disk, thresholds.min_free_disk_gb, `free ${disk.toFixed(1)}GB < ${thresholds.min_free_disk_gb}GB`);
407
+ }
408
+ }
409
+ return { ok: failures.length === 0, failures, thresholds };
410
+ }
411
+
412
+ /**
413
+ * Tier 0 consumable dispatch check (FR-OG-107, #2954). `prepareWorkerInvocation`
414
+ * callers and the conductor procedure call this before launch; over threshold
415
+ * means queue, pick another qualified host, or reject with the typed failures —
416
+ * never launch. When `parallelism.worker_dispatch` is off, legacy behaviour is
417
+ * unchanged: `{ enabled: false, ok: true }` with no signal sampling.
418
+ *
419
+ * @param {object} options `{ config, live?, readers?, thresholdOverlay?, paths? }`.
420
+ * `live` accepts a pre-sampled `readLiveHostResources()` value (tests and
421
+ * SharedHostScheduler pass their own); otherwise signals are sampled now.
422
+ * `paths` names the filesystems to sample free disk on (evidence root and
423
+ * worktree); without caller-supplied paths free disk is unknown and the
424
+ * gate fails closed rather than sampling the conductor's process cwd.
425
+ */
426
+ function checkWorkerDispatch({ config, live, readers, thresholdOverlay, paths } = {}) {
427
+ if (!config?.parallelism?.worker_dispatch) {
428
+ return { enabled: false, ok: true, failures: [], reason: 'worker_dispatch is off; dispatch gate not consulted (legacy behaviour)' };
429
+ }
430
+ let thresholds;
431
+ try {
432
+ thresholds = resolveDispatchThresholds({
433
+ buildDispatch: config?.build?.resource_preflight?.dispatch,
434
+ admissionHostResources: config?.parallelism?.admission?.hostResources,
435
+ overlay: thresholdOverlay,
436
+ });
437
+ } catch (error) {
438
+ return { enabled: true, ok: false, failures: [{ signal: 'thresholds', reason: error.message }], thresholds: undefined };
439
+ }
440
+ const sampled = live ?? readLiveHostResources(paths !== undefined ? { ...readers, paths } : (readers ?? {}));
441
+ return { enabled: true, ...evaluateDispatchGate(sampled, thresholds), live: sampled };
442
+ }
443
+
444
+ function printResult(result, { profile, configPath }) {
445
+ if (!result.enabled) {
446
+ console.log(`[build-preflight] skipped: build.resource_preflight.enabled is not true (${configPath})`);
447
+ return;
448
+ }
449
+
450
+ const checked = Object.keys(result.requirements);
451
+ if (checked.length === 0) {
452
+ console.log(`[build-preflight] ${profile}: enabled in configured mode; no explicit requirements set`);
453
+ return;
454
+ }
455
+
456
+ if (result.ok) {
457
+ console.log(`[build-preflight] ${profile}: host resources satisfy ${result.mode} requirements`);
458
+ return;
459
+ }
460
+
461
+ console.error(`[build-preflight] ${profile}: host resource preflight failed`);
462
+ for (const failure of result.failures) {
463
+ const unit = failure.unit === 'count' ? 'count' : 'gb';
464
+ console.error(
465
+ ` - ${failure.resource}: actual ${formatValue(failure.actual, unit)}, expected >= ${formatValue(failure.expected, unit)} (${failure.reason})`,
466
+ );
467
+ }
468
+ }
469
+
470
+ function printDispatchResult(result, { configPath }) {
471
+ const live = result.live ?? {};
472
+ const signals = [
473
+ `availGB=${live.availMemGb === undefined ? 'unknown' : live.availMemGb.toFixed(1)}`,
474
+ `load=${live.load1 === undefined ? 'unknown' : live.load1}`,
475
+ `cores=${live.cores ?? 'unknown'}`,
476
+ `memPSI10=${live.memPsiAvg10 ?? 'unknown'}`,
477
+ `memPSI60=${live.memPsiAvg60 ?? 'unknown'}`,
478
+ `workers=${live.workers ?? 'unknown'}`,
479
+ `freeDiskGB=${live.freeDiskGb === undefined ? 'unknown' : live.freeDiskGb.toFixed(1)}`,
480
+ ].join(' ');
481
+ if (!result.enabled) {
482
+ console.log(`[dispatch-preflight] skipped: ${result.reason} (${configPath})`);
483
+ return;
484
+ }
485
+ if (result.ok) {
486
+ console.log(`[dispatch-preflight] OK: host can take another worker or heavy run (${signals})`);
487
+ return;
488
+ }
489
+ console.error('[dispatch-preflight] BLOCK: no launch or heavy run on this host');
490
+ for (const failure of result.failures) {
491
+ console.error(` - ${failure.signal}: ${failure.reason}`);
492
+ }
493
+ console.error(` host: ${signals}`);
494
+ }
495
+
496
+ function main({ argv = process.argv.slice(2), env = process.env, readConfig = readProjectConfig, live, readers } = {}) {
497
+ const args = parseArgs(argv);
498
+ if (args.profile === 'dispatch') {
499
+ const { config, configPath } = readConfig(args.projectDir);
500
+ // Sample the project filesystem — the evidence it will hold — never the
501
+ // caller's process cwd.
502
+ const result = checkWorkerDispatch({ config, live, readers, paths: live !== undefined ? undefined : [args.projectDir] });
503
+ printDispatchResult(result, { configPath });
504
+ if (!result.ok) process.exitCode = 1;
505
+ return result;
506
+ }
507
+ if (env.AIWG_BUILD_PREFLIGHT_DONE === '1') return;
508
+ if (env.AIWG_BUILD_PREFLIGHT === '0' || env.AIWG_BUILD_PREFLIGHT === 'false') {
509
+ console.log('[build-preflight] skipped by AIWG_BUILD_PREFLIGHT=0');
510
+ return;
511
+ }
512
+ const { config, configPath } = readConfig(args.projectDir);
513
+ const hostResources = detectHostResources(args.projectDir);
514
+ const result = evaluatePreflight(config, hostResources);
515
+ printResult(result, { profile: args.profile, configPath });
516
+ if (!result.ok) process.exitCode = 1;
517
+ return result;
518
+ }
519
+
520
+ const invokedPath = process.argv[1] ? resolve(process.argv[1]) : '';
521
+ if (invokedPath === fileURLToPath(import.meta.url)) {
522
+ main();
523
+ }
524
+
525
+ export {
526
+ DEFAULT_AUTO_REQUIREMENTS,
527
+ DEFAULT_DISPATCH_THRESHOLDS,
528
+ checkWorkerDispatch,
529
+ defaultCountWorkers,
530
+ countWorkerUnits,
531
+ main,
532
+ detectHostResources,
533
+ evaluateDispatchGate,
534
+ evaluatePreflight,
535
+ normalizeRequirements,
536
+ parseLoadAverages,
537
+ parseMemAvailableGb,
538
+ parseMemoryPsi,
539
+ readLiveHostResources,
540
+ resolveDispatchThresholds,
541
+ };
@@ -37,7 +37,7 @@ let fs;
37
37
  try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; }
38
38
  import path from 'path';
39
39
  import os from 'os';
40
- import { getFrameworksForMode, normalizeDeploymentMode, skillMatchesProvider, isKernelSkill } from '../agents/providers/base.mjs';
40
+ import { contentHash, writeSidecarManifest, getFrameworksForMode, normalizeDeploymentMode, skillMatchesProvider, isKernelSkill } from '../agents/providers/base.mjs';
41
41
 
42
42
  const CODEX_SKILLS_DIR = path.join(os.homedir(), '.codex', 'skills');
43
43
  const MAX_NAME_LENGTH = 100;
@@ -433,6 +433,7 @@ function deploySkill(skill, targetDir, opts) {
433
433
  // during transform, so the SKILL.md alone isn't a reliable signal).
434
434
  // Cleanup keys off this presence.
435
435
  fs.writeFileSync(path.join(skillDir, '.aiwg-managed'), 'aiwg\n', 'utf8');
436
+ writeSidecarManifest(skillDir, { skillSource: path.dirname(skill.sourcePath), managed: { 'SKILL.md': { sourcePath: skill.sourcePath, hash: `sha256:${contentHash(skill.content)}` } } }, false);
436
437
  console.log(` deployed: ${skill.name}`);
437
438
 
438
439
  return { action: 'deploy', reason: 'success' };