@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.3

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 (202) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +187 -92
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/recommendations.js +13 -2
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +16 -5
  8. package/dist/cli/program.js +16 -2
  9. package/dist/cli/update/notifier.js +2 -2
  10. package/dist/cli.js +2 -1
  11. package/dist/commands/dag-rerun.js +55 -1
  12. package/dist/commands/init-model-catalog.js +464 -0
  13. package/dist/commands/init-upgrade.js +265 -97
  14. package/dist/commands/init.js +429 -28
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/executors/dag-pi-executor.js +1645 -29
  17. package/dist/executors/pi-extension-resolver.js +233 -0
  18. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  19. package/dist/executors/pi-sdk-executor.js +180 -33
  20. package/dist/executors/shell-executor.js +1150 -171
  21. package/dist/executors/shell-write-guard.js +14 -0
  22. package/dist/shared/dag-prompt-override.js +28 -0
  23. package/dist/shared/openspec-spec.js +70 -4
  24. package/dist/shared/operator/capabilities.js +32 -0
  25. package/dist/shared/playwright-cli-command-policy.js +15 -0
  26. package/dist/shared/update/console-notifier.js +100 -0
  27. package/dist/{cli → shared}/update/npm-client.js +41 -8
  28. package/dist/{cli → shared}/update/state.js +41 -13
  29. package/dist/task/config-types.js +19 -5
  30. package/dist/task/contract/project.js +3 -0
  31. package/dist/task/contract/schema.js +2 -1
  32. package/dist/task/frontend-project-capability.js +42 -16
  33. package/dist/task/source-prepare/parse-intent.js +24 -4
  34. package/dist/task/source-prepare/prepare.js +36 -0
  35. package/dist/worker/cli.js +31 -1
  36. package/dist/worker/console/chat/pi-console-config.js +81 -10
  37. package/dist/worker/console/chat/routes.js +1 -0
  38. package/dist/worker/console/dag-execution-receipt.js +33 -0
  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 +263 -0
  44. package/dist/worker/console/operator-user-error.js +2 -2
  45. package/dist/worker/console/routes.js +43 -2
  46. package/dist/worker/console/security.js +63 -9
  47. package/dist/worker/console/server.js +151 -6
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
  80. package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
  81. package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  107. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  108. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  109. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  110. package/dist/worker/observe/node-input.js +74 -3
  111. package/dist/worker/observe/node-process.js +377 -0
  112. package/dist/worker/observe/routes.js +36 -3
  113. package/dist/worker/observe/shell-handler-keys.js +34 -0
  114. package/dist/worker/observe/static/api.js +66 -0
  115. package/dist/worker/observe/static/constants.js +8 -1
  116. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  117. package/dist/worker/observe/static/format.js +23 -0
  118. package/dist/worker/observe/static/markdown-render.js +20 -1
  119. package/dist/worker/observe/static/operator-chrome.js +4 -0
  120. package/dist/worker/observe/static/state.js +13 -2
  121. package/dist/worker/observe/static/styles.css +73 -0
  122. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  123. package/dist/worker/observe/static/views/dag.js +8 -1
  124. package/dist/worker/observe/static/views/failures.js +5 -2
  125. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  126. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
  127. package/dist/workflows/dag/contract-output-registry.js +15 -0
  128. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  129. package/dist/workflows/dag/failure-category.js +12 -0
  130. package/dist/workflows/dag/frontend-closeout.js +221 -0
  131. package/dist/workflows/dag/frontend-design-policy.js +347 -0
  132. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  133. package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
  134. package/dist/workflows/dag/frontend-prewrite-gate.js +258 -393
  135. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  136. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  137. package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
  138. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  139. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  140. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  141. package/dist/workflows/dag/frontend-repair.js +7 -424
  142. package/dist/workflows/dag/frontend-review-context.js +195 -8
  143. package/dist/workflows/dag/frontend-risk.js +15 -2
  144. package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
  145. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  146. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  147. package/dist/workflows/dag/frontend-shape.js +425 -0
  148. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  149. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  150. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  151. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  152. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  153. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  154. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  155. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  156. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  157. package/dist/workflows/dag/frontend-typed-event-store.js +438 -0
  158. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  159. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  160. package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
  161. package/dist/workflows/dag/frontend-writer-admission.js +260 -0
  162. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  163. package/dist/workflows/dag/init-hybrid.js +572 -500
  164. package/dist/workflows/dag/node-execution.js +307 -70
  165. package/dist/workflows/dag/prompt.js +10 -18
  166. package/dist/workflows/dag/rerun-plan.js +144 -5
  167. package/dist/workflows/dag/rerun-run.js +58 -6
  168. package/dist/workflows/dag/retry-policy.js +228 -4
  169. package/dist/workflows/dag/runner.js +314 -119
  170. package/dist/workflows/dag/scheduler.js +114 -16
  171. package/dist/workflows/dag/structured-output-repair.js +712 -0
  172. package/dist/workflows/dag/types.js +235 -23
  173. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  174. package/dist/workflows/dag/validate.js +60 -10
  175. package/docs/init-surface.manifest.json +5 -0
  176. package/docs/operations/README.md +1 -1
  177. package/docs/templates/agent-dag.schema.json +44 -0
  178. package/docs/templates/frontend-test-dag.json +8 -10
  179. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  180. package/docs/templates/init-managed-agents.md +13 -3
  181. package/harness.json +3 -3
  182. package/package.json +5 -1
  183. package/scripts/next-info.mjs +356 -0
  184. package/scripts/next-publish-gate.mjs +238 -0
  185. package/scripts/release-source-binding.mjs +251 -0
  186. package/skills/codebase-scout/SKILL.md +1 -1
  187. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  188. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  189. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  190. package/skills/frontend-implementation/SKILL.md +4 -4
  191. package/skills/frontend-implementation/references/design-spec.md +11 -7
  192. package/skills/frontend-implementation/references/node-contracts.md +20 -11
  193. package/skills/loop-agent/references/command-reference.md +11 -4
  194. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  195. package/skills/playwright-cli/SKILL.md +22 -0
  196. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  197. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  198. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  199. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  200. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  201. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  202. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -1,9 +1,5 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
