@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
@@ -14,10 +14,12 @@ function readRecord(file) {
14
14
  || !Number.isSafeInteger(value.revision)
15
15
  || value.revision < 1
16
16
  || !value.state
17
- || value.state.schemaVersion !== 1) {
17
+ || value.state.schemaVersion !== 1
18
+ || (value.spawnObservedAt != null && (typeof value.spawnObservedAt !== 'string'
19
+ || !Number.isFinite(Date.parse(value.spawnObservedAt))))) {
18
20
  throw new Error(`Codex lifecycle record '${path.basename(file)}' is invalid`);
19
21
  }
20
- return value;
22
+ return Object.freeze({ ...value, spawnObservedAt: value.spawnObservedAt ?? null });
21
23
  }
22
24
  function recordFiles(root) {
23
25
  if (!fs.existsSync(root))
@@ -26,6 +28,15 @@ function recordFiles(root) {
26
28
  .filter((name) => /^[a-f0-9]{64}\.json$/.test(name))
27
29
  .map((name) => path.join(root, name));
28
30
  }
31
+ function hasActiveCodexLifecycleEvidence(root, query) {
32
+ return recordFiles(root).map(readRecord).some((record) => {
33
+ const evidence = record.state.startEvidence;
34
+ return record.consumer === null
35
+ && ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status)
36
+ && evidence?.nativeAgent === query.nativeAgent
37
+ && evidence.hookDefinitionHash === query.hookDefinitionHash;
38
+ });
39
+ }
29
40
  function writeRecord(file, record, expectedRevision) {
30
41
  const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
31
42
  fs.writeFileSync(temp, `${JSON.stringify(record, null, 2)}\n`, {
@@ -99,7 +110,7 @@ function createCodexLifecycleStore(options) {
99
110
  if (event.type === 'hook-child') {
100
111
  const matches = recordFiles(root).filter((file) => {
101
112
  const state = readRecord(file).state;
102
- return state.spawn?.sessionId === event.sessionId
113
+ return state.spawn?.sessionId === event.parentThreadId
103
114
  && state.spawn.nativeAgent === event.nativeAgent
104
115
  && (!state.child || state.child.childThreadId === event.childThreadId);
105
116
  });
@@ -121,6 +132,7 @@ function createCodexLifecycleStore(options) {
121
132
  function apply(event) {
122
133
  return withWriteLock(() => {
123
134
  const file = locate(event);
135
+ const observedAt = now();
124
136
  const current = fs.existsSync(file)
125
137
  ? readRecord(file)
126
138
  : Object.freeze({
@@ -129,7 +141,8 @@ function createCodexLifecycleStore(options) {
129
141
  state: createCodexLifecycleState(options.cliVersion),
130
142
  consumer: null,
131
143
  consumedAt: null,
132
- updatedAt: now()
144
+ spawnObservedAt: null,
145
+ updatedAt: observedAt
133
146
  });
134
147
  if (current.consumer)
135
148
  throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
@@ -140,12 +153,52 @@ function createCodexLifecycleStore(options) {
140
153
  state: nextState,
141
154
  consumer: null,
142
155
  consumedAt: null,
156
+ spawnObservedAt: event.type === 'hook-spawn' && current.revision === 0
157
+ ? observedAt
158
+ : current.spawnObservedAt,
159
+ updatedAt: observedAt
160
+ });
161
+ writeRecord(file, next, current.revision);
162
+ return Object.freeze({ path: file, revision: next.revision, state: next.state });
163
+ });
164
+ }
165
+ function applyToSpawn(identity, event) {
166
+ return withWriteLock(() => {
167
+ const file = path.join(root, `${digest(`${identity.sessionId}\0${identity.turnId}\0${identity.toolUseId}`)}.json`);
168
+ if (!fs.existsSync(file))
169
+ throw new Error('Codex lifecycle spawn identity was not found');
170
+ const current = readRecord(file);
171
+ if (current.consumer)
172
+ throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
173
+ if (current.state.spawn?.sessionId !== identity.sessionId
174
+ || current.state.spawn.turnId !== identity.turnId
175
+ || current.state.spawn.toolUseId !== identity.toolUseId)
176
+ throw new Error('Codex lifecycle spawn identity does not match the stored event');
177
+ const nextState = reduceCodexLifecycleEvent(current.state, event);
178
+ const next = Object.freeze({
179
+ ...current,
180
+ revision: current.revision + 1,
181
+ state: nextState,
143
182
  updatedAt: now()
144
183
  });
145
184
  writeRecord(file, next, current.revision);
146
185
  return Object.freeze({ path: file, revision: next.revision, state: next.state });
147
186
  });
148
187
  }
188
+ function findByParent(parentThreadId) {
189
+ const matches = recordFiles(root)
190
+ .map(readRecord)
191
+ .filter((record) => record.state.startEvidence?.parentThreadId === parentThreadId
192
+ && ['start-ready', 'observed-terminal', 'stop-ready'].includes(record.state.status));
193
+ const unconsumed = matches.filter((record) => !record.consumer);
194
+ if (unconsumed.length > 1)
195
+ throw new Error(`Codex lifecycle parent '${parentThreadId}' has ambiguous active children`);
196
+ if (unconsumed.length === 1)
197
+ return Object.freeze(unconsumed);
198
+ return Object.freeze(matches
199
+ .filter((record) => record.consumer && record.state.status === 'stop-ready')
200
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)));
201
+ }
149
202
  function read(childThreadId) {
150
203
  const matches = findByChild(childThreadId);
151
204
  if (matches.length !== 1)
@@ -161,8 +214,15 @@ function createCodexLifecycleStore(options) {
161
214
  throw new Error(`Codex lifecycle child '${childThreadId}' was not found uniquely`);
162
215
  const file = matches[0];
163
216
  const current = readRecord(file);
164
- if (current.consumer)
165
- throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
217
+ if (current.consumer) {
218
+ if (current.consumer !== consumer) {
219
+ throw new Error(`Codex lifecycle evidence was already consumed by '${current.consumer}'`);
220
+ }
221
+ if (expectedHookDefinitionHash
222
+ && current.state.startEvidence?.hookDefinitionHash !== expectedHookDefinitionHash)
223
+ throw new Error('Codex lifecycle hook definition hash is stale');
224
+ return current;
225
+ }
166
226
  if (current.state.status !== 'stop-ready')
167
227
  throw new Error('Codex lifecycle evidence is not stop-ready');
168
228
  if (expectedHookDefinitionHash
@@ -205,7 +265,7 @@ function createCodexLifecycleStore(options) {
205
265
  return changed;
206
266
  });
207
267
  }
208
- return Object.freeze({ root, apply, consume, expireBefore, read });
268
+ return Object.freeze({ root, apply, applyToSpawn, consume, expireBefore, findByParent, read });
209
269
  }
210
- export { createCodexLifecycleStore };
270
+ export { createCodexLifecycleStore, hasActiveCodexLifecycleEvidence };
211
271
  //# 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
  },
@@ -56,7 +56,6 @@
56
56
  ".agents/skills/",
57
57
  ".agents/templates/",
58
58
  ".agents/workflows/",
59
- ".agents/workspace/README.md",
60
59
  ".git-hooks/check-large-files.cjs",
61
60
  ".git-hooks/check-version-format.sh",
62
61
  ".github/scripts/",
@@ -84,6 +83,15 @@
84
83
  ".git-hooks/pre-commit",
85
84
  ".gitignore"
86
85
  ],
87
- "ejected": []
86
+ "ejected": [],
87
+ "retiredManaged": [
88
+ {
89
+ "path": ".agents/workspace/README.md",
90
+ "templateHashes": [
91
+ "sha256:967b27ca4008154c5abf99a1df728242f80ed3ace9bd98eb2828bc1c76af826c",
92
+ "sha256:15aa21942cf2a1396d27834f1d3d723b61a46f8d14600b706f39f3bebc21aea3"
93
+ ]
94
+ }
95
+ ]
88
96
  }
89
97
  }
