audit-tools 0.52.5 → 0.54.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/dist/audit/cli/auditStep.d.ts +1 -1
- package/dist/audit/cli/conceptualDispatch.d.ts +2 -2
- package/dist/audit/cli/conceptualDispatch.d.ts.map +1 -1
- package/dist/audit/cli/conceptualDispatch.js +6 -5
- package/dist/audit/cli/conceptualDispatch.js.map +1 -1
- package/dist/audit/cli/confirmIntentStep.d.ts.map +1 -1
- package/dist/audit/cli/confirmIntentStep.js +3 -16
- package/dist/audit/cli/confirmIntentStep.js.map +1 -1
- package/dist/audit/cli/dispatch/hostHandoff.d.ts +2 -0
- package/dist/audit/cli/dispatch/hostHandoff.d.ts.map +1 -1
- package/dist/audit/cli/dispatch/hostHandoff.js +22 -1
- package/dist/audit/cli/dispatch/hostHandoff.js.map +1 -1
- package/dist/audit/cli/dispatch/packetFilter.d.ts +1 -1
- package/dist/audit/cli/dispatch/packetFilter.d.ts.map +1 -1
- package/dist/audit/cli/dispatch/packetFilter.js +1 -1
- package/dist/audit/cli/dispatch/packetFilter.js.map +1 -1
- package/dist/audit/cli/functionalPreflight.d.ts.map +1 -1
- package/dist/audit/cli/functionalPreflight.js +6 -3
- package/dist/audit/cli/functionalPreflight.js.map +1 -1
- package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
- package/dist/audit/cli/nextStepCommand.js +57 -34
- package/dist/audit/cli/nextStepCommand.js.map +1 -1
- package/dist/audit/cli/nextStepHelpers.d.ts +3 -3
- package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
- package/dist/audit/cli/nextStepHelpers.js +52 -77
- package/dist/audit/cli/nextStepHelpers.js.map +1 -1
- package/dist/audit/cli/prompts.d.ts +1 -2
- package/dist/audit/cli/prompts.d.ts.map +1 -1
- package/dist/audit/cli/prompts.js +2 -12
- package/dist/audit/cli/prompts.js.map +1 -1
- package/dist/audit/cli/sampleRunCommand.d.ts +2 -2
- package/dist/audit/cli/semanticReviewStep.d.ts +11 -2
- package/dist/audit/cli/semanticReviewStep.d.ts.map +1 -1
- package/dist/audit/cli/semanticReviewStep.js +14 -10
- package/dist/audit/cli/semanticReviewStep.js.map +1 -1
- package/dist/audit/contracts/workerSchemas.d.ts +14 -14
- package/dist/audit/contracts/wrapperResponse.d.ts +6 -6
- package/dist/audit/extractors/graphRoutes.d.ts.map +1 -1
- package/dist/audit/extractors/graphRoutes.js +1 -3
- package/dist/audit/extractors/graphRoutes.js.map +1 -1
- package/dist/audit/io/artifacts.d.ts.map +1 -1
- package/dist/audit/io/artifacts.js +2 -3
- package/dist/audit/io/artifacts.js.map +1 -1
- package/dist/audit/orchestrator/advance.d.ts.map +1 -1
- package/dist/audit/orchestrator/advance.js +21 -1
- package/dist/audit/orchestrator/advance.js.map +1 -1
- package/dist/audit/orchestrator/architectureDiscovery.d.ts +25 -0
- package/dist/audit/orchestrator/architectureDiscovery.d.ts.map +1 -0
- package/dist/audit/orchestrator/architectureDiscovery.js +193 -0
- package/dist/audit/orchestrator/architectureDiscovery.js.map +1 -0
- package/dist/audit/orchestrator/dependencyMap.d.ts +20 -0
- package/dist/audit/orchestrator/dependencyMap.d.ts.map +1 -1
- package/dist/audit/orchestrator/dependencyMap.js +47 -0
- package/dist/audit/orchestrator/dependencyMap.js.map +1 -1
- package/dist/audit/orchestrator/dependencySlices.d.ts.map +1 -1
- package/dist/audit/orchestrator/dependencySlices.js +37 -3
- package/dist/audit/orchestrator/dependencySlices.js.map +1 -1
- package/dist/audit/orchestrator/designReviewProjection.d.ts +2 -6
- package/dist/audit/orchestrator/designReviewProjection.d.ts.map +1 -1
- package/dist/audit/orchestrator/designReviewProjection.js +2 -6
- package/dist/audit/orchestrator/designReviewProjection.js.map +1 -1
- package/dist/audit/orchestrator/designReviewPrompt.d.ts +2 -2
- package/dist/audit/orchestrator/designReviewPrompt.js +2 -2
- package/dist/audit/orchestrator/designReviewSnapshot.d.ts +2 -3
- package/dist/audit/orchestrator/designReviewSnapshot.d.ts.map +1 -1
- package/dist/audit/orchestrator/designReviewSnapshot.js +2 -3
- package/dist/audit/orchestrator/designReviewSnapshot.js.map +1 -1
- package/dist/audit/orchestrator/executorRunners.d.ts.map +1 -1
- package/dist/audit/orchestrator/executorRunners.js +1 -22
- package/dist/audit/orchestrator/executorRunners.js.map +1 -1
- package/dist/audit/orchestrator/executors.d.ts.map +1 -1
- package/dist/audit/orchestrator/executors.js +0 -12
- package/dist/audit/orchestrator/executors.js.map +1 -1
- package/dist/audit/orchestrator/ingestionExecutors.d.ts.map +1 -1
- package/dist/audit/orchestrator/ingestionExecutors.js +38 -37
- package/dist/audit/orchestrator/ingestionExecutors.js.map +1 -1
- package/dist/audit/orchestrator/nextStep.d.ts +2 -28
- package/dist/audit/orchestrator/nextStep.d.ts.map +1 -1
- package/dist/audit/orchestrator/nextStep.js +4 -37
- package/dist/audit/orchestrator/nextStep.js.map +1 -1
- package/dist/audit/orchestrator/obligationDerive.d.ts +2 -3
- package/dist/audit/orchestrator/obligationDerive.d.ts.map +1 -1
- package/dist/audit/orchestrator/obligationDerive.js +2 -3
- package/dist/audit/orchestrator/obligationDerive.js.map +1 -1
- package/dist/audit/orchestrator/pendingTasks.d.ts +2 -0
- package/dist/audit/orchestrator/pendingTasks.d.ts.map +1 -1
- package/dist/audit/orchestrator/pendingTasks.js +5 -1
- package/dist/audit/orchestrator/pendingTasks.js.map +1 -1
- package/dist/audit/orchestrator/planningExecutors.d.ts.map +1 -1
- package/dist/audit/orchestrator/planningExecutors.js +9 -0
- package/dist/audit/orchestrator/planningExecutors.js.map +1 -1
- package/dist/audit/orchestrator/requeueCommand.d.ts +1 -1
- package/dist/audit/orchestrator/resultBaseline.d.ts +5 -5
- package/dist/audit/orchestrator/resultBaseline.d.ts.map +1 -1
- package/dist/audit/orchestrator/resultBaseline.js +24 -47
- package/dist/audit/orchestrator/resultBaseline.js.map +1 -1
- package/dist/audit/orchestrator/selectiveDeepening/findingFollowup.d.ts.map +1 -1
- package/dist/audit/orchestrator/selectiveDeepening/findingFollowup.js +2 -0
- package/dist/audit/orchestrator/selectiveDeepening/findingFollowup.js.map +1 -1
- package/dist/audit/orchestrator/staleness.d.ts.map +1 -1
- package/dist/audit/orchestrator/staleness.js +14 -10
- package/dist/audit/orchestrator/staleness.js.map +1 -1
- package/dist/audit/orchestrator/state.js +1 -1
- package/dist/audit/orchestrator/state.js.map +1 -1
- package/dist/audit/orchestrator/synthesisExecutors.d.ts.map +1 -1
- package/dist/audit/orchestrator/synthesisExecutors.js +3 -0
- package/dist/audit/orchestrator/synthesisExecutors.js.map +1 -1
- package/dist/audit/types/auditState.d.ts +6 -6
- package/dist/audit/types/flowCoverage.d.ts +6 -6
- package/dist/audit/types.d.ts +24 -13
- package/dist/audit/types.d.ts.map +1 -1
- package/dist/audit/types.js +92 -11
- package/dist/audit/types.js.map +1 -1
- package/dist/remediate/contractPipeline/executionPlan.d.ts +1908 -0
- package/dist/remediate/contractPipeline/executionPlan.d.ts.map +1 -0
- package/dist/remediate/contractPipeline/executionPlan.js +467 -0
- package/dist/remediate/contractPipeline/executionPlan.js.map +1 -0
- package/dist/remediate/contractPipeline/runtimePlanAuthority.d.ts +12 -0
- package/dist/remediate/contractPipeline/runtimePlanAuthority.d.ts.map +1 -0
- package/dist/remediate/contractPipeline/runtimePlanAuthority.js +41 -0
- package/dist/remediate/contractPipeline/runtimePlanAuthority.js.map +1 -0
- package/dist/remediate/dedup/crossLensDedup.d.ts +3 -16
- package/dist/remediate/dedup/crossLensDedup.d.ts.map +1 -1
- package/dist/remediate/dedup/crossLensDedup.js +2 -32
- package/dist/remediate/dedup/crossLensDedup.js.map +1 -1
- package/dist/remediate/index.d.ts +2 -19
- package/dist/remediate/index.d.ts.map +1 -1
- package/dist/remediate/index.js +22 -138
- package/dist/remediate/index.js.map +1 -1
- package/dist/remediate/intake.d.ts +10 -10
- package/dist/remediate/intent/intentOrdering.d.ts +6 -8
- package/dist/remediate/intent/intentOrdering.d.ts.map +1 -1
- package/dist/remediate/intent/intentOrdering.js +24 -47
- package/dist/remediate/intent/intentOrdering.js.map +1 -1
- package/dist/remediate/phases/close.d.ts +25 -17
- package/dist/remediate/phases/close.d.ts.map +1 -1
- package/dist/remediate/phases/close.js +278 -218
- package/dist/remediate/phases/close.js.map +1 -1
- package/dist/remediate/phases/closeAcceptance.d.ts +42 -0
- package/dist/remediate/phases/closeAcceptance.d.ts.map +1 -0
- package/dist/remediate/phases/closeAcceptance.js +66 -0
- package/dist/remediate/phases/closeAcceptance.js.map +1 -0
- package/dist/remediate/phases/closeReviewProvenance.d.ts +1 -1
- package/dist/remediate/phases/closeReviewProvenance.d.ts.map +1 -1
- package/dist/remediate/phases/closeReviewProvenance.js +9 -8
- package/dist/remediate/phases/closeReviewProvenance.js.map +1 -1
- package/dist/remediate/phases/closeVerifyAnalyzerLeads.d.ts.map +1 -1
- package/dist/remediate/phases/closeVerifyAnalyzerLeads.js +11 -9
- package/dist/remediate/phases/closeVerifyAnalyzerLeads.js.map +1 -1
- package/dist/remediate/phases/closeVerifyHeadEvidence.d.ts +21 -86
- package/dist/remediate/phases/closeVerifyHeadEvidence.d.ts.map +1 -1
- package/dist/remediate/phases/closeVerifyHeadEvidence.js +30 -19
- package/dist/remediate/phases/closeVerifyHeadEvidence.js.map +1 -1
- package/dist/remediate/phases/plan.d.ts +3 -60
- package/dist/remediate/phases/plan.d.ts.map +1 -1
- package/dist/remediate/phases/plan.js +3 -227
- package/dist/remediate/phases/plan.js.map +1 -1
- package/dist/remediate/phases/triage.d.ts.map +1 -1
- package/dist/remediate/phases/triage.js +19 -30
- package/dist/remediate/phases/triage.js.map +1 -1
- package/dist/remediate/riskSignal.d.ts +9 -43
- package/dist/remediate/riskSignal.d.ts.map +1 -1
- package/dist/remediate/riskSignal.js +7 -28
- package/dist/remediate/riskSignal.js.map +1 -1
- package/dist/remediate/state/disposition.d.ts +2 -1
- package/dist/remediate/state/disposition.d.ts.map +1 -1
- package/dist/remediate/state/disposition.js +37 -1
- package/dist/remediate/state/disposition.js.map +1 -1
- package/dist/remediate/state/runIdentity.d.ts +2 -13
- package/dist/remediate/state/runIdentity.d.ts.map +1 -1
- package/dist/remediate/state/runIdentity.js +3 -13
- package/dist/remediate/state/runIdentity.js.map +1 -1
- package/dist/remediate/state/store.d.ts +12 -36
- package/dist/remediate/state/store.d.ts.map +1 -1
- package/dist/remediate/state/store.js +58 -150
- package/dist/remediate/state/store.js.map +1 -1
- package/dist/remediate/state/types.d.ts +406 -504
- package/dist/remediate/state/types.d.ts.map +1 -1
- package/dist/remediate/state/types.js +24 -104
- package/dist/remediate/state/types.js.map +1 -1
- package/dist/remediate/steps/contractPipeline.d.ts +7 -661
- package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
- package/dist/remediate/steps/contractPipeline.js +264 -3851
- package/dist/remediate/steps/contractPipeline.js.map +1 -1
- package/dist/remediate/steps/contractPipelinePrompts.d.ts +20 -142
- package/dist/remediate/steps/contractPipelinePrompts.d.ts.map +1 -1
- package/dist/remediate/steps/contractPipelinePrompts.js +48 -581
- package/dist/remediate/steps/contractPipelinePrompts.js.map +1 -1
- package/dist/remediate/steps/dispatch/contractConformanceReview.d.ts +4 -1
- package/dist/remediate/steps/dispatch/contractConformanceReview.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/contractConformanceReview.js +6 -3
- package/dist/remediate/steps/dispatch/contractConformanceReview.js.map +1 -1
- package/dist/remediate/steps/dispatch/hostContracts.d.ts +1 -1
- package/dist/remediate/steps/dispatch/hostContracts.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.d.ts +4 -21
- package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.js +161 -383
- package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
- package/dist/remediate/steps/finalGate.d.ts +34 -0
- package/dist/remediate/steps/finalGate.d.ts.map +1 -1
- package/dist/remediate/steps/finalGate.js +44 -4
- package/dist/remediate/steps/finalGate.js.map +1 -1
- package/dist/remediate/steps/frictionCloseout.d.ts +1 -30
- package/dist/remediate/steps/frictionCloseout.d.ts.map +1 -1
- package/dist/remediate/steps/frictionCloseout.js +6 -99
- package/dist/remediate/steps/frictionCloseout.js.map +1 -1
- package/dist/remediate/steps/nextStep.d.ts +2 -15
- package/dist/remediate/steps/nextStep.d.ts.map +1 -1
- package/dist/remediate/steps/nextStep.js +254 -1042
- package/dist/remediate/steps/nextStep.js.map +1 -1
- package/dist/remediate/steps/prompts.js +16 -16
- package/dist/remediate/steps/prompts.js.map +1 -1
- package/dist/remediate/steps/stepUtils.d.ts +1 -1
- package/dist/remediate/steps/stepUtils.d.ts.map +1 -1
- package/dist/remediate/steps/types.d.ts +1 -1
- package/dist/remediate/steps/types.d.ts.map +1 -1
- package/dist/remediate/steps/types.js +1 -1
- package/dist/remediate/steps/types.js.map +1 -1
- package/dist/remediate/types/options.d.ts +9 -0
- package/dist/remediate/types/options.d.ts.map +1 -1
- package/dist/remediate/validation/artifacts.d.ts.map +1 -1
- package/dist/remediate/validation/artifacts.js +10 -56
- package/dist/remediate/validation/artifacts.js.map +1 -1
- package/dist/remediate/validation/remediationState.d.ts +2 -2
- package/dist/remediate/validation/remediationState.d.ts.map +1 -1
- package/dist/remediate/validation/remediationState.js +17 -77
- package/dist/remediate/validation/remediationState.js.map +1 -1
- package/dist/remediate/validation/verificationReport.d.ts +3 -0
- package/dist/remediate/validation/verificationReport.d.ts.map +1 -0
- package/dist/remediate/validation/verificationReport.js +28 -0
- package/dist/remediate/validation/verificationReport.js.map +1 -0
- package/dist/shared/analyzers/types.d.ts +10 -10
- package/dist/shared/engine/obligationEngine.d.ts +2 -2
- package/dist/shared/friction/triage.d.ts +7 -28
- package/dist/shared/friction/triage.d.ts.map +1 -1
- package/dist/shared/friction/triage.js +7 -28
- package/dist/shared/friction/triage.js.map +1 -1
- package/dist/shared/graph/orderedReachability.js +1 -1
- package/dist/shared/graph/orderedReachability.js.map +1 -1
- package/dist/shared/index.d.ts +5 -6
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +3 -4
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/loopCorePaths.d.ts.map +1 -1
- package/dist/shared/loopCorePaths.js +37 -5
- package/dist/shared/loopCorePaths.js.map +1 -1
- package/dist/shared/prompts.d.ts +24 -10
- package/dist/shared/prompts.d.ts.map +1 -1
- package/dist/shared/prompts.js +27 -13
- package/dist/shared/prompts.js.map +1 -1
- package/dist/shared/reReview/projectionDiff.d.ts.map +1 -1
- package/dist/shared/reReview/projectionDiff.js +6 -14
- package/dist/shared/reReview/projectionDiff.js.map +1 -1
- package/dist/shared/types/executionPlan.d.ts +416 -0
- package/dist/shared/types/executionPlan.d.ts.map +1 -0
- package/dist/shared/types/executionPlan.js +132 -0
- package/dist/shared/types/executionPlan.js.map +1 -0
- package/dist/shared/types/remediationOutcome.d.ts +1378 -12
- package/dist/shared/types/remediationOutcome.d.ts.map +1 -1
- package/dist/shared/types/remediationOutcome.js +29 -1
- package/dist/shared/types/remediationOutcome.js.map +1 -1
- package/dist/shared/types/{contractPipeline/verification.d.ts → verificationReport.d.ts} +9 -37
- package/dist/shared/types/verificationReport.d.ts.map +1 -0
- package/dist/shared/types/verificationReport.js +4 -0
- package/dist/shared/types/verificationReport.js.map +1 -0
- package/dist/shared/validation/findingGrounding.d.ts +3 -5
- package/dist/shared/validation/findingGrounding.d.ts.map +1 -1
- package/dist/shared/validation/findingGrounding.js +4 -7
- package/dist/shared/validation/findingGrounding.js.map +1 -1
- package/package.json +2 -1
- package/scripts/shared/primitives.mjs +20 -0
- package/skills/audit-code/SKILL.md +5 -12
- package/skills/audit-code/audit-code.prompt.md +5 -8
- package/skills/audit-code/opencode-command-template.txt +2 -2
- package/skills/remediate-code/SKILL.md +5 -7
- package/skills/remediate-code/remediate-code.prompt.md +5 -5
- package/dispatch/lens-definitions.json +0 -46
- package/dist/remediate/contractPipeline/artifactNames.d.ts +0 -8
- package/dist/remediate/contractPipeline/artifactNames.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/artifactNames.js +0 -28
- package/dist/remediate/contractPipeline/artifactNames.js.map +0 -1
- package/dist/remediate/contractPipeline/artifactStore.d.ts +0 -219
- package/dist/remediate/contractPipeline/artifactStore.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/artifactStore.js +0 -436
- package/dist/remediate/contractPipeline/artifactStore.js.map +0 -1
- package/dist/remediate/contractPipeline/changeClassification.d.ts +0 -130
- package/dist/remediate/contractPipeline/changeClassification.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/changeClassification.js +0 -446
- package/dist/remediate/contractPipeline/changeClassification.js.map +0 -1
- package/dist/remediate/contractPipeline/contractReviewBinding.d.ts +0 -28
- package/dist/remediate/contractPipeline/contractReviewBinding.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/contractReviewBinding.js +0 -75
- package/dist/remediate/contractPipeline/contractReviewBinding.js.map +0 -1
- package/dist/remediate/contractPipeline/counterexampleFingerprint.d.ts +0 -27
- package/dist/remediate/contractPipeline/counterexampleFingerprint.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/counterexampleFingerprint.js +0 -37
- package/dist/remediate/contractPipeline/counterexampleFingerprint.js.map +0 -1
- package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts +0 -131
- package/dist/remediate/contractPipeline/cyclicSeamResolution.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/cyclicSeamResolution.js +0 -257
- package/dist/remediate/contractPipeline/cyclicSeamResolution.js.map +0 -1
- package/dist/remediate/contractPipeline/derive.d.ts +0 -173
- package/dist/remediate/contractPipeline/derive.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/derive.js +0 -477
- package/dist/remediate/contractPipeline/derive.js.map +0 -1
- package/dist/remediate/contractPipeline/finalizedContractFields.d.ts +0 -50
- package/dist/remediate/contractPipeline/finalizedContractFields.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/finalizedContractFields.js +0 -77
- package/dist/remediate/contractPipeline/finalizedContractFields.js.map +0 -1
- package/dist/remediate/contractPipeline/idRegistry.d.ts +0 -144
- package/dist/remediate/contractPipeline/idRegistry.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/idRegistry.js +0 -214
- package/dist/remediate/contractPipeline/idRegistry.js.map +0 -1
- package/dist/remediate/contractPipeline/obligationKinds.d.ts +0 -27
- package/dist/remediate/contractPipeline/obligationKinds.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/obligationKinds.js +0 -18
- package/dist/remediate/contractPipeline/obligationKinds.js.map +0 -1
- package/dist/remediate/contractPipeline/phaseCut.d.ts +0 -125
- package/dist/remediate/contractPipeline/phaseCut.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/phaseCut.js +0 -339
- package/dist/remediate/contractPipeline/phaseCut.js.map +0 -1
- package/dist/remediate/contractPipeline/phaseCutArtifact.d.ts +0 -31
- package/dist/remediate/contractPipeline/phaseCutArtifact.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/phaseCutArtifact.js +0 -48
- package/dist/remediate/contractPipeline/phaseCutArtifact.js.map +0 -1
- package/dist/remediate/contractPipeline/repairState.d.ts +0 -110
- package/dist/remediate/contractPipeline/repairState.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/repairState.js +0 -185
- package/dist/remediate/contractPipeline/repairState.js.map +0 -1
- package/dist/remediate/contractPipeline/reviewSnapshot.d.ts +0 -79
- package/dist/remediate/contractPipeline/reviewSnapshot.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/reviewSnapshot.js +0 -140
- package/dist/remediate/contractPipeline/reviewSnapshot.js.map +0 -1
- package/dist/remediate/contractPipeline/semanticProjection.d.ts +0 -64
- package/dist/remediate/contractPipeline/semanticProjection.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/semanticProjection.js +0 -136
- package/dist/remediate/contractPipeline/semanticProjection.js.map +0 -1
- package/dist/remediate/contractPipeline/sketchSource.d.ts +0 -194
- package/dist/remediate/contractPipeline/sketchSource.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/sketchSource.js +0 -262
- package/dist/remediate/contractPipeline/sketchSource.js.map +0 -1
- package/dist/remediate/contractPipeline/testPlanCarry.d.ts +0 -17
- package/dist/remediate/contractPipeline/testPlanCarry.d.ts.map +0 -1
- package/dist/remediate/contractPipeline/testPlanCarry.js +0 -72
- package/dist/remediate/contractPipeline/testPlanCarry.js.map +0 -1
- package/dist/remediate/steps/dispatch/marshal.d.ts +0 -7
- package/dist/remediate/steps/dispatch/marshal.d.ts.map +0 -1
- package/dist/remediate/steps/dispatch/marshal.js +0 -11
- package/dist/remediate/steps/dispatch/marshal.js.map +0 -1
- package/dist/remediate/validation/contractPipeline.d.ts +0 -35
- package/dist/remediate/validation/contractPipeline.d.ts.map +0 -1
- package/dist/remediate/validation/contractPipeline.js +0 -613
- package/dist/remediate/validation/contractPipeline.js.map +0 -1
- package/dist/remediate/validation/contractPipelineGates.d.ts +0 -272
- package/dist/remediate/validation/contractPipelineGates.d.ts.map +0 -1
- package/dist/remediate/validation/contractPipelineGates.js +0 -1397
- package/dist/remediate/validation/contractPipelineGates.js.map +0 -1
- package/dist/shared/types/contractPipeline/design.d.ts +0 -79
- package/dist/shared/types/contractPipeline/design.d.ts.map +0 -1
- package/dist/shared/types/contractPipeline/design.js +0 -16
- package/dist/shared/types/contractPipeline/design.js.map +0 -1
- package/dist/shared/types/contractPipeline/goal.d.ts +0 -41
- package/dist/shared/types/contractPipeline/goal.d.ts.map +0 -1
- package/dist/shared/types/contractPipeline/goal.js +0 -10
- package/dist/shared/types/contractPipeline/goal.js.map +0 -1
- package/dist/shared/types/contractPipeline/implementation.d.ts +0 -118
- package/dist/shared/types/contractPipeline/implementation.d.ts.map +0 -1
- package/dist/shared/types/contractPipeline/implementation.js +0 -13
- package/dist/shared/types/contractPipeline/implementation.js.map +0 -1
- package/dist/shared/types/contractPipeline/obligations.d.ts +0 -283
- package/dist/shared/types/contractPipeline/obligations.d.ts.map +0 -1
- package/dist/shared/types/contractPipeline/obligations.js +0 -84
- package/dist/shared/types/contractPipeline/obligations.js.map +0 -1
- package/dist/shared/types/contractPipeline/verification.d.ts.map +0 -1
- package/dist/shared/types/contractPipeline/verification.js +0 -10
- package/dist/shared/types/contractPipeline/verification.js.map +0 -1
- package/dist/shared/types/contractPipeline.d.ts +0 -48
- package/dist/shared/types/contractPipeline.d.ts.map +0 -1
- package/dist/shared/types/contractPipeline.js +0 -32
- package/dist/shared/types/contractPipeline.js.map +0 -1
- package/dist/shared/types/obligationLedger.d.ts +0 -39
- package/dist/shared/types/obligationLedger.d.ts.map +0 -1
- package/dist/shared/types/obligationLedger.js +0 -49
- package/dist/shared/types/obligationLedger.js.map +0 -1
|
@@ -1,54 +1,49 @@
|
|
|
1
|
+
import { RemediationPlanAuthorityError } from "../contractPipeline/runtimePlanAuthority.js";
|
|
1
2
|
import { stateRunId, requireStateRunId, currentHostBoundaryState } from "../state/runIdentity.js";
|
|
2
|
-
import {
|
|
3
|
+
import { presentReportStep } from "./frictionCloseout.js";
|
|
3
4
|
import { reviewFilterDispositionsPath, persistReviewFilterDispositions } from "../review/filterDispositions.js";
|
|
4
5
|
import { INTENT_INTERPRETATION_FILENAME, readPersistedIntentInterpretationSync, readOrRepairIntentInterpretation } from "../intent/intentPersistence.js";
|
|
5
6
|
import { requestedFindingSelection, renderFindingSelection } from "../intakeSelection.js";
|
|
6
7
|
import { parseCommandString } from "../../shared/tooling/commandShape.js";
|
|
7
|
-
// sites-pinned: tests/remediate/friction-capture-closeout.test.ts, tests/remediate/next-step-lifecycle.test.ts, tests/remediate/next-step-pipeline-dispatch.test.ts, tests/remediate/next-step-outcomes-contract.test.ts, tests/remediate/integration-pipeline.test.ts, tests/remediate/outcomes-roundtrip.test.ts, tests/remediate/phase-close.test.ts, tests/remediate/grounding.test.ts, tests/remediate/clarification-round-contract.test.ts, tests/remediate/next-step-review-gate.test.ts, tests/remediate/n-r04-intent-checkpoint.test.ts, tests/remediate/final-gate-red-pause.test.ts
|
|
8
|
+
// sites-pinned: tests/remediate/path-a-source-provenance.test.ts, tests/remediate/path-a-phantom-source.test.ts, tests/remediate/next-step-replan-safety.test.ts, tests/remediate/close-plan-authority.test.ts, tests/remediate/friction-capture-closeout.test.ts, tests/remediate/next-step-lifecycle.test.ts, tests/remediate/next-step-pipeline-dispatch.test.ts, tests/remediate/next-step-outcomes-contract.test.ts, tests/remediate/integration-pipeline.test.ts, tests/remediate/outcomes-roundtrip.test.ts, tests/remediate/phase-close.test.ts, tests/remediate/grounding.test.ts, tests/remediate/clarification-round-contract.test.ts, tests/remediate/next-step-review-gate.test.ts, tests/remediate/n-r04-intent-checkpoint.test.ts, tests/remediate/final-gate-red-pause.test.ts, tests/remediate/deferred-clarification.test.ts
|
|
8
9
|
// (the free-form branch's write
|
|
9
10
|
// scope is normalized — a backslash-spelled citation no longer wedges prepare)
|
|
10
11
|
import { AUDIT_TOOLS_DIRNAME } from "../../shared/io/auditToolsPaths.js";
|
|
11
12
|
import { loadRemediateSessionConfig } from "./sessionConfigLoad.js";
|
|
12
13
|
import { z } from "zod";
|
|
13
14
|
import { existsSync, statSync } from "node:fs";
|
|
14
|
-
import { mkdir, readFile, rename
|
|
15
|
+
import { mkdir, readFile, rename } from "node:fs/promises";
|
|
15
16
|
import { dirname, join, resolve } from "node:path";
|
|
16
17
|
import { StateStore, OPERATOR_LIFECYCLE_FILENAME } from "../state/store.js";
|
|
17
18
|
// IO / validation / rendering helpers
|
|
18
|
-
import { discardOnSchemaVersionMismatch, applyGuidanceFile, applyGuidanceText, readOptionalJsonFile, stagedAndUntracked, writeJsonFile, writeTextFile, buildAuditDeliverablePair, auditReadOf,
|
|
19
|
+
import { discardOnSchemaVersionMismatch, applyGuidanceFile, applyGuidanceText, readOptionalJsonFile, stagedAndUntracked, writeJsonFile, stableStringify, writeTextFile, buildAuditDeliverablePair, auditReadOf, isRecord, renderIngestReportLines, withFsRetry, RunLogger, coerceJsonObjectArg, renderPromptCommand, headCommit, hashContent, projectAuditFindingsReportSubset,
|
|
19
20
|
// obligation engine + intent
|
|
20
21
|
interpretFreeFormIntent, unresolvedFromClauses, advance, describeStoppedFold, linkFrictionRunIds,
|
|
21
22
|
// domain constants
|
|
22
|
-
LENSES, SEVERITIES, CLOSING_ACTIONS, detectProjectFacts, isClosingAction, neutralProjectFacts,
|
|
23
|
-
import {
|
|
24
|
-
import { groundExtractedFindings, } from "../phases/grounding.js";
|
|
23
|
+
LENSES, SEVERITIES, CLOSING_ACTIONS, detectProjectFacts, isClosingAction, neutralProjectFacts, } from "audit-tools/shared";
|
|
24
|
+
import { buildCoverageLedger } from "../phases/plan.js";
|
|
25
25
|
import { runTriagePhase } from "../phases/triage.js";
|
|
26
26
|
import { runClosePhase } from "../phases/close.js";
|
|
27
|
-
import {
|
|
28
|
-
import { readExtractedPlanIfPresent, } from "./dispatch/marshal.js";
|
|
29
|
-
import { ingestRemediationHostResults, hostDependencyLevels, permanentlyDeadPendingBlocks, prepareRemediationHostHandoff, RemediationHostPreparationError, remediationIssueRemedy } from "./dispatch/hostHandoff.js";
|
|
27
|
+
import { ingestRemediationHostResults, hostDependencyLevels, permanentlyDeadPendingUnits, prepareRemediationHostHandoff, RemediationHostPreparationError, remediationIssueRemedy } from "./dispatch/hostHandoff.js";
|
|
30
28
|
import { FileLockTimeoutError, withFileLock, } from "../../shared/io/fileLock.js";
|
|
31
29
|
import { AUDIT_FINDINGS_FILENAME, AUDIT_REPORT_FILENAME, auditArtifactsDir, auditFindingsPath, auditReportPath, promotedAuditFindingsPath, promotedAuditReportPath, remediationArtifactsDir, remediationRequiredTestLogsDir, } from "../../shared/io/auditToolsPaths.js";
|
|
32
30
|
import { callerWorkingDirectory, discoverRepoRoot, resolveRepoRoot, } from "../../shared/io/repoRoot.js";
|
|
33
31
|
import { writeCurrentStep } from "./stepWriter.js";
|
|
34
32
|
import { isTerminalStatus, isVerifiedCompleteStatus, } from "../state/itemStatus.js";
|
|
35
|
-
import { deduplicateCrossLensFindings, fixupBlocksAfterDedup, } from "../dedup/crossLensDedup.js";
|
|
36
|
-
import { checkAffectedFileIntegrity } from "../utils/fileIntegrity.js";
|
|
37
33
|
import { applyIntentOrdering } from "../intent/intentOrdering.js";
|
|
38
34
|
import { resolveIntakeStep } from "./intakeResolver.js";
|
|
39
|
-
import {
|
|
40
|
-
import { RUNTIME_RESIDUAL_DECLARATION, readFinalGateVerdict, runToolOwnedFinalGate, writeFinalGateRedRecord, writeFinalGateOutcomeRecord, writeFinalGateVerdict, } from "./finalGate.js";
|
|
35
|
+
import { RUNTIME_RESIDUAL_DECLARATION, finalGateDisabledReason, finalGateRecordPath, readFinalGateVerdict, runToolOwnedFinalGate, writeFinalGateRedRecord, recordFinalGateOutcome, writeFinalGateVerdict, } from "./finalGate.js";
|
|
41
36
|
import { renderGateAttribution, worktreeContentId, finalGateBinding, } from "./gateCommands.js";
|
|
42
|
-
import { buildNextContractPipelineStep,
|
|
37
|
+
import { buildNextContractPipelineStep, readApprovedExecutionPlan, writePathASeedFromFindings, normalizeBlockTouchedFiles, checkWriteScopePathsAgainstTrackedTree, } from "./contractPipeline.js";
|
|
43
38
|
import { compareCodeUnits } from "../../shared/compareCodeUnits.js";
|
|
44
|
-
import {
|
|
39
|
+
import { executionPlanPaths, readCanonicalPlan, readPlanSource, readPlanReviewHistory } from "../contractPipeline/executionPlan.js";
|
|
45
40
|
import { buildReviewRequest, applyReviewResolution, parseReviewResolution, REVIEW_REQUEST_SCHEMA_VERSION, } from "../review/reviewGate.js";
|
|
46
41
|
import { buildAutonomousReviewDecision } from "../review/autonomousGate.js";
|
|
47
42
|
import { runFindingFilterPass } from "../findingFilter.js";
|
|
48
43
|
import { intakePaths, isIntakeReady, manifestIsInputBound, readIntakeArtifacts, readProjectFacts, readSourceManifest, writeProjectFacts, resolveManifestSources, writeRemediationBrief, } from "../intake.js";
|
|
49
44
|
import { ensureIntakeRiskSignal, readIntakeRiskSignal, writeIntakeRiskSignal, escalateRiskSignal, findingRiskEvidence, distinctAffectedFiles, } from "../riskSignal.js";
|
|
50
45
|
import { isLegacyDraftCheckpoint, readIntentCheckpoint, readIntentCheckpointLenient, } from "audit-tools/shared";
|
|
51
|
-
import {
|
|
46
|
+
import { clarificationPrompt, collectIntakeClarificationsPrompt, collectStartingPointPrompt, loaderCommand, reviewApprovalPrompt, synthesizeIntakePrompt, triagePrompt, } from "./prompts.js";
|
|
52
47
|
// Single-sourced prose renders of the canonical lens / severity vocabularies
|
|
53
48
|
// (`audit-tools/shared` `LENSES` / `SEVERITIES`) for the intent-checkpoint
|
|
54
49
|
// prompt copy. Previously these 11-lens / 5-severity lists were hand-copied as
|
|
@@ -211,15 +206,14 @@ function isDefaultCandidateFresherThanReport(candidatePath, reportPath) {
|
|
|
211
206
|
return false;
|
|
212
207
|
}
|
|
213
208
|
}
|
|
214
|
-
function suppliedInputMatchesRun(inputResolution, manifest) {
|
|
209
|
+
function suppliedInputMatchesRun(inputResolution, manifest, boundSourcePaths = []) {
|
|
215
210
|
if (!inputResolution.supplied)
|
|
216
211
|
return false;
|
|
217
|
-
if (
|
|
212
|
+
if (manifest && !manifestIsInputBound(manifest))
|
|
218
213
|
return false;
|
|
219
214
|
const supplied = new Set(inputResolution.checked.map((p) => resolve(p)));
|
|
220
|
-
const recorded = new Set(manifest.sources
|
|
221
|
-
.
|
|
222
|
-
.map((s) => resolve(s.path)));
|
|
215
|
+
const recorded = new Set((manifest ? manifest.sources.filter((s) => s.type !== "conversation").map((s) => s.path) : boundSourcePaths)
|
|
216
|
+
.map((path) => resolve(path)));
|
|
223
217
|
if (supplied.size === 0 || supplied.size !== recorded.size)
|
|
224
218
|
return false;
|
|
225
219
|
for (const p of supplied)
|
|
@@ -231,9 +225,7 @@ export { classifyFindingRisk } from "./stepUtils.js";
|
|
|
231
225
|
export { isTerminalStatus, isVerifiedCompleteStatus };
|
|
232
226
|
export { hostDependencyLevels };
|
|
233
227
|
function documentableFindings(state) {
|
|
234
|
-
|
|
235
|
-
return [];
|
|
236
|
-
return state.plan.findings.filter((finding) => state.items?.[finding.id]?.status === "pending");
|
|
228
|
+
return state.plan?.units.filter(unit => state.items?.[unit.id]?.status === "pending") ?? [];
|
|
237
229
|
}
|
|
238
230
|
/**
|
|
239
231
|
* The dispatch frontier: the level-0 draw of the SAME dependency/phase
|
|
@@ -254,15 +246,15 @@ function dispatchFrontier(state) {
|
|
|
254
246
|
* the implementing obligation: marks the pending items of every permanently
|
|
255
247
|
* dead block `blocked` with the INV-RS-01 reason, and reports whether anything
|
|
256
248
|
* changed. Deadness is the workload boundary's own liveness analysis
|
|
257
|
-
* ({@link
|
|
249
|
+
* ({@link permanentlyDeadPendingUnits}), so a node held by an unanswered
|
|
258
250
|
* clarification — or by a lower phase that is merely still working — is NEVER
|
|
259
251
|
* mis-reported as an upstream failure (the 175cfb89 pin).
|
|
260
252
|
*/
|
|
261
253
|
function sweepPermanentlyDeadBlocks(state) {
|
|
262
254
|
let changed = false;
|
|
263
255
|
const now = new Date().toISOString();
|
|
264
|
-
for (const block of
|
|
265
|
-
for (const findingId of block.
|
|
256
|
+
for (const block of permanentlyDeadPendingUnits(state)) {
|
|
257
|
+
for (const findingId of [block.id]) {
|
|
266
258
|
const it = state.items?.[findingId];
|
|
267
259
|
if (!it || it.status !== "pending")
|
|
268
260
|
continue;
|
|
@@ -322,9 +314,9 @@ export function phaseBoundaryToGate(state) {
|
|
|
322
314
|
const dispatchPhase = Math.min(...frontier.map(phaseOf));
|
|
323
315
|
if (dispatchPhase <= 0)
|
|
324
316
|
return null;
|
|
325
|
-
const pristine = plan.
|
|
317
|
+
const pristine = plan.units
|
|
326
318
|
.filter((b) => phaseOf(b) === dispatchPhase)
|
|
327
|
-
.every((b) => b.
|
|
319
|
+
.every((b) => items[b.id]?.status === "pending");
|
|
328
320
|
return pristine ? dispatchPhase : null;
|
|
329
321
|
}
|
|
330
322
|
// Tool-owned final completion gate (INV-RS-10)
|
|
@@ -341,7 +333,7 @@ function resolvedOrTerminalItems(state) {
|
|
|
341
333
|
}
|
|
342
334
|
function allItemsTerminal(state) {
|
|
343
335
|
const items = Object.values(state.items ?? {});
|
|
344
|
-
return
|
|
336
|
+
return state.plan !== undefined && resolvedOrTerminalItems(state).length === items.length;
|
|
345
337
|
}
|
|
346
338
|
/**
|
|
347
339
|
* Reorder a finalized plan by the checkpoint's interpreted intent.
|
|
@@ -364,8 +356,8 @@ async function applyCheckpointIntentOrdering(artifactsDir, plan) {
|
|
|
364
356
|
const freeForm = checkpoint?.free_form_intent;
|
|
365
357
|
if (typeof freeForm !== "string" || freeForm.trim().length === 0)
|
|
366
358
|
return plan;
|
|
367
|
-
const ordered = applyIntentOrdering(plan.findings, plan.
|
|
368
|
-
return { ...plan, findings: ordered.findings,
|
|
359
|
+
const ordered = applyIntentOrdering(plan.findings, plan.units, interpretFreeFormIntent(freeForm));
|
|
360
|
+
return { ...plan, findings: ordered.findings, units: ordered.units };
|
|
369
361
|
}
|
|
370
362
|
/**
|
|
371
363
|
* The write scope a free-form finding's `affected_files` implies, in the ONE
|
|
@@ -385,105 +377,6 @@ async function applyCheckpointIntentOrdering(artifactsDir, plan) {
|
|
|
385
377
|
* leaves the finding itself intact — grounding already decided which paths are
|
|
386
378
|
* real, and this decides only how they are SPELLED.
|
|
387
379
|
*/
|
|
388
|
-
function normalizedWriteScope(root, finding) {
|
|
389
|
-
const normalized = [];
|
|
390
|
-
for (const affected of finding.affected_files) {
|
|
391
|
-
let path;
|
|
392
|
-
try {
|
|
393
|
-
path = repoRelativePath(root, affected.path, "affected_files[].path");
|
|
394
|
-
}
|
|
395
|
-
catch {
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
if (path.length > 0 && !normalized.includes(path))
|
|
399
|
-
normalized.push(path);
|
|
400
|
-
}
|
|
401
|
-
return normalized;
|
|
402
|
-
}
|
|
403
|
-
function normalizeExtractedPlan(root, value, facts) {
|
|
404
|
-
if (!isRecord(value)) {
|
|
405
|
-
throw new Error("extracted-plan.json must be an object.");
|
|
406
|
-
}
|
|
407
|
-
const rawFindings = Array.isArray(value.findings) ? value.findings : [];
|
|
408
|
-
const findings = rawFindings.map((finding) => {
|
|
409
|
-
if (!isRecord(finding))
|
|
410
|
-
return finding;
|
|
411
|
-
return {
|
|
412
|
-
category: "General",
|
|
413
|
-
affected_files: [],
|
|
414
|
-
evidence: [],
|
|
415
|
-
...finding,
|
|
416
|
-
};
|
|
417
|
-
});
|
|
418
|
-
const rawBlocks = Array.isArray(value.blocks) ? value.blocks : [];
|
|
419
|
-
const blocks = rawBlocks.length > 0
|
|
420
|
-
? rawBlocks.map((block) => {
|
|
421
|
-
if (!isRecord(block))
|
|
422
|
-
return block;
|
|
423
|
-
return {
|
|
424
|
-
parallel_safe: true,
|
|
425
|
-
dependencies: [],
|
|
426
|
-
// touched_files is REQUIRED on the block contract; default to an
|
|
427
|
-
// empty array so a free-form block that omits it still validates,
|
|
428
|
-
// while an explicit value on `block` wins via the spread below.
|
|
429
|
-
touched_files: [],
|
|
430
|
-
...block,
|
|
431
|
-
};
|
|
432
|
-
})
|
|
433
|
-
: findings.map((finding, index) => ({
|
|
434
|
-
block_id: `B-${String(index + 1).padStart(3, "0")}`,
|
|
435
|
-
items: [finding.id],
|
|
436
|
-
parallel_safe: true,
|
|
437
|
-
// Normalized, not copied: see normalizedWriteScope — a Windows-spelled
|
|
438
|
-
// citation would otherwise become a write scope the handoff refuses.
|
|
439
|
-
touched_files: normalizedWriteScope(root, finding),
|
|
440
|
-
}));
|
|
441
|
-
const dedup = deduplicateCrossLensFindings(findings);
|
|
442
|
-
const dedupBlocks = fixupBlocksAfterDedup(blocks, dedup.mergeMap);
|
|
443
|
-
const plan = {
|
|
444
|
-
plan_id: typeof value.plan_id === "string" ? value.plan_id : randomRunId("PLAN"),
|
|
445
|
-
...(typeof value.goal_id === "string" ? { goal_id: value.goal_id } : {}),
|
|
446
|
-
...(typeof value.source === "string" ? { source: value.source } : {}),
|
|
447
|
-
findings: dedup.findings,
|
|
448
|
-
blocks: dedupBlocks,
|
|
449
|
-
// Project facts (owner decision 92b0e2dd7cfdc06d): the input's own VALID
|
|
450
|
-
// values win — the contract pipeline detects them itself — and the run's
|
|
451
|
-
// detected facts fill the rest. Candidates are what the host chooses
|
|
452
|
-
// FROM at the intent checkpoint; nothing here selects one.
|
|
453
|
-
project_type: typeof value.project_type === "string" && value.project_type !== "unknown"
|
|
454
|
-
? value.project_type
|
|
455
|
-
: facts.project_type,
|
|
456
|
-
...(typeof value.test_command === "string" && value.test_command.trim().length > 0
|
|
457
|
-
? { test_command: value.test_command, test_command_source: "explicit" }
|
|
458
|
-
: facts.commands.test
|
|
459
|
-
? { test_command: renderPromptCommand(facts.commands.test), test_command_source: "project_facts" }
|
|
460
|
-
: {}),
|
|
461
|
-
...(typeof value.e2e_command === "string" && value.e2e_command.trim().length > 0
|
|
462
|
-
? { e2e_command: value.e2e_command, e2e_command_source: "explicit" }
|
|
463
|
-
: facts.commands.e2e
|
|
464
|
-
? { e2e_command: renderPromptCommand(facts.commands.e2e), e2e_command_source: "project_facts" }
|
|
465
|
-
: {}),
|
|
466
|
-
candidate_closing_actions: Array.isArray(value.candidate_closing_actions) &&
|
|
467
|
-
value.candidate_closing_actions.length > 0 &&
|
|
468
|
-
value.candidate_closing_actions.every(isClosingAction)
|
|
469
|
-
? value.candidate_closing_actions
|
|
470
|
-
: facts.candidate_closing_actions,
|
|
471
|
-
block_strategy: value.block_strategy === "test_graph" ||
|
|
472
|
-
value.block_strategy === "git_cocommit" ||
|
|
473
|
-
value.block_strategy === "file_overlap" ||
|
|
474
|
-
value.block_strategy === "manual"
|
|
475
|
-
? value.block_strategy
|
|
476
|
-
: undefined,
|
|
477
|
-
};
|
|
478
|
-
const issues = validateRemediationPlan(plan).filter((issue) => issue.severity === "error");
|
|
479
|
-
if (issues.length > 0) {
|
|
480
|
-
throw new Error(`Invalid extracted plan:\n${formatValidationIssues(issues)}`);
|
|
481
|
-
}
|
|
482
|
-
if (plan.findings.length === 0) {
|
|
483
|
-
throw new Error("Extracted plan contains zero findings.");
|
|
484
|
-
}
|
|
485
|
-
return { plan, sourceFindings: findings, mergeMap: dedup.mergeMap };
|
|
486
|
-
}
|
|
487
380
|
/**
|
|
488
381
|
* The closing plan the HOST chose on the confirmed checkpoint: its action, or
|
|
489
382
|
* `none` when the field is absent, plus the argv a `custom` choice carries.
|
|
@@ -523,13 +416,14 @@ function customCommandOf(checkpoint) {
|
|
|
523
416
|
async function saveStateForPlan(artifactsDir, existing, plan, planCoverage) {
|
|
524
417
|
const { host_handoff: _staleHostHandoff, conformance_review: priorConformance, ...carryForwardState } = existing;
|
|
525
418
|
const items = {};
|
|
526
|
-
const
|
|
527
|
-
for (const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
status: "pending"
|
|
531
|
-
|
|
532
|
-
|
|
419
|
+
const owner = await readOptionalJsonFile(join(executionPlanPaths(artifactsDir).directory, "owner-decision.json"));
|
|
420
|
+
for (const unit of plan.units) {
|
|
421
|
+
const prior = existing.plan?.units.find(entry => entry.id === unit.id);
|
|
422
|
+
items[unit.id] = prior && stableStringify(prior) === stableStringify(unit) && existing.items?.[unit.id]
|
|
423
|
+
? existing.items[unit.id] : { unit_id: unit.id, status: "pending" };
|
|
424
|
+
const declined = owner?.revision_sha256 === plan.review_revision_sha256 ? owner.declined_units.find(entry => entry.id === unit.id) : undefined;
|
|
425
|
+
if (declined && !isVerifiedCompleteStatus(items[unit.id].status))
|
|
426
|
+
items[unit.id] = { unit_id: unit.id, status: "ignored", failure_reason: declined.reason, completed_at: new Date().toISOString() };
|
|
533
427
|
}
|
|
534
428
|
const state = {
|
|
535
429
|
...carryForwardState,
|
|
@@ -544,137 +438,19 @@ async function saveStateForPlan(artifactsDir, existing, plan, planCoverage) {
|
|
|
544
438
|
await writeJsonFile(join(artifactsDir, "remediation_plan.json"), plan);
|
|
545
439
|
return state;
|
|
546
440
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const stripped = {};
|
|
561
|
-
for (const key of Object.keys(value).sort()) {
|
|
562
|
-
if (PLAN_TIME_BOOKKEEPING_KEYS.has(key))
|
|
563
|
-
continue;
|
|
564
|
-
stripped[key] = stripPlanTimeBookkeeping(value[key]);
|
|
565
|
-
}
|
|
566
|
-
return stripped;
|
|
567
|
-
}
|
|
568
|
-
/**
|
|
569
|
-
* The re-plan carry-forward identity of a finding: canonical JSON with the
|
|
570
|
-
* plan-time bookkeeping keys stripped, so a re-plan whose only delta is a
|
|
571
|
-
* recomputed file hash or a re-evaluated grounding flag carries the prior item
|
|
572
|
-
* forward, while a real change to the finding does not.
|
|
573
|
-
*
|
|
574
|
-
* EXPORTED so the invariant suite can call THIS function. It was module-internal,
|
|
575
|
-
* and the suite claiming to cover the invariant declared its own copy of the key
|
|
576
|
-
* set, the strip and the key builder — so dropping `evidence_grounded` from the
|
|
577
|
-
* production set, or widening it with a real field like `severity`, left the
|
|
578
|
-
* block green while carry-forward regressed. A test asserting against its own
|
|
579
|
-
* re-implementation pins nothing about shipped behaviour.
|
|
580
|
-
*/
|
|
581
|
-
export function findingCarryForwardKey(finding) {
|
|
582
|
-
return JSON.stringify(stripPlanTimeBookkeeping(finding));
|
|
583
|
-
}
|
|
584
|
-
// Single-block membership is enforced by `fixupBlocksAfterDedup` (each finding
|
|
585
|
-
// id appears in exactly one block's items after fixup), so this scan is a plain
|
|
586
|
-
// projection. First-wins in block order is kept as the tie-break for defense in
|
|
587
|
-
// depth — it is the same order fixup resolves ownership in, so the two can
|
|
588
|
-
// never disagree even on a malformed plan.
|
|
589
|
-
function blockIdsByFinding(plan) {
|
|
590
|
-
const byFinding = new Map();
|
|
591
|
-
for (const block of plan.blocks) {
|
|
592
|
-
for (const id of block.items) {
|
|
593
|
-
if (!byFinding.has(id)) {
|
|
594
|
-
byFinding.set(id, block.block_id);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
return byFinding;
|
|
599
|
-
}
|
|
600
|
-
function carryForwardMatchingItems(previous, replanned) {
|
|
601
|
-
if (!previous.plan || !previous.items || !replanned.plan || !replanned.items) {
|
|
602
|
-
return replanned;
|
|
603
|
-
}
|
|
604
|
-
const previousFindings = new Map(previous.plan.findings.map((finding) => [finding.id, finding]));
|
|
605
|
-
const replannedBlockIds = blockIdsByFinding(replanned.plan);
|
|
606
|
-
const items = { ...replanned.items };
|
|
607
|
-
let carried = false;
|
|
608
|
-
for (const finding of replanned.plan.findings) {
|
|
609
|
-
const previousFinding = previousFindings.get(finding.id);
|
|
610
|
-
const previousItem = previous.items[finding.id];
|
|
611
|
-
// A still-pending item carries no work to preserve, so it is re-minted from
|
|
612
|
-
// the fresh plan rather than carried forward. (This test used to also admit
|
|
613
|
-
// a pending item that held an `item_spec`; the document phase that produced
|
|
614
|
-
// one was dissolved and the field is gone, so the second condition could
|
|
615
|
-
// never be true and is not restated here.)
|
|
616
|
-
if (!previousFinding || !previousItem) {
|
|
617
|
-
continue;
|
|
618
|
-
}
|
|
619
|
-
if (previousItem.status === "pending") {
|
|
620
|
-
continue;
|
|
621
|
-
}
|
|
622
|
-
if (findingCarryForwardKey(previousFinding) !== findingCarryForwardKey(finding)) {
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
items[finding.id] = {
|
|
626
|
-
...previousItem,
|
|
627
|
-
block_id: replannedBlockIds.get(finding.id) ?? previousItem.block_id,
|
|
628
|
-
};
|
|
629
|
-
carried = true;
|
|
630
|
-
}
|
|
631
|
-
if (!carried) {
|
|
632
|
-
return replanned;
|
|
633
|
-
}
|
|
634
|
-
const hasPending = replanned.plan.findings.some((finding) => items[finding.id]?.status === "pending");
|
|
635
|
-
return {
|
|
636
|
-
...replanned,
|
|
637
|
-
items,
|
|
638
|
-
status: hasPending ? "planning" : replanned.status,
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
async function forceReplanFromExistingIntake(root, artifactsDir, previous, store, runLogger) {
|
|
642
|
-
const pendingState = {
|
|
643
|
-
status: "pending",
|
|
644
|
-
started_at: previous.started_at,
|
|
645
|
-
step_count: previous.step_count,
|
|
646
|
-
...(previous.conformance_review ? { conformance_review: previous.conformance_review } : {}),
|
|
647
|
-
// Carry the run-lifetime staging-manifest fields across a force-replan.
|
|
648
|
-
// Dropping run_start_dirty here would make handlePendingExtractedPlan's
|
|
649
|
-
// capture-once guard re-capture AFTER edits have landed, misclassifying
|
|
650
|
-
// the run's own hand-applied edits as pre-existing dirt (silently
|
|
651
|
-
// under-staged at close); applied_edit_surface is git-proven ground truth
|
|
652
|
-
// that must survive replanning for the same reason.
|
|
653
|
-
...(previous.run_start_dirty
|
|
654
|
-
? { run_start_dirty: previous.run_start_dirty }
|
|
655
|
-
: {}),
|
|
656
|
-
...(previous.applied_edit_surface
|
|
657
|
-
? { applied_edit_surface: previous.applied_edit_surface }
|
|
658
|
-
: {}),
|
|
659
|
-
};
|
|
660
|
-
const extractedPlan = await readExtractedPlanIfPresent(artifactsDir);
|
|
661
|
-
if (!extractedPlan) {
|
|
662
|
-
await store.saveState(pendingState);
|
|
663
|
-
return null;
|
|
664
|
-
}
|
|
665
|
-
const outcome = await handlePendingExtractedPlan(root, artifactsDir, pendingState, extractedPlan, runLogger);
|
|
666
|
-
if (outcome.kind === "discarded") {
|
|
667
|
-
// Carried up rather than collapsed to null: the decide loop emits the step
|
|
668
|
-
// that names the discard.
|
|
669
|
-
return outcome;
|
|
670
|
-
}
|
|
671
|
-
const replanned = outcome.state;
|
|
672
|
-
if (!replanned) {
|
|
673
|
-
return null;
|
|
674
|
-
}
|
|
675
|
-
const carried = carryForwardMatchingItems(previous, replanned);
|
|
676
|
-
await store.saveState(carried);
|
|
677
|
-
return carried;
|
|
441
|
+
async function forceReplanFromExistingIntake(root, artifactsDir, previous, runLogger) {
|
|
442
|
+
const step = await buildNextContractPipelineStep({ root, artifactsDir, runId: requireStateRunId(previous), forceRevision: false });
|
|
443
|
+
if (step)
|
|
444
|
+
return { kind: "planning_step", step };
|
|
445
|
+
const approved = await readApprovedExecutionPlan(artifactsDir);
|
|
446
|
+
if (!approved)
|
|
447
|
+
return { kind: "blocked", reason: "Revise and independently approve the current executable plan before replanning. Accepted execution history is preserved." };
|
|
448
|
+
const activated = await activateApprovedPlan(root, artifactsDir, previous, approved, runLogger);
|
|
449
|
+
if (activated.kind === "blocked")
|
|
450
|
+
return activated;
|
|
451
|
+
// Activation already carries unchanged history and applies CURRENT owner
|
|
452
|
+
// decisions. Reapplying old items here would undo a newly declined unit.
|
|
453
|
+
return activated.state;
|
|
678
454
|
}
|
|
679
455
|
/**
|
|
680
456
|
* The ingest report for the implement-dispatch prompt, as prompt lines.
|
|
@@ -794,7 +570,7 @@ async function buildImplementDispatchStep(ctx) {
|
|
|
794
570
|
step: await writeCurrentStep({
|
|
795
571
|
stepKind: "repair_handoff", status: "blocked", runId, repoRoot: root, artifactsDir,
|
|
796
572
|
prompt: `# Repair the planning output before dispatch\n\n${excerpt}\n\nFull validation details: ${diagnosticsPath}\n\n` +
|
|
797
|
-
`Owning source: ${sourcePath} (plan.
|
|
573
|
+
`Owning source: ${sourcePath} (plan.units and its generated handoff binding).\n` +
|
|
798
574
|
`Upstream planning input: ${join(artifactsDir, "extracted-plan.json")}.\n\n` +
|
|
799
575
|
"The current run and completed work are preserved. This dispatch cannot safely repair its upstream producer. " +
|
|
800
576
|
"Return the named validation errors to the planning producer; do not edit generated state, discard accepted work, or submit results under invalid bindings. " +
|
|
@@ -915,246 +691,44 @@ next-step joins in and takes on unclaimed work.
|
|
|
915
691
|
async function handleComplete(root, artifactsDir, state) {
|
|
916
692
|
return presentReportStep(root, artifactsDir, state);
|
|
917
693
|
}
|
|
918
|
-
/**
|
|
919
|
-
* Copy an unusable extracted plan somewhere recoverable and PROVE the copy
|
|
920
|
-
* landed, returning the archive path. Read back and compared byte-for-byte:
|
|
921
|
-
* "the write did not throw" is not evidence a file exists, and this is the last
|
|
922
|
-
* moment the plan is recoverable at all.
|
|
923
|
-
*
|
|
924
|
-
* THROWS rather than returning when the copy cannot be made or verified, so the
|
|
925
|
-
* caller's unlink is unreachable on that path — an irreversible delete never
|
|
926
|
-
* runs before its archive is written and verified.
|
|
927
|
-
*/
|
|
928
|
-
async function archiveExtractedPlan(extractedPlanPath) {
|
|
929
|
-
// BYTES, compared with Buffer.compare — the bar CP-NODE-3 set for the
|
|
930
|
-
// verified-archive promotion path. A utf8-string compare is a weaker claim
|
|
931
|
-
// than the one an archive has to make: it silently equates byte sequences that
|
|
932
|
-
// decode alike (a BOM, a lone surrogate, an invalid sequence replaced by
|
|
933
|
-
// U+FFFD on BOTH sides), so a corrupt copy can read as verified.
|
|
934
|
-
const original = await readFile(extractedPlanPath);
|
|
935
|
-
const archivePath = join(dirname(extractedPlanPath), "archive", `extracted-plan-${Date.now()}.json`);
|
|
936
|
-
await mkdir(dirname(archivePath), { recursive: true });
|
|
937
|
-
await writeFile(archivePath, original);
|
|
938
|
-
const readBack = await readFile(archivePath);
|
|
939
|
-
if (Buffer.compare(original, readBack) !== 0) {
|
|
940
|
-
throw new Error(`Extracted-plan archive at ${archivePath} does not match the original bytes.`);
|
|
941
|
-
}
|
|
942
|
-
return archivePath;
|
|
943
|
-
}
|
|
944
694
|
/** Render the step that STATES a discard, rather than asking for an input again. */
|
|
945
|
-
async function
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
// failures mean the extracted PLAN is unusable: normalization and grounding.
|
|
969
|
-
// It deliberately stops there. Everything after it — sizing, the dirty
|
|
970
|
-
// snapshot, the coverage ledger, persistence — fails for reasons that have
|
|
971
|
-
// nothing to do with the plan's content, so discarding the plan on those is
|
|
972
|
-
// both a data loss and a misdiagnosis.
|
|
973
|
-
//
|
|
974
|
-
// `resolvePlanContextBudget`'s refusal is the case that proved it. Its message
|
|
975
|
-
// asks the operator to declare a window, but it threw into this catch: the
|
|
976
|
-
// plan was deleted and the operator was told the file was corrupt. Because
|
|
977
|
-
// re-extraction cannot change the host's declared window, the next step
|
|
978
|
-
// reproduced it exactly — a deterministic loop that ate the extracted plan on
|
|
979
|
-
// every lap. Pinned by `tests/remediate/plan-sizing-refusal.test.ts`.
|
|
980
|
-
let plan;
|
|
981
|
-
let sourceFindings;
|
|
982
|
-
let mergeMap;
|
|
983
|
-
let grounding;
|
|
984
|
-
try {
|
|
985
|
-
({ plan, sourceFindings, mergeMap } = normalizeExtractedPlan(root, extractedPlan,
|
|
986
|
-
// Persisted by the confirm step; planning spawns nothing (the
|
|
987
|
-
// backend-independent planning contract), so it never detects here.
|
|
988
|
-
(await readProjectFacts(artifactsDir)) ?? neutralProjectFacts()));
|
|
989
|
-
// INTENT ORDERING, applied where the plan's findings and blocks are
|
|
990
|
-
// FINALIZED. `applyIntentOrdering` existed with no production caller at all:
|
|
991
|
-
// the checkpoint's interpreted intent was written and never read back, so
|
|
992
|
-
// "the work the user emphasised is dispatched first" was a property the code
|
|
993
|
-
// could state but not deliver. Ordering ONLY — it never drops or mutates a
|
|
994
|
-
// finding; every input is present in the output with a different order, so a
|
|
995
|
-
// plan whose checkpoint carries no intent is returned unchanged.
|
|
996
|
-
plan = await applyCheckpointIntentOrdering(artifactsDir, plan);
|
|
997
|
-
// Deterministic grounding for the LLM-extracted plan (this path never sees
|
|
998
|
-
// structured audit findings): strip phantom affected_files paths, drop
|
|
999
|
-
// findings whose every cited path was phantom, and classify evidence. No
|
|
1000
|
-
// bounded LLM repair here — the host re-extracts with the corrected prompt
|
|
1001
|
-
// if the whole plan grounds to nothing. Contract-pipeline-promoted plans
|
|
1002
|
-
// are grounded by construction (the traceability gate ties every node to
|
|
1003
|
-
// obligations/accepted counterexamples), so their obligation-reference
|
|
1004
|
-
// evidence is exempt from the path-citation check.
|
|
1005
|
-
grounding = await groundExtractedFindings(plan.findings, {
|
|
1006
|
-
root,
|
|
1007
|
-
evidenceGrounding: plan.source !== "contract_pipeline",
|
|
1008
|
-
});
|
|
1009
|
-
if (grounding.dropped.length > 0) {
|
|
1010
|
-
// DROPPED-ID BOOKKEEPING, durable. A caller reading a finding count across
|
|
1011
|
-
// the plan boundary must never receive the submitted count when findings
|
|
1012
|
-
// were dropped, so the ids, the dropped count and the surviving grounded
|
|
1013
|
-
// count all land in the run log — not only on stderr, which no artifact
|
|
1014
|
-
// captures.
|
|
1015
|
-
const droppedIds = grounding.dropped.map((d) => d.finding.id);
|
|
1016
|
-
runLogger.event({
|
|
1017
|
-
phase: "next-step",
|
|
1018
|
-
kind: "outcome",
|
|
1019
|
-
obligation: "plan_grounding",
|
|
1020
|
-
note: `grounding_dropped_findings dropped=${String(droppedIds.length)} ` +
|
|
1021
|
-
`grounded=${String(grounding.findings.length)} ` +
|
|
1022
|
-
`submitted=${String(plan.findings.length)} ` +
|
|
1023
|
-
`ids=${droppedIds.join(",")}`,
|
|
1024
|
-
});
|
|
1025
|
-
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`);
|
|
1026
|
-
}
|
|
1027
|
-
plan.findings = grounding.findings;
|
|
1028
|
-
const keptIds = new Set(plan.findings.map((f) => f.id));
|
|
1029
|
-
plan.blocks = plan.blocks
|
|
1030
|
-
.map((b) => ({ ...b, items: (b.items ?? []).filter((id) => keptIds.has(id)) }))
|
|
1031
|
-
.filter((b) => (b.items ?? []).length > 0);
|
|
1032
|
-
if (plan.findings.length === 0) {
|
|
1033
|
-
throw new Error("Every extracted finding cited only phantom paths; re-extract with real repo-relative paths.");
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
catch (error) {
|
|
1037
|
-
const paths = intakePaths(artifactsDir);
|
|
1038
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
1039
|
-
// ARCHIVE, VERIFY, THEN destroy — in that order, with the delete unreachable
|
|
1040
|
-
// if the archive did not land. This recovery used to unlink the plan
|
|
1041
|
-
// outright, unarchived and unverified, leaving a line on stderr as the only
|
|
1042
|
-
// record; the plan the run was built from was simply gone.
|
|
1043
|
-
let archivePath;
|
|
1044
|
-
if (existsSync(paths.extractedPlan)) {
|
|
1045
|
-
try {
|
|
1046
|
-
archivePath = await archiveExtractedPlan(paths.extractedPlan);
|
|
1047
|
-
}
|
|
1048
|
-
catch (archiveError) {
|
|
1049
|
-
const detail = archiveError instanceof Error
|
|
1050
|
-
? archiveError.message
|
|
1051
|
-
: String(archiveError);
|
|
1052
|
-
runLogger.event({
|
|
1053
|
-
phase: "next-step",
|
|
1054
|
-
kind: "error",
|
|
1055
|
-
obligation: "extracted_plan_recovery",
|
|
1056
|
-
note: `extracted_plan_archive_failed reason=${reason} archive_error=${detail}`,
|
|
1057
|
-
});
|
|
1058
|
-
// NOT a re-emitted extraction step. Returning null here would report a
|
|
1059
|
-
// routine "re-extract, please" while the plan was destroyed and nothing
|
|
1060
|
-
// held a copy of it.
|
|
1061
|
-
throw new Error(`Extracted plan at ${paths.extractedPlan} is unusable (${reason}) but could ` +
|
|
1062
|
-
`not be archived (${detail}); it was left in place rather than destroyed.`);
|
|
1063
|
-
}
|
|
1064
|
-
const { unlink } = await import("node:fs/promises");
|
|
1065
|
-
await unlink(paths.extractedPlan);
|
|
1066
|
-
}
|
|
1067
|
-
runLogger.event({
|
|
1068
|
-
phase: "next-step",
|
|
1069
|
-
kind: "outcome",
|
|
1070
|
-
obligation: "extracted_plan_recovery",
|
|
1071
|
-
note: `extracted_plan_removed reason=${reason} ` +
|
|
1072
|
-
`archive=${archivePath ?? "(nothing on disk to archive)"}`,
|
|
1073
|
-
});
|
|
1074
|
-
process.stderr.write(`[remediate-code] Unusable extracted-plan.json removed (${reason}); archived at ${archivePath ?? "(nothing on disk to archive)"}. Emitting the plan-discarded step.\n`);
|
|
1075
|
-
// The reason travels with the outcome, so the EMITTED STEP can state it.
|
|
1076
|
-
// Returning a bare `null` here is what made a destroyed plan indistinguishable
|
|
1077
|
-
// from a run that never had an input: the decide loop fell through to
|
|
1078
|
-
// `collect_starting_point` and the host was told to go and find an input it
|
|
1079
|
-
// had already supplied. Everything needed to say so was already computed —
|
|
1080
|
-
// it just had nowhere to go.
|
|
1081
|
-
return {
|
|
1082
|
-
kind: "discarded",
|
|
1083
|
-
reason,
|
|
1084
|
-
...(archivePath ? { archivePath } : {}),
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
|
-
// Past the recovery boundary: a failure below is a real failure and propagates.
|
|
1088
|
-
//
|
|
1089
|
-
// WHAT THE AUDIT READ, stamped by the tool. `normalizeExtractedPlan` builds
|
|
1090
|
-
// the plan field by field and deliberately has no `audit_read` line: the
|
|
1091
|
-
// extracted plan is host-writable, and this commit becomes terminal
|
|
1092
|
-
// dispositions at close. Only a plan the contract pipeline promoted from a
|
|
1093
|
-
// findings report has an audit-side source; every other plan states `null`.
|
|
1094
|
-
plan = {
|
|
1095
|
-
...plan,
|
|
1096
|
-
audit_read: plan.source === "contract_pipeline" ? await readSeedAuditRead(artifactsDir) : null,
|
|
1097
|
-
};
|
|
1098
|
-
const pipelined = await applyPlanPipeline(plan, { root, artifactsDir });
|
|
1099
|
-
// Run-start dirty snapshot for the V2 staging manifest, capture-once: the
|
|
1100
|
-
// extracted-plan join runs at plan time (before any remediation edit), so
|
|
1101
|
-
// the dirty set here is pre-existing user dirt — the close phase excludes
|
|
1102
|
-
// it from DECLARED-surface staging.
|
|
1103
|
-
if (!existing.run_start_dirty) {
|
|
1104
|
-
existing = {
|
|
1105
|
-
...existing,
|
|
1106
|
-
run_start_dirty: [...(await stagedAndUntracked(root))].sort(),
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
// Discarded on mismatch, so the gate re-asks rather than replaying operator
|
|
1110
|
-
// decisions under semantics they were not made under. Re-asking costs a repeat
|
|
1111
|
-
// answer; applying them blind could act on a keep/decline that no longer means
|
|
1112
|
-
// what it meant when it was recorded.
|
|
1113
|
-
const reviewDecision = discardOnSchemaVersionMismatch(await readOptionalJsonFile(reviewDecisionPath(artifactsDir)), REVIEW_DECISION_SCHEMA_VERSION);
|
|
1114
|
-
// Coverage ledger. Path A (structured_audit): the single filter pass ran at
|
|
1115
|
-
// intake over the ORIGINAL findings and persisted its dispositions — build
|
|
1116
|
-
// coverage over those originals so every audit finding gets exactly one
|
|
1117
|
-
// disposition (planned / folded_into / dropped_* / dropped_by_checkpoint /
|
|
1118
|
-
// declined_by_review), reconciling to the original count. Path B (no persisted
|
|
1119
|
-
// dispositions): build over the post-pipeline node findings as before. Either
|
|
1120
|
-
// way declined findings are recorded; their payloads recover at close from the
|
|
1121
|
-
// unfiltered intake source.
|
|
1122
|
-
const filterDisp = await readOptionalJsonFile(reviewFilterDispositionsPath(artifactsDir));
|
|
1123
|
-
const pipelinedBlockIds = blockIdsByFinding(pipelined);
|
|
1124
|
-
const coverage = filterDisp
|
|
1125
|
-
? buildCoverageLedger({
|
|
1126
|
-
planId: pipelined.plan_id,
|
|
1127
|
-
sourceFindings: filterDisp.originals,
|
|
1128
|
-
droppedNoEvidence: filterDisp.droppedNoEvidence,
|
|
1129
|
-
droppedByCheckpoint: filterDisp.droppedByCheckpoint,
|
|
1130
|
-
declinedByReview: reviewDecision?.declined ?? [],
|
|
1131
|
-
droppedPhantomPaths: new Map(filterDisp.droppedPhantomPaths),
|
|
1132
|
-
phantomPathsRemoved: new Map(filterDisp.phantomPathsRemoved),
|
|
1133
|
-
mergeMap: new Map(filterDisp.mergeMap),
|
|
1134
|
-
items: {}, // originals carry no node block_id; planned entries omit it
|
|
1135
|
-
})
|
|
1136
|
-
: buildCoverageLedger({
|
|
1137
|
-
planId: pipelined.plan_id,
|
|
1138
|
-
sourceFindings,
|
|
1139
|
-
droppedNoEvidence: [],
|
|
1140
|
-
droppedByCheckpoint: [],
|
|
1141
|
-
declinedByReview: reviewDecision?.declined ?? [],
|
|
1142
|
-
droppedPhantomPaths: new Map(grounding.dropped.map((d) => [d.finding.id, d.phantomPaths])),
|
|
1143
|
-
phantomPathsRemoved: grounding.phantomPathsByFinding,
|
|
1144
|
-
mergeMap,
|
|
1145
|
-
items: Object.fromEntries(pipelined.findings.map((finding) => [
|
|
1146
|
-
finding.id,
|
|
1147
|
-
{
|
|
1148
|
-
finding_id: finding.id,
|
|
1149
|
-
status: "pending",
|
|
1150
|
-
block_id: pipelinedBlockIds.get(finding.id) ?? "UNKNOWN",
|
|
1151
|
-
},
|
|
1152
|
-
])),
|
|
1153
|
-
});
|
|
1154
|
-
return {
|
|
1155
|
-
kind: "planned",
|
|
1156
|
-
state: await saveStateForPlan(artifactsDir, existing, pipelined, coverage),
|
|
695
|
+
async function emitPlanRevisionBlockedStep(root, artifactsDir, blocked) {
|
|
696
|
+
return writeCurrentStep({ stepKind: "contract_pipeline", status: "blocked", runId: null, repoRoot: root, artifactsDir,
|
|
697
|
+
prompt: `# Executable plan requires attention\n\n${blocked.reason}\nThe current plan and accepted execution history are preserved. Revise the plan through its tool-issued submission and obtain fresh review before continuing.`,
|
|
698
|
+
allowedCommands: [loaderCommand("next-step")], stopCondition: "Resolve the named plan/review issue before implementation.", artifactPaths: { execution_plan: executionPlanPaths(artifactsDir).canonical } });
|
|
699
|
+
}
|
|
700
|
+
async function activateApprovedPlan(root, artifactsDir, existing, _input, _runLogger) {
|
|
701
|
+
const approved = await readApprovedExecutionPlan(artifactsDir);
|
|
702
|
+
if (!approved)
|
|
703
|
+
return { kind: "blocked", reason: "No current independently approved executable plan exists. Re-run planning; unreviewed projections cannot dispatch." };
|
|
704
|
+
const facts = (await readProjectFacts(artifactsDir)) ?? neutralProjectFacts();
|
|
705
|
+
const { canonical, source } = approved;
|
|
706
|
+
let plan = {
|
|
707
|
+
...canonical.plan,
|
|
708
|
+
source: "execution_plan",
|
|
709
|
+
findings: source.findings,
|
|
710
|
+
...(source.request ? { request: source.request } : {}),
|
|
711
|
+
review_revision_sha256: canonical.revision_sha256,
|
|
712
|
+
review_counterexamples: (await readPlanReviewHistory(artifactsDir)).counterexamples.filter(example => canonical.plan.units.some(unit => unit.addresses_counterexample_ids.includes(example.id))),
|
|
713
|
+
project_type: facts.project_type,
|
|
714
|
+
candidate_closing_actions: facts.candidate_closing_actions,
|
|
715
|
+
audit_read: source.audit_read,
|
|
716
|
+
...(facts.commands.test ? { test_command: renderPromptCommand(facts.commands.test), test_command_source: "project_facts" } : {}),
|
|
717
|
+
...(facts.commands.e2e ? { e2e_command: renderPromptCommand(facts.commands.e2e), e2e_command_source: "project_facts" } : {}),
|
|
1157
718
|
};
|
|
719
|
+
plan = await applyCheckpointIntentOrdering(artifactsDir, plan);
|
|
720
|
+
if (!existing.run_start_dirty)
|
|
721
|
+
existing = { ...existing, run_start_dirty: [...await stagedAndUntracked(root)].sort() };
|
|
722
|
+
const filter = await readOptionalJsonFile(reviewFilterDispositionsPath(artifactsDir));
|
|
723
|
+
const decision = await readOptionalJsonFile(reviewDecisionPath(artifactsDir));
|
|
724
|
+
const coverage = buildCoverageLedger({
|
|
725
|
+
planId: plan.plan_id, sourceFindings: filter?.originals ?? source.findings,
|
|
726
|
+
droppedNoEvidence: filter?.droppedNoEvidence ?? [], droppedByCheckpoint: filter?.droppedByCheckpoint ?? [],
|
|
727
|
+
declinedByReview: decision?.declined ?? [], droppedPhantomPaths: new Map(filter?.droppedPhantomPaths ?? []),
|
|
728
|
+
phantomPathsRemoved: new Map(filter?.phantomPathsRemoved ?? []), mergeMap: new Map(filter?.mergeMap ?? []), units: plan.units,
|
|
729
|
+
});
|
|
730
|
+
const state = await saveStateForPlan(artifactsDir, existing, plan, coverage);
|
|
731
|
+
return { kind: "planned", state };
|
|
1158
732
|
}
|
|
1159
733
|
// ── Review-approval gate (go-forward program item 1) ───────────────────────────
|
|
1160
734
|
//
|
|
@@ -1215,17 +789,6 @@ async function archiveConsumedInputs(paths) {
|
|
|
1215
789
|
}
|
|
1216
790
|
}
|
|
1217
791
|
}
|
|
1218
|
-
// Up-front ambiguity gate (note 3, part A) — its own request/resolution/decision
|
|
1219
|
-
// files, mirroring the review gate so it fires (and halts) at most once per run.
|
|
1220
|
-
function ambiguityRequestPath(artifactsDir) {
|
|
1221
|
-
return join(artifactsDir, "ambiguity_request.json");
|
|
1222
|
-
}
|
|
1223
|
-
function ambiguityResolutionPath(artifactsDir) {
|
|
1224
|
-
return join(artifactsDir, "ambiguity_resolution.json");
|
|
1225
|
-
}
|
|
1226
|
-
function ambiguityDecisionPath(artifactsDir) {
|
|
1227
|
-
return join(artifactsDir, "ambiguity_decision.json");
|
|
1228
|
-
}
|
|
1229
792
|
/** Pull the Finding[] out of a parsed audit-findings.json payload. */
|
|
1230
793
|
function extractAuditFindings(parsed) {
|
|
1231
794
|
if (isRecord(parsed) && Array.isArray(parsed.findings)) {
|
|
@@ -1288,7 +851,7 @@ sourceAuditRead, autonomous = false) {
|
|
|
1288
851
|
// First crossing only: no decision yet AND the pipeline has not started.
|
|
1289
852
|
const gateOpen = survivors.length > 0 &&
|
|
1290
853
|
!existsSync(decisionPath) &&
|
|
1291
|
-
!
|
|
854
|
+
!existsSync(executionPlanPaths(artifactsDir).canonical);
|
|
1292
855
|
// Autonomous (unattended) mode: the gate NEVER halts. It re-evaluates the
|
|
1293
856
|
// survivors FRESH (no prior-run memory) and auto-approves only tier-safe +
|
|
1294
857
|
// allowlisted-change-kind findings; everything else is left LIVE. Leftovers
|
|
@@ -1416,15 +979,26 @@ sourceAuditRead) {
|
|
|
1416
979
|
});
|
|
1417
980
|
}
|
|
1418
981
|
async function handleReadyIntakeContractPipeline(root, artifactsDir, options, runLogger) {
|
|
1419
|
-
//
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
982
|
+
// Resume the tool-owned plan without reconstructing completed intake.
|
|
983
|
+
const earlyExtractedPlan = await readApprovedExecutionPlan(artifactsDir);
|
|
984
|
+
if (earlyExtractedPlan && !existsSync(executionPlanPaths(artifactsDir).submission)) {
|
|
985
|
+
const outcome = await activateApprovedPlan(root, artifactsDir, { status: "pending" }, earlyExtractedPlan, runLogger);
|
|
986
|
+
return outcome.kind === "blocked"
|
|
987
|
+
? emitPlanRevisionBlockedStep(root, artifactsDir, outcome)
|
|
988
|
+
: outcome.state;
|
|
989
|
+
}
|
|
990
|
+
if (await readPlanSource(artifactsDir)) {
|
|
991
|
+
const step = await buildNextContractPipelineStep({
|
|
992
|
+
root, artifactsDir, runId: randomRunId("CONTRACT"),
|
|
993
|
+
});
|
|
994
|
+
if (step)
|
|
995
|
+
return step;
|
|
996
|
+
const approved = await readApprovedExecutionPlan(artifactsDir);
|
|
997
|
+
if (!approved)
|
|
998
|
+
return null;
|
|
999
|
+
const outcome = await activateApprovedPlan(root, artifactsDir, { status: "pending" }, approved, runLogger);
|
|
1000
|
+
return outcome.kind === "blocked"
|
|
1001
|
+
? emitPlanRevisionBlockedStep(root, artifactsDir, outcome)
|
|
1428
1002
|
: outcome.state;
|
|
1429
1003
|
}
|
|
1430
1004
|
const intake = await readIntakeArtifacts(artifactsDir);
|
|
@@ -1476,10 +1050,6 @@ async function handleReadyIntakeContractPipeline(root, artifactsDir, options, ru
|
|
|
1476
1050
|
affectedFiles.push(...distinctAffectedFiles(extractAuditFindings(parsed)));
|
|
1477
1051
|
return { affectedFiles, goals: summary.goals };
|
|
1478
1052
|
});
|
|
1479
|
-
const pipeline = shouldEnterContractPipeline(artifactsDir, intake.summary.source_type);
|
|
1480
|
-
if (!pipeline.shouldHandleContractPipeline) {
|
|
1481
|
-
return null;
|
|
1482
|
-
}
|
|
1483
1053
|
const canonicalIntent = sessionIntentResult(options).intent;
|
|
1484
1054
|
// Path A: run the single filter pass over the ORIGINAL findings, present the
|
|
1485
1055
|
// SURVIVORS at the review gate (deduped / evidence-bearing / path-grounded /
|
|
@@ -1493,7 +1063,7 @@ async function handleReadyIntakeContractPipeline(root, artifactsDir, options, ru
|
|
|
1493
1063
|
const originals = extractAuditFindings(auditFindings);
|
|
1494
1064
|
if (originals.length > 0) {
|
|
1495
1065
|
const checkpoint = await readIntentCheckpoint(join(artifactsDir, "intent_checkpoint.json"));
|
|
1496
|
-
const filter = await runFindingFilterPass(originals, {
|
|
1066
|
+
const filter = await runFindingFilterPass(structuredClone(originals), {
|
|
1497
1067
|
root,
|
|
1498
1068
|
checkpoint: checkpoint ?? undefined,
|
|
1499
1069
|
evidenceGrounding: true,
|
|
@@ -1512,8 +1082,8 @@ async function handleReadyIntakeContractPipeline(root, artifactsDir, options, ru
|
|
|
1512
1082
|
// intake path/breadth/intent signal doesn't see) INTO the shared risk signal as
|
|
1513
1083
|
// escalate-on-evidence. The tier is the SINGLE classifier and the ONLY thing it
|
|
1514
1084
|
// selects is DEPTH: every run enters the contract pipeline, and a `low` tier
|
|
1515
|
-
//
|
|
1516
|
-
//
|
|
1085
|
+
// receives light adversarial depth. There is no second plan producer or
|
|
1086
|
+
// bypass that could disagree with this risk authority.
|
|
1517
1087
|
const findingEvidence = findingRiskEvidence(gate.approved);
|
|
1518
1088
|
let riskSignal = await readIntakeRiskSignal(artifactsDir);
|
|
1519
1089
|
if (findingEvidence && riskSignal) {
|
|
@@ -1529,17 +1099,28 @@ async function handleReadyIntakeContractPipeline(root, artifactsDir, options, ru
|
|
|
1529
1099
|
// narrower than the originals (anything filtered or declined), route the
|
|
1530
1100
|
// seed AND the pipeline's source inputs at a filtered file so a removed
|
|
1531
1101
|
// finding can never re-enter via the raw audit-findings.json (tool-enforced).
|
|
1532
|
-
|
|
1102
|
+
// Filtering annotates its working copies; source provenance stays byte-faithful.
|
|
1103
|
+
// Selection and risk use the filter's facts, while the plan owns original claims.
|
|
1104
|
+
const originalsById = new Map(originals.map(finding => [finding.id, finding]));
|
|
1105
|
+
const approvedOriginals = gate.approved.map(finding => {
|
|
1106
|
+
const original = originalsById.get(finding.id);
|
|
1107
|
+
if (!original)
|
|
1108
|
+
throw new Error(`Approved finding ${finding.id} has no original source payload.`);
|
|
1109
|
+
return original;
|
|
1110
|
+
});
|
|
1111
|
+
const approvedPayload = projectAuditFindingsReportSubset(auditFindings, approvedOriginals);
|
|
1533
1112
|
let seedSourcePath = auditSource.path;
|
|
1534
1113
|
if (gate.approved.length < originals.length) {
|
|
1535
|
-
await mkdir(
|
|
1536
|
-
seedSourcePath = join(
|
|
1114
|
+
await mkdir(executionPlanPaths(artifactsDir).directory, { recursive: true });
|
|
1115
|
+
seedSourcePath = join(executionPlanPaths(artifactsDir).directory, "approved-findings.json");
|
|
1537
1116
|
await writeJsonFile(seedSourcePath, approvedPayload);
|
|
1538
1117
|
reviewSourceSwap = { from: auditSource.path, to: seedSourcePath };
|
|
1539
1118
|
}
|
|
1540
1119
|
await writePathASeedFromFindings(artifactsDir, seedSourcePath, approvedPayload);
|
|
1541
1120
|
}
|
|
1542
1121
|
}
|
|
1122
|
+
if (auditSource && !await readPlanSource(artifactsDir))
|
|
1123
|
+
await writePathASeedFromFindings(artifactsDir, auditSource.path, await readAuditFindingsOnce());
|
|
1543
1124
|
const paths = intakePaths(artifactsDir);
|
|
1544
1125
|
const sourcePaths = new Set();
|
|
1545
1126
|
// The brief is the tool's render of the ready summary; write it before it
|
|
@@ -1566,22 +1147,19 @@ async function handleReadyIntakeContractPipeline(root, artifactsDir, options, ru
|
|
|
1566
1147
|
if (step) {
|
|
1567
1148
|
return step;
|
|
1568
1149
|
}
|
|
1569
|
-
const extractedPlan = await
|
|
1150
|
+
const extractedPlan = await readApprovedExecutionPlan(artifactsDir);
|
|
1570
1151
|
if (!extractedPlan) {
|
|
1571
1152
|
return null;
|
|
1572
1153
|
}
|
|
1573
|
-
const outcome = await
|
|
1574
|
-
return outcome.kind === "
|
|
1575
|
-
?
|
|
1154
|
+
const outcome = await activateApprovedPlan(root, artifactsDir, { status: "pending" }, extractedPlan, runLogger);
|
|
1155
|
+
return outcome.kind === "blocked"
|
|
1156
|
+
? emitPlanRevisionBlockedStep(root, artifactsDir, outcome)
|
|
1576
1157
|
: outcome.state;
|
|
1577
1158
|
}
|
|
1578
1159
|
async function handlePendingIntake(root, artifactsDir, options, runLogger) {
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1581
|
-
|
|
1582
|
-
// full intake artifact set is no longer present.
|
|
1583
|
-
const earlyExtractedPlan = await readExtractedPlanIfPresent(artifactsDir);
|
|
1584
|
-
if (earlyExtractedPlan) {
|
|
1160
|
+
// Existing source ownership survives pending review and approval invalidation.
|
|
1161
|
+
// The earlier confirm_intent obligation still guards this continuation.
|
|
1162
|
+
if (await readPlanSource(artifactsDir)) {
|
|
1585
1163
|
return handleReadyIntakeContractPipeline(root, artifactsDir, options, runLogger);
|
|
1586
1164
|
}
|
|
1587
1165
|
const inputResolution = resolveInputPaths(root, options.input);
|
|
@@ -1686,10 +1264,10 @@ const PLAN_CLARIFICATION_ACTIONS = ["clarified", "reject_finding", "defer"];
|
|
|
1686
1264
|
* - `rationale` is REQUIRED and non-empty on `clarified`: it becomes the item's
|
|
1687
1265
|
* `clarification_context`, the answer the next worker reads. A `clarified`
|
|
1688
1266
|
* entry without it re-opened the item with no answer attached.
|
|
1689
|
-
* - `scope_additions` lists the files the answer
|
|
1690
|
-
*
|
|
1691
|
-
*
|
|
1692
|
-
*
|
|
1267
|
+
* - `scope_additions` lists the files the answer requires the fix to touch. Each
|
|
1268
|
+
* must already be in the owning unit's reviewed `allowed_files`; a path outside
|
|
1269
|
+
* it is refused, because new write scope needs a revised, freshly reviewed plan.
|
|
1270
|
+
* Accepted entries never rewrite the plan, so its approved revision holds. It is allowed
|
|
1693
1271
|
* ONLY on `clarified`, the one action that re-opens the item; on
|
|
1694
1272
|
* `reject_finding` or `defer` it used to be ignored without a word. Each path
|
|
1695
1273
|
* is validated whole-file fail-closed BEFORE anything is applied
|
|
@@ -1698,7 +1276,7 @@ const PLAN_CLARIFICATION_ACTIONS = ["clarified", "reject_finding", "defer"];
|
|
|
1698
1276
|
*/
|
|
1699
1277
|
const PlanClarificationResolutionSchema = z
|
|
1700
1278
|
.object({
|
|
1701
|
-
|
|
1279
|
+
unit_id: z.string().min(1, "must be a non-empty finding id"),
|
|
1702
1280
|
action: z.enum(PLAN_CLARIFICATION_ACTIONS),
|
|
1703
1281
|
rationale: z.string().optional(),
|
|
1704
1282
|
scope_additions: z.array(z.string()).optional(),
|
|
@@ -1721,9 +1299,7 @@ const PlanClarificationResolutionSchema = z
|
|
|
1721
1299
|
}
|
|
1722
1300
|
});
|
|
1723
1301
|
/**
|
|
1724
|
-
* Read and validate
|
|
1725
|
-
* `clarification_resolution.json` and the up-front `ambiguity_resolution.json`
|
|
1726
|
-
* share this one parser.
|
|
1302
|
+
* Read and validate the mid-run `clarification_resolution.json` file.
|
|
1727
1303
|
*
|
|
1728
1304
|
* ONE shape is accepted: a bare JSON array of entries. Every entry is checked
|
|
1729
1305
|
* against {@link PlanClarificationResolutionSchema}, and a second entry for the
|
|
@@ -1763,12 +1339,12 @@ async function readPlanClarificationResolutions(path) {
|
|
|
1763
1339
|
}
|
|
1764
1340
|
return;
|
|
1765
1341
|
}
|
|
1766
|
-
const prior = firstEntryFor.get(parsed.data.
|
|
1342
|
+
const prior = firstEntryFor.get(parsed.data.unit_id);
|
|
1767
1343
|
if (prior !== undefined) {
|
|
1768
|
-
problems.push(`entry [${index}] \`
|
|
1344
|
+
problems.push(`entry [${index}] \`unit_id\`: \`${parsed.data.unit_id}\` is already answered by entry [${prior}]`);
|
|
1769
1345
|
return;
|
|
1770
1346
|
}
|
|
1771
|
-
firstEntryFor.set(parsed.data.
|
|
1347
|
+
firstEntryFor.set(parsed.data.unit_id, index);
|
|
1772
1348
|
resolutions.push(parsed.data);
|
|
1773
1349
|
});
|
|
1774
1350
|
return problems.length > 0
|
|
@@ -1784,7 +1360,7 @@ async function planClarificationRefusal(root, state, parsed, validIds, outsideSe
|
|
|
1784
1360
|
if (!parsed.ok)
|
|
1785
1361
|
return parsed.reason;
|
|
1786
1362
|
const unknownIds = parsed.resolutions
|
|
1787
|
-
.map((r) => r.
|
|
1363
|
+
.map((r) => r.unit_id)
|
|
1788
1364
|
.filter((id) => !validIds.has(id));
|
|
1789
1365
|
if (unknownIds.length > 0) {
|
|
1790
1366
|
return `finding id(s) ${outsideSetLabel}: ${unknownIds.map((i) => `\`${i}\``).join(", ")}`;
|
|
@@ -1809,42 +1385,23 @@ async function validateClarificationScopeAdditions(root, state, resolutions) {
|
|
|
1809
1385
|
for (const res of resolutions) {
|
|
1810
1386
|
if (res.action !== "clarified" || !res.scope_additions?.length)
|
|
1811
1387
|
continue;
|
|
1812
|
-
const block = state.plan?.
|
|
1388
|
+
const block = state.plan?.units?.find((b) => b.id === res.unit_id);
|
|
1813
1389
|
if (!block) {
|
|
1814
|
-
refusals.push(`scope_additions for \`${res.
|
|
1390
|
+
refusals.push(`scope_additions for \`${res.unit_id}\`: no plan block owns this finding, so ` +
|
|
1815
1391
|
`there is no write scope to widen.`);
|
|
1816
1392
|
continue;
|
|
1817
1393
|
}
|
|
1818
|
-
const normalized = normalizeBlockTouchedFiles(root, res.scope_additions, block.
|
|
1394
|
+
const normalized = normalizeBlockTouchedFiles(root, res.scope_additions, block.id);
|
|
1395
|
+
if (normalized.touched_files.some(path => !block.allowed_files.includes(path)))
|
|
1396
|
+
refusals.push(`Unit ${block.id} needs a revised executable plan and fresh review before its write scope can expand.`);
|
|
1819
1397
|
refusals.push(...normalized.refusals);
|
|
1820
|
-
refusals.push(...(await checkWriteScopePathsAgainstTrackedTree(root, normalized.touched_files, `scope_additions for \`${res.
|
|
1398
|
+
refusals.push(...(await checkWriteScopePathsAgainstTrackedTree(root, normalized.touched_files, `scope_additions for \`${res.unit_id}\` (block "${block.id}")`)));
|
|
1821
1399
|
}
|
|
1822
1400
|
return refusals;
|
|
1823
1401
|
}
|
|
1824
1402
|
/**
|
|
1825
|
-
* Apply one
|
|
1826
|
-
*
|
|
1827
|
-
* scope would churn the plan hash). The workload-binding invalidation is the
|
|
1828
|
-
* caller's existing `delete state.host_handoff` on any applied resolution, so
|
|
1829
|
-
* the next prepare re-mints the work item with the widened `allowed_files`
|
|
1830
|
-
* (the open-bugs :661 wedge class); the up-front ambiguity gate has no binding
|
|
1831
|
-
* by construction.
|
|
1832
|
-
*/
|
|
1833
|
-
function applyClarificationScopeAdditions(root, state, res) {
|
|
1834
|
-
if (res.action !== "clarified" || !res.scope_additions?.length)
|
|
1835
|
-
return;
|
|
1836
|
-
const block = state.plan?.blocks?.find((b) => b.items.includes(res.finding_id));
|
|
1837
|
-
if (!block)
|
|
1838
|
-
return; // refused upstream by validateClarificationScopeAdditions
|
|
1839
|
-
const normalized = normalizeBlockTouchedFiles(root, res.scope_additions, block.block_id);
|
|
1840
|
-
block.touched_files = [
|
|
1841
|
-
...new Set([...block.touched_files, ...normalized.touched_files]),
|
|
1842
|
-
].sort((left, right) => compareCodeUnits(left, right));
|
|
1843
|
-
}
|
|
1844
|
-
/**
|
|
1845
|
-
* Apply one clarification resolution to its item. Single-sourced so the up-front
|
|
1846
|
-
* ambiguity gate (part A) and the mid-run clarification round (part B) settle an
|
|
1847
|
-
* item identically: `clarified` re-opens it (pending) with the answer as context,
|
|
1403
|
+
* Apply one mid-run clarification resolution to its execution unit:
|
|
1404
|
+
* `clarified` re-opens it (pending) with the answer as context,
|
|
1848
1405
|
* `reject_finding` closes it as not-a-real-issue (terminal `deemed_inappropriate`
|
|
1849
1406
|
* disposition), and `defer` closes it as an explicit user deferral for this run.
|
|
1850
1407
|
* Never resurrects a terminal item.
|
|
@@ -1887,7 +1444,7 @@ async function applyPlanClarificationResolution(root, artifactsDir, state, store
|
|
|
1887
1444
|
// silent-continue alternative drops the user's answer and leaves its item
|
|
1888
1445
|
// waiting on a question nobody is asked, so a decision record never
|
|
1889
1446
|
// half-applies (open-bugs.md:110).
|
|
1890
|
-
const refusal = await planClarificationRefusal(root, state, parsed, new Set(pausedClarifications(state).map((q) => q.
|
|
1447
|
+
const refusal = await planClarificationRefusal(root, state, parsed, new Set(pausedClarifications(state).map((q) => q.unit_id)), "not waiting for a clarification");
|
|
1891
1448
|
if (refusal !== null || !parsed.ok) {
|
|
1892
1449
|
await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.refused-${Date.now()}`));
|
|
1893
1450
|
return {
|
|
@@ -1899,11 +1456,13 @@ async function applyPlanClarificationResolution(root, artifactsDir, state, store
|
|
|
1899
1456
|
const now = new Date().toISOString();
|
|
1900
1457
|
let appliedCount = 0;
|
|
1901
1458
|
for (const res of resolutions) {
|
|
1902
|
-
const item = state.items[res.
|
|
1459
|
+
const item = state.items[res.unit_id];
|
|
1903
1460
|
if (!item || isTerminalStatus(item.status))
|
|
1904
1461
|
continue;
|
|
1462
|
+
// Validation admits only paths already in the reviewed unit's allowed_files, so an
|
|
1463
|
+
// accepted scope_additions entry changes nothing: the reviewed plan is never rewritten
|
|
1464
|
+
// here, and its revision (which hashes author order) stays the approved one.
|
|
1905
1465
|
applyClarificationActionToItem(item, res, now);
|
|
1906
|
-
applyClarificationScopeAdditions(root, state, res);
|
|
1907
1466
|
appliedCount += 1;
|
|
1908
1467
|
}
|
|
1909
1468
|
// An applied answer mutates item state that is baked into the dispatch
|
|
@@ -1916,7 +1475,7 @@ async function applyPlanClarificationResolution(root, artifactsDir, state, store
|
|
|
1916
1475
|
if (existsSync(resolutionPath)) {
|
|
1917
1476
|
await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.consumed-${Date.now()}`));
|
|
1918
1477
|
}
|
|
1919
|
-
const remainingPending = state.plan.
|
|
1478
|
+
const remainingPending = state.plan.units.some((f) => state.items?.[f.id]?.status === "pending");
|
|
1920
1479
|
// Undecided-remainder guard, mirroring triage's still-blocked guard: a
|
|
1921
1480
|
// resolution covering only SOME of the paused items must not fall through to
|
|
1922
1481
|
// closing, which would force-close the undecided ones as `abandoned` and drop
|
|
@@ -1948,9 +1507,9 @@ async function applyPlanClarificationResolution(root, artifactsDir, state, store
|
|
|
1948
1507
|
function pausedClarifications(state) {
|
|
1949
1508
|
return Object.values(state.items ?? {})
|
|
1950
1509
|
.filter((item) => item.status === "needs_clarification")
|
|
1951
|
-
.sort((left, right) => compareCodeUnits(left.
|
|
1510
|
+
.sort((left, right) => compareCodeUnits(left.unit_id, right.unit_id))
|
|
1952
1511
|
.map((item) => ({
|
|
1953
|
-
|
|
1512
|
+
unit_id: item.unit_id,
|
|
1954
1513
|
// The state store refuses a paused item without a question, so the
|
|
1955
1514
|
// fallback is unreachable from a stored state; it keeps the render total.
|
|
1956
1515
|
...(item.clarification_question ?? {
|
|
@@ -2008,351 +1567,31 @@ async function handleWaitingForTriage(root, artifactsDir, state) {
|
|
|
2008
1567
|
* Returns a halt step while awaiting the user's decision, or null to proceed
|
|
2009
1568
|
* (decision recorded, any declined nodes marked terminal).
|
|
2010
1569
|
*/
|
|
2011
|
-
async function runPlanningReviewGate(root, artifactsDir, state, store) {
|
|
2012
|
-
const findings = state.plan?.findings ?? [];
|
|
2013
|
-
if (findings.length === 0)
|
|
2014
|
-
return null;
|
|
2015
|
-
const requestPath = reviewRequestPath(artifactsDir);
|
|
2016
|
-
const resolutionPath = reviewResolutionPath(artifactsDir);
|
|
2017
|
-
const decisionPath = reviewDecisionPath(artifactsDir);
|
|
2018
|
-
// Path B correlates on the LIVE plan's own id (INV-RSM-RESOLUTION-CORRELATE):
|
|
2019
|
-
// the plan exists at the planning point, so its plan_id is the natural
|
|
2020
|
-
// run-unique key — a stale resolution from an earlier plan can never match.
|
|
2021
|
-
const reviewPlanId = state.plan?.plan_id ?? randomRunId("path-b-review");
|
|
2022
|
-
if (!existsSync(resolutionPath)) {
|
|
2023
|
-
// Halt: present the tiered node findings and wait for the user's decision.
|
|
2024
|
-
const request = buildReviewRequest(findings, reviewPlanId);
|
|
2025
|
-
await writeJsonFile(requestPath, request);
|
|
2026
|
-
return handleWaitingForReviewApproval(root, artifactsDir, request);
|
|
2027
|
-
}
|
|
2028
|
-
// Resolution present: consume it into a durable, reasoned decision record.
|
|
2029
|
-
// Regenerable: a stale-schema request is treated as absent and rebuilt below.
|
|
2030
|
-
const request = discardOnSchemaVersionMismatch(await readOptionalJsonFile(requestPath), REVIEW_REQUEST_SCHEMA_VERSION) ?? buildReviewRequest(findings, reviewPlanId);
|
|
2031
|
-
const read = await readReviewResolution(root, artifactsDir, request, resolutionPath, requestPath);
|
|
2032
|
-
if ("halt" in read)
|
|
2033
|
-
return read.halt;
|
|
2034
|
-
const decision = applyReviewResolution(request, read.resolution);
|
|
2035
|
-
await writeReviewDecisionRecord(decisionPath, {
|
|
2036
|
-
planId: request.plan_id,
|
|
2037
|
-
approvedIds: decision.approved_ids,
|
|
2038
|
-
declined: decision.declined,
|
|
2039
|
-
});
|
|
2040
|
-
await archiveConsumedInputs([resolutionPath, requestPath]);
|
|
2041
|
-
// Declined nodes → recorded terminal disposition (never a silent close).
|
|
2042
|
-
let changed = false;
|
|
2043
|
-
for (const { finding_id, reason } of decision.declined) {
|
|
2044
|
-
const it = state.items?.[finding_id];
|
|
2045
|
-
if (it && !isTerminalStatus(it.status)) {
|
|
2046
|
-
const now = new Date().toISOString();
|
|
2047
|
-
it.status = "ignored";
|
|
2048
|
-
it.failure_reason = reason;
|
|
2049
|
-
it.started_at ??= now;
|
|
2050
|
-
it.completed_at = now;
|
|
2051
|
-
changed = true;
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
if (changed)
|
|
2055
|
-
await store.saveState(state);
|
|
2056
|
-
return null;
|
|
2057
|
-
}
|
|
2058
|
-
/**
|
|
2059
|
-
* Deterministic first pass (note 3, part A): scan the plan's non-terminal
|
|
2060
|
-
* findings for scoping/judgment ambiguity, classified into the canonical
|
|
2061
|
-
* clarification categories. These are CANDIDATES — the host reviews them against
|
|
2062
|
-
* the repo, dismisses false positives, and adds any it finds, before batching one
|
|
2063
|
-
* user round. Conservative by design: a candidate the host dismisses costs one
|
|
2064
|
-
* read; a real scoping question that falls silently to mid-run triage is the bug
|
|
2065
|
-
* this gate exists to prevent.
|
|
2066
|
-
*/
|
|
2067
|
-
function detectPlanAmbiguities(findings, items) {
|
|
2068
|
-
const out = [];
|
|
2069
|
-
for (const f of findings) {
|
|
2070
|
-
const item = items?.[f.id];
|
|
2071
|
-
if (item && isTerminalStatus(item.status))
|
|
2072
|
-
continue;
|
|
2073
|
-
const lens = (f.lens ?? "").toLowerCase();
|
|
2074
|
-
const fileCount = f.affected_files?.length ?? 0;
|
|
2075
|
-
const broadScope = (lens === "architecture" || lens === "maintainability") &&
|
|
2076
|
-
(fileCount === 0 || fileCount >= 5);
|
|
2077
|
-
if (broadScope) {
|
|
2078
|
-
out.push({
|
|
2079
|
-
finding_id: f.id,
|
|
2080
|
-
category: "scope_of_fix",
|
|
2081
|
-
description: `"${f.title}" is a finding in the ${lens} lens with ${fileCount === 0 ? "no cited files" : `${fileCount} affected files`}. ` +
|
|
2082
|
-
"Confirm how far the fix should reach: a minimal local change, or a broader restructure.",
|
|
2083
|
-
});
|
|
2084
|
-
continue;
|
|
2085
|
-
}
|
|
2086
|
-
if (f.confidence === "low") {
|
|
2087
|
-
out.push({
|
|
2088
|
-
finding_id: f.id,
|
|
2089
|
-
category: "issue_appropriateness",
|
|
2090
|
-
description: `"${f.title}" is a low-confidence finding. Confirm that it is a real issue to fix in this run.`,
|
|
2091
|
-
});
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
return out;
|
|
2095
|
-
}
|
|
2096
|
-
/**
|
|
2097
|
-
* Up-front ambiguity gate (note 3, part A). Mirrors {@link runPlanningReviewGate}:
|
|
2098
|
-
* it fires once at planning, BEFORE any implement dispatch, so scoping/judgment
|
|
2099
|
-
* ambiguity is asked as a single batched question up front rather than falling
|
|
2100
|
-
* silently to triage mid-run. Deterministic heuristics seed CANDIDATES; the host
|
|
2101
|
-
* reviews them with repo access, dismisses/adds, and batches one user round. Each
|
|
2102
|
-
* item is resolved as `clarified` (answered → re-opened), `reject_finding`
|
|
2103
|
-
* (not a real issue → `deemed_inappropriate`), or `defer` (the user's explicit choice to skip this run).
|
|
2104
|
-
*
|
|
2105
|
-
* Idempotent: once `ambiguity_decision.json` exists the gate is done and never
|
|
2106
|
-
* re-halts. An empty resolution proceeds (the host found nothing to ask).
|
|
2107
|
-
*/
|
|
2108
|
-
async function runPlanAmbiguityGate(root, artifactsDir, state, store) {
|
|
2109
|
-
const findings = state.plan?.findings ?? [];
|
|
2110
|
-
if (findings.length === 0)
|
|
2111
|
-
return null;
|
|
2112
|
-
const requestPath = ambiguityRequestPath(artifactsDir);
|
|
2113
|
-
const resolutionPath = ambiguityResolutionPath(artifactsDir);
|
|
2114
|
-
const decisionPath = ambiguityDecisionPath(artifactsDir);
|
|
2115
|
-
if (!existsSync(resolutionPath)) {
|
|
2116
|
-
// Deterministic detection is the gate trigger: with zero candidates there is
|
|
2117
|
-
// nothing for the host to review, so the plan proceeds without a round. Any
|
|
2118
|
-
// ambiguity the heuristics miss is still caught by the mid-run escape hatch
|
|
2119
|
-
// (part B). When candidates exist, halt for the host's review + the user's
|
|
2120
|
-
// batched answers.
|
|
2121
|
-
const candidates = detectPlanAmbiguities(findings, state.items);
|
|
2122
|
-
if (candidates.length === 0)
|
|
2123
|
-
return null;
|
|
2124
|
-
await writeJsonFile(requestPath, { candidates, findings });
|
|
2125
|
-
return writeCurrentStep({
|
|
2126
|
-
stepKind: "collect_clarifications",
|
|
2127
|
-
status: "blocked",
|
|
2128
|
-
runId: stateRunId(state),
|
|
2129
|
-
repoRoot: root,
|
|
2130
|
-
artifactsDir,
|
|
2131
|
-
prompt: ambiguityReviewPrompt(candidates, resolutionPath, findings.map((f) => f.id), undefined, requestPath),
|
|
2132
|
-
access: { read_paths: [requestPath, root], write_paths: [resolutionPath] },
|
|
2133
|
-
allowedCommands: [loaderCommand("next-step")],
|
|
2134
|
-
stopCondition: "Stop after reviewing the candidate ambiguities (and asking the user any genuine ones), unless the resolution is already written and the prompt told you to continue.",
|
|
2135
|
-
artifactPaths: {
|
|
2136
|
-
ambiguity_request: requestPath,
|
|
2137
|
-
ambiguity_resolution: resolutionPath,
|
|
2138
|
-
},
|
|
2139
|
-
});
|
|
2140
|
-
}
|
|
2141
|
-
// Resolution present: apply it to items, mark the gate done, archive inputs so
|
|
2142
|
-
// it cannot re-halt.
|
|
2143
|
-
// The same whole-file fail-closed contract as the mid-run round: a malformed
|
|
2144
|
-
// entry, a finding id outside the plan, or an invalid scope addition refuses
|
|
2145
|
-
// the WHOLE resolution (archived, nothing applied) and the gate re-halts with
|
|
2146
|
-
// the reason named — the silent-continue alternative drops a host answer,
|
|
2147
|
-
// leaving its item to fall to mid-run triage unexplained. At this up-front
|
|
2148
|
-
// gate no workload binding exists yet, so an applied scope widening simply
|
|
2149
|
-
// flows into the first dispatch.
|
|
2150
|
-
const parsed = await readPlanClarificationResolutions(resolutionPath);
|
|
2151
|
-
const refusal = await planClarificationRefusal(root, state, parsed, new Set(findings.map((f) => f.id)), "not in the plan");
|
|
2152
|
-
if (refusal !== null || !parsed.ok) {
|
|
2153
|
-
await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.refused-${Date.now()}`));
|
|
2154
|
-
const candidates = detectPlanAmbiguities(findings, state.items);
|
|
2155
|
-
await writeJsonFile(requestPath, { candidates, findings });
|
|
2156
|
-
return writeCurrentStep({
|
|
2157
|
-
stepKind: "collect_clarifications",
|
|
2158
|
-
status: "blocked",
|
|
2159
|
-
runId: stateRunId(state),
|
|
2160
|
-
repoRoot: root,
|
|
2161
|
-
artifactsDir,
|
|
2162
|
-
prompt: ambiguityReviewPrompt(candidates, resolutionPath, findings.map((f) => f.id), refusal ?? "the resolution could not be read", requestPath),
|
|
2163
|
-
access: { read_paths: [requestPath, root], write_paths: [resolutionPath] },
|
|
2164
|
-
allowedCommands: [loaderCommand("next-step")],
|
|
2165
|
-
stopCondition: "Stop after re-submitting a corrected ambiguity resolution, unless it is already written and the prompt told you to continue.",
|
|
2166
|
-
artifactPaths: {
|
|
2167
|
-
ambiguity_request: requestPath,
|
|
2168
|
-
ambiguity_resolution: resolutionPath,
|
|
2169
|
-
},
|
|
2170
|
-
});
|
|
2171
|
-
}
|
|
2172
|
-
const resolutions = parsed.resolutions;
|
|
2173
|
-
const now = new Date().toISOString();
|
|
2174
|
-
let changed = false;
|
|
2175
|
-
for (const res of resolutions) {
|
|
2176
|
-
const item = state.items?.[res.finding_id];
|
|
2177
|
-
if (!item || isTerminalStatus(item.status))
|
|
2178
|
-
continue;
|
|
2179
|
-
applyClarificationActionToItem(item, res, now);
|
|
2180
|
-
applyClarificationScopeAdditions(root, state, res);
|
|
2181
|
-
changed = true;
|
|
2182
|
-
}
|
|
2183
|
-
await writeJsonFile(decisionPath, { resolved_at: now, resolution_count: resolutions.length });
|
|
2184
|
-
for (const p of [resolutionPath, requestPath]) {
|
|
2185
|
-
if (existsSync(p)) {
|
|
2186
|
-
await withFsRetry(() => rename(p, `${p}.consumed-${Date.now()}`));
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
if (changed)
|
|
2190
|
-
await store.saveState(state);
|
|
2191
|
-
return null;
|
|
2192
|
-
}
|
|
2193
1570
|
async function handlePlanning(root, artifactsDir, state, store) {
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
// (decision already written) never double-reviews. Declined nodes get a
|
|
2200
|
-
// recorded terminal disposition.
|
|
2201
|
-
if (state.plan && !existsSync(reviewDecisionPath(artifactsDir))) {
|
|
2202
|
-
const halt = await runPlanningReviewGate(root, artifactsDir, state, store);
|
|
2203
|
-
if (halt)
|
|
2204
|
-
return { kind: "emit", step: halt };
|
|
2205
|
-
}
|
|
2206
|
-
// Up-front ambiguity gate (note 3, part A): resolve every scoping/judgment
|
|
2207
|
-
// ambiguity in ONE batched round here, before any implement dispatch, so a
|
|
2208
|
-
// question never falls silently to mid-run triage. Fires at most once per run.
|
|
2209
|
-
if (state.plan && !existsSync(ambiguityDecisionPath(artifactsDir))) {
|
|
2210
|
-
const halt = await runPlanAmbiguityGate(root, artifactsDir, state, store);
|
|
2211
|
-
if (halt)
|
|
2212
|
-
return { kind: "emit", step: halt };
|
|
2213
|
-
}
|
|
2214
|
-
// Document phase dissolved: planning transitions directly to implementing,
|
|
2215
|
-
// and the host workload reads finding context directly.
|
|
2216
|
-
const implementBlocks = dispatchFrontier(state);
|
|
2217
|
-
if (implementBlocks.length > 0) {
|
|
2218
|
-
if (state.plan) {
|
|
2219
|
-
const integrity = await checkAffectedFileIntegrity(root, state.plan.findings);
|
|
2220
|
-
if (!integrity.is_clean) {
|
|
2221
|
-
const details = [
|
|
2222
|
-
...integrity.changed.map((p) => `changed: ${p}`),
|
|
2223
|
-
...integrity.missing.map((p) => `missing: ${p}`),
|
|
2224
|
-
...integrity.io_errors.map((p) => `io-error: ${p}`),
|
|
2225
|
-
];
|
|
2226
|
-
const replanCommand = loaderCommand("next-step --force-replan");
|
|
2227
|
-
return { kind: "emit", step: await writeCurrentStep({
|
|
2228
|
-
stepKind: "collect_starting_point",
|
|
2229
|
-
status: "blocked",
|
|
2230
|
-
runId: stateRunId(state),
|
|
2231
|
-
repoRoot: root,
|
|
2232
|
-
artifactsDir,
|
|
2233
|
-
prompt: [
|
|
2234
|
-
"## File integrity check failed",
|
|
2235
|
-
"",
|
|
2236
|
-
"The following files have changed since the remediation plan was created:",
|
|
2237
|
-
...details.map((d) => `- ${d}`),
|
|
2238
|
-
"",
|
|
2239
|
-
"Re-run planning to pick up the current file state before implementation begins.",
|
|
2240
|
-
"Run:",
|
|
2241
|
-
"",
|
|
2242
|
-
`\`${replanCommand}\``,
|
|
2243
|
-
].join("\n"),
|
|
2244
|
-
allowedCommands: [replanCommand],
|
|
2245
|
-
stopCondition: "Stop after re-planning completes.",
|
|
2246
|
-
}) };
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
1571
|
+
const approved = await readApprovedExecutionPlan(artifactsDir);
|
|
1572
|
+
if (!approved || approved.canonical.revision_sha256 !== state.plan?.review_revision_sha256) {
|
|
1573
|
+
return { kind: "emit", step: await writeCurrentStep({ stepKind: "contract_pipeline", status: "blocked", runId: stateRunId(state), repoRoot: root, artifactsDir,
|
|
1574
|
+
prompt: "The executable plan has changed since approval. Re-run planning and obtain fresh bound review before implementation; accepted execution history remains intact.",
|
|
1575
|
+
stopCondition: "Approve the current executable revision before dispatch." }) };
|
|
2249
1576
|
}
|
|
2250
|
-
|
|
2251
|
-
// Any pending item that can never enter the host dependency frontier is
|
|
2252
|
-
// dead-ended (INV-RS-01): a prerequisite was skipped/blocked, so its
|
|
2253
|
-
// verified-complete edge can never be satisfied — never dispatch a dependent
|
|
2254
|
-
// against an upstream surface that did not land. Mark it blocked so the run
|
|
2255
|
-
// advances to close rather than looping forever. A node that is merely
|
|
2256
|
-
// waiting on a still-running prerequisite — or on a clarification answer —
|
|
2257
|
-
// is NOT dead; the liveness analysis behind the sweep holds it pending.
|
|
2258
|
-
if (implementBlocks.length === 0) {
|
|
1577
|
+
if (!dispatchFrontier(state).length)
|
|
2259
1578
|
sweepPermanentlyDeadBlocks(state);
|
|
2260
|
-
}
|
|
2261
1579
|
state.status = "implementing";
|
|
2262
1580
|
await store.saveState(state);
|
|
2263
1581
|
return { kind: "transition", state };
|
|
2264
1582
|
}
|
|
2265
|
-
async function handleImplementing(root, artifactsDir, state, runLogger, store
|
|
1583
|
+
async function handleImplementing(root, artifactsDir, state, runLogger, store) {
|
|
2266
1584
|
const triageStart = Date.now();
|
|
2267
1585
|
runLogger.event({ phase: "next-step", kind: "executor_start", obligation: state.status, note: "triage" });
|
|
2268
1586
|
const triaged = await runTriagePhase(state, { root, artifactsDir });
|
|
2269
1587
|
runLogger.event({ phase: "next-step", kind: "executor_end", obligation: state.status, note: "triage", duration_ms: Date.now() - triageStart });
|
|
2270
|
-
//
|
|
2271
|
-
//
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
// `status: "closing"` directly would satisfy the closing obligation on the
|
|
2275
|
-
// next scan and preempt the all-terminal → closing funnel's tool-owned final
|
|
2276
|
-
// gate — exactly the defect class the finding named. So the intent is
|
|
2277
|
-
// persisted under the NON-closing `triage` status first (keeping every
|
|
2278
|
-
// preparation runTriagePhase made — halted items already converted to
|
|
2279
|
-
// `abandoned`, `closing_context` stamped — so a gate-RED pause, which persists
|
|
2280
|
-
// nothing itself, cannot lose them: re-entering triage with zero blocked items
|
|
2281
|
-
// re-derives this exact intent instead of auto-retrying halted work), and only
|
|
2282
|
-
// then handed to the funnel, which runs the gate and owns the closing stamp.
|
|
2283
|
-
if (triaged.status === "closing") {
|
|
2284
|
-
const prepared = { ...triaged, status: "triage" };
|
|
2285
|
-
await store.saveState(prepared);
|
|
2286
|
-
return handleAllTerminalTransition(root, artifactsDir, prepared, store, options, runLogger);
|
|
2287
|
-
}
|
|
1588
|
+
// Triage may enter closing directly: the close itself owns mandatory final
|
|
1589
|
+
// acceptance, so a status transition cannot bypass it.
|
|
1590
|
+
if (triaged.status === "closing")
|
|
1591
|
+
return handleAllTerminalTransition(triaged, store);
|
|
2288
1592
|
await store.saveState(triaged);
|
|
2289
1593
|
return { kind: "transition", state: triaged };
|
|
2290
1594
|
}
|
|
2291
|
-
function hasResolvedItems(state) {
|
|
2292
|
-
return Object.values(state.items ?? {}).some((it) => isVerifiedCompleteStatus(it.status));
|
|
2293
|
-
}
|
|
2294
|
-
/**
|
|
2295
|
-
* WHICH suppression disabled the tool-owned final gate (INV-RS-10) for this run —
|
|
2296
|
-
* `skipFinalGate` (test hermeticity) or `REMEDIATE_SKIP_FINAL_GATE` — or null when
|
|
2297
|
-
* it is live. Single-sourced so the all-terminal gate and the per-phase boundary
|
|
2298
|
-
* gate agree, and so both RECORD the same reason.
|
|
2299
|
-
*
|
|
2300
|
-
* The reason is carried into the outcome record rather than collapsed to a
|
|
2301
|
-
* boolean: the environment skip is the quietest not-run in the system (it needs
|
|
2302
|
-
* no option, no argument and no code change to fire), so a record that says only
|
|
2303
|
-
* "disabled" would leave an operator unable to tell a deliberate test-hermeticity
|
|
2304
|
-
* run from a stray exported variable.
|
|
2305
|
-
*/
|
|
2306
|
-
function finalGateDisabledReason(options) {
|
|
2307
|
-
if (options.skipFinalGate === true)
|
|
2308
|
-
return "skipFinalGate option";
|
|
2309
|
-
if (process.env.REMEDIATE_SKIP_FINAL_GATE === "1" ||
|
|
2310
|
-
process.env.REMEDIATE_SKIP_FINAL_GATE === "true") {
|
|
2311
|
-
return "REMEDIATE_SKIP_FINAL_GATE environment variable";
|
|
2312
|
-
}
|
|
2313
|
-
return null;
|
|
2314
|
-
}
|
|
2315
|
-
/**
|
|
2316
|
-
* The ONE way a gate evaluation is recorded, shared by BOTH gate families (the
|
|
2317
|
-
* phase-boundary gate and the all-terminal funnel), so neither can grow a second
|
|
2318
|
-
* executed/scoped-out/disabled vocabulary of its own.
|
|
2319
|
-
*
|
|
2320
|
-
* Writes the durable {@link writeFinalGateOutcomeRecord} artifact AND the run-log
|
|
2321
|
-
* event from the same values, so the two can never disagree about which of the
|
|
2322
|
-
* three happened.
|
|
2323
|
-
*
|
|
2324
|
-
* This is the affirmation the gate lacked. The floor's control flow was already
|
|
2325
|
-
* right — a scoped-out gate is deliberately NON-BLOCKING, a declared scope rather
|
|
2326
|
-
* than a vacuous pass — but the only thing either consumer wrote was
|
|
2327
|
-
* `passed=<bool>`, which is `true` for an executed green floor, for a scoped-out
|
|
2328
|
-
* target that ran nothing, and for a suppressed gate that was never reached. All
|
|
2329
|
-
* three produced byte-identical records, so "the suite passed" and "no suite ran"
|
|
2330
|
-
* were indistinguishable after the fact.
|
|
2331
|
-
*/
|
|
2332
|
-
async function recordFinalGateOutcome(ctx) {
|
|
2333
|
-
const verdict = carriesGateVerdict(ctx.outcome);
|
|
2334
|
-
await writeFinalGateOutcomeRecord(ctx.artifactsDir, {
|
|
2335
|
-
scope: ctx.scope,
|
|
2336
|
-
outcome: ctx.outcome,
|
|
2337
|
-
passed: ctx.passed,
|
|
2338
|
-
commands_run: ctx.commandsRun,
|
|
2339
|
-
...(ctx.reason === undefined ? {} : { reason: ctx.reason }),
|
|
2340
|
-
});
|
|
2341
|
-
ctx.runLogger.event({
|
|
2342
|
-
phase: "next-step",
|
|
2343
|
-
kind: "executor_end",
|
|
2344
|
-
obligation: ctx.state.status,
|
|
2345
|
-
note: `${ctx.gateKey} outcome=${ctx.outcome} ` +
|
|
2346
|
-
// "n/a", never "true": a gate that ran nothing has no verdict, and the
|
|
2347
|
-
// durable record it is written beside carries `passed: null` for the
|
|
2348
|
-
// same reason. A `history` outcome DOES carry a verdict — a judge ruled
|
|
2349
|
-
// on this exact tree — so it prints its verdict like an executed one.
|
|
2350
|
-
`passed=${verdict ? String(ctx.passed) : "n/a"} ` +
|
|
2351
|
-
`commands=${verdict ? String(ctx.commandsRun) : "0"}` +
|
|
2352
|
-
(ctx.reason === undefined ? "" : ` reason=${ctx.reason}`),
|
|
2353
|
-
...(ctx.durationMs === undefined ? {} : { duration_ms: ctx.durationMs }),
|
|
2354
|
-
});
|
|
2355
|
-
}
|
|
2356
1595
|
/**
|
|
2357
1596
|
* The ONE response to a red tool-owned gate, shared by both gates that run it.
|
|
2358
1597
|
*
|
|
@@ -2382,7 +1621,7 @@ async function recordFinalGateOutcome(ctx) {
|
|
|
2382
1621
|
async function emitFinalGateRedStep(ctx) {
|
|
2383
1622
|
const { root, artifactsDir, state, scope, gate, runLogger } = ctx;
|
|
2384
1623
|
const failed = gate.results.find((r) => !r.passed);
|
|
2385
|
-
const recordPath = await writeFinalGateRedRecord(artifactsDir, scope, failed, {
|
|
1624
|
+
const recordPath = ctx.recordedPath ?? await writeFinalGateRedRecord(artifactsDir, scope, failed, {
|
|
2386
1625
|
root,
|
|
2387
1626
|
state,
|
|
2388
1627
|
});
|
|
@@ -2559,7 +1798,7 @@ async function runPhaseBoundaryGate(ctx) {
|
|
|
2559
1798
|
runLogger,
|
|
2560
1799
|
outcome: "history",
|
|
2561
1800
|
passed: cached.passed,
|
|
2562
|
-
commandsRun: cached.results.length,
|
|
1801
|
+
commandsRun: cached.results.filter(result => result.ran !== false).length,
|
|
2563
1802
|
reason: `a verdict for this exact tree content is already recorded (scope "${cached.scope}", ` +
|
|
2564
1803
|
`recorded_at ${cached.recorded_at}); the floor was NOT re-run because the tree it ` +
|
|
2565
1804
|
"would run against has not changed",
|
|
@@ -2615,7 +1854,7 @@ async function runPhaseBoundaryGate(ctx) {
|
|
|
2615
1854
|
runLogger,
|
|
2616
1855
|
outcome: gate.outcome,
|
|
2617
1856
|
passed: gate.passed,
|
|
2618
|
-
commandsRun: gate.results.length,
|
|
1857
|
+
commandsRun: gate.results.filter(result => result.ran !== false).length,
|
|
2619
1858
|
...(gate.outcome === "scoped_out"
|
|
2620
1859
|
? { reason: "no executable verification command declared; operator command required" }
|
|
2621
1860
|
: {}),
|
|
@@ -2638,109 +1877,71 @@ async function runPhaseBoundaryGate(ctx) {
|
|
|
2638
1877
|
runLogger,
|
|
2639
1878
|
});
|
|
2640
1879
|
}
|
|
2641
|
-
async function handleAllTerminalTransition(
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
const scope = "all-terminal final gate";
|
|
2645
|
-
// The tool-owned final gate (INV-RS-10) runs at the single all-terminal →
|
|
2646
|
-
// closing funnel, on EVERY arrival here. It is skipped only when:
|
|
2647
|
-
// - there is nothing resolved to validate (everything blocked/skipped), or
|
|
2648
|
-
// - it is explicitly disabled for test hermeticity.
|
|
2649
|
-
// There is deliberately no third "already gave up" skip: the flag that used to
|
|
2650
|
-
// provide one made a run that hit the old backstop's bound skip the suite check
|
|
2651
|
-
// permanently, so the gate it exists to enforce stopped running exactly when it
|
|
2652
|
-
// mattered most. The gate is INDEPENDENT of plan.test_command and runs through
|
|
2653
|
-
// the env-scrubbing runTracked path.
|
|
2654
|
-
if (!gateDisabled && hasResolvedItems(state)) {
|
|
2655
|
-
const gateStart = Date.now();
|
|
2656
|
-
runLogger.event({
|
|
2657
|
-
phase: "next-step",
|
|
2658
|
-
kind: "executor_start",
|
|
2659
|
-
obligation: state.status,
|
|
2660
|
-
note: "tool_owned_final_gate",
|
|
2661
|
-
});
|
|
2662
|
-
const gate = await runToolOwnedFinalGate(root, { runner: options.finalGateRunner, testCommand: explicitFinalGateTestCommand(state) });
|
|
2663
|
-
await recordFinalGateOutcome({
|
|
2664
|
-
artifactsDir,
|
|
2665
|
-
state,
|
|
2666
|
-
scope,
|
|
2667
|
-
gateKey: "tool_owned_final_gate",
|
|
2668
|
-
runLogger,
|
|
2669
|
-
outcome: gate.outcome,
|
|
2670
|
-
passed: gate.passed,
|
|
2671
|
-
commandsRun: gate.results.length,
|
|
2672
|
-
...(gate.outcome === "scoped_out"
|
|
2673
|
-
? { reason: "no executable verification command declared; operator command required" }
|
|
2674
|
-
: {}),
|
|
2675
|
-
durationMs: Date.now() - gateStart,
|
|
2676
|
-
});
|
|
2677
|
-
if (!gate.passed) {
|
|
2678
|
-
// A whole-repo red at the closing funnel is exactly as unattributable as
|
|
2679
|
-
// one at a phase boundary, so it gets the same answer: record and pause.
|
|
2680
|
-
// The run does NOT advance to `closing` — closing on a red would write a
|
|
2681
|
-
// report claiming an outcome the suite never corroborated.
|
|
2682
|
-
// No `tree` here on purpose: this funnel caches nothing (it runs the floor
|
|
2683
|
-
// on every arrival), so there is no verdict record for a red to be bound
|
|
2684
|
-
// to, and the prompt says exactly that instead of naming an id that would
|
|
2685
|
-
// be re-measured on the next call anyway.
|
|
2686
|
-
return emitFinalGateRedStep({
|
|
2687
|
-
root,
|
|
2688
|
-
artifactsDir,
|
|
2689
|
-
state,
|
|
2690
|
-
scope,
|
|
2691
|
-
where: "Before the close phase",
|
|
2692
|
-
gate,
|
|
2693
|
-
runLogger,
|
|
2694
|
-
});
|
|
2695
|
-
}
|
|
2696
|
-
}
|
|
2697
|
-
else {
|
|
2698
|
-
// The gate was DUE at the closing funnel and did not run. Recorded, with
|
|
2699
|
-
// WHICH suppression did it — the run is about to transition to `closing`
|
|
2700
|
-
// and write a completion report, and without this the report would be
|
|
2701
|
-
// byte-identical to one produced after a green floor.
|
|
2702
|
-
await recordFinalGateOutcome({
|
|
2703
|
-
artifactsDir,
|
|
2704
|
-
state,
|
|
2705
|
-
scope,
|
|
2706
|
-
gateKey: "tool_owned_final_gate",
|
|
2707
|
-
runLogger,
|
|
2708
|
-
outcome: "disabled",
|
|
2709
|
-
passed: false,
|
|
2710
|
-
commandsRun: 0,
|
|
2711
|
-
reason: disabledReason ??
|
|
2712
|
-
"no verified-complete items to validate (nothing resolved)",
|
|
2713
|
-
});
|
|
2714
|
-
}
|
|
1880
|
+
async function handleAllTerminalTransition(state, store) {
|
|
1881
|
+
// Final acceptance belongs to the actual close execution, after preview
|
|
1882
|
+
// pauses. A transition carries no executable verification verdict.
|
|
2715
1883
|
state.status = "closing";
|
|
2716
1884
|
await store.saveState(state);
|
|
2717
1885
|
return { kind: "transition", state };
|
|
2718
1886
|
}
|
|
2719
|
-
async function handleClosing(root, artifactsDir, state, runLogger, store) {
|
|
1887
|
+
async function handleClosing(root, artifactsDir, state, runLogger, store, options) {
|
|
2720
1888
|
const closeStart = Date.now();
|
|
2721
1889
|
runLogger.event({ phase: "next-step", kind: "executor_start", obligation: state.status, note: "close" });
|
|
2722
|
-
//
|
|
2723
|
-
//
|
|
2724
|
-
//
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
1890
|
+
// Target verification and closing are product obligations. Development
|
|
1891
|
+
// reflection is owned by this repository's sprint closeout; captured run
|
|
1892
|
+
// diagnostics are still archived by runClosePhase before cleanup.
|
|
1893
|
+
let paused;
|
|
1894
|
+
let closed;
|
|
1895
|
+
try {
|
|
1896
|
+
closed = await runClosePhase(state, {
|
|
1897
|
+
root, artifactsDir,
|
|
1898
|
+
skipFinalGate: options.skipFinalGate,
|
|
1899
|
+
finalGateRunner: options.finalGateRunner,
|
|
1900
|
+
finalizeClosing: options.finalizeClosing,
|
|
1901
|
+
onFinalGateRed: async (gate) => {
|
|
1902
|
+
paused = await emitFinalGateRedStep({
|
|
1903
|
+
root, artifactsDir, state, scope: "all-terminal final gate",
|
|
1904
|
+
where: "Before completing the close phase", gate, runLogger,
|
|
1905
|
+
recordedPath: finalGateRecordPath(artifactsDir),
|
|
1906
|
+
});
|
|
1907
|
+
},
|
|
1908
|
+
}, runLogger);
|
|
1909
|
+
}
|
|
1910
|
+
catch (error) {
|
|
1911
|
+
if (!(error instanceof RemediationPlanAuthorityError))
|
|
1912
|
+
throw error;
|
|
1913
|
+
return { kind: "emit", step: await emitPlanRevisionBlockedStep(root, artifactsDir, {
|
|
1914
|
+
reason: `Closing is paused before any closing action. ${error.message}`,
|
|
1915
|
+
}) };
|
|
1916
|
+
}
|
|
1917
|
+
if (paused)
|
|
1918
|
+
return paused;
|
|
2740
1919
|
runLogger.event({ phase: "next-step", kind: "executor_end", obligation: state.status, note: "close", duration_ms: Date.now() - closeStart });
|
|
2741
1920
|
if (closed.status !== "complete") {
|
|
2742
|
-
// Not done (preview / re-blocked to triage): persist and re-scan.
|
|
2743
1921
|
await store.saveState(closed);
|
|
1922
|
+
const preview = closed.closing_plan?.closing_action_preview;
|
|
1923
|
+
if (closed.status === "closing" && preview && !closed.closing_plan?.pre_authorized) {
|
|
1924
|
+
const approveCommand = loaderCommand("next-step --finalize-closing");
|
|
1925
|
+
return {
|
|
1926
|
+
kind: "emit",
|
|
1927
|
+
step: await writeCurrentStep({
|
|
1928
|
+
stepKind: "close_run", status: "ready", runId: stateRunId(closed),
|
|
1929
|
+
repoRoot: root, artifactsDir,
|
|
1930
|
+
prompt: [
|
|
1931
|
+
"# Approve closing action",
|
|
1932
|
+
`Ask the user to approve the closing action: ${closed.closing_plan?.action}.`,
|
|
1933
|
+
`Commit message: ${preview.commit_message}`,
|
|
1934
|
+
"Files:", ...preview.files.map(file => `- ${file}`),
|
|
1935
|
+
...(preview.leftover_files?.length ? ["Files left untouched:", ...preview.leftover_files.map(file => `- ${file}`)] : []),
|
|
1936
|
+
`Only after the user approves, run: ${approveCommand}`,
|
|
1937
|
+
"If the preview changes, approval is requested again. Final verification runs after approval.",
|
|
1938
|
+
].join("\n"),
|
|
1939
|
+
allowedCommands: [approveCommand],
|
|
1940
|
+
stopCondition: "Stop after presenting the closing preview and wait for user approval.",
|
|
1941
|
+
}),
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
// Re-blocked to triage: persist and re-scan.
|
|
2744
1945
|
return { kind: "transition", state: closed };
|
|
2745
1946
|
}
|
|
2746
1947
|
// Close-complete CROSSES the engine boundary: `complete` is a pre-intake
|
|
@@ -2837,7 +2038,7 @@ its own guard still matches.
|
|
|
2837
2038
|
async function handleUnhandledState(root, artifactsDir, state) {
|
|
2838
2039
|
const itemsByStatus = {};
|
|
2839
2040
|
for (const item of Object.values(state.items ?? {})) {
|
|
2840
|
-
(itemsByStatus[item.status] ??= []).push(item.
|
|
2041
|
+
(itemsByStatus[item.status] ??= []).push(item.unit_id);
|
|
2841
2042
|
}
|
|
2842
2043
|
const statusBreakdown = Object.entries(itemsByStatus)
|
|
2843
2044
|
.map(([status, ids]) => `- **${status}**: ${ids.join(", ")}`)
|
|
@@ -3221,7 +2422,7 @@ export const PRE_INTAKE_PRIORITY = [
|
|
|
3221
2422
|
*/
|
|
3222
2423
|
export function buildPreIntakeObligations(ctx, snapshot) {
|
|
3223
2424
|
const { artifactsDir, inputResolution } = ctx;
|
|
3224
|
-
const { existingCheckpoint, rejectedCheckpointFields, resumeAck, entryState, suppliedInputUnchanged, inputSelectionChanged, selectionAlreadyBound, selectedSourceChanged, guidanceFileSupplied } = snapshot;
|
|
2425
|
+
const { existingCheckpoint, rejectedCheckpointFields, resumeAck, entryState, suppliedInputUnchanged, sourceAlreadyBound, inputSelectionChanged, selectionAlreadyBound, selectedSourceChanged, guidanceFileSupplied } = snapshot;
|
|
3225
2426
|
const ip = intakePaths(artifactsDir);
|
|
3226
2427
|
const checkpointPath = join(artifactsDir, "intent_checkpoint.json");
|
|
3227
2428
|
const ackPath = join(artifactsDir, "confirm_resume_ack.json");
|
|
@@ -3243,7 +2444,7 @@ export function buildPreIntakeObligations(ctx, snapshot) {
|
|
|
3243
2444
|
derive: () => (selectionAlreadyBound && inputSelectionChanged) ||
|
|
3244
2445
|
(((inputResolution.supplied && !suppliedInputUnchanged) ||
|
|
3245
2446
|
inputSelectionChanged || guidanceFileSupplied) &&
|
|
3246
|
-
entryState != null && entryState.status !== "pending")
|
|
2447
|
+
(sourceAlreadyBound || (entryState != null && entryState.status !== "pending")))
|
|
3247
2448
|
? "missing"
|
|
3248
2449
|
: "satisfied",
|
|
3249
2450
|
execute: async (_state, c) => {
|
|
@@ -3316,7 +2517,8 @@ export function buildPreIntakeObligations(ctx, snapshot) {
|
|
|
3316
2517
|
const fires = invalidClosingAction ||
|
|
3317
2518
|
(!existsSync(checkpointPath) &&
|
|
3318
2519
|
(existsSync(ip.summary) ||
|
|
3319
|
-
existsSync(
|
|
2520
|
+
existsSync(executionPlanPaths(artifactsDir).canonical) ||
|
|
2521
|
+
existsSync(executionPlanPaths(artifactsDir).source) ||
|
|
3320
2522
|
activeRunState));
|
|
3321
2523
|
return fires ? "missing" : "satisfied";
|
|
3322
2524
|
},
|
|
@@ -3433,7 +2635,8 @@ Then run:
|
|
|
3433
2635
|
// file for confirmation via confirm_auto_discovered_input, mtime + type +
|
|
3434
2636
|
// finding count included) rather than silently re-showing the stale report.
|
|
3435
2637
|
const freshIntent = existsSync(ip.conversationStart) ||
|
|
3436
|
-
existsSync(
|
|
2638
|
+
existsSync(executionPlanPaths(artifactsDir).canonical) ||
|
|
2639
|
+
sourceAlreadyBound ||
|
|
3437
2640
|
(existsSync(ip.summary) && existingCheckpoint?.confirmed_by === "host") ||
|
|
3438
2641
|
isDefaultCandidateFresherThanReport(inputResolution.existing[0], reportPath);
|
|
3439
2642
|
return freshIntent ? "satisfied" : "missing";
|
|
@@ -3642,13 +2845,13 @@ export function buildMainObligations(ctx) {
|
|
|
3642
2845
|
await store.saveState(s);
|
|
3643
2846
|
return { kind: "transition", state: s };
|
|
3644
2847
|
}
|
|
3645
|
-
return handleImplementing(root, artifactsDir, s, runLogger, store
|
|
2848
|
+
return handleImplementing(root, artifactsDir, s, runLogger, store);
|
|
3646
2849
|
},
|
|
3647
2850
|
},
|
|
3648
2851
|
{
|
|
3649
2852
|
id: "triage",
|
|
3650
2853
|
derive: (state) => (state?.status === "triage" ? "missing" : "satisfied"),
|
|
3651
|
-
execute: async (state) => handleImplementing(root, artifactsDir, requireState(state), runLogger, store
|
|
2854
|
+
execute: async (state) => handleImplementing(root, artifactsDir, requireState(state), runLogger, store),
|
|
3652
2855
|
},
|
|
3653
2856
|
{
|
|
3654
2857
|
// planning with zero documentable findings is a user question, not a
|
|
@@ -3657,7 +2860,7 @@ export function buildMainObligations(ctx) {
|
|
|
3657
2860
|
id: "planning_zero",
|
|
3658
2861
|
derive: (state) => state != null &&
|
|
3659
2862
|
state.status === "planning" &&
|
|
3660
|
-
documentableFindings(state).length === 0
|
|
2863
|
+
documentableFindings(state).length === 0 && !allItemsTerminal(state)
|
|
3661
2864
|
? "missing"
|
|
3662
2865
|
: "satisfied",
|
|
3663
2866
|
execute: async (state) => ({
|
|
@@ -3670,12 +2873,12 @@ export function buildMainObligations(ctx) {
|
|
|
3670
2873
|
derive: (state) => state != null && allItemsTerminal(state) && state.status !== "closing"
|
|
3671
2874
|
? "missing"
|
|
3672
2875
|
: "satisfied",
|
|
3673
|
-
execute: async (state) => handleAllTerminalTransition(
|
|
2876
|
+
execute: async (state) => handleAllTerminalTransition(requireState(state), store),
|
|
3674
2877
|
},
|
|
3675
2878
|
{
|
|
3676
2879
|
id: "closing",
|
|
3677
2880
|
derive: (state) => (state?.status === "closing" ? "missing" : "satisfied"),
|
|
3678
|
-
execute: async (state) => handleClosing(root, artifactsDir, requireState(state), runLogger, store),
|
|
2881
|
+
execute: async (state) => handleClosing(root, artifactsDir, requireState(state), runLogger, store, options),
|
|
3679
2882
|
},
|
|
3680
2883
|
{
|
|
3681
2884
|
// Catch-all: reached only when no specific obligation matched. Always
|
|
@@ -3754,10 +2957,15 @@ async function advanceUnderPhaseLock(deps) {
|
|
|
3754
2957
|
}
|
|
3755
2958
|
if (options.guidanceFile && options.guidanceText !== undefined)
|
|
3756
2959
|
throw new Error("Choose --guidance or --guidance-file, not both.");
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
2960
|
+
const boundSource = await readPlanSource(artifactsDir);
|
|
2961
|
+
// A conflicting new source must reach the bounded choice before bootstrap
|
|
2962
|
+
// touches the existing conversation input (or throws on different bytes).
|
|
2963
|
+
if (!boundSource && (state == null || state.status === "pending")) {
|
|
2964
|
+
if (options.guidanceFile)
|
|
2965
|
+
applyGuidanceFile(artifactsDir, options.guidanceFile);
|
|
2966
|
+
if (options.guidanceText !== undefined)
|
|
2967
|
+
applyGuidanceText(artifactsDir, options.guidanceText);
|
|
2968
|
+
}
|
|
3761
2969
|
if (options.verificationCommand !== undefined && state?.plan) {
|
|
3762
2970
|
if (parseCommandString(options.verificationCommand).length === 0) {
|
|
3763
2971
|
throw new Error("--verification-command must name an executable command");
|
|
@@ -3787,14 +2995,17 @@ async function advanceUnderPhaseLock(deps) {
|
|
|
3787
2995
|
// Preamble — forceReplan re-grounds from existing intake. The whole decide loop
|
|
3788
2996
|
// runs once per host call (the engine folds planning → implementing → … through
|
|
3789
2997
|
// transitions, never a recursive decideNextStepLoop), so this fires at most once.
|
|
3790
|
-
|
|
2998
|
+
const currentCanonical = state?.plan ? await readCanonicalPlan(artifactsDir) : undefined;
|
|
2999
|
+
if (state != null && (options.forceReplan || existsSync(executionPlanPaths(artifactsDir).submission) || (currentCanonical && currentCanonical.revision_sha256 !== state.plan?.review_revision_sha256))) {
|
|
3791
3000
|
await countStep(state);
|
|
3792
|
-
const replanOutcome = await forceReplanFromExistingIntake(root, artifactsDir, state,
|
|
3001
|
+
const replanOutcome = await forceReplanFromExistingIntake(root, artifactsDir, state, runLogger);
|
|
3793
3002
|
// A discarded plan is REPORTED here, not collapsed into `state = null`. Once
|
|
3794
3003
|
// it is null the loop can no longer tell "the plan was destroyed, and here is
|
|
3795
3004
|
// why" from "there was never an input", and it emits the second.
|
|
3796
3005
|
if (replanOutcome !== null && "kind" in replanOutcome) {
|
|
3797
|
-
|
|
3006
|
+
if (replanOutcome.kind === "planning_step")
|
|
3007
|
+
return replanOutcome.step;
|
|
3008
|
+
return emitPlanRevisionBlockedStep(root, artifactsDir, replanOutcome);
|
|
3798
3009
|
}
|
|
3799
3010
|
state = replanOutcome;
|
|
3800
3011
|
}
|
|
@@ -3841,7 +3052,7 @@ async function advanceUnderPhaseLock(deps) {
|
|
|
3841
3052
|
hashContent(await readFile(recordedSelection.source_path, "utf8")) !== recordedSelection.source_hash);
|
|
3842
3053
|
const inputSelectionChanged = selectedSourceChanged ||
|
|
3843
3054
|
(requestedSelection !== undefined && JSON.stringify(requestedSelection) !== JSON.stringify(recordedSelection?.criteria));
|
|
3844
|
-
const suppliedInputUnchanged = !inputSelectionChanged && suppliedInputMatchesRun(inputResolution, sourceManifest);
|
|
3055
|
+
const suppliedInputUnchanged = !inputSelectionChanged && suppliedInputMatchesRun(inputResolution, sourceManifest, boundSource?.sources.map(source => source.path).filter(path => resolve(path) !== resolve(intakePaths(artifactsDir).brief)));
|
|
3845
3056
|
// The linear pre-intake gates run as obligations through the shared advance
|
|
3846
3057
|
// loop. An emit returns to the host; a transition re-scans within this call;
|
|
3847
3058
|
// exhausting them (step === null) means the run is past intake and falls
|
|
@@ -3863,10 +3074,11 @@ async function advanceUnderPhaseLock(deps) {
|
|
|
3863
3074
|
resumeAck,
|
|
3864
3075
|
entryState: state,
|
|
3865
3076
|
suppliedInputUnchanged,
|
|
3077
|
+
sourceAlreadyBound: boundSource !== undefined,
|
|
3866
3078
|
inputSelectionChanged,
|
|
3867
3079
|
selectionAlreadyBound: recordedSelection !== undefined,
|
|
3868
3080
|
selectedSourceChanged,
|
|
3869
|
-
guidanceFileSupplied: Boolean(options.guidanceFileSupplied),
|
|
3081
|
+
guidanceFileSupplied: Boolean(options.guidanceFileSupplied || options.guidanceFile || options.guidanceText !== undefined),
|
|
3870
3082
|
}),
|
|
3871
3083
|
}, state, ctx);
|
|
3872
3084
|
if (preIntake.step)
|