@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
@@ -0,0 +1,38 @@
1
+ /** Shared runtime resolver for config consumers and direct preflight tools. */
2
+ export const DEFAULT_DISPATCH_HOST_THRESHOLDS = Object.freeze({
3
+ min_avail_mem_gb: 8,
4
+ max_load_ratio: 1.0,
5
+ max_mem_psi_avg10: 30,
6
+ max_mem_psi_avg60: 10,
7
+ // 08 brief/evidence retention: stop new admissions when less than 2 GiB free remains.
8
+ min_free_disk_gb: 2,
9
+ });
10
+
11
+ /**
12
+ * Configured layers can only tighten one another and the conservative defaults;
13
+ * minimums take MAX and maximums take MIN across build, admission and task.
14
+ * Validate each layer before merging so an invalid value cannot be hidden.
15
+ */
16
+ export function resolveDispatchHostThresholds(buildDispatch, admissionHostResources, overlay) {
17
+ const configured = { ...DEFAULT_DISPATCH_HOST_THRESHOLDS };
18
+ const keys = [...Object.keys(DEFAULT_DISPATCH_HOST_THRESHOLDS), 'max_workers'];
19
+ for (const layer of [buildDispatch, admissionHostResources, overlay]) {
20
+ if (layer === undefined) continue;
21
+ if (layer === null || typeof layer !== 'object' || Array.isArray(layer)) {
22
+ throw new Error('OG_CONFIG_REFERENCE dispatch thresholds: must be an object');
23
+ }
24
+ for (const key of keys) {
25
+ const value = layer[key];
26
+ if (value === undefined) continue;
27
+ if (!Number.isFinite(value) || value < 0) {
28
+ throw new Error(`OG_CONFIG_REFERENCE dispatch threshold ${key}: must be a non-negative number`);
29
+ }
30
+ if (key === 'max_workers' && (!Number.isInteger(value) || value < 1)) {
31
+ throw new Error('OG_CONFIG_REFERENCE dispatch threshold max_workers: must be an integer >= 1 when set');
32
+ }
33
+ configured[key] = configured[key] === undefined ? value
34
+ : key.startsWith('min_') ? Math.max(configured[key], value) : Math.min(configured[key], value);
35
+ }
36
+ }
37
+ return { ...DEFAULT_DISPATCH_HOST_THRESHOLDS, ...configured };
38
+ }
@@ -8,9 +8,11 @@
8
8
  * @module src/config/gitignore
9
9
  */
10
10
  import * as fs from 'fs/promises';
11
+ import { existsSync, realpathSync } from 'fs';
11
12
  import * as path from 'path';
12
13
  import { execFile } from 'child_process';
13
14
  import { promisify } from 'util';
15
+ import { boundedSpawnSync } from '../process/bounded-child-process.mjs';
14
16
  const execFileAsync = promisify(execFile);
15
17
  const GITIGNORE_PROBE_BASENAME = '.aiwg-ignore-probe';
16
18
  // ===========================
@@ -205,4 +207,52 @@ export async function appendGitignore(projectRoot, patterns) {
205
207
  created: !fileExists && toAdd.length > 0,
206
208
  };
207
209
  }
