@aiwg/cli 2026.10.1 → 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 (361) hide show
  1. package/README.md +2 -2
  2. package/agentic/code/providers/capability-matrix.yaml +270 -0
  3. package/agentic/code/providers/model-capabilities.v1.json +3183 -24
  4. package/bin/aiwg.mjs +135 -47
  5. package/dist/src/a2a/codecs.js +9 -0
  6. package/dist/src/agents/grounding/knowledge-base.js +54 -6
  7. package/dist/src/artifacts/browser-export.js +6 -0
  8. package/dist/src/artifacts/checksum-manifest.js +1 -0
  9. package/dist/src/artifacts/citation-parser.js +173 -92
  10. package/dist/src/artifacts/citation-table-schema.js +59 -0
  11. package/dist/src/artifacts/cli.js +57 -18
  12. package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
  13. package/dist/src/artifacts/corpus-tools/vision-extract.js +107 -15
  14. package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
  15. package/dist/src/artifacts/corpus-views/renderers.js +3 -3
  16. package/dist/src/artifacts/discovery-eval.js +6 -4
  17. package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
  18. package/dist/src/artifacts/fortemi-core-sync.js +166 -40
  19. package/dist/src/artifacts/framework-capabilities.js +171 -0
  20. package/dist/src/artifacts/framework-index-paths.js +73 -0
  21. package/dist/src/artifacts/framework-source-digest.js +75 -0
  22. package/dist/src/artifacts/index-builder.js +285 -131
  23. package/dist/src/artifacts/index-files.js +132 -18
  24. package/dist/src/artifacts/index-preparation.js +65 -0
  25. package/dist/src/artifacts/index-refresh.js +576 -0
  26. package/dist/src/artifacts/index-worker.mjs +9 -0
  27. package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
  28. package/dist/src/artifacts/prebuilt-deployment.js +46 -0
  29. package/dist/src/artifacts/prebuilt-relations.js +80 -0
  30. package/dist/src/artifacts/query-engine.js +410 -88
  31. package/dist/src/artifacts/relations-builder.js +168 -0
  32. package/dist/src/artifacts/relations.js +148 -0
  33. package/dist/src/artifacts/repair.js +9 -4
  34. package/dist/src/artifacts/stats.js +5 -2
  35. package/dist/src/artifacts/types.js +30 -10
  36. package/dist/src/artifacts/watcher.js +1 -1
  37. package/dist/src/auth/credential-store.js +2 -1
  38. package/dist/src/catalog/cli.d.mts +5 -0
  39. package/dist/src/channel/manager.d.mts +20 -0
  40. package/dist/src/channel/manager.mjs +78 -12
  41. package/dist/src/cli/child-process-result.js +40 -0
  42. package/dist/src/cli/command-log.js +2 -3
  43. package/dist/src/cli/env.js +38 -16
  44. package/dist/src/cli/flag-parsing.js +236 -0
  45. package/dist/src/cli/handlers/artifacts.js +3 -0
  46. package/dist/src/cli/handlers/auth.js +3 -0
  47. package/dist/src/cli/handlers/decision.js +218 -5
  48. package/dist/src/cli/handlers/diagnose.js +13 -5
  49. package/dist/src/cli/handlers/effect.js +4 -2
  50. package/dist/src/cli/handlers/evidence.js +3 -0
  51. package/dist/src/cli/handlers/feedback.js +14 -5
  52. package/dist/src/cli/handlers/help.js +1 -0
  53. package/dist/src/cli/handlers/index.js +3 -1
  54. package/dist/src/cli/handlers/init.js +248 -30
  55. package/dist/src/cli/handlers/lint.js +5 -0
  56. package/dist/src/cli/handlers/mc.js +23 -0
  57. package/dist/src/cli/handlers/models.js +169 -10
  58. package/dist/src/cli/handlers/ralph-launcher.js +2 -0
  59. package/dist/src/cli/handlers/ralph.js +3 -0
  60. package/dist/src/cli/handlers/refresh.js +28 -0
  61. package/dist/src/cli/handlers/regenerate.js +14 -0
  62. package/dist/src/cli/handlers/resource-versions.js +3 -0
  63. package/dist/src/cli/handlers/rules.js +195 -0
  64. package/dist/src/cli/handlers/run.js +4 -0
  65. package/dist/src/cli/handlers/runtime-info.js +25 -11
  66. package/dist/src/cli/handlers/script-runner.js +19 -8
  67. package/dist/src/cli/handlers/session.js +406 -62
  68. package/dist/src/cli/handlers/sessions.js +243 -76
  69. package/dist/src/cli/handlers/setup-manifest.js +474 -79
  70. package/dist/src/cli/handlers/setup.js +180 -39
  71. package/dist/src/cli/handlers/steward.js +11 -0
  72. package/dist/src/cli/handlers/subcommands.js +60 -3
  73. package/dist/src/cli/handlers/team.js +18 -8
  74. package/dist/src/cli/handlers/uhp.js +3 -0
  75. package/dist/src/cli/handlers/use.js +307 -55
  76. package/dist/src/cli/handlers/utilities.js +125 -26
  77. package/dist/src/cli/handlers/version.js +8 -0
  78. package/dist/src/cli/prompt-utils.js +72 -0
  79. package/dist/src/cli/provider-resolution.js +11 -0
  80. package/dist/src/cli/router.js +15 -1
  81. package/dist/src/cli/services/deployment-verification.js +19 -9
  82. package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
  83. package/dist/src/cli/skill-usage.js +2 -3
  84. package/dist/src/config/aiwg-config.js +349 -35
  85. package/dist/src/config/cli.js +256 -23
  86. package/dist/src/config/decision-config.js +106 -0
  87. package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
  88. package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
  89. package/dist/src/config/gitignore.js +50 -0
  90. package/dist/src/config/index-graph-overrides.js +36 -0
  91. package/dist/src/config/project-artifacts-health.d.mts +37 -0
  92. package/dist/src/config/project-artifacts-health.mjs +14 -3
  93. package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
  94. package/dist/src/config/user-config-dir.d.mts +9 -0
  95. package/dist/src/config/user-config.js +38 -11
  96. package/dist/src/config/user-registry.js +23 -15
  97. package/dist/src/config/user-state.js +7 -0
  98. package/dist/src/decision/adapters/jev.js +75 -27
  99. package/dist/src/decision/ask.js +203 -43
  100. package/dist/src/decision/context-live-qualification.js +2 -2
  101. package/dist/src/decision/driver.js +14 -7
  102. package/dist/src/decision/ensemble/aggregate.js +23 -10
  103. package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
  104. package/dist/src/decision/entry.js +25 -6
  105. package/dist/src/decision/graph-live-qualification.js +1 -1
  106. package/dist/src/decision/graph-run.js +9 -0
  107. package/dist/src/decision/heldout/collector.js +15 -14
  108. package/dist/src/decision/heldout/contract.js +30 -7
  109. package/dist/src/decision/index.js +1 -0
  110. package/dist/src/decision/interop.js +60 -8
  111. package/dist/src/decision/jev-credentials.js +15 -1
  112. package/dist/src/decision/ledger.js +942 -0
  113. package/dist/src/decision/offload-activation.js +137 -0
  114. package/dist/src/decision/offload.js +550 -0
  115. package/dist/src/decision/qualification/load.js +4 -6
  116. package/dist/src/decision/savings.js +28 -0
  117. package/dist/src/decision/validate.js +6 -3
  118. package/dist/src/effects/dispatch-verifier.js +252 -0
  119. package/dist/src/effects/index.js +1 -0
  120. package/dist/src/effects/types.js +5 -1
  121. package/dist/src/effects/verifiers/git.js +2 -1
  122. package/dist/src/effects/verifiers/tracker.js +26 -14
  123. package/dist/src/extensions/commands/definitions.js +19 -0
  124. package/dist/src/extensions/project-local-remove.js +36 -1
  125. package/dist/src/features/installer.js +2 -2
  126. package/dist/src/flow/graph-metadata.js +5 -0
  127. package/dist/src/gates/providers/loader.js +64 -18
  128. package/dist/src/installation/manager-command.d.mts +18 -0
  129. package/dist/src/installation/manager-command.mjs +15 -3
  130. package/dist/src/installation/manager.d.mts +49 -0
  131. package/dist/src/installation/manager.mjs +95 -5
  132. package/dist/src/issues/cli.js +4 -4
  133. package/dist/src/issues/local.js +24 -5
  134. package/dist/src/jobs/runner.js +1 -1
  135. package/dist/src/lint/cli.js +27 -1
  136. package/dist/src/lint/loader.js +2 -1
  137. package/dist/src/lint/reporters.js +25 -1
  138. package/dist/src/lint/rule-activation.js +43 -0
  139. package/dist/src/lint/runner.js +137 -62
  140. package/dist/src/mcp/adapters/codex-runtime.js +366 -56
  141. package/dist/src/mcp/claude-install.d.mts +4 -0
  142. package/dist/src/mcp/claude-install.mjs +41 -0
  143. package/dist/src/mcp/cli.d.mts +1 -0
  144. package/dist/src/mcp/cli.mjs +620 -85
  145. package/dist/src/mcp/config-file.d.mts +28 -0
  146. package/dist/src/mcp/config-file.mjs +279 -0
  147. package/dist/src/mcp/config-layers.d.mts +36 -0
  148. package/dist/src/mcp/config-layers.mjs +348 -0
  149. package/dist/src/mcp/credentials.d.mts +51 -0
  150. package/dist/src/mcp/credentials.mjs +484 -0
  151. package/dist/src/mcp/grok-build-config.d.mts +6 -0
  152. package/dist/src/mcp/grok-build-config.mjs +17 -27
  153. package/dist/src/mcp/helpers.mjs +109 -4
  154. package/dist/src/mcp/muse-native-config.d.mts +13 -0
  155. package/dist/src/mcp/muse-native-config.mjs +1 -0
  156. package/dist/src/mcp/omp-config.d.mts +4 -0
  157. package/dist/src/mcp/omp-config.mjs +50 -29
  158. package/dist/src/mcp/profiles.js +97 -23
  159. package/dist/src/mcp/profiles.mjs +99 -27
  160. package/dist/src/mcp/registry.js +248 -131
  161. package/dist/src/mcp/registry.mjs +169 -100
  162. package/dist/src/mcp/server-config.d.mts +3 -0
  163. package/dist/src/mcp/server-config.mjs +89 -0
  164. package/dist/src/mcp/server.mjs +127 -82
  165. package/dist/src/mcp/test-simple.mjs +2 -1
  166. package/dist/src/mcp/toml-editor.d.mts +4 -0
  167. package/dist/src/mcp/toml-string.d.mts +1 -0
  168. package/dist/src/mcp/toml-string.mjs +9 -0
  169. package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
  170. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  171. package/dist/src/mcp/tool-filters.d.mts +46 -0
  172. package/dist/src/mcp/tool-filters.mjs +357 -0
  173. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  174. package/dist/src/mcp/tools/decision.mjs +22 -3
  175. package/dist/src/mcp/tools/discovery.mjs +62 -55
  176. package/dist/src/mcp/tools/orchestration.mjs +50 -2
  177. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  178. package/dist/src/mcp/tools/worker-invoke.mjs +2856 -0
  179. package/dist/src/mcp/tools/worker-shell-control.mjs +289 -0
  180. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  181. package/dist/src/memory/project-registry.js +2 -2
  182. package/dist/src/mission-protocol/codecs.js +647 -1
  183. package/dist/src/mission-protocol/types.js +110 -0
  184. package/dist/src/mission-protocol/worker-status.js +2 -0
  185. package/dist/src/models/config-loader.js +76 -14
  186. package/dist/src/models/grok-build-models.js +93 -41
  187. package/dist/src/models/model-capabilities.v1.json +3183 -24
  188. package/dist/src/models/model-discovery.js +931 -67
  189. package/dist/src/models/provider-policy.js +719 -11
  190. package/dist/src/models/qualification-digest.d.mts +2 -0
  191. package/dist/src/models/qualification-digest.mjs +17 -0
  192. package/dist/src/models/resolver.js +261 -0
  193. package/dist/src/models/worker-direct-readiness.js +550 -0
  194. package/dist/src/models/worker-dispatch-offer.js +169 -0
  195. package/dist/src/models/worker-health-readiness.js +144 -0
  196. package/dist/src/models/worker-installation.d.mts +39 -0
  197. package/dist/src/models/worker-installation.mjs +986 -0
  198. package/dist/src/models/wrapper-route.js +18 -0
  199. package/dist/src/ops/registry.js +9 -14
  200. package/dist/src/plugins/standalone-packager.js +4 -3
  201. package/dist/src/process/bounded-child-process.d.mts +84 -0
  202. package/dist/src/process/bounded-child-process.mjs +430 -0
  203. package/dist/src/process/cli-command.d.mts +16 -0
  204. package/dist/src/process/cli-command.mjs +75 -0
  205. package/dist/src/process/signal-child.d.mts +6 -0
  206. package/dist/src/process/signal-child.mjs +7 -0
  207. package/dist/src/providers/capability-matrix.js +8 -0
  208. package/dist/src/providers/capability-matrix.yaml +270 -0
  209. package/dist/src/providers/deployed-skill-roots.js +20 -0
  210. package/dist/src/providers/grok-build-acp.js +3 -2
  211. package/dist/src/providers/grok-build-headless.js +53 -23
  212. package/dist/src/providers/grok-build-inspect.js +1 -0
  213. package/dist/src/providers/omp-agent.d.mts +8 -0
  214. package/dist/src/providers/omp-diagnostics.d.mts +1 -0
  215. package/dist/src/providers/omp-paths.d.mts +13 -0
  216. package/dist/src/providers/provider-definitions.js +9 -1
  217. package/dist/src/providers/provider-definitions.mjs +9 -2
  218. package/dist/src/providers/skill-ownership.d.mts +9 -0
  219. package/dist/src/providers/skill-ownership.mjs +88 -0
  220. package/dist/src/rules/activation.d.mts +21 -0
  221. package/dist/src/rules/activation.mjs +62 -0
  222. package/dist/src/rules/inline-budget.d.mts +26 -0
  223. package/dist/src/rules/inline-budget.mjs +66 -0
  224. package/dist/src/rules/inventory.js +129 -0
  225. package/dist/src/rules/pin-state.js +73 -0
  226. package/dist/src/rules/pins.d.mts +14 -0
  227. package/dist/src/rules/pins.mjs +112 -0
  228. package/dist/src/rules/rule-frontmatter.schema.json +197 -0
  229. package/dist/src/serve/executor-registry.js +16 -24
  230. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  231. package/dist/src/serve/mission-conductor.js +47 -12
  232. package/dist/src/serve/shared-host-scheduler.js +2 -0
  233. package/dist/src/serve/worker-invocation.js +558 -0
  234. package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
  235. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  236. package/dist/src/sessions/adapter-registry.js +49 -0
  237. package/dist/src/sessions/adapters/claude.js +39 -54
  238. package/dist/src/sessions/adapters/codex.js +82 -67
  239. package/dist/src/sessions/adapters/copilot.js +4 -4
  240. package/dist/src/sessions/adapters/cursor.js +40 -57
  241. package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
  242. package/dist/src/sessions/adapters/factory.js +31 -27
  243. package/dist/src/sessions/adapters/generic.js +43 -9
  244. package/dist/src/sessions/adapters/grok-build.js +11 -11
  245. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  246. package/dist/src/sessions/adapters/grokbot.js +434 -24
  247. package/dist/src/sessions/adapters/hermes.js +27 -29
  248. package/dist/src/sessions/adapters/muse-workspace-scan.js +128 -0
  249. package/dist/src/sessions/adapters/muse.js +200 -104
  250. package/dist/src/sessions/adapters/omp.js +28 -14
  251. package/dist/src/sessions/adapters/openclaw.js +89 -79
  252. package/dist/src/sessions/adapters/opencode.js +27 -24
  253. package/dist/src/sessions/adapters/openhuman.js +18 -60
  254. package/dist/src/sessions/adapters/pi.js +30 -14
  255. package/dist/src/sessions/adapters/warp.js +3 -3
  256. package/dist/src/sessions/adapters/windsurf.js +19 -48
  257. package/dist/src/sessions/batch-contracts.js +25 -5
  258. package/dist/src/sessions/batch-import.js +72 -51
  259. package/dist/src/sessions/candidates.js +47 -11
  260. package/dist/src/sessions/contracts.js +75 -2
  261. package/dist/src/sessions/coverage-preflight.js +134 -0
  262. package/dist/src/sessions/discovery.js +3 -0
  263. package/dist/src/sessions/export-plan.js +4 -3
  264. package/dist/src/sessions/grok-build-cli.js +67 -5
  265. package/dist/src/sessions/import-exclusions.js +72 -0
  266. package/dist/src/sessions/import-lease.js +34 -18
  267. package/dist/src/sessions/import-limits.js +37 -0
  268. package/dist/src/sessions/importer.js +84 -46
  269. package/dist/src/sessions/index.js +2 -0
  270. package/dist/src/sessions/lifecycle.js +45 -0
  271. package/dist/src/sessions/policy.js +133 -25
  272. package/dist/src/sessions/readers.js +106 -53
  273. package/dist/src/sessions/refresh.js +106 -0
  274. package/dist/src/sessions/repository.js +199 -63
  275. package/dist/src/sessions/workspace-discovery.js +315 -88
  276. package/dist/src/sessions/workspace-matching.js +107 -0
  277. package/dist/src/skills/adapters/clawhub.js +10 -8
  278. package/dist/src/skills/adapters/openclaw.js +10 -7
  279. package/dist/src/skills/importer.js +1 -0
  280. package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
  281. package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
  282. package/dist/src/smiths/context-pipeline/finalization.js +6 -5
  283. package/dist/src/smiths/context-pipeline/generator.js +21 -4
  284. package/dist/src/smiths/context-pipeline/parallelism-section.js +195 -14
  285. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  286. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  287. package/dist/src/smiths/mcpsmith/example.js +1 -1
  288. package/dist/src/smiths/mcpsmith/generator.js +1 -1
  289. package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
  290. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +85 -31
  291. package/dist/src/storage/backends/fs.js +18 -76
  292. package/dist/src/storage/backends/logseq.js +68 -7
  293. package/dist/src/storage/backends/obsidian.js +1 -0
  294. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  295. package/dist/src/testing/generators/test-code-generator.js +57 -30
  296. package/dist/src/tracker/forge-http.js +2 -1
  297. package/dist/src/uhp/client.js +11 -1
  298. package/dist/src/uhp/mission.js +5 -0
  299. package/dist/src/update/checker.d.mts +2 -0
  300. package/dist/src/update/checker.mjs +5 -5
  301. package/dist/src/update/notifier.mjs +54 -2
  302. package/dist/src/update/service.d.mts +22 -0
  303. package/dist/src/utils/contained-filesystem.d.mts +37 -0
  304. package/dist/src/utils/contained-filesystem.mjs +683 -0
  305. package/dist/src/utils/temporary-directory.js +36 -0
  306. package/dist/src/writing/example-generator.js +1 -1
  307. package/dist/src/writing/fidelity.js +18 -1
  308. package/package.json +32 -2
  309. package/schemas/mission-v1.schema.json +2429 -0
  310. package/schemas/models/model-catalog.v1.schema.json +301 -0
  311. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  312. package/schemas/models/model-policy.v1.schema.json +14 -0
  313. package/schemas/models/provider-model-capabilities.v1.schema.json +1072 -0
  314. package/tools/_resolve-impl.mjs +39 -2
  315. package/tools/agents/deploy-agents.mjs +64 -23
  316. package/tools/agents/providers/base.mjs +153 -166
  317. package/tools/agents/providers/claude.mjs +8 -24
  318. package/tools/agents/providers/codex.mjs +18 -11
  319. package/tools/agents/providers/cursor.mjs +19 -18
  320. package/tools/agents/providers/grok-build.mjs +3 -1
  321. package/tools/agents/providers/grokbot.mjs +11 -12
  322. package/tools/agents/providers/hermes.mjs +62 -49
  323. package/tools/agents/providers/omp.mjs +5 -2
  324. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  325. package/tools/agents/providers/openclaw.mjs +9 -4
  326. package/tools/agents/providers/pi.mjs +5 -0
  327. package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
  328. package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
  329. package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
  330. package/tools/agents/providers/rule-delivery.mjs +1270 -0
  331. package/tools/agents/providers/warp.mjs +20 -26
  332. package/tools/cli/decision-offload-report.mjs +40 -0
  333. package/tools/cli/workspace-status.mjs +862 -0
  334. package/tools/plugin/package-plugins.mjs +186 -8
  335. package/tools/plugin/plugin-links.mjs +114 -0
  336. package/tools/providers/antigravity-transport.mjs +3 -1
  337. package/tools/providers/deepseek-harness-transport.mjs +360 -0
  338. package/tools/providers/omp-transport.mjs +194 -0
  339. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  340. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  341. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
  342. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  343. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  344. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
  345. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  346. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  347. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  348. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  349. package/tools/ralph-external/lib/pi-adapter.mjs +329 -0
  350. package/tools/ralph-external/lib/provider-adapter.mjs +748 -0
  351. package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
  352. package/tools/ralph-external/lib/session-environment.mjs +28 -0
  353. package/tools/ralph-external/lib/systemd-client.mjs +49 -0
  354. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  355. package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
  356. package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
  357. package/tools/ralph-external/process-monitor.mjs +1017 -0
  358. package/tools/ralph-external/session-launcher.mjs +5057 -0
  359. package/tools/scripts/resource-preflight.mjs +541 -0
  360. package/tools/skills/deploy-skills-codex.mjs +2 -1
  361. package/vscode-extension/schemas/aiwg.config.v1.json +2916 -0
