@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,332 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const INVALIDATION_HEADINGS = ['产物失效记录', 'Artifact Invalidation'] as const;
4
+ const OPERATION_COLUMNS = [
5
+ 'operation_id', 'source_family', 'source_artifact', 'source_round', 'source_sha256',
6
+ 'status', 'processed', 'total', 'created_at', 'updated_at', 'completed_at', 'error'
7
+ ] as const;
8
+ const TARGET_COLUMNS = [
9
+ 'target_id', 'operation_id', 'target_kind', 'target_family', 'target_artifact',
10
+ 'target_round', 'target_sha256', 'status', 'reason_code', 'updated_at'
11
+ ] as const;
12
+
13
+ type InvalidationStatus = 'pending' | 'in-progress' | 'completed';
14
+ type InvalidationTargetKind = 'artifact' | 'receipt' | 'approval' | 'reviewed-snapshot';
15
+ type InvalidationOperation = {
16
+ operationId: string;
17
+ sourceFamily: string;
18
+ sourceArtifact: string;
19
+ sourceRound: number;
20
+ sourceSha256: string;
21
+ status: InvalidationStatus;
22
+ processed: number;
23
+ total: number;
24
+ createdAt: string;
25
+ updatedAt: string;
26
+ completedAt: string;
27
+ error: string;
28
+ };
29
+ type InvalidationTarget = {
30
+ targetId: string;
31
+ operationId: string;
32
+ targetKind: InvalidationTargetKind;
33
+ targetFamily: string;
34
+ targetArtifact: string;
35
+ targetRound: number;
36
+ targetSha256: string;
37
+ status: InvalidationStatus;
38
+ reasonCode: string;
39
+ updatedAt: string;
40
+ };
41
+ type InvalidationDocument = {
42
+ operations: readonly InvalidationOperation[];
43
+ targets: readonly InvalidationTarget[];
44
+ };
45
+ type InvalidationParseResult =
46
+ | { ok: true; present: boolean; document: InvalidationDocument }
47
+ | { ok: false; code: 'TASK_INVALIDATION_INVALID'; message: string };
48
+ type InvalidationMutationResult = {
49
+ ok: true;
50
+ changed: boolean;
51
+ document: InvalidationDocument;
52
+ } | {
53
+ ok: false;
54
+ code: 'TASK_INVALIDATION_INVALID' | 'INVALIDATION_IDENTITY_CONFLICT';
55
+ message: string;
56
+ };
57
+
58
+ function invalid(message: string): InvalidationParseResult {
59
+ return { ok: false, code: 'TASK_INVALIDATION_INVALID', message };
60
+ }
61
+
62
+ function hashIdentity(value: unknown): string {
63
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 16);
64
+ }
65
+
66
+ function operationIdFor(source: Pick<InvalidationOperation, 'sourceFamily' | 'sourceArtifact' | 'sourceRound' | 'sourceSha256'>): string {
67
+ return `INV-${hashIdentity([source.sourceFamily, source.sourceArtifact, source.sourceRound, source.sourceSha256])}`;
68
+ }
69
+
70
+ function targetIdFor(operationId: string, target: Pick<InvalidationTarget, 'targetKind' | 'targetFamily' | 'targetArtifact' | 'targetRound' | 'targetSha256'>): string {
71
+ return `INV-T-${hashIdentity([operationId, target.targetKind, target.targetFamily, target.targetArtifact, target.targetRound, target.targetSha256])}`;
72
+ }
73
+
74
+ function createInvalidationOperation(
75
+ source: Partial<Pick<InvalidationOperation, 'operationId'>> & Omit<InvalidationOperation, 'operationId' | 'status' | 'processed' | 'total' | 'completedAt' | 'error'>,
76
+ targets: readonly InvalidationTarget[] = []
77
+ ): InvalidationOperation {
78
+ const operationId = source.operationId ?? operationIdFor(source);
79
+ return {
80
+ operationId, sourceFamily: source.sourceFamily, sourceArtifact: source.sourceArtifact,
81
+ sourceRound: source.sourceRound, sourceSha256: source.sourceSha256,
82
+ status: 'pending', processed: 0, total: targets.length,
83
+ createdAt: source.createdAt, updatedAt: source.updatedAt, completedAt: '', error: ''
84
+ };
85
+ }
86
+
87
+ function lineCells(line: string): string[] | null {
88
+ const first = line.indexOf('|');
89
+ const last = line.lastIndexOf('|');
90
+ if (first < 0 || first === last || line.slice(0, first).trim() || line.slice(last + 1).trim()) return null;
91
+ const inner = line.slice(first + 1, last);
92
+ const cells: string[] = [];
93
+ let start = 0;
94
+ let escaped = false;
95
+ for (let index = 0; index < inner.length; index += 1) {
96
+ const char = inner[index]!;
97
+ if (char === '|' && !escaped) {
98
+ cells.push(inner.slice(start, index));
99
+ start = index + 1;
100
+ }
101
+ escaped = char === '\\' && !escaped;
102
+ if (char !== '\\') escaped = false;
103
+ }
104
+ cells.push(inner.slice(start));
105
+ return cells.map((cell) => cell.replace(/\\([\\|])/g, '$1').trim());
106
+ }
107
+
108
+ function isSeparator(cells: readonly string[], count: number): boolean {
109
+ return cells.length === count && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
110
+ }
111
+
112
+ function sectionBody(content: string): string | null {
113
+ const heading = INVALIDATION_HEADINGS.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
114
+ const match = new RegExp(`^##\\s+(${heading})\\s*$`, 'm').exec(content);
115
+ if (!match) return null;
116
+ const start = (match.index ?? 0) + match[0].length;
117
+ const rest = content.slice(start);
118
+ const end = rest.search(/^##\s+/m);
119
+ return rest.slice(0, end < 0 ? rest.length : end);
120
+ }
121
+
122
+ function parseTable(body: string, heading: string, columns: readonly string[]): Record<string, string>[] | null | false {
123
+ const marker = new RegExp(`^###\\s+${heading}\\s*$`, 'm').exec(body);
124
+ if (!marker) return null;
125
+ const start = (marker.index ?? 0) + marker[0].length;
126
+ const rest = body.slice(start);
127
+ const end = rest.search(/^###\s+/m);
128
+ const lines = rest.slice(0, end < 0 ? rest.length : end).split(/\r?\n/).filter((line) => line.trim());
129
+ const header = lines.length > 0 ? lineCells(lines[0]!) : null;
130
+ const separator = lines.length > 1 ? lineCells(lines[1]!) : null;
131
+ if (!header || !separator || header.length !== columns.length || !header.every((value, index) => value === columns[index]) || !isSeparator(separator, columns.length)) return false;
132
+ const rows: Record<string, string>[] = [];
133
+ for (const line of lines.slice(2)) {
134
+ const cells = lineCells(line);
135
+ if (!cells || cells.length !== columns.length) return false;
136
+ rows.push(Object.fromEntries(columns.map((column, index) => [column, cells[index]!])));
137
+ }
138
+ return rows;
139
+ }
140
+
141
+ function required(value: string, field: string): string {
142
+ if (!value) throw new Error(`${field} is required`);
143
+ return value;
144
+ }
145
+
146
+ function status(value: string, field: string): InvalidationStatus {
147
+ if (value !== 'pending' && value !== 'in-progress' && value !== 'completed') throw new Error(`${field} has an invalid status`);
148
+ return value;
149
+ }
150
+
151
+ function numberValue(value: string, field: string, allowZero = true): number {
152
+ const parsed = Number(value);
153
+ if (!Number.isInteger(parsed) || (allowZero ? parsed < 0 : parsed < 1)) throw new Error(`${field} must be an integer`);
154
+ return parsed;
155
+ }
156
+
157
+ function parseInvalidationDocument(content: string): InvalidationParseResult {
158
+ const body = sectionBody(content);
159
+ if (body === null) return { ok: true, present: false, document: { operations: [], targets: [] } };
160
+ try {
161
+ const operationRows = parseTable(body, 'Operations', OPERATION_COLUMNS);
162
+ const targetRows = parseTable(body, 'Targets', TARGET_COLUMNS);
163
+ if (operationRows === null || targetRows === null || operationRows === false || targetRows === false) {
164
+ return invalid('invalidation section must contain Operations and Targets tables');
165
+ }
166
+ const operations = operationRows.map((row) => ({
167
+ operationId: required(row.operation_id!, 'operation_id'), sourceFamily: required(row.source_family!, 'source_family'),
168
+ sourceArtifact: required(row.source_artifact!, 'source_artifact'), sourceRound: numberValue(row.source_round!, 'source_round', false),
169
+ sourceSha256: required(row.source_sha256!, 'source_sha256'), status: status(row.status!, 'operation status'),
170
+ processed: numberValue(row.processed!, 'processed'), total: numberValue(row.total!, 'total'),
171
+ createdAt: required(row.created_at!, 'created_at'), updatedAt: required(row.updated_at!, 'updated_at'),
172
+ completedAt: row.completed_at ?? '', error: row.error ?? ''
173
+ } satisfies InvalidationOperation));
174
+ const targets = targetRows.map((row) => ({
175
+ targetId: required(row.target_id!, 'target_id'), operationId: required(row.operation_id!, 'operation_id'),
176
+ targetKind: row.target_kind as InvalidationTargetKind, targetFamily: required(row.target_family!, 'target_family'),
177
+ targetArtifact: required(row.target_artifact!, 'target_artifact'), targetRound: numberValue(row.target_round!, 'target_round', false),
178
+ targetSha256: required(row.target_sha256!, 'target_sha256'), status: status(row.status!, 'target status'),
179
+ reasonCode: required(row.reason_code!, 'reason_code'), updatedAt: required(row.updated_at!, 'updated_at')
180
+ } satisfies InvalidationTarget));
181
+ const operationIds = new Set<string>();
182
+ for (const operation of operations) {
183
+ if (operationIds.has(operation.operationId)) throw new Error(`duplicate operation '${operation.operationId}'`);
184
+ operationIds.add(operation.operationId);
185
+ if (operation.operationId !== operationIdFor(operation)) throw new Error(`operation '${operation.operationId}' has a non-canonical identity`);
186
+ if (operation.processed > operation.total) throw new Error(`operation '${operation.operationId}' progress is invalid`);
187
+ if (!/^[a-f0-9]{64}$/.test(operation.sourceSha256)) throw new Error(`operation '${operation.operationId}' source hash is invalid`);
188
+ }
189
+ const targetIds = new Set<string>();
190
+ for (const target of targets) {
191
+ if (!['artifact', 'receipt', 'approval', 'reviewed-snapshot'].includes(target.targetKind)) throw new Error(`target '${target.targetId}' kind is invalid`);
192
+ if (targetIds.has(target.targetId)) throw new Error(`duplicate target '${target.targetId}'`);
193
+ targetIds.add(target.targetId);
194
+ if (!operationIds.has(target.operationId)) throw new Error(`target '${target.targetId}' references an unknown operation`);
195
+ if (target.targetId !== targetIdFor(target.operationId, target)) throw new Error(`target '${target.targetId}' has a non-canonical identity`);
196
+ if (!/^[a-f0-9]{64}$/.test(target.targetSha256)) throw new Error(`target '${target.targetId}' hash is invalid`);
197
+ }
198
+ for (const operation of operations) {
199
+ const scoped = targets.filter((target) => target.operationId === operation.operationId);
200
+ const completed = scoped.filter((target) => target.status === 'completed').length;
201
+ if (operation.total !== scoped.length || operation.processed !== completed) throw new Error(`operation '${operation.operationId}' progress does not match its targets`);
202
+ if (operation.status === 'completed' && (completed !== scoped.length || !operation.completedAt)) throw new Error(`completed operation '${operation.operationId}' has incomplete targets`);
203
+ if (operation.status === 'pending' && completed !== 0) throw new Error(`pending operation '${operation.operationId}' has completed targets`);
204
+ if (operation.status === 'in-progress' && (completed === scoped.length || (completed === 0 && !scoped.some((target) => target.status === 'in-progress')))) throw new Error(`in-progress operation '${operation.operationId}' has invalid progress`);
205
+ }
206
+ return { ok: true, present: true, document: { operations, targets } };
207
+ } catch (error) {
208
+ return invalid(error instanceof Error ? error.message : String(error));
209
+ }
210
+ }
211
+
212
+ function cell(value: string | number): string {
213
+ return String(value).replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/[\r\n]/g, ' ');
214
+ }
215
+
216
+ function table(columns: readonly string[], rows: readonly (readonly (string | number)[])[]): string {
217
+ const header = `| ${columns.join(' | ')} |`;
218
+ const separator = `| ${columns.map(() => '---').join(' | ')} |`;
219
+ return [header, separator, ...rows.map((row) => `| ${row.map(cell).join(' | ')} |`)].join('\n');
220
+ }
221
+
222
+ function renderInvalidation(document: InvalidationDocument): string {
223
+ const operations = [...document.operations].sort((left, right) => left.operationId.localeCompare(right.operationId));
224
+ const targets = [...document.targets].sort((left, right) => left.targetId.localeCompare(right.targetId));
225
+ return [
226
+ '### Operations', '',
227
+ table(OPERATION_COLUMNS, operations.map((row) => [
228
+ row.operationId, row.sourceFamily, row.sourceArtifact, row.sourceRound, row.sourceSha256,
229
+ row.status, row.processed, row.total, row.createdAt, row.updatedAt, row.completedAt, row.error
230
+ ])), '',
231
+ '### Targets', '',
232
+ table(TARGET_COLUMNS, targets.map((row) => [
233
+ row.targetId, row.operationId, row.targetKind, row.targetFamily, row.targetArtifact,
234
+ row.targetRound, row.targetSha256, row.status, row.reasonCode, row.updatedAt
235
+ ]))
236
+ ].join('\n');
237
+ }
238
+
239
+ function reconcileInvalidation(document: InvalidationDocument, now: string, maxTargets = Number.POSITIVE_INFINITY): InvalidationMutationResult {
240
+ const nextTargets = document.targets.map((target) => {
241
+ if (target.status === 'completed' || maxTargets <= 0) return target;
242
+ maxTargets -= 1;
243
+ return { ...target, status: 'completed' as const, updatedAt: now };
244
+ });
245
+ const changed = nextTargets.some((target, index) => target !== document.targets[index]);
246
+ const nextOperations = document.operations.map((operation) => {
247
+ const scoped = nextTargets.filter((target) => target.operationId === operation.operationId);
248
+ const processed = scoped.filter((target) => target.status === 'completed').length;
249
+ const total = scoped.length;
250
+ const complete = total === processed;
251
+ const nextStatus: InvalidationStatus = complete ? 'completed' : processed > 0 ? 'in-progress' : 'pending';
252
+ if (operation.status === nextStatus && operation.processed === processed && operation.total === total && (!complete || operation.completedAt)) return operation;
253
+ return {
254
+ ...operation, status: nextStatus, processed, total, updatedAt: changed ? now : operation.updatedAt,
255
+ completedAt: complete ? (operation.completedAt || now) : ''
256
+ };
257
+ });
258
+ const operationChanged = nextOperations.some((operation, index) => operation !== document.operations[index]);
259
+ return { ok: true, changed: changed || operationChanged, document: { operations: nextOperations, targets: nextTargets } };
260
+ }
261
+
262
+ function upsertInvalidation(
263
+ document: InvalidationDocument,
264
+ operation: InvalidationOperation,
265
+ targets: readonly InvalidationTarget[]
266
+ ): InvalidationMutationResult {
267
+ const existing = document.operations.find((candidate) => candidate.operationId === operation.operationId);
268
+ if (existing && JSON.stringify(existing) !== JSON.stringify(operation)) {
269
+ return { ok: false, code: 'INVALIDATION_IDENTITY_CONFLICT', message: `operation '${operation.operationId}' identity conflicts with existing state` };
270
+ }
271
+ const existingTargets = document.targets.filter((target) => target.operationId === operation.operationId);
272
+ for (const target of targets) {
273
+ const old = existingTargets.find((candidate) => candidate.targetId === target.targetId);
274
+ if (old && JSON.stringify(old) !== JSON.stringify(target)) {
275
+ return { ok: false, code: 'INVALIDATION_IDENTITY_CONFLICT', message: `target '${target.targetId}' identity conflicts with existing state` };
276
+ }
277
+ }
278
+ if (existing) return { ok: true, changed: false, document };
279
+ return {
280
+ ok: true, changed: true,
281
+ document: { operations: [...document.operations, operation], targets: [...document.targets, ...targets] }
282
+ };
283
+ }
284
+
285
+ function invalidationBlocks(document: InvalidationDocument): boolean {
286
+ return document.operations.some((operation) => operation.status !== 'completed')
287
+ || document.targets.some((target) => target.status !== 'completed');
288
+ }
289
+
290
+ function isArtifactInvalidated(document: InvalidationDocument, family: string, artifact: string): boolean {
291
+ return document.targets.some((target) =>
292
+ target.status === 'completed'
293
+ && target.targetKind === 'artifact'
294
+ && target.targetFamily === family
295
+ && target.targetArtifact === artifact
296
+ );
297
+ }
298
+
299
+ function invalidationMutation(content: string, document: InvalidationDocument) {
300
+ const existing = sectionBody(content) !== null;
301
+ return {
302
+ kind: 'section' as const,
303
+ aliases: INVALIDATION_HEADINGS,
304
+ heading: existing && /^##\s+Artifact Invalidation\s*$/m.test(content) ? INVALIDATION_HEADINGS[1] : INVALIDATION_HEADINGS[0],
305
+ body: renderInvalidation(document)
306
+ };
307
+ }
308
+
309
+ export {
310
+ INVALIDATION_HEADINGS,
311
+ OPERATION_COLUMNS,
312
+ TARGET_COLUMNS,
313
+ createInvalidationOperation,
314
+ isArtifactInvalidated,
315
+ invalidationBlocks,
316
+ invalidationMutation,
317
+ operationIdFor,
318
+ parseInvalidationDocument,
319
+ reconcileInvalidation,
320
+ renderInvalidation,
321
+ targetIdFor,
322
+ upsertInvalidation
323
+ };
324
+ export type {
325
+ InvalidationDocument,
326
+ InvalidationMutationResult,
327
+ InvalidationOperation,
328
+ InvalidationParseResult,
329
+ InvalidationStatus,
330
+ InvalidationTarget,
331
+ InvalidationTargetKind
332
+ };
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
3
4
 
4
5
  import {
5
6
  declareImplementationInput,
@@ -13,6 +14,8 @@ import { resolveTaskRef } from './resolve-ref.ts';
13
14
  import { writeTask } from './write.ts';
14
15
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
15
16
  import { allowsManualOverride } from './guard-override.ts';
17
+ import { parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.ts';
18
+ import type { ReworkIntent, ReworkTarget } from './rework-intent.ts';
16
19
 
17
20
  type ReviewSeverity = 'blocker' | 'major' | 'minor';
18
21
  type ExecutorResponse = 'accepted' | 'adjusted' | 'refuted' | 'cannot-judge';
@@ -22,7 +25,8 @@ type LedgerIntent =
22
25
  | { kind: 'finding-respond'; taskRef: string; id: string; round: number; status: ExecutorResponse; evidence: string; dryRun?: boolean }
23
26
  | { kind: 'finding-review'; taskRef: string; id: string; status: ReviewDisposition; evidence: string; needsImplementation?: boolean; dryRun?: boolean }
24
27
  | { kind: 'decision-next-id'; taskRef: string }
25
- | { kind: 'decision-upsert'; taskRef: string; id: string; stage: ReviewStage; artifact: string; needsImplementation?: boolean; dryRun?: boolean };
28
+ | { kind: 'decision-upsert'; taskRef: string; id: string; stage: ReviewStage; artifact: string; needsImplementation?: boolean; dryRun?: boolean }
29
+ | { kind: 'rework-intent-upsert'; taskRef: string; intentId: string; findingId: string; sourceArtifact: string; sourceSha256: string; target: ReworkTarget; dryRun?: boolean };
26
30
 
27
31
  type LedgerIntentError = { code: string; message: string };
28
32
  type LedgerIntentResult = {
@@ -136,6 +140,36 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
136
140
  return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: nextHdId(rows), before: null, after: null, operations: [], error: null };
137
141
  }
138
142
 
143
+ if (intent.kind === 'rework-intent-upsert') {
144
+ if (!/^RI-[1-9]\d*$/.test(intent.intentId) || !/^(AN|PL|CD)-[1-9]\d*$/.test(intent.findingId) || !['analysis', 'plan', 'code'].includes(intent.target) || !/^[a-f0-9]{64}$/.test(intent.sourceSha256)) {
145
+ return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rework intent identity, target, or source hash is invalid', resolved.taskId, intent.intentId);
146
+ }
147
+ const finding = rows.find((row) => row.id === intent.findingId);
148
+ if (!finding || finding.severity === 'decision') return failed(intent, 'LEDGER_NOT_FOUND', `finding '${intent.findingId}' was not found`, resolved.taskId, intent.findingId);
149
+ const sourceFromEvidence = finding.evidence.split('#')[0];
150
+ if (sourceFromEvidence !== intent.sourceArtifact || !REVIEW_ARTIFACT[finding.stage as ReviewStage]?.test(intent.sourceArtifact)) {
151
+ return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact does not match finding evidence', resolved.taskId, intent.intentId);
152
+ }
153
+ try {
154
+ const actualHash = createHash('sha256').update(fs.readFileSync(path.join(resolved.taskDir, intent.sourceArtifact))).digest('hex');
155
+ if (actualHash !== intent.sourceSha256) return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact hash does not match', resolved.taskId, intent.intentId);
156
+ const parsed = parseReworkIntentDocument(content);
157
+ if (!parsed.ok) return failed(intent, parsed.code, parsed.message, resolved.taskId, intent.intentId);
158
+ const nextIntent: ReworkIntent = {
159
+ intentId: intent.intentId, findingId: intent.findingId, sourceArtifact: intent.sourceArtifact,
160
+ sourceSha256: intent.sourceSha256, target: intent.target, status: 'pending',
161
+ declaredAt: new Date().toISOString(), consumedAt: ''
162
+ };
163
+ const next = upsertReworkIntent(parsed.intents, nextIntent);
164
+ if (!next.changed) return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: intent.intentId, before: null, after: null, operations: [], error: null };
165
+ const result = writeTask({ taskRef: intent.taskRef, expectedState: 'active', dryRun: intent.dryRun, mutations: [reworkIntentMutation(content, next.intents)] }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
166
+ if (result.status === 'failed') return failed(intent, result.error.code, result.error.message, result.taskId, intent.intentId);
167
+ return { status: result.status, changed: result.changed, intent: intent.kind, taskId: result.taskId, entityId: intent.intentId, before: null, after: null, operations: result.operations, error: null };
168
+ } catch (error) {
169
+ return failed(intent, 'LEDGER_IDENTITY_CONFLICT', error instanceof Error ? error.message : String(error), resolved.taskId, intent.intentId);
170
+ }
171
+ }
172
+
139
173
  let before: LedgerRow | null = null;
140
174
  let after: LedgerRow;
141
175
  if (intent.kind === 'finding-upsert') {