codex-workflow-v2 2.0.0-beta.13.7 → 2.0.0-beta.13.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/reviewer-runtime-build.json +21 -13
- package/dist/src/alpha6/failed-step-planning-recovery.d.ts +10 -0
- package/dist/src/alpha6/failed-step-planning-recovery.js +93 -0
- package/dist/src/alpha6/failed-step-planning-recovery.js.map +1 -0
- package/dist/src/alpha6/plan-risk.d.ts +2 -0
- package/dist/src/alpha6/plan-risk.js +10 -3
- package/dist/src/alpha6/plan-risk.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 +4 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/lifecycle/corrective-replan.js +3 -3
- package/dist/src/lifecycle/corrective-replan.js.map +1 -1
- package/dist/src/state/corrective-replan-public.js +5 -4
- package/dist/src/state/corrective-replan-public.js.map +1 -1
- package/dist/src/state/corrective-yield-executor.js +2 -2
- package/dist/src/state/corrective-yield-executor.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 +49 -0
- package/dist/src/workflow.js +406 -238
- 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/pdf/sources/codex-workflow-v2-architecture-ru.md +1 -1
- package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +3 -5
- package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +2 -2
- package/docs/release.md +72 -0
- package/package.json +1 -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 +52 -6
package/dist/src/workflow.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readCorrectivePlanningAudit, readFailedStepPlanningRecovery } from './alpha6/failed-step-planning-recovery.js';
|
|
2
|
+
import { explainChange } from './change-explanation.js';
|
|
1
3
|
import { pendingReviewRunnerFingerprint, PENDING_REVIEW_UPDATE_SIDECAR, preparePendingReviewUpdateEvent, PENDING_REVIEW_SOURCE_VERSION, pendingReviewSourceBinding, pendingReviewBindingHash, assertPendingReviewTransport, verifiedPendingReviewTransport } from './pending-review-update.js';
|
|
2
4
|
import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
5
|
import { createHash } from 'node:crypto';
|
|
@@ -18,7 +20,7 @@ import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.j
|
|
|
18
20
|
import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, appendTaskWriterCredentialReplacement, assertTaskClaimAllowed, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, prepareTaskCorrectiveYield, readTaskC1Posture, readTaskHandoffEvents, } from './alpha6/handoff.js';
|
|
19
21
|
import { assessDownstreamProofCarryover, assessDownstreamProofReplanRecovery, assessDownstreamProofRecovery, downstreamProofInvalidationSidecar, downstreamProofReplanRecoverySidecar, prepareDownstreamProofInvalidation, prepareDownstreamProofReplanRecovery, readDownstreamProofInvalidations, readDownstreamProofReplanRecoveries, hashDirtyWorktree, } from './alpha6/downstream-proof.js';
|
|
20
22
|
import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
|
|
21
|
-
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, buildCandidatePlanRiskAuditEvent, buildPlanRiskAuditEvent, readCurrentPlanRiskAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
23
|
+
import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, buildCandidatePlanRiskAuditEvent, buildPlanRiskAuditEvent, readCurrentPlanRiskAudit, readCurrentPlanningObstructionAudit, readPlanRiskAuditEvents, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
|
|
22
24
|
import { hashCompletedSteps, hashExecutionAuthorization, preparePreExecutionReplanEvent, preExecutionReplanSidecar, readCurrentPreExecutionReplan, } from './alpha6/preexecution-replan.js';
|
|
23
25
|
import { assessRootCauseReplanDirtyCarryover, matchesTerminalCorrectiveYield, } from './alpha6/root-cause-replan-carryover.js';
|
|
24
26
|
import { appendCorrectiveDecisionEvent, appendPlanIntegrityRecoveryDecision, appendRemediationModeRecovery, appendStopEscalateOverride, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, effectiveStepAllowedWrites, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, deriveRollingCauseDecision, deriveCurrentCauseDecisionForTask, prepareRemediationEvent, prepareStopEscalateOverride, stopEscalateOverrideDecisionEventIds, } from './alpha6/remediation.js';
|
|
@@ -47,6 +49,8 @@ import { createEntityId } from './ulid.js';
|
|
|
47
49
|
import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
|
|
48
50
|
import { correctiveReplanBlockedNavigation, correctiveReplanNavigation, evaluateTaskCorrectiveReplanAvailability, evaluateTaskCorrectiveReplanCandidate, identifyCorrectiveReplanPosture, prepareCorrectiveReplanCandidate, TASK_CORRECTIVE_REPLAN_TRANSITION_ID, } from './lifecycle/corrective-replan.js';
|
|
49
51
|
const MAX_STRICT_REVIEW_INFRASTRUCTURE_FAILURES = 2;
|
|
52
|
+
// Release-reviewed exact compatibility target; never infer support from PACKAGE_VERSION.
|
|
53
|
+
const CONSUMED_CARRYOVER_TARGET_VERSION = '2.0.0-beta.13.9';
|
|
50
54
|
export class WorkflowService {
|
|
51
55
|
store;
|
|
52
56
|
now;
|
|
@@ -411,7 +415,7 @@ export class WorkflowService {
|
|
|
411
415
|
discoveries: this.store.listDiscoveries(identity.projectId),
|
|
412
416
|
tasks,
|
|
413
417
|
milestones: this.listMilestonesObserved(identity.projectId, rawMilestones, adoption.currentPosture),
|
|
414
|
-
delegations: this.
|
|
418
|
+
delegations: this.listDelegations(identity.projectId),
|
|
415
419
|
adoption,
|
|
416
420
|
};
|
|
417
421
|
}
|
|
@@ -428,7 +432,7 @@ export class WorkflowService {
|
|
|
428
432
|
if (confirmationCode.trim() !== gate.confirmationCode) {
|
|
429
433
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Delegation confirmation does not match the exact project and policy.', { projectId: identity.projectId, policyHash: gate.policyHash });
|
|
430
434
|
}
|
|
431
|
-
const duplicate = this.
|
|
435
|
+
const duplicate = this.listDelegations(identity.projectId)
|
|
432
436
|
.find((grant) => grant.policyHash === gate.policyHash);
|
|
433
437
|
if (duplicate) {
|
|
434
438
|
throw new WorkflowError('TRANSITION_BLOCKED', 'This exact delegation policy was already issued.', {
|
|
@@ -458,26 +462,35 @@ export class WorkflowService {
|
|
|
458
462
|
}, null);
|
|
459
463
|
}
|
|
460
464
|
revokeDelegation(repository, grantId, expectedRevision, actor, reason) {
|
|
461
|
-
const { identity } = this.#mutationContext(repository);
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
465
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
466
|
+
const before = this.store.readDelegation(identity.projectId, grantId);
|
|
467
|
+
const lease = before.scope.kind === 'milestone'
|
|
468
|
+
? locks.acquire(before.scope.id, 'workflow-core:delegation-revocation') : null;
|
|
469
|
+
try {
|
|
470
|
+
const grant = this.store.readDelegation(identity.projectId, grantId);
|
|
471
|
+
assertExpectedRevision(grant, expectedRevision);
|
|
472
|
+
if (grant.status !== 'active') {
|
|
473
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grantId} is already ${grant.status}.`);
|
|
474
|
+
}
|
|
475
|
+
if (actor.trim() !== grant.principal) {
|
|
476
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Only the recorded principal may revoke a delegation.', {
|
|
477
|
+
principal: grant.principal,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
if (!reason.trim())
|
|
481
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Delegation revocation requires a reason.');
|
|
482
|
+
return this.store.writeDelegation({
|
|
483
|
+
...grant,
|
|
484
|
+
status: 'revoked',
|
|
485
|
+
revokedAt: this.now().toISOString(),
|
|
486
|
+
revokedBy: grant.principal,
|
|
487
|
+
revokeReason: reason.trim(),
|
|
488
|
+
}, expectedRevision);
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
if (lease)
|
|
492
|
+
locks.release(lease.entityId, lease.token);
|
|
471
493
|
}
|
|
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
494
|
}
|
|
482
495
|
startDiscovery(repository, input) {
|
|
483
496
|
if (!input.goal.trim())
|
|
@@ -1359,34 +1372,53 @@ export class WorkflowService {
|
|
|
1359
1372
|
};
|
|
1360
1373
|
}
|
|
1361
1374
|
applyMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor, confirmationCode) {
|
|
1362
|
-
const { identity } = this.#mutationContext(repository);
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1375
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1376
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1377
|
+
try {
|
|
1378
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
1379
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1380
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
1381
|
+
const prepared = this.prepareMilestoneScopeChangeCandidate(identity.projectId, milestone, plan, actor);
|
|
1382
|
+
if (confirmationCode.trim() !== prepared.confirmationCode) {
|
|
1383
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone scope-change confirmation does not match the current Milestone state and requested scope.', {
|
|
1384
|
+
milestoneId,
|
|
1385
|
+
revision: milestone.revision,
|
|
1386
|
+
actor: prepared.actor,
|
|
1387
|
+
confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
return applyMilestoneScopeChangeTransaction({
|
|
1391
|
+
store: this.store,
|
|
1392
|
+
milestone,
|
|
1393
|
+
expectedRevision,
|
|
1394
|
+
prepared,
|
|
1395
|
+
now: this.now(),
|
|
1396
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
1373
1397
|
});
|
|
1374
1398
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1399
|
+
finally {
|
|
1400
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
listDelegations(projectId) {
|
|
1404
|
+
// Valid append-only issuance events can precede publication of their grant.
|
|
1405
|
+
// Such missing state conveys no authority, even if mkdir already succeeded.
|
|
1406
|
+
const issuedIds = new Set(this.store.listMilestones(projectId).flatMap(milestone => readMilestoneAutonomyEvents(this.store, projectId, milestone.id).map(event => event.delegationGrantId)));
|
|
1407
|
+
return this.store.listDelegations(projectId, issuedIds);
|
|
1383
1408
|
}
|
|
1384
1409
|
prepareMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt) {
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1410
|
+
const snapshot = this.observationSnapshot(repository);
|
|
1411
|
+
if (snapshot.observation.kind !== 'assessment') {
|
|
1412
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Autonomy preparation requires a coherent observation boundary.', {
|
|
1413
|
+
observation: snapshot.observation,
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
const identity = resolveRepositoryIdentity(repository);
|
|
1417
|
+
const milestone = inspectMilestoneWithIntegrity(this.store, identity.projectId, milestoneId, (taskId) => this.store.readTask(identity.projectId, taskId), () => this.store.listTasks(identity.projectId));
|
|
1387
1418
|
assertExpectedRevision(milestone, expectedRevision);
|
|
1388
1419
|
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
1389
1420
|
return prepareMilestoneAutonomyContract({
|
|
1421
|
+
store: this.store,
|
|
1390
1422
|
projectId: identity.projectId,
|
|
1391
1423
|
milestone,
|
|
1392
1424
|
principal,
|
|
@@ -1396,208 +1428,244 @@ export class WorkflowService {
|
|
|
1396
1428
|
});
|
|
1397
1429
|
}
|
|
1398
1430
|
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
|
-
|
|
1431
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1432
|
+
const lease = locks.acquire(milestoneId, 'workflow-core:autonomy-issuance');
|
|
1433
|
+
try {
|
|
1434
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1435
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1436
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
1437
|
+
return applyMilestoneAutonomyContract({
|
|
1438
|
+
store: this.store,
|
|
1439
|
+
projectId: identity.projectId,
|
|
1440
|
+
milestone,
|
|
1441
|
+
principal,
|
|
1442
|
+
delegate,
|
|
1443
|
+
expiresAt,
|
|
1444
|
+
confirmationCode,
|
|
1445
|
+
now: this.now(),
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
finally {
|
|
1449
|
+
locks.release(milestoneId, lease.token);
|
|
1450
|
+
}
|
|
1413
1451
|
}
|
|
1414
1452
|
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
|
-
|
|
1453
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1454
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1455
|
+
try {
|
|
1456
|
+
const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
|
|
1457
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1458
|
+
assertMilestoneScopeChangeStatusAllowed(milestone);
|
|
1459
|
+
const autonomy = requireActiveMilestoneAutonomy({
|
|
1460
|
+
store: this.store,
|
|
1461
|
+
projectId: identity.projectId,
|
|
1462
|
+
milestone,
|
|
1463
|
+
actor,
|
|
1464
|
+
now: this.now(),
|
|
1465
|
+
});
|
|
1466
|
+
validateMilestonePlan(plan);
|
|
1467
|
+
assertAutonomousPlanEvolution(milestone, plan, autonomy.contract);
|
|
1468
|
+
const prepared = prepareMilestoneScopeChangeCandidate({
|
|
1469
|
+
milestone,
|
|
1470
|
+
plan,
|
|
1471
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
1472
|
+
actor,
|
|
1473
|
+
autonomyContractEventHash: autonomy.contract.eventHash,
|
|
1474
|
+
});
|
|
1475
|
+
return applyMilestoneScopeChangeTransaction({
|
|
1476
|
+
store: this.store,
|
|
1477
|
+
milestone,
|
|
1478
|
+
expectedRevision,
|
|
1479
|
+
prepared,
|
|
1480
|
+
now: this.now(),
|
|
1481
|
+
readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
finally {
|
|
1485
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1486
|
+
}
|
|
1443
1487
|
}
|
|
1444
1488
|
authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
1445
|
-
const { identity } = this.#mutationContext(repository);
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1489
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1490
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1491
|
+
try {
|
|
1492
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1493
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1494
|
+
if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
|
|
1495
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
|
|
1496
|
+
}
|
|
1497
|
+
assertMilestoneDependenciesDeclared(milestone);
|
|
1498
|
+
this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
|
|
1499
|
+
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
1500
|
+
const planHash = this.store.hashArtifact(root, 'plan.json');
|
|
1501
|
+
if (planHash !== milestone.planHash) {
|
|
1502
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone Plan changed after it was recorded.');
|
|
1503
|
+
}
|
|
1504
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.execution_authorize', actor, milestone);
|
|
1505
|
+
const event = {
|
|
1506
|
+
kind: 'execution',
|
|
1507
|
+
decision: 'approved',
|
|
1508
|
+
actor: actor.trim() || 'user',
|
|
1509
|
+
recordedAt: this.now().toISOString(),
|
|
1510
|
+
briefHash: null,
|
|
1511
|
+
planHash,
|
|
1512
|
+
resultHash: null,
|
|
1513
|
+
evidenceHash: null,
|
|
1514
|
+
headCommit: runGit(identity.repositoryRoot, ['rev-parse', milestone.baseBranch]),
|
|
1515
|
+
reason,
|
|
1516
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1517
|
+
...(delegation ? { delegation } : {}),
|
|
1518
|
+
};
|
|
1519
|
+
return this.store.writeMilestone({ ...milestone, status: 'active', planHash, authorizations: [...milestone.authorizations, event] }, expectedRevision);
|
|
1450
1520
|
}
|
|
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.');
|
|
1521
|
+
finally {
|
|
1522
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1457
1523
|
}
|
|
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
1524
|
}
|
|
1475
1525
|
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),
|
|
1526
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1527
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1528
|
+
try {
|
|
1529
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1530
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1531
|
+
if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
|
|
1532
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} cannot be validated while ${milestone.status}.`);
|
|
1533
|
+
}
|
|
1534
|
+
this.assertMilestoneAuthorizationFresh(milestone);
|
|
1535
|
+
if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
|
|
1536
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone validation requires a clean base branch checkout.', {
|
|
1537
|
+
expectedBranch: milestone.baseBranch,
|
|
1538
|
+
actualBranch: currentBranch(identity.repositoryRoot),
|
|
1539
|
+
changedFiles: changedFiles(identity.repositoryRoot),
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
const incomplete = milestone.memberships.filter((membership) => {
|
|
1543
|
+
if (membership.disposition !== 'required')
|
|
1544
|
+
return false;
|
|
1545
|
+
return this.store.readTask(identity.projectId, membership.taskId).status !== 'merged';
|
|
1498
1546
|
});
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1547
|
+
if (incomplete.length > 0) {
|
|
1548
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Required Milestone Tasks are not merged.', {
|
|
1549
|
+
taskIds: incomplete.map((membership) => membership.taskId),
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
const checks = runChecks(identity.repositoryRoot, milestone.checks);
|
|
1553
|
+
const baseHead = headCommit(identity.repositoryRoot);
|
|
1554
|
+
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
1555
|
+
const stagedEvidence = this.store.stageArtifact(root, 'evidence.json', JSON.stringify({
|
|
1556
|
+
milestoneId,
|
|
1557
|
+
baseBranch: milestone.baseBranch,
|
|
1558
|
+
headCommit: baseHead,
|
|
1559
|
+
generatedAt: this.now().toISOString(),
|
|
1560
|
+
membershipRevision: milestone.membershipRevision,
|
|
1561
|
+
memberships: milestone.memberships,
|
|
1562
|
+
checks,
|
|
1563
|
+
}, null, 2));
|
|
1564
|
+
if (checks.some((check) => !check.passed)) {
|
|
1565
|
+
const saved = this.store.writeMilestone({ ...milestone, status: 'blocked', evidenceHash: stagedEvidence.hash }, expectedRevision);
|
|
1566
|
+
this.store.publishArtifact(stagedEvidence);
|
|
1567
|
+
return saved;
|
|
1568
|
+
}
|
|
1569
|
+
const result = [
|
|
1570
|
+
`# Milestone result: ${milestone.title}`,
|
|
1571
|
+
'',
|
|
1572
|
+
`Outcome: ${milestone.outcome}`,
|
|
1573
|
+
'',
|
|
1574
|
+
`Validated base: ${milestone.baseBranch}@${baseHead}`,
|
|
1575
|
+
'',
|
|
1576
|
+
'All required memberships are merged and all Milestone checks passed.',
|
|
1577
|
+
'',
|
|
1578
|
+
].join('\n');
|
|
1579
|
+
const stagedResult = this.store.stageArtifact(root, 'result.md', result);
|
|
1580
|
+
const saved = this.store.writeMilestone({
|
|
1581
|
+
...milestone,
|
|
1582
|
+
status: 'awaiting_final_acceptance',
|
|
1583
|
+
evidenceHash: stagedEvidence.hash,
|
|
1584
|
+
resultHash: stagedResult.hash,
|
|
1585
|
+
}, expectedRevision);
|
|
1514
1586
|
this.store.publishArtifact(stagedEvidence);
|
|
1587
|
+
this.store.publishArtifact(stagedResult);
|
|
1515
1588
|
return saved;
|
|
1516
1589
|
}
|
|
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;
|
|
1590
|
+
finally {
|
|
1591
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1592
|
+
}
|
|
1537
1593
|
}
|
|
1538
1594
|
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
|
-
|
|
1595
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1596
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1597
|
+
try {
|
|
1598
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1599
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1600
|
+
if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
|
|
1601
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not ready for acceptance.`);
|
|
1602
|
+
}
|
|
1603
|
+
if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
|
|
1604
|
+
throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone acceptance requires a clean base branch checkout.');
|
|
1605
|
+
}
|
|
1606
|
+
const root = this.store.milestoneRoot(identity.projectId, milestoneId);
|
|
1607
|
+
const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
|
|
1608
|
+
const currentHead = headCommit(identity.repositoryRoot);
|
|
1609
|
+
const resultHash = this.store.hashArtifact(root, 'result.md');
|
|
1610
|
+
const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
|
|
1611
|
+
if (evidence.headCommit !== currentHead || resultHash !== milestone.resultHash || evidenceHash !== milestone.evidenceHash) {
|
|
1612
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone validation evidence is stale.');
|
|
1613
|
+
}
|
|
1614
|
+
const normalizedActor = actor.trim();
|
|
1615
|
+
if (!normalizedActor) {
|
|
1616
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone final acceptance requires an explicit actor.');
|
|
1617
|
+
}
|
|
1618
|
+
const gate = milestoneFinalAcceptanceGate(milestone, currentHead);
|
|
1619
|
+
if (confirmationCode.trim() !== gate.confirmationCode) {
|
|
1620
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone final acceptance confirmation does not match the current validated state.', {
|
|
1621
|
+
milestoneId,
|
|
1622
|
+
revision: milestone.revision,
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.final_accept', normalizedActor, milestone);
|
|
1626
|
+
const event = {
|
|
1627
|
+
kind: 'final_acceptance',
|
|
1628
|
+
decision: 'approved',
|
|
1629
|
+
actor: normalizedActor,
|
|
1630
|
+
recordedAt: this.now().toISOString(),
|
|
1631
|
+
briefHash: null,
|
|
1632
|
+
planHash: milestone.planHash,
|
|
1633
|
+
resultHash,
|
|
1634
|
+
evidenceHash,
|
|
1635
|
+
headCommit: currentHead,
|
|
1636
|
+
reason: 'Milestone final acceptance confirmed against the current validated state.',
|
|
1637
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1638
|
+
...(delegation ? { delegation } : {}),
|
|
1639
|
+
};
|
|
1640
|
+
return this.store.writeMilestone({
|
|
1641
|
+
...milestone,
|
|
1642
|
+
status: 'accepted',
|
|
1643
|
+
resultHash,
|
|
1644
|
+
evidenceHash,
|
|
1645
|
+
acceptedHead: currentHead,
|
|
1646
|
+
authorizations: [...milestone.authorizations, event],
|
|
1647
|
+
}, expectedRevision);
|
|
1555
1648
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone final acceptance requires an explicit actor.');
|
|
1649
|
+
finally {
|
|
1650
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1559
1651
|
}
|
|
1560
|
-
const gate = milestoneFinalAcceptanceGate(milestone, currentHead);
|
|
1561
|
-
if (confirmationCode.trim() !== gate.confirmationCode) {
|
|
1562
|
-
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone final acceptance confirmation does not match the current validated state.', {
|
|
1563
|
-
milestoneId,
|
|
1564
|
-
revision: milestone.revision,
|
|
1565
|
-
});
|
|
1566
|
-
}
|
|
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
1652
|
}
|
|
1591
1653
|
cancelMilestone(repository, milestoneId, expectedRevision, reason) {
|
|
1592
1654
|
if (!reason.trim())
|
|
1593
1655
|
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone cancellation requires a reason.');
|
|
1594
|
-
const { identity } = this.#mutationContext(repository);
|
|
1595
|
-
const
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1656
|
+
const { identity, locks } = this.#mutationContext(repository);
|
|
1657
|
+
const authorityLease = locks.acquire(milestoneId, 'workflow-core:milestone-authority');
|
|
1658
|
+
try {
|
|
1659
|
+
const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
|
|
1660
|
+
assertExpectedRevision(milestone, expectedRevision);
|
|
1661
|
+
if (['accepted', 'cancelled'].includes(milestone.status)) {
|
|
1662
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
|
|
1663
|
+
}
|
|
1664
|
+
return this.store.writeMilestone({ ...milestone, status: 'cancelled', cancellationReason: reason.trim() }, expectedRevision);
|
|
1665
|
+
}
|
|
1666
|
+
finally {
|
|
1667
|
+
locks.release(milestoneId, authorityLease.token);
|
|
1599
1668
|
}
|
|
1600
|
-
return this.store.writeMilestone({ ...milestone, status: 'cancelled', cancellationReason: reason.trim() }, expectedRevision);
|
|
1601
1669
|
}
|
|
1602
1670
|
setTaskPlan(repository, taskId, expectedRevision, plan, correctiveAudit = null, planRiskAudit = null) {
|
|
1603
1671
|
validatePlan(plan);
|
|
@@ -2583,8 +2651,12 @@ export class WorkflowService {
|
|
|
2583
2651
|
});
|
|
2584
2652
|
}
|
|
2585
2653
|
}
|
|
2586
|
-
const
|
|
2587
|
-
const
|
|
2654
|
+
const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
2655
|
+
const navigation = planningRecovery
|
|
2656
|
+
? this.nextForTaskWithDelegation(task.projectId, context.identity.repositoryRoot, task, planningRecovery.current)
|
|
2657
|
+
: this.taskGitNavigationOverride(task) ?? nextForTask(task);
|
|
2658
|
+
const action = String(navigation.action === 'task writer-credential-replace'
|
|
2659
|
+
? navigation.blockedAction : navigation.action);
|
|
2588
2660
|
const currentLease = context.locks.inspect(taskId);
|
|
2589
2661
|
const writerTokenContract = writerTokenContractForAction(action, posture.state === 'claimed', currentLease !== null);
|
|
2590
2662
|
const requiredActor = writerTokenContract
|
|
@@ -3649,10 +3721,14 @@ export class WorkflowService {
|
|
|
3649
3721
|
assertExpectedRevision(task, expectedRevision);
|
|
3650
3722
|
this.proveOptionalWriterLeaseToken(this.#mutationContext(repository), taskId, writerToken);
|
|
3651
3723
|
this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
|
|
3652
|
-
|
|
3724
|
+
const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
3725
|
+
if (!['ready', 'needs_fix', 'blocked'].includes(task.status) && !planningRecovery) {
|
|
3653
3726
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
|
|
3654
3727
|
}
|
|
3655
|
-
|
|
3728
|
+
if (planningRecovery && correctiveAudit.decision === 'continue-fix') {
|
|
3729
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'A stopped rebound Plan requires replan, split, or escalation; continue-fix cannot restore execution authority.', { taskId, stepId });
|
|
3730
|
+
}
|
|
3731
|
+
const planRiskAudit = readCorrectivePlanningAudit(this.store, task);
|
|
3656
3732
|
if (!planRiskAudit) {
|
|
3657
3733
|
throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no current Plan Risk Audit for corrective decision recording.`, {
|
|
3658
3734
|
taskId,
|
|
@@ -4231,7 +4307,7 @@ export class WorkflowService {
|
|
|
4231
4307
|
throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
|
|
4232
4308
|
const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
|
|
4233
4309
|
const sourceVersion = '2.0.0-beta.13.6';
|
|
4234
|
-
const targetVersion =
|
|
4310
|
+
const targetVersion = CONSUMED_CARRYOVER_TARGET_VERSION;
|
|
4235
4311
|
const sourceSurfaces = {
|
|
4236
4312
|
declared: versions.declared,
|
|
4237
4313
|
locked: versions.locked,
|
|
@@ -4330,7 +4406,7 @@ export class WorkflowService {
|
|
|
4330
4406
|
consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate) {
|
|
4331
4407
|
const repositoryRoot = snapshot.identity.repositoryRoot;
|
|
4332
4408
|
const sourceVersion = '2.0.0-beta.13.6';
|
|
4333
|
-
const targetVersion =
|
|
4409
|
+
const targetVersion = CONSUMED_CARRYOVER_TARGET_VERSION;
|
|
4334
4410
|
const c1 = readTaskC1Posture(this.store, task);
|
|
4335
4411
|
const recorded = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan && task.systemCommits.includes(record.commitSha))?.rootCauseReplan;
|
|
4336
4412
|
const currentHead = headCommit(repositoryRoot);
|
|
@@ -5337,6 +5413,18 @@ export class WorkflowService {
|
|
|
5337
5413
|
const existing = this.readGraphBindingOrNull(identity.projectId, graphKind);
|
|
5338
5414
|
return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
|
|
5339
5415
|
}
|
|
5416
|
+
explainChange(repository, taskId, input) {
|
|
5417
|
+
const status = this.statusFromSnapshot(this.observationSnapshot(repository));
|
|
5418
|
+
const task = status.tasks.find(candidate => candidate.id === taskId);
|
|
5419
|
+
if (!task && status.observation.kind === 'assessment') {
|
|
5420
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'The requested Task is not present in the current observation.');
|
|
5421
|
+
}
|
|
5422
|
+
const navigation = this.nextFromStatus(repository, taskId, status);
|
|
5423
|
+
const step = task?.steps.find(candidate => candidate.id === navigation.stepId)
|
|
5424
|
+
?? task?.steps.find(candidate => candidate.status === 'in_progress' || candidate.status === 'failed');
|
|
5425
|
+
const effectiveTask = task && step ? taskWithEffectiveStepAllowedWrites(this.store, task, step.id) : task;
|
|
5426
|
+
return explainChange(input, repository, effectiveTask, navigation);
|
|
5427
|
+
}
|
|
5340
5428
|
next(repository, taskId = null) {
|
|
5341
5429
|
return withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository))));
|
|
5342
5430
|
}
|
|
@@ -5448,6 +5536,37 @@ export class WorkflowService {
|
|
|
5448
5536
|
}
|
|
5449
5537
|
const withAdoption = (value) => ({
|
|
5450
5538
|
...value,
|
|
5539
|
+
...(status.milestones.some((milestone) => milestone.status === 'active' && milestone.planHash)
|
|
5540
|
+
? { activeMilestoneAutonomyOptions: status.milestones
|
|
5541
|
+
.filter((milestone) => milestone.status === 'active' && milestone.planHash)
|
|
5542
|
+
.filter((milestone) => {
|
|
5543
|
+
const event = readMilestoneAutonomyEvents(this.store, status.projectId, milestone.id).at(-1);
|
|
5544
|
+
if (!event)
|
|
5545
|
+
return true;
|
|
5546
|
+
try {
|
|
5547
|
+
requireActiveMilestoneAutonomy({
|
|
5548
|
+
store: this.store, projectId: status.projectId, milestone,
|
|
5549
|
+
actor: event.delegate, now: this.now(),
|
|
5550
|
+
});
|
|
5551
|
+
return false;
|
|
5552
|
+
}
|
|
5553
|
+
catch (error) {
|
|
5554
|
+
if (error instanceof WorkflowError && error.code === 'TRANSITION_BLOCKED')
|
|
5555
|
+
return true;
|
|
5556
|
+
if (error instanceof WorkflowError && error.code === 'NOT_FOUND')
|
|
5557
|
+
return true;
|
|
5558
|
+
throw error;
|
|
5559
|
+
}
|
|
5560
|
+
})
|
|
5561
|
+
.map((milestone) => ({
|
|
5562
|
+
action: 'milestone autonomy-prepare',
|
|
5563
|
+
milestoneId: milestone.id,
|
|
5564
|
+
expectedRevision: milestone.revision,
|
|
5565
|
+
requiresExactHumanConfirmation: true,
|
|
5566
|
+
maximumDurationHours: 72,
|
|
5567
|
+
preparationRequired: true,
|
|
5568
|
+
meaning: 'Optional human-requested preparation; existing valid contracts cannot be replaced. Knowledge must be active.',
|
|
5569
|
+
})) } : {}),
|
|
5451
5570
|
adoption: adoptionInfo,
|
|
5452
5571
|
observation: status.observation,
|
|
5453
5572
|
});
|
|
@@ -5676,7 +5795,7 @@ export class WorkflowService {
|
|
|
5676
5795
|
&& !activeTask.steps.some((step) => step.status === 'failed');
|
|
5677
5796
|
if (activeTask.planHash && !requiresReplacementPlan) {
|
|
5678
5797
|
try {
|
|
5679
|
-
activePlanRiskAudit =
|
|
5798
|
+
activePlanRiskAudit = readCorrectivePlanningAudit(this.store, activeTask);
|
|
5680
5799
|
}
|
|
5681
5800
|
catch (error) {
|
|
5682
5801
|
if (!(error instanceof WorkflowError) || error.code !== 'TRANSITION_BLOCKED')
|
|
@@ -5706,6 +5825,9 @@ export class WorkflowService {
|
|
|
5706
5825
|
: new Map();
|
|
5707
5826
|
const remediationMembership = remediationMemberships.get(activeTask.id) ?? null;
|
|
5708
5827
|
const auditedRemediationEligible = activeTask.status === 'awaiting_execution_authorization'
|
|
5828
|
+
&& activeTask.planHash !== null
|
|
5829
|
+
&& activeTask.workspaceOwner === null
|
|
5830
|
+
&& activeTask.steps.every(step => step.status === 'planned')
|
|
5709
5831
|
&& remediationMilestone?.status === 'active'
|
|
5710
5832
|
&& remediationMembership?.disposition === 'required';
|
|
5711
5833
|
const remediationAncestorIds = new Set();
|
|
@@ -6316,7 +6438,9 @@ export class WorkflowService {
|
|
|
6316
6438
|
return { milestone, authorization, scopeChange };
|
|
6317
6439
|
}
|
|
6318
6440
|
nextForTaskWithDelegation(projectId, repositoryRoot, task, planRiskAudit = null) {
|
|
6319
|
-
const
|
|
6441
|
+
const planningRecovery = readFailedStepPlanningRecovery(this.store, task);
|
|
6442
|
+
// A stopped rebound Plan can expose planning recovery, never an approval route.
|
|
6443
|
+
const transition = planningRecovery ? null : task.status === 'awaiting_execution_authorization'
|
|
6320
6444
|
? 'task.execution_authorize'
|
|
6321
6445
|
: task.status === 'awaiting_final_acceptance'
|
|
6322
6446
|
? 'task.final_accept'
|
|
@@ -7119,12 +7243,56 @@ export class WorkflowService {
|
|
|
7119
7243
|
}
|
|
7120
7244
|
: {}),
|
|
7121
7245
|
};
|
|
7246
|
+
// A later independent audit may discover an obstruction absent from the sealed
|
|
7247
|
+
// fix review. Advertise the existing append-only decision route without turning
|
|
7248
|
+
// every ordinary repair into a mandatory replan or rewriting that review.
|
|
7249
|
+
const correctiveDecisionOrdinal = failedStepRemediations.length + 1;
|
|
7250
|
+
const optionalCorrectiveDecision = (result.action === 'task run' || planningRecovery !== null)
|
|
7251
|
+
&& (['needs_fix', 'ready'].includes(task.status) || planningRecovery !== null)
|
|
7252
|
+
&& failedStep && task.planHash && planRiskAudit
|
|
7253
|
+
&& planRiskAudit.reviewRequiredStepIds.includes(failedStep.id)
|
|
7254
|
+
&& !task.steps.some(step => step.status === 'in_progress')
|
|
7255
|
+
&& failedStepRemediations.length >= 2
|
|
7256
|
+
&& handoffPosture.state !== 'pending'
|
|
7257
|
+
&& !readCorrectiveDecisionEvents(this.store, task).some(event => event.stepId === failedStep.id && event.triggeringAttemptCount === correctiveDecisionOrdinal)
|
|
7258
|
+
&& currentBranch(repositoryRoot) === task.taskBranch && isClean(repositoryRoot)
|
|
7259
|
+
? {
|
|
7260
|
+
command: 'task corrective-decision',
|
|
7261
|
+
state: 'eligible',
|
|
7262
|
+
optional: true,
|
|
7263
|
+
taskId: task.id,
|
|
7264
|
+
stepId: failedStep.id,
|
|
7265
|
+
expectedRevision: task.revision,
|
|
7266
|
+
expectedBriefHash: task.briefHash,
|
|
7267
|
+
expectedPlanHash: task.planHash,
|
|
7268
|
+
expectedHeadCommit: headCommit(repositoryRoot),
|
|
7269
|
+
triggeringAttemptCount: correctiveDecisionOrdinal,
|
|
7270
|
+
coveredRemediationEvents: failedStepRemediations.map(event => ({ eventId: event.eventId, eventHash: event.eventHash })),
|
|
7271
|
+
recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
|
|
7272
|
+
auditorMustDifferFromLatestStrictReviewer: true,
|
|
7273
|
+
requiredActor: handoffPosture.state === 'claimed' ? handoffPosture.claimant : null,
|
|
7274
|
+
decisions: planningRecovery ? ['replan-required', 'split-required', 'stop-escalate']
|
|
7275
|
+
: ['continue-fix', 'replan-required', 'split-required', 'stop-escalate'],
|
|
7276
|
+
authority: 'none',
|
|
7277
|
+
preservesOriginalReview: true,
|
|
7278
|
+
humanApprovalRequiredForDecision: false,
|
|
7279
|
+
replanRequiresExistingHumanGate: true,
|
|
7280
|
+
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.',
|
|
7281
|
+
}
|
|
7282
|
+
: null;
|
|
7283
|
+
const resultWithCorrectiveOption = optionalCorrectiveDecision
|
|
7284
|
+
? { ...result, correctiveDecisionOption: optionalCorrectiveDecision,
|
|
7285
|
+
...(planningRecovery ? { action: 'task corrective-decision', blockedAction: 'task authorize',
|
|
7286
|
+
planningRecovery: { authority: 'planning-only', obstructionAuditHash: planningRecovery.current.audit.eventHash,
|
|
7287
|
+
baselineAuditHash: planningRecovery.baseline.eventHash } } : {}) }
|
|
7288
|
+
: planningRecovery && result.action === 'task authorize'
|
|
7289
|
+
? { ...result, action: 'doctor', blockedAction: 'task authorize' } : result;
|
|
7122
7290
|
const rootCauseAllowsPendingClaim = !rootCauseReplanDirtyCarryover?.applicable
|
|
7123
7291
|
|| (rootCauseReplanDirtyCarryover.blockers.length === 1
|
|
7124
7292
|
&& rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1'));
|
|
7125
7293
|
if (handoffPosture.state === 'pending' && rootCauseAllowsPendingClaim) {
|
|
7126
7294
|
return this.withTaskNavigationContracts(task, {
|
|
7127
|
-
...
|
|
7295
|
+
...resultWithCorrectiveOption,
|
|
7128
7296
|
action: 'task claim',
|
|
7129
7297
|
blockedAction: next.action,
|
|
7130
7298
|
});
|
|
@@ -7132,10 +7300,10 @@ export class WorkflowService {
|
|
|
7132
7300
|
const navigableResult = ['task plan-set']
|
|
7133
7301
|
.includes(String(result.action))
|
|
7134
7302
|
? {
|
|
7135
|
-
...
|
|
7303
|
+
...resultWithCorrectiveOption,
|
|
7136
7304
|
taskPlanContract: this.taskPlanContract(projectId, task),
|
|
7137
7305
|
}
|
|
7138
|
-
:
|
|
7306
|
+
: resultWithCorrectiveOption;
|
|
7139
7307
|
return this.withTaskNavigationContracts(task, navigableResult);
|
|
7140
7308
|
}
|
|
7141
7309
|
taskPlanContract(projectId, task) {
|
|
@@ -7200,7 +7368,7 @@ export class WorkflowService {
|
|
|
7200
7368
|
taskTerminalCorrectivePosture(task) {
|
|
7201
7369
|
if (!task.planHash)
|
|
7202
7370
|
return null;
|
|
7203
|
-
const planRiskAudit =
|
|
7371
|
+
const planRiskAudit = readCurrentPlanningObstructionAudit(this.store, task);
|
|
7204
7372
|
if (!planRiskAudit)
|
|
7205
7373
|
return null;
|
|
7206
7374
|
const posture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
|
|
@@ -7245,7 +7413,7 @@ export class WorkflowService {
|
|
|
7245
7413
|
}
|
|
7246
7414
|
}
|
|
7247
7415
|
delegatedApprovalOptions(projectId, transition, target) {
|
|
7248
|
-
return this.
|
|
7416
|
+
return this.listDelegations(projectId)
|
|
7249
7417
|
.filter((grant) => {
|
|
7250
7418
|
try {
|
|
7251
7419
|
return Boolean(this.delegatedAuthorization(projectId, grant.id, transition, grant.delegate, target));
|
|
@@ -7267,7 +7435,7 @@ export class WorkflowService {
|
|
|
7267
7435
|
}));
|
|
7268
7436
|
}
|
|
7269
7437
|
delegatedContextRefreshOptions(projectId, task, knowledgeMap, mode) {
|
|
7270
|
-
return this.
|
|
7438
|
+
return this.listDelegations(projectId)
|
|
7271
7439
|
.filter((grant) => {
|
|
7272
7440
|
try {
|
|
7273
7441
|
if (mode === 'delegated-plan-bounded-supporting-source') {
|