@fitlab-ai/agent-infra 0.9.7 → 0.9.9

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 (195) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +11 -1
  4. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  12. package/dist/lib/decide.js +68 -14
  13. package/dist/lib/git/workflow.js +10 -1
  14. package/dist/lib/init.js +1 -1
  15. package/dist/lib/internal/codex-lifecycle.js +19 -5
  16. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  17. package/dist/lib/internal/git-workflow.js +5 -7
  18. package/dist/lib/internal/platform-pr.js +20 -7
  19. package/dist/lib/internal/release-workflow.js +3 -4
  20. package/dist/lib/internal/sandbox-control.js +79 -2
  21. package/dist/lib/internal/task-activity.js +57 -7
  22. package/dist/lib/internal/task-event.js +57 -4
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-ledger.js +59 -5
  25. package/dist/lib/internal/task-lifecycle.js +59 -8
  26. package/dist/lib/internal/task-orchestration.js +161 -288
  27. package/dist/lib/internal/task-override.js +131 -0
  28. package/dist/lib/internal/task-review.js +64 -6
  29. package/dist/lib/internal/task-verify.js +65 -3
  30. package/dist/lib/internal/task-warning.js +57 -4
  31. package/dist/lib/platform/pr-summary.js +53 -13
  32. package/dist/lib/platform/pull-requests.js +205 -41
  33. package/dist/lib/runtime/agent-runtime.js +56 -0
  34. package/dist/lib/sandbox/commands/create.js +389 -311
  35. package/dist/lib/sandbox/commands/rm.js +316 -55
  36. package/dist/lib/sandbox/control/client.js +190 -6
  37. package/dist/lib/sandbox/control/container-identity.js +1 -1
  38. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  39. package/dist/lib/sandbox/control/executor.js +145 -5
  40. package/dist/lib/sandbox/control/lifecycle.js +395 -73
  41. package/dist/lib/sandbox/control/protocol.js +67 -6
  42. package/dist/lib/sandbox/control/state.js +27 -11
  43. package/dist/lib/sandbox/engine.js +14 -7
  44. package/dist/lib/sandbox/index.js +5 -3
  45. package/dist/lib/sandbox/managed-fs.js +5 -0
  46. package/dist/lib/sandbox/mounts.js +20 -5
  47. package/dist/lib/sandbox/recovery.js +78 -10
  48. package/dist/lib/sandbox/task-resolver.js +50 -6
  49. package/dist/lib/sandbox/workspace-identity.js +27 -0
  50. package/dist/lib/sandbox/workspace-view.js +62 -40
  51. package/dist/lib/sandbox/worktree-safety.js +215 -2
  52. package/dist/lib/server/process-state.js +51 -1
  53. package/dist/lib/task/activity-intent.js +4 -1
  54. package/dist/lib/task/codex-orchestration.js +158 -33
  55. package/dist/lib/task/commands/status.js +59 -1
  56. package/dist/lib/task/commit-operation.js +335 -0
  57. package/dist/lib/task/commit-policy.js +18 -0
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +279 -17
  60. package/dist/lib/task/events.js +9 -5
  61. package/dist/lib/task/finalization.js +567 -0
  62. package/dist/lib/task/guard-override.js +31 -0
  63. package/dist/lib/task/human-override.js +1240 -0
  64. package/dist/lib/task/ledger-intents.js +6 -3
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/operation-outcome.js +33 -0
  67. package/dist/lib/task/orchestration.js +199 -719
  68. package/dist/lib/task/review-finalization.js +3 -2
  69. package/dist/lib/task/short-id.js +59 -1
  70. package/dist/lib/task/task-execution-lock.js +6 -1
  71. package/dist/lib/task/verification-engine.js +138 -59
  72. package/dist/lib/task/verification.js +15 -2
  73. package/dist/lib/task/workflow-warning-intents.js +54 -3
  74. package/dist/lib/task/write.js +2 -1
  75. package/dist/lifecycle-build-manifest.json +3 -3
  76. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  77. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
  78. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
  80. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  82. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  83. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
  84. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  85. package/lib/decide.ts +59 -14
  86. package/lib/git/workflow.ts +11 -2
  87. package/lib/init.ts +1 -1
  88. package/lib/internal/codex-lifecycle.ts +18 -5
  89. package/lib/internal/codex-sandbox-controller.ts +6 -12
  90. package/lib/internal/git-workflow.ts +5 -6
  91. package/lib/internal/platform-pr.ts +14 -7
  92. package/lib/internal/release-workflow.ts +3 -4
  93. package/lib/internal/sandbox-control.ts +92 -2
  94. package/lib/internal/task-activity.ts +46 -6
  95. package/lib/internal/task-event.ts +53 -4
  96. package/lib/internal/task-finalization.ts +68 -0
  97. package/lib/internal/task-ledger.ts +48 -4
  98. package/lib/internal/task-lifecycle.ts +56 -12
  99. package/lib/internal/task-orchestration.ts +32 -107
  100. package/lib/internal/task-override.ts +138 -0
  101. package/lib/internal/task-review.ts +51 -6
  102. package/lib/internal/task-verify.ts +52 -3
  103. package/lib/internal/task-warning.ts +44 -3
  104. package/lib/platform/pr-summary.ts +65 -14
  105. package/lib/platform/pull-requests.ts +211 -41
  106. package/lib/runtime/agent-runtime.ts +76 -0
  107. package/lib/sandbox/commands/create.ts +88 -4
  108. package/lib/sandbox/commands/rm.ts +413 -61
  109. package/lib/sandbox/config.ts +2 -1
  110. package/lib/sandbox/control/client.ts +255 -6
  111. package/lib/sandbox/control/container-identity.ts +3 -3
  112. package/lib/sandbox/control/controller-registration.ts +347 -0
  113. package/lib/sandbox/control/executor.ts +172 -6
  114. package/lib/sandbox/control/lifecycle.ts +465 -77
  115. package/lib/sandbox/control/protocol.ts +85 -9
  116. package/lib/sandbox/control/state.ts +35 -18
  117. package/lib/sandbox/engine.ts +20 -9
  118. package/lib/sandbox/index.ts +5 -3
  119. package/lib/sandbox/managed-fs.ts +5 -0
  120. package/lib/sandbox/mounts.ts +23 -5
  121. package/lib/sandbox/recovery.ts +90 -9
  122. package/lib/sandbox/task-resolver.ts +58 -7
  123. package/lib/sandbox/workspace-identity.ts +50 -2
  124. package/lib/sandbox/workspace-view.ts +73 -43
  125. package/lib/sandbox/worktree-safety.ts +236 -2
  126. package/lib/server/process-state.ts +48 -1
  127. package/lib/task/activity-intent.ts +4 -2
  128. package/lib/task/codex-orchestration.ts +167 -33
  129. package/lib/task/commands/status.ts +78 -1
  130. package/lib/task/commit-operation.ts +430 -0
  131. package/lib/task/commit-policy.ts +30 -0
  132. package/lib/task/decision-intents.ts +4 -2
  133. package/lib/task/delegation-receipts.ts +304 -19
  134. package/lib/task/events.ts +10 -5
  135. package/lib/task/finalization.ts +719 -0
  136. package/lib/task/guard-override.ts +50 -0
  137. package/lib/task/human-override.ts +1503 -0
  138. package/lib/task/ledger-intents.ts +6 -3
  139. package/lib/task/lifecycle.ts +71 -14
  140. package/lib/task/operation-outcome.ts +48 -0
  141. package/lib/task/orchestration.ts +239 -922
  142. package/lib/task/review-finalization.ts +6 -2
  143. package/lib/task/short-id.ts +65 -1
  144. package/lib/task/task-execution-lock.ts +10 -1
  145. package/lib/task/verification-engine.ts +145 -64
  146. package/lib/task/verification.ts +18 -3
  147. package/lib/task/workflow-warning-intents.ts +61 -4
  148. package/lib/task/write.ts +4 -1
  149. package/package.json +2 -2
  150. package/templates/.agents/QUICKSTART.en.md +2 -2
  151. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  152. package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
  153. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
  154. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
  155. package/templates/.agents/rules/task-management.en.md +2 -2
  156. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  157. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  158. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  159. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  160. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  161. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  162. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  163. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  164. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  165. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  166. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  167. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  168. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  170. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  171. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  172. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  173. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  174. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  175. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  176. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  177. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  178. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  179. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  180. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  181. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  182. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  183. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  184. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  185. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  186. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  187. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  188. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
  189. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  190. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  191. package/templates/.codex/hooks.json +4 -4
  192. package/dist/lib/task/commit-finalization.js +0 -305
  193. package/dist/lib/task/commit-intent.js +0 -165
  194. package/lib/task/commit-finalization.ts +0 -419
  195. package/lib/task/commit-intent.ts +0 -230
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
 
