audit-tools 0.46.0 → 0.48.0

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 (106) hide show
  1. package/README.md +36 -16
  2. package/dispatch/merge-results.mjs +3 -3
  3. package/dispatch/validate-result.mjs +3 -3
  4. package/dist/audit/cli/cleanupCommand.d.ts.map +1 -1
  5. package/dist/audit/cli/cleanupCommand.js +7 -1
  6. package/dist/audit/cli/cleanupCommand.js.map +1 -1
  7. package/dist/audit/cli/nextStepCommand.d.ts +12 -0
  8. package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
  9. package/dist/audit/cli/nextStepCommand.js +15 -6
  10. package/dist/audit/cli/nextStepCommand.js.map +1 -1
  11. package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
  12. package/dist/audit/cli/nextStepHelpers.js +28 -2
  13. package/dist/audit/cli/nextStepHelpers.js.map +1 -1
  14. package/dist/audit/cli/sampleRunCommand.d.ts +215 -0
  15. package/dist/audit/cli/sampleRunCommand.d.ts.map +1 -1
  16. package/dist/audit/cli/sampleRunCommand.js +48 -2
  17. package/dist/audit/cli/sampleRunCommand.js.map +1 -1
  18. package/dist/audit/orchestrator/acquisitionExecutor.d.ts +2 -2
  19. package/dist/audit/orchestrator/acquisitionExecutor.d.ts.map +1 -1
  20. package/dist/audit/orchestrator/acquisitionExecutor.js.map +1 -1
  21. package/dist/audit/orchestrator/designReviewPrompt.d.ts.map +1 -1
  22. package/dist/audit/orchestrator/designReviewPrompt.js +7 -0
  23. package/dist/audit/orchestrator/designReviewPrompt.js.map +1 -1
  24. package/dist/audit/orchestrator/hostInputPause.d.ts +8 -2
  25. package/dist/audit/orchestrator/hostInputPause.d.ts.map +1 -1
  26. package/dist/audit/orchestrator/hostInputPause.js.map +1 -1
  27. package/dist/audit/orchestrator/intentInterpreter.d.ts +15 -18
  28. package/dist/audit/orchestrator/intentInterpreter.d.ts.map +1 -1
  29. package/dist/audit/orchestrator/intentInterpreter.js +12 -26
  30. package/dist/audit/orchestrator/intentInterpreter.js.map +1 -1
  31. package/dist/remediate/intake.js +1 -1
  32. package/dist/remediate/intake.js.map +1 -1
  33. package/dist/remediate/phases/close.d.ts +8 -0
  34. package/dist/remediate/phases/close.d.ts.map +1 -1
  35. package/dist/remediate/phases/close.js +11 -12
  36. package/dist/remediate/phases/close.js.map +1 -1
  37. package/dist/remediate/review/autonomousGate.d.ts +3 -7
  38. package/dist/remediate/review/autonomousGate.d.ts.map +1 -1
  39. package/dist/remediate/review/autonomousGate.js +10 -24
  40. package/dist/remediate/review/autonomousGate.js.map +1 -1
  41. package/dist/remediate/riskSignal.d.ts +8 -20
  42. package/dist/remediate/riskSignal.d.ts.map +1 -1
  43. package/dist/remediate/riskSignal.js +21 -76
  44. package/dist/remediate/riskSignal.js.map +1 -1
  45. package/dist/remediate/state/accessMemory.d.ts +2 -3
  46. package/dist/remediate/state/accessMemory.d.ts.map +1 -1
  47. package/dist/remediate/state/accessMemory.js +3 -6
  48. package/dist/remediate/state/accessMemory.js.map +1 -1
  49. package/dist/remediate/state/store.d.ts +6 -7
  50. package/dist/remediate/state/store.d.ts.map +1 -1
  51. package/dist/remediate/state/store.js.map +1 -1
  52. package/dist/remediate/state/types.d.ts +2 -84
  53. package/dist/remediate/state/types.d.ts.map +1 -1
  54. package/dist/remediate/state/types.js +1 -39
  55. package/dist/remediate/state/types.js.map +1 -1
  56. package/dist/remediate/steps/contractPipeline.d.ts +0 -19
  57. package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
  58. package/dist/remediate/steps/contractPipeline.js +75 -63
  59. package/dist/remediate/steps/contractPipeline.js.map +1 -1
  60. package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
  61. package/dist/remediate/steps/dispatch/hostHandoff.js +0 -2
  62. package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
  63. package/dist/remediate/steps/nextStep.d.ts +16 -7
  64. package/dist/remediate/steps/nextStep.d.ts.map +1 -1
  65. package/dist/remediate/steps/nextStep.js +171 -210
  66. package/dist/remediate/steps/nextStep.js.map +1 -1
  67. package/dist/remediate/steps/stepUtils.d.ts +10 -2
  68. package/dist/remediate/steps/stepUtils.d.ts.map +1 -1
  69. package/dist/remediate/steps/stepUtils.js +13 -3
  70. package/dist/remediate/steps/stepUtils.js.map +1 -1
  71. package/dist/remediate/steps/types.d.ts +1 -1
  72. package/dist/remediate/steps/types.d.ts.map +1 -1
  73. package/dist/remediate/steps/types.js.map +1 -1
  74. package/dist/remediate/validation/artifacts.d.ts.map +1 -1
  75. package/dist/remediate/validation/artifacts.js +1 -16
  76. package/dist/remediate/validation/artifacts.js.map +1 -1
  77. package/dist/remediate/validation/remediationState.d.ts +0 -1
  78. package/dist/remediate/validation/remediationState.d.ts.map +1 -1
  79. package/dist/remediate/validation/remediationState.js +0 -51
  80. package/dist/remediate/validation/remediationState.js.map +1 -1
  81. package/dist/shared/analyzerPolicy.d.ts +18 -6
  82. package/dist/shared/analyzerPolicy.d.ts.map +1 -1
  83. package/dist/shared/analyzerPolicy.js +15 -3
  84. package/dist/shared/analyzerPolicy.js.map +1 -1
  85. package/dist/shared/analyzers/acquisitionEngine.d.ts +14 -8
  86. package/dist/shared/analyzers/acquisitionEngine.d.ts.map +1 -1
  87. package/dist/shared/analyzers/acquisitionEngine.js +3 -4
  88. package/dist/shared/analyzers/acquisitionEngine.js.map +1 -1
  89. package/dist/shared/index.d.ts +2 -0
  90. package/dist/shared/index.d.ts.map +1 -1
  91. package/dist/shared/index.js +1 -0
  92. package/dist/shared/index.js.map +1 -1
  93. package/dist/shared/intent/constraintClauses.d.ts +31 -0
  94. package/dist/shared/intent/constraintClauses.d.ts.map +1 -0
  95. package/dist/shared/intent/constraintClauses.js +37 -0
  96. package/dist/shared/intent/constraintClauses.js.map +1 -0
  97. package/docs/audit-pkg/operator-guide.md +7 -4
  98. package/package.json +5 -2
  99. package/scripts/audit/postinstall.mjs +4 -2
  100. package/scripts/postinstall.mjs +1 -1
  101. package/scripts/remediate/postinstall.mjs +3 -1
  102. package/scripts/shared/install-host-assets.mjs +7 -3
  103. package/wrapper/audit-code-wrapper-build.mjs +3 -3
  104. package/wrapper/audit-code-wrapper-install-hosts.mjs +3 -3
  105. package/wrapper/audit-code-wrapper-lib.mjs +1 -0
  106. package/wrapper/remediate-code-wrapper-install-hosts.mjs +3 -3
