@fitlab-ai/agent-infra 0.9.8 → 0.9.10

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 (182) 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/codex-lifecycle/app-server.js +58 -21
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +25 -11
  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 +127 -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 +63 -92
  10. package/dist/lib/agent-clients/adapters/codex-sandbox.js +72 -11
  11. package/dist/lib/agent-clients/adapters/index.js +3 -1
  12. package/dist/lib/agent-clients/adapters/traecli.js +66 -0
  13. package/dist/lib/agent-clients/tokens.js +2 -2
  14. package/dist/lib/agent-clients/types.js +2 -1
  15. package/dist/lib/defaults.json +5 -0
  16. package/dist/lib/git/workflow.js +10 -1
  17. package/dist/lib/internal/codex-lifecycle.js +20 -6
  18. package/dist/lib/internal/codex-sandbox-controller.js +9 -15
  19. package/dist/lib/internal/git-workflow.js +5 -7
  20. package/dist/lib/internal/platform-pr.js +20 -7
  21. package/dist/lib/internal/release-workflow.js +2 -1
  22. package/dist/lib/internal/sandbox-control.js +79 -2
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-orchestration.js +156 -291
  25. package/dist/lib/{task/commands/validate.js → internal/task-validate.js} +75 -42
  26. package/dist/lib/platform/pr-summary.js +48 -10
  27. package/dist/lib/platform/pull-requests.js +205 -41
  28. package/dist/lib/run/tui.js +5 -2
  29. package/dist/lib/sandbox/commands/rm.js +316 -55
  30. package/dist/lib/sandbox/control/client.js +190 -6
  31. package/dist/lib/sandbox/control/controller-registration.js +275 -0
  32. package/dist/lib/sandbox/control/executor.js +140 -5
  33. package/dist/lib/sandbox/control/lifecycle.js +3 -1
  34. package/dist/lib/sandbox/control/protocol.js +67 -6
  35. package/dist/lib/sandbox/index.js +5 -3
  36. package/dist/lib/sandbox/managed-fs.js +5 -0
  37. package/dist/lib/sandbox/task-resolver.js +50 -6
  38. package/dist/lib/sandbox/workspace-identity.js +27 -0
  39. package/dist/lib/sandbox/worktree-safety.js +215 -2
  40. package/dist/lib/task/codex-orchestration.js +161 -39
  41. package/dist/lib/task/commands/status.js +59 -1
  42. package/dist/lib/task/commit-operation.js +335 -0
  43. package/dist/lib/task/commit-policy.js +18 -0
  44. package/dist/lib/task/delegation-receipts.js +248 -6
  45. package/dist/lib/task/finalization.js +593 -0
  46. package/dist/lib/task/index.js +0 -9
  47. package/dist/lib/task/operation-outcome.js +33 -0
  48. package/dist/lib/task/orchestration.js +187 -761
  49. package/dist/lib/task/short-id.js +59 -1
  50. package/dist/lib/task/task-execution-lock.js +6 -1
  51. package/dist/lib/task/verification-engine.js +110 -60
  52. package/dist/lib/task/verification.js +2 -1
  53. package/dist/lib/task/workflow-warning-intents.js +50 -1
  54. package/dist/lifecycle-build-manifest.json +3 -3
  55. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
  56. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +37 -10
  57. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
  58. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +163 -0
  59. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  60. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  61. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +91 -118
  62. package/lib/agent-clients/adapters/codex-sandbox.ts +89 -14
  63. package/lib/agent-clients/adapters/index.ts +3 -1
  64. package/lib/agent-clients/adapters/traecli.ts +67 -0
  65. package/lib/agent-clients/tokens.ts +2 -2
  66. package/lib/agent-clients/types.ts +2 -1
  67. package/lib/defaults.json +5 -0
  68. package/lib/git/workflow.ts +11 -2
  69. package/lib/internal/codex-lifecycle.ts +19 -6
  70. package/lib/internal/codex-sandbox-controller.ts +9 -14
  71. package/lib/internal/git-workflow.ts +5 -6
  72. package/lib/internal/platform-pr.ts +14 -7
  73. package/lib/internal/release-workflow.ts +2 -1
  74. package/lib/internal/sandbox-control.ts +92 -2
  75. package/lib/internal/task-finalization.ts +68 -0
  76. package/lib/internal/task-orchestration.ts +20 -103
  77. package/lib/{task/commands/validate.ts → internal/task-validate.ts} +75 -40
  78. package/lib/platform/pr-summary.ts +55 -11
  79. package/lib/platform/pull-requests.ts +211 -41
  80. package/lib/run/tui.ts +5 -3
  81. package/lib/sandbox/commands/rm.ts +413 -61
  82. package/lib/sandbox/control/client.ts +255 -6
  83. package/lib/sandbox/control/controller-registration.ts +352 -0
  84. package/lib/sandbox/control/executor.ts +169 -5
  85. package/lib/sandbox/control/lifecycle.ts +2 -1
  86. package/lib/sandbox/control/protocol.ts +80 -7
  87. package/lib/sandbox/index.ts +5 -3
  88. package/lib/sandbox/managed-fs.ts +5 -0
  89. package/lib/sandbox/task-resolver.ts +58 -7
  90. package/lib/sandbox/workspace-identity.ts +50 -2
  91. package/lib/sandbox/workspace-view.ts +3 -3
  92. package/lib/sandbox/worktree-safety.ts +236 -2
  93. package/lib/task/codex-orchestration.ts +168 -40
  94. package/lib/task/commands/status.ts +78 -1
  95. package/lib/task/commit-operation.ts +430 -0
  96. package/lib/task/commit-policy.ts +30 -0
  97. package/lib/task/delegation-receipts.ts +263 -4
  98. package/lib/task/finalization.ts +761 -0
  99. package/lib/task/index.ts +0 -8
  100. package/lib/task/operation-outcome.ts +48 -0
  101. package/lib/task/orchestration.ts +218 -982
  102. package/lib/task/short-id.ts +65 -1
  103. package/lib/task/task-execution-lock.ts +10 -1
  104. package/lib/task/verification-engine.ts +115 -63
  105. package/lib/task/verification.ts +3 -2
  106. package/lib/task/workflow-warning-intents.ts +57 -2
  107. package/package.json +2 -2
  108. package/templates/.agents/QUICKSTART.en.md +3 -4
  109. package/templates/.agents/QUICKSTART.zh-CN.md +3 -4
  110. package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
  111. package/templates/.agents/rules/README.en.md +1 -0
  112. package/templates/.agents/rules/README.zh-CN.md +1 -0
  113. package/templates/.agents/rules/compatibility-policy.en.md +39 -0
  114. package/templates/.agents/rules/compatibility-policy.zh-CN.md +39 -0
  115. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -4
  116. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -4
  117. package/templates/.agents/rules/pr-sync.en.md +1 -1
  118. package/templates/.agents/rules/pr-sync.github.en.md +2 -1
  119. package/templates/.agents/rules/pr-sync.github.zh-CN.md +2 -0
  120. package/templates/.agents/rules/pr-sync.zh-CN.md +1 -0
  121. package/templates/.agents/rules/review-method.en.md +1 -0
  122. package/templates/.agents/rules/review-method.zh-CN.md +1 -0
  123. package/templates/.agents/rules/task-management.en.md +2 -2
  124. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  125. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
  126. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
  127. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  128. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  129. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  130. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  131. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  132. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  133. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  134. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  135. package/templates/.agents/skills/code-task/SKILL.en.md +1 -0
  136. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
  137. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  138. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  139. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  140. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  141. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  142. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  143. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  144. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  145. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  146. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  147. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  148. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  149. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  150. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  151. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  152. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  153. package/templates/.agents/skills/create-pr/SKILL.en.md +17 -9
  154. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -9
  155. package/templates/.agents/skills/create-pr/reference/change-report.en.md +41 -0
  156. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +41 -0
  157. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  158. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  159. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -1
  160. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -1
  161. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +8 -6
  162. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +8 -6
  163. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +3 -3
  164. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +3 -3
  165. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +1 -1
  166. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +1 -1
  167. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  168. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  169. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  170. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  171. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  172. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  173. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +24 -0
  174. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  175. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  176. package/templates/.codex/hooks.json +4 -4
  177. package/templates/.trae/skills/update-agent-infra.en.md +8 -0
  178. package/templates/.trae/skills/update-agent-infra.zh-CN.md +8 -0
  179. package/dist/lib/task/commit-finalization.js +0 -305
  180. package/dist/lib/task/commit-intent.js +0 -165
  181. package/lib/task/commit-finalization.ts +0 -419
  182. package/lib/task/commit-intent.ts +0 -230
