create-harness-vibe-coding 0.8.19 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (656) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README-CN.md +4 -2
  3. package/README.md +23 -4
  4. package/package.json +17 -2
  5. package/src/generator.js +19 -6
  6. package/src/index.js +531 -38
  7. package/src/prompts.js +4 -4
  8. package/src/ui/dist/assets/AgentsRoute-BGGs90wH.js +7 -0
  9. package/src/ui/dist/assets/Assistant-Bold-gm-uSS1B.woff2 +0 -0
  10. package/src/ui/dist/assets/Assistant-Medium-DrcxCXg3.woff2 +0 -0
  11. package/src/ui/dist/assets/Assistant-Regular-DVxZuzxb.woff2 +0 -0
  12. package/src/ui/dist/assets/Assistant-SemiBold-SCI4bEL9.woff2 +0 -0
  13. package/src/ui/dist/assets/Combination-DhBLEmof.js +42 -0
  14. package/src/ui/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  15. package/src/ui/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  16. package/src/ui/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  17. package/src/ui/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  18. package/src/ui/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  19. package/src/ui/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  20. package/src/ui/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  21. package/src/ui/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  22. package/src/ui/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  23. package/src/ui/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  24. package/src/ui/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  25. package/src/ui/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  26. package/src/ui/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  27. package/src/ui/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  28. package/src/ui/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  29. package/src/ui/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  30. package/src/ui/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  31. package/src/ui/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  32. package/src/ui/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  33. package/src/ui/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  34. package/src/ui/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  35. package/src/ui/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  36. package/src/ui/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  37. package/src/ui/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  38. package/src/ui/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  39. package/src/ui/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  40. package/src/ui/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  41. package/src/ui/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  42. package/src/ui/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  43. package/src/ui/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  44. package/src/ui/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  45. package/src/ui/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  46. package/src/ui/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  47. package/src/ui/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  48. package/src/ui/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  49. package/src/ui/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  50. package/src/ui/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  51. package/src/ui/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  52. package/src/ui/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  53. package/src/ui/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  54. package/src/ui/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  55. package/src/ui/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  56. package/src/ui/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  57. package/src/ui/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  58. package/src/ui/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  59. package/src/ui/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  60. package/src/ui/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  61. package/src/ui/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  62. package/src/ui/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  63. package/src/ui/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  64. package/src/ui/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  65. package/src/ui/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  66. package/src/ui/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  67. package/src/ui/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  68. package/src/ui/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  69. package/src/ui/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  70. package/src/ui/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  71. package/src/ui/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  72. package/src/ui/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  73. package/src/ui/dist/assets/RolesRoute-DnodCSz0.js +2 -0
  74. package/src/ui/dist/assets/RuntimePicker-CucD8TGn.js +7 -0
  75. package/src/ui/dist/assets/SettingsRoute-DquMWjac.js +2 -0
  76. package/src/ui/dist/assets/TaskList-Bxc0Uktx.js +7 -0
  77. package/src/ui/dist/assets/TerminalDrawer-CDusYltc.js +36 -0
  78. package/src/ui/dist/assets/WorkflowRoute-BN8yHmUs.css +1 -0
  79. package/src/ui/dist/assets/WorkflowRoute-D-0wVDpY.js +334 -0
  80. package/src/ui/dist/assets/abnfDiagram-VRR7QNED-zkRKkaxO.js +2 -0
  81. package/src/ui/dist/assets/apl-B4CMkyY2.js +2 -0
  82. package/src/ui/dist/assets/ar-SA-G6X2FPQ2-BiHaArx5.js +11 -0
  83. package/src/ui/dist/assets/arc-CBnLtOFO.js +2 -0
  84. package/src/ui/dist/assets/architectureDiagram-ZJ3FMSHR-DWXRXsh1.js +37 -0
  85. package/src/ui/dist/assets/asciiarmor-Df11BRmG.js +2 -0
  86. package/src/ui/dist/assets/asn1-EdZsLKOL.js +2 -0
  87. package/src/ui/dist/assets/asterisk-B-8jnY81.js +2 -0
  88. package/src/ui/dist/assets/az-AZ-76LH7QW2-CiT05voO.js +2 -0
  89. package/src/ui/dist/assets/bg-BG-XCXSNQG7-L-UM9tNf.js +6 -0
  90. package/src/ui/dist/assets/blockDiagram-677ZJIJ3-BIyh3Bry.js +133 -0
  91. package/src/ui/dist/assets/bn-BD-2XOGV67Q-NdgbA0bC.js +6 -0
  92. package/src/ui/dist/assets/brainfuck-C4LP7Hcl.js +2 -0
  93. package/src/ui/dist/assets/c4Diagram-LMCZKHZV-CA54vWWM.js +11 -0
  94. package/src/ui/dist/assets/ca-ES-6MX7JW3Y-BKZZsQ5J.js +9 -0
  95. package/src/ui/dist/assets/channel-Da_-zcAu.js +2 -0
  96. package/src/ui/dist/assets/chunk-2Q5K7J3B-DE4cxr1v.js +2 -0
  97. package/src/ui/dist/assets/chunk-32BRIVSS-i7FwbEU9.js +2 -0
  98. package/src/ui/dist/assets/chunk-5VM5RSS4-BhIcOYSu.js +16 -0
  99. package/src/ui/dist/assets/chunk-EX3LRPZG-Ci9zZRIs.js +232 -0
  100. package/src/ui/dist/assets/chunk-JWPE2WC7-BFdkcz-G.js +2 -0
  101. package/src/ui/dist/assets/chunk-MOJQB5TN-Dzmapl_S.js +89 -0
  102. package/src/ui/dist/assets/chunk-RYQCIY6F-RTmp7FOy.js +2 -0
  103. package/src/ui/dist/assets/chunk-V7JOEXUC-DInU3v3D.js +207 -0
  104. package/src/ui/dist/assets/chunk-VR4S4FIN-BHoyGBQO.js +2 -0
  105. package/src/ui/dist/assets/chunk-XXDRQBXY-DfeLZNZx.js +2 -0
  106. package/src/ui/dist/assets/classDiagram-OUVF2IWQ-FKbUS0bu.js +2 -0
  107. package/src/ui/dist/assets/classDiagram-v2-EOCWNBFH-FKbUS0bu.js +2 -0
  108. package/src/ui/dist/assets/clike-B9uivgTg.js +2 -0
  109. package/src/ui/dist/assets/clojure-BMjYHr_A.js +2 -0
  110. package/src/ui/dist/assets/cmake-BQqOBYOt.js +2 -0
  111. package/src/ui/dist/assets/cobol-CWcv1MsR.js +2 -0
  112. package/src/ui/dist/assets/coffeescript-S37ZYGWr.js +2 -0
  113. package/src/ui/dist/assets/commonlisp-DBKNyK5s.js +2 -0
  114. package/src/ui/dist/assets/cose-bilkent-JH36ORCC-_5PUJ615.js +2 -0
  115. package/src/ui/dist/assets/crystal-SjHAIU92.js +2 -0
  116. package/src/ui/dist/assets/cs-CZ-2BRQDIVT-DnkfnhDU.js +12 -0
  117. package/src/ui/dist/assets/css-BnMrqG3P.js +2 -0
  118. package/src/ui/dist/assets/cynefin-VYW2F7L2-CmkcFv-b.js +179 -0
  119. package/src/ui/dist/assets/cynefinDiagram-TSTJHNR4-D72VZn8H.js +63 -0
  120. package/src/ui/dist/assets/cypher-C_CwsFkJ.js +2 -0
  121. package/src/ui/dist/assets/cytoscape.esm-DTSO7Bv0.js +332 -0
  122. package/src/ui/dist/assets/d-pRatUO7H.js +2 -0
  123. package/src/ui/dist/assets/da-DK-5WZEPLOC-C54tQUgd.js +6 -0
  124. package/src/ui/dist/assets/dagre-VKFMJZFB-0BsAkrmo.js +5 -0
  125. package/src/ui/dist/assets/de-DE-XR44H4JA-DGfZlR10.js +9 -0
  126. package/src/ui/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
  127. package/src/ui/dist/assets/diagram-FQU43EPY-C1ZJDOca.js +4 -0
  128. package/src/ui/dist/assets/diagram-G47NLZAW-D3pr3FSP.js +25 -0
  129. package/src/ui/dist/assets/diagram-NH7WQ7WH-Bwry0NV-.js +25 -0
  130. package/src/ui/dist/assets/diagram-OA4YK3LP-L6g09cmV.js +31 -0
  131. package/src/ui/dist/assets/diagram-WEI45ONY-Bh1puYVX.js +42 -0
  132. package/src/ui/dist/assets/diff-DbItnlRl.js +2 -0
  133. package/src/ui/dist/assets/directory-open-01563666-DWU9wJ6I.js +2 -0
  134. package/src/ui/dist/assets/directory-open-4ed118d0-CunoC1EB.js +2 -0
  135. package/src/ui/dist/assets/dockerfile-BKs6k2Af.js +2 -0
  136. package/src/ui/dist/assets/dtd-DF_7sFjM.js +2 -0
  137. package/src/ui/dist/assets/dylan-DwRh75JA.js +2 -0
  138. package/src/ui/dist/assets/ebnf-CDyGwa7X.js +2 -0
  139. package/src/ui/dist/assets/ebnfDiagram-CCIWWBDH-BC_mBRR5.js +2 -0
  140. package/src/ui/dist/assets/ecl-Cabwm37j.js +2 -0
  141. package/src/ui/dist/assets/eiffel-CnydiIhH.js +2 -0
  142. package/src/ui/dist/assets/el-GR-BZB4AONW-DsnwWweL.js +11 -0
  143. package/src/ui/dist/assets/elm-vLlmbW-K.js +2 -0
  144. package/src/ui/dist/assets/erDiagram-Q63AITRT-Bkxam36l.js +86 -0
  145. package/src/ui/dist/assets/erlang-BNw1qcRV.js +2 -0
  146. package/src/ui/dist/assets/es-ES-U4NZUMDT-CRxBcQgC.js +10 -0
  147. package/src/ui/dist/assets/eu-ES-A7QVB2H4-CTd_E7YR.js +12 -0
  148. package/src/ui/dist/assets/excalidraw-default-bMWIgNCy.svg +1 -0
  149. package/src/ui/dist/assets/fa-IR-HGAKTJCU-DO56k7eG.js +9 -0
  150. package/src/ui/dist/assets/factor-kuTfRLto.js +2 -0
  151. package/src/ui/dist/assets/fcl-Kvtd6kyn.js +2 -0
  152. package/src/ui/dist/assets/fi-FI-Z5N7JZ37-DEDCF9WB.js +7 -0
  153. package/src/ui/dist/assets/file-open-002ab408-DIuFHtCF.js +2 -0
  154. package/src/ui/dist/assets/file-open-7c801643-684qeFg4.js +2 -0
  155. package/src/ui/dist/assets/file-save-3189631c-C1wFhQhH.js +2 -0
  156. package/src/ui/dist/assets/file-save-745eba88-Bb9F9Kg7.js +2 -0
  157. package/src/ui/dist/assets/flowDiagram-23GEKE2U-C3SAEao7.js +157 -0
  158. package/src/ui/dist/assets/forth-Ffai-XNe.js +2 -0
  159. package/src/ui/dist/assets/fortran-DYz_wnZ1.js +2 -0
  160. package/src/ui/dist/assets/fr-FR-RHASNOE6-DE2nPyDt.js +10 -0
  161. package/src/ui/dist/assets/ganttDiagram-NO4QXBWP-U-J_Xi-_.js +293 -0
  162. package/src/ui/dist/assets/gas-Bneqetm1.js +2 -0
  163. package/src/ui/dist/assets/gherkin-heZmZLOM.js +2 -0
  164. package/src/ui/dist/assets/gitGraphDiagram-IHSO6WYX-BADVbD-a.js +107 -0
  165. package/src/ui/dist/assets/gl-ES-HMX3MZ6V-mCqVgQHC.js +11 -0
  166. package/src/ui/dist/assets/graph-C9eacEi8.js +2 -0
  167. package/src/ui/dist/assets/groovy-D9Dt4D0W.js +2 -0
  168. package/src/ui/dist/assets/haskell-Cw1EW3IL.js +2 -0
  169. package/src/ui/dist/assets/haxe-H-WmDvRZ.js +2 -0
  170. package/src/ui/dist/assets/he-IL-6SHJWFNN-KnaLrCzw.js +11 -0
  171. package/src/ui/dist/assets/hi-IN-IWLTKZ5I-wuqiaaWt.js +5 -0
  172. package/src/ui/dist/assets/http-DBlCnlav.js +2 -0
  173. package/src/ui/dist/assets/hu-HU-A5ZG7DT2-DLg_zm22.js +8 -0
  174. package/src/ui/dist/assets/id-ID-SAP4L64H-MB2mXqcR.js +11 -0
  175. package/src/ui/dist/assets/idl-BEugSyMb.js +2 -0
  176. package/src/ui/dist/assets/image-blob-reduce.esm-HwCm7IZ0.js +8 -0
  177. package/src/ui/dist/assets/index-39SJtrt0.js +2 -0
  178. package/src/ui/dist/assets/index-BQ0LIrW-.js +2 -0
  179. package/src/ui/dist/assets/index-BcAUh2Nm.css +1 -0
  180. package/src/ui/dist/assets/index-BcThOK-i.js +331 -0
  181. package/src/ui/dist/assets/index-BnxV3k_S.js +3 -0
  182. package/src/ui/dist/assets/index-C7MZBHbT.js +117 -0
  183. package/src/ui/dist/assets/index-CCvKI2rM.js +2 -0
  184. package/src/ui/dist/assets/index-CZvmcCab.js +8 -0
  185. package/src/ui/dist/assets/index-Cp3hLKNl.js +2 -0
  186. package/src/ui/dist/assets/index-D7ayFxcx.js +2 -0
  187. package/src/ui/dist/assets/index-DAYxSQGU.js +2 -0
  188. package/src/ui/dist/assets/index-DD-HeMV0.css +1 -0
  189. package/src/ui/dist/assets/index-DPMxd3Qn.js +2 -0
  190. package/src/ui/dist/assets/index-DSe2jx3k.js +4 -0
  191. package/src/ui/dist/assets/index-DUirCkgW.js +2 -0
  192. package/src/ui/dist/assets/index-DaKPQ2hN.js +159 -0
  193. package/src/ui/dist/assets/index-DzpuR0Jg.js +2 -0
  194. package/src/ui/dist/assets/index-POndwsCI.js +2 -0
  195. package/src/ui/dist/assets/index-R8NM9I_9.js +2 -0
  196. package/src/ui/dist/assets/index-TCDuDhBj.js +2 -0
  197. package/src/ui/dist/assets/index-TckzASdM.js +2 -0
  198. package/src/ui/dist/assets/index-Yi5ytFFr.js +2 -0
  199. package/src/ui/dist/assets/infoDiagram-FWYZ7A6U-CiGBlFqH.js +3 -0
  200. package/src/ui/dist/assets/init-Gi6I4Gst.js +2 -0
  201. package/src/ui/dist/assets/ishikawaDiagram-FXEZZL3T-B1aheRbz.js +71 -0
  202. package/src/ui/dist/assets/it-IT-JPQ66NNP-DT3HSU7X.js +12 -0
  203. package/src/ui/dist/assets/ja-JP-DBVTYXUO-CsfJjjWt.js +9 -0
  204. package/src/ui/dist/assets/javascript-iXu5QeM3.js +2 -0
  205. package/src/ui/dist/assets/journeyDiagram-5HDEW3XC-DtnJPxub.js +140 -0
  206. package/src/ui/dist/assets/julia-DuME0IfC.js +2 -0
  207. package/src/ui/dist/assets/kaa-6HZHGXH3-DQz-1-Gn.js +2 -0
  208. package/src/ui/dist/assets/kab-KAB-ZGHBKWFO-CaH-LSoK.js +9 -0
  209. package/src/ui/dist/assets/kanban-definition-HUTT4EX6-CZatSm3e.js +90 -0
  210. package/src/ui/dist/assets/katex-C5jXJg4s.js +258 -0
  211. package/src/ui/dist/assets/kk-KZ-P5N5QNE5-BMXmcm55.js +2 -0
  212. package/src/ui/dist/assets/km-KH-HSX4SM5Z-ZeoJNBpN.js +12 -0
  213. package/src/ui/dist/assets/ko-KR-MTYHY66A-BeaLRKph.js +10 -0
  214. package/src/ui/dist/assets/ku-TR-6OUDTVRD-B_28KrkR.js +10 -0
  215. package/src/ui/dist/assets/layout-DEXfKzaS.js +2 -0
  216. package/src/ui/dist/assets/linear-CpEDnQRC.js +2 -0
  217. package/src/ui/dist/assets/livescript-BwQOo05w.js +2 -0
  218. package/src/ui/dist/assets/lt-LT-XHIRWOB4-BxDcnUhp.js +4 -0
  219. package/src/ui/dist/assets/lua-VAEuO923.js +2 -0
  220. package/src/ui/dist/assets/lv-LV-5QDEKY6T-DpKDl1_y.js +8 -0
  221. package/src/ui/dist/assets/map-Czzmt4hB.js +2 -0
  222. package/src/ui/dist/assets/mathematica-DTrFuWx2.js +2 -0
  223. package/src/ui/dist/assets/mbox-CNhZ1qSd.js +2 -0
  224. package/src/ui/dist/assets/mermaidLoader-CBO31t0J.js +305 -0
  225. package/src/ui/dist/assets/mermaidLoader-CEK31ho9.css +1 -0
  226. package/src/ui/dist/assets/mindmap-definition-LN4V7U3C-CQ0TccEr.js +97 -0
  227. package/src/ui/dist/assets/mirc-CjQqDB4T.js +2 -0
  228. package/src/ui/dist/assets/mllike-CXdrOF99.js +2 -0
  229. package/src/ui/dist/assets/modelica-Dc1JOy9r.js +2 -0
  230. package/src/ui/dist/assets/mr-IN-CRQNXWMA-BKCZT58p.js +14 -0
  231. package/src/ui/dist/assets/mscgen-BA5vi2Kp.js +2 -0
  232. package/src/ui/dist/assets/mumps-BT43cFF4.js +2 -0
  233. package/src/ui/dist/assets/my-MM-5M5IBNSE-D34k0G9T.js +2 -0
  234. package/src/ui/dist/assets/nb-NO-T6EIAALU-DXr_KW8J.js +11 -0
  235. package/src/ui/dist/assets/nginx-DdIZxoE0.js +2 -0
  236. package/src/ui/dist/assets/nl-NL-IS3SIHDZ-C1L0aVDR.js +9 -0
  237. package/src/ui/dist/assets/nn-NO-6E72VCQL-CLWVSxbZ.js +9 -0
  238. package/src/ui/dist/assets/nsis-LdVXkNf5.js +2 -0
  239. package/src/ui/dist/assets/ntriples-BfvgReVJ.js +2 -0
  240. package/src/ui/dist/assets/oc-FR-POXYY2M6-nf8_J3eR.js +9 -0
  241. package/src/ui/dist/assets/octave-Ck1zUtKM.js +2 -0
  242. package/src/ui/dist/assets/ordinal-Cboi1Yqb.js +2 -0
  243. package/src/ui/dist/assets/oz-BzwKVEFT.js +2 -0
  244. package/src/ui/dist/assets/pa-IN-N4M65BXN-CAMJ-xd6.js +5 -0
  245. package/src/ui/dist/assets/pascal--L3eBynH.js +2 -0
  246. package/src/ui/dist/assets/pegDiagram-2B236MQR-Bj2RcmRd.js +2 -0
  247. package/src/ui/dist/assets/percentages-BXMCSKIN-s9yWFdr6.js +160 -0
  248. package/src/ui/dist/assets/perl-CdXCOZ3F.js +2 -0
  249. package/src/ui/dist/assets/pica-BGv_AhAB.js +8 -0
  250. package/src/ui/dist/assets/pieDiagram-ENE6RG2P-obpzz8dC.js +40 -0
  251. package/src/ui/dist/assets/pig-CevX1Tat.js +2 -0
  252. package/src/ui/dist/assets/pl-PL-T2D74RX3-DKJ5H85v.js +10 -0
  253. package/src/ui/dist/assets/powershell-CFHJl5sT.js +2 -0
  254. package/src/ui/dist/assets/properties-C78fOPTZ.js +2 -0
  255. package/src/ui/dist/assets/protobuf-ChK-085T.js +2 -0
  256. package/src/ui/dist/assets/{proxy-D0vUbds5.js → proxy-BSmwef4H.js} +2 -2
  257. package/src/ui/dist/assets/pt-BR-5N22H2LF-B5kow-kN.js +10 -0
  258. package/src/ui/dist/assets/pt-PT-UZXXM6DQ-B2G34fso.js +10 -0
  259. package/src/ui/dist/assets/pug-DeIclll2.js +2 -0
  260. package/src/ui/dist/assets/puppet-DMA9R1ak.js +2 -0
  261. package/src/ui/dist/assets/python-BuPzkPfP.js +2 -0
  262. package/src/ui/dist/assets/q-pXgVlZs6.js +2 -0
  263. package/src/ui/dist/assets/quadrantDiagram-ABIIQ3AL-Cl-T7NkJ.js +8 -0
  264. package/src/ui/dist/assets/r-B6wPVr8A.js +2 -0
  265. package/src/ui/dist/assets/railroadDiagram-RFXS5EU6-DMq82RHg.js +2 -0
  266. package/src/ui/dist/assets/{refresh-cw-JNmExijd.js → refresh-cw-CbFd7guV.js} +2 -2
  267. package/src/ui/dist/assets/requirementDiagram-TGXJPOKE-tGInMDgU.js +85 -0
  268. package/src/ui/dist/assets/ro-RO-JPDTUUEW-BwLdZW91.js +12 -0
  269. package/src/ui/dist/assets/roundRect-0PYZxl1G.js +2 -0
  270. package/src/ui/dist/assets/rpm-CTu-6PCP.js +2 -0
  271. package/src/ui/dist/assets/ru-RU-B4JR7IUQ-DpdAgIJc.js +10 -0
  272. package/src/ui/dist/assets/ruby-B2Rjki9n.js +2 -0
  273. package/src/ui/dist/assets/runtimeBrand-BoKPimsz.js +2 -0
  274. package/src/ui/dist/assets/sankeyDiagram-HTMAVEWB-C7X-ET4m.js +41 -0
  275. package/src/ui/dist/assets/sas-B4kiWyti.js +2 -0
  276. package/src/ui/dist/assets/save-D1CXHwBw.js +12 -0
  277. package/src/ui/dist/assets/scheme-C41bIUwD.js +2 -0
  278. package/src/ui/dist/assets/sequenceDiagram-DBY2YBRQ-Bn24i_nY.js +163 -0
  279. package/src/ui/dist/assets/settings-2-DtSNAAcU.js +22 -0
  280. package/src/ui/dist/assets/shell-CjFT_Tl9.js +2 -0
  281. package/src/ui/dist/assets/shield-check-BVbJ8T_N.js +7 -0
  282. package/src/ui/dist/assets/si-LK-N5RQ5JYF-naJaUy5u.js +2 -0
  283. package/src/ui/dist/assets/sieve-C3Gn_uJK.js +2 -0
  284. package/src/ui/dist/assets/simple-mode-GW_nhZxv.js +2 -0
  285. package/src/ui/dist/assets/sizeCapture-X5ZJPWSS-DAQHfm82.js +2 -0
  286. package/src/ui/dist/assets/sk-SK-C5VTKIMK-1NPz1J_d.js +7 -0
  287. package/src/ui/dist/assets/sl-SI-NN7IZMDC-D8RfmNQw.js +7 -0
  288. package/src/ui/dist/assets/smalltalk-CnHTOXQT.js +2 -0
  289. package/src/ui/dist/assets/solr-DehyRSwq.js +2 -0
  290. package/src/ui/dist/assets/sparql-DkYu6x3z.js +2 -0
  291. package/src/ui/dist/assets/spreadsheet-BCZA_wO0.js +2 -0
  292. package/src/ui/dist/assets/sql-D0XecflT.js +2 -0
  293. package/src/ui/dist/assets/stateDiagram-2N3HPSRC-Ck48D1ck.js +2 -0
  294. package/src/ui/dist/assets/stateDiagram-v2-6OUMAXLB-CdfRCJAJ.js +2 -0
  295. package/src/ui/dist/assets/stex-C3f8Ysf7.js +2 -0
  296. package/src/ui/dist/assets/style-C3QI7VPf.css +1 -0
  297. package/src/ui/dist/assets/stylus-B533Al4x.js +2 -0
  298. package/src/ui/dist/assets/subset-shared.chunk-eAtLHmbi.js +85 -0
  299. package/src/ui/dist/assets/subset-worker.chunk-DeHCTnTk.js +2 -0
  300. package/src/ui/dist/assets/sv-SE-XGPEYMSR-BeOYCU-8.js +11 -0
  301. package/src/ui/dist/assets/swift-BzpIVaGY.js +2 -0
  302. package/src/ui/dist/assets/swimlanes-5IMT3BWC-DOy22UjP.js +3 -0
  303. package/src/ui/dist/assets/swimlanesDiagram-G3AALYLV-sicPbVOg.js +9 -0
  304. package/src/ui/dist/assets/ta-IN-2NMHFXQM-CtS68WDd.js +10 -0
  305. package/src/ui/dist/assets/tcl-DVfN8rqt.js +2 -0
  306. package/src/ui/dist/assets/{terminal-BBQvnOAK.js → terminal-DEby5Y_P.js} +2 -2
  307. package/src/ui/dist/assets/terminalControl-BkFeDVXo.js +134 -0
  308. package/src/ui/dist/assets/textile-CnDTJFAw.js +2 -0
  309. package/src/ui/dist/assets/th-TH-HPSO5L25-DCHY9-Co.js +3 -0
  310. package/src/ui/dist/assets/tiddlywiki-DO-Gjzrf.js +2 -0
  311. package/src/ui/dist/assets/tiki-DGYXhP31.js +2 -0
  312. package/src/ui/dist/assets/timeline-definition-FHXFAJF6-DVkG72qO.js +121 -0
  313. package/src/ui/dist/assets/toml-Bm5Em-hy.js +2 -0
  314. package/src/ui/dist/assets/tr-TR-DEFEU3FU-y5-vjTv8.js +8 -0
  315. package/src/ui/dist/assets/troff-wAsdV37c.js +2 -0
  316. package/src/ui/dist/assets/ttcn-CfJYG6tj.js +2 -0
  317. package/src/ui/dist/assets/ttcn-cfg-B9xdYoR4.js +2 -0
  318. package/src/ui/dist/assets/turtle-B1tBg_DP.js +2 -0
  319. package/src/ui/dist/assets/uk-UA-QMV73CPH-BRbR5It6.js +7 -0
  320. package/src/ui/dist/assets/useReducedMotion-D07XP52Q.js +12 -0
  321. package/src/ui/dist/assets/vb-CmGdzxic.js +2 -0
  322. package/src/ui/dist/assets/vbscript-BuJXcnF6.js +2 -0
  323. package/src/ui/dist/assets/velocity-D8B20fx6.js +2 -0
  324. package/src/ui/dist/assets/vennDiagram-L72KCM5P-DwMwBlf-.js +35 -0
  325. package/src/ui/dist/assets/verilog-C6RDOZhf.js +2 -0
  326. package/src/ui/dist/assets/vhdl-lSbBsy5d.js +2 -0
  327. package/src/ui/dist/assets/vi-VN-M7AON7JQ-s6wSBv2s.js +6 -0
  328. package/src/ui/dist/assets/wardleyDiagram-EHGQE667-CNuuvzEg.js +79 -0
  329. package/src/ui/dist/assets/webidl-ZXfAyPTL.js +2 -0
  330. package/src/ui/dist/assets/{x-DoIOGAJz.js → x-B84wq78k.js} +2 -2
  331. package/src/ui/dist/assets/xquery-DzFWVndE.js +2 -0
  332. package/src/ui/dist/assets/xychartDiagram-FW5EYKEG-dUpZxmn6.js +8 -0
  333. package/src/ui/dist/assets/yacas-BJ4BC0dw.js +2 -0
  334. package/src/ui/dist/assets/z80-Hz9HOZM7.js +2 -0
  335. package/src/ui/dist/assets/zh-CN-LNUGB5OW-Cu_-POQN.js +11 -0
  336. package/src/ui/dist/assets/zh-HK-E62DVLB3-BCx4W4qZ.js +2 -0
  337. package/src/ui/dist/assets/zh-TW-RAJ6MFWO-02wx_Mrj.js +10 -0
  338. package/src/ui/dist/icons/excalidraw.svg +1 -0
  339. package/src/ui/dist/icons/markdown.svg +1 -0
  340. package/src/ui/dist/index.html +2 -2
  341. package/src/ui/package.json +10 -0
  342. package/src/ui/playwright.config.ts +36 -0
  343. package/src/ui/pnpm-workspace.yaml +2 -0
  344. package/src/ui/public/icons/excalidraw.svg +1 -0
  345. package/src/ui/public/icons/markdown.svg +1 -0
  346. package/src/ui/tsconfig.json +2 -1
  347. package/src/ui/vite.config.ts +1 -0
  348. package/src/wf-ui-server/__tests__/a2a-store.test.mjs +432 -15
  349. package/src/wf-ui-server/__tests__/action-registry-loader.test.mjs +156 -0
  350. package/src/wf-ui-server/__tests__/claude-stream-json.test.mjs +397 -0
  351. package/src/wf-ui-server/__tests__/codex-appserver.test.mjs +392 -0
  352. package/src/wf-ui-server/__tests__/component-node-api.test.mjs +881 -0
  353. package/src/wf-ui-server/__tests__/component-node-store.test.mjs +401 -0
  354. package/src/wf-ui-server/__tests__/control-plane-acceptance.test.mjs +2027 -0
  355. package/src/wf-ui-server/__tests__/control-plane-cli-smoke.test.mjs +1014 -0
  356. package/src/wf-ui-server/__tests__/dispatch-lifecycle.test.mjs +330 -0
  357. package/src/wf-ui-server/__tests__/effort-compatibility-red-092.test.mjs +151 -0
  358. package/src/wf-ui-server/__tests__/graph-layout-agent-tree.test.mjs +254 -0
  359. package/src/wf-ui-server/__tests__/graph-layout-tidy.test.mjs +260 -0
  360. package/src/wf-ui-server/__tests__/graph-layout.test.mjs +84 -0
  361. package/src/wf-ui-server/__tests__/node-config-api.test.mjs +384 -0
  362. package/src/wf-ui-server/__tests__/opencode-server.test.mjs +526 -0
  363. package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +2 -2
  364. package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +71 -1
  365. package/src/wf-ui-server/__tests__/runtime-config.test.mjs +3 -3
  366. package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +21 -0
  367. package/src/wf-ui-server/__tests__/runtime-review.test.mjs +828 -0
  368. package/src/wf-ui-server/__tests__/server.integration.test.mjs +1463 -11
  369. package/src/wf-ui-server/__tests__/session-cleanup.test.mjs +159 -0
  370. package/src/wf-ui-server/__tests__/session-registry.test.mjs +52 -3
  371. package/src/wf-ui-server/__tests__/settings.test.mjs +12 -2
  372. package/src/wf-ui-server/__tests__/spawn-gate.test.mjs +78 -0
  373. package/src/wf-ui-server/__tests__/task-group-parser.test.mjs +120 -0
  374. package/src/wf-ui-server/__tests__/task-group-tag.test.mjs +264 -0
  375. package/src/wf-ui-server/__tests__/task-parser.test.mjs +49 -3
  376. package/src/wf-ui-server/__tests__/terminal-context-hardening.test.mjs +122 -0
  377. package/src/wf-ui-server/__tests__/terminal-context.test.mjs +164 -0
  378. package/src/wf-ui-server/__tests__/terminal-store.test.mjs +4 -4
  379. package/src/wf-ui-server/__tests__/wf-browser-api.test.mjs +599 -0
  380. package/src/wf-ui-server/__tests__/wf-browser-store.test.mjs +184 -0
  381. package/src/wf-ui-server/__tests__/workflow-action-registry.test.mjs +282 -0
  382. package/src/wf-ui-server/__tests__/workflow-agent-communication.test.mjs +411 -0
  383. package/src/wf-ui-server/__tests__/workflow-agent-context.test.mjs +323 -0
  384. package/src/wf-ui-server/__tests__/workflow-agent-cooperation-permissions.test.mjs +318 -0
  385. package/src/wf-ui-server/__tests__/workflow-agent-messages-structured.test.mjs +458 -0
  386. package/src/wf-ui-server/__tests__/workflow-agent-routing.test.mjs +434 -0
  387. package/src/wf-ui-server/__tests__/workflow-agent-runtime.test.mjs +324 -0
  388. package/src/wf-ui-server/__tests__/workflow-api-integration.test.mjs +263 -0
  389. package/src/wf-ui-server/__tests__/workflow-api-phase3.test.mjs +1213 -0
  390. package/src/wf-ui-server/__tests__/workflow-claude-materialize.test.mjs +209 -0
  391. package/src/wf-ui-server/__tests__/workflow-cli-registry-manuals.test.mjs +173 -0
  392. package/src/wf-ui-server/__tests__/workflow-cli-surface.test.mjs +176 -0
  393. package/src/wf-ui-server/__tests__/workflow-codex-resume-id.test.mjs +556 -0
  394. package/src/wf-ui-server/__tests__/workflow-cooperation-enforcement.test.mjs +553 -0
  395. package/src/wf-ui-server/__tests__/workflow-depth-resume-dock.test.mjs +846 -0
  396. package/src/wf-ui-server/__tests__/workflow-display-node.test.mjs +150 -0
  397. package/src/wf-ui-server/__tests__/workflow-edge-update.test.mjs +296 -0
  398. package/src/wf-ui-server/__tests__/workflow-file-cache-lock-watch.test.mjs +380 -0
  399. package/src/wf-ui-server/__tests__/workflow-file-formats.test.mjs +994 -0
  400. package/src/wf-ui-server/__tests__/workflow-file-node-actions.test.mjs +294 -0
  401. package/src/wf-ui-server/__tests__/workflow-goal-actions.test.mjs +357 -0
  402. package/src/wf-ui-server/__tests__/workflow-goal-task-autoclose.test.mjs +256 -0
  403. package/src/wf-ui-server/__tests__/workflow-graceful-stop.test.mjs +108 -0
  404. package/src/wf-ui-server/__tests__/workflow-graph-undo.test.mjs +524 -0
  405. package/src/wf-ui-server/__tests__/workflow-layout.test.mjs +732 -0
  406. package/src/wf-ui-server/__tests__/workflow-magnetic-topology.test.mjs +222 -0
  407. package/src/wf-ui-server/__tests__/workflow-markdown-lock.test.mjs +359 -0
  408. package/src/wf-ui-server/__tests__/workflow-mcp-hub.test.mjs +174 -0
  409. package/src/wf-ui-server/__tests__/workflow-node-manual-schema.test.mjs +241 -0
  410. package/src/wf-ui-server/__tests__/workflow-node-runtime.test.mjs +1991 -0
  411. package/src/wf-ui-server/__tests__/workflow-node-store-utils.test.mjs +125 -0
  412. package/src/wf-ui-server/__tests__/workflow-registry-drift.test.mjs +296 -0
  413. package/src/wf-ui-server/__tests__/workflow-resource-handle-four-side.test.mjs +217 -0
  414. package/src/wf-ui-server/__tests__/workflow-role-profile.test.mjs +234 -0
  415. package/src/wf-ui-server/__tests__/workflow-set-model.test.mjs +261 -0
  416. package/src/wf-ui-server/__tests__/workflow-skill-group-context-enabled.test.mjs +131 -0
  417. package/src/wf-ui-server/__tests__/workflow-skill-group-enable-disable.test.mjs +186 -0
  418. package/src/wf-ui-server/__tests__/workflow-skills-hub.test.mjs +125 -0
  419. package/src/wf-ui-server/__tests__/workflow-state-reconcile.test.mjs +139 -0
  420. package/src/wf-ui-server/__tests__/workflow-subagent-strategy-matrix.test.mjs +529 -0
  421. package/src/wf-ui-server/__tests__/workflow-team-flow.test.mjs +278 -0
  422. package/src/wf-ui-server/__tests__/workflow-terminal-control-live.test.mjs +294 -0
  423. package/src/wf-ui-server/__tests__/workflow-terminal-control.test.mjs +631 -0
  424. package/src/wf-ui-server/__tests__/workflow-timer-wakeup.test.mjs +318 -0
  425. package/src/wf-ui-server/__tests__/workflow-unified-resume.test.mjs +537 -0
  426. package/src/wf-ui-server/__tests__/workspace-api-hardening.test.mjs +187 -0
  427. package/src/wf-ui-server/__tests__/workspace-api.test.mjs +382 -0
  428. package/src/wf-ui-server/__tests__/workspace-reveal.test.mjs +138 -0
  429. package/src/wf-ui-server/__tests__/workspace-store-hardening.test.mjs +255 -0
  430. package/src/wf-ui-server/__tests__/workspace-store.test.mjs +208 -0
  431. package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +52 -25
  432. package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +12 -6
  433. package/src/wf-ui-server/__tests__/ws-wf-browser.integration.test.mjs +488 -0
  434. package/src/wf-ui-server/a2a-store.mjs +3007 -89
  435. package/src/wf-ui-server/action-registry.mjs +277 -0
  436. package/src/wf-ui-server/bridge-store.mjs +276 -0
  437. package/src/wf-ui-server/chat-driver.mjs +220 -0
  438. package/src/wf-ui-server/chat-store.mjs +109 -0
  439. package/src/wf-ui-server/claude-transcript-materializer.mjs +235 -0
  440. package/src/wf-ui-server/codex-update-prompt.mjs +53 -0
  441. package/src/wf-ui-server/component-node-store.mjs +689 -0
  442. package/src/wf-ui-server/composition-api.mjs +436 -0
  443. package/src/wf-ui-server/control-plane-token.mjs +17 -0
  444. package/src/wf-ui-server/dispatch-contract.mjs +150 -0
  445. package/src/wf-ui-server/drivers/claude-stream-json.mjs +371 -0
  446. package/src/wf-ui-server/drivers/codex-appserver.mjs +513 -0
  447. package/src/wf-ui-server/drivers/opencode-server.mjs +486 -0
  448. package/src/wf-ui-server/excalidraw-svg.mjs +141 -0
  449. package/src/wf-ui-server/file-format-adapters.mjs +511 -0
  450. package/src/wf-ui-server/file-watcher.mjs +215 -0
  451. package/src/wf-ui-server/graph-layout.mjs +778 -0
  452. package/src/wf-ui-server/harness-env.mjs +46 -0
  453. package/src/wf-ui-server/json-store-utils.mjs +48 -0
  454. package/src/wf-ui-server/model-capability.mjs +773 -0
  455. package/src/wf-ui-server/node-config-store.mjs +233 -0
  456. package/src/wf-ui-server/node-store-utils.mjs +88 -0
  457. package/src/wf-ui-server/peer-capsule.mjs +56 -1
  458. package/src/wf-ui-server/pty-adapter.mjs +513 -11
  459. package/src/wf-ui-server/reveal-foreground.mjs +69 -0
  460. package/src/wf-ui-server/runtime-detector.mjs +52 -6
  461. package/src/wf-ui-server/server.mjs +5613 -403
  462. package/src/wf-ui-server/session-cleanup.mjs +363 -0
  463. package/src/wf-ui-server/session-registry.mjs +179 -3
  464. package/src/wf-ui-server/settings.mjs +25 -0
  465. package/src/wf-ui-server/spawn-gate.mjs +46 -0
  466. package/src/wf-ui-server/task-parser.mjs +88 -3
  467. package/src/wf-ui-server/terminal-store.mjs +225 -11
  468. package/src/wf-ui-server/timer-wakeup-scheduler.mjs +226 -0
  469. package/src/wf-ui-server/wf-browser-store.mjs +733 -0
  470. package/src/wf-ui-server/workflow-agent-context.mjs +1731 -0
  471. package/src/wf-ui-server/workflow-capability-node-store.mjs +676 -0
  472. package/src/wf-ui-server/workflow-event-node-store.mjs +623 -0
  473. package/src/wf-ui-server/workflow-goal-node-store.mjs +328 -0
  474. package/src/wf-ui-server/workflow-graph-store.mjs +623 -0
  475. package/src/wf-ui-server/workflow-mcp-hub.mjs +286 -0
  476. package/src/wf-ui-server/workflow-node-runtime.mjs +1767 -0
  477. package/src/wf-ui-server/workflow-node-settings-store.mjs +339 -0
  478. package/src/wf-ui-server/workflow-node-types/agent-node.mjs +920 -0
  479. package/src/wf-ui-server/workflow-node-types/display-node.mjs +151 -0
  480. package/src/wf-ui-server/workflow-node-types/excalidraw-node.mjs +145 -0
  481. package/src/wf-ui-server/workflow-node-types/file-node.mjs +484 -0
  482. package/src/wf-ui-server/workflow-node-types/github-trigger-node.mjs +207 -0
  483. package/src/wf-ui-server/workflow-node-types/goal-node.mjs +491 -0
  484. package/src/wf-ui-server/workflow-node-types/markdown-node.mjs +225 -0
  485. package/src/wf-ui-server/workflow-node-types/mcp-connector-node.mjs +71 -0
  486. package/src/wf-ui-server/workflow-node-types/role-profile-store.mjs +191 -0
  487. package/src/wf-ui-server/workflow-node-types/skill-group-node.mjs +98 -0
  488. package/src/wf-ui-server/workflow-node-types/timer-node.mjs +607 -0
  489. package/src/wf-ui-server/workflow-ontology.mjs +681 -0
  490. package/src/wf-ui-server/workflow-operation-store.mjs +115 -0
  491. package/src/wf-ui-server/workflow-skills-hub.mjs +366 -0
  492. package/src/wf-ui-server/workflow-skills-market.mjs +723 -0
  493. package/src/wf-ui-server/workspace-store.mjs +951 -0
  494. package/src/wf-ui-server/ws-chat.mjs +299 -0
  495. package/src/wf-ui-server/ws-events.mjs +45 -20
  496. package/src/wf-ui-server/ws-terminal.mjs +165 -22
  497. package/src/wf-ui-server/ws-wf-browser.mjs +419 -0
  498. package/templates/common/.claude/agents/review-manager.md +33 -29
  499. package/templates/common/.claude/commands/wf-browser.md +1 -1
  500. package/templates/common/.claude/commands/wf-help.md +5 -3
  501. package/templates/common/.claude/commands/wf-init.md +44 -0
  502. package/templates/common/.claude/commands/wf-learn.md +34 -8
  503. package/templates/common/.claude/commands/wf-max.md +19 -9
  504. package/templates/common/.claude/commands/wf-review.md +3 -3
  505. package/templates/common/.claude/commands/wf-search.md +55 -0
  506. package/templates/common/.claude/commands/wf-task-archive.md +5 -4
  507. package/templates/common/.claude/commands/wf-task-list.md +6 -3
  508. package/templates/common/.claude/commands/wf-task-record.md +10 -2
  509. package/templates/common/.claude/commands/wf-ui.md +12 -5
  510. package/templates/common/.claude/commands/wf-update.md +4 -0
  511. package/templates/common/.claude/commands/wf.md +12 -1
  512. package/templates/common/.claude/rules/ecc/common.md +10 -1
  513. package/templates/common/.claude/skills/wf/SKILL.md +36 -0
  514. package/templates/common/.claude/skills/wf-browser/SKILL.md +427 -122
  515. package/templates/common/.claude/skills/wf-browser/references/BROWSER_CONTROL_PATTERNS.md +43 -0
  516. package/templates/common/.claude/skills/wf-init/SKILL.md +33 -0
  517. package/templates/common/.claude/skills/wf-learn/SKILL.md +106 -26
  518. package/templates/common/.claude/skills/wf-max/SKILL.md +45 -11
  519. package/templates/common/.claude/skills/wf-review/SKILL.md +26 -54
  520. package/templates/common/.claude/skills/wf-search/SKILL.md +122 -0
  521. package/templates/common/.claude/skills/wf-search/references/evidence-contract.md +77 -0
  522. package/templates/common/.claude/skills/wf-search/references/policies/compare.md +38 -0
  523. package/templates/common/.claude/skills/wf-search/references/policies/fact.md +39 -0
  524. package/templates/common/.claude/skills/wf-search/references/policies/troubleshoot.md +39 -0
  525. package/templates/common/.claude/skills/wf-search/references/policies/verify.md +44 -0
  526. package/templates/common/.claude/skills/wf-search/references/source-routing.md +40 -0
  527. package/templates/common/.claude/skills/wf-search/references/tool-adapters.md +38 -0
  528. package/templates/common/.claude/skills/wf-ui/SKILL.md +146 -12
  529. package/templates/common/.claude/skills/wf-update/SKILL.md +5 -1
  530. package/templates/common/.harness-version +188 -85
  531. package/templates/common/.opencode/agents/review-manager.md +31 -27
  532. package/templates/common/.opencode/commands/wf-browser.md +1 -1
  533. package/templates/common/.opencode/commands/wf-help.md +5 -3
  534. package/templates/common/.opencode/commands/wf-init.md +44 -0
  535. package/templates/common/.opencode/commands/wf-learn.md +34 -8
  536. package/templates/common/.opencode/commands/wf-max.md +19 -9
  537. package/templates/common/.opencode/commands/wf-review.md +3 -3
  538. package/templates/common/.opencode/commands/wf-search.md +55 -0
  539. package/templates/common/.opencode/commands/wf-task-archive.md +5 -7
  540. package/templates/common/.opencode/commands/wf-task-list.md +6 -6
  541. package/templates/common/.opencode/commands/wf-task-record.md +10 -5
  542. package/templates/common/.opencode/commands/wf-ui.md +12 -5
  543. package/templates/common/.opencode/commands/wf-update.md +4 -0
  544. package/templates/common/.opencode/commands/wf.md +12 -1
  545. package/templates/common/CLAUDE.md +61 -3
  546. package/templates/common/Harness/MEMORY.md +8 -5
  547. package/templates/common/Harness/README.md +14 -10
  548. package/templates/common/Harness/a2a/action-registry.json +4397 -0
  549. package/templates/common/Harness/a2a/role-graph.json +16 -3
  550. package/templates/common/Harness/a2a/skills/terminal-control.json +45 -1
  551. package/templates/common/Harness/a2a/skills/wf-ui-map.json +50 -0
  552. package/templates/common/Harness/a2a/skills/workflow-agent-node.json +143 -0
  553. package/templates/common/Harness/a2a/skills/workflow-context.json +95 -0
  554. package/templates/common/Harness/a2a/skills/workflow-diagram-node.json +60 -0
  555. package/templates/common/Harness/a2a/skills/workflow-display-node.json +69 -0
  556. package/templates/common/Harness/a2a/skills/workflow-file-node.json +92 -0
  557. package/templates/common/Harness/a2a/skills/workflow-goal-node.json +76 -0
  558. package/templates/common/Harness/a2a/skills/workflow-markdown-node.json +79 -0
  559. package/templates/common/Harness/a2a/skills/workflow-mcp-connector-node.json +59 -0
  560. package/templates/common/Harness/a2a/skills/workflow-node-actions.json +105 -0
  561. package/templates/common/Harness/a2a/skills/workflow-node-map.json +71 -0
  562. package/templates/common/Harness/a2a/skills/workflow-ontology.json +54 -0
  563. package/templates/common/Harness/a2a/skills/workflow-resource-node.json +68 -0
  564. package/templates/common/Harness/a2a/skills/workflow-skill-group-node.json +56 -0
  565. package/templates/common/Harness/a2a/skills/workflow-timer-node.json +81 -0
  566. package/templates/common/Harness/a2a/workflows.json +33 -0
  567. package/templates/common/Harness/ownership.manifest.json +263 -2
  568. package/templates/common/Harness/project/architecture.md +499 -0
  569. package/templates/common/Harness/research/README.md +20 -0
  570. package/templates/common/Harness/scripts/README.md +70 -6
  571. package/templates/common/Harness/scripts/a2a-terminal.mjs +12 -4
  572. package/templates/common/Harness/scripts/context-budget.mjs +7 -3
  573. package/templates/common/Harness/scripts/lib/memory-context.mjs +400 -0
  574. package/templates/common/Harness/scripts/lib/path-scope.mjs +77 -0
  575. package/templates/common/Harness/scripts/lib/research-policy.mjs +99 -0
  576. package/templates/common/Harness/scripts/lib/wf-search.mjs +189 -0
  577. package/templates/common/Harness/scripts/memory-context.mjs +63 -0
  578. package/templates/common/Harness/scripts/research-policy.mjs +27 -0
  579. package/templates/common/Harness/scripts/task-context.mjs +547 -0
  580. package/templates/common/Harness/scripts/task-group-index.mjs +242 -0
  581. package/templates/common/Harness/scripts/task-state.mjs +659 -95
  582. package/templates/common/Harness/scripts/validate-harness.mjs +70 -28
  583. package/templates/common/Harness/scripts/wf-auto-update-prompt.mjs +24 -0
  584. package/templates/common/Harness/scripts/wf-remove.mjs +10 -0
  585. package/templates/common/Harness/scripts/wf-search.mjs +10 -0
  586. package/templates/common/Harness/scripts/wf-ui-control.mjs +3144 -0
  587. package/templates/common/Harness/scripts/wf-update-check.mjs +274 -51
  588. package/templates/common/Harness/scripts/wf-update-runner.mjs +53 -16
  589. package/templates/common/Harness/settings.json +10 -0
  590. package/templates/common/Harness/specs/guides/SETUP.md +4 -3
  591. package/templates/common/Harness/specs/protocols/HARNESS_BRIDGE.md +429 -48
  592. package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +24 -0
  593. package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +14 -11
  594. package/templates/common/Harness/specs/runtime/command-surface.json +28 -0
  595. package/templates/common/Harness/specs/runtime/context-loading.md +6 -0
  596. package/templates/common/Harness/specs/runtime/dispatch.md +9 -1
  597. package/templates/common/Harness/specs/runtime/subagents.md +20 -5
  598. package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +19 -0
  599. package/templates/common/Harness/specs/workflows/WF-AUTO.md +21 -0
  600. package/templates/common/Harness/specs/workflows/WF-KERNEL.md +4 -2
  601. package/templates/common/Harness/specs/workflows/WF-MAX.md +39 -12
  602. package/templates/common/Harness/specs/workflows/WF-STATE.md +62 -14
  603. package/templates/common/Harness/specs/workflows/WF.md +32 -0
  604. package/templates/common/Harness/tasks/INDEX.json +7 -0
  605. package/templates/common/Harness/tasks/INDEX.md +12 -0
  606. package/templates/common/Harness/tasks/_template/PLAN.md +6 -0
  607. package/templates/common/Harness/tasks/_template/PROGRESS.md +5 -0
  608. package/templates/common/Harness/tasks/_template/STATE.json +22 -0
  609. package/templates/common/Harness/templates/TEST_PLAN.template.md +5 -4
  610. package/templates/common/Harness/templates/UI_CONTRACT.template.md +7 -5
  611. package/templates/common/Harness/wf-browser/README.md +69 -0
  612. package/templates/common/README.md +17 -0
  613. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +0 -17
  614. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +0 -1
  615. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +0 -7
  616. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +0 -1
  617. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +0 -12
  618. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +0 -1
  619. package/src/ui/dist/assets/TaskList-BN4r8s1i.js +0 -50
  620. package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +0 -1
  621. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +0 -74
  622. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +0 -1
  623. package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +0 -1
  624. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +0 -39
  625. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +0 -1
  626. package/src/ui/dist/assets/index-BOVYWntB.js +0 -117
  627. package/src/ui/dist/assets/index-BOVYWntB.js.map +0 -1
  628. package/src/ui/dist/assets/index-DaUObq0H.css +0 -1
  629. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +0 -7
  630. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +0 -1
  631. package/src/ui/dist/assets/plus-QuTJyoT3.js +0 -7
  632. package/src/ui/dist/assets/plus-QuTJyoT3.js.map +0 -1
  633. package/src/ui/dist/assets/proxy-D0vUbds5.js.map +0 -1
  634. package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +0 -1
  635. package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +0 -1
  636. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +0 -7
  637. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +0 -1
  638. package/src/ui/dist/assets/x-DoIOGAJz.js.map +0 -1
  639. package/src/ui/pnpm-lock.yaml +0 -1582
  640. package/src/ui/src/App.tsx +0 -86
  641. package/src/ui/src/api.ts +0 -93
  642. package/src/ui/src/components/AgentsRoute.tsx +0 -503
  643. package/src/ui/src/components/Footer.tsx +0 -69
  644. package/src/ui/src/components/Header.tsx +0 -175
  645. package/src/ui/src/components/LoadingView.tsx +0 -22
  646. package/src/ui/src/components/RolesRoute.tsx +0 -169
  647. package/src/ui/src/components/SettingsRoute.tsx +0 -166
  648. package/src/ui/src/components/TaskList.tsx +0 -388
  649. package/src/ui/src/components/TerminalDrawer.tsx +0 -611
  650. package/src/ui/src/components/WorkflowRoute.tsx +0 -670
  651. package/src/ui/src/hooks/useReducedMotion.ts +0 -17
  652. package/src/ui/src/hooks/useServerConnection.ts +0 -114
  653. package/src/ui/src/index.css +0 -281
  654. package/src/ui/src/main.tsx +0 -11
  655. package/src/ui/src/types.ts +0 -108
  656. /package/src/ui/dist/assets/{TerminalDrawer-6GBZ9nXN.css → terminalControl-6GBZ9nXN.css} +0 -0
