@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,8 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
3
4
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
4
5
  import path from "node:path";
5
- import { writeDagNodeJsonArtifact, writeDagNodeTextArtifact, } from "../infrastructure/harness/artifact-store.js";
6
+ import { writeDagNodeJsonArtifact, writeDagNodeTextArtifact, writeDagRunJsonArtifact, } from "../infrastructure/harness/artifact-store.js";
6
7
  import { captureWorkspaceCheckpoint } from "../workflows/dag/workspace-checkpoint.js";
7
8
  import { truncateOutput } from "../shared/output-truncation.js";
8
9
  import { processTreeSpawnOptions, terminateProcessTree, } from "./process-tree.js";
@@ -14,15 +15,25 @@ import { buildRequirementCoverageGateShellCommand, expandShellPreset, buildVerdi
14
15
  import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend-test-analysis-contract.js";
15
16
  import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
16
17
  import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
18
+ import { assertFrontendSourceBindingFresh, buildContractFromCommittedFacts, deterministicSha256, frontendImplementationContractSchema, serializeDeterministicJson, sha256Hex, writeFrontendImplementationContractArtifact, } from "../workflows/dag/frontend-implementation-contract.js";
17
19
  import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
20
+ import { copyFrontendTestStandardScenarios } from "../workflows/dag/frontend-test-standard-scenarios.js";
21
+ import { probeFrontendTestEnvironment } from "../workflows/dag/frontend-test-environment-probe.js";
18
22
  import { renderFrontendTestL5Report } from "../workflows/dag/frontend-test-l5-report.js";
19
23
  import { validateFrontendCaseChecklist } from "../workflows/dag/frontend-test-case-checklist.js";
20
24
  import { materializeFrontendTestCaseManifest } from "../workflows/dag/frontend-test-case-manifest.js";
21
25
  import { renderFrontendTestHtmlReport } from "../workflows/dag/frontend-test-html-report.js";
22
26
  import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate, } from "../workflows/dag/frontend-verification-trace.js";
23
- import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
24
- import { formatFrontendFailureAssessStdout, formatFrontendRepairContractStdout, runFrontendFailureAssessGate, runFrontendRepairContractGate, } from "../workflows/dag/frontend-repair.js";
25
- import { formatFrontendPrewriteGateStdout, runFrontendPrewriteGate, } from "../workflows/dag/frontend-prewrite-gate.js";
27
+ import { captureFrontendWorktreeBaseline, formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
28
+ import { readTypedEventStoreFromJsonl } from "../workflows/dag/frontend-typed-event-store.js";
29
+ import { classifyFrontendFailure, } from "../workflows/dag/frontend-repair.js";
30
+ import { FRONTEND_CONTRACT_MAX_BYTES, formatFrontendPrewriteGateStdout, runFrontendPrewriteGate, } from "../workflows/dag/frontend-prewrite-gate.js";
31
+ import { evaluateFrontendDesignPolicy, } from "../workflows/dag/frontend-design-policy.js";
32
+ import { deriveFrontendWriterAdmissionShell, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, frontendWriterAdmissionResultV1Schema, } from "../workflows/dag/frontend-writer-admission.js";
33
+ import { deriveFrontendIntegrationFacts, buildFrontendTestHandoff, frontendCloseoutFactsSchema, renderFrontendCloseout, } from "../workflows/dag/frontend-closeout.js";
34
+ import { buildFrontendHumanDecisionEnvelope, evaluateHighAdmissionReceipt, routeVerificationConfig, } from "../workflows/dag/frontend-human-decision.js";
35
+ import { classifyFailureOwner, classifyIssueCategoryRestartPhase, classifyRestartPhase, } from "../workflows/dag/frontend-recovery-capsule.js";
36
+ import { pathMatchesPattern } from "../shared/git-progress.js";
26
37
  import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from "../workflows/dag/frontend-review-context.js";
27
38
  import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
28
39
  import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
@@ -121,6 +132,39 @@ function shouldReuseSuccessfulShellCommands(shell) {
121
132
  const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
122
133
  const SUMMARY_STDOUT_MAX = 4_000;
123
134
  const SUMMARY_STDERR_MAX = 2_000;
135
+ const TEST_TIMEOUT_CLASSIFY_MAX_CHARS = 8_000;
136
+ const TEST_TIMEOUT_MARKERS = [
137
+ /Test timed out in \d+ms/i,
138
+ /Timeout of \d+ms exceeded/i,
139
+ ];
140
+ const DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS = [
141
+ /AssertionError/i,
142
+ /expected:?\s/i,
143
+ /received:?\s/i,
144
+ /Snapshot /i,
145
+ /toMatchSnapshot/i,
146
+ /SyntaxError/i,
147
+ /TS\d{4}/,
148
+ /error TS/i,
149
+ /eslint/i,
150
+ /compile error/i,
151
+ /Failed to compile/i,
152
+ ];
153
+ function isIdentifiedTestCommand(command) {
154
+ return /(?:^|[\s"'`])(?:npx\s+)?vitest(?:\s|$)|(?:^|[\s"'`])npm(?:\.cmd)?\s+test(?:\s|$)|(?:^|[\s"'`])pnpm(?:\.cmd)?\s+test(?:\s|$)|(?:^|[\s"'`])yarn(?:\.cmd)?\s+test(?:\s|$)/i.test(command);
155
+ }
156
+ function classifyShellCommandFailure(input) {
157
+ if (input.processCategory !== "nonzero-exit")
158
+ return input.processCategory;
159
+ if (!isIdentifiedTestCommand(input.command))
160
+ return "nonzero-exit";
161
+ const sample = `${input.stdout}\n${input.stderr}`.slice(0, TEST_TIMEOUT_CLASSIFY_MAX_CHARS);
162
+ const hasTimeoutMarker = TEST_TIMEOUT_MARKERS.some((marker) => marker.test(sample));
163
+ if (!hasTimeoutMarker)
164
+ return "nonzero-exit";
165
+ const mixedDeterministicFailure = DETERMINISTIC_NON_TIMEOUT_FAILURE_MARKERS.some((marker) => marker.test(sample));
166
+ return mixedDeterministicFailure ? "nonzero-exit" : "test-timeout";
167
+ }
124
168
  const NODE_STDOUT_MAX = 12_000;
125
169
  const NODE_STDERR_MAX = 8_000;
126
170
  const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
@@ -327,16 +371,21 @@ export async function executeShellCommand(input) {
327
371
  });
328
372
  });
329
373
  child.on("close", (code) => {
330
- let failureCategory = "nonzero-exit";
374
+ let processCategory = "nonzero-exit";
331
375
  if (timedOut) {
332
- failureCategory = "timeout";
376
+ processCategory = "timeout";
333
377
  }
334
378
  else if (code === 0) {
335
- failureCategory = "success";
379
+ processCategory = "success";
336
380
  }
337
381
  finish({
338
382
  exitCode: code,
339
- failureCategory,
383
+ failureCategory: classifyShellCommandFailure({
384
+ command: input.command,
385
+ processCategory,
386
+ stdout: formatBoundedOutput(stdout),
387
+ stderr: formatBoundedOutput(stderr),
388
+ }),
340
389
  ok: code === 0 && !timedOut,
341
390
  });
342
391
  });
@@ -1740,6 +1789,45 @@ async function executePipelineCommands(input, meta, overrideCommands) {
1740
1789
  }
1741
1790
  return results;
1742
1791
  }