@@ -19,14 +19,16 @@ import { planPullRequestMetadata } from './pull-request-metadata.ts';
19
19
  import { platformResult } from './types.ts';
20
20
  import type { PlatformOperation, PlatformResult } from './types.ts';
21
21
  import { inspectCompletionArtifacts } from '../task/finalization-artifacts.ts';
22
- import { inspectCreatePrCommitGate } from '../task/commit-finalization.ts';
23
22
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.ts';
23
+ import { mergeOperationWarnings, type OperationWarning } from '../task/operation-outcome.ts';
24
24
 
25
25
  type PullRequestSnapshot = PlatformChangeRequestSnapshot;
26
26
 
27
27
  type PullRequestResult = PlatformResult & {
28
28
  task: { id: string | null; issueNumber: number | null; prNumber: number | null };
29
29
  pullRequest: PullRequestSnapshot | null;
30
+ result: 'pr_created' | 'pr_reused' | 'no_op' | 'pr_created_with_warnings' | 'pr_reused_with_warnings' | 'no_op_with_warnings' | 'failed' | 'blocked' | null;
31
+ warnings: readonly OperationWarning[];
30
32
  };
31
33
  type InspectionOptions = { cwd?: string; client?: unknown };
32
34
  type SharedOptions = { cwd?: string; client?: GitHubClient };
