codex-workflow-v2 2.0.0-beta.13.7 → 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.
Files changed (32) hide show
  1. package/README.md +1 -1
  2. package/dist/reviewer-runtime-build.json +13 -9
  3. package/dist/src/alpha7/autonomy.d.ts +3 -0
  4. package/dist/src/alpha7/autonomy.js +89 -42
  5. package/dist/src/alpha7/autonomy.js.map +1 -1
  6. package/dist/src/change-explanation.d.ts +64 -0
  7. package/dist/src/change-explanation.js +150 -0
  8. package/dist/src/change-explanation.js.map +1 -0
  9. package/dist/src/cli-actions.d.ts +1 -0
  10. package/dist/src/cli-actions.js +1 -0
  11. package/dist/src/cli-actions.js.map +1 -1
  12. package/dist/src/cli.js +4 -1
  13. package/dist/src/cli.js.map +1 -1
  14. package/dist/src/state/store.d.ts +1 -1
  15. package/dist/src/state/store.js +17 -3
  16. package/dist/src/state/store.js.map +1 -1
  17. package/dist/src/version.d.ts +1 -1
  18. package/dist/src/version.js +1 -1
  19. package/dist/src/workflow.d.ts +49 -0
  20. package/dist/src/workflow.js +376 -230
  21. package/dist/src/workflow.js.map +1 -1
  22. package/docs/change-model.md +118 -0
  23. package/docs/delegated-approval.md +26 -0
  24. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  25. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  26. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  27. package/docs/release.md +47 -0
  28. package/package.json +1 -1
  29. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +76 -0
  30. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +116 -3
  31. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +6 -1
  32. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +35 -6
