@fitlab-ai/agent-infra 0.9.5 → 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 (172) hide show
  1. package/dist/bin/internal-cli.js +8 -3
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
  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/evidence.js +14 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
  8. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -4
  11. package/dist/lib/internal/codex-lifecycle.js +169 -3
  12. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  13. package/dist/lib/internal/release-workflow.js +108 -11
  14. package/dist/lib/internal/sandbox-control.js +43 -6
  15. package/dist/lib/internal/task-create.js +12 -4
  16. package/dist/lib/internal/task-orchestration.js +40 -7
  17. package/dist/lib/platform/context.js +6 -4
  18. package/dist/lib/platform/github-client.js +55 -2
  19. package/dist/lib/platform/issue-comments.js +2 -1
  20. package/dist/lib/process-data/checkpoint.js +319 -0
  21. package/dist/lib/process-data/index.js +128 -60
  22. package/dist/lib/process-data/materialize.js +84 -0
  23. package/dist/lib/process-data/normalize.js +34 -1
  24. package/dist/lib/process-data/reconcile.js +55 -1
  25. package/dist/lib/process-data/sources.js +422 -1
  26. package/dist/lib/process-data/store.js +341 -11
  27. package/dist/lib/release/workflow.js +29 -21
  28. package/dist/lib/sandbox/commands/create.js +111 -60
  29. package/dist/lib/sandbox/commands/prune.js +40 -16
  30. package/dist/lib/sandbox/commands/rm.js +372 -94
  31. package/dist/lib/sandbox/commands/start.js +4 -3
  32. package/dist/lib/sandbox/control/client.js +138 -22
  33. package/dist/lib/sandbox/control/container-identity.js +64 -0
  34. package/dist/lib/sandbox/control/executor.js +142 -0
  35. package/dist/lib/sandbox/control/lifecycle.js +557 -0
  36. package/dist/lib/sandbox/control/protocol.js +69 -26
  37. package/dist/lib/sandbox/control/server.js +395 -99
  38. package/dist/lib/sandbox/control/state.js +191 -0
  39. package/dist/lib/sandbox/control/timing.js +8 -0
  40. package/dist/lib/sandbox/index.js +2 -2
  41. package/dist/lib/sandbox/managed-fs.js +14 -2
  42. package/dist/lib/sandbox/mounts.js +22 -19
  43. package/dist/lib/sandbox/recovery.js +284 -50
  44. package/dist/lib/sandbox/workspace-view.js +90 -10
  45. package/dist/lib/sandbox/worktree-safety.js +196 -0
  46. package/dist/lib/server/daemon.js +1 -1
  47. package/dist/lib/server/process-control.js +17 -1
  48. package/dist/lib/server/process-state.js +57 -20
  49. package/dist/lib/task/artifact-lifecycle.js +87 -15
  50. package/dist/lib/task/artifact-receipts.js +128 -0
  51. package/dist/lib/task/codex-orchestration.js +236 -0
  52. package/dist/lib/task/commands/status.js +3 -0
  53. package/dist/lib/task/commands/validate.js +289 -0
  54. package/dist/lib/task/commit-finalization.js +36 -9
  55. package/dist/lib/task/create.js +10 -2
  56. package/dist/lib/task/delegation-receipts.js +109 -2
  57. package/dist/lib/task/events.js +149 -6
  58. package/dist/lib/task/finalization-artifacts.js +2 -1
  59. package/dist/lib/task/implementation-inputs.js +1 -7
  60. package/dist/lib/task/index.js +9 -0
  61. package/dist/lib/task/ledger.js +13 -1
  62. package/dist/lib/task/orchestration.js +365 -15
  63. package/dist/lib/task/sections.js +5 -1
  64. package/dist/lib/task/task-execution-lock.js +5 -4
  65. package/dist/lib/task/verification-engine.js +78 -14
  66. package/dist/lib/task/verification.js +1 -0
  67. package/dist/lifecycle-build-manifest.json +6 -0
  68. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
  69. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  70. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  71. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
  72. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  73. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  74. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
  75. package/lib/agent-clients/adapters/codex.ts +1 -1
  76. package/lib/defaults.json +10 -2
  77. package/lib/git/workflow.ts +25 -3
  78. package/lib/internal/codex-lifecycle.ts +175 -4
  79. package/lib/internal/codex-sandbox-controller.ts +109 -0
  80. package/lib/internal/release-workflow.ts +107 -11
  81. package/lib/internal/sandbox-control.ts +38 -6
  82. package/lib/internal/task-create.ts +16 -4
  83. package/lib/internal/task-orchestration.ts +34 -7
  84. package/lib/platform/context.ts +8 -5
  85. package/lib/platform/github-client.ts +61 -3
  86. package/lib/platform/issue-comments.ts +2 -1
  87. package/lib/process-data/checkpoint.ts +335 -0
  88. package/lib/process-data/index.ts +112 -60
  89. package/lib/process-data/materialize.ts +83 -0
  90. package/lib/process-data/normalize.ts +29 -1
  91. package/lib/process-data/reconcile.ts +64 -2
  92. package/lib/process-data/sources.ts +449 -2
  93. package/lib/process-data/store.ts +304 -11
  94. package/lib/process-data/types.ts +124 -2
  95. package/lib/release/workflow.ts +69 -20
  96. package/lib/sandbox/commands/create.ts +70 -25
  97. package/lib/sandbox/commands/prune.ts +53 -15
  98. package/lib/sandbox/commands/rm.ts +423 -108
  99. package/lib/sandbox/commands/start.ts +4 -3
  100. package/lib/sandbox/control/client.ts +152 -28
  101. package/lib/sandbox/control/container-identity.ts +93 -0
  102. package/lib/sandbox/control/executor.ts +167 -0
  103. package/lib/sandbox/control/lifecycle.ts +573 -0
  104. package/lib/sandbox/control/protocol.ts +121 -68
  105. package/lib/sandbox/control/server.ts +398 -109
  106. package/lib/sandbox/control/state.ts +213 -0
  107. package/lib/sandbox/control/timing.ts +12 -0
  108. package/lib/sandbox/index.ts +2 -2
  109. package/lib/sandbox/managed-fs.ts +17 -2
  110. package/lib/sandbox/mounts.ts +16 -8
  111. package/lib/sandbox/recovery.ts +335 -65
  112. package/lib/sandbox/workspace-view.ts +102 -11
  113. package/lib/sandbox/worktree-safety.ts +234 -0
  114. package/lib/server/daemon.ts +1 -1
  115. package/lib/server/process-control.ts +24 -3
  116. package/lib/server/process-state.ts +82 -27
  117. package/lib/task/artifact-lifecycle.ts +78 -14
  118. package/lib/task/artifact-receipts.ts +161 -0
  119. package/lib/task/codex-orchestration.ts +319 -0
  120. package/lib/task/commands/status.ts +2 -0
  121. package/lib/task/commands/validate.ts +282 -0
  122. package/lib/task/commit-finalization.ts +52 -9
  123. package/lib/task/create.ts +11 -2
  124. package/lib/task/delegation-receipts.ts +208 -7
  125. package/lib/task/events.ts +131 -5
  126. package/lib/task/finalization-artifacts.ts +2 -1
  127. package/lib/task/implementation-inputs.ts +1 -5
  128. package/lib/task/index.ts +8 -0
  129. package/lib/task/ledger.ts +16 -1
  130. package/lib/task/orchestration.ts +552 -18
  131. package/lib/task/sections.ts +6 -2
  132. package/lib/task/task-execution-lock.ts +11 -9
  133. package/lib/task/verification-engine.ts +93 -24
  134. package/lib/task/verification.ts +2 -1
  135. package/package.json +1 -1
  136. package/templates/.agents/README.en.md +7 -0
  137. package/templates/.agents/README.zh-CN.md +5 -0
  138. package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
  139. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  140. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  141. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  142. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  143. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  144. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  145. package/templates/.agents/rules/review-handshake.en.md +3 -1
  146. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  147. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  148. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  149. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  150. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  151. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  152. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  153. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  154. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  155. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
  156. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
  157. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  158. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  159. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
  160. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
  161. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  162. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  163. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
  164. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
  165. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  166. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  167. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  168. package/templates/.codex/hooks.json +8 -8
  169. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  170. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
  171. package/templates/.agents/workspace/README.en.md +0 -26
  172. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -12,7 +12,7 @@ import type { CommitAttempt } from './activity-log.ts';