@@ -40,7 +42,8 @@ type CreateOptions = SharedOptions & {
40
42
  dryRun?: boolean;
41
43
  };
42
44
  type BindOptions = SharedOptions & { agent: string; pr: number; dryRun?: boolean };
43
- type SyncOptions = SharedOptions & { agent: string; metadata?: boolean; closingIssue?: boolean; dryRun?: boolean };
45
+ type PullRequestPrimaryResult = 'pr_created' | 'pr_reused' | 'no_op';
46
+ type SyncOptions = SharedOptions & { agent: string; metadata?: boolean; closingIssue?: boolean; dryRun?: boolean; primaryResult: PullRequestPrimaryResult };
44
47
  type ResolveExternalOptions = SharedOptions & { agent: string; pr?: number; dryRun?: boolean };
45
48
  type ExternalPullRequestSelection =
46
49
  | { status: 'normal'; candidates: PullRequestSnapshot[]; eligible: [] }
@@ -102,6 +105,12 @@ type ClosingPullRequestPage = {
102
105
  } } } };
103
106
  };
104
107
 
108
+ function warningResultForPrimary(primaryResult: PullRequestPrimaryResult): NonNullable<PullRequestResult['result']> {
109
+ if (primaryResult === 'pr_created') return 'pr_created_with_warnings';
110
+ if (primaryResult === 'pr_reused') return 'pr_reused_with_warnings';
111
+ return 'no_op_with_warnings';
112
+ }
113
+
105
114
  function result(
106
115
  status: PlatformResult['status'],
107
116
  taskId: string | null,
@@ -109,12 +118,15 @@ function result(
109
118
  prNumber: number | null,
110
119
  overrides: Partial<PullRequestResult> = {}
111
120
  ): PullRequestResult {
112
- return {
121
+ const output = {
113
122
  ...platformResult(status),
114
123
  task: { id: taskId, issueNumber, prNumber },
115
124
  pullRequest: null,
125
+ result: null,
126
+ warnings: [],
116
127
  ...overrides
117
128
  };
129
+ return { ...output, warnings: mergeOperationWarnings(output.warnings) };
118
130
  }
119
131
 
120
132
  function externalResult(
@@ -311,9 +323,9 @@ function expectedHead(repository: string, head: string): { repository: string; r
311
323
  return { repository: `${head.slice(0, colon)}/${repoName}`, ref: head.slice(colon + 1) };
312
324
  }
313
325
 
314
- function selectPullRequest(remotes: RemotePullRequest[], repository: string, head: string, base: string):
326
+ function selectPullRequest(remotes: RemotePullRequest[], repository: string, head: string, base: string, expectedSha?: string):
315
327
  | { status: 'resolved'; pullRequest: PullRequestSnapshot }
316
- | { status: 'missing' | 'ambiguous'; pullRequest: null } {
328
+ | { status: 'missing' | 'ambiguous' | 'head-mismatch'; pullRequest: null } {
317
329
  const wanted = expectedHead(repository, head);
318
330
  const matches = remotes.flatMap((remote) => {
319
331
  const normalized = normalizePullRequest(remote, repository);
@@ -321,10 +333,94 @@ function selectPullRequest(remotes: RemotePullRequest[], repository: string, hea
321
333
  normalized.head.ref === wanted.ref && normalized.base.repository === repository && normalized.base.ref === base
322
334
  ? [normalized] : [];
323
335
  });
336
+ if (expectedSha && matches.some((match) => match.head.sha !== expectedSha)) {
337
+ return { status: 'head-mismatch', pullRequest: null };
338
+ }
324
339
  if (matches.length === 1) return { status: 'resolved', pullRequest: matches[0]! };
325
340
  return { status: matches.length === 0 ? 'missing' : 'ambiguous', pullRequest: null };
326
341
  }
327
342
 
343
+ function repositoryHead(cwd: string): string {
344
+ return execFileSync('git', ['rev-parse', 'HEAD'], {
345
+ cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
346
+ }).trim();
347
+ }
348
+
349
+ function remoteBranchHead(cwd: string, repository: string, head: string):
350
+ | { ok: true; value: string }
351
+ | { ok: false; error: { code: string; message: string; retryable: boolean } } {
352
+ const wanted = expectedHead(repository, head);
353
+ if (!wanted.ref || !/^[A-Za-z0-9._/-]+$/.test(wanted.ref)) return {
354
+ ok: false,
355
+ error: { code: 'PR_HEAD_INVALID', message: 'Pull request head ref is invalid', retryable: false }
356
+ };
357
+ const remotes = configuredGitRemotes(cwd);
358
+ const remote = remotes.find((item) => repositoryKey(item.repository) === repositoryKey(wanted.repository))
359
+ ?? remotes.find((item) => item.name === 'origin');
360
+ if (!remote) return {
361
+ ok: false,
362
+ error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `No configured remote can verify ${wanted.repository}:${wanted.ref}`, retryable: false }
363
+ };
364
+ try {
365
+ const output = execFileSync('git', ['ls-remote', '--refs', remote.name, `refs/heads/${wanted.ref}`], {
366
+ cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
367
+ }).trim();
368
+ const match = output.split(/\r?\n/).map((line) => line.trim()).find((line) => line.endsWith(`refs/heads/${wanted.ref}`));
369
+ const sha = match?.split(/\s+/)[0] || null;
370
+ if (!sha || !/^[a-f0-9]{40}$/i.test(sha)) return {
371
+ ok: false,
372
+ error: { code: 'PR_REMOTE_BRANCH_MISSING', message: `Remote branch ${wanted.repository}:${wanted.ref} does not exist`, retryable: false }
373
+ };
374
+ return { ok: true, value: sha };
375
+ } catch (error) {
376
+ return {
377
+ ok: false,
378
+ error: { code: 'PR_REMOTE_BRANCH_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: true }
379
+ };
380
+ }
381
+ }
382
+
383
+ function verifyCreateHead(base: ReturnType<typeof resolvedContext> & { ok: true }, head: string) {
384
+ let localHead: string;
385
+ try {
386
+ localHead = repositoryHead(base.resolved.repoRoot);
387
+ } catch (error) {
388
+ return { ok: false as const, error: { code: 'PR_LOCAL_HEAD_UNAVAILABLE', message: error instanceof Error ? error.message : String(error), retryable: false } };
389
+ }
390
+ const remote = remoteBranchHead(base.resolved.repoRoot, base.context.platform.repository!, head);
391
+ if (!remote.ok) return remote;
392
+ if (remote.value !== localHead) return {
393
+ ok: false as const,
394
+ error: { code: 'PR_REMOTE_HEAD_MISMATCH', message: `Remote head ${remote.value} does not match local HEAD ${localHead}`, retryable: false }
395
+ };
396
+ return { ok: true as const, value: localHead };
397
+ }
398
+
399
+ function validateBoundPullRequest(
400
+ base: ReturnType<typeof resolvedContext> & { ok: true },
401
+ pullRequest: PullRequestSnapshot | null,
402
+ head: string,
403
+ baseRef: string,
404
+ expectedSha: string
405
+ ): { ok: true } | { ok: false; error: { code: string; message: string; retryable: boolean } } {
406
+ if (!pullRequest) return {
407
+ ok: false,
408
+ error: { code: 'PR_BIND_RECHECK_FAILED', message: 'Bound pull request identity is unavailable', retryable: false }
409
+ };
410
+ const wanted = expectedHead(base.context.platform.repository!, head);
411
+ if (
412
+ pullRequest.head.repository !== wanted.repository
413
+ || pullRequest.head.ref !== wanted.ref
414
+ || pullRequest.head.sha !== expectedSha
415
+ || pullRequest.base.repository !== base.context.platform.repository
416
+ || pullRequest.base.ref !== baseRef
417
+ ) return {
418
+ ok: false,
419
+ error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Bound pull request identity does not match the current expected head/base', retryable: false }
420
+ };
421
+ return { ok: true };
422
+ }
423
+
328
424
  function resolvedContext(taskRef: string, options: InspectionOptions) {
329
425
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
330
426
  if (!resolved.ok) return { ok: false as const, output: result('failed', resolved.taskId, null, null, {
@@ -464,18 +560,21 @@ registerPlatformCapabilities('github', {
464
560
  }
465
561
  });
466
562
 
467
- function locatePullRequest(base: ReturnType<typeof resolvedContext> & { ok: true }, head: string, target: string) {
563
+ function locatePullRequest(base: ReturnType<typeof resolvedContext> & { ok: true }, head: string, target: string, expectedSha?: string) {
468
564
  const repository = base.context.platform.repository!;
469
565
  const listed = base.client.json<RemotePullRequest[]>([
470
566
  'api', `repos/${repository}/pulls?state=open&base=${encodeURIComponent(target)}&per_page=100`
471
567
  ], { cwd: base.resolved.repoRoot });
472
568
  if (!listed.ok) return listed;
473
- const selected = selectPullRequest(listed.value, repository, head, target);
569
+ const selected = selectPullRequest(listed.value, repository, head, target, expectedSha);
474
570
  return selected.status === 'resolved'
475
571
  ? { ok: true as const, value: selected.pullRequest }
476
572
  : { ok: false as const, error: {
477
- code: selected.status === 'ambiguous' ? 'PR_IDENTITY_AMBIGUOUS' : 'PR_NOT_FOUND',
478
- message: selected.status === 'ambiguous' ? 'Multiple pull requests match the exact head/base identity' : 'No pull request matches the exact head/base identity',
573
+ code: selected.status === 'ambiguous' ? 'PR_IDENTITY_AMBIGUOUS'
574
+ : selected.status === 'head-mismatch' ? 'PR_HEAD_SHA_MISMATCH' : 'PR_NOT_FOUND',
575
+ message: selected.status === 'ambiguous' ? 'Multiple pull requests match the exact head/base identity'
576
+ : selected.status === 'head-mismatch' ? 'A pull request matches head/base but not the expected head SHA'
577
+ : 'No pull request matches the exact head/base identity',
479
578
  retryable: false
480
579
  } };
481
580
  }
@@ -755,24 +854,41 @@ function createPlatformPullRequestUnlocked(taskRef: string, options: CreateOptio
755
854
  if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
756
855
  error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
757
856
  });
758
- if (base.prNumber) return inspectPlatformPullRequest(taskRef, options);
759
- const gate = inspectCreatePrCommitGate(base.resolved.taskDir, base.resolved.repoRoot, base.resolved.taskId);
760
- if (!gate.allowed) return result('blocked', base.resolved.taskId, base.issueNumber, null, {
857
+ if (base.prNumber) {
858
+ const headCheck = verifyCreateHead(base, options.head);
859
+ if (!headCheck.ok) return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
860
+ platform: base.context.platform,
861
+ capabilities: base.context.capabilities,
862
+ error: headCheck.error
863
+ });
864
+ const inspected = inspectPlatformPullRequest(taskRef, options);
865
+ const identity = validateBoundPullRequest(base, inspected.pullRequest, options.head, options.base, headCheck.value);
866
+ if (!identity.ok) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
867
+ platform: base.context.platform,
868
+ capabilities: base.context.capabilities,
869
+ pullRequest: inspected.pullRequest,
870
+ error: identity.error
871
+ });
872
+ return { ...inspected, result: 'no_op' };
873
+ }
874
+ const headCheck = verifyCreateHead(base, options.head);
875
+ if (!headCheck.ok) return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
761
876
  platform: base.context.platform,
762
877
  capabilities: base.context.capabilities,
763
- error: {
764
- code: gate.code!,
765
- message: `${gate.message}; action=${gate.action}`,
766
- retryable: gate.action === 'rerun-commit' || gate.action === 'rerun-review-code'
767
- }
878
+ error: headCheck.error
768
879
  });
769
- const located = locatePullRequest(base, options.head, options.base);
880
+ const expectedHeadSha = headCheck.value;
881
+ const located = locatePullRequest(base, options.head, options.base, expectedHeadSha);
770
882
  if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS') return result('failed', base.resolved.taskId, base.issueNumber, null, {
771
883
  platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
772
884
  });
885
+ if (!located.ok && located.error.code === 'PR_HEAD_SHA_MISMATCH') return result('failed', base.resolved.taskId, base.issueNumber, null, {
886
+ platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
887
+ });
773
888
  if (options.dryRun) return result('planned', base.resolved.taskId, base.issueNumber, null, {
774
889
  platform: base.context.platform, capabilities: base.context.capabilities,
775
- operations: [{ name: located.ok ? 'pr:reuse' : 'pr:create', status: 'planned', reasonCode: null }], error: null
890
+ operations: [{ name: located.ok ? 'pr:reuse' : 'pr:create', status: 'planned', reasonCode: null }],
891
+ result: located.ok ? 'pr_reused' : 'pr_created', error: null
776
892
  });
777
893
  const started = writeCreateStarted(base, options.agent);
778
894
  if (started.status === 'failed') return result('failed', base.resolved.taskId, base.issueNumber, null, {
@@ -789,7 +905,7 @@ function createPlatformPullRequestUnlocked(taskRef: string, options: CreateOptio
789
905
  });
790
906
  if (response.ok) pullRequest = normalizePullRequest(response.value, repository);
791
907
  else if (response.error.retryable) {
792
- const recovered = locatePullRequest(base, options.head, options.base);
908
+ const recovered = locatePullRequest(base, options.head, options.base, expectedHeadSha);
793
909
  if (recovered.ok) pullRequest = recovered.value;
794
910
  else return result('blocked', base.resolved.taskId, base.issueNumber, null, {
795
911
  platform: base.context.platform, capabilities: base.context.capabilities,
@@ -803,8 +919,44 @@ function createPlatformPullRequestUnlocked(taskRef: string, options: CreateOptio
803
919
  if (!pullRequest) return result('failed', base.resolved.taskId, base.issueNumber, null, {
804
920
  error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'PR create response lacks validated identity', retryable: false }
805
921
  });
922
+ if (pullRequest.head.sha !== expectedHeadSha) return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
923
+ platform: base.context.platform, capabilities: base.context.capabilities,
924
+ resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
925
+ error: { code: 'PR_HEAD_SHA_MISMATCH', message: 'Created pull request head does not match the expected local HEAD', retryable: false }
926
+ });
806
927
  created = true;
807
928
  }
929
+ const bindHeadCheck = verifyCreateHead(base, options.head);
930
+ if (!bindHeadCheck.ok || bindHeadCheck.value !== expectedHeadSha) {
931
+ const error = bindHeadCheck.ok
932
+ ? { code: 'PR_REMOTE_HEAD_MISMATCH', message: 'Remote head changed before task binding', retryable: false }
933
+ : bindHeadCheck.error;
934
+ return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
935
+ platform: base.context.platform, capabilities: base.context.capabilities,
936
+ resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
937
+ });
938
+ }
939
+ const beforeBind = inspectGitHubPullRequest(base.client, base.context.platform.repository!, pullRequest.number, base.resolved.repoRoot);
940
+ if (!beforeBind.ok || !beforeBind.value) {
941
+ const error = beforeBind.ok
942
+ ? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false }
943
+ : beforeBind.error;
944
+ return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
945
+ platform: base.context.platform, capabilities: base.context.capabilities,
946
+ resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
947
+ });
948
+ }
949
+ if (
950
+ beforeBind.value.head.sha !== expectedHeadSha
951
+ || beforeBind.value.head.ref !== expectedHead(base.context.platform.repository!, options.head).ref
952
+ || beforeBind.value.base.ref !== options.base
953
+ || beforeBind.value.base.repository !== base.context.platform.repository
954
+ ) return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
955
+ platform: base.context.platform, capabilities: base.context.capabilities,
956
+ resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest: beforeBind.value,
957
+ error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
958
+ });
959
+ pullRequest = beforeBind.value;
808
960
  const refreshed = resolvedContext(taskRef, options);