@@ -12,6 +12,9 @@ function value(runner, cwd, args, trim = true) {
12
12
  const result = run(runner, cwd, args);
13
13
  return result.status === 0 ? (trim ? result.stdout.trim() : result.stdout.replace(/\n$/, '')) : null;
14
14
  }
15
+ function nulPaths(output) {
16
+ return output.split('\0').filter(Boolean);
17
+ }
15
18
  function cleanMessage(message) {
16
19
  return message.replace(/(https?:\/\/)[^\s/@:]+(?::[^\s/@]*)?@/gi, '$1***@').trim();
17
20
  }
@@ -64,9 +67,33 @@ function commitExplicitPaths(input, runner = defaultRunner) {
64
67
  const unrelatedStaged = before.snapshot.staged.filter((candidate) => !selected.has(candidate));
65
68
  if (unrelatedStaged.length > 0)
66
69
  return { status: 'failed', changed: false, snapshot: before.snapshot, operations: [], error: { code: 'GIT_STAGED_SCOPE_MISMATCH', message: `Unrelated staged paths: ${unrelatedStaged.join(', ')}` } };
67
- const add = run(runner, input.cwd, ['add', '--', ...input.paths]);
68
- if (add.status !== 0)
69
- return { status: 'failed', changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'failed', message: add.stderr.trim() }], error: { code: 'GIT_STAGE_FAILED', message: add.stderr.trim() } };
70
+ const stageFailure = (message) => ({ status: 'failed', changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'failed', message }], error: { code: 'GIT_STAGE_FAILED', message } });
71
+ const indexed = run(runner, input.cwd, ['ls-files', '-z', '--', ...input.paths]);
72
+ if (indexed.status !== 0)
73
+ return stageFailure(indexed.stderr.trim());
74
+ const headed = run(runner, input.cwd, ['ls-tree', '-r', '--name-only', '-z', 'HEAD', '--', ...input.paths]);
75
+ if (headed.status !== 0)
76
+ return stageFailure(headed.stderr.trim());
77
+ const indexedPaths = new Set(nulPaths(indexed.stdout));
78
+ const headPaths = new Set(nulPaths(headed.stdout));
79
+ const trackedPaths = [];
80
+ const addPaths = [];
81
+ for (const candidate of input.paths) {
82
+ if (indexedPaths.has(candidate))
83
+ trackedPaths.push(candidate);
84
+ else if (!headPaths.has(candidate) || fs.existsSync(path.join(input.cwd, candidate)))
85
+ addPaths.push(candidate);
86
+ }
87
+ if (trackedPaths.length > 0) {
88
+ const update = run(runner, input.cwd, ['add', '-u', '--', ...trackedPaths]);
89
+ if (update.status !== 0)
90
+ return stageFailure(update.stderr.trim());
91
+ }
92
+ if (addPaths.length > 0) {
93
+ const add = run(runner, input.cwd, ['add', '--', ...addPaths]);
94
+ if (add.status !== 0)
95
+ return stageFailure(add.stderr.trim());
96
+ }
70
97
  const staged = run(runner, input.cwd, ['diff', '--cached', '--quiet', '--']);
