audit-tools 0.28.2 → 0.28.4

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 (114) hide show
  1. package/dist/audit/cli/auditStep.d.ts +12 -1
  2. package/dist/audit/cli/auditStep.d.ts.map +1 -1
  3. package/dist/audit/cli/auditStep.js +1 -0
  4. package/dist/audit/cli/auditStep.js.map +1 -1
  5. package/dist/audit/cli/confirmIntentStep.d.ts.map +1 -1
  6. package/dist/audit/cli/confirmIntentStep.js +46 -35
  7. package/dist/audit/cli/confirmIntentStep.js.map +1 -1
  8. package/dist/audit/cli/dispatch/packetFilter.d.ts +1 -1
  9. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  10. package/dist/audit/cli/nextStepHelpers.js +5 -1
  11. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  12. package/dist/audit/cli/steps.d.ts +2 -2
  13. package/dist/audit/contracts/workerSchemas.d.ts +14 -14
  14. package/dist/audit/contracts/wrapperResponse.d.ts +4 -4
  15. package/dist/audit/extractors/analyzers/types.d.ts +3 -1
  16. package/dist/audit/extractors/analyzers/types.d.ts.map +1 -1
  17. package/dist/audit/extractors/analyzers/types.js +10 -1
  18. package/dist/audit/extractors/analyzers/types.js.map +1 -1
  19. package/dist/audit/orchestrator/advanceTypes.d.ts +8 -0
  20. package/dist/audit/orchestrator/advanceTypes.d.ts.map +1 -1
  21. package/dist/audit/orchestrator/executorRunners.d.ts.map +1 -1
  22. package/dist/audit/orchestrator/executorRunners.js +1 -1
  23. package/dist/audit/orchestrator/executorRunners.js.map +1 -1
  24. package/dist/audit/orchestrator/intentCheckpointExecutor.d.ts +19 -8
  25. package/dist/audit/orchestrator/intentCheckpointExecutor.d.ts.map +1 -1
  26. package/dist/audit/orchestrator/intentCheckpointExecutor.js +64 -40
  27. package/dist/audit/orchestrator/intentCheckpointExecutor.js.map +1 -1
  28. package/dist/audit/orchestrator/requeueCommand.d.ts +1 -1
  29. package/dist/audit/reporting/synthesis.d.ts.map +1 -1
  30. package/dist/audit/reporting/synthesis.js +15 -27
  31. package/dist/audit/reporting/synthesis.js.map +1 -1
  32. package/dist/audit/types/analyzerCapability.d.ts +69 -12
  33. package/dist/audit/types/analyzerCapability.d.ts.map +1 -1
  34. package/dist/audit/types/analyzerCapability.js +25 -1
  35. package/dist/audit/types/analyzerCapability.js.map +1 -1
  36. package/dist/audit/types/auditScope.d.ts +51 -12
  37. package/dist/audit/types/auditScope.d.ts.map +1 -1
  38. package/dist/audit/types/auditScope.js +49 -1
  39. package/dist/audit/types/auditScope.js.map +1 -1
  40. package/dist/audit/types/auditState.d.ts +60 -11
  41. package/dist/audit/types/auditState.d.ts.map +1 -1
  42. package/dist/audit/types/auditState.js +24 -1
  43. package/dist/audit/types/auditState.js.map +1 -1
  44. package/dist/audit/types/flowCoverage.d.ts +65 -8
  45. package/dist/audit/types/flowCoverage.d.ts.map +1 -1
  46. package/dist/audit/types/flowCoverage.js +21 -0
  47. package/dist/audit/types/flowCoverage.js.map +1 -1
  48. package/dist/audit/types/reviewPlanning.d.ts +2 -2
  49. package/dist/audit/types.d.ts +12 -12
  50. package/dist/remediate/contractPipeline/semanticProjection.d.ts.map +1 -1
  51. package/dist/remediate/contractPipeline/semanticProjection.js +36 -14
  52. package/dist/remediate/contractPipeline/semanticProjection.js.map +1 -1
  53. package/dist/remediate/phases/close.d.ts.map +1 -1
  54. package/dist/remediate/phases/close.js +27 -8
  55. package/dist/remediate/phases/close.js.map +1 -1
  56. package/dist/remediate/state/itemStatus.d.ts +5 -3
  57. package/dist/remediate/state/itemStatus.d.ts.map +1 -1
  58. package/dist/remediate/state/itemStatus.js +7 -3
  59. package/dist/remediate/state/itemStatus.js.map +1 -1
  60. package/dist/remediate/state/types.d.ts +7 -2
  61. package/dist/remediate/state/types.d.ts.map +1 -1
  62. package/dist/remediate/state/types.js +7 -0
  63. package/dist/remediate/state/types.js.map +1 -1
  64. package/dist/remediate/steps/dispatch.d.ts +109 -0
  65. package/dist/remediate/steps/dispatch.d.ts.map +1 -1
  66. package/dist/remediate/steps/dispatch.js +348 -55
  67. package/dist/remediate/steps/dispatch.js.map +1 -1
  68. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  69. package/dist/remediate/steps/nextStep.js +215 -21
  70. package/dist/remediate/steps/nextStep.js.map +1 -1
  71. package/dist/remediate/steps/prompts.d.ts +12 -0
  72. package/dist/remediate/steps/prompts.d.ts.map +1 -1
  73. package/dist/remediate/steps/prompts.js +83 -12
  74. package/dist/remediate/steps/prompts.js.map +1 -1
  75. package/dist/remediate/steps/rollingSession.d.ts.map +1 -1
  76. package/dist/remediate/steps/rollingSession.js +52 -5
  77. package/dist/remediate/steps/rollingSession.js.map +1 -1
  78. package/dist/remediate/steps/types.d.ts +10 -1
  79. package/dist/remediate/steps/types.d.ts.map +1 -1
  80. package/dist/remediate/steps/types.js.map +1 -1
  81. package/dist/remediate/validation/artifacts.d.ts.map +1 -1
  82. package/dist/remediate/validation/artifacts.js +5 -2
  83. package/dist/remediate/validation/artifacts.js.map +1 -1
  84. package/dist/remediate/validation/contractPipelineGates.d.ts.map +1 -1
  85. package/dist/remediate/validation/contractPipelineGates.js +25 -7
  86. package/dist/remediate/validation/contractPipelineGates.js.map +1 -1
  87. package/dist/shared/engine/obligationEngine.d.ts +16 -5
  88. package/dist/shared/engine/obligationEngine.d.ts.map +1 -1
  89. package/dist/shared/engine/obligationEngine.js +21 -0
  90. package/dist/shared/engine/obligationEngine.js.map +1 -1
  91. package/dist/shared/index.d.ts +6 -2
  92. package/dist/shared/index.d.ts.map +1 -1
  93. package/dist/shared/index.js +5 -2
  94. package/dist/shared/index.js.map +1 -1
  95. package/dist/shared/intent/freeFormIntentInterpreter.d.ts.map +1 -1
  96. package/dist/shared/intent/freeFormIntentInterpreter.js +5 -1
  97. package/dist/shared/intent/freeFormIntentInterpreter.js.map +1 -1
  98. package/dist/shared/reporting/findingDisplay.d.ts +71 -0
  99. package/dist/shared/reporting/findingDisplay.d.ts.map +1 -0
  100. package/dist/shared/reporting/findingDisplay.js +133 -0
  101. package/dist/shared/reporting/findingDisplay.js.map +1 -0
  102. package/dist/shared/types/intentCheckpoint.d.ts +151 -34
  103. package/dist/shared/types/intentCheckpoint.d.ts.map +1 -1
  104. package/dist/shared/types/intentCheckpoint.js +113 -1
  105. package/dist/shared/types/intentCheckpoint.js.map +1 -1
  106. package/dist/shared/types/remediationOutcome.d.ts +203 -24
  107. package/dist/shared/types/remediationOutcome.d.ts.map +1 -1
  108. package/dist/shared/types/remediationOutcome.js +65 -1
  109. package/dist/shared/types/remediationOutcome.js.map +1 -1
  110. package/dist/shared/types/sessionConfig.d.ts +3 -1
  111. package/dist/shared/types/sessionConfig.d.ts.map +1 -1
  112. package/dist/shared/types/sessionConfig.js +2 -0
  113. package/dist/shared/types/sessionConfig.js.map +1 -1
  114. package/package.json +1 -1
