@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
@@ -151,7 +151,7 @@ function checkOrchestrationState({ taskDir }) {
151
151
  if (isFailClosedLegacyPause(run)) {
152
152
  return passResult('orchestration-state', 'Legacy orchestration run is safely paused');
153
153
  }
154
- if (run.schemaVersion !== 2 || !['paused', 'completed'].includes(run.status)) {
154
+ if (![2, 3].includes(run.schemaVersion) || !['paused', 'completed'].includes(run.status)) {
155
155
  return failResult('orchestration-state', `Expected paused or completed run, received '${run.status}'`);
156
156
  }
157
157
  if (run.status === 'paused' && (!run.pause?.code || !run.pause?.message)) {
@@ -239,12 +239,29 @@ function checkOrchestrationEvidence({ taskDir }) {
239
239
  return failResult('orchestration-evidence', 'Run model policy source or recovery history is invalid');
240
240
  }
241
241
  for (const recovery of run.recoveryHistory) {
242
- if (recovery.code !== 'MODEL_POLICY_SUPPLEMENTED'
243
- || recovery.previousSchemaVersion !== 1
244
- || recovery.receiptCount !== 0
245
- || recovery.pendingDelegation !== false
246
- || !exactText(recovery.recoveredAt)
247
- || !recovery.policySource) {
242
+ const validModelRecovery = recovery.code === 'MODEL_POLICY_SUPPLEMENTED'
243
+ && recovery.previousSchemaVersion === 1
244
+ && recovery.receiptCount === 0
245
+ && recovery.pendingDelegation === false
246
+ && exactText(recovery.recoveredAt)
247
+ && recovery.policySource;
248
+ const guards = recovery.guards;
249
+ const validCapabilityRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
250
+ && recovery.previousSchemaVersion === 2
251
+ && recovery.previousStatus === 'paused'
252
+ && recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
253
+ && recovery.client === 'codex'
254
+ && recovery.resultingStatus === 'running'
255
+ && exactText(recovery.recoveredAt)
256
+ && guards?.stepCount === 0
257
+ && guards?.nextStage === null
258
+ && guards?.baselineEmpty === true
259
+ && guards?.receiptCount === 0
260
+ && guards?.pendingDelegation === false
261
+ && guards?.commitAuthorizationUnused === true
262
+ && guards?.completionEvidenceAbsent === true
263
+ && guards?.commitIntentAbsent === true;
264
+ if (!validModelRecovery && !validCapabilityRecovery) {
248
265
  return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
249
266
  }
250
267
  }
@@ -298,6 +315,21 @@ function checkOrchestrationEvidence({ taskDir }) {
298
315
  && receipt.reasoningEffortFallbackReason !== null) {
299
316
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
300
317
  }
318
+ if (receipt.client === 'codex') {
319
+ const host = receipt.hostEvidence;
320
+ if (host?.kind !== 'codex-lifecycle-v1'
321
+ || !exactText(host.hookDefinitionHash)
322
+ || !Number.isSafeInteger(host.startRevision)
323
+ || host.startRevision < 1) {
324
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex start evidence`);
325
+ }
326
+ if (['sealed', 'consumed'].includes(receipt.status) && (!Number.isSafeInteger(host.stopRevision)
327
+ || host.stopRevision <= host.startRevision
328
+ || host.consumer !== receipt.id
329
+ || !exactText(host.consumedAt))) {
330
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex consumed stop evidence`);
331
+ }
332
+ }
301
333
  }
302
334
  }
303
335
  if (run.receipts.some((receipt) => receipt.status !== 'consumed')) {
@@ -884,7 +916,13 @@ function checkPostReviewCommit({ taskDir, config }) {
884
916
  return blockedResult("post-review-commit", relation.message, relation.code);
885
917
  }
886
918
  if (relation.status === "strict") {
887
- return failResult("post-review-commit", "Merged PR evidence did not establish squash merge equivalence", "PR_MERGE_IDENTITY_INVALID");
919
+ return resolvePostReviewIdentityFailure(task.content, {
920
+ code: "PR_MERGE_IDENTITY_INVALID",
921
+ message: "Merged PR evidence did not establish squash merge equivalence"
922
+ });
923
+ }
924
+ if (relation.code === "PR_MERGE_IDENTITY_INVALID") {
925
+ return resolvePostReviewIdentityFailure(task.content, relation);
888
926
  }
889
927
  return failResult("post-review-commit", relation.message, relation.code);
890
928
  }
@@ -949,21 +987,47 @@ function checkPostReviewCommit({ taskDir, config }) {
949
987
  }
950
988
  const exemption = resolvePostReviewExemption(task.content);
951
989
  if (!exemption.ok)
952
- return failResult("post-review-commit", exemption.message);
953
- if (exemption.exempt) {
954
- return passResult("post-review-commit", `${commits.length} post-review commit(s) covered by a human-decided exemption`);
990
+ return failResult("post-review-commit", exemption.message, exemption.code);
991
+ if (exemption.exemptions.length > 0) {
992
+ return passResult("post-review-commit", `${commits.length} post-review commit(s) covered by a human-decided exemption: ${formatPostReviewExemptions(exemption.exemptions)}`);
955
993
  }
956
994
  return failResult("post-review-commit", `${commits.length} commit(s) to code/rule paths after reviewed commit ${sha.slice(0, 8)}; re-run review-code or record a human-decided exemption`);
957
995
  }
996
+ function resolvePostReviewIdentityFailure(content, failure) {
997
+ const exemption = resolvePostReviewExemption(content);
998
+ if (!exemption.ok) {
999
+ return failResult("post-review-commit", exemption.message, exemption.code);
1000
+ }
1001
+ if (exemption.exemptions.length === 0) {
1002
+ return failResult("post-review-commit", failure.message, failure.code);
1003
+ }
1004
+ return passResult("post-review-commit", `Human-decided post-review exemption overrode ${failure.code}: ${failure.message}; ${formatPostReviewExemptions(exemption.exemptions)}`);
1005
+ }
1006
+ function formatPostReviewExemptions(exemptions) {
1007
+ return exemptions.map((row) => `${row.id}: ${row.evidence}`).join("; ");
1008
+ }
958
1009
  function resolvePostReviewExemption(content) {
959
1010
  try {
1011
+ const rows = parseLedger(content);
1012
+ const invalid = validateLedgerRows(rows);
1013
+ if (invalid) {
1014
+ return {
1015
+ ok: false,
1016
+ code: "POST_REVIEW_EXEMPTION_INVALID",
1017
+ message: `${invalid.code}: ${invalid.message}`
1018
+ };
1019
+ }
960
1020
  return {
961
1021
  ok: true,
962
- exempt: parseLedger(content).some((row) => row.stage === POST_REVIEW_COMMIT_STAGE && row.status === "human-decided")
1022
+ exemptions: rows.filter((row) => row.stage === POST_REVIEW_COMMIT_STAGE)
963
1023
  };
964
1024
  }
965
- catch {
966
- return { ok: false, message: "Invalid disagreement ledger" };
1025
+ catch (error) {
1026
+ return {
1027
+ ok: false,
1028
+ code: "POST_REVIEW_EXEMPTION_INVALID",
1029
+ message: `Invalid disagreement ledger: ${error instanceof Error ? error.message : String(error)}`
1030
+ };
967
1031
  }
968
1032
  }
969
1033
  function checkReviewFact({ taskDir, artifactFile }) {
@@ -16,6 +16,7 @@ const VERIFICATION_CATALOG = {
16
16
  'code.completed': gate('code-task', 'active', 'code'),
17
17
  'review-code.completed': gate('review-code', 'active', 'review-code'),
18
18
  'manual-validation.completed': gate('complete-manual-validation', 'active', 'manual-validation'),
19
+ 'validation-run.completed': gate('run-manual-validation', 'active', 'validation-run'),
19
20
  'block-task.completed': gate('block-task', 'blocked'),
20
21
  'cancel-task.completed': gate('cancel-task', 'completed'),
21
22
  'commit.completed': gate('commit', 'active'),
@@ -0,0 +1,6 @@
1
+ {
2
+ "protocolVersion": 3,
3
+ "packageVersion": "0.9.7",
4
+ "sourceInputHash": "ee12d1ec7a586eeaba04deb4253ccd7f5d16552915ca141e493248c0728459a9",
5
+ "internalExecutableBuildHash": "0eceb914e6c1cb9937b86d34bfbf933d1010f499b9976813556146d2e6fa0132"
6
+ }
@@ -19,6 +19,12 @@ type DiscoveredHook = Readonly<{
19
19
  matcher: string | null;
20
20
  command: string;
21
21
  enabled: boolean;
22
+ source: string;
23
+ sourcePathDigest: string;
24
+ trustStatus: string;
25
+ currentHash: string;
26
+ isManaged: boolean;
27
+ pluginId: string | null;
22
28
  }>;
23
29
 
24
30
  type AppServerTransportOptions = Readonly<{
@@ -27,6 +33,8 @@ type AppServerTransportOptions = Readonly<{
27
33
  cwd?: string;
28
34
  env?: NodeJS.ProcessEnv;
29
35
  timeoutMs?: number;
36
+ rolloutReadAttempts?: number;
37
+ rolloutRetryMs?: number;
30
38
  }>;
31
39
 
32
40
  type CodexRuntimeIdentity = Readonly<{
@@ -37,10 +45,12 @@ type CodexRuntimeIdentity = Readonly<{
37
45
 
38
46
  const LIFECYCLE_HOOKS = Object.freeze([
39
47
  Object.freeze({ event: 'PreToolUse', matcher: '^collaborationspawn_agent$', phase: 'pre-tool' }),
40
- Object.freeze({ event: 'PostToolUse', matcher: '^collaborationspawn_agent$', phase: 'post-tool' }),
41
- Object.freeze({ event: 'SubagentStart', matcher: '^agent-infra-lifecycle-(executor|reviewer)$', phase: 'subagent-start' }),
42
- Object.freeze({ event: 'SubagentStop', matcher: '^agent-infra-lifecycle-(executor|reviewer)$', phase: 'subagent-stop' })
48
+ Object.freeze({ event: 'PostToolUse', matcher: '', phase: 'post-tool' }),
49
+ Object.freeze({ event: 'SubagentStart', matcher: '', phase: 'subagent-start' }),
50
+ Object.freeze({ event: 'SubagentStop', matcher: '', phase: 'subagent-stop' })
43
51
  ]);
52
+ const DEFAULT_ROLLOUT_READ_ATTEMPTS = 81;
53
+ const DEFAULT_ROLLOUT_RETRY_MS = 100;
44
54
 
45
55
  function object(value: unknown): JsonObject | null {
46
56
  return value && typeof value === 'object' && !Array.isArray(value)
@@ -52,6 +62,65 @@ function nonEmpty(value: unknown): string | null {
52
62
  return typeof value === 'string' && value.trim() ? value : null;
53
63
  }
54
64
 
65
+ function resolveCodexSpawnedChild(
66
+ transcriptPath: string,
67
+ expected: Readonly<{
68
+ sessionId: string;
69
+ toolUseId: string;
70
+ nativeAgent: string;
71
+ taskName: string;
72
+ requestedModel?: string;
73
+ requestedReasoningEffort?: string;
74
+ }>
75
+ ): string {
76
+ const name = path.basename(transcriptPath);
77
+ if (!transcriptPath || (!name.endsWith(`-${expected.sessionId}.jsonl`) && name !== `${expected.sessionId}.jsonl`)) {
78
+ throw new Error('Codex parent rollout path is invalid');
79
+ }
80
+ const stat = fs.lstatSync(transcriptPath);
81
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('Codex parent rollout is not a regular file');
82
+ const maxBytes = 8 * 1024 * 1024;
83
+ const offset = Math.max(0, stat.size - maxBytes);
84
+ const buffer = Buffer.alloc(stat.size - offset);
85
+ const descriptor = fs.openSync(transcriptPath, 'r');
86
+ try {
87
+ fs.readSync(descriptor, buffer, 0, buffer.length, offset);
88
+ } finally {
89
+ fs.closeSync(descriptor);
90
+ }
91
+ let text = buffer.toString('utf8');
92
+ if (offset) text = text.slice(text.indexOf('\n') + 1);
93
+ const records = text.split(/\r?\n/).filter(Boolean).map((line) => object(JSON.parse(line))).filter(Boolean);
94
+ const calls = records.filter((record) => {
95
+ const payload = object(record?.payload);
96
+ return record?.type === 'response_item'
97
+ && payload?.type === 'function_call'
98
+ && payload.call_id === expected.toolUseId
99
+ && payload.namespace === 'collaboration'
100
+ && payload.name === 'spawn_agent';
101
+ });
102
+ if (calls.length !== 1) throw new Error('Codex parent rollout spawn call was not found uniquely');
103
+ const call = object(calls[0]?.payload);
104
+ const args = object(JSON.parse(nonEmpty(call?.arguments) ?? '{}'));
105
+ if (
106
+ args?.agent_type !== expected.nativeAgent
107
+ || args.task_name !== expected.taskName
108
+ || (expected.requestedModel && args.model !== expected.requestedModel)
109
+ || (expected.requestedReasoningEffort && args.reasoning_effort !== expected.requestedReasoningEffort)
110
+ ) throw new Error('Codex parent rollout spawn identity does not match the hook event');
111
+ const activities = records.filter((record) => {
112
+ const payload = object(record?.payload);
113
+ return record?.type === 'event_msg'
114
+ && payload?.type === 'sub_agent_activity'
115
+ && payload.event_id === expected.toolUseId
116
+ && payload.kind === 'started'
117
+ && payload.agent_path === `/root/${expected.taskName}`
118
+ && nonEmpty(payload.agent_thread_id);
119
+ });
120
+ if (activities.length !== 1) throw new Error('Codex parent rollout child activity was not found uniquely');
121
+ return nonEmpty(object(activities[0]?.payload)?.agent_thread_id)!;
122
+ }
123
+
55
124
  function validateCodexLifecycleHookConfig(value: unknown): void {
56
125
  const hooks = object(object(value)?.hooks);
57
126
  const valid = hooks && LIFECYCLE_HOOKS.every(({ event, matcher, phase }) => {
@@ -63,7 +132,7 @@ function validateCodexLifecycleHookConfig(value: unknown): void {
63
132
  return entry.hooks.some((hookValue) => {
64
133
  const hook = object(hookValue);
65
134
  return hook?.type === 'command'
66
- && hook.timeout === 5
135
+ && hook.timeout === 15
67
136
  && hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`;
68
137
  });
69
138
  });
@@ -71,7 +140,11 @@ function validateCodexLifecycleHookConfig(value: unknown): void {
71
140
  if (!valid) throw new Error('CODEX_PREFLIGHT_HOOKS_INVALID: lifecycle hooks are invalid');
72
141
  }
73
142
 
74
- function parseCodexHooksList(value: unknown, repoRoot: string): readonly DiscoveredHook[] {
143
+ function parseCodexHooksList(
144
+ value: unknown,
145
+ repoRoot: string,
146
+ expectedSource: 'direct-host' | 'isolated-user' = 'direct-host'
147
+ ): readonly DiscoveredHook[] {
75
148
  const root = object(value);
76
149
  const entries = Array.isArray(root?.data) ? root.data : [];
77
150
  const entry = entries.map(object).find((candidate) => candidate?.cwd === repoRoot);
@@ -84,13 +157,28 @@ function parseCodexHooksList(value: unknown, repoRoot: string): readonly Discove
84
157
  eventName: String(hook.eventName ?? ''),
85
158
  matcher: typeof hook.matcher === 'string' ? hook.matcher : null,
86
159
  command: String(hook.command ?? ''),
87
- enabled: hook.enabled === true
160
+ enabled: hook.enabled === true,
161
+ source: String(hook.source ?? ''),
162
+ sourcePathDigest: typeof hook.sourcePath === 'string' && path.isAbsolute(hook.sourcePath)
163
+ ? crypto.createHash('sha256').update(hook.sourcePath).digest('hex')
164
+ : '',
165
+ trustStatus: String(hook.trustStatus ?? ''),
166
+ currentHash: String(hook.currentHash ?? ''),
167
+ isManaged: hook.isManaged === true,
168
+ pluginId: typeof hook.pluginId === 'string' ? hook.pluginId : null
88
169
  }));
89
170
  const valid = LIFECYCLE_HOOKS.every(({ event, matcher, phase }) => hooks.some((hook) =>
90
171
  hook.eventName === event.replace(/^./, (character) => character.toLowerCase())
91
172
  && hook.matcher === matcher
92
173
  && hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`
93
174
  && hook.enabled
175
+ && hook.currentHash.length > 0
176
+ && hook.sourcePathDigest.length === 64
177
+ && hook.pluginId === null
178
+ && (expectedSource === 'isolated-user'
179
+ ? hook.source === 'user' && hook.trustStatus !== 'modified'
180
+ : (hook.source === 'project' && hook.trustStatus === 'trusted')
181
+ || hook.isManaged && hook.trustStatus === 'managed')
94
182
  ));
95
183
  if (!valid) throw new Error('CODEX_PREFLIGHT_HOOKS_NOT_LOADED: lifecycle hooks are not loaded for this workspace');
96
184
  return Object.freeze(hooks);
@@ -120,26 +208,38 @@ function hasCodexRuntimeLiveness(
120
208
  });
121
209
  }
122
210
 
123
- function parseCodexThreadResolution(readValue: unknown, resumeValue: unknown): ThreadResolution {
211
+ function parseCodexThreadResolution(readValue: unknown, rolloutRecords: readonly unknown[]): ThreadResolution {
124
212
  const readRoot = object(readValue);
125
- const resumeRoot = object(resumeValue);
126
213
  const thread = object(readRoot?.thread);
127
- const resumedThread = object(resumeRoot?.thread);
128
214
  const source = object(thread?.source);
129
215
  const subAgent = object(source?.subAgent);
130
216
  const threadSpawn = object(subAgent?.thread_spawn);
217
+ const sessionMeta = rolloutRecords
218
+ .map(object)
219
+ .find((record) => record?.type === 'session_meta');
220
+ const sessionPayload = object(sessionMeta?.payload);
221
+ const turnContext = rolloutRecords
222
+ .map(object)
223
+ .filter((record) => record?.type === 'turn_context')
224
+ .at(-1);
225
+ const turnPayload = object(turnContext?.payload);
131
226
  const childThreadId = nonEmpty(thread?.id);
132
227
  const parentThreadId = nonEmpty(thread?.parentThreadId);
133
228
  const sourceParentThreadId = nonEmpty(threadSpawn?.parent_thread_id);
134
- const resumedChildThreadId = nonEmpty(resumedThread?.id);
135
- const model = nonEmpty(resumeRoot?.model);
136
- const reasoningEffort = nonEmpty(resumeRoot?.reasoningEffort ?? object(resumeRoot?.settings)?.effort);
229
+ const rolloutChildThreadId = nonEmpty(sessionPayload?.id);
230
+ const rolloutParentThreadId = nonEmpty(sessionPayload?.parent_thread_id);
231
+ const agentRole = nonEmpty(sessionPayload?.agent_role);
232
+ const model = nonEmpty(turnPayload?.model);
233
+ const reasoningEffort = nonEmpty(turnPayload?.effort);
137
234
  const forkedFromId = thread?.forkedFromId;
138
235
  if (
139
236
  !childThreadId
140
- || resumedChildThreadId !== childThreadId
237
+ || rolloutChildThreadId !== childThreadId
141
238
  || !parentThreadId
142
239
  || !sourceParentThreadId
240
+ || rolloutParentThreadId !== parentThreadId
241
+ || rolloutParentThreadId !== sourceParentThreadId
242
+ || !/^agent-infra-lifecycle-(executor|reviewer)$/.test(agentRole ?? '')
143
243
  || !model
144
244
  || !reasoningEffort
145
245
  || (forkedFromId !== null && typeof forkedFromId !== 'string')
@@ -152,7 +252,8 @@ function parseCodexThreadResolution(readValue: unknown, resumeValue: unknown): T
152
252
  childThreadId,
153
253
  parentThreadId,
154
254
  forkedFromId,
155
- sourceParentThreadId
255
+ sourceParentThreadId,
256
+ nativeAgent: agentRole!
156
257
  }),
157
258
  settings: Object.freeze({
158
259
  type: 'app-settings',
@@ -163,6 +264,49 @@ function parseCodexThreadResolution(readValue: unknown, resumeValue: unknown): T
163
264
  });
164
265
  }
165
266
 
267
+ function readCodexRolloutRecords(readValue: unknown, childThreadId: string): readonly unknown[] {
268
+ const thread = object(object(readValue)?.thread);
269
+ const rolloutPath = nonEmpty(thread?.path);
270
+ const rolloutName = rolloutPath ? path.basename(rolloutPath) : '';
271
+ if (!rolloutPath || (rolloutName !== `${childThreadId}.jsonl` && !rolloutName.endsWith(`-${childThreadId}.jsonl`))) {
272
+ throw new Error('Codex App Server rollout path is invalid');
273
+ }
274
+ const stat = fs.statSync(rolloutPath);
275
+ if (!stat.isFile()) throw new Error('Codex App Server rollout path is not a regular file');
276
+ const maxBytes = 1024 * 1024;
277
+ const bytes = Math.min(stat.size, maxBytes);
278
+ const descriptor = fs.openSync(rolloutPath, 'r');
279
+ try {
280
+ const buffer = Buffer.alloc(bytes);
281
+ fs.readSync(descriptor, buffer, 0, bytes, 0);
282
+ const text = buffer.toString('utf8');
283
+ const completeText = stat.size <= maxBytes ? text : text.slice(0, text.lastIndexOf('\n'));
284
+ return Object.freeze(completeText.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line)));
285
+ } finally {
286
+ fs.closeSync(descriptor);
287
+ }
288
+ }
289
+
290
+ async function resolveCodexRolloutRecords(
291
+ readValue: unknown,
292
+ childThreadId: string,
293
+ attempts: number,
294
+ retryMs: number
295
+ ): Promise<readonly unknown[]> {
296
+ let lastError: unknown;
297
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
298
+ try {
299
+ const records = readCodexRolloutRecords(readValue, childThreadId);
300
+ parseCodexThreadResolution(readValue, records);
301
+ return records;
302
+ } catch (error) {
303
+ lastError = error;
304
+ if (attempt + 1 < attempts) await new Promise((resolve) => setTimeout(resolve, retryMs));
305
+ }
306
+ }
307
+ throw lastError;
308
+ }
309
+
166
310
  function parseCodexTurnCompleted(value: unknown): Extract<CodexLifecycleEvent, { type: 'app-terminal' }> {
167
311
  const root = object(value);
168
312
  const turn = object(root?.turn);
@@ -333,12 +477,17 @@ async function resolveCodexThread(
333
477
  const readResult = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: false }));
334
478
  const readThread = object(readResult?.thread);
335
479
  if (nonEmpty(readThread?.id) !== childThreadId) throw new Error('Codex App Server returned the wrong child thread');
336
- const resumeResult = await transport.request('thread/resume', { threadId: childThreadId });
337
480
  let resolution: ThreadResolution;
338
481
  try {
339
- resolution = parseCodexThreadResolution(readResult, resumeResult);
482
+ const rolloutRecords = await resolveCodexRolloutRecords(
483
+ readResult,
484
+ childThreadId,
485
+ options.rolloutReadAttempts ?? DEFAULT_ROLLOUT_READ_ATTEMPTS,
486
+ options.rolloutRetryMs ?? DEFAULT_ROLLOUT_RETRY_MS
487
+ );
488
+ resolution = parseCodexThreadResolution(readResult, rolloutRecords);
340
489
  } catch {
341
- throw new Error('Codex App Server resumed settings are unavailable');
490
+ throw new Error('Codex App Server rollout metadata is unavailable');
342
491
  }
343
492
  const reroutes = transport.notifications
344
493
  .filter((entry) => entry.method === 'model/rerouted')
@@ -368,6 +517,8 @@ async function resolveCodexTerminal(
368
517
  if (nonEmpty(thread?.id) !== childThreadId) throw new Error('Codex App Server returned the wrong child thread');
369
518
  const turns = Array.isArray(thread?.turns) ? thread.turns : [];
370
519
  const turn = turns.at(-1);
520
+ const turnValue = object(turn);
521
+ if (!turnValue || turnValue.status === 'inProgress') throw new Error('CODEX_TURN_NOT_TERMINAL');
371
522
  return parseCodexTurnCompleted({ threadId: childThreadId, turn });
372
523
  } finally {
373
524
  transport.close();
@@ -415,13 +566,45 @@ async function preflightCodexLifecycleEvidence(
415
566
  let discoveredHooks: readonly DiscoveredHook[];
416
567
  try {
417
568
  await transport.initialize();
418
- discoveredHooks = parseCodexHooksList(await transport.request('hooks/list', { cwds: [repoRoot] }), repoRoot);
569
+ discoveredHooks = parseCodexHooksList(
570
+ await transport.request('hooks/list', { cwds: [repoRoot] }),
571
+ repoRoot,
572
+ process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT ? 'isolated-user' : 'direct-host'
573
+ );
419
574
  } finally { transport.close(); }
575
+ const lifecycleHooks = LIFECYCLE_HOOKS.map(({ event, matcher, phase }) => discoveredHooks.filter((hook) =>
576
+ hook.eventName === event.replace(/^./, (character) => character.toLowerCase())
577
+ && hook.matcher === matcher
578
+ && hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`
579
+ ));
580
+ if (lifecycleHooks.some((matches) => matches.length !== 1)) {
581
+ throw new Error('CODEX_PREFLIGHT_HOOK_SOURCE_AMBIGUOUS: lifecycle hook source is not unique');
582
+ }
583
+ const matchedHooks = lifecycleHooks.flat();
584
+ const sources = new Set(matchedHooks.map((hook) => hook.isManaged ? 'managed' : hook.source));
585
+ const sourcePathDigests = new Set(matchedHooks.map((hook) => hook.sourcePathDigest));
586
+ if (sources.size !== 1 || sourcePathDigests.size !== 1) {
587
+ throw new Error('CODEX_PREFLIGHT_HOOK_SOURCE_AMBIGUOUS: lifecycle hooks do not share one exact source');
588
+ }
589
+ const source = [...sources][0];
590
+ const hookSource = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT
591
+ ? 'isolated-user'
592
+ : source === 'managed' ? 'managed' : 'project';
420
593
  return Object.freeze({
421
594
  cliVersion: match[1],
422
595
  hookDefinitionHash,
423
596
  staticReady: true,
424
597
  discoveredHooks,
598
+ hookProvenance: Object.freeze({
599
+ hookSource,
600
+ hookSourcePathDigest: [...sourcePathDigests][0]!,
601
+ hookSourceHash: crypto.createHash('sha256').update(JSON.stringify(matchedHooks.map((hook) => ({
602
+ eventName: hook.eventName,
603
+ matcher: hook.matcher,
604
+ command: hook.command,
605
+ currentHash: hook.currentHash
606
+ })))).digest('hex')
607
+ }),
425
608
  runtimeLiveness,
426
609
  diagnostics: transport.diagnostics
427
610
  });
@@ -434,6 +617,7 @@ export {
434
617
  parseCodexHooksList,
435
618
  parseCodexThreadResolution,
436
619
  parseCodexTurnCompleted,
620
+ resolveCodexSpawnedChild,
437
621
  preflightCodexLifecycleEvidence,
438
622
  resolveCodexTerminal,
439
623
  resolveCodexThread,