@aiwg/cli 2026.10.3 → 2026.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/README.md +2 -2
  2. package/agentic/code/providers/model-capabilities.v1.json +96 -4
  3. package/bin/aiwg.mjs +50 -27
  4. package/dist/src/agents/grounding/knowledge-base.js +54 -6
  5. package/dist/src/artifacts/browser-export.js +6 -0
  6. package/dist/src/artifacts/checksum-manifest.js +1 -0
  7. package/dist/src/artifacts/citation-parser.js +173 -92
  8. package/dist/src/artifacts/citation-table-schema.js +59 -0
  9. package/dist/src/artifacts/cli.js +69 -18
  10. package/dist/src/artifacts/corpus-tools/snapshot.js +1 -1
  11. package/dist/src/artifacts/corpus-tools/vision-extract.js +2 -0
  12. package/dist/src/artifacts/corpus-views/ref-parser.js +3 -1
  13. package/dist/src/artifacts/corpus-views/renderers.js +3 -3
  14. package/dist/src/artifacts/discovery-eval.js +6 -4
  15. package/dist/src/artifacts/fortemi-core-query-adapter.js +36 -3
  16. package/dist/src/artifacts/fortemi-core-sync.js +166 -40
  17. package/dist/src/artifacts/framework-capabilities.js +171 -0
  18. package/dist/src/artifacts/framework-index-paths.js +73 -0
  19. package/dist/src/artifacts/framework-source-digest.js +75 -0
  20. package/dist/src/artifacts/index-builder.js +286 -131
  21. package/dist/src/artifacts/index-files.js +132 -18
  22. package/dist/src/artifacts/index-preparation.js +130 -0
  23. package/dist/src/artifacts/index-refresh.js +591 -0
  24. package/dist/src/artifacts/index-worker.mjs +11 -0
  25. package/dist/src/artifacts/prebuilt-build-lock.js +47 -4
  26. package/dist/src/artifacts/prebuilt-deployment.js +46 -0
  27. package/dist/src/artifacts/prebuilt-relations.js +80 -0
  28. package/dist/src/artifacts/query-engine.js +410 -88
  29. package/dist/src/artifacts/relations-builder.js +168 -0
  30. package/dist/src/artifacts/relations.js +148 -0
  31. package/dist/src/artifacts/stats.js +5 -2
  32. package/dist/src/artifacts/types.js +30 -10
  33. package/dist/src/artifacts/watcher.js +1 -1
  34. package/dist/src/auth/credential-store.js +2 -1
  35. package/dist/src/catalog/cli.d.mts +5 -0
  36. package/dist/src/channel/manager.d.mts +20 -0
  37. package/dist/src/channel/manager.mjs +19 -2
  38. package/dist/src/cli/command-log.js +2 -3
  39. package/dist/src/cli/handlers/artifacts.js +3 -0
  40. package/dist/src/cli/handlers/auth.js +3 -0
  41. package/dist/src/cli/handlers/decision.js +19 -12
  42. package/dist/src/cli/handlers/diagnose.js +4 -3
  43. package/dist/src/cli/handlers/effect.js +4 -2
  44. package/dist/src/cli/handlers/evidence.js +3 -0
  45. package/dist/src/cli/handlers/help.js +1 -0
  46. package/dist/src/cli/handlers/index.js +3 -1
  47. package/dist/src/cli/handlers/init.js +4 -1
  48. package/dist/src/cli/handlers/lint.js +5 -0
  49. package/dist/src/cli/handlers/models.js +104 -8
  50. package/dist/src/cli/handlers/ralph-launcher.js +2 -0
  51. package/dist/src/cli/handlers/refresh.js +12 -0
  52. package/dist/src/cli/handlers/resource-versions.js +3 -0
  53. package/dist/src/cli/handlers/rules.js +195 -0
  54. package/dist/src/cli/handlers/run.js +4 -0
  55. package/dist/src/cli/handlers/runtime-info.js +1 -2
  56. package/dist/src/cli/handlers/script-runner.js +3 -2
  57. package/dist/src/cli/handlers/session.js +19 -9
  58. package/dist/src/cli/handlers/sessions.js +137 -43
  59. package/dist/src/cli/handlers/setup-manifest.js +39 -44
  60. package/dist/src/cli/handlers/setup.js +41 -0
  61. package/dist/src/cli/handlers/steward.js +11 -0
  62. package/dist/src/cli/handlers/subcommands.js +57 -2
  63. package/dist/src/cli/handlers/uhp.js +3 -0
  64. package/dist/src/cli/handlers/use.js +294 -55
  65. package/dist/src/cli/handlers/utilities.js +26 -10
  66. package/dist/src/cli/handlers/version.js +8 -0
  67. package/dist/src/cli/router.js +15 -1
  68. package/dist/src/cli/services/deployment-verification.js +19 -9
  69. package/dist/src/cli/services/provider-prepare-coordinator.js +24 -0
  70. package/dist/src/cli/skill-usage.js +2 -3
  71. package/dist/src/config/aiwg-config.js +87 -37
  72. package/dist/src/config/cli.js +111 -5
  73. package/dist/src/config/decision-config.js +106 -0
  74. package/dist/src/config/dispatch-thresholds-runtime.d.mts +8 -0
  75. package/dist/src/config/gitignore.js +50 -0
  76. package/dist/src/config/index-graph-overrides.js +36 -0
  77. package/dist/src/config/project-artifacts-health.d.mts +37 -0
  78. package/dist/src/config/project-artifacts-runtime.d.mts +16 -0
  79. package/dist/src/config/user-config-dir.d.mts +9 -0
  80. package/dist/src/config/user-config.js +2 -1
  81. package/dist/src/config/user-registry.js +23 -15
  82. package/dist/src/config/user-state.js +7 -0
  83. package/dist/src/decision/ask.js +203 -43
  84. package/dist/src/decision/context-live-qualification.js +2 -2
  85. package/dist/src/decision/driver.js +13 -8
  86. package/dist/src/decision/ensemble/aggregate.js +23 -10
  87. package/dist/src/decision/ensemble-study/multifact-study.js +6 -2
  88. package/dist/src/decision/entry.js +2 -15
  89. package/dist/src/decision/graph-live-qualification.js +1 -1
  90. package/dist/src/decision/graph-run.js +9 -0
  91. package/dist/src/decision/heldout/collector.js +5 -13
  92. package/dist/src/decision/heldout/contract.js +30 -7
  93. package/dist/src/decision/jev-credentials.js +15 -1
  94. package/dist/src/decision/ledger.js +131 -28
  95. package/dist/src/decision/offload-activation.js +137 -0
  96. package/dist/src/decision/offload.js +550 -0
  97. package/dist/src/decision/qualification/load.js +4 -6
  98. package/dist/src/decision/savings.js +28 -0
  99. package/dist/src/effects/types.js +5 -1
  100. package/dist/src/effects/verifiers/git.js +2 -1
  101. package/dist/src/effects/verifiers/tracker.js +26 -14
  102. package/dist/src/extensions/commands/definitions.js +19 -0
  103. package/dist/src/extensions/project-local-remove.js +36 -1
  104. package/dist/src/gates/providers/loader.js +61 -17
  105. package/dist/src/installation/manager-command.d.mts +18 -0
  106. package/dist/src/installation/manager-command.mjs +14 -2
  107. package/dist/src/installation/manager.d.mts +49 -0
  108. package/dist/src/installation/manager.mjs +78 -3
  109. package/dist/src/issues/cli.js +4 -4
  110. package/dist/src/issues/local.js +24 -5
  111. package/dist/src/jobs/runner.js +1 -1
  112. package/dist/src/lint/cli.js +27 -1
  113. package/dist/src/lint/loader.js +2 -1
  114. package/dist/src/lint/reporters.js +25 -1
  115. package/dist/src/lint/rule-activation.js +43 -0
  116. package/dist/src/lint/runner.js +137 -62
  117. package/dist/src/mcp/adapters/codex-runtime.js +7 -1
  118. package/dist/src/mcp/claude-install.d.mts +4 -0
  119. package/dist/src/mcp/claude-install.mjs +41 -0
  120. package/dist/src/mcp/cli.d.mts +1 -0
  121. package/dist/src/mcp/cli.mjs +36 -11
  122. package/dist/src/mcp/config-file.d.mts +28 -0
  123. package/dist/src/mcp/config-layers.d.mts +36 -0
  124. package/dist/src/mcp/credentials.d.mts +51 -0
  125. package/dist/src/mcp/grok-build-config.d.mts +6 -0
  126. package/dist/src/mcp/grok-build-config.mjs +1 -1
  127. package/dist/src/mcp/helpers.mjs +3 -2
  128. package/dist/src/mcp/muse-native-config.d.mts +13 -0
  129. package/dist/src/mcp/muse-native-config.mjs +1 -0
  130. package/dist/src/mcp/omp-config.d.mts +4 -0
  131. package/dist/src/mcp/registry.js +3 -93
  132. package/dist/src/mcp/registry.mjs +3 -87
  133. package/dist/src/mcp/server-config.d.mts +3 -0
  134. package/dist/src/mcp/server-config.mjs +89 -0
  135. package/dist/src/mcp/server.mjs +37 -375
  136. package/dist/src/mcp/test-simple.mjs +2 -1
  137. package/dist/src/mcp/toml-editor.d.mts +4 -0
  138. package/dist/src/mcp/toml-string.d.mts +1 -0
  139. package/dist/src/mcp/toml-strip-mcp.d.mts +1 -0
  140. package/dist/src/mcp/tool-filters.d.mts +46 -0
  141. package/dist/src/mcp/tools/decision.mjs +12 -11
  142. package/dist/src/mcp/tools/discovery.mjs +62 -55
  143. package/dist/src/mcp/tools/orchestration.mjs +12 -2
  144. package/dist/src/mcp/tools/worker-invoke.mjs +1340 -23
  145. package/dist/src/mcp/tools/worker-shell-control.mjs +52 -21
  146. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +3 -3
  147. package/dist/src/mission-protocol/codecs.js +8 -0
  148. package/dist/src/models/config-loader.js +7 -2
  149. package/dist/src/models/model-capabilities.v1.json +96 -4
  150. package/dist/src/models/model-discovery.js +76 -11
  151. package/dist/src/models/provider-policy.js +101 -7
  152. package/dist/src/models/qualification-digest.d.mts +2 -0
  153. package/dist/src/models/resolver.js +12 -10
  154. package/dist/src/models/worker-direct-readiness.js +65 -44
  155. package/dist/src/models/worker-dispatch-offer.js +3 -3
  156. package/dist/src/models/worker-health-readiness.js +16 -3
  157. package/dist/src/models/worker-installation.d.mts +39 -0
  158. package/dist/src/models/worker-installation.mjs +135 -28
  159. package/dist/src/process/bounded-child-process.d.mts +84 -0
  160. package/dist/src/process/bounded-child-process.mjs +43 -0
  161. package/dist/src/process/cli-command.d.mts +16 -0
  162. package/dist/src/process/signal-child.d.mts +6 -0
  163. package/dist/src/process/signal-child.mjs +7 -0
  164. package/dist/src/providers/deployed-skill-roots.js +20 -0
  165. package/dist/src/providers/grok-build-acp.js +3 -2
  166. package/dist/src/providers/grok-build-headless.js +12 -4
  167. package/dist/src/providers/grok-build-inspect.js +1 -0
  168. package/dist/src/providers/omp-agent.d.mts +8 -0
  169. package/dist/src/providers/omp-diagnostics.d.mts +1 -0
  170. package/dist/src/providers/omp-paths.d.mts +13 -0
  171. package/dist/src/providers/skill-ownership.d.mts +9 -0
  172. package/dist/src/providers/skill-ownership.mjs +88 -0
  173. package/dist/src/rules/activation.d.mts +21 -0
  174. package/dist/src/rules/activation.mjs +62 -0
  175. package/dist/src/rules/inline-budget.d.mts +26 -0
  176. package/dist/src/rules/inline-budget.mjs +66 -0
  177. package/dist/src/rules/inventory.js +129 -0
  178. package/dist/src/rules/pin-state.js +73 -0
  179. package/dist/src/rules/pins.d.mts +14 -0
  180. package/dist/src/rules/pins.mjs +112 -0
  181. package/dist/src/rules/rule-frontmatter.schema.json +197 -0
  182. package/dist/src/serve/mission-conductor.js +38 -12
  183. package/dist/src/serve/worker-invocation.js +30 -10
  184. package/dist/src/serve/worker-stdin-frame.d.mts +10 -0
  185. package/dist/src/sessions/adapter-registry.js +23 -19
  186. package/dist/src/sessions/adapters/claude.js +39 -54
  187. package/dist/src/sessions/adapters/codex.js +95 -67
  188. package/dist/src/sessions/adapters/copilot.js +4 -4
  189. package/dist/src/sessions/adapters/cursor.js +40 -57
  190. package/dist/src/sessions/adapters/deepseek-harness.js +15 -12
  191. package/dist/src/sessions/adapters/factory.js +31 -27
  192. package/dist/src/sessions/adapters/generic.js +10 -10
  193. package/dist/src/sessions/adapters/grok-build.js +11 -11
  194. package/dist/src/sessions/adapters/grokbot.js +24 -13
  195. package/dist/src/sessions/adapters/hermes.js +27 -29
  196. package/dist/src/sessions/adapters/muse-workspace-scan.js +31 -11
  197. package/dist/src/sessions/adapters/muse.js +150 -93
  198. package/dist/src/sessions/adapters/omp.js +28 -14
  199. package/dist/src/sessions/adapters/openclaw.js +89 -79
  200. package/dist/src/sessions/adapters/opencode.js +27 -24
  201. package/dist/src/sessions/adapters/openhuman.js +18 -60
  202. package/dist/src/sessions/adapters/pi.js +29 -14
  203. package/dist/src/sessions/adapters/warp.js +3 -3
  204. package/dist/src/sessions/adapters/windsurf.js +19 -48
  205. package/dist/src/sessions/batch-contracts.js +16 -8
  206. package/dist/src/sessions/batch-import.js +252 -12
  207. package/dist/src/sessions/contracts.js +50 -3
  208. package/dist/src/sessions/coverage-preflight.js +27 -2
  209. package/dist/src/sessions/export-plan.js +4 -3
  210. package/dist/src/sessions/import-exclusions.js +72 -0
  211. package/dist/src/sessions/import-lease.js +34 -18
  212. package/dist/src/sessions/import-limits.js +37 -0
  213. package/dist/src/sessions/importer.js +60 -38
  214. package/dist/src/sessions/lifecycle.js +45 -0
  215. package/dist/src/sessions/policy.js +145 -26
  216. package/dist/src/sessions/readers.js +106 -53
  217. package/dist/src/sessions/refresh.js +105 -0
  218. package/dist/src/sessions/repository.js +151 -54
  219. package/dist/src/sessions/workspace-discovery.js +292 -60
  220. package/dist/src/sessions/workspace-matching.js +107 -0
  221. package/dist/src/skills/importer.js +1 -0
  222. package/dist/src/smiths/context-pipeline/aiwg-md.js +11 -0
  223. package/dist/src/smiths/context-pipeline/claude-hook.js +3 -26
  224. package/dist/src/smiths/context-pipeline/finalization.js +6 -5
  225. package/dist/src/smiths/context-pipeline/generator.js +21 -4
  226. package/dist/src/smiths/context-pipeline/parallelism-section.js +7 -15
  227. package/dist/src/smiths/mcpsmith/generator.js +1 -1
  228. package/dist/src/smiths/toolsmith/runtime-discovery.d.mts +10 -0
  229. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +76 -18
  230. package/dist/src/storage/backends/fs.js +18 -76
  231. package/dist/src/storage/backends/obsidian.js +1 -0
  232. package/dist/src/tracker/forge-http.js +2 -1
  233. package/dist/src/update/checker.d.mts +2 -0
  234. package/dist/src/update/service.d.mts +22 -0
  235. package/dist/src/utils/contained-filesystem.d.mts +37 -0
  236. package/dist/src/utils/contained-filesystem.mjs +683 -0
  237. package/dist/src/utils/temporary-directory.js +36 -0
  238. package/dist/src/writing/example-generator.js +1 -1
  239. package/package.json +15 -2
  240. package/schemas/models/model-catalog.v1.schema.json +1 -1
  241. package/schemas/models/provider-model-capabilities.v1.schema.json +6 -0
  242. package/tools/_resolve-impl.mjs +13 -15
  243. package/tools/agents/deploy-agents.mjs +49 -21
  244. package/tools/agents/providers/base.mjs +104 -134
  245. package/tools/agents/providers/claude.mjs +8 -24
  246. package/tools/agents/providers/codex.mjs +18 -11
  247. package/tools/agents/providers/cursor.mjs +19 -18
  248. package/tools/agents/providers/grok-build.mjs +3 -1
  249. package/tools/agents/providers/hermes.mjs +57 -47
  250. package/tools/agents/providers/openclaw.mjs +9 -4
  251. package/tools/agents/providers/pi.mjs +5 -0
  252. package/tools/agents/providers/provider-prepare-channel.mjs +5 -0
  253. package/tools/agents/providers/provider-prepare-coordinator.mjs +247 -0
  254. package/tools/agents/providers/provider-prepare-worker.mjs +84 -0
  255. package/tools/agents/providers/rule-delivery.mjs +1270 -0
  256. package/tools/agents/providers/warp.mjs +20 -26
  257. package/tools/cli/decision-offload-report.mjs +40 -0
  258. package/tools/cli/workspace-status.mjs +862 -0
  259. package/tools/plugin/package-plugins.mjs +186 -8
  260. package/tools/plugin/plugin-links.mjs +114 -0
  261. package/tools/providers/antigravity-transport.mjs +3 -1
  262. package/tools/providers/deepseek-harness-transport.mjs +6 -4
  263. package/tools/providers/omp-transport.mjs +4 -2
  264. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +1748 -0
  265. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +55 -0
  266. package/tools/ralph-external/lib/omp-adapter.mjs +1 -1
  267. package/tools/ralph-external/lib/pi-adapter.mjs +0 -4
  268. package/tools/ralph-external/lib/provider-adapter.mjs +59 -1
  269. package/tools/ralph-external/lib/recovery-probe.mjs +72 -0
  270. package/tools/ralph-external/lib/session-environment.mjs +28 -0
  271. package/tools/ralph-external/lib/systemd-client.mjs +49 -0
  272. package/tools/ralph-external/lib/worker-health.mjs +1 -1
  273. package/tools/ralph-external/lib/worker-isolation.mjs +717 -0
  274. package/tools/ralph-external/lib/worker-overflow.mjs +50 -0
  275. package/tools/ralph-external/process-monitor.mjs +1017 -0
  276. package/tools/ralph-external/session-launcher.mjs +5057 -0
  277. package/tools/scripts/resource-preflight.mjs +541 -0
  278. package/tools/skills/deploy-skills-codex.mjs +2 -1
  279. package/vscode-extension/schemas/aiwg.config.v1.json +1194 -231
