@tea-agent/loop-agent 0.41.1 → 0.42.0-next.10

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 (302) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +397 -5
  3. package/dist/adapters/context-transfer/optional-pi-handoff.js +31 -0
  4. package/dist/adapters/context-transfer/pi-session.js +61 -0
  5. package/dist/application/dag/generate-task-dag.js +75 -19
  6. package/dist/application/dag/run-dag.js +41 -0
  7. package/dist/application/task-lifecycle/advance.js +24 -5
  8. package/dist/application/task-lifecycle/observe.js +171 -17
  9. package/dist/application/task-lifecycle/plan-transitions.js +42 -7
  10. package/dist/application/task-lifecycle/recommendations.js +13 -2
  11. package/dist/build-stamp.json +3 -3
  12. package/dist/cli/command-definitions.js +19 -0
  13. package/dist/cli/program.js +29 -2
  14. package/dist/commands/client-recovery.js +3 -0
  15. package/dist/commands/dag-artifact.js +284 -0
  16. package/dist/commands/dag-context.js +184 -0
  17. package/dist/commands/dag-follow-up.js +138 -0
  18. package/dist/commands/dag-rerun-task.js +2 -0
  19. package/dist/commands/dag-rerun.js +206 -1
  20. package/dist/commands/init.js +27 -1
  21. package/dist/commands/task-advance.js +19 -0
  22. package/dist/executors/dag-pi-executor.js +4013 -86
  23. package/dist/executors/pi-executor.js +15 -4
  24. package/dist/executors/pi-read-budget-policy.js +239 -0
  25. package/dist/executors/pi-sdk-executor.js +3 -0
  26. package/dist/executors/shell-executor.js +978 -188
  27. package/dist/executors/shell-write-guard.js +7 -0
  28. package/dist/{worker → infrastructure}/console/app-data.js +4 -0
  29. package/dist/infrastructure/console/artifact-revision-store.js +430 -0
  30. package/dist/infrastructure/console/context-export-store.js +160 -0
  31. package/dist/infrastructure/console/dir-lock.js +132 -0
  32. package/dist/infrastructure/console/operation-store.js +417 -0
  33. package/dist/infrastructure/harness/artifact-store.js +10 -1
  34. package/dist/infrastructure/harness/atomic-write.js +12 -2
  35. package/dist/shared/context-transfer/artifact-revision.js +172 -0
  36. package/dist/shared/context-transfer.js +418 -0
  37. package/dist/shared/dag-failure-category.js +12 -0
  38. package/dist/shared/openspec-spec.js +70 -4
  39. package/dist/shared/operator/capabilities.js +21 -0
  40. package/dist/shared/operator/safe-run-summary.js +1 -0
  41. package/dist/shared/path-safety.js +93 -0
  42. package/dist/shared/preview.js +28 -4
  43. package/dist/task/config-types.js +107 -6
  44. package/dist/task/contract/adopt.js +4 -0
  45. package/dist/task/contract/import-revision.js +4 -0
  46. package/dist/task/contract/project.js +3 -0
  47. package/dist/task/contract/schema.js +2 -1
  48. package/dist/task/frontend-project-capability.js +203 -20
  49. package/dist/task/runtime.js +5 -2
  50. package/dist/task/source-prepare/build-draft.js +3 -3
  51. package/dist/task/source-prepare/fragment-inventory.js +64 -25
  52. package/dist/task/source-prepare/prepare.js +126 -21
  53. package/dist/task/source-prepare/semantic-intake.js +6 -2
  54. package/dist/task/source-references.js +22 -1
  55. package/dist/task/task-demand-routing.js +3 -0
  56. package/dist/worker/console/chat/chat-event-store.js +2 -2
  57. package/dist/worker/console/chat/model-resolver.js +7 -4
  58. package/dist/worker/console/chat/pi-runtime.js +86 -5
  59. package/dist/worker/console/chat/resource-preferences-store.js +1 -1
  60. package/dist/worker/console/chat/routes.js +136 -53
  61. package/dist/worker/console/chat/sdd-data-alignment.js +222 -0
  62. package/dist/worker/console/chat/session-catalog.js +2 -0
  63. package/dist/worker/console/chat/session-store.js +43 -1
  64. package/dist/worker/console/chat/shortcuts.js +18 -2
  65. package/dist/worker/console/chat/user-questions.js +1 -1
  66. package/dist/worker/console/console-update-runtime.js +1 -1
  67. package/dist/worker/console/context-transfer-diagnostics.js +198 -0
  68. package/dist/worker/console/dag-confirmation.js +1 -1
  69. package/dist/worker/console/dag-execution-receipt.js +1 -1
  70. package/dist/worker/console/doctor.js +1 -1
  71. package/dist/worker/console/draft-store.js +1 -1
  72. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  73. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  74. package/dist/worker/console/human-gate-token.js +1 -1
  75. package/dist/worker/console/index.js +6 -3
  76. package/dist/worker/console/interview/assessment.js +1 -1
  77. package/dist/worker/console/interview/session.js +1 -1
  78. package/dist/worker/console/operation-runner.js +45 -4
  79. package/dist/worker/console/operation-sse.js +1 -1
  80. package/dist/worker/console/operation-wait.js +1 -1
  81. package/dist/worker/console/operator-actions.js +273 -10
  82. package/dist/worker/console/operator-surface-health.js +1 -1
  83. package/dist/worker/console/operator-user-error.js +169 -0
  84. package/dist/worker/console/pi-plugins.js +57 -0
  85. package/dist/worker/console/pi-readiness.js +3 -2
  86. package/dist/worker/console/routes.js +637 -8
  87. package/dist/worker/console/security.js +35 -0
  88. package/dist/worker/console/server.js +29 -4
  89. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-D7oU983t.js → abnfDiagram-N423BO3Z-CcS17TBr.js} +1 -1
  90. package/dist/worker/console/static/assets/{arc-BrKSRdFS.js → arc-COptKq2S.js} +1 -1
  91. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CiXV7L56.js → architectureDiagram-T3A2C74G-h4LKMHjP.js} +1 -1
  92. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D_Hs_gIG.js → blockDiagram-VBNYF7ZC-COA1MOH0.js} +1 -1
  93. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DbM-Lqqh.js → c4Diagram-5PPSVZJV-DJUf0QPm.js} +1 -1
  94. package/dist/worker/console/static/assets/channel-DdBCaOJ6.js +1 -0
  95. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D1M8uqJw.js → chunk-2GRJ4B5K-mtWfKrUX.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-4D3RFHT6.js → chunk-2Q5K7J3B-B8pXsxDQ.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CdwpY_n9.js → chunk-5RXB4S5H-ipKzByl1.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BLeLZ9PG.js → chunk-5VM5RSS4-DYi3Ald_.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CeRNEMvc.js → chunk-6Q2QTUOP-DQtGYoty.js} +1 -1
  100. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjkHP8Vy.js → chunk-GF5L2VYU-BU0qS2YV.js} +1 -1
  101. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-B54ZUQY-.js → chunk-JWPE2WC7-DVH9dIGE.js} +1 -1
  102. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-4bKaMVmx.js → chunk-KBJHAD2P-Dt60SmgM.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Cyjfznu_.js → chunk-RYQCIY6F-BoaUGuEY.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CDN-zGFV.js → chunk-XXDRQBXY-_WHFDTp2.js} +1 -1
  105. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DZFra1GO.js +1 -0
  106. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DZFra1GO.js +1 -0
  107. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-HxJmz411.js → cose-bilkent-JH36ORCC-BxkbTIRd.js} +1 -1
  108. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DtSt5K2w.js → cynefin-VYW2F7L2-Bf2UVnoG.js} +1 -1
  109. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-VtkSpRmG.js → cynefinDiagram-MW4NZA55-Bo23q1J_.js} +1 -1
  110. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bj4DT4du.js → dagre-VZM6K2ZE-Dd_UU49i.js} +1 -1
  111. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Bpuk6BmP.js → diagram-7IWD3JNH-ebUa1a9y.js} +1 -1
  112. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BHYGv_yO.js → diagram-B4RE2ZJO-BucthU8r.js} +1 -1
  113. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DEx3WwnT.js → diagram-LBJQPF4R-BgqNpAkm.js} +1 -1
  114. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Cj0dS5e8.js → diagram-Q27KOJAE-DC4q5NGa.js} +1 -1
  115. package/dist/worker/console/static/assets/{diagram-UB23O5K3-B97h8l21.js → diagram-UB23O5K3-CBqCaMeb.js} +1 -1
  116. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DMSFAhBd.js → ebnfDiagram-BXEA7PRR-CGtnbQ3-.js} +1 -1
  117. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DhVOCbt6.js → erDiagram-JOGREHBK-CG3LUao5.js} +1 -1
  118. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-GlakITWL.js → flowDiagram-UKHOOZJN-D2ZVgoFS.js} +1 -1
  119. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-VxPpu9eQ.js → ganttDiagram-PKOTCBZU-DoYFZiKt.js} +1 -1
  120. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bs_e5DRT.js → gitGraphDiagram-DS77QQ5N-CKoP1s6j.js} +1 -1
  121. package/dist/worker/console/static/assets/index-CAZ2fC_X.css +1 -0
  122. package/dist/worker/console/static/assets/index-vbTcFnFs.js +449 -0
  123. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BXYuU4Uw.js → infoDiagram-6WML65LV-Duofv8p2.js} +1 -1
  124. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C34erCbU.js → ishikawaDiagram-WSZJBQD7-D2nlCkA1.js} +1 -1
  125. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DZCAiOU3.js → journeyDiagram-NVQOT4AX-Dd4IHdus.js} +1 -1
  126. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C9gaTdUm.js → kanban-definition-27J2QSJJ-Bi20AOdt.js} +1 -1
  127. package/dist/worker/console/static/assets/{linear-CvI0Z17U.js → linear-D59YJ9kB.js} +1 -1
  128. package/dist/worker/console/static/assets/{mermaid.core-DMMSIx7j.js → mermaid.core-Bl13LpNa.js} +5 -5
  129. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BKKP0EO6.js → mindmap-definition-FAOFIHXS-C6DLP5eY.js} +1 -1
  130. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-C3LiFbl5.js → pegDiagram-VL7TDLO6-BjO4cy64.js} +1 -1
  131. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Bb3Om2Dz.js → pieDiagram-7S7Q4E2Y-D7i2qZTG.js} +1 -1
  132. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DNthMjzc.js → quadrantDiagram-CIZ2JOQS-Dx8o_h0y.js} +1 -1
  133. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-miFsr4Tv.js → railroadDiagram-AXF67PYL-B55orI_t.js} +1 -1
  134. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-yXUncmn6.js → requirementDiagram-LRYGKXZP-zWaehp6f.js} +1 -1
  135. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-DLeWKLak.js → sankeyDiagram-W5VNT64P-CcA-pjvD.js} +1 -1
  136. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CI3he1R3.js → sequenceDiagram-SI44F4Z6-BOFbzNHI.js} +1 -1
  137. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B7jfDkS5.js → sizeCapture-X5ZJPWSS-BjAejah1.js} +1 -1
  138. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BmrZod7J.js → stateDiagram-OKZ733FA-DXYwxJwZ.js} +1 -1
  139. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Clg3V9t1.js +1 -0
  140. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CDOlHVSY.js → swimlanes-SLNWSIFB-OYiOch8n.js} +2 -2
  141. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DX1dxAAW.js +8 -0
  142. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cz-XX1gy.js → timeline-definition-Z64GVDOM-cZfH3nmU.js} +1 -1
  143. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-caV4cI8C.js → vennDiagram-T6HMQDX7-BDE7b3E1.js} +1 -1
  144. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-dImPlnIO.js → wardleyDiagram-T6FBY63Y-kyyGy9WJ.js} +1 -1
  145. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CtRYVJcn.js → xychartDiagram-ELKLHX3M-CJj6VTog.js} +1 -1
  146. package/dist/worker/console/static/index.html +7 -2
  147. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  148. package/dist/worker/console/static-src/app/useOperatorActions.js +3 -2
  149. package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -5
  150. package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
  151. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +10 -0
  152. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +258 -23
  153. package/dist/worker/console/static-src/operator-chat/useChatThread.js +112 -4
  154. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -4
  155. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +29 -6
  156. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +8 -2
  157. package/dist/worker/console/static-src/shell/console-update-reload.js +52 -0
  158. package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
  159. package/dist/worker/console/workspace-context.js +142 -2
  160. package/dist/worker/console/workspace-registry.js +2 -2
  161. package/dist/worker/continuation/worker-continuation.js +278 -0
  162. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  163. package/dist/worker/observe/health.js +1 -1
  164. package/dist/worker/observe/node-transparency.js +572 -0
  165. package/dist/worker/observe/routes.js +55 -1
  166. package/dist/worker/observe/static/api.js +69 -5
  167. package/dist/worker/observe/static/dag-context-reason-labels.d.ts +9 -0
  168. package/dist/worker/observe/static/dag-context-reason-labels.js +120 -0
  169. package/dist/worker/observe/static/dag-helpers.js +4 -2
  170. package/dist/worker/observe/static/dag-node-purpose.js +5 -0
  171. package/dist/worker/observe/static/inspect-workspace.js +23 -0
  172. package/dist/worker/observe/static/kpi.js +2 -2
  173. package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
  174. package/dist/worker/observe/static/operator-chrome.js +37 -23
  175. package/dist/worker/observe/static/relations.js +2 -2
  176. package/dist/worker/observe/static/router.d.ts +12 -1
  177. package/dist/worker/observe/static/router.js +48 -6
  178. package/dist/worker/observe/static/run-processing.js +4 -2
  179. package/dist/worker/observe/static/shell-chrome.js +2 -2
  180. package/dist/worker/observe/static/state.js +8 -1
  181. package/dist/worker/observe/static/styles.css +1092 -86
  182. package/dist/worker/observe/static/views/dag-inspector.js +1228 -64
  183. package/dist/worker/observe/static/views/dag.js +10 -2
  184. package/dist/worker/observe/static/views/dags.js +3 -1
  185. package/dist/worker/observe/static/views/dashboard.js +14 -6
  186. package/dist/worker/observe/static/views/pool.js +7 -2
  187. package/dist/worker/observe/static/views/run.js +12 -3
  188. package/dist/worker/observe/static/views/session-timeline.js +142 -18
  189. package/dist/worker/observe/static/views/task.js +7 -2
  190. package/dist/worker/outcomes/adapters.js +19 -0
  191. package/dist/worker/outcomes/types.js +1 -0
  192. package/dist/worker/pool/attempt-lease.js +97 -66
  193. package/dist/worker/pool/begin-attempt-with-lease.js +1 -0
  194. package/dist/worker/pool/reconcile.js +46 -1
  195. package/dist/worker/pool/run-store.js +2 -0
  196. package/dist/worker/pool/state-projection.js +2 -0
  197. package/dist/worker/task-spec/workflow-routing.js +8 -3
  198. package/dist/workflows/dag/artifact-bindings.js +149 -0
  199. package/dist/workflows/dag/artifact-revision-schema-registry.js +31 -0
  200. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +215 -28
  201. package/dist/workflows/dag/backend-test-markdown-workflow.js +13 -1
  202. package/dist/workflows/dag/backend-test-plan-protocol.js +106 -0
  203. package/dist/workflows/dag/backend-test-scenario-param.js +172 -25
  204. package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
  205. package/dist/workflows/dag/backend-test-writer-completeness.js +104 -12
  206. package/dist/workflows/dag/budget-enforcement.js +10 -0
  207. package/dist/workflows/dag/context-receipt.js +305 -0
  208. package/dist/workflows/dag/context-transfer/context-bundle.js +423 -0
  209. package/dist/workflows/dag/context-transfer/operator-actions.js +61 -0
  210. package/dist/workflows/dag/context-transfer/renderers.js +93 -0
  211. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  212. package/dist/workflows/dag/dag-retry-schema.js +138 -0
  213. package/dist/workflows/dag/frontend-closeout.js +221 -0
  214. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  215. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  216. package/dist/workflows/dag/frontend-implementation-contract.js +1237 -192
  217. package/dist/workflows/dag/frontend-plan-render.js +2 -1
  218. package/dist/workflows/dag/frontend-prewrite-gate.js +256 -350
  219. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  220. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  221. package/dist/workflows/dag/frontend-recovery-controller.js +226 -0
  222. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  223. package/dist/workflows/dag/frontend-recovery-plan.js +21 -10
  224. package/dist/workflows/dag/frontend-recovery-run.js +166 -34
  225. package/dist/workflows/dag/frontend-repair.js +1 -432
  226. package/dist/workflows/dag/frontend-review-context.js +261 -15
  227. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  228. package/dist/workflows/dag/frontend-shadow-dual-write.js +941 -0
  229. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  230. package/dist/workflows/dag/frontend-shape-facts.js +419 -0
  231. package/dist/workflows/dag/frontend-shape.js +435 -0
  232. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  233. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  234. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  235. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  236. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  237. package/dist/workflows/dag/frontend-verification-trace.js +252 -25
  238. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  239. package/dist/workflows/dag/frontend-writer-admission.js +319 -0
  240. package/dist/workflows/dag/frontend-writer-rollback.js +32 -0
  241. package/dist/workflows/dag/frontend-writer-status.js +256 -0
  242. package/dist/workflows/dag/init-hybrid.js +1174 -562
  243. package/dist/workflows/dag/interrupt-request.js +7 -0
  244. package/dist/workflows/dag/node-execution.js +940 -31
  245. package/dist/workflows/dag/path-safety.js +1 -0
  246. package/dist/workflows/dag/prompt.js +99 -7
  247. package/dist/workflows/dag/recovery-lease.js +170 -0
  248. package/dist/workflows/dag/report.js +37 -1
  249. package/dist/workflows/dag/rerun-feedback.js +315 -1
  250. package/dist/workflows/dag/rerun-plan.js +568 -3
  251. package/dist/workflows/dag/rerun-run.js +642 -18
  252. package/dist/workflows/dag/rerun-task.js +251 -13
  253. package/dist/workflows/dag/retry-policy.js +219 -104
  254. package/dist/workflows/dag/runner.js +596 -121
  255. package/dist/workflows/dag/scheduler.js +133 -20
  256. package/dist/workflows/dag/skill-snapshot.js +17 -0
  257. package/dist/workflows/dag/types.js +426 -22
  258. package/dist/workflows/dag/validate.js +28 -12
  259. package/docs/architecture/runtime-boundaries.md +6 -6
  260. package/docs/architecture/worker-and-feature.md +1 -1
  261. package/docs/examples/README.md +5 -0
  262. package/docs/init-surface.manifest.json +30 -12
  263. package/docs/skills/vetted-skill-registry.md +4 -2
  264. package/docs/templates/README.md +2 -0
  265. package/docs/templates/agent-dag-report.schema.json +8 -2
  266. package/docs/templates/agent-dag.schema.json +33 -1
  267. package/docs/templates/backend-test-dag.json +23 -20
  268. package/docs/templates/frontend-implementation-contract.schema.json +4 -1
  269. package/docs/templates/frontend-implementation-dag.json +89 -0
  270. package/docs/templates/product-line/task.yaml +1 -1
  271. package/docs/templates/spec-registry.schema.json +45 -0
  272. package/harness.json +1 -1
  273. package/package.json +6 -3
  274. package/skills/frontend-bounded-implement/SKILL.md +15 -14
  275. package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
  276. package/skills/frontend-contract/SKILL.md +23 -0
  277. package/skills/frontend-contract/references/contract-protocol.md +34 -0
  278. package/skills/frontend-design-review/SKILL.md +22 -41
  279. package/skills/frontend-plan/SKILL.md +26 -0
  280. package/skills/frontend-plan/references/decision-contract.md +37 -0
  281. package/skills/frontend-plan/references/design-decisions.md +17 -0
  282. package/skills/frontend-review/SKILL.md +20 -15
  283. package/skills/frontend-review/references/review-findings.md +6 -7
  284. package/skills/frontend-scout/SKILL.md +25 -0
  285. package/skills/frontend-scout/references/design-evidence.md +16 -0
  286. package/skills/frontend-scout/references/scout-evidence.md +23 -0
  287. package/skills/frontend-verification/SKILL.md +1 -1
  288. package/skills/loop-agent/references/command-reference.md +7 -0
  289. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  290. package/dist/worker/console/operation-store.js +0 -169
  291. package/dist/worker/console/static/assets/channel-D1xajGpe.js +0 -1
  292. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BmuGTfRr.js +0 -1
  293. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BmuGTfRr.js +0 -1
  294. package/dist/worker/console/static/assets/index-BLzcAykx.js +0 -451
  295. package/dist/worker/console/static/assets/index-Cs9JUSPs.css +0 -1
  296. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-D9ykKEWP.js +0 -1
  297. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BvPOXALT.js +0 -8
  298. package/skills/frontend-implementation/SKILL.md +0 -52
  299. package/skills/frontend-implementation/references/code-standards.md +0 -33
  300. package/skills/frontend-implementation/references/design-spec.md +0 -56
  301. package/skills/frontend-implementation/references/node-contracts.md +0 -31
  302. /package/dist/{worker → infrastructure}/console/repo-fingerprint.js +0 -0
