@tea-agent/loop-agent 0.38.2 → 0.38.4

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 (192) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +171 -96
  3. package/README.md +11 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +4 -0
  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 +428 -27
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/commands/task-advance.js +31 -0
  17. package/dist/executors/dag-pi-executor.js +76 -10
  18. package/dist/executors/pi-event-serializer.js +42 -1
  19. package/dist/executors/pi-executor.js +51 -0
  20. package/dist/executors/pi-extension-resolver.js +233 -0
  21. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  22. package/dist/executors/pi-sdk-executor.js +187 -59
  23. package/dist/executors/shell-executor.js +265 -15
  24. package/dist/executors/shell-write-guard.js +7 -0
  25. package/dist/shared/dag-prompt-override.js +28 -0
  26. package/dist/shared/operator/capabilities.js +32 -0
  27. package/dist/shared/playwright-cli-command-policy.js +15 -0
  28. package/dist/shared/update/console-notifier.js +100 -0
  29. package/dist/{cli → shared}/update/npm-client.js +41 -8
  30. package/dist/{cli → shared}/update/state.js +41 -13
  31. package/dist/task/contract/constants.js +1 -0
  32. package/dist/task/contract/diff.js +26 -0
  33. package/dist/task/contract/project.js +5 -0
  34. package/dist/task/contract/schema.js +6 -1
  35. package/dist/task/source-prepare/build-draft.js +15 -0
  36. package/dist/task/source-prepare/parse-intent.js +24 -4
  37. package/dist/worker/cli.js +31 -1
  38. package/dist/worker/console/chat/routes.js +1 -0
  39. package/dist/worker/console/dag-execution-receipt.js +33 -0
  40. package/dist/worker/console/operator-actions.js +116 -0
  41. package/dist/worker/console/operator-user-error.js +2 -2
  42. package/dist/worker/console/pi-readiness.js +4 -4
  43. package/dist/worker/console/routes.js +43 -2
  44. package/dist/worker/console/security.js +63 -9
  45. package/dist/worker/console/server.js +36 -5
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4bmo3iY.js → abnfDiagram-N423BO3Z-gauhD9kD.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-SAD7McOS.js → arc-pm3c2Rsl.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DAVLUdK1.js → architectureDiagram-T3A2C74G-D_JAg3wH.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CPPdv4vj.js → blockDiagram-VBNYF7ZC-Dk7-ex52.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D1GRGZax.js → c4Diagram-5PPSVZJV-Uval9oRI.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-CVrnUdZl.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D_Bq2ZqK.js → chunk-2GRJ4B5K-BSYjHs-v.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CPLVtamK.js → chunk-2Q5K7J3B-KGUuBXI8.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DnaZWcL3.js → chunk-5RXB4S5H-JqAtxD9N.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BRTmFgnq.js → chunk-5VM5RSS4-D6EJOVQK.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BRlfYZoH.js → chunk-6Q2QTUOP-B1JriSjm.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjNybD4u.js → chunk-GF5L2VYU-D62b96Jd.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3e-74fG.js → chunk-JWPE2WC7-BsNKEC_r.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Br3ir6f2.js → chunk-KBJHAD2P-B56v6-ZP.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-1NgVuRbh.js → chunk-RYQCIY6F-D2KPnd8K.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BTYuvuWN.js → chunk-XXDRQBXY-kzKSCaaL.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BC6Bgosf.js → cose-bilkent-JH36ORCC-C6N6KNJd.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DV-HypfL.js → cynefin-VYW2F7L2-DiyymT2D.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DhTaddO-.js → cynefinDiagram-MW4NZA55-BeKAXvf3.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BnEcOmiI.js → dagre-VZM6K2ZE-TkIdXfPF.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-xphGRVKp.js → diagram-7IWD3JNH-BFmSG3ks.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CrdD1mRG.js → diagram-B4RE2ZJO-DqpJT2Fs.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D4YdLsaF.js → diagram-LBJQPF4R-r5ATN43t.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LCKyGZ5U.js → diagram-Q27KOJAE-DEcSCMuR.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BeiTe7t0.js → diagram-UB23O5K3-VOvYRLVc.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BO2cq8GS.js → ebnfDiagram-BXEA7PRR-D7f4Cb1u.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmWww3Nt.js → erDiagram-JOGREHBK-ziUf3U0G.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C2kkUYg5.js → flowDiagram-UKHOOZJN-BoRdQ-1y.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DY_JZ2iQ.js → ganttDiagram-PKOTCBZU-B1-GncNm.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BizhCSw9.js → gitGraphDiagram-DS77QQ5N-CTW-ph8W.js} +1 -1
  78. package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
  79. package/dist/worker/console/static/assets/{index-BXN89VPX.js → index-DEbyrEfW.js} +147 -68
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Dlo2eK_d.js → infoDiagram-6WML65LV-BFA337tZ.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js → ishikawaDiagram-WSZJBQD7-BtEujgRR.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DiMDe5Yh.js → journeyDiagram-NVQOT4AX-C8ZwD2Jp.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-sN553oIN.js → kanban-definition-27J2QSJJ-Ck-z8B11.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-D9Py31Ld.js → linear-CAyXjQjN.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-D7Occzk_.js → mermaid.core-CcWKn6tS.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DjDRboiR.js → mindmap-definition-FAOFIHXS-C2NuDO7E.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Cgc4qlzp.js → pegDiagram-VL7TDLO6-BFub3PDz.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DxZj8pKd.js → pieDiagram-7S7Q4E2Y-BJx5THZ5.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CJ50MngR.js → quadrantDiagram-CIZ2JOQS-DhalkDWV.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BY-OD8Tr.js → railroadDiagram-AXF67PYL-CVcBM36o.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQFButfK.js → requirementDiagram-LRYGKXZP-D_vy4n0w.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BB_QUK_b.js → sankeyDiagram-W5VNT64P-DGYdJL1D.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CqhXZjyx.js → sequenceDiagram-SI44F4Z6-C1xJFg3k.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-2l1U4rgJ.js → sizeCapture-X5ZJPWSS-D_Fc43df.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BX1FbrhJ.js → stateDiagram-OKZ733FA-BhGbIjbu.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DOSnT7hb.js → swimlanes-SLNWSIFB-DVkNcO-V.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DVJQA7AN.js → timeline-definition-Z64GVDOM-BQU9ufV0.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-74uxl9gx.js → vennDiagram-T6HMQDX7-BCQhRmYB.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsBxlcg_.js → wardleyDiagram-T6FBY63Y-DWerOyHO.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-snuW9Mjw.js → xychartDiagram-ELKLHX3M-DxLeebQU.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  105. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  106. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  107. package/dist/worker/observe/node-input.js +74 -3
  108. package/dist/worker/observe/node-process.js +377 -0
  109. package/dist/worker/observe/routes.js +36 -3
  110. package/dist/worker/observe/shell-handler-keys.js +34 -0
  111. package/dist/worker/observe/static/api.js +66 -0
  112. package/dist/worker/observe/static/constants.js +8 -1
  113. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  114. package/dist/worker/observe/static/format.js +23 -0
  115. package/dist/worker/observe/static/markdown-render.js +20 -1
  116. package/dist/worker/observe/static/operator-chrome.js +4 -0
  117. package/dist/worker/observe/static/state.js +13 -2
  118. package/dist/worker/observe/static/styles.css +73 -0
  119. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  120. package/dist/worker/observe/static/views/dag.js +8 -1
  121. package/dist/worker/observe/static/views/failures.js +5 -2
  122. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  123. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +76 -6
  124. package/dist/workflows/dag/backend-test-scenario-partitions.js +73 -1
  125. package/dist/workflows/dag/contract-output-registry.js +15 -0
  126. package/dist/workflows/dag/failure-category.js +12 -0
  127. package/dist/workflows/dag/frontend-implementation-contract.js +145 -40
  128. package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
  129. package/dist/workflows/dag/frontend-repair.js +14 -0
  130. package/dist/workflows/dag/frontend-risk.js +15 -2
  131. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  132. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  133. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  134. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  135. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  136. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  137. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  138. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  139. package/dist/workflows/dag/init-hybrid.js +196 -117
  140. package/dist/workflows/dag/node-execution.js +139 -10
  141. package/dist/workflows/dag/prompt.js +15 -1
  142. package/dist/workflows/dag/rerun-plan.js +134 -3
  143. package/dist/workflows/dag/rerun-run.js +58 -6
  144. package/dist/workflows/dag/retry-policy.js +91 -0
  145. package/dist/workflows/dag/structured-output-repair.js +712 -0
  146. package/dist/workflows/dag/types.js +32 -1
  147. package/dist/workflows/dag/validate.js +41 -1
  148. package/docs/init-surface.manifest.json +5 -0
  149. package/docs/operations/README.md +1 -1
  150. package/docs/operations/local-development-environment.md +1 -5
  151. package/docs/skills/vetted-skill-registry.md +14 -0
  152. package/docs/templates/README.md +1 -1
  153. package/docs/templates/agent-dag.schema.json +44 -0
  154. package/docs/templates/backend-test-dag.json +2 -2
  155. package/docs/templates/frontend-test-dag.json +8 -10
  156. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  157. package/docs/templates/init-managed-agents.md +13 -3
  158. package/harness.json +2 -2
  159. package/package.json +12 -4
  160. package/scripts/next-info.mjs +356 -0
  161. package/scripts/next-publish-gate.mjs +238 -0
  162. package/scripts/release-source-binding.mjs +251 -0
  163. package/skills/analyze-product-requirements/SKILL.md +8 -5
  164. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  165. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  166. package/skills/analyze-product-requirements/references/example.md +50 -0
  167. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  168. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  169. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  170. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  171. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  172. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  173. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  174. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  175. package/skills/codebase-scout/SKILL.md +1 -1
  176. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  177. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  178. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  179. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  180. package/skills/improve-codebase-architecture/deepening.md +37 -0
  181. package/skills/improve-codebase-architecture/html-report.md +123 -0
  182. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  183. package/skills/improve-codebase-architecture/language.md +53 -0
  184. package/skills/loop-agent/references/command-reference.md +11 -4
  185. package/skills/playwright-cli/SKILL.md +22 -0
  186. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  187. package/dist/worker/console/static/assets/channel-R6oQIDkw.js +0 -1
  188. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +0 -1
  189. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +0 -1
  190. package/dist/worker/console/static/assets/index-CuH5CNFu.css +0 -1
  191. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +0 -1
  192. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +0 -8
