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.
Files changed (48) hide show
  1. package/README.md +43 -71
  2. package/dist/src/alpha6/handoff.d.ts +1 -0
  3. package/dist/src/alpha6/handoff.js +6 -0
  4. package/dist/src/alpha6/handoff.js.map +1 -1
  5. package/dist/src/alpha6/milestone.d.ts +3 -0
  6. package/dist/src/alpha6/milestone.js +57 -3
  7. package/dist/src/alpha6/milestone.js.map +1 -1
  8. package/dist/src/alpha6/plan-risk.js +61 -0
  9. package/dist/src/alpha6/plan-risk.js.map +1 -1
  10. package/dist/src/alpha6/remediation.d.ts +4 -20
  11. package/dist/src/alpha6/remediation.js +73 -150
  12. package/dist/src/alpha6/remediation.js.map +1 -1
  13. package/dist/src/alpha7/autonomy.d.ts +61 -0
  14. package/dist/src/alpha7/autonomy.js +256 -0
  15. package/dist/src/alpha7/autonomy.js.map +1 -0
  16. package/dist/src/cli.js +23 -16
  17. package/dist/src/cli.js.map +1 -1
  18. package/dist/src/contracts.d.ts +11 -6
  19. package/dist/src/index.d.ts +3 -0
  20. package/dist/src/index.js +3 -0
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/state/lock.d.ts +1 -3
  23. package/dist/src/state/lock.js +2 -5
  24. package/dist/src/state/lock.js.map +1 -1
  25. package/dist/src/version.d.ts +1 -1
  26. package/dist/src/version.js +1 -1
  27. package/dist/src/workflow.d.ts +21 -65
  28. package/dist/src/workflow.js +548 -657
  29. package/dist/src/workflow.js.map +1 -1
  30. package/docs/alpha7.1-implementation-brief.md +268 -0
  31. package/docs/autonomy-guardrails.md +56 -20
  32. package/docs/decisions.md +16 -4
  33. package/docs/delegated-approval.md +12 -5
  34. package/docs/development-flow.md +12 -7
  35. package/docs/release.md +4 -6
  36. package/docs/updating-existing-project.md +9 -39
  37. package/docs/validation-report.md +50 -63
  38. package/package.json +2 -2
  39. package/plugins/codex-workflow-gateway/references/protocol.md +53 -17
  40. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +33 -9
  41. package/references/state-machine.md +31 -13
  42. package/roles/delivery-coordinator.md +10 -1
  43. package/roles/technical-planner.md +3 -0
  44. package/schemas/context-envelope.schema.json +0 -10
  45. package/schemas/milestone-autonomy-event.schema.json +45 -0
  46. package/schemas/milestone-scope-change-event.schema.json +3 -1
  47. package/schemas/plan-risk-audit-event.schema.json +21 -4
  48. package/schemas/corrective-plan-audit-record.schema.json +0 -47
@@ -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, delegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
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 { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, readTaskC1Posture, } from './alpha6/handoff.js';
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 { appendCorrectivePlanAuditRecord, appendCorrectiveDecisionEvent, appendRemediationEvent, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, findFailedGuardedStepRequiringCorrectiveDecision, hasCorrectivePlanAuditBinding, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
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, readReviewerAttestations, } from './alpha6/review.js';
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
- alpha6CorrectiveRescueHead;
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
- const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
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', 'ready', 'needs_fix', 'blocked'].includes(task.status)) {
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
- const currentPlanRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
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 = ['needs_fix', 'awaiting_execution_authorization', 'ready'].includes(task.status)
604
- && currentPlanRiskAudit
678
+ const guardedRemediationGate = task.status === 'needs_fix' && currentPlanRiskAudit
605
679
  ? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
606
680
  : null;
607
- if (guardedRemediationGate && !guardedRemediationGate.correctiveDecision) {
608
- throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a separate corrective decision before a corrective Plan can be applied.`, {
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
- const guardedContinueFixDecision = guardedRemediationGate?.correctiveDecision?.decision === 'continue-fix'
626
- ? guardedRemediationGate.correctiveDecision
627
- : null;
628
- if (task.status === 'ready' && !guardedContinueFixDecision) {
629
- throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
630
- }
631
- if (guardedRemediationGate?.correctiveDecision && !guardedContinueFixDecision && currentReplanDecisions.length === 0) {
632
- throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision ${guardedRemediationGate.correctiveDecision.decision} prohibits continue-fix Plan application.`, {
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
- decisionEventId: guardedRemediationGate.correctiveDecision.eventId,
702
+ decision: correctiveAudit?.decision ?? null,
636
703
  });
