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

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 (76) hide show
  1. package/README.md +86 -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 +39 -0
  6. package/dist/src/alpha6/handoff.js +975 -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 +49 -0
  12. package/dist/src/alpha6/milestone.js +1049 -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 +847 -0
  16. package/dist/src/alpha6/plan-risk.js.map +1 -0
  17. package/dist/src/alpha6/remediation.d.ts +20 -0
  18. package/dist/src/alpha6/remediation.js +748 -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/cli.js +88 -19
  27. package/dist/src/cli.js.map +1 -1
  28. package/dist/src/contracts.d.ts +259 -0
  29. package/dist/src/git.js +2 -1
  30. package/dist/src/git.js.map +1 -1
  31. package/dist/src/index.d.ts +2 -0
  32. package/dist/src/reviewer.d.ts +6 -1
  33. package/dist/src/reviewer.js +145 -32
  34. package/dist/src/reviewer.js.map +1 -1
  35. package/dist/src/state/lock.d.ts +1 -0
  36. package/dist/src/state/lock.js +7 -1
  37. package/dist/src/state/lock.js.map +1 -1
  38. package/dist/src/state/store.d.ts +39 -1
  39. package/dist/src/state/store.js +127 -1
  40. package/dist/src/state/store.js.map +1 -1
  41. package/dist/src/version.d.ts +1 -1
  42. package/dist/src/version.js +1 -1
  43. package/dist/src/workflow.d.ts +78 -8
  44. package/dist/src/workflow.js +1312 -74
  45. package/dist/src/workflow.js.map +1 -1
  46. package/docs/autonomy-guardrails.md +143 -0
  47. package/docs/decisions.md +23 -0
  48. package/docs/delegated-approval.md +30 -5
  49. package/docs/development-flow.md +40 -1
  50. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +155 -136
  51. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +236 -223
  52. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +225 -206
  53. package/docs/project-memory.md +7 -0
  54. package/docs/release.md +7 -0
  55. package/docs/updating-existing-project.md +53 -5
  56. package/docs/validation-report.md +47 -34
  57. package/package.json +1 -1
  58. package/plugins/codex-workflow-gateway/references/protocol.md +150 -6
  59. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +16 -1
  60. package/references/git-policy.md +5 -2
  61. package/references/state-machine.md +36 -0
  62. package/references/validation-and-review.md +28 -1
  63. package/roles/delivery-coordinator.md +11 -0
  64. package/roles/independent-reviewer.md +3 -0
  65. package/roles/technical-planner.md +7 -0
  66. package/roles/worker.md +4 -0
  67. package/schemas/adoption-posture-event.schema.json +129 -0
  68. package/schemas/corrective-decision-event.schema.json +55 -0
  69. package/schemas/corrective-plan-audit.schema.json +20 -0
  70. package/schemas/milestone-scope-change-event.schema.json +68 -0
  71. package/schemas/milestone-transaction-journal.schema.json +95 -0
  72. package/schemas/plan-risk-audit-event.schema.json +90 -0
  73. package/schemas/remediation-event.schema.json +53 -0
  74. package/schemas/reviewer-attestation-event.schema.json +49 -0
  75. package/schemas/step-review-event.schema.json +74 -0
  76. package/schemas/task-handoff-event.schema.json +116 -0
@@ -1,16 +1,24 @@
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, delegationCanAuthorize, 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 { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, readTaskC1Posture, } from './alpha6/handoff.js';
12
+ import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
13
+ import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
14
+ import { appendCorrectiveDecisionEvent, appendRemediationEvent, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, findFailedGuardedStepRequiringCorrectiveDecision, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
15
+ import { applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
16
+ import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
11
17
  import { applyKnowledgeSelections, hashKnowledgeMap, inspectKnowledgeMap, normalizeRelativePath, reconcileKnowledgeMap, scanProjectKnowledge, selectKnowledgeSources, } from './memory.js';
18
+ import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
12
19
  import { commitStep, mergeTaskBranch, runChecks, startLocalTaskBranch, syncBaseIntoTask, validateTaskHistory, } from './git.js';
13
20
  import { changedFiles, currentBranch, gitIsAncestor, headCommit, isClean, resolveRepositoryIdentity, runGit, } from './repository.js';
21
+ import { launchStrictStepReviewer } from './reviewer.js';
14
22
  import { WriterLockManager } from './state/lock.js';
15
23
  import { FileStateStore } from './state/store.js';
16
24
  import { createEntityId } from './ulid.js';
@@ -24,6 +32,18 @@ export class WorkflowService {
24
32
  }
25
33
  context(repository) {
26
34
  const identity = resolveRepositoryIdentity(repository);
35
+ const projectFile = path.join(this.store.projectRoot(identity.projectId), 'project.json');
36
+ const projectAlreadyRegistered = existsSync(projectFile);
37
+ if (!projectAlreadyRegistered) {
38
+ initializeProjectRegistrationAdoption(this.store, {
39
+ projectId: identity.projectId,
40
+ repositoryIdentity: identity,
41
+ packageVersion: PACKAGE_VERSION,
42
+ protocolVersion: PROTOCOL_VERSION,
43
+ stateSchemaVersion: STATE_SCHEMA_VERSION,
44
+ now: this.now(),
45
+ });
46
+ }
27
47
  this.store.registerProject(identity);
28
48
  return {
29
49
  identity,
@@ -162,7 +182,7 @@ export class WorkflowService {
162
182
  if (!title.trim())
163
183
  throw new WorkflowError('INVALID_ARGUMENT', 'Task title is required.');
164
184
  if (milestoneId) {
165
- const milestone = this.store.readMilestone(identity.projectId, milestoneId);
185
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
166
186
  if (milestone.status === 'accepted' || milestone.status === 'cancelled') {
167
187
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
168
188
  }
@@ -271,17 +291,29 @@ export class WorkflowService {
271
291
  }
272
292
  setMilestonePlan(repository, milestoneId, expectedRevision, plan) {
273
293
  const { identity } = this.context(repository);
274
- const milestone = this.store.readMilestone(identity.projectId, milestoneId);
294
+ const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
275
295
  assertExpectedRevision(milestone, expectedRevision);
276
296
  if (!['planning', 'awaiting_execution_authorization', 'active', 'blocked'].includes(milestone.status)) {
277
297
  throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Milestone Plan while ${milestone.status}.`);
278
298
  }
299
+ if (milestone.status !== 'planning'
300
+ || milestone.planHash !== null
301
+ || milestone.membershipRevision !== 0
302
+ || milestone.taskIds.length > 0
303
+ || milestone.memberships.length > 0) {
304
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone Plan is no longer in the initial empty planning posture. Use explicit scope-change prepare/apply.', {
305
+ milestoneId,
306
+ revision: milestone.revision,
307
+ requiredAction: 'prepareMilestoneScopeChange -> applyMilestoneScopeChange',
308
+ });
309
+ }
279
310
  validateMilestonePlan(plan);
280
- const taskIds = plan.memberships.map((membership) => membership.taskId);
311
+ const normalized = normalizeMilestonePlan(plan);
312
+ const taskIds = normalized.taskIds;
281
313
  if (new Set(taskIds).size !== taskIds.length) {
282
314
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone memberships must have unique Task IDs.');
283
315
  }
284
- for (const membership of plan.memberships) {
316
+ for (const membership of normalized.memberships) {
285
317
  const task = this.store.readTask(identity.projectId, membership.taskId);
286
318
  if (task.milestoneId !== milestoneId) {
287
319
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} is not linked to Milestone ${milestoneId}.`);
@@ -296,22 +328,18 @@ export class WorkflowService {
296
328
  throw new WorkflowError('TRANSITION_BLOCKED', `Started Task ${task.id} cannot be removed from a Milestone.`);
297
329
  }
298
330
  }
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
331
  const authorizations = milestone.authorizations.map((authorization) => authorization.kind === 'execution' && authorization.decision === 'approved'
312
332
  ? { ...authorization, decision: 'superseded' }
313
333
  : authorization);
314
- const saved = this.store.writeMilestone({
334
+ const root = this.store.milestoneRoot(identity.projectId, milestoneId);
335
+ const stagedPlan = this.store.stageArtifact(root, 'plan.json', JSON.stringify({
336
+ outcome: normalized.outcome,
337
+ successSignal: normalized.successSignal,
338
+ acceptance: normalized.acceptance,
339
+ memberships: normalized.memberships,
340
+ checks: normalized.checks,
341
+ }, null, 2));
342
+ const candidate = {
315
343
  ...milestone,
316
344
  status: 'awaiting_execution_authorization',
317
345
  outcome: normalized.outcome,
@@ -327,13 +355,55 @@ export class WorkflowService {
327
355
  evidenceHash: null,
328
356
  acceptedHead: null,
329
357
  cancellationReason: null,
330
- }, expectedRevision);
358
+ };
359
+ assertMilestoneMembershipIntegrity(candidate, (taskId) => this.store.readTask(identity.projectId, taskId), {}, () => this.store.listTasks(identity.projectId));
360
+ const saved = this.store.writeMilestone(candidate, expectedRevision);
331
361
  this.store.publishArtifact(stagedPlan);
332
362
  return saved;
333
363
  }
364
+ prepareMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor) {
365
+ const { identity } = this.context(repository);
366
+ const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
367
+ assertExpectedRevision(milestone, expectedRevision);
368
+ assertMilestoneScopeChangeStatusAllowed(milestone);
369
+ const prepared = this.prepareMilestoneScopeChangeCandidate(identity.projectId, milestone, plan, actor);
370
+ return {
371
+ milestoneId: prepared.milestoneId,
372
+ revision: prepared.expectedRevision,
373
+ actor: prepared.actor,
374
+ candidatePlanHash: prepared.candidatePlanHash,
375
+ candidateMembershipRevision: prepared.candidateMembershipRevision,
376
+ confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
377
+ confirmationCode: prepared.confirmationCode,
378
+ after: prepared.after,
379
+ };
380
+ }
381
+ applyMilestoneScopeChange(repository, milestoneId, expectedRevision, plan, actor, confirmationCode) {
382
+ const { identity } = this.context(repository);
383
+ const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, milestoneId);
384
+ assertExpectedRevision(milestone, expectedRevision);
385
+ assertMilestoneScopeChangeStatusAllowed(milestone);
386
+ const prepared = this.prepareMilestoneScopeChangeCandidate(identity.projectId, milestone, plan, actor);
387
+ if (confirmationCode.trim() !== prepared.confirmationCode) {
388
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone scope-change confirmation does not match the current Milestone state and requested scope.', {
389
+ milestoneId,
390
+ revision: milestone.revision,
391
+ actor: prepared.actor,
392
+ confirmationCodeBindingHash: prepared.confirmationCodeBindingHash,
393
+ });
394
+ }
395
+ return applyMilestoneScopeChangeTransaction({
396
+ store: this.store,
397
+ milestone,
398
+ expectedRevision,
399
+ prepared,
400
+ now: this.now(),
401
+ readTask: (taskId) => this.store.readTask(identity.projectId, taskId),
402
+ });
403
+ }
334
404
  authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
335
405
  const { identity } = this.context(repository);
336
- const milestone = this.store.readMilestone(identity.projectId, milestoneId);
406
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
337
407
  assertExpectedRevision(milestone, expectedRevision);
338
408
  if (milestone.status !== 'awaiting_execution_authorization' || !milestone.planHash) {
339
409
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not awaiting authorization.`);
@@ -362,7 +432,7 @@ export class WorkflowService {
362
432
  }
363
433
  validateMilestone(repository, milestoneId, expectedRevision) {
364
434
  const { identity } = this.context(repository);
365
- const milestone = this.store.readMilestone(identity.projectId, milestoneId);
435
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
366
436
  assertExpectedRevision(milestone, expectedRevision);
367
437
  if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
368
438
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} cannot be validated while ${milestone.status}.`);
@@ -425,7 +495,7 @@ export class WorkflowService {
425
495
  }
426
496
  acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode, delegationGrantId = null) {
427
497
  const { identity } = this.context(repository);
428
- const milestone = this.store.readMilestone(identity.projectId, milestoneId);
498
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
429
499
  assertExpectedRevision(milestone, expectedRevision);
430
500
  if (milestone.status !== 'awaiting_final_acceptance' || !milestone.resultHash || !milestone.evidenceHash) {
431
501
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is not ready for acceptance.`);
@@ -480,23 +550,93 @@ export class WorkflowService {
480
550
  if (!reason.trim())
481
551
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone cancellation requires a reason.');
482
552
  const { identity } = this.context(repository);
483
- const milestone = this.store.readMilestone(identity.projectId, milestoneId);
553
+ const milestone = this.readMilestoneCurrent(identity.projectId, milestoneId);
484
554
  assertExpectedRevision(milestone, expectedRevision);
485
555
  if (['accepted', 'cancelled'].includes(milestone.status)) {
486
556
  throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} is ${milestone.status}.`);
487
557
  }
488
558
  return this.store.writeMilestone({ ...milestone, status: 'cancelled', cancellationReason: reason.trim() }, expectedRevision);
489
559
  }