71
98
  if (staged.status === 0)
72
99
  return { status: 'no-op', changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [{ name: 'stage', status: 'no-op' }], error: null };
@@ -119,7 +146,6 @@ function pushRebasedBranch(input, runner = defaultRunner) {
119
146
  .map((name) => value(runner, input.cwd, ['rev-parse', '--git-path', name]))
120
147
  .filter((candidate) => Boolean(candidate));
121
148
  if (snapshot.worktree.length > 0 || snapshot.staged.length > 0 ||
122
- run(runner, input.cwd, ['rev-parse', '-q', '--verify', 'REBASE_HEAD']).status === 0 ||
123
149
  rebasePaths.some((candidate) => fs.existsSync(path.isAbsolute(candidate) ? candidate : path.join(input.cwd, candidate)))) {
124
150
  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
151
  }
@@ -4,7 +4,12 @@ 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
- const USAGE = 'Usage: agent-infra-internal codex-lifecycle <hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
7
+ import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.js';
8
+ import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.js';
9
+ import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.js';
10
+ import { resolveTaskRef } from '../task/resolve-ref.js';
11
+ import { activateCodexOrchestrationDelegation, activateCodexSpawnDelegation, reconcileCodexOrchestrationDelegation, sealCodexOrchestrationDelegation, sealCodexParentDelegation } from '../task/codex-orchestration.js';
12
+ const USAGE = 'Usage: agent-infra-internal codex-lifecycle <capability-arm|hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
8
13
  const MANAGED_AGENT = /^agent-infra-lifecycle-(executor|reviewer)$/;
9
14
  function output(value) {
10
15
  process.stdout.write(`${JSON.stringify(value)}\n`);
@@ -13,6 +18,15 @@ function failure(code, message) {
13
18
  output({ status: 'failed', changed: false, evidence: null, diagnostics: [], error: { code, message } });
14
19
  process.exitCode = 1;
15
20
  }
21
+ function outputBridgeResult(result) {
22
+ if (result.error?.code === 'ORCHESTRATION_DELEGATION_MISSING') {
23
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
24
+ return;
25
+ }
26
+ output(result);
27
+ if (result.status !== 'running')
28
+ process.exitCode = 1;
29
+ }
16
30
  function parse(args) {
17
31
  if (args[0] === '--help' || args[0] === '-h') {
18
32
  process.stdout.write(USAGE);
@@ -24,7 +38,8 @@ function parse(args) {
24
38
  return null;
25
39
  }
26
40
  const allowed = {
27
- 'hook-event': ['--event'],
41
+ 'capability-arm': ['--task-id'],
42
+ 'hook-event': ['--event', '--bridge'],
28
43
  'resolve-start': ['--child-id'],
29
44
  'resolve-stop': ['--child-id'],
30
45
  preflight: ['--format', '--session-id', '--turn-id', '--tool-use-id'],
@@ -61,6 +76,16 @@ function hookDefinitionHash() {
61
76
  const file = path.join(process.cwd(), '.codex', 'hooks.json');
62
77
  return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
63
78
  }
79
+ function controllerBinding(taskId) {
80
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
81
+ if (!contextPath)
82
+ return undefined;
83
+ const context = verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot: process.cwd() });
84
+ return {
85
+ instanceDigest: context.controllerInstanceDigest,
86
+ controlGeneration: context.controlGeneration
87
+ };
88
+ }
64
89
  async function readStdin() {
65
90
  let input = '';
66
91
  process.stdin.setEncoding('utf8');
@@ -110,6 +135,12 @@ function recordFromPayload(phase, payload) {
110
135
  return null;
111
136
  throw new Error(`unknown Codex lifecycle hook phase '${phase}'`);
112
137
  }
138
+ function payloadText(payload, key) {
139
+ return payload && typeof payload === 'object' && !Array.isArray(payload)
140
+ && typeof payload[key] === 'string'
141
+ ? payload[key]
142
+ : '';
143
+ }
113
144
  async function codexLifecycle(args = []) {
114
145
  const parsed = parse(args);
115
146
  if (!parsed || process.exitCode)
@@ -140,18 +171,153 @@ async function codexLifecycle(args = []) {
140
171
  output({ status: 'ready', changed: false, evidence: result, diagnostics: result.diagnostics, error: null });
141
172
  return;
142
173
  }
174
+ if (parsed.operation === 'capability-arm') {
175
+ const taskId = parsed.values['--task-id'];
176
+ if (!taskId)
177
+ throw new Error('capability-arm requires --task-id');
178
+ const resolved = resolveTaskRef(taskId, { repoRoot: process.cwd() });
179
+ if (!resolved.ok)
180
+ throw new Error(`${resolved.code}: ${resolved.message}`);
181
+ const armed = createCodexCapabilityStore().arm({
182
+ taskId: resolved.taskId,
183
+ buildIdentity: computeLifecycleBuildIdentity(process.cwd()),
184
+ controller: controllerBinding(resolved.taskId)
185
+ });
186
+ output({
187
+ status: 'armed',
188
+ changed: true,
189
+ capabilityToken: armed.token,
190
+ marker: armed.marker,
191
+ expiresAt: armed.expiresAt,
192
+ buildIdentity: armed.buildIdentity,
193
+ error: null
194
+ });
195
+ return;
196
+ }
143
197
  const store = createCodexLifecycleStore({ cliVersion: cliVersion() });
144
198
  if (parsed.operation === 'hook-event') {
145
199
  const phase = parsed.values['--event'];
146
200
  if (!phase || !['pre-tool', 'subagent-start', 'post-tool', 'subagent-stop'].includes(phase)) {
147
201
  throw new Error('hook-event requires a known --event');
148
202
  }
149
- const event = recordFromPayload(phase, await readStdin());
203
+ if (parsed.values['--bridge'] !== undefined && parsed.values['--bridge'] !== 'true') {
204
+ throw new Error("hook-event --bridge must be 'true'");
205
+ }
206
+ const payload = await readStdin();
207
+ if (phase === 'post-tool') {
208
+ const capabilityToken = payloadText(payload, 'capabilityToken');
209
+ if (capabilityToken) {
210
+ const capabilityStore = createCodexCapabilityStore();
211
+ const armed = capabilityStore.inspect(capabilityToken);
212
+ const capability = createCodexCapabilityStore().attest({
213
+ token: capabilityToken,
214
+ sessionId: payloadText(payload, 'sessionId'),
215
+ turnId: payloadText(payload, 'turnId'),
216
+ toolUseId: payloadText(payload, 'toolUseId'),
217
+ hookDefinitionHash: payloadText(payload, 'hookDefinitionHash'),
218
+ buildIdentity: computeLifecycleBuildIdentity(process.cwd()),
219
+ controller: controllerBinding(armed.taskId)
220
+ });
221
+ output({
222
+ status: capability.status,
223
+ changed: true,
224
+ evidence: {
225
+ revision: capability.revision,
226
+ sessionId: capability.sessionId,
227
+ turnId: capability.turnId,
228
+ toolUseId: capability.toolUseId,
229
+ expiresAt: capability.expiresAt
230
+ },
231
+ diagnostics: [],
232
+ error: null
233
+ });
234
+ return;
235
+ }
236
+ }
237
+ if (phase === 'post-tool' && parsed.values['--bridge'] === 'true') {
238
+ const toolName = payloadText(payload, 'toolName');
239
+ if (toolName === 'collaborationspawn_agent') {
240
+ const nativeAgent = payloadText(payload, 'nativeAgent');
241
+ if (!MANAGED_AGENT.test(nativeAgent)) {
242
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
243
+ return;
244
+ }
245
+ const requestedModel = payloadText(payload, 'requestedModel');
246
+ const requestedReasoningEffort = payloadText(payload, 'requestedReasoningEffort');
247
+ const bridged = await activateCodexSpawnDelegation({
248
+ sessionId: payloadText(payload, 'sessionId'),
249
+ turnId: payloadText(payload, 'turnId'),
250
+ toolUseId: payloadText(payload, 'toolUseId'),
251
+ transcriptPath: payloadText(payload, 'transcriptPath'),
252
+ nativeAgent,
253
+ taskName: payloadText(payload, 'taskName'),
254
+ ...(requestedModel ? { requestedModel } : {}),
255
+ ...(requestedReasoningEffort ? { requestedReasoningEffort } : {})
256
+ }, { store });
257
+ outputBridgeResult(bridged);
258
+ return;
259
+ }
260
+ if (toolName === 'collaborationwait_agent') {
261
+ outputBridgeResult(await sealCodexParentDelegation(payloadText(payload, 'sessionId'), { store }));
262
+ return;
263
+ }
264
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
265
+ return;
266
+ }
267
+ const event = recordFromPayload(phase, payload);
150
268
  if (!event) {
269
+ if (phase === 'post-tool' && parsed.values['--bridge'] === 'true') {
270
+ const value = payload;
271
+ const childThreadId = String(value.childThreadId ?? '');
272
+ const reconciled = reconcileCodexOrchestrationDelegation(childThreadId);
273
+ outputBridgeResult(reconciled);
274
+ return;
275
+ }
151
276
  output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
152
277
  return;
153
278
  }
279
+ if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
280
+ try {
281
+ if (store.read(event.childThreadId).consumer) {
282
+ const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
283
+ outputBridgeResult(bridged);
284
+ return;
285
+ }
286
+ }
287
+ catch {
288
+ // The normal apply path below owns missing or invalid evidence errors.
289
+ }
290
+ }
291
+ if (parsed.values['--bridge'] === 'true' && event.type === 'hook-child') {
292
+ const resolved = await resolveCodexThread(event.childThreadId);
293
+ store.apply({
294
+ ...event,
295
+ parentThreadId: resolved.resolution.thread.parentThreadId
296
+ });
297
+ const bridged = await activateCodexOrchestrationDelegation(event.childThreadId, {
298
+ store,
299
+ resolveThread: async () => resolved
300
+ });
301
+ outputBridgeResult(bridged);
302
+ return;
303
+ }
304
+ if (event.type === 'hook-child') {
305
+ const resolved = await resolveCodexThread(event.childThreadId);
306
+ const result = store.apply({
307
+ ...event,
308
+ parentThreadId: resolved.resolution.thread.parentThreadId
309
+ });
310
+ output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: resolved.diagnostics, error: result.state.error });
311
+ if (result.state.status === 'invalid')
312
+ process.exitCode = 1;
313
+ return;
314
+ }
154
315
  const result = store.apply(event);
316
+ if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
317
+ const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
318
+ outputBridgeResult(bridged);
319
+ return;
320
+ }
155
321
  output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: [], error: result.state.error });
