codex-workflow-v2 2.0.0-alpha.7.2 → 2.0.0-beta.1

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 (38) hide show
  1. package/README.md +23 -5
  2. package/dist/src/alpha6/milestone.d.ts +17 -0
  3. package/dist/src/alpha6/milestone.js +54 -0
  4. package/dist/src/alpha6/milestone.js.map +1 -1
  5. package/dist/src/alpha6/remediation.d.ts +18 -1
  6. package/dist/src/alpha6/remediation.js +458 -9
  7. package/dist/src/alpha6/remediation.js.map +1 -1
  8. package/dist/src/beta1/project-transaction.d.ts +52 -0
  9. package/dist/src/beta1/project-transaction.js +297 -0
  10. package/dist/src/beta1/project-transaction.js.map +1 -0
  11. package/dist/src/cli.js +23 -1
  12. package/dist/src/cli.js.map +1 -1
  13. package/dist/src/contracts.d.ts +39 -0
  14. package/dist/src/diagnostics.d.ts +11 -0
  15. package/dist/src/diagnostics.js +54 -0
  16. package/dist/src/diagnostics.js.map +1 -1
  17. package/dist/src/git.js +2 -5
  18. package/dist/src/git.js.map +1 -1
  19. package/dist/src/version.d.ts +1 -1
  20. package/dist/src/version.js +1 -1
  21. package/dist/src/version.js.map +1 -1
  22. package/dist/src/workflow.d.ts +11 -1
  23. package/dist/src/workflow.js +519 -56
  24. package/dist/src/workflow.js.map +1 -1
  25. package/docs/alpha7.2.1-remediation-recovery-brief.md +86 -0
  26. package/docs/autonomy-guardrails.md +3 -2
  27. package/docs/beta1-stabilization-brief.md +165 -0
  28. package/docs/delegated-approval.md +4 -3
  29. package/docs/development-flow.md +6 -4
  30. package/docs/project-memory.md +7 -5
  31. package/docs/release.md +9 -2
  32. package/docs/split-required-recovery.md +53 -0
  33. package/docs/stable-release-defect-register.md +268 -0
  34. package/package.json +2 -2
  35. package/plugins/codex-workflow-gateway/references/protocol.md +6 -2
  36. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +9 -2
  37. package/schemas/remediation-mode-recovery-event.schema.json +42 -0
  38. package/schemas/task.schema.json +3 -1
@@ -8,12 +8,13 @@ import { assessDiscovery, mergeUnique } from './domain/discovery.js';
8
8
  import { pathAllowed, validatePlan } from './domain/validation.js';
9
9
  import { WorkflowError } from './errors.js';
10
10
  import { bindGraphEvidence, createGraphRefreshRequest, fallbackGraphBinding, inspectGraphBinding, validateGraphRequestCurrent, } from './graph.js';
11
- import { inspectLocalDependency } from './diagnostics.js';
11
+ import { inspectLocalDependency, inspectWorkflowVersions } from './diagnostics.js';
12
12
  import { appendTaskClaim, appendTaskHandback, appendTaskHandoff, assertTaskMutationAllowedByC1, defaultTaskWorkerActor, readTaskC1Posture, } from './alpha6/handoff.js';
13
13
  import { applyAdoptionPosture, assertAdoptionBaselinePreserved, buildAdoptionPreparation, initializeProjectRegistrationAdoption, readCurrentAdoptionPosture, } from './alpha6/adoption.js';
14
14
  import { appendCurrentPlanRiskAudit, appendReboundPlanRiskAudit, readCurrentPlanRiskAudit, validatePlanRiskAuditCandidate, } from './alpha6/plan-risk.js';
