@fitlab-ai/agent-infra 0.9.6 → 0.9.7

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 (127) hide show
  1. package/dist/bin/internal-cli.js +5 -0
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +45 -4
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +21 -5
  7. package/dist/lib/defaults.json +10 -2
  8. package/dist/lib/git/workflow.js +30 -3
  9. package/dist/lib/internal/codex-lifecycle.js +69 -1
  10. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  11. package/dist/lib/internal/task-orchestration.js +37 -5
  12. package/dist/lib/platform/context.js +6 -4
  13. package/dist/lib/platform/github-client.js +55 -2
  14. package/dist/lib/process-data/checkpoint.js +319 -0
  15. package/dist/lib/process-data/index.js +128 -60
  16. package/dist/lib/process-data/materialize.js +84 -0
  17. package/dist/lib/process-data/normalize.js +34 -1
  18. package/dist/lib/process-data/reconcile.js +55 -1
  19. package/dist/lib/process-data/sources.js +422 -1
  20. package/dist/lib/process-data/store.js +341 -11
  21. package/dist/lib/sandbox/commands/create.js +103 -51
  22. package/dist/lib/sandbox/commands/rm.js +64 -15
  23. package/dist/lib/sandbox/commands/start.js +4 -3
  24. package/dist/lib/sandbox/control/container-identity.js +64 -0
  25. package/dist/lib/sandbox/control/executor.js +13 -7
  26. package/dist/lib/sandbox/control/lifecycle.js +238 -29
  27. package/dist/lib/sandbox/control/protocol.js +7 -0
  28. package/dist/lib/sandbox/control/server.js +202 -26
  29. package/dist/lib/sandbox/control/state.js +25 -15
  30. package/dist/lib/sandbox/control/timing.js +8 -0
  31. package/dist/lib/sandbox/mounts.js +5 -13
  32. package/dist/lib/sandbox/recovery.js +59 -42
  33. package/dist/lib/sandbox/workspace-view.js +57 -40
  34. package/dist/lib/server/daemon.js +1 -1
  35. package/dist/lib/server/process-control.js +11 -1
  36. package/dist/lib/server/process-state.js +57 -20
  37. package/dist/lib/task/artifact-lifecycle.js +82 -11
  38. package/dist/lib/task/artifact-receipts.js +128 -0
  39. package/dist/lib/task/codex-orchestration.js +81 -6
  40. package/dist/lib/task/commands/validate.js +1 -1
  41. package/dist/lib/task/create.js +10 -2
  42. package/dist/lib/task/delegation-receipts.js +84 -2
  43. package/dist/lib/task/events.js +139 -2
  44. package/dist/lib/task/ledger.js +13 -1
  45. package/dist/lib/task/orchestration.js +235 -14
  46. package/dist/lib/task/sections.js +5 -1
  47. package/dist/lib/task/task-execution-lock.js +5 -4
  48. package/dist/lib/task/verification-engine.js +40 -8
  49. package/dist/lifecycle-build-manifest.json +6 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +60 -3
  51. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  52. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  53. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  54. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  55. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +33 -4
  56. package/lib/defaults.json +10 -2
  57. package/lib/git/workflow.ts +25 -2
  58. package/lib/internal/codex-lifecycle.ts +68 -1
  59. package/lib/internal/codex-sandbox-controller.ts +109 -0
  60. package/lib/internal/task-orchestration.ts +31 -4
  61. package/lib/platform/context.ts +8 -5
  62. package/lib/platform/github-client.ts +61 -3
  63. package/lib/process-data/checkpoint.ts +335 -0
  64. package/lib/process-data/index.ts +112 -60
  65. package/lib/process-data/materialize.ts +83 -0
  66. package/lib/process-data/normalize.ts +29 -1
  67. package/lib/process-data/reconcile.ts +64 -2
  68. package/lib/process-data/sources.ts +449 -2
  69. package/lib/process-data/store.ts +304 -11
  70. package/lib/process-data/types.ts +124 -2
  71. package/lib/sandbox/commands/create.ts +56 -7
  72. package/lib/sandbox/commands/rm.ts +64 -14
  73. package/lib/sandbox/commands/start.ts +4 -3
  74. package/lib/sandbox/control/container-identity.ts +93 -0
  75. package/lib/sandbox/control/executor.ts +14 -10
  76. package/lib/sandbox/control/lifecycle.ts +264 -33
  77. package/lib/sandbox/control/protocol.ts +31 -5
  78. package/lib/sandbox/control/server.ts +184 -24
  79. package/lib/sandbox/control/state.ts +34 -17
  80. package/lib/sandbox/control/timing.ts +12 -0
  81. package/lib/sandbox/mounts.ts +5 -15
  82. package/lib/sandbox/recovery.ts +71 -42
  83. package/lib/sandbox/workspace-view.ts +61 -40
  84. package/lib/server/daemon.ts +1 -1
  85. package/lib/server/process-control.ts +13 -3
  86. package/lib/server/process-state.ts +82 -27
  87. package/lib/task/artifact-lifecycle.ts +73 -10
  88. package/lib/task/artifact-receipts.ts +161 -0
  89. package/lib/task/codex-orchestration.ts +91 -5
  90. package/lib/task/commands/validate.ts +1 -1
  91. package/lib/task/create.ts +11 -2
  92. package/lib/task/delegation-receipts.ts +160 -8
  93. package/lib/task/events.ts +122 -1
  94. package/lib/task/ledger.ts +16 -1
  95. package/lib/task/orchestration.ts +356 -17
  96. package/lib/task/sections.ts +6 -2
  97. package/lib/task/task-execution-lock.ts +11 -9
  98. package/lib/task/verification-engine.ts +51 -16
  99. package/package.json +1 -1
  100. package/templates/.agents/README.en.md +7 -0
  101. package/templates/.agents/README.zh-CN.md +5 -0
  102. package/templates/.agents/hooks/lifecycle-delegation.js +25 -1
  103. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  104. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  105. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  106. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  107. package/templates/.agents/rules/review-handshake.en.md +3 -1
  108. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  109. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  110. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  111. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  112. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  113. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  114. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  115. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  116. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  117. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  118. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  119. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  120. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  121. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  122. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  123. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -0
  124. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -0
  125. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  126. package/templates/.agents/workspace/README.en.md +0 -26
  127. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -7,12 +7,41 @@ type DelegationRole = 'executor' | 'reviewer';
