codex-workflow-v2 2.0.0-alpha.4 → 2.0.0-alpha.7

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 (82) hide show
  1. package/README.md +114 -10
  2. package/dist/src/alpha6/adoption.d.ts +55 -0
  3. package/dist/src/alpha6/adoption.js +920 -0
  4. package/dist/src/alpha6/adoption.js.map +1 -0
  5. package/dist/src/alpha6/handoff.d.ts +40 -0
  6. package/dist/src/alpha6/handoff.js +981 -0
  7. package/dist/src/alpha6/handoff.js.map +1 -0
  8. package/dist/src/alpha6/journal.d.ts +30 -0
  9. package/dist/src/alpha6/journal.js +369 -0
  10. package/dist/src/alpha6/journal.js.map +1 -0
  11. package/dist/src/alpha6/milestone.d.ts +52 -0
  12. package/dist/src/alpha6/milestone.js +1103 -0
  13. package/dist/src/alpha6/milestone.js.map +1 -0
  14. package/dist/src/alpha6/plan-risk.d.ts +32 -0
  15. package/dist/src/alpha6/plan-risk.js +908 -0
  16. package/dist/src/alpha6/plan-risk.js.map +1 -0
  17. package/dist/src/alpha6/remediation.d.ts +21 -0
  18. package/dist/src/alpha6/remediation.js +754 -0
  19. package/dist/src/alpha6/remediation.js.map +1 -0
  20. package/dist/src/alpha6/review.d.ts +46 -0
  21. package/dist/src/alpha6/review.js +785 -0
  22. package/dist/src/alpha6/review.js.map +1 -0
  23. package/dist/src/alpha6/store-sidecars.d.ts +35 -0
  24. package/dist/src/alpha6/store-sidecars.js +281 -0
  25. package/dist/src/alpha6/store-sidecars.js.map +1 -0
  26. package/dist/src/alpha7/autonomy.d.ts +61 -0
  27. package/dist/src/alpha7/autonomy.js +256 -0
  28. package/dist/src/alpha7/autonomy.js.map +1 -0
  29. package/dist/src/cli.js +109 -19
  30. package/dist/src/cli.js.map +1 -1
  31. package/dist/src/contracts.d.ts +268 -0
  32. package/dist/src/git.js +2 -1
  33. package/dist/src/git.js.map +1 -1
  34. package/dist/src/index.d.ts +5 -0
  35. package/dist/src/index.js +3 -0
  36. package/dist/src/index.js.map +1 -1
  37. package/dist/src/reviewer.d.ts +6 -1
  38. package/dist/src/reviewer.js +145 -32
  39. package/dist/src/reviewer.js.map +1 -1
  40. package/dist/src/state/lock.d.ts +1 -0
  41. package/dist/src/state/lock.js +7 -1
  42. package/dist/src/state/lock.js.map +1 -1
  43. package/dist/src/state/store.d.ts +39 -1
  44. package/dist/src/state/store.js +127 -1
  45. package/dist/src/state/store.js.map +1 -1
  46. package/dist/src/version.d.ts +1 -1
  47. package/dist/src/version.js +1 -1
  48. package/dist/src/workflow.d.ts +113 -8
  49. package/dist/src/workflow.js +1635 -78
  50. package/dist/src/workflow.js.map +1 -1
  51. package/docs/autonomy-guardrails.md +164 -0
  52. package/docs/decisions.md +39 -0
  53. package/docs/delegated-approval.md +37 -5
  54. package/docs/development-flow.md +45 -1
  55. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +155 -136
  56. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +236 -223
  57. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +225 -206
  58. package/docs/project-memory.md +7 -0
  59. package/docs/release.md +9 -0
  60. package/docs/updating-existing-project.md +63 -5
  61. package/docs/validation-report.md +52 -34
  62. package/package.json +1 -1
  63. package/plugins/codex-workflow-gateway/references/protocol.md +182 -6
  64. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +35 -1
  65. package/references/git-policy.md +5 -2
  66. package/references/state-machine.md +43 -0
  67. package/references/validation-and-review.md +28 -1
  68. package/roles/delivery-coordinator.md +21 -1
  69. package/roles/independent-reviewer.md +3 -0
  70. package/roles/technical-planner.md +10 -0
  71. package/roles/worker.md +4 -0
  72. package/schemas/adoption-posture-event.schema.json +129 -0
  73. package/schemas/corrective-decision-event.schema.json +55 -0
  74. package/schemas/corrective-plan-audit.schema.json +20 -0
  75. package/schemas/milestone-autonomy-event.schema.json +45 -0
  76. package/schemas/milestone-scope-change-event.schema.json +70 -0
  77. package/schemas/milestone-transaction-journal.schema.json +95 -0
  78. package/schemas/plan-risk-audit-event.schema.json +107 -0
  79. package/schemas/remediation-event.schema.json +53 -0
  80. package/schemas/reviewer-attestation-event.schema.json +49 -0
  81. package/schemas/step-review-event.schema.json +74 -0
  82. package/schemas/task-handoff-event.schema.json +116 -0
@@ -1,16 +1,26 @@
1
- import { appendFileSync, readFileSync } from 'node:fs';
1
+ import { appendFileSync, existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import path from 'node:path';
4
4
  import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
5
5
  import { renderBrief, renderPlan, renderResult } from './artifacts.js';
6
- import { assertDelegationCanAuthorize, delegationCanAuthorize, prepareDelegationGrantGate, } from './delegation.js';
6
+ import { assertDelegationCanAuthorize, hashDelegationPolicy, prepareDelegationGrantGate, } from './delegation.js';
7
7
  import { assessDiscovery, mergeUnique } from './domain/discovery.js';
8
8
  import { pathAllowed, validatePlan } from './domain/validation.js';
9
9
  import { WorkflowError } from './errors.js';
10
10
  import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
11
+ import { inspectLocalDependency } from './diagnostics.js';
12
+ import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
13
+ import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
14
+ import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
15
+ import { appendCorrectiveDecisionEvent, appendRemediationEvent, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findFailedGuardedStepRequiringCorrectiveDecision, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
16
+ import { applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
17
+ import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
18
+ import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
11
19
  import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
20
+ import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
12
21
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
13
22
  import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
23
+ import { launchStrictStepReviewer } from './reviewer.js';
14
24
  import { WriterLockManager } from './state/lock.js';
15
25
  import { FileStateStore } from './state/store.js';
16
26
  import { createEntityId } from './ulid.js';
@@ -24,6 +34,18 @@ export class WorkflowService {
24
34
  }
25
35
  context(repository) {
26
36
  const identity = resolveRepositoryIdentity(repository);
37
+ const projectFile = path.join(this.store.projectRoot(identity.projectId), 'project.json');
38
+ const projectAlreadyRegistered = existsSync(projectFile);
39
+ if (!projectAlreadyRegistered) {
40
+ initializeProjectRegistrationAdoption(this.store, {
41
+ projectId: identity.projectId,
42
+ repositoryIdentity: identity,
43
+ packageVersion: PACKAGE_VERSION,
44
+ protocolVersion: PROTOCOL_VERSION,
45
+ stateSchemaVersion: STATE_SCHEMA_VERSION,
46
+ now: this.now(),
47
+ });
48
+ }
27
49
  this.store.registerProject(identity);
28
50
  return {
29
51
  identity,
@@ -162,7 +184,12 @@ export class WorkflowService {
162
184
  if (!title.trim())
163
185
  throw new WorkflowError('INVALID_ARGUMENT', 'Task title is required.');
164
186
  if (milestoneId) {
165
- const milestone = this.store.readMilestone(identity.projectId, milestoneId);
187
+ // A Milestone is assembled from independently materialized Task discoveries. During
188
+ // that bounded assembly window the reverse-membership set is intentionally incomplete;
189
+ // execution remains unavailable until one complete initial Milestone Plan classifies
190
+ // every linked Task. The scope-change reader preserves every other integrity check while
191
+ // allowing exactly that repairable reverse gap.
192
+ const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
166
193
  if (milestone.status === 'accepted' || milestone.status === 'cancelled') {
167
194
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
168
195
  }
@@ -271,17 +298,29 @@ export class WorkflowService {
271
298
  }
272
299
  setMilestonePlan(repository, milestoneId, expectedRevision, plan) {
273
300
  const { identity } = this.context(repository);
274
- const milestone = this.store.readMilestone(identity.projectId, milestoneId);
301
+ const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
275
302
  assertExpectedRevision(milestone, expectedRevision);
276
303
  if (!['planning', 'awaiting_execution_authorization', 'active', 'blocked'].includes(milestone.status)) {
277
304
  throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Milestone Plan while ${milestone.status}.`);
278
305
  }
306
+ if (milestone.status !== 'planning'
307
+ || milestone.planHash !== null
308
+ || milestone.membershipRevision !== 0
309
+ || milestone.taskIds.length > 0
310
+ || milestone.memberships.length > 0) {
311
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone Plan is no longer in the initial empty planning posture. Use explicit scope-change prepare/apply.', {
312
+ milestoneId,
313
+ revision: milestone.revision,
314
+ requiredAction: 'prepareMilestoneScopeChange -> applyMilestoneScopeChange',
315
+ });
316
+ }
279
317
  validateMilestonePlan(plan);
280
- const taskIds = plan.memberships.map((membership) => membership.taskId);
318
+ const normalized = normalizeMilestonePlan(plan);
319
+ const taskIds = normalized.taskIds;
281
320
  if (new Set(taskIds).size !== taskIds.length) {
282
321
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone memberships must have unique Task IDs.');
283
322
  }
284
- for (const membership of plan.memberships) {
323
+ for (const membership of normalized.memberships) {
285
324
  const task = this.store.readTask(identity.projectId, membership.taskId);
286
325
  if (task.milestoneId !== milestoneId) {
287
326
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} is not linked to Milestone ${milestoneId}.`);
@@ -296,22 +335,18 @@ export class WorkflowService {
296
335
  throw new WorkflowError('TRANSITION_BLOCKED', `Started Task ${task.id} cannot be removed from a Milestone.`);
297
336
  }
298
337
  }
299
- const root = this.store.milestoneRoot(identity.projectId, milestoneId);
300
- const normalized = {
301
- outcome: plan.outcome.trim(),
302
- successSignal: plan.successSignal.trim(),
303
- acceptance: [...plan.acceptance],
304
- memberships: plan.memberships.map((membership) => ({
305
- ...membership,
306
- reason: membership.reason.trim(),
307
- })),
308
- checks: [...plan.checks],
309
- };
310
- const stagedPlan = this.store.stageArtifact(root, 'plan.json', JSON.stringify(normalized, null, 2));
311
338
  const authorizations = milestone.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
312
339
  ? { ...authorization, decision: 'superseded' }
313
340
  : authorization);
314
- const saved = this.store.writeMilestone({
341
+ const root = this.store.milestoneRoot(identity.projectId, milestoneId);
342
+ const stagedPlan = this.store.stageArtifact(root, 'plan.json', JSON.stringify({
343
+ outcome: normalized.outcome,
344
+ successSignal: normalized.successSignal,
345
+ acceptance: normalized.acceptance,
346
+ memberships: normalized.memberships,
347
+ checks: normalized.checks,
348
+ }, null, 2));
349
+ const candidate = {
315
350
  ...milestone,
316
351
  status: 'awaiting_execution_authorization',
317
352
  outcome: normalized.outcome,
@@ -327,13 +362,113 @@ export class WorkflowService {
327
362
  evidenceHash: null,
328
363
  acceptedHead: null,
329
364
  cancellationReason: null,
330
- }, expectedRevision);
365
+ };
366
+ assertMilestoneMembershipIntegrity(candidate, (taskId) => this.store.readTask(identity.projectId, taskId), {}, () => this.store.listTasks(identity.projectId));
367
+ const saved = this.store.writeMilestone(candidate, expectedRevision);
331
368
  this.store.publishArtifact(stagedPlan);
332
369
  return saved;
333
370
  }
371
+ prepareMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor) {
372
+ const { identity } = this.context(repository);
373
+ const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
374
+ assertExpectedRevision(milestone, expectedRevision);
375
+ assertMilestoneScopeChangeStatusAllowed(milestone);
376
+ const prepared = this.prepareMilestoneScopeChangeCandidate(identity.projectId, milestone, plan, actor);
377
+ return {
378
+ milestoneId: prepared.milestoneId,
379
+ revision: prepared.expectedRevision,
380
+ actor: prepared.actor,
381
+ candidatePlanHash: prepared.candidatePlanHash,
382
+ candidateMembershipRevision: prepared.candidateMembershipRevision,
383
+ confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
384
+ confirmationCode: prepared.confirmationCode,
385
+ after: prepared.after,
386
+ };
387
+ }
388
+ applyMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor, confirmationCode) {
389
+ const { identity } = this.context(repository);
390
+ const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
391
+ assertExpectedRevision(milestone, expectedRevision);
392
+ assertMilestoneScopeChangeStatusAllowed(milestone);
393
+ const prepared = this.prepareMilestoneScopeChangeCandidate(identity.projectId, milestone, plan, actor);
394
+ if (confirmationCode.trim() !== prepared.confirmationCode) {
395
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone scope-change confirmation does not match the current Milestone state and requested scope.', {
396
+ milestoneId,
397
+ revision: milestone.revision,
398
+ actor: prepared.actor,
399
+ confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
400
+ });
401
+ }
402
+ return applyMilestoneScopeChangeTransaction({
403
+ store: this.store,
404
+ milestone,
405
+ expectedRevision,
406
+ prepared,
407
+ now: this.now(),
408
+ readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
409
+ });
410
+ }
411
+ prepareMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt) {
412
+ const { identity } = this.context(repository);
413
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
414
+ assertExpectedRevision(milestone, expectedRevision);
415
+ return prepareMilestoneAutonomyContract({
416
+ projectId: identity.projectId,
417
+ milestone,
418
+ principal,
419
+ delegate,
420
+ expiresAt,
421
+ now: this.now(),
422
+ });
423
+ }
424
+ grantMilestoneAutonomy(repository, milestoneId, expectedRevision, principal, delegate, expiresAt, confirmationCode) {
425
+ const { identity } = this.context(repository);
426
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
427
+ assertExpectedRevision(milestone, expectedRevision);
428
+ return applyMilestoneAutonomyContract({
429
+ store: this.store,
430
+ projectId: identity.projectId,
431
+ milestone,
432
+ principal,
433
+ delegate,
434
+ expiresAt,
435
+ confirmationCode,
436
+ now: this.now(),
437
+ });
438
+ }
439
+ evolveMilestonePlanAutonomously(repository, milestoneId, expectedRevision, plan, actor) {
440
+ const { identity } = this.context(repository);
441
+ const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
442
+ assertExpectedRevision(milestone, expectedRevision);
443
+ assertMilestoneScopeChangeStatusAllowed(milestone);
444
+ const autonomy = requireActiveMilestoneAutonomy({
445
+ store: this.store,
446
+ projectId: identity.projectId,
447
+ milestone,
448
+ actor,
449
+ now: this.now(),
450
+ });
451
+ validateMilestonePlan(plan);
452
+ assertAutonomousPlanEvolution(milestone, plan, autonomy.contract);
453
+ const prepared = prepareMilestoneScopeChangeCandidate({
454
+ milestone,
455
+ plan,
456
+ readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
457
+ actor,
458
+ autonomyContractEventHash: autonomy.contract.eventHash,
459
+ });
460
+ return applyMilestoneScopeChangeTransaction({
461
+ store: this.store,
462
+ milestone,
463
+ expectedRevision,
464
+ prepared,
465
+ now: this.now(),
466
+ readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
467
+ });
468
+ }
334
469
  authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
335
470
  const { identity } = this.context(repository);
336
- const milestone = this.store.readMilestone(identity.projectId, milestoneId);
471
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
337
472
  assertExpectedRevision(milestone, expectedRevision);
338
473
  if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
339
474
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
@@ -362,7 +497,7 @@ export class WorkflowService {
362
497
  }
363
498
  validateMilestone(repository, milestoneId, expectedRevision) {
364
499
  const { identity } = this.context(repository);
365
- const milestone = this.store.readMilestone(identity.projectId, milestoneId);
500
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
366
501
  assertExpectedRevision(milestone, expectedRevision);
367
502
  if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
368
503
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} cannot be validated while ${milestone.status}.`);
@@ -425,7 +560,7 @@ export class WorkflowService {
425
560
  }
426
561
  acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode, delegationGrantId = null) {
427
562
  const { identity } = this.context(repository);
428
- const milestone = this.store.readMilestone(identity.projectId, milestoneId);
563
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
429
564
  assertExpectedRevision(milestone, expectedRevision);
430
565
  if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
431
566
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not ready for acceptance.`);
@@ -480,23 +615,93 @@ export class WorkflowService {
480
615
  if (!reason.trim())
481
616
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone cancellation requires a reason.');
482
617
  const { identity } = this.context(repository);
483
- const milestone = this.store.readMilestone(identity.projectId, milestoneId);
618
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
484
619
  assertExpectedRevision(milestone, expectedRevision);
485
620
  if (['accepted', 'cancelled'].includes(milestone.status)) {
486
621
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
487
622
  }
488
623
  return this.store.writeMilestone({ ...milestone, status: 'cancelled', cancellationReason: reason.trim() }, expectedRevision);
489
624
  }