490
- setTaskPlan(repository, taskId, expectedRevision, plan) {
560
+ setTaskPlan(repository, taskId, expectedRevision, plan, correctiveAudit = null, planRiskAudit = null) {
491
561
  validatePlan(plan);
492
562
  const { identity } = this.context(repository);
493
563
  const knowledgeMap = this.assertKnowledgeMapBinding(identity.repositoryRoot, identity.projectId, plan.knowledgeMapRevision, plan.knowledgeMapHash);
494
564
  this.validateKnowledgeTargets(knowledgeMap, plan);
495
565
  const task = this.store.readTask(identity.projectId, taskId);
496
566
  assertExpectedRevision(task, expectedRevision);
497
- if (!['planning', 'awaiting_execution_authorization', 'needs_fix'].includes(task.status)) {
567
+ if (!['planning', 'awaiting_execution_authorization', 'needs_fix', 'blocked'].includes(task.status)) {
498
568
  throw new WorkflowError('TRANSITION_BLOCKED', `Cannot update Plan while Task is ${task.status}.`);
499
569
  }
570
+ const currentPlanCorrectiveDecisions = task.planHash
571
+ ? readCorrectiveDecisionEvents(this.store, task).filter((event) => event.planHash === task.planHash)
572
+ : [];
573
+ const currentReplanDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'replan-required');
574
+ const currentContinueFixDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'continue-fix');
575
+ const currentHardBlockingDecisions = currentPlanCorrectiveDecisions.filter((event) => event.decision === 'split-required' || event.decision === 'stop-escalate');
576
+ const currentReplanRequiredStepIds = [...new Set(currentReplanDecisions.map((event) => event.stepId))];
577
+ if (currentHardBlockingDecisions.length > 0) {
578
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has current corrective decisions that prohibit normal plan updates.`, {
579
+ taskId: task.id,
580
+ requiredAction: 'Escalate or split the work according to the recorded corrective decision.',
581
+ decisions: currentHardBlockingDecisions.map((event) => ({
582
+ eventId: event.eventId,
583
+ stepId: event.stepId,
584
+ decision: event.decision,
585
+ })),
586
+ });
587
+ }
588
+ if (task.status === 'blocked' && currentReplanDecisions.length === 0) {
589
+ 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.`, {
590
+ taskId: task.id,
591
+ status: task.status,
592
+ requiredAction: 'Resolve the blocked strict review or record a replan-required corrective decision first.',
593
+ });
594
+ }
595
+ if (currentContinueFixDecisions.length > 0) {
596
+ 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.`, {
597
+ taskId: task.id,
598
+ requiredAction: 'Run the guarded Step under the current Plan retry path, or escalate/cancel instead of issuing another plan-set at the same ordinal.',
599
+ decisions: currentContinueFixDecisions.map((event) => ({
600
+ eventId: event.eventId,
601
+ stepId: event.stepId,
602
+ triggeringAttemptCount: event.triggeringAttemptCount,
603
+ })),
604
+ });
605
+ }
606
+ const currentPlanRiskAudit = task.status === 'needs_fix'
607
+ ? readCurrentPlanRiskAudit(this.store, task)
608
+ : null;
609
+ const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(identity.projectId, taskId));
610
+ if (task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2) {
611
+ validateCorrectivePlanAudit(task, correctiveAudit, failedReviewAttempts);
612
+ }
613
+ const guardedRemediationGate = task.status === 'needs_fix' && currentPlanRiskAudit
614
+ ? findFailedGuardedStepRequiringCorrectiveDecision(this.store, task, currentPlanRiskAudit)
615
+ : null;
616
+ if (guardedRemediationGate && currentReplanDecisions.length === 0 && !correctiveAudit) {
617
+ throw new WorkflowError('TRANSITION_BLOCKED', `Guarded Step ${guardedRemediationGate.stepId} requires a corrective decision before another remediation run can be authorized.`, {
618
+ taskId: task.id,
619
+ stepId: guardedRemediationGate.stepId,
620
+ attemptCount: guardedRemediationGate.attemptCount,
621
+ requiredSidecar: 'corrective-decisions.jsonl',
622
+ allowedDecision: 'continue-fix',
623
+ escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
624
+ });
625
+ }
626
+ if (currentReplanDecisions.length > 0 && correctiveAudit) {
627
+ 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.`, {
628
+ taskId: task.id,
629
+ decision: correctiveAudit.decision,
630
+ requiredAction: 'Retry task plan-set without corrective-audit input; the recorded replan-required decision is the authorization for this recovery path.',
631
+ });
632
+ }
633
+ if (guardedRemediationGate && currentReplanDecisions.length === 0 && correctiveAudit?.decision !== 'continue-fix') {
634
+ 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.`, {
635
+ taskId: task.id,
636
+ stepId: guardedRemediationGate.stepId,
637
+ decision: correctiveAudit?.decision ?? null,
638
+ });
639
+ }
500
640
  for (const requirement of task.requirements) {
501
641
  if (!plan.requirements.includes(requirement)) {
502
642
  throw new WorkflowError('TRANSITION_BLOCKED', `Plan omits Task requirement ${requirement}.`);
@@ -525,7 +665,24 @@ export class WorkflowService {
525
665
  plannedSteps.every((step) => step.status === 'completed')) {
526
666
  throw new WorkflowError('TRANSITION_BLOCKED', 'A failed review requires at least one new or changed remediation Step.');
527
667
  }
528
- const saved = this.store.writeTask({
668
+ if (currentReplanRequiredStepIds.some((stepId) => !plannedSteps.some((step) => step.id === stepId))) {
669
+ 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.`, {
670
+ taskId: task.id,
671
+ requiredStepIds: currentReplanRequiredStepIds,
672
+ plannedStepIds: plannedSteps.map((step) => step.id),
673
+ });
674
+ }
675
+ if (guardedRemediationGate && currentReplanDecisions.length === 0 && !plannedSteps.some((step) => step.id === guardedRemediationGate.stepId)) {
676
+ throw new WorkflowError('TRANSITION_BLOCKED', `Continue-fix must retain guarded Step ${guardedRemediationGate.stepId}; removing or renaming it would reset the remediation breaker.`, {
677
+ taskId: task.id,
678
+ stepId: guardedRemediationGate.stepId,
679
+ });
680
+ }
681
+ if (correctiveAudit && guardedRemediationGate && currentReplanDecisions.length === 0 && currentPlanRiskAudit) {
682
+ assertCorrectiveAuditorIndependence(this.store, task, guardedRemediationGate.stepId, correctiveAudit.auditor);
683
+ appendCorrectiveDecisionEvent(this.store, task, guardedRemediationGate.stepId, correctiveAudit, currentPlanRiskAudit, stagedPlan.hash, this.now());
684
+ }
685
+ const candidate = {
529
686
  ...task,
530
687
  status: 'awaiting_execution_authorization',
531
688
  planHash: stagedPlan.hash,
@@ -545,8 +702,16 @@ export class WorkflowService {
545
702
  resultHash: null,
546
703
  evidenceHash: null,
547
704
  blockReason: null,
548
- }, expectedRevision);
705
+ };
706
+ if (planRiskAudit)
707
+ validatePlanRiskAuditCandidate(this.store, candidate, planRiskAudit);
708
+ const saved = this.store.writeTask(candidate, expectedRevision);
549
709
  this.store.publishArtifact(stagedPlan);
710
+ if (planRiskAudit)
711
+ appendCurrentPlanRiskAudit(this.store, saved, planRiskAudit, this.now());
712
+ if (correctiveAudit && task.status === 'needs_fix' && task.review.status === 'failed') {
713
+ appendCorrectivePlanAudit(this.store.taskRoot(identity.projectId, taskId), task, saved, failedReviewAttempts, correctiveAudit, this.now());
714
+ }
550
715
  return saved;
551
716
  }
