audit-tools 0.30.49 → 0.30.53

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 (165) hide show
  1. package/dist/audit/cli/advanceAuditCommand.d.ts.map +1 -1
  2. package/dist/audit/cli/advanceAuditCommand.js +8 -4
  3. package/dist/audit/cli/advanceAuditCommand.js.map +1 -1
  4. package/dist/audit/cli/args.d.ts.map +1 -1
  5. package/dist/audit/cli/args.js +9 -2
  6. package/dist/audit/cli/args.js.map +1 -1
  7. package/dist/audit/cli/mergeAndIngestCommand.d.ts.map +1 -1
  8. package/dist/audit/cli/mergeAndIngestCommand.js +6 -22
  9. package/dist/audit/cli/mergeAndIngestCommand.js.map +1 -1
  10. package/dist/audit/cli/prompts.d.ts.map +1 -1
  11. package/dist/audit/cli/prompts.js +3 -1
  12. package/dist/audit/cli/prompts.js.map +1 -1
  13. package/dist/audit/cli/validateResultCommand.d.ts.map +1 -1
  14. package/dist/audit/cli/validateResultCommand.js +19 -6
  15. package/dist/audit/cli/validateResultCommand.js.map +1 -1
  16. package/dist/audit/cli/workerRunCommand.d.ts.map +1 -1
  17. package/dist/audit/cli/workerRunCommand.js +6 -21
  18. package/dist/audit/cli/workerRunCommand.js.map +1 -1
  19. package/dist/audit/contracts/workerSchemas.d.ts +27 -3
  20. package/dist/audit/contracts/workerSchemas.d.ts.map +1 -1
  21. package/dist/audit/contracts/workerSchemas.js +5 -1
  22. package/dist/audit/contracts/workerSchemas.js.map +1 -1
  23. package/dist/audit/extractors/analyzers/html.d.ts +1 -0
  24. package/dist/audit/extractors/analyzers/html.d.ts.map +1 -1
  25. package/dist/audit/extractors/analyzers/html.js +5 -3
  26. package/dist/audit/extractors/analyzers/html.js.map +1 -1
  27. package/dist/audit/extractors/browserExtension.d.ts.map +1 -1
  28. package/dist/audit/extractors/browserExtension.js +1 -8
  29. package/dist/audit/extractors/browserExtension.js.map +1 -1
  30. package/dist/audit/extractors/graphRoutes.d.ts.map +1 -1
  31. package/dist/audit/extractors/graphRoutes.js +4 -1
  32. package/dist/audit/extractors/graphRoutes.js.map +1 -1
  33. package/dist/audit/orchestrator/autoFixExecutor.d.ts +8 -0
  34. package/dist/audit/orchestrator/autoFixExecutor.d.ts.map +1 -1
  35. package/dist/audit/orchestrator/autoFixExecutor.js +68 -35
  36. package/dist/audit/orchestrator/autoFixExecutor.js.map +1 -1
  37. package/dist/audit/orchestrator/rollingDispatch.d.ts +2 -2
  38. package/dist/audit/orchestrator/rollingDispatch.d.ts.map +1 -1
  39. package/dist/audit/orchestrator/runtimeCommand.d.ts.map +1 -1
  40. package/dist/audit/orchestrator/runtimeCommand.js +7 -1
  41. package/dist/audit/orchestrator/runtimeCommand.js.map +1 -1
  42. package/dist/audit/orchestrator/selectiveDeepening/index.d.ts.map +1 -1
  43. package/dist/audit/orchestrator/selectiveDeepening/index.js +12 -8
  44. package/dist/audit/orchestrator/selectiveDeepening/index.js.map +1 -1
  45. package/dist/audit/orchestrator/taskBuilder.d.ts.map +1 -1
  46. package/dist/audit/orchestrator/taskBuilder.js +0 -32
  47. package/dist/audit/orchestrator/taskBuilder.js.map +1 -1
  48. package/dist/audit/quota/discoveredLimits.d.ts +12 -0
  49. package/dist/audit/quota/discoveredLimits.d.ts.map +1 -1
  50. package/dist/audit/quota/discoveredLimits.js +30 -25
  51. package/dist/audit/quota/discoveredLimits.js.map +1 -1
  52. package/dist/audit/quota/index.d.ts +120 -18
  53. package/dist/audit/quota/index.d.ts.map +1 -1
  54. package/dist/audit/types/auditScope.d.ts +2 -2
  55. package/dist/audit/validation/auditResults.d.ts +16 -0
  56. package/dist/audit/validation/auditResults.d.ts.map +1 -1
  57. package/dist/audit/validation/auditResults.js +35 -1
  58. package/dist/audit/validation/auditResults.js.map +1 -1
  59. package/dist/remediate/phases/close.d.ts.map +1 -1
  60. package/dist/remediate/phases/close.js +16 -6
  61. package/dist/remediate/phases/close.js.map +1 -1
  62. package/dist/remediate/steps/contractPipeline.d.ts +4 -1
  63. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  64. package/dist/remediate/steps/contractPipeline.js +63 -47
  65. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  66. package/dist/remediate/steps/contractPipelinePrompts.d.ts +12 -1
  67. package/dist/remediate/steps/contractPipelinePrompts.d.ts.map +1 -1
  68. package/dist/remediate/steps/contractPipelinePrompts.js +29 -18
  69. package/dist/remediate/steps/contractPipelinePrompts.js.map +1 -1
  70. package/dist/remediate/steps/dispatch.d.ts +20 -3
  71. package/dist/remediate/steps/dispatch.d.ts.map +1 -1
  72. package/dist/remediate/steps/dispatch.js +221 -117
  73. package/dist/remediate/steps/dispatch.js.map +1 -1
  74. package/dist/remediate/steps/finalGate.d.ts +88 -0
  75. package/dist/remediate/steps/finalGate.d.ts.map +1 -0
  76. package/dist/remediate/steps/finalGate.js +171 -0
  77. package/dist/remediate/steps/finalGate.js.map +1 -0
  78. package/dist/remediate/steps/leanFastPath.d.ts.map +1 -1
  79. package/dist/remediate/steps/leanFastPath.js +12 -0
  80. package/dist/remediate/steps/leanFastPath.js.map +1 -1
  81. package/dist/remediate/steps/nextStep.d.ts +12 -82
  82. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  83. package/dist/remediate/steps/nextStep.js +142 -170
  84. package/dist/remediate/steps/nextStep.js.map +1 -1
  85. package/dist/remediate/steps/types.d.ts +1 -1
  86. package/dist/remediate/steps/types.d.ts.map +1 -1
  87. package/dist/remediate/steps/types.js.map +1 -1
  88. package/dist/remediate/validation/contractPipeline.d.ts.map +1 -1
  89. package/dist/remediate/validation/contractPipeline.js +17 -3
  90. package/dist/remediate/validation/contractPipeline.js.map +1 -1
  91. package/dist/shared/dispatch/rollingDispatch.d.ts +22 -1
  92. package/dist/shared/dispatch/rollingDispatch.d.ts.map +1 -1
  93. package/dist/shared/dispatch/rollingDispatch.js +147 -7
  94. package/dist/shared/dispatch/rollingDispatch.js.map +1 -1
  95. package/dist/shared/index.d.ts +6 -4
  96. package/dist/shared/index.d.ts.map +1 -1
  97. package/dist/shared/index.js +5 -3
  98. package/dist/shared/index.js.map +1 -1
  99. package/dist/shared/io/frictionCapture.d.ts +14 -1
  100. package/dist/shared/io/frictionCapture.d.ts.map +1 -1
  101. package/dist/shared/io/frictionCapture.js +28 -3
  102. package/dist/shared/io/frictionCapture.js.map +1 -1
  103. package/dist/shared/observability/runLog.d.ts +1 -1
  104. package/dist/shared/observability/runLog.d.ts.map +1 -1
  105. package/dist/shared/observability/runLog.js.map +1 -1
  106. package/dist/shared/providers/providerConfirmation.d.ts +16 -1
  107. package/dist/shared/providers/providerConfirmation.d.ts.map +1 -1
  108. package/dist/shared/providers/providerConfirmation.js +13 -13
  109. package/dist/shared/providers/providerConfirmation.js.map +1 -1
  110. package/dist/shared/providers/providerFactory.d.ts.map +1 -1
  111. package/dist/shared/providers/providerFactory.js +10 -14
  112. package/dist/shared/providers/providerFactory.js.map +1 -1
  113. package/dist/shared/providers/providerPathGuard.d.ts +21 -0
  114. package/dist/shared/providers/providerPathGuard.d.ts.map +1 -0
  115. package/dist/shared/providers/providerPathGuard.js +69 -0
  116. package/dist/shared/providers/providerPathGuard.js.map +1 -0
  117. package/dist/shared/providers/sharedProviderConfirmation.d.ts +14 -2
  118. package/dist/shared/providers/sharedProviderConfirmation.d.ts.map +1 -1
  119. package/dist/shared/providers/sharedProviderConfirmation.js +26 -5
  120. package/dist/shared/providers/sharedProviderConfirmation.js.map +1 -1
  121. package/dist/shared/quota/apiPool.d.ts +6 -1
  122. package/dist/shared/quota/apiPool.d.ts.map +1 -1
  123. package/dist/shared/quota/apiPool.js +13 -2
  124. package/dist/shared/quota/apiPool.js.map +1 -1
  125. package/dist/shared/quota/capacity.d.ts +87 -8
  126. package/dist/shared/quota/capacity.d.ts.map +1 -1
  127. package/dist/shared/quota/capacity.js +27 -2
  128. package/dist/shared/quota/capacity.js.map +1 -1
  129. package/dist/shared/quota/claudeOAuthQuotaSource.d.ts.map +1 -1
  130. package/dist/shared/quota/claudeOAuthQuotaSource.js +56 -0
  131. package/dist/shared/quota/claudeOAuthQuotaSource.js.map +1 -1
  132. package/dist/shared/quota/fileLock.d.ts +17 -2
  133. package/dist/shared/quota/fileLock.d.ts.map +1 -1
  134. package/dist/shared/quota/fileLock.js +21 -19
  135. package/dist/shared/quota/fileLock.js.map +1 -1
  136. package/dist/shared/quota/quotaSource.d.ts +62 -2
  137. package/dist/shared/quota/quotaSource.d.ts.map +1 -1
  138. package/dist/shared/quota/quotaSource.js +24 -0
  139. package/dist/shared/quota/quotaSource.js.map +1 -1
  140. package/dist/shared/quota/scheduler.d.ts +19 -2
  141. package/dist/shared/quota/scheduler.d.ts.map +1 -1
  142. package/dist/shared/quota/scheduler.js +162 -86
  143. package/dist/shared/quota/scheduler.js.map +1 -1
  144. package/dist/shared/quota/state.d.ts +22 -0
  145. package/dist/shared/quota/state.d.ts.map +1 -1
  146. package/dist/shared/quota/state.js +59 -0
  147. package/dist/shared/quota/state.js.map +1 -1
  148. package/dist/shared/quota/tokenBudgetView.d.ts +17 -0
  149. package/dist/shared/quota/tokenBudgetView.d.ts.map +1 -0
  150. package/dist/shared/quota/tokenBudgetView.js +77 -0
  151. package/dist/shared/quota/tokenBudgetView.js.map +1 -0
  152. package/dist/shared/quota/types.d.ts +19 -1
  153. package/dist/shared/quota/types.d.ts.map +1 -1
  154. package/dist/shared/quota/types.js +1 -2
  155. package/dist/shared/quota/types.js.map +1 -1
  156. package/dist/shared/repair/brokeredDispatch.d.ts.map +1 -1
  157. package/dist/shared/repair/brokeredDispatch.js +5 -3
  158. package/dist/shared/repair/brokeredDispatch.js.map +1 -1
  159. package/dist/shared/types/providerConfirmation.d.ts +9 -0
  160. package/dist/shared/types/providerConfirmation.d.ts.map +1 -1
  161. package/dist/shared/types/sessionConfig.d.ts +0 -3
  162. package/dist/shared/types/sessionConfig.d.ts.map +1 -1
  163. package/dist/shared/types/sessionConfig.js.map +1 -1
  164. package/package.json +2 -2
  165. package/scripts/audit/postinstall.mjs +22 -14
