audit-tools 0.34.29 → 0.34.30

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 (125) 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/providerConfirmationStep.d.ts.map +1 -1
  9. package/dist/audit/cli/providerConfirmationStep.js +19 -7
  10. package/dist/audit/cli/providerConfirmationStep.js.map +1 -1
  11. package/dist/audit/extractors/analyzers/typescript.d.ts.map +1 -1
  12. package/dist/audit/extractors/analyzers/typescript.js +3 -2
  13. package/dist/audit/extractors/analyzers/typescript.js.map +1 -1
  14. package/dist/audit/extractors/graph.d.ts.map +1 -1
  15. package/dist/audit/extractors/graph.js +4 -5
  16. package/dist/audit/extractors/graph.js.map +1 -1
  17. package/dist/audit/io/artifacts.d.ts +1 -12
  18. package/dist/audit/io/artifacts.d.ts.map +1 -1
  19. package/dist/audit/io/artifacts.js +9 -39
  20. package/dist/audit/io/artifacts.js.map +1 -1
  21. package/dist/audit/orchestrator/designReviewSnapshot.d.ts.map +1 -1
  22. package/dist/audit/orchestrator/designReviewSnapshot.js +4 -2
  23. package/dist/audit/orchestrator/designReviewSnapshot.js.map +1 -1
  24. package/dist/audit/orchestrator/intakeExecutors.d.ts.map +1 -1
  25. package/dist/audit/orchestrator/intakeExecutors.js +59 -3
  26. package/dist/audit/orchestrator/intakeExecutors.js.map +1 -1
  27. package/dist/remediate/contractPipeline/reviewSnapshot.d.ts +8 -0
  28. package/dist/remediate/contractPipeline/reviewSnapshot.d.ts.map +1 -1
  29. package/dist/remediate/contractPipeline/reviewSnapshot.js +10 -2
  30. package/dist/remediate/contractPipeline/reviewSnapshot.js.map +1 -1
  31. package/dist/remediate/contractPipeline/testPlanCarry.d.ts +7 -1
  32. package/dist/remediate/contractPipeline/testPlanCarry.d.ts.map +1 -1
  33. package/dist/remediate/contractPipeline/testPlanCarry.js +9 -3
  34. package/dist/remediate/contractPipeline/testPlanCarry.js.map +1 -1
  35. package/dist/remediate/phases/close.d.ts.map +1 -1
  36. package/dist/remediate/phases/close.js +10 -8
  37. package/dist/remediate/phases/close.js.map +1 -1
  38. package/dist/remediate/phases/plan.js +2 -2
  39. package/dist/remediate/phases/plan.js.map +1 -1
  40. package/dist/remediate/riskSignal.d.ts.map +1 -1
  41. package/dist/remediate/riskSignal.js +5 -2
  42. package/dist/remediate/riskSignal.js.map +1 -1
  43. package/dist/remediate/state/itemStatus.d.ts +26 -3
  44. package/dist/remediate/state/itemStatus.d.ts.map +1 -1
  45. package/dist/remediate/state/itemStatus.js +56 -18
  46. package/dist/remediate/state/itemStatus.js.map +1 -1
  47. package/dist/remediate/state/store.d.ts.map +1 -1
  48. package/dist/remediate/state/store.js +18 -0
  49. package/dist/remediate/state/store.js.map +1 -1
  50. package/dist/remediate/state/types.d.ts +18 -42
  51. package/dist/remediate/state/types.d.ts.map +1 -1
  52. package/dist/remediate/state/types.js +5 -3
  53. package/dist/remediate/state/types.js.map +1 -1
  54. package/dist/remediate/steps/contractPipeline.d.ts +1 -1
  55. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  56. package/dist/remediate/steps/contractPipeline.js +7 -1
  57. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  58. package/dist/remediate/steps/dispatch/marshal.d.ts.map +1 -1
  59. package/dist/remediate/steps/dispatch/marshal.js +16 -11
  60. package/dist/remediate/steps/dispatch/marshal.js.map +1 -1
  61. package/dist/remediate/steps/dispatch/worktreeLifecycle.d.ts.map +1 -1
  62. package/dist/remediate/steps/dispatch/worktreeLifecycle.js +10 -7
  63. package/dist/remediate/steps/dispatch/worktreeLifecycle.js.map +1 -1
  64. package/dist/remediate/steps/finalGate.d.ts.map +1 -1
  65. package/dist/remediate/steps/finalGate.js +11 -4
  66. package/dist/remediate/steps/finalGate.js.map +1 -1
  67. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  68. package/dist/remediate/steps/nextStep.js +82 -20
  69. package/dist/remediate/steps/nextStep.js.map +1 -1
  70. package/dist/remediate/steps/stepUtils.d.ts +23 -0
  71. package/dist/remediate/steps/stepUtils.d.ts.map +1 -1
  72. package/dist/remediate/steps/stepUtils.js +55 -1
  73. package/dist/remediate/steps/stepUtils.js.map +1 -1
  74. package/dist/shared/constitutionalDocPaths.d.ts +13 -0
  75. package/dist/shared/constitutionalDocPaths.d.ts.map +1 -0
  76. package/dist/shared/constitutionalDocPaths.js +73 -0
  77. package/dist/shared/constitutionalDocPaths.js.map +1 -0
  78. package/dist/shared/index.d.ts +5 -2
  79. package/dist/shared/index.d.ts.map +1 -1
  80. package/dist/shared/index.js +8 -1
  81. package/dist/shared/index.js.map +1 -1
  82. package/dist/shared/io/frictionCapture.d.ts.map +1 -1
  83. package/dist/shared/io/frictionCapture.js +6 -1
  84. package/dist/shared/io/frictionCapture.js.map +1 -1
  85. package/dist/shared/io/pathContainment.d.ts +46 -0
  86. package/dist/shared/io/pathContainment.d.ts.map +1 -0
  87. package/dist/shared/io/pathContainment.js +40 -0
  88. package/dist/shared/io/pathContainment.js.map +1 -0
  89. package/dist/shared/io/schemaVersion.d.ts +67 -0
  90. package/dist/shared/io/schemaVersion.d.ts.map +1 -0
  91. package/dist/shared/io/schemaVersion.js +87 -0
  92. package/dist/shared/io/schemaVersion.js.map +1 -0
  93. package/dist/shared/providers/auditorSources.d.ts +9 -1
  94. package/dist/shared/providers/auditorSources.d.ts.map +1 -1
  95. package/dist/shared/providers/auditorSources.js +46 -2
  96. package/dist/shared/providers/auditorSources.js.map +1 -1
  97. package/dist/shared/providers/claudeWorkerProvider.d.ts +0 -1
  98. package/dist/shared/providers/claudeWorkerProvider.d.ts.map +1 -1
  99. package/dist/shared/providers/claudeWorkerProvider.js +4 -2
  100. package/dist/shared/providers/claudeWorkerProvider.js.map +1 -1
  101. package/dist/shared/providers/openAiCompatibleProvider.d.ts.map +1 -1
  102. package/dist/shared/providers/openAiCompatibleProvider.js +5 -9
  103. package/dist/shared/providers/openAiCompatibleProvider.js.map +1 -1
  104. package/dist/shared/providers/sharedProviderConfirmation.d.ts +42 -1
  105. package/dist/shared/providers/sharedProviderConfirmation.d.ts.map +1 -1
  106. package/dist/shared/providers/sharedProviderConfirmation.js +197 -3
  107. package/dist/shared/providers/sharedProviderConfirmation.js.map +1 -1
  108. package/dist/shared/quota/coverage.js +1 -1
  109. package/dist/shared/repair/brokeredDispatch.d.ts.map +1 -1
  110. package/dist/shared/repair/brokeredDispatch.js +10 -48
  111. package/dist/shared/repair/brokeredDispatch.js.map +1 -1
  112. package/dist/shared/types/auditorDescriptor.d.ts +8 -2
  113. package/dist/shared/types/auditorDescriptor.d.ts.map +1 -1
  114. package/dist/shared/types/auditorDescriptor.js.map +1 -1
  115. package/dist/shared/types/sessionConfig.d.ts +25 -1
  116. package/dist/shared/types/sessionConfig.d.ts.map +1 -1
  117. package/dist/shared/types/sessionConfig.js +4 -0
  118. package/dist/shared/types/sessionConfig.js.map +1 -1
  119. package/docs/audit-pkg/operator-guide.md +19 -9
  120. package/docs/audit-pkg/release.md +4 -0
  121. package/package.json +10 -3
  122. package/dist/remediate/coverage/findingLedger.d.ts +0 -99
  123. package/dist/remediate/coverage/findingLedger.d.ts.map +0 -1
  124. package/dist/remediate/coverage/findingLedger.js +0 -110
  125. 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
  // ---------------------------------------------------------------------------
