codex-workflow-v2 2.0.0-beta.13.2 → 2.0.0-beta.13.5
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 +1 -1
- package/dist/reviewer-runtime-build.json +18 -10
- 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 -2
- package/dist/src/alpha6/remediation.js +155 -20
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/cli-actions.d.ts +1 -1
- package/dist/src/cli-actions.js +1 -0
- package/dist/src/cli-actions.js.map +1 -1
- package/dist/src/cli.js +44 -0
- package/dist/src/cli.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/state/corrective-yield-executor.js +10 -1
- package/dist/src/state/corrective-yield-executor.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 +19 -1
- package/dist/src/workflow.js +518 -84
- 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 +3 -3
- package/docs/release.md +59 -2
- package/docs/updating-existing-project.md +10 -2
- package/package.json +1 -1
- package/plugins/codex-workflow-gateway/references/chat-dispatch.md +128 -0
- package/plugins/codex-workflow-gateway/references/protocol.md +4 -0
- package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +185 -0
- package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +43 -0
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +50 -3
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +63 -6
- package/schemas/preexecution-replan-event.schema.json +45 -0
package/dist/src/workflow.js
CHANGED
|
@@ -8,6 +8,7 @@ import { inspectHistoricalStepProvenanceRecovery, omittedStepEvidenceCommits, }
|
|
|
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,9 @@ 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 { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, deriveCurrentCauseDecisionForTask, prepareRemediationEvent, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
|
|
22
24
|
import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
|
|
23
25
|
import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
|
|
24
26
|
import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
|
|
@@ -28,7 +30,7 @@ import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, en
|
|
|
28
30
|
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
29
31
|
import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, readValidatedStopOverrideContextRebindIfApplicable, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
|
|
30
32
|
import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
|
|
31
|
-
import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
|
|
33
|
+
import { canonicalJsonStringify, prepareSidecarAppend, sha256Hex } from './alpha6/store-sidecars.js';
|
|
32
34
|
import { ensureDirectory } from './fs-utils.js';
|
|
33
35
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
34
36
|
import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
|
|
@@ -47,9 +49,11 @@ const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
|
|
|
47
49
|
export class WorkflowService {
|
|
48
50
|
store;
|
|
49
51
|
now;
|
|
50
|
-
|
|
52
|
+
transactionHooks;
|
|
53
|
+
constructor(store = new FileStateStore(), now = () => new Date(), transactionHooks = {}) {
|
|
51
54
|
this.store = store;
|
|
52
55
|
this.now = now;
|
|
56
|
+
this.transactionHooks = transactionHooks;
|
|
53
57
|
}
|
|
54
58
|
#mutationContext(repository) {
|
|
55
59
|
const identity = resolveRepositoryIdentity(repository);
|
|
@@ -1601,6 +1605,10 @@ export class WorkflowService {
|
|
|
1601
1605
|
this.validateKnowledgeTargets(knowledgeMap, plan);
|
|
1602
1606
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1603
1607
|
assertExpectedRevision(task, expectedRevision);
|
|
1608
|
+
const preExecutionReplan = readCurrentPreExecutionReplan(this.store, task);
|
|
1609
|
+
const preExecutionReplanAudits = preExecutionReplan
|
|
1610
|
+
? resolvePreExecutionReplanAudits(this.store, task, preExecutionReplan)
|
|
1611
|
+
: null;
|
|
1604
1612
|
const correctiveDecisionEvents = task.planHash ? readCorrectiveDecisionEvents(this.store, task) : [];
|
|
1605
1613
|
const currentPlanCorrectiveDecisions = task.planHash
|
|
1606
1614
|
? correctiveDecisionEvents.filter((event) => event.planHash === task.planHash)
|
|
@@ -1747,6 +1755,9 @@ export class WorkflowService {
|
|
|
1747
1755
|
}
|
|
1748
1756
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
1749
1757
|
const preparedPlan = prepareCorrectiveReplanCandidate(task, plan);
|
|
1758
|
+
const changesPlanSemantics = !task.planHash
|
|
1759
|
+
|| normalizePlanKnowledgeBinding(preparedPlan.planBytes, 'candidate').semanticHash
|
|
1760
|
+
!== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(root, task.planHash, task.planHash), 'current').semanticHash;
|
|
1750
1761
|
const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
1751
1762
|
? { ...authorization, decision: 'superseded' }
|
|
1752
1763
|
: authorization);
|
|
@@ -1764,6 +1775,83 @@ export class WorkflowService {
|
|
|
1764
1775
|
...(task.invalidatedStepCommits ?? []),
|
|
1765
1776
|
...omittedStepEvidenceCommits(task, plannedSteps),
|
|
1766
1777
|
])];
|
|
1778
|
+
if (preExecutionReplan && preExecutionReplanAudits) {
|
|
1779
|
+
const replanC1Posture = readTaskC1Posture(this.store, task);
|
|
1780
|
+
const replanWriterLease = inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root);
|
|
1781
|
+
if (replanC1Posture.state !== 'none' || replanWriterLease) {
|
|
1782
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replacement Plan requires no C1 posture or writer lease.', {
|
|
1783
|
+
taskId: task.id,
|
|
1784
|
+
c1Posture: replanC1Posture.state,
|
|
1785
|
+
writerLeasePresent: replanWriterLease !== null,
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
if (!planRiskAudit) {
|
|
1789
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires an atomic replacement Plan Risk Audit.');
|
|
1790
|
+
}
|
|
1791
|
+
if (task.planObjective !== plan.objective
|
|
1792
|
+
|| canonicalJsonStringify(task.requirements) !== canonicalJsonStringify(plan.requirements)
|
|
1793
|
+
|| canonicalJsonStringify(task.acceptance) !== canonicalJsonStringify(plan.acceptance)) {
|
|
1794
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot change Task objective, requirements, or acceptance.', {
|
|
1795
|
+
taskId: task.id,
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
if (!changesPlanSemantics) {
|
|
1799
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a semantic Plan change.', { taskId: task.id });
|
|
1800
|
+
}
|
|
1801
|
+
const candidateSteps = new Map(plan.steps.map((step) => [step.id, step]));
|
|
1802
|
+
const missingStepIds = task.steps.filter((step) => !candidateSteps.has(step.id)).map((step) => step.id);
|
|
1803
|
+
if (missingStepIds.length > 0) {
|
|
1804
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must retain every existing Step identifier.', {
|
|
1805
|
+
taskId: task.id,
|
|
1806
|
+
missingStepIds,
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
const completedStepIds = task.steps.filter((step) => step.status === 'completed').map((step) => step.id);
|
|
1810
|
+
const lostCompletedStepIds = completedStepIds.filter((stepId) => !retainedCompletedSteps.has(stepId));
|
|
1811
|
+
if (lostCompletedStepIds.length > 0) {
|
|
1812
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must preserve the exact completed Step subgraph and evidence.', {
|
|
1813
|
+
taskId: task.id,
|
|
1814
|
+
lostCompletedStepIds,
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
const weakenedForbiddenScope = task.steps.flatMap((step) => {
|
|
1818
|
+
const candidate = candidateSteps.get(step.id);
|
|
1819
|
+
const missing = step.forbiddenScope.filter((scope) => !candidate.forbiddenScope.includes(scope));
|
|
1820
|
+
return missing.length > 0 ? [{ stepId: step.id, missing }] : [];
|
|
1821
|
+
});
|
|
1822
|
+
if (weakenedForbiddenScope.length > 0) {
|
|
1823
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot weaken existing forbiddenScope boundaries.', {
|
|
1824
|
+
taskId: task.id,
|
|
1825
|
+
weakenedForbiddenScope,
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
const missingRisks = task.risks.filter((risk) => !plan.risks.includes(risk));
|
|
1829
|
+
if (missingRisks.length > 0) {
|
|
1830
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot suppress existing Plan risks.', {
|
|
1831
|
+
taskId: task.id,
|
|
1832
|
+
missingRisks,
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
assertPlanRiskSafetyPreserved(preExecutionReplanAudits.obstructionAudit, planRiskAudit, 'Replacement Plan Risk Audit');
|
|
1836
|
+
const candidateEdgeKeys = new Set((plan.productionEdges ?? []).map((edge) => canonicalJsonStringify(edge)));
|
|
1837
|
+
const missingProductionEdges = (task.productionEdges ?? [])
|
|
1838
|
+
.filter((edge) => !candidateEdgeKeys.has(canonicalJsonStringify(edge)));
|
|
1839
|
+
if ((task.requiresProductionEdges === true && plan.requiresProductionEdges !== true)
|
|
1840
|
+
|| missingProductionEdges.length > 0) {
|
|
1841
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan cannot weaken production-edge authority.', {
|
|
1842
|
+
taskId: task.id,
|
|
1843
|
+
missingProductionEdges,
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
if (!isClean(identity.repositoryRoot) || headCommit(identity.repositoryRoot) !== preExecutionReplan.sourceHeadCommit) {
|
|
1847
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Pre-execution replacement Plan requires the exact clean recorded HEAD.', {
|
|
1848
|
+
taskId: task.id,
|
|
1849
|
+
expectedHead: preExecutionReplan.sourceHeadCommit,
|
|
1850
|
+
actualHead: headCommit(identity.repositoryRoot),
|
|
1851
|
+
changedFiles: changedFiles(identity.repositoryRoot),
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1767
1855
|
if (task.status === 'needs_fix' &&
|
|
1768
1856
|
task.review.status === 'failed' &&
|
|
1769
1857
|
plannedSteps.every((step) => step.status === 'completed')) {
|
|
@@ -1787,7 +1875,7 @@ export class WorkflowService {
|
|
|
1787
1875
|
: []),
|
|
1788
1876
|
...(rawWriterLease ? ['task.rolling-replan.no-writer-lease'] : []),
|
|
1789
1877
|
...(c1Posture.state !== 'none' ? ['task.rolling-replan.no-c1-claim'] : []),
|
|
1790
|
-
...(
|
|
1878
|
+
...(!changesPlanSemantics
|
|
1791
1879
|
? ['task.rolling-replan.semantic-plan-change']
|
|
1792
1880
|
: []),
|
|
1793
1881
|
...(!preparedPlan.stepIds.includes(correctiveReplanPosture.stepId)
|
|
@@ -1860,6 +1948,61 @@ export class WorkflowService {
|
|
|
1860
1948
|
};
|
|
1861
1949
|
if (planRiskAudit)
|
|
1862
1950
|
validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
|
|
1951
|
+
if (preExecutionReplan && preExecutionReplanAudits && planRiskAudit) {
|
|
1952
|
+
const now = this.now();
|
|
1953
|
+
const resultTask = {
|
|
1954
|
+
...candidate,
|
|
1955
|
+
revision: task.revision + 1,
|
|
1956
|
+
updatedAt: now.toISOString(),
|
|
1957
|
+
};
|
|
1958
|
+
const preparedAuditBase = buildCandidatePlanRiskAuditEvent(this.store, resultTask, planRiskAudit, now.toISOString(), now.getTime());
|
|
1959
|
+
const preparedAudit = prepareSidecarAppend(readPlanRiskAuditEvents(this.store, task), preparedAuditBase.event);
|
|
1960
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
1961
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
1962
|
+
const versionFile = path.join(root, '.versions', `plan.md.${preparedPlan.planHash}`);
|
|
1963
|
+
if (existsSync(versionFile) && sha256Hex(readFileSync(versionFile)) !== preparedPlan.planHash) {
|
|
1964
|
+
throw new WorkflowError('STATE_CORRUPT', 'Immutable replacement Plan version is corrupt.', {
|
|
1965
|
+
taskId: task.id,
|
|
1966
|
+
planHash: preparedPlan.planHash,
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
applyProjectTransaction({
|
|
1970
|
+
projectRoot,
|
|
1971
|
+
projectId: task.projectId,
|
|
1972
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
1973
|
+
kind: 'preexecution-replacement-plan-set',
|
|
1974
|
+
now,
|
|
1975
|
+
targets: [
|
|
1976
|
+
{ path: relative(path.join(root, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
|
|
1977
|
+
{ path: relative(path.join(root, 'plan.md')), content: preparedPlan.planBytes },
|
|
1978
|
+
{ path: relative(versionFile), content: preparedPlan.planBytes },
|
|
1979
|
+
{ path: relative(path.join(root, 'plan-risk-audits.jsonl')), content: preparedAudit.content },
|
|
1980
|
+
],
|
|
1981
|
+
...(this.transactionHooks.afterPreExecutionReplanTargetWrite
|
|
1982
|
+
? { afterTargetWrite: this.transactionHooks.afterPreExecutionReplanTargetWrite }
|
|
1983
|
+
: {}),
|
|
1984
|
+
validate: () => {
|
|
1985
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
1986
|
+
const currentAudit = readCurrentPlanRiskAudit(this.store, saved, { requireCurrentTaskRevision: true });
|
|
1987
|
+
if (saved.revision !== resultTask.revision
|
|
1988
|
+
|| saved.status !== 'awaiting_execution_authorization'
|
|
1989
|
+
|| saved.planHash !== preparedPlan.planHash
|
|
1990
|
+
|| this.store.hashArtifact(root, 'plan.md') !== preparedPlan.planHash
|
|
1991
|
+
|| sha256Hex(readFileSync(versionFile)) !== preparedPlan.planHash
|
|
1992
|
+
|| currentAudit?.audit.eventHash !== preparedAudit.event.eventHash
|
|
1993
|
+
|| hashCompletedSteps(saved) !== preExecutionReplan.completedStepsHash
|
|
1994
|
+
|| readCurrentPreExecutionReplan(this.store, saved) !== null
|
|
1995
|
+
|| saved.authorizations.some((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved')) {
|
|
1996
|
+
throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replacement Plan transaction readback failed.', {
|
|
1997
|
+
taskId: task.id,
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
if (saved.baseCommit)
|
|
2001
|
+
validateTaskHistory(identity.repositoryRoot, saved);
|
|
2002
|
+
},
|
|
2003
|
+
});
|
|
2004
|
+
return this.store.readTask(task.projectId, task.id);
|
|
2005
|
+
}
|
|
1863
2006
|
const stagedPlan = this.store.stageArtifact(root, 'plan.md', preparedPlan.planBytes);
|
|
1864
2007
|
if (stagedPlan.hash !== preparedPlan.planHash) {
|
|
1865
2008
|
throw new WorkflowError('STATE_CORRUPT', 'Prepared candidate Plan hash changed during staging.', {
|
|
@@ -2068,6 +2211,9 @@ export class WorkflowService {
|
|
|
2068
2211
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
2069
2212
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2070
2213
|
assertExpectedRevision(task, expectedRevision);
|
|
2214
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2215
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a semantic replacement Plan before execution authorization.', { taskId, requiredAction: 'task plan-set' });
|
|
2216
|
+
}
|
|
2071
2217
|
if (task.status !== 'awaiting_execution_authorization' || !task.planHash) {
|
|
2072
2218
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not awaiting execution authorization.`);
|
|
2073
2219
|
}
|
|
@@ -2125,9 +2271,143 @@ export class WorkflowService {
|
|
|
2125
2271
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
2126
2272
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
2127
2273
|
assertExpectedRevision(task, expectedRevision);
|
|
2274
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2275
|
+
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' });
|
|
2276
|
+
}
|
|
2128
2277
|
this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
|
|
2129
2278
|
return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
|
|
2130
2279
|
}
|
|
2280
|
+
recordPreExecutionReplan(repository, taskId, expectedRevision, actor, reason, binding, input) {
|
|
2281
|
+
const context = this.#mutationContext(repository);
|
|
2282
|
+
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
2283
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2284
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2285
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2286
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task already has a pending pre-execution replan.', {
|
|
2287
|
+
taskId,
|
|
2288
|
+
requiredAction: 'task plan-set',
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
if (task.status !== 'ready' || task.steps.some((step) => ['in_progress', 'failed'].includes(step.status))) {
|
|
2292
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a ready Task with no Step in progress or failed.', {
|
|
2293
|
+
taskId,
|
|
2294
|
+
status: task.status,
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
if (!isClean(context.identity.repositoryRoot)) {
|
|
2298
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Pre-execution replan requires a clean checkout.', {
|
|
2299
|
+
changedFiles: changedFiles(context.identity.repositoryRoot),
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
const c1Posture = readTaskC1Posture(this.store, task);
|
|
2303
|
+
const writerLease = context.locks.inspect(task.id);
|
|
2304
|
+
if (c1Posture.state !== 'none' || writerLease) {
|
|
2305
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires a clean authority boundary without C1 or writer lease.', {
|
|
2306
|
+
taskId,
|
|
2307
|
+
c1Posture: c1Posture.state,
|
|
2308
|
+
writerLeasePresent: writerLease !== null,
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
2312
|
+
if (task.baseCommit) {
|
|
2313
|
+
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
2314
|
+
}
|
|
2315
|
+
else if (task.workspaceOwner !== null || task.taskBranch !== null
|
|
2316
|
+
|| task.steps.some((step) => step.status === 'completed')) {
|
|
2317
|
+
throw new WorkflowError('STATE_CORRUPT', 'Unstarted pre-execution replan Task has inconsistent execution history.', {
|
|
2318
|
+
taskId,
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
const authorization = this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
2322
|
+
const sourceAudit = readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: false });
|
|
2323
|
+
if (!sourceAudit || !['approved', 'approved-with-rationale'].includes(sourceAudit.audit.decision)) {
|
|
2324
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan requires the current positive Plan Risk Audit.');
|
|
2325
|
+
}
|
|
2326
|
+
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
2327
|
+
const actualBinding = {
|
|
2328
|
+
expectedBriefHash: task.briefHash,
|
|
2329
|
+
expectedPlanHash: task.planHash,
|
|
2330
|
+
expectedPlanRiskAuditEventHash: sourceAudit.audit.eventHash,
|
|
2331
|
+
expectedHeadCommit: currentHead,
|
|
2332
|
+
};
|
|
2333
|
+
if (canonicalJsonStringify(binding) !== canonicalJsonStringify(actualBinding)) {
|
|
2334
|
+
throw new WorkflowError('STATE_CONFLICT', 'Pre-execution replan source binding is stale.', {
|
|
2335
|
+
expected: binding,
|
|
2336
|
+
actual: actualBinding,
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
if (input.decision !== 'stop-escalate') {
|
|
2340
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Pre-execution replan requires a stop-escalate Plan Risk Audit.');
|
|
2341
|
+
}
|
|
2342
|
+
if (actor.trim() !== input.auditor.trim() || input.planner.trim() !== sourceAudit.audit.planner) {
|
|
2343
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Pre-execution replan must be recorded by the independent auditor against the original planner.', {
|
|
2344
|
+
actor: actor.trim(),
|
|
2345
|
+
auditor: input.auditor.trim(),
|
|
2346
|
+
expectedPlanner: sourceAudit.audit.planner,
|
|
2347
|
+
actualPlanner: input.planner.trim(),
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
assertPlanRiskSafetyPreserved(sourceAudit.audit, input, 'Pre-execution replan audit');
|
|
2351
|
+
const now = this.now();
|
|
2352
|
+
const preparedAuditBase = buildPlanRiskAuditEvent(this.store, task, input, now.toISOString(), now.getTime());
|
|
2353
|
+
const preparedAudit = prepareSidecarAppend(readPlanRiskAuditEvents(this.store, task), preparedAuditBase.event);
|
|
2354
|
+
const preparedMarker = preparePreExecutionReplanEvent(this.store, task, {
|
|
2355
|
+
sourceAudit: sourceAudit.audit,
|
|
2356
|
+
obstructionAudit: preparedAudit.event,
|
|
2357
|
+
authorization,
|
|
2358
|
+
sourceHeadCommit: currentHead,
|
|
2359
|
+
actor,
|
|
2360
|
+
reason,
|
|
2361
|
+
}, now);
|
|
2362
|
+
const resultTask = {
|
|
2363
|
+
...task,
|
|
2364
|
+
revision: task.revision + 1,
|
|
2365
|
+
updatedAt: now.toISOString(),
|
|
2366
|
+
status: 'awaiting_execution_authorization',
|
|
2367
|
+
authorizations: task.authorizations.map((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved'
|
|
2368
|
+
? { ...candidate, decision: 'superseded' }
|
|
2369
|
+
: candidate),
|
|
2370
|
+
blockReason: reason.trim(),
|
|
2371
|
+
};
|
|
2372
|
+
const projectRoot = this.store.projectRoot(task.projectId);
|
|
2373
|
+
const taskRoot = this.store.taskRoot(task.projectId, task.id);
|
|
2374
|
+
const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
|
|
2375
|
+
applyProjectTransaction({
|
|
2376
|
+
projectRoot,
|
|
2377
|
+
projectId: task.projectId,
|
|
2378
|
+
transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
|
|
2379
|
+
kind: 'preexecution-plan-authority-withdrawal',
|
|
2380
|
+
now,
|
|
2381
|
+
targets: [
|
|
2382
|
+
{ path: relative(path.join(taskRoot, 'plan-risk-audits.jsonl')), content: preparedAudit.content },
|
|
2383
|
+
{ path: relative(path.join(taskRoot, preExecutionReplanSidecar())), content: preparedMarker.postimage },
|
|
2384
|
+
{ path: relative(path.join(taskRoot, 'state.json')), content: `${JSON.stringify(resultTask, null, 2)}\n` },
|
|
2385
|
+
],
|
|
2386
|
+
validate: () => {
|
|
2387
|
+
const saved = this.store.readTask(task.projectId, task.id);
|
|
2388
|
+
const latestAudit = readPlanRiskAuditEvents(this.store, saved).at(-1) ?? null;
|
|
2389
|
+
const marker = readCurrentPreExecutionReplan(this.store, saved);
|
|
2390
|
+
if (saved.revision !== resultTask.revision
|
|
2391
|
+
|| saved.status !== 'awaiting_execution_authorization'
|
|
2392
|
+
|| saved.planHash !== task.planHash
|
|
2393
|
+
|| latestAudit?.eventHash !== preparedAudit.event.eventHash
|
|
2394
|
+
|| marker?.eventHash !== preparedMarker.event.eventHash
|
|
2395
|
+
|| saved.authorizations.some((candidate) => candidate.kind === 'execution' && candidate.decision === 'approved')
|
|
2396
|
+
|| hashCompletedSteps(saved) !== preparedMarker.event.completedStepsHash
|
|
2397
|
+
|| headCommit(context.identity.repositoryRoot) !== currentHead
|
|
2398
|
+
|| !isClean(context.identity.repositoryRoot)) {
|
|
2399
|
+
throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replan transaction readback failed.', { taskId });
|
|
2400
|
+
}
|
|
2401
|
+
if (saved.baseCommit)
|
|
2402
|
+
validateTaskHistory(context.identity.repositoryRoot, saved);
|
|
2403
|
+
},
|
|
2404
|
+
});
|
|
2405
|
+
return {
|
|
2406
|
+
task: this.store.readTask(task.projectId, task.id),
|
|
2407
|
+
audit: preparedAudit.event,
|
|
2408
|
+
event: preparedMarker.event,
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2131
2411
|
showTaskHandoff(repository, taskId) {
|
|
2132
2412
|
const { identity } = this.#mutationContext(repository);
|
|
2133
2413
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
@@ -2141,6 +2421,12 @@ export class WorkflowService {
|
|
|
2141
2421
|
const context = this.#mutationContext(repository);
|
|
2142
2422
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2143
2423
|
assertExpectedRevision(task, expectedRevision);
|
|
2424
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
2425
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task ownership cannot change while a pre-execution replan is pending.', {
|
|
2426
|
+
taskId,
|
|
2427
|
+
requiredAction: 'task plan-set',
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2144
2430
|
const normalizedActor = actor.trim();
|
|
2145
2431
|
if (!normalizedActor) {
|
|
2146
2432
|
throw new WorkflowError('INVALID_ARGUMENT', 'Handoff actor is required.');
|
|
@@ -2748,7 +3034,7 @@ export class WorkflowService {
|
|
|
2748
3034
|
const resultingHistory = !preparedRemediation || preparedRemediation.postimage === null
|
|
2749
3035
|
? stepHistory
|
|
2750
3036
|
: [...stepHistory, preparedRemediation.event];
|
|
2751
|
-
const rollingCauseDecision =
|
|
3037
|
+
const rollingCauseDecision = deriveCurrentCauseDecisionForTask(this.store, task, stepId, resultingHistory);
|
|
2752
3038
|
const c1Posture = readTaskC1Posture(this.store, task);
|
|
2753
3039
|
const preparedYield = rollingCauseDecision === 'root-cause-replan' && c1Posture.state === 'claimed'
|
|
2754
3040
|
? prepareTaskCorrectiveYield(this.store, paused, {
|
|
@@ -3062,7 +3348,7 @@ export class WorkflowService {
|
|
|
3062
3348
|
const resultingHistory = preparedRemediation.postimage === null
|
|
3063
3349
|
? stepHistory
|
|
3064
3350
|
: [...stepHistory, preparedRemediation.event];
|
|
3065
|
-
const rollingCauseDecision =
|
|
3351
|
+
const rollingCauseDecision = deriveCurrentCauseDecisionForTask(this.store, task, stepId, resultingHistory);
|
|
3066
3352
|
const resolved = applyStrictStepReviewResolution(task, stepId, 'failed', summary);
|
|
3067
3353
|
const paused = {
|
|
3068
3354
|
...resolved,
|
|
@@ -3086,7 +3372,7 @@ export class WorkflowService {
|
|
|
3086
3372
|
? prepareTaskCorrectiveYield(this.store, paused, {
|
|
3087
3373
|
actor: c1Posture.claimant,
|
|
3088
3374
|
writerToken: reviewLease.token,
|
|
3089
|
-
reason: `Yield execution authority after
|
|
3375
|
+
reason: `Yield execution authority after authoritative replan cause ${preparedRemediation.event.eventId}.`,
|
|
3090
3376
|
eventId: `HOF-${sha256Hex([
|
|
3091
3377
|
'strict-review-root-cause-replan-pause-v1',
|
|
3092
3378
|
task.id,
|
|
@@ -3809,6 +4095,9 @@ export class WorkflowService {
|
|
|
3809
4095
|
const now = this.now().getTime();
|
|
3810
4096
|
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
3811
4097
|
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
4098
|
+
const pendingPreExecutionReplanTaskIds = tasks
|
|
4099
|
+
.filter((task) => readCurrentPreExecutionReplan(this.store, task) !== null)
|
|
4100
|
+
.map((task) => task.id);
|
|
3812
4101
|
const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
3813
4102
|
const inspectedVersions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
|
|
3814
4103
|
const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
|
|
@@ -3836,6 +4125,9 @@ export class WorkflowService {
|
|
|
3836
4125
|
...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
|
|
3837
4126
|
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
3838
4127
|
...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
|
|
4128
|
+
...(pendingPreExecutionReplanTaskIds.length > 0
|
|
4129
|
+
? [`Complete the pending pre-execution Plan replacement and reauthorization first: ${pendingPreExecutionReplanTaskIds.join(', ')}.`]
|
|
4130
|
+
: []),
|
|
3839
4131
|
...versionBlockers,
|
|
3840
4132
|
...observationPreflightBlockers(snapshot.observation),
|
|
3841
4133
|
];
|
|
@@ -3861,10 +4153,12 @@ export class WorkflowService {
|
|
|
3861
4153
|
milestoneBases,
|
|
3862
4154
|
},
|
|
3863
4155
|
dependencyTransport: {
|
|
3864
|
-
safe: versionBlockers.length === 0,
|
|
3865
|
-
requiredAction:
|
|
3866
|
-
?
|
|
3867
|
-
:
|
|
4156
|
+
safe: versionBlockers.length === 0 && pendingPreExecutionReplanTaskIds.length === 0,
|
|
4157
|
+
requiredAction: pendingPreExecutionReplanTaskIds.length > 0
|
|
4158
|
+
? 'Complete each pending pre-execution task plan-set and reauthorization before changing Workflow dependency HEAD.'
|
|
4159
|
+
: versionBlockers.length === 0
|
|
4160
|
+
? null
|
|
4161
|
+
: 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
|
|
3868
4162
|
},
|
|
3869
4163
|
adoption: snapshot.adoption,
|
|
3870
4164
|
observation: snapshot.observation,
|
|
@@ -5071,6 +5365,11 @@ export class WorkflowService {
|
|
|
5071
5365
|
throw error;
|
|
5072
5366
|
const blockingDecision = error.details.decision;
|
|
5073
5367
|
const blockingAudit = readPlanRiskAuditEvents(this.store, activeTask).at(-1) ?? null;
|
|
5368
|
+
const preExecutionReplan = readCurrentPreExecutionReplan(this.store, activeTask);
|
|
5369
|
+
const verifiedPreExecutionReplan = preExecutionReplan
|
|
5370
|
+
&& blockingAudit?.eventHash === preExecutionReplan.obstructionPlanRiskAuditEventHash
|
|
5371
|
+
? resolvePreExecutionReplanAudits(this.store, activeTask, preExecutionReplan)
|
|
5372
|
+
: null;
|
|
5074
5373
|
const existingRemediationEvent = blockingAudit && activeTask.milestoneId
|
|
5075
5374
|
? [...readMilestoneScopeChangeEvents(this.store, status.projectId, activeTask.milestoneId)]
|
|
5076
5375
|
.reverse()
|
|
@@ -5104,50 +5403,34 @@ export class WorkflowService {
|
|
|
5104
5403
|
const eligibleRemediationPredecessorTaskIds = [...remediationAncestorIds]
|
|
5105
5404
|
.filter((candidateTaskId) => tasksById.get(candidateTaskId)?.status === 'merged')
|
|
5106
5405
|
.sort();
|
|
5107
|
-
planRiskAuditRecovery =
|
|
5406
|
+
planRiskAuditRecovery = verifiedPreExecutionReplan && preExecutionReplan
|
|
5108
5407
|
? {
|
|
5109
5408
|
scope: 'task',
|
|
5110
5409
|
id: activeTask.id,
|
|
5111
5410
|
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',
|
|
5411
|
+
action: 'task plan-set',
|
|
5121
5412
|
blockedAction: 'task authorize',
|
|
5122
5413
|
revision: activeTask.revision,
|
|
5123
|
-
|
|
5124
|
-
state: 'recorded
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5414
|
+
preExecutionReplan: {
|
|
5415
|
+
state: 'recorded',
|
|
5416
|
+
eventId: preExecutionReplan.eventId,
|
|
5417
|
+
eventHash: preExecutionReplan.eventHash,
|
|
5418
|
+
sourcePlanHash: preExecutionReplan.sourcePlanHash,
|
|
5419
|
+
sourceHeadCommit: preExecutionReplan.sourceHeadCommit,
|
|
5420
|
+
obstructionPlanRiskAuditEventId: preExecutionReplan.obstructionPlanRiskAuditEventId,
|
|
5421
|
+
obstructionPlanRiskAuditEventHash: preExecutionReplan.obstructionPlanRiskAuditEventHash,
|
|
5422
|
+
preservesTaskScope: true,
|
|
5423
|
+
preservesCompletedStepAuthority: true,
|
|
5424
|
+
requiredAction: 'Replace the obstructed implementation Plan through ordinary task plan-set.',
|
|
5128
5425
|
},
|
|
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'
|
|
5426
|
+
taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
|
|
5427
|
+
}
|
|
5428
|
+
: blockingDecision === 'stop-escalate' && auditedRemediationEligible
|
|
5429
|
+
? {
|
|
5430
|
+
scope: 'task',
|
|
5431
|
+
id: activeTask.id,
|
|
5432
|
+
status: activeTask.status,
|
|
5433
|
+
action: existingRemediationTask?.status === 'merged'
|
|
5151
5434
|
? 'task plan-set'
|
|
5152
5435
|
: existingRemediationTask
|
|
5153
5436
|
? 'doctor'
|
|
@@ -5156,46 +5439,84 @@ export class WorkflowService {
|
|
|
5156
5439
|
: remediationDiscovery
|
|
5157
5440
|
? 'discovery update'
|
|
5158
5441
|
: '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
5442
|
blockedAction: 'task authorize',
|
|
5171
5443
|
revision: activeTask.revision,
|
|
5172
5444
|
planRiskAuditDecision: {
|
|
5173
5445
|
state: 'recorded-blocking',
|
|
5174
5446
|
decision: blockingDecision,
|
|
5175
5447
|
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.',
|
|
5448
|
+
requiredAction: 'Materialize one audited additive remediation Task, merge it, then replace the rejected Plan.',
|
|
5179
5449
|
},
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5450
|
+
auditedRemediation: {
|
|
5451
|
+
state: existingRemediationTask?.status === 'merged'
|
|
5452
|
+
? 'merged-replan-required'
|
|
5453
|
+
: existingRemediationTask
|
|
5454
|
+
? 'materialized-not-merged'
|
|
5455
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
5456
|
+
? 'ready-to-materialize'
|
|
5457
|
+
: 'discovery-required',
|
|
5458
|
+
humanApprovalRequired: false,
|
|
5459
|
+
blockedTaskId: activeTask.id,
|
|
5460
|
+
blockedTaskRevision: activeTask.revision,
|
|
5461
|
+
milestoneId: remediationMilestone.id,
|
|
5462
|
+
milestoneRevision: remediationMilestone.revision,
|
|
5463
|
+
planRiskAuditEventId: blockingAudit?.eventId ?? null,
|
|
5464
|
+
planRiskAuditEventHash: blockingAudit?.eventHash ?? null,
|
|
5465
|
+
discoveryId: remediationDiscovery?.id ?? null,
|
|
5466
|
+
discoveryRevision: remediationDiscovery?.revision ?? null,
|
|
5467
|
+
remediationTaskId: existingRemediationTask?.id ?? null,
|
|
5468
|
+
remediationTaskStatus: existingRemediationTask?.status ?? null,
|
|
5469
|
+
requiredPredecessorTaskIds: [...(remediationMembership.dependsOnTaskIds ?? [])],
|
|
5470
|
+
eligibleMergedAncestorTaskIds: eligibleRemediationPredecessorTaskIds,
|
|
5471
|
+
command: existingRemediationTask?.status === 'merged'
|
|
5472
|
+
? 'task plan-set'
|
|
5473
|
+
: existingRemediationTask
|
|
5474
|
+
? 'doctor'
|
|
5475
|
+
: remediationDiscovery?.status === 'ready_to_materialize'
|
|
5476
|
+
? 'milestone remediation-materialize'
|
|
5477
|
+
: remediationDiscovery
|
|
5478
|
+
? 'discovery update'
|
|
5479
|
+
: 'discovery start',
|
|
5480
|
+
boundedEffect: 'append-one-required-remediation-task-and-add-one-blocked-task-dependency',
|
|
5481
|
+
preservesMilestoneSemantics: true,
|
|
5197
5482
|
},
|
|
5198
|
-
|
|
5483
|
+
taskPlanContract: this.taskPlanContract(status.projectId, activeTask),
|
|
5484
|
+
}
|
|
5485
|
+
: blockingDecision === 'stop-escalate' || blockingDecision === 'split-required'
|
|
5486
|
+
? {
|
|
5487
|
+
scope: 'task',
|
|
5488
|
+
id: activeTask.id,
|
|
5489
|
+
status: activeTask.status,
|
|
5490
|
+
action: blockingDecision === 'stop-escalate' ? 'task plan-set' : 'doctor',
|
|
5491
|
+
blockedAction: 'task authorize',
|
|
5492
|
+
revision: activeTask.revision,
|
|
5493
|
+
planRiskAuditDecision: {
|
|
5494
|
+
state: 'recorded-blocking',
|
|
5495
|
+
decision: blockingDecision,
|
|
5496
|
+
reason: error.message,
|
|
5497
|
+
requiredAction: blockingDecision === 'stop-escalate'
|
|
5498
|
+
? 'Replace the rejected Step Plan before requesting fresh authorization.'
|
|
5499
|
+
: 'Resolve the required cross-Task split through an explicit Milestone topology decision.',
|
|
5500
|
+
},
|
|
5501
|
+
...(blockingDecision === 'stop-escalate'
|
|
5502
|
+
? { taskPlanContract: this.taskPlanContract(status.projectId, activeTask) }
|
|
5503
|
+
: {}),
|
|
5504
|
+
}
|
|
5505
|
+
: {
|
|
5506
|
+
scope: 'task',
|
|
5507
|
+
id: activeTask.id,
|
|
5508
|
+
status: activeTask.status,
|
|
5509
|
+
action: 'task plan-risk-audit',
|
|
5510
|
+
revision: activeTask.revision,
|
|
5511
|
+
planRiskAuditRecovery: {
|
|
5512
|
+
state: 'required',
|
|
5513
|
+
reason: error.message,
|
|
5514
|
+
currentBriefHash: activeTask.briefHash,
|
|
5515
|
+
currentPlanHash: activeTask.planHash,
|
|
5516
|
+
knowledgeMapRevision: activeTask.knowledgeMapRevision,
|
|
5517
|
+
knowledgeMapHash: activeTask.knowledgeMapHash,
|
|
5518
|
+
},
|
|
5519
|
+
};
|
|
5199
5520
|
}
|
|
5200
5521
|
}
|
|
5201
5522
|
const taskNext = activeTask.status === 'planning'
|
|
@@ -6071,8 +6392,8 @@ export class WorkflowService {
|
|
|
6071
6392
|
...(task.steps.find((step) => step.id === guardedCorrectivePosture.stepId)?.status !== 'failed'
|
|
6072
6393
|
? [`${guardedCorrectivePosture.stepId} must remain failed.`]
|
|
6073
6394
|
: []),
|
|
6074
|
-
...(writerLease ? ['No active or stale writer lease may remain after the failure pause.'] : []),
|
|
6075
|
-
...(rollingC1Posture.state !== 'none'
|
|
6395
|
+
...(writerLease && rollingC1Posture.state !== 'claimed' ? ['No active or stale writer lease may remain after the failure pause.'] : []),
|
|
6396
|
+
...(rollingC1Posture.state !== 'none' && rollingC1Posture.state !== 'claimed'
|
|
6076
6397
|
? ['No C1 claim may remain after the root-cause failure pause.']
|
|
6077
6398
|
: []),
|
|
6078
6399
|
];
|
|
@@ -6089,7 +6410,7 @@ export class WorkflowService {
|
|
|
6089
6410
|
},
|
|
6090
6411
|
}
|
|
6091
6412
|
: {
|
|
6092
|
-
action: 'task plan-set',
|
|
6413
|
+
action: rollingC1Posture.state === 'claimed' ? 'task corrective-yield' : 'task plan-set',
|
|
6093
6414
|
blockedAction: 'task run',
|
|
6094
6415
|
stepId: guardedCorrectivePosture.stepId,
|
|
6095
6416
|
rollingWave: {
|
|
@@ -6236,9 +6557,42 @@ export class WorkflowService {
|
|
|
6236
6557
|
},
|
|
6237
6558
|
}
|
|
6238
6559
|
: {};
|
|
6560
|
+
const preExecutionReplanBlockers = task.status === 'ready'
|
|
6561
|
+
? [
|
|
6562
|
+
...(task.steps.some((step) => step.status === 'in_progress') ? ['No Step may be in progress.'] : []),
|
|
6563
|
+
...(task.steps.some((step) => step.status === 'failed') ? ['No failed Step may require corrective recovery.'] : []),
|
|
6564
|
+
...(handoffPosture.state !== 'none' ? ['No C1 claim or pending handoff may exist.'] : []),
|
|
6565
|
+
...(inspectRawWriterLease(this.store.lockRoot(task.projectId), task.id, this.store.root)
|
|
6566
|
+
? ['No active or stale writer lease may exist.']
|
|
6567
|
+
: []),
|
|
6568
|
+
...(!isClean(repositoryRoot) ? ['Repository checkout must be clean.'] : []),
|
|
6569
|
+
...(!planRiskAudit || !['approved', 'approved-with-rationale'].includes(planRiskAudit.audit.decision)
|
|
6570
|
+
? ['A current positive Plan Risk Audit is required.']
|
|
6571
|
+
: []),
|
|
6572
|
+
]
|
|
6573
|
+
: [];
|
|
6574
|
+
const preExecutionReplanOption = task.status === 'ready' && task.planHash && planRiskAudit
|
|
6575
|
+
? {
|
|
6576
|
+
command: 'task preexecution-replan',
|
|
6577
|
+
state: preExecutionReplanBlockers.length === 0 ? 'eligible' : 'blocked',
|
|
6578
|
+
expectedRevision: task.revision,
|
|
6579
|
+
expectedBriefHash: task.briefHash,
|
|
6580
|
+
expectedPlanHash: task.planHash,
|
|
6581
|
+
expectedPlanRiskAuditEventHash: planRiskAudit.audit.eventHash,
|
|
6582
|
+
expectedHeadCommit: headCommit(repositoryRoot),
|
|
6583
|
+
auditDecision: 'stop-escalate',
|
|
6584
|
+
actorMustEqualAuditAuditor: true,
|
|
6585
|
+
humanApprovalRequired: false,
|
|
6586
|
+
boundedEffect: 'record-current-plan-obstruction-and-withdraw-execution-authorization',
|
|
6587
|
+
preservesTaskScope: true,
|
|
6588
|
+
preservesCompletedStepAuthority: true,
|
|
6589
|
+
blockers: preExecutionReplanBlockers,
|
|
6590
|
+
}
|
|
6591
|
+
: null;
|
|
6239
6592
|
const result = {
|
|
6240
6593
|
...next,
|
|
6241
6594
|
...handoffOverlay,
|
|
6595
|
+
...(preExecutionReplanOption ? { preExecutionReplanOption } : {}),
|
|
6242
6596
|
...(strictStepReviewStep
|
|
6243
6597
|
? {
|
|
6244
6598
|
action: 'task step-review',
|
|
@@ -6414,6 +6768,9 @@ export class WorkflowService {
|
|
|
6414
6768
|
};
|
|
6415
6769
|
}
|
|
6416
6770
|
assertTaskContextChangeAllowed(task) {
|
|
6771
|
+
if (readCurrentPreExecutionReplan(this.store, task)) {
|
|
6772
|
+
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' });
|
|
6773
|
+
}
|
|
6417
6774
|
if (task.status === 'needs_fix' && !task.steps.some((step) => step.status === 'failed')) {
|
|
6418
6775
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} requires a replacement Step Plan; Project Knowledge cannot rebind or refresh the stale Plan.`, {
|
|
6419
6776
|
taskId: task.id,
|
|
@@ -7623,6 +7980,83 @@ function taskHasStarted(task) {
|
|
|
7623
7980
|
'merged',
|
|
7624
7981
|
].includes(task.status);
|
|
7625
7982
|
}
|
|
7983
|
+
function resolvePreExecutionReplanAudits(store, task, marker) {
|
|
7984
|
+
const audits = readPlanRiskAuditEvents(store, task);
|
|
7985
|
+
const sourceAudit = audits.find((audit) => audit.eventId === marker.sourcePlanRiskAuditEventId
|
|
7986
|
+
&& audit.eventHash === marker.sourcePlanRiskAuditEventHash) ?? null;
|
|
7987
|
+
const obstructionAudit = audits.find((audit) => audit.eventId === marker.obstructionPlanRiskAuditEventId
|
|
7988
|
+
&& audit.eventHash === marker.obstructionPlanRiskAuditEventHash) ?? null;
|
|
7989
|
+
const sourceAuthorization = task.authorizations.find((authorization) => authorization.kind === 'execution'
|
|
7990
|
+
&& authorization.decision === 'superseded'
|
|
7991
|
+
&& hashExecutionAuthorization({ ...authorization, decision: 'approved' }) === marker.sourceExecutionAuthorizationHash);
|
|
7992
|
+
if (!sourceAudit || !obstructionAudit || audits.at(-1)?.eventHash !== obstructionAudit.eventHash
|
|
7993
|
+
|| !['approved', 'approved-with-rationale'].includes(sourceAudit.decision)
|
|
7994
|
+
|| obstructionAudit.decision !== 'stop-escalate'
|
|
7995
|
+
|| sourceAudit.planHash !== task.planHash || obstructionAudit.planHash !== task.planHash
|
|
7996
|
+
|| !sourceAuthorization) {
|
|
7997
|
+
throw new WorkflowError('STATE_CORRUPT', 'Pre-execution replan provenance chain is incomplete or inconsistent.', {
|
|
7998
|
+
taskId: task.id,
|
|
7999
|
+
eventId: marker.eventId,
|
|
8000
|
+
});
|
|
8001
|
+
}
|
|
8002
|
+
return { sourceAudit, obstructionAudit };
|
|
8003
|
+
}
|
|
8004
|
+
function assertPlanRiskSafetyPreserved(source, candidate, label) {
|
|
8005
|
+
const candidateByStep = new Map(candidate.stepClassifications.map((classification) => [classification.stepId, classification]));
|
|
8006
|
+
const missing = [];
|
|
8007
|
+
for (const sourceClassification of source.stepClassifications) {
|
|
8008
|
+
const candidateClassification = candidateByStep.get(sourceClassification.stepId);
|
|
8009
|
+
if (!candidateClassification) {
|
|
8010
|
+
missing.push({ stepId: sourceClassification.stepId, field: 'classification', values: [] });
|
|
8011
|
+
continue;
|
|
8012
|
+
}
|
|
8013
|
+
const preserveStrings = (field, sourceValues, candidateValues) => {
|
|
8014
|
+
const absent = sourceValues.filter((value) => !candidateValues.includes(value));
|
|
8015
|
+
if (absent.length > 0)
|
|
8016
|
+
missing.push({ stepId: sourceClassification.stepId, field, values: absent });
|
|
8017
|
+
};
|
|
8018
|
+
preserveStrings('categories', sourceClassification.categories, candidateClassification.categories);
|
|
8019
|
+
preserveStrings('failureModes', sourceClassification.failureModes, candidateClassification.failureModes);
|
|
8020
|
+
preserveStrings('requiredEvidence', sourceClassification.requiredEvidence, candidateClassification.requiredEvidence);
|
|
8021
|
+
if (sourceClassification.reviewRequired && !candidateClassification.reviewRequired) {
|
|
8022
|
+
missing.push({ stepId: sourceClassification.stepId, field: 'reviewRequired', values: [true] });
|
|
8023
|
+
}
|
|
8024
|
+
const proofBinding = (proof) => canonicalJsonStringify({ failureMode: proof.failureMode, evidence: proof.evidence });
|
|
8025
|
+
const candidateProofs = new Set((candidateClassification.proofObligations ?? []).map(proofBinding));
|
|
8026
|
+
const absentProofs = (sourceClassification.proofObligations ?? [])
|
|
8027
|
+
.filter((proof) => !candidateProofs.has(proofBinding(proof)));
|
|
8028
|
+
if (absentProofs.length > 0) {
|
|
8029
|
+
missing.push({ stepId: sourceClassification.stepId, field: 'proofObligations', values: absentProofs });
|
|
8030
|
+
}
|
|
8031
|
+
}
|
|
8032
|
+
if (missing.length > 0) {
|
|
8033
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `${label} cannot suppress existing guarded risk or proof authority.`, {
|
|
8034
|
+
missing,
|
|
8035
|
+
});
|
|
8036
|
+
}
|
|
8037
|
+
const sourceProofText = new Map(source.stepClassifications.flatMap((classification) => (classification.proofObligations ?? []).map((proof) => [
|
|
8038
|
+
canonicalJsonStringify({ stepId: classification.stepId, failureMode: proof.failureMode, evidence: proof.evidence }),
|
|
8039
|
+
canonicalJsonStringify({ counterexample: proof.counterexample, expectedResult: proof.expectedResult }),
|
|
8040
|
+
])));
|
|
8041
|
+
const revisedProofText = candidate.stepClassifications.some((classification) => (classification.proofObligations ?? []).some((proof) => {
|
|
8042
|
+
const binding = canonicalJsonStringify({
|
|
8043
|
+
stepId: classification.stepId,
|
|
8044
|
+
failureMode: proof.failureMode,
|
|
8045
|
+
evidence: proof.evidence,
|
|
8046
|
+
});
|
|
8047
|
+
const previous = sourceProofText.get(binding);
|
|
8048
|
+
return previous !== undefined
|
|
8049
|
+
&& previous !== canonicalJsonStringify({ counterexample: proof.counterexample, expectedResult: proof.expectedResult });
|
|
8050
|
+
}));
|
|
8051
|
+
if (revisedProofText && (candidate.decision !== 'approved-with-rationale'
|
|
8052
|
+
|| candidate.summary.trim() === source.summary.trim())) {
|
|
8053
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `${label} may revise proof prose only with approved-with-rationale and a new audit summary.`, {
|
|
8054
|
+
sourceSummary: source.summary,
|
|
8055
|
+
candidateSummary: candidate.summary,
|
|
8056
|
+
candidateDecision: candidate.decision,
|
|
8057
|
+
});
|
|
8058
|
+
}
|
|
8059
|
+
}
|
|
7626
8060
|
function taskCanRebindKnowledge(task) {
|
|
7627
8061
|
return [
|
|
7628
8062
|
'awaiting_execution_authorization',
|