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
@@ -1,8 +1,40 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { parseTaskCapsule, parseTaskList } from './task-parser.mjs';
3
+ import { parseTaskCapsule, parseTaskList, readActiveTaskId } from './task-parser.mjs';
4
4
  import { RUNTIME_DEFINITIONS } from './runtime-detector.mjs';
5
- import { listTerminalSessions } from './terminal-store.mjs';
5
+ import { listTerminalSessions, getSessionIndexSummary } from './terminal-store.mjs';
6
+ import { deleteComponentNode } from './component-node-store.mjs';
7
+ import { deleteCapabilityNode } from './workflow-capability-node-store.mjs';
8
+ import { deleteEventNode } from './workflow-event-node-store.mjs';
9
+ import { normalizeNodeConfig, recommendSkills } from './node-config-store.mjs';
10
+ import { componentNodeStatesForSnapshot, componentStateRefs, listLiveComponentNodes } from './component-node-store.mjs';
11
+ import { eventNodeStates, eventStateRefs, getEventNode, listEventNodes } from './workflow-event-node-store.mjs';
12
+ import { capabilityNodeStates, capabilityStateRefs, listCapabilityNodes } from './workflow-capability-node-store.mjs';
13
+ import { goalNodeStates, goalStateRefs, listGoalNodes } from './workflow-goal-node-store.mjs';
14
+ import { workflowOperationsSnapshot } from './workflow-operation-store.mjs';
15
+ import { buildAgentContext } from './workflow-agent-context.mjs';
16
+ import { listRoleProfiles } from './workflow-node-types/role-profile-store.mjs';
17
+
18
+ // Graph mutations are read-modify-write operations over a project-owned JSON
19
+ // document. Serialize callers in this process so two HTTP requests cannot
20
+ // both derive the same next graph version. The on-disk expectedVersion check
21
+ // remains the cross-process guard; this lock is the low-cost in-process lane.
22
+ const workflowGraphLocks = new Map();
23
+
24
+ export async function withWorkflowGraphLock(projectRoot, operation) {
25
+ const key = String(projectRoot || '');
26
+ const previous = workflowGraphLocks.get(key) || Promise.resolve();
27
+ let release;
28
+ const current = new Promise(resolve => { release = resolve; });
29
+ workflowGraphLocks.set(key, current);
30
+ await previous;
31
+ try {
32
+ return await operation();
33
+ } finally {
34
+ release();
35
+ if (workflowGraphLocks.get(key) === current) workflowGraphLocks.delete(key);
36
+ }
37
+ }
6
38
 
