@fitlab-ai/agent-infra 0.9.14 → 0.9.15

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 (250) hide show
  1. package/README.md +10 -0
  2. package/README.zh-CN.md +10 -0
  3. package/bin/internal-cli.sh +45 -0
  4. package/dist/bin/internal-cli.js +93 -30
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
  7. package/dist/lib/defaults.json +103 -103
  8. package/dist/lib/fs/durable-write.js +37 -0
  9. package/dist/lib/fs/file-lock.js +111 -0
  10. package/dist/lib/host-control/audit.js +19 -0
  11. package/dist/lib/host-control/client.js +142 -0
  12. package/dist/lib/host-control/command.js +49 -0
  13. package/dist/lib/host-control/path.js +160 -0
  14. package/dist/lib/host-control/secure-fs.js +140 -0
  15. package/dist/lib/host-control/server.js +159 -0
  16. package/dist/lib/host-control/service.js +112 -0
  17. package/dist/lib/init.js +11 -2
  18. package/dist/lib/internal/cli-route-inventory.js +3 -1
  19. package/dist/lib/internal/codex-lifecycle.js +6 -6
  20. package/dist/lib/internal/demo-promotion.js +322 -0
  21. package/dist/lib/internal/demo-transcript.js +454 -0
  22. package/dist/lib/internal/host-control.js +28 -0
  23. package/dist/lib/internal/manual-validation.js +322 -0
  24. package/dist/lib/internal/platform-pr.js +18 -4
  25. package/dist/lib/internal/release-workflow.js +63 -84
  26. package/dist/lib/internal/sandbox-control.js +149 -2
  27. package/dist/lib/internal/task-artifact.js +15 -262
  28. package/dist/lib/internal/task-create.js +6 -5
  29. package/dist/lib/internal/task-event.js +37 -37
  30. package/dist/lib/internal/task-operation-registry.js +101 -3
  31. package/dist/lib/internal/task-review.js +8 -53
  32. package/dist/lib/internal/task-validate.js +30 -10
  33. package/dist/lib/owned-path.js +33 -0
  34. package/dist/lib/platform/check-status.js +11 -0
  35. package/dist/lib/platform/comment-safety.js +4 -1
  36. package/dist/lib/platform/completion-backfill.js +1 -1
  37. package/dist/lib/platform/github-data.js +241 -0
  38. package/dist/lib/platform/github-provider.js +15 -20
  39. package/dist/lib/platform/issue-comments.js +2 -2
  40. package/dist/lib/platform/issues.js +1 -179
  41. package/dist/lib/platform/pr-checks.js +12 -65
  42. package/dist/lib/platform/pr-summary.js +55 -3
  43. package/dist/lib/platform/pull-requests.js +2 -105
  44. package/dist/lib/platform/snapshots.js +2 -0
  45. package/dist/lib/platform/verification-sync.js +127 -142
  46. package/dist/lib/release/workflow.js +1 -1
  47. package/dist/lib/sandbox/commands/create.js +7 -2
  48. package/dist/lib/sandbox/commands/rm.js +3 -1514
  49. package/dist/lib/sandbox/control/audit.js +203 -0
  50. package/dist/lib/sandbox/control/client.js +93 -54
  51. package/dist/lib/sandbox/control/executor.js +129 -65
  52. package/dist/lib/sandbox/control/finalization-response.js +27 -0
  53. package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
  54. package/dist/lib/sandbox/control/lifecycle.js +36 -2
  55. package/dist/lib/sandbox/control/native-file-lock.js +6 -98
  56. package/dist/lib/sandbox/control/protocol.js +55 -4
  57. package/dist/lib/sandbox/control/server.js +491 -120
  58. package/dist/lib/sandbox/control/state.js +58 -0
  59. package/dist/lib/sandbox/control/task-workflow.js +74 -0
  60. package/dist/lib/sandbox/control/workflow-executor.js +99 -0
  61. package/dist/lib/sandbox/index.js +2 -2
  62. package/dist/lib/sandbox/recovery.js +28 -4
  63. package/dist/lib/sandbox/removal.js +1717 -0
  64. package/dist/lib/sandbox/task-cleanup.js +217 -0
  65. package/dist/lib/sandbox/workspace-view.js +13 -14
  66. package/dist/lib/task/activity-intent.js +2 -1
  67. package/dist/lib/task/activity-log.js +30 -24
  68. package/dist/lib/task/artifact-command.js +101 -0
  69. package/dist/lib/task/artifact-lifecycle.js +30 -28
  70. package/dist/lib/task/artifact-name.js +36 -0
  71. package/dist/lib/task/artifact-operations.js +23 -69
  72. package/dist/lib/task/artifact-receipts.js +3 -15
  73. package/dist/lib/task/artifact-repair-intent.js +87 -16
  74. package/dist/lib/task/artifact-schema.js +6 -14
  75. package/dist/lib/task/capabilities.js +12 -19
  76. package/dist/lib/task/codex-orchestration.js +71 -25
  77. package/dist/lib/task/commit-identity.js +28 -0
  78. package/dist/lib/task/commit-operation.js +2 -16
  79. package/dist/lib/task/control-authority.js +371 -5
  80. package/dist/lib/task/control-recovery.js +128 -0
  81. package/dist/lib/task/create.js +62 -9
  82. package/dist/lib/task/decision-details.js +2 -46
  83. package/dist/lib/task/events.js +120 -9
  84. package/dist/lib/task/finalization-state.js +9 -0
  85. package/dist/lib/task/human-override.js +3 -6
  86. package/dist/lib/task/implementation-inputs.js +3 -3
  87. package/dist/lib/task/intermediate-cleanup.js +383 -0
  88. package/dist/lib/task/invalidation.js +18 -65
  89. package/dist/lib/task/ledger-intents.js +7 -22
  90. package/dist/lib/task/ledger.js +1 -3
  91. package/dist/lib/task/lifecycle.js +19 -1
  92. package/dist/lib/task/local-artifact-finalization.js +132 -157
  93. package/dist/lib/task/manual-validation-completion.js +26 -0
  94. package/dist/lib/task/manual-validation-evidence.js +130 -0
  95. package/dist/lib/task/manual-validation-receipt.js +101 -0
  96. package/dist/lib/task/manual-validation-shared.js +40 -0
  97. package/dist/lib/task/manual-validation-transaction.js +243 -0
  98. package/dist/lib/task/markdown.js +47 -0
  99. package/dist/lib/task/orchestration.js +14 -17
  100. package/dist/lib/task/qualification-audit.js +20 -34
  101. package/dist/lib/task/review-artifacts.js +2 -18
  102. package/dist/lib/task/review-command.js +25 -0
  103. package/dist/lib/task/review-finalization.js +125 -193
  104. package/dist/lib/task/review-fingerprint.js +2 -2
  105. package/dist/lib/task/sections.js +34 -56
  106. package/dist/lib/task/verification-engine.js +116 -323
  107. package/dist/lib/task/verification.js +1 -1
  108. package/dist/lib/task/workflow-command.js +17 -0
  109. package/dist/lib/task/workflow-warning-intents.js +0 -16
  110. package/dist/lib/task/workflow-warnings.js +28 -2
  111. package/dist/lib/update.js +2 -0
  112. package/dist/lifecycle-build-manifest.json +3 -3
  113. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
  114. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
  115. package/lib/fs/durable-write.ts +29 -0
  116. package/lib/fs/file-lock.ts +113 -0
  117. package/lib/host-control/audit.ts +22 -0
  118. package/lib/host-control/client.ts +172 -0
  119. package/lib/host-control/command.ts +60 -0
  120. package/lib/host-control/path.ts +167 -0
  121. package/lib/host-control/secure-fs.ts +135 -0
  122. package/lib/host-control/server.ts +180 -0
  123. package/lib/host-control/service.ts +133 -0
  124. package/lib/init.ts +15 -2
  125. package/lib/internal/cli-route-inventory.ts +3 -1
  126. package/lib/internal/codex-lifecycle.ts +6 -6
  127. package/lib/internal/demo-promotion.ts +354 -0
  128. package/lib/internal/demo-transcript.ts +419 -0
  129. package/lib/internal/host-control.ts +28 -0
  130. package/lib/internal/manual-validation.ts +306 -0
  131. package/lib/internal/platform-pr.ts +15 -4
  132. package/lib/internal/release-workflow.ts +74 -82
  133. package/lib/internal/sandbox-control.ts +162 -1
  134. package/lib/internal/task-artifact.ts +13 -216
  135. package/lib/internal/task-create.ts +5 -5
  136. package/lib/internal/task-event.ts +20 -11
  137. package/lib/internal/task-operation-registry.ts +111 -3
  138. package/lib/internal/task-review.ts +6 -51
  139. package/lib/internal/task-validate.ts +26 -10
  140. package/lib/owned-path.ts +26 -0
  141. package/lib/platform/check-status.ts +9 -0
  142. package/lib/platform/comment-safety.ts +5 -0
  143. package/lib/platform/completion-backfill.ts +1 -1
  144. package/lib/platform/github-data.ts +322 -0
  145. package/lib/platform/github-provider.ts +15 -20
  146. package/lib/platform/issue-comments.ts +3 -4
  147. package/lib/platform/issues.ts +4 -245
  148. package/lib/platform/pr-checks.ts +13 -77
  149. package/lib/platform/pr-summary.ts +61 -5
  150. package/lib/platform/pull-requests.ts +3 -170
  151. package/lib/platform/snapshots.ts +53 -0
  152. package/lib/platform/verification-sync.ts +145 -155
  153. package/lib/release/workflow.ts +3 -3
  154. package/lib/sandbox/commands/create.ts +7 -6
  155. package/lib/sandbox/commands/rm.ts +3 -1909
  156. package/lib/sandbox/control/audit.ts +253 -0
  157. package/lib/sandbox/control/client.ts +109 -66
  158. package/lib/sandbox/control/executor.ts +134 -74
  159. package/lib/sandbox/control/finalization-response.ts +29 -0
  160. package/lib/sandbox/control/identity-sentinel.ts +137 -0
  161. package/lib/sandbox/control/lifecycle.ts +45 -3
  162. package/lib/sandbox/control/native-file-lock.ts +6 -107
  163. package/lib/sandbox/control/protocol.ts +75 -6
  164. package/lib/sandbox/control/server.ts +538 -123
  165. package/lib/sandbox/control/state.ts +78 -0
  166. package/lib/sandbox/control/task-workflow.ts +95 -0
  167. package/lib/sandbox/control/workflow-executor.ts +102 -0
  168. package/lib/sandbox/index.ts +2 -2
  169. package/lib/sandbox/recovery.ts +28 -5
  170. package/lib/sandbox/removal.ts +2144 -0
  171. package/lib/sandbox/task-cleanup.ts +209 -0
  172. package/lib/sandbox/workspace-view.ts +17 -15
  173. package/lib/task/activity-intent.ts +2 -2
  174. package/lib/task/activity-log.ts +27 -19
  175. package/lib/task/artifact-command.ts +104 -0
  176. package/lib/task/artifact-lifecycle.ts +31 -26
  177. package/lib/task/artifact-name.ts +55 -0
  178. package/lib/task/artifact-operations.ts +22 -68
  179. package/lib/task/artifact-receipts.ts +3 -14
  180. package/lib/task/artifact-repair-intent.ts +99 -18
  181. package/lib/task/artifact-schema.ts +5 -34
  182. package/lib/task/capabilities.ts +13 -22
  183. package/lib/task/codex-orchestration.ts +85 -24
  184. package/lib/task/commit-identity.ts +31 -0
  185. package/lib/task/commit-operation.ts +2 -16
  186. package/lib/task/control-authority.ts +502 -5
  187. package/lib/task/control-recovery.ts +174 -0
  188. package/lib/task/create.ts +63 -8
  189. package/lib/task/decision-details.ts +3 -71
  190. package/lib/task/events.ts +131 -10
  191. package/lib/task/finalization-state.ts +11 -0
  192. package/lib/task/human-override.ts +3 -8
  193. package/lib/task/implementation-inputs.ts +3 -3
  194. package/lib/task/intermediate-cleanup.ts +455 -0
  195. package/lib/task/invalidation.ts +18 -62
  196. package/lib/task/ledger-intents.ts +7 -24
  197. package/lib/task/ledger.ts +1 -3
  198. package/lib/task/lifecycle.ts +17 -1
  199. package/lib/task/local-artifact-finalization.ts +148 -182
  200. package/lib/task/manual-validation-completion.ts +40 -0
  201. package/lib/task/manual-validation-evidence.ts +209 -0
  202. package/lib/task/manual-validation-receipt.ts +152 -0
  203. package/lib/task/manual-validation-shared.ts +50 -0
  204. package/lib/task/manual-validation-transaction.ts +304 -0
  205. package/lib/task/markdown.ts +70 -0
  206. package/lib/task/orchestration.ts +19 -19
  207. package/lib/task/qualification-audit.ts +22 -33
  208. package/lib/task/review-artifacts.ts +1 -20
  209. package/lib/task/review-command.ts +23 -0
  210. package/lib/task/review-finalization.ts +131 -259
  211. package/lib/task/review-fingerprint.ts +2 -2
  212. package/lib/task/sections.ts +33 -54
  213. package/lib/task/verification-engine.ts +110 -366
  214. package/lib/task/verification-types.ts +17 -0
  215. package/lib/task/verification.ts +1 -1
  216. package/lib/task/workflow-command.ts +18 -0
  217. package/lib/task/workflow-warning-intents.ts +0 -13
  218. package/lib/task/workflow-warnings.ts +21 -2
  219. package/lib/update.ts +6 -0
  220. package/package.json +6 -6
  221. package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
  222. package/templates/.agents/rules/testing-discipline.en.md +2 -0
  223. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
  224. package/templates/.agents/scripts/package.json +3 -0
  225. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +32 -20
  226. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +32 -20
  227. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  228. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  229. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
  230. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
  231. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
  232. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
  233. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  234. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  235. package/templates/.agents/skills/release/SKILL.en.md +7 -3
  236. package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
  237. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
  238. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
  239. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
  240. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
  241. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  242. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  243. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  244. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  245. package/templates/.agents/skills/test/SKILL.en.md +17 -6
  246. package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
  247. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  248. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  249. package/dist/lib/platform/adapters.js +0 -56
  250. package/lib/platform/adapters.ts +0 -197
