audit-tools 0.34.29 → 0.34.31

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 (151) hide show
  1. package/README.md +32 -11
  2. package/dist/audit/cli/args.d.ts +2 -2
  3. package/dist/audit/cli/args.d.ts.map +1 -1
  4. package/dist/audit/cli/args.js.map +1 -1
  5. package/dist/audit/cli/dispatch/paths.d.ts.map +1 -1
  6. package/dist/audit/cli/dispatch/paths.js +3 -9
  7. package/dist/audit/cli/dispatch/paths.js.map +1 -1
  8. package/dist/audit/cli/prepareDispatchCommand.d.ts +32 -0
  9. package/dist/audit/cli/prepareDispatchCommand.d.ts.map +1 -1
  10. package/dist/audit/cli/prepareDispatchCommand.js +39 -16
  11. package/dist/audit/cli/prepareDispatchCommand.js.map +1 -1
  12. package/dist/audit/cli/providerConfirmationStep.d.ts.map +1 -1
  13. package/dist/audit/cli/providerConfirmationStep.js +19 -7
  14. package/dist/audit/cli/providerConfirmationStep.js.map +1 -1
  15. package/dist/audit/cli/quotaCommand.d.ts.map +1 -1
  16. package/dist/audit/cli/quotaCommand.js +8 -20
  17. package/dist/audit/cli/quotaCommand.js.map +1 -1
  18. package/dist/audit/extractors/analyzers/typescript.d.ts.map +1 -1
  19. package/dist/audit/extractors/analyzers/typescript.js +3 -2
  20. package/dist/audit/extractors/analyzers/typescript.js.map +1 -1
  21. package/dist/audit/extractors/graph.d.ts.map +1 -1
  22. package/dist/audit/extractors/graph.js +4 -5
  23. package/dist/audit/extractors/graph.js.map +1 -1
  24. package/dist/audit/io/artifacts.d.ts +1 -12
  25. package/dist/audit/io/artifacts.d.ts.map +1 -1
  26. package/dist/audit/io/artifacts.js +9 -39
  27. package/dist/audit/io/artifacts.js.map +1 -1
  28. package/dist/audit/orchestrator/designReviewSnapshot.d.ts.map +1 -1
  29. package/dist/audit/orchestrator/designReviewSnapshot.js +4 -2
  30. package/dist/audit/orchestrator/designReviewSnapshot.js.map +1 -1
  31. package/dist/audit/orchestrator/intakeExecutors.d.ts.map +1 -1
  32. package/dist/audit/orchestrator/intakeExecutors.js +59 -3
  33. package/dist/audit/orchestrator/intakeExecutors.js.map +1 -1
  34. package/dist/remediate/contractPipeline/reviewSnapshot.d.ts +8 -0
  35. package/dist/remediate/contractPipeline/reviewSnapshot.d.ts.map +1 -1
  36. package/dist/remediate/contractPipeline/reviewSnapshot.js +10 -2
  37. package/dist/remediate/contractPipeline/reviewSnapshot.js.map +1 -1
  38. package/dist/remediate/contractPipeline/testPlanCarry.d.ts +7 -1
  39. package/dist/remediate/contractPipeline/testPlanCarry.d.ts.map +1 -1
  40. package/dist/remediate/contractPipeline/testPlanCarry.js +9 -3
  41. package/dist/remediate/contractPipeline/testPlanCarry.js.map +1 -1
  42. package/dist/remediate/phases/close.d.ts.map +1 -1
  43. package/dist/remediate/phases/close.js +10 -8
  44. package/dist/remediate/phases/close.js.map +1 -1
  45. package/dist/remediate/phases/plan.js +2 -2
  46. package/dist/remediate/phases/plan.js.map +1 -1
  47. package/dist/remediate/riskSignal.d.ts.map +1 -1
  48. package/dist/remediate/riskSignal.js +5 -2
  49. package/dist/remediate/riskSignal.js.map +1 -1
  50. package/dist/remediate/state/itemStatus.d.ts +26 -3
  51. package/dist/remediate/state/itemStatus.d.ts.map +1 -1
  52. package/dist/remediate/state/itemStatus.js +56 -18
  53. package/dist/remediate/state/itemStatus.js.map +1 -1
  54. package/dist/remediate/state/store.d.ts.map +1 -1
  55. package/dist/remediate/state/store.js +18 -0
  56. package/dist/remediate/state/store.js.map +1 -1
  57. package/dist/remediate/state/types.d.ts +18 -42
  58. package/dist/remediate/state/types.d.ts.map +1 -1
  59. package/dist/remediate/state/types.js +5 -3
  60. package/dist/remediate/state/types.js.map +1 -1
  61. package/dist/remediate/steps/contractPipeline.d.ts +1 -1
  62. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  63. package/dist/remediate/steps/contractPipeline.js +7 -1
  64. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  65. package/dist/remediate/steps/dispatch/implementPrompt.d.ts +2 -2
  66. package/dist/remediate/steps/dispatch/implementPrompt.d.ts.map +1 -1
  67. package/dist/remediate/steps/dispatch/marshal.d.ts.map +1 -1
  68. package/dist/remediate/steps/dispatch/marshal.js +34 -16
  69. package/dist/remediate/steps/dispatch/marshal.js.map +1 -1
  70. package/dist/remediate/steps/dispatch/worktreeLifecycle.d.ts.map +1 -1
  71. package/dist/remediate/steps/dispatch/worktreeLifecycle.js +10 -7
  72. package/dist/remediate/steps/dispatch/worktreeLifecycle.js.map +1 -1
  73. package/dist/remediate/steps/finalGate.d.ts.map +1 -1
  74. package/dist/remediate/steps/finalGate.js +11 -4
  75. package/dist/remediate/steps/finalGate.js.map +1 -1
  76. package/dist/remediate/steps/nextStep.d.ts +7 -2
  77. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  78. package/dist/remediate/steps/nextStep.js +99 -35
  79. package/dist/remediate/steps/nextStep.js.map +1 -1
  80. package/dist/remediate/steps/stepUtils.d.ts +23 -0
  81. package/dist/remediate/steps/stepUtils.d.ts.map +1 -1
  82. package/dist/remediate/steps/stepUtils.js +55 -1
  83. package/dist/remediate/steps/stepUtils.js.map +1 -1
  84. package/dist/remediate/steps/types.d.ts +21 -0
  85. package/dist/remediate/steps/types.d.ts.map +1 -1
  86. package/dist/remediate/steps/types.js.map +1 -1
  87. package/dist/remediate/validation/artifacts.d.ts.map +1 -1
  88. package/dist/remediate/validation/artifacts.js +9 -0
  89. package/dist/remediate/validation/artifacts.js.map +1 -1
  90. package/dist/remediate/validation/contractPipelineGates.d.ts +7 -3
  91. package/dist/remediate/validation/contractPipelineGates.d.ts.map +1 -1
  92. package/dist/remediate/validation/contractPipelineGates.js +9 -21
  93. package/dist/remediate/validation/contractPipelineGates.js.map +1 -1
  94. package/dist/shared/constitutionalDocPaths.d.ts +13 -0
  95. package/dist/shared/constitutionalDocPaths.d.ts.map +1 -0
  96. package/dist/shared/constitutionalDocPaths.js +73 -0
  97. package/dist/shared/constitutionalDocPaths.js.map +1 -0
  98. package/dist/shared/index.d.ts +5 -2
  99. package/dist/shared/index.d.ts.map +1 -1
  100. package/dist/shared/index.js +8 -1
  101. package/dist/shared/index.js.map +1 -1
  102. package/dist/shared/io/frictionCapture.d.ts.map +1 -1
  103. package/dist/shared/io/frictionCapture.js +6 -1
  104. package/dist/shared/io/frictionCapture.js.map +1 -1
  105. package/dist/shared/io/pathContainment.d.ts +46 -0
  106. package/dist/shared/io/pathContainment.d.ts.map +1 -0
  107. package/dist/shared/io/pathContainment.js +40 -0
  108. package/dist/shared/io/pathContainment.js.map +1 -0
  109. package/dist/shared/io/schemaVersion.d.ts +67 -0
  110. package/dist/shared/io/schemaVersion.d.ts.map +1 -0
  111. package/dist/shared/io/schemaVersion.js +87 -0
  112. package/dist/shared/io/schemaVersion.js.map +1 -0
  113. package/dist/shared/providers/auditorSources.d.ts +9 -1
  114. package/dist/shared/providers/auditorSources.d.ts.map +1 -1
  115. package/dist/shared/providers/auditorSources.js +46 -2
  116. package/dist/shared/providers/auditorSources.js.map +1 -1
  117. package/dist/shared/providers/claudeWorkerProvider.d.ts +0 -1
  118. package/dist/shared/providers/claudeWorkerProvider.d.ts.map +1 -1
  119. package/dist/shared/providers/claudeWorkerProvider.js +4 -2
  120. package/dist/shared/providers/claudeWorkerProvider.js.map +1 -1
  121. package/dist/shared/providers/openAiCompatibleProvider.d.ts.map +1 -1
  122. package/dist/shared/providers/openAiCompatibleProvider.js +47 -10
  123. package/dist/shared/providers/openAiCompatibleProvider.js.map +1 -1
  124. package/dist/shared/providers/sharedProviderConfirmation.d.ts +42 -1
  125. package/dist/shared/providers/sharedProviderConfirmation.d.ts.map +1 -1
  126. package/dist/shared/providers/sharedProviderConfirmation.js +197 -3
  127. package/dist/shared/providers/sharedProviderConfirmation.js.map +1 -1
  128. package/dist/shared/quota/coverage.js +1 -1
  129. package/dist/shared/repair/brokeredDispatch.d.ts.map +1 -1
  130. package/dist/shared/repair/brokeredDispatch.js +10 -48
  131. package/dist/shared/repair/brokeredDispatch.js.map +1 -1
  132. package/dist/shared/types/auditorDescriptor.d.ts +8 -2
  133. package/dist/shared/types/auditorDescriptor.d.ts.map +1 -1
  134. package/dist/shared/types/auditorDescriptor.js.map +1 -1
  135. package/dist/shared/types/sessionConfig.d.ts +25 -1
  136. package/dist/shared/types/sessionConfig.d.ts.map +1 -1
  137. package/dist/shared/types/sessionConfig.js +4 -0
  138. package/dist/shared/types/sessionConfig.js.map +1 -1
  139. package/dist/shared/validation/findingGrounding.d.ts +19 -7
  140. package/dist/shared/validation/findingGrounding.d.ts.map +1 -1
  141. package/dist/shared/validation/findingGrounding.js +22 -10
  142. package/dist/shared/validation/findingGrounding.js.map +1 -1
  143. package/docs/audit-pkg/operator-guide.md +19 -9
  144. package/docs/audit-pkg/release.md +4 -0
  145. package/package.json +10 -3
  146. package/scripts/audit/postinstall.mjs +16 -1
  147. package/scripts/remediate/postinstall.mjs +17 -1
  148. package/dist/remediate/coverage/findingLedger.d.ts +0 -99
  149. package/dist/remediate/coverage/findingLedger.d.ts.map +0 -1
  150. package/dist/remediate/coverage/findingLedger.js +0 -110
  151. package/dist/remediate/coverage/findingLedger.js.map +0 -1