@@ -1,12 +1,12 @@
1
1
  import { loadRemediateSessionConfig } from "./sessionConfigLoad.js";
2
- import { existsSync, statSync } from "node:fs";
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
3
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { StateStore } from "../state/store.js";
6
6
  // IO / validation / rendering helpers
7
7
  import { discardOnSchemaVersionMismatch, readOptionalJsonFile, stagedAndUntracked, writeJsonFile, writeTextFile, buildAuditDeliverablePair, formatValidationIssues, isRecord, withFsRetry, RunLogger, coerceJsonObjectArg, headCommit, projectAuditFindingsReportSubset,
8
8
  // obligation engine + intent
9
- interpretFreeFormIntent, advance, decideFrictionTriage, buildFrictionTriageBlock, linkFrictionRunIds,
9
+ interpretFreeFormIntent, interpretIntent, unresolvedFromClauses, advance, decideFrictionTriage, buildFrictionTriageBlock, linkFrictionRunIds,
10
10
  // domain constants
11
11
  LENSES, SEVERITIES, } from "audit-tools/shared";
12
12
  import { applyPlanPipeline, buildCoverageLedger } from "../phases/plan.js";
@@ -27,13 +27,13 @@ import { checkAffectedFileIntegrity } from "../utils/fileIntegrity.js";
27
27
  import { applyIntentOrdering } from "../intent/intentOrdering.js";
28
28
  import { resolveIntakeStep } from "./intakeResolver.js";
29
29
  import { runToolOwnedFinalGate, writeFinalGateRedRecord, writeFinalGateOutcomeRecord, } from "./finalGate.js";
30
- import { buildNextContractPipelineStep, shouldEnterContractPipeline, writePathASeedFromFindings, buildLeanExtractedPlan, } from "./contractPipeline.js";
30
+ import { buildNextContractPipelineStep, shouldEnterContractPipeline, writePathASeedFromFindings, } from "./contractPipeline.js";
31
31
  import { contractArtifactExists, contractPipelineDir, } from "../contractPipeline/artifactStore.js";