7
7
  type DelegationStage = 'analysis' | 'review-analysis' | 'plan' | 'review-plan' | 'code' | 'review-code' | 'commit';
8
8
  type DelegationStatus = 'prepared' | 'activated' | 'stage-completed' | 'sealed' | 'consumed' | 'aborted' | 'expired';
9
9
  type DelegationHostEvidence = Readonly<{
10
- kind: 'codex-lifecycle-v1';
10
+ kind: 'codex-lifecycle-v1' | 'codex-lifecycle-v2';
11
11
  hookDefinitionHash: string;
12
12
  startRevision: number;
13
13
  stopRevision: number | null;
14
14
  consumer: string | null;
15
15
  consumedAt: string | null;
16
+ protocolVersion?: number;
17
+ packageVersion?: string;
18
+ internalExecutableBuildHash?: string;
19
+ lifecycleContractHash?: string;
20
+ hookSource?: 'project' | 'managed' | 'isolated-user';
21
+ hookSourcePathDigest?: string;
22
+ hookSourceHash?: string;
23
+ capabilitySessionId?: string;
24
+ capabilityTurnId?: string;
25
+ capabilityToolUseId?: string;
26
+ spawnToolUseId?: string;
27
+ spawnObservedAt?: string;
28
+ controllerInstanceDigest?: string | null;
29
+ controlGeneration?: string | null;
30
+ }>;
31
+ type DelegationLifecycleProvenance = Readonly<{
32
+ protocolVersion: number;
33
+ packageVersion: string;
34
+ internalExecutableBuildHash: string;
35
+ lifecycleContractHash: string;
36
+ hookDefinitionHash: string;
37
+ hookSource: 'project' | 'managed' | 'isolated-user';
38
+ hookSourcePathDigest: string;
39
+ hookSourceHash: string;
40
+ capabilitySessionId: string;
41
+ capabilityTurnId: string;
42
+ capabilityToolUseId: string;
43
+ controllerInstanceDigest: string | null;
44
+ controlGeneration: string | null;
16
45
  }>;
