@tea-agent/loop-agent 0.41.0-next.3 → 0.41.1-beta.0

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 (200) hide show
  1. package/CHANGELOG.md +264 -32
  2. package/dist/application/dag/generate-task-dag.js +75 -19
  3. package/dist/application/task-lifecycle/advance.js +24 -5
  4. package/dist/application/task-lifecycle/observe.js +171 -17
  5. package/dist/application/task-lifecycle/plan-transitions.js +42 -7
  6. package/dist/application/task-lifecycle/recommendations.js +13 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +7 -0
  9. package/dist/cli/program.js +5 -0
  10. package/dist/commands/client-recovery.js +3 -0
  11. package/dist/commands/dag-follow-up.js +138 -0
  12. package/dist/commands/init.js +27 -1
  13. package/dist/commands/task-advance.js +19 -0
  14. package/dist/executors/dag-pi-executor.js +3784 -231
  15. package/dist/executors/pi-executor.js +15 -4
  16. package/dist/executors/pi-read-budget-policy.js +239 -0
  17. package/dist/executors/shell-executor.js +945 -217
  18. package/dist/executors/shell-write-guard.js +7 -0
  19. package/dist/shared/dag-failure-category.js +12 -0
  20. package/dist/shared/openspec-spec.js +70 -4
  21. package/dist/shared/operator/capabilities.js +7 -0
  22. package/dist/task/config-types.js +34 -7
  23. package/dist/task/contract/adopt.js +4 -0
  24. package/dist/task/contract/import-revision.js +4 -0
  25. package/dist/task/contract/project.js +3 -0
  26. package/dist/task/contract/schema.js +2 -1
  27. package/dist/task/frontend-project-capability.js +203 -20
  28. package/dist/task/runtime.js +5 -2
  29. package/dist/task/source-prepare/build-draft.js +3 -3
  30. package/dist/task/source-prepare/fragment-inventory.js +15 -9
  31. package/dist/task/source-prepare/prepare.js +89 -1
  32. package/dist/task/source-prepare/semantic-intake.js +6 -2
  33. package/dist/task/source-references.js +22 -1
  34. package/dist/worker/console/chat/pi-runtime.js +2 -3
  35. package/dist/worker/console/chat/routes.js +3 -12
  36. package/dist/worker/console/chat/semantic-activity.js +11 -12
  37. package/dist/worker/console/chat/session-stats.js +0 -96
  38. package/dist/worker/console/chat/turn-process.js +4 -28
  39. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  40. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  41. package/dist/worker/console/index.js +3 -0
  42. package/dist/worker/console/operation-store.js +226 -6
  43. package/dist/worker/console/operator-actions.js +169 -7
  44. package/dist/worker/console/pi-plugins.js +1 -3
  45. package/dist/worker/console/server.js +3 -0
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DwVJxUxz.js → abnfDiagram-N423BO3Z-Bho4we4c.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-DqfCcXdu.js → arc-TBDkTeK0.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-qMVtggRv.js → architectureDiagram-T3A2C74G-BF_fZrn2.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-ECrV38dh.js → blockDiagram-VBNYF7ZC-CCnPgUe2.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DBaaLqQv.js → c4Diagram-5PPSVZJV-BCYZJLAT.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-bnXW0U3C.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BvfNHkAz.js → chunk-2GRJ4B5K-D8K6adhU.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-C5StiIbc.js → chunk-2Q5K7J3B-DkT1Q78b.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-HabGiJkS.js → chunk-5RXB4S5H-DuhvpIr7.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BiTg127i.js → chunk-5VM5RSS4-BnK6dC49.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CZsXBPuR.js → chunk-6Q2QTUOP-DPz__z7r.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D-r1MxLK.js → chunk-GF5L2VYU-0kkppM2j.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DQxb9plr.js → chunk-JWPE2WC7-BB41iK5t.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DMndGvCT.js → chunk-KBJHAD2P-CiVrxKHU.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-ByL4BgOJ.js → chunk-RYQCIY6F-C2h9Xje1.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BVe1b1gL.js → chunk-XXDRQBXY-3iOSHdbM.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-rZpq_twR.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-rZpq_twR.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BAyGzN8j.js → cose-bilkent-JH36ORCC-BN3A7tkk.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-C5yVYU7E.js → cynefin-VYW2F7L2-CTuBy0Or.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-x-MwsEm9.js → cynefinDiagram-MW4NZA55-wb6tUlMN.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CT5Tle5F.js → dagre-VZM6K2ZE-GP1Ivx8B.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BARERhlS.js → diagram-7IWD3JNH-ejrcK6L0.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BwfUz0VM.js → diagram-B4RE2ZJO-YFkCCvUU.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Mk4r2WvJ.js → diagram-LBJQPF4R-B4rrvIaL.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-KUrxuBs0.js → diagram-Q27KOJAE-C-YIZcUf.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CjZOceZn.js → diagram-UB23O5K3-BPF5wcL1.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-rnwUCiEI.js → ebnfDiagram-BXEA7PRR-CU5SzXjO.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DM4j23FM.js → erDiagram-JOGREHBK-CpL414DE.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DD5HdDEs.js → flowDiagram-UKHOOZJN-MrhLzNau.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CIAGSF47.js → ganttDiagram-PKOTCBZU-Bg4qFuUo.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DbwFbVEL.js → gitGraphDiagram-DS77QQ5N-ColxSXLR.js} +1 -1
  78. package/dist/worker/console/static/assets/index-D3CC4eUz.css +1 -0
  79. package/dist/worker/console/static/assets/index-DLt4ZFvD.js +437 -0
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-B-Pb0T66.js → infoDiagram-6WML65LV-Czvtm6gc.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B9Unk-YX.js → ishikawaDiagram-WSZJBQD7-B7Xp8HyJ.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-C2F3JkPt.js → journeyDiagram-NVQOT4AX-DWYZcvph.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Bl8-Xcoz.js → kanban-definition-27J2QSJJ-KTLZRUjx.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-yK4uZmC5.js → linear-_Exn7bfl.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-Bpa6PmuE.js → mermaid.core-Cxg8kX8V.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BfGRoCSe.js → mindmap-definition-FAOFIHXS-oKrlo2Bt.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Bz6_HtST.js → pegDiagram-VL7TDLO6-XAeteiNd.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-i0Umbsby.js → pieDiagram-7S7Q4E2Y-BTf1K-lp.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DaVQBobe.js → quadrantDiagram-CIZ2JOQS-C0IlY0Jf.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-DMTGnSq7.js → railroadDiagram-AXF67PYL-BVbKtZhW.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-2ao_DaSq.js → requirementDiagram-LRYGKXZP-C5e4JFcr.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-2SG1lTB9.js → sankeyDiagram-W5VNT64P-Cz30hc29.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BQYOLybR.js → sequenceDiagram-SI44F4Z6-DS33n6d0.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-C8g3yjuI.js → sizeCapture-X5ZJPWSS-46w7v-9T.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-q_F8nAQG.js → stateDiagram-OKZ733FA-pbcD5SsZ.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-zBMYiUyi.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-8LrWpH2s.js → swimlanes-SLNWSIFB-DuivYBSj.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-d_rms-5i.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BQoY3qzl.js → timeline-definition-Z64GVDOM-Cb9VZVIl.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BDu1UjSp.js → vennDiagram-T6HMQDX7-D2-mfOgR.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C48M7Y0a.js → wardleyDiagram-T6FBY63Y-DeS2aC76.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-YU6FTl_F.js → xychartDiagram-ELKLHX3M-WGsrFjxH.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  105. package/dist/worker/console/static-src/app/useRunProgress.js +2 -9
  106. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -2
  107. package/dist/worker/console/static-src/operator-chat/useChatThread.js +0 -6
  108. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +3 -28
  109. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +0 -106
  110. package/dist/worker/console/workspace-context.js +111 -1
  111. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  112. package/dist/worker/observability/read-model.js +0 -4
  113. package/dist/worker/observe/static/dag-node-purpose.js +5 -0
  114. package/dist/worker/observe/static/views/dag-inspector.js +7 -2
  115. package/dist/worker/observe/static/views/session-timeline.js +15 -46
  116. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  117. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  118. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  119. package/dist/workflows/dag/frontend-closeout.js +221 -0
  120. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  121. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  122. package/dist/workflows/dag/frontend-implementation-contract.js +932 -162
  123. package/dist/workflows/dag/frontend-plan-render.js +2 -1
  124. package/dist/workflows/dag/frontend-prewrite-gate.js +255 -349
  125. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  126. package/dist/workflows/dag/frontend-read-budgets.js +12 -0
  127. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  128. package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
  129. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  130. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  131. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  132. package/dist/workflows/dag/frontend-repair.js +1 -432
  133. package/dist/workflows/dag/frontend-review-context.js +261 -15
  134. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  135. package/dist/workflows/dag/frontend-shadow-dual-write.js +914 -0
  136. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  137. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  138. package/dist/workflows/dag/frontend-shape.js +427 -0
  139. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  140. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  141. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  142. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  143. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  144. package/dist/workflows/dag/frontend-verification-trace.js +249 -24
  145. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  146. package/dist/workflows/dag/frontend-writer-admission.js +285 -0
  147. package/dist/workflows/dag/frontend-writer-status.js +256 -0
  148. package/dist/workflows/dag/init-hybrid.js +685 -494
  149. package/dist/workflows/dag/interrupt-request.js +7 -0
  150. package/dist/workflows/dag/node-execution.js +732 -4
  151. package/dist/workflows/dag/prompt.js +34 -1
  152. package/dist/workflows/dag/recovery-lease.js +80 -0
  153. package/dist/workflows/dag/report.js +37 -1
  154. package/dist/workflows/dag/rerun-feedback.js +256 -1
  155. package/dist/workflows/dag/rerun-plan.js +10 -2
  156. package/dist/workflows/dag/rerun-run.js +23 -0
  157. package/dist/workflows/dag/rerun-task.js +29 -0
  158. package/dist/workflows/dag/retry-policy.js +214 -104
  159. package/dist/workflows/dag/runner.js +429 -122
  160. package/dist/workflows/dag/scheduler.js +114 -16
  161. package/dist/workflows/dag/types.js +239 -12
  162. package/dist/workflows/dag/validate.js +28 -12
  163. package/docs/init-surface.manifest.json +30 -12
  164. package/docs/skills/vetted-skill-registry.md +4 -2
  165. package/docs/templates/README.md +2 -0
  166. package/docs/templates/agent-dag-report.schema.json +8 -2
  167. package/docs/templates/agent-dag.schema.json +1 -1
  168. package/docs/templates/frontend-implementation-contract.schema.json +4 -1
  169. package/docs/templates/frontend-implementation-dag.json +89 -0
  170. package/docs/templates/spec-registry.schema.json +45 -0
  171. package/harness.json +3 -3
  172. package/package.json +1 -1
  173. package/skills/frontend-bounded-implement/SKILL.md +15 -14
  174. package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
  175. package/skills/frontend-contract/SKILL.md +23 -0
  176. package/skills/frontend-contract/references/contract-protocol.md +34 -0
  177. package/skills/frontend-design-review/SKILL.md +22 -41
  178. package/skills/frontend-plan/SKILL.md +26 -0
  179. package/skills/frontend-plan/references/decision-contract.md +37 -0
  180. package/skills/frontend-plan/references/design-decisions.md +17 -0
  181. package/skills/frontend-review/SKILL.md +20 -15
  182. package/skills/frontend-review/references/review-findings.md +6 -7
  183. package/skills/frontend-scout/SKILL.md +25 -0
  184. package/skills/frontend-scout/references/design-evidence.md +16 -0
  185. package/skills/frontend-scout/references/scout-evidence.md +23 -0
  186. package/skills/frontend-verification/SKILL.md +1 -1
  187. package/skills/loop-agent/references/command-reference.md +1 -0
  188. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  189. package/dist/worker/console/static/assets/channel-CL1NRAmS.js +0 -1
  190. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-PBGzzjZA.js +0 -1
  191. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-PBGzzjZA.js +0 -1
  192. package/dist/worker/console/static/assets/index-BfleX-qj.css +0 -1
  193. package/dist/worker/console/static/assets/index-Bih9w9h9.js +0 -451
  194. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrxGBDJu.js +0 -1
  195. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BhpDxzAf.js +0 -8
  196. package/dist/worker/console/static-src/operator-chat/dag-progress-link.js +0 -22
  197. package/skills/frontend-implementation/SKILL.md +0 -52
  198. package/skills/frontend-implementation/references/code-standards.md +0 -33
  199. package/skills/frontend-implementation/references/design-spec.md +0 -56
  200. package/skills/frontend-implementation/references/node-contracts.md +0 -31