@@ -1,3 +1,4 @@
1
+ import { signalChild } from '../../process/signal-child.mjs';
1
2
  import { createHash, randomBytes } from 'node:crypto';
2
3
  import { spawn, spawnSync } from 'node:child_process';
3
4
  import { closeSync, lstatSync, openSync, readSync, readdirSync, readFileSync, readlinkSync, realpathSync, statSync, } from 'node:fs';
@@ -7,6 +8,7 @@ import { GateProviderEntrySchema } from '../../extensions/manifest.js';
7
8
  import { artifactDigest } from '../../decision/validate.js';
8
9
  import { providerSourceDigest } from './types.js';
9
10
  import { ISOLATED_CHILD_SOURCE } from './isolated-child-source.js';
11
+ import { positiveTimeoutMs } from '../../process/bounded-child-process.mjs';
10
12
  /**
11
13
  * Addon/extension metric-provider loading, isolated rework (P1-P5).
12
14
  *
@@ -52,6 +54,10 @@ export const GATE_PROVIDER_MAX_TIMEOUT_MS = 30000;
52
54
  export const GATE_PROVIDER_DEFAULT_MAX_RECORDS = 50000;
53
55
  /** Largest accepted isolated-run stdout body (single JSON message). */
54
56
  export const GATE_PROVIDER_MAX_OUTPUT_BYTES = 1024 * 1024;
