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
@@ -0,0 +1,1991 @@
1
+ import { describe, it, before, after } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import crypto from 'node:crypto';
6
+
7
+ // ── Helpers ──
8
+ function tempProjectRoot() {
9
+ const id = crypto.randomBytes(4).toString('hex');
10
+ const dir = path.join(process.cwd(), '.tmp-runtime-test-' + id);
11
+ fs.mkdirSync(path.join(dir, 'Harness', 'a2a', 'component-nodes'), { recursive: true });
12
+ fs.writeFileSync(path.join(dir, 'Harness', 'a2a', 'component-nodes', 'index.json'), JSON.stringify({ schemaVersion: 1, nodes: [] }));
13
+ fs.mkdirSync(path.join(dir, 'Harness', 'a2a'), { recursive: true });
14
+ fs.writeFileSync(path.join(dir, 'Harness', 'a2a', 'workflow-map.json'), JSON.stringify({ schemaVersion: 1, version: 1, nodes: [], edges: [], positions: {}, undoStack: [], redoStack: [], deletedNodes: [] }));
15
+ return dir;
16
+ }
17
+
18
+ function cleanup(dir) {
19
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
20
+ }
21
+
22
+ function seedAgentGraphNode(root, nodeId = 'session-event-agent-01', overrides = {}) {
23
+ const graph = runtime.loadWorkflowGraphMap(root);
24
+ const sessionId = overrides.sessionId || `${nodeId}-pty`;
25
+ runtime.writeWorkflowGraphMap(root, {
26
+ ...graph,
27
+ version: graph.version + 1,
28
+ nodes: [
29
+ ...(graph.nodes || []),
30
+ {
31
+ nodeId,
32
+ sessionId,
33
+ agentKind: overrides.agentKind || 'main',
34
+ runtime: overrides.runtime || 'codex',
35
+ status: overrides.status || 'stopped',
36
+ label: overrides.label || 'Event Agent',
37
+ cwd: root,
38
+ taskId: null,
39
+ role: overrides.role || overrides.agentKind || 'main',
40
+ ...(overrides.config ? { config: overrides.config } : {}),
41
+ ...(overrides.nodeConfig ? { nodeConfig: overrides.nodeConfig } : {}),
42
+ },
43
+ ],
44
+ positions: {
45
+ ...(graph.positions || {}),
46
+ [nodeId]: { x: 100, y: 120 },
47
+ },
48
+ });
49
+ return nodeId;
50
+ }
51
+
52
+ function seedActiveTask(root, taskId = 'task-runtime-goal') {
53
+ fs.mkdirSync(path.join(root, 'Harness', 'tasks', taskId), { recursive: true });
54
+ fs.writeFileSync(path.join(root, 'Harness', 'PROGRESS.md'), `# PROGRESS.md\n\n## Active Task\n\n- ${taskId}\n`, 'utf8');
55
+ fs.writeFileSync(path.join(root, 'Harness', 'tasks', taskId, 'STATE.json'), JSON.stringify({
56
+ schemaVersion: 1,
57
+ taskId,
58
+ status: 'active',
59
+ mode: 'direct',
60
+ tier: 'WF-Standard',
61
+ phase: 'implement',
62
+ gate: 'TEST-GATE',
63
+ updatedAt: '2026-08-05T00:00:00.000Z',
64
+ activeQuestion: null,
65
+ nextAction: 'Implement Advanced Timer and Goal node.',
66
+ acceptance: [
67
+ { id: 'W14-TIMER', status: 'tracked', text: 'Advanced Timer supports base heartbeat and watchdog control.' },
68
+ { id: 'W14-GOAL', status: 'tracked', text: 'Goal node exposes objective, acceptance, and two-phase completion.' },
69
+ ],
70
+ links: { dependsOn: [], blocks: [], related: [] },
71
+ }, null, 2), 'utf8');
72
+ fs.writeFileSync(path.join(root, 'Harness', 'tasks', taskId, 'PLAN.md'), '# Goal plan\n', 'utf8');
73
+ fs.writeFileSync(path.join(root, 'Harness', 'tasks', taskId, 'PROGRESS.md'), '# Goal progress\n', 'utf8');
74
+ return `goal-${taskId}`;
75
+ }
76
+
77
+ let runtime;
78
+ let graphStore;
79
+
80
+ before(async () => {
81
+ runtime = await import('../workflow-node-runtime.mjs');
82
+ graphStore = await import('../workflow-graph-store.mjs');
83
+ });
84
+
85
+ // ── Node CRUD ──
86
+ describe('Workflow Node Runtime', () => {
87
+ let root;
88
+ before(() => { root = tempProjectRoot(); });
89
+ after(() => cleanup(root));
90
+
91
+ it('creates a markdown node with settings', async () => {
92
+ const result = await runtime.createNode(root, { type: 'markdown', title: 'Test MD' });
93
+ assert.ok(result.ok);
94
+ assert.equal(result.node.kind, 'markdown');
95
+ assert.ok(result.state);
96
+ assert.equal(result.state.type, 'markdown');
97
+ assert.equal(result.node.ui.labels.title, 'Test MD');
98
+ assert.deepEqual(result.node.graph.handles, {
99
+ inputs: [],
100
+ outputs: [],
101
+ bidirectional: ['markdown'],
102
+ ports: ['markdown'],
103
+ physical: ['markdown:left', 'markdown:right', 'markdown:top', 'markdown:bottom'],
104
+ });
105
+ });
106
+
107
+ it('creates an excalidraw node', async () => {
108
+ const result = await runtime.createNode(root, { type: 'excalidraw', title: 'Test Draw' });
109
+ assert.ok(result.ok);
110
+ assert.equal(result.node.kind, 'excalidraw');
111
+ assert.ok(result.state.scene);
112
+ assert.ok(Array.isArray(result.state.scene.elements));
113
+ assert.deepEqual(result.node.graph.handles, {
114
+ inputs: [],
115
+ outputs: [],
116
+ bidirectional: ['scene'],
117
+ ports: ['scene'],
118
+ physical: ['scene:left', 'scene:right', 'scene:top', 'scene:bottom'],
119
+ });
120
+ });
121
+
122
+ it('creates a file node', async () => {
123
+ const result = await runtime.createNode(root, { type: 'file', title: 'Test File', file: { source: 'workspace', path: 'README.md', name: 'README.md', mime: 'text/markdown', size: 100 } });
124
+ assert.ok(result.ok);
125
+ assert.equal(result.node.kind, 'file');
126
+ assert.equal(result.state.file.name, 'README.md');
127
+ });
128
+
129
+ it('lists and connects only live component nodes when stale state files remain in the index', async () => {
130
+ const localRoot = tempProjectRoot();
131
+ try {
132
+ const live = await runtime.createNode(localRoot, { type: 'markdown', title: 'Live Markdown', markdown: 'here' });
133
+ const stale = await runtime.createNode(localRoot, { type: 'markdown', title: 'Stale Markdown', markdown: 'gone' });
134
+ fs.rmSync(path.join(localRoot, stale.node.stateRef.path), { force: true });
135
+
136
+ const listed = await runtime.listNodes(localRoot);
137
+ assert.equal(listed.nodes.some(node => node.nodeId === stale.node.nodeId), false);
138
+ assert.equal(listed.nodes.some(node => node.nodeId === live.node.nodeId), true);
139
+ assert.throws(
140
+ () => graphStore.connectNodes(localRoot, { from: stale.node.nodeId, to: live.node.nodeId }),
141
+ /Workflow node not found|ENDPOINT_NOT_FOUND/i,
142
+ );
143
+
144
+ const deleted = await runtime.deleteNode(localRoot, stale.node.nodeId);
145
+ assert.equal(deleted.ok, true);
146
+ assert.equal(deleted.nodeId, stale.node.nodeId);
147
+ assert.equal(runtime.listComponentNodes(localRoot).some(node => node.nodeId === stale.node.nodeId), false);
148
+ } finally {
149
+ cleanup(localRoot);
150
+ }
151
+ });
152
+
153
+ it('W11 creates a timer event node with directed event ports', async () => {
154
+ const result = await runtime.createNode(root, {
155
+ type: 'timer',
156
+ title: 'Daily Standup Timer',
157
+ schedule: { mode: 'manual', intervalSeconds: 900 },
158
+ payloadTemplate: { prompt: 'Summarize current workflow status.' },
159
+ });
160
+ assert.ok(result.ok);
161
+ assert.equal(result.node.kind, 'timer');
162
+ assert.equal(result.state.type, 'timer');
163
+ assert.equal(result.node.ui.testId, 'workflow-event-node');
164
+ assert.deepEqual(result.node.graph.handles, {
165
+ inputs: ['config'],
166
+ outputs: ['event'],
167
+ bidirectional: ['status'],
168
+ ports: ['event', 'config', 'status'],
169
+ physical: ['config:left', 'event:right', 'event:top', 'event:bottom'],
170
+ directions: {
171
+ event: 'source-to-target',
172
+ config: 'target-only',
173
+ status: 'bidirectional',
174
+ },
175
+ });
176
+ assert.ok(result.node.capabilities.includes('timer.fire'));
177
+ assert.equal(result.node.contentRef.kind, 'event-node-state');
178
+ });
179
+
180
+ it('W14-TIMER creates an Advanced Timer with base heartbeat, WDT, cadence, and control policy', async () => {
181
+ const result = await runtime.createNode(root, {
182
+ type: 'timer',
183
+ title: 'Advanced Heartbeat Timer',
184
+ enabled: true,
185
+ schedule: {
186
+ mode: 'loop',
187
+ intervalSeconds: 30,
188
+ cadence: { kind: 'sequence', sequenceSeconds: [15, 45, 90] },
189
+ },
190
+ heartbeat: {
191
+ base: { enabled: true, intervalSeconds: 30 },
192
+ watchdog: { enabled: true, intervalSeconds: 600, timeoutSeconds: 1800 },
193
+ },
194
+ whileGuard: { source: 'agentStatus', op: 'neq', value: 'stopped' },
195
+ taskBinding: { taskId: 'task-runtime-goal', phases: ['implement'], stopWhenClosed: true },
196
+ controlPolicy: { agentCanDisable: true, agentCanSetInterval: true, minIntervalSeconds: 5, maxIntervalSeconds: 7200 },
197
+ });
198
+
199
+ assert.equal(result.node.kind, 'timer');
200
+ assert.equal(result.state.schedule.mode, 'loop');
201
+ assert.deepEqual(result.state.schedule.cadence.sequenceSeconds, [15, 45, 90]);
202
+ assert.equal(result.state.heartbeat.base.enabled, true);
203
+ assert.equal(result.state.heartbeat.base.intervalSeconds, 30);
204
+ assert.equal(result.state.heartbeat.watchdog.enabled, true);
205
+ assert.equal(result.state.heartbeat.watchdog.intervalSeconds, 600);
206
+ assert.equal(result.state.heartbeat.watchdog.state, 'ok');
207
+ assert.equal(result.state.whileGuard.source, 'agentStatus');
208
+ assert.equal(result.state.taskBinding.taskId, 'task-runtime-goal');
209
+ for (const action of ['timer.enable', 'timer.disable', 'timer.setInterval', 'timer.setMode', 'timer.ackWatchdog', 'timer.resetWatchdog', 'timer.tick']) {
210
+ assert.ok(result.node.capabilities.includes(action), `missing ${action}`);
211
+ }
212
+ });
213
+
214
+ it('W16-TIMER persists Once, Adaptive, and Watchdog modes from the expanded editor contract', async () => {
215
+ const localRoot = tempProjectRoot();
216
+ try {
217
+ const timer = await runtime.createNode(localRoot, {
218
+ type: 'timer',
219
+ title: 'Expanded Timer',
220
+ enabled: true,
221
+ schedule: { mode: 'loop', intervalSeconds: 60, cadence: { kind: 'fixed' } },
222
+ heartbeat: {
223
+ base: { enabled: true, intervalSeconds: 60 },
224
+ watchdog: { enabled: false, intervalSeconds: 600, timeoutSeconds: 1800 },
225
+ },
226
+ });
227
+
228
+ const once = await runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.configure', {
229
+ title: 'One shot reminder',
230
+ enabled: true,
231
+ schedule: {
232
+ mode: 'once',
233
+ intervalSeconds: 90,
234
+ triggerAt: '2026-08-05T15:30:00.000Z',
235
+ cadence: { kind: 'fixed' },
236
+ },
237
+ heartbeat: {
238
+ base: { enabled: false, intervalSeconds: 90 },
239
+ watchdog: { enabled: false, intervalSeconds: 900, timeoutSeconds: 1800 },
240
+ },
241
+ });
242
+ assert.equal(once.result.schedule.mode, 'once');
243
+ assert.equal(once.result.schedule.triggerAt, '2026-08-05T15:30:00.000Z');
244
+ assert.equal(once.node.ui.labels.title, 'One shot reminder');
245
+
246
+ const adaptive = await runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.configure', {
247
+ schedule: {
248
+ mode: 'adaptive',
249
+ intervalSeconds: 45,
250
+ cadence: { kind: 'backoff', backoffFactor: 2.5, maxIntervalSeconds: 3600 },
251
+ },
252
+ heartbeat: {
253
+ base: { enabled: true, intervalSeconds: 45 },
254
+ watchdog: { enabled: true, intervalSeconds: 900, timeoutSeconds: 2700 },
255
+ },
256
+ });
257
+ assert.equal(adaptive.result.schedule.mode, 'adaptive');
258
+ assert.equal(adaptive.result.schedule.cadence.kind, 'backoff');
259
+ assert.equal(adaptive.result.heartbeat.watchdog.enabled, true);
260
+
261
+ const watchdog = await runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.setMode', {
262
+ mode: 'watchdog',
263
+ });
264
+ assert.equal(watchdog.result.state.schedule.mode, 'watchdog');
265
+ } finally {
266
+ cleanup(localRoot);
267
+ }
268
+ });
269
+
270
+ it('release-core/backend-gating rejects github-trigger creation without experimental opt-in', async () => {
271
+ await assert.rejects(
272
+ () => runtime.createNode(root, {
273
+ type: 'github-trigger',
274
+ title: 'Default GitHub Trigger',
275
+ }),
276
+ (error) => {
277
+ assert.equal(error.code, 'PLANNED_NODE_REQUIRES_EXPERIMENTAL');
278
+ assert.equal(error.statusCode, 409);
279
+ return true;
280
+ },
281
+ );
282
+ });
283
+
284
+ it('release-core/backend-gating marks github-trigger as planned experimental-only ontology', () => {
285
+ const ontology = runtime.workflowOntology();
286
+ assert.equal(ontology.nodeTypes['github-trigger'].status, 'planned');
287
+ assert.equal(ontology.nodeTypes['github-trigger'].experimentalOnly, true);
288
+ assert.equal(ontology.nodeTypes['github-trigger'].createRequiresExperimental, true);
289
+ });
290
+
291
+ it('W14 creates a github-trigger event node with directed event ports and bounded metadata', async () => {
292
+ const result = await runtime.createNode(root, {
293
+ type: 'github-trigger',
294
+ experimental: true,
295
+ title: 'Repo Activity Trigger',
296
+ repository: { owner: 'zingspark', name: 'create-harness-vibe-coding' },
297
+ eventFilters: { events: ['pull_request', 'push'], actions: ['opened', 'synchronize'], branches: ['main'] },
298
+ payloadTemplate: { prompt: 'Summarize the GitHub event for the connected Agent.' },
299
+ });
300
+ assert.ok(result.ok);
301
+ assert.equal(result.node.kind, 'github-trigger');
302
+ assert.equal(result.state.type, 'github-trigger');
303
+ assert.equal(result.state.repository.fullName, 'zingspark/create-harness-vibe-coding');
304
+ assert.deepEqual(result.state.eventFilters.events, ['pull_request', 'push']);
305
+ assert.equal(result.node.ui.testId, 'workflow-event-node');
306
+ assert.deepEqual(result.node.graph.handles, {
307
+ inputs: ['config'],
308
+ outputs: ['event'],
309
+ bidirectional: ['status'],
310
+ ports: ['event', 'config', 'status'],
311
+ physical: ['config:left', 'event:right', 'event:top', 'event:bottom'],
312
+ directions: {
313
+ event: 'source-to-target',
314
+ config: 'target-only',
315
+ status: 'bidirectional',
316
+ },
317
+ });
318
+ assert.ok(result.node.capabilities.includes('github-trigger.receive'));
319
+ assert.equal(result.node.contentRef.kind, 'event-node-state');
320
+ assert.equal(result.node.contentRef.eventKind, 'github-trigger');
321
+ assert.equal(result.node.settings.schemaId, 'github-trigger-settings');
322
+
323
+ const serialized = JSON.stringify(result);
324
+ for (const forbidden of ['"token"', '"secret"', '"authorization"', '"headers"', '"rawBody"', '"body"']) {
325
+ assert.equal(serialized.includes(forbidden), false, `github-trigger leaked forbidden field ${forbidden}`);
326
+ }
327
+ });
328
+
329
+ it('W12 creates a skill-group capability node with bidirectional capability ports', async () => {
330
+ const result = await runtime.createNode(root, {
331
+ type: 'skill-group',
332
+ title: 'Workflow Skill Pack',
333
+ description: 'Curated local workflow skills',
334
+ sourceGroup: { id: 'family:wf', label: 'WF Skills', kind: 'family' },
335
+ skills: [
336
+ { id: 'skill:wf-ui', name: 'wf-ui', title: 'WF-UI Adapter', description: 'Open the control panel', source: 'project' },
337
+ { id: 'skill:wf', name: 'wf', title: 'WF Mode', description: 'Workflow mode', source: 'project' },
338
+ { id: 'skill:wf-ui-duplicate', name: 'wf-ui', title: 'Duplicate ignored', source: 'project' },
339
+ ],
340
+ });
341
+ assert.ok(result.ok);
342
+ assert.equal(result.node.kind, 'skill-group');
343
+ assert.equal(result.node.lifecycle, 'capability-provider');
344
+ assert.equal(result.node.ui.testId, 'workflow-capability-node');
345
+ assert.equal(result.state.type, 'skill-group');
346
+ assert.deepEqual(result.state.skillNames, ['wf-ui', 'wf']);
347
+ assert.deepEqual(result.node.graph.handles, {
348
+ inputs: [],
349
+ outputs: [],
350
+ bidirectional: ['capability'],
351
+ ports: ['capability'],
352
+ physical: ['capability:left', 'capability:right', 'capability:top', 'capability:bottom'],
353
+ directions: {
354
+ capability: 'bidirectional',
355
+ },
356
+ });
357
+ assert.ok(result.node.capabilities.includes('skill-group.read'));
358
+ assert.equal(result.node.contentRef.kind, 'capability-node-state');
359
+ assert.ok(result.node.stateRef.path.startsWith('Harness/a2a/capability-nodes/'));
360
+ assert.ok(!JSON.stringify(result).includes('SKILL.md body'));
361
+ });
362
+
363
+ it('W13 creates an mcp-connector capability node from Hub metadata without spawning or leaking secrets', async () => {
364
+ const sentinel = path.join(root, 'mcp-runtime-spawned.txt');
365
+ fs.writeFileSync(path.join(root, '.mcp.json'), JSON.stringify({
366
+ mcpServers: {
367
+ github: {
368
+ command: process.execPath,
369
+ args: ['-e', `require('fs').writeFileSync(${JSON.stringify(sentinel)}, 'spawned')`, '@modelcontextprotocol/server-github'],
370
+ env: {
371
+ GITHUB_TOKEN: 'ghp_secret_value',
372
+ },
373
+ url: 'https://api.github.test/mcp/token/path-secret?token=query-secret',
374
+ },
375
+ },
376
+ }, null, 2));
377
+
378
+ const result = await runtime.createNode(root, {
379
+ type: 'mcp-connector',
380
+ mcpServerId: 'mcp:project-mcp:github',
381
+ title: 'GitHub MCP Connector',
382
+ });
383
+
384
+ assert.ok(result.ok);
385
+ assert.equal(fs.existsSync(sentinel), false, 'mcp-connector creation must not spawn the MCP command');
386
+ assert.equal(result.node.kind, 'mcp-connector');
387
+ assert.equal(result.node.lifecycle, 'capability-provider');
388
+ assert.equal(result.node.ui.testId, 'workflow-capability-node');
389
+ assert.equal(result.state.type, 'mcp-connector');
390
+ assert.equal(result.state.serverCount, 1);
391
+ assert.deepEqual(result.state.serverNames, ['github']);
392
+ assert.deepEqual(result.state.transports, ['http']);
393
+ assert.deepEqual(result.state.envKeyNames, ['GITHUB_TOKEN']);
394
+ assert.equal(result.state.servers[0].url, 'https://api.github.test/mcp/token/redacted');
395
+ assert.deepEqual(result.node.graph.handles, {
396
+ inputs: [],
397
+ outputs: [],
398
+ bidirectional: ['capability'],
399
+ ports: ['capability'],
400
+ physical: ['capability:left', 'capability:right', 'capability:top', 'capability:bottom'],
401
+ directions: {
402
+ capability: 'bidirectional',
403
+ },
404
+ });
405
+ assert.ok(result.node.capabilities.includes('mcp-connector.read'));
406
+ assert.equal(result.node.contentRef.capabilityKind, 'mcp-connector');
407
+ assert.equal(result.node.settings.schemaId, 'mcp-connector-settings');
408
+
409
+ const serialized = JSON.stringify(result);
410
+ assert.equal(serialized.includes('ghp_secret_value'), false);
411
+ assert.equal(serialized.includes('path-secret'), false);
412
+ assert.equal(serialized.includes('query-secret'), false);
413
+ assert.equal(serialized.includes('@modelcontextprotocol/server-github'), false);
414
+ assert.equal(serialized.includes('writeFileSync'), false);
415
+ assert.equal(serialized.includes(String(sentinel)), false);
416
+ for (const forbidden of ['"args"', '"env"', '"command"', '"raw"', '"rawConfig"', '"mcpConfig"', '"configBody"', '"body"', '"toolSchema"', '"resources"']) {
417
+ assert.equal(serialized.includes(forbidden), false, `mcp-connector leaked forbidden field ${forbidden}`);
418
+ }
419
+ });
420
+
421
+ it('lists created nodes', async () => {
422
+ const result = await runtime.listNodes(root);
423
+ assert.ok(Array.isArray(result.nodes));
424
+ assert.ok(result.nodes.length >= 3, `expected >=3, got ${result.nodes.length}`);
425
+ });
426
+
427
+ it('reads a single node snapshot', async () => {
428
+ const all = await runtime.listNodes(root);
429
+ const nodeId = all.nodes[0].nodeId;
430
+ const result = await runtime.getNode(root, nodeId);
431
+ assert.ok(result.node);
432
+ assert.equal(result.node.nodeId, nodeId);
433
+ assert.ok(result.state);
434
+ });
435
+
436
+ it('updates node state with revision guard', async () => {
437
+ const all = await runtime.listNodes(root);
438
+ const md = all.nodes.find(n => n.kind === 'markdown');
439
+ assert.ok(md, 'markdown node not found');
440
+ const result = await runtime.updateNodeState(root, md.nodeId, { revision: md.version, markdown: '# Hello World' });
441
+ assert.ok(result.ok);
442
+ assert.ok(result.state.markdown.includes('Hello World'));
443
+ assert.ok(result.revision > md.version);
444
+ });
445
+
446
+ it('rejects stale revision update', async () => {
447
+ const all = await runtime.listNodes(root);
448
+ const md = all.nodes.find(n => n.kind === 'markdown');
449
+ await assert.rejects(
450
+ () => runtime.updateNodeState(root, md.nodeId, { revision: 0, markdown: 'stale' }),
451
+ );
452
+ });
453
+
454
+ it('updates node settings', async () => {
455
+ const all = await runtime.listNodes(root);
456
+ const md = all.nodes.find(n => n.kind === 'markdown');
457
+ const result = await runtime.updateNodeSettings(root, md.nodeId, { editorMode: 'source', autoSave: true });
458
+ assert.ok(result.settings);
459
+ assert.ok(result.settings.values);
460
+ assert.equal(result.settings.values.editorMode, 'source');
461
+ assert.equal(result.settings.values.autoSave, true);
462
+ assert.ok(result.settings.revision >= 1);
463
+ });
464
+
465
+ it('executes markdown.read action', async () => {
466
+ const all = await runtime.listNodes(root);
467
+ const md = all.nodes.find(n => n.kind === 'markdown');
468
+ const result = await runtime.executeNodeAction(root, md.nodeId, 'markdown.read', {});
469
+ assert.ok(result.ok);
470
+ assert.equal(result.action, 'markdown.read');
471
+ assert.ok(typeof result.result.markdown === 'string');
472
+ });
473
+
474
+ it('executes excalidraw.saveScene and revision increases', async () => {
475
+ const all = await runtime.listNodes(root);
476
+ const draw = all.nodes.find(n => n.kind === 'excalidraw');
477
+ const scene = { elements: [{ id: 'e1', type: 'rectangle', x: 10, y: 20, width: 100, height: 80, strokeColor: '#6965DB', backgroundColor: '#e8e7ff' }], appState: { viewBackgroundColor: '#ffffff' }, files: {} };
478
+ const result = await runtime.executeNodeAction(root, draw.nodeId, 'excalidraw.saveScene', { scene });
479
+ assert.ok(result.ok);
480
+ assert.ok(result.result.revision > draw.version);
481
+ assert.ok(result.result.scene.elements.length > 0);
482
+ });
483
+
484
+ it('executes excalidraw.renderPreview returns non-empty', async () => {
485
+ const all = await runtime.listNodes(root);
486
+ const draw = all.nodes.find(n => n.kind === 'excalidraw');
487
+ const result = await runtime.executeNodeAction(root, draw.nodeId, 'excalidraw.renderPreview', {});
488
+ assert.ok(result.ok);
489
+ assert.ok(Array.isArray(result.result.preview.elements));
490
+ assert.ok(result.result.preview.hasContent);
491
+ });
492
+
493
+ it('executes file.readMeta', async () => {
494
+ const all = await runtime.listNodes(root);
495
+ const fileNode = all.nodes.find(n => n.kind === 'file');
496
+ const result = await runtime.executeNodeAction(root, fileNode.nodeId, 'file.readMeta', {});
497
+ assert.ok(result.ok);
498
+ assert.equal(result.result.file.name, 'README.md');
499
+ });
500
+
501
+ it('W11 executes timer.fire and records the latest event', async () => {
502
+ const all = await runtime.listNodes(root);
503
+ const timer = all.nodes.find(n => n.kind === 'timer');
504
+ assert.ok(timer, 'timer node must exist from W11 create test');
505
+ const result = await runtime.executeNodeAction(root, timer.nodeId, 'timer.fire', { payload: { reason: 'test-fire' } });
506
+ assert.ok(result.ok);
507
+ assert.equal(result.action, 'timer.fire');
508
+ assert.equal(result.node.kind, 'timer');
509
+ assert.equal(result.result.event.kind, 'timer.fire');
510
+ assert.equal(result.result.state.eventCount, 1);
511
+ assert.deepEqual(result.result.event.payload.reason, 'test-fire');
512
+ });
513
+
514
+ it('W14 executes github-trigger.receive and records only a bounded delivery summary', async () => {
515
+ const all = await runtime.listNodes(root);
516
+ const trigger = all.nodes.find(n => n.kind === 'github-trigger');
517
+ assert.ok(trigger, 'github-trigger node must exist from W14 create test');
518
+ const result = await runtime.executeNodeAction(root, trigger.nodeId, 'github-trigger.receive', {
519
+ event: 'pull_request',
520
+ action: 'opened',
521
+ deliveryId: 'delivery-123',
522
+ repository: { owner: 'zingspark', name: 'create-harness-vibe-coding' },
523
+ ref: 'refs/heads/main',
524
+ sender: 'octocat',
525
+ pullRequest: { number: 42, title: 'Add workflow trigger', url: 'https://github.example.test/pull/42' },
526
+ body: { token: 'ghp_secret_value', large: 'must not persist' },
527
+ headers: { authorization: 'Bearer secret' },
528
+ });
529
+ assert.ok(result.ok);
530
+ assert.equal(result.action, 'github-trigger.receive');
531
+ assert.equal(result.node.kind, 'github-trigger');
532
+ assert.equal(result.result.event.kind, 'github.pull_request');
533
+ assert.equal(result.result.event.action, 'opened');
534
+ assert.equal(result.result.event.deliveryId, 'delivery-123');
535
+ assert.equal(result.result.state.eventCount, 1);
536
+ assert.equal(result.result.state.lastEvent.pullRequest.number, 42);
537
+
538
+ const serialized = JSON.stringify(result);
539
+ assert.equal(serialized.includes('ghp_secret_value'), false);
540
+ assert.equal(serialized.includes('Bearer secret'), false);
541
+ assert.equal(serialized.includes('must not persist'), false);
542
+ for (const forbidden of ['"headers"', '"authorization"', '"rawBody"', '"body"', '"secret"', '"token"']) {
543
+ assert.equal(serialized.includes(forbidden), false, `github-trigger receive leaked forbidden field ${forbidden}`);
544
+ }
545
+ });
546
+
547
+ it('rejects unknown action', async () => {
548
+ const all = await runtime.listNodes(root);
549
+ const md = all.nodes.find(n => n.kind === 'markdown');
550
+ await assert.rejects(
551
+ () => runtime.executeNodeAction(root, md.nodeId, 'markdown.flyToMoon', {}),
552
+ );
553
+ });
554
+
555
+ it('deletes a node and cleans up', async () => {
556
+ const all = await runtime.listNodes(root);
557
+ const fileNode = all.nodes.find(n => n.kind === 'file');
558
+ const result = await runtime.deleteNode(root, fileNode.nodeId);
559
+ assert.ok(result.ok);
560
+ const after = await runtime.listNodes(root);
561
+ assert.ok(!after.nodes.some(n => n.nodeId === fileNode.nodeId));
562
+ });
563
+
564
+ it('restores a deleted component and its graph connections', async () => {
565
+ const localRoot = tempProjectRoot();
566
+ try {
567
+ const source = await runtime.createNode(localRoot, { type: 'markdown', title: 'Recoverable Notes', markdown: 'restore me' });
568
+ const target = await runtime.createNode(localRoot, { type: 'excalidraw', title: 'Recoverable Sketch' });
569
+ graphStore.connectNodes(localRoot, {
570
+ from: source.node.nodeId,
571
+ to: target.node.nodeId,
572
+ relation: 'context',
573
+ sourceHandle: 'markdown',
574
+ targetHandle: 'scene',
575
+ });
576
+
577
+ const deleted = await runtime.deleteNode(localRoot, target.node.nodeId);
578
+ assert.ok(deleted.ok);
579
+ assert.equal(runtime.listComponentNodes(localRoot).some(node => node.nodeId === target.node.nodeId), false);
580
+ assert.equal(runtime.loadWorkflowGraphMap(localRoot).edges.length, 0);
581
+
582
+ const restored = await runtime.restoreNode(localRoot, target.node.nodeId, { recovery: deleted.recovery });
583
+ assert.ok(restored.ok);
584
+ assert.equal(restored.node.nodeId, target.node.nodeId);
585
+ assert.equal(runtime.listComponentNodes(localRoot).some(node => node.nodeId === target.node.nodeId), true);
586
+ const graph = runtime.loadWorkflowGraphMap(localRoot);
587
+ assert.equal(graph.nodes.some(node => node.nodeId === target.node.nodeId), true);
588
+ assert.equal(graph.edges.some(edge => edge.source === source.node.nodeId && edge.target === target.node.nodeId), true);
589
+ } finally {
590
+ cleanup(localRoot);
591
+ }
592
+ });
593
+
594
+ it('deletes and restores a Goal anchor without losing the active task', async () => {
595
+ const localRoot = tempProjectRoot();
596
+ try {
597
+ const goalNodeId = seedActiveTask(localRoot, 'task-runtime-goal-recovery');
598
+ const deleted = await runtime.deleteNode(localRoot, goalNodeId);
599
+ assert.ok(deleted.ok);
600
+ assert.equal((await runtime.listNodes(localRoot)).nodes.some(node => node.nodeId === goalNodeId), false);
601
+ await assert.rejects(() => runtime.getNodeContext(localRoot, goalNodeId), error => error?.code === 'NOT_FOUND');
602
+ await assert.rejects(() => runtime.executeNodeAction(localRoot, goalNodeId, 'goal.update', { nextAction: 'must stay deleted' }), error => error?.code === 'NOT_FOUND');
603
+ assert.throws(
604
+ () => graphStore.connectNodes(localRoot, { from: goalNodeId, to: goalNodeId, relation: 'goal' }),
605
+ error => error?.code === 'ENDPOINT_NOT_FOUND',
606
+ );
607
+
608
+ const restored = await runtime.restoreNode(localRoot, goalNodeId, { recovery: deleted.recovery });
609
+ assert.ok(restored.ok);
610
+ assert.equal(restored.node.nodeId, goalNodeId);
611
+ assert.equal((await runtime.listNodes(localRoot)).nodes.some(node => node.nodeId === goalNodeId), true);
612
+ } finally {
613
+ cleanup(localRoot);
614
+ }
615
+ });
616
+
617
+ it('returns node context with connected peers', async () => {
618
+ const all = await runtime.listNodes(root);
619
+ const md = all.nodes.find(n => n.kind === 'markdown');
620
+ const ctx = await runtime.getNodeContext(root, md.nodeId);
621
+ assert.ok(ctx.node);
622
+ assert.ok(ctx.context);
623
+ assert.ok(Array.isArray(ctx.node.graph.connections));
624
+ });
625
+
626
+ it('AC-007 exposes workflow links as bidirectional runtime connections with endpoint roles', async () => {
627
+ const localRoot = tempProjectRoot();
628
+ try {
629
+ const source = await runtime.createNode(localRoot, { type: 'markdown', title: 'Source Notes' });
630
+ const target = await runtime.createNode(localRoot, { type: 'excalidraw', title: 'Target Sketch' });
631
+ graphStore.connectNodes(localRoot, {
632
+ from: source.node.nodeId,
633
+ to: target.node.nodeId,
634
+ relation: 'context',
635
+ sourceHandle: 'markdown',
636
+ targetHandle: 'scene',
637
+ });
638
+
639
+ const sourceContext = await runtime.getNodeContext(localRoot, source.node.nodeId);
640
+ const targetContext = await runtime.getNodeContext(localRoot, target.node.nodeId);
641
+ const sourceConnection = sourceContext.node.graph.connections.find(item => item.peerNodeId === target.node.nodeId);
642
+ const targetConnection = targetContext.node.graph.connections.find(item => item.peerNodeId === source.node.nodeId);
643
+
644
+ assert.deepEqual(sourceConnection, {
645
+ edgeId: `${source.node.nodeId}->${target.node.nodeId}`,
646
+ peerNodeId: target.node.nodeId,
647
+ endpointRole: 'source',
648
+ localHandle: 'markdown:right',
649
+ peerHandle: 'scene:left',
650
+ sourceHandle: 'markdown:right',
651
+ targetHandle: 'scene:left',
652
+ relation: 'context',
653
+ direction: 'bidirectional',
654
+ });
655
+ assert.deepEqual(targetConnection, {
656
+ edgeId: `${source.node.nodeId}->${target.node.nodeId}`,
657
+ peerNodeId: source.node.nodeId,
658
+ endpointRole: 'target',
659
+ localHandle: 'scene:left',
660
+ peerHandle: 'markdown:right',
661
+ sourceHandle: 'markdown:right',
662
+ targetHandle: 'scene:left',
663
+ relation: 'context',
664
+ direction: 'bidirectional',
665
+ });
666
+ } finally {
667
+ cleanup(localRoot);
668
+ }
669
+ });
670
+
671
+ it('W18-AGENT-GRAPH Main Agent executes graph actions through executeNodeAction and non-main actors are rejected', async () => {
672
+ const localRoot = tempProjectRoot();
673
+ try {
674
+ const mainAgentId = seedAgentGraphNode(localRoot, 'session-w18-main-agent', {
675
+ agentKind: 'main',
676
+ role: 'main',
677
+ label: 'W18 Main Agent',
678
+ });
679
+ const workerAgentId = seedAgentGraphNode(localRoot, 'session-w18-worker-agent', {
680
+ agentKind: 'subagent',
681
+ role: 'worker',
682
+ label: 'W18 Worker Agent',
683
+ status: 'running',
684
+ });
685
+ const targetNodeId = 'component-w18-runtime-notes';
686
+
687
+ const created = await runtime.executeNodeAction(localRoot, mainAgentId, 'agent.createNode', {
688
+ actorNodeId: mainAgentId,
689
+ nodeId: targetNodeId,
690
+ type: 'markdown',
691
+ title: 'W18 Runtime Notes',
692
+ position: { x: 240, y: 180 },
693
+ });
694
+ assert.equal(created.ok, true);
695
+ assert.ok(graphStore.getGraphSnapshot(localRoot).nodes.some(node => node.nodeId === targetNodeId));
696
+
697
+ const connected = await runtime.executeNodeAction(localRoot, mainAgentId, 'agent.connectNodes', {
698
+ actorNodeId: mainAgentId,
699
+ from: mainAgentId,
700
+ to: targetNodeId,
701
+ relation: 'context',
702
+ sourceHandle: 'context',
703
+ targetHandle: 'markdown',
704
+ });
705
+ assert.equal(connected.ok, true);
706
+ assert.ok(graphStore.getGraphSnapshot(localRoot).edges.some(edge => edge.from === mainAgentId && edge.to === targetNodeId));
707
+
708
+ const moved = await runtime.executeNodeAction(localRoot, mainAgentId, 'agent.moveNode', {
709
+ actorNodeId: mainAgentId,
710
+ targetNodeId,
711
+ position: { x: 410, y: 255 },
712
+ });
713
+ assert.equal(moved.ok, true);
714
+ assert.deepEqual(graphStore.getGraphSnapshot(localRoot).positions[targetNodeId], { x: 410, y: 255 });
715
+
716
+ const read = await runtime.executeNodeAction(localRoot, mainAgentId, 'agent.readGraph', {
717
+ actorNodeId: mainAgentId,
718
+ });
719
+ assert.equal(read.ok, true);
720
+ assert.ok(read.result?.graph?.nodes?.some(node => node.nodeId === targetNodeId));
721
+
722
+ const disconnected = await runtime.executeNodeAction(localRoot, mainAgentId, 'agent.disconnectNodes', {
723
+ actorNodeId: mainAgentId,
724
+ edgeId: `${mainAgentId}->${targetNodeId}`,
725
+ });
726
+ assert.equal(disconnected.ok, true);
727
+ assert.equal(graphStore.getGraphSnapshot(localRoot).edges.some(edge => edge.id === `${mainAgentId}->${targetNodeId}`), false);
728
+
729
+ const deleted = await runtime.executeNodeAction(localRoot, mainAgentId, 'agent.deleteNode', {
730
+ actorNodeId: mainAgentId,
731
+ targetNodeId,
732
+ });
733
+ assert.equal(deleted.ok, true);
734
+ assert.deepEqual(deleted.result.deletedNodeIds, [targetNodeId]);
735
+ assert.equal(graphStore.getGraphSnapshot(localRoot).nodes.some(node => node.nodeId === targetNodeId), false);
736
+
737
+ const batchA = await runtime.createNode(localRoot, {
738
+ nodeId: 'component-w18-delete-batch-a',
739
+ type: 'markdown',
740
+ title: 'W18 Delete Batch A',
741
+ });
742
+ const batchGoalId = seedActiveTask(localRoot, 'task-w18-delete-batch');
743
+ graphStore.connectNodes(localRoot, { from: mainAgentId, to: batchA.node.nodeId, relation: 'context' });
744
+ graphStore.connectNodes(localRoot, { from: mainAgentId, to: batchGoalId, relation: 'goal' });
745
+
746
+ const batch = await runtime.executeNodeAction(localRoot, mainAgentId, 'agent.deleteNodes', {
747
+ actorNodeId: mainAgentId,
748
+ all: true,
749
+ });
750
+ assert.equal(batch.ok, true);
751
+ assert.ok(batch.result.deletedNodeIds.includes(batchA.node.nodeId), JSON.stringify(batch.result));
752
+ assert.ok(batch.result.deletedNodeIds.includes(batchGoalId), JSON.stringify(batch.result));
753
+ assert.ok(batch.result.skippedNodeIds.includes(mainAgentId), JSON.stringify(batch.result));
754
+ const afterBatch = graphStore.getGraphSnapshot(localRoot);
755
+ assert.ok(afterBatch.nodes.some(node => node.nodeId === mainAgentId), 'Main Agent must not delete itself');
756
+ assert.equal(afterBatch.nodes.some(node => node.nodeId === batchA.node.nodeId), false);
757
+ assert.equal(afterBatch.nodes.some(node => node.nodeId === batchGoalId), false);
758
+ assert.equal((await runtime.listNodes(localRoot)).nodes.some(node => node.nodeId === batchGoalId), false, 'deleted virtual goal node must stay hidden from node runtime list');
759
+
760
+ await assert.rejects(
761
+ () => runtime.executeNodeAction(localRoot, mainAgentId, 'agent.deleteNode', {
762
+ actorNodeId: mainAgentId,
763
+ targetNodeId: mainAgentId,
764
+ }),
765
+ (error) => {
766
+ assert.equal(error.statusCode, 400);
767
+ assert.match(error.code || '', /CANNOT_DELETE_SELF/);
768
+ return true;
769
+ },
770
+ );
771
+
772
+ await assert.rejects(
773
+ () => runtime.executeNodeAction(localRoot, workerAgentId, 'agent.readGraph', {
774
+ actorNodeId: workerAgentId,
775
+ }),
776
+ (error) => {
777
+ assert.equal(error.statusCode, 403);
778
+ assert.match(error.code || '', /MAIN_AGENT_REQUIRED|AGENT_GRAPH_AUTH|UNAUTHORIZED|FORBIDDEN/);
779
+ return true;
780
+ },
781
+ );
782
+ } finally {
783
+ cleanup(localRoot);
784
+ }
785
+ });
786
+
787
+ it('W23 exposes connected Agent workers as delegateable context refs and ontology affordances', async () => {
788
+ const localRoot = tempProjectRoot();
789
+ try {
790
+ const mainAgentId = seedAgentGraphNode(localRoot, 'session-w23-main-agent', {
791
+ agentKind: 'main',
792
+ role: 'main',
793
+ label: 'W23 Main Agent',
794
+ status: 'running',
795
+ sessionId: 'w23-main-pty',
796
+ });
797
+ const workerAgentId = seedAgentGraphNode(localRoot, 'session-w23-worker-agent', {
798
+ agentKind: 'subagent',
799
+ role: 'worker',
800
+ label: 'W23 Worker Agent',
801
+ status: 'running',
802
+ sessionId: 'w23-worker-pty',
803
+ });
804
+
805
+ const edge = graphStore.connectNodes(localRoot, {
806
+ from: mainAgentId,
807
+ to: workerAgentId,
808
+ direction: 'bidirectional',
809
+ });
810
+ assert.equal(edge.edge.relation, 'delegation');
811
+
812
+ const context = await runtime.getNodeContext(localRoot, mainAgentId);
813
+ const workerRef = context.context.connectedAgentRefs.find(ref => ref.nodeId === workerAgentId);
814
+ assert.ok(workerRef, JSON.stringify(context.context.connectedAgentRefs));
815
+ assert.equal(workerRef.sessionId, 'w23-worker-pty');
816
+ assert.equal(workerRef.agentKind, 'subagent');
817
+ assert.equal(workerRef.delegation.canDelegate, true);
818
+ assert.equal(workerRef.delegation.state, 'ready');
819
+ assert.ok(workerRef.allowedActions.includes('agent.sendInput'));
820
+ assert.ok(workerRef.allowedActions.includes('agent.readOutput'));
821
+ assert.equal(workerRef.workspaceRef.path, 'Harness/a2a/nodes/w23-worker-pty');
822
+ assert.equal(workerRef.workspaceRef.initPath, 'Harness/a2a/nodes/w23-worker-pty/init.md');
823
+
824
+ const connectedPeer = context.context.connectedPeers.find(peer => peer.nodeId === workerAgentId);
825
+ assert.equal(connectedPeer.type, 'agent');
826
+ assert.equal(connectedPeer.sessionId, 'w23-worker-pty');
827
+
828
+ const affordance = context.context.affordances.find(item => item.nodeId === workerAgentId);
829
+ assert.equal(affordance.relationship, 'connected-agent-worker');
830
+ assert.equal(affordance.canDelegate, true);
831
+ assert.ok(affordance.allowedActions.includes('agent.sendInput'));
832
+ assert.ok(affordance.allowedActions.includes('agent.readTranscript'));
833
+ } finally {
834
+ cleanup(localRoot);
835
+ }
836
+ });
837
+ });
838
+
839
+ // ── Graph Store ──
840
+ describe('Workflow Graph Store', () => {
841
+ let root;
842
+ let nodeA;
843
+ let nodeB;
844
+ before(async () => {
845
+ root = tempProjectRoot();
846
+ // Create real component nodes so endpoint validation passes
847
+ const a = await runtime.createNode(root, { type: 'markdown', title: 'Graph Test A' });
848
+ const b = await runtime.createNode(root, { type: 'excalidraw', title: 'Graph Test B' });
849
+ nodeA = a.node.nodeId;
850
+ nodeB = b.node.nodeId;
851
+ });
852
+ after(() => cleanup(root));
853
+
854
+ it('connects two nodes semantically', () => {
855
+ const result = graphStore.connectNodes(root, { from: nodeA, to: nodeB, relation: 'wf-bridge' });
856
+ assert.ok(result.ok);
857
+ assert.ok(result.edge);
858
+ assert.equal(result.edge.from, nodeA);
859
+ assert.equal(result.edge.to, nodeB);
860
+ assert.equal(result.edge.sourceHandle, 'markdown:right');
861
+ assert.equal(result.edge.targetHandle, 'scene:left');
862
+ });
863
+
864
+ it('prevents duplicate edge', () => {
865
+ assert.throws(() => {
866
+ graphStore.connectNodes(root, { from: nodeA, to: nodeB });
867
+ }, /duplicate|exists|already/i);
868
+ });
869
+
870
+ it('AC-007 prevents reverse duplicate edges for bidirectional links', async () => {
871
+ const localRoot = tempProjectRoot();
872
+ try {
873
+ const a = await runtime.createNode(localRoot, { type: 'markdown', title: 'Reverse A' });
874
+ const b = await runtime.createNode(localRoot, { type: 'markdown', title: 'Reverse B' });
875
+ graphStore.connectNodes(localRoot, { from: a.node.nodeId, to: b.node.nodeId, sourceHandle: 'markdown', targetHandle: 'markdown' });
876
+ assert.throws(() => {
877
+ graphStore.connectNodes(localRoot, { from: b.node.nodeId, to: a.node.nodeId, sourceHandle: 'markdown', targetHandle: 'markdown' });
878
+ }, /duplicate|exists|already/i);
879
+ } finally {
880
+ cleanup(localRoot);
881
+ }
882
+ });
883
+
884
+ it('AC-007 preserves bare resource left/right physical side aliases', async () => {
885
+ const localRoot = tempProjectRoot();
886
+ try {
887
+ const a = await runtime.createNode(localRoot, { type: 'markdown', title: 'Physical A' });
888
+ const b = await runtime.createNode(localRoot, { type: 'excalidraw', title: 'Physical B' });
889
+ const edge = graphStore.connectNodes(localRoot, {
890
+ from: a.node.nodeId,
891
+ to: b.node.nodeId,
892
+ sourceHandle: 'left',
893
+ targetHandle: 'right',
894
+ });
895
+ assert.equal(edge.edge.sourceHandle, 'markdown:left');
896
+ assert.equal(edge.edge.targetHandle, 'scene:right');
897
+ const context = await runtime.getNodeContext(localRoot, a.node.nodeId);
898
+ assert.equal(context.node.graph.connections[0].sourceHandle, 'markdown:left');
899
+ assert.equal(context.node.graph.connections[0].targetHandle, 'scene:right');
900
+ assert.equal(context.node.graph.connections[0].direction, 'bidirectional');
901
+ } finally {
902
+ cleanup(localRoot);
903
+ }
904
+ });
905
+
906
+ it('AC-007 strips display labels and physical sides from semantic relations', async () => {
907
+ const localRoot = tempProjectRoot();
908
+ try {
909
+ const a = await runtime.createNode(localRoot, { type: 'markdown', title: 'Relation A' });
910
+ const b = await runtime.createNode(localRoot, { type: 'excalidraw', title: 'Relation B' });
911
+ const c = await runtime.createNode(localRoot, { type: 'file', title: 'Relation File', file: { source: 'workspace', path: 'rel.txt', name: 'rel.txt', mime: 'text/plain', size: 0 } });
912
+
913
+ const displayLabel = graphStore.connectNodes(localRoot, {
914
+ from: a.node.nodeId,
915
+ to: b.node.nodeId,
916
+ relation: 'markdown <-> scene',
917
+ sourceHandle: 'markdown',
918
+ targetHandle: 'scene',
919
+ });
920
+ assert.equal(displayLabel.edge.relation, 'wf-bridge');
921
+
922
+ const physicalRelation = graphStore.connectNodes(localRoot, {
923
+ from: a.node.nodeId,
924
+ to: c.node.nodeId,
925
+ relation: 'wf-bridge/markdown:right',
926
+ sourceHandle: 'markdown:right',
927
+ targetHandle: 'file',
928
+ });
929
+ assert.equal(physicalRelation.edge.relation, 'wf-bridge/markdown');
930
+
931
+ const context = await runtime.getNodeContext(localRoot, a.node.nodeId);
932
+ const relations = context.node.graph.connections.map(connection => connection.relation).sort();
933
+ assert.deepEqual(relations, ['wf-bridge', 'wf-bridge/markdown'].sort());
934
+ assert.ok(!JSON.stringify(context.node.graph.connections).includes('<->'));
935
+ assert.ok(!JSON.stringify(context.node.graph.connections.map(connection => connection.relation)).includes(':right'));
936
+ } finally {
937
+ cleanup(localRoot);
938
+ }
939
+ });
940
+
941
+ it('reads connections for a node', () => {
942
+ const result = graphStore.readConnections(root, nodeA);
943
+ assert.equal(result.nodeId, nodeA);
944
+ assert.ok(result.connections.length >= 1);
945
+ assert.equal(result.connections[0].direction, 'bidirectional');
946
+ assert.ok(['source', 'target'].includes(result.connections[0].endpointRole));
947
+ });
948
+
949
+ it('W11 allows directed event edges and keeps reverse event edges distinct', async () => {
950
+ const localRoot = tempProjectRoot();
951
+ try {
952
+ const timer = await runtime.createNode(localRoot, { type: 'timer', title: 'Manual Timer' });
953
+ const agentNodeId = seedAgentGraphNode(localRoot);
954
+ const graph = graphStore.getGraphSnapshot(localRoot);
955
+ graphStore.connectNodes(localRoot, {
956
+ from: timer.node.nodeId,
957
+ to: agentNodeId,
958
+ relation: 'event',
959
+ direction: 'source-to-target',
960
+ sourceHandle: 'event',
961
+ targetHandle: 'event.in',
962
+ });
963
+ const reverse = graphStore.connectNodes(localRoot, {
964
+ from: agentNodeId,
965
+ to: timer.node.nodeId,
966
+ relation: 'control',
967
+ direction: 'source-to-target',
968
+ sourceHandle: 'control',
969
+ targetHandle: 'config',
970
+ });
971
+ assert.equal(reverse.edge.direction, 'source-to-target');
972
+ assert.equal(graphStore.getGraphSnapshot(localRoot).edges.length, (graph.edges || []).length + 2);
973
+ const timerConnections = graphStore.readConnections(localRoot, timer.node.nodeId).connections;
974
+ assert.ok(timerConnections.some(connection => connection.direction === 'source-to-target' && connection.endpointRole === 'source'));
975
+ assert.ok(timerConnections.some(connection => connection.direction === 'source-to-target' && connection.endpointRole === 'target'));
976
+ } finally {
977
+ cleanup(localRoot);
978
+ }
979
+ });
980
+
981
+ it('W11 exposes connected event refs to an agent context', async () => {
982
+ const localRoot = tempProjectRoot();
983
+ try {
984
+ const timer = await runtime.createNode(localRoot, { type: 'timer', title: 'Agent Timer' });
985
+ seedAgentGraphNode(localRoot);
986
+ graphStore.connectNodes(localRoot, {
987
+ from: timer.node.nodeId,
988
+ to: 'session-event-agent-01',
989
+ relation: 'event',
990
+ direction: 'source-to-target',
991
+ sourceHandle: 'event',
992
+ targetHandle: 'event.in',
993
+ });
994
+ const context = await runtime.getNodeContext(localRoot, 'session-event-agent-01');
995
+ assert.ok(Array.isArray(context.context.connectedEventRefs), 'agent context must expose connectedEventRefs');
996
+ assert.equal(context.context.connectedEventRefs.length, 1);
997
+ assert.equal(context.context.connectedEventRefs[0].nodeId, timer.node.nodeId);
998
+ assert.equal(context.context.connectedEventRefs[0].direction, 'source-to-target');
999
+ assert.equal(context.context.connectedEventRefs[0].eventKind, 'timer');
1000
+ assert.equal(context.context.connectedEventRefs[0].connection.localHandle, 'event.in');
1001
+ } finally {
1002
+ cleanup(localRoot);
1003
+ }
1004
+ });
1005
+
1006
+ it('W34 exposes capsule dock links to Agent context without storing normal graph edges', async () => {
1007
+ const localRoot = tempProjectRoot();
1008
+ try {
1009
+ const timer = await runtime.createNode(localRoot, {
1010
+ type: 'timer',
1011
+ title: 'Docked Agent Timer',
1012
+ enabled: true,
1013
+ controlPolicy: { agentCanDisable: true, agentCanSetInterval: true },
1014
+ });
1015
+ const agentNodeId = seedAgentGraphNode(localRoot, 'session-docked-agent-01');
1016
+ const graph = runtime.loadWorkflowGraphMap(localRoot);
1017
+ runtime.writeWorkflowGraphMap(localRoot, {
1018
+ ...graph,
1019
+ version: graph.version + 1,
1020
+ edges: [],
1021
+ capsuleDockLinks: [{
1022
+ id: `dock:${agentNodeId}::${timer.node.nodeId}`,
1023
+ nodeIds: [agentNodeId, timer.node.nodeId].sort(),
1024
+ anchorId: timer.node.nodeId,
1025
+ draggedId: agentNodeId,
1026
+ side: 'top',
1027
+ edges: [],
1028
+ connections: [
1029
+ {
1030
+ source: timer.node.nodeId,
1031
+ target: agentNodeId,
1032
+ relation: 'event',
1033
+ direction: 'source-to-target',
1034
+ sourceHandle: 'event',
1035
+ targetHandle: 'event.in',
1036
+ },
1037
+ {
1038
+ source: agentNodeId,
1039
+ target: timer.node.nodeId,
1040
+ relation: 'control',
1041
+ direction: 'source-to-target',
1042
+ sourceHandle: 'context',
1043
+ targetHandle: 'config',
1044
+ },
1045
+ ],
1046
+ }],
1047
+ });
1048
+
1049
+ const stored = runtime.loadWorkflowGraphMap(localRoot);
1050
+ assert.equal(stored.edges.length, 0);
1051
+ assert.equal(stored.capsuleDockLinks.length, 1);
1052
+ assert.equal(stored.capsuleDockLinks[0].connections.length, 2);
1053
+
1054
+ const context = await runtime.getNodeContext(localRoot, agentNodeId);
1055
+ const ref = context.context.connectedEventRefs.find(item => item.nodeId === timer.node.nodeId);
1056
+ assert.ok(ref, JSON.stringify(context.context.connectedEventRefs));
1057
+ assert.equal(ref.connection.relation, 'event');
1058
+ assert.equal(ref.connection.localHandle, 'event.in');
1059
+ assert.ok(ref.allowedActions.includes('timer.setInterval'), JSON.stringify(ref.allowedActions));
1060
+
1061
+ const interval = await runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.setInterval', {
1062
+ actorNodeId: agentNodeId,
1063
+ intervalSeconds: 120,
1064
+ });
1065
+ assert.equal(interval.result.state.schedule.intervalSeconds, 120);
1066
+ } finally {
1067
+ cleanup(localRoot);
1068
+ }
1069
+ });
1070
+
1071
+ it('W34 migrates legacy dock-created edges and dedupes ordinary edge plus dock semantics', async () => {
1072
+ const localRoot = tempProjectRoot();
1073
+ try {
1074
+ const timer = await runtime.createNode(localRoot, {
1075
+ type: 'timer',
1076
+ title: 'Legacy Dock Timer',
1077
+ enabled: true,
1078
+ controlPolicy: { agentCanDisable: true, agentCanSetInterval: true },
1079
+ });
1080
+ const agentNodeId = seedAgentGraphNode(localRoot, 'session-legacy-dock-agent-01');
1081
+ const graph = runtime.loadWorkflowGraphMap(localRoot);
1082
+ const legacyEdges = [
1083
+ {
1084
+ id: 'legacy-dock-event',
1085
+ source: timer.node.nodeId,
1086
+ target: agentNodeId,
1087
+ relation: 'event',
1088
+ direction: 'source-to-target',
1089
+ sourceHandle: 'event',
1090
+ targetHandle: 'event.in',
1091
+ },
1092
+ {
1093
+ id: 'legacy-dock-control',
1094
+ source: agentNodeId,
1095
+ target: timer.node.nodeId,
1096
+ relation: 'control',
1097
+ direction: 'source-to-target',
1098
+ sourceHandle: 'context',
1099
+ targetHandle: 'config',
1100
+ },
1101
+ ];
1102
+ runtime.writeWorkflowGraphMap(localRoot, {
1103
+ ...graph,
1104
+ version: graph.version + 1,
1105
+ edges: legacyEdges,
1106
+ capsuleDockLinks: [{
1107
+ id: `dock:${agentNodeId}::${timer.node.nodeId}`,
1108
+ nodeIds: [agentNodeId, timer.node.nodeId].sort(),
1109
+ anchorId: timer.node.nodeId,
1110
+ draggedId: agentNodeId,
1111
+ side: 'left',
1112
+ edges: legacyEdges.map(edge => ({ edgeId: edge.id, retention: 'delete-on-detach' })),
1113
+ }],
1114
+ });
1115
+
1116
+ const migrated = runtime.loadWorkflowGraphMap(localRoot);
1117
+ assert.equal(migrated.edges.length, 0);
1118
+ assert.equal(migrated.capsuleDockLinks.length, 1);
1119
+ assert.equal(migrated.capsuleDockLinks[0].edges.length, 0);
1120
+ assert.equal(migrated.capsuleDockLinks[0].connections.length, 2);
1121
+
1122
+ const context = await runtime.getNodeContext(localRoot, agentNodeId);
1123
+ assert.equal(context.context.connectedEventRefs.filter(item => item.nodeId === timer.node.nodeId).length, 1);
1124
+
1125
+ const interval = await runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.setInterval', {
1126
+ actorNodeId: agentNodeId,
1127
+ intervalSeconds: 180,
1128
+ });
1129
+ assert.equal(interval.result.state.schedule.intervalSeconds, 180);
1130
+
1131
+ const duplicateGraph = runtime.loadWorkflowGraphMap(localRoot);
1132
+ runtime.writeWorkflowGraphMap(localRoot, {
1133
+ ...duplicateGraph,
1134
+ version: duplicateGraph.version + 1,
1135
+ edges: legacyEdges.map(edge => ({ ...edge, id: edge.id.replace('legacy-dock', 'manual') })),
1136
+ capsuleDockLinks: [{
1137
+ id: `dock:${agentNodeId}::${timer.node.nodeId}`,
1138
+ nodeIds: [agentNodeId, timer.node.nodeId].sort(),
1139
+ anchorId: timer.node.nodeId,
1140
+ draggedId: agentNodeId,
1141
+ side: 'right',
1142
+ edges: [],
1143
+ connections: legacyEdges.map(edge => ({
1144
+ source: edge.source,
1145
+ target: edge.target,
1146
+ relation: edge.relation,
1147
+ direction: edge.direction,
1148
+ sourceHandle: edge.sourceHandle,
1149
+ targetHandle: edge.targetHandle,
1150
+ })),
1151
+ }],
1152
+ });
1153
+ const duplicateContext = await runtime.getNodeContext(localRoot, agentNodeId);
1154
+ assert.equal(duplicateContext.context.connectedEventRefs.filter(item => item.nodeId === timer.node.nodeId).length, 1);
1155
+ } finally {
1156
+ cleanup(localRoot);
1157
+ }
1158
+ });
1159
+
1160
+ it('W14-TIMER lets a connected Agent control interval, watchdog ack, and disable through a control edge', async () => {
1161
+ const localRoot = tempProjectRoot();
1162
+ try {
1163
+ const agentNodeId = seedAgentGraphNode(localRoot, 'session-timer-agent-01');
1164
+ const timer = await runtime.createNode(localRoot, {
1165
+ type: 'timer',
1166
+ title: 'Agent-Controlled Timer',
1167
+ enabled: true,
1168
+ schedule: { mode: 'interval', intervalSeconds: 60 },
1169
+ heartbeat: {
1170
+ base: { enabled: true, intervalSeconds: 60 },
1171
+ watchdog: { enabled: true, intervalSeconds: 900, timeoutSeconds: 1800 },
1172
+ },
1173
+ controlPolicy: { agentCanDisable: true, agentCanSetInterval: true, minIntervalSeconds: 10, maxIntervalSeconds: 3600 },
1174
+ });
1175
+ graphStore.connectNodes(localRoot, {
1176
+ from: timer.node.nodeId,
1177
+ to: agentNodeId,
1178
+ relation: 'event',
1179
+ direction: 'source-to-target',
1180
+ sourceHandle: 'event',
1181
+ targetHandle: 'event.in',
1182
+ });
1183
+ graphStore.connectNodes(localRoot, {
1184
+ from: agentNodeId,
1185
+ to: timer.node.nodeId,
1186
+ relation: 'control',
1187
+ direction: 'source-to-target',
1188
+ sourceHandle: 'control',
1189
+ targetHandle: 'config',
1190
+ });
1191
+
1192
+ const context = await runtime.getNodeContext(localRoot, agentNodeId);
1193
+ const ref = context.context.connectedEventRefs.find(item => item.nodeId === timer.node.nodeId);
1194
+ assert.ok(ref, JSON.stringify(context.context.connectedEventRefs));
1195
+ assert.deepEqual(ref.allowedActions.sort(), ['timer.ackWatchdog', 'timer.configure', 'timer.disable', 'timer.enable', 'timer.resetWatchdog', 'timer.setInterval', 'timer.setMode'].sort());
1196
+ assert.equal(ref.heartbeat.watchdog.state, 'ok');
1197
+
1198
+ const interval = await runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.setInterval', {
1199
+ actorNodeId: agentNodeId,
1200
+ intervalSeconds: 120,
1201
+ lane: 'base',
1202
+ });
1203
+ assert.equal(interval.result.state.schedule.intervalSeconds, 120);
1204
+ assert.equal(interval.result.state.heartbeat.base.intervalSeconds, 120);
1205
+ assert.ok(Date.parse(interval.result.state.heartbeat.base.nextDueAt), 'timer.setInterval should schedule the next base heartbeat');
1206
+
1207
+ const ack = await runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.ackWatchdog', {
1208
+ actorNodeId: agentNodeId,
1209
+ now: '2026-08-05T00:01:00.000Z',
1210
+ });
1211
+ assert.equal(ack.result.state.heartbeat.watchdog.lastAckAt, '2026-08-05T00:01:00.000Z');
1212
+ assert.equal(ack.result.state.heartbeat.watchdog.state, 'ok');
1213
+
1214
+ const disabled = await runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.disable', {
1215
+ actorNodeId: agentNodeId,
1216
+ });
1217
+ assert.equal(disabled.result.state.enabled, false);
1218
+ assert.equal(disabled.result.state.heartbeat.base.enabled, false);
1219
+ assert.equal(disabled.result.state.heartbeat.base.nextDueAt, '');
1220
+ } finally {
1221
+ cleanup(localRoot);
1222
+ }
1223
+ });
1224
+
1225
+ it('W25-TIMER context distinguishes Timer event delivery from Agent control', async () => {
1226
+ const localRoot = tempProjectRoot();
1227
+ try {
1228
+ const agentNodeId = seedAgentGraphNode(localRoot, 'session-timer-agent-context-01');
1229
+ const timer = await runtime.createNode(localRoot, {
1230
+ type: 'timer',
1231
+ title: 'Event-only Timer',
1232
+ enabled: true,
1233
+ schedule: { mode: 'loop', intervalSeconds: 60 },
1234
+ heartbeat: {
1235
+ base: { enabled: true, intervalSeconds: 60 },
1236
+ watchdog: { enabled: true, intervalSeconds: 600, timeoutSeconds: 1800 },
1237
+ },
1238
+ controlPolicy: { agentCanDisable: true, agentCanSetInterval: true, minIntervalSeconds: 10, maxIntervalSeconds: 3600 },
1239
+ });
1240
+ assert.ok(Date.parse(timer.state.heartbeat.base.nextDueAt), 'enabled Timer creation should schedule nextDueAt');
1241
+
1242
+ graphStore.connectNodes(localRoot, {
1243
+ from: timer.node.nodeId,
1244
+ to: agentNodeId,
1245
+ relation: 'event',
1246
+ direction: 'source-to-target',
1247
+ sourceHandle: 'event',
1248
+ targetHandle: 'event.in',
1249
+ });
1250
+
1251
+ const eventOnlyContext = await runtime.getNodeContext(localRoot, agentNodeId);
1252
+ const eventOnlyRef = eventOnlyContext.context.connectedEventRefs.find(item => item.nodeId === timer.node.nodeId);
1253
+ assert.ok(eventOnlyRef, JSON.stringify(eventOnlyContext.context.connectedEventRefs));
1254
+ assert.equal(eventOnlyRef.canControl, false);
1255
+ assert.equal(eventOnlyRef.controlEdgeRequired, true);
1256
+ assert.ok(eventOnlyRef.deniedActions.includes('timer.setInterval'), JSON.stringify(eventOnlyRef.deniedActions));
1257
+ assert.match(eventOnlyRef.controlReason, /event delivery is not Timer control/);
1258
+ assert.ok(eventOnlyContext.context.availableOnDemandSkills.includes('workflow-timer-node'));
1259
+
1260
+ await assert.rejects(
1261
+ () => runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.setInterval', {
1262
+ actorNodeId: agentNodeId,
1263
+ intervalSeconds: 120,
1264
+ }),
1265
+ /Timer control action requires a source-to-target control edge/,
1266
+ );
1267
+
1268
+ graphStore.connectNodes(localRoot, {
1269
+ from: agentNodeId,
1270
+ to: timer.node.nodeId,
1271
+ relation: 'control',
1272
+ direction: 'source-to-target',
1273
+ sourceHandle: 'context',
1274
+ targetHandle: 'config',
1275
+ });
1276
+
1277
+ const controlContext = await runtime.getNodeContext(localRoot, agentNodeId);
1278
+ const controlRef = controlContext.context.connectedEventRefs.find(item => item.nodeId === timer.node.nodeId);
1279
+ assert.equal(controlRef.canControl, true);
1280
+ assert.equal(controlRef.controlEdgeRequired, false);
1281
+ assert.deepEqual(controlRef.deniedActions, []);
1282
+ assert.ok(controlRef.allowedActions.includes('timer.setInterval'), JSON.stringify(controlRef.allowedActions));
1283
+
1284
+ const updated = await runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.setInterval', {
1285
+ actorNodeId: agentNodeId,
1286
+ intervalSeconds: 120,
1287
+ });
1288
+ assert.equal(updated.result.state.schedule.intervalSeconds, 120);
1289
+ assert.ok(Date.parse(updated.result.state.heartbeat.base.nextDueAt), 'Timer API control should keep nextDueAt scheduled');
1290
+ } finally {
1291
+ cleanup(localRoot);
1292
+ }
1293
+ });
1294
+
1295
+ it('W16-TIMER requires a control edge for Agent-authored configure actions', async () => {
1296
+ const localRoot = tempProjectRoot();
1297
+ try {
1298
+ const agentNodeId = seedAgentGraphNode(localRoot, 'session-timer-agent-03');
1299
+ const unconnectedAgentNodeId = seedAgentGraphNode(localRoot, 'session-timer-agent-04');
1300
+ const timer = await runtime.createNode(localRoot, {
1301
+ type: 'timer',
1302
+ title: 'Configure Gate Timer',
1303
+ enabled: true,
1304
+ schedule: { mode: 'interval', intervalSeconds: 60 },
1305
+ controlPolicy: { agentCanDisable: true, agentCanSetInterval: true, agentCanSetMode: true, minIntervalSeconds: 10, maxIntervalSeconds: 3600 },
1306
+ });
1307
+
1308
+ await assert.rejects(
1309
+ () => runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.configure', {
1310
+ actorType: 'agent',
1311
+ schedule: { mode: 'watchdog', intervalSeconds: 120 },
1312
+ }),
1313
+ /Agent-authored node action requires actorNodeId or actorSessionId/,
1314
+ );
1315
+
1316
+ await assert.rejects(
1317
+ () => runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.configure', {
1318
+ actorNodeId: unconnectedAgentNodeId,
1319
+ schedule: { mode: 'watchdog', intervalSeconds: 120 },
1320
+ }),
1321
+ /Timer control action requires a source-to-target control edge from actor to timer/,
1322
+ );
1323
+
1324
+ graphStore.connectNodes(localRoot, {
1325
+ from: agentNodeId,
1326
+ to: timer.node.nodeId,
1327
+ relation: 'control',
1328
+ direction: 'source-to-target',
1329
+ sourceHandle: 'control',
1330
+ targetHandle: 'config',
1331
+ });
1332
+
1333
+ await assert.rejects(
1334
+ () => runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.configure', {
1335
+ actorNodeId: agentNodeId,
1336
+ actorSessionId: `${unconnectedAgentNodeId}-pty`,
1337
+ schedule: { mode: 'watchdog', intervalSeconds: 120 },
1338
+ }),
1339
+ /Agent action actor does not match actorSessionId/,
1340
+ );
1341
+
1342
+ const configured = await runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.configure', {
1343
+ actorNodeId: agentNodeId,
1344
+ schedule: { mode: 'watchdog', intervalSeconds: 120 },
1345
+ });
1346
+ assert.equal(configured.result.schedule.mode, 'watchdog');
1347
+ const sessionActorConfigured = await runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.configure', {
1348
+ actorSessionId: `${agentNodeId}-pty`,
1349
+ schedule: { mode: 'watchdog', intervalSeconds: 180 },
1350
+ });
1351
+ assert.equal(sessionActorConfigured.result.schedule.intervalSeconds, 180);
1352
+ } finally {
1353
+ cleanup(localRoot);
1354
+ }
1355
+ });
1356
+
1357
+ it('W14-TIMER computes due base heartbeat and watchdog timeout events without arbitrary code execution', async () => {
1358
+ const localRoot = tempProjectRoot();
1359
+ try {
1360
+ const timer = await runtime.createNode(localRoot, {
1361
+ type: 'timer',
1362
+ title: 'Due Timer',
1363
+ enabled: true,
1364
+ schedule: {
1365
+ mode: 'while',
1366
+ intervalSeconds: 60,
1367
+ cadence: { kind: 'fixed' },
1368
+ },
1369
+ heartbeat: {
1370
+ base: {
1371
+ enabled: true,
1372
+ intervalSeconds: 60,
1373
+ nextDueAt: '2026-08-05T00:00:00.000Z',
1374
+ },
1375
+ watchdog: {
1376
+ enabled: true,
1377
+ intervalSeconds: 600,
1378
+ timeoutSeconds: 300,
1379
+ lastPingAt: '2026-08-04T23:50:00.000Z',
1380
+ lastAckAt: '2026-08-04T23:40:00.000Z',
1381
+ state: 'waiting',
1382
+ },
1383
+ },
1384
+ whileGuard: { source: 'watchdogAck', op: 'exists', value: 'ignored' },
1385
+ });
1386
+
1387
+ const tick = await runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.tick', {
1388
+ now: '2026-08-05T00:01:00.000Z',
1389
+ });
1390
+ assert.equal(tick.result.events.length, 2);
1391
+ assert.deepEqual(tick.result.events.map(event => event.kind).sort(), ['timer.heartbeat.base', 'timer.watchdog.timeout']);
1392
+ assert.equal(tick.result.state.heartbeat.base.count, 1);
1393
+ assert.equal(tick.result.state.heartbeat.watchdog.state, 'missed');
1394
+ assert.equal(tick.result.state.heartbeat.watchdog.missedCount, 1);
1395
+ assert.equal(JSON.stringify(tick.result.state.whileGuard).includes('function'), false);
1396
+ } finally {
1397
+ cleanup(localRoot);
1398
+ }
1399
+ });
1400
+
1401
+ it('W14-GOAL exposes a synthetic Goal node and connectedGoalRefs without allowing direct Agent completion', async () => {
1402
+ const localRoot = tempProjectRoot();
1403
+ try {
1404
+ const goalNodeId = seedActiveTask(localRoot);
1405
+ const agentNodeId = seedAgentGraphNode(localRoot, 'session-goal-agent-01');
1406
+ const unconnectedAgentNodeId = seedAgentGraphNode(localRoot, 'session-goal-agent-02');
1407
+ const listed = await runtime.listNodes(localRoot);
1408
+ const goal = listed.nodes.find(node => node.kind === 'goal');
1409
+ assert.ok(goal, JSON.stringify(listed.nodes.map(node => node.kind)));
1410
+ assert.equal(goal.nodeId, goalNodeId);
1411
+ assert.equal(goal.ui.testId, 'workflow-goal-node');
1412
+ assert.ok(goal.capabilities.includes('goal.requestCompletion'));
1413
+
1414
+ await assert.rejects(
1415
+ () => runtime.executeNodeAction(localRoot, goalNodeId, 'goal.requestCompletion', {
1416
+ actorNodeId: unconnectedAgentNodeId,
1417
+ evidenceRefs: ['should-not-write'],
1418
+ note: 'Unconnected Agent should not update this Goal.',
1419
+ }),
1420
+ /Goal action requires a bidirectional goal edge from actor to goal/,
1421
+ );
1422
+
1423
+ graphStore.connectNodes(localRoot, {
1424
+ from: goalNodeId,
1425
+ to: agentNodeId,
1426
+ relation: 'goal',
1427
+ direction: 'bidirectional',
1428
+ sourceHandle: 'goal:right',
1429
+ targetHandle: 'context',
1430
+ });
1431
+
1432
+ const context = await runtime.getNodeContext(localRoot, agentNodeId);
1433
+ assert.ok(Array.isArray(context.context.connectedGoalRefs), 'agent context must expose connectedGoalRefs');
1434
+ const ref = context.context.connectedGoalRefs.find(item => item.nodeId === goalNodeId);
1435
+ assert.ok(ref, JSON.stringify(context.context.connectedGoalRefs));
1436
+ assert.equal(ref.status, 'active');
1437
+ assert.equal(ref.acceptance.length, 2);
1438
+ assert.equal(ref.progress.total, 2);
1439
+ assert.equal(ref.contentRef.kind, 'task-capsule');
1440
+ assert.equal(ref.contentRef.statePath, 'Harness/tasks/task-runtime-goal/STATE.json');
1441
+ assert.equal(ref.connection.relation, 'goal');
1442
+ assert.ok(context.context.connectedPeers.some(peer => peer.nodeId === goalNodeId && peer.type === 'goal'));
1443
+
1444
+ const proposal = await runtime.executeNodeAction(localRoot, goalNodeId, 'goal.requestCompletion', {
1445
+ actorNodeId: agentNodeId,
1446
+ evidenceRefs: ['backend-tests'],
1447
+ note: 'All W14 acceptance checks pass.',
1448
+ });
1449
+ assert.equal(proposal.result.state.status, 'proposed-complete');
1450
+ assert.equal(proposal.result.state.confirmation.proposedBy, agentNodeId);
1451
+ const updatedContext = await runtime.getNodeContext(localRoot, agentNodeId);
1452
+ const updatedRef = updatedContext.context.connectedGoalRefs.find(item => item.nodeId === goalNodeId);
1453
+ assert.equal(updatedRef.status, 'proposed-complete');
1454
+ assert.equal(updatedRef.confirmation.state, 'proposed');
1455
+ const taskState = JSON.parse(fs.readFileSync(path.join(localRoot, 'Harness', 'tasks', 'task-runtime-goal', 'STATE.json'), 'utf8'));
1456
+ assert.equal(taskState.status, 'active', 'Agent proposal must not directly complete task state');
1457
+ } finally {
1458
+ cleanup(localRoot);
1459
+ }
1460
+ });
1461
+
1462
+ it('W35-GOAL creates or restores the active task Goal node through runtime createNode', async () => {
1463
+ const localRoot = tempProjectRoot();
1464
+ try {
1465
+ const goalNodeId = seedActiveTask(localRoot, 'task-runtime-create-goal');
1466
+ const created = await runtime.createNode(localRoot, {
1467
+ type: 'goal',
1468
+ position: { x: 720, y: 180 },
1469
+ });
1470
+ assert.equal(created.node.kind, 'goal');
1471
+ assert.equal(created.node.nodeId, goalNodeId);
1472
+ assert.deepEqual(created.node.graph.position, { x: 720, y: 180 });
1473
+
1474
+ await runtime.deleteNode(localRoot, goalNodeId);
1475
+ const hidden = await runtime.listNodes(localRoot);
1476
+ assert.equal(hidden.nodes.some(node => node.nodeId === goalNodeId), false);
1477
+
1478
+ const restored = await runtime.createNode(localRoot, {
1479
+ type: 'goal',
1480
+ position: { x: 840, y: 220 },
1481
+ });
1482
+ assert.equal(restored.node.nodeId, goalNodeId);
1483
+ assert.deepEqual(restored.node.graph.position, { x: 840, y: 220 });
1484
+ const visible = await runtime.listNodes(localRoot);
1485
+ assert.equal(visible.nodes.some(node => node.nodeId === goalNodeId), true);
1486
+ } finally {
1487
+ cleanup(localRoot);
1488
+ }
1489
+ });
1490
+
1491
+ it('W34 exposes capsule Goal docks to Agent context without normal graph edges', async () => {
1492
+ const localRoot = tempProjectRoot();
1493
+ try {
1494
+ const goalNodeId = seedActiveTask(localRoot, 'task-runtime-docked-goal');
1495
+ const agentNodeId = seedAgentGraphNode(localRoot, 'session-docked-goal-agent-01');
1496
+ const graph = runtime.loadWorkflowGraphMap(localRoot);
1497
+ runtime.writeWorkflowGraphMap(localRoot, {
1498
+ ...graph,
1499
+ version: graph.version + 1,
1500
+ nodes: [
1501
+ ...(graph.nodes || []),
1502
+ {
1503
+ nodeId: goalNodeId,
1504
+ id: goalNodeId,
1505
+ type: 'goal',
1506
+ kind: 'goal-node',
1507
+ label: 'Docked Goal',
1508
+ status: 'active',
1509
+ },
1510
+ ],
1511
+ edges: [],
1512
+ capsuleDockLinks: [{
1513
+ id: `dock:${agentNodeId}::${goalNodeId}`,
1514
+ nodeIds: [agentNodeId, goalNodeId].sort(),
1515
+ anchorId: goalNodeId,
1516
+ draggedId: agentNodeId,
1517
+ side: 'bottom',
1518
+ edges: [],
1519
+ connections: [{
1520
+ source: goalNodeId,
1521
+ target: agentNodeId,
1522
+ relation: 'goal',
1523
+ direction: 'bidirectional',
1524
+ sourceHandle: 'goal:right',
1525
+ targetHandle: 'context',
1526
+ }],
1527
+ }],
1528
+ });
1529
+
1530
+ const stored = runtime.loadWorkflowGraphMap(localRoot);
1531
+ assert.equal(stored.edges.length, 0);
1532
+ assert.equal(stored.capsuleDockLinks.length, 1);
1533
+
1534
+ const context = await runtime.getNodeContext(localRoot, agentNodeId);
1535
+ const ref = context.context.connectedGoalRefs.find(item => item.nodeId === goalNodeId);
1536
+ assert.ok(ref, JSON.stringify(context.context.connectedGoalRefs));
1537
+ assert.equal(ref.connection.relation, 'goal');
1538
+ assert.equal(ref.connection.endpointRole, 'target');
1539
+
1540
+ const proposal = await runtime.executeNodeAction(localRoot, goalNodeId, 'goal.requestCompletion', {
1541
+ actorNodeId: agentNodeId,
1542
+ evidenceRefs: ['dock-link-context'],
1543
+ note: 'Goal dock grants Agent context without a normal graph edge.',
1544
+ });
1545
+ assert.equal(proposal.result.state.status, 'proposed-complete');
1546
+ assert.equal(proposal.result.state.confirmation.proposedBy, agentNodeId);
1547
+ } finally {
1548
+ cleanup(localRoot);
1549
+ }
1550
+ });
1551
+
1552
+ it('W16-GOAL lets users edit Goal metadata and requires linked Agents for Goal updates', async () => {
1553
+ const localRoot = tempProjectRoot();
1554
+ try {
1555
+ const goalNodeId = seedActiveTask(localRoot);
1556
+ const agentNodeId = seedAgentGraphNode(localRoot, 'session-goal-agent-03');
1557
+ const unconnectedAgentNodeId = seedAgentGraphNode(localRoot, 'session-goal-agent-04');
1558
+
1559
+ const userEdit = await runtime.executeNodeAction(localRoot, goalNodeId, 'goal.update', {
1560
+ title: 'Ship expanded Goal editing',
1561
+ objective: 'Editable objective text belongs in the Goal sidecar.',
1562
+ nextAction: 'Verify Goal editor save flow',
1563
+ status: 'completed',
1564
+ phase: 'forged-phase',
1565
+ gate: 'forged-gate',
1566
+ acceptance: [
1567
+ { id: 'W16-001', text: 'Double-click opens the Goal workbench', status: 'verified' },
1568
+ { id: 'W16-002', text: 'Linked Agent may update Goal metadata', status: 'tracked' },
1569
+ ],
1570
+ planItems: [
1571
+ { id: 'P-001', text: 'Inspect current Goal editor state', status: 'done' },
1572
+ { id: 'P-002', text: 'Wire Agent-filled plan list', status: 'todo' },
1573
+ ],
1574
+ wdt: { enabled: true, state: 'ok', timerNodeId: 'event-timer-w16' },
1575
+ });
1576
+ assert.equal(userEdit.result.state.title, 'Ship expanded Goal editing');
1577
+ assert.equal(userEdit.result.state.objective, 'Editable objective text belongs in the Goal sidecar.');
1578
+ assert.equal(userEdit.result.state.nextAction, 'Verify Goal editor save flow');
1579
+ assert.equal(userEdit.result.state.status, 'active');
1580
+ assert.notEqual(userEdit.result.state.phase, 'forged-phase');
1581
+ assert.notEqual(userEdit.result.state.gate, 'forged-gate');
1582
+ assert.equal(userEdit.result.state.acceptance.length, 2);
1583
+ assert.equal(userEdit.result.state.planItems.length, 2);
1584
+ assert.equal(userEdit.result.state.planItems[0].status, 'done');
1585
+ assert.equal(userEdit.result.state.progress.verified, 1);
1586
+
1587
+ await assert.rejects(
1588
+ () => runtime.executeNodeAction(localRoot, goalNodeId, 'goal.update', {
1589
+ actorType: 'agent',
1590
+ title: 'Agent write without actor should fail',
1591
+ }),
1592
+ /Agent-authored node action requires actorNodeId or actorSessionId/,
1593
+ );
1594
+
1595
+ await assert.rejects(
1596
+ () => runtime.executeNodeAction(localRoot, goalNodeId, 'goal.update', {
1597
+ actorNodeId: unconnectedAgentNodeId,
1598
+ title: 'Unlinked write should fail',
1599
+ }),
1600
+ /Goal action requires a bidirectional goal edge from actor to goal/,
1601
+ );
1602
+
1603
+ graphStore.connectNodes(localRoot, {
1604
+ from: agentNodeId,
1605
+ to: goalNodeId,
1606
+ relation: 'goal',
1607
+ direction: 'bidirectional',
1608
+ sourceHandle: 'right',
1609
+ targetHandle: 'goal:left',
1610
+ });
1611
+
1612
+ await assert.rejects(
1613
+ () => runtime.executeNodeAction(localRoot, goalNodeId, 'goal.update', {
1614
+ actorNodeId: agentNodeId,
1615
+ actorSessionId: `${unconnectedAgentNodeId}-pty`,
1616
+ title: 'Spoofed session write should fail',
1617
+ }),
1618
+ /Agent action actor does not match actorSessionId/,
1619
+ );
1620
+
1621
+ const agentEdit = await runtime.executeNodeAction(localRoot, goalNodeId, 'goal.update', {
1622
+ actorNodeId: agentNodeId,
1623
+ title: 'Linked Agent edited Goal',
1624
+ status: 'completed',
1625
+ phase: 'linked-forged-phase',
1626
+ gate: 'linked-forged-gate',
1627
+ acceptance: [
1628
+ { id: 'W16-001', text: 'Double-click opens the Goal workbench', status: 'verified' },
1629
+ ],
1630
+ planItems: [
1631
+ { id: 'P-001', text: 'Agent updates the visible plan list', status: 'done' },
1632
+ ],
1633
+ });
1634
+ assert.equal(agentEdit.result.state.title, 'Linked Agent edited Goal');
1635
+ assert.equal(agentEdit.result.state.status, 'active');
1636
+ assert.notEqual(agentEdit.result.state.phase, 'linked-forged-phase');
1637
+ assert.notEqual(agentEdit.result.state.gate, 'linked-forged-gate');
1638
+ assert.equal(agentEdit.result.state.acceptance.length, 1);
1639
+ assert.equal(agentEdit.result.state.planItems.length, 1);
1640
+ const context = await runtime.getNodeContext(localRoot, agentNodeId);
1641
+ const ref = context.context.connectedGoalRefs.find(item => item.nodeId === goalNodeId);
1642
+ assert.equal(ref.title, 'Linked Agent edited Goal');
1643
+ assert.equal(ref.acceptance[0].id, 'W16-001');
1644
+ assert.equal(ref.planItems[0].text, 'Agent updates the visible plan list');
1645
+ } finally {
1646
+ cleanup(localRoot);
1647
+ }
1648
+ });
1649
+
1650
+ it('W21-ONTOLOGY explains connected Agent affordances and timer control edge requirements', async () => {
1651
+ const localRoot = tempProjectRoot();
1652
+ try {
1653
+ const agentNodeId = seedAgentGraphNode(localRoot, 'session-ontology-main-agent', {
1654
+ agentKind: 'main',
1655
+ role: 'main',
1656
+ label: 'Ontology Main Agent',
1657
+ config: { skills: ['wf-browser'] },
1658
+ });
1659
+ const markdown = await runtime.createNode(localRoot, {
1660
+ type: 'markdown',
1661
+ title: 'Ontology Notes',
1662
+ markdown: 'Agent-readable notes',
1663
+ });
1664
+ const drawing = await runtime.createNode(localRoot, {
1665
+ type: 'excalidraw',
1666
+ title: 'Ontology Drawing',
1667
+ });
1668
+ const timer = await runtime.createNode(localRoot, {
1669
+ type: 'timer',
1670
+ title: 'Ontology Timer',
1671
+ enabled: true,
1672
+ schedule: { mode: 'loop', intervalSeconds: 60 },
1673
+ controlPolicy: { agentCanDisable: true, agentCanSetInterval: true },
1674
+ });
1675
+ const skills = await runtime.createNode(localRoot, {
1676
+ type: 'skill-group',
1677
+ title: 'Ontology Skills',
1678
+ skills: [
1679
+ { id: 'skill:wf-ui', name: 'wf-ui', title: 'WF UI' },
1680
+ ],
1681
+ });
1682
+ const goalNodeId = seedActiveTask(localRoot, 'task-ontology-goal');
1683
+
1684
+ graphStore.connectNodes(localRoot, {
1685
+ from: agentNodeId,
1686
+ to: markdown.node.nodeId,
1687
+ relation: 'context',
1688
+ sourceHandle: 'context',
1689
+ targetHandle: 'markdown',
1690
+ });
1691
+ graphStore.connectNodes(localRoot, {
1692
+ from: drawing.node.nodeId,
1693
+ to: agentNodeId,
1694
+ relation: 'context',
1695
+ sourceHandle: 'scene',
1696
+ targetHandle: 'context',
1697
+ });
1698
+ graphStore.connectNodes(localRoot, {
1699
+ from: timer.node.nodeId,
1700
+ to: agentNodeId,
1701
+ relation: 'event',
1702
+ direction: 'source-to-target',
1703
+ sourceHandle: 'event',
1704
+ targetHandle: 'event.in',
1705
+ });
1706
+ graphStore.connectNodes(localRoot, {
1707
+ from: skills.node.nodeId,
1708
+ to: agentNodeId,
1709
+ relation: 'capability',
1710
+ sourceHandle: 'capability',
1711
+ targetHandle: 'capability',
1712
+ });
1713
+ graphStore.connectNodes(localRoot, {
1714
+ from: goalNodeId,
1715
+ to: agentNodeId,
1716
+ relation: 'goal',
1717
+ direction: 'bidirectional',
1718
+ sourceHandle: 'goal',
1719
+ targetHandle: 'goal',
1720
+ });
1721
+
1722
+ const contextBeforeControl = await runtime.getNodeContext(localRoot, agentNodeId);
1723
+ assert.equal(contextBeforeControl.context.ontology.ontologyId, 'harness.workflow.ontology');
1724
+ assert.ok(Array.isArray(contextBeforeControl.context.affordances));
1725
+ assert.ok(contextBeforeControl.context.defaultSkills.includes('workflow-ontology'));
1726
+ assert.ok(contextBeforeControl.context.effectiveSkills.includes('workflow-ontology'));
1727
+ assert.ok(contextBeforeControl.context.effectiveSkills.includes('wf-browser'));
1728
+ const graphAffordance = contextBeforeControl.context.affordances.find(item => item.type === 'workflow-graph');
1729
+ assert.equal(graphAffordance.canControl, true);
1730
+ assert.ok(graphAffordance.allowedActions.includes('agent.createNode'));
1731
+
1732
+ const markdownAffordance = contextBeforeControl.context.affordances.find(item => item.nodeId === markdown.node.nodeId);
1733
+ assert.equal(markdownAffordance.canRead, true);
1734
+ assert.equal(markdownAffordance.canWrite, true);
1735
+ assert.equal(markdownAffordance.priority, 'preferred-output-target');
1736
+ assert.equal(markdownAffordance.outputModality, 'text');
1737
+ assert.ok(markdownAffordance.allowedActions.includes('markdown.replace'));
1738
+ assert.ok(markdownAffordance.preferredActions.includes('markdown.replace'));
1739
+ assert.match(markdownAffordance.useWhen, /long-form text/);
1740
+
1741
+ const drawingAffordance = contextBeforeControl.context.affordances.find(item => item.nodeId === drawing.node.nodeId);
1742
+ assert.equal(drawingAffordance.canRead, true);
1743
+ assert.equal(drawingAffordance.canWrite, true);
1744
+ assert.equal(drawingAffordance.priority, 'preferred-output-target');
1745
+ assert.equal(drawingAffordance.outputModality, 'diagram');
1746
+ assert.ok(drawingAffordance.allowedActions.includes('excalidraw.saveScene'));
1747
+ assert.ok(drawingAffordance.preferredActions.includes('excalidraw.saveScene'));
1748
+ assert.match(drawingAffordance.useWhen, /diagrams/);
1749
+
1750
+ const goalAffordance = contextBeforeControl.context.affordances.find(item => item.nodeId === goalNodeId);
1751
+ assert.equal(goalAffordance.canWrite, true);
1752
+ assert.ok(goalAffordance.allowedActions.includes('goal.update'));
1753
+
1754
+ const skillAffordance = contextBeforeControl.context.affordances.find(item => item.nodeId === skills.node.nodeId);
1755
+ assert.equal(skillAffordance.canRead, true);
1756
+ assert.equal(skillAffordance.canWrite, false);
1757
+ assert.ok(skillAffordance.allowedActions.includes('skill-group.read'));
1758
+ assert.ok(skillAffordance.deniedActions.includes('skill-group.configure'));
1759
+ const configuredSkillAffordance = contextBeforeControl.context.affordances.find(item => item.nodeId === 'skill:wf-browser');
1760
+ assert.equal(configuredSkillAffordance.canRead, true);
1761
+ assert.deepEqual(configuredSkillAffordance.allowedActions, ['capability:read']);
1762
+
1763
+ const timerBefore = contextBeforeControl.context.affordances.find(item => item.nodeId === timer.node.nodeId);
1764
+ assert.equal(timerBefore.canRead, true);
1765
+ assert.equal(timerBefore.canControl, false);
1766
+ assert.ok(timerBefore.deniedActions.includes('timer.configure'));
1767
+
1768
+ graphStore.connectNodes(localRoot, {
1769
+ from: agentNodeId,
1770
+ to: timer.node.nodeId,
1771
+ relation: 'control',
1772
+ direction: 'source-to-target',
1773
+ sourceHandle: 'control',
1774
+ targetHandle: 'config',
1775
+ });
1776
+ const contextAfterControl = await runtime.getNodeContext(localRoot, agentNodeId);
1777
+ const timerAfter = contextAfterControl.context.affordances.find(item => item.nodeId === timer.node.nodeId);
1778
+ assert.equal(timerAfter.canControl, true);
1779
+ assert.ok(timerAfter.allowedActions.includes('timer.configure'));
1780
+ assert.ok(timerAfter.allowedActions.includes('timer.setInterval'));
1781
+ assert.ok(!timerAfter.deniedActions.includes('timer.configure'));
1782
+ const sessionActorControl = await runtime.executeNodeAction(localRoot, timer.node.nodeId, 'timer.setInterval', {
1783
+ actorNodeId: 'session-ontology-main-agent-pty',
1784
+ intervalSeconds: 120,
1785
+ });
1786
+ assert.equal(sessionActorControl.result.state.schedule.intervalSeconds, 120);
1787
+ } finally {
1788
+ cleanup(localRoot);
1789
+ }
1790
+ });
1791
+
1792
+ it('W14 exposes connected github-trigger refs to an agent context without raw delivery leakage', async () => {
1793
+ const localRoot = tempProjectRoot();
1794
+ try {
1795
+ const trigger = await runtime.createNode(localRoot, {
1796
+ type: 'github-trigger',
1797
+ experimental: true,
1798
+ title: 'Agent GitHub Trigger',
1799
+ repository: { owner: 'zingspark', name: 'create-harness-vibe-coding' },
1800
+ eventFilters: { events: ['pull_request'], actions: ['opened'], branches: ['main'] },
1801
+ });
1802
+ await runtime.executeNodeAction(localRoot, trigger.node.nodeId, 'github-trigger.receive', {
1803
+ event: 'pull_request',
1804
+ action: 'opened',
1805
+ deliveryId: 'delivery-agent-1',
1806
+ repository: { owner: 'zingspark', name: 'create-harness-vibe-coding' },
1807
+ sender: 'octocat',
1808
+ pullRequest: { number: 7, title: 'Context check' },
1809
+ headers: { 'x-hub-signature-256': 'sha256=secret' },
1810
+ rawBody: '{"token":"secret"}',
1811
+ });
1812
+ seedAgentGraphNode(localRoot);
1813
+ graphStore.connectNodes(localRoot, {
1814
+ from: trigger.node.nodeId,
1815
+ to: 'session-event-agent-01',
1816
+ relation: 'event',
1817
+ direction: 'source-to-target',
1818
+ sourceHandle: 'event',
1819
+ targetHandle: 'event.in',
1820
+ });
1821
+ const context = await runtime.getNodeContext(localRoot, 'session-event-agent-01');
1822
+ assert.ok(Array.isArray(context.context.connectedEventRefs), 'agent context must expose connectedEventRefs');
1823
+ const ref = context.context.connectedEventRefs.find(item => item.nodeId === trigger.node.nodeId);
1824
+ assert.ok(ref, JSON.stringify(context.context.connectedEventRefs));
1825
+ assert.equal(ref.direction, 'source-to-target');
1826
+ assert.equal(ref.eventKind, 'github-trigger');
1827
+ assert.equal(ref.connection.localHandle, 'event.in');
1828
+ assert.equal(ref.repository.fullName, 'zingspark/create-harness-vibe-coding');
1829
+ assert.deepEqual(ref.eventFilters.events, ['pull_request']);
1830
+ assert.equal(ref.lastEvent.kind, 'github.pull_request');
1831
+ assert.equal(ref.lastEvent.pullRequest.number, 7);
1832
+
1833
+ const serialized = JSON.stringify(context.context.connectedEventRefs);
1834
+ assert.equal(serialized.includes('sha256=secret'), false);
1835
+ assert.equal(serialized.includes('"token"'), false);
1836
+ assert.equal(serialized.includes('rawBody'), false);
1837
+ assert.equal(serialized.includes('headers'), false);
1838
+ } finally {
1839
+ cleanup(localRoot);
1840
+ }
1841
+ });
1842
+
1843
+ it('W12 exposes connected skill-group capability refs to an agent context', async () => {
1844
+ const localRoot = tempProjectRoot();
1845
+ try {
1846
+ const pack = await runtime.createNode(localRoot, {
1847
+ type: 'skill-group',
1848
+ title: 'Agent Skill Pack',
1849
+ sourceGroup: { id: 'source:workflow', label: 'Workflow', kind: 'source' },
1850
+ skills: [
1851
+ { id: 'skill:wf-ui', name: 'wf-ui', title: 'WF-UI Adapter', description: 'Open wf-ui', source: 'project' },
1852
+ { id: 'skill:browser-lab', name: 'browser-lab', title: 'Browser Lab', description: 'Browser testing', source: 'project' },
1853
+ ],
1854
+ });
1855
+ seedAgentGraphNode(localRoot);
1856
+ const edge = graphStore.connectNodes(localRoot, {
1857
+ from: 'session-event-agent-01',
1858
+ to: pack.node.nodeId,
1859
+ relation: 'capability',
1860
+ sourceHandle: 'right',
1861
+ targetHandle: 'capability:left',
1862
+ });
1863
+ assert.equal(edge.edge.direction, 'bidirectional');
1864
+ assert.equal(edge.edge.relation, 'capability');
1865
+ assert.equal(edge.edge.targetHandle, 'capability:left');
1866
+
1867
+ const context = await runtime.getNodeContext(localRoot, 'session-event-agent-01');
1868
+ assert.ok(Array.isArray(context.context.connectedCapabilityRefs), 'agent context must expose connectedCapabilityRefs');
1869
+ assert.ok(Array.isArray(context.context.connectedCapabilityNodeRefs), 'agent context must expose capability-node provenance refs');
1870
+ assert.ok(context.context.effectiveSkills.includes('workflow-ontology'));
1871
+ assert.ok(context.context.effectiveSkills.includes('workflow-context'));
1872
+ assert.ok(context.context.effectiveSkills.includes('wf-ui'));
1873
+ assert.ok(context.context.effectiveSkills.includes('browser-lab'));
1874
+ assert.equal(context.context.connectedCapabilityNodeRefs.length, 1);
1875
+ assert.equal(context.context.connectedCapabilityNodeRefs[0].nodeId, pack.node.nodeId);
1876
+ assert.equal(context.context.connectedCapabilityNodeRefs[0].capabilityKind, 'skill-group');
1877
+ assert.equal(context.context.connectedCapabilityNodeRefs[0].executor, 'agent');
1878
+ assert.equal(context.context.connectedCapabilityNodeRefs[0].connection.relation, 'capability');
1879
+ assert.deepEqual(context.context.connectedCapabilityNodeRefs[0].skillNames, ['wf-ui', 'browser-lab']);
1880
+ assert.ok(Array.isArray(context.context.effectiveSkillGroups), 'agent context must expose effectiveSkillGroups');
1881
+ assert.equal(context.context.effectiveSkillGroups.length, 1);
1882
+ assert.equal(context.context.effectiveSkillGroups[0].nodeId, pack.node.nodeId);
1883
+ assert.equal(context.context.effectiveSkillGroups[0].label, 'Workflow');
1884
+ assert.deepEqual(context.context.effectiveSkillGroups[0].skillNames, ['wf-ui', 'browser-lab']);
1885
+ const serialized = JSON.stringify(context);
1886
+ assert.ok(!serialized.includes('C:\\\\'), 'context must not leak absolute Windows paths');
1887
+ assert.ok(!serialized.includes('/Users/'), 'context must not leak absolute POSIX user paths');
1888
+ assert.ok(!serialized.includes('SECRET_TOKEN'), 'context must not leak secret-like values');
1889
+ assert.ok(!serialized.includes('Open wf-ui'), 'context must not include per-skill descriptions');
1890
+ assert.ok(!serialized.includes('Browser testing'), 'context must not include per-skill descriptions');
1891
+ } finally {
1892
+ cleanup(localRoot);
1893
+ }
1894
+ });
1895
+
1896
+ it('W13 exposes connected mcp-connector refs to an agent context without adding effective skills', async () => {
1897
+ const localRoot = tempProjectRoot();
1898
+ try {
1899
+ const agentId = seedAgentGraphNode(localRoot, 'session-mcp-agent-01');
1900
+ const sentinel = path.join(localRoot, 'mcp-context-spawned.txt');
1901
+ fs.writeFileSync(path.join(localRoot, '.mcp.json'), JSON.stringify({
1902
+ mcpServers: {
1903
+ docs: {
1904
+ command: process.execPath,
1905
+ args: ['-e', `require('fs').writeFileSync(${JSON.stringify(sentinel)}, 'spawned')`, '@secret/mcp-server'],
1906
+ env: { MCP_DOCS_TOKEN: 'docs-secret-value' },
1907
+ url: 'https://docs.example.test/mcp/token/docs-secret-value',
1908
+ },
1909
+ },
1910
+ }, null, 2));
1911
+ const connector = await runtime.createNode(localRoot, {
1912
+ type: 'mcp-connector',
1913
+ mcpServerId: 'mcp:project-mcp:docs',
1914
+ title: 'Docs MCP',
1915
+ });
1916
+ await graphStore.connectNodes(localRoot, {
1917
+ from: agentId,
1918
+ to: connector.node.nodeId,
1919
+ relation: 'capability',
1920
+ direction: 'bidirectional',
1921
+ sourceHandle: 'right',
1922
+ targetHandle: 'capability:left',
1923
+ });
1924
+
1925
+ const context = await runtime.getNodeContext(localRoot, agentId);
1926
+ assert.equal(fs.existsSync(sentinel), false, 'agent context read must not spawn MCP command');
1927
+ assert.ok(Array.isArray(context.context.connectedCapabilityNodeRefs));
1928
+ const ref = context.context.connectedCapabilityNodeRefs.find(item => item.nodeId === connector.node.nodeId);
1929
+ assert.ok(ref, `missing mcp-connector ref in ${JSON.stringify(context.context.connectedCapabilityNodeRefs)}`);
1930
+ assert.equal(ref.capabilityKind, 'mcp-connector');
1931
+ assert.equal(ref.executor, 'agent');
1932
+ assert.equal(ref.connection.relation, 'capability');
1933
+ assert.deepEqual(ref.serverNames, ['docs']);
1934
+ assert.equal(ref.serverCount, 1);
1935
+ assert.ok(!context.context.effectiveSkills.includes('docs'), 'MCP servers must not be promoted into effectiveSkills');
1936
+
1937
+ const serialized = JSON.stringify(context);
1938
+ assert.equal(serialized.includes('docs-secret-value'), false);
1939
+ assert.equal(serialized.includes('@secret/mcp-server'), false);
1940
+ assert.equal(serialized.includes('writeFileSync'), false);
1941
+ assert.equal(serialized.includes(String(sentinel)), false);
1942
+ } finally {
1943
+ cleanup(localRoot);
1944
+ }
1945
+ });
1946
+
1947
+ it('AC-007 rejects updateEdge changes that create reverse duplicate bidirectional links', async () => {
1948
+ const localRoot = tempProjectRoot();
1949
+ try {
1950
+ const a = await runtime.createNode(localRoot, { type: 'markdown', title: 'Update A' });
1951
+ const b = await runtime.createNode(localRoot, { type: 'markdown', title: 'Update B' });
1952
+ const c = await runtime.createNode(localRoot, { type: 'markdown', title: 'Update C' });
1953
+ graphStore.connectNodes(localRoot, { from: a.node.nodeId, to: b.node.nodeId });
1954
+ const second = graphStore.connectNodes(localRoot, { from: a.node.nodeId, to: c.node.nodeId });
1955
+
1956
+ assert.throws(() => {
1957
+ graphStore.updateEdge(localRoot, second.edge.id, { from: b.node.nodeId, to: a.node.nodeId });
1958
+ }, /duplicate|DUPLICATE_EDGE|already/i);
1959
+ } finally {
1960
+ cleanup(localRoot);
1961
+ }
1962
+ });
1963
+
1964
+ it('disconnects by edge id', () => {
1965
+ const before = graphStore.getGraphSnapshot(root);
1966
+ const edge = before.edges[0];
1967
+ const result = graphStore.disconnectNodes(root, edge.id);
1968
+ assert.ok(result.ok);
1969
+ const after = graphStore.getGraphSnapshot(root);
1970
+ assert.equal(after.edges.length, before.edges.length - 1);
1971
+ });
1972
+
1973
+ it('rejects disconnect of nonexistent edge', () => {
1974
+ assert.throws(() => {
1975
+ graphStore.disconnectNodes(root, 'nonexistent-edge');
1976
+ }, /not found|NOT_FOUND|EDGE_NOT_FOUND/i);
1977
+ });
1978
+
1979
+ it('rejects connect with nonexistent endpoint', () => {
1980
+ assert.throws(() => {
1981
+ graphStore.connectNodes(root, { from: nodeA, to: 'component-nonexistent-ffffffff' });
1982
+ }, /not found|ENDPOINT_NOT_FOUND/i);
1983
+ });
1984
+
1985
+ it('getGraphSnapshot returns complete state', () => {
1986
+ const snapshot = graphStore.getGraphSnapshot(root);
1987
+ assert.ok(snapshot.nodes);
1988
+ assert.ok(snapshot.edges);
1989
+ assert.ok(snapshot.version >= 1);
1990
+ });
1991
+ });