32
32
  import { buildReviewRequest, applyReviewResolution, isResolutionForRequest, screenResolutionIds, REVIEW_REQUEST_SCHEMA_VERSION, } from "../review/reviewGate.js";
33
33
  import { buildAutonomousReviewDecision } from "../review/autonomousGate.js";
34
34
  import { runFindingFilterPass } from "../findingFilter.js";
35
35
  import { intakePaths, isIntakeReady, manifestIsInputBound, readIntakeArtifacts, resolveManifestSources, } from "../intake.js";
36
- import { ensureIntakeRiskSignal, readIntakeRiskSignal, writeIntakeRiskSignal, escalateRiskSignal, findingRiskEvidence, distinctAffectedFiles, interpretLeanLightReviewVerdict, LEAN_LIGHT_REVIEW_SCHEMA_VERSION, } from "../riskSignal.js";
36
+ import { ensureIntakeRiskSignal, readIntakeRiskSignal, writeIntakeRiskSignal, escalateRiskSignal, findingRiskEvidence, distinctAffectedFiles, } from "../riskSignal.js";
37
37
  import { ambiguityReviewPrompt, clarificationPrompt, collectIntakeClarificationsPrompt, collectStartingPointPrompt, loaderCommand, reviewApprovalPrompt, synthesizeIntakePrompt, triagePrompt, } from "./prompts.js";
38
38
  // Single-sourced prose renders of the canonical lens / severity vocabularies
39
39
  // (`audit-tools/shared` `LENSES` / `SEVERITIES`) for the intent-checkpoint