210
+ /**
211
+ * Drop files git ignores (#2555).
212
+ *
213
+ * Shared by lint and artifact discovery. `git check-ignore` is asked once for
214
+ * the whole set so the semantics are git's own (negations, nested
215
+ * `.gitignore`, `core.excludesFile`) rather than a re-implementation. Outside a
216
+ * repository, or without git, every file is kept. NUL delimiters preserve
217
+ * whitespace and quoting in filenames.
218
+ */
219
+ export function dropGitignored(targetDir, files) {
220
+ if (files.length === 0)
221
+ return files;
222
+ try {
223
+ // Avoid starting Git for synthetic/source trees outside a repository. Resolve
224
+ // symlinked working directories just as Git does, and probe on every call so
225
+ // initializing a repository between scans takes effect immediately. Explicit
226
+ // Git environment and bare repositories still use Git's own discovery.
227
+ if (!process.env.GIT_DIR && !process.env.GIT_WORK_TREE && !process.env.GIT_COMMON_DIR) {
228
+ let directory = realpathSync(targetDir);
229
+ for (;;) {
230
+ if (existsSync(path.join(directory, '.git')) ||
231
+ (existsSync(path.join(directory, 'HEAD')) &&
232
+ (existsSync(path.join(directory, 'objects')) || existsSync(path.join(directory, 'commondir')))))
233
+ break;
234
+ const parent = path.dirname(directory);
235
+ if (parent === directory)
236
+ return files;
237
+ directory = parent;
238
+ }
239
+ }
240
+ const result = boundedSpawnSync('git', ['check-ignore', '-z', '--stdin'], 'quick', {
241
+ cwd: targetDir,
242
+ input: `${files.join('\0')}\0`,
243
+ encoding: 'utf8',
244
+ });
245
+ // 0 = some paths ignored, 1 = none ignored, anything else = not a repo or
246
+ // git unavailable, in which case nothing is filtered.
247
+ if (result.error || (result.status !== 0 && result.status !== 1))
248
+ return files;
249
+ const ignored = new Set((result.stdout ?? '').split('\0').filter(Boolean));
250
+ if (ignored.size === 0)
251
+ return files;
252
+ return files.filter((file) => !ignored.has(file));
253
+ }
254
+ catch {
255
+ return files;
256
+ }
257
+ }
208
258
  //# sourceMappingURL=gitignore.js.map
@@ -0,0 +1,36 @@
1
+ export const OVERRIDABLE_INDEX_GRAPHS = ['project', 'codebase'];
2
+ /** Shared by config validation and graph loading, including non-CLI callers. */
3
+ export function validateIndexGraphOverrides(overrides) {
4
+ if (overrides === undefined)
5
+ return [];
6
+ if (typeof overrides !== 'object' || overrides === null || Array.isArray(overrides)) {
7
+ return ['index.graphOverrides: must be an object mapping supported built-in graph names to overrides'];
8
+ }
9
+ const errors = [];
10
+ for (const [name, raw] of Object.entries(overrides)) {
11
+ const where = `index.graphOverrides.${name}`;
12
+ if (!OVERRIDABLE_INDEX_GRAPHS.includes(name)) {
13
+ errors.push(`${where}: unsupported built-in graph override (supported: ${OVERRIDABLE_INDEX_GRAPHS.join(', ')})`);
14
+ continue;
15
+ }
16
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
17
+ errors.push(`${where}: must be an object`);
18
+ continue;
19
+ }
20
+ const override = raw;
21
+ for (const field of Object.keys(override)) {
22
+ if (field !== 'scanDirs' && field !== 'extensions') {
23
+ errors.push(`${where}.${field}: unknown field (supported: scanDirs, extensions)`);
24
+ }
25
+ }
26
+ for (const field of ['scanDirs', 'extensions']) {
27
+ const value = override[field];
28
+ if (value !== undefined && (!Array.isArray(value) || value.length === 0 ||
29
+ !value.every(item => typeof item === 'string' && item.trim().length > 0))) {
30
+ errors.push(`${where}.${field}: must be a non-empty array of strings`);
31
+ }
32
+ }
33
+ }
34
+ return errors;
35
+ }
36
+ //# sourceMappingURL=index-graph-overrides.js.map
@@ -0,0 +1,37 @@
1
+ export type ProjectArtifactHealthClassification =
2
+ | 'local'
3
+ | 'healthy-split-root'
4
+ | 'degraded-offline'
5
+ | 'legacy-missing-control-plane'
6
+ | 'duplicated-identical'
7
+ | 'duplicated-divergent';
8
+
9
+ export interface ProjectControlFileHealth {
10
+ file: string;
11
+ local: boolean;
12
+ external: boolean;
13
+ identical: boolean | null;
14
+ }
15
+
16
+ export interface ProjectArtifactHealth {
17
+ classification: ProjectArtifactHealthClassification;
18
+ severity: 'ok' | 'warning' | 'error';
19
+ external_configured: boolean;
20
+ source: { kind: string; key?: string; path?: string };
21
+ local_control_root: string;
22
+ artifact_root: string;
23
+ external_reachable: boolean;
24
+ control_files: ProjectControlFileHealth[];
25
+ missing_local_control_files: string[];
26
+ divergent_control_files: string[];
27
+ duplicated_local_corpus_files: string[];
28
+ divergent_local_corpus_files: string[];
29
+ repairable: boolean;
30
+ action: string | null;
31
+ }
32
+
33
+ export const PROJECT_CONTROL_PLANE_FILES: readonly string[];
34
+ export function auditProjectArtifactHealth(
35
+ projectDir: string,
36
+ env?: Record<string, string | undefined>,
37
+ ): ProjectArtifactHealth;
@@ -10,6 +10,7 @@ import { join, relative, resolve, sep } from 'node:path';
10
10
  import {
11
11
  DEFAULT_PROJECT_AIWG_DIR,
12
12
  PROJECT_AIWG_LOCATION_FILE,
13
+ isProjectArtifactRootExternal,
13
14
  readProjectArtifactLocation,
14
15
  resolveProjectAiwgDir,
15
16
  } from './project-artifacts-runtime.mjs';