@@ -2,7 +2,7 @@ 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, writeTextFile, buildAuditDeliverablePair, formatValidationIssues, isRecord, withFsRetry, RunLogger, DISPATCH_PROMPT_HANDOFF_NOTE, renderQuotaCoverageNudge, coerceJsonObjectArg, createRollingDispatcher, setQuotaStateDir, interpretFreeFormIntent, advance, decideFrictionTriage, buildFrictionTriageBlock, planHybridDispatch, readSettledPools, addSettledPool, sourceByPoolId, classifyProvider, selectDispatchDriver, renderDispatchDriverInstruction, HostSessionQuotaSource, buildProviderModelKey, captureStepBoundaryFriction } from "audit-tools/shared";
5
+ import { readOptionalJsonFile, writeJsonFile, writeTextFile, buildAuditDeliverablePair, formatValidationIssues, isRecord, withFsRetry, RunLogger, DISPATCH_PROMPT_HANDOFF_NOTE, renderQuotaCoverageNudge, renderTokenBudgetView, coerceJsonObjectArg, createRollingDispatcher, setQuotaStateDir, interpretFreeFormIntent, advance, decideFrictionTriage, buildFrictionTriageBlock, planHybridDispatch, readSettledPools, addSettledPool, sourceByPoolId, classifyProvider, selectDispatchDriver, renderDispatchDriverInstruction, HostSessionQuotaSource, buildProviderModelKey, captureStepBoundaryFriction, LENSES, SEVERITIES } 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";
@@ -14,12 +14,11 @@ import { ownershipSubWaves } from "../dispatch/ownershipScheduler.js";
14
14
  import { prepareHostRollingDispatch, nodeClaimRegistry, nodeSettledPoolsPath } from "./rollingSession.js";