@@ -496,7 +512,7 @@ export async function driveRollingDispatch(levels, options) {
496
512
  setQuotaStateDir(options.quotaStateDir);
497
513
  }
498
514
  const estimateTokens = options.estimateTokens ?? (() => 2000);
499
- const scopeForBlock = options.scopeForBlock ?? ((b) => b.touched_files ?? []);
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,
@@ -824,7 +840,7 @@ export async function driveRollingImplementDispatch(options) {
824
840
  // source surface (`touched_files` — the clean surface the harvest attributes to,
825
841
  // excluding the synthetic result-path in the dispatch write-scope). Absent on the
826
842
  // 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 ?? []);
843
+ const continuityScores = computeBlockContinuityScores(await readRemediationAccessMemory(artifactsDir), levels.flat(), (block) => block.touched_files);
828
844
  const driven = await driveRollingDispatch(levels, {
829
845
  confirmedPools,
830
846
  sessionConfig: options.sessionConfig ?? {},
@@ -834,7 +850,7 @@ export async function driveRollingImplementDispatch(options) {
834
850
  root,
835
851
  hostSession,
836
852
  continuityScores,
837
- scopeForBlock: (block) => writePathsByBlock.get(block.block_id) ?? block.touched_files ?? [],
853
+ scopeForBlock: (block) => writePathsByBlock.get(block.block_id) ?? block.touched_files,
838
854
  tierForBlock: (block) => tierByBlock.get(block.block_id),
839
855
  // Reactive cost verification: a declared-free source pool observed charging has
840
856
  // been demoted by the engine; surface it as reviewable friction so the operator
@@ -1939,7 +1955,11 @@ async function handlePendingExtractedPlan(root, artifactsDir, existing, extracte
1939
1955
  run_start_dirty: [...stagedAndUntracked(root)].sort(),
1940
1956
  };
1941
1957
  }
1942
- const reviewDecision = await readOptionalJsonFile(reviewDecisionPath(artifactsDir));
1958
+ // Discarded on mismatch, so the gate re-asks rather than replaying operator
1959
+ // decisions under semantics they were not made under. Re-asking costs a repeat
1960
+ // answer; applying them blind could act on a keep/decline that no longer means
1961
+ // what it meant when it was recorded.
1962
+ const reviewDecision = discardOnSchemaVersionMismatch(await readOptionalJsonFile(reviewDecisionPath(artifactsDir)), REVIEW_DECISION_SCHEMA_VERSION);
1943
1963
  // Coverage ledger. Path A (structured_audit): the single filter pass ran at
1944
1964
  // intake over the ORIGINAL findings and persisted its dispositions — build
1945
1965
  // coverage over those originals so every audit finding gets exactly one
@@ -2113,8 +2133,8 @@ async function runReviewApprovalGate(root, artifactsDir, survivors, autonomous =
2113
2133
  };
2114
2134
  }
2115
2135
  // Consume the resolution into a durable, reasoned decision record.
2116
- const request = (await readOptionalJsonFile(requestPath)) ??
2117
- buildReviewRequest(survivors, randomRunId("path-a-review"));
2136
+ // Regenerable: a stale-schema request is treated as absent and rebuilt below.
2137
+ const request = discardOnSchemaVersionMismatch(await readOptionalJsonFile(requestPath), REVIEW_REQUEST_SCHEMA_VERSION) ?? buildReviewRequest(survivors, randomRunId("path-a-review"));
2118
2138
  const resolution = await readOptionalJsonFile(resolutionPath);
2119
2139
  if (!isResolutionForRequest(request, resolution)) {
2120
2140
  // Stale cross-run resolution (plan_id mismatch): archive it and RE-HALT
@@ -2147,7 +2167,9 @@ async function runReviewApprovalGate(root, artifactsDir, survivors, autonomous =
2147
2167
  // decision approves a SUBSET with declined EMPTY (leftovers live but not
2148
2168
  // approved), so keying the replay on the declined set alone would silently
2149
2169
  // re-approve every leftover on the next call.
2150
- const decision = await readOptionalJsonFile(decisionPath);
2170
+ // Discarded on mismatch — see the note at the sibling read: a stale-schema
2171
+ // decision record re-asks rather than replaying stale operator intent.
2172
+ const decision = discardOnSchemaVersionMismatch(await readOptionalJsonFile(decisionPath), REVIEW_DECISION_SCHEMA_VERSION);
2151
2173
  const declined = decision?.declined ?? [];
2152
2174
  const declinedIds = new Set(declined.map((d) => d.finding_id));
2153
2175
  const approvedIds = decision ? new Set(decision.approved_ids ?? []) : undefined;
@@ -2712,7 +2734,17 @@ async function applyPlanClarificationResolution(artifactsDir, state, store) {
2712
2734
  await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.consumed-${Date.now()}`));
2713
2735
  }
2714
2736
  const remainingPending = state.plan.findings.some((f) => state.items?.[f.id]?.status === "pending");
2715
- state.status = remainingPending ? "implementing" : "closing";
2737
+ // Undecided-remainder guard, mirroring triage's still-blocked guard: a
2738
+ // resolution covering only SOME of the paused items must not fall through to
2739
+ // closing, which would force-close the undecided ones as `abandoned` and drop
2740
+ // their questions unanswered. Load-bearing now that the round runs at the
2741
+ // DRAINED end of the implement phase, where `remainingPending` is normally
2742
+ // false — the fall-through it guards is the common case, not a corner.
2743
+ state.status = remainingPending
2744
+ ? "implementing"
2745
+ : hasUnansweredClarification(state)
2746
+ ? "waiting_for_clarification"
2747
+ : "closing";
2716
2748
  state.clarifications = [];
2717
2749
  state.closing_plan ??= { action: "none" };
2718
2750
  await store.saveState(state);
@@ -2789,8 +2821,8 @@ async function runPlanningReviewGate(root, artifactsDir, state, store) {
2789
2821
  return handleWaitingForReviewApproval(root, artifactsDir, request);
2790
2822
  }
2791
2823
  // Resolution present: consume it into a durable, reasoned decision record.
2792
- const request = (await readOptionalJsonFile(requestPath)) ??
2793
- buildReviewRequest(findings, reviewPlanId);
2824
+ // Regenerable: a stale-schema request is treated as absent and rebuilt below.
2825
+ const request = discardOnSchemaVersionMismatch(await readOptionalJsonFile(requestPath), REVIEW_REQUEST_SCHEMA_VERSION) ?? buildReviewRequest(findings, reviewPlanId);
2794
2826
  const resolution = await readOptionalJsonFile(resolutionPath);
2795
2827
  if (!isResolutionForRequest(request, resolution)) {
2796
2828
  // Stale cross-run resolution: archive it and re-halt with the live request.
@@ -3751,6 +3783,7 @@ const MAIN_PRIORITY = [
3751
3783
  "waiting_for_triage",
3752
3784
  "planning_documentable",
3753
3785
  "partial_terminal",
3786
+ "deferred_clarification",
3754
3787
  "implementing",
3755
3788
  "triage",
3756
3789
  "planning_zero",
@@ -3875,6 +3908,35 @@ function buildMainObligations(ctx) {
3875
3908
  return handleAllTerminalTransition(root, artifactsDir, s, store, options, runLogger);
3876
3909
  },
3877
3910
  },
3911
+ {
3912
+ // Deferred clarification round. A worker question no longer freezes the run
3913
+ // at merge time; it waits HERE — at the END of the implement phase, once the
3914
+ // eligible dispatch frontier has drained — so every sibling's remaining work
3915
+ // lands first and the questions are asked in one batched window (the goals
3916
+ // doc's bounded-window promise).
3917
+ //
3918
+ // Ordered ABOVE `implementing` only because the derive already requires an
3919
+ // empty frontier: while any node is dispatchable this obligation is
3920
+ // satisfied and `implementing` dispatches it. Ordered above `triage` /
3921
+ // `all_terminal` / `closing` so an unanswered question can never be swept
3922
+ // past into close (triage with no blocked items routes straight to closing,
3923
+ // which would force-close the paused item as `abandoned` and lose the
3924
+ // question). It is NOT the only mid-phase halt — `partial_terminal` still
3925
+ // owns the quota pause, and keeps its higher slot.
3926
+ id: "deferred_clarification",
3927
+ derive: (state) => state != null &&
3928
+ (state.status === "implementing" || state.status === "triage") &&
3929
+ hasUnansweredClarification(state) &&
3930
+ implementableBlocks(state).length === 0
3931
+ ? "missing"
3932
+ : "satisfied",
3933
+ execute: async (state) => {
3934
+ const s = requireState(state);
3935
+ s.status = "waiting_for_clarification";
3936
+ await store.saveState(s);
3937
+ return { kind: "transition", state: s };
3938
+ },
3939
+ },
3878
3940
  {
3879
3941
  id: "implementing",
3880
3942
  derive: (state) => state?.status === "implementing" ? "missing" : "satisfied",