17
46
 
18
47
  type DelegationReceipt = Readonly<{
@@ -36,11 +65,19 @@ type DelegationReceipt = Readonly<{
36
65
  agent: string | null;
37
66
  status: DelegationStatus;
38
67
  workspaceSnapshotScope?: 'task';
68
+ lifecycleProvenance?: DelegationLifecycleProvenance | null;
39
69
  hostEvidence?: DelegationHostEvidence | null;
40
70
  beforeFingerprint: string;
41
71
  afterFingerprint: string | null;
42
72
  changedPaths: readonly string[];
43
73
  createdAt: string;
74
+ preparedMonotonicMs: number;
75
+ spawnDispatchMonotonicMs: number | null;
76
+ activationDeadlineMonotonicMs: number | null;
77
+ spawnDispatchedAt: string | null;
78
+ activationDeadlineAt: string | null;
79
+ startEvidenceMonotonicMs: number | null;
80
+ activatedMonotonicMs: number | null;
44
81
  activatedAt: string | null;
45
82
  sealedAt: string | null;
46
83
  consumedAt: string | null;
@@ -64,9 +101,10 @@ function fail(code: string, message: string): ReceiptFailure {
64
101
  }
65
102
 
66
103
  function prepareDelegation(
67
- input: Omit<DelegationReceipt, 'id' | 'requestedModel' | 'requestedReasoningEffort' | 'actualModel' | 'actualReasoningEffort' | 'modelFallbackReason' | 'reasoningEffortFallbackReason' | 'parentId' | 'childId' | 'spawnMode' | 'agent' | 'status' | 'hostEvidence' | 'afterFingerprint' | 'changedPaths' | 'createdAt' | 'activatedAt' | 'sealedAt' | 'consumedAt'> & Readonly<{ requestedModel: string; requestedReasoningEffort: string }>,
68
- options: { id?: () => string; now?: () => string } = {}
104
+ 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
+ options: { id?: () => string; now?: () => string; monotonicNow?: () => number } = {}
69
106
  ): DelegationReceipt {
107
+ const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))();
70
108
  return Object.freeze({
71
109
  ...input,
72
110
  id: (options.id ?? randomUUID)(),
@@ -83,12 +121,41 @@ function prepareDelegation(
83
121
  afterFingerprint: null,
84
122
  changedPaths: Object.freeze([]),
85
123
  createdAt: (options.now ?? (() => new Date().toISOString()))(),
124
+ preparedMonotonicMs: monotonic,
125
+ spawnDispatchMonotonicMs: null,
126
+ activationDeadlineMonotonicMs: null,
127
+ spawnDispatchedAt: null,
128
+ activationDeadlineAt: null,
129
+ startEvidenceMonotonicMs: null,
130
+ activatedMonotonicMs: null,
86
131
  activatedAt: null,
87
132
  sealedAt: null,
88
133
  consumedAt: null
89
134
  });
90
135
  }
91
136
 
137
+ function dispatchDelegation(
138
+ receipt: DelegationReceipt,
139
+ options: { now?: () => string; monotonicNow?: () => number; activationWindowMs?: number } = {}
140
+ ): ReceiptResult {
141
+ if (receipt.status !== 'prepared') {
142
+ return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
143
+ }
144
+ if (receipt.spawnDispatchMonotonicMs != null || receipt.spawnDispatchedAt != null) {
145
+ return fail('DELEGATION_DISPATCH_REPLAY', `delegation ${receipt.id} was already dispatched`);
146
+ }
147
+ const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))();
148
+ const dispatchedAt = (options.now ?? (() => new Date().toISOString()))();
149
+ const window = options.activationWindowMs ?? 15_000;
150
+ return { ok: true, receipt: Object.freeze({
151
+ ...receipt,
152
+ spawnDispatchMonotonicMs: monotonic,
153
+ activationDeadlineMonotonicMs: monotonic + window,
154
+ spawnDispatchedAt: dispatchedAt,
155
+ activationDeadlineAt: new Date(Date.parse(dispatchedAt) + window).toISOString()
156
+ }) };
157
+ }
158
+
92
159
  function activateDelegation(
93
160
  receipt: DelegationReceipt,
94
161
  event: Readonly<{
@@ -101,16 +168,45 @@ function activateDelegation(
101
168
  modelFallbackReason?: string;
102
169
  reasoningEffortFallbackReason?: string;
103
170
  hostEvidence?: Readonly<{
104
- kind: 'codex-lifecycle-v1';
171
+ kind: 'codex-lifecycle-v1' | 'codex-lifecycle-v2';
105
172
  hookDefinitionHash: string;
106
173
  startRevision: number;
174
+ protocolVersion?: number;
175
+ packageVersion?: string;
176
+ internalExecutableBuildHash?: string;
177
+ lifecycleContractHash?: string;
178
+ hookSource?: 'project' | 'managed' | 'isolated-user';
179
+ hookSourcePathDigest?: string;
180
+ hookSourceHash?: string;
181
+ capabilitySessionId?: string;
182
+ capabilityTurnId?: string;
183
+ capabilityToolUseId?: string;
184
+ spawnToolUseId?: string;
185
+ spawnObservedAt?: string;
186
+ controllerInstanceDigest?: string | null;
187
+ controlGeneration?: string | null;
107
188
  }>;
108
189
  }>,
109
- options: { now?: () => string } = {}
190
+ options: { now?: () => string; monotonicNow?: () => number } = {}
110
191
  ): ReceiptResult {
111
192
  const managedRole = managedDelegationRole(event.nativeAgent);
112
193
  if (!managedRole) return fail('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
113
194
  if (receipt.status !== 'prepared') return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
195
+ if (
196
+ receipt.spawnDispatchMonotonicMs == null
197
+ || receipt.spawnDispatchedAt == null
198
+ || receipt.activationDeadlineAt == null
199
+ || receipt.activationDeadlineMonotonicMs == null
200
+ || !Number.isFinite(Date.parse(receipt.spawnDispatchedAt))
201
+ || !Number.isFinite(Date.parse(receipt.activationDeadlineAt))
202
+ ) {
203
+ return fail('DELEGATION_NOT_DISPATCHED', `delegation ${receipt.id} has not reached the native spawn dispatch boundary`);
204
+ }
205
+ const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))();
206
+ const wallNow = Date.parse((options.now ?? (() => new Date().toISOString()))());
207
+ if (wallNow > Date.parse(receipt.activationDeadlineAt)) {
208
+ return fail('DELEGATION_ACTIVATION_TIMEOUT', `delegation ${receipt.id} activation evidence arrived after its deadline`);
209
+ }
114
210
  if (managedRole !== receipt.role) return fail('DELEGATION_ROLE_MISMATCH', `managed role ${managedRole} does not match ${receipt.role}`);
115
211
  if (!event.parentId || (receipt.parentId !== null && event.parentId !== receipt.parentId) || !event.childId || event.childId === event.parentId) {
116
212
  return fail('DELEGATION_IDENTITY_INVALID', 'native parent/child identity does not match the prepared delegation');
@@ -127,6 +223,10 @@ function activateDelegation(
127
223
  if (!actualReasoningEffort || actualReasoningEffort.trim() !== actualReasoningEffort) {
128
224
  return fail('DELEGATION_REASONING_EFFORT_MISSING', 'native start event must provide a non-empty actual reasoning effort');
129
225
  }
226
+ if (
227
+ receipt.client === 'codex'
228
+ && event.hostEvidence?.kind !== 'codex-lifecycle-v2'
229
+ ) return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex activation requires trusted lifecycle-v2 host evidence');
130
230
  if (
131
231
  actualReasoningEffort !== receipt.requestedReasoningEffort
132
232
  && (!event.reasoningEffortFallbackReason || event.reasoningEffortFallbackReason.trim() === '')
@@ -148,6 +248,33 @@ function activateDelegation(
148
248
  || event.hostEvidence.startRevision < 1
149
249
  )
150
250
  ) return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex start evidence reference is invalid');
251
+ if (event.hostEvidence?.kind === 'codex-lifecycle-v2') {
252
+ const expected = receipt.lifecycleProvenance;
253
+ const spawnObservedAt = Date.parse(event.hostEvidence.spawnObservedAt ?? '');
254
+ const spawnDispatchedAt = Date.parse(receipt.spawnDispatchedAt);
255
+ const activationDeadlineAt = Date.parse(receipt.activationDeadlineAt);
256
+ if (
257
+ !expected
258
+ || event.hostEvidence.protocolVersion !== expected.protocolVersion
259
+ || event.hostEvidence.packageVersion !== expected.packageVersion
260
+ || event.hostEvidence.internalExecutableBuildHash !== expected.internalExecutableBuildHash
261
+ || event.hostEvidence.lifecycleContractHash !== expected.lifecycleContractHash
262
+ || event.hostEvidence.hookDefinitionHash !== expected.hookDefinitionHash
263
+ || event.hostEvidence.hookSource !== expected.hookSource
264
+ || event.hostEvidence.hookSourcePathDigest !== expected.hookSourcePathDigest
265
+ || event.hostEvidence.hookSourceHash !== expected.hookSourceHash
266
+ || event.hostEvidence.capabilitySessionId !== expected.capabilitySessionId
267
+ || event.hostEvidence.capabilityTurnId !== expected.capabilityTurnId
268
+ || !event.hostEvidence.spawnToolUseId?.trim()
269
+ || event.hostEvidence.spawnToolUseId === expected.capabilityToolUseId
270
+ || !Number.isFinite(spawnObservedAt)
271
+ || spawnObservedAt < spawnDispatchedAt
272
+ || spawnObservedAt > activationDeadlineAt
273
+ || event.parentId !== expected.capabilitySessionId
274
+ || (event.hostEvidence.controllerInstanceDigest ?? null) !== expected.controllerInstanceDigest
275
+ || (event.hostEvidence.controlGeneration ?? null) !== expected.controlGeneration
276
+ ) return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex lifecycle provenance does not match the prepared receipt');
277
+ }
151
278
  return { ok: true, receipt: Object.freeze({
152
279
  ...receipt,
153
280
  status: 'activated',
@@ -164,10 +291,22 @@ function activateDelegation(
164
291
  consumer: null,
165
292
  consumedAt: null
166
293
  }) : receipt.hostEvidence ?? null,
167
- activatedAt: (options.now ?? (() => new Date().toISOString()))()
294
+ startEvidenceMonotonicMs: monotonic,
295
+ activatedMonotonicMs: (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))(),
296
+ activatedAt: new Date(wallNow).toISOString()
168
297
  }) };
169
298
  }