@@ -8,6 +8,7 @@ import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-sto
8
8
  import { findPackageRoot } from "../../shared/package-metadata.js";
9
9
  import { pathMatchesPattern } from "../../shared/git-progress.js";
10
10
  import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
11
+ import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, openspecCitationsRel, parseOpenspecCitationsBlock, persistRuntimeSkeleton, } from "./structured-output-repair.js";
11
12
  export const frontendNormalizationActionSchema = z.enum([
12
13
  "remove-trailing-commas",
13
14
  "strip-comments",
@@ -1485,7 +1486,13 @@ export async function analyzeFrontendImplementationContract(input) {
1485
1486
  rawOutput = input.rawContractText;
1486
1487
  }
1487
1488
  else if (input.fromNodeId !== undefined) {
1488
- const record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
1489
+ let record;
1490
+ try {
1491
+ record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
1492
+ }
1493
+ catch (error) {
1494
+ throw new Error(`cannot parse node record ${input.fromNodeId}: ${error instanceof Error ? error.message : String(error)}`);
1495
+ }
1489
1496
  rawOutput = record.assistantText ?? record.stdout ?? "";
1490
1497
  }
1491
1498
  else {
@@ -1716,7 +1723,11 @@ async function dropUnresolvedVerificationSymbols(input) {
1716
1723
  return changed;
1717
1724
  }
1718
1725
  function extractSingleOpenspecCitationsBlock(text) {
1719
- const blocks = text.match(/```openspec-citations[ \t]*\r?\n[\s\S]*?\r?\n```/g) ?? [];
1726
+ // Line-anchored so a directly closed empty fence (no candidate rows) still
1727
+ // counts as the one required block instead of "found 0" retry loops.
1728
+ const blocks = [
1729
+ ...text.matchAll(/^```openspec-citations[ \t]*\r?\n([\s\S]*?)^```[ \t]*$/gim),
1730
+ ].map((match) => match[0]);
1720
1731
  if (blocks.length !== 1) {
1721
1732
  throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
1722
1733
  }
@@ -1731,7 +1742,7 @@ async function writeFrontendPlanCandidateRaw(input) {
1731
1742
  * Compile the planner's editable RFC 7386 patch against the deterministic
1732
1743
  * runtime skeleton. Every attempt is preserved under contracts/candidates so
1733
1744
  * format/schema failures can be inspected and replayed without trusting model
1734
- * prose. A successful result returns canonical full-contract text for all
1745
+ * prose. A successful result returns a path-only pointer plus the hash-bound canonical artifact for
1735
1746
  * downstream review and prewrite nodes.
1736
1747
  */
1737
1748
  export async function validateFrontendPlanPatchNodeOutput(input) {
@@ -1804,27 +1815,60 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
1804
1815
  ],
1805
1816
  errors: [],
1806
1817
  });
1818
+ const canonicalRel = canonicalContractRel(nodeId);
1819
+ const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalRel, analysis.canonical);
1820
+ const parsedCitations = parseOpenspecCitationsBlock(citationsBlock);
1821
+ await writeDeterministicJsonArtifact(input.runDir, openspecCitationsRel(nodeId), {
1822
+ citations: parsedCitations.citations,
1823
+ block: citationsBlock,
1824
+ });
1825
+ await persistRuntimeSkeleton({
1826
+ runDir: input.runDir,
1827
+ nodeId,
1828
+ skeleton,
1829
+ });
1830
+ const artifact = {
1831
+ path: canonicalArtifact.path,
1832
+ sha256: canonicalArtifact.sha256,
1833
+ schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
1834
+ };
1807
1835
  return {
1808
1836
  ok: true,
1809
1837
  contract: analysis.canonical,
1810
- normalizedText: [
1811
- "Frontend implementation contract compiled from the runtime skeleton.",
1812
- "",
1813
- "```json",
1814
- serializeDeterministicJson(analysis.canonical),
1815
- "```",
1816
- citationsBlock,
1817
- ].join("\n"),
1838
+ artifact,
1839
+ normalizedText: formatStructuredArtifactPointer(artifact),
1818
1840
  };
1819
1841
  }
1820
1842
  catch (error) {
1821
1843
  const reason = error instanceof Error ? error.message : String(error);
1844
+ const classified = classifyStructuredContractFailure({
1845
+ reason,
1846
+ kind: error instanceof FrontendContractFailure ? error.kind : undefined,
1847
+ });
1822
1848
  await writeReport({
1823
- classification: "retryable-invalid",
1849
+ classification: classified.classification,
1824
1850
  normalizationActions: [],
1825
- errors: [{ path: "$", code: "invalid-output", message: reason }],
1851
+ errors: classified.errors,
1826
1852
  });
1827
- return { ok: false, reason };
1853
+ return {
1854
+ ok: false,
1855
+ reason,
1856
+ classification: classified.classification,
1857
+ errors: classified.errors,
1858
+ };
1859
+ }
1860
+ }
1861
+ export async function readFrontendCanonicalCandidate(runDir, nodeId) {
1862
+ const candidatePath = path.join(runDir, canonicalContractRel(nodeId));
1863
+ try {
1864
+ const raw = await readFile(candidatePath, "utf8");
1865
+ const parsed = frontendImplementationContractSchema.safeParse(JSON.parse(raw));
1866
+ return parsed.success ? parsed.data : undefined;
1867
+ }
1868
+ catch (error) {
1869
+ if (error.code === "ENOENT")
1870
+ return undefined;
1871
+ return undefined;
1828
1872
  }
1829
1873
  }