@@ -1,8 +1,10 @@
1
+ import { aggregateModalAnswers } from './ensemble/aggregate.js';
2
+ import { readAiwgConfigCandidate } from '../config/aiwg-config.js';
1
3
  import { readFile } from 'node:fs/promises';
2
4
  import { JEV_ENDPOINT, JevDecisionAdapter } from './adapters/jev.js';
3
5
  import { evaluateDecisionRuleset } from './evaluate.js';
4
6
  import { artifactPin } from './validate.js';
5
- import { JEV_CREDENTIAL_REF, JEV_DEFAULT_REGION, lookupJevCredential, removeJevCredentialFile, resolveJevCredentialBytes, writeJevCredentialFile, } from './jev-credentials.js';
7
+ import { JEV_CREDENTIAL_REF, JEV_DEFAULT_REGION, lookupJevCredential, removeJevCredentialFile, writeJevCredentialFile, } from './jev-credentials.js';
6
8
  const CONTEXT_LIMIT_BYTES = 32 * 1024;
7
9
  const MODEL = 'jev-latest';
8
10
  const PURPOSE = 'agent-bounded-decision';
@@ -39,9 +41,22 @@ export function validateDecisionAskInput(input) {
39
41
  if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
40
42
  usageError('invalid-threshold', '--threshold must be a probability from 0 to 1');
41
43
  }