552
717
  rebindTaskKnowledge(repository, taskId, expectedRevision) {
@@ -630,8 +795,62 @@ export class WorkflowService {
630
795
  this.store.publishArtifact(stagedPlan);
631
796
  return saved;
632
797
  }
798
+ refreshTaskContext(repository, taskId, expectedTaskRevision, expectedKnowledgeMapRevision, actor, delegationGrantId) {
799
+ if (!delegationGrantId) {
800
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task context refresh combines approval transitions and therefore requires an explicit delegated approval grant.');
801
+ }
802
+ const { identity } = this.context(repository);
803
+ const task = this.store.readTask(identity.projectId, taskId);
804
+ const currentMap = this.store.readKnowledgeMap(identity.projectId);
805
+ assertExpectedRevision(task, expectedTaskRevision);
806
+ assertExpectedRevision(currentMap, expectedKnowledgeMapRevision);
807
+ if (!taskCanRebindKnowledge(task)) {
808
+ throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while Task is ${task.status}.`);
809
+ }
810
+ const activeStep = task.steps.find((step) => step.status === 'in_progress');
811
+ if (activeStep) {
812
+ throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while ${activeStep.id} is in progress.`);
813
+ }
814
+ const inspectedMap = inspectKnowledgeMap(currentMap, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
815
+ const transitions = [];
816
+ const currentPlanRiskAudit = readCurrentPlanRiskAudit(this.store, task);
817
+ // Validate every delegated transition before the first write so a narrow grant cannot
818
+ // leave the composite operation half-applied.
819
+ this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
820
+ let knowledgeMap = currentMap;
821
+ if (inspectedMap.status !== 'active' || !inspectedMap.approval) {
822
+ if (currentMap.status !== 'active' || !currentMap.approval || inspectedMap.status !== 'stale') {
823
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with content-only drift.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
824
+ }
825
+ assertContentOnlyKnowledgeRefresh(currentMap, inspectedMap);
826
+ this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspectedMap);
827
+ knowledgeMap = this.reconcileKnowledgeMap(repository, currentMap.revision);
828
+ transitions.push('project-memory reconcile');
829
+ knowledgeMap = this.approveKnowledgeMap(repository, knowledgeMap.revision, [], actor, delegationGrantId);
830
+ transitions.push('project-memory approve');
831
+ }
832
+ const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
833
+ if (task.knowledgeMapRevision === knowledgeMap.revision
834
+ && task.knowledgeMapHash === knowledgeMapHash) {
835
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
836
+ }
837
+ let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
838
+ if (currentPlanRiskAudit) {
839
+ appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
840
+ }
841
+ transitions.push('task knowledge-rebind');
842
+ refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, 'Delegated content-only context refresh.', delegationGrantId);
843
+ transitions.push('task execution authorize');
844
+ return {
845
+ mode: 'delegated-content-only',
846
+ transitions,
847
+ knowledgeMap,
848
+ task: refreshedTask,
849
+ };
850
+ }
633
851
  authorizeTask(repository, taskId, expectedRevision, actor, reason = '', delegationGrantId = null) {
634
852
  const { identity } = this.context(repository);
853
+ this.readCurrentAdoptionPostureStrict(identity.projectId);
635
854
  const task = this.store.readTask(identity.projectId, taskId);
636
855
  assertExpectedRevision(task, expectedRevision);
637
856
  if (task.status !== 'awaiting_execution_authorization' || !task.planHash) {
@@ -641,6 +860,7 @@ export class WorkflowService {
641
860
  const root = this.store.taskRoot(identity.projectId, taskId);
642
861
  const briefHash = this.store.hashArtifact(root, 'brief.md');
643
862
  const planHash = this.store.hashArtifact(root, 'plan.md');
863
+ readCurrentPlanRiskAudit(this.store, task, { requireCurrentTaskRevision: true });
644
864
  const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
645
865
  const event = {
646
866
  kind: 'execution',
@@ -658,15 +878,164 @@ export class WorkflowService {
658
878
  };
659
879
  return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
660
880
  }
881
+ recordPlanRiskAudit(repository, taskId, expectedRevision, input) {
882
+ const { identity } = this.context(repository);
883
+ this.readCurrentAdoptionPostureStrict(identity.projectId);
884
+ const task = this.store.readTask(identity.projectId, taskId);
885
+ assertExpectedRevision(task, expectedRevision);
886
+ this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
887
+ return appendCurrentPlanRiskAudit(this.store, task, input, this.now());
888
+ }
889
+ showTaskHandoff(repository, taskId) {
890
+ const { identity } = this.context(repository);
891
+ const task = this.store.readTask(identity.projectId, taskId);
892
+ return {
893
+ task,
894
+ posture: readTaskC1Posture(this.store, task),
895
+ claimTokenAvailable: false,
896
+ };
897
+ }
898
+ handoffTask(repository, taskId, expectedRevision, actor, targetActor, reason, writerToken = null, grantId = null, expiresAt = null) {
899
+ const context = this.context(repository);
900
+ const task = this.store.readTask(context.identity.projectId, taskId);
901
+ assertExpectedRevision(task, expectedRevision);
902
+ const normalizedActor = actor.trim();
903
+ if (!normalizedActor) {
904
+ throw new WorkflowError('INVALID_ARGUMENT', 'Handoff actor is required.');
905
+ }
906
+ if (grantId)
907
+ this.assertHandoffGrant(context.identity.projectId, task, targetActor, grantId);
908
+ const milestone = task.milestoneId ? this.readMilestoneCurrent(task.projectId, task.milestoneId) : null;
909
+ const milestones = milestone ? this.listMilestonesCurrent(task.projectId) : [];
910
+ const milestoneDisplayNumber = milestone
911
+ ? milestones.findIndex((candidate) => candidate.id === milestone.id) + 1
912
+ : null;
913
+ const taskDisplayNumber = milestone
914
+ ? milestone.memberships.findIndex((membership) => membership.taskId === task.id) + 1
915
+ : null;
916
+ const currentLease = context.locks.inspect(taskId);
917
+ let lease;
918
+ let acquiredHere = false;
919
+ if (currentLease) {
920
+ if (currentLease.owner !== normalizedActor) {
921
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} active writer lease belongs to a different actor.`, {
922
+ taskId,
923
+ leaseOwner: currentLease.owner,
924
+ actor: normalizedActor,
925
+ });
926
+ }
927
+ if (writerToken === null) {
928
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} handoff requires the active writer token while a lease is present.`, {
929
+ taskId,
930
+ leaseOwner: currentLease.owner,
931
+ expiresAt: currentLease.expiresAt,
932
+ });
933
+ }
934
+ lease = context.locks.heartbeat(taskId, writerToken);
935
+ }
936
+ else {
937
+ if (writerToken !== null) {
938
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
939
+ taskId,
940
+ });
941
+ }
942
+ acquiredHere = true;
943
+ lease = context.locks.acquire(taskId, normalizedActor);
944
+ }
945
+ const releaseLease = () => context.locks.release(taskId, lease.token);
946
+ try {
947
+ const prepared = appendTaskHandoff(this.store, task, {
948
+ actor: normalizedActor,
949
+ targetActor,
950
+ reason,
951
+ writerToken: lease.token,
952
+ milestoneDisplayNumber,
953
+ taskDisplayNumber,
954
+ expectedNext: String(nextForTask(task).action ?? 'task show'),
955
+ grantId,
956
+ expiresAt: expiresAt ?? new Date(this.now().getTime() + 24 * 60 * 60 * 1000).toISOString(),
957
+ }, this.now());
958
+ releaseLease();
959
+ return { task, event: prepared.event, bundle: prepared.bundle };
960
+ }
961
+ catch (error) {
962
+ if (acquiredHere)
963
+ releaseLease();
964
+ throw error;
965
+ }
966
+ }
967
+ claimTask(repository, taskId, expectedRevision, actor, claimToken, writerToken = null) {
968
+ const context = this.context(repository);
969
+ const task = this.store.readTask(context.identity.projectId, taskId);
970
+ assertExpectedRevision(task, expectedRevision);
971
+ const normalizedActor = actor.trim();
972
+ if (!normalizedActor) {
973
+ throw new WorkflowError('INVALID_ARGUMENT', 'Claim actor is required.');
974
+ }
975
+ const pendingHandoff = readTaskC1Posture(this.store, task);
976
+ if (pendingHandoff.state === 'pending' && pendingHandoff.latestEvent.grantId) {
977
+ this.assertHandoffGrant(context.identity.projectId, task, normalizedActor, pendingHandoff.latestEvent.grantId);
978
+ }
979
+ const existingLease = context.locks.inspect(taskId);
980
+ let lease;
981
+ let acquiredHere = false;
982
+ if (existingLease) {
983
+ if (existingLease.owner !== normalizedActor) {
984
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} existing writer lease belongs to a different actor.`, {
985
+ taskId,
986
+ leaseOwner: existingLease.owner,
987
+ actor: normalizedActor,
988
+ });
989
+ }
990
+ if (writerToken === null) {
991
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} claim requires the existing writer token.`, {
992
+ taskId,
993
+ leaseOwner: existingLease.owner,
994
+ });
995
+ }
996
+ lease = context.locks.heartbeat(taskId, writerToken);
997
+ }
998
+ else {
999
+ if (writerToken !== null) {
1000
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
1001
+ taskId,
1002
+ });
1003
+ }
1004
+ acquiredHere = true;
1005
+ lease = context.locks.acquire(taskId, normalizedActor);
1006
+ }
1007
+ try {
1008
+ const event = appendTaskClaim(this.store, task, actor, claimToken, lease.token, this.now());
1009
+ return { task, event, lease };
1010
+ }
1011
+ catch (error) {
1012
+ if (acquiredHere)
1013
+ context.locks.release(taskId, lease.token);
1014
+ throw error;
1015
+ }
1016
+ }
1017
+ handbackTask(repository, taskId, expectedRevision, actor, reason, writerToken = null, limitations = [], recommendedNext = 'Coordinator reviews the terminal Task result and recorded evidence.') {
1018
+ const context = this.context(repository);
1019
+ const task = this.store.readTask(context.identity.projectId, taskId);
1020
+ assertExpectedRevision(task, expectedRevision);
1021
+ const lease = task.status === 'merged' || task.status === 'cancelled'
1022
+ ? context.locks.inspect(taskId)
1023
+ : this.proveWriterLeaseTokenForHandoff(context, taskId, writerToken);
1024
+ const event = appendTaskHandback(this.store, task, actor, reason, lease?.token ?? writerToken, limitations, recommendedNext, this.now());
1025
+ if (lease)
1026
+ context.locks.release(taskId, lease.token);
1027
+ return { task, event };
1028
+ }
661
1029
  startTask(repository, taskId, expectedRevision, workspaceOwner) {
662
1030
  const { identity } = this.context(repository);
663
1031
  const task = this.store.readTask(identity.projectId, taskId);
664
1032
  assertExpectedRevision(task, expectedRevision);
1033
+ this.requireExecutionAdoptionPosture(identity, taskId);
665
1034
  if (task.status !== 'ready')
666
1035
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is ${task.status}.`);
667
1036
  this.assertExecutionAuthorizationFresh(task);
668
1037
  if (task.milestoneId) {
669
- const milestone = this.store.readMilestone(identity.projectId, task.milestoneId);
1038
+ const milestone = this.readMilestoneCurrent(identity.projectId, task.milestoneId);
670
1039
  const membership = milestone.memberships.find((candidate) => candidate.taskId === taskId);
671
1040
  if (milestone.status !== 'active' || membership?.disposition !== 'required') {
672
1041
  throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task requires an active, authorized membership.', {
@@ -675,6 +1044,15 @@ export class WorkflowService {
675
1044
  disposition: membership?.disposition ?? null,
676
1045
  });
677
1046
  }
1047
+ const handoffPosture = readTaskC1Posture(this.store, task);
1048
+ if (handoffPosture.state !== 'claimed') {
1049
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone-linked Task start requires a prepared and claimed C1 handoff bundle.', {
1050
+ taskId: task.id,
1051
+ milestoneId: milestone.id,
1052
+ c1Posture: handoffPosture.state,
1053
+ requiredAction: handoffPosture.state === 'pending' ? 'Claim the prepared handoff.' : 'Prepare and claim a C1 handoff.',
1054
+ });
1055
+ }
678
1056
  }
679
1057
  if (!isClean(identity.repositoryRoot)) {
680
1058
  throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Task start requires a clean checkout.', {
@@ -700,20 +1078,48 @@ export class WorkflowService {
700
1078
  }
701
1079
  return this.store.writeTask({ ...task, baseCommit, taskBranch, workspaceOwner, blockReason: null }, expectedRevision);
702
1080
  }
703
- runStep(repository, taskId, stepId, expectedRevision, owner, writerToken) {
1081
+ runStep(repository, taskId, stepId, expectedRevision, actor, writerToken) {
704
1082
  const context = this.context(repository);
705
1083
  const task = this.store.readTask(context.identity.projectId, taskId);
706
1084
  assertExpectedRevision(task, expectedRevision);
1085
+ this.requireExecutionAdoptionPosture(context.identity, taskId);
707
1086
  if (!['ready', 'in_progress', 'needs_fix'].includes(task.status)) {
708
1087
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run a Step while ${task.status}.`);
709
1088
  }
710
1089
  this.assertExecutionAuthorizationFresh(task);
711
1090
  const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1091
+ const currentPlanBlockingDecisions = readCorrectiveDecisionEvents(this.store, task)
1092
+ .filter((event) => event.planHash === task.planHash && event.decision !== 'continue-fix');
1093
+ if (currentPlanBlockingDecisions.length > 0) {
1094
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run any Step while the current Plan has active blocking corrective decisions.`, {
1095
+ taskId,
1096
+ decisions: currentPlanBlockingDecisions.map((event) => ({
1097
+ eventId: event.eventId,
1098
+ stepId: event.stepId,
1099
+ decision: event.decision,
1100
+ planHash: event.planHash,
1101
+ triggeringAttemptCount: event.triggeringAttemptCount,
1102
+ })),
1103
+ });
1104
+ }
712
1105
  assertTaskBranch(context.identity.repositoryRoot, task);
713
1106
  const step = requireStep(task, stepId);
1107
+ const remediationHistoryForStep = readRemediationEvents(this.store, task).filter((event) => event.stepId === stepId);
714
1108
  if (!['planned', 'failed'].includes(step.status)) {
715
1109
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} cannot start from ${step.status}.`);
716
1110
  }