637
704
  }
638
- if (guardedContinueFixDecision) {
639
- validateGuardedCorrectivePlanAudit(this.store, task, guardedRemediationGate.stepId, guardedContinueFixDecision, correctiveAudit);
640
- }
641
- if (guardedContinueFixDecision) {
642
- if (!sameOrderedStrings(plan.requirements, task.requirements)) {
643
- throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan requirements must exactly preserve Task scope.', {
644
- expected: task.requirements,
645
- actual: plan.requirements,
646
- });
647
- }
648
- if (!sameOrderedStrings(plan.acceptance, task.acceptance)) {
649
- throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan acceptance must exactly preserve Task scope.', {
650
- expected: task.acceptance,
651
- actual: plan.acceptance,
652
- });
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
- else {
656
- for (const requirement of task.requirements) {
657
- if (!plan.requirements.includes(requirement)) {
658
- throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task requirement ${requirement}.`);
659
- }
660
- }
661
- for (const acceptance of task.acceptance) {
662
- if (!plan.acceptance.includes(acceptance)) {
663
- throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task acceptance ${acceptance}.`);
664
- }
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 (guardedContinueFixDecision) {
706
- assertNonTargetStepsUnchanged(task, plan, guardedRemediationGate.stepId);
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
- const effectivePlanRiskAudit = planRiskAudit ?? (guardedContinueFixDecision ? currentPlanRiskAudit?.audit ?? null : null);
730
- if (effectivePlanRiskAudit)
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 (effectivePlanRiskAudit)
735
- appendCurrentPlanRiskAudit(this.store, saved, effectivePlanRiskAudit, this.now());
736
- if (guardedContinueFixDecision && correctiveAudit) {
737
- appendCorrectivePlanAuditRecord(this.store, task, saved, guardedRemediationGate.stepId, guardedContinueFixDecision, correctiveAudit, this.now());
738
- }
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 breakerPlanRiskAuditWillBePreserved = currentPlanRiskAudit
853
- && findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit) !== null;
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 || inspectedMap.status !== 'stale') {
860
- throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with content-only drift.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
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
- assertContentOnlyKnowledgeRefresh(currentMap, inspectedMap);
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.approveKnowledgeMap(repository, knowledgeMap.revision, [], actor, delegationGrantId);
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 && !breakerPlanRiskAuditWillBePreserved) {
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, 'Delegated content-only context refresh.', delegationGrantId);
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: 'delegated-content-only',
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
- const remediationGate = planRiskAudit && isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)
1158
- ? assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit)
1159
- : null;
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, allowHistoricalStrictReview = false) {
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 = allowHistoricalStrictReview
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', allowStaleKnowledgeBinding);
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 (!['needs_fix', 'awaiting_execution_authorization', 'ready'].includes(task.status)) {
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
- if (step.status !== 'failed') {
1800
- throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a currently failed guarded Step.`, {
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
- alpha6CorrectiveRescuePreflight(repository) {
2075
- const identity = resolveRepositoryIdentity(repository);
2076
- const locks = new WriterLockManager(this.store.lockRoot(identity.projectId), this.now, { ensureRoot: false });
2077
- const dependency = inspectLocalDependency(identity.repositoryRoot);
2078
- const tasks = this.store.listTasks(identity.projectId);
2079
- const leases = locks.list();
2080
- const dirty = changedFiles(identity.repositoryRoot);
2081
- const currentHead = headCommit(identity.repositoryRoot);
2082
- const candidates = [];
2083
- const malformed = [];
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.revision !== 14 || task.status !== 'ready')
1943
+ if (task.status !== 'in_progress')
2086
1944
  continue;
2087
- const step = task.steps.find((entry) => entry.id === 'STEP-001') ?? null;
2088
- if (!step || step.status !== 'failed' || !task.planHash)
1945
+ const planRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
1946
+ if (!planRiskAudit)
2089
1947
  continue;
2090
- try {
2091
- const root = this.store.taskRoot(task.projectId, task.id);
2092
- if (this.store.hashArtifact(root, 'brief.md') !== task.briefHash) {
2093
- throw new WorkflowError('STATE_CORRUPT', 'Brief artifact changed after the recovery boundary.');
2094
- }
2095
- const discovery = this.store.readDiscovery(task.projectId, task.discoveryId);
2096
- if (sha256Hex(renderBrief(discovery, task.title)) !== task.briefHash) {
2097
- throw new WorkflowError('STATE_CORRUPT', 'Recovery Task Brief no longer matches its immutable Discovery scope.');
2098
- }
2099
- if (this.store.hashArtifact(root, 'plan.md') !== task.planHash) {
2100
- throw new WorkflowError('STATE_CORRUPT', 'Plan artifact does not match the recovery Task binding.');
2101
- }
2102
- const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
2103
- if (!planRiskAudit || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
2104
- throw new WorkflowError('STATE_CORRUPT', 'Recovery Step is not guarded by the exact current Plan Risk Audit.');
2105
- }
2106
- const allRemediationEvents = readRemediationEvents(this.store, task);
2107
- const remediationEvents = allRemediationEvents.filter((event) => event.stepId === step.id);
2108
- if (allRemediationEvents.length !== 2
2109
- || remediationEvents.length !== allRemediationEvents.length
2110
- || remediationEvents.some((event, index) => event.attemptOrdinal !== index + 1
2111
- || event.mode !== 'ordinary'
2112
- || event.failureKind !== 'review-failed'
2113
- || event.failureEvidence.reviewEventHash === null
2114
- || event.failureEvidence.reviewerAttestationHash === null)) {
2115
- throw new WorkflowError('STATE_CORRUPT', 'Recovery requires exactly two ordinary verified strict-review failures.');
2116
- }
2117
- const failureIdentities = new Set(remediationEvents.map((event) => [
2118
- event.failureEvidence.completionCommit,
2119
- event.failureEvidence.reviewEventHash,
2120
- event.failureEvidence.reviewerAttestationHash,
2121
- ].join('\0')));
2122
- if (failureIdentities.size !== 2) {
2123
- throw new WorkflowError('STATE_CORRUPT', 'Recovery strict-review failures are not distinct.');
2124
- }
2125
- const attestations = readReviewerAttestations(this.store, task);
2126
- if (remediationEvents.some((event) => {
2127
- const attestation = attestations.find((entry) => entry.eventHash === event.failureEvidence.reviewerAttestationHash);
2128
- return !attestation || attestation.isolationResult !== 'verified';
2129
- })) {
2130
- throw new WorkflowError('STATE_CORRUPT', 'Recovery strict-review failures lack verified reviewer isolation.');
2131
- }
2132
- if (readCorrectiveDecisionEvents(this.store, task).some((event) => event.stepId === step.id)) {
2133
- throw new WorkflowError('STATE_CORRUPT', 'Recovery Task already has a corrective decision.');
2134
- }
2135
- const posture = readTaskC1Posture(this.store, task);
2136
- if (posture.state !== 'claimed' || !posture.writerLeaseTokenHash) {
2137
- throw new WorkflowError('STATE_CORRUPT', 'Recovery Task requires claimed C1 with a bound writer lease token.');
2138
- }
2139
- const lease = leases.find((entry) => entry.entityId === task.id) ?? null;
2140
- if (!lease
2141
- || lease.owner !== posture.claimant
2142
- || sha256Hex(lease.token) !== posture.writerLeaseTokenHash) {
2143
- throw new WorkflowError('STATE_CORRUPT', 'Recovery lease does not exactly match the C1 claimant/token binding.');
2144
- }
2145
- const { token: _secret, ...publicLease } = lease;
2146
- let milestoneScopeHash = null;
2147
- if (task.milestoneId) {
2148
- const milestone = this.store.readMilestone(task.projectId, task.milestoneId);
2149
- const matchingMemberships = milestone.memberships.filter((entry) => entry.taskId === task.id);
2150
- if (matchingMemberships.length !== 1 || !milestone.taskIds.includes(task.id)) {
2151
- throw new WorkflowError('STATE_CORRUPT', 'Recovery Task no longer matches exact Milestone membership scope.');
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
- milestoneScopeHash = milestoneScopeDigest(milestone);
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 = candidates.length === 1 ? candidates[0] : null;
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
- ...(currentHead !== this.alpha6CorrectiveRescueHead
2184
- ? [`HEAD must exactly equal ${this.alpha6CorrectiveRescueHead}.`]
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
- ...(candidates.length !== 1
2187
- ? [`Expected exactly one alpha.6 corrective recovery candidate; found ${candidates.length}.`]
1994
+ ...(candidate && candidate.currentHead !== candidate.completionCommit
1995
+ ? ['HEAD does not match the pending strict review completion commit.']
2188
1996
  : []),
2189
- ...(candidate && leases.some((lease) => lease.entityId !== candidate.taskId)
2190
- ? ['An unrelated writer lease exists outside the recovery Task.']
1997
+ ...(staleLeases.some((lease) => !candidateLeaseIds.has(lease.entityId))
1998
+ ? ['A stale writer lease exists outside the rescue Task.']
2191
1999
  : []),
2192
- ...malformed.map((message) => `Corrective recovery candidate is invalid: ${message}`),
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: blockers.length === 0 && candidate !== null,
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
- requiredHead: this.alpha6CorrectiveRescueHead,
2201
- currentHead,
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
- const planRiskAudit = activeTask.planHash
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 contextRefreshOptions = action === 'project-memory reconcile'
2422
- && storedKnowledgeMap.status === 'active'
2155
+ const refreshAssessment = storedKnowledgeMap.status === 'active'
2423
2156
  && storedKnowledgeMap.approval
2424
- && isContentOnlyKnowledgeRefresh(storedKnowledgeMap, knowledgeMap)
2425
- ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap)
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: 'delegated-content-only',
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
- const planRiskAudit = activeTask.planHash && activeTask.status !== 'planning'
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 guardedFailedSteps = planRiskAudit
2599
- ? task.steps.filter((step) => step.status === 'failed' && planRiskAudit.reviewRequiredStepIds.includes(step.id))
2600
- : [];
2601
- const breakerSteps = guardedFailedSteps.map((step) => ({
2602
- step,
2603
- attemptCount: readRemediationEvents(this.store, task).filter((event) => event.stepId === step.id).length,
2604
- })).filter((entry) => entry.attemptCount >= 2);
2605
- if (breakerSteps.length > 1) {
2606
- throw new WorkflowError('STATE_CORRUPT', 'Multiple guarded failed Steps simultaneously carry remediation-breaker posture.', {
2607
- taskId: task.id,
2608
- stepIds: breakerSteps.map((entry) => entry.step.id),
2609
- });
2610
- }
2611
- const breakerStep = breakerSteps[0] ?? null;
2612
- const guardedCorrectiveGate = breakerStep && breakerStep.attemptCount === 2 && planRiskAudit
2613
- ? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, planRiskAudit)
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
- ...(guardedCorrectiveGate
2690
- ? guardedCorrectiveGate.correctiveDecision === null
2691
- ? {
2692
- action: 'task corrective-decision',
2693
- stepId: guardedCorrectiveGate.stepId,
2694
- correctiveDecisionGate: {
2695
- attemptCount: guardedCorrectiveGate.attemptCount,
2696
- auditorMustDifferFromLatestStrictReviewer: true,
2697
- allowedDecision: 'continue-fix',
2698
- escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
2699
- humanApprovalRequired: false,
2700
- },
2701
- }
2702
- : correctivePlanRequired
2703
- ? {
2704
- action: 'task plan-set with independent corrective audit',
2705
- stepId: guardedCorrectiveGate.stepId,
2706
- correctivePlanGate: {
2707
- attemptCount: guardedCorrectiveGate.attemptCount,
2708
- decisionEventId: guardedCorrectiveGate.correctiveDecision.eventId,
2709
- fullPlanRequired: true,
2710
- auditFileRequired: true,
2711
- auditorMustDifferFromDecisionAuditor: true,
2712
- auditorMustDifferFromLatestStrictReviewer: true,
2713
- allowedDecision: 'continue-fix',
2714
- },
2715
- }
2716
- : guardedCorrectiveGate.correctiveDecision.decision !== 'continue-fix'
2717
- ? {
2718
- action: 'task corrective-hard-stop',
2719
- stepId: guardedCorrectiveGate.stepId,
2720
- correctiveDecision: guardedCorrectiveGate.correctiveDecision.decision,
2721
- }
2722
- : {}
2723
- : {}),
2724
- ...(breakerStep?.attemptCount && breakerStep.attemptCount >= 3
2725
- ? {
2726
- action: 'task corrective-hard-stop',
2727
- stepId: breakerStep.step.id,
2728
- correctiveHardStop: {
2729
- failedAttemptCount: breakerStep.attemptCount,
2730
- continueFixAllowed: false,
2731
- allowedDecisions: ['split-required', 'stop-escalate'],
2732
- },
2733
- }
2734
- : {}),
2735
- ...(delegatedApprovalOptions.length > 0 && !breakerStep ? { delegatedApprovalOptions } : {}),
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) => delegationCanAuthorize(grant, transition, target, this.now()))
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) => delegationCanAuthorize(grant, 'project_memory.approve', knowledgeMap, this.now()))
2762
- .filter((grant) => delegationCanAuthorize(grant, 'task.execution_authorize', task, this.now()))
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.');