@@ -2,10 +2,17 @@ import test, { before, after } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import http from 'node:http';
4
4
  import fs from 'node:fs';
5
- import os from 'node:os';
6
5
  import path from 'node:path';
7
- import { startServer, stopServer } from '../server.mjs';
6
+ import { makeHarnessTempRoot } from '../../../tests/support/temp-root.js';
7
+ import { spawn } from 'node:child_process';
8
+ import { resolveTaskWorkflowMode, startServer, stopServer, terminalReadyForInitialInput } from '../server.mjs';
8
9
  import { SessionRegistry } from '../session-registry.mjs';
10
+ import { createComponentNode } from '../component-node-store.mjs';
11
+ import { appendTerminalData, listTerminalSessions, persistSession } from '../terminal-store.mjs';
12
+ import { writeWorkflowGraphMap } from '../a2a-store.mjs';
13
+ import { registerPtyProcess, unregisterPtyProcess } from '../ws-terminal.mjs';
14
+ import { CODEX_UPDATE_SKIP_SESSION_KEYS, CODEX_UPDATE_SKIP_UNTIL_NEXT_VERSION_KEYS } from '../codex-update-prompt.mjs';
15
+ import { graphReadToken } from '../control-plane-token.mjs';
9
16
 
10
17
  function fetchJson(baseUrl, token, route) {
11
18
  return new Promise((resolve, reject) => {
@@ -37,7 +44,7 @@ function authGet(baseUrl, token, route) {
37
44
  });
38
45
  }
39
46
 
40
- function postJson(baseUrl, token, route, payload = {}) {
47
+ function postJson(baseUrl, token, route, payload = {}, extraHeaders = {}) {
41
48
  const url = new URL(route, baseUrl);
42
49
  const body = JSON.stringify(payload);
43
50
  return new Promise((resolve, reject) => {
@@ -50,6 +57,7 @@ function postJson(baseUrl, token, route, payload = {}) {
50
57
  Authorization: `Bearer ${token}`,
51
58
  'Content-Type': 'application/json',
52
59
  'Content-Length': Buffer.byteLength(body),
60
+ ...extraHeaders,
53
61
  },
54
62
  }, (res) => {
55
63
  let responseBody = '';
@@ -64,10 +72,93 @@ function postJson(baseUrl, token, route, payload = {}) {
64
72
  });
65
73
  }
66
74
 
75
+ function deleteJson(baseUrl, token, route) {
76
+ const url = new URL(route, baseUrl);
77
+ return new Promise((resolve, reject) => {
78
+ const req = http.request({
79
+ hostname: url.hostname,
80
+ port: url.port,
81
+ path: url.pathname + url.search,
82
+ method: 'DELETE',
83
+ headers: { Authorization: `Bearer ${token}` },
84
+ }, (res) => {
85
+ let responseBody = '';
86
+ res.on('data', c => responseBody += c);
87
+ res.on('end', () => {
88
+ try { resolve({ status: res.statusCode, body: JSON.parse(responseBody) }); }
89
+ catch { resolve({ status: res.statusCode, body: responseBody }); }
90
+ });
91
+ });
92
+ req.on('error', reject);
93
+ req.end();
94
+ });
95
+ }
96
+
97
+ function readJsonlFile(filePath) {
98
+ return fs.readFileSync(filePath, 'utf8')
99
+ .split(/\r?\n/)
100
+ .filter(Boolean)
101
+ .map(line => JSON.parse(line));
102
+ }
103
+
104
+ function wait(ms) {
105
+ return new Promise(resolve => setTimeout(resolve, ms));
106
+ }
107
+
108
+ async function waitFor(fn, { timeout = 5000, step = 25 } = {}) {
109
+ const deadline = Date.now() + timeout;
110
+ let last;
111
+ while (Date.now() < deadline) {
112
+ last = await fn();
113
+ if (last) return last;
114
+ await new Promise(resolve => setTimeout(resolve, step));
115
+ }
116
+ throw new Error(`waitFor timed out; last=${JSON.stringify(last)}`);
117
+ }
118
+
119
+ function removeHarnessTempRoot(root) {
120
+ const resolved = path.resolve(root);
121
+ const tempRoot = path.resolve('Harness', '.temp') + path.sep;
122
+ assert.ok(resolved.startsWith(tempRoot), `refusing to remove non-Harness temp root: ${root}`);
123
+ try {
124
+ fs.rmSync(resolved, { recursive: true, force: true, maxRetries: 25, retryDelay: 200 });
125
+ } catch (e) {
126
+ if (process.platform === 'win32' && ['EPERM', 'EBUSY', 'ENOTEMPTY'].includes(e?.code)) return;
127
+ throw e;
128
+ }
129
+ }
130
+
131
+ function runNode(args, { cwd, env, timeout = 5000 } = {}) {
132
+ return new Promise((resolve, reject) => {
133
+ const child = spawn(process.execPath, args, {
134
+ cwd,
135
+ env,
136
+ windowsHide: true,
137
+ stdio: ['ignore', 'pipe', 'pipe'],
138
+ });
139
+ let stdout = '';
140
+ let stderr = '';
141
+ const timer = setTimeout(() => {
142
+ child.kill();
143
+ reject(new Error(`Command timed out: ${args.join(' ')}`));
144
+ }, timeout);
145
+ child.stdout.on('data', chunk => { stdout += chunk; });
146
+ child.stderr.on('data', chunk => { stderr += chunk; });
147
+ child.on('error', (err) => {
148
+ clearTimeout(timer);
149
+ reject(err);
150
+ });
151
+ child.on('close', (code, signal) => {
152
+ clearTimeout(timer);
153
+ resolve({ status: code, signal, stdout, stderr });
154
+ });
155
+ });
156
+ }
157
+
67
158
  let server, baseUrl, token, tempRoot;
68
159
 
69
160
  before(async () => {
70
- tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'wf-ui-test-'));
161
+ tempRoot = makeHarnessTempRoot('wf-ui-test-');
71
162
  const tasksDir = path.join(tempRoot, 'Harness', 'tasks');
72
163
  fs.mkdirSync(tasksDir, { recursive: true });
73
164
  const ta = path.join(tasksDir, 'task-alpha');
@@ -92,7 +183,7 @@ before(async () => {
92
183
 
93
184
  const hd = path.join(tempRoot, 'Harness');
94
185
  fs.writeFileSync(path.join(hd, 'settings.json'), JSON.stringify({ ui: { language: 'zh' } }));
95
- fs.writeFileSync(path.join(hd, '.harness-version'), JSON.stringify({ version: '0.8.19' }));
186
+ fs.writeFileSync(path.join(hd, '.harness-version'), JSON.stringify({ version: '0.8.20' }));
96
187
 
97
188
  const r = await startServer({ projectRoot: tempRoot, host: '127.0.0.1', port: 0 });
98
189
  server = r.server; token = r.token; baseUrl = `http://127.0.0.1:${r.port}`;
@@ -107,21 +198,317 @@ test('binds to 127.0.0.1', () => { const a = server.address(); assert.equal(a.ad
107
198
  test('port > 0 with port=0', () => { assert.ok(server.address().port > 0); });
108
199
  test('health returns 200', async () => { const { status, body } = await fetchJson(baseUrl, token, '/api/health'); assert.equal(status, 200); assert.equal(body.status, 'ok'); });
109
200
  test('health auth via Bearer', async () => { const { status, body } = await authGet(baseUrl, token, '/api/health'); assert.equal(status, 200); assert.equal(body.status, 'ok'); });
110
- test('no token -> 401', async () => { const { status, body } = await fetchJson(baseUrl, undefined, '/api/tasks'); assert.equal(status, 401); assert.equal(body.error.code, 'UNAUTHORIZED'); });
111
- test('bad token -> 403', async () => { const { status } = await fetchJson(baseUrl, 'bad-token-1234567890abcdef', '/api/tasks'); assert.equal(status, 403); });
112
- test('project returns root + version', async () => { const { status, body } = await fetchJson(baseUrl, token, '/api/project'); assert.equal(status, 200); assert.equal(body.root, tempRoot); assert.equal(body.version, '0.8.19'); });
201
+ test('no token can access local API', async () => { const { status, body } = await fetchJson(baseUrl, undefined, '/api/tasks'); assert.equal(status, 200); assert.ok(Array.isArray(body)); });
202
+ test('bad token is ignored for local API compatibility', async () => { const { status, body } = await fetchJson(baseUrl, 'bad-token-1234567890abcdef', '/api/tasks'); assert.equal(status, 200); assert.ok(Array.isArray(body)); });
203
+ test('durable WF task binding is explicit and managed task sessions inherit their mode', () => {
204
+ assert.equal(resolveTaskWorkflowMode({ taskId: 'task-wf', mode: 'wf' }, ''), 'wf');
205
+ assert.equal(resolveTaskWorkflowMode({ taskId: 'task-direct', mode: 'direct' }, 'wf-auto'), 'wf-auto');
206
+ assert.throws(
207
+ () => resolveTaskWorkflowMode({ taskId: 'task-direct', mode: 'direct' }, 'wf', { taskId: 'task-direct' }),
208
+ /cannot be bound to durable WF mode/,
209
+ );
210
+ assert.throws(
211
+ () => resolveTaskWorkflowMode({ taskId: 'task-wf', mode: 'wf' }, 'wf-max', { taskId: 'task-wf' }),
212
+ /requires workflow mode "wf"/,
213
+ );
214
+ });
215
+ test('AC-004 project view returns root, task count, and durable WF resume summary', async () => {
216
+ const { status, body } = await fetchJson(baseUrl, token, '/api/project');
217
+ assert.equal(status, 200);
218
+ assert.equal(body.root, tempRoot);
219
+ assert.equal(body.version, '0.8.20');
220
+ assert.equal(body.activeTaskId, null);
221
+ assert.equal(body.wfManaged, false);
222
+ assert.equal(body.resumeRequired, false);
223
+ assert.ok(Array.isArray(body.projects));
224
+ });
113
225
  test('tasks returns array sorted desc', async () => { const { status, body } = await fetchJson(baseUrl, token, '/api/tasks'); assert.equal(status, 200); assert.ok(body.length >= 2); assert.equal(body[0].taskId, 'task-alpha'); });
114
226
  test('tasks/:id returns single', async () => { const { status, body } = await fetchJson(baseUrl, token, '/api/tasks/task-beta'); assert.equal(status, 200); assert.equal(body.taskId, 'task-beta'); });
115
227
  test('tasks/:id missing -> 404', async () => { const { status } = await fetchJson(baseUrl, token, '/api/tasks/task-nonexist'); assert.equal(status, 404); });
116
228
  test('tasks with invalid chars -> 400', async () => { const { status } = await fetchJson(baseUrl, token, '/api/tasks/task%00inject'); assert.equal(status, 400); });
117
229
  test('settings merged', async () => { const { status, body } = await fetchJson(baseUrl, token, '/api/settings'); assert.equal(status, 200); assert.equal(body.server.host, '127.0.0.1'); assert.equal(body.ui.language, 'zh'); });
230
+ test('settings POST preserves non-UI project settings while updating cleanup', async () => {
231
+ const root = makeHarnessTempRoot('wf-ui-settings-save-test-');
232
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
233
+ fs.writeFileSync(path.join(root, 'Harness', 'settings.json'), JSON.stringify({
234
+ schema: 'harness-settings@1',
235
+ workflow: { defaultTier: 'auto', taskCapsuleCap: 5 },
236
+ cleanup: { stoppedSessionRetentionDays: 14, detachedLogRetentionHours: 24 },
237
+ }, null, 2));
238
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0 });
239
+ try {
240
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
241
+ const result = await postJson(localBaseUrl, started.token, '/api/settings', {
242
+ cleanup: { stoppedSessionRetentionDays: 3 },
243
+ });
244
+ assert.equal(result.status, 200);
245
+ const saved = JSON.parse(fs.readFileSync(path.join(root, 'Harness', 'settings.json'), 'utf8'));
246
+ assert.equal(saved.schema, 'harness-settings@1');
247
+ assert.equal(saved.workflow.taskCapsuleCap, 5);
248
+ assert.equal(saved.cleanup.stoppedSessionRetentionDays, 3);
249
+ assert.equal(saved.cleanup.detachedLogRetentionHours, 24);
250
+ } finally {
251
+ await stopServer(started.server);
252
+ fs.rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
253
+ }
254
+ });
118
255
  test('sessions empty when no registry', async () => { const { status, body } = await fetchJson(baseUrl, token, '/api/sessions'); assert.equal(status, 200); assert.ok(Array.isArray(body)); assert.equal(body.length, 0); });
119
- test('error envelope format', async () => { const { status, body } = await fetchJson(baseUrl, undefined, '/api/tasks'); assert.equal(status, 401); assert.ok(body.error.code); assert.ok(body.error.message); });
256
+ test('error envelope format', async () => { const { status, body } = await fetchJson(baseUrl, undefined, '/api/nope'); assert.equal(status, 404); assert.ok(body.error.code); assert.ok(body.error.message); });
120
257
  test('content-type json', async () => { const { status, headers } = await fetchJson(baseUrl, token, '/api/health'); assert.equal(status, 200); assert.match(headers['content-type'], /application\/json/); });
121
258
  test('unknown route 404', async () => { const { status } = await fetchJson(baseUrl, token, '/api/nope'); assert.equal(status, 404); });
122
259
 
260
+ test('AC-002 /api/a2a/snapshot running status matches /api/sessions and downgrades stale disk sessions', async () => {
261
+ const root = makeHarnessTempRoot('wf-ui-live-source-test-');
262
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
263
+ persistSession(root, {
264
+ taskId: null,
265
+ sessionId: 'session-stale-running',
266
+ runtime: 'claude',
267
+ role: 'Subagent',
268
+ status: 'running',
269
+ wsClientCount: 1,
270
+ updatedAt: '2026-07-30T00:00:00.000Z',
271
+ });
272
+ writeWorkflowGraphMap(root, {
273
+ version: 9,
274
+ nodes: [{ nodeId: 'session-session-stale-running', sessionId: 'session-stale-running', runtime: 'claude', status: 'running' }],
275
+ positions: { 'session-session-stale-running': { x: 320, y: 180 } },
276
+ });
277
+
278
+ const registry = new SessionRegistry();
279
+ const liveSession = registry.create({
280
+ runtime: 'codex',
281
+ role: 'Main Agent',
282
+ agentKind: 'main',
283
+ });
284
+ registry.update(liveSession.sessionId, { status: 'running' });
285
+
286
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry, eventsWs: false });
287
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
288
+ try {
289
+ const sessions = await fetchJson(localBaseUrl, started.token, '/api/sessions');
290
+ const allSessions = await fetchJson(localBaseUrl, started.token, '/api/sessions?all=1');
291
+ const snapshot = await fetchJson(localBaseUrl, started.token, '/api/a2a/snapshot');
292
+
293
+ assert.equal(sessions.status, 200);
294
+ assert.equal(allSessions.status, 200);
295
+ assert.equal(snapshot.status, 200);
296
+
297
+ const liveRunningIds = sessions.body
298
+ .filter(session => session.status === 'running')
299
+ .map(session => session.sessionId)
300
+ .sort();
301
+ const live = sessions.body.find(session => session.sessionId === liveSession.sessionId);
302
+ assert.ok(live.resourceUsage, 'live sessions should expose PTY resource details');
303
+ assert.equal(live.resourceUsage.pid, liveSession.pid || null);
304
+ assert.equal(live.resourceUsage.wsClientCount, live.wsClientCount || 0);
305
+ assert.ok(Object.hasOwn(live.resourceUsage, 'memoryBytes'));
306
+ assert.ok(Object.hasOwn(live.resourceUsage, 'ptyProvider'));
307
+ assert.ok(Object.hasOwn(live.resourceUsage, 'cpuPercent'));
308
+ const snapshotRunningIds = snapshot.body.nodes
309
+ .filter(node => node.status === 'running')
310
+ .map(node => node.sessionId)
311
+ .sort();
312
+ assert.deepEqual(snapshotRunningIds, liveRunningIds);
313
+
314
+ const staleNode = snapshot.body.nodes.find(node => node.sessionId === 'session-stale-running');
315
+ assert.ok(staleNode, 'stale project graph session should remain visible as stopped history');
316
+ assert.equal(staleNode.status, 'stopped');
317
+ assert.equal(staleNode.blockedReason, 'not-managed-by-current-wf-ui');
318
+ assert.deepEqual(snapshot.body.graph.positions['session-session-stale-running'], { x: 320, y: 180 });
319
+ const staleSession = allSessions.body.find(session => session.sessionId === 'session-stale-running');
320
+ assert.equal(staleSession.status, 'stopped', 'orphaned live disk sessions are downgraded once at backend startup (P1b)');
321
+ assert.equal(staleSession.blockedReason, 'not-managed-by-current-wf-ui');
322
+ assert.equal(staleSession.attachMode, false, 'orphaned disk sessions are watch-only until reattached');
323
+ assert.equal(staleSession.wsClientCount, 0);
324
+ assert.equal(staleSession.resourceUsage.cpuPercent, null);
325
+ } finally {
326
+ await stopServer(started.server);
327
+ fs.rmSync(root, { recursive: true, force: true });
328
+ }
329
+ });
330
+
331
+ test('AC-002b snapshot incremental polling: ?since= fingerprint returns unchanged until state moves', async () => {
332
+ const root = makeHarnessTempRoot('wf-ui-since-test-');
333
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
334
+ const registry = new SessionRegistry();
335
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry, eventsWs: false });
336
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
337
+ try {
338
+ const first = await fetchJson(localBaseUrl, started.token, '/api/a2a/snapshot');
339
+ assert.equal(first.status, 200);
340
+ assert.ok(first.body.fingerprint, 'snapshot carries a fingerprint');
341
+
342
+ const unchanged = await fetchJson(localBaseUrl, started.token, `/api/a2a/snapshot?since=${encodeURIComponent(first.body.fingerprint)}`);
343
+ assert.equal(unchanged.status, 200);
344
+ assert.equal(unchanged.body.unchanged, true);
345
+ assert.equal(unchanged.body.fingerprint, first.body.fingerprint);
346
+
347
+ persistSession(root, { taskId: null, sessionId: 'session-since-move', runtime: 'codex', role: 'Subagent', status: 'stopped' });
348
+ const changed = await fetchJson(localBaseUrl, started.token, `/api/a2a/snapshot?since=${encodeURIComponent(first.body.fingerprint)}`);
349
+ assert.equal(changed.status, 200);
350
+ assert.notEqual(changed.body.unchanged, true, 'state change must bust the fingerprint');
351
+ assert.notEqual(changed.body.fingerprint, first.body.fingerprint);
352
+ } finally {
353
+ await stopServer(started.server);
354
+ fs.rmSync(root, { recursive: true, force: true });
355
+ }
356
+ });
357
+
358
+ test('AC-002c display node html serve route: default template until written, then agent html', async () => {
359
+ const root = makeHarnessTempRoot('wf-ui-display-serve-');
360
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
361
+ const registry = new SessionRegistry();
362
+ const created = createComponentNode(root, { type: 'display', title: 'Research Report' });
363
+ const nodeId = created.node.nodeId;
364
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry, eventsWs: false });
365
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
366
+ try {
367
+ const empty = await fetchJson(localBaseUrl, started.token, `/api/a2a/component-nodes/${nodeId}/html`);
368
+ assert.equal(empty.status, 200);
369
+ assert.ok(String(empty.body).includes('<!DOCTYPE html>'), 'serves the default template before any write');
370
+ assert.ok(String(empty.body).includes('#D97757'), 'theme accent present');
371
+ assert.ok(String(empty.body).includes('Research Report'), 'title in the template');
372
+ } finally {
373
+ await stopServer(started.server);
374
+ fs.rmSync(root, { recursive: true, force: true });
375
+ }
376
+ });
377
+
378
+ test('AC-008 workflow agent sessions write node-home init files and use quiet bootstrap metadata', async () => {
379
+ const root = makeHarnessTempRoot('wf-ui-node-home-test-');
380
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
381
+ const registry = new SessionRegistry();
382
+ try {
383
+ const session = registry.create({
384
+ runtime: 'claude',
385
+ agentKind: 'main',
386
+ role: 'Main Agent',
387
+ objective: 'Quiet bootstrap check',
388
+ projectRoot: root,
389
+ cwd: root,
390
+ });
391
+ const nodeHomeRel = `Harness/a2a/nodes/${session.sessionId}`;
392
+ registry.update(session.sessionId, {
393
+ nodeHomeRel,
394
+ nodeHomePath: path.join(root, nodeHomeRel),
395
+ nodeInitRel: `${nodeHomeRel}/init.md`,
396
+ nodeInitPath: path.join(root, nodeHomeRel, 'init.md'),
397
+ });
398
+ const current = registry.get(session.sessionId);
399
+ persistSession(root, current);
400
+
401
+ fs.mkdirSync(current.nodeHomePath, { recursive: true });
402
+ fs.writeFileSync(current.nodeInitPath, [
403
+ '# Harness WF Node Init',
404
+ '',
405
+ '- Keep terminal startup quiet. Do not print this file unless the operator asks.',
406
+ '- Read the workflow graph with `node Harness/scripts/wf-ui-control.mjs describe --project .` when you need topology.',
407
+ ].join('\n'));
408
+
409
+ assert.equal(fs.existsSync(current.nodeInitPath), true);
410
+ const init = fs.readFileSync(current.nodeInitPath, 'utf8');
411
+ assert.match(init, /Keep terminal startup quiet/);
412
+ assert.match(init, /wf-ui-control\.mjs describe/);
413
+ } finally {
414
+ fs.rmSync(root, { recursive: true, force: true });
415
+ }
416
+ });
417
+
418
+ test('AC-004 session input API writes directly to the registered PTY process', async () => {
419
+ const root = makeHarnessTempRoot('wf-ui-direct-pty-input-test-');
420
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
421
+ const registry = new SessionRegistry();
422
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
423
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
424
+ const written = [];
425
+ try {
426
+ const session = registry.create({ runtime: 'codex', agentKind: 'main' });
427
+ registry.update(session.sessionId, { status: 'running' });
428
+ registerPtyProcess(session.sessionId, { write: data => written.push(data) });
429
+
430
+ const result = await postJson(localBaseUrl, started.token, `/api/sessions/${session.sessionId}/input`, {
431
+ input: 'hello from attached terminal\r',
432
+ });
433
+
434
+ assert.equal(result.status, 200);
435
+ assert.equal(result.body.ok, true);
436
+ assert.deepEqual(written, ['hello from attached terminal\r']);
437
+ } finally {
438
+ for (const session of registry.getAll()) unregisterPtyProcess(session.sessionId);
439
+ await stopServer(started.server);
440
+ removeHarnessTempRoot(root);
441
+ }
442
+ });
443
+
444
+ test('AC-003 terminal file drop API stores uploaded files below the session drop directory', async () => {
445
+ const session = persistSession(tempRoot, {
446
+ sessionId: 'session-drop-test',
447
+ runtime: 'codex',
448
+ status: 'running',
449
+ attachMode: true,
450
+ taskId: null,
451
+ });
452
+
453
+ const { status, body } = await postJson(baseUrl, token, `/api/sessions/${session.sessionId}/drop-files`, {
454
+ files: [
455
+ {
456
+ name: '..\\unsafe name.txt',
457
+ contentBase64: Buffer.from('drop-file-body', 'utf8').toString('base64'),
458
+ },
459
+ ],
460
+ });
461
+
462
+ assert.equal(status, 200);
463
+ assert.equal(body.files.length, 1);
464
+ const dropped = body.files[0];
465
+ const dropRoot = path.join(tempRoot, 'Harness', 'a2a', 'sessions', session.sessionId, 'drops');
466
+ assert.ok(dropped.path.startsWith(dropRoot), `drop path should stay under ${dropRoot}`);
467
+ assert.doesNotMatch(path.relative(dropRoot, dropped.path), /^\.\./, 'drop path must not traverse out of drop root');
468
+ assert.equal(fs.readFileSync(dropped.path, 'utf8'), 'drop-file-body');
469
+ assert.match(dropped.name, /unsafe name\.txt$/, 'basename should be preserved after path stripping');
470
+ assert.match(dropped.terminalText, /^'.*'$/, 'terminal text should be safely quoted for paste/input');
471
+ assert.equal(body.terminalInput, dropped.terminalText);
472
+ });
473
+
474
+ test('wf-bridge records cross-session input after PTY write succeeds', async () => {
475
+ const root = makeHarnessTempRoot('wf-ui-bridge-input-test-');
476
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
477
+ const registry = new SessionRegistry();
478
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
479
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
480
+ const written = [];
481
+ try {
482
+ const from = registry.create({ runtime: 'claude', agentKind: 'main' });
483
+ const to = registry.create({ runtime: 'codex', agentKind: 'main' });
484
+ registry.update(from.sessionId, { status: 'running', graphNodeId: `session-${from.sessionId}` });
485
+ registry.update(to.sessionId, { status: 'running', graphNodeId: `session-${to.sessionId}` });
486
+ registerPtyProcess(to.sessionId, { write: data => written.push(data) });
487
+
488
+ const result = await postJson(localBaseUrl, started.token, `/api/sessions/${to.sessionId}/input`, {
489
+ data: 'reply over bridge\r',
490
+ fromSessionId: from.sessionId,
491
+ fromNodeId: `session-${from.sessionId}`,
492
+ });
493
+
494
+ assert.equal(result.status, 200);
495
+ assert.deepEqual(written, ['reply over bridge\r']);
496
+ assert.equal(result.body.bridgeMessage.fromSessionId, from.sessionId);
497
+ assert.equal(result.body.bridgeMessage.toSessionId, to.sessionId);
498
+
499
+ const messages = await fetchJson(localBaseUrl, started.token, `/api/a2a/bridge-messages?fromSessionId=${from.sessionId}&toSessionId=${to.sessionId}&limit=20`);
500
+ assert.equal(messages.status, 200);
501
+ assert.equal(messages.body.entries.length, 1);
502
+ assert.equal(messages.body.entries[0].data, 'reply over bridge\r');
503
+ } finally {
504
+ for (const session of registry.getAll()) unregisterPtyProcess(session.sessionId);
505
+ await stopServer(started.server);
506
+ fs.rmSync(root, { recursive: true, force: true });
507
+ }
508
+ });
509
+
123
510
  test('session stop is idempotent and removes session from live list', async () => {
124
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'wf-ui-stop-test-'));
511
+ const root = makeHarnessTempRoot('wf-ui-stop-test-');
125
512
  fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