809
961
  if (!refreshed.ok) return result('failed', base.resolved.taskId, base.issueNumber, null, {
810
962
  pullRequest, error: { code: 'PR_CREATED_BIND_FAILED', message: pullRequest.url, retryable: false }
@@ -819,7 +971,8 @@ function createPlatformPullRequestUnlocked(taskRef: string, options: CreateOptio
819
971
  return result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
820
972
  changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
821
973
  resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
822
- operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }], error: null
974
+ operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
975
+ result: created ? 'pr_created' : 'pr_reused', error: null
823
976
  });
824
977
  }
825
978
 
@@ -831,7 +984,6 @@ function createPlatformPullRequest(taskRef: string, options: CreateOptions): Pul
831
984
  error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
832
985
  });
833
986
  }
834
- if (base.prNumber) return inspectPlatformPullRequest(taskRef, options);
835
987
  try {
836
988
  return withTaskExecutionLock(
837
989
  base.resolved.repoRoot,
@@ -860,23 +1012,40 @@ function createPlatformPullRequest(taskRef: string, options: CreateOptions): Pul
860
1012
  }
861
1013
 
862
1014
  function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullRequestResult {
1015
+ const warningResult = warningResultForPrimary(options.primaryResult);
1016
+ const softenFailure = (output: PullRequestResult): PullRequestResult => {
1017
+ const warning = output.error && output.task.prNumber !== null
1018
+ && !['PR_NOT_LINKED', 'PR_BIND_CONFLICT'].includes(output.error.code)
1019
+ ? {
1020
+ code: output.error.code,
1021
+ message: output.error.message,
1022
+ retryable: output.error.retryable,
1023
+ step: 'pr-metadata',
1024
+ target: `pull-request:${output.task.prNumber}`,
1025
+ severity: 'ACTION_REQUIRED' as const
1026
+ }
1027
+ : null;
1028
+ return warning
1029
+ ? { ...output, status: 'applied', changed: false, error: null, result: warningResult, warnings: mergeOperationWarnings([warning]) }
1030
+ : output;
1031
+ };
863
1032
  const base = resolvedContext(taskRef, options);
864
- if (!base.ok) return base.output;
865
- if (!base.prNumber) return result('failed', base.resolved.taskId, base.issueNumber, null, {
1033
+ if (!base.ok) return softenFailure(base.output);
1034
+ if (!base.prNumber) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
866
1035
  error: { code: 'PR_NOT_LINKED', message: 'Task has no valid pr_number', retryable: false }
867
- });
1036
+ }));
868
1037
  const fetched = inspectGitHubPullRequest(base.client, base.context.platform.repository!, base.prNumber, base.resolved.repoRoot);
