@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
@@ -11,15 +11,25 @@ import { inspectPlatformIssue } from './issues.js';
11
11
  import { planPullRequestMetadata } from './pull-request-metadata.js';
12
12
  import { platformResult } from './types.js';
13
13
  import { inspectCompletionArtifacts } from '../task/finalization-artifacts.js';
14
- import { inspectCreatePrCommitGate } from '../task/commit-finalization.js';
15
14
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
15
+ import { mergeOperationWarnings } from '../task/operation-outcome.js';
16
+ function warningResultForPrimary(primaryResult) {
17
+ if (primaryResult === 'pr_created')
18
+ return 'pr_created_with_warnings';
19
+ if (primaryResult === 'pr_reused')
20
+ return 'pr_reused_with_warnings';
21
+ return 'no_op_with_warnings';
22
+ }
16
23
  function result(status, taskId, issueNumber, prNumber, overrides = {}) {
17
- return {
24
+ const output = {
18
25
  ...platformResult(status),
19
26
  task: { id: taskId, issueNumber, prNumber },
20
27
  pullRequest: null,
28
+ result: null,
29
+ warnings: [],
21
30
  ...overrides
22
31
  };
32
+ return { ...output, warnings: mergeOperationWarnings(output.warnings) };
23
33
  }
24
34
  function externalResult(base, overrides = {}) {
25
35
  return {
@@ -202,7 +212,7 @@ function expectedHead(repository, head) {
202
212
  const repoName = repository.split('/')[1] || '';
203
213
  return { repository: `${head.slice(0, colon)}/${repoName}`, ref: head.slice(colon + 1) };
204
214
  }
205
- function selectPullRequest(remotes, repository, head, base) {
215
+ function selectPullRequest(remotes, repository, head, base, expectedSha) {
206
216
  const wanted = expectedHead(repository, head);
207
217
  const matches = remotes.flatMap((remote) => {
208
218
  const normalized = normalizePullRequest(remote, repository);
@@ -210,10 +220,89 @@ function selectPullRequest(remotes, repository, head, base) {
210
220
  normalized.head.ref === wanted.ref && normalized.base.repository === repository && normalized.base.ref === base
211
221
  ? [normalized] : [];
212
222
  });
223
+ if (expectedSha && matches.some((match) => match.head.sha !== expectedSha)) {
224
+ return { status: 'head-mismatch', pullRequest: null };
225
+ }
213
226
  if (matches.length === 1)
214
227
  return { status: 'resolved', pullRequest: matches[0] };
215
228
  return { status: matches.length === 0 ? 'missing' : 'ambiguous', pullRequest: null };
216
229
  }
230
+ function repositoryHead(cwd) {
231
+ return execFileSync('git', ['rev-parse', 'HEAD'], {
232
+ cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
233
+ }).trim();
234
+ }
235
+ function remoteBranchHead(cwd, repository, head) {
236
+ const wanted = expectedHead(repository, head);
237
+ if (!wanted.ref || !/^[A-Za-z0-9._/-]+$/.test(wanted.ref))
238
+ return {
239
+ ok: false,
240
+ error: { code: 'PR_HEAD_INVALID', message: 'Pull request head ref is invalid', retryable: false }
241
+ };
242
+ const remotes = configuredGitRemotes(cwd);
243
+ const remote = remotes.find((item) => repositoryKey(item.repository) === repositoryKey(wanted.repository))
244
+ ?? remotes.find((item) => item.name === 'origin');
245
+ if (!remote)
246
+ return {
247
+ ok: false,
248
+ error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `No configured remote can verify ${wanted.repository}:${wanted.ref}`, retryable: false }
249
+ };
250
+ try {
251
+ const output = execFileSync('git', ['ls-remote', '--refs', remote.name, `refs/heads/${wanted.ref}`], {
252
+ cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
253
+ }).trim();
254
+ const match = output.split(/\r?\n/).map((line) => line.trim()).find((line) => line.endsWith(`refs/heads/${wanted.ref}`));
255
+ const sha = match?.split(/\s+/)[0] || null;
256
+ if (!sha || !/^[a-f0-9]{40}$/i.test(sha))
257
+ return {
258
+ ok: false,
259
+ error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `Remote branch ${wanted.repository}:${wanted.ref} does not exist`, retryable: false }
260
+ };
261
+ return { ok: true, value: sha };
262
+ }
263
+ catch (error) {
264
+ return {
265
+ ok: false,
266
+ error: { code: 'PR_REMOTE_BRANCH_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: true }
267
+ };
268
+ }
269
+ }
270
+ function verifyCreateHead(base, head) {
271
+ let localHead;
272
+ try {
273
+ localHead = repositoryHead(base.resolved.repoRoot);
274
+ }
275
+ catch (error) {
276
+ return { ok: false, error: { code: 'PR_LOCAL_HEAD_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false } };
277
+ }
278
+ const remote = remoteBranchHead(base.resolved.repoRoot, base.context.platform.repository, head);
279
+ if (!remote.ok)
280
+ return remote;
281
+ if (remote.value !== localHead)
282
+ return {
283
+ ok: false,
284
+ error: { code: 'PR_REMOTE_HEAD_MISMATCH', message: `Remote head ${remote.value} does not match local HEAD ${localHead}`, retryable: false }
285
+ };
286
+ return { ok: true, value: localHead };
287
+ }
288
+ function validateBoundPullRequest(base, pullRequest, head, baseRef, expectedSha) {
289
+ if (!pullRequest)
290
+ return {
291
+ ok: false,
292
+ error: { code: 'PR_BIND_RECHECK_FAILED', message: 'Bound pull request identity is unavailable', retryable: false }
293
+ };
294
+ const wanted = expectedHead(base.context.platform.repository, head);
295
+ if (pullRequest.head.repository !== wanted.repository
296
+ || pullRequest.head.ref !== wanted.ref
297
+ || pullRequest.head.sha !== expectedSha
298
+ || pullRequest.base.repository !== base.context.platform.repository
299
+ || pullRequest.base.ref !== baseRef)
300
+ return {
301
+ ok: false,
302
+ error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Bound pull request identity does not match the current expected head/base', retryable: false }
303
+ };
304
+ return { ok: true };
305
+ }
217
306
  function resolvedContext(taskRef, options) {
218
307
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
219
308
  if (!resolved.ok)
@@ -338,19 +427,22 @@ registerPlatformCapabilities('github', {
338
427
  return resolveGitHubChangeRequestGitEvidence(context);
339
428
  }
340
429
  });
341
- function locatePullRequest(base, head, target) {
430
+ function locatePullRequest(base, head, target, expectedSha) {
342
431
  const repository = base.context.platform.repository;
343
432
  const listed = base.client.json([
344
433
  'api', `repos/${repository}/pulls?state=open&base=${encodeURIComponent(target)}&per_page=100`
345
434
  ], { cwd: base.resolved.repoRoot });
346
435
  if (!listed.ok)
347
436
  return listed;
348
- const selected = selectPullRequest(listed.value, repository, head, target);
437
+ const selected = selectPullRequest(listed.value, repository, head, target, expectedSha);
349
438
  return selected.status === 'resolved'
350
439
  ? { ok: true, value: selected.pullRequest }
351
440
  : { ok: false, error: {
352
- code: selected.status === 'ambiguous' ? 'PR_IDENTITY_AMBIGUOUS' : 'PR_NOT_FOUND',
353
- message: selected.status === 'ambiguous' ? 'Multiple pull requests match the exact head/base identity' : 'No pull request matches the exact head/base identity',
441
+ code: selected.status === 'ambiguous' ? 'PR_IDENTITY_AMBIGUOUS'
442
+ : selected.status === 'head-mismatch' ? 'PR_HEAD_SHA_MISMATCH' : 'PR_NOT_FOUND',
443
+ message: selected.status === 'ambiguous' ? 'Multiple pull requests match the exact head/base identity'
444
+ : selected.status === 'head-mismatch' ? 'A pull request matches head/base but not the expected head SHA'
445
+ : 'No pull request matches the exact head/base identity',
354
446
  retryable: false
355
447
  } };
356
448
  }
@@ -622,28 +714,47 @@ function createPlatformPullRequestUnlocked(taskRef, options) {
622
714
  return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
623
715
  error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
624
716
  });
625
- if (base.prNumber)
626
- return inspectPlatformPullRequest(taskRef, options);
627
- const gate = inspectCreatePrCommitGate(base.resolved.taskDir, base.resolved.repoRoot, base.resolved.taskId);
628
- if (!gate.allowed)
629
- return result('blocked', base.resolved.taskId, base.issueNumber, null, {
717
+ if (base.prNumber) {
718
+ const headCheck = verifyCreateHead(base, options.head);
719
+ if (!headCheck.ok)
720
+ return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
721
+ platform: base.context.platform,
722
+ capabilities: base.context.capabilities,
723
+ error: headCheck.error
724
+ });
725
+ const inspected = inspectPlatformPullRequest(taskRef, options);
726
+ const identity = validateBoundPullRequest(base, inspected.pullRequest, options.head, options.base, headCheck.value);
727
+ if (!identity.ok)
728
+ return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
729
+ platform: base.context.platform,
730
+ capabilities: base.context.capabilities,
731
+ pullRequest: inspected.pullRequest,
732
+ error: identity.error
733
+ });
734
+ return { ...inspected, result: 'no_op' };
735
+ }
736
+ const headCheck = verifyCreateHead(base, options.head);
737
+ if (!headCheck.ok)
738
+ return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
630
739
  platform: base.context.platform,
631
740
  capabilities: base.context.capabilities,
632
- error: {
633
- code: gate.code,
634
- message: `${gate.message}; action=${gate.action}`,
635
- retryable: gate.action === 'rerun-commit' || gate.action === 'rerun-review-code'
636
- }
741
+ error: headCheck.error
637
742
  });
638
- const located = locatePullRequest(base, options.head, options.base);
743
+ const expectedHeadSha = headCheck.value;
744
+ const located = locatePullRequest(base, options.head, options.base, expectedHeadSha);
639
745
  if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS')
640
746
  return result('failed', base.resolved.taskId, base.issueNumber, null, {
641
747
  platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
642
748
  });
749
+ if (!located.ok && located.error.code === 'PR_HEAD_SHA_MISMATCH')
750
+ return result('failed', base.resolved.taskId, base.issueNumber, null, {
751
+ platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
752
+ });
643
753
  if (options.dryRun)
644
754
  return result('planned', base.resolved.taskId, base.issueNumber, null, {
645
755
  platform: base.context.platform, capabilities: base.context.capabilities,
646
- operations: [{ name: located.ok ? 'pr:reuse' : 'pr:create', status: 'planned', reasonCode: null }], error: null
756
+ operations: [{ name: located.ok ? 'pr:reuse' : 'pr:create', status: 'planned', reasonCode: null }],
757
+ result: located.ok ? 'pr_reused' : 'pr_created', error: null
647
758
  });
648
759
  const started = writeCreateStarted(base, options.agent);
649
760
  if (started.status === 'failed')
@@ -662,7 +773,7 @@ function createPlatformPullRequestUnlocked(taskRef, options) {
662
773
  if (response.ok)
663
774
  pullRequest = normalizePullRequest(response.value, repository);
664
775
  else if (response.error.retryable) {
665
- const recovered = locatePullRequest(base, options.head, options.base);
776
+ const recovered = locatePullRequest(base, options.head, options.base, expectedHeadSha);
666
777
  if (recovered.ok)
667
778
  pullRequest = recovered.value;
668
779
  else
@@ -681,8 +792,44 @@ function createPlatformPullRequestUnlocked(taskRef, options) {
681
792
  return result('failed', base.resolved.taskId, base.issueNumber, null, {
682
793
  error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'PR create response lacks validated identity', retryable: false }
683
794
  });
795
+ if (pullRequest.head.sha !== expectedHeadSha)
796
+ return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
797
+ platform: base.context.platform, capabilities: base.context.capabilities,
798
+ resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
799
+ error: { code: 'PR_HEAD_SHA_MISMATCH', message: 'Created pull request head does not match the expected local HEAD', retryable: false }
800
+ });
684
801
  created = true;
685
802
  }
803
+ const bindHeadCheck = verifyCreateHead(base, options.head);
804
+ if (!bindHeadCheck.ok || bindHeadCheck.value !== expectedHeadSha) {
805
+ const error = bindHeadCheck.ok
806
+ ? { code: 'PR_REMOTE_HEAD_MISMATCH', message: 'Remote head changed before task binding', retryable: false }
807
+ : bindHeadCheck.error;
808
+ return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
809
+ platform: base.context.platform, capabilities: base.context.capabilities,
810
+ resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
811
+ });
812
+ }
813
+ const beforeBind = inspectGitHubPullRequest(base.client, base.context.platform.repository, pullRequest.number, base.resolved.repoRoot);
814
+ if (!beforeBind.ok || !beforeBind.value) {
815
+ const error = beforeBind.ok
816
+ ? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false }
817
+ : beforeBind.error;
818
+ return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
819
+ platform: base.context.platform, capabilities: base.context.capabilities,
820
+ resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
821
+ });
822
+ }
823
+ if (beforeBind.value.head.sha !== expectedHeadSha
824
+ || beforeBind.value.head.ref !== expectedHead(base.context.platform.repository, options.head).ref
825
+ || beforeBind.value.base.ref !== options.base
826
+ || beforeBind.value.base.repository !== base.context.platform.repository)
827
+ return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
828
+ platform: base.context.platform, capabilities: base.context.capabilities,
829
+ resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest: beforeBind.value,
830
+ error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
831
+ });
832
+ pullRequest = beforeBind.value;
686
833
  const refreshed = resolvedContext(taskRef, options);
