@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
@@ -0,0 +1,262 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import type { InvalidationDocument } from './invalidation.ts';
5
+ import { invalidationBlocks, isArtifactInvalidated, parseInvalidationDocument } from './invalidation.ts';
6
+ import { receiptForOutput } from './artifact-receipts.ts';
7
+ import { parseTypedTaskFrontmatter } from './frontmatter.ts';
8
+ import { parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
9
+ import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.ts';
10
+ import { parseReworkIntentDocument } from './rework-intent.ts';
11
+ import type { ReworkIntent } from './rework-intent.ts';
12
+ import { sha256File } from './artifact-receipts.ts';
13
+ import { hasOpenLifecycleExecution } from './activity-log.ts';
14
+
15
+ type LifecycleAction =
16
+ | 'analysis' | 'review-analysis' | 'plan' | 'review-plan'
17
+ | 'code' | 'review-code' | 'manual-validation' | 'validation-run';
18
+ type TriggerInitiator = 'human' | 'model' | 'orchestrator';
19
+ type TriggerReason = 'user-request' | 'new-requirement' | 'upstream-fact-doubt' | 'review-finding' | 'retry' | 'validation-rerun';
20
+ type ExplicitTrigger = {
21
+ initiator: TriggerInitiator;
22
+ requestId: string;
23
+ requestedAction: LifecycleAction;
24
+ reasonCode: TriggerReason;
25
+ sourceFinding?: string;
26
+ sourceArtifact?: string;
27
+ sourceSha256?: string;
28
+ implementationInput?: string;
29
+ /** False marks the ordinary recommended workflow event; it is not an authorization fact. */
30
+ explicitRequest?: boolean;
31
+ };
32
+ type LifecycleFacts = {
33
+ taskState: string;
34
+ currentStep: string;
35
+ artifacts: Partial<Record<LifecycleAction, readonly string[]>>;
36
+ staleArtifacts?: Partial<Record<LifecycleAction, readonly string[]>>;
37
+ reviewedInputs?: Partial<Record<'review-analysis' | 'review-plan' | 'review-code', string>>;
38
+ artifactHashes: Readonly<Record<string, string>>;
39
+ reviews: Partial<Record<'review-analysis' | 'review-plan' | 'review-code', 'approved' | 'changes-requested' | 'rejected'>>;
40
+ invalidation: InvalidationDocument;
41
+ reworkIntents?: readonly ReworkIntent[];
42
+ unresolvedLedger: Record<'analysis' | 'plan' | 'code', number>;
43
+ executionBusy: boolean;
44
+ recommendedAction?: LifecycleAction | null;
45
+ };
46
+ type CapabilityResult = {
47
+ allowed: boolean;
48
+ reasonCode: string;
49
+ evidence: readonly string[];
50
+ };
51
+ type LifecycleRecommendation = {
52
+ action: LifecycleAction | null;
53
+ reasonCode: string;
54
+ evidence: readonly string[];
55
+ };
56
+ type LifecycleFactsResult =
57
+ | { ok: true; facts: LifecycleFacts }
58
+ | { ok: false; code: 'TASK_CAPABILITY_FACTS_INVALID'; message: string };
59
+
60
+ const REASONS = new Set<TriggerReason>([
61
+ 'user-request', 'new-requirement', 'upstream-fact-doubt', 'review-finding', 'retry', 'validation-rerun'
62
+ ]);
63
+
64
+ function deny(reasonCode: string, ...evidence: string[]): CapabilityResult {
65
+ return { allowed: false, reasonCode, evidence };
66
+ }
67
+
68
+ function allow(...evidence: string[]): CapabilityResult {
69
+ return { allowed: true, reasonCode: 'CAPABILITY_ALLOWED', evidence };
70
+ }
71
+
72
+ function hasArtifact(facts: LifecycleFacts, action: LifecycleAction): boolean {
73
+ return (facts.artifacts[action]?.length ?? 0) > 0;
74
+ }
75
+
76
+ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: ExplicitTrigger): CapabilityResult {
77
+ if (!trigger || trigger.requestedAction !== action) return deny('TRIGGER_ACTION_MISMATCH', `requested=${trigger?.requestedAction ?? 'missing'}`);
78
+ if (!trigger.requestId || !trigger.requestId.trim()) return deny('TRIGGER_REQUEST_ID_REQUIRED');
79
+ if (!REASONS.has(trigger.reasonCode)) return deny('TRIGGER_REASON_INVALID');
80
+ if (trigger.sourceArtifact && !trigger.sourceSha256) return deny('SOURCE_ARTIFACT_HASH_REQUIRED');
81
+ if (trigger.sourceSha256 && !trigger.sourceArtifact) return deny('SOURCE_ARTIFACT_REQUIRED');
82
+ if (trigger.sourceArtifact && trigger.sourceSha256) {
83
+ const actual = facts.artifactHashes[trigger.sourceArtifact];
84
+ if (!actual) return deny('SOURCE_ARTIFACT_MISSING', trigger.sourceArtifact);
85
+ if (actual !== trigger.sourceSha256) return deny('SOURCE_ARTIFACT_HASH_MISMATCH', trigger.sourceArtifact);
86
+ }
87
+ if (facts.taskState !== 'active') return deny('TASK_NOT_ACTIVE', `state=${facts.taskState}`);
88
+ if (invalidationBlocks(facts.invalidation)) return deny('INVALIDATION_INCOMPLETE');
89
+ if (facts.executionBusy) return deny('EXECUTION_BUSY');
90
+
91
+ const implicit = trigger.explicitRequest === false;
92
+ if (action === 'analysis') {
93
+ if (!implicit || hasArtifact(facts, 'analysis') || facts.recommendedAction === 'analysis') return allow('analysis-capability');
94
+ return deny('ANALYSIS_REQUEST_REQUIRES_EXPLICIT_TRIGGER');
95
+ }
96
+ if (action === 'review-analysis') {
97
+ return hasArtifact(facts, 'analysis') ? allow('analysis-artifact') : deny('ANALYSIS_ARTIFACT_REQUIRED');
98
+ }
99
+ if (action === 'plan') {
100
+ if (!hasArtifact(facts, 'review-analysis')) return deny('ANALYSIS_REVIEW_REQUIRED');
101
+ if (!reviewMatchesLatest(facts, 'analysis', 'review-analysis')) return deny('ANALYSIS_REVIEW_NOT_LATEST');
102
+ if (facts.reviews['review-analysis'] !== 'approved') return deny('ANALYSIS_REVIEW_NOT_APPROVED');
103
+ if (facts.unresolvedLedger.analysis > 0) return deny('ANALYSIS_LEDGER_BLOCKED', `unresolved=${facts.unresolvedLedger.analysis}`);
104
+ return allow('analysis-review-approved');
105
+ }
106
+ if (action === 'review-plan') {
107
+ return hasArtifact(facts, 'plan') ? allow('plan-artifact') : deny('PLAN_ARTIFACT_REQUIRED');
108
+ }
109
+ if (action === 'code') {
110
+ if (trigger.implementationInput) {
111
+ if (!hasArtifact(facts, 'review-code')) return deny('CODE_REVIEW_REQUIRED');
112
+ if (!reviewMatchesLatest(facts, 'code', 'review-code')) return deny('CODE_REVIEW_NOT_LATEST');
113
+ if (facts.reviews['review-code'] !== 'approved') return deny('CODE_REVIEW_NOT_APPROVED');
114
+ return allow('implementation-input');
115
+ }
116
+ if (!hasArtifact(facts, 'review-plan')) return deny('PLAN_REVIEW_REQUIRED');
117
+ if (!reviewMatchesLatest(facts, 'plan', 'review-plan')) return deny('PLAN_REVIEW_NOT_LATEST');
118
+ if (facts.reviews['review-plan'] !== 'approved') return deny('PLAN_REVIEW_NOT_APPROVED');
119
+ if (facts.unresolvedLedger.plan > 0) return deny('PLAN_LEDGER_BLOCKED', `unresolved=${facts.unresolvedLedger.plan}`);
120
+ return allow('plan-review-approved');
121
+ }
122
+ if (action === 'review-code') {
123
+ return hasArtifact(facts, 'code') ? allow('code-artifact') : deny('CODE_ARTIFACT_REQUIRED');
124
+ }
125
+ if (action === 'manual-validation' || action === 'validation-run') {
126
+ if (!hasArtifact(facts, 'review-code')) return deny('CODE_REVIEW_REQUIRED');
127
+ if (!reviewMatchesLatest(facts, 'code', 'review-code')) return deny('CODE_REVIEW_NOT_LATEST');
128
+ if (facts.reviews['review-code'] !== 'approved') return deny('CODE_REVIEW_NOT_APPROVED');
129
+ if (facts.unresolvedLedger.code > 0) return deny('CODE_LEDGER_BLOCKED', `unresolved=${facts.unresolvedLedger.code}`);
130
+ return allow('code-review-approved');
131
+ }
132
+ return deny('CAPABILITY_ACTION_UNKNOWN');
133
+ }
134
+
135
+ function artifactRound(name: string): number {
136
+ const match = /-r(\d+)\.md$/.exec(name);
137
+ return match ? Number(match[1]) : 1;
138
+ }
139
+
140
+ function latestArtifact(names: readonly string[]): string | null {
141
+ return [...names].sort((left, right) => artifactRound(right) - artifactRound(left) || left.localeCompare(right))[0] ?? null;
142
+ }
143
+
144
+ function reviewedInputName(content: string, expectedFamily: 'analysis' | 'plan' | 'code'): string | null {
145
+ const header = content.split(/\r?\n/).findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
146
+ if (header < 0) return null;
147
+ const referenceBlock = content.split(/\r?\n/).slice(header, header + 12).join('\n');
148
+ const expected = new RegExp('`' + expectedFamily + '(?:-r[2-9]|-r[1-9]\\d+)?\\.md`');
149
+ return expected.exec(referenceBlock)?.[0].slice(1, -1) ?? null;
150
+ }
151
+
152
+ function reviewMatchesLatest(facts: LifecycleFacts, source: 'analysis' | 'plan' | 'code', review: 'review-analysis' | 'review-plan' | 'review-code'): boolean {
153
+ const latestSource = latestArtifact(facts.artifacts[source] ?? []);
154
+ return Boolean(latestSource && facts.reviewedInputs?.[review] === latestSource);
155
+ }
156
+
157
+ function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
158
+ if (invalidationBlocks(facts.invalidation)) return { action: null, reasonCode: 'INVALIDATION_INCOMPLETE', evidence: ['reconcile task invalidation before routing'] };
159
+ const pendingIntent = (facts.reworkIntents ?? []).find((intent) => intent.status === 'pending');
160
+ if (pendingIntent) return { action: pendingIntent.target, reasonCode: 'REWORK_INTENT_PENDING', evidence: [pendingIntent.intentId, pendingIntent.findingId] };
161
+ if (!hasArtifact(facts, 'analysis')) return { action: 'analysis', reasonCode: 'ANALYSIS_ARTIFACT_MISSING', evidence: ['analysis artifact is absent'] };
162
+ if (!hasArtifact(facts, 'review-analysis') || !reviewMatchesLatest(facts, 'analysis', 'review-analysis')) {
163
+ return { action: 'review-analysis', reasonCode: 'ANALYSIS_REVIEW_MISSING', evidence: ['analysis review does not bind the latest analysis artifact'] };
164
+ }
165
+ if (facts.reviews['review-analysis'] !== 'approved' || facts.unresolvedLedger.analysis > 0) {
166
+ return { action: 'analysis', reasonCode: 'ANALYSIS_REWORK_REQUIRED', evidence: ['analysis review or ledger is not clear'] };
167
+ }
168
+ if (!hasArtifact(facts, 'plan')) return { action: 'plan', reasonCode: 'PLAN_ARTIFACT_MISSING', evidence: ['plan artifact is absent'] };
169
+ if (!hasArtifact(facts, 'review-plan') || !reviewMatchesLatest(facts, 'plan', 'review-plan')) {
170
+ return { action: 'review-plan', reasonCode: 'PLAN_REVIEW_MISSING', evidence: ['plan review does not bind the latest plan artifact'] };
171
+ }
172
+ if (facts.reviews['review-plan'] !== 'approved' || facts.unresolvedLedger.plan > 0) {
173
+ return { action: 'plan', reasonCode: 'PLAN_REWORK_REQUIRED', evidence: ['plan review or ledger is not clear'] };
174
+ }
175
+ if (!hasArtifact(facts, 'code')) return { action: 'code', reasonCode: 'CODE_ARTIFACT_MISSING', evidence: ['code artifact is absent'] };
176
+ if (!hasArtifact(facts, 'review-code') || !reviewMatchesLatest(facts, 'code', 'review-code')) {
177
+ return { action: 'review-code', reasonCode: 'CODE_REVIEW_MISSING', evidence: ['code review does not bind the latest code artifact'] };
178
+ }
179
+ if (facts.reviews['review-code'] !== 'approved') {
180
+ return { action: 'code', reasonCode: 'CODE_REWORK_REQUIRED', evidence: ['code review or ledger is not clear'] };
181
+ }
182
+ return { action: null, reasonCode: 'LIFECYCLE_REVIEWED', evidence: ['latest code review is approved'] };
183
+ }
184
+
185
+ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'active', executionBusy = false): LifecycleFactsResult {
186
+ try {
187
+ const metadata = parseTypedTaskFrontmatter(content);
188
+ const invalidation = parseInvalidationDocument(content);
189
+ if (!invalidation.ok) return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: invalidation.message };
190
+ const rework = parseReworkIntentDocument(content);
191
+ if (!rework.ok) return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: rework.message };
192
+ const ledger = parseLedgerDocument(content);
193
+ if (ledger.present) {
194
+ const invalid = validateLedgerRows(ledger.rows);
195
+ if (invalid) return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: invalid.message };
196
+ }
197
+ const files = fs.readdirSync(taskDir).filter((name) => {
198
+ if (!name.endsWith('.md')) return false;
199
+ const stat = fs.lstatSync(path.join(taskDir, name));
200
+ return stat.isFile() && !stat.isSymbolicLink();
201
+ });
202
+ const artifactFamilies = {
203
+ analysis: /^analysis(?:-r\d+)?\.md$/,
204
+ 'review-analysis': /^review-analysis(?:-r\d+)?\.md$/,
205
+ plan: /^plan(?:-r\d+)?\.md$/,
206
+ 'review-plan': /^review-plan(?:-r\d+)?\.md$/,
207
+ code: /^code(?:-r\d+)?\.md$/,
208
+ 'review-code': /^review-code(?:-r\d+)?\.md$/
209
+ } as const;
210
+ const familyFor = (name: string): keyof typeof artifactFamilies | null =>
211
+ (Object.keys(artifactFamilies) as Array<keyof typeof artifactFamilies>)
212
+ .find((family) => artifactFamilies[family].test(name)) ?? null;
213
+ const activeFiles = files.filter((name) => {
214
+ const family = familyFor(name);
215
+ return !family || !isArtifactInvalidated(invalidation.document, family, name);
216
+ });
217
+ const artifactHashes: Record<string, string> = {};
218
+ for (const name of activeFiles) artifactHashes[name] = sha256File(path.join(taskDir, name));
219
+ const artifacts: Partial<Record<LifecycleAction, readonly string[]>> = Object.fromEntries(
220
+ (Object.keys(artifactFamilies) as Array<keyof typeof artifactFamilies>).map((family) => [
221
+ family, activeFiles.filter((name) => artifactFamilies[family].test(name))
222
+ ])
223
+ ) as Partial<Record<LifecycleAction, readonly string[]>>;
224
+ const staleArtifacts: Partial<Record<LifecycleAction, readonly string[]>> = Object.fromEntries(
225
+ (Object.keys(artifactFamilies) as Array<keyof typeof artifactFamilies>).map((family) => [
226
+ family, files.filter((name) => artifactFamilies[family].test(name) && isArtifactInvalidated(invalidation.document, family, name))
227
+ ])
228
+ ) as Partial<Record<LifecycleAction, readonly string[]>>;
229
+ const reviews: LifecycleFacts['reviews'] = {};
230
+ const reviewedInputs: NonNullable<LifecycleFacts['reviewedInputs']> = {};
231
+ for (const family of ['review-analysis', 'review-plan', 'review-code'] as const) {
232
+ const latest = latestArtifact(artifacts[family] ?? []);
233
+ if (!latest) continue;
234
+ const reviewContent = fs.readFileSync(path.join(taskDir, latest), 'utf8');
235
+ const expectedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
236
+ const input = reviewedInputName(reviewContent, expectedFamily);
237
+ const receipt = input ? receiptForOutput(content, latest) : null;
238
+ if (input && receipt?.input === input && artifactHashes[input] === receipt.inputSha256) reviewedInputs[family] = input;
239
+ const parsed = parseReviewSummary(reviewContent);
240
+ if (!parsed.ok) continue;
241
+ const verdict = resolveCanonicalVerdict(parsed.summary);
242
+ if (verdict.ok) reviews[family] = verdict.verdict === 'Approved' ? 'approved' : verdict.verdict === 'Changes Requested' ? 'changes-requested' : 'rejected';
243
+ }
244
+ const unresolvedLedger = { analysis: 0, plan: 0, code: 0 };
245
+ if (ledger.present) {
246
+ for (const stage of ['analysis', 'plan', 'code'] as const) unresolvedLedger[stage] = summarizeLedgerStage(ledger.rows, stage).unresolved.length;
247
+ }
248
+ const facts: LifecycleFacts = {
249
+ taskState, currentStep: String(metadata.current_step ?? ''), artifacts, reviews,
250
+ staleArtifacts, reviewedInputs, artifactHashes,
251
+ invalidation: invalidation.document, reworkIntents: rework.intents,
252
+ unresolvedLedger, executionBusy: executionBusy || hasOpenLifecycleExecution(content), recommendedAction: null
253
+ };
254
+ facts.recommendedAction = recommendNext(facts).action;
255
+ return { ok: true, facts };
256
+ } catch (error) {
257
+ return { ok: false, code: 'TASK_CAPABILITY_FACTS_INVALID', message: error instanceof Error ? error.message : String(error) };
258
+ }
259
+ }
260
+
261
+ export { buildLifecycleFacts, canStart, recommendNext };
262
+ export type { CapabilityResult, ExplicitTrigger, LifecycleAction, LifecycleFacts, LifecycleFactsResult, LifecycleRecommendation, TriggerInitiator, TriggerReason };
@@ -12,6 +12,9 @@ import { parseActivityLog, pairEntries, startedBackedRows } from '../activity-lo
12
12
  import { readRun, type OrchestrationRun } from '../orchestration.ts';
