audit-tools 0.28.7 → 0.28.9

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 (209) hide show
  1. package/audit-code-wrapper-install-hosts.mjs +133 -27
  2. package/dist/audit/cli/confirmIntentStep.d.ts +1 -0
  3. package/dist/audit/cli/confirmIntentStep.d.ts.map +1 -1
  4. package/dist/audit/cli/confirmIntentStep.js +14 -10
  5. package/dist/audit/cli/confirmIntentStep.js.map +1 -1
  6. package/dist/audit/cli/dispatch/quotaPool.d.ts.map +1 -1
  7. package/dist/audit/cli/dispatch/quotaPool.js +4 -3
  8. package/dist/audit/cli/dispatch/quotaPool.js.map +1 -1
  9. package/dist/audit/cli/dispatch.d.ts +15 -1
  10. package/dist/audit/cli/dispatch.d.ts.map +1 -1
  11. package/dist/audit/cli/dispatch.js +54 -19
  12. package/dist/audit/cli/dispatch.js.map +1 -1
  13. package/dist/audit/cli/hybridDispatch.d.ts +46 -0
  14. package/dist/audit/cli/hybridDispatch.d.ts.map +1 -0
  15. package/dist/audit/cli/hybridDispatch.js +77 -0
  16. package/dist/audit/cli/hybridDispatch.js.map +1 -0
  17. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  18. package/dist/audit/cli/nextStepHelpers.js +111 -2
  19. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  20. package/dist/audit/cli/quotaCommand.d.ts.map +1 -1
  21. package/dist/audit/cli/quotaCommand.js +20 -10
  22. package/dist/audit/cli/quotaCommand.js.map +1 -1
  23. package/dist/audit/cli/reviewRun.d.ts +8 -0
  24. package/dist/audit/cli/reviewRun.d.ts.map +1 -1
  25. package/dist/audit/cli/reviewRun.js +1 -1
  26. package/dist/audit/cli/reviewRun.js.map +1 -1
  27. package/dist/audit/cli/rollingAuditDispatch.d.ts +57 -4
  28. package/dist/audit/cli/rollingAuditDispatch.d.ts.map +1 -1
  29. package/dist/audit/cli/rollingAuditDispatch.js +137 -10
  30. package/dist/audit/cli/rollingAuditDispatch.js.map +1 -1
  31. package/dist/audit/cli/scoreAuditCommand.d.ts +16 -0
  32. package/dist/audit/cli/scoreAuditCommand.d.ts.map +1 -0
  33. package/dist/audit/cli/scoreAuditCommand.js +82 -0
  34. package/dist/audit/cli/scoreAuditCommand.js.map +1 -0
  35. package/dist/audit/cli/submitPacketCommand.d.ts.map +1 -1
  36. package/dist/audit/cli/submitPacketCommand.js +8 -1
  37. package/dist/audit/cli/submitPacketCommand.js.map +1 -1
  38. package/dist/audit/cli.d.ts.map +1 -1
  39. package/dist/audit/cli.js +5 -1
  40. package/dist/audit/cli.js.map +1 -1
  41. package/dist/audit/orchestrator/designReviewProjection.d.ts +9 -1
  42. package/dist/audit/orchestrator/designReviewProjection.d.ts.map +1 -1
  43. package/dist/audit/orchestrator/designReviewProjection.js +19 -1
  44. package/dist/audit/orchestrator/designReviewProjection.js.map +1 -1
  45. package/dist/audit/orchestrator/designReviewPrompt.d.ts +2 -0
  46. package/dist/audit/orchestrator/designReviewPrompt.d.ts.map +1 -1
  47. package/dist/audit/orchestrator/designReviewPrompt.js +11 -1
  48. package/dist/audit/orchestrator/designReviewPrompt.js.map +1 -1
  49. package/dist/audit/orchestrator/executorRunners.js +1 -1
  50. package/dist/audit/orchestrator/executorRunners.js.map +1 -1
  51. package/dist/audit/orchestrator/intakeExecutors.d.ts +9 -1
  52. package/dist/audit/orchestrator/intakeExecutors.d.ts.map +1 -1
  53. package/dist/audit/orchestrator/intakeExecutors.js +16 -3
  54. package/dist/audit/orchestrator/intakeExecutors.js.map +1 -1
  55. package/dist/audit/orchestrator/intentCheckpointExecutor.d.ts.map +1 -1
  56. package/dist/audit/orchestrator/intentCheckpointExecutor.js +3 -0
  57. package/dist/audit/orchestrator/intentCheckpointExecutor.js.map +1 -1
  58. package/dist/audit/orchestrator/intentInterpreter.d.ts +21 -7
  59. package/dist/audit/orchestrator/intentInterpreter.d.ts.map +1 -1
  60. package/dist/audit/orchestrator/intentInterpreter.js +30 -11
  61. package/dist/audit/orchestrator/intentInterpreter.js.map +1 -1
  62. package/dist/audit/orchestrator/intentScopeDisposition.d.ts +49 -0
  63. package/dist/audit/orchestrator/intentScopeDisposition.d.ts.map +1 -0
  64. package/dist/audit/orchestrator/intentScopeDisposition.js +57 -0
  65. package/dist/audit/orchestrator/intentScopeDisposition.js.map +1 -0
  66. package/dist/audit/orchestrator/rollingDispatch.d.ts +9 -0
  67. package/dist/audit/orchestrator/rollingDispatch.d.ts.map +1 -1
  68. package/dist/audit/orchestrator/rollingDispatch.js +10 -0
  69. package/dist/audit/orchestrator/rollingDispatch.js.map +1 -1
  70. package/dist/audit/orchestrator/scope.d.ts +1 -0
  71. package/dist/audit/orchestrator/scope.d.ts.map +1 -1
  72. package/dist/audit/orchestrator/scope.js +2 -7
  73. package/dist/audit/orchestrator/scope.js.map +1 -1
  74. package/dist/audit/quota/index.d.ts +5 -0
  75. package/dist/audit/quota/index.d.ts.map +1 -1
  76. package/dist/audit/reporting/scoreAudit.d.ts +161 -0
  77. package/dist/audit/reporting/scoreAudit.d.ts.map +1 -0
  78. package/dist/audit/reporting/scoreAudit.js +224 -0
  79. package/dist/audit/reporting/scoreAudit.js.map +1 -0
  80. package/dist/audit/types/activeDispatch.d.ts +29 -1
  81. package/dist/audit/types/activeDispatch.d.ts.map +1 -1
  82. package/dist/audit/types/activeDispatch.js.map +1 -1
  83. package/dist/audit/types/analyzerCapability.d.ts +6 -6
  84. package/dist/audit/validation/auditResults.d.ts +13 -0
  85. package/dist/audit/validation/auditResults.d.ts.map +1 -1
  86. package/dist/audit/validation/auditResults.js +35 -12
  87. package/dist/audit/validation/auditResults.js.map +1 -1
  88. package/dist/remediate/contractPipeline/changeClassification.d.ts +146 -0
  89. package/dist/remediate/contractPipeline/changeClassification.d.ts.map +1 -0
  90. package/dist/remediate/contractPipeline/changeClassification.js +398 -0
  91. package/dist/remediate/contractPipeline/changeClassification.js.map +1 -0
  92. package/dist/remediate/contractPipeline/derive.d.ts.map +1 -1
  93. package/dist/remediate/contractPipeline/derive.js +9 -0
  94. package/dist/remediate/contractPipeline/derive.js.map +1 -1
  95. package/dist/remediate/intent/intentOrdering.d.ts +51 -0
  96. package/dist/remediate/intent/intentOrdering.d.ts.map +1 -0
  97. package/dist/remediate/intent/intentOrdering.js +150 -0
  98. package/dist/remediate/intent/intentOrdering.js.map +1 -0
  99. package/dist/remediate/phases/plan.d.ts.map +1 -1
  100. package/dist/remediate/phases/plan.js +14 -1
  101. package/dist/remediate/phases/plan.js.map +1 -1
  102. package/dist/remediate/providers/index.d.ts +32 -0
  103. package/dist/remediate/providers/index.d.ts.map +1 -1
  104. package/dist/remediate/providers/index.js +28 -1
  105. package/dist/remediate/providers/index.js.map +1 -1
  106. package/dist/remediate/steps/contractPipeline.d.ts +15 -1
  107. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  108. package/dist/remediate/steps/contractPipeline.js +282 -14
  109. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  110. package/dist/remediate/steps/dispatch.d.ts +56 -1
  111. package/dist/remediate/steps/dispatch.d.ts.map +1 -1
  112. package/dist/remediate/steps/dispatch.js +120 -29
  113. package/dist/remediate/steps/dispatch.js.map +1 -1
  114. package/dist/remediate/steps/nextStep.d.ts +53 -2
  115. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  116. package/dist/remediate/steps/nextStep.js +253 -81
  117. package/dist/remediate/steps/nextStep.js.map +1 -1
  118. package/dist/remediate/steps/providerNodeDispatch.d.ts +8 -1
  119. package/dist/remediate/steps/providerNodeDispatch.d.ts.map +1 -1
  120. package/dist/remediate/steps/providerNodeDispatch.js +7 -2
  121. package/dist/remediate/steps/providerNodeDispatch.js.map +1 -1
  122. package/dist/remediate/steps/rollingSession.d.ts +55 -1
  123. package/dist/remediate/steps/rollingSession.d.ts.map +1 -1
  124. package/dist/remediate/steps/rollingSession.js +128 -19
  125. package/dist/remediate/steps/rollingSession.js.map +1 -1
  126. package/dist/remediate/validation/contractPipelineGates.d.ts +23 -8
  127. package/dist/remediate/validation/contractPipelineGates.d.ts.map +1 -1
  128. package/dist/remediate/validation/contractPipelineGates.js +43 -38
  129. package/dist/remediate/validation/contractPipelineGates.js.map +1 -1
  130. package/dist/shared/dispatch/coordinator.d.ts +170 -0
  131. package/dist/shared/dispatch/coordinator.d.ts.map +1 -0
  132. package/dist/shared/dispatch/coordinator.js +177 -0
  133. package/dist/shared/dispatch/coordinator.js.map +1 -0
  134. package/dist/shared/dispatch/hybridDispatch.d.ts +74 -0
  135. package/dist/shared/dispatch/hybridDispatch.d.ts.map +1 -0
  136. package/dist/shared/dispatch/hybridDispatch.js +54 -0
  137. package/dist/shared/dispatch/hybridDispatch.js.map +1 -0
  138. package/dist/shared/dispatch/settledPools.d.ts +21 -0
  139. package/dist/shared/dispatch/settledPools.d.ts.map +1 -0
  140. package/dist/shared/dispatch/settledPools.js +34 -0
  141. package/dist/shared/dispatch/settledPools.js.map +1 -0
  142. package/dist/shared/index.d.ts +16 -6
  143. package/dist/shared/index.d.ts.map +1 -1
  144. package/dist/shared/index.js +15 -3
  145. package/dist/shared/index.js.map +1 -1
  146. package/dist/shared/intent/clauseInterpreter.d.ts +19 -0
  147. package/dist/shared/intent/clauseInterpreter.d.ts.map +1 -1
  148. package/dist/shared/intent/clauseInterpreter.js +19 -0
  149. package/dist/shared/intent/clauseInterpreter.js.map +1 -1
  150. package/dist/shared/io/json.d.ts +25 -0
  151. package/dist/shared/io/json.d.ts.map +1 -1
  152. package/dist/shared/io/json.js +67 -0
  153. package/dist/shared/io/json.js.map +1 -1
  154. package/dist/shared/providers/opencodeProvider.d.ts.map +1 -1
  155. package/dist/shared/providers/opencodeProvider.js +12 -0
  156. package/dist/shared/providers/opencodeProvider.js.map +1 -1
  157. package/dist/shared/providers/providerConfirmation.d.ts.map +1 -1
  158. package/dist/shared/providers/providerConfirmation.js +9 -1
  159. package/dist/shared/providers/providerConfirmation.js.map +1 -1
  160. package/dist/shared/providers/providerFactory.d.ts +1 -0
  161. package/dist/shared/providers/providerFactory.d.ts.map +1 -1
  162. package/dist/shared/providers/providerFactory.js +10 -8
  163. package/dist/shared/providers/providerFactory.js.map +1 -1
  164. package/dist/shared/providers/sharedProviderConfirmation.d.ts +134 -0
  165. package/dist/shared/providers/sharedProviderConfirmation.d.ts.map +1 -0
  166. package/dist/shared/providers/sharedProviderConfirmation.js +244 -0
  167. package/dist/shared/providers/sharedProviderConfirmation.js.map +1 -0
  168. package/dist/shared/quota/apiPool.d.ts +67 -0
  169. package/dist/shared/quota/apiPool.d.ts.map +1 -0
  170. package/dist/shared/quota/apiPool.js +139 -0
  171. package/dist/shared/quota/apiPool.js.map +1 -0
  172. package/dist/shared/quota/capacity.d.ts +29 -1
  173. package/dist/shared/quota/capacity.d.ts.map +1 -1
  174. package/dist/shared/quota/capacity.js +5 -0
  175. package/dist/shared/quota/capacity.js.map +1 -1
  176. package/dist/shared/quota/claimRegistry.d.ts +64 -0
  177. package/dist/shared/quota/claimRegistry.d.ts.map +1 -0
  178. package/dist/shared/quota/claimRegistry.js +157 -0
  179. package/dist/shared/quota/claimRegistry.js.map +1 -0
  180. package/dist/shared/quota/compositeQuotaSource.d.ts +10 -1
  181. package/dist/shared/quota/compositeQuotaSource.d.ts.map +1 -1
  182. package/dist/shared/quota/compositeQuotaSource.js +34 -6
  183. package/dist/shared/quota/compositeQuotaSource.js.map +1 -1
  184. package/dist/shared/quota/httpQuotaSource.d.ts +9 -1
  185. package/dist/shared/quota/httpQuotaSource.d.ts.map +1 -1
  186. package/dist/shared/quota/httpQuotaSource.js +33 -8
  187. package/dist/shared/quota/httpQuotaSource.js.map +1 -1
  188. package/dist/shared/quota/learnedQuotaSource.d.ts +7 -1
  189. package/dist/shared/quota/learnedQuotaSource.d.ts.map +1 -1
  190. package/dist/shared/quota/learnedQuotaSource.js +18 -7
  191. package/dist/shared/quota/learnedQuotaSource.js.map +1 -1
  192. package/dist/shared/quota/quotaSource.d.ts +41 -0
  193. package/dist/shared/quota/quotaSource.d.ts.map +1 -1
  194. package/dist/shared/quota/quotaSource.js +19 -0
  195. package/dist/shared/quota/quotaSource.js.map +1 -1
  196. package/dist/shared/types/contractPipeline/obligations.d.ts +42 -0
  197. package/dist/shared/types/contractPipeline/obligations.d.ts.map +1 -1
  198. package/dist/shared/types/contractPipeline.d.ts +1 -1
  199. package/dist/shared/types/contractPipeline.d.ts.map +1 -1
  200. package/dist/shared/types/contractPipeline.js.map +1 -1
  201. package/dist/shared/types/intentCheckpoint.d.ts +14 -0
  202. package/dist/shared/types/intentCheckpoint.d.ts.map +1 -1
  203. package/dist/shared/types/intentCheckpoint.js +10 -0
  204. package/dist/shared/types/intentCheckpoint.js.map +1 -1
  205. package/dist/shared/types/sessionConfig.d.ts +60 -0
  206. package/dist/shared/types/sessionConfig.d.ts.map +1 -1
  207. package/dist/shared/types/sessionConfig.js +13 -0
  208. package/dist/shared/types/sessionConfig.js.map +1 -1
  209. package/package.json +3 -2
