codex-workflow-v2 2.0.0-beta.13.3 → 2.0.0-beta.13.6
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/README.md +4 -1
- package/dist/reviewer-runtime-build.json +23 -11
- package/dist/src/alpha6/plan-risk.d.ts +1 -0
- package/dist/src/alpha6/plan-risk.js +9 -6
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- package/dist/src/alpha6/preexecution-replan.d.ts +38 -0
- package/dist/src/alpha6/preexecution-replan.js +130 -0
- package/dist/src/alpha6/preexecution-replan.js.map +1 -0
- package/dist/src/alpha6/remediation.d.ts +4 -3
- package/dist/src/alpha6/remediation.js +147 -21
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/root-cause-replan-carryover.d.ts +17 -0
- package/dist/src/alpha6/root-cause-replan-carryover.js +372 -0
- package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -0
- package/dist/src/cli-actions.d.ts +2 -2
- package/dist/src/cli-actions.js +2 -0
- package/dist/src/cli-actions.js.map +1 -1
- package/dist/src/cli.js +51 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +35 -0
- package/dist/src/dependency-provenance.d.ts +3 -2
- package/dist/src/dependency-provenance.js +33 -2
- package/dist/src/dependency-provenance.js.map +1 -1
- package/dist/src/domain/plan-semantics.d.ts +11 -0
- package/dist/src/domain/plan-semantics.js +49 -0
- package/dist/src/domain/plan-semantics.js.map +1 -0
- package/dist/src/gateway-handshake.js +1 -0
- package/dist/src/gateway-handshake.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/workflow.d.ts +21 -1
- package/dist/src/workflow.js +793 -89
- package/dist/src/workflow.js.map +1 -1
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +1 -1
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +2 -2
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +2 -2
- package/docs/release.md +72 -0
- package/package.json +1 -1
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +72 -2
- package/schemas/preexecution-replan-event.schema.json +45 -0
- package/schemas/task.schema.json +55 -0
package/dist/src/workflow.js
CHANGED
|
@@ -3,11 +3,12 @@ import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from
|
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
|
|
6
|
-
import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, } from './dependency-provenance.js';
|
|
6
|
+
import { inspectBoundedWorkflowDependencyCommit, inspectDependencyKnowledgeRefresh, inspectDependencyProvenanceRecovery, workflowDependencyVersionAt, } from './dependency-provenance.js';
|
|
7
7
|
import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, } from './historical-step-provenance.js';
|
|
8
8
|
import { renderBrief, renderPlan, renderResult } from './artifacts.js';
|
|
9
9
|
import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
|
|
10
10
|
import { assessDiscovery, mergeUnique } from './domain/discovery.js';
|
|
11
|
+
import { normalizePlanKnowledgeBinding, readVerifiedPlanArtifact } from './domain/plan-semantics.js';
|
|
11
12
|
import { deriveCompletedStepCarryover, normalizePlanStepDefinition, } from './domain/completed-step-carryover.js';
|
|
12
13
|
import { pathAllowed, validatePlan } from './domain/validation.js';
|
|
13
14
|
import { WorkflowError } from './errors.js';
|
|
@@ -17,8 +18,10 @@ import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.j
|
|
|
17
18
|
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
18
19
|
import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
|
|
19
20
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
20
|
-
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
21
|
-
import {
|
|
21
|
+
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, buildCandidatePlanRiskAuditEvent, buildPlanRiskAuditEvent, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
22
|
+
import { hashCompletedSteps, hashExecutionAuthorization, preparePreExecutionReplanEvent, preExecutionReplanSidecar, readCurrentPreExecutionReplan, } from './alpha6/preexecution-replan.js';
|
|
23
|
+
import { assessRootCauseReplanDirtyCarryover, } from './alpha6/root-cause-replan-carryover.js';
|
|
24
|
+
import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, deriveCurrentCauseDecisionForTask, prepareRemediationEvent, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
|
|
22
25
|
import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
|
|
23
26
|
import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
|
|
24
27
|
import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
|
|
@@ -28,7 +31,7 @@ import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, en
|
|
|
28
31
|
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
29
32
|
import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, readValidatedStopOverrideContextRebindIfApplicable, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
|
|
30
33
|
import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
|
|
31
|
-
import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
|
|
34
|
+
import { canonicalJsonStringify, prepareSidecarAppend, sha256Hex } from './alpha6/store-sidecars.js';
|
|
32
35
|
import { ensureDirectory } from './fs-utils.js';
|
|
33
36
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
34
37
|
import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
|
|
@@ -47,9 +50,11 @@ const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
|
|
|
47
50
|
export class WorkflowService {
|
|
48
51
|
store;
|
|
49
52
|
now;
|
|
50
|
-
|
|
53
|
+
transactionHooks;
|
|
54
|
+
constructor(store = new FileStateStore(), now = () => new Date(), transactionHooks = {}) {
|
|
51
55
|
this.store = store;
|
|
52
56
|
this.now = now;
|
|
57
|
+
this.transactionHooks = transactionHooks;
|
|
53
58
|
}
|
|
54
59
|
#mutationContext(repository) {
|
|
55
60
|
const identity = resolveRepositoryIdentity(repository);
|
|
@@ -1601,6 +1606,10 @@ export class WorkflowService {
|
|
|
1601
1606
|
this.validateKnowledgeTargets(knowledgeMap, plan);
|
|
1602
1607
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1603
1608
|
assertExpectedRevision(task, expectedRevision);
|
|
1609
|
+
const preExecutionReplan = readCurrentPreExecutionReplan(this.store, task);
|
|
1610
|
+
const preExecutionReplanAudits = preExecutionReplan
|
|
1611
|
+
? resolvePreExecutionReplanAudits(this.store, task, preExecutionReplan)
|
|
1612
|
+
: null;
|
|
1604
1613
|
const correctiveDecisionEvents = task.planHash ? readCorrectiveDecisionEvents(this.store, task) : [];
|
|
1605
1614
|
const currentPlanCorrectiveDecisions = task.planHash
|
|
1606
1615
|
? correctiveDecisionEvents.filter((event) => event.planHash === task.planHash)
|
|
@@ -1747,6 +1756,9 @@ export class WorkflowService {
|
|
|
1747
1756
|
}
|
|
1748
1757
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
1749
1758
|
const preparedPlan = prepareCorrectiveReplanCandidate(task, plan);
|
|
1759
|
+
const changesPlanSemantics = !task.planHash
|
|
1760
|
+
|| normalizePlanKnowledgeBinding(preparedPlan.planBytes, 'candidate').semanticHash
|
|
1761
|
+
!== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(root, task.planHash, task.planHash), 'current').semanticHash;
|
|
1750
1762
|
const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
1751
1763
|
? { ...authorization, decision: 'superseded' }
|
|
1752
1764
|
: authorization);
|
|
@@ -1764,6 +1776,83 @@ export class WorkflowService {
|
|
|
1764
1776
|
...(task.invalidatedStepCommits ?? []),
|
|
1765
1777
|
...omittedStepEvidenceCommits(task, plannedSteps),
|
|
1766
1778
|
])];
|
|
1779
|
+
if (preExecutionReplan && preExecutionReplanAudits) {
|
|
1780
|
+
const replanC1Posture = readTaskC1Posture(this.store, task);
|
|
1781
|
+
const replanWriterLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
|
|
1782
|
+
if (replanC1Posture.state !== 'none' || replanWriterLease) {
|
|
1783
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replacement Plan requires no C1 posture or writer lease.', {
|
|
1784
|
+
taskId: task.id,
|
|
1785
|
+
c1Posture: replanC1Posture.state,
|
|
1786
|
+
writerLeasePresent: replanWriterLease !== null,
|
|
1787
|
+
});
|
|
1788
|
+
}
|
|
1789
|
+
if (!planRiskAudit) {
|
|
1790
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires an atomic replacement Plan Risk Audit.');
|
|
1791
|
+
}
|
|
1792
|
+
if (task.planObjective !== plan.objective
|
|
1793
|
+
|| canonicalJsonStringify(task.requirements) !== canonicalJsonStringify(plan.requirements)
|
|
1794
|
+
|| canonicalJsonStringify(task.acceptance) !== canonicalJsonStringify(plan.acceptance)) {
|
|
1795
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot change Task objective, requirements, or acceptance.', {
|
|
1796
|
+
taskId: task.id,
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
if (!changesPlanSemantics) {
|
|
1800
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a semantic Plan change.', { taskId: task.id });
|
|
1801
|
+
}
|
|
1802
|
+
const candidateSteps = new Map(plan.steps.map((step) => [step.id, step]));
|
|
1803
|
+
const missingStepIds = task.steps.filter((step) => !candidateSteps.has(step.id)).map((step) => step.id);
|
|
1804
|
+
if (missingStepIds.length > 0) {
|
|
1805
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must retain every existing Step identifier.', {
|
|
1806
|
+
taskId: task.id,
|
|
1807
|
+
missingStepIds,
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
const completedStepIds = task.steps.filter((step) => step.status === 'completed').map((step) => step.id);
|
|
1811
|
+
const lostCompletedStepIds = completedStepIds.filter((stepId) => !retainedCompletedSteps.has(stepId));
|
|
1812
|
+
if (lostCompletedStepIds.length > 0) {
|
|
1813
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must preserve the exact completed Step subgraph and evidence.', {
|
|
1814
|
+
taskId: task.id,
|
|
1815
|
+
lostCompletedStepIds,
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
const weakenedForbiddenScope = task.steps.flatMap((step) => {
|
|
1819
|
+
const candidate = candidateSteps.get(step.id);
|
|
1820
|
+
const missing = step.forbiddenScope.filter((scope) => !candidate.forbiddenScope.includes(scope));
|
|
1821
|
+
return missing.length > 0 ? [{ stepId: step.id, missing }] : [];
|
|
1822
|
+
});
|
|
1823
|
+
if (weakenedForbiddenScope.length > 0) {
|
|
1824
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot weaken existing forbiddenScope boundaries.', {
|
|
1825
|
+
taskId: task.id,
|
|
1826
|
+
weakenedForbiddenScope,
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
const missingRisks = task.risks.filter((risk) => !plan.risks.includes(risk));
|
|
1830
|
+
if (missingRisks.length > 0) {
|
|
1831
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot suppress existing Plan risks.', {
|
|
1832
|
+
taskId: task.id,
|
|
1833
|
+
missingRisks,
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
assertPlanRiskSafetyPreserved(preExecutionReplanAudits.obstructionAudit, planRiskAudit, 'Replacement Plan Risk Audit');
|
|
1837
|
+
const candidateEdgeKeys = new Set((plan.productionEdges ?? []).map((edge) => canonicalJsonStringify(edge)));
|
|
1838
|
+
const missingProductionEdges = (task.productionEdges ?? [])
|
|
1839
|
+
.filter((edge) => !candidateEdgeKeys.has(canonicalJsonStringify(edge)));
|
|
1840
|
+
if ((task.requiresProductionEdges === true && plan.requiresProductionEdges !== true)
|
|
1841
|
+
|| missingProductionEdges.length > 0) {
|
|
1842
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot weaken production-edge authority.', {
|
|
1843
|
+
taskId: task.id,
|
|
1844
|
+
missingProductionEdges,
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== preExecutionReplan.sourceHeadCommit) {
|
|
1848
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Pre-execution replacement Plan requires the exact clean recorded HEAD.', {
|
|
1849
|
+
taskId: task.id,
|
|
1850
|
+
expectedHead: preExecutionReplan.sourceHeadCommit,
|
|
1851
|
+
actualHead: headCommit(identity.repositoryRoot),
|
|
1852
|
+
changedFiles: changedFiles(identity.repositoryRoot),
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1767
1856
|
if (task.status === 'needs_fix' &&
|
|
1768
1857
|
task.review.status === 'failed' &&
|
|
1769
1858
|
plannedSteps.every((step) => step.status === 'completed')) {
|
|
@@ -1787,7 +1876,7 @@ export class WorkflowService {
|
|
|
1787
1876
|
: []),
|
|
1788
1877
|
...(rawWriterLease ? ['task.rolling-replan.no-writer-lease'] : []),
|
|
1789
1878
|
...(c1Posture.state !== 'none' ? ['task.rolling-replan.no-c1-claim'] : []),
|
|
1790
|
-
...(
|
|
1879
|
+
...(!changesPlanSemantics
|
|
1791
1880
|
? ['task.rolling-replan.semantic-plan-change']
|
|
1792
1881
|
: []),
|
|
1793
1882
|
...(!preparedPlan.stepIds.includes(correctiveReplanPosture.stepId)
|
|
@@ -1860,6 +1949,61 @@ export class WorkflowService {
|
|
|
1860
1949
|
};
|
|
1861
1950
|
if (planRiskAudit)
|
|
1862
1951
|
validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
|
|
1952
|
+
if (preExecutionReplan && preExecutionReplanAudits && planRiskAudit) {
|
|
1953
|
+
const now = this.now();
|
|
1954
|
+
const resultTask = {
|
|
1955
|
+
...candidate,
|
|
1956
|
+
revision: task.revision + 1,
|
|
1957
|
+
updatedAt: now.toISOString(),
|
|
1958
|
+
};
|
|
1959
|
+
const preparedAuditBase = buildCandidatePlanRiskAuditEvent(this.store, resultTask, planRiskAudit, now.toISOString(), now.getTime());
|
|
1960
|
+
const preparedAudit = prepareSidecarAppend(readPlanRiskAuditEvents(this.store, task), preparedAuditBase.event);
|
|
1961
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
1962
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
1963
|
+
const versionFile = path.join(root, '.versions', `plan.md.${preparedPlan.planHash}`);
|
|
1964
|
+
if (existsSync(versionFile) && sha256Hex(readFileSync(versionFile)) !== preparedPlan.planHash) {
|
|
1965
|
+
throw new WorkflowError('STATE_CORRUPT', 'Immutable replacement Plan version is corrupt.', {
|
|
1966
|
+
taskId: task.id,
|
|
1967
|
+
planHash: preparedPlan.planHash,
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
applyProjectTransaction({
|
|
1971
|
+
projectRoot,
|
|
1972
|
+
projectId: task.projectId,
|
|
1973
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
1974
|
+
kind: 'preexecution-replacement-plan-set',
|
|
1975
|
+
now,
|
|
1976
|
+
targets: [
|
|
1977
|
+
{ path: relative(path.join(root, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
|
|
1978
|
+
{ path: relative(path.join(root, 'plan.md')), content: preparedPlan.planBytes },
|
|
1979
|
+
{ path: relative(versionFile), content: preparedPlan.planBytes },
|
|
1980
|
+
{ path: relative(path.join(root, 'plan-risk-audits.jsonl')), content: preparedAudit.content },
|
|
1981
|
+
],
|
|
1982
|
+
...(this.transactionHooks.afterPreExecutionReplanTargetWrite
|
|
1983
|
+
? { afterTargetWrite: this.transactionHooks.afterPreExecutionReplanTargetWrite }
|
|
1984
|
+
: {}),
|
|
1985
|
+
validate: () => {
|
|
1986
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
1987
|
+
const currentAudit = readCurrentPlanRiskAudit(this.store, saved, { requireCurrentTaskRevision: true });
|
|
1988
|
+
if (saved.revision !== resultTask.revision
|
|
1989
|
+
|| saved.status !== 'awaiting_execution_authorization'
|
|
1990
|
+
|| saved.planHash !== preparedPlan.planHash
|
|
1991
|
+
|| this.store.hashArtifact(root, 'plan.md') !== preparedPlan.planHash
|
|
1992
|
+
|| sha256Hex(readFileSync(versionFile)) !== preparedPlan.planHash
|
|
1993
|
+
|| currentAudit?.audit.eventHash !== preparedAudit.event.eventHash
|
|
1994
|
+
|| hashCompletedSteps(saved) !== preExecutionReplan.completedStepsHash
|
|
1995
|
+
|| readCurrentPreExecutionReplan(this.store, saved) !== null
|
|
1996
|
+
|| saved.authorizations.some((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved')) {
|
|
1997
|
+
throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replacement Plan transaction readback failed.', {
|
|
1998
|
+
taskId: task.id,
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
if (saved.baseCommit)
|
|
2002
|
+
validateTaskHistory(identity.repositoryRoot, saved);
|
|
2003
|
+
},
|
|
2004
|
+
});
|
|
2005
|
+
return this.store.readTask(task.projectId, task.id);
|
|
2006
|
+
}
|
|
1863
2007
|
const stagedPlan = this.store.stageArtifact(root, 'plan.md', preparedPlan.planBytes);
|
|
1864
2008
|
if (stagedPlan.hash !== preparedPlan.planHash) {
|
|
1865
2009
|
throw new WorkflowError('STATE_CORRUPT', 'Prepared candidate Plan hash changed during staging.', {
|
|
@@ -2068,6 +2212,9 @@ export class WorkflowService {
|
|
|
2068
2212
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
2069
2213
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2070
2214
|
assertExpectedRevision(task, expectedRevision);
|
|
2215
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2216
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a semantic replacement Plan before execution authorization.', { taskId, requiredAction: 'task plan-set' });
|
|
2217
|
+
}
|
|
2071
2218
|
if (task.status !== 'awaiting_execution_authorization' || !task.planHash) {
|
|
2072
2219
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not awaiting execution authorization.`);
|
|
2073
2220
|
}
|
|
@@ -2125,9 +2272,143 @@ export class WorkflowService {
|
|
|
2125
2272
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
2126
2273
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2127
2274
|
assertExpectedRevision(task, expectedRevision);
|
|
2275
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2276
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires task plan-set; the blocking audit cannot be replaced in place.', { taskId, requiredAction: 'task plan-set' });
|
|
2277
|
+
}
|
|
2128
2278
|
this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
|
|
2129
2279
|
return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
|
|
2130
2280
|
}
|
|
2281
|
+
recordPreExecutionReplan(repository, taskId, expectedRevision, actor, reason, binding, input) {
|
|
2282
|
+
const context = this.#mutationContext(repository);
|
|
2283
|
+
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
2284
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2285
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2286
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2287
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task already has a pending pre-execution replan.', {
|
|
2288
|
+
taskId,
|
|
2289
|
+
requiredAction: 'task plan-set',
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
if (task.status !== 'ready' || task.steps.some((step) => ['in_progress', 'failed'].includes(step.status))) {
|
|
2293
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a ready Task with no Step in progress or failed.', {
|
|
2294
|
+
taskId,
|
|
2295
|
+
status: task.status,
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
if (!isClean(context.identity.repositoryRoot)) {
|
|
2299
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Pre-execution replan requires a clean checkout.', {
|
|
2300
|
+
changedFiles: changedFiles(context.identity.repositoryRoot),
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
const c1Posture = readTaskC1Posture(this.store, task);
|
|
2304
|
+
const writerLease = context.locks.inspect(task.id);
|
|
2305
|
+
if (c1Posture.state !== 'none' || writerLease) {
|
|
2306
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a clean authority boundary without C1 or writer lease.', {
|
|
2307
|
+
taskId,
|
|
2308
|
+
c1Posture: c1Posture.state,
|
|
2309
|
+
writerLeasePresent: writerLease !== null,
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
2313
|
+
if (task.baseCommit) {
|
|
2314
|
+
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
2315
|
+
}
|
|
2316
|
+
else if (task.workspaceOwner !== null || task.taskBranch !== null
|
|
2317
|
+
|| task.steps.some((step) => step.status === 'completed')) {
|
|
2318
|
+
throw new WorkflowError('STATE_CORRUPT', 'Unstarted pre-execution replan Task has inconsistent execution history.', {
|
|
2319
|
+
taskId,
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
const authorization = this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
2323
|
+
const sourceAudit = readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: false });
|
|
2324
|
+
if (!sourceAudit || !['approved', 'approved-with-rationale'].includes(sourceAudit.audit.decision)) {
|
|
2325
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires the current positive Plan Risk Audit.');
|
|
2326
|
+
}
|
|
2327
|
+
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
2328
|
+
const actualBinding = {
|
|
2329
|
+
expectedBriefHash: task.briefHash,
|
|
2330
|
+
expectedPlanHash: task.planHash,
|
|
2331
|
+
expectedPlanRiskAuditEventHash: sourceAudit.audit.eventHash,
|
|
2332
|
+
expectedHeadCommit: currentHead,
|
|
2333
|
+
};
|
|
2334
|
+
if (canonicalJsonStringify(binding) !== canonicalJsonStringify(actualBinding)) {
|
|
2335
|
+
throw new WorkflowError('STATE_CONFLICT', 'Pre-execution replan source binding is stale.', {
|
|
2336
|
+
expected: binding,
|
|
2337
|
+
actual: actualBinding,
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
if (input.decision !== 'stop-escalate') {
|
|
2341
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Pre-execution replan requires a stop-escalate Plan Risk Audit.');
|
|
2342
|
+
}
|
|
2343
|
+
if (actor.trim() !== input.auditor.trim() || input.planner.trim() !== sourceAudit.audit.planner) {
|
|
2344
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must be recorded by the independent auditor against the original planner.', {
|
|
2345
|
+
actor: actor.trim(),
|
|
2346
|
+
auditor: input.auditor.trim(),
|
|
2347
|
+
expectedPlanner: sourceAudit.audit.planner,
|
|
2348
|
+
actualPlanner: input.planner.trim(),
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
assertPlanRiskSafetyPreserved(sourceAudit.audit, input, 'Pre-execution replan audit');
|
|
2352
|
+
const now = this.now();
|
|
2353
|
+
const preparedAuditBase = buildPlanRiskAuditEvent(this.store, task, input, now.toISOString(), now.getTime());
|
|
2354
|
+
const preparedAudit = prepareSidecarAppend(readPlanRiskAuditEvents(this.store, task), preparedAuditBase.event);
|
|
2355
|
+
const preparedMarker = preparePreExecutionReplanEvent(this.store, task, {
|
|
2356
|
+
sourceAudit: sourceAudit.audit,
|
|
2357
|
+
obstructionAudit: preparedAudit.event,
|
|
2358
|
+
authorization,
|
|
2359
|
+
sourceHeadCommit: currentHead,
|
|
2360
|
+
actor,
|
|
2361
|
+
reason,
|
|
2362
|
+
}, now);
|
|
2363
|
+
const resultTask = {
|
|
2364
|
+
...task,
|
|
2365
|
+
revision: task.revision + 1,
|
|
2366
|
+
updatedAt: now.toISOString(),
|
|
2367
|
+
status: 'awaiting_execution_authorization',
|
|
2368
|
+
authorizations: task.authorizations.map((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved'
|
|
2369
|
+
? { ...candidate, decision: 'superseded' }
|
|
2370
|
+
: candidate),
|
|
2371
|
+
blockReason: reason.trim(),
|
|
2372
|
+
};
|
|
2373
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
2374
|
+
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
2375
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
2376
|
+
applyProjectTransaction({
|
|
2377
|
+
projectRoot,
|
|
2378
|
+
projectId: task.projectId,
|
|
2379
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
2380
|
+
kind: 'preexecution-plan-authority-withdrawal',
|
|
2381
|
+
now,
|
|
2382
|
+
targets: [
|
|
2383
|
+
{ path: relative(path.join(taskRoot, 'plan-risk-audits.jsonl')), content: preparedAudit.content },
|
|
2384
|
+
{ path: relative(path.join(taskRoot, preExecutionReplanSidecar())), content: preparedMarker.postimage },
|
|
2385
|
+
{ path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
|
|
2386
|
+
],
|
|
2387
|
+
validate: () => {
|
|
2388
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
2389
|
+
const latestAudit = readPlanRiskAuditEvents(this.store, saved).at(-1) ?? null;
|
|
2390
|
+
const marker = readCurrentPreExecutionReplan(this.store, saved);
|
|
2391
|
+
if (saved.revision !== resultTask.revision
|
|
2392
|
+
|| saved.status !== 'awaiting_execution_authorization'
|
|
2393
|
+
|| saved.planHash !== task.planHash
|
|
2394
|
+
|| latestAudit?.eventHash !== preparedAudit.event.eventHash
|
|
2395
|
+
|| marker?.eventHash !== preparedMarker.event.eventHash
|
|
2396
|
+
|| saved.authorizations.some((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved')
|
|
2397
|
+
|| hashCompletedSteps(saved) !== preparedMarker.event.completedStepsHash
|
|
2398
|
+
|| headCommit(context.identity.repositoryRoot) !== currentHead
|
|
2399
|
+
|| !isClean(context.identity.repositoryRoot)) {
|
|
2400
|
+
throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replan transaction readback failed.', { taskId });
|
|
2401
|
+
}
|
|
2402
|
+
if (saved.baseCommit)
|
|
2403
|
+
validateTaskHistory(context.identity.repositoryRoot, saved);
|
|
2404
|
+
},
|
|
2405
|
+
});
|
|
2406
|
+
return {
|
|
2407
|
+
task: this.store.readTask(task.projectId, task.id),
|
|
2408
|
+
audit: preparedAudit.event,
|
|
2409
|
+
event: preparedMarker.event,
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2131
2412
|
showTaskHandoff(repository, taskId) {
|
|
2132
2413
|
const { identity } = this.#mutationContext(repository);
|
|
2133
2414
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
@@ -2141,6 +2422,12 @@ export class WorkflowService {
|
|
|
2141
2422
|
const context = this.#mutationContext(repository);
|
|
2142
2423
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2143
2424
|
assertExpectedRevision(task, expectedRevision);
|
|
2425
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2426
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task ownership cannot change while a pre-execution replan is pending.', {
|
|
2427
|
+
taskId,
|
|
2428
|
+
requiredAction: 'task plan-set',
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2144
2431
|
const normalizedActor = actor.trim();
|
|
2145
2432
|
if (!normalizedActor) {
|
|
2146
2433
|
throw new WorkflowError('INVALID_ARGUMENT', 'Handoff actor is required.');
|
|
@@ -2470,11 +2757,15 @@ export class WorkflowService {
|
|
|
2470
2757
|
const planIntegrityDirtyCarryover = step.status === 'planned' && dirty.length > 0
|
|
2471
2758
|
? this.assessPostRebindCheckSupportDirtyCarryover(context.identity.repositoryRoot, task, stepId)
|
|
2472
2759
|
: null;
|
|
2760
|
+
const rootCauseReplanDirtyCarryover = step.status === 'planned' && dirty.length > 0
|
|
2761
|
+
? this.assessCurrentRootCauseReplanDirtyCarryover(context.identity.repositoryRoot, task, stepId)
|
|
2762
|
+
: null;
|
|
2473
2763
|
if (step.status === 'planned'
|
|
2474
2764
|
&& dirty.length > 0
|
|
2475
2765
|
&& !resumesCorrectiveReplan
|
|
2476
2766
|
&& !downstreamProofCarryover?.eligible
|
|
2477
|
-
&& !planIntegrityDirtyCarryover?.eligible
|
|
2767
|
+
&& !planIntegrityDirtyCarryover?.eligible
|
|
2768
|
+
&& !rootCauseReplanDirtyCarryover?.eligible) {
|
|
2478
2769
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
2479
2770
|
changedFiles: dirty,
|
|
2480
2771
|
...(downstreamProofCarryover?.applicable
|
|
@@ -2483,6 +2774,9 @@ export class WorkflowService {
|
|
|
2483
2774
|
...(planIntegrityDirtyCarryover?.applicable
|
|
2484
2775
|
? { planIntegrityDirtyCarryoverBlockers: planIntegrityDirtyCarryover.blockers }
|
|
2485
2776
|
: {}),
|
|
2777
|
+
...(rootCauseReplanDirtyCarryover?.applicable
|
|
2778
|
+
? { rootCauseReplanDirtyCarryoverBlockers: rootCauseReplanDirtyCarryover.blockers }
|
|
2779
|
+
: {}),
|
|
2486
2780
|
});
|
|
2487
2781
|
}
|
|
2488
2782
|
const dirtyOutsideStep = dirty.filter((file) => !pathAllowed(file, allowedWrites));
|
|
@@ -2748,7 +3042,7 @@ export class WorkflowService {
|
|
|
2748
3042
|
const resultingHistory = !preparedRemediation || preparedRemediation.postimage === null
|
|
2749
3043
|
? stepHistory
|
|
2750
3044
|
: [...stepHistory, preparedRemediation.event];
|
|
2751
|
-
const rollingCauseDecision =
|
|
3045
|
+
const rollingCauseDecision = deriveCurrentCauseDecisionForTask(this.store, task, stepId, resultingHistory);
|
|
2752
3046
|
const c1Posture = readTaskC1Posture(this.store, task);
|
|
2753
3047
|
const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed'
|
|
2754
3048
|
? prepareTaskCorrectiveYield(this.store, paused, {
|
|
@@ -3062,7 +3356,7 @@ export class WorkflowService {
|
|
|
3062
3356
|
const resultingHistory = preparedRemediation.postimage === null
|
|
3063
3357
|
? stepHistory
|
|
3064
3358
|
: [...stepHistory, preparedRemediation.event];
|
|
3065
|
-
const rollingCauseDecision =
|
|
3359
|
+
const rollingCauseDecision = deriveCurrentCauseDecisionForTask(this.store, task, stepId, resultingHistory);
|
|
3066
3360
|
const resolved = applyStrictStepReviewResolution(task, stepId, 'failed', summary);
|
|
3067
3361
|
const paused = {
|
|
3068
3362
|
...resolved,
|
|
@@ -3809,6 +4103,9 @@ export class WorkflowService {
|
|
|
3809
4103
|
const now = this.now().getTime();
|
|
3810
4104
|
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
3811
4105
|
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
4106
|
+
const pendingPreExecutionReplanTaskIds = tasks
|
|
4107
|
+
.filter((task) => readCurrentPreExecutionReplan(this.store, task) !== null)
|
|
4108
|
+
.map((task) => task.id);
|
|
3812
4109
|
const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
3813
4110
|
const inspectedVersions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
|
|
3814
4111
|
const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
|
|
@@ -3836,6 +4133,9 @@ export class WorkflowService {
|
|
|
3836
4133
|
...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
|
|
3837
4134
|
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
3838
4135
|
...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
|
|
4136
|
+
...(pendingPreExecutionReplanTaskIds.length > 0
|
|
4137
|
+
? [`Complete the pending pre-execution Plan replacement and reauthorization first: ${pendingPreExecutionReplanTaskIds.join(', ')}.`]
|
|
4138
|
+
: []),
|
|
3839
4139
|
...versionBlockers,
|
|
3840
4140
|
...observationPreflightBlockers(snapshot.observation),
|
|
3841
4141
|
];
|
|
@@ -3861,10 +4161,12 @@ export class WorkflowService {
|
|
|
3861
4161
|
milestoneBases,
|
|
3862
4162
|
},
|
|
3863
4163
|
dependencyTransport: {
|
|
3864
|
-
safe: versionBlockers.length === 0,
|
|
3865
|
-
requiredAction:
|
|
3866
|
-
?
|
|
3867
|
-
:
|
|
4164
|
+
safe: versionBlockers.length === 0 && pendingPreExecutionReplanTaskIds.length === 0,
|
|
4165
|
+
requiredAction: pendingPreExecutionReplanTaskIds.length > 0
|
|
4166
|
+
? 'Complete each pending pre-execution task plan-set and reauthorization before changing Workflow dependency HEAD.'
|
|
4167
|
+
: versionBlockers.length === 0
|
|
4168
|
+
? null
|
|
4169
|
+
: 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
|
|
3868
4170
|
},
|
|
3869
4171
|
adoption: snapshot.adoption,
|
|
3870
4172
|
observation: snapshot.observation,
|
|
@@ -3880,7 +4182,145 @@ export class WorkflowService {
|
|
|
3880
4182
|
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
3881
4183
|
if (!task)
|
|
3882
4184
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3883
|
-
|
|
4185
|
+
const carryover = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task);
|
|
4186
|
+
const preflight = this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, null, carryover.eligible ? carryover.compatibility : null);
|
|
4187
|
+
if (!preflight.rootCauseReplan)
|
|
4188
|
+
return preflight;
|
|
4189
|
+
let baseTip = null;
|
|
4190
|
+
let baseDependency = null;
|
|
4191
|
+
try {
|
|
4192
|
+
baseTip = runGit(snapshot.identity.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
4193
|
+
baseDependency = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, baseTip);
|
|
4194
|
+
}
|
|
4195
|
+
catch {
|
|
4196
|
+
// The blocker below keeps a missing or unreadable base reference fail closed.
|
|
4197
|
+
}
|
|
4198
|
+
const bridgeBlockers = [
|
|
4199
|
+
...(preflight.dependencyCommits.length !== 1
|
|
4200
|
+
? ['Corrective carryover recovery requires exactly one Task dependency-only commit.'] : []),
|
|
4201
|
+
...(preflight.parentCommitSha !== preflight.rootCauseReplan.sourceHeadCommit
|
|
4202
|
+
? ['Corrective carryover dependency parent must equal the bound source HEAD.'] : []),
|
|
4203
|
+
...(preflight.parentCommitSha
|
|
4204
|
+
&& workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha) !== '2.0.0-beta.13.5'
|
|
4205
|
+
? ['Corrective carryover dependency parent must declare exact source 2.0.0-beta.13.5.'] : []),
|
|
4206
|
+
...(preflight.dependencyCommits.at(-1)?.packageVersion !== '2.0.0-beta.13.6'
|
|
4207
|
+
? ['Corrective carryover dependency commit must declare exact target 2.0.0-beta.13.6.'] : []),
|
|
4208
|
+
...(!baseDependency
|
|
4209
|
+
|| baseDependency.commitSha !== baseTip
|
|
4210
|
+
|| baseDependency.packageVersion !== '2.0.0-beta.13.6'
|
|
4211
|
+
|| workflowDependencyVersionAt(snapshot.identity.repositoryRoot, baseDependency.parentCommitSha) !== '2.0.0-beta.13.5'
|
|
4212
|
+
? ['Corrective carryover Milestone-base tip must be one exact 2.0.0-beta.13.5 to 2.0.0-beta.13.6 dependency-only commit.'] : []),
|
|
4213
|
+
];
|
|
4214
|
+
return bridgeBlockers.length === 0
|
|
4215
|
+
? preflight
|
|
4216
|
+
: {
|
|
4217
|
+
...preflight,
|
|
4218
|
+
eligible: false,
|
|
4219
|
+
action: null,
|
|
4220
|
+
commitSha: null,
|
|
4221
|
+
parentCommitSha: null,
|
|
4222
|
+
blockers: [...new Set([...preflight.blockers, ...bridgeBlockers])],
|
|
4223
|
+
};
|
|
4224
|
+
}
|
|
4225
|
+
correctiveCarryoverUpdatePreflight(repository, taskId) {
|
|
4226
|
+
const snapshot = this.observationSnapshot(repository);
|
|
4227
|
+
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4228
|
+
if (!task)
|
|
4229
|
+
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4230
|
+
const sourceVersion = '2.0.0-beta.13.5';
|
|
4231
|
+
const targetVersion = '2.0.0-beta.13.6';
|
|
4232
|
+
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4233
|
+
const sourceSurfaces = {
|
|
4234
|
+
declared: versions.declared,
|
|
4235
|
+
locked: versions.locked,
|
|
4236
|
+
installed: versions.installed,
|
|
4237
|
+
currentBranch: versions.currentBranch,
|
|
4238
|
+
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4239
|
+
};
|
|
4240
|
+
const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task);
|
|
4241
|
+
const c1 = readTaskC1Posture(this.store, task);
|
|
4242
|
+
const now = this.now().getTime();
|
|
4243
|
+
const activeLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
4244
|
+
const staleLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
4245
|
+
const transactionCount = snapshot.observation.pendingProjectTransactions.length
|
|
4246
|
+
+ snapshot.observation.pendingMilestoneTransactions.length
|
|
4247
|
+
+ snapshot.observation.pendingTaskTransactions.length;
|
|
4248
|
+
const authorizationBlockers = [];
|
|
4249
|
+
if (assessment.eligible) {
|
|
4250
|
+
try {
|
|
4251
|
+
this.assertExecutionAuthorizationFresh(task, snapshot.identity.repositoryRoot);
|
|
4252
|
+
}
|
|
4253
|
+
catch (error) {
|
|
4254
|
+
authorizationBlockers.push(error instanceof Error ? error.message : String(error));
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
const baseBlockers = [
|
|
4258
|
+
...assessment.blockers,
|
|
4259
|
+
...authorizationBlockers,
|
|
4260
|
+
...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
|
|
4261
|
+
...Object.entries(sourceSurfaces)
|
|
4262
|
+
.filter(([, version]) => version !== sourceVersion)
|
|
4263
|
+
.map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
|
|
4264
|
+
...observationPreflightBlockers(snapshot.observation),
|
|
4265
|
+
...(transactionCount > 0 ? ['No pending Workflow transaction may exist.'] : []),
|
|
4266
|
+
...(snapshot.observation.corruptTaskTransactions.length > 0 ? ['No corrupt Task transaction may exist.'] : []),
|
|
4267
|
+
...(snapshot.observation.coreTaskOperations.length > 0 ? ['No Core Task operation may exist.'] : []),
|
|
4268
|
+
...(c1.state !== 'claimed' || assessment.compatibility?.claimant !== c1.claimant
|
|
4269
|
+
? ['The exact original Worker C1 claim must remain current.'] : []),
|
|
4270
|
+
...(activeLeases.length > 0 ? ['No live writer lease may exist during corrective carryover transport.'] : []),
|
|
4271
|
+
...(staleLeases.some((lease) => lease.entityId !== task.id
|
|
4272
|
+
|| c1.state !== 'claimed' || lease.owner !== c1.claimant
|
|
4273
|
+
|| createHash('sha256').update(lease.token).digest('hex') !== c1.writerLeaseTokenHash)
|
|
4274
|
+
? ['No unrelated or differently owned stale writer lease may exist.'] : []),
|
|
4275
|
+
...(staleLeases.length > 1 ? ['At most the exact corrective Task stale lease may exist.'] : []),
|
|
4276
|
+
];
|
|
4277
|
+
const repairableStale = baseBlockers.length === 0 && staleLeases.length === 1
|
|
4278
|
+
? staleLeases[0]
|
|
4279
|
+
: null;
|
|
4280
|
+
const blockers = [
|
|
4281
|
+
...baseBlockers,
|
|
4282
|
+
...(repairableStale
|
|
4283
|
+
? ['Repair the exact expired claimant lease with source locks repair, then run status, next, and this preflight again.']
|
|
4284
|
+
: []),
|
|
4285
|
+
];
|
|
4286
|
+
const compatibility = assessment.compatibility;
|
|
4287
|
+
return {
|
|
4288
|
+
readOnly: true,
|
|
4289
|
+
eligible: Boolean(assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0),
|
|
4290
|
+
action: assessment.eligible && compatibility && blockers.length === 0 && staleLeases.length === 0
|
|
4291
|
+
? 'update corrective-carryover-transport'
|
|
4292
|
+
: null,
|
|
4293
|
+
projectId: task.projectId,
|
|
4294
|
+
taskId: task.id,
|
|
4295
|
+
taskRevision: task.revision,
|
|
4296
|
+
stepId: compatibility?.stepId ?? null,
|
|
4297
|
+
sourceVersion,
|
|
4298
|
+
targetVersion,
|
|
4299
|
+
sourceHeadCommit: compatibility?.sourceHeadCommit ?? null,
|
|
4300
|
+
taskBranch: task.taskBranch,
|
|
4301
|
+
baseBranch: task.baseBranch,
|
|
4302
|
+
dirtyFiles: compatibility?.dirtyFiles ?? [],
|
|
4303
|
+
dirtyWorktreeHash: compatibility?.dirtyWorktreeHash ?? null,
|
|
4304
|
+
compatibilityHash: compatibility?.compatibilityHash ?? null,
|
|
4305
|
+
c1Posture: c1.state === 'claimed' ? {
|
|
4306
|
+
state: 'claimed',
|
|
4307
|
+
claimant: c1.claimant,
|
|
4308
|
+
claimEventId: compatibility?.c1ClaimEventId ?? null,
|
|
4309
|
+
claimEventHash: compatibility?.c1ClaimEventHash ?? null,
|
|
4310
|
+
} : null,
|
|
4311
|
+
staleLeaseRepair: repairableStale ? [{
|
|
4312
|
+
action: 'locks repair',
|
|
4313
|
+
entityId: task.id,
|
|
4314
|
+
owner: repairableStale.owner,
|
|
4315
|
+
expiresAt: repairableStale.expiresAt,
|
|
4316
|
+
}] : [],
|
|
4317
|
+
requiredTransport: [
|
|
4318
|
+
'Commit only package.json and package-lock.json on the active Milestone base.',
|
|
4319
|
+
'Commit only the same dependency files on the Task branch while preserving the bound dirty bytes.',
|
|
4320
|
+
'Install the exact target package, register dependency provenance, refresh context, replace the same Worker credential, then follow fresh next.',
|
|
4321
|
+
],
|
|
4322
|
+
blockers,
|
|
4323
|
+
};
|
|
3884
4324
|
}
|
|
3885
4325
|
historicalStepProvenanceRecoveryPreflight(repository, taskId) {
|
|
3886
4326
|
const snapshot = this.observationSnapshot(repository);
|
|
@@ -4351,7 +4791,9 @@ export class WorkflowService {
|
|
|
4351
4791
|
? Boolean(record.historicalStepProvenance)
|
|
4352
4792
|
: recoveryMode === 'plan-integrity'
|
|
4353
4793
|
? Boolean(record.planIntegrity)
|
|
4354
|
-
:
|
|
4794
|
+
: recoveryMode === false && record.rootCauseReplan
|
|
4795
|
+
? true
|
|
4796
|
+
: Boolean(record.activeDownstreamProof) === recoveryMode));
|
|
4355
4797
|
if (prior && current.systemCommits.includes(currentHead))
|
|
4356
4798
|
return current;
|
|
4357
4799
|
assertExpectedRevision(current, expectedRevision);
|
|
@@ -4367,13 +4809,26 @@ export class WorkflowService {
|
|
|
4367
4809
|
? this.planIntegrityDependencyRecoveryPreflight(repository, taskId)
|
|
4368
4810
|
: recoveryMode
|
|
4369
4811
|
? this.activeDownstreamProofDependencyRecoveryPreflight(repository, taskId)
|
|
4370
|
-
: this.
|
|
4812
|
+
: this.dependencyProvenanceRecoveryPreflight(repository, taskId);
|
|
4371
4813
|
if (!preflight.eligible || !preflight.commitSha || !preflight.parentCommitSha) {
|
|
4372
4814
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'The bounded dependency provenance recovery preflight failed.', {
|
|
4373
4815
|
taskId,
|
|
4374
4816
|
blockers: preflight.blockers,
|
|
4375
4817
|
});
|
|
4376
4818
|
}
|
|
4819
|
+
if (preflight.rootCauseReplan) {
|
|
4820
|
+
const posture = readTaskC1Posture(this.store, task);
|
|
4821
|
+
if (posture.state !== 'claimed'
|
|
4822
|
+
|| posture.claimant !== preflight.rootCauseReplan.claimant
|
|
4823
|
+
|| normalizedActor !== posture.claimant) {
|
|
4824
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective carryover dependency recovery requires the exact retained C1 claimant actor.', {
|
|
4825
|
+
taskId,
|
|
4826
|
+
actor: normalizedActor,
|
|
4827
|
+
requiredActor: preflight.rootCauseReplan.claimant,
|
|
4828
|
+
posture: posture.state,
|
|
4829
|
+
});
|
|
4830
|
+
}
|
|
4831
|
+
}
|
|
4377
4832
|
const now = this.now();
|
|
4378
4833
|
const recoveredRecords = preflight.dependencyCommits.map((dependencyCommit, index) => {
|
|
4379
4834
|
const isTerminalCommit = index === preflight.dependencyCommits.length - 1;
|
|
@@ -4399,6 +4854,9 @@ export class WorkflowService {
|
|
|
4399
4854
|
...(isTerminalCommit && preflight.planIntegrity
|
|
4400
4855
|
? { planIntegrity: preflight.planIntegrity }
|
|
4401
4856
|
: {}),
|
|
4857
|
+
...(isTerminalCommit && preflight.rootCauseReplan
|
|
4858
|
+
? { rootCauseReplan: preflight.rootCauseReplan }
|
|
4859
|
+
: {}),
|
|
4402
4860
|
};
|
|
4403
4861
|
});
|
|
4404
4862
|
const nextTask = {
|
|
@@ -5071,6 +5529,11 @@ export class WorkflowService {
|
|
|
5071
5529
|
throw error;
|
|
5072
5530
|
const blockingDecision = error.details.decision;
|
|
5073
5531
|
const blockingAudit = readPlanRiskAuditEvents(this.store, activeTask).at(-1) ?? null;
|
|
5532
|
+
const preExecutionReplan = readCurrentPreExecutionReplan(this.store, activeTask);
|
|
5533
|
+
const verifiedPreExecutionReplan = preExecutionReplan
|
|
5534
|
+
&& blockingAudit?.eventHash === preExecutionReplan.obstructionPlanRiskAuditEventHash
|
|
5535
|
+
? resolvePreExecutionReplanAudits(this.store, activeTask, preExecutionReplan)
|
|
5536
|
+
: null;
|
|
5074
5537
|
const existingRemediationEvent = blockingAudit && activeTask.milestoneId
|
|
5075
5538
|
? [...readMilestoneScopeChangeEvents(this.store, status.projectId, activeTask.milestoneId)]
|
|
5076
5539
|
.reverse()
|
|
@@ -5104,50 +5567,34 @@ export class WorkflowService {
|
|
|
5104
5567
|
const eligibleRemediationPredecessorTaskIds = [...remediationAncestorIds]
|
|
5105
5568
|
.filter((candidateTaskId) => tasksById.get(candidateTaskId)?.status === 'merged')
|
|
5106
5569
|
.sort();
|
|
5107
|
-
planRiskAuditRecovery =
|
|
5570
|
+
planRiskAuditRecovery = verifiedPreExecutionReplan && preExecutionReplan
|
|
5108
5571
|
? {
|
|
5109
5572
|
scope: 'task',
|
|
5110
5573
|
id: activeTask.id,
|
|
5111
5574
|
status: activeTask.status,
|
|
5112
|
-
action:
|
|
5113
|
-
? 'task plan-set'
|
|
5114
|
-
: existingRemediationTask
|
|
5115
|
-
? 'doctor'
|
|
5116
|
-
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
5117
|
-
? 'milestone remediation-materialize'
|
|
5118
|
-
: remediationDiscovery
|
|
5119
|
-
? 'discovery update'
|
|
5120
|
-
: 'discovery start',
|
|
5575
|
+
action: 'task plan-set',
|
|
5121
5576
|
blockedAction: 'task authorize',
|
|
5122
5577
|
revision: activeTask.revision,
|
|
5123
|
-
|
|
5124
|
-
state: 'recorded
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5578
|
+
preExecutionReplan: {
|
|
5579
|
+
state: 'recorded',
|
|
5580
|
+
eventId: preExecutionReplan.eventId,
|
|
5581
|
+
eventHash: preExecutionReplan.eventHash,
|
|
5582
|
+
sourcePlanHash: preExecutionReplan.sourcePlanHash,
|
|
5583
|
+
sourceHeadCommit: preExecutionReplan.sourceHeadCommit,
|
|
5584
|
+
obstructionPlanRiskAuditEventId: preExecutionReplan.obstructionPlanRiskAuditEventId,
|
|
5585
|
+
obstructionPlanRiskAuditEventHash: preExecutionReplan.obstructionPlanRiskAuditEventHash,
|
|
5586
|
+
preservesTaskScope: true,
|
|
5587
|
+
preservesCompletedStepAuthority: true,
|
|
5588
|
+
requiredAction: 'Replace the obstructed implementation Plan through ordinary task plan-set.',
|
|
5128
5589
|
},
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
humanApprovalRequired: false,
|
|
5138
|
-
blockedTaskId: activeTask.id,
|
|
5139
|
-
blockedTaskRevision: activeTask.revision,
|
|
5140
|
-
milestoneId: remediationMilestone.id,
|
|
5141
|
-
milestoneRevision: remediationMilestone.revision,
|
|
5142
|
-
planRiskAuditEventId: blockingAudit?.eventId ?? null,
|
|
5143
|
-
planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
|
|
5144
|
-
discoveryId: remediationDiscovery?.id ?? null,
|
|
5145
|
-
discoveryRevision: remediationDiscovery?.revision ?? null,
|
|
5146
|
-
remediationTaskId: existingRemediationTask?.id ?? null,
|
|
5147
|
-
remediationTaskStatus: existingRemediationTask?.status ?? null,
|
|
5148
|
-
requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
|
|
5149
|
-
eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
|
|
5150
|
-
command: existingRemediationTask?.status === 'merged'
|
|
5590
|
+
taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
|
|
5591
|
+
}
|
|
5592
|
+
: blockingDecision === 'stop-escalate' && auditedRemediationEligible
|
|
5593
|
+
? {
|
|
5594
|
+
scope: 'task',
|
|
5595
|
+
id: activeTask.id,
|
|
5596
|
+
status: activeTask.status,
|
|
5597
|
+
action: existingRemediationTask?.status === 'merged'
|
|
5151
5598
|
? 'task plan-set'
|
|
5152
5599
|
: existingRemediationTask
|
|
5153
5600
|
? 'doctor'
|
|
@@ -5156,46 +5603,84 @@ export class WorkflowService {
|
|
|
5156
5603
|
: remediationDiscovery
|
|
5157
5604
|
? 'discovery update'
|
|
5158
5605
|
: 'discovery start',
|
|
5159
|
-
boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
|
|
5160
|
-
preservesMilestoneSemantics: true,
|
|
5161
|
-
},
|
|
5162
|
-
taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
|
|
5163
|
-
}
|
|
5164
|
-
: blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
|
|
5165
|
-
? {
|
|
5166
|
-
scope: 'task',
|
|
5167
|
-
id: activeTask.id,
|
|
5168
|
-
status: activeTask.status,
|
|
5169
|
-
action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
|
|
5170
5606
|
blockedAction: 'task authorize',
|
|
5171
5607
|
revision: activeTask.revision,
|
|
5172
5608
|
planRiskAuditDecision: {
|
|
5173
5609
|
state: 'recorded-blocking',
|
|
5174
5610
|
decision: blockingDecision,
|
|
5175
5611
|
reason: error.message,
|
|
5176
|
-
requiredAction:
|
|
5177
|
-
? 'Replace the rejected Step Plan before requesting fresh authorization.'
|
|
5178
|
-
: 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
|
|
5612
|
+
requiredAction: 'Materialize one audited additive remediation Task, merge it, then replace the rejected Plan.',
|
|
5179
5613
|
},
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5614
|
+
auditedRemediation: {
|
|
5615
|
+
state: existingRemediationTask?.status === 'merged'
|
|
5616
|
+
? 'merged-replan-required'
|
|
5617
|
+
: existingRemediationTask
|
|
5618
|
+
? 'materialized-not-merged'
|
|
5619
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
5620
|
+
? 'ready-to-materialize'
|
|
5621
|
+
: 'discovery-required',
|
|
5622
|
+
humanApprovalRequired: false,
|
|
5623
|
+
blockedTaskId: activeTask.id,
|
|
5624
|
+
blockedTaskRevision: activeTask.revision,
|
|
5625
|
+
milestoneId: remediationMilestone.id,
|
|
5626
|
+
milestoneRevision: remediationMilestone.revision,
|
|
5627
|
+
planRiskAuditEventId: blockingAudit?.eventId ?? null,
|
|
5628
|
+
planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
|
|
5629
|
+
discoveryId: remediationDiscovery?.id ?? null,
|
|
5630
|
+
discoveryRevision: remediationDiscovery?.revision ?? null,
|
|
5631
|
+
remediationTaskId: existingRemediationTask?.id ?? null,
|
|
5632
|
+
remediationTaskStatus: existingRemediationTask?.status ?? null,
|
|
5633
|
+
requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
|
|
5634
|
+
eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
|
|
5635
|
+
command: existingRemediationTask?.status === 'merged'
|
|
5636
|
+
? 'task plan-set'
|
|
5637
|
+
: existingRemediationTask
|
|
5638
|
+
? 'doctor'
|
|
5639
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
5640
|
+
? 'milestone remediation-materialize'
|
|
5641
|
+
: remediationDiscovery
|
|
5642
|
+
? 'discovery update'
|
|
5643
|
+
: 'discovery start',
|
|
5644
|
+
boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
|
|
5645
|
+
preservesMilestoneSemantics: true,
|
|
5197
5646
|
},
|
|
5198
|
-
|
|
5647
|
+
taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
|
|
5648
|
+
}
|
|
5649
|
+
: blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
|
|
5650
|
+
? {
|
|
5651
|
+
scope: 'task',
|
|
5652
|
+
id: activeTask.id,
|
|
5653
|
+
status: activeTask.status,
|
|
5654
|
+
action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
|
|
5655
|
+
blockedAction: 'task authorize',
|
|
5656
|
+
revision: activeTask.revision,
|
|
5657
|
+
planRiskAuditDecision: {
|
|
5658
|
+
state: 'recorded-blocking',
|
|
5659
|
+
decision: blockingDecision,
|
|
5660
|
+
reason: error.message,
|
|
5661
|
+
requiredAction: blockingDecision === 'stop-escalate'
|
|
5662
|
+
? 'Replace the rejected Step Plan before requesting fresh authorization.'
|
|
5663
|
+
: 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
|
|
5664
|
+
},
|
|
5665
|
+
...(blockingDecision === 'stop-escalate'
|
|
5666
|
+
? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
|
|
5667
|
+
: {}),
|
|
5668
|
+
}
|
|
5669
|
+
: {
|
|
5670
|
+
scope: 'task',
|
|
5671
|
+
id: activeTask.id,
|
|
5672
|
+
status: activeTask.status,
|
|
5673
|
+
action: 'task plan-risk-audit',
|
|
5674
|
+
revision: activeTask.revision,
|
|
5675
|
+
planRiskAuditRecovery: {
|
|
5676
|
+
state: 'required',
|
|
5677
|
+
reason: error.message,
|
|
5678
|
+
currentBriefHash: activeTask.briefHash,
|
|
5679
|
+
currentPlanHash: activeTask.planHash,
|
|
5680
|
+
knowledgeMapRevision: activeTask.knowledgeMapRevision,
|
|
5681
|
+
knowledgeMapHash: activeTask.knowledgeMapHash,
|
|
5682
|
+
},
|
|
5683
|
+
};
|
|
5199
5684
|
}
|
|
5200
5685
|
}
|
|
5201
5686
|
const taskNext = activeTask.status === 'planning'
|
|
@@ -5718,6 +6203,9 @@ export class WorkflowService {
|
|
|
5718
6203
|
}
|
|
5719
6204
|
const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
5720
6205
|
const nextStepId = typeof next.stepId === 'string' ? next.stepId : null;
|
|
6206
|
+
const rootCauseStepId = nextStepId
|
|
6207
|
+
?? task.steps.find((candidate) => candidate.status === 'planned')?.id
|
|
6208
|
+
?? null;
|
|
5721
6209
|
const downstreamProofCarryover = next.action === 'task run'
|
|
5722
6210
|
&& nextStepId
|
|
5723
6211
|
&& changedFiles(repositoryRoot).length > 0
|
|
@@ -5728,6 +6216,24 @@ export class WorkflowService {
|
|
|
5728
6216
|
&& changedFiles(repositoryRoot).length > 0
|
|
5729
6217
|
? this.assessPostRebindCheckSupportDirtyCarryover(repositoryRoot, task, nextStepId)
|
|
5730
6218
|
: null;
|
|
6219
|
+
let rootCauseReplanDirtyCarryover = ['task run', 'task handoff-prepare', 'task claim'].includes(String(next.action))
|
|
6220
|
+
&& rootCauseStepId
|
|
6221
|
+
&& changedFiles(repositoryRoot).length > 0
|
|
6222
|
+
? this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, rootCauseStepId)
|
|
6223
|
+
: null;
|
|
6224
|
+
if (rootCauseReplanDirtyCarryover?.eligible) {
|
|
6225
|
+
try {
|
|
6226
|
+
this.assertExecutionAuthorizationFresh(task, repositoryRoot);
|
|
6227
|
+
}
|
|
6228
|
+
catch (error) {
|
|
6229
|
+
rootCauseReplanDirtyCarryover = {
|
|
6230
|
+
applicable: true,
|
|
6231
|
+
eligible: false,
|
|
6232
|
+
compatibility: null,
|
|
6233
|
+
blockers: [error instanceof Error ? error.message : String(error)],
|
|
6234
|
+
};
|
|
6235
|
+
}
|
|
6236
|
+
}
|
|
5731
6237
|
let mechanicalFeasibilityOverlay = {};
|
|
5732
6238
|
if (task.status === 'awaiting_execution_authorization' && task.planHash) {
|
|
5733
6239
|
try {
|
|
@@ -6236,9 +6742,42 @@ export class WorkflowService {
|
|
|
6236
6742
|
},
|
|
6237
6743
|
}
|
|
6238
6744
|
: {};
|
|
6745
|
+
const preExecutionReplanBlockers = task.status === 'ready'
|
|
6746
|
+
? [
|
|
6747
|
+
...(task.steps.some((step) => step.status === 'in_progress') ? ['No Step may be in progress.'] : []),
|
|
6748
|
+
...(task.steps.some((step) => step.status === 'failed') ? ['No failed Step may require corrective recovery.'] : []),
|
|
6749
|
+
...(handoffPosture.state !== 'none' ? ['No C1 claim or pending handoff may exist.'] : []),
|
|
6750
|
+
...(inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
|
|
6751
|
+
? ['No active or stale writer lease may exist.']
|
|
6752
|
+
: []),
|
|
6753
|
+
...(!isClean(repositoryRoot) ? ['Repository checkout must be clean.'] : []),
|
|
6754
|
+
...(!planRiskAudit || !['approved', 'approved-with-rationale'].includes(planRiskAudit.audit.decision)
|
|
6755
|
+
? ['A current positive Plan Risk Audit is required.']
|
|
6756
|
+
: []),
|
|
6757
|
+
]
|
|
6758
|
+
: [];
|
|
6759
|
+
const preExecutionReplanOption = task.status === 'ready' && task.planHash && planRiskAudit
|
|
6760
|
+
? {
|
|
6761
|
+
command: 'task preexecution-replan',
|
|
6762
|
+
state: preExecutionReplanBlockers.length === 0 ? 'eligible' : 'blocked',
|
|
6763
|
+
expectedRevision: task.revision,
|
|
6764
|
+
expectedBriefHash: task.briefHash,
|
|
6765
|
+
expectedPlanHash: task.planHash,
|
|
6766
|
+
expectedPlanRiskAuditEventHash: planRiskAudit.audit.eventHash,
|
|
6767
|
+
expectedHeadCommit: headCommit(repositoryRoot),
|
|
6768
|
+
auditDecision: 'stop-escalate',
|
|
6769
|
+
actorMustEqualAuditAuditor: true,
|
|
6770
|
+
humanApprovalRequired: false,
|
|
6771
|
+
boundedEffect: 'record-current-plan-obstruction-and-withdraw-execution-authorization',
|
|
6772
|
+
preservesTaskScope: true,
|
|
6773
|
+
preservesCompletedStepAuthority: true,
|
|
6774
|
+
blockers: preExecutionReplanBlockers,
|
|
6775
|
+
}
|
|
6776
|
+
: null;
|
|
6239
6777
|
const result = {
|
|
6240
6778
|
...next,
|
|
6241
6779
|
...handoffOverlay,
|
|
6780
|
+
...(preExecutionReplanOption ? { preExecutionReplanOption } : {}),
|
|
6242
6781
|
...(strictStepReviewStep
|
|
6243
6782
|
? {
|
|
6244
6783
|
action: 'task step-review',
|
|
@@ -6324,7 +6863,7 @@ export class WorkflowService {
|
|
|
6324
6863
|
downstreamProofCarryover: {
|
|
6325
6864
|
state: 'validated',
|
|
6326
6865
|
invalidationEventId: downstreamProofCarryover.invalidationEventId,
|
|
6327
|
-
stepId:
|
|
6866
|
+
stepId: rootCauseStepId,
|
|
6328
6867
|
dirtyFiles: downstreamProofCarryover.dirtyFiles,
|
|
6329
6868
|
dirtyWorktreeHash: downstreamProofCarryover.dirtyWorktreeHash,
|
|
6330
6869
|
meaning: 'The replacement Step may adopt exactly these preserved uncommitted bytes on task run.',
|
|
@@ -6364,6 +6903,52 @@ export class WorkflowService {
|
|
|
6364
6903
|
},
|
|
6365
6904
|
}
|
|
6366
6905
|
: {}),
|
|
6906
|
+
...(rootCauseReplanDirtyCarryover?.applicable
|
|
6907
|
+
? rootCauseReplanDirtyCarryover.eligible && rootCauseReplanDirtyCarryover.compatibility
|
|
6908
|
+
? {
|
|
6909
|
+
action: 'task run',
|
|
6910
|
+
rootCauseReplanDirtyCarryover: {
|
|
6911
|
+
state: 'validated',
|
|
6912
|
+
stepId: nextStepId,
|
|
6913
|
+
compatibilityHash: rootCauseReplanDirtyCarryover.compatibility.compatibilityHash,
|
|
6914
|
+
dirtyFiles: rootCauseReplanDirtyCarryover.compatibility.dirtyFiles,
|
|
6915
|
+
dirtyWorktreeHash: rootCauseReplanDirtyCarryover.compatibility.dirtyWorktreeHash,
|
|
6916
|
+
meaning: 'The replacement Step may adopt the exact retained root-cause failure bytes.',
|
|
6917
|
+
},
|
|
6918
|
+
}
|
|
6919
|
+
: rootCauseReplanDirtyCarryover.blockers.length === 1
|
|
6920
|
+
&& rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1')
|
|
6921
|
+
? handoffPosture.state === 'none'
|
|
6922
|
+
? {
|
|
6923
|
+
action: 'task handoff-prepare',
|
|
6924
|
+
blockedAction: 'task run',
|
|
6925
|
+
rootCauseReplanDirtyCarryover: {
|
|
6926
|
+
state: 'awaiting-c1',
|
|
6927
|
+
stepId: rootCauseStepId,
|
|
6928
|
+
requiredActor: defaultTaskWorkerActor(task.id),
|
|
6929
|
+
},
|
|
6930
|
+
}
|
|
6931
|
+
: handoffPosture.state === 'pending'
|
|
6932
|
+
? {}
|
|
6933
|
+
: {
|
|
6934
|
+
action: 'doctor',
|
|
6935
|
+
blockedAction: 'task run',
|
|
6936
|
+
rootCauseReplanDirtyCarryover: {
|
|
6937
|
+
state: 'blocked',
|
|
6938
|
+
stepId: rootCauseStepId,
|
|
6939
|
+
blockers: rootCauseReplanDirtyCarryover.blockers,
|
|
6940
|
+
},
|
|
6941
|
+
}
|
|
6942
|
+
: {
|
|
6943
|
+
action: 'doctor',
|
|
6944
|
+
blockedAction: 'task run',
|
|
6945
|
+
rootCauseReplanDirtyCarryover: {
|
|
6946
|
+
state: 'blocked',
|
|
6947
|
+
stepId: rootCauseStepId,
|
|
6948
|
+
blockers: rootCauseReplanDirtyCarryover.blockers,
|
|
6949
|
+
},
|
|
6950
|
+
}
|
|
6951
|
+
: {}),
|
|
6367
6952
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
6368
6953
|
...(correctiveDerivedAuthority && correctiveDerivedActor
|
|
6369
6954
|
? {
|
|
@@ -6380,7 +6965,10 @@ export class WorkflowService {
|
|
|
6380
6965
|
}
|
|
6381
6966
|
: {}),
|
|
6382
6967
|
};
|
|
6383
|
-
|
|
6968
|
+
const rootCauseAllowsPendingClaim = !rootCauseReplanDirtyCarryover?.applicable
|
|
6969
|
+
|| (rootCauseReplanDirtyCarryover.blockers.length === 1
|
|
6970
|
+
&& rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1'));
|
|
6971
|
+
if (handoffPosture.state === 'pending' && rootCauseAllowsPendingClaim) {
|
|
6384
6972
|
return this.withTaskNavigationContracts(task, {
|
|
6385
6973
|
...result,
|
|
6386
6974
|
action: 'task claim',
|
|
@@ -6414,6 +7002,9 @@ export class WorkflowService {
|
|
|
6414
7002
|
};
|
|
6415
7003
|
}
|
|
6416
7004
|
assertTaskContextChangeAllowed(task) {
|
|
7005
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
7006
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has a pending pre-execution replan; replace the implementation Plan before changing context.`, { taskId: task.id, requiredAction: 'task plan-set' });
|
|
7007
|
+
}
|
|
6417
7008
|
if (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed')) {
|
|
6418
7009
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} requires a replacement Step Plan; Project Knowledge cannot rebind or refresh the stale Plan.`, {
|
|
6419
7010
|
taskId: task.id,
|
|
@@ -6899,7 +7490,7 @@ export class WorkflowService {
|
|
|
6899
7490
|
blockers,
|
|
6900
7491
|
};
|
|
6901
7492
|
}
|
|
6902
|
-
inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null) {
|
|
7493
|
+
inspectDependencyProvenanceCandidate(snapshot, task, activeDownstreamProof = null, downstreamProofReplan = null, historicalStepProvenance = null, planIntegrity = null, rootCauseReplan = null) {
|
|
6903
7494
|
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
6904
7495
|
const now = this.now().getTime();
|
|
6905
7496
|
return inspectDependencyProvenanceRecovery(snapshot.identity.repositoryRoot, task, {
|
|
@@ -6919,7 +7510,43 @@ export class WorkflowService {
|
|
|
6919
7510
|
currentBranch: versions.currentBranch,
|
|
6920
7511
|
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
6921
7512
|
},
|
|
6922
|
-
}, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity);
|
|
7513
|
+
}, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity, rootCauseReplan);
|
|
7514
|
+
}
|
|
7515
|
+
assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId) {
|
|
7516
|
+
const recorded = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan
|
|
7517
|
+
&& task.systemCommits.includes(record.commitSha))?.rootCauseReplan ?? null;
|
|
7518
|
+
if (recorded) {
|
|
7519
|
+
if (stepId && stepId !== recorded.stepId) {
|
|
7520
|
+
return {
|
|
7521
|
+
applicable: true,
|
|
7522
|
+
eligible: false,
|
|
7523
|
+
compatibility: null,
|
|
7524
|
+
blockers: [`Recorded corrective carryover is bound to ${recorded.stepId}, not requested ${stepId}.`],
|
|
7525
|
+
};
|
|
7526
|
+
}
|
|
7527
|
+
return assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, recorded.stepId, {
|
|
7528
|
+
binding: recorded,
|
|
7529
|
+
});
|
|
7530
|
+
}
|
|
7531
|
+
const candidates = task.steps
|
|
7532
|
+
.filter((step) => step.status === 'planned' && (!stepId || step.id === stepId))
|
|
7533
|
+
.flatMap((step) => [...new Set(readRemediationEvents(this.store, task)
|
|
7534
|
+
.filter((event) => event.stepId === step.id && event.failureKind === 'checks-failed')
|
|
7535
|
+
.map((event) => event.failureEvidence.completionCommit))]
|
|
7536
|
+
.map((historyHead) => assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, step.id, { historyHead })))
|
|
7537
|
+
.filter((assessment) => assessment.applicable);
|
|
7538
|
+
const eligible = candidates.filter((assessment) => assessment.eligible && assessment.compatibility);
|
|
7539
|
+
if (eligible.length === 1)
|
|
7540
|
+
return eligible[0];
|
|
7541
|
+
return {
|
|
7542
|
+
applicable: candidates.length > 0,
|
|
7543
|
+
eligible: false,
|
|
7544
|
+
compatibility: null,
|
|
7545
|
+
blockers: [...new Set([
|
|
7546
|
+
...candidates.flatMap((assessment) => assessment.blockers),
|
|
7547
|
+
...(eligible.length > 1 ? ['Corrective carryover history resolved to more than one eligible boundary.'] : []),
|
|
7548
|
+
])],
|
|
7549
|
+
};
|
|
6923
7550
|
}
|
|
6924
7551
|
inspectHistoricalStepProvenanceCandidate(snapshot, task, options = {}) {
|
|
6925
7552
|
const now = this.now().getTime();
|
|
@@ -7623,6 +8250,83 @@ function taskHasStarted(task) {
|
|
|
7623
8250
|
'merged',
|
|
7624
8251
|
].includes(task.status);
|
|
7625
8252
|
}
|
|
8253
|
+
function resolvePreExecutionReplanAudits(store, task, marker) {
|
|
8254
|
+
const audits = readPlanRiskAuditEvents(store, task);
|
|
8255
|
+
const sourceAudit = audits.find((audit) => audit.eventId === marker.sourcePlanRiskAuditEventId
|
|
8256
|
+
&& audit.eventHash === marker.sourcePlanRiskAuditEventHash) ?? null;
|
|
8257
|
+
const obstructionAudit = audits.find((audit) => audit.eventId === marker.obstructionPlanRiskAuditEventId
|
|
8258
|
+
&& audit.eventHash === marker.obstructionPlanRiskAuditEventHash) ?? null;
|
|
8259
|
+
const sourceAuthorization = task.authorizations.find((authorization) => authorization.kind === 'execution'
|
|
8260
|
+
&& authorization.decision === 'superseded'
|
|
8261
|
+
&& hashExecutionAuthorization({ ...authorization, decision: 'approved' }) === marker.sourceExecutionAuthorizationHash);
|
|
8262
|
+
if (!sourceAudit || !obstructionAudit || audits.at(-1)?.eventHash !== obstructionAudit.eventHash
|
|
8263
|
+
|| !['approved', 'approved-with-rationale'].includes(sourceAudit.decision)
|
|
8264
|
+
|| obstructionAudit.decision !== 'stop-escalate'
|
|
8265
|
+
|| sourceAudit.planHash !== task.planHash || obstructionAudit.planHash !== task.planHash
|
|
8266
|
+
|| !sourceAuthorization) {
|
|
8267
|
+
throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replan provenance chain is incomplete or inconsistent.', {
|
|
8268
|
+
taskId: task.id,
|
|
8269
|
+
eventId: marker.eventId,
|
|
8270
|
+
});
|
|
8271
|
+
}
|
|
8272
|
+
return { sourceAudit, obstructionAudit };
|
|
8273
|
+
}
|
|
8274
|
+
function assertPlanRiskSafetyPreserved(source, candidate, label) {
|
|
8275
|
+
const candidateByStep = new Map(candidate.stepClassifications.map((classification) => [classification.stepId, classification]));
|
|
8276
|
+
const missing = [];
|
|
8277
|
+
for (const sourceClassification of source.stepClassifications) {
|
|
8278
|
+
const candidateClassification = candidateByStep.get(sourceClassification.stepId);
|
|
8279
|
+
if (!candidateClassification) {
|
|
8280
|
+
missing.push({ stepId: sourceClassification.stepId, field: 'classification', values: [] });
|
|
8281
|
+
continue;
|
|
8282
|
+
}
|
|
8283
|
+
const preserveStrings = (field, sourceValues, candidateValues) => {
|
|
8284
|
+
const absent = sourceValues.filter((value) => !candidateValues.includes(value));
|
|
8285
|
+
if (absent.length > 0)
|
|
8286
|
+
missing.push({ stepId: sourceClassification.stepId, field, values: absent });
|
|
8287
|
+
};
|
|
8288
|
+
preserveStrings('categories', sourceClassification.categories, candidateClassification.categories);
|
|
8289
|
+
preserveStrings('failureModes', sourceClassification.failureModes, candidateClassification.failureModes);
|
|
8290
|
+
preserveStrings('requiredEvidence', sourceClassification.requiredEvidence, candidateClassification.requiredEvidence);
|
|
8291
|
+
if (sourceClassification.reviewRequired && !candidateClassification.reviewRequired) {
|
|
8292
|
+
missing.push({ stepId: sourceClassification.stepId, field: 'reviewRequired', values: [true] });
|
|
8293
|
+
}
|
|
8294
|
+
const proofBinding = (proof) => canonicalJsonStringify({ failureMode: proof.failureMode, evidence: proof.evidence });
|
|
8295
|
+
const candidateProofs = new Set((candidateClassification.proofObligations ?? []).map(proofBinding));
|
|
8296
|
+
const absentProofs = (sourceClassification.proofObligations ?? [])
|
|
8297
|
+
.filter((proof) => !candidateProofs.has(proofBinding(proof)));
|
|
8298
|
+
if (absentProofs.length > 0) {
|
|
8299
|
+
missing.push({ stepId: sourceClassification.stepId, field: 'proofObligations', values: absentProofs });
|
|
8300
|
+
}
|
|
8301
|
+
}
|
|
8302
|
+
if (missing.length > 0) {
|
|
8303
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `${label} cannot suppress existing guarded risk or proof authority.`, {
|
|
8304
|
+
missing,
|
|
8305
|
+
});
|
|
8306
|
+
}
|
|
8307
|
+
const sourceProofText = new Map(source.stepClassifications.flatMap((classification) => (classification.proofObligations ?? []).map((proof) => [
|
|
8308
|
+
canonicalJsonStringify({ stepId: classification.stepId, failureMode: proof.failureMode, evidence: proof.evidence }),
|
|
8309
|
+
canonicalJsonStringify({ counterexample: proof.counterexample, expectedResult: proof.expectedResult }),
|
|
8310
|
+
])));
|
|
8311
|
+
const revisedProofText = candidate.stepClassifications.some((classification) => (classification.proofObligations ?? []).some((proof) => {
|
|
8312
|
+
const binding = canonicalJsonStringify({
|
|
8313
|
+
stepId: classification.stepId,
|
|
8314
|
+
failureMode: proof.failureMode,
|
|
8315
|
+
evidence: proof.evidence,
|
|
8316
|
+
});
|
|
8317
|
+
const previous = sourceProofText.get(binding);
|
|
8318
|
+
return previous !== undefined
|
|
8319
|
+
&& previous !== canonicalJsonStringify({ counterexample: proof.counterexample, expectedResult: proof.expectedResult });
|
|
8320
|
+
}));
|
|
8321
|
+
if (revisedProofText && (candidate.decision !== 'approved-with-rationale'
|
|
8322
|
+
|| candidate.summary.trim() === source.summary.trim())) {
|
|
8323
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `${label} may revise proof prose only with approved-with-rationale and a new audit summary.`, {
|
|
8324
|
+
sourceSummary: source.summary,
|
|
8325
|
+
candidateSummary: candidate.summary,
|
|
8326
|
+
candidateDecision: candidate.decision,
|
|
8327
|
+
});
|
|
8328
|
+
}
|
|
8329
|
+
}
|
|
7626
8330
|
function taskCanRebindKnowledge(task) {
|
|
7627
8331
|
return [
|
|
7628
8332
|
'awaiting_execution_authorization',
|