869
- if (!fetched.ok) return result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error });
870
- if (!options.metadata && !options.closingIssue) return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1038
+ if (!fetched.ok) return softenFailure(result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error }));
1039
+ if (!options.metadata && !options.closingIssue) return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
871
1040
  error: { code: 'PR_PAYLOAD_INVALID', message: 'sync requires a desired-state option', retryable: false }
872
- });
873
- if (!base.issueNumber) return result('degraded', base.resolved.taskId, null, base.prNumber, {
1041
+ }));
1042
+ if (!base.issueNumber) return softenFailure(result('degraded', base.resolved.taskId, null, base.prNumber, {
874
1043
  pullRequest: fetched.value, error: { code: 'ISSUE_NOT_LINKED', message: 'Task has no linked Issue to copy metadata from', retryable: false }
875
- });
1044
+ }));
876
1045
  const issue = inspectPlatformIssue(taskRef, { cwd: base.resolved.repoRoot, client: base.client });
877
- if (!issue.issue) return result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
1046
+ if (!issue.issue) return softenFailure(result(issue.status, base.resolved.taskId, base.issueNumber, base.prNumber, {
878
1047
  platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: fetched.value, error: issue.error
879
- });
1048
+ }));
880
1049
  const planned = planPullRequestMetadata({
881
1050
  pullRequest: fetched.value,
882
1051
  issue: issue.issue,
@@ -886,9 +1055,9 @@ function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullReq
886
1055
  }).operations.filter((operation) => options.metadata || operation.name === 'closing-issue')
