codex-workflow-v2 2.0.0-beta.13.6 → 2.0.0-beta.13.8
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 +3 -1
- package/dist/reviewer-runtime-build.json +14 -10
- package/dist/src/alpha6/root-cause-replan-carryover.d.ts +2 -1
- package/dist/src/alpha6/root-cause-replan-carryover.js +32 -8
- package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
- package/dist/src/alpha7/autonomy.d.ts +3 -0
- package/dist/src/alpha7/autonomy.js +89 -42
- package/dist/src/alpha7/autonomy.js.map +1 -1
- package/dist/src/change-explanation.d.ts +64 -0
- package/dist/src/change-explanation.js +150 -0
- package/dist/src/change-explanation.js.map +1 -0
- package/dist/src/cli-actions.d.ts +1 -0
- package/dist/src/cli-actions.js +1 -0
- package/dist/src/cli-actions.js.map +1 -1
- package/dist/src/cli.js +9 -3
- package/dist/src/cli.js.map +1 -1
- package/dist/src/state/store.d.ts +1 -1
- package/dist/src/state/store.js +17 -3
- package/dist/src/state/store.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 +54 -1
- package/dist/src/workflow.js +596 -250
- package/dist/src/workflow.js.map +1 -1
- package/docs/change-model.md +118 -0
- package/docs/delegated-approval.md +26 -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/release.md +79 -0
- package/package.json +2 -1
- package/plugins/codex-workflow-gateway/references/chat-dispatch.md +76 -0
- package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +116 -3
- package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +6 -1
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -3
package/dist/src/workflow.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { explainChange } from './change-explanation.js';
|
|
1
2
|
import { pendingReviewRunnerFingerprint, PENDING_REVIEW_UPDATE_SIDECAR, preparePendingReviewUpdateEvent, PENDING_REVIEW_SOURCE_VERSION, pendingReviewSourceBinding, pendingReviewBindingHash, assertPendingReviewTransport, verifiedPendingReviewTransport } from './pending-review-update.js';
|
|
2
3
|
import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
4
|
import { createHash } from 'node:crypto';
|
|
@@ -15,12 +16,12 @@ import { WorkflowError } from './errors.js';
|
|
|
15
16
|
import { withObservedRouteMetadata } from './observed-routes.js';
|
|
16
17
|
import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
|
|
17
18
|
import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
|
|
18
|
-
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, } from './alpha6/handoff.js';
|
|
19
|
+
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, readTaskHandoffEvents, } from './alpha6/handoff.js';
|
|
19
20
|
import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
|
|
20
21
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
21
22
|
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, buildCandidatePlanRiskAuditEvent, buildPlanRiskAuditEvent, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
22
23
|
import { hashCompletedSteps, hashExecutionAuthorization, preparePreExecutionReplanEvent, preExecutionReplanSidecar, readCurrentPreExecutionReplan, } from './alpha6/preexecution-replan.js';
|
|
23
|
-
import { assessRootCauseReplanDirtyCarryover, } from './alpha6/root-cause-replan-carryover.js';
|
|
24
|
+
import { assessRootCauseReplanDirtyCarryover, matchesTerminalCorrectiveYield, } from './alpha6/root-cause-replan-carryover.js';
|
|
24
25
|
import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, deriveCurrentCauseDecisionForTask, prepareRemediationEvent, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
|
|
25
26
|
import { assessPlanIntegrityRecovery, defaultPlanIntegrityRecoveryActor, } from './alpha6/plan-integrity.js';
|
|
26
27
|
import { mechanicalCauseBindings, normalizeMechanicalFailures, } from './alpha6/remediation-cause.js';
|
|
@@ -411,7 +412,7 @@ export class WorkflowService {
|
|
|
411
412
|
discoveries: this.store.listDiscoveries(identity.projectId),
|
|
412
413
|
tasks,
|
|
413
414
|
milestones: this.listMilestonesObserved(identity.projectId, rawMilestones, adoption.currentPosture),
|
|
414
|
-
delegations: this.
|
|
415
|
+
delegations: this.listDelegations(identity.projectId),
|
|
415
416
|
adoption,
|
|
416
417
|
};
|
|
417
418
|
}
|
|
@@ -428,7 +429,7 @@ export class WorkflowService {
|
|
|
428
429
|
if (confirmationCode.trim() !== gate.confirmationCode) {
|
|
429
430
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Delegation confirmation does not match the exact project and policy.', { projectId: identity.projectId, policyHash: gate.policyHash });
|
|
430
431
|
}
|
|
431
|
-
const duplicate = this.
|
|
432
|
+
const duplicate = this.listDelegations(identity.projectId)
|
|
432
433
|
.find((grant) => grant.policyHash === gate.policyHash);
|
|
433
434
|
if (duplicate) {
|
|
434
435
|
throw new WorkflowError('TRANSITION_BLOCKED', 'This exact delegation policy was already issued.', {
|
|
@@ -458,26 +459,35 @@ export class WorkflowService {
|
|
|
458
459
|
}, null);
|
|
459
460
|
}
|
|
460
461
|
revokeDelegation(repository, grantId, expectedRevision, actor, reason) {
|
|
461
|
-
const { identity } = this.#mutationContext(repository);
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
462
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
463
|
+
const before = this.store.readDelegation(identity.projectId, grantId);
|
|
464
|
+
const lease = before.scope.kind === 'milestone'
|
|
465
|
+
? locks.acquire(before.scope.id, 'workflow-core:delegation-revocation') : null;
|
|
466
|
+
try {
|
|
467
|
+
const grant = this.store.readDelegation(identity.projectId, grantId);
|
|
468
|
+
assertExpectedRevision(grant, expectedRevision);
|
|
469
|
+
if (grant.status !== 'active') {
|
|
470
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grantId} is already ${grant.status}.`);
|
|
471
|
+
}
|
|
472
|
+
if (actor.trim() !== grant.principal) {
|
|
473
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Only the recorded principal may revoke a delegation.', {
|
|
474
|
+
principal: grant.principal,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (!reason.trim())
|
|
478
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Delegation revocation requires a reason.');
|
|
479
|
+
return this.store.writeDelegation({
|
|
480
|
+
...grant,
|
|
481
|
+
status: 'revoked',
|
|
482
|
+
revokedAt: this.now().toISOString(),
|
|
483
|
+
revokedBy: grant.principal,
|
|
484
|
+
revokeReason: reason.trim(),
|
|
485
|
+
}, expectedRevision);
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
if (lease)
|
|
489
|
+
locks.release(lease.entityId, lease.token);
|
|
471
490
|
}
|
|
472
|
-
if (!reason.trim())
|
|
473
|
-
throw new WorkflowError('INVALID_ARGUMENT', 'Delegation revocation requires a reason.');
|
|
474
|
-
return this.store.writeDelegation({
|
|
475
|
-
...grant,
|
|
476
|
-
status: 'revoked',
|
|
477
|
-
revokedAt: this.now().toISOString(),
|
|
478
|
-
revokedBy: grant.principal,
|
|
479
|
-
revokeReason: reason.trim(),
|
|
480
|
-
}, expectedRevision);
|
|
481
491
|
}
|
|
482
492
|
startDiscovery(repository, input) {
|
|
483
493
|
if (!input.goal.trim())
|
|
@@ -1359,34 +1369,53 @@ export class WorkflowService {
|
|
|
1359
1369
|
};
|
|
1360
1370
|
}
|
|
1361
1371
|
applyMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor, confirmationCode) {
|
|
1362
|
-
const { identity } = this.#mutationContext(repository);
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1372
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1373
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1374
|
+
try {
|
|
1375
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
1376
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1377
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
1378
|
+
const prepared = this.prepareMilestoneScopeChangeCandidate(identity.projectId, milestone, plan, actor);
|
|
1379
|
+
if (confirmationCode.trim() !== prepared.confirmationCode) {
|
|
1380
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone scope-change confirmation does not match the current Milestone state and requested scope.', {
|
|
1381
|
+
milestoneId,
|
|
1382
|
+
revision: milestone.revision,
|
|
1383
|
+
actor: prepared.actor,
|
|
1384
|
+
confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
return applyMilestoneScopeChangeTransaction({
|
|
1388
|
+
store: this.store,
|
|
1389
|
+
milestone,
|
|
1390
|
+
expectedRevision,
|
|
1391
|
+
prepared,
|
|
1392
|
+
now: this.now(),
|
|
1393
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
1373
1394
|
});
|
|
1374
1395
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1396
|
+
finally {
|
|
1397
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
listDelegations(projectId) {
|
|
1401
|
+
// Valid append-only issuance events can precede publication of their grant.
|
|
1402
|
+
// Such missing state conveys no authority, even if mkdir already succeeded.
|
|
1403
|
+
const issuedIds = new Set(this.store.listMilestones(projectId).flatMap(milestone => readMilestoneAutonomyEvents(this.store, projectId, milestone.id).map(event => event.delegationGrantId)));
|
|
1404
|
+
return this.store.listDelegations(projectId, issuedIds);
|
|
1383
1405
|
}
|
|
1384
1406
|
prepareMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt) {
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1407
|
+
const snapshot = this.observationSnapshot(repository);
|
|
1408
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
1409
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Autonomy preparation requires a coherent observation boundary.', {
|
|
1410
|
+
observation: snapshot.observation,
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
1414
|
+
const milestone = inspectMilestoneWithIntegrity(this.store, identity.projectId, milestoneId, (taskId) => this.store.readTask(identity.projectId, taskId), () => this.store.listTasks(identity.projectId));
|
|
1387
1415
|
assertExpectedRevision(milestone, expectedRevision);
|
|
1388
1416
|
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
1389
1417
|
return prepareMilestoneAutonomyContract({
|
|
1418
|
+
store: this.store,
|
|
1390
1419
|
projectId: identity.projectId,
|
|
1391
1420
|
milestone,
|
|
1392
1421
|
principal,
|
|
@@ -1396,208 +1425,244 @@ export class WorkflowService {
|
|
|
1396
1425
|
});
|
|
1397
1426
|
}
|
|
1398
1427
|
grantMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt, confirmationCode) {
|
|
1399
|
-
const { identity } = this.#mutationContext(repository);
|
|
1400
|
-
const
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1428
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1429
|
+
const lease = locks.acquire(milestoneId, 'workflow-core:autonomy-issuance');
|
|
1430
|
+
try {
|
|
1431
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1432
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1433
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
1434
|
+
return applyMilestoneAutonomyContract({
|
|
1435
|
+
store: this.store,
|
|
1436
|
+
projectId: identity.projectId,
|
|
1437
|
+
milestone,
|
|
1438
|
+
principal,
|
|
1439
|
+
delegate,
|
|
1440
|
+
expiresAt,
|
|
1441
|
+
confirmationCode,
|
|
1442
|
+
now: this.now(),
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
finally {
|
|
1446
|
+
locks.release(milestoneId, lease.token);
|
|
1447
|
+
}
|
|
1413
1448
|
}
|
|
1414
1449
|
evolveMilestonePlanAutonomously(repository, milestoneId, expectedRevision, plan, actor) {
|
|
1415
|
-
const { identity } = this.#mutationContext(repository);
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
milestone,
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1450
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1451
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1452
|
+
try {
|
|
1453
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
1454
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1455
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
1456
|
+
const autonomy = requireActiveMilestoneAutonomy({
|
|
1457
|
+
store: this.store,
|
|
1458
|
+
projectId: identity.projectId,
|
|
1459
|
+
milestone,
|
|
1460
|
+
actor,
|
|
1461
|
+
now: this.now(),
|
|
1462
|
+
});
|
|
1463
|
+
validateMilestonePlan(plan);
|
|
1464
|
+
assertAutonomousPlanEvolution(milestone, plan, autonomy.contract);
|
|
1465
|
+
const prepared = prepareMilestoneScopeChangeCandidate({
|
|
1466
|
+
milestone,
|
|
1467
|
+
plan,
|
|
1468
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
1469
|
+
actor,
|
|
1470
|
+
autonomyContractEventHash: autonomy.contract.eventHash,
|
|
1471
|
+
});
|
|
1472
|
+
return applyMilestoneScopeChangeTransaction({
|
|
1473
|
+
store: this.store,
|
|
1474
|
+
milestone,
|
|
1475
|
+
expectedRevision,
|
|
1476
|
+
prepared,
|
|
1477
|
+
now: this.now(),
|
|
1478
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
finally {
|
|
1482
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1483
|
+
}
|
|
1443
1484
|
}
|
|
1444
1485
|
authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
1445
|
-
const { identity } = this.#mutationContext(repository);
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1486
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1487
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1488
|
+
try {
|
|
1489
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1490
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1491
|
+
if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
|
|
1492
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
|
|
1493
|
+
}
|
|
1494
|
+
assertMilestoneDependenciesDeclared(milestone);
|
|
1495
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
1496
|
+
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
1497
|
+
const planHash = this.store.hashArtifact(root, 'plan.json');
|
|
1498
|
+
if (planHash !== milestone.planHash) {
|
|
1499
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone Plan changed after it was recorded.');
|
|
1500
|
+
}
|
|
1501
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.execution_authorize', actor, milestone);
|
|
1502
|
+
const event = {
|
|
1503
|
+
kind: 'execution',
|
|
1504
|
+
decision: 'approved',
|
|
1505
|
+
actor: actor.trim() || 'user',
|
|
1506
|
+
recordedAt: this.now().toISOString(),
|
|
1507
|
+
briefHash: null,
|
|
1508
|
+
planHash,
|
|
1509
|
+
resultHash: null,
|
|
1510
|
+
evidenceHash: null,
|
|
1511
|
+
headCommit: runGit(identity.repositoryRoot, ['rev-parse', milestone.baseBranch]),
|
|
1512
|
+
reason,
|
|
1513
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1514
|
+
...(delegation ? { delegation } : {}),
|
|
1515
|
+
};
|
|
1516
|
+
return this.store.writeMilestone({ ...milestone, status: 'active', planHash, authorizations: [...milestone.authorizations, event] }, expectedRevision);
|
|
1450
1517
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
1454
|
-
const planHash = this.store.hashArtifact(root, 'plan.json');
|
|
1455
|
-
if (planHash !== milestone.planHash) {
|
|
1456
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone Plan changed after it was recorded.');
|
|
1518
|
+
finally {
|
|
1519
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1457
1520
|
}
|
|
1458
|
-
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.execution_authorize', actor, milestone);
|
|
1459
|
-
const event = {
|
|
1460
|
-
kind: 'execution',
|
|
1461
|
-
decision: 'approved',
|
|
1462
|
-
actor: actor.trim() || 'user',
|
|
1463
|
-
recordedAt: this.now().toISOString(),
|
|
1464
|
-
briefHash: null,
|
|
1465
|
-
planHash,
|
|
1466
|
-
resultHash: null,
|
|
1467
|
-
evidenceHash: null,
|
|
1468
|
-
headCommit: runGit(identity.repositoryRoot, ['rev-parse', milestone.baseBranch]),
|
|
1469
|
-
reason,
|
|
1470
|
-
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1471
|
-
...(delegation ? { delegation } : {}),
|
|
1472
|
-
};
|
|
1473
|
-
return this.store.writeMilestone({ ...milestone, status: 'active', planHash, authorizations: [...milestone.authorizations, event] }, expectedRevision);
|
|
1474
1521
|
}
|
|
1475
1522
|
validateMilestone(repository, milestoneId, expectedRevision) {
|
|
1476
|
-
const { identity } = this.#mutationContext(repository);
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Required Milestone Tasks are not merged.', {
|
|
1497
|
-
taskIds: incomplete.map((membership) => membership.taskId),
|
|
1523
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1524
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1525
|
+
try {
|
|
1526
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1527
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1528
|
+
if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
|
|
1529
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} cannot be validated while ${milestone.status}.`);
|
|
1530
|
+
}
|
|
1531
|
+
this.assertMilestoneAuthorizationFresh(milestone);
|
|
1532
|
+
if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
|
|
1533
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone validation requires a clean base branch checkout.', {
|
|
1534
|
+
expectedBranch: milestone.baseBranch,
|
|
1535
|
+
actualBranch: currentBranch(identity.repositoryRoot),
|
|
1536
|
+
changedFiles: changedFiles(identity.repositoryRoot),
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
const incomplete = milestone.memberships.filter((membership) => {
|
|
1540
|
+
if (membership.disposition !== 'required')
|
|
1541
|
+
return false;
|
|
1542
|
+
return this.store.readTask(identity.projectId, membership.taskId).status !== 'merged';
|
|
1498
1543
|
});
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1544
|
+
if (incomplete.length > 0) {
|
|
1545
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Required Milestone Tasks are not merged.', {
|
|
1546
|
+
taskIds: incomplete.map((membership) => membership.taskId),
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
const checks = runChecks(identity.repositoryRoot, milestone.checks);
|
|
1550
|
+
const baseHead = headCommit(identity.repositoryRoot);
|
|
1551
|
+
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
1552
|
+
const stagedEvidence = this.store.stageArtifact(root, 'evidence.json', JSON.stringify({
|
|
1553
|
+
milestoneId,
|
|
1554
|
+
baseBranch: milestone.baseBranch,
|
|
1555
|
+
headCommit: baseHead,
|
|
1556
|
+
generatedAt: this.now().toISOString(),
|
|
1557
|
+
membershipRevision: milestone.membershipRevision,
|
|
1558
|
+
memberships: milestone.memberships,
|
|
1559
|
+
checks,
|
|
1560
|
+
}, null, 2));
|
|
1561
|
+
if (checks.some((check) => !check.passed)) {
|
|
1562
|
+
const saved = this.store.writeMilestone({ ...milestone, status: 'blocked', evidenceHash: stagedEvidence.hash }, expectedRevision);
|
|
1563
|
+
this.store.publishArtifact(stagedEvidence);
|
|
1564
|
+
return saved;
|
|
1565
|
+
}
|
|
1566
|
+
const result = [
|
|
1567
|
+
`# Milestone result: ${milestone.title}`,
|
|
1568
|
+
'',
|
|
1569
|
+
`Outcome: ${milestone.outcome}`,
|
|
1570
|
+
'',
|
|
1571
|
+
`Validated base: ${milestone.baseBranch}@${baseHead}`,
|
|
1572
|
+
'',
|
|
1573
|
+
'All required memberships are merged and all Milestone checks passed.',
|
|
1574
|
+
'',
|
|
1575
|
+
].join('\n');
|
|
1576
|
+
const stagedResult = this.store.stageArtifact(root, 'result.md', result);
|
|
1577
|
+
const saved = this.store.writeMilestone({
|
|
1578
|
+
...milestone,
|
|
1579
|
+
status: 'awaiting_final_acceptance',
|
|
1580
|
+
evidenceHash: stagedEvidence.hash,
|
|
1581
|
+
resultHash: stagedResult.hash,
|
|
1582
|
+
}, expectedRevision);
|
|
1514
1583
|
this.store.publishArtifact(stagedEvidence);
|
|
1584
|
+
this.store.publishArtifact(stagedResult);
|
|
1515
1585
|
return saved;
|
|
1516
1586
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
`Outcome: ${milestone.outcome}`,
|
|
1521
|
-
'',
|
|
1522
|
-
`Validated base: ${milestone.baseBranch}@${baseHead}`,
|
|
1523
|
-
'',
|
|
1524
|
-
'All required memberships are merged and all Milestone checks passed.',
|
|
1525
|
-
'',
|
|
1526
|
-
].join('\n');
|
|
1527
|
-
const stagedResult = this.store.stageArtifact(root, 'result.md', result);
|
|
1528
|
-
const saved = this.store.writeMilestone({
|
|
1529
|
-
...milestone,
|
|
1530
|
-
status: 'awaiting_final_acceptance',
|
|
1531
|
-
evidenceHash: stagedEvidence.hash,
|
|
1532
|
-
resultHash: stagedResult.hash,
|
|
1533
|
-
}, expectedRevision);
|
|
1534
|
-
this.store.publishArtifact(stagedEvidence);
|
|
1535
|
-
this.store.publishArtifact(stagedResult);
|
|
1536
|
-
return saved;
|
|
1587
|
+
finally {
|
|
1588
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1589
|
+
}
|
|
1537
1590
|
}
|
|
1538
1591
|
acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode, delegationGrantId = null) {
|
|
1539
|
-
const { identity } = this.#mutationContext(repository);
|
|
1540
|
-
const
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1592
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1593
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1594
|
+
try {
|
|
1595
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1596
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1597
|
+
if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
|
|
1598
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not ready for acceptance.`);
|
|
1599
|
+
}
|
|
1600
|
+
if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
|
|
1601
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone acceptance requires a clean base branch checkout.');
|
|
1602
|
+
}
|
|
1603
|
+
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
1604
|
+
const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
|
|
1605
|
+
const currentHead = headCommit(identity.repositoryRoot);
|
|
1606
|
+
const resultHash = this.store.hashArtifact(root, 'result.md');
|
|
1607
|
+
const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
|
|
1608
|
+
if (evidence.headCommit !== currentHead || resultHash !== milestone.resultHash || evidenceHash !== milestone.evidenceHash) {
|
|
1609
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone validation evidence is stale.');
|
|
1610
|
+
}
|
|
1611
|
+
const normalizedActor = actor.trim();
|
|
1612
|
+
if (!normalizedActor) {
|
|
1613
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone final acceptance requires an explicit actor.');
|
|
1614
|
+
}
|
|
1615
|
+
const gate = milestoneFinalAcceptanceGate(milestone, currentHead);
|
|
1616
|
+
if (confirmationCode.trim() !== gate.confirmationCode) {
|
|
1617
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone final acceptance confirmation does not match the current validated state.', {
|
|
1618
|
+
milestoneId,
|
|
1619
|
+
revision: milestone.revision,
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.final_accept', normalizedActor, milestone);
|
|
1623
|
+
const event = {
|
|
1624
|
+
kind: 'final_acceptance',
|
|
1625
|
+
decision: 'approved',
|
|
1626
|
+
actor: normalizedActor,
|
|
1627
|
+
recordedAt: this.now().toISOString(),
|
|
1628
|
+
briefHash: null,
|
|
1629
|
+
planHash: milestone.planHash,
|
|
1630
|
+
resultHash,
|
|
1631
|
+
evidenceHash,
|
|
1632
|
+
headCommit: currentHead,
|
|
1633
|
+
reason: 'Milestone final acceptance confirmed against the current validated state.',
|
|
1634
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1635
|
+
...(delegation ? { delegation } : {}),
|
|
1636
|
+
};
|
|
1637
|
+
return this.store.writeMilestone({
|
|
1638
|
+
...milestone,
|
|
1639
|
+
status: 'accepted',
|
|
1640
|
+
resultHash,
|
|
1641
|
+
evidenceHash,
|
|
1642
|
+
acceptedHead: currentHead,
|
|
1643
|
+
authorizations: [...milestone.authorizations, event],
|
|
1644
|
+
}, expectedRevision);
|
|
1559
1645
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone final acceptance confirmation does not match the current validated state.', {
|
|
1563
|
-
milestoneId,
|
|
1564
|
-
revision: milestone.revision,
|
|
1565
|
-
});
|
|
1646
|
+
finally {
|
|
1647
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1566
1648
|
}
|
|
1567
|
-
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.final_accept', normalizedActor, milestone);
|
|
1568
|
-
const event = {
|
|
1569
|
-
kind: 'final_acceptance',
|
|
1570
|
-
decision: 'approved',
|
|
1571
|
-
actor: normalizedActor,
|
|
1572
|
-
recordedAt: this.now().toISOString(),
|
|
1573
|
-
briefHash: null,
|
|
1574
|
-
planHash: milestone.planHash,
|
|
1575
|
-
resultHash,
|
|
1576
|
-
evidenceHash,
|
|
1577
|
-
headCommit: currentHead,
|
|
1578
|
-
reason: 'Milestone final acceptance confirmed against the current validated state.',
|
|
1579
|
-
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1580
|
-
...(delegation ? { delegation } : {}),
|
|
1581
|
-
};
|
|
1582
|
-
return this.store.writeMilestone({
|
|
1583
|
-
...milestone,
|
|
1584
|
-
status: 'accepted',
|
|
1585
|
-
resultHash,
|
|
1586
|
-
evidenceHash,
|
|
1587
|
-
acceptedHead: currentHead,
|
|
1588
|
-
authorizations: [...milestone.authorizations, event],
|
|
1589
|
-
}, expectedRevision);
|
|
1590
1649
|
}
|
|
1591
1650
|
cancelMilestone(repository, milestoneId, expectedRevision, reason) {
|
|
1592
1651
|
if (!reason.trim())
|
|
1593
1652
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone cancellation requires a reason.');
|
|
1594
|
-
const { identity } = this.#mutationContext(repository);
|
|
1595
|
-
const
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1653
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1654
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1655
|
+
try {
|
|
1656
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1657
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1658
|
+
if (['accepted', 'cancelled'].includes(milestone.status)) {
|
|
1659
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
|
|
1660
|
+
}
|
|
1661
|
+
return this.store.writeMilestone({ ...milestone, status: 'cancelled', cancellationReason: reason.trim() }, expectedRevision);
|
|
1662
|
+
}
|
|
1663
|
+
finally {
|
|
1664
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1599
1665
|
}
|
|
1600
|
-
return this.store.writeMilestone({ ...milestone, status: 'cancelled', cancellationReason: reason.trim() }, expectedRevision);
|
|
1601
1666
|
}
|
|
1602
1667
|
setTaskPlan(repository, taskId, expectedRevision, plan, correctiveAudit = null, planRiskAudit = null) {
|
|
1603
1668
|
validatePlan(plan);
|
|
@@ -4182,7 +4247,8 @@ export class WorkflowService {
|
|
|
4182
4247
|
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4183
4248
|
if (!task)
|
|
4184
4249
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4185
|
-
const
|
|
4250
|
+
const candidate = inspectBoundedWorkflowDependencyCommit(snapshot.identity.repositoryRoot, headCommit(snapshot.identity.repositoryRoot));
|
|
4251
|
+
const carryover = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task, undefined, candidate?.parentCommitSha);
|
|
4186
4252
|
const preflight = this.inspectDependencyProvenanceCandidate(snapshot, task, null, null, null, null, carryover.eligible ? carryover.compatibility : null);
|
|
4187
4253
|
if (!preflight.rootCauseReplan)
|
|
4188
4254
|
return preflight;
|
|
@@ -4201,15 +4267,16 @@ export class WorkflowService {
|
|
|
4201
4267
|
...(preflight.parentCommitSha !== preflight.rootCauseReplan.sourceHeadCommit
|
|
4202
4268
|
? ['Corrective carryover dependency parent must equal the bound source HEAD.'] : []),
|
|
4203
4269
|
...(preflight.parentCommitSha
|
|
4204
|
-
&& workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha) !== '2.0.0-beta.13.
|
|
4205
|
-
? ['Corrective carryover dependency parent must declare exact source
|
|
4206
|
-
...(preflight.dependencyCommits.at(-1)?.packageVersion !==
|
|
4207
|
-
? [
|
|
4270
|
+
&& workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha) !== '2.0.0-beta.13.6'
|
|
4271
|
+
? ['Corrective carryover dependency parent must declare exact source beta.13.6.'] : []),
|
|
4272
|
+
...(preflight.dependencyCommits.at(-1)?.packageVersion !== PACKAGE_VERSION
|
|
4273
|
+
? [`Corrective carryover dependency commit must declare exact target ${PACKAGE_VERSION}.`] : []),
|
|
4208
4274
|
...(!baseDependency
|
|
4209
4275
|
|| baseDependency.commitSha !== baseTip
|
|
4210
|
-
|| baseDependency.packageVersion !==
|
|
4211
|
-
|| workflowDependencyVersionAt(snapshot.identity.repositoryRoot, baseDependency.parentCommitSha)
|
|
4212
|
-
|
|
4276
|
+
|| baseDependency.packageVersion !== PACKAGE_VERSION
|
|
4277
|
+
|| workflowDependencyVersionAt(snapshot.identity.repositoryRoot, baseDependency.parentCommitSha)
|
|
4278
|
+
!== workflowDependencyVersionAt(snapshot.identity.repositoryRoot, preflight.parentCommitSha ?? '')
|
|
4279
|
+
? ['Corrective carryover Milestone-base tip must be one dependency-only commit with the same exact source and runtime target.'] : []),
|
|
4213
4280
|
];
|
|
4214
4281
|
return bridgeBlockers.length === 0
|
|
4215
4282
|
? preflight
|
|
@@ -4222,14 +4289,14 @@ export class WorkflowService {
|
|
|
4222
4289
|
blockers: [...new Set([...preflight.blockers, ...bridgeBlockers])],
|
|
4223
4290
|
};
|
|
4224
4291
|
}
|
|
4225
|
-
correctiveCarryoverUpdatePreflight(repository, taskId) {
|
|
4292
|
+
correctiveCarryoverUpdatePreflight(repository, taskId, candidate = null) {
|
|
4226
4293
|
const snapshot = this.observationSnapshot(repository);
|
|
4227
4294
|
const task = snapshot.tasks.find((candidate) => candidate.id === taskId);
|
|
4228
4295
|
if (!task)
|
|
4229
4296
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4230
|
-
const sourceVersion = '2.0.0-beta.13.5';
|
|
4231
|
-
const targetVersion = '2.0.0-beta.13.6';
|
|
4232
4297
|
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4298
|
+
const sourceVersion = '2.0.0-beta.13.6';
|
|
4299
|
+
const targetVersion = '2.0.0-beta.13.8';
|
|
4233
4300
|
const sourceSurfaces = {
|
|
4234
4301
|
declared: versions.declared,
|
|
4235
4302
|
locked: versions.locked,
|
|
@@ -4237,8 +4304,11 @@ export class WorkflowService {
|
|
|
4237
4304
|
currentBranch: versions.currentBranch,
|
|
4238
4305
|
baseBranch: versions.milestoneBases.find((base) => base.branch === task.baseBranch)?.version ?? null,
|
|
4239
4306
|
};
|
|
4240
|
-
const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task);
|
|
4241
4307
|
const c1 = readTaskC1Posture(this.store, task);
|
|
4308
|
+
if (c1.state !== 'claimed') {
|
|
4309
|
+
return this.consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate);
|
|
4310
|
+
}
|
|
4311
|
+
const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(snapshot.identity.repositoryRoot, task);
|
|
4242
4312
|
const now = this.now().getTime();
|
|
4243
4313
|
const activeLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) > now);
|
|
4244
4314
|
const staleLeases = snapshot.leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
|
|
@@ -4322,6 +4392,155 @@ export class WorkflowService {
|
|
|
4322
4392
|
blockers,
|
|
4323
4393
|
};
|
|
4324
4394
|
}
|
|
4395
|
+
consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate) {
|
|
4396
|
+
const repositoryRoot = snapshot.identity.repositoryRoot;
|
|
4397
|
+
const sourceVersion = '2.0.0-beta.13.6';
|
|
4398
|
+
const targetVersion = '2.0.0-beta.13.8';
|
|
4399
|
+
const c1 = readTaskC1Posture(this.store, task);
|
|
4400
|
+
const recorded = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan && task.systemCommits.includes(record.commitSha))?.rootCauseReplan;
|
|
4401
|
+
const currentHead = headCommit(repositoryRoot);
|
|
4402
|
+
const dirtyFiles = changedFiles(repositoryRoot).sort();
|
|
4403
|
+
const dirtyWorktreeHash = hashDirtyWorktree(repositoryRoot, dirtyFiles);
|
|
4404
|
+
const navigation = this.next(repositoryRoot, task.id);
|
|
4405
|
+
const action = String(navigation.action);
|
|
4406
|
+
const stepId = typeof navigation.stepId === 'string' ? navigation.stepId : null;
|
|
4407
|
+
const step = task.steps.find((candidate) => candidate.id === stepId) ?? null;
|
|
4408
|
+
const terminal = readRemediationEvents(this.store, task)
|
|
4409
|
+
.filter((event) => event.stepId === stepId && event.failureKind === 'checks-failed').at(-1);
|
|
4410
|
+
const matchingYields = readTaskHandoffEvents(this.store, task).filter((event) => terminal && matchesTerminalCorrectiveYield(event, terminal, task.id));
|
|
4411
|
+
const yielded = matchingYields.length === 1 ? matchingYields[0] : null;
|
|
4412
|
+
const assessment = this.assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId ?? undefined);
|
|
4413
|
+
const missingClaimOnly = assessment.blockers.length === 1
|
|
4414
|
+
&& assessment.blockers[0].startsWith('Corrective carryover requires the retained claimed C1');
|
|
4415
|
+
const rollingWave = navigation.rollingWave;
|
|
4416
|
+
const sourceActionAllowed = (action === 'task plan-set' && step?.status === 'failed'
|
|
4417
|
+
&& rollingWave?.state === 'root-cause-replan-required' && !assessment.applicable && c1.state === 'none')
|
|
4418
|
+
|| (action === 'task handoff-prepare' && step?.status === 'planned' && missingClaimOnly && c1.state === 'none')
|
|
4419
|
+
|| (action === 'task claim' && step?.status === 'planned' && missingClaimOnly
|
|
4420
|
+
&& c1.state === 'pending' && c1.targetActor === yielded?.actor);
|
|
4421
|
+
const blockers = [
|
|
4422
|
+
...(PACKAGE_VERSION !== targetVersion ? [`Runner must be the exact ${targetVersion} compatibility target.`] : []),
|
|
4423
|
+
...Object.entries(sourceSurfaces).filter(([, version]) => version !== sourceVersion)
|
|
4424
|
+
.map(([surface]) => `${surface} Workflow version must equal the exact ${sourceVersion} source.`),
|
|
4425
|
+
...observationPreflightBlockers(snapshot.observation),
|
|
4426
|
+
...(snapshot.leases.length > 0 ? ['No active or stale writer lease may exist before source recovery.'] : []),
|
|
4427
|
+
...(snapshot.observation.pendingProjectTransactions.length
|
|
4428
|
+
+ snapshot.observation.pendingMilestoneTransactions.length
|
|
4429
|
+
+ snapshot.observation.pendingTaskTransactions.length
|
|
4430
|
+
+ snapshot.observation.corruptTaskTransactions.length
|
|
4431
|
+
+ snapshot.observation.coreTaskOperations.length > 0 ? ['No pending or corrupt transaction or Core operation may exist.'] : []),
|
|
4432
|
+
...(!recorded || recorded.stepId === stepId
|
|
4433
|
+
|| task.steps.find((candidate) => candidate.id === recorded.stepId)?.status !== 'completed'
|
|
4434
|
+
? ['A different, verified completed Step must own the historical carryover binding.'] : []),
|
|
4435
|
+
...(!sourceActionAllowed ? ['No exact source Plan replacement or original-Worker handoff action is eligible.'] : []),
|
|
4436
|
+
...(!(task.status === 'ready' || (action === 'task plan-set' && task.status === 'needs_fix'))
|
|
4437
|
+
|| task.workspaceOwner !== 'local'
|
|
4438
|
+
|| currentBranch(repositoryRoot) !== task.taskBranch
|
|
4439
|
+
|| task.steps.some((candidate) => candidate.status === 'in_progress')
|
|
4440
|
+
? ['The Task must retain its local, no-running-Step source recovery boundary.'] : []),
|
|
4441
|
+
...(!step || dirtyFiles.length === 0
|
|
4442
|
+
|| dirtyFiles.some((file) => !pathAllowed(file, step.allowedWrites) || pathAllowed(file, step.forbiddenScope))
|
|
4443
|
+
? ['Retained dirty files must remain inside the exact current Step scope.'] : []),
|
|
4444
|
+
...(!terminal || terminal.failureEvidence.completionCommit !== currentHead
|
|
4445
|
+
|| terminal.failureEvidence.dirtyWorktreeHash !== dirtyWorktreeHash
|
|
4446
|
+
? ['The terminal repeated failure HEAD and dirty-byte binding must remain unchanged.'] : []),
|
|
4447
|
+
...(!yielded || !terminal || yielded.planHash === null
|
|
4448
|
+
|| !Number.isFinite(Date.parse(yielded.recordedAt))
|
|
4449
|
+
|| Date.parse(yielded.recordedAt) < Date.parse(terminal.recordedAt)
|
|
4450
|
+
? ['The terminal failure must retain its exact completed original-Worker corrective yield.'] : []),
|
|
4451
|
+
];
|
|
4452
|
+
let candidateBinding = null;
|
|
4453
|
+
if (action === 'task plan-set') {
|
|
4454
|
+
if (!candidate) {
|
|
4455
|
+
blockers.push('Source Plan replacement requires candidate Plan and independent Plan Risk Audit inputs.');
|
|
4456
|
+
}
|
|
4457
|
+
else {
|
|
4458
|
+
try {
|
|
4459
|
+
validatePlan(candidate.plan);
|
|
4460
|
+
const retained = deriveCompletedStepCarryover(task, candidate.plan.steps);
|
|
4461
|
+
if (task.steps.some((completed) => completed.status === 'completed' && !retained.has(completed.id))) {
|
|
4462
|
+
throw new Error('Candidate must preserve every completed Step definition and its execution authority exactly.');
|
|
4463
|
+
}
|
|
4464
|
+
const correctiveStep = candidate.plan.steps.find((candidateStep) => candidateStep.id === stepId);
|
|
4465
|
+
if (!correctiveStep || dirtyFiles.some((file) => !pathAllowed(file, correctiveStep.allowedWrites) || pathAllowed(file, correctiveStep.forbiddenScope))) {
|
|
4466
|
+
throw new Error('Candidate must retain the exact failed Step and confine every dirty path to its replacement scope.');
|
|
4467
|
+
}
|
|
4468
|
+
if (correctiveStep.dependencies.some((dependency) => !retained.has(dependency))) {
|
|
4469
|
+
throw new Error('Candidate corrective Step dependencies must retain completed execution authority.');
|
|
4470
|
+
}
|
|
4471
|
+
if (candidate.plan.objective !== task.planObjective
|
|
4472
|
+
|| canonicalJsonStringify(candidate.plan.requirements) !== canonicalJsonStringify(task.requirements)
|
|
4473
|
+
|| canonicalJsonStringify(candidate.plan.acceptance) !== canonicalJsonStringify(task.acceptance)) {
|
|
4474
|
+
throw new Error('Candidate cannot change Task objective, requirements or acceptance.');
|
|
4475
|
+
}
|
|
4476
|
+
const map = this.assertKnowledgeMapBinding(repositoryRoot, task.projectId, candidate.plan.knowledgeMapRevision, candidate.plan.knowledgeMapHash);
|
|
4477
|
+
this.validateKnowledgeTargets(map, candidate.plan);
|
|
4478
|
+
const prepared = prepareCorrectiveReplanCandidate(task, candidate.plan);
|
|
4479
|
+
if (!task.planHash || normalizePlanKnowledgeBinding(prepared.planBytes, 'source recovery candidate').semanticHash
|
|
4480
|
+
=== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), task.planHash, task.planHash), 'current failed Plan').semanticHash) {
|
|
4481
|
+
throw new Error('Candidate must change the failed Plan semantics before source recovery.');
|
|
4482
|
+
}
|
|
4483
|
+
if (!['approved', 'approved-with-rationale'].includes(candidate.planRiskAudit.decision)) {
|
|
4484
|
+
throw new Error('Source recovery candidate requires a positive independent Plan Risk Audit.');
|
|
4485
|
+
}
|
|
4486
|
+
const auditTask = {
|
|
4487
|
+
...task, status: 'awaiting_execution_authorization', planHash: prepared.planHash,
|
|
4488
|
+
knowledgeMapRevision: candidate.plan.knowledgeMapRevision,
|
|
4489
|
+
knowledgeMapHash: candidate.plan.knowledgeMapHash,
|
|
4490
|
+
steps: candidate.plan.steps.map((candidateStep) => retained.get(candidateStep.id) ?? {
|
|
4491
|
+
...candidateStep, status: 'planned', evidence: null, failurePause: null,
|
|
4492
|
+
}),
|
|
4493
|
+
};
|
|
4494
|
+
validatePlanRiskAuditCandidate(this.store, auditTask, candidate.planRiskAudit);
|
|
4495
|
+
candidateBinding = {
|
|
4496
|
+
candidatePlanHash: prepared.planHash,
|
|
4497
|
+
candidateInputHash: sha256Hex(canonicalJsonStringify(candidate.plan)),
|
|
4498
|
+
riskAuditInputHash: sha256Hex(canonicalJsonStringify(candidate.planRiskAudit)),
|
|
4499
|
+
};
|
|
4500
|
+
}
|
|
4501
|
+
catch (error) {
|
|
4502
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
try {
|
|
4507
|
+
validateTaskHistory(repositoryRoot, task);
|
|
4508
|
+
this.assertTaskKnowledgeBinding(repositoryRoot, task);
|
|
4509
|
+
const sourceAudit = [...readPlanRiskAuditEvents(this.store, task)].reverse().find((event) => event.taskRevision <= (terminal?.failureEvidence.executionTaskRevision ?? -1));
|
|
4510
|
+
if (!sourceAudit || !yielded?.planHash || !task.planHash
|
|
4511
|
+
|| normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), sourceAudit.planHash, task.planHash), 'terminal source').semanticHash
|
|
4512
|
+
!== normalizePlanKnowledgeBinding(readVerifiedPlanArtifact(this.store.taskRoot(task.projectId, task.id), yielded.planHash, task.planHash), 'terminal corrective yield').semanticHash) {
|
|
4513
|
+
throw new Error('Terminal failure and exact corrective yield must share a verified source Plan.');
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
catch (error) {
|
|
4517
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
4518
|
+
}
|
|
4519
|
+
const binding = {
|
|
4520
|
+
taskId: task.id, taskRevision: task.revision, stepId, briefHash: task.briefHash,
|
|
4521
|
+
planHash: task.planHash, planRiskAuditEventHash: readCurrentPlanRiskAudit(this.store, task)?.audit.eventHash ?? null,
|
|
4522
|
+
sourceHeadCommit: currentHead, dirtyFiles, dirtyWorktreeHash,
|
|
4523
|
+
completedStepsHash: hashCompletedSteps(task), historicalCarryoverHash: recorded?.compatibilityHash ?? null,
|
|
4524
|
+
terminalRemediationEventHash: terminal?.eventHash ?? null,
|
|
4525
|
+
correctiveYieldEventHash: yielded?.eventHash ?? null, originalWorker: yielded?.actor ?? null,
|
|
4526
|
+
c1Posture: c1.state,
|
|
4527
|
+
candidateBinding,
|
|
4528
|
+
};
|
|
4529
|
+
return {
|
|
4530
|
+
readOnly: true, eligible: false, sourceVersion, targetVersion, ...binding,
|
|
4531
|
+
action: null, transportAllowed: false,
|
|
4532
|
+
...(action === 'task plan-set' ? { taskPlanContract: navigation.taskPlanContract } : {}),
|
|
4533
|
+
compatibilityHash: sha256Hex(canonicalJsonStringify(binding)),
|
|
4534
|
+
sourceRecovery: blockers.length === 0 ? {
|
|
4535
|
+
eligible: true, action,
|
|
4536
|
+
...(action === 'task plan-set' ? { ...candidateBinding } : {}),
|
|
4537
|
+
...(action === 'task handoff-prepare' ? { targetActor: yielded.actor } : {}),
|
|
4538
|
+
...(action === 'task claim' ? { actor: yielded.actor } : {}),
|
|
4539
|
+
instruction: 'Execute only this exact action with the installed source package; run source status then next and repeat this read-only preflight. Ordinary Plan Risk Audit and execution authorization retain their normal gates.',
|
|
4540
|
+
} : null,
|
|
4541
|
+
blockers,
|
|
4542
|
+
};
|
|
4543
|
+
}
|
|
4325
4544
|
historicalStepProvenanceRecoveryPreflight(repository, taskId) {
|
|
4326
4545
|
const snapshot = this.observationSnapshot(repository);
|
|
4327
4546
|
if (snapshot.observation.kind !== 'assessment') {
|
|
@@ -5183,6 +5402,18 @@ export class WorkflowService {
|
|
|
5183
5402
|
const existing = this.readGraphBindingOrNull(identity.projectId, graphKind);
|
|
5184
5403
|
return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
|
|
5185
5404
|
}
|
|
5405
|
+
explainChange(repository, taskId, input) {
|
|
5406
|
+
const status = this.statusFromSnapshot(this.observationSnapshot(repository));
|
|
5407
|
+
const task = status.tasks.find(candidate => candidate.id === taskId);
|
|
5408
|
+
if (!task && status.observation.kind === 'assessment') {
|
|
5409
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'The requested Task is not present in the current observation.');
|
|
5410
|
+
}
|
|
5411
|
+
const navigation = this.nextFromStatus(repository, taskId, status);
|
|
5412
|
+
const step = task?.steps.find(candidate => candidate.id === navigation.stepId)
|
|
5413
|
+
?? task?.steps.find(candidate => candidate.status === 'in_progress' || candidate.status === 'failed');
|
|
5414
|
+
const effectiveTask = task && step ? taskWithEffectiveStepAllowedWrites(this.store, task, step.id) : task;
|
|
5415
|
+
return explainChange(input, repository, effectiveTask, navigation);
|
|
5416
|
+
}
|
|
5186
5417
|
next(repository, taskId = null) {
|
|
5187
5418
|
return withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository))));
|
|
5188
5419
|
}
|
|
@@ -5294,6 +5525,37 @@ export class WorkflowService {
|
|
|
5294
5525
|
}
|
|
5295
5526
|
const withAdoption = (value) => ({
|
|
5296
5527
|
...value,
|
|
5528
|
+
...(status.milestones.some((milestone) => milestone.status === 'active' && milestone.planHash)
|
|
5529
|
+
? { activeMilestoneAutonomyOptions: status.milestones
|
|
5530
|
+
.filter((milestone) => milestone.status === 'active' && milestone.planHash)
|
|
5531
|
+
.filter((milestone) => {
|
|
5532
|
+
const event = readMilestoneAutonomyEvents(this.store, status.projectId, milestone.id).at(-1);
|
|
5533
|
+
if (!event)
|
|
5534
|
+
return true;
|
|
5535
|
+
try {
|
|
5536
|
+
requireActiveMilestoneAutonomy({
|
|
5537
|
+
store: this.store, projectId: status.projectId, milestone,
|
|
5538
|
+
actor: event.delegate, now: this.now(),
|
|
5539
|
+
});
|
|
5540
|
+
return false;
|
|
5541
|
+
}
|
|
5542
|
+
catch (error) {
|
|
5543
|
+
if (error instanceof WorkflowError && error.code === 'TRANSITION_BLOCKED')
|
|
5544
|
+
return true;
|
|
5545
|
+
if (error instanceof WorkflowError && error.code === 'NOT_FOUND')
|
|
5546
|
+
return true;
|
|
5547
|
+
throw error;
|
|
5548
|
+
}
|
|
5549
|
+
})
|
|
5550
|
+
.map((milestone) => ({
|
|
5551
|
+
action: 'milestone autonomy-prepare',
|
|
5552
|
+
milestoneId: milestone.id,
|
|
5553
|
+
expectedRevision: milestone.revision,
|
|
5554
|
+
requiresExactHumanConfirmation: true,
|
|
5555
|
+
maximumDurationHours: 72,
|
|
5556
|
+
preparationRequired: true,
|
|
5557
|
+
meaning: 'Optional human-requested preparation; existing valid contracts cannot be replaced. Knowledge must be active.',
|
|
5558
|
+
})) } : {}),
|
|
5297
5559
|
adoption: adoptionInfo,
|
|
5298
5560
|
observation: status.observation,
|
|
5299
5561
|
});
|
|
@@ -6965,12 +7227,50 @@ export class WorkflowService {
|
|
|
6965
7227
|
}
|
|
6966
7228
|
: {}),
|
|
6967
7229
|
};
|
|
7230
|
+
// A later independent audit may discover an obstruction absent from the sealed
|
|
7231
|
+
// fix review. Advertise the existing append-only decision route without turning
|
|
7232
|
+
// every ordinary repair into a mandatory replan or rewriting that review.
|
|
7233
|
+
const correctiveDecisionOrdinal = failedStepRemediations.length + 1;
|
|
7234
|
+
const optionalCorrectiveDecision = result.action === 'task run'
|
|
7235
|
+
&& ['needs_fix', 'ready'].includes(task.status)
|
|
7236
|
+
&& failedStep && task.planHash && planRiskAudit
|
|
7237
|
+
&& planRiskAudit.reviewRequiredStepIds.includes(failedStep.id)
|
|
7238
|
+
&& !task.steps.some(step => step.status === 'in_progress')
|
|
7239
|
+
&& failedStepRemediations.length >= 2
|
|
7240
|
+
&& handoffPosture.state !== 'pending'
|
|
7241
|
+
&& !readCorrectiveDecisionEvents(this.store, task).some(event => event.stepId === failedStep.id && event.triggeringAttemptCount === correctiveDecisionOrdinal)
|
|
7242
|
+
&& currentBranch(repositoryRoot) === task.taskBranch && isClean(repositoryRoot)
|
|
7243
|
+
? {
|
|
7244
|
+
command: 'task corrective-decision',
|
|
7245
|
+
state: 'eligible',
|
|
7246
|
+
optional: true,
|
|
7247
|
+
taskId: task.id,
|
|
7248
|
+
stepId: failedStep.id,
|
|
7249
|
+
expectedRevision: task.revision,
|
|
7250
|
+
expectedBriefHash: task.briefHash,
|
|
7251
|
+
expectedPlanHash: task.planHash,
|
|
7252
|
+
expectedHeadCommit: headCommit(repositoryRoot),
|
|
7253
|
+
triggeringAttemptCount: correctiveDecisionOrdinal,
|
|
7254
|
+
coveredRemediationEvents: failedStepRemediations.map(event => ({ eventId: event.eventId, eventHash: event.eventHash })),
|
|
7255
|
+
recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
|
|
7256
|
+
auditorMustDifferFromLatestStrictReviewer: true,
|
|
7257
|
+
requiredActor: handoffPosture.state === 'claimed' ? handoffPosture.claimant : null,
|
|
7258
|
+
decisions: ['continue-fix', 'replan-required', 'split-required', 'stop-escalate'],
|
|
7259
|
+
authority: 'none',
|
|
7260
|
+
preservesOriginalReview: true,
|
|
7261
|
+
humanApprovalRequiredForDecision: false,
|
|
7262
|
+
replanRequiresExistingHumanGate: true,
|
|
7263
|
+
requiredAction: 'Only for newly assessed evidence: obtain an independent corrective audit, refresh next and existing writer authority, then record the decision. This option does not authorize Plan replacement or extra writes.',
|
|
7264
|
+
}
|
|
7265
|
+
: null;
|
|
7266
|
+
const resultWithCorrectiveOption = optionalCorrectiveDecision
|
|
7267
|
+
? { ...result, correctiveDecisionOption: optionalCorrectiveDecision } : result;
|
|
6968
7268
|
const rootCauseAllowsPendingClaim = !rootCauseReplanDirtyCarryover?.applicable
|
|
6969
7269
|
|| (rootCauseReplanDirtyCarryover.blockers.length === 1
|
|
6970
7270
|
&& rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1'));
|
|
6971
7271
|
if (handoffPosture.state === 'pending' && rootCauseAllowsPendingClaim) {
|
|
6972
7272
|
return this.withTaskNavigationContracts(task, {
|
|
6973
|
-
...
|
|
7273
|
+
...resultWithCorrectiveOption,
|
|
6974
7274
|
action: 'task claim',
|
|
6975
7275
|
blockedAction: next.action,
|
|
6976
7276
|
});
|
|
@@ -6978,10 +7278,10 @@ export class WorkflowService {
|
|
|
6978
7278
|
const navigableResult = ['task plan-set']
|
|
6979
7279
|
.includes(String(result.action))
|
|
6980
7280
|
? {
|
|
6981
|
-
...
|
|
7281
|
+
...resultWithCorrectiveOption,
|
|
6982
7282
|
taskPlanContract: this.taskPlanContract(projectId, task),
|
|
6983
7283
|
}
|
|
6984
|
-
:
|
|
7284
|
+
: resultWithCorrectiveOption;
|
|
6985
7285
|
return this.withTaskNavigationContracts(task, navigableResult);
|
|
6986
7286
|
}
|
|
6987
7287
|
taskPlanContract(projectId, task) {
|
|
@@ -7091,7 +7391,7 @@ export class WorkflowService {
|
|
|
7091
7391
|
}
|
|
7092
7392
|
}
|
|
7093
7393
|
delegatedApprovalOptions(projectId, transition, target) {
|
|
7094
|
-
return this.
|
|
7394
|
+
return this.listDelegations(projectId)
|
|
7095
7395
|
.filter((grant) => {
|
|
7096
7396
|
try {
|
|
7097
7397
|
return Boolean(this.delegatedAuthorization(projectId, grant.id, transition, grant.delegate, target));
|
|
@@ -7113,7 +7413,7 @@ export class WorkflowService {
|
|
|
7113
7413
|
}));
|
|
7114
7414
|
}
|
|
7115
7415
|
delegatedContextRefreshOptions(projectId, task, knowledgeMap, mode) {
|
|
7116
|
-
return this.
|
|
7416
|
+
return this.listDelegations(projectId)
|
|
7117
7417
|
.filter((grant) => {
|
|
7118
7418
|
try {
|
|
7119
7419
|
if (mode === 'delegated-plan-bounded-supporting-source') {
|
|
@@ -7512,21 +7812,67 @@ export class WorkflowService {
|
|
|
7512
7812
|
},
|
|
7513
7813
|
}, activeDownstreamProof, downstreamProofReplan, historicalStepProvenance, planIntegrity, rootCauseReplan);
|
|
7514
7814
|
}
|
|
7515
|
-
assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId) {
|
|
7516
|
-
const
|
|
7517
|
-
&& task.systemCommits.includes(record.commitSha))
|
|
7815
|
+
assessCurrentRootCauseReplanDirtyCarryover(repositoryRoot, task, stepId, historyHead) {
|
|
7816
|
+
const recordedRecovery = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan
|
|
7817
|
+
&& task.systemCommits.includes(record.commitSha)) ?? null;
|
|
7818
|
+
const recorded = recordedRecovery?.rootCauseReplan ?? null;
|
|
7518
7819
|
if (recorded) {
|
|
7519
|
-
|
|
7820
|
+
const recordedStep = task.steps.find((candidate) => candidate.id === recorded.stepId) ?? null;
|
|
7821
|
+
if (!recordedStep) {
|
|
7520
7822
|
return {
|
|
7521
7823
|
applicable: true,
|
|
7522
7824
|
eligible: false,
|
|
7523
7825
|
compatibility: null,
|
|
7524
|
-
blockers: [`Recorded corrective carryover
|
|
7826
|
+
blockers: [`Recorded corrective carryover Step ${recorded.stepId} is missing from the current Plan.`],
|
|
7525
7827
|
};
|
|
7526
7828
|
}
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7829
|
+
if (recordedStep.status === 'completed') {
|
|
7830
|
+
const blockers = [];
|
|
7831
|
+
if (!recordedStep.evidence) {
|
|
7832
|
+
blockers.push(`Completed corrective carryover Step ${recorded.stepId} has no execution evidence.`);
|
|
7833
|
+
}
|
|
7834
|
+
try {
|
|
7835
|
+
validateTaskHistory(repositoryRoot, task, historyHead);
|
|
7836
|
+
const sourceAudit = readPlanRiskAuditEvents(this.store, task).find((event) => event.eventId === recorded.replacementPlanRiskAuditEventId
|
|
7837
|
+
&& event.eventHash === recorded.replacementPlanRiskAuditEventHash) ?? null;
|
|
7838
|
+
const classification = sourceAudit?.stepClassifications.find((candidate) => candidate.stepId === recorded.stepId) ?? null;
|
|
7839
|
+
if (!sourceAudit || !classification) {
|
|
7840
|
+
blockers.push('Completed corrective carryover Step lost its exact replacement Plan Risk Audit binding.');
|
|
7841
|
+
}
|
|
7842
|
+
else if (classification.reviewRequired
|
|
7843
|
+
&& recordedStep.evidence
|
|
7844
|
+
&& !verifiedHistoricalStepReviewBindings(this.store, task).some((binding) => binding.stepId === recorded.stepId
|
|
7845
|
+
&& binding.commitSha === recordedStep.evidence.commitSha)) {
|
|
7846
|
+
blockers.push(`Completed corrective carryover Step ${recorded.stepId} lacks verified strict-review proof.`);
|
|
7847
|
+
}
|
|
7848
|
+
}
|
|
7849
|
+
catch (error) {
|
|
7850
|
+
blockers.push(error instanceof Error ? error.message : String(error));
|
|
7851
|
+
}
|
|
7852
|
+
if (blockers.length > 0) {
|
|
7853
|
+
return {
|
|
7854
|
+
applicable: true,
|
|
7855
|
+
eligible: false,
|
|
7856
|
+
compatibility: null,
|
|
7857
|
+
blockers: [...new Set(blockers)],
|
|
7858
|
+
};
|
|
7859
|
+
}
|
|
7860
|
+
// A reviewed, history-valid completed Step consumed this carryover. Keep its
|
|
7861
|
+
// immutable recovery record, but let later Steps use their own lifecycle route.
|
|
7862
|
+
}
|
|
7863
|
+
else {
|
|
7864
|
+
if (stepId && stepId !== recorded.stepId) {
|
|
7865
|
+
return {
|
|
7866
|
+
applicable: true,
|
|
7867
|
+
eligible: false,
|
|
7868
|
+
compatibility: null,
|
|
7869
|
+
blockers: [`Recorded corrective carryover is bound to ${recorded.stepId}, not requested ${stepId}.`],
|
|
7870
|
+
};
|
|
7871
|
+
}
|
|
7872
|
+
return assessRootCauseReplanDirtyCarryover(this.store, repositoryRoot, task, recorded.stepId, {
|
|
7873
|
+
binding: recorded,
|
|
7874
|
+
});
|
|
7875
|
+
}
|
|
7530
7876
|
}
|
|
7531
7877
|
const candidates = task.steps
|
|
7532
7878
|
.filter((step) => step.status === 'planned' && (!stepId || step.id === stepId))
|