1111
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
1112
+ if (remediationHistoryForStep.length > 0 && (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds))) {
1113
+ 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.`, {
1114
+ taskId,
1115
+ stepId,
1116
+ remediationAttemptCount: remediationHistoryForStep.length,
1117
+ currentAuditPresent: Boolean(planRiskAudit),
1118
+ });
1119
+ }
1120
+ if (planRiskAudit && isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
1121
+ assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
1122
+ }
717
1123
  const dirty = changedFiles(context.identity.repositoryRoot);
718
1124
  if (step.status === 'planned' && dirty.length > 0) {
719
1125
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
@@ -732,18 +1138,41 @@ export class WorkflowService {
732
1138
  incompleteDependencies,
733
1139
  });
734
1140
  }
1141
+ const handoffPosture = readTaskC1Posture(this.store, task);
1142
+ if (handoffPosture.state === 'pending') {
1143
+ this.assertTaskMutationAllowedByHandoff(task, actor, null);
1144
+ }
735
1145
  const existing = context.locks.inspect(taskId);
736
1146
  let acquiredHere = false;
737
- const lease = existing
738
- ? writerToken
739
- ? context.locks.heartbeat(taskId, writerToken)
1147
+ let lease;
1148
+ if (handoffPosture.state === 'claimed') {
1149
+ if (!existing) {
1150
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} claimed C1 posture requires an active writer lease before task run.`, {
1151
+ taskId,
1152
+ claimant: handoffPosture.claimant,
1153
+ });
1154
+ }
1155
+ if (!writerToken) {
1156
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} claimed C1 posture requires the bound writer token for task run.`, {
1157
+ taskId,
1158
+ claimant: handoffPosture.claimant,
1159
+ });
1160
+ }
1161
+ lease = context.locks.heartbeat(taskId, writerToken);
1162
+ this.assertTaskMutationAllowedByHandoff(task, actor, lease.token);
1163
+ }
1164
+ else {
1165
+ lease = existing
1166
+ ? writerToken
1167
+ ? context.locks.heartbeat(taskId, writerToken)
1168
+ : (() => {
1169
+ throw new WorkflowError('LOCKED', `${taskId} already has a writer.`, { owner: existing.owner });
1170
+ })()
740
1171
  : (() => {
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
- })();
1172
+ acquiredHere = true;
1173
+ return context.locks.acquire(taskId, actor.trim() || 'worker');
1174
+ })();
1175
+ }
747
1176
  const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'in_progress' } : candidate);
748
1177
  let saved;
749
1178
  try {
@@ -761,7 +1190,7 @@ export class WorkflowService {
761
1190
  protocolVersion: PROTOCOL_VERSION,
762
1191
  packageVersion: PACKAGE_VERSION,
763
1192
  role: 'worker',
764
- objective: step.objective,
1193
+ objective: `${step.objective} Leave all Step changes uncommitted; Workflow core creates the atomic commit through task step-complete.`,
765
1194
  inputs: [
766
1195
  path.join(this.store.taskRoot(task.projectId, task.id), 'brief.md'),
767
1196
  path.join(this.store.taskRoot(task.projectId, task.id), 'plan.md'),
@@ -769,7 +1198,10 @@ export class WorkflowService {
769
1198
  allowedReads: [identityRelative(context.identity.repositoryRoot, '.')],
770
1199
  allowedWrites: step.allowedWrites,
771
1200
  commands: step.checks,
772
- forbiddenScope: step.forbiddenScope,
1201
+ forbiddenScope: [
1202
+ ...step.forbiddenScope,
1203
+ 'Manual Git staging, commits, amendments, resets, rebases, or other history mutations. Workflow core owns the Step commit.',
1204
+ ],
773
1205
  resultSchema: {
774
1206
  type: 'object',
775
1207
  required: ['summary', 'changedFiles', 'checks'],
@@ -783,6 +1215,7 @@ export class WorkflowService {
783
1215
  'Required change falls outside allowedWrites.',
784
1216
  'Implementation requires changing Brief, Plan, or acceptance.',
785
1217
  'A configured check cannot be run or interpreted safely.',
1218
+ 'Do not run git add or git commit. Return uncommitted changes to the coordinator, which must invoke task step-complete.',
786
1219
  ],
787
1220
  contextBudget: 'medium',
788
1221
  taskId,
@@ -801,6 +1234,14 @@ export class WorkflowService {
801
1234
  ? this.requireActiveKnowledgeMap(identity.repositoryRoot, identity.projectId)
802
1235
  : this.assertTaskKnowledgeBinding(identity.repositoryRoot, task);
803
1236
  const root = this.store.taskRoot(task.projectId, task.id);
1237
+ const planRiskAudit = task.planHash && !['planning', 'merged', 'cancelled'].includes(task.status)
1238
+ ? readCurrentPlanRiskAudit(this.store, task)
1239
+ : null;
1240
+ const planRiskReviewScope = planRiskAudit
1241
+ ? planRiskAudit.reviewRequiredStepIds.length > 0
1242
+ ? ` Current Plan Risk Audit marks ${planRiskAudit.reviewRequiredStepIds.join(', ')} for strict review.`
1243
+ : ' Current Plan Risk Audit marks no Steps for strict review.'
1244
+ : '';
804
1245
  const common = {
805
1246
  protocolVersion: PROTOCOL_VERSION,
806
1247
  packageVersion: PACKAGE_VERSION,
@@ -822,17 +1263,25 @@ export class WorkflowService {
822
1263
  if (role === 'technical-planner') {
823
1264
  return {
824
1265
  ...common,
825
- objective: `Produce an execution-ready Plan for ${task.title}.`,
1266
+ 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 in the original Plan.${planRiskReviewScope}`,
826
1267
  resultSchema: {
827
1268
  type: 'object',
828
1269
  required: ['objective', 'requirements', 'acceptance', 'risks', 'steps'],
829
1270
  },
1271
+ stopConditions: [
1272
+ ...common.stopConditions,
1273
+ 'A migration is planned without a populated forward-upgrade check.',
1274
+ 'Concurrency or cursor/state ownership is planned without deterministic race checks.',
1275
+ 'Restart, replay, or crash recovery is claimed without restart/crash-window evidence.',
1276
+ 'An external provider is used without failure-injection and hidden-default verification.',
1277
+ 'The safe risk slice crosses persistence, recovery orchestration, and executable composition and should be split.',
1278
+ ],
830
1279
  };
831
1280
  }
832
1281
  if (role === 'independent-reviewer') {
833
1282
  return {
834
1283
  ...common,
835
- objective: `Review ${task.title} against Brief, Plan, diff, checks, and evidence without modifying files.`,
1284
+ objective: `Review ${task.title} against Brief, Plan, diff, checks, and evidence without modifying files.${planRiskReviewScope}`,
836
1285
  inputs: [...common.inputs, ...(task.evidenceHash ? [path.join(root, 'evidence.json')] : [])],
837
1286
  resultSchema: {
838
1287
  type: 'object',
@@ -842,28 +1291,65 @@ export class WorkflowService {
842
1291
  ...common.stopConditions,
843
1292
  'The checkout is dirty or the evidence does not identify the reviewed commit.',
844
1293
  'Read-only reviewer isolation cannot be established; return unverified.',
1294
+ 'High-risk behavior lacks populated-upgrade, race, restart/replay, crash-window, or provider-failure evidence required by the Plan.',
845
1295
  ],
846
1296
  };
847
1297
  }
848
1298
  return {
849
1299
  ...common,
850
- objective: `Coordinate the next valid state transition for ${task.title}.`,
1300
+ objective: `Coordinate the next valid state transition for ${task.title}.${planRiskReviewScope}`,
851
1301
  resultSchema: { type: 'object', required: ['recommendedAction', 'reason'] },
852
1302
  };
853
1303
  }
854
- completeStep(repository, taskId, stepId, expectedRevision, writerToken, notes) {
1304
+ completeStep(repository, taskId, stepId, expectedRevision, writerToken, notes, actor) {
855
1305
  const context = this.context(repository);
856
- context.locks.heartbeat(taskId, writerToken);
1306
+ const lease = context.locks.heartbeat(taskId, writerToken);
1307
+ this.readCurrentAdoptionPostureStrict(context.identity.projectId);
857
1308
  const task = this.store.readTask(context.identity.projectId, taskId);
858
1309
  assertExpectedRevision(task, expectedRevision);
1310
+ this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
859
1311
  assertTaskBranch(context.identity.repositoryRoot, task);
860
1312
  const step = requireStep(task, stepId);
861
1313
  if (task.status !== 'in_progress' || step.status !== 'in_progress') {
862
1314
  throw new WorkflowError('TRANSITION_BLOCKED', `${stepId} is not in progress.`);
863
1315
  }
1316
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
1317
+ const reviewRequired = planRiskAudit
1318
+ ? isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)
1319
+ : false;
1320
+ if (reviewRequired && step.evidence) {
1321
+ validateTaskHistory(context.identity.repositoryRoot, task);
1322
+ if (!isClean(context.identity.repositoryRoot)) {
1323
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review recovery requires a clean Task checkout.', {
1324
+ changedFiles: changedFiles(context.identity.repositoryRoot),
1325
+ });
1326
+ }
1327
+ const currentHead = headCommit(context.identity.repositoryRoot);
1328
+ if (currentHead !== step.evidence.commitSha) {
1329
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review recovery requires HEAD to remain at the recorded completion commit.', {
1330
+ stepId,
1331
+ expectedCommit: step.evidence.commitSha,
1332
+ actualCommit: currentHead,
1333
+ });
1334
+ }
1335
+ const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
1336
+ if (!currentCycle.pendingEvent && !currentCycle.latestMatchingEvent) {
1337
+ ensurePendingStrictStepReview(this.store, task, stepId, this.now());
1338
+ }
1339
+ return task;
1340
+ }
1341
+ validateTaskHistory(context.identity.repositoryRoot, task);
864
1342
  const checks = runChecks(context.identity.repositoryRoot, step.checks);
865
1343
  const failed = checks.filter((check) => !check.passed);
866
1344
  if (failed.length > 0) {
1345
+ if (reviewRequired && planRiskAudit) {
1346
+ appendRemediationEvent(this.store, task, stepId, 'checks-failed', {
1347
+ completionCommit: headCommit(context.identity.repositoryRoot),
1348
+ checksHash: sha256Hex(canonicalJsonStringify(checks)),
1349
+ reviewEventHash: null,
1350
+ reviewerAttestationHash: null,
1351
+ }, planRiskAudit, this.now());
1352
+ }
867
1353
  const steps = task.steps.map((candidate) => candidate.id === stepId ? { ...candidate, status: 'failed' } : candidate);
868
1354
  return this.store.writeTask({ ...task, status: 'needs_fix', steps, blockReason: `${stepId} checks failed.` }, expectedRevision);
869
1355
  }
@@ -875,6 +1361,18 @@ export class WorkflowService {
875
1361
  checks,
876
1362
  notes,
877
1363
  };
1364
+ if (reviewRequired) {
1365
+ const saved = this.store.writeTask({
1366
+ ...task,
1367
+ status: 'in_progress',
1368
+ steps: task.steps.map((candidate) => candidate.id === stepId
1369
+ ? { ...candidate, status: 'in_progress', evidence }
1370
+ : candidate),
1371
+ blockReason: null,
1372
+ }, expectedRevision);
1373
+ ensurePendingStrictStepReview(this.store, saved, stepId, this.now());
1374
+ return saved;
1375
+ }
878
1376
  const completedIds = new Set([
879
1377
  ...task.steps.filter((candidate) => candidate.status === 'completed').map((candidate) => candidate.id),
880
1378
  stepId,
@@ -889,11 +1387,111 @@ export class WorkflowService {
889
1387
  });
890
1388
  return this.store.writeTask({ ...task, status: 'in_progress', steps, blockReason: null }, expectedRevision);
891
1389
  }