490
- setTaskPlan(repository, taskId, expectedRevision, plan) {
625
+ setTaskPlan(repository, taskId, expectedRevision, plan, correctiveAudit = null, planRiskAudit = null) {
491
626
  validatePlan(plan);
492
627
  const { identity } = this.context(repository);
493
628
  const knowledgeMap = this.assertKnowledgeMapBinding(identity.repositoryRoot, identity.projectId, plan.knowledgeMapRevision, plan.knowledgeMapHash);
494
629
  this.validateKnowledgeTargets(knowledgeMap, plan);
495
630
  const task = this.store.readTask(identity.projectId, taskId);
496
631
  assertExpectedRevision(task, expectedRevision);
497
- if (!['planning', 'awaiting_execution_authorization', 'needs_fix'].includes(task.status)) {
632
+ if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
498
633
  throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
499
634
  }
635
+ const currentPlanCorrectiveDecisions = task.planHash
636
+ ? readCorrectiveDecisionEvents(this.store, task).filter((event) => event.planHash === task.planHash)
637
+ : [];
638
+ const currentReplanDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'replan-required');
639
+ const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'continue-fix');
640
+ const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required' || event.decision === 'stop-escalate');
641
+ const currentReplanRequiredStepIds = [...new Set(currentReplanDecisions.map((event) => event.stepId))];
642
+ if (currentHardBlockingDecisions.length > 0) {
643
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has current corrective decisions that prohibit normal plan updates.`, {
644
+ taskId: task.id,
645
+ requiredAction: 'Escalate or split the work according to the recorded corrective decision.',
646
+ decisions: currentHardBlockingDecisions.map((event) => ({
647
+ eventId: event.eventId,
648
+ stepId: event.stepId,
649
+ decision: event.decision,
650
+ })),
651
+ });
652
+ }
653
+ if (task.status === 'blocked' && currentReplanDecisions.length === 0) {
654
+ throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task ${task.id} is blocked unless the current Plan already carries a replan-required corrective decision.`, {
655
+ taskId: task.id,
656
+ status: task.status,
657
+ requiredAction: 'Resolve the blocked strict review or record a replan-required corrective decision first.',
658
+ });
659
+ }
660
+ if (currentContinueFixDecisions.length > 0) {
661
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} already has a current continue-fix corrective decision; use the current Plan retry path instead of issuing another plan-set.`, {
662
+ taskId: task.id,
663
+ requiredAction: 'Run the guarded Step under the current Plan retry path, or escalate/cancel instead of issuing another plan-set at the same ordinal.',
664
+ decisions: currentContinueFixDecisions.map((event) => ({
665
+ eventId: event.eventId,
666
+ stepId: event.stepId,
667
+ triggeringAttemptCount: event.triggeringAttemptCount,
668
+ })),
669
+ });
670
+ }
671
+ const currentPlanRiskAudit = task.status === 'needs_fix'
672
+ ? readCurrentPlanRiskAudit(this.store, task)
673
+ : null;
674
+ const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
675
+ if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
676
+ validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
677
+ }
678
+ const guardedRemediationGate = task.status === 'needs_fix' && currentPlanRiskAudit
679
+ ? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
680
+ : null;
681
+ if (guardedRemediationGate && currentReplanDecisions.length === 0 && !correctiveAudit) {
682
+ throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a corrective decision before another remediation run can be authorized.`, {
683
+ taskId: task.id,
684
+ stepId: guardedRemediationGate.stepId,
685
+ attemptCount: guardedRemediationGate.attemptCount,
686
+ requiredSidecar: 'corrective-decisions.jsonl',
687
+ allowedDecision: 'continue-fix',
688
+ escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
689
+ });
690
+ }
691
+ if (currentReplanDecisions.length > 0 && correctiveAudit) {
692
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} already has a current replan-required corrective decision; submit the replacement Plan without recording another decision in plan-set.`, {
693
+ taskId: task.id,
694
+ decision: correctiveAudit.decision,
695
+ requiredAction: 'Retry task plan-set without corrective-audit input; the recorded replan-required decision is the authorization for this recovery path.',
696
+ });
697
+ }
698
+ if (guardedRemediationGate && currentReplanDecisions.length === 0 && correctiveAudit?.decision !== 'continue-fix') {
699
+ throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a separate corrective-decision record before execution may continue; plan-set only supports continue-fix.`, {
700
+ taskId: task.id,
701
+ stepId: guardedRemediationGate.stepId,
702
+ decision: correctiveAudit?.decision ?? null,
703
+ });
704
+ }
500
705
  for (const requirement of task.requirements) {
501
706
  if (!plan.requirements.includes(requirement)) {
502
707
  throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task requirement ${requirement}.`);
@@ -525,7 +730,24 @@ export class WorkflowService {
525
730
  plannedSteps.every((step) => step.status === 'completed')) {
526
731
  throw new WorkflowError('TRANSITION_BLOCKED', 'A failed review requires at least one new or changed remediation Step.');
527
732
  }
528
- const saved = this.store.writeTask({
733
+ if (currentReplanRequiredStepIds.some((stepId) => !plannedSteps.some((step) => step.id === stepId))) {
734
+ throw new WorkflowError('TRANSITION_BLOCKED', `Replacement Plan under replan-required must retain every guarded Step targeted by the current corrective decision; removing or renaming one would reset remediation history.`, {
735
+ taskId: task.id,
736
+ requiredStepIds: currentReplanRequiredStepIds,
737
+ plannedStepIds: plannedSteps.map((step) => step.id),
738
+ });
739
+ }
740
+ if (guardedRemediationGate && currentReplanDecisions.length === 0 && !plannedSteps.some((step) => step.id === guardedRemediationGate.stepId)) {
741
+ throw new WorkflowError('TRANSITION_BLOCKED', `Continue-fix must retain guarded Step ${guardedRemediationGate.stepId}; removing or renaming it would reset the remediation breaker.`, {
742
+ taskId: task.id,
743
+ stepId: guardedRemediationGate.stepId,
744
+ });
745
+ }
746
+ if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
747
+ assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
748
+ appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, stagedPlan.hash, this.now());
749
+ }
750
+ const candidate = {
529
751
  ...task,
530
752
  status: 'awaiting_execution_authorization',
531
753
  planHash: stagedPlan.hash,
@@ -545,8 +767,16 @@ export class WorkflowService {
545
767
  resultHash: null,
546
768
  evidenceHash: null,
547
769
  blockReason: null,
548
- }, expectedRevision);
770
+ };
771
+ if (planRiskAudit)
772
+ validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
773
+ const saved = this.store.writeTask(candidate, expectedRevision);
549
774
  this.store.publishArtifact(stagedPlan);
775
+ if (planRiskAudit)
776
+ appendCurrentPlanRiskAudit(this.store, saved, planRiskAudit, this.now());
777
+ if (correctiveAudit && task.status === 'needs_fix' && task.review.status === 'failed') {
778
+ appendCorrectivePlanAudit(this.store.taskRoot(identity.projectId, taskId), task, saved, failedReviewAttempts, correctiveAudit, this.now());
779
+ }
550
780
  return saved;
551
781
  }
552
782
  rebindTaskKnowledge(repository, taskId, expectedRevision) {
@@ -630,8 +860,62 @@ export class WorkflowService {
630
860
  this.store.publishArtifact(stagedPlan);
631
861
  return saved;
632
862
  }
863
+ refreshTaskContext(repository, taskId, expectedTaskRevision, expectedKnowledgeMapRevision, actor, delegationGrantId) {
864
+ if (!delegationGrantId) {
865
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task context refresh combines approval transitions and therefore requires an explicit delegated approval grant.');
866
+ }
867
+ const { identity } = this.context(repository);
868
+ const task = this.store.readTask(identity.projectId, taskId);
869
+ const currentMap = this.store.readKnowledgeMap(identity.projectId);
870
+ assertExpectedRevision(task, expectedTaskRevision);
871
+ assertExpectedRevision(currentMap, expectedKnowledgeMapRevision);
872
+ if (!taskCanRebindKnowledge(task)) {
873
+ throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while Task is ${task.status}.`);
874
+ }
875
+ const activeStep = task.steps.find((step) => step.status === 'in_progress');
876
+ if (activeStep) {
877
+ throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while ${activeStep.id} is in progress.`);
878
+ }
879
+ const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
880
+ const transitions = [];
881
+ const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
882
+ // Validate every delegated transition before the first write so a narrow grant cannot
883
+ // leave the composite operation half-applied.
884
+ this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
885
+ let knowledgeMap = currentMap;
886
+ if (inspectedMap.status !== 'active' || !inspectedMap.approval) {
887
+ if (currentMap.status !== 'active' || !currentMap.approval || inspectedMap.status !== 'stale') {
888
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with content-only drift.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
889
+ }
890
+ assertContentOnlyKnowledgeRefresh(currentMap, inspectedMap);
891
+ this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspectedMap, task.id);
892
+ knowledgeMap = this.reconcileKnowledgeMap(repository, currentMap.revision);
893
+ transitions.push('project-memory reconcile');
894
+ knowledgeMap = this.approveKnowledgeMapForTaskRefresh(repository, knowledgeMap.revision, [], actor, delegationGrantId, task.id);
895
+ transitions.push('project-memory approve');
896
+ }
897
+ const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
898
+ if (task.knowledgeMapRevision === knowledgeMap.revision
899
+ && task.knowledgeMapHash === knowledgeMapHash) {
900
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
901
+ }
902
+ let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
903
+ if (currentPlanRiskAudit) {
904
+ appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
905
+ }
906
+ transitions.push('task knowledge-rebind');
907
+ refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, 'Delegated content-only context refresh.', delegationGrantId);
908
+ transitions.push('task execution authorize');
909
+ return {
910
+ mode: 'delegated-content-only',
911
+ transitions,
912
+ knowledgeMap,
913
+ task: refreshedTask,
914
+ };
915
+ }
633
916
  authorizeTask(repository, taskId, expectedRevision, actor, reason = '', delegationGrantId = null) {
634
917
  const { identity } = this.context(repository);
918
+ this.readCurrentAdoptionPostureStrict(identity.projectId);
635
919
  const task = this.store.readTask(identity.projectId, taskId);
636
920
  assertExpectedRevision(task, expectedRevision);
637
921
  if (task.status !== 'awaiting_execution_authorization' || !task.planHash) {
@@ -641,6 +925,7 @@ export class WorkflowService {
641
925
  const root = this.store.taskRoot(identity.projectId, taskId);
642
926
  const briefHash = this.store.hashArtifact(root, 'brief.md');
643
927
  const planHash = this.store.hashArtifact(root, 'plan.md');
928
+ readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: true });
644
929
  const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
645
930
  const event = {
646
931
  kind: 'execution',
@@ -658,15 +943,164 @@ export class WorkflowService {
658
943
  };
659
944
  return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
660
945
  }
946
+ recordPlanRiskAudit(repository, taskId, expectedRevision, input) {
947
+ const { identity } = this.context(repository);
948
+ this.readCurrentAdoptionPostureStrict(identity.projectId);
949
+ const task = this.store.readTask(identity.projectId, taskId);
950
+ assertExpectedRevision(task, expectedRevision);
951
+ this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
952
+ return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
953
+ }
954
+ showTaskHandoff(repository, taskId) {
955
+ const { identity } = this.context(repository);
956
+ const task = this.store.readTask(identity.projectId, taskId);
957
+ return {
958
+ task,
959
+ posture: readTaskC1Posture(this.store, task),
960
+ claimTokenAvailable: false,
961
+ };
962
+ }
963
+ handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null) {
964
+ const context = this.context(repository);
965
+ const task = this.store.readTask(context.identity.projectId, taskId);
966
+ assertExpectedRevision(task, expectedRevision);
967
+ const normalizedActor = actor.trim();
968
+ if (!normalizedActor) {
969
+ throw new WorkflowError('INVALID_ARGUMENT', 'Handoff actor is required.');
970
+ }
971
+ if (grantId)
972
+ this.assertHandoffGrant(context.identity.projectId, task, targetActor, grantId);
973
+ const milestone = task.milestoneId ? this.readMilestoneCurrent(task.projectId, task.milestoneId) : null;
974
+ const milestones = milestone ? this.listMilestonesCurrent(task.projectId) : [];
975
+ const milestoneDisplayNumber = milestone
976
+ ? milestones.findIndex((candidate) => candidate.id === milestone.id) + 1
977
+ : null;
978
+ const taskDisplayNumber = milestone
979
+ ? milestone.memberships.findIndex((membership) => membership.taskId === task.id) + 1
980
+ : null;
981
+ const currentLease = context.locks.inspect(taskId);
982
+ let lease;
983
+ let acquiredHere = false;
984
+ if (currentLease) {
985
+ if (currentLease.owner !== normalizedActor) {
986
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} active writer lease belongs to a different actor.`, {
987
+ taskId,
988
+ leaseOwner: currentLease.owner,
989
+ actor: normalizedActor,
990
+ });
991
+ }
992
+ if (writerToken === null) {
993
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} handoff requires the active writer token while a lease is present.`, {
994
+ taskId,
995
+ leaseOwner: currentLease.owner,
996
+ expiresAt: currentLease.expiresAt,
997
+ });
998
+ }
999
+ lease = context.locks.heartbeat(taskId, writerToken);
1000
+ }
1001
+ else {
1002
+ if (writerToken !== null) {
1003
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
1004
+ taskId,
1005
+ });
1006
+ }
1007
+ acquiredHere = true;
1008
+ lease = context.locks.acquire(taskId, normalizedActor);
1009
+ }
1010
+ const releaseLease = () => context.locks.release(taskId, lease.token);
1011
+ try {
1012
+ const prepared = appendTaskHandoff(this.store, task, {
1013
+ actor: normalizedActor,
1014
+ targetActor,
1015
+ reason,
1016
+ writerToken: lease.token,
1017
+ milestoneDisplayNumber,
1018
+ taskDisplayNumber,
1019
+ expectedNext: String(nextForTask(task).action ?? 'task show'),
1020
+ grantId,
1021
+ expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
1022
+ }, this.now());
1023
+ releaseLease();
1024
+ return { task, event: prepared.event, bundle: prepared.bundle };
1025
+ }
1026
+ catch (error) {
1027
+ if (acquiredHere)
1028
+ releaseLease();
1029
+ throw error;
1030
+ }
1031
+ }
1032
+ claimTask(repository, taskId, expectedRevision, actor, claimToken, writerToken = null) {
1033
+ const context = this.context(repository);
1034
+ const task = this.store.readTask(context.identity.projectId, taskId);
1035
+ assertExpectedRevision(task, expectedRevision);
1036
+ const normalizedActor = actor.trim();
1037
+ if (!normalizedActor) {
1038
+ throw new WorkflowError('INVALID_ARGUMENT', 'Claim actor is required.');
1039
+ }
1040
+ const pendingHandoff = readTaskC1Posture(this.store, task);
1041
+ if (pendingHandoff.state === 'pending' && pendingHandoff.latestEvent.grantId) {
1042
+ this.assertHandoffGrant(context.identity.projectId, task, normalizedActor, pendingHandoff.latestEvent.grantId);
1043
+ }
1044
+ const existingLease = context.locks.inspect(taskId);
1045
+ let lease;
1046
+ let acquiredHere = false;
1047
+ if (existingLease) {
1048
+ if (existingLease.owner !== normalizedActor) {
1049
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} existing writer lease belongs to a different actor.`, {
1050
+ taskId,
1051
+ leaseOwner: existingLease.owner,
1052
+ actor: normalizedActor,
1053
+ });
1054
+ }
1055
+ if (writerToken === null) {
1056
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} claim requires the existing writer token.`, {
1057
+ taskId,
1058
+ leaseOwner: existingLease.owner,
1059
+ });
1060
+ }
1061
+ lease = context.locks.heartbeat(taskId, writerToken);
1062
+ }
1063
+ else {
1064
+ if (writerToken !== null) {
1065
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
1066
+ taskId,
1067
+ });
1068
+ }
1069
+ acquiredHere = true;
1070
+ lease = context.locks.acquire(taskId, normalizedActor);
1071
+ }
1072
+ try {
1073
+ const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now());
1074
+ return { task, event, lease };
1075
+ }
1076
+ catch (error) {
1077
+ if (acquiredHere)
1078
+ context.locks.release(taskId, lease.token);
1079
+ throw error;
1080
+ }
1081
+ }
1082
+ handbackTask(repository, taskId, expectedRevision, actor, reason, writerToken = null, limitations = [], recommendedNext = 'Coordinator reviews the terminal Task result and recorded evidence.') {
1083
+ const context = this.context(repository);
1084
+ const task = this.store.readTask(context.identity.projectId, taskId);
1085
+ assertExpectedRevision(task, expectedRevision);
1086
+ const lease = task.status === 'merged' || task.status === 'cancelled'
1087
+ ? context.locks.inspect(taskId)
1088
+ : this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
1089
+ const event = appendTaskHandback(this.store, task, actor, reason, lease?.token ?? writerToken, limitations, recommendedNext, this.now());
1090
+ if (lease)
1091
+ context.locks.release(taskId, lease.token);
1092
+ return { task, event };
1093
+ }
661
1094
  startTask(repository, taskId, expectedRevision, workspaceOwner) {
662
1095
  const { identity } = this.context(repository);
663
1096
  const task = this.store.readTask(identity.projectId, taskId);
664
1097
  assertExpectedRevision(task, expectedRevision);
1098
+ this.requireExecutionAdoptionPosture(identity, taskId);
665
1099
  if (task.status !== 'ready')
666
1100
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is ${task.status}.`);
667
1101
  this.assertExecutionAuthorizationFresh(task);