7
39
  const DEFAULT_ROLE_GRAPH = {
8
40
  schemaVersion: 1,
@@ -13,7 +45,7 @@ const DEFAULT_ROLE_GRAPH = {
13
45
  role: 'CEO Leader',
14
46
  level: 0,
15
47
  kind: 'controller',
16
- skills: ['wf', 'subagent-orchestrator', 'terminal-control'],
48
+ skills: ['wf', 'subagent-orchestrator', 'terminal-control', 'workflow-node-map', 'workflow-ontology', 'workflow-context', 'workflow-node-actions'],
17
49
  permissions: ['plan', 'dispatch', 'read-terminals', 'send-terminal-input'],
18
50
  },
19
51
  {
@@ -37,7 +69,7 @@ const DEFAULT_ROLE_GRAPH = {
37
69
  role: 'Backend Expert',
38
70
  level: 1,
39
71
  kind: 'implementer',
40
- skills: ['runtime-detection', 'terminal-control', 'a2a-files'],
72
+ skills: ['runtime-detection', 'terminal-control', 'workflow-node-map', 'workflow-ontology', 'workflow-context', 'workflow-node-actions', 'a2a-files'],
41
73
  permissions: ['read-code', 'write-backend', 'terminal-io'],
42
74
  },
43
75
  {
@@ -69,7 +101,7 @@ const DEFAULT_ROLE_GRAPH = {
69
101
  role: 'Terminal Controller',
70
102
  level: 1,
71
103
  kind: 'runtime',
72
- skills: ['terminal-control'],
104
+ skills: ['terminal-control', 'workflow-node-map', 'workflow-ontology', 'workflow-context', 'workflow-node-actions'],
73
105
  permissions: ['read-terminals', 'send-terminal-input'],
74
106
  },
75
107
  ],
@@ -84,70 +116,2439 @@ const DEFAULT_ROLE_GRAPH = {
84
116
  ],
85
117
  };
86
118
 
87
- const DEFAULT_TERMINAL_CONTROL_SKILL = {
88
- schemaVersion: 1,
89
- skillId: 'terminal-control',
90
- name: 'Terminal Control',
91
- source: 'Harness/a2a/skills/terminal-control.json',
92
- description: 'Bounded A2A tools for listing terminal sessions, reading output ranges, globbing terminal events, and attach-gated input forwarding.',
93
- tools: [
94
- { name: 'list-sessions', reads: ['Harness/tasks/**/sessions/**/STATE.json', 'Harness/a2a/sessions/**/STATE.json'] },
95
- { name: 'read-range', reads: ['Harness/tasks/**/sessions/**/terminal.jsonl', 'Harness/a2a/sessions/**/terminal.jsonl'], args: ['session', 'from', 'to', 'tail'] },
96
- { name: 'glob-events', reads: ['Harness/tasks/**/sessions/**/events.jsonl', 'Harness/a2a/sessions/**/events.jsonl'], args: ['pattern', 'since', 'limit'] },
97
- { name: 'send-input', writes: ['Harness/tasks/**/sessions/**/input-requests.jsonl', 'Harness/a2a/sessions/**/input-requests.jsonl'], args: ['session', 'text'] },
98
- ],
99
- defaultMode: 'watch',
100
- inputPolicy: 'attach-mode-required',
119
+ const DEFAULT_TERMINAL_CONTROL_SKILL = {
120
+ schemaVersion: 1,
121
+ skillId: 'terminal-control',
122
+ name: 'Terminal Control',
123
+ source: 'Harness/a2a/skills/terminal-control.json',
124
+ description: 'Bounded A2A tools for listing terminal sessions, reading output ranges, globbing terminal events, and attach-gated input forwarding.',
125
+ triggers: [
126
+ 'list sessions',
127
+ 'read terminal',
128
+ 'read transcript',
129
+ 'tail output',
130
+ 'send input',
131
+ 'send agent message',
132
+ 'broadcast agent message',
133
+ 'delegate agent',
134
+ 'agent messages',
135
+ 'read agent output',
136
+ 'bridge messages',
137
+ 'terminal events',
138
+ ],
139
+ tools: [
140
+ { name: 'list-sessions', reads: ['Harness/tasks/**/sessions/**/STATE.json', 'Harness/a2a/sessions/**/STATE.json'] },
141
+ { name: 'read-range', reads: ['Harness/tasks/**/sessions/**/terminal.jsonl', 'Harness/a2a/sessions/**/terminal.jsonl'], args: ['session', 'from', 'to', 'tail'] },
142
+ { name: 'glob-events', reads: ['Harness/tasks/**/sessions/**/events.jsonl', 'Harness/a2a/sessions/**/events.jsonl'], args: ['pattern', 'since', 'limit'] },
143
+ { name: 'send-input', writes: ['Harness/tasks/**/sessions/**/input-requests.jsonl', 'Harness/a2a/sessions/**/input-requests.jsonl'], args: ['session', 'text'] },
144
+ { name: 'send-agent-message', writes: ['Harness/tasks/**/sessions/**/input-requests.jsonl', 'Harness/a2a/sessions/**/input-requests.jsonl', 'Harness/a2a/bridges/**'], args: ['to', 'text'] },
145
+ { name: 'broadcast-agent-message', writes: ['Harness/tasks/**/sessions/**/input-requests.jsonl', 'Harness/a2a/sessions/**/input-requests.jsonl', 'Harness/a2a/bridges/**'], args: ['to', 'text', 'topic'] },
146
+ { name: 'read-agent-messages', reads: ['Harness/a2a/bridges/**'], args: ['peer', 'tail'] },
147
+ { name: 'delegate-agent', writes: ['Harness/tasks/**/sessions/**/input-requests.jsonl', 'Harness/a2a/sessions/**/input-requests.jsonl', 'Harness/a2a/bridges/**'], args: ['node', 'text'] },
148
+ { name: 'read-agent', reads: ['Harness/tasks/**/sessions/**/terminal.jsonl', 'Harness/a2a/sessions/**/terminal.jsonl'], args: ['node', 'action'] },
149
+ ],
150
+ defaultMode: 'watch',
151
+ inputPolicy: 'attach-mode-required',
152
+ };
153
+
154
+ const DEFAULT_WORKFLOW_SKILL_ENVIRONMENT = [
155
+ 'HARNESS_PEER_SESSION_ID',
156
+ 'HARNESS_PEER_RUNTIME',
157
+ 'HARNESS_AGENT_KIND',
158
+ 'HARNESS_WORKFLOW_MODE',
159
+ 'HARNESS_WORKFLOW_NODE_ID',
160
+ 'HARNESS_WORKFLOW_MAP',
161
+ 'HARNESS_NODE_HOME',
162
+ 'HARNESS_NODE_INIT',
163
+ 'HARNESS_WF_UI_URL',
164
+ ];
165
+
166
+ const DEFAULT_WORKFLOW_NODE_MAP_SKILL = {
167
+ schemaVersion: 1,
168
+ skillId: 'workflow-node-map',
169
+ name: 'Workflow Node Map',
170
+ source: 'Harness/a2a/skills/workflow-node-map.json',
171
+ description: 'Canonical API-only workflow node-map read and mutation commands for Main Agent nodes.',
172
+ triggers: [
173
+ 'read graph',
174
+ 'create node',
175
+ 'connect nodes',
176
+ 'disconnect nodes',
177
+ 'move node',
178
+ 'delete node',
179
+ 'spawn agent node',
180
+ 'delegate worker',
181
+ 'control canvas',
182
+ 'arrange workflow',
183
+ '控制画布',
184
+ '连接节点',
185
+ '删除节点',
186
+ '连接状态',
187
+ '连线',
188
+ '谁连着',
189
+ ],
190
+ commands: [
191
+ {
192
+ name: 'workflow-node-map-read',
193
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-map --action readGraph',
194
+ description: 'Main Agent only. Read the graph through the typed backend Agent action and record an operation.',
195
+ },
196
+ {
197
+ name: 'workflow-node-map-create',
198
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-map --action createNode --type markdown --title "..."',
199
+ description: 'Main Agent only. Create workflow nodes through POST /api/workflow/nodes/:actor/actions/agent.createNode.',
200
+ },
201
+ {
202
+ name: 'workflow-node-map-connect',
203
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-map --action connectNodes --from <nodeOrSession> --to <nodeOrSession>',
204
+ description: 'Main Agent only. Connect nodes through POST /api/workflow/nodes/:actor/actions/agent.connectNodes.',
205
+ },
206
+ {
207
+ name: 'workflow-node-map-disconnect',
208
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-map --action disconnectNodes --edge <edgeId>',
209
+ description: 'Main Agent only. Disconnect nodes through POST /api/workflow/nodes/:actor/actions/agent.disconnectNodes.',
210
+ },
211
+ {
212
+ name: 'workflow-node-map-move',
213
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-map --action moveNode --node <nodeId> --x 0 --y 0',
214
+ description: 'Main Agent only. Move a node through POST /api/workflow/nodes/:actor/actions/agent.moveNode.',
215
+ },
216
+ {
217
+ name: 'workflow-node-map-delete',
218
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-map --action deleteNode --node <nodeIdOrSessionId>',
219
+ description: 'Main Agent only. Delete or hide one node through POST /api/workflow/nodes/:actor/actions/agent.deleteNode.',
220
+ },
221
+ {
222
+ name: 'workflow-node-map-delete-all',
223
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-map --action deleteNodes --all true',
224
+ description: 'Main Agent only. Delete or hide all allowed nodes through agent.deleteNodes. The actor and live Agents are skipped by default.',
225
+ },
226
+ {
227
+ name: 'create-agent',
228
+ command: 'node Harness/scripts/wf-ui-control.mjs create-agent --agent-kind subagent --runtime claude --objective "..."',
229
+ description: 'Main Agent only. Create a managed PTY node through wf-ui.',
230
+ },
231
+ {
232
+ name: 'connect-agent-worker',
233
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-map --action connectNodes --from <mainAgentNodeId> --to <workerAgentNodeId> --relation delegation',
234
+ description: 'Main Agent only. Connect a managed Agent worker so it appears in connectedAgentRefs and can be delegated with delegate-agent.',
235
+ },
236
+ {
237
+ name: 'legacy-node-map',
238
+ command: 'node Harness/scripts/wf-ui-control.mjs node-map --action readGraph',
239
+ description: 'Compatibility alias. Prefer workflow-node-map for new Agent prompts and skills.',
240
+ },
241
+ ],
242
+ environment: DEFAULT_WORKFLOW_SKILL_ENVIRONMENT,
243
+ policy: {
244
+ mainAgent: [
245
+ 'api-only-node-map-control',
246
+ 'no-direct-workflow-map-writes',
247
+ 'workflow-node-map',
248
+ 'read-workflow-map-diagnostic-only',
249
+ 'agent.readGraph',
250
+ 'agent.createNode',
251
+ 'agent.connectNodes',
252
+ 'agent.disconnectNodes',
253
+ 'agent.moveNode',
254
+ 'agent.deleteNode',
255
+ 'agent.deleteNodes',
256
+ 'create-subagent-node',
257
+ 'connect-agent-worker',
258
+ 'connect-managed-nodes',
259
+ 'delete-saved-node',
260
+ 'stop-managed-node',
261
+ ],
262
+ subagent: ['read-workflow-map-diagnostic-only', 'return-evidence'],
263
+ subagentDenied: ['create-agent-node', 'create-task-node', 'workflow-node-map-control', 'node-map-control', 'workflow-map-file-mutation', 'spawn-pty-subagents'],
264
+ denied: ['no-source-file-reads-for-graph-state'],
265
+ },
266
+ };
267
+
268
+ const DEFAULT_WORKFLOW_CONTEXT_SKILL = {
269
+ schemaVersion: 1,
270
+ skillId: 'workflow-context',
271
+ name: 'Workflow Context',
272
+ source: 'Harness/a2a/skills/workflow-context.json',
273
+ description: 'Canonical read-only workflow context commands for Agent nodes. Uses GET /api/workflow/context/:node for connected refs.',
274
+ triggers: [
275
+ 'read context',
276
+ 'workflow context',
277
+ 'node map context',
278
+ 'graph context',
279
+ 'connected nodes',
280
+ 'what am I connected to',
281
+ 'what is connected',
282
+ 'nearby nodes',
283
+ 'linked nodes',
284
+ 'connected resources',
285
+ 'connected agents',
286
+ 'connected peers',
287
+ 'goal refs',
288
+ 'goal node',
289
+ 'timer refs',
290
+ 'timer node',
291
+ 'agent node',
292
+ 'node identity',
293
+ 'current node',
294
+ 'current task',
295
+ 'current goal',
296
+ 'workflow status',
297
+ 'edge refs',
298
+ 'handle refs',
299
+ 'port refs',
300
+ 'magnet dock',
301
+ 'resource node',
302
+ 'output node',
303
+ 'effective skills',
304
+ 'where am I',
305
+ 'observe before acting',
306
+ '感知节点',
307
+ '读取上下文',
308
+ '连接了什么',
309
+ '连接的节点',
310
+ '周围节点',
311
+ '节点身份',
312
+ '当前节点',
313
+ '当前任务',
314
+ '当前目标',
315
+ '磁吸连接',
316
+ '连接状态',
317
+ '是否有连接',
318
+ '检查连接',
319
+ '连线',
320
+ '谁连着',
321
+ '我连着',
322
+ '自己连接',
323
+ 'node map 连接',
324
+ ],
325
+ commands: [
326
+ {
327
+ name: 'self',
328
+ command: 'node Harness/scripts/wf-ui-control.mjs self',
329
+ description: 'Print this PTY node identity and workflow environment.',
330
+ },
331
+ {
332
+ name: 'describe',
333
+ command: 'node Harness/scripts/wf-ui-control.mjs describe',
334
+ description: 'Read-only topology summary from the wf-ui snapshot when available. Local workflow-map fallback is diagnostic only.',
335
+ },
336
+ {
337
+ name: 'snapshot',
338
+ command: 'node Harness/scripts/wf-ui-control.mjs snapshot',
339
+ description: 'Read the complete wf-ui workflow snapshot. Do not mutate node-map files from this output.',
340
+ },
341
+ {
342
+ name: 'workflow-context-self',
343
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-context',
344
+ description: 'Read this node context, including connected peers, connectedAgentRefs, connectedResourceRefs, connectedGoalRefs, connectedEventRefs, connectedCapabilityRefs, and effectiveSkills.',
345
+ },
346
+ {
347
+ name: 'workflow-context-node',
348
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-context --node <graphNodeIdOrSessionId>',
349
+ description: 'Read another workflow node context through GET /api/workflow/context/:node.',
350
+ },
351
+ {
352
+ name: 'bridge-messages',
353
+ command: 'node Harness/scripts/wf-ui-control.mjs bridge-messages --from <sessionId> --to <sessionId>',
354
+ description: 'Main Agent only. Read the recorded wf-bridge conversation between two managed sessions.',
355
+ },
356
+ ],
357
+ environment: DEFAULT_WORKFLOW_SKILL_ENVIRONMENT,
358
+ policy: {
359
+ mainAgent: [
360
+ 'workflow-context',
361
+ 'read-connected-context',
362
+ 'read-workflow-snapshot',
363
+ 'read-workflow-map-diagnostic-only',
364
+ 'read-bridge-messages',
365
+ ],
366
+ subagent: [
367
+ 'workflow-context',
368
+ 'read-self-context',
369
+ 'read-workflow-map-diagnostic-only',
370
+ 'return-evidence',
371
+ ],
372
+ denied: [
373
+ 'no-source-file-reads-for-graph-state',
374
+ 'workflow-map-file-mutation',
375
+ 'component-state-file-mutation',
376
+ 'event-state-file-mutation',
377
+ 'goal-state-file-mutation',
378
+ 'capability-state-file-mutation',
379
+ 'direct-json-state-mutation',
380
+ 'node-home-file-mutation',
381
+ 'task-state-mutation',
382
+ ],
383
+ },
384
+ };
385
+
386
+ const DEFAULT_WORKFLOW_ONTOLOGY_SKILL = {
387
+ schemaVersion: 1,
388
+ skillId: 'workflow-ontology',
389
+ name: 'Workflow Ontology',
390
+ source: 'Harness/a2a/skills/workflow-ontology.json',
391
+ description: 'Canonical read-only semantic model for Agent nodes. Uses GET /api/workflow/ontology so Agents understand node classes, relations, and action affordances before acting.',
392
+ triggers: [
393
+ 'workflow semantics',
394
+ 'node ontology',
395
+ 'node classes',
396
+ 'relation semantics',
397
+ 'action affordances',
398
+ 'choose capability',
399
+ 'choose connected node',
400
+ 'before acting',
401
+ '语义',
402
+ '节点能力',
403
+ ],
404
+ commands: [
405
+ {
406
+ name: 'workflow-ontology',
407
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-ontology',
408
+ description: 'Read the workflow ontology: node classes, relation semantics, action rules, and Agent operation language.',
409
+ },
410
+ {
411
+ name: 'workflow-context-after-ontology',
412
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-context',
413
+ description: 'Read this Agent context after loading the ontology. Context should be interpreted through connected refs and affordances, not raw graph shape alone.',
414
+ },
415
+ {
416
+ name: 'workflow-node-action-after-ontology',
417
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <graphNodeIdOrSessionId> --action <type.action>',
418
+ description: 'Only run typed node actions that the ontology/context affordances allow for the acting Agent and edge relation.',
419
+ },
420
+ ],
421
+ environment: DEFAULT_WORKFLOW_SKILL_ENVIRONMENT,
422
+ policy: {
423
+ mainAgent: [
424
+ 'workflow-ontology',
425
+ 'read-semantic-node-model',
426
+ 'read-action-affordances',
427
+ 'read-before-acting',
428
+ 'prefer-connected-resource-output',
429
+ 'prefer-typed-backend-actions',
430
+ ],
431
+ subagent: [
432
+ 'workflow-ontology',
433
+ 'read-semantic-node-model',
434
+ 'return-evidence',
435
+ ],
436
+ denied: [
437
+ 'workflow-map-file-mutation',
438
+ 'component-state-file-mutation',
439
+ 'event-state-file-mutation',
440
+ 'capability-state-file-mutation',
441
+ 'goal-state-file-mutation',
442
+ 'task-state-mutation',
443
+ 'invented-node-actions',
444
+ ],
445
+ },
446
+ };
447
+
448
+ const DEFAULT_WORKFLOW_NODE_ACTIONS_SKILL = {
449
+ schemaVersion: 1,
450
+ skillId: 'workflow-node-actions',
451
+ name: 'Workflow Node Actions',
452
+ source: 'Harness/a2a/skills/workflow-node-actions.json',
453
+ description: 'Canonical typed node action command surface. Uses POST /api/workflow/nodes/:node/actions/:action.',
454
+ triggers: [
455
+ 'node action',
456
+ 'write markdown',
457
+ 'append markdown',
458
+ 'draw diagram',
459
+ 'draw flowchart',
460
+ 'update excalidraw',
461
+ 'render preview',
462
+ 'read timer',
463
+ 'configure timer',
464
+ 'timer interval',
465
+ 'timer control',
466
+ 'start timer',
467
+ 'stop timer',
468
+ 'update goal',
469
+ 'send agent message',
470
+ 'broadcast agent message',
471
+ 'reply agent',
472
+ 'read bridge messages',
473
+ 'shared markdown context',
474
+ 'agent direct message',
475
+ 'agent group message',
476
+ '发送消息',
477
+ '群发消息',
478
+ '回复 Agent',
479
+ '共享上下文',
480
+ '共享 Markdown',
481
+ 'delegate agent',
482
+ 'read agent output',
483
+ '绘制流程图',
484
+ '写入节点',
485
+ '回读节点',
486
+ ],
487
+ commands: [
488
+ {
489
+ name: 'workflow-node-action',
490
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <graphNodeIdOrSessionId> --action <type.action>',
491
+ description: 'Run a typed backend node action with optional --payload JSON. The CLI includes actorNodeId from the Agent environment when available.',
492
+ },
493
+ {
494
+ name: 'markdown-read',
495
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <markdownNodeId> --action markdown.read',
496
+ description: 'Read Markdown node content through the typed Markdown adapter.',
497
+ },
498
+ {
499
+ name: 'markdown-replace',
500
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <markdownNodeId> --action markdown.replace --payload "{\\"markdown\\":\\"...\\"}"',
501
+ description: 'Replace connected Markdown node content. Prefer this for durable text output when a writable Markdown node is connected.',
502
+ },
503
+ {
504
+ name: 'markdown-append',
505
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <markdownNodeId> --action markdown.append --payload "{\\"markdown\\":\\"...\\"}"',
506
+ description: 'Append to connected Markdown node content for incremental notes or reports.',
507
+ },
508
+ {
509
+ name: 'excalidraw-read-scene',
510
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <excalidrawNodeId> --action excalidraw.readScene',
511
+ description: 'Read connected Excalidraw scene content through the typed Excalidraw adapter.',
512
+ },
513
+ {
514
+ name: 'excalidraw-save-scene',
515
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <excalidrawNodeId> --action excalidraw.saveScene --payload "{\\"scene\\":{\\"elements\\":[],\\"appState\\":{},\\"files\\":{}}}"',
516
+ description: 'Replace connected Excalidraw scene content. Prefer this for diagrams, flowcharts, visual plans, and sketches when a writable Excalidraw node is connected.',
517
+ },
518
+ {
519
+ name: 'excalidraw-patch-scene',
520
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <excalidrawNodeId> --action excalidraw.patchScene --payload "{\\"patch\\":{\\"elements\\":[]}}"',
521
+ description: 'Patch connected Excalidraw scene content for incremental diagram updates.',
522
+ },
523
+ {
524
+ name: 'timer-configure',
525
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <timerNodeId> --action timer.configure --payload "{\\"schedule\\":{}}"',
526
+ description: 'Configure Timer metadata through the typed Timer adapter. Agent-authored timer.configure requires a control edge and must never edit Harness/a2a/event-nodes/**/state.json directly.',
527
+ },
528
+ {
529
+ name: 'timer-read',
530
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <timerNodeId> --action timer.read',
531
+ description: 'Read Timer state through the typed Timer adapter.',
532
+ },
533
+ {
534
+ name: 'timer-set-interval',
535
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <timerNodeId> --action timer.setInterval --payload "{\\"intervalSeconds\\":120,\\"lane\\":\\"base\\"}"',
536
+ description: 'Set the Timer interval through the backend Timer adapter. Requires connectedEventRefs[].allowedActions to include timer.setInterval.',
537
+ },
538
+ {
539
+ name: 'timer-enable',
540
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <timerNodeId> --action timer.enable',
541
+ description: 'Start or resume a Timer through the backend Timer adapter. Requires a Timer control edge for Agent-authored actions.',
542
+ },
543
+ {
544
+ name: 'timer-disable',
545
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <timerNodeId> --action timer.disable',
546
+ description: 'Stop a Timer through the backend Timer adapter. Requires a Timer control edge and controlPolicy.agentCanDisable.',
547
+ },
548
+ {
549
+ name: 'goal-update',
550
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <goalNodeId> --action goal.update --payload "{\\"nextAction\\":\\"...\\",\\"planItems\\":[{\\"id\\":\\"P-001\\",\\"text\\":\\"...\\",\\"status\\":\\"todo\\"}]}"',
551
+ description: 'Update editable Goal metadata and Agent-authored plan items through the typed Goal adapter. Agent-authored goal.update requires a linked Goal edge.',
552
+ },
553
+ {
554
+ name: 'send-input',
555
+ command: 'node Harness/scripts/wf-ui-control.mjs send-input --session <sessionId> --text "..."',
556
+ description: 'Main Agent only. Send terminal input to a managed PTY node and record a wf-bridge message.',
557
+ },
558
+ {
559
+ name: 'send-agent-message',
560
+ command: 'node Harness/scripts/wf-ui-control.mjs send-agent-message --to <agentNodeIdOrSessionId> --text "..."',
561
+ description: 'Send a one-to-one message from this Agent node to a connected Agent node through agent.sendMessage and record a wf-bridge envelope.',
562
+ },
563
+ {
564
+ name: 'broadcast-agent-message',
565
+ command: 'node Harness/scripts/wf-ui-control.mjs broadcast-agent-message --text "..." --topic <topic>',
566
+ description: 'Broadcast one message from this Agent node to connected Agent nodes, or to an explicit --to list, through agent.broadcastMessage.',
567
+ },
568
+ {
569
+ name: 'read-agent-messages',
570
+ command: 'node Harness/scripts/wf-ui-control.mjs read-agent-messages --peer <agentNodeIdOrSessionId> --tail 80',
571
+ description: 'Read recorded wf-bridge message envelopes between this Agent node and a connected peer.',
572
+ },
573
+ {
574
+ name: 'delegate-agent',
575
+ command: 'node Harness/scripts/wf-ui-control.mjs delegate-agent --node <agentNodeIdOrSessionId> --text "..."',
576
+ description: 'Main Agent only. Delegate work to a connected Agent node by graph node id or session id through agent.sendInput.',
577
+ },
578
+ {
579
+ name: 'read-agent',
580
+ command: 'node Harness/scripts/wf-ui-control.mjs read-agent --node <agentNodeIdOrSessionId> --action output --tail 80',
581
+ description: 'Read a connected Agent node output, transcript, or context through typed agent.readOutput/readTranscript/readContext.',
582
+ },
583
+ ],
584
+ environment: DEFAULT_WORKFLOW_SKILL_ENVIRONMENT,
585
+ policy: {
586
+ mainAgent: [
587
+ 'workflow-node-actions',
588
+ 'typed-backend-node-actions',
589
+ 'read-before-mutating',
590
+ 'verify-connected-context',
591
+ 'prefer-connected-resource-output',
592
+ 'prefer-connected-agent-workers',
593
+ 'prefer-agent-message-actions',
594
+ 'prefer-shared-markdown-context',
595
+ 'api-only-node-state-control',
596
+ 'timer-typed-actions',
597
+ 'send-terminal-input',
598
+ ],
599
+ subagent: [
600
+ 'read-actions-only-when-authorized',
601
+ 'send-message-to-connected-agent-peers',
602
+ 'reply-through-agent-message-actions',
603
+ 'return-evidence',
604
+ ],
605
+ denied: [
606
+ 'no-source-file-reads-for-graph-state',
607
+ 'workflow-map-file-mutation',
608
+ 'component-state-file-mutation',
609
+ 'event-state-file-mutation',
610
+ 'direct-event-node-state-file-edit',
611
+ 'event-node-state-json-mutation',
612
+ 'capability-state-file-mutation',
613
+ 'goal-state-file-mutation',
614
+ 'direct-goal-node-state-file-edit',
615
+ 'direct-json-state-mutation',
616
+ 'task-state-mutation',
617
+ 'secret-payloads',
618
+ ],
619
+ },
620
+ };
621
+
622
+ const DEFAULT_WORKFLOW_TIMER_NODE_SKILL = {
623
+ schemaVersion: 1,
624
+ skillId: 'workflow-timer-node',
625
+ name: 'Workflow Timer Node',
626
+ source: 'Harness/a2a/skills/workflow-timer-node.json',
627
+ description: 'On-demand Agent guide for controlling Timer nodes through typed backend actions only.',
628
+ nodeType: 'timer',
629
+ triggers: [
630
+ 'timer node',
631
+ 'timer control',
632
+ 'timer interval',
633
+ 'configure timer',
634
+ 'start timer',
635
+ 'stop timer',
636
+ 'timer countdown',
637
+ 'health check timer',
638
+ 'connected timer',
639
+ ],
640
+ controlSurface: {
641
+ endpoint: 'POST /api/workflow/nodes/:node/actions/:action',
642
+ readContextFirst: 'node Harness/scripts/wf-ui-control.mjs workflow-context --project .',
643
+ storage: 'Harness/a2a/event-nodes/**/state.json is backend-owned diagnostic storage, never an Agent control surface.',
644
+ connectionRequirement: 'Agent-authored Timer control requires an Agent -> Timer relation=control edge. Timer -> Agent relation=event only delivers events.',
645
+ },
646
+ commands: [
647
+ {
648
+ name: 'timer-context',
649
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-context --project .',
650
+ description: 'Read connectedEventRefs and confirm canControl/allowedActions before controlling a Timer.',
651
+ },
652
+ {
653
+ name: 'timer-read',
654
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <timerNodeId> --action timer.read --project .',
655
+ description: 'Read Timer state through the backend Timer adapter.',
656
+ },
657
+ {
658
+ name: 'timer-set-interval',
659
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <timerNodeId> --action timer.setInterval --payload "{\\"intervalSeconds\\":120,\\"lane\\":\\"base\\"}" --project .',
660
+ description: 'Set base loop interval through the backend. Requires connectedEventRefs[].allowedActions to include timer.setInterval.',
661
+ },
662
+ {
663
+ name: 'timer-enable',
664
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <timerNodeId> --action timer.enable --project .',
665
+ description: 'Start or resume Timer scheduling through the backend.',
666
+ },
667
+ {
668
+ name: 'timer-disable',
669
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <timerNodeId> --action timer.disable --project .',
670
+ description: 'Stop Timer scheduling through the backend. Requires controlPolicy.agentCanDisable.',
671
+ },
672
+ {
673
+ name: 'timer-set-mode',
674
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <timerNodeId> --action timer.setMode --payload "{\\"mode\\":\\"loop\\"}" --project .',
675
+ description: 'Change Timer schedule mode through the backend when policy allows.',
676
+ },
677
+ {
678
+ name: 'timer-ack-watchdog',
679
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <timerNodeId> --action timer.ackWatchdog --payload "{\\"now\\":\\"<iso>\\"}" --project .',
680
+ description: 'Acknowledge Timer watchdog through the backend when policy allows.',
681
+ },
682
+ {
683
+ name: 'timer-reset-watchdog',
684
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <timerNodeId> --action timer.resetWatchdog --payload "{\\"now\\":\\"<iso>\\"}" --project .',
685
+ description: 'Reset Timer watchdog state through the backend when policy allows.',
686
+ },
687
+ ],
688
+ environment: DEFAULT_WORKFLOW_SKILL_ENVIRONMENT,
689
+ policy: {
690
+ allowed: ['timer.read', 'timer.configure', 'timer.enable', 'timer.disable', 'timer.setInterval', 'timer.setMode', 'timer.ackWatchdog', 'timer.resetWatchdog'],
691
+ requires: ['workflow-context-before-action', 'timer-control-edge-for-agent-actions', 'typed-backend-node-actions'],
692
+ denied: [
693
+ 'direct-event-node-state-file-edit',
694
+ 'event-node-state-json-mutation',
695
+ 'direct-json-state-mutation',
696
+ 'workflow-map-file-mutation',
697
+ 'invented-timer-actions',
698
+ ],
699
+ },
700
+ };
701
+
702
+ const DEFAULT_WORKFLOW_GOAL_NODE_SKILL = {
703
+ schemaVersion: 1,
704
+ skillId: 'workflow-goal-node',
705
+ name: 'Workflow Goal Node',
706
+ source: 'Harness/a2a/skills/workflow-goal-node.json',
707
+ description: 'On-demand Agent guide for reading and updating Goal nodes through typed backend actions only.',
708
+ nodeType: 'goal',
709
+ triggers: [
710
+ 'goal node',
711
+ 'goal update',
712
+ 'acceptance',
713
+ 'request completion',
714
+ 'return to work',
715
+ 'goal watchdog',
716
+ 'connected goal',
717
+ ],
718
+ controlSurface: {
719
+ endpoint: 'POST /api/workflow/nodes/:node/actions/:action',
720
+ readContextFirst: 'node Harness/scripts/wf-ui-control.mjs workflow-context --project .',
721
+ storage: 'Goal/task state sidecars are backend-owned diagnostic storage, never an Agent control surface.',
722
+ connectionRequirement: 'Agent-authored Goal edits require a linked bidirectional goal edge.',
723
+ },
724
+ commands: [
725
+ {
726
+ name: 'goal-read',
727
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <goalNodeId> --action goal.read --project .',
728
+ description: 'Read Goal state through the backend Goal adapter.',
729
+ },
730
+ {
731
+ name: 'goal-update',
732
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <goalNodeId> --action goal.update --payload "{\\"nextAction\\":\\"...\\",\\"planItems\\":[{\\"id\\":\\"P-001\\",\\"text\\":\\"...\\",\\"status\\":\\"todo\\"}]}" --project .',
733
+ description: 'Update editable Goal metadata and Agent-authored plan items through the backend.',
734
+ },
735
+ {
736
+ name: 'goal-request-completion',
737
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <goalNodeId> --action goal.requestCompletion --payload "{\\"evidenceRefs\\":[\\"...\\"],\\"note\\":\\"...\\"}" --project .',
738
+ description: 'Request Goal completion without directly completing task state.',
739
+ },
740
+ {
741
+ name: 'goal-return-to-work',
742
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <goalNodeId> --action goal.returnToWork --payload "{\\"note\\":\\"...\\"}" --project .',
743
+ description: 'Return a proposed-complete Goal to active work through the backend.',
744
+ },
745
+ {
746
+ name: 'goal-ack-watchdog',
747
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <goalNodeId> --action goal.ackWatchdog --payload "{\\"timerNodeId\\":\\"<timerNodeId>\\"}" --project .',
748
+ description: 'Acknowledge Goal watchdog state through the backend.',
749
+ },
750
+ ],
751
+ environment: DEFAULT_WORKFLOW_SKILL_ENVIRONMENT,
752
+ policy: {
753
+ allowed: ['goal.read', 'goal.update', 'goal.requestCompletion', 'goal.returnToWork', 'goal.ackWatchdog', 'goal.add', 'goal.delete', 'goal.replace', 'goal.check', 'goal.uncheck', 'goal.complete', 'goal.reopen'],
754
+ requires: ['workflow-context-before-action', 'linked-goal-edge-for-agent-actions', 'typed-backend-node-actions'],
755
+ denied: [
756
+ 'direct-goal-node-state-file-edit',
757
+ 'goal-node-state-json-mutation',
758
+ 'task-state-mutation',
759
+ 'direct-json-state-mutation',
760
+ 'workflow-map-file-mutation',
761
+ ],
762
+ },
763
+ };
764
+
765
+ const DEFAULT_WORKFLOW_AGENT_NODE_SKILL = {
766
+ schemaVersion: 1,
767
+ skillId: 'workflow-agent-node',
768
+ name: 'Workflow Agent Node',
769
+ source: 'Harness/a2a/skills/workflow-agent-node.json',
770
+ description: 'On-demand Agent guide for delegating to and reading connected Agent nodes through backend and wf-bridge actions.',
771
+ nodeType: 'agent',
772
+ triggers: [
773
+ 'agent node',
774
+ 'delegate agent',
775
+ 'send agent message',
776
+ 'broadcast agent message',
777
+ 'reply agent',
778
+ 'agent direct message',
779
+ 'agent group message',
780
+ 'shared context',
781
+ '发送消息',
782
+ '群发消息',
783
+ '回复 Agent',
784
+ '共享上下文',
785
+ '共享 Markdown',
786
+ 'read agent output',
787
+ 'read transcript',
788
+ 'start agent',
789
+ 'stop agent',
790
+ 'connected agent',
791
+ ],
792
+ controlSurface: {
793
+ sendMessage: 'POST /api/workflow/nodes/:sender/actions/agent.sendMessage',
794
+ broadcastMessage: 'POST /api/workflow/nodes/:sender/actions/agent.broadcastMessage',
795
+ readMessages: 'POST /api/workflow/nodes/:sender/actions/agent.readMessages',
796
+ sendInput: 'POST /api/workflow/nodes/:node/actions/agent.sendInput',
797
+ readOutput: 'POST /api/workflow/nodes/:node/actions/agent.readOutput',
798
+ bridge: 'wf-bridge routes record Agent-to-Agent communication.',
799
+ storage: 'Harness/a2a/nodes/** and terminal logs are runtime records, not manual control surfaces.',
800
+ },
801
+ commands: [
802
+ {
803
+ name: 'agent-context',
804
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-context --node <agentNodeIdOrSessionId> --project .',
805
+ description: 'Read a connected Agent node context through the backend.',
806
+ },
807
+ {
808
+ name: 'delegate-agent',
809
+ command: 'node Harness/scripts/wf-ui-control.mjs delegate-agent --node <agentNodeIdOrSessionId> --text "..." --project .',
810
+ description: 'Delegate work to a connected Agent node through agent.sendInput.',
811
+ },
812
+ {
813
+ name: 'send-agent-message',
814
+ command: 'node Harness/scripts/wf-ui-control.mjs send-agent-message --to <agentNodeIdOrSessionId> --text "..." --project .',
815
+ description: 'Send a direct connected Agent-to-Agent message through agent.sendMessage. Use this for replies as well as first contact.',
816
+ },
817
+ {
818
+ name: 'broadcast-agent-message',
819
+ command: 'node Harness/scripts/wf-ui-control.mjs broadcast-agent-message --text "..." --topic <topic> --project .',
820
+ description: 'Broadcast a message to all connected Agent peers, or pass --to a,b,c for an explicit connected group.',
821
+ },
822
+ {
823
+ name: 'read-agent-messages',
824
+ command: 'node Harness/scripts/wf-ui-control.mjs read-agent-messages --peer <agentNodeIdOrSessionId> --tail 80 --project .',
825
+ description: 'Read recorded wf-bridge message envelopes between this Agent and a connected peer.',
826
+ },
827
+ {
828
+ name: 'read-agent-output',
829
+ command: 'node Harness/scripts/wf-ui-control.mjs read-agent --node <agentNodeIdOrSessionId> --action output --tail 80 --project .',
830
+ description: 'Read connected Agent output through typed backend routes.',
831
+ },
832
+ {
833
+ name: 'read-agent-transcript',
834
+ command: 'node Harness/scripts/wf-ui-control.mjs read-agent --node <agentNodeIdOrSessionId> --action transcript --tail 120 --project .',
835
+ description: 'Read connected Agent transcript through typed backend routes.',
836
+ },
837
+ {
838
+ name: 'bridge-messages',
839
+ command: 'node Harness/scripts/wf-ui-control.mjs bridge-messages --from <sessionId> --to <sessionId> --project .',
840
+ description: 'Read recorded wf-bridge messages between managed sessions.',
841
+ },
842
+ ],
843
+ environment: DEFAULT_WORKFLOW_SKILL_ENVIRONMENT,
844
+ policy: {
845
+ allowed: ['agent.readContext', 'agent.readOutput', 'agent.readTranscript', 'agent.sendMessage', 'agent.broadcastMessage', 'agent.readMessages', 'agent.sendInput'],
846
+ requires: ['connected-agent-ref', 'typed-backend-agent-actions'],
847
+ denied: [
848
+ 'node-home-file-mutation',
849
+ 'terminal-log-mutation',
850
+ 'session-state-file-mutation',
851
+ 'direct-json-state-mutation',
852
+ 'unmanaged-pty-spawn',
853
+ ],
854
+ },
855
+ };
856
+
857
+ const DEFAULT_WORKFLOW_RESOURCE_NODE_SKILL = {
858
+ schemaVersion: 1,
859
+ skillId: 'workflow-resource-node',
860
+ name: 'Workflow Resource Node',
861
+ source: 'Harness/a2a/skills/workflow-resource-node.json',
862
+ description: 'On-demand Agent guide for File, Markdown, and Excalidraw resource nodes through typed backend actions only.',
863
+ nodeType: 'resource',
864
+ triggers: [
865
+ 'resource node',
866
+ 'markdown node',
867
+ 'file node',
868
+ 'diagram node',
869
+ 'excalidraw node',
870
+ 'connected output',
871
+ 'write notes',
872
+ 'draw diagram',
873
+ ],
874
+ controlSurface: {
875
+ endpoint: 'POST /api/workflow/nodes/:node/actions/:action',
876
+ readContextFirst: 'node Harness/scripts/wf-ui-control.mjs workflow-context --project .',
877
+ storage: 'Harness/a2a/component-nodes/**/state.json is backend-owned diagnostic storage, never an Agent control surface.',
878
+ outputRouting: 'Prefer connected writable resource nodes over terminal prose for durable output.',
879
+ },
880
+ commands: [
881
+ {
882
+ name: 'markdown-read',
883
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <markdownNodeId> --action markdown.read --project .',
884
+ description: 'Read Markdown content through the backend Markdown adapter.',
885
+ },
886
+ {
887
+ name: 'markdown-replace',
888
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <markdownNodeId> --action markdown.replace --payload "{\\"markdown\\":\\"...\\"}" --project .',
889
+ description: 'Replace Markdown content through the backend Markdown adapter.',
890
+ },
891
+ {
892
+ name: 'markdown-append',
893
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <markdownNodeId> --action markdown.append --payload "{\\"markdown\\":\\"...\\"}" --project .',
894
+ description: 'Append Markdown content through the backend Markdown adapter.',
895
+ },
896
+ {
897
+ name: 'excalidraw-save-scene',
898
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <excalidrawNodeId> --action excalidraw.saveScene --payload "{\\"scene\\":{\\"elements\\":[],\\"appState\\":{},\\"files\\":{}}}" --project .',
899
+ description: 'Save an Excalidraw scene through the backend Excalidraw adapter.',
900
+ },
901
+ {
902
+ name: 'file-read-text',
903
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <fileNodeId> --action file.readText --payload "{\\"offset\\":0,\\"limit\\":8192}" --project .',
904
+ description: 'Read bounded text from a File node through the backend File adapter.',
905
+ },
906
+ {
907
+ name: 'file-read-meta',
908
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <fileNodeId> --action file.readMeta --project .',
909
+ description: 'Read File node metadata through the backend File adapter.',
910
+ },
911
+ ],
912
+ environment: DEFAULT_WORKFLOW_SKILL_ENVIRONMENT,
913
+ policy: {
914
+ allowed: ['markdown.read', 'markdown.replace', 'markdown.append', 'markdown.patch', 'markdown.find', 'markdown.acquireLock', 'markdown.releaseLock', 'excalidraw.readScene', 'excalidraw.saveScene', 'excalidraw.patchScene', 'excalidraw.renderPreview', 'file.readMeta', 'file.readText', 'file.readBytes', 'file.refresh'],
915
+ requires: ['workflow-context-before-action', 'typed-backend-node-actions'],
916
+ denied: [
917
+ 'component-state-file-mutation',
918
+ 'direct-component-node-state-file-edit',
919
+ 'direct-json-state-mutation',
920
+ 'workflow-map-file-mutation',
921
+ 'secret-payloads',
922
+ ],
923
+ },
924
+ };
925
+
926
+ const DEFAULT_WORKFLOW_MARKDOWN_NODE_SKILL = {
927
+ schemaVersion: 1,
928
+ skillId: 'workflow-markdown-node',
929
+ name: 'Workflow Markdown Node',
930
+ source: 'Harness/a2a/skills/workflow-markdown-node.json',
931
+ description: 'On-demand Agent guide for reading and writing Markdown nodes through typed backend actions only.',
932
+ nodeType: 'markdown',
933
+ triggers: [
934
+ 'markdown node',
935
+ 'write notes',
936
+ 'append markdown',
937
+ 'replace markdown',
938
+ 'connected markdown',
939
+ 'durable text output',
940
+ ],
941
+ controlSurface: {
942
+ endpoint: 'POST /api/workflow/nodes/:node/actions/:action',
943
+ readContextFirst: 'node Harness/scripts/wf-ui-control.mjs workflow-context --project .',
944
+ storage: 'Harness/a2a/component-nodes/**/state.json is backend-owned diagnostic storage, never an Agent control surface.',
945
+ outputRouting: 'Prefer connected writable Markdown nodes for durable notes, reports, task plans, and long-form text.',
946
+ },
947
+ commands: [
948
+ {
949
+ name: 'markdown-read',
950
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <markdownNodeId> --action markdown.read --project .',
951
+ description: 'Read Markdown content through the backend Markdown adapter.',
952
+ },
953
+ {
954
+ name: 'markdown-append',
955
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <markdownNodeId> --action markdown.append --payload "{\\"markdown\\":\\"...\\"}" --project .',
956
+ description: 'Append Markdown content through the backend Markdown adapter.',
957
+ },
958
+ {
959
+ name: 'markdown-replace',
960
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <markdownNodeId> --action markdown.replace --payload "{\\"markdown\\":\\"...\\"}" --project .',
961
+ description: 'Replace Markdown content through the backend Markdown adapter.',
962
+ },
963
+ {
964
+ name: 'markdown-patch',
965
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <markdownNodeId> --action markdown.patch --payload "{\\"patch\\":\\"...\\"}" --project .',
966
+ description: 'Patch Markdown content through the backend when a patch payload is available.',
967
+ },
968
+ ],
969
+ environment: DEFAULT_WORKFLOW_SKILL_ENVIRONMENT,
970
+ policy: {
971
+ allowed: ['markdown.read', 'markdown.append', 'markdown.replace', 'markdown.patch', 'markdown.find', 'markdown.acquireLock', 'markdown.releaseLock'],
972
+ requires: ['workflow-context-before-action', 'connected-resource-ref', 'typed-backend-node-actions'],
973
+ denied: [
974
+ 'component-state-file-mutation',
975
+ 'direct-component-node-state-file-edit',
976
+ 'direct-json-state-mutation',
977
+ 'workflow-map-file-mutation',
978
+ ],
979
+ },
980
+ };
981
+
982
+ const DEFAULT_WORKFLOW_DIAGRAM_NODE_SKILL = {
983
+ schemaVersion: 1,
984
+ skillId: 'workflow-diagram-node',
985
+ name: 'Workflow Diagram Node',
986
+ source: 'Harness/a2a/skills/workflow-diagram-node.json',
987
+ description: 'On-demand Agent guide for reading and writing Excalidraw Diagram nodes through typed backend actions only.',
988
+ nodeType: 'excalidraw',
989
+ createKind: 'diagram',
990
+ triggers: [
991
+ 'diagram node',
992
+ 'excalidraw node',
993
+ 'draw diagram',
994
+ 'draw flowchart',
995
+ 'save scene',
996
+ 'connected diagram',
997
+ 'visual output',
998
+ ],
999
+ controlSurface: {
1000
+ endpoint: 'POST /api/workflow/nodes/:node/actions/:action',
1001
+ readContextFirst: 'node Harness/scripts/wf-ui-control.mjs workflow-context --project .',
1002
+ storage: 'Harness/a2a/component-nodes/**/state.json is backend-owned diagnostic storage, never an Agent control surface.',
1003
+ outputRouting: 'Prefer connected writable Diagram nodes for flowcharts, sketches, architecture maps, and visual plans.',
1004
+ },
1005
+ commands: [
1006
+ {
1007
+ name: 'excalidraw-read-scene',
1008
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <diagramNodeId> --action excalidraw.readScene --project .',
1009
+ description: 'Read an Excalidraw scene through the backend Excalidraw adapter.',
1010
+ },
1011
+ {
1012
+ name: 'excalidraw-save-scene',
1013
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <diagramNodeId> --action excalidraw.saveScene --payload "{\\"scene\\":{\\"elements\\":[],\\"appState\\":{},\\"files\\":{}}}" --project .',
1014
+ description: 'Replace the Excalidraw scene through the backend Excalidraw adapter.',
1015
+ },
1016
+ {
1017
+ name: 'excalidraw-patch-scene',
1018
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <diagramNodeId> --action excalidraw.patchScene --payload "{\\"elements\\":[]}" --project .',
1019
+ description: 'Patch Excalidraw scene elements through the backend when a bounded patch is available.',
1020
+ },
1021
+ ],
1022
+ environment: DEFAULT_WORKFLOW_SKILL_ENVIRONMENT,
1023
+ policy: {
1024
+ allowed: ['excalidraw.readScene', 'excalidraw.saveScene', 'excalidraw.patchScene', 'excalidraw.renderPreview'],
1025
+ requires: ['workflow-context-before-action', 'connected-resource-ref', 'typed-backend-node-actions'],
1026
+ denied: [
1027
+ 'component-state-file-mutation',
1028
+ 'direct-component-node-state-file-edit',
1029
+ 'direct-json-state-mutation',
1030
+ 'workflow-map-file-mutation',
1031
+ ],
1032
+ },
1033
+ };
1034
+
1035
+ const DEFAULT_WORKFLOW_FILE_NODE_SKILL = {
1036
+ schemaVersion: 1,
1037
+ skillId: 'workflow-file-node',
1038
+ name: 'Workflow File Node',
1039
+ source: 'Harness/a2a/skills/workflow-file-node.json',
1040
+ description: 'On-demand Agent guide for inspecting File nodes through typed backend actions only.',
1041
+ nodeType: 'file',
1042
+ triggers: [
1043
+ 'file node',
1044
+ 'read file',
1045
+ 'file metadata',
1046
+ 'workspace file',
1047
+ 'uploaded file',
1048
+ 'connected file',
1049
+ ],
1050
+ controlSurface: {
1051
+ endpoint: 'POST /api/workflow/nodes/:node/actions/:action',
1052
+ readContextFirst: 'node Harness/scripts/wf-ui-control.mjs workflow-context --project .',
1053
+ storage: 'Harness/a2a/component-nodes/**/state.json is backend-owned diagnostic storage, never an Agent control surface.',
1054
+ contentRoute: 'Use backend file/meta/text routes exposed by the File node adapter; do not infer absolute filesystem paths from state files.',
1055
+ },
1056
+ commands: [
1057
+ {
1058
+ name: 'file-read-meta',
1059
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <fileNodeId> --action file.readMeta --project .',
1060
+ description: 'Read File node metadata through the backend File adapter.',
1061
+ },
1062
+ {
1063
+ name: 'file-read-text',
1064
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <fileNodeId> --action file.readText --payload "{\\"offset\\":0,\\"limit\\":8192}" --project .',
1065
+ description: 'Read bounded text from a File node through the backend File adapter.',
1066
+ },
1067
+ {
1068
+ name: 'file-refresh',
1069
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-node-action --node <fileNodeId> --action file.refresh --project .',
1070
+ description: 'Refresh File node metadata through the backend when the underlying file may have changed.',
1071
+ },
1072
+ ],
1073
+ environment: DEFAULT_WORKFLOW_SKILL_ENVIRONMENT,
1074
+ policy: {
1075
+ allowed: ['file.readMeta', 'file.readText', 'file.readBytes', 'file.refresh'],
1076
+ requires: ['workflow-context-before-action', 'connected-resource-ref', 'typed-backend-node-actions'],
1077
+ denied: [
1078
+ 'component-state-file-mutation',
1079
+ 'direct-component-node-state-file-edit',
1080
+ 'direct-json-state-mutation',
1081
+ 'absolute-path-leakage',
1082
+ 'secret-payloads',
1083
+ ],
1084
+ },
1085
+ };
1086
+
1087
+ const DEFAULT_WORKFLOW_SKILL_GROUP_NODE_SKILL = {
1088
+ schemaVersion: 1,
1089
+ skillId: 'workflow-skill-group-node',
1090
+ name: 'Workflow Skill Group Node',
1091
+ source: 'Harness/a2a/skills/workflow-skill-group-node.json',
1092
+ description: 'On-demand Agent guide for using Skill group capability nodes as metadata capability providers.',
1093
+ nodeType: 'skill-group',
1094
+ triggers: [
1095
+ 'skill group node',
1096
+ 'skill bundle',
1097
+ 'capability pack',
1098
+ 'connected skill group',
1099
+ 'attached skills',
1100
+ ],
1101
+ controlSurface: {
1102
+ endpoint: 'GET /api/workflow/context/:node',
1103
+ readContextFirst: 'node Harness/scripts/wf-ui-control.mjs workflow-context --project .',
1104
+ storage: 'Harness/a2a/capability-nodes/**/state.json is backend-owned diagnostic storage, never an Agent control surface.',
1105
+ executionModel: 'Skill group nodes advertise skills to connected Agents; they are not independent executors.',
1106
+ },
1107
+ commands: [
1108
+ {
1109
+ name: 'skill-group-context',
1110
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-context --project .',
1111
+ description: 'Read connectedCapabilityRefs and effectiveSkills contributed by connected Skill group nodes.',
1112
+ }
1113
+ ],
1114
+ environment: DEFAULT_WORKFLOW_SKILL_ENVIRONMENT,
1115
+ policy: {
1116
+ allowed: ['state:read', 'capability:read', 'skill.metadata.read'],
1117
+ requires: ['workflow-context-before-action', 'connected-capability-ref'],
1118
+ denied: [
1119
+ 'capability-state-file-mutation',
1120
+ 'direct-capability-node-state-file-edit',
1121
+ 'direct-json-state-mutation',
1122
+ 'independent-skill-execution',
1123
+ 'secret-payloads',
1124
+ ],
1125
+ },
1126
+ };
1127
+
1128
+ const DEFAULT_WORKFLOW_MCP_CONNECTOR_NODE_SKILL = {
1129
+ schemaVersion: 1,
1130
+ skillId: 'workflow-mcp-connector-node',
1131
+ name: 'Workflow MCP Connector Node',
1132
+ source: 'Harness/a2a/skills/workflow-mcp-connector-node.json',
1133
+ description: 'On-demand Agent guide for MCP connector nodes as metadata-only capability providers.',
1134
+ nodeType: 'mcp-connector',
1135
+ createKind: 'mcp',
1136
+ triggers: [
1137
+ 'mcp connector node',
1138
+ 'mcp node',
1139
+ 'mcp tools',
1140
+ 'mcp resources',
1141
+ 'connected mcp',
1142
+ 'capability provider',
1143
+ ],
1144
+ controlSurface: {
1145
+ endpoint: 'GET /api/workflow/context/:node',
1146
+ readContextFirst: 'node Harness/scripts/wf-ui-control.mjs workflow-context --project .',
1147
+ storage: 'Harness/a2a/capability-nodes/**/state.json is backend-owned diagnostic storage, never an Agent control surface.',
1148
+ executionModel: 'MCP connector nodes expose sanitized metadata only; direct tool invocation needs a separate permission/audit contract.',
1149
+ },
1150
+ commands: [
1151
+ {
1152
+ name: 'mcp-connector-context',
1153
+ command: 'node Harness/scripts/wf-ui-control.mjs workflow-context --project .',
1154
+ description: 'Read connectedCapabilityRefs for sanitized MCP connector metadata.',
1155
+ }
1156
+ ],
1157
+ environment: DEFAULT_WORKFLOW_SKILL_ENVIRONMENT,
1158
+ policy: {
1159
+ allowed: ['state:read', 'capability:read', 'mcp.metadata.read'],
1160
+ requires: ['workflow-context-before-action', 'connected-capability-ref'],
1161
+ denied: [
1162
+ 'capability-state-file-mutation',
1163
+ 'direct-capability-node-state-file-edit',
1164
+ 'direct-json-state-mutation',
1165
+ 'mcp-tool-invocation-without-permission',
1166
+ 'mcp-server-spawn',
1167
+ 'secret-payloads',
1168
+ ],
1169
+ },
1170
+ };
1171
+
1172
+ const DEFAULT_WF_UI_MAP_SKILL = {
1173
+ schemaVersion: 1,
1174
+ skillId: 'wf-ui-map',
1175
+ name: 'WF UI Map Legacy Alias',
1176
+ source: 'Harness/a2a/skills/wf-ui-map.json',
1177
+ compatibilityAliasFor: 'workflow-node-map',
1178
+ description: 'Compatibility alias for workflow-node-map. Prefer workflow-node-map in new Agent prompts; node-map remains supported for older nodes.',
1179
+ commands: [
1180
+ {
1181
+ name: 'node-map-read',
1182
+ command: 'node Harness/scripts/wf-ui-control.mjs node-map --action readGraph',
1183
+ description: 'Legacy alias for workflow-node-map --action readGraph.',
1184
+ },
1185
+ {
1186
+ name: 'node-map-connect',
1187
+ command: 'node Harness/scripts/wf-ui-control.mjs node-map --action connectNodes --from <nodeOrSession> --to <nodeOrSession>',
1188
+ description: 'Legacy alias for workflow-node-map --action connectNodes through POST /api/workflow/nodes/:actor/actions/agent.connectNodes.',
1189
+ },
1190
+ {
1191
+ name: 'node-map-delete',
1192
+ command: 'node Harness/scripts/wf-ui-control.mjs node-map --action deleteNode --node <nodeIdOrSessionId>',
1193
+ description: 'Legacy alias for workflow-node-map --action deleteNode through POST /api/workflow/nodes/:actor/actions/agent.deleteNode.',
1194
+ },
1195
+ {
1196
+ name: 'node-map-delete-all',
1197
+ command: 'node Harness/scripts/wf-ui-control.mjs node-map --action deleteNodes --all true',
1198
+ description: 'Legacy alias for workflow-node-map --action deleteNodes through agent.deleteNodes.',
1199
+ },
1200
+ ],
1201
+ environment: DEFAULT_WORKFLOW_SKILL_ENVIRONMENT,
1202
+ policy: DEFAULT_WORKFLOW_NODE_MAP_SKILL.policy,
1203
+ };
1204
+
1205
+ const BUILT_IN_WORKFLOWS = [
1206
+ {
1207
+ id: 'wf-standard',
1208
+ command: '/wf',
1209
+ label: 'WF Standard',
1210
+ description: 'CEO-led workflow with A2A terminal orchestration and acceptance gates.',
1211
+ defaultCeoPrompt: 'Act as the Harness /wf CEO. Create a concise plan, choose terminal agents only when useful, coordinate A2A READ/WRITE/GLOB evidence, and keep task binding optional unless explicitly needed.',
1212
+ },
1213
+ {
1214
+ id: 'wf-max',
1215
+ command: '/wf-max',
1216
+ label: 'WF Max',
1217
+ description: 'Deeper CEO workflow for broader architecture and verification passes.',
1218
+ defaultCeoPrompt: 'Act as the Harness /wf-max CEO. Expand exploration only where it improves decisions, then coordinate terminal agents through Harness A2A evidence.',
1219
+ },
1220
+ {
1221
+ id: 'wf-browser',
1222
+ command: '/wf-browser',
1223
+ label: 'WF Browser',
1224
+ description: 'Browser verification workflow with terminal agents and E2E evidence.',
1225
+ defaultCeoPrompt: 'Act as the Harness /wf-browser CEO. Coordinate implementation terminals and browser verification, then report evidence.',
1226
+ },
1227
+ {
1228
+ id: 'wf-review',
1229
+ command: '/wf-review',
1230
+ label: 'WF Review',
1231
+ description: 'Independent review workflow for risks, regressions, and missing tests.',
1232
+ defaultCeoPrompt: 'Act as the Harness /wf-review controller. Inspect the current changes, dispatch the smallest useful bounded set of clean native reviewer subagents, deduplicate evidence, and return prioritized findings. Never invoke another CLI or reviewer recursion.',
1233
+ },
1234
+ ];
1235
+
1236
+ function a2aRoot(projectRoot) {
1237
+ return path.join(projectRoot, 'Harness', 'a2a');
1238
+ }
1239
+
1240
+ function writeJsonIfMissing(filePath, data) {
1241
+ if (fs.existsSync(filePath)) return;
1242
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1243
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
1244
+ }
1245
+
1246
+ function readJson(filePath, fallback) {
1247
+ try {
1248
+ if (!fs.existsSync(filePath)) return fallback;
1249
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
1250
+ } catch {
1251
+ return fallback;
1252
+ }
1253
+ }
1254
+
1255
+ function writeJson(filePath, data) {
1256
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1257
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
1258
+ }
1259
+
1260
+ function graphMapPath(projectRoot) {
1261
+ return path.join(a2aRoot(projectRoot), 'workflow-map.json');
1262
+ }
1263
+
1264
+ function isPlainObject(value) {
1265
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
1266
+ }
1267
+
1268
+ function graphMapError(message, { statusCode = 400, code = 'BAD_REQUEST', details } = {}) {
1269
+ const err = new Error(message);
1270
+ err.statusCode = statusCode;
1271
+ err.code = code;
1272
+ if (details !== undefined) err.details = details;
1273
+ return err;
1274
+ }
1275
+
1276
+ function normalizeExpectedGraphVersion(value) {
1277
+ if (value === undefined || value === null || value === '') return null;
1278
+ const text = String(value).trim();
1279
+ const match = text.match(/^(?:W\/)?"?(\d+)"?$/i);
1280
+ const version = match ? Number(match[1]) : Number(text);
1281
+ if (!Number.isInteger(version) || version < 0) {
1282
+ throw graphMapError('expectedVersion must be a non-negative integer');
1283
+ }
1284
+ return version;
1285
+ }
1286
+
1287
+ function normalizeGraphPositions(graph) {
1288
+ if (
1289
+ graph?.positions
1290
+ && typeof graph.positions === 'object'
1291
+ && !Array.isArray(graph.positions)
1292
+ && Object.keys(graph.positions).length > 0
1293
+ ) {
1294
+ return graph.positions;
1295
+ }
1296
+ const undoStack = Array.isArray(graph?.undoStack) ? graph.undoStack : [];
1297
+ for (let index = undoStack.length - 1; index >= 0; index -= 1) {
1298
+ const positions = undoStack[index]?.positions;
1299
+ if (positions && typeof positions === 'object' && !Array.isArray(positions) && Object.keys(positions).length > 0) {
1300
+ return positions;
1301
+ }
1302
+ }
1303
+ const redoStack = Array.isArray(graph?.redoStack) ? graph.redoStack : [];
1304
+ for (let index = 0; index < redoStack.length; index += 1) {
1305
+ const positions = redoStack[index]?.positions;
1306
+ if (positions && typeof positions === 'object' && !Array.isArray(positions) && Object.keys(positions).length > 0) {
1307
+ return positions;
1308
+ }
1309
+ }
1310
+ const positions = {};
1311
+ for (const node of Array.isArray(graph?.nodes) ? graph.nodes : []) {
1312
+ const nodeId = node.nodeId || node.id;
1313
+ const position = node.position || (Number.isFinite(Number(node.x)) && Number.isFinite(Number(node.y))
1314
+ ? { x: Number(node.x), y: Number(node.y) }
1315
+ : null);
1316
+ if (nodeId && position) {
1317
+ positions[nodeId] = {
1318
+ x: Number(position.x) || 0,
1319
+ y: Number(position.y) || 0,
1320
+ };
1321
+ }
1322
+ }
1323
+ return positions;
1324
+ }
1325
+
1326
+ function normalizeDeletedNodes(graph) {
1327
+ const rows = Array.isArray(graph?.deletedNodes) ? graph.deletedNodes : [];
1328
+ return rows
1329
+ .map((node) => ({
1330
+ ...(isPlainObject(node?.node) ? { node: node.node } : {}),
1331
+ ...(Array.isArray(node?.edges) ? { edges: node.edges } : {}),
1332
+ ...(node?.position && isPlainObject(node.position) ? { position: node.position } : {}),
1333
+ nodeId: String(node?.nodeId || node?.id || '').trim(),
1334
+ sessionId: node?.sessionId ? String(node.sessionId).trim() : null,
1335
+ deletedAt: node?.deletedAt || new Date().toISOString(),
1336
+ }))
1337
+ .filter(node => node.nodeId || node.sessionId);
1338
+ }
1339
+
1340
+ function mergeDeletedNodes(...groups) {
1341
+ const byKey = new Map();
1342
+ for (const group of groups) {
1343
+ for (const node of normalizeDeletedNodes({ deletedNodes: group })) {
1344
+ const key = node.nodeId || `session:${node.sessionId}`;
1345
+ byKey.set(key, { ...byKey.get(key), ...node });
1346
+ }
1347
+ }
1348
+ return [...byKey.values()];
1349
+ }
1350
+
1351
+ function deletedNodeSets(deletedNodes) {
1352
+ return {
1353
+ nodeIds: new Set(deletedNodes.map(node => node.nodeId).filter(Boolean)),
1354
+ sessionIds: new Set(deletedNodes.map(node => node.sessionId).filter(Boolean)),
1355
+ };
1356
+ }
1357
+
1358
+ function deletedGraphNodeIdSet(graph) {
1359
+ return new Set((Array.isArray(graph?.deletedNodes) ? graph.deletedNodes : [])
1360
+ .map(node => node?.nodeId)
1361
+ .filter(Boolean));
1362
+ }
1363
+
1364
+ function visibleGoalNodes(projectRoot, graph) {
1365
+ const deletedGraphNodeIds = deletedGraphNodeIdSet(graph);
1366
+ return listGoalNodes(projectRoot).filter(node => !deletedGraphNodeIds.has(node.nodeId));
1367
+ }
1368
+
1369
+ function visibleGoalRefRecord(projectRoot, graph) {
1370
+ const deletedGraphNodeIds = deletedGraphNodeIdSet(graph);
1371
+ return Object.fromEntries(
1372
+ Object.entries(goalStateRefs(projectRoot))
1373
+ .filter(([nodeId, ref]) => !deletedGraphNodeIds.has(nodeId) && !deletedGraphNodeIds.has(ref?.nodeId)),
1374
+ );
1375
+ }
1376
+
1377
+ function visibleGoalStateRecord(projectRoot, graph) {
1378
+ const deletedGraphNodeIds = deletedGraphNodeIdSet(graph);
1379
+ return Object.fromEntries(
1380
+ Object.entries(goalNodeStates(projectRoot))
1381
+ .filter(([nodeId, state]) => !deletedGraphNodeIds.has(nodeId) && !deletedGraphNodeIds.has(state?.nodeId)),
1382
+ );
1383
+ }
1384
+
1385
+ function graphNodeMatches(node, nodeIds, sessionIds) {
1386
+ const nodeId = node?.nodeId || node?.id || '';
1387
+ const sessionId = node?.sessionId || '';
1388
+ return (
1389
+ (nodeId && nodeIds.has(nodeId))
1390
+ || (sessionId && sessionIds.has(sessionId))
1391
+ || (sessionId && nodeIds.has(`session-${sessionId}`))
1392
+ );
1393
+ }
1394
+
1395
+ function graphNodeDedupeKey(node) {
1396
+ return node?.nodeId || node?.id || (node?.sessionId ? `session-${node.sessionId}` : '');
1397
+ }
1398
+
1399
+ function normalizeGraphNode(node) {
1400
+ if (!node || typeof node !== 'object') return node;
1401
+ return { ...node, status: normalizeSessionStatus(node.status) };
1402
+ }
1403
+
1404
+ function dedupeGraphNodes(nodes) {
1405
+ const seen = new Set();
1406
+ const result = [];
1407
+ for (const node of Array.isArray(nodes) ? nodes : []) {
1408
+ const normalized = normalizeGraphNode(node);
1409
+ const key = graphNodeDedupeKey(normalized);
1410
+ if (key && seen.has(key)) continue;
1411
+ if (key) seen.add(key);
1412
+ result.push(normalized);
1413
+ }
1414
+ return result;
1415
+ }
1416
+
1417
+ function graphNodeId(node) {
1418
+ return node?.nodeId || node?.id || (node?.sessionId ? `session-${node.sessionId}` : '');
1419
+ }
1420
+
1421
+ function mergeStatefulGraphNodes(projectRoot, nodes = [], positions = {}) {
1422
+ const componentNodes = listLiveComponentNodes(projectRoot);
1423
+ const componentIds = new Set(componentNodes.map(node => node.nodeId));
1424
+ const eventNodes = listEventNodes(projectRoot);
1425
+ const eventIds = new Set(eventNodes.map(node => node.nodeId));
1426
+ const capabilityNodes = listCapabilityNodes(projectRoot);
1427
+ const capabilityIds = new Set(capabilityNodes.map(node => node.nodeId));
1428
+ const goalNodes = listGoalNodes(projectRoot);
1429
+ const goalIds = new Set(goalNodes.map(node => node.nodeId));
1430
+ const incomingById = new Map((Array.isArray(nodes) ? nodes : [])
1431
+ .map(node => [graphNodeId(node), node])
1432
+ .filter(([id]) => id));
1433
+ const nonComponentNodes = (Array.isArray(nodes) ? nodes : [])
1434
+ .filter(node => (
1435
+ node?.kind !== 'component-node'
1436
+ && node?.kind !== 'event-node'
1437
+ && node?.kind !== 'capability-node'
1438
+ && node?.kind !== 'goal-node'
1439
+ && !componentIds.has(graphNodeId(node))
1440
+ && !eventIds.has(graphNodeId(node))
1441
+ && !capabilityIds.has(graphNodeId(node))
1442
+ && !goalIds.has(graphNodeId(node))
1443
+ ));
1444
+ const mergedComponentNodes = componentNodes.map(node => ({
1445
+ ...node,
1446
+ position: positions?.[node.nodeId]
1447
+ || incomingById.get(node.nodeId)?.position
1448
+ || node.position,
1449
+ }));
1450
+ const mergedEventNodes = eventNodes.map(node => ({
1451
+ ...node,
1452
+ position: positions?.[node.nodeId]
1453
+ || incomingById.get(node.nodeId)?.position
1454
+ || node.position,
1455
+ }));
1456
+ const mergedCapabilityNodes = capabilityNodes.map(node => ({
1457
+ ...node,
1458
+ position: positions?.[node.nodeId]
1459
+ || incomingById.get(node.nodeId)?.position
1460
+ || node.position,
1461
+ }));
1462
+ const mergedGoalNodes = goalNodes.map(node => ({
1463
+ ...node,
1464
+ position: positions?.[node.nodeId]
1465
+ || incomingById.get(node.nodeId)?.position
1466
+ || node.position,
1467
+ }));
1468
+ return dedupeGraphNodes([...nonComponentNodes, ...mergedComponentNodes, ...mergedEventNodes, ...mergedCapabilityNodes, ...mergedGoalNodes]);
1469
+ }
1470
+
1471
+ function graphEdgeMatches(edge, nodeIds, sessionIds) {
1472
+ return (
1473
+ (edge?.from && nodeIds.has(edge.from))
1474
+ || (edge?.to && nodeIds.has(edge.to))
1475
+ || (edge?.source && nodeIds.has(edge.source))
1476
+ || (edge?.target && nodeIds.has(edge.target))
1477
+ || (edge?.fromSessionId && sessionIds.has(edge.fromSessionId))
1478
+ || (edge?.toSessionId && sessionIds.has(edge.toSessionId))
1479
+ );
1480
+ }
1481
+
1482
+ export function workflowEdgeEndpoints(edge) {
1483
+ const from = String(edge?.from || edge?.source || '').trim();
1484
+ const to = String(edge?.to || edge?.target || '').trim();
1485
+ if (!from || !to) return null;
1486
+ return { from, to };
1487
+ }
1488
+
1489
+ export function workflowEdgePairKey(edge) {
1490
+ const endpoints = workflowEdgeEndpoints(edge);
1491
+ if (!endpoints) return '';
1492
+ if (normalizeWorkflowEdgeDirection(edge?.direction) === 'source-to-target') {
1493
+ const relation = normalizeSemanticRelation(edge?.relation) || 'wf-bridge';
1494
+ const sourceHandle = String(edge?.sourceHandle || '').trim();
1495
+ const targetHandle = String(edge?.targetHandle || '').trim();
1496
+ return `${endpoints.from}->${endpoints.to}:${relation}:${sourceHandle}->${targetHandle}`;
1497
+ }
1498
+ return [endpoints.from, endpoints.to].sort().join('<->');
1499
+ }
1500
+
1501
+ function workflowNodeTypeIndex(projectRoot, nodes = []) {
1502
+ const index = new Map();
1503
+ for (const node of listLiveComponentNodes(projectRoot)) {
1504
+ if (node?.nodeId && node?.type) index.set(node.nodeId, node.type);
1505
+ }
1506
+ for (const node of listEventNodes(projectRoot)) {
1507
+ if (node?.nodeId && node?.type) index.set(node.nodeId, node.type);
1508
+ }
1509
+ for (const node of listCapabilityNodes(projectRoot)) {
1510
+ if (node?.nodeId && node?.type) index.set(node.nodeId, node.type);
1511
+ }
1512
+ for (const node of listGoalNodes(projectRoot)) {
1513
+ if (node?.nodeId) index.set(node.nodeId, 'goal');
1514
+ }
1515
+ for (const node of Array.isArray(nodes) ? nodes : []) {
1516
+ const nodeId = String(node?.nodeId || node?.id || '').trim();
1517
+ const type = String(node?.componentType || node?.type || '').trim();
1518
+ if (nodeId && ['markdown', 'excalidraw', 'file', 'timer', 'github-trigger', 'skill-group', 'mcp-connector', 'goal'].includes(type)) index.set(nodeId, type);
1519
+ }
1520
+ return index;
1521
+ }
1522
+
1523
+ function filterEdgesForGraphNodes(edges = [], nodes = []) {
1524
+ const nodeIds = new Set(nodes.map(node => graphNodeId(node)).filter(Boolean));
1525
+ return (Array.isArray(edges) ? edges : []).filter((edge) => {
1526
+ const endpoints = workflowEdgeEndpoints(edge);
1527
+ if (!endpoints) return false;
1528
+ return nodeIds.has(endpoints.from) && nodeIds.has(endpoints.to);
1529
+ });
1530
+ }
1531
+
1532
+ function resourcePrimaryPort(type) {
1533
+ if (type === 'markdown') return 'markdown';
1534
+ if (type === 'excalidraw') return 'scene';
1535
+ if (type === 'file') return 'file';
1536
+ return '';
1537
+ }
1538
+
1539
+ function resourceHandleAliases(type) {
1540
+ if (type === 'markdown') return new Set(['markdown', 'plaintext', 'content', 'selection', 'input', 'output', 'top', 'right', 'bottom', 'left']);
1541
+ if (type === 'excalidraw') return new Set(['scene', 'image', 'diagram', 'excalidraw', 'content', 'selection', 'input', 'output', 'top', 'right', 'bottom', 'left']);
1542
+ if (type === 'file') return new Set(['file', 'path', 'document', 'content', 'selection', 'input', 'output', 'top', 'right', 'bottom', 'left']);
1543
+ if (type === 'skill-group') return new Set(['capability', 'skill-group', 'skills', 'skill', 'left', 'right', 'input', 'output']);
1544
+ if (type === 'mcp-connector') return new Set(['capability', 'mcp', 'mcp-connector', 'connector', 'server', 'tool', 'tools', 'resource', 'resources', 'left', 'right', 'input', 'output']);
1545
+ if (type === 'goal') return new Set(['goal', 'task', 'acceptance', 'left', 'right', 'context', 'input', 'output']);
1546
+ return new Set();
1547
+ }
1548
+
1549
+ function normalizeComponentEdgeHandle(type, endpointRole, value) {
1550
+ const handle = String(value || '').trim();
1551
+ if (type === 'goal') {
1552
+ if (!handle) return `goal:${endpointRole === 'source' ? 'right' : 'left'}`;
1553
+ const physicalMatch = handle.match(/^([^:]+):(left|right)$/);
1554
+ if (physicalMatch && physicalMatch[1] === 'goal') return `goal:${physicalMatch[2]}`;
1555
+ const side = handle.toLowerCase();
1556
+ if (side === 'left' || side === 'right') return `goal:${side}`;
1557
+ const semantic = handle.split(':')[0].trim().toLowerCase();
1558
+ if (resourceHandleAliases(type).has(semantic)) {
1559
+ return `goal:${endpointRole === 'source' ? 'right' : 'left'}`;
1560
+ }
1561
+ return `goal:${endpointRole === 'source' ? 'right' : 'left'}`;
1562
+ }
1563
+ if (type === 'skill-group' || type === 'mcp-connector') {
1564
+ if (!handle) return `capability:${endpointRole === 'source' ? 'right' : 'left'}`;
1565
+ const physicalMatch = handle.match(/^([^:]+):(left|right)$/);
1566
+ if (physicalMatch && physicalMatch[1] === 'capability') return `capability:${physicalMatch[2]}`;
1567
+ const side = handle.toLowerCase();
1568
+ if (side === 'left' || side === 'right') return `capability:${side}`;
1569
+ const semantic = handle.split(':')[0].trim().toLowerCase();
1570
+ if (resourceHandleAliases(type).has(semantic)) {
1571
+ return `capability:${endpointRole === 'source' ? 'right' : 'left'}`;
1572
+ }
1573
+ return `capability:${endpointRole === 'source' ? 'right' : 'left'}`;
1574
+ }
1575
+ if (!['markdown', 'excalidraw', 'file'].includes(type)) return handle || null;
1576
+ const primary = resourcePrimaryPort(type);
1577
+ if (!handle) return `${primary}:${endpointRole === 'source' ? 'right' : 'left'}`;
1578
+ const physicalMatch = handle.match(/^([^:]+):(top|right|bottom|left)$/);
1579
+ if (physicalMatch) {
1580
+ const port = physicalMatch[1];
1581
+ const side = physicalMatch[2];
1582
+ if (port === primary) return `${primary}:${side}`;
1583
+ }
1584
+ const side = handle.toLowerCase();
1585
+ if (side === 'top' || side === 'right' || side === 'bottom' || side === 'left') return `${primary}:${side}`;
1586
+ const semantic = handle.split(':')[0].trim().toLowerCase();
1587
+ if (resourceHandleAliases(type).has(semantic)) {
1588
+ return `${primary}:${endpointRole === 'source' ? 'right' : 'left'}`;
1589
+ }
1590
+ return `${primary}:${endpointRole === 'source' ? 'right' : 'left'}`;
1591
+ }
1592
+
1593
+ function normalizeAgentEdgeHandle(endpointRole, value) {
1594
+ const handle = String(value || '').trim();
1595
+ if (!handle) return null;
1596
+ if (endpointRole === 'target' && handle === 'left') return 'context';
1597
+ if (endpointRole === 'source' && handle === 'output') return 'bottom';
1598
+ return handle;
1599
+ }
1600
+
1601
+ function normalizeWorkflowEdgeHandle(projectRoot, nodeTypes, nodeId, endpointRole, value) {
1602
+ const type = nodeTypes.get(nodeId);
1603
+ if (type === 'timer' || type === 'github-trigger') {
1604
+ const handle = String(value || '').trim();
1605
+ if (handle) return handle;
1606
+ return endpointRole === 'source' ? 'event' : 'config';
1607
+ }
1608
+ if (type) return normalizeComponentEdgeHandle(type, endpointRole, value);
1609
+ return normalizeAgentEdgeHandle(endpointRole, value);
1610
+ }
1611
+
1612
+ function normalizeWorkflowEdgeDirection(value) {
1613
+ return String(value || '').trim() === 'source-to-target' ? 'source-to-target' : 'bidirectional';
1614
+ }
1615
+
1616
+ // Graph-map is a legacy whole-document write surface, but it must not be a
1617
+ // second way around the typed workflow ontology. Keep this check here (next
1618
+ // to the canonical edge normalizer) so every graph writer, including the
1619
+ // legacy PUT endpoint, applies the same Timer/Agent contract.
1620
+ export function assertWorkflowGraphEdgeOntology(projectRoot, graph = {}, edges = []) {
1621
+ const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
1622
+ const nodeIds = new Set(nodes.map(node => graphNodeId(node)).filter(Boolean));
1623
+ const nodeTypes = workflowNodeTypeIndex(projectRoot, nodes);
1624
+ const agentIds = new Set(nodes
1625
+ .filter(node => node?.sessionId)
1626
+ .map(node => graphNodeId(node))
1627
+ .filter(Boolean));
1628
+ const deletedIds = new Set((Array.isArray(graph?.deletedNodes) ? graph.deletedNodes : [])
1629
+ .map(node => String(node?.nodeId || '').trim())
1630
+ .filter(Boolean));
1631
+
1632
+ const fail = (message, code = 'INVALID_EDGE_ONTOLOGY', details = {}) => {
1633
+ throw graphMapError(message, { statusCode: 400, code, details });
1634
+ };
1635
+
1636
+ const normalized = [];
1637
+ for (const rawEdge of Array.isArray(edges) ? edges : []) {
1638
+ const endpoints = workflowEdgeEndpoints(rawEdge);
1639
+ if (!endpoints) fail('Workflow edge requires from and to endpoints', 'INVALID_ENDPOINT');
1640
+ if (!nodeIds.has(endpoints.from) && !deletedIds.has(endpoints.from)) {
1641
+ fail(`Workflow edge source node not found: ${endpoints.from}`, 'ENDPOINT_NOT_FOUND', { nodeId: endpoints.from });
1642
+ }
1643
+ if (!nodeIds.has(endpoints.to) && !deletedIds.has(endpoints.to)) {
1644
+ fail(`Workflow edge target node not found: ${endpoints.to}`, 'ENDPOINT_NOT_FOUND', { nodeId: endpoints.to });
1645
+ }
1646
+
1647
+ const edge = normalizeWorkflowGraphEdge(projectRoot, rawEdge, nodes);
1648
+ const relation = String(edge.relation || '').trim();
1649
+ if (relation === 'event' || relation === 'control') {
1650
+ const fromType = nodeTypes.get(endpoints.from) || '';
1651
+ const toType = nodeTypes.get(endpoints.to) || '';
1652
+ const direction = normalizeWorkflowEdgeDirection(edge.direction);
1653
+ const sourceHandle = String(edge.sourceHandle || '').trim().toLowerCase();
1654
+ const targetHandle = String(edge.targetHandle || '').trim().toLowerCase();
1655
+ if (direction !== 'source-to-target') {
1656
+ fail(`${relation} edge must use source-to-target direction`, 'INVALID_EDGE_ONTOLOGY', {
1657
+ relation, direction,
1658
+ });
1659
+ }
1660
+ if (relation === 'event') {
1661
+ // GitHub triggers are distinct event sources, but valid event.in
1662
+ // links remain useful for context. They must not be accepted through
1663
+ // Timer's generic `event` handle or timer wakeup path.
1664
+ const configuredGithubTrigger = fromType === 'github-trigger' && (() => {
1665
+ try {
1666
+ const current = getEventNode(projectRoot, endpoints.from);
1667
+ return Boolean(current?.state?.repository?.fullName);
1668
+ } catch {
1669
+ return false;
1670
+ }
1671
+ })();
1672
+ if ((fromType !== 'timer' && !configuredGithubTrigger) || !agentIds.has(endpoints.to)) {
1673
+ fail('event edges must connect a configured Timer/GitHub Trigger -> Agent', 'INVALID_EDGE_ONTOLOGY', {
1674
+ relation, from: endpoints.from, to: endpoints.to, fromType, toType,
1675
+ });
1676
+ }
1677
+ if (sourceHandle && !['event', 'event:right', 'event:top', 'event:bottom'].includes(sourceHandle)) {
1678
+ fail('Timer event edges require the event output handle', 'INVALID_EDGE_HANDLE', { relation, sourceHandle });
1679
+ }
1680
+ const allowedTargetHandles = fromType === 'github-trigger'
1681
+ ? ['event.in']
1682
+ : ['event', 'event.in', 'context', 'input'];
1683
+ if (targetHandle && !allowedTargetHandles.includes(targetHandle)) {
1684
+ fail('Timer event edges require an Agent event input handle', 'INVALID_EDGE_HANDLE', { relation, targetHandle });
1685
+ }
1686
+ } else {
1687
+ if (!agentIds.has(endpoints.from) || toType !== 'timer') {
1688
+ fail('control edges must connect Agent -> Timer', 'INVALID_EDGE_ONTOLOGY', {
1689
+ relation, from: endpoints.from, to: endpoints.to, fromType, toType,
1690
+ });
1691
+ }
1692
+ if (sourceHandle && !['context', 'control', 'output', 'input'].includes(sourceHandle)) {
1693
+ fail('Timer control edges require the Agent context handle', 'INVALID_EDGE_HANDLE', { relation, sourceHandle });
1694
+ }
1695
+ if (targetHandle && !['config', 'config:left'].includes(targetHandle)) {
1696
+ fail('Timer control edges require the config input handle', 'INVALID_EDGE_HANDLE', { relation, targetHandle });
1697
+ }
1698
+ }
1699
+ }
1700
+ normalized.push(edge);
1701
+ }
1702
+ assertUniqueWorkflowEdgePairs(normalized);
1703
+ return normalized;
1704
+ }
1705
+
1706
+ function relationFromEdge(edge) {
1707
+ const relation = normalizeSemanticRelation(edge?.relation);
1708
+ if (relation) return relation;
1709
+ const label = normalizeSemanticRelation(edge?.label);
1710
+ if (label) return label;
1711
+ return 'wf-bridge';
1712
+ }
1713
+
1714
+ function normalizeSemanticRelation(value) {
1715
+ const relation = String(value || '').trim();
1716
+ if (!relation) return '';
1717
+ if (/(?:<->|->)/.test(relation)) return '';
1718
+ return relation.replace(/\b(markdown|scene|file):(top|right|bottom|left)\b/g, '$1').trim();
1719
+ }
1720
+
1721
+ export function assertUniqueWorkflowEdgePairs(edges = []) {
1722
+ const seen = new Map();
1723
+ for (const edge of Array.isArray(edges) ? edges : []) {
1724
+ const key = workflowEdgePairKey(edge);
1725
+ if (!key) continue;
1726
+ const endpoints = workflowEdgeEndpoints(edge);
1727
+ if (seen.has(key)) {
1728
+ const previous = seen.get(key);
1729
+ throw graphMapError(`Duplicate workflow edge endpoints: ${endpoints.from} <-> ${endpoints.to}`, {
1730
+ statusCode: 409,
1731
+ code: 'DUPLICATE_EDGE',
1732
+ details: {
1733
+ edgeId: edge?.id || `${endpoints.from}->${endpoints.to}`,
1734
+ previousEdgeId: previous?.id || `${previous?.from}->${previous?.to}`,
1735
+ from: endpoints.from,
1736
+ to: endpoints.to,
1737
+ },
1738
+ });
1739
+ }
1740
+ seen.set(key, { ...endpoints, id: edge?.id || `${endpoints.from}->${endpoints.to}` });
1741
+ }
1742
+ }
1743
+
1744
+ export function normalizeWorkflowGraphEdge(projectRoot, edge, nodes = []) {
1745
+ if (!edge || typeof edge !== 'object') return edge;
1746
+ const endpoints = workflowEdgeEndpoints(edge);
1747
+ if (!endpoints) return edge;
1748
+ const nodeTypes = workflowNodeTypeIndex(projectRoot, nodes);
1749
+ const id = String(edge.id || `${endpoints.from}->${endpoints.to}`).trim() || `${endpoints.from}->${endpoints.to}`;
1750
+ const direction = normalizeWorkflowEdgeDirection(edge.direction);
1751
+ const relation = relationFromEdge(edge);
1752
+ return {
1753
+ ...edge,
1754
+ id,
1755
+ kind: edge.kind || (direction === 'source-to-target' && relation === 'event' ? 'event-link' : 'workflow-link'),
1756
+ from: endpoints.from,
1757
+ to: endpoints.to,
1758
+ source: endpoints.from,
1759
+ target: endpoints.to,
1760
+ relation,
1761
+ sourceHandle: normalizeWorkflowEdgeHandle(projectRoot, nodeTypes, endpoints.from, 'source', edge.sourceHandle),
1762
+ targetHandle: normalizeWorkflowEdgeHandle(projectRoot, nodeTypes, endpoints.to, 'target', edge.targetHandle),
1763
+ direction,
1764
+ };
1765
+ }
1766
+
1767
+ function normalizeWorkflowGraphEdges(projectRoot, edges = [], nodes = [], { rejectDuplicates = true } = {}) {
1768
+ const normalized = [];
1769
+ const seen = new Set();
1770
+ for (const edge of Array.isArray(edges) ? edges : []) {
1771
+ const next = normalizeWorkflowGraphEdge(projectRoot, edge, nodes);
1772
+ const key = workflowEdgePairKey(next);
1773
+ if (key && seen.has(key)) {
1774
+ if (rejectDuplicates) assertUniqueWorkflowEdgePairs([...normalized, next]);
1775
+ continue;
1776
+ }
1777
+ if (key) seen.add(key);
1778
+ normalized.push(next);
1779
+ }
1780
+ if (rejectDuplicates) assertUniqueWorkflowEdgePairs(normalized);
1781
+ return normalized;
1782
+ }
1783
+
1784
+ function normalizeCapsuleDockSide(value) {
1785
+ const side = String(value || '').trim();
1786
+ return ['left', 'right', 'top', 'bottom'].includes(side) ? side : 'right';
1787
+ }
1788
+
1789
+ function capsuleDockPairId(left, right) {
1790
+ return [String(left || '').trim(), String(right || '').trim()].sort().join('::');
1791
+ }
1792
+
1793
+ function capsuleDockConnectionId(connection = {}) {
1794
+ const source = String(connection.source || connection.from || '').trim();
1795
+ const target = String(connection.target || connection.to || '').trim();
1796
+ const relation = String(connection.relation || 'wf-bridge').trim();
1797
+ const direction = normalizeWorkflowEdgeDirection(connection.direction);
1798
+ const sourceHandle = String(connection.sourceHandle || '').trim();
1799
+ const targetHandle = String(connection.targetHandle || '').trim();
1800
+ return `${source}->${target}:${relation}:${direction}:${sourceHandle}->${targetHandle}`;
1801
+ }
1802
+
1803
+ function normalizeCapsuleDockConnection(connection) {
1804
+ if (!connection || typeof connection !== 'object') return null;
1805
+ const source = String(connection.source || connection.from || '').trim();
1806
+ const target = String(connection.target || connection.to || '').trim();
1807
+ if (!source || !target || source === target) return null;
1808
+ const relation = String(connection.relation || 'wf-bridge').trim() || 'wf-bridge';
1809
+ const direction = normalizeWorkflowEdgeDirection(connection.direction);
1810
+ const sourceHandle = String(connection.sourceHandle || '').trim();
1811
+ const targetHandle = String(connection.targetHandle || '').trim();
1812
+ return {
1813
+ id: String(connection.id || capsuleDockConnectionId({ source, target, relation, direction, sourceHandle, targetHandle })).trim(),
1814
+ source,
1815
+ target,
1816
+ from: source,
1817
+ to: target,
1818
+ relation,
1819
+ direction,
1820
+ sourceHandle: sourceHandle || null,
1821
+ targetHandle: targetHandle || null,
1822
+ kind: 'capsule-dock-link',
1823
+ };
1824
+ }
1825
+
1826
+ function normalizeCapsuleDockLinks(value, nodes = [], edges = []) {
1827
+ if (!Array.isArray(value)) return [];
1828
+ const graphNodeIds = new Set((Array.isArray(nodes) ? nodes : [])
1829
+ .map(node => graphNodeId(node))
1830
+ .filter(Boolean));
1831
+ const edgeById = new Map((Array.isArray(edges) ? edges : [])
1832
+ .map(edge => [String(edge?.id || '').trim(), edge])
1833
+ .filter(([id]) => id));
1834
+ const result = new Map();
1835
+ for (const item of value) {
1836
+ if (!item || typeof item !== 'object') continue;
1837
+ const rawNodeIds = Array.isArray(item.nodeIds)
1838
+ ? item.nodeIds.map(nodeId => String(nodeId || '').trim()).filter(Boolean)
1839
+ : [];
1840
+ const anchorId = String(item.anchorId || rawNodeIds[0] || '').trim();
1841
+ const draggedId = String(item.draggedId || rawNodeIds[1] || '').trim();
1842
+ if (!anchorId || !draggedId || anchorId === draggedId) continue;
1843
+ if (graphNodeIds.size > 0 && (!graphNodeIds.has(anchorId) || !graphNodeIds.has(draggedId))) continue;
1844
+ const sortedNodeIds = [anchorId, draggedId].sort();
1845
+ const pairId = capsuleDockPairId(sortedNodeIds[0], sortedNodeIds[1]);
1846
+ const rawEdgeBindings = Array.isArray(item.edges)
1847
+ ? item.edges
1848
+ : (Array.isArray(item.edgeIds) ? item.edgeIds : []);
1849
+ const edgesForDisplay = rawEdgeBindings
1850
+ .map((binding) => {
1851
+ if (typeof binding === 'string') return { edgeId: binding, retention: 'keep' };
1852
+ if (!binding || typeof binding !== 'object') return null;
1853
+ const edgeId = String(binding.edgeId || binding.id || '').trim();
1854
+ if (!edgeId) return null;
1855
+ return {
1856
+ edgeId,
1857
+ retention: binding.retention === 'delete-on-detach' ? 'delete-on-detach' : 'keep',
1858
+ };
1859
+ })
1860
+ .filter(Boolean)
1861
+ .filter(binding => edgeById.has(binding.edgeId));
1862
+ const rawConnections = Array.isArray(item.connections) ? item.connections : [];
1863
+ const connections = rawConnections
1864
+ .map(normalizeCapsuleDockConnection)
1865
+ .filter(Boolean)
1866
+ .filter(connection => (
1867
+ (!graphNodeIds.size || (graphNodeIds.has(connection.source) && graphNodeIds.has(connection.target)))
1868
+ && sortedNodeIds.includes(connection.source)
1869
+ && sortedNodeIds.includes(connection.target)
1870
+ ));
1871
+ if (connections.length === 0) {
1872
+ for (const binding of edgesForDisplay) {
1873
+ const edge = edgeById.get(binding.edgeId);
1874
+ const connection = normalizeCapsuleDockConnection({
1875
+ id: `dock:${pairId}:${binding.edgeId}`,
1876
+ source: edge?.source || edge?.from,
1877
+ target: edge?.target || edge?.to,
1878
+ relation: edge?.relation,
1879
+ direction: edge?.direction,
1880
+ sourceHandle: edge?.sourceHandle,
1881
+ targetHandle: edge?.targetHandle,
1882
+ });
1883
+ if (connection) connections.push(connection);
1884
+ }
1885
+ }
1886
+ if (connections.length === 0) continue;
1887
+ const dedupedConnections = [...new Map(connections.map(connection => [connection.id, connection])).values()];
1888
+ result.set(pairId, {
1889
+ id: String(item.id || `dock:${pairId}`).trim() || `dock:${pairId}`,
1890
+ nodeIds: sortedNodeIds,
1891
+ anchorId,
1892
+ draggedId,
1893
+ side: normalizeCapsuleDockSide(item.side),
1894
+ edges: edgesForDisplay,
1895
+ connections: dedupedConnections,
1896
+ });
1897
+ }
1898
+ return [...result.values()];
1899
+ }
1900
+
1901
+ function magneticGroupIdFor(members) {
1902
+ const key = [...members].sort().join('::');
1903
+ let hash = 5381;
1904
+ for (let i = 0; i < key.length; i++) {
1905
+ hash = ((hash << 5) + hash + key.charCodeAt(i)) >>> 0;
1906
+ }
1907
+ return `mag-${hash.toString(36)}`;
1908
+ }
1909
+
1910
+ // Pure function: computes connected components (magnetic topology) from
1911
+ // capsuleDockLinks. No IO, no Date.now/Math.random — output is deterministic
1912
+ // given the input. Each dock link connects its two nodeIds as an undirected
1913
+ // edge. `directMagneticNeighbors` are nodes sharing a dock link with this node;
1914
+ // `magneticReachableNodes` are all OTHER nodes in the same component
1915
+ // (transitive closure, includes direct neighbors).
1916
+ export function computeMagneticTopology(capsuleDockLinks, opts = {}) {
1917
+ const links = Array.isArray(capsuleDockLinks) ? capsuleDockLinks : [];
1918
+ const extraNodeIds = Array.isArray(opts && opts.nodeIds) ? opts.nodeIds : [];
1919
+ const adjacency = new Map();
1920
+ const ensure = (id) => {
1921
+ if (!adjacency.has(id)) adjacency.set(id, new Set());
1922
+ return adjacency.get(id);
1923
+ };
1924
+ for (const id of extraNodeIds) {
1925
+ const nodeId = String(id || '').trim();
1926
+ if (nodeId) ensure(nodeId);
1927
+ }
1928
+ for (const link of links) {
1929
+ if (!link || !Array.isArray(link.nodeIds)) continue;
1930
+ const pair = link.nodeIds.map(id => String(id || '').trim()).filter(Boolean);
1931
+ if (pair.length < 2) {
1932
+ if (pair.length === 1) ensure(pair[0]);
1933
+ continue;
1934
+ }
1935
+ const [a, b] = pair;
1936
+ ensure(a).add(b);
1937
+ ensure(b).add(a);
1938
+ }
1939
+ if (adjacency.size === 0) {
1940
+ return { groups: [], byNode: {} };
1941
+ }
1942
+ const visited = new Set();
1943
+ const components = [];
1944
+ for (const start of [...adjacency.keys()].sort()) {
1945
+ if (visited.has(start)) continue;
1946
+ const component = [];
1947
+ const stack = [start];
1948
+ visited.add(start);
1949
+ while (stack.length) {
1950
+ const node = stack.pop();
1951
+ component.push(node);
1952
+ const neighbors = adjacency.get(node) || new Set();
1953
+ for (const next of [...neighbors].sort()) {
1954
+ if (visited.has(next)) continue;
1955
+ visited.add(next);
1956
+ stack.push(next);
1957
+ }
1958
+ }
1959
+ components.push(component.sort());
1960
+ }
1961
+ components.sort((x, y) => (x[0] < y[0] ? -1 : x[0] > y[0] ? 1 : 0));
1962
+ const groups = components.map(members => ({
1963
+ magneticGroupId: magneticGroupIdFor(members),
1964
+ nodeIds: members,
1965
+ }));
1966
+ const byNode = {};
1967
+ for (const { magneticGroupId, nodeIds } of groups) {
1968
+ for (const node of nodeIds) {
1969
+ const direct = [...(adjacency.get(node) || new Set())].sort();
1970
+ byNode[node] = {
1971
+ magneticGroupId,
1972
+ directMagneticNeighbors: direct,
1973
+ magneticReachableNodes: nodeIds.filter(id => id !== node),
1974
+ };
1975
+ }
1976
+ }
1977
+ return { groups, byNode };
1978
+ }
1979
+
1980
+ // ── Agent find / connect / create routing (agent-team-cooperation-spec §4) ──
1981
+
1982
+ const CANONICAL_ROLE_TITLES = new Set([
1983
+ 'ceo',
1984
+ 'manager',
1985
+ 'implementer',
1986
+ 'reviewer',
1987
+ 'verifier',
1988
+ 'planner',
1989
+ 'terminal-controller',
1990
+ ]);
1991
+
1992
+ // role-graph kind → canonical roleTitle (spec §3.1)
1993
+ const ROLE_GRAPH_KIND_TO_TITLE = {
1994
+ controller: 'ceo',
1995
+ planner: 'planner',
1996
+ architecture: 'architect',
1997
+ implementer: 'implementer',
1998
+ review: 'reviewer',
1999
+ validation: 'verifier',
2000
+ runtime: 'terminal-controller',
101
2001
  };
102
2002
 
103
- const BUILT_IN_WORKFLOWS = [
104
- {
105
- id: 'wf-standard',
106
- command: '/wf',
107
- label: 'WF Standard',
108
- description: 'CEO-led workflow with A2A terminal orchestration and acceptance gates.',
109
- defaultCeoPrompt: 'Act as the Harness /wf CEO. Create a concise plan, choose terminal agents only when useful, coordinate A2A READ/WRITE/GLOB evidence, and keep task binding optional unless explicitly needed.',
110
- },
111
- {
112
- id: 'wf-max',
113
- command: '/wf-max',
114
- label: 'WF Max',
115
- description: 'Deeper CEO workflow for broader architecture and verification passes.',
116
- defaultCeoPrompt: 'Act as the Harness /wf-max CEO. Expand exploration only where it improves decisions, then coordinate terminal agents through Harness A2A evidence.',
117
- },
118
- {
119
- id: 'wf-browser',
120
- command: '/wf-browser',
121
- label: 'WF Browser',
122
- description: 'Browser verification workflow with terminal agents and E2E evidence.',
123
- defaultCeoPrompt: 'Act as the Harness /wf-browser CEO. Coordinate implementation terminals and browser verification, then report evidence.',
124
- },
125
- {
126
- id: 'wf-review',
127
- command: '/wf-review',
128
- label: 'WF Review',
129
- description: 'Independent review workflow for risks, regressions, and missing tests.',
130
- defaultCeoPrompt: 'Act as the Harness /wf-review CEO. Inspect the current changes, ask terminal peers for independent findings when useful, and return prioritized review evidence.',
131
- },
132
- ];
2003
+ const GOAL_NODE_ID_RE = /^goal-[A-Za-z0-9][A-Za-z0-9_.-]*$/;
133
2004
 
134
- function a2aRoot(projectRoot) {
135
- return path.join(projectRoot, 'Harness', 'a2a');
2005
+ // Spec §3.2: agentKind stays a lifecycle flag; roleTitle is the role identity.
2006
+ // 'Main Agent'/'main'/ceo 'ceo'; 'Subagent'/'subagent' → legacy
2007
+ // 'terminal-agent' (the no-profile default for manually spawned PTYs).
2008
+ function normalizeRoleTitle(value, agentKind = '') {
2009
+ const raw = String(value || '').trim();
2010
+ if (!raw) return agentKind === 'main' ? 'ceo' : '';
2011
+ const lower = raw.toLowerCase();
2012
+ if (lower === 'main' || lower === 'main agent' || lower === 'ceo') return 'ceo';
2013
+ if (lower === 'subagent' || lower === 'sub') return 'terminal-agent';
2014
+ if (ROLE_GRAPH_KIND_TO_TITLE[lower]) return ROLE_GRAPH_KIND_TO_TITLE[lower];
2015
+ return raw;
136
2016
  }
137
2017
 
138
- function writeJsonIfMissing(filePath, data) {
139
- if (fs.existsSync(filePath)) return;
140
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
141
- fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
2018
+ function agentRefHasGraphEdge(graph, nodeId) {
2019
+ const edgeConnected = (Array.isArray(graph.edges) ? graph.edges : []).some((edge) => {
2020
+ const from = String(edge.from || edge.source || '').trim();
2021
+ const to = String(edge.to || edge.target || '').trim();
2022
+ return from === nodeId || to === nodeId;
2023
+ });
2024
+ if (edgeConnected) return true;
2025
+ // Dock-linked agents are magnetically connected even without a graph edge
2026
+ // (spec §4.1 `connected`); without this, auto-connect would add a duplicate
2027
+ // delegation edge for already-docked agents (F5).
2028
+ return (Array.isArray(graph.capsuleDockLinks) ? graph.capsuleDockLinks : []).some((link) => {
2029
+ const nodeIds = Array.isArray(link?.nodeIds) ? link.nodeIds : [];
2030
+ return nodeIds.includes(nodeId);
2031
+ });
142
2032
  }
143
2033
 
144
- function readJson(filePath, fallback) {
145
- try {
146
- if (!fs.existsSync(filePath)) return fallback;
147
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
148
- } catch {
149
- return fallback;
2034
+ function matchAgentQuery(ref, { role, runtime, provider, capability, title } = {}) {
2035
+ if (role) {
2036
+ const r = String(role).trim().toLowerCase();
2037
+ const roleTitle = String(ref.roleTitle || '').toLowerCase();
2038
+ const agentKind = String(ref.agentKind || '').toLowerCase();
2039
+ // Synonym vocabulary (spec §3.2): 'main'/'main agent' → agentKind main;
2040
+ // 'subagent'/'sub' → agentKind subagent.
2041
+ const synonym = (r === 'main' || r === 'main agent')
2042
+ ? agentKind === 'main'
2043
+ : (r === 'subagent' || r === 'sub') ? agentKind === 'subagent' : false;
2044
+ // role-graph kind → canonical roleTitle map (spec §3.1).
2045
+ const kindMapped = ROLE_GRAPH_KIND_TO_TITLE[r] ? roleTitle === ROLE_GRAPH_KIND_TO_TITLE[r] : false;
2046
+ // Tightened precedence (F5): exact match first, synonym map second,
2047
+ // substring only as the last resort for free-form roleTitles — substring
2048
+ // alone may over-match (e.g. role=re) and must never outrank an exact or
2049
+ // synonym hit.
2050
+ if (roleTitle !== r && !synonym && !kindMapped && !roleTitle.includes(r)) return false;
2051
+ }
2052
+ if (runtime && String(ref.runtime || '').trim().toLowerCase() !== String(runtime).trim().toLowerCase()) return false;
2053
+ if (provider && String(ref.provider || '').trim().toLowerCase() !== String(provider).trim().toLowerCase()) return false;
2054
+ if (capability) {
2055
+ const c = String(capability).trim().toLowerCase();
2056
+ if (!(Array.isArray(ref.capabilities) ? ref.capabilities : [])
2057
+ .some(item => String(item).toLowerCase().includes(c))) return false;
2058
+ }
2059
+ if (title) {
2060
+ const t = String(title).trim().toLowerCase();
2061
+ const haystack = [ref.displayName, ref.roleTitle, ref.nodeId].join(' ').toLowerCase();
2062
+ if (!haystack.includes(t)) return false;
2063
+ }
2064
+ return true;
2065
+ }
2066
+
2067
+ // Spec §4.1: find existing agents by role / runtime / provider / capability /
2068
+ // title. Role matches canonical roleTitle, free-form roleTitle, or agentKind
2069
+ // synonym; capability and title are substring matches.
2070
+ export async function findAgents(projectRoot, { role, runtime, provider, capability, title } = {}, sessionRegistry = null) {
2071
+ const graph = loadWorkflowGraphMap(projectRoot);
2072
+ const graphNodes = (Array.isArray(graph.nodes) ? graph.nodes : []).filter(node => node.sessionId);
2073
+ const bySessionId = new Map();
2074
+ for (const session of listTerminalSessions(projectRoot)) {
2075
+ bySessionId.set(session.sessionId, session);
2076
+ }
2077
+ if (sessionRegistry && typeof sessionRegistry.getAll === 'function') {
2078
+ for (const session of sessionRegistry.getAll()) {
2079
+ bySessionId.set(session.sessionId, { ...(bySessionId.get(session.sessionId) || {}), ...session });
2080
+ }
2081
+ }
2082
+ const profiles = new Map(listRoleProfiles(projectRoot).map(profile => [profile.nodeId, profile]));
2083
+ const refs = graphNodes.map((node) => {
2084
+ const nodeId = node.nodeId || node.id || '';
2085
+ const session = bySessionId.get(node.sessionId) || {};
2086
+ const profile = profiles.get(nodeId) || null;
2087
+ const roleTitle = String(
2088
+ profile?.roleTitle
2089
+ || session.roleTitle
2090
+ || normalizeRoleTitle(node.role || session.role || '', node.agentKind || session.agentKind)
2091
+ || ''
2092
+ ).trim();
2093
+ return {
2094
+ nodeId,
2095
+ displayName: String(profile?.displayName || session.displayName || node.label || node.role || nodeId || '').trim(),
2096
+ roleTitle,
2097
+ agentKind: String(session.agentKind || node.agentKind || '').trim() || 'subagent',
2098
+ runtime: String(profile?.runtime || session.runtime || node.runtime || '').trim(),
2099
+ provider: String(profile?.provider || session.provider || node.provider || '').trim(),
2100
+ capabilities: Array.isArray(profile?.capabilities)
2101
+ ? [...profile.capabilities]
2102
+ : (Array.isArray(session.capabilities) ? [...session.capabilities] : []),
2103
+ connected: agentRefHasGraphEdge(graph, nodeId),
2104
+ status: String(session.status || node.status || 'stopped').trim(),
2105
+ };
2106
+ });
2107
+ const matches = refs.filter(ref => matchAgentQuery(ref, { role, runtime, provider, capability, title }));
2108
+ const query = {};
2109
+ for (const [key, value] of Object.entries({ role, runtime, provider, capability, title })) {
2110
+ if (value) query[key] = value;
2111
+ }
2112
+ return { query, matches, count: matches.length };
2113
+ }
2114
+
2115
+ // Spec §4.2: exactly one match + not connected → connect (bidirectional
2116
+ // delegation edge) and report the decision. Idempotent: no duplicate edges.
2117
+ export function autoConnectAgent(projectRoot, from, to, options = {}) {
2118
+ const fromId = String(from || '').trim();
2119
+ const toId = String(to || '').trim();
2120
+ if (!fromId || !toId || fromId === toId) {
2121
+ throw graphMapError('Auto-connect requires two distinct workflow node ids', {
2122
+ statusCode: 400,
2123
+ code: 'INVALID_ENDPOINT',
2124
+ });
2125
+ }
2126
+ const graph = loadWorkflowGraphMap(projectRoot);
2127
+ const direction = String(options.direction || '').trim() === 'source-to-target' ? 'source-to-target' : 'bidirectional';
2128
+ const relation = String(options.relation || '').trim() || 'delegation';
2129
+ const pairKey = workflowEdgePairKey({ from: fromId, to: toId, direction });
2130
+ const existing = (Array.isArray(graph.edges) ? graph.edges : [])
2131
+ .find(edge => workflowEdgePairKey(edge) === pairKey);
2132
+ if (existing) return { ok: true, connected: true, edge: existing };
2133
+ const edge = {
2134
+ id: `${fromId}->${toId}`,
2135
+ from: fromId,
2136
+ to: toId,
2137
+ relation,
2138
+ sourceHandle: null,
2139
+ targetHandle: null,
2140
+ direction,
2141
+ };
2142
+ writeWorkflowGraphMap(projectRoot, {
2143
+ ...graph,
2144
+ version: graph.version + 1,
2145
+ edges: [...(Array.isArray(graph.edges) ? graph.edges : []), edge],
2146
+ });
2147
+ return { ok: true, connected: false, edge };
2148
+ }
2149
+
2150
+ function magneticGroupForNode(topology, nodeId) {
2151
+ const info = topology.byNode[nodeId];
2152
+ if (!info) return null;
2153
+ return (Array.isArray(topology.groups) ? topology.groups : [])
2154
+ .find(group => group.magneticGroupId === info.magneticGroupId) || null;
2155
+ }
2156
+
2157
+ function isGoalNodeId(projectRoot, graph, id) {
2158
+ if (!GOAL_NODE_ID_RE.test(String(id || '').trim())) return false;
2159
+ const graphNode = (Array.isArray(graph.nodes) ? graph.nodes : [])
2160
+ .find(node => (node.nodeId || node.id) === id);
2161
+ // Store-merged goal nodes carry kind 'goal-node'; injected nodes without a
2162
+ // kind are still goal-shaped ids. Anything else is not a goal node.
2163
+ if (graphNode && graphNode.kind && graphNode.kind !== 'goal-node') return false;
2164
+ return true;
2165
+ }
2166
+
2167
+ function isTimerNodeId(projectRoot, id) {
2168
+ return (listEventNodes(projectRoot) || []).some(node => (node.nodeId || node.id) === id);
2169
+ }
2170
+
2171
+ function isAgentNodeId(graph, id) {
2172
+ return (Array.isArray(graph.nodes) ? graph.nodes : []).some(node => (
2173
+ (node.nodeId || node.id) === id
2174
+ && node.sessionId
2175
+ ));
2176
+ }
2177
+
2178
+ // Spec §6.2 (AC-015/T12): a magnetic group that contains a Timer and an Agent
2179
+ // may contain at most one Goal node. Throws the goal_already_bound shape when
2180
+ // the group of `goalNodeId` already has a Goal bound to its Timer.
2181
+ //
2182
+ // The count is CANDIDATE-INCLUSIVE (F2): the candidate goal is added to the
2183
+ // topology before counting, so a second Goal that is about to be connected or
2184
+ // created into a group with an existing Goal is rejected even though it is not
2185
+ // yet present in the graph's capsuleDockLinks. `opts.extraDockLinks` simulates
2186
+ // links that do not exist yet (e.g. the edge being connected); a free-floating
2187
+ // candidate with no links forms its own singleton group and passes, so
2188
+ // connect-time enforcement catches the second Goal later.
2189
+ export function assertSingleGoalPerGroup(projectRoot, goalNodeId, opts = {}) {
2190
+ const nodeId = String(goalNodeId || '').trim();
2191
+ const graph = loadWorkflowGraphMap(projectRoot);
2192
+ const extraLinks = Array.isArray(opts?.extraDockLinks) ? opts.extraDockLinks : [];
2193
+ const extraNodeIds = [...(Array.isArray(opts?.extraNodeIds) ? opts.extraNodeIds : [])];
2194
+ if (nodeId) extraNodeIds.push(nodeId);
2195
+ const topology = computeMagneticTopology(
2196
+ [...(Array.isArray(graph.capsuleDockLinks) ? graph.capsuleDockLinks : []), ...extraLinks],
2197
+ { nodeIds: extraNodeIds },
2198
+ );
2199
+ const group = magneticGroupForNode(topology, nodeId);
2200
+ if (!group) return { ok: true, magneticGroupId: null, groupNodeIds: [] };
2201
+ const goalNodeIds = group.nodeIds.filter(id => isGoalNodeId(projectRoot, graph, id));
2202
+ const timerNodeIds = group.nodeIds.filter(id => isTimerNodeId(projectRoot, id));
2203
+ const agentNodeIds = group.nodeIds.filter(id => isAgentNodeId(graph, id));
2204
+ if (timerNodeIds.length > 0 && agentNodeIds.length > 0 && goalNodeIds.length > 1) {
2205
+ const existingGoalNodeId = goalNodeIds.find(id => id !== nodeId) || goalNodeIds[0];
2206
+ const timerNodeId = timerNodeIds[0];
2207
+ const error = new Error(`This group already has a Goal (${existingGoalNodeId}) bound to its Timer (${timerNodeId}).`);
2208
+ error.code = 'goal_already_bound';
2209
+ error.statusCode = 409;
2210
+ error.existingGoalNodeId = existingGoalNodeId;
2211
+ error.timerNodeId = timerNodeId;
2212
+ throw error;
2213
+ }
2214
+ return { ok: true, magneticGroupId: group.magneticGroupId, groupNodeIds: group.nodeIds };
2215
+ }
2216
+
2217
+ function capsuleDockDeleteOnDetachEdgeIds(dockLinks = []) {
2218
+ const edgeIds = new Set();
2219
+ for (const link of Array.isArray(dockLinks) ? dockLinks : []) {
2220
+ for (const binding of Array.isArray(link?.edges) ? link.edges : []) {
2221
+ if (binding?.retention !== 'delete-on-detach') continue;
2222
+ const edgeId = String(binding.edgeId || binding.id || '').trim();
2223
+ if (edgeId) edgeIds.add(edgeId);
2224
+ }
2225
+ }
2226
+ return edgeIds;
2227
+ }
2228
+
2229
+ function pruneCapsuleDockLinksByEdgeIds(dockLinks = [], edgeIds = new Set()) {
2230
+ if (!edgeIds.size) return dockLinks;
2231
+ return (Array.isArray(dockLinks) ? dockLinks : [])
2232
+ .map(link => ({
2233
+ ...link,
2234
+ edges: (Array.isArray(link?.edges) ? link.edges : [])
2235
+ .filter(binding => !edgeIds.has(String(binding?.edgeId || binding?.id || '').trim())),
2236
+ }))
2237
+ .filter(link => Array.isArray(link.connections) && link.connections.length > 0);
2238
+ }
2239
+
2240
+ function workflowGraphEdgeSemanticKey(edge = {}) {
2241
+ const from = String(edge.from || edge.source || '').trim();
2242
+ const to = String(edge.to || edge.target || '').trim();
2243
+ if (!from || !to) return '';
2244
+ const relation = String(edge.relation || 'wf-bridge').trim() || 'wf-bridge';
2245
+ const direction = normalizeWorkflowEdgeDirection(edge.direction);
2246
+ const sourceHandle = String(edge.sourceHandle || '').trim();
2247
+ const targetHandle = String(edge.targetHandle || '').trim();
2248
+ if (direction !== 'source-to-target') {
2249
+ return [
2250
+ direction,
2251
+ relation,
2252
+ ...[`${from}:${sourceHandle}`, `${to}:${targetHandle}`].sort(),
2253
+ ].join('|');
2254
+ }
2255
+ return [direction, from, sourceHandle, relation, to, targetHandle].join('|');
2256
+ }
2257
+
2258
+ function dedupeWorkflowGraphEdgesBySemantic(edges = []) {
2259
+ const seen = new Set();
2260
+ const result = [];
2261
+ for (const edge of Array.isArray(edges) ? edges : []) {
2262
+ const key = workflowGraphEdgeSemanticKey(edge);
2263
+ const fallbackKey = String(edge?.id || '').trim();
2264
+ const dedupeKey = key || fallbackKey;
2265
+ if (!dedupeKey || seen.has(dedupeKey)) continue;
2266
+ seen.add(dedupeKey);
2267
+ result.push(edge);
2268
+ }
2269
+ return result;
2270
+ }
2271
+
2272
+ function filterPositionsForDeleted(positions, deletedNodes) {
2273
+ const next = { ...(positions || {}) };
2274
+ for (const node of deletedNodes) {
2275
+ if (node.nodeId) delete next[node.nodeId];
2276
+ if (node.sessionId) delete next[`session-${node.sessionId}`];
2277
+ }
2278
+ return next;
2279
+ }
2280
+
2281
+ function filterHistoryForDeleted(history, deletedNodes) {
2282
+ if (!Array.isArray(history)) return [];
2283
+ const { nodeIds, sessionIds } = deletedNodeSets(deletedNodes);
2284
+ const nodeMatchesDeleted = (node) => {
2285
+ if (!node || typeof node !== 'object') return false;
2286
+ const nodeId = String(node.nodeId || node.id || '').trim();
2287
+ const sessionId = String(node.sessionId || '').trim();
2288
+ return Boolean(
2289
+ (nodeId && (nodeIds.has(nodeId) || sessionIds.has(nodeId)))
2290
+ || (sessionId && sessionIds.has(sessionId))
2291
+ );
2292
+ };
2293
+ const filterDockLinks = (links) => (Array.isArray(links) ? links : []).filter(link => (
2294
+ Array.isArray(link?.nodeIds)
2295
+ && !link.nodeIds.some(nodeId => nodeIds.has(nodeId))
2296
+ && ![link.anchorId, link.draggedId].some(nodeId => nodeIds.has(nodeId))
2297
+ ));
2298
+ const filterSlice = (slice) => {
2299
+ if (!slice || typeof slice !== 'object') return slice;
2300
+ return {
2301
+ ...slice,
2302
+ positions: filterPositionsForDeleted(slice.positions, deletedNodes),
2303
+ edges: Array.isArray(slice.edges) ? slice.edges.filter(edge => !graphEdgeMatches(edge, nodeIds, sessionIds)) : [],
2304
+ dockLinks: filterDockLinks(slice.dockLinks),
2305
+ nodes: Array.isArray(slice.nodes) ? slice.nodes.filter(node => !nodeMatchesDeleted(node)) : [],
2306
+ };
2307
+ };
2308
+ return history.map((entry) => {
2309
+ // A delete-inverse op (its inverse.nodes slice carries the deleted node)
2310
+ // must survive intact so undo can restore the node and its edges (P5).
2311
+ if (Array.isArray(entry?.inverse?.nodes) && entry.inverse.nodes.some(nodeMatchesDeleted)) return entry;
2312
+ return {
2313
+ ...entry,
2314
+ positions: filterPositionsForDeleted(entry?.positions, deletedNodes),
2315
+ edges: Array.isArray(entry?.edges)
2316
+ ? entry.edges.filter(edge => !graphEdgeMatches(edge, nodeIds, sessionIds))
2317
+ : [],
2318
+ capsuleDockLinks: filterDockLinks(entry?.capsuleDockLinks),
2319
+ inverse: filterSlice(entry?.inverse),
2320
+ forward: filterSlice(entry?.forward),
2321
+ };
2322
+ });
2323
+ }
2324
+
2325
+ export function loadWorkflowGraphMap(projectRoot) {
2326
+ ensureA2aDefaults(projectRoot);
2327
+ const fallback = {
2328
+ schemaVersion: 1,
2329
+ version: 1,
2330
+ nodes: [],
2331
+ edges: [],
2332
+ capsuleDockLinks: [],
2333
+ positions: {},
2334
+ undoStack: [],
2335
+ redoStack: [],
2336
+ deletedNodes: [],
2337
+ };
2338
+ const graph = readJson(graphMapPath(projectRoot), fallback);
2339
+ const positions = normalizeGraphPositions(graph);
2340
+ const deletedNodes = normalizeDeletedNodes(graph);
2341
+ const { nodeIds, sessionIds } = deletedNodeSets(deletedNodes);
2342
+ const nodes = mergeStatefulGraphNodes(projectRoot, Array.isArray(graph.nodes) ? graph.nodes : [], positions)
2343
+ .filter(node => !graphNodeMatches(node, nodeIds, sessionIds));
2344
+ let edges = normalizeWorkflowGraphEdges(
2345
+ projectRoot,
2346
+ filterEdgesForGraphNodes(
2347
+ Array.isArray(graph.edges) ? graph.edges.filter(edge => !graphEdgeMatches(edge, nodeIds, sessionIds)) : [],
2348
+ nodes,
2349
+ ),
2350
+ nodes,
2351
+ { rejectDuplicates: false },
2352
+ );
2353
+ let capsuleDockLinks = normalizeCapsuleDockLinks(graph.capsuleDockLinks, nodes, edges);
2354
+ const legacyDockEdgeIds = capsuleDockDeleteOnDetachEdgeIds(capsuleDockLinks);
2355
+ if (legacyDockEdgeIds.size > 0) {
2356
+ edges = edges.filter(edge => !legacyDockEdgeIds.has(String(edge?.id || '').trim()));
2357
+ capsuleDockLinks = pruneCapsuleDockLinksByEdgeIds(capsuleDockLinks, legacyDockEdgeIds);
2358
+ }
2359
+ return {
2360
+ ...fallback,
2361
+ ...graph,
2362
+ schemaVersion: Number(graph.schemaVersion || 1),
2363
+ version: Number(graph.version || graph.graphVersion || 1),
2364
+ nodes,
2365
+ edges,
2366
+ capsuleDockLinks,
2367
+ positions: filterPositionsForDeleted(positions, deletedNodes),
2368
+ undoStack: filterHistoryForDeleted(graph.undoStack, deletedNodes),
2369
+ redoStack: filterHistoryForDeleted(graph.redoStack, deletedNodes),
2370
+ deletedNodes,
2371
+ graphContextPath: graphMapPath(projectRoot),
2372
+ };
2373
+ }
2374
+
2375
+ export function writeWorkflowGraphMap(projectRoot, graph = {}, options = {}) {
2376
+ const current = loadWorkflowGraphMap(projectRoot);
2377
+ const graphPayload = isPlainObject(graph) ? graph : {};
2378
+ const expectedVersion = normalizeExpectedGraphVersion(
2379
+ graphPayload.expectedVersion !== undefined ? graphPayload.expectedVersion : options.expectedVersion
2380
+ );
2381
+ if (expectedVersion !== null && expectedVersion !== current.version) {
2382
+ throw graphMapError(`Stale graph-map write rejected: expected version ${expectedVersion}, current version ${current.version}`, {
2383
+ statusCode: 409,
2384
+ code: 'STALE_GRAPH_VERSION',
2385
+ details: { expectedVersion, currentVersion: current.version },
2386
+ });
2387
+ }
2388
+
2389
+ const { expectedVersion: _expectedVersion, ifMatch: _ifMatch, ...graphPatch } = graphPayload;
2390
+ const deletedNodes = mergeDeletedNodes(current.deletedNodes, graphPatch.deletedNodes);
2391
+ const { nodeIds, sessionIds } = deletedNodeSets(deletedNodes);
2392
+ const rawNodes = Array.isArray(graphPatch.nodes) ? graphPatch.nodes : current.nodes;
2393
+ const rawEdges = Array.isArray(graphPatch.edges) ? graphPatch.edges : current.edges;
2394
+ const rawPositions = graphPatch.positions && typeof graphPatch.positions === 'object' && !Array.isArray(graphPatch.positions)
2395
+ ? graphPatch.positions
2396
+ : current.positions;
2397
+ const nodes = mergeStatefulGraphNodes(projectRoot, rawNodes, rawPositions)
2398
+ .filter(node => !graphNodeMatches(node, nodeIds, sessionIds));
2399
+ // Validate before the legacy endpoint silently filters dangling edges. A
2400
+ // graph-map PUT may omit deleted nodes for compatibility, but it may not
2401
+ // introduce an unknown endpoint or bypass the Timer/Agent edge ontology.
2402
+ assertWorkflowGraphEdgeOntology(projectRoot, {
2403
+ ...current,
2404
+ ...graphPatch,
2405
+ nodes,
2406
+ deletedNodes,
2407
+ }, rawEdges);
2408
+ let edges = normalizeWorkflowGraphEdges(
2409
+ projectRoot,
2410
+ filterEdgesForGraphNodes(rawEdges.filter(edge => !graphEdgeMatches(edge, nodeIds, sessionIds)), nodes),
2411
+ nodes,
2412
+ );
2413
+ let capsuleDockLinks = normalizeCapsuleDockLinks(
2414
+ Array.isArray(graphPatch.capsuleDockLinks) ? graphPatch.capsuleDockLinks : current.capsuleDockLinks,
2415
+ nodes,
2416
+ edges,
2417
+ );
2418
+ const legacyDockEdgeIds = capsuleDockDeleteOnDetachEdgeIds(capsuleDockLinks);
2419
+ if (legacyDockEdgeIds.size > 0) {
2420
+ edges = edges.filter(edge => !legacyDockEdgeIds.has(String(edge?.id || '').trim()));
2421
+ capsuleDockLinks = pruneCapsuleDockLinksByEdgeIds(capsuleDockLinks, legacyDockEdgeIds);
2422
+ }
2423
+ // Server-side graph history is authoritative (P5 undo design): the UI PUTs
2424
+ // undoStack:[] and must not wipe recorded history. Payload stacks are used
2425
+ // only when they carry real content (non-empty legacy writes) or when the
2426
+ // caller explicitly opts in via options.overrideHistory (undo/redo apply).
2427
+ const overrideHistory = options?.overrideHistory === true;
2428
+ const payloadUndoStack = Array.isArray(graphPatch.undoStack) ? graphPatch.undoStack : null;
2429
+ const payloadRedoStack = Array.isArray(graphPatch.redoStack) ? graphPatch.redoStack : null;
2430
+ const nextUndoStack = overrideHistory
2431
+ ? (payloadUndoStack || current.undoStack)
2432
+ : (payloadUndoStack && payloadUndoStack.length > 0 ? payloadUndoStack : current.undoStack);
2433
+ const nextRedoStack = overrideHistory
2434
+ ? (payloadRedoStack || current.redoStack)
2435
+ : (payloadRedoStack && payloadRedoStack.length > 0 ? payloadRedoStack : current.redoStack);
2436
+ const next = {
2437
+ ...current,
2438
+ ...graphPatch,
2439
+ schemaVersion: 1,
2440
+ version: Number(graphPatch.version || graphPatch.graphVersion || current.version + 1),
2441
+ nodes,
2442
+ edges,
2443
+ capsuleDockLinks,
2444
+ positions: filterPositionsForDeleted(rawPositions, deletedNodes),
2445
+ undoStack: filterHistoryForDeleted(nextUndoStack, deletedNodes),
2446
+ redoStack: filterHistoryForDeleted(nextRedoStack, deletedNodes),
2447
+ deletedNodes,
2448
+ updatedAt: new Date().toISOString(),
2449
+ };
2450
+ writeJson(graphMapPath(projectRoot), next);
2451
+ invalidateSnapshotCache(projectRoot);
2452
+ return { ...next, graphContextPath: graphMapPath(projectRoot) };
2453
+ }
2454
+
2455
+ export function updateWorkflowGraphSessionNode(projectRoot, sessionId, patch = {}) {
2456
+ const current = loadWorkflowGraphMap(projectRoot);
2457
+ let changed = false;
2458
+ const nodes = current.nodes.map((node) => {
2459
+ if (node.sessionId !== sessionId) return node;
2460
+ changed = true;
2461
+ return { ...node, ...patch, nodeId: node.nodeId || node.id };
2462
+ });
2463
+ if (!changed) return current;
2464
+ return writeWorkflowGraphMap(projectRoot, {
2465
+ ...current,
2466
+ version: current.version + 1,
2467
+ nodes,
2468
+ });
2469
+ }
2470
+
2471
+ export function removeWorkflowGraphNode(projectRoot, nodeIdOrSessionId, options = {}) {
2472
+ const current = loadWorkflowGraphMap(projectRoot);
2473
+ const key = String(nodeIdOrSessionId || '').trim();
2474
+ const target = current.nodes.find(node => {
2475
+ const nodeId = node.nodeId || node.id || '';
2476
+ const sessionId = node.sessionId || '';
2477
+ return nodeId === key || sessionId === key || (sessionId && `session-${sessionId}` === key);
2478
+ }) || null;
2479
+ const nodeId = target?.nodeId || target?.id || key;
2480
+ const sessionId = target?.sessionId || (key.startsWith('session-') ? key.slice('session-'.length) : null);
2481
+ const deletedAt = new Date().toISOString();
2482
+ const candidateEdges = Array.isArray(options.edges) ? options.edges : current.edges;
2483
+ const affectedEdges = candidateEdges.filter(edge => graphEdgeMatches(edge, new Set([nodeId]), new Set([sessionId].filter(Boolean))));
2484
+ const recoveryNode = options.node || target || { nodeId, sessionId: sessionId || null };
2485
+ const deletedNode = {
2486
+ nodeId,
2487
+ sessionId: sessionId || null,
2488
+ deletedAt,
2489
+ node: recoveryNode,
2490
+ edges: affectedEdges,
2491
+ position: current.positions?.[nodeId] || recoveryNode.position,
2492
+ };
2493
+ const deletedNodes = mergeDeletedNodes(current.deletedNodes, [deletedNode]);
2494
+ const { nodeIds, sessionIds } = deletedNodeSets(deletedNodes);
2495
+ // P5: delete callers pass options.undoStack/redoStack (the op recorded
2496
+ // BEFORE the delete) so the fresh delete-op survives the history filter;
2497
+ // other callers keep the existing filtered-current behavior.
2498
+ const overrideHistory = Array.isArray(options.undoStack) || Array.isArray(options.redoStack);
2499
+ const graph = writeWorkflowGraphMap(projectRoot, {
2500
+ ...current,
2501
+ version: current.version + 1,
2502
+ nodes: current.nodes.filter(node => !graphNodeMatches(node, nodeIds, sessionIds)),
2503
+ edges: current.edges.filter(edge => !graphEdgeMatches(edge, nodeIds, sessionIds)),
2504
+ positions: filterPositionsForDeleted(current.positions, deletedNodes),
2505
+ undoStack: Array.isArray(options.undoStack) ? options.undoStack : current.undoStack,
2506
+ redoStack: Array.isArray(options.redoStack) ? options.redoStack : current.redoStack,
2507
+ deletedNodes,
2508
+ }, { overrideHistory });
2509
+ // Typed-node stores live OUTSIDE the graph map (event/component/capability
2510
+ // stores); remove their state too, or the snapshot resurrects a "deleted"
2511
+ // typed node from its store record. Undo restores store state from the
2512
+ // inverse slice's eventState/componentState/capabilityState fields.
2513
+ try { deleteComponentNode(projectRoot, nodeId); } catch { /* not a component node */ }
2514
+ try { deleteCapabilityNode(projectRoot, nodeId); } catch { /* not a capability node */ }
2515
+ try { deleteEventNode(projectRoot, nodeId); } catch { /* not an event node */ }
2516
+ return { ok: true, removed: deletedNode, graph };
2517
+ }
2518
+
2519
+ export function restoreWorkflowGraphNode(projectRoot, nodeIdOrSessionId) {
2520
+ const fallback = { schemaVersion: 1, version: 1, nodes: [], edges: [], positions: {}, deletedNodes: [] };
2521
+ const raw = readJson(graphMapPath(projectRoot), fallback);
2522
+ const deletedNodes = normalizeDeletedNodes(raw);
2523
+ const key = String(nodeIdOrSessionId || '').trim();
2524
+ const recovery = deletedNodes.find(item => item.nodeId === key || item.sessionId === key || `session-${item.sessionId}` === key);
2525
+ if (!recovery) {
2526
+ throw graphMapError(`Deleted workflow node not found: ${key}`, { statusCode: 404, code: 'DELETED_NODE_NOT_FOUND' });
150
2527
  }
2528
+ const restoredNode = recovery.node || { nodeId: recovery.nodeId, sessionId: recovery.sessionId };
2529
+ const restoredNodeId = restoredNode.nodeId || restoredNode.id || recovery.nodeId;
2530
+ const nodes = Array.isArray(raw.nodes) ? raw.nodes.filter(node => !graphNodeMatches(node, new Set([restoredNodeId]), new Set([recovery.sessionId].filter(Boolean)))) : [];
2531
+ nodes.push({ ...restoredNode, nodeId: restoredNode.nodeId || restoredNode.id || restoredNodeId });
2532
+ const existingEdgeIds = new Set((Array.isArray(raw.edges) ? raw.edges : []).map(edge => edge.id));
2533
+ const edges = [
2534
+ ...(Array.isArray(raw.edges) ? raw.edges : []),
2535
+ ...(recovery.edges || []).filter(edge => edge?.id && !existingEdgeIds.has(edge.id)),
2536
+ ];
2537
+ const positions = { ...(raw.positions || {}) };
2538
+ if (recovery.position) positions[restoredNodeId] = recovery.position;
2539
+ else if (restoredNode.position) positions[restoredNodeId] = restoredNode.position;
2540
+ const remainingDeletedNodes = deletedNodes.filter(item => item !== recovery);
2541
+ writeJson(graphMapPath(projectRoot), {
2542
+ ...raw,
2543
+ version: Number(raw.version || 1) + 1,
2544
+ nodes,
2545
+ edges,
2546
+ positions,
2547
+ deletedNodes: remainingDeletedNodes,
2548
+ updatedAt: new Date().toISOString(),
2549
+ });
2550
+ invalidateSnapshotCache(projectRoot);
2551
+ return { ok: true, restored: restoredNode, graph: loadWorkflowGraphMap(projectRoot) };
151
2552
  }
152
2553
 
153
2554
  export function ensureA2aDefaults(projectRoot) {
@@ -163,6 +2564,24 @@ export function ensureA2aDefaults(projectRoot) {
163
2564
  workflows: BUILT_IN_WORKFLOWS,
164
2565
  });
165
2566
  writeJsonIfMissing(path.join(root, 'skills', 'terminal-control.json'), DEFAULT_TERMINAL_CONTROL_SKILL);
2567
+ writeJsonIfMissing(path.join(root, 'skills', 'workflow-node-map.json'), DEFAULT_WORKFLOW_NODE_MAP_SKILL);
2568
+ writeJsonIfMissing(path.join(root, 'skills', 'workflow-ontology.json'), DEFAULT_WORKFLOW_ONTOLOGY_SKILL);
2569
+ writeJsonIfMissing(path.join(root, 'skills', 'workflow-context.json'), DEFAULT_WORKFLOW_CONTEXT_SKILL);
2570
+ writeJsonIfMissing(path.join(root, 'skills', 'workflow-node-actions.json'), DEFAULT_WORKFLOW_NODE_ACTIONS_SKILL);
2571
+ writeJsonIfMissing(path.join(root, 'skills', 'workflow-timer-node.json'), DEFAULT_WORKFLOW_TIMER_NODE_SKILL);
2572
+ writeJsonIfMissing(path.join(root, 'skills', 'workflow-goal-node.json'), DEFAULT_WORKFLOW_GOAL_NODE_SKILL);
2573
+ writeJsonIfMissing(path.join(root, 'skills', 'workflow-agent-node.json'), DEFAULT_WORKFLOW_AGENT_NODE_SKILL);
2574
+ writeJsonIfMissing(path.join(root, 'skills', 'workflow-resource-node.json'), DEFAULT_WORKFLOW_RESOURCE_NODE_SKILL);
2575
+ writeJsonIfMissing(path.join(root, 'skills', 'workflow-markdown-node.json'), DEFAULT_WORKFLOW_MARKDOWN_NODE_SKILL);
2576
+ writeJsonIfMissing(path.join(root, 'skills', 'workflow-diagram-node.json'), DEFAULT_WORKFLOW_DIAGRAM_NODE_SKILL);
2577
+ writeJsonIfMissing(path.join(root, 'skills', 'workflow-file-node.json'), DEFAULT_WORKFLOW_FILE_NODE_SKILL);
2578
+ writeJsonIfMissing(path.join(root, 'skills', 'workflow-skill-group-node.json'), DEFAULT_WORKFLOW_SKILL_GROUP_NODE_SKILL);
2579
+ writeJsonIfMissing(path.join(root, 'skills', 'workflow-mcp-connector-node.json'), DEFAULT_WORKFLOW_MCP_CONNECTOR_NODE_SKILL);
2580
+ writeJsonIfMissing(path.join(root, 'skills', 'wf-ui-map.json'), DEFAULT_WF_UI_MAP_SKILL);
2581
+ // No snapshot invalidation here: ensureA2aDefaults runs on READ paths
2582
+ // (loadRoleGraph → every snapshot build). Invalidating would wipe the memo
2583
+ // on every build and make it useless. Files it creates are read by the
2584
+ // same call stack right after, so no staleness window exists.
166
2585
  return root;
167
2586
  }
168
2587
 
@@ -192,16 +2611,281 @@ export function loadBuiltInWorkflows(projectRoot) {
192
2611
  return Array.isArray(registry.workflows) ? registry.workflows : BUILT_IN_WORKFLOWS;
193
2612
  }
194
2613
 
195
- function readActiveTaskId(projectRoot) {
196
- const progressPath = path.join(projectRoot, 'Harness', 'PROGRESS.md');
197
- try {
198
- const content = fs.readFileSync(progressPath, 'utf8');
199
- const marker = content.match(/## Active Task\s+^- ([^\r\n]+)/m);
200
- if (marker) return marker[1].trim();
201
- } catch {
202
- return null;
2614
+ function graphNodeIdForSession(session) {
2615
+ return session.graphNodeId || `session-${session.sessionId}`;
2616
+ }
2617
+
2618
+ function isLiveStatus(status) {
2619
+ return status === 'running' || status === 'starting';
2620
+ }
2621
+
2622
+ function normalizeSessionStatus(status) {
2623
+ return status === 'saved' ? 'stopped' : status;
2624
+ }
2625
+
2626
+ function lifecycleForSession(session) {
2627
+ const status = normalizeSessionStatus(session.status);
2628
+ if (isLiveStatus(status)) return 'live';
2629
+ if (status === 'stopping') return 'stopping';
2630
+ return 'stopped';
2631
+ }
2632
+
2633
+ function runtimeStateForSession(session) {
2634
+ if (session.blockedReason === 'not-managed-by-current-wf-ui') return 'not-managed';
2635
+ return normalizeSessionStatus(session.status) || 'unknown';
2636
+ }
2637
+
2638
+ function nodeConfigSummary(session) {
2639
+ const config = normalizeNodeConfig(session.nodeConfig, session);
2640
+ const recommendation = recommendSkills(config, session);
2641
+ return {
2642
+ config,
2643
+ restartRequired: Boolean(session.restartRequired),
2644
+ restartRequiredFields: Array.isArray(session.restartRequiredFields) ? session.restartRequiredFields : [],
2645
+ configRevision: Number(session.configRevision || 0),
2646
+ recommendedSkills: recommendation.recommendedSkills,
2647
+ recommendationReason: recommendation.recommendationReason,
2648
+ displayName: session.displayName || '',
2649
+ roleTitle: session.roleTitle || '',
2650
+ };
2651
+ }
2652
+
2653
+ function controlForSession(session) {
2654
+ const live = isLiveStatus(normalizeSessionStatus(session.status));
2655
+ const managed = Boolean(session.managedByCurrentServer);
2656
+ const canStop = managed && live;
2657
+ const canStart = Boolean(session.sessionId && session.runtime) && !live;
2658
+ return {
2659
+ canReadGraph: Boolean(session.sessionId),
2660
+ canModifyGraph: managed && live && session.agentKind === 'main',
2661
+ canStart,
2662
+ canStop,
2663
+ canDelete: Boolean(session.sessionId) && !live,
2664
+ canOpenTerminal: managed && live,
2665
+ canOpenTranscript: Boolean(session.sessionId),
2666
+ canSendInput: managed && live,
2667
+ canCreateAgent: canStop && session.agentKind === 'main',
2668
+ };
2669
+ }
2670
+
2671
+ function buildSessionGraph(projectRoot, workflowId, sessions) {
2672
+ const persisted = loadWorkflowGraphMap(projectRoot);
2673
+ const persistedByNodeId = new Map((persisted.nodes || []).map(node => [node.nodeId || node.id, node]));
2674
+ const persistedBySessionId = new Map((persisted.nodes || []).filter(node => node.sessionId).map(node => [node.sessionId, node]));
2675
+ const componentNodes = listLiveComponentNodes(projectRoot).map(node => ({
2676
+ ...node,
2677
+ position: persisted.positions?.[node.nodeId]
2678
+ || persistedByNodeId.get(node.nodeId)?.position
2679
+ || node.position,
2680
+ }));
2681
+ const componentRefs = componentStateRefs(projectRoot);
2682
+ const eventNodes = listEventNodes(projectRoot).map(node => ({
2683
+ ...node,
2684
+ label: node.title,
2685
+ level: 2,
2686
+ status: 'ready',
2687
+ lifecycle: 'event-source',
2688
+ runtimeState: 'ready',
2689
+ managedByCurrentServer: true,
2690
+ control: {
2691
+ canReadGraph: true,
2692
+ canModifyGraph: true,
2693
+ canDelete: true,
2694
+ canCreateComponentNode: true,
2695
+ },
2696
+ position: persisted.positions?.[node.nodeId]
2697
+ || persistedByNodeId.get(node.nodeId)?.position
2698
+ || node.position,
2699
+ }));
2700
+ const eventRefs = eventStateRefs(projectRoot);
2701
+ const capabilityNodes = listCapabilityNodes(projectRoot).map(node => ({
2702
+ ...node,
2703
+ label: node.title,
2704
+ level: 2,
2705
+ status: 'ready',
2706
+ lifecycle: 'capability-provider',
2707
+ runtimeState: 'ready',
2708
+ managedByCurrentServer: true,
2709
+ control: {
2710
+ canReadGraph: true,
2711
+ canModifyGraph: true,
2712
+ canDelete: true,
2713
+ canCreateComponentNode: true,
2714
+ },
2715
+ position: persisted.positions?.[node.nodeId]
2716
+ || persistedByNodeId.get(node.nodeId)?.position
2717
+ || node.position,
2718
+ }));
2719
+ const capabilityRefs = capabilityStateRefs(projectRoot);
2720
+ const goalNodes = visibleGoalNodes(projectRoot, persisted).map(node => ({
2721
+ ...node,
2722
+ label: node.title,
2723
+ level: 1,
2724
+ status: node.status || 'active',
2725
+ lifecycle: 'goal-anchor',
2726
+ runtimeState: node.runtimeState || node.status || 'active',
2727
+ managedByCurrentServer: true,
2728
+ control: {
2729
+ canReadGraph: true,
2730
+ canModifyGraph: true,
2731
+ canDelete: true,
2732
+ canCreateComponentNode: false,
2733
+ },
2734
+ position: persisted.positions?.[node.nodeId]
2735
+ || persistedByNodeId.get(node.nodeId)?.position
2736
+ || node.position,
2737
+ }));
2738
+ const goalRefs = visibleGoalRefRecord(projectRoot, persisted);
2739
+ const sessionNodes = sessions.map((session) => {
2740
+ const configSummary = nodeConfigSummary(session);
2741
+ return {
2742
+ nodeId: graphNodeIdForSession(session),
2743
+ kind: 'terminal-session',
2744
+ sessionId: session.sessionId,
2745
+ peerId: session.peerId,
2746
+ agentKind: session.agentKind || (String(session.role || '').toLowerCase().includes('ceo') ? 'main' : 'subagent'),
2747
+ runtime: session.runtime,
2748
+ role: session.role,
2749
+ taskId: session.taskId || null,
2750
+ cwd: session.cwd || session.projectRoot || '',
2751
+ status: session.status,
2752
+ lifecycle: lifecycleForSession(session),
2753
+ runtimeState: runtimeStateForSession(session),
2754
+ managedByCurrentServer: Boolean(session.managedByCurrentServer),
2755
+ control: controlForSession(session),
2756
+ parentAgentId: session.parentAgentId || null,
2757
+ parentNodeId: session.parentNodeId || null,
2758
+ subagentMode: subagentModeForSnapshot(session.subagentMode),
2759
+ ...configSummary,
2760
+ position: persisted.positions?.[graphNodeIdForSession(session)]
2761
+ || persisted.positions?.[`session-${session.sessionId}`]
2762
+ || persistedByNodeId.get(graphNodeIdForSession(session))?.position
2763
+ || persistedBySessionId.get(session.sessionId)?.position
2764
+ || null,
2765
+ };
2766
+ });
2767
+ const graphNodes = [...sessionNodes, ...componentNodes, ...eventNodes, ...capabilityNodes, ...goalNodes];
2768
+ const bySessionId = new Map(sessionNodes.map(node => [node.sessionId, node]));
2769
+ const byNodeId = new Map(graphNodes.map(node => [node.nodeId, node]));
2770
+ const edgesById = new Map();
2771
+ const edgePairs = new Set();
2772
+
2773
+ for (const edge of persisted.edges) {
2774
+ const edgeFrom = edge.from || edge.source;
2775
+ const edgeTo = edge.to || edge.target;
2776
+ const fromNode = byNodeId.get(edgeFrom) || bySessionId.get(edge.fromSessionId || edgeFrom);
2777
+ const toNode = byNodeId.get(edgeTo) || bySessionId.get(edge.toSessionId || edgeTo);
2778
+ if (!fromNode || !toNode) continue;
2779
+ const direction = normalizeWorkflowEdgeDirection(edge.direction);
2780
+ const relation = edge.relation || 'wf-bridge';
2781
+ const pairKey = workflowEdgePairKey({ from: fromNode.nodeId, to: toNode.nodeId, direction });
2782
+ if (pairKey && edgePairs.has(pairKey)) continue;
2783
+ if (pairKey) edgePairs.add(pairKey);
2784
+ edgesById.set(edge.id || `${fromNode.nodeId}->${toNode.nodeId}`, {
2785
+ id: edge.id || `${fromNode.nodeId}->${toNode.nodeId}`,
2786
+ kind: edge.kind || (direction === 'source-to-target' && relation === 'event'
2787
+ ? 'event-link'
2788
+ : (fromNode.sessionId && toNode.sessionId ? 'communication-permission' : 'workflow-link')),
2789
+ from: fromNode.nodeId,
2790
+ to: toNode.nodeId,
2791
+ source: fromNode.nodeId,
2792
+ target: toNode.nodeId,
2793
+ fromSessionId: fromNode.sessionId || null,
2794
+ toSessionId: toNode.sessionId || null,
2795
+ relation,
2796
+ sourceHandle: edge.sourceHandle || null,
2797
+ targetHandle: edge.targetHandle || null,
2798
+ direction,
2799
+ offset: Number.isFinite(Number(edge.offset)) ? Number(edge.offset) : undefined,
2800
+ });
2801
+ }
2802
+
2803
+ for (const node of sessionNodes) {
2804
+ if (!node.parentAgentId) continue;
2805
+ const parent = bySessionId.get(node.parentAgentId);
2806
+ if (!parent) continue;
2807
+ const id = `${parent.nodeId}->${node.nodeId}`;
2808
+ const pairKey = workflowEdgePairKey({ from: parent.nodeId, to: node.nodeId });
2809
+ if (pairKey && edgePairs.has(pairKey)) continue;
2810
+ if (pairKey) edgePairs.add(pairKey);
2811
+ edgesById.set(id, {
2812
+ id,
2813
+ kind: 'communication-permission',
2814
+ from: parent.nodeId,
2815
+ to: node.nodeId,
2816
+ source: parent.nodeId,
2817
+ target: node.nodeId,
2818
+ fromSessionId: parent.sessionId,
2819
+ toSessionId: node.sessionId,
2820
+ relation: 'wf-bridge',
2821
+ sourceHandle: null,
2822
+ targetHandle: null,
2823
+ direction: 'bidirectional',
2824
+ });
2825
+ }
2826
+
2827
+ const edges = [...edgesById.values()];
2828
+ const capsuleDockLinks = Array.isArray(persisted.capsuleDockLinks) ? persisted.capsuleDockLinks : [];
2829
+ const capsuleDockConnectionEdges = capsuleDockLinks.flatMap(link => (
2830
+ Array.isArray(link.connections) ? link.connections : []
2831
+ ).map((connection) => {
2832
+ const fromNode = byNodeId.get(connection.source || connection.from);
2833
+ const toNode = byNodeId.get(connection.target || connection.to);
2834
+ if (!fromNode || !toNode) return null;
2835
+ const direction = normalizeWorkflowEdgeDirection(connection.direction);
2836
+ return {
2837
+ id: connection.id || `dock:${link.id}:${fromNode.nodeId}->${toNode.nodeId}`,
2838
+ kind: 'capsule-dock-link',
2839
+ from: fromNode.nodeId,
2840
+ to: toNode.nodeId,
2841
+ source: fromNode.nodeId,
2842
+ target: toNode.nodeId,
2843
+ fromSessionId: fromNode.sessionId || null,
2844
+ toSessionId: toNode.sessionId || null,
2845
+ relation: connection.relation || 'wf-bridge',
2846
+ sourceHandle: connection.sourceHandle || null,
2847
+ targetHandle: connection.targetHandle || null,
2848
+ direction,
2849
+ dockLinkId: link.id,
2850
+ };
2851
+ }).filter(Boolean));
2852
+ const contextEdges = dedupeWorkflowGraphEdgesBySemantic([...edges, ...capsuleDockConnectionEdges]);
2853
+ const graphContextBySessionId = {};
2854
+ for (const node of sessionNodes) {
2855
+ const outbound = contextEdges.filter(edge => edge.fromSessionId === node.sessionId).map(edge => edge.toSessionId);
2856
+ const inbound = contextEdges.filter(edge => edge.toSessionId === node.sessionId).map(edge => edge.fromSessionId);
2857
+ // Per-node Agent context is built by the canonical module (identical shape
2858
+ // to /api/workflow/context/:id). Session-enumeration extras stay local.
2859
+ const agentContext = buildAgentContext(projectRoot, node, persisted).context;
2860
+ graphContextBySessionId[node.sessionId] = {
2861
+ ...agentContext,
2862
+ agentKind: node.agentKind,
2863
+ parentAgentId: node.parentAgentId || inbound[0] || null,
2864
+ connectedPeerIds: [...new Set([...outbound, ...inbound])],
2865
+ outboundPeerIds: outbound,
2866
+ inboundPeerIds: inbound,
2867
+ };
203
2868
  }
204
- return null;
2869
+
2870
+ return {
2871
+ graph: {
2872
+ schemaVersion: 1,
2873
+ workflowId,
2874
+ version: persisted.version,
2875
+ nodes: graphNodes,
2876
+ edges,
2877
+ capsuleDockLinks,
2878
+ positions: persisted.positions || {},
2879
+ undoStack: persisted.undoStack,
2880
+ redoStack: persisted.redoStack,
2881
+ deletedNodes: persisted.deletedNodes || [],
2882
+ componentStateRefs: componentRefs,
2883
+ eventStateRefs: eventRefs,
2884
+ goalStateRefs: goalRefs,
2885
+ graphContextPath: graphMapPath(projectRoot),
2886
+ },
2887
+ graphContextBySessionId,
2888
+ };
205
2889
  }
206
2890
 
207
2891
  function activeTask(projectRoot) {
@@ -209,25 +2893,177 @@ function activeTask(projectRoot) {
209
2893
  const activeTaskId = readActiveTaskId(projectRoot);
210
2894
  if (activeTaskId) {
211
2895
  const capsule = parseTaskCapsule(path.join(tasksRoot, activeTaskId));
212
- if (capsule) return capsule;
2896
+ if (capsule && capsule.resumeRequired) return capsule;
2897
+ }
2898
+ // Never infer WF management from task recency. A legacy project without an
2899
+ // active pointer may still resume its sole open managed task, but direct
2900
+ // tasks and ambiguous multiple-managed-task projects stay unbound until the
2901
+ // controller selects a focus explicitly.
2902
+ const candidates = parseTaskList(tasksRoot).filter(task => task.resumeRequired);
2903
+ return candidates.length === 1 ? candidates[0] : null;
2904
+ }
2905
+
2906
+ function subagentModeForSnapshot(value) {
2907
+ const mode = String(value || 'built-in-subagents');
2908
+ // Backward compat: legacy 'wf-subagents' sessions are exposed under the
2909
+ // canonical 'wf-node-subagents' id in snapshots.
2910
+ return mode === 'wf-subagents' ? 'wf-node-subagents' : mode;
2911
+ }
2912
+
2913
+ // ── Snapshot memo (P1a) ───────────────────────────────────────────────────────
2914
+ // Full snapshots rescan every session dir (~1s). Memoize on the cheap state
2915
+ // fingerprint with a short TTL; writes and the WS watcher call
2916
+ // invalidateSnapshotCache() so disk changes are never served longer than TTL.
2917
+ const SNAPSHOT_MEMO_TTL_MS = 250;
2918
+ const SESSION_CACHE_FRESH_MS = 1500;
2919
+ const snapshotMemo = new Map(); // projectRoot -> { fingerprint, builtAt, snapshot }
2920
+ const sessionStateCache = new Map(); // `${projectRoot}\u0000${sessionId}` -> { mtimeMs, size, state }
2921
+
2922
+ // Clears the memo + session-scan cache for a project root. Safe to call at any
2923
+ // time (e.g. from the WS watcher on external disk changes): never throws.
2924
+ export function invalidateSnapshotCache(projectRoot) {
2925
+ try {
2926
+ snapshotMemo.delete(projectRoot);
2927
+ const prefix = `${projectRoot}\u0000`;
2928
+ for (const key of sessionStateCache.keys()) {
2929
+ if (key.startsWith(prefix)) sessionStateCache.delete(key);
2930
+ }
2931
+ } catch {
2932
+ // No-op: invalidation is best-effort.
2933
+ }
2934
+ }
2935
+
2936
+ // Session-scan cache for the snapshot path: a session's STATE.json is re-read
2937
+ // only when its dir mtime or size changed. The dir listing stays live so new /
2938
+ // removed session dirs are seen immediately. Mirror of
2939
+ // terminal-store.listTerminalSessions (kept local; terminal-store is untouched).
2940
+ function cachedListTerminalSessions(projectRoot) {
2941
+ const tasksRoot = path.join(projectRoot, 'Harness', 'tasks');
2942
+ const dirs = [];
2943
+ let taskEntries = [];
2944
+ try {
2945
+ taskEntries = fs.readdirSync(tasksRoot, { withFileTypes: true });
2946
+ } catch {
2947
+ taskEntries = [];
2948
+ }
2949
+ for (const taskEntry of taskEntries) {
2950
+ if (!taskEntry.isDirectory() || taskEntry.name.startsWith('_')) continue;
2951
+ const sessionsRoot = path.join(tasksRoot, taskEntry.name, 'sessions');
2952
+ let sessionEntries = [];
2953
+ try {
2954
+ sessionEntries = fs.readdirSync(sessionsRoot, { withFileTypes: true });
2955
+ } catch {
2956
+ continue;
2957
+ }
2958
+ for (const sessionEntry of sessionEntries) {
2959
+ if (sessionEntry.isDirectory()) dirs.push(path.join(sessionsRoot, sessionEntry.name));
2960
+ }
2961
+ }
2962
+ const a2aSessionsRoot = path.join(projectRoot, 'Harness', 'a2a', 'sessions');
2963
+ try {
2964
+ for (const sessionEntry of fs.readdirSync(a2aSessionsRoot, { withFileTypes: true })) {
2965
+ if (sessionEntry.isDirectory()) dirs.push(path.join(a2aSessionsRoot, sessionEntry.name));
2966
+ }
2967
+ } catch {
2968
+ // No unbound A2A sessions yet.
2969
+ }
2970
+ const sessions = [];
2971
+ for (const dir of dirs) {
2972
+ const statePath = path.join(dir, 'STATE.json');
2973
+ let stat = null;
2974
+ try {
2975
+ stat = fs.statSync(statePath);
2976
+ } catch {
2977
+ continue;
2978
+ }
2979
+ const key = `${projectRoot}\u0000${path.basename(dir)}`;
2980
+ const cached = sessionStateCache.get(key);
2981
+ // Fresh-file guard: files modified within the last 1.5s are always
2982
+ // re-read. mtimeMs has ms granularity, so two same-size rewrites inside
2983
+ // one millisecond (fast test sequences, restart flows) would otherwise
2984
+ // hit a stale cache entry forever.
2985
+ const freshlyWritten = Date.now() - stat.mtimeMs < SESSION_CACHE_FRESH_MS;
2986
+ if (cached && !freshlyWritten && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
2987
+ if (cached.state !== null) sessions.push(cached.state);
2988
+ continue;
2989
+ }
2990
+ const state = readJson(statePath, null);
2991
+ sessionStateCache.set(key, { mtimeMs: stat.mtimeMs, size: stat.size, state });
2992
+ if (state) sessions.push(state);
2993
+ }
2994
+ return sessions;
2995
+ }
2996
+
2997
+ // Cheap state fingerprint for incremental snapshot polling (P1a): the graph
2998
+ // version + session index summary (in-memory, no per-request dir scan).
2999
+ export function stateFingerprint(projectRoot) {
3000
+ const { count, latestUpdatedAt } = getSessionIndexSummary();
3001
+ const graph = loadWorkflowGraphMap(projectRoot);
3002
+ let mapMeta = '';
3003
+ try {
3004
+ const st = fs.statSync(graphMapPath(projectRoot));
3005
+ mapMeta = `${st.mtimeMs}|${st.size}`;
3006
+ } catch {
3007
+ // Graph map missing — the version term above already reflects that.
213
3008
  }
214
- return parseTaskList(tasksRoot)[0] || null;
3009
+ return `${graph.version || 0}|${count}|${latestUpdatedAt || ''}|${mapMeta}`;
215
3010
  }
216
3011
 
3012
+ // Memoized snapshot build: same shape as always; a memo hit within the TTL
3013
+ // returns the previous object (safe — the server only serializes it).
217
3014
  export function buildWorkflowSnapshot(projectRoot, sessionRegistry) {
3015
+ const fingerprint = stateFingerprint(projectRoot);
3016
+ const memo = snapshotMemo.get(projectRoot);
3017
+ if (memo && memo.fingerprint === fingerprint && Date.now() - memo.builtAt < SNAPSHOT_MEMO_TTL_MS) {
3018
+ return memo.snapshot;
3019
+ }
3020
+ const snapshot = buildWorkflowSnapshotInner(projectRoot, sessionRegistry);
3021
+ snapshotMemo.set(projectRoot, { fingerprint, builtAt: Date.now(), snapshot });
3022
+ return snapshot;
3023
+ }
3024
+
3025
+ function buildWorkflowSnapshotInner(projectRoot, sessionRegistry) {
218
3026
  const roleGraph = loadRoleGraph(projectRoot);
219
3027
  const task = activeTask(projectRoot);
220
3028
  const memorySessions = sessionRegistry && typeof sessionRegistry.getAll === 'function'
221
3029
  ? sessionRegistry.getAll()
222
3030
  : [];
223
- const diskSessions = listTerminalSessions(projectRoot);
3031
+ const diskSessions = cachedListTerminalSessions(projectRoot);
224
3032
  const bySessionId = new Map();
225
- for (const session of diskSessions) bySessionId.set(session.sessionId, session);
3033
+ const currentSessionIds = new Set(memorySessions.map(session => session.sessionId));
3034
+ const liveSessionIds = new Set(memorySessions.map(session => session.sessionId));
3035
+ for (const session of diskSessions) {
3036
+ const orphanedLiveState = !liveSessionIds.has(session.sessionId) && ['running', 'starting'].includes(session.status);
3037
+ bySessionId.set(session.sessionId, orphanedLiveState
3038
+ ? { ...session, status: session.status, attachMode: false, blockedReason: session.blockedReason || 'not-managed-by-current-wf-ui' }
3039
+ : { ...session, status: normalizeSessionStatus(session.status) });
3040
+ }
226
3041
  for (const session of memorySessions) {
227
- bySessionId.set(session.sessionId, { ...bySessionId.get(session.sessionId), ...session });
3042
+ bySessionId.set(session.sessionId, { ...bySessionId.get(session.sessionId), ...session, status: normalizeSessionStatus(session.status) });
228
3043
  }
229
3044
  const sessions = [...bySessionId.values()]
230
3045
  .sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')));
3046
+ const persisted = loadWorkflowGraphMap(projectRoot);
3047
+ const persistedNodeIds = new Set((persisted.nodes || []).map(node => node.nodeId || node.id).filter(Boolean));
3048
+ const persistedSessionIds = new Set((persisted.nodes || []).map(node => node.sessionId).filter(Boolean));
3049
+ const visibleSessions = sessions
3050
+ .map((session) => {
3051
+ const persistedBySession = (persisted.nodes || []).find(node => node.sessionId === session.sessionId);
3052
+ return {
3053
+ ...session,
3054
+ managedByCurrentServer: currentSessionIds.has(session.sessionId),
3055
+ graphNodeId: session.graphNodeId || persistedBySession?.nodeId || graphNodeIdForSession(session),
3056
+ };
3057
+ })
3058
+ .filter((session) => {
3059
+ if (session.graphReplacedBySessionId && !currentSessionIds.has(session.sessionId)) return false;
3060
+ return (
3061
+ currentSessionIds.has(session.sessionId)
3062
+ || persistedSessionIds.has(session.sessionId)
3063
+ || isLiveStatus(session.status)
3064
+ || persistedNodeIds.has(graphNodeIdForSession(session))
3065
+ );
3066
+ });
231
3067
 
232
3068
  const roleNodes = roleGraph.agents.map((agent) => ({
233
3069
  id: agent.agentId,
@@ -239,45 +3075,118 @@ export function buildWorkflowSnapshot(projectRoot, sessionRegistry) {
239
3075
  permissions: agent.permissions || [],
240
3076
  }));
241
3077
 
242
- const nodes = [...roleNodes];
243
- const edges = [...roleGraph.edges];
244
- for (const session of sessions) {
245
- const nodeId = `session-${session.sessionId}`;
3078
+ const nodes = [];
3079
+ for (const session of visibleSessions) {
246
3080
  const isWorkflowCeo = session.workflowMode === 'wf' && String(session.role || '').toLowerCase().includes('ceo');
3081
+ const graphNodeId = graphNodeIdForSession(session);
3082
+ const lifecycle = lifecycleForSession(session);
3083
+ const runtimeState = runtimeStateForSession(session);
3084
+ const control = controlForSession(session);
3085
+ const status = normalizeSessionStatus(session.status);
3086
+ const configSummary = nodeConfigSummary(session);
247
3087
  nodes.push({
248
- id: nodeId,
249
- label: isWorkflowCeo ? `${session.runtime} /wf CEO` : `${session.runtime} terminal`,
3088
+ id: graphNodeId,
3089
+ label: isWorkflowCeo ? `${session.runtime} WF CEO` : `${session.runtime} ${session.agentKind === 'main' ? 'main agent' : 'subagent'}`,
250
3090
  kind: 'terminal-session',
251
3091
  level: isWorkflowCeo ? 1 : 2,
252
- status: session.status,
3092
+ status,
3093
+ lifecycle,
3094
+ runtimeState,
3095
+ managedByCurrentServer: Boolean(session.managedByCurrentServer),
3096
+ control,
3097
+ blockedReason: session.blockedReason,
253
3098
  sessionId: session.sessionId,
254
3099
  taskId: session.taskId,
3100
+ agentKind: session.agentKind,
3101
+ role: session.role,
3102
+ displayName: session.displayName ?? '',
3103
+ roleTitle: session.roleTitle ?? '',
255
3104
  peerId: session.peerId,
256
3105
  runtime: session.runtime,
257
3106
  model: session.model,
258
3107
  provider: session.provider,
259
- subagentMode: session.subagentMode,
3108
+ subagentMode: subagentModeForSnapshot(session.subagentMode),
260
3109
  workflowMode: session.workflowMode,
261
3110
  objective: session.objective,
3111
+ cwd: session.cwd,
3112
+ graphNodeId,
3113
+ parentAgentId: session.parentAgentId || null,
3114
+ parentNodeId: session.parentNodeId || null,
3115
+ nodeHomePath: session.nodeHomePath || '',
3116
+ nodeHomeRel: session.nodeHomeRel || '',
3117
+ nodeInitPath: session.nodeInitPath || '',
3118
+ nodeInitRel: session.nodeInitRel || '',
3119
+ ...configSummary,
3120
+ });
3121
+ }
3122
+ for (const componentNode of listLiveComponentNodes(projectRoot)) {
3123
+ nodes.push({
3124
+ ...componentNode,
3125
+ label: componentNode.title,
3126
+ level: 2,
3127
+ status: 'ready',
262
3128
  });
263
- edges.push({
264
- from: isWorkflowCeo ? 'ceo' : 'terminal-controller',
265
- to: nodeId,
266
- relation: isWorkflowCeo ? 'runs-wf-terminal' : 'owns-session',
3129
+ }
3130
+ for (const eventNode of listEventNodes(projectRoot)) {
3131
+ nodes.push({
3132
+ ...eventNode,
3133
+ label: eventNode.title,
3134
+ level: 2,
3135
+ status: 'ready',
3136
+ lifecycle: 'event-source',
3137
+ runtimeState: 'ready',
3138
+ });
3139
+ }
3140
+ for (const goalNode of visibleGoalNodes(projectRoot, persisted)) {
3141
+ nodes.push({
3142
+ ...goalNode,
3143
+ label: goalNode.title,
3144
+ level: 1,
3145
+ status: goalNode.status || 'active',
3146
+ lifecycle: 'goal-anchor',
3147
+ runtimeState: goalNode.runtimeState || goalNode.status || 'active',
267
3148
  });
268
3149
  }
269
3150
 
3151
+ const workflowId = task ? `workflow-${task.taskId}` : 'workflow-none';
3152
+ const { graph, graphContextBySessionId } = buildSessionGraph(projectRoot, workflowId, visibleSessions);
3153
+ const nodeIdsByGraphId = new Map(nodes
3154
+ .map(node => [node.graphNodeId || node.nodeId || node.id, node.id])
3155
+ .filter(([key]) => key));
3156
+ const edges = graph.edges
3157
+ .map(edge => ({
3158
+ id: edge.id || `${edge.from}->${edge.to}`,
3159
+ from: nodeIdsByGraphId.get(edge.from) || edge.from,
3160
+ to: nodeIdsByGraphId.get(edge.to) || edge.to,
3161
+ source: nodeIdsByGraphId.get(edge.source || edge.from) || edge.source || edge.from,
3162
+ target: nodeIdsByGraphId.get(edge.target || edge.to) || edge.target || edge.to,
3163
+ relation: edge.relation || 'wf-bridge',
3164
+ fromSessionId: edge.fromSessionId || null,
3165
+ toSessionId: edge.toSessionId || null,
3166
+ sourceHandle: edge.sourceHandle || null,
3167
+ targetHandle: edge.targetHandle || null,
3168
+ direction: normalizeWorkflowEdgeDirection(edge.direction),
3169
+ offset: Number.isFinite(Number(edge.offset)) ? Number(edge.offset) : undefined,
3170
+ }))
3171
+ .filter(edge => edge.from && edge.to);
3172
+
270
3173
  return {
271
3174
  schemaVersion: 1,
272
- workflowId: task ? `workflow-${task.taskId}` : 'workflow-none',
3175
+ snapshotVersion: graph.version,
3176
+ generatedAt: new Date().toISOString(),
3177
+ workflowId,
273
3178
  taskId: task?.taskId || null,
274
3179
  mode: task?.mode || null,
3180
+ wfManaged: Boolean(task?.wfManaged),
3181
+ resumeRequired: Boolean(task?.resumeRequired),
3182
+ taskProject: task?.project || null,
3183
+ taskTags: Array.isArray(task?.tags) ? task.tags : [],
275
3184
  phase: task?.phase || null,
276
3185
  gate: task?.gate || null,
277
3186
  rootAgentId: roleGraph.rootAgentId,
278
3187
  subagentModes: [
279
3188
  { id: 'built-in-subagents', label: 'Built-in Subagents' },
280
- { id: 'wf-subagents', label: 'WF Subagents' },
3189
+ { id: 'wf-node-subagents', label: 'WF Node Subagents' },
281
3190
  ],
282
3191
  availableWorkflows: loadBuiltInWorkflows(projectRoot),
283
3192
  roles: {
@@ -289,8 +3198,17 @@ export function buildWorkflowSnapshot(projectRoot, sessionRegistry) {
289
3198
  blocks: task.blocks,
290
3199
  acceptance: task.acceptance,
291
3200
  } : null,
3201
+ workflow: {
3202
+ operations: workflowOperationsSnapshot(projectRoot),
3203
+ },
292
3204
  nodes,
293
3205
  edges,
294
3206
  sessions,
3207
+ graph,
3208
+ componentNodes: componentNodeStatesForSnapshot(projectRoot),
3209
+ eventNodes: eventNodeStates(projectRoot),
3210
+ capabilityNodes: capabilityNodeStates(projectRoot),
3211
+ goalNodes: visibleGoalStateRecord(projectRoot, graph),
3212
+ graphContextBySessionId,
295
3213
  };
296
3214
  }