@fitlab-ai/agent-infra 0.9.11 → 0.9.12

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 (193) hide show
  1. package/README.md +6 -6
  2. package/README.zh-CN.md +6 -6
  3. package/dist/bin/internal-cli.js +5 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +71 -10
  5. package/dist/lib/defaults.json +4 -0
  6. package/dist/lib/git/workflow.js +43 -1
  7. package/dist/lib/init.js +1 -0
  8. package/dist/lib/internal/git-workflow.js +8 -1
  9. package/dist/lib/internal/platform-pr.js +8 -2
  10. package/dist/lib/internal/task-delivery.js +59 -0
  11. package/dist/lib/platform/github-client.js +7 -1
  12. package/dist/lib/platform/pr-checks.js +8 -5
  13. package/dist/lib/platform/pr-summary.js +12 -8
  14. package/dist/lib/platform/pull-requests.js +393 -91
  15. package/dist/lib/platform/verification-required.js +13 -2
  16. package/dist/lib/platform/verification-sync.js +9 -4
  17. package/dist/lib/pr-review/evidence-grading.js +12 -4
  18. package/dist/lib/sandbox/config.js +5 -0
  19. package/dist/lib/sandbox/control/lifecycle.js +29 -7
  20. package/dist/lib/server/process-state.js +2 -2
  21. package/dist/lib/task/artifact-lifecycle.js +2 -2
  22. package/dist/lib/task/commit-intent.js +100 -0
  23. package/dist/lib/task/commit-operation.js +195 -101
  24. package/dist/lib/task/create.js +33 -7
  25. package/dist/lib/task/decision-details.js +3 -48
  26. package/dist/lib/task/delivery-target.js +97 -0
  27. package/dist/lib/task/delivery.js +153 -0
  28. package/dist/lib/task/human-override.js +5 -2
  29. package/dist/lib/task/lifecycle.js +106 -9
  30. package/dist/lib/task/orchestration.js +55 -62
  31. package/dist/lib/task/pr-delivery-fact.js +182 -0
  32. package/dist/lib/task/review-finalization.js +4 -8
  33. package/dist/lib/task/review-fingerprint.js +8 -0
  34. package/dist/lib/task/verification-engine.js +48 -24
  35. package/dist/lib/task/verification-review.js +1 -1
  36. package/dist/lib/update.js +9 -0
  37. package/dist/lifecycle-build-manifest.json +3 -3
  38. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +105 -10
  39. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +12 -1
  40. package/lib/defaults.json +4 -0
  41. package/lib/git/workflow.ts +38 -1
  42. package/lib/init.ts +3 -0
  43. package/lib/internal/git-workflow.ts +7 -1
  44. package/lib/internal/platform-pr.ts +8 -1
  45. package/lib/internal/task-delivery.ts +53 -0
  46. package/lib/platform/github-client.ts +8 -1
  47. package/lib/platform/pr-checks.ts +7 -5
  48. package/lib/platform/pr-summary.ts +10 -7
  49. package/lib/platform/pull-requests.ts +425 -87
  50. package/lib/platform/verification-required.ts +12 -2
  51. package/lib/platform/verification-sync.ts +9 -4
  52. package/lib/pr-review/evidence-grading.ts +7 -4
  53. package/lib/sandbox/config.ts +7 -0
  54. package/lib/sandbox/control/lifecycle.ts +30 -8
  55. package/lib/server/process-state.ts +2 -2
  56. package/lib/task/artifact-lifecycle.ts +2 -2
  57. package/lib/task/commit-intent.ts +137 -0
  58. package/lib/task/commit-operation.ts +174 -87
  59. package/lib/task/create.ts +34 -9
  60. package/lib/task/decision-details.ts +2 -62
  61. package/lib/task/delivery-target.ts +130 -0
  62. package/lib/task/delivery.ts +160 -0
  63. package/lib/task/human-override.ts +5 -2
  64. package/lib/task/lifecycle.ts +93 -4
  65. package/lib/task/orchestration.ts +62 -65
  66. package/lib/task/pr-delivery-fact.ts +254 -0
  67. package/lib/task/review-finalization.ts +4 -14
  68. package/lib/task/review-fingerprint.ts +10 -0
  69. package/lib/task/verification-engine.ts +45 -28
  70. package/lib/task/verification-review.ts +2 -0
  71. package/lib/update.ts +10 -0
  72. package/package.json +1 -1
  73. package/templates/.agents/rules/README.en.md +4 -2
  74. package/templates/.agents/rules/README.zh-CN.md +4 -2
  75. package/templates/.agents/rules/create-issue.en.md +11 -2
  76. package/templates/.agents/rules/create-issue.zh-CN.md +24 -2
  77. package/templates/.agents/rules/issue-pr-commands.en.md +4 -17
  78. package/templates/.agents/rules/issue-sync.en.md +26 -5
  79. package/templates/.agents/rules/issue-sync.zh-CN.md +62 -5
  80. package/templates/.agents/rules/label-milestone-setup.en.md +15 -2
  81. package/templates/.agents/rules/label-milestone-setup.zh-CN.md +15 -2
  82. package/templates/.agents/rules/local-artifact-repair.en.md +46 -0
  83. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +46 -0
  84. package/templates/.agents/rules/milestone-inference.en.md +12 -2
  85. package/templates/.agents/rules/milestone-inference.zh-CN.md +25 -3
  86. package/templates/.agents/rules/pr-sync.en.md +22 -13
  87. package/templates/.agents/rules/pr-sync.zh-CN.md +22 -1
  88. package/templates/.agents/rules/release-commands.en.md +42 -3
  89. package/templates/.agents/rules/release-commands.zh-CN.md +42 -3
  90. package/templates/.agents/rules/security-alerts.en.md +17 -2
  91. package/templates/.agents/rules/security-alerts.zh-CN.md +17 -2
  92. package/templates/.agents/rules/sync-content-generation.en.md +8 -0
  93. package/templates/.agents/rules/sync-content-generation.zh-CN.md +8 -0
  94. package/templates/.agents/rules/task-management.en.md +1 -1
  95. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  96. package/templates/.agents/scripts/security-alerts.github.sh +151 -0
  97. package/templates/.agents/scripts/security-alerts.sh +31 -0
  98. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
  99. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
  100. package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
  101. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
  102. package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
  103. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
  104. package/templates/.agents/skills/code-task/SKILL.en.md +5 -2
  105. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +6 -3
  106. package/templates/.agents/skills/commit/SKILL.en.md +3 -3
  107. package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -3
  108. package/templates/.agents/skills/commit/config/verify.json +1 -1
  109. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +3 -3
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +3 -3
  111. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  112. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  113. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -2
  114. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -2
  115. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -1
  116. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -1
  117. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  118. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  119. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
  120. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
  121. package/templates/.agents/skills/complete-task/SKILL.en.md +10 -10
  122. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -10
  123. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +2 -2
  124. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +2 -2
  125. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  126. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  127. package/templates/.agents/skills/create-task/SKILL.en.md +1 -0
  128. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -0
  129. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  130. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  131. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  132. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  133. package/templates/.agents/skills/init-labels/SKILL.en.md +9 -10
  134. package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +9 -10
  135. package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +57 -1
  136. package/templates/.agents/skills/init-labels/scripts/init-labels.sh +27 -1
  137. package/templates/.agents/skills/init-milestones/SKILL.en.md +3 -5
  138. package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +3 -5
  139. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -0
  140. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -0
  141. package/templates/.agents/skills/review-analysis/SKILL.en.md +4 -3
  142. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +4 -3
  143. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +18 -0
  144. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +18 -0
  145. package/templates/.agents/skills/review-code/SKILL.en.md +7 -6
  146. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +7 -6
  147. package/templates/.agents/skills/review-code/reference/output-templates.en.md +19 -1
  148. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +19 -1
  149. package/templates/.agents/skills/review-code/reference/report-template.en.md +4 -2
  150. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +4 -2
  151. package/templates/.agents/skills/review-plan/SKILL.en.md +4 -3
  152. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +4 -3
  153. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +18 -0
  154. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +18 -0
  155. package/templates/.agents/skills/review-pr/SKILL.en.md +1 -0
  156. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -0
  157. package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +2 -2
  158. package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +2 -2
  159. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -0
  160. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -0
  161. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  162. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  163. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +4 -0
  164. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  165. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  166. package/templates/.agents/templates/task.en.md +5 -1
  167. package/templates/.agents/templates/task.zh-CN.md +5 -1
  168. package/templates/.agents/workflows/bug-fix.en.yaml +9 -8
  169. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +9 -8
  170. package/templates/.agents/workflows/feature-development.en.yaml +9 -8
  171. package/templates/.agents/workflows/feature-development.zh-CN.yaml +9 -8
  172. package/templates/.agents/workflows/refactoring.en.yaml +9 -8
  173. package/templates/.agents/workflows/refactoring.zh-CN.yaml +9 -8
  174. package/templates/.agents/rules/create-issue.github.en.md +0 -14
  175. package/templates/.agents/rules/create-issue.github.zh-CN.md +0 -27
  176. package/templates/.agents/rules/issue-pr-commands.github.en.md +0 -34
  177. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +0 -49
  178. package/templates/.agents/rules/issue-sync.github.en.md +0 -40
  179. package/templates/.agents/rules/issue-sync.github.zh-CN.md +0 -76
  180. package/templates/.agents/rules/label-milestone-setup.github.en.md +0 -60
  181. package/templates/.agents/rules/label-milestone-setup.github.zh-CN.md +0 -60
  182. package/templates/.agents/rules/milestone-inference.github.en.md +0 -15
  183. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +0 -27
  184. package/templates/.agents/rules/pr-checks-commands.github.en.md +0 -18
  185. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +0 -34
  186. package/templates/.agents/rules/pr-sync.github.en.md +0 -43
  187. package/templates/.agents/rules/pr-sync.github.zh-CN.md +0 -69
  188. package/templates/.agents/rules/release-commands.github.en.md +0 -44
  189. package/templates/.agents/rules/release-commands.github.zh-CN.md +0 -44
  190. package/templates/.agents/rules/security-alerts.github.en.md +0 -43
  191. package/templates/.agents/rules/security-alerts.github.zh-CN.md +0 -43
  192. /package/templates/.agents/rules/{issue-fields.github.en.md → issue-fields.en.md} +0 -0
  193. /package/templates/.agents/rules/{issue-fields.github.zh-CN.md → issue-fields.zh-CN.md} +0 -0
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
+ import path from 'node:path';
2
3
  import { execFileSync } from 'node:child_process';