1792
+ /** A+B (AC-008): derive unresolved verification entrypoints from committed plan
1793
+ * facts. A verification target is unresolved when its commandLabel does not
1794
+ * match any frozen command label from the verify shell evidence bundle.
1795
+ * Returns `{ ref, digest }` entries; a missing/unreadable plan ledger fails
1796
+ * closed to [] so the router restarts the contract instead of inventing refs. */
1797
+ async function deriveUnresolvedVerificationEntrypointRefs(runDir, frozenCommandLabels) {
1798
+ try {
1799
+ const { restorePlanPatchFromCommittedFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
1800
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, "frontend-plan-pi", "plan-typed-facts.jsonl"));
1801
+ const patch = restorePlanPatchFromCommittedFacts(records);
1802
+ const targets = patch
1803
+ ?.verificationTargets;
1804
+ if (!Array.isArray(targets))
1805
+ return [];
1806
+ const frozen = new Set(frozenCommandLabels
1807
+ .map((label) => label.trim().toLowerCase())
1808
+ .filter(Boolean));
1809
+ const refs = [];
1810
+ for (const item of targets) {
1811
+ const target = item;
1812
+ const commandLabel = typeof target.commandLabel === "string" ? target.commandLabel : "";
1813
+ if (commandLabel &&
1814
+ frozen.has(commandLabel.trim().toLowerCase())) {
1815
+ continue;
1816
+ }
1817
+ const file = typeof target.file === "string" ? target.file : "";
1818
+ const symbol = typeof target.symbol === "string" ? target.symbol : "";
1819
+ const id = typeof target.id === "string" ? target.id : "";
1820
+ const ref = file || symbol || id;
1821
+ if (!ref)
1822
+ continue;
1823
+ refs.push({ ref, digest: sha256Hex(ref) });
1824
+ }
1825
+ return refs;
1826
+ }
1827
+ catch {
1828
+ return [];
1829
+ }
1830
+ }
1743
1831
  async function executeFrontendVerificationBundle(input, meta) {
1744
1832
  const started = Date.now();
1745
1833
  const shell = input.task.shell;
@@ -1894,75 +1982,807 @@ async function executeFrontendVerificationBundle(input, meta) {
1894
1982
  catch (error) {
1895
1983
  traceError = error instanceof Error ? error : new Error(String(error));
1896
1984
  }
1897
- if (bundle.mode === "repair") {
1898
- if (firstFailure || lintSyntheticFailure || traceError) {
1985
+ const verifyFailed = firstFailure !== undefined || lintSyntheticFailure !== undefined || traceError !== undefined;
1986
+ if (verifyFailed) {
1987
+ // M8: verify failure is TERMINAL. There is no same-run repair branch and
1988
+ // no frontend-repair-assessment.json; we only materialize failure
1989
+ // ownership facts (frontend-repair classification + recovery capsule
1990
+ // ownership) for the operation controller / recovery run.
1991
+ const failedNodeId = input.task.id;
1992
+ const failureCategory = firstFailure?.failureCategory ??
1993
+ lintSyntheticFailure?.failureCategory ??
1994
+ (traceError ? "invalid-output" : undefined) ??
1995
+ "invalid-output";
1996
+ const failureClass = classifyFrontendFailure({
1997
+ nodeId: failedNodeId,
1998
+ failureCategory,
1999
+ stdout: firstFailure?.stdout,
2000
+ stderr: firstFailure?.stderr ?? traceError?.message,
2001
+ });
2002
+ // A verify failure means the writer's candidate failed verification, so
2003
+ // the recovery ownership maps to the implement phase (frontend-recovery
2004
+ // capsule classification, fail-closed to implement-pi).
2005
+ const classifiedOwner = classifyFailureOwner({
2006
+ failureSource: "frontend-verify-shell",
2007
+ nodeId: failedNodeId,
2008
+ rawFailureCategory: failureCategory,
2009
+ });
2010
+ const failureOwner = classifiedOwner === "unknown" ? "implement-pi" : classifiedOwner;
2011
+ const restartPhase = classifyRestartPhase(failureOwner);
2012
+ // A+B (AC-008): verification-config routing. Produce structured
2013
+ // unresolved entrypoint refs from committed plan facts and the frozen
2014
+ // command labels; a non-empty unresolved set routes to
2015
+ // verification-config-decision and materializes the frozen
2016
+ // verification-config envelope. An empty/untyped signal fails closed to
2017
+ // contract-restart. The decision is recorded so the recovery controller
2018
+ // never re-classifies the same signal twice.
2019
+ let verificationConfigRoute;
2020
+ if (failureOwner === "verification-config") {
2021
+ const frozenCommandLabels = [
2022
+ ...(bundle.staticEvidence.commandLabels ?? []),
2023
+ ...(bundle.behaviorEvidence.commandLabels ?? []),
2024
+ ...(bundle.mockEvidence?.commandLabels ?? []),
2025
+ ];
2026
+ const unresolvedEntrypointRefs = await deriveUnresolvedVerificationEntrypointRefs(meta.runDir, frozenCommandLabels);
2027
+ verificationConfigRoute = routeVerificationConfig({
2028
+ unresolvedEntrypointRefs: unresolvedEntrypointRefs.map((ref) => ref.ref),
2029
+ });
2030
+ const allowlistedResolvers = [
2031
+ ...new Set(frozenCommandLabels
2032
+ .map((label) => label.trim())
2033
+ .filter(Boolean)),
2034
+ ].sort();
2035
+ if (verificationConfigRoute.decision ===
2036
+ "verification-config-decision" &&
2037
+ unresolvedEntrypointRefs.length > 0 &&
2038
+ allowlistedResolvers.length > 0) {
2039
+ const envelope = buildFrontendHumanDecisionEnvelope({
2040
+ operationId: input.task.id,
2041
+ runId: meta.runId,
2042
+ operationRevision: 1,
2043
+ targetDigest: sha256Hex(frozenCommandLabels.slice().sort().join("\n")),
2044
+ staleOnTargetDigestChange: true,
2045
+ kind: "verification-config",
2046
+ resumeAction: "select-verification-config",
2047
+ payload: {
2048
+ unresolvedEntrypointRefs,
2049
+ allowlistedResolvers,
2050
+ },
2051
+ });
2052
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verification-config-envelope.json", envelope);
2053
+ }
2054
+ }
2055
+ const verifyFailure = {
2056
+ schemaVersion: 1,
2057
+ schemaId: "frontend-verify-failure-v1",
2058
+ nodeId: failedNodeId,
2059
+ failureCategory,
2060
+ failureClass,
2061
+ failureOwner,
2062
+ restartPhase,
2063
+ ...(verificationConfigRoute
2064
+ ? { verificationConfigRoute }
2065
+ : {}),
2066
+ commandResults,
2067
+ failedAt: new Date().toISOString(),
2068
+ };
2069
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-verify-failure.json", verifyFailure);
2070
+ return {
2071
+ ok: false,
2072
+ stdout: summarizeCommandResults(results).stdout,
2073
+ stderr: firstFailure?.stderr ||
2074
+ lintSyntheticFailure?.stderr ||
2075
+ traceError?.message ||
2076
+ "frontend verification failed",
2077
+ failureCategory,
2078
+ durationMs: Date.now() - started,
2079
+ ...{ commandResults },
2080
+ };
2081
+ }
2082
+ // A+B (AC-008): when lint is `require-clean` but the assessment proves
2083
+ // baseline-debt, materialize the frozen `baseline-debt-acceptance` envelope
2084
+ // whose only positive resume is `accept-baseline-debt`. Evidence stays
2085
+ // `baseline-debt` and is never rewritten to `passed`.
2086
+ if (lintAssessment?.status === "baseline-debt") {
2087
+ const baselineEvidenceRefs = (lintAssessment.rawEvidenceRefs ?? [])
2088
+ .filter((ref) => ref?.path && ref?.sha256)
2089
+ .map((ref) => ({ ref: ref.path, digest: ref.sha256 }));
2090
+ if (baselineEvidenceRefs.length > 0) {
2091
+ const envelope = buildFrontendHumanDecisionEnvelope({
2092
+ operationId: input.task.id,
2093
+ runId: meta.runId,
2094
+ operationRevision: 1,
2095
+ targetDigest: lintAssessment.commandIdentity.sha256,
2096
+ staleOnTargetDigestChange: true,
2097
+ kind: "baseline-debt-acceptance",
2098
+ resumeAction: "accept-baseline-debt",
2099
+ payload: {
2100
+ baselineEvidenceRefs,
2101
+ candidateDigest: lintAssessment.commandIdentity.sha256,
2102
+ commandDigest: lintAssessment.commandIdentity.sha256,
2103
+ resultingPolicy: "allow-non-regression",
2104
+ },
2105
+ });
2106
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-baseline-debt-acceptance-envelope.json", envelope);
2107
+ }
2108
+ }
2109
+ return {
2110
+ ok: true,
2111
+ stdout: lintAssessment?.status === "baseline-debt"
2112
+ ? "Frontend verification bundle: pass with lint baseline-debt"
2113
+ : "Frontend verification bundle: pass",
2114
+ stderr: "",
2115
+ failureCategory: "success",
2116
+ durationMs: Date.now() - started,
2117
+ ...{ commandResults },
2118
+ };
2119
+ }
2120
+ /* ------------------------------------------------------------------ *
2121
+ * M8 deterministic frontend shell nodes: design policy, writer *
2122
+ * admission, closeout. These replace the prewrite-gate authorization *
2123
+ * path and the Pi closeout node. *
2124
+ * ------------------------------------------------------------------ */
2125
+ async function readRunDirJson(runDir, relativePath) {
2126
+ return JSON.parse(await readFile(path.join(runDir, relativePath), "utf8"));
2127
+ }
2128
+ /** Committed typed review terminal kinds from the review node's typed event
2129
+ * store (AC-003: typed terminal tools are the only authoritative verdict). */
2130
+ async function collectReviewTerminalKinds(runDir, reviewNodeId) {
2131
+ try {
2132
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
2133
+ return records
2134
+ .filter((record) => record.phase === "committed")
2135
+ .map((record) => {
2136
+ const kind = record.fact.kind;
2137
+ return typeof kind === "string" ? kind : "";
2138
+ })
2139
+ .filter(Boolean);
2140
+ }
2141
+ catch {
2142
+ return [];
2143
+ }
2144
+ }
2145
+ /** Committed typed design terminal kinds from the design review node's typed
2146
+ * event store (M8: the committed approve_design / request_design_changes fact
2147
+ * is the only authoritative verdict; the legacy first-line VERDICT text is
2148
+ * never read for writer admission). Fail-closed on a missing/unreadable store. */
2149
+ async function collectDesignTerminalKinds(runDir, designNodeId) {
2150
+ try {
2151
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
2152
+ return records
2153
+ .filter((record) => record.phase === "committed")
2154
+ .map((record) => {
2155
+ const kind = record.fact.kind;
2156
+ return typeof kind === "string" ? kind : "";
2157
+ })
2158
+ .filter(Boolean);
2159
+ }
2160
+ catch {
2161
+ return [];
2162
+ }
2163
+ }
2164
+ /** A+B (AC-009): deterministic §4.10 restart phase for a committed
2165
+ * `request_review_changes` fact. The typed issueCategory is mapped through the
2166
+ * frozen classifier; `unknown` (or an unreadable store) fails closed to
2167
+ * `undefined` so the runtime never trusts an untyped category. */
2168
+ async function collectReviewRestartPhase(runDir, reviewNodeId) {
2169
+ try {
2170
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, reviewNodeId, "review-typed-facts.jsonl"));
2171
+ const request = records.find((record) => record.phase === "committed" &&
2172
+ record.fact.kind === "request_review_changes");
2173
+ const issueCategory = request?.fact
2174
+ ?.issueCategory;
2175
+ return typeof issueCategory === "string"
2176
+ ? (classifyIssueCategoryRestartPhase(issueCategory) ?? undefined)
2177
+ : undefined;
2178
+ }
2179
+ catch {
2180
+ return undefined;
2181
+ }
2182
+ }
2183
+ /** A+B (AC-009): deterministic §4.10 restart phase for a committed
2184
+ * `request_design_changes` fact. Mirrors the review collector. */
2185
+ async function collectDesignRestartPhase(runDir, designNodeId) {
2186
+ try {
2187
+ const records = await readTypedEventStoreFromJsonl(path.join(runDir, designNodeId, "design-typed-facts.jsonl"));
2188
+ const request = records.find((record) => record.phase === "committed" &&
2189
+ record.fact.kind === "request_design_changes");
2190
+ const issueCategory = request?.fact
2191
+ ?.issueCategory;
2192
+ return typeof issueCategory === "string"
2193
+ ? (classifyIssueCategoryRestartPhase(issueCategory) ?? undefined)
2194
+ : undefined;
2195
+ }
2196
+ catch {
2197
+ return undefined;
2198
+ }
2199
+ }
2200
+ /**
2201
+ * Resolve the deterministic runtime skeleton for the frontend plan patch from
2202
+ * the DAG spec. The plan task carries it via `structuredContractOutput.skeleton`
2203
+ * (the only schema-recognized persistent channel); absence is a fail-closed
2204
+ * generation/configuration defect, never a reason to fall back to text.
2205
+ */
2206
+ function resolveFrontendContractSkeleton(spec, planNodeId) {
2207
+ const planTask = spec.tasks.find((task) => task.id === planNodeId);
2208
+ const skeleton = planTask?.structuredContractOutput?.skeleton;
2209
+ if (!skeleton || Object.keys(skeleton).length === 0) {
2210
+ throw new Error(`frontend design policy blocked: missing runtime contract skeleton for ${planNodeId}`);
2211
+ }
2212
+ return skeleton;
2213
+ }
2214
+ async function collectFrontendSourceFreshness(input) {
2215
+ const entries = [];
2216
+ for (const source of input.sourceBinding?.sources ?? []) {
2217
+ entries.push({ path: source.path, sha256: source.sha256, fresh: true });
2218
+ }
2219
+ for (const evidencePath of input.contract.designEvidence.paths) {
2220
+ if (entries.some((entry) => entry.path === evidencePath))
2221
+ continue;
2222
+ try {
2223
+ const bytes = await readFile(path.join(input.workspaceRoot, evidencePath));
2224
+ entries.push({
2225
+ path: evidencePath,
2226
+ sha256: createHash("sha256").update(bytes).digest("hex"),
2227
+ fresh: true,
2228
+ });
2229
+ }
2230
+ catch {
2231
+ entries.push({
2232
+ path: evidencePath,
2233
+ sha256: "0".repeat(64),
2234
+ fresh: false,
2235
+ });
2236
+ }
2237
+ }
2238
+ return entries;
2239
+ }
2240
+ /** R3/R4 openspec invariants: frozen candidates must exist and content
2241
+ * snapshots must not have drifted. Citation is enforced upstream by the
2242
+ * prewrite gate read-event evidence and ledger specReference conformance. */
2243
+ async function checkFrontendOpenspecFacts(input) {
2244
+ const frozenCandidatePaths = input.config.openspecCandidatePaths ?? [];
2245
+ const candidateRoot = input.workspaceRoot;
2246
+ const missingCandidates = [];
2247
+ for (const candidate of frozenCandidatePaths) {
2248
+ try {
2249
+ await stat(path.join(candidateRoot, candidate));
2250
+ }
2251
+ catch {
2252
+ missingCandidates.push(candidate);
2253
+ }
2254
+ }
2255
+ if (missingCandidates.length > 0) {
2256
+ throw new Error(`frontend design policy blocked: openspec candidate paths frozen at generation time no longer exist on disk: ${missingCandidates.join(", ")}`);
2257
+ }
2258
+ const snapshots = input.config.openspecCandidateSnapshots;
2259
+ if (snapshots && snapshots.length > 0) {
2260
+ for (const snapshot of snapshots) {
2261
+ try {
2262
+ const bytes = await readFile(path.join(candidateRoot, snapshot.path));
2263
+ const actual = createHash("sha256").update(bytes).digest("hex");
2264
+ if (actual !== snapshot.sha256) {
2265
+ throw new Error(`frontend design policy blocked: openspec candidate content hash drifted: ${snapshot.path}`);
2266
+ }
2267
+ }
2268
+ catch (error) {
2269
+ if (error instanceof Error &&
2270
+ error.message.startsWith("frontend design policy blocked")) {
2271
+ throw error;
2272
+ }
2273
+ throw new Error(`frontend design policy blocked: openspec candidate unreadable: ${snapshot.path}`);
2274
+ }
2275
+ }
2276
+ }
2277
+ }
2278
+ async function executeFrontendDesignPolicy(input, meta) {
2279
+ const started = Date.now();
2280
+ const shell = input.task.shell;
2281
+ const config = shell.frontendDesignPolicy;
2282
+ try {
2283
+ // 1. Materialize the canonical contract from committed typed facts. After
2284
+ // M9a the plan node's assistantText is a pure Markdown narrative; the
2285
+ // compile authority is the committed plan ledger ⊕ runtime skeleton.
2286
+ // Fail closed on missing committed facts — never fall back to text.
2287
+ const { readCommittedOriginFacts, checkCommittedOriginFacts } = await import("../workflows/dag/frontend-shadow-dual-write.js");
2288
+ // A+B (AC-001/003): fail-closed committed contract/scout typed facts.
2289
+ // Missing/uncommitted facts block here; never fall back to text parsing.
2290
+ const committedContractFacts = await readCommittedOriginFacts(meta.runDir, "frontend-contract-pi", "contract-typed-facts.jsonl");
2291
+ const contractCheck = checkCommittedOriginFacts({
2292
+ records: committedContractFacts,
2293
+ origin: "contract",
2294
+ });
2295
+ if (!contractCheck.ok) {
2296
+ return {
2297
+ ok: false,
2298
+ stdout: "",
2299
+ stderr: `frontend design policy blocked: ${contractCheck.reason}`,
2300
+ failureCategory: "invalid-output",
2301
+ durationMs: Date.now() - started,
2302
+ };
2303
+ }
2304
+ const committedScoutFacts = await readCommittedOriginFacts(meta.runDir, "frontend-scout-pi", "scout-typed-facts.jsonl");
2305
+ const scoutCheck = checkCommittedOriginFacts({
2306
+ records: committedScoutFacts,
2307
+ origin: "scout",
2308
+ });
2309
+ if (!scoutCheck.ok) {
2310
+ return {
2311
+ ok: false,
2312
+ stdout: "",
2313
+ stderr: `frontend design policy blocked: ${scoutCheck.reason}`,
2314
+ failureCategory: "invalid-output",
2315
+ durationMs: Date.now() - started,
2316
+ };
2317
+ }
2318
+ const skeleton = resolveFrontendContractSkeleton(meta.spec, config.planFromNodeId);
2319
+ const assembled = await buildContractFromCommittedFacts({
2320
+ runDir: meta.runDir,
2321
+ planNodeId: config.planFromNodeId,
2322
+ contractNodeId: "frontend-contract-pi",
2323
+ sourceBinding: meta.spec.sourceBinding,
2324
+ skeleton,
2325
+ });
2326
+ if (!assembled.ok) {
2327
+ return {
2328
+ ok: false,
2329
+ stdout: "",
2330
+ stderr: `frontend design policy blocked: ${assembled.reason}`,
2331
+ failureCategory: "invalid-output",
2332
+ durationMs: Date.now() - started,
2333
+ };
2334
+ }
2335
+ const contract = assembled.canonical;
2336
+ const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(contract), "utf8");
2337
+ if (contractSizeBytes > FRONTEND_CONTRACT_MAX_BYTES) {
2338
+ return {
2339
+ ok: false,
2340
+ stdout: "",
2341
+ stderr: `frontend design policy blocked: canonical contract serialization is ${contractSizeBytes} bytes, exceeding the ${FRONTEND_CONTRACT_MAX_BYTES}-byte limit`,
2342
+ failureCategory: "invalid-output",
2343
+ durationMs: Date.now() - started,
2344
+ };
2345
+ }
2346
+ // 2. Requirement id retention (R11): search the canonical contract
2347
+ // serialization instead of the plan node narrative.
2348
+ const contractText = serializeDeterministicJson(contract);
2349
+ const missingIds = config.requiredRequirementIds.filter((id) => !contractText.includes(id));
2350
+ if (missingIds.length > 0) {
2351
+ return {
2352
+ ok: false,
2353
+ stdout: "",
2354
+ stderr: `frontend design policy blocked: missing requirement ids: ${missingIds.join(", ")}`,
2355
+ failureCategory: "invalid-output",
2356
+ durationMs: Date.now() - started,
2357
+ };
2358
+ }
2359
+ // 3. Mock strategy allowed + frozen command binding (R1).
2360
+ const mockStrategy = contract.mockApi.strategy;
2361
+ if (!config.allowedMockStrategies.includes(mockStrategy)) {
2362
+ return {
2363
+ ok: false,
2364
+ stdout: "",
2365
+ stderr: `frontend contract mock strategy "${mockStrategy}" is incompatible with the generated allowed strategies [${config.allowedMockStrategies.join(", ")}]`,
2366
+ failureCategory: "invalid-output",
2367
+ durationMs: Date.now() - started,
2368
+ };
2369
+ }
2370
+ if (mockStrategy !== "not-needed" && config.mockCommandLabels.length === 0) {
2371
+ return {
2372
+ ok: false,
2373
+ stdout: "",
2374
+ stderr: `frontend design policy blocked: contract selected Mock strategy ${mockStrategy} but the DAG has no authorized Mock verification commands`,
2375
+ failureCategory: "invalid-output",
2376
+ durationMs: Date.now() - started,
2377
+ };
2378
+ }
2379
+ // 4. writeSet containment.
2380
+ if (config.implementationWriteSet) {
2381
+ const writeSet = new Set(config.implementationWriteSet);
2382
+ const uncoveredTargets = contract.targets.files.filter((target) => ![...writeSet].some((pattern) => pathMatchesPattern(target, pattern) || target === pattern));
2383
+ if (uncoveredTargets.length > 0) {
2384
+ return {
2385
+ ok: false,
2386
+ stdout: "",
2387
+ stderr: `frontend design policy blocked: contract target is outside implementation writeSet: ${uncoveredTargets.join(", ")}`,
2388
+ failureCategory: "invalid-output",
2389
+ durationMs: Date.now() - started,
2390
+ };
2391
+ }
2392
+ const uncoveredVTs = contract.verificationTargets
2393
+ .filter((vt) => vt.type !== "static")
2394
+ .filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
2395
+ if (uncoveredVTs.length > 0) {
2396
+ return {
2397
+ ok: false,
2398
+ stdout: "",
2399
+ stderr: `frontend design policy blocked: verification target is outside implementation writeSet: ${uncoveredVTs.map((vt) => vt.file).join(", ")}`,
2400
+ failureCategory: "invalid-output",
2401
+ durationMs: Date.now() - started,
2402
+ };
2403
+ }
2404
+ }
2405
+ // 5. Source freshness (R4) + freshness entries.
2406
+ if (config.requireSourceFreshness) {
2407
+ if (!meta.spec.sourceBinding) {
2408
+ throw new Error("frontend design policy requires sourceBinding for freshness check");
2409
+ }
2410
+ await assertFrontendSourceBindingFresh({
2411
+ workspaceRoot: input.cwd,
2412
+ binding: meta.spec.sourceBinding,
2413
+ });
2414
+ }
2415
+ const sourceFreshness = await collectFrontendSourceFreshness({
2416
+ workspaceRoot: input.cwd,
2417
+ sourceBinding: meta.spec.sourceBinding,
2418
+ contract,
2419
+ });
2420
+ // 6. Openspec invariants (R3): frozen candidates exist and snapshots have
2421
+ // not drifted. Citation is superseded by prewrite-gate read evidence.
2422
+ await checkFrontendOpenspecFacts({
2423
+ workspaceRoot: input.cwd,
2424
+ config,
2425
+ });
2426
+ // 7. Deterministic design policy (M8 D2: designApproval is optional here;
2427
+ // the design verdict is enforced by the writer-admission shell).
2428
+ const policyResult = evaluateFrontendDesignPolicy({
2429
+ contract,
2430
+ allowedMockStrategies: config.allowedMockStrategies,
2431
+ sourceFreshness,
2432
+ componentSpecCandidatePaths: config.componentSpecCandidatePaths ?? [],
2433
+ allowedDependencies: config.allowedDependencies,
2434
+ writeSetPatterns: config.implementationWriteSet ?? [],
2435
+ });
2436
+ // 8. Write canonical contract + policy result.
2437
+ await writeFrontendImplementationContractArtifact({
2438
+ runDir: meta.runDir,
2439
+ outputDir: config.outputDir,
2440
+ artifactName: config.artifactName,
2441
+ canonical: contract,
2442
+ });
2443
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-design-policy-result.json", policyResult);
2444
+ if (policyResult.classification === "blocked") {
1899
2445
  return {
1900
2446
  ok: false,
1901
- stdout: summarizeCommandResults(results).stdout,
1902
- stderr: firstFailure?.stderr ||
1903
- lintSyntheticFailure?.stderr ||
1904
- traceError?.message ||
1905
- "frontend reverify failed",
1906
- failureCategory: firstFailure?.failureCategory ??
1907
- lintSyntheticFailure?.failureCategory ??
1908
- "invalid-output",
2447
+ stdout: "",
2448
+ stderr: `frontend design policy blocked: ${policyResult.findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ")}`,
2449
+ failureCategory: "invalid-output",
1909
2450
  durationMs: Date.now() - started,
1910
- ...{ commandResults },
1911
2451
  };
1912
2452
  }
1913
2453
  return {
1914
2454
  ok: true,
1915
- stdout: lintAssessment?.status === "baseline-debt"
1916
- ? "Frontend reverify bundle: pass with lint baseline-debt"
1917
- : "Frontend reverify bundle: pass",
2455
+ stdout: `Frontend design policy: approved findings=0\nContract: ${path.posix.join(config.outputDir, config.artifactName)}`,
1918
2456
  stderr: "",
1919
2457
  failureCategory: "success",
1920
2458
  durationMs: Date.now() - started,
1921
- ...{ commandResults },
1922
2459
  };
1923
2460
  }
1924
- const failureFacts = [];
1925
- if (firstFailure || lintSyntheticFailure) {
1926
- const failureStdout = firstFailure
1927
- ? [firstFailure.command, firstFailure.stdout].filter(Boolean).join("\n")
1928
- : "";
1929
- failureFacts.push({
1930
- nodeId: input.task.id,
1931
- record: {
1932
- status: "FINISHED",
1933
- failureCategory: firstFailure?.failureCategory ??
1934
- lintSyntheticFailure?.failureCategory,
1935
- // Keep classification scoped to the failed command. Aggregate
1936
- // successful output may contain unrelated writeSet-like JSON.
1937
- stdout: failureStdout,
1938
- stderr: firstFailure?.stderr ?? lintSyntheticFailure?.stderr,
1939
- commandResults,
1940
- },
1941
- });
2461
+ catch (error) {
2462
+ return {
2463
+ ok: false,
2464
+ stdout: "",
2465
+ stderr: error instanceof Error ? error.message : String(error),
2466
+ failureCategory: "invalid-output",
2467
+ durationMs: Date.now() - started,
2468
+ };
1942
2469
  }
1943
- if (traceError) {
1944
- failureFacts.push({
1945
- nodeId: "frontend-verification-trace-shell",
1946
- record: {
1947
- status: "FINISHED",
1948
- failureCategory: "invalid-output",
1949
- stderr: traceError.message,
1950
- },
2470
+ }
2471
+ async function executeFrontendWriterAdmission(input, meta) {
2472
+ const started = Date.now();
2473
+ const shell = input.task.shell;
2474
+ const config = shell.frontendWriterAdmission;
2475
+ try {
2476
+ // 1. Design verdict pass is a hard admission condition (M8 D2). In small
2477
+ // topology the design review node is absent and the check is vacuous.
2478
+ // The verdict is read from the committed typed design terminal fact, not
2479
+ // from the legacy first-line VERDICT text protocol (AC-003/AC-004).
2480
+ let verdictPass = true;
2481
+ let designRestartPhase;
2482
+ if (config.designReviewFromNodeId) {
2483
+ const designKinds = await collectDesignTerminalKinds(meta.runDir, config.designReviewFromNodeId);
2484
+ verdictPass =
2485
+ designKinds.length === 1 && designKinds[0] === "approve_design";
2486
+ // A+B (AC-009): request_design_changes issueCategory maps to the
2487
+ // deterministic §4.10 restart phase; unknown fails closed to undefined.
2488
+ designRestartPhase = verdictPass
2489
+ ? undefined
2490
+ : await collectDesignRestartPhase(meta.runDir, config.designReviewFromNodeId);
2491
+ }
2492
+ // 2. Read the canonical contract + design-policy result.
2493
+ const contractRaw = await readRunDirJson(meta.runDir, "contracts/frontend-implementation-contract.json");
2494
+ const contract = frontendImplementationContractSchema.parse(contractRaw);
2495
+ const policyRaw = (await readRunDirJson(meta.runDir, "contracts/frontend-design-policy-result.json"));
2496
+ if (policyRaw.schemaId !== "frontend-design-policy-v1") {
2497
+ throw new Error("frontend writer admission: design policy result artifact invalid");
2498
+ }
2499
+ const designPolicyResult = {
2500
+ schemaId: "frontend-design-policy-v1",
2501
+ classification: policyRaw.classification === "approved" ? "approved" : "blocked",
2502
+ findings: policyRaw.findings ?? [],
2503
+ digest: "",
2504
+ };
2505
+ // 3. Freeze the pre-writer worktree baseline (cumulative diff baseline).
2506
+ await captureFrontendWorktreeBaseline({
2507
+ runDir: meta.runDir,
2508
+ workspaceRoot: input.cwd,
1951
2509
  });
2510
+ const baselineRaw = (await readRunDirJson(meta.runDir, "contracts/frontend-worktree-baseline.json"));
2511
+ const baseline = (baselineRaw.files ?? [])
2512
+ .map((file) => ({
2513
+ path: file,
2514
+ sha256: baselineRaw.hashes?.[file] ?? null,
2515
+ }))
2516
+ .filter((entry) => entry.sha256 !== null);
2517
+ // 4. Writer-preceding lint baseline (R5) when lint is configured.
2518
+ let lintResults = [];
2519
+ if ((config.lintCommands?.length ?? 0) > 0) {
2520
+ for (const command of config.lintCommands) {
2521
+ const commandNumber = lintResults.length + 1;
2522
+ const result = await executeShellCommand({
2523
+ command,
2524
+ cwd: input.cwd,
2525
+ timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
2526
+ envAllowlist: shell.envAllowlist,
2527
+ dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2528
+ reportActivity: input.reportActivity,
2529
+ outputArtifacts: {
2530
+ stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
2531
+ stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
2532
+ },
2533
+ });
2534
+ lintResults.push(result);
2535
+ }
2536
+ await materializeFrontendLintBaseline({
2537
+ runDir: meta.runDir,
2538
+ workspaceRoot: input.cwd,
2539
+ commands: config.lintCommands,
2540
+ results: lintResults,
2541
+ worktreeChanged: false,
2542
+ });
2543
+ }
2544
+ // 5. Derive admission (writeSet + digests + findings). Glob entries in
2545
+ // the contract targets (e.g. `src/**` inherited from the skeleton) are
2546
+ // expanded against the concrete workspace files so the derived writeSet
2547
+ // contains only real paths the writer can touch.
2548
+ const workspaceFiles = await collectWorkspaceFiles(input.cwd);
2549
+ const admission = deriveFrontendWriterAdmissionShell({
2550
+ contract,
2551
+ designPolicyResult,
2552
+ baseline,
2553
+ frozenCommandLabels: config.frozenCommandLabels,
2554
+ allowedMockStrategies: config.allowedMockStrategies,
2555
+ workspaceFiles,
2556
+ });
2557
+ // 6. Design verdict enforcement: request-revision is a blocked admission.
2558
+ const failureSource = policyRaw.classification === "blocked"
2559
+ ? "frontend-design-policy-shell"
2560
+ : "frontend-writer-admission-shell";
2561
+ const findings = verdictPass
2562
+ ? admission.findings
2563
+ : [
2564
+ ...admission.findings,
2565
+ {
2566
+ code: "design-approval-request-revision",
2567
+ message: designRestartPhase !== undefined
2568
+ ? `design review typed terminal verdict is not approve_design (restartPhase=${designRestartPhase})`
2569
+ : "design review typed terminal verdict is not approve_design",
2570
+ },
2571
+ ];
2572
+ const classification = findings.length === 0 && admission.classification === "accepted"
2573
+ ? admission.classification
2574
+ : findings.length === 0 &&
2575
+ admission.classification === "requires-human-approval"
2576
+ ? "requires-human-approval"
2577
+ : "blocked";
2578
+ const result = frontendWriterAdmissionResultV1Schema.parse({
2579
+ schemaVersion: 1,
2580
+ schemaId: "frontend-writer-admission-shell-v1",
2581
+ classification,
2582
+ writeSet: admission.writeSet,
2583
+ writeSetDigest: admission.writeSetDigest,
2584
+ baselineDigest: admission.baselineDigest,
2585
+ admissionDigest: admission.admissionDigest,
2586
+ ...(admission.baselineDebtPolicy !== undefined
2587
+ ? { baselineDebtPolicy: admission.baselineDebtPolicy }
2588
+ : {}),
2589
+ findings,
2590
+ failureSource,
2591
+ });
2592
+ await writeDagRunJsonArtifact(meta.runDir, FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT, result);
2593
+ // A+B (AC-007): High topology materializes a `high-admission-receipt-v1`
2594
+ // catalog whose candidate digest must match the admission digest before
2595
+ // the gate can transition from `requires-human-approval` to `accepted`.
2596
+ // Any digest drift resolves to `stale` (never silently accepted).
2597
+ if (classification === "requires-human-approval") {
2598
+ const receipt = evaluateHighAdmissionReceipt({
2599
+ candidateDigest: result.admissionDigest,
2600
+ admissionDigest: result.admissionDigest,
2601
+ });
2602
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-high-admission-receipt.json", receipt);
2603
+ if (receipt.status !== "accepted") {
2604
+ return {
2605
+ ok: false,
2606
+ stdout: "",
2607
+ stderr: `frontend writer admission stale: high-admission receipt candidate digest drifted from admission digest`,
2608
+ failureCategory: "invalid-output",
2609
+ durationMs: Date.now() - started,
2610
+ };
2611
+ }
2612
+ }
2613
+ if (classification === "blocked") {
2614
+ return {
2615
+ ok: false,
2616
+ stdout: "",
2617
+ stderr: `frontend writer admission ${classification}: ${findings.map((finding) => `${finding.code}: ${finding.message}`).join("; ")}`,
2618
+ failureCategory: "invalid-output",
2619
+ durationMs: Date.now() - started,
2620
+ };
2621
+ }
2622
+ return {
2623
+ ok: true,
2624
+ stdout: `Frontend writer admission: ${classification} writeSet=${result.writeSet.length} writeSetDigest=${result.writeSetDigest}`,
2625
+ stderr: "",
2626
+ failureCategory: "success",
2627
+ durationMs: Date.now() - started,
2628
+ };
1952
2629
  }
2630
+ catch (error) {
2631
+ return {
2632
+ ok: false,
2633
+ stdout: "",
2634
+ stderr: error instanceof Error ? error.message : String(error),
2635
+ failureCategory: "invalid-output",
2636
+ durationMs: Date.now() - started,
2637
+ };
2638
+ }
2639
+ }
2640
+ async function executeFrontendContractImport(input, meta) {
2641
+ const started = Date.now();
2642
+ const shell = input.task.shell;
2643
+ const config = shell.frontendContractImport;
1953
2644
  try {
1954
- const assessment = await runFrontendFailureAssessGate({
1955
- runDir: meta.runDir,
1956
- failureFacts,
2645
+ const binding = meta.spec.sourceBinding;
2646
+ if (!binding) {
2647
+ throw new Error("frontend contract-import requires a DAG sourceBinding to re-validate freshness");
2648
+ }
2649
+ if (config.requireSourceFreshness) {
2650
+ await assertFrontendSourceBindingFresh({
2651
+ workspaceRoot: input.cwd,
2652
+ binding,
2653
+ });
2654
+ }
2655
+ const digest = createHash("sha256")
2656
+ .update(JSON.stringify({
2657
+ taskId: binding.taskId,
2658
+ sources: binding.sources,
2659
+ artifactName: config.artifactName,
2660
+ }))
2661
+ .digest("hex");
2662
+ const facts = {
2663
+ schemaVersion: 1,
2664
+ schemaId: "frontend-contract-import-v1",
2665
+ artifactName: config.artifactName,
2666
+ sourceBindingTaskId: binding.taskId,
2667
+ sourceCount: binding.sources.length,
2668
+ fresh: true,
2669
+ digest,
2670
+ };
2671
+ return {
2672
+ ok: true,
2673
+ stdout: JSON.stringify(facts, null, 2),
2674
+ stderr: "",
2675
+ failureCategory: "success",
2676
+ durationMs: Date.now() - started,
2677
+ };
2678
+ }
2679
+ catch (error) {
2680
+ return {
2681
+ ok: false,
2682
+ stdout: "",
2683
+ stderr: error instanceof Error ? error.message : String(error),
2684
+ failureCategory: "invalid-output",
2685
+ durationMs: Date.now() - started,
2686
+ };
2687
+ }
2688
+ }
2689
+ async function executeFrontendCloseout(input, meta) {
2690
+ const started = Date.now();
2691
+ const shell = input.task.shell;
2692
+ const config = shell.frontendCloseout;
2693
+ try {
2694
+ const context = (await readRunDirJson(meta.runDir, "contracts/frontend-review-context.json"));
2695
+ const contract = frontendImplementationContractSchema.parse(context.contract);
2696
+ const trace = context.verificationTrace;
2697
+ const verificationTargets = (trace?.targets ?? []).map((target) => ({
2698
+ status: target.status === "ok" ? "ok" : "failed",
2699
+ }));
2700
+ const mockEvidenceBound = contract.mockApi.strategy !== "not-needed" &&
2701
+ typeof trace?.mockCommandNodeId === "string" &&
2702
+ trace.mockCommandNodeId.length > 0 &&
2703
+ Array.isArray(trace?.mockCommandLabels) &&
2704
+ trace.mockCommandLabels.length > 0 &&
2705
+ Array.isArray(trace?.mockCommandTexts) &&
2706
+ trace.mockCommandTexts.length === trace.mockCommandLabels.length;
2707
+ const integrationFacts = deriveFrontendIntegrationFacts({
2708
+ mockStrategy: contract.mockApi.strategy,
2709
+ realApiEvidence: { present: false, passed: false },
2710
+ mockEvidenceBound,
2711
+ verificationTargets,
1957
2712
  });
1958
- await runFrontendRepairContractGate({ runDir: meta.runDir });
2713
+ // Typed review terminal (AC-003) from the committed typed event store.
2714
+ const typedKinds = await collectReviewTerminalKinds(meta.runDir, config.reviewFromNodeId);
2715
+ const typedVerdict = typedKinds.length === 1 &&
2716
+ (typedKinds[0] === "approve_review" || typedKinds[0] === "request_review_changes")
2717
+ ? typedKinds[0]
2718
+ : null;
2719
+ // A+B (AC-009): request_review_changes issueCategory maps to the
2720
+ // deterministic §4.10 restart phase. Recorded as a run-owned audit
2721
+ // artifact so the frozen closeout schema is never widened; `unknown`
2722
+ // fails closed to no restart phase.
2723
+ const reviewRestartPhase = typedVerdict === "request_review_changes"
2724
+ ? await collectReviewRestartPhase(meta.runDir, config.reviewFromNodeId)
2725
+ : undefined;
2726
+ if (reviewRestartPhase !== undefined) {
2727
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-review-restart-phase.json", {
2728
+ schemaVersion: 1,
2729
+ nodeId: config.reviewFromNodeId,
2730
+ restartPhase: reviewRestartPhase,
2731
+ });
2732
+ }
2733
+ const facts = frontendCloseoutFactsSchema.parse({
2734
+ schemaVersion: 1,
2735
+ schemaId: "frontend-closeout-facts-v1",
2736
+ coverageMatrix: verificationTargets.map((target, index) => ({
2737
+ verificationTargetId: trace?.targets?.[index]?.id ?? `vt-${index}`,
2738
+ status: target.status === "ok" ? "passed" : "failed",
2739
+ })),
2740
+ lintStatus: "unavailable",
2741
+ integrationFacts,
2742
+ reviewApproval: {
2743
+ typedVerdict,
2744
+ legacyVerdict: null,
2745
+ match: true,
2746
+ },
2747
+ cumulativeDiff: {
2748
+ cumulativeDigest: (typeof context.verificationTrace === "object" &&
2749
+ context.verificationTrace !== null &&
2750
+ "schemaId" in context.verificationTrace
2751
+ ? deterministicSha256({ verificationTrace: context.verificationTrace })
2752
+ : deterministicSha256({})),
2753
+ empty: false,
2754
+ },
2755
+ notRun: { browser: "not-run", visual: "not-run" },
2756
+ pendingRealIntegration: integrationFacts !== "real-api",
2757
+ });
2758
+ const rendered = renderFrontendCloseout(facts);
2759
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-closeout-facts.json", facts);
2760
+ // A+B (AC-010): materialize the deterministic frontend-test handoff.
2761
+ // browser/visual/runtime-a11y are always `not-run` and the handoff is
2762
+ // always `deferred-to-frontend-test`, so it never blocks implementation
2763
+ // and can never be rewritten into a passed result.
2764
+ const handoffTaskId = meta.spec.sourceBinding?.taskId ?? input.task.id;
2765
+ const sourceDigest = meta.spec.sourceBinding
2766
+ ? createHash("sha256")
2767
+ .update(JSON.stringify({
2768
+ taskId: meta.spec.sourceBinding.taskId,
2769
+ sources: meta.spec.sourceBinding.sources,
2770
+ }))
2771
+ .digest("hex")
2772
+ : deterministicSha256({});
2773
+ const handoff = buildFrontendTestHandoff({
2774
+ taskId: handoffTaskId,
2775
+ sourceDigest,
2776
+ candidateHash: facts.cumulativeDiff.cumulativeDigest,
2777
+ });
2778
+ await writeDagRunJsonArtifact(meta.runDir, "contracts/frontend-test-handoff.json", handoff);
2779
+ await writeDagNodeTextArtifact(meta.runDir, input.task.id, "frontend-closeout.md", rendered);
1959
2780
  return {
1960
2781
  ok: true,
1961
- stdout: formatFrontendFailureAssessStdout(assessment),
2782
+ stdout: `Frontend closeout rendered\n${rendered.split("\n").slice(0, 8).join("\n")}`,
1962
2783
  stderr: "",
1963
2784
  failureCategory: "success",
1964
2785
  durationMs: Date.now() - started,
1965
- ...{ commandResults },
1966
2786
  };
1967
2787
  }
1968
2788
  catch (error) {
@@ -1972,7 +2792,6 @@ async function executeFrontendVerificationBundle(input, meta) {
1972
2792
  stderr: error instanceof Error ? error.message : String(error),
1973
2793
  failureCategory: "invalid-output",
1974
2794
  durationMs: Date.now() - started,
1975
- ...{ commandResults },
1976
2795
  };
1977
2796
  }
1978
2797
  }
@@ -2061,6 +2880,52 @@ async function executeFrontendTestHtmlReport(input, meta) {
2061
2880
  };
2062
2881
  }
2063
2882
  }
2883
+ async function executeFrontendTestStandardScenarios(input) {
2884
+ const started = Date.now();
2885
+ try {
2886
+ const result = await copyFrontendTestStandardScenarios({
2887
+ workspaceRoot: input.cwd,
2888
+ });
2889
+ return {
2890
+ ok: true,
2891
+ stdout: JSON.stringify(result),
2892
+ stderr: "",
2893
+ failureCategory: "success",
2894
+ durationMs: Date.now() - started,
2895
+ };
2896
+ }
2897
+ catch (error) {
2898
+ return {
2899
+ ok: false,
2900
+ stdout: "",
2901
+ stderr: error instanceof Error ? error.message : String(error),
2902
+ failureCategory: "invalid-output",
2903
+ durationMs: Date.now() - started,
2904
+ };
2905
+ }
2906
+ }
2907
+ async function executeFrontendTestEnvironmentProbe(input) {
2908
+ const started = Date.now();
2909
+ const result = await probeFrontendTestEnvironment({
2910
+ workspaceRoot: input.cwd,
2911
+ });
2912
+ if (result.ok) {
2913
+ return {
2914
+ ok: true,
2915
+ stdout: result.stdout,
2916
+ stderr: "",
2917
+ failureCategory: "success",
2918
+ durationMs: Date.now() - started,
2919
+ };
2920
+ }
2921
+ return {
2922
+ ok: false,
2923
+ stdout: "",
2924
+ stderr: result.stderr,
2925
+ failureCategory: "nonzero-exit",
2926
+ durationMs: Date.now() - started,
2927
+ };
2928
+ }
2064
2929
  async function executeFrontendTestEvidenceValidation(input) {
2065
2930
  const started = Date.now();
2066
2931
  try {
@@ -2238,69 +3103,76 @@ async function executeFrontendTestReports(input, meta) {
2238
3103
  };
2239
3104
  }
2240
3105
  }
2241
- async function executeFrontendLintBaseline(input, meta) {
2242
- const started = Date.now();
2243
- const shell = input.task.shell;
2244
- const baseline = shell.frontendLintBaseline;
2245
- const cwd = resolveShellCwd(input.cwd, shell.cwd);
2246
- let beforeStatus;
2247
- try {
2248
- beforeStatus = await readGitStatusPorcelain(input.cwd);
2249
- }
2250
- catch {
2251
- beforeStatus = undefined;
3106
+ async function writeShellPresetProcessReceipt(input) {
3107
+ await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "process-receipt.json", {
3108
+ schemaVersion: 1,
3109
+ kind: "shell-preset",
3110
+ preset: input.preset,
3111
+ ok: input.ok,
3112
+ durationMs: input.durationMs,
3113
+ steps: input.steps,
3114
+ });
3115
+ }
3116
+ async function ensureShellPresetProcessReceipt(input) {
3117
+ const receiptPath = path.join(input.runDir, input.nodeId, "process-receipt.json");
3118
+ if (existsSync(receiptPath))
3119
+ return;
3120
+ if (existsSync(path.join(input.runDir, input.nodeId, "shell-command-receipt.json"))) {
3121
+ return;
2252
3122
  }
2253
- const results = [];
2254
- for (const command of baseline.lintCommands) {
2255
- const commandNumber = results.length + 1;
2256
- results.push(await executeShellCommand({
2257
- command,
2258
- cwd,
2259
- timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
2260
- envAllowlist: shell.envAllowlist,
2261
- dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2262
- outputArtifacts: {
2263
- stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
2264
- stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
3123
+ const detail = input.result.ok ? input.result.stdout : input.result.stderr;
3124
+ await writeShellPresetProcessReceipt({
3125
+ runDir: input.runDir,
3126
+ nodeId: input.nodeId,
3127
+ preset: input.preset,
3128
+ ok: input.result.ok,
3129
+ durationMs: input.result.durationMs,
3130
+ steps: [
3131
+ {
3132
+ id: "run",
3133
+ label: input.preset,
3134
+ ok: input.result.ok,
3135
+ detail: detail?.slice(0, 8 * 1024),
3136
+ durationMs: input.result.durationMs,
2265
3137
  },
2266
- }));
2267
- }
2268
- let afterStatus;
2269
- try {
2270
- afterStatus = await readGitStatusPorcelain(input.cwd);
2271
- }
2272
- catch {
2273
- afterStatus = undefined;
2274
- }
2275
- try {
2276
- const materialized = await materializeFrontendLintBaseline({
2277
- runDir: meta.runDir,
2278
- workspaceRoot: input.cwd,
2279
- commands: baseline.lintCommands,
2280
- results,
2281
- worktreeChanged: beforeStatus === undefined ||
2282
- afterStatus === undefined ||
2283
- beforeStatus !== afterStatus,
2284
- });
2285
- return {
2286
- ok: true,
2287
- stdout: `Frontend lint baseline: ${materialized.artifact.status}\nArtifact: ${materialized.ref.path}\nDiagnostics: ${materialized.artifact.diagnostics.length}`,
2288
- stderr: materialized.artifact.reason ?? "",
2289
- failureCategory: "success",
2290
- durationMs: Date.now() - started,
2291
- };
2292
- }
2293
- catch (error) {
2294
- return {
2295
- ok: false,
2296
- stdout: "",
2297
- stderr: error instanceof Error ? error.message : String(error),
2298
- failureCategory: "invalid-output",
2299
- durationMs: Date.now() - started,
2300
- };
2301
- }
3138
+ ],
3139
+ });
2302
3140
  }
2303
- export async function executeDagShellNode(input, meta) {
3141
+ function namedShellHandler(shell) {
3142
+ if (!shell)
3143
+ return null;
3144
+ const record = shell;
3145
+ for (const key of [
3146
+ "finalWriteSetApprovalGate",
3147
+ "frontendTestStandardScenarios",
3148
+ "frontendTestEnvironmentProbe",
3149
+ "frontendBrowserToolPreflight",
3150
+ "frontendPrewriteGate",
3151
+ "frontendLintBaseline",
3152
+ "frontendVerificationBundle",
3153
+ "frontendReviewContext",
3154
+ "frontendTestCaseChecklist",
3155
+ "frontendTestCaseManifest",
3156
+ "frontendTestEvidenceValidation",
3157
+ "frontendTestResultFinalize",
3158
+ "frontendTestL5Report",
3159
+ "frontendTestHtmlReport",
3160
+ "frontendTestReports",
3161
+ "backendTestPipeline",
3162
+ "jsonArtifactGate",
3163
+ "projectGovernanceGate",
3164
+ "requirementCoverageGate",
3165
+ "verdictGate",
3166
+ "verifyEvidence",
3167
+ "repairArtifactGate",
3168
+ "jacocoCoverage",
3169
+ ]) {
3170
+ if (record[key] != null)
3171
+ return key;
3172
+ }
3173
+ return null;
3174
+ }
3175
+ async function executeDagShellNodeInner(input, meta) {
2304
3176
  const shell = input.task.shell;
2305
3177
  if (shell?.finalWriteSetApprovalGate) {
2306
3178
  const started = Date.now();
@@ -2339,30 +3211,89 @@ export async function executeDagShellNode(input, meta) {
2339
3211
  };
2340
3212
  }
2341
3213
  }
3214
+ if (shell?.frontendTestStandardScenarios) {
3215
+ return executeFrontendTestStandardScenarios(input);
3216
+ }
3217
+ if (shell?.frontendTestEnvironmentProbe) {
3218
+ return executeFrontendTestEnvironmentProbe(input);
3219
+ }
2342
3220
  if (shell?.frontendBrowserToolPreflight) {
2343
3221
  const started = Date.now();
3222
+ const steps = [];
3223
+ const finish = async (result) => {
3224
+ await writeShellPresetProcessReceipt({
3225
+ runDir: meta.runDir,
3226
+ nodeId: input.task.id,
3227
+ preset: "frontendBrowserToolPreflight",
3228
+ ok: result.ok,
3229
+ durationMs: result.durationMs,
3230
+ steps: steps.length > 0
3231
+ ? steps
3232
+ : [
3233
+ {
3234
+ id: "run",
3235
+ label: "浏览器工具能力探测",
3236
+ ok: result.ok,
3237
+ detail: result.ok ? result.stdout : result.stderr,
3238
+ durationMs: result.durationMs,
3239
+ },
3240
+ ],
3241
+ });
3242
+ return result;
3243
+ };
2344
3244
  try {
2345
3245
  if (resolvePiBackend() === "cli-only") {
3246
+ steps.push({
3247
+ id: "pi-backend",
3248
+ label: "Pi SDK 后端可用",
3249
+ ok: false,
3250
+ detail: "CODE_AGENT_PI_BACKEND=cli-only",
3251
+ });
2346
3252
  throw new Error("browser-command-capability-unavailable: CODE_AGENT_PI_BACKEND=cli-only rejects SDK-only playwright_cli");
2347
3253
  }
3254
+ steps.push({
3255
+ id: "pi-backend",
3256
+ label: "Pi SDK 后端可用",
3257
+ ok: true,
3258
+ });
2348
3259
  const sdkCapability = await checkPiSdkCustomToolCapability(input.cwd);
2349
3260
  if (!sdkCapability.ok) {
3261
+ steps.push({
3262
+ id: "sdk-custom-tool",
3263
+ label: "Pi SDK custom-tool 能力",
3264
+ ok: false,
3265
+ detail: sdkCapability.detail,
3266
+ });
2350
3267
  throw new Error(`browser-command-capability-unavailable: ${sdkCapability.detail}`);
2351
3268
  }
3269
+ steps.push({
3270
+ id: "sdk-custom-tool",
3271
+ label: "Pi SDK custom-tool 能力",
3272
+ ok: true,
3273
+ });
2352
3274
  const toolProbe = await createPlaywrightCliTool({
2353
3275
  repoRoot: input.cwd,
2354
3276
  runDir: meta.runDir,
2355
3277
  nodeId: input.task.id,
2356
3278
  caseId: "frontend-browser-preflight",
2357
3279
  evidenceDir: "testcase/frontend/evidence/frontend-browser-preflight",
2358
- // Capability-only registration probe. The tested URL is selected later by
2359
- // retrieve-frontend-test-context-pi and validated by the environment shell.
2360
3280
  baseUrl: "http://localhost:5173/",
2361
3281
  inputRoot: "testcase/frontend/fixtures",
2362
3282
  });
2363
3283
  if (toolProbe.name !== "playwright_cli") {
3284
+ steps.push({
3285
+ id: "structured-tool",
3286
+ label: "注册 playwright_cli",
3287
+ ok: false,
3288
+ detail: toolProbe.name,
3289
+ });
2364
3290
  throw new Error("browser-command-capability-unavailable: structured playwright_cli tool registration failed");
2365
3291
  }
3292
+ steps.push({
3293
+ id: "structured-tool",
3294
+ label: "注册 playwright_cli",
3295
+ ok: true,
3296
+ });
2366
3297
  const launcher = resolvePlaywrightCliLauncher(input.cwd);
2367
3298
  const result = await defaultPlaywrightCliCommandRunner({
2368
3299
  executable: launcher.executable,
@@ -2375,8 +3306,19 @@ export async function executeDagShellNode(input, meta) {
2375
3306
  if (result.exitCode !== 0 ||
2376
3307
  result.timedOut ||
2377
3308
  !["open", "close", "find", "snapshot", "click"].every((command) => new RegExp(`\\b${command}\\b`, "i").test(help))) {
3309
+ steps.push({
3310
+ id: "playwright-cli-help",
3311
+ label: "playwright-cli --help 合同",
3312
+ ok: false,
3313
+ detail: help.slice(0, 8 * 1024),
3314
+ });
2378
3315
  throw new Error("playwright-cli-contract-incompatible");
2379
3316
  }
3317
+ steps.push({
3318
+ id: "playwright-cli-help",
3319
+ label: "playwright-cli --help 合同",
3320
+ ok: true,
3321
+ });
2380
3322
  const capability = {
2381
3323
  schemaVersion: 1,
2382
3324
  status: "ready",
@@ -2387,24 +3329,36 @@ export async function executeDagShellNode(input, meta) {
2387
3329
  targetUrlAuthority: "retrieve-context-and-environment-probe",
2388
3330
  };
2389
3331
  await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "frontend-browser-capability.json", capability);
2390
- return {
3332
+ steps.push({
3333
+ id: "capability-artifact",
3334
+ label: "写出 frontend-browser-capability.json",
3335
+ ok: true,
3336
+ detail: JSON.stringify(capability, null, 2),
3337
+ });
3338
+ return await finish({
2391
3339
  ok: true,
2392
3340
  stdout: JSON.stringify(capability),
2393
3341
  stderr: "",
2394
3342
  failureCategory: "success",
2395
3343
  durationMs: Date.now() - started,
2396
- };
3344
+ });
2397
3345
  }
2398
3346
  catch (error) {
2399
- return {
3347
+ return await finish({
2400
3348
  ok: false,
2401
3349
  stdout: "",
2402
3350
  stderr: `playwright-cli-unavailable: ${error instanceof Error ? error.message : String(error)}`,
2403
3351
  failureCategory: "tool-policy",
2404
3352
  durationMs: Date.now() - started,
2405
- };
3353
+ });
2406
3354
  }
2407
3355
  }
3356
+ if (shell?.frontendDesignPolicy) {
3357
+ return executeFrontendDesignPolicy(input, meta);
3358
+ }
3359
+ if (shell?.frontendWriterAdmission) {
3360
+ return executeFrontendWriterAdmission(input, meta);
3361
+ }
2408
3362
  if (shell?.frontendPrewriteGate) {
2409
3363
  const started = Date.now();
2410
3364
  try {
@@ -2433,9 +3387,6 @@ export async function executeDagShellNode(input, meta) {
2433
3387
  };
2434
3388
  }
2435
3389
  }
2436
- if (shell?.frontendLintBaseline) {
2437
- return executeFrontendLintBaseline(input, meta);
2438
- }
2439
3390
  if (shell?.frontendVerificationBundle) {
2440
3391
  return executeFrontendVerificationBundle(input, meta);
2441
3392
  }
@@ -2465,6 +3416,12 @@ export async function executeDagShellNode(input, meta) {
2465
3416
  };
2466
3417
  }
2467
3418
  }
3419
+ if (shell?.frontendContractImport) {
3420
+ return executeFrontendContractImport(input, meta);
3421
+ }
3422
+ if (shell?.frontendCloseout) {
3423
+ return executeFrontendCloseout(input, meta);
3424
+ }
2468
3425
  if (shell?.frontendTestCaseChecklist) {
2469
3426
  return executeFrontendTestCaseChecklist(input, meta);
2470
3427
  }
@@ -2686,53 +3643,23 @@ export async function executeDagShellNode(input, meta) {
2686
3643
  }
2687
3644
  if (shell?.commands?.length === 1 &&
2688
3645
  shell.commands[0] === "frontend-failure-assess-gate") {
2689
- const started = Date.now();
2690
- try {
2691
- const result = await runFrontendFailureAssessGate({
2692
- runDir: meta.runDir,
2693
- });
2694
- return {
2695
- ok: true,
2696
- stdout: formatFrontendFailureAssessStdout(result),
2697
- stderr: "",
2698
- failureCategory: "success",
2699
- durationMs: Date.now() - started,
2700
- };
2701
- }
2702
- catch (error) {
2703
- return {
2704
- ok: false,
2705
- stdout: "",
2706
- stderr: error instanceof Error ? error.message : String(error),
2707
- failureCategory: "invalid-output",
2708
- durationMs: Date.now() - started,
2709
- };
2710
- }
3646
+ return {
3647
+ ok: false,
3648
+ stdout: "",
3649
+ stderr: "frontend-failure-assess-gate legacy command removed in M8: verify failure is terminal and materializes failure ownership facts in the verification bundle itself",
3650
+ failureCategory: "invalid-output",
3651
+ durationMs: 0,
3652
+ };
2711
3653
  }
2712
3654
  if (shell?.commands?.length === 1 &&
2713
3655
  shell.commands[0] === "frontend-repair-contract-gate") {
2714
- const started = Date.now();
2715
- try {
2716
- const result = await runFrontendRepairContractGate({
2717
- runDir: meta.runDir,
2718
- });
2719
- return {
2720
- ok: true,
2721
- stdout: formatFrontendRepairContractStdout(result.assessment),
2722
- stderr: "",
2723
- failureCategory: "success",
2724
- durationMs: Date.now() - started,
2725
- };
2726
- }
2727
- catch (error) {
2728
- return {
2729
- ok: false,
2730
- stdout: "",
2731
- stderr: error instanceof Error ? error.message : String(error),
2732
- failureCategory: "invalid-output",
2733
- durationMs: Date.now() - started,
2734
- };
2735
- }
3656
+ return {
3657
+ ok: false,
3658
+ stdout: "",
3659
+ stderr: "frontend-repair-contract-gate legacy command removed in M8: no same-run repair branch exists in the new topology",
3660
+ failureCategory: "invalid-output",
3661
+ durationMs: 0,
3662
+ };
2736
3663
  }
2737
3664
  if (shell?.projectGovernanceGate) {
2738
3665
  const started = Date.now();
@@ -2824,6 +3751,7 @@ export async function executeDagShellNode(input, meta) {
2824
3751
  const cwd = resolveShellCwd(input.cwd, shell.cwd);
2825
3752
  const timeoutMs = shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
2826
3753
  const failFast = shell.failFast !== false;
3754
+ const attemptNumber = input.attempt ?? 1;
2827
3755
  const results = [];
2828
3756
  const executedCommandKeys = new Set();
2829
3757
  let beforeStatus;
@@ -2919,8 +3847,8 @@ export async function executeDagShellNode(input, meta) {
2919
3847
  dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
2920
3848
  reportActivity: input.reportActivity,
2921
3849
  outputArtifacts: {
2922
- stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
2923
- stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
3850
+ stdoutPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stdout.txt`),
3851
+ stderrPath: path.join(meta.runDir, input.task.id, `attempt-${attemptNumber}`, "commands", `${commandNumber}.stderr.txt`),
2924
3852
  },
2925
3853
  });
2926
3854
  results.push(result);
@@ -2979,6 +3907,7 @@ export async function executeDagShellNode(input, meta) {
2979
3907
  schemaVersion: 1,
2980
3908
  runId: meta.runId,
2981
3909
  nodeId: input.task.id,
3910
+ attempt: attemptNumber,
2982
3911
  workspaceFingerprint: workspaceFingerprint ?? null,
2983
3912
  failFast,
2984
3913
  commands: results.map((result) => ({
@@ -2997,6 +3926,7 @@ export async function executeDagShellNode(input, meta) {
2997
3926
  subchecks: parseCheckRepoSubcheckDurations(result),
2998
3927
  })),
2999
3928
  };
3929
+ await writeDagNodeJsonArtifact(meta.runDir, input.task.id, path.posix.join(`attempt-${attemptNumber}`, "shell-command-receipt.json"), receipt);
3000
3930
  await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "shell-command-receipt.json", receipt);
3001
3931
  const summaryMarkdown = buildShellResultSummaryMarkdown({
3002
3932
  nodeId: input.task.id,
@@ -3048,8 +3978,57 @@ export async function executeDagShellNode(input, meta) {
3048
3978
  stdout: `${recordPrefix}${reuseNote}${aggregate.stdout}`,
3049
3979
  stderr: stderrParts.filter(Boolean).join("\n\n"),
3050
3980
  failureCategory,
3981
+ ...(firstFailure?.failureCategory
3982
+ ? { rawFailureCategory: firstFailure.failureCategory }
3983
+ : {}),
3051
3984
  durationMs: Date.now() - started,
3052
3985
  // extra field consumed by node-execution if supported; also embedded in stdout marker
3053
3986
  ...{ commandResults },
3054
3987
  };
3055
3988
  }
3989
+ /**
3990
+ * Enumerate the workspace's concrete file paths for glob writeSet expansion.
3991
+ *
3992
+ * Uses `git ls-files -co --exclude-standard` (tracked + untracked, excluding
3993
+ * gitignored) so the expanded writeSet reflects real files the writer can
3994
+ * touch. Falls back to [] when the workspace is not a git repository or git
3995
+ * fails — in that case glob entries fail closed as non-concrete paths.
3996
+ */
3997
+ async function collectWorkspaceFiles(workspaceRoot) {
3998
+ try {
3999
+ const files = await new Promise((resolve, reject) => {
4000
+ const child = spawn("git", ["ls-files", "-co", "--exclude-standard", "-z"], { cwd: workspaceRoot, stdio: ["ignore", "pipe", "ignore"] });
4001
+ const chunks = [];
4002
+ child.stdout.on("data", (chunk) => chunks.push(chunk));
4003
+ child.on("error", reject);
4004
+ child.on("close", (code) => {
4005
+ if (code === 0) {
4006
+ resolve(Buffer.concat(chunks)
4007
+ .toString("utf8")
4008
+ .split("\0")
4009
+ .filter(Boolean));
4010
+ }
4011
+ else {
4012
+ reject(new Error(`git ls-files exited ${String(code)}`));
4013
+ }
4014
+ });
4015
+ });
4016
+ return files.map((file) => file.replace(/\\/g, "/")).sort();
4017
+ }
4018
+ catch {
4019
+ return [];
4020
+ }
4021
+ }
4022
+ export async function executeDagShellNode(input, meta) {
4023
+ const handler = namedShellHandler(input.task.shell);
4024
+ const result = await executeDagShellNodeInner(input, meta);
4025
+ if (handler) {
4026
+ await ensureShellPresetProcessReceipt({
4027
+ runDir: meta.runDir,
4028
+ nodeId: input.task.id,
4029
+ preset: handler,
4030
+ result,
4031
+ });
4032
+ }
4033
+ return result;
4034
+ }