668
1102
  if (task.milestoneId) {
669
- const milestone = this.store.readMilestone(identity.projectId, task.milestoneId);
1103
+ const milestone = this.readMilestoneCurrent(identity.projectId, task.milestoneId);
670
1104
  const membership = milestone.memberships.find((candidate) => candidate.taskId === taskId);
671
1105
  if (milestone.status !== 'active' || membership?.disposition !== 'required') {
672
1106
  throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task requires an active, authorized membership.', {
@@ -675,6 +1109,15 @@ export class WorkflowService {
675
1109
  disposition: membership?.disposition ?? null,
676
1110
  });
677
1111
  }
1112
+ const handoffPosture = readTaskC1Posture(this.store, task);
1113
+ if (handoffPosture.state !== 'claimed') {
1114
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a prepared and claimed C1 handoff bundle.', {
1115
+ taskId: task.id,
1116
+ milestoneId: milestone.id,
1117
+ c1Posture: handoffPosture.state,
1118
+ requiredAction: handoffPosture.state === 'pending' ? 'Claim the prepared handoff.' : 'Prepare and claim a C1 handoff.',
1119
+ });
1120
+ }
678
1121
  }
679
1122
  if (!isClean(identity.repositoryRoot)) {
680
1123
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start requires a clean checkout.', {
@@ -700,20 +1143,48 @@ export class WorkflowService {
700
1143
  }
701
1144
  return this.store.writeTask({ ...task, baseCommit, taskBranch, workspaceOwner, blockReason: null }, expectedRevision);
702
1145
  }
703
- runStep(repository, taskId, stepId, expectedRevision, owner, writerToken) {
1146
+ runStep(repository, taskId, stepId, expectedRevision, actor, writerToken) {
704
1147
  const context = this.context(repository);
705
1148
  const task = this.store.readTask(context.identity.projectId, taskId);
706
1149
  assertExpectedRevision(task, expectedRevision);
1150
+ this.requireExecutionAdoptionPosture(context.identity, taskId);
707
1151
  if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
708
1152
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run a Step while ${task.status}.`);
709
1153
  }
710
1154
  this.assertExecutionAuthorizationFresh(task);
711
1155
  const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1156
+ const currentPlanBlockingDecisions = readCorrectiveDecisionEvents(this.store, task)
1157
+ .filter((event) => event.planHash === task.planHash && event.decision !== 'continue-fix');
1158
+ if (currentPlanBlockingDecisions.length > 0) {
1159
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run any Step while the current Plan has active blocking corrective decisions.`, {
1160
+ taskId,
1161
+ decisions: currentPlanBlockingDecisions.map((event) => ({
1162
+ eventId: event.eventId,
1163
+ stepId: event.stepId,
1164
+ decision: event.decision,
1165
+ planHash: event.planHash,
1166
+ triggeringAttemptCount: event.triggeringAttemptCount,
1167
+ })),
1168
+ });
1169
+ }
712
1170
  assertTaskBranch(context.identity.repositoryRoot, task);
713
1171
  const step = requireStep(task, stepId);
1172
+ const remediationHistoryForStep = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
714
1173
  if (!['planned', 'failed'].includes(step.status)) {
715
1174
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
716
1175
  }