@@ -2,15 +2,15 @@ import { existsSync } from "node:fs";
2
2
  import { mkdir, readFile, rename } from "node:fs/promises";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { StateStore } from "../state/store.js";
5
- import { readOptionalJsonFile, writeJsonFile, formatValidationIssues, isRecord, withFsRetry, RunLogger, DO_NOT_TOKEN_WRAP_NOTE, DISPATCH_PROMPT_HANDOFF_NOTE, coerceJsonObjectArg, createRollingDispatcher, setQuotaStateDir, interpretFreeFormIntent, advance } from "audit-tools/shared";
5
+ import { readOptionalJsonFile, writeJsonFile, formatValidationIssues, isRecord, withFsRetry, RunLogger, DO_NOT_TOKEN_WRAP_NOTE, DISPATCH_PROMPT_HANDOFF_NOTE, coerceJsonObjectArg, createRollingDispatcher, setQuotaStateDir, interpretFreeFormIntent, advance, planHybridDispatch, readSettledPools, addSettledPool, sourceByPoolId } from "audit-tools/shared";
6
6
  import { applyPlanPipeline, buildCoverageLedger } from "../phases/plan.js";
7
7
  import { groundExtractedFindings } from "../phases/grounding.js";
8
8
  import { runTriagePhase } from "../phases/triage.js";