15
15
  import { writeCurrentStep } from "./stepWriter.js";
16
16
  import { dependencyVerifiedComplete } from "./stepUtils.js";
17
- import { isTerminalStatus, isVerifiedCompleteStatus, isSkipStatus, } from "../state/itemStatus.js";
17
+ import { isTerminalStatus, isVerifiedCompleteStatus, } from "../state/itemStatus.js";
18
18
  import { deduplicateCrossLensFindings, fixupBlocksAfterDedup, } from "../dedup/crossLensDedup.js";
19
19
  import { checkAffectedFileIntegrity } from "../utils/fileIntegrity.js";
20
20
  import { resolveIntakeStep } from "./intakeResolver.js";
21
- import { runCommand } from "../utils/commands.js";
22
- import { isAuditToolsMonorepo, toolOwnedFinalGateCommands, } from "./gateCommands.js";
21
+ import { runToolOwnedFinalGate, applyCoarseReblock, readFinalGateSidecar, writeFinalGateSidecar, } from "./finalGate.js";
23
22
  import { buildNextContractPipelineStep, shouldEnterContractPipeline, writePathASeedFromFindings, } from "./contractPipeline.js";
24
23
  import { evaluateFastPath, buildLeanExtractedPlan, distinctAffectedFiles, interpretLeanLightReviewVerdict, LEAN_LIGHT_REVIEW_SCHEMA_VERSION, } from "./leanFastPath.js";