892
- submitTask(repository, taskId, expectedRevision, writerToken) {
1390
+ reviewStep(repository, taskId, stepId, expectedRevision, runner, actor, writerToken = null) {
893
1391
  const context = this.context(repository);
894
- context.locks.heartbeat(taskId, writerToken);
1392
+ this.readCurrentAdoptionPostureStrict(context.identity.projectId);
895
1393
  const task = this.store.readTask(context.identity.projectId, taskId);
896
1394
  assertExpectedRevision(task, expectedRevision);
1395
+ this.proveOptionalWriterLeaseToken(context, taskId, writerToken);
1396
+ this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
1397
+ if (task.status !== 'in_progress' && task.status !== 'blocked') {
1398
+ throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review cannot run while task ${taskId} is ${task.status}.`);
1399
+ }
1400
+ assertTaskBranch(context.identity.repositoryRoot, task);
1401
+ this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1402
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
1403
+ if (!planRiskAudit || !isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
1404
+ throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not marked for strict review by the current Plan Risk Audit.`, {
1405
+ taskId: task.id,
1406
+ stepId,
1407
+ });
1408
+ }
1409
+ const step = requireStep(task, stepId);
1410
+ if (step.status !== 'in_progress' || !step.evidence) {
1411
+ throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review requires ${stepId} to remain in progress with canonical evidence.`, {
1412
+ taskId: task.id,
1413
+ stepId,
1414
+ stepStatus: step.status,
1415
+ });
1416
+ }
1417
+ validateTaskHistory(context.identity.repositoryRoot, task);
1418
+ if (!isClean(context.identity.repositoryRoot)) {
1419
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review requires a clean Task checkout.', {
1420
+ changedFiles: changedFiles(context.identity.repositoryRoot),
1421
+ });
1422
+ }
1423
+ const currentHead = headCommit(context.identity.repositoryRoot);
1424
+ if (currentHead !== step.evidence.commitSha) {
1425
+ throw new WorkflowError('GIT_PRECONDITION_FAILED', 'Strict Step Review requires HEAD to remain at the recorded completion commit.', {
1426
+ taskId: task.id,
1427
+ stepId,
1428
+ expectedCommit: step.evidence.commitSha,
1429
+ actualCommit: currentHead,
1430
+ });
1431
+ }
1432
+ const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
1433
+ if (task.status === 'blocked') {
1434
+ if (currentCycle.resolution !== 'unverified') {
1435
+ throw new WorkflowError('TRANSITION_BLOCKED', `Strict Step Review retry requires ${stepId} to be blocked by an unverified prior cycle.`, {
1436
+ taskId: task.id,
1437
+ stepId,
1438
+ resolution: currentCycle.resolution,
1439
+ });
1440
+ }
1441
+ assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit);
1442
+ }
1443
+ else if (currentCycle.resolution !== 'pending') {
1444
+ if (currentCycle.resolution === 'failed' || currentCycle.resolution === 'unverified') {
1445
+ if (!currentCycle.reviewRecordedEvent || !currentCycle.reviewerAttestationEvent) {
1446
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review recovery requires exact review and attestation sidecars.', {
1447
+ taskId: task.id,
1448
+ stepId,
1449
+ resolution: currentCycle.resolution,
1450
+ });
1451
+ }
1452
+ appendRemediationEvent(this.store, task, stepId, currentCycle.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
1453
+ completionCommit: currentCycle.reviewRecordedEvent.completionCommit,
1454
+ checksHash: currentCycle.reviewRecordedEvent.checksHash,
1455
+ reviewEventHash: currentCycle.reviewRecordedEvent.eventHash,
1456
+ reviewerAttestationHash: currentCycle.reviewerAttestationEvent.eventHash,
1457
+ }, planRiskAudit, this.now());
1458
+ }
1459
+ return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, currentCycle.resolution), expectedRevision);
1460
+ }
1461
+ ensurePendingStrictStepReview(this.store, task, stepId, this.now());
1462
+ const envelope = this.taskContext(repository, taskId, 'independent-reviewer');
1463
+ const launch = launchStrictStepReviewer(context.identity.repositoryRoot, task, this.store.taskRoot(task.projectId, task.id), envelope, stepId, step.evidence, runner);
1464
+ const recorded = recordStrictStepReview(this.store, task, stepId, {
1465
+ review: launch.review,
1466
+ reviewerOutputHash: launch.reviewerOutputHash,
1467
+ reviewedCommit: launch.reviewedCommit,
1468
+ isolationResult: launch.verifiedIsolation ? 'verified' : 'unverified',
1469
+ }, this.now());
1470
+ if (recorded.resolution === 'failed' || recorded.resolution === 'unverified') {
1471
+ const reviewRecordedEvent = recorded.reviewRecordedEvent;
1472
+ const reviewerAttestationEvent = recorded.reviewerAttestationEvent;
1473
+ if (!reviewRecordedEvent || !reviewerAttestationEvent) {
1474
+ throw new WorkflowError('STATE_CORRUPT', 'Strict Step Review remediation recording requires review and attestation sidecars.', {
1475
+ taskId: task.id,
1476
+ stepId,
1477
+ resolution: recorded.resolution,
1478
+ });
1479
+ }
1480
+ appendRemediationEvent(this.store, task, stepId, recorded.resolution === 'failed' ? 'review-failed' : 'review-unverified', {
1481
+ completionCommit: reviewRecordedEvent.completionCommit,
1482
+ checksHash: reviewRecordedEvent.checksHash,
1483
+ reviewEventHash: reviewRecordedEvent.eventHash,
1484
+ reviewerAttestationHash: reviewerAttestationEvent.eventHash,
1485
+ }, planRiskAudit, this.now());
1486
+ }
1487
+ return this.store.writeTask(applyStrictStepReviewResolution(task, stepId, recorded.resolution, launch.review.summary), expectedRevision);
1488
+ }
1489
+ submitTask(repository, taskId, expectedRevision, writerToken, actor) {
1490
+ const context = this.context(repository);
1491
+ const lease = context.locks.heartbeat(taskId, writerToken);
1492
+ const task = this.store.readTask(context.identity.projectId, taskId);
1493
+ assertExpectedRevision(task, expectedRevision);
1494
+ this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
897
1495
  assertTaskBranch(context.identity.repositoryRoot, task);
898
1496
  if (!['in_progress', 'ready'].includes(task.status)) {
899
1497
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot be submitted while ${task.status}.`);
@@ -920,10 +1518,13 @@ export class WorkflowService {
920
1518
  this.store.publishArtifact(stagedEvidence);
921
1519
  return saved;
922
1520
  }
923
- recordReview(repository, taskId, expectedRevision, review) {
1521
+ recordReview(repository, taskId, expectedRevision, review, actor, writerToken = null) {
924
1522
  const { identity } = this.context(repository);
1523
+ this.readCurrentAdoptionPostureStrict(identity.projectId);
925
1524
  const task = this.store.readTask(identity.projectId, taskId);
926
1525
  assertExpectedRevision(task, expectedRevision);
1526
+ this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
1527
+ this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
927
1528
  const retryingUnverified = task.status === 'blocked' && task.review.status === 'unverified';
928
1529
  if (task.status !== 'validating' && !retryingUnverified) {
929
1530
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not ready for review.`);
@@ -947,7 +1548,11 @@ export class WorkflowService {
947
1548
  if (recorded.findings.length > 0) {
948
1549
  const file = path.join(this.store.taskRoot(task.projectId, task.id), 'findings.jsonl');
949
1550
  for (const finding of recorded.findings) {
950
- appendFileSync(file, `${JSON.stringify({ ...finding, recordedAt: recorded.recordedAt })}\n`, {
1551
+ appendFileSync(file, `${JSON.stringify({
1552
+ ...finding,
1553
+ reviewer: recorded.reviewer,
1554
+ recordedAt: recorded.recordedAt,
1555
+ })}\n`, {
951
1556
  encoding: 'utf8',
952
1557
  mode: 0o600,
953
1558
  });
@@ -955,6 +1560,59 @@ export class WorkflowService {
955
1560
  }
956
1561
  return saved;
957
1562
  }
1563
+ recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
1564
+ const { identity } = this.context(repository);
1565
+ this.readCurrentAdoptionPostureStrict(identity.projectId);
1566
+ const task = this.store.readTask(identity.projectId, taskId);
1567
+ assertExpectedRevision(task, expectedRevision);
1568
+ this.proveOptionalWriterLeaseToken(this.context(repository), taskId, writerToken);
1569
+ this.assertTaskMutationAllowedByHandoff(task, actor ?? null, writerToken);
1570
+ if (task.status !== 'needs_fix' && task.status !== 'blocked') {
1571
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot record a corrective decision while ${task.status}.`);
1572
+ }
1573
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
1574
+ if (!planRiskAudit) {
1575
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no current Plan Risk Audit for corrective decision recording.`, {
1576
+ taskId,
1577
+ stepId,
1578
+ });
1579
+ }
1580
+ if (!task.planHash) {
1581
+ throw new WorkflowError('STATE_CORRUPT', `Task ${taskId} has no current Plan hash for corrective decision recording.`, {
1582
+ taskId,
1583
+ stepId,
1584
+ });
1585
+ }
1586
+ const step = requireStep(task, stepId);
1587
+ if (!isStepReviewRequired(task, stepId, planRiskAudit.reviewRequiredStepIds)) {
1588
+ throw new WorkflowError('TRANSITION_BLOCKED', `Step ${stepId} is not guarded by the current Plan Risk Audit and cannot receive a corrective decision.`, {
1589
+ taskId,
1590
+ stepId,
1591
+ });
1592
+ }
1593
+ if (task.status === 'needs_fix') {
1594
+ if (step.status !== 'failed') {
1595
+ throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a currently failed guarded Step while task ${taskId} is needs_fix.`, {
1596
+ taskId,
1597
+ stepId,
1598
+ stepStatus: step.status,
1599
+ });
1600
+ }
1601
+ }
1602
+ else {
1603
+ const currentCycle = buildCurrentStrictStepReviewCycle(this.store, task, stepId);
1604
+ if (step.status !== 'in_progress' || !step.evidence || currentCycle.resolution !== 'unverified') {
1605
+ throw new WorkflowError('TRANSITION_BLOCKED', `Corrective decision for ${stepId} requires a blocked unverified guarded Step with current evidence.`, {
1606
+ taskId,
1607
+ stepId,
1608
+ stepStatus: step.status,
1609
+ resolution: currentCycle.resolution,
1610
+ });
1611
+ }
1612
+ }
1613
+ assertCorrectiveAuditorIndependence(this.store, task, stepId, correctiveAudit.auditor);
1614
+ return appendCorrectiveDecisionEvent(this.store, task, stepId, correctiveAudit, planRiskAudit, task.planHash, this.now());
1615
+ }
958
1616
  setTaskResult(repository, taskId, expectedRevision, summary, limitations) {
959
1617
  const { identity } = this.context(repository);
960
1618
  const task = this.store.readTask(identity.projectId, taskId);
@@ -1059,10 +1717,12 @@ export class WorkflowService {
1059
1717
  this.store.publishArtifact(stagedEvidence);
1060
1718
  return saved;
1061
1719
  }
1062
- mergeTask(repository, taskId, expectedRevision, writerToken) {
1720
+ mergeTask(repository, taskId, expectedRevision, writerToken, actor) {
1063
1721
  const context = this.context(repository);
1064
1722
  const task = this.store.readTask(context.identity.projectId, taskId);
1065
1723
  assertExpectedRevision(task, expectedRevision);
1724
+ const lease = context.locks.heartbeat(taskId, writerToken);
1725
+ this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
1066
1726
  if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.taskBranch || !task.baseCommit) {
1067
1727
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not ready for a local merge.`);
1068
1728
  }
@@ -1079,10 +1739,12 @@ export class WorkflowService {
1079
1739
  context.locks.release(taskId, writerToken);
1080
1740
  return saved;
1081
1741
  }
1082
- confirmExternalMerge(repository, taskId, expectedRevision, writerToken) {
1742
+ confirmExternalMerge(repository, taskId, expectedRevision, writerToken, actor) {
1083
1743
  const context = this.context(repository);
1084
1744
  const task = this.store.readTask(context.identity.projectId, taskId);
1085
1745
  assertExpectedRevision(task, expectedRevision);
1746
+ const lease = context.locks.heartbeat(taskId, writerToken);
1747
+ this.assertTaskMutationAllowedByHandoff(task, actor ?? null, lease.token);
1086
1748
  if (task.status !== 'ready_to_merge' || task.workspaceOwner !== 'external') {
1087
1749
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} is not awaiting external merge confirmation.`);
1088
1750
  }
@@ -1096,12 +1758,91 @@ export class WorkflowService {
1096
1758
  }
1097
1759
  status(repository) {
1098
1760
  const { identity } = this.context(repository);
1761
+ const tasks = this.store.listTasks(identity.projectId);
1762
+ const rawMilestones = this.store.listMilestones(identity.projectId);
1763
+ const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones);
1099
1764
  return {
1100
1765
  projectId: identity.projectId,
1101
1766
  discoveries: this.store.listDiscoveries(identity.projectId),
1102
- tasks: this.store.listTasks(identity.projectId),
1103
- milestones: this.store.listMilestones(identity.projectId),
1767
+ tasks,
1768
+ milestones: this.listMilestonesCurrent(identity.projectId, adoption.currentPosture),
1104
1769
  delegations: this.store.listDelegations(identity.projectId),
1770
+ adoption,
1771
+ };
1772
+ }
1773
+ recoverMilestoneTransactions(repository) {
1774
+ const { identity } = this.context(repository);
1775
+ const milestones = this.listMilestonesCurrent(identity.projectId, readCurrentAdoptionPosture(this.store, identity.projectId));
1776
+ return {
1777
+ projectId: identity.projectId,
1778
+ recoveredMilestoneIds: milestones.map((milestone) => milestone.id),
1779
+ };
1780
+ }
1781
+ prepareAdoption(repository) {
1782
+ const { identity } = this.context(repository);
1783
+ return this.inspectAdoptionPreparation(identity, this.store.listTasks(identity.projectId), this.store.listMilestones(identity.projectId));
1784
+ }
1785
+ applyAdoption(repository, actor, confirmationCode) {
1786
+ const context = this.context(repository);
1787
+ const tasks = context.store.listTasks(context.identity.projectId);
1788
+ const milestones = context.store.listMilestones(context.identity.projectId);
1789
+ return applyAdoptionPosture(this.store, {
1790
+ projectId: context.identity.projectId,
1791
+ repositoryIdentity: context.identity,
1792
+ packageVersion: PACKAGE_VERSION,
1793
+ protocolVersion: PROTOCOL_VERSION,
1794
+ stateSchemaVersion: STATE_SCHEMA_VERSION,
1795
+ tasks,
1796
+ milestones,
1797
+ sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
1798
+ blockers: this.computeAdoptionBoundaryBlockers(context.identity, tasks, context.locks.list()),
1799
+ now: this.now(),
1800
+ }, actor, confirmationCode);
1801
+ }
1802
+ updatePreflight(repository) {
1803
+ const context = this.context(repository);
1804
+ const tasks = context.store.listTasks(context.identity.projectId);
1805
+ const milestones = context.store.listMilestones(context.identity.projectId);
1806
+ const dirty = changedFiles(context.identity.repositoryRoot);
1807
+ const runningSteps = tasks.flatMap((task) => task.steps
1808
+ .filter((step) => step.status === 'in_progress')
1809
+ .map((step) => ({ taskId: task.id, stepId: step.id })));
1810
+ const leases = context.locks.list();
1811
+ const now = this.now().getTime();
1812
+ const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
1813
+ const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
1814
+ const blockers = [
1815
+ ...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
1816
+ ...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
1817
+ ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
1818
+ ...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
1819
+ ];
1820
+ const adoption = buildAdoptionPreparation({
1821
+ projectId: context.identity.projectId,
1822
+ repositoryIdentity: context.identity,
1823
+ packageVersion: PACKAGE_VERSION,
1824
+ protocolVersion: PROTOCOL_VERSION,
1825
+ stateSchemaVersion: STATE_SCHEMA_VERSION,
1826
+ tasks,
1827
+ milestones,
1828
+ sidecarInventory: this.collectAdoptionSidecarInventory(context.identity.projectId, tasks, milestones),
1829
+ blockers,
1830
+ now: this.now(),
1831
+ }, readCurrentAdoptionPosture(this.store, context.identity.projectId));
1832
+ return {
1833
+ safe: blockers.length === 0,
1834
+ packageVersion: PACKAGE_VERSION,
1835
+ protocolVersion: PROTOCOL_VERSION,
1836
+ stateSchemaVersion: STATE_SCHEMA_VERSION,
1837
+ projectId: context.identity.projectId,
1838
+ repositoryRoot: context.identity.repositoryRoot,
1839
+ clean: dirty.length === 0,
1840
+ changedFiles: dirty,
1841
+ runningSteps,
1842
+ activeLeases,
1843
+ staleLeases,
1844
+ blockers,
1845
+ adoption,
1105
1846
  };
1106
1847
  }
