@fitlab-ai/agent-infra 0.9.8 → 0.9.9

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 (133) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +6 -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 +8 -1
  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 +147 -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 +56 -67
  10. package/dist/lib/git/workflow.js +10 -1
  11. package/dist/lib/internal/codex-lifecycle.js +19 -5
  12. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  13. package/dist/lib/internal/git-workflow.js +5 -7
  14. package/dist/lib/internal/platform-pr.js +20 -7
  15. package/dist/lib/internal/release-workflow.js +2 -1
  16. package/dist/lib/internal/sandbox-control.js +79 -2
  17. package/dist/lib/internal/task-finalization.js +54 -0
  18. package/dist/lib/internal/task-orchestration.js +156 -291
  19. package/dist/lib/platform/pr-summary.js +48 -10
  20. package/dist/lib/platform/pull-requests.js +205 -41
  21. package/dist/lib/sandbox/commands/rm.js +316 -55
  22. package/dist/lib/sandbox/control/client.js +190 -6
  23. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  24. package/dist/lib/sandbox/control/executor.js +137 -5
  25. package/dist/lib/sandbox/control/lifecycle.js +3 -1
  26. package/dist/lib/sandbox/control/protocol.js +67 -6
  27. package/dist/lib/sandbox/index.js +5 -3
  28. package/dist/lib/sandbox/managed-fs.js +5 -0
  29. package/dist/lib/sandbox/task-resolver.js +50 -6
  30. package/dist/lib/sandbox/workspace-identity.js +27 -0
  31. package/dist/lib/sandbox/worktree-safety.js +215 -2
  32. package/dist/lib/task/codex-orchestration.js +158 -33
  33. package/dist/lib/task/commands/status.js +59 -1
  34. package/dist/lib/task/commit-operation.js +335 -0
  35. package/dist/lib/task/commit-policy.js +18 -0
  36. package/dist/lib/task/delegation-receipts.js +243 -2
  37. package/dist/lib/task/finalization.js +567 -0
  38. package/dist/lib/task/operation-outcome.js +33 -0
  39. package/dist/lib/task/orchestration.js +187 -761
  40. package/dist/lib/task/short-id.js +59 -1
  41. package/dist/lib/task/task-execution-lock.js +6 -1
  42. package/dist/lib/task/verification-engine.js +113 -60
  43. package/dist/lib/task/verification.js +2 -1
  44. package/dist/lib/task/workflow-warning-intents.js +50 -1
  45. package/dist/lifecycle-build-manifest.json +3 -3
  46. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
  47. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  48. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
  49. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  51. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  52. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +83 -91
  53. package/lib/git/workflow.ts +11 -2
  54. package/lib/internal/codex-lifecycle.ts +18 -5
  55. package/lib/internal/codex-sandbox-controller.ts +6 -12
  56. package/lib/internal/git-workflow.ts +5 -6
  57. package/lib/internal/platform-pr.ts +14 -7
  58. package/lib/internal/release-workflow.ts +2 -1
  59. package/lib/internal/sandbox-control.ts +92 -2
  60. package/lib/internal/task-finalization.ts +68 -0
  61. package/lib/internal/task-orchestration.ts +20 -103
  62. package/lib/platform/pr-summary.ts +55 -11
  63. package/lib/platform/pull-requests.ts +211 -41
  64. package/lib/sandbox/commands/rm.ts +413 -61
  65. package/lib/sandbox/control/client.ts +255 -6
  66. package/lib/sandbox/control/controller-registration.ts +347 -0
  67. package/lib/sandbox/control/executor.ts +163 -5
  68. package/lib/sandbox/control/lifecycle.ts +2 -1
  69. package/lib/sandbox/control/protocol.ts +80 -7
  70. package/lib/sandbox/index.ts +5 -3
  71. package/lib/sandbox/managed-fs.ts +5 -0
  72. package/lib/sandbox/task-resolver.ts +58 -7
  73. package/lib/sandbox/workspace-identity.ts +50 -2
  74. package/lib/sandbox/workspace-view.ts +3 -3
  75. package/lib/sandbox/worktree-safety.ts +236 -2
  76. package/lib/task/codex-orchestration.ts +167 -33
  77. package/lib/task/commands/status.ts +78 -1
  78. package/lib/task/commit-operation.ts +430 -0
  79. package/lib/task/commit-policy.ts +30 -0
  80. package/lib/task/delegation-receipts.ts +258 -1
  81. package/lib/task/finalization.ts +719 -0
  82. package/lib/task/operation-outcome.ts +48 -0
  83. package/lib/task/orchestration.ts +217 -982
  84. package/lib/task/short-id.ts +65 -1
  85. package/lib/task/task-execution-lock.ts +10 -1
  86. package/lib/task/verification-engine.ts +118 -63
  87. package/lib/task/verification.ts +3 -2
  88. package/lib/task/workflow-warning-intents.ts +57 -2
  89. package/package.json +2 -2
  90. package/templates/.agents/QUICKSTART.en.md +2 -2
  91. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  92. package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
  93. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  94. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  95. package/templates/.agents/rules/task-management.en.md +2 -2
  96. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  97. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  98. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  99. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  100. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  101. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  102. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  103. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  104. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  105. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  106. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  107. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  108. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  109. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  111. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  112. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  113. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  114. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  115. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  116. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  117. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  118. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  119. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  120. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  121. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  122. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  123. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  124. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  125. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  126. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  127. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  128. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  129. package/templates/.codex/hooks.json +4 -4
  130. package/dist/lib/task/commit-finalization.js +0 -305
  131. package/dist/lib/task/commit-intent.js +0 -165
  132. package/lib/task/commit-finalization.ts +0 -419
  133. 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,22 @@ 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';
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';
11
13
  import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