887
1056
  .filter((operation) => options.closingIssue || operation.name !== 'closing-issue');
888
1057
  const operations: PlatformOperation[] = planned.map(({ name, status, reasonCode }) => ({ name, status, reasonCode }));
889
- if (options.dryRun) return result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
1058
+ if (options.dryRun) return softenFailure(result(planned.some((operation) => operation.status === 'planned') ? 'planned' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
890
1059
  platform: base.context.platform, capabilities: base.context.capabilities, pullRequest: fetched.value, operations, error: null
891
- });
1060
+ }));
892
1061
  const payload: Record<string, unknown> = {};
893
1062
  for (const operation of planned) {
894
1063
  if (operation.status !== 'planned') continue;
@@ -897,26 +1066,26 @@ function syncPlatformPullRequest(taskRef: string, options: SyncOptions): PullReq
897
1066
  if (operation.name === 'closing-issue') payload.body = operation.value;
898
1067
  if (operation.name === 'milestone') {
899
1068
  const milestones = base.client.json<unknown[]>(['api', '--paginate', '--slurp', `repos/${base.context.platform.repository}/milestones?state=open&per_page=100`], { cwd: base.resolved.repoRoot });
900
- if (!milestones.ok) return result(milestones.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: milestones.error });
1069
+ if (!milestones.ok) return softenFailure(result(milestones.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: milestones.error }));
901
1070
  const flat = milestones.value.flatMap((entry) => Array.isArray(entry) ? entry : [entry]) as Array<{ title?: string; number?: number }>;