3
- import { parseTaskFrontmatter } from '../task/frontmatter.js';
4
+ import { parseTypedTaskFrontmatter } from '../task/frontmatter.js';
4
5
  import { resolveTaskRef } from '../task/resolve-ref.js';
5
6
  import { extractSection } from '../task/sections.js';
6
7
  import { captureTaskWriteMetadata, writeTask } from '../task/write.js';
@@ -12,7 +13,9 @@ import { planPullRequestMetadata } from './pull-request-metadata.js';
12
13
  import { platformResult } from './types.js';
13
14
  import { inspectCompletionArtifacts } from '../task/finalization-artifacts.js';
14
15
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
16
+ import { resolveDeliveryTarget } from '../task/delivery-target.js';
15
17
  import { mergeOperationWarnings } from '../task/operation-outcome.js';
18
+ import { buildBoundFact, buildSkippedFact, factFrontmatterMutation, readPrDeliveryFact } from '../task/pr-delivery-fact.js';
16
19
  function warningResultForPrimary(primaryResult) {
17
20
  if (primaryResult === 'pr_created')
18
21
  return 'pr_created_with_warnings';
@@ -26,6 +29,7 @@ function result(status, taskId, issueNumber, prNumber, overrides = {}) {
26
29
  task: { id: taskId, issueNumber, prNumber },
27
30
  pullRequest: null,
28
31
  result: null,
32
+ creation: null,
29
33
  warnings: [],
30
34
  ...overrides
31
35
  };
@@ -42,6 +46,12 @@ function externalResult(base, overrides = {}) {
42
46
  ...overrides
43
47
  };
44
48
  }
49
+ function withCreation(output, creation) {
50
+ return { ...output, creation };
51
+ }
52
+ const PRECONDITION_NOT_CREATED = {
53
+ kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false
54
+ };
45
55
  function normalizePullRequest(remote, repository) {
46
56
  const number = Number(remote.number);
47
57
  const headRepository = remote.head?.repo?.full_name;
@@ -111,7 +121,7 @@ function hasCompleteExternalIdentity(pullRequest) {
111
121
  ];
112
122
  return Number.isInteger(pullRequest.number) && pullRequest.number > 0 &&
113
123
  required.every((value) => Boolean(value?.trim())) &&
114
- Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha);
124
+ (pullRequest.state === 'open' || Boolean(pullRequest.mergedAt) === Boolean(pullRequest.mergeCommitSha));
115
125
  }