156
322
  if (result.state.status === 'invalid')
157
323
  process.exitCode = 1;
@@ -0,0 +1,106 @@
1
+ import { runCodexSandboxController, verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.js';
2
+ const USAGE = 'Usage: agent-infra-internal codex-sandbox-controller <run|verify-context> [options]\n';
3
+ function fail(code, message, exitCode = 1) {
4
+ process.stdout.write(`${JSON.stringify({
5
+ status: 'failed',
6
+ changed: false,
7
+ error: { code, message }
8
+ })}\n`);
9
+ process.exitCode = exitCode;
10
+ }
11
+ function parse(args) {
12
+ if (args[0] === '--help' || args[0] === '-h') {
13
+ process.stdout.write(USAGE);
14
+ return null;
15
+ }
16
+ const operation = args[0];
17
+ if (!operation || !['run', 'verify-context'].includes(operation)) {
18
+ process.stderr.write(USAGE);
19
+ fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', 'operation must be run or verify-context', 2);
20
+ return null;
21
+ }
22
+ const allowed = new Set([
23
+ '--task-id',
24
+ '--task-ref',
25
+ '--context',
26
+ '--executor-model',
27
+ '--executor-reasoning-effort',
28
+ '--reviewer-model',
29
+ '--reviewer-reasoning-effort'
30
+ ]);
31
+ const values = {};
32
+ for (let index = 1; index < args.length; index += 1) {
33
+ const flag = args[index];
34
+ const value = args[++index];
35
+ if (!allowed.has(flag) || Object.hasOwn(values, flag) || !value || value.startsWith('--')) {
36
+ fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', `invalid option '${flag}'`, 2);
37
+ return null;
38
+ }
39
+ values[flag] = value;
40
+ }
41
+ return { operation, values };
42
+ }
43
+ async function codexSandboxController(args = []) {
44
+ const parsed = parse(args);
45
+ if (!parsed || process.exitCode)
46
+ return;
47
+ const taskId = parsed.values['--task-id'];
48
+ if (!taskId) {
49
+ fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', '--task-id is required', 2);
50
+ return;
51
+ }
52
+ try {
53
+ if (parsed.operation === 'verify-context') {
54
+ const contextPath = parsed.values['--context']
55
+ ?? process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
56
+ if (!contextPath)
57
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_MISSING');
58
+ const context = verifyCodexSandboxControllerContext(contextPath, taskId);
59
+ process.stdout.write(`${JSON.stringify({
60
+ status: 'ready',
61
+ changed: false,
62
+ context: {
63
+ taskId: context.taskId,
64
+ controlGeneration: context.controlGeneration,
65
+ controllerInstanceDigest: context.controllerInstanceDigest,
66
+ expiresAt: context.expiresAt,
67
+ buildIdentity: context.buildIdentity,
68
+ hookDefinitionHash: context.hookDefinitionHash,
69
+ lifecycleProfilesHash: context.lifecycleProfilesHash
70
+ },
71
+ error: null
72
+ })}\n`);
73
+ return;
74
+ }
75
+ const taskRef = parsed.values['--task-ref'];
76
+ if (!taskRef)
77
+ throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_REF_REQUIRED');
78
+ const policy = [
79
+ '--executor-model',
80
+ '--executor-reasoning-effort',
81
+ '--reviewer-model',
82
+ '--reviewer-reasoning-effort'
83
+ ];
84
+ if (policy.some((flag) => parsed.values[flag] !== undefined)
85
+ && policy.some((flag) => parsed.values[flag] === undefined)) {
86
+ throw new Error('CODEX_SANDBOX_CONTROLLER_MODEL_POLICY_INVALID');
87
+ }
88
+ const exitCode = await runCodexSandboxController({
89
+ taskId,
90
+ taskRef,
91
+ executorModel: parsed.values['--executor-model'],
92
+ executorReasoningEffort: parsed.values['--executor-reasoning-effort'],
93
+ reviewerModel: parsed.values['--reviewer-model'],
94
+ reviewerReasoningEffort: parsed.values['--reviewer-reasoning-effort']
95
+ });
96
+ process.exitCode = exitCode;
97
+ }
98
+ catch (error) {
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ const code = /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1]
101
+ ?? 'CODEX_SANDBOX_CONTROLLER_FAILED';
102
+ fail(code, message);
103
+ }
104
+ }
105
+ export { codexSandboxController };
106
+ //# sourceMappingURL=codex-sandbox-controller.js.map