57
+ /** V8 old generation; RLIMIT_DATA also bounds off-heap ArrayBuffer allocations. */
58
+ export const GATE_PROVIDER_MAX_OLD_SPACE_MB = 128;
59
+ // Node reserves ~512 MiB for its code range before any provider runs.
60
+ export const GATE_PROVIDER_MAX_DATA_BYTES = 768 * 1024 * 1024;
55
61
  export class GateProviderError extends Error {
56
62
  code;
57
63
  constructor(message, code) {
@@ -316,11 +322,16 @@ function prepareIsolatedRun(record, args) {
316
322
  // OS sandbox (C1c/C1d): bubblewrap is the isolation boundary.
317
323
  // ---------------------------------------------------------------------------
318
324
  const BWRAP_CANDIDATES = ['/usr/bin/bwrap', '/bin/bwrap', '/usr/local/bin/bwrap'];
325
+ const PRLIMIT_CANDIDATES = ['/usr/bin/prlimit', '/bin/prlimit', '/usr/local/bin/prlimit'];
319
326
  const SANDBOX_NODE_PATH = '/aiwg/bin/node';
320
327
  const SANDBOX_CWD = '/tmp';
321
328
  const SANDBOX_SELF_TEST_TIMEOUT_MS = 15000;
322
329
  const SANDBOX_NAMESPACES = ['net', 'pid', 'ipc', 'uts', 'user', 'mnt'];
330
+ const MEMORY_NODE_ARGS = [
331
+ `--max-old-space-size=${GATE_PROVIDER_MAX_OLD_SPACE_MB}`, '--max-semi-space-size=16',
332
+ ];
323
333
  const DRIVER_NODE_ARGS = [
334
+ ...MEMORY_NODE_ARGS,
324
335
  '--experimental-vm-modules', '--permission', '--disallow-code-generation-from-strings',
325
336
  '--no-warnings', '--input-type=module', '-e', ISOLATED_CHILD_SOURCE,
326
337
  ];
@@ -329,8 +340,16 @@ const RESULT_FRAME = 'AIWG-RESULT ';
329
340
  const SANDBOX_SELF_TEST_SOURCE = [
330
341
  "import fs from 'node:fs';",
331
342
  "import net from 'node:net';",
343
+ "import v8 from 'node:v8';",
332
344
  "const input = JSON.parse(fs.readFileSync(0, 'utf8'));",
333
- 'const out = { env: {}, ns: {}, visible: [], connect: null };',
345
+ 'const out = { env: {}, ns: {}, visible: [], connect: null, memory: null };',
346
+ // Read the inherited kernel limits AND exercise the allocation path. Do
347
+ // not touch the allocation if the wrapper is broken: fail without committing it.
348
+ "const data = /^Max data size\\s+(\\S+)\\s+(\\S+)/m.exec(fs.readFileSync('/proc/self/limits', 'utf8'));",
349
+ 'let allocationBlocked = false;',
350
+ 'try { new Uint8Array(input.maxDataBytes); } catch (error) { allocationBlocked = error instanceof RangeError; }',
351
+ 'out.memory = { dataSoftBytes: Number(data?.[1]) || null, dataHardBytes: Number(data?.[2]) || null,',
352
+ ' heapLimitBytes: v8.getHeapStatistics().heap_size_limit, allocationBlocked };',
334
353
  'for (const key of Object.keys(process.env)) out.env[key] = String(process.env[key]);',
335
354
  "for (const name of input.namespaces) { try { out.ns[name] = fs.readlinkSync('/proc/self/ns/' + name); } catch { out.ns[name] = null; } }",
336
355
  'for (const path of input.paths) { try { fs.lstatSync(path); out.visible.push(path); } catch { /* absent */ } }',
@@ -348,7 +367,7 @@ let sandboxOverride = null;
348
367
  let sandboxVerdict;
349
368
  let runtimeBindCache;
350
369
  /**
351
- * Test-only hook: point the runner at another bwrap path (for example a
370
+ * Test-only hook: point the runner at another bwrap/prlimit path (for example a
352
371
  * missing binary, or one that fails the self-test) or reset with `null`.
353
372
  * It can only make the runner refuse: whatever path is configured must still
354
373
  * pass the capability self-test before any provider runs.
@@ -359,6 +378,13 @@ export function configureIsolationSandboxForTests(options) {
359
378
  }
360
379
  function resolveBwrapPath() {
361
380
  const candidates = sandboxOverride?.bwrapPath !== undefined ? [sandboxOverride.bwrapPath] : BWRAP_CANDIDATES;
381
+ return resolveExecutable(candidates);
382
+ }
383
+ function resolvePrlimitPath() {
384
+ const candidates = sandboxOverride?.prlimitPath !== undefined ? [sandboxOverride.prlimitPath] : PRLIMIT_CANDIDATES;
385
+ return resolveExecutable(candidates);
386
+ }
387
+ function resolveExecutable(candidates) {
362
388
  for (const candidate of candidates) {
363
389
  try {
364
390
  const status = statSync(candidate);
@@ -475,6 +501,10 @@ function sandboxCommand(nodeArgs) {
475
501
  if (bwrap === null) {
476
502
  throw new GateProviderError('bundle providers require bubblewrap (bwrap) for OS-level isolation and none was found: refusing to run', 'sandbox-unavailable');
477
503
  }
504
+ const prlimit = resolvePrlimitPath();
505
+ if (prlimit === null) {
506
+ throw new GateProviderError('bundle providers require prlimit for a kernel memory cap and none was found: refusing to run', 'sandbox-unavailable');
507
+ }
478
508
  const args = [
479
509
  '--unshare-all', '--unshare-user', '--disable-userns',
480
510
  '--die-with-parent', '--new-session', '--clearenv', '--cap-drop', 'ALL',
@@ -482,7 +512,13 @@ function sandboxCommand(nodeArgs) {
482
512
  for (const [source, target] of runtimeBinds())
483
513
  args.push('--ro-bind', source, target);
484
514
  args.push('--tmpfs', SANDBOX_CWD, '--proc', '/proc', '--dev', '/dev', '--chdir', SANDBOX_CWD, '--', SANDBOX_NODE_PATH, ...nodeArgs);
485
- return { command: bwrap, args, env: {} };
515
+ return {
516
+ command: prlimit,
517
+ // Both limits are hard as well as soft, inherited across exec into bwrap
518
+ // and node. Core dumps are disabled for bounded heap exhaustion failures.
519
+ args: [`--data=${GATE_PROVIDER_MAX_DATA_BYTES}:${GATE_PROVIDER_MAX_DATA_BYTES}`, '--core=0:0', '--', bwrap, ...args],
520
+ env: {},
521
+ };
486
522
  }
487
523
  /**
488
524
  * The exact sandboxed launch used for every provider run: bubblewrap with
@@ -490,7 +526,8 @@ function sandboxCommand(nodeArgs) {
490
526
  * cleared environment, new session, die-with-parent, read-only node runtime
491
527
  * binds, a private tmpfs, and node with `--permission` (no fs, no child
492
528
  * processes, no workers) plus `--disallow-code-generation-from-strings`.
493
- * The spawn environment itself is empty.
529
+ * prlimit applies a hard data cap (including off-heap memory); V8 has a
530
+ * separate heap cap. The spawn environment itself is empty.
494
531
  */
495
532
  export function isolationLaunchPlan() {
496
533
  return sandboxCommand(DRIVER_NODE_ARGS);
@@ -514,11 +551,11 @@ function hostProbePaths() {
514
551
  }
515
552
  function selfTestInput(port) {
516
553
  const paths = hostProbePaths();
517
- return { paths, input: JSON.stringify({ namespaces: SANDBOX_NAMESPACES, paths, port }) };
554
+ return { paths, input: JSON.stringify({ namespaces: SANDBOX_NAMESPACES, paths, port, maxDataBytes: GATE_PROVIDER_MAX_DATA_BYTES }) };
518
555
  }
519
556
  function judgeSelfTest(stdout, status, paths, port) {
520
557
  const report = {
521
- ok: false, reason: '', envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null,
558
+ ok: false, reason: '', envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null, memory: null,
522
559
  };
523
560
  let observed;
524
561
  try {
@@ -549,7 +586,15 @@ function judgeSelfTest(stdout, status, paths, port) {
549
586
  }
550
587
  report.hostPathsVisible = (observed.visible ?? []).filter(path => paths.includes(path));
551
588
  report.connect = observed.connect ?? null;
589
+ report.memory = observed.memory ?? null;
552
590
  const problems = [];
591
+ const memory = report.memory;
592
+ if (memory === null
593
+ || memory.dataSoftBytes !== GATE_PROVIDER_MAX_DATA_BYTES || memory.dataHardBytes !== GATE_PROVIDER_MAX_DATA_BYTES
594
+ || !(memory.heapLimitBytes > 0 && memory.heapLimitBytes <= (GATE_PROVIDER_MAX_OLD_SPACE_MB + 48) * 1024 * 1024)
595
+ || memory.allocationBlocked !== true) {
596
+ problems.push('memory cap not enforced');
597
+ }
553
598
  if (report.envKeys.length)
554
599
  problems.push(`environment leaks ${report.envKeys.join(',')}`);
555
600
  if (report.sharedNamespaces.length)
@@ -564,7 +609,7 @@ function judgeSelfTest(stdout, status, paths, port) {
564
609
  }
565
610
  /**
566
611
  * Runs the sandbox capability self-test asynchronously and returns the
567
- * observations (empty env, private namespaces, no host paths, optional
612
+ * observations (enforced memory caps, empty env, private namespaces, no host paths, optional
568
613
  * loopback connect to a parent listener on `connectPort`).
569
614
  */
570
615
  export function probeIsolationSandbox(options) {
@@ -572,20 +617,20 @@ export function probeIsolationSandbox(options) {
572
617
  let plan;
573
618
  let probe;
574
619
  try {
575
- plan = sandboxCommand(['--no-warnings', '--input-type=module', '-e', SANDBOX_SELF_TEST_SOURCE]);
620
+ plan = sandboxCommand([...MEMORY_NODE_ARGS, '--no-warnings', '--input-type=module', '-e', SANDBOX_SELF_TEST_SOURCE]);
576
621
  probe = selfTestInput(port);
577
622
  }
578
623
  catch (error) {
579
624
  return Promise.resolve({
580
625
  ok: false, reason: error instanceof Error ? error.message : 'sandbox unavailable',
581
- envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null,
626
+ envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null, memory: null,
582
627
  });
583
628
  }
584
629
  return new Promise((resolvePromise) => {
585
630
  const child = spawn(plan.command, plan.args, { env: plan.env, stdio: ['pipe', 'pipe', 'ignore'] });
586
631
  const chunks = [];
587
632
  const timer = setTimeout(() => { try {
588
- child.kill('SIGKILL');
633
+ signalChild(child, 'SIGKILL');
589
634
  }
590
635
  catch { /* exited */ } }, SANDBOX_SELF_TEST_TIMEOUT_MS);
591
636
  child.stdout?.on('data', (chunk) => { chunks.push(chunk); });
@@ -594,7 +639,7 @@ export function probeIsolationSandbox(options) {
594
639
  clearTimeout(timer);
595
640
  resolvePromise({
596
641
  ok: false, reason: `sandbox failed to start: ${error.message}`,
597
- envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null,
642
+ envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null, memory: null,
598
643
  });
599
644
  });
600
645
  child.on('close', (code) => {
@@ -606,15 +651,15 @@ export function probeIsolationSandbox(options) {
606
651
  }
607
652
  /**
608
653
  * Fail-closed gate before every provider run: bwrap must exist and pass the
609
- * capability self-test (cached per bwrap path for the process lifetime).
654
+ * capability self-test (cached per bwrap/prlimit path for the process lifetime).
610
655
  * There is no unsandboxed fallback.
611
656
  */
612
657
  function ensureIsolationSandbox() {
613
- const key = resolveBwrapPath() ?? '<missing>';
658
+ const key = JSON.stringify([resolveBwrapPath(), resolvePrlimitPath()]);
614
659
  if (sandboxVerdict === undefined || sandboxVerdict.key !== key) {
615
660
  let verdict;
616
661
  try {
617
- const plan = sandboxCommand(['--no-warnings', '--input-type=module', '-e', SANDBOX_SELF_TEST_SOURCE]);
662
+ const plan = sandboxCommand([...MEMORY_NODE_ARGS, '--no-warnings', '--input-type=module', '-e', SANDBOX_SELF_TEST_SOURCE]);
618
663
  const probe = selfTestInput(null);
619
664
  const completed = spawnSync(plan.command, plan.args, {
620
665
  env: plan.env, input: probe.input, encoding: 'utf8', timeout: SANDBOX_SELF_TEST_TIMEOUT_MS,
@@ -684,7 +729,8 @@ function runIsolatedRecordSync(record, args, limits) {
684
729
  const completed = spawnSync(plan.command, plan.args, {
685
730
  env: plan.env,
686
731
  input,
687
- timeout: limits.timeoutMs,
732
+ // Validated upstream; clamped here so the bound holds at the call site.
733
+ timeout: positiveTimeoutMs(limits.timeoutMs, GATE_PROVIDER_DEFAULT_TIMEOUT_MS),
688
734
  killSignal: 'SIGKILL',
689
735
  maxBuffer: limits.maxOutputBytes,
690
736
  encoding: 'utf8',
@@ -741,14 +787,14 @@ function runIsolatedRecord(record, args, limits) {
741
787
  }
742
788
  const timer = setTimeout(() => {
743
789
  try {
744
- child.kill('SIGKILL');
790
+ signalChild(child, 'SIGKILL');
745
791
  }
746
792
  catch { /* already exited */ }
747
793
  settle(() => rejectPromise(new GateProviderError(`provider ${record.id} timed out`, 'timeout')));
748
794
  }, limits.timeoutMs);
749
795
  const onAbort = () => {
750
796
  try {
751
- child.kill('SIGKILL');
797
+ signalChild(child, 'SIGKILL');
752
798
  }
753
799
  catch { /* already exited */ }
754
800
  settle(() => rejectPromise(new GateProviderError(`provider ${record.id} was cancelled`, 'cancelled')));
@@ -762,7 +808,7 @@ function runIsolatedRecord(record, args, limits) {
762
808
  if (bytes > limits.maxOutputBytes) {
763
809
  capped = true;
764
810
  try {
765
- child.kill('SIGKILL');
811
+ signalChild(child, 'SIGKILL');
766
812
  }
767
813
  catch { /* already exited */ }
768
814
  settle(() => rejectPromise(new GateProviderError(`provider ${record.id} exceeds its output cap`, 'rejected')));
@@ -0,0 +1,18 @@
1
+ export interface ManagerCommandOptions {
2
+ platform?: NodeJS.Platform;
3
+ env?: NodeJS.ProcessEnv;
4
+ execute?: (file: string, args: string[], options?: Record<string, unknown>) => unknown;
5
+ execOptions?: Record<string, unknown>;
6
+ }
7
+
8
+ export function resolveManagerCommand(
9
+ file: string,
10
+ args: string[],
11
+ options?: ManagerCommandOptions,
12
+ ): { file: string; args: string[] };
13
+
14
+ export function executeManagerCommand(
15
+ file: string,
16
+ args: string[],
17
+ options?: ManagerCommandOptions,
18
+ ): unknown;
@@ -1,7 +1,17 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
 
3
+ const cmdMetaChars = /([()\][%!^"`<>&|;, *?])/g;
4
+
3
5
  function quoteCmdArgument(value) {
4
- return `"${String(value).replace(/%/g, '%%').replace(/"/g, '""')}"`;
6
+ // Match cross-spawn's argument transport: first escape quotes and trailing
7
+ // backslashes for the target's argv parser, then protect cmd metacharacters
8
+ // outside quotes. Doubling % only works in batch source, not a /c operand.
9
+ const argument = String(value)
10
+ .replace(/(?=(\\+?)?)\1"/g, '$1$1\\"')
11
+ .replace(/(?=(\\+?)?)\1$/, '$1$1');
12
+ // Batch wrappers forward operands through another cmd parse (typically %*),
13
+ // so retain one layer of caret escaping for that second parse.
14
+ return `"${argument}"`.replace(cmdMetaChars, '^$1').replace(cmdMetaChars, '^$1');
5
15
  }
6
16
 
7
17
  /**
@@ -17,10 +27,12 @@ export function resolveManagerCommand(file, args, options = {}) {
17
27
 
18
28
  const env = options.env ?? process.env;
19
29
  const commandInterpreter = env.ComSpec || env.COMSPEC || 'cmd.exe';
20
- const command = `"${[file, ...args].map(quoteCmdArgument).join(' ')}"`;
30
+ // The command itself is consumed by the first parse; only its operands need
31
+ // the second escape layer. Escape spaces in the path as well as operators.
32
+ const command = `"${[String(file).replace(cmdMetaChars, '^$1'), ...args.map(quoteCmdArgument)].join(' ')}"`;
21
33
  return {
22
34
  file: commandInterpreter,
23
- args: ['/d', '/s', '/c', command],
35
+ args: ['/d', '/v:off', '/s', '/c', command],
24
36
  // The payload above already contains cmd.exe-native quoting. Instruct
25
37
  // Node not to apply MSVCRT escaping to those embedded quotes (#173).
26
38
  windowsVerbatimArguments: true,
@@ -0,0 +1,49 @@
1
+ export type InstallationMethod = 'npm' | 'web' | 'source';
2
+ export type InstallationRunMode = 'normal' | 'development';
3
+ export type InstallationChannel = 'stable' | 'next' | 'nightly' | 'edge';
4
+ export interface InstallationIdentity {
5
+ schemaVersion: 1;
6
+ runMode: InstallationRunMode;
7
+ method: InstallationMethod;
8
+ root: string;
9
+ updateStrategy: 'npm-global' | 'signed-web' | 'source-git';
10
+ managerExecutable: string | null;
11
+ channel: InstallationChannel;
12
+ edgePath: string | null;
13
+ checkOnStartup: boolean;
14
+ lastUpdateCheck: number | string | null;
15
+ updateCheckInterval: number;
16
+ recordedAt: string;
17
+ }
18
+ export interface InstallationStatus {
19
+ state: 'aligned' | 'mismatch' | 'stale' | 'unrecorded';
20
+ identity: InstallationIdentity | null;
21
+ canonicalRoot?: string;
22
+ actualRoot: string;
23
+ actualMethod: InstallationMethod;
24
+ /** Root AIWG reads its corpus from. Equals actualRoot unless an edge launcher redirect applies. */
25
+ frameworkRoot: string;
26
+ /** Edge/customize mode only: the running executable's own package root (#2505). */
27
+ launcher: { root: string; method: InstallationMethod } | null;
28
+ drift: string[];
29
+ managerProbe: { state: 'usable' | 'failed'; error?: string } | null;
30
+ }
31
+ export const INSTALLATION_IDENTITY_VERSION: 1;
32
+ export const INSTALLATION_FILE: string;
33
+ export function installationFile(options?: Record<string, unknown>): string;
34
+ export function inferInstallationMethod(root: string): InstallationMethod;
35
+ export function createInstallationIdentity(options: Record<string, any>): InstallationIdentity;
36
+ export function saveInstallationIdentity(identity: InstallationIdentity, options?: Record<string, any>): InstallationIdentity;
37
+ export function loadInstallationIdentity(options?: Record<string, any>): InstallationIdentity | null;
38
+ export function inspectInstallation(options: Record<string, any>): InstallationStatus;
39
+ export interface InstallationCopy {
40
+ root: string;
41
+ executables: string[];
42
+ onPath: boolean;
43
+ registered: boolean;
44
+ }
45
+ export function inspectInstallationCopies(options?: Record<string, any>): InstallationCopy[];
46
+ export function formatInstallationDiagnostic(status: InstallationStatus, options?: { readOnlyCommand?: string }): string;
47
+ export function assertCanonicalInstallation(options: Record<string, any>): InstallationStatus;
48
+ export function adoptInstallation(options: Record<string, any>): InstallationStatus;
49
+ export function switchInstallation(options: Record<string, any>): InstallationStatus;
@@ -1,16 +1,18 @@
1
- import { execFileSync } from 'node:child_process';
1
+ import { boundedExecFileSync } from '../process/bounded-child-process.mjs';
2
2
  import {
3
3
  accessSync,
4
4
  constants,
5
5
  existsSync,
6
6
  mkdirSync,
7
7
  readFileSync,
8
+ readdirSync,
8
9
  realpathSync,
9
10
  renameSync,
10
11
  statSync,
11
12
  writeFileSync,
12
13
  } from 'node:fs';
13
14
  import path from 'node:path';
15
+ import os from 'node:os';
14
16
  import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
15
17
  import { executeManagerCommand } from './manager-command.mjs';
16
18
 
@@ -50,7 +52,22 @@ function resolveExecutable(name, options = {}) {
50
52
  if (existsSync(besideNode)) return canonicalPath(besideNode);
51
53
  try {
52
54
  const finder = process.platform === 'win32' ? 'where.exe' : 'which';
53
- const found = execFileSync(finder, [name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
55
+ // Go/no-go review, blocker 1B: this runs unconditionally on first
56
+ // launch (creating the installation identity) for *every* command,
57
+ // including a credential-handling one — before `bin/aiwg.mjs` even
58
+ // knows which command that is. `which`/`where.exe` only need `PATH`
59
+ // (and, on Windows, `PATHEXT`/`SystemRoot` for its own resolution), so
60
+ // this never inherits the whole parent environment the way an absent
61
+ // `env` option otherwise would.
62
+ const lookupEnv = {
63
+ PATH: env.PATH ?? '',
64
+ ...(process.platform === 'win32' ? { PATHEXT: env.PATHEXT ?? '', SystemRoot: env.SystemRoot ?? '' } : {}),
65
+ };
66
+ const found = boundedExecFileSync(finder, [name], 'quick', {
67
+ encoding: 'utf8',
68
+ stdio: ['ignore', 'pipe', 'ignore'],
69
+ env: lookupEnv,
70
+ })
54
71
  .split(/\r?\n/, 1)[0]?.trim();
55
72
  return found ? canonicalPath(found) : null;
56
73
  } catch {
@@ -254,13 +271,86 @@ export function inspectInstallation(options = {}) {
254
271
  };
255
272
  }
256
273
 
257
- export function formatInstallationDiagnostic(status) {
274
+ /** Inspect executable copies without launching them or changing installation identity. */
275
+ export function inspectInstallationCopies(options = {}) {
276
+ const env = options.env ?? process.env;
277
+ const home = options.homeDir ?? os.homedir();
278
+ const identity = options.identity ?? loadInstallationIdentity({ ...options, createIfMissing: false });
279
+ const copies = new Map();
280
+ const names = process.platform === 'win32' ? ['aiwg', 'aiwg.cmd', 'aiwg.exe'] : ['aiwg'];
281
+
282
+ function addDirectory(directory, onPath) {
283
+ for (const name of names) {
284
+ const executable = path.resolve(directory, name);
285
+ if (!executableIsUsable(executable)) continue;
286
+ let root = path.dirname(canonicalPath(executable));
287
+ while (!['aiwg', '@aiwg/cli'].includes(packageName(root))) {
288
+ const parent = path.dirname(root);
289
+ if (parent === root) { root = null; break; }
290
+ root = parent;
291
+ }
292
+ // npm's Windows command shims are files rather than symlinks.
293
+ if (!root && process.platform === 'win32') {
294
+ const candidate = path.join(directory, 'node_modules', 'aiwg');
295
+ if (packageName(candidate) === 'aiwg') root = canonicalPath(candidate);
296
+ }
297
+ if (!root) continue;
298
+ let copy = copies.get(root);
299
+ if (!copy) {
300
+ const status = identity ? inspectInstallation({ actualRoot: root, identity, createIfMissing: false }) : null;
301
+ copy = {
302
+ root,
303
+ executables: [],
304
+ onPath: false,
305
+ registered: Boolean(status && status.frameworkRoot === status.canonicalRoot),
306
+ };
307
+ copies.set(root, copy);
308
+ }
309
+ if (!copy.executables.includes(executable)) copy.executables.push(executable);
310
+ copy.onPath ||= onPath;
311
+ }
312
+ }
313
+
314
+ const pathDirectories = (env.PATH ?? '').split(path.delimiter);
315
+ for (const directory of new Set(pathDirectories)) addDirectory(directory, true);
316
+
317
+ const nvmRoots = new Set([path.join(home, '.nvm'), env.NVM_DIR].filter(Boolean));
318
+ // A running/registered nvm copy or NVM_BIN also identifies custom nvm homes.
319
+ for (const location of [options.actualRoot, identity?.root, env.NVM_BIN, ...pathDirectories]) {
320
+ if (!location) continue;
321
+ let directory = path.resolve(location);
322
+ while (path.dirname(directory) !== directory) {
323
+ if (path.basename(directory) === 'node' && path.basename(path.dirname(directory)) === 'versions') {
324
+ nvmRoots.add(path.dirname(path.dirname(directory)));
325
+ break;
326
+ }
327
+ directory = path.dirname(directory);
328
+ }
329
+ }
330
+ for (const nvmRoot of nvmRoots) {
331
+ const versions = path.join(nvmRoot, 'versions', 'node');
332
+ try {
333
+ for (const version of readdirSync(versions, { withFileTypes: true })) {
334
+ if (version.isDirectory()) addDirectory(path.join(versions, version.name, 'bin'), false);
335
+ }
336
+ } catch { /* nvm is optional and may no longer be installed. */ }
337
+ }
338
+ return [...copies.values()];
339
+ }
340
+
341
+ export function formatInstallationDiagnostic(status, options = {}) {
258
342
  if (status.state === 'aligned') return 'Canonical installation is aligned.';
259
343
  return [
260
- 'AIWG installation identity drift detected; update and refresh are blocked.',
344
+ options.readOnlyCommand
345
+ ? `installation identity drift detected; \`${options.readOnlyCommand}\` is read-only and continues, but update, refresh, and deployment stay blocked until the identity is repaired.`
346
+ : 'AIWG installation identity drift detected; update and refresh are blocked.',
347
+ `Running installation: ${status.actualMethod} at ${status.actualRoot}`,
348
+ `Registered installation: ${status.identity?.method ?? 'unrecorded'} at ${status.identity?.root ?? '(none)'}`,
261
349
  ...status.drift.map((item) => `- ${item}`),
350
+ 'Diagnose PATH and nvm copies: aiwg doctor',
262
351
  'Inspect: aiwg installation show',
263
- 'Adopt this installation: aiwg installation adopt',
352
+ `Register this running copy deliberately: aiwg installation adopt --method ${status.actualMethod} --yes`,
353
+ 'To keep the registered install, put its executable first on PATH; remove unwanted npm copies using npm from their Node prefix (npm uninstall -g aiwg).',
264
354
  'Switch deliberately: aiwg installation switch --root <path> --method <npm|web|source> [--manager <absolute-path>]',
265
355
  ].join('\n');
266
356
  }
@@ -310,9 +310,9 @@ async function handleClose(issues, rest, args) {
310
310
  }
311
311
  async function handleIndex(issues, rest, args) {
312
312
  if (rest[0] !== 'rebuild')
313
- throw new Error('Usage: aiwg issue index rebuild');
314
- const index = await issues.rebuildIssueIndex();
315
- printJsonOrText(args, index, `Rebuilt local issue index (${index.issues.length} issues)`);
313
+ throw new Error('Usage: aiwg issue index rebuild [--force]');
314
+ const index = await issues.rebuildIssueIndex({ force: args.flags.has('force') });
315
+ printJsonOrText(args, index, `Local issue index ready (${index.issues.length} issues)`);
316
316
  }
317
317
  function requiredStringFlag(args, flag) {
318
318
  const value = stringFlag(args, flag);
@@ -406,6 +406,6 @@ function printUsage() {
406
406
  aiwg issue sync map-comments <KEY> --map-file path [--json]
407
407
  aiwg issue comment <KEY> --body "..." [--author name]
408
408
  aiwg issue close <KEY> [--reason "..."]
409
- aiwg issue index rebuild`);
409
+ aiwg issue index rebuild [--force]`);
410
410
  }
411
411
  //# sourceMappingURL=cli.js.map
@@ -1,5 +1,6 @@
1
1
  import { constants } from 'fs';
2
- import { mkdir, open, readFile, readdir, rename, unlink, writeFile } from 'fs/promises';
2
+ import { mkdir, open, readFile, readdir, rename, stat, unlink, writeFile } from 'fs/promises';
3
+ import { createHash } from 'node:crypto';
3
4
  import { basename, dirname, join, relative, resolve, sep } from 'path';
4
5
  import { hostname } from 'os';
5
6
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
@@ -328,15 +329,32 @@ export class LocalIssueProviderCore {
328
329
  const idx = events.findIndex((event) => event.event_id === since);
329
330
  return idx >= 0 ? events.slice(idx + 1) : events;
330
331
  }
331
- async rebuildIssueIndex() {
332
+ async rebuildIssueIndex(options = {}) {
332
333
  await this.ensureLayout();
333
334
  return this.lockManager.withIndexLock('rebuild issue index', async () => {
334
335
  const itemDir = this.path('items');
335
- const entries = await readdir(itemDir, { withFileTypes: true }).catch(() => []);
336
+ const entries = (await readdir(itemDir, { withFileTypes: true }))
337
+ .filter(entry => entry.isFile() && entry.name.endsWith('.md'))
338
+ .sort((a, b) => a.name.localeCompare(b.name));
339
+ const inputs = await Promise.all(entries.map(async (entry) => {
340
+ const metadata = await stat(join(itemDir, entry.name));
341
+ return [entry.name, metadata.mtimeMs, metadata.ctimeMs, metadata.size];
342
+ }));
343
+ const inputFingerprint = createHash('sha256').update(JSON.stringify([1, inputs])).digest('hex');
344
+ if (!options.force) {
345
+ try {
346
+ const cached = JSON.parse(await readFile(this.path('index/issues.index.json'), 'utf-8'));
347
+ if (cached?.provider === 'local' && Array.isArray(cached.issues) && cached.inputFingerprint === inputFingerprint) {
348
+ return cached;
349
+ }
350
+ }
351
+ catch (error) {
352
+ if (!(error instanceof SyntaxError) && !(isNodeError(error) && error.code === 'ENOENT'))
353
+ throw error;
354
+ }
355
+ }
336
356
  const issues = [];
337
357
  for (const entry of entries) {
338
- if (!entry.isFile() || !entry.name.endsWith('.md'))
339
- continue;
340
358
  const record = parseIssueMarkdown(await readFile(join(itemDir, entry.name), 'utf-8'));
341
359
  issues.push({
342
360
  id: record.fields.id,
@@ -355,6 +373,7 @@ export class LocalIssueProviderCore {
355
373
  const index = {
356
374
  provider: 'local',
357
375
  rebuilt_at: new Date().toISOString(),
376
+ inputFingerprint,
358
377
  issues,
359
378
  };
360
379
  await atomicWrite(this.path('index/issues.index.json'), `${JSON.stringify(index, null, 2)}\n`);
@@ -184,7 +184,7 @@ async function acquireLock(stateRoot, flow) {
184
184
  const lock = JSON.parse(await fs.readFile(file, 'utf8'));
185
185
  const tooOld = !lock.startedAt || Date.now() - Date.parse(lock.startedAt) > (flow.spec.workItem.claimTtlSeconds ?? 900) * 1000;
186
186
  let alive = false;
187
- if (typeof lock.pid === 'number') {
187
+ if (Number.isSafeInteger(lock.pid) && (lock.pid ?? 0) > 0) {
188
188
  try {
189
189
  process.kill(lock.pid, 0);
190
190
  alive = true;
@@ -13,6 +13,7 @@
13
13
  * @issue #810
14
14
  */
15
15
  import fs from 'fs';
16
+ import { readAiwgConfigCandidate } from '../config/aiwg-config.js';
16
17
  import path from 'path';
17
18
  import { fileURLToPath } from 'url';
18
19
  import { discoverRulesets } from './loader.js';
@@ -71,9 +72,15 @@ function parseArgs(args) {
71
72
  else if (arg === '--format' && i + 1 < args.length) {
72
73
  options.format = args[++i];
73
74
  }
75
+ else if (arg === '--startup-plan' && i + 1 < args.length) {
76
+ options.startupPlan = args[++i];
77
+ }
74
78
  else if (arg === '--ci') {
75
79
  options.ci = true;
76
80
  }
81
+ else if (arg === '--allow-empty') {
82
+ options.allowEmpty = true;
83
+ }
77
84
  else if (arg === '--fail-on' && i + 1 < args.length) {
78
85
  options.failOn = args[++i];
79
86
  }
@@ -102,6 +109,14 @@ function parseArgs(args) {
102
109
  export async function main(args) {
103
110
  const options = parseArgs(args);
104
111
  const cwd = process.cwd();
112
+ if (args.includes('--help') || args.includes('-h')) {
113
+ console.log('Usage: aiwg lint <target> [--ruleset <name>] [--format full|summary|json] [--ci] [--allow-empty] [--fail-on error|warn|info] [--no-gitignore]');
114
+ console.log(' --ci Fail on lint violations, zero files checked or zero rules applied');
115
+ console.log(' --allow-empty Allow zero files or zero applied rules under --ci');
116
+ console.log(' --startup-plan <json> Guard a complete projected startup set { provider, files: [{ path, bytes | content }] }');
117
+ console.log(' --list-rulesets | --list-rules <ruleset>');
118
+ return;
119
+ }
105
120
  // Discover available rulesets. `cwd` is where the user runs the lint (used
106
121
  // to find deployed `.aiwg/frameworks/*/lint/` overlays); `AIWG_ROOT` is the
107
122
  // AIWG install (used to find shipped `agentic/code/frameworks/*/lint/`).
@@ -126,7 +141,7 @@ export async function main(args) {
126
141
  }
127
142
  // Require a target for actual linting
128
143
  if (!options.target) {
129
- console.error('Usage: aiwg lint <target> [--ruleset <name>] [--format full|summary|json] [--ci] [--fail-on error|warn|info] [--no-gitignore]');
144
+ console.error('Usage: aiwg lint <target> [--ruleset <name>] [--format full|summary|json] [--ci] [--allow-empty] [--fail-on error|warn|info] [--no-gitignore]');
130
145
  console.error(' aiwg lint --list-rulesets');
131
146
  console.error(' aiwg lint --list-rules <ruleset>');
132
147
  process.exitCode = 1;
@@ -159,9 +174,20 @@ export async function main(args) {
159
174
  }
160
175
  return;
161
176
  }
177
+ let startupBudget;
178
+ if (options.startupPlan) {
179
+ const plan = JSON.parse(fs.readFileSync(path.resolve(cwd, options.startupPlan), 'utf8'));
180
+ if (!plan || typeof plan.provider !== 'string' || !Array.isArray(plan.files)) {
181
+ throw new Error('Startup plan must contain provider and files (the complete assembled startup set).');
182
+ }
183
+ startupBudget = { provider: plan.provider, files: plan.files, config: await readAiwgConfigCandidate(cwd) ?? {} };
184
+ }
162
185
  // Run lint
163
186
  const result = await runLint(targetDir, selectedRulesets, {
187
+ startupBudget,
164
188
  recursive: options.recursive,
189
+ ci: options.ci,
190
+ allowEmpty: options.allowEmpty,
165
191
  failOn: options.failOn,
166
192
  respectGitignore: options.respectGitignore ?? true,
167
193
  });
@@ -8,6 +8,7 @@
8
8
  * @issue #810
9
9
  */
10
10
  import fs from 'fs';
11
+ import { ruleActivationRuleset } from './rule-activation.js';
11
12
  import fsp from 'fs/promises';
12
13
  import path from 'path';
13
14
  import { projectAiwgPath } from '../config/project-artifacts.js';
@@ -178,7 +179,7 @@ export async function loadRuleset(lintDir, frameworkId) {
178
179
  * 2. Deployed frameworks: .aiwg/frameworks/{id}/lint/
179
180
  */
180
181
  export async function discoverRulesets(cwd, frameworkRoot) {
181
- const rulesets = [];
182
+ const rulesets = [ruleActivationRuleset];
182
183
  // Check framework source directories
183
184
  const frameworksDir = path.join(frameworkRoot, 'agentic', 'code', 'frameworks');
184
185
  if (fs.existsSync(frameworksDir)) {