@@ -57,8 +58,16 @@ export function auditProjectArtifactHealth(projectDir, env = process.env) {
57
58
  const localRoot = join(root, DEFAULT_PROJECT_AIWG_DIR);
58
59
  const artifactRoot = resolveProjectAiwgDir(root, env);
59
60
  const source = configuredSource(root, env);
60
- const externalConfigured = resolve(localRoot) !== resolve(artifactRoot);
61
- const externalReachable = existsSync(artifactRoot) && statSync(artifactRoot).isDirectory();
61
+ // Cheap, read-only check for whether an external root is configured at all
62
+ // — reused so repair's early refusal (src/artifacts/repair.ts) and this
63
+ // audit agree on the same definition of "local mode" (#274).
64
+ const externalConfigured = isProjectArtifactRootExternal(root, env);
65
+ // In local mode (no external root configured) artifactRoot === localRoot:
66
+ // there is nothing external to reach or compare against. Skip the
67
+ // reachability stat and the full-corpus listing/byte-compare entirely —
68
+ // previously this read and byte-compared every local payload file with
69
+ // itself on every `aiwg status`/`doctor`/`repair` call (#274).
70
+ const externalReachable = externalConfigured && existsSync(artifactRoot) && statSync(artifactRoot).isDirectory();
62
71
 
63
72
  const controls = PROJECT_CONTROL_PLANE_FILES.map((file) => {
64
73
  const localPath = join(localRoot, file);
@@ -69,7 +78,9 @@ export function auditProjectArtifactHealth(projectDir, env = process.env) {
69
78
  });
70
79
  const missingLocal = controls.filter((item) => !item.local).map((item) => item.file);
71
80
  const divergentControl = controls.filter((item) => item.identical === false).map((item) => item.file);
72
- const localPayload = listFiles(localRoot).filter((file) => !PROJECT_CONTROL_PLANE_FILES.includes(file));
81
+ const localPayload = externalConfigured
82
+ ? listFiles(localRoot).filter((file) => !PROJECT_CONTROL_PLANE_FILES.includes(file))
83
+ : [];
73
84
  const divergentPayload = externalReachable
74
85
  ? localPayload.filter((file) => !sameFile(join(localRoot, file), join(artifactRoot, file)))
75
86
  : [];
@@ -0,0 +1,16 @@
1
+ export const DEFAULT_PROJECT_AIWG_DIR: '.aiwg';
2
+ export const AIWG_ARTIFACTS_PATH_ENV: 'AIWG_ARTIFACTS_PATH';
3
+ export const PROJECT_AIWG_LOCATION_FILE: '.aiwg-location';
4
+
5
+ export type ProjectArtifactEnv = Record<string, string | undefined>;
6
+
7
+ export function expandProjectArtifactPath(pathValue: string, projectDir: string): string;
8
+ export function parseProjectArtifactLocation(contents: string): string | null;
9
+ export function readProjectArtifactLocation(projectDir: string): string | null;
10
+ export function resolveProjectAiwgDir(projectDir: string, env?: ProjectArtifactEnv): string;
11
+ export function isProjectArtifactRootExternal(projectDir: string, env?: ProjectArtifactEnv): boolean;
12
+ export function resolveProjectAiwgDirForWrite(projectDir: string, env?: ProjectArtifactEnv): string;
13
+ export function resolveProjectControlDir(projectDir: string): string;
14
+ export function projectAiwgPath(projectDir: string, ...segments: string[]): string;
15
+ export function projectAiwgWritePath(projectDir: string, ...segments: string[]): string;
16
+ export function projectControlPath(projectDir: string, ...segments: string[]): string;
@@ -0,0 +1,9 @@
1
+ export interface UserConfigDirOptions {
2
+ configDir?: string;
3
+ env?: Record<string, string | undefined>;
4
+ homeDir?: string;
5
+ exists?: (path: string) => boolean;
6
+ }
7
+
8
+ export function resolveUserConfigDir(options?: UserConfigDirOptions): string;
9
+ export function userConfigFile(name: string, options?: UserConfigDirOptions): string;
@@ -24,6 +24,7 @@ import { resolveUserConfigDir } from './user-config-dir.mjs';
24
24
  */
25
25
  export const KNOWN_CONFIG_FILES = [
26
26
  { filename: 'config.yaml', description: 'Global AIWG user preferences' },
27
+ { filename: 'aiwg.config', description: 'Operator rule pins and capability groups' },
27
28
  { filename: 'models.json', description: 'Model tier and provider overrides' },
28
29
  { filename: 'ops.json', description: 'Ops workspace registry' },
29
30
  { filename: 'mcp-servers.json', description: 'MCP server registry (single source of truth)' },
@@ -122,7 +123,7 @@ export class UserConfig {
122
123
  try {
123
124
  await access(filePath);
124
125
  const content = await readFile(filePath, 'utf-8');
125
- if (spec.filename.endsWith('.json')) {
126
+ if (spec.filename.endsWith('.json') || spec.filename === 'aiwg.config') {
126
127
  result[spec.filename] = JSON.parse(content);
127
128
  }
128
129
  else {
@@ -245,10 +246,8 @@ export class UserConfig {
245
246
  if (this.configCache) {
246
247
  return this.configCache;
247
248
  }
248
- const configPath = resolve(this.configDir, 'config.yaml');
249
- try {
250
- const content = await readFile(configPath, 'utf-8');
251
- const parsed = parseYamlSimple(content);
249
+ const parsed = await this.readConfig();
250
+ if (parsed) {
252
251
  this.configCache = {
253
252
  ...DEFAULT_USER_CONFIG,
254
253
  ...parsed,
@@ -259,7 +258,7 @@ export class UserConfig {
259
258
  };
260
259
  return this.configCache;
261
260
  }
262
- catch {
261
+ else {
263
262
  // File doesn't exist — return defaults (don't create file yet)
264
263
  this.configCache = {
265
264
  ...DEFAULT_USER_CONFIG,
@@ -271,10 +270,24 @@ export class UserConfig {
271
270
  return this.configCache;
272
271
  }
273
272
  }
273
+ async readConfig() {
274
+ const configPath = resolve(this.configDir, 'config.yaml');
275
+ try {
276
+ const content = await readFile(configPath, 'utf-8');
277
+ return parseYamlSimple(content);
278
+ }
279
+ catch (err) {
280
+ if (err.code === 'ENOENT')
281
+ return null;
282
+ throw new Error(`Failed to load ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
283
+ }
284
+ }
274
285
  /**
275
286
  * Save config.yaml
276
287
  */
277
288
  async saveConfig(config) {
289
+ // Recheck the existing file even when defaults or cached values are being saved.
290
+ await this.readConfig();
278
291
  await this.ensureDir();
279
292
  const configPath = resolve(this.configDir, 'config.yaml');
280
293
  const yaml = serializeYamlSimple(config);
@@ -304,11 +317,15 @@ export function parseYamlSimple(content) {
304
317
  continue;
305
318
  const key = match[1].trim();
306
319
  const rawValue = match[2].trim();
320
+ // Config keys have no legitimate use for prototype-related names at any level.
321
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
322
+ throw new Error('Unsafe config key: prototype-related names are not allowed');
323
+ }
307
324
  if (indent === 0) {
308
325
  if (rawValue === '' || rawValue === undefined) {
309
326
  // Section header
310
327
  currentSection = key;
311
- if (!(key in result)) {
328
+ if (!Object.hasOwn(result, key)) {
312
329
  result[key] = {};
313
330
  }
314
331
  }
@@ -375,7 +392,7 @@ function getNestedValue(obj, path) {
375
392
  const parts = path.split('.');
376
393
  let current = obj;
377
394
  for (const part of parts) {
378
- if (current === null || current === undefined || typeof current !== 'object') {
395
+ if (current === null || current === undefined || typeof current !== 'object' || !Object.hasOwn(current, part)) {
379
396
  return undefined;
380
397
  }
381
398
  current = current[part];
@@ -384,15 +401,25 @@ function getNestedValue(obj, path) {
384
401
  }
385
402
  function setNestedValue(obj, path, value) {
386
403
  const parts = path.split('.');
404
+ // Reject the entire path before mutating config; key uses below also guard each segment.
405
+ if (parts.some(part => part === '__proto__' || part === 'constructor' || part === 'prototype')) {
406
+ throw new Error('Unsafe config key path: prototype-related segments are not allowed');
407
+ }
387
408
  let current = obj;
388
- for (let i = 0; i < parts.length - 1; i++) {
409
+ for (let i = 0; i < parts.length; i++) {
389
410
  const part = parts[i];
390
- if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
411
+ if (part === '__proto__' || part === 'constructor' || part === 'prototype') {
412
+ throw new Error('Unsafe config key path: prototype-related segments are not allowed');
413
+ }
414
+ if (i === parts.length - 1) {
415
+ current[part] = value;
416
+ return;
417
+ }
418
+ if (!Object.hasOwn(current, part) || typeof current[part] !== 'object' || current[part] === null) {
391
419
  current[part] = {};
392
420
  }
393
421
  current = current[part];
394
422
  }
395
- current[parts[parts.length - 1]] = value;
396
423
  }
397
424
  function parseValue(value) {
398
425
  if (value === 'true')
@@ -12,7 +12,6 @@
12
12
  * --scope user` works from any cwd.
13
13
  */
14
14
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
15
- import { existsSync } from 'node:fs';
16
15
  import { dirname } from 'node:path';
17
16
  import { userScopeConfigPath } from '../cli/scope-resolver.js';
18
17
  /**
@@ -46,28 +45,37 @@ export function emptyUserRegistry() {
46
45
  return { version: '1', installed: {} };
47
46
  }
48
47
  /**
49
- * Read the per-user registry. Returns an empty registry when the file doesn't
50
- * exist — that's the normal case before the operator's first `aiwg use --scope
51
- * user`. On parse failure returns the empty registry and writes a stderr
52
- * warning (the broken file is preserved so the operator can inspect it).
48
+ * Read the user registry while preserving whether empty means absent or
49
+ * unreadable. Callers making adoption/readiness claims must not collapse a
50
+ * corrupt or inaccessible registry into proof that an addon is missing.
53
51
  */
54
- export async function readUserRegistry() {
55
- const path = userRegistryPath();
56
- if (!existsSync(path))
57
- return emptyUserRegistry();
52
+ export async function inspectUserRegistry() {
53
+ const registryFile = userRegistryPath();
58
54
  try {
59
- const raw = await readFile(path, 'utf-8');
55
+ const raw = await readFile(registryFile, 'utf-8');
60
56
  const parsed = JSON.parse(raw);
61
- if (!parsed.installed || typeof parsed.installed !== 'object') {
62
- return emptyUserRegistry();
57
+ if (!parsed.installed || typeof parsed.installed !== 'object' || Array.isArray(parsed.installed)) {
58
+ throw new Error('installed must be an object');
63
59
  }
64
- return { version: '1', installed: parsed.installed };
60
+ return { registry: { version: '1', installed: parsed.installed }, status: 'available' };
65
61
  }
66
62
  catch (err) {
67
- process.stderr.write(`[user-registry] failed to parse ${path}: ${err instanceof Error ? err.message : String(err)} — using empty registry\n`);
68
- return emptyUserRegistry();
63
+ if (err.code === 'ENOENT') {
64
+ return { registry: emptyUserRegistry(), status: 'absent' };
65
+ }
66
+ process.stderr.write(`[user-registry] failed to parse ${registryFile}: ${err instanceof Error ? err.message : String(err)} — using empty registry\n`);
67
+ return { registry: emptyUserRegistry(), status: 'unreadable' };
69
68
  }
70
69
  }
70
+ /**
71
+ * Read the per-user registry. Returns an empty registry when the file doesn't
72
+ * exist — that's the normal case before the operator's first `aiwg use --scope
73
+ * user`. On parse failure returns the empty registry and writes a stderr
74
+ * warning (the broken file is preserved so the operator can inspect it).
75
+ */
76
+ export async function readUserRegistry() {
77
+ return (await inspectUserRegistry()).registry;
78
+ }
71
79
  /**
72
80
  * Persist the user registry. Creates the parent directory if missing.
73
81
  */
@@ -0,0 +1,7 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ /** Host-local AIWG state, shared by CLI telemetry and maintenance leases. */
4
+ export function getUserStateDir(env = process.env) {
5
+ return path.join(env.XDG_STATE_HOME || path.join(env.HOME || os.homedir(), '.local', 'state'), 'aiwg');
6
+ }
7
+ //# sourceMappingURL=user-state.js.map
@@ -2,7 +2,6 @@ import { DecisionValidationError, validateDecisionValue, validateDistribution }
2
2
  import { canonicalJson } from '../../security/artifact-trust.js';
3
3
  import { partitionProjectedState } from '../projection.js';
4
4
  import { isTraceparent } from '../telemetry/context.js';
5
- import { runInNewContext } from 'node:vm';
6
5
  import { lookup } from 'node:dns/promises';
7
6
  import { isIP } from 'node:net';
8
7
  import { request as httpsRequest } from 'node:https';
@@ -12,7 +11,7 @@ export const JEV_ENDPOINT = 'https://api.typesafe.ai/v1/systemone';
12
11
  const MAX_HEADER_BYTES = 32 * 1024;
13
12
  const MAX_BODY_BYTES = 1024 * 1024;
14
13
  const MAX_REQUEST_ID_LENGTH = 128;
15
- const MAX_PARSE_MS = 50;
14
+ const MAX_PARSE_DEPTH = 64;
16
15
  /** Safe resolver taxonomy; messages are intentionally discarded at this boundary. */
17
16
  export class JevCredentialError extends Error {
18
17
  category;
@@ -45,6 +44,7 @@ export class JevDecisionAdapter {
45
44
  this.region = options.region?.trim() ? options.region : null;
46
45
  this.observeResponseBody = options.observeResponseBody;
47
46
  }
47
+ /** Used only for non-2xx error bodies, where there is no separate successful parse to stay consistent with. */
48
48
  observeResponse(body) {
49
49
  if (!this.observeResponseBody)
50
50
  return;
@@ -61,6 +61,30 @@ export class JevDecisionAdapter {
61
61
  this.observeResponseBody(body, { actualModel: typeof parsed.model === 'string' && parsed.model ? parsed.model : null,
62
62
  usage });
63
63
  }
64
+ /**
65
+ * Derives the observer's model/usage from a value `parseBoundedJson` has already produced (or `undefined`
66
+ * on a failed parse) -- never from a second, independent parse of the same bytes. `parseBoundedJson` runs
67
+ * inside a bounded-time VM context (`MAX_PARSE_MS`); calling it twice on identical bytes let one call
68
+ * succeed while the other spuriously hit that wall-clock bound under host contention, so the reported
69
+ * metadata could disagree with the actual parsed answer even though both described one response.
70
+ */
71
+ observationMetadataOf(parsed) {
72
+ let record = {};
73
+ try {
74
+ record = asRecord(parsed);
75
+ }
76
+ catch { /* A non-object (or absent) top-level value has no model; usage stays unknown. */ }
77
+ let usage = emptyUsage();
78
+ try {
79
+ usage = normalizeUsage(record.usage);
80
+ }
81
+ catch { /* Missing usage is unknown, including on auth errors. */ }
82
+ return { actualModel: typeof record.model === 'string' && record.model ? record.model : null, usage };
83
+ }
84
+ /** Observes exactly once, from the single parse attempt `parsed` already represents (successful or not). */
85
+ observeParsed(body, parsed) {
86
+ this.observeResponseBody?.(body, this.observationMetadataOf(parsed));
87
+ }
64
88
  async capabilities() {
65
89
  return {
66
90
  answerKinds: ['choice', 'ordinal-score', 'truth-probability'],
@@ -191,9 +215,7 @@ export class JevDecisionAdapter {
191
215
  try {
192
216
  parsedError = parseBoundedJson(errorBody);
193
217
  }
194
- catch (error) {
195
- if (error && typeof error === 'object' && 'code' in error && error.code === 'ERR_SCRIPT_EXECUTION_TIMEOUT')
196
- throw error;
218
+ catch {
197
219
  parsedError = null;
198
220
  }
199
221
  if (parsedError && typeof parsedError === 'object' && !Array.isArray(parsedError)) {
@@ -217,8 +239,15 @@ export class JevDecisionAdapter {
217
239
  let parsed;
218
240
  try {
219
241
  const encoded = await readBoundedBody(response, signal);
220
- this.observeResponse(encoded);
221
- parsed = parseBoundedJson(encoded);
242
+ // One parse attempt; the observer reads its outcome (success or failure) before answer validation,
243
+ // so an invalid answer still gets model/usage checked, and a parse failure is reported as unknown
244
+ // to both the observer and the main path -- never divergently.
245
+ try {
246
+ parsed = parseBoundedJson(encoded);
247
+ }
248
+ finally {
249
+ this.observeParsed(encoded, parsed);
250
+ }
222
251
  if (request.signal.aborted)
223
252
  return externalInterruption(request, 'terminal-response', metadata);
224
253
  if (deadline.aborted)
@@ -334,8 +363,14 @@ export class JevDecisionAdapter {
334
363
  }
335
364
  try {
336
365
  const encoded = await readBoundedBody(response, signal);
337
- this.observeResponse(encoded);
338
- const parsed = parseBoundedJson(encoded);
366
+ // One parse attempt; see the single-request `evaluate()` for why observation never re-parses.
367
+ let parsed;
368
+ try {
369
+ parsed = parseBoundedJson(encoded);
370
+ }
371
+ finally {
372
+ this.observeParsed(encoded, parsed);
373
+ }
339
374
  const normalized = normalizeBatchResponse(requests, parsed).map(answer => ({
340
375
  questionId: answer.questionId,
341
376
  observation: { ...answer.observation, ...metadata },
@@ -574,19 +609,21 @@ function safeRequestId(value) {
574
609
  return typeof value === 'string' && value.length > 0 && value.length <= MAX_REQUEST_ID_LENGTH
575
610
  && /^[\x21-\x7e]+$/.test(value) && !value.includes(',') ? value : null;
576
611
  }
577
- function parseBoundedJson(text) {
578
- rejectDuplicateAnswerKeys(text);
579
- return structuredClone(runInNewContext('JSON.parse(input)', { input: text }, { timeout: MAX_PARSE_MS }));
580
- }
581
- /** Detect duplicate keys in the response's answer map before JSON.parse erases them. */
582
- function rejectDuplicateAnswerKeys(text) {
583
- const marker = /"answers"\s*:\s*\{/g.exec(text);
584
- if (!marker)
585
- return;
586
- let index = marker.index + marker[0].length;
587
- let depth = 1;
588
- const keys = new Set();
589
- while (index < text.length && depth > 0) {
612
+ /** @internal Exported from this module for direct admission and CPU-cost evidence, not the public barrel. */
613
+ export function parseBoundedJson(text) {
614
+ // Bound work, not elapsed time: a VM timeout includes host scheduling delay.
615
+ // The stream reader also enforces this bound, before UTF-8 decoding.
616
+ if (text.length > MAX_BODY_BYTES || Buffer.byteLength(text, 'utf8') > MAX_BODY_BYTES) {
617
+ throw new DecisionValidationError('Jev response too large');
618
+ }
619
+ // Scan iteratively before allocation by JSON.parse. Total key text/count is
620
+ // bounded by body bytes; no recursive walk or suffix copying is needed.
621
+ // String/key spans are disjoint; skip the whitespace consumed by lookahead.
622
+ // Each code unit is visited once. Key slicing/decoding/hashing sum to
623
+ // O(body bytes), as does native parsing.
624
+ const containers = [];
625
+ let index = 0;
626
+ while (index < text.length) {
590
627
  const char = text[index];
591
628
  if (char === '"') {
592
629
  const start = index++;
@@ -600,20 +637,31 @@ function rejectDuplicateAnswerKeys(text) {
600
637
  else if (next === '"')
601
638
  break;
602
639
  }
603
- if (depth === 1 && /^\s*:/.test(text.slice(index))) {
640
+ let next = index;
641
+ while (next < text.length && /[\x20\t\r\n]/.test(text[next]))
642
+ next += 1;
643
+ const keys = containers.at(-1);
644
+ if (keys && text[next] === ':') {
604
645
  const key = JSON.parse(text.slice(start, index));
605
646
  if (keys.has(key))
606
- throw new DecisionValidationError('duplicate Jev answer key');
647
+ throw new DecisionValidationError('duplicate Jev response key');
607
648
  keys.add(key);
608
649
  }
650
+ index = next;
609
651
  continue;
610
652
  }
611
- if (char === '{' || char === '[')
612
- depth += 1;
653
+ if (char === '{' || char === '[') {
654
+ if (containers.length >= MAX_PARSE_DEPTH)
655
+ throw new DecisionValidationError('Jev response nesting too deep');
656
+ containers.push(char === '{' ? new Set() : null);
657
+ }
613
658
  else if (char === '}' || char === ']')
614
- depth -= 1;
659
+ containers.pop();
615
660
  index += 1;
616
661
  }
662
+ // Native parsing checks the complete JSON grammar, with no reviver or code
663
+ // execution. Input bytes and nesting bound its work and allocated graph.
664
+ return JSON.parse(text);
617
665
  }
618
666
  function headersWithinLimit(headers) {
619
667
  let bytes = 0;