1176
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
1177
+ if (remediationHistoryForStep.length > 0 && (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds))) {
1178
+ throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} has remediation history but is no longer marked for guarded review by the current Plan Risk Audit; execution is blocked until the audit and Step posture are reconciled.`, {
1179
+ taskId,
1180
+ stepId,
1181
+ remediationAttemptCount: remediationHistoryForStep.length,
1182
+ currentAuditPresent: Boolean(planRiskAudit),
1183
+ });
1184
+ }
1185
+ if (planRiskAudit && isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
1186
+ assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
1187
+ }
717
1188
  const dirty = changedFiles(context.identity.repositoryRoot);
718
1189
  if (step.status === 'planned' && dirty.length > 0) {
719
1190
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
@@ -732,18 +1203,41 @@ export class WorkflowService {
732
1203
  incompleteDependencies,
733
1204
  });
734
1205
  }
1206
+ const handoffPosture = readTaskC1Posture(this.store, task);
1207
+ if (handoffPosture.state === 'pending') {
1208
+ this.assertTaskMutationAllowedByHandoff(task, actor, null);
1209
+ }
735
1210
  const existing = context.locks.inspect(taskId);
736
1211
  let acquiredHere = false;
737
- const lease = existing
738
- ? writerToken
739
- ? context.locks.heartbeat(taskId, writerToken)
1212
+ let lease;
1213
+ if (handoffPosture.state === 'claimed') {
1214
+ if (!existing) {
1215
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} claimed C1 posture requires an active writer lease before task run.`, {
1216
+ taskId,
1217
+ claimant: handoffPosture.claimant,
1218
+ });
1219
+ }
1220
+ if (!writerToken) {
1221
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} claimed C1 posture requires the bound writer token for task run.`, {
1222
+ taskId,
1223
+ claimant: handoffPosture.claimant,
1224
+ });
1225
+ }
1226
+ lease = context.locks.heartbeat(taskId, writerToken);
1227
+ this.assertTaskMutationAllowedByHandoff(task, actor, lease.token);
1228
+ }
1229
+ else {
1230
+ lease = existing
1231
+ ? writerToken
1232
+ ? context.locks.heartbeat(taskId, writerToken)
1233
+ : (() => {
1234
+ throw new WorkflowError('LOCKED', `${taskId} already has a writer.`, { owner: existing.owner });
1235
+ })()
740
1236
  : (() => {
741
- throw new WorkflowError('LOCKED', `${taskId} already has a writer.`, { owner: existing.owner });
742
- })()
743
- : (() => {
744
- acquiredHere = true;
745
- return context.locks.acquire(taskId, owner.trim() || 'worker');
746
- })();
1237
+ acquiredHere = true;
1238
+ return context.locks.acquire(taskId, actor.trim() || 'worker');
1239
+ })();
1240
+ }
747
1241
  const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'in_progress' } : candidate);
748
1242
  let saved;
749
1243
  try {
@@ -761,7 +1255,7 @@ export class WorkflowService {
761
1255
  protocolVersion: PROTOCOL_VERSION,
762
1256
  packageVersion: PACKAGE_VERSION,
763
1257
  role: 'worker',
764
- objective: step.objective,
1258
+ objective: `${step.objective} Leave all Step changes uncommitted; Workflow core creates the atomic commit through task step-complete.`,
765
1259
  inputs: [
766
1260
  path.join(this.store.taskRoot(task.projectId, task.id), 'brief.md'),
767
1261
  path.join(this.store.taskRoot(task.projectId, task.id), 'plan.md'),
@@ -769,7 +1263,10 @@ export class WorkflowService {
769
1263
  allowedReads: [identityRelative(context.identity.repositoryRoot, '.')],
770
1264
  allowedWrites: step.allowedWrites,
771
1265
  commands: step.checks,
772
- forbiddenScope: step.forbiddenScope,
1266
+ forbiddenScope: [
1267
+ ...step.forbiddenScope,
1268
+ 'Manual Git staging, commits, amendments, resets, rebases, or other history mutations. Workflow core owns the Step commit.',
1269
+ ],
773
1270
  resultSchema: {
774
1271
  type: 'object',
775
1272
  required: ['summary', 'changedFiles', 'checks'],
@@ -783,6 +1280,7 @@ export class WorkflowService {
783
1280
  'Required change falls outside allowedWrites.',
784
1281
  'Implementation requires changing Brief, Plan, or acceptance.',
785
1282
  'A configured check cannot be run or interpreted safely.',
1283
+ 'Do not run git add or git commit. Return uncommitted changes to the coordinator, which must invoke task step-complete.',
786
1284
  ],
787
1285
  contextBudget: 'medium',
788
1286
  taskId,
@@ -797,10 +1295,22 @@ export class WorkflowService {
797
1295
  taskContext(repository, taskId, role) {
798
1296
  const { identity } = this.context(repository);
799
1297
  const task = this.store.readTask(identity.projectId, taskId);
1298
+ const historicalStrictReview = role === 'independent-reviewer'
1299
+ && task.steps.some((step) => step.status === 'in_progress' && step.evidence !== null);
800
1300
  const knowledgeMap = task.knowledgeMapRevision === null
801
1301
  ? this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId)
802
- : this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
1302
+ : historicalStrictReview
1303
+ ? this.strictReviewKnowledgeMap(identity.repositoryRoot, task)
1304
+ : this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
803
1305
  const root = this.store.taskRoot(task.projectId, task.id);
1306
+ const planRiskAudit = task.planHash && !['planning', 'merged', 'cancelled'].includes(task.status)
1307
+ ? readCurrentPlanRiskAudit(this.store, task)
1308
+ : null;
1309
+ const planRiskReviewScope = planRiskAudit
1310
+ ? planRiskAudit.reviewRequiredStepIds.length > 0
1311
+ ? ` Current Plan Risk Audit marks ${planRiskAudit.reviewRequiredStepIds.join(', ')} for strict review.`
1312
+ : ' Current Plan Risk Audit marks no Steps for strict review.'
1313
+ : '';
804
1314
  const common = {
805
1315
  protocolVersion: PROTOCOL_VERSION,
806
1316
  packageVersion: PACKAGE_VERSION,
@@ -822,17 +1332,26 @@ export class WorkflowService {
822
1332
  if (role === 'technical-planner') {
823
1333
  return {
824
1334
  ...common,
825
- objective: `Produce an execution-ready Plan for ${task.title}.`,
1335
+ objective: `Produce an execution-ready Plan for ${task.title}. Perform an explicit risk review before plan-set: migrations, concurrency, restart/replay, crash windows, and external-provider behavior require failure-specific checks and executable proof obligations in the original Plan.${planRiskReviewScope}`,
826
1336
  resultSchema: {
827
1337
  type: 'object',
828
1338
  required: ['objective', 'requirements', 'acceptance', 'risks', 'steps'],
829
1339
  },
1340
+ stopConditions: [
1341
+ ...common.stopConditions,
1342
+ 'A migration is planned without a populated forward-upgrade check.',
1343
+ 'Concurrency or cursor/state ownership is planned without deterministic race checks.',
1344
+ 'Restart, replay, or crash recovery is claimed without restart/crash-window evidence.',
1345
+ 'An external provider is used without failure-injection and hidden-default verification.',
1346
+ 'A guarded failure mode lacks a hostile counterexample, expected safe result, or exact executable evidence command.',
1347
+ 'The safe risk slice crosses persistence, recovery orchestration, and executable composition and should be split.',
1348
+ ],
830
1349
  };
831
1350
  }
832
1351
  if (role === 'independent-reviewer') {
833
1352
  return {
834
1353
  ...common,
835
- objective: `Review ${task.title} against Brief, Plan, diff, checks, and evidence without modifying files.`,
1354
+ objective: `Review ${task.title} against Brief, Plan, diff, checks, and evidence without modifying files.${planRiskReviewScope}`,
836
1355
  inputs: [...common.inputs, ...(task.evidenceHash ? [path.join(root, 'evidence.json')] : [])],
837
1356
  resultSchema: {
838
1357
  type: 'object',
@@ -842,28 +1361,65 @@ export class WorkflowService {
842
1361
  ...common.stopConditions,
843
1362
  'The checkout is dirty or the evidence does not identify the reviewed commit.',
844
1363
  'Read-only reviewer isolation cannot be established; return unverified.',
1364
+ 'High-risk behavior lacks populated-upgrade, race, restart/replay, crash-window, or provider-failure evidence required by the Plan.',
845
1365
  ],
846
1366
  };
847
1367
  }
848
1368
  return {
849
1369
  ...common,
850
- objective: `Coordinate the next valid state transition for ${task.title}.`,
1370
+ objective: `Coordinate the next valid state transition for ${task.title}.${planRiskReviewScope}`,
851
1371
  resultSchema: { type: 'object', required: ['recommendedAction', 'reason'] },
852
1372
  };
853
1373
  }
854
- completeStep(repository, taskId, stepId, expectedRevision, writerToken, notes) {
1374
+ completeStep(repository, taskId, stepId, expectedRevision, writerToken, notes, actor) {
855
1375
  const context = this.context(repository);
856
- context.locks.heartbeat(taskId, writerToken);
1376
+ const lease = context.locks.heartbeat(taskId, writerToken);
1377
+ this.readCurrentAdoptionPostureStrict(context.identity.projectId);
857
1378
  const task = this.store.readTask(context.identity.projectId, taskId);
858
1379
  assertExpectedRevision(task, expectedRevision);
1380
+ this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
859
1381
  assertTaskBranch(context.identity.repositoryRoot, task);
860
1382
  const step = requireStep(task, stepId);
861
1383
  if (task.status !== 'in_progress' || step.status !== 'in_progress') {
862
1384
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} is not in progress.`);
863
1385
  }
1386
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
1387
+ const reviewRequired = planRiskAudit
1388
+ ? isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)
1389
+ : false;
1390
+ if (reviewRequired && step.evidence) {
1391
+ validateTaskHistory(context.identity.repositoryRoot, task);
1392
+ if (!isClean(context.identity.repositoryRoot)) {
1393
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review recovery requires a clean Task checkout.', {
1394
+ changedFiles: changedFiles(context.identity.repositoryRoot),
1395
+ });
1396
+ }
1397
+ const currentHead = headCommit(context.identity.repositoryRoot);
1398
+ if (currentHead !== step.evidence.commitSha) {
1399
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review recovery requires HEAD to remain at the recorded completion commit.', {
1400
+ stepId,
1401
+ expectedCommit: step.evidence.commitSha,
1402
+ actualCommit: currentHead,
1403
+ });
1404
+ }
1405
+ const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
1406
+ if (!currentCycle.pendingEvent && !currentCycle.latestMatchingEvent) {
1407
+ ensurePendingStrictStepReview(this.store, task, stepId, this.now());
1408
+ }
1409
+ return task;
1410
+ }
1411
+ validateTaskHistory(context.identity.repositoryRoot, task);
864
1412
  const checks = runChecks(context.identity.repositoryRoot, step.checks);
865
1413
  const failed = checks.filter((check) => !check.passed);
866
1414
  if (failed.length > 0) {
1415
+ if (reviewRequired && planRiskAudit) {
1416
+ appendRemediationEvent(this.store, task, stepId, 'checks-failed', {
1417
+ completionCommit: headCommit(context.identity.repositoryRoot),
1418
+ checksHash: sha256Hex(canonicalJsonStringify(checks)),
1419
+ reviewEventHash: null,
1420
+ reviewerAttestationHash: null,
1421
+ }, planRiskAudit, this.now());
1422
+ }
867
1423
  const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'failed' } : candidate);
868
1424
  return this.store.writeTask({ ...task, status: 'needs_fix', steps, blockReason: `${stepId} checks failed.` }, expectedRevision);
869
1425
  }
@@ -875,6 +1431,18 @@ export class WorkflowService {
875
1431
  checks,
876
1432
  notes,
877
1433
  };
1434
+ if (reviewRequired) {
1435
+ const saved = this.store.writeTask({
1436
+ ...task,
1437
+ status: 'in_progress',
1438
+ steps: task.steps.map((candidate) => candidate.id === stepId
1439
+ ? { ...candidate, status: 'in_progress', evidence }
1440
+ : candidate),
1441
+ blockReason: null,
1442
+ }, expectedRevision);
1443
+ ensurePendingStrictStepReview(this.store, saved, stepId, this.now());
1444
+ return saved;
1445
+ }
878
1446
  const completedIds = new Set([
879
1447
  ...task.steps.filter((candidate) => candidate.status === 'completed').map((candidate) => candidate.id),
880
1448
  stepId,
@@ -889,11 +1457,117 @@ export class WorkflowService {
889
1457
  });
890
1458
  return this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
891
1459
  }
892
- submitTask(repository, taskId, expectedRevision, writerToken) {
1460
+ reviewStep(repository, taskId, stepId, expectedRevision, runner, actor, writerToken = null) {
893
1461
  const context = this.context(repository);
894
- context.locks.heartbeat(taskId, writerToken);
1462
+ this.readCurrentAdoptionPostureStrict(context.identity.projectId);
895
1463
  const task = this.store.readTask(context.identity.projectId, taskId);
896
1464
  assertExpectedRevision(task, expectedRevision);
1465
+ this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
1466
+ this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
1467
+ if (task.status !== 'in_progress' && task.status !== 'blocked') {
1468
+ throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
1469
+ }
1470
+ assertTaskBranch(context.identity.repositoryRoot, task);
1471
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
1472
+ if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
1473
+ throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`, {
1474
+ taskId: task.id,
1475
+ stepId,
1476
+ });
1477
+ }
1478
+ const step = requireStep(task, stepId);
1479
+ if (step.status !== 'in_progress' || !step.evidence) {
1480
+ throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review requires ${stepId} to remain in progress with canonical evidence.`, {
1481
+ taskId: task.id,
1482
+ stepId,
1483
+ stepStatus: step.status,
1484
+ });
1485
+ }
1486
+ // A strict review is bound to the immutable completion commit, Task Plan, and the
1487
+ // Knowledge Map revision that authorized the Step. Project knowledge commonly becomes
1488
+ // stale because the completion commit itself changes a classified source. Requiring the
1489
+ // *current* map here creates a deadlock: review cannot run before rebind, while rebind is
1490
+ // intentionally forbidden until the in-progress Step becomes terminal. The pending
1491
+ // strict-review binding below validates the historical Task context; a current-map rebind
1492
+ // remains mandatory after the review finalizes the Step and before another Step can run.
1493
+ validateTaskHistory(context.identity.repositoryRoot, task);
1494
+ if (!isClean(context.identity.repositoryRoot)) {
1495
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review requires a clean Task checkout.', {
1496
+ changedFiles: changedFiles(context.identity.repositoryRoot),
1497
+ });
1498
+ }
1499
+ const currentHead = headCommit(context.identity.repositoryRoot);
1500
+ if (currentHead !== step.evidence.commitSha) {
1501
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review requires HEAD to remain at the recorded completion commit.', {
1502
+ taskId: task.id,
1503
+ stepId,
1504
+ expectedCommit: step.evidence.commitSha,
1505
+ actualCommit: currentHead,
1506
+ });
1507
+ }
1508
+ const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
1509
+ if (task.status === 'blocked') {
1510
+ if (currentCycle.resolution !== 'unverified') {
1511
+ throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review retry requires ${stepId} to be blocked by an unverified prior cycle.`, {
1512
+ taskId: task.id,
1513
+ stepId,
1514
+ resolution: currentCycle.resolution,
1515
+ });
1516
+ }
1517
+ assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
1518
+ }
1519
+ else if (currentCycle.resolution !== 'pending') {
1520
+ if (currentCycle.resolution === 'failed' || currentCycle.resolution === 'unverified') {
1521
+ if (!currentCycle.reviewRecordedEvent || !currentCycle.reviewerAttestationEvent) {
1522
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review recovery requires exact review and attestation sidecars.', {
1523
+ taskId: task.id,
1524
+ stepId,
1525
+ resolution: currentCycle.resolution,
1526
+ });
1527
+ }
1528
+ appendRemediationEvent(this.store, task, stepId, currentCycle.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
1529
+ completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
1530
+ checksHash: currentCycle.reviewRecordedEvent.checksHash,
1531
+ reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
1532
+ reviewerAttestationHash: currentCycle.reviewerAttestationEvent.eventHash,
1533
+ }, planRiskAudit, this.now());
1534
+ }
1535
+ return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
1536
+ }
1537
+ ensurePendingStrictStepReview(this.store, task, stepId, this.now());
1538
+ const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
1539
+ const launch = launchStrictStepReviewer(context.identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, runner);
1540
+ const recorded = recordStrictStepReview(this.store, task, stepId, {
1541
+ review: launch.review,
1542
+ reviewerOutputHash: launch.reviewerOutputHash,
1543
+ reviewedCommit: launch.reviewedCommit,
1544
+ isolationResult: launch.verifiedIsolation ? 'verified' : 'unverified',
1545
+ }, this.now());
1546
+ if (recorded.resolution === 'failed' || recorded.resolution === 'unverified') {
1547
+ const reviewRecordedEvent = recorded.reviewRecordedEvent;
1548
+ const reviewerAttestationEvent = recorded.reviewerAttestationEvent;
1549
+ if (!reviewRecordedEvent || !reviewerAttestationEvent) {
1550
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review remediation recording requires review and attestation sidecars.', {
1551
+ taskId: task.id,
1552
+ stepId,
1553
+ resolution: recorded.resolution,
1554
+ });
1555
+ }
1556
+ appendRemediationEvent(this.store, task, stepId, recorded.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
1557
+ completionCommit: reviewRecordedEvent.completionCommit,
1558
+ checksHash: reviewRecordedEvent.checksHash,
1559
+ reviewEventHash: reviewRecordedEvent.eventHash,
1560
+ reviewerAttestationHash: reviewerAttestationEvent.eventHash,
1561
+ }, planRiskAudit, this.now());
1562
+ }
1563
+ return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
1564
+ }
1565
+ submitTask(repository, taskId, expectedRevision, writerToken, actor) {
1566
+ const context = this.context(repository);
1567
+ const lease = context.locks.heartbeat(taskId, writerToken);
1568
+ const task = this.store.readTask(context.identity.projectId, taskId);
1569
+ assertExpectedRevision(task, expectedRevision);
1570
+ this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
897
1571
  assertTaskBranch(context.identity.repositoryRoot, task);
898
1572
  if (!['in_progress', 'ready'].includes(task.status)) {
899
1573
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot be submitted while ${task.status}.`);
@@ -920,10 +1594,13 @@ export class WorkflowService {
920
1594
  this.store.publishArtifact(stagedEvidence);
921
1595
  return saved;
922
1596
  }
923
- recordReview(repository, taskId, expectedRevision, review) {
1597
+ recordReview(repository, taskId, expectedRevision, review, actor, writerToken = null) {
924
1598
  const { identity } = this.context(repository);
1599
+ this.readCurrentAdoptionPostureStrict(identity.projectId);
925
1600
  const task = this.store.readTask(identity.projectId, taskId);
926
1601
  assertExpectedRevision(task, expectedRevision);
1602
+ this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
1603
+ this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
927
1604
  const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
928
1605
  if (task.status !== 'validating' && !retryingUnverified) {
929
1606
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not ready for review.`);
@@ -947,7 +1624,11 @@ export class WorkflowService {
947
1624
  if (recorded.findings.length > 0) {
948
1625
  const file = path.join(this.store.taskRoot(task.projectId, task.id), 'findings.jsonl');
949
1626
  for (const finding of recorded.findings) {
950
- appendFileSync(file, `${JSON.stringify({ ...finding, recordedAt: recorded.recordedAt })}\n`, {
1627
+ appendFileSync(file, `${JSON.stringify({
1628
+ ...finding,
1629
+ reviewer: recorded.reviewer,
1630
+ recordedAt: recorded.recordedAt,
1631
+ })}\n`, {
951
1632
  encoding: 'utf8',
952
1633
  mode: 0o600,
953
1634
  });
@@ -955,6 +1636,59 @@ export class WorkflowService {
955
1636
  }
956
1637
  return saved;
957
1638
  }
1639
+ recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
1640
+ const { identity } = this.context(repository);
1641
+ this.readCurrentAdoptionPostureStrict(identity.projectId);
1642
+ const task = this.store.readTask(identity.projectId, taskId);
1643
+ assertExpectedRevision(task, expectedRevision);
1644
+ this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
1645
+ this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
1646
+ if (task.status !== 'needs_fix' && task.status !== 'blocked') {
1647
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
1648
+ }
1649
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
1650
+ if (!planRiskAudit) {
1651
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no current Plan Risk Audit for corrective decision recording.`, {
1652
+ taskId,
1653
+ stepId,
1654
+ });
1655
+ }
1656
+ if (!task.planHash) {
1657
+ throw new WorkflowError('STATE_CORRUPT', `Task ${taskId} has no current Plan hash for corrective decision recording.`, {
1658
+ taskId,
1659
+ stepId,
1660
+ });
1661
+ }
1662
+ const step = requireStep(task, stepId);
1663
+ if (!isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
1664
+ throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit and cannot receive a corrective decision.`, {
1665
+ taskId,
1666
+ stepId,
1667
+ });
1668
+ }
1669
+ if (task.status === 'needs_fix') {
1670
+ if (step.status !== 'failed') {
1671
+ throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a currently failed guarded Step while task ${taskId} is needs_fix.`, {
1672
+ taskId,
1673
+ stepId,
1674
+ stepStatus: step.status,
1675
+ });
1676
+ }
1677
+ }
1678
+ else {
1679
+ const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
1680
+ if (step.status !== 'in_progress' || !step.evidence || currentCycle.resolution !== 'unverified') {
1681
+ throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a blocked unverified guarded Step with current evidence.`, {
1682
+ taskId,
1683
+ stepId,
1684
+ stepStatus: step.status,
1685
+ resolution: currentCycle.resolution,
1686
+ });
1687
+ }
1688
+ }
1689
+ assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
1690
+ return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
1691
+ }
958
1692
  setTaskResult(repository, taskId, expectedRevision, summary, limitations) {
959
1693
  const { identity } = this.context(repository);
960
1694
  const task = this.store.readTask(identity.projectId, taskId);
@@ -1059,10 +1793,12 @@ export class WorkflowService {
1059
1793
  this.store.publishArtifact(stagedEvidence);
1060
1794
  return saved;
1061
1795
  }
1062
- mergeTask(repository, taskId, expectedRevision, writerToken) {
1796
+ mergeTask(repository, taskId, expectedRevision, writerToken, actor) {
1063
1797
  const context = this.context(repository);
1064
1798
  const task = this.store.readTask(context.identity.projectId, taskId);
1065
1799
  assertExpectedRevision(task, expectedRevision);
1800
+ const lease = context.locks.heartbeat(taskId, writerToken);
1801
+ this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
1066
1802
  if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.taskBranch || !task.baseCommit) {
1067
1803
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not ready for a local merge.`);
1068
1804
  }
