@fitlab-ai/agent-infra 0.9.12 → 0.9.13

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 (239) hide show
  1. package/README.md +11 -11
  2. package/README.zh-CN.md +11 -11
  3. package/dist/bin/internal-cli.js +24 -19
  4. package/dist/lib/agent-clients/next-steps.js +6 -1
  5. package/dist/lib/decide.js +3 -3
  6. package/dist/lib/internal/platform-checks.js +3 -3
  7. package/dist/lib/internal/platform-comment.js +9 -9
  8. package/dist/lib/internal/platform-context.js +2 -2
  9. package/dist/lib/internal/platform-issue.js +8 -9
  10. package/dist/lib/internal/platform-pr-review.js +10 -10
  11. package/dist/lib/internal/platform-pr.js +63 -24
  12. package/dist/lib/internal/platform-release-notes.js +3 -3
  13. package/dist/lib/internal/pr-review-grade.js +11 -9
  14. package/dist/lib/internal/release-workflow.js +4 -4
  15. package/dist/lib/internal/sandbox-control.js +30 -4
  16. package/dist/lib/internal/task-activity.js +3 -3
  17. package/dist/lib/internal/task-artifact.js +76 -24
  18. package/dist/lib/internal/task-create.js +2 -2
  19. package/dist/lib/internal/task-event.js +6 -3
  20. package/dist/lib/internal/task-finalization.js +2 -2
  21. package/dist/lib/internal/task-invalidation.js +60 -0
  22. package/dist/lib/internal/task-ledger.js +9 -6
  23. package/dist/lib/internal/task-lifecycle.js +2 -2
  24. package/dist/lib/internal/task-override.js +4 -4
  25. package/dist/lib/internal/task-review.js +3 -3
  26. package/dist/lib/internal/task-verify.js +6 -6
  27. package/dist/lib/internal/task-warning.js +3 -3
  28. package/dist/lib/platform/adapters.js +1 -3
  29. package/dist/lib/platform/change-request-git-evidence.js +27 -7
  30. package/dist/lib/platform/completion-backfill.js +6 -5
  31. package/dist/lib/platform/context.js +70 -130
  32. package/dist/lib/platform/github-client.js +1 -1
  33. package/dist/lib/platform/github-provider.js +903 -0
  34. package/dist/lib/platform/in-label-sync.js +143 -0
  35. package/dist/lib/platform/issue-comments.js +94 -41
  36. package/dist/lib/platform/issues.js +335 -189
  37. package/dist/lib/platform/metadata-labels.js +6 -2
  38. package/dist/lib/platform/none-provider.js +28 -0
  39. package/dist/lib/platform/pr-change-report.js +551 -0
  40. package/dist/lib/platform/pr-checks.js +119 -104
  41. package/dist/lib/platform/pr-review.js +88 -72
  42. package/dist/lib/platform/pr-summary.js +520 -50
  43. package/dist/lib/platform/provider-bridge.js +45 -0
  44. package/dist/lib/platform/provider-contract.d.ts +487 -0
  45. package/dist/lib/platform/provider-contract.js +95 -0
  46. package/dist/lib/platform/provider-loader.js +251 -0
  47. package/dist/lib/platform/provider-validation.js +504 -0
  48. package/dist/lib/platform/pull-request-metadata.js +8 -6
  49. package/dist/lib/platform/pull-requests.js +783 -537
  50. package/dist/lib/platform/release-notes.js +78 -55
  51. package/dist/lib/platform/releases.js +79 -50
  52. package/dist/lib/platform/resource-identity.d.ts +26 -0
  53. package/dist/lib/platform/resource-identity.js +92 -0
  54. package/dist/lib/platform/task-identities.js +31 -0
  55. package/dist/lib/platform/verification-required.js +11 -12
  56. package/dist/lib/platform/verification-sync.js +112 -238
  57. package/dist/lib/pr-review/evidence-grading.js +19 -6
  58. package/dist/lib/run/index.js +63 -23
  59. package/dist/lib/sandbox/control/client.js +96 -15
  60. package/dist/lib/sandbox/control/executor.js +3 -3
  61. package/dist/lib/sandbox/control/protocol.js +77 -0
  62. package/dist/lib/sandbox/control/server.js +398 -25
  63. package/dist/lib/sandbox/control/state.js +204 -1
  64. package/dist/lib/server/protocol.js +38 -1
  65. package/dist/lib/task/activity-log.js +8 -1
  66. package/dist/lib/task/artifact-lifecycle.js +33 -5
  67. package/dist/lib/task/capabilities.js +239 -0
  68. package/dist/lib/task/commands/status.js +32 -0
  69. package/dist/lib/task/commit-operation.js +12 -1
  70. package/dist/lib/task/control-authority.js +8 -5
  71. package/dist/lib/task/create-service.js +8 -8
  72. package/dist/lib/task/current-contract.js +5 -1
  73. package/dist/lib/task/decision-details.js +1 -1
  74. package/dist/lib/task/events.js +297 -17
  75. package/dist/lib/task/finalization.js +37 -13
  76. package/dist/lib/task/human-override.js +58 -36
  77. package/dist/lib/task/invalidation-command.js +86 -0
  78. package/dist/lib/task/invalidation.js +260 -0
  79. package/dist/lib/task/ledger-intents.js +37 -0
  80. package/dist/lib/task/local-artifact-finalization.js +374 -0
  81. package/dist/lib/task/orchestration.js +49 -93
  82. package/dist/lib/task/pr-delivery-fact.js +102 -101
  83. package/dist/lib/task/review-fingerprint.js +24 -2
  84. package/dist/lib/task/rework-intent.js +138 -0
  85. package/dist/lib/task/skill-scope.js +23 -0
  86. package/dist/lib/task/task-execution-lock.js +27 -17
  87. package/dist/lib/task/verification-engine.js +43 -16
  88. package/dist/lib/task/verification.js +3 -3
  89. package/dist/lib/task/workflow-warning-intents.js +1 -1
  90. package/dist/lib/task/write.js +11 -0
  91. package/dist/lib/version.js +11 -1
  92. package/dist/lifecycle-build-manifest.json +3 -3
  93. package/lib/agent-clients/next-steps.ts +6 -1
  94. package/lib/decide.ts +3 -3
  95. package/lib/init.ts +1 -1
  96. package/lib/internal/platform-checks.ts +3 -3
  97. package/lib/internal/platform-comment.ts +8 -8
  98. package/lib/internal/platform-context.ts +2 -2
  99. package/lib/internal/platform-issue.ts +7 -8
  100. package/lib/internal/platform-pr-review.ts +9 -9
  101. package/lib/internal/platform-pr.ts +47 -22
  102. package/lib/internal/platform-release-notes.ts +3 -3
  103. package/lib/internal/pr-review-grade.ts +10 -8
  104. package/lib/internal/release-workflow.ts +4 -4
  105. package/lib/internal/sandbox-control.ts +27 -3
  106. package/lib/internal/task-activity.ts +3 -3
  107. package/lib/internal/task-artifact.ts +65 -22
  108. package/lib/internal/task-create.ts +2 -2
  109. package/lib/internal/task-event.ts +6 -3
  110. package/lib/internal/task-finalization.ts +3 -3
  111. package/lib/internal/task-invalidation.ts +39 -0
  112. package/lib/internal/task-ledger.ts +11 -8
  113. package/lib/internal/task-lifecycle.ts +2 -2
  114. package/lib/internal/task-override.ts +6 -6
  115. package/lib/internal/task-review.ts +3 -3
  116. package/lib/internal/task-verify.ts +6 -6
  117. package/lib/internal/task-warning.ts +3 -3
  118. package/lib/platform/adapters.ts +3 -2
  119. package/lib/platform/change-request-git-evidence.ts +36 -9
  120. package/lib/platform/completion-backfill.ts +9 -8
  121. package/lib/platform/context.ts +97 -134
  122. package/lib/platform/github-client.ts +2 -2
  123. package/lib/platform/github-provider.ts +910 -0
  124. package/lib/platform/in-label-sync.ts +208 -0
  125. package/lib/platform/issue-comments.ts +97 -47
  126. package/lib/platform/issues.ts +344 -174
  127. package/lib/platform/metadata-labels.ts +5 -2
  128. package/lib/platform/none-provider.ts +35 -0
  129. package/lib/platform/pr-change-report.ts +645 -0
  130. package/lib/platform/pr-checks.ts +126 -106
  131. package/lib/platform/pr-review.ts +88 -85
  132. package/lib/platform/pr-summary.ts +560 -53
  133. package/lib/platform/provider-bridge.ts +82 -0
  134. package/lib/platform/provider-contract.ts +536 -0
  135. package/lib/platform/provider-loader.ts +307 -0
  136. package/lib/platform/provider-validation.ts +529 -0
  137. package/lib/platform/pull-request-metadata.ts +10 -7
  138. package/lib/platform/pull-requests.ts +826 -543
  139. package/lib/platform/release-notes.ts +76 -59
  140. package/lib/platform/releases.ts +77 -43
  141. package/lib/platform/resource-identity.ts +121 -0
  142. package/lib/platform/task-identities.ts +40 -0
  143. package/lib/platform/types.ts +4 -2
  144. package/lib/platform/verification-required.ts +11 -13
  145. package/lib/platform/verification-sync.ts +112 -292
  146. package/lib/pr-review/evidence-grading.ts +25 -8
  147. package/lib/run/index.ts +57 -22
  148. package/lib/sandbox/control/client.ts +105 -15
  149. package/lib/sandbox/control/executor.ts +4 -4
  150. package/lib/sandbox/control/protocol.ts +113 -0
  151. package/lib/sandbox/control/server.ts +431 -23
  152. package/lib/sandbox/control/state.ts +251 -1
  153. package/lib/server/protocol.ts +36 -1
  154. package/lib/task/activity-log.ts +8 -0
  155. package/lib/task/artifact-lifecycle.ts +32 -6
  156. package/lib/task/capabilities.ts +262 -0
  157. package/lib/task/commands/status.ts +38 -0
  158. package/lib/task/commit-operation.ts +12 -1
  159. package/lib/task/control-authority.ts +11 -8
  160. package/lib/task/create-service.ts +8 -8
  161. package/lib/task/current-contract.ts +6 -1
  162. package/lib/task/decision-details.ts +6 -2
  163. package/lib/task/events.ts +296 -16
  164. package/lib/task/finalization.ts +50 -14
  165. package/lib/task/human-override.ts +70 -46
  166. package/lib/task/invalidation-command.ts +86 -0
  167. package/lib/task/invalidation.ts +332 -0
  168. package/lib/task/ledger-intents.ts +35 -1
  169. package/lib/task/local-artifact-finalization.ts +563 -0
  170. package/lib/task/orchestration.ts +52 -85
  171. package/lib/task/pr-delivery-fact.ts +131 -148
  172. package/lib/task/review-fingerprint.ts +23 -2
  173. package/lib/task/rework-intent.ts +165 -0
  174. package/lib/task/skill-scope.ts +24 -0
  175. package/lib/task/task-execution-lock.ts +23 -14
  176. package/lib/task/verification-engine.ts +52 -17
  177. package/lib/task/verification.ts +4 -4
  178. package/lib/task/workflow-warning-intents.ts +1 -1
  179. package/lib/task/write.ts +19 -0
  180. package/lib/update.ts +1 -1
  181. package/lib/version.ts +15 -1
  182. package/package.json +7 -1
  183. package/templates/.agents/rules/README.en.md +1 -1
  184. package/templates/.agents/rules/README.zh-CN.md +1 -1
  185. package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
  186. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
  187. package/templates/.agents/rules/issue-sync.en.md +2 -0
  188. package/templates/.agents/rules/issue-sync.zh-CN.md +2 -0
  189. package/templates/.agents/rules/local-artifact-repair.en.md +9 -2
  190. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +9 -2
  191. package/templates/.agents/rules/pr-sync.en.md +10 -4
  192. package/templates/.agents/rules/pr-sync.zh-CN.md +17 -4
  193. package/templates/.agents/rules/task-short-id.en.md +1 -4
  194. package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
  195. package/templates/.agents/skills/analyze-task/SKILL.en.md +13 -3
  196. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -3
  197. package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
  198. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
  199. package/templates/.agents/skills/code-task/SKILL.en.md +27 -8
  200. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +29 -10
  201. package/templates/.agents/skills/code-task/config/verify.en.json +1 -0
  202. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -0
  203. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +2 -0
  204. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
  205. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +3 -3
  206. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +3 -3
  207. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -3
  208. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -3
  209. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +4 -3
  210. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +4 -3
  211. package/templates/.agents/skills/complete-task/SKILL.en.md +21 -3
  212. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +20 -3
  213. package/templates/.agents/skills/create-pr/SKILL.en.md +7 -6
  214. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -7
  215. package/templates/.agents/skills/create-pr/config/verify.json +1 -0
  216. package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
  217. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
  218. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
  219. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
  220. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  221. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  222. package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
  223. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -3
  224. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -3
  225. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
  226. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
  227. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  228. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  229. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
  230. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
  231. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -2
  232. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -2
  233. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  234. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  235. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +6 -0
  236. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
  237. package/templates/.agents/templates/task.en.md +18 -1
  238. package/templates/.agents/templates/task.zh-CN.md +18 -1
  239. package/templates/.github/workflows/pr-label.yml +7 -26
