codex-workflow-v2 2.0.0-alpha.6 → 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 +57 -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 +15 -1
- 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/version.js.map +1 -1
- package/dist/src/workflow.d.ts +84 -4
- package/dist/src/workflow.js +677 -66
- package/dist/src/workflow.js.map +1 -1
- package/docs/decisions.md +4 -0
- package/docs/release.md +4 -0
- package/docs/updating-existing-project.md +40 -0
- package/docs/validation-report.md +25 -7
- package/package.json +2 -2
- package/plugins/codex-workflow-gateway/references/protocol.md +5 -0
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +7 -0
- package/schemas/context-envelope.schema.json +10 -0
- package/schemas/corrective-plan-audit-record.schema.json +47 -0
package/dist/src/workflow.js
CHANGED
|
@@ -7,13 +7,14 @@ import { assertDelegationCanAuthorize, delegationCanAuthorize, hashDelegationPol
|
|
|
7
7
|
import { assessDiscovery, mergeUnique } from './domain/discovery.js';
|
|
8
8
|
import { pathAllowed, validatePlan } from './domain/validation.js';
|
|
9
9
|
import { WorkflowError } from './errors.js';
|
|
10
|
+
import { inspectLocalDependency } from './diagnostics.js';
|
|
10
11
|
import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
|
|
11
12
|
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
12
13
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
13
14
|
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
14
|
-
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';
|
|
15
16
|
import { applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
16
|
-
import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
|
|
17
|
+
import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, readReviewerAttestations, } from './alpha6/review.js';
|
|
17
18
|
import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
|
|
18
19
|
import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
|
|
19
20
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
@@ -23,12 +24,15 @@ import { WriterLockManager } from './state/lock.js';
|
|
|
23
24
|
import { FileStateStore } from './state/store.js';
|
|
24
25
|
import { createEntityId } from './ulid.js';
|
|
25
26
|
import { PACKAGE_VERSION } from './version.js';
|
|
27
|
+
const ALPHA6_CORRECTIVE_RESCUE_HEAD = '5c63798cc7b8fbb5e1c8b26ebf93876bc642295a';
|
|
26
28
|
export class WorkflowService {
|
|
27
29
|
store;
|
|
28
30
|
now;
|
|
29
|
-
|
|
31
|
+
alpha6CorrectiveRescueHead;
|
|
32
|
+
constructor(store = new FileStateStore(), now = () => new Date(), alpha6CorrectiveRescueHead = ALPHA6_CORRECTIVE_RESCUE_HEAD) {
|
|
30
33
|
this.store = store;
|
|
31
34
|
this.now = now;
|
|
35
|
+
this.alpha6CorrectiveRescueHead = alpha6CorrectiveRescueHead;
|
|
32
36
|
}
|
|
33
37
|
context(repository) {
|
|
34
38
|
const identity = resolveRepositoryIdentity(repository);
|
|
@@ -564,14 +568,13 @@ export class WorkflowService {
|
|
|
564
568
|
this.validateKnowledgeTargets(knowledgeMap, plan);
|
|
565
569
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
566
570
|
assertExpectedRevision(task, expectedRevision);
|
|
567
|
-
if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
|
|
571
|
+
if (!['planning', 'awaiting_execution_authorization', 'ready', 'needs_fix', 'blocked'].includes(task.status)) {
|
|
568
572
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
|
|
569
573
|
}
|
|
570
574
|
const currentPlanCorrectiveDecisions = task.planHash
|
|
571
575
|
? readCorrectiveDecisionEvents(this.store, task).filter((event) => event.planHash === task.planHash)
|
|
572
576
|
: [];
|
|
573
577
|
const currentReplanDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'replan-required');
|
|
574
|
-
const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'continue-fix');
|
|
575
578
|
const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required' || event.decision === 'stop-escalate');
|
|
576
579
|
const currentReplanRequiredStepIds = [...new Set(currentReplanDecisions.map((event) => event.stepId))];
|
|
577
580
|
if (currentHardBlockingDecisions.length > 0) {
|
|
@@ -592,33 +595,22 @@ export class WorkflowService {
|
|
|
592
595
|
requiredAction: 'Resolve the blocked strict review or record a replan-required corrective decision first.',
|
|
593
596
|
});
|
|
594
597
|
}
|
|
595
|
-
|
|
596
|
-
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.`, {
|
|
597
|
-
taskId: task.id,
|
|
598
|
-
requiredAction: 'Run the guarded Step under the current Plan retry path, or escalate/cancel instead of issuing another plan-set at the same ordinal.',
|
|
599
|
-
decisions: currentContinueFixDecisions.map((event) => ({
|
|
600
|
-
eventId: event.eventId,
|
|
601
|
-
stepId: event.stepId,
|
|
602
|
-
triggeringAttemptCount: event.triggeringAttemptCount,
|
|
603
|
-
})),
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
const currentPlanRiskAudit = task.status === 'needs_fix'
|
|
607
|
-
? readCurrentPlanRiskAudit(this.store, task)
|
|
608
|
-
: null;
|
|
598
|
+
const currentPlanRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
|
|
609
599
|
const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
|
|
610
600
|
if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
|
|
611
601
|
validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
|
|
612
602
|
}
|
|
613
|
-
const guardedRemediationGate =
|
|
603
|
+
const guardedRemediationGate = ['needs_fix', 'awaiting_execution_authorization', 'ready'].includes(task.status)
|
|
604
|
+
&& currentPlanRiskAudit
|
|
614
605
|
? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
|
|
615
606
|
: null;
|
|
616
|
-
if (guardedRemediationGate &&
|
|
617
|
-
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.`, {
|
|
618
609
|
taskId: task.id,
|
|
619
610
|
stepId: guardedRemediationGate.stepId,
|
|
620
611
|
attemptCount: guardedRemediationGate.attemptCount,
|
|
621
612
|
requiredSidecar: 'corrective-decisions.jsonl',
|
|
613
|
+
requiredAction: 'Run task corrective-decision first.',
|
|
622
614
|
allowedDecision: 'continue-fix',
|
|
623
615
|
escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
|
|
624
616
|
});
|
|
@@ -630,25 +622,57 @@ export class WorkflowService {
|
|
|
630
622
|
requiredAction: 'Retry task plan-set without corrective-audit input; the recorded replan-required decision is the authorization for this recovery path.',
|
|
631
623
|
});
|
|
632
624
|
}
|
|
633
|
-
|
|
634
|
-
|
|
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.`, {
|
|
635
633
|
taskId: task.id,
|
|
636
634
|
stepId: guardedRemediationGate.stepId,
|
|
637
|
-
|
|
635
|
+
decisionEventId: guardedRemediationGate.correctiveDecision.eventId,
|
|
638
636
|
});
|
|
639
637
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
+
});
|
|
643
653
|
}
|
|
644
654
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
+
}
|
|
648
665
|
}
|
|
649
666
|
}
|
|
650
667
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
651
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
|
+
}
|
|
652
676
|
const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
653
677
|
? { ...authorization, decision: 'superseded' }
|
|
654
678
|
: authorization);
|
|
@@ -678,9 +702,8 @@ export class WorkflowService {
|
|
|
678
702
|
stepId: guardedRemediationGate.stepId,
|
|
679
703
|
});
|
|
680
704
|
}
|
|
681
|
-
if (
|
|
682
|
-
|
|
683
|
-
appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, stagedPlan.hash, this.now());
|
|
705
|
+
if (guardedContinueFixDecision) {
|
|
706
|
+
assertNonTargetStepsUnchanged(task, plan, guardedRemediationGate.stepId);
|
|
684
707
|
}
|
|
685
708
|
const candidate = {
|
|
686
709
|
...task,
|
|
@@ -703,12 +726,16 @@ export class WorkflowService {
|
|
|
703
726
|
evidenceHash: null,
|
|
704
727
|
blockReason: null,
|
|
705
728
|
};
|
|
706
|
-
|
|
707
|
-
|
|
729
|
+
const effectivePlanRiskAudit = planRiskAudit ?? (guardedContinueFixDecision ? currentPlanRiskAudit?.audit ?? null : null);
|
|
730
|
+
if (effectivePlanRiskAudit)
|
|
731
|
+
validatePlanRiskAuditCandidate(this.store, candidate, effectivePlanRiskAudit);
|
|
708
732
|
const saved = this.store.writeTask(candidate, expectedRevision);
|
|
709
733
|
this.store.publishArtifact(stagedPlan);
|
|
710
|
-
if (
|
|
711
|
-
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
|
+
}
|
|
712
739
|
if (correctiveAudit && task.status === 'needs_fix' && task.review.status === 'failed') {
|
|
713
740
|
appendCorrectivePlanAudit(this.store.taskRoot(identity.projectId, taskId), task, saved, failedReviewAttempts, correctiveAudit, this.now());
|
|
714
741
|
}
|
|
@@ -734,6 +761,11 @@ export class WorkflowService {
|
|
|
734
761
|
if (activeStep) {
|
|
735
762
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot rebind Task Plan knowledge while ${activeStep.id} is in progress.`);
|
|
736
763
|
}
|
|
764
|
+
const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
765
|
+
const breakerPlanRiskAudit = currentPlanRiskAudit
|
|
766
|
+
&& findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
|
|
767
|
+
? currentPlanRiskAudit
|
|
768
|
+
: null;
|
|
737
769
|
const knowledgeMap = this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
738
770
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
739
771
|
if (task.knowledgeMapRevision === knowledgeMap.revision &&
|
|
@@ -793,6 +825,9 @@ export class WorkflowService {
|
|
|
793
825
|
blockReason: null,
|
|
794
826
|
}, expectedRevision);
|
|
795
827
|
this.store.publishArtifact(stagedPlan);
|
|
828
|
+
if (breakerPlanRiskAudit) {
|
|
829
|
+
appendReboundPlanRiskAudit(this.store, task, saved, breakerPlanRiskAudit, this.now());
|
|
830
|
+
}
|
|
796
831
|
return saved;
|
|
797
832
|
}
|
|
798
833
|
refreshTaskContext(repository, taskId, expectedTaskRevision, expectedKnowledgeMapRevision, actor, delegationGrantId) {
|
|
@@ -814,6 +849,8 @@ export class WorkflowService {
|
|
|
814
849
|
const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
|
|
815
850
|
const transitions = [];
|
|
816
851
|
const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
852
|
+
const breakerPlanRiskAuditWillBePreserved = currentPlanRiskAudit
|
|
853
|
+
&& findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit) !== null;
|
|
817
854
|
// Validate every delegated transition before the first write so a narrow grant cannot
|
|
818
855
|
// leave the composite operation half-applied.
|
|
819
856
|
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
@@ -835,7 +872,7 @@ export class WorkflowService {
|
|
|
835
872
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
|
|
836
873
|
}
|
|
837
874
|
let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
|
|
838
|
-
if (currentPlanRiskAudit) {
|
|
875
|
+
if (currentPlanRiskAudit && !breakerPlanRiskAuditWillBePreserved) {
|
|
839
876
|
appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
|
|
840
877
|
}
|
|
841
878
|
transitions.push('task knowledge-rebind');
|
|
@@ -1117,9 +1154,9 @@ export class WorkflowService {
|
|
|
1117
1154
|
currentAuditPresent: Boolean(planRiskAudit),
|
|
1118
1155
|
});
|
|
1119
1156
|
}
|
|
1120
|
-
|
|
1121
|
-
assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit)
|
|
1122
|
-
|
|
1157
|
+
const remediationGate = planRiskAudit && isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)
|
|
1158
|
+
? assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit)
|
|
1159
|
+
: null;
|
|
1123
1160
|
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1124
1161
|
if (step.status === 'planned' && dirty.length > 0) {
|
|
1125
1162
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
@@ -1222,17 +1259,31 @@ export class WorkflowService {
|
|
|
1222
1259
|
stepId,
|
|
1223
1260
|
expectedRevision: saved.revision,
|
|
1224
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
|
+
: {}),
|
|
1225
1271
|
knowledgeMap: knowledgeSummary(knowledgeMap),
|
|
1226
1272
|
knowledgeSources: selectKnowledgeSources(knowledgeMap, 'worker', task.knowledgeTargets.map((target) => target.path)),
|
|
1227
1273
|
},
|
|
1228
1274
|
};
|
|
1229
1275
|
}
|
|
1230
|
-
taskContext(repository, taskId, role) {
|
|
1276
|
+
taskContext(repository, taskId, role, allowHistoricalStrictReview = false) {
|
|
1231
1277
|
const { identity } = this.context(repository);
|
|
1232
1278
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1279
|
+
const historicalStrictReview = allowHistoricalStrictReview
|
|
1280
|
+
&& role === 'independent-reviewer'
|
|
1281
|
+
&& task.steps.some((step) => step.status === 'in_progress' && step.evidence !== null);
|
|
1233
1282
|
const knowledgeMap = task.knowledgeMapRevision === null
|
|
1234
1283
|
? this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId)
|
|
1235
|
-
:
|
|
1284
|
+
: historicalStrictReview
|
|
1285
|
+
? this.strictReviewKnowledgeMap(identity.repositoryRoot, task)
|
|
1286
|
+
: this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
|
|
1236
1287
|
const root = this.store.taskRoot(task.projectId, task.id);
|
|
1237
1288
|
const planRiskAudit = task.planHash && !['planning', 'merged', 'cancelled'].includes(task.status)
|
|
1238
1289
|
? readCurrentPlanRiskAudit(this.store, task)
|
|
@@ -1387,7 +1438,161 @@ export class WorkflowService {
|
|
|
1387
1438
|
});
|
|
1388
1439
|
return this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
|
|
1389
1440
|
}
|
|
1441
|
+
alpha6C1StrictReviewRescuePreflight(repository) {
|
|
1442
|
+
const context = this.context(repository);
|
|
1443
|
+
const dependency = inspectLocalDependency(context.identity.repositoryRoot);
|
|
1444
|
+
const tasks = context.store.listTasks(context.identity.projectId);
|
|
1445
|
+
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1446
|
+
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
1447
|
+
const now = this.now().getTime();
|
|
1448
|
+
const candidates = [];
|
|
1449
|
+
const malformedCandidates = [];
|
|
1450
|
+
for (const task of tasks) {
|
|
1451
|
+
if (task.status !== 'in_progress')
|
|
1452
|
+
continue;
|
|
1453
|
+
const planRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
|
|
1454
|
+
if (!planRiskAudit)
|
|
1455
|
+
continue;
|
|
1456
|
+
for (const step of task.steps) {
|
|
1457
|
+
if (step.status !== 'in_progress' || !step.evidence)
|
|
1458
|
+
continue;
|
|
1459
|
+
if (!isStepReviewRequired(task, step.id, planRiskAudit.reviewRequiredStepIds))
|
|
1460
|
+
continue;
|
|
1461
|
+
try {
|
|
1462
|
+
const cycle = buildCurrentStrictStepReviewCycle(this.store, task, step.id);
|
|
1463
|
+
if (cycle.resolution !== 'pending')
|
|
1464
|
+
continue;
|
|
1465
|
+
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
1466
|
+
const storedKnowledgeMap = this.store.readKnowledgeMap(context.identity.projectId);
|
|
1467
|
+
const knowledgeMap = inspectKnowledgeMap(storedKnowledgeMap, scanProjectKnowledge(context.identity.repositoryRoot, context.identity.projectId, this.now()));
|
|
1468
|
+
const knowledgeBindingStale = knowledgeMap.status !== 'active'
|
|
1469
|
+
|| task.knowledgeMapRevision !== knowledgeMap.revision
|
|
1470
|
+
|| task.knowledgeMapHash !== hashKnowledgeMap(knowledgeMap);
|
|
1471
|
+
const posture = readTaskC1Posture(this.store, task);
|
|
1472
|
+
const lease = context.locks.inspect(task.id);
|
|
1473
|
+
const claimant = posture.state === 'claimed' ? posture.claimant : null;
|
|
1474
|
+
const tokenBound = posture.state === 'claimed'
|
|
1475
|
+
&& lease !== null
|
|
1476
|
+
&& posture.writerLeaseTokenHash !== null
|
|
1477
|
+
&& posture.writerLeaseTokenHash === sha256Hex(lease.token);
|
|
1478
|
+
candidates.push({
|
|
1479
|
+
taskId: task.id,
|
|
1480
|
+
taskRevision: task.revision,
|
|
1481
|
+
stepId: step.id,
|
|
1482
|
+
completionCommit: step.evidence.commitSha,
|
|
1483
|
+
currentHead,
|
|
1484
|
+
knowledgeBindingStale,
|
|
1485
|
+
claimant,
|
|
1486
|
+
handoffExpiresAt: posture.state === 'claimed' ? posture.latestEvent.expiresAt : null,
|
|
1487
|
+
lease: lease
|
|
1488
|
+
? {
|
|
1489
|
+
entityId: lease.entityId,
|
|
1490
|
+
owner: lease.owner,
|
|
1491
|
+
pid: lease.pid,
|
|
1492
|
+
hostname: lease.hostname,
|
|
1493
|
+
acquiredAt: lease.acquiredAt,
|
|
1494
|
+
heartbeatAt: lease.heartbeatAt,
|
|
1495
|
+
expiresAt: lease.expiresAt,
|
|
1496
|
+
stale: Date.parse(lease.expiresAt) <= now,
|
|
1497
|
+
tokenBound,
|
|
1498
|
+
}
|
|
1499
|
+
: null,
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
catch (error) {
|
|
1503
|
+
malformedCandidates.push(error instanceof Error ? error.message : String(error));
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
const candidate = candidates.length === 1 ? candidates[0] : null;
|
|
1508
|
+
const unrelatedLeases = context.locks.list().filter((lease) => lease.entityId !== candidate?.taskId);
|
|
1509
|
+
const blockers = [
|
|
1510
|
+
...(dependency.declared !== '2.0.0-alpha.6'
|
|
1511
|
+
? ['Project package.json must declare codex-workflow-v2 exactly at 2.0.0-alpha.6.']
|
|
1512
|
+
: []),
|
|
1513
|
+
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
1514
|
+
...(candidates.length !== 1
|
|
1515
|
+
? [`Expected exactly one pending alpha.6 strict review; found ${candidates.length}.`]
|
|
1516
|
+
: []),
|
|
1517
|
+
...(candidate && candidate.currentHead !== candidate.completionCommit
|
|
1518
|
+
? ['HEAD does not match the pending strict review completion commit.']
|
|
1519
|
+
: []),
|
|
1520
|
+
...(candidate && !candidate.knowledgeBindingStale
|
|
1521
|
+
? ['The pending strict review is not blocked by a stale Project Knowledge Map.']
|
|
1522
|
+
: []),
|
|
1523
|
+
...(candidate && candidate.claimant === null
|
|
1524
|
+
? ['The rescue Task does not have an active claimed C1 posture.']
|
|
1525
|
+
: []),
|
|
1526
|
+
...(candidate && candidate.lease === null
|
|
1527
|
+
? ['The claimed C1 rescue Task has no bound writer lease.']
|
|
1528
|
+
: []),
|
|
1529
|
+
...(candidate?.lease && candidate.claimant !== candidate.lease.owner
|
|
1530
|
+
? ['The writer lease owner does not match the active C1 claimant.']
|
|
1531
|
+
: []),
|
|
1532
|
+
...(candidate?.lease && !candidate.lease.tokenBound
|
|
1533
|
+
? ['The writer lease token does not match the active C1 binding.']
|
|
1534
|
+
: []),
|
|
1535
|
+
...(unrelatedLeases.length > 0
|
|
1536
|
+
? ['A writer lease exists outside the exact rescue Task.']
|
|
1537
|
+
: []),
|
|
1538
|
+
...malformedCandidates.map((message) => `Pending strict review state is invalid: ${message}`),
|
|
1539
|
+
];
|
|
1540
|
+
const eligible = blockers.length === 0 && candidate !== null && candidate.claimant !== null;
|
|
1541
|
+
return {
|
|
1542
|
+
eligible,
|
|
1543
|
+
mutationFree: true,
|
|
1544
|
+
runnerPackageVersion: PACKAGE_VERSION,
|
|
1545
|
+
requiredProjectPackageVersion: '2.0.0-alpha.6',
|
|
1546
|
+
declaredProjectPackageVersion: dependency.declared,
|
|
1547
|
+
projectId: context.identity.projectId,
|
|
1548
|
+
repositoryRoot: context.identity.repositoryRoot,
|
|
1549
|
+
clean: dirty.length === 0,
|
|
1550
|
+
candidate,
|
|
1551
|
+
blockers,
|
|
1552
|
+
requiredAction: eligible && candidate && candidate.claimant
|
|
1553
|
+
? {
|
|
1554
|
+
action: 'update rescue-review',
|
|
1555
|
+
taskId: candidate.taskId,
|
|
1556
|
+
stepId: candidate.stepId,
|
|
1557
|
+
expectedRevision: candidate.taskRevision,
|
|
1558
|
+
actor: candidate.claimant,
|
|
1559
|
+
requiresBoundWriterToken: true,
|
|
1560
|
+
}
|
|
1561
|
+
: null,
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
alpha6C1StrictReviewRescue(repository, taskId, stepId, expectedRevision, actor, writerToken, runner) {
|
|
1565
|
+
const preflight = this.alpha6C1StrictReviewRescuePreflight(repository);
|
|
1566
|
+
if (!preflight.eligible || !preflight.candidate || !preflight.requiredAction) {
|
|
1567
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Alpha.6 C1 strict-review rescue preflight is not eligible.', {
|
|
1568
|
+
blockers: preflight.blockers,
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
const candidate = preflight.candidate;
|
|
1572
|
+
if (candidate.taskId !== taskId
|
|
1573
|
+
|| candidate.stepId !== stepId
|
|
1574
|
+
|| candidate.taskRevision !== expectedRevision) {
|
|
1575
|
+
throw new WorkflowError('STATE_CONFLICT', 'Rescue command does not match the exact preflight candidate.', {
|
|
1576
|
+
expected: preflight.requiredAction,
|
|
1577
|
+
actual: { taskId, stepId, expectedRevision, actor },
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
if (candidate.claimant !== actor.trim()) {
|
|
1581
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Rescue actor does not match the active C1 claimant.', {
|
|
1582
|
+
expectedActor: candidate.claimant,
|
|
1583
|
+
actualActor: actor.trim(),
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
const lease = this.context(repository).locks.inspect(taskId);
|
|
1587
|
+
if (!lease || lease.token !== writerToken) {
|
|
1588
|
+
throw new WorkflowError('LOCK_TOKEN_INVALID', `Writer token does not own ${taskId}`);
|
|
1589
|
+
}
|
|
1590
|
+
return this.reviewStepWithKnowledgePolicy(repository, taskId, stepId, expectedRevision, runner, actor, writerToken, true);
|
|
1591
|
+
}
|
|
1390
1592
|
reviewStep(repository, taskId, stepId, expectedRevision, runner, actor, writerToken = null) {
|
|
1593
|
+
return this.reviewStepWithKnowledgePolicy(repository, taskId, stepId, expectedRevision, runner, actor, writerToken, false);
|
|
1594
|
+
}
|
|
1595
|
+
reviewStepWithKnowledgePolicy(repository, taskId, stepId, expectedRevision, runner, actor, writerToken, allowStaleKnowledgeBinding) {
|
|
1391
1596
|
const context = this.context(repository);
|
|
1392
1597
|
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
1393
1598
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
@@ -1398,7 +1603,8 @@ export class WorkflowService {
|
|
|
1398
1603
|
throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
|
|
1399
1604
|
}
|
|
1400
1605
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
1401
|
-
|
|
1606
|
+
if (!allowStaleKnowledgeBinding)
|
|
1607
|
+
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1402
1608
|
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
1403
1609
|
if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
1404
1610
|
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`, {
|
|
@@ -1459,7 +1665,7 @@ export class WorkflowService {
|
|
|
1459
1665
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
|
|
1460
1666
|
}
|
|
1461
1667
|
ensurePendingStrictStepReview(this.store, task, stepId, this.now());
|
|
1462
|
-
const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
|
|
1668
|
+
const envelope = this.taskContext(repository, taskId, 'independent-reviewer', allowStaleKnowledgeBinding);
|
|
1463
1669
|
const launch = launchStrictStepReviewer(context.identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, runner);
|
|
1464
1670
|
const recorded = recordStrictStepReview(this.store, task, stepId, {
|
|
1465
1671
|
review: launch.review,
|
|
@@ -1567,7 +1773,7 @@ export class WorkflowService {
|
|
|
1567
1773
|
assertExpectedRevision(task, expectedRevision);
|
|
1568
1774
|
this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
|
|
1569
1775
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
1570
|
-
if (
|
|
1776
|
+
if (!['needs_fix', 'awaiting_execution_authorization', 'ready'].includes(task.status)) {
|
|
1571
1777
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
|
|
1572
1778
|
}
|
|
1573
1779
|
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
@@ -1590,25 +1796,45 @@ export class WorkflowService {
|
|
|
1590
1796
|
stepId,
|
|
1591
1797
|
});
|
|
1592
1798
|
}
|
|
1593
|
-
if (
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
}
|
|
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
|
+
});
|
|
1601
1806
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
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
|
+
});
|
|
1612
1838
|
}
|
|
1613
1839
|
assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
|
|
1614
1840
|
return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
|
|
@@ -1845,6 +2071,243 @@ export class WorkflowService {
|
|
|
1845
2071
|
adoption,
|
|
1846
2072
|
};
|
|
1847
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
|
+
}
|
|
1848
2311
|
scanKnowledgeMap(repository) {
|
|
1849
2312
|
const { identity } = this.context(repository);
|
|
1850
2313
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
@@ -2132,6 +2595,25 @@ export class WorkflowService {
|
|
|
2132
2595
|
? this.delegatedApprovalOptions(projectId, transition, task)
|
|
2133
2596
|
: [];
|
|
2134
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);
|
|
2135
2617
|
const next = nextForTask(task);
|
|
2136
2618
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
2137
2619
|
const strictStepReviewStep = planRiskAudit
|
|
@@ -2204,7 +2686,53 @@ export class WorkflowService {
|
|
|
2204
2686
|
},
|
|
2205
2687
|
}
|
|
2206
2688
|
: {}),
|
|
2207
|
-
...(
|
|
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 } : {}),
|
|
2208
2736
|
};
|
|
2209
2737
|
if (handoffPosture.state === 'pending') {
|
|
2210
2738
|
return {
|
|
@@ -2371,6 +2899,28 @@ export class WorkflowService {
|
|
|
2371
2899
|
}
|
|
2372
2900
|
return inspected;
|
|
2373
2901
|
}
|
|
2902
|
+
strictReviewKnowledgeMap(repositoryRoot, task) {
|
|
2903
|
+
const stored = this.store.readKnowledgeMap(task.projectId);
|
|
2904
|
+
const inspected = inspectKnowledgeMap(stored, scanProjectKnowledge(repositoryRoot, task.projectId, this.now()));
|
|
2905
|
+
if (inspected.status === 'active' && inspected.approval)
|
|
2906
|
+
return inspected;
|
|
2907
|
+
const storedHash = hashKnowledgeMap(stored);
|
|
2908
|
+
if (stored.status === 'active'
|
|
2909
|
+
&& stored.approval
|
|
2910
|
+
&& task.knowledgeMapRevision === stored.revision
|
|
2911
|
+
&& task.knowledgeMapHash === storedHash
|
|
2912
|
+
&& stored.approval.mapHash === storedHash) {
|
|
2913
|
+
return stored;
|
|
2914
|
+
}
|
|
2915
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review has neither a current active Knowledge Map nor its exact historical Task binding.', {
|
|
2916
|
+
taskId: task.id,
|
|
2917
|
+
inspectedStatus: inspected.status,
|
|
2918
|
+
taskKnowledgeMapRevision: task.knowledgeMapRevision,
|
|
2919
|
+
storedKnowledgeMapRevision: stored.revision,
|
|
2920
|
+
taskKnowledgeMapHash: task.knowledgeMapHash,
|
|
2921
|
+
storedKnowledgeMapHash: storedHash,
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2374
2924
|
inspectAdoptionPreparation(identity, tasks, milestones) {
|
|
2375
2925
|
const locks = new WriterLockManager(this.store.lockRoot(identity.projectId), this.now);
|
|
2376
2926
|
const currentPosture = readCurrentAdoptionPosture(this.store, identity.projectId);
|
|
@@ -2698,6 +3248,67 @@ function validateCorrectivePlanAudit(task, audit, failedReviewAttempts) {
|
|
|
2698
3248
|
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective Auditor decision ${audit.decision} requires coordinator/user redirection before implementation continues.`, { decision: audit.decision, summary: audit.summary });
|
|
2699
3249
|
}
|
|
2700
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
|
+
}
|
|
2701
3312
|
function appendCorrectivePlanAudit(taskRoot, previous, saved, failedReviewAttempts, audit, now) {
|
|
2702
3313
|
appendFileSync(path.join(taskRoot, 'corrective-plan-audits.jsonl'), `${JSON.stringify({
|
|
2703
3314
|
recordedAt: now.toISOString(),
|