25
24
  import { contractArtifactExists, contractPipelineDir, } from "../contractPipeline/artifactStore.js";
@@ -29,6 +28,26 @@ import { runFindingFilterPass } from "../findingFilter.js";
29
28
  import { intakePaths, isIntakeReady, readIntakeArtifacts, resolveManifestSources, } from "../intake.js";
30
29
  import { ensureIntakeRiskSignal, readIntakeRiskSignal, writeIntakeRiskSignal, escalateRiskSignal, } from "../riskSignal.js";
31
30
  import { ambiguityReviewPrompt, clarificationPrompt, collectIntakeClarificationsPrompt, collectStartingPointPrompt, loaderCommand, reviewApprovalPrompt, synthesizeIntakePrompt, triagePrompt, } from "./prompts.js";
31
+ // Single-sourced prose renders of the canonical lens / severity vocabularies
32
+ // (`audit-tools/shared` `LENSES` / `SEVERITIES`) for the intent-checkpoint
33
+ // prompt copy. Previously these 11-lens / 5-severity lists were hand-copied as
34
+ // backtick-quoted literals in three places in this file and would silently drift
35
+ // from the canonical enum (the very drift `types/lens.ts` exists to prevent).
36
+ const VALID_LENSES_PROSE = LENSES.map((lens) => `\`${lens}\``).join(", ");
37
+ const VALID_SEVERITIES_PROSE = SEVERITIES.map((sev) => `\`${sev}\``).join(", ");
38
+ /**
39
+ * The host's resolved provider identity for quota-key / driver-classification
40
+ * purposes: the configured `sessionConfig.provider`, defaulting to the
41
+ * conversation host (`claude-code`) when unset or `auto`. Single-sourced so the
42
+ * `?? "claude-code"` default and the `auto`-exclusion live in ONE place rather
43
+ * than being re-spelled (with an ad-hoc cast) at each dispatch call site.
44
+ */
45
+ function resolveHostProviderName(sessionConfig) {
46
+ const provider = sessionConfig?.provider;
47
+ if (provider === undefined || provider === "auto")
48
+ return "claude-code";
49
+ return provider;
50
+ }
32
51
  export function resolveHostDispatchCapability(options) {
33
52
  if (options.hostCanDispatchSubagents !== undefined) {
34
53
  return options.hostCanDispatchSubagents;
@@ -498,11 +517,42 @@ export async function driveRollingDispatch(levels, options) {
498
517
  });
499
518
  dispatcher.enqueue(packets);
500
519
  levelResults.push(...(await dispatcher.run()));
520
+ // Piece D: surface the wave's partial-completion terminal (quota_paused /
521
+ // empty_pool). Aggregate across waves keeping the EARLIEST-reset quota_paused
522
+ // terminal (a paused wave is retryable and should resume soonest); union the
523
+ // stranded ids so a later step re-dispatches all of them. An empty_pool
524
+ // terminal only stands when no quota_paused terminal was seen.
525
+ out.terminal = mergePartialTerminals(out.terminal, dispatcher.getTerminal());
501
526
  }
502
527
  out.levels.push({ blockIds: level.map((b) => b.block_id), results: levelResults });
503
528
  }
504
529
  return out;
505
530
  }