@@ -3,7 +3,7 @@ import { existsSync, statSync } from "node:fs";
3
3
  import { mkdir, readFile, rename } from "node:fs/promises";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { StateStore } from "../state/store.js";
6
- import { readConfirmedDispatchPolicy, readConfirmedCapabilityRanks, resolveDispatchExclusion, readOptionalJsonFile, readValidatedRepoSessionIntent, stagedAndUntracked, writeJsonFile, writeTextFile, buildAuditDeliverablePair, formatValidationIssues, isRecord, withFsRetry, RunLogger, DISPATCH_PROMPT_HANDOFF_NOTE, renderHostScratchNote, hostScratchDir, renderQuotaCoverageNudge, renderTokenBudgetView, coerceJsonObjectArg, driveRolling, resolveLedgerBudgets, setQuotaStateDir, detectHostDispatchWall, admissionBlockedOnBudget, classifyEmptyGrantCause, reconcileAdmissionLeasesFromQuotaFile, buildQuotaPausedTerminal, interpretFreeFormIntent, advance, decideFrictionTriage, buildFrictionTriageBlock, planHybridDispatch, readSettledPools, addSettledPool, isPoolSettlingOutcome, isInProcessWorkerProvider, sourceByPoolId, classifyProvider, selectDispatchDriver, renderDispatchDriverInstruction, HostSessionQuotaSource, quotaPoolKey, captureStepBoundaryFriction, captureZeroCapacityFriction, captureCostDriftFriction, captureCreditExhaustionFriction, captureQuotaUnclassifiedFriction, captureModelUnavailableFriction, capturePacketTooLargeFriction, createDispatchDecisionLog, LENSES, SEVERITIES, resolveHostProviderName, resolveHostDispatchProviderName, resolveHostDispatchCapability as sharedResolveHostDispatchCapability, resolveAutonomousMode, resolveRollingEngineFlag, DEFAULT_CONTEXT_TOKENS } from "audit-tools/shared";
6
+ import { discardOnSchemaVersionMismatch, readConfirmedDispatchPolicy, readConfirmedCapabilityRanks, resolveDispatchExclusion, readOptionalJsonFile, readValidatedRepoSessionIntent, stagedAndUntracked, writeJsonFile, writeTextFile, buildAuditDeliverablePair, formatValidationIssues, isRecord, withFsRetry, RunLogger, DISPATCH_PROMPT_HANDOFF_NOTE, renderHostScratchNote, hostScratchDir, renderQuotaCoverageNudge, renderTokenBudgetView, coerceJsonObjectArg, driveRolling, resolveLedgerBudgets, setQuotaStateDir, detectHostDispatchWall, admissionBlockedOnBudget, classifyEmptyGrantCause, reconcileAdmissionLeasesFromQuotaFile, buildQuotaPausedTerminal, interpretFreeFormIntent, advance, decideFrictionTriage, buildFrictionTriageBlock, planHybridDispatch, readSettledPools, addSettledPool, isPoolSettlingOutcome, isInProcessWorkerProvider, sourceByPoolId, classifyProvider, selectDispatchDriver, renderDispatchDriverInstruction, HostSessionQuotaSource, quotaPoolKey, captureStepBoundaryFriction, captureZeroCapacityFriction, captureCostDriftFriction, captureCreditExhaustionFriction, captureQuotaUnclassifiedFriction, captureModelUnavailableFriction, capturePacketTooLargeFriction, createDispatchDecisionLog, LENSES, SEVERITIES, resolveHostProviderName, resolveHostDispatchProviderName, resolveHostDispatchCapability as sharedResolveHostDispatchCapability, resolveAutonomousMode, resolveRollingEngineFlag, DEFAULT_CONTEXT_TOKENS } from "audit-tools/shared";
7
7
  import { readRemediationAccessMemory, computeBlockContinuityScores } from "../state/accessMemory.js";