12
12
  import { commitIntentPath, digest, readCommitIntent } from './commit-intent.ts';
13
13
  import type { CommitIntent } from './commit-intent.ts';
14
14
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
15
- import { parseReviewSummary } from './review-artifacts.ts';
15
+ import { artifactName, maxRound, parseReviewSummary } from './review-artifacts.ts';
16
16
  import {
17
17
  extractReviewBaseline,
18
18
  extractReviewedSnapshotTree,
@@ -43,6 +43,7 @@ type CommitFinalizationInspection = Readonly<{
43
43
  currentHead: string;
44
44
  committedHead: string | null;
45
45
  commitNote: string | null;
46
+ activityNote: string | null;
46
47
  needsAnchor: boolean;
47
48
  needsLog: boolean;
48
49
  intent: CommitIntent | null;
@@ -87,6 +88,28 @@ function isAncestor(repoRoot: string, ancestor: string, descendant: string): boo
87
88
  }).status === 0;
88
89
  }
89
90
 
91
+ function hasMatchingApprovedReview(
92
+ taskDir: string,
93
+ beforeRound: number,
94
+ baseline: string,
95
+ tree: string
96
+ ): boolean {
97
+ const entries = fs.readdirSync(taskDir);
98
+ for (let round = Math.min(beforeRound - 1, maxRound(entries, 'review-code')); round >= 1; round -= 1) {
99
+ const file = path.join(taskDir, artifactName('review-code', round));
100
+ if (!fs.existsSync(file)) continue;
101
+ const content = fs.readFileSync(file, 'utf8');
102
+ const summary = parseReviewSummary(content);
103
+ if (
104
+ summary.ok
105
+ && summary.summary.verdict === 'Approved'
106
+ && extractReviewBaseline(content) === baseline
107
+ && extractReviewedSnapshotTree(content) === tree
108
+ ) return true;
109
+ }
110
+ return false;
111
+ }
112
+
90
113
  function outcome(
91
114
  disposition: CommitFinalizationDisposition,
92
115
  overrides: Partial<CommitFinalizationInspection> = {}
@@ -98,6 +121,7 @@ function outcome(
98
121
  currentHead: '',
99
122
  committedHead: null,
100
123
  commitNote: null,
124
+ activityNote: null,
101
125
  needsAnchor: false,
102
126
  needsLog: false,
103
127
  intent: null,
@@ -212,6 +236,12 @@ function inspectCommitFinalization(
212
236
  });
213
237
  }
