@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
@@ -29,7 +29,6 @@ import type {
29
29
  } from './delegation-receipts.ts';
30
30
  import { normalizeAgentClients } from '../agent-clients/config.ts';
31
31
  import { resolveAgentRuntimeStoreRoot } from '../runtime/agent-runtime.ts';
32
- import { inspectPlatformPullRequest } from '../platform/pull-requests.ts';
33
32
  import {
34
33
  getAgentClientCapability,
35
34
  getAgentClientDelegationEvidence,
@@ -52,6 +51,8 @@ import { assertGitRepositoryBinding } from '../git/worktree-identity.ts';
52
51
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
53
52
  import { hasActiveCodexLifecycleEvidence } from '../agent-clients/adapters/codex-lifecycle/store.ts';
54
53
  import type { CodexCapabilityProvenanceDetail } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
54
+ import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead, extractReviewedHead } from './review-fingerprint.ts';
55
+ import { resolveDeliveryTarget, resolveDiffBase, resolveTargetHead } from './delivery-target.ts';
55
56
 
56
57
  type OrchestrationStatus = 'running' | 'paused' | 'completed';
57
58
  type ModelPolicySource = Readonly<{
@@ -84,8 +85,8 @@ type CleanCompletionEvidence = Readonly<{
84
85
  headTree: string;
85
86
  worktreeTree: string;
86
87
  lastReviewedCommit: string;
87
- prNumber: number;
88
- prHead: string;
88
+ prNumber: number | null;
89
+ prHead: string | null;
89
90
  }>;
90
91
  type OrchestrationRun = Readonly<{
91
92
  taskId: string;
@@ -273,9 +274,8 @@ function isCompletionEvidence(value: unknown): value is CleanCompletionEvidence
273
274
  && exactText(value.headTree)
274
275
  && exactText(value.worktreeTree)
275
276
  && exactText(value.lastReviewedCommit)
276
- && Number.isSafeInteger(value.prNumber)
277
- && (value.prNumber as number) > 0
278
- && exactText(value.prHead);
277
+ && (value.prNumber === null || (Number.isSafeInteger(value.prNumber) && (value.prNumber as number) > 0))
278
+ && (value.prHead === null || exactText(value.prHead));
279
279
  }
280
280
 
281
281
  const ORCHESTRATION_RUN_KEYS = [
@@ -523,6 +523,44 @@ function artifactName(family: string, round: number): string {
523
523
  return round === 1 ? `${family}.md` : `${family}-r${round}.md`;
524
524
  }
525
525
 
526
+ function validateSavedCurrentDiffBase(
527
+ repoRoot: string,
528
+ metadata: ReturnType<typeof parseTypedTaskFrontmatter>,
529
+ taskDir: string,
530
+ reviewedHead: string
531
+ ): Readonly<{ code: string; message: string }> | null {
532
+ const reviewRound = highestRound(taskDir, 'review-code');
533
+ const reviewContent = fs.readFileSync(path.join(taskDir, artifactName('review-code', reviewRound)), 'utf8');
534
+ const savedTargetHead = extractReviewTargetHead(reviewContent);
535
+ const savedDiffBase = extractReviewDiffBase(reviewContent);
536
+ const savedReviewedHead = extractReviewedHead(reviewContent) || extractReviewBaseline(reviewContent);
537
+ if (!savedTargetHead && !savedDiffBase && !savedReviewedHead) return null;
538
+ if (savedReviewedHead && savedReviewedHead !== reviewedHead) {
539
+ return { code: 'ORCHESTRATION_REVIEWED_HEAD_MISMATCH', message: 'review-code saved reviewed head does not match last_reviewed_commit' };
540
+ }
541
+ if (!savedTargetHead || !savedDiffBase) {
542
+ return { code: 'ORCHESTRATION_REVIEW_TARGET_INVALID', message: 'review-code must save target head and diff base together' };
543
+ }
544
+ const savedBase = resolveDiffBase(repoRoot, reviewedHead, savedTargetHead);
545
+ if (!savedBase.ok) return { code: 'ORCHESTRATION_REVIEW_TARGET_INVALID', message: savedBase.message };
546
+ if (savedBase.diffBase !== savedDiffBase) {
547
+ return { code: 'ORCHESTRATION_REVIEW_DIFF_BASE_MISMATCH', message: 'saved review diff base does not match reviewed head and saved target head' };
548
+ }
549
+ const target = resolveDeliveryTarget(repoRoot, {
550
+ remote: typeof metadata.delivery_remote === 'string' ? metadata.delivery_remote : undefined,
551
+ baseRef: typeof metadata.delivery_base_ref === 'string' ? metadata.delivery_base_ref : undefined
552
+ });
553
+ if (!target.ok) return { code: target.code, message: target.message };
554
+ const currentTarget = resolveTargetHead(repoRoot, target.value);
555
+ if (!currentTarget.ok) return { code: currentTarget.code, message: currentTarget.message };
556
+ const currentBase = resolveDiffBase(repoRoot, reviewedHead, currentTarget.head!);
557
+ if (!currentBase.ok) return { code: currentBase.code, message: currentBase.message };
558
+ if (currentBase.diffBase !== savedDiffBase) {
559
+ return { code: 'ORCHESTRATION_REVIEW_TARGET_CHANGED', message: 'current delivery target changed the reviewed diff base; re-run review-code' };
560
+ }
561
+ return null;
562
+ }
563
+
526
564
  function hasReviewAfterArtifact(taskDir: string, family: string, reviewFamily: string): boolean {
527
565
  const artifact = inspectArtifactDirectory(taskDir, family as 'analysis' | 'plan' | 'code');
528
566
  const review = inspectArtifactDirectory(taskDir, reviewFamily as 'review-analysis' | 'review-plan' | 'review-code');
@@ -656,29 +694,8 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
656
694
  if (run?.status === 'completed') {
657
695
  return { status: 'completed', changed: false, taskId: resolved.taskId, run, next: null, error: null };
658
696
  }
659
-
660
- let config: unknown;
661
- try {
662
- config = JSON.parse(fs.readFileSync(path.join(resolved.repoRoot, '.agents', '.airc.json'), 'utf8'));
663
- } catch (error) {
664
- return failed('ORCHESTRATION_CONFIG_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
665
- }
666
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
667
- return failed('ORCHESTRATION_CONFIG_INVALID', 'project configuration must be an object', resolved.taskId);
668
- }
669
- const projectConfig = config as Record<string, unknown>;
670
- if (!Object.hasOwn(projectConfig, 'prFlow') || projectConfig.prFlow === 'disabled') {
671
- return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
672
- }
673
- if (projectConfig.prFlow !== 'required') {
674
- return failed('ORCHESTRATION_CONFIG_INVALID', "project configuration 'prFlow' must be 'required' or 'disabled'", resolved.taskId);
675
- }
676
- const prNumber = Number(metadata.pr_number);
677
- if (!Number.isInteger(prNumber) || prNumber <= 0) {
678
- return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
679
- }
680
697
  if (!run) {
681
- return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
698
+ return { status: 'running', changed: false, taskId: resolved.taskId, run, next: null, error: null };
682
699
  }
683
700
  if (run.status !== 'running' || run.pendingDelegation !== null) {
684
701
  return failed('ORCHESTRATION_RUN_NOT_RUNNING', 'clean completion requires an idle running orchestration', resolved.taskId);
@@ -692,35 +709,19 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
692
709
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
693
710
  }
694
711
  if (before.headTree !== before.worktreeTree) {
695
- return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
712
+ return { status: 'running', changed: false, taskId: resolved.taskId, run, next: null, error: null };
696
713
  }
697
714
  const lastReviewedCommit = String(metadata.last_reviewed_commit ?? '');
698
715
  if (before.head !== lastReviewedCommit) {
699
716
  return failed('ORCHESTRATION_REVIEWED_HEAD_MISMATCH', 'local HEAD does not match last_reviewed_commit', resolved.taskId);
700
717
  }
701
-
702
- const inspect: NonNullable<OrchestrationOptions['inspectPullRequest']> =
703
- options.inspectPullRequest ?? inspectPlatformPullRequest;
704
- let inspection: ReturnType<NonNullable<OrchestrationOptions['inspectPullRequest']>>;
705
- try {
706
- inspection = inspect(resolved.taskId, { cwd: gitRoot });
707
- } catch (error) {
708
- return failed('ORCHESTRATION_PR_INSPECTION_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
709
- }
710
- if (inspection.status === 'blocked') {
711
- return failed('ORCHESTRATION_PR_INSPECTION_BLOCKED', inspection.error?.message ?? 'pull request inspection is blocked', resolved.taskId);
712
- }
713
- if (inspection.status === 'failed') {
714
- return failed('ORCHESTRATION_PR_INSPECTION_FAILED', inspection.error?.message ?? 'pull request inspection failed', resolved.taskId);
715
- }
716
- const inspectedPrNumber = inspection.task?.prNumber ?? inspection.prNumber;
717
- const prHead = inspection.pullRequest?.head?.sha;
718
- if (typeof prHead !== 'string') {
719
- return failed('ORCHESTRATION_PR_INSPECTION_INVALID', 'pull request inspection returned incomplete identity evidence', resolved.taskId);
720
- }
721
- if (inspectedPrNumber !== prNumber || prHead !== before.head) {
722
- return failed('ORCHESTRATION_PR_HEAD_MISMATCH', 'pull request identity does not match the reviewed local HEAD', resolved.taskId);
723
- }
718
+ const diffBaseError = validateSavedCurrentDiffBase(
719
+ resolved.repoRoot,
720
+ metadata,
721
+ resolved.taskDir,
722
+ lastReviewedCommit
723
+ );
724
+ if (diffBaseError) return failed(diffBaseError.code, diffBaseError.message, resolved.taskId);
724
725
 
725
726
  let after: RepositorySnapshot;
726
727
  try {
@@ -729,7 +730,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
729
730
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
730
731
  }
731
732
  if (after.headTree !== after.worktreeTree) {
732
- return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
733
+ return { status: 'running', changed: false, taskId: resolved.taskId, run, next: null, error: null };
733
734
  }
734
735
  if (after.head !== before.head || after.headTree !== before.headTree) {
735
736
  return failed('ORCHESTRATION_REPOSITORY_CHANGED', 'repository changed while clean completion evidence was collected', resolved.taskId);
@@ -743,8 +744,8 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
743
744
  headTree: after.headTree,
744
745
  worktreeTree: after.worktreeTree,
745
746
  lastReviewedCommit,
746
- prNumber,
747
- prHead
747
+ prNumber: null,
748
+ prHead: null
748
749
  };
749
750
  const completed = withUpdatedRun(run, {
750
751
  status: 'completed',
@@ -852,10 +853,7 @@ function prepareOrchestrationDelegationUnlocked(
852
853
  }
853
854
  const updated = withUpdatedRun(run, {
854
855
  nextStage: next.stage,
855
- pendingDelegation: receipt,
856
- commitAuthorization: next.stage === 'commit' && run.commitAuthorization.issuedAt === null
857
- ? { issuedAt: (options.now ?? (() => new Date().toISOString()))(), consumedAt: null }
858
- : run.commitAuthorization
856
+ pendingDelegation: receipt
859
857
  });
860
858
  saveRun(resolved.taskDir, updated);
861
859
  return { status: 'running', changed: true, taskId: resolved.taskId, run: updated, next, error: null };
@@ -1460,16 +1458,15 @@ function advanceOrchestration(taskRef: string, options: OrchestrationOptions = {
1460
1458
  if (!run?.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
1461
1459
  const result = consumeDelegation(run.pendingDelegation, { now: options.now });
1462
1460
  if (!result.ok) return pauseOrchestration(taskRef, result.code, result.message, true, options);
1463
- const completed = result.receipt.stage === 'commit';
1461
+ if (result.receipt.stage === 'commit') {
1462
+ return failed('ORCHESTRATED_COMMIT_REMOVED', 'Orchestrated commit is no longer a lifecycle stage; use local checkpoint delivery from code-task', resolved.taskId);
1463
+ }
1464
1464
  const updated = withUpdatedRun(run, {
1465
- status: completed ? 'completed' : 'running',
1465
+ status: 'running',
1466
1466
  nextStage: null,
1467
1467
  stepCount: run.stepCount + 1,
1468
1468
  pendingDelegation: null,
1469
- receipts: Object.freeze([...run.receipts, result.receipt]),
1470
- commitAuthorization: completed
1471
- ? { ...run.commitAuthorization, consumedAt: (options.now ?? (() => new Date().toISOString()))() }
1472
- : run.commitAuthorization
1469
+ receipts: Object.freeze([...run.receipts, result.receipt])
1473
1470
  });
1474
1471
  saveRun(resolved.taskDir, updated);
1475
1472
  return { status: updated.status, changed: true, taskId: resolved.taskId, run: updated, next: null, error: null };
@@ -0,0 +1,254 @@
1
+ const PR_DELIVERY_FACT_KEY = 'pr_delivery_fact';
2
+
3
+ type PrDeliveryIdentity = {
4
+ repository: string;
5
+ number: number;
6
+ nodeId: string;
7
+ url: string;
8
+ head: { repository: string; ref: string; sha: string };
9
+ base: { repository: string; ref: string; sha: string };
10
+ };
11
+
12
+ type PrDeliveryBindingSource =
13
+ | 'created'
14
+ | 'reused'
15
+ | 'explicit-bind'
16
+ | 'external-unique'
17
+ | 'external-explicit';
18
+
19
+ type PrDeliveryProvenance =
20
+ | 'create-post'
21
+ | 'reuse'
22
+ | 'explicit-bind'
23
+ | 'external-unique'
24
+ | 'external-explicit';
25
+
26
+ type PrDeliveryFact =
27
+ | { version: 1; state: 'unbound'; reason: 'initial' }
28
+ | { version: 1; state: 'skipped'; reason: 'explicit'; decidedAt: string }
29
+ | {
30
+ version: 1;
31
+ state: 'bound';
32
+ identity: PrDeliveryIdentity;
33
+ binding: {
34
+ status: 'verified';
35
+ source: PrDeliveryBindingSource;
36
+ verifiedAt: string;
37
+ issueNumber: number | null;
38
+ remoteState: 'open' | 'closed';
39
+ mergedAt: string | null;
40
+ mergeCommitSha: string | null;
41
+ };
42
+ provenance: { establishedBy: PrDeliveryProvenance };
43
+ };
44
+
45
+ type CreationOutcome =
46
+ | { kind: 'planned'; action: 'create' | 'reuse'; createdByCurrentOperation: false }
47
+ | { kind: 'not-created'; reason: 'precondition-failed' | 'post-rejected'; createdByCurrentOperation: false }
48
+ | { kind: 'created'; createdByCurrentOperation: true }
49
+ | { kind: 'reused'; createdByCurrentOperation: false }
50
+ | { kind: 'no-op'; createdByCurrentOperation: false }
51
+ | { kind: 'unknown'; errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' };
52
+
53
+ type PrDeliveryFactReadResult =
54
+ | { status: 'valid'; fact: PrDeliveryFact }
55
+ | { status: 'missing'; fact: null }
56
+ | { status: 'invalid'; fact: null; error: Error };
57
+
58
+ function factError(message: string): Error & { code: string } {
59
+ return Object.assign(new Error(`PR_DELIVERY_FACT_INVALID: ${message}`), { code: 'PR_DELIVERY_FACT_INVALID' });
60
+ }
61
+
62
+ function isRecord(value: unknown): value is Record<string, unknown> {
63
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
64
+ }
65
+
66
+ function exactKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {
67
+ const actual = Object.keys(value).sort();
68
+ const expected = [...keys].sort();
69
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
70
+ throw factError(`${label} has an invalid field set`);
71
+ }
72
+ }
73
+
74
+ function text(value: unknown, label: string): string {
75
+ if (typeof value !== 'string' || !value || value.trim() !== value) throw factError(`${label} must be a non-empty string`);
76
+ return value;
77
+ }
78
+
79
+ function nullableText(value: unknown, label: string): string | null {
80
+ return value === null ? null : text(value, label);
81
+ }
82
+
83
+ function positiveNumber(value: unknown, label: string): number {
84
+ if (!Number.isSafeInteger(value) || (value as number) <= 0) throw factError(`${label} must be a positive integer`);
85
+ return value as number;
86
+ }
87
+
88
+ function optionalTimestamp(value: unknown, label: string): string | null {
89
+ const result = nullableText(value, label);
90
+ if (result !== null && Number.isNaN(Date.parse(result))) throw factError(`${label} must be a valid timestamp`);
91
+ return result;
92
+ }
93
+
94
+ function parseIdentity(value: unknown): PrDeliveryIdentity {
95
+ if (!isRecord(value)) throw factError('identity must be an object');
96
+ exactKeys(value, ['repository', 'number', 'nodeId', 'url', 'head', 'base'], 'identity');
97
+ const head = parseRef(value.head, 'head');
98
+ const base = parseRef(value.base, 'base');
99
+ return {
100
+ repository: text(value.repository, 'identity.repository'),
101
+ number: positiveNumber(value.number, 'identity.number'),
102
+ nodeId: text(value.nodeId, 'identity.nodeId'),
103
+ url: text(value.url, 'identity.url'),
104
+ head,
105
+ base
106
+ };
107
+ }
108
+
109
+ function parseRef(value: unknown, label: string): { repository: string; ref: string; sha: string } {
110
+ if (!isRecord(value)) throw factError(`${label} must be an object`);
111
+ exactKeys(value, ['repository', 'ref', 'sha'], label);
112
+ return {
113
+ repository: text(value.repository, `${label}.repository`),
114
+ ref: text(value.ref, `${label}.ref`),
115
+ sha: text(value.sha, `${label}.sha`)
116
+ };
117
+ }
118
+
119
+ function parseFact(value: unknown): PrDeliveryFact {
120
+ if (!isRecord(value)) throw factError('fact must be an object');
121
+ if (value.version !== 1 || typeof value.state !== 'string') throw factError('version or state is invalid');
122
+ if (value.state === 'unbound') {
123
+ exactKeys(value, ['version', 'state', 'reason'], 'unbound fact');
124
+ if (value.reason !== 'initial') throw factError('unbound reason is invalid');
125
+ return { version: 1, state: 'unbound', reason: value.reason };
126
+ }
127
+ if (value.state === 'skipped') {
128
+ exactKeys(value, ['version', 'state', 'reason', 'decidedAt'], 'skipped fact');
129
+ if (value.reason !== 'explicit') throw factError('skipped reason is invalid');
130
+ const decidedAt = text(value.decidedAt, 'skipped.decidedAt');
131
+ if (Number.isNaN(Date.parse(decidedAt))) throw factError('skipped.decidedAt must be a valid timestamp');
132
+ return { version: 1, state: 'skipped', reason: 'explicit', decidedAt };
133
+ }
134
+ if (value.state !== 'bound') throw factError('state is invalid');
135
+ exactKeys(value, ['version', 'state', 'identity', 'binding', 'provenance'], 'bound fact');
136
+ if (!isRecord(value.binding)) throw factError('binding must be an object');
137
+ exactKeys(value.binding, ['status', 'source', 'verifiedAt', 'issueNumber', 'remoteState', 'mergedAt', 'mergeCommitSha'], 'binding');
138
+ if (value.binding.status !== 'verified') throw factError('binding status is invalid');
139
+ const sources: readonly PrDeliveryBindingSource[] = ['created', 'reused', 'explicit-bind', 'external-unique', 'external-explicit'];
140
+ if (!sources.includes(value.binding.source as PrDeliveryBindingSource)) throw factError('binding source is invalid');
141
+ const verifiedAt = text(value.binding.verifiedAt, 'binding.verifiedAt');
142
+ if (Number.isNaN(Date.parse(verifiedAt))) throw factError('binding.verifiedAt must be a valid timestamp');
143
+ const issueNumber = value.binding.issueNumber === null ? null : positiveNumber(value.binding.issueNumber, 'binding.issueNumber');
144
+ if (value.binding.remoteState !== 'open' && value.binding.remoteState !== 'closed') throw factError('binding.remoteState is invalid');
145
+ const mergedAt = optionalTimestamp(value.binding.mergedAt, 'binding.mergedAt');
146
+ const mergeCommitSha = nullableText(value.binding.mergeCommitSha, 'binding.mergeCommitSha');
147
+ if (Boolean(mergedAt) !== Boolean(mergeCommitSha)) throw factError('mergedAt and mergeCommitSha must be paired');
148
+ if (!isRecord(value.provenance)) throw factError('provenance must be an object');
149
+ exactKeys(value.provenance, ['establishedBy'], 'provenance');
150
+ const provenance: readonly PrDeliveryProvenance[] = ['create-post', 'reuse', 'explicit-bind', 'external-unique', 'external-explicit'];
151
+ if (!provenance.includes(value.provenance.establishedBy as PrDeliveryProvenance)) throw factError('provenance.establishedBy is invalid');
152
+ if (value.provenance.establishedBy !== provenanceForSource(value.binding.source as PrDeliveryBindingSource)) {
153
+ throw factError('binding.source and provenance.establishedBy do not match');
154
+ }
155
+ return {
156
+ version: 1,
157
+ state: 'bound',
158
+ identity: parseIdentity(value.identity),
159
+ binding: {
160
+ status: 'verified',
161
+ source: value.binding.source as PrDeliveryBindingSource,
162
+ verifiedAt,
163
+ issueNumber,
164
+ remoteState: value.binding.remoteState,
165
+ mergedAt,
166
+ mergeCommitSha
167
+ },
168
+ provenance: { establishedBy: value.provenance.establishedBy as PrDeliveryProvenance }
169
+ };
170
+ }
171
+
172
+ function decodePrDeliveryFact(value: unknown): PrDeliveryFact {
173
+ if (typeof value === 'string') {
174
+ try { return parseFact(JSON.parse(value)); }
175
+ catch (error) { throw error instanceof Error && error.message.startsWith('PR_DELIVERY_FACT_INVALID:') ? error : factError('value is not valid JSON'); }
176
+ }
177
+ return parseFact(value);
178
+ }
179
+
180
+ function encodePrDeliveryFact(fact: PrDeliveryFact): string {
181
+ return JSON.stringify(parseFact(fact));
182
+ }
183
+
184
+ function readPrDeliveryFact(metadata: Record<string, unknown>): PrDeliveryFactReadResult {
185
+ if (!Object.hasOwn(metadata, PR_DELIVERY_FACT_KEY) || metadata[PR_DELIVERY_FACT_KEY] === '') return { status: 'missing', fact: null };
186
+ try { return { status: 'valid', fact: decodePrDeliveryFact(metadata[PR_DELIVERY_FACT_KEY]) }; }
187
+ catch (error) { return { status: 'invalid', fact: null, error: error instanceof Error ? error : factError(String(error)) }; }
188
+ }
189
+
190
+ function buildUnboundFact(): PrDeliveryFact {
191
+ return { version: 1, state: 'unbound', reason: 'initial' };
192
+ }
193
+
194
+ function buildSkippedFact(decidedAt: string): PrDeliveryFact {
195
+ return parseFact({ version: 1, state: 'skipped', reason: 'explicit', decidedAt });
196
+ }
197
+
198
+ function provenanceForSource(source: PrDeliveryBindingSource): PrDeliveryProvenance {
199
+ return source === 'created' ? 'create-post' : source === 'reused' ? 'reuse' : source;
200
+ }
201
+
202
+ function buildBoundFact(input: {
203
+ identity: PrDeliveryIdentity;
204
+ source: PrDeliveryBindingSource;
205
+ verifiedAt: string;
206
+ issueNumber?: number | null;
207
+ remoteState: 'open' | 'closed';
208
+ mergedAt?: string | null;
209
+ mergeCommitSha?: string | null;
210
+ }): PrDeliveryFact {
211
+ return parseFact({
212
+ version: 1,
213
+ state: 'bound',
214
+ identity: input.identity,
215
+ binding: {
216
+ status: 'verified',
217
+ source: input.source,
218
+ verifiedAt: input.verifiedAt,
219
+ issueNumber: input.issueNumber ?? null,
220
+ remoteState: input.remoteState,
221
+ mergedAt: input.mergedAt ?? null,
222
+ mergeCommitSha: input.mergeCommitSha ?? null
223
+ },
224
+ provenance: { establishedBy: provenanceForSource(input.source) }
225
+ });
226
+ }
227
+
228
+ function factFrontmatterMutation(fact: PrDeliveryFact): { set: { [PR_DELIVERY_FACT_KEY]: string } } {
229
+ return { set: { [PR_DELIVERY_FACT_KEY]: encodePrDeliveryFact(fact) } };
230
+ }
231
+
232
+ function identityFromPullRequest(pullRequest: PrDeliveryIdentity): PrDeliveryIdentity {
233
+ return parseIdentity(pullRequest);
234
+ }
235
+
236
+ export {
237
+ PR_DELIVERY_FACT_KEY,
238
+ buildBoundFact,
239
+ buildSkippedFact,
240
+ buildUnboundFact,
241
+ decodePrDeliveryFact,
242
+ encodePrDeliveryFact,
243
+ factFrontmatterMutation,
244
+ identityFromPullRequest,
245
+ readPrDeliveryFact
246
+ };
247
+ export type {
248
+ CreationOutcome,
249
+ PrDeliveryBindingSource,
250
+ PrDeliveryFact,
251
+ PrDeliveryFactReadResult,
252
+ PrDeliveryIdentity,
253
+ PrDeliveryProvenance
254
+ };
@@ -7,7 +7,7 @@ import { parseArtifactName, validateCompletedArtifact } from './artifact-lifecyc
7
7
  import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
8
8
  import type { LedgerStageStatus, ReviewStage } from './ledger.ts';
9
9
  import { finalizeReviewSummaryContent } from './review-artifacts.ts';
10
- import { inspectDecisionDetailDuplicates, repairDecisionDetailDuplicates } from './decision-details.ts';
10
+ import { inspectDecisionDetailDuplicates } from './decision-details.ts';
11
11
  import { resolveTaskRef } from './resolve-ref.ts';
12
12
  import { validateLifecycleExecution } from './lifecycle-execution.ts';
13
13
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
@@ -214,17 +214,7 @@ function finalizeReviewSummaryUnlocked(
214
214
  );
215
215
  }
216
216
  const stageStatus = summarizeLedgerStage(rows, stage);
217
- const repaired = repairDecisionDetailDuplicates(artifactContent);
218
- if (!repaired.ok) {
219
- return failed(
220
- request,
221
- 'REVIEW_DECISION_DETAIL_INVALID',
222
- `${repaired.code}: ${repaired.message}`,
223
- resolved.taskId,
224
- stageStatus
225
- );
226
- }
227
- const detailInspection = inspectDecisionDetailDuplicates(repaired.content);
217
+ const detailInspection = inspectDecisionDetailDuplicates(artifactContent);
228
218
  if (!detailInspection.ok) {
229
219
  return failed(
230
220
  request,
@@ -235,13 +225,13 @@ function finalizeReviewSummaryUnlocked(
235
225
  );
236
226
  }
237
227
  const transformed = finalizeReviewSummaryContent(
238
- repaired.content,
228
+ artifactContent,
239
229
  stageStatus.unresolvedFindingCounts
240
230
  );
241
231
  if (!transformed.ok) {
242
232
  return failed(request, transformed.code, transformed.message, resolved.taskId, stageStatus);
243
233
  }
244
- if (!repaired.changed && !transformed.changed) {
234
+ if (!transformed.changed) {
245
235
  return {
246
236
  ...failed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, stageStatus),
247
237
  status: 'no-op',
@@ -63,6 +63,16 @@ export function extractReviewBaseline(content: string): string {
63
63
  return match ? match[1]!.trim().replace(/`/g, "") : "";
64
64
  }
65
65
 
66
+ export function extractReviewTargetHead(content: string): string {
67
+ const match = String(content).match(/^[-*]?\s*\*\*(?:审查目标提交|审查目标 HEAD|Review Target Commit|Review Target Head)\*\*[::]\s*(.*?)\s*$/m);
68
+ return match ? match[1]!.trim().replace(/`/g, "") : "";
69
+ }
70
+
71
+ export function extractReviewedHead(content: string): string {
72
+ const match = String(content).match(/^[-*]?\s*\*\*(?:审查已检视提交|已检视提交|Reviewed Head Commit|Reviewed Head)\*\*[::]\s*(.*?)\s*$/m);
73
+ return match ? match[1]!.trim().replace(/`/g, "") : "";
74
+ }
75
+
66
76
  export function extractReviewDiffBase(content: string): string {
67
77
  const match = String(content).match(/^[-*]?\s*\*\*(?:审查差异基线|Reviewed Diff Base)\*\*[::]\s*(.*?)\s*$/m);
68
78
  return match ? match[1]!.trim().replace(/`/g, "") : "";