@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
@@ -5,11 +5,11 @@ import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import semver from 'semver';
7
7
  import * as toml from 'smol-toml';
8
- import { requestSandboxControl } from '../../../sandbox/control/client.js';
8
+ import { requestCodexControllerClose, requestCodexControllerOpen, requestCodexControllerVerify, requestSandboxControl } from '../../../sandbox/control/client.js';
9
9
  import { readSandboxControlStatus } from '../../../sandbox/control/state.js';
10
- import { getProcessStartTime, isProcessAlive } from '../../../server/process-state.js';
10
+ import { getProcessStartTime } from '../../../server/process-state.js';
11
11
  import { computeLifecycleBuildIdentity } from './build-identity.js';
12
- const CONTEXT_TTL_MS = 4 * 60 * 60 * 1_000;
12
+ import { contextFromControllerLease, controllerProofFromContext, verifyCodexSandboxControllerContext as verifyContextFile, writeCodexSandboxControllerContext } from './controller-context.js';
13
13
  function digestFiles(files) {
14
14
  const hash = crypto.createHash('sha256');
15
15
  for (const file of [...files].sort()) {
@@ -108,6 +108,26 @@ function verifyControl(taskId, control) {
108
108
  if (payload.taskId !== taskId)
109
109
  throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
110
110
  }
111
+ function verifyCodexSandboxControllerContext(contextPath, options = {}) {
112
+ const control = options.control ?? controlFromEnvironment();
113
+ const context = verifyContextFile(contextPath, {
114
+ repoRoot: options.repoRoot,
115
+ now: options.now,
116
+ generation: control.generation,
117
+ probeProcess: options.probeProcess
118
+ });
119
+ const verified = (options.requestControllerVerify ?? requestCodexControllerVerify)({
120
+ controllerProof: controllerProofFromContext(context),
121
+ ...control,
122
+ timeoutMs: 30_000
123
+ });
124
+ if (verified.binding.taskId !== context.taskId
125
+ || verified.binding.controlGeneration !== context.controlGeneration
126
+ || verified.binding.controllerInstanceDigest !== context.controllerInstanceDigest) {
127
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
128
+ }
129
+ return context;
130
+ }
111
131
  function detectedCodexVersion() {
112
132
  const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
113
133
  const version = /codex-cli\s+(\d+\.\d+\.\d+)/u.exec(result.stdout ?? '')?.[1];
@@ -191,7 +211,6 @@ function prepareCodexSandboxController(input, options = {}) {
191
211
  const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
192
212
  const sourceHome = path.resolve(options.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex'));
193
213
  const control = options.control ?? controlFromEnvironment();
194
- (options.verifyTaskBinding ?? verifyControl)(input.taskId, control);
195
214
  const version = (options.codexVersion ?? detectedCodexVersion)();
196
215
  if (!semver.gte(version, '0.147.0'))
197
216
  throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNSUPPORTED');
@@ -200,48 +219,36 @@ function prepareCodexSandboxController(input, options = {}) {
200
219
  fs.mkdirSync(runtimeRoot, { recursive: true, mode: 0o700 });
201
220
  fs.chmodSync(runtimeRoot, 0o700);
202
221
  const key = crypto.createHash('sha256')
203
- .update(`${input.taskId}\0${control.generation}`)
222
+ .update(`${process.pid}\0${control.generation}`)
204
223
  .digest('hex');
205
- const leasePath = path.join(runtimeRoot, `${key}.lease.json`);
206
- if (fs.existsSync(leasePath)) {
207
- const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
208
- if (Number.isSafeInteger(lease.pid) && typeof lease.startTime === 'number'
209
- && getProcessStartTime(lease.pid) === lease.startTime) {
210
- throw new Error('CODEX_SANDBOX_CONTROLLER_BUSY');
211
- }
212
- fs.unlinkSync(leasePath);
213
- }
214
224
  const parentStartTime = getProcessStartTime(process.pid);
215
225
  if (!parentStartTime)
216
226
  throw new Error('CODEX_SANDBOX_CONTROLLER_PROCESS_IDENTITY_INVALID');
217
- const instanceDigest = crypto.randomBytes(32).toString('hex');
218
- fs.writeFileSync(leasePath, `${JSON.stringify({
219
- version: 1,
220
- pid: process.pid,
221
- startTime: parentStartTime,
222
- taskId: input.taskId,
223
- generation: control.generation,
224
- instanceDigest
225
- })}\n`, { mode: 0o600, flag: 'wx' });
226
227
  const home = fs.mkdtempSync(path.join(runtimeRoot, `${key}-`));
227
228
  fs.chmodSync(home, 0o700);
228
229
  let cleaned = false;
230
+ let context = null;
229
231
  const cleanup = () => {
230
232
  if (cleaned)
231
233
  return;
232
234
  cleaned = true;
235
+ if (context) {
236
+ try {
237
+ (options.closeController ?? requestCodexControllerClose)({
238
+ controllerProcess: context.controllerProcess,
239
+ controllerProof: controllerProofFromContext(context),
240
+ ...control,
241
+ timeoutMs: 30_000
242
+ });
243
+ }
244
+ catch {
245
+ // The wrapper is exiting; dead-process recovery safely handles an unknown close result.
246
+ }
247
+ }
233
248
  const relative = path.relative(runtimeRoot, home);
234
249
  if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
235
250
  fs.rmSync(home, { recursive: true, force: true });
236
251
  }
237
- try {
238
- const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
239
- if (lease.instanceDigest === instanceDigest)
240
- fs.unlinkSync(leasePath);
241
- }
242
- catch {
243
- // A missing or replaced lease is owned by another cleanup path.
244
- }
245
252
  };
246
253
  try {
247
254
  const auth = path.join(sourceHome, 'auth.json');
@@ -263,22 +270,25 @@ function prepareCodexSandboxController(input, options = {}) {
263
270
  throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
264
271
  }
265
272
  const buildIdentity = computeLifecycleBuildIdentity(repoRoot);
266
- const now = (options.now ?? Date.now)();
267
- const context = Object.freeze({
268
- version: 1,
269
- taskId: input.taskId,
270
- controlGeneration: control.generation,
271
- controllerInstanceDigest: instanceDigest,
272
- parentPid: process.pid,
273
- parentStartTime,
274
- issuedAt: now,
275
- expiresAt: now + CONTEXT_TTL_MS,
276
- buildIdentity,
273
+ const opened = (options.openController ?? requestCodexControllerOpen)({
274
+ controllerProcess: { pid: process.pid, startTime: parentStartTime },
275
+ ...control,
276
+ timeoutMs: 30_000
277
+ });
278
+ const taskId = opened.lease.taskId;
279
+ context = contextFromControllerLease(opened.lease, {
277
280
  hookDefinitionHash: crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex'),
278
281
  lifecycleProfilesHash: digestFiles([executor, reviewer])
279
282
  });
283
+ if (opened.lease.controlGeneration !== control.generation
284
+ || opened.lease.controllerProcess.pid !== process.pid
285
+ || opened.lease.controllerProcess.startTime !== parentStartTime
286
+ || JSON.stringify(opened.lease.buildIdentity) !== JSON.stringify(buildIdentity)) {
287
+ throw new Error('SANDBOX_CONTROL_RESULT_INVALID');
288
+ }
289
+ (options.verifyTaskBinding ?? verifyControl)(taskId, control);
280
290
  const contextPath = path.join(home, 'controller-context.json');
281
- fs.writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600 });
291
+ writeCodexSandboxControllerContext(contextPath, context);
282
292
  const shimDir = path.join(home, 'bin');
283
293
  fs.mkdirSync(shimDir, { mode: 0o700 });
284
294
  const internalCli = path.resolve(process.argv[1] ?? path.join(repoRoot, 'bin', 'internal-cli.ts'));
@@ -292,7 +302,7 @@ function prepareCodexSandboxController(input, options = {}) {
292
302
  input.reviewerModel ? `--reviewer-model ${input.reviewerModel}` : '',
293
303
  input.reviewerReasoningEffort ? `--reviewer-reasoning-effort ${input.reviewerReasoningEffort}` : ''
294
304
  ].filter(Boolean).join(' ');
295
- const prompt = `$run-task ${input.taskRef}${policy ? ` ${policy}` : ''}`;
305
+ const prompt = `$run-task ${taskId}${policy ? ` ${policy}` : ''}`;
296
306
  const args = Object.freeze([
297
307
  'exec',
298
308
  '--enable', 'hooks',
@@ -303,7 +313,7 @@ function prepareCodexSandboxController(input, options = {}) {
303
313
  '-C', repoRoot,
304
314
  prompt
305
315
  ]);
306
- const env = isolatedEnvironment(home, shimDir, contextPath, input.taskId, control, providerEnvironment, options.environment ?? process.env);
316
+ const env = isolatedEnvironment(home, shimDir, contextPath, taskId, control, providerEnvironment, options.environment ?? process.env);
307
317
  return Object.freeze({
308
318
  command: 'codex',
309
319
  args,
@@ -319,27 +329,6 @@ function prepareCodexSandboxController(input, options = {}) {
319
329
  throw error;
320
330
  }
321
331
  }
322
- function verifyCodexSandboxControllerContext(contextPath, taskId, options = {}) {
323
- const stat = fs.lstatSync(contextPath);
324
- if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) {
325
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
326
- }
327
- const context = JSON.parse(fs.readFileSync(contextPath, 'utf8'));
328
- const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
329
- if (context.version !== 1
330
- || context.taskId !== taskId
331
- || context.controlGeneration !== generation
332
- || context.expiresAt < (options.now ?? Date.now())
333
- || getProcessStartTime(context.parentPid) !== context.parentStartTime
334
- || !isProcessAlive(context.parentPid)) {
335
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
336
- }
337
- const current = computeLifecycleBuildIdentity(options.repoRoot ?? process.cwd());
338
- if (JSON.stringify(current) !== JSON.stringify(context.buildIdentity)) {
339
- throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
340
- }
341
- return Object.freeze(context);
342
- }
343
332
  async function runCodexSandboxController(input, options = {}) {
344
333
  const prepared = prepareCodexSandboxController(input, options);
345
334
  try {
@@ -394,5 +383,5 @@ async function runCodexSandboxController(input, options = {}) {
394
383
  prepared.cleanup();
395
384
  }
396
385
  }
397
- export { prepareCodexSandboxController, runCodexSandboxController, verifyCodexSandboxControllerContext };
386
+ export { prepareCodexSandboxController, runCodexSandboxController, verifyCodexSandboxControllerContext, controllerProofFromContext };
398
387
  //# sourceMappingURL=sandbox-controller.js.map
@@ -124,7 +124,16 @@ function pushGitRefs(input, runner = defaultRunner) {
124
124
  }
125
125
  const canonical = ref.startsWith('refs/') ? ref : `refs/heads/${ref}`;
126
126
  const remoteFact = run(runner, input.cwd, ['ls-remote', '--exit-code', input.remote, canonical]);
127
- operations.push({ name: 'push', ref, status: remoteFact.status === 0 ? 'applied' : 'failed', message: remoteFact.status === 0 ? undefined : 'Remote ref verification failed' });
127
+ const remoteSha = remoteFact.status === 0 ? remoteFact.stdout.trim().split(/\s+/)[0] ?? null : null;
128
+ const verified = remoteFact.status === 0 && (!input.expectedSha || remoteSha === input.expectedSha);
129
+ operations.push({
130
+ name: 'push',
131
+ ref,
132
+ status: verified ? 'applied' : 'failed',
133
+ message: verified ? undefined : input.expectedSha
134
+ ? `Remote ref verification failed: expected ${input.expectedSha}, received ${remoteSha ?? 'unavailable'}`
135
+ : 'Remote ref verification failed'
136
+ });
128
137
  }
129
138
  const failures = operations.filter((item) => item.status === 'failed').length;
130
139
  return { status: failures === 0 ? 'applied' : failures === operations.length ? 'failed' : 'degraded', 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 };
@@ -8,6 +8,7 @@ import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-life
8
8
  import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.js';
9
9
  import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.js';
10
10
  import { resolveTaskRef } from '../task/resolve-ref.js';
11
+ import { hasSealableOrchestrationDelegation } from '../task/orchestration.js';
11
12
  import { activateCodexOrchestrationDelegation, activateCodexSpawnDelegation, reconcileCodexOrchestrationDelegation, sealCodexOrchestrationDelegation, sealCodexParentDelegation } from '../task/codex-orchestration.js';
12
13
  const USAGE = 'Usage: agent-infra-internal codex-lifecycle <capability-arm|hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
13
14
  const MANAGED_AGENT = /^agent-infra-lifecycle-(executor|reviewer)$/;
@@ -80,7 +81,7 @@ function controllerBinding(taskId) {
80
81
  const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
81
82
  if (!contextPath)
82
83
  return undefined;
83
- const context = verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot: process.cwd() });
84
+ const context = verifyCodexSandboxControllerContext(contextPath, { repoRoot: process.cwd() });
84
85
  return {
85
86
  instanceDigest: context.controllerInstanceDigest,
86
87
  controlGeneration: context.controlGeneration
@@ -121,7 +122,8 @@ function recordFromPayload(phase, payload) {
121
122
  sessionId: text('sessionId'),
122
123
  turnId: text('turnId'),
123
124
  childThreadId: text('childThreadId'),
124
- nativeAgent: text('nativeAgent')
125
+ nativeAgent: text('nativeAgent'),
126
+ source: 'hook'
125
127
  };
126
128
  if (phase === 'subagent-stop')
127
129
  return {
@@ -314,8 +316,17 @@ async function codexLifecycle(args = []) {
314
316
  }
315
317
  const result = store.apply(event);
316
318
  if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
317
- const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
318
- outputBridgeResult(bridged);
319
+ if (!hasSealableOrchestrationDelegation('codex', event.childThreadId, { repoRoot: process.cwd() })) {
320
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
321
+ return;
322
+ }
323
+ output({
324
+ status: result.state.status,
325
+ changed: true,
326
+ evidence: result.state,
327
+ diagnostics: [],
328
+ error: result.state.error
329
+ });
319
330
  return;
320
331
  }
321
332
  output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: [], error: result.state.error });
@@ -341,7 +352,10 @@ async function codexLifecycle(args = []) {
341
352
  return;
342
353
  }
343
354
  if (parsed.operation === 'resolve-stop') {
344
- const terminal = await resolveCodexTerminal(childThreadId);
355
+ const stopTurnId = store.read(childThreadId).state.stop?.turnId;
356
+ if (!stopTurnId)
357
+ throw new Error('Codex lifecycle stop hook is not available');
358
+ const terminal = await resolveCodexTerminal(childThreadId, stopTurnId);
345
359
  const latest = store.apply(terminal);
346
360
  if (latest.state.status !== 'stop-ready') {
347
361
  throw new Error(`Codex lifecycle stop evidence is not ready (status=${latest.state.status})`);
@@ -20,8 +20,6 @@ function parse(args) {
20
20
  return null;
21
21
  }
22
22
  const allowed = new Set([
23
- '--task-id',
24
- '--task-ref',
25
23
  '--context',
26
24
  '--executor-model',
27
25
  '--executor-reasoning-effort',
@@ -31,6 +29,11 @@ function parse(args) {
31
29
  const values = {};
32
30
  for (let index = 1; index < args.length; index += 1) {
33
31
  const flag = args[index];
32
+ if (flag === '--task-id' || flag.startsWith('--task-id=')
33
+ || flag === '--task-ref' || flag.startsWith('--task-ref=')) {
34
+ fail('CODEX_SANDBOX_CONTROLLER_IDENTITY_OPTION_UNSUPPORTED', 'controller task identity options are not supported', 2);
35
+ return null;
36
+ }
34
37
  const value = args[++index];
35
38
  if (!allowed.has(flag) || Object.hasOwn(values, flag) || !value || value.startsWith('--')) {
36
39
  fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', `invalid option '${flag}'`, 2);
@@ -44,18 +47,13 @@ async function codexSandboxController(args = []) {
44
47
  const parsed = parse(args);
45
48
  if (!parsed || process.exitCode)
46
49
  return;
47
- const taskId = parsed.values['--task-id'];
48
- if (!taskId) {
49
- fail('CODEX_SANDBOX_CONTROLLER_PAYLOAD_INVALID', '--task-id is required', 2);
50
- return;
51
- }
52
50
  try {
53
51
  if (parsed.operation === 'verify-context') {
54
52
  const contextPath = parsed.values['--context']
55
53
  ?? process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
56
54
  if (!contextPath)
57
55
  throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_MISSING');
58
- const context = verifyCodexSandboxControllerContext(contextPath, taskId);
56
+ const context = verifyCodexSandboxControllerContext(contextPath);
59
57
  process.stdout.write(`${JSON.stringify({
60
58
  status: 'ready',
61
59
  changed: false,
@@ -72,9 +70,6 @@ async function codexSandboxController(args = []) {
72
70
  })}\n`);
73
71
  return;
74
72
  }
75
- const taskRef = parsed.values['--task-ref'];
76
- if (!taskRef)
77
- throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_REF_REQUIRED');
78
73
  const policy = [
79
74
  '--executor-model',
80
75
  '--executor-reasoning-effort',
@@ -86,8 +81,6 @@ async function codexSandboxController(args = []) {
86
81
  throw new Error('CODEX_SANDBOX_CONTROLLER_MODEL_POLICY_INVALID');
87
82
  }
88
83
  const exitCode = await runCodexSandboxController({
89
- taskId,
90
- taskRef,
91
84
  executorModel: parsed.values['--executor-model'],
92
85
  executorReasoningEffort: parsed.values['--executor-reasoning-effort'],
93
86
  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.js';
2
+ import { inspectGitWorkflow, pushRebasedBranch } from '../git/workflow.js';
3
3
  import { compareReviewTrees, snapshotReview } from '../git/review-snapshot.js';
4
4
  import { resolvePostReviewGlobs } from '../task/review-fingerprint.js';
5
+ import { executeCommitOperation } from '../task/commit-operation.js';
5
6
  function output(value) { process.stdout.write(`${JSON.stringify(value)}\n`); }
6
7
  function gitWorkflow(args = []) {
7
8
  const [action, ...rest] = args;
@@ -16,12 +17,9 @@ function gitWorkflow(args = []) {
16
17
  else if (action === 'commit') {
17
18
  const inputIndex = rest.indexOf('--input');
18
19
  const input = inputIndex >= 0 && rest[inputIndex + 1] ? JSON.parse(fs.readFileSync(rest[inputIndex + 1], 'utf8')) : null;
19
- result = input ? commitExplicitPaths({ cwd: cwd ?? process.cwd(), ...input }) : { status: 'failed', changed: false, error: { code: 'GIT_INPUT_REQUIRED', message: '--input JSON file is required' } };
20
- }
21
- else if (action === 'push') {
22
- const inputIndex = rest.indexOf('--input');
23
- const input = inputIndex >= 0 && rest[inputIndex + 1] ? JSON.parse(fs.readFileSync(rest[inputIndex + 1], 'utf8')) : null;
24
- result = input ? pushGitRefs({ cwd: cwd ?? process.cwd(), ...input }) : { status: 'failed', changed: false, error: { code: 'GIT_INPUT_REQUIRED', message: '--input JSON file is required' } };
20
+ result = input
21
+ ? executeCommitOperation({ cwd: cwd ?? process.cwd(), ...input })
22
+ : { status: 'failed', changed: false, error: { code: 'GIT_INPUT_REQUIRED', message: '--input JSON file is required' } };
25
23
  }
26
24
  else if (action === 'push-rebased') {
27
25
  const inputIndex = rest.indexOf('--input');
@@ -7,12 +7,12 @@ const USAGE = `Usage: agent-infra-internal platform-pr inspect <task-ref> [--cwd
7
7
  agent-infra-internal platform-pr resolve-external <task-ref> --agent <agent> [--pr <N>] [--dry-run] [--cwd <path>]
8
8
  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>]
9
9
  agent-infra-internal platform-pr bind <task-ref> --pr <N> --agent <agent> [--dry-run] [--cwd <path>]
10
- agent-infra-internal platform-pr sync <task-ref> --agent <agent> [--metadata] [--closing-issue] [--dry-run] [--cwd <path>]
10
+ agent-infra-internal platform-pr sync <task-ref> --agent <agent> [--metadata] [--closing-issue] --result <pr_created|pr_reused|no_op> [--dry-run] [--cwd <path>]
11
11
  agent-infra-internal platform-pr summary-context <task-ref> [--cwd <path>]
12
- agent-infra-internal platform-pr summary-sync <task-ref> --agent <agent> --body-file <path|-> [--dry-run] [--cwd <path>]
12
+ 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>]
13
13
  `;
14
14
  const BOOLEAN_FLAGS = new Set(['--draft', '--dry-run', '--metadata', '--closing-issue']);
15
- const VALUE_FLAGS = new Set(['--cwd', '--agent', '--base', '--head', '--title-file', '--body-file', '--pr']);
15
+ const VALUE_FLAGS = new Set(['--cwd', '--agent', '--base', '--head', '--title-file', '--body-file', '--pr', '--result']);
16
16
  function key(flag) {
17
17
  return flag.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
18
18
  }
@@ -79,9 +79,9 @@ function platformPr(args = []) {
79
79
  'resolve-external': ['cwd', 'agent', 'pr', 'dryRun'],
80
80
  create: ['cwd', 'agent', 'base', 'head', 'titleFile', 'bodyFile', 'draft', 'dryRun'],
81
81
  bind: ['cwd', 'agent', 'pr', 'dryRun'],
82
- sync: ['cwd', 'agent', 'metadata', 'closingIssue', 'dryRun'],
82
+ sync: ['cwd', 'agent', 'metadata', 'closingIssue', 'result', 'dryRun'],
83
83
  'summary-context': ['cwd'],
84
- 'summary-sync': ['cwd', 'agent', 'bodyFile', 'dryRun']
84
+ 'summary-sync': ['cwd', 'agent', 'bodyFile', 'result', 'dryRun']
85
85
  };
86
86
  const unexpected = Object.keys(values).find((name) => !allowed[operation].includes(name));
87
87
  if (unexpected) {
@@ -106,6 +106,11 @@ function platformPr(args = []) {
106
106
  return;
107
107
  }
108
108
  values.agent = agent;
109
+ const primaryResult = values.result === undefined ? undefined : values.result;
110
+ if (primaryResult !== undefined && !['pr_created', 'pr_reused', 'no_op'].includes(primaryResult)) {
111
+ fail('--result must be pr_created, pr_reused, or no_op');
112
+ return;
113
+ }
109
114
  if (operation === 'resolve-external') {
110
115
  const pr = values.pr === undefined ? undefined : Number(values.pr);
111
116
  if (pr !== undefined && (!Number.isInteger(pr) || pr <= 0)) {
@@ -129,7 +134,11 @@ function platformPr(args = []) {
129
134
  fail('sync requires --metadata or --closing-issue');
130
135
  return;
131
136
  }
132
- finish(syncPlatformPullRequest(taskRef, { cwd, agent: values.agent, metadata: values.metadata === true, closingIssue: values.closingIssue === true, dryRun: values.dryRun === true }));
137
+ if (!primaryResult) {
138
+ fail('sync requires --result pr_created, pr_reused, or no_op');
139
+ return;
140
+ }
141
+ finish(syncPlatformPullRequest(taskRef, { cwd, agent: values.agent, metadata: values.metadata === true, closingIssue: values.closingIssue === true, primaryResult: primaryResult, dryRun: values.dryRun === true }));
133
142
  return;
134
143
  }
135
144
  if (operation === 'summary-sync') {
@@ -137,8 +146,12 @@ function platformPr(args = []) {
137
146
  fail('summary-sync requires --body-file');
138
147
  return;
139
148
  }
149
+ if (!primaryResult) {
150
+ fail('summary-sync requires --result pr_created, pr_reused, or no_op');
151
+ return;
152
+ }
140
153
  try {
141
- finish(syncPullRequestSummary(taskRef, { cwd, agent: values.agent, body: readFile(values.bodyFile, cwd), dryRun: values.dryRun === true }));
154
+ finish(syncPullRequestSummary(taskRef, { cwd, agent: values.agent, body: readFile(values.bodyFile, cwd), primaryResult: primaryResult, dryRun: values.dryRun === true }));
142
155
  }
143
156
  catch (error) {
144
157
  fail(`unable to read body file: ${error instanceof Error ? error.message : String(error)}`);
@@ -10,12 +10,13 @@ import { releaseSnapshot } from '../release/workflow.js';
10
10
  function command(cwd, executable, args) {
11
11
  return spawnSync(executable, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
12
12
  }
13
+ // The tape is the canonical init interaction contract. Do not hash all of
14
+ // lib/init.ts: non-visual config serialization changes must not re-record it.
13
15
  const DEMO_INPUT_PATHS = [
14
16
  'assets/demo-init.tape',
15
17
  'scripts/demo-regen.sh',
16
18
  'scripts/normalize-gif-duration.py',
17
19
  'bin/cli.ts',
18
- 'lib/init.ts',
19
20
  'lib/log.ts',
20
21
  'lib/prompt.ts',
21
22
  'lib/paths.ts',
@@ -1,6 +1,70 @@
1
- import { requestSandboxControl, SandboxControlClientError } from '../sandbox/control/client.js';
1
+ import { requestSandboxControl, requestSandboxTaskFinalization, requestSandboxTaskControl, SandboxControlClientError } from '../sandbox/control/client.js';
2
+ import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
2
3
  import { serveSandboxControl } from '../sandbox/control/server.js';
3
4
  import { runSandboxControlExecutor } from '../sandbox/control/executor.js';
5
+ import { controllerProofFromContext, verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.js';
6
+ function isCanonicalCodexPrepare(args) {
7
+ if (args[1] !== 'prepare')
8
+ return false;
9
+ const clients = [];
10
+ for (let index = 2; index < args.length; index += 1) {
11
+ if (args[index] === '--client')
12
+ clients.push(args[index + 1] ?? '');
13
+ else if (args[index]?.startsWith('--client='))
14
+ clients.push(args[index].slice('--client='.length));
15
+ }
16
+ return clients.length === 1 && clients[0] === 'codex';
17
+ }
18
+ function writeFinalizationEnvelope(status, accepted, error, changed = false, result = null) {
19
+ process.stdout.write(`${JSON.stringify({ version: 1, status, changed, accepted, result, error })}\n`);
20
+ }
21
+ function finalizationErrorStatus(error) {
22
+ if (error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
23
+ return 'unknown';
24
+ return error.retryable ? 'blocked' : 'failed';
25
+ }
26
+ function sandboxFinalizationClient(args) {
27
+ if (args.length !== 4 || !args[0] || args[1] !== 'complete' || args[2] !== '--agent' || !args[3]) {
28
+ const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: 'task ref, complete intent, and --agent are required', retryable: false };
29
+ writeFinalizationEnvelope('failed', false, error);
30
+ process.stderr.write('Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n');
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+ const agent = normalizeAgentToken(args[3]);
35
+ if (!agent) {
36
+ const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message: `invalid --agent: ${AGENT_USAGE_HINT}`, retryable: false };
37
+ writeFinalizationEnvelope('failed', false, error);
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+ let response;
42
+ try {
43
+ response = requestSandboxTaskFinalization({ agent });
44
+ }
45
+ catch (error) {
46
+ if (!(error instanceof SandboxControlClientError))
47
+ throw error;
48
+ writeFinalizationEnvelope(finalizationErrorStatus(error.detail), error.accepted, error.detail);
49
+ process.stderr.write(`${error.detail.message}\n`);
50
+ process.exitCode = error.detail.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.detail.retryable ? 2 : 1;
51
+ return;
52
+ }
53
+ if (response.phase === 'rejected') {
54
+ const error = response.error ?? {
55
+ code: 'SANDBOX_CONTROL_REJECTED',
56
+ message: 'sandbox control rejected the finalization request',
57
+ retryable: false
58
+ };
59
+ writeFinalizationEnvelope(finalizationErrorStatus(error), error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN', error);
60
+ process.stderr.write(`${error.message}\n`);
61
+ process.exitCode = error.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN' ? 1 : error.retryable ? 2 : 1;
62
+ return;
63
+ }
64
+ process.stdout.write(response.stdout);
65
+ process.stderr.write(response.stderr);
66
+ process.exitCode = response.exitCode ?? 1;
67
+ }
4
68
  async function sandboxControl(args) {
5
69
  const [operation, ...rest] = args;
6
70
  if (operation === 'serve') {
@@ -33,9 +97,22 @@ async function sandboxControl(args) {
33
97
  }
34
98
  if (operation === 'client') {
35
99
  const [family = '', ...commandArgs] = rest;
100
+ if (family === 'task-finalization') {
101
+ sandboxFinalizationClient(commandArgs);
102
+ return;
103
+ }
36
104
  let response;
37
105
  try {
38
- response = requestSandboxControl({ family, args: commandArgs });
106
+ if (family === 'task-orchestration' && isCanonicalCodexPrepare(commandArgs)) {
107
+ const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
108
+ const proof = contextPath
109
+ ? controllerProofFromContext(verifyCodexSandboxControllerContext(contextPath))
110
+ : null;
111
+ response = requestSandboxTaskControl({ family, args: commandArgs, controllerProof: proof });
112
+ }
113
+ else {
114
+ response = requestSandboxControl({ family, args: commandArgs });
115
+ }
39
116
  }
40
117
  catch (error) {
41
118
  if (!(error instanceof SandboxControlClientError))
@@ -0,0 +1,54 @@
1
+ import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
2
+ import { applyTaskFinalization } from '../task/finalization.js';
3
+ import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
4
+ import { verifyTaskEvent } from '../task/verification.js';
5
+ const USAGE = 'Usage: agent-infra-internal task-finalization <N | TASK-id> complete --agent <agent>\n';
6
+ function envelope(status, changed, accepted, result, error) {
7
+ return `${JSON.stringify({ version: 1, status, changed, accepted, result, error })}\n`;
8
+ }
9
+ function exitCode(status) {
10
+ return status === 'completed' ? 0 : status === 'blocked' ? 2 : 1;
11
+ }
12
+ function fail(message) {
13
+ const error = { code: 'TASK_FINALIZATION_PAYLOAD_INVALID', message, retryable: false };
14
+ process.stdout.write(envelope('failed', false, false, null, error));
15
+ process.stderr.write(USAGE);
16
+ process.exitCode = 1;
17
+ }
18
+ function taskFinalization(args = []) {
19
+ if (args[0] === '--help' || args[0] === '-h') {
20
+ process.stdout.write(USAGE);
21
+ return;
22
+ }
23
+ if (args.length !== 4 || !args[0] || args[1] !== 'complete' || args[2] !== '--agent' || !args[3]) {
24
+ fail('task ref, complete intent, and --agent are required');
25
+ return;
26
+ }
27
+ const agent = normalizeAgentToken(args[3]);
28
+ if (!agent) {
29
+ fail(`invalid --agent: ${AGENT_USAGE_HINT}`);
30
+ return;
31
+ }
32
+ let repoRoot;
33
+ try {
34
+ repoRoot = detectRepoRoot();
35
+ }
36
+ catch (error) {
37
+ const detail = { code: 'REPO_ROOT_NOT_FOUND', message: error instanceof Error ? error.message : String(error), retryable: false };
38
+ process.stdout.write(envelope('failed', false, true, null, detail));
39
+ process.exitCode = 1;
40
+ return;
41
+ }
42
+ const resolved = resolveTaskRef(args[0], { repoRoot });
43
+ if (!resolved.ok) {
44
+ const detail = { code: resolved.code, message: resolved.message, retryable: false };
45
+ process.stdout.write(envelope('failed', false, true, null, detail));
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+ const result = applyTaskFinalization({ taskRef: resolved.taskId, intent: 'complete', agent }, { repoRoot, preflight: (request, options) => verifyTaskEvent({ ...request, event: 'complete-task.hard-preflight' }, options) });
50
+ process.stdout.write(envelope(result.status, result.changed, true, result, result.error));
51
+ process.exitCode = exitCode(result.status);
52
+ }
53
+ export { taskFinalization };
54
+ //# sourceMappingURL=task-finalization.js.map