13
13
  import type { DelegationReceipt } from '../delegation-receipts.ts';
14
14
  import { statusCard, type DisplayMessage } from '../../server/display.ts';
15
+ import { invalidationBlocks, parseInvalidationDocument } from '../invalidation.ts';
16
+ import { parseReworkIntentDocument } from '../rework-intent.ts';
17
+ import { buildLifecycleFacts, recommendNext } from '../capabilities.ts';
15
18
 
16
19
  const USAGE = `Usage: ai task status [--task <ref> | -t <ref>]
17
20
 
@@ -381,6 +384,8 @@ type StatusModel = {
381
384
  workflow: WorkflowInfo;
382
385
  runtime: RuntimeInfo;
383
386
  orchestration: OrchestrationInfo;
387
+ invalidation?: { status: string; processed: number; total: number; pendingIntents: number };
388
+ recommendation?: string;
384
389
  git: GitInfo;
385
390
  };
386
391
 
@@ -429,6 +434,18 @@ function renderStatus(model: StatusModel): string[] {
429
434
  lines.push(...renderPairs(model.artifacts.groups.map((group) => [group.stage, group.files.join(', ')])));
430
435
  }
431
436
 
437
+ lines.push(
438
+ '',
439
+ 'Invalidation',
440
+ ...renderPairs([
441
+ ['status', model.invalidation?.status ?? 'unknown'],
442
+ ['processed', `${model.invalidation?.processed ?? 0}/${model.invalidation?.total ?? 0}`],
443
+ ['pending_intents', String(model.invalidation?.pendingIntents ?? 0)]
444
+ ])
445
+ );
446
+
447
+ lines.push('', 'Recommendation', ...renderPairs([['action', model.recommendation ?? DASH]]));
448
+
432
449
  lines.push(
433
450
  '',
434
451
  'Orchestration',
@@ -494,6 +511,8 @@ function renderStatus(model: StatusModel): string[] {
494
511
  }
495
512
 
496
513
  function modelTone(model: StatusModel): 'info' | 'success' | 'warning' | 'danger' | 'running' {
514
+ if (model.invalidation?.status === 'invalid') return 'danger';
515
+ if (model.invalidation?.status === 'pending' || model.invalidation?.status === 'in-progress') return 'warning';
497
516
  if (model.orchestration.status === 'paused') return 'warning';
498
517
  if (model.orchestration.status === 'running') return 'running';
499
518
  if (model.orchestration.status === 'completed') return 'success';
@@ -511,6 +530,23 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
511
530
  const run = input.run ?? makeRunner(input.repoRoot);
512
531
  const artifacts = enumerateArtifacts(input.taskDir);
513
532
  const workflow = collectWorkflow(content, input.now);
533
+ const invalidation = parseInvalidationDocument(content);
534
+ const rework = parseReworkIntentDocument(content);
535
+ const pendingIntents = rework.ok ? rework.intents.filter((intent) => intent.status === 'pending').length : 0;
536
+ const invalidationInfo = !invalidation.ok
537
+ ? { status: 'invalid', processed: 0, total: 0, pendingIntents }
538
+ : {
539
+ status: invalidation.present
540
+ ? invalidationBlocks(invalidation.document)
541
+ ? invalidation.document.operations.some((operation) => operation.status === 'in-progress') ? 'in-progress' : 'pending'
542
+ : 'completed'
543
+ : 'none',
544
+ processed: invalidation.document.operations.reduce((sum, operation) => sum + operation.processed, 0),
545
+ total: invalidation.document.operations.reduce((sum, operation) => sum + operation.total, 0),
546
+ pendingIntents
547
+ };
548
+ const facts = buildLifecycleFacts(input.taskDir, content, fm.status ?? 'active');
549
+ const recommendation = facts.ok ? (recommendNext(facts.facts).action ?? DASH) : DASH;
514
550
 
515
551
  return {
516
552
  taskId: input.taskId,
@@ -522,6 +558,8 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
522
558
  workflow,
523
559
  runtime: collectRuntime(input.taskDir, workflow, run),
524
560
  orchestration: collectOrchestration(input.taskDir),
561
+ invalidation: invalidationInfo,
562
+ recommendation,
525
563
  git: collectGit(fm.branch ?? '', run)
526
564
  };
527
565
  }
@@ -210,7 +210,18 @@ function syncTaskCommit(
210
210
  );
211
211
  const metadata = captureTaskWriteMetadata();
212
212
  const commitNote = gitText(task.repoRoot, ['show', '-s', '--format=%h %s', committedHead]);
213
- const alreadyLogged = activity.entries.some((entry) => entry.step === 'Commit' && entry.note === commitNote);
213
+ let matchingCommitIndex = -1;
214
+ for (let index = activity.entries.length - 1; index >= 0; index -= 1) {
215
+ const entry = activity.entries[index]!;
216
+ if (entry.step === 'Commit' && entry.note === commitNote) {
217
+ matchingCommitIndex = index;
218
+ break;
219
+ }
220
+ }
221
+ const reviewedAfterCommit = matchingCommitIndex >= 0 && activity.entries
222
+ .slice(matchingCommitIndex + 1)
223
+ .some((entry) => /^Review Code \(Round \d+\)$/.test(entry.step));
224
+ const alreadyLogged = matchingCommitIndex >= 0 && !reviewedAfterCommit;
214
225
  const mutations: TaskMutation[] = [];
215
226
  if (!alreadyLogged) mutations.push({
216
227
  kind: 'section',
@@ -206,15 +206,15 @@ function lifecycleTaskId(request: TaskLifecycleControlRequest, repoRoot: string)
206
206
  }
207
207
  }
208
208
 
209
- function applyLifecycleWithAuthority(
209
+ async function applyLifecycleWithAuthority(
210
210
  context: TaskControlExecutionContext,
211
211
  request: TaskLifecycleControlRequest
212
- ): TaskLifecycleResult & { humanOverride?: unknown } {
212
+ ): Promise<TaskLifecycleResult & { humanOverride?: unknown }> {
213
213
  const conflict = overrideDryRunConflict(request as unknown as Record<string, unknown>);
214
214
  if (conflict) {
215
215
  return taskLifecycleFailure(request, { code: 'LIFECYCLE_PAYLOAD_INVALID', message: conflict.message });
216
216
  }
217
- const execute = (): TaskLifecycleResult & { humanOverride?: unknown } => {
217
+ const execute = async (): Promise<TaskLifecycleResult & { humanOverride?: unknown }> => {
218
218
  const lifecycleResult = applyTaskLifecycle(request, { repoRoot: context.repoRoot });
219
219
  if (lifecycleResult.status !== 'failed' || !request.overrideTicket) return lifecycleResult;
220
220
  if (!request.overrideTarget || !request.overrideScope) {
@@ -223,7 +223,7 @@ function applyLifecycleWithAuthority(
223
223
  error: { code: 'OVERRIDE_PAYLOAD_INVALID', message: 'override ticket requires --override-target and --override-scope' }
224
224
  };
225
225
  }
226
- const override = consumeHumanOverride({
226
+ const override = await consumeHumanOverride({
227
227
  taskRef: request.taskRef,
228
228
  ticketId: request.overrideTicket,
229
229
  failureId: failureId('lifecycle.apply', lifecycleResult.error?.code ?? 'LIFECYCLE_FAILED'),
@@ -238,9 +238,9 @@ function applyLifecycleWithAuthority(
238
238
  return { ...lifecycleResult, ...override, humanOverride: override, error: null };
239
239
  };
240
240
  const taskId = lifecycleTaskId(request, context.repoRoot);
241
- if (!taskId) return execute();
241
+ if (!taskId) return await execute();
242
242
  try {
243
- return withTaskExecutionLock(
243
+ return await withTaskExecutionLock(
244
244
  context.repoRoot,
245
245
  taskId,
246
246
  `task-lifecycle.${request.intent}`,
@@ -322,11 +322,11 @@ function orchestrationFailure(error: unknown): OrchestrationResult {
322
322
  export function dispatchTaskControlOperation(
323
323
  context: TaskControlExecutionContext,
324
324
  operation: Extract<TaskControlOperation, { family: 'task-lifecycle' }>
325
- ): TaskLifecycleResult;
325
+ ): Promise<TaskLifecycleResult & { humanOverride?: unknown }>;
326
326
  export function dispatchTaskControlOperation(
327
327
  context: TaskControlExecutionContext,
328
328
  operation: Extract<TaskControlOperation, { family: 'task-finalization' }>
329
- ): TaskFinalizationResult;
329
+ ): Promise<TaskFinalizationResult>;
330
330
  export function dispatchTaskControlOperation(
331
331
  context: TaskControlExecutionContext,
332
332
  operation: Extract<TaskControlOperation, { family: 'task-orchestration' }>
@@ -345,6 +345,9 @@ export function dispatchTaskControlOperation(
345
345
  operationTaskId(context, operation.request.taskRef);
346
346
  return applyTaskFinalization(operation.request, {
347
347
  repoRoot: context.repoRoot,
348
+ ...(context.source === 'sandbox-executor' ? {
349
+ controlBinding: { generation: context.generation, requestId: context.requestId }
350
+ } : {}),
348
351
  preflight: (request, options) => verifyTaskEvent(
349
352
  { ...request, event: 'complete-task.hard-preflight' }, options
350
353
  )
@@ -42,8 +42,8 @@ const DEFAULT_DEPENDENCIES: TaskCreateDependencies = {
42
42
  addWarning: applyWorkflowWarningIntent
43
43
  };
44
44
 
45
- function verifyCreatedTask(repoRoot: string, taskId: string): { operation: TaskCreateOperation; error: TaskCreateResult['error'] } {
46
- const verification = verifyTaskEvent(
45
+ async function verifyCreatedTask(repoRoot: string, taskId: string): Promise<{ operation: TaskCreateOperation; error: TaskCreateResult['error'] }> {
46
+ const verification = await verifyTaskEvent(
47
47
  { taskRef: taskId, event: 'create-task.completed' },
48
48
  { repoRoot }
49
49
  );
@@ -78,7 +78,7 @@ function issueFromTask(repoRoot: string, taskId: string): { number: number; url:
78
78
  return match ? { number: Number(match[1]), url: '' } : null;
79
79
  }
80
80
 
81
- function createTask(value: unknown, options: CreateTaskOptions): TaskCreateResult {
81
+ async function createTask(value: unknown, options: CreateTaskOptions): Promise<TaskCreateResult> {
82
82
  const dependencies = { ...DEFAULT_DEPENDENCIES, ...options.dependencies };
83
83
  let candidate: TaskCreateCandidateV1;
84
84
  let local;
@@ -101,13 +101,13 @@ function createTask(value: unknown, options: CreateTaskOptions): TaskCreateResul
101
101
  name: 'task:local', status: local.status, reasonCode: null
102
102
  }];
103
103
  let issue = issueFromTask(options.repoRoot, local.task.id);
104
- const created = dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
104
+ const created = await dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
105
105
  operations.push(...platformOperations('platform-create', created.operations));
106
106
  if (created.issue) issue = { number: created.issue.number, url: created.issue.url };
107
107
 
108
108
  let platformFailure: PlatformResult | null = created.status === 'failed' || created.status === 'blocked' ? created : null;
109
109
  if (!platformFailure && (created.task.issueNumber || issue)) {
110
- const synced = dependencies.syncIssue(local.task.id, {
110
+ const synced = await dependencies.syncIssue(local.task.id, {
111
111
  cwd: options.repoRoot,
112
112
  agent: candidate.agent,
113
113
  status: 'waiting-for-triage',
@@ -120,7 +120,7 @@ function createTask(value: unknown, options: CreateTaskOptions): TaskCreateResul
120
120
  if (synced.issue) issue = { number: synced.issue.number, url: synced.issue.url };
121
121
  if (synced.status === 'failed' || synced.status === 'blocked') platformFailure = synced;
122
122
  if (!platformFailure) {
123
- const commented = dependencies.syncComment(local.task.id, { cwd: options.repoRoot, kind: 'task', agent: candidate.agent });
123
+ const commented = await dependencies.syncComment(local.task.id, { cwd: options.repoRoot, kind: 'task', agent: candidate.agent });
124
124
  operations.push(...platformOperations('platform-comment', commented.operations));
125
125
  if (commented.status === 'failed' || commented.status === 'blocked') platformFailure = commented;
126
126
  }
@@ -133,7 +133,7 @@ function createTask(value: unknown, options: CreateTaskOptions): TaskCreateResul
133
133
  code: 'ISSUE_CREATE_FAILED', target: 'issue', message: `${error.code}: ${error.message}`,
134
134
  action: 'Fix platform authentication or connectivity, then retry Issue creation for this task.'
135
135
  }, { repoRoot: options.repoRoot });
136
- const verified = verifyCreatedTask(options.repoRoot, local.task.id);
136
+ const verified = await verifyCreatedTask(options.repoRoot, local.task.id);
137
137
  operations.push(verified.operation);
138
138
  if (verified.error) return {
139
139
  status: verified.error.retryable ? 'blocked' : 'failed', changed: local.changed,
@@ -150,7 +150,7 @@ function createTask(value: unknown, options: CreateTaskOptions): TaskCreateResul
150
150
  };
151
151
  }
152
152
 
153
- const verified = verifyCreatedTask(options.repoRoot, local.task.id);
153
+ const verified = await verifyCreatedTask(options.repoRoot, local.task.id);
154
154
  operations.push(verified.operation);
155
155
  if (verified.error) return {
156
156
  status: verified.error.retryable ? 'blocked' : 'failed', changed: local.changed,
@@ -2,6 +2,8 @@ import { isValidAgentInfraVersion } from '../version.ts';
2
2
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
3
3
  import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, validateLedgerRows } from './ledger.ts';
4
4
  import type { LedgerDocument } from './ledger.ts';
5
+ import { parseInvalidationDocument } from './invalidation.ts';
6
+ import type { InvalidationDocument } from './invalidation.ts';
5
7
 
6
8
  type CurrentTaskContractErrorCode = 'TASK_DOCUMENT_INVALID' | 'TASK_CURRENT_CONTRACT_INVALID';
7
9
  type CurrentTaskContractResult =
@@ -9,6 +11,7 @@ type CurrentTaskContractResult =
9
11
  ok: true;
10
12
  metadata: ReturnType<typeof parseTypedTaskFrontmatter>;
11
13
  ledger: LedgerDocument;
14
+ invalidation: InvalidationDocument;
12
15
  }
13
16
  | {
14
17
  ok: false;
@@ -42,7 +45,9 @@ function validateCurrentTaskContract(content: string): CurrentTaskContractResult
42
45
  }
43
46
  const invalid = validateLedgerRows(ledger.rows);
44
47
  if (invalid) return { ok: false, code: 'TASK_CURRENT_CONTRACT_INVALID', message: `${invalid.code}: ${invalid.message}` };
45
- return { ok: true, metadata, ledger };
48
+ const invalidation = parseInvalidationDocument(content);
49
+ if (!invalidation.ok) return { ok: false, code: 'TASK_CURRENT_CONTRACT_INVALID', message: `${invalidation.code}: ${invalidation.message}` };
50
+ return { ok: true, metadata, ledger, invalidation: invalidation.document };
46
51
  }
47
52
 
48
53
  export { validateCurrentTaskContract };
@@ -270,11 +270,15 @@ export {
270
270
  normalizeDecisionDetailForDisplay,
271
271
  parseDecisionDetailBlocks,
272
272
  parseDecisionEvidence,
273
- resolveDecisionDetail
273
+ resolveDecisionDetail,
274
+ scanVisibleMarkdown
274
275
  };
275
276
  export type {
276
277
  DecisionDetailBlock,
277
278
  DecisionDetailDuplicate,
278
279
  DecisionDetailInspection,
279
- DecisionDetailResolution
280
+ DecisionDetailResolution,
281
+ SourceLine,
282
+ VisibleHeading,
283
+ VisibleMarkdown
280
284
  };