116
126
  function selectExternalPullRequest(candidates, repository, existingPrNumber, explicitPrNumber) {
117
127
  const invalid = candidates.find((candidate) => !hasCompleteExternalIdentity(candidate));
@@ -135,6 +145,7 @@ function selectExternalPullRequest(candidates, repository, existingPrNumber, exp
135
145
  const wantedRepository = repositoryKey(repository);
136
146
  const eligible = unique.filter((candidate) => repositoryKey(candidate.repository) === wantedRepository &&
137
147
  repositoryKey(candidate.base.repository) === wantedRepository &&
148
+ candidate.state === 'closed' &&
138
149
  Boolean(candidate.mergedAt && candidate.mergeCommitSha));
139
150
  if (explicitPrNumber !== null) {
140
151
  const selected = unique.find((candidate) => candidate.number === explicitPrNumber);
@@ -303,6 +314,68 @@ function validateBoundPullRequest(base, pullRequest, head, baseRef, expectedSha)
303
314
  };
304
315
  return { ok: true };
305
316
  }
317
+ function resolveTaskDeliveryTarget(repoRoot, frontmatter) {
318
+ const existing = {
319
+ ...(typeof frontmatter.delivery_remote === 'string' && frontmatter.delivery_remote
320
+ ? { remote: frontmatter.delivery_remote } : {}),
321
+ ...(typeof frontmatter.delivery_base_ref === 'string' && frontmatter.delivery_base_ref
322
+ ? { baseRef: frontmatter.delivery_base_ref } : {})
323
+ };
324
+ const target = resolveDeliveryTarget(repoRoot, existing);
325
+ return target.ok
326
+ ? { ok: true, value: target.value }
327
+ : { ok: false, error: { code: target.code, message: target.message, retryable: false } };
328
+ }
329
+ function taskDeliveryTarget(base) {
330
+ return resolveTaskDeliveryTarget(base.resolved.repoRoot, base.frontmatter);
331
+ }
332
+ function validateIdentityAgainstTarget(repository, target, pullRequest, options = {}) {
333
+ if (!pullRequest || !hasCompleteExternalIdentity(pullRequest))
334
+ return {
335
+ ok: false,
336
+ error: { code: options.errorCode || 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity is incomplete', retryable: false }
337
+ };
338
+ const wantedHead = options.expectedHead ? expectedHead(repository, options.expectedHead) : null;
339
+ const matches = repositoryKey(pullRequest.repository) === repositoryKey(repository)
340
+ && repositoryKey(pullRequest.base.repository) === repositoryKey(repository)
341
+ && pullRequest.base.ref === target.baseRef
342
+ && (!wantedHead || (repositoryKey(pullRequest.head.repository) === repositoryKey(wantedHead.repository) && pullRequest.head.ref === wantedHead.ref))
343
+ && (!options.expectedHeadSha || pullRequest.head.sha === options.expectedHeadSha);
344
+ if (!matches)
345
+ return {
346
+ ok: false,
347
+ error: {
348
+ code: options.errorCode || 'PR_BIND_IDENTITY_MISMATCH',
349
+ message: 'Pull request identity does not match the task branch and delivery target',
350
+ retryable: false
351
+ }
352
+ };
353
+ return { ok: true };
354
+ }
355
+ function validateWriterIdentity(base, pullRequest, options = {}) {
356
+ const target = taskDeliveryTarget(base);
357
+ if (!target.ok)
358
+ return target;
359
+ return validateIdentityAgainstTarget(base.context.platform.repository, target.value, pullRequest, options);
360
+ }
361
+ function sameDeliveryIdentity(left, right) {
362
+ return JSON.stringify(deliveryIdentity(left)) === JSON.stringify(deliveryIdentity(right));
363
+ }
364
+ function validateExternalMergedEvidence(initial, rechecked) {
365
+ if (initial.state !== 'closed'
366
+ || !initial.mergedAt
367
+ || !initial.mergeCommitSha
368
+ || rechecked.state !== 'closed'
369
+ || !rechecked.mergedAt
370
+ || !rechecked.mergeCommitSha
371
+ || initial.mergedAt !== rechecked.mergedAt
372
+ || initial.mergeCommitSha !== rechecked.mergeCommitSha)
373
+ return {
374
+ ok: false,
375
+ error: { code: 'PR_EXTERNAL_IDENTITY_MISMATCH', message: 'Selected pull request merged evidence changed before task binding', retryable: false }
376
+ };
377
+ return { ok: true };
378
+ }
306
379
  function resolvedContext(taskRef, options) {
307
380
  const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
308
381
  if (!resolved.ok)
@@ -310,9 +383,15 @@ function resolvedContext(taskRef, options) {
310
383
  error: { code: resolved.code, message: resolved.message, retryable: false }
311
384
  }) };
312
385
  const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
313
- const frontmatter = parseTaskFrontmatter(content);
386
+ const frontmatter = parseTypedTaskFrontmatter(content);
387
+ const factRead = readPrDeliveryFact(frontmatter);
388
+ if (factRead.status === 'invalid')
389
+ return { ok: false, output: result('failed', resolved.taskId, null, null, {
390
+ error: { code: 'PR_DELIVERY_FACT_INVALID', message: factRead.error.message, retryable: false }
391
+ }) };
392
+ const fact = factRead.status === 'valid' ? factRead.fact : null;
314
393
  const issue = Number(frontmatter.issue_number);
315
- const pr = Number(frontmatter.pr_number);
394
+ const pr = fact?.state === 'bound' ? fact.identity.number : NaN;
316
395
  const issueNumber = Number.isInteger(issue) && issue > 0 ? issue : null;
317
396
  const prNumber = Number.isInteger(pr) && pr > 0 ? pr : null;
318
397
  const client = options.client || createGitHubClient();
@@ -322,7 +401,7 @@ function resolvedContext(taskRef, options) {
322
401
  return { ok: false, output: result(context.status, resolved.taskId, issueNumber, prNumber, {
323
402
  platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
324
403
  }) };
325
- return { ok: true, resolved, content, frontmatter, issueNumber, prNumber, client, context };
404
+ return { ok: true, resolved, content, frontmatter, fact, issueNumber, prNumber, client, context };
326
405
  }
327
406
  function inspectGitHubPullRequest(client, repository, number, cwd) {
328
407
  const fetched = client.json(['api', `repos/${repository}/pulls/${number}`], { cwd });
@@ -450,10 +529,14 @@ function inspectPlatformPullRequest(taskRef, options = {}) {
450
529
  const base = resolvedContext(taskRef, options);
451
530
  if (!base.ok)
452
531
  return base.output;
532
+ if (!base.fact)
533
+ return result('no-op', base.resolved.taskId, base.issueNumber, null, {
534
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
535
+ });
453
536
  if (!base.prNumber)
454
537
  return result('no-op', base.resolved.taskId, base.issueNumber, null, {
455
538
  platform: base.context.platform, capabilities: base.context.capabilities,
456
- error: { code: 'PR_NOT_LINKED', message: 'Task has no valid pr_number', retryable: false }
539
+ error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
457
540
  });
458
541
  const fetched = inspectPlatformChangeRequest(base.context.platform.type, {
459
542
  cwd: base.resolved.repoRoot,
@@ -518,6 +601,30 @@ function appendActivity(content, line) {
518
601
  const body = extractSection(content, ['活动日志', 'Activity Log']);
519
602
  return `${body.replace(/\s+$/, '')}${body.trim() ? '\n' : ''}${line}`;
520
603
  }
604
+ function deliveryIdentity(pullRequest) {
605
+ return {
606
+ repository: pullRequest.repository,
607
+ number: pullRequest.number,
608
+ nodeId: pullRequest.nodeId,
609
+ url: pullRequest.url,
610
+ head: { ...pullRequest.head },
611
+ base: { ...pullRequest.base }
612
+ };
613
+ }
614
+ function factTimestamp(timestamp) {
615
+ return new Date(timestamp.replace(' ', 'T')).toISOString();
616
+ }
617
+ function boundFactFor(pullRequest, source, issueNumber, verifiedAt) {
618
+ return buildBoundFact({
619
+ identity: deliveryIdentity(pullRequest),
620
+ source,
621
+ issueNumber,
622
+ verifiedAt: factTimestamp(verifiedAt),
623
+ remoteState: pullRequest.state,
624
+ mergedAt: pullRequest.state === 'closed' ? pullRequest.mergedAt : null,
625
+ mergeCommitSha: pullRequest.state === 'closed' ? pullRequest.mergeCommitSha : null
626
+ });
627
+ }
521
628
  function writeCreateStarted(base, agent) {
522
629
  const starts = (base.content.match(/\*\*Create PR \[started\]\*\*/g) || []).length;
523
630
  const dones = (base.content.match(/\*\*Create PR\*\*/g) || []).length;
@@ -530,21 +637,35 @@ function writeCreateStarted(base, agent) {
530
637
  mutations: [{ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(base.content, `- ${metadata.timestamp} — **Create PR [started]** by ${agent} — started`) }]
531
638
  }, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
532
639
  }
533
- function bindIdentity(base, pullRequest, agent, dryRun = false) {
640
+ function bindIdentity(base, pullRequest, agent, source, dryRun = false, expectedHead, expectedHeadSha) {
641
+ const identity = validateWriterIdentity(base, pullRequest, {
642
+ expectedHead: source === 'explicit-bind' ? String(base.frontmatter.branch || '') : expectedHead,
643
+ expectedHeadSha,
644
+ errorCode: 'PR_BIND_IDENTITY_MISMATCH'
645
+ });
646
+ if (!identity.ok)
647
+ return { status: 'failed', error: identity.error };
534
648
  if (base.prNumber && base.prNumber !== pullRequest.number)
535
649
  return { status: 'failed', error: {
536
650
  code: 'PR_BIND_CONFLICT', message: `Task is already bound to PR #${base.prNumber}`
537
651
  } };
538
- if (base.prNumber === pullRequest.number)
652
+ if (base.prNumber === pullRequest.number && base.fact?.state === 'bound') {
653
+ if (JSON.stringify(base.fact.identity) !== JSON.stringify(deliveryIdentity(pullRequest)))
654
+ return { status: 'failed', error: {
655
+ code: 'PR_BIND_IDENTITY_MISMATCH', message: `Task binding for PR #${pullRequest.number} does not match the fetched pull request identity`
656
+ } };
539
657
  return { status: 'no-op', error: null };
658
+ }
540
659
  const current = fs.readFileSync(base.resolved.taskMdPath, 'utf8');
541
660
  const metadata = captureTaskWriteMetadata();
661
+ const fact = boundFactFor(pullRequest, source, base.issueNumber, metadata.timestamp);
662
+ const factMutation = factFrontmatterMutation(fact);
542
663
  return writeTask({
543
664
  taskRef: base.resolved.taskId,
544
665
  expectedState: 'active',
545
666
  dryRun,
546
667
  mutations: [
547
- { kind: 'frontmatter', set: { pr_number: pullRequest.number, pr_status: 'created', assigned_to: agent } },
668
+ { kind: 'frontmatter', set: { ...factMutation.set, assigned_to: agent } },
548
669
  { kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(current, `- ${metadata.timestamp} — **Create PR** by ${agent} — PR #${pullRequest.number} created → ${pullRequest.url}`) }
549
670
  ]
550
671
  }, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
@@ -553,6 +674,10 @@ function bindPlatformPullRequest(taskRef, options) {
553
674
  const base = resolvedContext(taskRef, options);
554
675
  if (!base.ok)
555
676
  return base.output;
677
+ if (!base.fact)
678
+ return result('failed', base.resolved.taskId, base.issueNumber, null, {
679
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
680
+ });
556
681
  if (!Number.isInteger(options.pr) || options.pr <= 0)
557
682
  return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
558
683
  error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
@@ -564,14 +689,33 @@ function bindPlatformPullRequest(taskRef, options) {
564
689
  const fetched = inspectGitHubPullRequest(base.client, base.context.platform.repository, options.pr, base.resolved.repoRoot);
565
690
  if (!fetched.ok)
566
691
  return result(fetched.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, { error: fetched.error });
567
- const written = bindIdentity(base, fetched.value, options.agent, options.dryRun);
692
+ const initialIdentity = validateWriterIdentity(base, fetched.value, {
693
+ expectedHead: String(base.frontmatter.branch || ''),
694
+ errorCode: 'PR_BIND_IDENTITY_MISMATCH'
695
+ });
696
+ if (!initialIdentity.ok)
697
+ return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
698
+ pullRequest: fetched.value, error: initialIdentity.error
699
+ });
700
+ const rechecked = inspectGitHubPullRequest(base.client, base.context.platform.repository, options.pr, base.resolved.repoRoot);
701
+ if (!rechecked.ok || !rechecked.value)
702
+ return result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
703
+ pullRequest: fetched.value,
704
+ error: rechecked.ok ? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false } : rechecked.error
705
+ });
706
+ if (!sameDeliveryIdentity(fetched.value, rechecked.value))
707
+ return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
708
+ pullRequest: rechecked.value,
709
+ error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
710
+ });
711
+ const written = bindIdentity(base, rechecked.value, options.agent, 'explicit-bind', options.dryRun);
568
712
  if (written.status === 'failed')
569
713
  return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
570
714
  pullRequest: fetched.value, error: { code: written.error.code, message: written.error.message, retryable: false }
571
715
  });
572
- return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : fetched.value.number, {
716
+ return result(options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : rechecked.value.number, {
573
717
  changed: !options.dryRun && written.status === 'applied', platform: base.context.platform, capabilities: base.context.capabilities,
574
- resource: { kind: 'pull-request', number: fetched.value.number }, pullRequest: fetched.value,
718
+ resource: { kind: 'pull-request', number: rechecked.value.number }, pullRequest: rechecked.value,
575
719
  operations: [{ name: 'task:bind-pr', status: options.dryRun ? 'planned' : written.status === 'no-op' ? 'no-op' : 'applied', reasonCode: null }], error: null
576
720
  });
577
721
  }
@@ -596,6 +740,10 @@ function resolveExternalPullRequest(taskRef, options) {
596
740
  const base = resolvedContext(taskRef, options);
597
741
  if (!base.ok)
598
742
  return externalResult(base.output);
743
+ if (!base.fact)
744
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, null, {
745
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
746
+ }));
599
747
  const inventory = inspectCompletionArtifacts(base.resolved.taskId, { repoRoot: base.resolved.repoRoot });