1107
1848
  scanKnowledgeMap(repository) {
@@ -1177,12 +1918,35 @@ export class WorkflowService {
1177
1918
  }
1178
1919
  next(repository) {
1179
1920
  const status = this.status(repository);
1921
+ const adoptionInfo = {
1922
+ status: status.adoption.status,
1923
+ safe: status.adoption.safe,
1924
+ blockers: status.adoption.blockers,
1925
+ baselineDigestHash: status.adoption.baselineDigestHash,
1926
+ sidecarBaselineHash: status.adoption.sidecarBaselineHash,
1927
+ confirmationCodeBindingHash: status.adoption.confirmationCodeBindingHash,
1928
+ confirmationCode: status.adoption.confirmationCode,
1929
+ currentEventType: status.adoption.currentPosture?.eventType ?? null,
1930
+ };
1931
+ if (status.adoption.status !== 'applied') {
1932
+ return {
1933
+ scope: 'project',
1934
+ projectId: status.projectId,
1935
+ status: status.adoption.status,
1936
+ action: status.adoption.safe ? 'state adoption-apply' : 'restore safe boundary, then state adoption-prepare',
1937
+ adoption: adoptionInfo,
1938
+ };
1939
+ }
1940
+ const withAdoption = (value) => ({ ...value, adoption: adoptionInfo });
1180
1941
  const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
1181
1942
  const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
1182
1943
  const activeTask = status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
1183
1944
  if (activeTask) {
1184
1945
  if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
1185
- return this.nextForTaskWithDelegation(status.projectId, activeTask);
1946
+ const planRiskAudit = activeTask.planHash
1947
+ ? readCurrentPlanRiskAudit(this.store, activeTask)
1948
+ : null;
1949
+ return withAdoption(this.nextForTaskWithDelegation(status.projectId, activeTask, planRiskAudit));
1186
1950
  }
1187
1951
  const knowledgeMap = this.knowledgeMapStatus(repository);
1188
1952
  if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
@@ -1190,21 +1954,37 @@ export class WorkflowService {
1190
1954
  const delegatedApprovalOptions = action === 'project-memory approve'
1191
1955
  ? this.delegatedApprovalOptions(status.projectId, 'project_memory.approve', knowledgeMap)
1192
1956
  : [];
1193
- return {
1957
+ const storedKnowledgeMap = this.store.readKnowledgeMap(status.projectId);
1958
+ const contextRefreshOptions = action === 'project-memory reconcile'
1959
+ && storedKnowledgeMap.status === 'active'
1960
+ && storedKnowledgeMap.approval
1961
+ && isContentOnlyKnowledgeRefresh(storedKnowledgeMap, knowledgeMap)
1962
+ ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap)
1963
+ : [];
1964
+ return withAdoption({
1194
1965
  scope: 'project-memory',
1195
1966
  status: knowledgeMap.status,
1196
1967
  action,
1197
1968
  revision: knowledgeMap.revision,
1198
1969
  taskId: activeTask.id,
1199
1970
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
1200
- };
1971
+ ...(contextRefreshOptions.length > 0
1972
+ ? {
1973
+ contextRefresh: {
1974
+ action: 'task context-refresh',
1975
+ mode: 'delegated-content-only',
1976
+ options: contextRefreshOptions,
1977
+ },
1978
+ }
1979
+ : {}),
1980
+ });
1201
1981
  }
1202
1982
  const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
1203
1983
  if (taskCanRebindKnowledge(activeTask) &&
1204
1984
  activeTask.planHash &&
1205
1985
  (activeTask.knowledgeMapRevision !== knowledgeMap.revision ||
1206
1986
  activeTask.knowledgeMapHash !== knowledgeMapHash)) {
1207
- return {
1987
+ return withAdoption({
1208
1988
  scope: 'task',
1209
1989
  id: activeTask.id,
1210
1990
  status: activeTask.status,
@@ -1212,9 +1992,12 @@ export class WorkflowService {
1212
1992
  revision: activeTask.revision,
1213
1993
  knowledgeMapRevision: knowledgeMap.revision,
1214
1994
  knowledgeMapHash,
1215
- };
1995
+ });
1216
1996
  }
1217
- return this.nextForTaskWithDelegation(status.projectId, activeTask);
1997
+ const planRiskAudit = activeTask.planHash && activeTask.status !== 'planning'
1998
+ ? readCurrentPlanRiskAudit(this.store, activeTask)
1999
+ : null;
2000
+ return withAdoption(this.nextForTaskWithDelegation(status.projectId, activeTask, planRiskAudit));
1218
2001
  }
1219
2002
  const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
1220
2003
  if (activeMilestone) {
@@ -1232,17 +2015,17 @@ export class WorkflowService {
1232
2015
  evidence.headCommit !== currentHead ||
1233
2016
  resultHash !== activeMilestone.resultHash ||
1234
2017
  evidenceHash !== activeMilestone.evidenceHash) {
1235
- return {
2018
+ return withAdoption({
1236
2019
  scope: 'milestone',
1237
2020
  id: activeMilestone.id,
1238
2021
  status: activeMilestone.status,
1239
2022
  action: 'restore clean base, then milestone validate',
1240
2023
  revision: activeMilestone.revision,
1241
2024
  reason: 'final validation evidence is stale',
1242
- };
2025
+ });
1243
2026
  }
1244
2027
  const delegatedApprovalOptions = this.delegatedApprovalOptions(status.projectId, 'milestone.final_accept', activeMilestone);
1245
- return {
2028
+ return withAdoption({
1246
2029
  scope: 'milestone',
1247
2030
  id: activeMilestone.id,
1248
2031
  status: activeMilestone.status,
@@ -1251,32 +2034,53 @@ export class WorkflowService {
1251
2034
  : 'await explicit human final acceptance',
1252
2035
  revision: activeMilestone.revision,
1253
2036
  requiredHumanGate: milestoneFinalAcceptanceGate(activeMilestone, evidence.headCommit),
2037
+ completionPolicy: {
2038
+ resultEvidenceAndHeadMustMatch: true,
2039
+ finalAcceptanceTransition: 'milestone.final_accept',
2040
+ delegatedActorMustRemainDelegate: true,
2041
+ },
1254
2042
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
1255
- };
2043
+ });
1256
2044
  }
1257
2045
  const delegatedApprovalOptions = activeMilestone.status === 'awaiting_execution_authorization'
1258
2046
  ? this.delegatedApprovalOptions(status.projectId, 'milestone.execution_authorize', activeMilestone)
1259
2047
  : [];
1260
- return {
2048
+ return withAdoption({
1261
2049
  scope: 'milestone',
1262
2050
  id: activeMilestone.id,
1263
2051
  status: activeMilestone.status,
1264
2052
  action: nextForMilestone(activeMilestone),
1265
2053
  revision: activeMilestone.revision,
2054
+ ...(activeMilestone.status === 'active'
2055
+ ? {
2056
+ completionPolicy: {
2057
+ requiredTasksMustBeMerged: true,
2058
+ validationAction: 'milestone validate',
2059
+ finalAcceptanceTransition: 'milestone.final_accept',
2060
+ delegatedActorMustRemainDelegate: true,
2061
+ requiredTasks: activeMilestone.memberships
2062
+ .filter((membership) => membership.disposition === 'required')
2063
+ .map((membership) => {
2064
+ const task = status.tasks.find((candidate) => candidate.id === membership.taskId);
2065
+ return { taskId: membership.taskId, status: task?.status ?? 'missing' };
2066
+ }),
2067
+ },
2068
+ }
2069
+ : {}),
1266
2070
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
1267
- };
2071
+ });
1268
2072
  }
1269
2073
  const activeDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status));
1270
2074
  if (activeDiscovery) {
1271
- return {
2075
+ return withAdoption({
1272
2076
  scope: 'discovery',
1273
2077
  id: activeDiscovery.id,
1274
2078
  status: activeDiscovery.status,
1275
2079
  action: activeDiscovery.status === 'ready_to_materialize' ? 'discovery materialize' : 'discovery update',
1276
2080
  readiness: assessDiscovery(activeDiscovery),
1277
- };
2081
+ });
1278
2082
  }
1279
- return { scope: 'repository', status: 'idle', action: 'discovery start' };
2083
+ return withAdoption({ scope: 'repository', status: 'idle', action: 'discovery start' });
1280
2084
  }