531
+ /**
532
+ * Fold a newly-observed partial-completion terminal into the run's aggregate
533
+ * (piece D). Prefers `quota_paused` (retryable) over `empty_pool`; among
534
+ * quota_paused terminals keeps the EARLIEST `earliest_reset_at`; always unions the
535
+ * stranded ids so no stranded node is lost across waves.
536
+ */
537
+ function mergePartialTerminals(prior, next) {
538
+ if (!next)
539
+ return prior;
540
+ if (!prior)
541
+ return next;
542
+ const strandedIds = [...new Set([...prior.stranded_ids, ...next.stranded_ids])];
543
+ const priorPaused = prior.reason === "quota_paused";
544
+ const nextPaused = next.reason === "quota_paused";
545
+ if (priorPaused || nextPaused) {
546
+ const resets = [prior.earliest_reset_at, next.earliest_reset_at].filter((r) => typeof r === "string");
547
+ const earliest = resets.length > 0 ? resets.sort()[0] : undefined;
548
+ return {
549
+ reason: "quota_paused",
550
+ stranded_ids: strandedIds,
551
+ ...(earliest ? { earliest_reset_at: earliest } : {}),
552
+ };
553
+ }
554
+ return { reason: prior.reason, stranded_ids: strandedIds };
555
+ }
506
556
  /**
507
557
  * Backends the orchestrator can drive IN-PROCESS as the per-node implement worker
508
558
  * via `driveRollingImplementDispatch` (it resolves + launches the provider with each
@@ -599,8 +649,7 @@ export async function driveRollingImplementDispatch(options) {
599
649
  // onEscalation routes to the single step-boundary friction chokepoint with this
600
650
  // driver's artifactsDir/runId, so a bounded account-wall escalation surfaces as
601
651
  // reviewable friction instead of only a stderr line.
602
- const providerName = options.sessionConfig?.provider ??
603
- "claude-code";
652
+ const providerName = resolveHostProviderName(options.sessionConfig);
604
653
  const hostSessionModelKey = buildProviderModelKey(providerName, options.sessionConfig
605
654
  ?.block_quota?.host_model ??
606
655
  options.waveOptions?.hostModelId ??
@@ -716,7 +765,7 @@ export async function driveRollingImplementDispatch(options) {
716
765
  // the merge-time write-scope gate reads — for file-ownership-disjoint admission
717
766
  // (INV-SOO). A block with no plan scope falls back to its touched_files.
718
767
  const writePathsByBlock = new Map(allBlockScopes.map((s) => [s.block_id, s.write_paths]));
719
- await driveRollingDispatch(levels, {
768
+ const driven = await driveRollingDispatch(levels, {
720
769
  confirmedPools,
721
770
  sessionConfig: options.sessionConfig ?? {},
722
771
  dispatchNode: dispatchNodeWithWorktree,
@@ -726,6 +775,19 @@ export async function driveRollingImplementDispatch(options) {
726
775
  hostSession,
727
776
  scopeForBlock: (block) => writePathsByBlock.get(block.block_id) ?? block.touched_files ?? [],
728
777
  });
778
+ // Piece D — persist the rolling engine's partial-completion terminal onto state
779
+ // BEFORE the merge, so the merge can SKIP-block the quota_paused stranded nodes
780
+ // (their worker rate-limited → no result file, but they must stay PENDING for a
781
+ // later step to redispatch clean — the worktrees were PRESERVED by
782
+ // acceptNodeWorktree). An `empty_pool` terminal does not affect the merge (its
783
+ // nodes are genuine failures); the `partial_terminal` obligation blocks them.
784
+ if (driven.terminal) {
785
+ const pre = await new StateStore(artifactsDir).loadState();
786
+ if (pre) {
787
+ pre.partial_completion_terminal = driven.terminal;
788
+ await new StateStore(artifactsDir).saveState(pre);
789
+ }
790
+ }
729
791
  // Deterministic merge: same path the wave flow uses (tolerant remap, write-scope
730
792
  // gate against each verified branch, lost-update detection). Worktrees are
731
793
  // already removed; the merge reads each node's result file + branch diff.
@@ -821,161 +883,13 @@ export async function executeInProcessPartition(params) {
821
883
  // Tool-owned final completion gate (INV-RS-10) + coarse re-block (INV-RS-09)
822
884
  // ---------------------------------------------------------------------------
823
885
  //
824
- // INV-RS-10: the final completion gate is a TOOL-OWNED, NON-VACUOUS suite that
825
- // is INDEPENDENT of any `plan.test_command`. A run can only land green when this
826
- // suite passes; a vacuous/unset `plan.test_command` can never substitute for it.
827
- // The suite is executed through the env-scrubbing `runTracked` path
828
- // (`runCommand`), which strips CLAUDECODE / CLAUDE_CODE_* so the gate runs in a
829
- // clean environment regardless of the host session.
830
- //
831
- // Hard floor (always run, in order — single package, single-flight build — CE-001):
832
- // 1. npm run build (one tsc build for the whole package)
833
- // 2. npm run check (typecheck, no emit)
834
- // 3. BUILD-FREE unit suites at the repo root, each invoked directly so dist is
835
- // never rebuilt or raced:
836
- // - shared+audit: node --import tsx/esm --test tests/shared/*.test.mjs tests/audit/*.test.mjs
837
- // - remediate-code: npx vitest run
838
- //
839
- // CE-002: the hard floor is scoped to build + typecheck + unit. The
840
- // runtime/packaged-bin smoke surface (the `verify:release` smokes) is recorded
841
- // as a DECLARED RESIDUAL the floor does not gate, rather than run inline — the
842
- // packaged-bin smokes are the known Windows-flaky / EPERM surface and an in-loop
843
- // gate must converge deterministically, so they are surfaced for a separate
844
- // pass instead of being able to strand the run.
845
- // `FinalGateCommandSpec`, `isAuditToolsMonorepo`, and `toolOwnedFinalGateCommands`
846
- // are single-sourced in the leaf module `gateCommands.ts` (so `dispatch.ts` can
847
- // derive the same pinned merged-base check without an import cycle). Imported above
848
- // for local use here and re-exported to preserve this module's public surface +
849
- // test imports.
850
- export { isAuditToolsMonorepo, toolOwnedFinalGateCommands };
851
- /**
852
- * Run the tool-owned final gate (INV-RS-10). Each command runs through
853
- * `runCommand` → shared `runTracked`, which scrubs CLAUDECODE / CLAUDE_CODE_*.
854
- * The first failing command short-circuits the floor (a broken build makes the
855
- * later layers meaningless). A `runner` may be injected for tests. When the
856
- * audit-tools suite does not apply (non-monorepo target), the gate is
857
- * `scoped_out` (does not block) rather than vacuously passing.
858
- */
859
- export async function runToolOwnedFinalGate(root, opts = {}) {
860
- const runtime_residual = {
861
- surface: "runtime/packaged-bin smokes (verify:release)",
862
- commands: [
863
- "npm run smoke:packaged-audit-code",
864
- "npm run smoke:packaged-remediate-code",
865
- ],
866
- };
867
- const commands = toolOwnedFinalGateCommands(root);
868
- if (commands.length === 0) {
869
- // Audit-tools-specific suite does not apply here — declared scope, not a
870
- // vacuous pass (it never substitutes for a real gate on the audit-tools repo).
871
- return { passed: true, results: [], scoped_out: true, runtime_residual };
872
- }
873
- const runner = opts.runner ??
874
- ((argv, cwd, packageDir) => {
875
- const [command, ...args] = argv;
876
- // Package-scoped unit suites run with cwd at the package (no `npm -w`); the
877
- // monorepo-root build/check commands run at the repo root.
878
- const effectiveCwd = packageDir ? join(root, packageDir) : cwd;
879
- // runCommand → runTracked strips CLAUDECODE / CLAUDE_CODE_* (INV-RS-10).
880
- const result = runCommand(command, args, {
881
- cwd: effectiveCwd,
882
- stdio: ["ignore", "pipe", "pipe"],
883
- });
884
- return { status: result.status };
885
- });
886
- const results = [];
887
- let passed = true;
888
- for (const spec of commands) {
889
- const { status } = runner(spec.argv, root, spec.package_dir);
890
- const cmdPassed = status === 0;
891
- results.push({
892
- argv: spec.argv,
893
- layer: spec.layer,
894
- ...(spec.package_dir ? { package_dir: spec.package_dir } : {}),
895
- exit_code: status,
896
- passed: cmdPassed,
897
- });
898
- if (!cmdPassed) {
899
- passed = false;
900
- break; // short-circuit: later layers are meaningless on a broken floor
901
- }
902
- }
903
- return { passed, results, scoped_out: false, runtime_residual };
904
- }
905
- /**
906
- * The bound on coarse re-block iterations before the run converges to a terminal
907
- * `blocked` close (CE-003). Two re-block attempts give a flaky-but-recoverable
908
- * suite a chance to settle; the third unattributable red terminates
909
- * deterministically rather than livelocking.
910
- */
911
- export const COARSE_REBLOCK_BOUND = 2;
912
- const FINAL_GATE_STATE_FILENAME = "final-gate.json";
913
- async function readFinalGateSidecar(artifactsDir) {
914
- const sidecar = await readOptionalJsonFile(join(artifactsDir, FINAL_GATE_STATE_FILENAME));
915
- const n = sidecar?.coarse_reblock_count;
916
- return {
917
- count: typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : 0,
918
- terminated: sidecar?.terminated === true,
919
- };
920
- }
921
- async function writeFinalGateSidecar(artifactsDir, count, terminated) {
922
- await writeJsonFile(join(artifactsDir, FINAL_GATE_STATE_FILENAME), {
923
- schema_version: "remediate-code-final-gate/v1alpha1",
924
- coarse_reblock_count: count,
925
- terminated,
926
- });
927
- }
928
- /**
929
- * Coarse re-block-ALL-non-terminal on an unattributable final-gate red
930
- * (INV-RS-09) with a bounded, monotonic auto-terminate (CE-003).
931
- *
932
- * The tool-owned gate is whole-repo, so a red is inherently unattributable to a
933
- * single node. Below the bound, EVERY non-skip item (including `resolved` ones —
934
- * a resolved item's own change may have caused the red) is re-opened to `pending`
935
- * and the run re-attempts the whole repo through the rolling scheduler
936
- * (`reattempt_all` → `implementing`). At or above the bound, the run STOPS
937
- * re-attempting and converges DETERMINISTICALLY: every non-skip item becomes
938
- * terminal `blocked` and the run advances to `closing`.
939
- *
940
- * CE-003 no-human-host guarantee: the loop is owned entirely by the gate + the
941
- * rolling scheduler — it NEVER routes through the human triage prompt
942
- * (`waiting_for_triage`) and is bounded by `bound`, so a permanently-red sibling
943
- * converges to a terminal `blocked` close deterministically: never livelocking,
944
- * never stranding on a human prompt, and never force-closed to green (a RED gate
945
- * always leaves `blocked` items, so close.ts's `!anyBlocked` guard keeps the run
946
- * out of the fully-green path). User SKIP dispositions (ignored /
947
- * deemed_inappropriate) are settled decisions and are left alone. Pure (the
948
- * counter is supplied / returned).
949
- */
950
- export function applyCoarseReblock(state, currentCount, gateSummary, bound = COARSE_REBLOCK_BOUND) {
951
- const now = new Date().toISOString();
952
- if (currentCount >= bound) {
953
- // Bounded auto-terminate: converge DETERMINISTICALLY to a terminal `blocked`
954
- // close for a no-human host — never livelock, never a triage prompt, never green.
955
- for (const it of Object.values(state.items ?? {})) {
956
- if (isSkipStatus(it.status))
957
- continue; // settled user decision — never overturn
958
- it.status = "blocked";
959
- it.started_at ??= now;
960
- it.completed_at = now;
961
- it.failure_reason =
962
- `Tool-owned final gate failed and the coarse re-block backstop reached its ` +
963
- `bound (${bound}); converging to a terminal blocked close (no-human host). ${gateSummary}`;
964
- }
965
- return { state, action: "terminal_blocked", next_count: currentCount };
966
- }
967
- // Below the bound: re-open every non-skip item to `pending` and re-attempt the
968
- // whole repo via the rolling scheduler (NOT the human triage prompt).
969
- for (const it of Object.values(state.items ?? {})) {
970
- if (isSkipStatus(it.status))
971
- continue;
972
- it.status = "pending";
973
- it.failure_context =
974
- `Re-attempted by the coarse final-gate backstop (unattributable whole-repo red). ${gateSummary}`;
975
- delete it.completed_at;
976
- }
977
- return { state, action: "reattempt_all", next_count: currentCount + 1 };
978
- }
886
+ // The gate runner, its bounded coarse-reblock backstop, and the sidecar counter
887
+ // I/O were extracted behaviour-preservingly into the sibling leaf module
888
+ // `finalGate.ts` (CP-NODE-1). They are imported below for local use in the
889
+ // completion handler and re-exported to preserve this module's public surface +
890
+ // existing test imports. See `finalGate.ts` for the INV-RS-10 / CE-001 / CE-002
891
+ // documentation.
892
+ export { isAuditToolsMonorepo, toolOwnedFinalGateCommands, runToolOwnedFinalGate, applyCoarseReblock, COARSE_REBLOCK_BOUND, } from "./finalGate.js";
979
893
  function resolvedOrTerminalItems(state) {
980
894
  return Object.values(state.items ?? {}).filter((item) => isTerminalStatus(item.status));
981
895
  }
