@fitlab-ai/agent-infra 0.9.5 → 0.9.6

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 (106) hide show
  1. package/dist/bin/internal-cli.js +3 -3
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +126 -16
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +48 -4
  5. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  6. package/dist/lib/git/workflow.js +0 -1
  7. package/dist/lib/internal/codex-lifecycle.js +100 -2
  8. package/dist/lib/internal/release-workflow.js +108 -11
  9. package/dist/lib/internal/sandbox-control.js +43 -6
  10. package/dist/lib/internal/task-create.js +12 -4
  11. package/dist/lib/internal/task-orchestration.js +5 -4
  12. package/dist/lib/platform/issue-comments.js +2 -1
  13. package/dist/lib/release/workflow.js +29 -21
  14. package/dist/lib/sandbox/commands/create.js +15 -16
  15. package/dist/lib/sandbox/commands/prune.js +40 -16
  16. package/dist/lib/sandbox/commands/rm.js +318 -89
  17. package/dist/lib/sandbox/control/client.js +138 -22
  18. package/dist/lib/sandbox/control/executor.js +136 -0
  19. package/dist/lib/sandbox/control/lifecycle.js +348 -0
  20. package/dist/lib/sandbox/control/protocol.js +62 -26
  21. package/dist/lib/sandbox/control/server.js +222 -102
  22. package/dist/lib/sandbox/control/state.js +181 -0
  23. package/dist/lib/sandbox/index.js +2 -2
  24. package/dist/lib/sandbox/managed-fs.js +14 -2
  25. package/dist/lib/sandbox/mounts.js +35 -24
  26. package/dist/lib/sandbox/recovery.js +262 -45
  27. package/dist/lib/sandbox/workspace-view.js +73 -10
  28. package/dist/lib/sandbox/worktree-safety.js +196 -0
  29. package/dist/lib/server/process-control.js +6 -0
  30. package/dist/lib/task/artifact-lifecycle.js +5 -4
  31. package/dist/lib/task/codex-orchestration.js +161 -0
  32. package/dist/lib/task/commands/status.js +3 -0
  33. package/dist/lib/task/commands/validate.js +289 -0
  34. package/dist/lib/task/commit-finalization.js +36 -9
  35. package/dist/lib/task/delegation-receipts.js +25 -0
  36. package/dist/lib/task/events.js +10 -4
  37. package/dist/lib/task/finalization-artifacts.js +2 -1
  38. package/dist/lib/task/implementation-inputs.js +1 -7
  39. package/dist/lib/task/index.js +9 -0
  40. package/dist/lib/task/orchestration.js +133 -4
  41. package/dist/lib/task/verification-engine.js +38 -6
  42. package/dist/lib/task/verification.js +1 -0
  43. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +142 -15
  44. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
  45. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +51 -3
  46. package/lib/agent-clients/adapters/codex.ts +1 -1
  47. package/lib/git/workflow.ts +0 -1
  48. package/lib/internal/codex-lifecycle.ts +107 -3
  49. package/lib/internal/release-workflow.ts +107 -11
  50. package/lib/internal/sandbox-control.ts +38 -6
  51. package/lib/internal/task-create.ts +16 -4
  52. package/lib/internal/task-orchestration.ts +4 -4
  53. package/lib/platform/issue-comments.ts +2 -1
  54. package/lib/release/workflow.ts +69 -20
  55. package/lib/sandbox/commands/create.ts +17 -21
  56. package/lib/sandbox/commands/prune.ts +53 -15
  57. package/lib/sandbox/commands/rm.ts +368 -103
  58. package/lib/sandbox/control/client.ts +152 -28
  59. package/lib/sandbox/control/executor.ts +163 -0
  60. package/lib/sandbox/control/lifecycle.ts +342 -0
  61. package/lib/sandbox/control/protocol.ts +96 -69
  62. package/lib/sandbox/control/server.ts +239 -110
  63. package/lib/sandbox/control/state.ts +196 -0
  64. package/lib/sandbox/index.ts +2 -2
  65. package/lib/sandbox/managed-fs.ts +17 -2
  66. package/lib/sandbox/mounts.ts +31 -13
  67. package/lib/sandbox/recovery.ts +301 -60
  68. package/lib/sandbox/workspace-view.ts +81 -11
  69. package/lib/sandbox/worktree-safety.ts +234 -0
  70. package/lib/server/process-control.ts +11 -0
  71. package/lib/task/artifact-lifecycle.ts +5 -4
  72. package/lib/task/codex-orchestration.ts +233 -0
  73. package/lib/task/commands/status.ts +2 -0
  74. package/lib/task/commands/validate.ts +282 -0
  75. package/lib/task/commit-finalization.ts +52 -9
  76. package/lib/task/delegation-receipts.ts +52 -3
  77. package/lib/task/events.ts +9 -4
  78. package/lib/task/finalization-artifacts.ts +2 -1
  79. package/lib/task/implementation-inputs.ts +1 -5
  80. package/lib/task/index.ts +8 -0
  81. package/lib/task/orchestration.ts +199 -4
  82. package/lib/task/verification-engine.ts +42 -8
  83. package/lib/task/verification.ts +2 -1
  84. package/package.json +1 -1
  85. package/templates/.agents/hooks/lifecycle-delegation.js +48 -12
  86. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  87. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  88. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  89. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  90. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  91. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  92. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  93. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  94. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
  95. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
  96. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
  97. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
  98. package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
  99. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
  100. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
  101. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
  102. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  103. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  104. package/templates/.codex/hooks.json +8 -8
  105. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  106. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