@@ -449,7 +449,7 @@ function stripPlanTimeBookkeeping(value) {
449
449
  * The re-plan carry-forward identity of a finding: canonical JSON with the
450
450
  * plan-time bookkeeping keys stripped, so a re-plan whose only delta is a
451
451
  * recomputed file hash or a re-evaluated grounding flag carries the prior item
452
- * (and its `item_spec`) forward, while a real change to the finding does not.
452
+ * forward, while a real change to the finding does not.
453
453
  *
454
454
  * EXPORTED so the invariant suite can call THIS function. It was module-internal,
455
455
  * and the suite claiming to cover the invariant declared its own copy of the key
@@ -488,14 +488,15 @@ function carryForwardMatchingItems(previous, replanned) {
488
488
  for (const finding of replanned.plan.findings) {
489
489
  const previousFinding = previousFindings.get(finding.id);
490
490
  const previousItem = previous.items[finding.id];
491
- // Skip items that were never documented (pending with no item_spec). Under
492
- // N-R13 (document phase dissolved), a pending item that already has an
493
- // item_spec from a prior planning/document pass should carry forward
494
- // together with its spec rather than being discarded.
491
+ // A still-pending item carries no work to preserve, so it is re-minted from
492
+ // the fresh plan rather than carried forward. (This test used to also admit
493
+ // a pending item that held an `item_spec`; the document phase that produced
494
+ // one was dissolved by N-R13 and the field is gone, so the second condition
495
+ // could never be true and is not restated here.)
495
496
  if (!previousFinding || !previousItem) {
496
497
  continue;
497
498
  }
498
- if (previousItem.status === "pending" && !previousItem.item_spec) {
499
+ if (previousItem.status === "pending") {
499
500
  continue;
500
501
  }
501
502
  if (findingCarryForwardKey(previousFinding) !== findingCarryForwardKey(finding)) {
@@ -1259,112 +1260,6 @@ async function runReviewApprovalGate(root, artifactsDir, survivors, autonomous =
1259
1260
  declined,
1260
1261
  };
1261
1262
  }
1262
- // ── T1 slice 3b — lean-path light adversarial review gate ───────────────────────
1263
- //
1264
- // Mirrors the review-approval gate's emit→resume→consume idiom, but the step is
1265
- // WORK (status "ready"), not a human decision — so it runs identically in
1266
- // attended and autonomous modes (the host agent executes the light pass like any
1267
- // phase). Fires (and at most once) only when the fast path is eligible.
1268
- function leanLightReviewVerdictPath(artifactsDir) {
1269
- return join(artifactsDir, "lean_light_review_verdict.json");
1270
- }
1271
- function leanLightReviewDecisionPath(artifactsDir) {
1272
- return join(artifactsDir, "lean_light_review_decision.json");
1273
- }
1274
- /** Render the bounded light-adversarial-review prompt over the approved findings. */
1275
- function leanLightReviewPrompt(approved, verdictPath) {
1276
- const findingLines = approved
1277
- .map((f) => {
1278
- const files = (f.affected_files ?? [])
1279
- .map((loc) => loc?.path)
1280
- .filter((p) => Boolean(p))
1281
- .join(", ");
1282
- return `- \`${f.id}\` — ${f.title ?? "(untitled)"}${files ? ` (${files})` : ""}`;
1283
- })
1284
- .join("\n");
1285
- const nextCommand = loaderCommand("next-step");
1286
- return `# Lean fast path — light adversarial review
1287
-
1288
- These findings cleared the simplicity gate and are headed for the lean fast path (straight to plan→implement, skipping the full contract pipeline). Before they are trusted, do ONE **lightweight adversarial pass** — the floor that replaces the full design loop here. This is proportionate, not an exhaustive counterexample search: remediation legitimately catches upstream audit errors, so nothing skips review entirely.
1289
-
1290
- ## Findings to review
1291
- ${findingLines || "_(none)_"}
1292
-
1293
- ## Your task
1294
- Adopt a brief adversarial stance and ask, across the set:
1295
- - Is any finding actually wrong, already-fixed, or not grounded in the cited code?
1296
- - Would the proposed fix break a caller, an invariant, or an adjacent behavior?
1297
- - Are any two of these coupled / sharing a file in a way that needs seam reconciliation (i.e. NOT really independent)?
1298
- - Is anything subtler or more architectural than the simplicity gate assumed?
1299
-
1300
- Write your verdict to exactly:
1301
-
1302
- \`${verdictPath}\`
1303
-
1304
- \`\`\`json
1305
- {
1306
- "schema_version": "${LEAN_LIGHT_REVIEW_SCHEMA_VERSION}",
1307
- "disposition": "clear | escalate",
1308
- "concerns": ["<required & non-empty when escalate; the concrete concern(s)>"],
1309
- "created_at": "<ISO-8601>"
1310
- }
1311
- \`\`\`
1312
-
1313
- - **clear** — the light pass surfaced no real concern; the lean path may proceed.
1314
- - **escalate** — a genuine concern surfaced. This is evidence the change is harder than assessed; the run escalates to the full contract pipeline (full independent review). When in doubt, escalate — a wrong call costs extra pipeline work, never a skipped review.
1315
-
1316
- After writing the verdict, run:
1317
-
1318
- \`${nextCommand}\`
1319
- `;
1320
- }
1321
- /**
1322
- * The lean light-review gate. Idempotent across next-step calls: once a decision
1323
- * is recorded it is consumed directly. Returns a halt step while awaiting the
1324
- * host's verdict, then `clear` (proceed to the lean plan) or `escalate` (route to
1325
- * the full pipeline with the concerns).
1326
- */
1327
- async function runLeanLightReviewGate(root, artifactsDir, approved) {
1328
- const decisionPath = leanLightReviewDecisionPath(artifactsDir);
1329
- const existing = await readOptionalJsonFile(decisionPath);
1330
- if (existing) {
1331
- return existing.disposition === "clear"
1332
- ? { kind: "clear" }
1333
- : { kind: "escalate", concerns: existing.concerns ?? [] };
1334
- }
1335
- const verdictPath = leanLightReviewVerdictPath(artifactsDir);
1336
- if (!existsSync(verdictPath)) {
1337
- const step = await writeCurrentStep({
1338
- stepKind: "lean_light_review",
1339
- status: "ready",
1340
- runId: randomRunId("LEANREVIEW"),
1341
- repoRoot: root,
1342
- artifactsDir,
1343
- prompt: leanLightReviewPrompt(approved, verdictPath),
1344
- allowedCommands: [loaderCommand("next-step")],
1345
- stopCondition: "Stop after writing the lean light-review verdict and running next-step.",
1346
- artifactPaths: { lean_light_review_verdict: verdictPath },
1347
- });
1348
- return { kind: "halt", step };
1349
- }
1350
- // Verdict present → interpret + record a durable decision, then archive the
1351
- // consumed verdict so the gate can never re-emit.
1352
- const raw = await readOptionalJsonFile(verdictPath);
1353
- const interp = interpretLeanLightReviewVerdict(raw);
1354
- const record = {
1355
- schema_version: LEAN_LIGHT_REVIEW_SCHEMA_VERSION,
1356
- disposition: interp.disposition,
1357
- concerns: interp.concerns,
1358
- created_at: new Date().toISOString(),
1359
- };
1360
- await writeJsonFile(decisionPath, record);
1361
- if (existsSync(verdictPath)) {
1362
- await withFsRetry(() => rename(verdictPath, `${verdictPath}.consumed-${Date.now()}`));
1363
- }
1364
- return interp.disposition === "clear"
1365
- ? { kind: "clear" }
1366
- : { kind: "escalate", concerns: interp.concerns };
1367
- }
1368
1263
  /**
1369
1264
  * Re-emit the autonomous-run leftovers (findings left LIVE, neither auto-fixed
1370
1265
  * nor durably rejected) as a standard, re-consumable audit deliverable pair:
@@ -1522,22 +1417,13 @@ async function handleReadyIntakeContractPipeline(root, artifactsDir, options, ru
1522
1417
  }
1523
1418
  // Persist the filter dispositions so coverage is built over the originals.
1524
1419
  await persistReviewFilterDispositions(artifactsDir, originals, filter);
1525
- // Lean path = the `low` risk tier's realization (D-68 the standalone lean
1526
- // fast-path folded into the self-scaling dial; its two mechanisms now live in
1527
- // `riskSignal.ts` (light-review) + `contractPipeline.ts` (lean plan builder)).
1528
- // A run skips the heavy contract DESIGN loop and
1529
- // synthesizes the extracted plan directly IFF its effective risk tier is `low`;
1530
- // the plan→implement→close machinery (per-node verify-before-merge + the final
1531
- // whole-repo gate) is the retained safety net. Runs only here — on Path A
1532
- // (structured_audit), the only intake with a pre-existing finding set to judge.
1533
- //
1534
- // First fold the APPROVED set's finding-level risk (grounding / confidence /
1420
+ // Fold the APPROVED set's finding-level risk (grounding / confidence /
1535
1421
  // coupling / systemic / architecture / count — the finding-QUALITY dimension the
1536
1422
  // intake path/breadth/intent signal doesn't see) INTO the shared risk signal as
1537
- // escalate-on-evidence. This makes the tier the SINGLE classifier: there is no
1538
- // separate fast-path boolean that can DISAGREE with it (a grounded handful
1539
- // touching a risk subsystem stays `high` and takes the full pipeline, instead of
1540
- // bypassing it as the old parallel `evaluateFastPath` allowed).
1423
+ // escalate-on-evidence. The tier is the SINGLE classifier and the ONLY thing it
1424
+ // selects is DEPTH: every run enters the contract pipeline, and a `low` tier
1425
+ // traverses it shallowly (collapsed round-trips, light adversarial depth). There
1426
+ // is no second plan producer and no bypass — see COLLAPSE_GROUPS.
1541
1427
  const findingEvidence = findingRiskEvidence(gate.approved);
1542
1428
  let riskSignal = await readIntakeRiskSignal(artifactsDir);
1543
1429
  if (findingEvidence && riskSignal) {
@@ -1549,62 +1435,6 @@ async function handleReadyIntakeContractPipeline(root, artifactsDir, options, ru
1549
1435
  await writeIntakeRiskSignal(artifactsDir, riskSignal);
1550
1436
  }
1551
1437
  }
1552
- // T1 slice 3b — the lean tier is NOT zero-scrutiny: a `low`-tier run first runs
1553
- // a bounded LIGHT adversarial review over the approved findings (the floor,
1554
- // never off — `adversarialDepthForTier("low") === "light"`). A clear verdict
1555
- // proceeds to the lean plan; a verdict that surfaces a real concern escalates
1556
- // the risk signal and routes to the full pipeline below.
1557
- if (gate.approved.length > 0 && riskSignal?.tier === "low") {
1558
- const review = await runLeanLightReviewGate(root, artifactsDir, gate.approved);
1559
- if (review.kind === "halt") {
1560
- return review.step;
1561
- }
1562
- if (review.kind === "escalate") {
1563
- // Escalate-on-evidence: raise the signal to at least `medium` so the
1564
- // full pipeline's adversarial depth is `full` (see slice 3a), then
1565
- // fall through to the full pipeline.
1566
- await writeIntakeRiskSignal(artifactsDir, escalateRiskSignal(riskSignal, {
1567
- tier: "medium",
1568
- reason: `lean light review surfaced a concern: ${review.concerns.join("; ")}`,
1569
- }));
1570
- runLogger.event({
1571
- phase: "next-step",
1572
- kind: "outcome",
1573
- obligation: "lean_fast_path",
1574
- note: `lean_fast_path_escalated concerns=${String(review.concerns.length)} ` +
1575
- `detail=${review.concerns.join("; ")}`,
1576
- });
1577
- process.stderr.write(`[remediate-code] Lean light review escalated (${review.concerns.join("; ")}); routing to the full contract pipeline.\n`);
1578
- }
1579
- else {
1580
- // Clear verdict → proceed with the lean plan.
1581
- const leanPlan = buildLeanExtractedPlan(gate.approved, randomRunId("LEAN"));
1582
- await writeJsonFile(intakePaths(artifactsDir).extractedPlan, leanPlan);
1583
- runLogger.event({
1584
- phase: "next-step",
1585
- kind: "outcome",
1586
- obligation: "lean_fast_path",
1587
- note: `lean_fast_path_routed findings=${String(gate.approved.length)} ` +
1588
- `rationale=${riskSignal.rationale.join("; ")}`,
1589
- });
1590
- process.stderr.write(`[remediate-code] Lean fast path (risk tier low): ${riskSignal.rationale.join("; ")}; light review clear. Routing to plan→implement.\n`);
1591
- const planned = await handlePendingExtractedPlan(root, artifactsDir, { status: "pending" }, leanPlan, runLogger);
1592
- if (planned) {
1593
- return planned;
1594
- }
1595
- // Defensive: a deterministically-built lean plan should always
1596
- // normalize. If it somehow didn't, handlePendingExtractedPlan removed
1597
- // the file; fall through to the full pipeline (the safety net) rather
1598
- // than stalling the run.
1599
- runLogger.event({
1600
- phase: "next-step",
1601
- kind: "outcome",
1602
- obligation: "lean_fast_path",
1603
- note: "lean_fast_path_fallback plan failed to materialize; routing to the contract pipeline",
1604
- });
1605
- process.stderr.write("[remediate-code] Lean fast-path plan failed to materialize; falling back to the contract pipeline.\n");
1606
- }
1607
- }
1608
1438
  // Seed the pipeline with the approved survivors only. When that set is
1609
1439
  // narrower than the originals (anything filtered or declined), route the
1610
1440
  // seed AND the pipeline's source inputs at a filtered file so a removed
@@ -2144,9 +1974,8 @@ async function handlePlanning(root, artifactsDir, state, store) {
2144
1974
  if (halt)
2145
1975
  return { kind: "emit", step: halt };
2146
1976
  }
2147
- // Document phase dissolved: planning transitions directly to implementing.
2148
- // The host workload reads item_spec from the plan DAG node when present, or
2149
- // uses finding context directly when absent.
1977
+ // Document phase dissolved (N-R13): planning transitions directly to
1978
+ // implementing, and the host workload reads finding context directly.
2150
1979
  const implementBlocks = implementableBlocks(state);
2151
1980
  if (implementBlocks.length > 0) {
2152
1981
  if (state.plan) {
@@ -2876,7 +2705,11 @@ Once the file is written, run:
2876
2705
  // on an LLM-authored free-text `intent_interpretation`.
2877
2706
  /** Sidecar artifact recording the deterministic interpretation of free_form_intent. */
2878
2707
  export const INTENT_INTERPRETATION_FILENAME = "intent-interpretation.json";
2879
- export const INTENT_INTERPRETATION_SCHEMA_VERSION = "remediate-code-intent-interpretation/v1alpha1";
2708
+ // v1alpha2: unencodable_clauses carries identity-keyed records (clause_id +
2709
+ // checkpoint_question), not bare strings — the shape the blocking consumer
2710
+ // reads. A v1alpha1 sidecar (string[]) is stale and is repaired by
2711
+ // re-derivation; it had no readers, so no migration path is owed.
2712
+ export const INTENT_INTERPRETATION_SCHEMA_VERSION = "remediate-code-intent-interpretation/v1alpha2";
2880
2713
  /**
2881
2714
  * Interpret a confirmed checkpoint's `free_form_intent` via the shared
2882
2715
  * deterministic interpreter and persist the structured signals to a sidecar
@@ -2896,33 +2729,99 @@ runLogger) {
2896
2729
  if (typeof raw !== "string" || raw.trim().length === 0)
2897
2730
  return null;
2898
2731
  const interpreted = interpretFreeFormIntent(raw);
2732
+ // The clause pipeline (interpretIntent) owns identity + blocking questions;
2733
+ // the hint interpreter above owns lens/priority/scope signals. Both are
2734
+ // deterministic draws over the same input.
2735
+ const clauseResult = interpretIntent(raw);
2736
+ const unencodable_clauses = [];
2737
+ for (const clause of clauseResult.clauses) {
2738
+ if (clause.encodable || !clause.checkpoint_question)
2739
+ continue;
2740
+ unencodable_clauses.push({
2741
+ clause_id: clause.clause_id,
2742
+ text: clause.text,
2743
+ checkpoint_question: clause.checkpoint_question,
2744
+ });
2745
+ }
2899
2746
  const persisted = {
2900
2747
  schema_version: INTENT_INTERPRETATION_SCHEMA_VERSION,
2901
2748
  interpreted,
2902
- unencodable_clauses: interpreted.unencodableClauses,
2749
+ unencodable_clauses,
2903
2750
  created_at: new Date().toISOString(),
2904
2751
  };
2905
2752
  try {
2906
2753
  await writeJsonFile(join(artifactsDir, INTENT_INTERPRETATION_FILENAME), persisted);
2907
2754
  }
2908
2755
  catch {
2909
- // Best-effort sidecar: a write failure must never block the decide loop.
2756
+ // Best-effort WRITE: a write failure must never crash the decide loop.
2757
+ // Enforcement does not depend on it — the consumer re-derives when the
2758
+ // sidecar is missing (readOrRepairIntentInterpretation).
2910
2759
  }
2911
- if (interpreted.unencodableClauses.length > 0) {
2760
+ if (unencodable_clauses.length > 0) {
2761
+ const clauseTexts = unencodable_clauses.map((c) => c.text);
2912
2762
  runLogger?.event({
2913
2763
  phase: "next-step",
2914
2764
  kind: "outcome",
2915
2765
  obligation: "interpret_intent",
2916
- note: `intent_unencodable_clauses count=${String(interpreted.unencodableClauses.length)} ` +
2917
- `clauses=${interpreted.unencodableClauses.join("; ")}`,
2766
+ note: `intent_unencodable_clauses count=${String(unencodable_clauses.length)} ` +
2767
+ `clauses=${clauseTexts.join("; ")}`,
2918
2768
  });
2919
- process.stderr.write(`[remediate-code] free_form_intent: ${interpreted.unencodableClauses.length} ` +
2920
- `clause(s) could not be encoded as lens/priority/scope signals and are ` +
2921
- `surfaced for promotion to constraints: ` +
2922
- `${interpreted.unencodableClauses.join("; ")}\n`);
2769
+ process.stderr.write(`[remediate-code] free_form_intent: ${unencodable_clauses.length} ` +
2770
+ `clause(s) could not be encoded as lens/priority/scope signals and ` +
2771
+ `block planning until answered via constraint_clauses: ` +
2772
+ `${clauseTexts.join("; ")}\n`);
2923
2773
  }
2924
2774
  return persisted;
2925
2775
  }
2776
+ /**
2777
+ * Read the persisted intent interpretation — the LOAD-BEARING input to the
2778
+ * constraint-clause gate — repairing it by re-derivation when it is missing,
2779
+ * unparseable, or carries a stale schema_version. Returns null only when
2780
+ * there is nothing to interpret (no confirmed checkpoint / empty intent).
2781
+ */
2782
+ export async function readOrRepairIntentInterpretation(artifactsDir, checkpoint, runLogger) {
2783
+ if (!checkpoint || checkpoint.confirmed_by !== "host")
2784
+ return null;
2785
+ const raw = checkpoint.free_form_intent;
2786
+ if (typeof raw !== "string" || raw.trim().length === 0)
2787
+ return null;
2788
+ const sidecarPath = join(artifactsDir, INTENT_INTERPRETATION_FILENAME);
2789
+ try {
2790
+ const parsed = parsePersistedIntentInterpretation(JSON.parse(await readFile(sidecarPath, "utf8")));
2791
+ if (parsed)
2792
+ return parsed;
2793
+ }
2794
+ catch {
2795
+ // Missing or unreadable — fall through to repair.
2796
+ }
2797
+ return interpretConfirmedCheckpointIntent(artifactsDir, checkpoint, runLogger);
2798
+ }
2799
+ /** Pure shape gate for the sidecar: current version + record-shaped clauses, else null. */
2800
+ function parsePersistedIntentInterpretation(parsed) {
2801
+ if (isRecord(parsed) &&
2802
+ parsed.schema_version === INTENT_INTERPRETATION_SCHEMA_VERSION &&
2803
+ Array.isArray(parsed.unencodable_clauses) &&
2804
+ parsed.unencodable_clauses.every((c) => isRecord(c) &&
2805
+ typeof c.clause_id === "string" &&
2806
+ typeof c.text === "string" &&
2807
+ typeof c.checkpoint_question === "string")) {
2808
+ return parsed;
2809
+ }
2810
+ return null;
2811
+ }
2812
+ /**
2813
+ * Sync sidecar read for the obligation's derive scan. Returns the persisted
2814
+ * interpretation, or null when the sidecar is missing, unreadable, or stale —
2815
+ * the execute path repairs via {@link readOrRepairIntentInterpretation}.
2816
+ */
2817
+ function readPersistedIntentInterpretationSync(sidecarPath) {
2818
+ try {
2819
+ return parsePersistedIntentInterpretation(JSON.parse(readFileSync(sidecarPath, "utf8")));
2820
+ }
2821
+ catch {
2822
+ return null;
2823
+ }
2824
+ }
2926
2825
  /**
2927
2826
  * Narrow a nullable engine state to non-null inside an executor whose `derive`
2928
2827
  * only marks it actionable when the state is present — a violation is an engine
@@ -3050,19 +2949,81 @@ function buildPreIntakeObligations(ctx, snapshot) {
3050
2949
  },
3051
2950
  {
3052
2951
  // Past the intent gate: interpret the confirmed checkpoint's
3053
- // free_form_intent once (INV-S04) and persist the structured signals. A
3054
- // transition (state unchanged) — the re-scan skips it once the sidecar
3055
- // exists.
2952
+ // free_form_intent once (INV-S04), persist the structured signals, and
2953
+ // ENFORCE the unencodable-clause contract: an unanswered clause blocks
2954
+ // the decide loop until the host resolves it via a `constraint_clauses`
2955
+ // entry on the checkpoint (CE-004, identity-keyed — the shared matcher
2956
+ // in audit-tools/shared intent/constraintClauses.ts, the same core the
2957
+ // audit gate uses). The PERSISTED sidecar is the consumed input; a
2958
+ // missing or stale sidecar is repaired by re-derivation, never skipped.
3056
2959
  id: "interpret_intent",
3057
- derive: () => existingCheckpoint?.confirmed_by === "host" &&
3058
- typeof existingCheckpoint.free_form_intent === "string" &&
3059
- existingCheckpoint.free_form_intent.trim().length > 0 &&
3060
- !existsSync(interpretationPath)
3061
- ? "missing"
3062
- : "satisfied",
2960
+ derive: () => {
2961
+ if (existingCheckpoint?.confirmed_by !== "host" ||
2962
+ typeof existingCheckpoint.free_form_intent !== "string" ||
2963
+ existingCheckpoint.free_form_intent.trim().length === 0) {
2964
+ return "satisfied";
2965
+ }
2966
+ const sidecar = readPersistedIntentInterpretationSync(interpretationPath);
2967
+ if (sidecar === null)
2968
+ return "missing";
2969
+ return unresolvedFromClauses(sidecar.unencodable_clauses, existingCheckpoint)
2970
+ .length > 0
2971
+ ? "missing"
2972
+ : "satisfied";
2973
+ },
3063
2974
  execute: async (state, c) => {
3064
- await interpretConfirmedCheckpointIntent(artifactsDir, existingCheckpoint, c.runLogger);
3065
- return { kind: "transition", state };
2975
+ const persisted = await readOrRepairIntentInterpretation(artifactsDir, existingCheckpoint, c.runLogger);
2976
+ const unresolved = persisted
2977
+ ? unresolvedFromClauses(persisted.unencodable_clauses, existingCheckpoint)
2978
+ : [];
2979
+ if (unresolved.length === 0)
2980
+ return { kind: "transition", state };
2981
+ const nextCommand = loaderCommand("next-step");
2982
+ const checkpointPath = join(artifactsDir, "intent_checkpoint.json");
2983
+ const clauseLines = unresolved
2984
+ .map((clause) => `- **${clause.clause_id}** — "${clause.text}"\n Question: ${clause.checkpoint_question}`)
2985
+ .join("\n");
2986
+ const prompt = `
2987
+ # Resolve Free-Form Intent Constraints
2988
+
2989
+ ${unresolved.length} clause(s) of the confirmed checkpoint's \`free_form_intent\` could
2990
+ not be encoded as lens/priority/scope signals. Each needs an explicit answer
2991
+ before planning proceeds — an unanswered clause would otherwise be silently
2992
+ dropped.
2993
+
2994
+ ${clauseLines}
2995
+
2996
+ Answer each clause by adding a \`constraint_clauses\` entry to
2997
+ \`intent_checkpoint.json\` (keep the exact \`clause_id\` — answers are keyed on
2998
+ clause identity, not on the question text):
2999
+
3000
+ \`\`\`json
3001
+ "constraint_clauses": [
3002
+ { "clause_id": "<the clause_id above>", "text": "<the clause text>", "checkpoint_question": "<the question above>", "host_answer": "<how to apply this constraint>" }
3003
+ ]
3004
+ \`\`\`
3005
+
3006
+ Then run:
3007
+
3008
+ \`${nextCommand}\`
3009
+ `;
3010
+ return {
3011
+ kind: "emit",
3012
+ step: await writeCurrentStep({
3013
+ stepKind: "confirm_intent",
3014
+ status: "blocked",
3015
+ runId: stateRunId(state),
3016
+ repoRoot: c.root,
3017
+ artifactsDir,
3018
+ prompt,
3019
+ allowedCommands: [nextCommand],
3020
+ stopCondition: "Stop after adding constraint_clauses answers to intent_checkpoint.json and running next-step.",
3021
+ artifactPaths: {
3022
+ intent_checkpoint: checkpointPath,
3023
+ intent_interpretation: interpretationPath,
3024
+ },
3025
+ }),
3026
+ };
3066
3027
  },
3067
3028
  },
3068
3029
  {