8
8
  import { applyPlanPipeline, buildCoverageLedger } from "../phases/plan.js";
9
9
  import { groundExtractedFindings } from "../phases/grounding.js";
@@ -18,7 +18,7 @@ import { claimWithBackoff, withClaimHeartbeat } from "../../shared/quota/claimLe
18
18
  import { AUDIT_FINDINGS_FILENAME, AUDIT_REPORT_FILENAME, auditArtifactsDir, auditFindingsPath, auditReportPath, auditToolsDir, nodeClaimsPath, promotedAuditFindingsPath, promotedAuditReportPath, remediationArtifactsDir, } from "../../shared/io/auditToolsPaths.js";
19
19
  import { resolveRepoRoot } from "../../shared/io/repoRoot.js";
20
20
  import { writeCurrentStep } from "./stepWriter.js";
21
- import { dependencyVerifiedComplete } from "./stepUtils.js";
21
+ import { dependencyAwaitingClarification, dependencyVerifiedComplete, } from "./stepUtils.js";
22
22
  import { isTerminalStatus, isVerifiedCompleteStatus, } from "../state/itemStatus.js";
23
23
  import { deduplicateCrossLensFindings, fixupBlocksAfterDedup, } from "../dedup/crossLensDedup.js";
24
24
  import { checkAffectedFileIntegrity } from "../utils/fileIntegrity.js";