600
748
  if (inventory.status === 'failed')
601
749
  return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
@@ -649,8 +797,40 @@ function resolveExternalPullRequest(taskRef, options) {
649
797
  capabilities: base.context.capabilities,
650
798
  error: { code: selected.code, message: selected.message, retryable: false }
651
799
  }), { candidates: selected.candidates, eligible: selected.eligible });
652
- const note = externalEvidenceNote(base.issueNumber, selected.source, selected.selected);
653
- const identityNote = externalIdentityEvidenceNote(base.issueNumber, selected.selected);
800
+ const initialIdentity = validateWriterIdentity(base, selected.selected, { errorCode: 'PR_EXTERNAL_IDENTITY_MISMATCH' });
801
+ if (!initialIdentity.ok)
802
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
803
+ platform: base.context.platform, capabilities: base.context.capabilities,
804
+ pullRequest: selected.selected, error: initialIdentity.error
805
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
806
+ const rechecked = inspectGitHubPullRequest(base.client, base.context.platform.repository, selected.selected.number, base.resolved.repoRoot);
807
+ if (!rechecked.ok || !rechecked.value)
808
+ return externalResult(result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
809
+ platform: base.context.platform, capabilities: base.context.capabilities,
810
+ pullRequest: selected.selected,
811
+ error: rechecked.ok ? { code: 'PR_EXTERNAL_RECHECK_FAILED', message: 'Selected pull request recheck returned no identity', retryable: false } : rechecked.error
812
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
813
+ if (!sameDeliveryIdentity(selected.selected, rechecked.value))
814
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
815
+ platform: base.context.platform, capabilities: base.context.capabilities,
816
+ pullRequest: rechecked.value,
817
+ error: { code: 'PR_EXTERNAL_IDENTITY_MISMATCH', message: 'Selected pull request identity changed before task binding', retryable: false }
818
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
819
+ const finalIdentity = validateWriterIdentity(base, rechecked.value, { errorCode: 'PR_EXTERNAL_IDENTITY_MISMATCH' });
820
+ if (!finalIdentity.ok)
821
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
822
+ platform: base.context.platform, capabilities: base.context.capabilities,
823
+ pullRequest: rechecked.value, error: finalIdentity.error
824
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
825
+ const finalEvidence = validateExternalMergedEvidence(selected.selected, rechecked.value);
826
+ if (!finalEvidence.ok)
827
+ return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
828
+ platform: base.context.platform, capabilities: base.context.capabilities,
829
+ pullRequest: rechecked.value, error: finalEvidence.error
830
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: rechecked.value });
831
+ const selectedPullRequest = rechecked.value;
832
+ const note = externalEvidenceNote(base.issueNumber, selected.source, selectedPullRequest);
833
+ const identityNote = externalIdentityEvidenceNote(base.issueNumber, selectedPullRequest);
654
834
  const activityEntry = `**Bind External PR** by ${options.agent} — ${note}`;