1830
1874
  /**
@@ -1845,60 +1889,121 @@ export async function validateFrontendContractNodeOutput(input) {
1845
1889
  return { ok: true, contract: analysis.canonical };
1846
1890
  }
1847
1891
  catch (error) {
1892
+ const reason = error instanceof Error ? error.message : String(error);
1893
+ const classified = classifyStructuredContractFailure({
1894
+ reason,
1895
+ kind: error instanceof FrontendContractFailure ? error.kind : undefined,
1896
+ });
1848
1897
  return {
1849
1898
  ok: false,
1850
- reason: error instanceof Error ? error.message : String(error),
1899
+ reason,
1900
+ classification: classified.classification,
1901
+ errors: classified.errors,
1851
1902
  };
1852
1903
  }
1853
1904
  }
1854
1905
  /**
1855
- * Node-output self-check for the plan revision node, which emits an RFC 7386
1856
- * merge-patch delta against the original contract instead of a full contract.
1857
- * Deliberately loose and format-level: the output must contain exactly one
1858
- * ```openspec-citations fenced block and must extract (after the citations
1859
- * blocks are stripped) to exactly one JSON object. The delta carries no
1860
- * schemaVersion/targets, so no full-contract schema/semantic checks run here;
1861
- * content parsing, patch application, and merged-contract validation stay with
1862
- * the prewrite gate, which remains the only authority.
1906
+ * Node-output self-check for the plan revision node. Format-level checks still
1907
+ * require one openspec-citations fence and one JSON object. When the original
1908
+ * canonical contract and sourceBinding are present, this node is the merge
1909
+ * authority: it applies the RFC 7386 patch, validates the full contract, and
1910
+ * writes the revision node's own hash-bound canonical artifact.
1863
1911
  */