9
9
  import { runClosePhase } from "../phases/close.js";
10
10
  import { validateRemediationPlan } from "../validation/remediationState.js";
11
- import { mergeImplementResults, prepareImplementDispatch, readExtractedPlanIfPresent, buildConfirmedPools, createWorktree, removeWorktree, resetNodeWorktreeAndBranch, acceptNodeWorktree, recordNodeAcceptOutcome, ensureWorktreeNodeModules, seedUntrackedDeclaredPaths, worktreePath, worktreeBranchForBlock, blockScopesFromPlan, } from "./dispatch.js";
11
+ import { mergeImplementResults, prepareImplementDispatch, readExtractedPlanIfPresent, buildConfirmedPools, executeNodeInWorktree, blockScopesFromPlan, declaredPathsFromPlan, } from "./dispatch.js";
12
12
  import { makeProviderNodeDispatcher } from "./providerNodeDispatch.js";
13
- import { prepareHostRollingDispatch } from "./rollingSession.js";
13
+ import { prepareHostRollingDispatch, nodeClaimRegistry, nodeSettledPoolsPath } from "./rollingSession.js";
14
14
  import { writeCurrentStep } from "./stepWriter.js";
15
15
  import { dependencyVerifiedComplete } from "./stepUtils.js";
16
16
  import { isTerminalStatus, isVerifiedCompleteStatus, isSkipStatus, } from "../state/itemStatus.js";
