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.
- package/README.md +1 -1
- package/dist/src/alpha6/plan-risk.d.ts +1 -0
- package/dist/src/alpha6/plan-risk.js +2 -2
- package/dist/src/alpha6/plan-risk.js.map +1 -1
- package/dist/src/alpha6/remediation.d.ts +4 -1
- package/dist/src/alpha6/remediation.js +84 -19
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha7/corrective-recovery.d.ts +26 -0
- package/dist/src/alpha7/corrective-recovery.js +593 -0
- package/dist/src/alpha7/corrective-recovery.js.map +1 -0
- package/dist/src/cli.js +4 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +27 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/workflow.d.ts +2 -0
- package/dist/src/workflow.js +302 -56
- package/dist/src/workflow.js.map +1 -1
- package/docs/alpha7.1-implementation-brief.md +268 -0
- package/docs/alpha7.2-corrective-context-refresh-brief.md +484 -0
- package/docs/autonomy-guardrails.md +29 -14
- package/package.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +13 -4
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +16 -4
- package/references/state-machine.md +17 -6
- package/schemas/corrective-decision-recovery-event.schema.json +71 -0
package/dist/src/workflow.js
CHANGED
|
@@ -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
|
|
888
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
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
|
|
2150
|
-
&& storedKnowledgeMap.status === 'active'
|
|
2176
|
+
const refreshAssessment = storedKnowledgeMap.status === 'active'
|
|
2151
2177
|
&& storedKnowledgeMap.approval
|
|
2152
|
-
|
|
2153
|
-
|
|
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:
|
|
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
|
|
2387
|
-
?
|
|
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
|
-
...
|
|
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.');
|