@@ -26,7 +26,7 @@ import { resolveIntakeStep } from "./intakeResolver.js";
26
26
  import { runToolOwnedFinalGate, applyCoarseReblock, readFinalGateSidecar, writeFinalGateSidecar, } from "./finalGate.js";
27
27
  import { buildNextContractPipelineStep, shouldEnterContractPipeline, writePathASeedFromFindings, buildLeanExtractedPlan, } from "./contractPipeline.js";
28
28
  import { contractArtifactExists, contractPipelineDir, } from "../contractPipeline/artifactStore.js";
29
- import { buildReviewRequest, applyReviewResolution, isResolutionForRequest, } from "../review/reviewGate.js";
29
+ import { buildReviewRequest, applyReviewResolution, isResolutionForRequest, REVIEW_REQUEST_SCHEMA_VERSION, } from "../review/reviewGate.js";
30
30
  import { buildAutonomousReviewDecision } from "../review/autonomousGate.js";
31
31
  import { runFindingFilterPass } from "../findingFilter.js";
32
32
  import { intakePaths, isIntakeReady, readIntakeArtifacts, resolveManifestSources, } from "../intake.js";
@@ -320,19 +320,35 @@ function implementableBlocks(state) {
320
320
  }));
321
321
  }
322
322
  /**
323
- * Pending nodes that are NOT eligible because at least one dependency did not
324
- * reach a verified-complete disposition (a prerequisite was skipped, blocked, or
325
- * is still pending). Once no eligible block remains, these are dead-ended: the
326
- * rolling scheduler marks them `blocked` (their upstream surface never landed)
327
- * rather than looping forever. Used by `handlePlanning` to make that transition
328
- * deterministic.
323
+ * Pending nodes that are genuinely DEAD-ENDED: at least one dependency did not
324
+ * reach a verified-complete disposition and never will (a prerequisite was
325
+ * skipped or blocked, or the edges are cyclic). Once no eligible block remains,
326
+ * these are marked `blocked` — their upstream surface never landed — rather than
327
+ * looping forever.
328
+ *
329
+ * A node held only by an UNANSWERED WORKER QUESTION is deliberately excluded
330
+ * (`dependencyAwaitingClarification`): since the clarification round is deferred
331
+ * to the end of the implement phase, such a node reaches this sweep while its
332
+ * answer is still outstanding, and blocking it would report "upstream failed"
333
+ * for what is really "awaiting an answer". It stays `pending` and is re-decided
334
+ * once the answer lands.
329
335
  */
