@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
@@ -6,11 +6,22 @@ import path from 'node:path';
6
6
  import semver from 'semver';
7
7
  import * as toml from 'smol-toml';
8
8
 
9
- import { requestSandboxControl } from '../../../sandbox/control/client.ts';
9
+ import {
10
+ requestCodexControllerClose,
11
+ requestCodexControllerOpen,
12
+ requestCodexControllerVerify,
13
+ requestSandboxControl
14
+ } from '../../../sandbox/control/client.ts';
10
15
  import { readSandboxControlStatus } from '../../../sandbox/control/state.ts';
11
- import { getProcessStartTime, isProcessAlive } from '../../../server/process-state.ts';
16
+ import { getProcessStartTime, type ProcessIdentity } from '../../../server/process-state.ts';
12
17
  import { computeLifecycleBuildIdentity } from './build-identity.ts';
13
- import type { LifecycleBuildIdentity } from './build-identity.ts';
18
+ import {
19
+ contextFromControllerLease,
20
+ controllerProofFromContext,
21
+ verifyCodexSandboxControllerContext as verifyContextFile,
22
+ writeCodexSandboxControllerContext,
23
+ type CodexSandboxControllerContextV2
24
+ } from './controller-context.ts';
14
25
 
15
26
  type ControllerControl = Readonly<{
16
27
  token: string;
@@ -20,23 +31,7 @@ type ControllerControl = Readonly<{
20
31
  runtimeDir: string;
21
32
  }>;
22
33
 
23
- type CodexSandboxControllerContext = Readonly<{
24
- version: 1;
25
- taskId: string;
26
- controlGeneration: string;
27
- controllerInstanceDigest: string;
28
- parentPid: number;
29
- parentStartTime: number;
30
- issuedAt: number;
31
- expiresAt: number;
32
- buildIdentity: LifecycleBuildIdentity;
33
- hookDefinitionHash: string;
34
- lifecycleProfilesHash: string;
35
- }>;
36
-
37
34
  type ControllerInput = Readonly<{
38
- taskId: string;
39
- taskRef: string;
40
35
  executorModel?: string;
41
36
  executorReasoningEffort?: string;
42
37
  reviewerModel?: string;
@@ -51,6 +46,8 @@ type ControllerOptions = Readonly<{
51
46
  now?: () => number;
52
47
  codexVersion?: () => string;
53
48
  verifyTaskBinding?: (taskId: string, control: ControllerControl) => void;
49
+ openController?: typeof requestCodexControllerOpen;
50
+ closeController?: typeof requestCodexControllerClose;
54
51
  environment?: NodeJS.ProcessEnv;
55
52
  }>;
56
53
 
@@ -60,11 +57,18 @@ type PreparedCodexSandboxController = Readonly<{
60
57
  env: NodeJS.ProcessEnv;
61
58
  home: string;
62
59
  contextPath: string;
63
- context: CodexSandboxControllerContext;
60
+ context: CodexSandboxControllerContextV2;
64
61
  cleanup: () => void;
65
62
  }>;
66
63
 
67
- const CONTEXT_TTL_MS = 4 * 60 * 60 * 1_000;
64
+ type VerifyContextOptions = Readonly<{
65
+ repoRoot?: string;
66
+ now?: number;
67
+ generation?: string;
68
+ probeProcess?: (identity: ProcessIdentity) => 'alive' | 'dead' | 'unknown';
69
+ control?: ControllerControl;
70
+ requestControllerVerify?: typeof requestCodexControllerVerify;
71
+ }>;
68
72
 
69
73
  function digestFiles(files: readonly string[]): string {
70
74
  const hash = crypto.createHash('sha256');
@@ -162,6 +166,30 @@ function verifyControl(taskId: string, control: ControllerControl): void {
162
166
  if (payload.taskId !== taskId) throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
163
167
  }
164
168
 
169
+ function verifyCodexSandboxControllerContext(
170
+ contextPath: string,
171
+ options: VerifyContextOptions = {}
172
+ ): CodexSandboxControllerContextV2 {
173
+ const control = options.control ?? controlFromEnvironment();
174
+ const context = verifyContextFile(contextPath, {
175
+ repoRoot: options.repoRoot,
176
+ now: options.now,
177
+ generation: control.generation,
178
+ probeProcess: options.probeProcess
179
+ });
180
+ const verified = (options.requestControllerVerify ?? requestCodexControllerVerify)({
181
+ controllerProof: controllerProofFromContext(context),
182
+ ...control,
183
+ timeoutMs: 30_000
184
+ });
185
+ if (verified.binding.taskId !== context.taskId
186
+ || verified.binding.controlGeneration !== context.controlGeneration
187
+ || verified.binding.controllerInstanceDigest !== context.controllerInstanceDigest) {
188
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
189
+ }
190
+ return context;
191
+ }
192
+
165
193
  function detectedCodexVersion(): string {
166
194
  const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
167
195
  const version = /codex-cli\s+(\d+\.\d+\.\d+)/u.exec(result.stdout ?? '')?.[1];
@@ -256,7 +284,6 @@ function prepareCodexSandboxController(
256
284
  const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
257
285
  const sourceHome = path.resolve(options.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex'));
258
286
  const control = options.control ?? controlFromEnvironment();
259
- (options.verifyTaskBinding ?? verifyControl)(input.taskId, control);
260
287
  const version = (options.codexVersion ?? detectedCodexVersion)();
261
288
  if (!semver.gte(version, '0.147.0')) throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNSUPPORTED');
262
289
 
@@ -265,46 +292,33 @@ function prepareCodexSandboxController(
265
292
  fs.mkdirSync(runtimeRoot, { recursive: true, mode: 0o700 });
266
293
  fs.chmodSync(runtimeRoot, 0o700);
267
294
  const key = crypto.createHash('sha256')
268
- .update(`${input.taskId}\0${control.generation}`)
295
+ .update(`${process.pid}\0${control.generation}`)
269
296
  .digest('hex');
270
- const leasePath = path.join(runtimeRoot, `${key}.lease.json`);
271
- if (fs.existsSync(leasePath)) {
272
- const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8')) as { pid?: number; startTime?: number };
273
- if (Number.isSafeInteger(lease.pid) && typeof lease.startTime === 'number'
274
- && getProcessStartTime(lease.pid!) === lease.startTime) {
275
- throw new Error('CODEX_SANDBOX_CONTROLLER_BUSY');
276
- }
277
- fs.unlinkSync(leasePath);
278
- }
279
-
280
297
  const parentStartTime = getProcessStartTime(process.pid);
281
298
  if (!parentStartTime) throw new Error('CODEX_SANDBOX_CONTROLLER_PROCESS_IDENTITY_INVALID');
282
- const instanceDigest = crypto.randomBytes(32).toString('hex');
283
- fs.writeFileSync(leasePath, `${JSON.stringify({
284
- version: 1,
285
- pid: process.pid,
286
- startTime: parentStartTime,
287
- taskId: input.taskId,
288
- generation: control.generation,
289
- instanceDigest
290
- })}\n`, { mode: 0o600, flag: 'wx' });
291
-
292
299
  const home = fs.mkdtempSync(path.join(runtimeRoot, `${key}-`));
293
300
  fs.chmodSync(home, 0o700);
294
301
  let cleaned = false;
302
+ let context: CodexSandboxControllerContextV2 | null = null;
295
303
  const cleanup = () => {
296
304
  if (cleaned) return;
297
305
  cleaned = true;
306
+ if (context) {
307
+ try {
308
+ (options.closeController ?? requestCodexControllerClose)({
309
+ controllerProcess: context.controllerProcess,
310
+ controllerProof: controllerProofFromContext(context),
311
+ ...control,
312
+ timeoutMs: 30_000
313
+ });
314
+ } catch {
315
+ // The wrapper is exiting; dead-process recovery safely handles an unknown close result.
316
+ }
317
+ }
298
318
  const relative = path.relative(runtimeRoot, home);
299
319
  if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
300
320
  fs.rmSync(home, { recursive: true, force: true });
301
321
  }
302
- try {
303
- const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8')) as { instanceDigest?: string };
304
- if (lease.instanceDigest === instanceDigest) fs.unlinkSync(leasePath);
305
- } catch {
306
- // A missing or replaced lease is owned by another cleanup path.
307
- }
308
322
  };
309
323
 
310
324
  try {
@@ -333,22 +347,25 @@ function prepareCodexSandboxController(
333
347
  }
334
348
 
335
349
  const buildIdentity = computeLifecycleBuildIdentity(repoRoot);
336
- const now = (options.now ?? Date.now)();
337
- const context: CodexSandboxControllerContext = Object.freeze({
338
- version: 1,
339
- taskId: input.taskId,
340
- controlGeneration: control.generation,
341
- controllerInstanceDigest: instanceDigest,
342
- parentPid: process.pid,
343
- parentStartTime,
344
- issuedAt: now,
345
- expiresAt: now + CONTEXT_TTL_MS,
346
- buildIdentity,
350
+ const opened = (options.openController ?? requestCodexControllerOpen)({
351
+ controllerProcess: { pid: process.pid, startTime: parentStartTime },
352
+ ...control,
353
+ timeoutMs: 30_000
354
+ });
355
+ const taskId = opened.lease.taskId;
356
+ context = contextFromControllerLease(opened.lease, {
347
357
  hookDefinitionHash: crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex'),
348
358
  lifecycleProfilesHash: digestFiles([executor, reviewer])
349
359
  });
360
+ if (opened.lease.controlGeneration !== control.generation
361
+ || opened.lease.controllerProcess.pid !== process.pid
362
+ || opened.lease.controllerProcess.startTime !== parentStartTime
363
+ || JSON.stringify(opened.lease.buildIdentity) !== JSON.stringify(buildIdentity)) {
364
+ throw new Error('SANDBOX_CONTROL_RESULT_INVALID');
365
+ }
366
+ (options.verifyTaskBinding ?? verifyControl)(taskId, control);
350
367
  const contextPath = path.join(home, 'controller-context.json');
351
- fs.writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600 });
368
+ writeCodexSandboxControllerContext(contextPath, context);
352
369
 
353
370
  const shimDir = path.join(home, 'bin');
354
371
  fs.mkdirSync(shimDir, { mode: 0o700 });
@@ -364,7 +381,7 @@ function prepareCodexSandboxController(
364
381
  input.reviewerModel ? `--reviewer-model ${input.reviewerModel}` : '',
365
382
  input.reviewerReasoningEffort ? `--reviewer-reasoning-effort ${input.reviewerReasoningEffort}` : ''
366
383
  ].filter(Boolean).join(' ');
367
- const prompt = `$run-task ${input.taskRef}${policy ? ` ${policy}` : ''}`;
384
+ const prompt = `$run-task ${taskId}${policy ? ` ${policy}` : ''}`;
368
385
  const args = Object.freeze([
369
386
  'exec',
370
387
  '--enable', 'hooks',
@@ -379,7 +396,7 @@ function prepareCodexSandboxController(
379
396
  home,
380
397
  shimDir,
381
398
  contextPath,
382
- input.taskId,
399
+ taskId,
383
400
  control,
384
401
  providerEnvironment,
385
402
  options.environment ?? process.env
@@ -399,32 +416,6 @@ function prepareCodexSandboxController(
399
416
  }
400
417
  }
401
418
 
402
- function verifyCodexSandboxControllerContext(
403
- contextPath: string,
404
- taskId: string,
405
- options: Readonly<{ repoRoot?: string; now?: number; generation?: string }> = {}
406
- ): CodexSandboxControllerContext {
407
- const stat = fs.lstatSync(contextPath);
408
- if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) {
409
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
410
- }
411
- const context = JSON.parse(fs.readFileSync(contextPath, 'utf8')) as CodexSandboxControllerContext;
412
- const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
413
- if (context.version !== 1
414
- || context.taskId !== taskId
415
- || context.controlGeneration !== generation
416
- || context.expiresAt < (options.now ?? Date.now())
417
- || getProcessStartTime(context.parentPid) !== context.parentStartTime
418
- || !isProcessAlive(context.parentPid)) {
419
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
420
- }
421
- const current = computeLifecycleBuildIdentity(options.repoRoot ?? process.cwd());
422
- if (JSON.stringify(current) !== JSON.stringify(context.buildIdentity)) {
423
- throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
424
- }
425
- return Object.freeze(context);
426
- }
427
-
428
419
  async function runCodexSandboxController(
429
420
  input: ControllerInput,
430
421
  options: ControllerOptions = {}
@@ -469,10 +460,11 @@ async function runCodexSandboxController(
469
460
  export {
470
461
  prepareCodexSandboxController,
471
462
  runCodexSandboxController,
472
- verifyCodexSandboxControllerContext
463
+ verifyCodexSandboxControllerContext,
464
+ controllerProofFromContext
473
465
  };
474
466
  export type {
475
- CodexSandboxControllerContext,
467
+ CodexSandboxControllerContextV2 as CodexSandboxControllerContext,
476
468
  ControllerControl,
477
469
  ControllerInput,
478
470
  ControllerOptions,
@@ -105,7 +105,7 @@ function commitExplicitPaths(input: { cwd: string; paths: readonly string[]; mes
105
105
  return { status: 'applied' as const, changed: true, snapshot: after.snapshot, operations: [{ name: 'stage', status: 'applied' as const }, { name: 'commit', status: 'applied' as const }], error: null };
106
106
  }
107
107
 
108
- function pushGitRefs(input: { cwd: string; remote: string; refs: readonly string[] }, runner: GitRunner = defaultRunner) {
108
+ function pushGitRefs(input: { cwd: string; remote: string; refs: readonly string[]; expectedSha?: string }, runner: GitRunner = defaultRunner) {
109
109
  if (!input.remote || input.refs.length === 0) return { status: 'failed' as const, changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [], error: { code: 'GIT_PUSH_INPUT_INVALID', message: 'Remote and refs are required' } };
110
110
  const operations: GitOperation[] = [];
111
111
  for (const ref of input.refs) {
@@ -120,7 +120,16 @@ function pushGitRefs(input: { cwd: string; remote: string; refs: readonly string
120
120
  }
121
121
  const canonical = ref.startsWith('refs/') ? ref : `refs/heads/${ref}`;
122
122
  const remoteFact = run(runner, input.cwd, ['ls-remote', '--exit-code', input.remote, canonical]);
123
- operations.push({ name: 'push', ref, status: remoteFact.status === 0 ? 'applied' : 'failed', message: remoteFact.status === 0 ? undefined : 'Remote ref verification failed' });
123
+ const remoteSha = remoteFact.status === 0 ? remoteFact.stdout.trim().split(/\s+/)[0] ?? null : null;
124
+ const verified = remoteFact.status === 0 && (!input.expectedSha || remoteSha === input.expectedSha);
125
+ operations.push({
126
+ name: 'push',
127
+ ref,
128
+ status: verified ? 'applied' : 'failed',
129
+ message: verified ? undefined : input.expectedSha
130
+ ? `Remote ref verification failed: expected ${input.expectedSha}, received ${remoteSha ?? 'unavailable'}`
131
+ : 'Remote ref verification failed'
132
+ });
124
133
  }
125
134
  const failures = operations.filter((item) => item.status === 'failed').length;
126
135
  return { status: failures === 0 ? 'applied' as const : failures === operations.length ? 'failed' as const : 'degraded' as const, changed: failures < operations.length, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations, error: failures ? { code: 'GIT_PUSH_PARTIAL', message: `${failures} ref push operation(s) failed` } : null };
@@ -14,6 +14,7 @@ import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-l
14
14
  import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
15
15
  import type { CodexLifecycleEvent } from '../agent-clients/adapters/codex-lifecycle/evidence.ts';
16
16
  import { resolveTaskRef } from '../task/resolve-ref.ts';
17
+ import { hasSealableOrchestrationDelegation } from '../task/orchestration.ts';
17
18
  import {
18
19
  activateCodexOrchestrationDelegation,
19
20
  activateCodexSpawnDelegation,
@@ -100,7 +101,7 @@ function hookDefinitionHash(): string {
100
101
  function controllerBinding(taskId: string) {
101
102
  const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
102
103
  if (!contextPath) return undefined;
103
- const context = verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot: process.cwd() });
104
+ const context = verifyCodexSandboxControllerContext(contextPath, { repoRoot: process.cwd() });
104
105
  return {
105
106
  instanceDigest: context.controllerInstanceDigest,
106
107
  controlGeneration: context.controlGeneration
@@ -139,7 +140,8 @@ function recordFromPayload(phase: string, payload: unknown): CodexLifecycleEvent
139
140
  sessionId: text('sessionId'),
140
141
  turnId: text('turnId'),
141
142
  childThreadId: text('childThreadId'),
142
- nativeAgent: text('nativeAgent')
143
+ nativeAgent: text('nativeAgent'),
144
+ source: 'hook'
143
145
  };
144
146
  if (phase === 'subagent-stop') return {
145
147
  type: 'hook-stop',
@@ -326,8 +328,17 @@ async function codexLifecycle(args: string[] = []): Promise<void> {
326
328
  }
327
329
  const result = store.apply(event);
328
330
  if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
329
- const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
330
- outputBridgeResult(bridged);
331
+ if (!hasSealableOrchestrationDelegation('codex', event.childThreadId, { repoRoot: process.cwd() })) {
332
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
333
+ return;
334
+ }
335
+ output({
336
+ status: result.state.status,
337
+ changed: true,
338
+ evidence: result.state,
339
+ diagnostics: [],
340
+ error: result.state.error
341
+ });
331
342
  return;
332
343
  }
333
344
  output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: [], error: result.state.error });
@@ -350,7 +361,9 @@ async function codexLifecycle(args: string[] = []): Promise<void> {
350
361
  return;
351
362
  }
352
363
  if (parsed.operation === 'resolve-stop') {
353
- const terminal = await resolveCodexTerminal(childThreadId);
364
+ const stopTurnId = store.read(childThreadId).state.stop?.turnId;
365
+ if (!stopTurnId) throw new Error('Codex lifecycle stop hook is not available');
366
+ const terminal = await resolveCodexTerminal(childThreadId, stopTurnId);
354
367
  const latest = store.apply(terminal);
355
368
  if (latest.state.status !== 'stop-ready') {
356
369
  throw new Error(`Codex lifecycle stop evidence is not ready (status=${latest.state.status})`);
@@ -26,8 +26,6 @@ function parse(args: string[]): Readonly<{ operation: string; values: Readonly<R
26
26
  return null;
27
27
  }
28
28
  const allowed = new Set([
29
- '--task-id',
30
- '--task-ref',
31
29
  '--context',
32
30
  '--executor-model',
33
31
  '--executor-reasoning-effort',
@@ -37,6 +35,11 @@ function parse(args: string[]): Readonly<{ operation: string; values: Readonly<R
37
35
  const values: Record<string, string> = {};
38
36
  for (let index = 1; index < args.length; index += 1) {
39
37
  const flag = args[index]!;
38
+ if (flag === '--task-id' || flag.startsWith('--task-id=')
39
+ || flag === '--task-ref' || flag.startsWith('--task-ref=')) {
40
+ fail('CODEX_SANDBOX_CONTROLLER_IDENTITY_OPTION_UNSUPPORTED', 'controller task identity options are not supported', 2);
41
+ return null;
42
+ }
40
43
  const value = args[++index];
41
44
  if (!allowed.has(flag) || Object.hasOwn(values, flag) || !value || value.startsWith('--')) {
42
45
  fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', `invalid option '${flag}'`, 2);
@@ -50,17 +53,12 @@ function parse(args: string[]): Readonly<{ operation: string; values: Readonly<R
50
53
  async function codexSandboxController(args: string[] = []): Promise<void> {
51
54
  const parsed = parse(args);
52
55
  if (!parsed || process.exitCode) return;
53
- const taskId = parsed.values['--task-id'];
54
- if (!taskId) {
55
- fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', '--task-id is required', 2);
56
- return;
57
- }
58
56
  try {
59
57
  if (parsed.operation === 'verify-context') {
60
58
  const contextPath = parsed.values['--context']
61
59
  ?? process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
62
60
  if (!contextPath) throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_MISSING');
63
- const context = verifyCodexSandboxControllerContext(contextPath, taskId);
61
+ const context = verifyCodexSandboxControllerContext(contextPath);
64
62
  process.stdout.write(`${JSON.stringify({
65
63
  status: 'ready',
66
64
  changed: false,
@@ -77,8 +75,6 @@ async function codexSandboxController(args: string[] = []): Promise<void> {
77
75
  })}\n`);
78
76
  return;
79
77
  }
80
- const taskRef = parsed.values['--task-ref'];
81
- if (!taskRef) throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_REF_REQUIRED');
82
78
  const policy = [
83
79
  '--executor-model',
84
80
  '--executor-reasoning-effort',
@@ -90,8 +86,6 @@ async function codexSandboxController(args: string[] = []): Promise<void> {
90
86
  throw new Error('CODEX_SANDBOX_CONTROLLER_MODEL_POLICY_INVALID');
91
87
  }
92
88
  const exitCode = await runCodexSandboxController({
93
- taskId,
94
- taskRef,
95
89
  executorModel: parsed.values['--executor-model'],
96
90
  executorReasoningEffort: parsed.values['--executor-reasoning-effort'],
97
91
  reviewerModel: parsed.values['--reviewer-model'],
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
- import { commitExplicitPaths, inspectGitWorkflow, pushGitRefs, pushRebasedBranch } from '../git/workflow.ts';
2
+ import { inspectGitWorkflow, pushRebasedBranch } from '../git/workflow.ts';
3
3
  import { compareReviewTrees, snapshotReview } from '../git/review-snapshot.ts';
4
4
  import { resolvePostReviewGlobs } from '../task/review-fingerprint.ts';
5
+ import { executeCommitOperation } from '../task/commit-operation.ts';
5
6
 
6
7
  function output(value: unknown): void { process.stdout.write(`${JSON.stringify(value)}\n`); }
7
8
 
@@ -18,11 +19,9 @@ function gitWorkflow(args: string[] = []): void {
18
19
  else if (action === 'commit') {
19
20
  const inputIndex = rest.indexOf('--input');
20
21
  const input = inputIndex >= 0 && rest[inputIndex + 1] ? JSON.parse(fs.readFileSync(rest[inputIndex + 1]!, 'utf8')) : null;
21
- result = input ? commitExplicitPaths({ cwd: cwd ?? process.cwd(), ...input }) : { status: 'failed', changed: false, error: { code: 'GIT_INPUT_REQUIRED', message: '--input JSON file is required' } };
22
- } else if (action === 'push') {
23
- const inputIndex = rest.indexOf('--input');
24
- const input = inputIndex >= 0 && rest[inputIndex + 1] ? JSON.parse(fs.readFileSync(rest[inputIndex + 1]!, 'utf8')) : null;
25
- result = input ? pushGitRefs({ cwd: cwd ?? process.cwd(), ...input }) : { status: 'failed', changed: false, error: { code: 'GIT_INPUT_REQUIRED', message: '--input JSON file is required' } };
22
+ result = input
23
+ ? executeCommitOperation({ cwd: cwd ?? process.cwd(), ...input })
24
+ : { status: 'failed', changed: false, error: { code: 'GIT_INPUT_REQUIRED', message: '--input JSON file is required' } };
26
25
  } else if (action === 'push-rebased') {
27
26
  const inputIndex = rest.indexOf('--input');
28
27
  const input = inputIndex >= 0 && rest[inputIndex + 1] ? JSON.parse(fs.readFileSync(rest[inputIndex + 1]!, 'utf8')) : null;
@@ -17,13 +17,13 @@ const USAGE = `Usage: agent-infra-internal platform-pr inspect <task-ref> [--cwd
17
17
  agent-infra-internal platform-pr resolve-external <task-ref> --agent <agent> [--pr <N>] [--dry-run] [--cwd <path>]
18
18
  agent-infra-internal platform-pr create <task-ref> --agent <agent> --base <branch> --head <branch> --title-file <path|-> --body-file <path|-> [--draft] [--dry-run] [--cwd <path>]
19
19
  agent-infra-internal platform-pr bind <task-ref> --pr <N> --agent <agent> [--dry-run] [--cwd <path>]
20
- agent-infra-internal platform-pr sync <task-ref> --agent <agent> [--metadata] [--closing-issue] [--dry-run] [--cwd <path>]
20
+ agent-infra-internal platform-pr sync <task-ref> --agent <agent> [--metadata] [--closing-issue] --result <pr_created|pr_reused|no_op> [--dry-run] [--cwd <path>]
21
21
  agent-infra-internal platform-pr summary-context <task-ref> [--cwd <path>]
22
- agent-infra-internal platform-pr summary-sync <task-ref> --agent <agent> --body-file <path|-> [--dry-run] [--cwd <path>]
22
+ agent-infra-internal platform-pr summary-sync <task-ref> --agent <agent> --body-file <path|-> --result <pr_created|pr_reused|no_op> [--dry-run] [--cwd <path>]
23
23
  `;
24
24
 
25
25
  const BOOLEAN_FLAGS = new Set(['--draft', '--dry-run', '--metadata', '--closing-issue']);
26
- const VALUE_FLAGS = new Set(['--cwd', '--agent', '--base', '--head', '--title-file', '--body-file', '--pr']);
26
+ const VALUE_FLAGS = new Set(['--cwd', '--agent', '--base', '--head', '--title-file', '--body-file', '--pr', '--result']);
27
27
 
28
28
  function key(flag: string): string {
29
29
  return flag.slice(2).replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
@@ -76,9 +76,9 @@ function platformPr(args: string[] = []): void {
76
76
  'resolve-external': ['cwd', 'agent', 'pr', 'dryRun'],
77
77
  create: ['cwd', 'agent', 'base', 'head', 'titleFile', 'bodyFile', 'draft', 'dryRun'],
78
78
  bind: ['cwd', 'agent', 'pr', 'dryRun'],
79
- sync: ['cwd', 'agent', 'metadata', 'closingIssue', 'dryRun'],
79
+ sync: ['cwd', 'agent', 'metadata', 'closingIssue', 'result', 'dryRun'],
80
80
  'summary-context': ['cwd'],
81
- 'summary-sync': ['cwd', 'agent', 'bodyFile', 'dryRun']
81
+ 'summary-sync': ['cwd', 'agent', 'bodyFile', 'result', 'dryRun']
82
82
  };
83
83
  const unexpected = Object.keys(values).find((name) => !allowed[operation]!.includes(name));
84
84
  if (unexpected) { fail(`${operation} does not accept --${unexpected}`); return; }
@@ -88,6 +88,11 @@ function platformPr(args: string[] = []): void {
88
88
  const agent = normalizeAgentToken(values.agent);
89
89
  if (!agent) { fail(`invalid --agent '${values.agent}': ${AGENT_USAGE_HINT}`); return; }
90
90
  values.agent = agent;
91
+ const primaryResult = values.result === undefined ? undefined : values.result;
92
+ if (primaryResult !== undefined && !['pr_created', 'pr_reused', 'no_op'].includes(primaryResult as string)) {
93
+ fail('--result must be pr_created, pr_reused, or no_op');
94
+ return;
95
+ }
91
96
  if (operation === 'resolve-external') {
92
97
  const pr = values.pr === undefined ? undefined : Number(values.pr);
93
98
  if (pr !== undefined && (!Number.isInteger(pr) || pr <= 0)) { fail('resolve-external requires a positive --pr'); return; }
@@ -102,13 +107,15 @@ function platformPr(args: string[] = []): void {
102
107
  }
103
108
  if (operation === 'sync') {
104
109
  if (values.metadata !== true && values.closingIssue !== true) { fail('sync requires --metadata or --closing-issue'); return; }
105
- finish(syncPlatformPullRequest(taskRef, { cwd, agent: values.agent, metadata: values.metadata === true, closingIssue: values.closingIssue === true, dryRun: values.dryRun === true }));
110
+ if (!primaryResult) { fail('sync requires --result pr_created, pr_reused, or no_op'); return; }
111
+ finish(syncPlatformPullRequest(taskRef, { cwd, agent: values.agent, metadata: values.metadata === true, closingIssue: values.closingIssue === true, primaryResult: primaryResult as 'pr_created' | 'pr_reused' | 'no_op', dryRun: values.dryRun === true }));
106
112
  return;
107
113
  }
108
114
  if (operation === 'summary-sync') {
109
115
  if (typeof values.bodyFile !== 'string') { fail('summary-sync requires --body-file'); return; }
116
+ if (!primaryResult) { fail('summary-sync requires --result pr_created, pr_reused, or no_op'); return; }
110
117
  try {
111
- finish(syncPullRequestSummary(taskRef, { cwd, agent: values.agent, body: readFile(values.bodyFile, cwd), dryRun: values.dryRun === true }));
118
+ finish(syncPullRequestSummary(taskRef, { cwd, agent: values.agent, body: readFile(values.bodyFile, cwd), primaryResult: primaryResult as 'pr_created' | 'pr_reused' | 'no_op', dryRun: values.dryRun === true }));
112
119
  } catch (error) {
113
120
  fail(`unable to read body file: ${error instanceof Error ? error.message : String(error)}`);
114
121
  }
@@ -25,12 +25,13 @@ type DemoResult = {
25
25
  outputPath: string | null;
26
26
  };
27
27
 
28
+ // The tape is the canonical init interaction contract. Do not hash all of
29
+ // lib/init.ts: non-visual config serialization changes must not re-record it.
28
30
  const DEMO_INPUT_PATHS = [
29
31
  'assets/demo-init.tape',
30
32
  'scripts/demo-regen.sh',
31
33
  'scripts/normalize-gif-duration.py',
32
34
  'bin/cli.ts',
33
- 'lib/init.ts',
34
35
  'lib/log.ts',
35
36
  'lib/prompt.ts',
36
37
  'lib/paths.ts',
@@ -1,6 +1,84 @@
1
- import { requestSandboxControl, SandboxControlClientError } from '../sandbox/control/client.ts';
1
+ import {
2
+ requestSandboxControl,
3
+ requestSandboxTaskFinalization,
4
+ requestSandboxTaskControl,
5
+ SandboxControlClientError
6
+ } from '../sandbox/control/client.ts';
7
+ import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.ts';
2
8
  import { serveSandboxControl } from '../sandbox/control/server.ts';
3
9
  import { runSandboxControlExecutor } from '../sandbox/control/executor.ts';
10
+ import {
11
+ controllerProofFromContext,
12
+ verifyCodexSandboxControllerContext
13
+ } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
14
+
15
+ function isCanonicalCodexPrepare(args: readonly string[]): boolean {
16
+ if (args[1] !== 'prepare') return false;
17
+ const clients: string[] = [];
18
+ for (let index = 2; index < args.length; index += 1) {
19
+ if (args[index] === '--client') clients.push(args[index + 1] ?? '');
20
+ else if (args[index]?.startsWith('--client=')) clients.push(args[index]!.slice('--client='.length));
21
+ }
22
+ return clients.length === 1 && clients[0] === 'codex';
23
+ }
24
+
25
+ type FinalizationStatus = 'completed' | 'failed' | 'blocked' | 'unknown';
26
+
27
+ function writeFinalizationEnvelope(
28
+ status: FinalizationStatus,
29
+ accepted: boolean,
30
+ error: { code: string; message: string; retryable: boolean },
31
+ changed = false,
32
+ result: unknown = null
33
+ ): void {
34
+ process.stdout.write(`${JSON.stringify({ version: 1, status, changed, accepted, result, error })}\n`);
35
+ }
36
+
37
+ function finalizationErrorStatus(error: { retryable: boolean; code: string }): FinalizationStatus {
38
+ if (error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') return 'unknown';
39
+ return error.retryable ? 'blocked' : 'failed';
40
+ }
41
+
42
+ function sandboxFinalizationClient(args: string[]): void {
43
+ if (args.length !== 4 || !args[0] || args[1] !== 'complete' || args[2] !== '--agent' || !args[3]) {
44
+ const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'task ref, complete intent, and --agent are required', retryable: false };
45
+ writeFinalizationEnvelope('failed', false, error);
46
+ process.stderr.write('Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n');
47
+ process.exitCode = 1;
48
+ return;
49
+ }
50
+ const agent = normalizeAgentToken(args[3]);
51
+ if (!agent) {
52
+ const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: `invalid --agent: ${AGENT_USAGE_HINT}`, retryable: false };
53
+ writeFinalizationEnvelope('failed', false, error);
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ let response;
58
+ try {
59
+ response = requestSandboxTaskFinalization({ agent });
60
+ } catch (error) {
61
+ if (!(error instanceof SandboxControlClientError)) throw error;
62
+ writeFinalizationEnvelope(finalizationErrorStatus(error.detail), error.accepted, error.detail);
63
+ process.stderr.write(`${error.detail.message}\n`);
64
+ process.exitCode = error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.detail.retryable ? 2 : 1;
65
+ return;
66
+ }
67
+ if (response.phase === 'rejected') {
68
+ const error = response.error ?? {
69
+ code: 'SANDBOX_CONTROL_REJECTED',
70
+ message: 'sandbox control rejected the finalization request',
71
+ retryable: false
72
+ };
73
+ writeFinalizationEnvelope(finalizationErrorStatus(error), error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN', error);
74
+ process.stderr.write(`${error.message}\n`);
75
+ process.exitCode = error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.retryable ? 2 : 1;
76
+ return;
77
+ }
78
+ process.stdout.write(response.stdout);
79
+ process.stderr.write(response.stderr);
80
+ process.exitCode = response.exitCode ?? 1;
81
+ }
4
82
 
5
83
  async function sandboxControl(args: string[]): Promise<void> {
6
84
  const [operation, ...rest] = args;
@@ -31,9 +109,21 @@ async function sandboxControl(args: string[]): Promise<void> {
31
109
  }
32
110
  if (operation === 'client') {
33
111
  const [family = '', ...commandArgs] = rest;
112
+ if (family === 'task-finalization') {
113
+ sandboxFinalizationClient(commandArgs);
114
+ return;
115
+ }
34
116
  let response;
35
117
  try {
36
- response = requestSandboxControl({ family, args: commandArgs });
118
+ if (family === 'task-orchestration' && isCanonicalCodexPrepare(commandArgs)) {
119
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
120
+ const proof = contextPath
121
+ ? controllerProofFromContext(verifyCodexSandboxControllerContext(contextPath))
122
+ : null;
123
+ response = requestSandboxTaskControl({ family, args: commandArgs, controllerProof: proof });
124
+ } else {
125
+ response = requestSandboxControl({ family, args: commandArgs });
126
+ }
37
127
  } catch (error) {
38
128
  if (!(error instanceof SandboxControlClientError)) throw error;
39
129
  process.stderr.write(`${error.detail.message}\n`);