1864
1912
  export async function validateFrontendRevisionPatchNodeOutput(input) {
1913
+ const fail = (reason) => {
1914
+ const classified = classifyStructuredContractFailure({ reason });
1915
+ return {
1916
+ ok: false,
1917
+ reason,
1918
+ classification: classified.classification,
1919
+ errors: classified.errors,
1920
+ };
1921
+ };
1922
+ const nodeId = input.nodeId ?? "frontend-plan-revision-pi";
1923
+ const attempt = input.attempt ?? 1;
1924
+ await writeFrontendPlanCandidateRaw({
1925
+ runDir: input.runDir,
1926
+ nodeId,
1927
+ attempt,
1928
+ text: input.text,
1929
+ });
1865
1930
  let citationsBlock;
1866
1931
  try {
1867
1932
  citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
1868
1933
  }
1869
1934
  catch (error) {
1870
- return {
1871
- ok: false,
1872
- reason: error instanceof Error ? error.message : String(error),
1873
- };
1935
+ return fail(error instanceof Error ? error.message : String(error));
1874
1936
  }
1875
- // Strip the citation blocks before extraction: their per-row JSON objects
1876
- // would otherwise count as competing balanced objects in the extractor.
1877
1937
  const textWithoutCitations = input.text.replace(citationsBlock, "");
1878
1938
  let patch;
1879
1939
  try {
1880
1940
  patch = extractFrontendImplementationJson(textWithoutCitations);
1881
1941
  }
1882
1942
  catch (error) {
1883
- return {
1884
- ok: false,
1885
- reason: `revision patch output must contain exactly one valid JSON object: ${error instanceof Error ? error.message : String(error)}`,
1886
- };
1943
+ return fail(`revision patch output must contain exactly one valid JSON object: ${error instanceof Error ? error.message : String(error)}`);
1887
1944
  }
1888
1945
  if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
1889
- return {
1890
- ok: false,
1891
- reason: "revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)",
1892
- };
1946
+ return fail("revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)");
1893
1947
  }