330
336
  function blockedByUnsatisfiedDependency(state) {
331
337
  if (!state.plan || !state.items)
332
338
  return [];
333
339
  return state.plan.blocks.filter((block) => !dependencyVerifiedComplete(block, state) &&
340
+ !dependencyAwaitingClarification(block, state) &&
334
341
  block.items.some((findingId) => state.items?.[findingId]?.status === "pending"));
335
342
  }
343
+ /**
344
+ * Whether any item is paused on a worker question that has not been answered yet.
345
+ * Drives the deferred clarification round (the `deferred_clarification`
346
+ * obligation): the question waits until the implement frontier drains, then is
347
+ * asked in one batched window.
348
+ */
349
+ function hasUnansweredClarification(state) {
350
+ return Object.values(state.items ?? {}).some((it) => it.status === "needs_clarification");
351
+ }
336
352
  // ---------------------------------------------------------------------------
337
353
  // Rolling per-node scheduler (CP-BLOCK-N-rolling-scheduler)
338
354
  // ---------------------------------------------------------------------------
@@ -495,8 +511,8 @@ export async function driveRollingDispatch(levels, options) {
495
511
  if (options.quotaStateDir) {
496
512
  setQuotaStateDir(options.quotaStateDir);
497
513
  }
498
- const estimateTokens = options.estimateTokens ?? (() => 2000);
499
- const scopeForBlock = options.scopeForBlock ?? ((b) => b.touched_files ?? []);
514
+ const estimateTokens = options.estimateTokens;
515
+ const scopeForBlock = options.scopeForBlock ?? ((b) => b.touched_files);
500
516
  const allBlocks = levels.flat();
501
517
  const blockById = new Map(allBlocks.map((b) => [b.block_id, b]));
502
518
  const hostSession = options.hostSession;
@@ -787,7 +803,7 @@ export async function driveRollingImplementDispatch(options) {
787
803
  artifactsDir,
788
804
  runId,
789
805
  resultPath,
790
- seedPaths: block.touched_files ?? [],
806
+ seedPaths: block.touched_files,
791
807
  allBlockScopes,
792
808
  additionalVerifyCommands: targetedCommandsForBlock(state, block.block_id),
793
809
  dispatchNode,
@@ -819,12 +835,15 @@ export async function driveRollingImplementDispatch(options) {
819
835
  // the merge-time write-scope gate reads — for file-ownership-disjoint admission
820
836
  // (INV-SOO). A block with no plan scope falls back to its touched_files.
821
837
  const writePathsByBlock = new Map(allBlockScopes.map((s) => [s.block_id, s.write_paths]));
838
+ const tokensByBlock = new Map(plan.items
839
+ .filter((i) => typeof i.block_id === "string")
840
+ .map((i) => [i.block_id, i.estimated_input_tokens]));
822
841
  // Continuity bias (context-efficiency track, increment 2d): load the harvested
823
842
  // access-memory and reduce it to a per-block mass keyed on each block's declared
824
843
  // source surface (`touched_files` — the clean surface the harvest attributes to,
825
844
  // excluding the synthetic result-path in the dispatch write-scope). Absent on the
826
845
  // first pass (no merge yet) ⇒ empty map ⇒ pure block_id sub-wave ordering.
827
- const continuityScores = computeBlockContinuityScores(await readRemediationAccessMemory(artifactsDir), levels.flat(), (block) => block.touched_files ?? []);
846
+ const continuityScores = computeBlockContinuityScores(await readRemediationAccessMemory(artifactsDir), levels.flat(), (block) => block.touched_files);
828
847
  const driven = await driveRollingDispatch(levels, {
829
848
  confirmedPools,
830
849
  sessionConfig: options.sessionConfig ?? {},
@@ -834,7 +853,19 @@ export async function driveRollingImplementDispatch(options) {
834
853
  root,
835
854
  hostSession,
836
855
  continuityScores,
837
- scopeForBlock: (block) => writePathsByBlock.get(block.block_id) ?? block.touched_files ?? [],
856
+ scopeForBlock: (block) => writePathsByBlock.get(block.block_id) ?? block.touched_files,
857
+ // The node's real size, read off the plan item that already carries it. No
858
+ // fallback: a block in the drive that is absent from the plan is a producer
859
+ // desync, and defaulting it would silently restore the flat estimate that
860
+ // made every node look identical to the fit gates.
861
+ estimateTokens: (block) => {
862
+ const estimate = tokensByBlock.get(block.block_id);
863
+ if (estimate === undefined) {
864
+ throw new Error(`Block ${block.block_id} is being dispatched but carries no dispatch-plan item, ` +
865
+ `so its token estimate is unknown. The drive and the plan have desynced.`);
866
+ }
867
+ return estimate;
868
+ },
838
869
  tierForBlock: (block) => tierByBlock.get(block.block_id),
839
870
  // Reactive cost verification: a declared-free source pool observed charging has
840
871
  // been demoted by the engine; surface it as reviewable friction so the operator
@@ -914,19 +945,6 @@ export async function driveRollingImplementDispatch(options) {
914
945
  ...(driven.terminal ? { terminal: driven.terminal } : {}),
915
946
  };
916
947
  }
917
- /**
918
- * Flat per-node input-token estimate for the A-8 hybrid frontier split. The
919
- * coordinator splits by per-pool SLOTS (concurrency) — host vs. backend — so a
920
- * uniform estimate is sufficient here; it matches the default the in-process engine
921
- * uses (`driveRollingDispatch`'s `() => 2000`), keeping the two paths consistent.
922
- *
923
- * ⚠ **Deliberately still flat.** Replacing it with the real per-node estimate was
924
- * built and REVERTED (2026-07-24): it is accurate, and that is precisely the
925
- * problem — an honest estimate makes "this node fits no pool" REACHABLE, and both
926
- * consumers mishandle that case terminally rather than resumably. See the backlog
927
- * entry; the fit gates cannot consume a real estimate until that routing is fixed.
928
- */
929
- const HYBRID_NODE_TOKEN_ESTIMATE = 2000;
930
948
  // ---------------------------------------------------------------------------
931
949
  // Tool-owned final completion gate (INV-RS-10) + coarse re-block (INV-RS-09)
932
950
  // ---------------------------------------------------------------------------
@@ -1338,7 +1356,7 @@ async function buildImplementDispatchStep(ctx) {
1338
1356
  });
1339
1357
  const frontier = plan.items
1340
1358
  .filter((i) => typeof i.block_id === "string")
1341
- .map((i) => ({ id: i.block_id, estimatedTokens: HYBRID_NODE_TOKEN_ESTIMATE }));
1359
+ .map((i) => ({ id: i.block_id, estimatedTokens: i.estimated_input_tokens }));
1342
1360
  if (frontier.length === 0) {
1343
1361
  const merged = await mergeImplementResults({ root, artifactsDir }, runId);
1344
1362
  return { kind: "transition", state: merged };
@@ -1939,7 +1957,11 @@ async function handlePendingExtractedPlan(root, artifactsDir, existing, extracte
1939
1957
  run_start_dirty: [...stagedAndUntracked(root)].sort(),
1940
1958
  };
1941
1959
  }
1942
- const reviewDecision = await readOptionalJsonFile(reviewDecisionPath(artifactsDir));
1960
+ // Discarded on mismatch, so the gate re-asks rather than replaying operator
1961
+ // decisions under semantics they were not made under. Re-asking costs a repeat
1962
+ // answer; applying them blind could act on a keep/decline that no longer means
1963
+ // what it meant when it was recorded.
1964
+ const reviewDecision = discardOnSchemaVersionMismatch(await readOptionalJsonFile(reviewDecisionPath(artifactsDir)), REVIEW_DECISION_SCHEMA_VERSION);
1943
1965
  // Coverage ledger. Path A (structured_audit): the single filter pass ran at
1944
1966
  // intake over the ORIGINAL findings and persisted its dispositions — build
1945
1967
  // coverage over those originals so every audit finding gets exactly one
@@ -2113,8 +2135,8 @@ async function runReviewApprovalGate(root, artifactsDir, survivors, autonomous =
2113
2135
  };
2114
2136
  }
2115
2137
  // Consume the resolution into a durable, reasoned decision record.
2116
- const request = (await readOptionalJsonFile(requestPath)) ??
2117
- buildReviewRequest(survivors, randomRunId("path-a-review"));
2138
+ // Regenerable: a stale-schema request is treated as absent and rebuilt below.
2139
+ const request = discardOnSchemaVersionMismatch(await readOptionalJsonFile(requestPath), REVIEW_REQUEST_SCHEMA_VERSION) ?? buildReviewRequest(survivors, randomRunId("path-a-review"));
2118
2140
  const resolution = await readOptionalJsonFile(resolutionPath);
2119
2141
  if (!isResolutionForRequest(request, resolution)) {
2120
2142
  // Stale cross-run resolution (plan_id mismatch): archive it and RE-HALT
@@ -2147,7 +2169,9 @@ async function runReviewApprovalGate(root, artifactsDir, survivors, autonomous =
2147
2169
  // decision approves a SUBSET with declined EMPTY (leftovers live but not
2148
2170
  // approved), so keying the replay on the declined set alone would silently
2149
2171
  // re-approve every leftover on the next call.
2150
- const decision = await readOptionalJsonFile(decisionPath);
2172
+ // Discarded on mismatch — see the note at the sibling read: a stale-schema
2173
+ // decision record re-asks rather than replaying stale operator intent.
2174
+ const decision = discardOnSchemaVersionMismatch(await readOptionalJsonFile(decisionPath), REVIEW_DECISION_SCHEMA_VERSION);
2151
2175
  const declined = decision?.declined ?? [];
2152
2176
  const declinedIds = new Set(declined.map((d) => d.finding_id));
2153
2177
  const approvedIds = decision ? new Set(decision.approved_ids ?? []) : undefined;
@@ -2712,7 +2736,17 @@ async function applyPlanClarificationResolution(artifactsDir, state, store) {
2712
2736
  await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.consumed-${Date.now()}`));
2713
2737
  }
2714
2738
  const remainingPending = state.plan.findings.some((f) => state.items?.[f.id]?.status === "pending");
2715
- state.status = remainingPending ? "implementing" : "closing";
2739
+ // Undecided-remainder guard, mirroring triage's still-blocked guard: a
2740
+ // resolution covering only SOME of the paused items must not fall through to
2741
+ // closing, which would force-close the undecided ones as `abandoned` and drop
2742
+ // their questions unanswered. Load-bearing now that the round runs at the
2743
+ // DRAINED end of the implement phase, where `remainingPending` is normally
2744
+ // false — the fall-through it guards is the common case, not a corner.
2745
+ state.status = remainingPending
2746
+ ? "implementing"
2747
+ : hasUnansweredClarification(state)
2748
+ ? "waiting_for_clarification"
2749
+ : "closing";
2716
2750
  state.clarifications = [];
2717
2751
  state.closing_plan ??= { action: "none" };
2718
2752
  await store.saveState(state);
@@ -2789,8 +2823,8 @@ async function runPlanningReviewGate(root, artifactsDir, state, store) {
2789
2823
  return handleWaitingForReviewApproval(root, artifactsDir, request);
2790
2824
  }
2791
2825
  // Resolution present: consume it into a durable, reasoned decision record.
2792
- const request = (await readOptionalJsonFile(requestPath)) ??
2793
- buildReviewRequest(findings, reviewPlanId);
2826
+ // Regenerable: a stale-schema request is treated as absent and rebuilt below.
2827
+ const request = discardOnSchemaVersionMismatch(await readOptionalJsonFile(requestPath), REVIEW_REQUEST_SCHEMA_VERSION) ?? buildReviewRequest(findings, reviewPlanId);
2794
2828
  const resolution = await readOptionalJsonFile(resolutionPath);
2795
2829
  if (!isResolutionForRequest(request, resolution)) {
2796
2830
  // Stale cross-run resolution: archive it and re-halt with the live request.
@@ -3751,6 +3785,7 @@ const MAIN_PRIORITY = [
3751
3785
  "waiting_for_triage",
3752
3786
  "planning_documentable",
3753
3787
  "partial_terminal",
3788
+ "deferred_clarification",
3754
3789
  "implementing",
3755
3790
  "triage",
3756
3791
  "planning_zero",
@@ -3875,6 +3910,35 @@ function buildMainObligations(ctx) {
3875
3910
  return handleAllTerminalTransition(root, artifactsDir, s, store, options, runLogger);
3876
3911
  },
3877
3912
  },
3913
+ {
3914
+ // Deferred clarification round. A worker question no longer freezes the run
3915
+ // at merge time; it waits HERE — at the END of the implement phase, once the
3916
+ // eligible dispatch frontier has drained — so every sibling's remaining work
3917
+ // lands first and the questions are asked in one batched window (the goals
3918
+ // doc's bounded-window promise).
3919
+ //
3920
+ // Ordered ABOVE `implementing` only because the derive already requires an
3921
+ // empty frontier: while any node is dispatchable this obligation is
3922
+ // satisfied and `implementing` dispatches it. Ordered above `triage` /
3923
+ // `all_terminal` / `closing` so an unanswered question can never be swept
3924
+ // past into close (triage with no blocked items routes straight to closing,
3925
+ // which would force-close the paused item as `abandoned` and lose the
3926
+ // question). It is NOT the only mid-phase halt — `partial_terminal` still
3927
+ // owns the quota pause, and keeps its higher slot.
3928
+ id: "deferred_clarification",
3929
+ derive: (state) => state != null &&
3930
+ (state.status === "implementing" || state.status === "triage") &&
3931
+ hasUnansweredClarification(state) &&
3932
+ implementableBlocks(state).length === 0
3933
+ ? "missing"
3934
+ : "satisfied",
3935
+ execute: async (state) => {
3936
+ const s = requireState(state);
3937
+ s.status = "waiting_for_clarification";
3938
+ await store.saveState(s);
3939
+ return { kind: "transition", state: s };
3940
+ },
3941
+ },
3878
3942
  {
3879
3943
  id: "implementing",
3880
3944
  derive: (state) => state?.status === "implementing" ? "missing" : "satisfied",