687
834
  if (!refreshed.ok)
688
835
  return result('failed', base.resolved.taskId, base.issueNumber, null, {
@@ -699,7 +846,8 @@ function createPlatformPullRequestUnlocked(taskRef, options) {
699
846
  return result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
700
847
  changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
701
848
  resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
702
- operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }], error: null
849
+ operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
850
+ result: created ? 'pr_created' : 'pr_reused', error: null
703
851
  });
704
852
  }
705
853
  function createPlatformPullRequest(taskRef, options) {
@@ -711,8 +859,6 @@ function createPlatformPullRequest(taskRef, options) {
711
859
  error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
712
860
  });
713
861
  }
714
- if (base.prNumber)
715
- return inspectPlatformPullRequest(taskRef, options);
716
862
  try {
717
863
  return withTaskExecutionLock(base.resolved.repoRoot, base.resolved.taskId, 'platform-pr.create', () => createPlatformPullRequestUnlocked(taskRef, options));
718
864
  }
@@ -736,29 +882,46 @@ function createPlatformPullRequest(taskRef, options) {
736
882
  }
737
883
  }
738
884
  function syncPlatformPullRequest(taskRef, options) {
885
+ const warningResult = warningResultForPrimary(options.primaryResult);
886
+ const softenFailure = (output) => {
887
+ const warning = output.error && output.task.prNumber !== null
888
+ && !['PR_NOT_LINKED', 'PR_BIND_CONFLICT'].includes(output.error.code)
889
+ ? {
890
+ code: output.error.code,
891
+ message: output.error.message,
892
+ retryable: output.error.retryable,
893
+ step: 'pr-metadata',
894
+ target: `pull-request:${output.task.prNumber}`,
895
+ severity: 'ACTION_REQUIRED'
896
+ }
897
+ : null;
898
+ return warning
899
+ ? { ...output, status: 'applied', changed: false, error: null, result: warningResult, warnings: mergeOperationWarnings([warning]) }
900
+ : output;
901
+ };
739
902
  const base = resolvedContext(taskRef, options);
740
903
  if (!base.ok)
741
- return base.output;
904
+ return softenFailure(base.output);
742
905
  if (!base.prNumber)
743
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
906
+ return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
744
907
  error: { code: 'PR_NOT_LINKED', message: 'Task has no valid pr_number', retryable: false }
745
- });
908
+ }));
746
909
  const fetched = inspectGitHubPullRequest(base.client, base.context.platform.repository, base.prNumber, base.resolved.repoRoot);
