@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
@@ -21,77 +21,37 @@ function nullableText(value) {
21
21
  function nullableSafeInteger(value) {
22
22
  return value === null || (Number.isSafeInteger(value) && value >= 0);
23
23
  }
24
- const LIFECYCLE_PROVENANCE_KEYS = [
25
- 'protocolVersion', 'packageVersion', 'internalExecutableBuildHash', 'lifecycleContractHash',
26
- 'hookDefinitionHash', 'hookSource', 'hookSourcePathDigest', 'hookSourceHash',
27
- 'capabilitySessionId', 'capabilityTurnId', 'capabilityToolUseId',
28
- 'controllerInstanceDigest', 'controlGeneration'
29
- ];
30
- function isDelegationLifecycleProvenance(value) {
31
- if (!hasExactKeys(value, LIFECYCLE_PROVENANCE_KEYS))
32
- return false;
33
- return Number.isSafeInteger(value.protocolVersion)
34
- && value.protocolVersion > 0
35
- && exactText(value.packageVersion)
36
- && exactText(value.internalExecutableBuildHash)
37
- && exactText(value.lifecycleContractHash)
38
- && exactText(value.hookDefinitionHash)
39
- && ['project', 'managed', 'isolated-user'].includes(value.hookSource)
40
- && exactText(value.hookSourcePathDigest)
41
- && exactText(value.hookSourceHash)
42
- && exactText(value.capabilitySessionId)
43
- && exactText(value.capabilityTurnId)
44
- && exactText(value.capabilityToolUseId)
45
- && nullableText(value.controllerInstanceDigest)
46
- && nullableText(value.controlGeneration);
47
- }
48
- const HOST_EVIDENCE_REQUIRED_KEYS = [
49
- 'kind', 'hookDefinitionHash', 'startRevision', 'stopRevision', 'consumer', 'consumedAt'
50
- ];
51
- const HOST_EVIDENCE_OPTIONAL_KEYS = [
52
- 'protocolVersion', 'packageVersion', 'internalExecutableBuildHash', 'lifecycleContractHash',
53
- 'hookSource', 'hookSourcePathDigest', 'hookSourceHash', 'capabilitySessionId',
54
- 'capabilityTurnId', 'capabilityToolUseId', 'spawnToolUseId', 'spawnObservedAt',
55
- 'controllerInstanceDigest', 'controlGeneration'
56
- ];
57
- function isDelegationHostEvidence(value) {
58
- if (!hasExactKeys(value, HOST_EVIDENCE_REQUIRED_KEYS, HOST_EVIDENCE_OPTIONAL_KEYS))
59
- return false;
60
- if (!['codex-lifecycle-v1', 'codex-lifecycle-v2'].includes(value.kind))
61
- return false;
62
- if (!exactText(value.hookDefinitionHash)
63
- || !Number.isSafeInteger(value.startRevision) || value.startRevision < 1
64
- || !nullableSafeInteger(value.stopRevision)
65
- || !nullableText(value.consumer)
66
- || !nullableText(value.consumedAt))
67
- return false;
68
- if (value.kind === 'codex-lifecycle-v1')
69
- return true;
70
- return Number.isSafeInteger(value.protocolVersion)
71
- && value.protocolVersion > 0
72
- && exactText(value.packageVersion)
73
- && exactText(value.internalExecutableBuildHash)
74
- && exactText(value.lifecycleContractHash)
75
- && ['project', 'managed', 'isolated-user'].includes(value.hookSource)
76
- && exactText(value.hookSourcePathDigest)
77
- && exactText(value.hookSourceHash)
78
- && exactText(value.capabilitySessionId)
79
- && exactText(value.capabilityTurnId)
80
- && exactText(value.spawnToolUseId)
81
- && exactText(value.spawnObservedAt)
82
- && nullableText(value.controllerInstanceDigest)
83
- && nullableText(value.controlGeneration);
84
- }
85
24
  const RECEIPT_KEYS = [
86
25
  'id', 'taskId', 'runId', 'role', 'stage', 'round', 'artifact', 'client',
87
26
  'requestedModel', 'requestedReasoningEffort', 'actualModel', 'actualReasoningEffort',
88
27
  'modelFallbackReason', 'reasoningEffortFallbackReason', 'parentId', 'childId',
89
- 'spawnMode', 'agent', 'status', 'workspaceSnapshotScope', 'lifecycleProvenance',
90
- 'hostEvidence', 'beforeFingerprint', 'afterFingerprint', 'changedPaths', 'createdAt',
28
+ 'spawnMode', 'agent', 'status', 'workspaceSnapshotScope',
29
+ 'beforeFingerprint', 'afterFingerprint', 'changedPaths', 'createdAt',
91
30
  'preparedMonotonicMs', 'spawnDispatchMonotonicMs', 'activationDeadlineMonotonicMs',
92
31
  'spawnDispatchedAt', 'activationDeadlineAt', 'startEvidenceMonotonicMs',
93
32
  'activatedMonotonicMs', 'activatedAt', 'sealedAt', 'consumedAt'
94
33
  ];
34
+ const RECEIPT_OPTIONAL_KEYS = ['adapterEvidence'];
35
+ function isJsonRecord(value) {
36
+ if (!value || typeof value !== 'object' || Array.isArray(value))
37
+ return false;
38
+ try {
39
+ return JSON.parse(JSON.stringify(value)) !== null;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ function withClientEvidence(receipt, evidence) {
46
+ if (evidence === undefined)
47
+ return receipt.adapterEvidence ?? Object.freeze({});
48
+ if (!isJsonRecord(evidence))
49
+ return null;
50
+ return Object.freeze({
51
+ ...(receipt.adapterEvidence ?? {}),
52
+ [receipt.client]: JSON.parse(JSON.stringify(evidence))
53
+ });
54
+ }
95
55
  function hasStatusBoundEvidence(receipt) {
96
56
  const dispatchFields = [
97
57
  receipt.spawnDispatchMonotonicMs,
@@ -114,7 +74,6 @@ function hasStatusBoundEvidence(receipt) {
114
74
  && receipt.modelFallbackReason === null
115
75
  && receipt.reasoningEffortFallbackReason === null
116
76
  && receipt.agent === null
117
- && receipt.hostEvidence === null
118
77
  && receipt.startEvidenceMonotonicMs === null
119
78
  && receipt.activatedMonotonicMs === null
120
79
  && receipt.activatedAt === null
@@ -139,18 +98,12 @@ function hasStatusBoundEvidence(receipt) {
139
98
  && receipt.consumedAt === null;
140
99
  }
141
100
  if (receipt.status === 'aborted') {
142
- const host = receipt.hostEvidence;
143
101
  return receipt.agent === null
144
102
  && receipt.afterFingerprint === null
145
103
  && receipt.changedPaths.length === 0
146
104
  && receipt.sealedAt === null
147
105
  && receipt.consumedAt === null
148
- && host !== null
149
- && host !== undefined
150
- && Number.isSafeInteger(host.stopRevision)
151
- && host.stopRevision > host.startRevision
152
- && host.consumer === receipt.id
153
- && exactText(host.consumedAt);
106
+ && receipt.childId !== null;
154
107
  }
155
108
  if (!exactText(receipt.agent)
156
109
  || normalizeAgentToken(receipt.agent) !== normalizeAgentToken(receipt.client))
@@ -168,7 +121,7 @@ function hasStatusBoundEvidence(receipt) {
168
121
  : receipt.status === 'consumed' && exactText(receipt.consumedAt);
169
122
  }
170
123
  function isDelegationReceipt(value) {
171
- if (!hasExactKeys(value, RECEIPT_KEYS))
124
+ if (!hasExactKeys(value, RECEIPT_KEYS, RECEIPT_OPTIONAL_KEYS))
172
125
  return false;
173
126
  const structurallyValid = exactText(value.id)
174
127
  && exactText(value.taskId)
@@ -191,8 +144,7 @@ function isDelegationReceipt(value) {
191
144
  && nullableText(value.agent)
192
145
  && ['prepared', 'activated', 'stage-completed', 'sealed', 'consumed', 'aborted', 'expired'].includes(value.status)
193
146
  && value.workspaceSnapshotScope === 'task'
194
- && (value.lifecycleProvenance === null || isDelegationLifecycleProvenance(value.lifecycleProvenance))
195
- && (value.hostEvidence === null || isDelegationHostEvidence(value.hostEvidence))
147
+ && (value.adapterEvidence === undefined || isJsonRecord(value.adapterEvidence))
196
148
  && exactText(value.beforeFingerprint)
197
149
  && nullableText(value.afterFingerprint)
198
150
  && Array.isArray(value.changedPaths)
@@ -214,14 +166,6 @@ function isDelegationReceipt(value) {
214
166
  const receipt = value;
215
167
  if (!hasStatusBoundEvidence(receipt))
216
168
  return false;
217
- const host = receipt.hostEvidence;
218
- if (receipt.client === 'codex' && receipt.activatedAt !== null) {
219
- if (host?.kind !== 'codex-lifecycle-v2' || host.capabilitySessionId !== receipt.parentId)
220
- return false;
221
- if (['sealed', 'consumed'].includes(receipt.status)) {
222
- return Number.isSafeInteger(host.stopRevision) && host.stopRevision > host.startRevision;
223
- }
224
- }
225
169
  return true;
226
170
  }
227
171
  const MANAGED_AGENTS = {
@@ -238,10 +182,35 @@ function fail(code, message) {
238
182
  function foldBlankToNull(value) {
239
183
  return value?.trim() ? value : null;
240
184
  }
185
+ function validateSealRequest(receipt, event) {
186
+ if (receipt.status !== 'stage-completed') {
187
+ return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected stage-completed`);
188
+ }
189
+ if (event.childId !== receipt.childId || event.exitCode !== 0) {
190
+ return fail('DELEGATION_STOP_INVALID', 'native stop identity or exit status is invalid');
191
+ }
192
+ const reviewerPathFailure = validateReviewerSealPaths(receipt, event.changedPaths);
193
+ return reviewerPathFailure;
194
+ }
195
+ function validateReviewerSealPaths(receipt, changedPaths) {
196
+ if (receipt.role !== 'reviewer')
197
+ return null;
198
+ const taskRoot = `.agents/workspace/active/${receipt.taskId}/`;
199
+ const allowed = new Set([
200
+ `${taskRoot}${receipt.artifact}`,
201
+ `${taskRoot}task.md`,
202
+ `${taskRoot}.runtime/orchestration.json`
203
+ ]);
204
+ const disallowed = changedPaths.find((entry) => !allowed.has(entry));
205
+ return disallowed
206
+ ? fail('DELEGATION_REVIEWER_WRITE_FORBIDDEN', `reviewer changed forbidden path '${disallowed}'`)
207
+ : null;
208
+ }
241
209
  function prepareDelegation(input, options = {}) {
242
210
  const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)))();
243
211
  return Object.freeze({
244
212
  ...input,
213
+ adapterEvidence: Object.freeze({ ...(input.adapterEvidence ?? {}) }),
245
214
  id: (options.id ?? randomUUID)(),
246
215
  actualModel: null,
247
216
  actualReasoningEffort: null,
@@ -252,7 +221,6 @@ function prepareDelegation(input, options = {}) {
252
221
  spawnMode: null,
253
222
  agent: null,
254
223
  status: 'prepared',
255
- hostEvidence: null,
256
224
  afterFingerprint: null,
257
225
  changedPaths: Object.freeze([]),
258
226
  createdAt: (options.now ?? (() => new Date().toISOString()))(),
@@ -310,33 +278,29 @@ function activateDelegation(receipt, event, options = {}) {
310
278
  if (!event.parentId || (receipt.parentId !== null && event.parentId !== receipt.parentId) || !event.childId || event.childId === event.parentId) {
311
279
  return fail('DELEGATION_IDENTITY_INVALID', 'native parent/child identity does not match the prepared delegation');
312
280
  }
313
- // spawnMode 的必需性按 client 判断:claude-code 宿主结构上不提供该字段,跳过;其余 client 维持硬校验
314
- if (receipt.client !== 'claude-code' && event.spawnMode !== 'fresh') {
281
+ const evidencePolicy = options.evidencePolicy ?? {};
282
+ if (evidencePolicy.spawnModeRequired !== false && event.spawnMode !== 'fresh') {
315
283
  return fail('DELEGATION_FORK_FORBIDDEN', `spawn mode '${event.spawnMode}' is not fresh`);
316
284
  }
317
- // model/effort 维度:claude-code 如实记录、不做门禁;其余 client 维持现状硬校验(HDR-2)
318
285
  const actualModel = foldBlankToNull(event.actualModel);
319
286
  const actualReasoningEffort = foldBlankToNull(event.actualReasoningEffort);
320
- if (receipt.client !== 'claude-code') {
287
+ if (evidencePolicy.actualModelRequired !== false) {
321
288
  if (!actualModel || actualModel.trim() !== actualModel) {
322
289
  return fail('DELEGATION_MODEL_IDENTITY_MISSING', 'native start event must provide a non-empty actual model identity');
323
290
  }
324
291
  }
325
292
  if (actualModel !== null && actualModel !== receipt.requestedModel) {
326
- if (receipt.client !== 'claude-code' && (!event.modelFallbackReason || event.modelFallbackReason.trim() === '')) {
293
+ if (evidencePolicy.fallbackReasonRequired !== false && (!event.modelFallbackReason || event.modelFallbackReason.trim() === '')) {
327
294
  return fail('DELEGATION_MODEL_FALLBACK_UNRECORDED', 'actual model differs from requested model without a fallback reason');
328
295
  }
329
296
  }
330
- if (receipt.client !== 'claude-code') {
297
+ if (evidencePolicy.actualReasoningEffortRequired !== false) {
331
298
  if (!actualReasoningEffort || actualReasoningEffort.trim() !== actualReasoningEffort) {
332
299
  return fail('DELEGATION_REASONING_EFFORT_MISSING', 'native start event must provide a non-empty actual reasoning effort');
333
300
  }
334
301
  }
335
- if (receipt.client === 'codex'
336
- && event.hostEvidence?.kind !== 'codex-lifecycle-v2')
337
- return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex activation requires trusted lifecycle-v2 host evidence');
338
302
  if (actualReasoningEffort !== null && actualReasoningEffort !== receipt.requestedReasoningEffort) {
339
- if (receipt.client !== 'claude-code' && (!event.reasoningEffortFallbackReason || event.reasoningEffortFallbackReason.trim() === '')) {
303
+ if (evidencePolicy.fallbackReasonRequired !== false && (!event.reasoningEffortFallbackReason || event.reasoningEffortFallbackReason.trim() === '')) {
340
304
  return fail('DELEGATION_REASONING_EFFORT_FALLBACK_UNRECORDED', 'actual reasoning effort differs from requested effort without a fallback reason');
341
305
  }
342
306
  }
@@ -349,16 +313,9 @@ function activateDelegation(receipt, event, options = {}) {
349
313
  && event.reasoningEffortFallbackReason) {
350
314
  return fail('DELEGATION_REASONING_EFFORT_FALLBACK_INVALID', 'reasoning-effort fallback reason is only valid when actual effort differs');
351
315
  }
352
- if (event.hostEvidence
353
- && (receipt.client !== 'codex'
354
- || !event.hostEvidence.hookDefinitionHash.trim()
355
- || !Number.isSafeInteger(event.hostEvidence.startRevision)
356
- || event.hostEvidence.startRevision < 1))
357
- return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex start evidence reference is invalid');
358
- if (event.hostEvidence?.kind === 'codex-lifecycle-v2'
359
- && event.hostEvidence.capabilitySessionId !== event.parentId) {
360
- return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex start evidence does not match the observed parent');
361
- }
316
+ const adapterEvidence = withClientEvidence(receipt, event.clientEvidence);
317
+ if (!adapterEvidence)
318
+ return fail('DELEGATION_ADAPTER_EVIDENCE_INVALID', 'adapter evidence must be a JSON object');
362
319
  return { ok: true, receipt: Object.freeze({
363
320
  ...receipt,
364
321
  status: 'activated',
@@ -369,12 +326,7 @@ function activateDelegation(receipt, event, options = {}) {
369
326
  actualReasoningEffort,
370
327
  modelFallbackReason: event.modelFallbackReason ?? null,
371
328
  reasoningEffortFallbackReason: event.reasoningEffortFallbackReason ?? null,
372
- hostEvidence: event.hostEvidence ? Object.freeze({
373
- ...event.hostEvidence,
374
- stopRevision: null,
375
- consumer: null,
376
- consumedAt: null
377
- }) : receipt.hostEvidence ?? null,
329
+ adapterEvidence,
378
330
  startEvidenceMonotonicMs: monotonic,
379
331
  activatedMonotonicMs: (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)))(),
380
332
  activatedAt: new Date(wallNow).toISOString()
@@ -393,22 +345,11 @@ function abortActivatedDelegation(receipt, event) {
393
345
  if (receipt.status !== 'activated') {
394
346
  return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected activated`);
395
347
  }
396
- if (receipt.client !== 'codex'
397
- || !receipt.lifecycleProvenance
398
- || receipt.hostEvidence?.kind !== 'codex-lifecycle-v2'
399
- || typeof receipt.lifecycleProvenance.controllerInstanceDigest !== 'string'
400
- || typeof receipt.lifecycleProvenance.controlGeneration !== 'string'
401
- || typeof receipt.hostEvidence.controllerInstanceDigest !== 'string'
402
- || typeof receipt.hostEvidence.controlGeneration !== 'string') {
403
- return fail('DELEGATION_RECOVERY_EVIDENCE_INVALID', 'activated delegation lacks the current Codex controller binding');
404
- }
405
- if (event.childId !== receipt.childId
406
- || !Number.isSafeInteger(event.stopRevision)
407
- || event.stopRevision <= receipt.hostEvidence.startRevision
408
- || event.consumer !== receipt.id
409
- || !exactText(event.consumedAt)) {
410
- return fail('DELEGATION_RECOVERY_EVIDENCE_INVALID', 'recovery stop evidence does not match the activated delegation');
411
- }
348
+ if (event.childId !== receipt.childId)
349
+ return fail('DELEGATION_RECOVERY_EVIDENCE_INVALID', 'recovery child does not match the activated delegation');
350
+ const adapterEvidence = withClientEvidence(receipt, event.clientEvidence);
351
+ if (!adapterEvidence)
352
+ return fail('DELEGATION_ADAPTER_EVIDENCE_INVALID', 'adapter evidence must be a JSON object');
412
353
  return {
413
354
  ok: true,
414
355
  receipt: Object.freeze({
@@ -419,12 +360,7 @@ function abortActivatedDelegation(receipt, event) {
419
360
  changedPaths: Object.freeze([]),
420
361
  sealedAt: null,
421
362
  consumedAt: null,
422
- hostEvidence: Object.freeze({
423
- ...receipt.hostEvidence,
424
- stopRevision: event.stopRevision,
425
- consumer: event.consumer,
426
- consumedAt: event.consumedAt
427
- })
363
+ adapterEvidence
428
364
  })
429
365
  };
430
366
  }
@@ -440,32 +376,12 @@ function completeDelegationStage(receipt, event) {
440
376
  return { ok: true, receipt: Object.freeze({ ...receipt, status: 'stage-completed', agent: event.agent }) };
441
377
  }
442
378
  function sealDelegation(receipt, event, options = {}) {
443
- if (receipt.status !== 'stage-completed')
444
- return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected stage-completed`);
445
- if (receipt.client === 'codex' && options.requireHostEvidence !== false && !event.hostEvidence) {
446
- return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex sealing requires consumed lifecycle-v2 host evidence');
447
- }
448
- if (event.childId !== receipt.childId || event.exitCode !== 0)
449
- return fail('DELEGATION_STOP_INVALID', 'native stop identity or exit status is invalid');
450
- if (receipt.role === 'reviewer') {
451
- const taskRoot = `.agents/workspace/active/${receipt.taskId}/`;
452
- const allowed = new Set([
453
- `${taskRoot}${receipt.artifact}`,
454
- `${taskRoot}task.md`,
455
- `${taskRoot}orchestration.json`
456
- ]);
457
- const disallowed = event.changedPaths.find((entry) => !allowed.has(entry));
458
- if (disallowed)
459
- return fail('DELEGATION_REVIEWER_WRITE_FORBIDDEN', `reviewer changed forbidden path '${disallowed}'`);
460
- }
461
- if (event.hostEvidence) {
462
- if (!receipt.hostEvidence
463
- || !Number.isSafeInteger(event.hostEvidence.stopRevision)
464
- || event.hostEvidence.stopRevision <= receipt.hostEvidence.startRevision
465
- || event.hostEvidence.consumer !== receipt.id
466
- || !event.hostEvidence.consumedAt.trim())
467
- return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex stop evidence reference is invalid');
468
- }
379
+ const validationFailure = validateSealRequest(receipt, event);
380
+ if (validationFailure)
381
+ return validationFailure;
382
+ const adapterEvidence = withClientEvidence(receipt, event.clientEvidence);
383
+ if (!adapterEvidence)
384
+ return fail('DELEGATION_ADAPTER_EVIDENCE_INVALID', 'adapter evidence must be a JSON object');
469
385
  return { ok: true, receipt: Object.freeze({
470
386
  ...receipt,
471
387
  status: 'sealed',
@@ -475,10 +391,7 @@ function sealDelegation(receipt, event, options = {}) {
475
391
  actualReasoningEffort: receipt.actualReasoningEffort ?? foldBlankToNull(event.actualReasoningEffort),
476
392
  modelFallbackReason: receipt.modelFallbackReason ?? event.modelFallbackReason ?? null,
477
393
  reasoningEffortFallbackReason: receipt.reasoningEffortFallbackReason ?? event.reasoningEffortFallbackReason ?? null,
478
- hostEvidence: event.hostEvidence && receipt.hostEvidence ? Object.freeze({
479
- ...receipt.hostEvidence,
480
- ...event.hostEvidence
481
- }) : receipt.hostEvidence ?? null,
394
+ adapterEvidence,
482
395
  sealedAt: (options.now ?? (() => new Date().toISOString()))()
483
396
  }) };
484
397
  }
@@ -25,7 +25,6 @@ import { getArtifactSchema } from './artifact-schema.js';
25
25
  import { canonicalSemanticDigest, inspectArtifactContract } from './artifact-operations.js';
26
26
  import { inspectReviewIdentity } from './review-identity.js';
27
27
  import { parseCompletionFacts } from './completion-facts.js';
28
- import { consumeLifecycleRecoveryAttestation, lifecycleRecoveryAttestationDigest, validateLifecycleRecoveryAttestation } from './control-authority.js';
29
28
  import { readManualValidationCompletion } from './manual-validation-completion.js';
30
29
  import { parseLifecyclePathDecision } from './lifecycle-path.js';
31
30
  const eventCatalog = [
@@ -349,6 +348,65 @@ function openStartedIdentity(rows, family) {
349
348
  });
350
349
  return matches.length === 1 ? matches[0] : matches.length > 1 ? { conflict: true } : null;
351
350
  }
351
+ function repairMissingCodeFixQualifier(request, family, open, repoRoot) {
352
+ if (family !== 'code' || open.fixFor !== undefined || !request.fixFor || request.implementationInput !== undefined) {
353
+ return {
354
+ error: {
355
+ code: 'EVENT_ARTIFACT_CONFLICT',
356
+ message: `open code start cannot be repaired (family=${family}, openFixFor=${open.fixFor ?? 'none'}, requestedFixFor=${request.fixFor ?? 'none'}, implementationInput=${request.implementationInput ?? 'none'})`
357
+ },
358
+ context: null
359
+ };
360
+ }
361
+ const repair = normalizeStarted(request, repoRoot);
362
+ if ('error' in repair)
363
+ return { error: repair.error, context: repair.context };
364
+ if (repair.request.round !== open.round || repair.request.fixFor !== request.fixFor || repair.request.implementationInput !== undefined) {
365
+ return { error: { code: 'EVENT_ARTIFACT_CONFLICT', message: 'current code artifact context does not match the open round and requested fix input' }, context: repair.context };
366
+ }
367
+ return repair;
368
+ }
369
+ function resolveOpenStartedEvent(request, family, open, repoRoot) {
370
+ if (open.row.agent !== request.agent)
371
+ return { status: 'error', error: { code: 'EVENT_LOG_CONFLICT', message: 'open started event has a different agent' } };
372
+ if (family === 'manual-validation' && request.transactionId !== undefined) {
373
+ const existingTransactionId = /(?:^|;\s*)transaction=([A-Za-z0-9][A-Za-z0-9._-]{0,127})(?:;|$)/u.exec(open.row.note)?.[1];
374
+ if (existingTransactionId !== undefined && existingTransactionId !== request.transactionId) {
375
+ return { status: 'error', error: { code: 'EVENT_LOG_CONFLICT', message: 'open manual-validation started event has a different transactionId' } };
376
+ }
377
+ }
378
+ if (request.round !== undefined && request.round !== open.round) {
379
+ return { status: 'error', error: { code: 'EVENT_ARTIFACT_CONFLICT', message: `round ${request.round} conflicts with open round ${open.round}` } };
380
+ }
381
+ if (request.fixFor !== undefined && request.fixFor !== open.fixFor) {
382
+ const repair = repairMissingCodeFixQualifier(request, family, open, repoRoot);
383
+ if (!repair)
384
+ return { status: 'error', error: { code: 'EVENT_ARTIFACT_CONFLICT', message: `fixFor '${request.fixFor}' conflicts with open event` } };
385
+ if ('error' in repair)
386
+ return { status: 'error', error: repair.error, context: repair.context };
387
+ return { status: 'repair', repair: repair.request, context: repair.context, superseded: { action: open.row.step } };
388
+ }
389
+ if (request.implementationInput !== open.implementationInput) {
390
+ return { status: 'error', error: { code: 'EVENT_ARTIFACT_CONFLICT', message: `implementationInput '${request.implementationInput ?? ''}' conflicts with open event` } };
391
+ }
392
+ return {
393
+ status: 'replay',
394
+ replay: {
395
+ ...request,
396
+ round: open.round,
397
+ artifact: artifactName(FAMILY[family].artifact, open.round),
398
+ fixFor: open.fixFor,
399
+ implementationInput: open.implementationInput
400
+ },
401
+ startedAt: open.row.started
402
+ };
403
+ }
404
+ function hasOtherOpenStarted(rows, allowedAction) {
405
+ return rows.some((row) => row.started && !row.done && row.step !== allowedAction);
406
+ }
407
+ function hasConflictingOpenStarted(rows, recovery) {
408
+ return recovery ? hasOtherOpenStarted(rows, recovery.action) : rows.some((row) => row.started && !row.done);
409
+ }
352
410
  function reviewInputFamily(family) {
353
411
  return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
354
412
  }
@@ -538,38 +596,34 @@ function applyTaskEventUnlocked(request, options = {}) {
538
596
  const rows = startedBackedRows(pairEntries(section.entries));
539
597
  let normalized = request;
540
598
  let artifactContext = null;
599
+ let supersededOpenStart = null;
541
600
  if (initialParts.phase === 'started') {
542
601
  const openIdentity = openStartedIdentity(rows, initialParts.family);
543
602
  if (openIdentity && 'conflict' in openIdentity)
544
603
  return failed(request, { code: 'EVENT_LOG_CONFLICT', message: 'artifact family has more than one open started event' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
545
604
  if (openIdentity) {
546
- if (openIdentity.row.agent !== request.agent)
547
- return failed(request, { code: 'EVENT_LOG_CONFLICT', message: 'open started event has a different agent' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
548
- if (initialParts.family === 'manual-validation' && request.transactionId !== undefined) {
549
- const existingTransactionId = /(?:^|;\s*)transaction=([A-Za-z0-9][A-Za-z0-9._-]{0,127})(?:;|$)/u.exec(openIdentity.row.note)?.[1];
550
- if (existingTransactionId !== undefined && existingTransactionId !== request.transactionId)
551
- return failed(request, { code: 'EVENT_LOG_CONFLICT', message: 'open manual-validation started event has a different transactionId' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
552
- }
553
- if (request.round !== undefined && request.round !== openIdentity.round)
554
- return failed(request, { code: 'EVENT_ARTIFACT_CONFLICT', message: `round ${request.round} conflicts with open round ${openIdentity.round}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
555
- if (request.fixFor !== undefined && request.fixFor !== openIdentity.fixFor)
556
- return failed(request, { code: 'EVENT_ARTIFACT_CONFLICT', message: `fixFor '${request.fixFor}' conflicts with open event` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
557
- if (request.implementationInput !== openIdentity.implementationInput)
558
- return failed(request, { code: 'EVENT_ARTIFACT_CONFLICT', message: `implementationInput '${request.implementationInput ?? ''}' conflicts with open event` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
559
- normalized = { ...request, round: openIdentity.round, artifact: artifactName(FAMILY[initialParts.family].artifact, openIdentity.round), fixFor: openIdentity.fixFor, implementationInput: openIdentity.implementationInput };
560
- return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, typeof frontmatter.current_step === 'string' ? frontmatter.current_step : '', identity(normalized), openIdentity.row.started, frontmatter, null);
561
- }
562
- if (rows.some((item) => item.started && !item.done)) {
605
+ const resolvedStart = resolveOpenStartedEvent(request, initialParts.family, openIdentity, resolved.repoRoot);
606
+ if (resolvedStart.status === 'error')
607
+ return failed(request, resolvedStart.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
608
+ if (resolvedStart.status === 'replay')
609
+ return successNoOp(resolvedStart.replay, resolved.taskId, resolved.taskMdPath, typeof frontmatter.current_step === 'string' ? frontmatter.current_step : '', identity(resolvedStart.replay), resolvedStart.startedAt, frontmatter, null);
610
+ normalized = resolvedStart.repair;
611
+ artifactContext = resolvedStart.context;
612
+ supersededOpenStart = resolvedStart.superseded;
613
+ }
614
+ if (hasConflictingOpenStarted(rows, supersededOpenStart)) {
563
615
  return failed(request, {
564
616
  code: 'EVENT_TRANSITION_INVALID',
565
617
  message: 'EXECUTION_BUSY: another lifecycle execution is open'
566
618
  }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
567
619
  }
568
- const result = normalizeStarted(request, resolved.repoRoot);
569
- artifactContext = result.context;
570
- if ('error' in result)
571
- return failed(request, result.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, artifactContext });
572
- normalized = result.request;
620
+ if (!supersededOpenStart) {
621
+ const result = normalizeStarted(request, resolved.repoRoot);
622
+ artifactContext = result.context;
623
+ if ('error' in result)
624
+ return failed(request, result.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, artifactContext });
625
+ normalized = result.request;
626
+ }
573
627
  }
574
628
  else if (initialParts.phase === 'completed') {
575
629
  const identity = request.artifact ? parseArtifactName(request.artifact) : null;
@@ -577,25 +631,6 @@ function applyTaskEventUnlocked(request, options = {}) {
577
631
  normalized = { ...request, round: identity.round };
578
632
  }
579
633
  const eventIdentity = identity(normalized);
580
- const lifecycleAuthority = options.lifecycleRecoveryAttestation ?? null;
581
- if (lifecycleAuthority) {
582
- try {
583
- validateLifecycleRecoveryAttestation(lifecycleAuthority);
584
- }
585
- catch (error) {
586
- return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: error instanceof Error ? error.message : String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
587
- }
588
- const expectedFamily = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
589
- if (eventIdentity.phase !== 'completed'
590
- || lifecycleAuthority.phase !== 'task-event.completed'
591
- || lifecycleAuthority.taskId !== resolved.taskId
592
- || lifecycleAuthority.family !== expectedFamily
593
- || lifecycleAuthority.artifact !== normalized.artifact
594
- || lifecycleAuthority.round !== normalized.round
595
- || (normalized.requestId !== undefined && lifecycleAuthority.lifecycleRequestId !== normalized.requestId)) {
596
- return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: 'lifecycle completion authority does not match the event tuple' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
597
- }
598
- }
599
634
  const currentStep = typeof frontmatter.current_step === 'string' ? frontmatter.current_step : '';
600
635
  const matchingRows = rows.filter((item) => item.step === eventIdentity.action);
601
636
  const manual = eventIdentity.family === 'manual-validation';
@@ -689,7 +724,8 @@ function applyTaskEventUnlocked(request, options = {}) {
689
724
  const capability = canStart(lifecycleAction(eventIdentity.family), facts.facts, trigger);
690
725
  const safetyFailure = capability.reasonCode === 'TASK_NOT_ACTIVE'
691
726
  || capability.reasonCode === 'LIFECYCLE_EXECUTION_OPEN';
692
- if (!capability.allowed && (normalized.initiator === 'orchestrator' || safetyFailure)) {
727
+ if (!capability.allowed && (normalized.initiator === 'orchestrator' || safetyFailure)
728
+ && !(supersededOpenStart && capability.reasonCode === 'LIFECYCLE_EXECUTION_OPEN')) {
693
729
  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 });
694
730
  }
695
731
  }
@@ -759,7 +795,15 @@ function applyTaskEventUnlocked(request, options = {}) {
759
795
  }
760
796
  const step = eventIdentity.phase === 'started' || eventIdentity.target === null ? currentStep : eventIdentity.target;
761
797
  const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
762
- const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
798
+ const supersededBody = supersededOpenStart
799
+ ? appendActivityEntry(section, {
800
+ time: metadata.timestamp,
801
+ step: `${supersededOpenStart.action} [aborted]`,
802
+ agent: normalized.agent,
803
+ note: `superseded by ${eventIdentity.action}`
804
+ })
805
+ : section.body;
806
+ const body = appendActivityEntry(supersededOpenStart ? { ...section, body: supersededBody } : section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
763
807
  const frontmatterSet = { current_step: step, assigned_to: normalized.agent };
764
808
  if (currentFact)
765
809
  frontmatterSet.completion_facts = JSON.stringify(replaceCompletionFact(parseCompletionFacts(frontmatter.completion_facts), currentFact));
@@ -9,7 +9,7 @@ import { inspectPlatformIssue, syncPlatformIssue } from '../platform/issues.js';
9
9
  import { resourceIdentityEquals } from '../platform/resource-identity.js';
10
10
  import { parseTaskFrontmatter } from './frontmatter.js';
11
11
  import { recoverPlatformOperations } from './platform-operation-recovery.js';
12
- import { applyTaskLifecycle, inspectTaskLifecycleProgress, previewTaskCompletion } from './lifecycle.js';
12
+ import { applyTaskLifecycle, applyTaskLifecycleWithinTaskLock, inspectTaskLifecycleProgress, previewTaskCompletion } from './lifecycle.js';
13
13
  import { enumerateAllTaskDirs, resolveTaskRef } from './resolve-ref.js';
14
14
  import { inspectShortIdRegistry } from './short-id.js';
15
15
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
@@ -684,7 +684,7 @@ function preparedResult(taskId, receipt, steps, changed) {
684
684
  }
685
685
  async function prepareUnderLock(request, taskId, options) {
686
686
  const repoRoot = path.resolve(options.repoRoot);
687
- const lifecycle = options.lifecycle ?? applyTaskLifecycle;
687
+ const lifecycle = options.lifecycle ?? ((lifecycleRequest, lifecycleOptions) => (applyTaskLifecycleWithinTaskLock(lifecycleRequest, lifecycleOptions ?? { repoRoot }, taskId)));
688
688
  const backfill = options.backfill ?? backfillCompletionComments;
689
689
  const commentSync = options.commentSync ?? syncPlatformComment;
690
690
  const issueSync = options.issueSync ?? syncPlatformIssue;
@@ -746,7 +746,9 @@ async function prepareUnderLock(request, taskId, options) {
746
746
  const issueOperations = synced.operations;
747
747
  const badOperation = issueOperations?.find((operation) => ['requirements', 'issue-type', 'fields'].includes(operation.name)
748
748
  && operation.status !== 'applied' && operation.status !== 'no-op'
749
- && !(operation.name === 'requirements' && operation.status === 'skipped' && operation.reasonCode === 'NO_REQUIREMENTS_ANCHOR'));
749
+ && !(operation.name === 'requirements' && operation.status === 'skipped' && operation.reasonCode === 'NO_REQUIREMENTS_ANCHOR')
750
+ && !(!synced.capabilities.push && ['issue-type', 'fields'].includes(operation.name)
751
+ && operation.status === 'skipped' && operation.reasonCode === 'PUSH_REQUIRED'));
750
752
  if ((synced.status !== 'applied' && synced.status !== 'no-op') || synced.error || badOperation) {
751
753
  const error = synced.error ?? {
752
754
  code: badOperation?.reasonCode || 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
@@ -755,6 +757,15 @@ async function prepareUnderLock(request, taskId, options) {
755
757
  };
756
758
  return failed(taskId, errorOf(error, 'FINALIZATION_ISSUE_METADATA_FAILED', true));
757
759
  }
760
+ if (!synced.capabilities.push) {
761
+ const missingCapabilitySkip = ['issue-type', 'fields'].find((name) => !issueOperations?.some((operation) => operation.name === name && operation.status === 'skipped' && operation.reasonCode === 'PUSH_REQUIRED'));
762
+ if (missingCapabilitySkip)
763
+ return failed(taskId, {
764
+ code: 'FINALIZATION_ISSUE_METADATA_UNCONFIRMED',
765
+ message: `Issue metadata operation '${missingCapabilitySkip}' did not report the required PUSH_REQUIRED capability skip`,
766
+ retryable: true
767
+ });
768
+ }
758
769
  let inspected;
759
770
  try {
760
771
  inspected = await issueInspect(taskId, { cwd: repoRoot });
@@ -770,7 +781,8 @@ async function prepareUnderLock(request, taskId, options) {
770
781
  retryable: true
771
782
  });
772
783
  }
773
- for (const name of ['requirements', 'issue-type', 'fields']) {
784
+ const rereadNames = synced.capabilities.push ? ['requirements', 'issue-type', 'fields'] : ['requirements'];
785
+ for (const name of rereadNames) {
774
786
  const operation = issueOperations?.find((candidate) => candidate.name === name);
775
787
  if (!operation)
776
788
  return failed(taskId, {
@@ -1024,8 +1036,11 @@ async function commitPreparedTaskFinalization(request, options) {
1024
1036
  retryable: false
1025
1037
  });
1026
1038
  receipt = updateReceipt(repoRoot, receipt, { verification: 'pending' });
1027
- const lifecycle = options.lifecycle ?? applyTaskLifecycle;
1028
- const result = lifecycle({ taskRef: resolved.taskId, intent: 'complete', agent: request.agent }, { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) });
1039
+ const lifecycleRequest = { taskRef: resolved.taskId, intent: 'complete', agent: request.agent };
1040
+ const lifecycleOptions = { repoRoot, ...(options.metadataProvider ? { metadataProvider: options.metadataProvider } : {}) };
1041
+ const result = options.lifecycle
1042
+ ? options.lifecycle(lifecycleRequest, lifecycleOptions)
1043
+ : applyTaskLifecycleWithinTaskLock(lifecycleRequest, lifecycleOptions, resolved.taskId);
1029
1044
  const step = lifecycleStep(result);
1030
1045
  if (result.status !== 'applied' && result.status !== 'no-op') {
1031
1046
  const updated = updateReceipt(repoRoot, receipt, { lifecycle: 'pending', lastError: step.error });
@@ -1118,6 +1133,20 @@ async function applyTaskFinalization(request, options) {
1118
1133
  const prepared = await prepareTaskFinalization(request, options);
1119
1134
  if (prepared.status !== 'prepared')
1120
1135
  return prepared;
1136
+ if (options.beforeLifecycle) {
1137
+ try {
1138
+ await options.beforeLifecycle(prepared.taskId);
1139
+ }
1140
+ catch (error) {
1141
+ const detail = errorOf(error, 'TASK_FINALIZATION_PRE_LIFECYCLE_FAILED', true);
1142
+ return failed(prepared.taskId, detail, {
1143
+ changed: prepared.changed,
1144
+ completedSteps: prepared.completedSteps,
1145
+ pendingSteps: prepared.pendingSteps,
1146
+ warnings: prepared.warnings
1147
+ });
1148
+ }
1149
+ }
1121
1150
  return commitPreparedTaskFinalization(request, options);
1122
1151
  }
1123
1152
  export { applyFinalizationReceiptMutation, commitPreparedTaskFinalization, applyTaskFinalization, prepareTaskFinalization, issueCapability as createFinalizationCapability, readTaskFinalizationReceipt, terminalResult };