@fitlab-ai/agent-infra 0.9.7 → 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 (195) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +11 -1
  4. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  12. package/dist/lib/decide.js +68 -14
  13. package/dist/lib/git/workflow.js +10 -1
  14. package/dist/lib/init.js +1 -1
  15. package/dist/lib/internal/codex-lifecycle.js +19 -5
  16. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  17. package/dist/lib/internal/git-workflow.js +5 -7
  18. package/dist/lib/internal/platform-pr.js +20 -7
  19. package/dist/lib/internal/release-workflow.js +3 -4
  20. package/dist/lib/internal/sandbox-control.js +79 -2
  21. package/dist/lib/internal/task-activity.js +57 -7
  22. package/dist/lib/internal/task-event.js +57 -4
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-ledger.js +59 -5
  25. package/dist/lib/internal/task-lifecycle.js +59 -8
  26. package/dist/lib/internal/task-orchestration.js +161 -288
  27. package/dist/lib/internal/task-override.js +131 -0
  28. package/dist/lib/internal/task-review.js +64 -6
  29. package/dist/lib/internal/task-verify.js +65 -3
  30. package/dist/lib/internal/task-warning.js +57 -4
  31. package/dist/lib/platform/pr-summary.js +53 -13
  32. package/dist/lib/platform/pull-requests.js +205 -41
  33. package/dist/lib/runtime/agent-runtime.js +56 -0
  34. package/dist/lib/sandbox/commands/create.js +389 -311
  35. package/dist/lib/sandbox/commands/rm.js +316 -55
  36. package/dist/lib/sandbox/control/client.js +190 -6
  37. package/dist/lib/sandbox/control/container-identity.js +1 -1
  38. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  39. package/dist/lib/sandbox/control/executor.js +145 -5
  40. package/dist/lib/sandbox/control/lifecycle.js +395 -73
  41. package/dist/lib/sandbox/control/protocol.js +67 -6
  42. package/dist/lib/sandbox/control/state.js +27 -11
  43. package/dist/lib/sandbox/engine.js +14 -7
  44. package/dist/lib/sandbox/index.js +5 -3
  45. package/dist/lib/sandbox/managed-fs.js +5 -0
  46. package/dist/lib/sandbox/mounts.js +20 -5
  47. package/dist/lib/sandbox/recovery.js +78 -10
  48. package/dist/lib/sandbox/task-resolver.js +50 -6
  49. package/dist/lib/sandbox/workspace-identity.js +27 -0
  50. package/dist/lib/sandbox/workspace-view.js +62 -40
  51. package/dist/lib/sandbox/worktree-safety.js +215 -2
  52. package/dist/lib/server/process-state.js +51 -1
  53. package/dist/lib/task/activity-intent.js +4 -1
  54. package/dist/lib/task/codex-orchestration.js +158 -33
  55. package/dist/lib/task/commands/status.js +59 -1
  56. package/dist/lib/task/commit-operation.js +335 -0
  57. package/dist/lib/task/commit-policy.js +18 -0
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +279 -17
  60. package/dist/lib/task/events.js +9 -5
  61. package/dist/lib/task/finalization.js +567 -0
  62. package/dist/lib/task/guard-override.js +31 -0
  63. package/dist/lib/task/human-override.js +1240 -0
  64. package/dist/lib/task/ledger-intents.js +6 -3
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/operation-outcome.js +33 -0
  67. package/dist/lib/task/orchestration.js +199 -719
  68. package/dist/lib/task/review-finalization.js +3 -2
  69. package/dist/lib/task/short-id.js +59 -1
  70. package/dist/lib/task/task-execution-lock.js +6 -1
  71. package/dist/lib/task/verification-engine.js +138 -59
  72. package/dist/lib/task/verification.js +15 -2
  73. package/dist/lib/task/workflow-warning-intents.js +54 -3
  74. package/dist/lib/task/write.js +2 -1
  75. package/dist/lifecycle-build-manifest.json +3 -3
  76. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  77. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
  78. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
  80. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  82. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  83. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
  84. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  85. package/lib/decide.ts +59 -14
  86. package/lib/git/workflow.ts +11 -2
  87. package/lib/init.ts +1 -1
  88. package/lib/internal/codex-lifecycle.ts +18 -5
  89. package/lib/internal/codex-sandbox-controller.ts +6 -12
  90. package/lib/internal/git-workflow.ts +5 -6
  91. package/lib/internal/platform-pr.ts +14 -7
  92. package/lib/internal/release-workflow.ts +3 -4
  93. package/lib/internal/sandbox-control.ts +92 -2
  94. package/lib/internal/task-activity.ts +46 -6
  95. package/lib/internal/task-event.ts +53 -4
  96. package/lib/internal/task-finalization.ts +68 -0
  97. package/lib/internal/task-ledger.ts +48 -4
  98. package/lib/internal/task-lifecycle.ts +56 -12
  99. package/lib/internal/task-orchestration.ts +32 -107
  100. package/lib/internal/task-override.ts +138 -0
  101. package/lib/internal/task-review.ts +51 -6
  102. package/lib/internal/task-verify.ts +52 -3
  103. package/lib/internal/task-warning.ts +44 -3
  104. package/lib/platform/pr-summary.ts +65 -14
  105. package/lib/platform/pull-requests.ts +211 -41
  106. package/lib/runtime/agent-runtime.ts +76 -0
  107. package/lib/sandbox/commands/create.ts +88 -4
  108. package/lib/sandbox/commands/rm.ts +413 -61
  109. package/lib/sandbox/config.ts +2 -1
  110. package/lib/sandbox/control/client.ts +255 -6
  111. package/lib/sandbox/control/container-identity.ts +3 -3
  112. package/lib/sandbox/control/controller-registration.ts +347 -0
  113. package/lib/sandbox/control/executor.ts +172 -6
  114. package/lib/sandbox/control/lifecycle.ts +465 -77
  115. package/lib/sandbox/control/protocol.ts +85 -9
  116. package/lib/sandbox/control/state.ts +35 -18
  117. package/lib/sandbox/engine.ts +20 -9
  118. package/lib/sandbox/index.ts +5 -3
  119. package/lib/sandbox/managed-fs.ts +5 -0
  120. package/lib/sandbox/mounts.ts +23 -5
  121. package/lib/sandbox/recovery.ts +90 -9
  122. package/lib/sandbox/task-resolver.ts +58 -7
  123. package/lib/sandbox/workspace-identity.ts +50 -2
  124. package/lib/sandbox/workspace-view.ts +73 -43
  125. package/lib/sandbox/worktree-safety.ts +236 -2
  126. package/lib/server/process-state.ts +48 -1
  127. package/lib/task/activity-intent.ts +4 -2
  128. package/lib/task/codex-orchestration.ts +167 -33
  129. package/lib/task/commands/status.ts +78 -1
  130. package/lib/task/commit-operation.ts +430 -0
  131. package/lib/task/commit-policy.ts +30 -0
  132. package/lib/task/decision-intents.ts +4 -2
  133. package/lib/task/delegation-receipts.ts +304 -19
  134. package/lib/task/events.ts +10 -5
  135. package/lib/task/finalization.ts +719 -0
  136. package/lib/task/guard-override.ts +50 -0
  137. package/lib/task/human-override.ts +1503 -0
  138. package/lib/task/ledger-intents.ts +6 -3
  139. package/lib/task/lifecycle.ts +71 -14
  140. package/lib/task/operation-outcome.ts +48 -0
  141. package/lib/task/orchestration.ts +239 -922
  142. package/lib/task/review-finalization.ts +6 -2
  143. package/lib/task/short-id.ts +65 -1
  144. package/lib/task/task-execution-lock.ts +10 -1
  145. package/lib/task/verification-engine.ts +145 -64
  146. package/lib/task/verification.ts +18 -3
  147. package/lib/task/workflow-warning-intents.ts +61 -4
  148. package/lib/task/write.ts +4 -1
  149. package/package.json +2 -2
  150. package/templates/.agents/QUICKSTART.en.md +2 -2
  151. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  152. package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
  153. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
  154. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
  155. package/templates/.agents/rules/task-management.en.md +2 -2
  156. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  157. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  158. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  159. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  160. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  161. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  162. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  163. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  164. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  165. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  166. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  167. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  168. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  170. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  171. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  172. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  173. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  174. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  175. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  176. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  177. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  178. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  179. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  180. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  181. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  182. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  183. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  184. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  185. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  186. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  187. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  188. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
  189. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  190. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  191. package/templates/.codex/hooks.json +4 -4
  192. package/dist/lib/task/commit-finalization.js +0 -305
  193. package/dist/lib/task/commit-intent.js +0 -165
  194. package/lib/task/commit-finalization.ts +0 -419
  195. 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()) {
@@ -38,12 +38,39 @@ function controlFromEnvironment() {
38
38
  const generation = process.env.AGENT_INFRA_CONTROL_GENERATION;
39
39
  const channelDir = process.env.AGENT_INFRA_CONTROL_DIR;
40
40
  const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
41
- if (!token || !generation || !channelDir || !statusDir) {
41
+ const runtimeDir = process.env.AGENT_INFRA_RUNTIME_DIR;
42
+ if (!token || !generation || !channelDir || !statusDir || !runtimeDir) {
42
43
  throw new Error('CODEX_SANDBOX_CONTROLLER_CONTROL_MISSING');
43
44
  }
44
- return { token, generation, channelDir, statusDir };
45
+ return { token, generation, channelDir, statusDir, runtimeDir };
46
+ }
47
+ function verifyRuntime(runtimeDir) {
48
+ let stat;
49
+ try {
50
+ stat = fs.lstatSync(runtimeDir);
51
+ }
52
+ catch {
53
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
54
+ }
55
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
56
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
57
+ }
58
+ const probe = path.join(runtimeDir, `.controller-${process.pid}-${crypto.randomUUID()}.probe`);
59
+ try {
60
+ fs.writeFileSync(probe, 'runtime-probe\n', { encoding: 'utf8', flag: 'wx', mode: 0o600 });
61
+ if (fs.readFileSync(probe, 'utf8') !== 'runtime-probe\n') {
62
+ throw new Error('probe mismatch');
63
+ }
64
+ }
65
+ catch {
66
+ throw new Error('CODEX_SANDBOX_CONTROLLER_RUNTIME_UNAVAILABLE');
67
+ }
68
+ finally {
69
+ fs.rmSync(probe, { force: true });
70
+ }
45
71
  }
46
72
  function verifyControl(taskId, control) {
73
+ verifyRuntime(control.runtimeDir);
47
74
  let status = null;
48
75
  let lastError = null;
49
76
  for (let attempt = 0; attempt < 5; attempt += 1) {
@@ -81,6 +108,26 @@ function verifyControl(taskId, control) {
81
108
  if (payload.taskId !== taskId)
82
109
  throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
83
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
+ }
84
131
  function detectedCodexVersion() {
85
132
  const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
86
133
  const version = /codex-cli\s+(\d+\.\d+\.\d+)/u.exec(result.stdout ?? '')?.[1];
@@ -153,6 +200,7 @@ function isolatedEnvironment(home, shimDir, contextPath, taskId, control, provid
153
200
  AGENT_INFRA_CONTROL_GENERATION: control.generation,
154
201
  AGENT_INFRA_CONTROL_DIR: control.channelDir,
155
202
  AGENT_INFRA_CONTROL_STATUS_DIR: control.statusDir,
203
+ AGENT_INFRA_RUNTIME_DIR: control.runtimeDir,
156
204
  AGENT_INFRA_CODEX_CONTROLLER_CONTEXT: contextPath
157
205
  };
158
206
  }
@@ -163,7 +211,6 @@ function prepareCodexSandboxController(input, options = {}) {
163
211
  const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
164
212
  const sourceHome = path.resolve(options.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex'));
165
213
  const control = options.control ?? controlFromEnvironment();
166
- (options.verifyTaskBinding ?? verifyControl)(input.taskId, control);
167
214
  const version = (options.codexVersion ?? detectedCodexVersion)();
168
215
  if (!semver.gte(version, '0.147.0'))
169
216
  throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNSUPPORTED');
@@ -172,48 +219,36 @@ function prepareCodexSandboxController(input, options = {}) {
172
219
  fs.mkdirSync(runtimeRoot, { recursive: true, mode: 0o700 });
173
220
  fs.chmodSync(runtimeRoot, 0o700);
174
221
  const key = crypto.createHash('sha256')
175
- .update(`${input.taskId}\0${control.generation}`)
222
+ .update(`${process.pid}\0${control.generation}`)
176
223
  .digest('hex');
177
- const leasePath = path.join(runtimeRoot, `${key}.lease.json`);
178
- if (fs.existsSync(leasePath)) {
179
- const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
180
- if (Number.isSafeInteger(lease.pid) && typeof lease.startTime === 'number'
181
- && getProcessStartTime(lease.pid) === lease.startTime) {
182
- throw new Error('CODEX_SANDBOX_CONTROLLER_BUSY');
183
- }
184
- fs.unlinkSync(leasePath);
185
- }
186
224
  const parentStartTime = getProcessStartTime(process.pid);
187
225
  if (!parentStartTime)
188
226
  throw new Error('CODEX_SANDBOX_CONTROLLER_PROCESS_IDENTITY_INVALID');
189
- const instanceDigest = crypto.randomBytes(32).toString('hex');
190
- fs.writeFileSync(leasePath, `${JSON.stringify({
191
- version: 1,
192
- pid: process.pid,
193
- startTime: parentStartTime,
194
- taskId: input.taskId,
195
- generation: control.generation,
196
- instanceDigest
197
- })}\n`, { mode: 0o600, flag: 'wx' });
198
227
  const home = fs.mkdtempSync(path.join(runtimeRoot, `${key}-`));
199
228
  fs.chmodSync(home, 0o700);
200
229
  let cleaned = false;
230
+ let context = null;
201
231
  const cleanup = () => {
202
232
  if (cleaned)
203
233
  return;
204
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
+ }
205
248
  const relative = path.relative(runtimeRoot, home);
206
249
  if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
207
250
  fs.rmSync(home, { recursive: true, force: true });
208
251
  }
209
- try {
210
- const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8'));
211
- if (lease.instanceDigest === instanceDigest)
212
- fs.unlinkSync(leasePath);
213
- }
214
- catch {
215
- // A missing or replaced lease is owned by another cleanup path.
216
- }
217
252
  };
218
253
  try {
219
254
  const auth = path.join(sourceHome, 'auth.json');
@@ -235,22 +270,25 @@ function prepareCodexSandboxController(input, options = {}) {
235
270
  throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
236
271
  }
237
272
  const buildIdentity = computeLifecycleBuildIdentity(repoRoot);
238
- const now = (options.now ?? Date.now)();
239
- const context = Object.freeze({
240
- version: 1,
241
- taskId: input.taskId,
242
- controlGeneration: control.generation,
243
- controllerInstanceDigest: instanceDigest,
244
- parentPid: process.pid,
245
- parentStartTime,
246
- issuedAt: now,
247
- expiresAt: now + CONTEXT_TTL_MS,
248
- 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, {
249
280
  hookDefinitionHash: crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex'),
250
281
  lifecycleProfilesHash: digestFiles([executor, reviewer])
251
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);
252
290
  const contextPath = path.join(home, 'controller-context.json');
253
- fs.writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600 });
291
+ writeCodexSandboxControllerContext(contextPath, context);
254
292
  const shimDir = path.join(home, 'bin');
255
293
  fs.mkdirSync(shimDir, { mode: 0o700 });
256
294
  const internalCli = path.resolve(process.argv[1] ?? path.join(repoRoot, 'bin', 'internal-cli.ts'));
@@ -264,7 +302,7 @@ function prepareCodexSandboxController(input, options = {}) {
264
302
  input.reviewerModel ? `--reviewer-model ${input.reviewerModel}` : '',
265
303
  input.reviewerReasoningEffort ? `--reviewer-reasoning-effort ${input.reviewerReasoningEffort}` : ''
266
304
  ].filter(Boolean).join(' ');
267
- const prompt = `$run-task ${input.taskRef}${policy ? ` ${policy}` : ''}`;
305
+ const prompt = `$run-task ${taskId}${policy ? ` ${policy}` : ''}`;
268
306
  const args = Object.freeze([
269
307
  'exec',
270
308
  '--enable', 'hooks',
@@ -275,7 +313,7 @@ function prepareCodexSandboxController(input, options = {}) {
275
313
  '-C', repoRoot,
276
314
  prompt
277
315
  ]);
278
- 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);
279
317
  return Object.freeze({
280
318
  command: 'codex',
281
319
  args,
@@ -291,27 +329,6 @@ function prepareCodexSandboxController(input, options = {}) {
291
329
  throw error;
292
330
  }
293
331
  }
294
- function verifyCodexSandboxControllerContext(contextPath, taskId, options = {}) {
295
- const stat = fs.lstatSync(contextPath);
296
- if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) {
297
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
298
- }
299
- const context = JSON.parse(fs.readFileSync(contextPath, 'utf8'));
300
- const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
301
- if (context.version !== 1
302
- || context.taskId !== taskId
303
- || context.controlGeneration !== generation
304
- || context.expiresAt < (options.now ?? Date.now())
305
- || getProcessStartTime(context.parentPid) !== context.parentStartTime
306
- || !isProcessAlive(context.parentPid)) {
307
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
308
- }
309
- const current = computeLifecycleBuildIdentity(options.repoRoot ?? process.cwd());
310
- if (JSON.stringify(current) !== JSON.stringify(context.buildIdentity)) {
311
- throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
312
- }
313
- return Object.freeze(context);
314
- }
315
332
  async function runCodexSandboxController(input, options = {}) {
316
333
  const prepared = prepareCodexSandboxController(input, options);
317
334
  try {
@@ -366,5 +383,5 @@ async function runCodexSandboxController(input, options = {}) {
366
383
  prepared.cleanup();
367
384
  }
368
385
  }
369
- export { prepareCodexSandboxController, runCodexSandboxController, verifyCodexSandboxControllerContext };
386
+ export { prepareCodexSandboxController, runCodexSandboxController, verifyCodexSandboxControllerContext, controllerProofFromContext };
370
387
  //# sourceMappingURL=sandbox-controller.js.map
@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { createCodexLifecycleState, expireCodexLifecycleState, reduceCodexLifecycleEvent } from './evidence.js';
5
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js';
5
6
  const LOCK_RETRY_MS = 10;
6
7
  const LOCK_TIMEOUT_MS = 1_000;
7
8
  const LOCK_STALE_MS = 30_000;
@@ -58,7 +59,7 @@ function writeRecord(file, record, expectedRevision) {
58
59
  }
59
60
  }
60
61
  function createCodexLifecycleStore(options) {
61
- const root = options.root ?? path.join(process.cwd(), '.agents', 'workspace', '.runtime', 'codex-lifecycle');
62
+ const root = options.root ?? resolveAgentRuntimeStoreRoot({ store: 'lifecycle' });
62
63
  const now = options.now ?? (() => new Date().toISOString());
63
64
  fs.mkdirSync(root, { recursive: true, mode: 0o700 });
64
65
  fs.chmodSync(root, 0o700);
@@ -2,25 +2,44 @@ import { VERSION } from './version.js';
2
2
  import { parseTaskScope } from './task/command-options.js';
3
3
  import { applyHumanDecision } from './task/decision-intents.js';
4
4
  import { canonicalTimestamp } from './task/write.js';
5
+ import { consumeHumanOverride, failureId } from './task/human-override.js';
6
+ import { resolveTaskRef } from './task/resolve-ref.js';
7
+ import { TaskExecutionLockError, withTaskExecutionLock } from './task/task-execution-lock.js';
5
8
  function parseDecisionParts(parts) {
6
9
  const decision = [];
7
10
  let needsImplementation;
11
+ let overrideTicket;
12
+ let overrideTarget;
13
+ let overrideScope;
8
14
  for (let index = 0; index < parts.length; index += 1) {
9
- if (parts[index] !== '--needs-implementation') {
10
- decision.push(parts[index]);
11
- continue;
15
+ const part = parts[index];
16
+ if (part === '--needs-implementation') {
17
+ if (needsImplementation !== undefined)
18
+ throw new Error("duplicate option '--needs-implementation'");
19
+ const value = parts[++index];
20
+ if (value !== 'true' && value !== 'false') {
21
+ throw new Error("--needs-implementation must be 'true' or 'false'");
22
+ }
23
+ needsImplementation = value === 'true';
12
24
  }
13
- if (needsImplementation !== undefined)
14
- throw new Error("duplicate option '--needs-implementation'");
15
- const value = parts[++index];
16
- if (value !== 'true' && value !== 'false') {
17
- throw new Error("--needs-implementation must be 'true' or 'false'");
25
+ else if (part === '--override-ticket' || part === '--override-target' || part === '--override-scope') {
26
+ const value = parts[++index];
27
+ if (!value)
28
+ throw new Error(`${part} requires a value`);
29
+ if (part === '--override-ticket')
30
+ overrideTicket = value;
31
+ else if (part === '--override-target')
32
+ overrideTarget = value;
33
+ else
34
+ overrideScope = value;
35
+ }
36
+ else {
37
+ decision.push(parts[index]);
18
38
  }
19
- needsImplementation = value === 'true';
20
39
  }
21
40
  if (decision.length === 0)
22
41
  throw new Error('decision content is required');
23
- return { decision: decision.join(' '), needsImplementation };
42
+ return { decision: decision.join(' '), needsImplementation, overrideTicket, overrideTarget, overrideScope };
24
43
  }
25
44
  export async function decide(args, options = {}) {
26
45
  let scope;
@@ -79,19 +98,54 @@ export async function decide(args, options = {}) {
79
98
  try {
80
99
  const parsedDecision = parseDecisionParts(decisionParts);
81
100
  const now = (options.now ?? canonicalTimestamp)();
82
- const result = applyHumanDecision({
101
+ const request = {
83
102
  taskRef, selector, decision: parsedDecision.decision,
84
103
  needsImplementation: parsedDecision.needsImplementation
85
- }, {
104
+ };
105
+ const writeOptions = {
86
106
  repoRoot: options.repoRoot,
87
107
  metadataProvider: () => ({ timestamp: now, agentInfraVersion: options.version ?? VERSION })
88
- });
108
+ };
109
+ const resolved = taskRef ? resolveTaskRef(taskRef, { repoRoot: options.repoRoot }) : null;
110
+ let result;
111
+ const execute = () => {
112
+ let current = applyHumanDecision(request, writeOptions);
113
+ if (current.status !== 'failed' || !parsedDecision.overrideTicket)
114
+ return current;
115
+ if (!parsedDecision.overrideTarget || !parsedDecision.overrideScope)
116
+ throw new Error('override ticket requires --override-target and --override-scope');
117
+ const consumed = consumeHumanOverride({
118
+ taskRef: resolved?.ok ? resolved.taskId : String(taskRef ?? ''),
119
+ ticketId: parsedDecision.overrideTicket,
120
+ failureId: failureId('decision-intent', current.error?.code ?? 'TASK_STATE_MISMATCH'),
121
+ target: parsedDecision.overrideTarget,
122
+ scope: parsedDecision.overrideScope
123
+ }, {
124
+ ...writeOptions,
125
+ effectExecutor: (capability) => {
126
+ const retried = applyHumanDecision(request, { ...writeOptions, manualOverride: capability });
127
+ current = retried;
128
+ return retried.status === 'failed' || retried.status === 'planned'
129
+ ? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no decision effect was committed' : `${retried.error?.code ?? 'DECISION_FAILED'}: ${retried.error?.message ?? 'manual decision effect failed'}` }
130
+ : null;
131
+ }
132
+ });
133
+ if (consumed.status === 'failed')
134
+ throw new Error(consumed.error.message);
135
+ return current;
136
+ };
137
+ result = resolved?.ok
138
+ ? withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-decision', execute)
139
+ : execute();
89
140
  if (result.error)
90
141
  throw new Error(result.error.message);
91
142
  return 0;
92
143
  }
93
144
  catch (error) {
94
- process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
145
+ const message = error instanceof TaskExecutionLockError
146
+ ? `${error.code}: ${error.message}`
147
+ : error instanceof Error ? error.message : String(error);
148
+ process.stderr.write(`Error: ${message}\n`);
95
149
  return 1;
96
150
  }
97
151
  }
@@ -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 };
package/dist/lib/init.js CHANGED
@@ -180,7 +180,7 @@ async function cmdInit() {
180
180
  agentClients: serializeAgentClients(defaultState)
181
181
  };
182
182
  if (sandboxEngine)
183
- config.sandbox.engine = sandboxEngine;
183
+ config.sandbox.engine = { [currentPlatform]: sandboxEngine };
184
184
  if (templateSources.length > 0)
185
185
  config.templates = { sources: templateSources };
186
186
  if (skillSources.length > 0)
@@ -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,20 +10,19 @@ 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',
22
23
  'lib/render.ts',
23
24
  'lib/builtin-tuis.ts',
24
- 'lib/sandbox/engines/',
25
- 'src/sync-templates.js',
26
- 'templates/'
25
+ 'lib/sandbox/engines/'
27
26
  ];
28
27
  const DEMO_DIGEST_PATH = 'assets/demo-init.inputs.sha256';
29
28
  const DEMO_OUTPUT_PATH = 'assets/demo-init.gif';