1281
2085
  delegatedAuthorization(projectId, grantId, transition, actor, target) {
1282
2086
  if (!grantId) {
@@ -1288,7 +2092,37 @@ export class WorkflowService {
1288
2092
  const grant = this.store.readDelegation(projectId, grantId);
1289
2093
  return assertDelegationCanAuthorize(grant, transition, actor, target, this.now());
1290
2094
  }
1291
- nextForTaskWithDelegation(projectId, task) {
2095
+ assertHandoffGrant(projectId, task, targetActor, grantId) {
2096
+ const grant = this.store.readDelegation(projectId, grantId);
2097
+ const policyHash = hashDelegationPolicy(projectId, {
2098
+ principal: grant.principal,
2099
+ delegate: grant.delegate,
2100
+ scope: grant.scope,
2101
+ transitions: grant.transitions,
2102
+ expiresAt: grant.expiresAt,
2103
+ });
2104
+ const scopeMatches = grant.scope.kind === 'project'
2105
+ || (grant.scope.kind === 'task' && grant.scope.id === task.id)
2106
+ || (grant.scope.kind === 'milestone' && grant.scope.id === task.milestoneId);
2107
+ if (grant.status !== 'active'
2108
+ || grant.policyHash !== policyHash
2109
+ || Date.parse(grant.expiresAt) <= this.now().getTime()
2110
+ || grant.delegate !== targetActor.trim()
2111
+ || !scopeMatches) {
2112
+ throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grantId} cannot be bound to this C1 handoff.`, {
2113
+ grantId,
2114
+ taskId: task.id,
2115
+ targetActor: targetActor.trim(),
2116
+ grantStatus: grant.status,
2117
+ expectedPolicyHash: policyHash,
2118
+ actualPolicyHash: grant.policyHash,
2119
+ grantDelegate: grant.delegate,
2120
+ grantScope: grant.scope,
2121
+ grantExpiresAt: grant.expiresAt,
2122
+ });
2123
+ }
2124
+ }
2125
+ nextForTaskWithDelegation(projectId, task, planRiskAudit = null) {
1292
2126
  const transition = task.status === 'awaiting_execution_authorization'
1293
2127
  ? 'task.execution_authorize'
1294
2128
  : task.status === 'awaiting_final_acceptance'
@@ -1297,10 +2131,89 @@ export class WorkflowService {
1297
2131
  const delegatedApprovalOptions = transition
1298
2132
  ? this.delegatedApprovalOptions(projectId, transition, task)
1299
2133
  : [];
1300
- return {
1301
- ...nextForTask(task),
2134
+ const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(projectId, task.id));
2135
+ const next = nextForTask(task);
2136
+ const handoffPosture = readTaskC1Posture(this.store, task);
2137
+ const strictStepReviewStep = planRiskAudit
2138
+ ? task.steps.find((step) => {
2139
+ if (step.status !== 'in_progress' || !step.evidence || !planRiskAudit.reviewRequiredStepIds.includes(step.id)) {
2140
+ return false;
2141
+ }
2142
+ if (task.status !== 'blocked')
2143
+ return true;
2144
+ return buildCurrentStrictStepReviewCycle(this.store, task, step.id).resolution === 'unverified';
2145
+ }) ?? null
2146
+ : null;
2147
+ const handoffOverlay = handoffPosture.state === 'pending'
2148
+ ? {
2149
+ action: 'task claim',
2150
+ blockedAction: next.action,
2151
+ c1Handoff: {
2152
+ state: 'pending',
2153
+ targetActor: handoffPosture.targetActor,
2154
+ eventId: handoffPosture.latestEvent.eventId,
2155
+ },
2156
+ }
2157
+ : handoffPosture.state === 'claimed'
2158
+ ? {
2159
+ c1Handoff: {
2160
+ state: 'claimed',
2161
+ claimant: handoffPosture.claimant,
2162
+ writerLeaseTokenBound: handoffPosture.writerLeaseTokenHash !== null,
2163
+ requiredActor: handoffPosture.claimant,
2164
+ },
2165
+ }
2166
+ : {};
2167
+ const result = {
2168
+ ...next,
2169
+ ...handoffOverlay,
2170
+ ...(strictStepReviewStep
2171
+ ? {
2172
+ action: 'task step-review',
2173
+ stepId: strictStepReviewStep.id,
2174
+ commitPolicy: {
2175
+ owner: 'workflow-core',
2176
+ manualCommitAllowed: false,
2177
+ completionAction: 'task step-review',
2178
+ },
2179
+ strictStepReview: {
2180
+ reviewRequired: true,
2181
+ completionCommit: strictStepReviewStep.evidence.commitSha,
2182
+ },
2183
+ }
2184
+ : {}),
2185
+ ...(planRiskAudit
2186
+ ? {
2187
+ planRiskAudit: {
2188
+ decision: planRiskAudit.audit.decision,
2189
+ scope: planRiskAudit.scope,
2190
+ postureEventType: planRiskAudit.postureEventType,
2191
+ reviewRequiredStepIds: planRiskAudit.reviewRequiredStepIds,
2192
+ },
2193
+ }
2194
+ : {}),
2195
+ ...(task.status === 'needs_fix' && task.review.status === 'failed' && failedReviewAttempts >= 2
2196
+ ? {
2197
+ action: 'task plan-set with independent corrective audit',
2198
+ correctivePlanGate: {
2199
+ failedReviewAttempts,
2200
+ auditFileRequired: true,
2201
+ auditorMustDifferFromFailedReviewer: true,
2202
+ allowedDecision: 'continue-fix',
2203
+ escalationDecisions: ['replan-required', 'split-required', 'stop-escalate'],
2204
+ },
2205
+ }
2206
+ : {}),
1302
2207
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
1303
2208
  };
2209
+ if (handoffPosture.state === 'pending') {
2210
+ return {
2211
+ ...result,
2212
+ action: 'task claim',
2213
+ blockedAction: next.action,
2214
+ };
2215
+ }
2216
+ return result;
1304
2217
  }
1305
2218
  delegatedApprovalOptions(projectId, transition, target) {
1306
2219
  return this.store.listDelegations(projectId)
@@ -1315,6 +2228,21 @@ export class WorkflowService {
1315
2228
  policyHash: grant.policyHash,
1316
2229
  }));
1317
2230
  }
2231
+ delegatedContextRefreshOptions(projectId, task, knowledgeMap) {
2232
+ return this.store.listDelegations(projectId)
2233
+ .filter((grant) => delegationCanAuthorize(grant, 'project_memory.approve', knowledgeMap, this.now()))
2234
+ .filter((grant) => delegationCanAuthorize(grant, 'task.execution_authorize', task, this.now()))
2235
+ .map((grant) => ({
2236
+ grantId: grant.id,
2237
+ principal: grant.principal,
2238
+ delegate: grant.delegate,
2239
+ transition: 'task.execution_authorize',
2240
+ transitions: ['project_memory.approve', 'task.execution_authorize'],
2241
+ scope: grant.scope,
2242
+ expiresAt: grant.expiresAt,
2243
+ policyHash: grant.policyHash,
2244
+ }));
2245
+ }
1318
2246
  assertDelegationScopeExists(projectId, policy) {
1319
2247
  if (policy.scope.kind === 'task') {
1320
2248
  this.store.readTask(projectId, policy.scope.id);
@@ -1352,6 +2280,75 @@ export class WorkflowService {
1352
2280
  throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone authorization is stale.');
1353
2281
  }
1354
2282
  }
2283
+ readMilestoneCurrent(projectId, milestoneId) {
2284
+ return readMilestoneWithIntegrity(this.store, projectId, milestoneId, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId));
2285
+ }
2286
+ readMilestoneScopeChangeCurrent(projectId, milestoneId) {
2287
+ return readMilestoneForScopeChange(this.store, projectId, milestoneId, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId));
2288
+ }
2289
+ listMilestonesCurrent(projectId, posture = this.readCurrentAdoptionPostureStrict(projectId)) {
2290
+ const milestones = this.store.listMilestones(projectId);
2291
+ if (!posture)
2292
+ return milestones;
2293
+ const grandfatheredMilestones = new Set(posture.terminalMilestones.map((milestone) => `${milestone.milestoneId}:${milestone.revision}:${milestone.status}`));
2294
+ return milestones.map((milestone) => (grandfatheredMilestones.has(`${milestone.id}:${milestone.revision}:${milestone.status}`)
2295
+ ? milestone
2296
+ : readMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
2297
+ }
2298
+ prepareMilestoneScopeChangeCandidate(projectId, milestone, plan, actor) {
2299
+ validateMilestonePlan(plan);
2300
+ return prepareMilestoneScopeChangeCandidate({
2301
+ milestone,
2302
+ plan,
2303
+ readTask: (taskId) => this.store.readTask(projectId, taskId),
2304
+ actor,
2305
+ });
2306
+ }
2307
+ proveWriterLeaseTokenForHandoff(context, taskId, writerToken) {
2308
+ const currentLease = context.locks.inspect(taskId);
2309
+ if (!currentLease) {
2310
+ if (writerToken !== null) {
2311
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
2312
+ taskId,
2313
+ });
2314
+ }
2315
+ return null;
2316
+ }
2317
+ if (writerToken === null) {
2318
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} requires the active writer token while a lease is present.`, {
2319
+ taskId,
2320
+ leaseOwner: currentLease.owner,
2321
+ expiresAt: currentLease.expiresAt,
2322
+ });
2323
+ }
2324
+ return context.locks.heartbeat(taskId, writerToken);
2325
+ }
2326
+ proveOptionalWriterLeaseToken(context, taskId, writerToken) {
2327
+ const currentLease = context.locks.inspect(taskId);
2328
+ if (!currentLease) {
2329
+ if (writerToken !== null) {
2330
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} has no active writer lease for the provided writer token.`, {
2331
+ taskId,
2332
+ });
2333
+ }
2334
+ return null;
2335
+ }
2336
+ if (writerToken === null)
2337
+ return null;
2338
+ return context.locks.heartbeat(taskId, writerToken);
2339
+ }
2340
+ assertTaskMutationAllowedByHandoff(task, actor, writerToken) {
2341
+ const posture = readTaskC1Posture(this.store, task);
2342
+ if (posture.state === 'none')
2343
+ return;
2344
+ if (!actor || !actor.trim()) {
2345
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has active C1 coordination and requires an explicit actor for mutation.`, {
2346
+ taskId: task.id,
2347
+ posture: posture.state,
2348
+ });
2349
+ }
2350
+ assertTaskMutationAllowedByC1(this.store, task, { actor: actor.trim(), writerToken });
2351
+ }
1355
2352
  readGraphBindingOrNull(projectId, graphKind) {
1356
2353
  try {
1357
2354
  return this.store.readGraphBinding(projectId, graphKind);
@@ -1374,6 +2371,71 @@ export class WorkflowService {
1374
2371
  }
1375
2372
  return inspected;
1376
2373
  }
2374
+ inspectAdoptionPreparation(identity, tasks, milestones) {
2375
+ const locks = new WriterLockManager(this.store.lockRoot(identity.projectId), this.now);
2376
+ const currentPosture = readCurrentAdoptionPosture(this.store, identity.projectId);
2377
+ if (currentPosture) {
2378
+ assertAdoptionBaselinePreserved(currentPosture, tasks, milestones);
2379
+ }
2380
+ return buildAdoptionPreparation({
2381
+ projectId: identity.projectId,
2382
+ repositoryIdentity: identity,
2383
+ packageVersion: PACKAGE_VERSION,
2384
+ protocolVersion: PROTOCOL_VERSION,
2385
+ stateSchemaVersion: STATE_SCHEMA_VERSION,
2386
+ tasks,
2387
+ milestones,
2388
+ sidecarInventory: this.collectAdoptionSidecarInventory(identity.projectId, tasks, milestones),
2389
+ blockers: this.computeAdoptionBoundaryBlockers(identity, tasks, locks.list()),
2390
+ now: this.now(),
2391
+ }, currentPosture);
2392
+ }
2393
+ computeAdoptionBoundaryBlockers(identity, tasks, leases) {
2394
+ const dirty = changedFiles(identity.repositoryRoot);
2395
+ const runningSteps = tasks.flatMap((task) => task.steps
2396
+ .filter((step) => step.status === 'in_progress')
2397
+ .map((step) => ({ taskId: task.id, stepId: step.id })));
2398
+ const now = this.now().getTime();
2399
+ const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
2400
+ const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
2401
+ return [
2402
+ ...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
2403
+ ...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
2404
+ ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
2405
+ ...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
2406
+ ];
2407
+ }
2408
+ collectAdoptionSidecarInventory(projectId, tasks, milestones) {
2409
+ const inventory = [];
2410
+ const collect = (scope, ownerId, root) => {
2411
+ if (!existsSync(root))
2412
+ return;
2413
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
2414
+ if (entry.isSymbolicLink()) {
2415
+ throw new WorkflowError('STATE_CORRUPT', `Adoption sidecar inventory rejects symlink entries: ${path.join(root, entry.name)}`);
2416
+ }
2417
+ if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
2418
+ continue;
2419
+ const filePath = path.join(root, entry.name);
2420
+ const stat = lstatSync(filePath);
2421
+ if (!stat.isFile()) {
2422
+ throw new WorkflowError('STATE_CORRUPT', `Adoption sidecar inventory requires regular files only: ${filePath}`);
2423
+ }
2424
+ inventory.push({
2425
+ scope,
2426
+ ownerId,
2427
+ sidecarName: entry.name,
2428
+ contentHash: createHash('sha256').update(readFileSync(filePath)).digest('hex'),
2429
+ });
2430
+ }
2431
+ };
2432
+ collect('project', projectId, this.store.projectRoot(projectId));
2433
+ for (const task of tasks)
2434
+ collect('task', task.id, this.store.taskRoot(projectId, task.id));
2435
+ for (const milestone of milestones)
2436
+ collect('milestone', milestone.id, this.store.milestoneRoot(projectId, milestone.id));
2437
+ return inventory;
2438
+ }
1377
2439
  assertKnowledgeMapBinding(repositoryRoot, projectId, revision, mapHash) {
1378
2440
  const map = this.requireActiveKnowledgeMap(repositoryRoot, projectId);
1379
2441
  const currentHash = hashKnowledgeMap(map);
@@ -1387,6 +2449,29 @@ export class WorkflowService {
1387
2449
  }
1388
2450
  return map;
1389
2451
  }
