@fitlab-ai/agent-infra 0.11.11 → 0.11.13

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 (154) hide show
  1. package/dist/bin/internal-cli.js +2 -2
  2. package/dist/lib/agent-clients/adapter.js +7 -1
  3. package/dist/lib/agent-clients/adapters/claude-code.js +5 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +78 -63
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/binding.js +114 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +447 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +130 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +131 -0
  9. package/dist/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.js +2 -2
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +48 -1
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +124 -62
  12. package/dist/lib/{internal/codex-sandbox-controller.js → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js} +3 -3
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -5
  14. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -213
  15. package/dist/lib/agent-clients/adapters/codex-orchestration.js +468 -0
  16. package/dist/lib/agent-clients/adapters/codex.js +40 -1
  17. package/dist/lib/agent-clients/adapters/index.js +2 -10
  18. package/dist/lib/agent-clients/tokens.js +3 -2
  19. package/dist/lib/internal/agent-client.js +132 -72
  20. package/dist/lib/internal/cli-route-inventory.js +2 -2
  21. package/dist/lib/internal/sandbox-control.js +1 -81
  22. package/dist/lib/internal/task-finalization.js +26 -19
  23. package/dist/lib/internal/task-lifecycle.js +1 -1
  24. package/dist/lib/internal/task-operation-registry.js +1 -1
  25. package/dist/lib/internal/task-validate.js +1 -1
  26. package/dist/lib/platform/github-client.js +33 -3
  27. package/dist/lib/platform/issues.js +18 -6
  28. package/dist/lib/runtime/agent-runtime.js +6 -29
  29. package/dist/lib/sandbox/commands/control-status.js +1 -0
  30. package/dist/lib/sandbox/commands/create.js +7 -2
  31. package/dist/lib/sandbox/commands/ls.js +4 -5
  32. package/dist/lib/sandbox/commands/rm.js +1 -1
  33. package/dist/lib/sandbox/control/agent-operation.js +34 -0
  34. package/dist/lib/sandbox/control/client.js +14 -201
  35. package/dist/lib/sandbox/control/executor.js +12 -125
  36. package/dist/lib/sandbox/control/lifecycle.js +66 -4
  37. package/dist/lib/sandbox/control/protocol.js +15 -54
  38. package/dist/lib/sandbox/control/server.js +216 -281
  39. package/dist/lib/sandbox/control/task-view.js +0 -13
  40. package/dist/lib/sandbox/mounts.js +9 -1
  41. package/dist/lib/sandbox/recovery.js +31 -10
  42. package/dist/lib/sandbox/removal.js +28 -5
  43. package/dist/lib/sandbox/workspace-identity.js +18 -6
  44. package/dist/lib/sandbox/workspace-view.js +56 -19
  45. package/dist/lib/task/commands/ls.js +3 -3
  46. package/dist/lib/task/control-authority.js +56 -264
  47. package/dist/lib/task/control-recovery.js +2 -3
  48. package/dist/lib/task/create.js +2 -2
  49. package/dist/lib/task/delegation-receipts.js +74 -161
  50. package/dist/lib/task/events.js +88 -44
  51. package/dist/lib/task/finalization.js +35 -6
  52. package/dist/lib/task/lifecycle.js +274 -115
  53. package/dist/lib/task/orchestration.js +201 -194
  54. package/dist/lib/task/runtime-paths.js +10 -0
  55. package/dist/lib/task/short-id.js +37 -27
  56. package/dist/lib/task/verification-engine.js +32 -26
  57. package/dist/lib/task/workspace-snapshot.js +1 -1
  58. package/dist/lifecycle-build-manifest.json +3 -3
  59. package/lib/agent-clients/adapter.ts +39 -0
  60. package/lib/agent-clients/adapters/claude-code.ts +5 -1
  61. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +105 -80
  62. package/lib/agent-clients/adapters/codex-lifecycle/binding.ts +147 -0
  63. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +510 -0
  64. package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +158 -0
  65. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +1 -1
  66. package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +151 -0
  67. package/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.ts +4 -4
  68. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +43 -0
  69. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +8 -5
  70. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +190 -65
  71. package/lib/{internal/codex-sandbox-controller.ts → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts} +2 -2
  72. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -5
  73. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +270 -224
  74. package/lib/agent-clients/adapters/codex-orchestration.ts +632 -0
  75. package/lib/agent-clients/adapters/codex.ts +44 -1
  76. package/lib/agent-clients/adapters/index.ts +7 -20
  77. package/lib/agent-clients/tokens.ts +4 -3
  78. package/lib/internal/agent-client.ts +142 -73
  79. package/lib/internal/cli-route-inventory.ts +2 -2
  80. package/lib/internal/sandbox-control.ts +0 -97
  81. package/lib/internal/task-finalization.ts +30 -18
  82. package/lib/internal/task-lifecycle.ts +1 -1
  83. package/lib/internal/task-operation-registry.ts +1 -1
  84. package/lib/internal/task-validate.ts +1 -1
  85. package/lib/platform/github-client.ts +34 -3
  86. package/lib/platform/issues.ts +18 -6
  87. package/lib/runtime/agent-runtime.ts +6 -41
  88. package/lib/sandbox/commands/control-status.ts +1 -0
  89. package/lib/sandbox/commands/create.ts +7 -1
  90. package/lib/sandbox/commands/ls.ts +4 -5
  91. package/lib/sandbox/commands/rm.ts +1 -1
  92. package/lib/sandbox/control/agent-operation.ts +49 -0
  93. package/lib/sandbox/control/client.ts +20 -254
  94. package/lib/sandbox/control/executor.ts +13 -136
  95. package/lib/sandbox/control/lifecycle.ts +67 -4
  96. package/lib/sandbox/control/protocol.ts +22 -66
  97. package/lib/sandbox/control/server.ts +21 -95
  98. package/lib/sandbox/control/task-view.ts +0 -19
  99. package/lib/sandbox/mounts.ts +10 -1
  100. package/lib/sandbox/recovery.ts +38 -17
  101. package/lib/sandbox/removal.ts +33 -7
  102. package/lib/sandbox/workspace-identity.ts +16 -6
  103. package/lib/sandbox/workspace-view.ts +50 -18
  104. package/lib/task/commands/ls.ts +3 -3
  105. package/lib/task/control-authority.ts +66 -378
  106. package/lib/task/control-recovery.ts +3 -4
  107. package/lib/task/create.ts +3 -3
  108. package/lib/task/delegation-receipts.ts +87 -231
  109. package/lib/task/events.ts +100 -42
  110. package/lib/task/finalization.ts +37 -7
  111. package/lib/task/lifecycle.ts +257 -109
  112. package/lib/task/orchestration.ts +260 -237
  113. package/lib/task/runtime-paths.ts +12 -0
  114. package/lib/task/short-id.ts +33 -40
  115. package/lib/task/verification-engine.ts +30 -25
  116. package/lib/task/workspace-snapshot.ts +1 -1
  117. package/package.json +1 -1
  118. package/templates/.agents/hooks/lifecycle-delegation.js +1 -25
  119. package/templates/.agents/rules/README.en.md +2 -1
  120. package/templates/.agents/rules/README.zh-CN.md +2 -1
  121. package/templates/.agents/rules/next-step-output.en.md +6 -19
  122. package/templates/.agents/rules/next-step-output.zh-CN.md +6 -19
  123. package/templates/.agents/rules/strategy-pattern.en.md +25 -0
  124. package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
  125. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
  126. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  127. package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
  128. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
  129. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  130. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  131. package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
  132. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
  133. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +0 -1
  134. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +0 -1
  135. package/templates/.agents/skills/complete-task/SKILL.en.md +5 -29
  136. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -26
  137. package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
  138. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
  139. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  140. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  141. package/templates/.agents/skills/review-code/SKILL.en.md +1 -1
  142. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +1 -1
  143. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  144. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  145. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  146. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  147. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  148. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  149. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +0 -546
  150. package/dist/lib/internal/codex-lifecycle.js +0 -385
  151. package/dist/lib/task/codex-orchestration.js +0 -206
  152. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +0 -776
  153. package/lib/internal/codex-lifecycle.ts +0 -390
  154. package/lib/task/codex-orchestration.ts +0 -276