@@ -365,6 +365,28 @@ function resolvesToInProcessDispatchProvider(sessionConfig) {
365
365
  const provider = sessionConfig?.provider;
366
366
  return provider !== undefined && IN_PROCESS_DISPATCH_PROVIDERS.has(provider);
367
367
  }
368
+ /**
369
+ * Whether a confirmed pool is one the orchestrator launches IN-PROCESS this cycle
370
+ * (vs. the conversation host's subagent pool). The remediate classification for the
371
+ * shared `planHybridDispatch` split — single-sourced off the same provider set
372
+ * `resolvesToInProcessDispatchProvider` uses.
373
+ */
374
+ function isInProcessPool(pool) {
375
+ return IN_PROCESS_DISPATCH_PROVIDERS.has(pool.providerName);
376
+ }
377
+ /**
378
+ * Release a node's shared claim on a terminal accept (token-checked through the
379
+ * registry), then drop the held token. Idempotent: a node with no held token (a
380
+ * peer-owned skip, or a double-release) is a no-op. Single-sourced so both the
381
+ * success and the error paths of the in-process driver free the claim identically.
382
+ */
383
+ async function releaseNodeClaim(registry, claimTokens, blockId) {
384
+ const token = claimTokens.get(blockId);
385
+ if (!token)
386
+ return;
387
+ await registry.release(blockId, token);
388
+ claimTokens.delete(blockId);
389
+ }
368
390
  /**
369
391
  * Drive the implement phase through the in-process rolling engine. Engages the
370
392
  * shared `createRollingDispatcher` (via `driveRollingDispatch`) over
@@ -410,9 +432,19 @@ export async function driveRollingImplementDispatch(options) {
410
432
  const promptPathByBlock = new Map(plan.items
411
433
  .filter((i) => typeof i.block_id === "string")
412
434
  .map((i) => [i.block_id, i.prompt_path]));
435
+ // Confirmed pools: quota-derived concurrency, never the raw host flag (INV-QD-11).
436
+ const confirmedPools = await buildConfirmedPools({
437
+ sessionConfig: options.sessionConfig,
438
+ hostMaxConcurrent: options.waveOptions?.hostMaxConcurrent,
439
+ hostContextTokens: options.waveOptions?.hostContextTokens,
440
+ hostOutputTokens: options.waveOptions?.hostOutputTokens,
441
+ hostModels: options.waveOptions?.hostModels,
442
+ hostModelId: options.waveOptions?.hostModelId,
443
+ });
413
444
  // The live per-node worker: the configured provider, launched with the node's
414
445
  // worktree-rooted prompt and cwd = its worktree. Tests inject `options.dispatchNode`
415
- // to exercise the engine without spawning a real worker.
446
+ // to exercise the engine without spawning a real worker. A node on a source-backed
447
+ // pool launches FROM its source's config (A-8 generic dispatchable sources).
416
448
  const dispatchNode = options.dispatchNode ??
417
449
  makeProviderNodeDispatcher({
418
450
  root,
@@ -420,16 +452,8 @@ export async function driveRollingImplementDispatch(options) {
420
452
  runId,
421
453
  sessionConfig: options.sessionConfig,
422
454
  promptPathByBlock,
455
+ sourceByPoolId: sourceByPoolId(confirmedPools),
423
456
  });
424
- // Confirmed pools: quota-derived concurrency, never the raw host flag (INV-QD-11).
425
- const confirmedPools = await buildConfirmedPools({
426
- sessionConfig: options.sessionConfig,
427
- hostMaxConcurrent: options.waveOptions?.hostMaxConcurrent,
428
- hostContextTokens: options.waveOptions?.hostContextTokens,
429
- hostOutputTokens: options.waveOptions?.hostOutputTokens,
430
- hostModels: options.waveOptions?.hostModels,
431
- hostModelId: options.waveOptions?.hostModelId,
432
- });
433
457
  // Load state to partition the eligible frontier into rolling dependency levels.
434
458
  const state = await new StateStore(artifactsDir).loadState();
435
459
  if (!state)
@@ -441,78 +465,64 @@ export async function driveRollingImplementDispatch(options) {
441
465
  .map((level) => level.filter((b) => plannedBlockIds.has(b.block_id)))
442
466
  .filter((level) => level.length > 0);
443
467
  const nodeOutcomes = [];
468
+ // The SAME file-backed claim registry the host-subagent driver claims through
469
+ // (`nodeClaimRegistry`, keyed only to run + artifacts dir). Claiming a node here
470
+ // BEFORE creating its worktree makes the in-process and host-subagent drivers
471
+ // mutually exclusive on a node — exactly-one-claimant across both (A-10), the
472
+ // cross-driver double-dispatch guard. Owner tokens are held in-memory for the
473
+ // life of this run: a `rate_limited` re-queue re-enters for the same block, so a
474
+ // node already claimed by THIS driver reuses its token rather than self-colliding.
475
+ const registry = nodeClaimRegistry(artifactsDir, runId);
476
+ const claimTokens = new Map();
444
477
  // Per-node worktree dispatch + verify-before-accept, wrapped so the rolling
445
478
  // engine's dispatchNode callback always RESOLVES (never rejects).
446
479
  const dispatchNodeWithWorktree = async (block, slot) => {
447
- const branch = worktreeBranchForBlock(block.block_id, runId);
448
- const wt = worktreePath(root, block.block_id, runId);
449
480
  const resultPath = resultPathByBlock.get(block.block_id);
450
- try {
451
- // Idempotent reset of any worktree dir AND leftover branch from a prior
452
- // attempt before creating this node's isolated worktree. A `rate_limited`
453
- // re-queue re-enters this dispatcher for the same block with the branch
454
- // still present; a bare `createWorktree -b` would then fail with "branch
455
- // already exists". `resetNodeWorktreeAndBranch` clears both (+ prunes stale
456
- // admin entries) so every (re-)dispatch starts clean from HEAD.
457
- resetNodeWorktreeAndBranch(root, wt, branch);
458
- createWorktree(root, wt, branch);
459
- // A fresh worktree has no node_modules (gitignored); link the main checkout's
460
- // so this node's verify commands can run.
461
- ensureWorktreeNodeModules(root, wt);
462
- // Bring in declared targets that are untracked/ignored in the main tree so a
463
- // committed-files-only worktree can still see this node's own targets.
464
- // `touched_files` is the block's authoritative declared write set (the same
465
- // source the dispatch plan's write scope is derived from).
466
- seedUntrackedDeclaredPaths(root, wt, block.touched_files ?? []);
467
- const result = await dispatchNode({
468
- block,
469
- slot,
470
- worktreeRoot: wt,
471
- resultPath,
472
- });
473
- // Shared post-worker lifecycle (commit → verify → merge), identical to the
474
- // host-subagent driver's `accept-node` callback. Records the LIFECYCLE
475
- // outcome but returns the worker's TRANSPORT result to the engine (so a
476
- // rate_limited worker re-queues; a verify-failure routes to triage via merge).
477
- // Verify commands are DERIVED from the node's actually-touched tests inside
478
- // acceptNodeWorktree (post-commit) — omit them here so a host-authored path
479
- // can't mis-verify. The accept-time write-scope gate likewise adjudicates the
480
- // node's ACTUAL git edits (ground truth) against all blocks' declared scopes.
481
- const accept = acceptNodeWorktree({
482
- root,
483
- runId,
484
- blockId: block.block_id,
485
- worktreeRoot: wt,
486
- branch,
487
- workerOutcome: result.outcome,
488
- scope: { allBlockScopes },
489
- });
490
- // Persist the tool-owned verify/merge outcome so finalization blocks a node
491
- // that self-reported resolved but never actually landed (OBL-DS-06). Parity
492
- // with the host-subagent driver's `accept-node` callback.
493
- await recordNodeAcceptOutcome(artifactsDir, runId, block.block_id, accept);
494
- nodeOutcomes.push({
495
- block_id: block.block_id,
496
- outcome: accept.outcome,
497
- verify_passed: accept.verifyPassed,
498
- merged: accept.merged,
499
- });
500
- return result;
481
+ // Claim BEFORE any worktree work (CE-001). A node THIS driver already holds (a
482
+ // rate_limited re-queue) reuses its token. A node a PEER driver holds is its to
483
+ // run skip the worker + accept lifecycle entirely so we never double-dispatch
484
+ // or double-merge it; the run-level `mergeImplementResults` reconciles from the
485
+ // peer's accept outcome.
486
+ if (!claimTokens.has(block.block_id)) {
487
+ const claim = await registry.claim(block.block_id, "in-process");
488
+ if (!claim.acquired) {
489
+ nodeOutcomes.push({ block_id: block.block_id, outcome: "success", verify_passed: false, merged: false });
490
+ return {
491
+ packet: { id: block.block_id, payload: { block_id: block.block_id }, estimatedTokens: 0, complexity: 0.5 },
492
+ outcome: "success",
493
+ };
494
+ }
495
+ claimTokens.set(block.block_id, claim.ownerToken);
501
496
  }
502
- catch (err) {
503
- removeWorktree(root, wt);
504
- await recordNodeAcceptOutcome(artifactsDir, runId, block.block_id, {
505
- outcome: "error",
506
- verifyPassed: false,
507
- merged: false,
508
- });
509
- nodeOutcomes.push({ block_id: block.block_id, outcome: "error", verify_passed: false, merged: false });
510
- return {
511
- packet: { id: block.block_id, payload: { block_id: block.block_id }, estimatedTokens: 0, complexity: 0.5 },
512
- outcome: "error",
513
- error: err,
514
- };
497
+ // The shared per-node worktree lifecycle (reset → create → link node_modules →
498
+ // seed → dispatch → commit/verify/write-scope/merge → record), identical to the
499
+ // A-8 hybrid executor and behaviourally to the host-subagent driver's
500
+ // `accept-node` callback. `touched_files` is the block's authoritative declared
501
+ // write set (the source the dispatch plan's write scope is derived from).
502
+ const { result, accept } = await executeNodeInWorktree({
503
+ block,
504
+ slot,
505
+ root,
506
+ artifactsDir,
507
+ runId,
508
+ resultPath,
509
+ seedPaths: block.touched_files ?? [],
510
+ allBlockScopes,
511
+ dispatchNode,
512
+ });
513
+ nodeOutcomes.push({
514
+ block_id: block.block_id,
515
+ outcome: accept.outcome,
516
+ verify_passed: accept.verifyPassed,
517
+ merged: accept.merged,
518
+ });
519
+ // Release the claim ONLY on a terminal accept. A `rate_limited` worker re-queues
520
+ // (still owned work — keep the claim so a peer can't grab it mid-retry); success /
521
+ // error / timeout is terminal → free it through the shared registry (token-checked).
522
+ if (result.outcome !== "rate_limited") {
523
+ await releaseNodeClaim(registry, claimTokens, block.block_id);
515
524
  }
525
+ return result;
516
526
  };
517
527
  await driveRollingDispatch(levels, {
518
528
  confirmedPools,
@@ -531,6 +541,86 @@ export async function driveRollingImplementDispatch(options) {
531
541
  state_status: merged.status,
532
542
  };
533
543
  }
544
+ /**
545
+ * Flat per-node input-token estimate for the A-8 hybrid frontier split. The
546
+ * coordinator splits by per-pool SLOTS (concurrency) — host vs. backend — so a
547
+ * uniform estimate is sufficient here; it matches the default the in-process engine
548
+ * uses (`driveRollingDispatch`'s `() => 2000`), keeping the two paths consistent.
549
+ */
550
+ const HYBRID_NODE_TOKEN_ESTIMATE = 2000;
551
+ /**
552
+ * Run the A-8 coordinator's IN-PROCESS partition this cycle. Each node was already
553
+ * claimed by the coordinator and assigned to a backend pool (NIM / codex / …), so it
554
+ * is launched on THAT pool's provider — binding the slot's providerName to the
555
+ * per-node assignment is what routes a node to its assigned backend — cwd-confined to
556
+ * its worktree, through the shared `executeNodeInWorktree` lifecycle (commit → verify
557
+ * → write-scope → merge); then the coordinator's claim is released.
558
+ *
559
+ * Nodes run concurrently: the coordinator's proactive split already bounded the
560
+ * partition to the backend pools' capacity, so the partition size IS the safe
561
+ * concurrency. The host partition runs in parallel via the host-subagent driver;
562
+ * both write accept-outcome sidecars the run-level `mergeImplementResults` reconciles.
563
+ *
564
+ * Run-once (no in-pass re-queue): a node whose worker rate-limits or errors is not
565
+ * merged (its worktree drops) and is routed to triage by the deterministic merge —
566
+ * bounded, never a livelock. (Cross-cycle settled-pool re-balancing is a follow-up.)
567
+ */
568
+ export async function executeInProcessPartition(params) {
569
+ const { root, artifactsDir, runId, sessionConfig, partition, plan, coordinator } = params;
570
+ if (partition.length === 0)
571
+ return { nodes: [] };
572
+ const allBlockScopes = blockScopesFromPlan(plan);
573
+ const withBlockId = plan.items.filter((i) => typeof i.block_id === "string");
574
+ const promptPathByBlock = new Map(withBlockId.map((i) => [i.block_id, i.prompt_path]));
575
+ const resultPathByBlock = new Map(withBlockId.map((i) => [i.block_id, i.result_path]));
576
+ const state = await new StateStore(artifactsDir).loadState();
577
+ const blockById = new Map((state?.plan?.blocks ?? []).map((b) => [b.block_id, b]));
578
+ const dispatchNode = params.dispatchNode ??
579
+ makeProviderNodeDispatcher({
580
+ root,
581
+ artifactsDir,
582
+ runId,
583
+ sessionConfig,
584
+ promptPathByBlock,
585
+ sourceByPoolId: params.sourceByPoolId,
586
+ });
587
+ const nodes = await Promise.all(partition.map(async (a) => {
588
+ // `a` IS the coordinator's NodeAssignment — release it directly on terminal.
589
+ const resultPath = resultPathByBlock.get(a.nodeId);
590
+ if (!resultPath) {
591
+ // No prepared prompt/result for this node — release + mark error (the merge
592
+ // routes it to triage); never silently drop a claimed node.
593
+ await coordinator.release(a);
594
+ return { block_id: a.nodeId, outcome: "error", verify_passed: false, merged: false };
595
+ }
596
+ const block = blockById.get(a.nodeId) ?? { block_id: a.nodeId };
597
+ const slot = {
598
+ providerName: a.providerName,
599
+ hostModel: a.hostModel,
600
+ poolId: a.poolId,
601
+ };
602
+ const { accept } = await executeNodeInWorktree({
603
+ block,
604
+ slot,
605
+ root,
606
+ artifactsDir,
607
+ runId,
608
+ resultPath,
609
+ seedPaths: declaredPathsFromPlan(plan, a.nodeId),
610
+ allBlockScopes,
611
+ dispatchNode,
612
+ });
613
+ // Run-once → terminal; free the coordinator claim (token-checked).
614
+ await coordinator.release(a);
615
+ return {
616
+ block_id: a.nodeId,
617
+ outcome: accept.outcome,
618
+ verify_passed: accept.verifyPassed,
619
+ merged: accept.merged,
620
+ };
621
+ }));
622
+ return { nodes };
623
+ }
534
624
  /**
535
625
  * Whether `root` is the audit-tools monorepo — the repo the tool-owned final
536
626
  * gate's suite (INV-RS-10, literally the audit-tools build/check/per-package
@@ -969,7 +1059,89 @@ async function buildImplementDispatchStep(ctx) {
969
1059
  return { kind: "transition", state: await store.loadState() };
970
1060
  }
971
1061
  if (rollingEngineEnabled && canDispatchImpl) {
972
- const rolling = await prepareHostRollingDispatch({ root, artifactsDir }, runId, waveOptsImpl);
1062
+ // A-8 hybrid spill: when an in-process backend pool is ALSO confirmed (a
1063
+ // configured NIM/openai-compatible endpoint alongside the conversation host),
1064
+ // split the eligible frontier across BOTH pool classes via the shared
1065
+ // HybridSpillCoordinator (single claimant, proactive capacity split) — the
1066
+ // orchestrator runs the in-process partition THIS cycle while the host spawns
1067
+ // subagents for its partition. Pure host-subagent dispatch falls out when no
1068
+ // backend pool is confirmed (the coordinator has nothing to split against).
1069
+ const confirmedPools = await buildConfirmedPools({
1070
+ sessionConfig: sessionConfigImpl ?? null,
1071
+ hostMaxConcurrent: options.hostMaxConcurrent,
1072
+ hostContextTokens: options.hostContextTokens,
1073
+ hostOutputTokens: options.hostOutputTokens,
1074
+ hostModels: options.hostModels,
1075
+ hostModelId: options.hostModelId,
1076
+ });
1077
+ const backendPools = confirmedPools.filter(isInProcessPool);
1078
+ let rolling;
1079
+ if (backendPools.length > 0) {
1080
+ // Prepare the frontier ONCE (worktree-rooted prompts) so the coordinator
1081
+ // split, the in-process executor, and the host driver all read the same plan.
1082
+ const plan = await prepareImplementDispatch({ root, artifactsDir }, runId, undefined, {
1083
+ ...waveOptsImpl,
1084
+ worktreeRootedPrompts: true,
1085
+ });
1086
+ const frontier = plan.items
1087
+ .filter((i) => typeof i.block_id === "string")
1088
+ .map((i) => ({ id: i.block_id, estimatedTokens: HYBRID_NODE_TOKEN_ESTIMATE }));
1089
+ if (frontier.length === 0) {
1090
+ await mergeImplementResults({ root, artifactsDir }, runId);
1091
+ return { kind: "transition", state: await store.loadState() };
1092
+ }
1093
+ // One coordinator over the shared claim registry splits + claims each node to
1094
+ // exactly one pool. DC-4: the settled set is cross-cycle (persisted) — a backend
1095
+ // pool that exhausted on a prior cycle is excluded here, so its work falls to the
1096
+ // host-subagent pool instead of re-looping on a dead backend.
1097
+ const settledPath = nodeSettledPoolsPath(artifactsDir, runId);
1098
+ const settled = await readSettledPools(settledPath);
1099
+ const partition = await planHybridDispatch({
1100
+ frontier,
1101
+ pools: confirmedPools,
1102
+ sessionConfig: sessionConfigImpl ?? {},
1103
+ claimRegistry: nodeClaimRegistry(artifactsDir, runId),
1104
+ readSettled: () => settled,
1105
+ onSettle: async (id) => {
1106
+ settled.add(id);
1107
+ await addSettledPool(settledPath, id);
1108
+ },
1109
+ isInProcess: isInProcessPool,
1110
+ });
1111
+ // Run the in-process partition now (each node on its assigned backend pool,
1112
+ // launched FROM that pool's source config — A-8 generic dispatchable sources).
1113
+ const inProcessOutcome = await executeInProcessPartition({
1114
+ root,
1115
+ artifactsDir,
1116
+ runId,
1117
+ sessionConfig: sessionConfigImpl ?? null,
1118
+ partition: partition.inProcess,
1119
+ plan,
1120
+ coordinator: partition.coordinator,
1121
+ sourceByPoolId: sourceByPoolId(confirmedPools),
1122
+ });
1123
+ // DC-4: a backend pool whose node rate-limited is exhausted → settle it
1124
+ // (cross-cycle) so the next cycle routes its share to the host pool.
1125
+ const rateLimited = new Set(inProcessOutcome.nodes.filter((n) => n.outcome === "rate_limited").map((n) => n.block_id));
1126
+ const exhaustedPools = new Set(partition.inProcess.filter((a) => rateLimited.has(a.nodeId)).map((a) => a.poolId));
1127
+ for (const poolId of exhaustedPools) {
1128
+ await partition.coordinator.settlePool(poolId);
1129
+ }
1130
+ // The backend carried the whole batch (or every host node was contested by a
1131
+ // peer driver) → nothing for the host this cycle; merge what landed + transition.
1132
+ if (partition.host.length === 0) {
1133
+ await mergeImplementResults({ root, artifactsDir }, runId);
1134
+ return { kind: "transition", state: await store.loadState() };
1135
+ }
1136
+ // Hand the host partition (pre-claimed) to the host-subagent driver.
1137
+ rolling = await prepareHostRollingDispatch({ root, artifactsDir }, runId, waveOptsImpl, {
1138
+ plan,
1139
+ partition: partition.host.map((a) => ({ block_id: a.nodeId, ownerToken: a.ownerToken })),
1140
+ });
1141
+ }
1142
+ else {
1143
+ rolling = await prepareHostRollingDispatch({ root, artifactsDir }, runId, waveOptsImpl);
1144
+ }
973
1145
  // Everything eligible may already be done/skipped — fold straight to merge
974
1146
  // rather than emitting a dispatch step with zero nodes.
975
1147
  if (rolling.session.frontier.length === 0) {
@@ -2321,7 +2493,7 @@ Only \`scope_summary\` and \`intent_summary\` are required; add the optional fie
2321
2493
  "confirmed_by": "host",
2322
2494
  "scope_summary": "<the files/areas in scope>",
2323
2495
  "intent_summary": "<the goal, e.g. full-remediation / security-only>",
2324
- "free_form_intent": "<optional: guidance threaded into remediation worker prompts>",
2496
+ "free_form_intent": "<optional: interpreted into lens/priority ordering at planning; never threaded verbatim into worker prompts>",
2325
2497
  "filters": {
2326
2498
  "severity": ["critical", "high"],
2327
2499
  "lenses": ["security", "reliability"],