@@ -8,7 +8,7 @@ const command = process.argv[2] || '';
8
8
  if (process.env.AGENT_INFRA_CONTROL_TOKEN
9
9
  && (command === 'task-lifecycle' || command === 'task-orchestration')) {
10
10
  const { sandboxControl } = await import('../lib/internal/sandbox-control.js');
11
- sandboxControl(['client', command, ...process.argv.slice(3)]);
11
+ await sandboxControl(['client', command, ...process.argv.slice(3)]);
12
12
  }
13
13
  else
14
14
  switch (command) {
@@ -19,7 +19,7 @@ else
19
19
  }
20
20
  case 'sandbox-control': {
21
21
  const { sandboxControl } = await import('../lib/internal/sandbox-control.js');
22
- sandboxControl(process.argv.slice(3));
22
+ await sandboxControl(process.argv.slice(3));
23
23
  break;
24
24
  }
25
25
  case 'agent-client': {
@@ -109,7 +109,7 @@ else
109
109
  }
110
110
  case 'task-orchestration': {
111
111
  const { taskOrchestration } = await import('../lib/internal/task-orchestration.js');
112
- taskOrchestration(process.argv.slice(3));
112
+ await taskOrchestration(process.argv.slice(3));
113
113
  break;
114
114
  }
115
115
  case 'task-review': {
@@ -6,10 +6,12 @@ import path from 'node:path';
6
6
  import semver from 'semver';
7
7
  const LIFECYCLE_HOOKS = Object.freeze([
8
8
  Object.freeze({ event: 'PreToolUse', matcher: '^collaborationspawn_agent$', phase: 'pre-tool' }),
9
- Object.freeze({ event: 'PostToolUse', matcher: '^collaborationspawn_agent$', phase: 'post-tool' }),
10
- Object.freeze({ event: 'SubagentStart', matcher: '^agent-infra-lifecycle-(executor|reviewer)$', phase: 'subagent-start' }),
11
- Object.freeze({ event: 'SubagentStop', matcher: '^agent-infra-lifecycle-(executor|reviewer)$', phase: 'subagent-stop' })
9
+ Object.freeze({ event: 'PostToolUse', matcher: '', phase: 'post-tool' }),
10
+ Object.freeze({ event: 'SubagentStart', matcher: '', phase: 'subagent-start' }),
11
+ Object.freeze({ event: 'SubagentStop', matcher: '', phase: 'subagent-stop' })
12
12
  ]);
13
+ const DEFAULT_ROLLOUT_READ_ATTEMPTS = 81;
14
+ const DEFAULT_ROLLOUT_RETRY_MS = 100;
13
15
  function object(value) {
14
16
  return value && typeof value === 'object' && !Array.isArray(value)
15
17
  ? value
@@ -18,6 +20,58 @@ function object(value) {
18
20
  function nonEmpty(value) {
19
21
  return typeof value === 'string' && value.trim() ? value : null;
20
22
  }
23
+ function resolveCodexSpawnedChild(transcriptPath, expected) {
24
+ const name = path.basename(transcriptPath);
25
+ if (!transcriptPath || (!name.endsWith(`-${expected.sessionId}.jsonl`) && name !== `${expected.sessionId}.jsonl`)) {
26
+ throw new Error('Codex parent rollout path is invalid');
27
+ }
28
+ const stat = fs.lstatSync(transcriptPath);
29
+ if (!stat.isFile() || stat.isSymbolicLink())
30
+ throw new Error('Codex parent rollout is not a regular file');
31
+ const maxBytes = 8 * 1024 * 1024;
32
+ const offset = Math.max(0, stat.size - maxBytes);
33
+ const buffer = Buffer.alloc(stat.size - offset);
34
+ const descriptor = fs.openSync(transcriptPath, 'r');
35
+ try {
36
+ fs.readSync(descriptor, buffer, 0, buffer.length, offset);
37
+ }
38
+ finally {
39
+ fs.closeSync(descriptor);
40
+ }
41
+ let text = buffer.toString('utf8');
42
+ if (offset)
43
+ text = text.slice(text.indexOf('\n') + 1);
44
+ const records = text.split(/\r?\n/).filter(Boolean).map((line) => object(JSON.parse(line))).filter(Boolean);
45
+ const calls = records.filter((record) => {
46
+ const payload = object(record?.payload);
47
+ return record?.type === 'response_item'
48
+ && payload?.type === 'function_call'
49
+ && payload.call_id === expected.toolUseId
50
+ && payload.namespace === 'collaboration'
51
+ && payload.name === 'spawn_agent';
52
+ });
53
+ if (calls.length !== 1)
54
+ throw new Error('Codex parent rollout spawn call was not found uniquely');
55
+ const call = object(calls[0]?.payload);
56
+ const args = object(JSON.parse(nonEmpty(call?.arguments) ?? '{}'));
57
+ if (args?.agent_type !== expected.nativeAgent
58
+ || args.task_name !== expected.taskName
59
+ || (expected.requestedModel && args.model !== expected.requestedModel)
60
+ || (expected.requestedReasoningEffort && args.reasoning_effort !== expected.requestedReasoningEffort))
61
+ throw new Error('Codex parent rollout spawn identity does not match the hook event');
62
+ const activities = records.filter((record) => {
63
+ const payload = object(record?.payload);
64
+ return record?.type === 'event_msg'
65
+ && payload?.type === 'sub_agent_activity'
66
+ && payload.event_id === expected.toolUseId
67
+ && payload.kind === 'started'
68
+ && payload.agent_path === `/root/${expected.taskName}`
69
+ && nonEmpty(payload.agent_thread_id);
70
+ });
71
+ if (activities.length !== 1)
72
+ throw new Error('Codex parent rollout child activity was not found uniquely');
73
+ return nonEmpty(object(activities[0]?.payload)?.agent_thread_id);
74
+ }
21
75
  function validateCodexLifecycleHookConfig(value) {
22
76
  const hooks = object(object(value)?.hooks);
23
77
  const valid = hooks && LIFECYCLE_HOOKS.every(({ event, matcher, phase }) => {
@@ -31,7 +85,7 @@ function validateCodexLifecycleHookConfig(value) {
31
85
  return entry.hooks.some((hookValue) => {
32
86
  const hook = object(hookValue);
33
87
  return hook?.type === 'command'
34
- && hook.timeout === 5
88
+ && hook.timeout === 15
35
89
  && hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`;
36
90
  });
37
91
  });
@@ -84,25 +138,37 @@ function hasCodexRuntimeLiveness(runtimeRoot, hookDefinitionHash, identity) {
84
138
  }
85
139
  });
86
140
  }
87
- function parseCodexThreadResolution(readValue, resumeValue) {
141
+ function parseCodexThreadResolution(readValue, rolloutRecords) {
88
142
  const readRoot = object(readValue);
89
- const resumeRoot = object(resumeValue);
90
143
  const thread = object(readRoot?.thread);
91
- const resumedThread = object(resumeRoot?.thread);
92
144
  const source = object(thread?.source);
93
145
  const subAgent = object(source?.subAgent);
94
146
  const threadSpawn = object(subAgent?.thread_spawn);
147
+ const sessionMeta = rolloutRecords
148
+ .map(object)
149
+ .find((record) => record?.type === 'session_meta');
150
+ const sessionPayload = object(sessionMeta?.payload);
151
+ const turnContext = rolloutRecords
152
+ .map(object)
153
+ .filter((record) => record?.type === 'turn_context')
154
+ .at(-1);
155
+ const turnPayload = object(turnContext?.payload);
95
156
  const childThreadId = nonEmpty(thread?.id);
96
157
  const parentThreadId = nonEmpty(thread?.parentThreadId);
97
158
  const sourceParentThreadId = nonEmpty(threadSpawn?.parent_thread_id);
98
- const resumedChildThreadId = nonEmpty(resumedThread?.id);
99
- const model = nonEmpty(resumeRoot?.model);
100
- const reasoningEffort = nonEmpty(resumeRoot?.reasoningEffort ?? object(resumeRoot?.settings)?.effort);
159
+ const rolloutChildThreadId = nonEmpty(sessionPayload?.id);
160
+ const rolloutParentThreadId = nonEmpty(sessionPayload?.parent_thread_id);
161
+ const agentRole = nonEmpty(sessionPayload?.agent_role);
162
+ const model = nonEmpty(turnPayload?.model);
163
+ const reasoningEffort = nonEmpty(turnPayload?.effort);
101
164
  const forkedFromId = thread?.forkedFromId;
102
165
  if (!childThreadId
103
- || resumedChildThreadId !== childThreadId
166
+ || rolloutChildThreadId !== childThreadId
104
167
  || !parentThreadId
105
168
  || !sourceParentThreadId
169
+ || rolloutParentThreadId !== parentThreadId
170
+ || rolloutParentThreadId !== sourceParentThreadId
171
+ || !/^agent-infra-lifecycle-(executor|reviewer)$/.test(agentRole ?? '')
106
172
  || !model
107
173
  || !reasoningEffort
108
174
  || (forkedFromId !== null && typeof forkedFromId !== 'string')) {
@@ -114,7 +180,8 @@ function parseCodexThreadResolution(readValue, resumeValue) {
114
180
  childThreadId,
115
181
  parentThreadId,
116
182
  forkedFromId,
117
- sourceParentThreadId
183
+ sourceParentThreadId,
184
+ nativeAgent: agentRole
118
185
  }),
119
186
  settings: Object.freeze({
120
187
  type: 'app-settings',
@@ -124,6 +191,46 @@ function parseCodexThreadResolution(readValue, resumeValue) {
124
191
  })
125
192
  });
126
193
  }
194
+ function readCodexRolloutRecords(readValue, childThreadId) {
195
+ const thread = object(object(readValue)?.thread);
196
+ const rolloutPath = nonEmpty(thread?.path);
197
+ const rolloutName = rolloutPath ? path.basename(rolloutPath) : '';
198
+ if (!rolloutPath || (rolloutName !== `${childThreadId}.jsonl` && !rolloutName.endsWith(`-${childThreadId}.jsonl`))) {
199
+ throw new Error('Codex App Server rollout path is invalid');
200
+ }
201
+ const stat = fs.statSync(rolloutPath);
202
+ if (!stat.isFile())
203
+ throw new Error('Codex App Server rollout path is not a regular file');
204
+ const maxBytes = 1024 * 1024;
205
+ const bytes = Math.min(stat.size, maxBytes);
206
+ const descriptor = fs.openSync(rolloutPath, 'r');
207
+ try {
208
+ const buffer = Buffer.alloc(bytes);
209
+ fs.readSync(descriptor, buffer, 0, bytes, 0);
210
+ const text = buffer.toString('utf8');
211
+ const completeText = stat.size <= maxBytes ? text : text.slice(0, text.lastIndexOf('\n'));
212
+ return Object.freeze(completeText.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line)));
213
+ }
214
+ finally {
215
+ fs.closeSync(descriptor);
216
+ }
217
+ }
218
+ async function resolveCodexRolloutRecords(readValue, childThreadId, attempts, retryMs) {
219
+ let lastError;
220
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
221
+ try {
222
+ const records = readCodexRolloutRecords(readValue, childThreadId);
223
+ parseCodexThreadResolution(readValue, records);
224
+ return records;
225
+ }
226
+ catch (error) {
227
+ lastError = error;
228
+ if (attempt + 1 < attempts)
229
+ await new Promise((resolve) => setTimeout(resolve, retryMs));
230
+ }
231
+ }
232
+ throw lastError;
233
+ }
127
234
  function parseCodexTurnCompleted(value) {
128
235
  const root = object(value);
129
236
  const turn = object(root?.turn);
@@ -287,13 +394,13 @@ async function resolveCodexThread(childThreadId, options = {}) {
287
394
  const readThread = object(readResult?.thread);
288
395
  if (nonEmpty(readThread?.id) !== childThreadId)
289
396
  throw new Error('Codex App Server returned the wrong child thread');
290
- const resumeResult = await transport.request('thread/resume', { threadId: childThreadId });
291
397
  let resolution;
292
398
  try {
293
- resolution = parseCodexThreadResolution(readResult, resumeResult);
399
+ const rolloutRecords = await resolveCodexRolloutRecords(readResult, childThreadId, options.rolloutReadAttempts ?? DEFAULT_ROLLOUT_READ_ATTEMPTS, options.rolloutRetryMs ?? DEFAULT_ROLLOUT_RETRY_MS);
400
+ resolution = parseCodexThreadResolution(readResult, rolloutRecords);
294
401
  }
295
402
  catch {
296
- throw new Error('Codex App Server resumed settings are unavailable');
403
+ throw new Error('Codex App Server rollout metadata is unavailable');
297
404
  }
298
405
  const reroutes = transport.notifications
299
406
  .filter((entry) => entry.method === 'model/rerouted')
@@ -322,6 +429,9 @@ async function resolveCodexTerminal(childThreadId, options = {}) {
322
429
  throw new Error('Codex App Server returned the wrong child thread');
323
430
  const turns = Array.isArray(thread?.turns) ? thread.turns : [];
324
431
  const turn = turns.at(-1);
432
+ const turnValue = object(turn);
433
+ if (!turnValue || turnValue.status === 'inProgress')
434
+ throw new Error('CODEX_TURN_NOT_TERMINAL');
325
435
  return parseCodexTurnCompleted({ threadId: childThreadId, turn });
326
436
  }
327
437
  finally {
@@ -378,5 +488,5 @@ async function preflightCodexLifecycleEvidence(repoRoot = process.cwd(), runtime
378
488
  diagnostics: transport.diagnostics
379
489
  });
380
490
  }
381
- export { CodexAppServerTransport, hasCodexRuntimeLiveness, parseCodexModelReroute, parseCodexHooksList, parseCodexThreadResolution, parseCodexTurnCompleted, preflightCodexLifecycleEvidence, resolveCodexTerminal, resolveCodexThread, validateCodexLifecycleHookConfig };
491
+ export { CodexAppServerTransport, hasCodexRuntimeLiveness, parseCodexModelReroute, parseCodexHooksList, parseCodexThreadResolution, parseCodexTurnCompleted, resolveCodexSpawnedChild, preflightCodexLifecycleEvidence, resolveCodexTerminal, resolveCodexThread, validateCodexLifecycleHookConfig };
382
492
  //# sourceMappingURL=app-server.js.map
@@ -35,17 +35,29 @@ function requiredStrings(event) {
35
35
  }
36
36
  function derive(state) {
37
37
  const { spawn, child, thread, settings, reroute, terminal, stop } = state;
38
- if (spawn && child && (spawn.sessionId !== child.sessionId
38
+ if (spawn && child && (spawn.sessionId !== child.parentThreadId
39
39
  || spawn.nativeAgent !== child.nativeAgent)) {
40
40
  return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook spawn and child identity do not match');
41
41
  }
42
+ if (child && child.sessionId !== child.parentThreadId) {
43
+ return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'child event session does not match the parent thread');
44
+ }
42
45
  if (child && thread && child.childThreadId !== thread.childThreadId) {
43
46
  return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook and App Server child identity do not match');
44
47
  }
48
+ if (child && thread && child.nativeAgent !== thread.nativeAgent) {
49
+ return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook and App Server agent roles do not match');
50
+ }
51
+ if (child && thread && child.parentThreadId !== thread.parentThreadId) {
52
+ return invalid(state, 'CODEX_EVIDENCE_PARENT_MISMATCH', 'hook and App Server parent identity do not match');
53
+ }
45
54
  if (spawn && thread && (thread.parentThreadId !== spawn.sessionId
46
55
  || thread.sourceParentThreadId !== spawn.sessionId)) {
47
56
  return invalid(state, 'CODEX_EVIDENCE_PARENT_MISMATCH', 'App Server parent identity does not match the spawning session');
48
57
  }
58
+ if (spawn && thread && spawn.nativeAgent !== thread.nativeAgent) {
59
+ return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'spawn and App Server agent roles do not match');
60
+ }
49
61
  if (thread?.forkedFromId !== null && thread?.forkedFromId !== undefined) {
50
62
  return invalid(state, 'CODEX_EVIDENCE_FORK_FORBIDDEN', 'forked child threads are not fresh lifecycle evidence');
51
63
  }
@@ -105,7 +117,7 @@ function derive(state) {
105
117
  childThreadId: startEvidence.childThreadId,
106
118
  turnId: terminal.turnId,
107
119
  terminalStatus: 'completed',
108
- hookStopObserved: true
120
+ hookStopObserved: stop.source !== 'parent-rollout'
109
121
  });
110
122
  }
111
123
  const status = stopEvidence
@@ -99,7 +99,7 @@ function createCodexLifecycleStore(options) {
99
99
  if (event.type === 'hook-child') {
100
100
  const matches = recordFiles(root).filter((file) => {
101
101
  const state = readRecord(file).state;
102
- return state.spawn?.sessionId === event.sessionId
102
+ return state.spawn?.sessionId === event.parentThreadId
103
103
  && state.spawn.nativeAgent === event.nativeAgent
104
104
  && (!state.child || state.child.childThreadId === event.childThreadId);
105
105
  });
@@ -146,6 +146,43 @@ function createCodexLifecycleStore(options) {
146
146
  return Object.freeze({ path: file, revision: next.revision, state: next.state });
147
147
  });
148
148
  }
149
+ function applyToSpawn(identity, event) {
150
+ return withWriteLock(() => {
151
+ const file = path.join(root, `${digest(`${identity.sessionId}\0${identity.turnId}\0${identity.toolUseId}`)}.json`);
152
+ if (!fs.existsSync(file))
153
+ throw new Error('Codex lifecycle spawn identity was not found');
154
+ const current = readRecord(file);
155
+ if (current.consumer)
156
+ throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
157
+ if (current.state.spawn?.sessionId !== identity.sessionId
158
+ || current.state.spawn.turnId !== identity.turnId
159
+ || current.state.spawn.toolUseId !== identity.toolUseId)
160
+ throw new Error('Codex lifecycle spawn identity does not match the stored event');
161
+ const nextState = reduceCodexLifecycleEvent(current.state, event);
162
+ const next = Object.freeze({
163
+ ...current,
164
+ revision: current.revision + 1,
165
+ state: nextState,
166
+ updatedAt: now()
167
+ });
168
+ writeRecord(file, next, current.revision);
169
+ return Object.freeze({ path: file, revision: next.revision, state: next.state });
170
+ });
171
+ }
172
+ function findByParent(parentThreadId) {
173
+ const matches = recordFiles(root)
174
+ .map(readRecord)
175
+ .filter((record) => record.state.startEvidence?.parentThreadId === parentThreadId
176
+ && ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status));
177
+ const unconsumed = matches.filter((record) => !record.consumer);
178
+ if (unconsumed.length > 1)
179
+ throw new Error(`Codex lifecycle parent '${parentThreadId}' has ambiguous active children`);
180
+ if (unconsumed.length === 1)
181
+ return Object.freeze(unconsumed);
182
+ return Object.freeze(matches
183
+ .filter((record) => record.consumer && record.state.status === 'stop-ready')
184
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)));
185
+ }
149
186
  function read(childThreadId) {
150
187
  const matches = findByChild(childThreadId);
151
188
  if (matches.length !== 1)
@@ -161,8 +198,15 @@ function createCodexLifecycleStore(options) {
161
198
  throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
162
199
  const file = matches[0];
163
200
  const current = readRecord(file);
164
- if (current.consumer)
165
- throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
201
+ if (current.consumer) {
202
+ if (current.consumer !== consumer) {
203
+ throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
204
+ }
205
+ if (expectedHookDefinitionHash
206
+ && current.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash)
207
+ throw new Error('Codex lifecycle hook definition hash is stale');
208
+ return current;
209
+ }
166
210
  if (current.state.status !== 'stop-ready')
167
211
  throw new Error('Codex lifecycle evidence is not stop-ready');
168
212
  if (expectedHookDefinitionHash
@@ -205,7 +249,7 @@ function createCodexLifecycleStore(options) {
205
249
  return changed;
206
250
  });
207
251
  }
208
- return Object.freeze({ root, apply, consume, expireBefore, read });
252
+ return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
209
253
  }
210
254
  export { createCodexLifecycleStore };
211
255
  //# sourceMappingURL=store.js.map
@@ -11,7 +11,7 @@ const codexAdapter = defineAgentClientAdapter({
11
11
  commands: { level: 'integrated' },
12
12
  hooks: { level: 'integrated' },
13
13
  subagents: { level: 'experimental' },
14
- orchestration: { level: 'unsupported' },
14
+ orchestration: { level: 'experimental' },
15
15
  sandbox: { level: 'integrated' },
16
16
  verification: { level: 'compatible' }
17
17
  },
@@ -119,7 +119,6 @@ function pushRebasedBranch(input, runner = defaultRunner) {
119
119
  .map((name) => value(runner, input.cwd, ['rev-parse', '--git-path', name]))
120
120
  .filter((candidate) => Boolean(candidate));
121
121
  if (snapshot.worktree.length > 0 || snapshot.staged.length > 0 ||
122
- run(runner, input.cwd, ['rev-parse', '-q', '--verify', 'REBASE_HEAD']).status === 0 ||
123
122
  rebasePaths.some((candidate) => fs.existsSync(path.isAbsolute(candidate) ? candidate : path.join(input.cwd, candidate)))) {
124
123
  return { status: 'failed', changed: false, snapshot, operations: [], error: { code: 'GIT_LOCAL_STATE_UNSAFE', message: 'Working tree must be clean with no rebase in progress' } };
125
124
  }
@@ -4,6 +4,7 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { preflightCodexLifecycleEvidence, resolveCodexTerminal, resolveCodexThread } from '../agent-clients/adapters/codex-lifecycle/app-server.js';
6
6
  import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.js';
7
+ import { activateCodexOrchestrationDelegation, activateCodexSpawnDelegation, reconcileCodexOrchestrationDelegation, sealCodexOrchestrationDelegation, sealCodexParentDelegation } from '../task/codex-orchestration.js';
7
8
  const USAGE = 'Usage: agent-infra-internal codex-lifecycle <hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
8
9
  const MANAGED_AGENT = /^agent-infra-lifecycle-(executor|reviewer)$/;
9
10
  function output(value) {
@@ -13,6 +14,15 @@ function failure(code, message) {
13
14
  output({ status: 'failed', changed: false, evidence: null, diagnostics: [], error: { code, message } });
14
15
  process.exitCode = 1;
15
16
  }
17
+ function outputBridgeResult(result) {
18
+ if (result.error?.code === 'ORCHESTRATION_DELEGATION_MISSING') {
19
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
20
+ return;
21
+ }
22
+ output(result);
23
+ if (result.status !== 'running')
24
+ process.exitCode = 1;
25
+ }
16
26
  function parse(args) {
17
27
  if (args[0] === '--help' || args[0] === '-h') {
18
28
  process.stdout.write(USAGE);
@@ -24,7 +34,7 @@ function parse(args) {
24
34
  return null;
25
35
  }
26
36
  const allowed = {
27
- 'hook-event': ['--event'],
37
+ 'hook-event': ['--event', '--bridge'],
28
38
  'resolve-start': ['--child-id'],
29
39
  'resolve-stop': ['--child-id'],
30
40
  preflight: ['--format', '--session-id', '--turn-id', '--tool-use-id'],
@@ -110,6 +120,12 @@ function recordFromPayload(phase, payload) {
110
120
  return null;
111
121
  throw new Error(`unknown Codex lifecycle hook phase '${phase}'`);
112
122
  }
123
+ function payloadText(payload, key) {
124
+ return payload && typeof payload === 'object' && !Array.isArray(payload)
125
+ && typeof payload[key] === 'string'
126
+ ? payload[key]
127
+ : '';
128
+ }
113
129
  async function codexLifecycle(args = []) {
114
130
  const parsed = parse(args);
115
131
  if (!parsed || process.exitCode)
@@ -146,12 +162,94 @@ async function codexLifecycle(args = []) {
146
162
  if (!phase || !['pre-tool', 'subagent-start', 'post-tool', 'subagent-stop'].includes(phase)) {
147
163
  throw new Error('hook-event requires a known --event');
148
164
  }
149
- const event = recordFromPayload(phase, await readStdin());
165
+ if (parsed.values['--bridge'] !== undefined && parsed.values['--bridge'] !== 'true') {
166
+ throw new Error("hook-event --bridge must be 'true'");
167
+ }
168
+ const payload = await readStdin();
169
+ if (phase === 'post-tool' && parsed.values['--bridge'] === 'true') {
170
+ const toolName = payloadText(payload, 'toolName');
171
+ if (toolName === 'collaborationspawn_agent') {
172
+ const nativeAgent = payloadText(payload, 'nativeAgent');
173
+ if (!MANAGED_AGENT.test(nativeAgent)) {
174
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
175
+ return;
176
+ }
177
+ const requestedModel = payloadText(payload, 'requestedModel');
178
+ const requestedReasoningEffort = payloadText(payload, 'requestedReasoningEffort');
179
+ const bridged = await activateCodexSpawnDelegation({
180
+ sessionId: payloadText(payload, 'sessionId'),
181
+ turnId: payloadText(payload, 'turnId'),
182
+ toolUseId: payloadText(payload, 'toolUseId'),
183
+ transcriptPath: payloadText(payload, 'transcriptPath'),
184
+ nativeAgent,
185
+ taskName: payloadText(payload, 'taskName'),
186
+ ...(requestedModel ? { requestedModel } : {}),
187
+ ...(requestedReasoningEffort ? { requestedReasoningEffort } : {})
188
+ }, { store });
189
+ outputBridgeResult(bridged);
190
+ return;
191
+ }
192
+ if (toolName === 'collaborationwait_agent') {
193
+ outputBridgeResult(await sealCodexParentDelegation(payloadText(payload, 'sessionId'), { store }));
194
+ return;
195
+ }
196
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
197
+ return;
198
+ }
199
+ const event = recordFromPayload(phase, payload);
150
200
  if (!event) {
201
+ if (phase === 'post-tool' && parsed.values['--bridge'] === 'true') {
202
+ const value = payload;
203
+ const childThreadId = String(value.childThreadId ?? '');
204
+ const reconciled = reconcileCodexOrchestrationDelegation(childThreadId);
205
+ outputBridgeResult(reconciled);
206
+ return;
207
+ }
151
208
  output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
152
209
  return;
153
210
  }
211
+ if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
212
+ try {
213
+ if (store.read(event.childThreadId).consumer) {
214
+ const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
215
+ outputBridgeResult(bridged);
216
+ return;
217
+ }
218
+ }
219
+ catch {
220
+ // The normal apply path below owns missing or invalid evidence errors.
221
+ }
222
+ }
223
+ if (parsed.values['--bridge'] === 'true' && event.type === 'hook-child') {
224
+ const resolved = await resolveCodexThread(event.childThreadId);
225
+ store.apply({
226
+ ...event,
227
+ parentThreadId: resolved.resolution.thread.parentThreadId
228
+ });
229
+ const bridged = await activateCodexOrchestrationDelegation(event.childThreadId, {
230
+ store,
231
+ resolveThread: async () => resolved
232
+ });
233
+ outputBridgeResult(bridged);
234
+ return;
235
+ }
236
+ if (event.type === 'hook-child') {
237
+ const resolved = await resolveCodexThread(event.childThreadId);
238
+ const result = store.apply({
239
+ ...event,
240
+ parentThreadId: resolved.resolution.thread.parentThreadId
241
+ });
242
+ output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: resolved.diagnostics, error: result.state.error });
243
+ if (result.state.status === 'invalid')
244
+ process.exitCode = 1;
245
+ return;
246
+ }
154
247
  const result = store.apply(event);
248
+ if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
249
+ const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
250
+ outputBridgeResult(bridged);
251
+ return;
252
+ }
155
253
  output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: [], error: result.state.error });
156
254
  if (result.state.status === 'invalid')
157
255
  process.exitCode = 1;