codex-workflow-v2 2.0.0-alpha.7 → 2.0.0-alpha.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,10 +12,11 @@ import { inspectLocalDependency } from './diagnostics.js';
12
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 { appendCorrectiveDecisionEvent, appendRemediationEvent, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findFailedGuardedStepRequiringCorrectiveDecision, 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
17
  import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
18
18
  import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
19
+ import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
19
20
  import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
20
21
  import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
21
22
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
@@ -879,15 +880,25 @@ export class WorkflowService {
879
880
  const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
880
881
  const transitions = [];
881
882
  const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
883
+ const currentPlanArtifactHash = task.planHash
884
+ ? this.store.hashArtifact(this.store.taskRoot(identity.projectId, task.id), 'plan.md')
885
+ : null;
886
+ const refreshAssessment = assessDelegatedKnowledgeRefresh(currentMap, inspectedMap, task, currentPlanArtifactHash);
882
887
  // Validate every delegated transition before the first write so a narrow grant cannot
883
888
  // leave the composite operation half-applied.
884
889
  this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
885
890
  let knowledgeMap = currentMap;
886
891
  if (inspectedMap.status !== 'active' || !inspectedMap.approval) {
887
- if (currentMap.status !== 'active' || !currentMap.approval || inspectedMap.status !== 'stale') {
888
- throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with content-only drift.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
892
+ if (currentMap.status !== 'active' || !currentMap.approval) {
893
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with safe delegated drift.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
894
+ }
895
+ assertDelegatedKnowledgeRefresh(refreshAssessment);
896
+ if (inspectedMap.status !== 'stale') {
897
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Safe delegated Knowledge drift must reconcile to a stale candidate before approval.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
898
+ }
899
+ if (refreshAssessment.mode === 'delegated-plan-bounded-supporting-source') {
900
+ this.assertPlanBoundedSupportingSourceGrant(identity.projectId, delegationGrantId, actor, task);
889
901
  }
890
- assertContentOnlyKnowledgeRefresh(currentMap, inspectedMap);
891
902
  this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspectedMap, task.id);
892
903
  knowledgeMap = this.reconcileKnowledgeMap(repository, currentMap.revision);
893
904
  transitions.push('project-memory reconcile');
@@ -904,11 +915,14 @@ export class WorkflowService {
904
915
  appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
905
916
  }
906
917
  transitions.push('task knowledge-rebind');
907
- refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, 'Delegated content-only context refresh.', delegationGrantId);
918
+ refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, refreshAssessment.mode === 'delegated-plan-bounded-supporting-source'
919
+ ? 'Delegated Plan-bounded supporting-source context refresh.'
920
+ : 'Delegated content-only context refresh.', delegationGrantId);
908
921
  transitions.push('task execution authorize');
909
922
  return {
910
- mode: 'delegated-content-only',
923
+ mode: refreshAssessment.mode,
911
924
  transitions,
925
+ addedSources: refreshAssessment.addedSources,
912
926
  knowledgeMap,
913
927
  task: refreshedTask,
914
928
  };
@@ -1183,7 +1197,15 @@ export class WorkflowService {
1183
1197
  });
1184
1198
  }
1185
1199
  if (planRiskAudit && isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
1186
- assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
1200
+ const guardedPosture = readGuardedRemediationPostureForStep(this.store, task, stepId, planRiskAudit);
1201
+ const staleDecision = guardedPosture?.correctiveDecision
1202
+ && guardedPosture.correctiveDecision.planHash !== task.planHash
1203
+ ? guardedPosture.correctiveDecision
1204
+ : null;
1205
+ const recovery = staleDecision
1206
+ ? requireCorrectiveDecisionRecoveryForRun(this.store, task, stepId, actor, writerToken)
1207
+ : null;
1208
+ assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
1187
1209
  }
1188
1210
  const dirty = changedFiles(context.identity.repositoryRoot);
1189
1211
  if (step.status === 'planned' && dirty.length > 0) {
@@ -1636,6 +1658,18 @@ export class WorkflowService {
1636
1658
  }
1637
1659
  return saved;
1638
1660
  }