@@ -8,7 +8,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, worktreePath, worktreeBranchForBlock, } from "./dispatch.js";
11
+ import { mergeImplementResults, prepareImplementDispatch, readExtractedPlanIfPresent, buildConfirmedPools, createWorktree, removeWorktree, resetNodeWorktreeAndBranch, acceptNodeWorktree, recordNodeAcceptOutcome, ensureWorktreeNodeModules, seedUntrackedDeclaredPaths, worktreePath, worktreeBranchForBlock, blockScopesFromPlan, } from "./dispatch.js";
12
12
  import { makeProviderNodeDispatcher } from "./providerNodeDispatch.js";
13
13
  import { prepareHostRollingDispatch } from "./rollingSession.js";
14
14
  import { writeCurrentStep } from "./stepWriter.js";
@@ -24,7 +24,7 @@ import { contractArtifactExists, contractPipelineDir, } from "../contractPipelin
24
24
  import { buildReviewRequest, applyReviewResolution, } from "../review/reviewGate.js";
25
25
  import { runFindingFilterPass } from "../findingFilter.js";
26
26
  import { intakePaths, isIntakeReady, readIntakeArtifacts, resolveManifestSources, } from "../intake.js";
27
- import { clarificationPrompt, collectIntakeClarificationsPrompt, collectStartingPointPrompt, loaderCommand, reviewApprovalPrompt, synthesizeIntakePrompt, triagePrompt, } from "./prompts.js";
27
+ import { ambiguityReviewPrompt, clarificationPrompt, collectIntakeClarificationsPrompt, collectStartingPointPrompt, loaderCommand, reviewApprovalPrompt, synthesizeIntakePrompt, triagePrompt, } from "./prompts.js";
28
28
  export function resolveHostDispatchCapability(options) {
29
29
  if (options.hostCanDispatchSubagents !== undefined) {
30
30
  return options.hostCanDispatchSubagents;
@@ -141,6 +141,30 @@ function resolveInputPaths(root, input) {
141
141
  checked,
142
142
  };
143
143
  }
144
+ /**
145
+ * True when a supplied `--input` is the SAME input the existing run was already
146
+ * built from (its recorded intake source manifest, `created_from: "input"`, with
147
+ * a path set equal to the supplied paths). The `/remediate-code` loader re-passes
148
+ * the same `--input` on every `next-step`; treating that unchanged input as a
149
+ * RESUME — not an `input_conflict` — spares the host a needless resume/restart ack
150
+ * dance, while a genuinely DIFFERENT input still trips the conflict gate. Enforced
151
+ * in the tool, never by asking the loader to remember to drop the flag (a needed
152
+ * manual flag is a bug signal).
153
+ */
154
+ function suppliedInputMatchesRun(inputResolution, manifest) {
155
+ if (!inputResolution.supplied)
156
+ return false;
157
+ if (!manifest || manifest.created_from !== "input")
158
+ return false;
159
+ const supplied = new Set(inputResolution.checked.map((p) => resolve(p)));
160
+ const recorded = new Set(manifest.sources.map((s) => resolve(s.path)));
161
+ if (supplied.size === 0 || supplied.size !== recorded.size)
162
+ return false;
163
+ for (const p of supplied)
164
+ if (!recorded.has(p))
165
+ return false;
166
+ return true;
167
+ }
144
168
  export { NO_CHANGE_RE, dependenciesSatisfied, dependencyVerifiedComplete, specIndicatesNoChange, classifyFindingRisk, } from "./stepUtils.js";
145
169
  export { isTerminalStatus, isVerifiedCompleteStatus };
146
170
  function documentableFindings(state) {
@@ -377,6 +401,10 @@ export async function driveRollingImplementDispatch(options) {
377
401
  const resultPathByBlock = new Map(plan.items
378
402
  .filter((i) => typeof i.block_id === "string")
379
403
  .map((i) => [i.block_id, i.result_path]));
404
+ // Every block's declared write scope, for the accept-time write-scope gate's
405
+ // amendment ownership adjudication (so an amended path owned by a sibling block
406
+ // is a seam conflict, not a silent grant). Built once from the in-memory plan.
407
+ const allBlockScopes = blockScopesFromPlan(plan);
380
408
  // Map each planned block id to its worktree-rooted prompt path so the
381
409
  // provider-backed dispatcher launches the worker with the right prompt.
382
410
  const promptPathByBlock = new Map(plan.items
@@ -431,6 +459,11 @@ export async function driveRollingImplementDispatch(options) {
431
459
  // A fresh worktree has no node_modules (gitignored); link the main checkout's
432
460
  // so this node's verify commands can run.
433
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 ?? []);
434
467
  const result = await dispatchNode({
435
468
  block,
436
469
  slot,
@@ -445,6 +478,9 @@ export async function driveRollingImplementDispatch(options) {
445
478
  const finding = state.plan?.findings.find((f) => f.id === id);
446
479
  return finding?.targeted_commands ?? [];
447
480
  }));
481
+ // The worker's self-reported amendments, adjudicated (never trusted as the
482
+ // gate input) by the accept-time write-scope gate before the cherry-pick.
483
+ const workerResult = await readOptionalJsonFile(resultPath);
448
484
  const accept = acceptNodeWorktree({
449
485
  root,
450
486
  runId,
@@ -453,6 +489,7 @@ export async function driveRollingImplementDispatch(options) {
453
489
  branch,
454
490
  workerOutcome: result.outcome,
455
491
  targetedCommands: targeted,
492
+ scope: { allBlockScopes, amendedFiles: workerResult?.amended_files ?? [] },
456
493
  });
457
494
  // Persist the tool-owned verify/merge outcome so finalization blocks a node
458
495
  // that self-reported resolved but never actually landed (OBL-DS-06). Parity
@@ -949,7 +986,7 @@ async function buildImplementDispatchStep(ctx) {
949
986
  }
950
987
  const rollMerge = loaderCommand(`merge-implement-results --run-id ${runId}`);
951
988
  const rollNext = loaderCommand("next-step");
952
- const acceptCmd = loaderCommand("accept-node --id <BLOCK_ID>");
989
+ const acceptCmd = loaderCommand(`accept-node --id <BLOCK_ID> --run-id ${runId}`);
953
990
  const nodeLines = rolling.initial
954
991
  .map((n) => `- \`${n.block_id}\` — prompt: \`${n.prompt_path}\` — worktree (subagent cwd): \`${n.worktree_root}\``)
955
992
  .join("\n");
@@ -1221,6 +1258,17 @@ function reviewResolutionPath(artifactsDir) {
1221
1258
  function reviewDecisionPath(artifactsDir) {
1222
1259
  return join(artifactsDir, "review_decision.json");
1223
1260
  }
1261
+ // Up-front ambiguity gate (note 3, part A) — its own request/resolution/decision
1262
+ // files, mirroring the review gate so it fires (and halts) at most once per run.
1263
+ function ambiguityRequestPath(artifactsDir) {
1264
+ return join(artifactsDir, "ambiguity_request.json");
1265
+ }
1266
+ function ambiguityResolutionPath(artifactsDir) {
1267
+ return join(artifactsDir, "ambiguity_resolution.json");
1268
+ }
1269
+ function ambiguityDecisionPath(artifactsDir) {
1270
+ return join(artifactsDir, "ambiguity_decision.json");
1271
+ }
1224
1272
  /** Pull the Finding[] out of a parsed audit-findings.json payload. */
1225
1273
  function extractAuditFindings(parsed) {
1226
1274
  if (isRecord(parsed) && Array.isArray(parsed.findings)) {
@@ -1534,11 +1582,19 @@ Stop after presenting this choice. Do not advance the run until the user decides
1534
1582
  },
1535
1583
  });
1536
1584
  }
1585
+ const PLAN_CLARIFICATION_ACTIONS = [
1586
+ "clarified",
1587
+ "deemed_inappropriate",
1588
+ "defer",
1589
+ ];
1590
+ function isPlanClarificationAction(value) {
1591
+ return (typeof value === "string" &&
1592
+ PLAN_CLARIFICATION_ACTIONS.includes(value));
1593
+ }
1537
1594
  function normalizePlanClarificationResolutions(value) {
1538
1595
  if (Array.isArray(value)) {
1539
1596
  return value.filter(isRecord).flatMap((entry) => {
1540
- if (typeof entry.finding_id === "string" &&
1541
- (entry.action === "clarified" || entry.action === "deemed_inappropriate")) {
1597
+ if (typeof entry.finding_id === "string" && isPlanClarificationAction(entry.action)) {
1542
1598
  return [
1543
1599
  {
1544
1600
  finding_id: entry.finding_id,
@@ -1561,7 +1617,7 @@ function normalizePlanClarificationResolutions(value) {
1561
1617
  return Object.entries(value).flatMap(([findingId, entry]) => {
1562
1618
  if (!isRecord(entry))
1563
1619
  return [];
1564
- if (entry.action !== "clarified" && entry.action !== "deemed_inappropriate")
1620
+ if (!isPlanClarificationAction(entry.action))
1565
1621
  return [];
1566
1622
  return [{
1567
1623
  finding_id: typeof entry.finding_id === "string" ? entry.finding_id : findingId,
@@ -1570,6 +1626,33 @@ function normalizePlanClarificationResolutions(value) {
1570
1626
  }];
1571
1627
  });
1572
1628
  }
1629
+ /**
1630
+ * Apply one clarification resolution to its item. Single-sourced so the up-front
1631
+ * ambiguity gate (part A) and the mid-run clarification round (part B) settle an
1632
+ * item identically: `clarified` re-opens it (pending) with the answer as context,
1633
+ * `deemed_inappropriate` closes it as not-a-real-issue, and `defer` closes it as
1634
+ * an explicit user deferral for this run. Never resurrects a terminal item.
1635
+ */
1636
+ function applyClarificationActionToItem(item, res, now) {
1637
+ if (res.action === "deemed_inappropriate") {
1638
+ item.status = "deemed_inappropriate";
1639
+ item.failure_reason = res.rationale;
1640
+ item.started_at ??= now;
1641
+ item.completed_at = now;
1642
+ }
1643
+ else if (res.action === "defer") {
1644
+ item.status = "ignored";
1645
+ item.failure_reason = res.rationale
1646
+ ? `User-deferred for this run: ${res.rationale}`
1647
+ : "User-deferred for this run.";
1648
+ item.started_at ??= now;
1649
+ item.completed_at = now;
1650
+ }
1651
+ else {
1652
+ item.status = "pending";
1653
+ item.clarification_context = res.rationale;
1654
+ }
1655
+ }
1573
1656
  /**
1574
1657
  * Consume clarification_resolution.json for plan-phase clarifications.
1575
1658
  * Mirrors the triage resolution consume: deemed_inappropriate → terminal,
@@ -1583,18 +1666,9 @@ async function applyPlanClarificationResolution(artifactsDir, state, store) {
1583
1666
  const now = new Date().toISOString();
1584
1667
  for (const res of resolutions) {
1585
1668
  const item = state.items[res.finding_id];
1586
- if (!item)
1669
+ if (!item || isTerminalStatus(item.status))
1587
1670
  continue;
1588
- if (res.action === "deemed_inappropriate") {
1589
- item.status = "deemed_inappropriate";
1590
- item.failure_reason = res.rationale;
1591
- item.started_at ??= now;
1592
- item.completed_at = now;
1593
- }
1594
- else {
1595
- item.status = "pending";
1596
- item.clarification_context = res.rationale;
1597
- }
1671
+ applyClarificationActionToItem(item, res, now);
1598
1672
  }
1599
1673
  if (existsSync(resolutionPath)) {
1600
1674
  await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.consumed-${Date.now()}`));
@@ -1710,6 +1784,110 @@ async function runPlanningReviewGate(root, artifactsDir, state, store) {
1710
1784
  await store.saveState(state);
1711
1785
  return null;
1712
1786
  }
1787
+ /**
1788
+ * Deterministic first pass (note 3, part A): scan the plan's non-terminal
1789
+ * findings for scoping/judgment ambiguity, classified into the canonical
1790
+ * clarification categories. These are CANDIDATES — the host reviews them against
1791
+ * the repo, dismisses false positives, and adds any it finds, before batching one
1792
+ * user round. Conservative by design: a candidate the host dismisses costs one
1793
+ * read; a real scoping question that falls silently to mid-run triage is the bug
1794
+ * this gate exists to prevent.
1795
+ */
1796
+ function detectPlanAmbiguities(findings, items) {
1797
+ const out = [];
1798
+ for (const f of findings) {
1799
+ const item = items?.[f.id];
1800
+ if (item && isTerminalStatus(item.status))
1801
+ continue;
1802
+ const lens = (f.lens ?? "").toLowerCase();
1803
+ const fileCount = f.affected_files?.length ?? 0;
1804
+ const broadScope = (lens === "architecture" || lens === "maintainability") &&
1805
+ (fileCount === 0 || fileCount >= 5);
1806
+ if (broadScope) {
1807
+ out.push({
1808
+ finding_id: f.id,
1809
+ category: "scope_of_fix",
1810
+ description: `"${f.title}" is a ${lens} finding with ${fileCount === 0 ? "no cited files" : `${fileCount} affected files`}; ` +
1811
+ "confirm how far the fix should reach (minimal local change vs. broader restructuring).",
1812
+ });
1813
+ continue;
1814
+ }
1815
+ if (f.confidence === "low") {
1816
+ out.push({
1817
+ finding_id: f.id,
1818
+ category: "issue_appropriateness",
1819
+ description: `"${f.title}" is a low-confidence finding; confirm it is a real issue worth fixing in this run.`,
1820
+ });
1821
+ }
1822
+ }
1823
+ return out;
1824
+ }
1825
+ /**
1826
+ * Up-front ambiguity gate (note 3, part A). Mirrors {@link runPlanningReviewGate}:
1827
+ * it fires once at planning, BEFORE any implement dispatch, so scoping/judgment
1828
+ * ambiguity is asked as a single batched question up front rather than falling
1829
+ * silently to triage mid-run. Deterministic heuristics seed CANDIDATES; the host
1830
+ * reviews them with repo access, dismisses/adds, and batches one user round. Each
1831
+ * item is resolved as `clarified` (answered → re-opened), `deemed_inappropriate`
1832
+ * (not a real issue), or `defer` (the user's explicit choice to skip this run).
1833
+ *
1834
+ * Idempotent: once `ambiguity_decision.json` exists the gate is done and never
1835
+ * re-halts. An empty resolution proceeds (the host found nothing to ask).
1836
+ */
1837
+ async function runPlanAmbiguityGate(root, artifactsDir, state, store) {
1838
+ const findings = state.plan?.findings ?? [];
1839
+ if (findings.length === 0)
1840
+ return null;
1841
+ const requestPath = ambiguityRequestPath(artifactsDir);
1842
+ const resolutionPath = ambiguityResolutionPath(artifactsDir);
1843
+ const decisionPath = ambiguityDecisionPath(artifactsDir);
1844
+ if (!existsSync(resolutionPath)) {
1845
+ // Deterministic detection is the gate trigger: with zero candidates there is
1846
+ // nothing for the host to review, so the plan proceeds without a round. Any
1847
+ // ambiguity the heuristics miss is still caught by the mid-run escape hatch
1848
+ // (part B). When candidates exist, halt for the host's review + the user's
1849
+ // batched answers.
1850
+ const candidates = detectPlanAmbiguities(findings, state.items);
1851
+ if (candidates.length === 0)
1852
+ return null;
1853
+ await writeJsonFile(requestPath, candidates);
1854
+ return writeCurrentStep({
1855
+ stepKind: "collect_clarifications",
1856
+ status: "blocked",
1857
+ runId: stateRunId(state),
1858
+ repoRoot: root,
1859
+ artifactsDir,
1860
+ prompt: ambiguityReviewPrompt(candidates, resolutionPath),
1861
+ allowedCommands: [loaderCommand("next-step")],
1862
+ stopCondition: "Stop after reviewing the candidate ambiguities (and asking the user any genuine ones), unless the resolution is already written and the prompt told you to continue.",
1863
+ artifactPaths: {
1864
+ ambiguity_request: requestPath,
1865
+ ambiguity_resolution: resolutionPath,
1866
+ },
1867
+ });
1868
+ }
1869
+ // Resolution present: apply it to items, mark the gate done, archive inputs so
1870
+ // it cannot re-halt.
1871
+ const resolutions = normalizePlanClarificationResolutions(await readOptionalJsonFile(resolutionPath));
1872
+ const now = new Date().toISOString();
1873
+ let changed = false;
1874
+ for (const res of resolutions) {
1875
+ const item = state.items?.[res.finding_id];
1876
+ if (!item || isTerminalStatus(item.status))
1877
+ continue;
1878
+ applyClarificationActionToItem(item, res, now);
1879
+ changed = true;
1880
+ }
1881
+ await writeJsonFile(decisionPath, { resolved_at: now, resolution_count: resolutions.length });
1882
+ for (const p of [resolutionPath, requestPath]) {
1883
+ if (existsSync(p)) {
1884
+ await withFsRetry(() => rename(p, `${p}.consumed-${Date.now()}`));
1885
+ }
1886
+ }
1887
+ if (changed)
1888
+ await store.saveState(state);
1889
+ return null;
1890
+ }
1713
1891
  async function handlePlanning(root, artifactsDir, state, store) {
1714
1892
  // Review-necessity gate (Path B). Path A records its review decision at intake,
1715
1893
  // over the ORIGINAL findings, before the contract pipeline collapses them into
@@ -1723,6 +1901,14 @@ async function handlePlanning(root, artifactsDir, state, store) {
1723
1901
  if (halt)
1724
1902
  return { kind: "emit", step: halt };
1725
1903
  }
1904
+ // Up-front ambiguity gate (note 3, part A): resolve every scoping/judgment
1905
+ // ambiguity in ONE batched round here, before any implement dispatch, so a
1906
+ // question never falls silently to mid-run triage. Fires at most once per run.
1907
+ if (state.plan && !existsSync(ambiguityDecisionPath(artifactsDir))) {
1908
+ const halt = await runPlanAmbiguityGate(root, artifactsDir, state, store);
1909
+ if (halt)
1910
+ return { kind: "emit", step: halt };
1911
+ }
1726
1912
  // Document phase dissolved: planning transitions directly to implementing.
1727
1913
  // The rolling implement dispatch reads item_spec from the plan DAG node when
1728
1914
  // present, or uses finding context directly when absent.
@@ -2264,7 +2450,7 @@ const PRE_INTAKE_PRIORITY = [
2264
2450
  */
2265
2451
  function buildPreIntakeObligations(ctx, snapshot) {
2266
2452
  const { artifactsDir, inputResolution } = ctx;
2267
- const { existingCheckpoint, resumeAck, entryState } = snapshot;
2453
+ const { existingCheckpoint, resumeAck, entryState, suppliedInputUnchanged } = snapshot;
2268
2454
  const ip = intakePaths(artifactsDir);
2269
2455
  const checkpointPath = join(artifactsDir, "intent_checkpoint.json");
2270
2456
  const ackPath = join(artifactsDir, "confirm_resume_ack.json");
@@ -2276,11 +2462,14 @@ function buildPreIntakeObligations(ctx, snapshot) {
2276
2462
  const warned = { value: false };
2277
2463
  return [
2278
2464
  {
2279
- // A new --input against a run already past intake must not silently resume
2280
- // the old plan; require an explicit resume-vs-restart choice. Derives from
2281
- // the frozen entry state never an intake-created one.
2465
+ // A new, DIFFERENT --input against a run already past intake must not
2466
+ // silently resume the old plan; require an explicit resume-vs-restart
2467
+ // choice. The SAME --input re-passed (the loader does this every next-step)
2468
+ // is an unchanged input → a resume, not a conflict. Derives from the frozen
2469
+ // entry state — never an intake-created one.
2282
2470
  id: "input_conflict",
2283
2471
  derive: () => inputResolution.supplied &&
2472
+ !suppliedInputUnchanged &&
2284
2473
  entryState != null &&
2285
2474
  entryState.status !== "pending"
2286
2475
  ? "missing"
@@ -2680,6 +2869,10 @@ async function decideNextStepLoop(options, runLogger) {
2680
2869
  ? await readOptionalJsonFile(checkpointPath)
2681
2870
  : undefined;
2682
2871
  const resumeAck = await readOptionalJsonFile(join(artifactsDir, "confirm_resume_ack.json"));
2872
+ // Whether a supplied `--input` matches the input the existing run was built
2873
+ // from — so re-passing the same `--input` (the loader does this each next-step)
2874
+ // resumes rather than tripping the input_conflict gate.
2875
+ const suppliedInputUnchanged = suppliedInputMatchesRun(inputResolution, await readOptionalJsonFile(intakePaths(artifactsDir).sourceManifest));
2683
2876
  // The linear pre-intake gates run as obligations through the shared advance
2684
2877
  // loop. An emit returns to the host; a transition re-scans within this call;
2685
2878
  // exhausting them (step === null) means the run is past intake and falls
@@ -2699,6 +2892,7 @@ async function decideNextStepLoop(options, runLogger) {
2699
2892
  existingCheckpoint,
2700
2893
  resumeAck,
2701
2894
  entryState: state,
2895
+ suppliedInputUnchanged,
2702
2896
  }),
2703
2897
  }, state, ctx);
2704
2898
  if (preIntake.step)