@@ -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';
@@ -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.store.listDelegations(identity.projectId),
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.store.listDelegations(identity.projectId)
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 grant = this.store.readDelegation(identity.projectId, grantId);
463
- assertExpectedRevision(grant, expectedRevision);
464
- if (grant.status !== 'active') {
465
- throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grantId} is already ${grant.status}.`);
466
- }
467
- if (actor.trim() !== grant.principal) {
468
- throw new WorkflowError('TRANSITION_BLOCKED', 'Only the recorded principal may revoke a delegation.', {
469
- principal: grant.principal,
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 milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
1364
- assertExpectedRevision(milestone, expectedRevision);
1365
- assertMilestoneScopeChangeStatusAllowed(milestone);
1366
- const prepared = this.prepareMilestoneScopeChangeCandidate(identity.projectId, milestone, plan, actor);
1367
- if (confirmationCode.trim() !== prepared.confirmationCode) {
1368
- throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone scope-change confirmation does not match the current Milestone state and requested scope.', {
1369
- milestoneId,
1370
- revision: milestone.revision,
1371
- actor: prepared.actor,
1372
- confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
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
- return applyMilestoneScopeChangeTransaction({
1376
- store: this.store,
1377
- milestone,
1378
- expectedRevision,
1379
- prepared,
1380
- now: this.now(),
1381
- readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
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 { identity } = this.#mutationContext(repository);
1386
- const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
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 milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1401
- assertExpectedRevision(milestone, expectedRevision);
1402
- this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
1403
- return applyMilestoneAutonomyContract({
1404
- store: this.store,
1405
- projectId: identity.projectId,
1406
- milestone,
1407
- principal,
1408
- delegate,
1409
- expiresAt,
1410
- confirmationCode,
1411
- now: this.now(),
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 milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
1417
- assertExpectedRevision(milestone, expectedRevision);
1418
- assertMilestoneScopeChangeStatusAllowed(milestone);
1419
- const autonomy = requireActiveMilestoneAutonomy({
1420
- store: this.store,
1421
- projectId: identity.projectId,
1422
- milestone,
1423
- actor,
1424
- now: this.now(),
1425
- });
1426
- validateMilestonePlan(plan);
1427
- assertAutonomousPlanEvolution(milestone, plan, autonomy.contract);
1428
- const prepared = prepareMilestoneScopeChangeCandidate({
1429
- milestone,
1430
- plan,
1431
- readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
1432
- actor,
1433
- autonomyContractEventHash: autonomy.contract.eventHash,
1434
- });
1435
- return applyMilestoneScopeChangeTransaction({
1436
- store: this.store,
1437
- milestone,
1438
- expectedRevision,
1439
- prepared,
1440
- now: this.now(),
1441
- readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
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 milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1447
- assertExpectedRevision(milestone, expectedRevision);
1448
- if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
1449
- throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
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
- assertMilestoneDependenciesDeclared(milestone);
1452
- this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId);
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 milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1478
- assertExpectedRevision(milestone, expectedRevision);
1479
- if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
1480
- throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} cannot be validated while ${milestone.status}.`);
1481
- }
1482
- this.assertMilestoneAuthorizationFresh(milestone);
1483
- if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
1484
- throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone validation requires a clean base branch checkout.', {
1485
- expectedBranch: milestone.baseBranch,
1486
- actualBranch: currentBranch(identity.repositoryRoot),
1487
- changedFiles: changedFiles(identity.repositoryRoot),
1488
- });
1489
- }
1490
- const incomplete = milestone.memberships.filter((membership) => {
1491
- if (membership.disposition !== 'required')
1492
- return false;
1493
- return this.store.readTask(identity.projectId, membership.taskId).status !== 'merged';
1494
- });
1495
- if (incomplete.length > 0) {
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
- const checks = runChecks(identity.repositoryRoot, milestone.checks);
1501
- const baseHead = headCommit(identity.repositoryRoot);
1502
- const root = this.store.milestoneRoot(identity.projectId, milestoneId);
1503
- const stagedEvidence = this.store.stageArtifact(root, 'evidence.json', JSON.stringify({
1504
- milestoneId,
1505
- baseBranch: milestone.baseBranch,
1506
- headCommit: baseHead,
1507
- generatedAt: this.now().toISOString(),
1508
- membershipRevision: milestone.membershipRevision,
1509
- memberships: milestone.memberships,
1510
- checks,
1511
- }, null, 2));
1512
- if (checks.some((check) => !check.passed)) {
1513
- const saved = this.store.writeMilestone({ ...milestone, status: 'blocked', evidenceHash: stagedEvidence.hash }, expectedRevision);
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
- const result = [
1518
- `# Milestone result: ${milestone.title}`,
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 milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1541
- assertExpectedRevision(milestone, expectedRevision);
1542
- if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
1543
- throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not ready for acceptance.`);
1544
- }
1545
- if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
1546
- throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Milestone acceptance requires a clean base branch checkout.');
1547
- }
1548
- const root = this.store.milestoneRoot(identity.projectId, milestoneId);
1549
- const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
1550
- const currentHead = headCommit(identity.repositoryRoot);
1551
- const resultHash = this.store.hashArtifact(root, 'result.md');
1552
- const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
1553
- if (evidence.headCommit !== currentHead || resultHash !== milestone.resultHash || evidenceHash !== milestone.evidenceHash) {
1554
- throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone validation evidence is stale.');
1555
- }
1556
- const normalizedActor = actor.trim();
1557
- if (!normalizedActor) {
1558
- throw new WorkflowError('INVALID_ARGUMENT', 'Milestone final acceptance requires an explicit actor.');
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
- 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
- });
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 milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
1596
- assertExpectedRevision(milestone, expectedRevision);
1597
- if (['accepted', 'cancelled'].includes(milestone.status)) {
1598
- throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
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);
@@ -4231,7 +4296,7 @@ export class WorkflowService {
4231
4296
  throw new WorkflowError('NOT_FOUND', `Task ${taskId} was not found in this repository.`);
4232
4297
  const versions = inspectWorkflowVersions(snapshot.identity.repositoryRoot, [task.baseBranch]);
4233
4298
  const sourceVersion = '2.0.0-beta.13.6';
4234
- const targetVersion = '2.0.0-beta.13.7';
4299
+ const targetVersion = '2.0.0-beta.13.8';
4235
4300
  const sourceSurfaces = {
4236
4301
  declared: versions.declared,
4237
4302
  locked: versions.locked,
@@ -4330,7 +4395,7 @@ export class WorkflowService {
4330
4395
  consumedCarryoverSourceRecoveryPreflight(snapshot, task, sourceSurfaces, candidate) {
4331
4396
  const repositoryRoot = snapshot.identity.repositoryRoot;
4332
4397
  const sourceVersion = '2.0.0-beta.13.6';
4333
- const targetVersion = '2.0.0-beta.13.7';
4398
+ const targetVersion = '2.0.0-beta.13.8';
4334
4399
  const c1 = readTaskC1Posture(this.store, task);
4335
4400
  const recorded = [...(task.dependencyProvenanceRecoveries ?? [])].reverse().find((record) => record.rootCauseReplan && task.systemCommits.includes(record.commitSha))?.rootCauseReplan;
4336
4401
  const currentHead = headCommit(repositoryRoot);
@@ -5337,6 +5402,18 @@ export class WorkflowService {
5337
5402
  const existing = this.readGraphBindingOrNull(identity.projectId, graphKind);
5338
5403
  return this.store.writeGraphBinding(fallbackGraphBinding(identity.projectId, request, reason, existing, this.now()), expectedRevision);
5339
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
+ }
5340
5417
  next(repository, taskId = null) {
5341
5418
  return withObservedRouteMetadata(this.nextFromStatus(repository, taskId, this.statusFromSnapshot(this.observationSnapshot(repository))));
5342
5419
  }
@@ -5448,6 +5525,37 @@ export class WorkflowService {
5448
5525
  }
5449
5526
  const withAdoption = (value) => ({
5450
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
+ })) } : {}),
5451
5559
  adoption: adoptionInfo,
5452
5560
  observation: status.observation,
5453
5561
  });
@@ -7119,12 +7227,50 @@ export class WorkflowService {
7119
7227
  }
7120
7228
  : {}),
7121
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;
7122
7268
  const rootCauseAllowsPendingClaim = !rootCauseReplanDirtyCarryover?.applicable
7123
7269
  || (rootCauseReplanDirtyCarryover.blockers.length === 1
7124
7270
  && rootCauseReplanDirtyCarryover.blockers[0]?.startsWith('Corrective carryover requires the retained claimed C1'));
7125
7271
  if (handoffPosture.state === 'pending' && rootCauseAllowsPendingClaim) {
7126
7272
  return this.withTaskNavigationContracts(task, {
7127
- ...result,
7273
+ ...resultWithCorrectiveOption,
7128
7274
  action: 'task claim',
7129
7275
  blockedAction: next.action,
7130
7276
  });
@@ -7132,10 +7278,10 @@ export class WorkflowService {
7132
7278
  const navigableResult = ['task plan-set']
7133
7279
  .includes(String(result.action))
7134
7280
  ? {
7135
- ...result,
7281
+ ...resultWithCorrectiveOption,
7136
7282
  taskPlanContract: this.taskPlanContract(projectId, task),
7137
7283
  }
7138
- : result;
7284
+ : resultWithCorrectiveOption;
7139
7285
  return this.withTaskNavigationContracts(task, navigableResult);
7140
7286
  }
7141
7287
  taskPlanContract(projectId, task) {
@@ -7245,7 +7391,7 @@ export class WorkflowService {
7245
7391
  }
7246
7392
  }
7247
7393
  delegatedApprovalOptions(projectId, transition, target) {
7248
- return this.store.listDelegations(projectId)
7394
+ return this.listDelegations(projectId)
7249
7395
  .filter((grant) => {
7250
7396
  try {
7251
7397
  return Boolean(this.delegatedAuthorization(projectId, grant.id, transition, grant.delegate, target));
@@ -7267,7 +7413,7 @@ export class WorkflowService {
7267
7413
  }));
7268
7414
  }
7269
7415
  delegatedContextRefreshOptions(projectId, task, knowledgeMap, mode) {
7270
- return this.store.listDelegations(projectId)
7416
+ return this.listDelegations(projectId)
7271
7417
  .filter((grant) => {
7272
7418
  try {
7273
7419
  if (mode === 'delegated-plan-bounded-supporting-source') {