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.
- package/CHANGELOG.md +49 -0
- package/README-CN.md +4 -2
- package/README.md +23 -4
- package/package.json +17 -2
- package/src/generator.js +19 -6
- package/src/index.js +531 -38
- package/src/prompts.js +4 -4
- package/src/ui/dist/assets/AgentsRoute-BGGs90wH.js +7 -0
- package/src/ui/dist/assets/Assistant-Bold-gm-uSS1B.woff2 +0 -0
- package/src/ui/dist/assets/Assistant-Medium-DrcxCXg3.woff2 +0 -0
- package/src/ui/dist/assets/Assistant-Regular-DVxZuzxb.woff2 +0 -0
- package/src/ui/dist/assets/Assistant-SemiBold-SCI4bEL9.woff2 +0 -0
- package/src/ui/dist/assets/Combination-DhBLEmof.js +42 -0
- package/src/ui/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/src/ui/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/src/ui/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/src/ui/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/src/ui/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/src/ui/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/src/ui/dist/assets/RolesRoute-DnodCSz0.js +2 -0
- package/src/ui/dist/assets/RuntimePicker-CucD8TGn.js +7 -0
- package/src/ui/dist/assets/SettingsRoute-DquMWjac.js +2 -0
- package/src/ui/dist/assets/TaskList-Bxc0Uktx.js +7 -0
- package/src/ui/dist/assets/TerminalDrawer-CDusYltc.js +36 -0
- package/src/ui/dist/assets/WorkflowRoute-BN8yHmUs.css +1 -0
- package/src/ui/dist/assets/WorkflowRoute-D-0wVDpY.js +334 -0
- package/src/ui/dist/assets/abnfDiagram-VRR7QNED-zkRKkaxO.js +2 -0
- package/src/ui/dist/assets/apl-B4CMkyY2.js +2 -0
- package/src/ui/dist/assets/ar-SA-G6X2FPQ2-BiHaArx5.js +11 -0
- package/src/ui/dist/assets/arc-CBnLtOFO.js +2 -0
- package/src/ui/dist/assets/architectureDiagram-ZJ3FMSHR-DWXRXsh1.js +37 -0
- package/src/ui/dist/assets/asciiarmor-Df11BRmG.js +2 -0
- package/src/ui/dist/assets/asn1-EdZsLKOL.js +2 -0
- package/src/ui/dist/assets/asterisk-B-8jnY81.js +2 -0
- package/src/ui/dist/assets/az-AZ-76LH7QW2-CiT05voO.js +2 -0
- package/src/ui/dist/assets/bg-BG-XCXSNQG7-L-UM9tNf.js +6 -0
- package/src/ui/dist/assets/blockDiagram-677ZJIJ3-BIyh3Bry.js +133 -0
- package/src/ui/dist/assets/bn-BD-2XOGV67Q-NdgbA0bC.js +6 -0
- package/src/ui/dist/assets/brainfuck-C4LP7Hcl.js +2 -0
- package/src/ui/dist/assets/c4Diagram-LMCZKHZV-CA54vWWM.js +11 -0
- package/src/ui/dist/assets/ca-ES-6MX7JW3Y-BKZZsQ5J.js +9 -0
- package/src/ui/dist/assets/channel-Da_-zcAu.js +2 -0
- package/src/ui/dist/assets/chunk-2Q5K7J3B-DE4cxr1v.js +2 -0
- package/src/ui/dist/assets/chunk-32BRIVSS-i7FwbEU9.js +2 -0
- package/src/ui/dist/assets/chunk-5VM5RSS4-BhIcOYSu.js +16 -0
- package/src/ui/dist/assets/chunk-EX3LRPZG-Ci9zZRIs.js +232 -0
- package/src/ui/dist/assets/chunk-JWPE2WC7-BFdkcz-G.js +2 -0
- package/src/ui/dist/assets/chunk-MOJQB5TN-Dzmapl_S.js +89 -0
- package/src/ui/dist/assets/chunk-RYQCIY6F-RTmp7FOy.js +2 -0
- package/src/ui/dist/assets/chunk-V7JOEXUC-DInU3v3D.js +207 -0
- package/src/ui/dist/assets/chunk-VR4S4FIN-BHoyGBQO.js +2 -0
- package/src/ui/dist/assets/chunk-XXDRQBXY-DfeLZNZx.js +2 -0
- package/src/ui/dist/assets/classDiagram-OUVF2IWQ-FKbUS0bu.js +2 -0
- package/src/ui/dist/assets/classDiagram-v2-EOCWNBFH-FKbUS0bu.js +2 -0
- package/src/ui/dist/assets/clike-B9uivgTg.js +2 -0
- package/src/ui/dist/assets/clojure-BMjYHr_A.js +2 -0
- package/src/ui/dist/assets/cmake-BQqOBYOt.js +2 -0
- package/src/ui/dist/assets/cobol-CWcv1MsR.js +2 -0
- package/src/ui/dist/assets/coffeescript-S37ZYGWr.js +2 -0
- package/src/ui/dist/assets/commonlisp-DBKNyK5s.js +2 -0
- package/src/ui/dist/assets/cose-bilkent-JH36ORCC-_5PUJ615.js +2 -0
- package/src/ui/dist/assets/crystal-SjHAIU92.js +2 -0
- package/src/ui/dist/assets/cs-CZ-2BRQDIVT-DnkfnhDU.js +12 -0
- package/src/ui/dist/assets/css-BnMrqG3P.js +2 -0
- package/src/ui/dist/assets/cynefin-VYW2F7L2-CmkcFv-b.js +179 -0
- package/src/ui/dist/assets/cynefinDiagram-TSTJHNR4-D72VZn8H.js +63 -0
- package/src/ui/dist/assets/cypher-C_CwsFkJ.js +2 -0
- package/src/ui/dist/assets/cytoscape.esm-DTSO7Bv0.js +332 -0
- package/src/ui/dist/assets/d-pRatUO7H.js +2 -0
- package/src/ui/dist/assets/da-DK-5WZEPLOC-C54tQUgd.js +6 -0
- package/src/ui/dist/assets/dagre-VKFMJZFB-0BsAkrmo.js +5 -0
- package/src/ui/dist/assets/de-DE-XR44H4JA-DGfZlR10.js +9 -0
- package/src/ui/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
- package/src/ui/dist/assets/diagram-FQU43EPY-C1ZJDOca.js +4 -0
- package/src/ui/dist/assets/diagram-G47NLZAW-D3pr3FSP.js +25 -0
- package/src/ui/dist/assets/diagram-NH7WQ7WH-Bwry0NV-.js +25 -0
- package/src/ui/dist/assets/diagram-OA4YK3LP-L6g09cmV.js +31 -0
- package/src/ui/dist/assets/diagram-WEI45ONY-Bh1puYVX.js +42 -0
- package/src/ui/dist/assets/diff-DbItnlRl.js +2 -0
- package/src/ui/dist/assets/directory-open-01563666-DWU9wJ6I.js +2 -0
- package/src/ui/dist/assets/directory-open-4ed118d0-CunoC1EB.js +2 -0
- package/src/ui/dist/assets/dockerfile-BKs6k2Af.js +2 -0
- package/src/ui/dist/assets/dtd-DF_7sFjM.js +2 -0
- package/src/ui/dist/assets/dylan-DwRh75JA.js +2 -0
- package/src/ui/dist/assets/ebnf-CDyGwa7X.js +2 -0
- package/src/ui/dist/assets/ebnfDiagram-CCIWWBDH-BC_mBRR5.js +2 -0
- package/src/ui/dist/assets/ecl-Cabwm37j.js +2 -0
- package/src/ui/dist/assets/eiffel-CnydiIhH.js +2 -0
- package/src/ui/dist/assets/el-GR-BZB4AONW-DsnwWweL.js +11 -0
- package/src/ui/dist/assets/elm-vLlmbW-K.js +2 -0
- package/src/ui/dist/assets/erDiagram-Q63AITRT-Bkxam36l.js +86 -0
- package/src/ui/dist/assets/erlang-BNw1qcRV.js +2 -0
- package/src/ui/dist/assets/es-ES-U4NZUMDT-CRxBcQgC.js +10 -0
- package/src/ui/dist/assets/eu-ES-A7QVB2H4-CTd_E7YR.js +12 -0
- package/src/ui/dist/assets/excalidraw-default-bMWIgNCy.svg +1 -0
- package/src/ui/dist/assets/fa-IR-HGAKTJCU-DO56k7eG.js +9 -0
- package/src/ui/dist/assets/factor-kuTfRLto.js +2 -0
- package/src/ui/dist/assets/fcl-Kvtd6kyn.js +2 -0
- package/src/ui/dist/assets/fi-FI-Z5N7JZ37-DEDCF9WB.js +7 -0
- package/src/ui/dist/assets/file-open-002ab408-DIuFHtCF.js +2 -0
- package/src/ui/dist/assets/file-open-7c801643-684qeFg4.js +2 -0
- package/src/ui/dist/assets/file-save-3189631c-C1wFhQhH.js +2 -0
- package/src/ui/dist/assets/file-save-745eba88-Bb9F9Kg7.js +2 -0
- package/src/ui/dist/assets/flowDiagram-23GEKE2U-C3SAEao7.js +157 -0
- package/src/ui/dist/assets/forth-Ffai-XNe.js +2 -0
- package/src/ui/dist/assets/fortran-DYz_wnZ1.js +2 -0
- package/src/ui/dist/assets/fr-FR-RHASNOE6-DE2nPyDt.js +10 -0
- package/src/ui/dist/assets/ganttDiagram-NO4QXBWP-U-J_Xi-_.js +293 -0
- package/src/ui/dist/assets/gas-Bneqetm1.js +2 -0
- package/src/ui/dist/assets/gherkin-heZmZLOM.js +2 -0
- package/src/ui/dist/assets/gitGraphDiagram-IHSO6WYX-BADVbD-a.js +107 -0
- package/src/ui/dist/assets/gl-ES-HMX3MZ6V-mCqVgQHC.js +11 -0
- package/src/ui/dist/assets/graph-C9eacEi8.js +2 -0
- package/src/ui/dist/assets/groovy-D9Dt4D0W.js +2 -0
- package/src/ui/dist/assets/haskell-Cw1EW3IL.js +2 -0
- package/src/ui/dist/assets/haxe-H-WmDvRZ.js +2 -0
- package/src/ui/dist/assets/he-IL-6SHJWFNN-KnaLrCzw.js +11 -0
- package/src/ui/dist/assets/hi-IN-IWLTKZ5I-wuqiaaWt.js +5 -0
- package/src/ui/dist/assets/http-DBlCnlav.js +2 -0
- package/src/ui/dist/assets/hu-HU-A5ZG7DT2-DLg_zm22.js +8 -0
- package/src/ui/dist/assets/id-ID-SAP4L64H-MB2mXqcR.js +11 -0
- package/src/ui/dist/assets/idl-BEugSyMb.js +2 -0
- package/src/ui/dist/assets/image-blob-reduce.esm-HwCm7IZ0.js +8 -0
- package/src/ui/dist/assets/index-39SJtrt0.js +2 -0
- package/src/ui/dist/assets/index-BQ0LIrW-.js +2 -0
- package/src/ui/dist/assets/index-BcAUh2Nm.css +1 -0
- package/src/ui/dist/assets/index-BcThOK-i.js +331 -0
- package/src/ui/dist/assets/index-BnxV3k_S.js +3 -0
- package/src/ui/dist/assets/index-C7MZBHbT.js +117 -0
- package/src/ui/dist/assets/index-CCvKI2rM.js +2 -0
- package/src/ui/dist/assets/index-CZvmcCab.js +8 -0
- package/src/ui/dist/assets/index-Cp3hLKNl.js +2 -0
- package/src/ui/dist/assets/index-D7ayFxcx.js +2 -0
- package/src/ui/dist/assets/index-DAYxSQGU.js +2 -0
- package/src/ui/dist/assets/index-DD-HeMV0.css +1 -0
- package/src/ui/dist/assets/index-DPMxd3Qn.js +2 -0
- package/src/ui/dist/assets/index-DSe2jx3k.js +4 -0
- package/src/ui/dist/assets/index-DUirCkgW.js +2 -0
- package/src/ui/dist/assets/index-DaKPQ2hN.js +159 -0
- package/src/ui/dist/assets/index-DzpuR0Jg.js +2 -0
- package/src/ui/dist/assets/index-POndwsCI.js +2 -0
- package/src/ui/dist/assets/index-R8NM9I_9.js +2 -0
- package/src/ui/dist/assets/index-TCDuDhBj.js +2 -0
- package/src/ui/dist/assets/index-TckzASdM.js +2 -0
- package/src/ui/dist/assets/index-Yi5ytFFr.js +2 -0
- package/src/ui/dist/assets/infoDiagram-FWYZ7A6U-CiGBlFqH.js +3 -0
- package/src/ui/dist/assets/init-Gi6I4Gst.js +2 -0
- package/src/ui/dist/assets/ishikawaDiagram-FXEZZL3T-B1aheRbz.js +71 -0
- package/src/ui/dist/assets/it-IT-JPQ66NNP-DT3HSU7X.js +12 -0
- package/src/ui/dist/assets/ja-JP-DBVTYXUO-CsfJjjWt.js +9 -0
- package/src/ui/dist/assets/javascript-iXu5QeM3.js +2 -0
- package/src/ui/dist/assets/journeyDiagram-5HDEW3XC-DtnJPxub.js +140 -0
- package/src/ui/dist/assets/julia-DuME0IfC.js +2 -0
- package/src/ui/dist/assets/kaa-6HZHGXH3-DQz-1-Gn.js +2 -0
- package/src/ui/dist/assets/kab-KAB-ZGHBKWFO-CaH-LSoK.js +9 -0
- package/src/ui/dist/assets/kanban-definition-HUTT4EX6-CZatSm3e.js +90 -0
- package/src/ui/dist/assets/katex-C5jXJg4s.js +258 -0
- package/src/ui/dist/assets/kk-KZ-P5N5QNE5-BMXmcm55.js +2 -0
- package/src/ui/dist/assets/km-KH-HSX4SM5Z-ZeoJNBpN.js +12 -0
- package/src/ui/dist/assets/ko-KR-MTYHY66A-BeaLRKph.js +10 -0
- package/src/ui/dist/assets/ku-TR-6OUDTVRD-B_28KrkR.js +10 -0
- package/src/ui/dist/assets/layout-DEXfKzaS.js +2 -0
- package/src/ui/dist/assets/linear-CpEDnQRC.js +2 -0
- package/src/ui/dist/assets/livescript-BwQOo05w.js +2 -0
- package/src/ui/dist/assets/lt-LT-XHIRWOB4-BxDcnUhp.js +4 -0
- package/src/ui/dist/assets/lua-VAEuO923.js +2 -0
- package/src/ui/dist/assets/lv-LV-5QDEKY6T-DpKDl1_y.js +8 -0
- package/src/ui/dist/assets/map-Czzmt4hB.js +2 -0
- package/src/ui/dist/assets/mathematica-DTrFuWx2.js +2 -0
- package/src/ui/dist/assets/mbox-CNhZ1qSd.js +2 -0
- package/src/ui/dist/assets/mermaidLoader-CBO31t0J.js +305 -0
- package/src/ui/dist/assets/mermaidLoader-CEK31ho9.css +1 -0
- package/src/ui/dist/assets/mindmap-definition-LN4V7U3C-CQ0TccEr.js +97 -0
- package/src/ui/dist/assets/mirc-CjQqDB4T.js +2 -0
- package/src/ui/dist/assets/mllike-CXdrOF99.js +2 -0
- package/src/ui/dist/assets/modelica-Dc1JOy9r.js +2 -0
- package/src/ui/dist/assets/mr-IN-CRQNXWMA-BKCZT58p.js +14 -0
- package/src/ui/dist/assets/mscgen-BA5vi2Kp.js +2 -0
- package/src/ui/dist/assets/mumps-BT43cFF4.js +2 -0
- package/src/ui/dist/assets/my-MM-5M5IBNSE-D34k0G9T.js +2 -0
- package/src/ui/dist/assets/nb-NO-T6EIAALU-DXr_KW8J.js +11 -0
- package/src/ui/dist/assets/nginx-DdIZxoE0.js +2 -0
- package/src/ui/dist/assets/nl-NL-IS3SIHDZ-C1L0aVDR.js +9 -0
- package/src/ui/dist/assets/nn-NO-6E72VCQL-CLWVSxbZ.js +9 -0
- package/src/ui/dist/assets/nsis-LdVXkNf5.js +2 -0
- package/src/ui/dist/assets/ntriples-BfvgReVJ.js +2 -0
- package/src/ui/dist/assets/oc-FR-POXYY2M6-nf8_J3eR.js +9 -0
- package/src/ui/dist/assets/octave-Ck1zUtKM.js +2 -0
- package/src/ui/dist/assets/ordinal-Cboi1Yqb.js +2 -0
- package/src/ui/dist/assets/oz-BzwKVEFT.js +2 -0
- package/src/ui/dist/assets/pa-IN-N4M65BXN-CAMJ-xd6.js +5 -0
- package/src/ui/dist/assets/pascal--L3eBynH.js +2 -0
- package/src/ui/dist/assets/pegDiagram-2B236MQR-Bj2RcmRd.js +2 -0
- package/src/ui/dist/assets/percentages-BXMCSKIN-s9yWFdr6.js +160 -0
- package/src/ui/dist/assets/perl-CdXCOZ3F.js +2 -0
- package/src/ui/dist/assets/pica-BGv_AhAB.js +8 -0
- package/src/ui/dist/assets/pieDiagram-ENE6RG2P-obpzz8dC.js +40 -0
- package/src/ui/dist/assets/pig-CevX1Tat.js +2 -0
- package/src/ui/dist/assets/pl-PL-T2D74RX3-DKJ5H85v.js +10 -0
- package/src/ui/dist/assets/powershell-CFHJl5sT.js +2 -0
- package/src/ui/dist/assets/properties-C78fOPTZ.js +2 -0
- package/src/ui/dist/assets/protobuf-ChK-085T.js +2 -0
- package/src/ui/dist/assets/{proxy-D0vUbds5.js → proxy-BSmwef4H.js} +2 -2
- package/src/ui/dist/assets/pt-BR-5N22H2LF-B5kow-kN.js +10 -0
- package/src/ui/dist/assets/pt-PT-UZXXM6DQ-B2G34fso.js +10 -0
- package/src/ui/dist/assets/pug-DeIclll2.js +2 -0
- package/src/ui/dist/assets/puppet-DMA9R1ak.js +2 -0
- package/src/ui/dist/assets/python-BuPzkPfP.js +2 -0
- package/src/ui/dist/assets/q-pXgVlZs6.js +2 -0
- package/src/ui/dist/assets/quadrantDiagram-ABIIQ3AL-Cl-T7NkJ.js +8 -0
- package/src/ui/dist/assets/r-B6wPVr8A.js +2 -0
- package/src/ui/dist/assets/railroadDiagram-RFXS5EU6-DMq82RHg.js +2 -0
- package/src/ui/dist/assets/{refresh-cw-JNmExijd.js → refresh-cw-CbFd7guV.js} +2 -2
- package/src/ui/dist/assets/requirementDiagram-TGXJPOKE-tGInMDgU.js +85 -0
- package/src/ui/dist/assets/ro-RO-JPDTUUEW-BwLdZW91.js +12 -0
- package/src/ui/dist/assets/roundRect-0PYZxl1G.js +2 -0
- package/src/ui/dist/assets/rpm-CTu-6PCP.js +2 -0
- package/src/ui/dist/assets/ru-RU-B4JR7IUQ-DpdAgIJc.js +10 -0
- package/src/ui/dist/assets/ruby-B2Rjki9n.js +2 -0
- package/src/ui/dist/assets/runtimeBrand-BoKPimsz.js +2 -0
- package/src/ui/dist/assets/sankeyDiagram-HTMAVEWB-C7X-ET4m.js +41 -0
- package/src/ui/dist/assets/sas-B4kiWyti.js +2 -0
- package/src/ui/dist/assets/save-D1CXHwBw.js +12 -0
- package/src/ui/dist/assets/scheme-C41bIUwD.js +2 -0
- package/src/ui/dist/assets/sequenceDiagram-DBY2YBRQ-Bn24i_nY.js +163 -0
- package/src/ui/dist/assets/settings-2-DtSNAAcU.js +22 -0
- package/src/ui/dist/assets/shell-CjFT_Tl9.js +2 -0
- package/src/ui/dist/assets/shield-check-BVbJ8T_N.js +7 -0
- package/src/ui/dist/assets/si-LK-N5RQ5JYF-naJaUy5u.js +2 -0
- package/src/ui/dist/assets/sieve-C3Gn_uJK.js +2 -0
- package/src/ui/dist/assets/simple-mode-GW_nhZxv.js +2 -0
- package/src/ui/dist/assets/sizeCapture-X5ZJPWSS-DAQHfm82.js +2 -0
- package/src/ui/dist/assets/sk-SK-C5VTKIMK-1NPz1J_d.js +7 -0
- package/src/ui/dist/assets/sl-SI-NN7IZMDC-D8RfmNQw.js +7 -0
- package/src/ui/dist/assets/smalltalk-CnHTOXQT.js +2 -0
- package/src/ui/dist/assets/solr-DehyRSwq.js +2 -0
- package/src/ui/dist/assets/sparql-DkYu6x3z.js +2 -0
- package/src/ui/dist/assets/spreadsheet-BCZA_wO0.js +2 -0
- package/src/ui/dist/assets/sql-D0XecflT.js +2 -0
- package/src/ui/dist/assets/stateDiagram-2N3HPSRC-Ck48D1ck.js +2 -0
- package/src/ui/dist/assets/stateDiagram-v2-6OUMAXLB-CdfRCJAJ.js +2 -0
- package/src/ui/dist/assets/stex-C3f8Ysf7.js +2 -0
- package/src/ui/dist/assets/style-C3QI7VPf.css +1 -0
- package/src/ui/dist/assets/stylus-B533Al4x.js +2 -0
- package/src/ui/dist/assets/subset-shared.chunk-eAtLHmbi.js +85 -0
- package/src/ui/dist/assets/subset-worker.chunk-DeHCTnTk.js +2 -0
- package/src/ui/dist/assets/sv-SE-XGPEYMSR-BeOYCU-8.js +11 -0
- package/src/ui/dist/assets/swift-BzpIVaGY.js +2 -0
- package/src/ui/dist/assets/swimlanes-5IMT3BWC-DOy22UjP.js +3 -0
- package/src/ui/dist/assets/swimlanesDiagram-G3AALYLV-sicPbVOg.js +9 -0
- package/src/ui/dist/assets/ta-IN-2NMHFXQM-CtS68WDd.js +10 -0
- package/src/ui/dist/assets/tcl-DVfN8rqt.js +2 -0
- package/src/ui/dist/assets/{terminal-BBQvnOAK.js → terminal-DEby5Y_P.js} +2 -2
- package/src/ui/dist/assets/terminalControl-BkFeDVXo.js +134 -0
- package/src/ui/dist/assets/textile-CnDTJFAw.js +2 -0
- package/src/ui/dist/assets/th-TH-HPSO5L25-DCHY9-Co.js +3 -0
- package/src/ui/dist/assets/tiddlywiki-DO-Gjzrf.js +2 -0
- package/src/ui/dist/assets/tiki-DGYXhP31.js +2 -0
- package/src/ui/dist/assets/timeline-definition-FHXFAJF6-DVkG72qO.js +121 -0
- package/src/ui/dist/assets/toml-Bm5Em-hy.js +2 -0
- package/src/ui/dist/assets/tr-TR-DEFEU3FU-y5-vjTv8.js +8 -0
- package/src/ui/dist/assets/troff-wAsdV37c.js +2 -0
- package/src/ui/dist/assets/ttcn-CfJYG6tj.js +2 -0
- package/src/ui/dist/assets/ttcn-cfg-B9xdYoR4.js +2 -0
- package/src/ui/dist/assets/turtle-B1tBg_DP.js +2 -0
- package/src/ui/dist/assets/uk-UA-QMV73CPH-BRbR5It6.js +7 -0
- package/src/ui/dist/assets/useReducedMotion-D07XP52Q.js +12 -0
- package/src/ui/dist/assets/vb-CmGdzxic.js +2 -0
- package/src/ui/dist/assets/vbscript-BuJXcnF6.js +2 -0
- package/src/ui/dist/assets/velocity-D8B20fx6.js +2 -0
- package/src/ui/dist/assets/vennDiagram-L72KCM5P-DwMwBlf-.js +35 -0
- package/src/ui/dist/assets/verilog-C6RDOZhf.js +2 -0
- package/src/ui/dist/assets/vhdl-lSbBsy5d.js +2 -0
- package/src/ui/dist/assets/vi-VN-M7AON7JQ-s6wSBv2s.js +6 -0
- package/src/ui/dist/assets/wardleyDiagram-EHGQE667-CNuuvzEg.js +79 -0
- package/src/ui/dist/assets/webidl-ZXfAyPTL.js +2 -0
- package/src/ui/dist/assets/{x-DoIOGAJz.js → x-B84wq78k.js} +2 -2
- package/src/ui/dist/assets/xquery-DzFWVndE.js +2 -0
- package/src/ui/dist/assets/xychartDiagram-FW5EYKEG-dUpZxmn6.js +8 -0
- package/src/ui/dist/assets/yacas-BJ4BC0dw.js +2 -0
- package/src/ui/dist/assets/z80-Hz9HOZM7.js +2 -0
- package/src/ui/dist/assets/zh-CN-LNUGB5OW-Cu_-POQN.js +11 -0
- package/src/ui/dist/assets/zh-HK-E62DVLB3-BCx4W4qZ.js +2 -0
- package/src/ui/dist/assets/zh-TW-RAJ6MFWO-02wx_Mrj.js +10 -0
- package/src/ui/dist/icons/excalidraw.svg +1 -0
- package/src/ui/dist/icons/markdown.svg +1 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/package.json +10 -0
- package/src/ui/playwright.config.ts +36 -0
- package/src/ui/pnpm-workspace.yaml +2 -0
- package/src/ui/public/icons/excalidraw.svg +1 -0
- package/src/ui/public/icons/markdown.svg +1 -0
- package/src/ui/tsconfig.json +2 -1
- package/src/ui/vite.config.ts +1 -0
- package/src/wf-ui-server/__tests__/a2a-store.test.mjs +432 -15
- package/src/wf-ui-server/__tests__/action-registry-loader.test.mjs +156 -0
- package/src/wf-ui-server/__tests__/claude-stream-json.test.mjs +397 -0
- package/src/wf-ui-server/__tests__/codex-appserver.test.mjs +392 -0
- package/src/wf-ui-server/__tests__/component-node-api.test.mjs +881 -0
- package/src/wf-ui-server/__tests__/component-node-store.test.mjs +401 -0
- package/src/wf-ui-server/__tests__/control-plane-acceptance.test.mjs +2027 -0
- package/src/wf-ui-server/__tests__/control-plane-cli-smoke.test.mjs +1014 -0
- package/src/wf-ui-server/__tests__/dispatch-lifecycle.test.mjs +330 -0
- package/src/wf-ui-server/__tests__/effort-compatibility-red-092.test.mjs +151 -0
- package/src/wf-ui-server/__tests__/graph-layout-agent-tree.test.mjs +254 -0
- package/src/wf-ui-server/__tests__/graph-layout-tidy.test.mjs +260 -0
- package/src/wf-ui-server/__tests__/graph-layout.test.mjs +84 -0
- package/src/wf-ui-server/__tests__/node-config-api.test.mjs +384 -0
- package/src/wf-ui-server/__tests__/opencode-server.test.mjs +526 -0
- package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +2 -2
- package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +71 -1
- package/src/wf-ui-server/__tests__/runtime-config.test.mjs +3 -3
- package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +21 -0
- package/src/wf-ui-server/__tests__/runtime-review.test.mjs +828 -0
- package/src/wf-ui-server/__tests__/server.integration.test.mjs +1463 -11
- package/src/wf-ui-server/__tests__/session-cleanup.test.mjs +159 -0
- package/src/wf-ui-server/__tests__/session-registry.test.mjs +52 -3
- package/src/wf-ui-server/__tests__/settings.test.mjs +12 -2
- package/src/wf-ui-server/__tests__/spawn-gate.test.mjs +78 -0
- package/src/wf-ui-server/__tests__/task-group-parser.test.mjs +120 -0
- package/src/wf-ui-server/__tests__/task-group-tag.test.mjs +264 -0
- package/src/wf-ui-server/__tests__/task-parser.test.mjs +49 -3
- package/src/wf-ui-server/__tests__/terminal-context-hardening.test.mjs +122 -0
- package/src/wf-ui-server/__tests__/terminal-context.test.mjs +164 -0
- package/src/wf-ui-server/__tests__/terminal-store.test.mjs +4 -4
- package/src/wf-ui-server/__tests__/wf-browser-api.test.mjs +599 -0
- package/src/wf-ui-server/__tests__/wf-browser-store.test.mjs +184 -0
- package/src/wf-ui-server/__tests__/workflow-action-registry.test.mjs +282 -0
- package/src/wf-ui-server/__tests__/workflow-agent-communication.test.mjs +411 -0
- package/src/wf-ui-server/__tests__/workflow-agent-context.test.mjs +323 -0
- package/src/wf-ui-server/__tests__/workflow-agent-cooperation-permissions.test.mjs +318 -0
- package/src/wf-ui-server/__tests__/workflow-agent-messages-structured.test.mjs +458 -0
- package/src/wf-ui-server/__tests__/workflow-agent-routing.test.mjs +434 -0
- package/src/wf-ui-server/__tests__/workflow-agent-runtime.test.mjs +324 -0
- package/src/wf-ui-server/__tests__/workflow-api-integration.test.mjs +263 -0
- package/src/wf-ui-server/__tests__/workflow-api-phase3.test.mjs +1213 -0
- package/src/wf-ui-server/__tests__/workflow-claude-materialize.test.mjs +209 -0
- package/src/wf-ui-server/__tests__/workflow-cli-registry-manuals.test.mjs +173 -0
- package/src/wf-ui-server/__tests__/workflow-cli-surface.test.mjs +176 -0
- package/src/wf-ui-server/__tests__/workflow-codex-resume-id.test.mjs +556 -0
- package/src/wf-ui-server/__tests__/workflow-cooperation-enforcement.test.mjs +553 -0
- package/src/wf-ui-server/__tests__/workflow-depth-resume-dock.test.mjs +846 -0
- package/src/wf-ui-server/__tests__/workflow-display-node.test.mjs +150 -0
- package/src/wf-ui-server/__tests__/workflow-edge-update.test.mjs +296 -0
- package/src/wf-ui-server/__tests__/workflow-file-cache-lock-watch.test.mjs +380 -0
- package/src/wf-ui-server/__tests__/workflow-file-formats.test.mjs +994 -0
- package/src/wf-ui-server/__tests__/workflow-file-node-actions.test.mjs +294 -0
- package/src/wf-ui-server/__tests__/workflow-goal-actions.test.mjs +357 -0
- package/src/wf-ui-server/__tests__/workflow-goal-task-autoclose.test.mjs +256 -0
- package/src/wf-ui-server/__tests__/workflow-graceful-stop.test.mjs +108 -0
- package/src/wf-ui-server/__tests__/workflow-graph-undo.test.mjs +524 -0
- package/src/wf-ui-server/__tests__/workflow-layout.test.mjs +732 -0
- package/src/wf-ui-server/__tests__/workflow-magnetic-topology.test.mjs +222 -0
- package/src/wf-ui-server/__tests__/workflow-markdown-lock.test.mjs +359 -0
- package/src/wf-ui-server/__tests__/workflow-mcp-hub.test.mjs +174 -0
- package/src/wf-ui-server/__tests__/workflow-node-manual-schema.test.mjs +241 -0
- package/src/wf-ui-server/__tests__/workflow-node-runtime.test.mjs +1991 -0
- package/src/wf-ui-server/__tests__/workflow-node-store-utils.test.mjs +125 -0
- package/src/wf-ui-server/__tests__/workflow-registry-drift.test.mjs +296 -0
- package/src/wf-ui-server/__tests__/workflow-resource-handle-four-side.test.mjs +217 -0
- package/src/wf-ui-server/__tests__/workflow-role-profile.test.mjs +234 -0
- package/src/wf-ui-server/__tests__/workflow-set-model.test.mjs +261 -0
- package/src/wf-ui-server/__tests__/workflow-skill-group-context-enabled.test.mjs +131 -0
- package/src/wf-ui-server/__tests__/workflow-skill-group-enable-disable.test.mjs +186 -0
- package/src/wf-ui-server/__tests__/workflow-skills-hub.test.mjs +125 -0
- package/src/wf-ui-server/__tests__/workflow-state-reconcile.test.mjs +139 -0
- package/src/wf-ui-server/__tests__/workflow-subagent-strategy-matrix.test.mjs +529 -0
- package/src/wf-ui-server/__tests__/workflow-team-flow.test.mjs +278 -0
- package/src/wf-ui-server/__tests__/workflow-terminal-control-live.test.mjs +294 -0
- package/src/wf-ui-server/__tests__/workflow-terminal-control.test.mjs +631 -0
- package/src/wf-ui-server/__tests__/workflow-timer-wakeup.test.mjs +318 -0
- package/src/wf-ui-server/__tests__/workflow-unified-resume.test.mjs +537 -0
- package/src/wf-ui-server/__tests__/workspace-api-hardening.test.mjs +187 -0
- package/src/wf-ui-server/__tests__/workspace-api.test.mjs +382 -0
- package/src/wf-ui-server/__tests__/workspace-reveal.test.mjs +138 -0
- package/src/wf-ui-server/__tests__/workspace-store-hardening.test.mjs +255 -0
- package/src/wf-ui-server/__tests__/workspace-store.test.mjs +208 -0
- package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +52 -25
- package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +12 -6
- package/src/wf-ui-server/__tests__/ws-wf-browser.integration.test.mjs +488 -0
- package/src/wf-ui-server/a2a-store.mjs +3007 -89
- package/src/wf-ui-server/action-registry.mjs +277 -0
- package/src/wf-ui-server/bridge-store.mjs +276 -0
- package/src/wf-ui-server/chat-driver.mjs +220 -0
- package/src/wf-ui-server/chat-store.mjs +109 -0
- package/src/wf-ui-server/claude-transcript-materializer.mjs +235 -0
- package/src/wf-ui-server/codex-update-prompt.mjs +53 -0
- package/src/wf-ui-server/component-node-store.mjs +689 -0
- package/src/wf-ui-server/composition-api.mjs +436 -0
- package/src/wf-ui-server/control-plane-token.mjs +17 -0
- package/src/wf-ui-server/dispatch-contract.mjs +150 -0
- package/src/wf-ui-server/drivers/claude-stream-json.mjs +371 -0
- package/src/wf-ui-server/drivers/codex-appserver.mjs +513 -0
- package/src/wf-ui-server/drivers/opencode-server.mjs +486 -0
- package/src/wf-ui-server/excalidraw-svg.mjs +141 -0
- package/src/wf-ui-server/file-format-adapters.mjs +511 -0
- package/src/wf-ui-server/file-watcher.mjs +215 -0
- package/src/wf-ui-server/graph-layout.mjs +778 -0
- package/src/wf-ui-server/harness-env.mjs +46 -0
- package/src/wf-ui-server/json-store-utils.mjs +48 -0
- package/src/wf-ui-server/model-capability.mjs +773 -0
- package/src/wf-ui-server/node-config-store.mjs +233 -0
- package/src/wf-ui-server/node-store-utils.mjs +88 -0
- package/src/wf-ui-server/peer-capsule.mjs +56 -1
- package/src/wf-ui-server/pty-adapter.mjs +513 -11
- package/src/wf-ui-server/reveal-foreground.mjs +69 -0
- package/src/wf-ui-server/runtime-detector.mjs +52 -6
- package/src/wf-ui-server/server.mjs +5613 -403
- package/src/wf-ui-server/session-cleanup.mjs +363 -0
- package/src/wf-ui-server/session-registry.mjs +179 -3
- package/src/wf-ui-server/settings.mjs +25 -0
- package/src/wf-ui-server/spawn-gate.mjs +46 -0
- package/src/wf-ui-server/task-parser.mjs +88 -3
- package/src/wf-ui-server/terminal-store.mjs +225 -11
- package/src/wf-ui-server/timer-wakeup-scheduler.mjs +226 -0
- package/src/wf-ui-server/wf-browser-store.mjs +733 -0
- package/src/wf-ui-server/workflow-agent-context.mjs +1731 -0
- package/src/wf-ui-server/workflow-capability-node-store.mjs +676 -0
- package/src/wf-ui-server/workflow-event-node-store.mjs +623 -0
- package/src/wf-ui-server/workflow-goal-node-store.mjs +328 -0
- package/src/wf-ui-server/workflow-graph-store.mjs +623 -0
- package/src/wf-ui-server/workflow-mcp-hub.mjs +286 -0
- package/src/wf-ui-server/workflow-node-runtime.mjs +1767 -0
- package/src/wf-ui-server/workflow-node-settings-store.mjs +339 -0
- package/src/wf-ui-server/workflow-node-types/agent-node.mjs +920 -0
- package/src/wf-ui-server/workflow-node-types/display-node.mjs +151 -0
- package/src/wf-ui-server/workflow-node-types/excalidraw-node.mjs +145 -0
- package/src/wf-ui-server/workflow-node-types/file-node.mjs +484 -0
- package/src/wf-ui-server/workflow-node-types/github-trigger-node.mjs +207 -0
- package/src/wf-ui-server/workflow-node-types/goal-node.mjs +491 -0
- package/src/wf-ui-server/workflow-node-types/markdown-node.mjs +225 -0
- package/src/wf-ui-server/workflow-node-types/mcp-connector-node.mjs +71 -0
- package/src/wf-ui-server/workflow-node-types/role-profile-store.mjs +191 -0
- package/src/wf-ui-server/workflow-node-types/skill-group-node.mjs +98 -0
- package/src/wf-ui-server/workflow-node-types/timer-node.mjs +607 -0
- package/src/wf-ui-server/workflow-ontology.mjs +681 -0
- package/src/wf-ui-server/workflow-operation-store.mjs +115 -0
- package/src/wf-ui-server/workflow-skills-hub.mjs +366 -0
- package/src/wf-ui-server/workflow-skills-market.mjs +723 -0
- package/src/wf-ui-server/workspace-store.mjs +951 -0
- package/src/wf-ui-server/ws-chat.mjs +299 -0
- package/src/wf-ui-server/ws-events.mjs +45 -20
- package/src/wf-ui-server/ws-terminal.mjs +165 -22
- package/src/wf-ui-server/ws-wf-browser.mjs +419 -0
- package/templates/common/.claude/agents/review-manager.md +33 -29
- package/templates/common/.claude/commands/wf-browser.md +1 -1
- package/templates/common/.claude/commands/wf-help.md +5 -3
- package/templates/common/.claude/commands/wf-init.md +44 -0
- package/templates/common/.claude/commands/wf-learn.md +34 -8
- package/templates/common/.claude/commands/wf-max.md +19 -9
- package/templates/common/.claude/commands/wf-review.md +3 -3
- package/templates/common/.claude/commands/wf-search.md +55 -0
- package/templates/common/.claude/commands/wf-task-archive.md +5 -4
- package/templates/common/.claude/commands/wf-task-list.md +6 -3
- package/templates/common/.claude/commands/wf-task-record.md +10 -2
- package/templates/common/.claude/commands/wf-ui.md +12 -5
- package/templates/common/.claude/commands/wf-update.md +4 -0
- package/templates/common/.claude/commands/wf.md +12 -1
- package/templates/common/.claude/rules/ecc/common.md +10 -1
- package/templates/common/.claude/skills/wf/SKILL.md +36 -0
- package/templates/common/.claude/skills/wf-browser/SKILL.md +427 -122
- package/templates/common/.claude/skills/wf-browser/references/BROWSER_CONTROL_PATTERNS.md +43 -0
- package/templates/common/.claude/skills/wf-init/SKILL.md +33 -0
- package/templates/common/.claude/skills/wf-learn/SKILL.md +106 -26
- package/templates/common/.claude/skills/wf-max/SKILL.md +45 -11
- package/templates/common/.claude/skills/wf-review/SKILL.md +26 -54
- package/templates/common/.claude/skills/wf-search/SKILL.md +122 -0
- package/templates/common/.claude/skills/wf-search/references/evidence-contract.md +77 -0
- package/templates/common/.claude/skills/wf-search/references/policies/compare.md +38 -0
- package/templates/common/.claude/skills/wf-search/references/policies/fact.md +39 -0
- package/templates/common/.claude/skills/wf-search/references/policies/troubleshoot.md +39 -0
- package/templates/common/.claude/skills/wf-search/references/policies/verify.md +44 -0
- package/templates/common/.claude/skills/wf-search/references/source-routing.md +40 -0
- package/templates/common/.claude/skills/wf-search/references/tool-adapters.md +38 -0
- package/templates/common/.claude/skills/wf-ui/SKILL.md +146 -12
- package/templates/common/.claude/skills/wf-update/SKILL.md +5 -1
- package/templates/common/.harness-version +188 -85
- package/templates/common/.opencode/agents/review-manager.md +31 -27
- package/templates/common/.opencode/commands/wf-browser.md +1 -1
- package/templates/common/.opencode/commands/wf-help.md +5 -3
- package/templates/common/.opencode/commands/wf-init.md +44 -0
- package/templates/common/.opencode/commands/wf-learn.md +34 -8
- package/templates/common/.opencode/commands/wf-max.md +19 -9
- package/templates/common/.opencode/commands/wf-review.md +3 -3
- package/templates/common/.opencode/commands/wf-search.md +55 -0
- package/templates/common/.opencode/commands/wf-task-archive.md +5 -7
- package/templates/common/.opencode/commands/wf-task-list.md +6 -6
- package/templates/common/.opencode/commands/wf-task-record.md +10 -5
- package/templates/common/.opencode/commands/wf-ui.md +12 -5
- package/templates/common/.opencode/commands/wf-update.md +4 -0
- package/templates/common/.opencode/commands/wf.md +12 -1
- package/templates/common/CLAUDE.md +61 -3
- package/templates/common/Harness/MEMORY.md +8 -5
- package/templates/common/Harness/README.md +14 -10
- package/templates/common/Harness/a2a/action-registry.json +4397 -0
- package/templates/common/Harness/a2a/role-graph.json +16 -3
- package/templates/common/Harness/a2a/skills/terminal-control.json +45 -1
- package/templates/common/Harness/a2a/skills/wf-ui-map.json +50 -0
- package/templates/common/Harness/a2a/skills/workflow-agent-node.json +143 -0
- package/templates/common/Harness/a2a/skills/workflow-context.json +95 -0
- package/templates/common/Harness/a2a/skills/workflow-diagram-node.json +60 -0
- package/templates/common/Harness/a2a/skills/workflow-display-node.json +69 -0
- package/templates/common/Harness/a2a/skills/workflow-file-node.json +92 -0
- package/templates/common/Harness/a2a/skills/workflow-goal-node.json +76 -0
- package/templates/common/Harness/a2a/skills/workflow-markdown-node.json +79 -0
- package/templates/common/Harness/a2a/skills/workflow-mcp-connector-node.json +59 -0
- package/templates/common/Harness/a2a/skills/workflow-node-actions.json +105 -0
- package/templates/common/Harness/a2a/skills/workflow-node-map.json +71 -0
- package/templates/common/Harness/a2a/skills/workflow-ontology.json +54 -0
- package/templates/common/Harness/a2a/skills/workflow-resource-node.json +68 -0
- package/templates/common/Harness/a2a/skills/workflow-skill-group-node.json +56 -0
- package/templates/common/Harness/a2a/skills/workflow-timer-node.json +81 -0
- package/templates/common/Harness/a2a/workflows.json +33 -0
- package/templates/common/Harness/ownership.manifest.json +263 -2
- package/templates/common/Harness/project/architecture.md +499 -0
- package/templates/common/Harness/research/README.md +20 -0
- package/templates/common/Harness/scripts/README.md +70 -6
- package/templates/common/Harness/scripts/a2a-terminal.mjs +12 -4
- package/templates/common/Harness/scripts/context-budget.mjs +7 -3
- package/templates/common/Harness/scripts/lib/memory-context.mjs +400 -0
- package/templates/common/Harness/scripts/lib/path-scope.mjs +77 -0
- package/templates/common/Harness/scripts/lib/research-policy.mjs +99 -0
- package/templates/common/Harness/scripts/lib/wf-search.mjs +189 -0
- package/templates/common/Harness/scripts/memory-context.mjs +63 -0
- package/templates/common/Harness/scripts/research-policy.mjs +27 -0
- package/templates/common/Harness/scripts/task-context.mjs +547 -0
- package/templates/common/Harness/scripts/task-group-index.mjs +242 -0
- package/templates/common/Harness/scripts/task-state.mjs +659 -95
- package/templates/common/Harness/scripts/validate-harness.mjs +70 -28
- package/templates/common/Harness/scripts/wf-auto-update-prompt.mjs +24 -0
- package/templates/common/Harness/scripts/wf-remove.mjs +10 -0
- package/templates/common/Harness/scripts/wf-search.mjs +10 -0
- package/templates/common/Harness/scripts/wf-ui-control.mjs +3144 -0
- package/templates/common/Harness/scripts/wf-update-check.mjs +274 -51
- package/templates/common/Harness/scripts/wf-update-runner.mjs +53 -16
- package/templates/common/Harness/settings.json +10 -0
- package/templates/common/Harness/specs/guides/SETUP.md +4 -3
- package/templates/common/Harness/specs/protocols/HARNESS_BRIDGE.md +429 -48
- package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +24 -0
- package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +14 -11
- package/templates/common/Harness/specs/runtime/command-surface.json +28 -0
- package/templates/common/Harness/specs/runtime/context-loading.md +6 -0
- package/templates/common/Harness/specs/runtime/dispatch.md +9 -1
- package/templates/common/Harness/specs/runtime/subagents.md +20 -5
- package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +19 -0
- package/templates/common/Harness/specs/workflows/WF-AUTO.md +21 -0
- package/templates/common/Harness/specs/workflows/WF-KERNEL.md +4 -2
- package/templates/common/Harness/specs/workflows/WF-MAX.md +39 -12
- package/templates/common/Harness/specs/workflows/WF-STATE.md +62 -14
- package/templates/common/Harness/specs/workflows/WF.md +32 -0
- package/templates/common/Harness/tasks/INDEX.json +7 -0
- package/templates/common/Harness/tasks/INDEX.md +12 -0
- package/templates/common/Harness/tasks/_template/PLAN.md +6 -0
- package/templates/common/Harness/tasks/_template/PROGRESS.md +5 -0
- package/templates/common/Harness/tasks/_template/STATE.json +22 -0
- package/templates/common/Harness/templates/TEST_PLAN.template.md +5 -4
- package/templates/common/Harness/templates/UI_CONTRACT.template.md +7 -5
- package/templates/common/Harness/wf-browser/README.md +69 -0
- package/templates/common/README.md +17 -0
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +0 -17
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +0 -1
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +0 -7
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +0 -1
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +0 -12
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +0 -1
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js +0 -50
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +0 -1
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +0 -74
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +0 -1
- package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +0 -1
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +0 -39
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +0 -1
- package/src/ui/dist/assets/index-BOVYWntB.js +0 -117
- package/src/ui/dist/assets/index-BOVYWntB.js.map +0 -1
- package/src/ui/dist/assets/index-DaUObq0H.css +0 -1
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +0 -7
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +0 -1
- package/src/ui/dist/assets/plus-QuTJyoT3.js +0 -7
- package/src/ui/dist/assets/plus-QuTJyoT3.js.map +0 -1
- package/src/ui/dist/assets/proxy-D0vUbds5.js.map +0 -1
- package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +0 -1
- package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +0 -1
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +0 -7
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +0 -1
- package/src/ui/dist/assets/x-DoIOGAJz.js.map +0 -1
- package/src/ui/pnpm-lock.yaml +0 -1582
- package/src/ui/src/App.tsx +0 -86
- package/src/ui/src/api.ts +0 -93
- package/src/ui/src/components/AgentsRoute.tsx +0 -503
- package/src/ui/src/components/Footer.tsx +0 -69
- package/src/ui/src/components/Header.tsx +0 -175
- package/src/ui/src/components/LoadingView.tsx +0 -22
- package/src/ui/src/components/RolesRoute.tsx +0 -169
- package/src/ui/src/components/SettingsRoute.tsx +0 -166
- package/src/ui/src/components/TaskList.tsx +0 -388
- package/src/ui/src/components/TerminalDrawer.tsx +0 -611
- package/src/ui/src/components/WorkflowRoute.tsx +0 -670
- package/src/ui/src/hooks/useReducedMotion.ts +0 -17
- package/src/ui/src/hooks/useServerConnection.ts +0 -114
- package/src/ui/src/index.css +0 -281
- package/src/ui/src/main.tsx +0 -11
- package/src/ui/src/types.ts +0 -108
- /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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
3031
|
+
const diskSessions = cachedListTerminalSessions(projectRoot);
|
|
224
3032
|
const bySessionId = new Map();
|
|
225
|
-
|
|
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 = [
|
|
243
|
-
const
|
|
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:
|
|
249
|
-
label: isWorkflowCeo ? `${session.runtime}
|
|
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
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
}
|