12
14
  import type { LifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
13
15
  import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
16
+ import { verifyCodexSandboxControllerContext as verifyControllerContextFile } from '../agent-clients/adapters/codex-lifecycle/controller-context.ts';
14
17
  import type { AgentClientId } from '../agent-clients/types.ts';
15
18
  import { resolveTaskRef } from './resolve-ref.ts';
16
19
  import {
17
20
  activateMatchingOrchestrationDelegation,
18
21
  hasActivatableOrchestrationDelegation,
19
22
  hasSealableOrchestrationDelegation,
23
+ OrchestrationStateError,
20
24
  pauseMatchingOrchestrationDelegation,
21
25
  prepareOrchestrationDelegation,
22
26
  reconcileMatchingOrchestrationDelegation,
@@ -52,10 +56,27 @@ function coreOptions(options: CodexBridgeOptions): OrchestrationOptions {
52
56
  return { ...options.orchestrationOptions, repoRoot: options.repoRoot ?? options.orchestrationOptions?.repoRoot };
53
57
  }
54
58
 
55
- function bridgeFailure(code: string, message: string): OrchestrationResult {
59
+ function bridgeFailure(code: string, message: string, detail?: CodexCapabilityProvenanceDetail): OrchestrationResult {
56
60
  return {
57
61
  status: 'failed', changed: false, taskId: null, run: null, next: null,
58
- error: { code, message }
62
+ error: { code, message, ...(detail ? { detail } : {}) }
63
+ };
64
+ }
65
+
66
+ function capabilityFailure(error: unknown): NonNullable<OrchestrationResult['error']> {
67
+ const detailValue = error instanceof Error
68
+ ? (error as Error & { detail?: unknown }).detail
69
+ : undefined;
70
+ return {
71
+ code: error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
72
+ ? error.name
73
+ : 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED',
74
+ message: error instanceof Error ? error.message : String(error),
75
+ ...(error instanceof Error
76
+ && error.name === 'CODEX_CAPABILITY_PROVENANCE_MISMATCH'
77
+ && isCodexCapabilityProvenanceDetail(detailValue)
78
+ ? { detail: detailValue }
79
+ : {})
59
80
  };
60
81
  }
61
82
 
@@ -71,11 +92,54 @@ function requiredStore(options: CodexBridgeOptions): LifecycleStore {
71
92
  return options.store;
72
93
  }
73
94
 
74
- function controllerContext(taskId: string, repoRoot: string) {
95
+ function controllerContext(
96
+ repoRoot: string,
97
+ brokerController: Readonly<{ instanceDigest: string; controlGeneration: string }> | null
98
+ ) {
75
99
  const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
76
- return contextPath
77
- ? verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot })
78
- : null;
100
+ if (!contextPath) return null;
101
+ if (brokerController) {
102
+ return verifyControllerContextFile(contextPath, {
103
+ repoRoot,
104
+ generation: process.env.AGENT_INFRA_CONTROL_GENERATION ?? brokerController.controlGeneration
105
+ });
106
+ }
107
+ return verifyCodexSandboxControllerContext(contextPath, { repoRoot });
108
+ }
109
+
110
+ function brokerBindingConflictsWithContext(
111
+ repoRoot: string,
112
+ brokerController: Readonly<{ instanceDigest: string; controlGeneration: string }> | null
113
+ ): boolean {
114
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
115
+ if (!contextPath || !brokerController) return false;
116
+ const context = verifyControllerContextFile(contextPath, {
117
+ repoRoot,
118
+ generation: process.env.AGENT_INFRA_CONTROL_GENERATION ?? brokerController.controlGeneration
119
+ });
120
+ return context.controllerInstanceDigest !== brokerController.instanceDigest
121
+ || context.controlGeneration !== brokerController.controlGeneration;
122
+ }
123
+
124
+ function brokerControllerBinding(): Readonly<{ instanceDigest: string; controlGeneration: string }> | null {
125
+ const raw = process.env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING;
126
+ if (!raw) return null;
127
+ let value: unknown;
128
+ try {
129
+ value = JSON.parse(raw);
130
+ } catch {
131
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BINDING_INVALID');
132
+ }
133
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
134
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BINDING_INVALID');
135
+ }
136
+ const binding = value as Record<string, unknown>;
137
+ if (Object.keys(binding).sort().join(',') !== 'controlGeneration,instanceDigest'
138
+ || typeof binding.instanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(binding.instanceDigest)
139
+ || typeof binding.controlGeneration !== 'string' || binding.controlGeneration.length === 0) {
140
+ throw new Error('CODEX_SANDBOX_CONTROLLER_BINDING_INVALID');
141
+ }
142
+ return binding as { instanceDigest: string; controlGeneration: string };
79
143
  }
80
144
 
81
145
  async function prepareCodexOrchestrationDelegation(
@@ -101,36 +165,101 @@ async function prepareCodexOrchestrationDelegation(
101
165
  );
102
166
  }
103
167
  const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
104
- const controller = controllerContext(resolved.taskId, repoRoot);
168
+ const brokerController = brokerControllerBinding();
169
+ if (brokerBindingConflictsWithContext(repoRoot, brokerController)) {
170
+ return bridgeFailure(
171
+ 'CODEX_SANDBOX_CONTROLLER_BINDING_MISMATCH',
172
+ 'broker and local controller bindings do not match'
173
+ );
174
+ }
175
+ const localController = controllerContext(repoRoot, brokerController);
176
+ if (localController && brokerController
177
+ && (localController.controllerInstanceDigest !== brokerController.instanceDigest
178
+ || localController.controlGeneration !== brokerController.controlGeneration)) {
179
+ return bridgeFailure(
180
+ 'CODEX_SANDBOX_CONTROLLER_BINDING_MISMATCH',
181
+ 'broker and local controller bindings do not match'
182
+ );
183
+ }
184
+ const controller = brokerController ?? (localController ? {
185
+ instanceDigest: localController.controllerInstanceDigest,
186
+ controlGeneration: localController.controlGeneration
187
+ } : null);
105
188
  const capabilityStore = options.capabilityStore ?? createCodexCapabilityStore();
106
- const consumed = capabilityStore.consume(input.capabilityToken, {
189
+ const attested = capabilityStore.inspect(input.capabilityToken);
190
+ const lifecycleProvenance = {
191
+ ...attested.buildIdentity,
192
+ hookDefinitionHash: preflight.hookDefinitionHash,
193
+ ...preflight.hookProvenance,
194
+ capabilitySessionId: attested.sessionId!,
195
+ capabilityTurnId: attested.turnId!,
196
+ capabilityToolUseId: attested.toolUseId!,
197
+ controllerInstanceDigest: controller?.instanceDigest ?? null,
198
+ controlGeneration: controller?.controlGeneration ?? null
199
+ };
200
+ const capabilityExpected = {
107
201
  taskId: resolved.taskId,
108
202
  hookDefinitionHash: preflight.hookDefinitionHash,
109
203
  buildIdentity,
110
204
  ...(controller ? { controller: {
111
- instanceDigest: controller.controllerInstanceDigest,
205
+ instanceDigest: controller.instanceDigest,
112
206
  controlGeneration: controller.controlGeneration
113
207
  } } : {})
114
- });
208
+ };
115
209
  return prepareOrchestrationDelegation(taskRef, {
116
210
  ...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
211
+ lifecycleProvenance
212
+ }, {
213
+ ...coreOptions(options),
214
+ validateLifecycleCapability: () => {
215
+ try {
216
+ const validated = capabilityStore.validate(input.capabilityToken!, capabilityExpected);
217
+ if (validated.sessionId !== attested.sessionId
218
+ || validated.turnId !== attested.turnId
219
+ || validated.toolUseId !== attested.toolUseId) {
220
+ return {
221
+ code: 'CODEX_CAPABILITY_IDENTITY_CHANGED',
222
+ message: 'capability identity changed during validation'
223
+ };
224
+ }
225
+ return null;
226
+ } catch (error) {
227
+ return capabilityFailure(error);
228
+ }
229
+ },
230
+ consumeLifecycleCapability: () => {
231
+ try {
232
+ const consumed = capabilityStore.consume(input.capabilityToken!, capabilityExpected);
233
+ if (consumed.sessionId !== attested.sessionId
234
+ || consumed.turnId !== attested.turnId
235
+ || consumed.toolUseId !== attested.toolUseId) {
236
+ return {
237
+ code: 'CODEX_CAPABILITY_IDENTITY_CHANGED',
238
+ message: 'capability identity changed during consumption'
239
+ };
240
+ }
241
+ return null;
242
+ } catch (error) {
243
+ return capabilityFailure(error);
244
+ }
126
245
  }
127
- }, coreOptions(options));
246
+ });
128
247
  } catch (error) {
248
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
249
+ const detailValue = error instanceof Error
250
+ ? (error as Error & { detail?: unknown }).detail
251
+ : undefined;
252
+ const detail = error instanceof Error
253
+ && error.name === 'CODEX_CAPABILITY_PROVENANCE_MISMATCH'
254
+ && isCodexCapabilityProvenanceDetail(detailValue)
255
+ ? detailValue
256
+ : undefined;
129
257
  return bridgeFailure(
130
258
  error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
131
259
  ? error.name
132
260
  : 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED',
133
- error instanceof Error ? error.message : String(error)
261
+ error instanceof Error ? error.message : String(error),
262
+ detail
134
263
  );
135
264
  }