3
+ import { isAgentClientId } from '../agent-clients/types.ts';
3
4
  import type { AgentClientId } from '../agent-clients/types.ts';
4
5
  import { normalizeAgentToken } from '../agent-clients/tokens.ts';
5
6
 
@@ -87,6 +88,261 @@ type ReceiptFailure = Readonly<{ ok: false; code: string; message: string; recei
87
88
  type ReceiptSuccess = Readonly<{ ok: true; receipt: DelegationReceipt; code?: never; message?: never }>;
88
89
  type ReceiptResult = ReceiptSuccess | ReceiptFailure;
89
90
 
91
+ function isRecord(value: unknown): value is Record<string, unknown> {
92
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
93
+ }
94
+
95
+ function hasExactKeys(
96
+ value: unknown,
97
+ required: readonly string[],
98
+ optional: readonly string[] = []
99
+ ): value is Record<string, unknown> {
100
+ if (!isRecord(value)) return false;
101
+ const keys = Object.keys(value);
102
+ const allowed = new Set([...required, ...optional]);
103
+ return required.every((key) => Object.prototype.hasOwnProperty.call(value, key))
104
+ && keys.every((key) => allowed.has(key));
105
+ }
106
+
107
+ function exactText(value: unknown): value is string {
108
+ return typeof value === 'string' && value.length > 0 && value.trim() === value;
109
+ }
110
+
111
+ function nullableText(value: unknown): value is string | null {
112
+ return value === null || exactText(value);
113
+ }
114
+
115
+ function nullableSafeInteger(value: unknown): value is number | null {
116
+ return value === null || (Number.isSafeInteger(value) && (value as number) >= 0);
117
+ }
118
+
119
+ const LIFECYCLE_PROVENANCE_KEYS = [
120
+ 'protocolVersion', 'packageVersion', 'internalExecutableBuildHash', 'lifecycleContractHash',
121
+ 'hookDefinitionHash', 'hookSource', 'hookSourcePathDigest', 'hookSourceHash',
122
+ 'capabilitySessionId', 'capabilityTurnId', 'capabilityToolUseId',
123
+ 'controllerInstanceDigest', 'controlGeneration'
124
+ ] as const;
125
+
126
+ function isDelegationLifecycleProvenance(value: unknown): value is DelegationLifecycleProvenance {
127
+ if (!hasExactKeys(value, LIFECYCLE_PROVENANCE_KEYS)) return false;
128
+ return Number.isSafeInteger(value.protocolVersion)
129
+ && (value.protocolVersion as number) > 0
130
+ && exactText(value.packageVersion)
131
+ && exactText(value.internalExecutableBuildHash)
132
+ && exactText(value.lifecycleContractHash)
133
+ && exactText(value.hookDefinitionHash)
134
+ && ['project', 'managed', 'isolated-user'].includes(value.hookSource as string)
135
+ && exactText(value.hookSourcePathDigest)
136
+ && exactText(value.hookSourceHash)
137
+ && exactText(value.capabilitySessionId)
138
+ && exactText(value.capabilityTurnId)
139
+ && exactText(value.capabilityToolUseId)
140
+ && nullableText(value.controllerInstanceDigest)
141
+ && nullableText(value.controlGeneration);
142
+ }
143
+
144
+ const HOST_EVIDENCE_REQUIRED_KEYS = [
145
+ 'kind', 'hookDefinitionHash', 'startRevision', 'stopRevision', 'consumer', 'consumedAt'
146
+ ] as const;
147
+ const HOST_EVIDENCE_OPTIONAL_KEYS = [
148
+ 'protocolVersion', 'packageVersion', 'internalExecutableBuildHash', 'lifecycleContractHash',
149
+ 'hookSource', 'hookSourcePathDigest', 'hookSourceHash', 'capabilitySessionId',
150
+ 'capabilityTurnId', 'capabilityToolUseId', 'spawnToolUseId', 'spawnObservedAt',
151
+ 'controllerInstanceDigest', 'controlGeneration'
152
+ ] as const;
153
+
154
+ function isDelegationHostEvidence(value: unknown): value is DelegationHostEvidence {
155
+ if (!hasExactKeys(value, HOST_EVIDENCE_REQUIRED_KEYS, HOST_EVIDENCE_OPTIONAL_KEYS)) return false;
156
+ if (!['codex-lifecycle-v1', 'codex-lifecycle-v2'].includes(value.kind as string)) return false;
157
+ if (!exactText(value.hookDefinitionHash)
158
+ || !Number.isSafeInteger(value.startRevision) || (value.startRevision as number) < 1
159
+ || !nullableSafeInteger(value.stopRevision)
160
+ || !nullableText(value.consumer)
161
+ || !nullableText(value.consumedAt)) return false;
162
+ if (value.kind === 'codex-lifecycle-v1') return true;
163
+ return Number.isSafeInteger(value.protocolVersion)
164
+ && (value.protocolVersion as number) > 0
165
+ && exactText(value.packageVersion)
166
+ && exactText(value.internalExecutableBuildHash)
167
+ && exactText(value.lifecycleContractHash)
168
+ && ['project', 'managed', 'isolated-user'].includes(value.hookSource as string)
169
+ && exactText(value.hookSourcePathDigest)
170
+ && exactText(value.hookSourceHash)
171
+ && exactText(value.capabilitySessionId)
172
+ && exactText(value.capabilityTurnId)
173
+ && exactText(value.spawnToolUseId)
174
+ && exactText(value.spawnObservedAt)
175
+ && nullableText(value.controllerInstanceDigest)
176
+ && nullableText(value.controlGeneration);
177
+ }
178
+
179
+ function hasCurrentCodexEvidence(receipt: DelegationReceipt): boolean {
180
+ const provenance = receipt.lifecycleProvenance;
181
+ if (!provenance) return false;
182
+ const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
183
+ if (!activated) return receipt.hostEvidence === null;
184
+ const host = receipt.hostEvidence;
185
+ if (
186
+ host?.kind !== 'codex-lifecycle-v2'
187
+ || host.protocolVersion !== provenance.protocolVersion
188
+ || host.packageVersion !== provenance.packageVersion
189
+ || host.internalExecutableBuildHash !== provenance.internalExecutableBuildHash
190
+ || host.lifecycleContractHash !== provenance.lifecycleContractHash
191
+ || host.hookDefinitionHash !== provenance.hookDefinitionHash
192
+ || host.hookSource !== provenance.hookSource
193
+ || host.hookSourcePathDigest !== provenance.hookSourcePathDigest
194
+ || host.hookSourceHash !== provenance.hookSourceHash
195
+ || host.capabilitySessionId !== provenance.capabilitySessionId
196
+ || host.capabilityTurnId !== provenance.capabilityTurnId
197
+ || host.controllerInstanceDigest !== provenance.controllerInstanceDigest
198
+ || host.controlGeneration !== provenance.controlGeneration
199
+ || receipt.parentId !== provenance.capabilitySessionId
200
+ || host.spawnToolUseId === provenance.capabilityToolUseId
201
+ ) return false;
202
+ const spawnObservedAt = Date.parse(host.spawnObservedAt ?? '');
203
+ const spawnDispatchedAt = Date.parse(receipt.spawnDispatchedAt ?? '');
204
+ const activationDeadlineAt = Date.parse(receipt.activationDeadlineAt ?? '');
205
+ if (
206
+ !Number.isFinite(spawnObservedAt)
207
+ || !Number.isFinite(spawnDispatchedAt)
208
+ || !Number.isFinite(activationDeadlineAt)
209
+ || spawnObservedAt < spawnDispatchedAt
210
+ || spawnObservedAt > activationDeadlineAt
211
+ ) return false;
212
+ if (['sealed', 'consumed'].includes(receipt.status)) {
213
+ return Number.isSafeInteger(host.stopRevision)
214
+ && (host.stopRevision as number) > host.startRevision
215
+ && host.consumer === receipt.id
216
+ && exactText(host.consumedAt);
217
+ }
218
+ return host.stopRevision === null && host.consumer === null && host.consumedAt === null;
219
+ }
220
+
221
+ const RECEIPT_KEYS = [
222
+ 'id', 'taskId', 'runId', 'role', 'stage', 'round', 'artifact', 'client',
223
+ 'requestedModel', 'requestedReasoningEffort', 'actualModel', 'actualReasoningEffort',
224
+ 'modelFallbackReason', 'reasoningEffortFallbackReason', 'parentId', 'childId',
225
+ 'spawnMode', 'agent', 'status', 'workspaceSnapshotScope', 'lifecycleProvenance',
226
+ 'hostEvidence', 'beforeFingerprint', 'afterFingerprint', 'changedPaths', 'createdAt',
227
+ 'preparedMonotonicMs', 'spawnDispatchMonotonicMs', 'activationDeadlineMonotonicMs',
228
+ 'spawnDispatchedAt', 'activationDeadlineAt', 'startEvidenceMonotonicMs',
229
+ 'activatedMonotonicMs', 'activatedAt', 'sealedAt', 'consumedAt'
230
+ ] as const;
231
+
232
+ function hasStatusBoundEvidence(receipt: DelegationReceipt): boolean {
233
+ const dispatchFields = [
234
+ receipt.spawnDispatchMonotonicMs,
235
+ receipt.activationDeadlineMonotonicMs,
236
+ receipt.spawnDispatchedAt,
237
+ receipt.activationDeadlineAt
238
+ ];
239
+ const dispatchEmpty = dispatchFields.every((field) => field === null);
240
+ const dispatchComplete = dispatchFields.every((field) => field !== null);
241
+ if (!dispatchEmpty && !dispatchComplete) return false;
242
+
243
+ const beforeActivation = ['prepared', 'aborted', 'expired'].includes(receipt.status);
244
+ if (beforeActivation) {
245
+ return receipt.parentId === null
246
+ && receipt.childId === null
247
+ && receipt.spawnMode === null
248
+ && receipt.actualModel === null
249
+ && receipt.actualReasoningEffort === null
250
+ && receipt.modelFallbackReason === null
251
+ && receipt.reasoningEffortFallbackReason === null
252
+ && receipt.agent === null
253
+ && receipt.hostEvidence === null
254
+ && receipt.startEvidenceMonotonicMs === null
255
+ && receipt.activatedMonotonicMs === null
256
+ && receipt.activatedAt === null
257
+ && receipt.afterFingerprint === null
258
+ && receipt.changedPaths.length === 0
259
+ && receipt.sealedAt === null
260
+ && receipt.consumedAt === null;
261
+ }
262
+
263
+ if (
264
+ !dispatchComplete
265
+ || !exactText(receipt.parentId)
266
+ || !exactText(receipt.childId)
267
+ || receipt.parentId === receipt.childId
268
+ || receipt.startEvidenceMonotonicMs === null
269
+ || receipt.activatedMonotonicMs === null
270
+ || !exactText(receipt.activatedAt)
271
+ ) return false;
272
+
273
+ if (receipt.status === 'activated') {
274
+ return receipt.agent === null
275
+ && receipt.afterFingerprint === null
276
+ && receipt.changedPaths.length === 0
277
+ && receipt.sealedAt === null
278
+ && receipt.consumedAt === null;
279
+ }
280
+
281
+ if (
282
+ !exactText(receipt.agent)
283
+ || normalizeAgentToken(receipt.agent) !== normalizeAgentToken(receipt.client)
284
+ ) return false;
285
+
286
+ if (receipt.status === 'stage-completed') {
287
+ return receipt.afterFingerprint === null
288
+ && receipt.changedPaths.length === 0
289
+ && receipt.sealedAt === null
290
+ && receipt.consumedAt === null;
291
+ }
292
+
293
+ if (!exactText(receipt.afterFingerprint) || !exactText(receipt.sealedAt)) return false;
294
+ return receipt.status === 'sealed'
295
+ ? receipt.consumedAt === null
296
+ : receipt.status === 'consumed' && exactText(receipt.consumedAt);
297
+ }
298
+
299
+ function isDelegationReceipt(value: unknown): value is DelegationReceipt {
300
+ if (!hasExactKeys(value, RECEIPT_KEYS)) return false;
301
+ const structurallyValid = exactText(value.id)
302
+ && exactText(value.taskId)
303
+ && exactText(value.runId)
304
+ && ['executor', 'reviewer'].includes(value.role as string)
305
+ && ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code', 'commit'].includes(value.stage as string)
306
+ && Number.isSafeInteger(value.round)
307
+ && (value.round as number) > 0
308
+ && exactText(value.artifact)
309
+ && isAgentClientId(value.client)
310
+ && exactText(value.requestedModel)
311
+ && exactText(value.requestedReasoningEffort)
312
+ && nullableText(value.actualModel)
313
+ && nullableText(value.actualReasoningEffort)
314
+ && nullableText(value.modelFallbackReason)
315
+ && nullableText(value.reasoningEffortFallbackReason)
316
+ && nullableText(value.parentId)
317
+ && nullableText(value.childId)
318
+ && nullableText(value.spawnMode)
319
+ && nullableText(value.agent)
320
+ && ['prepared', 'activated', 'stage-completed', 'sealed', 'consumed', 'aborted', 'expired'].includes(value.status as string)
321
+ && value.workspaceSnapshotScope === 'task'
322
+ && (value.lifecycleProvenance === null || isDelegationLifecycleProvenance(value.lifecycleProvenance))
323
+ && (value.hostEvidence === null || isDelegationHostEvidence(value.hostEvidence))
324
+ && exactText(value.beforeFingerprint)
325
+ && nullableText(value.afterFingerprint)
326
+ && Array.isArray(value.changedPaths)
327
+ && value.changedPaths.every(exactText)
328
+ && exactText(value.createdAt)
329
+ && nullableSafeInteger(value.preparedMonotonicMs)
330
+ && value.preparedMonotonicMs !== null
331
+ && nullableSafeInteger(value.spawnDispatchMonotonicMs)
332
+ && nullableSafeInteger(value.activationDeadlineMonotonicMs)
333
+ && nullableText(value.spawnDispatchedAt)
334
+ && nullableText(value.activationDeadlineAt)
335
+ && nullableSafeInteger(value.startEvidenceMonotonicMs)
336
+ && nullableSafeInteger(value.activatedMonotonicMs)
337
+ && nullableText(value.activatedAt)
338
+ && nullableText(value.sealedAt)
339
+ && nullableText(value.consumedAt);
340
+ if (!structurallyValid) return false;
341
+ const receipt = value as unknown as DelegationReceipt;
342
+ return hasStatusBoundEvidence(receipt)
343
+ && (receipt.client !== 'codex' || hasCurrentCodexEvidence(receipt));
344
+ }
345
+
90
346
  const MANAGED_AGENTS = {
91
347
  'agent-infra-lifecycle-executor': 'executor',
92
348
  'agent-infra-lifecycle-reviewer': 'reviewer'
@@ -100,6 +356,11 @@ function fail(code: string, message: string): ReceiptFailure {
100
356
  return { ok: false, code, message };
101
357
  }
102
358
 
359
+ // 空白串折叠为 null,保持"缺失即 null"的单一表示(Start/Stop 两处共用,避免任一入口把 " " 当成已观察到的值入账)
360
+ function foldBlankToNull(value: string | undefined): string | null {
361
+ return value?.trim() ? value : null;
362
+ }
363
+
103
364
  function prepareDelegation(
104
365
  input: Omit<DelegationReceipt, 'id' | 'requestedModel' | 'requestedReasoningEffort' | 'actualModel' | 'actualReasoningEffort' | 'modelFallbackReason' | 'reasoningEffortFallbackReason' | 'parentId' | 'childId' | 'spawnMode' | 'agent' | 'status' | 'hostEvidence' | 'afterFingerprint' | 'changedPaths' | 'createdAt' | 'preparedMonotonicMs' | 'spawnDispatchMonotonicMs' | 'activationDeadlineMonotonicMs' | 'spawnDispatchedAt' | 'activationDeadlineAt' | 'startEvidenceMonotonicMs' | 'activatedMonotonicMs' | 'activatedAt' | 'sealedAt' | 'consumedAt'> & Readonly<{ requestedModel: string; requestedReasoningEffort: string }>,
105
366
  options: { id?: () => string; now?: () => string; monotonicNow?: () => number } = {}
@@ -146,7 +407,7 @@ function dispatchDelegation(
146
407
  }
147
408
  const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))();
148
409
  const dispatchedAt = (options.now ?? (() => new Date().toISOString()))();
149
- const window = options.activationWindowMs ?? 15_000;
410
+ const window = options.activationWindowMs ?? 60_000;
150
411
  return { ok: true, receipt: Object.freeze({
151
412
  ...receipt,
152
413
  spawnDispatchMonotonicMs: monotonic,
@@ -162,7 +423,7 @@ function activateDelegation(
162
423
  nativeAgent: string;
163
424
  childId: string;
164
425
  parentId: string;
165
- spawnMode: string;
426
+ spawnMode?: string;
166
427
  actualModel?: string;
167
428
  actualReasoningEffort?: string;
168
429
  modelFallbackReason?: string;
@@ -211,32 +472,46 @@ function activateDelegation(
211
472
  if (!event.parentId || (receipt.parentId !== null && event.parentId !== receipt.parentId) || !event.childId || event.childId === event.parentId) {
212
473
  return fail('DELEGATION_IDENTITY_INVALID', 'native parent/child identity does not match the prepared delegation');
213
474
  }
214
- if (event.spawnMode !== 'fresh') return fail('DELEGATION_FORK_FORBIDDEN', `spawn mode '${event.spawnMode}' is not fresh`);
215
- const actualModel = event.actualModel;
216
- if (!actualModel || actualModel.trim() !== actualModel) {
217
- return fail('DELEGATION_MODEL_IDENTITY_MISSING', 'native start event must provide a non-empty actual model identity');
475
+ // spawnMode 的必需性按 client 判断:claude-code 宿主结构上不提供该字段,跳过;其余 client 维持硬校验
476
+ if (receipt.client !== 'claude-code' && event.spawnMode !== 'fresh') {
477
+ return fail('DELEGATION_FORK_FORBIDDEN', `spawn mode '${event.spawnMode}' is not fresh`);
218
478
  }
219
- if (actualModel !== receipt.requestedModel && (!event.modelFallbackReason || event.modelFallbackReason.trim() === '')) {
220
- return fail('DELEGATION_MODEL_FALLBACK_UNRECORDED', 'actual model differs from requested model without a fallback reason');
479
+ // model/effort 维度:claude-code 如实记录、不做门禁;其余 client 维持现状硬校验(HDR-2)
480
+ const actualModel = foldBlankToNull(event.actualModel);
481
+ const actualReasoningEffort = foldBlankToNull(event.actualReasoningEffort);
482
+ if (receipt.client !== 'claude-code') {
483
+ if (!actualModel || actualModel.trim() !== actualModel) {
484
+ return fail('DELEGATION_MODEL_IDENTITY_MISSING', 'native start event must provide a non-empty actual model identity');
485
+ }
221
486
  }
222
- const actualReasoningEffort = event.actualReasoningEffort;
223
- if (!actualReasoningEffort || actualReasoningEffort.trim() !== actualReasoningEffort) {
224
- return fail('DELEGATION_REASONING_EFFORT_MISSING', 'native start event must provide a non-empty actual reasoning effort');
487
+ if (actualModel !== null && actualModel !== receipt.requestedModel) {
488
+ if (receipt.client !== 'claude-code' && (!event.modelFallbackReason || event.modelFallbackReason.trim() === '')) {
489
+ return fail('DELEGATION_MODEL_FALLBACK_UNRECORDED', 'actual model differs from requested model without a fallback reason');
490
+ }
491
+ }
492
+ if (receipt.client !== 'claude-code') {
493
+ if (!actualReasoningEffort || actualReasoningEffort.trim() !== actualReasoningEffort) {
494
+ return fail('DELEGATION_REASONING_EFFORT_MISSING', 'native start event must provide a non-empty actual reasoning effort');
495
+ }
225
496
  }
226
497
  if (
227
498
  receipt.client === 'codex'
228
499
  && event.hostEvidence?.kind !== 'codex-lifecycle-v2'
229
500
  ) return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex activation requires trusted lifecycle-v2 host evidence');
230
- if (
231
- actualReasoningEffort !== receipt.requestedReasoningEffort
232
- && (!event.reasoningEffortFallbackReason || event.reasoningEffortFallbackReason.trim() === '')
233
- ) {
234
- return fail('DELEGATION_REASONING_EFFORT_FALLBACK_UNRECORDED', 'actual reasoning effort differs from requested effort without a fallback reason');
501
+ if (actualReasoningEffort !== null && actualReasoningEffort !== receipt.requestedReasoningEffort) {
502
+ if (receipt.client !== 'claude-code' && (!event.reasoningEffortFallbackReason || event.reasoningEffortFallbackReason.trim() === '')) {
503
+ return fail('DELEGATION_REASONING_EFFORT_FALLBACK_UNRECORDED', 'actual reasoning effort differs from requested effort without a fallback reason');
504
+ }
235
505
  }
236
- if (actualModel === receipt.requestedModel && event.modelFallbackReason) {
506
+ // "无关 fallback 理由"检查对所有 client 保持不变——防止编造理由的红线,不属于 HDR-2 放宽范围
507
+ // actualModel/actualReasoningEffort 为 null(未观察到)时同样没有"实际不同"这回事,理由一样是无关的
508
+ if ((actualModel === null || actualModel === receipt.requestedModel) && event.modelFallbackReason) {
237
509
  return fail('DELEGATION_MODEL_FALLBACK_INVALID', 'model fallback reason is only valid when actual model differs');
238
510
  }
239
- if (actualReasoningEffort === receipt.requestedReasoningEffort && event.reasoningEffortFallbackReason) {
511
+ if (
512
+ (actualReasoningEffort === null || actualReasoningEffort === receipt.requestedReasoningEffort)
513
+ && event.reasoningEffortFallbackReason
514
+ ) {
240
515
  return fail('DELEGATION_REASONING_EFFORT_FALLBACK_INVALID', 'reasoning-effort fallback reason is only valid when actual effort differs');
241
516
  }
242
517
  if (
@@ -280,7 +555,7 @@ function activateDelegation(
280
555
  status: 'activated',
281
556
  parentId: event.parentId,
282
557
  childId: event.childId,
283
- spawnMode: event.spawnMode,
558
+ spawnMode: event.spawnMode ?? null,
284
559
  actualModel,
285
560
  actualReasoningEffort,
286
561
  modelFallbackReason: event.modelFallbackReason ?? null,
@@ -328,6 +603,10 @@ function sealDelegation(
328
603
  exitCode: number;
329
604
  afterFingerprint: string;
330
605
  changedPaths: readonly string[];
606
+ actualModel?: string;
607
+ actualReasoningEffort?: string;
608
+ modelFallbackReason?: string;
609
+ reasoningEffortFallbackReason?: string;
331
610
  hostEvidence?: Readonly<{ stopRevision: number; consumer: string; consumedAt: string }>;
332
611
  }>,
333
612
  options: { now?: () => string; requireHostEvidence?: boolean } = {}
@@ -361,6 +640,10 @@ function sealDelegation(
361
640
  status: 'sealed',
362
641
  afterFingerprint: event.afterFingerprint,
363
642
  changedPaths: Object.freeze([...event.changedPaths]),
643
+ actualModel: receipt.actualModel ?? foldBlankToNull(event.actualModel),
644
+ actualReasoningEffort: receipt.actualReasoningEffort ?? foldBlankToNull(event.actualReasoningEffort),
645
+ modelFallbackReason: receipt.modelFallbackReason ?? event.modelFallbackReason ?? null,
646
+ reasoningEffortFallbackReason: receipt.reasoningEffortFallbackReason ?? event.reasoningEffortFallbackReason ?? null,
364
647
  hostEvidence: event.hostEvidence && receipt.hostEvidence ? Object.freeze({
365
648
  ...receipt.hostEvidence,
366
649
  ...event.hostEvidence
@@ -385,6 +668,8 @@ export {
385
668
  completeDelegationStage,
386
669
  consumeDelegation,
387
670
  dispatchDelegation,
671
+ foldBlankToNull,
672
+ isDelegationReceipt,
388
673
  managedDelegationRole,
389
674
  prepareDelegation,
390
675
  sealDelegation
@@ -32,6 +32,7 @@ import type { OrchestrationStageCompletion } from './orchestration.ts';
32
32
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
33
33
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
34
34
  import type { TaskOperationSummary, TaskWriteErrorCode, TaskWriteOptions } from './write.ts';
35
+ import { allowsManualOverride } from './guard-override.ts';
35
36
 
36
37
  const eventCatalog = [
37
38
  'analyze.started', 'analyze.awaiting-input', 'analyze.completed',
@@ -53,6 +54,7 @@ type TaskEventErrorCode =
53
54
  | ArtifactErrorCode | TaskWriteErrorCode;
54
55
  type TaskEventRequest = {
55
56
  taskRef: string; event: TaskEventName | string; agent: string; dryRun?: boolean; orchestrated?: boolean;
57
+ overrideTicket?: string; overrideTarget?: string; overrideScope?: string;
56
58
  round?: number; question?: number; artifact?: string; fixFor?: string; implementationInput?: string;
57
59
  verdict?: Verdict; blockers?: number; major?: number; minor?: number;
58
60
  manualValidation?: number; filesModified?: number; testsPassed?: number;
@@ -61,6 +63,7 @@ type TaskEventRequest = {
61
63
  type TaskEventError = { code: TaskEventErrorCode; message: string };
62
64
  type TaskEventOptions = TaskWriteOptions & {
63
65
  commitOrchestrationCompletion?: (plan: OrchestrationStageCompletion) => void;
66
+ lockAlreadyHeld?: boolean;
64
67
  };
65
68
  type TaskEventResult = {
66
69
  status: 'planned' | 'applied' | 'no-op' | 'failed'; changed: boolean;
@@ -73,7 +76,7 @@ type TaskEventResult = {
73
76
  operations: readonly TaskOperationSummary[]; error: TaskEventError | null;
74
77
  };
75
78
 
76
- const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun']);
79
+ const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun', 'overrideTicket', 'overrideTarget', 'overrideScope']);
77
80
  const SCHEMAS: Record<TaskEventName, { required?: string[]; optional?: string[] }> = {
78
81
  'analyze.started': { optional: ['round'] },
79
82
  'analyze.awaiting-input': { required: ['question'] },
@@ -357,7 +360,8 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
357
360
  if (invalid) return failed(request, invalid);
358
361
  const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
359
362
  if (!resolved.ok) return failed(request, { code: resolved.code, message: resolved.message }, { taskId: resolved.taskId });
360
- if (resolved.state !== 'active') return failed(request, { code: 'TASK_STATE_MISMATCH', message: `task ${resolved.taskId} is ${resolved.state}, expected active` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
363
+ const stateOverride = allowsManualOverride(options.manualOverride, 'task-event', 'TASK_STATE_MISMATCH');
364
+ if (resolved.state !== 'active' && !stateOverride) return failed(request, { code: 'TASK_STATE_MISMATCH', message: `task ${resolved.taskId} is ${resolved.state}, expected active` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
361
365
  let content: string;
362
366
  try { content = fs.readFileSync(resolved.taskMdPath, 'utf8'); }
363
367
  catch (error) { return failed(request, { code: 'TASK_READ_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
@@ -406,7 +410,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
406
410
  const done = completedRows.find((item) => item.note === eventIdentity.note);
407
411
  if (eventIdentity.phase === 'completed' && done) return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, done.done, frontmatter, artifactContext);
408
412
  if (eventIdentity.phase === 'completed' && completedRows.length > 0 && openRows.length === 0) return failed(normalized, { code: 'EVENT_LOG_CONFLICT', message: 'event identity is already completed with a different payload' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
409
- if (eventIdentity.phase === 'completed' && !row?.started) return failed(normalized, { code: 'EVENT_START_MISSING', message: 'completion requires one open matching started event' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
413
+ if (eventIdentity.phase === 'completed' && !row?.started && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_START_MISSING')) return failed(normalized, { code: 'EVENT_START_MISSING', message: 'completion requires one open matching started event' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
410
414
  if (eventIdentity.phase === 'completed') {
411
415
  const validated = validateCompletedArtifact(resolved.taskDir, FAMILY[eventIdentity.family].artifact, normalized.artifact!, normalized.round);
412
416
  if (!validated.ok) return failed(normalized, validated.error, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
@@ -417,7 +421,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
417
421
  return successNoOp(normalized, resolved.taskId, resolved.taskMdPath, currentStep, eventIdentity, existing.time, frontmatter, artifactContext);
418
422
  }
419
423
  const allowed = FAMILY[eventIdentity.family][eventIdentity.phase === 'started' ? 'started' : 'completed'];
420
- if (!(allowed as readonly string[]).includes(currentStep)) return failed(normalized, { code: 'EVENT_TRANSITION_INVALID', message: `${normalized.event} is not allowed from '${currentStep}'` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
424
+ if (!(allowed as readonly string[]).includes(currentStep) && !allowsManualOverride(options.manualOverride, 'task-event', 'EVENT_TRANSITION_INVALID')) return failed(normalized, { code: 'EVENT_TRANSITION_INVALID', message: `${normalized.event} is not allowed from '${currentStep}'` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
421
425
  const findingCountError = validateReviewFindingCounts(
422
426
  normalized,
423
427
  content,
@@ -522,7 +526,7 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
522
526
  });
523
527
  }
524
528
  mutations.push({ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body });
525
- const result = writeTask({ taskRef: normalized.taskRef, expectedState: 'active', dryRun: normalized.dryRun, mutations }, { ...options, metadataProvider: () => metadata });
529
+ const result = writeTask({ taskRef: normalized.taskRef, expectedState: stateOverride ? resolved.state : 'active', dryRun: normalized.dryRun, mutations }, { ...options, metadataProvider: () => metadata });
526
530
  if (result.status === 'failed') return failed(normalized, result.error, { taskId: result.taskId, taskMdPath: result.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, timestamp: result.timestamp, agentInfraVersion: result.agentInfraVersion, operations: result.operations, artifactContext });
527
531
  if (!normalized.dryRun && orchestrationCompletion) {
528
532
  try {
@@ -560,6 +564,7 @@ function applyTaskEvent(request: TaskEventRequest, options: TaskEventOptions = {
560
564
  const invalid = validateTaskEventRequest(request);
561
565
  if (invalid) return failed(request, invalid);
562
566
  const parts = eventParts(request.event);
567
+ if (options.lockAlreadyHeld) return applyTaskEventUnlocked(request, options);
563
568
  if (request.dryRun || parts.phase !== 'completed' || parts.family === 'manual-validation') {
564
569
  return applyTaskEventUnlocked(request, options);
565
570
  }