902
1071
  payload.milestone = flat.find((entry) => entry.title === operation.value)?.number ?? null;
903
1072
  }
904
1073
  }
905
1074
  if (Object.keys(payload).length === 0) {
906
1075
  const degraded = planned.some((operation) => operation.status === 'skipped');
907
- return result(degraded ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
1076
+ return softenFailure(result(degraded ? 'degraded' : 'no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
908
1077
  platform: base.context.platform, capabilities: base.context.capabilities, resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value, operations, error: null
909
- });
1078
+ }));
910
1079
  }
911
1080
  const patched = base.client.json<RemotePullRequest>(['api', `repos/${base.context.platform.repository}/issues/${base.prNumber}`, '-X', 'PATCH', '--input', '-'], {
912
1081
  cwd: base.resolved.repoRoot, method: 'PATCH', input: JSON.stringify(payload)
913
1082
  });
914
- if (!patched.ok) return result(patched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: patched.error });
915
- return result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
1083
+ if (!patched.ok) return softenFailure(result(patched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { pullRequest: fetched.value, operations, error: patched.error }));
1084
+ return softenFailure(result(planned.some((operation) => operation.status === 'skipped') ? 'degraded' : 'applied', base.resolved.taskId, base.issueNumber, base.prNumber, {
916
1085
  changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
917
1086
  resource: { kind: 'pull-request', number: base.prNumber }, pullRequest: fetched.value,
918
1087
  operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
919
- });
1088
+ }));
920
1089
  }