@@ -4,8 +4,7 @@ import { randomUUID } from 'node:crypto';
4
4
 
5
5
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
6
6
  import { validateCurrentTaskContract } from './current-contract.ts';
7
- import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.ts';
8
- import { inspectArtifactDirectory } from './artifact-lifecycle.ts';
7
+ import { parseReviewSummary } from './review-artifacts.ts';
9
8
  import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
10
9
  import type { LedgerDocument } from './ledger.ts';
11
10
  import { resolveTaskRef } from './resolve-ref.ts';
@@ -53,6 +52,11 @@ import { hasActiveCodexLifecycleEvidence } from '../agent-clients/adapters/codex
53
52
  import type { CodexCapabilityProvenanceDetail } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
54
53
  import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead, extractReviewedHead } from './review-fingerprint.ts';
55
54
  import { resolveDeliveryTarget, resolveDiffBase, resolveTargetHead } from './delivery-target.ts';
55
+ import { buildLifecycleFacts, canStart, recommendNext } from './capabilities.ts';
56
+ import { hasOpenLifecycleExecution } from './activity-log.ts';
57
+ import { invalidationBlocks } from './invalidation.ts';
58
+ import type { InvalidationDocument } from './invalidation.ts';
59
+ import type { LifecycleAction, LifecycleFacts } from './capabilities.ts';
56
60
 