15
- import { appendCorrectiveDecisionEvent, appendRemediationEvent, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
16
- import { applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
15
+ import { appendCorrectiveDecisionEvent, appendRemediationEvent, appendRemediationModeRecovery, assertCorrectiveAuditorIndependence, assertGuardedRemediationAttemptAllowed, defaultCorrectiveAuditorActor, findCurrentGuardedRemediationPosture, findRemediationModeRecoveryCandidate, findFailedGuardedStepRequiringCorrectiveDecision, readGuardedRemediationPostureForStep, readCorrectiveDecisionEvents, readRemediationEvents, } from './alpha6/remediation.js';
16
+ import { applyMilestoneScopeChangeTransaction, assertMilestoneMembershipIntegrity, assertMilestoneScopeChangeStatusAllowed, normalizeMilestonePlan, prepareMilestoneScopeChangeCandidate, prepareMilestoneScopeChangeBundle, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.js';
17
+ import { applyProjectTransaction, recoverProjectTransactions, runRollbackProjectTransaction, } from './beta1/project-transaction.js';
17
18
  import { buildCurrentStrictStepReviewCycle, ensurePendingStrictStepReview, isStepReviewRequired, recordStrictStepReview, } from './alpha6/review.js';
18
19
  import { applyMilestoneAutonomyContract, assertAutonomousPlanEvolution, prepareMilestoneAutonomyContract, readMilestoneAutonomyEvents, requireActiveMilestoneAutonomy, } from './alpha7/autonomy.js';
19
20
  import { appendCorrectiveDecisionRecovery, readValidatedCorrectiveDecisionRecoveryForNavigation, requireCorrectiveDecisionRecoveryForRun, } from './alpha7/corrective-recovery.js';
@@ -35,7 +36,9 @@ export class WorkflowService {
35
36
  }
36
37
  context(repository) {
37
38
  const identity = resolveRepositoryIdentity(repository);
38
- const projectFile = path.join(this.store.projectRoot(identity.projectId), 'project.json');
39
+ const projectRoot = this.store.projectRoot(identity.projectId);
40
+ recoverProjectTransactions(projectRoot, identity.projectId);
41
+ const projectFile = path.join(projectRoot, 'project.json');
39
42
  const projectAlreadyRegistered = existsSync(projectFile);
40
43
  if (!projectAlreadyRegistered) {
41
44
  initializeProjectRegistrationAdoption(this.store, {
@@ -249,6 +252,257 @@ export class WorkflowService {
249
252
  const savedDiscovery = this.store.writeDiscovery({ ...discovery, status: 'materialized', materializedEntityId: taskId }, expectedRevision);
250
253
  return { discovery: savedDiscovery, task: savedTask, taskRoot };
251
254
  }
255
+ replaceSplitRequiredTask(repository, oldTaskId, expectedOldTaskRevision, discoveryId, expectedDiscoveryRevision, expectedMilestoneRevision, title, actor) {
256
+ const { identity, locks } = this.context(repository);
257
+ const oldTask = this.store.readTask(identity.projectId, oldTaskId);
258
+ assertExpectedRevision(oldTask, expectedOldTaskRevision);
259
+ if (!oldTask.milestoneId) {
260
+ throw new WorkflowError('TRANSITION_BLOCKED', 'A split-required replacement requires a Milestone-linked Task.');
261
+ }
262
+ const milestone = this.readMilestoneScopeChangeCurrent(identity.projectId, oldTask.milestoneId);
263
+ assertExpectedRevision(milestone, expectedMilestoneRevision);
264
+ assertMilestoneScopeChangeStatusAllowed(milestone);
265
+ const membership = milestone.memberships.find((candidate) => candidate.taskId === oldTask.id);
266
+ if (membership?.disposition !== 'required') {
267
+ throw new WorkflowError('TRANSITION_BLOCKED', 'The terminal Task must still be a required Milestone member.', {
268
+ taskId: oldTask.id,
269
+ milestoneId: milestone.id,
270
+ disposition: membership?.disposition ?? null,
271
+ });
272
+ }
273
+ const riskAudit = readCurrentPlanRiskAudit(this.store, oldTask);
274
+ if (!riskAudit)
275
+ throw new WorkflowError('STATE_CORRUPT', 'The terminal Task has no current Plan Risk Audit.');
276
+ const posture = findCurrentGuardedRemediationPosture(this.store, oldTask, riskAudit);
277
+ if (posture?.posture !== 'split-required' || posture.correctiveDecision?.decision !== 'split-required') {
278
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task replacement is available only for the exact split-required posture.', {
279
+ taskId: oldTask.id,
280
+ posture: posture?.posture ?? null,
281
+ decision: posture?.correctiveDecision?.decision ?? null,
282
+ });
283
+ }
284
+ const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
285
+ assertExpectedRevision(discovery, expectedDiscoveryRevision);
286
+ const readiness = assessDiscovery(discovery);
287
+ if (!readiness.ready || discovery.status !== 'ready_to_materialize') {
288
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Replacement Discovery is not ready to materialize.', {
289
+ discoveryId,
290
+ missing: readiness.missing,
291
+ });
292
+ }
293
+ const normalizedTitle = title.trim();
294
+ const normalizedActor = actor.trim();
295
+ if (!normalizedTitle)
296
+ throw new WorkflowError('INVALID_ARGUMENT', 'Replacement Task title is required.');
297
+ if (!normalizedActor)
298
+ throw new WorkflowError('INVALID_ARGUMENT', 'Replacement actor is required.');
299
+ let autonomyContractEventHash;
300
+ let activeAutonomy = null;
301
+ if (normalizedActor.startsWith('agent:')) {
302
+ activeAutonomy = requireActiveMilestoneAutonomy({
303
+ store: this.store,
304
+ projectId: identity.projectId,
305
+ milestone,
306
+ actor: normalizedActor,
307
+ now: this.now(),
308
+ });
309
+ autonomyContractEventHash = activeAutonomy.contract.eventHash;
310
+ }
311
+ const now = this.now();
312
+ const timestamp = now.toISOString();
313
+ const replacementTaskId = createEntityId('TASK', now.getTime());
314
+ const briefContent = renderBrief(discovery, normalizedTitle);
315
+ const normalizedBriefContent = briefContent.endsWith('\n') ? briefContent : `${briefContent}\n`;
316
+ const briefHash = sha256Hex(normalizedBriefContent);
317
+ const replacementTask = {
318
+ schemaVersion: STATE_SCHEMA_VERSION,
319
+ revision: 1,
320
+ id: replacementTaskId,
321
+ projectId: identity.projectId,
322
+ createdAt: timestamp,
323
+ updatedAt: timestamp,
324
+ kind: 'task',
325
+ status: 'planning',
326
+ title: normalizedTitle,
327
+ slug: slugify(normalizedTitle),
328
+ discoveryId,
329
+ milestoneId: milestone.id,
330
+ repositoryRoot: identity.repositoryRoot,
331
+ baseBranch: milestone.baseBranch,
332
+ baseCommit: null,
333
+ taskBranch: null,
334
+ workspaceOwner: null,
335
+ briefHash,
336
+ planHash: null,
337
+ planObjective: null,
338
+ resultHash: null,
339
+ evidenceHash: null,
340
+ requirements: discovery.acceptance.map((_, index) => `REQ-${String(index + 1).padStart(3, '0')}`),
341
+ acceptance: discovery.acceptance.map((_, index) => `AC-${String(index + 1).padStart(3, '0')}`),
342
+ risks: discovery.assumptions,
343
+ knowledgeImpact: null,
344
+ knowledgeImpactReason: null,
345
+ knowledgeTargets: [],
346
+ knowledgeMapRevision: null,
347
+ knowledgeMapHash: null,
348
+ graphUse: null,
349
+ steps: [],
350
+ authorizations: [],
351
+ knowledgeRebinds: [],
352
+ review: emptyReview(),
353
+ blockReason: null,
354
+ mergedCommit: null,
355
+ systemCommits: [],
356
+ replacementForTaskId: oldTask.id,
357
+ replacedByTaskId: null,
358
+ };
359
+ const cancelledOldTask = {
360
+ ...oldTask,
361
+ revision: oldTask.revision + 1,
362
+ updatedAt: timestamp,
363
+ status: 'cancelled',
364
+ blockReason: `Replaced after split-required by ${replacementTaskId}.`,
365
+ replacedByTaskId: replacementTaskId,
366
+ };
367
+ const consumedDiscovery = {
368
+ ...discovery,
369
+ revision: discovery.revision + 1,
370
+ updatedAt: timestamp,
371
+ status: 'materialized',
372
+ materializedEntityId: replacementTaskId,
373
+ };
374
+ const replacementReason = `Replacement for ${oldTask.id} after split-required decision ${posture.correctiveDecision.eventId}.`;
375
+ const plan = {
376
+ outcome: milestone.outcome,
377
+ successSignal: milestone.successSignal,
378
+ acceptance: milestone.acceptance,
379
+ checks: milestone.checks,
380
+ memberships: [
381
+ ...milestone.memberships.map((candidate) => candidate.taskId === oldTask.id
382
+ ? { ...candidate, disposition: 'cancelled', reason: replacementReason }
383
+ : candidate),
384
+ { taskId: replacementTaskId, disposition: 'required', reason: replacementReason },
385
+ ],
386
+ };
387
+ if (autonomyContractEventHash) {
388
+ const autonomy = requireActiveMilestoneAutonomy({
389
+ store: this.store,
390
+ projectId: identity.projectId,
391
+ milestone,
392
+ actor: normalizedActor,
393
+ now,
394
+ });
395
+ assertAutonomousPlanEvolution(milestone, plan, autonomy.contract);
396
+ }
397
+ const readCandidateTask = (taskId) => {
398
+ if (taskId === oldTask.id)
399
+ return cancelledOldTask;
400
+ if (taskId === replacementTaskId)
401
+ return replacementTask;
402
+ return this.store.readTask(identity.projectId, taskId);
403
+ };
404
+ const prepared = prepareMilestoneScopeChangeCandidate({
405
+ milestone,
406
+ plan,
407
+ readTask: readCandidateTask,
408
+ actor: normalizedActor,
409
+ ...(autonomyContractEventHash ? { autonomyContractEventHash } : {}),
410
+ });
411
+ const bundle = prepareMilestoneScopeChangeBundle({
412
+ store: this.store,
413
+ milestone,
414
+ prepared,
415
+ now,
416
+ readTask: readCandidateTask,
417
+ });
418
+ const currentKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
419
+ const scannedKnowledgeMap = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, now);
420
+ const inspectedKnowledgeMap = inspectKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
421
+ let refreshedKnowledgeMap = null;
422
+ if (activeAutonomy
423
+ && currentKnowledgeMap.status === 'active'
424
+ && currentKnowledgeMap.approval
425
+ && (inspectedKnowledgeMap.status !== 'active' || !inspectedKnowledgeMap.approval)) {
426
+ const refreshAssessment = assessDelegatedKnowledgeRefresh(currentKnowledgeMap, inspectedKnowledgeMap, oldTask, oldTask.planHash
427
+ ? this.store.hashArtifact(this.store.taskRoot(identity.projectId, oldTask.id), 'plan.md')
428
+ : null);
429
+ if (refreshAssessment.eligible) {
430
+ if (refreshAssessment.mode === 'delegated-plan-bounded-supporting-source') {
431
+ this.assertPlanBoundedSupportingSourceGrant(identity.projectId, activeAutonomy.grant.id, normalizedActor, oldTask);
432
+ }
433
+ const delegation = this.delegatedAuthorization(identity.projectId, activeAutonomy.grant.id, 'project_memory.approve', normalizedActor, inspectedKnowledgeMap, oldTask.id);
434
+ if (!delegation) {
435
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Replacement Knowledge refresh requires delegated approval evidence.');
436
+ }
437
+ const reconciled = reconcileKnowledgeMap(currentKnowledgeMap, scannedKnowledgeMap);
438
+ const approved = applyKnowledgeSelections(reconciled, [], normalizedActor, now);
439
+ if (!approved.approval) {
440
+ throw new WorkflowError('STATE_CORRUPT', 'Replacement Knowledge refresh did not produce approval evidence.');
441
+ }
442
+ refreshedKnowledgeMap = {
443
+ ...approved,
444
+ revision: currentKnowledgeMap.revision + 2,
445
+ approval: {
446
+ ...approved.approval,
447
+ authorizationMode: 'delegated',
448
+ delegation,
449
+ },
450
+ };
451
+ }
452
+ }
453
+ const projectRoot = this.store.projectRoot(identity.projectId);
454
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
455
+ applyProjectTransaction({
456
+ projectRoot,
457
+ projectId: identity.projectId,
458
+ transactionId: `tx-${createEntityId('TASK', now.getTime()).toLowerCase()}`,
459
+ kind: 'split-required-task-replacement',
460
+ now,
461
+ targets: [
462
+ { path: relative(path.join(this.store.discoveryRoot(identity.projectId, discovery.id), 'state.json')), content: `${JSON.stringify(consumedDiscovery, null, 2)}\n` },
463
+ { path: relative(path.join(this.store.taskRoot(identity.projectId, replacementTaskId), 'brief.md')), content: normalizedBriefContent },
464
+ { path: relative(path.join(this.store.taskRoot(identity.projectId, replacementTaskId), 'state.json')), content: `${JSON.stringify(replacementTask, null, 2)}\n` },
465
+ { path: relative(path.join(this.store.taskRoot(identity.projectId, oldTask.id), 'state.json')), content: `${JSON.stringify(cancelledOldTask, null, 2)}\n` },
466
+ { path: relative(path.join(this.store.lockRoot(identity.projectId), `${oldTask.id}.json`)), content: null },
467
+ ...(refreshedKnowledgeMap
468
+ ? [{ path: relative(path.join(projectRoot, 'knowledge-map.json')), content: `${JSON.stringify(refreshedKnowledgeMap, null, 2)}\n` }]
469
+ : []),
470
+ { path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'plan.json')), content: bundle.planContent },
471
+ { path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'scope-change-events.jsonl')), content: bundle.sidecarContent },
472
+ { path: relative(path.join(this.store.milestoneRoot(identity.projectId, milestone.id), 'state.json')), content: bundle.stateContent },
473
+ ],
474
+ validate: () => {
475
+ const savedDiscovery = this.store.readDiscovery(identity.projectId, discovery.id);
476
+ const savedOldTask = this.store.readTask(identity.projectId, oldTask.id);
477
+ const savedReplacement = this.store.readTask(identity.projectId, replacementTaskId);
478
+ const savedMilestone = this.store.readMilestone(identity.projectId, milestone.id);
479
+ if (savedDiscovery.materializedEntityId !== replacementTaskId
480
+ || savedOldTask.status !== 'cancelled'
481
+ || savedOldTask.replacedByTaskId !== replacementTaskId
482
+ || savedReplacement.replacementForTaskId !== oldTask.id) {
483
+ throw new WorkflowError('STATE_CORRUPT', 'Replacement transaction relationship validation failed.');
484
+ }
485
+ if (locks.inspect(oldTask.id)) {
486
+ throw new WorkflowError('STATE_CORRUPT', 'Replacement transaction retained authority for the cancelled Task.');
487
+ }
488
+ if (refreshedKnowledgeMap) {
489
+ const savedKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
490
+ if (savedKnowledgeMap.revision !== refreshedKnowledgeMap.revision
491
+ || savedKnowledgeMap.status !== 'active'
492
+ || savedKnowledgeMap.approval?.authorizationMode !== 'delegated') {
493
+ throw new WorkflowError('STATE_CORRUPT', 'Replacement transaction Knowledge refresh validation failed.');
494
+ }
495
+ }
496
+ assertMilestoneMembershipIntegrity(savedMilestone, (taskId) => this.store.readTask(identity.projectId, taskId));
497
+ },
498
+ });
499
+ return {
500
+ oldTask: this.store.readTask(identity.projectId, oldTask.id),
501
+ replacementTask: this.store.readTask(identity.projectId, replacementTaskId),
502
+ discovery: this.store.readDiscovery(identity.projectId, discovery.id),
503
+ milestone: this.readMilestoneCurrent(identity.projectId, milestone.id),
504
+ };
505
+ }
252
506
  materializeMilestone(repository, discoveryId, expectedRevision, title, baseBranch = 'main') {
253
507
  const { identity } = this.context(repository);
254
508
  const discovery = this.store.readDiscovery(identity.projectId, discoveryId);
@@ -784,6 +1038,7 @@ export class WorkflowService {
784
1038
  const { identity } = this.context(repository);
785
1039
  const task = this.store.readTask(identity.projectId, taskId);
786
1040
  assertExpectedRevision(task, expectedRevision);
1041
+ this.assertTaskContextChangeAllowed(task);
787
1042
  if (!taskCanRebindKnowledge(task)) {
788
1043
  throw new WorkflowError('TRANSITION_BLOCKED', `Cannot rebind Task Plan knowledge while Task is ${task.status}.`);
789
1044
  }
@@ -870,6 +1125,7 @@ export class WorkflowService {
870
1125
  const currentMap = this.store.readKnowledgeMap(identity.projectId);
871
1126
  assertExpectedRevision(task, expectedTaskRevision);
872
1127
  assertExpectedRevision(currentMap, expectedKnowledgeMapRevision);
1128
+ this.assertTaskContextChangeAllowed(task);
873
1129
  if (!taskCanRebindKnowledge(task)) {
874
1130
  throw new WorkflowError('TRANSITION_BLOCKED', `Cannot refresh Task context while Task is ${task.status}.`);
875
1131
  }
@@ -887,45 +1143,72 @@ export class WorkflowService {
887
1143
  // Validate every delegated transition before the first write so a narrow grant cannot
888
1144
  // leave the composite operation half-applied.
889
1145
  this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
890
- let knowledgeMap = currentMap;
891
- if (inspectedMap.status !== 'active' || !inspectedMap.approval) {
892
- if (currentMap.status !== 'active' || !currentMap.approval) {
893
- throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with safe delegated drift.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
894
- }
895
- assertDelegatedKnowledgeRefresh(refreshAssessment);
896
- if (inspectedMap.status !== 'stale') {
897
- throw new WorkflowError('TRANSITION_BLOCKED', 'Safe delegated Knowledge drift must reconcile to a stale candidate before approval.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
898
- }
899
- if (refreshAssessment.mode === 'delegated-plan-bounded-supporting-source') {
900
- this.assertPlanBoundedSupportingSourceGrant(identity.projectId, delegationGrantId, actor, task);
901
- }
902
- this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspectedMap, task.id);
903
- knowledgeMap = this.reconcileKnowledgeMap(repository, currentMap.revision);
904
- transitions.push('project-memory reconcile');
905
- knowledgeMap = this.approveKnowledgeMapForTaskRefresh(repository, knowledgeMap.revision, [], actor, delegationGrantId, task.id);
906
- transitions.push('project-memory approve');
907
- }
908
- const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
909
- if (task.knowledgeMapRevision === knowledgeMap.revision
910
- && task.knowledgeMapHash === knowledgeMapHash) {
911
- throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
912
- }
913
- let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
914
- if (currentPlanRiskAudit) {
915
- appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
916
- }
917
- transitions.push('task knowledge-rebind');
918
- refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, refreshAssessment.mode === 'delegated-plan-bounded-supporting-source'
919
- ? 'Delegated Plan-bounded supporting-source context refresh.'
920
- : 'Delegated content-only context refresh.', delegationGrantId);
921
- transitions.push('task execution authorize');
922
- return {
923
- mode: refreshAssessment.mode,
924
- transitions,
925
- addedSources: refreshAssessment.addedSources,
926
- knowledgeMap,
927
- task: refreshedTask,
928
- };
1146
+ const projectRoot = this.store.projectRoot(identity.projectId);
1147
+ const relative = (absolute) => path.relative(projectRoot, absolute).split(path.sep).join('/');
1148
+ return runRollbackProjectTransaction({
1149
+ projectRoot,
1150
+ projectId: identity.projectId,
1151
+ transactionId: `tx-${createEntityId('TASK', this.now().getTime()).toLowerCase()}`,
1152
+ kind: 'delegated-task-context-refresh',
1153
+ now: this.now(),
1154
+ targetPaths: [
1155
+ relative(path.join(projectRoot, 'knowledge-map.json')),
1156
+ relative(path.join(this.store.taskRoot(identity.projectId, task.id), 'state.json')),
1157
+ relative(path.join(this.store.taskRoot(identity.projectId, task.id), 'plan.md')),
1158
+ relative(path.join(this.store.taskRoot(identity.projectId, task.id), 'plan-risk-audits.jsonl')),
1159
+ ],
1160
+ mutate: () => {
1161
+ let knowledgeMap = currentMap;
1162
+ if (inspectedMap.status !== 'active' || !inspectedMap.approval) {
1163
+ if (currentMap.status !== 'active' || !currentMap.approval) {
1164
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Automatic Task context refresh requires a previously approved map with safe delegated drift.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
1165
+ }
1166
+ assertDelegatedKnowledgeRefresh(refreshAssessment);
1167
+ if (inspectedMap.status !== 'stale') {
1168
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Safe delegated Knowledge drift must reconcile to a stale candidate before approval.', { currentStatus: currentMap.status, inspectedStatus: inspectedMap.status });
1169
+ }
1170
+ if (refreshAssessment.mode === 'delegated-plan-bounded-supporting-source') {
1171
+ this.assertPlanBoundedSupportingSourceGrant(identity.projectId, delegationGrantId, actor, task);
1172
+ }
1173
+ this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspectedMap, task.id);
1174
+ knowledgeMap = this.reconcileKnowledgeMap(repository, currentMap.revision, true);
1175
+ transitions.push('project-memory reconcile');
1176
+ knowledgeMap = this.approveKnowledgeMapForTaskRefresh(repository, knowledgeMap.revision, [], actor, delegationGrantId, task.id);
1177
+ transitions.push('project-memory approve');
1178
+ }
1179
+ const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
1180
+ if (task.knowledgeMapRevision === knowledgeMap.revision
1181
+ && task.knowledgeMapHash === knowledgeMapHash) {
1182
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Task context is already current; refresh is a no-op.');
1183
+ }
1184
+ let refreshedTask = this.rebindTaskKnowledge(repository, taskId, task.revision);
1185
+ if (currentPlanRiskAudit) {
1186
+ appendReboundPlanRiskAudit(this.store, task, refreshedTask, currentPlanRiskAudit, this.now());
1187
+ }
1188
+ transitions.push('task knowledge-rebind');
1189
+ refreshedTask = this.authorizeTask(repository, taskId, refreshedTask.revision, actor, refreshAssessment.mode === 'delegated-plan-bounded-supporting-source'
1190
+ ? 'Delegated Plan-bounded supporting-source context refresh.'
1191
+ : 'Delegated content-only context refresh.', delegationGrantId);
1192
+ transitions.push('task execution authorize');
1193
+ return {
1194
+ mode: refreshAssessment.mode,
1195
+ transitions,
1196
+ addedSources: refreshAssessment.addedSources,
1197
+ knowledgeMap,
1198
+ task: refreshedTask,
1199
+ };
1200
+ },
1201
+ validate: (result) => {
1202
+ const currentTask = this.store.readTask(identity.projectId, task.id);
1203
+ if (currentTask.revision !== result.task.revision || currentTask.status !== 'ready') {
1204
+ throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction did not commit an authorized Task.');
1205
+ }
1206
+ const currentKnowledgeMap = this.store.readKnowledgeMap(identity.projectId);
1207
+ if (currentKnowledgeMap.revision !== result.knowledgeMap.revision) {
1208
+ throw new WorkflowError('STATE_CORRUPT', 'Context refresh transaction Knowledge Map revision mismatch.');
1209
+ }
1210
+ },
1211
+ });
929
1212
  }
930
1213
  authorizeTask(repository, taskId, expectedRevision, actor, reason = '', delegationGrantId = null) {
931
1214
  const { identity } = this.context(repository);
@@ -1166,7 +1449,6 @@ export class WorkflowService {
1166
1449
  throw new WorkflowError('TRANSITION_BLOCKED', `Task ${taskId} cannot run a Step while ${task.status}.`);
1167
1450
  }
1168
1451
  this.assertExecutionAuthorizationFresh(task);
1169
- const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1170
1452
  const currentPlanBlockingDecisions = readCorrectiveDecisionEvents(this.store, task)
1171
1453
  .filter((event) => event.planHash === task.planHash && event.decision !== 'continue-fix');
1172
1454
  if (currentPlanBlockingDecisions.length > 0) {
@@ -1207,6 +1489,7 @@ export class WorkflowService {
1207
1489
  : null;
1208
1490
  assertGuardedRemediationAttemptAllowed(this.store, task, stepId, planRiskAudit, recovery?.correctiveDecisionEventId ?? null);
1209
1491
  }
1492
+ const knowledgeMap = this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1210
1493
  const dirty = changedFiles(context.identity.repositoryRoot);
1211
1494
  if (step.status === 'planned' && dirty.length > 0) {
1212
1495
  throw new WorkflowError('GIT_PRECONDITION_FAILED', `${stepId} must start from a clean checkout.`, {
@@ -1670,6 +1953,13 @@ export class WorkflowService {
1670
1953
  this.assertTaskKnowledgeBinding(context.identity.repositoryRoot, task);
1671
1954
  return appendCorrectiveDecisionRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
1672
1955
  }
1956
+ recoverRemediationMode(repository, taskId, stepId, expectedRevision, actor, writerToken) {
1957
+ const context = this.context(repository);
1958
+ const task = this.store.readTask(context.identity.projectId, taskId);
1959
+ assertExpectedRevision(task, expectedRevision);
1960
+ this.requireExecutionAdoptionPosture(context.identity, taskId);
1961
+ return appendRemediationModeRecovery(this.store, task, stepId, expectedRevision, actor, writerToken, this.now());
1962
+ }
1673
1963
  recordStepCorrectiveDecision(repository, taskId, stepId, expectedRevision, correctiveAudit, actor, writerToken = null) {
1674
1964
  const { identity } = this.context(repository);
1675
1965
  this.readCurrentAdoptionPostureStrict(identity.projectId);
@@ -1914,11 +2204,34 @@ export class WorkflowService {
1914
2204
  const now = this.now().getTime();
1915
2205
  const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
1916
2206
  const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
2207
+ const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
2208
+ const inspectedVersions = inspectWorkflowVersions(context.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
2209
+ const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
2210
+ ...base,
2211
+ milestoneIds: activeMilestones.filter((milestone) => milestone.baseBranch === base.branch).map((milestone) => milestone.id),
2212
+ }));
2213
+ const dependencyPresent = inspectedVersions.declared !== null
2214
+ || inspectedVersions.locked !== null
2215
+ || inspectedVersions.installed !== null;
2216
+ const versionValues = dependencyPresent
2217
+ ? [
2218
+ inspectedVersions.declared,
2219
+ inspectedVersions.locked,
2220
+ inspectedVersions.installed,
2221
+ inspectedVersions.currentBranch,
2222
+ ...milestoneBases.map((base) => base.version),
2223
+ ]
2224
+ : [];
2225
+ const versionsAligned = versionValues.every((version) => version === PACKAGE_VERSION);
2226
+ const versionBlockers = dependencyPresent && !versionsAligned
2227
+ ? ['Workflow dependency versions diverge across declared, locked, installed, current-branch, or active Milestone base state.']
2228
+ : [];
1917
2229
  const blockers = [
1918
2230
  ...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
1919
2231
  ...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
1920
2232
  ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
1921
2233
  ...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
2234
+ ...versionBlockers,
1922
2235
  ];
1923
2236
  const adoption = buildAdoptionPreparation({
1924
2237
  projectId: context.identity.projectId,
@@ -1945,6 +2258,20 @@ export class WorkflowService {
1945
2258
  activeLeases,
1946
2259
  staleLeases,
1947
2260
  blockers,
2261
+ workflowVersions: {
2262
+ runtime: inspectedVersions.runtime,
2263
+ declared: inspectedVersions.declared,
2264
+ locked: inspectedVersions.locked,
2265
+ installed: inspectedVersions.installed,
2266
+ currentBranch: inspectedVersions.currentBranch,
2267
+ milestoneBases,
2268
+ },
2269
+ dependencyTransport: {
2270
+ safe: versionBlockers.length === 0,
2271
+ requiredAction: versionBlockers.length === 0
2272
+ ? null
2273
+ : 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
2274
+ },
1948
2275
  adoption,
1949
2276
  };
1950
2277
  }
@@ -2094,11 +2421,30 @@ export class WorkflowService {
2094
2421
  };
2095
2422
  return this.store.writeKnowledgeMap(approved, expectedRevision);
2096
2423
  }
2097
- reconcileKnowledgeMap(repository, expectedRevision) {
2424
+ reconcileKnowledgeMap(repository, expectedRevision, allowAtomicTaskRefresh = false) {
2098
2425
  const { identity } = this.context(repository);
2099
2426
  const current = this.store.readKnowledgeMap(identity.projectId);
2100
2427
  assertExpectedRevision(current, expectedRevision);
2101
2428
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
2429
+ if (!allowAtomicTaskRefresh && current.status === 'active' && current.approval) {
2430
+ const milestonesById = new Map(this.listMilestonesCurrent(identity.projectId).map((milestone) => [milestone.id, milestone]));
2431
+ const activeTask = this.store
2432
+ .listTasks(identity.projectId)
2433
+ .find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
2434
+ if (activeTask?.planHash
2435
+ && taskCanRebindKnowledge(activeTask)
2436
+ && !this.taskHasTerminalCorrectiveDecision(activeTask)) {
2437
+ const inspected = inspectKnowledgeMap(current, scanned);
2438
+ const assessment = assessDelegatedKnowledgeRefresh(current, inspected, activeTask, this.store.hashArtifact(this.store.taskRoot(identity.projectId, activeTask.id), 'plan.md'));
2439
+ if (assessment.eligible
2440
+ && this.delegatedContextRefreshOptions(identity.projectId, activeTask, inspected, assessment.mode).length > 0) {
2441
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
2442
+ taskId: activeTask.id,
2443
+ requiredAction: 'Run the exact task context-refresh action returned by next.',
2444
+ });
2445
+ }
2446
+ }
2447
+ }
2102
2448
  return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
2103
2449
  }
2104
2450
  graphRefreshRequest(repository, graphKind, mode = 'moderate') {
@@ -2157,10 +2503,17 @@ export class WorkflowService {
2157
2503
  const taskNext = activeTask.status === 'planning'
2158
2504
  ? null
2159
2505
  : this.nextForTaskWithDelegation(status.projectId, activeTask, activePlanRiskAudit);
2160
- // Pending strict review is completion work for the already-recorded commit. It must
2161
- // outrank Project Knowledge refresh/rebind; otherwise the active Step makes the rebind
2162
- // returned by `next` impossible to execute.
2163
- if (taskNext?.action === 'task step-review') {
2506
+ // Active Step work must outrank Project Knowledge refresh/rebind. Both completion and
2507
+ // strict review are executable while context refresh correctly remains forbidden.
2508
+ const hardStop = taskNext?.action === 'task corrective-decision'
2509
+ && taskNext.correctiveDecisionGate?.hardStop === true;
2510
+ const terminalCorrectiveDecision = taskNext?.correctiveDecisionGate?.decision;
2511
+ if (taskNext?.action === 'task step-complete'
2512
+ || taskNext?.action === 'task step-review'
2513
+ || taskNext?.action === 'task remediation-mode-recover'
2514
+ || hardStop
2515
+ || terminalCorrectiveDecision === 'split-required'
2516
+ || terminalCorrectiveDecision === 'stop-escalate') {
2164
2517
  return withAdoption(taskNext);
2165
2518
  }
2166
2519
  if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
@@ -2183,11 +2536,17 @@ export class WorkflowService {
2183
2536
  ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
2184
2537
  : [];
2185
2538
  return withAdoption({
2186
- scope: 'project-memory',
2539
+ scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
2187
2540
  status: knowledgeMap.status,
2188
- action,
2541
+ action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
2189
2542
  revision: knowledgeMap.revision,
2190
- taskId: activeTask.id,
2543
+ ...(contextRefreshOptions.length > 0
2544
+ ? { taskId: activeTask.id }
2545
+ : {
2546
+ stalenessOriginTaskId: activeTask.id,
2547
+ authorizesTask: false,
2548
+ provenanceNotice: 'This Task identifier is diagnostic provenance only; the Project Knowledge action does not authorize or resume it.',
2549
+ }),
2191
2550
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
2192
2551
  ...(contextRefreshOptions.length > 0
2193
2552
  ? {
@@ -2423,9 +2782,18 @@ export class WorkflowService {
2423
2782
  ? this.delegatedApprovalOptions(projectId, transition, task)
2424
2783
  : [];
2425
2784
  const failedReviewAttempts = countFailedReviewAttempts(this.store.taskRoot(projectId, task.id));
2426
- const guardedCorrectivePosture = planRiskAudit
2427
- ? findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit)
2428
- : null;
2785
+ let remediationModeRecovery = null;
2786
+ let guardedCorrectivePosture = null;
2787
+ if (planRiskAudit) {
2788
+ try {
2789
+ guardedCorrectivePosture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
2790
+ }
2791
+ catch (error) {
2792
+ remediationModeRecovery = findRemediationModeRecoveryCandidate(this.store, task);
2793
+ if (!remediationModeRecovery)
2794
+ throw error;
2795
+ }
2796
+ }
2429
2797
  const next = nextForTask(task);
2430
2798
  const handoffPosture = readTaskC1Posture(this.store, task);
2431
2799
  const strictStepReviewStep = planRiskAudit
@@ -2469,7 +2837,24 @@ export class WorkflowService {
2469
2837
  }
2470
2838
  : {};
2471
2839
  let guardedCorrectiveOverlay = {};
2472
- if (!strictStepReviewStep && guardedCorrectivePosture) {
2840
+ if (remediationModeRecovery) {
2841
+ guardedCorrectiveOverlay = {
2842
+ action: 'task remediation-mode-recover',
2843
+ stepId: remediationModeRecovery.remediation.stepId,
2844
+ remediationModeRecovery: {
2845
+ state: 'required',
2846
+ remediationEventId: remediationModeRecovery.remediation.eventId,
2847
+ remediationEventHash: remediationModeRecovery.remediation.eventHash,
2848
+ correctiveDecisionEventId: remediationModeRecovery.decision.eventId,
2849
+ correctiveDecisionRecoveryEventId: remediationModeRecovery.correctiveRecovery.eventId,
2850
+ attemptOrdinal: remediationModeRecovery.remediation.attemptOrdinal,
2851
+ fromMode: 'ordinary',
2852
+ toMode: 'corrective',
2853
+ requiredActor: remediationModeRecovery.actor,
2854
+ },
2855
+ };
2856
+ }
2857
+ if (!remediationModeRecovery && !strictStepReviewStep && guardedCorrectivePosture) {
2473
2858
  switch (guardedCorrectivePosture.posture) {
2474
2859
  case 'decision-required':
2475
2860
  if (guardedCorrectivePosture.correctiveDecision
@@ -2556,6 +2941,56 @@ export class WorkflowService {
2556
2941
  };
2557
2942
  break;
2558
2943
  case 'split-required':
2944
+ {
2945
+ const replacementDiscoveries = this.store.listDiscoveries(projectId)
2946
+ .filter((discovery) => discovery.status === 'ready_to_materialize');
2947
+ const replacementDiscovery = replacementDiscoveries.length === 1 ? replacementDiscoveries[0] : null;
2948
+ const replacementMilestone = task.milestoneId
2949
+ ? this.store.readMilestone(projectId, task.milestoneId)
2950
+ : null;
2951
+ guardedCorrectiveOverlay = {
2952
+ action: replacementDiscovery ? 'task replacement-materialize' : 'discovery start',
2953
+ ...(replacementDiscovery ? {} : { blockedAction: 'task replacement-materialize' }),
2954
+ stepId: guardedCorrectivePosture.stepId,
2955
+ correctiveDecisionGate: {
2956
+ attemptCount: guardedCorrectivePosture.attemptCount,
2957
+ decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
2958
+ decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
2959
+ humanApprovalRequired: false,
2960
+ },
2961
+ replacementGate: {
2962
+ milestoneId: task.milestoneId,
2963
+ oldTaskId: task.id,
2964
+ requiredDiscoveryStatus: 'ready_to_materialize',
2965
+ oldMembershipDisposition: 'cancelled',
2966
+ replacementMembershipDisposition: 'required',
2967
+ transaction: 'journal-recoverable',
2968
+ discoveryCandidates: replacementDiscoveries.map((discovery) => ({
2969
+ id: discovery.id,
2970
+ revision: discovery.revision,
2971
+ goal: discovery.goal,
2972
+ })),
2973
+ ...(replacementDiscovery && replacementMilestone
2974
+ ? {
2975
+ exactTransition: {
2976
+ command: 'task replacement-materialize',
2977
+ taskId: task.id,
2978
+ expectedTaskRevision: task.revision,
2979
+ discoveryId: replacementDiscovery.id,
2980
+ expectedDiscoveryRevision: replacementDiscovery.revision,
2981
+ milestoneId: replacementMilestone.id,
2982
+ expectedMilestoneRevision: replacementMilestone.revision,
2983
+ },
2984
+ }
2985
+ : {
2986
+ requiredAction: replacementDiscoveries.length === 0
2987
+ ? 'Create and complete one thin replacement Discovery, then run next again.'
2988
+ : 'Select exactly one ready replacement Discovery explicitly; next will not guess between candidates.',
2989
+ }),
2990
+ },
2991
+ };
2992
+ }
2993
+ break;
2559
2994
  case 'stop-escalate':
2560
2995
  guardedCorrectiveOverlay = {
2561
2996
  action: 'doctor',
@@ -2624,6 +3059,34 @@ export class WorkflowService {
2624
3059
  }
2625
3060
  return result;
2626
3061
  }
3062
+ assertTaskContextChangeAllowed(task) {
3063
+ const posture = this.taskTerminalCorrectivePosture(task);
3064
+ if (!posture)
3065
+ return;
3066
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has terminal corrective decision ${posture.posture}; Project Knowledge cannot rebind or refresh this Task.`, {
3067
+ taskId: task.id,
3068
+ stepId: posture.stepId,
3069
+ decision: posture.posture,
3070
+ decisionEventId: posture.correctiveDecision?.eventId ?? null,
3071
+ requiredAction: posture.posture === 'split-required'
3072
+ ? 'Materialize an atomic replacement Task through the exact replacement route.'
3073
+ : 'Escalate outside this Task; stop-escalate cannot be resumed or rebound.',
3074
+ });
3075
+ }
3076
+ taskHasTerminalCorrectiveDecision(task) {
3077
+ return this.taskTerminalCorrectivePosture(task) !== null;
3078
+ }
3079
+ taskTerminalCorrectivePosture(task) {
3080
+ if (!task.planHash)
3081
+ return null;
3082
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
3083
+ if (!planRiskAudit)
3084
+ return null;
3085
+ const posture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
3086
+ return posture?.posture === 'split-required' || posture?.posture === 'stop-escalate'
3087
+ ? posture
3088
+ : null;
3089
+ }
2627
3090
  delegatedApprovalOptions(projectId, transition, target) {
2628
3091
  return this.store.listDelegations(projectId)
2629
3092
  .filter((grant) => {