audit-tools 0.50.10 → 0.50.12
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/remediate/contractPipeline/derive.d.ts +7 -7
- package/dist/remediate/contractPipeline/derive.d.ts.map +1 -1
- package/dist/remediate/contractPipeline/derive.js +19 -21
- package/dist/remediate/contractPipeline/derive.js.map +1 -1
- package/dist/remediate/contractPipeline/phaseCut.d.ts +46 -8
- package/dist/remediate/contractPipeline/phaseCut.d.ts.map +1 -1
- package/dist/remediate/contractPipeline/phaseCut.js +71 -40
- package/dist/remediate/contractPipeline/phaseCut.js.map +1 -1
- package/dist/remediate/contractPipeline/phaseCutArtifact.d.ts +1 -1
- package/dist/remediate/contractPipeline/phaseCutArtifact.js +1 -1
- package/dist/remediate/contractPipeline/repairState.d.ts +108 -0
- package/dist/remediate/contractPipeline/repairState.d.ts.map +1 -0
- package/dist/remediate/contractPipeline/repairState.js +184 -0
- package/dist/remediate/contractPipeline/repairState.js.map +1 -0
- package/dist/remediate/index.d.ts.map +1 -1
- package/dist/remediate/index.js +4 -0
- package/dist/remediate/index.js.map +1 -1
- package/dist/remediate/state/types.d.ts +62 -0
- package/dist/remediate/state/types.d.ts.map +1 -1
- package/dist/remediate/state/types.js +17 -0
- package/dist/remediate/state/types.js.map +1 -1
- package/dist/remediate/steps/contractPipeline.d.ts +8 -0
- package/dist/remediate/steps/contractPipeline.d.ts.map +1 -1
- package/dist/remediate/steps/contractPipeline.js +192 -58
- package/dist/remediate/steps/contractPipeline.js.map +1 -1
- package/dist/remediate/steps/contractPipelinePrompts.d.ts.map +1 -1
- package/dist/remediate/steps/contractPipelinePrompts.js +3 -2
- package/dist/remediate/steps/contractPipelinePrompts.js.map +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.d.ts +19 -0
- package/dist/remediate/steps/dispatch/hostHandoff.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/hostHandoff.js +69 -0
- package/dist/remediate/steps/dispatch/hostHandoff.js.map +1 -1
- package/dist/remediate/steps/nextStep.d.ts +1 -1
- package/dist/remediate/steps/nextStep.d.ts.map +1 -1
- package/dist/remediate/steps/nextStep.js +151 -84
- package/dist/remediate/steps/nextStep.js.map +1 -1
- package/dist/remediate/steps/prompts.d.ts.map +1 -1
- package/dist/remediate/steps/prompts.js +20 -2
- package/dist/remediate/steps/prompts.js.map +1 -1
- package/dist/remediate/steps/stepUtils.d.ts +1 -37
- package/dist/remediate/steps/stepUtils.d.ts.map +1 -1
- package/dist/remediate/steps/stepUtils.js +0 -79
- package/dist/remediate/steps/stepUtils.js.map +1 -1
- package/dist/remediate/validation/artifacts.d.ts.map +1 -1
- package/dist/remediate/validation/artifacts.js +4 -0
- package/dist/remediate/validation/artifacts.js.map +1 -1
- package/dist/remediate/validation/contractPipeline.d.ts.map +1 -1
- package/dist/remediate/validation/contractPipeline.js +14 -0
- package/dist/remediate/validation/contractPipeline.js.map +1 -1
- package/dist/remediate/validation/contractPipelineGates.d.ts +9 -2
- package/dist/remediate/validation/contractPipelineGates.d.ts.map +1 -1
- package/dist/remediate/validation/contractPipelineGates.js +13 -7
- package/dist/remediate/validation/contractPipelineGates.js.map +1 -1
- package/package.json +1 -1
|
@@ -15,19 +15,19 @@ import { runTriagePhase } from "../phases/triage.js";
|
|
|
15
15
|
import { runClosePhase } from "../phases/close.js";
|
|
16
16
|
import { validateRemediationPlan } from "../validation/remediationState.js";
|
|
17
17
|
import { readExtractedPlanIfPresent, } from "./dispatch.js";
|
|
18
|
-
import { ingestRemediationHostResults, hostDependencyLevels, precomputeRecoveryTestVerdicts, prepareRemediationHostHandoff, } from "./dispatch/hostHandoff.js";
|
|
18
|
+
import { ingestRemediationHostResults, hostDependencyLevels, permanentlyDeadPendingBlocks, precomputeRecoveryTestVerdicts, prepareRemediationHostHandoff, } from "./dispatch/hostHandoff.js";
|
|
19
19
|
import { FileLockTimeoutError, withFileLock, } from "../../shared/io/fileLock.js";
|
|
20
20
|
import { AUDIT_FINDINGS_FILENAME, AUDIT_REPORT_FILENAME, auditArtifactsDir, auditFindingsPath, auditReportPath, promotedAuditFindingsPath, promotedAuditReportPath, remediationArtifactsDir, } from "../../shared/io/auditToolsPaths.js";
|
|
21
21
|
import { callerWorkingDirectory, discoverRepoRoot, resolveRepoRoot, } from "../../shared/io/repoRoot.js";
|
|
22
22
|
import { writeCurrentStep } from "./stepWriter.js";
|
|
23
|
-
import { dependencyAwaitingClarification, dependencyVerifiedComplete, } from "./stepUtils.js";
|
|
24
23
|
import { isTerminalStatus, isVerifiedCompleteStatus, } from "../state/itemStatus.js";
|
|
25
24
|
import { deduplicateCrossLensFindings, fixupBlocksAfterDedup, } from "../dedup/crossLensDedup.js";
|
|
26
25
|
import { checkAffectedFileIntegrity } from "../utils/fileIntegrity.js";
|
|
27
26
|
import { applyIntentOrdering } from "../intent/intentOrdering.js";
|
|
28
27
|
import { resolveIntakeStep } from "./intakeResolver.js";
|
|
29
28
|
import { runToolOwnedFinalGate, writeFinalGateRedRecord, writeFinalGateOutcomeRecord, } from "./finalGate.js";
|
|
30
|
-
import { buildNextContractPipelineStep, shouldEnterContractPipeline, writePathASeedFromFindings, } from "./contractPipeline.js";
|
|
29
|
+
import { buildNextContractPipelineStep, shouldEnterContractPipeline, writePathASeedFromFindings, normalizeBlockTouchedFiles, checkWriteScopePathsAgainstTrackedTree, } from "./contractPipeline.js";
|
|
30
|
+
import { compareCodeUnits } from "../../shared/compareCodeUnits.js";
|
|
31
31
|
import { contractArtifactExists, contractPipelineDir, } from "../contractPipeline/artifactStore.js";
|
|
32
32
|
import { buildReviewRequest, applyReviewResolution, isResolutionForRequest, screenResolutionIds, REVIEW_REQUEST_SCHEMA_VERSION, } from "../review/reviewGate.js";
|
|
33
33
|
import { buildAutonomousReviewDecision } from "../review/autonomousGate.js";
|
|
@@ -219,7 +219,7 @@ function suppliedInputMatchesRun(inputResolution, manifest) {
|
|
|
219
219
|
return false;
|
|
220
220
|
return true;
|
|
221
221
|
}
|
|
222
|
-
export {
|
|
222
|
+
export { classifyFindingRisk } from "./stepUtils.js";
|
|
223
223
|
export { isTerminalStatus, isVerifiedCompleteStatus };
|
|
224
224
|
export { hostDependencyLevels };
|
|
225
225
|
function documentableFindings(state) {
|
|
@@ -228,41 +228,50 @@ function documentableFindings(state) {
|
|
|
228
228
|
return state.plan.findings.filter((finding) => state.items?.[finding.id]?.status === "pending");
|
|
229
229
|
}
|
|
230
230
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
231
|
+
* The dispatch frontier: the level-0 draw of the SAME dependency/phase
|
|
232
|
+
* partition the host workload builder emits from ({@link hostDependencyLevels}).
|
|
233
|
+
* The guard and the builder reading ONE computation is what keeps the
|
|
234
|
+
* "Cannot prepare an empty remediation host workload" throw unreachable from
|
|
235
|
+
* the scheduler: whenever this is non-empty, the builder derives the identical
|
|
236
|
+
* non-empty set (the 2026-08-23 empty-frontier incident — the retired
|
|
237
|
+
* edge-only predicate ignored the phase barrier and dependency existence, so
|
|
238
|
+
* the guard dispatched frontiers the builder refused, and next-step died
|
|
239
|
+
* instead of pausing).
|
|
236
240
|
*/
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
return [];
|
|
240
|
-
return state.plan.blocks.filter((block) => dependencyVerifiedComplete(block, state) &&
|
|
241
|
-
block.items.some((findingId) => {
|
|
242
|
-
const item = state.items?.[findingId];
|
|
243
|
-
return item?.status === "pending";
|
|
244
|
-
}));
|
|
241
|
+
function dispatchFrontier(state) {
|
|
242
|
+
return hostDependencyLevels(state)[0] ?? [];
|
|
245
243
|
}
|
|
246
244
|
/**
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* (`dependencyAwaitingClarification`): since the clarification round is deferred
|
|
255
|
-
* to the end of the implement phase, such a node reaches this sweep while its
|
|
256
|
-
* answer is still outstanding, and blocking it would report "upstream failed"
|
|
257
|
-
* for what is really "awaiting an answer". It stays `pending` and is re-decided
|
|
258
|
-
* once the answer lands.
|
|
245
|
+
* The ONE dead-end sweep, shared by the planning→implementing transition and
|
|
246
|
+
* the implementing obligation: marks the pending items of every permanently
|
|
247
|
+
* dead block `blocked` with the INV-RS-01 reason, and reports whether anything
|
|
248
|
+
* changed. Deadness is the workload boundary's own liveness analysis
|
|
249
|
+
* ({@link permanentlyDeadPendingBlocks}), so a node held by an unanswered
|
|
250
|
+
* clarification — or by a lower phase that is merely still working — is NEVER
|
|
251
|
+
* mis-reported as an upstream failure (the 175cfb89 pin).
|
|
259
252
|
*/
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
253
|
+
function sweepPermanentlyDeadBlocks(state) {
|
|
254
|
+
let changed = false;
|
|
255
|
+
const now = new Date().toISOString();
|
|
256
|
+
for (const block of permanentlyDeadPendingBlocks(state)) {
|
|
257
|
+
for (const findingId of block.items) {
|
|
258
|
+
const it = state.items?.[findingId];
|
|
259
|
+
if (!it || it.status !== "pending")
|
|
260
|
+
continue;
|
|
261
|
+
it.status = "blocked";
|
|
262
|
+
it.started_at ??= now;
|
|
263
|
+
it.completed_at = now;
|
|
264
|
+
it.failure_reason =
|
|
265
|
+
it.failure_reason ??
|
|
266
|
+
"A prerequisite can never reach a verified-complete disposition " +
|
|
267
|
+
"(a dependency or lower-phase block was skipped, blocked, or abandoned; " +
|
|
268
|
+
"a declared dependency resolves to no block; or the dependencies are " +
|
|
269
|
+
"cyclic); the host handoff will not expose this node against an " +
|
|
270
|
+
"upstream surface that never landed (INV-RS-01).";
|
|
271
|
+
changed = true;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return changed;
|
|
266
275
|
}
|
|
267
276
|
/**
|
|
268
277
|
* Whether any item is paused on a worker question that has not been answered yet.
|
|
@@ -1590,6 +1599,14 @@ function isPlanClarificationAction(value) {
|
|
|
1590
1599
|
return (typeof value === "string" &&
|
|
1591
1600
|
PLAN_CLARIFICATION_ACTIONS.includes(value));
|
|
1592
1601
|
}
|
|
1602
|
+
/** Carry `scope_additions` through when it is an array; keep string entries. */
|
|
1603
|
+
function normalizedScopeAdditions(entry) {
|
|
1604
|
+
if (!Array.isArray(entry.scope_additions))
|
|
1605
|
+
return {};
|
|
1606
|
+
return {
|
|
1607
|
+
scope_additions: entry.scope_additions.filter((p) => typeof p === "string"),
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1593
1610
|
function normalizePlanClarificationResolutions(value) {
|
|
1594
1611
|
if (Array.isArray(value)) {
|
|
1595
1612
|
return value.filter(isRecord).flatMap((entry) => {
|
|
@@ -1599,6 +1616,7 @@ function normalizePlanClarificationResolutions(value) {
|
|
|
1599
1616
|
finding_id: entry.finding_id,
|
|
1600
1617
|
action: entry.action,
|
|
1601
1618
|
rationale: typeof entry.rationale === "string" ? entry.rationale : undefined,
|
|
1619
|
+
...normalizedScopeAdditions(entry),
|
|
1602
1620
|
},
|
|
1603
1621
|
];
|
|
1604
1622
|
}
|
|
@@ -1622,9 +1640,56 @@ function normalizePlanClarificationResolutions(value) {
|
|
|
1622
1640
|
finding_id: typeof entry.finding_id === "string" ? entry.finding_id : findingId,
|
|
1623
1641
|
action: entry.action,
|
|
1624
1642
|
rationale: typeof entry.rationale === "string" ? entry.rationale : undefined,
|
|
1643
|
+
...normalizedScopeAdditions(entry),
|
|
1625
1644
|
}];
|
|
1626
1645
|
});
|
|
1627
1646
|
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Validate every `clarified` resolution's `scope_additions` BEFORE anything is
|
|
1649
|
+
* applied: each entry must normalize beneath the repository root against the
|
|
1650
|
+
* finding's owning block (the one write-scope normalizer), must clear the SAME
|
|
1651
|
+
* tracked-tree rule the promotion gate enforced (the delta lane must not
|
|
1652
|
+
* bypass it), and a finding with no owning block has no scope to widen. ANY
|
|
1653
|
+
* refusal refuses the WHOLE resolution file — the uniform id-join contract's
|
|
1654
|
+
* shape: a half-applied decision record is worse than a re-ask.
|
|
1655
|
+
*/
|
|
1656
|
+
async function validateClarificationScopeAdditions(root, state, resolutions) {
|
|
1657
|
+
const refusals = [];
|
|
1658
|
+
for (const res of resolutions) {
|
|
1659
|
+
if (res.action !== "clarified" || !res.scope_additions?.length)
|
|
1660
|
+
continue;
|
|
1661
|
+
const block = state.plan?.blocks?.find((b) => b.items.includes(res.finding_id));
|
|
1662
|
+
if (!block) {
|
|
1663
|
+
refusals.push(`scope_additions for \`${res.finding_id}\`: no plan block owns this finding, so ` +
|
|
1664
|
+
`there is no write scope to widen.`);
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
const normalized = normalizeBlockTouchedFiles(root, res.scope_additions, block.block_id);
|
|
1668
|
+
refusals.push(...normalized.refusals);
|
|
1669
|
+
refusals.push(...(await checkWriteScopePathsAgainstTrackedTree(root, normalized.touched_files, `scope_additions for \`${res.finding_id}\` (block "${block.block_id}")`)));
|
|
1670
|
+
}
|
|
1671
|
+
return refusals;
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Apply one validated scope delta: union the normalized additions into the
|
|
1675
|
+
* owning block's `touched_files` (content-sorted — an incidentally-ordered
|
|
1676
|
+
* scope would churn the plan hash). The workload-binding invalidation is the
|
|
1677
|
+
* caller's existing `delete state.host_handoff` on any applied resolution, so
|
|
1678
|
+
* the next prepare re-mints the work item with the widened `allowed_files`
|
|
1679
|
+
* (the open-bugs :661 wedge class); the up-front ambiguity gate has no binding
|
|
1680
|
+
* by construction.
|
|
1681
|
+
*/
|
|
1682
|
+
function applyClarificationScopeAdditions(root, state, res) {
|
|
1683
|
+
if (res.action !== "clarified" || !res.scope_additions?.length)
|
|
1684
|
+
return;
|
|
1685
|
+
const block = state.plan?.blocks?.find((b) => b.items.includes(res.finding_id));
|
|
1686
|
+
if (!block)
|
|
1687
|
+
return; // refused upstream by validateClarificationScopeAdditions
|
|
1688
|
+
const normalized = normalizeBlockTouchedFiles(root, res.scope_additions, block.block_id);
|
|
1689
|
+
block.touched_files = [
|
|
1690
|
+
...new Set([...block.touched_files, ...normalized.touched_files]),
|
|
1691
|
+
].sort((left, right) => compareCodeUnits(left, right));
|
|
1692
|
+
}
|
|
1628
1693
|
/**
|
|
1629
1694
|
* Apply one clarification resolution to its item. Single-sourced so the up-front
|
|
1630
1695
|
* ambiguity gate (part A) and the mid-run clarification round (part B) settle an
|
|
@@ -1677,6 +1742,17 @@ async function applyPlanClarificationResolution(root, artifactsDir, state, store
|
|
|
1677
1742
|
step: await handleWaitingForClarification(root, artifactsDir, state, `finding id(s) not in the plan: ${unknownIds.map((i) => `\`${i}\``).join(", ")}`),
|
|
1678
1743
|
};
|
|
1679
1744
|
}
|
|
1745
|
+
// Scope deltas ride the same whole-file fail-closed contract as unknown ids:
|
|
1746
|
+
// an invalid addition refuses everything, so a decision record never
|
|
1747
|
+
// half-applies (open-bugs.md:110).
|
|
1748
|
+
const scopeRefusals = await validateClarificationScopeAdditions(root, state, resolutions);
|
|
1749
|
+
if (scopeRefusals.length > 0) {
|
|
1750
|
+
await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.refused-${Date.now()}`));
|
|
1751
|
+
return {
|
|
1752
|
+
kind: "refused",
|
|
1753
|
+
step: await handleWaitingForClarification(root, artifactsDir, state, `invalid scope_additions — fix and re-submit: ${scopeRefusals.join(" | ")}`),
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1680
1756
|
const now = new Date().toISOString();
|
|
1681
1757
|
let appliedCount = 0;
|
|
1682
1758
|
for (const res of resolutions) {
|
|
@@ -1684,6 +1760,7 @@ async function applyPlanClarificationResolution(root, artifactsDir, state, store
|
|
|
1684
1760
|
if (!item || isTerminalStatus(item.status))
|
|
1685
1761
|
continue;
|
|
1686
1762
|
applyClarificationActionToItem(item, res, now);
|
|
1763
|
+
applyClarificationScopeAdditions(root, state, res);
|
|
1687
1764
|
appliedCount += 1;
|
|
1688
1765
|
}
|
|
1689
1766
|
// An applied answer mutates item state that is baked into the dispatch
|
|
@@ -1947,6 +2024,29 @@ async function runPlanAmbiguityGate(root, artifactsDir, state, store) {
|
|
|
1947
2024
|
},
|
|
1948
2025
|
});
|
|
1949
2026
|
}
|
|
2027
|
+
// Scope deltas: the same whole-file fail-closed contract as unknown ids
|
|
2028
|
+
// (open-bugs.md:110). At this up-front gate no workload binding exists yet,
|
|
2029
|
+
// so an applied widening simply flows into the first dispatch.
|
|
2030
|
+
const scopeRefusals = await validateClarificationScopeAdditions(root, state, resolutions);
|
|
2031
|
+
if (scopeRefusals.length > 0) {
|
|
2032
|
+
await withFsRetry(() => rename(resolutionPath, `${resolutionPath}.refused-${Date.now()}`));
|
|
2033
|
+
const candidates = (await readOptionalJsonFile(requestPath)) ??
|
|
2034
|
+
detectPlanAmbiguities(findings, state.items);
|
|
2035
|
+
return writeCurrentStep({
|
|
2036
|
+
stepKind: "collect_clarifications",
|
|
2037
|
+
status: "blocked",
|
|
2038
|
+
runId: stateRunId(state),
|
|
2039
|
+
repoRoot: root,
|
|
2040
|
+
artifactsDir,
|
|
2041
|
+
prompt: ambiguityReviewPrompt(candidates, resolutionPath, findings.map((f) => f.id), `invalid scope_additions — fix and re-submit: ${scopeRefusals.join(" | ")}`),
|
|
2042
|
+
allowedCommands: [loaderCommand("next-step")],
|
|
2043
|
+
stopCondition: "Stop after re-submitting a corrected ambiguity resolution, unless it is already written and the prompt told you to continue.",
|
|
2044
|
+
artifactPaths: {
|
|
2045
|
+
ambiguity_request: requestPath,
|
|
2046
|
+
ambiguity_resolution: resolutionPath,
|
|
2047
|
+
},
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
1950
2050
|
const now = new Date().toISOString();
|
|
1951
2051
|
let changed = false;
|
|
1952
2052
|
for (const res of resolutions) {
|
|
@@ -1954,6 +2054,7 @@ async function runPlanAmbiguityGate(root, artifactsDir, state, store) {
|
|
|
1954
2054
|
if (!item || isTerminalStatus(item.status))
|
|
1955
2055
|
continue;
|
|
1956
2056
|
applyClarificationActionToItem(item, res, now);
|
|
2057
|
+
applyClarificationScopeAdditions(root, state, res);
|
|
1957
2058
|
changed = true;
|
|
1958
2059
|
}
|
|
1959
2060
|
await writeJsonFile(decisionPath, { resolved_at: now, resolution_count: resolutions.length });
|
|
@@ -1989,7 +2090,7 @@ async function handlePlanning(root, artifactsDir, state, store) {
|
|
|
1989
2090
|
}
|
|
1990
2091
|
// Document phase dissolved (N-R13): planning transitions directly to
|
|
1991
2092
|
// implementing, and the host workload reads finding context directly.
|
|
1992
|
-
const implementBlocks =
|
|
2093
|
+
const implementBlocks = dispatchFrontier(state);
|
|
1993
2094
|
if (implementBlocks.length > 0) {
|
|
1994
2095
|
if (state.plan) {
|
|
1995
2096
|
const integrity = await checkAffectedFileIntegrity(root, state.plan.findings);
|
|
@@ -2024,28 +2125,15 @@ async function handlePlanning(root, artifactsDir, state, store) {
|
|
|
2024
2125
|
}
|
|
2025
2126
|
}
|
|
2026
2127
|
// Transition directly to implementing — no separate document round.
|
|
2027
|
-
// Any pending item
|
|
2128
|
+
// Any pending item that can never enter the host dependency frontier is
|
|
2028
2129
|
// dead-ended (INV-RS-01): a prerequisite was skipped/blocked, so its
|
|
2029
2130
|
// verified-complete edge can never be satisfied — never dispatch a dependent
|
|
2030
2131
|
// against an upstream surface that did not land. Mark it blocked so the run
|
|
2031
2132
|
// advances to close rather than looping forever. A node that is merely
|
|
2032
|
-
// waiting on a still-running prerequisite
|
|
2033
|
-
//
|
|
2133
|
+
// waiting on a still-running prerequisite — or on a clarification answer —
|
|
2134
|
+
// is NOT dead; the liveness analysis behind the sweep holds it pending.
|
|
2034
2135
|
if (implementBlocks.length === 0) {
|
|
2035
|
-
|
|
2036
|
-
for (const findingId of block.items) {
|
|
2037
|
-
const it = state.items?.[findingId];
|
|
2038
|
-
if (!it || it.status !== "pending")
|
|
2039
|
-
continue;
|
|
2040
|
-
it.status = "blocked";
|
|
2041
|
-
it.failure_reason =
|
|
2042
|
-
it.failure_reason ??
|
|
2043
|
-
"A dependency node did not reach a verified-complete disposition " +
|
|
2044
|
-
"(a prerequisite was skipped, blocked, or the dependencies are cyclic); " +
|
|
2045
|
-
"the host handoff will not expose this node against an upstream " +
|
|
2046
|
-
"surface that never landed (INV-RS-01).";
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2136
|
+
sweepPermanentlyDeadBlocks(state);
|
|
2049
2137
|
}
|
|
2050
2138
|
state.status = "implementing";
|
|
2051
2139
|
await store.saveState(state);
|
|
@@ -3236,7 +3324,7 @@ export function buildMainObligations(ctx) {
|
|
|
3236
3324
|
derive: (state) => state != null &&
|
|
3237
3325
|
(state.status === "implementing" || state.status === "triage") &&
|
|
3238
3326
|
hasUnansweredClarification(state) &&
|
|
3239
|
-
|
|
3327
|
+
dispatchFrontier(state).length === 0
|
|
3240
3328
|
? "missing"
|
|
3241
3329
|
: "satisfied",
|
|
3242
3330
|
execute: async (state) => {
|
|
@@ -3251,9 +3339,9 @@ export function buildMainObligations(ctx) {
|
|
|
3251
3339
|
derive: (state) => state?.status === "implementing" ? "missing" : "satisfied",
|
|
3252
3340
|
execute: async (state) => {
|
|
3253
3341
|
const s = requireState(state);
|
|
3254
|
-
//
|
|
3255
|
-
// has left "pending".
|
|
3256
|
-
const pendingBlocks =
|
|
3342
|
+
// A non-empty dispatch frontier dispatches; triage only runs once every
|
|
3343
|
+
// item has left "pending".
|
|
3344
|
+
const pendingBlocks = dispatchFrontier(s);
|
|
3257
3345
|
if (pendingBlocks.length > 0) {
|
|
3258
3346
|
// Per-phase boundary gate (T3): before opening a phase P > 0, run the
|
|
3259
3347
|
// whole-repo suite once over the just-landed foundations. A red PAUSES
|
|
@@ -3277,33 +3365,12 @@ export function buildMainObligations(ctx) {
|
|
|
3277
3365
|
runLogger,
|
|
3278
3366
|
});
|
|
3279
3367
|
}
|
|
3280
|
-
// Dead-end pending nodes
|
|
3281
|
-
//
|
|
3282
|
-
//
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
let changed = false;
|
|
3287
|
-
for (const block of deadEnded) {
|
|
3288
|
-
for (const findingId of block.items) {
|
|
3289
|
-
const it = s.items?.[findingId];
|
|
3290
|
-
if (!it || it.status !== "pending")
|
|
3291
|
-
continue;
|
|
3292
|
-
it.status = "blocked";
|
|
3293
|
-
it.started_at ??= now;
|
|
3294
|
-
it.completed_at = now;
|
|
3295
|
-
it.failure_reason =
|
|
3296
|
-
it.failure_reason ??
|
|
3297
|
-
"A dependency node did not reach a verified-complete disposition " +
|
|
3298
|
-
"(a prerequisite was skipped, blocked, or the dependencies are cyclic); " +
|
|
3299
|
-
"the host handoff will not expose this node (INV-RS-01).";
|
|
3300
|
-
changed = true;
|
|
3301
|
-
}
|
|
3302
|
-
}
|
|
3303
|
-
if (changed) {
|
|
3304
|
-
await store.saveState(s);
|
|
3305
|
-
return { kind: "transition", state: s };
|
|
3306
|
-
}
|
|
3368
|
+
// Dead-end pending nodes that can never enter the frontier (INV-RS-01)
|
|
3369
|
+
// so the implementing→triage loop can't livelock; transition so the
|
|
3370
|
+
// engine re-scans on the updated state.
|
|
3371
|
+
if (sweepPermanentlyDeadBlocks(s)) {
|
|
3372
|
+
await store.saveState(s);
|
|
3373
|
+
return { kind: "transition", state: s };
|
|
3307
3374
|
}
|
|
3308
3375
|
return handleImplementing(root, artifactsDir, s, runLogger, store, options);
|
|
3309
3376
|
},
|