655
835
  const auditLines = base.content.split(/\r?\n/).filter((line) => line.includes('**Bind External PR**'));
656
836
  const alreadyAudited = auditLines.some((line) => line.endsWith(identityNote));
@@ -658,25 +838,25 @@ function resolveExternalPullRequest(taskRef, options) {
658
838
  return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
659
839
  platform: base.context.platform,
660
840
  capabilities: base.context.capabilities,
661
- pullRequest: selected.selected,
841
+ pullRequest: selectedPullRequest,
662
842
  error: { code: 'PR_BIND_CONFLICT', message: 'Existing external PR audit evidence conflicts with the selected identity', retryable: false }
663
- }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
664
- if (alreadyAudited && base.prNumber !== selected.selected.number)
843
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
844
+ if (alreadyAudited && base.prNumber !== selectedPullRequest.number)
665
845
  return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
666
846
  platform: base.context.platform,
667
847
  capabilities: base.context.capabilities,
668
- pullRequest: selected.selected,
848
+ pullRequest: selectedPullRequest,
669
849
  error: { code: 'PR_BIND_CONFLICT', message: 'External PR audit evidence does not match the task binding', retryable: false }
670
- }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
671
- if (base.prNumber === selected.selected.number && alreadyAudited) {
850
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
851
+ if (base.prNumber === selectedPullRequest.number && alreadyAudited) {
672
852
  return externalResult(result('no-op', base.resolved.taskId, base.issueNumber, base.prNumber, {
673
853
  platform: base.context.platform,
674
854
  capabilities: base.context.capabilities,
675
- resource: { kind: 'pull-request', number: selected.selected.number },
676
- pullRequest: selected.selected,
855
+ resource: { kind: 'pull-request', number: selectedPullRequest.number },
856
+ pullRequest: selectedPullRequest,
677
857
  operations: [{ name: 'task:bind-external-pr', status: 'no-op', reasonCode: 'EVIDENCE_ALREADY_RECORDED' }],
678
858
  error: null
679
- }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
859
+ }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
680
860
  }
681
861
  const metadata = captureTaskWriteMetadata();
682
862
  const write = writeTask({
@@ -684,7 +864,7 @@ function resolveExternalPullRequest(taskRef, options) {
684
864
  expectedState: 'active',
685
865
  dryRun: options.dryRun,
686
866
  mutations: [
687
- { kind: 'frontmatter', set: { pr_number: selected.selected.number, pr_status: 'created', assigned_to: options.agent } },
867
+ { kind: 'frontmatter', set: { ...factFrontmatterMutation(boundFactFor(selectedPullRequest, selected.source === 'unique' ? 'external-unique' : 'external-explicit', base.issueNumber, metadata.timestamp)).set, assigned_to: options.agent } },
688
868
  { kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(base.content, `- ${metadata.timestamp} — ${activityEntry}`) }
689
869
  ]
690
870
  }, { repoRoot: base.resolved.repoRoot, metadataProvider: () => metadata });
@@ -692,193 +872,221 @@ function resolveExternalPullRequest(taskRef, options) {
692
872
  return externalResult(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
693
873
  platform: base.context.platform,
694
874
  capabilities: base.context.capabilities,
695
- pullRequest: selected.selected,
875
+ pullRequest: selectedPullRequest,
696
876
  error: { code: write.error.code, message: write.error.message, retryable: false }
697
- }), { candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
877
+ }), { candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
698
878
  const operationStatus = options.dryRun ? 'planned' : write.status === 'applied' ? 'applied' : 'no-op';
699
- return externalResult(result(operationStatus, base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : selected.selected.number, {
879
+ return externalResult(result(operationStatus, base.resolved.taskId, base.issueNumber, options.dryRun ? base.prNumber : selectedPullRequest.number, {
700
880
  changed: !options.dryRun && write.changed,
701
881
  platform: base.context.platform,
702
882
  capabilities: base.context.capabilities,
703
- resource: { kind: 'pull-request', number: selected.selected.number },
704
- pullRequest: selected.selected,
883
+ resource: { kind: 'pull-request', number: selectedPullRequest.number },
884
+ pullRequest: selectedPullRequest,
705
885
  operations: [{ name: 'task:bind-external-pr', status: operationStatus, reasonCode: null }],
706
886
  error: null
707
- }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selected.selected });
887
+ }), { mode: 'external', authorization: selected.source, candidates: selected.candidates, eligible: selected.eligible, selected: selectedPullRequest });
708
888
  }
709
889
  function createPlatformPullRequestUnlocked(taskRef, options) {
710
890
  const base = resolvedContext(taskRef, options);
711
891
  if (!base.ok)
712
- return base.output;
892
+ return withCreation(base.output, PRECONDITION_NOT_CREATED);
893
+ if (!base.fact)
894
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
895
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
896
+ }), PRECONDITION_NOT_CREATED);
713
897
  if (!options.base || !options.head || !options.title.trim() || !options.body.trim())