136
265
  }
@@ -161,14 +290,8 @@ async function activateCodexOrchestrationDelegation(
161
290
  });
162
291
  const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
163
292
  const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
164
- const contextTaskId = contextPath
165
- ? (JSON.parse(fs.readFileSync(contextPath, 'utf8')) as { taskId?: unknown }).taskId
166
- : null;
167
- if (contextPath && typeof contextTaskId !== 'string') {
168
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
169
- }
170
293
  const controller = contextPath
171
- ? verifyCodexSandboxControllerContext(contextPath, contextTaskId as string, { repoRoot })
294
+ ? verifyCodexSandboxControllerContext(contextPath, { repoRoot })
172
295
  : null;
173
296
  return activateMatchingOrchestrationDelegation('codex', {
174
297
  nativeAgent: evidence.nativeAgent,
@@ -196,6 +319,7 @@ async function activateCodexOrchestrationDelegation(
196
319
  }
197
320
  }, coreOptions(options));
198
321
  } catch (error) {
322
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
199
323
  return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
200
324
  }
201
325
  }
@@ -223,6 +347,7 @@ async function activateCodexSpawnDelegation(
223
347
  resolveThread: async () => resolved
224
348
  });
225
349
  } catch (error) {
350
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
226
351
  return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
227
352
  }