921
1090
 
922
1091
  export {
@@ -933,4 +1102,5 @@ export {
933
1102
  selectPullRequest,
934
1103
  syncPlatformPullRequest
935
1104
  };
936
- export type { BindOptions, CreateOptions, ExternalPullRequestResult, ExternalPullRequestSelection, PullRequestResult, PullRequestSnapshot, ResolveExternalOptions, SyncOptions };
1105
+ export type { BindOptions, CreateOptions, ExternalPullRequestResult, ExternalPullRequestSelection, PullRequestPrimaryResult, PullRequestResult, PullRequestSnapshot, ResolveExternalOptions, SyncOptions };
1106
+ export { warningResultForPrimary };
package/lib/run/tui.ts CHANGED
@@ -2,14 +2,15 @@ import { renderAgentClientInvocation } from '../agent-clients/invocation.ts';
2
2
  import { getAgentClientAdapter } from '../agent-clients/registry.ts';
3
3
  import type { AgentClientId } from '../agent-clients/types.ts';
4
4
 
5
- export type TuiName = 'claude' | 'codex' | 'antigravity' | 'opencode';
5
+ export type TuiName = 'claude' | 'codex' | 'antigravity' | 'opencode' | 'traecli';
6
6
 
7
- const TUI_NAMES = new Set(['claude', 'codex', 'antigravity', 'opencode']);
7
+ const TUI_NAMES = new Set(['claude', 'codex', 'antigravity', 'opencode', 'traecli']);
8
8
  const TUI_AGENT_CLIENT_IDS: Readonly<Record<TuiName, AgentClientId>> = Object.freeze({
9
9
  claude: 'claude-code',
10
10
  codex: 'codex',
11
11
  antigravity: 'antigravity-cli',
12
- opencode: 'opencode'
12
+ opencode: 'opencode',
13
+ traecli: 'traecli'
13
14
  });
14
15
 
15
16
  export type CommandConfig = {
@@ -46,5 +47,6 @@ export function buildTuiCommand(tui: TuiName, prompt: string): [string, string[]
46
47
  if (tui === 'claude') return ['claude', ['--dangerously-skip-permissions', '--print', prompt]];
47
48
  if (tui === 'antigravity') return ['agy', ['--dangerously-skip-permissions', '--print', prompt]];
48
49
  if (tui === 'opencode') return ['opencode', ['run', '--dangerously-skip-permissions', prompt]];
50
+ if (tui === 'traecli') return ['traecli', ['exec', '--dangerously-bypass-approvals-and-sandbox', prompt]];
49
51
  return ['codex', ['exec', '--dangerously-bypass-approvals-and-sandbox', prompt]];
50
52
  }