@@ -4,11 +4,166 @@ import path from "node:path";
4
4
  import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
5
5
  import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "./frontend-worktree-diff.js";
6
6
  import { frontendLintAssessmentArtifactSchema } from "./frontend-lint-baseline.js";
7
- import { frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
8
- import { frontendRepairAssessmentSchema } from "./frontend-repair.js";
7
+ import { deterministicSha256, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, frontendImplementationContractSchema, writeFrontendContractReviewArtifacts, } from "./frontend-implementation-contract.js";
9
8
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
10
- import { ledgerStatsLine, parseLedgerJson, } from "../../task/source-prepare/ledger.js";
11
- export const FRONTEND_REVIEW_CONTEXT_SCHEMA_ID = "frontend-review-context-v1";
9
+ import { ledgerStatsLine, parseLedgerJson } from "../../task/source-prepare/ledger.js";
10
+ export const FRONTEND_REVIEW_CONTEXT_SCHEMA_ID = "frontend-review-context-v2";
11
+ /**
12
+ * Deterministically assemble the cumulative diff from a task baseline and the
13
+ * current candidate snapshot. Same input always yields the same output:
14
+ * paths are sorted, statuses are derived from a fixed decision table, and both
15
+ * digests use canonical JSON hashing.
16
+ */
17
+ export function assembleCumulativeDiffContext(input) {
18
+ const baselineByPath = new Map(input.baselineEntries.map((entry) => [entry.path, entry.sha256]));
19
+ const currentByPath = new Map(input.currentEntries.map((entry) => [entry.path, entry.sha256]));
20
+ const allPaths = new Set([
21
+ ...baselineByPath.keys(),
22
+ ...currentByPath.keys(),
23
+ ]);
24
+ const sortedPaths = [...allPaths].sort();
25
+ const perPath = sortedPaths
26
+ .map((p) => {
27
+ const baselineSha256 = baselineByPath.get(p) ?? null;
28
+ const hasCurrent = currentByPath.has(p);
29
+ const currentSha256 = hasCurrent
30
+ ? (currentByPath.get(p) ?? null)
31
+ : null;
32
+ let status;
33
+ if (baselineSha256 === null) {
34
+ status = currentSha256 === null ? "deleted" : "added";
35
+ }
36
+ else if (currentSha256 === null) {
37
+ status = hasCurrent ? "deleted" : "unchanged";
38
+ }
39
+ else {
40
+ status =
41
+ currentSha256 === baselineSha256 ? "unchanged" : "modified";
42
+ }
43
+ return { path: p, status, baselineSha256, currentSha256 };
44
+ })
45
+ // Cumulative diff carries only actual changes. The worktree baseline is
46
+ // the full repository inventory (git ls-files -co), so an unfiltered
47
+ // perPath enumerates thousands of unchanged hashes (e.g. 3000+ entries
48
+ // ≈ 700KB), ballooning the review context and blowing the review node
49
+ // read budget on a single read. Unchanged entries add no diff evidence.
50
+ .filter((entry) => entry.status !== "unchanged");
51
+ const baselineDigest = deterministicSha256([...input.baselineEntries].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
52
+ const cumulativeDigest = deterministicSha256(perPath);
53
+ return {
54
+ schemaId: "frontend-cumulative-diff-context-v1",
55
+ baselineDigest,
56
+ perPath,
57
+ cumulativeDigest,
58
+ empty: perPath.length === 0,
59
+ };
60
+ }
61
+ async function sha256File(filePath) {
62
+ try {
63
+ const bytes = await readFile(filePath);
64
+ return createHash("sha256").update(bytes).digest("hex");
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ /**
71
+ * Capture the cumulative diff from the frozen worktree baseline and the
72
+ * run-owned diff gate output. Pure status/digest logic lives in
73
+ * `assembleCumulativeDiffContext`; this wrapper only does IO.
74
+ */
75
+ export async function captureCumulativeDiffContext(input) {
76
+ const root = path.resolve(input.workspaceRoot);
77
+ let baselineEntries = [];
78
+ try {
79
+ const raw = JSON.parse(await readFile(path.join(input.runDir, "contracts/frontend-worktree-baseline.json"), "utf8"));
80
+ baselineEntries = (raw.files ?? [])
81
+ .map((file) => ({ path: file, sha256: raw.hashes?.[file] ?? null }))
82
+ .filter((entry) => entry.sha256 !== null);
83
+ }
84
+ catch {
85
+ // Missing baseline → empty baseline entries (still deterministic).
86
+ }
87
+ const candidatePaths = [
88
+ ...new Set([...input.diff.changedFiles, ...input.diff.untrackedFiles]),
89
+ ];
90
+ const currentEntries = [];
91
+ for (const file of candidatePaths) {
92
+ currentEntries.push({
93
+ path: file,
94
+ sha256: await sha256File(path.join(root, file)),
95
+ });
96
+ }
97
+ return assembleCumulativeDiffContext({ baselineEntries, currentEntries });
98
+ }
99
+ const TYPED_TO_LEGACY = {
100
+ approve_review: "pass",
101
+ request_review_changes: "request-revision",
102
+ };
103
+ function isTypedReviewTerminalVerdict(value) {
104
+ return value === "approve_review" || value === "request_review_changes";
105
+ }
106
+ function isLegacyReviewVerdict(value) {
107
+ return value === "pass" || value === "request-revision";
108
+ }
109
+ /**
110
+ * Deterministic, fail-closed equivalence between the typed review terminal
111
+ * verdict(s) observed in the attempt and the legacy JSON verdict.
112
+ *
113
+ * Fail-closed cases (all yield `match: false` with an explicit reason):
114
+ * - no typed terminal fact;
115
+ * - more than one typed terminal fact (conflicting);
116
+ * - an unknown typed terminal kind;
117
+ * - a missing/illegal legacy JSON verdict.
118
+ */
119
+ export function compareTypedReviewToLegacyJsonVerdict(input) {
120
+ const kinds = [...input.typedKinds];
121
+ const legacyVerdict = isLegacyReviewVerdict(input.legacyVerdict ?? "")
122
+ ? input.legacyVerdict
123
+ : undefined;
124
+ if (kinds.length === 0) {
125
+ return {
126
+ typedVerdict: undefined,
127
+ legacyVerdict,
128
+ match: false,
129
+ reason: "typed review terminal fact missing",
130
+ };
131
+ }
132
+ if (kinds.length > 1) {
133
+ return {
134
+ typedVerdict: undefined,
135
+ legacyVerdict,
136
+ match: false,
137
+ reason: `conflicting typed review terminal facts: ${[...new Set(kinds)].join(",")}`,
138
+ };
139
+ }
140
+ const typedVerdict = kinds[0];
141
+ if (!isTypedReviewTerminalVerdict(typedVerdict)) {
142
+ return {
143
+ typedVerdict: undefined,
144
+ legacyVerdict,
145
+ match: false,
146
+ reason: `unknown typed review terminal fact kind: ${typedVerdict}`,
147
+ };
148
+ }
149
+ if (!legacyVerdict) {
150
+ return {
151
+ typedVerdict,
152
+ legacyVerdict: undefined,
153
+ match: false,
154
+ reason: "legacy JSON verdict missing or not pass|request-revision",
155
+ };
156
+ }
157
+ const expected = TYPED_TO_LEGACY[typedVerdict];
158
+ return {
159
+ typedVerdict,
160
+ legacyVerdict,
161
+ match: expected === legacyVerdict,
162
+ reason: expected === legacyVerdict
163
+ ? "typed review verdict matches legacy JSON verdict"
164
+ : `typed review verdict ${typedVerdict} maps to ${expected} but legacy JSON verdict is ${legacyVerdict}`,
165
+ };
166
+ }
12
167
  /**
13
168
  * review-context 稳定失败枚举(AC-011 / AC-HARD-005):v2 drift / manifest 失败返回
14
169
  * 结构化、可消费的 failure code(稳定枚举 + reason 的 envelope),既有调用方无需字符串
@@ -52,6 +207,37 @@ async function readOptionalLintAssessment(runDir) {
52
207
  throw new Error(`frontend review context invalid ${relativePath}`);
53
208
  }
54
209
  }
210
+ async function readOptionalRepairAssessment(runDir) {
211
+ const relativePath = "contracts/frontend-repair-assessment.json";
212
+ try {
213
+ return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
214
+ }
215
+ catch (error) {
216
+ if (error &&
217
+ typeof error === "object" &&
218
+ "code" in error &&
219
+ error.code === "ENOENT") {
220
+ return undefined;
221
+ }
222
+ throw new Error(`frontend review context invalid ${relativePath}`);
223
+ }
224
+ }
225
+ /** M8: verify failure ownership facts written by frontend-verify-shell. */
226
+ async function readOptionalVerifyFailure(runDir) {
227
+ const relativePath = "contracts/frontend-verify-failure.json";
228
+ try {
229
+ return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
230
+ }
231
+ catch (error) {
232
+ if (error &&
233
+ typeof error === "object" &&
234
+ "code" in error &&
235
+ error.code === "ENOENT") {
236
+ return undefined;
237
+ }
238
+ throw new Error(`frontend review context invalid ${relativePath}`);
239
+ }
240
+ }
55
241
  function assertReviewEvidence(input) {
56
242
  const contract = frontendImplementationContractSchema.safeParse(input.contract);
57
243
  if (!contract.success) {
@@ -94,47 +280,105 @@ function assertReviewEvidence(input) {
94
280
  trace.mockStrategy !== contract.data.mockApi.strategy) {
95
281
  throw new FrontendReviewContextFailure("review-context-verification-unbound", "frontend review context verification trace is not bound to a passing contract");
96
282
  }
97
- const assessment = frontendRepairAssessmentSchema.safeParse(input.repairAssessment);
98
- if (!assessment.success) {
99
- throw new FrontendReviewContextFailure("review-context-invalid-contract", "frontend review context invalid repair assessment");
100
- }
101
283
  }
102
284
  export async function runFrontendReviewContextGate(input) {
103
- const contract = await readRequiredJson(input.runDir, "contracts/frontend-implementation-contract.json");
285
+ const contractRelativePath = "contracts/frontend-implementation-contract.json";
286
+ let contractRaw;
287
+ try {
288
+ contractRaw = await readFile(path.join(input.runDir, contractRelativePath), "utf8");
289
+ }
290
+ catch (error) {
291
+ throw new FrontendReviewContextFailure("review-context-manifest-missing", `frontend review context missing or invalid ${contractRelativePath}: ${error instanceof Error ? error.message : String(error)}`);
292
+ }
293
+ let contract;
294
+ try {
295
+ contract = JSON.parse(contractRaw);
296
+ }
297
+ catch (error) {
298
+ throw new FrontendReviewContextFailure("review-context-manifest-missing", `frontend review context missing or invalid ${contractRelativePath}: ${error instanceof Error ? error.message : String(error)}`);
299
+ }
104
300
  const parsedContract = frontendImplementationContractSchema.safeParse(contract);
105
301
  if (!parsedContract.success) {
106
302
  throw new FrontendReviewContextFailure("review-context-invalid-contract", "frontend review context invalid implementation contract");
107
303
  }
304
+ // Authorized diff surface = deliverable files + every verification-target
305
+ // file the contract references. The writer legitimately writes VT test
306
+ // artifacts (behavior verification), and the reviewer must audit their
307
+ // diffs; scoping this to targets.files alone failed the baseline-overlap
308
+ // check on app.test.js (r19 extreme smoke).
309
+ const authorizedChangedPaths = [
310
+ ...new Set([
311
+ ...parsedContract.data.targets.files,
312
+ ...parsedContract.data.verificationTargets.map((target) => target.file),
313
+ ]),
314
+ ];
108
315
  const diff = await runFrontendWorktreeDiffGate({
109
316
  ...input,
110
- authorizedChangedPaths: parsedContract.data.targets.files,
317
+ authorizedChangedPaths,
111
318
  });
112
319
  const verificationTrace = await readRequiredJson(input.runDir, "contracts/frontend-verification-trace.json");
113
- const repairAssessment = await readRequiredJson(input.runDir, "contracts/frontend-repair-assessment.json");
320
+ const repairAssessment = await readOptionalRepairAssessment(input.runDir);
321
+ const verifyFailure = await readOptionalVerifyFailure(input.runDir);
114
322
  const lintAssessment = await readOptionalLintAssessment(input.runDir);
323
+ const cumulativeDiff = await captureCumulativeDiffContext({
324
+ runDir: input.runDir,
325
+ workspaceRoot: input.workspaceRoot,
326
+ diff,
327
+ });
115
328
  assertReviewEvidence({ contract, verificationTrace, repairAssessment });
116
329
  // Source fidelity ledger 核对(AC-008 / AC-FIDELITY-008):ledger 缺失/漂移即阻断;
117
330
  // 同时把核对清单注入 review-context 供最终 reviewer 机械化核对。
118
331
  const sourceFidelity = await buildReviewSourceFidelityChecklist(input, parsedContract.data);
332
+ const contractReviewArtifacts = await writeFrontendContractReviewArtifacts({
333
+ runDir: input.runDir,
334
+ outputDir: "contracts",
335
+ artifactName: "frontend-implementation-contract.json",
336
+ canonical: parsedContract.data,
337
+ canonicalArtifact: {
338
+ path: path.join(input.runDir, contractRelativePath),
339
+ sha256: createHash("sha256").update(contractRaw).digest("hex"),
340
+ bytes: Buffer.byteLength(contractRaw, "utf8"),
341
+ },
342
+ });
119
343
  const payload = {
120
- schemaVersion: 1,
344
+ schemaVersion: 2,
121
345
  schemaId: FRONTEND_REVIEW_CONTEXT_SCHEMA_ID,
122
- contract,
346
+ contractRef: {
347
+ path: contractRelativePath,
348
+ schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
349
+ sha256: contractReviewArtifacts.index.contract.sha256,
350
+ bytes: contractReviewArtifacts.index.contract.bytes,
351
+ indexPath: contractReviewArtifacts.index.relativePath,
352
+ indexSha256: contractReviewArtifacts.index.artifactSha256,
353
+ capacityDiagnosticPath: contractReviewArtifacts.capacity.relativePath,
354
+ capacityDiagnosticSha256: contractReviewArtifacts.capacity.artifactSha256,
355
+ sections: contractReviewArtifacts.index.sections,
356
+ },
123
357
  verificationTrace,
124
- repairAssessment,
358
+ ...(repairAssessment ? { repairAssessment } : {}),
359
+ ...(verifyFailure ? { verifyFailure } : {}),
125
360
  ...(lintAssessment ? { lintAssessment } : {}),
126
361
  sourceFidelity,
127
362
  diff: {
128
363
  schemaId: diff.schemaId,
129
364
  patchPath: diff.patchPath,
130
365
  patchSha256: diff.patchSha256,
366
+ reviewSummaryPath: diff.reviewSummaryPath,
367
+ reviewIndexPath: diff.reviewIndexPath,
131
368
  changedFiles: diff.changedFiles,
132
369
  untrackedFiles: diff.untrackedFiles,
133
370
  empty: diff.empty,
134
371
  },
372
+ cumulativeDiff,
135
373
  };
136
374
  const artifactPath = await writeDagRunJsonArtifact(input.runDir, "contracts/frontend-review-context.json", payload);
137
- return { ok: true, artifactPath, diff };
375
+ return {
376
+ ok: true,
377
+ artifactPath,
378
+ diff,
379
+ contractRef: payload.contractRef,
380
+ capacity: contractReviewArtifacts.capacity,
381
+ };
138
382
  }
139
383
  /**
140
384
  * 最终 review 核对清单(机械化):ledger 缺失/漂移、canonical requirement 漏项、
@@ -193,6 +437,8 @@ export function formatFrontendReviewContextStdout(result) {
193
437
  return [
194
438
  "Frontend review context: pass",
195
439
  `Artifact: ${result.artifactPath}`,
440
+ `Contract index: ${result.contractRef.indexPath}`,
441
+ `Capacity: ${result.capacity.overallLevel} (${result.capacity.relativePath}, non-blocking)`,
196
442
  formatFrontendWorktreeDiffStdout(result.diff),
197
443
  ].join("\n");
198
444
  }
@@ -0,0 +1,270 @@
1
+ import path from "node:path";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { readTypedEventStoreFromJsonl, } from "./frontend-typed-event-store.js";
4
+ /**
5
+ * Frontend review findings → downstream work (direction A).
6
+ *
7
+ * When `frontend-review-pi` commits `request_review_changes`, the implementation
8
+ * must not silently finish: the run becomes "not passed" (partial_failed +
9
+ * terminalReason=review-request-changes), and the committed findings are
10
+ * rendered into a downstream-task PRD draft that a later DAG round can consume
11
+ * instead of repeating the previous flow. Rendering is deterministic (pure);
12
+ * only the JSONL read and PRD write are IO.
13
+ *
14
+ * The design-review analog (`request_design_changes`) reuses the same PRD
15
+ * rendering and typed-fact channel: a design rejection is a definite finding
16
+ * (not a protocol/environment error), so it auto-re-enters the next plan round
17
+ * with the committed design findings instead of terminating the run.
18
+ */
19
+ export const FRONTEND_REVIEW_FINDINGS_PRD_REL = "contracts/frontend-review-findings-prd.md";
20
+ /** Review node id whose committed typed facts drive the findings PRD. */
21
+ export const FRONTEND_REVIEW_NODE_ID = "frontend-review-pi";
22
+ /** Design review node id whose committed typed facts drive the findings PRD. */
23
+ export const FRONTEND_DESIGN_REVIEW_NODE_ID = "frontend-design-review-pi";
24
+ /** Deterministic issueCategory → failure owner mapping (A+B AC-009 §4.10). */
25
+ export function classifyReviewIssueCategoryFailureOwner(issueCategory) {
26
+ switch (issueCategory) {
27
+ case "implementation-mismatch":
28
+ return "implementation";
29
+ case "approved-design-defect":
30
+ return "plan";
31
+ case "target-surface-defect":
32
+ return "scout";
33
+ case "contract-requirement-gap":
34
+ return "contract";
35
+ case "unknown":
36
+ default:
37
+ return "unknown";
38
+ }
39
+ }
40
+ const API_SIGNAL_RE = /\b(api|fetch|axios|graphql|endpoint|远程|接口)\b/i;
41
+ /** Negations for the API signal. CJK "无 API/无接口" has no \b boundary, so
42
+ * it is matched separately from the ASCII no api/not-needed forms. */
43
+ const API_NEGATION_RE = /(?:^|[^\w])(?:no\s+api|not-needed)(?:$|[^\w])|无\s*api|无\s*接口/i;
44
+ const SMALL_SIGNAL_WORDS = [
45
+ "单文件局部修复",
46
+ "局部样式",
47
+ "文案调整",
48
+ "无远程接口",
49
+ ];
50
+ function topLevelDir(p) {
51
+ const normalized = p.replace(/\\/g, "/");
52
+ const first = normalized.split("/").filter(Boolean)[0] ?? "";
53
+ return first;
54
+ }
55
+ /** Collect unique non-empty finding `file` values (sorted, de-duplicated). */
56
+ function findingTargetFiles(fact) {
57
+ const files = new Set();
58
+ for (const finding of fact.findings) {
59
+ if (typeof finding.file === "string" && finding.file.trim()) {
60
+ files.add(finding.file.trim());
61
+ }
62
+ }
63
+ return [...files].sort();
64
+ }
65
+ /**
66
+ * Deterministically derive the fix scope for the downstream task. Same fact
67
+ * always yields the same scope. The shape mirrors `resolveFrontendTaskShape`
68
+ * signals (concentrated paths, no remote API → micro/small) so that when the
69
+ * PRD draft is used as the next task's requirement, the frontend DAG generator
70
+ * routes a small fix to micro/small instead of the full 11-node standard
71
+ * topology.
72
+ */
73
+ export function deriveFrontendReviewFixScope(fact) {
74
+ const targetFiles = findingTargetFiles(fact);
75
+ const blob = fact.findings
76
+ .map((f) => `${f.issue} ${f.requiredChange ?? ""} ${f.file ?? ""}`)
77
+ .join("\n");
78
+ const hasApiSignal = API_SIGNAL_RE.test(blob) && !API_NEGATION_RE.test(blob);
79
+ const topDirs = new Set(targetFiles.map(topLevelDir));
80
+ const concentrated = targetFiles.length > 0 && topDirs.size <= 1 && targetFiles.length <= 4;
81
+ let shape;
82
+ let complexity;
83
+ let reason;
84
+ if (targetFiles.length === 0) {
85
+ shape = "standard";
86
+ complexity = "medium";
87
+ reason =
88
+ "findings carry no file references; scope unknown, keep standard topology";
89
+ }
90
+ else if (hasApiSignal) {
91
+ shape = "standard";
92
+ complexity = "medium";
93
+ reason = `findings reference a remote API signal; standard topology required (files=${targetFiles.length})`;
94
+ }
95
+ else if (concentrated && targetFiles.length <= 1) {
96
+ shape = "micro";
97
+ complexity = "trivial";
98
+ reason = "single-file concentrated fix without remote API";
99
+ }
100
+ else if (concentrated) {
101
+ shape = "small";
102
+ complexity = "low";
103
+ reason = `concentrated fix across ${targetFiles.length} file(s) in one top-level directory`;
104
+ }
105
+ else {
106
+ shape = "standard";
107
+ complexity = "medium";
108
+ reason = `fix spans ${targetFiles.length} file(s) across ${topDirs.size} top-level directories; standard topology`;
109
+ }
110
+ const allowedPaths = [
111
+ ...new Set([
112
+ ...targetFiles,
113
+ ...targetFiles.map((f) => `${topLevelDir(f)}/**`),
114
+ ]),
115
+ ].sort();
116
+ return {
117
+ shape,
118
+ complexity,
119
+ targetFiles,
120
+ allowedPaths,
121
+ smallSignals: [...SMALL_SIGNAL_WORDS],
122
+ hasApiSignal,
123
+ reason,
124
+ };
125
+ }
126
+ /**
127
+ * Read the committed `request_review_changes` fact from the review node's typed
128
+ * event store. Returns undefined when no committed request fact exists (e.g.
129
+ * approve_review, missing store, or an unreadable store).
130
+ */
131
+ export async function readCommittedReviewRequestFact(input) {
132
+ const { runDir: reviewRunDir } = input;
133
+ const reviewNodeId = input.reviewNodeId ?? FRONTEND_REVIEW_NODE_ID;
134
+ return readCommittedRequestFactByKind({
135
+ runDir: reviewRunDir,
136
+ nodeId: reviewNodeId,
137
+ factsFile: "review-typed-facts.jsonl",
138
+ kind: "request_review_changes",
139
+ });
140
+ }
141
+ /**
142
+ * Read the committed `request_design_changes` fact from the design review
143
+ * node's typed event store. Returns undefined when no committed request fact
144
+ * exists (e.g. approve_design, missing store, or an unreadable store).
145
+ */
146
+ export async function readCommittedDesignRequestFact(input) {
147
+ const { runDir: designRunDir } = input;
148
+ const designNodeId = input.designNodeId ?? FRONTEND_DESIGN_REVIEW_NODE_ID;
149
+ return readCommittedRequestFactByKind({
150
+ runDir: designRunDir,
151
+ nodeId: designNodeId,
152
+ factsFile: "design-typed-facts.jsonl",
153
+ kind: "request_design_changes",
154
+ });
155
+ }
156
+ async function readCommittedRequestFactByKind(input) {
157
+ let records;
158
+ try {
159
+ records = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.nodeId, input.factsFile));
160
+ }
161
+ catch {
162
+ return undefined;
163
+ }
164
+ const committed = records.filter((record) => record.phase === "committed");
165
+ if (committed.length === 0)
166
+ return undefined;
167
+ const request = committed.find((record) => record.fact.kind === input.kind);
168
+ if (!request)
169
+ return undefined;
170
+ const fact = request.fact;
171
+ if (typeof fact !== "object" || fact === null)
172
+ return undefined;
173
+ return fact;
174
+ }
175
+ function bulletList(items) {
176
+ if (!items || items.length === 0)
177
+ return "_(none)_";
178
+ return items.map((item) => `- ${item}`).join("\n");
179
+ }
180
+ function findingLines(findings) {
181
+ if (findings.length === 0)
182
+ return "_(none)_";
183
+ return findings
184
+ .map((finding) => {
185
+ const location = [finding.file, finding.line]
186
+ .filter((value) => value !== undefined)
187
+ .join(":");
188
+ const prefix = location ? `[${location}] ` : "";
189
+ const requiredChange = finding.requiredChange
190
+ ? `\n - requiredChange: ${finding.requiredChange}`
191
+ : "";
192
+ return `- ${prefix}${finding.severity}: ${finding.issue}${requiredChange}`;
193
+ })
194
+ .join("\n");
195
+ }
196
+ /**
197
+ * Deterministically render a downstream-task PRD draft from a committed
198
+ * `request_review_changes` / `request_design_changes` fact. Same fact always
199
+ * renders byte-identical output; the parent run id and review node id are the
200
+ * only context.
201
+ */
202
+ export function renderFrontendReviewFindingsPrd(input) {
203
+ const { parentRunId, fact } = input;
204
+ const isDesign = fact.kind === "request_design_changes";
205
+ const reviewNodeId = input.reviewNodeId ??
206
+ (isDesign ? FRONTEND_DESIGN_REVIEW_NODE_ID : FRONTEND_REVIEW_NODE_ID);
207
+ const restartPhase = classifyReviewIssueCategoryFailureOwner(fact.issueCategory);
208
+ const scope = deriveFrontendReviewFixScope(fact);
209
+ const targetSummary = scope.targetFiles.length > 0
210
+ ? scope.targetFiles.join(", ")
211
+ : fact.issueCategory;
212
+ return [
213
+ `# 修复 ${isDesign ? "设计" : "review"} 发现(${scope.shape}):${targetSummary}`,
214
+ "",
215
+ "## 背景",
216
+ "",
217
+ `- parentRunId: \`${parentRunId}\``,
218
+ `- reviewNodeId: \`${reviewNodeId}\``,
219
+ `- typedVerdict: \`${fact.kind}\``,
220
+ `- issueCategory: \`${fact.issueCategory}\``,
221
+ `- restartPhase/failureOwner: \`${restartPhase}\``,
222
+ "",
223
+ "## 修复范围(供下一轮任务生成路由)",
224
+ "",
225
+ `- 建议 shape:\`${scope.shape}\`(micro=7 / small=10 / standard=11 节点)`,
226
+ `- 建议 complexity:\`${scope.complexity}\``,
227
+ `- 推导依据:${scope.reason}`,
228
+ ...(scope.hasApiSignal ? ["- ⚠️ findings 含远程 API/接口信号,走 standard 拓扑"] : []),
229
+ scope.targetFiles.length > 0
230
+ ? [`- 目标文件:${scope.targetFiles.map((f) => `\`${f}\``).join(", ")}`]
231
+ : ["- 目标文件:_(findings 未提供 file 引用,需人工补全)_"],
232
+ scope.allowedPaths.length > 0
233
+ ? [`- 建议 allowedPaths:${scope.allowedPaths.map((p) => `\`${p}\``).join(", ")}`]
234
+ : [],
235
+ scope.shape === "micro" || scope.shape === "small"
236
+ ? [`- 范围信号:${scope.smallSignals.join("、")}(供 shape 分类器识别,避免退化为全节点流程)`]
237
+ : [],
238
+ "",
239
+ "## 问题清单",
240
+ "",
241
+ findingLines(fact.findings),
242
+ "",
243
+ "## 证据引用",
244
+ "",
245
+ bulletList(fact.evidenceRefs),
246
+ "",
247
+ "## 修复目标",
248
+ "",
249
+ `- 按 issueCategory 映射的重启阶段(${restartPhase})定点修复,不复用上一轮未通过的实现。`,
250
+ "- 逐条关闭上面「问题清单」中的 findings,并保留上一轮已通过的验证目标继续有效。",
251
+ `- 完成修复后重新走 frontend DAG 的 ${isDesign ? "plan → design" : "implement → verify → review"} 链,直到${isDesign ? " design-review 提交 approve_design" : " review 提交 approve_review"}。`,
252
+ "",
253
+ ].join("\n");
254
+ }
255
+ /** Write the findings PRD draft under the run dir (run-owned artifact). */
256
+ export async function writeFrontendReviewFindingsPrd(input) {
257
+ const markdown = renderFrontendReviewFindingsPrd(input);
258
+ const abs = path.join(input.runDir, FRONTEND_REVIEW_FINDINGS_PRD_REL);
259
+ await mkdir(path.dirname(abs), { recursive: true });
260
+ await writeFile(abs, markdown, "utf8");
261
+ return FRONTEND_REVIEW_FINDINGS_PRD_REL;
262
+ }
263
+ export async function readFrontendReviewFindingsPrd(runDir) {
264
+ try {
265
+ return await readFile(path.join(runDir, FRONTEND_REVIEW_FINDINGS_PRD_REL), "utf8");
266
+ }
267
+ catch {
268
+ return undefined;
269
+ }
270
+ }