1
  import { z } from "zod";
4
- import { pathMatchesPattern } from "../../shared/git-progress.js";
5
- import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
6
- export const FRONTEND_REPAIR_ASSESSMENT_SCHEMA_ID = "frontend-repair-assessment-v1";
2
+ import { FRONTEND_NO_VERIFICATION_MARKER_TEXT, } from "./frontend-verification-trace.js";
7
3
  export const frontendRepairFailureClassSchema = z.enum([
8
4
  "typecheck",
9
5
  "build",
@@ -28,60 +24,17 @@ export const frontendCommandCategorySchema = z.enum([
28
24
  "unit-test",
29
25
  "trace",
30
26
  ]);
31
- const REPAIRABLE = new Set([
32
- "typecheck",
33
- "build",
34
- "lint",
35
- "component-test",
36
- "unit-test",
37
- "trace",
38
- ]);
39
- export function isFrontendRepairable(failureClass) {
40
- return REPAIRABLE.has(failureClass);
41
- }
42
- /**
43
- * Deterministic commandLabel → commandCategory mapping (AC-3).
44
- * `targetType` comes from the contract verificationTargets[].type
45
- * (static | unit | component | integration | mock); the literal commandLabel
46
- * wins over type so a `static` typecheck target is never misread as a test.
47
- */
48
- export function mapCommandLabelToCategory(commandLabel, targetType) {
49
- const label = commandLabel.toLowerCase();
50
- if (/(?:typecheck|check-types|type-check|\btsc\b)/.test(label))
51
- return "typecheck";
52
- if (/(?:vite\s+build|next\s+build|webpack|rollup|\bbuild\b)/.test(label))
53
- return "build";
54
- if (/(?:eslint|\blint\b)/.test(label))
55
- return "lint";
56
- if (targetType === "component")
57
- return "component-test";
58
- if (targetType === "unit")
59
- return "unit-test";
60
- if (/(?:component|testing-library|render\()/.test(label))
61
- return "component-test";
62
- return "unit-test";
63
- }
64
- export function isReviewRepairable(finding, implementWriteSet) {
65
- const hasFile = Boolean(finding.file && finding.file.trim().length > 0);
66
- const hasLocation = Boolean((finding.line !== undefined && finding.line > 0) ||
67
- (finding.symbol && finding.symbol.trim().length > 0));
68
- const hasRequirement = Boolean((finding.requirementId && finding.requirementId.trim().length > 0) ||
69
- (finding.acceptanceCriteriaId &&
70
- finding.acceptanceCriteriaId.trim().length > 0));
71
- const fixScope = finding.fixScope ?? (hasFile ? [finding.file] : []);
72
- const fixScopeSubset = fixScope.length > 0 &&
73
- fixScope.every((entry) => isPathWithinWriteSet(entry, implementWriteSet));
74
- return (hasFile &&
75
- hasLocation &&
76
- hasRequirement &&
77
- fixScopeSubset &&
78
- finding.requiresContractReplan !== true);
79
- }
80
27
  export function classifyFrontendFailure(input) {
81
28
  // AC-2: structured evidence takes priority over string-blob heuristics.
82
29
  if (input.failureCategory === "write-guard") {
83
30
  return "path";
84
31
  }
32
+ if (`${input.stderr ?? ""}\n${input.stdout ?? ""}`.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT)) {
33
+ // Verification unavailability is a generation-time contract gap: no
34
+ // authorized verification command exists. Bounded repair cannot fix it;
35
+ // the operator must declare commands and regenerate the DAG.
36
+ return "contract";
37
+ }
85
38
  if (input.traceTarget || input.commandCategory === "trace") {
86
39
  return "trace";
87
40
  }
@@ -169,373 +122,3 @@ export function classifyFrontendFailure(input) {
169
122
  return "path";
170
123
  return "unknown";
171
124
  }
172
- export const frontendRepairAssessmentSchema = z
173
- .object({
174
- schemaVersion: z.literal(1),
175
- schemaId: z.literal(FRONTEND_REPAIR_ASSESSMENT_SCHEMA_ID),
176
- contract: z
177
- .object({
178
- relativePath: z.string().min(1),
179
- schemaId: z.string().min(1),
180
- })
181
- .strict(),
182
- failedNodeIds: z.array(z.string().min(1)),
183
- failureClass: frontendRepairFailureClassSchema,
184
- eligible: z.boolean(),
185
- reason: z.string().min(1),
186
- attempt: z.number().int().min(1).max(2),
187
- maxAttempts: z.literal(1),
188
- allowedRepairWriteSet: z.array(z.string().min(1)),
189
- evidenceRefs: z.array(z.string().min(1)),
190
- browserStatus: z.literal("not-run"),
191
- // AC-5 structured evidence fields (all optional so existing minimal
192
- // fixtures and frontend-review-context safeParse stay compatible).
193
- verificationTargetId: z.string().min(1).optional(),
194
- commandLabel: z.string().min(1).optional(),
195
- exitCode: z.number().int().optional(),
196
- commandCategory: frontendCommandCategorySchema.optional(),
197
- requirementIds: z.array(z.string().min(1)).optional(),
198
- acceptanceCriteriaIds: z.array(z.string().min(1)).optional(),
199
- fixScope: z.array(z.string().min(1)).optional(),
200
- changedPaths: z.array(z.string().min(1)).optional(),
201
- traceRef: z
202
- .object({
203
- relativePath: z.string().min(1),
204
- schemaId: z.string().min(1),
205
- })
206
- .strict()
207
- .optional(),
208
- })
209
- .strict();
210
- function nodeHadFailure(record) {
211
- if (record.status === "ERROR")
212
- return true;
213
- if (recordIndicatesCommandFailure(record))
214
- return true;
215
- const results = record.commandResults ?? [];
216
- if (results.some((item) => item.ok === false))
217
- return true;
218
- return false;
219
- }
220
- function normalize(value) {
221
- return value.replace(/\\/g, "/").replace(/^\.\//, "");
222
- }
223
- /**
224
- * Whether a concrete path or glob entry is contained by the implement writeSet.
225
- * Exact glob equality is the norm (repair writeSet === implement writeSet);
226
- * concrete (non-glob) paths may also be covered by an implement glob.
227
- */
228
- export function isPathWithinWriteSet(entry, implementWriteSet) {
229
- const normalizedEntry = normalize(entry);
230
- const normalizedImplement = implementWriteSet.map(normalize);
231
- if (normalizedImplement.includes(normalizedEntry))
232
- return true;
233
- if (!normalizedEntry.includes("*")) {
234
- return implementWriteSet.some((pattern) => pathMatchesPattern(normalizedEntry, pattern));
235
- }
236
- return false;
237
- }
238
- export function assertWriteSetSubset(repairWriteSet, implementWriteSet) {
239
- for (const entry of repairWriteSet) {
240
- if (!isPathWithinWriteSet(entry, implementWriteSet)) {
241
- throw new Error(`repair writeSet entry "${entry}" is not ⊆ implement writeSet`);
242
- }
243
- }
244
- }
245
- async function loadImplementWriteSet(runDir, fallback) {
246
- if (fallback.length > 0)
247
- return fallback;
248
- const candidates = ["run.json", "dag.json", "spec.json"];
249
- for (const name of candidates) {
250
- try {
251
- const raw = JSON.parse(await readFile(path.join(runDir, name), "utf8"));
252
- const tasks = raw.spec?.tasks ?? raw.tasks ?? [];
253
- const implement = tasks.find((task) => task.id === "frontend-implement-pi");
254
- if (implement?.writeSet?.length)
255
- return implement.writeSet;
256
- }
257
- catch {
258
- // continue
259
- }
260
- }
261
- return fallback;
262
- }
263
- function matchVerificationTarget(contract, commandLabel, command) {
264
- for (const target of contract.verificationTargets) {
265
- if (commandLabel && target.commandLabel === commandLabel)
266
- return target;
267
- if (command &&
268
- (command.includes(target.commandLabel) ||
269
- target.commandLabel.includes(command))) {
270
- return target;
271
- }
272
- }
273
- return undefined;
274
- }
275
- function deriveFailureEvidence(input) {
276
- const evidence = {};
277
- for (const { record } of input.failed) {
278
- const results = record.commandResults ?? [];
279
- const failedResult = results.find((item) => item.ok === false) ?? results[0];
280
- if (failedResult &&
281
- evidence.exitCode === undefined &&
282
- typeof failedResult.exitCode === "number") {
283
- evidence.exitCode = failedResult.exitCode;
284
- }
285
- if (failedResult?.commandLabel && !evidence.commandLabel) {
286
- evidence.commandLabel = failedResult.commandLabel;
287
- }
288
- // Structured trace facts passed by the verification bundle / trace gate.
289
- const failedTraceTarget = (record.traceTargets ?? []).find((target) => target.status === "failed");
290
- if (failedTraceTarget && !evidence.verificationTargetId) {
291
- evidence.verificationTargetId = failedTraceTarget.id;
292
- if (!evidence.commandLabel)
293
- evidence.commandLabel = failedTraceTarget.commandLabel;
294
- evidence.traceTarget = failedTraceTarget;
295
- evidence.traceRef = {
296
- relativePath: "contracts/frontend-verification-trace.json",
297
- schemaId: "frontend-verification-trace-v1",
298
- };
299
- }
300
- if (record.changedPaths?.length && !evidence.changedPaths) {
301
- evidence.changedPaths = [...record.changedPaths];
302
- }
303
- }
304
- // Resolve commandLabel → contract verification target → category + scope.
305
- const target = matchVerificationTarget(input.contract, evidence.commandLabel);
306
- if (target) {
307
- if (!evidence.verificationTargetId)
308
- evidence.verificationTargetId = target.id;
309
- if (!evidence.commandLabel)
310
- evidence.commandLabel = target.commandLabel;
311
- evidence.requirementIds ??= [...target.requirementIds];
312
- evidence.acceptanceCriteriaIds ??= [...target.requirementIds];
313
- evidence.fixScope ??= [target.file];
314
- }
315
- if (evidence.commandLabel) {
316
- evidence.commandCategory ??= mapCommandLabelToCategory(evidence.commandLabel, target?.type);
317
- }
318
- return evidence;
319
- }
320
- export async function runFrontendFailureAssessGate(input) {
321
- const attempt = input.attempt ?? 1;
322
- const implementWriteSet = await loadImplementWriteSet(input.runDir, input.implementWriteSet ?? []);
323
- if (implementWriteSet.length === 0) {
324
- throw new Error("assess: implement writeSet unavailable");
325
- }
326
- const contractRel = "contracts/frontend-implementation-contract.json";
327
- const contractPath = path.join(input.runDir, contractRel);
328
- let contractSchemaId = FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID;
329
- let contract;
330
- try {
331
- const raw = JSON.parse(await readFile(contractPath, "utf8"));
332
- const parsed = frontendImplementationContractSchema.safeParse(raw);
333
- if (!parsed.success) {
334
- throw new Error("invalid frontend implementation contract");
335
- }
336
- contract = parsed.data;
337
- contractSchemaId = FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID;
338
- }
339
- catch {
340
- // still allow assessment of failures without contract? fail-closed to contract
341
- throw new Error("assess: missing or invalid contracts/frontend-implementation-contract.json");
342
- }
343
- const candidateNodeIds = [
344
- "frontend-verify-assess-shell",
345
- "frontend-reverify-shell",
346
- "frontend-static-verify-shell",
347
- "frontend-behavior-verify-shell",
348
- "frontend-verification-trace-shell",
349
- "frontend-static-reverify-shell",
350
- "frontend-behavior-reverify-shell",
351
- "frontend-verification-retrace-shell",
352
- ];
353
- const failed = [
354
- ...(input.failureFacts ?? []).filter(({ record }) => nodeHadFailure(record) || recordIndicatesCommandFailure(record)),
355
- ];
356
- for (const nodeId of candidateNodeIds) {
357
- if (failed.some((item) => item.nodeId === nodeId))
358
- continue;
359
- const nodePath = path.join(input.runDir, `${nodeId}.json`);
360
- try {
361
- const record = JSON.parse(await readFile(nodePath, "utf8"));
362
- if (nodeHadFailure(record) || recordIndicatesCommandFailure(record)) {
363
- failed.push({ nodeId, record });
364
- }
365
- }
366
- catch {
367
- // missing optional nodes ok
368
- }
369
- }
370
- if (failed.length === 0) {
371
- const assessment = {
372
- schemaVersion: 1,
373
- schemaId: FRONTEND_REPAIR_ASSESSMENT_SCHEMA_ID,
374
- contract: {
375
- relativePath: contractRel,
376
- schemaId: contractSchemaId,
377
- },
378
- failedNodeIds: [],
379
- failureClass: "unknown",
380
- eligible: false,
381
- reason: "no failed frontend verify/trace nodes in current run",
382
- attempt,
383
- maxAttempts: 1,
384
- allowedRepairWriteSet: [...implementWriteSet],
385
- evidenceRefs: [contractRel],
386
- browserStatus: "not-run",
387
- };
388
- await writeAssessment(input.runDir, assessment);
389
- return assessment;
390
- }
391
- const primary = failed[0];
392
- const evidence = deriveFailureEvidence({ contract, failed });
393
- const failureClass = classifyFrontendFailure({
394
- nodeId: primary.nodeId,
395
- failureCategory: primary.record.failureCategory,
396
- stdout: primary.record.stdout,
397
- stderr: primary.record.stderr,
398
- commandLabel: evidence.commandLabel,
399
- commandCategory: evidence.commandCategory,
400
- exitCode: evidence.exitCode,
401
- traceTarget: evidence.traceTarget ?? null,
402
- changedPaths: evidence.changedPaths,
403
- });
404
- // AC-5: repairable=true requires structured evidence (a resolved exitCode,
405
- // commandCategory, or trace-bound verification target). review has its own
406
- // stricter gate (AC-4) and is never unconditionally repairable.
407
- const baseEligible = isFrontendRepairable(failureClass) && attempt <= 1;
408
- let eligible;
409
- let reason;
410
- if (failureClass === "review") {
411
- const finding = primary.record.reviewFinding ?? {};
412
- eligible = attempt <= 1 && isReviewRepairable(finding, implementWriteSet);
413
- reason = eligible
414
- ? `repairable review finding on ${primary.nodeId}`
415
- : `review finding lacks file/line-symbol/requirement evidence or is outside writeSet on ${primary.nodeId}`;
416
- }
417
- else {
418
- const hasStructuredEvidence = evidence.exitCode !== undefined ||
419
- evidence.commandCategory !== undefined ||
420
- Boolean(evidence.verificationTargetId);
421
- eligible = baseEligible && hasStructuredEvidence;
422
- reason = eligible
423
- ? `repairable failureClass=${failureClass} on ${primary.nodeId}`
424
- : !baseEligible
425
- ? `non-repairable or ineligible: failureClass=${failureClass} attempt=${attempt}`
426
- : `missing structured evidence for failureClass=${failureClass} on ${primary.nodeId}`;
427
- }
428
- // fixScope and changedPaths must stay inside the implement writeSet.
429
- if (evidence.fixScope?.length) {
430
- assertWriteSetSubset(evidence.fixScope, implementWriteSet);
431
- }
432
- if (evidence.changedPaths?.length) {
433
- assertWriteSetSubset(evidence.changedPaths, implementWriteSet);
434
- }
435
- const assessment = {
436
- schemaVersion: 1,
437
- schemaId: FRONTEND_REPAIR_ASSESSMENT_SCHEMA_ID,
438
- contract: {
439
- relativePath: contractRel,
440
- schemaId: contractSchemaId,
441
- },
442
- failedNodeIds: failed.map((item) => item.nodeId),
443
- failureClass,
444
- eligible,
445
- reason,
446
- attempt,
447
- maxAttempts: 1,
448
- allowedRepairWriteSet: [...implementWriteSet],
449
- evidenceRefs: [contractRel, ...failed.map((item) => `${item.nodeId}.json`)],
450
- browserStatus: "not-run",
451
- ...(evidence.verificationTargetId
452
- ? { verificationTargetId: evidence.verificationTargetId }
453
- : {}),
454
- ...(evidence.commandLabel ? { commandLabel: evidence.commandLabel } : {}),
455
- ...(evidence.exitCode !== undefined ? { exitCode: evidence.exitCode } : {}),
456
- ...(evidence.commandCategory
457
- ? { commandCategory: evidence.commandCategory }
458
- : {}),
459
- ...(evidence.requirementIds?.length
460
- ? { requirementIds: evidence.requirementIds }
461
- : {}),
462
- ...(evidence.acceptanceCriteriaIds?.length
463
- ? { acceptanceCriteriaIds: evidence.acceptanceCriteriaIds }
464
- : {}),
465
- ...(evidence.fixScope?.length ? { fixScope: evidence.fixScope } : {}),
466
- ...(evidence.changedPaths?.length
467
- ? { changedPaths: evidence.changedPaths }
468
- : {}),
469
- ...(evidence.traceRef ? { traceRef: evidence.traceRef } : {}),
470
- };
471
- await writeAssessment(input.runDir, assessment);
472
- return assessment;
473
- }
474
- function recordIndicatesCommandFailure(record) {
475
- // Prefer explicit commandResults when present
476
- if (record.commandResults?.some((item) => item.ok === false))
477
- return true;
478
- // Node may be FINISHED with nonZeroExitPolicy=record; look for harness markers
479
- const stdout = record.stdout ?? "";
480
- if (/nonZeroExitPolicy=record/.test(stdout) &&
481
- /exitCode=[1-9]/.test(stdout)) {
482
- return true;
483
- }
484
- if (record.failureCategory && record.failureCategory !== "success") {
485
- // recorded failures may still be FINISHED
486
- if (record.status === "FINISHED") {
487
- return (record.failureCategory === "nonzero-exit" ||
488
- record.failureCategory === "invalid-output" ||
489
- record.failureCategory === "timeout");
490
- }
491
- }
492
- return false;
493
- }
494
- async function writeAssessment(runDir, assessment) {
495
- const relative = "contracts/frontend-repair-assessment.json";
496
- const absolute = path.join(runDir, relative);
497
- await mkdir(path.dirname(absolute), { recursive: true });
498
- await writeFile(absolute, `${JSON.stringify(assessment, null, 2)}\n`, "utf8");
499
- return absolute;
500
- }
501
- export async function runFrontendRepairContractGate(input) {
502
- const relative = "contracts/frontend-repair-assessment.json";
503
- const absolute = path.join(input.runDir, relative);
504
- const raw = JSON.parse(await readFile(absolute, "utf8"));
505
- const parsed = frontendRepairAssessmentSchema.safeParse(raw);
506
- if (!parsed.success) {
507
- throw new Error(`repair-contract: invalid assessment: ${parsed.error.issues
508
- .map((i) => i.message)
509
- .join("; ")}`);
510
- }
511
- const assessment = parsed.data;
512
- const implementWriteSet = await loadImplementWriteSet(input.runDir, input.implementWriteSet ?? []);
513
- assertWriteSetSubset(assessment.allowedRepairWriteSet, implementWriteSet.length
514
- ? implementWriteSet
515
- : assessment.allowedRepairWriteSet);
516
- if (assessment.attempt > assessment.maxAttempts) {
517
- throw new Error("repair-contract: attempt exceeds maxAttempts");
518
- }
519
- if (assessment.failedNodeIds.length === 0) {
520
- return { ok: true, assessment };
521
- }
522
- if (!assessment.eligible) {
523
- throw new Error(`repair-contract: non-repairable failure (${assessment.failureClass}): ${assessment.reason}`);
524
- }
525
- if (!isFrontendRepairable(assessment.failureClass) &&
526
- assessment.failureClass !== "review") {
527
- throw new Error(`repair-contract: failureClass ${assessment.failureClass} is not repairable`);
528
- }
529
- return { ok: true, assessment };
530
- }
531
- export function formatFrontendFailureAssessStdout(assessment) {
532
- return JSON.stringify(assessment);
533
- }
534
- export function formatFrontendRepairContractStdout(assessment) {
535
- return [
536
- "Frontend repair contract: pass",
537
- `eligible=${assessment.eligible}`,
538
- `failureClass=${assessment.failureClass}`,
539
- `writeSetSize=${assessment.allowedRepairWriteSet.length}`,
540
- ].join("\n");
541
- }
@@ -1,11 +1,159 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
4
5
  import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "./frontend-worktree-diff.js";
5
6
  import { frontendLintAssessmentArtifactSchema } from "./frontend-lint-baseline.js";
6
- import { frontendImplementationContractSchema } from "./frontend-implementation-contract.js";
7
- import { frontendRepairAssessmentSchema } from "./frontend-repair.js";
7
+ import { deterministicSha256, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
8
8
  export const FRONTEND_REVIEW_CONTEXT_SCHEMA_ID = "frontend-review-context-v1";
9
+ /**
10
+ * Deterministically assemble the cumulative diff from a task baseline and the
11
+ * current candidate snapshot. Same input always yields the same output:
12
+ * paths are sorted, statuses are derived from a fixed decision table, and both
13
+ * digests use canonical JSON hashing.
14
+ */
15
+ export function assembleCumulativeDiffContext(input) {
16
+ const baselineByPath = new Map(input.baselineEntries.map((entry) => [entry.path, entry.sha256]));
17
+ const currentByPath = new Map(input.currentEntries.map((entry) => [entry.path, entry.sha256]));
18
+ const allPaths = new Set([
19
+ ...baselineByPath.keys(),
20
+ ...currentByPath.keys(),
21
+ ]);
22
+ const sortedPaths = [...allPaths].sort();
23
+ const perPath = sortedPaths.map((p) => {
24
+ const baselineSha256 = baselineByPath.get(p) ?? null;
25
+ const hasCurrent = currentByPath.has(p);
26
+ const currentSha256 = hasCurrent
27
+ ? (currentByPath.get(p) ?? null)
28
+ : null;
29
+ let status;
30
+ if (baselineSha256 === null) {
31
+ status = currentSha256 === null ? "deleted" : "added";
32
+ }
33
+ else if (currentSha256 === null) {
34
+ status = hasCurrent ? "deleted" : "unchanged";
35
+ }
36
+ else {
37
+ status = currentSha256 === baselineSha256 ? "unchanged" : "modified";
38
+ }
39
+ return { path: p, status, baselineSha256, currentSha256 };
40
+ });
41
+ const baselineDigest = deterministicSha256([...input.baselineEntries].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
42
+ const cumulativeDigest = deterministicSha256(perPath);
43
+ return {
44
+ schemaId: "frontend-cumulative-diff-context-v1",
45
+ baselineDigest,
46
+ perPath,
47
+ cumulativeDigest,
48
+ empty: perPath.every((entry) => entry.status === "unchanged"),
49
+ };
50
+ }
51
+ async function sha256File(filePath) {
52
+ try {
53
+ const bytes = await readFile(filePath);
54
+ return createHash("sha256").update(bytes).digest("hex");
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ /**
61
+ * Capture the cumulative diff from the frozen worktree baseline and the
62
+ * run-owned diff gate output. Pure status/digest logic lives in
63
+ * `assembleCumulativeDiffContext`; this wrapper only does IO.
64
+ */
65
+ export async function captureCumulativeDiffContext(input) {
66
+ const root = path.resolve(input.workspaceRoot);
67
+ let baselineEntries = [];
68
+ try {
69
+ const raw = JSON.parse(await readFile(path.join(input.runDir, "contracts/frontend-worktree-baseline.json"), "utf8"));
70
+ baselineEntries = (raw.files ?? [])
71
+ .map((file) => ({ path: file, sha256: raw.hashes?.[file] ?? null }))
72
+ .filter((entry) => entry.sha256 !== null);
73
+ }
74
+ catch {
75
+ // Missing baseline → empty baseline entries (still deterministic).
76
+ }
77
+ const candidatePaths = [
78
+ ...new Set([...input.diff.changedFiles, ...input.diff.untrackedFiles]),
79
+ ];
80
+ const currentEntries = [];
81
+ for (const file of candidatePaths) {
82
+ currentEntries.push({
83
+ path: file,
84
+ sha256: await sha256File(path.join(root, file)),
85
+ });
86
+ }
87
+ return assembleCumulativeDiffContext({ baselineEntries, currentEntries });
88
+ }
89
+ const TYPED_TO_LEGACY = {
90
+ approve_review: "pass",
91
+ request_review_changes: "request-revision",
92
+ };
93
+ function isTypedReviewTerminalVerdict(value) {
94
+ return value === "approve_review" || value === "request_review_changes";
95
+ }
96
+ function isLegacyReviewVerdict(value) {
97
+ return value === "pass" || value === "request-revision";
98
+ }
99
+ /**
100
+ * Deterministic, fail-closed equivalence between the typed review terminal
101
+ * verdict(s) observed in the attempt and the legacy JSON verdict.
102
+ *
103
+ * Fail-closed cases (all yield `match: false` with an explicit reason):
104
+ * - no typed terminal fact;
105
+ * - more than one typed terminal fact (conflicting);
106
+ * - an unknown typed terminal kind;
107
+ * - a missing/illegal legacy JSON verdict.
108
+ */
109
+ export function compareTypedReviewToLegacyJsonVerdict(input) {
110
+ const kinds = [...input.typedKinds];
111
+ const legacyVerdict = isLegacyReviewVerdict(input.legacyVerdict ?? "")
112
+ ? input.legacyVerdict
113
+ : undefined;
114
+ if (kinds.length === 0) {
115
+ return {
116
+ typedVerdict: undefined,
117
+ legacyVerdict,
118
+ match: false,
119
+ reason: "typed review terminal fact missing",
120
+ };
121
+ }
122
+ if (kinds.length > 1) {
123
+ return {
124
+ typedVerdict: undefined,
125
+ legacyVerdict,
126
+ match: false,
127
+ reason: `conflicting typed review terminal facts: ${[...new Set(kinds)].join(",")}`,
128
+ };
129
+ }
130
+ const typedVerdict = kinds[0];
131
+ if (!isTypedReviewTerminalVerdict(typedVerdict)) {
132
+ return {
133
+ typedVerdict: undefined,
134
+ legacyVerdict,
135
+ match: false,
136
+ reason: `unknown typed review terminal fact kind: ${typedVerdict}`,
137
+ };
138
+ }
139
+ if (!legacyVerdict) {
140
+ return {
141
+ typedVerdict,
142
+ legacyVerdict: undefined,
143
+ match: false,
144
+ reason: "legacy JSON verdict missing or not pass|request-revision",
145
+ };
146
+ }
147
+ const expected = TYPED_TO_LEGACY[typedVerdict];
148
+ return {
149
+ typedVerdict,
150
+ legacyVerdict,
151
+ match: expected === legacyVerdict,
152
+ reason: expected === legacyVerdict
153
+ ? "typed review verdict matches legacy JSON verdict"
154
+ : `typed review verdict ${typedVerdict} maps to ${expected} but legacy JSON verdict is ${legacyVerdict}`,
155
+ };
156
+ }
9
157
  async function readRequiredJson(runDir, relativePath) {
10
158
  try {
11
159
  return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
@@ -30,6 +178,37 @@ async function readOptionalLintAssessment(runDir) {
30
178
  throw new Error(`frontend review context invalid ${relativePath}`);
31
179
  }
32
180
  }
181
+ async function readOptionalRepairAssessment(runDir) {
182
+ const relativePath = "contracts/frontend-repair-assessment.json";
183
+ try {
184
+ return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
185
+ }
186
+ catch (error) {
187
+ if (error &&
188
+ typeof error === "object" &&
189
+ "code" in error &&
190
+ error.code === "ENOENT") {
191
+ return undefined;
192
+ }
193
+ throw new Error(`frontend review context invalid ${relativePath}`);
194
+ }
195
+ }
196
+ /** M8: verify failure ownership facts written by frontend-verify-shell. */
197
+ async function readOptionalVerifyFailure(runDir) {
198
+ const relativePath = "contracts/frontend-verify-failure.json";
199
+ try {
200
+ return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
201
+ }
202
+ catch (error) {
203
+ if (error &&
204
+ typeof error === "object" &&
205
+ "code" in error &&
206
+ error.code === "ENOENT") {
207
+ return undefined;
208
+ }
209
+ throw new Error(`frontend review context invalid ${relativePath}`);
210
+ }
211
+ }
33
212
  function assertReviewEvidence(input) {
34
213
  const contract = frontendImplementationContractSchema.safeParse(input.contract);
35
214
  if (!contract.success)
@@ -71,9 +250,9 @@ function assertReviewEvidence(input) {
71
250
  trace.mockStrategy !== contract.data.mockApi.strategy) {
72
251
  throw new Error("frontend review context verification trace is not bound to a passing contract");
73
252
  }
74
- const assessment = frontendRepairAssessmentSchema.safeParse(input.repairAssessment);
75
- if (!assessment.success)
76
- throw new Error("frontend review context invalid repair assessment");
253
+ // M8: no repair-assessment hard requirement. When the artifact exists it must
254
+ // be schema-valid; when absent the verify failure capsule (if any) is
255
+ // already surfaced through the payload as verifyFailure.
77
256
  }
78
257
  export async function runFrontendReviewContextGate(input) {
79
258
  const contract = await readRequiredJson(input.runDir, "contracts/frontend-implementation-contract.json");
@@ -85,15 +264,22 @@ export async function runFrontendReviewContextGate(input) {
85
264
  authorizedChangedPaths: parsedContract.data.targets.files,
86
265
  });
87
266
  const verificationTrace = await readRequiredJson(input.runDir, "contracts/frontend-verification-trace.json");
88
- const repairAssessment = await readRequiredJson(input.runDir, "contracts/frontend-repair-assessment.json");
267
+ const repairAssessment = await readOptionalRepairAssessment(input.runDir);
268
+ const verifyFailure = await readOptionalVerifyFailure(input.runDir);
89
269
  const lintAssessment = await readOptionalLintAssessment(input.runDir);
90
- assertReviewEvidence({ contract, verificationTrace, repairAssessment });
270
+ assertReviewEvidence({ contract, verificationTrace });
271
+ const cumulativeDiff = await captureCumulativeDiffContext({
272
+ runDir: input.runDir,
273
+ workspaceRoot: input.workspaceRoot,
274
+ diff,
275
+ });
91
276
  const payload = {
92
277
  schemaVersion: 1,
93
278
  schemaId: FRONTEND_REVIEW_CONTEXT_SCHEMA_ID,
94
279
  contract,
95
280
  verificationTrace,
96
- repairAssessment,
281
+ ...(repairAssessment ? { repairAssessment } : {}),
282
+ ...(verifyFailure ? { verifyFailure } : {}),
97
283
  ...(lintAssessment ? { lintAssessment } : {}),
98
284
  diff: {
99
285
  schemaId: diff.schemaId,
@@ -103,6 +289,7 @@ export async function runFrontendReviewContextGate(input) {
103
289
  untrackedFiles: diff.untrackedFiles,
104
290
  empty: diff.empty,
105
291
  },
292
+ cumulativeDiff,
106
293
  };
107
294
  const artifactPath = await writeDagRunJsonArtifact(input.runDir, "contracts/frontend-review-context.json", payload);
108
295
  return { ok: true, artifactPath, diff };