@@ -1079,10 +1815,12 @@ export class WorkflowService {
1079
1815
  context.locks.release(taskId, writerToken);
1080
1816
  return saved;
1081
1817
  }
1082
- confirmExternalMerge(repository, taskId, expectedRevision, writerToken) {
1818
+ confirmExternalMerge(repository, taskId, expectedRevision, writerToken, actor) {
1083
1819
  const context = this.context(repository);
1084
1820
  const task = this.store.readTask(context.identity.projectId, taskId);
1085
1821
  assertExpectedRevision(task, expectedRevision);
1822
+ const lease = context.locks.heartbeat(taskId, writerToken);
1823
+ this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
1086
1824
  if (task.status !== 'ready_to_merge' || task.workspaceOwner !== 'external') {
1087
1825
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not awaiting external merge confirmation.`);
1088
1826
  }
@@ -1096,12 +1834,194 @@ export class WorkflowService {
1096
1834
  }
1097
1835
  status(repository) {
1098
1836
  const { identity } = this.context(repository);
1837
+ const tasks = this.store.listTasks(identity.projectId);
1838
+ const rawMilestones = this.store.listMilestones(identity.projectId);
1839
+ const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones);
1099
1840
  return {
1100
1841
  projectId: identity.projectId,
1101
1842
  discoveries: this.store.listDiscoveries(identity.projectId),
1102
- tasks: this.store.listTasks(identity.projectId),
1103
- milestones: this.store.listMilestones(identity.projectId),
1843
+ tasks,
1844
+ milestones: this.listMilestonesCurrent(identity.projectId, adoption.currentPosture),
1104
1845
  delegations: this.store.listDelegations(identity.projectId),
1846
+ adoption,
1847
+ };
1848
+ }
1849
+ recoverMilestoneTransactions(repository) {
1850
+ const { identity } = this.context(repository);
1851
+ const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
1852
+ return {
1853
+ projectId: identity.projectId,
1854
+ recoveredMilestoneIds: milestones.map((milestone) => milestone.id),
1855
+ };
1856
+ }
1857
+ prepareAdoption(repository) {
1858
+ const { identity } = this.context(repository);
1859
+ return this.inspectAdoptionPreparation(identity, this.store.listTasks(identity.projectId), this.store.listMilestones(identity.projectId));
1860
+ }
1861
+ applyAdoption(repository, actor, confirmationCode) {
1862
+ const context = this.context(repository);
1863
+ const tasks = context.store.listTasks(context.identity.projectId);
1864
+ const milestones = context.store.listMilestones(context.identity.projectId);
1865
+ return applyAdoptionPosture(this.store, {
1866
+ projectId: context.identity.projectId,
1867
+ repositoryIdentity: context.identity,
1868
+ packageVersion: PACKAGE_VERSION,
1869
+ protocolVersion: PROTOCOL_VERSION,
1870
+ stateSchemaVersion: STATE_SCHEMA_VERSION,
1871
+ tasks,
1872
+ milestones,
1873
+ sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
1874
+ blockers: this.computeAdoptionBoundaryBlockers(context.identity, tasks, context.locks.list()),
1875
+ now: this.now(),
1876
+ }, actor, confirmationCode);
1877
+ }
1878
+ updatePreflight(repository) {
1879
+ const context = this.context(repository);
1880
+ const tasks = context.store.listTasks(context.identity.projectId);
1881
+ const milestones = context.store.listMilestones(context.identity.projectId);
1882
+ const dirty = changedFiles(context.identity.repositoryRoot);
1883
+ const runningSteps = tasks.flatMap((task) => task.steps
1884
+ .filter((step) => step.status === 'in_progress')
1885
+ .map((step) => ({ taskId: task.id, stepId: step.id })));
1886
+ const leases = context.locks.list();
1887
+ const now = this.now().getTime();
1888
+ const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
1889
+ const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
1890
+ const blockers = [
1891
+ ...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
1892
+ ...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
1893
+ ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
1894
+ ...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
1895
+ ];
1896
+ const adoption = buildAdoptionPreparation({
1897
+ projectId: context.identity.projectId,
1898
+ repositoryIdentity: context.identity,
1899
+ packageVersion: PACKAGE_VERSION,
1900
+ protocolVersion: PROTOCOL_VERSION,
1901
+ stateSchemaVersion: STATE_SCHEMA_VERSION,
1902
+ tasks,
1903
+ milestones,
1904
+ sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
1905
+ blockers,
1906
+ now: this.now(),
1907
+ }, readCurrentAdoptionPosture(this.store, context.identity.projectId));
1908
+ return {
1909
+ safe: blockers.length === 0,
1910
+ packageVersion: PACKAGE_VERSION,
1911
+ protocolVersion: PROTOCOL_VERSION,
1912
+ stateSchemaVersion: STATE_SCHEMA_VERSION,
1913
+ projectId: context.identity.projectId,
1914
+ repositoryRoot: context.identity.repositoryRoot,
1915
+ clean: dirty.length === 0,
1916
+ changedFiles: dirty,
1917
+ runningSteps,
1918
+ activeLeases,
1919
+ staleLeases,
1920
+ blockers,
1921
+ adoption,
1922
+ };
1923
+ }
1924
+ alpha6StrictReviewRescuePreflight(repository) {
1925
+ const context = this.context(repository);
1926
+ const dependency = inspectLocalDependency(context.identity.repositoryRoot);
1927
+ const tasks = context.store.listTasks(context.identity.projectId);
1928
+ const leases = context.locks.list();
1929
+ const now = this.now().getTime();
1930
+ const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
1931
+ const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
1932
+ const dirty = changedFiles(context.identity.repositoryRoot);
1933
+ const currentHead = headCommit(context.identity.repositoryRoot);
1934
+ const pendingCandidates = [];
1935
+ const malformedCandidates = [];
1936
+ for (const task of tasks) {
1937
+ if (task.status !== 'in_progress')
1938
+ continue;
1939
+ const planRiskAudit = task.planHash ? readCurrentPlanRiskAudit(this.store, task) : null;
1940
+ if (!planRiskAudit)
1941
+ continue;
1942
+ for (const step of task.steps) {
1943
+ if (step.status !== 'in_progress' || !step.evidence)
1944
+ continue;
1945
+ if (!isStepReviewRequired(task, step.id, planRiskAudit.reviewRequiredStepIds))
1946
+ continue;
1947
+ try {
1948
+ const cycle = buildCurrentStrictStepReviewCycle(this.store, task, step.id);
1949
+ if (cycle.resolution !== 'pending')
1950
+ continue;
1951
+ validateTaskHistory(context.identity.repositoryRoot, task);
1952
+ let knowledgeBindingStale = false;
1953
+ try {
1954
+ const storedKnowledgeMap = this.store.readKnowledgeMap(context.identity.projectId);
1955
+ const knowledgeMap = inspectKnowledgeMap(storedKnowledgeMap, scanProjectKnowledge(context.identity.repositoryRoot, context.identity.projectId, this.now()));
1956
+ knowledgeBindingStale = knowledgeMap.status !== 'active'
1957
+ || task.knowledgeMapRevision !== knowledgeMap.revision
1958
+ || task.knowledgeMapHash !== hashKnowledgeMap(knowledgeMap);
1959
+ }
1960
+ catch {
1961
+ knowledgeBindingStale = true;
1962
+ }
1963
+ pendingCandidates.push({
1964
+ taskId: task.id,
1965
+ taskRevision: task.revision,
1966
+ stepId: step.id,
1967
+ completionCommit: step.evidence.commitSha,
1968
+ currentHead,
1969
+ knowledgeBindingStale,
1970
+ });
1971
+ }
1972
+ catch (error) {
1973
+ malformedCandidates.push(error instanceof Error ? error.message : String(error));
1974
+ }
1975
+ }
1976
+ }
1977
+ const candidate = pendingCandidates.length === 1 ? pendingCandidates[0] : null;
1978
+ const candidateLeaseIds = new Set(candidate ? [candidate.taskId] : []);
1979
+ const blockers = [
1980
+ ...(dependency.declared !== '2.0.0-alpha.6'
1981
+ ? ['Project package.json must declare codex-workflow-v2 exactly at 2.0.0-alpha.6.']
1982
+ : []),
1983
+ ...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
1984
+ ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
1985
+ ...(pendingCandidates.length !== 1
1986
+ ? [`Expected exactly one pending alpha.6 strict review; found ${pendingCandidates.length}.`]
1987
+ : []),
1988
+ ...(candidate && candidate.currentHead !== candidate.completionCommit
1989
+ ? ['HEAD does not match the pending strict review completion commit.']
1990
+ : []),
1991
+ ...(staleLeases.some((lease) => !candidateLeaseIds.has(lease.entityId))
1992
+ ? ['A stale writer lease exists outside the rescue Task.']
1993
+ : []),
1994
+ ...malformedCandidates.map((message) => `Pending strict review state is invalid: ${message}`),
1995
+ ];
1996
+ const eligible = blockers.length === 0 && candidate !== null;
1997
+ const requiredActions = eligible
1998
+ ? [
1999
+ ...staleLeases.map((lease) => ({
2000
+ action: 'locks repair',
2001
+ entityId: lease.entityId,
2002
+ })),
2003
+ {
2004
+ action: 'task step-review',
2005
+ taskId: candidate.taskId,
2006
+ stepId: candidate.stepId,
2007
+ expectedRevision: candidate.taskRevision,
2008
+ },
2009
+ ]
2010
+ : [];
2011
+ return {
2012
+ eligible,
2013
+ mutationFree: true,
2014
+ runnerPackageVersion: PACKAGE_VERSION,
2015
+ requiredProjectPackageVersion: '2.0.0-alpha.6',
2016
+ declaredProjectPackageVersion: dependency.declared,
2017
+ projectId: context.identity.projectId,
2018
+ repositoryRoot: context.identity.repositoryRoot,
2019
+ clean: dirty.length === 0,
2020
+ activeLeases,
2021
+ staleLeases,
2022
+ candidate,
2023
+ blockers,
2024
+ requiredActions,
1105
2025
  };
1106
2026
  }
1107
2027
  scanKnowledgeMap(repository) {
@@ -1128,11 +2048,14 @@ export class WorkflowService {
1128
2048
  return inspectKnowledgeMap(current, scanned);
1129
2049
  }
1130
2050
  approveKnowledgeMap(repository, expectedRevision, selections, actor, delegationGrantId = null) {
2051
+ return this.approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, null);
2052
+ }
2053
+ approveKnowledgeMapForTaskRefresh(repository, expectedRevision, selections, actor, delegationGrantId, autonomyTaskId) {
1131
2054
  const { identity } = this.context(repository);
1132
2055
  const current = this.store.readKnowledgeMap(identity.projectId);
1133
2056
  assertExpectedRevision(current, expectedRevision);
1134
2057
  const inspected = inspectKnowledgeMap(current, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
1135
- const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspected);
2058
+ const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspected, autonomyTaskId);
1136
2059
  const approved = applyKnowledgeSelections(inspected, selections, actor, this.now());
1137
2060
  if (!approved.approval) {
1138
2061
  throw new WorkflowError('STATE_CORRUPT', 'Knowledge Map approval was not recorded.');
@@ -1177,12 +2100,44 @@ export class WorkflowService {
1177
2100
  }
1178
2101
  next(repository) {
1179
2102
  const status = this.status(repository);
2103
+ const adoptionInfo = {
2104
+ status: status.adoption.status,
2105
+ safe: status.adoption.safe,
2106
+ blockers: status.adoption.blockers,
2107
+ baselineDigestHash: status.adoption.baselineDigestHash,
2108
+ sidecarBaselineHash: status.adoption.sidecarBaselineHash,
2109
+ confirmationCodeBindingHash: status.adoption.confirmationCodeBindingHash,
2110
+ confirmationCode: status.adoption.confirmationCode,
2111
+ currentEventType: status.adoption.currentPosture?.eventType ?? null,
2112
+ };
2113
+ if (status.adoption.status !== 'applied') {
2114
+ return {
2115
+ scope: 'project',
2116
+ projectId: status.projectId,
2117
+ status: status.adoption.status,
2118
+ action: status.adoption.safe ? 'state adoption-apply' : 'restore safe boundary, then state adoption-prepare',
2119
+ adoption: adoptionInfo,
2120
+ };
2121
+ }
2122
+ const withAdoption = (value) => ({ ...value, adoption: adoptionInfo });
1180
2123
  const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
1181
2124
  const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
1182
2125
  const activeTask = status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
1183
2126
  if (activeTask) {
2127
+ const activePlanRiskAudit = activeTask.planHash
2128
+ ? readCurrentPlanRiskAudit(this.store, activeTask)
2129
+ : null;
2130
+ const taskNext = activeTask.status === 'planning'
2131
+ ? null
2132
+ : this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
2133
+ // Pending strict review is completion work for the already-recorded commit. It must
2134
+ // outrank Project Knowledge refresh/rebind; otherwise the active Step makes the rebind
2135
+ // returned by `next` impossible to execute.
2136
+ if (taskNext?.action === 'task step-review') {
2137
+ return withAdoption(taskNext);
2138
+ }
1184
2139
  if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
1185
- return this.nextForTaskWithDelegation(status.projectId, activeTask);
2140
+ return withAdoption(taskNext);
1186
2141
  }
1187
2142
  const knowledgeMap = this.knowledgeMapStatus(repository);
1188
2143
  if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
@@ -1190,21 +2145,37 @@ export class WorkflowService {
1190
2145
  const delegatedApprovalOptions = action === 'project-memory approve'
1191
2146
  ? this.delegatedApprovalOptions(status.projectId, 'project_memory.approve', knowledgeMap)
1192
2147
  : [];
1193
- return {
2148
+ const storedKnowledgeMap = this.store.readKnowledgeMap(status.projectId);
2149
+ const contextRefreshOptions = action === 'project-memory reconcile'
2150
+ && storedKnowledgeMap.status === 'active'
2151
+ && storedKnowledgeMap.approval
2152
+ && isContentOnlyKnowledgeRefresh(storedKnowledgeMap, knowledgeMap)
2153
+ ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap)
2154
+ : [];
2155
+ return withAdoption({
1194
2156
  scope: 'project-memory',
1195
2157
  status: knowledgeMap.status,
1196
2158
  action,
1197
2159
  revision: knowledgeMap.revision,
1198
2160
  taskId: activeTask.id,
1199
2161
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
1200
- };
2162
+ ...(contextRefreshOptions.length > 0
2163
+ ? {
2164
+ contextRefresh: {
2165
+ action: 'task context-refresh',
2166
+ mode: 'delegated-content-only',
2167
+ options: contextRefreshOptions,
2168
+ },
2169
+ }
2170
+ : {}),
2171
+ });
1201
2172
  }
1202
2173
  const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
1203
2174
  if (taskCanRebindKnowledge(activeTask) &&
1204
2175
  activeTask.planHash &&
1205
2176
  (activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
1206
2177
  activeTask.knowledgeMapHash !== knowledgeMapHash)) {
1207
- return {
2178
+ return withAdoption({
1208
2179
  scope: 'task',
1209
2180
  id: activeTask.id,
1210
2181
  status: activeTask.status,
@@ -1212,9 +2183,9 @@ export class WorkflowService {
1212
2183
  revision: activeTask.revision,
1213
2184
  knowledgeMapRevision: knowledgeMap.revision,
1214
2185
  knowledgeMapHash,
1215
- };
2186
+ });
1216
2187
  }
1217
- return this.nextForTaskWithDelegation(status.projectId, activeTask);
2188
+ return withAdoption(taskNext ?? this.nextForTaskWithDelegation(status.projectId, activeTask, null));
1218
2189
  }
1219
2190
  const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
1220
2191
  if (activeMilestone) {
@@ -1232,17 +2203,17 @@ export class WorkflowService {
1232
2203
  evidence.headCommit !== currentHead ||
1233
2204
  resultHash !== activeMilestone.resultHash ||
1234
2205
  evidenceHash !== activeMilestone.evidenceHash) {
1235
- return {
2206
+ return withAdoption({
1236
2207
  scope: 'milestone',
1237
2208
  id: activeMilestone.id,
1238
2209
  status: activeMilestone.status,
1239
2210
  action: 'restore clean base, then milestone validate',
1240
2211
  revision: activeMilestone.revision,
1241
2212
  reason: 'final validation evidence is stale',
1242
- };
2213
+ });
1243
2214
  }
1244
2215
  const delegatedApprovalOptions = this.delegatedApprovalOptions(status.projectId, 'milestone.final_accept', activeMilestone);
1245
- return {
2216
+ return withAdoption({
1246
2217
  scope: 'milestone',
1247
2218
  id: activeMilestone.id,
1248
2219
  status: activeMilestone.status,
@@ -1251,34 +2222,55 @@ export class WorkflowService {
1251
2222
  : 'await explicit human final acceptance',
1252
2223
  revision: activeMilestone.revision,
1253
2224
  requiredHumanGate: milestoneFinalAcceptanceGate(activeMilestone, evidence.headCommit),
2225
+ completionPolicy: {
2226
+ resultEvidenceAndHeadMustMatch: true,
2227
+ finalAcceptanceTransition: 'milestone.final_accept',
2228
+ delegatedActorMustRemainDelegate: true,
2229
+ },
1254
2230
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
1255
- };
2231
+ });
1256
2232
  }
1257
2233
  const delegatedApprovalOptions = activeMilestone.status === 'awaiting_execution_authorization'
1258
2234
  ? this.delegatedApprovalOptions(status.projectId, 'milestone.execution_authorize', activeMilestone)
1259
2235
  : [];
1260
- return {
2236
+ return withAdoption({
1261
2237
  scope: 'milestone',
1262
2238
  id: activeMilestone.id,
1263
2239
  status: activeMilestone.status,
1264
2240
  action: nextForMilestone(activeMilestone),
1265
2241
  revision: activeMilestone.revision,
2242
+ ...(activeMilestone.status === 'active'
2243
+ ? {
2244
+ completionPolicy: {
2245
+ requiredTasksMustBeMerged: true,
2246
+ validationAction: 'milestone validate',
2247
+ finalAcceptanceTransition: 'milestone.final_accept',
2248
+ delegatedActorMustRemainDelegate: true,
2249
+ requiredTasks: activeMilestone.memberships
2250
+ .filter((membership) => membership.disposition === 'required')
2251
+ .map((membership) => {
2252
+ const task = status.tasks.find((candidate) => candidate.id === membership.taskId);
2253
+ return { taskId: membership.taskId, status: task?.status ?? 'missing' };
2254
+ }),
2255
+ },
2256
+ }
2257
+ : {}),
1266
2258
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
1267
- };
2259
+ });
1268
2260
  }
1269
2261
  const activeDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status));
1270
2262
  if (activeDiscovery) {
1271
- return {
2263
+ return withAdoption({
1272
2264
  scope: 'discovery',
1273
2265
  id: activeDiscovery.id,
1274
2266
  status: activeDiscovery.status,
1275
2267
  action: activeDiscovery.status === 'ready_to_materialize' ? 'discovery materialize' : 'discovery update',
1276
2268
  readiness: assessDiscovery(activeDiscovery),
1277
- };
2269
+ });
1278
2270
  }
1279
- return { scope: 'repository', status: 'idle', action: 'discovery start' };
2271
+ return withAdoption({ scope: 'repository', status: 'idle', action: 'discovery start' });
1280
2272
  }
1281
- delegatedAuthorization(projectId, grantId, transition, actor, target) {
2273
+ delegatedAuthorization(projectId, grantId, transition, actor, target, autonomyTaskId = null) {
1282
2274
  if (!grantId) {
1283
2275
  if (actor.trim().startsWith('agent:')) {
1284
2276
  throw new WorkflowError('TRANSITION_BLOCKED', 'An agent actor requires an explicit delegated approval grant.');
@@ -1286,9 +2278,102 @@ export class WorkflowService {
1286
2278
  return null;
1287
2279
  }
1288
2280
  const grant = this.store.readDelegation(projectId, grantId);
2281
+ if (grant.scope.kind === 'milestone') {
2282
+ const autonomyContract = readMilestoneAutonomyEvents(this.store, projectId, grant.scope.id)
2283
+ .find((event) => event.delegationGrantId === grant.id) ?? null;
2284
+ if (autonomyContract) {
2285
+ const contextTask = target.kind === 'knowledge-map' && autonomyTaskId
2286
+ ? this.store.readTask(projectId, autonomyTaskId)
2287
+ : null;
2288
+ const milestone = target.kind === 'milestone'
2289
+ ? target
2290
+ : target.kind === 'task' && target.milestoneId === grant.scope.id
2291
+ ? this.store.readMilestone(projectId, grant.scope.id)
2292
+ : contextTask?.milestoneId === grant.scope.id
2293
+ ? this.store.readMilestone(projectId, grant.scope.id)
2294
+ : null;
2295
+ if (!milestone) {
2296
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone autonomy grant cannot authorize this target.');
2297
+ }
2298
+ const activeAutonomy = requireActiveMilestoneAutonomy({
2299
+ store: this.store,
2300
+ projectId,
2301
+ milestone,
2302
+ actor,
2303
+ now: this.now(),
2304
+ });
2305
+ if (activeAutonomy.grant.id !== grant.id) {
2306
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone autonomy grant has been superseded.');
2307
+ }
2308
+ if (target.kind === 'knowledge-map') {
2309
+ if (transition !== 'project_memory.approve'
2310
+ || !contextTask
2311
+ || !taskCanRebindKnowledge(contextTask)) {
2312
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone autonomy may approve Project Memory only inside a content-only Task context refresh.');
2313
+ }
2314
+ if (!grant.transitions.includes(transition)) {
2315
+ throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grant.id} does not allow ${transition}.`);
2316
+ }
2317
+ return {
2318
+ grantId: grant.id,
2319
+ policyHash: grant.policyHash,
2320
+ principal: grant.principal,
2321
+ delegate: grant.delegate,
2322
+ transition,
2323
+ };
2324
+ }
2325
+ }
2326
+ }
1289
2327
  return assertDelegationCanAuthorize(grant, transition, actor, target, this.now());
