codex-workflow-v2 2.0.0-alpha.7.2.1 → 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.
@@ -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
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, readMilestoneForScopeChange, readMilestoneWithIntegrity, } from './alpha6/milestone.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);
@@ -1921,11 +2204,34 @@ export class WorkflowService {
1921
2204
  const now = this.now().getTime();
1922
2205
  const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
1923
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
+ : [];
1924
2229
  const blockers = [
1925
2230
  ...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
1926
2231
  ...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
1927
2232
  ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
1928
2233
  ...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
2234
+ ...versionBlockers,
1929
2235
  ];
1930
2236
  const adoption = buildAdoptionPreparation({
1931
2237
  projectId: context.identity.projectId,
@@ -1952,6 +2258,20 @@ export class WorkflowService {
1952
2258
  activeLeases,
1953
2259
  staleLeases,
1954
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
+ },
1955
2275
  adoption,
1956
2276
  };
1957
2277
  }
@@ -2101,11 +2421,30 @@ export class WorkflowService {
2101
2421
  };
2102
2422
  return this.store.writeKnowledgeMap(approved, expectedRevision);
2103
2423
  }
2104
- reconcileKnowledgeMap(repository, expectedRevision) {
2424
+ reconcileKnowledgeMap(repository, expectedRevision, allowAtomicTaskRefresh = false) {
2105
2425
  const { identity } = this.context(repository);
2106
2426
  const current = this.store.readKnowledgeMap(identity.projectId);
2107
2427
  assertExpectedRevision(current, expectedRevision);
2108
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
+ }
2109
2448
  return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
2110
2449
  }
2111
2450
  graphRefreshRequest(repository, graphKind, mode = 'moderate') {
@@ -2168,10 +2507,13 @@ export class WorkflowService {
2168
2507
  // strict review are executable while context refresh correctly remains forbidden.
2169
2508
  const hardStop = taskNext?.action === 'task corrective-decision'
2170
2509
  && taskNext.correctiveDecisionGate?.hardStop === true;
2510
+ const terminalCorrectiveDecision = taskNext?.correctiveDecisionGate?.decision;
2171
2511
  if (taskNext?.action === 'task step-complete'
2172
2512
  || taskNext?.action === 'task step-review'
2173
2513
  || taskNext?.action === 'task remediation-mode-recover'
2174
- || hardStop) {
2514
+ || hardStop
2515
+ || terminalCorrectiveDecision === 'split-required'
2516
+ || terminalCorrectiveDecision === 'stop-escalate') {
2175
2517
  return withAdoption(taskNext);
2176
2518
  }
2177
2519
  if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
@@ -2194,11 +2536,17 @@ export class WorkflowService {
2194
2536
  ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
2195
2537
  : [];
2196
2538
  return withAdoption({
2197
- scope: 'project-memory',
2539
+ scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
2198
2540
  status: knowledgeMap.status,
2199
- action,
2541
+ action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
2200
2542
  revision: knowledgeMap.revision,
2201
- 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
+ }),
2202
2550
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
2203
2551
  ...(contextRefreshOptions.length > 0
2204
2552
  ? {
@@ -2593,6 +2941,56 @@ export class WorkflowService {
2593
2941
  };
2594
2942
  break;
2595
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;
2596
2994
  case 'stop-escalate':
2597
2995
  guardedCorrectiveOverlay = {
2598
2996
  action: 'doctor',
@@ -2661,6 +3059,34 @@ export class WorkflowService {
2661
3059
  }
2662
3060
  return result;
2663
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
+ }
2664
3090
  delegatedApprovalOptions(projectId, transition, target) {
2665
3091
  return this.store.listDelegations(projectId)
2666
3092
  .filter((grant) => {