codex-workflow-v2 2.0.0-alpha.6.2 → 2.0.0-alpha.7.1
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 +43 -71
- package/dist/src/alpha6/handoff.d.ts +1 -0
- package/dist/src/alpha6/handoff.js +6 -0
- package/dist/src/alpha6/handoff.js.map +1 -1
- package/dist/src/alpha6/milestone.d.ts +3 -0
- package/dist/src/alpha6/milestone.js +57 -3
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/plan-risk.js +61 -0
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- package/dist/src/alpha6/remediation.d.ts +4 -20
- package/dist/src/alpha6/remediation.js +73 -150
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha7/autonomy.d.ts +61 -0
- package/dist/src/alpha7/autonomy.js +256 -0
- package/dist/src/alpha7/autonomy.js.map +1 -0
- package/dist/src/cli.js +23 -16
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +11 -6
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/state/lock.d.ts +1 -3
- package/dist/src/state/lock.js +2 -5
- 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 +21 -65
- package/dist/src/workflow.js +548 -657
- package/dist/src/workflow.js.map +1 -1
- package/docs/alpha7.1-implementation-brief.md +268 -0
- package/docs/autonomy-guardrails.md +56 -20
- package/docs/decisions.md +16 -4
- package/docs/delegated-approval.md +12 -5
- package/docs/development-flow.md +12 -7
- package/docs/release.md +4 -6
- package/docs/updating-existing-project.md +9 -39
- package/docs/validation-report.md +50 -63
- package/package.json +2 -2
- package/plugins/codex-workflow-gateway/references/protocol.md +53 -17
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +33 -9
- package/references/state-machine.md +31 -13
- package/roles/delivery-coordinator.md +10 -1
- package/roles/technical-planner.md +3 -0
- package/schemas/context-envelope.schema.json +0 -10
- package/schemas/milestone-autonomy-event.schema.json +45 -0
- package/schemas/milestone-scope-change-event.schema.json +3 -1
- package/schemas/plan-risk-audit-event.schema.json +21 -4
- package/schemas/corrective-plan-audit-record.schema.json +0 -47
package/dist/src/workflow.js
CHANGED
|
@@ -3,18 +3,19 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
|
|
5
5
|
import { renderBrief, renderPlan, renderResult } from './artifacts.js';
|
|
6
|
-
import { assertDelegationCanAuthorize,
|
|
6
|
+
import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
|
|
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';
|
|
11
10
|
import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
|
|
12
|
-
import {
|
|
11
|
+
import { inspectLocalDependency } from './diagnostics.js';
|
|
12
|
+
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, 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 {
|
|
15
|
+
import { appendCorrectiveDecisionEvent, appendRemediationEvent, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, 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,
|
|
17
|
+
import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
|
|
18
|
+
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
18
19
|
import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
|
|
19
20
|
import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
|
|
20
21
|
import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
|
|
@@ -24,15 +25,12 @@ import { WriterLockManager } from './state/lock.js';
|
|
|
24
25
|
import { FileStateStore } from './state/store.js';
|
|
25
26
|
import { createEntityId } from './ulid.js';
|
|
26
27
|
import { PACKAGE_VERSION } from './version.js';
|
|
27
|
-
const ALPHA6_CORRECTIVE_RESCUE_HEAD = '5c63798cc7b8fbb5e1c8b26ebf93876bc642295a';
|
|
28
28
|
export class WorkflowService {
|
|
29
29
|
store;
|
|
30
30
|
now;
|
|
31
|
-
|
|
32
|
-
constructor(store = new FileStateStore(), now = () => new Date(), alpha6CorrectiveRescueHead = ALPHA6_CORRECTIVE_RESCUE_HEAD) {
|
|
31
|
+
constructor(store = new FileStateStore(), now = () => new Date()) {
|
|
33
32
|
this.store = store;
|
|
34
33
|
this.now = now;
|
|
35
|
-
this.alpha6CorrectiveRescueHead = alpha6CorrectiveRescueHead;
|
|
36
34
|
}
|
|
37
35
|
context(repository) {
|
|
38
36
|
const identity = resolveRepositoryIdentity(repository);
|
|
@@ -186,7 +184,12 @@ export class WorkflowService {
|
|
|
186
184
|
if (!title.trim())
|
|
187
185
|
throw new WorkflowError('INVALID_ARGUMENT', 'Task title is required.');
|
|
188
186
|
if (milestoneId) {
|
|
189
|
-
|
|
187
|
+
// A Milestone is assembled from independently materialized Task discoveries. During
|
|
188
|
+
// that bounded assembly window the reverse-membership set is intentionally incomplete;
|
|
189
|
+
// execution remains unavailable until one complete initial Milestone Plan classifies
|
|
190
|
+
// every linked Task. The scope-change reader preserves every other integrity check while
|
|
191
|
+
// allowing exactly that repairable reverse gap.
|
|
192
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
190
193
|
if (milestone.status === 'accepted' || milestone.status === 'cancelled') {
|
|
191
194
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
|
|
192
195
|
}
|
|
@@ -405,6 +408,64 @@ export class WorkflowService {
|
|
|
405
408
|
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
406
409
|
});
|
|
407
410
|
}
|
|
411
|
+
prepareMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt) {
|
|
412
|
+
const { identity } = this.context(repository);
|
|
413
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
414
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
415
|
+
return prepareMilestoneAutonomyContract({
|
|
416
|
+
projectId: identity.projectId,
|
|
417
|
+
milestone,
|
|
418
|
+
principal,
|
|
419
|
+
delegate,
|
|
420
|
+
expiresAt,
|
|
421
|
+
now: this.now(),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
grantMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt, confirmationCode) {
|
|
425
|
+
const { identity } = this.context(repository);
|
|
426
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
427
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
428
|
+
return applyMilestoneAutonomyContract({
|
|
429
|
+
store: this.store,
|
|
430
|
+
projectId: identity.projectId,
|
|
431
|
+
milestone,
|
|
432
|
+
principal,
|
|
433
|
+
delegate,
|
|
434
|
+
expiresAt,
|
|
435
|
+
confirmationCode,
|
|
436
|
+
now: this.now(),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
evolveMilestonePlanAutonomously(repository, milestoneId, expectedRevision, plan, actor) {
|
|
440
|
+
const { identity } = this.context(repository);
|
|
441
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
442
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
443
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
444
|
+
const autonomy = requireActiveMilestoneAutonomy({
|
|
445
|
+
store: this.store,
|
|
446
|
+
projectId: identity.projectId,
|
|
447
|
+
milestone,
|
|
448
|
+
actor,
|
|
449
|
+
now: this.now(),
|
|
450
|
+
});
|
|
451
|
+
validateMilestonePlan(plan);
|
|
452
|
+
assertAutonomousPlanEvolution(milestone, plan, autonomy.contract);
|
|
453
|
+
const prepared = prepareMilestoneScopeChangeCandidate({
|
|
454
|
+
milestone,
|
|
455
|
+
plan,
|
|
456
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
457
|
+
actor,
|
|
458
|
+
autonomyContractEventHash: autonomy.contract.eventHash,
|
|
459
|
+
});
|
|
460
|
+
return applyMilestoneScopeChangeTransaction({
|
|
461
|
+
store: this.store,
|
|
462
|
+
milestone,
|
|
463
|
+
expectedRevision,
|
|
464
|
+
prepared,
|
|
465
|
+
now: this.now(),
|
|
466
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
408
469
|
authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
409
470
|
const { identity } = this.context(repository);
|
|
410
471
|
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
@@ -568,13 +629,14 @@ export class WorkflowService {
|
|
|
568
629
|
this.validateKnowledgeTargets(knowledgeMap, plan);
|
|
569
630
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
570
631
|
assertExpectedRevision(task, expectedRevision);
|
|
571
|
-
if (!['planning', 'awaiting_execution_authorization', '
|
|
632
|
+
if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
|
|
572
633
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
|
|
573
634
|
}
|
|
574
635
|
const currentPlanCorrectiveDecisions = task.planHash
|
|
575
636
|
? readCorrectiveDecisionEvents(this.store, task).filter((event) => event.planHash === task.planHash)
|
|
576
637
|
: [];
|
|
577
638
|
const currentReplanDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'replan-required');
|
|
639
|
+
const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'continue-fix');
|
|
578
640
|
const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required' || event.decision === 'stop-escalate');
|
|
579
641
|
const currentReplanRequiredStepIds = [...new Set(currentReplanDecisions.map((event) => event.stepId))];
|
|
580
642
|
if (currentHardBlockingDecisions.length > 0) {
|
|
@@ -595,22 +657,33 @@ export class WorkflowService {
|
|
|
595
657
|
requiredAction: 'Resolve the blocked strict review or record a replan-required corrective decision first.',
|
|
596
658
|
});
|
|
597
659
|
}
|
|
598
|
-
|
|
660
|
+
if (currentContinueFixDecisions.length > 0) {
|
|
661
|
+
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.`, {
|
|
662
|
+
taskId: task.id,
|
|
663
|
+
requiredAction: 'Run the guarded Step under the current Plan retry path, or escalate/cancel instead of issuing another plan-set at the same ordinal.',
|
|
664
|
+
decisions: currentContinueFixDecisions.map((event) => ({
|
|
665
|
+
eventId: event.eventId,
|
|
666
|
+
stepId: event.stepId,
|
|
667
|
+
triggeringAttemptCount: event.triggeringAttemptCount,
|
|
668
|
+
})),
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
const currentPlanRiskAudit = task.status === 'needs_fix'
|
|
672
|
+
? readCurrentPlanRiskAudit(this.store, task)
|
|
673
|
+
: null;
|
|
599
674
|
const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
|
|
600
675
|
if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
|
|
601
676
|
validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
|
|
602
677
|
}
|
|
603
|
-
const guardedRemediationGate =
|
|
604
|
-
&& currentPlanRiskAudit
|
|
678
|
+
const guardedRemediationGate = task.status === 'needs_fix' && currentPlanRiskAudit
|
|
605
679
|
? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
|
|
606
680
|
: null;
|
|
607
|
-
if (guardedRemediationGate && !
|
|
608
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a
|
|
681
|
+
if (guardedRemediationGate && currentReplanDecisions.length === 0 && !correctiveAudit) {
|
|
682
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a corrective decision before another remediation run can be authorized.`, {
|
|
609
683
|
taskId: task.id,
|
|
610
684
|
stepId: guardedRemediationGate.stepId,
|
|
611
685
|
attemptCount: guardedRemediationGate.attemptCount,
|
|
612
686
|
requiredSidecar: 'corrective-decisions.jsonl',
|
|
613
|
-
requiredAction: 'Run task corrective-decision first.',
|
|
614
687
|
allowedDecision: 'continue-fix',
|
|
615
688
|
escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
|
|
616
689
|
});
|
|
@@ -622,57 +695,25 @@ export class WorkflowService {
|
|
|
622
695
|
requiredAction: 'Retry task plan-set without corrective-audit input; the recorded replan-required decision is the authorization for this recovery path.',
|
|
623
696
|
});
|
|
624
697
|
}
|
|
625
|
-
|
|
626
|
-
|
|
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.`, {
|
|
698
|
+
if (guardedRemediationGate && currentReplanDecisions.length === 0 && correctiveAudit?.decision !== 'continue-fix') {
|
|
699
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a separate corrective-decision record before execution may continue; plan-set only supports continue-fix.`, {
|
|
633
700
|
taskId: task.id,
|
|
634
701
|
stepId: guardedRemediationGate.stepId,
|
|
635
|
-
|
|
702
|
+
decision: correctiveAudit?.decision ?? null,
|
|
636
703
|
});
|
|
637
704
|
}
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
});
|
|
705
|
+
for (const requirement of task.requirements) {
|
|
706
|
+
if (!plan.requirements.includes(requirement)) {
|
|
707
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task requirement ${requirement}.`);
|
|
653
708
|
}
|
|
654
709
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
}
|
|
710
|
+
for (const acceptance of task.acceptance) {
|
|
711
|
+
if (!plan.acceptance.includes(acceptance)) {
|
|
712
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task acceptance ${acceptance}.`);
|
|
665
713
|
}
|
|
666
714
|
}
|
|
667
715
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
668
716
|
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
|
-
}
|
|
676
717
|
const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
677
718
|
? { ...authorization, decision: 'superseded' }
|
|
678
719
|
: authorization);
|
|
@@ -702,8 +743,9 @@ export class WorkflowService {
|
|
|
702
743
|
stepId: guardedRemediationGate.stepId,
|
|
703
744
|
});
|
|
704
745
|
}
|
|
705
|
-
if (
|
|
706
|
-
|
|
746
|
+
if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
|
|
747
|
+
assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
|
|
748
|
+
appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, stagedPlan.hash, this.now());
|
|
707
749
|
}
|
|
708
750
|
const candidate = {
|
|
709
751
|
...task,
|
|
@@ -726,16 +768,12 @@ export class WorkflowService {
|
|
|
726
768
|
evidenceHash: null,
|
|
727
769
|
blockReason: null,
|
|
728
770
|
};
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
validatePlanRiskAuditCandidate(this.store, candidate, effectivePlanRiskAudit);
|
|
771
|
+
if (planRiskAudit)
|
|
772
|
+
validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
|
|
732
773
|
const saved = this.store.writeTask(candidate, expectedRevision);
|
|
733
774
|
this.store.publishArtifact(stagedPlan);
|
|
734
|
-
if (
|
|
735
|
-
appendCurrentPlanRiskAudit(this.store, saved,
|
|
736
|
-
if (guardedContinueFixDecision && correctiveAudit) {
|
|
737
|
-
appendCorrectivePlanAuditRecord(this.store, task, saved, guardedRemediationGate.stepId, guardedContinueFixDecision, correctiveAudit, this.now());
|
|
738
|
-
}
|
|
775
|
+
if (planRiskAudit)
|
|
776
|
+
appendCurrentPlanRiskAudit(this.store, saved, planRiskAudit, this.now());
|
|
739
777
|
if (correctiveAudit && task.status === 'needs_fix' && task.review.status === 'failed') {
|
|
740
778
|
appendCorrectivePlanAudit(this.store.taskRoot(identity.projectId, taskId), task, saved, failedReviewAttempts, correctiveAudit, this.now());
|
|
741
779
|
}
|
|
@@ -761,11 +799,6 @@ export class WorkflowService {
|
|
|
761
799
|
if (activeStep) {
|
|
762
800
|
throw new WorkflowError('TRANSITION_BLOCKED', `Cannot rebind Task Plan knowledge while ${activeStep.id} is in progress.`);
|
|
763
801
|
}
|
|
764
|
-
const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
765
|
-
const breakerPlanRiskAudit = currentPlanRiskAudit
|
|
766
|
-
&& findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
|
|
767
|
-
? currentPlanRiskAudit
|
|
768
|
-
: null;
|
|
769
802
|
const knowledgeMap = this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
770
803
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
771
804
|
if (task.knowledgeMapRevision === knowledgeMap.revision &&
|
|
@@ -825,9 +858,6 @@ export class WorkflowService {
|
|
|
825
858
|
blockReason: null,
|
|
826
859
|
}, expectedRevision);
|
|
827
860
|
this.store.publishArtifact(stagedPlan);
|
|
828
|
-
if (breakerPlanRiskAudit) {
|
|
829
|
-
appendReboundPlanRiskAudit(this.store, task, saved, breakerPlanRiskAudit, this.now());
|
|
830
|
-
}
|
|
831
861
|
return saved;
|
|
832
862
|
}
|
|
833
863
|
refreshTaskContext(repository, taskId, expectedTaskRevision, expectedKnowledgeMapRevision, actor, delegationGrantId) {
|
|
@@ -849,21 +879,29 @@ export class WorkflowService {
|
|
|
849
879
|
const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
|
|
850
880
|
const transitions = [];
|
|
851
881
|
const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
852
|
-
const
|
|
853
|
-
|
|
882
|
+
const currentPlanArtifactHash = task.planHash
|
|
883
|
+
? this.store.hashArtifact(this.store.taskRoot(identity.projectId, task.id), 'plan.md')
|
|
884
|
+
: null;
|
|
885
|
+
const refreshAssessment = assessDelegatedKnowledgeRefresh(currentMap, inspectedMap, task, currentPlanArtifactHash);
|
|
854
886
|
// Validate every delegated transition before the first write so a narrow grant cannot
|
|
855
887
|
// leave the composite operation half-applied.
|
|
856
888
|
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
857
889
|
let knowledgeMap = currentMap;
|
|
858
890
|
if (inspectedMap.status !== 'active' || !inspectedMap.approval) {
|
|
859
|
-
if (currentMap.status !== 'active' || !currentMap.approval
|
|
860
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with
|
|
891
|
+
if (currentMap.status !== 'active' || !currentMap.approval) {
|
|
892
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with safe delegated drift.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
|
|
893
|
+
}
|
|
894
|
+
assertDelegatedKnowledgeRefresh(refreshAssessment);
|
|
895
|
+
if (inspectedMap.status !== 'stale') {
|
|
896
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Safe delegated Knowledge drift must reconcile to a stale candidate before approval.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
|
|
897
|
+
}
|
|
898
|
+
if (refreshAssessment.mode === 'delegated-plan-bounded-supporting-source') {
|
|
899
|
+
this.assertPlanBoundedSupportingSourceGrant(identity.projectId, delegationGrantId, actor, task);
|
|
861
900
|
}
|
|
862
|
-
|
|
863
|
-
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspectedMap);
|
|
901
|
+
this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspectedMap, task.id);
|
|
864
902
|
knowledgeMap = this.reconcileKnowledgeMap(repository, currentMap.revision);
|
|
865
903
|
transitions.push('project-memory reconcile');
|
|
866
|
-
knowledgeMap = this.
|
|
904
|
+
knowledgeMap = this.approveKnowledgeMapForTaskRefresh(repository, knowledgeMap.revision, [], actor, delegationGrantId, task.id);
|
|
867
905
|
transitions.push('project-memory approve');
|
|
868
906
|
}
|
|
869
907
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
@@ -872,15 +910,18 @@ export class WorkflowService {
|
|
|
872
910
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
|
|
873
911
|
}
|
|
874
912
|
let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
|
|
875
|
-
if (currentPlanRiskAudit
|
|
913
|
+
if (currentPlanRiskAudit) {
|
|
876
914
|
appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
|
|
877
915
|
}
|
|
878
916
|
transitions.push('task knowledge-rebind');
|
|
879
|
-
refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor,
|
|
917
|
+
refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, refreshAssessment.mode === 'delegated-plan-bounded-supporting-source'
|
|
918
|
+
? 'Delegated Plan-bounded supporting-source context refresh.'
|
|
919
|
+
: 'Delegated content-only context refresh.', delegationGrantId);
|
|
880
920
|
transitions.push('task execution authorize');
|
|
881
921
|
return {
|
|
882
|
-
mode:
|
|
922
|
+
mode: refreshAssessment.mode,
|
|
883
923
|
transitions,
|
|
924
|
+
addedSources: refreshAssessment.addedSources,
|
|
884
925
|
knowledgeMap,
|
|
885
926
|
task: refreshedTask,
|
|
886
927
|
};
|
|
@@ -1154,9 +1195,9 @@ export class WorkflowService {
|
|
|
1154
1195
|
currentAuditPresent: Boolean(planRiskAudit),
|
|
1155
1196
|
});
|
|
1156
1197
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1198
|
+
if (planRiskAudit && isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
1199
|
+
assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
|
|
1200
|
+
}
|
|
1160
1201
|
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1161
1202
|
if (step.status === 'planned' && dirty.length > 0) {
|
|
1162
1203
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
|
|
@@ -1259,25 +1300,15 @@ export class WorkflowService {
|
|
|
1259
1300
|
stepId,
|
|
1260
1301
|
expectedRevision: saved.revision,
|
|
1261
1302
|
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
|
-
: {}),
|
|
1271
1303
|
knowledgeMap: knowledgeSummary(knowledgeMap),
|
|
1272
1304
|
knowledgeSources: selectKnowledgeSources(knowledgeMap, 'worker', task.knowledgeTargets.map((target) => target.path)),
|
|
1273
1305
|
},
|
|
1274
1306
|
};
|
|
1275
1307
|
}
|
|
1276
|
-
taskContext(repository, taskId, role
|
|
1308
|
+
taskContext(repository, taskId, role) {
|
|
1277
1309
|
const { identity } = this.context(repository);
|
|
1278
1310
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
1279
|
-
const historicalStrictReview =
|
|
1280
|
-
&& role === 'independent-reviewer'
|
|
1311
|
+
const historicalStrictReview = role === 'independent-reviewer'
|
|
1281
1312
|
&& task.steps.some((step) => step.status === 'in_progress' && step.evidence !== null);
|
|
1282
1313
|
const knowledgeMap = task.knowledgeMapRevision === null
|
|
1283
1314
|
? this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId)
|
|
@@ -1314,7 +1345,7 @@ export class WorkflowService {
|
|
|
1314
1345
|
if (role === 'technical-planner') {
|
|
1315
1346
|
return {
|
|
1316
1347
|
...common,
|
|
1317
|
-
objective: `Produce an execution-ready Plan for ${task.title}. Perform an explicit risk review before plan-set: migrations, concurrency, restart/replay, crash windows, and external-provider behavior require failure-specific checks in the original Plan.${planRiskReviewScope}`,
|
|
1348
|
+
objective: `Produce an execution-ready Plan for ${task.title}. Perform an explicit risk review before plan-set: migrations, concurrency, restart/replay, crash windows, and external-provider behavior require failure-specific checks and executable proof obligations in the original Plan.${planRiskReviewScope}`,
|
|
1318
1349
|
resultSchema: {
|
|
1319
1350
|
type: 'object',
|
|
1320
1351
|
required: ['objective', 'requirements', 'acceptance', 'risks', 'steps'],
|
|
@@ -1325,6 +1356,7 @@ export class WorkflowService {
|
|
|
1325
1356
|
'Concurrency or cursor/state ownership is planned without deterministic race checks.',
|
|
1326
1357
|
'Restart, replay, or crash recovery is claimed without restart/crash-window evidence.',
|
|
1327
1358
|
'An external provider is used without failure-injection and hidden-default verification.',
|
|
1359
|
+
'A guarded failure mode lacks a hostile counterexample, expected safe result, or exact executable evidence command.',
|
|
1328
1360
|
'The safe risk slice crosses persistence, recovery orchestration, and executable composition and should be split.',
|
|
1329
1361
|
],
|
|
1330
1362
|
};
|
|
@@ -1438,161 +1470,7 @@ export class WorkflowService {
|
|
|
1438
1470
|
});
|
|
1439
1471
|
return this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
|
|
1440
1472
|
}
|
|
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
|
-
}
|
|
1592
1473
|
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) {
|
|
1596
1474
|
const context = this.context(repository);
|
|
1597
1475
|
this.readCurrentAdoptionPostureStrict(context.identity.projectId);
|
|
1598
1476
|
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
@@ -1603,8 +1481,6 @@ export class WorkflowService {
|
|
|
1603
1481
|
throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
|
|
1604
1482
|
}
|
|
1605
1483
|
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
1606
|
-
if (!allowStaleKnowledgeBinding)
|
|
1607
|
-
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
1608
1484
|
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
1609
1485
|
if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
1610
1486
|
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`, {
|
|
@@ -1620,6 +1496,13 @@ export class WorkflowService {
|
|
|
1620
1496
|
stepStatus: step.status,
|
|
1621
1497
|
});
|
|
1622
1498
|
}
|
|
1499
|
+
// A strict review is bound to the immutable completion commit, Task Plan, and the
|
|
1500
|
+
// Knowledge Map revision that authorized the Step. Project knowledge commonly becomes
|
|
1501
|
+
// stale because the completion commit itself changes a classified source. Requiring the
|
|
1502
|
+
// *current* map here creates a deadlock: review cannot run before rebind, while rebind is
|
|
1503
|
+
// intentionally forbidden until the in-progress Step becomes terminal. The pending
|
|
1504
|
+
// strict-review binding below validates the historical Task context; a current-map rebind
|
|
1505
|
+
// remains mandatory after the review finalizes the Step and before another Step can run.
|
|
1623
1506
|
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
1624
1507
|
if (!isClean(context.identity.repositoryRoot)) {
|
|
1625
1508
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review requires a clean Task checkout.', {
|
|
@@ -1665,7 +1548,7 @@ export class WorkflowService {
|
|
|
1665
1548
|
return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
|
|
1666
1549
|
}
|
|
1667
1550
|
ensurePendingStrictStepReview(this.store, task, stepId, this.now());
|
|
1668
|
-
const envelope = this.taskContext(repository, taskId, 'independent-reviewer'
|
|
1551
|
+
const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
|
|
1669
1552
|
const launch = launchStrictStepReviewer(context.identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, runner);
|
|
1670
1553
|
const recorded = recordStrictStepReview(this.store, task, stepId, {
|
|
1671
1554
|
review: launch.review,
|
|
@@ -1773,7 +1656,7 @@ export class WorkflowService {
|
|
|
1773
1656
|
assertExpectedRevision(task, expectedRevision);
|
|
1774
1657
|
this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
|
|
1775
1658
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
1776
|
-
if (!['
|
|
1659
|
+
if (!['ready', 'needs_fix', 'blocked'].includes(task.status)) {
|
|
1777
1660
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
|
|
1778
1661
|
}
|
|
1779
1662
|
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
@@ -1789,51 +1672,24 @@ export class WorkflowService {
|
|
|
1789
1672
|
stepId,
|
|
1790
1673
|
});
|
|
1791
1674
|
}
|
|
1792
|
-
const step = requireStep(task, stepId);
|
|
1793
1675
|
if (!isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
1794
1676
|
throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit and cannot receive a corrective decision.`, {
|
|
1795
1677
|
taskId,
|
|
1796
1678
|
stepId,
|
|
1797
1679
|
});
|
|
1798
1680
|
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1681
|
+
const posture = readGuardedRemediationPostureForStep(this.store, task, stepId, planRiskAudit);
|
|
1682
|
+
if (!posture) {
|
|
1683
|
+
const step = requireStep(task, stepId);
|
|
1684
|
+
const currentCycle = step.status === 'in_progress' && step.evidence
|
|
1685
|
+
? buildCurrentStrictStepReviewCycle(this.store, task, stepId).resolution
|
|
1686
|
+
: null;
|
|
1687
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a failed guarded Step or a blocked unverified strict-review retry posture.`, {
|
|
1801
1688
|
taskId,
|
|
1802
1689
|
stepId,
|
|
1803
1690
|
taskStatus: task.status,
|
|
1804
1691
|
stepStatus: step.status,
|
|
1805
|
-
|
|
1806
|
-
}
|
|
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,
|
|
1692
|
+
resolution: currentCycle,
|
|
1837
1693
|
});
|
|
1838
1694
|
}
|
|
1839
1695
|
assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
|
|
@@ -2071,243 +1927,109 @@ export class WorkflowService {
|
|
|
2071
1927
|
adoption,
|
|
2072
1928
|
};
|
|
2073
1929
|
}
|
|
2074
|
-
|
|
2075
|
-
const
|
|
2076
|
-
const
|
|
2077
|
-
const
|
|
2078
|
-
const
|
|
2079
|
-
const
|
|
2080
|
-
const
|
|
2081
|
-
const
|
|
2082
|
-
const
|
|
2083
|
-
const
|
|
1930
|
+
alpha6StrictReviewRescuePreflight(repository) {
|
|
1931
|
+
const context = this.context(repository);
|
|
1932
|
+
const dependency = inspectLocalDependency(context.identity.repositoryRoot);
|
|
1933
|
+
const tasks = context.store.listTasks(context.identity.projectId);
|
|
1934
|
+
const leases = context.locks.list();
|
|
1935
|
+
const now = this.now().getTime();
|
|
1936
|
+
const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
1937
|
+
const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
1938
|
+
const dirty = changedFiles(context.identity.repositoryRoot);
|
|
1939
|
+
const currentHead = headCommit(context.identity.repositoryRoot);
|
|
1940
|
+
const pendingCandidates = [];
|
|
1941
|
+
const malformedCandidates = [];
|
|
2084
1942
|
for (const task of tasks) {
|
|
2085
|
-
if (task.
|
|
1943
|
+
if (task.status !== 'in_progress')
|
|
2086
1944
|
continue;
|
|
2087
|
-
const
|
|
2088
|
-
if (!
|
|
1945
|
+
const planRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
|
|
1946
|
+
if (!planRiskAudit)
|
|
2089
1947
|
continue;
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
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.');
|
|
1948
|
+
for (const step of task.steps) {
|
|
1949
|
+
if (step.status !== 'in_progress' || !step.evidence)
|
|
1950
|
+
continue;
|
|
1951
|
+
if (!isStepReviewRequired(task, step.id, planRiskAudit.reviewRequiredStepIds))
|
|
1952
|
+
continue;
|
|
1953
|
+
try {
|
|
1954
|
+
const cycle = buildCurrentStrictStepReviewCycle(this.store, task, step.id);
|
|
1955
|
+
if (cycle.resolution !== 'pending')
|
|
1956
|
+
continue;
|
|
1957
|
+
validateTaskHistory(context.identity.repositoryRoot, task);
|
|
1958
|
+
let knowledgeBindingStale = false;
|
|
1959
|
+
try {
|
|
1960
|
+
const storedKnowledgeMap = this.store.readKnowledgeMap(context.identity.projectId);
|
|
1961
|
+
const knowledgeMap = inspectKnowledgeMap(storedKnowledgeMap, scanProjectKnowledge(context.identity.repositoryRoot, context.identity.projectId, this.now()));
|
|
1962
|
+
knowledgeBindingStale = knowledgeMap.status !== 'active'
|
|
1963
|
+
|| task.knowledgeMapRevision !== knowledgeMap.revision
|
|
1964
|
+
|| task.knowledgeMapHash !== hashKnowledgeMap(knowledgeMap);
|
|
2152
1965
|
}
|
|
2153
|
-
|
|
1966
|
+
catch {
|
|
1967
|
+
knowledgeBindingStale = true;
|
|
1968
|
+
}
|
|
1969
|
+
pendingCandidates.push({
|
|
1970
|
+
taskId: task.id,
|
|
1971
|
+
taskRevision: task.revision,
|
|
1972
|
+
stepId: step.id,
|
|
1973
|
+
completionCommit: step.evidence.commitSha,
|
|
1974
|
+
currentHead,
|
|
1975
|
+
knowledgeBindingStale,
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
catch (error) {
|
|
1979
|
+
malformedCandidates.push(error instanceof Error ? error.message : String(error));
|
|
2154
1980
|
}
|
|
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
1981
|
}
|
|
2176
1982
|
}
|
|
2177
|
-
const candidate =
|
|
1983
|
+
const candidate = pendingCandidates.length === 1 ? pendingCandidates[0] : null;
|
|
1984
|
+
const candidateLeaseIds = new Set(candidate ? [candidate.taskId] : []);
|
|
2178
1985
|
const blockers = [
|
|
2179
1986
|
...(dependency.declared !== '2.0.0-alpha.6'
|
|
2180
1987
|
? ['Project package.json must declare codex-workflow-v2 exactly at 2.0.0-alpha.6.']
|
|
2181
1988
|
: []),
|
|
2182
1989
|
...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
|
|
2183
|
-
...(
|
|
2184
|
-
|
|
1990
|
+
...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
|
|
1991
|
+
...(pendingCandidates.length !== 1
|
|
1992
|
+
? [`Expected exactly one pending alpha.6 strict review; found ${pendingCandidates.length}.`]
|
|
2185
1993
|
: []),
|
|
2186
|
-
...(
|
|
2187
|
-
? [
|
|
1994
|
+
...(candidate && candidate.currentHead !== candidate.completionCommit
|
|
1995
|
+
? ['HEAD does not match the pending strict review completion commit.']
|
|
2188
1996
|
: []),
|
|
2189
|
-
...(
|
|
2190
|
-
? ['
|
|
1997
|
+
...(staleLeases.some((lease) => !candidateLeaseIds.has(lease.entityId))
|
|
1998
|
+
? ['A stale writer lease exists outside the rescue Task.']
|
|
2191
1999
|
: []),
|
|
2192
|
-
...
|
|
2000
|
+
...malformedCandidates.map((message) => `Pending strict review state is invalid: ${message}`),
|
|
2193
2001
|
];
|
|
2002
|
+
const eligible = blockers.length === 0 && candidate !== null;
|
|
2003
|
+
const requiredActions = eligible
|
|
2004
|
+
? [
|
|
2005
|
+
...staleLeases.map((lease) => ({
|
|
2006
|
+
action: 'locks repair',
|
|
2007
|
+
entityId: lease.entityId,
|
|
2008
|
+
})),
|
|
2009
|
+
{
|
|
2010
|
+
action: 'task step-review',
|
|
2011
|
+
taskId: candidate.taskId,
|
|
2012
|
+
stepId: candidate.stepId,
|
|
2013
|
+
expectedRevision: candidate.taskRevision,
|
|
2014
|
+
},
|
|
2015
|
+
]
|
|
2016
|
+
: [];
|
|
2194
2017
|
return {
|
|
2195
|
-
eligible
|
|
2018
|
+
eligible,
|
|
2196
2019
|
mutationFree: true,
|
|
2197
2020
|
runnerPackageVersion: PACKAGE_VERSION,
|
|
2198
2021
|
requiredProjectPackageVersion: '2.0.0-alpha.6',
|
|
2199
2022
|
declaredProjectPackageVersion: dependency.declared,
|
|
2200
|
-
|
|
2201
|
-
|
|
2023
|
+
projectId: context.identity.projectId,
|
|
2024
|
+
repositoryRoot: context.identity.repositoryRoot,
|
|
2202
2025
|
clean: dirty.length === 0,
|
|
2026
|
+
activeLeases,
|
|
2027
|
+
staleLeases,
|
|
2203
2028
|
candidate,
|
|
2204
2029
|
blockers,
|
|
2030
|
+
requiredActions,
|
|
2205
2031
|
};
|
|
2206
2032
|
}
|
|
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
|
-
}
|
|
2311
2033
|
scanKnowledgeMap(repository) {
|
|
2312
2034
|
const { identity } = this.context(repository);
|
|
2313
2035
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
@@ -2332,11 +2054,14 @@ export class WorkflowService {
|
|
|
2332
2054
|
return inspectKnowledgeMap(current, scanned);
|
|
2333
2055
|
}
|
|
2334
2056
|
approveKnowledgeMap(repository, expectedRevision, selections, actor, delegationGrantId = null) {
|
|
2057
|
+
return this.approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, null);
|
|
2058
|
+
}
|
|
2059
|
+
approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, autonomyTaskId) {
|
|
2335
2060
|
const { identity } = this.context(repository);
|
|
2336
2061
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
2337
2062
|
assertExpectedRevision(current, expectedRevision);
|
|
2338
2063
|
const inspected = inspectKnowledgeMap(current, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
|
|
2339
|
-
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspected);
|
|
2064
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspected, autonomyTaskId);
|
|
2340
2065
|
const approved = applyKnowledgeSelections(inspected, selections, actor, this.now());
|
|
2341
2066
|
if (!approved.approval) {
|
|
2342
2067
|
throw new WorkflowError('STATE_CORRUPT', 'Knowledge Map approval was not recorded.');
|
|
@@ -2405,11 +2130,20 @@ export class WorkflowService {
|
|
|
2405
2130
|
const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
|
|
2406
2131
|
const activeTask = status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
|
|
2407
2132
|
if (activeTask) {
|
|
2133
|
+
const activePlanRiskAudit = activeTask.planHash
|
|
2134
|
+
? readCurrentPlanRiskAudit(this.store, activeTask)
|
|
2135
|
+
: null;
|
|
2136
|
+
const taskNext = activeTask.status === 'planning'
|
|
2137
|
+
? null
|
|
2138
|
+
: this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
|
|
2139
|
+
// Pending strict review is completion work for the already-recorded commit. It must
|
|
2140
|
+
// outrank Project Knowledge refresh/rebind; otherwise the active Step makes the rebind
|
|
2141
|
+
// returned by `next` impossible to execute.
|
|
2142
|
+
if (taskNext?.action === 'task step-review') {
|
|
2143
|
+
return withAdoption(taskNext);
|
|
2144
|
+
}
|
|
2408
2145
|
if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
|
|
2409
|
-
|
|
2410
|
-
? readCurrentPlanRiskAudit(this.store, activeTask)
|
|
2411
|
-
: null;
|
|
2412
|
-
return withAdoption(this.nextForTaskWithDelegation(status.projectId, activeTask, planRiskAudit));
|
|
2146
|
+
return withAdoption(taskNext);
|
|
2413
2147
|
}
|
|
2414
2148
|
const knowledgeMap = this.knowledgeMapStatus(repository);
|
|
2415
2149
|
if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
|
|
@@ -2418,11 +2152,14 @@ export class WorkflowService {
|
|
|
2418
2152
|
? this.delegatedApprovalOptions(status.projectId, 'project_memory.approve', knowledgeMap)
|
|
2419
2153
|
: [];
|
|
2420
2154
|
const storedKnowledgeMap = this.store.readKnowledgeMap(status.projectId);
|
|
2421
|
-
const
|
|
2422
|
-
&& storedKnowledgeMap.status === 'active'
|
|
2155
|
+
const refreshAssessment = storedKnowledgeMap.status === 'active'
|
|
2423
2156
|
&& storedKnowledgeMap.approval
|
|
2424
|
-
|
|
2425
|
-
|
|
2157
|
+
? assessDelegatedKnowledgeRefresh(storedKnowledgeMap, knowledgeMap, activeTask, activeTask.planHash
|
|
2158
|
+
? this.store.hashArtifact(this.store.taskRoot(status.projectId, activeTask.id), 'plan.md')
|
|
2159
|
+
: null)
|
|
2160
|
+
: null;
|
|
2161
|
+
const contextRefreshOptions = action === 'project-memory reconcile' && refreshAssessment?.eligible
|
|
2162
|
+
? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
|
|
2426
2163
|
: [];
|
|
2427
2164
|
return withAdoption({
|
|
2428
2165
|
scope: 'project-memory',
|
|
@@ -2435,11 +2172,21 @@ export class WorkflowService {
|
|
|
2435
2172
|
? {
|
|
2436
2173
|
contextRefresh: {
|
|
2437
2174
|
action: 'task context-refresh',
|
|
2438
|
-
mode:
|
|
2175
|
+
mode: refreshAssessment.mode,
|
|
2176
|
+
addedSources: refreshAssessment.addedSources,
|
|
2439
2177
|
options: contextRefreshOptions,
|
|
2440
2178
|
},
|
|
2441
2179
|
}
|
|
2442
2180
|
: {}),
|
|
2181
|
+
...(refreshAssessment && !refreshAssessment.eligible
|
|
2182
|
+
? {
|
|
2183
|
+
contextRefreshBlocked: {
|
|
2184
|
+
reason: 'Knowledge drift is outside the atomic delegated refresh predicate.',
|
|
2185
|
+
unsafeDifferences: refreshAssessment.unsafeDifferences,
|
|
2186
|
+
requiredAction: 'Inspect and approve the Knowledge Map through the ordinary human flow.',
|
|
2187
|
+
},
|
|
2188
|
+
}
|
|
2189
|
+
: {}),
|
|
2443
2190
|
});
|
|
2444
2191
|
}
|
|
2445
2192
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
@@ -2457,10 +2204,7 @@ export class WorkflowService {
|
|
|
2457
2204
|
knowledgeMapHash,
|
|
2458
2205
|
});
|
|
2459
2206
|
}
|
|
2460
|
-
|
|
2461
|
-
? readCurrentPlanRiskAudit(this.store, activeTask)
|
|
2462
|
-
: null;
|
|
2463
|
-
return withAdoption(this.nextForTaskWithDelegation(status.projectId, activeTask, planRiskAudit));
|
|
2207
|
+
return withAdoption(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null));
|
|
2464
2208
|
}
|
|
2465
2209
|
const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
2466
2210
|
if (activeMilestone) {
|
|
@@ -2545,7 +2289,7 @@ export class WorkflowService {
|
|
|
2545
2289
|
}
|
|
2546
2290
|
return withAdoption({ scope: 'repository', status: 'idle', action: 'discovery start' });
|
|
2547
2291
|
}
|
|
2548
|
-
delegatedAuthorization(projectId, grantId, transition, actor, target) {
|
|
2292
|
+
delegatedAuthorization(projectId, grantId, transition, actor, target, autonomyTaskId = null) {
|
|
2549
2293
|
if (!grantId) {
|
|
2550
2294
|
if (actor.trim().startsWith('agent:')) {
|
|
2551
2295
|
throw new WorkflowError('TRANSITION_BLOCKED', 'An agent actor requires an explicit delegated approval grant.');
|
|
@@ -2553,6 +2297,52 @@ export class WorkflowService {
|
|
|
2553
2297
|
return null;
|
|
2554
2298
|
}
|
|
2555
2299
|
const grant = this.store.readDelegation(projectId, grantId);
|
|
2300
|
+
if (grant.scope.kind === 'milestone') {
|
|
2301
|
+
const autonomyContract = readMilestoneAutonomyEvents(this.store, projectId, grant.scope.id)
|
|
2302
|
+
.find((event) => event.delegationGrantId === grant.id) ?? null;
|
|
2303
|
+
if (autonomyContract) {
|
|
2304
|
+
const contextTask = target.kind === 'knowledge-map' && autonomyTaskId
|
|
2305
|
+
? this.store.readTask(projectId, autonomyTaskId)
|
|
2306
|
+
: null;
|
|
2307
|
+
const milestone = target.kind === 'milestone'
|
|
2308
|
+
? target
|
|
2309
|
+
: target.kind === 'task' && target.milestoneId === grant.scope.id
|
|
2310
|
+
? this.store.readMilestone(projectId, grant.scope.id)
|
|
2311
|
+
: contextTask?.milestoneId === grant.scope.id
|
|
2312
|
+
? this.store.readMilestone(projectId, grant.scope.id)
|
|
2313
|
+
: null;
|
|
2314
|
+
if (!milestone) {
|
|
2315
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone autonomy grant cannot authorize this target.');
|
|
2316
|
+
}
|
|
2317
|
+
const activeAutonomy = requireActiveMilestoneAutonomy({
|
|
2318
|
+
store: this.store,
|
|
2319
|
+
projectId,
|
|
2320
|
+
milestone,
|
|
2321
|
+
actor,
|
|
2322
|
+
now: this.now(),
|
|
2323
|
+
});
|
|
2324
|
+
if (activeAutonomy.grant.id !== grant.id) {
|
|
2325
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone autonomy grant has been superseded.');
|
|
2326
|
+
}
|
|
2327
|
+
if (target.kind === 'knowledge-map') {
|
|
2328
|
+
if (transition !== 'project_memory.approve'
|
|
2329
|
+
|| !contextTask
|
|
2330
|
+
|| !taskCanRebindKnowledge(contextTask)) {
|
|
2331
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone autonomy may approve Project Memory only inside a content-only Task context refresh.');
|
|
2332
|
+
}
|
|
2333
|
+
if (!grant.transitions.includes(transition)) {
|
|
2334
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grant.id} does not allow ${transition}.`);
|
|
2335
|
+
}
|
|
2336
|
+
return {
|
|
2337
|
+
grantId: grant.id,
|
|
2338
|
+
policyHash: grant.policyHash,
|
|
2339
|
+
principal: grant.principal,
|
|
2340
|
+
delegate: grant.delegate,
|
|
2341
|
+
transition,
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2556
2346
|
return assertDelegationCanAuthorize(grant, transition, actor, target, this.now());
|
|
2557
2347
|
}
|
|
2558
2348
|
assertHandoffGrant(projectId, task, targetActor, grantId) {
|
|
@@ -2584,6 +2374,23 @@ export class WorkflowService {
|
|
|
2584
2374
|
grantExpiresAt: grant.expiresAt,
|
|
2585
2375
|
});
|
|
2586
2376
|
}
|
|
2377
|
+
if (grant.scope.kind === 'milestone') {
|
|
2378
|
+
const autonomyContract = readMilestoneAutonomyEvents(this.store, projectId, grant.scope.id)
|
|
2379
|
+
.find((event) => event.delegationGrantId === grant.id) ?? null;
|
|
2380
|
+
if (autonomyContract) {
|
|
2381
|
+
const milestone = this.store.readMilestone(projectId, grant.scope.id);
|
|
2382
|
+
const activeAutonomy = requireActiveMilestoneAutonomy({
|
|
2383
|
+
store: this.store,
|
|
2384
|
+
projectId,
|
|
2385
|
+
milestone,
|
|
2386
|
+
actor: targetActor,
|
|
2387
|
+
now: this.now(),
|
|
2388
|
+
});
|
|
2389
|
+
if (activeAutonomy.grant.id !== grant.id) {
|
|
2390
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone autonomy grant has been superseded.');
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2587
2394
|
}
|
|
2588
2395
|
nextForTaskWithDelegation(projectId, task, planRiskAudit = null) {
|
|
2589
2396
|
const transition = task.status === 'awaiting_execution_authorization'
|
|
@@ -2595,25 +2402,9 @@ export class WorkflowService {
|
|
|
2595
2402
|
? this.delegatedApprovalOptions(projectId, transition, task)
|
|
2596
2403
|
: [];
|
|
2597
2404
|
const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(projectId, task.id));
|
|
2598
|
-
const
|
|
2599
|
-
?
|
|
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)
|
|
2405
|
+
const guardedCorrectivePosture = planRiskAudit
|
|
2406
|
+
? findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit)
|
|
2614
2407
|
: null;
|
|
2615
|
-
const correctivePlanRequired = guardedCorrectiveGate?.correctiveDecision?.decision === 'continue-fix'
|
|
2616
|
-
&& !hasCorrectivePlanAuditBinding(this.store, task, guardedCorrectiveGate.correctiveDecision);
|
|
2617
2408
|
const next = nextForTask(task);
|
|
2618
2409
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
2619
2410
|
const strictStepReviewStep = planRiskAudit
|
|
@@ -2645,7 +2436,79 @@ export class WorkflowService {
|
|
|
2645
2436
|
requiredActor: handoffPosture.claimant,
|
|
2646
2437
|
},
|
|
2647
2438
|
}
|
|
2648
|
-
:
|
|
2439
|
+
: task.milestoneId && next.action === 'task start'
|
|
2440
|
+
? {
|
|
2441
|
+
action: 'task handoff-prepare',
|
|
2442
|
+
blockedAction: next.action,
|
|
2443
|
+
c1Handoff: {
|
|
2444
|
+
state: 'required',
|
|
2445
|
+
recommendedTargetActor: defaultTaskWorkerActor(task.id),
|
|
2446
|
+
targetActorDerived: true,
|
|
2447
|
+
},
|
|
2448
|
+
}
|
|
2449
|
+
: {};
|
|
2450
|
+
let guardedCorrectiveOverlay = {};
|
|
2451
|
+
if (!strictStepReviewStep && guardedCorrectivePosture) {
|
|
2452
|
+
switch (guardedCorrectivePosture.posture) {
|
|
2453
|
+
case 'decision-required':
|
|
2454
|
+
guardedCorrectiveOverlay = {
|
|
2455
|
+
action: 'task corrective-decision',
|
|
2456
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
2457
|
+
correctiveDecisionGate: {
|
|
2458
|
+
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
2459
|
+
recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
|
|
2460
|
+
auditorDerived: true,
|
|
2461
|
+
auditorMustDifferFromLatestStrictReviewer: true,
|
|
2462
|
+
allowedDecision: 'continue-fix',
|
|
2463
|
+
escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
|
|
2464
|
+
humanApprovalRequired: false,
|
|
2465
|
+
},
|
|
2466
|
+
};
|
|
2467
|
+
break;
|
|
2468
|
+
case 'hard-stop':
|
|
2469
|
+
guardedCorrectiveOverlay = {
|
|
2470
|
+
action: 'task corrective-decision',
|
|
2471
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
2472
|
+
correctiveDecisionGate: {
|
|
2473
|
+
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
2474
|
+
recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
|
|
2475
|
+
auditorDerived: true,
|
|
2476
|
+
auditorMustDifferFromLatestStrictReviewer: true,
|
|
2477
|
+
allowedDecisions: ['split-required', 'stop-escalate'],
|
|
2478
|
+
hardStop: true,
|
|
2479
|
+
humanApprovalRequired: false,
|
|
2480
|
+
},
|
|
2481
|
+
};
|
|
2482
|
+
break;
|
|
2483
|
+
case 'replan-required':
|
|
2484
|
+
guardedCorrectiveOverlay = {
|
|
2485
|
+
action: 'task plan-set',
|
|
2486
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
2487
|
+
correctiveDecisionGate: {
|
|
2488
|
+
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
2489
|
+
decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
|
|
2490
|
+
decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
|
|
2491
|
+
requiredAction: 'Record an actual new Plan posture before another remediation run.',
|
|
2492
|
+
},
|
|
2493
|
+
};
|
|
2494
|
+
break;
|
|
2495
|
+
case 'split-required':
|
|
2496
|
+
case 'stop-escalate':
|
|
2497
|
+
guardedCorrectiveOverlay = {
|
|
2498
|
+
action: 'doctor',
|
|
2499
|
+
stepId: guardedCorrectivePosture.stepId,
|
|
2500
|
+
correctiveDecisionGate: {
|
|
2501
|
+
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
2502
|
+
decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
|
|
2503
|
+
decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
|
|
2504
|
+
humanApprovalRequired: false,
|
|
2505
|
+
},
|
|
2506
|
+
};
|
|
2507
|
+
break;
|
|
2508
|
+
default:
|
|
2509
|
+
break;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2649
2512
|
const result = {
|
|
2650
2513
|
...next,
|
|
2651
2514
|
...handoffOverlay,
|
|
@@ -2686,53 +2549,8 @@ export class WorkflowService {
|
|
|
2686
2549
|
},
|
|
2687
2550
|
}
|
|
2688
2551
|
: {}),
|
|
2689
|
-
...
|
|
2690
|
-
|
|
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 } : {}),
|
|
2552
|
+
...guardedCorrectiveOverlay,
|
|
2553
|
+
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
2736
2554
|
};
|
|
2737
2555
|
if (handoffPosture.state === 'pending') {
|
|
2738
2556
|
return {
|
|
@@ -2745,7 +2563,16 @@ export class WorkflowService {
|
|
|
2745
2563
|
}
|
|
2746
2564
|
delegatedApprovalOptions(projectId, transition, target) {
|
|
2747
2565
|
return this.store.listDelegations(projectId)
|
|
2748
|
-
.filter((grant) =>
|
|
2566
|
+
.filter((grant) => {
|
|
2567
|
+
try {
|
|
2568
|
+
return Boolean(this.delegatedAuthorization(projectId, grant.id, transition, grant.delegate, target));
|
|
2569
|
+
}
|
|
2570
|
+
catch (error) {
|
|
2571
|
+
if (error instanceof WorkflowError && error.code === 'TRANSITION_BLOCKED')
|
|
2572
|
+
return false;
|
|
2573
|
+
throw error;
|
|
2574
|
+
}
|
|
2575
|
+
})
|
|
2749
2576
|
.map((grant) => ({
|
|
2750
2577
|
grantId: grant.id,
|
|
2751
2578
|
principal: grant.principal,
|
|
@@ -2756,10 +2583,22 @@ export class WorkflowService {
|
|
|
2756
2583
|
policyHash: grant.policyHash,
|
|
2757
2584
|
}));
|
|
2758
2585
|
}
|
|
2759
|
-
delegatedContextRefreshOptions(projectId, task, knowledgeMap) {
|
|
2586
|
+
delegatedContextRefreshOptions(projectId, task, knowledgeMap, mode) {
|
|
2760
2587
|
return this.store.listDelegations(projectId)
|
|
2761
|
-
.filter((grant) =>
|
|
2762
|
-
|
|
2588
|
+
.filter((grant) => {
|
|
2589
|
+
try {
|
|
2590
|
+
if (mode === 'delegated-plan-bounded-supporting-source') {
|
|
2591
|
+
this.assertPlanBoundedSupportingSourceGrant(projectId, grant.id, grant.delegate, task);
|
|
2592
|
+
}
|
|
2593
|
+
return Boolean(this.delegatedAuthorization(projectId, grant.id, 'project_memory.approve', grant.delegate, knowledgeMap, task.id)
|
|
2594
|
+
&& this.delegatedAuthorization(projectId, grant.id, 'task.execution_authorize', grant.delegate, task));
|
|
2595
|
+
}
|
|
2596
|
+
catch (error) {
|
|
2597
|
+
if (error instanceof WorkflowError && error.code === 'TRANSITION_BLOCKED')
|
|
2598
|
+
return false;
|
|
2599
|
+
throw error;
|
|
2600
|
+
}
|
|
2601
|
+
})
|
|
2763
2602
|
.map((grant) => ({
|
|
2764
2603
|
grantId: grant.id,
|
|
2765
2604
|
principal: grant.principal,
|
|
@@ -2771,6 +2610,23 @@ export class WorkflowService {
|
|
|
2771
2610
|
policyHash: grant.policyHash,
|
|
2772
2611
|
}));
|
|
2773
2612
|
}
|
|
2613
|
+
assertPlanBoundedSupportingSourceGrant(projectId, grantId, actor, task) {
|
|
2614
|
+
const grant = this.store.readDelegation(projectId, grantId);
|
|
2615
|
+
if (!task.milestoneId || grant.scope.kind !== 'milestone' || grant.scope.id !== task.milestoneId) {
|
|
2616
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-bounded supporting-source refresh requires the active Task Milestone autonomy grant.', { taskId: task.id, taskMilestoneId: task.milestoneId, grantScope: grant.scope });
|
|
2617
|
+
}
|
|
2618
|
+
const milestone = this.store.readMilestone(projectId, task.milestoneId);
|
|
2619
|
+
const autonomy = requireActiveMilestoneAutonomy({
|
|
2620
|
+
store: this.store,
|
|
2621
|
+
projectId,
|
|
2622
|
+
milestone,
|
|
2623
|
+
actor,
|
|
2624
|
+
now: this.now(),
|
|
2625
|
+
});
|
|
2626
|
+
if (autonomy.grant.id !== grant.id) {
|
|
2627
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone autonomy grant has been superseded.');
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2774
2630
|
assertDelegationScopeExists(projectId, policy) {
|
|
2775
2631
|
if (policy.scope.kind === 'task') {
|
|
2776
2632
|
this.store.readTask(projectId, policy.scope.id);
|
|
@@ -3248,67 +3104,6 @@ function validateCorrectivePlanAudit(task, audit, failedReviewAttempts) {
|
|
|
3248
3104
|
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective Auditor decision ${audit.decision} requires coordinator/user redirection before implementation continues.`, { decision: audit.decision, summary: audit.summary });
|
|
3249
3105
|
}
|
|
3250
3106
|
}
|
|
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
|
-
}
|
|
3312
3107
|
function appendCorrectivePlanAudit(taskRoot, previous, saved, failedReviewAttempts, audit, now) {
|
|
3313
3108
|
appendFileSync(path.join(taskRoot, 'corrective-plan-audits.jsonl'), `${JSON.stringify({
|
|
3314
3109
|
recordedAt: now.toISOString(),
|
|
@@ -3320,11 +3115,6 @@ function appendCorrectivePlanAudit(taskRoot, previous, saved, failedReviewAttemp
|
|
|
3320
3115
|
...audit,
|
|
3321
3116
|
})}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
3322
3117
|
}
|
|
3323
|
-
function assertContentOnlyKnowledgeRefresh(approved, inspected) {
|
|
3324
|
-
if (!isContentOnlyKnowledgeRefresh(approved, inspected)) {
|
|
3325
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh is limited to content-hash drift. Knowledge sources, categories, scopes, authorities, gaps, or conflicts changed.', { requiredAction: 'Run project-memory reconcile, inspect the classification diff, and approve it explicitly.' });
|
|
3326
|
-
}
|
|
3327
|
-
}
|
|
3328
3118
|
function isContentOnlyKnowledgeRefresh(approved, inspected) {
|
|
3329
3119
|
const classification = (map) => JSON.stringify({
|
|
3330
3120
|
entries: map.entries.map((entry) => ({
|
|
@@ -3340,6 +3130,107 @@ function isContentOnlyKnowledgeRefresh(approved, inspected) {
|
|
|
3340
3130
|
});
|
|
3341
3131
|
return classification(approved) === classification(inspected);
|
|
3342
3132
|
}
|
|
3133
|
+
function assertDelegatedKnowledgeRefresh(assessment) {
|
|
3134
|
+
if (assessment.eligible)
|
|
3135
|
+
return;
|
|
3136
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Knowledge drift is outside the atomic delegated context-refresh predicate: it is neither content-hash drift nor an exact Plan-bounded supporting-source addition.', {
|
|
3137
|
+
unsafeDifferences: assessment.unsafeDifferences,
|
|
3138
|
+
requiredAction: 'Run project-memory reconcile, inspect the classification diff, and approve it explicitly.',
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
function assessDelegatedKnowledgeRefresh(approved, inspected, task, currentPlanArtifactHash) {
|
|
3142
|
+
if (isContentOnlyKnowledgeRefresh(approved, inspected)) {
|
|
3143
|
+
return {
|
|
3144
|
+
eligible: true,
|
|
3145
|
+
mode: 'delegated-content-only',
|
|
3146
|
+
addedSources: [],
|
|
3147
|
+
unsafeDifferences: [],
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
const unsafeDifferences = [];
|
|
3151
|
+
const sourceKey = (source) => `${source.category}\0${source.path}\0${source.scope}`;
|
|
3152
|
+
const approvedByKey = new Map(approved.entries.map((source) => [sourceKey(source), source]));
|
|
3153
|
+
const inspectedByKey = new Map(inspected.entries.map((source) => [sourceKey(source), source]));
|
|
3154
|
+
const inspectedByPath = new Map(inspected.entries.map((source) => [source.path, source]));
|
|
3155
|
+
for (const source of approved.entries) {
|
|
3156
|
+
const candidate = inspectedByKey.get(sourceKey(source));
|
|
3157
|
+
if (!candidate) {
|
|
3158
|
+
const samePath = inspectedByPath.get(source.path);
|
|
3159
|
+
unsafeDifferences.push(samePath
|
|
3160
|
+
? `Existing source ${source.path} changed category or scope from ${source.category}/${source.scope} to ${samePath.category}/${samePath.scope}.`
|
|
3161
|
+
: `Previously approved source was removed: ${source.path}.`);
|
|
3162
|
+
continue;
|
|
3163
|
+
}
|
|
3164
|
+
if (candidate.authority !== source.authority) {
|
|
3165
|
+
unsafeDifferences.push(`Existing source ${source.path} changed authority from ${source.authority} to ${candidate.authority}.`);
|
|
3166
|
+
}
|
|
3167
|
+
if (candidate.id !== source.id || candidate.discoveredBy !== source.discoveredBy) {
|
|
3168
|
+
unsafeDifferences.push(`Existing source ${source.path} changed classification identity.`);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
const addedSources = inspected.entries.filter((source) => !approvedByKey.has(sourceKey(source)));
|
|
3172
|
+
const currentExecutionAuthorization = [...task.authorizations].reverse().find((authorization) => authorization.kind === 'execution'
|
|
3173
|
+
&& authorization.decision === 'approved'
|
|
3174
|
+
&& authorization.planHash === task.planHash
|
|
3175
|
+
&& authorization.briefHash === task.briefHash);
|
|
3176
|
+
if (!task.planHash || !currentExecutionAuthorization) {
|
|
3177
|
+
unsafeDifferences.push('The current Task Plan was not execution-authorized before Knowledge drift.');
|
|
3178
|
+
}
|
|
3179
|
+
if (!task.planHash || currentPlanArtifactHash !== task.planHash) {
|
|
3180
|
+
unsafeDifferences.push('The current Task Plan artifact does not match its authorized state binding.');
|
|
3181
|
+
}
|
|
3182
|
+
if (task.knowledgeImpact !== 'create') {
|
|
3183
|
+
unsafeDifferences.push(`Task Plan knowledgeImpact is ${task.knowledgeImpact ?? 'unset'}, not create.`);
|
|
3184
|
+
}
|
|
3185
|
+
const declaredTargets = new Set();
|
|
3186
|
+
for (const target of task.knowledgeTargets) {
|
|
3187
|
+
let normalized;
|
|
3188
|
+
try {
|
|
3189
|
+
normalized = normalizeRelativePath(target.path);
|
|
3190
|
+
}
|
|
3191
|
+
catch {
|
|
3192
|
+
unsafeDifferences.push(`Task Plan Knowledge target is not an exact repository-relative path: ${target.path}.`);
|
|
3193
|
+
continue;
|
|
3194
|
+
}
|
|
3195
|
+
if (normalized !== target.path || /[*?\[\]{}]/.test(target.path) || target.path.endsWith('/')) {
|
|
3196
|
+
unsafeDifferences.push(`Task Plan Knowledge target is wildcard, directory-like, or not normalized: ${target.path}.`);
|
|
3197
|
+
continue;
|
|
3198
|
+
}
|
|
3199
|
+
declaredTargets.add(`${target.category}\0${target.path}`);
|
|
3200
|
+
}
|
|
3201
|
+
if (addedSources.length === 0) {
|
|
3202
|
+
unsafeDifferences.push('No new Knowledge source matches the Plan-bounded addition path.');
|
|
3203
|
+
}
|
|
3204
|
+
for (const source of addedSources) {
|
|
3205
|
+
if (source.authority !== 'supporting') {
|
|
3206
|
+
unsafeDifferences.push(`New source ${source.path} has forbidden ${source.authority} authority.`);
|
|
3207
|
+
}
|
|
3208
|
+
if (!declaredTargets.has(`${source.category}\0${source.path}`)) {
|
|
3209
|
+
unsafeDifferences.push(`New source ${source.path} (${source.category}) was not declared by the authorized Task Plan.`);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
if (JSON.stringify(approved.gaps) !== JSON.stringify(inspected.gaps)) {
|
|
3213
|
+
unsafeDifferences.push('Project Knowledge gaps changed.');
|
|
3214
|
+
}
|
|
3215
|
+
if (JSON.stringify(approved.conflicts) !== JSON.stringify(inspected.conflicts)) {
|
|
3216
|
+
unsafeDifferences.push('Project Knowledge conflicts changed.');
|
|
3217
|
+
}
|
|
3218
|
+
if (inspected.conflicts.length > 0) {
|
|
3219
|
+
unsafeDifferences.push('The inspected Project Knowledge Map contains conflicts.');
|
|
3220
|
+
}
|
|
3221
|
+
return {
|
|
3222
|
+
eligible: unsafeDifferences.length === 0,
|
|
3223
|
+
mode: 'delegated-plan-bounded-supporting-source',
|
|
3224
|
+
addedSources: addedSources.map(({ id, path: sourcePath, category, scope, authority }) => ({
|
|
3225
|
+
id,
|
|
3226
|
+
path: sourcePath,
|
|
3227
|
+
category,
|
|
3228
|
+
scope,
|
|
3229
|
+
authority,
|
|
3230
|
+
})),
|
|
3231
|
+
unsafeDifferences: [...new Set(unsafeDifferences)],
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3343
3234
|
function validateMilestonePlan(plan) {
|
|
3344
3235
|
if (!plan.outcome.trim())
|
|
3345
3236
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone outcome is required.');
|