1894
1948
  const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
1895
1949
  if (protectedViolations.length > 0) {
1950
+ return fail(`frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
1951
+ }
1952
+ const original = (await readFrontendCanonicalCandidate(input.runDir, "frontend-plan-pi")) ??
1953
+ (await readFrontendCanonicalCandidate(input.runDir, nodeId));
1954
+ if (!input.sourceBinding) {
1955
+ return { ok: true, contract: patch };
1956
+ }
1957
+ if (!original) {
1958
+ return fail("frontend revision patch requires the original frontend-plan-pi canonical contract");
1959
+ }
1960
+ try {
1961
+ const merged = applyFrontendContractMergePatch(original, patch);
1962
+ const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
1963
+ runDir: input.runDir,
1964
+ contract: merged,
1965
+ });
1966
+ const analysis = await analyzeFrontendImplementationContract({
1967
+ runDir: input.runDir,
1968
+ rawContractText: serializeDeterministicJson(merged),
1969
+ sourceBinding: input.sourceBinding,
1970
+ });
1971
+ const candidateDir = path.posix.join("contracts", "candidates", nodeId);
1972
+ await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`), patch);
1973
+ await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
1974
+ await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.validation.json`), {
1975
+ classification: "accepted-normalized",
1976
+ normalizationActions: [
1977
+ "apply-original-canonical",
1978
+ ...(droppedUnresolvedSymbols
1979
+ ? ["drop-unresolved-verification-symbols"]
1980
+ : []),
1981
+ ...analysis.normalizationActions,
1982
+ ],
1983
+ errors: [],
1984
+ });
1985
+ const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalContractRel(nodeId), analysis.canonical);
1986
+ const parsedCitations = parseOpenspecCitationsBlock(citationsBlock);
1987
+ await writeDeterministicJsonArtifact(input.runDir, openspecCitationsRel(nodeId), {
1988
+ citations: parsedCitations.citations,
1989
+ block: citationsBlock,
1990
+ });
1991
+ const artifact = {
1992
+ path: canonicalArtifact.path,
1993
+ sha256: canonicalArtifact.sha256,
1994
+ schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
1995
+ };
1896
1996
  return {
1897
- ok: false,
1898
- reason: `frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`,
1997
+ ok: true,
1998
+ contract: analysis.canonical,
1999
+ artifact,
2000
+ normalizedText: formatStructuredArtifactPointer(artifact),
1899
2001
  };
1900
2002
  }
1901
- return { ok: true, contract: patch };
2003
+ catch (error) {
2004
+ const reason = error instanceof Error ? error.message : String(error);
2005
+ return fail(reason);
2006
+ }
1902
2007
  }
1903
2008
  export async function materializeFrontendImplementationContract(input) {
1904
2009
  const analysis = await analyzeFrontendImplementationContract(input);
@@ -1,10 +1,11 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { readFile, stat } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { z } from "zod";
4
5
  import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
5
6
  import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
6
7
  import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
7
- import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, applyFrontendImplementationContractPatch, extractFrontendImplementationJson, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
8
+ import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
8
9
  import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
9
10
  import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
10
11
  import { pathMatchesPattern } from "../../shared/git-progress.js";
@@ -157,60 +158,68 @@ async function selectNode(runDir, primary, fallbacks) {
157
158
  }
158
159
  throw new Error(`frontend prewrite gate no FINISHED candidate: ${rejections.join("; ")}`);
159
160
  }
160
- async function readNodeText(runDir, nodeId) {
161
- const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
161
+ async function readFinishedNodeRecord(runDir, nodeId) {
162
+ let record;
163
+ try {
164
+ record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
165
+ }
166
+ catch (error) {
167
+ throw new Error(`frontend prewrite gate cannot parse node record ${nodeId}: ${error instanceof Error ? error.message : String(error)}`);
168
+ }
162
169
  if (record.status !== "FINISHED") {
163
170
  throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
164
171
  }
165
- // Pi execution mirrors a successful structured-contract normalization into
166
- // both assistantText and stdout. Treat node output the same way as the DAG
167
- // runtime's canonicalNodeOutput(): assistantText is authoritative and stdout
168
- // is only a fallback. Concatenating the two duplicate fields makes a single
169
- // canonical plan appear as two complete JSON contracts to the prewrite gate.
172
+ return record;
173
+ }
174
+ async function readNodeText(runDir, nodeId) {
175
+ const record = await readFinishedNodeRecord(runDir, nodeId);
170
176
  const text = record.assistantText?.trim() || record.stdout?.trim() || "";
171
177
  if (!text)
172
178
  throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
173
179
  return text;
174
180
  }
175
- async function readNodeRawText(runDir, nodeId) {
176
- const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
177
- return record.assistantText ?? record.stdout ?? "";
178
- }
179
- /**
180
- * Same shape predicate as the internal `isContract` inside
181
- * `extractFrontendImplementationJson` (frontend-implementation-contract.ts):
182
- * schemaVersion 1 + a plain `targets` object + `requirements` and
183
- * `verificationTargets` arrays. A merge-patch delta does not carry these
184
- * fields, so this is the deterministic boundary between a full contract and a
185
- * patch. Keep the two predicates in sync.
186
- */
187
- function isFullContractShape(value) {
188
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
189
- return false;
190
- }
191
- const record = value;
192
- const targets = record.targets;
193
- return (record.schemaVersion === 1 &&
194
- targets !== null &&
195
- typeof targets === "object" &&
196
- !Array.isArray(targets) &&
197
- Array.isArray(record.requirements) &&
198
- Array.isArray(record.verificationTargets));
199
- }
200
- /**
201
- * Shared merge pipeline for revisionPatch mode and content-driven
202
- * compatibility: read the original contract node, extract the original
203
- * contract and the effective node's patch, apply the RFC 7386 merge-patch, and
204
- * serialize deterministically. Throws on any failure; callers map the error to
205
- * their own classification (retryable-invalid for explicit revisionPatch mode,
206
- * blocked for content-driven fallback).
207
- */
208
- async function mergeRevisionPatch(runDir, planText, originalNodeId) {
209
- const originalText = await readNodeText(runDir, originalNodeId);
210
- const originalValue = extractFrontendImplementationJson(originalText);
211
- const patchValue = extractFrontendImplementationJson(planText);
212
- const merged = applyFrontendImplementationContractPatch(originalValue, patchValue);
213
- return serializeDeterministicJson(merged);
181
+ async function readCanonicalContractFromNode(runDir, nodeId) {
182
+ const record = await readFinishedNodeRecord(runDir, nodeId);
183
+ if (!record.structuredArtifactPath) {
184
+ throw new Error(`frontend prewrite gate missing structured artifact for ${nodeId}`);
185
+ }
186
+ const artifactPath = path.isAbsolute(record.structuredArtifactPath)
187
+ ? record.structuredArtifactPath
188
+ : path.join(runDir, record.structuredArtifactPath);
189
+ const resolvedRun = path.resolve(runDir);
190
+ const resolvedArtifact = path.resolve(artifactPath);
191
+ const relative = path.relative(resolvedRun, resolvedArtifact);
192
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
193
+ throw new Error(`frontend prewrite gate structured artifact escapes runDir: ${record.structuredArtifactPath}`);
194
+ }
195
+ const bytes = await readFile(resolvedArtifact);
196
+ const digest = createHash("sha256").update(bytes).digest("hex");
197
+ if (record.structuredArtifactSha256 &&
198
+ record.structuredArtifactSha256 !== digest) {
199
+ throw new Error(`frontend prewrite gate structured artifact hash mismatch for ${nodeId}`);
200
+ }
201
+ if (record.structuredArtifactSchemaId &&
202
+ record.structuredArtifactSchemaId !== FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID) {
203
+ throw new Error(`frontend prewrite gate structured artifact schema mismatch for ${nodeId}: ${record.structuredArtifactSchemaId}`);
204
+ }
205
+ const contractText = bytes.toString("utf8");
206
+ let citationsText = "";
207
+ try {
208
+ const citationsRaw = await readFile(path.join(path.dirname(resolvedArtifact), "openspec-citations.json"), "utf8");
209
+ try {
210
+ const parsed = JSON.parse(citationsRaw);
211
+ citationsText =
212
+ typeof parsed.block === "string" ? parsed.block : citationsRaw;
213
+ }
214
+ catch {
215
+ citationsText = citationsRaw;
216
+ }
217
+ }
218
+ catch (error) {
219
+ if (error.code !== "ENOENT")
220
+ throw error;
221
+ }
222
+ return { contractText, citationsText, rawSha256: digest };
214
223
  }
215
224
  function firstNonEmptyVerdictLine(text) {
216
225
  const lines = text
@@ -439,7 +448,30 @@ export async function runFrontendPrewriteGate(input) {
439
448
  // remains an ERROR and is intentionally not classified into prewrite-result.
440
449
  const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
441
450
  const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
442
- const candidateRawSha256 = sha256Hex(await readNodeRawText(input.runDir, planNodeId));
451
+ let planArtifact;
452
+ try {
453
+ planArtifact = await readCanonicalContractFromNode(input.runDir, planNodeId);
454
+ }
455
+ catch (error) {
456
+ return finalizePrewrite(input, {
457
+ planNodeId,
458
+ reviewNodeId,
459
+ verdict: "",
460
+ candidateRawSha256: sha256Hex(""),
461
+ candidateJsonSha256: null,
462
+ canonicalSha256: null,
463
+ normalizationActions: [],
464
+ mockStrategy: undefined,
465
+ artifact: undefined,
466
+ failureReason: error instanceof Error ? error.message : String(error),
467
+ failureCode: "plan-text-unreadable",
468
+ classification: "retryable-invalid",
469
+ openspecReadPaths: [],
470
+ openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
471
+ openspecPolicy: input.config.openspecPolicy ?? "scan-strict",
472
+ });
473
+ }
474
+ const candidateRawSha256 = planArtifact.rawSha256;
443
475
  const openspecPolicy = input.config.openspecPolicy ?? "scan-strict";
444
476
  const basePending = {
445
477
  planNodeId,
@@ -458,18 +490,9 @@ export async function runFrontendPrewriteGate(input) {
458
490
  openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
459
491
  openspecPolicy,
460
492
  };
461
- let planText;
462
- try {
463
- planText = await readNodeText(input.runDir, planNodeId);
464
- }
465
- catch (error) {
466
- return finalizePrewrite(input, {
467
- ...basePending,
468
- classification: "retryable-invalid",
469
- failureReason: error instanceof Error ? error.message : String(error),
470
- failureCode: "plan-text-unreadable",
471
- });
472
- }
493
+ const planText = [planArtifact.contractText, planArtifact.citationsText]
494
+ .filter(Boolean)
495
+ .join("\n");
473
496
  let reviewText;
474
497
  try {
475
498
  reviewText = await readNodeText(input.runDir, reviewNodeId);
@@ -492,77 +515,7 @@ export async function runFrontendPrewriteGate(input) {
492
515
  failureCode: "verdict-not-pass",
493
516
  });
494
517
  }
495
- // revisionPatch mode: the effective plan node emits an RFC 7386 merge-patch
496
- // delta against the original contract node (planFallbackFromNodeIds[0]).
497
- // Legacy DAGs without revisionPatch (undefined) keep full-contract semantics.
498
- const revisionPatchMode = input.config.revisionPatch === true &&
499
- planNodeId === input.config.planFromNodeId &&
500
- input.config.planFallbackFromNodeIds.length > 0;
501
- let effectiveContractText = null;
502
- if (revisionPatchMode) {
503
- const originalNodeId = input.config.planFallbackFromNodeIds[0];
504
- try {
505
- effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
506
- }
507
- catch (error) {
508
- return finalizePrewrite(input, {
509
- ...basePending,
510
- verdict,
511
- classification: "retryable-invalid",
512
- failureReason: `frontend prewrite gate revisionPatch mode requires original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}`,
513
- failureCode: "contract-invalid",
514
- });
515
- }
516
- }
517
- else {
518
- // Content-driven compatibility (Bug 1): legacy DAGs without
519
- // revisionPatch can still receive a merge-patch delta from the revision
520
- // node. Detect the output shape instead of trusting the DAG flag alone.
521
- // - Full contract shape → keep legacy full-contract semantics unchanged.
522
- // - Patch shape with a usable original contract node → auto-merge and
523
- // run the existing analysis/materialization pipeline.
524
- // - Patch shape but unmergeable → blocked + revision-patch-unmergeable
525
- // (no retryable loop).
526
- let planJsonValue = null;
527
- try {
528
- planJsonValue = extractFrontendImplementationJson(planText);
529
- }
530
- catch {
531
- // Not machine-readable JSON: fall back to the legacy full-contract
532
- // path where analyzeFrontendImplementationContract surfaces the
533
- // original diagnostic (fail-closed without a new failure mode).
534
- }
535
- if (planJsonValue !== null && !isFullContractShape(planJsonValue)) {
536
- const originalNodeId = input.config.planFallbackFromNodeIds[0];
537
- if (!originalNodeId || originalNodeId === planNodeId) {
538
- return finalizePrewrite(input, {
539
- ...basePending,
540
- verdict,
541
- classification: "blocked",
542
- failureReason: "frontend prewrite gate: the effective plan output is a merge-patch delta but no distinct original contract node is available. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.",
543
- failureCode: "revision-patch-unmergeable",
544
- });
545
- }
546
- try {
547
- const originalText = await readNodeText(input.runDir, originalNodeId);
548
- const originalValue = extractFrontendImplementationJson(originalText);
549
- if (!isFullContractShape(originalValue)) {
550
- throw new Error(`original contract node ${originalNodeId} is not a full frontend implementation contract`);
551
- }
552
- effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
553
- }
554
- catch (error) {
555
- return finalizePrewrite(input, {
556
- ...basePending,
557
- verdict,
558
- classification: "blocked",
559
- failureReason: `frontend prewrite gate: the effective plan output is a merge-patch delta but it cannot be merged onto the original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.`,
560
- failureCode: "revision-patch-unmergeable",
561
- });
562
- }
563
- }
564
- }
565
- const requirementIdSearchText = effectiveContractText ?? planText;
518
+ const requirementIdSearchText = planArtifact.contractText;
566
519
  const missingIds = input.config.requiredRequirementIds.filter((id) => !requirementIdSearchText.includes(id));
567
520
  if (missingIds.length > 0) {
568
521
  return finalizePrewrite(input, {
@@ -610,14 +563,13 @@ export async function runFrontendPrewriteGate(input) {
610
563
  workspaceRoot,
611
564
  });
612
565
  }
613
- // Candidate -> canonical conversion. The gate consumes the selected
614
- // plan/revision assistant output directly and is the only conversion point.
566
+ // Candidate -> canonical conversion. The gate consumes the producing node's
567
+ // hash-bound canonical artifact and never re-merges plan/revision stdout.
615
568
  let analysis;
616
569
  try {
617
570
  analysis = await analyzeFrontendImplementationContract({
618
571
  runDir: input.runDir,
619
- fromNodeId: effectiveContractText ? undefined : planNodeId,
620
- rawContractText: effectiveContractText ?? undefined,
572
+ rawContractText: planArtifact.contractText,
621
573
  sourceBinding: input.sourceBinding,
622
574
  });
623
575
  }
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { z } from "zod";
4
4
  import { pathMatchesPattern } from "../../shared/git-progress.js";
5
5
  import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
6
+ import { FRONTEND_NO_VERIFICATION_MARKER_TEXT, FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE, } from "./frontend-verification-trace.js";
6
7
  export const FRONTEND_REPAIR_ASSESSMENT_SCHEMA_ID = "frontend-repair-assessment-v1";
7
8
  export const frontendRepairFailureClassSchema = z.enum([
8
9
  "typecheck",
@@ -82,6 +83,12 @@ export function classifyFrontendFailure(input) {
82
83
  if (input.failureCategory === "write-guard") {
83
84
  return "path";
84
85
  }
86
+ if (`${input.stderr ?? ""}\n${input.stdout ?? ""}`.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT)) {
87
+ // Verification unavailability is a generation-time contract gap: no
88
+ // authorized verification command exists. Bounded repair cannot fix it;
89
+ // the operator must declare commands and regenerate the DAG.
90
+ return "contract";
91
+ }
85
92
  if (input.traceTarget || input.commandCategory === "trace") {
86
93
  return "trace";
87
94
  }
@@ -424,6 +431,13 @@ export async function runFrontendFailureAssessGate(input) {
424
431
  : !baseEligible
425
432
  ? `non-repairable or ineligible: failureClass=${failureClass} attempt=${attempt}`
426
433
  : `missing structured evidence for failureClass=${failureClass} on ${primary.nodeId}`;
434
+ if (!eligible &&
435
+ `${primary.record.stderr ?? ""}`.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT)) {
436
+ // Surface the actionable unavailable-verification reason in the
437
+ // assessment (and the non-repairable gate error) instead of a
438
+ // generic class label.
439
+ reason = FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE;
440
+ }
427
441
  }
428
442
  // fixScope and changedPaths must stay inside the implement writeSet.
429
443
  if (evidence.fixScope?.length) {
@@ -2,6 +2,12 @@
2
2
  * Deterministic frontend-implementation risk classifier (M4).
3
3
  * Pure function: no I/O, no model, no side effects.
4
4
  */
5
+ /**
6
+ * Dependency-installation language in task text. Mentioning package.json or
7
+ * allowing its path is NOT dependency introduction by itself; the manifest
8
+ * path check in classifyFrontendRisk requires this language to escalate.
9
+ */
10
+ const DEPENDENCY_INSTALL_LANGUAGE = /new\s+dependency|add\s+dependencies?|install\s+dependencies?|npm\s+(?:install|i)\b|yarn\s+(?:add|install)\b|pnpm\s+(?:add|install)\b|bun\s+(?:add|install)\b|新增依赖|添加依赖|安装依赖|引入依赖/i;
5
11
  const HIGH_RISK_PATTERNS = [
6
12
  {
7
13
  id: "new-route",
@@ -25,7 +31,7 @@ const HIGH_RISK_PATTERNS = [
25
31
  },
26
32
  {
27
33
  id: "new-dependency",
28
- re: /new\s+dependency|add\s+dependency|npm\s+install|新增依赖|package\.json/i,
34
+ re: DEPENDENCY_INSTALL_LANGUAGE,
29
35
  },
30
36
  {
31
37
  id: "schema-api-change",
@@ -88,7 +94,14 @@ export function classifyFrontendRisk(input) {
88
94
  const paths = input.allowedPaths ?? [];
89
95
  const spread = pathSpreadScore(paths);
90
96
  if (paths.some((p) => /package\.json|pnpm-lock|yarn\.lock|package-lock/.test(p))) {
91
- signals.push("new-dependency");
97
+ if (DEPENDENCY_INSTALL_LANGUAGE.test(blob)) {
98
+ signals.push("new-dependency");
99
+ }
100
+ else {
101
+ // Editing manifest scripts/config is not dependency introduction;
102
+ // record the rejected escalation instead of forcing full gates.
103
+ rejectedSignals.push("new-dependency:manifest-path-without-install-language");
104
+ }
92
105
  }
93
106
  // Explicit multi-domain language only — path count alone is not high-risk
94
107
  if (spread >= 3 &&