@@ -0,0 +1,304 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+
5
+ import { validateManualValidationReceipt } from './manual-validation-receipt.ts';
6
+ import {
7
+ MANUAL_VALIDATION_ARTIFACT as ARTIFACT,
8
+ MANUAL_VALIDATION_SHA40 as SHA40,
9
+ MANUAL_VALIDATION_SHA64 as SHA64,
10
+ MANUAL_VALIDATION_TASK_ID as TASK_ID,
11
+ exactKeys,
12
+ isRecord,
13
+ validTimestamp,
14
+ writeJsonAtomic
15
+ } from './manual-validation-shared.ts';
16
+
17
+ const MANUAL_VALIDATION_TRANSACTION_SCHEMA = 'agent-infra/manual-validation-transaction';
18
+ const MANUAL_VALIDATION_TRANSACTION_VERSION = 1;
19
+ const TRANSACTION_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
20
+
21
+ type ManualValidationTransactionPhase = 'prepared' | 'summary-staged' | 'receipt-committed' | 'final-promotion-in-progress' | 'committed' | 'aborted' | 'recovery-required';
22
+ type ManualValidationSummaryPreimage = Readonly<{ commentId: number | string | null; body: string; digest: string }>;
23
+ type ManualValidationTransaction = Readonly<{
24
+ schema: typeof MANUAL_VALIDATION_TRANSACTION_SCHEMA;
25
+ version: typeof MANUAL_VALIDATION_TRANSACTION_VERSION;
26
+ transactionId: string;
27
+ taskId: string;
28
+ prNumber: number;
29
+ prHeadSha: string;
30
+ evidenceDigest: string;
31
+ summaryPreimage: ManualValidationSummaryPreimage;
32
+ pendingSummaryDigest: string;
33
+ finalSummaryDigest: string;
34
+ artifact: string;
35
+ phase: ManualValidationTransactionPhase;
36
+ committedReceipt: string | null;
37
+ eventAppended: boolean;
38
+ attempt: number;
39
+ postWriteVerified: boolean;
40
+ createdAt: string;
41
+ updatedAt: string;
42
+ error: string | null;
43
+ }>;
44
+ type ManualValidationTransactionInput = Omit<ManualValidationTransaction, 'schema' | 'version' | 'phase' | 'committedReceipt' | 'eventAppended' | 'postWriteVerified' | 'error'> & {
45
+ transactionId?: string;
46
+ };
47
+ type ManualValidationTransactionPatch = Partial<Pick<ManualValidationTransaction, 'committedReceipt' | 'eventAppended' | 'postWriteVerified' | 'error' | 'updatedAt' | 'attempt'>>;
48
+ type ManualValidationTransactionErrorCode =
49
+ | 'MANUAL_VALIDATION_TRANSACTION_INVALID'
50
+ | 'MANUAL_VALIDATION_TRANSACTION_MISSING'
51
+ | 'MANUAL_VALIDATION_TRANSACTION_IDENTITY_MISMATCH'
52
+ | 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID';
53
+ type ManualValidationTransactionError = { code: ManualValidationTransactionErrorCode; message: string };
54
+ type ManualValidationTransactionResult =
55
+ | { ok: true; value: ManualValidationTransaction }
56
+ | { ok: false; error: ManualValidationTransactionError };
57
+ type ManualValidationGenerationArchiveOptions = Readonly<{
58
+ afterReceiptMove?: () => void;
59
+ }>;
60
+
61
+ function invalid(message: string): ManualValidationTransactionResult {
62
+ return { ok: false, error: { code: 'MANUAL_VALIDATION_TRANSACTION_INVALID', message } };
63
+ }
64
+
65
+ function summaryPreimageDigest(body: string): string {
66
+ return createHash('sha256').update(body, 'utf8').digest('hex');
67
+ }
68
+
69
+ function validateManualValidationTransaction(value: unknown, expected?: Partial<Pick<ManualValidationTransaction, 'transactionId' | 'taskId' | 'prNumber' | 'prHeadSha' | 'evidenceDigest' | 'artifact'>>): ManualValidationTransactionResult {
70
+ if (!isRecord(value)) return invalid('transaction must be a JSON object');
71
+ const keys = ['schema', 'version', 'transactionId', 'taskId', 'prNumber', 'prHeadSha', 'evidenceDigest', 'summaryPreimage', 'pendingSummaryDigest', 'finalSummaryDigest', 'artifact', 'phase', 'committedReceipt', 'eventAppended', 'attempt', 'postWriteVerified', 'createdAt', 'updatedAt', 'error'];
72
+ const keyError = exactKeys(value, keys, 'transaction');
73
+ if (keyError) return invalid(keyError);
74
+ if (value.schema !== MANUAL_VALIDATION_TRANSACTION_SCHEMA || value.version !== MANUAL_VALIDATION_TRANSACTION_VERSION) return invalid('transaction schema or version is unsupported');
75
+ if (typeof value.transactionId !== 'string' || !TRANSACTION_ID.test(value.transactionId)) return invalid('transactionId is invalid');
76
+ if (typeof value.taskId !== 'string' || !TASK_ID.test(value.taskId)) return invalid('taskId is invalid');
77
+ const prNumber = value.prNumber;
78
+ if (!Number.isSafeInteger(prNumber) || (prNumber as number) <= 0) return invalid('prNumber is invalid');
79
+ if (typeof value.prHeadSha !== 'string' || !SHA40.test(value.prHeadSha)) return invalid('prHeadSha is invalid');
80
+ if (typeof value.evidenceDigest !== 'string' || !SHA64.test(value.evidenceDigest)) return invalid('evidenceDigest is invalid');
81
+ if (!isRecord(value.summaryPreimage)) return invalid('summaryPreimage is invalid');
82
+ const preimage = value.summaryPreimage;
83
+ const preimageKeyError = exactKeys(preimage, ['commentId', 'body', 'digest'], 'summaryPreimage');
84
+ if (preimageKeyError) return invalid(preimageKeyError);
85
+ if (preimage.commentId !== null && typeof preimage.commentId !== 'number' && typeof preimage.commentId !== 'string') return invalid('summaryPreimage.commentId is invalid');
86
+ if (typeof preimage.body !== 'string' || typeof preimage.digest !== 'string' || !SHA64.test(preimage.digest) || summaryPreimageDigest(preimage.body) !== preimage.digest) return invalid('summaryPreimage digest is invalid');
87
+ for (const field of ['pendingSummaryDigest', 'finalSummaryDigest'] as const) if (typeof value[field] !== 'string' || !SHA64.test(value[field])) return invalid(`${field} is invalid`);
88
+ if (typeof value.artifact !== 'string' || !ARTIFACT.test(value.artifact)) return invalid('artifact is invalid');
89
+ if (!['prepared', 'summary-staged', 'receipt-committed', 'final-promotion-in-progress', 'committed', 'aborted', 'recovery-required'].includes(value.phase as string)) return invalid('phase is invalid');
90
+ if (value.committedReceipt !== null && (typeof value.committedReceipt !== 'string' || !SHA64.test(value.committedReceipt))) return invalid('committedReceipt is invalid');
91
+ if (typeof value.eventAppended !== 'boolean' || typeof value.postWriteVerified !== 'boolean') return invalid('transaction completion flags are invalid');
92
+ const attempt = value.attempt;
93
+ if (!Number.isSafeInteger(attempt) || (attempt as number) < 1) return invalid('attempt is invalid');
94
+ if (!validTimestamp(value.createdAt) || !validTimestamp(value.updatedAt)) return invalid('transaction timestamps are invalid');
95
+ if (value.error !== null && (typeof value.error !== 'string' || !value.error || /[\r\n]/u.test(value.error))) return invalid('transaction error is invalid');
96
+ if (['receipt-committed', 'final-promotion-in-progress', 'committed'].includes(value.phase as string) && value.committedReceipt === null) return invalid('receipt-backed phases require committedReceipt');
97
+ if (['final-promotion-in-progress', 'committed'].includes(value.phase as string) && !value.eventAppended) return invalid('final promotion requires an appended completion event');
98
+ if (value.phase === 'committed' && !value.postWriteVerified) return invalid('committed phase requires post-write verification');
99
+ const transaction = value as unknown as ManualValidationTransaction;
100
+ if (expected && Object.entries(expected).some(([key, expectedValue]) => expectedValue !== undefined && transaction[key as keyof ManualValidationTransaction] !== expectedValue)) {
101
+ return { ok: false, error: { code: 'MANUAL_VALIDATION_TRANSACTION_IDENTITY_MISMATCH', message: 'transaction identity does not match the expected operation' } };
102
+ }
103
+ return { ok: true, value: transaction };
104
+ }
105
+
106
+ function createManualValidationTransaction(input: ManualValidationTransactionInput): ManualValidationTransaction {
107
+ const transaction: ManualValidationTransaction = {
108
+ schema: MANUAL_VALIDATION_TRANSACTION_SCHEMA,
109
+ version: MANUAL_VALIDATION_TRANSACTION_VERSION,
110
+ transactionId: input.transactionId ?? `mv-${randomUUID()}`,
111
+ taskId: input.taskId,
112
+ prNumber: input.prNumber,
113
+ prHeadSha: input.prHeadSha,
114
+ evidenceDigest: input.evidenceDigest,
115
+ summaryPreimage: input.summaryPreimage,
116
+ pendingSummaryDigest: input.pendingSummaryDigest,
117
+ finalSummaryDigest: input.finalSummaryDigest,
118
+ artifact: input.artifact,
119
+ phase: 'prepared',
120
+ committedReceipt: null,
121
+ eventAppended: false,
122
+ attempt: input.attempt,
123
+ postWriteVerified: false,
124
+ createdAt: input.createdAt,
125
+ updatedAt: input.updatedAt,
126
+ error: null
127
+ };
128
+ const checked = validateManualValidationTransaction(transaction);
129
+ if (!checked.ok) throw new Error(`${checked.error.code}: ${checked.error.message}`);
130
+ return checked.value;
131
+ }
132
+
133
+ const allowedTransitions: Record<ManualValidationTransactionPhase, readonly ManualValidationTransactionPhase[]> = {
134
+ prepared: ['prepared', 'summary-staged', 'aborted', 'recovery-required'],
135
+ 'summary-staged': ['summary-staged', 'receipt-committed', 'aborted', 'recovery-required'],
136
+ 'receipt-committed': ['receipt-committed', 'final-promotion-in-progress', 'aborted', 'recovery-required'],
137
+ 'final-promotion-in-progress': ['final-promotion-in-progress', 'committed', 'aborted', 'recovery-required'],
138
+ committed: ['committed'],
139
+ aborted: ['aborted', 'prepared'],
140
+ 'recovery-required': ['recovery-required', 'prepared']
141
+ };
142
+
143
+ function transitionManualValidationTransaction(
144
+ current: ManualValidationTransaction,
145
+ phase: ManualValidationTransactionPhase,
146
+ patch: ManualValidationTransactionPatch = {}
147
+ ): ManualValidationTransactionResult {
148
+ if (!allowedTransitions[current.phase].includes(phase)) return { ok: false, error: { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: `cannot transition from ${current.phase} to ${phase}` } };
149
+ if (phase === 'final-promotion-in-progress' && (!current.eventAppended && patch.eventAppended !== true)) return { ok: false, error: { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: 'final promotion requires an appended completion event' } };
150
+ const next: ManualValidationTransaction = {
151
+ ...current,
152
+ phase,
153
+ committedReceipt: patch.committedReceipt !== undefined ? patch.committedReceipt : current.committedReceipt,
154
+ eventAppended: patch.eventAppended ?? current.eventAppended,
155
+ postWriteVerified: patch.postWriteVerified ?? current.postWriteVerified,
156
+ attempt: patch.attempt ?? current.attempt,
157
+ error: patch.error !== undefined ? patch.error : current.error,
158
+ updatedAt: patch.updatedAt ?? new Date().toISOString()
159
+ };
160
+ const checked = validateManualValidationTransaction(next);
161
+ if (!checked.ok) return checked;
162
+ return checked;
163
+ }
164
+
165
+ function retryManualValidationTransaction(current: ManualValidationTransaction, now = new Date().toISOString()): ManualValidationTransactionResult {
166
+ if (!['aborted', 'recovery-required'].includes(current.phase)) return { ok: false, error: { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: 'only aborted or recovery-required transactions can be retried' } };
167
+ return transitionManualValidationTransaction(current, 'prepared', {
168
+ updatedAt: now,
169
+ error: null,
170
+ committedReceipt: null,
171
+ eventAppended: false,
172
+ postWriteVerified: false,
173
+ attempt: current.attempt + 1
174
+ });
175
+ }
176
+
177
+ function manualValidationTransactionPath(taskDir: string): string {
178
+ return path.join(taskDir, '.manual-validation', 'transaction.json');
179
+ }
180
+
181
+ function manualValidationGenerationPaths(taskDir: string, transaction: ManualValidationTransaction): {
182
+ receipt: { current: string; history: string };
183
+ transaction: { current: string; history: string };
184
+ } {
185
+ const stateDir = path.join(taskDir, '.manual-validation');
186
+ const historyDir = path.join(stateDir, 'history');
187
+ return {
188
+ receipt: {
189
+ current: path.join(stateDir, 'receipt.json'),
190
+ history: path.join(historyDir, `receipt-${transaction.transactionId}-attempt-${transaction.attempt}.json`)
191
+ },
192
+ transaction: {
193
+ current: manualValidationTransactionPath(taskDir),
194
+ history: path.join(historyDir, `transaction-${transaction.transactionId}-attempt-${transaction.attempt}.json`)
195
+ }
196
+ };
197
+ }
198
+
199
+ function readReceiptAt(file: string, transaction: ManualValidationTransaction): 'missing' | 'valid' {
200
+ let value: unknown;
201
+ try {
202
+ value = JSON.parse(fs.readFileSync(file, 'utf8')) as unknown;
203
+ } catch (error) {
204
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing';
205
+ throw new Error(`manual-validation receipt is unreadable: ${error instanceof Error ? error.message : String(error)}`);
206
+ }
207
+ const checked = validateManualValidationReceipt(value, {
208
+ transactionId: transaction.transactionId,
209
+ taskId: transaction.taskId,
210
+ prNumber: transaction.prNumber,
211
+ prHeadSha: transaction.prHeadSha,
212
+ evidenceDigest: transaction.evidenceDigest,
213
+ artifact: transaction.artifact
214
+ });
215
+ if (!checked.ok) throw new Error(`manual-validation receipt is invalid: ${checked.error.message}`);
216
+ if (transaction.committedReceipt !== null && checked.value.receiptDigest !== transaction.committedReceipt) {
217
+ throw new Error('manual-validation receipt does not match the committed transaction');
218
+ }
219
+ return 'valid';
220
+ }
221
+
222
+ function validateManualValidationGenerationArchive(taskDir: string, transaction: ManualValidationTransaction, requireReceipt = false): void {
223
+ const paths = manualValidationGenerationPaths(taskDir, transaction);
224
+ const currentTransaction = fs.existsSync(paths.transaction.current);
225
+ const archivedTransaction = fs.existsSync(paths.transaction.history);
226
+ if (currentTransaction && archivedTransaction) throw new Error(`manual-validation archive has conflicting transaction files for ${transaction.transactionId}`);
227
+ if (!currentTransaction && !archivedTransaction) throw new Error(`manual-validation archive source is missing: ${paths.transaction.current}`);
228
+ const currentReceipt = requireReceipt
229
+ ? readReceiptAt(paths.receipt.current, transaction)
230
+ : (fs.existsSync(paths.receipt.current) ? 'valid' : 'missing');
231
+ const archivedReceipt = requireReceipt
232
+ ? readReceiptAt(paths.receipt.history, transaction)
233
+ : (fs.existsSync(paths.receipt.history) ? 'valid' : 'missing');
234
+ if (requireReceipt && currentReceipt === 'missing' && archivedReceipt === 'missing') {
235
+ throw new Error(`manual-validation receipt is missing for transaction ${transaction.transactionId}`);
236
+ }
237
+ if (currentReceipt === 'valid' && archivedReceipt === 'valid') {
238
+ throw new Error(`manual-validation archive has conflicting receipt files for ${transaction.transactionId}`);
239
+ }
240
+ }
241
+
242
+ function archiveManualValidationGeneration(
243
+ taskDir: string,
244
+ transaction: ManualValidationTransaction,
245
+ requireReceipt = false,
246
+ options: ManualValidationGenerationArchiveOptions = {}
247
+ ): void {
248
+ const paths = manualValidationGenerationPaths(taskDir, transaction);
249
+ validateManualValidationGenerationArchive(taskDir, transaction, requireReceipt);
250
+ fs.mkdirSync(path.dirname(paths.receipt.history), { recursive: true });
251
+ const move = (source: string, target: string, required: boolean): void => {
252
+ if (!fs.existsSync(source)) {
253
+ if (fs.existsSync(target) || !required) return;
254
+ throw new Error(`manual-validation archive source is missing: ${source}`);
255
+ }
256
+ if (fs.existsSync(target)) throw new Error(`manual-validation archive target already exists: ${target}`);
257
+ fs.renameSync(source, target);
258
+ };
259
+ move(paths.receipt.current, paths.receipt.history, requireReceipt);
260
+ options.afterReceiptMove?.();
261
+ move(paths.transaction.current, paths.transaction.history, true);
262
+ }
263
+
264
+ function writeManualValidationTransactionAtomic(taskDir: string, transaction: ManualValidationTransaction): string {
265
+ const checked = validateManualValidationTransaction(transaction);
266
+ if (!checked.ok) throw new Error(`${checked.error.code}: ${checked.error.message}`);
267
+ const target = manualValidationTransactionPath(taskDir);
268
+ return writeJsonAtomic(target, transaction);
269
+ }
270
+
271
+ function readManualValidationTransaction(taskDir: string, expected?: Parameters<typeof validateManualValidationTransaction>[1]): ManualValidationTransactionResult {
272
+ const file = manualValidationTransactionPath(taskDir);
273
+ let value: unknown;
274
+ try { value = JSON.parse(fs.readFileSync(file, 'utf8')) as unknown; }
275
+ catch (error) {
276
+ return { ok: false, error: { code: (error as NodeJS.ErrnoException).code === 'ENOENT' ? 'MANUAL_VALIDATION_TRANSACTION_MISSING' : 'MANUAL_VALIDATION_TRANSACTION_INVALID', message: error instanceof Error ? error.message : String(error) } };
277
+ }
278
+ return validateManualValidationTransaction(value, expected);
279
+ }
280
+
281
+ export {
282
+ MANUAL_VALIDATION_TRANSACTION_SCHEMA,
283
+ MANUAL_VALIDATION_TRANSACTION_VERSION,
284
+ archiveManualValidationGeneration,
285
+ createManualValidationTransaction,
286
+ manualValidationTransactionPath,
287
+ validateManualValidationGenerationArchive,
288
+ readManualValidationTransaction,
289
+ retryManualValidationTransaction,
290
+ summaryPreimageDigest,
291
+ transitionManualValidationTransaction,
292
+ validateManualValidationTransaction,
293
+ writeManualValidationTransactionAtomic
294
+ };
295
+ export type {
296
+ ManualValidationSummaryPreimage,
297
+ ManualValidationTransaction,
298
+ ManualValidationTransactionError,
299
+ ManualValidationTransactionErrorCode,
300
+ ManualValidationTransactionInput,
301
+ ManualValidationTransactionPatch,
302
+ ManualValidationTransactionPhase,
303
+ ManualValidationTransactionResult
304
+ };
@@ -0,0 +1,70 @@
1
+ type SourceLine = {
2
+ start: number;
3
+ end: number;
4
+ text: string;
5
+ };
6
+
7
+ type VisibleHeading = SourceLine & {
8
+ level: number;
9
+ text: string;
10
+ };
11
+
12
+ type VisibleAnchor = SourceLine & {
13
+ id: string;
14
+ };
15
+
16
+ type VisibleMarkdown = {
17
+ hasUnclosedFence: boolean;
18
+ lines: SourceLine[];
19
+ headings: VisibleHeading[];
20
+ anchors: VisibleAnchor[];
21
+ };
22
+
23
+ const HEADING = /^ {0,3}(#{1,6})[ \t]+(.+?)\s*$/;
24
+ const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/;
25
+ const FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})\s*$/;
26
+ const ANCHOR = /^\s*<a\s+id=(['"])([^'"]+)\1\s*><\/a>\s*$/;
27
+
28
+ function sourceLines(content: string): SourceLine[] {
29
+ const lines: SourceLine[] = [];
30
+ let start = 0;
31
+ while (start < content.length) {
32
+ const newline = content.indexOf('\n', start);
33
+ const end = newline === -1 ? content.length : newline;
34
+ lines.push({ start, end, text: content.slice(start, end).replace(/\r$/, '') });
35
+ start = newline === -1 ? content.length : newline + 1;
36
+ }
37
+ return lines;
38
+ }
39
+
40
+ function scanVisibleMarkdown(content: string): VisibleMarkdown {
41
+ const lines = sourceLines(content);
42
+ const visible: SourceLine[] = [];
43
+ const headings: VisibleHeading[] = [];
44
+ const anchors: VisibleAnchor[] = [];
45
+ let fence: { character: '`' | '~'; length: number } | null = null;
46
+
47
+ for (const line of lines) {
48
+ if (fence) {
49
+ const close = line.text.match(FENCE_CLOSE);
50
+ if (close && close[1]![0] === fence.character && close[1]!.length >= fence.length) fence = null;
51
+ continue;
52
+ }
53
+ const open = line.text.match(FENCE_OPEN);
54
+ const marker = open?.[1];
55
+ const info = open?.[2] ?? '';
56
+ if (marker && !(marker[0] === '`' && info.includes('`'))) {
57
+ fence = { character: marker[0] as '`' | '~', length: marker.length };
58
+ continue;
59
+ }
60
+ visible.push(line);
61
+ const heading = line.text.match(HEADING);
62
+ if (heading) headings.push({ ...line, level: heading[1]!.length, text: heading[2]!.trim() });
63
+ const anchor = line.text.match(ANCHOR);
64
+ if (anchor) anchors.push({ ...line, id: anchor[2]! });
65
+ }
66
+ return { lines: visible, headings, anchors, hasUnclosedFence: fence !== null };
67
+ }
68
+
69
+ export { scanVisibleMarkdown };
70
+ export type { SourceLine, VisibleHeading, VisibleMarkdown };
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
4
 
5
+ import { artifactName, maxArtifactRound } from './artifact-name.ts';
5
6
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
6
7
  import { validateCurrentTaskContract } from './current-contract.ts';
7
8
  import { parseReviewSummary } from './review-artifacts.ts';
@@ -39,6 +40,7 @@ import type {
39
40
  OrchestrationModelPolicy,
40
41
  OrchestrationRolePolicy
41
42
  } from '../agent-clients/types.ts';
43
+ import type { LifecycleRecoveryAttestationV1 } from './control-authority.ts';
42
44
  import {
43
45
  captureRepositorySnapshot,
44
46
  captureWorkspaceSnapshot,
@@ -173,6 +175,7 @@ type OrchestrationOptions = {
173
175
  supportsLifecycleDelegation?: (client: AgentClientId) => boolean;
174
176
  validateLifecycleCapability?: () => Exclude<OrchestrationResult['error'], null> | null;
175
177
  consumeLifecycleCapability?: () => Exclude<OrchestrationResult['error'], null> | null;
178
+ lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
176
179
  token?: () => string;
177
180
  monotonicNow?: () => number;
178
181
  sleep?: (milliseconds: number) => Promise<void>;
@@ -275,7 +278,7 @@ function isRecovery(value: unknown): value is OrchestrationRecovery {
275
278
  && value.resultingStatus === 'running';
276
279
  }
277
280
 
278
- function isCompletionEvidence(value: unknown): value is CleanCompletionEvidence {
281
+ export function isCompletionEvidence(value: unknown): value is CleanCompletionEvidence {
279
282
  return hasExactKeys(value, [
280
283
  'kind', 'observedAt', 'head', 'headTree', 'worktreeTree', 'lastReviewedCommit', 'prNumber', 'prHead'
281
284
  ])
@@ -604,25 +607,13 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
604
607
  return { status: 'running', changed: true, taskId: resolved.taskId, run, next: null, error: null };
605
608
  }
606
609
 
607
- function highestRound(taskDir: string, family: string): number {
608
- const pattern = new RegExp(`^${family}(?:-r(\\d+))?\\.md$`);
609
- return fs.readdirSync(taskDir).reduce((max, name) => {
610
- const match = pattern.exec(name);
611
- return match ? Math.max(max, match[1] ? Number(match[1]) : 1) : max;
612
- }, 0);
613
- }
614
-
615
- function artifactName(family: string, round: number): string {
616
- return round === 1 ? `${family}.md` : `${family}-r${round}.md`;
617
- }
618
-
619
610
  function validateSavedCurrentDiffBase(
620
611
  repoRoot: string,
621
612
  metadata: ReturnType<typeof parseTypedTaskFrontmatter>,
622
613
  taskDir: string,
623
614
  reviewedHead: string
624
615
  ): Readonly<{ code: string; message: string }> | null {
625
- const reviewRound = highestRound(taskDir, 'review-code');
616
+ const reviewRound = maxArtifactRound(fs.readdirSync(taskDir), 'review-code');
626
617
  const reviewContent = fs.readFileSync(path.join(taskDir, artifactName('review-code', reviewRound)), 'utf8');
627
618
  const savedTargetHead = extractReviewTargetHead(reviewContent);
628
619
  const savedDiffBase = extractReviewDiffBase(reviewContent);
@@ -660,10 +651,9 @@ function routeFromFacts(facts: LifecycleFacts): Omit<OrchestrationNext, 'request
660
651
  const action = recommendation.action;
661
652
  if (action === 'manual-validation' || action === 'validation-run') return null;
662
653
  const family = action === 'analysis' ? 'analysis' : action;
663
- const round = Math.max(0, ...[...(facts.artifacts[family] ?? []), ...(facts.staleArtifacts?.[family] ?? [])].map((name) => {
664
- const match = /-r(\d+)\.md$/.exec(name);
665
- return match ? Number(match[1]) : 1;
666
- })) + 1;
654
+ const round = maxArtifactRound([
655
+ ...(facts.artifacts[family] ?? []), ...(facts.staleArtifacts?.[family] ?? [])
656
+ ], family) + 1;
667
657
  const command = action === 'analysis' ? 'analyze-task' : action === 'plan' ? 'plan-task' : action === 'code' ? 'code-task' : action;
668
658
  const role = action.startsWith('review-') ? 'reviewer' : 'executor';
669
659
  return { action: command, role, stage: action, round, artifact: artifactName(family, round) };
@@ -719,7 +709,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
719
709
  const routed = routeFromFacts(facts.facts);
720
710
  if (!routed) return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
721
711
  if ('completion' in routed) {
722
- const reviewRound = highestRound(resolved.taskDir, 'review-code');
712
+ const reviewRound = maxArtifactRound(fs.readdirSync(resolved.taskDir), 'review-code');
723
713
  const review = parseReviewSummary(fs.readFileSync(
724
714
  path.join(resolved.taskDir, artifactName('review-code', reviewRound)),
725
715
  'utf8'
@@ -830,6 +820,7 @@ function prepareOrchestrationDelegationUnlocked(
830
820
  requestedModel?: string;
831
821
  requestedReasoningEffort?: string;
832
822
  lifecycleProvenance?: DelegationLifecycleProvenance;
823
+ lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
833
824
  }>,
834
825
  options: OrchestrationOptions = {}
835
826
  ): OrchestrationResult {
@@ -866,6 +857,13 @@ function prepareOrchestrationDelegationUnlocked(
866
857
  if (input.client === 'codex' && !input.lifecycleProvenance) {
867
858
  return failed('ORCHESTRATION_CODEX_PROVENANCE_REQUIRED', 'Codex prepare requires lifecycle provenance', resolved.taskId);
868
859
  }
860
+ if (input.lifecycleRecoveryAttestation
861
+ && (input.lifecycleRecoveryAttestation.phase !== 'orchestration.prepare'
862
+ || input.lifecycleRecoveryAttestation.taskId !== resolved.taskId
863
+ || input.lifecycleRecoveryAttestation.artifact !== next.artifact
864
+ || input.lifecycleRecoveryAttestation.round !== next.round)) {
865
+ return failed('LIFECYCLE_AUTHORITY_TUPLE_MISMATCH', 'orchestration authority does not match the routed stage', resolved.taskId);
866
+ }
869
867
  const expectedPolicy = run.modelPolicy[next.role];
870
868
  if (input.requestedModel !== expectedPolicy.model) {
871
869
  return failed('ORCHESTRATION_REQUESTED_MODEL_MISMATCH', `requested model does not match the persisted ${next.role} model`, resolved.taskId);
@@ -930,6 +928,7 @@ function prepareOrchestrationDelegation(
930
928
  requestedModel?: string;
931
929
  requestedReasoningEffort?: string;
932
930
  lifecycleProvenance?: DelegationLifecycleProvenance;
931
+ lifecycleRecoveryAttestation?: LifecycleRecoveryAttestationV1 | null;
933
932
  }>,
934
933
  options: OrchestrationOptions = {}
935
934
  ): OrchestrationResult {
@@ -1606,6 +1605,7 @@ export {
1606
1605
  statusOrchestration,
1607
1606
  };
1608
1607
  export type {
1608
+ CleanCompletionEvidence,
1609
1609
  OrchestrationCompletionPlanResult,
1610
1610
  OrchestrationModelPolicy,
1611
1611
  OrchestrationNext,
@@ -1,7 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
2
 
3
+ import { parseArtifactName } from './artifact-name.ts';
3
4
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
4
- import { parseTable } from './sections.ts';
5
+ import { findSectionRange, parseTable } from './sections.ts';
6
+ import { scanVisibleMarkdown } from './markdown.ts';
5
7
 
6
8
  const TASK_CONSTRAINT_HEADINGS = ['约束', 'Constraints'] as const;
7
9
  const TASK_CANDIDATE_HEADINGS = ['候选与否决方案', 'Candidate and Rejected Options', 'Candidates and Rejected Options', 'Candidates and Rejected Alternatives'] as const;
@@ -170,21 +172,9 @@ function taskInputDigest(qualification: Pick<TaskQualification, 'constraints' |
170
172
  return digest(canonicalTaskInput(qualification));
171
173
  }
172
174
 
173
- function sectionBody(content: string, aliases: readonly string[], level = 2): string | null {
174
- const heading = aliases.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
175
- const marker = '#'.repeat(level);
176
- const match = new RegExp(`^${marker}\\s+(${heading})\\s*$`, 'm').exec(content);
177
- if (!match) return null;
178
- const rest = content.slice((match.index ?? 0) + match[0].length);
179
- const end = rest.search(new RegExp(`^#{2,${level}}\\s+`, 'm'));
180
- return rest.slice(0, end < 0 ? rest.length : end);
181
- }
182
-
183
- function stripSection(content: string, aliases: readonly string[]): string {
184
- const body = sectionBody(content, aliases);
185
- if (body === null) return normalizeText(content);
186
- const heading = aliases.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
187
- return normalizeText(content.replace(new RegExp(`^##\\s+(?:${heading})\\s*$[\\s\\S]*?(?=^##\\s+|(?![\\s\\S]))`, 'm'), ''));
175
+ function sectionBody(content: string, aliases: readonly string[], level: 2 | 3 = 2): string | null {
176
+ const section = findSectionRange(content, aliases, level);
177
+ return section ? content.slice(section.bodyStart, section.end) : null;
188
178
  }
189
179
 
190
180
  function stripFrontmatter(content: string): string {
@@ -218,7 +208,7 @@ function nonConstraintInputDigest(content: string): string {
218
208
  ['返工意图', 'Rework Intents'],
219
209
  CONFIRMATION_HEADINGS,
220
210
  ['完成检查清单', 'Completion Checklist']
221
- ].reduce((value, aliases) => stripSection(value, aliases), projection);
211
+ ].reduce((value, aliases) => stripSectionAtLevel(value, aliases, 2), projection);
222
212
  return digest(projection);
223
213
  }
224
214
 
@@ -231,12 +221,9 @@ function stripTaskInputSection(content: string, aliases: readonly string[]): str
231
221
  return stripSectionAtLevel(result, aliases, 2);
232
222
  }
233
223
 
234
- function stripSectionAtLevel(content: string, aliases: readonly string[], level: number): string {
235
- const body = sectionBody(content, aliases, level);
236
- if (body === null) return normalizeText(content);
237
- const heading = aliases.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
238
- const marker = '#'.repeat(level);
239
- return normalizeText(content.replace(new RegExp(`^${marker}\\s+(?:${heading})\\s*$[\\s\\S]*?(?=^#{2,${level}}\\s+|(?![\\s\\S]))`, 'm'), ''));
224
+ function stripSectionAtLevel(content: string, aliases: readonly string[], level: 2 | 3): string {
225
+ const section = findSectionRange(content, aliases, level);
226
+ return normalizeText(section ? content.slice(0, section.start) + content.slice(section.end) : content);
240
227
  }
241
228
 
242
229
  function parseTaskQualification(content: string): { ok: true; qualification: TaskQualification } | { ok: false; code: string; message: string } {
@@ -246,12 +233,14 @@ function parseTaskQualification(content: string): { ok: true; qualification: Tas
246
233
  const hasCandidateHeading = candidateBody !== null;
247
234
  if ((!hasConstraintHeading && !hasCandidateHeading)
248
235
  || (hasConstraintHeading && hasCandidateHeading
249
- && !/^\s*\|/m.test(constraintBody!) && !/^\s*\|/m.test(candidateBody!))) {
236
+ && !scanVisibleMarkdown(constraintBody!).lines.some((line) => line.text.trim().startsWith('|'))
237
+ && !scanVisibleMarkdown(candidateBody!).lines.some((line) => line.text.trim().startsWith('|')))) {
250
238
  return { ok: true, qualification: { present: false, constraints: [], candidates: [], constraintDigest: digest([]), taskInputDigest: digest({ constraints: [], candidates: [] }), nonConstraintInputDigest: nonConstraintInputDigest(content) } };
251
239
  }
252
240
  if (!hasConstraintHeading || !hasCandidateHeading) {
253
- const hasCanonicalTable = new RegExp(`\\|\\s*${CONSTRAINT_COLUMNS.join('\\s*\\|\\s*')}\\s*\\|`).test(content)
254
- || new RegExp(`\\|\\s*${CANDIDATE_COLUMNS.join('\\s*\\|\\s*')}\\s*\\|`).test(content);
241
+ const visible = scanVisibleMarkdown(content).lines.map((line) => line.text).join('\n');
242
+ const hasCanonicalTable = new RegExp(`\\|\\s*${CONSTRAINT_COLUMNS.join('\\s*\\|\\s*')}\\s*\\|`).test(visible)
243
+ || new RegExp(`\\|\\s*${CANDIDATE_COLUMNS.join('\\s*\\|\\s*')}\\s*\\|`).test(visible);
255
244
  if (hasCanonicalTable) return { ok: false, code: 'QUALIFICATION_TASK_CONTRACT_INVALID', message: 'task qualification requires both constraints and candidates sections' };
256
245
  return { ok: true, qualification: { present: false, constraints: [], candidates: [], constraintDigest: digest([]), taskInputDigest: digest({ constraints: [], candidates: [] }), nonConstraintInputDigest: nonConstraintInputDigest(content) } };
257
246
  }
@@ -315,10 +304,10 @@ function parseAuditTable(content: string, heading: (typeof AUDIT_SUBSECTIONS)[nu
315
304
  return table?.rows.map((row) => ({ ...row.values })) ?? null;
316
305
  }
317
306
 
318
- function parseArtifactName(value: string): { family: ArtifactFamily; round: number } | null {
319
- const match = /^(analysis|review-analysis|plan|review-plan|code|review-code)(?:-r([2-9]|[1-9]\d+))?\.md$/.exec(value);
320
- if (!match) return null;
321
- return { family: match[1] as ArtifactFamily, round: match[2] ? Number(match[2]) : 1 };
307
+ function parseAuditArtifactName(value: string): { family: ArtifactFamily; round: number } | null {
308
+ const identity = parseArtifactName(value);
309
+ return identity && ARTIFACT_FAMILIES.includes(identity.family as ArtifactFamily)
310
+ ? { family: identity.family as ArtifactFamily, round: identity.round } : null;
322
311
  }
323
312
 
324
313
  function upstreamArtifactDigest(rows: readonly UpstreamRelation[]): string {
@@ -327,7 +316,7 @@ function upstreamArtifactDigest(rows: readonly UpstreamRelation[]): string {
327
316
  }
328
317
 
329
318
  function validateUpstreamRelation(row: UpstreamRelation): void {
330
- const identity = parseArtifactName(row.upstreamArtifact);
319
+ const identity = parseAuditArtifactName(row.upstreamArtifact);
331
320
  if (!ARTIFACT_FAMILIES.includes(row.upstreamFamily) || !identity || identity.family !== row.upstreamFamily
332
321
  || identity.round !== row.upstreamRound || !Number.isSafeInteger(row.upstreamRound)
333
322
  || !/^[a-f0-9]{64}$/i.test(row.upstreamSha256) || !RELATIONS.includes(row.relation)) {
@@ -368,7 +357,7 @@ function expectedQualificationRelations(
368
357
  const inputName = family === 'code' ? frontmatter.code_input_artifact : family.startsWith('review-') ? frontmatter.review_input_artifact : '';
369
358
  const inputSha256 = family === 'code' ? frontmatter.code_input_sha256 : family.startsWith('review-') ? frontmatter.review_input_sha256 : '';
370
359
  if (typeof inputName !== 'string' || !inputName || typeof inputSha256 !== 'string' || !inputSha256) return { ok: true, relations: undefined };
371
- const identity = parseArtifactName(inputName);
360
+ const identity = parseAuditArtifactName(inputName);
372
361
  if (!identity) return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: `started input '${inputName}' is not canonical` };
373
362
  const relation: UpstreamRelation = {
374
363
  upstreamFamily: identity.family, upstreamArtifact: inputName, upstreamRound: identity.round,
@@ -507,7 +496,7 @@ function validateQualificationAudit(
507
496
  || upstreamArtifactDigest(options.expectedUpstreamRelations) !== upstreamArtifactDigest(audit.audit.upstreamRelations)
508
497
  )) return { ok: false, code: 'QUALIFICATION_UPSTREAM_RELATION_MISMATCH', message: 'qualification audit upstream relations do not match the started artifact inputs' };
509
498
  if (options.family && options.artifact) {
510
- const identity = parseArtifactName(options.artifact);
499
+ const identity = parseAuditArtifactName(options.artifact);
511
500
  if (!identity || identity.family !== options.family) return { ok: false, code: 'QUALIFICATION_ARTIFACT_IDENTITY_INVALID', message: `artifact '${options.artifact}' is not canonical for '${options.family}'` };
512
501
  }
513
502
  return { ok: true, qualification: task.qualification, audit: audit.audit };
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import { scanVisibleMarkdown } from './decision-details.ts';
4
+ import { scanVisibleMarkdown } from './markdown.ts';
5
5
 
6
6
  type ReviewFindingCounts = {
7
7
  blocker: number;
@@ -53,23 +53,6 @@ function escapeRegExp(value: string): string {
53
53
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
54
54
  }
55
55
 
56
- function maxRound(entries: string[], stem: string): number {
57
- let max = 0;
58
- for (const entry of entries) {
59
- if (entry === `${stem}.md`) {
60
- max = Math.max(max, 1);
61
- continue;
62
- }
63
- const match = entry.match(new RegExp(`^${escapeRegExp(stem)}-r(\\d+)\\.md$`));
64
- if (match) max = Math.max(max, Number(match[1]));
65
- }
66
- return max;
67
- }
68
-
69
- function artifactName(stem: string, round: number): string {
70
- return round === 1 ? `${stem}.md` : `${stem}-r${round}.md`;
71
- }
72
-
73
56
  function normalizeVerdict(raw: unknown): ReviewVerdict | '' {
74
57
  const value = String(raw).trim().toLowerCase();
75
58
  if (value === '通过' || value === 'approved') return 'Approved';
@@ -260,12 +243,10 @@ function parseVerdict(reviewPath: string): PathVerdictResult {
260
243
  }
261
244
 
262
245
  export {
263
- artifactName,
264
246
  equalCounts,
265
247
  escapeRegExp,
266
248
  extractSection,
267
249
  finalizeReviewSummaryContent,
268
- maxRound,
269
250
  normalizeVerdict,
270
251
  parseReviewSummary,
271
252
  parseVerdict,