1290
2328
  }
1291
- nextForTaskWithDelegation(projectId, task) {
2329
+ assertHandoffGrant(projectId, task, targetActor, grantId) {
2330
+ const grant = this.store.readDelegation(projectId, grantId);
2331
+ const policyHash = hashDelegationPolicy(projectId, {
2332
+ principal: grant.principal,
2333
+ delegate: grant.delegate,
2334
+ scope: grant.scope,
2335
+ transitions: grant.transitions,
2336
+ expiresAt: grant.expiresAt,
2337
+ });
2338
+ const scopeMatches = grant.scope.kind === 'project'
2339
+ || (grant.scope.kind === 'task' && grant.scope.id === task.id)
2340
+ || (grant.scope.kind === 'milestone' && grant.scope.id === task.milestoneId);
2341
+ if (grant.status !== 'active'
2342
+ || grant.policyHash !== policyHash
2343
+ || Date.parse(grant.expiresAt) <= this.now().getTime()
2344
+ || grant.delegate !== targetActor.trim()
2345
+ || !scopeMatches) {
2346
+ throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grantId} cannot be bound to this C1 handoff.`, {
2347
+ grantId,
2348
+ taskId: task.id,
2349
+ targetActor: targetActor.trim(),
2350
+ grantStatus: grant.status,
2351
+ expectedPolicyHash: policyHash,
2352
+ actualPolicyHash: grant.policyHash,
2353
+ grantDelegate: grant.delegate,
2354
+ grantScope: grant.scope,
2355
+ grantExpiresAt: grant.expiresAt,
2356
+ });
2357
+ }
2358
+ if (grant.scope.kind === 'milestone') {
2359
+ const autonomyContract = readMilestoneAutonomyEvents(this.store, projectId, grant.scope.id)
2360
+ .find((event) => event.delegationGrantId === grant.id) ?? null;
2361
+ if (autonomyContract) {
2362
+ const milestone = this.store.readMilestone(projectId, grant.scope.id);
2363
+ const activeAutonomy = requireActiveMilestoneAutonomy({
2364
+ store: this.store,
2365
+ projectId,
2366
+ milestone,
2367
+ actor: targetActor,
2368
+ now: this.now(),
2369
+ });
2370
+ if (activeAutonomy.grant.id !== grant.id) {
2371
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone autonomy grant has been superseded.');
2372
+ }
2373
+ }
2374
+ }
2375
+ }
2376
+ nextForTaskWithDelegation(projectId, task, planRiskAudit = null) {
1292
2377
  const transition = task.status === 'awaiting_execution_authorization'
1293
2378
  ? 'task.execution_authorize'
1294
2379
  : task.status === 'awaiting_final_acceptance'
@@ -1297,14 +2382,130 @@ export class WorkflowService {
1297
2382
  const delegatedApprovalOptions = transition
1298
2383
  ? this.delegatedApprovalOptions(projectId, transition, task)
1299
2384
  : [];
1300
- return {
1301
- ...nextForTask(task),
2385
+ const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(projectId, task.id));
2386
+ const guardedCorrectiveGate = task.status === 'needs_fix' && planRiskAudit
2387
+ ? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, planRiskAudit)
2388
+ : null;
2389
+ const next = nextForTask(task);
2390
+ const handoffPosture = readTaskC1Posture(this.store, task);
2391
+ const strictStepReviewStep = planRiskAudit
2392
+ ? task.steps.find((step) => {
2393
+ if (step.status !== 'in_progress' || !step.evidence || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
2394
+ return false;
2395
+ }
2396
+ if (task.status !== 'blocked')
2397
+ return true;
2398
+ return buildCurrentStrictStepReviewCycle(this.store, task, step.id).resolution === 'unverified';
2399
+ }) ?? null
2400
+ : null;
2401
+ const handoffOverlay = handoffPosture.state === 'pending'
2402
+ ? {
2403
+ action: 'task claim',
2404
+ blockedAction: next.action,
2405
+ c1Handoff: {
2406
+ state: 'pending',
2407
+ targetActor: handoffPosture.targetActor,
2408
+ eventId: handoffPosture.latestEvent.eventId,
2409
+ },
2410
+ }
2411
+ : handoffPosture.state === 'claimed'
2412
+ ? {
2413
+ c1Handoff: {
2414
+ state: 'claimed',
2415
+ claimant: handoffPosture.claimant,
2416
+ writerLeaseTokenBound: handoffPosture.writerLeaseTokenHash !== null,
2417
+ requiredActor: handoffPosture.claimant,
2418
+ },
2419
+ }
2420
+ : task.milestoneId && next.action === 'task start'
2421
+ ? {
2422
+ action: 'task handoff-prepare',
2423
+ blockedAction: next.action,
2424
+ c1Handoff: {
2425
+ state: 'required',
2426
+ recommendedTargetActor: defaultTaskWorkerActor(task.id),
2427
+ targetActorDerived: true,
2428
+ },
2429
+ }
2430
+ : {};
2431
+ const result = {
2432
+ ...next,
2433
+ ...handoffOverlay,
2434
+ ...(strictStepReviewStep
2435
+ ? {
2436
+ action: 'task step-review',
2437
+ stepId: strictStepReviewStep.id,
2438
+ commitPolicy: {
2439
+ owner: 'workflow-core',
2440
+ manualCommitAllowed: false,
2441
+ completionAction: 'task step-review',
2442
+ },
2443
+ strictStepReview: {
2444
+ reviewRequired: true,
2445
+ completionCommit: strictStepReviewStep.evidence.commitSha,
2446
+ },
2447
+ }
2448
+ : {}),
2449
+ ...(planRiskAudit
2450
+ ? {
2451
+ planRiskAudit: {
2452
+ decision: planRiskAudit.audit.decision,
2453
+ scope: planRiskAudit.scope,
2454
+ postureEventType: planRiskAudit.postureEventType,
2455
+ reviewRequiredStepIds: planRiskAudit.reviewRequiredStepIds,
2456
+ },
2457
+ }
2458
+ : {}),
2459
+ ...(task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2
2460
+ ? {
2461
+ action: 'task plan-set with independent corrective audit',
2462
+ correctivePlanGate: {
2463
+ failedReviewAttempts,
2464
+ auditFileRequired: true,
2465
+ auditorMustDifferFromFailedReviewer: true,
2466
+ allowedDecision: 'continue-fix',
2467
+ escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
2468
+ },
2469
+ }
2470
+ : {}),
2471
+ ...(guardedCorrectiveGate
2472
+ ? {
2473
+ action: 'task corrective-decision',
2474
+ stepId: guardedCorrectiveGate.stepId,
2475
+ correctiveDecisionGate: {
2476
+ attemptCount: guardedCorrectiveGate.attemptCount,
2477
+ recommendedAuditor: defaultCorrectiveAuditorActor(task.id),
2478
+ auditorDerived: true,
2479
+ auditorMustDifferFromLatestStrictReviewer: true,
2480
+ allowedDecision: 'continue-fix',
2481
+ escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
2482
+ humanApprovalRequired: false,
2483
+ },
2484
+ }
2485
+ : {}),
1302
2486
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
1303
2487
  };
2488
+ if (handoffPosture.state === 'pending') {
2489
+ return {
2490
+ ...result,
2491
+ action: 'task claim',
2492
+ blockedAction: next.action,
2493
+ };
2494
+ }
2495
+ return result;
1304
2496
  }
1305
2497
  delegatedApprovalOptions(projectId, transition, target) {
1306
2498
  return this.store.listDelegations(projectId)
1307
- .filter((grant) => delegationCanAuthorize(grant, transition, target, this.now()))
2499
+ .filter((grant) => {
2500
+ try {
2501
+ return Boolean(this.delegatedAuthorization(projectId, grant.id, transition, grant.delegate, target));
2502
+ }
2503
+ catch (error) {
2504
+ if (error instanceof WorkflowError && error.code === 'TRANSITION_BLOCKED')
2505
+ return false;
2506
+ throw error;
2507
+ }
2508
+ })
1308
2509
  .map((grant) => ({
1309
2510
  grantId: grant.id,
1310
2511
  principal: grant.principal,
@@ -1315,6 +2516,30 @@ export class WorkflowService {
1315
2516
  policyHash: grant.policyHash,
1316
2517
  }));
1317
2518
  }
2519
+ delegatedContextRefreshOptions(projectId, task, knowledgeMap) {
2520
+ return this.store.listDelegations(projectId)
2521
+ .filter((grant) => {
2522
+ try {
2523
+ return Boolean(this.delegatedAuthorization(projectId, grant.id, 'project_memory.approve', grant.delegate, knowledgeMap, task.id)
2524
+ && this.delegatedAuthorization(projectId, grant.id, 'task.execution_authorize', grant.delegate, task));
2525
+ }
2526
+ catch (error) {
2527
+ if (error instanceof WorkflowError && error.code === 'TRANSITION_BLOCKED')
2528
+ return false;
2529
+ throw error;
2530
+ }
2531
+ })
2532
+ .map((grant) => ({
2533
+ grantId: grant.id,
2534
+ principal: grant.principal,
2535
+ delegate: grant.delegate,
2536
+ transition: 'task.execution_authorize',
2537
+ transitions: ['project_memory.approve', 'task.execution_authorize'],
2538
+ scope: grant.scope,
2539
+ expiresAt: grant.expiresAt,
2540
+ policyHash: grant.policyHash,
2541
+ }));
2542
+ }
1318
2543
  assertDelegationScopeExists(projectId, policy) {
1319
2544
  if (policy.scope.kind === 'task') {
1320
2545
  this.store.readTask(projectId, policy.scope.id);
@@ -1352,6 +2577,75 @@ export class WorkflowService {
1352
2577
  throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone authorization is stale.');
1353
2578
  }
1354
2579
  }
2580
+ readMilestoneCurrent(projectId, milestoneId) {
2581
+ return readMilestoneWithIntegrity(this.store, projectId, milestoneId, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId));
2582
+ }
2583
+ readMilestoneScopeChangeCurrent(projectId, milestoneId) {
2584
+ return readMilestoneForScopeChange(this.store, projectId, milestoneId, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId));
2585
+ }
2586
+ listMilestonesCurrent(projectId, posture = this.readCurrentAdoptionPostureStrict(projectId)) {
2587
+ const milestones = this.store.listMilestones(projectId);
2588
+ if (!posture)
2589
+ return milestones;
2590
+ const grandfatheredMilestones = new Set(posture.terminalMilestones.map((milestone) => `${milestone.milestoneId}:${milestone.revision}:${milestone.status}`));
2591
+ return milestones.map((milestone) => (grandfatheredMilestones.has(`${milestone.id}:${milestone.revision}:${milestone.status}`)
2592
+ ? milestone
2593
+ : readMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
2594
+ }
2595
+ prepareMilestoneScopeChangeCandidate(projectId, milestone, plan, actor) {
2596
+ validateMilestonePlan(plan);
2597
+ return prepareMilestoneScopeChangeCandidate({
2598
+ milestone,
2599
+ plan,
2600
+ readTask: (taskId) => this.store.readTask(projectId, taskId),
2601
+ actor,
2602
+ });
2603
+ }
2604
+ proveWriterLeaseTokenForHandoff(context, taskId, writerToken) {
2605
+ const currentLease = context.locks.inspect(taskId);
2606
+ if (!currentLease) {
2607
+ if (writerToken !== null) {
2608
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
2609
+ taskId,
2610
+ });
2611
+ }
2612
+ return null;
2613
+ }
2614
+ if (writerToken === null) {
2615
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} requires the active writer token while a lease is present.`, {
2616
+ taskId,
2617
+ leaseOwner: currentLease.owner,
2618
+ expiresAt: currentLease.expiresAt,
2619
+ });
2620
+ }
2621
+ return context.locks.heartbeat(taskId, writerToken);
2622
+ }
2623
+ proveOptionalWriterLeaseToken(context, taskId, writerToken) {
2624
+ const currentLease = context.locks.inspect(taskId);
2625
+ if (!currentLease) {
2626
+ if (writerToken !== null) {
2627
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
2628
+ taskId,
2629
+ });
2630
+ }
2631
+ return null;
2632
+ }
2633
+ if (writerToken === null)
2634
+ return null;
2635
+ return context.locks.heartbeat(taskId, writerToken);
2636
+ }
2637
+ assertTaskMutationAllowedByHandoff(task, actor, writerToken) {
2638
+ const posture = readTaskC1Posture(this.store, task);
2639
+ if (posture.state === 'none')
2640
+ return;
2641
+ if (!actor || !actor.trim()) {
2642
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has active C1 coordination and requires an explicit actor for mutation.`, {
2643
+ taskId: task.id,
2644
+ posture: posture.state,
2645
+ });
2646
+ }
2647
+ assertTaskMutationAllowedByC1(this.store, task, { actor: actor.trim(), writerToken });
2648
+ }
1355
2649
  readGraphBindingOrNull(projectId, graphKind) {
1356
2650
  try {
1357
2651
  return this.store.readGraphBinding(projectId, graphKind);
@@ -1374,6 +2668,93 @@ export class WorkflowService {
1374
2668
  }
1375
2669
  return inspected;
1376
2670
  }
2671
+ strictReviewKnowledgeMap(repositoryRoot, task) {
2672
+ const stored = this.store.readKnowledgeMap(task.projectId);
2673
+ const inspected = inspectKnowledgeMap(stored, scanProjectKnowledge(repositoryRoot, task.projectId, this.now()));
2674
+ if (inspected.status === 'active' && inspected.approval)
2675
+ return inspected;
2676
+ const storedHash = hashKnowledgeMap(stored);
2677
+ if (stored.status === 'active'
2678
+ && stored.approval
2679
+ && task.knowledgeMapRevision === stored.revision
2680
+ && task.knowledgeMapHash === storedHash
2681
+ && stored.approval.mapHash === storedHash) {
2682
+ return stored;
2683
+ }
2684
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Strict Step Review has neither a current active Knowledge Map nor its exact historical Task binding.', {
2685
+ taskId: task.id,
2686
+ inspectedStatus: inspected.status,
2687
+ taskKnowledgeMapRevision: task.knowledgeMapRevision,
2688
+ storedKnowledgeMapRevision: stored.revision,
2689
+ taskKnowledgeMapHash: task.knowledgeMapHash,
2690
+ storedKnowledgeMapHash: storedHash,
2691
+ });
2692
+ }
2693
+ inspectAdoptionPreparation(identity, tasks, milestones) {
2694
+ const locks = new WriterLockManager(this.store.lockRoot(identity.projectId), this.now);
2695
+ const currentPosture = readCurrentAdoptionPosture(this.store, identity.projectId);
2696
+ if (currentPosture) {
2697
+ assertAdoptionBaselinePreserved(currentPosture, tasks, milestones);
2698
+ }
2699
+ return buildAdoptionPreparation({
2700
+ projectId: identity.projectId,
2701
+ repositoryIdentity: identity,
2702
+ packageVersion: PACKAGE_VERSION,
2703
+ protocolVersion: PROTOCOL_VERSION,
2704
+ stateSchemaVersion: STATE_SCHEMA_VERSION,
2705
+ tasks,
2706
+ milestones,
2707
+ sidecarInventory: this.collectAdoptionSidecarInventory(identity.projectId, tasks, milestones),
2708
+ blockers: this.computeAdoptionBoundaryBlockers(identity, tasks, locks.list()),
2709
+ now: this.now(),
2710
+ }, currentPosture);
2711
+ }
2712
+ computeAdoptionBoundaryBlockers(identity, tasks, leases) {
2713
+ const dirty = changedFiles(identity.repositoryRoot);
2714
+ const runningSteps = tasks.flatMap((task) => task.steps
2715
+ .filter((step) => step.status === 'in_progress')
2716
+ .map((step) => ({ taskId: task.id, stepId: step.id })));
2717
+ const now = this.now().getTime();
2718
+ const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
2719
+ const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
2720
+ return [
2721
+ ...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
2722
+ ...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
2723
+ ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
2724
+ ...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
2725
+ ];
2726
+ }
2727
+ collectAdoptionSidecarInventory(projectId, tasks, milestones) {
2728
+ const inventory = [];
2729
+ const collect = (scope, ownerId, root) => {
2730
+ if (!existsSync(root))
2731
+ return;
2732
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
2733
+ if (entry.isSymbolicLink()) {
2734
+ throw new WorkflowError('STATE_CORRUPT', `Adoption sidecar inventory rejects symlink entries: ${path.join(root, entry.name)}`);
2735
+ }
2736
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
2737
+ continue;
2738
+ const filePath = path.join(root, entry.name);
2739
+ const stat = lstatSync(filePath);
2740
+ if (!stat.isFile()) {
2741
+ throw new WorkflowError('STATE_CORRUPT', `Adoption sidecar inventory requires regular files only: ${filePath}`);
2742
+ }
2743
+ inventory.push({
2744
+ scope,
2745
+ ownerId,
2746
+ sidecarName: entry.name,
2747
+ contentHash: createHash('sha256').update(readFileSync(filePath)).digest('hex'),
2748
+ });
2749
+ }
2750
+ };
2751
+ collect('project', projectId, this.store.projectRoot(projectId));
2752
+ for (const task of tasks)
2753
+ collect('task', task.id, this.store.taskRoot(projectId, task.id));
2754
+ for (const milestone of milestones)
2755
+ collect('milestone', milestone.id, this.store.milestoneRoot(projectId, milestone.id));
2756
+ return inventory;
2757
+ }
1377
2758
  assertKnowledgeMapBinding(repositoryRoot, projectId, revision, mapHash) {
1378
2759
  const map = this.requireActiveKnowledgeMap(repositoryRoot, projectId);
1379
2760
  const currentHash = hashKnowledgeMap(map);
@@ -1387,6 +2768,29 @@ export class WorkflowService {
1387
2768
  }
1388
2769
  return map;
1389
2770
  }
2771
+ readCurrentAdoptionPostureStrict(projectId) {
2772
+ const posture = readCurrentAdoptionPosture(this.store, projectId);
2773
+ if (!posture)
2774
+ return null;
2775
+ assertAdoptionBaselinePreserved(posture, this.store.listTasks(projectId), this.store.listMilestones(projectId));
2776
+ return posture;
2777
+ }
2778
+ requireExecutionAdoptionPosture(identity, taskId) {
2779
+ const tasks = this.store.listTasks(identity.projectId);
2780
+ const milestones = this.store.listMilestones(identity.projectId);
2781
+ const posture = this.readCurrentAdoptionPostureStrict(identity.projectId);
2782
+ if (posture)
2783
+ return posture;
2784
+ const adoption = this.inspectAdoptionPreparation(identity, tasks, milestones);
2785
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Alpha.6 adoption posture is required before execution can proceed.', {
2786
+ taskId,
2787
+ adoptionStatus: adoption.status,
2788
+ requiredAction: adoption.safe ? 'state adoption-apply' : 'restore safe boundary, then state adoption-prepare',
2789
+ blockers: adoption.blockers,
2790
+ confirmationCode: adoption.confirmationCode,
2791
+ confirmationCodeBindingHash: adoption.confirmationCodeBindingHash,
2792
+ });
2793
+ }
1390
2794
  assertTaskKnowledgeBinding(repositoryRoot, task) {
1391
2795
  if (task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
1392
2796
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has no Project Knowledge Map binding.`);
@@ -1458,6 +2862,62 @@ function dependenciesComplete(step, completed) {
1458
2862
  const completedIds = new Set(completed);
1459
2863
  return step.dependencies.every((dependency) => completedIds.has(dependency));
1460
2864
  }
2865
+ function applyStrictStepReviewResolution(task, stepId, resolution, summary) {
2866
+ if (resolution === 'pending') {
2867
+ throw new WorkflowError('STATE_CORRUPT', `Strict Step Review resolution is still pending for ${stepId}.`, {
2868
+ taskId: task.id,
2869
+ stepId,
2870
+ });
2871
+ }
2872
+ const step = requireStep(task, stepId);
2873
+ if (!step.evidence) {
2874
+ throw new WorkflowError('STATE_CORRUPT', `Strict Step Review resolution requires canonical evidence for ${stepId}.`, {
2875
+ taskId: task.id,
2876
+ stepId,
2877
+ resolution,
2878
+ });
2879
+ }
2880
+ if (resolution === 'passed_verified') {
2881
+ const completedIds = new Set([
2882
+ ...task.steps.filter((candidate) => candidate.status === 'completed').map((candidate) => candidate.id),
2883
+ stepId,
2884
+ ]);
2885
+ return {
2886
+ ...task,
2887
+ status: 'in_progress',
2888
+ steps: task.steps.map((candidate) => {
2889
+ if (candidate.id === stepId)
2890
+ return { ...candidate, status: 'completed', evidence: step.evidence };
2891
+ if (candidate.status === 'blocked' && dependenciesComplete(candidate, completedIds)) {
2892
+ return { ...candidate, status: 'planned' };
2893
+ }
2894
+ return candidate;
2895
+ }),
2896
+ blockReason: null,
2897
+ };
2898
+ }
2899
+ if (resolution === 'failed') {
2900
+ return {
2901
+ ...task,
2902
+ status: 'needs_fix',
2903
+ steps: task.steps.map((candidate) => candidate.id === stepId
2904
+ ? { ...candidate, status: 'failed', evidence: null }
2905
+ : candidate),
2906
+ blockReason: summary?.trim() || `Strict Step Review failed for ${stepId}.`,
2907
+ systemCommits: task.systemCommits.includes(step.evidence.commitSha)
2908
+ ? task.systemCommits
2909
+ : [...task.systemCommits, step.evidence.commitSha],
2910
+ };
2911
+ }
2912
+ return {
2913
+ ...task,
2914
+ status: 'blocked',
2915
+ steps: task.steps.map((candidate) => candidate.id === stepId
2916
+ ? { ...candidate, status: 'in_progress', evidence: step.evidence }
2917
+ : candidate),
2918
+ blockReason: summary?.trim() || `Strict Step Review isolation could not be verified for ${stepId}.`,
2919
+ };
2920
+ }
1461
2921
  function identityRelative(repositoryRoot, value) {
1462
2922
  return path.resolve(repositoryRoot, value);
1463
2923
  }
@@ -1511,6 +2971,83 @@ function validateReviewInput(review) {
1511
2971
  throw new WorkflowError('INVALID_ARGUMENT', 'A failed review requires at least one finding.');
1512
2972
  }
1513
2973
  }
