@fitlab-ai/agent-infra 0.9.8 → 0.9.10

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 (182) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +11 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +58 -21
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +25 -11
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +127 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +63 -92
  10. package/dist/lib/agent-clients/adapters/codex-sandbox.js +72 -11
  11. package/dist/lib/agent-clients/adapters/index.js +3 -1
  12. package/dist/lib/agent-clients/adapters/traecli.js +66 -0
  13. package/dist/lib/agent-clients/tokens.js +2 -2
  14. package/dist/lib/agent-clients/types.js +2 -1
  15. package/dist/lib/defaults.json +5 -0
  16. package/dist/lib/git/workflow.js +10 -1
  17. package/dist/lib/internal/codex-lifecycle.js +20 -6
  18. package/dist/lib/internal/codex-sandbox-controller.js +9 -15
  19. package/dist/lib/internal/git-workflow.js +5 -7
  20. package/dist/lib/internal/platform-pr.js +20 -7
  21. package/dist/lib/internal/release-workflow.js +2 -1
  22. package/dist/lib/internal/sandbox-control.js +79 -2
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-orchestration.js +156 -291
  25. package/dist/lib/{task/commands/validate.js → internal/task-validate.js} +75 -42
  26. package/dist/lib/platform/pr-summary.js +48 -10
  27. package/dist/lib/platform/pull-requests.js +205 -41
  28. package/dist/lib/run/tui.js +5 -2
  29. package/dist/lib/sandbox/commands/rm.js +316 -55
  30. package/dist/lib/sandbox/control/client.js +190 -6
  31. package/dist/lib/sandbox/control/controller-registration.js +275 -0
  32. package/dist/lib/sandbox/control/executor.js +140 -5
  33. package/dist/lib/sandbox/control/lifecycle.js +3 -1
  34. package/dist/lib/sandbox/control/protocol.js +67 -6
  35. package/dist/lib/sandbox/index.js +5 -3
  36. package/dist/lib/sandbox/managed-fs.js +5 -0
  37. package/dist/lib/sandbox/task-resolver.js +50 -6
  38. package/dist/lib/sandbox/workspace-identity.js +27 -0
  39. package/dist/lib/sandbox/worktree-safety.js +215 -2
  40. package/dist/lib/task/codex-orchestration.js +161 -39
  41. package/dist/lib/task/commands/status.js +59 -1
  42. package/dist/lib/task/commit-operation.js +335 -0
  43. package/dist/lib/task/commit-policy.js +18 -0
  44. package/dist/lib/task/delegation-receipts.js +248 -6
  45. package/dist/lib/task/finalization.js +593 -0
  46. package/dist/lib/task/index.js +0 -9
  47. package/dist/lib/task/operation-outcome.js +33 -0
  48. package/dist/lib/task/orchestration.js +187 -761
  49. package/dist/lib/task/short-id.js +59 -1
  50. package/dist/lib/task/task-execution-lock.js +6 -1
  51. package/dist/lib/task/verification-engine.js +110 -60
  52. package/dist/lib/task/verification.js +2 -1
  53. package/dist/lib/task/workflow-warning-intents.js +50 -1
  54. package/dist/lifecycle-build-manifest.json +3 -3
  55. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
  56. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +37 -10
  57. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
  58. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +163 -0
  59. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  60. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  61. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +91 -118
  62. package/lib/agent-clients/adapters/codex-sandbox.ts +89 -14
  63. package/lib/agent-clients/adapters/index.ts +3 -1
  64. package/lib/agent-clients/adapters/traecli.ts +67 -0
  65. package/lib/agent-clients/tokens.ts +2 -2
  66. package/lib/agent-clients/types.ts +2 -1
  67. package/lib/defaults.json +5 -0
  68. package/lib/git/workflow.ts +11 -2
  69. package/lib/internal/codex-lifecycle.ts +19 -6
  70. package/lib/internal/codex-sandbox-controller.ts +9 -14
  71. package/lib/internal/git-workflow.ts +5 -6
  72. package/lib/internal/platform-pr.ts +14 -7
  73. package/lib/internal/release-workflow.ts +2 -1
  74. package/lib/internal/sandbox-control.ts +92 -2
  75. package/lib/internal/task-finalization.ts +68 -0
  76. package/lib/internal/task-orchestration.ts +20 -103
  77. package/lib/{task/commands/validate.ts → internal/task-validate.ts} +75 -40
  78. package/lib/platform/pr-summary.ts +55 -11
  79. package/lib/platform/pull-requests.ts +211 -41
  80. package/lib/run/tui.ts +5 -3
  81. package/lib/sandbox/commands/rm.ts +413 -61
  82. package/lib/sandbox/control/client.ts +255 -6
  83. package/lib/sandbox/control/controller-registration.ts +352 -0
  84. package/lib/sandbox/control/executor.ts +169 -5
  85. package/lib/sandbox/control/lifecycle.ts +2 -1
  86. package/lib/sandbox/control/protocol.ts +80 -7
  87. package/lib/sandbox/index.ts +5 -3
  88. package/lib/sandbox/managed-fs.ts +5 -0
  89. package/lib/sandbox/task-resolver.ts +58 -7
  90. package/lib/sandbox/workspace-identity.ts +50 -2
  91. package/lib/sandbox/workspace-view.ts +3 -3
  92. package/lib/sandbox/worktree-safety.ts +236 -2
  93. package/lib/task/codex-orchestration.ts +168 -40
  94. package/lib/task/commands/status.ts +78 -1
  95. package/lib/task/commit-operation.ts +430 -0
  96. package/lib/task/commit-policy.ts +30 -0
  97. package/lib/task/delegation-receipts.ts +263 -4
  98. package/lib/task/finalization.ts +761 -0
  99. package/lib/task/index.ts +0 -8
  100. package/lib/task/operation-outcome.ts +48 -0
  101. package/lib/task/orchestration.ts +218 -982
  102. package/lib/task/short-id.ts +65 -1
  103. package/lib/task/task-execution-lock.ts +10 -1
  104. package/lib/task/verification-engine.ts +115 -63
  105. package/lib/task/verification.ts +3 -2
  106. package/lib/task/workflow-warning-intents.ts +57 -2
  107. package/package.json +2 -2
  108. package/templates/.agents/QUICKSTART.en.md +3 -4
  109. package/templates/.agents/QUICKSTART.zh-CN.md +3 -4
  110. package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
  111. package/templates/.agents/rules/README.en.md +1 -0
  112. package/templates/.agents/rules/README.zh-CN.md +1 -0
  113. package/templates/.agents/rules/compatibility-policy.en.md +39 -0
  114. package/templates/.agents/rules/compatibility-policy.zh-CN.md +39 -0
  115. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -4
  116. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -4
  117. package/templates/.agents/rules/pr-sync.en.md +1 -1
  118. package/templates/.agents/rules/pr-sync.github.en.md +2 -1
  119. package/templates/.agents/rules/pr-sync.github.zh-CN.md +2 -0
  120. package/templates/.agents/rules/pr-sync.zh-CN.md +1 -0
  121. package/templates/.agents/rules/review-method.en.md +1 -0
  122. package/templates/.agents/rules/review-method.zh-CN.md +1 -0
  123. package/templates/.agents/rules/task-management.en.md +2 -2
  124. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  125. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
  126. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
  127. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  128. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  129. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  130. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  131. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  132. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  133. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  134. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  135. package/templates/.agents/skills/code-task/SKILL.en.md +1 -0
  136. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
  137. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  138. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  139. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  140. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  141. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  142. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  143. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  144. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  145. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  146. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  147. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  148. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  149. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  150. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  151. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  152. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  153. package/templates/.agents/skills/create-pr/SKILL.en.md +17 -9
  154. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -9
  155. package/templates/.agents/skills/create-pr/reference/change-report.en.md +41 -0
  156. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +41 -0
  157. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  158. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  159. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -1
  160. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -1
  161. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +8 -6
  162. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +8 -6
  163. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +3 -3
  164. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +3 -3
  165. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +1 -1
  166. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +1 -1
  167. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  168. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  169. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  170. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  171. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  172. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  173. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +24 -0
  174. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  175. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  176. package/templates/.codex/hooks.json +4 -4
  177. package/templates/.trae/skills/update-agent-infra.en.md +8 -0
  178. package/templates/.trae/skills/update-agent-infra.zh-CN.md +8 -0
  179. package/dist/lib/task/commit-finalization.js +0 -305
  180. package/dist/lib/task/commit-intent.js +0 -165
  181. package/lib/task/commit-finalization.ts +0 -419
  182. package/lib/task/commit-intent.ts +0 -230
