audit-tools 0.42.1 → 0.44.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.
- package/dispatch/merge-results.mjs +229 -140
- package/dispatch/validate-result.mjs +98 -40
- package/dist/audit/cli/conceptualDispatch.d.ts.map +1 -1
- package/dist/audit/cli/conceptualDispatch.js +35 -3
- package/dist/audit/cli/conceptualDispatch.js.map +1 -1
- package/dist/audit/cli/dispatch/hostHandoff.d.ts +9 -1
- package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
- package/dist/audit/cli/dispatch/hostHandoff.js +94 -38
- package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
- package/dist/audit/cli/laneSubmissions.d.ts +2 -2
- package/dist/audit/cli/laneSubmissions.js +2 -2
- package/dist/audit/cli/nextStepCommand.d.ts +64 -0
- package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
- package/dist/audit/cli/nextStepCommand.js +877 -783
- package/dist/audit/cli/nextStepCommand.js.map +1 -1
- package/dist/audit/cli/nextStepHelpers.d.ts +12 -36
- package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
- package/dist/audit/cli/nextStepHelpers.js +34 -77
- package/dist/audit/cli/nextStepHelpers.js.map +1 -1
- package/dist/audit/cli/steps.d.ts +2 -2
- package/dist/audit/extractors/graph.d.ts +28 -2
- package/dist/audit/extractors/graph.d.ts.map +1 -1
- package/dist/audit/extractors/graph.js +122 -11
- package/dist/audit/extractors/graph.js.map +1 -1
- package/dist/audit/extractors/graphRoutes.d.ts +8 -0
- package/dist/audit/extractors/graphRoutes.d.ts.map +1 -1
- package/dist/audit/extractors/graphRoutes.js +71 -4
- package/dist/audit/extractors/graphRoutes.js.map +1 -1
- package/dist/audit/io/artifacts.d.ts +55 -0
- package/dist/audit/io/artifacts.d.ts.map +1 -1
- package/dist/audit/io/artifacts.js +189 -12
- package/dist/audit/io/artifacts.js.map +1 -1
- package/dist/audit/io/toolingManifest.d.ts +11 -0
- package/dist/audit/io/toolingManifest.d.ts.map +1 -1
- package/dist/audit/io/toolingManifest.js +13 -3
- package/dist/audit/io/toolingManifest.js.map +1 -1
- package/dist/audit/orchestrator/advance.d.ts +64 -0
- package/dist/audit/orchestrator/advance.d.ts.map +1 -1
- package/dist/audit/orchestrator/advance.js +191 -57
- package/dist/audit/orchestrator/advance.js.map +1 -1
- package/dist/audit/orchestrator/charterClarificationExecutor.d.ts.map +1 -1
- package/dist/audit/orchestrator/charterClarificationExecutor.js +55 -21
- package/dist/audit/orchestrator/charterClarificationExecutor.js.map +1 -1
- package/dist/audit/orchestrator/flowCoverage.d.ts.map +1 -1
- package/dist/audit/orchestrator/flowCoverage.js +7 -10
- package/dist/audit/orchestrator/flowCoverage.js.map +1 -1
- package/dist/audit/orchestrator/flowPlanning.d.ts +54 -1
- package/dist/audit/orchestrator/flowPlanning.d.ts.map +1 -1
- package/dist/audit/orchestrator/flowPlanning.js +77 -18
- package/dist/audit/orchestrator/flowPlanning.js.map +1 -1
- package/dist/audit/orchestrator/flowRequeue.d.ts.map +1 -1
- package/dist/audit/orchestrator/flowRequeue.js +6 -10
- package/dist/audit/orchestrator/flowRequeue.js.map +1 -1
- package/dist/audit/orchestrator/requeueFold.d.ts.map +1 -1
- package/dist/audit/orchestrator/requeueFold.js +18 -5
- package/dist/audit/orchestrator/requeueFold.js.map +1 -1
- package/dist/audit/orchestrator/staleness.d.ts +24 -1
- package/dist/audit/orchestrator/staleness.d.ts.map +1 -1
- package/dist/audit/orchestrator/staleness.js +147 -16
- package/dist/audit/orchestrator/staleness.js.map +1 -1
- package/dist/audit/orchestrator/syntaxResolutionExecutor.d.ts +26 -0
- package/dist/audit/orchestrator/syntaxResolutionExecutor.d.ts.map +1 -1
- package/dist/audit/orchestrator/syntaxResolutionExecutor.js +97 -12
- package/dist/audit/orchestrator/syntaxResolutionExecutor.js.map +1 -1
- package/dist/audit/orchestrator/taskBuilder.d.ts.map +1 -1
- package/dist/audit/orchestrator/taskBuilder.js +111 -21
- package/dist/audit/orchestrator/taskBuilder.js.map +1 -1
- package/dist/audit/orchestrator/trivialAudit.d.ts +17 -1
- package/dist/audit/orchestrator/trivialAudit.d.ts.map +1 -1
- package/dist/audit/orchestrator/trivialAudit.js +31 -4
- package/dist/audit/orchestrator/trivialAudit.js.map +1 -1
- package/dist/audit/types/analyzerCapability.d.ts +6 -6
- package/dist/audit/types/charterRegister.d.ts +30 -12
- package/dist/audit/types/charterRegister.d.ts.map +1 -1
- package/dist/audit/types/charterRegister.js +22 -9
- package/dist/audit/types/charterRegister.js.map +1 -1
- package/dist/audit/validation/anchorGrounding.d.ts.map +1 -1
- package/dist/audit/validation/anchorGrounding.js +17 -0
- package/dist/audit/validation/anchorGrounding.js.map +1 -1
- package/dist/audit/validation/auditResults.d.ts +17 -0
- package/dist/audit/validation/auditResults.d.ts.map +1 -1
- package/dist/audit/validation/auditResults.js +50 -17
- package/dist/audit/validation/auditResults.js.map +1 -1
- package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts +48 -0
- package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts.map +1 -1
- package/dist/remediate/contractPipeline/cyclicSeamResolution.js +128 -0
- package/dist/remediate/contractPipeline/cyclicSeamResolution.js.map +1 -1
- package/dist/remediate/contractPipeline/derive.d.ts +8 -5
- package/dist/remediate/contractPipeline/derive.d.ts.map +1 -1
- package/dist/remediate/contractPipeline/derive.js +8 -16
- package/dist/remediate/contractPipeline/derive.js.map +1 -1
- package/dist/remediate/index.d.ts.map +1 -1
- package/dist/remediate/index.js +43 -2
- package/dist/remediate/index.js.map +1 -1
- package/dist/remediate/intent/intentOrdering.d.ts +13 -5
- package/dist/remediate/intent/intentOrdering.d.ts.map +1 -1
- package/dist/remediate/intent/intentOrdering.js +56 -25
- package/dist/remediate/intent/intentOrdering.js.map +1 -1
- package/dist/remediate/phases/close.d.ts +57 -2
- package/dist/remediate/phases/close.d.ts.map +1 -1
- package/dist/remediate/phases/close.js +260 -36
- package/dist/remediate/phases/close.js.map +1 -1
- package/dist/remediate/phases/triage.d.ts.map +1 -1
- package/dist/remediate/phases/triage.js +14 -2
- package/dist/remediate/phases/triage.js.map +1 -1
- package/dist/remediate/state/disposition.d.ts +26 -4
- package/dist/remediate/state/disposition.d.ts.map +1 -1
- package/dist/remediate/state/itemStatus.d.ts +33 -6
- package/dist/remediate/state/itemStatus.d.ts.map +1 -1
- package/dist/remediate/state/itemStatus.js +154 -39
- package/dist/remediate/state/itemStatus.js.map +1 -1
- package/dist/remediate/state/types.d.ts +41 -2
- package/dist/remediate/state/types.d.ts.map +1 -1
- package/dist/remediate/state/types.js.map +1 -1
- package/dist/remediate/steps/contractPipeline.d.ts +296 -15
- package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
- package/dist/remediate/steps/contractPipeline.js +1783 -1001
- package/dist/remediate/steps/contractPipeline.js.map +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.d.ts +137 -2
- package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.js +657 -59
- package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
- package/dist/remediate/steps/finalGate.d.ts +136 -37
- package/dist/remediate/steps/finalGate.d.ts.map +1 -1
- package/dist/remediate/steps/finalGate.js +120 -86
- package/dist/remediate/steps/finalGate.js.map +1 -1
- package/dist/remediate/steps/gateCommands.d.ts +8 -0
- package/dist/remediate/steps/gateCommands.d.ts.map +1 -1
- package/dist/remediate/steps/gateCommands.js +40 -11
- package/dist/remediate/steps/gateCommands.js.map +1 -1
- package/dist/remediate/steps/nextStep.d.ts +96 -5
- package/dist/remediate/steps/nextStep.d.ts.map +1 -1
- package/dist/remediate/steps/nextStep.js +668 -147
- package/dist/remediate/steps/nextStep.js.map +1 -1
- package/dist/remediate/steps/types.d.ts +4 -3
- package/dist/remediate/steps/types.d.ts.map +1 -1
- package/dist/remediate/steps/types.js +10 -2
- package/dist/remediate/steps/types.js.map +1 -1
- package/dist/remediate/validation/artifacts.d.ts +20 -2
- package/dist/remediate/validation/artifacts.d.ts.map +1 -1
- package/dist/remediate/validation/artifacts.js +224 -130
- package/dist/remediate/validation/artifacts.js.map +1 -1
- package/dist/remediate/validation/contractPipelineGates.d.ts +58 -0
- package/dist/remediate/validation/contractPipelineGates.d.ts.map +1 -1
- package/dist/remediate/validation/contractPipelineGates.js +143 -27
- package/dist/remediate/validation/contractPipelineGates.js.map +1 -1
- package/dist/shared/analyzers/acquisitionEngine.d.ts +72 -25
- package/dist/shared/analyzers/acquisitionEngine.d.ts.map +1 -1
- package/dist/shared/analyzers/acquisitionEngine.js +149 -15
- package/dist/shared/analyzers/acquisitionEngine.js.map +1 -1
- package/dist/shared/analyzers/binaryAcquisition.d.ts +27 -2
- package/dist/shared/analyzers/binaryAcquisition.d.ts.map +1 -1
- package/dist/shared/analyzers/binaryAcquisition.js +149 -43
- package/dist/shared/analyzers/binaryAcquisition.js.map +1 -1
- package/dist/shared/analyzers/candidates.d.ts +9 -8
- package/dist/shared/analyzers/candidates.d.ts.map +1 -1
- package/dist/shared/analyzers/candidates.js +96 -64
- package/dist/shared/analyzers/candidates.js.map +1 -1
- package/dist/shared/analyzers/normalizeExternal.d.ts +40 -12
- package/dist/shared/analyzers/normalizeExternal.d.ts.map +1 -1
- package/dist/shared/analyzers/normalizeExternal.js +74 -18
- package/dist/shared/analyzers/normalizeExternal.js.map +1 -1
- package/dist/shared/analyzers/rubocop.d.ts +14 -13
- package/dist/shared/analyzers/rubocop.d.ts.map +1 -1
- package/dist/shared/analyzers/rubocop.js +29 -6
- package/dist/shared/analyzers/rubocop.js.map +1 -1
- package/dist/shared/analyzers/types.d.ts +196 -13
- package/dist/shared/analyzers/types.d.ts.map +1 -1
- package/dist/shared/analyzers/types.js +122 -9
- package/dist/shared/analyzers/types.js.map +1 -1
- package/dist/shared/decompose/charterClarification.d.ts +12 -1
- package/dist/shared/decompose/charterClarification.d.ts.map +1 -1
- package/dist/shared/decompose/charterClarification.js +11 -4
- package/dist/shared/decompose/charterClarification.js.map +1 -1
- package/dist/shared/decompose/charterExtraction.d.ts +20 -10
- package/dist/shared/decompose/charterExtraction.d.ts.map +1 -1
- package/dist/shared/decompose/charterExtraction.js +52 -23
- package/dist/shared/decompose/charterExtraction.js.map +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +5 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/intent/freeFormIntentInterpreter.d.ts +12 -2
- package/dist/shared/intent/freeFormIntentInterpreter.d.ts.map +1 -1
- package/dist/shared/intent/freeFormIntentInterpreter.js +27 -22
- package/dist/shared/intent/freeFormIntentInterpreter.js.map +1 -1
- package/dist/shared/intent/pathScope.d.ts +12 -1
- package/dist/shared/intent/pathScope.d.ts.map +1 -1
- package/dist/shared/intent/pathScope.js +56 -19
- package/dist/shared/intent/pathScope.js.map +1 -1
- package/dist/shared/intent/sharedIntentData.d.ts +19 -1
- package/dist/shared/intent/sharedIntentData.d.ts.map +1 -1
- package/dist/shared/intent/sharedIntentData.js +33 -8
- package/dist/shared/intent/sharedIntentData.js.map +1 -1
- package/dist/shared/io/fileLock.d.ts +12 -0
- package/dist/shared/io/fileLock.d.ts.map +1 -1
- package/dist/shared/io/fileLock.js +48 -5
- package/dist/shared/io/fileLock.js.map +1 -1
- package/dist/shared/io/json.d.ts +13 -0
- package/dist/shared/io/json.d.ts.map +1 -1
- package/dist/shared/io/json.js +19 -1
- package/dist/shared/io/json.js.map +1 -1
- package/dist/shared/steps/stepEmissionScaffold.d.ts +69 -0
- package/dist/shared/steps/stepEmissionScaffold.d.ts.map +1 -0
- package/dist/shared/steps/stepEmissionScaffold.js +83 -0
- package/dist/shared/steps/stepEmissionScaffold.js.map +1 -0
- package/dist/shared/submission/handRecovery.d.ts.map +1 -1
- package/dist/shared/submission/handRecovery.js +69 -4
- package/dist/shared/submission/handRecovery.js.map +1 -1
- package/dist/shared/submission/submissionIdentity.d.ts +10 -1
- package/dist/shared/submission/submissionIdentity.d.ts.map +1 -1
- package/dist/shared/submission/submissionIdentity.js +12 -3
- package/dist/shared/submission/submissionIdentity.js.map +1 -1
- package/dist/shared/submission/submissionLedger.d.ts +43 -2
- package/dist/shared/submission/submissionLedger.d.ts.map +1 -1
- package/dist/shared/submission/submissionLedger.js +50 -7
- package/dist/shared/submission/submissionLedger.js.map +1 -1
- package/dist/shared/tooling/allowlistedExec.d.ts +9 -0
- package/dist/shared/tooling/allowlistedExec.d.ts.map +1 -1
- package/dist/shared/tooling/allowlistedExec.js +19 -2
- package/dist/shared/tooling/allowlistedExec.js.map +1 -1
- package/dist/shared/tooling/commandShape.d.ts +21 -0
- package/dist/shared/tooling/commandShape.d.ts.map +1 -0
- package/dist/shared/tooling/commandShape.js +120 -0
- package/dist/shared/tooling/commandShape.js.map +1 -0
- package/dist/shared/types/charter.d.ts +24 -0
- package/dist/shared/types/charter.d.ts.map +1 -1
- package/dist/shared/types/charter.js +9 -0
- package/dist/shared/types/charter.js.map +1 -1
- package/dist/shared/types/remediationOutcome.d.ts +282 -9
- package/dist/shared/types/remediationOutcome.d.ts.map +1 -1
- package/dist/shared/types/remediationOutcome.js +174 -1
- package/dist/shared/types/remediationOutcome.js.map +1 -1
- package/package.json +2 -1
- package/remediate-code.mjs +15 -0
- package/wrapper/remediate-code-wrapper-install-hosts.mjs +45 -4
- package/wrapper/remediate-code-wrapper-install-renderers.mjs +12 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadRemediateSessionConfig } from "./sessionConfigLoad.js";
|
|
2
2
|
import { existsSync, statSync } from "node:fs";
|
|
3
|
-
import { mkdir, readFile, rename } from "node:fs/promises";
|
|
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
|
|
@@ -15,17 +15,18 @@ import { runTriagePhase } from "../phases/triage.js";
|
|
|
15
15
|
import { runClosePhase } from "../phases/close.js";
|
|
16
16
|
import { validateRemediationPlan } from "../validation/remediationState.js";
|
|
17
17
|
import { readExtractedPlanIfPresent, } from "./dispatch.js";
|
|
18
|
-
import { ingestRemediationHostResults, hostDependencyLevels, prepareRemediationHostHandoff, } from "./dispatch/hostHandoff.js";
|
|
18
|
+
import { ingestRemediationHostResults, hostDependencyLevels, precomputeRecoveryTestVerdicts, prepareRemediationHostHandoff, } from "./dispatch/hostHandoff.js";
|
|
19
19
|
import { FileLockTimeoutError, withFileLock, } from "../../shared/io/fileLock.js";
|
|
20
|
-
import { AUDIT_FINDINGS_FILENAME, AUDIT_REPORT_FILENAME, auditArtifactsDir, auditFindingsPath, auditReportPath,
|
|
20
|
+
import { AUDIT_FINDINGS_FILENAME, AUDIT_REPORT_FILENAME, auditArtifactsDir, auditFindingsPath, auditReportPath, promotedAuditFindingsPath, promotedAuditReportPath, remediationArtifactsDir, } from "../../shared/io/auditToolsPaths.js";
|
|
21
21
|
import { resolveRepoRoot } from "../../shared/io/repoRoot.js";
|
|
22
22
|
import { writeCurrentStep } from "./stepWriter.js";
|
|
23
23
|
import { dependencyAwaitingClarification, dependencyVerifiedComplete, } from "./stepUtils.js";
|
|
24
24
|
import { isTerminalStatus, isVerifiedCompleteStatus, } from "../state/itemStatus.js";
|
|
25
25
|
import { deduplicateCrossLensFindings, fixupBlocksAfterDedup, } from "../dedup/crossLensDedup.js";
|
|
26
26
|
import { checkAffectedFileIntegrity } from "../utils/fileIntegrity.js";
|
|
27
|
+
import { applyIntentOrdering } from "../intent/intentOrdering.js";
|
|
27
28
|
import { resolveIntakeStep } from "./intakeResolver.js";
|
|
28
|
-
import { runToolOwnedFinalGate,
|
|
29
|
+
import { runToolOwnedFinalGate, writeFinalGateRedRecord, writeFinalGateOutcomeRecord, } from "./finalGate.js";
|
|
29
30
|
import { buildNextContractPipelineStep, shouldEnterContractPipeline, writePathASeedFromFindings, buildLeanExtractedPlan, } from "./contractPipeline.js";
|
|
30
31
|
import { contractArtifactExists, contractPipelineDir, } from "../contractPipeline/artifactStore.js";
|
|
31
32
|
import { buildReviewRequest, applyReviewResolution, isResolutionForRequest, screenResolutionIds, REVIEW_REQUEST_SCHEMA_VERSION, } from "../review/reviewGate.js";
|
|
@@ -69,7 +70,31 @@ function stateRunId(state) {
|
|
|
69
70
|
// record path is deterministic across multiple next-step calls on the same run.
|
|
70
71
|
return state?.plan?.plan_id ?? "run";
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Where an autonomous run's LEFTOVER deliverable pair lands.
|
|
75
|
+
*
|
|
76
|
+
* REMEDIATION-OWNED, deliberately. This module used to write the canonical
|
|
77
|
+
* `.audit-tools/audit-findings.json` + `audit-report.md` pair directly and
|
|
78
|
+
* unarchived, which destroys the audit source `defaultInputCandidates` resolves
|
|
79
|
+
* FIRST — the original contract becomes unrecoverable for any external consumer
|
|
80
|
+
* (INV-RNF-NO-CANONICAL-PAIR-WRITE). The canonical pair belongs to
|
|
81
|
+
* audit-artifact-promotion-lifecycle, whose exported write-with-archive is the
|
|
82
|
+
* only sanctioned way to replace it.
|
|
83
|
+
*/
|
|
84
|
+
export function autonomousLeftoverFindingsPath(root) {
|
|
85
|
+
return join(remediationArtifactsDir(root), "autonomous-leftovers-findings.json");
|
|
86
|
+
}
|
|
87
|
+
export function autonomousLeftoverReportPath(root) {
|
|
88
|
+
return join(remediationArtifactsDir(root), "autonomous-leftovers-report.md");
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The intake sources a bare `next-step` discovers, IN PRIORITY ORDER — index 0
|
|
92
|
+
* wins. Exported so the ordering can be asserted by CALLING it: the property
|
|
93
|
+
* that matters ("a real audit always beats this run's own leftovers") is a fact
|
|
94
|
+
* about the returned array, and a test that reads it out of the source text is
|
|
95
|
+
* asserting the prose, not the order.
|
|
96
|
+
*/
|
|
97
|
+
export function defaultInputCandidates(root) {
|
|
73
98
|
// Prefer the canonical machine contract (audit-findings.json) over its
|
|
74
99
|
// human-facing render (audit-report.md). The JSON is the source of truth on
|
|
75
100
|
// both sides of the audit -> remediate pipeline, and feeding it triggers the
|
|
@@ -83,6 +108,13 @@ function defaultInputCandidates(root) {
|
|
|
83
108
|
promotedAuditReportPath(auditDir),
|
|
84
109
|
auditReportPath(auditDir),
|
|
85
110
|
join(root, AUDIT_REPORT_FILENAME),
|
|
111
|
+
// LAST, so a real audit always wins. The autonomous leftover pair moved off
|
|
112
|
+
// the canonical paths (it may no longer overwrite them), and without a
|
|
113
|
+
// candidate entry the next unattended run would stop round-tripping its own
|
|
114
|
+
// leftovers back through intake — the behaviour the old canonical write was
|
|
115
|
+
// there to provide, kept without the destructive overwrite.
|
|
116
|
+
autonomousLeftoverFindingsPath(root),
|
|
117
|
+
autonomousLeftoverReportPath(root),
|
|
86
118
|
];
|
|
87
119
|
}
|
|
88
120
|
function inputValues(input) {
|
|
@@ -269,16 +301,15 @@ export function phaseBoundaryToGate(state) {
|
|
|
269
301
|
.every((b) => b.items.every((id) => items[id]?.status === "pending"));
|
|
270
302
|
return pristine ? dispatchPhase : null;
|
|
271
303
|
}
|
|
272
|
-
// Tool-owned final completion gate (INV-RS-10)
|
|
304
|
+
// Tool-owned final completion gate (INV-RS-10)
|
|
273
305
|
// ---------------------------------------------------------------------------
|
|
274
306
|
//
|
|
275
|
-
// The gate runner
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
// completion handler and re-exported to preserve this module's public surface +
|
|
307
|
+
// The gate runner and its red record live in the sibling leaf module
|
|
308
|
+
// `finalGate.ts`. They are imported below for local use in the boundary and
|
|
309
|
+
// completion gates and re-exported to preserve this module's public surface +
|
|
279
310
|
// existing test imports. See `finalGate.ts` for the INV-RS-10 / CE-001 / CE-002
|
|
280
311
|
// documentation.
|
|
281
|
-
export { isAuditToolsMonorepo, toolOwnedFinalGateCommands, runToolOwnedFinalGate,
|
|
312
|
+
export { isAuditToolsMonorepo, toolOwnedFinalGateCommands, runToolOwnedFinalGate, finalGateOutcomePath, writeFinalGateOutcomeRecord, } from "./finalGate.js";
|
|
282
313
|
function resolvedOrTerminalItems(state) {
|
|
283
314
|
return Object.values(state.items ?? {}).filter((item) => isTerminalStatus(item.status));
|
|
284
315
|
}
|
|
@@ -286,6 +317,26 @@ function allItemsTerminal(state) {
|
|
|
286
317
|
const items = Object.values(state.items ?? {});
|
|
287
318
|
return items.length > 0 && resolvedOrTerminalItems(state).length === items.length;
|
|
288
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Reorder a finalized plan by the checkpoint's interpreted intent.
|
|
322
|
+
*
|
|
323
|
+
* The checkpoint persists the operator's `free_form_intent` verbatim; the
|
|
324
|
+
* structured `InterpretedIntent` is derived from it by the single shared
|
|
325
|
+
* interpreter. INV-S04: the raw directive is never read past this line — only
|
|
326
|
+
* the derived lens-weight / priority / scope signals reach the ordering, so the
|
|
327
|
+
* verbatim string cannot leak into a worker prompt through this path.
|
|
328
|
+
*
|
|
329
|
+
* Absent checkpoint, absent intent, or an intent that interprets to nothing all
|
|
330
|
+
* return the plan untouched.
|
|
331
|
+
*/
|
|
332
|
+
async function applyCheckpointIntentOrdering(artifactsDir, plan) {
|
|
333
|
+
const checkpoint = await readOptionalJsonFile(join(artifactsDir, "intent_checkpoint.json")).catch(() => undefined);
|
|
334
|
+
const freeForm = checkpoint?.free_form_intent;
|
|
335
|
+
if (typeof freeForm !== "string" || freeForm.trim().length === 0)
|
|
336
|
+
return plan;
|
|
337
|
+
const ordered = applyIntentOrdering(plan.findings, plan.blocks, interpretFreeFormIntent(freeForm));
|
|
338
|
+
return { ...plan, findings: ordered.findings, blocks: ordered.blocks };
|
|
339
|
+
}
|
|
289
340
|
function normalizeExtractedPlan(value) {
|
|
290
341
|
if (!isRecord(value)) {
|
|
291
342
|
throw new Error("extracted-plan.json must be an object.");
|
|
@@ -394,7 +445,20 @@ function stripPlanTimeBookkeeping(value) {
|
|
|
394
445
|
}
|
|
395
446
|
return stripped;
|
|
396
447
|
}
|
|
397
|
-
|
|
448
|
+
/**
|
|
449
|
+
* The re-plan carry-forward identity of a finding: canonical JSON with the
|
|
450
|
+
* plan-time bookkeeping keys stripped, so a re-plan whose only delta is a
|
|
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.
|
|
453
|
+
*
|
|
454
|
+
* EXPORTED so the invariant suite can call THIS function. It was module-internal,
|
|
455
|
+
* and the suite claiming to cover the invariant declared its own copy of the key
|
|
456
|
+
* set, the strip and the key builder — so dropping `evidence_grounded` from the
|
|
457
|
+
* production set, or widening it with a real field like `severity`, left the
|
|
458
|
+
* block green while carry-forward regressed. A test asserting against its own
|
|
459
|
+
* re-implementation pins nothing about shipped behaviour.
|
|
460
|
+
*/
|
|
461
|
+
export function findingCarryForwardKey(finding) {
|
|
398
462
|
return JSON.stringify(stripPlanTimeBookkeeping(finding));
|
|
399
463
|
}
|
|
400
464
|
// Single-block membership is enforced by `fixupBlocksAfterDedup` (each finding
|
|
@@ -453,7 +517,7 @@ function carryForwardMatchingItems(previous, replanned) {
|
|
|
453
517
|
status: hasPending ? "planning" : replanned.status,
|
|
454
518
|
};
|
|
455
519
|
}
|
|
456
|
-
async function forceReplanFromExistingIntake(root, artifactsDir, previous, store) {
|
|
520
|
+
async function forceReplanFromExistingIntake(root, artifactsDir, previous, store, runLogger) {
|
|
457
521
|
const pendingState = {
|
|
458
522
|
status: "pending",
|
|
459
523
|
started_at: previous.started_at,
|
|
@@ -476,7 +540,7 @@ async function forceReplanFromExistingIntake(root, artifactsDir, previous, store
|
|
|
476
540
|
await store.saveState(pendingState);
|
|
477
541
|
return null;
|
|
478
542
|
}
|
|
479
|
-
const replanned = await handlePendingExtractedPlan(root, artifactsDir, pendingState, extractedPlan);
|
|
543
|
+
const replanned = await handlePendingExtractedPlan(root, artifactsDir, pendingState, extractedPlan, runLogger);
|
|
480
544
|
if (!replanned) {
|
|
481
545
|
return null;
|
|
482
546
|
}
|
|
@@ -525,8 +589,120 @@ function currentHostBoundaryState(state) {
|
|
|
525
589
|
...state,
|
|
526
590
|
};
|
|
527
591
|
}
|
|
592
|
+
/**
|
|
593
|
+
* The `recover-ingest` verb's whole body: ingest the host's landed results in
|
|
594
|
+
* RECOVERY mode and persist through the same file-locked, atomically-writing
|
|
595
|
+
* store, with the same `contract_version` strip.
|
|
596
|
+
*
|
|
597
|
+
* It is a separate verb rather than a flag on `next-step` because the
|
|
598
|
+
* relaxation it enables must be an operator's explicit act — see
|
|
599
|
+
* `ingestRemediationHostResults`, which states what is waived and the residual
|
|
600
|
+
* risk. Nothing else here differs from the normal ingestion: the same workload,
|
|
601
|
+
* the same contract gates, the same eligibility frontier.
|
|
602
|
+
*
|
|
603
|
+
* ## Why this runs in two phases
|
|
604
|
+
*
|
|
605
|
+
* A required-test rerun is `spawnSync`, which blocks the event loop for its
|
|
606
|
+
* whole duration. Run inside the state lock, it would starve the lock's own
|
|
607
|
+
* heartbeat timer (`setInterval` in the shared fileLock) — the held lock's mtime
|
|
608
|
+
* would stop being refreshed, a second acquirer would classify it as stale at
|
|
609
|
+
* ~30s and steal it, and mutual exclusion would be gone precisely during the
|
|
610
|
+
* longest critical section in the codebase. Holding a lock across a blocking
|
|
611
|
+
* spawn is therefore not merely slow; it is unsound.
|
|
612
|
+
*
|
|
613
|
+
* So:
|
|
614
|
+
*
|
|
615
|
+
* - **Phase 1, UNLOCKED.** Snapshot the state, capture HEAD, and run every
|
|
616
|
+
* distinct required-test command exactly once
|
|
617
|
+
* (`precomputeRecoveryTestVerdicts`). HEAD is captured BEFORE the spawns, not
|
|
618
|
+
* after, because a host-authored command that MOVES HEAD would otherwise
|
|
619
|
+
* produce verdicts of mixed provenance and go undetected. (The guard compares
|
|
620
|
+
* commit shas: it sees HEAD movement, not worktree dirt — a command that only
|
|
621
|
+
* dirties files is invisible to it, which is acceptable because phase 2's
|
|
622
|
+
* corroboration is commit-based.)
|
|
623
|
+
* - **Phase 2, LOCKED.** Re-read HEAD and abort the whole recovery if it moved
|
|
624
|
+
* (`tree_moved_between_phases`) — the phase-1 verdicts would describe a tree
|
|
625
|
+
* that no longer exists, and nothing is accepted or appended. Otherwise ingest
|
|
626
|
+
* with the pre-computed verdicts, which the ingest only READS: in recovery
|
|
627
|
+
* mode it never spawns, and a command missing from the table fails closed.
|
|
628
|
+
*
|
|
629
|
+
* What remains inside the lock is git plumbing (ancestry, ref scan, diff-tree),
|
|
630
|
+
* the ledger append, and the state write — sub-second work, comfortably inside
|
|
631
|
+
* heartbeat coverage. The HEAD-unchanged guard closes the gap the phase split
|
|
632
|
+
* opens; the operational protocol is still one writer at a time, now enforced by
|
|
633
|
+
* a lock that cannot be stolen mid-hold instead of by convention.
|
|
634
|
+
*
|
|
635
|
+
* One accepted cost: `StateStore.mutate` always writes, so a recovery run that
|
|
636
|
+
* changes nothing rewrites `state.json` with identical content. Expressing a
|
|
637
|
+
* true no-op means plumbing the locked store's `SKIP_WRITE` sentinel through
|
|
638
|
+
* `StateStore.mutate`, which is a change to the store's API rather than to this
|
|
639
|
+
* verb. The `state_changed` flag on the returned summary stays authoritative
|
|
640
|
+
* for callers either way.
|
|
641
|
+
*/
|
|
642
|
+
export async function recoverIngestHostResults(options) {
|
|
643
|
+
const root = resolveRoot(options.root);
|
|
644
|
+
const artifactsDir = resolveArtifactsDir(root, options.artifactsDir);
|
|
645
|
+
const store = new StateStore(artifactsDir);
|
|
646
|
+
// ── Phase 1: unlocked ────────────────────────────────────────────────────
|
|
647
|
+
const snapshot = await store.loadState();
|
|
648
|
+
if (!snapshot) {
|
|
649
|
+
throw new Error(`No remediation state at ${artifactsDir} — there is nothing to ingest.`);
|
|
650
|
+
}
|
|
651
|
+
const headBeforeTests = headCommit(root);
|
|
652
|
+
const requiredTestVerdicts = await precomputeRecoveryTestVerdicts({
|
|
653
|
+
root,
|
|
654
|
+
artifactsDir,
|
|
655
|
+
runId: options.runId,
|
|
656
|
+
state: currentHostBoundaryState(snapshot),
|
|
657
|
+
});
|
|
658
|
+
if (requiredTestVerdicts === "unsupported_retired_state") {
|
|
659
|
+
throw new Error("Remediation state uses a retired dispatch shape and cannot cross the host handoff boundary.");
|
|
660
|
+
}
|
|
661
|
+
// ── Phase 2: locked, spawn-free ──────────────────────────────────────────
|
|
662
|
+
let ingested;
|
|
663
|
+
await store.mutate(async (state) => {
|
|
664
|
+
if (!state) {
|
|
665
|
+
throw new Error(`No remediation state at ${artifactsDir} — there is nothing to ingest.`);
|
|
666
|
+
}
|
|
667
|
+
const headNow = headCommit(root);
|
|
668
|
+
if (headNow !== headBeforeTests) {
|
|
669
|
+
ingested = {
|
|
670
|
+
accepted_count: 0,
|
|
671
|
+
completed_work_item_ids: [],
|
|
672
|
+
pending_work_item_ids: state.host_handoff?.work_item_ids ?? [],
|
|
673
|
+
issues: [
|
|
674
|
+
{
|
|
675
|
+
code: "tree_moved_between_phases",
|
|
676
|
+
message: `HEAD moved from ${headBeforeTests ?? "(none)"} to ${headNow ?? "(none)"} ` +
|
|
677
|
+
"while the required tests were running, so their verdicts no longer describe " +
|
|
678
|
+
"this tree. Nothing was accepted; re-run recover-ingest on a settled tree.",
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
state_changed: false,
|
|
682
|
+
state: currentHostBoundaryState(state),
|
|
683
|
+
};
|
|
684
|
+
return state;
|
|
685
|
+
}
|
|
686
|
+
const outcome = await ingestRemediationHostResults({
|
|
687
|
+
root,
|
|
688
|
+
artifactsDir,
|
|
689
|
+
runId: options.runId,
|
|
690
|
+
state: currentHostBoundaryState(state),
|
|
691
|
+
recovery: { requiredTestVerdicts },
|
|
692
|
+
});
|
|
693
|
+
if (outcome === "unsupported_retired_state") {
|
|
694
|
+
throw new Error("Remediation state uses a retired dispatch shape and cannot cross the host handoff boundary.");
|
|
695
|
+
}
|
|
696
|
+
ingested = outcome;
|
|
697
|
+
if (!outcome.state_changed)
|
|
698
|
+
return state;
|
|
699
|
+
const { contract_version: _contractVersion, ...persistableState } = outcome.state;
|
|
700
|
+
return persistableState;
|
|
701
|
+
});
|
|
702
|
+
return ingested;
|
|
703
|
+
}
|
|
528
704
|
async function buildImplementDispatchStep(ctx) {
|
|
529
|
-
const { root, artifactsDir, state, store } = ctx;
|
|
705
|
+
const { root, artifactsDir, state, store, runLogger } = ctx;
|
|
530
706
|
const runId = stateRunId(state);
|
|
531
707
|
const boundaryState = currentHostBoundaryState(state);
|
|
532
708
|
const ingested = await ingestRemediationHostResults({
|
|
@@ -564,6 +740,21 @@ async function buildImplementDispatchStep(ctx) {
|
|
|
564
740
|
host_handoff: handoff.handoff_record,
|
|
565
741
|
});
|
|
566
742
|
}
|
|
743
|
+
// Ingest issues reached the PROMPT and nothing else — a channel that survives
|
|
744
|
+
// exactly as long as the host reads that one step. They are also the durable
|
|
745
|
+
// record of which submitted results were rejected and why, so they are logged
|
|
746
|
+
// as well as rendered.
|
|
747
|
+
for (const issue of ingested.issues) {
|
|
748
|
+
runLogger.event({
|
|
749
|
+
phase: "next-step",
|
|
750
|
+
kind: "outcome",
|
|
751
|
+
obligation: "host_ingest",
|
|
752
|
+
note: `host_ingest_issue code=${issue.code}` +
|
|
753
|
+
(issue.work_item_id ? ` work_item=${issue.work_item_id}` : "") +
|
|
754
|
+
(issue.result_path ? ` result=${issue.result_path}` : "") +
|
|
755
|
+
` message=${issue.message}`,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
567
758
|
const resultDiagnostics = ingested.issues.length === 0
|
|
568
759
|
? ""
|
|
569
760
|
: `
|
|
@@ -668,7 +859,39 @@ async function handleComplete(root, artifactsDir, state) {
|
|
|
668
859
|
export async function decideRemediateFrictionCloseout(artifactsDir, state) {
|
|
669
860
|
return decideFrictionTriage(artifactsDir, stateRunId(state), "remediate-code");
|
|
670
861
|
}
|
|
671
|
-
|
|
862
|
+
/**
|
|
863
|
+
* Copy an unusable extracted plan somewhere recoverable and PROVE the copy
|
|
864
|
+
* landed, returning the archive path. Read back and compared byte-for-byte:
|
|
865
|
+
* "the write did not throw" is not evidence a file exists, and this is the last
|
|
866
|
+
* moment the plan is recoverable at all.
|
|
867
|
+
*
|
|
868
|
+
* THROWS rather than returning when the copy cannot be made or verified, so the
|
|
869
|
+
* caller's unlink is unreachable on that path — an irreversible delete never
|
|
870
|
+
* runs before its archive is written and verified.
|
|
871
|
+
*/
|
|
872
|
+
async function archiveExtractedPlan(extractedPlanPath) {
|
|
873
|
+
// BYTES, compared with Buffer.compare — the bar CP-NODE-3 set for the
|
|
874
|
+
// verified-archive promotion path. A utf8-string compare is a weaker claim
|
|
875
|
+
// than the one an archive has to make: it silently equates byte sequences that
|
|
876
|
+
// decode alike (a BOM, a lone surrogate, an invalid sequence replaced by
|
|
877
|
+
// U+FFFD on BOTH sides), so a corrupt copy can read as verified.
|
|
878
|
+
const original = await readFile(extractedPlanPath);
|
|
879
|
+
const archivePath = join(dirname(extractedPlanPath), "archive", `extracted-plan-${Date.now()}.json`);
|
|
880
|
+
await mkdir(dirname(archivePath), { recursive: true });
|
|
881
|
+
await writeFile(archivePath, original);
|
|
882
|
+
const readBack = await readFile(archivePath);
|
|
883
|
+
if (Buffer.compare(original, readBack) !== 0) {
|
|
884
|
+
throw new Error(`Extracted-plan archive at ${archivePath} does not match the original bytes.`);
|
|
885
|
+
}
|
|
886
|
+
return archivePath;
|
|
887
|
+
}
|
|
888
|
+
async function handlePendingExtractedPlan(root, artifactsDir, existing, extractedPlan,
|
|
889
|
+
// The plan path's high-consequence events — findings dropped by grounding, and
|
|
890
|
+
// the plan being destroyed — are DURABLE, so the logger is a parameter rather
|
|
891
|
+
// than a module-level singleton reached for at the point of use. stderr is not
|
|
892
|
+
// captured into the artifact dir; before this, the durable tree held no trace
|
|
893
|
+
// that a plan had been destroyed or that findings had been dropped.
|
|
894
|
+
runLogger) {
|
|
672
895
|
// The discard-and-re-extract recovery below covers EXACTLY the region whose
|
|
673
896
|
// failures mean the extracted PLAN is unusable: normalization and grounding.
|
|
674
897
|
// It deliberately stops there. Everything after it — sizing, the dirty
|
|
@@ -688,6 +911,14 @@ async function handlePendingExtractedPlan(root, artifactsDir, existing, extracte
|
|
|
688
911
|
let grounding;
|
|
689
912
|
try {
|
|
690
913
|
({ plan, sourceFindings, mergeMap } = normalizeExtractedPlan(extractedPlan));
|
|
914
|
+
// INTENT ORDERING, applied where the plan's findings and blocks are
|
|
915
|
+
// FINALIZED. `applyIntentOrdering` existed with no production caller at all:
|
|
916
|
+
// the checkpoint's interpreted intent was written and never read back, so
|
|
917
|
+
// "the work the user emphasised is dispatched first" was a property the code
|
|
918
|
+
// could state but not deliver. Ordering ONLY — it never drops or mutates a
|
|
919
|
+
// finding; every input is present in the output with a different order, so a
|
|
920
|
+
// plan whose checkpoint carries no intent is returned unchanged.
|
|
921
|
+
plan = await applyCheckpointIntentOrdering(artifactsDir, plan);
|
|
691
922
|
// Deterministic grounding for the LLM-extracted plan (this path never sees
|
|
692
923
|
// structured audit findings): strip phantom affected_files paths, drop
|
|
693
924
|
// findings whose every cited path was phantom, and classify evidence. No
|
|
@@ -701,6 +932,21 @@ async function handlePendingExtractedPlan(root, artifactsDir, existing, extracte
|
|
|
701
932
|
evidenceGrounding: plan.source !== "contract_pipeline",
|
|
702
933
|
});
|
|
703
934
|
if (grounding.dropped.length > 0) {
|
|
935
|
+
// DROPPED-ID BOOKKEEPING, durable. A caller reading a finding count across
|
|
936
|
+
// the plan boundary must never receive the submitted count when findings
|
|
937
|
+
// were dropped, so the ids, the dropped count and the surviving grounded
|
|
938
|
+
// count all land in the run log — not only on stderr, which no artifact
|
|
939
|
+
// captures.
|
|
940
|
+
const droppedIds = grounding.dropped.map((d) => d.finding.id);
|
|
941
|
+
runLogger.event({
|
|
942
|
+
phase: "next-step",
|
|
943
|
+
kind: "outcome",
|
|
944
|
+
obligation: "plan_grounding",
|
|
945
|
+
note: `grounding_dropped_findings dropped=${String(droppedIds.length)} ` +
|
|
946
|
+
`grounded=${String(grounding.findings.length)} ` +
|
|
947
|
+
`submitted=${String(plan.findings.length)} ` +
|
|
948
|
+
`ids=${droppedIds.join(",")}`,
|
|
949
|
+
});
|
|
704
950
|
process.stderr.write(`[remediate-code] Grounding dropped ${grounding.dropped.length} extracted finding(s) whose cited paths do not exist: ${grounding.dropped.map((d) => `${d.finding.id} (${d.phantomPaths.join(", ")})`).join("; ")}\n`);
|
|
705
951
|
}
|
|
706
952
|
plan.findings = grounding.findings;
|
|
@@ -714,12 +960,43 @@ async function handlePendingExtractedPlan(root, artifactsDir, existing, extracte
|
|
|
714
960
|
}
|
|
715
961
|
catch (error) {
|
|
716
962
|
const paths = intakePaths(artifactsDir);
|
|
717
|
-
|
|
963
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
964
|
+
// ARCHIVE, VERIFY, THEN destroy — in that order, with the delete unreachable
|
|
965
|
+
// if the archive did not land. This recovery used to unlink the plan
|
|
966
|
+
// outright, unarchived and unverified, leaving a line on stderr as the only
|
|
967
|
+
// record; the plan the run was built from was simply gone.
|
|
968
|
+
let archivePath;
|
|
969
|
+
if (existsSync(paths.extractedPlan)) {
|
|
970
|
+
try {
|
|
971
|
+
archivePath = await archiveExtractedPlan(paths.extractedPlan);
|
|
972
|
+
}
|
|
973
|
+
catch (archiveError) {
|
|
974
|
+
const detail = archiveError instanceof Error
|
|
975
|
+
? archiveError.message
|
|
976
|
+
: String(archiveError);
|
|
977
|
+
runLogger.event({
|
|
978
|
+
phase: "next-step",
|
|
979
|
+
kind: "error",
|
|
980
|
+
obligation: "extracted_plan_recovery",
|
|
981
|
+
note: `extracted_plan_archive_failed reason=${reason} archive_error=${detail}`,
|
|
982
|
+
});
|
|
983
|
+
// NOT a re-emitted extraction step. Returning null here would report a
|
|
984
|
+
// routine "re-extract, please" while the plan was destroyed and nothing
|
|
985
|
+
// held a copy of it.
|
|
986
|
+
throw new Error(`Extracted plan at ${paths.extractedPlan} is unusable (${reason}) but could ` +
|
|
987
|
+
`not be archived (${detail}); it was left in place rather than destroyed.`);
|
|
988
|
+
}
|
|
718
989
|
const { unlink } = await import("node:fs/promises");
|
|
719
990
|
await unlink(paths.extractedPlan);
|
|
720
991
|
}
|
|
721
|
-
|
|
722
|
-
|
|
992
|
+
runLogger.event({
|
|
993
|
+
phase: "next-step",
|
|
994
|
+
kind: "outcome",
|
|
995
|
+
obligation: "extracted_plan_recovery",
|
|
996
|
+
note: `extracted_plan_removed reason=${reason} ` +
|
|
997
|
+
`archive=${archivePath ?? "(nothing on disk to archive)"}`,
|
|
998
|
+
});
|
|
999
|
+
process.stderr.write(`[remediate-code] Unusable extracted-plan.json removed (${reason}); archived at ${archivePath ?? "(nothing on disk to archive)"}. Re-emitting extraction step.\n`);
|
|
723
1000
|
return null;
|
|
724
1001
|
}
|
|
725
1002
|
// Past the recovery boundary: a failure below is a real failure and propagates.
|
|
@@ -1102,15 +1379,33 @@ async function emitAutonomousLeftoverDeliverable(root, artifactsDir, leftovers)
|
|
|
1102
1379
|
"fail-closed non-destructiveness allowlist (or not tier-safe), so not auto-fixed. " +
|
|
1103
1380
|
"They carry NO declined disposition — re-run remediation to re-evaluate them.",
|
|
1104
1381
|
});
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1382
|
+
// REMEDIATION-OWNED PATH. This used to write the canonical
|
|
1383
|
+
// `.audit-tools/audit-findings.json` + `audit-report.md` pair directly, with
|
|
1384
|
+
// no archive: the audit source that `defaultInputCandidates` resolves first
|
|
1385
|
+
// was silently replaced by a remediation-authored render, and the original
|
|
1386
|
+
// contract was unrecoverable. The canonical pair is
|
|
1387
|
+
// audit-artifact-promotion-lifecycle's; anything that must replace it goes
|
|
1388
|
+
// through its exported write-with-archive
|
|
1389
|
+
// (artifact:canonical-audit-deliverable-write-path), never a raw write here.
|
|
1390
|
+
const findingsPath = autonomousLeftoverFindingsPath(root);
|
|
1391
|
+
const reportPath = autonomousLeftoverReportPath(root);
|
|
1392
|
+
await mkdir(dirname(findingsPath), { recursive: true });
|
|
1110
1393
|
await Promise.all([
|
|
1111
|
-
writeJsonFile(
|
|
1112
|
-
writeTextFile(
|
|
1394
|
+
writeJsonFile(findingsPath, pair.findings_report),
|
|
1395
|
+
writeTextFile(reportPath, pair.report_markdown),
|
|
1113
1396
|
]);
|
|
1397
|
+
// NAMED IN THE DURABLE LOG. A leftover emit that left no event was invisible
|
|
1398
|
+
// after the fact: an operator could not tell an emit from a silent no-op.
|
|
1399
|
+
// The run log is opened here rather than threaded down: the two frames above
|
|
1400
|
+
// this one carry no logger, and widening both signatures to pass one through
|
|
1401
|
+
// would touch call paths this change has no other business in.
|
|
1402
|
+
new RunLogger(join(artifactsDir, "run.log.jsonl"), { enabled: true }).event({
|
|
1403
|
+
phase: "next-step",
|
|
1404
|
+
kind: "outcome",
|
|
1405
|
+
obligation: "autonomous_leftovers",
|
|
1406
|
+
note: `autonomous_leftover_deliverable findings=${String(leftovers.length)} ` +
|
|
1407
|
+
`path=${findingsPath}`,
|
|
1408
|
+
});
|
|
1114
1409
|
}
|
|
1115
1410
|
// ── Path-A filter dispositions (persisted for the coverage ledger) ──────────────
|
|
1116
1411
|
// The single filter pass runs at intake over the ORIGINAL findings; its
|
|
@@ -1133,14 +1428,14 @@ async function persistReviewFilterDispositions(artifactsDir, originals, filter)
|
|
|
1133
1428
|
};
|
|
1134
1429
|
await writeJsonFile(reviewFilterDispositionsPath(artifactsDir), payload);
|
|
1135
1430
|
}
|
|
1136
|
-
async function handleReadyIntakeContractPipeline(root, artifactsDir, options) {
|
|
1431
|
+
async function handleReadyIntakeContractPipeline(root, artifactsDir, options, runLogger) {
|
|
1137
1432
|
// Fast path: if an extracted-plan.json already exists (pipeline complete or
|
|
1138
1433
|
// promoted from a previous contract pipeline run), consume it directly without
|
|
1139
1434
|
// requiring intake artifacts. This handles both "plan promoted, ready to
|
|
1140
1435
|
// ground+plan" and the grounding tests that write extracted-plan.json directly.
|
|
1141
1436
|
const earlyExtractedPlan = await readExtractedPlanIfPresent(artifactsDir);
|
|
1142
1437
|
if (earlyExtractedPlan) {
|
|
1143
|
-
return handlePendingExtractedPlan(root, artifactsDir, { status: "pending" }, earlyExtractedPlan);
|
|
1438
|
+
return handlePendingExtractedPlan(root, artifactsDir, { status: "pending" }, earlyExtractedPlan, runLogger);
|
|
1144
1439
|
}
|
|
1145
1440
|
const intake = await readIntakeArtifacts(artifactsDir);
|
|
1146
1441
|
if (!intake.summary || !isIntakeReady(intake.summary)) {
|
|
@@ -1267,14 +1562,28 @@ async function handleReadyIntakeContractPipeline(root, artifactsDir, options) {
|
|
|
1267
1562
|
tier: "medium",
|
|
1268
1563
|
reason: `lean light review surfaced a concern: ${review.concerns.join("; ")}`,
|
|
1269
1564
|
}));
|
|
1565
|
+
runLogger.event({
|
|
1566
|
+
phase: "next-step",
|
|
1567
|
+
kind: "outcome",
|
|
1568
|
+
obligation: "lean_fast_path",
|
|
1569
|
+
note: `lean_fast_path_escalated concerns=${String(review.concerns.length)} ` +
|
|
1570
|
+
`detail=${review.concerns.join("; ")}`,
|
|
1571
|
+
});
|
|
1270
1572
|
process.stderr.write(`[remediate-code] Lean light review escalated (${review.concerns.join("; ")}); routing to the full contract pipeline.\n`);
|
|
1271
1573
|
}
|
|
1272
1574
|
else {
|
|
1273
1575
|
// Clear verdict → proceed with the lean plan.
|
|
1274
1576
|
const leanPlan = buildLeanExtractedPlan(gate.approved, randomRunId("LEAN"));
|
|
1275
1577
|
await writeJsonFile(intakePaths(artifactsDir).extractedPlan, leanPlan);
|
|
1578
|
+
runLogger.event({
|
|
1579
|
+
phase: "next-step",
|
|
1580
|
+
kind: "outcome",
|
|
1581
|
+
obligation: "lean_fast_path",
|
|
1582
|
+
note: `lean_fast_path_routed findings=${String(gate.approved.length)} ` +
|
|
1583
|
+
`rationale=${riskSignal.rationale.join("; ")}`,
|
|
1584
|
+
});
|
|
1276
1585
|
process.stderr.write(`[remediate-code] Lean fast path (risk tier low): ${riskSignal.rationale.join("; ")}; light review clear. Routing to plan→implement.\n`);
|
|
1277
|
-
const planned = await handlePendingExtractedPlan(root, artifactsDir, { status: "pending" }, leanPlan);
|
|
1586
|
+
const planned = await handlePendingExtractedPlan(root, artifactsDir, { status: "pending" }, leanPlan, runLogger);
|
|
1278
1587
|
if (planned) {
|
|
1279
1588
|
return planned;
|
|
1280
1589
|
}
|
|
@@ -1282,6 +1591,12 @@ async function handleReadyIntakeContractPipeline(root, artifactsDir, options) {
|
|
|
1282
1591
|
// normalize. If it somehow didn't, handlePendingExtractedPlan removed
|
|
1283
1592
|
// the file; fall through to the full pipeline (the safety net) rather
|
|
1284
1593
|
// than stalling the run.
|
|
1594
|
+
runLogger.event({
|
|
1595
|
+
phase: "next-step",
|
|
1596
|
+
kind: "outcome",
|
|
1597
|
+
obligation: "lean_fast_path",
|
|
1598
|
+
note: "lean_fast_path_fallback plan failed to materialize; routing to the contract pipeline",
|
|
1599
|
+
});
|
|
1285
1600
|
process.stderr.write("[remediate-code] Lean fast-path plan failed to materialize; falling back to the contract pipeline.\n");
|
|
1286
1601
|
}
|
|
1287
1602
|
}
|
|
@@ -1329,16 +1644,16 @@ async function handleReadyIntakeContractPipeline(root, artifactsDir, options) {
|
|
|
1329
1644
|
if (!extractedPlan) {
|
|
1330
1645
|
return null;
|
|
1331
1646
|
}
|
|
1332
|
-
return handlePendingExtractedPlan(root, artifactsDir, { status: "pending" }, extractedPlan);
|
|
1647
|
+
return handlePendingExtractedPlan(root, artifactsDir, { status: "pending" }, extractedPlan, runLogger);
|
|
1333
1648
|
}
|
|
1334
|
-
async function handlePendingIntake(root, artifactsDir, options) {
|
|
1649
|
+
async function handlePendingIntake(root, artifactsDir, options, runLogger) {
|
|
1335
1650
|
// Short-circuit: if an extracted-plan.json already exists (promoted from the
|
|
1336
1651
|
// contract pipeline), consume it directly without requiring intake artifacts.
|
|
1337
1652
|
// This allows decideNextStep to resume a plan-grounding pass even when the
|
|
1338
1653
|
// full intake artifact set is no longer present.
|
|
1339
1654
|
const earlyExtractedPlan = await readExtractedPlanIfPresent(artifactsDir);
|
|
1340
1655
|
if (earlyExtractedPlan) {
|
|
1341
|
-
return handleReadyIntakeContractPipeline(root, artifactsDir, options);
|
|
1656
|
+
return handleReadyIntakeContractPipeline(root, artifactsDir, options, runLogger);
|
|
1342
1657
|
}
|
|
1343
1658
|
const inputResolution = resolveInputPaths(root, options.input);
|
|
1344
1659
|
const intakeResult = await resolveIntakeStep({
|
|
@@ -1356,7 +1671,7 @@ async function handlePendingIntake(root, artifactsDir, options) {
|
|
|
1356
1671
|
return intakeResult.step;
|
|
1357
1672
|
}
|
|
1358
1673
|
// Intake is complete — route both paths through the contract pipeline.
|
|
1359
|
-
return handleReadyIntakeContractPipeline(root, artifactsDir, options);
|
|
1674
|
+
return handleReadyIntakeContractPipeline(root, artifactsDir, options, runLogger);
|
|
1360
1675
|
}
|
|
1361
1676
|
async function handleNoState(root, artifactsDir) {
|
|
1362
1677
|
const paths = intakePaths(artifactsDir);
|
|
@@ -1894,14 +2209,151 @@ function hasResolvedItems(state) {
|
|
|
1894
2209
|
return Object.values(state.items ?? {}).some((it) => isVerifiedCompleteStatus(it.status));
|
|
1895
2210
|
}
|
|
1896
2211
|
/**
|
|
1897
|
-
*
|
|
1898
|
-
* `skipFinalGate` (test hermeticity) or `REMEDIATE_SKIP_FINAL_GATE
|
|
1899
|
-
* so the all-terminal gate and the per-phase boundary
|
|
2212
|
+
* WHICH suppression disabled the tool-owned final gate (INV-RS-10) for this run —
|
|
2213
|
+
* `skipFinalGate` (test hermeticity) or `REMEDIATE_SKIP_FINAL_GATE` — or null when
|
|
2214
|
+
* it is live. Single-sourced so the all-terminal gate and the per-phase boundary
|
|
2215
|
+
* gate agree, and so both RECORD the same reason.
|
|
2216
|
+
*
|
|
2217
|
+
* The reason is carried into the outcome record rather than collapsed to a
|
|
2218
|
+
* boolean: the environment skip is the quietest not-run in the system (it needs
|
|
2219
|
+
* no option, no argument and no code change to fire), so a record that says only
|
|
2220
|
+
* "disabled" would leave an operator unable to tell a deliberate test-hermeticity
|
|
2221
|
+
* run from a stray exported variable.
|
|
1900
2222
|
*/
|
|
1901
|
-
function
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2223
|
+
function finalGateDisabledReason(options) {
|
|
2224
|
+
if (options.skipFinalGate === true)
|
|
2225
|
+
return "skipFinalGate option";
|
|
2226
|
+
if (process.env.REMEDIATE_SKIP_FINAL_GATE === "1" ||
|
|
2227
|
+
process.env.REMEDIATE_SKIP_FINAL_GATE === "true") {
|
|
2228
|
+
return "REMEDIATE_SKIP_FINAL_GATE environment variable";
|
|
2229
|
+
}
|
|
2230
|
+
return null;
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* The ONE way a gate evaluation is recorded, shared by BOTH gate families (the
|
|
2234
|
+
* phase-boundary gate and the all-terminal funnel), so neither can grow a second
|
|
2235
|
+
* executed/scoped-out/disabled vocabulary of its own.
|
|
2236
|
+
*
|
|
2237
|
+
* Writes the durable {@link writeFinalGateOutcomeRecord} artifact AND the run-log
|
|
2238
|
+
* event from the same values, so the two can never disagree about which of the
|
|
2239
|
+
* three happened.
|
|
2240
|
+
*
|
|
2241
|
+
* This is the affirmation the gate lacked. The floor's control flow was already
|
|
2242
|
+
* right — a scoped-out gate is deliberately NON-BLOCKING, a declared scope rather
|
|
2243
|
+
* than a vacuous pass — but the only thing either consumer wrote was
|
|
2244
|
+
* `passed=<bool>`, which is `true` for an executed green floor, for a scoped-out
|
|
2245
|
+
* target that ran nothing, and for a suppressed gate that was never reached. All
|
|
2246
|
+
* three produced byte-identical records, so "the suite passed" and "no suite ran"
|
|
2247
|
+
* were indistinguishable after the fact.
|
|
2248
|
+
*/
|
|
2249
|
+
async function recordFinalGateOutcome(ctx) {
|
|
2250
|
+
const ran = ctx.outcome === "executed";
|
|
2251
|
+
await writeFinalGateOutcomeRecord(ctx.artifactsDir, {
|
|
2252
|
+
scope: ctx.scope,
|
|
2253
|
+
outcome: ctx.outcome,
|
|
2254
|
+
passed: ctx.passed,
|
|
2255
|
+
commands_run: ctx.commandsRun,
|
|
2256
|
+
...(ctx.reason === undefined ? {} : { reason: ctx.reason }),
|
|
2257
|
+
});
|
|
2258
|
+
ctx.runLogger.event({
|
|
2259
|
+
phase: "next-step",
|
|
2260
|
+
kind: "executor_end",
|
|
2261
|
+
obligation: ctx.state.status,
|
|
2262
|
+
note: `${ctx.gateKey} outcome=${ctx.outcome} ` +
|
|
2263
|
+
// "n/a", never "true": a gate that ran nothing has no verdict, and the
|
|
2264
|
+
// durable record it is written beside carries `passed: null` for the
|
|
2265
|
+
// same reason.
|
|
2266
|
+
`passed=${ran ? String(ctx.passed) : "n/a"} ` +
|
|
2267
|
+
`commands=${ran ? String(ctx.commandsRun) : "0"}` +
|
|
2268
|
+
(ctx.reason === undefined ? "" : ` reason=${ctx.reason}`),
|
|
2269
|
+
...(ctx.durationMs === undefined ? {} : { duration_ms: ctx.durationMs }),
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
/**
|
|
2273
|
+
* The ONE response to a red tool-owned gate, shared by both gates that run it.
|
|
2274
|
+
*
|
|
2275
|
+
* Records the failing command beneath the artifacts dir and emits a resumable
|
|
2276
|
+
* `final_gate_red` step. It MUTATES NOTHING — no item status, no `state.status`,
|
|
2277
|
+
* no persisted state write at all — because a whole-repo red is unattributable:
|
|
2278
|
+
* nothing in the gate computes which item or path caused it, so every response
|
|
2279
|
+
* that touches items is guessing. The predecessor guessed by re-opening all of
|
|
2280
|
+
* them, and on 2026-08-20 that erased 21 accepted resolutions over a red from an
|
|
2281
|
+
* unrelated landed commit.
|
|
2282
|
+
*
|
|
2283
|
+
* Resumable BY CONSTRUCTION rather than by stored progress: the next next-step
|
|
2284
|
+
* re-runs the gate, and a green one proceeds exactly as if the red never
|
|
2285
|
+
* happened. There is nothing to reset and no counter that can strand the run.
|
|
2286
|
+
*
|
|
2287
|
+
* The prompt carries the failing command line and the PATH to the record — never
|
|
2288
|
+
* the captured output, which stays in the artifact where a multi-KB suite log
|
|
2289
|
+
* costs nothing.
|
|
2290
|
+
*
|
|
2291
|
+
* COST OF THE PAUSE, stated in the prompt rather than discovered: there is no
|
|
2292
|
+
* cached verdict, so EVERY next-step taken while the suite is red re-runs the
|
|
2293
|
+
* whole gate — a full build plus the whole suite, minutes, holding the phase
|
|
2294
|
+
* lock throughout. That is the deliberate price of having no counter to strand
|
|
2295
|
+
* the run on, and it makes polling expensive: the host should re-run once it has
|
|
2296
|
+
* actually fixed something, not on a timer.
|
|
2297
|
+
*/
|
|
2298
|
+
async function emitFinalGateRedStep(ctx) {
|
|
2299
|
+
const { root, artifactsDir, state, scope, gate, runLogger } = ctx;
|
|
2300
|
+
const failed = gate.results.find((r) => !r.passed);
|
|
2301
|
+
const recordPath = await writeFinalGateRedRecord(artifactsDir, scope, failed);
|
|
2302
|
+
const failingCommand = failed
|
|
2303
|
+
? `${failed.argv.join(" ")} (exit ${String(failed.exit_code)})`
|
|
2304
|
+
: "(the gate reported no failing command)";
|
|
2305
|
+
runLogger.event({
|
|
2306
|
+
phase: "next-step",
|
|
2307
|
+
kind: "outcome",
|
|
2308
|
+
obligation: state.status,
|
|
2309
|
+
note: `final_gate_red scope=${scope} command=${failed ? failed.argv.join(" ") : "unknown"}`,
|
|
2310
|
+
});
|
|
2311
|
+
const nextCommand = loaderCommand("next-step");
|
|
2312
|
+
return {
|
|
2313
|
+
kind: "emit",
|
|
2314
|
+
step: await writeCurrentStep({
|
|
2315
|
+
stepKind: "final_gate_red",
|
|
2316
|
+
status: "blocked",
|
|
2317
|
+
runId: stateRunId(state),
|
|
2318
|
+
repoRoot: root,
|
|
2319
|
+
artifactsDir,
|
|
2320
|
+
prompt: `
|
|
2321
|
+
# Remediation paused — the repository suite is red
|
|
2322
|
+
|
|
2323
|
+
The tool-owned gate (${scope}) ran the repository's own build/typecheck/test
|
|
2324
|
+
floor and it FAILED. Nothing about this run has been changed: every item keeps
|
|
2325
|
+
the status it had, the run stays in the same phase, and no work was discarded.
|
|
2326
|
+
|
|
2327
|
+
Failing command:
|
|
2328
|
+
|
|
2329
|
+
\`${failingCommand}\`
|
|
2330
|
+
|
|
2331
|
+
The captured output tail is recorded at:
|
|
2332
|
+
|
|
2333
|
+
\`${recordPath}\`
|
|
2334
|
+
|
|
2335
|
+
A red here is whole-repo and says nothing about which remediation item caused
|
|
2336
|
+
it — it may not be this run's doing at all (a commit landed alongside the run is
|
|
2337
|
+
enough). So this is a PAUSE, not a verdict on the work.
|
|
2338
|
+
|
|
2339
|
+
Fix the failing command — or confirm it was already broken independently of this
|
|
2340
|
+
run — then run:
|
|
2341
|
+
|
|
2342
|
+
\`${nextCommand}\`
|
|
2343
|
+
|
|
2344
|
+
The gate re-runs from scratch. The moment it is green the run continues exactly
|
|
2345
|
+
where it left off.
|
|
2346
|
+
|
|
2347
|
+
Re-run it DELIBERATELY, not on a timer: there is no cached verdict, so every
|
|
2348
|
+
next-step taken while the suite is red re-runs the entire gate — a full build
|
|
2349
|
+
plus the whole suite, minutes, holding the run's phase lock the whole time.
|
|
2350
|
+
Fix something first, then re-run.
|
|
2351
|
+
`,
|
|
2352
|
+
allowedCommands: [nextCommand],
|
|
2353
|
+
stopCondition: "Stop. Make the repository suite green, then re-run next-step to resume the run.",
|
|
2354
|
+
artifactPaths: { final_gate_record: recordPath },
|
|
2355
|
+
}),
|
|
2356
|
+
};
|
|
1905
2357
|
}
|
|
1906
2358
|
/**
|
|
1907
2359
|
* Whole-repo test-suite gate at a foundations→consumers PHASE BOUNDARY (T3). Runs
|
|
@@ -1909,24 +2361,42 @@ function finalGateDisabled(options) {
|
|
|
1909
2361
|
* so an integration break introduced by a just-completed foundations phase is
|
|
1910
2362
|
* caught — and attributed to that phase — before consumers are built on top of it
|
|
1911
2363
|
* (strictly earlier + more attributable than the all-terminal gate, whose red is
|
|
1912
|
-
* unattributable across every phase).
|
|
1913
|
-
*
|
|
1914
|
-
*
|
|
2364
|
+
* unattributable across every phase).
|
|
2365
|
+
*
|
|
2366
|
+
* A red RECORDS and PAUSES — see {@link emitFinalGateRedStep}. It mutates no item,
|
|
2367
|
+
* moves no phase, and writes no state. (It used to re-open every item and, at a
|
|
2368
|
+
* bound, abandon the run; that backstop is gone, along with the counter sidecar
|
|
2369
|
+
* that drove it.)
|
|
1915
2370
|
*
|
|
1916
|
-
* Returns
|
|
1917
|
-
*
|
|
1918
|
-
*
|
|
2371
|
+
* Returns the pause step when the gate is RED, or null when no gate is due this
|
|
2372
|
+
* pass OR the gate is GREEN — in which case the caller proceeds to dispatch the
|
|
2373
|
+
* phase.
|
|
1919
2374
|
*/
|
|
1920
2375
|
async function runPhaseBoundaryGate(ctx) {
|
|
1921
|
-
const { root, artifactsDir, state, options,
|
|
1922
|
-
|
|
1923
|
-
|
|
2376
|
+
const { root, artifactsDir, state, options, runLogger } = ctx;
|
|
2377
|
+
// Whether a gate is DUE is decided BEFORE whether it is suppressed. The old
|
|
2378
|
+
// order asked the suppression first and returned, so a disabled run could not
|
|
2379
|
+
// tell "no gate was due this pass" from "a gate was due and skipped" — and the
|
|
2380
|
+
// second is the one worth recording.
|
|
1924
2381
|
const phase = phaseBoundaryToGate(state);
|
|
1925
2382
|
if (phase == null)
|
|
1926
2383
|
return null;
|
|
1927
|
-
const
|
|
1928
|
-
|
|
1929
|
-
|
|
2384
|
+
const scope = `phase ${phase} boundary`;
|
|
2385
|
+
const disabledReason = finalGateDisabledReason(options);
|
|
2386
|
+
if (disabledReason !== null) {
|
|
2387
|
+
await recordFinalGateOutcome({
|
|
2388
|
+
artifactsDir,
|
|
2389
|
+
state,
|
|
2390
|
+
scope,
|
|
2391
|
+
gateKey: `phase_boundary_gate phase=${phase}`,
|
|
2392
|
+
runLogger,
|
|
2393
|
+
outcome: "disabled",
|
|
2394
|
+
passed: false,
|
|
2395
|
+
commandsRun: 0,
|
|
2396
|
+
reason: disabledReason,
|
|
2397
|
+
});
|
|
2398
|
+
return null;
|
|
2399
|
+
}
|
|
1930
2400
|
const gateStart = Date.now();
|
|
1931
2401
|
runLogger.event({
|
|
1932
2402
|
phase: "next-step",
|
|
@@ -1935,46 +2405,47 @@ async function runPhaseBoundaryGate(ctx) {
|
|
|
1935
2405
|
note: `phase_boundary_gate phase=${phase}`,
|
|
1936
2406
|
});
|
|
1937
2407
|
const gate = await runToolOwnedFinalGate(root, { runner: options.finalGateRunner });
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2408
|
+
await recordFinalGateOutcome({
|
|
2409
|
+
artifactsDir,
|
|
2410
|
+
state,
|
|
2411
|
+
scope,
|
|
2412
|
+
gateKey: `phase_boundary_gate phase=${phase}`,
|
|
2413
|
+
runLogger,
|
|
2414
|
+
outcome: gate.outcome,
|
|
2415
|
+
passed: gate.passed,
|
|
2416
|
+
commandsRun: gate.results.length,
|
|
2417
|
+
...(gate.outcome === "scoped_out"
|
|
2418
|
+
? { reason: "target is not the audit-tools monorepo" }
|
|
2419
|
+
: {}),
|
|
2420
|
+
durationMs: Date.now() - gateStart,
|
|
1944
2421
|
});
|
|
1945
2422
|
if (gate.passed)
|
|
1946
|
-
return null; // green → dispatch
|
|
1947
|
-
// RED at the boundary
|
|
1948
|
-
//
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
runLogger.event({
|
|
1957
|
-
phase: "next-step",
|
|
1958
|
-
kind: "outcome",
|
|
1959
|
-
obligation: state.status,
|
|
1960
|
-
note: `phase_boundary_coarse_reblock phase=${phase} action=${decision.action} count=${decision.next_count}`,
|
|
2423
|
+
return null; // green (or declared-out-of-scope) → dispatch
|
|
2424
|
+
// RED at the boundary. The next phase does NOT dispatch — but nothing is
|
|
2425
|
+
// re-opened or closed either; the run pauses exactly where it stands.
|
|
2426
|
+
return emitFinalGateRedStep({
|
|
2427
|
+
root,
|
|
2428
|
+
artifactsDir,
|
|
2429
|
+
state,
|
|
2430
|
+
scope,
|
|
2431
|
+
gate,
|
|
2432
|
+
runLogger,
|
|
1961
2433
|
});
|
|
1962
|
-
decision.state.status =
|
|
1963
|
-
decision.action === "reattempt_all" ? "implementing" : "closing";
|
|
1964
|
-
await store.saveState(decision.state);
|
|
1965
|
-
return { kind: "transition", state: decision.state };
|
|
1966
2434
|
}
|
|
1967
2435
|
async function handleAllTerminalTransition(root, artifactsDir, state, store, options, runLogger) {
|
|
1968
|
-
const
|
|
1969
|
-
const
|
|
2436
|
+
const disabledReason = finalGateDisabledReason(options);
|
|
2437
|
+
const gateDisabled = disabledReason !== null;
|
|
2438
|
+
const scope = "all-terminal final gate";
|
|
1970
2439
|
// The tool-owned final gate (INV-RS-10) runs at the single all-terminal →
|
|
1971
|
-
// closing funnel,
|
|
2440
|
+
// closing funnel, on EVERY arrival here. It is skipped only when:
|
|
1972
2441
|
// - there is nothing resolved to validate (everything blocked/skipped), or
|
|
1973
|
-
// - the bounded backstop already terminated (CE-003 — never re-run after), or
|
|
1974
2442
|
// - it is explicitly disabled for test hermeticity.
|
|
1975
|
-
//
|
|
1976
|
-
//
|
|
1977
|
-
|
|
2443
|
+
// There is deliberately no third "already gave up" skip: the flag that used to
|
|
2444
|
+
// provide one made a run that hit the old backstop's bound skip the suite check
|
|
2445
|
+
// permanently, so the gate it exists to enforce stopped running exactly when it
|
|
2446
|
+
// mattered most. The gate is INDEPENDENT of plan.test_command and runs through
|
|
2447
|
+
// the env-scrubbing runTracked path.
|
|
2448
|
+
if (!gateDisabled && hasResolvedItems(state)) {
|
|
1978
2449
|
const gateStart = Date.now();
|
|
1979
2450
|
runLogger.event({
|
|
1980
2451
|
phase: "next-step",
|
|
@@ -1983,37 +2454,53 @@ async function handleAllTerminalTransition(root, artifactsDir, state, store, opt
|
|
|
1983
2454
|
note: "tool_owned_final_gate",
|
|
1984
2455
|
});
|
|
1985
2456
|
const gate = await runToolOwnedFinalGate(root, { runner: options.finalGateRunner });
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2457
|
+
await recordFinalGateOutcome({
|
|
2458
|
+
artifactsDir,
|
|
2459
|
+
state,
|
|
2460
|
+
scope,
|
|
2461
|
+
gateKey: "tool_owned_final_gate",
|
|
2462
|
+
runLogger,
|
|
2463
|
+
outcome: gate.outcome,
|
|
2464
|
+
passed: gate.passed,
|
|
2465
|
+
commandsRun: gate.results.length,
|
|
2466
|
+
...(gate.outcome === "scoped_out"
|
|
2467
|
+
? { reason: "target is not the audit-tools monorepo" }
|
|
2468
|
+
: {}),
|
|
2469
|
+
durationMs: Date.now() - gateStart,
|
|
1992
2470
|
});
|
|
1993
2471
|
if (!gate.passed) {
|
|
1994
|
-
//
|
|
1995
|
-
//
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
obligation: state.status,
|
|
2006
|
-
note: `coarse_reblock action=${decision.action} count=${decision.next_count}`,
|
|
2472
|
+
// A whole-repo red at the closing funnel is exactly as unattributable as
|
|
2473
|
+
// one at a phase boundary, so it gets the same answer: record and pause.
|
|
2474
|
+
// The run does NOT advance to `closing` — closing on a red would write a
|
|
2475
|
+
// report claiming an outcome the suite never corroborated.
|
|
2476
|
+
return emitFinalGateRedStep({
|
|
2477
|
+
root,
|
|
2478
|
+
artifactsDir,
|
|
2479
|
+
state,
|
|
2480
|
+
scope,
|
|
2481
|
+
gate,
|
|
2482
|
+
runLogger,
|
|
2007
2483
|
});
|
|
2008
|
-
// reattempt_all → re-open items to pending and re-emit the host workload
|
|
2009
|
-
// (NEVER the human triage prompt — CE-003 no-human-host path); terminal_blocked
|
|
2010
|
-
// → everything non-skip is now blocked, so close writes the partial report.
|
|
2011
|
-
decision.state.status =
|
|
2012
|
-
decision.action === "reattempt_all" ? "implementing" : "closing";
|
|
2013
|
-
await store.saveState(decision.state);
|
|
2014
|
-
return { kind: "transition", state: decision.state };
|
|
2015
2484
|
}
|
|
2016
2485
|
}
|
|
2486
|
+
else {
|
|
2487
|
+
// The gate was DUE at the closing funnel and did not run. Recorded, with
|
|
2488
|
+
// WHICH suppression did it — the run is about to transition to `closing`
|
|
2489
|
+
// and write a completion report, and without this the report would be
|
|
2490
|
+
// byte-identical to one produced after a green floor.
|
|
2491
|
+
await recordFinalGateOutcome({
|
|
2492
|
+
artifactsDir,
|
|
2493
|
+
state,
|
|
2494
|
+
scope,
|
|
2495
|
+
gateKey: "tool_owned_final_gate",
|
|
2496
|
+
runLogger,
|
|
2497
|
+
outcome: "disabled",
|
|
2498
|
+
passed: false,
|
|
2499
|
+
commandsRun: 0,
|
|
2500
|
+
reason: disabledReason ??
|
|
2501
|
+
"no verified-complete items to validate (nothing resolved)",
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2017
2504
|
state.status = "closing";
|
|
2018
2505
|
await store.saveState(state);
|
|
2019
2506
|
return { kind: "transition", state };
|
|
@@ -2368,7 +2855,11 @@ export const INTENT_INTERPRETATION_SCHEMA_VERSION = "remediate-code-intent-inter
|
|
|
2368
2855
|
* throws into the decide loop. The raw `free_form_intent` string is NOT returned
|
|
2369
2856
|
* or threaded anywhere — only the structured `InterpretedIntent` is (INV-S04).
|
|
2370
2857
|
*/
|
|
2371
|
-
export async function interpretConfirmedCheckpointIntent(artifactsDir, checkpoint
|
|
2858
|
+
export async function interpretConfirmedCheckpointIntent(artifactsDir, checkpoint,
|
|
2859
|
+
// Optional so the exported helper stays callable standalone; the decide loop
|
|
2860
|
+
// always supplies it, because an unencodable clause is an operator-visible
|
|
2861
|
+
// loss of intent and belongs in the durable log, not only on stderr.
|
|
2862
|
+
runLogger) {
|
|
2372
2863
|
if (!checkpoint || checkpoint.confirmed_by !== "host")
|
|
2373
2864
|
return null;
|
|
2374
2865
|
const raw = checkpoint.free_form_intent;
|
|
@@ -2388,6 +2879,13 @@ export async function interpretConfirmedCheckpointIntent(artifactsDir, checkpoin
|
|
|
2388
2879
|
// Best-effort sidecar: a write failure must never block the decide loop.
|
|
2389
2880
|
}
|
|
2390
2881
|
if (interpreted.unencodableClauses.length > 0) {
|
|
2882
|
+
runLogger?.event({
|
|
2883
|
+
phase: "next-step",
|
|
2884
|
+
kind: "outcome",
|
|
2885
|
+
obligation: "interpret_intent",
|
|
2886
|
+
note: `intent_unencodable_clauses count=${String(interpreted.unencodableClauses.length)} ` +
|
|
2887
|
+
`clauses=${interpreted.unencodableClauses.join("; ")}`,
|
|
2888
|
+
});
|
|
2391
2889
|
process.stderr.write(`[remediate-code] free_form_intent: ${interpreted.unencodableClauses.length} ` +
|
|
2392
2890
|
`clause(s) could not be encoded as lens/priority/scope signals and are ` +
|
|
2393
2891
|
`surfaced for promotion to constraints: ` +
|
|
@@ -2532,8 +3030,8 @@ function buildPreIntakeObligations(ctx, snapshot) {
|
|
|
2532
3030
|
!existsSync(interpretationPath)
|
|
2533
3031
|
? "missing"
|
|
2534
3032
|
: "satisfied",
|
|
2535
|
-
execute: async (state) => {
|
|
2536
|
-
await interpretConfirmedCheckpointIntent(artifactsDir, existingCheckpoint);
|
|
3033
|
+
execute: async (state, c) => {
|
|
3034
|
+
await interpretConfirmedCheckpointIntent(artifactsDir, existingCheckpoint, c.runLogger);
|
|
2537
3035
|
return { kind: "transition", state };
|
|
2538
3036
|
},
|
|
2539
3037
|
},
|
|
@@ -2587,7 +3085,7 @@ function buildPreIntakeObligations(ctx, snapshot) {
|
|
|
2587
3085
|
id: "pending_intake",
|
|
2588
3086
|
derive: (state) => (state == null ? "missing" : "satisfied"),
|
|
2589
3087
|
execute: async (_state, c) => {
|
|
2590
|
-
const outcome = await handlePendingIntake(c.root, c.artifactsDir, c.options);
|
|
3088
|
+
const outcome = await handlePendingIntake(c.root, c.artifactsDir, c.options, c.runLogger);
|
|
2591
3089
|
if (outcome && "step_kind" in outcome) {
|
|
2592
3090
|
return { kind: "emit", step: outcome };
|
|
2593
3091
|
}
|
|
@@ -2714,14 +3212,14 @@ function buildMainObligations(ctx) {
|
|
|
2714
3212
|
const pendingBlocks = implementableBlocks(s);
|
|
2715
3213
|
if (pendingBlocks.length > 0) {
|
|
2716
3214
|
// Per-phase boundary gate (T3): before opening a phase P > 0, run the
|
|
2717
|
-
// whole-repo suite once over the just-landed foundations. A red
|
|
2718
|
-
// here (
|
|
3215
|
+
// whole-repo suite once over the just-landed foundations. A red PAUSES
|
|
3216
|
+
// here (an emitted step, no state written); green / no-boundary falls
|
|
3217
|
+
// through to dispatch.
|
|
2719
3218
|
const gated = await runPhaseBoundaryGate({
|
|
2720
3219
|
root,
|
|
2721
3220
|
artifactsDir,
|
|
2722
3221
|
state: s,
|
|
2723
3222
|
options,
|
|
2724
|
-
store,
|
|
2725
3223
|
runLogger,
|
|
2726
3224
|
});
|
|
2727
3225
|
if (gated)
|
|
@@ -2732,6 +3230,7 @@ function buildMainObligations(ctx) {
|
|
|
2732
3230
|
state: s,
|
|
2733
3231
|
options,
|
|
2734
3232
|
store,
|
|
3233
|
+
runLogger,
|
|
2735
3234
|
});
|
|
2736
3235
|
}
|
|
2737
3236
|
// Dead-end pending nodes whose dependency never reached verified-complete
|
|
@@ -2810,17 +3309,56 @@ function buildMainObligations(ctx) {
|
|
|
2810
3309
|
},
|
|
2811
3310
|
];
|
|
2812
3311
|
}
|
|
3312
|
+
/**
|
|
3313
|
+
* ONE mutex for the WHOLE advance.
|
|
3314
|
+
*
|
|
3315
|
+
* The pre-intake segment used to run OUTSIDE the lock, with only the main
|
|
3316
|
+
* advance inside it — and the pre-intake segment is where the review-approval
|
|
3317
|
+
* gate and the autonomous leftover emit live, so two concurrent next-step calls
|
|
3318
|
+
* could both take the autonomous branch. Serializing only the second half made
|
|
3319
|
+
* the mutex a statement about which code was easy to wrap, not about which work
|
|
3320
|
+
* is serial: the entire state-machine advance is serial, so the entire advance
|
|
3321
|
+
* is guarded.
|
|
3322
|
+
*
|
|
3323
|
+
* The state is loaded once here (outside) purely to name the run in the
|
|
3324
|
+
* `phase_busy` step; the advance re-loads under the lock, so a peer that
|
|
3325
|
+
* persisted between the two is never clobbered.
|
|
3326
|
+
*/
|
|
2813
3327
|
async function decideNextStepLoop(options, runLogger) {
|
|
2814
3328
|
const root = resolveRoot(options.root);
|
|
2815
3329
|
const artifactsDir = resolveArtifactsDir(root, options.artifactsDir);
|
|
2816
3330
|
await mkdir(artifactsDir, { recursive: true });
|
|
2817
3331
|
const store = new StateStore(artifactsDir);
|
|
2818
|
-
|
|
3332
|
+
const entryState = await store.loadState();
|
|
2819
3333
|
runLogger.event({
|
|
2820
3334
|
phase: "next-step",
|
|
2821
3335
|
kind: "state",
|
|
2822
|
-
obligation:
|
|
3336
|
+
obligation: entryState?.status ?? "pending",
|
|
2823
3337
|
});
|
|
3338
|
+
try {
|
|
3339
|
+
return await withFileLock(join(artifactsDir, "phase.lock"), () => advanceUnderPhaseLock({ root, artifactsDir, store, options, runLogger }), PHASE_LOCK_TIMEOUT_MS);
|
|
3340
|
+
}
|
|
3341
|
+
catch (error) {
|
|
3342
|
+
if (error instanceof FileLockTimeoutError) {
|
|
3343
|
+
return buildPhaseBusyStep({
|
|
3344
|
+
root,
|
|
3345
|
+
artifactsDir,
|
|
3346
|
+
runId: stateRunId(entryState),
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
throw error;
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
/**
|
|
3353
|
+
* The serial state-machine advance itself — pre-intake gates, then the main
|
|
3354
|
+
* obligation fold. Runs with `<artifactsDir>/phase.lock` HELD for its whole
|
|
3355
|
+
* duration; never call it without that lock.
|
|
3356
|
+
*/
|
|
3357
|
+
async function advanceUnderPhaseLock(deps) {
|
|
3358
|
+
const { root, artifactsDir, store, options, runLogger } = deps;
|
|
3359
|
+
// Loaded FRESH under the mutex: a peer may have advanced and persisted state
|
|
3360
|
+
// between the entry read and this process winning the lock.
|
|
3361
|
+
let state = await store.loadState();
|
|
2824
3362
|
// step_count is incremented once per host invocation. The `counted` flag guards
|
|
2825
3363
|
// the shared `countStep` closure so the forceReplan preamble, the pre-intake
|
|
2826
3364
|
// obligation executors, and the post-intake count point can never double-count
|
|
@@ -2844,7 +3382,7 @@ async function decideNextStepLoop(options, runLogger) {
|
|
|
2844
3382
|
// transitions, never a recursive decideNextStepLoop), so this fires at most once.
|
|
2845
3383
|
if (options.forceReplan && state != null) {
|
|
2846
3384
|
await countStep(state);
|
|
2847
|
-
state = await forceReplanFromExistingIntake(root, artifactsDir, state, store);
|
|
3385
|
+
state = await forceReplanFromExistingIntake(root, artifactsDir, state, store, runLogger);
|
|
2848
3386
|
}
|
|
2849
3387
|
// Pre-read the once-async signals the pre-intake derive()s consume
|
|
2850
3388
|
// synchronously (no transition inside this advance call rewrites either file).
|
|
@@ -2890,32 +3428,15 @@ async function decideNextStepLoop(options, runLogger) {
|
|
|
2890
3428
|
return handleNoState(root, artifactsDir);
|
|
2891
3429
|
}
|
|
2892
3430
|
await countStep(state);
|
|
2893
|
-
//
|
|
2894
|
-
//
|
|
2895
|
-
//
|
|
2896
|
-
|
|
2897
|
-
const
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
return {
|
|
2904
|
-
advanceState,
|
|
2905
|
-
outcome: await advance({ priority: MAIN_PRIORITY, obligations: buildMainObligations(ctx) }, advanceState, ctx),
|
|
2906
|
-
};
|
|
2907
|
-
}, PHASE_LOCK_TIMEOUT_MS);
|
|
2908
|
-
if (main.outcome.step)
|
|
2909
|
-
return main.outcome.step;
|
|
2910
|
-
// The unhandled catch-all always emits on a non-null state, so a null step
|
|
2911
|
-
// here is unreachable; keep an explicit fallback rather than a non-null assert.
|
|
2912
|
-
return handleUnhandledState(root, artifactsDir, main.advanceState);
|
|
2913
|
-
}
|
|
2914
|
-
catch (error) {
|
|
2915
|
-
if (error instanceof FileLockTimeoutError) {
|
|
2916
|
-
return buildPhaseBusyStep({ root, artifactsDir, runId: phaseRunId });
|
|
2917
|
-
}
|
|
2918
|
-
throw error;
|
|
2919
|
-
}
|
|
3431
|
+
// Re-read between the two folds: a pre-intake executor may persist through the
|
|
3432
|
+
// store without returning the persisted value, so the main fold reads from disk
|
|
3433
|
+
// exactly as it did when it owned its own lock acquisition.
|
|
3434
|
+
const advanceState = (await store.loadState()) ?? state;
|
|
3435
|
+
const main = await advance({ priority: MAIN_PRIORITY, obligations: buildMainObligations(ctx) }, advanceState, ctx);
|
|
3436
|
+
if (main.step)
|
|
3437
|
+
return main.step;
|
|
3438
|
+
// The unhandled catch-all always emits on a non-null state, so a null step
|
|
3439
|
+
// here is unreachable; keep an explicit fallback rather than a non-null assert.
|
|
3440
|
+
return handleUnhandledState(root, artifactsDir, advanceState);
|
|
2920
3441
|
}
|
|
2921
3442
|
//# sourceMappingURL=nextStep.js.map
|