714
- return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
898
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
715
899
  error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
716
- });
900
+ }), PRECONDITION_NOT_CREATED);
901
+ const target = taskDeliveryTarget(base);
902
+ if (!target.ok)
903
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
904
+ error: target.error
905
+ }), PRECONDITION_NOT_CREATED);
906
+ if (options.base !== target.value.baseRef)
907
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
908
+ platform: base.context.platform, capabilities: base.context.capabilities,
909
+ error: { code: 'PR_DELIVERY_TARGET_MISMATCH', message: `Pull request base '${options.base}' does not match delivery target '${target.value.baseRef}'`, retryable: false }
910
+ }), PRECONDITION_NOT_CREATED);
717
911
  if (base.prNumber) {
718
912
  const headCheck = verifyCreateHead(base, options.head);
719
913
  if (!headCheck.ok)
720
- return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
914
+ return withCreation(result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
721
915
  platform: base.context.platform,
722
916
  capabilities: base.context.capabilities,
723
917
  error: headCheck.error
724
- });
918
+ }), PRECONDITION_NOT_CREATED);
725
919
  const inspected = inspectPlatformPullRequest(taskRef, options);
726
920
  const identity = validateBoundPullRequest(base, inspected.pullRequest, options.head, options.base, headCheck.value);
727
921
  if (!identity.ok)
728
- return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
922
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
729
923
  platform: base.context.platform,
730
924
  capabilities: base.context.capabilities,
731
925
  pullRequest: inspected.pullRequest,
732
926
  error: identity.error
733
- });
734
- return { ...inspected, result: 'no_op' };
927
+ }), PRECONDITION_NOT_CREATED);
928
+ return withCreation({ ...inspected, result: 'no_op' }, { kind: 'no-op', createdByCurrentOperation: false });
735
929
  }
736
930
  const headCheck = verifyCreateHead(base, options.head);
737
931
  if (!headCheck.ok)
738
- return result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
932
+ return withCreation(result(headCheck.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
739
933
  platform: base.context.platform,
740
934
  capabilities: base.context.capabilities,
741
935
  error: headCheck.error
742
- });
936
+ }), PRECONDITION_NOT_CREATED);
743
937
  const expectedHeadSha = headCheck.value;
744
938
  const located = locatePullRequest(base, options.head, options.base, expectedHeadSha);
745
939
  if (!located.ok && located.error.code === 'PR_IDENTITY_AMBIGUOUS')
746
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
940
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
747
941
  platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
748
- });
942
+ }), PRECONDITION_NOT_CREATED);
749
943
  if (!located.ok && located.error.code === 'PR_HEAD_SHA_MISMATCH')
750
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
944
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
751
945
  platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
752
- });
946
+ }), PRECONDITION_NOT_CREATED);
947
+ if (!located.ok && located.error.code !== 'PR_NOT_FOUND')
948
+ return withCreation(result(located.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
949
+ platform: base.context.platform, capabilities: base.context.capabilities, error: located.error
950
+ }), PRECONDITION_NOT_CREATED);
753
951
  if (options.dryRun)
754
- return result('planned', base.resolved.taskId, base.issueNumber, null, {
952
+ return withCreation(result('planned', base.resolved.taskId, base.issueNumber, null, {
755
953
  platform: base.context.platform, capabilities: base.context.capabilities,
756
954
  operations: [{ name: located.ok ? 'pr:reuse' : 'pr:create', status: 'planned', reasonCode: null }],
757
955
  result: located.ok ? 'pr_reused' : 'pr_created', error: null
758
- });
956
+ }), { kind: 'planned', action: located.ok ? 'reuse' : 'create', createdByCurrentOperation: false });
759
957
  const started = writeCreateStarted(base, options.agent);
760
958
  if (started.status === 'failed')
761
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
959
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
762
960
  error: { code: started.error.code, message: started.error.message, retryable: false }
763
- });
961
+ }), PRECONDITION_NOT_CREATED);
764
962
  let pullRequest = located.ok ? located.value : null;
765
963
  let created = false;
766
964
  if (!pullRequest) {
767
965
  const repository = base.context.platform.repository;
966
+ if (options.phase)
967
+ options.phase.value = 'post-dispatched';
768
968
  const response = base.client.json(['api', `repos/${repository}/pulls`, '-X', 'POST', '--input', '-'], {
769
969
  cwd: base.resolved.repoRoot,
770
970
  method: 'POST',
771
971
  input: JSON.stringify({ title: options.title, body: options.body, head: options.head, base: options.base, draft: Boolean(options.draft) })
772
972
  });
773
- if (response.ok)
973
+ if (response.ok) {
974
+ // The remote POST is accepted before any normalization, recheck, or task write.
975
+ // From this point on the current call has created a remote resource even if
976
+ // local post-processing later fails.
977
+ created = true;
978
+ if (options.phase)
979
+ options.phase.value = 'post-accepted';
774
980
  pullRequest = normalizePullRequest(response.value, repository);
981
+ }
775
982
  else if (response.error.retryable) {
776
- const recovered = locatePullRequest(base, options.head, options.base, expectedHeadSha);
777
- if (recovered.ok)
778
- pullRequest = recovered.value;
779
- else
780
- return result('blocked', base.resolved.taskId, base.issueNumber, null, {
781
- platform: base.context.platform, capabilities: base.context.capabilities,
782
- operations: [{ name: 'pr:create', status: 'failed', reasonCode: 'PR_CREATE_OUTCOME_UNKNOWN' }],
783
- error: { code: 'PR_CREATE_OUTCOME_UNKNOWN', message: response.error.message, retryable: true }
784
- });
983
+ return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
984
+ platform: base.context.platform, capabilities: base.context.capabilities,
985
+ operations: [{ name: 'pr:create', status: 'failed', reasonCode: 'PR_CREATE_OUTCOME_UNKNOWN' }],
986
+ error: { code: 'PR_CREATE_OUTCOME_UNKNOWN', message: response.error.message, retryable: true }
987
+ }), { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' });
785
988
  }
786
989
  else
787
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
990
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
788
991
  platform: base.context.platform, capabilities: base.context.capabilities,
789
992
  operations: [{ name: 'pr:create', status: 'failed', reasonCode: response.error.code }], error: response.error
790
- });
993
+ }), { kind: 'not-created', reason: 'post-rejected', createdByCurrentOperation: false });
791
994
  if (!pullRequest)
792
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
995
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
793
996
  error: { code: 'PR_CREATE_RESPONSE_INVALID', message: 'PR create response lacks validated identity', retryable: false }
794
- });
997
+ }), { kind: 'created', createdByCurrentOperation: true });
795
998
  if (pullRequest.head.sha !== expectedHeadSha)
796
- return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
999
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
797
1000
  platform: base.context.platform, capabilities: base.context.capabilities,
798
1001
  resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
799
1002
  error: { code: 'PR_HEAD_SHA_MISMATCH', message: 'Created pull request head does not match the expected local HEAD', retryable: false }