@@ -1378,8 +1292,7 @@ async function buildImplementDispatchStep(ctx) {
1378
1292
  // S-BROKER-WIRING: pick the dispatch DRIVER (delegate the rolling loop to a
1379
1293
  // dedicated dispatcher subagent vs. drive it from the top host) off the
1380
1294
  // single classification + the live frontier/slot count — not host prose.
1381
- const hostProvider = sessionConfigImpl?.provider ??
1382
- "claude-code";
1295
+ const hostProvider = resolveHostProviderName(sessionConfigImpl);
1383
1296
  const driverSelection = selectDispatchDriver({
1384
1297
  classification: classifyProvider(hostProvider),
1385
1298
  eligibleItemCount: rolling.session.frontier.length,
@@ -1437,6 +1350,8 @@ Then run:
1437
1350
 
1438
1351
  ${renderQuotaCoverageNudge(rolling.quotaPath, artifactsDir)}
1439
1352
 
1353
+ ${renderTokenBudgetView(rolling.quotaPath)}
1354
+
1440
1355
  ${DISPATCH_PROMPT_HANDOFF_NOTE}
1441
1356
  `,
1442
1357
  allowedCommands: [acceptCmd, rollMerge, rollNext],
@@ -1533,6 +1448,8 @@ ${SHARED_REBUILD_BETWEEN_LEVELS_NOTE}
1533
1448
 
1534
1449
  ${renderQuotaCoverageNudge(implQuotaPath, artifactsDir)}
1535
1450
 
1451
+ ${renderTokenBudgetView(implQuotaPath)}
1452
+
1536
1453
  ${DISPATCH_PROMPT_HANDOFF_NOTE}
1537
1454
 
1538
1455
  After all results exist:
@@ -1548,6 +1465,41 @@ Then run:
1548
1465
  artifactPaths: { dispatch_plan: planPath, dispatch_quota: implQuotaPath },
1549
1466
  }) };
1550
1467
  }
1468
+ /**
1469
+ * Piece D — the quota-paused resumable step. The rolling engine stranded one or
1470
+ * more nodes because every eligible provider pool hit a host session limit and is
1471
+ * paused until its stated reset. The stranded nodes stay PENDING (their worktrees
1472
+ * were preserved), so re-running next-step at/after `resetAt` redispatches them
1473
+ * clean. Emitted (not blocked) so the run is resumable, never a failure.
1474
+ */
1475
+ async function buildQuotaPausedStep(params) {
1476
+ const { root, artifactsDir, runId, strandedIds, resetAt } = params;
1477
+ const nextCommand = loaderCommand("next-step");
1478
+ const resetLine = resetAt
1479
+ ? `The earliest provider reset is \`${resetAt}\`. Wait until then, then run:`
1480
+ : `Wait for the provider session limit to reset, then run:`;
1481
+ return writeCurrentStep({
1482
+ stepKind: "quota_paused",
1483
+ status: "ready",
1484
+ runId,
1485
+ repoRoot: root,
1486
+ artifactsDir,
1487
+ prompt: `
1488
+ # Remediation paused — provider session limit
1489
+
1490
+ Every eligible provider pool hit a host session limit and is paused until its
1491
+ stated reset. ${strandedIds.length} node(s) are stranded and remain PENDING —
1492
+ their worktrees were preserved, so they will redispatch clean on resume. Nothing
1493
+ was blocked or failed; this is a resumable pause.
1494
+
1495
+ ${resetLine}
1496
+
1497
+ \`${nextCommand}\`
1498
+ `,
1499
+ allowedCommands: [nextCommand],
1500
+ stopCondition: "Stop and wait for the provider reset. Re-running next-step resumes the stranded nodes.",
1501
+ });
1502
+ }
1551
1503
  // --- Per-state handlers -----------------------------------------------------
1552
1504
  // Each handler owns one branch of the original decideNextStepInner dispatch.
1553
1505
  // Handlers that emit a step return RemediationStep directly; handlers that need
@@ -2216,7 +2168,7 @@ Choose one explicitly and report the choice to the user:
2216
2168
  1. **Resume the existing run** — re-run WITHOUT \`--input\`: \`${loaderCommand("next-step")}\`
2217
2169
  2. **Start fresh from the new input** — first move aside or delete the existing
2218
2170
  \`${artifactsDir}\` directory (and the stale \`remediation-report.md\` /
2219
- \`remediation-report.json\` in \`.audit-tools/\`, which would otherwise be overwritten on completion),
2171
+ \`remediation-outcomes.json\` in \`.audit-tools/\`, which would otherwise be overwritten on completion),
2220
2172
  then re-run \`${loaderCommand("next-step --input <path>")}\`.
2221
2173
 
2222
2174
  Stop after presenting this choice. Do not advance the run until the user decides.
@@ -3021,10 +2973,8 @@ To confirm, write the final checkpoint to:
3021
2973
  \`\`\`
3022
2974
 
3023
2975
  Adjust \`filters\`, \`excluded_scope\`, \`must_not_touch\`, or \`free_form_intent\` to
3024
- narrow scope. Valid severities: \`critical\`, \`high\`, \`medium\`, \`low\`, \`info\`.
3025
- Valid lenses: \`correctness\`, \`architecture\`, \`maintainability\`, \`security\`,
3026
- \`reliability\`, \`performance\`, \`data_integrity\`, \`tests\`, \`operability\`,
3027
- \`config_deployment\`, \`observability\`.
2976
+ narrow scope. Valid severities: ${VALID_SEVERITIES_PROSE}.
2977
+ Valid lenses: ${VALID_LENSES_PROSE}.
3028
2978
 
3029
2979
  Once written with \`"confirmed_by": "host"\`, run:
3030
2980
 
@@ -3061,7 +3011,7 @@ Only \`scope_summary\` and \`intent_summary\` are required; add the optional fie
3061
3011
  }
3062
3012
  \`\`\`
3063
3013
 
3064
- - \`filters\` drop findings that don't match BEFORE planning, so only the work you want is remediated. Valid severities: \`critical\`, \`high\`, \`medium\`, \`low\`, \`info\`. Valid lenses: \`correctness\`, \`architecture\`, \`maintainability\`, \`security\`, \`reliability\`, \`performance\`, \`data_integrity\`, \`tests\`, \`operability\`, \`config_deployment\`, \`observability\`. Draw \`packages\`/\`themes\` from the findings in the audit report.
3014
+ - \`filters\` drop findings that don't match BEFORE planning, so only the work you want is remediated. Valid severities: ${VALID_SEVERITIES_PROSE}. Valid lenses: ${VALID_LENSES_PROSE}. Draw \`packages\`/\`themes\` from the findings in the audit report.
3065
3015
  - \`excluded_scope\` drops findings whose files match a path or directory prefix; \`must_not_touch\` globs are never written.
3066
3016
  - Skipped findings are listed in the final remediation report under "Skipped by Intent Checkpoint".
3067
3017
  - Leave the optional fields out to remediate everything in the report.
@@ -3421,6 +3371,28 @@ function buildMainObligations(ctx) {
3421
3371
  const terminal = s.partial_completion_terminal;
3422
3372
  if (!terminal)
3423
3373
  return { kind: "transition", state: s };
3374
+ // Piece D — quota_paused is a RETRYABLE pause, NOT a failure. The stranded
3375
+ // nodes stay PENDING (their worktrees were preserved), nothing is blocked,
3376
+ // and the close transition is NOT forced. EMIT a paused step (terminating
3377
+ // this advance loop) that tells the host to re-run next-step at/after the
3378
+ // stated reset, when the pool's session limit has cleared and the stranded
3379
+ // nodes redispatch clean. Clearing the terminal here (before the emit) so
3380
+ // the resuming step starts fresh; the pending nodes are the durable signal.
3381
+ if (terminal.reason === "quota_paused") {
3382
+ const resetAt = terminal.earliest_reset_at ?? null;
3383
+ delete s.partial_completion_terminal;
3384
+ await store.saveState(s);
3385
+ return {
3386
+ kind: "emit",
3387
+ step: await buildQuotaPausedStep({
3388
+ root,
3389
+ artifactsDir,
3390
+ runId: stateRunId(s),
3391
+ strandedIds: terminal.stranded_ids ?? [],
3392
+ resetAt,
3393
+ }),
3394
+ };
3395
+ }
3424
3396
  const strandedSet = new Set(terminal.stranded_ids ?? []);
3425
3397
  for (const it of Object.values(s.items ?? {})) {
3426
3398
  if (isTerminalStatus(it.status))