@@ -1,7 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
+ import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import { runProbe } from './shell.ts';
6
+ import { resolveSandboxTarget } from './workspace-identity.ts';
5
7
 
6
8
  type WorktreeChange = {
7
9
  indexStatus: string;
@@ -16,6 +18,8 @@ type WorktreeSnapshot = {
16
18
  head: string;
17
19
  changes: readonly WorktreeChange[];
18
20
  identity: string;
21
+ source?: 'registered' | 'recovered';
22
+ recovery?: WorktreeRecoveryContext;
19
23
  };
20
24
 
21
25
  type WorktreeInspection =
@@ -28,6 +32,14 @@ type WorktreeRemovalPermit = {
28
32
  snapshot: WorktreeSnapshot;
29
33
  };
30
34
 
35
+ type WorktreeRecoveryContext = Readonly<{
36
+ repoRoot: string;
37
+ worktreeBase: string;
38
+ branch: string;
39
+ identitySource: 'branch-only' | 'task-bound';
40
+ taskId: string | null;
41
+ }>;
42
+
31
43
  type Probe = typeof runProbe;
32
44
 
33
45
  function probeText(probe: Probe, cwd: string, args: string[]): string {
@@ -164,6 +176,203 @@ function captureOnce(worktree: string, probe: Probe): WorktreeSnapshot {
164
176
  return { worktree: resolvedWorktree, branch, head, changes, identity: hash.digest('hex') };
165
177
  }
166
178
 
179
+ function probeTextWithEnv(
180
+ probe: Probe,
181
+ repoRoot: string,
182
+ args: string[],
183
+ env: NodeJS.ProcessEnv
184
+ ): string {
185
+ const result = probe('git', args, { cwd: repoRoot, encoding: 'utf8', env });
186
+ if (result.error) throw result.error;
187
+ if (result.status !== 0) {
188
+ const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : result.stderr.toString('utf8').trim();
189
+ throw new Error(stderr || `git ${args[0] ?? 'command'} failed with exit code ${result.status ?? 'unknown'}`);
190
+ }
191
+ return typeof result.stdout === 'string' ? result.stdout : result.stdout.toString('utf8');
192
+ }
193
+
194
+ function probeMetadataCommand(
195
+ probe: Probe,
196
+ repoRoot: string,
197
+ args: string[],
198
+ env: NodeJS.ProcessEnv
199
+ ): boolean {
200
+ const result = probe('git', args, { cwd: repoRoot, encoding: 'utf8', env });
201
+ if (result.error) throw result.error;
202
+ return result.status === 0;
203
+ }
204
+
205
+ function hasValidHeadContent(content: string): boolean {
206
+ return /^(?:ref:\s+refs\/\S+|[0-9a-f]{40}|[0-9a-f]{64})$/i.test(content.trim());
207
+ }
208
+
209
+ function assertRecoverableMetadata(worktree: string, repoRoot: string, probe: Probe): void {
210
+ const dotGit = path.join(worktree, '.git');
211
+ let dotGitStat: fs.Stats;
212
+ try {
213
+ dotGitStat = fs.lstatSync(dotGit);
214
+ } catch (error) {
215
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
216
+ throw error;
217
+ }
218
+ if (dotGitStat.isSymbolicLink() || dotGitStat.isDirectory()) {
219
+ throw new Error('WORKTREE_RECOVERY_METADATA_NOT_ELIGIBLE');
220
+ }
221
+ if (!dotGitStat.isFile()) throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
222
+
223
+ const content = fs.readFileSync(dotGit, 'utf8').trim();
224
+ const match = /^gitdir:\s*(.+)$/i.exec(content);
225
+ if (!match) throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
226
+
227
+ const commonDir = fs.realpathSync.native(path.resolve(repoRoot, probeText(probe, repoRoot, [
228
+ 'rev-parse', '--git-common-dir'
229
+ ]).trim()));
230
+ const adminRoot = path.join(commonDir, 'worktrees');
231
+ const adminPath = path.resolve(path.dirname(dotGit), match[1]!);
232
+ const relative = path.relative(adminRoot, adminPath);
233
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative) || relative.includes(path.sep)) {
234
+ throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
235
+ }
236
+
237
+ if (!fs.existsSync(adminPath)) return;
238
+ const adminStat = fs.lstatSync(adminPath);
239
+ if (adminStat.isSymbolicLink()) throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
240
+ if (!adminStat.isDirectory()) return;
241
+
242
+ const requiredFiles = ['HEAD', 'gitdir', 'commondir', 'index'];
243
+ if (requiredFiles.some((name) => !fs.existsSync(path.join(adminPath, name)))) return;
244
+ for (const name of requiredFiles) {
245
+ const filePath = path.join(adminPath, name);
246
+ const stat = fs.lstatSync(filePath);
247
+ if (stat.isSymbolicLink() || !stat.isFile()) throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
248
+ }
249
+
250
+ const adminGitdir = fs.readFileSync(path.join(adminPath, 'gitdir'), 'utf8').trim();
251
+ const adminCommonDir = fs.readFileSync(path.join(adminPath, 'commondir'), 'utf8').trim();
252
+ const adminGitdirPath = adminGitdir
253
+ ? fs.realpathSync.native(path.resolve(adminPath, adminGitdir))
254
+ : '';
255
+ const canonicalDotGit = fs.realpathSync.native(dotGit);
256
+ const adminCommonDirPath = adminCommonDir
257
+ ? fs.realpathSync.native(path.resolve(adminPath, adminCommonDir))
258
+ : '';
259
+ if (!adminGitdir || adminGitdirPath !== canonicalDotGit
260
+ || !adminCommonDir || adminCommonDirPath !== commonDir) {
261
+ throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
262
+ }
263
+
264
+ const headContent = fs.readFileSync(path.join(adminPath, 'HEAD'), 'utf8');
265
+ const indexContent = fs.readFileSync(path.join(adminPath, 'index'));
266
+ if (!hasValidHeadContent(headContent) || indexContent.subarray(0, 4).toString('ascii') !== 'DIRC') return;
267
+
268
+ const validationEnv: NodeJS.ProcessEnv = { ...process.env };
269
+ for (const key of ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE', 'GIT_WORK_TREE', 'GIT_PREFIX']) {
270
+ delete validationEnv[key];
271
+ }
272
+ validationEnv.GIT_DIR = adminPath;
273
+ validationEnv.GIT_WORK_TREE = path.resolve(worktree);
274
+ if (!probeMetadataCommand(probe, repoRoot, ['rev-parse', '--verify', 'HEAD^{commit}'], validationEnv)) return;
275
+ if (!probeMetadataCommand(probe, repoRoot, ['ls-files', '--stage', '-z'], validationEnv)) return;
276
+ throw new Error('WORKTREE_RECOVERY_METADATA_REGISTERED');
277
+ }
278
+
279
+ function parseRecoveredStatus(raw: string): WorktreeChange[] {
280
+ const records = raw.split('\0');
281
+ const changes: WorktreeChange[] = [];
282
+ for (let index = 0; index < records.length;) {
283
+ const status = records[index++] ?? '';
284
+ if (!status) continue;
285
+ const code = status[0] ?? '';
286
+ if (code === 'R' || code === 'C') {
287
+ const originalPath = records[index++];
288
+ const nextPath = records[index++];
289
+ if (!originalPath || !nextPath) throw new Error('Unable to parse recovered Git rename record');
290
+ changes.push({ indexStatus: code, worktreeStatus: ' ', path: nextPath, originalPath });
291
+ continue;
292
+ }
293
+ const filePath = records[index++];
294
+ if (!filePath) throw new Error('Unable to parse recovered Git status record');
295
+ changes.push({
296
+ indexStatus: code === 'D' ? ' ' : code,
297
+ worktreeStatus: code === 'D' ? 'D' : ' ',
298
+ path: filePath
299
+ });
300
+ }
301
+ return changes;
302
+ }
303
+
304
+ function captureRecoveredOnce(
305
+ worktree: string,
306
+ recovery: WorktreeRecoveryContext,
307
+ probe: Probe
308
+ ): WorktreeSnapshot {
309
+ const resolvedWorktree = path.resolve(worktree);
310
+ const resolvedBase = path.resolve(recovery.worktreeBase);
311
+ const relative = path.relative(resolvedBase, resolvedWorktree);
312
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
313
+ throw new Error(`WORKTREE_RECOVERY_PATH_INVALID: ${resolvedWorktree}`);
314
+ }
315
+ const stat = fs.lstatSync(resolvedWorktree);
316
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('WORKTREE_RECOVERY_PATH_INVALID');
317
+ const canonicalBase = fs.realpathSync.native(resolvedBase);
318
+ const canonicalWorktree = fs.realpathSync.native(resolvedWorktree);
319
+ const canonicalRelative = path.relative(canonicalBase, canonicalWorktree);
320
+ if (!canonicalRelative || canonicalRelative.startsWith('..') || path.isAbsolute(canonicalRelative)) {
321
+ throw new Error('WORKTREE_RECOVERY_PATH_INVALID');
322
+ }
323
+ const resolvedWorkspace = resolveSandboxTarget(recovery.branch, recovery.repoRoot).workspace;
324
+ if (recovery.identitySource === 'branch-only' && resolvedWorkspace.mode !== 'branch-only') {
325
+ throw new Error('WORKTREE_RECOVERY_IDENTITY_CHANGED');
326
+ }
327
+ if (recovery.identitySource === 'task-bound'
328
+ && (resolvedWorkspace.mode !== 'task-bound' || resolvedWorkspace.taskId !== recovery.taskId)) {
329
+ throw new Error('WORKTREE_RECOVERY_IDENTITY_CHANGED');
330
+ }
331
+ assertRecoverableMetadata(resolvedWorktree, recovery.repoRoot, probe);
332
+
333
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-recovered-index-'));
334
+ const env: NodeJS.ProcessEnv = { ...process.env };
335
+ for (const key of ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_PREFIX']) delete env[key];
336
+ env.GIT_INDEX_FILE = path.join(tempDir, 'index');
337
+ env.GIT_WORK_TREE = resolvedWorktree;
338
+ try {
339
+ const baseline = probeTextWithEnv(probe, recovery.repoRoot, [
340
+ 'rev-parse', '--verify', `refs/heads/${recovery.branch}^{commit}`
341
+ ], env).trim();
342
+ if (!baseline) throw new Error(`WORKTREE_RECOVERY_BRANCH_INVALID: ${recovery.branch}`);
343
+ probeTextWithEnv(probe, recovery.repoRoot, ['read-tree', baseline], env);
344
+ probeTextWithEnv(probe, recovery.repoRoot, ['add', '-A', '--', '.'], env);
345
+ const status = probeTextWithEnv(probe, recovery.repoRoot, [
346
+ 'diff', '--cached', '--name-status', '--find-renames', '-z', '--', '.'
347
+ ], env);
348
+ const changes = parseRecoveredStatus(status);
349
+ const hash = createHash('sha256');
350
+ updatePart(hash, 'source', 'recovered');
351
+ updatePart(hash, 'worktree', resolvedWorktree);
352
+ updatePart(hash, 'repo-root', path.resolve(recovery.repoRoot));
353
+ updatePart(hash, 'branch', recovery.branch);
354
+ updatePart(hash, 'baseline', baseline);
355
+ updatePart(hash, 'identity-source', recovery.identitySource);
356
+ updatePart(hash, 'task-id', recovery.taskId ?? '');
357
+ updatePart(hash, 'status', status);
358
+ for (const change of changes) {
359
+ hashPath(hash, resolvedWorktree, change.path, probe);
360
+ if (change.originalPath) hashPath(hash, resolvedWorktree, change.originalPath, probe);
361
+ }
362
+ return {
363
+ worktree: resolvedWorktree,
364
+ branch: recovery.branch,
365
+ head: baseline,
366
+ changes,
367
+ identity: hash.digest('hex'),
368
+ source: 'recovered',
369
+ recovery: { ...recovery }
370
+ };
371
+ } finally {
372
+ fs.rmSync(tempDir, { recursive: true, force: true });
373
+ }
374
+ }
375
+
167
376
  function inspectWorktree(worktree: string, { probe = runProbe }: { probe?: Probe } = {}): WorktreeInspection {
168
377
  const resolvedWorktree = path.resolve(worktree);
169
378
  try {
@@ -182,6 +391,28 @@ function inspectWorktree(worktree: string, { probe = runProbe }: { probe?: Probe
182
391
  }
183
392
  }
184
393
 
394
+ function inspectRecoveredWorktree(
395
+ worktree: string,
396
+ recovery: WorktreeRecoveryContext,
397
+ { probe = runProbe }: { probe?: Probe } = {}
398
+ ): WorktreeInspection {
399
+ const resolvedWorktree = path.resolve(worktree);
400
+ try {
401
+ const first = captureRecoveredOnce(resolvedWorktree, recovery, probe);
402
+ const second = captureRecoveredOnce(resolvedWorktree, recovery, probe);
403
+ if (first.identity !== second.identity) {
404
+ return { status: 'failed', worktree: resolvedWorktree, message: 'Snapshot changed while recovering worktree' };
405
+ }
406
+ return { status: second.changes.length === 0 ? 'clean' : 'dirty', snapshot: second };
407
+ } catch (error) {
408
+ return {
409
+ status: 'failed',
410
+ worktree: resolvedWorktree,
411
+ message: error instanceof Error ? error.message : String(error)
412
+ };
413
+ }
414
+ }
415
+
185
416
  function inspectWorktrees(worktrees: readonly string[]): WorktreeInspection[] {
186
417
  return [...new Set(worktrees.map((worktree) => path.resolve(worktree)))].sort()
187
418
  .map((worktree) => inspectWorktree(worktree));
@@ -201,7 +432,9 @@ function verifyWorktreePermit(
201
432
  permit: WorktreeRemovalPermit,
202
433
  options: { probe?: Probe } = {}
203
434
  ): WorktreeSnapshot {
204
- const current = inspectWorktree(permit.snapshot.worktree, options);
435
+ const current = permit.snapshot.source === 'recovered' && permit.snapshot.recovery
436
+ ? inspectRecoveredWorktree(permit.snapshot.worktree, permit.snapshot.recovery, options)
437
+ : inspectWorktree(permit.snapshot.worktree, options);
205
438
  if (current.status === 'failed') throw new Error(`Unable to verify worktree permit: ${current.message}`);
206
439
  if (current.snapshot.identity !== permit.snapshot.identity || (permit.mode === 'clean' && current.status !== 'clean')) {
207
440
  throw new Error(`Worktree changed after authorization: ${permit.snapshot.worktree}`);
@@ -227,8 +460,9 @@ export {
227
460
  createCleanPermit,
228
461
  createDiscardPermit,
229
462
  formatWorktreeSnapshot,
463
+ inspectRecoveredWorktree,
230
464
  inspectWorktree,
231
465
  inspectWorktrees,
232
466
  verifyWorktreePermit
233
467
  };
234
- export type { WorktreeChange, WorktreeInspection, WorktreeRemovalPermit, WorktreeSnapshot };
468
+ export type { WorktreeChange, WorktreeInspection, WorktreeRecoveryContext, WorktreeRemovalPermit, WorktreeSnapshot };
@@ -1,5 +1,3 @@
1
- import fs from 'node:fs';
2
-
3
1
  import {
4
2
  preflightCodexLifecycleEvidence,
5
3
  resolveCodexSpawnedChild,
@@ -7,16 +5,28 @@ import {
7
5
  resolveCodexThread
8
6
  } from '../agent-clients/adapters/codex-lifecycle/app-server.ts';
9
7
  import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.ts';
10
- import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
11
- import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
12
- import type { LifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
13
- import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
8
+ import {
9
+ createCodexCapabilityStore,
10
+ isCodexCapabilityProvenanceDetail
11
+ } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
12
+ import type { CodexCapabilityProvenanceDetail } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
13
+ import {
14
+ computeLifecycleBuildIdentity,
15
+ verifyLifecycleBuildIdentity,
16
+ type LifecycleBuildIdentity,
17
+ type LifecycleIdentityWarning
18
+ } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
19
+ import { verifyCodexSandboxControllerContextWithWarnings as verifySandboxControllerWithWarnings } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
20
+ import {
21
+ verifyCodexSandboxControllerContextWithWarnings as verifyControllerContextFileWithWarnings
22
+ } from '../agent-clients/adapters/codex-lifecycle/controller-context.ts';
14
23
  import type { AgentClientId } from '../agent-clients/types.ts';
15
24
  import { resolveTaskRef } from './resolve-ref.ts';
16
25
  import {
17
26
  activateMatchingOrchestrationDelegation,
18
27
  hasActivatableOrchestrationDelegation,
19
28
  hasSealableOrchestrationDelegation,
29
+ OrchestrationStateError,
20
30
  pauseMatchingOrchestrationDelegation,
21
31
  prepareOrchestrationDelegation,
22
32
  reconcileMatchingOrchestrationDelegation,
@@ -52,10 +62,27 @@ function coreOptions(options: CodexBridgeOptions): OrchestrationOptions {
52
62
  return { ...options.orchestrationOptions, repoRoot: options.repoRoot ?? options.orchestrationOptions?.repoRoot };
53
63
  }
54
64
 
55
- function bridgeFailure(code: string, message: string): OrchestrationResult {
65
+ function bridgeFailure(code: string, message: string, detail?: CodexCapabilityProvenanceDetail): OrchestrationResult {
56
66
  return {
57
67
  status: 'failed', changed: false, taskId: null, run: null, next: null,
58
- error: { code, message }
68
+ error: { code, message, ...(detail ? { detail } : {}) }
69
+ };
70
+ }
71
+
72
+ function capabilityFailure(error: unknown): NonNullable<OrchestrationResult['error']> {
73
+ const detailValue = error instanceof Error
74
+ ? (error as Error & { detail?: unknown }).detail
75
+ : undefined;
76
+ return {
77
+ code: error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
78
+ ? error.name
79
+ : 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED',
80
+ message: error instanceof Error ? error.message : String(error),
81
+ ...(error instanceof Error
82
+ && error.name === 'CODEX_CAPABILITY_PROVENANCE_MISMATCH'
83
+ && isCodexCapabilityProvenanceDetail(detailValue)
84
+ ? { detail: detailValue }
85
+ : {})
59
86
  };
60
87
  }
61
88
 
@@ -71,11 +98,25 @@ function requiredStore(options: CodexBridgeOptions): LifecycleStore {
71
98
  return options.store;
72
99
  }
73
100
 
74
- function controllerContext(taskId: string, repoRoot: string) {
75
- const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
76
- return contextPath
77
- ? verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot })
78
- : null;
101
+ function brokerControllerBinding(): Readonly<{ instanceDigest: string; controlGeneration: string }> | null {
102
+ const raw = process.env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING;
103
+ if (!raw) return null;
104
+ let value: unknown;
105
+ try {
106
+ value = JSON.parse(raw);
107
+ } catch {
108
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BINDING_INVALID');
109
+ }
110
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
111
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BINDING_INVALID');
112
+ }
113
+ const binding = value as Record<string, unknown>;
114
+ if (Object.keys(binding).sort().join(',') !== 'controlGeneration,instanceDigest'
115
+ || typeof binding.instanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(binding.instanceDigest)
116
+ || typeof binding.controlGeneration !== 'string' || binding.controlGeneration.length === 0) {
117
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BINDING_INVALID');
118
+ }
119
+ return binding as { instanceDigest: string; controlGeneration: string };
79
120
  }
80
121
 
81
122
  async function prepareCodexOrchestrationDelegation(
@@ -101,36 +142,113 @@ async function prepareCodexOrchestrationDelegation(
101
142
  );
102
143
  }
103
144
  const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
104
- const controller = controllerContext(resolved.taskId, repoRoot);
145
+ const brokerController = brokerControllerBinding();
146
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
147
+ const contextVerification = contextPath
148
+ ? (brokerController
149
+ ? verifyControllerContextFileWithWarnings(contextPath, {
150
+ repoRoot,
151
+ generation: process.env.AGENT_INFRA_CONTROL_GENERATION ?? brokerController.controlGeneration
152
+ })
153
+ : verifySandboxControllerWithWarnings(contextPath, { repoRoot }))
154
+ : null;
155
+ const identityWarnings: LifecycleIdentityWarning[] = [...(contextVerification?.warnings ?? [])];
156
+ const localController = contextVerification?.context ?? null;
157
+ if (localController && brokerController
158
+ && (localController.controllerInstanceDigest !== brokerController.instanceDigest
159
+ || localController.controlGeneration !== brokerController.controlGeneration)) {
160
+ return bridgeFailure(
161
+ 'CODEX_SANDBOX_CONTROLLER_BINDING_MISMATCH',
162
+ 'broker and local controller bindings do not match'
163
+ );
164
+ }
165
+ const controller = brokerController ?? (localController ? {
166
+ instanceDigest: localController.controllerInstanceDigest,
167
+ controlGeneration: localController.controlGeneration
168
+ } : null);
105
169
  const capabilityStore = options.capabilityStore ?? createCodexCapabilityStore();
106
- const consumed = capabilityStore.consume(input.capabilityToken, {
170
+ const attested = capabilityStore.inspect(input.capabilityToken);
171
+ const capabilityIdentity = verifyLifecycleBuildIdentity(attested.buildIdentity, buildIdentity);
172
+ if (!capabilityIdentity.ok) {
173
+ return bridgeFailure(capabilityIdentity.code!, capabilityIdentity.message!);
174
+ }
175
+ identityWarnings.push(...capabilityIdentity.warnings);
176
+ const lifecycleProvenance = {
177
+ ...attested.buildIdentity,
178
+ hookDefinitionHash: preflight.hookDefinitionHash,
179
+ ...preflight.hookProvenance,
180
+ capabilitySessionId: attested.sessionId!,
181
+ capabilityTurnId: attested.turnId!,
182
+ capabilityToolUseId: attested.toolUseId!,
183
+ controllerInstanceDigest: controller?.instanceDigest ?? null,
184
+ controlGeneration: controller?.controlGeneration ?? null
185
+ };
186
+ const capabilityExpected = {
107
187
  taskId: resolved.taskId,
108
188
  hookDefinitionHash: preflight.hookDefinitionHash,
109
189
  buildIdentity,
110
190
  ...(controller ? { controller: {
111
- instanceDigest: controller.controllerInstanceDigest,
191
+ instanceDigest: controller.instanceDigest,
112
192
  controlGeneration: controller.controlGeneration
113
193
  } } : {})
114
- });
115
- return prepareOrchestrationDelegation(taskRef, {
194
+ };
195
+ const prepared = prepareOrchestrationDelegation(taskRef, {
116
196
  ...input,
117
- lifecycleProvenance: {
118
- ...consumed.buildIdentity,
119
- hookDefinitionHash: preflight.hookDefinitionHash,
120
- ...preflight.hookProvenance,
121
- capabilitySessionId: consumed.sessionId!,
122
- capabilityTurnId: consumed.turnId!,
123
- capabilityToolUseId: consumed.toolUseId!,
124
- controllerInstanceDigest: controller?.controllerInstanceDigest ?? null,
125
- controlGeneration: controller?.controlGeneration ?? null
197
+ lifecycleProvenance
198
+ }, {
199
+ ...coreOptions(options),
200
+ validateLifecycleCapability: () => {
201
+ try {
202
+ const validated = capabilityStore.validate(input.capabilityToken!, capabilityExpected);
203
+ if (validated.sessionId !== attested.sessionId
204
+ || validated.turnId !== attested.turnId
205
+ || validated.toolUseId !== attested.toolUseId) {
206
+ return {
207
+ code: 'CODEX_CAPABILITY_IDENTITY_CHANGED',
208
+ message: 'capability identity changed during validation'
209
+ };
210
+ }
211
+ return null;
212
+ } catch (error) {
213
+ return capabilityFailure(error);
214
+ }
215
+ },
216
+ consumeLifecycleCapability: () => {
217
+ try {
218
+ const consumed = capabilityStore.consume(input.capabilityToken!, capabilityExpected);
219
+ if (consumed.sessionId !== attested.sessionId
220
+ || consumed.turnId !== attested.turnId
221
+ || consumed.toolUseId !== attested.toolUseId) {
222
+ return {
223
+ code: 'CODEX_CAPABILITY_IDENTITY_CHANGED',
224
+ message: 'capability identity changed during consumption'
225
+ };
226
+ }
227
+ return null;
228
+ } catch (error) {
229
+ return capabilityFailure(error);
230
+ }
126
231
  }
127
- }, coreOptions(options));
232
+ });
233
+ return identityWarnings.length > 0 && prepared.status !== 'failed'
234
+ ? { ...prepared, warnings: Object.freeze(identityWarnings) }
235
+ : prepared;
128
236
  } catch (error) {
237
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
238
+ const detailValue = error instanceof Error
239
+ ? (error as Error & { detail?: unknown }).detail
240
+ : undefined;
241
+ const detail = error instanceof Error
242
+ && error.name === 'CODEX_CAPABILITY_PROVENANCE_MISMATCH'
243
+ && isCodexCapabilityProvenanceDetail(detailValue)
244
+ ? detailValue
245
+ : undefined;
129
246
  return bridgeFailure(
130
247
  error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
131
248
  ? error.name
132
249
  : 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED',
133
- error instanceof Error ? error.message : String(error)
250
+ error instanceof Error ? error.message : String(error),
251
+ detail
134
252
  );
135
253
  }
136
254
  }
@@ -161,16 +279,11 @@ async function activateCodexOrchestrationDelegation(
161
279
  });
162
280
  const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
163
281
  const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
164
- const contextTaskId = contextPath
165
- ? (JSON.parse(fs.readFileSync(contextPath, 'utf8')) as { taskId?: unknown }).taskId
282
+ const contextVerification = contextPath
283
+ ? verifySandboxControllerWithWarnings(contextPath, { repoRoot })
166
284
  : null;
167
- if (contextPath && typeof contextTaskId !== 'string') {
168
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
169
- }
170
- const controller = contextPath
171
- ? verifyCodexSandboxControllerContext(contextPath, contextTaskId as string, { repoRoot })
172
- : null;
173
- return activateMatchingOrchestrationDelegation('codex', {
285
+ const controller = contextVerification?.context ?? null;
286
+ const activated = activateMatchingOrchestrationDelegation('codex', {
174
287
  nativeAgent: evidence.nativeAgent,
175
288
  childId: evidence.childThreadId,
176
289
  parentId: evidence.parentThreadId,
@@ -195,7 +308,12 @@ async function activateCodexOrchestrationDelegation(
195
308
  controlGeneration: controller?.controlGeneration ?? null
196
309
  }
197
310
  }, coreOptions(options));
311
+ const warnings = contextVerification?.warnings ?? [];
312
+ return warnings.length > 0 && activated.status !== 'failed'
313
+ ? { ...activated, warnings: Object.freeze(warnings) }
314
+ : activated;
198
315
  } catch (error) {
316
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
199
317
  return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
200
318
  }
201
319
  }
@@ -223,6 +341,7 @@ async function activateCodexSpawnDelegation(
223
341
  resolveThread: async () => resolved
224
342
  });
225
343
  } catch (error) {
344
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
226
345
  return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
227
346
  }
228
347
  }
@@ -238,7 +357,11 @@ async function sealCodexOrchestrationDelegation(
238
357
  const store = requiredStore(options);
239
358
  const existing = store.read(childThreadId);
240
359
  if (!existing.consumer) {
241
- store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(childThreadId));
360
+ const stopTurnId = existing.state.stop?.turnId;
361
+ if (!stopTurnId) {
362
+ return pauseBridge('ORCHESTRATION_CODEX_STOP_EVIDENCE_INVALID', 'Codex lifecycle stop hook is not available', options);
363
+ }
364
+ store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(childThreadId, stopTurnId));
242
365
  }
243
366
  const record = store.read(childThreadId);
244
367
  const evidence = record.state.stopEvidence;
@@ -259,6 +382,7 @@ async function sealCodexOrchestrationDelegation(
259
382
  coreOptions(options)
260
383
  );
261
384
  } catch (error) {
385
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
262
386
  return pauseBridge('ORCHESTRATION_CODEX_STOP_FAILED', error instanceof Error ? error.message : String(error), options);
263
387
  }
264
388
  }
@@ -280,7 +404,10 @@ async function sealCodexParentDelegation(
280
404
  const start = active.state.startEvidence!;
281
405
  const child = active.state.child!;
282
406
  if (!active.state.terminal) {
283
- store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(start.childThreadId));
407
+ const resolveTerminal = options.resolveTerminal ?? resolveCodexTerminal;
408
+ store.apply(active.state.stop
409
+ ? await resolveTerminal(start.childThreadId, active.state.stop.turnId)
410
+ : await resolveTerminal(start.childThreadId));
284
411
  }
285
412
  if (!store.read(start.childThreadId).state.stop) {
286
413
  store.apply({
@@ -294,6 +421,7 @@ async function sealCodexParentDelegation(
294
421
  }
295
422
  return sealCodexOrchestrationDelegation(start.childThreadId, options);
296
423
  } catch (error) {
424
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
297
425
  if (error instanceof Error && error.message === 'CODEX_TURN_NOT_TERMINAL') {
298
426
  return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'The matching Codex child has not completed');
299
427
  }