1661
+ recoverStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, actor, writerToken) {
1662
+ const context = this.context(repository);
1663
+ const task = this.store.readTask(context.identity.projectId, taskId);
1664
+ assertExpectedRevision(task, expectedRevision);
1665
+ this.requireExecutionAdoptionPosture(context.identity, taskId);
1666
+ if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
1667
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot recover a corrective decision while ${task.status}.`);
1668
+ }
1669
+ this.assertExecutionAuthorizationFresh(task);
1670
+ this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1671
+ return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
1672
+ }
1639
1673
  recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
1640
1674
  const { identity } = this.context(repository);
1641
1675
  this.readCurrentAdoptionPostureStrict(identity.projectId);
@@ -1643,7 +1677,7 @@ export class WorkflowService {
1643
1677
  assertExpectedRevision(task, expectedRevision);
1644
1678
  this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
1645
1679
  this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
1646
- if (task.status !== 'needs_fix' && task.status !== 'blocked') {
1680
+ if (!['ready', 'needs_fix', 'blocked'].includes(task.status)) {
1647
1681
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
1648
1682
  }
1649
1683
  const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
@@ -1659,32 +1693,25 @@ export class WorkflowService {
1659
1693
  stepId,
1660
1694
  });
1661
1695
  }
1662
- const step = requireStep(task, stepId);
1663
1696
  if (!isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
1664
1697
  throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit and cannot receive a corrective decision.`, {
1665
1698
  taskId,
1666
1699
  stepId,
1667
1700
  });
1668
1701
  }