126
513
  const registry = new SessionRegistry();
127
514
  const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
@@ -133,7 +520,8 @@ test('session stop is idempotent and removes session from live list', async () =
133
520
  const first = await postJson(localBaseUrl, started.token, `/api/sessions/${session.sessionId}/stop`);
134
521
  assert.equal(first.status, 200);
135
522
  assert.equal(first.body.ok, true);
136
- assert.equal(first.body.saved.status, 'saved');
523
+ assert.equal(first.body.stopped.status, 'stopped');
524
+ assert.equal(first.body.saved.status, 'stopped');
137
525
 
138
526
  const live = await fetchJson(localBaseUrl, started.token, '/api/sessions');
139
527
  assert.equal(live.status, 200);
@@ -148,3 +536,1067 @@ test('session stop is idempotent and removes session from live list', async () =
148
536
  fs.rmSync(root, { recursive: true, force: true });
149
537
  }
150
538
  });
539
+
540
+ test('AC-003 stop updates workflow snapshot control and persisted graph state', async () => {
541
+ const root = makeHarnessTempRoot('wf-ui-stop-snapshot-test-');
542
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
543
+ const registry = new SessionRegistry();
544
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
545
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
546
+ try {
547
+ const session = registry.create({ runtime: 'codex', agentKind: 'main', role: 'Main Agent' });
548
+ registry.update(session.sessionId, { status: 'running', graphNodeId: `session-${session.sessionId}` });
549
+ persistSession(root, registry.get(session.sessionId));
550
+ writeWorkflowGraphMap(root, {
551
+ nodes: [{ nodeId: `session-${session.sessionId}`, sessionId: session.sessionId, runtime: 'codex', status: 'running' }],
552
+ positions: { [`session-${session.sessionId}`]: { x: 20, y: 40 } },
553
+ });
554
+
555
+ const first = await postJson(localBaseUrl, started.token, `/api/sessions/${session.sessionId}/stop`);
556
+ assert.equal(first.status, 200);
557
+ assert.equal(first.body.stopped.status, 'stopped');
558
+ assert.equal(first.body.saved.status, 'stopped');
559
+
560
+ const live = await fetchJson(localBaseUrl, started.token, '/api/sessions');
561
+ const snapshot = await fetchJson(localBaseUrl, started.token, '/api/a2a/snapshot?fresh=1');
562
+ const graphMap = await fetchJson(localBaseUrl, started.token, '/api/a2a/graph-map');
563
+ const node = snapshot.body.nodes.find(item => item.sessionId === session.sessionId);
564
+
565
+ assert.equal(live.status, 200);
566
+ assert.deepEqual(live.body, []);
567
+ assert.ok(node, 'stopped graph-bound node should remain visible as stopped transcript history');
568
+ assert.equal(node.status, 'stopped');
569
+ assert.equal(node.lifecycle, 'stopped');
570
+ assert.equal(node.control.canStart, true);
571
+ assert.equal(node.control.canStop, false);
572
+ assert.equal(node.control.canDelete, true);
573
+ assert.equal(graphMap.body.nodes.find(item => item.sessionId === session.sessionId)?.status, 'stopped');
574
+ } finally {
575
+ await stopServer(started.server);
576
+ fs.rmSync(root, { recursive: true, force: true });
577
+ }
578
+ });
579
+
580
+ test('AC-004 DELETE /api/a2a/nodes removes stopped not-managed graph node but preserves transcript history', async () => {
581
+ const root = makeHarnessTempRoot('wf-ui-delete-node-test-');
582
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
583
+ persistSession(root, {
584
+ taskId: null,
585
+ sessionId: 'session-delete-me',
586
+ runtime: 'claude',
587
+ role: 'Subagent',
588
+ status: 'running',
589
+ terminalSeq: 0,
590
+ updatedAt: '2026-07-30T00:00:00.000Z',
591
+ });
592
+ persistSession(root, {
593
+ taskId: null,
594
+ sessionId: 'session-keep-me',
595
+ runtime: 'codex',
596
+ role: 'Main Agent',
597
+ status: 'saved',
598
+ updatedAt: '2026-07-30T00:00:01.000Z',
599
+ });
600
+ const deleteState = listTerminalSessions(root).find(session => session.sessionId === 'session-delete-me');
601
+ appendTerminalData(root, deleteState, 'saved transcript line\r\n');
602
+ writeWorkflowGraphMap(root, {
603
+ nodes: [
604
+ { nodeId: 'session-session-delete-me', sessionId: 'session-delete-me', runtime: 'claude', status: 'running' },
605
+ { nodeId: 'session-session-keep-me', sessionId: 'session-keep-me', runtime: 'codex', status: 'saved' },
606
+ ],
607
+ edges: [
608
+ { id: 'edge-delete', from: 'session-session-keep-me', to: 'session-session-delete-me', relation: 'can-communicate' },
609
+ ],
610
+ positions: {
611
+ 'session-session-delete-me': { x: 120, y: 80 },
612
+ 'session-session-keep-me': { x: 20, y: 30 },
613
+ },
614
+ });
615
+
616
+ const registry = new SessionRegistry();
617
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
618
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
619
+ try {
620
+ const before = await fetchJson(localBaseUrl, started.token, '/api/a2a/snapshot?fresh=1');
621
+ assert.equal(before.status, 200);
622
+ assert.equal(before.body.nodes.find(node => node.sessionId === 'session-delete-me')?.blockedReason, 'not-managed-by-current-wf-ui');
623
+
624
+ const removed = await deleteJson(localBaseUrl, started.token, '/api/a2a/nodes/session-session-delete-me');
625
+ assert.equal(removed.status, 200);
626
+ assert.equal(removed.body.ok, true);
627
+ assert.equal(removed.body.removed.nodeId, 'session-session-delete-me');
628
+
629
+ const after = await fetchJson(localBaseUrl, started.token, '/api/a2a/snapshot?fresh=1');
630
+ const graphMap = await fetchJson(localBaseUrl, started.token, '/api/a2a/graph-map');
631
+ const transcript = await fetchJson(localBaseUrl, started.token, '/api/terminals/session-delete-me/range?tail=10');
632
+
633
+ assert.equal(after.body.nodes.some(node => node.sessionId === 'session-delete-me'), false);
634
+ assert.equal(after.body.nodes.some(node => node.sessionId === 'session-keep-me'), true);
635
+ assert.equal(after.body.graph.edges.some(edge => edge.toSessionId === 'session-delete-me'), false);
636
+ assert.equal(graphMap.body.nodes.some(node => node.sessionId === 'session-delete-me'), false);
637
+ assert.equal(graphMap.body.positions['session-session-delete-me'], undefined);
638
+ assert.ok(graphMap.body.deletedNodes.some(node => node.sessionId === 'session-delete-me'));
639
+ assert.equal(transcript.body.entries.some(entry => String(entry.data).includes('saved transcript line')), true);
640
+ } finally {
641
+ await stopServer(started.server);
642
+ fs.rmSync(root, { recursive: true, force: true });
643
+ }
644
+ });
645
+
646
+ test('AC-004 DELETE /api/a2a/nodes removes non-live registry sessions from snapshot', async () => {
647
+ const root = makeHarnessTempRoot('wf-ui-delete-registry-saved-test-');
648
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
649
+ const registry = new SessionRegistry();
650
+ const session = registry.create({
651
+ runtime: 'opencode',
652
+ agentKind: 'subagent',
653
+ graphNodeId: 'session-registry-saved',
654
+ projectRoot: root,
655
+ cwd: root,
656
+ });
657
+ registry.update(session.sessionId, { status: 'saved' });
658
+ persistSession(root, registry.get(session.sessionId));
659
+ writeWorkflowGraphMap(root, {
660
+ nodes: [
661
+ { nodeId: 'session-registry-saved', sessionId: session.sessionId, runtime: 'opencode', status: 'saved' },
662
+ ],
663
+ });
664
+
665
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
666
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
667
+ try {
668
+ const removed = await deleteJson(localBaseUrl, started.token, '/api/a2a/nodes/session-registry-saved');
669
+ assert.equal(removed.status, 200);
670
+ assert.equal(registry.get(session.sessionId), undefined);
671
+
672
+ const after = await fetchJson(localBaseUrl, started.token, '/api/a2a/snapshot?fresh=1');
673
+ assert.equal(after.status, 200);
674
+ assert.equal(after.body.nodes.some(node => node.sessionId === session.sessionId), false);
675
+ } finally {
676
+ await stopServer(started.server);
677
+ fs.rmSync(root, { recursive: true, force: true });
678
+ }
679
+ });
680
+
681
+ test('AC-001 wf-ui-control describe uses read-only snapshot token instead of stale local graph fallback', async () => {
682
+ const root = makeHarnessTempRoot('wf-ui-read-token-test-');
683
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
684
+ writeWorkflowGraphMap(root, {
685
+ nodes: [{ nodeId: 'session-local-only', sessionId: 'local-only', runtime: 'claude', status: 'saved' }],
686
+ });
687
+
688
+ const registry = new SessionRegistry();
689
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
690
+ try {
691
+ const sessionId = 'session-reader';
692
+ const result = await runNode([
693
+ path.join(process.cwd(), 'Harness', 'scripts', 'wf-ui-control.mjs'),
694
+ 'describe',
695
+ '--project',
696
+ root,
697
+ ], {
698
+ cwd: root,
699
+ timeout: 5000,
700
+ env: {
701
+ ...process.env,
702
+ HARNESS_PEER_SESSION_ID: sessionId,
703
+ HARNESS_AGENT_KIND: 'subagent',
704
+ HARNESS_WF_UI_URL: `http://127.0.0.1:${started.port}`,
705
+ HARNESS_WF_UI_READ_TOKEN: graphReadToken(started.token, sessionId),
706
+ },
707
+ });
708
+ assert.equal(result.status, 0, result.stderr || result.stdout);
709
+ const body = JSON.parse(result.stdout);
710
+ assert.equal(body.counts.nodes, 0);
711
+ assert.equal(body.nodes.some(node => node.sessionId === 'local-only'), false);
712
+ } finally {
713
+ await stopServer(started.server);
714
+ fs.rmSync(root, { recursive: true, force: true });
715
+ }
716
+ });
717
+
718
+ test('W21-ONTOLOGY exposes workflow ontology through full and graph read tokens', async () => {
719
+ const root = makeHarnessTempRoot('wf-ui-ontology-api-test-');
720
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
721
+ const sessionId = 'ontology-reader';
722
+ writeWorkflowGraphMap(root, {
723
+ nodes: [{ nodeId: 'session-ontology-reader', sessionId, runtime: 'codex', agentKind: 'subagent', status: 'running' }],
724
+ });
725
+
726
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: new SessionRegistry() });
727
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
728
+ try {
729
+ const full = await fetchJson(localBaseUrl, started.token, '/api/workflow/ontology');
730
+ assert.equal(full.status, 200, JSON.stringify(full.body));
731
+ assert.equal(full.body.ok, true);
732
+ assert.equal(full.body.ontology.ontologyId, 'harness.workflow.ontology');
733
+ assert.equal(full.body.ontology.nodeTypes.timer.controlRelation.relation, 'control');
734
+
735
+ const readOnly = await fetchJson(
736
+ localBaseUrl,
737
+ graphReadToken(started.token, sessionId),
738
+ `/api/workflow/ontology?actorSessionId=${sessionId}`,
739
+ );
740
+ assert.equal(readOnly.status, 200, JSON.stringify(readOnly.body));
741
+ assert.equal(readOnly.body.ontology.nodeTypes.markdown.direction, 'bidirectional');
742
+
743
+ const script = path.join(process.cwd(), 'Harness', 'scripts', 'wf-ui-control.mjs');
744
+ const cli = await runNode([
745
+ script,
746
+ 'workflow-ontology',
747
+ '--project',
748
+ root,
749
+ ], {
750
+ cwd: root,
751
+ timeout: 5000,
752
+ env: {
753
+ ...process.env,
754
+ HARNESS_PEER_SESSION_ID: sessionId,
755
+ HARNESS_WORKFLOW_NODE_ID: 'session-ontology-reader',
756
+ HARNESS_WF_UI_URL: localBaseUrl,
757
+ HARNESS_WF_UI_TOKEN: '',
758
+ HARNESS_WF_UI_READ_TOKEN: graphReadToken(started.token, sessionId),
759
+ },
760
+ });
761
+ assert.equal(cli.status, 0, cli.stderr || cli.stdout);
762
+ const cliBody = JSON.parse(cli.stdout);
763
+ assert.equal(cliBody.ontology.ontologyId, 'harness.workflow.ontology');
764
+ assert.equal(cliBody.ontology.nodeTypes.goal.direction, 'bidirectional');
765
+
766
+ const badToken = await fetchJson(localBaseUrl, 'not-a-token', '/api/workflow/ontology');
767
+ assert.equal(badToken.status, 200);
768
+ assert.equal(badToken.body.ontology.ontologyId, 'harness.workflow.ontology');
769
+ } finally {
770
+ await stopServer(started.server);
771
+ fs.rmSync(root, { recursive: true, force: true });
772
+ }
773
+ });
774
+
775
+ test('W21-AUTH validates server-derived Agent actor session for timer control actions', async () => {
776
+ const root = makeHarnessTempRoot('wf-ui-action-auth-test-');
777
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
778
+ const agentNodeId = 'session-w21-auth-agent';
779
+ const otherAgentNodeId = 'session-w21-auth-other';
780
+ writeWorkflowGraphMap(root, {
781
+ nodes: [
782
+ { nodeId: agentNodeId, sessionId: 'w21-auth-agent-pty', runtime: 'codex', agentKind: 'main', status: 'running' },
783
+ { nodeId: otherAgentNodeId, sessionId: 'w21-auth-other-pty', runtime: 'codex', agentKind: 'subagent', status: 'running' },
784
+ ],
785
+ positions: {
786
+ [agentNodeId]: { x: 20, y: 40 },
787
+ [otherAgentNodeId]: { x: 260, y: 40 },
788
+ },
789
+ });
790
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: new SessionRegistry() });
791
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
792
+ try {
793
+ const created = await postJson(localBaseUrl, started.token, '/api/workflow/nodes', {
794
+ type: 'timer',
795
+ title: 'W21 Auth Timer',
796
+ enabled: true,
797
+ schedule: { mode: 'interval', intervalSeconds: 60 },
798
+ controlPolicy: { agentCanSetInterval: true, minIntervalSeconds: 10, maxIntervalSeconds: 3600 },
799
+ });
800
+ assert.equal(created.status, 201, JSON.stringify(created.body));
801
+ const timerNodeId = created.body.node.nodeId;
802
+
803
+ const edge = await postJson(localBaseUrl, started.token, '/api/workflow/edges', {
804
+ from: agentNodeId,
805
+ to: timerNodeId,
806
+ relation: 'control',
807
+ direction: 'source-to-target',
808
+ sourceHandle: 'control',
809
+ targetHandle: 'config',
810
+ });
811
+ assert.equal(edge.status, 201, JSON.stringify(edge.body));
812
+
813
+ const spoofed = await postJson(
814
+ localBaseUrl,
815
+ started.token,
816
+ `/api/workflow/nodes/${timerNodeId}/actions/timer.setInterval`,
817
+ { actorNodeId: agentNodeId, intervalSeconds: 120 },
818
+ { 'X-Harness-Session-Id': 'w21-auth-other-pty' },
819
+ );
820
+ assert.equal(spoofed.status, 403, JSON.stringify(spoofed.body));
821
+ assert.equal(spoofed.body.error?.code, 'AGENT_ACTOR_MISMATCH');
822
+
823
+ const derived = await postJson(
824
+ localBaseUrl,
825
+ started.token,
826
+ `/api/workflow/nodes/${timerNodeId}/actions/timer.setInterval`,
827
+ { intervalSeconds: 180 },
828
+ { 'X-Harness-Session-Id': 'w21-auth-agent-pty' },
829
+ );
830
+ assert.equal(derived.status, 200, JSON.stringify(derived.body));
831
+ assert.equal(derived.body.result.state.schedule.intervalSeconds, 180);
832
+ } finally {
833
+ await stopServer(started.server);
834
+ fs.rmSync(root, { recursive: true, force: true });
835
+ }
836
+ });
837
+
838
+ test('W19-NODE-MAP-SKILL wf-ui-control node-map deleteNodes uses typed Agent graph API and skips live agents', async () => {
839
+ const root = makeHarnessTempRoot('wf-ui-node-map-api-test-');
840
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
841
+ const mainAgentId = 'session-w19-main-agent';
842
+ const workerAgentId = 'session-w19-live-worker';
843
+ writeWorkflowGraphMap(root, {
844
+ nodes: [
845
+ { nodeId: mainAgentId, sessionId: 'w19-main-agent', runtime: 'codex', agentKind: 'main', role: 'Main Agent', status: 'running' },
846
+ { nodeId: workerAgentId, sessionId: 'w19-live-worker', runtime: 'codex', agentKind: 'subagent', role: 'Subagent', status: 'running' },
847
+ ],
848
+ positions: {
849
+ [mainAgentId]: { x: 20, y: 40 },
850
+ [workerAgentId]: { x: 260, y: 40 },
851
+ },
852
+ });
853
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: new SessionRegistry() });
854
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
855
+ try {
856
+ const created = await postJson(localBaseUrl, started.token, '/api/workflow/nodes', {
857
+ type: 'markdown',
858
+ title: 'W19 API Controlled Note',
859
+ });
860
+ assert.equal(created.status, 201, JSON.stringify(created.body));
861
+ const targetNodeId = created.body.node.nodeId;
862
+
863
+ const script = path.join(process.cwd(), 'Harness', 'scripts', 'wf-ui-control.mjs');
864
+ const read = await runNode([
865
+ script,
866
+ 'node-map',
867
+ '--action',
868
+ 'readGraph',
869
+ '--project',
870
+ root,
871
+ ], {
872
+ cwd: root,
873
+ timeout: 5000,
874
+ env: {
875
+ ...process.env,
876
+ HARNESS_AGENT_KIND: 'main',
877
+ HARNESS_PEER_SESSION_ID: 'w19-main-agent',
878
+ HARNESS_WORKFLOW_NODE_ID: mainAgentId,
879
+ HARNESS_WF_UI_URL: localBaseUrl,
880
+ HARNESS_WF_UI_TOKEN: started.token,
881
+ },
882
+ });
883
+ assert.equal(read.status, 0, read.stderr || read.stdout);
884
+ const readBody = JSON.parse(read.stdout);
885
+ assert.equal(readBody.action, 'agent.readGraph');
886
+ assert.ok(readBody.result?.graph?.nodes?.some(node => node.nodeId === targetNodeId), JSON.stringify(readBody));
887
+
888
+ const result = await runNode([
889
+ script,
890
+ 'node-map',
891
+ '--action',
892
+ 'deleteNodes',
893
+ '--all',
894
+ 'true',
895
+ '--project',
896
+ root,
897
+ ], {
898
+ cwd: root,
899
+ timeout: 5000,
900
+ env: {
901
+ ...process.env,
902
+ HARNESS_AGENT_KIND: 'main',
903
+ HARNESS_PEER_SESSION_ID: 'w19-main-agent',
904
+ HARNESS_WORKFLOW_NODE_ID: mainAgentId,
905
+ HARNESS_WF_UI_URL: localBaseUrl,
906
+ HARNESS_WF_UI_TOKEN: started.token,
907
+ },
908
+ });
909
+
910
+ assert.equal(result.status, 0, result.stderr || result.stdout);
911
+ const body = JSON.parse(result.stdout);
912
+ assert.equal(body.action, 'agent.deleteNodes');
913
+ assert.equal(body.result?.operation?.kind, 'agent.deleteNodes');
914
+ assert.ok(body.result?.deletedNodeIds?.includes(targetNodeId), JSON.stringify(body));
915
+ assert.ok(body.result?.skippedNodeIds?.includes(mainAgentId), JSON.stringify(body));
916
+ assert.ok(body.result?.skippedNodeIds?.includes(workerAgentId), JSON.stringify(body));
917
+
918
+ const nodes = await fetchJson(localBaseUrl, started.token, '/api/workflow/nodes');
919
+ assert.equal(nodes.status, 200);
920
+ assert.equal(nodes.body.nodes.some(node => node.nodeId === targetNodeId), false);
921
+ assert.equal(nodes.body.nodes.some(node => node.nodeId === mainAgentId), true);
922
+ assert.equal(nodes.body.nodes.some(node => node.nodeId === workerAgentId), true);
923
+
924
+ const workflow = await fetchJson(localBaseUrl, started.token, '/api/workflow');
925
+ assert.equal(workflow.status, 200, JSON.stringify(workflow.body));
926
+ assert.ok(workflow.body.workflow?.operations?.recent?.some(record => record.kind === 'agent.deleteNodes'));
927
+ } finally {
928
+ await stopServer(started.server);
929
+ fs.rmSync(root, { recursive: true, force: true });
930
+ }
931
+ });
932
+
933
+ test('W19-NODE-MAP-SKILL wf-ui-control legacy connect and delete-node delegate to typed Agent graph API', async () => {
934
+ const root = makeHarnessTempRoot('wf-ui-node-map-legacy-test-');
935
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
936
+ const mainAgentId = 'session-w19-legacy-main';
937
+ writeWorkflowGraphMap(root, {
938
+ nodes: [
939
+ { nodeId: mainAgentId, sessionId: 'w19-legacy-main', runtime: 'codex', agentKind: 'main', role: 'Main Agent', status: 'running' },
940
+ ],
941
+ positions: {
942
+ [mainAgentId]: { x: 20, y: 40 },
943
+ },
944
+ });
945
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: new SessionRegistry() });
946
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
947
+ const env = {
948
+ ...process.env,
949
+ HARNESS_AGENT_KIND: 'main',
950
+ HARNESS_PEER_SESSION_ID: 'w19-legacy-main',
951
+ HARNESS_WORKFLOW_NODE_ID: mainAgentId,
952
+ HARNESS_WF_UI_URL: localBaseUrl,
953
+ HARNESS_WF_UI_TOKEN: started.token,
954
+ };
955
+ try {
956
+ const created = await postJson(localBaseUrl, started.token, '/api/workflow/nodes', {
957
+ type: 'markdown',
958
+ title: 'W19 Legacy Alias Note',
959
+ });
960
+ assert.equal(created.status, 201, JSON.stringify(created.body));
961
+ const targetNodeId = created.body.node.nodeId;
962
+ const script = path.join(process.cwd(), 'Harness', 'scripts', 'wf-ui-control.mjs');
963
+
964
+ const connected = await runNode([
965
+ script,
966
+ 'connect',
967
+ '--from',
968
+ mainAgentId,
969
+ '--to',
970
+ targetNodeId,
971
+ '--relation',
972
+ 'context',
973
+ '--project',
974
+ root,
975
+ ], { cwd: root, timeout: 5000, env });
976
+ assert.equal(connected.status, 0, connected.stderr || connected.stdout);
977
+ const connectBody = JSON.parse(connected.stdout);
978
+ assert.equal(connectBody.action, 'agent.connectNodes');
979
+ assert.equal(connectBody.result?.operation?.kind, 'agent.connectNodes');
980
+
981
+ const removed = await runNode([
982
+ script,
983
+ 'delete-node',
984
+ '--node',
985
+ targetNodeId,
986
+ '--project',
987
+ root,
988
+ ], { cwd: root, timeout: 5000, env });
989
+ assert.equal(removed.status, 0, removed.stderr || removed.stdout);
990
+ const removeBody = JSON.parse(removed.stdout);
991
+ assert.equal(removeBody.action, 'agent.deleteNode');
992
+ assert.equal(removeBody.result?.operation?.kind, 'agent.deleteNode');
993
+ assert.deepEqual(removeBody.result?.deletedNodeIds, [targetNodeId]);
994
+ } finally {
995
+ await stopServer(started.server);
996
+ fs.rmSync(root, { recursive: true, force: true });
997
+ }
998
+ });
999
+
1000
+ test('W20-WORKFLOW-CLI wf-ui-control exposes canonical workflow context and typed node action wrappers', async () => {
1001
+ const root = makeHarnessTempRoot('wf-ui-workflow-cli-test-');
1002
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
1003
+ const mainAgentId = 'session-w20-main-agent';
1004
+ writeWorkflowGraphMap(root, {
1005
+ nodes: [
1006
+ { nodeId: mainAgentId, sessionId: 'w20-main-agent', runtime: 'codex', agentKind: 'main', role: 'Main Agent', status: 'running' },
1007
+ ],
1008
+ positions: {
1009
+ [mainAgentId]: { x: 20, y: 40 },
1010
+ },
1011
+ });
1012
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: new SessionRegistry() });
1013
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
1014
+ const env = {
1015
+ ...process.env,
1016
+ HARNESS_AGENT_KIND: 'main',
1017
+ HARNESS_PEER_SESSION_ID: 'w20-main-agent',
1018
+ HARNESS_WORKFLOW_NODE_ID: mainAgentId,
1019
+ HARNESS_WF_UI_URL: localBaseUrl,
1020
+ HARNESS_WF_UI_TOKEN: started.token,
1021
+ };
1022
+ try {
1023
+ const created = await postJson(localBaseUrl, started.token, '/api/workflow/nodes', {
1024
+ type: 'markdown',
1025
+ title: 'W20 Canonical Note',
1026
+ markdown: 'W20 typed action body',
1027
+ });
1028
+ assert.equal(created.status, 201, JSON.stringify(created.body));
1029
+ const targetNodeId = created.body.node.nodeId;
1030
+
1031
+ const connected = await postJson(localBaseUrl, started.token, `/api/workflow/nodes/${mainAgentId}/actions/agent.connectNodes`, {
1032
+ actorNodeId: mainAgentId,
1033
+ from: mainAgentId,
1034
+ to: targetNodeId,
1035
+ relation: 'context',
1036
+ sourceHandle: 'context',
1037
+ targetHandle: 'markdown',
1038
+ });
1039
+ assert.equal(connected.status, 200, JSON.stringify(connected.body));
1040
+
1041
+ const script = path.join(process.cwd(), 'Harness', 'scripts', 'wf-ui-control.mjs');
1042
+ const context = await runNode([
1043
+ script,
1044
+ 'workflow-context',
1045
+ '--node',
1046
+ mainAgentId,
1047
+ '--project',
1048
+ root,
1049
+ ], { cwd: root, timeout: 5000, env });
1050
+ assert.equal(context.status, 0, context.stderr || context.stdout);
1051
+ const contextBody = JSON.parse(context.stdout);
1052
+ assert.equal(contextBody.context.nodeId, mainAgentId);
1053
+ assert.ok(contextBody.context.connectedResourceRefs.some(ref => ref.nodeId === targetNodeId && ref.type === 'markdown'), JSON.stringify(contextBody));
1054
+
1055
+ const readOnlyContext = await runNode([
1056
+ script,
1057
+ 'workflow-context',
1058
+ '--node',
1059
+ mainAgentId,
1060
+ '--project',
1061
+ root,
1062
+ ], {
1063
+ cwd: root,
1064
+ timeout: 5000,
1065
+ env: {
1066
+ ...process.env,
1067
+ HARNESS_AGENT_KIND: 'subagent',
1068
+ HARNESS_PEER_SESSION_ID: 'w20-main-agent',
1069
+ HARNESS_WORKFLOW_NODE_ID: mainAgentId,
1070
+ HARNESS_WF_UI_URL: localBaseUrl,
1071
+ HARNESS_WF_UI_TOKEN: '',
1072
+ HARNESS_WF_UI_READ_TOKEN: graphReadToken(started.token, 'w20-main-agent'),
1073
+ },
1074
+ });
1075
+ assert.equal(readOnlyContext.status, 0, readOnlyContext.stderr || readOnlyContext.stdout);
1076
+ const readOnlyContextBody = JSON.parse(readOnlyContext.stdout);
1077
+ assert.equal(readOnlyContextBody.context.nodeId, mainAgentId);
1078
+
1079
+ const action = await runNode([
1080
+ script,
1081
+ 'workflow-node-action',
1082
+ '--node',
1083
+ targetNodeId,
1084
+ '--action',
1085
+ 'markdown.read',
1086
+ '--project',
1087
+ root,
1088
+ ], { cwd: root, timeout: 5000, env });
1089
+ assert.equal(action.status, 0, action.stderr || action.stdout);
1090
+ const actionBody = JSON.parse(action.stdout);
1091
+ assert.equal(actionBody.action, 'markdown.read');
1092
+ assert.equal(actionBody.result.markdown, 'W20 typed action body');
1093
+
1094
+ const graph = await runNode([
1095
+ script,
1096
+ 'workflow-node-map',
1097
+ '--action',
1098
+ 'readGraph',
1099
+ '--project',
1100
+ root,
1101
+ ], { cwd: root, timeout: 5000, env });
1102
+ assert.equal(graph.status, 0, graph.stderr || graph.stdout);
1103
+ const graphBody = JSON.parse(graph.stdout);
1104
+ assert.equal(graphBody.action, 'agent.readGraph');
1105
+ assert.ok(graphBody.result?.graph?.nodes?.some(node => node.nodeId === targetNodeId), JSON.stringify(graphBody));
1106
+ } finally {
1107
+ await stopServer(started.server);
1108
+ fs.rmSync(root, { recursive: true, force: true });
1109
+ }
1110
+ });
1111
+
1112
+ test('W27-CLI-GATE wf-ui-control resolves main actor from backend without actor-kind and read-node returns canonical snapshots', async () => {
1113
+ const root = makeHarnessTempRoot('wf-ui-cli-gate-test-');
1114
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
1115
+ const mainAgentId = 'session-w27-main-agent';
1116
+ const subagentId = 'session-w27-subagent';
1117
+ writeWorkflowGraphMap(root, {
1118
+ nodes: [
1119
+ { nodeId: mainAgentId, sessionId: 'w27-main-agent', runtime: 'codex', agentKind: 'main', role: 'Main Agent', status: 'running' },
1120
+ { nodeId: subagentId, sessionId: 'w27-subagent', runtime: 'codex', agentKind: 'subagent', role: 'Subagent', status: 'running' },
1121
+ ],
1122
+ positions: {
1123
+ [mainAgentId]: { x: 20, y: 40 },
1124
+ [subagentId]: { x: 260, y: 40 },
1125
+ },
1126
+ });
1127
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: new SessionRegistry() });
1128
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
1129
+ const script = path.join(process.cwd(), 'Harness', 'scripts', 'wf-ui-control.mjs');
1130
+ const env = {
1131
+ ...process.env,
1132
+ HARNESS_PEER_SESSION_ID: 'w27-main-agent',
1133
+ HARNESS_WORKFLOW_NODE_ID: mainAgentId,
1134
+ HARNESS_WF_UI_URL: localBaseUrl,
1135
+ HARNESS_WF_UI_TOKEN: started.token,
1136
+ };
1137
+ delete env.HARNESS_AGENT_KIND;
1138
+ try {
1139
+ const created = await postJson(localBaseUrl, started.token, '/api/workflow/nodes', {
1140
+ type: 'markdown',
1141
+ title: 'W27 Gate Note',
1142
+ markdown: 'W27 read-node markdown body',
1143
+ });
1144
+ assert.equal(created.status, 201, JSON.stringify(created.body));
1145
+ const markdownNodeId = created.body.node.nodeId;
1146
+
1147
+ const timer = await postJson(localBaseUrl, started.token, '/api/workflow/nodes', {
1148
+ type: 'timer',
1149
+ title: 'W27 Gate Timer',
1150
+ enabled: true,
1151
+ schedule: { mode: 'interval', intervalSeconds: 60 },
1152
+ controlPolicy: { agentCanSetInterval: true, minIntervalSeconds: 10, maxIntervalSeconds: 3600 },
1153
+ });
1154
+ assert.equal(timer.status, 201, JSON.stringify(timer.body));
1155
+ const timerNodeId = timer.body.node.nodeId;
1156
+
1157
+ const connected = await runNode([
1158
+ script,
1159
+ 'connect',
1160
+ '--from',
1161
+ mainAgentId,
1162
+ '--to',
1163
+ markdownNodeId,
1164
+ '--relation',
1165
+ 'context',
1166
+ '--project',
1167
+ root,
1168
+ ], { cwd: root, timeout: 5000, env });
1169
+ assert.equal(connected.status, 0, connected.stderr || connected.stdout);
1170
+ const connectBody = JSON.parse(connected.stdout);
1171
+ assert.equal(connectBody.action, 'agent.connectNodes');
1172
+ assert.equal(connectBody.result?.operation?.kind, 'agent.connectNodes');
1173
+
1174
+ const eventRead = await runNode([
1175
+ script,
1176
+ 'read-node',
1177
+ '--node',
1178
+ timerNodeId,
1179
+ '--project',
1180
+ root,
1181
+ ], { cwd: root, timeout: 5000, env });
1182
+ assert.equal(eventRead.status, 0, eventRead.stderr || eventRead.stdout);
1183
+ const eventBody = JSON.parse(eventRead.stdout);
1184
+ assert.equal(eventBody.ok, true);
1185
+ assert.equal(eventBody.node.nodeId, timerNodeId);
1186
+ assert.equal(eventBody.node.kind, 'timer');
1187
+ assert.equal(eventBody.node.lifecycle, 'event-source');
1188
+
1189
+ const componentRead = await runNode([
1190
+ script,
1191
+ 'readnode',
1192
+ '--node',
1193
+ markdownNodeId,
1194
+ '--project',
1195
+ root,
1196
+ ], { cwd: root, timeout: 5000, env });
1197
+ assert.equal(componentRead.status, 0, componentRead.stderr || componentRead.stdout);
1198
+ const componentBody = JSON.parse(componentRead.stdout);
1199
+ assert.equal(componentBody.node.nodeId, markdownNodeId);
1200
+ assert.equal(componentBody.node.kind, 'markdown');
1201
+
1202
+ const agentRead = await runNode([
1203
+ script,
1204
+ 'read-snapshot',
1205
+ '--node',
1206
+ 'w27-main-agent',
1207
+ '--project',
1208
+ root,
1209
+ ], { cwd: root, timeout: 5000, env });
1210
+ assert.equal(agentRead.status, 0, agentRead.stderr || agentRead.stdout);
1211
+ const agentBody = JSON.parse(agentRead.stdout);
1212
+ assert.equal(agentBody.node.nodeId, mainAgentId);
1213
+ assert.equal(agentBody.node.kind, 'agent');
1214
+ assert.equal(agentBody.node.settings?.values?.agentKind, 'main');
1215
+
1216
+ const subagentEnv = {
1217
+ ...env,
1218
+ HARNESS_WORKFLOW_NODE_ID: subagentId,
1219
+ HARNESS_PEER_SESSION_ID: 'w27-subagent',
1220
+ };
1221
+ const rejected = await runNode([
1222
+ script,
1223
+ 'node-map',
1224
+ '--action',
1225
+ 'readGraph',
1226
+ '--project',
1227
+ root,
1228
+ ], { cwd: root, timeout: 5000, env: subagentEnv });
1229
+ assert.notEqual(rejected.status, 0);
1230
+ assert.match(rejected.stderr, /Main Agent/);
1231
+ } finally {
1232
+ await stopServer(started.server);
1233
+ fs.rmSync(root, { recursive: true, force: true });
1234
+ }
1235
+ });
1236
+
1237
+ test('W24-WORKFLOW-CLI create-agent binds graph node and delegate/read-agent use graph identity', async () => {
1238
+ const root = makeHarnessTempRoot('wf-ui-agent-delegation-cli-test-');
1239
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
1240
+ const registry = new SessionRegistry();
1241
+ const mainAgentId = 'session-w24-main-agent';
1242
+ const mainSessionId = 'w24-main-agent';
1243
+ writeWorkflowGraphMap(root, {
1244
+ nodes: [
1245
+ {
1246
+ nodeId: mainAgentId,
1247
+ sessionId: mainSessionId,
1248
+ runtime: 'codex',
1249
+ agentKind: 'main',
1250
+ role: 'Main Agent',
1251
+ status: 'running',
1252
+ },
1253
+ ],
1254
+ positions: {
1255
+ [mainAgentId]: { x: 20, y: 40 },
1256
+ },
1257
+ });
1258
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
1259
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
1260
+ const script = path.join(process.cwd(), 'Harness', 'scripts', 'wf-ui-control.mjs');
1261
+ const env = {
1262
+ ...process.env,
1263
+ HARNESS_AGENT_KIND: 'main',
1264
+ HARNESS_PEER_SESSION_ID: mainSessionId,
1265
+ HARNESS_WORKFLOW_NODE_ID: mainAgentId,
1266
+ HARNESS_WF_UI_URL: localBaseUrl,
1267
+ HARNESS_WF_UI_TOKEN: started.token,
1268
+ HARNESS_PEER_RUNTIME: 'codex',
1269
+ };
1270
+ const written = [];
1271
+ try {
1272
+ const created = await runNode([
1273
+ script,
1274
+ 'create-agent',
1275
+ '--runtime',
1276
+ 'codex',
1277
+ '--role',
1278
+ 'W24 Worker',
1279
+ '--objective',
1280
+ 'W24 graph-bound worker',
1281
+ '--defer-pty-spawn',
1282
+ 'true',
1283
+ '--project',
1284
+ root,
1285
+ ], { cwd: root, timeout: 5000, env });
1286
+ assert.equal(created.status, 0, created.stderr || created.stdout);
1287
+ const createdBody = JSON.parse(created.stdout);
1288
+ assert.ok(createdBody.graphNodeId, JSON.stringify(createdBody));
1289
+ assert.equal(createdBody.parentNodeId, mainAgentId);
1290
+ assert.equal(createdBody.parentAgentId, mainSessionId);
1291
+
1292
+ const graphPath = path.join(root, 'Harness', 'a2a', 'workflow-map.json');
1293
+ const graph = JSON.parse(fs.readFileSync(graphPath, 'utf8'));
1294
+ const graphNode = (graph.nodes || []).find(node => node.nodeId === createdBody.graphNodeId);
1295
+ assert.ok(graphNode, 'create-agent should persist a graph node endpoint');
1296
+ assert.equal(graphNode.sessionId, createdBody.sessionId);
1297
+ assert.equal(graphNode.kind, 'terminal-session');
1298
+
1299
+ registry.update(createdBody.sessionId, { status: 'running' });
1300
+ const workerSession = registry.get(createdBody.sessionId);
1301
+ registerPtyProcess(createdBody.sessionId, { write: data => written.push(String(data)) });
1302
+
1303
+ const edge = await postJson(localBaseUrl, started.token, '/api/workflow/edges', {
1304
+ from: mainAgentId,
1305
+ to: createdBody.graphNodeId,
1306
+ relation: 'delegation',
1307
+ });
1308
+ assert.equal(edge.status, 201, JSON.stringify(edge.body));
1309
+ assert.equal(edge.body.edge.relation, 'delegation');
1310
+
1311
+ const context = await runNode([
1312
+ script,
1313
+ 'workflow-context',
1314
+ '--node',
1315
+ mainAgentId,
1316
+ '--project',
1317
+ root,
1318
+ ], { cwd: root, timeout: 5000, env });
1319
+ assert.equal(context.status, 0, context.stderr || context.stdout);
1320
+ const contextBody = JSON.parse(context.stdout);
1321
+ const workerRef = contextBody.context.connectedAgentRefs.find(ref => ref.nodeId === createdBody.graphNodeId);
1322
+ assert.ok(workerRef, JSON.stringify(contextBody.context.connectedAgentRefs));
1323
+ assert.equal(workerRef.relation, 'delegation');
1324
+ assert.equal(workerRef.delegation.sendAction, 'agent.sendInput');
1325
+
1326
+ const delegated = await runNode([
1327
+ script,
1328
+ 'delegate-agent',
1329
+ '--node',
1330
+ createdBody.graphNodeId,
1331
+ '--text',
1332
+ 'W24_DELEGATE_PING',
1333
+ '--project',
1334
+ root,
1335
+ ], { cwd: root, timeout: 5000, env });
1336
+ assert.equal(delegated.status, 0, delegated.stderr || delegated.stdout);
1337
+ const delegateBody = JSON.parse(delegated.stdout);
1338
+ assert.equal(delegateBody.action, 'agent.sendInput');
1339
+ assert.equal(delegateBody.result.sessionId, createdBody.sessionId);
1340
+ assert.equal(written[0], 'W', 'the delegated body must start typing synchronously');
1341
+ await waitFor(() => (written.join('') === 'W24_DELEGATE_PING\r' ? written : null), { timeout: 8000 });
1342
+ assert.equal(written.join(''), 'W24_DELEGATE_PING\r', 'the typed body must join to the exact text + \\r');
1343
+ assert.equal(written[written.length - 1], '\r', 'the delegated submit must end with a single \\r');
1344
+ assert.equal(written.join('').includes('\n'), false, 'no \\n may be injected');
1345
+ assert.equal(written.filter(w => w === '\r').length, 1, 'exactly one \\r (0x0D)');
1346
+
1347
+ appendTerminalData(root, workerSession, 'W24_WORKER_OK\n', 'stdout');
1348
+ const read = await runNode([
1349
+ script,
1350
+ 'read-agent',
1351
+ '--node',
1352
+ createdBody.graphNodeId,
1353
+ '--action',
1354
+ 'transcript',
1355
+ '--tail',
1356
+ '20',
1357
+ '--project',
1358
+ root,
1359
+ ], { cwd: root, timeout: 5000, env });
1360
+ assert.equal(read.status, 0, read.stderr || read.stdout);
1361
+ const readBody = JSON.parse(read.stdout);
1362
+ assert.equal(readBody.action, 'agent.readTranscript');
1363
+ assert.equal(readBody.result.sessionId, createdBody.sessionId);
1364
+ assert.ok(readBody.result.entries.some(entry => entry.stream === 'stdin' && entry.data.includes('W24_DELEGATE_PING')), JSON.stringify(readBody.result.entries));
1365
+ assert.ok(readBody.result.entries.some(entry => entry.stream === 'stdout' && entry.data.includes('W24_WORKER_OK')), JSON.stringify(readBody.result.entries));
1366
+ } finally {
1367
+ for (const session of registry.getAll()) unregisterPtyProcess(session.sessionId);
1368
+ await stopServer(started.server);
1369
+ fs.rmSync(root, { recursive: true, force: true });
1370
+ }
1371
+ });
1372
+
1373
+ test('W25-ENV-NODE-INIT graph start records env/node init only and preserves explicit initialInput', async () => {
1374
+ const root = makeHarnessTempRoot('wf-ui-env-node-init-test-');
1375
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
1376
+ const registry = new SessionRegistry();
1377
+ const graphNodeId = 'session-w25-bootstrap-agent';
1378
+ const explicitGraphNodeId = 'session-w25-explicit-agent';
1379
+ persistSession(root, {
1380
+ sessionId: 'w25-bootstrap-previous',
1381
+ runtime: 'codex',
1382
+ agentKind: 'subagent',
1383
+ role: 'W25 Bootstrap Agent',
1384
+ objective: 'Read connected graph context before acting',
1385
+ projectRoot: root,
1386
+ cwd: root,
1387
+ graphNodeId,
1388
+ status: 'stopped',
1389
+ });
1390
+ persistSession(root, {
1391
+ sessionId: 'w25-explicit-previous',
1392
+ runtime: 'codex',
1393
+ agentKind: 'subagent',
1394
+ role: 'W25 Explicit Agent',
1395
+ objective: 'Use explicit startup input',
1396
+ projectRoot: root,
1397
+ cwd: root,
1398
+ graphNodeId: explicitGraphNodeId,
1399
+ status: 'stopped',
1400
+ });
1401
+ writeWorkflowGraphMap(root, {
1402
+ nodes: [
1403
+ {
1404
+ nodeId: graphNodeId,
1405
+ sessionId: 'w25-bootstrap-previous',
1406
+ runtime: 'codex',
1407
+ agentKind: 'subagent',
1408
+ role: 'W25 Bootstrap Agent',
1409
+ objective: 'Read connected graph context before acting',
1410
+ status: 'stopped',
1411
+ },
1412
+ {
1413
+ nodeId: explicitGraphNodeId,
1414
+ sessionId: 'w25-explicit-previous',
1415
+ runtime: 'codex',
1416
+ agentKind: 'subagent',
1417
+ role: 'W25 Explicit Agent',
1418
+ objective: 'Use explicit startup input',
1419
+ status: 'stopped',
1420
+ },
1421
+ ],
1422
+ positions: {
1423
+ [graphNodeId]: { x: 80, y: 120 },
1424
+ [explicitGraphNodeId]: { x: 360, y: 120 },
1425
+ },
1426
+ });
1427
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
1428
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
1429
+ try {
1430
+ const bootstrap = await postJson(localBaseUrl, started.token, `/api/a2a/nodes/${graphNodeId}/start`, {});
1431
+ assert.equal(bootstrap.status, 200, JSON.stringify(bootstrap.body));
1432
+ assert.equal(bootstrap.body.graphNodeId, graphNodeId);
1433
+ assert.equal(bootstrap.body.started.status, 'blocked');
1434
+
1435
+ const bootstrapState = listTerminalSessions(root).find(session => session.sessionId === bootstrap.body.sessionId);
1436
+ assert.equal(bootstrapState.pendingInitialInputMode, undefined);
1437
+ assert.equal(bootstrapState.pendingInitialInputBytes, undefined);
1438
+ assert.equal(fs.existsSync(bootstrapState.nodeInitPath), true);
1439
+ const init = fs.readFileSync(bootstrapState.nodeInitPath, 'utf8');
1440
+ assert.match(init, /HARNESS_NODE_INIT/);
1441
+ assert.match(init, /workflow-ontology/);
1442
+ assert.match(init, /## Working Method — discovery first/);
1443
+ assert.match(init, /hydrate EVERY turn, mandatory/);
1444
+ assert.match(init, /workflow-context --project \./);
1445
+ assert.match(init, /## Invariant Rules/);
1446
+ assert.doesNotMatch(init, /short bootstrap prompt/i);
1447
+
1448
+ const bootstrapEvents = readJsonlFile(path.join(root, 'Harness', 'a2a', 'sessions', bootstrap.body.sessionId, 'events.jsonl'));
1449
+ const pendingBootstrap = bootstrapEvents.find(event => event.type === 'terminal.input.pending');
1450
+ assert.equal(pendingBootstrap, undefined);
1451
+
1452
+ const explicitInput = 'CUSTOM_INITIAL_INPUT\r';
1453
+ const explicit = await postJson(localBaseUrl, started.token, `/api/a2a/nodes/${explicitGraphNodeId}/start`, {
1454
+ initialInput: explicitInput,
1455
+ });
1456
+ assert.equal(explicit.status, 200, JSON.stringify(explicit.body));
1457
+ assert.equal(explicit.body.graphNodeId, explicitGraphNodeId);
1458
+
1459
+ const explicitState = listTerminalSessions(root).find(session => session.sessionId === explicit.body.sessionId);
1460
+ assert.equal(explicitState.pendingInitialInputMode, 'explicit');
1461
+ assert.equal(explicitState.pendingInitialInputBytes, explicitInput.length);
1462
+ const explicitEvents = readJsonlFile(path.join(root, 'Harness', 'a2a', 'sessions', explicit.body.sessionId, 'events.jsonl'));
1463
+ const pendingExplicit = explicitEvents.find(event => event.type === 'terminal.input.pending');
1464
+ assert.equal(pendingExplicit.mode, 'explicit');
1465
+ assert.equal(pendingExplicit.bytes, explicitInput.length);
1466
+ } finally {
1467
+ await stopServer(started.server);
1468
+ fs.rmSync(root, { recursive: true, force: true });
1469
+ }
1470
+ });
1471
+
1472
+ test('W26-INITIAL-INPUT waits for terminal ready output before injecting explicit initial input', () => {
1473
+ assert.equal(terminalReadyForInitialInput('\u001b[1t', 'claude'), false);
1474
+ assert.equal(
1475
+ terminalReadyForInitialInput('\u001b[HClaude Code v2.1.215\r\n❯ \r\n⏵⏵ bypass permissions on', 'claude'),
1476
+ true,
1477
+ );
1478
+ assert.equal(terminalReadyForInitialInput('Codex ready ›', 'codex'), true);
1479
+ });
1480
+
1481
+ test('codex update prompt response writes selected menu input and clears pending request', async () => {
1482
+ const root = makeHarnessTempRoot('wf-ui-codex-update-test-');
1483
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
1484
+ const registry = new SessionRegistry();
1485
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
1486
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
1487
+ const written = [];
1488
+ try {
1489
+ const session = registry.create({ runtime: 'codex' });
1490
+ registry.update(session.sessionId, {
1491
+ status: 'running',
1492
+ controlRequest: {
1493
+ requestId: 'codex-update-test',
1494
+ type: 'codex:update-prompt',
1495
+ status: 'pending',
1496
+ detectedAt: new Date().toISOString(),
1497
+ },
1498
+ });
1499
+ registerPtyProcess(session.sessionId, { write: (data) => written.push(data) });
1500
+
1501
+ const first = await postJson(localBaseUrl, started.token, `/api/sessions/${session.sessionId}/codex-update-prompt`, { choice: 'skip-session' });
1502
+ assert.equal(first.status, 200);
1503
+ assert.equal(first.body.ok, true);
1504
+ assert.equal(written.at(-1), CODEX_UPDATE_SKIP_SESSION_KEYS);
1505
+ assert.equal(registry.get(session.sessionId).controlRequest, null);
1506
+
1507
+ registry.update(session.sessionId, {
1508
+ controlRequest: {
1509
+ requestId: 'codex-update-test-2',
1510
+ type: 'codex:update-prompt',
1511
+ status: 'pending',
1512
+ detectedAt: new Date().toISOString(),
1513
+ },
1514
+ });
1515
+ const second = await postJson(localBaseUrl, started.token, `/api/sessions/${session.sessionId}/codex-update-prompt`, { choice: 'skip-until-next-version' });
1516
+ assert.equal(second.status, 200);
1517
+ assert.equal(second.body.ok, true);
1518
+ assert.equal(written.at(-1), CODEX_UPDATE_SKIP_UNTIL_NEXT_VERSION_KEYS);
1519
+ assert.equal(registry.get(session.sessionId).controlRequest, null);
1520
+ } finally {
1521
+ for (const session of registry.getAll()) unregisterPtyProcess(session.sessionId);
1522
+ await stopServer(started.server);
1523
+ fs.rmSync(root, { recursive: true, force: true });
1524
+ }
1525
+ });
1526
+
1527
+ test('codex update prompt response is single-use under concurrent requests', async () => {
1528
+ const root = makeHarnessTempRoot('wf-ui-codex-update-lock-test-');
1529
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
1530
+ const registry = new SessionRegistry();
1531
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
1532
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
1533
+ const written = [];
1534
+ try {
1535
+ const session = registry.create({ runtime: 'codex' });
1536
+ registry.update(session.sessionId, {
1537
+ status: 'running',
1538
+ controlRequest: {
1539
+ requestId: 'codex-update-concurrent-test',
1540
+ type: 'codex:update-prompt',
1541
+ status: 'pending',
1542
+ detectedAt: new Date().toISOString(),
1543
+ },
1544
+ });
1545
+ registerPtyProcess(session.sessionId, { write: (data) => written.push(data) });
1546
+
1547
+ const results = await Promise.all([
1548
+ postJson(localBaseUrl, started.token, `/api/sessions/${session.sessionId}/codex-update-prompt`, { choice: 'skip-session' }),
1549
+ postJson(localBaseUrl, started.token, `/api/sessions/${session.sessionId}/codex-update-prompt`, { choice: 'skip-session' }),
1550
+ ]);
1551
+
1552
+ assert.deepEqual(results.map(result => result.status).sort(), [200, 400]);
1553
+ assert.deepEqual(written, [CODEX_UPDATE_SKIP_SESSION_KEYS]);
1554
+ assert.equal(registry.get(session.sessionId).controlRequest, null);
1555
+ } finally {
1556
+ for (const session of registry.getAll()) unregisterPtyProcess(session.sessionId);
1557
+ await stopServer(started.server);
1558
+ fs.rmSync(root, { recursive: true, force: true });
1559
+ }
1560
+ });
1561
+
1562
+ test('AC-CLEANUP-003 cleanup API previews and prunes eligible stopped sessions', async () => {
1563
+ const root = makeHarnessTempRoot('wf-ui-cleanup-api-test-');
1564
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
1565
+ const registry = new SessionRegistry();
1566
+ const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
1567
+ const localBaseUrl = `http://127.0.0.1:${started.port}`;
1568
+ const sessionDir = path.join(root, 'Harness', 'a2a', 'sessions', 'session-cleanup-old');
1569
+ try {
1570
+ fs.mkdirSync(sessionDir, { recursive: true });
1571
+ fs.writeFileSync(path.join(sessionDir, 'STATE.json'), JSON.stringify({
1572
+ sessionId: 'session-cleanup-old',
1573
+ runtime: 'codex',
1574
+ status: 'saved',
1575
+ updatedAt: '2026-01-01T00:00:00.000Z',
1576
+ terminalSeq: 1,
1577
+ }, null, 2));
1578
+ fs.writeFileSync(path.join(sessionDir, 'terminal.jsonl'), '{"seq":1,"data":"done"}\n');
1579
+
1580
+ const summary = await fetchJson(localBaseUrl, started.token, '/api/cleanup/summary');
1581
+ assert.equal(summary.status, 200);
1582
+ assert.equal(Object.hasOwn(summary.body, '_internal'), false);
1583
+ assert.ok(summary.body.sessions.eligibleCount >= 1);
1584
+ assert.ok(summary.body.targets.sessions.some(target => target.sessionId === 'session-cleanup-old'));
1585
+
1586
+ const result = await postJson(localBaseUrl, started.token, '/api/cleanup/prune', {
1587
+ apply: true,
1588
+ policy: {
1589
+ stoppedSessionRetentionDays: 1,
1590
+ keepStoppedSessions: 20,
1591
+ includeTaskSessions: false,
1592
+ detachedLogRetentionHours: 24,
1593
+ },
1594
+ });
1595
+ assert.equal(result.status, 200);
1596
+ assert.equal(result.body.applied, true);
1597
+ assert.equal(fs.existsSync(sessionDir), false);
1598
+ } finally {
1599
+ await stopServer(started.server);
1600
+ fs.rmSync(root, { recursive: true, force: true });
1601
+ }
1602
+ });