codex-workflow-v2 2.0.0-alpha.7.2.1 → 2.0.0-beta.2

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 (39) hide show
  1. package/README.md +24 -5
  2. package/dist/src/alpha6/milestone.d.ts +27 -0
  3. package/dist/src/alpha6/milestone.js +152 -1
  4. package/dist/src/alpha6/milestone.js.map +1 -1
  5. package/dist/src/beta1/project-transaction.d.ts +52 -0
  6. package/dist/src/beta1/project-transaction.js +297 -0
  7. package/dist/src/beta1/project-transaction.js.map +1 -0
  8. package/dist/src/cli.js +239 -4
  9. package/dist/src/cli.js.map +1 -1
  10. package/dist/src/contracts.d.ts +20 -0
  11. package/dist/src/diagnostics.d.ts +11 -0
  12. package/dist/src/diagnostics.js +54 -0
  13. package/dist/src/diagnostics.js.map +1 -1
  14. package/dist/src/git.js +2 -5
  15. package/dist/src/git.js.map +1 -1
  16. package/dist/src/reviewer.d.ts +4 -0
  17. package/dist/src/reviewer.js +24 -7
  18. package/dist/src/reviewer.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 +28 -1
  23. package/dist/src/workflow.js +658 -54
  24. package/dist/src/workflow.js.map +1 -1
  25. package/docs/autonomy-guardrails.md +11 -3
  26. package/docs/beta1-stabilization-brief.md +165 -0
  27. package/docs/beta2-initial-assembly-navigation-brief.md +616 -0
  28. package/docs/delegated-approval.md +4 -3
  29. package/docs/development-flow.md +23 -6
  30. package/docs/project-memory.md +7 -5
  31. package/docs/release.md +16 -2
  32. package/docs/split-required-recovery.md +54 -0
  33. package/docs/stable-release-defect-register.md +597 -0
  34. package/docs/updating-existing-project.md +2 -0
  35. package/package.json +2 -2
  36. package/plugins/codex-workflow-gateway/references/protocol.md +62 -3
  37. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +53 -2
  38. package/references/state-machine.md +4 -1
  39. 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
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 { assessMilestoneInitialAssembly, 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);
@@ -1871,11 +2154,15 @@ export class WorkflowService {
1871
2154
  const tasks = this.store.listTasks(identity.projectId);
1872
2155
  const rawMilestones = this.store.listMilestones(identity.projectId);
1873
2156
  const adoption = this.inspectAdoptionPreparation(identity, tasks, rawMilestones);
2157
+ const milestones = this.listMilestonesCurrent(identity.projectId, adoption.currentPosture);
1874
2158
  return {
1875
2159
  projectId: identity.projectId,
1876
2160
  discoveries: this.store.listDiscoveries(identity.projectId),
1877
2161
  tasks,
1878
- milestones: this.listMilestonesCurrent(identity.projectId, adoption.currentPosture),
2162
+ milestones,
2163
+ milestoneInitialAssembly: milestones
2164
+ .map((milestone) => this.describeMilestoneInitialAssembly(identity.projectId, milestone, tasks))
2165
+ .filter((value) => value !== null),
1879
2166
  delegations: this.store.listDelegations(identity.projectId),
1880
2167
  adoption,
1881
2168
  };
@@ -1921,11 +2208,34 @@ export class WorkflowService {
1921
2208
  const now = this.now().getTime();
1922
2209
  const activeLeases = leases.filter((lease) => Date.parse(lease.expiresAt) > now);
1923
2210
  const staleLeases = leases.filter((lease) => Date.parse(lease.expiresAt) <= now);
2211
+ const activeMilestones = milestones.filter((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
2212
+ const inspectedVersions = inspectWorkflowVersions(context.identity.repositoryRoot, activeMilestones.map((milestone) => milestone.baseBranch));
2213
+ const milestoneBases = inspectedVersions.milestoneBases.map((base) => ({
2214
+ ...base,
2215
+ milestoneIds: activeMilestones.filter((milestone) => milestone.baseBranch === base.branch).map((milestone) => milestone.id),
2216
+ }));
2217
+ const dependencyPresent = inspectedVersions.declared !== null
2218
+ || inspectedVersions.locked !== null
2219
+ || inspectedVersions.installed !== null;
2220
+ const versionValues = dependencyPresent
2221
+ ? [
2222
+ inspectedVersions.declared,
2223
+ inspectedVersions.locked,
2224
+ inspectedVersions.installed,
2225
+ inspectedVersions.currentBranch,
2226
+ ...milestoneBases.map((base) => base.version),
2227
+ ]
2228
+ : [];
2229
+ const versionsAligned = versionValues.every((version) => version === PACKAGE_VERSION);
2230
+ const versionBlockers = dependencyPresent && !versionsAligned
2231
+ ? ['Workflow dependency versions diverge across declared, locked, installed, current-branch, or active Milestone base state.']
2232
+ : [];
1924
2233
  const blockers = [
1925
2234
  ...(dirty.length > 0 ? ['Repository checkout is not clean.'] : []),
1926
2235
  ...(runningSteps.length > 0 ? ['At least one Worker Step is in progress.'] : []),
1927
2236
  ...(activeLeases.length > 0 ? ['At least one writer lease is active.'] : []),
1928
2237
  ...(staleLeases.length > 0 ? ['At least one stale writer lease requires explicit repair.'] : []),
2238
+ ...versionBlockers,
1929
2239
  ];
1930
2240
  const adoption = buildAdoptionPreparation({
1931
2241
  projectId: context.identity.projectId,
@@ -1952,6 +2262,20 @@ export class WorkflowService {
1952
2262
  activeLeases,
1953
2263
  staleLeases,
1954
2264
  blockers,
2265
+ workflowVersions: {
2266
+ runtime: inspectedVersions.runtime,
2267
+ declared: inspectedVersions.declared,
2268
+ locked: inspectedVersions.locked,
2269
+ installed: inspectedVersions.installed,
2270
+ currentBranch: inspectedVersions.currentBranch,
2271
+ milestoneBases,
2272
+ },
2273
+ dependencyTransport: {
2274
+ safe: versionBlockers.length === 0,
2275
+ requiredAction: versionBlockers.length === 0
2276
+ ? null
2277
+ : 'Land only package.json/package-lock.json dependency changes on every active Milestone base; do not transport unrelated product commits.',
2278
+ },
1955
2279
  adoption,
1956
2280
  };
1957
2281
  }
@@ -2101,11 +2425,30 @@ export class WorkflowService {
2101
2425
  };
2102
2426
  return this.store.writeKnowledgeMap(approved, expectedRevision);
2103
2427
  }
2104
- reconcileKnowledgeMap(repository, expectedRevision) {
2428
+ reconcileKnowledgeMap(repository, expectedRevision, allowAtomicTaskRefresh = false) {
2105
2429
  const { identity } = this.context(repository);
2106
2430
  const current = this.store.readKnowledgeMap(identity.projectId);
2107
2431
  assertExpectedRevision(current, expectedRevision);
2108
2432
  const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
2433
+ if (!allowAtomicTaskRefresh && current.status === 'active' && current.approval) {
2434
+ const milestonesById = new Map(this.listMilestonesCurrent(identity.projectId).map((milestone) => [milestone.id, milestone]));
2435
+ const activeTask = this.store
2436
+ .listTasks(identity.projectId)
2437
+ .find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
2438
+ if (activeTask?.planHash
2439
+ && taskCanRebindKnowledge(activeTask)
2440
+ && !this.taskHasTerminalCorrectiveDecision(activeTask)) {
2441
+ const inspected = inspectKnowledgeMap(current, scanned);
2442
+ const assessment = assessDelegatedKnowledgeRefresh(current, inspected, activeTask, this.store.hashArtifact(this.store.taskRoot(identity.projectId, activeTask.id), 'plan.md'));
2443
+ if (assessment.eligible
2444
+ && this.delegatedContextRefreshOptions(identity.projectId, activeTask, inspected, assessment.mode).length > 0) {
2445
+ throw new WorkflowError('TRANSITION_BLOCKED', 'Standalone Project Knowledge reconcile would strand an authorized Task context refresh.', {
2446
+ taskId: activeTask.id,
2447
+ requiredAction: 'Run the exact task context-refresh action returned by next.',
2448
+ });
2449
+ }
2450
+ }
2451
+ }
2109
2452
  return this.store.writeKnowledgeMap(reconcileKnowledgeMap(current, scanned), expectedRevision);
2110
2453
  }
2111
2454
  graphRefreshRequest(repository, graphKind, mode = 'moderate') {
@@ -2168,10 +2511,13 @@ export class WorkflowService {
2168
2511
  // strict review are executable while context refresh correctly remains forbidden.
2169
2512
  const hardStop = taskNext?.action === 'task corrective-decision'
2170
2513
  && taskNext.correctiveDecisionGate?.hardStop === true;
2514
+ const terminalCorrectiveDecision = taskNext?.correctiveDecisionGate?.decision;
2171
2515
  if (taskNext?.action === 'task step-complete'
2172
2516
  || taskNext?.action === 'task step-review'
2173
2517
  || taskNext?.action === 'task remediation-mode-recover'
2174
- || hardStop) {
2518
+ || hardStop
2519
+ || terminalCorrectiveDecision === 'split-required'
2520
+ || terminalCorrectiveDecision === 'stop-escalate') {
2175
2521
  return withAdoption(taskNext);
2176
2522
  }
2177
2523
  if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
@@ -2194,11 +2540,17 @@ export class WorkflowService {
2194
2540
  ? this.delegatedContextRefreshOptions(status.projectId, activeTask, knowledgeMap, refreshAssessment.mode)
2195
2541
  : [];
2196
2542
  return withAdoption({
2197
- scope: 'project-memory',
2543
+ scope: contextRefreshOptions.length > 0 ? 'task' : 'project-memory',
2198
2544
  status: knowledgeMap.status,
2199
- action,
2545
+ action: contextRefreshOptions.length > 0 ? 'task context-refresh' : action,
2200
2546
  revision: knowledgeMap.revision,
2201
- taskId: activeTask.id,
2547
+ ...(contextRefreshOptions.length > 0
2548
+ ? { taskId: activeTask.id }
2549
+ : {
2550
+ stalenessOriginTaskId: activeTask.id,
2551
+ authorizesTask: false,
2552
+ provenanceNotice: 'This Task identifier is diagnostic provenance only; the Project Knowledge action does not authorize or resume it.',
2553
+ }),
2202
2554
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
2203
2555
  ...(contextRefreshOptions.length > 0
2204
2556
  ? {
@@ -2240,6 +2592,31 @@ export class WorkflowService {
2240
2592
  }
2241
2593
  const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
2242
2594
  if (activeMilestone) {
2595
+ const initialAssembly = status.milestoneInitialAssembly
2596
+ .find((candidate) => candidate.milestoneId === activeMilestone.id) ?? null;
2597
+ if (initialAssembly) {
2598
+ const activeDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status));
2599
+ if (activeDiscovery) {
2600
+ return withAdoption({
2601
+ scope: 'discovery',
2602
+ id: activeDiscovery.id,
2603
+ status: activeDiscovery.status,
2604
+ action: activeDiscovery.status === 'ready_to_materialize' ? 'discovery materialize' : 'discovery update',
2605
+ readiness: assessDiscovery(activeDiscovery),
2606
+ milestoneInitialAssembly: initialAssembly,
2607
+ });
2608
+ }
2609
+ return withAdoption({
2610
+ scope: 'milestone',
2611
+ id: activeMilestone.id,
2612
+ status: activeMilestone.status,
2613
+ action: 'milestone initial-assembly',
2614
+ revision: activeMilestone.revision,
2615
+ linkedTaskIds: initialAssembly.linkedTaskIds,
2616
+ executionAvailable: initialAssembly.executionAvailable,
2617
+ milestoneInitialAssembly: initialAssembly,
2618
+ });
2619
+ }
2243
2620
  if (activeMilestone.status === 'awaiting_final_acceptance') {
2244
2621
  const root = this.store.milestoneRoot(activeMilestone.projectId, activeMilestone.id);
2245
2622
  const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
@@ -2446,7 +2823,7 @@ export class WorkflowService {
2446
2823
  throw error;
2447
2824
  }
2448
2825
  }
2449
- const next = nextForTask(task);
2826
+ const next = this.taskGitNavigationOverride(task) ?? nextForTask(task);
2450
2827
  const handoffPosture = readTaskC1Posture(this.store, task);
2451
2828
  const strictStepReviewStep = planRiskAudit
2452
2829
  ? task.steps.find((step) => {
@@ -2593,6 +2970,56 @@ export class WorkflowService {
2593
2970
  };
2594
2971
  break;
2595
2972
  case 'split-required':
2973
+ {
2974
+ const replacementDiscoveries = this.store.listDiscoveries(projectId)
2975
+ .filter((discovery) => discovery.status === 'ready_to_materialize');
2976
+ const replacementDiscovery = replacementDiscoveries.length === 1 ? replacementDiscoveries[0] : null;
2977
+ const replacementMilestone = task.milestoneId
2978
+ ? this.store.readMilestone(projectId, task.milestoneId)
2979
+ : null;
2980
+ guardedCorrectiveOverlay = {
2981
+ action: replacementDiscovery ? 'task replacement-materialize' : 'discovery start',
2982
+ ...(replacementDiscovery ? {} : { blockedAction: 'task replacement-materialize' }),
2983
+ stepId: guardedCorrectivePosture.stepId,
2984
+ correctiveDecisionGate: {
2985
+ attemptCount: guardedCorrectivePosture.attemptCount,
2986
+ decision: guardedCorrectivePosture.correctiveDecision?.decision ?? null,
2987
+ decisionEventId: guardedCorrectivePosture.correctiveDecision?.eventId ?? null,
2988
+ humanApprovalRequired: false,
2989
+ },
2990
+ replacementGate: {
2991
+ milestoneId: task.milestoneId,
2992
+ oldTaskId: task.id,
2993
+ requiredDiscoveryStatus: 'ready_to_materialize',
2994
+ oldMembershipDisposition: 'cancelled',
2995
+ replacementMembershipDisposition: 'required',
2996
+ transaction: 'journal-recoverable',
2997
+ discoveryCandidates: replacementDiscoveries.map((discovery) => ({
2998
+ id: discovery.id,
2999
+ revision: discovery.revision,
3000
+ goal: discovery.goal,
3001
+ })),
3002
+ ...(replacementDiscovery && replacementMilestone
3003
+ ? {
3004
+ exactTransition: {
3005
+ command: 'task replacement-materialize',
3006
+ taskId: task.id,
3007
+ expectedTaskRevision: task.revision,
3008
+ discoveryId: replacementDiscovery.id,
3009
+ expectedDiscoveryRevision: replacementDiscovery.revision,
3010
+ milestoneId: replacementMilestone.id,
3011
+ expectedMilestoneRevision: replacementMilestone.revision,
3012
+ },
3013
+ }
3014
+ : {
3015
+ requiredAction: replacementDiscoveries.length === 0
3016
+ ? 'Create and complete one thin replacement Discovery, then run next again.'
3017
+ : 'Select exactly one ready replacement Discovery explicitly; next will not guess between candidates.',
3018
+ }),
3019
+ },
3020
+ };
3021
+ }
3022
+ break;
2596
3023
  case 'stop-escalate':
2597
3024
  guardedCorrectiveOverlay = {
2598
3025
  action: 'doctor',
@@ -2653,13 +3080,59 @@ export class WorkflowService {
2653
3080
  ...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
2654
3081
  };
2655
3082
  if (handoffPosture.state === 'pending') {
2656
- return {
3083
+ return this.withTaskNavigationContracts(task, {
2657
3084
  ...result,
2658
3085
  action: 'task claim',
2659
3086
  blockedAction: next.action,
2660
- };
3087
+ });
2661
3088
  }
2662
- return result;
3089
+ const navigableResult = ['task plan-set', 'task plan-set with independent corrective audit']
3090
+ .includes(String(result.action))
3091
+ ? {
3092
+ ...result,
3093
+ taskPlanContract: this.taskPlanContract(projectId, task),
3094
+ }
3095
+ : result;
3096
+ return this.withTaskNavigationContracts(task, navigableResult);
3097
+ }
3098
+ taskPlanContract(projectId, task) {
3099
+ const map = this.store.readKnowledgeMap(projectId);
3100
+ return {
3101
+ requiredRequirementIds: [...task.requirements],
3102
+ requiredAcceptanceIds: [...task.acceptance],
3103
+ currentRisks: [...task.risks],
3104
+ exactInclusionRequired: true,
3105
+ briefHash: task.briefHash,
3106
+ knowledgeMap: knowledgeSummary(map),
3107
+ };
3108
+ }
3109
+ assertTaskContextChangeAllowed(task) {
3110
+ const posture = this.taskTerminalCorrectivePosture(task);
3111
+ if (!posture)
3112
+ return;
3113
+ throw new WorkflowError('TRANSITION_BLOCKED', `Task ${task.id} has terminal corrective decision ${posture.posture}; Project Knowledge cannot rebind or refresh this Task.`, {
3114
+ taskId: task.id,
3115
+ stepId: posture.stepId,
3116
+ decision: posture.posture,
3117
+ decisionEventId: posture.correctiveDecision?.eventId ?? null,
3118
+ requiredAction: posture.posture === 'split-required'
3119
+ ? 'Materialize an atomic replacement Task through the exact replacement route.'
3120
+ : 'Escalate outside this Task; stop-escalate cannot be resumed or rebound.',
3121
+ });
3122
+ }
3123
+ taskHasTerminalCorrectiveDecision(task) {
3124
+ return this.taskTerminalCorrectivePosture(task) !== null;
3125
+ }
3126
+ taskTerminalCorrectivePosture(task) {
3127
+ if (!task.planHash)
3128
+ return null;
3129
+ const planRiskAudit = readCurrentPlanRiskAudit(this.store, task);
3130
+ if (!planRiskAudit)
3131
+ return null;
3132
+ const posture = findCurrentGuardedRemediationPosture(this.store, task, planRiskAudit);
3133
+ return posture?.posture === 'split-required' || posture?.posture === 'stop-escalate'
3134
+ ? posture
3135
+ : null;
2663
3136
  }
2664
3137
  delegatedApprovalOptions(projectId, transition, target) {
2665
3138
  return this.store.listDelegations(projectId)
@@ -2779,6 +3252,70 @@ export class WorkflowService {
2779
3252
  ? milestone
2780
3253
  : readMilestoneWithIntegrity(this.store, projectId, milestone.id, (taskId) => this.store.readTask(projectId, taskId), () => this.store.listTasks(projectId))));
2781
3254
  }
3255
+ taskGitNavigationOverride(task) {
3256
+ if (task.status !== 'merging' || task.workspaceOwner !== 'local' || !task.baseCommit || !task.taskBranch) {
3257
+ return null;
3258
+ }
3259
+ const currentBase = runGit(task.repositoryRoot, ['rev-parse', task.baseBranch]);
3260
+ if (currentBase === task.baseCommit)
3261
+ return null;
3262
+ return {
3263
+ scope: 'task',
3264
+ id: task.id,
3265
+ status: task.status,
3266
+ action: 'task sync-base',
3267
+ revision: task.revision,
3268
+ baseSyncRequired: {
3269
+ baseBranch: task.baseBranch,
3270
+ recordedBaseCommit: task.baseCommit,
3271
+ currentBaseCommit: currentBase,
3272
+ },
3273
+ };
3274
+ }
3275
+ withTaskNavigationContracts(task, next) {
3276
+ const action = typeof next.action === 'string' ? next.action : null;
3277
+ if (!action)
3278
+ return next;
3279
+ const policy = gitMutationPolicyForAction(task, action);
3280
+ const claimedC1Posture = readTaskC1Posture(this.store, task).state === 'claimed';
3281
+ const writerTokenContract = writerTokenContractForAction(action, claimedC1Posture);
3282
+ return {
3283
+ ...next,
3284
+ ...(policy ? { gitMutationPolicy: policy } : {}),
3285
+ ...(writerTokenContract ? { writerTokenContract } : {}),
3286
+ };
3287
+ }
3288
+ describeMilestoneInitialAssembly(projectId, milestone, tasks) {
3289
+ const assessment = assessMilestoneInitialAssembly({
3290
+ store: this.store,
3291
+ projectId,
3292
+ milestone,
3293
+ listTasks: () => tasks,
3294
+ });
3295
+ if (!assessment.eligible)
3296
+ return null;
3297
+ const options = [
3298
+ {
3299
+ action: 'linked-task-assembly',
3300
+ commands: ['discovery start', 'discovery materialize'],
3301
+ description: 'Start or materialize the next linked Task Discovery for this Milestone before closing the initial membership Plan.',
3302
+ },
3303
+ ];
3304
+ if (assessment.orphanTaskIds.length > 0) {
3305
+ options.push({
3306
+ action: 'milestone plan-set',
3307
+ commands: ['milestone plan-set'],
3308
+ description: 'Record one complete initial Milestone Plan that classifies every currently linked Task.',
3309
+ });
3310
+ }
3311
+ return {
3312
+ milestoneId: milestone.id,
3313
+ revision: milestone.revision,
3314
+ linkedTaskIds: assessment.orphanTaskIds,
3315
+ executionAvailable: false,
3316
+ options,
3317
+ };
3318
+ }
2782
3319
  prepareMilestoneScopeChangeCandidate(projectId, milestone, plan, actor) {
2783
3320
  validateMilestonePlan(plan);
2784
3321
  return prepareMilestoneScopeChangeCandidate({
@@ -3434,13 +3971,13 @@ function nextForTask(task) {
3434
3971
  awaiting_execution_authorization: 'task authorize',
3435
3972
  ready: task.workspaceOwner ? (stepsComplete ? 'task submit' : 'task run') : 'task start',
3436
3973
  in_progress: activeStep ? 'task step-complete' : stepsComplete ? 'task submit' : 'task run',
3437
- validating: task.review.status === 'passed' ? 'task result-set' : 'task review-record',
3974
+ validating: task.review.status === 'passed' ? 'task result-set' : 'task review-launch',
3438
3975
  needs_fix: task.steps.some((step) => step.status === 'failed') ? 'task run' : 'task plan-set',
3439
3976
  awaiting_final_acceptance: 'task accept',
3440
3977
  merging: 'task merge',
3441
3978
  ready_to_merge: 'task merge-confirm',
3442
3979
  merged: 'none',
3443
- blocked: task.review.status === 'unverified' ? 'task review-record' : 'doctor',
3980
+ blocked: task.review.status === 'unverified' ? 'task review-launch' : 'doctor',
3444
3981
  cancelled: 'none',
3445
3982
  };
3446
3983
  const selectedStep = activeStep ?? failedStep ?? runnableStep;
@@ -3462,6 +3999,73 @@ function nextForTask(task) {
3462
3999
  : {}),
3463
4000
  };
3464
4001
  }
4002
+ function gitMutationPolicyForAction(task, action) {
4003
+ const metadataWrites = ['.git/index', '.git/index.lock', '.git/HEAD', '.git/refs', '.git/logs'];
4004
+ switch (action) {
4005
+ case 'task start':
4006
+ return {
4007
+ permission: 'git-metadata-write',
4008
+ requiredBeforeFirstCall: true,
4009
+ boundedToTransition: true,
4010
+ transition: 'task start',
4011
+ appliesWhen: { workspaceOwner: 'local' },
4012
+ metadataWrites,
4013
+ };
4014
+ case 'task step-complete':
4015
+ return {
4016
+ permission: 'git-metadata-write',
4017
+ requiredBeforeFirstCall: true,
4018
+ boundedToTransition: true,
4019
+ transition: 'task step-complete',
4020
+ metadataWrites,
4021
+ };
4022
+ case 'task merge':
4023
+ if (task.workspaceOwner !== 'local')
4024
+ return null;
4025
+ return {
4026
+ permission: 'git-metadata-write',
4027
+ requiredBeforeFirstCall: true,
4028
+ boundedToTransition: true,
4029
+ transition: 'task merge',
4030
+ metadataWrites,
4031
+ };
4032
+ case 'task sync-base':
4033
+ return {
4034
+ permission: 'git-metadata-write',
4035
+ requiredBeforeFirstCall: true,
4036
+ boundedToTransition: true,
4037
+ transition: 'task sync-base',
4038
+ metadataWrites,
4039
+ };
4040
+ default:
4041
+ return null;
4042
+ }
4043
+ }
4044
+ function writerTokenContractForAction(action, claimedC1Posture = false) {
4045
+ const alwaysLeaseBound = ['task step-complete', 'task submit', 'task merge', 'task merge-confirm'];
4046
+ const claimedC1LeaseBound = [
4047
+ 'task run',
4048
+ 'task step-review',
4049
+ 'task review-record',
4050
+ 'task review-launch',
4051
+ 'task corrective-decision-recover',
4052
+ 'task remediation-mode-recover',
4053
+ 'task corrective-decision',
4054
+ 'task handoff',
4055
+ 'task handoff-prepare',
4056
+ 'task handback',
4057
+ 'task handback-create',
4058
+ ];
4059
+ if (!alwaysLeaseBound.includes(action) && !(claimedC1Posture && claimedC1LeaseBound.includes(action))) {
4060
+ return null;
4061
+ }
4062
+ return {
4063
+ required: true,
4064
+ option: '--writer-token',
4065
+ source: 'active-writer-lease',
4066
+ sensitive: true,
4067
+ };
4068
+ }
3465
4069
  function knowledgeSummary(map) {
3466
4070
  return {
3467
4071
  revision: map.revision,