44
+ for (const key of ['minAgreement', 'agreementConfidenceFloor']) {
45
+ const value = input[key];
46
+ if (value !== undefined && (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1)) {
47
+ usageError('invalid-agreement-policy', `${key} must be a probability from 0 to 1`);
48
+ }
49
+ }
42
50
  const timeoutMs = input.timeoutMs ?? 15_000;
43
51
  if (!Number.isInteger(timeoutMs) || timeoutMs <= 0)
44
52
  usageError('invalid-timeout', '--timeout-ms must be a positive integer');
53
+ const passes = input.passes ?? DEFAULT_DECISION_PASSES;
54
+ const escalationPasses = input.escalationPasses ?? defaultEscalationPasses(passes);
55
+ if (!Number.isSafeInteger(passes) || passes <= 0)
56
+ usageError('invalid-passes', '--passes must be a positive safe integer');
57
+ if (!Number.isSafeInteger(escalationPasses) || escalationPasses < passes) {
58
+ usageError('invalid-escalation-passes', '--escalation-passes must be a positive safe integer >= passes');
59
+ }
45
60
  if (input.mode.kind === 'choices') {
46
61
  if (input.mode.choices.length < 2 || input.mode.choices.length > 255)
47
62
  usageError('invalid-choices', '--choices requires 2..255 ids');
@@ -239,66 +254,209 @@ function publicAnswer(mode, value, distribution) {
239
254
  }
240
255
  return typeof value === 'string' ? { answer: value } : { answer: null };
241
256
  }
