codex-workflow-v2 2.0.0-alpha.6.1 → 2.0.0-alpha.6.2
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 +29 -1
- package/dist/src/alpha6/remediation.d.ts +20 -0
- package/dist/src/alpha6/remediation.js +139 -4
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/cli.js +7 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +5 -0
- package/dist/src/state/lock.d.ts +3 -1
- package/dist/src/state/lock.js +5 -2
- package/dist/src/state/lock.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 +39 -3
- package/dist/src/workflow.js +490 -62
- package/dist/src/workflow.js.map +1 -1
- package/docs/release.md +4 -0
- package/docs/updating-existing-project.md +18 -0
- package/docs/validation-report.md +19 -10
- package/package.json +2 -2
- package/schemas/context-envelope.schema.json +10 -0
- package/schemas/corrective-plan-audit-record.schema.json +47 -0
package/dist/src/workflow.js
CHANGED
|
@@ -12,9 +12,9 @@ import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, ins
|
|
|
12
12
|
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
13
13
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
14
14
|
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
15
|
-
import { appendCorrectiveDecisionEvent, appendRemediationEvent, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, findFailedGuardedStepRequiringCorrectiveDecision, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
|
|
15
|
+
import { appendCorrectivePlanAuditRecord, appendCorrectiveDecisionEvent, appendRemediationEvent, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, findFailedGuardedStepRequiringCorrectiveDecision, hasCorrectivePlanAuditBinding, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
|
|
16
16
|
import { applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
17
|
-
import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
|
|
17
|
+
import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, readReviewerAttestations, } from './alpha6/review.js';
|
|
18
18
|
import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
|
|
19
19
|
import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
|
|
20
20
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
@@ -24,12 +24,15 @@ import { WriterLockManager } from './state/lock.js';
|
|
|
24
24
|
import { FileStateStore } from './state/store.js';
|
|
25
25
|
import { createEntityId } from './ulid.js';
|
|
26
26
|
import { PACKAGE_VERSION } from './version.js';
|
|
27
|
+
const ALPHA6_CORRECTIVE_RESCUE_HEAD = '5c63798cc7b8fbb5e1c8b26ebf93876bc642295a';
|
|
27
28
|
export class WorkflowService {
|
|
28
29
|
store;
|
|
29
30
|
now;
|
|
30
|
-
|
|
31
|
+
alpha6CorrectiveRescueHead;
|
|
32
|
+
constructor(store = new FileStateStore(), now = () => new Date(), alpha6CorrectiveRescueHead = ALPHA6_CORRECTIVE_RESCUE_HEAD) {
|
|
31
33
|
this.store = store;
|
|
32
34
|
this.now = now;
|
|
35
|
+
this.alpha6CorrectiveRescueHead = alpha6CorrectiveRescueHead;
|
|
33
36
|
}
|
|
34
37
|
context(repository) {
|
|
35
38
|
const identity = resolveRepositoryIdentity(repository);
|
|
@@ -565,14 +568,13 @@ export class WorkflowService {
|
|
|
565
568
|
this.validateKnowledgeTargets(knowledgeMap, plan);
|
|
566
569
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
567
570
|
assertExpectedRevision(task, expectedRevision);
|
|
568
|
-
if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
|
|
571
|
+
if (!['planning', 'awaiting_execution_authorization', 'ready', 'needs_fix', 'blocked'].includes(task.status)) {
|
|
569
572
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
|
|
570
573
|
}
|
|
571
574
|
const currentPlanCorrectiveDecisions = task.planHash
|
|
572
575
|
? readCorrectiveDecisionEvents(this.store, task).filter((event) => event.planHash === task.planHash)
|
|
573
576
|
: [];
|
|
574
577
|
const currentReplanDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'replan-required');
|
|
575
|
-
const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'continue-fix');
|
|
576
578
|
const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required' || event.decision === 'stop-escalate');
|
|
577
579
|
const currentReplanRequiredStepIds = [...new Set(currentReplanDecisions.map((event) => event.stepId))];
|
|
578
580
|
if (currentHardBlockingDecisions.length > 0) {
|
|
@@ -593,33 +595,22 @@ export class WorkflowService {
|
|
|
593
595
|
requiredAction: 'Resolve the blocked strict review or record a replan-required corrective decision first.',
|
|
594
596
|
});
|
|
595
597
|
}
|
|
596
|
-
|
|
597
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} already has a current continue-fix corrective decision; use the current Plan retry path instead of issuing another plan-set.`, {
|
|
598
|
-
taskId: task.id,
|
|
599
|
-
requiredAction: 'Run the guarded Step under the current Plan retry path, or escalate/cancel instead of issuing another plan-set at the same ordinal.',
|
|
600
|
-
decisions: currentContinueFixDecisions.map((event) => ({
|
|
601
|
-
eventId: event.eventId,
|
|
602
|
-
stepId: event.stepId,
|
|
603
|
-
triggeringAttemptCount: event.triggeringAttemptCount,
|
|
604
|
-
})),
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
const currentPlanRiskAudit = task.status === 'needs_fix'
|
|
608
|
-
? readCurrentPlanRiskAudit(this.store, task)
|
|
609
|
-
: null;
|
|
598
|
+
const currentPlanRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
|
|
610
599
|
const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
|
|
611
600
|
if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
|
|
612
601
|
validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
|
|
613
602
|
}
|
|
614
|
-
const guardedRemediationGate =
|
|
603
|
+
const guardedRemediationGate = ['needs_fix', 'awaiting_execution_authorization', 'ready'].includes(task.status)
|
|
604
|
+
&& currentPlanRiskAudit
|
|
615
605
|
? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
|
|
616
606
|
: null;
|
|
617
|
-
if (guardedRemediationGate &&
|
|
618
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a corrective decision before
|
|
607
|
+
if (guardedRemediationGate && !guardedRemediationGate.correctiveDecision) {
|
|
608
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a separate corrective decision before a corrective Plan can be applied.`, {
|
|
619
609
|
taskId: task.id,
|
|
620
610
|
stepId: guardedRemediationGate.stepId,
|
|
621
611
|
attemptCount: guardedRemediationGate.attemptCount,
|
|
622
612
|
requiredSidecar: 'corrective-decisions.jsonl',
|
|
613
|
+
requiredAction: 'Run task corrective-decision first.',
|
|
623
614
|
allowedDecision: 'continue-fix',
|
|
624
615
|
escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
|
|
625
616
|
});
|
|
@@ -631,25 +622,57 @@ export class WorkflowService {
|
|
|
631
622
|
requiredAction: 'Retry task plan-set without corrective-audit input; the recorded replan-required decision is the authorization for this recovery path.',
|
|
632
623
|
});
|
|
633
624
|
}
|
|
634
|
-
|
|
635
|
-
|
|
625
|
+
const guardedContinueFixDecision = guardedRemediationGate?.correctiveDecision?.decision === 'continue-fix'
|
|
626
|
+
? guardedRemediationGate.correctiveDecision
|
|
627
|
+
: null;
|
|
628
|
+
if (task.status === 'ready' && !guardedContinueFixDecision) {
|
|
629
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
|
|
630
|
+
}
|
|
631
|
+
if (guardedRemediationGate?.correctiveDecision && !guardedContinueFixDecision && currentReplanDecisions.length === 0) {
|
|
632
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision ${guardedRemediationGate.correctiveDecision.decision} prohibits continue-fix Plan application.`, {
|
|
636
633
|
taskId: task.id,
|
|
637
634
|
stepId: guardedRemediationGate.stepId,
|
|
638
|
-
|
|
635
|
+
decisionEventId: guardedRemediationGate.correctiveDecision.eventId,
|
|
639
636
|
});
|
|
640
637
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
638
|
+
if (guardedContinueFixDecision) {
|
|
639
|
+
validateGuardedCorrectivePlanAudit(this.store, task, guardedRemediationGate.stepId, guardedContinueFixDecision, correctiveAudit);
|
|
640
|
+
}
|
|
641
|
+
if (guardedContinueFixDecision) {
|
|
642
|
+
if (!sameOrderedStrings(plan.requirements, task.requirements)) {
|
|
643
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan requirements must exactly preserve Task scope.', {
|
|
644
|
+
expected: task.requirements,
|
|
645
|
+
actual: plan.requirements,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
if (!sameOrderedStrings(plan.acceptance, task.acceptance)) {
|
|
649
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan acceptance must exactly preserve Task scope.', {
|
|
650
|
+
expected: task.acceptance,
|
|
651
|
+
actual: plan.acceptance,
|
|
652
|
+
});
|
|
644
653
|
}
|
|
645
654
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
655
|
+
else {
|
|
656
|
+
for (const requirement of task.requirements) {
|
|
657
|
+
if (!plan.requirements.includes(requirement)) {
|
|
658
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task requirement ${requirement}.`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
for (const acceptance of task.acceptance) {
|
|
662
|
+
if (!plan.acceptance.includes(acceptance)) {
|
|
663
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task acceptance ${acceptance}.`);
|
|
664
|
+
}
|
|
649
665
|
}
|
|
650
666
|
}
|
|
651
667
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
652
668
|
const stagedPlan = this.store.stageArtifact(root, 'plan.md', renderPlan(task, plan));
|
|
669
|
+
if (guardedContinueFixDecision && stagedPlan.hash === task.planHash) {
|
|
670
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Continue-fix requires a changed full corrective Plan.', {
|
|
671
|
+
taskId: task.id,
|
|
672
|
+
stepId: guardedRemediationGate.stepId,
|
|
673
|
+
planHash: task.planHash,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
653
676
|
const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
654
677
|
? { ...authorization, decision: 'superseded' }
|
|
655
678
|
: authorization);
|
|
@@ -679,9 +702,8 @@ export class WorkflowService {
|
|
|
679
702
|
stepId: guardedRemediationGate.stepId,
|
|
680
703
|
});
|
|
681
704
|
}
|
|
682
|
-
if (
|
|
683
|
-
|
|
684
|
-
appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, stagedPlan.hash, this.now());
|
|
705
|
+
if (guardedContinueFixDecision) {
|
|
706
|
+
assertNonTargetStepsUnchanged(task, plan, guardedRemediationGate.stepId);
|
|
685
707
|
}
|
|
686
708
|
const candidate = {
|
|
687
709
|
...task,
|
|
@@ -704,12 +726,16 @@ export class WorkflowService {
|
|
|
704
726
|
evidenceHash: null,
|
|
705
727
|
blockReason: null,
|
|
706
728
|
};
|
|
707
|
-
|
|
708
|
-
|
|
729
|
+
const effectivePlanRiskAudit = planRiskAudit ?? (guardedContinueFixDecision ? currentPlanRiskAudit?.audit ?? null : null);
|
|
730
|
+
if (effectivePlanRiskAudit)
|
|
731
|
+
validatePlanRiskAuditCandidate(this.store, candidate, effectivePlanRiskAudit);
|
|
709
732
|
const saved = this.store.writeTask(candidate, expectedRevision);
|
|
710
733
|
this.store.publishArtifact(stagedPlan);
|
|
711
|
-
if (
|
|
712
|
-
appendCurrentPlanRiskAudit(this.store, saved,
|
|
734
|
+
if (effectivePlanRiskAudit)
|
|
735
|
+
appendCurrentPlanRiskAudit(this.store, saved, effectivePlanRiskAudit, this.now());
|
|
736
|
+
if (guardedContinueFixDecision && correctiveAudit) {
|
|
737
|
+
appendCorrectivePlanAuditRecord(this.store, task, saved, guardedRemediationGate.stepId, guardedContinueFixDecision, correctiveAudit, this.now());
|
|
738
|
+
}
|
|
713
739
|
if (correctiveAudit && task.status === 'needs_fix' && task.review.status === 'failed') {
|
|
714
740
|
appendCorrectivePlanAudit(this.store.taskRoot(identity.projectId, taskId), task, saved, failedReviewAttempts, correctiveAudit, this.now());
|
|
715
741
|
}
|
|
@@ -735,6 +761,11 @@ export class WorkflowService {
|
|
|
735
761
|
if (activeStep) {
|
|
736
762
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot rebind Task Plan knowledge while ${activeStep.id} is in progress.`);
|
|
737
763
|
}
|
|
764
|
+
const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
765
|
+
const breakerPlanRiskAudit = currentPlanRiskAudit
|
|
766
|
+
&& findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
|
|
767
|
+
? currentPlanRiskAudit
|
|
768
|
+
: null;
|
|
738
769
|
const knowledgeMap = this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
739
770
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
740
771
|
if (task.knowledgeMapRevision === knowledgeMap.revision &&
|
|
@@ -794,6 +825,9 @@ export class WorkflowService {
|
|
|
794
825
|
blockReason: null,
|
|
795
826
|
}, expectedRevision);
|
|
796
827
|
this.store.publishArtifact(stagedPlan);
|
|
828
|
+
if (breakerPlanRiskAudit) {
|
|
829
|
+
appendReboundPlanRiskAudit(this.store, task, saved, breakerPlanRiskAudit, this.now());
|
|
830
|
+
}
|
|
797
831
|
return saved;
|
|
798
832
|
}
|
|
799
833
|
refreshTaskContext(repository, taskId, expectedTaskRevision, expectedKnowledgeMapRevision, actor, delegationGrantId) {
|
|
@@ -815,6 +849,8 @@ export class WorkflowService {
|
|
|
815
849
|
const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
|
|
816
850
|
const transitions = [];
|
|
817
851
|
const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
852
|
+
const breakerPlanRiskAuditWillBePreserved = currentPlanRiskAudit
|
|
853
|
+
&& findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit) !== null;
|
|
818
854
|
// Validate every delegated transition before the first write so a narrow grant cannot
|
|
819
855
|
// leave the composite operation half-applied.
|
|
820
856
|
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
@@ -836,7 +872,7 @@ export class WorkflowService {
|
|
|
836
872
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
|
|
837
873
|
}
|
|
838
874
|
let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
|
|
839
|
-
if (currentPlanRiskAudit) {
|
|
875
|
+
if (currentPlanRiskAudit && !breakerPlanRiskAuditWillBePreserved) {
|
|
840
876
|
appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
|
|
841
877
|
}
|
|
842
878
|
transitions.push('task knowledge-rebind');
|
|
@@ -1118,9 +1154,9 @@ export class WorkflowService {
|
|
|
1118
1154
|
currentAuditPresent: Boolean(planRiskAudit),
|
|
1119
1155
|
});
|
|
1120
1156
|
}
|
|
1121
|
-
|
|
1122
|
-
assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit)
|
|
1123
|
-
|
|
1157
|
+
const remediationGate = planRiskAudit && isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)
|
|
1158
|
+
? assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit)
|
|
1159
|
+
: null;
|
|
1124
1160
|
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1125
1161
|
if (step.status === 'planned' && dirty.length > 0) {
|
|
1126
1162
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
@@ -1223,6 +1259,15 @@ export class WorkflowService {
|
|
|
1223
1259
|
stepId,
|
|
1224
1260
|
expectedRevision: saved.revision,
|
|
1225
1261
|
writerToken: lease.token,
|
|
1262
|
+
...(remediationGate
|
|
1263
|
+
? {
|
|
1264
|
+
remediation: {
|
|
1265
|
+
attemptOrdinal: remediationGate.nextAttemptOrdinal,
|
|
1266
|
+
mode: remediationGate.mode,
|
|
1267
|
+
correctiveDecisionEventId: remediationGate.correctiveDecision?.eventId ?? null,
|
|
1268
|
+
},
|
|
1269
|
+
}
|
|
1270
|
+
: {}),
|
|
1226
1271
|
knowledgeMap: knowledgeSummary(knowledgeMap),
|
|
1227
1272
|
knowledgeSources: selectKnowledgeSources(knowledgeMap, 'worker', task.knowledgeTargets.map((target) => target.path)),
|
|
1228
1273
|
},
|
|
@@ -1728,7 +1773,7 @@ export class WorkflowService {
|
|
|
1728
1773
|
assertExpectedRevision(task, expectedRevision);
|
|
1729
1774
|
this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
|
|
1730
1775
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
1731
|
-
if (
|
|
1776
|
+
if (!['needs_fix', 'awaiting_execution_authorization', 'ready'].includes(task.status)) {
|
|
1732
1777
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
|
|
1733
1778
|
}
|
|
1734
1779
|
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
@@ -1751,25 +1796,45 @@ export class WorkflowService {
|
|
|
1751
1796
|
stepId,
|
|
1752
1797
|
});
|
|
1753
1798
|
}
|
|
1754
|
-
if (
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
}
|
|
1799
|
+
if (step.status !== 'failed') {
|
|
1800
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a currently failed guarded Step.`, {
|
|
1801
|
+
taskId,
|
|
1802
|
+
stepId,
|
|
1803
|
+
taskStatus: task.status,
|
|
1804
|
+
stepStatus: step.status,
|
|
1805
|
+
});
|
|
1762
1806
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1807
|
+
const remediationEvents = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
|
|
1808
|
+
if (remediationEvents.length >= 3) {
|
|
1809
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} reached the hard stop after its third failed remediation review.`, {
|
|
1810
|
+
taskId,
|
|
1811
|
+
stepId,
|
|
1812
|
+
failedAttemptCount: remediationEvents.length,
|
|
1813
|
+
allowedDecisions: ['split-required', 'stop-escalate'],
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
if (remediationEvents.length !== 2) {
|
|
1817
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires exactly two guarded failures.`, {
|
|
1818
|
+
taskId,
|
|
1819
|
+
stepId,
|
|
1820
|
+
attemptCount: remediationEvents.length,
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
const existingDecision = readCorrectiveDecisionEvents(this.store, task)
|
|
1824
|
+
.find((event) => event.stepId === stepId && event.triggeringAttemptCount === 3) ?? null;
|
|
1825
|
+
if (existingDecision) {
|
|
1826
|
+
throw new WorkflowError('STATE_CONFLICT', `Corrective decision for ${stepId} already exists.`, {
|
|
1827
|
+
taskId,
|
|
1828
|
+
stepId,
|
|
1829
|
+
decisionEventId: existingDecision.eventId,
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
const root = this.store.taskRoot(task.projectId, task.id);
|
|
1833
|
+
if (this.store.hashArtifact(root, 'brief.md') !== task.briefHash || this.store.hashArtifact(root, 'plan.md') !== task.planHash) {
|
|
1834
|
+
throw new WorkflowError('STATE_CORRUPT', 'Corrective decision requires unchanged Brief and exact current Plan bindings.', {
|
|
1835
|
+
taskId,
|
|
1836
|
+
stepId,
|
|
1837
|
+
});
|
|
1773
1838
|
}
|
|
1774
1839
|
assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
|
|
1775
1840
|
return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
|
|
@@ -2006,6 +2071,243 @@ export class WorkflowService {
|
|
|
2006
2071
|
adoption,
|
|
2007
2072
|
};
|
|
2008
2073
|
}
|
|
2074
|
+
alpha6CorrectiveRescuePreflight(repository) {
|
|
2075
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
2076
|
+
const locks = new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, { ensureRoot: false });
|
|
2077
|
+
const dependency = inspectLocalDependency(identity.repositoryRoot);
|
|
2078
|
+
const tasks = this.store.listTasks(identity.projectId);
|
|
2079
|
+
const leases = locks.list();
|
|
2080
|
+
const dirty = changedFiles(identity.repositoryRoot);
|
|
2081
|
+
const currentHead = headCommit(identity.repositoryRoot);
|
|
2082
|
+
const candidates = [];
|
|
2083
|
+
const malformed = [];
|
|
2084
|
+
for (const task of tasks) {
|
|
2085
|
+
if (task.revision !== 14 || task.status !== 'ready')
|
|
2086
|
+
continue;
|
|
2087
|
+
const step = task.steps.find((entry) => entry.id === 'STEP-001') ?? null;
|
|
2088
|
+
if (!step || step.status !== 'failed' || !task.planHash)
|
|
2089
|
+
continue;
|
|
2090
|
+
try {
|
|
2091
|
+
const root = this.store.taskRoot(task.projectId, task.id);
|
|
2092
|
+
if (this.store.hashArtifact(root, 'brief.md') !== task.briefHash) {
|
|
2093
|
+
throw new WorkflowError('STATE_CORRUPT', 'Brief artifact changed after the recovery boundary.');
|
|
2094
|
+
}
|
|
2095
|
+
const discovery = this.store.readDiscovery(task.projectId, task.discoveryId);
|
|
2096
|
+
if (sha256Hex(renderBrief(discovery, task.title)) !== task.briefHash) {
|
|
2097
|
+
throw new WorkflowError('STATE_CORRUPT', 'Recovery Task Brief no longer matches its immutable Discovery scope.');
|
|
2098
|
+
}
|
|
2099
|
+
if (this.store.hashArtifact(root, 'plan.md') !== task.planHash) {
|
|
2100
|
+
throw new WorkflowError('STATE_CORRUPT', 'Plan artifact does not match the recovery Task binding.');
|
|
2101
|
+
}
|
|
2102
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
2103
|
+
if (!planRiskAudit || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
|
|
2104
|
+
throw new WorkflowError('STATE_CORRUPT', 'Recovery Step is not guarded by the exact current Plan Risk Audit.');
|
|
2105
|
+
}
|
|
2106
|
+
const allRemediationEvents = readRemediationEvents(this.store, task);
|
|
2107
|
+
const remediationEvents = allRemediationEvents.filter((event) => event.stepId === step.id);
|
|
2108
|
+
if (allRemediationEvents.length !== 2
|
|
2109
|
+
|| remediationEvents.length !== allRemediationEvents.length
|
|
2110
|
+
|| remediationEvents.some((event, index) => event.attemptOrdinal !== index + 1
|
|
2111
|
+
|| event.mode !== 'ordinary'
|
|
2112
|
+
|| event.failureKind !== 'review-failed'
|
|
2113
|
+
|| event.failureEvidence.reviewEventHash === null
|
|
2114
|
+
|| event.failureEvidence.reviewerAttestationHash === null)) {
|
|
2115
|
+
throw new WorkflowError('STATE_CORRUPT', 'Recovery requires exactly two ordinary verified strict-review failures.');
|
|
2116
|
+
}
|
|
2117
|
+
const failureIdentities = new Set(remediationEvents.map((event) => [
|
|
2118
|
+
event.failureEvidence.completionCommit,
|
|
2119
|
+
event.failureEvidence.reviewEventHash,
|
|
2120
|
+
event.failureEvidence.reviewerAttestationHash,
|
|
2121
|
+
].join('\0')));
|
|
2122
|
+
if (failureIdentities.size !== 2) {
|
|
2123
|
+
throw new WorkflowError('STATE_CORRUPT', 'Recovery strict-review failures are not distinct.');
|
|
2124
|
+
}
|
|
2125
|
+
const attestations = readReviewerAttestations(this.store, task);
|
|
2126
|
+
if (remediationEvents.some((event) => {
|
|
2127
|
+
const attestation = attestations.find((entry) => entry.eventHash === event.failureEvidence.reviewerAttestationHash);
|
|
2128
|
+
return !attestation || attestation.isolationResult !== 'verified';
|
|
2129
|
+
})) {
|
|
2130
|
+
throw new WorkflowError('STATE_CORRUPT', 'Recovery strict-review failures lack verified reviewer isolation.');
|
|
2131
|
+
}
|
|
2132
|
+
if (readCorrectiveDecisionEvents(this.store, task).some((event) => event.stepId === step.id)) {
|
|
2133
|
+
throw new WorkflowError('STATE_CORRUPT', 'Recovery Task already has a corrective decision.');
|
|
2134
|
+
}
|
|
2135
|
+
const posture = readTaskC1Posture(this.store, task);
|
|
2136
|
+
if (posture.state !== 'claimed' || !posture.writerLeaseTokenHash) {
|
|
2137
|
+
throw new WorkflowError('STATE_CORRUPT', 'Recovery Task requires claimed C1 with a bound writer lease token.');
|
|
2138
|
+
}
|
|
2139
|
+
const lease = leases.find((entry) => entry.entityId === task.id) ?? null;
|
|
2140
|
+
if (!lease
|
|
2141
|
+
|| lease.owner !== posture.claimant
|
|
2142
|
+
|| sha256Hex(lease.token) !== posture.writerLeaseTokenHash) {
|
|
2143
|
+
throw new WorkflowError('STATE_CORRUPT', 'Recovery lease does not exactly match the C1 claimant/token binding.');
|
|
2144
|
+
}
|
|
2145
|
+
const { token: _secret, ...publicLease } = lease;
|
|
2146
|
+
let milestoneScopeHash = null;
|
|
2147
|
+
if (task.milestoneId) {
|
|
2148
|
+
const milestone = this.store.readMilestone(task.projectId, task.milestoneId);
|
|
2149
|
+
const matchingMemberships = milestone.memberships.filter((entry) => entry.taskId === task.id);
|
|
2150
|
+
if (matchingMemberships.length !== 1 || !milestone.taskIds.includes(task.id)) {
|
|
2151
|
+
throw new WorkflowError('STATE_CORRUPT', 'Recovery Task no longer matches exact Milestone membership scope.');
|
|
2152
|
+
}
|
|
2153
|
+
milestoneScopeHash = milestoneScopeDigest(milestone);
|
|
2154
|
+
}
|
|
2155
|
+
candidates.push({
|
|
2156
|
+
taskId: task.id,
|
|
2157
|
+
taskRevision: 14,
|
|
2158
|
+
taskStatus: 'ready',
|
|
2159
|
+
stepId: 'STEP-001',
|
|
2160
|
+
stepStatus: 'failed',
|
|
2161
|
+
attemptCount: 2,
|
|
2162
|
+
briefHash: task.briefHash,
|
|
2163
|
+
planHash: task.planHash,
|
|
2164
|
+
claimant: posture.claimant,
|
|
2165
|
+
lease: {
|
|
2166
|
+
...publicLease,
|
|
2167
|
+
stale: Date.parse(lease.expiresAt) <= this.now().getTime(),
|
|
2168
|
+
tokenBound: true,
|
|
2169
|
+
},
|
|
2170
|
+
milestoneScopeHash,
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2173
|
+
catch (error) {
|
|
2174
|
+
malformed.push(error instanceof Error ? error.message : String(error));
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
const candidate = candidates.length === 1 ? candidates[0] : null;
|
|
2178
|
+
const blockers = [
|
|
2179
|
+
...(dependency.declared !== '2.0.0-alpha.6'
|
|
2180
|
+
? ['Project package.json must declare codex-workflow-v2 exactly at 2.0.0-alpha.6.']
|
|
2181
|
+
: []),
|
|
2182
|
+
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
2183
|
+
...(currentHead !== this.alpha6CorrectiveRescueHead
|
|
2184
|
+
? [`HEAD must exactly equal ${this.alpha6CorrectiveRescueHead}.`]
|
|
2185
|
+
: []),
|
|
2186
|
+
...(candidates.length !== 1
|
|
2187
|
+
? [`Expected exactly one alpha.6 corrective recovery candidate; found ${candidates.length}.`]
|
|
2188
|
+
: []),
|
|
2189
|
+
...(candidate && leases.some((lease) => lease.entityId !== candidate.taskId)
|
|
2190
|
+
? ['An unrelated writer lease exists outside the recovery Task.']
|
|
2191
|
+
: []),
|
|
2192
|
+
...malformed.map((message) => `Corrective recovery candidate is invalid: ${message}`),
|
|
2193
|
+
];
|
|
2194
|
+
return {
|
|
2195
|
+
eligible: blockers.length === 0 && candidate !== null,
|
|
2196
|
+
mutationFree: true,
|
|
2197
|
+
runnerPackageVersion: PACKAGE_VERSION,
|
|
2198
|
+
requiredProjectPackageVersion: '2.0.0-alpha.6',
|
|
2199
|
+
declaredProjectPackageVersion: dependency.declared,
|
|
2200
|
+
requiredHead: this.alpha6CorrectiveRescueHead,
|
|
2201
|
+
currentHead,
|
|
2202
|
+
clean: dirty.length === 0,
|
|
2203
|
+
candidate,
|
|
2204
|
+
blockers,
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
alpha6CorrectiveRescueApply(repository, taskId, expectedRevision, actor, writerToken, plan, decisionAudit, correctivePlanAudit) {
|
|
2208
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
2209
|
+
const locks = new WriterLockManager(this.store.lockRoot(identity.projectId), this.now);
|
|
2210
|
+
const task = this.store.readTask(identity.projectId, taskId);
|
|
2211
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2212
|
+
const posture = readTaskC1Posture(this.store, task);
|
|
2213
|
+
const lease = locks.inspect(taskId);
|
|
2214
|
+
// Authentication is deliberately completed before heartbeat. A wrong actor/token must
|
|
2215
|
+
// not refresh a stale lease or create any observable recovery mutation.
|
|
2216
|
+
if (posture.state !== 'claimed'
|
|
2217
|
+
|| actor.trim() !== posture.claimant
|
|
2218
|
+
|| !lease
|
|
2219
|
+
|| lease.owner !== posture.claimant
|
|
2220
|
+
|| lease.token !== writerToken
|
|
2221
|
+
|| !posture.writerLeaseTokenHash
|
|
2222
|
+
|| sha256Hex(writerToken) !== posture.writerLeaseTokenHash) {
|
|
2223
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective rescue actor/token does not match the claimed C1 lease binding.', {
|
|
2224
|
+
taskId,
|
|
2225
|
+
actor: actor.trim(),
|
|
2226
|
+
claimant: posture.state === 'claimed' ? posture.claimant : null,
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
const preflight = this.alpha6CorrectiveRescuePreflight(repository);
|
|
2230
|
+
if (!preflight.eligible || preflight.candidate?.taskId !== taskId || expectedRevision !== 14) {
|
|
2231
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective rescue preflight is not eligible for the requested Task.', {
|
|
2232
|
+
taskId,
|
|
2233
|
+
expectedRevision,
|
|
2234
|
+
blockers: preflight.blockers,
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
validatePlan(plan);
|
|
2238
|
+
const knowledgeMap = this.assertKnowledgeMapBinding(identity.repositoryRoot, identity.projectId, plan.knowledgeMapRevision, plan.knowledgeMapHash);
|
|
2239
|
+
this.validateKnowledgeTargets(knowledgeMap, plan);
|
|
2240
|
+
if (!sameOrderedStrings(plan.requirements, task.requirements)
|
|
2241
|
+
|| !sameOrderedStrings(plan.acceptance, task.acceptance)) {
|
|
2242
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective rescue Plan must preserve requirements and acceptance exactly.');
|
|
2243
|
+
}
|
|
2244
|
+
assertNonTargetStepsUnchanged(task, plan, 'STEP-001');
|
|
2245
|
+
if (decisionAudit.decision !== 'continue-fix') {
|
|
2246
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective rescue only supports an independent continue-fix decision.');
|
|
2247
|
+
}
|
|
2248
|
+
assertCorrectiveAuditorIndependence(this.store, task, 'STEP-001', decisionAudit.auditor);
|
|
2249
|
+
if (correctivePlanAudit.auditor.trim() === decisionAudit.auditor.trim()) {
|
|
2250
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective rescue Plan auditor must be independent from the decision auditor.');
|
|
2251
|
+
}
|
|
2252
|
+
if (!decisionAudit.auditor.trim()
|
|
2253
|
+
|| !decisionAudit.summary.trim()
|
|
2254
|
+
|| !correctivePlanAudit.auditor.trim()
|
|
2255
|
+
|| !correctivePlanAudit.summary.trim()
|
|
2256
|
+
|| correctivePlanAudit.decision !== 'continue-fix'
|
|
2257
|
+
|| decisionAudit.reviewedFindingIds.length === 0
|
|
2258
|
+
|| correctivePlanAudit.reviewedFindingIds.length === 0) {
|
|
2259
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective rescue audits must independently cover the exact strict-review finding.');
|
|
2260
|
+
}
|
|
2261
|
+
assertCorrectiveAuditorIndependence(this.store, task, 'STEP-001', correctivePlanAudit.auditor);
|
|
2262
|
+
const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
2263
|
+
if (!currentPlanRiskAudit) {
|
|
2264
|
+
throw new WorkflowError('STATE_CORRUPT', 'Corrective rescue requires the exact current Plan Risk Audit.');
|
|
2265
|
+
}
|
|
2266
|
+
const stagedPlan = this.store.stageArtifact(this.store.taskRoot(task.projectId, task.id), 'plan.md', renderPlan(task, plan));
|
|
2267
|
+
if (stagedPlan.hash === task.planHash) {
|
|
2268
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective rescue requires a changed full corrective Plan.');
|
|
2269
|
+
}
|
|
2270
|
+
const completedIds = task.steps
|
|
2271
|
+
.filter((candidate) => candidate.status === 'completed')
|
|
2272
|
+
.map((candidate) => candidate.id);
|
|
2273
|
+
const previewSteps = plan.steps.map((step) => preserveCompletedStep(task, step) ?? ({
|
|
2274
|
+
...step,
|
|
2275
|
+
status: dependenciesComplete(step, completedIds) ? 'planned' : 'blocked',
|
|
2276
|
+
evidence: null,
|
|
2277
|
+
}));
|
|
2278
|
+
validatePlanRiskAuditCandidate(this.store, {
|
|
2279
|
+
...task,
|
|
2280
|
+
status: 'awaiting_execution_authorization',
|
|
2281
|
+
planHash: stagedPlan.hash,
|
|
2282
|
+
planObjective: plan.objective,
|
|
2283
|
+
risks: plan.risks,
|
|
2284
|
+
knowledgeImpact: plan.knowledgeImpact,
|
|
2285
|
+
knowledgeImpactReason: plan.knowledgeImpactReason,
|
|
2286
|
+
knowledgeTargets: plan.knowledgeTargets,
|
|
2287
|
+
knowledgeMapRevision: plan.knowledgeMapRevision,
|
|
2288
|
+
knowledgeMapHash: plan.knowledgeMapHash,
|
|
2289
|
+
graphUse: plan.graphUse,
|
|
2290
|
+
steps: previewSteps,
|
|
2291
|
+
}, currentPlanRiskAudit.audit);
|
|
2292
|
+
const milestoneScopeBefore = task.milestoneId
|
|
2293
|
+
? milestoneScopeDigest(this.store.readMilestone(task.projectId, task.milestoneId))
|
|
2294
|
+
: null;
|
|
2295
|
+
const refreshedLease = locks.heartbeat(taskId, writerToken);
|
|
2296
|
+
const decision = appendCorrectiveDecisionEvent(this.store, task, 'STEP-001', decisionAudit, currentPlanRiskAudit, task.planHash, this.now());
|
|
2297
|
+
validateGuardedCorrectivePlanAudit(this.store, task, 'STEP-001', decision, correctivePlanAudit);
|
|
2298
|
+
const saved = this.setTaskPlan(repository, taskId, expectedRevision, plan, correctivePlanAudit, null);
|
|
2299
|
+
if (this.store.hashArtifact(this.store.taskRoot(task.projectId, task.id), 'brief.md') !== preflight.candidate.briefHash) {
|
|
2300
|
+
throw new WorkflowError('STATE_CORRUPT', 'Corrective rescue changed the immutable Brief.');
|
|
2301
|
+
}
|
|
2302
|
+
const milestoneScopeAfter = task.milestoneId
|
|
2303
|
+
? milestoneScopeDigest(this.store.readMilestone(task.projectId, task.milestoneId))
|
|
2304
|
+
: null;
|
|
2305
|
+
if (milestoneScopeAfter !== milestoneScopeBefore) {
|
|
2306
|
+
throw new WorkflowError('STATE_CORRUPT', 'Corrective rescue changed Milestone scope.');
|
|
2307
|
+
}
|
|
2308
|
+
const { token: _secret, ...publicLease } = refreshedLease;
|
|
2309
|
+
return { task: saved, decision, lease: { ...publicLease, tokenRetained: true } };
|
|
2310
|
+
}
|
|
2009
2311
|
scanKnowledgeMap(repository) {
|
|
2010
2312
|
const { identity } = this.context(repository);
|
|
2011
2313
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
@@ -2293,6 +2595,25 @@ export class WorkflowService {
|
|
|
2293
2595
|
? this.delegatedApprovalOptions(projectId, transition, task)
|
|
2294
2596
|
: [];
|
|
2295
2597
|
const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(projectId, task.id));
|
|
2598
|
+
const guardedFailedSteps = planRiskAudit
|
|
2599
|
+
? task.steps.filter((step) => step.status === 'failed' && planRiskAudit.reviewRequiredStepIds.includes(step.id))
|
|
2600
|
+
: [];
|
|
2601
|
+
const breakerSteps = guardedFailedSteps.map((step) => ({
|
|
2602
|
+
step,
|
|
2603
|
+
attemptCount: readRemediationEvents(this.store, task).filter((event) => event.stepId === step.id).length,
|
|
2604
|
+
})).filter((entry) => entry.attemptCount >= 2);
|
|
2605
|
+
if (breakerSteps.length > 1) {
|
|
2606
|
+
throw new WorkflowError('STATE_CORRUPT', 'Multiple guarded failed Steps simultaneously carry remediation-breaker posture.', {
|
|
2607
|
+
taskId: task.id,
|
|
2608
|
+
stepIds: breakerSteps.map((entry) => entry.step.id),
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
const breakerStep = breakerSteps[0] ?? null;
|
|
2612
|
+
const guardedCorrectiveGate = breakerStep && breakerStep.attemptCount === 2 && planRiskAudit
|
|
2613
|
+
? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, planRiskAudit)
|
|
2614
|
+
: null;
|
|
2615
|
+
const correctivePlanRequired = guardedCorrectiveGate?.correctiveDecision?.decision === 'continue-fix'
|
|
2616
|
+
&& !hasCorrectivePlanAuditBinding(this.store, task, guardedCorrectiveGate.correctiveDecision);
|
|
2296
2617
|
const next = nextForTask(task);
|
|
2297
2618
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
2298
2619
|
const strictStepReviewStep = planRiskAudit
|
|
@@ -2365,7 +2686,53 @@ export class WorkflowService {
|
|
|
2365
2686
|
},
|
|
2366
2687
|
}
|
|
2367
2688
|
: {}),
|
|
2368
|
-
...(
|
|
2689
|
+
...(guardedCorrectiveGate
|
|
2690
|
+
? guardedCorrectiveGate.correctiveDecision === null
|
|
2691
|
+
? {
|
|
2692
|
+
action: 'task corrective-decision',
|
|
2693
|
+
stepId: guardedCorrectiveGate.stepId,
|
|
2694
|
+
correctiveDecisionGate: {
|
|
2695
|
+
attemptCount: guardedCorrectiveGate.attemptCount,
|
|
2696
|
+
auditorMustDifferFromLatestStrictReviewer: true,
|
|
2697
|
+
allowedDecision: 'continue-fix',
|
|
2698
|
+
escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
|
|
2699
|
+
humanApprovalRequired: false,
|
|
2700
|
+
},
|
|
2701
|
+
}
|
|
2702
|
+
: correctivePlanRequired
|
|
2703
|
+
? {
|
|
2704
|
+
action: 'task plan-set with independent corrective audit',
|
|
2705
|
+
stepId: guardedCorrectiveGate.stepId,
|
|
2706
|
+
correctivePlanGate: {
|
|
2707
|
+
attemptCount: guardedCorrectiveGate.attemptCount,
|
|
2708
|
+
decisionEventId: guardedCorrectiveGate.correctiveDecision.eventId,
|
|
2709
|
+
fullPlanRequired: true,
|
|
2710
|
+
auditFileRequired: true,
|
|
2711
|
+
auditorMustDifferFromDecisionAuditor: true,
|
|
2712
|
+
auditorMustDifferFromLatestStrictReviewer: true,
|
|
2713
|
+
allowedDecision: 'continue-fix',
|
|
2714
|
+
},
|
|
2715
|
+
}
|
|
2716
|
+
: guardedCorrectiveGate.correctiveDecision.decision !== 'continue-fix'
|
|
2717
|
+
? {
|
|
2718
|
+
action: 'task corrective-hard-stop',
|
|
2719
|
+
stepId: guardedCorrectiveGate.stepId,
|
|
2720
|
+
correctiveDecision: guardedCorrectiveGate.correctiveDecision.decision,
|
|
2721
|
+
}
|
|
2722
|
+
: {}
|
|
2723
|
+
: {}),
|
|
2724
|
+
...(breakerStep?.attemptCount && breakerStep.attemptCount >= 3
|
|
2725
|
+
? {
|
|
2726
|
+
action: 'task corrective-hard-stop',
|
|
2727
|
+
stepId: breakerStep.step.id,
|
|
2728
|
+
correctiveHardStop: {
|
|
2729
|
+
failedAttemptCount: breakerStep.attemptCount,
|
|
2730
|
+
continueFixAllowed: false,
|
|
2731
|
+
allowedDecisions: ['split-required', 'stop-escalate'],
|
|
2732
|
+
},
|
|
2733
|
+
}
|
|
2734
|
+
: {}),
|
|
2735
|
+
...(delegatedApprovalOptions.length > 0 && !breakerStep ? { delegatedApprovalOptions } : {}),
|
|
2369
2736
|
};
|
|
2370
2737
|
if (handoffPosture.state === 'pending') {
|
|
2371
2738
|
return {
|
|
@@ -2881,6 +3248,67 @@ function validateCorrectivePlanAudit(task, audit, failedReviewAttempts) {
|
|
|
2881
3248
|
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective Auditor decision ${audit.decision} requires coordinator/user redirection before implementation continues.`, { decision: audit.decision, summary: audit.summary });
|
|
2882
3249
|
}
|
|
2883
3250
|
}
|
|
3251
|
+
function validateGuardedCorrectivePlanAudit(store, task, stepId, decision, audit) {
|
|
3252
|
+
if (!audit) {
|
|
3253
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${stepId} requires a full corrective Plan with an independent corrective audit.`, {
|
|
3254
|
+
taskId: task.id,
|
|
3255
|
+
stepId,
|
|
3256
|
+
decisionEventId: decision.eventId,
|
|
3257
|
+
requiredAction: 'Pass the full Plan and --corrective-audit-file to task plan-set.',
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
if (audit.decision !== 'continue-fix' || !audit.auditor.trim() || !audit.summary.trim()) {
|
|
3261
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit must independently approve continue-fix with auditor and summary.');
|
|
3262
|
+
}
|
|
3263
|
+
if (audit.auditor.trim() === decision.auditor.trim()) {
|
|
3264
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan auditor must be distinct from the corrective-decision auditor.', {
|
|
3265
|
+
taskId: task.id,
|
|
3266
|
+
stepId,
|
|
3267
|
+
decisionAuditor: decision.auditor,
|
|
3268
|
+
planAuditor: audit.auditor.trim(),
|
|
3269
|
+
});
|
|
3270
|
+
}
|
|
3271
|
+
assertCorrectiveAuditorIndependence(store, task, stepId, audit.auditor);
|
|
3272
|
+
const reviewed = new Set(audit.reviewedFindingIds);
|
|
3273
|
+
if (reviewed.size !== audit.reviewedFindingIds.length || audit.reviewedFindingIds.some((id) => !id.trim())) {
|
|
3274
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit finding IDs must be non-empty and unique.');
|
|
3275
|
+
}
|
|
3276
|
+
const missing = decision.coveredFindingIds.filter((id) => !reviewed.has(id));
|
|
3277
|
+
if (missing.length > 0) {
|
|
3278
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan audit does not cover every finding in the corrective decision.', {
|
|
3279
|
+
taskId: task.id,
|
|
3280
|
+
stepId,
|
|
3281
|
+
missingFindingIds: missing,
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
function sameOrderedStrings(left, right) {
|
|
3286
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
3287
|
+
}
|
|
3288
|
+
function assertNonTargetStepsUnchanged(task, plan, targetStepId) {
|
|
3289
|
+
const stripRuntime = (step) => stripStepRuntime(step);
|
|
3290
|
+
const previous = task.steps.filter((step) => step.id !== targetStepId).map(stripRuntime);
|
|
3291
|
+
const replacement = plan.steps.filter((step) => step.id !== targetStepId);
|
|
3292
|
+
if (JSON.stringify(previous) !== JSON.stringify(replacement)) {
|
|
3293
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan must preserve every non-target Step exactly.', {
|
|
3294
|
+
taskId: task.id,
|
|
3295
|
+
targetStepId,
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
function milestoneScopeDigest(milestone) {
|
|
3300
|
+
return sha256Hex(canonicalJsonStringify({
|
|
3301
|
+
id: milestone.id,
|
|
3302
|
+
outcome: milestone.outcome,
|
|
3303
|
+
successSignal: milestone.successSignal,
|
|
3304
|
+
acceptance: milestone.acceptance,
|
|
3305
|
+
checks: milestone.checks,
|
|
3306
|
+
taskIds: milestone.taskIds,
|
|
3307
|
+
memberships: milestone.memberships,
|
|
3308
|
+
membershipRevision: milestone.membershipRevision,
|
|
3309
|
+
planHash: milestone.planHash,
|
|
3310
|
+
}));
|
|
3311
|
+
}
|
|
2884
3312
|
function appendCorrectivePlanAudit(taskRoot, previous, saved, failedReviewAttempts, audit, now) {
|
|
2885
3313
|
appendFileSync(path.join(taskRoot, 'corrective-plan-audits.jsonl'), `${JSON.stringify({
|
|
2886
3314
|
recordedAt: now.toISOString(),
|