57
61
  type OrchestrationStatus = 'running' | 'paused' | 'completed';
58
62
  type ModelPolicySource = Readonly<{
@@ -108,7 +112,7 @@ type OrchestrationRun = Readonly<{
108
112
  updatedAt: string;
109
113
  }>;
110
114
  type OrchestrationNext = Readonly<{
111
- action: 'analyze-task' | 'review-analysis' | 'plan-task' | 'review-plan' | 'code-task' | 'review-code' | 'commit';
115
+ action: 'analyze-task' | 'review-analysis' | 'plan-task' | 'review-plan' | 'code-task' | 'review-code';
112
116
  role: DelegationRole;
113
117
  stage: DelegationStage;
114
118
  round: number;
@@ -561,78 +565,19 @@ function validateSavedCurrentDiffBase(
561
565
  return null;
562
566
  }
563
567
 
564
- function hasReviewAfterArtifact(taskDir: string, family: string, reviewFamily: string): boolean {
565
- const artifact = inspectArtifactDirectory(taskDir, family as 'analysis' | 'plan' | 'code');
566
- const review = inspectArtifactDirectory(taskDir, reviewFamily as 'review-analysis' | 'review-plan' | 'review-code');
567
- return artifact.status === 'ready'
568
- && review.status === 'ready'
569
- && artifact.latest !== null
570
- && review.reviewedInput?.name === artifact.latest.name;
571
- }
572
-
573
- function latestReviewApproved(taskDir: string, family: 'review-analysis' | 'review-plan' | 'review-code'): { approved: boolean; error: string | null } {
574
- const round = highestRound(taskDir, family);
575
- if (round === 0) return { approved: false, error: null };
576
- let content: string;
577
- try { content = fs.readFileSync(path.join(taskDir, artifactName(family, round)), 'utf8'); }
578
- catch (error) { return { approved: false, error: String(error) }; }
579
- const parsed = parseReviewSummary(content);
580
- if (!parsed.ok) return { approved: false, error: `${parsed.code}: ${parsed.message}` };
581
- const verdict = resolveCanonicalVerdict(parsed.summary);
582
- if (!verdict.ok) return { approved: false, error: `${verdict.code}: ${verdict.message}` };
583
- return { approved: verdict.verdict === 'Approved', error: null };
584
- }
585
-
586
- function routeFromFacts(taskDir: string): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort'> | { error: string } | null {
587
- const analysisRound = highestRound(taskDir, 'analysis');
588
- const analysisReviewRound = highestRound(taskDir, 'review-analysis');
589
- const planRound = highestRound(taskDir, 'plan');
590
- const planReviewRound = highestRound(taskDir, 'review-plan');
591
- const codeRound = highestRound(taskDir, 'code');
592
- const codeReviewRound = highestRound(taskDir, 'review-code');
593
- if (analysisRound === 0) {
594
- const round = analysisRound + 1;
595
- return { action: 'analyze-task', role: 'executor', stage: 'analysis', round, artifact: artifactName('analysis', round) };
596
- }
597
- if (!hasReviewAfterArtifact(taskDir, 'analysis', 'review-analysis')) {
598
- const round = analysisReviewRound + 1;
599
- return { action: 'review-analysis', role: 'reviewer', stage: 'review-analysis', round, artifact: artifactName('review-analysis', round) };
600
- }
601
- const analysisVerdict = latestReviewApproved(taskDir, 'review-analysis');
602
- if (analysisVerdict.error) return { error: analysisVerdict.error };
603
- if (!analysisVerdict.approved) {
604
- const round = analysisRound + 1;
605
- return { action: 'analyze-task', role: 'executor', stage: 'analysis', round, artifact: artifactName('analysis', round) };
606
- }
607
- if (planRound === 0) {
608
- const round = planRound + 1;
609
- return { action: 'plan-task', role: 'executor', stage: 'plan', round, artifact: artifactName('plan', round) };
610
- }
611
- if (!hasReviewAfterArtifact(taskDir, 'plan', 'review-plan')) {
612
- const round = planReviewRound + 1;
613
- return { action: 'review-plan', role: 'reviewer', stage: 'review-plan', round, artifact: artifactName('review-plan', round) };
614
- }
615
- const planVerdict = latestReviewApproved(taskDir, 'review-plan');
616
- if (planVerdict.error) return { error: planVerdict.error };
617
- if (!planVerdict.approved) {
618
- const round = planRound + 1;
619
- return { action: 'plan-task', role: 'executor', stage: 'plan', round, artifact: artifactName('plan', round) };
620
- }
621
- if (codeRound === 0) {
622
- const round = codeRound + 1;
623
- return { action: 'code-task', role: 'executor', stage: 'code', round, artifact: artifactName('code', round) };
624
- }
625
- if (!hasReviewAfterArtifact(taskDir, 'code', 'review-code')) {
626
- const round = codeReviewRound + 1;
627
- return { action: 'review-code', role: 'reviewer', stage: 'review-code', round, artifact: artifactName('review-code', round) };
628
- }
629
- const codeVerdict = latestReviewApproved(taskDir, 'review-code');
630
- if (codeVerdict.error) return { error: codeVerdict.error };
631
- if (!codeVerdict.approved) {
632
- const round = codeRound + 1;
633
- return { action: 'code-task', role: 'executor', stage: 'code', round, artifact: artifactName('code', round) };
634
- }
635
- return { action: 'commit', role: 'executor', stage: 'commit', round: 1, artifact: 'commit' };
568
+ function routeFromFacts(facts: LifecycleFacts): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort'> | { completion: true } | null {
569
+ const recommendation = recommendNext(facts);
570
+ if (!recommendation.action) return { completion: true };
571
+ const action = recommendation.action;
572
+ if (action === 'manual-validation' || action === 'validation-run') return null;
573
+ const family = action === 'analysis' ? 'analysis' : action;
574
+ const round = Math.max(0, ...[...(facts.artifacts[family] ?? []), ...(facts.staleArtifacts?.[family] ?? [])].map((name) => {
575
+ const match = /-r(\d+)\.md$/.exec(name);
576
+ return match ? Number(match[1]) : 1;
577
+ })) + 1;
578
+ const command = action === 'analysis' ? 'analyze-task' : action === 'plan' ? 'plan-task' : action === 'code' ? 'code-task' : action;
579
+ const role = action.startsWith('review-') ? 'reviewer' : 'executor';
580
+ return { action: command, role, stage: action, round, artifact: artifactName(family, round) };
636
581
  }
637
582
 
638
583
  function routeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
@@ -652,11 +597,13 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
652
597
  }