2974
+ function countFailedReviewAttempts(taskRoot) {
2975
+ const file = path.join(taskRoot, 'findings.jsonl');
2976
+ if (!existsSync(file))
2977
+ return 0;
2978
+ const attempts = new Set();
2979
+ for (const line of readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
2980
+ try {
2981
+ const finding = JSON.parse(line);
2982
+ if (typeof finding.recordedAt === 'string' && finding.recordedAt) {
2983
+ attempts.add(`${finding.recordedAt}\0${typeof finding.reviewer === 'string' ? finding.reviewer : ''}`);
2984
+ }
2985
+ }
2986
+ catch {
2987
+ throw new WorkflowError('STATE_CORRUPT', `Invalid review finding log: ${file}`);
2988
+ }
2989
+ }
2990
+ return attempts.size;
2991
+ }
2992
+ function validateCorrectivePlanAudit(task, audit, failedReviewAttempts) {
2993
+ if (!audit) {
2994
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has ${failedReviewAttempts} failed independent reviews. A separate corrective Plan audit is required before another plan-set.`, {
2995
+ failedReviewAttempts,
2996
+ requiredAction: 'Run a separate read-only Auditor and pass its evidence through --corrective-audit-file.',
2997
+ });
2998
+ }
2999
+ if (!audit.auditor.trim() || !audit.summary.trim()) {
3000
+ throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit requires auditor and summary.');
3001
+ }
3002
+ if (audit.auditor.trim() === task.review.reviewer.trim()) {
3003
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan audit must be performed by an Auditor distinct from the failed-review author.');
3004
+ }
3005
+ const latestFindingIds = new Set(task.review.findings.map((finding) => finding.id));
3006
+ const reviewedFindingIds = new Set(audit.reviewedFindingIds);
3007
+ if (reviewedFindingIds.size !== audit.reviewedFindingIds.length || audit.reviewedFindingIds.some((id) => !id.trim())) {
3008
+ throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit finding IDs must be non-empty and unique.');
3009
+ }
3010
+ const missing = [...latestFindingIds].filter((findingId) => !reviewedFindingIds.has(findingId));
3011
+ if (missing.length > 0) {
3012
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan audit does not cover every latest finding.', {
3013
+ missingFindingIds: missing,
3014
+ });
3015
+ }
3016
+ if (audit.decision !== 'continue-fix') {
3017
+ throw new WorkflowError('TRANSITION_BLOCKED', `Corrective Auditor decision ${audit.decision} requires coordinator/user redirection before implementation continues.`, { decision: audit.decision, summary: audit.summary });
3018
+ }
3019
+ }
3020
+ function appendCorrectivePlanAudit(taskRoot, previous, saved, failedReviewAttempts, audit, now) {
3021
+ appendFileSync(path.join(taskRoot, 'corrective-plan-audits.jsonl'), `${JSON.stringify({
3022
+ recordedAt: now.toISOString(),
3023
+ taskId: previous.id,
3024
+ taskRevision: previous.revision,
3025
+ failedReviewAttempts,
3026
+ previousPlanHash: previous.planHash,
3027
+ correctivePlanHash: saved.planHash,
3028
+ ...audit,
3029
+ })}\n`, { encoding: 'utf8', mode: 0o600 });
3030
+ }
3031
+ function assertContentOnlyKnowledgeRefresh(approved, inspected) {
3032
+ if (!isContentOnlyKnowledgeRefresh(approved, inspected)) {
3033
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh is limited to content-hash drift. Knowledge sources, categories, scopes, authorities, gaps, or conflicts changed.', { requiredAction: 'Run project-memory reconcile, inspect the classification diff, and approve it explicitly.' });
3034
+ }
3035
+ }
3036
+ function isContentOnlyKnowledgeRefresh(approved, inspected) {
3037
+ const classification = (map) => JSON.stringify({
3038
+ entries: map.entries.map((entry) => ({
3039
+ id: entry.id,
3040
+ category: entry.category,
3041
+ path: entry.path,
3042
+ scope: entry.scope,
3043
+ authority: entry.authority,
3044
+ discoveredBy: entry.discoveredBy,
3045
+ })),
3046
+ gaps: map.gaps,
3047
+ conflicts: map.conflicts,
3048
+ });
3049
+ return classification(approved) === classification(inspected);
3050
+ }
1514
3051
  function validateMilestonePlan(plan) {
1515
3052
  if (!plan.outcome.trim())
1516
3053
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone outcome is required.');
@@ -1606,11 +3143,14 @@ function nextForMilestone(milestone) {
1606
3143
  }
1607
3144
  function nextForTask(task) {
1608
3145
  const stepsComplete = task.steps.every((step) => step.status === 'completed' || step.status === 'skipped');
3146
+ const activeStep = task.steps.find((step) => step.status === 'in_progress');
3147
+ const failedStep = task.steps.find((step) => step.status === 'failed');
3148
+ const runnableStep = task.steps.find((step) => step.status === 'planned' && step.dependencies.every((dependency) => ['completed', 'skipped'].includes(requireStep(task, dependency).status)));
1609
3149
  const actionByStatus = {
1610
3150
  planning: 'task plan-set',
1611
3151
  awaiting_execution_authorization: 'task authorize',
1612
3152
  ready: task.workspaceOwner ? (stepsComplete ? 'task submit' : 'task run') : 'task start',
1613
- in_progress: stepsComplete ? 'task submit' : 'task run or task submit',
3153
+ in_progress: activeStep ? 'task step-complete' : stepsComplete ? 'task submit' : 'task run',
1614
3154
  validating: task.review.status === 'passed' ? 'task result-set' : 'task review-record',
1615
3155
  needs_fix: task.steps.some((step) => step.status === 'failed') ? 'task run' : 'task plan-set',
1616
3156
  awaiting_final_acceptance: 'task accept',
@@ -1620,7 +3160,24 @@ function nextForTask(task) {
1620
3160
  blocked: task.review.status === 'unverified' ? 'task review-record' : 'doctor',
1621
3161
  cancelled: 'none',
1622
3162
  };
1623
- return { scope: 'task', id: task.id, status: task.status, action: actionByStatus[task.status], revision: task.revision };
3163
+ const selectedStep = activeStep ?? failedStep ?? runnableStep;
3164
+ return {
3165
+ scope: 'task',
3166
+ id: task.id,
3167
+ status: task.status,
3168
+ action: actionByStatus[task.status],
3169
+ revision: task.revision,
3170
+ ...(selectedStep ? { stepId: selectedStep.id } : {}),
3171
+ ...(activeStep
3172
+ ? {
3173
+ commitPolicy: {
3174
+ owner: 'workflow-core',
3175
+ manualCommitAllowed: false,
3176
+ completionAction: 'task step-complete',
3177
+ },
3178
+ }
3179
+ : {}),
3180
+ };
1624
3181
  }
1625
3182
  function knowledgeSummary(map) {
1626
3183
  return {