747
910
  if (!fetched.ok)
748
- return result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error });
911
+ return softenFailure(result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error }));
749
912
  if (!options.metadata && !options.closingIssue)
750
- return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
913
+ return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
751
914
  error: { code: 'PR_PAYLOAD_INVALID', message: 'sync requires a desired-state option', retryable: false }
752
- });
915
+ }));
753
916
  if (!base.issueNumber)
754
- return result('degraded', base.resolved.taskId, null, base.prNumber, {
917
+ return softenFailure(result('degraded', base.resolved.taskId, null, base.prNumber, {
755
918
  pullRequest: fetched.value, error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no linked Issue to copy metadata from', retryable: false }
756
- });
919
+ }));
757
920
  const issue = inspectPlatformIssue(taskRef, { cwd: base.resolved.repoRoot, client: base.client });
758
921
  if (!issue.issue)
759
- return result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
922
+ return softenFailure(result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
760
923
  platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: fetched.value, error: issue.error
761
- });
924
+ }));
762
925
  const planned = planPullRequestMetadata({
763
926
  pullRequest: fetched.value,
764
927
  issue: issue.issue,
@@ -769,9 +932,9 @@ function syncPlatformPullRequest(taskRef, options) {
769
932
  .filter((operation) => options.closingIssue || operation.name !== 'closing-issue');
770
933
  const operations = planned.map(({ name, status, reasonCode }) => ({ name, status, reasonCode }));
771
934
  if (options.dryRun)
772
- return result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
935
+ return softenFailure(result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
773
936
  platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: fetched.value, operations, error: null
774
- });
937
+ }));
775
938
  const payload = {};