228
353
  }
@@ -238,7 +363,11 @@ async function sealCodexOrchestrationDelegation(
238
363
  const store = requiredStore(options);
239
364
  const existing = store.read(childThreadId);
240
365
  if (!existing.consumer) {
241
- store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(childThreadId));
366
+ const stopTurnId = existing.state.stop?.turnId;
367
+ if (!stopTurnId) {
368
+ return pauseBridge('ORCHESTRATION_CODEX_STOP_EVIDENCE_INVALID', 'Codex lifecycle stop hook is not available', options);
369
+ }
370
+ store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(childThreadId, stopTurnId));
242
371
  }
243
372
  const record = store.read(childThreadId);
244
373
  const evidence = record.state.stopEvidence;
@@ -259,6 +388,7 @@ async function sealCodexOrchestrationDelegation(
259
388
  coreOptions(options)
260
389
  );
261
390
  } catch (error) {
391
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
262
392
  return pauseBridge('ORCHESTRATION_CODEX_STOP_FAILED', error instanceof Error ? error.message : String(error), options);
263
393
  }
264
394
  }
@@ -280,7 +410,10 @@ async function sealCodexParentDelegation(
280
410
  const start = active.state.startEvidence!;
281
411
  const child = active.state.child!;
282
412
  if (!active.state.terminal) {
283
- store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(start.childThreadId));
413
+ const resolveTerminal = options.resolveTerminal ?? resolveCodexTerminal;
414
+ store.apply(active.state.stop
415
+ ? await resolveTerminal(start.childThreadId, active.state.stop.turnId)
416
+ : await resolveTerminal(start.childThreadId));
284
417
  }
285
418
  if (!store.read(start.childThreadId).state.stop) {
286
419
  store.apply({
@@ -294,6 +427,7 @@ async function sealCodexParentDelegation(
294
427
  }
295
428
  return sealCodexOrchestrationDelegation(start.childThreadId, options);
296
429
  } catch (error) {
430
+ if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
297
431
  if (error instanceof Error && error.message === 'CODEX_TURN_NOT_TERMINAL') {
298
432
  return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'The matching Codex child has not completed');
299
433
  }