@@ -45,12 +45,6 @@ import { getArtifactSchema } from './artifact-schema.ts';
45
45
  import { canonicalSemanticDigest, inspectArtifactContract } from './artifact-operations.ts';
46
46
  import { inspectReviewIdentity } from './review-identity.ts';
47
47
  import { parseCompletionFacts, type CompletionFact } from './completion-facts.ts';
48
- import {
49
- consumeLifecycleRecoveryAttestation,
50
- lifecycleRecoveryAttestationDigest,
51
- validateLifecycleRecoveryAttestation,
52
- type LifecycleRecoveryAttestationV1
53
- } from './control-authority.ts';
54
48
  import { readManualValidationCompletion } from './manual-validation-completion.ts';
55
49
  import { parseLifecyclePathDecision } from './lifecycle-path.ts';
56
50
 
@@ -93,8 +87,6 @@ type TaskEventError = { code: TaskEventErrorCode; message: string };
93
87
  type TaskEventOptions = TaskWriteOptions & {
94
88
  commitOrchestrationCompletion?: (plan: OrchestrationStageCompletion) => void;
95
89
  lockAlreadyHeld?: boolean;
96
- lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
97
- deferLifecycleRecoveryConsumption?: boolean;
98
90
  };
99
91
  type TaskEventResult = {
100
92
  status: 'planned' | 'applied' | 'no-op' | 'failed'; changed: boolean;
@@ -395,6 +387,78 @@ function openStartedIdentity(rows: ReturnType<typeof pairEntries>, family: Event
395
387
  return matches.length === 1 ? matches[0] : matches.length > 1 ? { conflict: true as const } : null;
396
388
  }
397
389
 
390
+ function repairMissingCodeFixQualifier(
391
+ request: TaskEventRequest,
392
+ family: EventFamily,
393
+ open: { row: { step: string }; round: number; fixFor?: string; implementationInput?: string },
394
+ repoRoot: string
395
+ ) {
396
+ if (family !== 'code' || open.fixFor !== undefined || !request.fixFor || request.implementationInput !== undefined) {
397
+ return {
398
+ error: {
399
+ code: 'EVENT_ARTIFACT_CONFLICT',
400
+ message: `open code start cannot be repaired (family=${family}, openFixFor=${open.fixFor ?? 'none'}, requestedFixFor=${request.fixFor ?? 'none'}, implementationInput=${request.implementationInput ?? 'none'})`
401
+ } as TaskEventError,
402
+ context: null
403
+ };
404
+ }
405
+ const repair = normalizeStarted(request, repoRoot);
406
+ if ('error' in repair) return { error: repair.error, context: repair.context };
407
+ if (repair.request.round !== open.round || repair.request.fixFor !== request.fixFor || repair.request.implementationInput !== undefined) {
408
+ return { error: { code: 'EVENT_ARTIFACT_CONFLICT', message: 'current code artifact context does not match the open round and requested fix input' } as TaskEventError, context: repair.context };
409
+ }
410
+ return repair;
411
+ }
412
+
413
+ function resolveOpenStartedEvent(
414
+ request: TaskEventRequest,
415
+ family: EventFamily,
416
+ open: { row: { step: string; agent: string; started: string; note: string }; round: number; fixFor?: string; implementationInput?: string },
417
+ repoRoot: string
418
+ ) {
419
+ if (open.row.agent !== request.agent) return { status: 'error' as const, error: { code: 'EVENT_LOG_CONFLICT', message: 'open started event has a different agent' } as TaskEventError };
420
+ if (family === 'manual-validation' && request.transactionId !== undefined) {
421
+ const existingTransactionId = /(?:^|;\s*)transaction=([A-Za-z0-9][A-Za-z0-9._-]{0,127})(?:;|$)/u.exec(open.row.note)?.[1];
422
+ if (existingTransactionId !== undefined && existingTransactionId !== request.transactionId) {
423
+ return { status: 'error' as const, error: { code: 'EVENT_LOG_CONFLICT', message: 'open manual-validation started event has a different transactionId' } as TaskEventError };
424
+ }
425
+ }
426
+ if (request.round !== undefined && request.round !== open.round) {
427
+ return { status: 'error' as const, error: { code: 'EVENT_ARTIFACT_CONFLICT', message: `round ${request.round} conflicts with open round ${open.round}` } as TaskEventError };
428
+ }
429
+ if (request.fixFor !== undefined && request.fixFor !== open.fixFor) {
430
+ const repair = repairMissingCodeFixQualifier(request, family, open, repoRoot);
431
+ if (!repair) return { status: 'error' as const, error: { code: 'EVENT_ARTIFACT_CONFLICT', message: `fixFor '${request.fixFor}' conflicts with open event` } as TaskEventError };
432
+ if ('error' in repair) return { status: 'error' as const, error: repair.error, context: repair.context };
433
+ return { status: 'repair' as const, repair: repair.request, context: repair.context, superseded: { action: open.row.step } };
434
+ }
435
+ if (request.implementationInput !== open.implementationInput) {
436
+ return { status: 'error' as const, error: { code: 'EVENT_ARTIFACT_CONFLICT', message: `implementationInput '${request.implementationInput ?? ''}' conflicts with open event` } as TaskEventError };
437
+ }
438
+ return {
439
+ status: 'replay' as const,
440
+ replay: {
441
+ ...request,
442
+ round: open.round,
443
+ artifact: artifactName(FAMILY[family].artifact, open.round),
444
+ fixFor: open.fixFor,
445
+ implementationInput: open.implementationInput
446
+ },
447
+ startedAt: open.row.started
448
+ };
449
+ }
450
+
451
+ function hasOtherOpenStarted(rows: ReturnType<typeof pairEntries>, allowedAction: string): boolean {
452
+ return rows.some((row) => row.started && !row.done && row.step !== allowedAction);
453
+ }
454
+
455
+ function hasConflictingOpenStarted(
456
+ rows: ReturnType<typeof pairEntries>,
457
+ recovery: { action: string } | null
458
+ ): boolean {
459
+ return recovery ? hasOtherOpenStarted(rows, recovery.action) : rows.some((row) => row.started && !row.done);
460
+ }
461
+
398
462
  function reviewInputFamily(family: EventFamily): ArtifactFamily {
399
463
  return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
400
464
  }
@@ -581,53 +645,35 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
581
645
  const rows = startedBackedRows(pairEntries(section.entries));
582
646
  let normalized = request;
583
647
  let artifactContext: ArtifactContextResult | null = null;
648
+ let supersededOpenStart: { action: string } | null = null;
584
649
  if (initialParts.phase === 'started') {
585
650
  const openIdentity = openStartedIdentity(rows, initialParts.family);
586
651
  if (openIdentity && 'conflict' in openIdentity) return failed(request, { code: 'EVENT_LOG_CONFLICT', message: 'artifact family has more than one open started event' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
587
652
  if (openIdentity) {
588
- if (openIdentity.row.agent !== request.agent) return failed(request, { code: 'EVENT_LOG_CONFLICT', message: 'open started event has a different agent' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
589
- if (initialParts.family === 'manual-validation' && request.transactionId !== undefined) {
590
- const existingTransactionId = /(?:^|;\s*)transaction=([A-Za-z0-9][A-Za-z0-9._-]{0,127})(?:;|$)/u.exec(openIdentity.row.note)?.[1];
591
- if (existingTransactionId !== undefined && existingTransactionId !== request.transactionId) return failed(request, { code: 'EVENT_LOG_CONFLICT', message: 'open manual-validation started event has a different transactionId' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
592
- }
593
- if (request.round !== undefined && request.round !== openIdentity.round) return failed(request, { code: 'EVENT_ARTIFACT_CONFLICT', message: `round ${request.round} conflicts with open round ${openIdentity.round}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
594
- if (request.fixFor !== undefined && request.fixFor !== openIdentity.fixFor) return failed(request, { code: 'EVENT_ARTIFACT_CONFLICT', message: `fixFor '${request.fixFor}' conflicts with open event` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
595
- if (request.implementationInput !== openIdentity.implementationInput) return failed(request, { code: 'EVENT_ARTIFACT_CONFLICT', message: `implementationInput '${request.implementationInput ?? ''}' conflicts with open event` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
596
- normalized = { ...request, round: openIdentity.round, artifact: artifactName(FAMILY[initialParts.family].artifact, openIdentity.round), fixFor: openIdentity.fixFor, implementationInput: openIdentity.implementationInput };
597
- return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, typeof frontmatter.current_step === 'string' ? frontmatter.current_step : '', identity(normalized), openIdentity.row.started, frontmatter, null);
653
+ const resolvedStart = resolveOpenStartedEvent(request, initialParts.family, openIdentity, resolved.repoRoot);
654
+ if (resolvedStart.status === 'error') return failed(request, resolvedStart.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
655
+ if (resolvedStart.status === 'replay') return successNoOp(resolvedStart.replay, resolved.taskId, resolved.taskMdPath, typeof frontmatter.current_step === 'string' ? frontmatter.current_step : '', identity(resolvedStart.replay), resolvedStart.startedAt, frontmatter, null);
656
+ normalized = resolvedStart.repair;
657
+ artifactContext = resolvedStart.context;
658
+ supersededOpenStart = resolvedStart.superseded;
598
659
  }
599
- if (rows.some((item) => item.started && !item.done)) {
660
+ if (hasConflictingOpenStarted(rows, supersededOpenStart)) {
600
661
  return failed(request, {
601
662
  code: 'EVENT_TRANSITION_INVALID',
602
663
  message: 'EXECUTION_BUSY: another lifecycle execution is open'
603
664
  }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
604
665
  }
605
- const result = normalizeStarted(request, resolved.repoRoot);
606
- artifactContext = result.context;
607
- if ('error' in result) return failed(request, result.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, artifactContext });
608
- normalized = result.request;
666
+ if (!supersededOpenStart) {
667
+ const result = normalizeStarted(request, resolved.repoRoot);
668
+ artifactContext = result.context;
669
+ if ('error' in result) return failed(request, result.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, artifactContext });
670
+ normalized = result.request;
671
+ }
609
672
  } else if (initialParts.phase === 'completed') {
610
673
  const identity = request.artifact ? parseArtifactName(request.artifact) : null;
611
674
  if (identity?.family === FAMILY[initialParts.family].artifact) normalized = { ...request, round: identity.round };
612
675
  }
613
676
  const eventIdentity = identity(normalized);
614
- const lifecycleAuthority = options.lifecycleRecoveryAttestation ?? null;
615
- if (lifecycleAuthority) {
616
- try { validateLifecycleRecoveryAttestation(lifecycleAuthority); }
617
- catch (error) {
618
- return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: error instanceof Error ? error.message : String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
619
- }
620
- const expectedFamily = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
621
- if (eventIdentity.phase !== 'completed'
622
- || lifecycleAuthority.phase !== 'task-event.completed'
623
- || lifecycleAuthority.taskId !== resolved.taskId
624
- || lifecycleAuthority.family !== expectedFamily
625
- || lifecycleAuthority.artifact !== normalized.artifact
626
- || lifecycleAuthority.round !== normalized.round
627
- || (normalized.requestId !== undefined && lifecycleAuthority.lifecycleRequestId !== normalized.requestId)) {
628
- return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: 'lifecycle completion authority does not match the event tuple' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
629
- }
630
- }
631
677
  const currentStep = typeof frontmatter.current_step === 'string' ? frontmatter.current_step : '';
632
678
  const matchingRows = rows.filter((item) => item.step === eventIdentity.action);
633
679
  const manual = eventIdentity.family === 'manual-validation';
@@ -716,7 +762,8 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
716
762
  const capability = canStart(lifecycleAction(eventIdentity.family), facts.facts, trigger);
717
763
  const safetyFailure = capability.reasonCode === 'TASK_NOT_ACTIVE'
718
764
  || capability.reasonCode === 'LIFECYCLE_EXECUTION_OPEN';
719
- if (!capability.allowed && (normalized.initiator === 'orchestrator' || safetyFailure)) {
765
+ if (!capability.allowed && (normalized.initiator === 'orchestrator' || safetyFailure)
766
+ && !(supersededOpenStart && capability.reasonCode === 'LIFECYCLE_EXECUTION_OPEN')) {
720
767
  return failed(normalized, { code: 'EVENT_TRANSITION_INVALID', message: `${capability.reasonCode}: ${capability.evidence.join(', ')}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
721
768
  }
722
769
  }
@@ -792,7 +839,18 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
792
839
  }
793
840
  const step = eventIdentity.phase === 'started' || eventIdentity.target === null ? currentStep : eventIdentity.target;
794
841
  const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
795
- const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
842
+ const supersededBody = supersededOpenStart
843
+ ? appendActivityEntry(section, {
844
+ time: metadata.timestamp,
845
+ step: `${supersededOpenStart.action} [aborted]`,
846
+ agent: normalized.agent,
847
+ note: `superseded by ${eventIdentity.action}`
848
+ })
849
+ : section.body;
850
+ const body = appendActivityEntry(
851
+ supersededOpenStart ? { ...section, body: supersededBody } : section,
852
+ { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note }
853
+ );
796
854
  const frontmatterSet: Record<string, string> = { current_step: step, assigned_to: normalized.agent };
797
855
  if (currentFact) frontmatterSet.completion_facts = JSON.stringify(replaceCompletionFact(parseCompletionFacts(frontmatter.completion_facts), currentFact));
798
856
  if (completedArtifact && ['analyze', 'plan', 'code'].includes(eventIdentity.family)) {
@@ -14,6 +14,7 @@ import { parseTaskFrontmatter } from './frontmatter.ts';
14
14
  import { recoverPlatformOperations } from './platform-operation-recovery.ts';
15
15
  import {
16
16
  applyTaskLifecycle,
17
+ applyTaskLifecycleWithinTaskLock,
17
18
  inspectTaskLifecycleProgress,
18
19
  type TaskLifecycleOptions,
19
20
  type TaskLifecycleRequest,
@@ -78,6 +79,7 @@ type TaskFinalizationOptions = Readonly<{
78
79
  issueInspect?: typeof inspectPlatformIssue;
79
80
  verify?: typeof verifyTaskEvent;
80
81
  preflight?: typeof verifyTaskEvent;
82
+ beforeLifecycle?: (taskId: string) => Promise<void>;
81
83
  }>;
82
84
 
83
85
  type TaskFinalizationReceipt = Readonly<{
@@ -865,7 +867,9 @@ async function prepareUnderLock(
865
867
  options: TaskFinalizationOptions
866
868
  ): Promise<TaskFinalizationResult> {
867
869
  const repoRoot = path.resolve(options.repoRoot);
868
- const lifecycle = options.lifecycle ?? applyTaskLifecycle;
870
+ const lifecycle = options.lifecycle ?? ((lifecycleRequest, lifecycleOptions) => (
871
+ applyTaskLifecycleWithinTaskLock(lifecycleRequest, lifecycleOptions ?? { repoRoot }, taskId)
872
+ ));
869
873
  const backfill = options.backfill ?? backfillCompletionComments;
870
874
  const commentSync = options.commentSync ?? syncPlatformComment;
871
875
  const issueSync = options.issueSync ?? syncPlatformIssue;
@@ -929,6 +933,8 @@ async function prepareUnderLock(
929
933
  ['requirements', 'issue-type', 'fields'].includes(operation.name)
930
934
  && operation.status !== 'applied' && operation.status !== 'no-op'
931
935
  && !(operation.name === 'requirements' && operation.status === 'skipped' && operation.reasonCode === 'NO_REQUIREMENTS_ANCHOR')
936
+ && !(!synced.capabilities.push && ['issue-type', 'fields'].includes(operation.name)
937
+ && operation.status === 'skipped' && operation.reasonCode === 'PUSH_REQUIRED')
932
938
  );
933
939
  if ((synced.status !== 'applied' && synced.status !== 'no-op') || synced.error || badOperation) {
934
940
  const error = synced.error ?? {
@@ -938,6 +944,16 @@ async function prepareUnderLock(
938
944
  };
939
945
  return failed(taskId, errorOf(error, 'FINALIZATION_ISSUE_METADATA_FAILED', true));
940
946
  }
947
+ if (!synced.capabilities.push) {
948
+ const missingCapabilitySkip = ['issue-type', 'fields'].find((name) => !issueOperations?.some((operation) =>
949
+ operation.name === name && operation.status === 'skipped' && operation.reasonCode === 'PUSH_REQUIRED'
950
+ ));
951
+ if (missingCapabilitySkip) return failed(taskId, {
952
+ code: 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
953
+ message: `Issue metadata operation '${missingCapabilitySkip}' did not report the required PUSH_REQUIRED capability skip`,
954
+ retryable: true
955
+ });
956
+ }
941
957
  let inspected: IssueResult;
942
958
  try { inspected = await issueInspect(taskId, { cwd: repoRoot }); }
943
959
  catch (error) { return failed(taskId, errorOf(error, 'FINALIZATION_ISSUE_INSPECT_FAILED', true)); }
@@ -949,7 +965,8 @@ async function prepareUnderLock(
949
965
  retryable: true
950
966
  });
951
967
  }
952
- for (const name of ['requirements', 'issue-type', 'fields']) {
968
+ const rereadNames = synced.capabilities.push ? ['requirements', 'issue-type', 'fields'] : ['requirements'];
969
+ for (const name of rereadNames) {
953
970
  const operation = issueOperations?.find((candidate) => candidate.name === name);
954
971
  if (!operation) return failed(taskId, {
955
972
  code: 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
@@ -1189,11 +1206,11 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
1189
1206
  retryable: false
1190
1207
  });
1191
1208
  receipt = updateReceipt(repoRoot, receipt, { verification: 'pending' });
1192
- const lifecycle = options.lifecycle ?? applyTaskLifecycle;
1193
- const result = lifecycle(
1194
- { taskRef: resolved.taskId, intent: 'complete', agent: request.agent },
1195
- { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) }
1196
- );
1209
+ const lifecycleRequest = { taskRef: resolved.taskId, intent: 'complete' as const, agent: request.agent };
1210
+ const lifecycleOptions = { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) };
1211
+ const result = options.lifecycle
1212
+ ? options.lifecycle(lifecycleRequest, lifecycleOptions)
1213
+ : applyTaskLifecycleWithinTaskLock(lifecycleRequest, lifecycleOptions, resolved.taskId);
1197
1214
  const step = lifecycleStep(result);
1198
1215
  if (result.status !== 'applied' && result.status !== 'no-op') {
1199
1216
  const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: step.error });
@@ -1287,6 +1304,19 @@ async function commitPreparedTaskFinalization(request: TaskFinalizationRequest,
1287
1304
  async function applyTaskFinalization(request: TaskFinalizationRequest, options: TaskFinalizationOptions): Promise<TaskFinalizationResult> {
1288
1305
  const prepared = await prepareTaskFinalization(request, options);
1289
1306
  if (prepared.status !== 'prepared') return prepared;
1307
+ if (options.beforeLifecycle) {
1308
+ try {
1309
+ await options.beforeLifecycle(prepared.taskId!);
1310
+ } catch (error) {
1311
+ const detail = errorOf(error, 'TASK_FINALIZATION_PRE_LIFECYCLE_FAILED', true);
1312
+ return failed(prepared.taskId, detail, {
1313
+ changed: prepared.changed,
1314
+ completedSteps: prepared.completedSteps,
1315
+ pendingSteps: prepared.pendingSteps,
1316
+ warnings: prepared.warnings
1317
+ });
1318
+ }
1319
+ }
1290
1320
  return commitPreparedTaskFinalization(request, options);
1291
1321
  }
1292
1322