776
939
  for (const operation of planned) {
777
940
  if (operation.status !== 'planned')
@@ -785,27 +948,28 @@ function syncPlatformPullRequest(taskRef, options) {
785
948
  if (operation.name === 'milestone') {
786
949
  const milestones = base.client.json(['api', '--paginate', '--slurp', `repos/${base.context.platform.repository}/milestones?state=open&per_page=100`], { cwd: base.resolved.repoRoot });
787
950
  if (!milestones.ok)
788
- return result(milestones.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: milestones.error });
951
+ return softenFailure(result(milestones.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: milestones.error }));
789
952
  const flat = milestones.value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]);
790
953
  payload.milestone = flat.find((entry) => entry.title === operation.value)?.number ?? null;
791
954
  }
792
955
  }
793
956
  if (Object.keys(payload).length === 0) {
794
957
  const degraded = planned.some((operation) => operation.status === 'skipped');
795
- return result(degraded ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
958
+ return softenFailure(result(degraded ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
796
959
  platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value, operations, error: null
797
- });
960
+ }));
798
961
  }
799
962
  const patched = base.client.json(['api', `repos/${base.context.platform.repository}/issues/${base.prNumber}`, '-X', 'PATCH', '--input', '-'], {
800
963
  cwd: base.resolved.repoRoot, method: 'PATCH', input: JSON.stringify(payload)
801
964
  });