800
- });
801
- created = true;
1003
+ }), { kind: 'created', createdByCurrentOperation: true });
802
1004
  }
803
1005
  const bindHeadCheck = verifyCreateHead(base, options.head);
804
1006
  if (!bindHeadCheck.ok || bindHeadCheck.value !== expectedHeadSha) {
805
1007
  const error = bindHeadCheck.ok
806
1008
  ? { code: 'PR_REMOTE_HEAD_MISMATCH', message: 'Remote head changed before task binding', retryable: false }
807
1009
  : bindHeadCheck.error;
808
- return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1010
+ return withCreation(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
809
1011
  platform: base.context.platform, capabilities: base.context.capabilities,
810
1012
  resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
811
- });
1013
+ }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
812
1014
  }
813
1015
  const beforeBind = inspectGitHubPullRequest(base.client, base.context.platform.repository, pullRequest.number, base.resolved.repoRoot);
814
1016
  if (!beforeBind.ok || !beforeBind.value) {
815
1017
  const error = beforeBind.ok
816
1018
  ? { code: 'PR_BIND_RECHECK_FAILED', message: 'Pull request recheck returned no identity', retryable: false }
817
1019
  : beforeBind.error;
818
- return result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1020
+ return withCreation(result(error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
819
1021
  platform: base.context.platform, capabilities: base.context.capabilities,
820
1022
  resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest, error
821
- });
1023
+ }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
822
1024
  }
823
1025
  if (beforeBind.value.head.sha !== expectedHeadSha
824
1026
  || beforeBind.value.head.ref !== expectedHead(base.context.platform.repository, options.head).ref
825
1027
  || beforeBind.value.base.ref !== options.base
826
1028
  || beforeBind.value.base.repository !== base.context.platform.repository)
827
- return result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1029
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, pullRequest.number, {
828
1030
  platform: base.context.platform, capabilities: base.context.capabilities,
829
1031
  resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest: beforeBind.value,
830
1032
  error: { code: 'PR_BIND_IDENTITY_MISMATCH', message: 'Pull request identity changed before task binding', retryable: false }
831
- });
1033
+ }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
832
1034
  pullRequest = beforeBind.value;
833
1035
  const refreshed = resolvedContext(taskRef, options);
834
1036
  if (!refreshed.ok)
835
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
1037
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
836
1038
  pullRequest, error: { code: 'PR_CREATED_BIND_FAILED', message: pullRequest.url, retryable: false }
837
- });
838
- const bound = bindIdentity(refreshed, pullRequest, options.agent);
1039
+ }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'not-created', reason: 'precondition-failed', createdByCurrentOperation: false });
1040
+ const bound = bindIdentity(refreshed, pullRequest, options.agent, created ? 'created' : 'reused', false, options.head, expectedHeadSha);
839
1041
  if (bound.status === 'failed')
840
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
1042
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
841
1043
  platform: base.context.platform, capabilities: base.context.capabilities,
842
1044
  resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
843
1045
  operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: 'applied', reasonCode: null }, { name: 'task:bind-pr', status: 'failed', reasonCode: bound.error.code }],
844
1046
  error: { code: 'PR_CREATED_BIND_FAILED', message: `${pullRequest.url}: ${bound.error.message}`, retryable: false }
845
- });
846
- return result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
1047
+ }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'reused', createdByCurrentOperation: false });
1048
+ return withCreation(result('applied', base.resolved.taskId, base.issueNumber, pullRequest.number, {
847
1049
  changed: true, platform: base.context.platform, capabilities: base.context.capabilities,
848
1050
  resource: { kind: 'pull-request', number: pullRequest.number }, pullRequest,
849
1051
  operations: [{ name: created ? 'pr:create' : 'pr:reuse', status: created ? 'applied' : 'no-op', reasonCode: null }, { name: 'task:bind-pr', status: 'applied', reasonCode: null }],
850
1052
  result: created ? 'pr_created' : 'pr_reused', error: null
851
- });
1053
+ }), created ? { kind: 'created', createdByCurrentOperation: true } : { kind: 'reused', createdByCurrentOperation: false });
852
1054
  }
853
1055
  function createPlatformPullRequest(taskRef, options) {
854
1056
  const base = resolvedContext(taskRef, options);
855
1057
  if (!base.ok)
856
- return base.output;
1058
+ return withCreation(base.output, PRECONDITION_NOT_CREATED);
857
1059
  if (!options.base || !options.head || !options.title.trim() || !options.body.trim()) {
858
- return result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
1060
+ return withCreation(result('failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
859
1061
  error: { code: 'PR_PAYLOAD_INVALID', message: 'base, head, title and body are required', retryable: false }
860
- });
1062
+ }), PRECONDITION_NOT_CREATED);
861
1063
  }
1064
+ const phase = { value: 'before-post' };
862
1065
  try {
863
- return withTaskExecutionLock(base.resolved.repoRoot, base.resolved.taskId, 'platform-pr.create', () => createPlatformPullRequestUnlocked(taskRef, options));
1066
+ return withTaskExecutionLock(base.resolved.repoRoot, base.resolved.taskId, 'platform-pr.create', () => createPlatformPullRequestUnlocked(taskRef, { ...options, phase }));
864
1067
  }
865
1068
  catch (error) {
866
1069
  if (error instanceof TaskExecutionLockError) {
867
- return result('blocked', base.resolved.taskId, base.issueNumber, null, {
1070
+ return withCreation(result('blocked', base.resolved.taskId, base.issueNumber, null, {
868
1071
  platform: base.context.platform,
869
1072
  capabilities: base.context.capabilities,
870
1073
  error: { code: error.code, message: error.message, retryable: true }
871
- });
1074
+ }), PRECONDITION_NOT_CREATED);
872
1075
  }
873
- return result('failed', base.resolved.taskId, base.issueNumber, null, {
1076
+ const creation = phase.value === 'post-accepted'
1077
+ ? { kind: 'created', createdByCurrentOperation: true }
1078
+ : phase.value === 'post-dispatched'
1079
+ ? { kind: 'unknown', errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' }
1080
+ : PRECONDITION_NOT_CREATED;
1081
+ return withCreation(result(phase.value === 'post-dispatched' ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, null, {
874
1082
  platform: base.context.platform,
875
1083
  capabilities: base.context.capabilities,
876
1084
  error: {
877
- code: 'PR_CREATE_FAILED',
1085
+ code: phase.value === 'post-dispatched' ? 'PR_CREATE_OUTCOME_UNKNOWN' : 'PR_CREATE_FAILED',
878
1086
  message: error instanceof Error ? error.message : String(error),
879
- retryable: false
1087
+ retryable: phase.value === 'post-dispatched'
880
1088
  }
881
- });
1089
+ }), creation);
882
1090
  }
883
1091
  }
884
1092
  function syncPlatformPullRequest(taskRef, options) {
@@ -902,9 +1110,13 @@ function syncPlatformPullRequest(taskRef, options) {
902
1110
  const base = resolvedContext(taskRef, options);
903
1111
  if (!base.ok)
904
1112
  return softenFailure(base.output);
1113
+ if (!base.fact)
1114
+ return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
1115
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
1116
+ }));
905
1117
  if (!base.prNumber)
