@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.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 (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 +1691 -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 +1152 -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 +60 -17
  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 +366 -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 +343 -391
  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 +439 -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
@@ -2,6 +2,18 @@ import { readFile, stat, writeFile, mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
4
4
  export const FRONTEND_VERIFICATION_TRACE_SCHEMA_ID = "frontend-verification-trace-v1";
5
+ /**
6
+ * Shared marker for a project that exposes no usable verification command at
7
+ * generation time. The marker command runs (and logs not-run honestly), but it
8
+ * must never count as successful verification evidence: labels carrying this
9
+ * text are excluded from successful evidence and targets bound to it fail the
10
+ * trace gate instead of passing it.
11
+ */
12
+ export const FRONTEND_NO_VERIFICATION_MARKER_TEXT = "No frontend verification command configured";
13
+ export function isFrontendNoVerificationMarkerLabel(label) {
14
+ return label.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT);
15
+ }
16
+ export const FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE = `frontend verification unavailable: the only frozen static command is the no-op not-run marker (${FRONTEND_NO_VERIFICATION_MARKER_TEXT}); declare verification commands via task.json --verify or 执行约束.md and regenerate the DAG`;
5
17
  async function readJsonFile(filePath) {
6
18
  return JSON.parse(await readFile(filePath, "utf8"));
7
19
  }
@@ -37,7 +49,7 @@ function symbolEvidenceCandidates(symbol) {
37
49
  return [symbol, describeTitle, "describe("];
38
50
  return [symbol];
39
51
  }
40
- function isConfigurationVerificationFile(file) {
52
+ export function isConfigurationVerificationFile(file) {
41
53
  return /(?:^|\/)(?:package\.json|tsconfig(?:\.[^/]+)?\.json|(?:vite|webpack|rollup|docusaurus)\.config\.[cm]?[jt]s)$/.test(file.replace(/\\/g, "/"));
42
54
  }
43
55
  /**
@@ -52,7 +64,14 @@ export async function evaluateVerificationTargets(input) {
52
64
  const hardIssues = [];
53
65
  for (const target of input.contract.verificationTargets) {
54
66
  const issues = [];
55
- const owners = input.labelOwners.get(target.commandLabel);
67
+ if (isFrontendNoVerificationMarkerLabel(target.commandLabel)) {
68
+ // A target bound to the not-run marker has no verification behind it;
69
+ // passing it would let the run finish green without any check.
70
+ issues.push(`${target.id}: ${FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE}`);
71
+ }
72
+ const owners = isFrontendNoVerificationMarkerLabel(target.commandLabel)
73
+ ? []
74
+ : (input.labelOwners.get(target.commandLabel) ?? []);
56
75
  if (!owners?.length) {
57
76
  issues.push(`commandLabel not executed in current-run frozen evidence: ${target.commandLabel}`);
58
77
  }
@@ -82,6 +101,26 @@ export async function evaluateVerificationTargets(input) {
82
101
  }
83
102
  return { targets, hardIssues };
84
103
  }
104
+ /**
105
+ * Shared deterministic symbol resolvability check used by BOTH the verify
106
+ * trace gate and the plan-time canonical drop (`dropUnresolvedVerificationSymbols`):
107
+ * whether `symbol` can be matched inside `content`. Handles `all describe
108
+ * blocks`, `describe('title')` extraction, and slash-delimited multi-symbol
109
+ * labels the same way everywhere, so plan-time normalization and the final
110
+ * trace gate can never disagree.
111
+ */
112
+ export function verificationSymbolMatchesContent(symbol, content) {
113
+ if (symbol.trim().toLowerCase() === "all describe blocks")
114
+ return content.includes("describe(");
115
+ const symbols = symbol.includes(" / ")
116
+ ? symbol
117
+ .split(/[((]/, 1)[0]
118
+ .split("/")
119
+ .map((item) => item.trim())
120
+ .filter(Boolean)
121
+ : [symbol];
122
+ return symbols.every((item) => symbolEvidenceCandidates(item).some((candidate) => content.includes(candidate)));
123
+ }
85
124
  async function assertFileAndSymbol(input) {
86
125
  const issues = [];
87
126
  const absolute = path.resolve(input.workspaceRoot, input.file);
@@ -105,18 +144,8 @@ async function assertFileAndSymbol(input) {
105
144
  }
106
145
  if (input.symbol) {
107
146
  const content = await readFile(absolute, "utf8");
108
- const symbols = input.symbol.includes(" / ")
109
- ? input.symbol
110
- .split(/[((]/, 1)[0]
111
- .split("/")
112
- .map((symbol) => symbol.trim())
113
- .filter(Boolean)
114
- : [input.symbol];
115
- for (const symbol of symbols) {
116
- const matched = symbolEvidenceCandidates(symbol).some((candidate) => content.includes(candidate));
117
- if (!matched) {
118
- issues.push(`symbol not found: ${symbol} in ${input.file}`);
119
- }
147
+ if (!verificationSymbolMatchesContent(input.symbol, content)) {
148
+ issues.push(`symbol not found: ${input.symbol} in ${input.file}`);
120
149
  }
121
150
  }
122
151
  return issues;
@@ -189,11 +218,17 @@ export async function runFrontendVerificationTraceGate(input) {
189
218
  throw new Error("trace: selected Mock strategy requires successful Mock verification commands");
190
219
  }
191
220
  if (input.evidence) {
192
- if (input.evidence.static.commandLabels.length === 0) {
193
- throw new Error("trace: no successful static verification commands in current run");
221
+ // The no-op not-run marker never counts as successful verification
222
+ // evidence, even if it executed with exit code 0.
223
+ const effectiveStaticLabels = input.evidence.static.commandLabels.filter((label) => !isFrontendNoVerificationMarkerLabel(label));
224
+ const effectiveBehaviorLabels = input.evidence.behavior.commandLabels.filter((label) => !isFrontendNoVerificationMarkerLabel(label));
225
+ if (effectiveStaticLabels.length === 0) {
226
+ throw new Error(input.evidence.static.commandLabels.length > 0
227
+ ? `trace: ${FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE}`
228
+ : "trace: no successful static verification commands in current run");
194
229
  }
195
230
  if (requiresBehaviorVerification &&
196
- input.evidence.behavior.commandLabels.length === 0) {
231
+ effectiveBehaviorLabels.length === 0) {
197
232
  throw new Error("trace: no successful behavior verification commands in current run");
198
233
  }
199
234
  }
@@ -10,21 +10,52 @@ async function runGit(cwd, args) {
10
10
  const result = await runResilientGitCommand({ cwd, args, readOnly: true });
11
11
  return { code: result.code, stdout: result.stdout, stderr: result.stderr };
12
12
  }
13
+ /**
14
+ * Ephemeral interpreter/tool caches that writers may create while drafting
15
+ * tests (kept in sync with `isEphemeralToolCachePath` in
16
+ * src/executors/shell-write-guard.ts). They are not product evidence: tool
17
+ * cache writes must neither fail the write guard nor appear in the review
18
+ * diff as pre-existing changes.
19
+ */
20
+ export function isEphemeralToolCachePath(filePath) {
21
+ const normalized = filePath.replace(/\\/g, "/");
22
+ if (!normalized)
23
+ return false;
24
+ if (normalized === ".pytest_cache" ||
25
+ normalized.startsWith(".pytest_cache/")) {
26
+ return true;
27
+ }
28
+ if (normalized === ".mypy_cache" || normalized.startsWith(".mypy_cache/")) {
29
+ return true;
30
+ }
31
+ if (normalized === ".ruff_cache" || normalized.startsWith(".ruff_cache/")) {
32
+ return true;
33
+ }
34
+ if (normalized === ".playwright-cli" ||
35
+ normalized.startsWith(".playwright-cli/")) {
36
+ return true;
37
+ }
38
+ if (normalized === ".coverage" || normalized.startsWith(".coverage.")) {
39
+ return true;
40
+ }
41
+ // vite/vitest runtime caches live under node_modules/.vite (e.g.
42
+ // node_modules/.vite/vitest/results.json). node_modules is the gitignored
43
+ // dependency tree — tool cache writes there are never a product change.
44
+ if (normalized.startsWith("node_modules/.vite/")) {
45
+ return true;
46
+ }
47
+ if (/(?:^|\/)__pycache__(?:\/|$)/.test(normalized))
48
+ return true;
49
+ if (/\.(?:pyc|pyo)$/.test(normalized))
50
+ return true;
51
+ return false;
52
+ }
13
53
  function splitLines(text) {
14
54
  return text
15
55
  .split(/\r?\n/)
16
56
  .map((line) => line.trim())
17
57
  .filter(Boolean);
18
58
  }
19
- function statusPaths(status) {
20
- return status
21
- .split(/\r?\n/)
22
- .map((line) => line.trimEnd())
23
- .filter((line) => line.length >= 4)
24
- .map((line) => line.slice(3).split(" -> ").at(-1) ?? "")
25
- .filter(Boolean)
26
- .sort();
27
- }
28
59
  async function sha256File(filePath) {
29
60
  try {
30
61
  const bytes = await readFile(filePath);
@@ -37,15 +68,26 @@ async function sha256File(filePath) {
37
68
  export async function captureFrontendWorktreeBaseline(input) {
38
69
  const run = input.dependencies?.runGit ?? runGit;
39
70
  const root = path.resolve(input.workspaceRoot);
40
- const status = await run(root, [
41
- "status",
42
- "--porcelain=v1",
43
- "--untracked-files=all",
71
+ // Baseline = the full concrete workspace inventory the writer may touch
72
+ // (tracked + untracked, gitignored excluded), NOT just currently-changed
73
+ // files. Using `git status` here made a clean repository baseline-empty
74
+ // (status has no entries when nothing changed), which wrongly blocked
75
+ // writer admission with baseline-empty.
76
+ const ls = await run(root, [
77
+ "ls-files",
78
+ "-co",
79
+ "--exclude-standard",
80
+ "-z",
44
81
  ]);
45
- if (status.code !== 0) {
46
- throw new Error(`frontend worktree baseline failed: ${status.stderr.trim() || status.stdout.trim()}`);
82
+ if (ls.code !== 0) {
83
+ throw new Error(`frontend worktree baseline failed: ${ls.stderr.trim() || ls.stdout.trim()}`);
47
84
  }
48
- const files = statusPaths(status.stdout);
85
+ const files = ls.stdout
86
+ .split("\0")
87
+ .map((line) => line.trim())
88
+ .filter(Boolean)
89
+ .filter((file) => !isEphemeralToolCachePath(file))
90
+ .sort();
49
91
  const hashes = {};
50
92
  for (const file of files)
51
93
  hashes[file] = await sha256File(path.join(root, file));
@@ -112,8 +154,12 @@ export async function runFrontendWorktreeDiffGate(input) {
112
154
  if (untracked.code !== 0) {
113
155
  throw new Error(`frontend worktree diff gate ls-files failed: ${untracked.stderr.trim() || untracked.stdout.trim()}`);
114
156
  }
115
- const changedFilesAll = splitLines(nameOnly.stdout).sort();
116
- const untrackedFilesAll = splitLines(untracked.stdout).sort();
157
+ const changedFilesAll = splitLines(nameOnly.stdout)
158
+ .filter((file) => !isEphemeralToolCachePath(file))
159
+ .sort();
160
+ const untrackedFilesAll = splitLines(untracked.stdout)
161
+ .filter((file) => !isEphemeralToolCachePath(file))
162
+ .sort();
117
163
  const authorizedChangedPaths = input.authorizedChangedPaths ?? [];
118
164
  if (baseline) {
119
165
  for (const file of baseline.files) {
@@ -199,3 +245,36 @@ export function formatFrontendWorktreeDiffStdout(result) {
199
245
  .filter(Boolean)
200
246
  .join("\n");
201
247
  }
248
+ /**
249
+ * Expand glob-shaped contract paths into concrete workspace file paths.
250
+ *
251
+ * The frontend contract schema allows `targets.files` entries such as
252
+ * `src/**` (skeleton inherits the task allowedPaths), but writer admission
253
+ * derives the concrete writeSet and requires real file paths. This helper
254
+ * matches every candidate against the enumerated workspace files and returns
255
+ * the concrete paths. Candidates that are already concrete are kept as-is;
256
+ * globs that match nothing are dropped (the derived writeSet only contains
257
+ * paths the writer can actually touch).
258
+ */
259
+ export function expandFrontendGlobPaths(input) {
260
+ const concrete = new Set();
261
+ const patterns = [];
262
+ for (const candidate of input.candidates) {
263
+ const normalized = candidate.replace(/\\/g, "/");
264
+ if (normalized.includes("*")) {
265
+ patterns.push(normalized);
266
+ }
267
+ else {
268
+ concrete.add(normalized);
269
+ }
270
+ }
271
+ if (patterns.length === 0)
272
+ return [...concrete].sort();
273
+ const workspace = [...new Set(input.workspaceFiles.map((f) => f.replace(/\\/g, "/")))];
274
+ for (const file of workspace) {
275
+ if (patterns.some((pattern) => pathMatchesPattern(file, pattern))) {
276
+ concrete.add(file);
277
+ }
278
+ }
279
+ return [...concrete].sort();
280
+ }
@@ -0,0 +1,260 @@
1
+ import { deterministicSha256, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
2
+ import { expandFrontendGlobPaths } from "./frontend-worktree-diff.js";
3
+ import { z } from "zod";
4
+ /** A+B (AC-006): four-state writer admission classification. `accepted` is the
5
+ * only authorized state; `requires-human-approval` is the High-topology
6
+ * high-admission-receipt gate; `blocked` is deterministic denial; `stale` is
7
+ * digest drift (source refresh / re-contract). */
8
+ export const frontendWriterAdmissionClassificationSchema = z.enum([
9
+ "accepted",
10
+ "requires-human-approval",
11
+ "blocked",
12
+ "stale",
13
+ // Legacy fixture alias: writeSet-external test helpers still emit `admitted`.
14
+ // Canonical producers always emit one of the four values above.
15
+ "admitted",
16
+ ]);
17
+ /** A+B (AC-006): Micro topology uses `requirement-to-target-and-evidence`
18
+ * coverage; Small/Standard/High use `requirement-to-decision`. The two schemas
19
+ * are deliberately separated so a Micro evidence coverage can never be read as
20
+ * a decision coverage. */
21
+ export const frontendAdmissionCoverageSchema = z.discriminatedUnion("coverageId", [
22
+ z
23
+ .object({
24
+ coverageId: z.literal("requirement-to-target-and-evidence"),
25
+ requirementId: z.string().min(1),
26
+ targetPaths: z.array(z.string().min(1)),
27
+ evidenceRefs: z.array(z.string().min(1)),
28
+ })
29
+ .strict(),
30
+ z
31
+ .object({
32
+ coverageId: z.literal("requirement-to-decision"),
33
+ requirementId: z.string().min(1),
34
+ decisionRefs: z.array(z.string().min(1)),
35
+ })
36
+ .strict(),
37
+ ]);
38
+ /** Failure-source values shared with frontend recovery planning (M8). */
39
+ export const frontendAdmissionFailureSourceSchema = z.enum([
40
+ "frontend-plan-pi",
41
+ "frontend-design-policy-shell",
42
+ "frontend-writer-admission-shell",
43
+ "frontend-implement-pi",
44
+ "frontend-verify-shell",
45
+ ]);
46
+ /**
47
+ * Deterministic artifact schema for `contracts/frontend-writer-admission-result.json`.
48
+ * Scheduler admission gate and frontend recovery planning read this artifact
49
+ * (fail-closed); it replaces the legacy frontend-prewrite-result-v1 as the
50
+ * write authorization source.
51
+ */
52
+ export const frontendWriterAdmissionResultV1Schema = z
53
+ .object({
54
+ schemaVersion: z.literal(1),
55
+ schemaId: z.literal("frontend-writer-admission-shell-v1"),
56
+ classification: frontendWriterAdmissionClassificationSchema,
57
+ writeSet: z.array(z.string().min(1)),
58
+ writeSetDigest: z.string().regex(/^[a-f0-9]{64}$/),
59
+ baselineDigest: z.string().regex(/^[a-f0-9]{64}$/),
60
+ admissionDigest: z.string().regex(/^[a-f0-9]{64}$/),
61
+ baselineDebtPolicy: z.string().min(1).optional(),
62
+ findings: z.array(z
63
+ .object({
64
+ code: z.string().min(1),
65
+ message: z.string().min(1),
66
+ path: z.string().optional(),
67
+ })
68
+ .strict()),
69
+ failureSource: frontendAdmissionFailureSourceSchema.optional(),
70
+ })
71
+ .strict();
72
+ /** Artifact relative path written by the frontend-writer-admission-shell. */
73
+ export const FRONTEND_WRITER_ADMISSION_RESULT_ARTIFACT = "contracts/frontend-writer-admission-result.json";
74
+ function sortFindings(findings) {
75
+ return [...findings].sort((a, b) => {
76
+ if (a.code !== b.code)
77
+ return a.code < b.code ? -1 : 1;
78
+ if (a.message !== b.message)
79
+ return a.message < b.message ? -1 : 1;
80
+ const pathA = a.path ?? "";
81
+ const pathB = b.path ?? "";
82
+ if (pathA !== pathB)
83
+ return pathA < pathB ? -1 : 1;
84
+ return 0;
85
+ });
86
+ }
87
+ /** Reject non-concrete writeSet entries: absolute paths, traversal, globstar,
88
+ * repo-root, and empty/duplicate-slash segments. */
89
+ export function isConcreteWriteSetPath(value) {
90
+ const path = value.trim();
91
+ if (path === "" || path === "." || path === "./")
92
+ return false;
93
+ if (path.startsWith("/") || path.includes("\\"))
94
+ return false;
95
+ const segments = path.split("/");
96
+ if (segments.some((segment) => segment === ".." || segment === "**" || segment === "")) {
97
+ return false;
98
+ }
99
+ return true;
100
+ }
101
+ /** Derive the concrete writeSet from implementation targets ∪ non-static
102
+ * verification target files. Entries are de-duplicated and lexicographically
103
+ * sorted (frozen). */
104
+ export function deriveWriteSet(contract, options) {
105
+ const findings = [];
106
+ const candidates = [
107
+ ...contract.targets.files,
108
+ ...contract.verificationTargets
109
+ .filter((target) => target.type !== "static")
110
+ .map((target) => target.file),
111
+ ];
112
+ // Expand glob entries (e.g. `src/**` inherited from the skeleton) against
113
+ // the enumerated workspace files so the derived writeSet contains only
114
+ // concrete paths. Without a file list, globs are treated as concrete and
115
+ // rejected by isConcreteWriteSetPath below (fail-closed).
116
+ const expanded = options?.workspaceFiles
117
+ ? expandFrontendGlobPaths({
118
+ candidates,
119
+ workspaceFiles: options.workspaceFiles,
120
+ })
121
+ : candidates;
122
+ const writeSet = new Set();
123
+ for (const candidate of expanded) {
124
+ if (!isConcreteWriteSetPath(candidate)) {
125
+ findings.push({
126
+ code: "write-set-path-not-concrete",
127
+ message: `writeSet entry is not a concrete path: ${candidate}`,
128
+ path: candidate,
129
+ });
130
+ continue;
131
+ }
132
+ writeSet.add(candidate);
133
+ }
134
+ return { writeSet: [...writeSet].sort(), findings };
135
+ }
136
+ /** Sort and freeze the injected baseline; duplicate paths and empty baselines
137
+ * are blocking (collection itself is production wiring, not this shell). */
138
+ export function freezeBaseline(baseline) {
139
+ const findings = [];
140
+ if (baseline.length === 0) {
141
+ findings.push({
142
+ code: "baseline-empty",
143
+ message: "writer admission baseline is empty",
144
+ });
145
+ }
146
+ const seen = new Set();
147
+ for (const entry of baseline) {
148
+ if (seen.has(entry.path)) {
149
+ findings.push({
150
+ code: "baseline-duplicate-path",
151
+ message: `baseline contains duplicate path: ${entry.path}`,
152
+ path: entry.path,
153
+ });
154
+ }
155
+ seen.add(entry.path);
156
+ }
157
+ const sorted = [...baseline].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
158
+ return { baselineDigest: deterministicSha256(sorted), findings };
159
+ }
160
+ /** Combine mock strategy, allowed strategies digest, frozen command digest,
161
+ * production-default-off flag and real-integration expectation into one
162
+ * deterministic admission digest. */
163
+ export function deriveAdmissionDigest(contract, allowedMockStrategies, frozenCommandLabels, baselineDebtPolicy) {
164
+ return deterministicSha256({
165
+ mockApiStrategy: contract.mockApi.strategy,
166
+ allowedMockStrategiesDigest: deterministicSha256([...allowedMockStrategies].sort()),
167
+ frozenCommandLabelsDigest: deterministicSha256([...frozenCommandLabels].sort()),
168
+ productionDefaultOff: contract.mockApi.productionDefaultOff === true,
169
+ hasRealIntegrationGap: Boolean(contract.realIntegrationGap),
170
+ baselineDebtPolicy: baselineDebtPolicy ?? "",
171
+ });
172
+ }
173
+ /** Shape-aware deterministic admission: re-validate the typed contract, honor
174
+ * the design-policy result, derive/freeze the writeSet, digest the baseline,
175
+ * and assemble the admission digest. */
176
+ export function deriveFrontendWriterAdmissionShell(input) {
177
+ const findings = [];
178
+ const shape = frontendImplementationContractSchema.safeParse(input.contract);
179
+ if (!shape.success) {
180
+ findings.push({
181
+ code: "contract-shape-invalid",
182
+ message: `contract fails strict shape validation: ${shape.error.issues[0]?.message ?? "unknown issue"}`,
183
+ path: shape.error.issues[0]?.path.join(".") || undefined,
184
+ });
185
+ }
186
+ if (input.designPolicyResult.classification === "blocked") {
187
+ findings.push({
188
+ code: "design-policy-blocked",
189
+ message: `design policy is blocked with ${input.designPolicyResult.findings.length} finding(s)`,
190
+ });
191
+ }
192
+ const derived = deriveWriteSet(input.contract, {
193
+ workspaceFiles: input.workspaceFiles,
194
+ });
195
+ findings.push(...derived.findings);
196
+ const baseline = freezeBaseline(input.baseline);
197
+ findings.push(...baseline.findings);
198
+ const sortedFindings = sortFindings(findings);
199
+ // Deterministic admission: any finding blocks. High-risk contracts keep the
200
+ // full policy/writeSet/baseline gates but do NOT stop for a human approval
201
+ // step — the DAG runs unattended, so `requires-human-approval` would strand
202
+ // every high-risk writer. The receipt is still materialized for audit; the
203
+ // scheduler authorizes `accepted` directly.
204
+ const classification = sortedFindings.length > 0 ? "blocked" : "accepted";
205
+ const result = {
206
+ schemaId: "frontend-writer-admission-shell-v1",
207
+ classification,
208
+ writeSet: derived.writeSet,
209
+ writeSetDigest: deterministicSha256(derived.writeSet),
210
+ baselineDigest: baseline.baselineDigest,
211
+ admissionDigest: deriveAdmissionDigest(input.contract, input.allowedMockStrategies, input.frozenCommandLabels, input.baselineDebtPolicy),
212
+ ...(input.baselineDebtPolicy !== undefined
213
+ ? { baselineDebtPolicy: input.baselineDebtPolicy }
214
+ : {}),
215
+ findings: sortedFindings,
216
+ };
217
+ return result;
218
+ }
219
+ function normalizeNewVerdict(classification) {
220
+ return classification === "accepted" ||
221
+ classification === "requires-human-approval" ||
222
+ classification === "admitted"
223
+ ? "accepted"
224
+ : "blocked";
225
+ }
226
+ function normalizeLegacyVerdict(classification) {
227
+ return classification === "accepted" || classification === "accepted-normalized"
228
+ ? "accepted"
229
+ : "blocked";
230
+ }
231
+ export function compareAdmissionToLegacyGate(input) {
232
+ const newVerdict = normalizeNewVerdict(input.newClassification);
233
+ const legacyVerdict = normalizeLegacyVerdict(input.legacyClassification);
234
+ return {
235
+ newClassification: input.newClassification,
236
+ legacyClassification: input.legacyClassification,
237
+ newVerdict,
238
+ legacyVerdict,
239
+ match: newVerdict === legacyVerdict,
240
+ };
241
+ }
242
+ /** Render a stable, deterministically ordered mismatch list for the evaluation
243
+ * set. Input order is irrelevant: rows are sorted by a canonical key. */
244
+ export function renderAdmissionShadowDiff(results) {
245
+ const indexed = results.map((result, index) => ({ ...result, index }));
246
+ const sorted = indexed.sort((a, b) => {
247
+ const keyA = `${a.legacyClassification}|${a.newClassification}`;
248
+ const keyB = `${b.legacyClassification}|${b.newClassification}`;
249
+ if (keyA !== keyB)
250
+ return keyA < keyB ? -1 : 1;
251
+ return a.index - b.index;
252
+ });
253
+ const mismatches = sorted.filter((row) => !row.match);
254
+ const lines = [
255
+ "## Admission Shadow Diff",
256
+ `${sorted.length} comparisons, ${mismatches.length} mismatches`,
257
+ ...sorted.map((row) => `- ${row.legacyClassification} -> ${row.newClassification}: legacy=${row.legacyVerdict} new=${row.newVerdict}${row.match ? " match" : " MISMATCH"}`),
258
+ ];
259
+ return lines.join("\n");
260
+ }