codex-workflow-v2 2.0.0-beta.10 → 2.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -9
- 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 +2 -1
- package/dist/src/alpha6/remediation.js +168 -3
- package/dist/src/alpha6/remediation.js.map +1 -1
- package/dist/src/cli.js +13 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +69 -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/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 +399 -90
- package/dist/src/workflow.js.map +1 -1
- package/docs/autonomy-guardrails.md +21 -0
- package/docs/beta11-plan-integrity-recovery-brief.md +38 -0
- package/docs/development-flow.md +31 -6
- package/docs/lifecycle/state-machine-stabilization.md +1 -1
- 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 +234 -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 +413 -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 +15 -6
- package/docs/split-required-recovery.md +19 -26
- package/docs/validation-report.md +116 -77
- package/package.json +5 -1
- package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +19 -2
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +17 -3
- package/references/state-machine.md +9 -5
- 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/task-handoff-event.schema.json +18 -2
- package/scripts/generate-pdf-docs.py +512 -0
- package/scripts/run-pdf-docs.mjs +62 -0
package/dist/src/workflow.js
CHANGED
|
@@ -13,8 +13,10 @@ 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
21
|
import { buildCurrentStrictStepReviewCycle, countUnverifiedStrictStepReviews, ensurePendingStrictStepReview, isStepReviewRequired, latestPassedVerifiedStepReviewEvent, recordStrictStepReview, verifiedHistoricalStepReviewCommits, } from './alpha6/review.js';
|
|
20
22
|
import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.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');
|
|
@@ -1397,7 +1413,24 @@ export class WorkflowService {
|
|
|
1397
1413
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
1398
1414
|
const briefHash = this.store.hashArtifact(root, 'brief.md');
|
|
1399
1415
|
const planHash = this.store.hashArtifact(root, 'plan.md');
|
|
1416
|
+
if (task.briefHash !== briefHash || task.planHash !== planHash) {
|
|
1417
|
+
throw new WorkflowError('STATE_CORRUPT', 'Task Plan authority does not match its stored artifacts.', {
|
|
1418
|
+
taskId: task.id,
|
|
1419
|
+
stateBriefHash: task.briefHash,
|
|
1420
|
+
artifactBriefHash: briefHash,
|
|
1421
|
+
statePlanHash: task.planHash,
|
|
1422
|
+
artifactPlanHash: planHash,
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1400
1425
|
readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: true });
|
|
1426
|
+
const mechanicalFeasibility = evaluateMechanicalFeasibility(identity.repositoryRoot, task, briefHash, planHash);
|
|
1427
|
+
if (mechanicalFeasibility.decision === 'blocked') {
|
|
1428
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Task Plan contains a proven mechanical contradiction.', {
|
|
1429
|
+
diagnosticCode: 'MECHANICAL_PLAN_INFEASIBILITY',
|
|
1430
|
+
mechanicalFeasibility,
|
|
1431
|
+
requiredAction: 'task plan-set',
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1401
1434
|
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
1402
1435
|
const event = {
|
|
1403
1436
|
kind: 'execution',
|
|
@@ -1407,11 +1440,12 @@ export class WorkflowService {
|
|
|
1407
1440
|
briefHash,
|
|
1408
1441
|
planHash,
|
|
1409
1442
|
resultHash: null,
|
|
1410
|
-
evidenceHash:
|
|
1411
|
-
headCommit:
|
|
1443
|
+
evidenceHash: hashMechanicalFeasibilityEvidence(mechanicalFeasibility),
|
|
1444
|
+
headCommit: mechanicalFeasibility.evaluatedHead,
|
|
1412
1445
|
reason,
|
|
1413
1446
|
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1414
1447
|
...(delegation ? { delegation } : {}),
|
|
1448
|
+
mechanicalFeasibility,
|
|
1415
1449
|
};
|
|
1416
1450
|
return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
|
|
1417
1451
|
}
|
|
@@ -1443,6 +1477,9 @@ export class WorkflowService {
|
|
|
1443
1477
|
if (grantId)
|
|
1444
1478
|
this.assertHandoffGrant(context.identity.projectId, task, targetActor, grantId);
|
|
1445
1479
|
const milestone = task.milestoneId ? this.readMilestoneCurrent(task.projectId, task.milestoneId) : null;
|
|
1480
|
+
const dependencyBinding = milestone
|
|
1481
|
+
? this.currentMilestoneDependencyBinding(milestone, task.id)
|
|
1482
|
+
: null;
|
|
1446
1483
|
const milestones = milestone ? this.listMilestonesCurrent(task.projectId) : [];
|
|
1447
1484
|
const milestoneDisplayNumber = milestone
|
|
1448
1485
|
? milestones.findIndex((candidate) => candidate.id === milestone.id) + 1
|
|
@@ -1488,6 +1525,7 @@ export class WorkflowService {
|
|
|
1488
1525
|
writerToken: lease.token,
|
|
1489
1526
|
milestoneDisplayNumber,
|
|
1490
1527
|
taskDisplayNumber,
|
|
1528
|
+
dependencyBinding,
|
|
1491
1529
|
expectedNext: String(nextForTask(task).action ?? 'task show'),
|
|
1492
1530
|
grantId,
|
|
1493
1531
|
expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
|
|
@@ -1510,7 +1548,11 @@ export class WorkflowService {
|
|
|
1510
1548
|
if (!normalizedActor) {
|
|
1511
1549
|
throw new WorkflowError('INVALID_ARGUMENT', 'Claim actor is required.');
|
|
1512
1550
|
}
|
|
1513
|
-
|
|
1551
|
+
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
1552
|
+
const dependencyBinding = task.milestoneId
|
|
1553
|
+
? this.currentMilestoneDependencyBinding(this.readMilestoneCurrent(task.projectId, task.milestoneId), task.id)
|
|
1554
|
+
: null;
|
|
1555
|
+
const pendingHandoff = assertTaskClaimAllowed(this.store, task, normalizedActor, claimToken, this.now(), dependencyBinding);
|
|
1514
1556
|
if (pendingHandoff.latestEvent.grantId) {
|
|
1515
1557
|
this.assertHandoffGrant(context.identity.projectId, task, normalizedActor, pendingHandoff.latestEvent.grantId);
|
|
1516
1558
|
}
|
|
@@ -1543,7 +1585,7 @@ export class WorkflowService {
|
|
|
1543
1585
|
lease = context.locks.acquire(taskId, normalizedActor);
|
|
1544
1586
|
}
|
|
1545
1587
|
try {
|
|
1546
|
-
const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now());
|
|
1588
|
+
const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now(), dependencyBinding);
|
|
1547
1589
|
return {
|
|
1548
1590
|
task,
|
|
1549
1591
|
event,
|
|
@@ -1567,6 +1609,22 @@ export class WorkflowService {
|
|
|
1567
1609
|
if (!normalizedActor)
|
|
1568
1610
|
throw new WorkflowError('INVALID_ARGUMENT', 'Writer credential replacement actor is required.');
|
|
1569
1611
|
const posture = readTaskC1Posture(this.store, task);
|
|
1612
|
+
if (posture.state === 'claimed' && task.milestoneId) {
|
|
1613
|
+
const dependencyBinding = this.currentMilestoneDependencyBinding(this.readMilestoneCurrent(task.projectId, task.milestoneId), task.id);
|
|
1614
|
+
const claimedBinding = posture.latestEvent.dependencyBinding ?? null;
|
|
1615
|
+
if (claimedBinding === null
|
|
1616
|
+
|| claimedBinding.milestoneRevision !== dependencyBinding.milestoneRevision
|
|
1617
|
+
|| claimedBinding.membershipRevision !== dependencyBinding.membershipRevision
|
|
1618
|
+
|| claimedBinding.milestonePlanHash !== dependencyBinding.milestonePlanHash) {
|
|
1619
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Writer credential replacement requires a current Milestone dependency binding.', {
|
|
1620
|
+
diagnosticCode: 'MILESTONE_DEPENDENCY_BINDING_STALE',
|
|
1621
|
+
taskId: task.id,
|
|
1622
|
+
milestoneId: task.milestoneId,
|
|
1623
|
+
claimDependencyBinding: claimedBinding,
|
|
1624
|
+
currentDependencyBinding: dependencyBinding,
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1570
1628
|
const navigation = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
1571
1629
|
const action = String(navigation.action);
|
|
1572
1630
|
const currentLease = context.locks.inspect(taskId);
|
|
@@ -1615,17 +1673,10 @@ export class WorkflowService {
|
|
|
1615
1673
|
this.requireExecutionAdoptionPosture(identity, taskId);
|
|
1616
1674
|
if (task.status !== 'ready')
|
|
1617
1675
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is ${task.status}.`);
|
|
1618
|
-
this.assertExecutionAuthorizationFresh(task);
|
|
1676
|
+
const executionAuthorization = this.assertExecutionAuthorizationFresh(task, identity.repositoryRoot);
|
|
1619
1677
|
if (task.milestoneId) {
|
|
1620
1678
|
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
|
-
}
|
|
1679
|
+
const dependencyBinding = this.currentMilestoneDependencyBinding(milestone, task.id);
|
|
1629
1680
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
1630
1681
|
if (handoffPosture.state !== 'claimed') {
|
|
1631
1682
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a prepared and claimed C1 handoff bundle.', {
|
|
@@ -1635,6 +1686,20 @@ export class WorkflowService {
|
|
|
1635
1686
|
requiredAction: handoffPosture.state === 'pending' ? 'Claim the prepared handoff.' : 'Prepare and claim a C1 handoff.',
|
|
1636
1687
|
});
|
|
1637
1688
|
}
|
|
1689
|
+
const claimBinding = handoffPosture.latestEvent.dependencyBinding ?? null;
|
|
1690
|
+
if (claimBinding === null
|
|
1691
|
+
|| claimBinding.milestoneRevision !== dependencyBinding.milestoneRevision
|
|
1692
|
+
|| claimBinding.membershipRevision !== dependencyBinding.membershipRevision
|
|
1693
|
+
|| claimBinding.milestonePlanHash !== dependencyBinding.milestonePlanHash) {
|
|
1694
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a current dependency-bound C1 claim.', {
|
|
1695
|
+
diagnosticCode: 'MILESTONE_DEPENDENCY_BINDING_STALE',
|
|
1696
|
+
taskId: task.id,
|
|
1697
|
+
milestoneId: milestone.id,
|
|
1698
|
+
claimDependencyBinding: handoffPosture.latestEvent.dependencyBinding ?? null,
|
|
1699
|
+
currentDependencyBinding: dependencyBinding,
|
|
1700
|
+
requiredAction: 'Prepare and claim a new C1 handoff from the current Milestone Plan.',
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1638
1703
|
}
|
|
1639
1704
|
if (!isClean(identity.repositoryRoot)) {
|
|
1640
1705
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start requires a clean checkout.', {
|
|
@@ -1644,12 +1709,22 @@ export class WorkflowService {
|
|
|
1644
1709
|
const branch = `codex/task-${task.id.slice(5, 15).toLowerCase()}-${task.slug}`;
|
|
1645
1710
|
let baseCommit;
|
|
1646
1711
|
let taskBranch;
|
|
1712
|
+
const authorizedBaseCommit = executionAuthorization.mechanicalFeasibility?.sourceKind === 'pre-start-base'
|
|
1713
|
+
? executionAuthorization.mechanicalFeasibility.evaluatedHead
|
|
1714
|
+
: null;
|
|
1647
1715
|
if (workspaceOwner === 'local') {
|
|
1648
|
-
baseCommit = startLocalTaskBranch(identity.repositoryRoot, branch, task.baseBranch);
|
|
1716
|
+
baseCommit = startLocalTaskBranch(identity.repositoryRoot, branch, task.baseBranch, authorizedBaseCommit);
|
|
1649
1717
|
taskBranch = branch;
|
|
1650
1718
|
}
|
|
1651
1719
|
else {
|
|
1652
1720
|
baseCommit = runGit(identity.repositoryRoot, ['rev-parse', task.baseBranch]);
|
|
1721
|
+
if (authorizedBaseCommit !== null && baseCommit !== authorizedBaseCommit) {
|
|
1722
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start base moved after execution authorization.', {
|
|
1723
|
+
baseBranch: task.baseBranch,
|
|
1724
|
+
authorizedBaseCommit,
|
|
1725
|
+
currentBaseCommit: baseCommit,
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1653
1728
|
taskBranch = currentBranch(identity.repositoryRoot);
|
|
1654
1729
|
if (!taskBranch || taskBranch === task.baseBranch) {
|
|
1655
1730
|
throw new WorkflowError('GIT_PRECONDITION_FAILED', `Externally owned execution requires a dedicated branch different from ${task.baseBranch}.`);
|
|
@@ -1668,7 +1743,7 @@ export class WorkflowService {
|
|
|
1668
1743
|
if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
|
|
1669
1744
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run a Step while ${task.status}.`);
|
|
1670
1745
|
}
|
|
1671
|
-
this.assertExecutionAuthorizationFresh(task);
|
|
1746
|
+
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
1672
1747
|
const currentPlanBlockingDecisions = readCorrectiveDecisionEvents(this.store, task)
|
|
1673
1748
|
.filter((event) => event.planHash === task.planHash && event.decision !== 'continue-fix');
|
|
1674
1749
|
if (currentPlanBlockingDecisions.length > 0) {
|
|
@@ -2304,7 +2379,7 @@ export class WorkflowService {
|
|
|
2304
2379
|
if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
|
|
2305
2380
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot recover a corrective decision while ${task.status}.`);
|
|
2306
2381
|
}
|
|
2307
|
-
this.assertExecutionAuthorizationFresh(task);
|
|
2382
|
+
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
2308
2383
|
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
2309
2384
|
return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
|
|
2310
2385
|
}
|
|
@@ -2315,6 +2390,47 @@ export class WorkflowService {
|
|
|
2315
2390
|
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
2316
2391
|
return appendRemediationModeRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
|
|
2317
2392
|
}
|
|
2393
|
+
recoverPlanIntegrity(repository, taskId, stepId, expectedRevision, actor, writerToken) {
|
|
2394
|
+
const context = this.#mutationContext(repository);
|
|
2395
|
+
const task = this.store.readTask(context.identity.projectId, taskId);
|
|
2396
|
+
assertExpectedRevision(task, expectedRevision);
|
|
2397
|
+
this.requireExecutionAdoptionPosture(context.identity, taskId);
|
|
2398
|
+
this.assertExecutionAuthorizationFresh(task, context.identity.repositoryRoot);
|
|
2399
|
+
this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
|
|
2400
|
+
assertTaskBranch(context.identity.repositoryRoot, task);
|
|
2401
|
+
this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
|
|
2402
|
+
this.assertTaskMutationAllowedByHandoff(task, actor, writerToken);
|
|
2403
|
+
const handoff = readTaskC1Posture(this.store, task);
|
|
2404
|
+
const requiredActor = handoff.state === 'claimed'
|
|
2405
|
+
? handoff.claimant
|
|
2406
|
+
: defaultPlanIntegrityRecoveryActor(task.id);
|
|
2407
|
+
if (actor.trim() !== requiredActor) {
|
|
2408
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity recovery actor does not match the fresh Workflow route.', {
|
|
2409
|
+
taskId,
|
|
2410
|
+
stepId,
|
|
2411
|
+
requiredActor,
|
|
2412
|
+
actualActor: actor.trim(),
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
|
|
2416
|
+
if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
|
|
2417
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Plan-integrity recovery requires the failed Step to be guarded by the current Plan Risk Audit.', {
|
|
2418
|
+
taskId,
|
|
2419
|
+
stepId,
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
const assessment = assessPlanIntegrityRecovery(this.store, context.identity.repositoryRoot, task, stepId);
|
|
2423
|
+
if (!assessment.applicable || !assessment.eligible || !assessment.evidence) {
|
|
2424
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Bounded Plan-integrity recovery is not eligible for the current Task state.', {
|
|
2425
|
+
taskId,
|
|
2426
|
+
stepId,
|
|
2427
|
+
applicable: assessment.applicable,
|
|
2428
|
+
blockers: assessment.blockers,
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
const event = appendPlanIntegrityRecoveryDecision(this.store, task, stepId, defaultPlanIntegrityRecoveryActor(task.id), assessment.evidence, this.now());
|
|
2432
|
+
return { assessment, event };
|
|
2433
|
+
}
|
|
2318
2434
|
recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
|
|
2319
2435
|
const { identity } = this.#mutationContext(repository);
|
|
2320
2436
|
this.readCurrentAdoptionPostureStrict(identity.projectId);
|
|
@@ -2693,6 +2809,13 @@ export class WorkflowService {
|
|
|
2693
2809
|
if (!task)
|
|
2694
2810
|
throw new WorkflowError('STATE_CORRUPT', `Milestone member ${membership.taskId} does not exist.`);
|
|
2695
2811
|
const stepsCompleted = task.steps.filter((step) => ['completed', 'skipped'].includes(step.status)).length;
|
|
2812
|
+
const dependency = assessMilestoneTaskDependencies(milestone, task.id, (candidateTaskId) => {
|
|
2813
|
+
const candidate = tasksById.get(candidateTaskId);
|
|
2814
|
+
if (!candidate)
|
|
2815
|
+
throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
|
|
2816
|
+
return candidate;
|
|
2817
|
+
});
|
|
2818
|
+
const executionEligible = milestone.status === 'active' && !['merged', 'cancelled'].includes(task.status);
|
|
2696
2819
|
return {
|
|
2697
2820
|
ordinal: `T${String(index + 1).padStart(2, '0')}`,
|
|
2698
2821
|
taskId: task.id,
|
|
@@ -2701,6 +2824,15 @@ export class WorkflowService {
|
|
|
2701
2824
|
revision: task.revision,
|
|
2702
2825
|
disposition: membership.disposition,
|
|
2703
2826
|
membershipReason: membership.reason,
|
|
2827
|
+
dependencyState: dependency.dependencyState,
|
|
2828
|
+
dependsOnTaskIds: dependency.dependsOnTaskIds,
|
|
2829
|
+
unsatisfiedPredecessors: dependency.unsatisfiedPredecessors,
|
|
2830
|
+
runnable: executionEligible && dependency.runnable,
|
|
2831
|
+
dependencyBlockReason: !executionEligible
|
|
2832
|
+
? milestone.status !== 'active'
|
|
2833
|
+
? `Milestone is ${milestone.status}, not active.`
|
|
2834
|
+
: `Task is terminal (${task.status}).`
|
|
2835
|
+
: dependency.blockReason,
|
|
2704
2836
|
replacementForTaskId: task.replacementForTaskId ?? null,
|
|
2705
2837
|
replacedByTaskId: task.replacedByTaskId ?? null,
|
|
2706
2838
|
stepsCompleted,
|
|
@@ -3030,21 +3162,34 @@ export class WorkflowService {
|
|
|
3030
3162
|
const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
|
|
3031
3163
|
const inspectCurrentKnowledgeMap = () => inspectKnowledgeMap(this.store.readKnowledgeMap(status.projectId), scanProjectKnowledge(repositoryRoot, status.projectId, this.now()));
|
|
3032
3164
|
const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
|
|
3165
|
+
const tasksById = new Map(status.tasks.map((task) => [task.id, task]));
|
|
3033
3166
|
const requestedTask = taskId === null
|
|
3034
3167
|
? null
|
|
3035
3168
|
: status.tasks.find((task) => task.id === taskId) ?? null;
|
|
3036
3169
|
if (taskId !== null && !requestedTask) {
|
|
3037
3170
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
3038
3171
|
}
|
|
3039
|
-
if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById)) {
|
|
3172
|
+
if (requestedTask && !taskIsActionableFromRepositoryNext(requestedTask, milestonesById, tasksById)) {
|
|
3173
|
+
const requestedMilestone = requestedTask.milestoneId
|
|
3174
|
+
? milestonesById.get(requestedTask.milestoneId) ?? null
|
|
3175
|
+
: null;
|
|
3176
|
+
const dependency = requestedMilestone
|
|
3177
|
+
? assessMilestoneTaskDependencies(requestedMilestone, requestedTask.id, (candidateTaskId) => {
|
|
3178
|
+
const candidate = tasksById.get(candidateTaskId);
|
|
3179
|
+
if (!candidate)
|
|
3180
|
+
throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
|
|
3181
|
+
return candidate;
|
|
3182
|
+
})
|
|
3183
|
+
: null;
|
|
3040
3184
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${requestedTask.id} is not actionable from repository next.`, {
|
|
3041
3185
|
taskId: requestedTask.id,
|
|
3042
3186
|
taskStatus: requestedTask.status,
|
|
3043
3187
|
milestoneId: requestedTask.milestoneId,
|
|
3188
|
+
...(dependency ? { dependency } : {}),
|
|
3044
3189
|
});
|
|
3045
3190
|
}
|
|
3046
3191
|
const activeTask = requestedTask
|
|
3047
|
-
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
|
|
3192
|
+
?? status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById, tasksById));
|
|
3048
3193
|
if (activeTask) {
|
|
3049
3194
|
const dependencyRecovery = this.dependencyProvenanceRecoveryPreflight(repositoryRoot, activeTask.id);
|
|
3050
3195
|
if (dependencyRecovery.eligible) {
|
|
@@ -3109,7 +3254,7 @@ export class WorkflowService {
|
|
|
3109
3254
|
const taskNext = activeTask.status === 'planning'
|
|
3110
3255
|
? null
|
|
3111
3256
|
: planRiskAuditRecovery
|
|
3112
|
-
?? this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
|
|
3257
|
+
?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, activePlanRiskAudit);
|
|
3113
3258
|
// Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
|
|
3114
3259
|
// strict review are executable while context refresh correctly remains forbidden.
|
|
3115
3260
|
const hardStop = taskNext?.action === 'task corrective-decision'
|
|
@@ -3118,6 +3263,8 @@ export class WorkflowService {
|
|
|
3118
3263
|
if (taskNext?.action === 'task step-complete'
|
|
3119
3264
|
|| taskNext?.action === 'task step-review'
|
|
3120
3265
|
|| taskNext?.action === 'task remediation-mode-recover'
|
|
3266
|
+
|| taskNext?.action === 'task plan-integrity-recover'
|
|
3267
|
+
|| taskNext?.blockedAction === 'task plan-integrity-recover'
|
|
3121
3268
|
|| hardStop
|
|
3122
3269
|
|| terminalCorrectiveDecision === 'split-required'
|
|
3123
3270
|
|| terminalCorrectiveDecision === 'stop-escalate') {
|
|
@@ -3216,7 +3363,7 @@ export class WorkflowService {
|
|
|
3216
3363
|
knowledgeMapHash,
|
|
3217
3364
|
}));
|
|
3218
3365
|
}
|
|
3219
|
-
return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null)));
|
|
3366
|
+
return withAdoption(withMilestoneCancellation(taskNext ?? this.nextForTaskWithDelegation(status.projectId, repositoryRoot, activeTask, null)));
|
|
3220
3367
|
}
|
|
3221
3368
|
let repositoryKnowledgeMap;
|
|
3222
3369
|
try {
|
|
@@ -3351,7 +3498,21 @@ export class WorkflowService {
|
|
|
3351
3498
|
.filter((membership) => membership.disposition === 'required')
|
|
3352
3499
|
.map((membership) => {
|
|
3353
3500
|
const task = status.tasks.find((candidate) => candidate.id === membership.taskId);
|
|
3354
|
-
|
|
3501
|
+
const dependency = assessMilestoneTaskDependencies(activeMilestone, membership.taskId, (candidateTaskId) => {
|
|
3502
|
+
const candidate = status.tasks.find((taskCandidate) => taskCandidate.id === candidateTaskId);
|
|
3503
|
+
if (!candidate)
|
|
3504
|
+
throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
|
|
3505
|
+
return candidate;
|
|
3506
|
+
});
|
|
3507
|
+
return {
|
|
3508
|
+
taskId: membership.taskId,
|
|
3509
|
+
status: task?.status ?? 'missing',
|
|
3510
|
+
dependencyState: dependency.dependencyState,
|
|
3511
|
+
dependsOnTaskIds: dependency.dependsOnTaskIds,
|
|
3512
|
+
unsatisfiedPredecessors: dependency.unsatisfiedPredecessors,
|
|
3513
|
+
runnable: Boolean(task && !['merged', 'cancelled'].includes(task.status) && dependency.runnable),
|
|
3514
|
+
dependencyBlockReason: dependency.blockReason,
|
|
3515
|
+
};
|
|
3355
3516
|
}),
|
|
3356
3517
|
},
|
|
3357
3518
|
}
|
|
@@ -3474,7 +3635,21 @@ export class WorkflowService {
|
|
|
3474
3635
|
}
|
|
3475
3636
|
}
|
|
3476
3637
|
}
|
|
3477
|
-
|
|
3638
|
+
currentMilestoneDependencyBinding(milestone, taskId) {
|
|
3639
|
+
assertMilestoneTaskRunnable(milestone, taskId, (candidateTaskId) => this.store.readTask(milestone.projectId, candidateTaskId));
|
|
3640
|
+
if (!milestone.planHash) {
|
|
3641
|
+
throw new WorkflowError('STATE_CORRUPT', `Active Milestone ${milestone.id} has no Plan hash.`, {
|
|
3642
|
+
milestoneId: milestone.id,
|
|
3643
|
+
taskId,
|
|
3644
|
+
});
|
|
3645
|
+
}
|
|
3646
|
+
return {
|
|
3647
|
+
milestoneRevision: milestone.revision,
|
|
3648
|
+
membershipRevision: milestone.membershipRevision,
|
|
3649
|
+
milestonePlanHash: milestone.planHash,
|
|
3650
|
+
};
|
|
3651
|
+
}
|
|
3652
|
+
nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
|
|
3478
3653
|
const transition = task.status === 'awaiting_execution_authorization'
|
|
3479
3654
|
? 'task.execution_authorize'
|
|
3480
3655
|
: task.status === 'awaiting_final_acceptance'
|
|
@@ -3510,7 +3685,59 @@ export class WorkflowService {
|
|
|
3510
3685
|
}
|
|
3511
3686
|
}
|
|
3512
3687
|
const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
3688
|
+
let mechanicalFeasibilityOverlay = {};
|
|
3689
|
+
if (task.status === 'awaiting_execution_authorization' && task.planHash) {
|
|
3690
|
+
try {
|
|
3691
|
+
const taskRoot = this.store.taskRoot(projectId, task.id);
|
|
3692
|
+
const briefHash = this.store.hashArtifact(taskRoot, 'brief.md');
|
|
3693
|
+
const planHash = this.store.hashArtifact(taskRoot, 'plan.md');
|
|
3694
|
+
if (task.briefHash !== briefHash || task.planHash !== planHash) {
|
|
3695
|
+
throw new WorkflowError('STATE_CORRUPT', 'Task Plan authority does not match its stored artifacts.', {
|
|
3696
|
+
taskId: task.id,
|
|
3697
|
+
stateBriefHash: task.briefHash,
|
|
3698
|
+
artifactBriefHash: briefHash,
|
|
3699
|
+
statePlanHash: task.planHash,
|
|
3700
|
+
artifactPlanHash: planHash,
|
|
3701
|
+
});
|
|
3702
|
+
}
|
|
3703
|
+
const mechanicalFeasibility = evaluateMechanicalFeasibility(repositoryRoot, task, briefHash, planHash);
|
|
3704
|
+
mechanicalFeasibilityOverlay = mechanicalFeasibility.decision === 'blocked'
|
|
3705
|
+
? {
|
|
3706
|
+
action: 'task plan-set',
|
|
3707
|
+
blockedAction: 'task authorize',
|
|
3708
|
+
mechanicalFeasibility,
|
|
3709
|
+
mechanicalFeasibilityGate: {
|
|
3710
|
+
state: 'blocked',
|
|
3711
|
+
diagnosticCode: 'MECHANICAL_PLAN_INFEASIBILITY',
|
|
3712
|
+
requiredAction: 'Replace the current Plan through task plan-set, then request fresh execution authorization.',
|
|
3713
|
+
},
|
|
3714
|
+
}
|
|
3715
|
+
: {
|
|
3716
|
+
mechanicalFeasibility,
|
|
3717
|
+
mechanicalFeasibilityGate: {
|
|
3718
|
+
state: 'clear',
|
|
3719
|
+
meaning: 'No supported analyzer proved infeasibility; semantic sufficiency is not asserted.',
|
|
3720
|
+
},
|
|
3721
|
+
};
|
|
3722
|
+
}
|
|
3723
|
+
catch (error) {
|
|
3724
|
+
mechanicalFeasibilityOverlay = {
|
|
3725
|
+
action: 'doctor',
|
|
3726
|
+
blockedAction: 'task authorize',
|
|
3727
|
+
mechanicalFeasibilityGate: {
|
|
3728
|
+
state: 'unavailable',
|
|
3729
|
+
diagnosticCode: error instanceof WorkflowError ? error.code : 'UNKNOWN_ERROR',
|
|
3730
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
3731
|
+
requiredAction: 'Restore the exact Plan/source authority before execution authorization.',
|
|
3732
|
+
},
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3513
3736
|
const handoffPosture = readTaskC1Posture(this.store, task);
|
|
3737
|
+
const failedStep = task.steps.find((step) => step.status === 'failed') ?? null;
|
|
3738
|
+
const planIntegrityAssessment = !remediationModeRecovery && planRiskAudit && failedStep
|
|
3739
|
+
? assessPlanIntegrityRecovery(this.store, repositoryRoot, task, failedStep.id)
|
|
3740
|
+
: null;
|
|
3514
3741
|
const strictStepReviewStep = planRiskAudit
|
|
3515
3742
|
? task.steps.find((step) => {
|
|
3516
3743
|
if (step.status !== 'in_progress' || !step.evidence || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
|
|
@@ -3623,6 +3850,39 @@ export class WorkflowService {
|
|
|
3623
3850
|
};
|
|
3624
3851
|
};
|
|
3625
3852
|
let guardedCorrectiveOverlay = {};
|
|
3853
|
+
if (planIntegrityAssessment?.applicable) {
|
|
3854
|
+
const requiredActor = handoffPosture.state === 'claimed'
|
|
3855
|
+
? handoffPosture.claimant
|
|
3856
|
+
: defaultPlanIntegrityRecoveryActor(task.id);
|
|
3857
|
+
guardedCorrectiveOverlay = planIntegrityAssessment.eligible
|
|
3858
|
+
? {
|
|
3859
|
+
action: 'task plan-integrity-recover',
|
|
3860
|
+
blockedAction: 'task run',
|
|
3861
|
+
stepId: planIntegrityAssessment.stepId,
|
|
3862
|
+
planIntegrityRecovery: {
|
|
3863
|
+
state: 'eligible',
|
|
3864
|
+
reasonCode: 'MISSING_NPM_SCRIPT_OUTSIDE_ALLOWED_WRITES',
|
|
3865
|
+
expectedRevision: task.revision,
|
|
3866
|
+
requiredActor,
|
|
3867
|
+
evidence: planIntegrityAssessment.evidence,
|
|
3868
|
+
boundedEffect: 'record-replan-required-decision',
|
|
3869
|
+
preservesWorktree: true,
|
|
3870
|
+
},
|
|
3871
|
+
}
|
|
3872
|
+
: {
|
|
3873
|
+
action: 'doctor',
|
|
3874
|
+
blockedAction: 'task plan-integrity-recover',
|
|
3875
|
+
stepId: planIntegrityAssessment.stepId,
|
|
3876
|
+
planIntegrityRecovery: {
|
|
3877
|
+
state: 'blocked',
|
|
3878
|
+
reasonCode: 'PLAN_INTEGRITY_RECOVERY_BLOCKED',
|
|
3879
|
+
expectedRevision: task.revision,
|
|
3880
|
+
requiredActor,
|
|
3881
|
+
blockers: planIntegrityAssessment.blockers,
|
|
3882
|
+
evidence: planIntegrityAssessment.evidence,
|
|
3883
|
+
},
|
|
3884
|
+
};
|
|
3885
|
+
}
|
|
3626
3886
|
if (remediationModeRecovery) {
|
|
3627
3887
|
guardedCorrectiveOverlay = {
|
|
3628
3888
|
action: 'task remediation-mode-recover',
|
|
@@ -3726,13 +3986,29 @@ export class WorkflowService {
|
|
|
3726
3986
|
{
|
|
3727
3987
|
const replacementDiscoveries = this.store.listDiscoveries(projectId)
|
|
3728
3988
|
.filter((discovery) => discovery.status === 'ready_to_materialize');
|
|
3729
|
-
const replacementDiscovery = replacementDiscoveries.length === 1 ? replacementDiscoveries[0] : null;
|
|
3730
3989
|
const replacementMilestone = task.milestoneId
|
|
3731
3990
|
? this.store.readMilestone(projectId, task.milestoneId)
|
|
3732
3991
|
: null;
|
|
3992
|
+
const incidentDependencies = replacementMilestone
|
|
3993
|
+
? replacementMilestone.memberships.flatMap((membership) => {
|
|
3994
|
+
if (!Array.isArray(membership.dependsOnTaskIds))
|
|
3995
|
+
return [];
|
|
3996
|
+
return [
|
|
3997
|
+
...(membership.taskId === task.id
|
|
3998
|
+
? membership.dependsOnTaskIds.map((predecessorTaskId) => ({
|
|
3999
|
+
predecessorTaskId,
|
|
4000
|
+
consumerTaskId: membership.taskId,
|
|
4001
|
+
}))
|
|
4002
|
+
: []),
|
|
4003
|
+
...(membership.dependsOnTaskIds.includes(task.id)
|
|
4004
|
+
? [{ predecessorTaskId: task.id, consumerTaskId: membership.taskId }]
|
|
4005
|
+
: []),
|
|
4006
|
+
];
|
|
4007
|
+
})
|
|
4008
|
+
: [];
|
|
3733
4009
|
guardedCorrectiveOverlay = {
|
|
3734
|
-
action:
|
|
3735
|
-
|
|
4010
|
+
action: 'doctor',
|
|
4011
|
+
blockedAction: 'task replacement-materialize',
|
|
3736
4012
|
stepId: guardedCorrectivePosture.stepId,
|
|
3737
4013
|
correctiveDecisionGate: {
|
|
3738
4014
|
attemptCount: guardedCorrectivePosture.attemptCount,
|
|
@@ -3746,29 +4022,17 @@ export class WorkflowService {
|
|
|
3746
4022
|
requiredDiscoveryStatus: 'ready_to_materialize',
|
|
3747
4023
|
oldMembershipDisposition: 'cancelled',
|
|
3748
4024
|
replacementMembershipDisposition: 'required',
|
|
3749
|
-
transaction: '
|
|
4025
|
+
transaction: 'disabled',
|
|
4026
|
+
retainedImplementation: 'journal-recoverable-1-to-1',
|
|
4027
|
+
diagnosticCode: 'STRUCTURAL_REPLACEMENT_REQUIRED',
|
|
4028
|
+
structuralReplacementAvailable: false,
|
|
4029
|
+
incidentDependencies,
|
|
3750
4030
|
discoveryCandidates: replacementDiscoveries.map((discovery) => ({
|
|
3751
4031
|
id: discovery.id,
|
|
3752
4032
|
revision: discovery.revision,
|
|
3753
4033
|
goal: discovery.goal,
|
|
3754
4034
|
})),
|
|
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
|
-
}),
|
|
4035
|
+
requiredAction: 'Materialize P04-B/P05 before replacing this split-required Task.',
|
|
3772
4036
|
},
|
|
3773
4037
|
};
|
|
3774
4038
|
}
|
|
@@ -3854,6 +4118,7 @@ export class WorkflowService {
|
|
|
3854
4118
|
}
|
|
3855
4119
|
: {}),
|
|
3856
4120
|
...guardedCorrectiveOverlay,
|
|
4121
|
+
...mechanicalFeasibilityOverlay,
|
|
3857
4122
|
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
3858
4123
|
};
|
|
3859
4124
|
if (handoffPosture.state === 'pending') {
|
|
@@ -3967,8 +4232,9 @@ export class WorkflowService {
|
|
|
3967
4232
|
}));
|
|
3968
4233
|
}
|
|
3969
4234
|
findDelegatedContextRefreshOwner(projectId, tasks, milestonesById, approved, inspected) {
|
|
4235
|
+
const tasksById = new Map(tasks.map((task) => [task.id, task]));
|
|
3970
4236
|
for (const task of tasks) {
|
|
3971
|
-
if (!taskIsActionableFromRepositoryNext(task, milestonesById)
|
|
4237
|
+
if (!taskIsActionableFromRepositoryNext(task, milestonesById, tasksById)
|
|
3972
4238
|
|| !task.planHash
|
|
3973
4239
|
|| !taskCanRebindKnowledge(task)
|
|
3974
4240
|
|| this.taskHasTerminalCorrectiveDecision(task))
|
|
@@ -4007,7 +4273,7 @@ export class WorkflowService {
|
|
|
4007
4273
|
if (policy.scope.kind === 'milestone')
|
|
4008
4274
|
this.store.readMilestone(projectId, policy.scope.id);
|
|
4009
4275
|
}
|
|
4010
|
-
assertExecutionAuthorizationFresh(task) {
|
|
4276
|
+
assertExecutionAuthorizationFresh(task, repositoryRoot) {
|
|
4011
4277
|
const root = this.store.taskRoot(task.projectId, task.id);
|
|
4012
4278
|
const authorization = [...task.authorizations]
|
|
4013
4279
|
.reverse()
|
|
@@ -4016,14 +4282,35 @@ export class WorkflowService {
|
|
|
4016
4282
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Execution authorization is missing.');
|
|
4017
4283
|
const briefHash = this.store.hashArtifact(root, 'brief.md');
|
|
4018
4284
|
const planHash = this.store.hashArtifact(root, 'plan.md');
|
|
4019
|
-
if (authorization.briefHash !== briefHash
|
|
4285
|
+
if (authorization.briefHash !== briefHash
|
|
4286
|
+
|| authorization.planHash !== planHash
|
|
4287
|
+
|| task.briefHash !== briefHash
|
|
4288
|
+
|| task.planHash !== planHash) {
|
|
4020
4289
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Execution authorization is stale.', {
|
|
4021
4290
|
authorizedBriefHash: authorization.briefHash,
|
|
4022
4291
|
currentBriefHash: briefHash,
|
|
4023
4292
|
authorizedPlanHash: authorization.planHash,
|
|
4024
4293
|
currentPlanHash: planHash,
|
|
4294
|
+
stateBriefHash: task.briefHash,
|
|
4295
|
+
statePlanHash: task.planHash,
|
|
4296
|
+
});
|
|
4297
|
+
}
|
|
4298
|
+
const mechanicalFeasibility = authorization.mechanicalFeasibility;
|
|
4299
|
+
if (!mechanicalFeasibility)
|
|
4300
|
+
return authorization;
|
|
4301
|
+
const evidenceHash = hashMechanicalFeasibilityEvidence(mechanicalFeasibility);
|
|
4302
|
+
if (authorization.evidenceHash !== evidenceHash
|
|
4303
|
+
|| authorization.headCommit !== mechanicalFeasibility.evaluatedHead) {
|
|
4304
|
+
throw new WorkflowError('STATE_CORRUPT', 'Mechanical-feasibility evidence does not match its authorization event.', {
|
|
4305
|
+
taskId: task.id,
|
|
4306
|
+
authorizedEvidenceHash: authorization.evidenceHash,
|
|
4307
|
+
currentEvidenceHash: evidenceHash,
|
|
4308
|
+
authorizedHeadCommit: authorization.headCommit,
|
|
4309
|
+
evaluatedHead: mechanicalFeasibility.evaluatedHead,
|
|
4025
4310
|
});
|
|
4026
4311
|
}
|
|
4312
|
+
assertMechanicalFeasibilityFresh(repositoryRoot, task, mechanicalFeasibility, briefHash, planHash);
|
|
4313
|
+
return authorization;
|
|
4027
4314
|
}
|
|
4028
4315
|
assertMilestoneAuthorizationFresh(milestone) {
|
|
4029
4316
|
const authorization = [...milestone.authorizations]
|
|
@@ -4744,12 +5031,25 @@ function validateMilestonePlan(plan) {
|
|
|
4744
5031
|
if (plan.acceptance.length === 0 || plan.acceptance.some((value) => !value.trim())) {
|
|
4745
5032
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone acceptance must contain substantive entries.');
|
|
4746
5033
|
}
|
|
4747
|
-
if (plan.memberships.length === 0) {
|
|
5034
|
+
if (!Array.isArray(plan.memberships) || plan.memberships.length === 0) {
|
|
4748
5035
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone requires at least one Task membership.');
|
|
4749
5036
|
}
|
|
4750
5037
|
for (const membership of plan.memberships) {
|
|
4751
|
-
if (!membership.
|
|
5038
|
+
if (!membership || typeof membership !== 'object' || Array.isArray(membership)) {
|
|
5039
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership must be an object.');
|
|
5040
|
+
}
|
|
5041
|
+
if (typeof membership.taskId !== 'string' || !membership.taskId.trim()) {
|
|
4752
5042
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership requires Task ID.');
|
|
5043
|
+
}
|
|
5044
|
+
if (typeof membership.reason !== 'string') {
|
|
5045
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone membership reason must be a string.');
|
|
5046
|
+
}
|
|
5047
|
+
if (!Array.isArray(membership.dependsOnTaskIds)) {
|
|
5048
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Every Milestone membership must declare dependsOnTaskIds explicitly.');
|
|
5049
|
+
}
|
|
5050
|
+
if (membership.dependsOnTaskIds.some((taskId) => typeof taskId !== 'string' || !taskId.trim())) {
|
|
5051
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone dependency IDs must be non-empty strings.');
|
|
5052
|
+
}
|
|
4753
5053
|
if (membership.disposition !== 'required' && !membership.reason.trim()) {
|
|
4754
5054
|
throw new WorkflowError('INVALID_ARGUMENT', `Milestone ${membership.disposition} membership requires a reason.`);
|
|
4755
5055
|
}
|
|
@@ -4779,7 +5079,7 @@ function taskCanRebindKnowledge(task) {
|
|
|
4779
5079
|
'awaiting_final_acceptance',
|
|
4780
5080
|
].includes(task.status);
|
|
4781
5081
|
}
|
|
4782
|
-
function taskIsActionableFromRepositoryNext(task, milestonesById) {
|
|
5082
|
+
function taskIsActionableFromRepositoryNext(task, milestonesById, tasksById) {
|
|
4783
5083
|
if (task.status === 'merged' || task.status === 'cancelled')
|
|
4784
5084
|
return false;
|
|
4785
5085
|
if (!task.milestoneId)
|
|
@@ -4787,10 +5087,18 @@ function taskIsActionableFromRepositoryNext(task, milestonesById) {
|
|
|
4787
5087
|
const milestone = milestonesById.get(task.milestoneId);
|
|
4788
5088
|
if (!milestone)
|
|
4789
5089
|
return false;
|
|
4790
|
-
if (
|
|
4791
|
-
return
|
|
4792
|
-
const
|
|
4793
|
-
|
|
5090
|
+
if (milestone.status !== 'active')
|
|
5091
|
+
return false;
|
|
5092
|
+
const assessment = assessMilestoneTaskDependencies(milestone, task.id, (candidateTaskId) => {
|
|
5093
|
+
const candidate = tasksById.get(candidateTaskId);
|
|
5094
|
+
if (!candidate)
|
|
5095
|
+
throw new WorkflowError('NOT_FOUND', `Task ${candidateTaskId} was not found.`);
|
|
5096
|
+
return candidate;
|
|
5097
|
+
});
|
|
5098
|
+
return assessment.runnable;
|
|
5099
|
+
}
|
|
5100
|
+
function structuralReplacementIsDisabled() {
|
|
5101
|
+
return true;
|
|
4794
5102
|
}
|
|
4795
5103
|
function milestoneFinalAcceptanceGate(milestone, headCommit) {
|
|
4796
5104
|
if (!milestone.resultHash || !milestone.evidenceHash) {
|
|
@@ -4920,6 +5228,7 @@ function writerTokenContractForAction(action, claimedC1Posture = false) {
|
|
|
4920
5228
|
'task review-sealed-record',
|
|
4921
5229
|
'task corrective-decision-recover',
|
|
4922
5230
|
'task remediation-mode-recover',
|
|
5231
|
+
'task plan-integrity-recover',
|
|
4923
5232
|
'task corrective-decision',
|
|
4924
5233
|
'task corrective-yield',
|
|
4925
5234
|
'task handoff',
|