1669
- if (task.status === 'needs_fix') {
1670
- if (step.status !== 'failed') {
1671
- throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a currently failed guarded Step while task ${taskId} is needs_fix.`, {
1672
- taskId,
1673
- stepId,
1674
- stepStatus: step.status,
1675
- });
1676
- }
1677
- }
1678
- else {
1679
- const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
1680
- if (step.status !== 'in_progress' || !step.evidence || currentCycle.resolution !== 'unverified') {
1681
- throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a blocked unverified guarded Step with current evidence.`, {
1682
- taskId,
1683
- stepId,
1684
- stepStatus: step.status,
1685
- resolution: currentCycle.resolution,
1686
- });
1687
- }
1702
+ const posture = readGuardedRemediationPostureForStep(this.store, task, stepId, planRiskAudit);
1703
+ if (!posture) {
1704
+ const step = requireStep(task, stepId);
1705
+ const currentCycle = step.status === 'in_progress' && step.evidence
1706
+ ? buildCurrentStrictStepReviewCycle(this.store, task, stepId).resolution
1707
+ : null;
1708
+ throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a failed guarded Step or a blocked unverified strict-review retry posture.`, {
1709
+ taskId,
1710
+ stepId,
1711
+ taskStatus: task.status,
1712
+ stepStatus: step.status,
1713
+ resolution: currentCycle,
1714
+ });
1688
1715
  }
1689
1716
  assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
1690
1717
  return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
@@ -2146,11 +2173,14 @@ export class WorkflowService {
2146
2173
  ? this.delegatedApprovalOptions(status.projectId, 'project_memory.approve', knowledgeMap)
2147
2174
  : [];
2148
2175
  const storedKnowledgeMap = this.store.readKnowledgeMap(status.projectId);
2149
- const contextRefreshOptions = action === 'project-memory reconcile'
2150
- && storedKnowledgeMap.status === 'active'
2176
+ const refreshAssessment = storedKnowledgeMap.status === 'active'
2151
2177
  && storedKnowledgeMap.approval
2152
- && isContentOnlyKnowledgeRefresh(storedKnowledgeMap, knowledgeMap)
2153
- ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap)
2178
+ ? assessDelegatedKnowledgeRefresh(storedKnowledgeMap, knowledgeMap, activeTask, activeTask.planHash
2179
+ ? this.store.hashArtifact(this.store.taskRoot(status.projectId, activeTask.id), 'plan.md')
2180
+ : null)
2181
+ : null;
2182
+ const contextRefreshOptions = action === 'project-memory reconcile' && refreshAssessment?.eligible
2183
+ ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
2154
2184
  : [];
2155
2185
  return withAdoption({
2156
2186
  scope: 'project-memory',
@@ -2163,11 +2193,21 @@ export class WorkflowService {
2163
2193
  ? {
2164
2194
  contextRefresh: {
2165
2195
  action: 'task context-refresh',
2166
- mode: 'delegated-content-only',
2196
+ mode: refreshAssessment.mode,
2197
+ addedSources: refreshAssessment.addedSources,
2167
2198
  options: contextRefreshOptions,
2168
2199
  },
2169
2200
  }
2170
2201
  : {}),
2202
+ ...(refreshAssessment && !refreshAssessment.eligible
2203
+ ? {
2204
+ contextRefreshBlocked: {
2205
+ reason: 'Knowledge drift is outside the atomic delegated refresh predicate.',
2206
+ unsafeDifferences: refreshAssessment.unsafeDifferences,
2207
+ requiredAction: 'Inspect and approve the Knowledge Map through the ordinary human flow.',
2208
+ },
2209
+ }
2210
+ : {}),
2171
2211
  });
2172
2212
  }
2173
2213
  const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
@@ -2383,8 +2423,8 @@ export class WorkflowService {
2383
2423
  ? this.delegatedApprovalOptions(projectId, transition, task)
2384
2424
  : [];
2385
2425
  const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(projectId, task.id));
2386
- const guardedCorrectiveGate = task.status === 'needs_fix' && planRiskAudit
2387
- ? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, planRiskAudit)
2426
+ const guardedCorrectivePosture = planRiskAudit
2427
+ ? findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit)
2388
2428
  : null;
2389
2429
  const next = nextForTask(task);
2390
2430
  const handoffPosture = readTaskC1Posture(this.store, task);
@@ -2428,6 +2468,110 @@ export class WorkflowService {
2428
2468
  },
2429
2469
  }
2430
2470
  : {};
2471
+ let guardedCorrectiveOverlay = {};
2472
+ if (!strictStepReviewStep && guardedCorrectivePosture) {
2473
+ switch (guardedCorrectivePosture.posture) {
2474
+ case 'decision-required':
2475
+ if (guardedCorrectivePosture.correctiveDecision
2476
+ && guardedCorrectivePosture.correctiveDecision.planHash !== task.planHash) {
2477
+ try {
2478
+ const navigation = readValidatedCorrectiveDecisionRecoveryForNavigation(this.store, task, guardedCorrectivePosture.stepId);
2479
+ guardedCorrectiveOverlay = navigation.recovery
2480
+ ? {
2481
+ action: 'task run',
2482
+ stepId: guardedCorrectivePosture.stepId,
2483
+ correctiveDecisionRecovery: {
2484
+ state: 'validated',
2485
+ recoveryEventId: navigation.recovery.eventId,
2486
+ correctiveDecisionEventId: navigation.assessment.decision.eventId,
2487
+ },
2488
+ }
2489
+ : {
2490
+ action: 'task corrective-decision-recover',
2491
+ stepId: guardedCorrectivePosture.stepId,
2492
+ correctiveDecisionRecovery: {
2493
+ state: 'required',
2494
+ correctiveDecisionEventId: navigation.assessment.decision.eventId,
2495
+ decisionPlanHash: navigation.assessment.decision.planHash,
2496
+ reboundPlanHash: task.planHash,
2497
+ triggeringAttemptCount: navigation.assessment.decision.triggeringAttemptCount,
2498
+ requiredActor: readTaskC1Posture(this.store, task).state === 'claimed'
2499
+ ? readTaskC1Posture(this.store, task).claimant
2500
+ : null,
2501
+ },
2502
+ };
2503
+ }
2504
+ catch (error) {
2505
+ guardedCorrectiveOverlay = {
2506
+ action: 'doctor',
2507
+ stepId: guardedCorrectivePosture.stepId,
2508
+ blockedAction: 'task corrective-decision-recover',
2509
+ correctiveDecisionRecovery: {
2510
+ state: 'blocked',
2511
+ reason: error instanceof Error ? error.message : String(error),
2512
+ },
2513
+ };
2514
+ }
2515
+ break;
2516
+ }
2517
+ guardedCorrectiveOverlay = {
2518
+ action: 'task corrective-decision',
2519
+ stepId: guardedCorrectivePosture.stepId,
2520
+ correctiveDecisionGate: {
2521
+ attemptCount: guardedCorrectivePosture.attemptCount,
2522
+ recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
2523
+ auditorDerived: true,
2524
+ auditorMustDifferFromLatestStrictReviewer: true,
2525
+ allowedDecision: 'continue-fix',
2526
+ escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
2527
+ humanApprovalRequired: false,
2528
+ },
2529
+ };
2530
+ break;
2531
+ case 'hard-stop':
2532
+ guardedCorrectiveOverlay = {
2533
+ action: 'task corrective-decision',
2534
+ stepId: guardedCorrectivePosture.stepId,
2535
+ correctiveDecisionGate: {
2536
+ attemptCount: guardedCorrectivePosture.attemptCount,
2537
+ recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
2538
+ auditorDerived: true,
2539
+ auditorMustDifferFromLatestStrictReviewer: true,
2540
+ allowedDecisions: ['split-required', 'stop-escalate'],
2541
+ hardStop: true,
2542
+ humanApprovalRequired: false,
2543
+ },
2544
+ };
2545
+ break;
2546
+ case 'replan-required':
2547
+ guardedCorrectiveOverlay = {
2548
+ action: 'task plan-set',
2549
+ stepId: guardedCorrectivePosture.stepId,
2550
+ correctiveDecisionGate: {
2551
+ attemptCount: guardedCorrectivePosture.attemptCount,
2552
+ decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
2553
+ decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
2554
+ requiredAction: 'Record an actual new Plan posture before another remediation run.',
2555
+ },
2556
+ };
2557
+ break;
2558
+ case 'split-required':
2559
+ case 'stop-escalate':
2560
+ guardedCorrectiveOverlay = {
2561
+ action: 'doctor',
2562
+ stepId: guardedCorrectivePosture.stepId,
2563
+ correctiveDecisionGate: {
2564
+ attemptCount: guardedCorrectivePosture.attemptCount,
2565
+ decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
2566
+ decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
2567
+ humanApprovalRequired: false,
2568
+ },
2569
+ };
2570
+ break;
2571
+ default:
2572
+ break;
2573
+ }
2574
+ }
2431
2575
  const result = {
2432
2576
  ...next,
2433
2577
  ...handoffOverlay,
@@ -2468,21 +2612,7 @@ export class WorkflowService {
2468
2612
  },
2469
2613
  }
2470
2614
  : {}),
2471
- ...(guardedCorrectiveGate
2472
- ? {
2473
- action: 'task corrective-decision',
2474
- stepId: guardedCorrectiveGate.stepId,
2475
- correctiveDecisionGate: {
2476
- attemptCount: guardedCorrectiveGate.attemptCount,
2477
- recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
2478
- auditorDerived: true,
2479
- auditorMustDifferFromLatestStrictReviewer: true,
2480
- allowedDecision: 'continue-fix',
2481
- escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
2482
- humanApprovalRequired: false,
2483
- },
2484
- }
2485
- : {}),
2615
+ ...guardedCorrectiveOverlay,
2486
2616
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
2487
2617
  };
2488
2618
  if (handoffPosture.state === 'pending') {
@@ -2516,10 +2646,13 @@ export class WorkflowService {
2516
2646
  policyHash: grant.policyHash,
2517
2647
  }));
2518
2648
  }
2519
- delegatedContextRefreshOptions(projectId, task, knowledgeMap) {
2649
+ delegatedContextRefreshOptions(projectId, task, knowledgeMap, mode) {
2520
2650
  return this.store.listDelegations(projectId)
2521
2651
  .filter((grant) => {
2522
2652
  try {
2653
+ if (mode === 'delegated-plan-bounded-supporting-source') {
2654
+ this.assertPlanBoundedSupportingSourceGrant(projectId, grant.id, grant.delegate, task);
2655
+ }
2523
2656
  return Boolean(this.delegatedAuthorization(projectId, grant.id, 'project_memory.approve', grant.delegate, knowledgeMap, task.id)
2524
2657
  && this.delegatedAuthorization(projectId, grant.id, 'task.execution_authorize', grant.delegate, task));
2525
2658
  }
@@ -2540,6 +2673,23 @@ export class WorkflowService {
2540
2673
  policyHash: grant.policyHash,
2541
2674
  }));
2542
2675
  }
2676
+ assertPlanBoundedSupportingSourceGrant(projectId, grantId, actor, task) {
2677
+ const grant = this.store.readDelegation(projectId, grantId);
2678
+ if (!task.milestoneId || grant.scope.kind !== 'milestone' || grant.scope.id !== task.milestoneId) {
2679
+ 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 });
2680
+ }
2681
+ const milestone = this.store.readMilestone(projectId, task.milestoneId);
2682
+ const autonomy = requireActiveMilestoneAutonomy({
2683
+ store: this.store,
2684
+ projectId,
2685
+ milestone,
2686
+ actor,
2687
+ now: this.now(),
2688
+ });
2689
+ if (autonomy.grant.id !== grant.id) {
2690
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone autonomy grant has been superseded.');
2691
+ }
2692
+ }
2543
2693
  assertDelegationScopeExists(projectId, policy) {
2544
2694
  if (policy.scope.kind === 'task') {
2545
2695
  this.store.readTask(projectId, policy.scope.id);
@@ -3028,11 +3178,6 @@ function appendCorrectivePlanAudit(taskRoot, previous, saved, failedReviewAttemp
3028
3178
  ...audit,
3029
3179
  })}\n`, { encoding: 'utf8', mode: 0o600 });