@@ -59,6 +59,7 @@ const CLOSED_SET = new Set([
59
59
  "null",
60
60
  "min-1",
61
61
  "min",
62
+ "max-1",
62
63
  "max",
63
64
  "max+1",
64
65
  "pattern-invalid",
@@ -155,14 +156,19 @@ function parseScenarioBoundToken(raw) {
155
156
  function parseScenarioIntentLine(line) {
156
157
  // Target may contain commas (body.title,body.status). Intent may contain `|` / spaces.
157
158
  // Stop field values only at known trailing keys or end-of-line.
158
- const precise = /场景意图\s*[::]\s*(TP-[A-Z0-9-]+)\s*[|;]\s*operation\s*=\s*([^;\n]+?)\s*[|;]\s*target\s*=\s*([^;\n]+?)\s*[|;]\s*intent\s*=\s*([^;\n]+?)(?:\s*[|;]\s*bound\s*=\s*([^\s;|\n]+))?(?:\s*[|;]\s*example\s*=\s*([^;\n]+))?(?:\s*[|;]\s*expectedCode\s*=\s*[^;\n]+)?\s*$/i.exec(line.trim());
159
+ const unfenced = line
160
+ .trim()
161
+ .replace(/^[-*+]\s+/, "")
162
+ .replace(/^`(.+)`$/, "$1")
163
+ .trim();
164
+ const precise = /场景意图\s*[::]\s*(TP-[A-Z0-9-]+)\s*[|;]\s*operation\s*=\s*([^;\n]+?)\s*[|;]\s*target\s*=\s*([^;\n]+?)\s*[|;]\s*intent\s*=\s*([^;\n]+?)(?:\s*[|;]\s*bound\s*=\s*([^\s;`|\n]+))?(?:\s*[|;]\s*example\s*=\s*([^;\n]*))?(?:\s*[|;]\s*expectedCode\s*=\s*[^;\n]+)?\s*$/i.exec(unfenced);
159
165
  if (precise) {
160
166
  return {
161
167
  tpId: precise[1],
162
168
  rawIntent: (precise[4] ?? "unknown").trim(),
163
169
  target: precise[3]?.trim(),
164
170
  bound: parseScenarioBoundToken(precise[5]),
165
- example: precise[6]?.trim(),
171
+ example: precise[6] === undefined ? undefined : precise[6].trim(),
166
172
  legacy: false,
167
173
  };
168
174
  }
@@ -192,6 +198,53 @@ function resolveScenarioIntentField(target, field) {
192
198
  target.split(/\s*,\s*/)[0];
193
199
  return preferred?.split(".").at(-1);
194
200
  }
201
+ function normalizeLengthIntentAlias(rawIntent) {
202
+ const lower = rawIntent.trim().toLowerCase();
203
+ if (lower === "max-1" || lower === "maxminus1" || lower === "max_minus_1") {
204
+ return "max-1";
205
+ }
206
+ if (lower === "min-1" || lower === "minminus1" || lower === "min_minus_1") {
207
+ return "min-1";
208
+ }
209
+ return lower;
210
+ }
211
+ function proseLengthBound(haystack, field) {
212
+ const fieldAlt = field ? field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : undefined;
213
+ const scoped = fieldAlt ? new RegExp(`${fieldAlt}[^\\n]{0,160}`, "ig") : undefined;
214
+ const windows = [];
215
+ if (scoped) {
216
+ for (const match of haystack.matchAll(scoped)) {
217
+ windows.push(match[0] ?? "");
218
+ }
219
+ }
220
+ windows.push(haystack);
221
+ let max;
222
+ let min;
223
+ const proseMaxRe = /(\d{1,4})\s*个字符的最大/i;
224
+ for (const window of windows) {
225
+ const proseMax = proseMaxRe.exec(window);
226
+ if (proseMax && max === undefined)
227
+ max = Number(proseMax[1]);
228
+ const nums = [...window.matchAll(/(?<![+\-])\d{1,4}/g)]
229
+ .map((item) => Number(item[0]))
230
+ .filter((item) => Number.isFinite(item));
231
+ for (let i = 0; i + 2 < nums.length; i += 1) {
232
+ const a = nums[i];
233
+ const b = nums[i + 1];
234
+ const c = nums[i + 2];
235
+ // Prefer n-1,n,n+1 around a documented max (100/2000), not 0,1,99 or 99,100,101's first two plus leftover.
236
+ if (b === a + 1 && c === b + 1 && b >= 10) {
237
+ max ??= b;
238
+ break;
239
+ }
240
+ }
241
+ if (nums.includes(0) && nums.includes(1))
242
+ min ??= 1;
243
+ if (max !== undefined && min !== undefined)
244
+ break;
245
+ }
246
+ return { max, min };
247
+ }
195
248
  function resolveCustomLiteralIntent(rawIntent, field) {
196
249
  const lower = rawIntent.toLowerCase();
197
250
  if (!lower.startsWith("custom-literal:")) {
@@ -250,7 +303,7 @@ export function inferScenarioParamIntent(input) {
250
303
  const field = resolveScenarioIntentField(machine.target, machine.field);
251
304
  const rawIntent = machine.rawIntent.trim();
252
305
  const example = machine.example;
253
- const lower = rawIntent.toLowerCase();
306
+ const lower = normalizeLengthIntentAlias(rawIntent);
254
307
  let normalizedIntent = lower === "nominal-operation" ? "nominal" : lower;
255
308
  // Writers sometimes mark a short "typical" length sample as min/max with bound=LEN<nominal>.
256
309
  // Treat that as nominal so a short helper/string is not forced to equal maxLength.
@@ -273,16 +326,16 @@ export function inferScenarioParamIntent(input) {
273
326
  new RegExp(`(?:minLength|min(?:imum)?|下限|最小长度)\\s*[=::]?\\s*(\\d{1,4})[^\\n]{0,80}?${fieldEsc}`, "i").exec(haystack) ??
274
327
  /(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(haystack)
275
328
  : /(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(haystack);
329
+ const proseBound = proseLengthBound(haystack, field);
276
330
  const prefersMinBound = normalizedIntent === "min" ||
277
331
  normalizedIntent === "min-1" ||
278
332
  /(?:MIN|LEN1|LENGTH-?1|最短|最小)/i.test(tpId);
279
- const boundFromBody = prefersMinBound
280
- ? minBoundFromBody ?? maxBoundFromBody
281
- : maxBoundFromBody ?? minBoundFromBody;
333
+ const boundFromBody = prefersMinBound ? minBoundFromBody : maxBoundFromBody ?? minBoundFromBody;
282
334
  const boundFromTp = /(?:LEN(?:GTH)?|长度)[_-]?(\d{1,5})(?:[_-]|$)/i.exec(tpId) ??
283
- /(?:MIN|MAX)[_-]?(\d{1,5})(?:[_-]|$)/i.exec(tpId);
335
+ /(?:MIN|MAX)[_-](\d{2,5})(?:[_-]|$)/i.exec(tpId);
284
336
  const lengthIntent = normalizedIntent === "min" ||
285
337
  normalizedIntent === "min-1" ||
338
+ normalizedIntent === "max-1" ||
286
339
  normalizedIntent === "max" ||
287
340
  normalizedIntent === "max+1";
288
341
  // Length intents: explicit machine bound >0, then TP-encoded LEN1/LEN100,
@@ -294,13 +347,35 @@ export function inferScenarioParamIntent(input) {
294
347
  : normalizedIntent === "max+1"
295
348
  ? Math.max(0, boundFromTpNumber - 1)
296
349
  : boundFromTpNumber;
297
- const bound = machine.bound && machine.bound > 0
298
- ? machine.bound
299
- : lengthIntent && boundFromTpNormalized !== undefined
300
- ? boundFromTpNormalized
301
- : boundFromBody
302
- ? Number(boundFromBody[1])
303
- : machine.bound;
350
+ const boundFromProse = prefersMinBound ? proseBound.min : proseBound.max;
351
+ const offsetIntent = normalizedIntent === "max-1" ||
352
+ normalizedIntent === "max+1" ||
353
+ normalizedIntent === "min-1";
354
+ const contractBound = boundFromBody
355
+ ? Number(boundFromBody[1])
356
+ : boundFromProse !== undefined && boundFromProse >= 0
357
+ ? boundFromProse
358
+ : undefined;
359
+ const machineBoundPositive = typeof machine.bound === "number" && Number.isFinite(machine.bound) && machine.bound > 0;
360
+ const minLengthFromBody = prefersMinBound && minBoundFromBody ? Number(minBoundFromBody[1]) : undefined;
361
+ const machineBoundZeroMin = prefersMinBound &&
362
+ machine.bound === 0 &&
363
+ minLengthFromBody === undefined &&
364
+ boundFromTpNormalized === undefined;
365
+ const bound = offsetIntent && contractBound !== undefined && contractBound > 0
366
+ ? contractBound
367
+ : machineBoundPositive && !offsetIntent
368
+ ? machine.bound
369
+ : lengthIntent && boundFromTpNormalized !== undefined
370
+ ? boundFromTpNormalized
371
+ : minLengthFromBody !== undefined && minLengthFromBody >= 0
372
+ ? minLengthFromBody
373
+ : machineBoundZeroMin
374
+ ? 0
375
+ : contractBound !== undefined &&
376
+ (prefersMinBound ? contractBound >= 0 : contractBound > 0)
377
+ ? contractBound
378
+ : undefined;
304
379
  if (rawIntent.toLowerCase().startsWith("custom-literal:")) {
305
380
  const resolved = resolveCustomLiteralIntent(rawIntent, field);
306
381
  const rawLiteral = rawIntent.slice(rawIntent.indexOf(":") + 1).trim();
@@ -387,13 +462,15 @@ export function inferScenarioParamIntent(input) {
387
462
  const boundMatch = fieldScopedBound ??
388
463
  /(?:maxLength|max(?:imum)?|上限|最大长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`) ??
389
464
  /(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`);
390
- const bound = boundMatch ? Number(boundMatch[1]) : undefined;
465
+ const proseBound = proseLengthBound(`${caseBody}
466
+ ${dataSection}`, field);
467
+ const bound = boundMatch ? Number(boundMatch[1]) : proseBound.max ?? proseBound.min;
391
468
  const exampleMatch = /(?:反例|invalid example|example)\s*[=::]\s*[`"]?([^`"\n]+)[`"]?/i.exec(caseBody);
392
469
  const example = exampleMatch?.[1]?.trim();
393
470
  if (/EMPTY|空白|空串|空字符串/.test(upper) || /空字符串|空串|empty string/i.test(caseBody)) {
394
471
  return { intent: "empty", field, bound, example, intentSource: "tp-fallback" };
395
472
  }
396
- if (/MISSING|缺省|缺失|省略/.test(upper) || /缺少字段|缺失字段|missing field/i.test(caseBody)) {
473
+ if (/MISSING|OMITTED|\bOMIT\b|缺省|缺失|省略/.test(upper) || /缺少字段|缺失字段|missing field/i.test(caseBody)) {
397
474
  return { intent: "missing", field, bound, example, intentSource: "tp-fallback" };
398
475
  }
399
476
  if (/\bNULL\b|空值/.test(upper) || /\bnull\b/i.test(caseBody)) {
@@ -402,6 +479,9 @@ export function inferScenarioParamIntent(input) {
402
479
  if (/MAX[-_]?PLUS[-_]?1|MAX\+1|超长|越界|LENGTH[-_]?OVER|OVER[-_]?LENGTH|OVER/.test(upper)) {
403
480
  return { intent: "max+1", field, bound, example, intentSource: "tp-fallback" };
404
481
  }
482
+ if (/MAX[-_]1(?:$|[-_])|max-1/i.test(upper)) {
483
+ return { intent: "max-1", field, bound, example, intentSource: "tp-fallback" };
484
+ }
405
485
  if (/\bMAX\b|最大/.test(upper)) {
406
486
  return { intent: "max", field, bound, example, intentSource: "tp-fallback" };
407
487
  }
@@ -425,7 +505,7 @@ export function inferScenarioParamIntent(input) {
425
505
  }
426
506
  return { intent: "unknown", field, bound, example, intentSource: "tp-fallback" };
427
507
  }
428
- const OMIT_SENTINEL_RE = /^(?:_OMIT|OMIT|MISSING|Ellipsis|\.\.\.)\b/;
508
+ const OMIT_SENTINEL_RE = /^(?:_OMIT|_MISSING|OMIT|MISSING|Ellipsis|\.\.\.)\b/;
429
509
  const HTTP_EXPECTED_CODE_RE = /^(?:2xx|3xx|4xx|5xx|[1-5]\d{2}|HTTP_[1-5]\d{2})$/i;
430
510
  /** Semantic custom-literal tokens that describe a value class, not a raw string. */
431
511
  function isWhitespaceSemanticLiteral(token) {
@@ -774,6 +854,10 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
774
854
  (/["'][A-Za-z_][A-Za-z0-9_]*["']\s*:/.test(dictText) ||
775
855
  /(?:payload|body|data|json)\s*=/.test(block));
776
856
  if (dictLooksLikePayload && !field) {
857
+ const emptyField = /["']([A-Za-z_][A-Za-z0-9_.]*)["']\s*:\s*(?:""|'')/.exec(dictText);
858
+ if (emptyField?.[1]) {
859
+ return { kind: "empty-string", text: '""', length: 0, literal: "" };
860
+ }
777
861
  return { kind: "dict", text: dictText.replace(/\s+/g, " ").slice(0, 160) };
778
862
  }
779
863
  if (dictLooksLikePayload && field) {
@@ -803,7 +887,7 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
803
887
  };
804
888
  }
805
889
  // Bare omit sentinel before id=...
806
- if (/pytest\.param\s*\(\s*(?:_OMIT|OMIT|MISSING|\.\.\.|Ellipsis)\s*(?:,|\)|$)/.test(block)) {
890
+ if (/pytest\.param\s*\(\s*(?:_OMIT|_MISSING|OMIT|MISSING|\.\.\.|Ellipsis)\s*(?:,|\)|$)/.test(block)) {
807
891
  return {
808
892
  kind: "missing-key",
809
893
  text: field ? `missing:${field}` : "missing-key",
@@ -865,7 +949,7 @@ export function observeParamFeatures(block, field, constants, parameterNames) {
865
949
  if (paramOpen) {
866
950
  const afterOpen = block.slice((paramOpen.index ?? 0) + paramOpen[0].length);
867
951
  const positionalTokens = [];
868
- const tokenRe = /\s*(_OMIT|OMIT|MISSING|Ellipsis|\.\.\.|None|True|False|-?\d+(?:\.\d+)?|"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|(?:["'][^"'\\]["']\s*\*\s*(?:\d{1,5}|[A-Za-z_][A-Za-z0-9_]*))|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[A-Za-z_][A-Za-z0-9_]*)\s*(?:,|\)|$)/g;
952
+ const tokenRe = /\s*(_OMIT|_MISSING|OMIT|MISSING|Ellipsis|\.\.\.|None|True|False|-?\d+(?:\.\d+)?|"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'|(?:["'][^"'\\]["']\s*\*\s*(?:\d{1,5}|[A-Za-z_][A-Za-z0-9_]*))|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[A-Za-z_][A-Za-z0-9_]*)\s*(?:,|\)|$)/g;
869
953
  let match;
870
954
  while ((match = tokenRe.exec(afterOpen)) !== null) {
871
955
  const between = afterOpen.slice(0, match.index);
@@ -1008,12 +1092,25 @@ function compareIntent(intent, observed, bound, example, field) {
1008
1092
  return "MATCH";
1009
1093
  return len !== undefined ? "MISMATCH" : "UNDETERMINED";
1010
1094
  }
1095
+ case "max-1":
1096
+ if (bound === undefined)
1097
+ return "UNDETERMINED";
1098
+ {
1099
+ const expectedMax1 = Math.max(0, bound - 1);
1100
+ const len = observedLengthValue(observed);
1101
+ if (len === expectedMax1)
1102
+ return "MATCH";
1103
+ return len !== undefined ? "MISMATCH" : "UNDETERMINED";
1104
+ }
1011
1105
  case "min":
1012
1106
  if (bound === undefined)
1013
1107
  return "UNDETERMINED";
1014
1108
  {
1015
1109
  if (isMinLengthHelper(observed))
1016
1110
  return "MATCH";
1111
+ // Optional min=0 may omit the key instead of sending "".
1112
+ if (bound === 0 && observed.kind === "missing-key")
1113
+ return "MATCH";
1017
1114
  const len = observedLengthValue(observed);
1018
1115
  if (len === bound)
1019
1116
  return "MATCH";
@@ -1116,7 +1213,11 @@ function compareIntent(intent, observed, bound, example, field) {
1116
1213
  ? "UNDETERMINED"
1117
1214
  : "MISMATCH";
1118
1215
  }
1119
- return literal === expected
1216
+ const comparableObserved = observed.kind === "boolean"
1217
+ ? (observed.literal === "True" ? "true" : observed.literal === "False" ? "false" : literal)
1218
+ : literal;
1219
+ const comparableExpected = expected === "True" ? "true" : expected === "False" ? "false" : expected;
1220
+ return comparableObserved === comparableExpected
1120
1221
  ? "MATCH"
1121
1222
  : observed.kind === "unknown"
1122
1223
  ? "UNDETERMINED"
@@ -1163,6 +1264,10 @@ function suggestedFixFor(intent, field, bound, example) {
1163
1264
  return field && bound !== undefined
1164
1265
  ? `set ${field}="x"*${bound + 1}`
1165
1266
  : undefined;
1267
+ case "max-1":
1268
+ return field && bound !== undefined
1269
+ ? `set ${field}="x"*${Math.max(0, bound - 1)}`
1270
+ : undefined;
1166
1271
  case "min":
1167
1272
  return field && bound !== undefined
1168
1273
  ? `set ${field}="x"*${bound}`
@@ -1207,6 +1312,12 @@ function boundedScenarioText(value, logicalLength) {
1207
1312
  const suffix = `<truncated length=${logicalLength ?? value.length}>`;
1208
1313
  return `${value.slice(0, Math.max(0, MAX_PERSISTED_SCENARIO_TEXT - suffix.length - 3))}...${suffix}`;
1209
1314
  }
1315
+ /** Facts schema forbids negative lengths. Internal helper sentinels stay in-memory only. */
1316
+ function persistedObservedLength(length) {
1317
+ return typeof length === "number" && Number.isFinite(length) && length >= 0
1318
+ ? Math.trunc(length)
1319
+ : undefined;
1320
+ }
1210
1321
  function localPythonFunctionRegion(source, functionName) {
1211
1322
  const escaped = functionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1212
1323
  const startMatch = new RegExp(`^def\\s+${escaped}\\s*\\(`, "m").exec(source);
@@ -1365,6 +1476,32 @@ function hasSafeSameModulePayloadHelper(source, block, parameterNames) {
1365
1476
  return false;
1366
1477
  return Boolean(safeSameModuleDictHelper(source, call.name));
1367
1478
  }
1479
+ function observeBranchPayloadField(input) {
1480
+ const region = scenarioFunctionRegion(input.source, input.tpId);
1481
+ if (!region || !input.block)
1482
+ return undefined;
1483
+ const args = extractPytestParamTopLevelArgs(input.block);
1484
+ const escapedField = input.field.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
1485
+ for (let index = 0; index < input.parameterNames.length; index += 1) {
1486
+ const parameter = input.parameterNames[index];
1487
+ const raw = args[index]?.trim();
1488
+ if (!raw || !/^['\"](?:[^'\"\\\\]|\\\\.)*['\"]$/.test(raw))
1489
+ continue;
1490
+ const branchValue = raw.slice(1, -1).replace(/\\\\([\\\"'])/g, "$1");
1491
+ const escapedParameter = parameter.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
1492
+ const escapedBranchValue = branchValue.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&");
1493
+ const branch = new RegExp(`(?:^|\\n)([ \\t]*)if\\s+${escapedParameter}\\s*==\\s*['\"]${escapedBranchValue}['\"]\\s*:\\s*([\\s\\S]*?)(?=\\n\\1(?:elif\\b|else\\s*:)|\\n\\1(?:if\\b|def\\b)|$)`, "m").exec(region)?.[2];
1494
+ if (!branch)
1495
+ continue;
1496
+ const value = new RegExp(`['\"]${escapedField}['\"]\\s*:\\s*([^,}\\n]+)`).exec(branch)?.[1];
1497
+ if (!value)
1498
+ continue;
1499
+ const observed = observeLiteralToken(value.trim(), input.field, input.constants);
1500
+ if (observed)
1501
+ return observed;
1502
+ }
1503
+ return undefined;
1504
+ }
1368
1505
  function observeDeterministicFunctionFieldOverride(input) {
1369
1506
  const region = scenarioFunctionRegion(input.source, input.tpId);
1370
1507
  if (!region)
@@ -1449,11 +1586,12 @@ function scenarioParamDiagnosticValues(intent, observed) {
1449
1586
  const expectedRaw = intent.startsWith("custom-literal:")
1450
1587
  ? intent.slice("custom-literal:".length)
1451
1588
  : intent === "unknown" ? undefined : intent;
1589
+ const persistedLength = persistedObservedLength(observed.length);
1452
1590
  return {
1453
1591
  expectedRaw,
1454
- observedRaw: boundedScenarioText(observed.text, observed.length),
1592
+ observedRaw: boundedScenarioText(observed.text, persistedLength),
1455
1593
  expectedNormalized: expectedRaw === undefined ? undefined : normalizeCustomLiteralExpected(expectedRaw).slice(0, 512),
1456
- observedNormalized: observed.literal === undefined ? undefined : boundedScenarioText(normalizeScenarioLiteralText(observed.literal), observed.length),
1594
+ observedNormalized: observed.literal === undefined ? undefined : boundedScenarioText(normalizeScenarioLiteralText(observed.literal), persistedLength),
1457
1595
  };
1458
1596
  }
1459
1597
  export async function assessBackendScenarioParamConsistency(input) {
@@ -1496,7 +1634,10 @@ export async function assessBackendScenarioParamConsistency(input) {
1496
1634
  constants,
1497
1635
  })
1498
1636
  : undefined;
1499
- const observed = functionFieldOverride ?? helperFieldObserved ?? observeParamFeatures(block, inferred.field, constants, parameterNames);
1637
+ const branchFieldObserved = source && inferred.field
1638
+ ? observeBranchPayloadField({ source, tpId, block, parameterNames, field: inferred.field, constants })
1639
+ : undefined;
1640
+ const observed = functionFieldOverride ?? helperFieldObserved ?? branchFieldObserved ?? observeParamFeatures(block, inferred.field, constants, parameterNames);
1500
1641
  // Request-level / health checks often have no pytest.param payload row,
1501
1642
  // or only a TP-id label positional (no field value).
1502
1643
  const requestLevelNoParam = Boolean(source) &&
@@ -1539,8 +1680,8 @@ export async function assessBackendScenarioParamConsistency(input) {
1539
1680
  tpId,
1540
1681
  field: inferred.field,
1541
1682
  intent: inferred.intent,
1542
- observed: boundedScenarioText(observed.text, observed.length),
1543
- observedLength: observed.length,
1683
+ observed: boundedScenarioText(observed.text, persistedObservedLength(observed.length)),
1684
+ observedLength: persistedObservedLength(observed.length),
1544
1685
  status,
1545
1686
  repairability,
1546
1687
  reasonCode: status === "MATCH" ? "SCENARIO_PARAM_MATCH" : status === "MISMATCH" ? "SCENARIO_PARAM_MISMATCH" : "SCENARIO_PARAM_UNDETERMINED",
@@ -1709,6 +1850,12 @@ export function deterministicRewriteScenarioParam(input) {
1709
1850
  }
1710
1851
  valueLiteral = `"x" * ${bound + 1}`;
1711
1852
  break;
1853
+ case "max-1":
1854
+ if (bound === undefined) {
1855
+ return { ok: false, source: input.source, detail: "missing bound for max-1" };
1856
+ }
1857
+ valueLiteral = `"x" * ${Math.max(0, bound - 1)}`;
1858
+ break;
1712
1859
  case "min-1":
1713
1860
  if (bound === undefined) {
1714
1861
  return { ok: false, source: input.source, detail: "missing bound for min-1" };
@@ -43,6 +43,67 @@ function splitCells(raw) {
43
43
  .map((cell) => cell.trim())
44
44
  .filter((cell, index, cells) => !(cell === "" && (index === 0 || index === cells.length - 1)));
45
45
  }
46
+ function isExactSlotInventory(extras, partitionId) {
47
+ const tokens = extras
48
+ .join(";")
49
+ .split(/[;,,;]/)
50
+ .map((token) => token.trim().replace(/`/g, ""))
51
+ .filter((token) => token.length > 0);
52
+ if (tokens.length === 0)
53
+ return false;
54
+ const prefix = `TP-${partitionId.trim().toUpperCase()}-`;
55
+ return tokens.every((token) => /^TP-[A-Z0-9-]+$/i.test(token) &&
56
+ (token.toUpperCase().startsWith(prefix) || token.toUpperCase().startsWith("TP-SP-")));
57
+ }
58
+ function trimExactSlotInventoryCells(cells) {
59
+ if (cells.length <= 7)
60
+ return { cells, trimmed: false };
61
+ const partitionId = cells[0] ?? "";
62
+ if (!isExactSlotInventory(cells.slice(7), partitionId)) {
63
+ return { cells, trimmed: false };
64
+ }
65
+ return { cells: cells.slice(0, 7), trimmed: true };
66
+ }
67
+ export function canonicalizeScenarioPartitionTable(markdown) {
68
+ const lines = markdown.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
69
+ const headingIndex = lines.findIndex((line) => PARTITION_HEADING.test(line.trim()));
70
+ if (headingIndex === -1) {
71
+ return { markdown: lines.join("\n"), changed: false, trimmedRows: 0 };
72
+ }
73
+ let headerIndex = -1;
74
+ let sectionEnd = lines.length;
75
+ for (let i = headingIndex + 1; i < lines.length; i++) {
76
+ if (/^##\s+/.test(lines[i].trim()) && !PARTITION_HEADING.test(lines[i].trim())) {
77
+ sectionEnd = i;
78
+ break;
79
+ }
80
+ if (PARTITION_TABLE_HEADER.test(lines[i])) {
81
+ headerIndex = i;
82
+ }
83
+ }
84
+ if (headerIndex === -1) {
85
+ return { markdown: lines.join("\n"), changed: false, trimmedRows: 0 };
86
+ }
87
+ let trimmedRows = 0;
88
+ for (let i = headerIndex + 2; i < sectionEnd; i++) {
89
+ const line = lines[i];
90
+ if (!TABLE_ROW.test(line)) {
91
+ if (/^##\s+/.test(line) || /^#\s+/.test(line))
92
+ break;
93
+ continue;
94
+ }
95
+ const { cells, trimmed } = trimExactSlotInventoryCells(splitCells(line));
96
+ if (!trimmed)
97
+ continue;
98
+ lines[i] = `| ${cells.join(" | ")} |`;
99
+ trimmedRows += 1;
100
+ }
101
+ return {
102
+ markdown: lines.join("\n"),
103
+ changed: trimmedRows > 0,
104
+ trimmedRows,
105
+ };
106
+ }
46
107
  /**
47
108
  * Parse the `## Scenario Partitions` section of the README into declared
48
109
  * partitions. Returns { partitions, issues }; issues are fail-closed parse
@@ -77,7 +138,7 @@ export function parseScenarioPartitions(readmeMarkdown) {
77
138
  break;
78
139
  continue;
79
140
  }
80
- const cells = splitCells(line);
141
+ const { cells } = trimExactSlotInventoryCells(splitCells(line));
81
142
  if (cells.length !== 7) {
82
143
  issues.push(`MISSING_PARTITION: row has ${cells.length} cells, expected 7: ${line.trim()}`);
83
144
  continue;
@@ -4,7 +4,8 @@ import path from "node:path";
4
4
  import { z } from "zod";
5
5
  import { BACKEND_TEST_MAX_MODULE_COUNT, isOpaqueHashBackendTestModuleStem, isPriorityOnlyBackendTestModuleStem, } from "./backend-test-module-stem.js";
6
6
  import { resolveBackendTestLayout, } from "./backend-test-layout.js";
7
- import { inspectBackendTestMarkdownTestPointBindings } from "./backend-test-case-coverage-analysis.js";
7
+ import { canonicalizeBackendTestMarkdownTestPointBindings, inspectBackendTestMarkdownTestPointBindings, } from "./backend-test-case-coverage-analysis.js";
8
+ import { BACKEND_TEST_SCENARIO_PARTITION_SLOTS_SCHEMA_ID, canonicalizeBackendTestPlanProtocolHeadings, collectMarkdownPartitionSlotCoverage, lookupRequiredVariantSlotsForMarkdownPath, } from "./backend-test-plan-protocol.js";
8
9
  /**
9
10
  * Maps a backend-test generation writer task id to its writer-progress role.
10
11
  * Returns undefined for nodes that are not completeness-gated generators.
@@ -47,8 +48,16 @@ const progressSchema = z
47
48
  path: z.string().optional(),
48
49
  detail: z.string(),
49
50
  recoverable: z.boolean(),
51
+ failureSubreason: z.literal("markdown-test-point-binding-format").optional(),
52
+ affectedCases: z.array(z.string()).optional(),
53
+ testPointIds: z.array(z.string()).optional(),
54
+ canonicalizationEligible: z.boolean().optional(),
55
+ canonicalizationOutcome: z.enum(["not-attempted", "changed", "already-canonical", "unsafe", "revalidation-failed"]).optional(),
50
56
  })
51
57
  .strict()),
58
+ canonicalizationOutcome: z.enum(["not-attempted", "changed", "already-canonical", "unsafe", "revalidation-failed"]).optional(),
59
+ canonicalizedTestPointCount: z.number().int().min(0).optional(),
60
+ lightweightRepairEligible: z.boolean().optional(),
52
61
  })
53
62
  .strict();
54
63
  async function exists(filePath) {
@@ -119,10 +128,8 @@ function looksLikeValidModuleStem(raw) {
119
128
  return moduleStemInvalidReason(raw) === undefined;
120
129
  }
121
130
  function extractCanonicalModuleIndexSection(readme) {
122
- const lines = readme
123
- .replaceAll("\r\n", "\n")
124
- .replaceAll("\r", "\n")
125
- .split("\n");
131
+ const canonical = canonicalizeBackendTestPlanProtocolHeadings(readme).markdown;
132
+ const lines = canonical.split("\n");
126
133
  const headings = lines.flatMap((line, index) => line.trim() === "## Module Index" ? [index] : []);
127
134
  if (headings.length === 0) {
128
135
  return { section: "", structureIssues: ["missing-module-index"] };
@@ -362,8 +369,6 @@ function pythonParseable(source) {
362
369
  const normalized = source.replace(/\r\n/g, "\n");
363
370
  if (!normalized.trim())
364
371
  return false;
365
- if (/^\s*(?:def|class|import|from)\b/m.test(normalized) === false)
366
- return false;
367
372
  const structural = pythonStructureOutsideStrings(normalized);
368
373
  if (!structural.stringsClosed)
369
374
  return false;
@@ -380,7 +385,13 @@ function pythonParseable(source) {
380
385
  return false;
381
386
  if (openBraces !== closeBraces)
382
387
  return false;
383
- return true;
388
+ // Business modules still need a statement. Evidence-only mapped scripts may
389
+ // be a closed module docstring + Case-ID with no `def test_` (R10 health).
390
+ if (/^\s*(?:def|class|import|from|__all__\s*=)/m.test(normalized))
391
+ return true;
392
+ return (/^\s*(?:'''|""")/.test(normalized) &&
393
+ /\bCase-ID\s*:/i.test(normalized) &&
394
+ !/^\s*(?:async\s+)?def\s+test_/m.test(normalized));
384
395
  }
385
396
  /**
386
397
  * Forbidden generated shared helper/factory namespaces for the self-contained
@@ -511,6 +522,19 @@ export async function loadBackendTestWriterProgressForRetry(runDir, taskId) {
511
522
  * child's own target file(s): md children are checked for structural
512
523
  * completeness, pytest children for parseability. Sibling modules are ignored.
513
524
  */
525
+ async function loadScenarioPartitionSlotInventory(runDir) {
526
+ if (!runDir)
527
+ return undefined;
528
+ try {
529
+ const raw = JSON.parse(await readFile(path.join(runDir, "contracts", "backend-test-scenario-partition-slots.json"), "utf8"));
530
+ if (raw?.schemaId !== BACKEND_TEST_SCENARIO_PARTITION_SLOTS_SCHEMA_ID)
531
+ return undefined;
532
+ return raw;
533
+ }
534
+ catch {
535
+ return undefined;
536
+ }
537
+ }
514
538
  export async function assessBackendTestShardChildCompleteness(input) {
515
539
  const role = input.role;
516
540
  const targetPaths = input.writeSet
@@ -520,6 +544,12 @@ export async function assessBackendTestShardChildCompleteness(input) {
520
544
  const actualPaths = [];
521
545
  const missingPaths = [];
522
546
  const brokenPaths = [];
547
+ let canonicalizationOutcome = role === "md-generate" ? "already-canonical" : undefined;
548
+ let canonicalizedTestPointCount = 0;
549
+ let lightweightRepairEligible = false;
550
+ const slotInventory = role === "md-generate"
551
+ ? await loadScenarioPartitionSlotInventory(input.runDir)
552
+ : undefined;
523
553
  for (const target of targetPaths) {
524
554
  // Rendered map-child writeSet entries are concrete (e.g.
525
555
  // testcase/md/health.md / testcase/test_orders.py); glob-only entries are
@@ -538,16 +568,65 @@ export async function assessBackendTestShardChildCompleteness(input) {
538
568
  continue;
539
569
  }
540
570
  actualPaths.push(target);
541
- const body = await readFile(absolute, "utf8");
571
+ let body = await readFile(absolute, "utf8");
542
572
  if (role === "md-generate") {
573
+ const beforeBindings = inspectBackendTestMarkdownTestPointBindings(body);
574
+ const canonicalization = canonicalizeBackendTestMarkdownTestPointBindings(body);
575
+ if (canonicalization.changed) {
576
+ await writeFile(absolute, canonicalization.markdown, "utf8");
577
+ body = canonicalization.markdown;
578
+ canonicalizationOutcome = "changed";
579
+ canonicalizedTestPointCount += canonicalization.canonicalizedTestPoints.length;
580
+ }
581
+ const afterBindings = inspectBackendTestMarkdownTestPointBindings(body);
582
+ const unresolved = afterBindings.flatMap((entry) => entry.unclassifiedTestPoints);
583
+ const bindingCases = afterBindings
584
+ .filter((entry) => entry.unclassifiedTestPoints.length > 0 || entry.duplicateBindingTestPoints.length > 0)
585
+ .map((entry) => entry.caseId);
543
586
  const structural = moduleStructurallyComplete(body);
587
+ const requiredSlots = lookupRequiredVariantSlotsForMarkdownPath(slotInventory, target);
588
+ const slotCoverage = requiredSlots.length > 0
589
+ ? collectMarkdownPartitionSlotCoverage(body, requiredSlots)
590
+ : undefined;
591
+ const missingSlots = orderedUnique([
592
+ ...(slotCoverage?.missingFromDeclared ?? []),
593
+ ...(slotCoverage?.missingFromVariant ?? []),
594
+ ]);
595
+ if (missingSlots.length > 0) {
596
+ lightweightRepairEligible = true;
597
+ if (!brokenPaths.includes(target))
598
+ brokenPaths.push(target);
599
+ issues.push({
600
+ code: "T5",
601
+ path: target,
602
+ detail: `incomplete-partition-slot-set: missing ${missingSlots.join(", ")}; Scheme A requires each exact slot as an independent variant Test Point, not Case count or SINGLE/MULTIPLE aliases`,
603
+ recoverable: true,
604
+ affectedCases: afterBindings.map((entry) => entry.caseId),
605
+ testPointIds: missingSlots,
606
+ });
607
+ }
544
608
  if (!structural.ok) {
609
+ lightweightRepairEligible = bindingCases.length > 0;
610
+ if (bindingCases.length > 0)
611
+ canonicalizationOutcome = canonicalization.changed ? "revalidation-failed" : "unsafe";
545
612
  brokenPaths.push(target);
613
+ const bindingFormatIssue = bindingCases.length > 0;
546
614
  issues.push({
547
615
  code: "T5",
548
616
  path: target,
549
617
  detail: moduleStructuralDetail(structural),
550
618
  recoverable: true,
619
+ ...(bindingFormatIssue
620
+ ? {
621
+ failureSubreason: "markdown-test-point-binding-format",
622
+ affectedCases: orderedUnique(bindingCases),
623
+ testPointIds: orderedUnique(unresolved),
624
+ canonicalizationEligible: canonicalization.canonicalizedTestPoints.length > 0,
625
+ canonicalizationOutcome: canonicalization.changed
626
+ ? (unresolved.length === 0 ? "changed" : "revalidation-failed")
627
+ : (beforeBindings.some((entry) => entry.unclassifiedTestPoints.length > 0) ? "unsafe" : "already-canonical"),
628
+ }
629
+ : {}),
551
630
  });
552
631
  }
553
632
  }
@@ -586,6 +665,9 @@ export async function assessBackendTestShardChildCompleteness(input) {
586
665
  brokenPaths: orderedUnique(brokenPaths),
587
666
  targetPaths: recoverableTargets,
588
667
  issues,
668
+ ...(role === "md-generate"
669
+ ? { canonicalizationOutcome, canonicalizedTestPointCount, lightweightRepairEligible }
670
+ : {}),
589
671
  };
590
672
  }
591
673
  export async function assessBackendTestMdWriterCompleteness(workspaceRoot, layout) {
@@ -609,7 +691,7 @@ export async function assessBackendTestMdWriterCompleteness(workspaceRoot, layou
609
691
  }
610
692
  else {
611
693
  actualPaths.push(readmeRel);
612
- readme = await readFile(readmePath, "utf8");
694
+ readme = canonicalizeBackendTestPlanProtocolHeadings(await readFile(readmePath, "utf8")).markdown;
613
695
  if (!readme.includes("## Coverage Scope")) {
614
696
  brokenPaths.push(readmeRel);
615
697
  issues.push({
@@ -853,7 +935,7 @@ export async function writeBackendTestWriterProgressArtifacts(input) {
853
935
  "## Issues",
854
936
  "",
855
937
  ...(input.progress.issues.length
856
- ? input.progress.issues.map((issue) => `- [${issue.code}] ${issue.path ?? "(workspace)"}: ${issue.detail}`)
938
+ ? input.progress.issues.map((issue) => `- [${issue.code}] ${issue.path ?? "(workspace)"}: ${issue.detail}${issue.failureSubreason ? `; failureSubreason=${issue.failureSubreason}; canonicalizationOutcome=${issue.canonicalizationOutcome ?? "not-attempted"}; canonicalizationEligible=${issue.canonicalizationEligible ?? false}; affectedCases=${(issue.affectedCases ?? []).join(",") || "none"}; testPointIds=${(issue.testPointIds ?? []).join(",") || "none"}` : ""}`)
857
939
  : ["- None"]),
858
940
  "",
859
941
  `<!-- sha256:${createHash("sha256").update(JSON.stringify(parsed)).digest("hex")} -->`,
@@ -862,6 +944,16 @@ export async function writeBackendTestWriterProgressArtifacts(input) {
862
944
  await writeFile(reportPath, body, "utf8");
863
945
  return { factsPath, reportPath };
864
946
  }
947
+ export function isBackendTestPytestCollectionRepairOutcomeRecoveryCandidate(task) {
948
+ return (task.id === "repair-backend-pytest-collection-pi" &&
949
+ task.executor === "pi" &&
950
+ task.role === "implementer" &&
951
+ task.toolProfile === "write" &&
952
+ task.writePolicy === "exclusive" &&
953
+ (task.writeSet?.length ?? 0) > 0 &&
954
+ task.writerOutcomePolicy?.type === "implementation-outcome-v1" &&
955
+ task.writerOutcomePolicy.requireChangedFiles === true);
956
+ }
865
957
  export function isBackendTestCompletenessRetryCandidate(task) {
866
958
  if (task.executor !== "pi" || task.role !== "implementer")
867
959
  return false;
@@ -933,7 +1025,7 @@ export async function assessBackendTestMdPlanCompleteness(workspaceRoot, layout)
933
1025
  }
934
1026
  else {
935
1027
  actualPaths.push(planReadmeRel);
936
- const readme = await readFile(readmePath, "utf8");
1028
+ const readme = canonicalizeBackendTestPlanProtocolHeadings(await readFile(readmePath, "utf8")).markdown;
937
1029
  if (!readme.includes("## Coverage Scope")) {
938
1030
  brokenPaths.push(planReadmeRel);
939
1031
  issues.push({