802
965
  if (!patched.ok)
803
- return result(patched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: patched.error });
804
- return result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
966
+ return softenFailure(result(patched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: patched.error }));
967
+ return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
805
968
  changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
806
969
  resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value,
807
970
  operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
808
- });
971
+ }));
809
972
  }
810
973
  export { bindPlatformPullRequest, createPlatformPullRequest, inspectGitHubPullRequest, inspectGitHubIssueClosingChangeRequests, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, normalizePullRequest, resolveGitHubChangeRequestGitEvidence, resolveExternalPullRequest, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest };
974
+ export { warningResultForPrimary };
811
975
  //# sourceMappingURL=pull-requests.js.map
@@ -0,0 +1,56 @@
1
+ import path from 'node:path';
2
+ const CLIENT_NAMESPACE = /^[a-z][a-z0-9-]{0,31}$/u;
3
+ function runtimeError(code, message) {
4
+ const error = new Error(`${code}: ${message}`);
5
+ error.name = code;
6
+ return error;
7
+ }
8
+ function boundControlContext(env) {
9
+ return [
10
+ 'AGENT_INFRA_CONTROL_TOKEN',
11
+ 'AGENT_INFRA_CONTROL_GENERATION',
12
+ 'AGENT_INFRA_EXECUTOR_MANIFEST',
13
+ 'AGENT_INFRA_CODEX_CONTROLLER_CONTEXT'
14
+ ].some((key) => typeof env[key] === 'string' && env[key].length > 0);
15
+ }
16
+ function clientNamespace(client) {
17
+ if (!CLIENT_NAMESPACE.test(client)) {
18
+ throw runtimeError('AGENT_INFRA_RUNTIME_CLIENT_INVALID', `invalid runtime client namespace '${client}'`);
19
+ }
20
+ return client;
21
+ }
22
+ export function resolveAgentRuntimeRoot(options = {}) {
23
+ const env = options.env ?? process.env;
24
+ const configured = env.AGENT_INFRA_RUNTIME_DIR;
25
+ if (configured !== undefined && configured.length > 0) {
26
+ if (!path.isAbsolute(configured)) {
27
+ throw runtimeError('AGENT_INFRA_RUNTIME_DIR_INVALID', 'runtime directory must be absolute');
28
+ }
29
+ return path.resolve(configured);
30
+ }
31
+ if (boundControlContext(env)) {
32
+ throw runtimeError('AGENT_INFRA_RUNTIME_DIR_REQUIRED', 'task-bound control context requires AGENT_INFRA_RUNTIME_DIR');
33
+ }
34
+ return path.join(path.resolve(options.repoRoot ?? process.cwd()), '.agents', 'workspace', '.runtime');
35
+ }
36
+ export function resolveAgentRuntimeStoreRoot(options = {}) {
37
+ const client = clientNamespace(options.client ?? 'codex');
38
+ if (options.explicitRoot)
39
+ return path.resolve(options.explicitRoot);
40
+ const env = options.env ?? process.env;
41
+ const runtimeRoot = resolveAgentRuntimeRoot({ repoRoot: options.repoRoot, env });
42
+ if (env.AGENT_INFRA_RUNTIME_DIR) {
43
+ if (!options.store) {
44
+ throw runtimeError('AGENT_INFRA_RUNTIME_STORE_INVALID', 'store kind is required for task-bound runtime');
45
+ }
46
+ return path.join(runtimeRoot, 'clients', client, options.store);
47
+ }
48
+ if (client === 'codex' && options.store) {
49
+ return path.join(runtimeRoot, `codex-${options.store}`);
50
+ }
51
+ if (!options.store) {
52
+ throw runtimeError('AGENT_INFRA_RUNTIME_STORE_INVALID', 'store kind is required');
53
+ }
54
+ return path.join(runtimeRoot, 'clients', client, options.store);
55
+ }
56
+ //# sourceMappingURL=agent-runtime.js.map