653
598
  let metadata: ReturnType<typeof parseTypedTaskFrontmatter>;
654
599
  let currentLedger: LedgerDocument | null = null;
600
+ let currentInvalidation: InvalidationDocument = { operations: [], targets: [] };
655
601
  if (resolved.state === 'active') {
656
602
  const contract = validateCurrentTaskContract(content);
657
603
  if (!contract.ok) return failed('ORCHESTRATION_CURRENT_CONTRACT_INVALID', contract.message, resolved.taskId);
658
604
  metadata = contract.metadata;
659
605
  currentLedger = contract.ledger;
606
+ currentInvalidation = contract.invalidation;
660
607
  } else {
661
608
  try {
662
609
  metadata = parseTypedTaskFrontmatter(content);
@@ -664,17 +611,25 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
664
611
  return failed('ORCHESTRATION_TASK_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
665
612
  }
666
613
  }
667
- const routed = routeFromFacts(resolved.taskDir);
668
- if (routed && 'error' in routed) return failed('ORCHESTRATION_REVIEW_INVALID', routed.error, resolved.taskId);
614
+ if (invalidationBlocks(currentInvalidation)) {
615
+ return failed('ORCHESTRATION_INVALIDATION_INCOMPLETE', 'artifact invalidation is incomplete; reconcile task.md before routing downstream work', resolved.taskId);
616
+ }
617
+ let run: OrchestrationRun | null;
618
+ try {
619
+ run = readRun(resolved.taskDir);
620
+ } catch (error) {
621
+ return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
622
+ }
623
+ const facts = buildLifecycleFacts(
624
+ resolved.taskDir,
625
+ content,
626
+ resolved.state,
627
+ hasOpenLifecycleExecution(content) || Boolean(run?.pendingDelegation)
628
+ );
629
+ if (!facts.ok) return failed('ORCHESTRATION_CAPABILITY_FACTS_INVALID', facts.message, resolved.taskId);
630
+ const routed = routeFromFacts(facts.facts);
669
631
  if (!routed) return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
670
- const run = readRun(resolved.taskDir);
671
- const policy = run ? rolePolicy(run, routed.role) : null;
672
- const next: OrchestrationNext = {
673
- ...routed,
674
- requestedModel: policy?.model ?? null,
675
- requestedReasoningEffort: policy?.reasoningEffort ?? null
676
- };
677
- if (next.stage === 'commit') {
632
+ if ('completion' in routed) {
678
633
  const reviewRound = highestRound(resolved.taskDir, 'review-code');
679
634
  const review = parseReviewSummary(fs.readFileSync(
680
635
  path.join(resolved.taskDir, artifactName('review-code', reviewRound)),
@@ -756,6 +711,18 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
756
711
  saveRun(resolved.taskDir, completed);
757
712
  return { status: 'completed', changed: true, taskId: resolved.taskId, run: completed, next: null, error: null };
758
713
  }
714
+ const action = routed.stage as LifecycleAction;
715
+ const capability = canStart(action, facts.facts, {
716
+ initiator: 'orchestrator', requestId: `orchestration:${resolved.taskId}:${routed.stage}:${routed.round}`,
717
+ requestedAction: action, reasonCode: 'user-request', explicitRequest: true
718
+ });
719
+ if (!capability.allowed) return failed('ORCHESTRATION_CAPABILITY_DENIED', `${capability.reasonCode}: ${capability.evidence.join(', ')}`, resolved.taskId);
720
+ const policy = run ? rolePolicy(run, routed.role) : null;
721
+ const next: OrchestrationNext = {
722
+ ...routed,
723
+ requestedModel: policy?.model ?? null,
724
+ requestedReasoningEffort: policy?.reasoningEffort ?? null
725
+ };
759
726
  return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
760
727
  }
761
728
 
@@ -1,6 +1,23 @@
1
+ import type { ResourceIdentity } from '../platform/resource-identity.ts';
2
+ import { isLegacyCompatibilityEnabled, legacyCompatibilityError, VERSION } from '../version.ts';
3
+ import {
4
+ isResourceIdentity,
5
+ parseResourceIdentity,
6
+ resourceIdentityNumber,
7
+ serializeResourceIdentity
8
+ } from '../platform/resource-identity.ts';
9
+
1
10
  const PR_DELIVERY_FACT_KEY = 'pr_delivery_fact';
2
11
 
3
12
  type PrDeliveryIdentity = {
13
+ resource: ResourceIdentity;
14
+ repository: string;
15
+ url: string;
16
+ head: { repository: string; ref: string; sha: string };
17
+ base: { repository: string; ref: string; sha: string };
18
+ };
19
+
20
+ type LegacyPrDeliveryIdentity = {
4
21
  repository: string;
5
22
  number: number;
6
23
  nodeId: string;
@@ -8,33 +25,30 @@ type PrDeliveryIdentity = {
8
25
  head: { repository: string; ref: string; sha: string };
9
26
  base: { repository: string; ref: string; sha: string };
10
27
  };
28
+ type CurrentPrDeliveryIdentityInput = {
29
+ resource: ResourceIdentity;
30
+ repository: string;
31
+ url: string;
32
+ head: { repository: string; ref: string; sha: string };
33
+ base: { repository: string; ref: string; sha: string };
34
+ };
35
+ type PrDeliveryIdentityInput = CurrentPrDeliveryIdentityInput;
11
36
 
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';
37
+ type PrDeliveryBindingSource = 'created' | 'reused' | 'explicit-bind' | 'external-unique' | 'external-explicit';
38
+ type PrDeliveryProvenance = 'create-post' | 'reuse' | 'explicit-bind' | 'external-unique' | 'external-explicit';
25
39
 
26
40
  type PrDeliveryFact =
27
- | { version: 1; state: 'unbound'; reason: 'initial' }
28
- | { version: 1; state: 'skipped'; reason: 'explicit'; decidedAt: string }
41
+ | { version: 2; state: 'unbound'; reason: 'initial' }
42
+ | { version: 2; state: 'skipped'; reason: 'explicit'; decidedAt: string }
29
43
  | {
30
- version: 1;
44
+ version: 2;
31
45
  state: 'bound';
32
46
  identity: PrDeliveryIdentity;
33
47
  binding: {
34
48
  status: 'verified';
35
49
  source: PrDeliveryBindingSource;
36
50
  verifiedAt: string;
37
- issueNumber: number | null;
51
+ issueIdentity: ResourceIdentity | null;
38
52
  remoteState: 'open' | 'closed';
39
53
  mergedAt: string | null;
40
54
  mergeCommitSha: string | null;
@@ -50,205 +64,174 @@ type CreationOutcome =
50
64
  | { kind: 'no-op'; createdByCurrentOperation: false }
51
65
  | { kind: 'unknown'; errorCode: 'PR_CREATE_OUTCOME_UNKNOWN' };
52
66
 
67
+ type PrDeliveryFactError = Error & { code: string };
53
68
  type PrDeliveryFactReadResult =
54
69
  | { status: 'valid'; fact: PrDeliveryFact }
55
70
  | { status: 'missing'; fact: null }
56
- | { status: 'invalid'; fact: null; error: Error };
71
+ | { status: 'invalid'; fact: null; error: PrDeliveryFactError };
57
72
 
58
73
  function factError(message: string): Error & { code: string } {
59
74
  return Object.assign(new Error(`PR_DELIVERY_FACT_INVALID: ${message}`), { code: 'PR_DELIVERY_FACT_INVALID' });
60
75
  }
61
-
62
- function isRecord(value: unknown): value is Record<string, unknown> {
63
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
76
+ function asFactError(error: unknown): PrDeliveryFactError {
77
+ if (error instanceof Error && typeof (error as { code?: unknown }).code === 'string') return error as PrDeliveryFactError;
78
+ return factError(error instanceof Error ? error.message : String(error));
64
79
  }
65
-
80
+ function isRecord(value: unknown): value is Record<string, unknown> { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); }
66
81
  function exactKeys(value: Record<string, unknown>, keys: readonly string[], label: string): void {
67
82
  const actual = Object.keys(value).sort();
68
83
  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
- }
84
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) throw factError(`${label} has an invalid field set`);
72
85
  }
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`);
86
+ function text(value: unknown, label: string, allowEmpty = false): string {
87
+ if (typeof value !== 'string' || (!allowEmpty && !value) || value.trim() !== value) throw factError(`${label} must be a string`);
76
88
  return value;
77
89
  }
78
-
79
- function nullableText(value: unknown, label: string): string | null {
80
- return value === null ? null : text(value, label);
90
+ function nullableText(value: unknown, label: string): string | null { return value === null ? null : text(value, label); }
91
+ function timestamp(value: unknown, label: string): string {
92
+ const result = text(value, label);
93
+ if (Number.isNaN(Date.parse(result))) throw factError(`${label} must be a valid timestamp`);
94
+ return new Date(result).toISOString();
81
95
  }
82
-
83
96
  function positiveNumber(value: unknown, label: string): number {
84
97
  if (!Number.isSafeInteger(value) || (value as number) <= 0) throw factError(`${label} must be a positive integer`);
85
98
  return value as number;
86
99
  }
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;
100
+ function parseRef(value: unknown, label: string): { repository: string; ref: string; sha: string } {
101
+ if (!isRecord(value)) throw factError(`${label} must be an object`);
102
+ exactKeys(value, ['repository', 'ref', 'sha'], label);
103
+ return { repository: text(value.repository, `${label}.repository`), ref: text(value.ref, `${label}.ref`), sha: text(value.sha, `${label}.sha`) };
92
104
  }
93
105
 
94
- function parseIdentity(value: unknown): PrDeliveryIdentity {
106
+ function canonicalIdentity(value: unknown): PrDeliveryIdentity {
95
107
  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');
108
+ exactKeys(value, ['resource', 'repository', 'url', 'head', 'base'], 'identity');
99
109
  return {
110
+ resource: parseResourceIdentity(value.resource, 'identity.resource'),
100
111
  repository: text(value.repository, 'identity.repository'),
101
- number: positiveNumber(value.number, 'identity.number'),
102
- nodeId: text(value.nodeId, 'identity.nodeId'),
103
112
  url: text(value.url, 'identity.url'),
104
- head,
105
- base
113
+ head: parseRef(value.head, 'identity.head'),
114
+ base: parseRef(value.base, 'identity.base')
106
115
  };
107
116
  }
108
117
 
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);
118
+ function decodeLegacyIdentity(value: unknown): PrDeliveryIdentity {
119
+ if (!isRecord(value)) throw factError('legacy identity must be an object');
120
+ // TODO(compat): Remove the v1 identity decoder before the first stable v1.0.0 release.
121
+ exactKeys(value, ['repository', 'number', 'nodeId', 'url', 'head', 'base'], 'legacy identity');
122
+ const legacy = {
123
+ number: positiveNumber(value.number, 'legacy identity.number'),
124
+ repository: text(value.repository, 'identity.repository'),
125
+ nodeId: text(value.nodeId, 'legacy identity.nodeId'),
126
+ url: text(value.url, 'identity.url'),
127
+ head: parseRef(value.head, 'identity.head'),
128
+ base: parseRef(value.base, 'identity.base')
129
+ } satisfies LegacyPrDeliveryIdentity;
112
130
  return {
113
- repository: text(value.repository, `${label}.repository`),
114
- ref: text(value.ref, `${label}.ref`),
115
- sha: text(value.sha, `${label}.sha`)
131
+ resource: { kind: 'number', value: legacy.number },
132
+ repository: legacy.repository,
133
+ url: legacy.url,
134
+ head: legacy.head,
135
+ base: legacy.base
116
136
  };
117
137
  }
118
138
 
119
- function parseFact(value: unknown): PrDeliveryFact {
139
+ function provenanceForSource(source: PrDeliveryBindingSource): PrDeliveryProvenance { return source === 'created' ? 'create-post' : source === 'reused' ? 'reuse' : source; }
140
+
141
+ function parseFact(value: unknown, runtimeVersion = VERSION): PrDeliveryFact {
120
142
  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');
143
+ if ((value.version !== 1 && value.version !== 2) || typeof value.state !== 'string') throw factError('version or state is invalid');
144
+ if (value.version === 1 && !isLegacyCompatibilityEnabled(runtimeVersion)) {
145
+ throw legacyCompatibilityError('v1 PR delivery fact', runtimeVersion);
146
+ }
122
147
  if (value.state === 'unbound') {
123
148
  exactKeys(value, ['version', 'state', 'reason'], 'unbound fact');
124
149
  if (value.reason !== 'initial') throw factError('unbound reason is invalid');
125
- return { version: 1, state: 'unbound', reason: value.reason };
150
+ return { version: 2, state: 'unbound', reason: 'initial' };
126
151
  }
127
152
  if (value.state === 'skipped') {
128
153
  exactKeys(value, ['version', 'state', 'reason', 'decidedAt'], 'skipped fact');
129
154
  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 };
155
+ return { version: 2, state: 'skipped', reason: 'explicit', decidedAt: timestamp(value.decidedAt, 'skipped.decidedAt') };
133
156
  }
134
157
  if (value.state !== 'bound') throw factError('state is invalid');
158
+ if (value.version === 1) {
159
+ // TODO(compat): Remove this v1-to-v2 shape conversion before the first stable v1.0.0 release.
160
+ exactKeys(value, ['version', 'state', 'identity', 'binding', 'provenance'], 'legacy bound fact');
161
+ if (!isRecord(value.binding) || !isRecord(value.provenance)) throw factError('legacy binding or provenance is invalid');
162
+ exactKeys(value.binding, ['status', 'source', 'verifiedAt', 'issueNumber', 'remoteState', 'mergedAt', 'mergeCommitSha'], 'legacy binding');
163
+ const issueNumber = value.binding.issueNumber === null ? null : positiveNumber(value.binding.issueNumber, 'legacy binding.issueNumber');
164
+ return parseFact({
165
+ version: 2,
166
+ state: 'bound',
167
+ identity: decodeLegacyIdentity(value.identity),
168
+ binding: {
169
+ status: 'verified', source: value.binding.source, verifiedAt: timestamp(value.binding.verifiedAt, 'binding.verifiedAt'),
170
+ issueIdentity: issueNumber === null ? null : { kind: 'number', value: issueNumber }, remoteState: value.binding.remoteState,
171
+ mergedAt: value.binding.mergedAt === null ? null : timestamp(value.binding.mergedAt, 'binding.mergedAt'), mergeCommitSha: nullableText(value.binding.mergeCommitSha, 'binding.mergeCommitSha')
172
+ },
173
+ provenance: value.provenance
174
+ }, runtimeVersion);
175
+ }
135
176
  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');
177
+ if (!isRecord(value.binding) || !isRecord(value.provenance)) throw factError('binding or provenance is invalid');
178
+ exactKeys(value.binding, ['status', 'source', 'verifiedAt', 'issueIdentity', 'remoteState', 'mergedAt', 'mergeCommitSha'], 'binding');
138
179
  if (value.binding.status !== 'verified') throw factError('binding status is invalid');
139
180
  const sources: readonly PrDeliveryBindingSource[] = ['created', 'reused', 'explicit-bind', 'external-unique', 'external-explicit'];
140
181
  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');
182
+ const issueIdentity = value.binding.issueIdentity === null ? null : parseResourceIdentity(value.binding.issueIdentity, 'binding.issueIdentity');
144
183
  if (value.binding.remoteState !== 'open' && value.binding.remoteState !== 'closed') throw factError('binding.remoteState is invalid');
145
- const mergedAt = optionalTimestamp(value.binding.mergedAt, 'binding.mergedAt');
184
+ const mergedAt = value.binding.mergedAt === null ? null : timestamp(value.binding.mergedAt, 'binding.mergedAt');
146
185
  const mergeCommitSha = nullableText(value.binding.mergeCommitSha, 'binding.mergeCommitSha');
147
186
  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
187
  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
- }
188
+ const provenances: readonly PrDeliveryProvenance[] = ['create-post', 'reuse', 'explicit-bind', 'external-unique', 'external-explicit'];
189
+ if (!provenances.includes(value.provenance.establishedBy as PrDeliveryProvenance)) throw factError('provenance.establishedBy is invalid');
190
+ if (value.provenance.establishedBy !== provenanceForSource(value.binding.source as PrDeliveryBindingSource)) throw factError('binding.source and provenance.establishedBy do not match');
155
191
  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
- },
192
+ version: 2, state: 'bound', identity: canonicalIdentity(value.identity),
193
+ binding: { status: 'verified', source: value.binding.source as PrDeliveryBindingSource, verifiedAt: timestamp(value.binding.verifiedAt, 'binding.verifiedAt'), issueIdentity, remoteState: value.binding.remoteState, mergedAt, mergeCommitSha },
168
194
  provenance: { establishedBy: value.provenance.establishedBy as PrDeliveryProvenance }
169
195
  };
170
196
  }
171
197
 
172
- function decodePrDeliveryFact(value: unknown): PrDeliveryFact {
198
+ function decodePrDeliveryFact(value: unknown, runtimeVersion = VERSION): PrDeliveryFact {
173
199
  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'); }
200
+ let decoded: unknown;
201
+ try { decoded = JSON.parse(value); }
202
+ catch { throw factError('value is not valid JSON'); }
203
+ return parseFact(decoded, runtimeVersion);
176
204
  }
177
- return parseFact(value);
205
+ return parseFact(value, runtimeVersion);
178
206
  }
179
-
180
- function encodePrDeliveryFact(fact: PrDeliveryFact): string {
181
- return JSON.stringify(parseFact(fact));
182
- }
183
-
184
- function readPrDeliveryFact(metadata: Record<string, unknown>): PrDeliveryFactReadResult {
207
+ function encodePrDeliveryFact(fact: PrDeliveryFact): string { return JSON.stringify(parseFact(fact)); }
208
+ function readPrDeliveryFact(metadata: Record<string, unknown>, runtimeVersion = VERSION): PrDeliveryFactReadResult {
185
209
  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;
210
+ try { return { status: 'valid', fact: decodePrDeliveryFact(metadata[PR_DELIVERY_FACT_KEY], runtimeVersion) }; }
211
+ catch (error) { return { status: 'invalid', fact: null, error: asFactError(error) }; }
200
212
  }
213
+ function buildUnboundFact(): PrDeliveryFact { return { version: 2, state: 'unbound', reason: 'initial' }; }
214
+ function buildSkippedFact(decidedAt: string): PrDeliveryFact { return parseFact({ version: 2, state: 'skipped', reason: 'explicit', decidedAt }); }
201
215
 
202
216
  function buildBoundFact(input: {
203
- identity: PrDeliveryIdentity;
217
+ identity: CurrentPrDeliveryIdentityInput;
204
218
  source: PrDeliveryBindingSource;
205
219
  verifiedAt: string;
206
- issueNumber?: number | null;
220
+ issueIdentity?: ResourceIdentity | null;
207
221
  remoteState: 'open' | 'closed';
208
222
  mergedAt?: string | null;
209
223
  mergeCommitSha?: string | null;
210
224
  }): 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
- });
225
+ return parseFact({ version: 2, state: 'bound', identity: input.identity, binding: {
226
+ status: 'verified', source: input.source, verifiedAt: input.verifiedAt,
227
+ issueIdentity: input.issueIdentity === undefined ? null : input.issueIdentity,
228
+ remoteState: input.remoteState, mergedAt: input.mergedAt ?? null, mergeCommitSha: input.mergeCommitSha ?? null
229
+ }, provenance: { establishedBy: provenanceForSource(input.source) } });
226
230
  }
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);
231
+ function factFrontmatterMutation(fact: PrDeliveryFact): { set: { [PR_DELIVERY_FACT_KEY]: string } } { return { set: { [PR_DELIVERY_FACT_KEY]: encodePrDeliveryFact(fact) } }; }
232
+ function identityFromPullRequest(pullRequest: PrDeliveryIdentityInput): PrDeliveryIdentity {
233
+ return canonicalIdentity(pullRequest);
234
234
  }
235
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
- };
236
+ export { PR_DELIVERY_FACT_KEY, buildBoundFact, buildSkippedFact, buildUnboundFact, decodePrDeliveryFact, encodePrDeliveryFact, factFrontmatterMutation, identityFromPullRequest, readPrDeliveryFact, serializeResourceIdentity };
237
+ export type { CreationOutcome, PrDeliveryBindingSource, PrDeliveryFact, PrDeliveryFactError, PrDeliveryFactReadResult, PrDeliveryIdentity, PrDeliveryIdentityInput, PrDeliveryProvenance };
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
4
  import { artifactName, maxRound } from "./review-artifacts.ts";
5
+ import { invalidationBlocks, isArtifactInvalidated, parseInvalidationDocument } from "./invalidation.ts";
5
6
 
6
7
  type ReviewedGitTree = string & { readonly __reviewedGitTree: unique symbol };
7
8
  const REVIEWED_TREE_RE = /^[a-f0-9]{40,64}$/;
@@ -46,15 +47,35 @@ export function findAuthoritativeReviewCodeArtifact(taskDir: string) {
46
47
  const entries = fs.existsSync(taskDir) ? fs.readdirSync(taskDir) : [];
47
48
  const round = maxRound(entries, "review-code");
48
49
  if (round === 0) {
49
- return { ok: false, round: 0, fileName: null, path: null };
50
+ return { ok: false, round: 0, fileName: null, path: null, error: null };
50
51
  }
51
52
 
52
53
  const fileName = artifactName("review-code", round);
54
+ let taskContent: string;
55
+ try {
56
+ taskContent = fs.readFileSync(path.join(taskDir, "task.md"), "utf8");
57
+ } catch (error) {
58
+ return {
59
+ ok: false, round, fileName, path: null,
60
+ error: error instanceof Error ? error.message : String(error)
61
+ };
62
+ }
63
+ const invalidation = parseInvalidationDocument(taskContent);
64
+ if (!invalidation.ok) {
65
+ return { ok: false, round, fileName, path: null, error: invalidation.message };
66
+ }
67
+ if (invalidationBlocks(invalidation.document)) {
68
+ return { ok: false, round, fileName, path: null, error: "artifact invalidation is incomplete; reconcile task.md before using review-code" };
69
+ }
70
+ if (isArtifactInvalidated(invalidation.document, "review-code", fileName)) {
71
+ return { ok: false, round, fileName, path: null, error: `${fileName} is invalidated; re-run review-code` };
72
+ }
53
73
  return {
54
74
  ok: true,
55
75
  round,
56
76
  fileName,
57
- path: path.join(taskDir, fileName)
77
+ path: path.join(taskDir, fileName),
78
+ error: null
58
79
  };
59
80
  }
60
81