codex-workflow-v2 2.0.0-beta.10 → 2.0.0-beta.12
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 +47 -13
- package/dist/src/alpha6/handoff.d.ts +4 -3
- package/dist/src/alpha6/handoff.js +58 -3
- package/dist/src/alpha6/handoff.js.map +1 -1
- package/dist/src/alpha6/mechanical-feasibility.d.ts +7 -0
- package/dist/src/alpha6/mechanical-feasibility.js +303 -0
- package/dist/src/alpha6/mechanical-feasibility.js.map +1 -0
- package/dist/src/alpha6/milestone.d.ts +25 -2
- package/dist/src/alpha6/milestone.js +359 -32
- package/dist/src/alpha6/milestone.js.map +1 -1
- package/dist/src/alpha6/plan-integrity.d.ts +14 -0
- package/dist/src/alpha6/plan-integrity.js +127 -0
- package/dist/src/alpha6/plan-integrity.js.map +1 -0
- package/dist/src/alpha6/remediation.d.ts +3 -2
- package/dist/src/alpha6/remediation.js +168 -55
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/alpha6/review.d.ts +5 -0
- package/dist/src/alpha6/review.js +110 -2
- package/dist/src/alpha6/review.js.map +1 -1
- package/dist/src/cli.js +14 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +82 -1
- package/dist/src/git.d.ts +1 -1
- package/dist/src/git.js +8 -1
- package/dist/src/git.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/lifecycle/corrective-replan.js +8 -3
- package/dist/src/lifecycle/corrective-replan.js.map +1 -1
- package/dist/src/reviewer.d.ts +26 -0
- package/dist/src/reviewer.js +54 -1
- package/dist/src/reviewer.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/workflow.d.ts +5 -0
- package/dist/src/workflow.js +449 -148
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +42 -33
- package/docs/beta11-plan-integrity-recovery-brief.md +38 -0
- package/docs/decisions.md +5 -4
- package/docs/delegated-approval.md +5 -4
- package/docs/development-flow.md +38 -12
- package/docs/lifecycle/state-machine-stabilization.md +2 -2
- package/docs/pdf/README.md +24 -0
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
- package/docs/pdf/requirements.txt +1 -0
- package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +235 -0
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +334 -0
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +414 -0
- package/docs/problem-briefs/01-pre-implementation-integrity.md +478 -0
- package/docs/problem-briefs/02-minimal-step-integrity.md +411 -0
- package/docs/problem-briefs/03-minimal-agent-context-integrity.md +358 -0
- package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +566 -0
- package/docs/problem-briefs/BRIEF-TEMPLATE.md +56 -0
- package/docs/problem-briefs/README.md +120 -0
- package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +90 -0
- package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +246 -0
- package/docs/release.md +17 -6
- package/docs/split-required-recovery.md +19 -26
- package/docs/validation-report.md +83 -70
- package/package.json +5 -1
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +31 -9
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +23 -6
- package/references/state-machine.md +18 -13
- package/references/validation-and-review.md +11 -4
- package/schemas/authorization-event.schema.json +64 -1
- package/schemas/corrective-decision-event.schema.json +48 -3
- package/schemas/milestone-scope-change-event.schema.json +6 -1
- package/schemas/milestone.schema.json +6 -1
- package/schemas/review-result.schema.json +17 -1
- package/schemas/step-review-event.schema.json +15 -0
- package/schemas/task-handoff-event.schema.json +18 -2
- package/scripts/generate-pdf-docs.py +513 -0
- package/scripts/run-pdf-docs.mjs +62 -0
package/dist/src/workflow.js
CHANGED
|
@@ -13,10 +13,12 @@ import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.j
|
|
|
13
13
|
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
14
14
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
15
15
|
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
16
|
-
import { appendCorrectiveDecisionEvent, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
|
|
17
|
-
import {
|
|
16
|
+
import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
|
|
17
|
+
import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
|
|
18
|
+
import { assertMechanicalFeasibilityFresh, evaluateMechanicalFeasibility, hashMechanicalFeasibilityEvidence, } from './alpha6/mechanical-feasibility.js';
|
|
19
|
+
import { assessMilestoneInitialAssembly, applyInitialMilestonePlanTransaction, applyMilestoneScopeChangeTransaction, assertMilestoneDependenciesDeclared, assertMilestoneTaskRunnable, assessMilestoneTaskDependencies, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, inspectMilestoneWithIntegrity, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
|
|
18
20
|
import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
|
|
19
|
-
import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, } from './alpha6/review.js';
|
|
21
|
+
import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestFailedStepReviewRouting, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, } from './alpha6/review.js';
|
|
20
22
|
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
|
|
21
23
|
import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
|
|
22
24
|
import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
|
|
@@ -394,6 +396,32 @@ export class WorkflowService {
|
|
|
394
396
|
disposition: membership?.disposition ?? null,
|
|
395
397
|
});
|
|
396
398
|
}
|
|
399
|
+
const incidentDependencies = milestone.memberships.flatMap((candidate) => {
|
|
400
|
+
if (!Array.isArray(candidate.dependsOnTaskIds))
|
|
401
|
+
return [];
|
|
402
|
+
return [
|
|
403
|
+
...(candidate.taskId === oldTask.id
|
|
404
|
+
? candidate.dependsOnTaskIds.map((predecessorTaskId) => ({
|
|
405
|
+
predecessorTaskId,
|
|
406
|
+
consumerTaskId: candidate.taskId,
|
|
407
|
+
}))
|
|
408
|
+
: []),
|
|
409
|
+
...(candidate.dependsOnTaskIds.includes(oldTask.id)
|
|
410
|
+
? [{ predecessorTaskId: oldTask.id, consumerTaskId: candidate.taskId }]
|
|
411
|
+
: []),
|
|
412
|
+
];
|
|
413
|
+
});
|
|
414
|
+
if (structuralReplacementIsDisabled()) {
|
|
415
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Structural replacement is disabled until a topology-preserving replacement transaction is implemented.', {
|
|
416
|
+
diagnosticCode: 'STRUCTURAL_REPLACEMENT_REQUIRED',
|
|
417
|
+
milestoneId: milestone.id,
|
|
418
|
+
oldTaskId: oldTask.id,
|
|
419
|
+
incidentDependencies,
|
|
420
|
+
requiredAction: 'Materialize P04-B/P05 before replacing this split-required Task.',
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
/* P04-B retains the existing one-for-one transaction below as implementation evidence,
|
|
424
|
+
but this route must remain unreachable until dependency rewiring is specified. */
|
|
397
425
|
const riskAudit = readCurrentPlanRiskAudit(this.store, oldTask);
|
|
398
426
|
if (!riskAudit)
|
|
399
427
|
throw new WorkflowError('STATE_CORRUPT', 'The terminal Task has no current Plan Risk Audit.');
|
|
@@ -503,9 +531,19 @@ export class WorkflowService {
|
|
|
503
531
|
checks: milestone.checks,
|
|
504
532
|
memberships: [
|
|
505
533
|
...milestone.memberships.map((candidate) => candidate.taskId === oldTask.id
|
|
506
|
-
? {
|
|
507
|
-
|
|
508
|
-
|
|
534
|
+
? {
|
|
535
|
+
...candidate,
|
|
536
|
+
disposition: 'cancelled',
|
|
537
|
+
reason: replacementReason,
|
|
538
|
+
dependsOnTaskIds: [...(candidate.dependsOnTaskIds ?? [])],
|
|
539
|
+
}
|
|
540
|
+
: { ...candidate, dependsOnTaskIds: [...(candidate.dependsOnTaskIds ?? [])] }),
|
|
541
|
+
{
|
|
542
|
+
taskId: replacementTaskId,
|
|
543
|
+
disposition: 'required',
|
|
544
|
+
reason: replacementReason,
|
|
545
|
+
dependsOnTaskIds: [...(membership.dependsOnTaskIds ?? [])],
|
|
546
|
+
},
|
|
509
547
|
],
|
|
510
548
|
};
|
|
511
549
|
if (autonomyContractEventHash) {
|
|
@@ -714,38 +752,15 @@ export class WorkflowService {
|
|
|
714
752
|
throw new WorkflowError('TRANSITION_BLOCKED', `Started Task ${task.id} cannot be removed from a Milestone.`);
|
|
715
753
|
}
|
|
716
754
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
checks: normalized.checks,
|
|
727
|
-
}, null, 2));
|
|
728
|
-
const candidate = {
|
|
729
|
-
...milestone,
|
|
730
|
-
status: 'awaiting_execution_authorization',
|
|
731
|
-
outcome: normalized.outcome,
|
|
732
|
-
successSignal: normalized.successSignal,
|
|
733
|
-
acceptance: normalized.acceptance,
|
|
734
|
-
taskIds,
|
|
735
|
-
memberships: normalized.memberships,
|
|
736
|
-
checks: normalized.checks,
|
|
737
|
-
membershipRevision: milestone.membershipRevision + 1,
|
|
738
|
-
authorizations,
|
|
739
|
-
planHash: stagedPlan.hash,
|
|
740
|
-
resultHash: null,
|
|
741
|
-
evidenceHash: null,
|
|
742
|
-
acceptedHead: null,
|
|
743
|
-
cancellationReason: null,
|
|
744
|
-
};
|
|
745
|
-
assertMilestoneMembershipIntegrity(candidate, (taskId) => this.store.readTask(identity.projectId, taskId), {}, () => this.store.listTasks(identity.projectId));
|
|
746
|
-
const saved = this.store.writeMilestone(candidate, expectedRevision);
|
|
747
|
-
this.store.publishArtifact(stagedPlan);
|
|
748
|
-
return saved;
|
|
755
|
+
return applyInitialMilestonePlanTransaction({
|
|
756
|
+
store: this.store,
|
|
757
|
+
milestone,
|
|
758
|
+
expectedRevision,
|
|
759
|
+
normalizedPlan: normalized,
|
|
760
|
+
now: this.now(),
|
|
761
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
762
|
+
listTasks: () => this.store.listTasks(identity.projectId),
|
|
763
|
+
});
|
|
749
764
|
}
|
|
750
765
|
prepareMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor) {
|
|
751
766
|
const { identity } = this.#mutationContext(repository);
|
|
@@ -854,6 +869,7 @@ export class WorkflowService {
|
|
|
854
869
|
if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
|
|
855
870
|
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
|
|
856
871
|
}
|
|
872
|
+
assertMilestoneDependenciesDeclared(milestone);
|
|
857
873
|
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
858
874
|
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
859
875
|
const planHash = this.store.hashArtifact(root, 'plan.json');
|
|
@@ -1089,9 +1105,6 @@ export class WorkflowService {
|
|
|
1089
1105
|
});
|
|
1090
1106
|
}
|
|
1091
1107
|
const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
|
|
1092
|
-
if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
|
|
1093
|
-
validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
|
|
1094
|
-
}
|
|
1095
1108
|
const guardedRemediationGate = task.status === 'needs_fix' && currentPlanRiskAudit
|
|
1096
1109
|
? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
|
|
1097
1110
|
: null;
|
|
@@ -1129,6 +1142,23 @@ export class WorkflowService {
|
|
|
1129
1142
|
throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task acceptance ${acceptance}.`);
|
|
1130
1143
|
}
|
|
1131
1144
|
}
|
|
1145
|
+
if (task.planHash && task.status === 'needs_fix') {
|
|
1146
|
+
if (task.planObjective !== null && plan.objective !== task.planObjective) {
|
|
1147
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation may change the implementation Plan but not the Task objective.', {
|
|
1148
|
+
taskId: task.id,
|
|
1149
|
+
currentObjective: task.planObjective,
|
|
1150
|
+
requestedObjective: plan.objective,
|
|
1151
|
+
requiredAction: 'Obtain an explicit Task scope decision instead of changing the objective through remediation.',
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
if (JSON.stringify(plan.requirements) !== JSON.stringify(task.requirements)
|
|
1155
|
+
|| JSON.stringify(plan.acceptance) !== JSON.stringify(task.acceptance)) {
|
|
1156
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Remediation cannot change Task requirements or acceptance.', {
|
|
1157
|
+
taskId: task.id,
|
|
1158
|
+
requiredAction: 'Obtain an explicit Task scope decision; replan may change only implementation Steps.',
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1132
1162
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
1133
1163
|
const preparedPlan = prepareCorrectiveReplanCandidate(task, plan);
|
|
1134
1164
|
const authorizations = task.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
|
|
@@ -1397,7 +1427,24 @@ export class WorkflowService {
|
|
|
1397
1427
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
1398
1428
|
const briefHash = this.store.hashArtifact(root, 'brief.md');
|
|
1399
1429
|
const planHash = this.store.hashArtifact(root, 'plan.md');
|
|
1430
|
+
if (task.briefHash !== briefHash || task.planHash !== planHash) {
|
|
1431
|
+
throw new WorkflowError('STATE_CORRUPT', 'Task Plan authority does not match its stored artifacts.', {
|
|
1432
|
+
taskId: task.id,
|
|
1433
|
+
stateBriefHash: task.briefHash,
|
|
1434
|
+
artifactBriefHash: briefHash,
|
|
1435
|
+
statePlanHash: task.planHash,
|
|
1436
|
+
artifactPlanHash: planHash,
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1400
1439
|
readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: true });
|
|
1440
|
+
const mechanicalFeasibility = evaluateMechanicalFeasibility(identity.repositoryRoot, task, briefHash, planHash);
|
|
1441
|
+
if (mechanicalFeasibility.decision === 'blocked') {
|
|
1442
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task Plan contains a proven mechanical contradiction.', {
|
|
1443
|
+
diagnosticCode: 'MECHANICAL_PLAN_INFEASIBILITY',
|
|
1444
|
+
mechanicalFeasibility,
|
|
1445
|
+
requiredAction: 'task plan-set',
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1401
1448
|
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
1402
1449
|
const event = {
|
|
1403
1450
|
kind: 'execution',
|
|
@@ -1407,11 +1454,12 @@ export class WorkflowService {
|
|
|
1407
1454
|
briefHash,
|
|
1408
1455
|
planHash,
|
|
1409
1456
|
resultHash: null,
|
|
1410
|
-
evidenceHash:
|
|
1411
|
-
headCommit:
|
|
1457
|
+
evidenceHash: hashMechanicalFeasibilityEvidence(mechanicalFeasibility),
|
|
1458
|
+
headCommit: mechanicalFeasibility.evaluatedHead,
|
|
1412
1459
|
reason,
|
|
1413
1460
|
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1414
1461
|
...(delegation ? { delegation } : {}),
|
|
1462
|
+
mechanicalFeasibility,
|
|
1415
1463
|
};
|
|
1416
1464
|
return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
|
|
1417
1465
|
}
|
|
@@ -1443,6 +1491,9 @@ export class WorkflowService {
|
|
|
1443
1491
|
if (grantId)
|
|
1444
1492
|
this.assertHandoffGrant(context.identity.projectId, task, targetActor, grantId);
|
|
1445
1493
|
const milestone = task.milestoneId ? this.readMilestoneCurrent(task.projectId, task.milestoneId) : null;
|
|
1494
|
+
const dependencyBinding = milestone
|
|
1495
|
+
? this.currentMilestoneDependencyBinding(milestone, task.id)
|
|
1496
|
+
: null;
|
|
1446
1497
|
const milestones = milestone ? this.listMilestonesCurrent(task.projectId) : [];
|
|
1447
1498
|
const milestoneDisplayNumber = milestone
|
|
1448
1499
|
? milestones.findIndex((candidate) => candidate.id === milestone.id) + 1
|
|
@@ -1488,6 +1539,7 @@ export class WorkflowService {
|
|
|
1488
1539
|
writerToken: lease.token,
|
|
1489
1540
|
milestoneDisplayNumber,
|
|
1490
1541
|
taskDisplayNumber,
|
|
1542
|
+
dependencyBinding,
|
|
1491
1543
|
expectedNext: String(nextForTask(task).action ?? 'task show'),
|
|
1492
1544
|
grantId,
|
|
1493
1545
|
expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
|
|
@@ -1510,7 +1562,11 @@ export class WorkflowService {
|
|
|
1510
1562
|
if (!normalizedActor) {
|
|
1511
1563
|
throw new WorkflowError('INVALID_ARGUMENT', 'Claim actor is required.');
|
|
1512
1564
|
}
|
|
1513
|
-
|
|
1565
|
+
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
1566
|
+
const dependencyBinding = task.milestoneId
|
|
1567
|
+
? this.currentMilestoneDependencyBinding(this.readMilestoneCurrent(task.projectId, task.milestoneId), task.id)
|
|
1568
|
+
: null;
|
|
1569
|
+
const pendingHandoff = assertTaskClaimAllowed(this.store, task, normalizedActor, claimToken, this.now(), dependencyBinding);
|
|
1514
1570
|
if (pendingHandoff.latestEvent.grantId) {
|
|
1515
1571
|
this.assertHandoffGrant(context.identity.projectId, task, normalizedActor, pendingHandoff.latestEvent.grantId);
|
|
1516
1572
|
}
|
|
@@ -1543,7 +1599,7 @@ export class WorkflowService {
|
|
|
1543
1599
|
lease = context.locks.acquire(taskId, normalizedActor);
|
|
1544
1600
|
}
|
|
1545
1601
|
try {
|
|
1546
|
-
const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now());
|
|
1602
|
+
const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now(), dependencyBinding);
|
|
1547
1603
|
return {
|
|
1548
1604
|
task,
|
|
1549
1605
|
event,
|
|
@@ -1567,6 +1623,22 @@ export class WorkflowService {
|
|
|
1567
1623
|
if (!normalizedActor)
|
|
1568
1624
|
throw new WorkflowError('INVALID_ARGUMENT', 'Writer credential replacement actor is required.');
|
|
1569
1625
|
const posture = readTaskC1Posture(this.store, task);
|
|
1626
|
+
if (posture.state === 'claimed' && task.milestoneId) {
|
|
1627
|
+
const dependencyBinding = this.currentMilestoneDependencyBinding(this.readMilestoneCurrent(task.projectId, task.milestoneId), task.id);
|
|
1628
|
+
const claimedBinding = posture.latestEvent.dependencyBinding ?? null;
|
|
1629
|
+
if (claimedBinding === null
|
|
1630
|
+
|| claimedBinding.milestoneRevision !== dependencyBinding.milestoneRevision
|
|
1631
|
+
|| claimedBinding.membershipRevision !== dependencyBinding.membershipRevision
|
|
1632
|
+
|| claimedBinding.milestonePlanHash !== dependencyBinding.milestonePlanHash) {
|
|
1633
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Writer credential replacement requires a current Milestone dependency binding.', {
|
|
1634
|
+
diagnosticCode: 'MILESTONE_DEPENDENCY_BINDING_STALE',
|
|
1635
|
+
taskId: task.id,
|
|
1636
|
+
milestoneId: task.milestoneId,
|
|
1637
|
+
claimDependencyBinding: claimedBinding,
|
|
1638
|
+
currentDependencyBinding: dependencyBinding,
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1570
1642
|
const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
1571
1643
|
const action = String(navigation.action);
|
|
1572
1644
|
const currentLease = context.locks.inspect(taskId);
|
|
@@ -1615,17 +1687,10 @@ export class WorkflowService {
|
|
|
1615
1687
|
this.requireExecutionAdoptionPosture(identity, taskId);
|
|
1616
1688
|
if (task.status !== 'ready')
|
|
1617
1689
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is ${task.status}.`);
|
|
1618
|
-
this.assertExecutionAuthorizationFresh(task);
|
|
1690
|
+
const executionAuthorization = this.assertExecutionAuthorizationFresh(task, identity.repositoryRoot);
|
|
1619
1691
|
if (task.milestoneId) {
|
|
1620
1692
|
const milestone = this.readMilestoneCurrent(identity.projectId, task.milestoneId);
|
|
1621
|
-
const
|
|
1622
|
-
if (milestone.status !== 'active' || membership?.disposition !== 'required') {
|
|
1623
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task requires an active, authorized membership.', {
|
|
1624
|
-
milestoneId: milestone.id,
|
|
1625
|
-
milestoneStatus: milestone.status,
|
|
1626
|
-
disposition: membership?.disposition ?? null,
|
|
1627
|
-
});
|
|
1628
|
-
}
|
|
1693
|
+
const dependencyBinding = this.currentMilestoneDependencyBinding(milestone, task.id);
|
|
1629
1694
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
1630
1695
|
if (handoffPosture.state !== 'claimed') {
|
|
1631
1696
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a prepared and claimed C1 handoff bundle.', {
|
|
@@ -1635,6 +1700,20 @@ export class WorkflowService {
|
|
|
1635
1700
|
requiredAction: handoffPosture.state === 'pending' ? 'Claim the prepared handoff.' : 'Prepare and claim a C1 handoff.',
|
|
1636
1701
|
});
|
|
1637
1702
|
}
|
|
1703
|
+
const claimBinding = handoffPosture.latestEvent.dependencyBinding ?? null;
|
|
1704
|
+
if (claimBinding === null
|
|
1705
|
+
|| claimBinding.milestoneRevision !== dependencyBinding.milestoneRevision
|
|
1706
|
+
|| claimBinding.membershipRevision !== dependencyBinding.membershipRevision
|
|
1707
|
+
|| claimBinding.milestonePlanHash !== dependencyBinding.milestonePlanHash) {
|
|
1708
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a current dependency-bound C1 claim.', {
|
|
1709
|
+
diagnosticCode: 'MILESTONE_DEPENDENCY_BINDING_STALE',
|
|
1710
|
+
taskId: task.id,
|
|
1711
|
+
milestoneId: milestone.id,
|
|
1712
|
+
claimDependencyBinding: handoffPosture.latestEvent.dependencyBinding ?? null,
|
|
1713
|
+
currentDependencyBinding: dependencyBinding,
|
|
1714
|
+
requiredAction: 'Prepare and claim a new C1 handoff from the current Milestone Plan.',
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1638
1717
|
}
|
|
1639
1718
|
if (!isClean(identity.repositoryRoot)) {
|
|
1640
1719
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start requires a clean checkout.', {
|
|
@@ -1644,12 +1723,22 @@ export class WorkflowService {
|
|
|
1644
1723
|
const branch = `codex/task-${task.id.slice(5, 15).toLowerCase()}-${task.slug}`;
|
|
1645
1724
|
let baseCommit;
|
|
1646
1725
|
let taskBranch;
|
|
1726
|
+
const authorizedBaseCommit = executionAuthorization.mechanicalFeasibility?.sourceKind === 'pre-start-base'
|
|
1727
|
+
? executionAuthorization.mechanicalFeasibility.evaluatedHead
|
|
1728
|
+
: null;
|
|
1647
1729
|
if (workspaceOwner === 'local') {
|
|
1648
|
-
baseCommit = startLocalTaskBranch(identity.repositoryRoot, branch, task.baseBranch);
|
|
1730
|
+
baseCommit = startLocalTaskBranch(identity.repositoryRoot, branch, task.baseBranch, authorizedBaseCommit);
|
|
1649
1731
|
taskBranch = branch;
|
|
1650
1732
|
}
|
|
1651
1733
|
else {
|
|
1652
1734
|
baseCommit = runGit(identity.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
1735
|
+
if (authorizedBaseCommit !== null && baseCommit !== authorizedBaseCommit) {
|
|
1736
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start base moved after execution authorization.', {
|
|
1737
|
+
baseBranch: task.baseBranch,
|
|
1738
|
+
authorizedBaseCommit,
|
|
1739
|
+
currentBaseCommit: baseCommit,
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1653
1742
|
taskBranch = currentBranch(identity.repositoryRoot);
|
|
1654
1743
|
if (!taskBranch || taskBranch === task.baseBranch) {
|
|
1655
1744
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `Externally owned execution requires a dedicated branch different from ${task.baseBranch}.`);
|
|
@@ -1668,7 +1757,7 @@ export class WorkflowService {
|
|
|
1668
1757
|
if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
|
|
1669
1758
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run a Step while ${task.status}.`);
|
|
1670
1759
|
}
|
|
1671
|
-
this.assertExecutionAuthorizationFresh(task);
|
|
1760
|
+
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
1672
1761
|
const currentPlanBlockingDecisions = readCorrectiveDecisionEvents(this.store, task)
|
|
1673
1762
|
.filter((event) => event.planHash === task.planHash && event.decision !== 'continue-fix');
|
|
1674
1763
|
if (currentPlanBlockingDecisions.length > 0) {
|
|
@@ -2304,7 +2393,7 @@ export class WorkflowService {
|
|
|
2304
2393
|
if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
|
|
2305
2394
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot recover a corrective decision while ${task.status}.`);
|
|
2306
2395
|
}
|
|
2307
|
-
this.assertExecutionAuthorizationFresh(task);
|
|
2396
|
+
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
2308
2397
|
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
2309
2398
|
return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
|
|
2310
2399
|
}
|
|
@@ -2315,6 +2404,47 @@ export class WorkflowService {
|
|
|
2315
2404
|
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
2316
2405
|
return appendRemediationModeRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
|
|
2317
2406
|
}
|
|
2407
|
+
recoverPlanIntegrity(repository, taskId, stepId, expectedRevision, actor, writerToken) {
|
|
2408
|
+
const context = this.#mutationContext(repository);
|
|
2409
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2410
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2411
|
+
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
2412
|
+
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
2413
|
+
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
2414
|
+
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
2415
|
+
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2416
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2417
|
+
const handoff = readTaskC1Posture(this.store, task);
|
|
2418
|
+
const requiredActor = handoff.state === 'claimed'
|
|
2419
|
+
? handoff.claimant
|
|
2420
|
+
: defaultPlanIntegrityRecoveryActor(task.id);
|
|
2421
|
+
if (actor.trim() !== requiredActor) {
|
|
2422
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity recovery actor does not match the fresh Workflow route.', {
|
|
2423
|
+
taskId,
|
|
2424
|
+
stepId,
|
|
2425
|
+
requiredActor,
|
|
2426
|
+
actualActor: actor.trim(),
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
2430
|
+
if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
2431
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity recovery requires the failed Step to be guarded by the current Plan Risk Audit.', {
|
|
2432
|
+
taskId,
|
|
2433
|
+
stepId,
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
const assessment = assessPlanIntegrityRecovery(this.store, context.identity.repositoryRoot, task, stepId);
|
|
2437
|
+
if (!assessment.applicable || !assessment.eligible || !assessment.evidence) {
|
|
2438
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Bounded Plan-integrity recovery is not eligible for the current Task state.', {
|
|
2439
|
+
taskId,
|
|
2440
|
+
stepId,
|
|
2441
|
+
applicable: assessment.applicable,
|
|
2442
|
+
blockers: assessment.blockers,
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
|
|
2446
|
+
return { assessment, event };
|
|
2447
|
+
}
|
|
2318
2448
|
recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
|
|
2319
2449
|
const { identity } = this.#mutationContext(repository);
|
|
2320
2450
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
@@ -2693,6 +2823,13 @@ export class WorkflowService {
|
|
|
2693
2823
|
if (!task)
|
|
2694
2824
|
throw new WorkflowError('STATE_CORRUPT', `Milestone member ${membership.taskId} does not exist.`);
|
|
2695
2825
|
const stepsCompleted = task.steps.filter((step) => ['completed', 'skipped'].includes(step.status)).length;
|
|
2826
|
+
const dependency = assessMilestoneTaskDependencies(milestone, task.id, (candidateTaskId) => {
|
|
2827
|
+
const candidate = tasksById.get(candidateTaskId);
|
|
2828
|
+
if (!candidate)
|
|
2829
|
+
throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
|
|
2830
|
+
return candidate;
|
|
2831
|
+
});
|
|
2832
|
+
const executionEligible = milestone.status === 'active' && !['merged', 'cancelled'].includes(task.status);
|
|
2696
2833
|
return {
|
|
2697
2834
|
ordinal: `T${String(index + 1).padStart(2, '0')}`,
|
|
2698
2835
|
taskId: task.id,
|
|
@@ -2701,6 +2838,15 @@ export class WorkflowService {
|
|
|
2701
2838
|
revision: task.revision,
|
|
2702
2839
|
disposition: membership.disposition,
|
|
2703
2840
|
membershipReason: membership.reason,
|
|
2841
|
+
dependencyState: dependency.dependencyState,
|
|
2842
|
+
dependsOnTaskIds: dependency.dependsOnTaskIds,
|
|
2843
|
+
unsatisfiedPredecessors: dependency.unsatisfiedPredecessors,
|
|
2844
|
+
runnable: executionEligible && dependency.runnable,
|
|
2845
|
+
dependencyBlockReason: !executionEligible
|
|
2846
|
+
? milestone.status !== 'active'
|
|
2847
|
+
? `Milestone is ${milestone.status}, not active.`
|
|
2848
|
+
: `Task is terminal (${task.status}).`
|
|
2849
|
+
: dependency.blockReason,
|
|
2704
2850
|
replacementForTaskId: task.replacementForTaskId ?? null,
|
|
2705
2851
|
replacedByTaskId: task.replacedByTaskId ?? null,
|
|
2706
2852
|
stepsCompleted,
|
|
@@ -3030,21 +3176,34 @@ export class WorkflowService {
|
|
|
3030
3176
|
const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
|
|
3031
3177
|
const inspectCurrentKnowledgeMap = () => inspectKnowledgeMap(this.store.readKnowledgeMap(status.projectId), scanProjectKnowledge(repositoryRoot, status.projectId, this.now()));
|
|
3032
3178
|
const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
|
|
3179
|
+
const tasksById = new Map(status.tasks.map((task) => [task.id, task]));
|
|
3033
3180
|
const requestedTask = taskId === null
|
|
3034
3181
|
? null
|
|
3035
3182
|
: status.tasks.find((task) => task.id === taskId) ?? null;
|
|
3036
3183
|
if (taskId !== null && !requestedTask) {
|
|
3037
3184
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3038
3185
|
}
|
|
3039
|
-
if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById)) {
|
|
3186
|
+
if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById, tasksById)) {
|
|
3187
|
+
const requestedMilestone = requestedTask.milestoneId
|
|
3188
|
+
? milestonesById.get(requestedTask.milestoneId) ?? null
|
|
3189
|
+
: null;
|
|
3190
|
+
const dependency = requestedMilestone
|
|
3191
|
+
? assessMilestoneTaskDependencies(requestedMilestone, requestedTask.id, (candidateTaskId) => {
|
|
3192
|
+
const candidate = tasksById.get(candidateTaskId);
|
|
3193
|
+
if (!candidate)
|
|
3194
|
+
throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
|
|
3195
|
+
return candidate;
|
|
3196
|
+
})
|
|
3197
|
+
: null;
|
|
3040
3198
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} is not actionable from repository next.`, {
|
|
3041
3199
|
taskId: requestedTask.id,
|
|
3042
3200
|
taskStatus: requestedTask.status,
|
|
3043
3201
|
milestoneId: requestedTask.milestoneId,
|
|
3202
|
+
...(dependency ? { dependency } : {}),
|
|
3044
3203
|
});
|
|
3045
3204
|
}
|
|
3046
3205
|
const activeTask = requestedTask
|
|
3047
|
-
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
|
|
3206
|
+
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
|
|
3048
3207
|
if (activeTask) {
|
|
3049
3208
|
const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
3050
3209
|
if (dependencyRecovery.eligible) {
|
|
@@ -3109,7 +3268,7 @@ export class WorkflowService {
|
|
|
3109
3268
|
const taskNext = activeTask.status === 'planning'
|
|
3110
3269
|
? null
|
|
3111
3270
|
: planRiskAuditRecovery
|
|
3112
|
-
?? this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
|
|
3271
|
+
?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, activePlanRiskAudit);
|
|
3113
3272
|
// Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
|
|
3114
3273
|
// strict review are executable while context refresh correctly remains forbidden.
|
|
3115
3274
|
const hardStop = taskNext?.action === 'task corrective-decision'
|
|
@@ -3118,6 +3277,8 @@ export class WorkflowService {
|
|
|
3118
3277
|
if (taskNext?.action === 'task step-complete'
|
|
3119
3278
|
|| taskNext?.action === 'task step-review'
|
|
3120
3279
|
|| taskNext?.action === 'task remediation-mode-recover'
|
|
3280
|
+
|| taskNext?.action === 'task plan-integrity-recover'
|
|
3281
|
+
|| taskNext?.blockedAction === 'task plan-integrity-recover'
|
|
3121
3282
|
|| hardStop
|
|
3122
3283
|
|| terminalCorrectiveDecision === 'split-required'
|
|
3123
3284
|
|| terminalCorrectiveDecision === 'stop-escalate') {
|
|
@@ -3216,7 +3377,7 @@ export class WorkflowService {
|
|
|
3216
3377
|
knowledgeMapHash,
|
|
3217
3378
|
}));
|
|
3218
3379
|
}
|
|
3219
|
-
return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null)));
|
|
3380
|
+
return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, null)));
|
|
3220
3381
|
}
|
|
3221
3382
|
let repositoryKnowledgeMap;
|
|
3222
3383
|
try {
|
|
@@ -3351,7 +3512,21 @@ export class WorkflowService {
|
|
|
3351
3512
|
.filter((membership) => membership.disposition === 'required')
|
|
3352
3513
|
.map((membership) => {
|
|
3353
3514
|
const task = status.tasks.find((candidate) => candidate.id === membership.taskId);
|
|
3354
|
-
|
|
3515
|
+
const dependency = assessMilestoneTaskDependencies(activeMilestone, membership.taskId, (candidateTaskId) => {
|
|
3516
|
+
const candidate = status.tasks.find((taskCandidate) => taskCandidate.id === candidateTaskId);
|
|
3517
|
+
if (!candidate)
|
|
3518
|
+
throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
|
|
3519
|
+
return candidate;
|
|
3520
|
+
});
|
|
3521
|
+
return {
|
|
3522
|
+
taskId: membership.taskId,
|
|
3523
|
+
status: task?.status ?? 'missing',
|
|
3524
|
+
dependencyState: dependency.dependencyState,
|
|
3525
|
+
dependsOnTaskIds: dependency.dependsOnTaskIds,
|
|
3526
|
+
unsatisfiedPredecessors: dependency.unsatisfiedPredecessors,
|
|
3527
|
+
runnable: Boolean(task && !['merged', 'cancelled'].includes(task.status) && dependency.runnable),
|
|
3528
|
+
dependencyBlockReason: dependency.blockReason,
|
|
3529
|
+
};
|
|
3355
3530
|
}),
|
|
3356
3531
|
},
|
|
3357
3532
|
}
|
|
@@ -3474,7 +3649,21 @@ export class WorkflowService {
|
|
|
3474
3649
|
}
|
|
3475
3650
|
}
|
|
3476
3651
|
}
|
|
3477
|
-
|
|
3652
|
+
currentMilestoneDependencyBinding(milestone, taskId) {
|
|
3653
|
+
assertMilestoneTaskRunnable(milestone, taskId, (candidateTaskId) => this.store.readTask(milestone.projectId, candidateTaskId));
|
|
3654
|
+
if (!milestone.planHash) {
|
|
3655
|
+
throw new WorkflowError('STATE_CORRUPT', `Active Milestone ${milestone.id} has no Plan hash.`, {
|
|
3656
|
+
milestoneId: milestone.id,
|
|
3657
|
+
taskId,
|
|
3658
|
+
});
|
|
3659
|
+
}
|
|
3660
|
+
return {
|
|
3661
|
+
milestoneRevision: milestone.revision,
|
|
3662
|
+
membershipRevision: milestone.membershipRevision,
|
|
3663
|
+
milestonePlanHash: milestone.planHash,
|
|
3664
|
+
};
|
|
3665
|
+
}
|
|
3666
|
+
nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
|
|
3478
3667
|
const transition = task.status === 'awaiting_execution_authorization'
|
|
3479
3668
|
? 'task.execution_authorize'
|
|
3480
3669
|
: task.status === 'awaiting_final_acceptance'
|
|
@@ -3483,7 +3672,6 @@ export class WorkflowService {
|
|
|
3483
3672
|
const delegatedApprovalOptions = transition
|
|
3484
3673
|
? this.delegatedApprovalOptions(projectId, transition, task)
|
|
3485
3674
|
: [];
|
|
3486
|
-
const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(projectId, task.id));
|
|
3487
3675
|
let remediationModeRecovery = null;
|
|
3488
3676
|
let guardedCorrectivePosture = null;
|
|
3489
3677
|
if (planRiskAudit) {
|
|
@@ -3510,7 +3698,62 @@ export class WorkflowService {
|
|
|
3510
3698
|
}
|
|
3511
3699
|
}
|
|
3512
3700
|
const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
3701
|
+
let mechanicalFeasibilityOverlay = {};
|
|
3702
|
+
if (task.status === 'awaiting_execution_authorization' && task.planHash) {
|
|
3703
|
+
try {
|
|
3704
|
+
const taskRoot = this.store.taskRoot(projectId, task.id);
|
|
3705
|
+
const briefHash = this.store.hashArtifact(taskRoot, 'brief.md');
|
|
3706
|
+
const planHash = this.store.hashArtifact(taskRoot, 'plan.md');
|
|
3707
|
+
if (task.briefHash !== briefHash || task.planHash !== planHash) {
|
|
3708
|
+
throw new WorkflowError('STATE_CORRUPT', 'Task Plan authority does not match its stored artifacts.', {
|
|
3709
|
+
taskId: task.id,
|
|
3710
|
+
stateBriefHash: task.briefHash,
|
|
3711
|
+
artifactBriefHash: briefHash,
|
|
3712
|
+
statePlanHash: task.planHash,
|
|
3713
|
+
artifactPlanHash: planHash,
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
const mechanicalFeasibility = evaluateMechanicalFeasibility(repositoryRoot, task, briefHash, planHash);
|
|
3717
|
+
mechanicalFeasibilityOverlay = mechanicalFeasibility.decision === 'blocked'
|
|
3718
|
+
? {
|
|
3719
|
+
action: 'task plan-set',
|
|
3720
|
+
blockedAction: 'task authorize',
|
|
3721
|
+
mechanicalFeasibility,
|
|
3722
|
+
mechanicalFeasibilityGate: {
|
|
3723
|
+
state: 'blocked',
|
|
3724
|
+
diagnosticCode: 'MECHANICAL_PLAN_INFEASIBILITY',
|
|
3725
|
+
requiredAction: 'Replace the current Plan through task plan-set, then request fresh execution authorization.',
|
|
3726
|
+
},
|
|
3727
|
+
}
|
|
3728
|
+
: {
|
|
3729
|
+
mechanicalFeasibility,
|
|
3730
|
+
mechanicalFeasibilityGate: {
|
|
3731
|
+
state: 'clear',
|
|
3732
|
+
meaning: 'No supported analyzer proved infeasibility; semantic sufficiency is not asserted.',
|
|
3733
|
+
},
|
|
3734
|
+
};
|
|
3735
|
+
}
|
|
3736
|
+
catch (error) {
|
|
3737
|
+
mechanicalFeasibilityOverlay = {
|
|
3738
|
+
action: 'doctor',
|
|
3739
|
+
blockedAction: 'task authorize',
|
|
3740
|
+
mechanicalFeasibilityGate: {
|
|
3741
|
+
state: 'unavailable',
|
|
3742
|
+
diagnosticCode: error instanceof WorkflowError ? error.code : 'UNKNOWN_ERROR',
|
|
3743
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
3744
|
+
requiredAction: 'Restore the exact Plan/source authority before execution authorization.',
|
|
3745
|
+
},
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3513
3749
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
3750
|
+
const failedStep = task.steps.find((step) => step.status === 'failed') ?? null;
|
|
3751
|
+
const failedStepReviewRouting = failedStep
|
|
3752
|
+
? latestFailedStepReviewRouting(this.store, task, failedStep.id)
|
|
3753
|
+
: null;
|
|
3754
|
+
const planIntegrityAssessment = !remediationModeRecovery && planRiskAudit && failedStep
|
|
3755
|
+
? assessPlanIntegrityRecovery(this.store, repositoryRoot, task, failedStep.id)
|
|
3756
|
+
: null;
|
|
3514
3757
|
const strictStepReviewStep = planRiskAudit
|
|
3515
3758
|
? task.steps.find((step) => {
|
|
3516
3759
|
if (step.status !== 'in_progress' || !step.evidence || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
|
|
@@ -3623,6 +3866,39 @@ export class WorkflowService {
|
|
|
3623
3866
|
};
|
|
3624
3867
|
};
|
|
3625
3868
|
let guardedCorrectiveOverlay = {};
|
|
3869
|
+
if (planIntegrityAssessment?.applicable) {
|
|
3870
|
+
const requiredActor = handoffPosture.state === 'claimed'
|
|
3871
|
+
? handoffPosture.claimant
|
|
3872
|
+
: defaultPlanIntegrityRecoveryActor(task.id);
|
|
3873
|
+
guardedCorrectiveOverlay = planIntegrityAssessment.eligible
|
|
3874
|
+
? {
|
|
3875
|
+
action: 'task plan-integrity-recover',
|
|
3876
|
+
blockedAction: 'task run',
|
|
3877
|
+
stepId: planIntegrityAssessment.stepId,
|
|
3878
|
+
planIntegrityRecovery: {
|
|
3879
|
+
state: 'eligible',
|
|
3880
|
+
reasonCode: 'MISSING_NPM_SCRIPT_OUTSIDE_ALLOWED_WRITES',
|
|
3881
|
+
expectedRevision: task.revision,
|
|
3882
|
+
requiredActor,
|
|
3883
|
+
evidence: planIntegrityAssessment.evidence,
|
|
3884
|
+
boundedEffect: 'record-replan-required-decision',
|
|
3885
|
+
preservesWorktree: true,
|
|
3886
|
+
},
|
|
3887
|
+
}
|
|
3888
|
+
: {
|
|
3889
|
+
action: 'doctor',
|
|
3890
|
+
blockedAction: 'task plan-integrity-recover',
|
|
3891
|
+
stepId: planIntegrityAssessment.stepId,
|
|
3892
|
+
planIntegrityRecovery: {
|
|
3893
|
+
state: 'blocked',
|
|
3894
|
+
reasonCode: 'PLAN_INTEGRITY_RECOVERY_BLOCKED',
|
|
3895
|
+
expectedRevision: task.revision,
|
|
3896
|
+
requiredActor,
|
|
3897
|
+
blockers: planIntegrityAssessment.blockers,
|
|
3898
|
+
evidence: planIntegrityAssessment.evidence,
|
|
3899
|
+
},
|
|
3900
|
+
};
|
|
3901
|
+
}
|
|
3626
3902
|
if (remediationModeRecovery) {
|
|
3627
3903
|
guardedCorrectiveOverlay = {
|
|
3628
3904
|
action: 'task remediation-mode-recover',
|
|
@@ -3704,21 +3980,6 @@ export class WorkflowService {
|
|
|
3704
3980
|
},
|
|
3705
3981
|
};
|
|
3706
3982
|
break;
|
|
3707
|
-
case 'hard-stop':
|
|
3708
|
-
guardedCorrectiveOverlay = {
|
|
3709
|
-
action: 'task corrective-decision',
|
|
3710
|
-
stepId: guardedCorrectivePosture.stepId,
|
|
3711
|
-
correctiveDecisionGate: {
|
|
3712
|
-
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
3713
|
-
recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
|
|
3714
|
-
auditorDerived: true,
|
|
3715
|
-
auditorMustDifferFromLatestStrictReviewer: true,
|
|
3716
|
-
allowedDecisions: ['split-required', 'stop-escalate'],
|
|
3717
|
-
hardStop: true,
|
|
3718
|
-
humanApprovalRequired: false,
|
|
3719
|
-
},
|
|
3720
|
-
};
|
|
3721
|
-
break;
|
|
3722
3983
|
case 'replan-required':
|
|
3723
3984
|
guardedCorrectiveOverlay = correctiveReplanOverlay(guardedCorrectivePosture);
|
|
3724
3985
|
break;
|
|
@@ -3726,13 +3987,29 @@ export class WorkflowService {
|
|
|
3726
3987
|
{
|
|
3727
3988
|
const replacementDiscoveries = this.store.listDiscoveries(projectId)
|
|
3728
3989
|
.filter((discovery) => discovery.status === 'ready_to_materialize');
|
|
3729
|
-
const replacementDiscovery = replacementDiscoveries.length === 1 ? replacementDiscoveries[0] : null;
|
|
3730
3990
|
const replacementMilestone = task.milestoneId
|
|
3731
3991
|
? this.store.readMilestone(projectId, task.milestoneId)
|
|
3732
3992
|
: null;
|
|
3993
|
+
const incidentDependencies = replacementMilestone
|
|
3994
|
+
? replacementMilestone.memberships.flatMap((membership) => {
|
|
3995
|
+
if (!Array.isArray(membership.dependsOnTaskIds))
|
|
3996
|
+
return [];
|
|
3997
|
+
return [
|
|
3998
|
+
...(membership.taskId === task.id
|
|
3999
|
+
? membership.dependsOnTaskIds.map((predecessorTaskId) => ({
|
|
4000
|
+
predecessorTaskId,
|
|
4001
|
+
consumerTaskId: membership.taskId,
|
|
4002
|
+
}))
|
|
4003
|
+
: []),
|
|
4004
|
+
...(membership.dependsOnTaskIds.includes(task.id)
|
|
4005
|
+
? [{ predecessorTaskId: task.id, consumerTaskId: membership.taskId }]
|
|
4006
|
+
: []),
|
|
4007
|
+
];
|
|
4008
|
+
})
|
|
4009
|
+
: [];
|
|
3733
4010
|
guardedCorrectiveOverlay = {
|
|
3734
|
-
action:
|
|
3735
|
-
|
|
4011
|
+
action: 'doctor',
|
|
4012
|
+
blockedAction: 'task replacement-materialize',
|
|
3736
4013
|
stepId: guardedCorrectivePosture.stepId,
|
|
3737
4014
|
correctiveDecisionGate: {
|
|
3738
4015
|
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
@@ -3746,29 +4023,17 @@ export class WorkflowService {
|
|
|
3746
4023
|
requiredDiscoveryStatus: 'ready_to_materialize',
|
|
3747
4024
|
oldMembershipDisposition: 'cancelled',
|
|
3748
4025
|
replacementMembershipDisposition: 'required',
|
|
3749
|
-
transaction: '
|
|
4026
|
+
transaction: 'disabled',
|
|
4027
|
+
retainedImplementation: 'journal-recoverable-1-to-1',
|
|
4028
|
+
diagnosticCode: 'STRUCTURAL_REPLACEMENT_REQUIRED',
|
|
4029
|
+
structuralReplacementAvailable: false,
|
|
4030
|
+
incidentDependencies,
|
|
3750
4031
|
discoveryCandidates: replacementDiscoveries.map((discovery) => ({
|
|
3751
4032
|
id: discovery.id,
|
|
3752
4033
|
revision: discovery.revision,
|
|
3753
4034
|
goal: discovery.goal,
|
|
3754
4035
|
})),
|
|
3755
|
-
|
|
3756
|
-
? {
|
|
3757
|
-
exactTransition: {
|
|
3758
|
-
command: 'task replacement-materialize',
|
|
3759
|
-
taskId: task.id,
|
|
3760
|
-
expectedTaskRevision: task.revision,
|
|
3761
|
-
discoveryId: replacementDiscovery.id,
|
|
3762
|
-
expectedDiscoveryRevision: replacementDiscovery.revision,
|
|
3763
|
-
milestoneId: replacementMilestone.id,
|
|
3764
|
-
expectedMilestoneRevision: replacementMilestone.revision,
|
|
3765
|
-
},
|
|
3766
|
-
}
|
|
3767
|
-
: {
|
|
3768
|
-
requiredAction: replacementDiscoveries.length === 0
|
|
3769
|
-
? 'Create and complete one thin replacement Discovery, then run next again.'
|
|
3770
|
-
: 'Select exactly one ready replacement Discovery explicitly; next will not guess between candidates.',
|
|
3771
|
-
}),
|
|
4036
|
+
requiredAction: 'Materialize P04-B/P05 before replacing this split-required Task.',
|
|
3772
4037
|
},
|
|
3773
4038
|
};
|
|
3774
4039
|
}
|
|
@@ -3841,19 +4106,21 @@ export class WorkflowService {
|
|
|
3841
4106
|
},
|
|
3842
4107
|
}
|
|
3843
4108
|
: {}),
|
|
3844
|
-
...
|
|
4109
|
+
...guardedCorrectiveOverlay,
|
|
4110
|
+
...(failedStepReviewRouting?.route === 'replan' && next.action === 'task run'
|
|
3845
4111
|
? {
|
|
3846
|
-
action: 'task plan-set
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
4112
|
+
action: 'task plan-set',
|
|
4113
|
+
blockedAction: 'task run',
|
|
4114
|
+
reviewRemediation: {
|
|
4115
|
+
route: 'replan',
|
|
4116
|
+
source: 'strict-step-review',
|
|
4117
|
+
stepId: failedStep?.id ?? null,
|
|
4118
|
+
planConflicts: failedStepReviewRouting.planConflicts,
|
|
4119
|
+
boundary: 'Task objective, requirements, and acceptance remain immutable; only implementation Steps may change.',
|
|
3853
4120
|
},
|
|
3854
4121
|
}
|
|
3855
4122
|
: {}),
|
|
3856
|
-
...
|
|
4123
|
+
...mechanicalFeasibilityOverlay,
|
|
3857
4124
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
3858
4125
|
};
|
|
3859
4126
|
if (handoffPosture.state === 'pending') {
|
|
@@ -3863,7 +4130,7 @@ export class WorkflowService {
|
|
|
3863
4130
|
blockedAction: next.action,
|
|
3864
4131
|
});
|
|
3865
4132
|
}
|
|
3866
|
-
const navigableResult = ['task plan-set'
|
|
4133
|
+
const navigableResult = ['task plan-set']
|
|
3867
4134
|
.includes(String(result.action))
|
|
3868
4135
|
? {
|
|
3869
4136
|
...result,
|
|
@@ -3967,8 +4234,9 @@ export class WorkflowService {
|
|
|
3967
4234
|
}));
|
|
3968
4235
|
}
|
|
3969
4236
|
findDelegatedContextRefreshOwner(projectId, tasks, milestonesById, approved, inspected) {
|
|
4237
|
+
const tasksById = new Map(tasks.map((task) => [task.id, task]));
|
|
3970
4238
|
for (const task of tasks) {
|
|
3971
|
-
if (!taskIsActionableFromRepositoryNext(task, milestonesById)
|
|
4239
|
+
if (!taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
|
|
3972
4240
|
|| !task.planHash
|
|
3973
4241
|
|| !taskCanRebindKnowledge(task)
|
|
3974
4242
|
|| this.taskHasTerminalCorrectiveDecision(task))
|
|
@@ -4007,7 +4275,7 @@ export class WorkflowService {
|
|
|
4007
4275
|
if (policy.scope.kind === 'milestone')
|
|
4008
4276
|
this.store.readMilestone(projectId, policy.scope.id);
|
|
4009
4277
|
}
|
|
4010
|
-
assertExecutionAuthorizationFresh(task) {
|
|
4278
|
+
assertExecutionAuthorizationFresh(task, repositoryRoot) {
|
|
4011
4279
|
const root = this.store.taskRoot(task.projectId, task.id);
|
|
4012
4280
|
const authorization = [...task.authorizations]
|
|
4013
4281
|
.reverse()
|
|
@@ -4016,14 +4284,35 @@ export class WorkflowService {
|
|
|
4016
4284
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Execution authorization is missing.');
|
|
4017
4285
|
const briefHash = this.store.hashArtifact(root, 'brief.md');
|
|
4018
4286
|
const planHash = this.store.hashArtifact(root, 'plan.md');
|
|
4019
|
-
if (authorization.briefHash !== briefHash
|
|
4287
|
+
if (authorization.briefHash !== briefHash
|
|
4288
|
+
|| authorization.planHash !== planHash
|
|
4289
|
+
|| task.briefHash !== briefHash
|
|
4290
|
+
|| task.planHash !== planHash) {
|
|
4020
4291
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Execution authorization is stale.', {
|
|
4021
4292
|
authorizedBriefHash: authorization.briefHash,
|
|
4022
4293
|
currentBriefHash: briefHash,
|
|
4023
4294
|
authorizedPlanHash: authorization.planHash,
|
|
4024
4295
|
currentPlanHash: planHash,
|
|
4296
|
+
stateBriefHash: task.briefHash,
|
|
4297
|
+
statePlanHash: task.planHash,
|
|
4298
|
+
});
|
|
4299
|
+
}
|
|
4300
|
+
const mechanicalFeasibility = authorization.mechanicalFeasibility;
|
|
4301
|
+
if (!mechanicalFeasibility)
|
|
4302
|
+
return authorization;
|
|
4303
|
+
const evidenceHash = hashMechanicalFeasibilityEvidence(mechanicalFeasibility);
|
|
4304
|
+
if (authorization.evidenceHash !== evidenceHash
|
|
4305
|
+
|| authorization.headCommit !== mechanicalFeasibility.evaluatedHead) {
|
|
4306
|
+
throw new WorkflowError('STATE_CORRUPT', 'Mechanical-feasibility evidence does not match its authorization event.', {
|
|
4307
|
+
taskId: task.id,
|
|
4308
|
+
authorizedEvidenceHash: authorization.evidenceHash,
|
|
4309
|
+
currentEvidenceHash: evidenceHash,
|
|
4310
|
+
authorizedHeadCommit: authorization.headCommit,
|
|
4311
|
+
evaluatedHead: mechanicalFeasibility.evaluatedHead,
|
|
4025
4312
|
});
|
|
4026
4313
|
}
|
|
4314
|
+
assertMechanicalFeasibilityFresh(repositoryRoot, task, mechanicalFeasibility, briefHash, planHash);
|
|
4315
|
+
return authorization;
|
|
4027
4316
|
}
|
|
4028
4317
|
assertMilestoneAuthorizationFresh(milestone) {
|
|
4029
4318
|
const authorization = [...milestone.authorizations]
|
|
@@ -4562,6 +4851,24 @@ function validateReviewInput(review) {
|
|
|
4562
4851
|
if (review.status === 'failed' && review.findings.length === 0) {
|
|
4563
4852
|
throw new WorkflowError('INVALID_ARGUMENT', 'A failed review requires at least one finding.');
|
|
4564
4853
|
}
|
|
4854
|
+
for (const finding of review.findings) {
|
|
4855
|
+
const route = finding.route ?? 'fix';
|
|
4856
|
+
if (route !== 'fix' && route !== 'replan') {
|
|
4857
|
+
throw new WorkflowError('INVALID_ARGUMENT', `Review finding ${finding.id} has an invalid remediation route.`);
|
|
4858
|
+
}
|
|
4859
|
+
if (route === 'fix' && finding.planConflict !== undefined && finding.planConflict !== null) {
|
|
4860
|
+
throw new WorkflowError('INVALID_ARGUMENT', `Review finding ${finding.id} cannot carry a Plan conflict on the fix route.`);
|
|
4861
|
+
}
|
|
4862
|
+
if (route === 'replan') {
|
|
4863
|
+
const conflict = finding.planConflict;
|
|
4864
|
+
if (!conflict
|
|
4865
|
+
|| !conflict.planClause?.trim()
|
|
4866
|
+
|| !conflict.whyFixCannotFit?.trim()
|
|
4867
|
+
|| !conflict.minimalPlanChange?.trim()) {
|
|
4868
|
+
throw new WorkflowError('INVALID_ARGUMENT', `Review finding ${finding.id} requires exact Plan-conflict evidence for replan.`);
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4565
4872
|
}
|
|
4566
4873
|
function countFailedReviewAttempts(taskRoot) {
|
|
4567
4874
|
const file = path.join(taskRoot, 'findings.jsonl');
|
|
@@ -4581,34 +4888,6 @@ function countFailedReviewAttempts(taskRoot) {
|
|
|
4581
4888
|
}
|
|
4582
4889
|
return attempts.size;
|
|
4583
4890
|
}
|
|
4584
|
-
function validateCorrectivePlanAudit(task, audit, failedReviewAttempts) {
|
|
4585
|
-
if (!audit) {
|
|
4586
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has ${failedReviewAttempts} failed independent reviews. A separate corrective Plan audit is required before another plan-set.`, {
|
|
4587
|
-
failedReviewAttempts,
|
|
4588
|
-
requiredAction: 'Run a separate read-only Auditor and pass its evidence through --corrective-audit-file.',
|
|
4589
|
-
});
|
|
4590
|
-
}
|
|
4591
|
-
if (!audit.auditor.trim() || !audit.summary.trim()) {
|
|
4592
|
-
throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit requires auditor and summary.');
|
|
4593
|
-
}
|
|
4594
|
-
if (audit.auditor.trim() === task.review.reviewer.trim()) {
|
|
4595
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan audit must be performed by an Auditor distinct from the failed-review author.');
|
|
4596
|
-
}
|
|
4597
|
-
const latestFindingIds = new Set(task.review.findings.map((finding) => finding.id));
|
|
4598
|
-
const reviewedFindingIds = new Set(audit.reviewedFindingIds);
|
|
4599
|
-
if (reviewedFindingIds.size !== audit.reviewedFindingIds.length || audit.reviewedFindingIds.some((id) => !id.trim())) {
|
|
4600
|
-
throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit finding IDs must be non-empty and unique.');
|
|
4601
|
-
}
|
|
4602
|
-
const missing = [...latestFindingIds].filter((findingId) => !reviewedFindingIds.has(findingId));
|
|
4603
|
-
if (missing.length > 0) {
|
|
4604
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan audit does not cover every latest finding.', {
|
|
4605
|
-
missingFindingIds: missing,
|
|
4606
|
-
});
|
|
4607
|
-
}
|
|
4608
|
-
if (audit.decision !== 'continue-fix') {
|
|
4609
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Corrective Auditor decision ${audit.decision} requires coordinator/user redirection before implementation continues.`, { decision: audit.decision, summary: audit.summary });
|
|
4610
|
-
}
|
|
4611
|
-
}
|
|
4612
4891
|
function appendCorrectivePlanAudit(taskRoot, previous, saved, failedReviewAttempts, audit, now) {
|
|
4613
4892
|
appendFileSync(path.join(taskRoot, 'corrective-plan-audits.jsonl'), `${JSON.stringify({
|
|
4614
4893
|
recordedAt: now.toISOString(),
|
|
@@ -4744,12 +5023,25 @@ function validateMilestonePlan(plan) {
|
|
|
4744
5023
|
if (plan.acceptance.length === 0 || plan.acceptance.some((value) => !value.trim())) {
|
|
4745
5024
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone acceptance must contain substantive entries.');
|
|
4746
5025
|
}
|
|
4747
|
-
if (plan.memberships.length === 0) {
|
|
5026
|
+
if (!Array.isArray(plan.memberships) || plan.memberships.length === 0) {
|
|
4748
5027
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone requires at least one Task membership.');
|
|
4749
5028
|
}
|
|
4750
5029
|
for (const membership of plan.memberships) {
|
|
4751
|
-
if (!membership.
|
|
5030
|
+
if (!membership || typeof membership !== 'object' || Array.isArray(membership)) {
|
|
5031
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership must be an object.');
|
|
5032
|
+
}
|
|
5033
|
+
if (typeof membership.taskId !== 'string' || !membership.taskId.trim()) {
|
|
4752
5034
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership requires Task ID.');
|
|
5035
|
+
}
|
|
5036
|
+
if (typeof membership.reason !== 'string') {
|
|
5037
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership reason must be a string.');
|
|
5038
|
+
}
|
|
5039
|
+
if (!Array.isArray(membership.dependsOnTaskIds)) {
|
|
5040
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Every Milestone membership must declare dependsOnTaskIds explicitly.');
|
|
5041
|
+
}
|
|
5042
|
+
if (membership.dependsOnTaskIds.some((taskId) => typeof taskId !== 'string' || !taskId.trim())) {
|
|
5043
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone dependency IDs must be non-empty strings.');
|
|
5044
|
+
}
|
|
4753
5045
|
if (membership.disposition !== 'required' && !membership.reason.trim()) {
|
|
4754
5046
|
throw new WorkflowError('INVALID_ARGUMENT', `Milestone ${membership.disposition} membership requires a reason.`);
|
|
4755
5047
|
}
|
|
@@ -4779,7 +5071,7 @@ function taskCanRebindKnowledge(task) {
|
|
|
4779
5071
|
'awaiting_final_acceptance',
|
|
4780
5072
|
].includes(task.status);
|
|
4781
5073
|
}
|
|
4782
|
-
function taskIsActionableFromRepositoryNext(task, milestonesById) {
|
|
5074
|
+
function taskIsActionableFromRepositoryNext(task, milestonesById, tasksById) {
|
|
4783
5075
|
if (task.status === 'merged' || task.status === 'cancelled')
|
|
4784
5076
|
return false;
|
|
4785
5077
|
if (!task.milestoneId)
|
|
@@ -4787,10 +5079,18 @@ function taskIsActionableFromRepositoryNext(task, milestonesById) {
|
|
|
4787
5079
|
const milestone = milestonesById.get(task.milestoneId);
|
|
4788
5080
|
if (!milestone)
|
|
4789
5081
|
return false;
|
|
4790
|
-
if (
|
|
4791
|
-
return
|
|
4792
|
-
const
|
|
4793
|
-
|
|
5082
|
+
if (milestone.status !== 'active')
|
|
5083
|
+
return false;
|
|
5084
|
+
const assessment = assessMilestoneTaskDependencies(milestone, task.id, (candidateTaskId) => {
|
|
5085
|
+
const candidate = tasksById.get(candidateTaskId);
|
|
5086
|
+
if (!candidate)
|
|
5087
|
+
throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
|
|
5088
|
+
return candidate;
|
|
5089
|
+
});
|
|
5090
|
+
return assessment.runnable;
|
|
5091
|
+
}
|
|
5092
|
+
function structuralReplacementIsDisabled() {
|
|
5093
|
+
return true;
|
|
4794
5094
|
}
|
|
4795
5095
|
function milestoneFinalAcceptanceGate(milestone, headCommit) {
|
|
4796
5096
|
if (!milestone.resultHash || !milestone.evidenceHash) {
|
|
@@ -4920,6 +5220,7 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
|
|
|
4920
5220
|
'task review-sealed-record',
|
|
4921
5221
|
'task corrective-decision-recover',
|
|
4922
5222
|
'task remediation-mode-recover',
|
|
5223
|
+
'task plan-integrity-recover',
|
|
4923
5224
|
'task corrective-decision',
|
|
4924
5225
|
'task corrective-yield',
|
|
4925
5226
|
'task handoff',
|