170
299
 
300
+ function abortPreparedDelegation(receipt: DelegationReceipt): ReceiptResult {
301
+ if (receipt.status !== 'prepared') {
302
+ return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
303
+ }
304
+ return {
305
+ ok: true,
306
+ receipt: Object.freeze({ ...receipt, status: 'aborted' as const })
307
+ };
308
+ }
309
+
171
310
  function completeDelegationStage(
172
311
  receipt: DelegationReceipt,
173
312
  event: Readonly<{ stage: DelegationStage; round: number; artifact: string; agent: string }>
@@ -191,9 +330,12 @@ function sealDelegation(
191
330
  changedPaths: readonly string[];
192
331
  hostEvidence?: Readonly<{ stopRevision: number; consumer: string; consumedAt: string }>;
193
332
  }>,
194
- options: { now?: () => string } = {}
333
+ options: { now?: () => string; requireHostEvidence?: boolean } = {}
195
334
  ): ReceiptResult {
196
335
  if (receipt.status !== 'stage-completed') return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected stage-completed`);
336
+ if (receipt.client === 'codex' && options.requireHostEvidence !== false && !event.hostEvidence) {
337
+ return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex sealing requires consumed lifecycle-v2 host evidence');
338
+ }
197
339
  if (event.childId !== receipt.childId || event.exitCode !== 0) return fail('DELEGATION_STOP_INVALID', 'native stop identity or exit status is invalid');
198
340
  if (receipt.role === 'reviewer') {
199
341
  const taskRoot = `.agents/workspace/active/${receipt.taskId}/`;
@@ -239,10 +381,20 @@ function consumeDelegation(receipt: DelegationReceipt, options: { now?: () => st
239
381
 
240
382
  export {
241
383
  activateDelegation,
384
+ abortPreparedDelegation,
242
385
  completeDelegationStage,
243
386
  consumeDelegation,
387
+ dispatchDelegation,
244
388
  managedDelegationRole,
245
389
  prepareDelegation,
246
390
  sealDelegation
247
391
  };
248
- export type { DelegationHostEvidence, DelegationReceipt, DelegationRole, DelegationStage, DelegationStatus, ReceiptResult };
392
+ export type {
393
+ DelegationHostEvidence,
394
+ DelegationLifecycleProvenance,
395
+ DelegationReceipt,
396
+ DelegationRole,
397
+ DelegationStage,
398
+ DelegationStatus,
399
+ ReceiptResult
400
+ };
@@ -4,11 +4,16 @@ import { appendActivityEntry, locateActivityLog, pairEntries } from './activity-
4
4
  import {
5
5
  buildArtifactLinkSection,
6
6
  artifactName,
7
+ inspectArtifactDirectory,
8
+ parseReviewedInputReference,
9
+ parseCodePlanInputReference,
7
10
  parseArtifactName,
8
11
  resolveArtifactContext,
9
12
  validateCompletedArtifact
10
13
  } from './artifact-lifecycle.ts';
11
14
  import type { ArtifactContextResult, ArtifactErrorCode, ArtifactFamily, ArtifactIdentity } from './artifact-lifecycle.ts';
15
+ import { ArtifactReceiptError, sha256File, upsertArtifactReceipt } from './artifact-receipts.ts';
16
+ import type { ArtifactReceipt } from './artifact-receipts.ts';
12
17
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
13
18
  import {
14
19
  consumeImplementationInput,
@@ -273,6 +278,80 @@ function openStartedIdentity(rows: ReturnType<typeof pairEntries>, family: Event
273
278
  return matches.length === 1 ? matches[0] : matches.length > 1 ? { conflict: true as const } : null;
274
279
  }
275
280
 
281
+ function reviewInputFamily(family: EventFamily): ArtifactFamily {
282
+ return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
283
+ }
284
+
285
+ function buildCompletionReceipt(
286
+ taskDir: string,
287
+ family: EventFamily,
288
+ artifact: ArtifactIdentity,
289
+ completedAt: string,
290
+ frontmatter: Record<string, unknown>
291
+ ): { ok: true; receipt: ArtifactReceipt } | { ok: false; message: string } | null {
292
+ if (family === 'code') {
293
+ let content: string;
294
+ try { content = fs.readFileSync(artifact.path, 'utf8'); }
295
+ catch (error) { return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` }; }
296
+ const input = parseCodePlanInputReference(content);
297
+ if (!input) return { ok: false, message: `${artifact.name} does not reference a canonical plan artifact` };
298
+ const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
299
+ const startedSha256 = typeof frontmatter.code_input_sha256 === 'string' ? frontmatter.code_input_sha256 : '';
300
+ if (!startedInput || !startedSha256) return { ok: false, message: 'code.started plan input context is missing' };
301
+ if (input.name !== startedInput) return { ok: false, message: `${artifact.name} plan input '${input.name}' does not match code.started input '${startedInput}'` };
302
+ const plan = inspectArtifactDirectory(taskDir, 'plan');
303
+ if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== input.name) {
304
+ return { ok: false, message: `code input '${input.name}' is not the latest plan artifact` };
305
+ }
306
+ try {
307
+ const inputSha256 = sha256File(plan.latest.path);
308
+ if (inputSha256 !== startedSha256) return { ok: false, message: `code input ${input.name} changed after code.started` };
309
+ return {
310
+ ok: true,
311
+ receipt: {
312
+ event: 'code.completed', output: artifact.name, input: input.name,
313
+ inputSha256, completedAt
314
+ }
315
+ };
316
+ } catch (error) {
317
+ return { ok: false, message: `cannot hash code input plan: ${error instanceof Error ? error.message : String(error)}` };
318
+ }
319
+ }
320
+ if (!family.startsWith('review-')) return null;
321
+ const expectedFamily = reviewInputFamily(family);
322
+ let content: string;
323
+ try { content = fs.readFileSync(artifact.path, 'utf8'); }
324
+ catch (error) { return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` }; }
325
+ const input = parseReviewedInputReference(content, expectedFamily);
326
+ if (!input) return { ok: false, message: `${artifact.name} does not reference a canonical ${expectedFamily} artifact` };
327
+ const startedInput = typeof frontmatter.review_input_artifact === 'string' ? frontmatter.review_input_artifact : '';
328
+ const startedSha256 = typeof frontmatter.review_input_sha256 === 'string' ? frontmatter.review_input_sha256 : '';
329
+ if (!startedInput || !startedSha256) return { ok: false, message: 'review started input context is missing' };
330
+ if (input.name !== startedInput) return { ok: false, message: `${artifact.name} input '${input.name}' does not match review.started input '${startedInput}'` };
331
+ const current = inspectArtifactDirectory(taskDir, expectedFamily);
332
+ if (current.status !== 'ready' || !current.latest || current.latest.name !== input.name) {
333
+ return { ok: false, message: `review input '${input.name}' is not the latest ${expectedFamily} artifact` };
334
+ }
335
+ const event = family === 'review-analysis'
336
+ ? 'review-analysis.completed' as const
337
+ : family === 'review-plan' ? 'review-plan.completed' as const : 'review-code.completed' as const;
338
+ try {
339
+ const stat = fs.lstatSync(current.latest.path);
340
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('review input is not a regular file');
341
+ const inputSha256 = sha256File(current.latest.path);
342
+ if (inputSha256 !== startedSha256) return { ok: false, message: `review input ${input.name} changed after review.started` };
343
+ return {
344
+ ok: true,
345
+ receipt: {
346
+ event, output: artifact.name, input: input.name,
347
+ inputSha256, completedAt
348
+ }
349
+ };
350
+ } catch (error) {
351
+ return { ok: false, message: `cannot hash review input ${input.name}: ${error instanceof Error ? error.message : String(error)}` };
352
+ }
353
+ }
354
+
276
355
  function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOptions = {}): TaskEventResult {
277
356
  const invalid = validateTaskEventRequest(request);
278
357
  if (invalid) return failed(request, invalid);
@@ -371,18 +450,60 @@ function applyTaskEventUnlocked(request: TaskEventRequest, options: TaskEventOpt
371
450
  let metadata;
372
451
  try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
373
452
  catch (error) { return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath }); }
453
+ let completionReceipt: ArtifactReceipt | null = null;
454
+ if (eventIdentity.phase === 'completed' && completedArtifact) {
455
+ const receipt = buildCompletionReceipt(resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
456
+ if (receipt && !receipt.ok) {
457
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: receipt.message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
458
+ }
459
+ completionReceipt = receipt?.receipt ?? null;
460
+ }
374
461
  const step = eventIdentity.phase === 'started' || eventIdentity.target === null ? currentStep : eventIdentity.target;
375
462
  const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
376
463
  const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
377
464
  const frontmatterSet: Record<string, string> = { current_step: step, assigned_to: normalized.agent };
465
+ let frontmatterRemove: string[] | undefined;
466
+ if (eventIdentity.phase === 'started' && eventIdentity.family === 'code') {
467
+ const planInput = artifactContext?.inputs.find((input) => input.family === 'plan');
468
+ if (!planInput) return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: 'code.started plan input context is unavailable' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
469
+ try {
470
+ frontmatterSet.code_input_artifact = planInput.name;
471
+ frontmatterSet.code_input_sha256 = sha256File(planInput.path);
472
+ } catch (error) {
473
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot hash code.started plan input: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
474
+ }
475
+ } else if (eventIdentity.phase === 'completed' && eventIdentity.family === 'code') {
476
+ frontmatterRemove = ['code_input_artifact', 'code_input_sha256'];
477
+ } else if (eventIdentity.phase === 'started' && eventIdentity.family.startsWith('review-')) {
478
+ const expectedFamily = reviewInputFamily(eventIdentity.family);
479
+ const input = artifactContext?.inputs.find((candidate) => candidate.family === expectedFamily);
480
+ if (!input) return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `review.started ${expectedFamily} input context is unavailable` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
481
+ try {
482
+ frontmatterSet.review_input_artifact = input.name;
483
+ frontmatterSet.review_input_sha256 = sha256File(input.path);
484
+ } catch (error) {
485
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot hash review.started input: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
486
+ }
487
+ } else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
488
+ frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
489
+ }
378
490
  if (eventIdentity.phase === 'started' && normalized.implementationInput) frontmatterSet.last_reviewed_commit = '';
379
491
  const mutations: Parameters<typeof writeTask>[0]['mutations'][number][] = [
380
- { kind: 'frontmatter', set: frontmatterSet }
492
+ { kind: 'frontmatter', set: frontmatterSet, remove: frontmatterRemove }
381
493
  ];
382
494
  if (completedArtifact) {
383
495
  const link = buildArtifactLinkSection(content, completedArtifact);
384
496
  mutations.push({ kind: 'section', aliases: link.aliases, heading: link.heading, body: link.body });
385
497
  }
498
+ if (completionReceipt) {
499
+ try {
500
+ const receiptSection = upsertArtifactReceipt(content, completionReceipt);
501
+ mutations.push({ kind: 'section', aliases: receiptSection.aliases, heading: receiptSection.heading, body: receiptSection.body });
502
+ } catch (error) {
503
+ const message = error instanceof ArtifactReceiptError ? error.message : String(error);
504
+ return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
505
+ }
506
+ }
386
507
  if (eventIdentity.phase === 'completed' && normalized.implementationInput) {
387
508
  let implementationRows;
388
509
  try {
@@ -61,7 +61,22 @@ function validateLedgerRows(rows: readonly LedgerRow[]): LedgerValidationError |
61
61
  for (const row of rows) {
62
62
  if (seen.has(row.id)) return { code: 'LEDGER_DUPLICATE_ID', message: `duplicate ledger id '${row.id}'` };
63
63
  seen.add(row.id);
64
- if (/^PRC-[1-9]\d*$/.test(row.id)) continue;
64
+ if (/^PRC-[1-9]\d*$/.test(row.id)) {
65
+ if (
66
+ row.stage !== 'post-review-commit' ||
67
+ row.round !== '-' ||
68
+ row.severity !== '-' ||
69
+ row.status !== 'human-decided' ||
70
+ row.evidence === '' ||
71
+ /[\r\n]/.test(row.evidence)
72
+ ) {
73
+ return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' is invalid` };
74
+ }
75
+ continue;
76
+ }
77
+ if (row.stage === 'post-review-commit') {
78
+ return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' must use a PRC id` };
79
+ }
65
80
  if (!/^(AN|PL|CD|HD)-[1-9]\d*$/.test(row.id)) return { code: 'LEDGER_ID_INVALID', message: `ledger id '${row.id}' is invalid` };
66
81
  if (!['analysis', 'plan', 'code', 'post-review-commit'].includes(row.stage)) return { code: 'LEDGER_STAGE_INVALID', message: `ledger stage '${row.stage}' is invalid` };
67
82
  if (!FINDING_STATUSES.has(row.status)) return { code: 'LEDGER_STATUS_INVALID', message: `ledger status '${row.status}' is invalid` };