906
1118
  return softenFailure(result('failed', base.resolved.taskId, base.issueNumber, null, {
907
- error: { code: 'PR_NOT_LINKED', message: 'Task has no valid pr_number', retryable: false }
1119
+ error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false }
908
1120
  }));
909
1121
  const fetched = inspectGitHubPullRequest(base.client, base.context.platform.repository, base.prNumber, base.resolved.repoRoot);
910
1122
  if (!fetched.ok)
@@ -925,7 +1137,7 @@ function syncPlatformPullRequest(taskRef, options) {
925
1137
  const planned = planPullRequestMetadata({
926
1138
  pullRequest: fetched.value,
927
1139
  issue: issue.issue,
928
- taskType: base.frontmatter.type || 'task',
1140
+ taskType: String(base.frontmatter.type || 'task'),
929
1141
  issueNumber: base.issueNumber,
930
1142
  capabilities: base.context.capabilities
931
1143
  }).operations.filter((operation) => options.metadata || operation.name === 'closing-issue')
@@ -970,6 +1182,96 @@ function syncPlatformPullRequest(taskRef, options) {
970
1182
  operations: operations.map((operation) => operation.status === 'planned' ? { ...operation, status: 'applied' } : operation), error: null
971
1183
  }));
972
1184
  }
973
- export { bindPlatformPullRequest, createPlatformPullRequest, inspectGitHubPullRequest, inspectGitHubIssueClosingChangeRequests, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, normalizePullRequest, resolveGitHubChangeRequestGitEvidence, resolveExternalPullRequest, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest };
1185
+ function readProjectPrFlow(repoRoot) {
1186
+ try {
1187
+ const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
1188
+ return config.prFlow === 'required' || config.prFlow === 'disabled' ? config.prFlow : undefined;
1189
+ }
1190
+ catch {
1191
+ return undefined;
1192
+ }
1193
+ }
1194
+ function skipPlatformPullRequestFactUnlocked(resolved, options) {
1195
+ let content;
1196
+ let frontmatter;
1197
+ try {
1198
+ content = fs.readFileSync(resolved.taskMdPath, 'utf8');
1199
+ frontmatter = parseTypedTaskFrontmatter(content);
1200
+ }
1201
+ catch (error) {
1202
+ return result('failed', resolved.taskId, null, null, {
1203
+ error: { code: 'TASK_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
1204
+ });
1205
+ }
1206
+ const issue = Number(frontmatter.issue_number);
1207
+ const issueNumber = Number.isSafeInteger(issue) && issue > 0 ? issue : null;
1208
+ const existing = readPrDeliveryFact(frontmatter);
1209
+ if (existing.status === 'invalid')
1210
+ return result('failed', resolved.taskId, issueNumber, null, {
1211
+ error: { code: 'PR_DELIVERY_FACT_INVALID', message: existing.error.message, retryable: false }
1212
+ });
1213
+ if (existing.status === 'missing')
1214
+ return result('failed', resolved.taskId, issueNumber, null, {
1215
+ error: { code: 'PR_DELIVERY_FACT_MISSING', message: 'Task has no pr_delivery_fact; repair the task metadata or create a new task', retryable: false }
1216
+ });
1217
+ if (existing.fact.state === 'skipped')
1218
+ return result('no-op', resolved.taskId, issueNumber, null, {
1219
+ operations: [{ name: 'task:skip-pr', status: 'no-op', reasonCode: null }],
1220
+ error: null
1221
+ });
1222
+ if (existing.fact.state === 'bound')
1223
+ return result('failed', resolved.taskId, issueNumber, existing.fact.identity.number, {
1224
+ error: { code: 'PR_DELIVERY_FACT_CONFLICT', message: `Task is already bound to PR #${existing.fact.identity.number}`, retryable: false }
1225
+ });
1226
+ if (readProjectPrFlow(resolved.repoRoot) === 'required')
1227
+ return result('failed', resolved.taskId, issueNumber, null, {
1228
+ error: { code: 'PR_SKIP_FORBIDDEN', message: 'Cannot skip PR delivery when prFlow is required', retryable: false }
1229
+ });
1230
+ const metadata = captureTaskWriteMetadata();
1231
+ const fact = buildSkippedFact(factTimestamp(metadata.timestamp));
1232
+ const write = writeTask({
1233
+ taskRef: resolved.taskId,
1234
+ expectedState: 'active',
1235
+ dryRun: options.dryRun,
1236
+ mutations: [
1237
+ { kind: 'frontmatter', set: { ...factFrontmatterMutation(fact).set, assigned_to: options.agent } },
1238
+ { kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: '活动日志', body: appendActivity(content, `- ${metadata.timestamp} — **Skip PR** by ${options.agent} — PR delivery explicitly skipped`) }
1239
+ ]
1240
+ }, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
1241
+ if (write.status === 'failed')
1242
+ return result('failed', resolved.taskId, issueNumber, null, {
1243
+ error: { code: write.error.code, message: write.error.message, retryable: false }
1244
+ });
1245
+ const status = options.dryRun ? 'planned' : write.status === 'no-op' ? 'no-op' : 'applied';
1246
+ return result(status, resolved.taskId, issueNumber, null, {
1247
+ changed: !options.dryRun && write.changed,
1248
+ operations: [{ name: 'task:skip-pr', status, reasonCode: null }],
1249
+ error: null
1250
+ });
1251
+ }
1252
+ function skipPlatformPullRequestFact(taskRef, options) {
1253
+ const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
1254
+ if (!resolved.ok)
1255
+ return result('failed', resolved.taskId, null, null, {
1256
+ error: { code: resolved.code, message: resolved.message, retryable: false }
1257
+ });
1258
+ if (resolved.state !== 'active')
1259
+ return result('failed', resolved.taskId, null, null, {
1260
+ error: { code: 'TASK_STATE_INVALID', message: 'platform-pr skip requires an active task', retryable: false }
1261
+ });
1262
+ try {
1263
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'platform-pr.skip', () => skipPlatformPullRequestFactUnlocked(resolved, options));
1264
+ }
1265
+ catch (error) {
1266
+ if (error instanceof TaskExecutionLockError)
1267
+ return result('blocked', resolved.taskId, null, null, {
1268
+ error: { code: error.code, message: error.message, retryable: true }
1269
+ });
1270
+ return result('failed', resolved.taskId, null, null, {
1271
+ error: { code: 'PR_SKIP_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false }
1272
+ });
1273
+ }
1274
+ }
1275
+ export { bindPlatformPullRequest, createPlatformPullRequest, inspectGitHubPullRequest, inspectGitHubIssueClosingChangeRequests, inspectPlatformPullRequest, inspectPlatformPullRequestByNumber, normalizePullRequest, resolveGitHubChangeRequestGitEvidence, resolveExternalPullRequest, skipPlatformPullRequestFact, selectExternalPullRequest, selectPullRequest, syncPlatformPullRequest };
974
1276
  export { warningResultForPrimary };
975
1277
  //# sourceMappingURL=pull-requests.js.map