214
238
 
239
+ const commitNote = git(repoRoot, ['show', '-s', '--format=%h%x20%s', intent.committedHead]);
240
+ const matchingDone = commitRows.filter((row) => row.done !== '' && row.note === commitNote);
241
+ const pushOnlyRetry = intent.phase === 'pushed'
242
+ && intent.baselineHead === intent.committedHead
243
+ && matchingDone.length === 1
244
+ && openRows.length === 1;
215
245
  const review = findAuthoritativeReviewCodeArtifact(taskDir);
216
246
  let needsAnchor = false;
217
247
  if (review.ok && review.path) {
@@ -228,7 +258,16 @@ function inspectCommitFinalization(
228
258
  return invalid('authoritative review-code evidence is not a valid approved snapshot', currentHead, 'COMMIT_FINALIZATION_EVIDENCE_INVALID', shared);
229
259
  }
230
260
  const committedTree = git(repoRoot, ['rev-parse', `${intent.committedHead}^{tree}`]);
231
- if (reviewBaseline !== intent.baselineHead || reviewTree !== committedTree) {
261
+ const matchingCurrentReview = reviewBaseline === intent.baselineHead && reviewTree === committedTree;
262
+ if (
263
+ !matchingCurrentReview
264
+ && !hasMatchingApprovedReview(
265
+ taskDir,
266
+ review.round,
267
+ intent.baselineHead,
268
+ committedTree
269
+ )
270
+ ) {
232
271
  return outcome('conflict', {
233
272
  ...shared,
234
273
  code: 'COMMIT_FINALIZATION_CONFLICT',
@@ -237,18 +276,16 @@ function inspectCommitFinalization(
237
276
  }
238
277
 
239
278
  const anchor = String(metadata.last_reviewed_commit ?? '').trim();
240
- if (anchor && anchor !== intent.committedHead) {
279
+ if (anchor && anchor !== intent.committedHead && anchor !== intent.baselineHead) {
241
280
  return outcome('conflict', {
242
281
  ...shared,
243
282
  code: 'COMMIT_FINALIZATION_CONFLICT',
244
283
  message: 'task review anchor conflicts with the committed intent'
245
284
  });
246
285
  }
247
- needsAnchor = anchor === '';
286
+ needsAnchor = anchor !== intent.committedHead;
248
287
  }
249
- const commitNote = git(repoRoot, ['show', '-s', '--format=%h%x20%s', intent.committedHead]);
250
- const matchingDone = commitRows.filter((row) => row.done !== '' && row.note === commitNote);
251
- const needsLog = matchingDone.length === 0 && openRows.length === 1;
288
+ const needsLog = (matchingDone.length === 0 && openRows.length === 1) || pushOnlyRetry;
252
289
  if (!(needsLog || (matchingDone.length === 1 && openRows.length === 0))) {
253
290
  return outcome('conflict', {
254
291
  ...shared,
@@ -261,6 +298,7 @@ function inspectCommitFinalization(
261
298
  ...shared,
262
299
  message: 'commit finalization can be recovered',
263
300
  commitNote,
301
+ activityNote: pushOnlyRetry ? `Pushed ${commitNote}` : commitNote,
264
302
  needsAnchor,
265
303
  needsLog
266
304
  });
@@ -272,7 +310,12 @@ function planCommitTaskFinalization(
272
310
  agent: string,
273
311
  timestamp: string
274
312
  ): CommitTaskFinalizationPlan {
275
- if (inspection.disposition !== 'recoverable' || !inspection.committedHead || !inspection.commitNote) {
313
+ if (
314
+ inspection.disposition !== 'recoverable'
315
+ || !inspection.committedHead
316
+ || !inspection.commitNote
317
+ || !inspection.activityNote
318
+ ) {
276
319
  throw new Error('commit finalization task mutations require a recoverable inspection');
277
320
  }
278
321
  const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
@@ -293,7 +336,7 @@ function planCommitTaskFinalization(
293
336
  time: timestamp,
294
337
  step: 'Commit',
295
338
  agent,
296
- note: inspection.commitNote
339
+ note: inspection.activityNote
297
340
  })
298
341
  });
299
342
  }
@@ -231,6 +231,14 @@ function assertRealDirectory(directory: string, code: string): void {
231
231
  if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(code);
232
232
  }
233
233
 
234
+ function ensureRealDirectory(directory: string, code: string): void {
235
+ if (fs.existsSync(directory)) {
236
+ assertRealDirectory(directory, code);
237
+ return;
238
+ }
239
+ fs.mkdirSync(directory, { mode: 0o700 });
240
+ }
241
+
234
242
  function withCreateLock<T>(repoRoot: string, workspaceRoot: string, operation: () => T): T {
235
243
  const lockRoot = path.join(workspaceRoot, '.task-create.lock');
236
244
  fs.mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
@@ -289,8 +297,9 @@ function createLocalTask(value: unknown, options: LocalTaskCreateOptions): Local
289
297
  const repoRoot = fs.realpathSync.native(options.repoRoot);
290
298
  const workspaceRoot = path.join(repoRoot, '.agents', 'workspace');
291
299
  const activeRoot = path.join(workspaceRoot, 'active');
292
- assertRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
293
- assertRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
300
+ ensureRealDirectory(path.join(repoRoot, '.agents'), 'TASK_CREATE_WORKSPACE_INVALID');
301
+ ensureRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
302
+ ensureRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
294
303
  const project = readProject(repoRoot);
295
304
  const canonical = canonicalTaskCreateCandidate(candidate);
296
305
  const candidateDigest = sha256(canonical);
@@ -6,6 +6,43 @@ import { normalizeAgentToken } from '../agent-clients/tokens.ts';
6
6
  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
+ type DelegationHostEvidence = Readonly<{
10
+ kind: 'codex-lifecycle-v1' | 'codex-lifecycle-v2';
11
+ hookDefinitionHash: string;
12
+ startRevision: number;
13
+ stopRevision: number | null;
14
+ consumer: string | null;
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;
45
+ }>;
9
46
 
10
47
  type DelegationReceipt = Readonly<{
11
48
  id: string;
@@ -28,10 +65,19 @@ type DelegationReceipt = Readonly<{
28
65
  agent: string | null;
29
66
  status: DelegationStatus;
30
67
  workspaceSnapshotScope?: 'task';
68
+ lifecycleProvenance?: DelegationLifecycleProvenance | null;
69
+ hostEvidence?: DelegationHostEvidence | null;
31
70
  beforeFingerprint: string;
32
71
  afterFingerprint: string | null;
33
72
  changedPaths: readonly string[];
34
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;
35
81
  activatedAt: string | null;
36
82
  sealedAt: string | null;
37
83
  consumedAt: string | null;
@@ -55,9 +101,10 @@ function fail(code: string, message: string): ReceiptFailure {
55
101
  }
56
102
 
57
103
  function prepareDelegation(
58
- input: Omit<DelegationReceipt, 'id' | 'requestedModel' | 'requestedReasoningEffort' | 'actualModel' | 'actualReasoningEffort' | 'modelFallbackReason' | 'reasoningEffortFallbackReason' | 'parentId' | 'childId' | 'spawnMode' | 'agent' | 'status' | 'afterFingerprint' | 'changedPaths' | 'createdAt' | 'activatedAt' | 'sealedAt' | 'consumedAt'> & Readonly<{ requestedModel: string; requestedReasoningEffort: string }>,
59
- 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 } = {}
60
106
  ): DelegationReceipt {
107
+ const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))();
61
108
  return Object.freeze({
62
109
  ...input,
63
110
  id: (options.id ?? randomUUID)(),
@@ -70,15 +117,45 @@ function prepareDelegation(
70
117
  spawnMode: null,
71
118
  agent: null,
72
119
  status: 'prepared' as const,
120
+ hostEvidence: null,
73
121
  afterFingerprint: null,
74
122
  changedPaths: Object.freeze([]),
75
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,
76
131
  activatedAt: null,
77
132
  sealedAt: null,
78
133
  consumedAt: null
79
134
  });
80
135
  }
81
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
+
82
159
  function activateDelegation(
83
160
  receipt: DelegationReceipt,
84
161
  event: Readonly<{
@@ -90,12 +167,46 @@ function activateDelegation(
90
167
  actualReasoningEffort?: string;
91
168
  modelFallbackReason?: string;
92
169
  reasoningEffortFallbackReason?: string;
170
+ hostEvidence?: Readonly<{
171
+ kind: 'codex-lifecycle-v1' | 'codex-lifecycle-v2';
172
+ hookDefinitionHash: string;
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;
188
+ }>;
93
189
  }>,
94
- options: { now?: () => string } = {}
190
+ options: { now?: () => string; monotonicNow?: () => number } = {}
95
191
  ): ReceiptResult {
96
192
  const managedRole = managedDelegationRole(event.nativeAgent);
97
193
  if (!managedRole) return fail('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
98
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
+ }
99
210
  if (managedRole !== receipt.role) return fail('DELEGATION_ROLE_MISMATCH', `managed role ${managedRole} does not match ${receipt.role}`);
100
211
  if (!event.parentId || (receipt.parentId !== null && event.parentId !== receipt.parentId) || !event.childId || event.childId === event.parentId) {
101
212
  return fail('DELEGATION_IDENTITY_INVALID', 'native parent/child identity does not match the prepared delegation');
@@ -112,6 +223,10 @@ function activateDelegation(
112
223
  if (!actualReasoningEffort || actualReasoningEffort.trim() !== actualReasoningEffort) {
113
224
  return fail('DELEGATION_REASONING_EFFORT_MISSING', 'native start event must provide a non-empty actual reasoning effort');
114
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');
115
230
  if (
116
231
  actualReasoningEffort !== receipt.requestedReasoningEffort
117
232
  && (!event.reasoningEffortFallbackReason || event.reasoningEffortFallbackReason.trim() === '')
@@ -124,6 +239,42 @@ function activateDelegation(
124
239
  if (actualReasoningEffort === receipt.requestedReasoningEffort && event.reasoningEffortFallbackReason) {
125
240
  return fail('DELEGATION_REASONING_EFFORT_FALLBACK_INVALID', 'reasoning-effort fallback reason is only valid when actual effort differs');
126
241
  }
242
+ if (
243
+ event.hostEvidence
244
+ && (
245
+ receipt.client !== 'codex'
246
+ || !event.hostEvidence.hookDefinitionHash.trim()
247
+ || !Number.isSafeInteger(event.hostEvidence.startRevision)
248
+ || event.hostEvidence.startRevision < 1
249
+ )
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
+ }
127
278
  return { ok: true, receipt: Object.freeze({
128
279
  ...receipt,
129
280
  status: 'activated',
@@ -134,10 +285,28 @@ function activateDelegation(
134
285
  actualReasoningEffort,
135
286
  modelFallbackReason: event.modelFallbackReason ?? null,
136
287
  reasoningEffortFallbackReason: event.reasoningEffortFallbackReason ?? null,
137
- activatedAt: (options.now ?? (() => new Date().toISOString()))()
288
+ hostEvidence: event.hostEvidence ? Object.freeze({
289
+ ...event.hostEvidence,
290
+ stopRevision: null,
291
+ consumer: null,
292
+ consumedAt: null
293
+ }) : receipt.hostEvidence ?? null,
294
+ startEvidenceMonotonicMs: monotonic,
295
+ activatedMonotonicMs: (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))(),
296
+ activatedAt: new Date(wallNow).toISOString()
138
297
  }) };
139
298
  }
140
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
+
141
310
  function completeDelegationStage(
142
311
  receipt: DelegationReceipt,
143
312
  event: Readonly<{ stage: DelegationStage; round: number; artifact: string; agent: string }>
@@ -154,10 +323,19 @@ function completeDelegationStage(
154
323
 
155
324
  function sealDelegation(
156
325
  receipt: DelegationReceipt,
157
- event: Readonly<{ childId: string; exitCode: number; afterFingerprint: string; changedPaths: readonly string[] }>,
158
- options: { now?: () => string } = {}
326
+ event: Readonly<{
327
+ childId: string;
328
+ exitCode: number;
329
+ afterFingerprint: string;
330
+ changedPaths: readonly string[];
331
+ hostEvidence?: Readonly<{ stopRevision: number; consumer: string; consumedAt: string }>;
332
+ }>,
333
+ options: { now?: () => string; requireHostEvidence?: boolean } = {}
159
334
  ): ReceiptResult {
160
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
+ }
161
339
  if (event.childId !== receipt.childId || event.exitCode !== 0) return fail('DELEGATION_STOP_INVALID', 'native stop identity or exit status is invalid');
162
340
  if (receipt.role === 'reviewer') {
163
341
  const taskRoot = `.agents/workspace/active/${receipt.taskId}/`;
@@ -169,11 +347,24 @@ function sealDelegation(
169
347
  const disallowed = event.changedPaths.find((entry) => !allowed.has(entry));
170
348
  if (disallowed) return fail('DELEGATION_REVIEWER_WRITE_FORBIDDEN', `reviewer changed forbidden path '${disallowed}'`);
171
349
  }
350
+ if (event.hostEvidence) {
351
+ if (
352
+ !receipt.hostEvidence
353
+ || !Number.isSafeInteger(event.hostEvidence.stopRevision)
354
+ || event.hostEvidence.stopRevision <= receipt.hostEvidence.startRevision
355
+ || event.hostEvidence.consumer !== receipt.id
356
+ || !event.hostEvidence.consumedAt.trim()
357
+ ) return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex stop evidence reference is invalid');
358
+ }
172
359
  return { ok: true, receipt: Object.freeze({
173
360
  ...receipt,
174
361
  status: 'sealed',
175
362
  afterFingerprint: event.afterFingerprint,
176
363
  changedPaths: Object.freeze([...event.changedPaths]),
364
+ hostEvidence: event.hostEvidence && receipt.hostEvidence ? Object.freeze({
365
+ ...receipt.hostEvidence,
366
+ ...event.hostEvidence
367
+ }) : receipt.hostEvidence ?? null,
177
368
  sealedAt: (options.now ?? (() => new Date().toISOString()))()
178
369
  }) };
179
370
  }
@@ -190,10 +381,20 @@ function consumeDelegation(receipt: DelegationReceipt, options: { now?: () => st
190
381
 
191
382
  export {
192
383
  activateDelegation,
384
+ abortPreparedDelegation,
193
385
  completeDelegationStage,
194
386
  consumeDelegation,
387
+ dispatchDelegation,
195
388
  managedDelegationRole,
196
389
  prepareDelegation,
197
390
  sealDelegation
198
391
  };
199
- export type { DelegationReceipt, DelegationRole, DelegationStage, DelegationStatus, ReceiptResult };
392
+ export type {
393
+ DelegationHostEvidence,
394
+ DelegationLifecycleProvenance,
395
+ DelegationReceipt,
396
+ DelegationRole,
397
+ DelegationStage,
398
+ DelegationStatus,
399
+ ReceiptResult
400
+ };