2452
+ readCurrentAdoptionPostureStrict(projectId) {
2453
+ const posture = readCurrentAdoptionPosture(this.store, projectId);
2454
+ if (!posture)
2455
+ return null;
2456
+ assertAdoptionBaselinePreserved(posture, this.store.listTasks(projectId), this.store.listMilestones(projectId));
2457
+ return posture;
2458
+ }
2459
+ requireExecutionAdoptionPosture(identity, taskId) {
2460
+ const tasks = this.store.listTasks(identity.projectId);
2461
+ const milestones = this.store.listMilestones(identity.projectId);
2462
+ const posture = this.readCurrentAdoptionPostureStrict(identity.projectId);
2463
+ if (posture)
2464
+ return posture;
2465
+ const adoption = this.inspectAdoptionPreparation(identity, tasks, milestones);
2466
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Alpha.6 adoption posture is required before execution can proceed.', {
2467
+ taskId,
2468
+ adoptionStatus: adoption.status,
2469
+ requiredAction: adoption.safe ? 'state adoption-apply' : 'restore safe boundary, then state adoption-prepare',
2470
+ blockers: adoption.blockers,
2471
+ confirmationCode: adoption.confirmationCode,
2472
+ confirmationCodeBindingHash: adoption.confirmationCodeBindingHash,
2473
+ });
2474
+ }
1390
2475
  assertTaskKnowledgeBinding(repositoryRoot, task) {
1391
2476
  if (task.knowledgeMapRevision === null || !task.knowledgeMapHash) {
1392
2477
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has no Project Knowledge Map binding.`);
@@ -1458,6 +2543,62 @@ function dependenciesComplete(step, completed) {
1458
2543
  const completedIds = new Set(completed);
1459
2544
  return step.dependencies.every((dependency) => completedIds.has(dependency));
1460
2545
  }
2546
+ function applyStrictStepReviewResolution(task, stepId, resolution, summary) {
2547
+ if (resolution === 'pending') {
2548
+ throw new WorkflowError('STATE_CORRUPT', `Strict Step Review resolution is still pending for ${stepId}.`, {
2549
+ taskId: task.id,
2550
+ stepId,
2551
+ });
2552
+ }
2553
+ const step = requireStep(task, stepId);
2554
+ if (!step.evidence) {
2555
+ throw new WorkflowError('STATE_CORRUPT', `Strict Step Review resolution requires canonical evidence for ${stepId}.`, {
2556
+ taskId: task.id,
2557
+ stepId,
2558
+ resolution,
2559
+ });
2560
+ }
2561
+ if (resolution === 'passed_verified') {
2562
+ const completedIds = new Set([
2563
+ ...task.steps.filter((candidate) => candidate.status === 'completed').map((candidate) => candidate.id),
2564
+ stepId,
2565
+ ]);
2566
+ return {
2567
+ ...task,
2568
+ status: 'in_progress',
2569
+ steps: task.steps.map((candidate) => {
2570
+ if (candidate.id === stepId)
2571
+ return { ...candidate, status: 'completed', evidence: step.evidence };
2572
+ if (candidate.status === 'blocked' && dependenciesComplete(candidate, completedIds)) {
2573
+ return { ...candidate, status: 'planned' };
2574
+ }
2575
+ return candidate;
2576
+ }),
2577
+ blockReason: null,
2578
+ };
2579
+ }
2580
+ if (resolution === 'failed') {
2581
+ return {
2582
+ ...task,
2583
+ status: 'needs_fix',
2584
+ steps: task.steps.map((candidate) => candidate.id === stepId
2585
+ ? { ...candidate, status: 'failed', evidence: null }
2586
+ : candidate),
2587
+ blockReason: summary?.trim() || `Strict Step Review failed for ${stepId}.`,
2588
+ systemCommits: task.systemCommits.includes(step.evidence.commitSha)
2589
+ ? task.systemCommits
2590
+ : [...task.systemCommits, step.evidence.commitSha],
2591
+ };
2592
+ }
2593
+ return {
2594
+ ...task,
2595
+ status: 'blocked',
2596
+ steps: task.steps.map((candidate) => candidate.id === stepId
2597
+ ? { ...candidate, status: 'in_progress', evidence: step.evidence }
2598
+ : candidate),
2599
+ blockReason: summary?.trim() || `Strict Step Review isolation could not be verified for ${stepId}.`,
2600
+ };
2601
+ }
1461
2602
  function identityRelative(repositoryRoot, value) {
1462
2603
  return path.resolve(repositoryRoot, value);
1463
2604
  }
@@ -1511,6 +2652,83 @@ function validateReviewInput(review) {
1511
2652
  throw new WorkflowError('INVALID_ARGUMENT', 'A failed review requires at least one finding.');
1512
2653
  }
1513
2654
  }
2655
+ function countFailedReviewAttempts(taskRoot) {
2656
+ const file = path.join(taskRoot, 'findings.jsonl');
2657
+ if (!existsSync(file))
2658
+ return 0;
2659
+ const attempts = new Set();
2660
+ for (const line of readFileSync(file, 'utf8').split('\n').filter(Boolean)) {
2661
+ try {
2662
+ const finding = JSON.parse(line);
2663
+ if (typeof finding.recordedAt === 'string' && finding.recordedAt) {
2664
+ attempts.add(`${finding.recordedAt}\0${typeof finding.reviewer === 'string' ? finding.reviewer : ''}`);
2665
+ }
2666
+ }
2667
+ catch {
2668
+ throw new WorkflowError('STATE_CORRUPT', `Invalid review finding log: ${file}`);
2669
+ }
2670
+ }
2671
+ return attempts.size;
2672
+ }
2673
+ function validateCorrectivePlanAudit(task, audit, failedReviewAttempts) {
2674
+ if (!audit) {
2675
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has ${failedReviewAttempts} failed independent reviews. A separate corrective Plan audit is required before another plan-set.`, {
2676
+ failedReviewAttempts,
2677
+ requiredAction: 'Run a separate read-only Auditor and pass its evidence through --corrective-audit-file.',
2678
+ });
2679
+ }
2680
+ if (!audit.auditor.trim() || !audit.summary.trim()) {
2681
+ throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit requires auditor and summary.');
2682
+ }
2683
+ if (audit.auditor.trim() === task.review.reviewer.trim()) {
2684
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan audit must be performed by an Auditor distinct from the failed-review author.');
2685
+ }
2686
+ const latestFindingIds = new Set(task.review.findings.map((finding) => finding.id));
2687
+ const reviewedFindingIds = new Set(audit.reviewedFindingIds);
2688
+ if (reviewedFindingIds.size !== audit.reviewedFindingIds.length || audit.reviewedFindingIds.some((id) => !id.trim())) {
2689
+ throw new WorkflowError('INVALID_ARGUMENT', 'Corrective Plan audit finding IDs must be non-empty and unique.');
2690
+ }
2691
+ const missing = [...latestFindingIds].filter((findingId) => !reviewedFindingIds.has(findingId));
2692
+ if (missing.length > 0) {
2693
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Corrective Plan audit does not cover every latest finding.', {
2694
+ missingFindingIds: missing,
2695
+ });
2696
+ }
2697
+ if (audit.decision !== 'continue-fix') {
2698
+ throw new WorkflowError('TRANSITION_BLOCKED', `Corrective Auditor decision ${audit.decision} requires coordinator/user redirection before implementation continues.`, { decision: audit.decision, summary: audit.summary });
2699
+ }
2700
+ }
2701
+ function appendCorrectivePlanAudit(taskRoot, previous, saved, failedReviewAttempts, audit, now) {
2702
+ appendFileSync(path.join(taskRoot, 'corrective-plan-audits.jsonl'), `${JSON.stringify({
2703
+ recordedAt: now.toISOString(),
2704
+ taskId: previous.id,
2705
+ taskRevision: previous.revision,
2706
+ failedReviewAttempts,
2707
+ previousPlanHash: previous.planHash,
2708
+ correctivePlanHash: saved.planHash,
2709
+ ...audit,
2710
+ })}\n`, { encoding: 'utf8', mode: 0o600 });
2711
+ }
2712
+ function assertContentOnlyKnowledgeRefresh(approved, inspected) {
2713
+ if (!isContentOnlyKnowledgeRefresh(approved, inspected)) {
2714
+ 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.' });
2715
+ }
2716
+ }
2717
+ function isContentOnlyKnowledgeRefresh(approved, inspected) {
2718
+ const classification = (map) => JSON.stringify({
2719
+ entries: map.entries.map((entry) => ({
2720
+ id: entry.id,
2721
+ category: entry.category,
2722
+ path: entry.path,
2723
+ scope: entry.scope,
2724
+ authority: entry.authority,
2725
+ discoveredBy: entry.discoveredBy,
2726
+ })),
2727
+ gaps: map.gaps,
2728
+ conflicts: map.conflicts,
2729
+ });
2730
+ return classification(approved) === classification(inspected);
2731
+ }
1514
2732
  function validateMilestonePlan(plan) {
1515
2733
  if (!plan.outcome.trim())
1516
2734
  throw new WorkflowError('INVALID_ARGUMENT', 'Milestone outcome is required.');
@@ -1606,11 +2824,14 @@ function nextForMilestone(milestone) {
1606
2824
  }
1607
2825
  function nextForTask(task) {
1608
2826
  const stepsComplete = task.steps.every((step) => step.status === 'completed' || step.status === 'skipped');
2827
+ const activeStep = task.steps.find((step) => step.status === 'in_progress');
2828
+ const failedStep = task.steps.find((step) => step.status === 'failed');
2829
+ const runnableStep = task.steps.find((step) => step.status === 'planned' && step.dependencies.every((dependency) => ['completed', 'skipped'].includes(requireStep(task, dependency).status)));
1609
2830
  const actionByStatus = {
1610
2831
  planning: 'task plan-set',
1611
2832
  awaiting_execution_authorization: 'task authorize',
1612
2833
  ready: task.workspaceOwner ? (stepsComplete ? 'task submit' : 'task run') : 'task start',
1613
- in_progress: stepsComplete ? 'task submit' : 'task run or task submit',
2834
+ in_progress: activeStep ? 'task step-complete' : stepsComplete ? 'task submit' : 'task run',
1614
2835
  validating: task.review.status === 'passed' ? 'task result-set' : 'task review-record',
1615
2836
  needs_fix: task.steps.some((step) => step.status === 'failed') ? 'task run' : 'task plan-set',
1616
2837
  awaiting_final_acceptance: 'task accept',
@@ -1620,7 +2841,24 @@ function nextForTask(task) {
1620
2841
  blocked: task.review.status === 'unverified' ? 'task review-record' : 'doctor',
1621
2842
  cancelled: 'none',
1622
2843
  };
1623
- return { scope: 'task', id: task.id, status: task.status, action: actionByStatus[task.status], revision: task.revision };
2844
+ const selectedStep = activeStep ?? failedStep ?? runnableStep;
2845
+ return {
2846
+ scope: 'task',
2847
+ id: task.id,
2848
+ status: task.status,
2849
+ action: actionByStatus[task.status],
2850
+ revision: task.revision,
2851
+ ...(selectedStep ? { stepId: selectedStep.id } : {}),
2852
+ ...(activeStep
2853
+ ? {
2854
+ commitPolicy: {
2855
+ owner: 'workflow-core',
2856
+ manualCommitAllowed: false,
2857
+ completionAction: 'task step-complete',
2858
+ },
2859
+ }
2860
+ : {}),
2861
+ };
1624
2862
  }
1625
2863
  function knowledgeSummary(map) {
1626
2864
  return {