257
+ /** Product default: 5 passes, escalating to 10 when the first round is not decisive. */
258
+ export const DEFAULT_DECISION_PASSES = 5;
259
+ /** Escalation defaults to twice the initial pass count (5 -> 10, 10 -> 20). */
260
+ export function defaultEscalationPasses(passes) {
261
+ return passes * 2;
262
+ }
242
263
  export async function askDecision(input, options = {}) {
264
+ if (options.cwd) {
265
+ let config;
266
+ try {
267
+ config = await readAiwgConfigCandidate(options.cwd);
268
+ }
269
+ catch (error) {
270
+ usageError('invalid-config', error instanceof Error ? error.message : 'Invalid project config');
271
+ }
272
+ input = { ...input, passes: input.passes ?? config?.decision?.ask?.passes,
273
+ escalationPasses: input.escalationPasses ?? config?.decision?.ask?.escalation_passes,
274
+ minAgreement: input.minAgreement ?? config?.decision?.ask?.min_agreement,
275
+ agreementConfidenceFloor: input.agreementConfidenceFloor ?? config?.decision?.ask?.agreement_confidence_floor };
276
+ }
243
277
  validateDecisionAskInput(input);
244
278
  const started = options.now?.() ?? Date.now();
279
+ const wallStarted = Date.now();
280
+ const passesRequested = input.passes ?? DEFAULT_DECISION_PASSES;
281
+ const escalationPasses = input.escalationPasses ?? defaultEscalationPasses(passesRequested);
282
+ const threshold = input.threshold ?? 0.8;
283
+ const minAgreement = input.minAgreement;
284
+ const agreementConfidenceFloor = input.agreementConfidenceFloor ?? 0.55;
285
+ const timeoutMs = input.timeoutMs ?? 15_000;
286
+ const passes = [];
287
+ let escalated = false;
288
+ let earlyStop = null;
289
+ const labels = input.mode.kind === 'yes-no' ? ['false', 'true']
290
+ : input.mode.kind === 'choices' ? input.mode.choices.map(parseChoice).map(choice => choice.id)
291
+ : Array.from({ length: input.mode.high - input.mode.low + 1 }, (_, index) => String(input.mode.kind === 'scale' ? input.mode.low + index : index));
292
+ const aggregate = () => {
293
+ const successful = passes.filter(pass => pass.status === 'succeeded');
294
+ const modal = aggregateModalAnswers(labels, successful.map(pass => String(pass.answer)));
295
+ const confidences = successful.flatMap(pass => pass.confidence === null ? [] : [pass.confidence]);
296
+ return {
297
+ algorithm: 'majority-v1', semantics: 'stability-signal-not-correctness',
298
+ passesRequested, escalationPasses, passesRun: passes.length, escalated, earlyStop,
299
+ minAgreement: minAgreement ?? null, agreementConfidenceFloor,
300
+ // Failed/abstaining passes have no vote but remain in the denominator.
301
+ agreementRatio: passes.length ? Math.max(0, ...Object.values(modal.votes)) / passes.length : 0,
302
+ votes: modal.votes,
303
+ answer: modal.value === null ? null : input.mode.kind === 'yes-no' ? modal.value === 'true'
304
+ : input.mode.kind === 'scale' ? Number(modal.value) : modal.value,
305
+ confidenceSummary: { count: confidences.length, missing: passes.length - confidences.length,
306
+ mean: confidences.length ? confidences.reduce((sum, value) => sum + value, 0) / confidences.length : null,
307
+ min: confidences.length ? Math.min(...confidences) : null, max: confidences.length ? Math.max(...confidences) : null },
308
+ passes: [...passes],
309
+ };
310
+ };
311
+ // Sampling agreement is stability evidence, not calibrated correctness.
312
+ // Confidence acceptance retains the original requirement that every pass succeeds.
313
+ const answeredBy = (summary) => {
314
+ const successful = passes.filter(pass => pass.status === 'succeeded');
315
+ if (successful.length < passesRequested || summary.answer === null)
316
+ return null;
317
+ const mean = summary.confidenceSummary.mean;
318
+ if (successful.length === passes.length && !summary.confidenceSummary.missing
319
+ && summary.agreementRatio >= threshold && mean !== null && mean >= threshold)
320
+ return 'confidence';
321
+ if (minAgreement !== undefined && summary.agreementRatio >= minAgreement
322
+ && summary.confidenceSummary.count === successful.length && mean !== null
323
+ && mean + Number.EPSILON >= agreementConfidenceFloor)
324
+ return 'agreement';
325
+ return null;
326
+ };
327
+ const problem = (summary) => {
328
+ if (answeredBy(summary))
329
+ return null;
330
+ const failed = passes.find(pass => pass.status !== 'succeeded');
331
+ if (failed)
332
+ return failed.reason;
333
+ if (summary.agreementRatio < threshold)
334
+ return 'low-agreement';
335
+ return 'low-confidence';
336
+ };
337
+ const finish = (forcedReason) => {
338
+ const summary = aggregate();
339
+ const reason = forcedReason ?? problem(summary);
340
+ const latencyMs = Math.max(0, (options.now?.() ?? Date.now()) - started);
341
+ const model = passes.map(pass => pass.model).filter(model => model !== null).at(-1) ?? null;
342
+ const totalTokens = (key) => {
343
+ const values = passes.flatMap(pass => pass.usage?.[key] == null ? [] : [pass.usage[key]]);
344
+ return values.length ? values.reduce((sum, value) => sum + value, 0) : null;
345
+ };
346
+ const combinedUsage = passes.some(pass => pass.usage !== null)
347
+ ? { inputTokens: totalTokens('inputTokens'), outputTokens: totalTokens('outputTokens') } : null;
348
+ const result = reason ? fallback(reason, latencyMs) : {
349
+ schema: 'aiwg-decision-ask/v1', status: 'answered', answer: summary.answer,
350
+ confidence: summary.agreementRatio * summary.confidenceSummary.mean, fallback: null, reason: 'none',
351
+ model, usage: combinedUsage, latencyMs,
352
+ ...(input.mode.kind === 'yes-no' ? { probability: passes.reduce((sum, pass) => sum + (pass.probability ?? 0), 0)
353
+ / passes.filter(pass => pass.status === 'succeeded').length } : {}),
354
+ };
355
+ return { ...result, model, usage: combinedUsage, aggregate: summary, answeredBy: reason ? null : answeredBy(summary) };
356
+ };
245
357
  let credential;
246
358
  try {
247
359
  credential = await lookupJevCredential(options);
248
360
  }
249
361
  catch {
250
- return fallback('not-configured', 0);
362
+ return finish('not-configured');
251
363
  }
252
364
  if (!credential?.enabled || !credential.token)
253
- return fallback('not-configured', 0);
254
- const timeoutMs = input.timeoutMs ?? 15_000;
365
+ return finish('not-configured');
255
366
  const def = definition(input);
256
367
  const rules = ruleset(def);
257
- const bind = binding(rules, def.spec.answer.kind, timeoutMs);
258
368
  const adapter = options.adapter ?? options.createAdapter?.({ region: credential.region, endpoint: credential.endpoint })
259
369
  ?? new JevDecisionAdapter({ region: credential.region, ...(credential.endpoint ? {
260
- endpoint: credential.endpoint,
261
- allowedOrigins: [new URL(credential.endpoint).origin],
370
+ endpoint: credential.endpoint, allowedOrigins: [new URL(credential.endpoint).origin],
262
371
  } : {}) });
263
- let result;
264
- try {
265
- result = await (options.evaluate ?? evaluateDecisionRuleset)({
266
- ruleset: rules,
267
- binding: bind,
268
- definitions: { [def.metadata.id]: def },
269
- input: { message: contextMessage(input) },
270
- runId: options.runId ?? 'aiwg-decision-ask',
271
- invocationId: options.invocationId ?? `ask-${Date.now()}`,
272
- adapters: { jev: adapter },
273
- resolveCredential: options.resolveCredential ?? (logicalRef => resolveJevCredentialBytes(logicalRef, options)),
274
- projection: { resolve: () => projectionPolicy(credential.region, credential.endpoint) },
372
+ const controller = new AbortController();
373
+ const remainingMs = () => timeoutMs - (Date.now() - wallStarted);
374
+ const timer = setTimeout(() => controller.abort(), Math.max(0, remainingMs()));
375
+ const invocationId = options.invocationId ?? `ask-${Date.now()}`;
376
+ const runPass = async (index) => {
377
+ const passStarted = Date.now();
378
+ let removeListener = () => { };
379
+ const interrupted = new Promise((_resolve, reject) => {
380
+ const abort = () => reject(new Error('ask deadline'));
381
+ if (controller.signal.aborted)
382
+ abort();
383
+ else {
384
+ controller.signal.addEventListener('abort', abort, { once: true });
385
+ removeListener = () => controller.signal.removeEventListener('abort', abort);
386
+ }
275
387
  });
388
+ try {
389
+ const result = await Promise.race([interrupted, (options.evaluate ?? evaluateDecisionRuleset)({
390
+ ruleset: rules, binding: binding(rules, def.spec.answer.kind, Math.max(1, remainingMs())),
391
+ definitions: { [def.metadata.id]: def }, input: { message: contextMessage(input) },
392
+ runId: options.runId ?? 'aiwg-decision-ask', invocationId: `${invocationId}-pass-${index}`,
393
+ signal: controller.signal, adapters: { jev: adapter },
394
+ resolveCredential: options.resolveCredential ?? (() => Promise.resolve(new TextEncoder().encode(credential.token))),
395
+ projection: { resolve: () => projectionPolicy(credential.region, credential.endpoint) },
396
+ })]);
397
+ const evaluation = result.spec.evaluations.answer;
398
+ const attempt = evaluation?.spec.attempts.at(-1);
399
+ const succeeded = evaluation?.spec.status === 'success';
400
+ return { index, status: succeeded ? 'succeeded' : evaluation?.spec.status === 'abstained' ? 'abstained' : 'failed',
401
+ reason: succeeded ? 'none' : evaluation?.spec.reason ?? result.spec.reason,
402
+ ...(succeeded ? publicAnswer(input.mode, evaluation.spec.value, evaluation.spec.uncertainty?.distribution) : { answer: null }),
403
+ confidence: succeeded ? confidenceFor(input.mode, evaluation) : null,
404
+ model: attempt?.actualModel ?? null, usage: usage(attempt?.usage), latencyMs: Date.now() - passStarted };
405
+ }
406
+ catch {
407
+ return { index, status: 'failed', reason: controller.signal.aborted ? 'timeout' : 'jev-error', answer: null,
408
+ confidence: null, model: null, usage: null, latencyMs: Date.now() - passStarted };
409
+ }
410
+ finally {
411
+ removeListener();
412
+ }
413
+ };
414
+ const certainFailure = (total) => {
415
+ const summary = aggregate();
416
+ const remaining = total - passes.length;
417
+ const bestVotes = Math.max(0, ...Object.values(summary.votes));
418
+ const successful = passes.filter(pass => pass.status === 'succeeded');
419
+ const confidenceSum = successful.reduce((sum, pass) => sum + (pass.confidence ?? 0), 0);
420
+ // Give every unspent pass a matching vote and perfect confidence. Stop only
421
+ // when even that best case cannot answer, allowing roundoff at the boundary.
422
+ const bestAgreement = (bestVotes + remaining) / total;
423
+ const knownConfidence = successful.every(pass => pass.confidence !== null);
424
+ const confidenceReachable = successful.length === passes.length && knownConfidence
425
+ && bestAgreement + 1e-12 >= threshold && (confidenceSum + remaining) / total + 1e-12 >= threshold;
426
+ const agreementReachable = minAgreement !== undefined && knownConfidence
427
+ && successful.length + remaining >= passesRequested
428
+ && bestAgreement + 1e-12 >= minAgreement
429
+ && (confidenceSum + remaining) / (successful.length + remaining) + 1e-12 >= agreementConfidenceFloor;
430
+ return !confidenceReachable && !agreementReachable;
431
+ };
432
+ try {
433
+ let target = passesRequested;
434
+ while (passes.length < target) {
435
+ if (controller.signal.aborted || remainingMs() <= 0) {
436
+ earlyStop = 'timeout';
437
+ return finish('timeout');
438
+ }
439
+ const offset = passes.length;
440
+ if (offset >= passesRequested)
441
+ escalated = true;
442
+ passes.push(...await Promise.all(Array.from({ length: Math.min(4, target - offset) }, (_, index) => runPass(offset + index))));
443
+ if (controller.signal.aborted || remainingMs() <= 0) {
444
+ earlyStop = 'timeout';
445
+ return finish('timeout');
446
+ }
447
+ if (passes.length >= passesRequested && problem(aggregate()) && escalationPasses > passesRequested) {
448
+ target = escalationPasses;
449
+ }
450
+ if (passes.length < target && (passes.length >= passesRequested || certainFailure(passesRequested)) && certainFailure(escalationPasses)) {
451
+ earlyStop = 'certain-failure';
452
+ return finish();
453
+ }
454
+ }
455
+ return finish();
276
456
  }
277
- catch {
278
- return fallback('jev-error', Math.max(0, (options.now?.() ?? Date.now()) - started));
457
+ finally {
458
+ clearTimeout(timer);
279
459
  }
280
- const latencyMs = Math.max(0, (options.now?.() ?? Date.now()) - started);
281
- const evaluation = result.spec.evaluations.answer;
282
- const attempt = evaluation?.spec.attempts.at(-1);
283
- if (!evaluation || evaluation.spec.status !== 'success')
284
- return fallback(evaluation?.spec.reason ?? result.spec.reason, latencyMs, attempt);
285
- const confidence = confidenceFor(input.mode, evaluation);
286
- const threshold = input.threshold ?? 0.8;
287
- if (confidence === null || confidence < threshold)
288
- return fallback('low-confidence', latencyMs, attempt);
289
- const answer = publicAnswer(input.mode, evaluation.spec.value, evaluation.spec.uncertainty?.distribution);
290
- return {
291
- schema: 'aiwg-decision-ask/v1',
292
- status: 'answered',
293
- answer: answer.answer,
294
- confidence,
295
- ...(answer.probability === undefined ? {} : { probability: answer.probability }),
296
- fallback: null,
297
- reason: 'none',
298
- model: attempt?.actualModel ?? null,
299
- usage: usage(attempt?.usage),
300
- latencyMs,
301
- };
302
460
  }
303
461
  export async function readDecisionContextFile(filePath) {
304
462
  const content = await readFile(filePath, 'utf8');
@@ -342,6 +500,8 @@ export async function setupJev(input = {}, options = {}) {
342
500
  mode: { kind: 'yes-no' },
343
501
  threshold: 0,
344
502
  timeoutMs: 15_000,
503
+ passes: 1,
504
+ escalationPasses: 1,
345
505
  }, options);
346
506
  result.verified = verified.status === 'answered';
347
507
  result.model = verified.model;
@@ -223,7 +223,7 @@ export function validateContextLiveApproval(approval, corpus) {
223
223
  throw new Error('TV-12 collection margin is below the preregistered maximum');
224
224
  }
225
225
  export async function assertContextLiveSource(root, expected) {
226
- const git = (...args) => execFileSync('git', args, { cwd: root, encoding: 'utf8', timeout: 10_000 }).trim();
226
+ const git = (...args) => execFileSync('git', args, { cwd: root, encoding: 'utf8', timeout: 10_000, killSignal: 'SIGKILL' }).trim();
227
227
  if (git('rev-parse', 'HEAD') !== expected || git('status', '--porcelain', '--untracked-files=all'))
228
228
  throw new Error('TV-12 requires clean exact source commit');
229
229
  }
@@ -452,7 +452,7 @@ export async function collectContextLiveCase(item, corpus, approval, host, budge
452
452
  }
453
453
  /** `exact` (run evidence) requires the canonical root itself; `within` (prepared inputs) allows a descendant. */
454
454
  export async function assertContextArtifactRoot(sourceRoot, artifactRoot, mode = 'exact') {
455
- const route = JSON.parse(execFileSync('aiwg', ['artifacts', 'path', '--json', '--check-write'], { cwd: sourceRoot, encoding: 'utf8', timeout: 10_000 }));
455
+ const route = JSON.parse(execFileSync('aiwg', ['artifacts', 'path', '--json', '--check-write'], { cwd: sourceRoot, encoding: 'utf8', timeout: 10_000, killSignal: 'SIGKILL' }));
456
456
  if (typeof route.artifact_root !== 'string')
457
457
  throw new Error('Canonical artifact root unavailable');
458
458
  const delta = relative(await realpath(route.artifact_root), await realpath(artifactRoot));
@@ -1,12 +1,15 @@
1
+ import { resolveDecisionEnablement } from '../config/decision-config.js';
1
2
  import { existsSync } from 'node:fs';
2
3
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
4
  import path from 'node:path';
4
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
6
  import { askDecision, setupJev, DecisionAskUsageError, readDecisionContextFile, validateDecisionAskInput, } from './ask.js';
6
- import { recordDecisionAsk, decisionLedgerHistory, decisionLedgerStats, resolveDecisionSessionId, parseDecisionLedgerSince, DecisionLedgerSinceError, DECISION_LEDGER_SCHEMA, ATTRIBUTION_FIELD_PATTERN, MAX_ATTRIBUTION_FIELD_LENGTH, MAX_RECORDED_CHOICE_IDS, MAX_RECORD_TEXT_BYTES, MAX_LEDGER_RECORD_BYTES, DECISION_LEDGER_RECORD_TIMEOUT_MS, DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS, DECISION_SAVINGS_ESTIMATION_METHOD, buildDecisionLedgerRecord, appendDecisionLedgerRecord, filterDecisionLedgerRecords, } from './ledger.js';
7
+ import { recordDecisionAsk, decisionLedgerHistory, decisionLedgerStats, resolveDecisionSessionId, resolveDecisionSession, DECISION_SESSION_ENV_VARS, parseDecisionLedgerSince, DecisionLedgerSinceError, DECISION_LEDGER_SCHEMA, DECISION_LEDGER_SCHEMA_V1, ATTRIBUTION_FIELD_PATTERN, MAX_ATTRIBUTION_FIELD_LENGTH, MAX_RECORDED_CHOICE_IDS, MAX_RECORD_TEXT_BYTES, MAX_LEDGER_RECORD_BYTES, DECISION_LEDGER_RECORD_TIMEOUT_MS, DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS, DECISION_SAVINGS_ESTIMATION_METHOD, buildDecisionLedgerRecord, appendDecisionLedgerRecord, filterDecisionLedgerRecords, } from './ledger.js';
8
+ export { DEFAULT_DECISION_SAVINGS_WEIGHTS, decisionSavingsWeights } from './savings.js';
7
9
  import { resolveActiveProvider } from '../cli/provider-resolution.js';
10
+ export { appendDecisionOpportunity, buildDecisionOffloadSnapshot, decisionOffloadCategory, DECISION_OFFLOAD_CHECKPOINT_COVERAGE, DECISION_OFFLOAD_EXECUTABLE_COVERAGE, DECISION_OFFLOAD_SNAPSHOT_SCHEMA, DECISION_OPPORTUNITY_SCHEMA, runDecisionOffloadCheckpoint, } from './offload.js';
8
11
  import { assertDecisionEvaluateDispatcherConfig, decisionPatternPacks, getDecisionPatternPack, governedPatternArtifacts, listDecisionPatterns, patternInputSchema, patternOutputSchema, planLiveDecisionPattern, runOfflineDecisionPattern, validateAgainstSchema, validateDecisionDocument, validateDefinition, validateRuleset, } from './index.js';
9
- export { askDecision, setupJev, readDecisionContextFile, validateDecisionAskInput, DecisionAskUsageError, recordDecisionAsk, decisionLedgerHistory, decisionLedgerStats, resolveDecisionSessionId, parseDecisionLedgerSince, DecisionLedgerSinceError, DECISION_LEDGER_SCHEMA, ATTRIBUTION_FIELD_PATTERN, MAX_ATTRIBUTION_FIELD_LENGTH, MAX_RECORDED_CHOICE_IDS, MAX_RECORD_TEXT_BYTES, MAX_LEDGER_RECORD_BYTES, DECISION_LEDGER_RECORD_TIMEOUT_MS, DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS, DECISION_SAVINGS_ESTIMATION_METHOD, buildDecisionLedgerRecord, appendDecisionLedgerRecord, filterDecisionLedgerRecords, resolveActiveProvider, };
12
+ export { askDecision, setupJev, readDecisionContextFile, validateDecisionAskInput, DecisionAskUsageError, recordDecisionAsk, decisionLedgerHistory, decisionLedgerStats, resolveDecisionSessionId, resolveDecisionSession, DECISION_SESSION_ENV_VARS, parseDecisionLedgerSince, DecisionLedgerSinceError, DECISION_LEDGER_SCHEMA, DECISION_LEDGER_SCHEMA_V1, ATTRIBUTION_FIELD_PATTERN, MAX_ATTRIBUTION_FIELD_LENGTH, MAX_RECORDED_CHOICE_IDS, MAX_RECORD_TEXT_BYTES, MAX_LEDGER_RECORD_BYTES, DECISION_LEDGER_RECORD_TIMEOUT_MS, DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS, DECISION_SAVINGS_ESTIMATION_METHOD, buildDecisionLedgerRecord, appendDecisionLedgerRecord, filterDecisionLedgerRecords, resolveActiveProvider, };
10
13
  const DECISION_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
11
14
  const VALIDATION_TARGETS = ['request', 'definition', 'ruleset', 'binding'];
12
15
  const OFFLINE_ADAPTER = 'agentic/code/addons/decision-engine/examples/fixture-jev-adapter.mjs';
@@ -224,9 +227,11 @@ export function decisionCapabilities(options = {}) {
224
227
  const env = driverEnv(options);
225
228
  const profiles = parseMcpRequestProfiles(env.AIWG_DECISION_MCP_REQUESTS, options.cwd ?? process.cwd());
226
229
  const packs = decisionPatternPacks;
230
+ const enablement = resolveDecisionEnablement(options);
227
231
  return {
228
232
  schema: 'aiwg-decision-driver-capabilities/v1',
229
- enabled: env.AIWG_DECISION_ENABLED === '1',
233
+ enabled: enablement.enabled,
234
+ enabledSource: enablement.source,
230
235
  offlineReady: true,
231
236
  primitives: [...new Set(packs.map(pack => pack.primitive))].sort(),
232
237
  patterns: {
@@ -238,19 +243,19 @@ export function decisionCapabilities(options = {}) {
238
243
  features: {
239
244
  patterns: ['list', 'show', 'offline-run', 'live-plan'],
240
245
  validation: [...VALIDATION_TARGETS],
241
- evaluate: { requiresEnv: 'AIWG_DECISION_ENABLED=1', mcpRequiresOptIn: true, mcpProfiles: profiles.map(profile => profile.name) },
246
+ evaluate: { requiresEnablement: true, mcpRequiresOptIn: true, mcpProfiles: profiles.map(profile => profile.name) },
242
247
  setup: ['synthetic-classification'],
243
248
  experimental: packs.filter(pack => pack.status === 'experimental').map(pack => pack.id),
244
249
  },
245
250
  backend: {
246
- configured: env.AIWG_DECISION_ENABLED === '1',
251
+ configured: enablement.enabled,
247
252
  probed: false,
248
253
  status: 'not-probed',
249
254
  note: 'Capabilities do not infer credential, network, dispatcher, or backend readiness.',
250
255
  },
251
256
  configuration: {
252
257
  env: {
253
- AIWG_DECISION_ENABLED: env.AIWG_DECISION_ENABLED === '1' ? 'set' : 'unset',
258
+ AIWG_DECISION_ENABLED: ['0', '1'].includes(env.AIWG_DECISION_ENABLED ?? '') ? 'set' : 'unset',
254
259
  AIWG_DECISION_MCP_REQUESTS: profiles.length ? 'configured' : 'unset',
255
260
  },
256
261
  },
@@ -323,7 +328,7 @@ export async function validateDecisionInput(target, filePath, options = {}) {
323
328
  export async function evaluateRequestPath(requestPath, options = {}, evaluateOptions = {}) {
324
329
  const env = driverEnv(options);
325
330
  const resolved = resolvePath(requestPath, options.cwd ?? process.cwd());
326
- if (env.AIWG_DECISION_ENABLED !== '1') {
331
+ if (!resolveDecisionEnablement(options).enabled) {
327
332
  return { schema: 'aiwg-decision-evaluate/v1', status: 'denied', reason: 'explicit-enable-required', exitCode: 2, result: null };
328
333
  }
329
334
  const validation = await validateDecisionInput('request', resolved, options);
@@ -338,7 +343,7 @@ export async function evaluateRequestPath(requestPath, options = {}, evaluateOpt
338
343
  argv.push('--host-policy-module', resolvePath(evaluateOptions.hostPolicyModulePath, options.cwd ?? process.cwd()));
339
344
  const exitCode = await dispatcher.runDecisionEvaluate({
340
345
  argv,
341
- env,
346
+ env: { ...env, AIWG_DECISION_ENABLED: '1' },
342
347
  runtime,
343
348
  stdout: { write(chunk) { stdout += String(chunk); return true; } },
344
349
  stderr: { write(chunk) { stderr += String(chunk); return true; } },
@@ -1,5 +1,19 @@
1
1
  import { admitEntry } from '../entry.js';
2
2
  import { compareEnsembleKeys, ensembleContractDigest, EnsembleContractError, validateEnsemblePolicy, } from './contract.js';
3
+ /** Modal buckets, with the existing locale-independent canonical tie break. */
4
+ export function aggregateModalAnswers(labels, answers) {
5
+ const ordered = [...labels].sort(compareEnsembleKeys);
6
+ const votes = Object.fromEntries(ordered.map(label => [label, 0]));
7
+ for (const answer of answers) {
8
+ if (!Object.hasOwn(votes, answer))
9
+ throw new EnsembleContractError('unknown modal answer', 'semantic');
10
+ votes[answer] += 1;
11
+ }
12
+ const top = Math.max(0, ...Object.values(votes));
13
+ const winners = ordered.filter(label => votes[label] === top);
14
+ return { value: answers.length ? winners[0] : null, votes, tie: winners.length > 1,
15
+ agreementRatio: answers.length ? top / answers.length : 0 };
16
+ }
3
17
  /** Deterministic reference aggregation over already-recorded member results. It never dispatches.
4
18
  * Member order does not matter: results are put in canonical (memberId, sampleIndex) order first,
5
19
  * so every sum and tie-break runs in the same sequence for any permutation of the input. */
@@ -24,12 +38,8 @@ export function aggregateEnsembleResults(policyInput, resultsInput, options = {}
24
38
  const { metric } = policy.disagreement;
25
39
  let candidate = null;
26
40
  let tie = false;
27
- const votes = () => {
28
- const counts = Object.fromEntries(labels.map(label => [label, 0]));
29
- for (const result of succeeded)
30
- counts[vote(policy, result)] += 1;
31
- return counts;
32
- };
41
+ const modal = policy.primitive === 'ordinal-score' ? null
42
+ : aggregateModalAnswers(labels, succeeded.map(result => vote(policy, result)));
33
43
  const distributions = () => succeeded.map(result => distributionOf(policy, labels, result));
34
44
  const meanOf = (items) => Object.fromEntries(labels.map(label => [label, n ? round(items.reduce((sum, item) => sum + item[label], 0) / n) : 0]));
35
45
  const scores = () => succeeded.map(result => result.value);
@@ -40,7 +50,7 @@ export function aggregateEnsembleResults(policyInput, resultsInput, options = {}
40
50
  return n ? winners[0] : null;
41
51
  };
42
52
  if (algorithm === 'majority-v1' || metric === 'vote-share-v1' || metric === 'normalized-entropy-v1')
43
- statistics.votes = votes();
53
+ statistics.votes = modal.votes;
44
54
  if (policy.primitive === 'truth-probability' && (algorithm === 'mean-probability-v1' || metric === 'jensen-shannon-v1')) {
45
55
  statistics.meanProbability = n ? round(succeeded.reduce((sum, result) => sum + result.value, 0) / n) : null;
46
56
  }
@@ -55,8 +65,10 @@ export function aggregateEnsembleResults(policyInput, resultsInput, options = {}
55
65
  const middle = Math.floor(n / 2);
56
66
  statistics.medianScore = round(n % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2);
57
67
  }
58
- if (algorithm === 'majority-v1')
59
- candidate = pick(statistics.votes);
68
+ if (algorithm === 'majority-v1') {
69
+ candidate = modal.value;
70
+ tie = modal.tie;
71
+ }
60
72
  else if (algorithm === 'mean-probability-v1')
61
73
  candidate = policy.primitive === 'choice' ? pick(statistics.meanDistribution) : statistics.meanProbability;
62
74
  else if (algorithm === 'score-distribution-mean-v1')
@@ -68,7 +80,8 @@ export function aggregateEnsembleResults(policyInput, resultsInput, options = {}
68
80
  tie = n % 2 === 0 && sorted[middle - 1] !== sorted[middle];
69
81
  candidate = tie ? sorted[middle - 1] : statistics.medianScore;
70
82
  }
71
- const disagreementValue = n === 0 ? 1 : disagreement(policy, statistics, distributions, n);
83
+ const disagreementValue = n === 0 ? 1 : metric === 'vote-share-v1' ? 1 - modal.agreementRatio
84
+ : disagreement(policy, statistics, distributions, n);
72
85
  const valueBps = Math.round(Math.min(1, Math.max(0, disagreementValue)) * 10_000);
73
86
  const exceeded = valueBps > policy.disagreement.thresholdBps;
74
87
  let disposition = 'accept';
@@ -249,9 +249,13 @@ function dispatchOrder(rows, seed) {
249
249
  return [...development, ...test];
250
250
  }
251
251
  export function prepareD17MultifactStudy(seed, moduleDigest, sourceDigests) {
252
- if (!/^[a-z0-9][a-z0-9-]{0,31}$/.test(seed) || !/^sha256:[a-f0-9]{64}$/.test(moduleDigest)
252
+ if (typeof seed === 'string' && seed.length > 32)
253
+ refuse('seed-too-long');
254
+ if (typeof seed !== 'string' || !/^[a-z0-9][a-z0-9-]{0,31}$/.test(seed))
255
+ refuse('seed-invalid');
256
+ if (!/^sha256:[a-f0-9]{64}$/.test(moduleDigest)
253
257
  || !Object.keys(sourceDigests).length || Object.values(sourceDigests).some(v => !/^sha256:[a-f0-9]{64}$/.test(v)))
254
- throw new Error('D17-MF source or seed pins');
258
+ refuse('source-pin-mismatch');
255
259
  const generated = [];
256
260
  const gold = { schemaVersion: 'decision-d17mf-gold/v1', labels: {}, worlds: {} };
257
261
  for (let index = 0; index < D17MF_ROWS; index++) {
@@ -17,10 +17,7 @@ export class EntryAdmissionError extends Error {
17
17
  }
18
18
  }
19
19
  /**
20
- * Starts an admission-budget clock, feature-detecting
21
- * `process.threadCpuUsage()` once for this reading. `elapsedAdmissionMs`
22
- * below reads the *type* of what this returns rather than re-detecting the
23
- * feature, so one admission check always uses one clock end to end.
20
+ * Starts an admission-budget clock using per-thread CPU time.
24
21
  *
25
22
  * Per-thread CPU time measures only the work the admitting call itself did
26
23
  * on this thread, so it is immune both to host scheduling contention
@@ -29,22 +26,12 @@ export class EntryAdmissionError extends Error {
29
26
  * other threads' or background GC's CPU time landing on this same process
30
27
  * (which a process-wide `process.cpuUsage()` clock would still charge here
31
28
  * -- not a safe substitute for the same reason `performance.now()` was not).
32
- *
33
- * `process.threadCpuUsage()` was added in Node v22.19.0 (per the installed
34
- * `@types/node` `@since` annotation) and is absent on this project's
35
- * `engines.node` floor (`>=20.0.0` in package.json) and on Node
36
- * 22.0.0-22.18.x. On a runtime without it, this falls back to the
37
- * pre-#3047 monotonic wall-clock guard as an explicitly documented
38
- * compatibility fallback: it restores the original host-load sensitivity on
39
- * those older runtimes, it does not resolve it there.
40
29
  */
41
30
  export function admissionClockStart() {
42
- return typeof process.threadCpuUsage === 'function' ? process.threadCpuUsage() : performance.now();
31
+ return process.threadCpuUsage();
43
32
  }
44
33
  /** Elapsed time since `start` (from `admissionClockStart`), in milliseconds. */
45
34
  export function elapsedAdmissionMs(start) {
46
- if (typeof start === 'number')
47
- return performance.now() - start;
48
35
  const diff = process.threadCpuUsage(start);
49
36
  return (diff.user + diff.system) / 1000;
50
37
  }
@@ -796,7 +796,7 @@ export async function dagLivePriorSpend(artifactRoot) {
796
796
  export async function assertDagLiveArtifactRoot(sourceRoot, artifactRoot) {
797
797
  let route;
798
798
  try {
799
- route = JSON.parse(execFileSync('aiwg', ['artifacts', 'path', '--json', '--check-write'], { cwd: sourceRoot, encoding: 'utf8', timeout: 30_000 }));
799
+ route = JSON.parse(execFileSync('aiwg', ['artifacts', 'path', '--json', '--check-write'], { cwd: sourceRoot, encoding: 'utf8', timeout: 30_000, killSignal: 'SIGKILL' }));
800
800
  }
801
801
  catch {
802
802
  throw new Error('D12 requires the canonical artifact root');
@@ -99,6 +99,15 @@ export function finalizeDecisionGraphRun(graph, plan, report, records, ceilings
99
99
  report.realizedResources.nodeRuns !== flowRecords.length + approvedGates ||
100
100
  report.realizedResources.tokens !== flowRecords.reduce((sum, item) => sum + item.response.usage.tokens, 0) ||
101
101
  Math.abs(report.realizedResources.costUsd - flowRecords.reduce((sum, item) => sum + item.response.usage.costUsd, 0)) > 1e-9) {
102
+ // Admission or settlement can reject (for example after a wall-clock deadline)
103
+ // before the adapter emits an observation. Flow retains that failure in its
104
+ // trace; surface the cause instead of misdiagnosing it as lost evidence.
105
+ // Still reject the run: an unobserved invocation cannot produce a receipt.
106
+ const failure = report.status === 'failed' && report.trace.find(event => event.type === 'node-attempt-failed' && event.nodeId && !observed.has(event.nodeId) &&
107
+ graph.nodes.some(node => node.id === event.nodeId) &&
108
+ report.checkpoint.failed?.includes(event.nodeId) && typeof event.message === 'string');
109
+ if (failure)
110
+ throw new DecisionGraphError(`Flow graph node '${failure.nodeId}' failed before observation: ${failure.message}`);
102
111
  throw new DecisionGraphError('missing Flow graph observation');
103
112
  }
104
113
  const candidates = graph.terminals.filter(id => observed.get(id)?.status === 'ok');
@@ -10,7 +10,7 @@ import { QUALIFICATION_PRIVACY_SURFACES, scanQualificationPrivacy } from '../qua
10
10
  import { executeQualificationPlan, verifyQualificationArtifacts, writeQualificationEvidenceManifest } from '../qualification/runner.js';
11
11
  import { qualificationIntegrityAllowlistProblems } from '../qualification/release.js';
12
12
  import { redactText } from '../../governance/redaction.js';
13
- import { heldoutDigest, heldoutRequest, heldoutReservationMicros, heldoutReservationTokens, HeldoutError, HELDOUT_ENV_GATE, heldoutRowsInScope, planHeldoutCollection, validateHeldoutBundle } from './contract.js';
13
+ import { heldoutDigest, heldoutRequest, heldoutReservationMicros, heldoutReservationTokens, HeldoutError, HELDOUT_ENV_GATE, heldoutRowsInScope, heldoutResumeAttempts, heldoutPendingRequests, planHeldoutCollection, validateHeldoutBundle } from './contract.js';
14
14
  import { appendHeldoutSpend, appendHeldoutEvent, heldoutDirectory, heldoutEvidenceDigest, heldoutRunsRoot, readHeldoutFile, readHeldoutJournal, reconcileHeldoutBaseline, scanHeldoutSpend, writeHeldoutFile, validateHeldoutJournal, heldoutCollectionAllowance } from './journal.js';
15
15
  import { readHeldoutFrozen, sealHeldoutCalibrationPhase, validateHeldoutPhaseAccess } from './calibration.js';
16
16
  import { D17_MULTIFACT_CALIBRATION_NOT_AFTER, usesD17MultifactGenerator } from './study-generators.js';
@@ -70,13 +70,7 @@ export async function collectHeldoutStudy(options) {
70
70
  throw new HeldoutError('prior-stop');
71
71
  if (!offline && prior.runs.some(run => run.corpusDigest === a.corpusDigest && run.source === 'injected-transport'))
72
72
  throw new HeldoutError('injected-prior');
73
- const old = prior.attempts.filter(attempt => attempt.study === a.study && attempt.corpusDigest === a.corpusDigest);
74
- if (old.some(attempt => attempt.preregistrationDigest !== a.preregistrationDigest))
75
- throw new HeldoutError('changed-preregistration');
76
- const latest = new Map();
77
- for (const attempt of old)
78
- if (attempt.ordinal >= (latest.get(key(attempt))?.ordinal ?? 0))
79
- latest.set(key(attempt), attempt);
73
+ const { old, latest } = heldoutResumeAttempts(bundle, prior.attempts);
80
74
  const now = options.now ?? Date.now, sleep = options.sleep ?? delay, started = now();
81
75
  // D17-MF (#2850) is scored only through the registered D17 member calibrator: a session whose whole dispatch window
82
76
  // (start plus the preregistered session limit) does not end before the calibrator expires never starts. A seed
@@ -127,20 +121,18 @@ export async function collectHeldoutStudy(options) {
127
121
  }
128
122
  if (failed.has(row.id) || overSlice(row.slice, row.split))
129
123
  continue;
130
- const remaining = row.requests.filter(request => latest.get(`${row.id}/${request.id}`)?.result?.disposition !== 'success');
124
+ const remaining = heldoutPendingRequests(row, plan, latest);
131
125
  if (!remaining.length)
132
126
  continue;
133
127
  // Checkpoint before a subject/paired arms: enough time for every bounded attempt and interval.
134
- const pairBound = remaining.length * (1 + plan.providerFailurePolicy.maxRetries) * (plan.requestTimeoutMs + plan.minDispatchIntervalMs);
128
+ const pairBound = remaining.reduce((n, request) => n + request.attempts, 0) * (plan.requestTimeoutMs + plan.minDispatchIntervalMs);
135
129
  if (now() - started + pairBound > plan.sessionLimitMs * 0.8) {
136
130
  checkpoint = true;
137
131
  break;
138
132
  }
139
- for (const request of remaining) {
133
+ for (const { request, first } of remaining) {
140
134
  if (reason || failed.has(row.id))
141
135
  break;
142
- const previous = latest.get(`${row.id}/${request.id}`);
143
- const first = previous ? previous.ordinal + 1 : 1;
144
136
  for (let ordinal = first; ordinal <= 1 + plan.providerFailurePolicy.maxRetries; ordinal++) {
145
137
  if (options.signal?.aborted) {
146
138
  reason = 'cancelled';