3030
3180
  }
3031
- function assertContentOnlyKnowledgeRefresh(approved, inspected) {
3032
- if (!isContentOnlyKnowledgeRefresh(approved, inspected)) {
3033
- 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.' });
3034
- }
3035
- }
3036
3181
  function isContentOnlyKnowledgeRefresh(approved, inspected) {
3037
3182
  const classification = (map) => JSON.stringify({
3038
3183
  entries: map.entries.map((entry) => ({
@@ -3048,6 +3193,107 @@ function isContentOnlyKnowledgeRefresh(approved, inspected) {
3048
3193
  });
3049
3194
  return classification(approved) === classification(inspected);
3050
3195
  }
3196
+ function assertDelegatedKnowledgeRefresh(assessment) {
3197
+ if (assessment.eligible)
3198
+ return;
3199
+ 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.', {
3200
+ unsafeDifferences: assessment.unsafeDifferences,
3201
+ requiredAction: 'Run project-memory reconcile, inspect the classification diff, and approve it explicitly.',
3202
+ });
3203
+ }
3204
+ function assessDelegatedKnowledgeRefresh(approved, inspected, task, currentPlanArtifactHash) {
3205
+ if (isContentOnlyKnowledgeRefresh(approved, inspected)) {
3206
+ return {
3207
+ eligible: true,
3208
+ mode: 'delegated-content-only',
3209
+ addedSources: [],
3210
+ unsafeDifferences: [],
3211
+ };
3212
+ }
3213
+ const unsafeDifferences = [];
3214
+ const sourceKey = (source) => `${source.category}\0${source.path}\0${source.scope}`;
3215
+ const approvedByKey = new Map(approved.entries.map((source) => [sourceKey(source), source]));
3216
+ const inspectedByKey = new Map(inspected.entries.map((source) => [sourceKey(source), source]));
3217
+ const inspectedByPath = new Map(inspected.entries.map((source) => [source.path, source]));
3218
+ for (const source of approved.entries) {
3219
+ const candidate = inspectedByKey.get(sourceKey(source));
3220
+ if (!candidate) {
3221
+ const samePath = inspectedByPath.get(source.path);
3222
+ unsafeDifferences.push(samePath
3223
+ ? `Existing source ${source.path} changed category or scope from ${source.category}/${source.scope} to ${samePath.category}/${samePath.scope}.`
3224
+ : `Previously approved source was removed: ${source.path}.`);
3225
+ continue;
3226
+ }
3227
+ if (candidate.authority !== source.authority) {
3228
+ unsafeDifferences.push(`Existing source ${source.path} changed authority from ${source.authority} to ${candidate.authority}.`);
3229
+ }
3230
+ if (candidate.id !== source.id || candidate.discoveredBy !== source.discoveredBy) {
3231
+ unsafeDifferences.push(`Existing source ${source.path} changed classification identity.`);
3232
+ }
3233
+ }
3234
+ const addedSources = inspected.entries.filter((source) => !approvedByKey.has(sourceKey(source)));
3235
+ const currentExecutionAuthorization = [...task.authorizations].reverse().find((authorization) => authorization.kind === 'execution'
3236
+ && authorization.decision === 'approved'
3237
+ && authorization.planHash === task.planHash
3238
+ && authorization.briefHash === task.briefHash);
3239
+ if (!task.planHash || !currentExecutionAuthorization) {
3240
+ unsafeDifferences.push('The current Task Plan was not execution-authorized before Knowledge drift.');
3241
+ }
3242
+ if (!task.planHash || currentPlanArtifactHash !== task.planHash) {
3243
+ unsafeDifferences.push('The current Task Plan artifact does not match its authorized state binding.');
3244
+ }
3245
+ if (task.knowledgeImpact !== 'create') {
3246
+ unsafeDifferences.push(`Task Plan knowledgeImpact is ${task.knowledgeImpact ?? 'unset'}, not create.`);
3247
+ }
3248
+ const declaredTargets = new Set();
3249
+ for (const target of task.knowledgeTargets) {
3250
+ let normalized;
3251
+ try {
3252
+ normalized = normalizeRelativePath(target.path);
3253
+ }
3254
+ catch {
3255
+ unsafeDifferences.push(`Task Plan Knowledge target is not an exact repository-relative path: ${target.path}.`);
3256
+ continue;
3257
+ }
3258
+ if (normalized !== target.path || /[*?\[\]{}]/.test(target.path) || target.path.endsWith('/')) {
3259
+ unsafeDifferences.push(`Task Plan Knowledge target is wildcard, directory-like, or not normalized: ${target.path}.`);
3260
+ continue;
3261
+ }
3262
+ declaredTargets.add(`${target.category}\0${target.path}`);
3263
+ }
3264
+ if (addedSources.length === 0) {
3265
+ unsafeDifferences.push('No new Knowledge source matches the Plan-bounded addition path.');
3266
+ }
3267
+ for (const source of addedSources) {
3268
+ if (source.authority !== 'supporting') {
3269
+ unsafeDifferences.push(`New source ${source.path} has forbidden ${source.authority} authority.`);
3270
+ }
3271
+ if (!declaredTargets.has(`${source.category}\0${source.path}`)) {
3272
+ unsafeDifferences.push(`New source ${source.path} (${source.category}) was not declared by the authorized Task Plan.`);
3273
+ }
3274
+ }
3275
+ if (JSON.stringify(approved.gaps) !== JSON.stringify(inspected.gaps)) {
3276
+ unsafeDifferences.push('Project Knowledge gaps changed.');
3277
+ }
3278
+ if (JSON.stringify(approved.conflicts) !== JSON.stringify(inspected.conflicts)) {
3279
+ unsafeDifferences.push('Project Knowledge conflicts changed.');
3280
+ }
3281
+ if (inspected.conflicts.length > 0) {
3282
+ unsafeDifferences.push('The inspected Project Knowledge Map contains conflicts.');
3283
+ }
3284
+ return {
3285
+ eligible: unsafeDifferences.length === 0,
3286
+ mode: 'delegated-plan-bounded-supporting-source',
3287
+ addedSources: addedSources.map(({ id, path: sourcePath, category, scope, authority }) => ({
3288
+ id,
3289
+ path: sourcePath,
3290
+ category,
3291
+ scope,
3292
+ authority,
3293
+ })),
3294
+ unsafeDifferences: [...new Set(unsafeDifferences)],
3295
+ };
3296
+ }
3051
3297
  function validateMilestonePlan(plan) {
3052
3298
  if (!plan.outcome.trim())
3053
3299
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone outcome is required.');