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
|
@@ -2,10 +2,17 @@ import test, { before, after } from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import http from 'node:http';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
-
import os from 'node:os';
|
|
6
5
|
import path from 'node:path';
|
|
7
|
-
import {
|
|
6
|
+
import { makeHarnessTempRoot } from '../../../tests/support/temp-root.js';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { resolveTaskWorkflowMode, startServer, stopServer, terminalReadyForInitialInput } from '../server.mjs';
|
|
8
9
|
import { SessionRegistry } from '../session-registry.mjs';
|
|
10
|
+
import { createComponentNode } from '../component-node-store.mjs';
|
|
11
|
+
import { appendTerminalData, listTerminalSessions, persistSession } from '../terminal-store.mjs';
|
|
12
|
+
import { writeWorkflowGraphMap } from '../a2a-store.mjs';
|
|
13
|
+
import { registerPtyProcess, unregisterPtyProcess } from '../ws-terminal.mjs';
|
|
14
|
+
import { CODEX_UPDATE_SKIP_SESSION_KEYS, CODEX_UPDATE_SKIP_UNTIL_NEXT_VERSION_KEYS } from '../codex-update-prompt.mjs';
|
|
15
|
+
import { graphReadToken } from '../control-plane-token.mjs';
|
|
9
16
|
|
|
10
17
|
function fetchJson(baseUrl, token, route) {
|
|
11
18
|
return new Promise((resolve, reject) => {
|
|
@@ -37,7 +44,7 @@ function authGet(baseUrl, token, route) {
|
|
|
37
44
|
});
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
function postJson(baseUrl, token, route, payload = {}) {
|
|
47
|
+
function postJson(baseUrl, token, route, payload = {}, extraHeaders = {}) {
|
|
41
48
|
const url = new URL(route, baseUrl);
|
|
42
49
|
const body = JSON.stringify(payload);
|
|
43
50
|
return new Promise((resolve, reject) => {
|
|
@@ -50,6 +57,7 @@ function postJson(baseUrl, token, route, payload = {}) {
|
|
|
50
57
|
Authorization: `Bearer ${token}`,
|
|
51
58
|
'Content-Type': 'application/json',
|
|
52
59
|
'Content-Length': Buffer.byteLength(body),
|
|
60
|
+
...extraHeaders,
|
|
53
61
|
},
|
|
54
62
|
}, (res) => {
|
|
55
63
|
let responseBody = '';
|
|
@@ -64,10 +72,93 @@ function postJson(baseUrl, token, route, payload = {}) {
|
|
|
64
72
|
});
|
|
65
73
|
}
|
|
66
74
|
|
|
75
|
+
function deleteJson(baseUrl, token, route) {
|
|
76
|
+
const url = new URL(route, baseUrl);
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const req = http.request({
|
|
79
|
+
hostname: url.hostname,
|
|
80
|
+
port: url.port,
|
|
81
|
+
path: url.pathname + url.search,
|
|
82
|
+
method: 'DELETE',
|
|
83
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
84
|
+
}, (res) => {
|
|
85
|
+
let responseBody = '';
|
|
86
|
+
res.on('data', c => responseBody += c);
|
|
87
|
+
res.on('end', () => {
|
|
88
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(responseBody) }); }
|
|
89
|
+
catch { resolve({ status: res.statusCode, body: responseBody }); }
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
req.on('error', reject);
|
|
93
|
+
req.end();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readJsonlFile(filePath) {
|
|
98
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
99
|
+
.split(/\r?\n/)
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.map(line => JSON.parse(line));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function wait(ms) {
|
|
105
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function waitFor(fn, { timeout = 5000, step = 25 } = {}) {
|
|
109
|
+
const deadline = Date.now() + timeout;
|
|
110
|
+
let last;
|
|
111
|
+
while (Date.now() < deadline) {
|
|
112
|
+
last = await fn();
|
|
113
|
+
if (last) return last;
|
|
114
|
+
await new Promise(resolve => setTimeout(resolve, step));
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`waitFor timed out; last=${JSON.stringify(last)}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function removeHarnessTempRoot(root) {
|
|
120
|
+
const resolved = path.resolve(root);
|
|
121
|
+
const tempRoot = path.resolve('Harness', '.temp') + path.sep;
|
|
122
|
+
assert.ok(resolved.startsWith(tempRoot), `refusing to remove non-Harness temp root: ${root}`);
|
|
123
|
+
try {
|
|
124
|
+
fs.rmSync(resolved, { recursive: true, force: true, maxRetries: 25, retryDelay: 200 });
|
|
125
|
+
} catch (e) {
|
|
126
|
+
if (process.platform === 'win32' && ['EPERM', 'EBUSY', 'ENOTEMPTY'].includes(e?.code)) return;
|
|
127
|
+
throw e;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function runNode(args, { cwd, env, timeout = 5000 } = {}) {
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
const child = spawn(process.execPath, args, {
|
|
134
|
+
cwd,
|
|
135
|
+
env,
|
|
136
|
+
windowsHide: true,
|
|
137
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
138
|
+
});
|
|
139
|
+
let stdout = '';
|
|
140
|
+
let stderr = '';
|
|
141
|
+
const timer = setTimeout(() => {
|
|
142
|
+
child.kill();
|
|
143
|
+
reject(new Error(`Command timed out: ${args.join(' ')}`));
|
|
144
|
+
}, timeout);
|
|
145
|
+
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
146
|
+
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
147
|
+
child.on('error', (err) => {
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
reject(err);
|
|
150
|
+
});
|
|
151
|
+
child.on('close', (code, signal) => {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
resolve({ status: code, signal, stdout, stderr });
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
67
158
|
let server, baseUrl, token, tempRoot;
|
|
68
159
|
|
|
69
160
|
before(async () => {
|
|
70
|
-
tempRoot =
|
|
161
|
+
tempRoot = makeHarnessTempRoot('wf-ui-test-');
|
|
71
162
|
const tasksDir = path.join(tempRoot, 'Harness', 'tasks');
|
|
72
163
|
fs.mkdirSync(tasksDir, { recursive: true });
|
|
73
164
|
const ta = path.join(tasksDir, 'task-alpha');
|
|
@@ -92,7 +183,7 @@ before(async () => {
|
|
|
92
183
|
|
|
93
184
|
const hd = path.join(tempRoot, 'Harness');
|
|
94
185
|
fs.writeFileSync(path.join(hd, 'settings.json'), JSON.stringify({ ui: { language: 'zh' } }));
|
|
95
|
-
fs.writeFileSync(path.join(hd, '.harness-version'), JSON.stringify({ version: '0.8.
|
|
186
|
+
fs.writeFileSync(path.join(hd, '.harness-version'), JSON.stringify({ version: '0.8.20' }));
|
|
96
187
|
|
|
97
188
|
const r = await startServer({ projectRoot: tempRoot, host: '127.0.0.1', port: 0 });
|
|
98
189
|
server = r.server; token = r.token; baseUrl = `http://127.0.0.1:${r.port}`;
|
|
@@ -107,21 +198,317 @@ test('binds to 127.0.0.1', () => { const a = server.address(); assert.equal(a.ad
|
|
|
107
198
|
test('port > 0 with port=0', () => { assert.ok(server.address().port > 0); });
|
|
108
199
|
test('health returns 200', async () => { const { status, body } = await fetchJson(baseUrl, token, '/api/health'); assert.equal(status, 200); assert.equal(body.status, 'ok'); });
|
|
109
200
|
test('health auth via Bearer', async () => { const { status, body } = await authGet(baseUrl, token, '/api/health'); assert.equal(status, 200); assert.equal(body.status, 'ok'); });
|
|
110
|
-
test('no token
|
|
111
|
-
test('bad token
|
|
112
|
-
test('
|
|
201
|
+
test('no token can access local API', async () => { const { status, body } = await fetchJson(baseUrl, undefined, '/api/tasks'); assert.equal(status, 200); assert.ok(Array.isArray(body)); });
|
|
202
|
+
test('bad token is ignored for local API compatibility', async () => { const { status, body } = await fetchJson(baseUrl, 'bad-token-1234567890abcdef', '/api/tasks'); assert.equal(status, 200); assert.ok(Array.isArray(body)); });
|
|
203
|
+
test('durable WF task binding is explicit and managed task sessions inherit their mode', () => {
|
|
204
|
+
assert.equal(resolveTaskWorkflowMode({ taskId: 'task-wf', mode: 'wf' }, ''), 'wf');
|
|
205
|
+
assert.equal(resolveTaskWorkflowMode({ taskId: 'task-direct', mode: 'direct' }, 'wf-auto'), 'wf-auto');
|
|
206
|
+
assert.throws(
|
|
207
|
+
() => resolveTaskWorkflowMode({ taskId: 'task-direct', mode: 'direct' }, 'wf', { taskId: 'task-direct' }),
|
|
208
|
+
/cannot be bound to durable WF mode/,
|
|
209
|
+
);
|
|
210
|
+
assert.throws(
|
|
211
|
+
() => resolveTaskWorkflowMode({ taskId: 'task-wf', mode: 'wf' }, 'wf-max', { taskId: 'task-wf' }),
|
|
212
|
+
/requires workflow mode "wf"/,
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
test('AC-004 project view returns root, task count, and durable WF resume summary', async () => {
|
|
216
|
+
const { status, body } = await fetchJson(baseUrl, token, '/api/project');
|
|
217
|
+
assert.equal(status, 200);
|
|
218
|
+
assert.equal(body.root, tempRoot);
|
|
219
|
+
assert.equal(body.version, '0.8.20');
|
|
220
|
+
assert.equal(body.activeTaskId, null);
|
|
221
|
+
assert.equal(body.wfManaged, false);
|
|
222
|
+
assert.equal(body.resumeRequired, false);
|
|
223
|
+
assert.ok(Array.isArray(body.projects));
|
|
224
|
+
});
|
|
113
225
|
test('tasks returns array sorted desc', async () => { const { status, body } = await fetchJson(baseUrl, token, '/api/tasks'); assert.equal(status, 200); assert.ok(body.length >= 2); assert.equal(body[0].taskId, 'task-alpha'); });
|
|
114
226
|
test('tasks/:id returns single', async () => { const { status, body } = await fetchJson(baseUrl, token, '/api/tasks/task-beta'); assert.equal(status, 200); assert.equal(body.taskId, 'task-beta'); });
|
|
115
227
|
test('tasks/:id missing -> 404', async () => { const { status } = await fetchJson(baseUrl, token, '/api/tasks/task-nonexist'); assert.equal(status, 404); });
|
|
116
228
|
test('tasks with invalid chars -> 400', async () => { const { status } = await fetchJson(baseUrl, token, '/api/tasks/task%00inject'); assert.equal(status, 400); });
|
|
117
229
|
test('settings merged', async () => { const { status, body } = await fetchJson(baseUrl, token, '/api/settings'); assert.equal(status, 200); assert.equal(body.server.host, '127.0.0.1'); assert.equal(body.ui.language, 'zh'); });
|
|
230
|
+
test('settings POST preserves non-UI project settings while updating cleanup', async () => {
|
|
231
|
+
const root = makeHarnessTempRoot('wf-ui-settings-save-test-');
|
|
232
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
233
|
+
fs.writeFileSync(path.join(root, 'Harness', 'settings.json'), JSON.stringify({
|
|
234
|
+
schema: 'harness-settings@1',
|
|
235
|
+
workflow: { defaultTier: 'auto', taskCapsuleCap: 5 },
|
|
236
|
+
cleanup: { stoppedSessionRetentionDays: 14, detachedLogRetentionHours: 24 },
|
|
237
|
+
}, null, 2));
|
|
238
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0 });
|
|
239
|
+
try {
|
|
240
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
241
|
+
const result = await postJson(localBaseUrl, started.token, '/api/settings', {
|
|
242
|
+
cleanup: { stoppedSessionRetentionDays: 3 },
|
|
243
|
+
});
|
|
244
|
+
assert.equal(result.status, 200);
|
|
245
|
+
const saved = JSON.parse(fs.readFileSync(path.join(root, 'Harness', 'settings.json'), 'utf8'));
|
|
246
|
+
assert.equal(saved.schema, 'harness-settings@1');
|
|
247
|
+
assert.equal(saved.workflow.taskCapsuleCap, 5);
|
|
248
|
+
assert.equal(saved.cleanup.stoppedSessionRetentionDays, 3);
|
|
249
|
+
assert.equal(saved.cleanup.detachedLogRetentionHours, 24);
|
|
250
|
+
} finally {
|
|
251
|
+
await stopServer(started.server);
|
|
252
|
+
fs.rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
118
255
|
test('sessions empty when no registry', async () => { const { status, body } = await fetchJson(baseUrl, token, '/api/sessions'); assert.equal(status, 200); assert.ok(Array.isArray(body)); assert.equal(body.length, 0); });
|
|
119
|
-
test('error envelope format', async () => { const { status, body } = await fetchJson(baseUrl, undefined, '/api/
|
|
256
|
+
test('error envelope format', async () => { const { status, body } = await fetchJson(baseUrl, undefined, '/api/nope'); assert.equal(status, 404); assert.ok(body.error.code); assert.ok(body.error.message); });
|
|
120
257
|
test('content-type json', async () => { const { status, headers } = await fetchJson(baseUrl, token, '/api/health'); assert.equal(status, 200); assert.match(headers['content-type'], /application\/json/); });
|
|
121
258
|
test('unknown route 404', async () => { const { status } = await fetchJson(baseUrl, token, '/api/nope'); assert.equal(status, 404); });
|
|
122
259
|
|
|
260
|
+
test('AC-002 /api/a2a/snapshot running status matches /api/sessions and downgrades stale disk sessions', async () => {
|
|
261
|
+
const root = makeHarnessTempRoot('wf-ui-live-source-test-');
|
|
262
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
263
|
+
persistSession(root, {
|
|
264
|
+
taskId: null,
|
|
265
|
+
sessionId: 'session-stale-running',
|
|
266
|
+
runtime: 'claude',
|
|
267
|
+
role: 'Subagent',
|
|
268
|
+
status: 'running',
|
|
269
|
+
wsClientCount: 1,
|
|
270
|
+
updatedAt: '2026-07-30T00:00:00.000Z',
|
|
271
|
+
});
|
|
272
|
+
writeWorkflowGraphMap(root, {
|
|
273
|
+
version: 9,
|
|
274
|
+
nodes: [{ nodeId: 'session-session-stale-running', sessionId: 'session-stale-running', runtime: 'claude', status: 'running' }],
|
|
275
|
+
positions: { 'session-session-stale-running': { x: 320, y: 180 } },
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const registry = new SessionRegistry();
|
|
279
|
+
const liveSession = registry.create({
|
|
280
|
+
runtime: 'codex',
|
|
281
|
+
role: 'Main Agent',
|
|
282
|
+
agentKind: 'main',
|
|
283
|
+
});
|
|
284
|
+
registry.update(liveSession.sessionId, { status: 'running' });
|
|
285
|
+
|
|
286
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry, eventsWs: false });
|
|
287
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
288
|
+
try {
|
|
289
|
+
const sessions = await fetchJson(localBaseUrl, started.token, '/api/sessions');
|
|
290
|
+
const allSessions = await fetchJson(localBaseUrl, started.token, '/api/sessions?all=1');
|
|
291
|
+
const snapshot = await fetchJson(localBaseUrl, started.token, '/api/a2a/snapshot');
|
|
292
|
+
|
|
293
|
+
assert.equal(sessions.status, 200);
|
|
294
|
+
assert.equal(allSessions.status, 200);
|
|
295
|
+
assert.equal(snapshot.status, 200);
|
|
296
|
+
|
|
297
|
+
const liveRunningIds = sessions.body
|
|
298
|
+
.filter(session => session.status === 'running')
|
|
299
|
+
.map(session => session.sessionId)
|
|
300
|
+
.sort();
|
|
301
|
+
const live = sessions.body.find(session => session.sessionId === liveSession.sessionId);
|
|
302
|
+
assert.ok(live.resourceUsage, 'live sessions should expose PTY resource details');
|
|
303
|
+
assert.equal(live.resourceUsage.pid, liveSession.pid || null);
|
|
304
|
+
assert.equal(live.resourceUsage.wsClientCount, live.wsClientCount || 0);
|
|
305
|
+
assert.ok(Object.hasOwn(live.resourceUsage, 'memoryBytes'));
|
|
306
|
+
assert.ok(Object.hasOwn(live.resourceUsage, 'ptyProvider'));
|
|
307
|
+
assert.ok(Object.hasOwn(live.resourceUsage, 'cpuPercent'));
|
|
308
|
+
const snapshotRunningIds = snapshot.body.nodes
|
|
309
|
+
.filter(node => node.status === 'running')
|
|
310
|
+
.map(node => node.sessionId)
|
|
311
|
+
.sort();
|
|
312
|
+
assert.deepEqual(snapshotRunningIds, liveRunningIds);
|
|
313
|
+
|
|
314
|
+
const staleNode = snapshot.body.nodes.find(node => node.sessionId === 'session-stale-running');
|
|
315
|
+
assert.ok(staleNode, 'stale project graph session should remain visible as stopped history');
|
|
316
|
+
assert.equal(staleNode.status, 'stopped');
|
|
317
|
+
assert.equal(staleNode.blockedReason, 'not-managed-by-current-wf-ui');
|
|
318
|
+
assert.deepEqual(snapshot.body.graph.positions['session-session-stale-running'], { x: 320, y: 180 });
|
|
319
|
+
const staleSession = allSessions.body.find(session => session.sessionId === 'session-stale-running');
|
|
320
|
+
assert.equal(staleSession.status, 'stopped', 'orphaned live disk sessions are downgraded once at backend startup (P1b)');
|
|
321
|
+
assert.equal(staleSession.blockedReason, 'not-managed-by-current-wf-ui');
|
|
322
|
+
assert.equal(staleSession.attachMode, false, 'orphaned disk sessions are watch-only until reattached');
|
|
323
|
+
assert.equal(staleSession.wsClientCount, 0);
|
|
324
|
+
assert.equal(staleSession.resourceUsage.cpuPercent, null);
|
|
325
|
+
} finally {
|
|
326
|
+
await stopServer(started.server);
|
|
327
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('AC-002b snapshot incremental polling: ?since= fingerprint returns unchanged until state moves', async () => {
|
|
332
|
+
const root = makeHarnessTempRoot('wf-ui-since-test-');
|
|
333
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
334
|
+
const registry = new SessionRegistry();
|
|
335
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry, eventsWs: false });
|
|
336
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
337
|
+
try {
|
|
338
|
+
const first = await fetchJson(localBaseUrl, started.token, '/api/a2a/snapshot');
|
|
339
|
+
assert.equal(first.status, 200);
|
|
340
|
+
assert.ok(first.body.fingerprint, 'snapshot carries a fingerprint');
|
|
341
|
+
|
|
342
|
+
const unchanged = await fetchJson(localBaseUrl, started.token, `/api/a2a/snapshot?since=${encodeURIComponent(first.body.fingerprint)}`);
|
|
343
|
+
assert.equal(unchanged.status, 200);
|
|
344
|
+
assert.equal(unchanged.body.unchanged, true);
|
|
345
|
+
assert.equal(unchanged.body.fingerprint, first.body.fingerprint);
|
|
346
|
+
|
|
347
|
+
persistSession(root, { taskId: null, sessionId: 'session-since-move', runtime: 'codex', role: 'Subagent', status: 'stopped' });
|
|
348
|
+
const changed = await fetchJson(localBaseUrl, started.token, `/api/a2a/snapshot?since=${encodeURIComponent(first.body.fingerprint)}`);
|
|
349
|
+
assert.equal(changed.status, 200);
|
|
350
|
+
assert.notEqual(changed.body.unchanged, true, 'state change must bust the fingerprint');
|
|
351
|
+
assert.notEqual(changed.body.fingerprint, first.body.fingerprint);
|
|
352
|
+
} finally {
|
|
353
|
+
await stopServer(started.server);
|
|
354
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('AC-002c display node html serve route: default template until written, then agent html', async () => {
|
|
359
|
+
const root = makeHarnessTempRoot('wf-ui-display-serve-');
|
|
360
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
361
|
+
const registry = new SessionRegistry();
|
|
362
|
+
const created = createComponentNode(root, { type: 'display', title: 'Research Report' });
|
|
363
|
+
const nodeId = created.node.nodeId;
|
|
364
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry, eventsWs: false });
|
|
365
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
366
|
+
try {
|
|
367
|
+
const empty = await fetchJson(localBaseUrl, started.token, `/api/a2a/component-nodes/${nodeId}/html`);
|
|
368
|
+
assert.equal(empty.status, 200);
|
|
369
|
+
assert.ok(String(empty.body).includes('<!DOCTYPE html>'), 'serves the default template before any write');
|
|
370
|
+
assert.ok(String(empty.body).includes('#D97757'), 'theme accent present');
|
|
371
|
+
assert.ok(String(empty.body).includes('Research Report'), 'title in the template');
|
|
372
|
+
} finally {
|
|
373
|
+
await stopServer(started.server);
|
|
374
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test('AC-008 workflow agent sessions write node-home init files and use quiet bootstrap metadata', async () => {
|
|
379
|
+
const root = makeHarnessTempRoot('wf-ui-node-home-test-');
|
|
380
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
381
|
+
const registry = new SessionRegistry();
|
|
382
|
+
try {
|
|
383
|
+
const session = registry.create({
|
|
384
|
+
runtime: 'claude',
|
|
385
|
+
agentKind: 'main',
|
|
386
|
+
role: 'Main Agent',
|
|
387
|
+
objective: 'Quiet bootstrap check',
|
|
388
|
+
projectRoot: root,
|
|
389
|
+
cwd: root,
|
|
390
|
+
});
|
|
391
|
+
const nodeHomeRel = `Harness/a2a/nodes/${session.sessionId}`;
|
|
392
|
+
registry.update(session.sessionId, {
|
|
393
|
+
nodeHomeRel,
|
|
394
|
+
nodeHomePath: path.join(root, nodeHomeRel),
|
|
395
|
+
nodeInitRel: `${nodeHomeRel}/init.md`,
|
|
396
|
+
nodeInitPath: path.join(root, nodeHomeRel, 'init.md'),
|
|
397
|
+
});
|
|
398
|
+
const current = registry.get(session.sessionId);
|
|
399
|
+
persistSession(root, current);
|
|
400
|
+
|
|
401
|
+
fs.mkdirSync(current.nodeHomePath, { recursive: true });
|
|
402
|
+
fs.writeFileSync(current.nodeInitPath, [
|
|
403
|
+
'# Harness WF Node Init',
|
|
404
|
+
'',
|
|
405
|
+
'- Keep terminal startup quiet. Do not print this file unless the operator asks.',
|
|
406
|
+
'- Read the workflow graph with `node Harness/scripts/wf-ui-control.mjs describe --project .` when you need topology.',
|
|
407
|
+
].join('\n'));
|
|
408
|
+
|
|
409
|
+
assert.equal(fs.existsSync(current.nodeInitPath), true);
|
|
410
|
+
const init = fs.readFileSync(current.nodeInitPath, 'utf8');
|
|
411
|
+
assert.match(init, /Keep terminal startup quiet/);
|
|
412
|
+
assert.match(init, /wf-ui-control\.mjs describe/);
|
|
413
|
+
} finally {
|
|
414
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test('AC-004 session input API writes directly to the registered PTY process', async () => {
|
|
419
|
+
const root = makeHarnessTempRoot('wf-ui-direct-pty-input-test-');
|
|
420
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
421
|
+
const registry = new SessionRegistry();
|
|
422
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
|
|
423
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
424
|
+
const written = [];
|
|
425
|
+
try {
|
|
426
|
+
const session = registry.create({ runtime: 'codex', agentKind: 'main' });
|
|
427
|
+
registry.update(session.sessionId, { status: 'running' });
|
|
428
|
+
registerPtyProcess(session.sessionId, { write: data => written.push(data) });
|
|
429
|
+
|
|
430
|
+
const result = await postJson(localBaseUrl, started.token, `/api/sessions/${session.sessionId}/input`, {
|
|
431
|
+
input: 'hello from attached terminal\r',
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
assert.equal(result.status, 200);
|
|
435
|
+
assert.equal(result.body.ok, true);
|
|
436
|
+
assert.deepEqual(written, ['hello from attached terminal\r']);
|
|
437
|
+
} finally {
|
|
438
|
+
for (const session of registry.getAll()) unregisterPtyProcess(session.sessionId);
|
|
439
|
+
await stopServer(started.server);
|
|
440
|
+
removeHarnessTempRoot(root);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test('AC-003 terminal file drop API stores uploaded files below the session drop directory', async () => {
|
|
445
|
+
const session = persistSession(tempRoot, {
|
|
446
|
+
sessionId: 'session-drop-test',
|
|
447
|
+
runtime: 'codex',
|
|
448
|
+
status: 'running',
|
|
449
|
+
attachMode: true,
|
|
450
|
+
taskId: null,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const { status, body } = await postJson(baseUrl, token, `/api/sessions/${session.sessionId}/drop-files`, {
|
|
454
|
+
files: [
|
|
455
|
+
{
|
|
456
|
+
name: '..\\unsafe name.txt',
|
|
457
|
+
contentBase64: Buffer.from('drop-file-body', 'utf8').toString('base64'),
|
|
458
|
+
},
|
|
459
|
+
],
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
assert.equal(status, 200);
|
|
463
|
+
assert.equal(body.files.length, 1);
|
|
464
|
+
const dropped = body.files[0];
|
|
465
|
+
const dropRoot = path.join(tempRoot, 'Harness', 'a2a', 'sessions', session.sessionId, 'drops');
|
|
466
|
+
assert.ok(dropped.path.startsWith(dropRoot), `drop path should stay under ${dropRoot}`);
|
|
467
|
+
assert.doesNotMatch(path.relative(dropRoot, dropped.path), /^\.\./, 'drop path must not traverse out of drop root');
|
|
468
|
+
assert.equal(fs.readFileSync(dropped.path, 'utf8'), 'drop-file-body');
|
|
469
|
+
assert.match(dropped.name, /unsafe name\.txt$/, 'basename should be preserved after path stripping');
|
|
470
|
+
assert.match(dropped.terminalText, /^'.*'$/, 'terminal text should be safely quoted for paste/input');
|
|
471
|
+
assert.equal(body.terminalInput, dropped.terminalText);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test('wf-bridge records cross-session input after PTY write succeeds', async () => {
|
|
475
|
+
const root = makeHarnessTempRoot('wf-ui-bridge-input-test-');
|
|
476
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
477
|
+
const registry = new SessionRegistry();
|
|
478
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
|
|
479
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
480
|
+
const written = [];
|
|
481
|
+
try {
|
|
482
|
+
const from = registry.create({ runtime: 'claude', agentKind: 'main' });
|
|
483
|
+
const to = registry.create({ runtime: 'codex', agentKind: 'main' });
|
|
484
|
+
registry.update(from.sessionId, { status: 'running', graphNodeId: `session-${from.sessionId}` });
|
|
485
|
+
registry.update(to.sessionId, { status: 'running', graphNodeId: `session-${to.sessionId}` });
|
|
486
|
+
registerPtyProcess(to.sessionId, { write: data => written.push(data) });
|
|
487
|
+
|
|
488
|
+
const result = await postJson(localBaseUrl, started.token, `/api/sessions/${to.sessionId}/input`, {
|
|
489
|
+
data: 'reply over bridge\r',
|
|
490
|
+
fromSessionId: from.sessionId,
|
|
491
|
+
fromNodeId: `session-${from.sessionId}`,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
assert.equal(result.status, 200);
|
|
495
|
+
assert.deepEqual(written, ['reply over bridge\r']);
|
|
496
|
+
assert.equal(result.body.bridgeMessage.fromSessionId, from.sessionId);
|
|
497
|
+
assert.equal(result.body.bridgeMessage.toSessionId, to.sessionId);
|
|
498
|
+
|
|
499
|
+
const messages = await fetchJson(localBaseUrl, started.token, `/api/a2a/bridge-messages?fromSessionId=${from.sessionId}&toSessionId=${to.sessionId}&limit=20`);
|
|
500
|
+
assert.equal(messages.status, 200);
|
|
501
|
+
assert.equal(messages.body.entries.length, 1);
|
|
502
|
+
assert.equal(messages.body.entries[0].data, 'reply over bridge\r');
|
|
503
|
+
} finally {
|
|
504
|
+
for (const session of registry.getAll()) unregisterPtyProcess(session.sessionId);
|
|
505
|
+
await stopServer(started.server);
|
|
506
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
123
510
|
test('session stop is idempotent and removes session from live list', async () => {
|
|
124
|
-
const root =
|
|
511
|
+
const root = makeHarnessTempRoot('wf-ui-stop-test-');
|
|
125
512
|
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
126
513
|
const registry = new SessionRegistry();
|
|
127
514
|
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
|
|
@@ -133,7 +520,8 @@ test('session stop is idempotent and removes session from live list', async () =
|
|
|
133
520
|
const first = await postJson(localBaseUrl, started.token, `/api/sessions/${session.sessionId}/stop`);
|
|
134
521
|
assert.equal(first.status, 200);
|
|
135
522
|
assert.equal(first.body.ok, true);
|
|
136
|
-
assert.equal(first.body.
|
|
523
|
+
assert.equal(first.body.stopped.status, 'stopped');
|
|
524
|
+
assert.equal(first.body.saved.status, 'stopped');
|
|
137
525
|
|
|
138
526
|
const live = await fetchJson(localBaseUrl, started.token, '/api/sessions');
|
|
139
527
|
assert.equal(live.status, 200);
|
|
@@ -148,3 +536,1067 @@ test('session stop is idempotent and removes session from live list', async () =
|
|
|
148
536
|
fs.rmSync(root, { recursive: true, force: true });
|
|
149
537
|
}
|
|
150
538
|
});
|
|
539
|
+
|
|
540
|
+
test('AC-003 stop updates workflow snapshot control and persisted graph state', async () => {
|
|
541
|
+
const root = makeHarnessTempRoot('wf-ui-stop-snapshot-test-');
|
|
542
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
543
|
+
const registry = new SessionRegistry();
|
|
544
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
|
|
545
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
546
|
+
try {
|
|
547
|
+
const session = registry.create({ runtime: 'codex', agentKind: 'main', role: 'Main Agent' });
|
|
548
|
+
registry.update(session.sessionId, { status: 'running', graphNodeId: `session-${session.sessionId}` });
|
|
549
|
+
persistSession(root, registry.get(session.sessionId));
|
|
550
|
+
writeWorkflowGraphMap(root, {
|
|
551
|
+
nodes: [{ nodeId: `session-${session.sessionId}`, sessionId: session.sessionId, runtime: 'codex', status: 'running' }],
|
|
552
|
+
positions: { [`session-${session.sessionId}`]: { x: 20, y: 40 } },
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
const first = await postJson(localBaseUrl, started.token, `/api/sessions/${session.sessionId}/stop`);
|
|
556
|
+
assert.equal(first.status, 200);
|
|
557
|
+
assert.equal(first.body.stopped.status, 'stopped');
|
|
558
|
+
assert.equal(first.body.saved.status, 'stopped');
|
|
559
|
+
|
|
560
|
+
const live = await fetchJson(localBaseUrl, started.token, '/api/sessions');
|
|
561
|
+
const snapshot = await fetchJson(localBaseUrl, started.token, '/api/a2a/snapshot?fresh=1');
|
|
562
|
+
const graphMap = await fetchJson(localBaseUrl, started.token, '/api/a2a/graph-map');
|
|
563
|
+
const node = snapshot.body.nodes.find(item => item.sessionId === session.sessionId);
|
|
564
|
+
|
|
565
|
+
assert.equal(live.status, 200);
|
|
566
|
+
assert.deepEqual(live.body, []);
|
|
567
|
+
assert.ok(node, 'stopped graph-bound node should remain visible as stopped transcript history');
|
|
568
|
+
assert.equal(node.status, 'stopped');
|
|
569
|
+
assert.equal(node.lifecycle, 'stopped');
|
|
570
|
+
assert.equal(node.control.canStart, true);
|
|
571
|
+
assert.equal(node.control.canStop, false);
|
|
572
|
+
assert.equal(node.control.canDelete, true);
|
|
573
|
+
assert.equal(graphMap.body.nodes.find(item => item.sessionId === session.sessionId)?.status, 'stopped');
|
|
574
|
+
} finally {
|
|
575
|
+
await stopServer(started.server);
|
|
576
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test('AC-004 DELETE /api/a2a/nodes removes stopped not-managed graph node but preserves transcript history', async () => {
|
|
581
|
+
const root = makeHarnessTempRoot('wf-ui-delete-node-test-');
|
|
582
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
583
|
+
persistSession(root, {
|
|
584
|
+
taskId: null,
|
|
585
|
+
sessionId: 'session-delete-me',
|
|
586
|
+
runtime: 'claude',
|
|
587
|
+
role: 'Subagent',
|
|
588
|
+
status: 'running',
|
|
589
|
+
terminalSeq: 0,
|
|
590
|
+
updatedAt: '2026-07-30T00:00:00.000Z',
|
|
591
|
+
});
|
|
592
|
+
persistSession(root, {
|
|
593
|
+
taskId: null,
|
|
594
|
+
sessionId: 'session-keep-me',
|
|
595
|
+
runtime: 'codex',
|
|
596
|
+
role: 'Main Agent',
|
|
597
|
+
status: 'saved',
|
|
598
|
+
updatedAt: '2026-07-30T00:00:01.000Z',
|
|
599
|
+
});
|
|
600
|
+
const deleteState = listTerminalSessions(root).find(session => session.sessionId === 'session-delete-me');
|
|
601
|
+
appendTerminalData(root, deleteState, 'saved transcript line\r\n');
|
|
602
|
+
writeWorkflowGraphMap(root, {
|
|
603
|
+
nodes: [
|
|
604
|
+
{ nodeId: 'session-session-delete-me', sessionId: 'session-delete-me', runtime: 'claude', status: 'running' },
|
|
605
|
+
{ nodeId: 'session-session-keep-me', sessionId: 'session-keep-me', runtime: 'codex', status: 'saved' },
|
|
606
|
+
],
|
|
607
|
+
edges: [
|
|
608
|
+
{ id: 'edge-delete', from: 'session-session-keep-me', to: 'session-session-delete-me', relation: 'can-communicate' },
|
|
609
|
+
],
|
|
610
|
+
positions: {
|
|
611
|
+
'session-session-delete-me': { x: 120, y: 80 },
|
|
612
|
+
'session-session-keep-me': { x: 20, y: 30 },
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const registry = new SessionRegistry();
|
|
617
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
|
|
618
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
619
|
+
try {
|
|
620
|
+
const before = await fetchJson(localBaseUrl, started.token, '/api/a2a/snapshot?fresh=1');
|
|
621
|
+
assert.equal(before.status, 200);
|
|
622
|
+
assert.equal(before.body.nodes.find(node => node.sessionId === 'session-delete-me')?.blockedReason, 'not-managed-by-current-wf-ui');
|
|
623
|
+
|
|
624
|
+
const removed = await deleteJson(localBaseUrl, started.token, '/api/a2a/nodes/session-session-delete-me');
|
|
625
|
+
assert.equal(removed.status, 200);
|
|
626
|
+
assert.equal(removed.body.ok, true);
|
|
627
|
+
assert.equal(removed.body.removed.nodeId, 'session-session-delete-me');
|
|
628
|
+
|
|
629
|
+
const after = await fetchJson(localBaseUrl, started.token, '/api/a2a/snapshot?fresh=1');
|
|
630
|
+
const graphMap = await fetchJson(localBaseUrl, started.token, '/api/a2a/graph-map');
|
|
631
|
+
const transcript = await fetchJson(localBaseUrl, started.token, '/api/terminals/session-delete-me/range?tail=10');
|
|
632
|
+
|
|
633
|
+
assert.equal(after.body.nodes.some(node => node.sessionId === 'session-delete-me'), false);
|
|
634
|
+
assert.equal(after.body.nodes.some(node => node.sessionId === 'session-keep-me'), true);
|
|
635
|
+
assert.equal(after.body.graph.edges.some(edge => edge.toSessionId === 'session-delete-me'), false);
|
|
636
|
+
assert.equal(graphMap.body.nodes.some(node => node.sessionId === 'session-delete-me'), false);
|
|
637
|
+
assert.equal(graphMap.body.positions['session-session-delete-me'], undefined);
|
|
638
|
+
assert.ok(graphMap.body.deletedNodes.some(node => node.sessionId === 'session-delete-me'));
|
|
639
|
+
assert.equal(transcript.body.entries.some(entry => String(entry.data).includes('saved transcript line')), true);
|
|
640
|
+
} finally {
|
|
641
|
+
await stopServer(started.server);
|
|
642
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test('AC-004 DELETE /api/a2a/nodes removes non-live registry sessions from snapshot', async () => {
|
|
647
|
+
const root = makeHarnessTempRoot('wf-ui-delete-registry-saved-test-');
|
|
648
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
649
|
+
const registry = new SessionRegistry();
|
|
650
|
+
const session = registry.create({
|
|
651
|
+
runtime: 'opencode',
|
|
652
|
+
agentKind: 'subagent',
|
|
653
|
+
graphNodeId: 'session-registry-saved',
|
|
654
|
+
projectRoot: root,
|
|
655
|
+
cwd: root,
|
|
656
|
+
});
|
|
657
|
+
registry.update(session.sessionId, { status: 'saved' });
|
|
658
|
+
persistSession(root, registry.get(session.sessionId));
|
|
659
|
+
writeWorkflowGraphMap(root, {
|
|
660
|
+
nodes: [
|
|
661
|
+
{ nodeId: 'session-registry-saved', sessionId: session.sessionId, runtime: 'opencode', status: 'saved' },
|
|
662
|
+
],
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
|
|
666
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
667
|
+
try {
|
|
668
|
+
const removed = await deleteJson(localBaseUrl, started.token, '/api/a2a/nodes/session-registry-saved');
|
|
669
|
+
assert.equal(removed.status, 200);
|
|
670
|
+
assert.equal(registry.get(session.sessionId), undefined);
|
|
671
|
+
|
|
672
|
+
const after = await fetchJson(localBaseUrl, started.token, '/api/a2a/snapshot?fresh=1');
|
|
673
|
+
assert.equal(after.status, 200);
|
|
674
|
+
assert.equal(after.body.nodes.some(node => node.sessionId === session.sessionId), false);
|
|
675
|
+
} finally {
|
|
676
|
+
await stopServer(started.server);
|
|
677
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
test('AC-001 wf-ui-control describe uses read-only snapshot token instead of stale local graph fallback', async () => {
|
|
682
|
+
const root = makeHarnessTempRoot('wf-ui-read-token-test-');
|
|
683
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
684
|
+
writeWorkflowGraphMap(root, {
|
|
685
|
+
nodes: [{ nodeId: 'session-local-only', sessionId: 'local-only', runtime: 'claude', status: 'saved' }],
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const registry = new SessionRegistry();
|
|
689
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
|
|
690
|
+
try {
|
|
691
|
+
const sessionId = 'session-reader';
|
|
692
|
+
const result = await runNode([
|
|
693
|
+
path.join(process.cwd(), 'Harness', 'scripts', 'wf-ui-control.mjs'),
|
|
694
|
+
'describe',
|
|
695
|
+
'--project',
|
|
696
|
+
root,
|
|
697
|
+
], {
|
|
698
|
+
cwd: root,
|
|
699
|
+
timeout: 5000,
|
|
700
|
+
env: {
|
|
701
|
+
...process.env,
|
|
702
|
+
HARNESS_PEER_SESSION_ID: sessionId,
|
|
703
|
+
HARNESS_AGENT_KIND: 'subagent',
|
|
704
|
+
HARNESS_WF_UI_URL: `http://127.0.0.1:${started.port}`,
|
|
705
|
+
HARNESS_WF_UI_READ_TOKEN: graphReadToken(started.token, sessionId),
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
709
|
+
const body = JSON.parse(result.stdout);
|
|
710
|
+
assert.equal(body.counts.nodes, 0);
|
|
711
|
+
assert.equal(body.nodes.some(node => node.sessionId === 'local-only'), false);
|
|
712
|
+
} finally {
|
|
713
|
+
await stopServer(started.server);
|
|
714
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
test('W21-ONTOLOGY exposes workflow ontology through full and graph read tokens', async () => {
|
|
719
|
+
const root = makeHarnessTempRoot('wf-ui-ontology-api-test-');
|
|
720
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
721
|
+
const sessionId = 'ontology-reader';
|
|
722
|
+
writeWorkflowGraphMap(root, {
|
|
723
|
+
nodes: [{ nodeId: 'session-ontology-reader', sessionId, runtime: 'codex', agentKind: 'subagent', status: 'running' }],
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: new SessionRegistry() });
|
|
727
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
728
|
+
try {
|
|
729
|
+
const full = await fetchJson(localBaseUrl, started.token, '/api/workflow/ontology');
|
|
730
|
+
assert.equal(full.status, 200, JSON.stringify(full.body));
|
|
731
|
+
assert.equal(full.body.ok, true);
|
|
732
|
+
assert.equal(full.body.ontology.ontologyId, 'harness.workflow.ontology');
|
|
733
|
+
assert.equal(full.body.ontology.nodeTypes.timer.controlRelation.relation, 'control');
|
|
734
|
+
|
|
735
|
+
const readOnly = await fetchJson(
|
|
736
|
+
localBaseUrl,
|
|
737
|
+
graphReadToken(started.token, sessionId),
|
|
738
|
+
`/api/workflow/ontology?actorSessionId=${sessionId}`,
|
|
739
|
+
);
|
|
740
|
+
assert.equal(readOnly.status, 200, JSON.stringify(readOnly.body));
|
|
741
|
+
assert.equal(readOnly.body.ontology.nodeTypes.markdown.direction, 'bidirectional');
|
|
742
|
+
|
|
743
|
+
const script = path.join(process.cwd(), 'Harness', 'scripts', 'wf-ui-control.mjs');
|
|
744
|
+
const cli = await runNode([
|
|
745
|
+
script,
|
|
746
|
+
'workflow-ontology',
|
|
747
|
+
'--project',
|
|
748
|
+
root,
|
|
749
|
+
], {
|
|
750
|
+
cwd: root,
|
|
751
|
+
timeout: 5000,
|
|
752
|
+
env: {
|
|
753
|
+
...process.env,
|
|
754
|
+
HARNESS_PEER_SESSION_ID: sessionId,
|
|
755
|
+
HARNESS_WORKFLOW_NODE_ID: 'session-ontology-reader',
|
|
756
|
+
HARNESS_WF_UI_URL: localBaseUrl,
|
|
757
|
+
HARNESS_WF_UI_TOKEN: '',
|
|
758
|
+
HARNESS_WF_UI_READ_TOKEN: graphReadToken(started.token, sessionId),
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
assert.equal(cli.status, 0, cli.stderr || cli.stdout);
|
|
762
|
+
const cliBody = JSON.parse(cli.stdout);
|
|
763
|
+
assert.equal(cliBody.ontology.ontologyId, 'harness.workflow.ontology');
|
|
764
|
+
assert.equal(cliBody.ontology.nodeTypes.goal.direction, 'bidirectional');
|
|
765
|
+
|
|
766
|
+
const badToken = await fetchJson(localBaseUrl, 'not-a-token', '/api/workflow/ontology');
|
|
767
|
+
assert.equal(badToken.status, 200);
|
|
768
|
+
assert.equal(badToken.body.ontology.ontologyId, 'harness.workflow.ontology');
|
|
769
|
+
} finally {
|
|
770
|
+
await stopServer(started.server);
|
|
771
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test('W21-AUTH validates server-derived Agent actor session for timer control actions', async () => {
|
|
776
|
+
const root = makeHarnessTempRoot('wf-ui-action-auth-test-');
|
|
777
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
778
|
+
const agentNodeId = 'session-w21-auth-agent';
|
|
779
|
+
const otherAgentNodeId = 'session-w21-auth-other';
|
|
780
|
+
writeWorkflowGraphMap(root, {
|
|
781
|
+
nodes: [
|
|
782
|
+
{ nodeId: agentNodeId, sessionId: 'w21-auth-agent-pty', runtime: 'codex', agentKind: 'main', status: 'running' },
|
|
783
|
+
{ nodeId: otherAgentNodeId, sessionId: 'w21-auth-other-pty', runtime: 'codex', agentKind: 'subagent', status: 'running' },
|
|
784
|
+
],
|
|
785
|
+
positions: {
|
|
786
|
+
[agentNodeId]: { x: 20, y: 40 },
|
|
787
|
+
[otherAgentNodeId]: { x: 260, y: 40 },
|
|
788
|
+
},
|
|
789
|
+
});
|
|
790
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: new SessionRegistry() });
|
|
791
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
792
|
+
try {
|
|
793
|
+
const created = await postJson(localBaseUrl, started.token, '/api/workflow/nodes', {
|
|
794
|
+
type: 'timer',
|
|
795
|
+
title: 'W21 Auth Timer',
|
|
796
|
+
enabled: true,
|
|
797
|
+
schedule: { mode: 'interval', intervalSeconds: 60 },
|
|
798
|
+
controlPolicy: { agentCanSetInterval: true, minIntervalSeconds: 10, maxIntervalSeconds: 3600 },
|
|
799
|
+
});
|
|
800
|
+
assert.equal(created.status, 201, JSON.stringify(created.body));
|
|
801
|
+
const timerNodeId = created.body.node.nodeId;
|
|
802
|
+
|
|
803
|
+
const edge = await postJson(localBaseUrl, started.token, '/api/workflow/edges', {
|
|
804
|
+
from: agentNodeId,
|
|
805
|
+
to: timerNodeId,
|
|
806
|
+
relation: 'control',
|
|
807
|
+
direction: 'source-to-target',
|
|
808
|
+
sourceHandle: 'control',
|
|
809
|
+
targetHandle: 'config',
|
|
810
|
+
});
|
|
811
|
+
assert.equal(edge.status, 201, JSON.stringify(edge.body));
|
|
812
|
+
|
|
813
|
+
const spoofed = await postJson(
|
|
814
|
+
localBaseUrl,
|
|
815
|
+
started.token,
|
|
816
|
+
`/api/workflow/nodes/${timerNodeId}/actions/timer.setInterval`,
|
|
817
|
+
{ actorNodeId: agentNodeId, intervalSeconds: 120 },
|
|
818
|
+
{ 'X-Harness-Session-Id': 'w21-auth-other-pty' },
|
|
819
|
+
);
|
|
820
|
+
assert.equal(spoofed.status, 403, JSON.stringify(spoofed.body));
|
|
821
|
+
assert.equal(spoofed.body.error?.code, 'AGENT_ACTOR_MISMATCH');
|
|
822
|
+
|
|
823
|
+
const derived = await postJson(
|
|
824
|
+
localBaseUrl,
|
|
825
|
+
started.token,
|
|
826
|
+
`/api/workflow/nodes/${timerNodeId}/actions/timer.setInterval`,
|
|
827
|
+
{ intervalSeconds: 180 },
|
|
828
|
+
{ 'X-Harness-Session-Id': 'w21-auth-agent-pty' },
|
|
829
|
+
);
|
|
830
|
+
assert.equal(derived.status, 200, JSON.stringify(derived.body));
|
|
831
|
+
assert.equal(derived.body.result.state.schedule.intervalSeconds, 180);
|
|
832
|
+
} finally {
|
|
833
|
+
await stopServer(started.server);
|
|
834
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
test('W19-NODE-MAP-SKILL wf-ui-control node-map deleteNodes uses typed Agent graph API and skips live agents', async () => {
|
|
839
|
+
const root = makeHarnessTempRoot('wf-ui-node-map-api-test-');
|
|
840
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
841
|
+
const mainAgentId = 'session-w19-main-agent';
|
|
842
|
+
const workerAgentId = 'session-w19-live-worker';
|
|
843
|
+
writeWorkflowGraphMap(root, {
|
|
844
|
+
nodes: [
|
|
845
|
+
{ nodeId: mainAgentId, sessionId: 'w19-main-agent', runtime: 'codex', agentKind: 'main', role: 'Main Agent', status: 'running' },
|
|
846
|
+
{ nodeId: workerAgentId, sessionId: 'w19-live-worker', runtime: 'codex', agentKind: 'subagent', role: 'Subagent', status: 'running' },
|
|
847
|
+
],
|
|
848
|
+
positions: {
|
|
849
|
+
[mainAgentId]: { x: 20, y: 40 },
|
|
850
|
+
[workerAgentId]: { x: 260, y: 40 },
|
|
851
|
+
},
|
|
852
|
+
});
|
|
853
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: new SessionRegistry() });
|
|
854
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
855
|
+
try {
|
|
856
|
+
const created = await postJson(localBaseUrl, started.token, '/api/workflow/nodes', {
|
|
857
|
+
type: 'markdown',
|
|
858
|
+
title: 'W19 API Controlled Note',
|
|
859
|
+
});
|
|
860
|
+
assert.equal(created.status, 201, JSON.stringify(created.body));
|
|
861
|
+
const targetNodeId = created.body.node.nodeId;
|
|
862
|
+
|
|
863
|
+
const script = path.join(process.cwd(), 'Harness', 'scripts', 'wf-ui-control.mjs');
|
|
864
|
+
const read = await runNode([
|
|
865
|
+
script,
|
|
866
|
+
'node-map',
|
|
867
|
+
'--action',
|
|
868
|
+
'readGraph',
|
|
869
|
+
'--project',
|
|
870
|
+
root,
|
|
871
|
+
], {
|
|
872
|
+
cwd: root,
|
|
873
|
+
timeout: 5000,
|
|
874
|
+
env: {
|
|
875
|
+
...process.env,
|
|
876
|
+
HARNESS_AGENT_KIND: 'main',
|
|
877
|
+
HARNESS_PEER_SESSION_ID: 'w19-main-agent',
|
|
878
|
+
HARNESS_WORKFLOW_NODE_ID: mainAgentId,
|
|
879
|
+
HARNESS_WF_UI_URL: localBaseUrl,
|
|
880
|
+
HARNESS_WF_UI_TOKEN: started.token,
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
assert.equal(read.status, 0, read.stderr || read.stdout);
|
|
884
|
+
const readBody = JSON.parse(read.stdout);
|
|
885
|
+
assert.equal(readBody.action, 'agent.readGraph');
|
|
886
|
+
assert.ok(readBody.result?.graph?.nodes?.some(node => node.nodeId === targetNodeId), JSON.stringify(readBody));
|
|
887
|
+
|
|
888
|
+
const result = await runNode([
|
|
889
|
+
script,
|
|
890
|
+
'node-map',
|
|
891
|
+
'--action',
|
|
892
|
+
'deleteNodes',
|
|
893
|
+
'--all',
|
|
894
|
+
'true',
|
|
895
|
+
'--project',
|
|
896
|
+
root,
|
|
897
|
+
], {
|
|
898
|
+
cwd: root,
|
|
899
|
+
timeout: 5000,
|
|
900
|
+
env: {
|
|
901
|
+
...process.env,
|
|
902
|
+
HARNESS_AGENT_KIND: 'main',
|
|
903
|
+
HARNESS_PEER_SESSION_ID: 'w19-main-agent',
|
|
904
|
+
HARNESS_WORKFLOW_NODE_ID: mainAgentId,
|
|
905
|
+
HARNESS_WF_UI_URL: localBaseUrl,
|
|
906
|
+
HARNESS_WF_UI_TOKEN: started.token,
|
|
907
|
+
},
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
911
|
+
const body = JSON.parse(result.stdout);
|
|
912
|
+
assert.equal(body.action, 'agent.deleteNodes');
|
|
913
|
+
assert.equal(body.result?.operation?.kind, 'agent.deleteNodes');
|
|
914
|
+
assert.ok(body.result?.deletedNodeIds?.includes(targetNodeId), JSON.stringify(body));
|
|
915
|
+
assert.ok(body.result?.skippedNodeIds?.includes(mainAgentId), JSON.stringify(body));
|
|
916
|
+
assert.ok(body.result?.skippedNodeIds?.includes(workerAgentId), JSON.stringify(body));
|
|
917
|
+
|
|
918
|
+
const nodes = await fetchJson(localBaseUrl, started.token, '/api/workflow/nodes');
|
|
919
|
+
assert.equal(nodes.status, 200);
|
|
920
|
+
assert.equal(nodes.body.nodes.some(node => node.nodeId === targetNodeId), false);
|
|
921
|
+
assert.equal(nodes.body.nodes.some(node => node.nodeId === mainAgentId), true);
|
|
922
|
+
assert.equal(nodes.body.nodes.some(node => node.nodeId === workerAgentId), true);
|
|
923
|
+
|
|
924
|
+
const workflow = await fetchJson(localBaseUrl, started.token, '/api/workflow');
|
|
925
|
+
assert.equal(workflow.status, 200, JSON.stringify(workflow.body));
|
|
926
|
+
assert.ok(workflow.body.workflow?.operations?.recent?.some(record => record.kind === 'agent.deleteNodes'));
|
|
927
|
+
} finally {
|
|
928
|
+
await stopServer(started.server);
|
|
929
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
test('W19-NODE-MAP-SKILL wf-ui-control legacy connect and delete-node delegate to typed Agent graph API', async () => {
|
|
934
|
+
const root = makeHarnessTempRoot('wf-ui-node-map-legacy-test-');
|
|
935
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
936
|
+
const mainAgentId = 'session-w19-legacy-main';
|
|
937
|
+
writeWorkflowGraphMap(root, {
|
|
938
|
+
nodes: [
|
|
939
|
+
{ nodeId: mainAgentId, sessionId: 'w19-legacy-main', runtime: 'codex', agentKind: 'main', role: 'Main Agent', status: 'running' },
|
|
940
|
+
],
|
|
941
|
+
positions: {
|
|
942
|
+
[mainAgentId]: { x: 20, y: 40 },
|
|
943
|
+
},
|
|
944
|
+
});
|
|
945
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: new SessionRegistry() });
|
|
946
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
947
|
+
const env = {
|
|
948
|
+
...process.env,
|
|
949
|
+
HARNESS_AGENT_KIND: 'main',
|
|
950
|
+
HARNESS_PEER_SESSION_ID: 'w19-legacy-main',
|
|
951
|
+
HARNESS_WORKFLOW_NODE_ID: mainAgentId,
|
|
952
|
+
HARNESS_WF_UI_URL: localBaseUrl,
|
|
953
|
+
HARNESS_WF_UI_TOKEN: started.token,
|
|
954
|
+
};
|
|
955
|
+
try {
|
|
956
|
+
const created = await postJson(localBaseUrl, started.token, '/api/workflow/nodes', {
|
|
957
|
+
type: 'markdown',
|
|
958
|
+
title: 'W19 Legacy Alias Note',
|
|
959
|
+
});
|
|
960
|
+
assert.equal(created.status, 201, JSON.stringify(created.body));
|
|
961
|
+
const targetNodeId = created.body.node.nodeId;
|
|
962
|
+
const script = path.join(process.cwd(), 'Harness', 'scripts', 'wf-ui-control.mjs');
|
|
963
|
+
|
|
964
|
+
const connected = await runNode([
|
|
965
|
+
script,
|
|
966
|
+
'connect',
|
|
967
|
+
'--from',
|
|
968
|
+
mainAgentId,
|
|
969
|
+
'--to',
|
|
970
|
+
targetNodeId,
|
|
971
|
+
'--relation',
|
|
972
|
+
'context',
|
|
973
|
+
'--project',
|
|
974
|
+
root,
|
|
975
|
+
], { cwd: root, timeout: 5000, env });
|
|
976
|
+
assert.equal(connected.status, 0, connected.stderr || connected.stdout);
|
|
977
|
+
const connectBody = JSON.parse(connected.stdout);
|
|
978
|
+
assert.equal(connectBody.action, 'agent.connectNodes');
|
|
979
|
+
assert.equal(connectBody.result?.operation?.kind, 'agent.connectNodes');
|
|
980
|
+
|
|
981
|
+
const removed = await runNode([
|
|
982
|
+
script,
|
|
983
|
+
'delete-node',
|
|
984
|
+
'--node',
|
|
985
|
+
targetNodeId,
|
|
986
|
+
'--project',
|
|
987
|
+
root,
|
|
988
|
+
], { cwd: root, timeout: 5000, env });
|
|
989
|
+
assert.equal(removed.status, 0, removed.stderr || removed.stdout);
|
|
990
|
+
const removeBody = JSON.parse(removed.stdout);
|
|
991
|
+
assert.equal(removeBody.action, 'agent.deleteNode');
|
|
992
|
+
assert.equal(removeBody.result?.operation?.kind, 'agent.deleteNode');
|
|
993
|
+
assert.deepEqual(removeBody.result?.deletedNodeIds, [targetNodeId]);
|
|
994
|
+
} finally {
|
|
995
|
+
await stopServer(started.server);
|
|
996
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
test('W20-WORKFLOW-CLI wf-ui-control exposes canonical workflow context and typed node action wrappers', async () => {
|
|
1001
|
+
const root = makeHarnessTempRoot('wf-ui-workflow-cli-test-');
|
|
1002
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
1003
|
+
const mainAgentId = 'session-w20-main-agent';
|
|
1004
|
+
writeWorkflowGraphMap(root, {
|
|
1005
|
+
nodes: [
|
|
1006
|
+
{ nodeId: mainAgentId, sessionId: 'w20-main-agent', runtime: 'codex', agentKind: 'main', role: 'Main Agent', status: 'running' },
|
|
1007
|
+
],
|
|
1008
|
+
positions: {
|
|
1009
|
+
[mainAgentId]: { x: 20, y: 40 },
|
|
1010
|
+
},
|
|
1011
|
+
});
|
|
1012
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: new SessionRegistry() });
|
|
1013
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
1014
|
+
const env = {
|
|
1015
|
+
...process.env,
|
|
1016
|
+
HARNESS_AGENT_KIND: 'main',
|
|
1017
|
+
HARNESS_PEER_SESSION_ID: 'w20-main-agent',
|
|
1018
|
+
HARNESS_WORKFLOW_NODE_ID: mainAgentId,
|
|
1019
|
+
HARNESS_WF_UI_URL: localBaseUrl,
|
|
1020
|
+
HARNESS_WF_UI_TOKEN: started.token,
|
|
1021
|
+
};
|
|
1022
|
+
try {
|
|
1023
|
+
const created = await postJson(localBaseUrl, started.token, '/api/workflow/nodes', {
|
|
1024
|
+
type: 'markdown',
|
|
1025
|
+
title: 'W20 Canonical Note',
|
|
1026
|
+
markdown: 'W20 typed action body',
|
|
1027
|
+
});
|
|
1028
|
+
assert.equal(created.status, 201, JSON.stringify(created.body));
|
|
1029
|
+
const targetNodeId = created.body.node.nodeId;
|
|
1030
|
+
|
|
1031
|
+
const connected = await postJson(localBaseUrl, started.token, `/api/workflow/nodes/${mainAgentId}/actions/agent.connectNodes`, {
|
|
1032
|
+
actorNodeId: mainAgentId,
|
|
1033
|
+
from: mainAgentId,
|
|
1034
|
+
to: targetNodeId,
|
|
1035
|
+
relation: 'context',
|
|
1036
|
+
sourceHandle: 'context',
|
|
1037
|
+
targetHandle: 'markdown',
|
|
1038
|
+
});
|
|
1039
|
+
assert.equal(connected.status, 200, JSON.stringify(connected.body));
|
|
1040
|
+
|
|
1041
|
+
const script = path.join(process.cwd(), 'Harness', 'scripts', 'wf-ui-control.mjs');
|
|
1042
|
+
const context = await runNode([
|
|
1043
|
+
script,
|
|
1044
|
+
'workflow-context',
|
|
1045
|
+
'--node',
|
|
1046
|
+
mainAgentId,
|
|
1047
|
+
'--project',
|
|
1048
|
+
root,
|
|
1049
|
+
], { cwd: root, timeout: 5000, env });
|
|
1050
|
+
assert.equal(context.status, 0, context.stderr || context.stdout);
|
|
1051
|
+
const contextBody = JSON.parse(context.stdout);
|
|
1052
|
+
assert.equal(contextBody.context.nodeId, mainAgentId);
|
|
1053
|
+
assert.ok(contextBody.context.connectedResourceRefs.some(ref => ref.nodeId === targetNodeId && ref.type === 'markdown'), JSON.stringify(contextBody));
|
|
1054
|
+
|
|
1055
|
+
const readOnlyContext = await runNode([
|
|
1056
|
+
script,
|
|
1057
|
+
'workflow-context',
|
|
1058
|
+
'--node',
|
|
1059
|
+
mainAgentId,
|
|
1060
|
+
'--project',
|
|
1061
|
+
root,
|
|
1062
|
+
], {
|
|
1063
|
+
cwd: root,
|
|
1064
|
+
timeout: 5000,
|
|
1065
|
+
env: {
|
|
1066
|
+
...process.env,
|
|
1067
|
+
HARNESS_AGENT_KIND: 'subagent',
|
|
1068
|
+
HARNESS_PEER_SESSION_ID: 'w20-main-agent',
|
|
1069
|
+
HARNESS_WORKFLOW_NODE_ID: mainAgentId,
|
|
1070
|
+
HARNESS_WF_UI_URL: localBaseUrl,
|
|
1071
|
+
HARNESS_WF_UI_TOKEN: '',
|
|
1072
|
+
HARNESS_WF_UI_READ_TOKEN: graphReadToken(started.token, 'w20-main-agent'),
|
|
1073
|
+
},
|
|
1074
|
+
});
|
|
1075
|
+
assert.equal(readOnlyContext.status, 0, readOnlyContext.stderr || readOnlyContext.stdout);
|
|
1076
|
+
const readOnlyContextBody = JSON.parse(readOnlyContext.stdout);
|
|
1077
|
+
assert.equal(readOnlyContextBody.context.nodeId, mainAgentId);
|
|
1078
|
+
|
|
1079
|
+
const action = await runNode([
|
|
1080
|
+
script,
|
|
1081
|
+
'workflow-node-action',
|
|
1082
|
+
'--node',
|
|
1083
|
+
targetNodeId,
|
|
1084
|
+
'--action',
|
|
1085
|
+
'markdown.read',
|
|
1086
|
+
'--project',
|
|
1087
|
+
root,
|
|
1088
|
+
], { cwd: root, timeout: 5000, env });
|
|
1089
|
+
assert.equal(action.status, 0, action.stderr || action.stdout);
|
|
1090
|
+
const actionBody = JSON.parse(action.stdout);
|
|
1091
|
+
assert.equal(actionBody.action, 'markdown.read');
|
|
1092
|
+
assert.equal(actionBody.result.markdown, 'W20 typed action body');
|
|
1093
|
+
|
|
1094
|
+
const graph = await runNode([
|
|
1095
|
+
script,
|
|
1096
|
+
'workflow-node-map',
|
|
1097
|
+
'--action',
|
|
1098
|
+
'readGraph',
|
|
1099
|
+
'--project',
|
|
1100
|
+
root,
|
|
1101
|
+
], { cwd: root, timeout: 5000, env });
|
|
1102
|
+
assert.equal(graph.status, 0, graph.stderr || graph.stdout);
|
|
1103
|
+
const graphBody = JSON.parse(graph.stdout);
|
|
1104
|
+
assert.equal(graphBody.action, 'agent.readGraph');
|
|
1105
|
+
assert.ok(graphBody.result?.graph?.nodes?.some(node => node.nodeId === targetNodeId), JSON.stringify(graphBody));
|
|
1106
|
+
} finally {
|
|
1107
|
+
await stopServer(started.server);
|
|
1108
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
test('W27-CLI-GATE wf-ui-control resolves main actor from backend without actor-kind and read-node returns canonical snapshots', async () => {
|
|
1113
|
+
const root = makeHarnessTempRoot('wf-ui-cli-gate-test-');
|
|
1114
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
1115
|
+
const mainAgentId = 'session-w27-main-agent';
|
|
1116
|
+
const subagentId = 'session-w27-subagent';
|
|
1117
|
+
writeWorkflowGraphMap(root, {
|
|
1118
|
+
nodes: [
|
|
1119
|
+
{ nodeId: mainAgentId, sessionId: 'w27-main-agent', runtime: 'codex', agentKind: 'main', role: 'Main Agent', status: 'running' },
|
|
1120
|
+
{ nodeId: subagentId, sessionId: 'w27-subagent', runtime: 'codex', agentKind: 'subagent', role: 'Subagent', status: 'running' },
|
|
1121
|
+
],
|
|
1122
|
+
positions: {
|
|
1123
|
+
[mainAgentId]: { x: 20, y: 40 },
|
|
1124
|
+
[subagentId]: { x: 260, y: 40 },
|
|
1125
|
+
},
|
|
1126
|
+
});
|
|
1127
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: new SessionRegistry() });
|
|
1128
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
1129
|
+
const script = path.join(process.cwd(), 'Harness', 'scripts', 'wf-ui-control.mjs');
|
|
1130
|
+
const env = {
|
|
1131
|
+
...process.env,
|
|
1132
|
+
HARNESS_PEER_SESSION_ID: 'w27-main-agent',
|
|
1133
|
+
HARNESS_WORKFLOW_NODE_ID: mainAgentId,
|
|
1134
|
+
HARNESS_WF_UI_URL: localBaseUrl,
|
|
1135
|
+
HARNESS_WF_UI_TOKEN: started.token,
|
|
1136
|
+
};
|
|
1137
|
+
delete env.HARNESS_AGENT_KIND;
|
|
1138
|
+
try {
|
|
1139
|
+
const created = await postJson(localBaseUrl, started.token, '/api/workflow/nodes', {
|
|
1140
|
+
type: 'markdown',
|
|
1141
|
+
title: 'W27 Gate Note',
|
|
1142
|
+
markdown: 'W27 read-node markdown body',
|
|
1143
|
+
});
|
|
1144
|
+
assert.equal(created.status, 201, JSON.stringify(created.body));
|
|
1145
|
+
const markdownNodeId = created.body.node.nodeId;
|
|
1146
|
+
|
|
1147
|
+
const timer = await postJson(localBaseUrl, started.token, '/api/workflow/nodes', {
|
|
1148
|
+
type: 'timer',
|
|
1149
|
+
title: 'W27 Gate Timer',
|
|
1150
|
+
enabled: true,
|
|
1151
|
+
schedule: { mode: 'interval', intervalSeconds: 60 },
|
|
1152
|
+
controlPolicy: { agentCanSetInterval: true, minIntervalSeconds: 10, maxIntervalSeconds: 3600 },
|
|
1153
|
+
});
|
|
1154
|
+
assert.equal(timer.status, 201, JSON.stringify(timer.body));
|
|
1155
|
+
const timerNodeId = timer.body.node.nodeId;
|
|
1156
|
+
|
|
1157
|
+
const connected = await runNode([
|
|
1158
|
+
script,
|
|
1159
|
+
'connect',
|
|
1160
|
+
'--from',
|
|
1161
|
+
mainAgentId,
|
|
1162
|
+
'--to',
|
|
1163
|
+
markdownNodeId,
|
|
1164
|
+
'--relation',
|
|
1165
|
+
'context',
|
|
1166
|
+
'--project',
|
|
1167
|
+
root,
|
|
1168
|
+
], { cwd: root, timeout: 5000, env });
|
|
1169
|
+
assert.equal(connected.status, 0, connected.stderr || connected.stdout);
|
|
1170
|
+
const connectBody = JSON.parse(connected.stdout);
|
|
1171
|
+
assert.equal(connectBody.action, 'agent.connectNodes');
|
|
1172
|
+
assert.equal(connectBody.result?.operation?.kind, 'agent.connectNodes');
|
|
1173
|
+
|
|
1174
|
+
const eventRead = await runNode([
|
|
1175
|
+
script,
|
|
1176
|
+
'read-node',
|
|
1177
|
+
'--node',
|
|
1178
|
+
timerNodeId,
|
|
1179
|
+
'--project',
|
|
1180
|
+
root,
|
|
1181
|
+
], { cwd: root, timeout: 5000, env });
|
|
1182
|
+
assert.equal(eventRead.status, 0, eventRead.stderr || eventRead.stdout);
|
|
1183
|
+
const eventBody = JSON.parse(eventRead.stdout);
|
|
1184
|
+
assert.equal(eventBody.ok, true);
|
|
1185
|
+
assert.equal(eventBody.node.nodeId, timerNodeId);
|
|
1186
|
+
assert.equal(eventBody.node.kind, 'timer');
|
|
1187
|
+
assert.equal(eventBody.node.lifecycle, 'event-source');
|
|
1188
|
+
|
|
1189
|
+
const componentRead = await runNode([
|
|
1190
|
+
script,
|
|
1191
|
+
'readnode',
|
|
1192
|
+
'--node',
|
|
1193
|
+
markdownNodeId,
|
|
1194
|
+
'--project',
|
|
1195
|
+
root,
|
|
1196
|
+
], { cwd: root, timeout: 5000, env });
|
|
1197
|
+
assert.equal(componentRead.status, 0, componentRead.stderr || componentRead.stdout);
|
|
1198
|
+
const componentBody = JSON.parse(componentRead.stdout);
|
|
1199
|
+
assert.equal(componentBody.node.nodeId, markdownNodeId);
|
|
1200
|
+
assert.equal(componentBody.node.kind, 'markdown');
|
|
1201
|
+
|
|
1202
|
+
const agentRead = await runNode([
|
|
1203
|
+
script,
|
|
1204
|
+
'read-snapshot',
|
|
1205
|
+
'--node',
|
|
1206
|
+
'w27-main-agent',
|
|
1207
|
+
'--project',
|
|
1208
|
+
root,
|
|
1209
|
+
], { cwd: root, timeout: 5000, env });
|
|
1210
|
+
assert.equal(agentRead.status, 0, agentRead.stderr || agentRead.stdout);
|
|
1211
|
+
const agentBody = JSON.parse(agentRead.stdout);
|
|
1212
|
+
assert.equal(agentBody.node.nodeId, mainAgentId);
|
|
1213
|
+
assert.equal(agentBody.node.kind, 'agent');
|
|
1214
|
+
assert.equal(agentBody.node.settings?.values?.agentKind, 'main');
|
|
1215
|
+
|
|
1216
|
+
const subagentEnv = {
|
|
1217
|
+
...env,
|
|
1218
|
+
HARNESS_WORKFLOW_NODE_ID: subagentId,
|
|
1219
|
+
HARNESS_PEER_SESSION_ID: 'w27-subagent',
|
|
1220
|
+
};
|
|
1221
|
+
const rejected = await runNode([
|
|
1222
|
+
script,
|
|
1223
|
+
'node-map',
|
|
1224
|
+
'--action',
|
|
1225
|
+
'readGraph',
|
|
1226
|
+
'--project',
|
|
1227
|
+
root,
|
|
1228
|
+
], { cwd: root, timeout: 5000, env: subagentEnv });
|
|
1229
|
+
assert.notEqual(rejected.status, 0);
|
|
1230
|
+
assert.match(rejected.stderr, /Main Agent/);
|
|
1231
|
+
} finally {
|
|
1232
|
+
await stopServer(started.server);
|
|
1233
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
test('W24-WORKFLOW-CLI create-agent binds graph node and delegate/read-agent use graph identity', async () => {
|
|
1238
|
+
const root = makeHarnessTempRoot('wf-ui-agent-delegation-cli-test-');
|
|
1239
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
1240
|
+
const registry = new SessionRegistry();
|
|
1241
|
+
const mainAgentId = 'session-w24-main-agent';
|
|
1242
|
+
const mainSessionId = 'w24-main-agent';
|
|
1243
|
+
writeWorkflowGraphMap(root, {
|
|
1244
|
+
nodes: [
|
|
1245
|
+
{
|
|
1246
|
+
nodeId: mainAgentId,
|
|
1247
|
+
sessionId: mainSessionId,
|
|
1248
|
+
runtime: 'codex',
|
|
1249
|
+
agentKind: 'main',
|
|
1250
|
+
role: 'Main Agent',
|
|
1251
|
+
status: 'running',
|
|
1252
|
+
},
|
|
1253
|
+
],
|
|
1254
|
+
positions: {
|
|
1255
|
+
[mainAgentId]: { x: 20, y: 40 },
|
|
1256
|
+
},
|
|
1257
|
+
});
|
|
1258
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
|
|
1259
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
1260
|
+
const script = path.join(process.cwd(), 'Harness', 'scripts', 'wf-ui-control.mjs');
|
|
1261
|
+
const env = {
|
|
1262
|
+
...process.env,
|
|
1263
|
+
HARNESS_AGENT_KIND: 'main',
|
|
1264
|
+
HARNESS_PEER_SESSION_ID: mainSessionId,
|
|
1265
|
+
HARNESS_WORKFLOW_NODE_ID: mainAgentId,
|
|
1266
|
+
HARNESS_WF_UI_URL: localBaseUrl,
|
|
1267
|
+
HARNESS_WF_UI_TOKEN: started.token,
|
|
1268
|
+
HARNESS_PEER_RUNTIME: 'codex',
|
|
1269
|
+
};
|
|
1270
|
+
const written = [];
|
|
1271
|
+
try {
|
|
1272
|
+
const created = await runNode([
|
|
1273
|
+
script,
|
|
1274
|
+
'create-agent',
|
|
1275
|
+
'--runtime',
|
|
1276
|
+
'codex',
|
|
1277
|
+
'--role',
|
|
1278
|
+
'W24 Worker',
|
|
1279
|
+
'--objective',
|
|
1280
|
+
'W24 graph-bound worker',
|
|
1281
|
+
'--defer-pty-spawn',
|
|
1282
|
+
'true',
|
|
1283
|
+
'--project',
|
|
1284
|
+
root,
|
|
1285
|
+
], { cwd: root, timeout: 5000, env });
|
|
1286
|
+
assert.equal(created.status, 0, created.stderr || created.stdout);
|
|
1287
|
+
const createdBody = JSON.parse(created.stdout);
|
|
1288
|
+
assert.ok(createdBody.graphNodeId, JSON.stringify(createdBody));
|
|
1289
|
+
assert.equal(createdBody.parentNodeId, mainAgentId);
|
|
1290
|
+
assert.equal(createdBody.parentAgentId, mainSessionId);
|
|
1291
|
+
|
|
1292
|
+
const graphPath = path.join(root, 'Harness', 'a2a', 'workflow-map.json');
|
|
1293
|
+
const graph = JSON.parse(fs.readFileSync(graphPath, 'utf8'));
|
|
1294
|
+
const graphNode = (graph.nodes || []).find(node => node.nodeId === createdBody.graphNodeId);
|
|
1295
|
+
assert.ok(graphNode, 'create-agent should persist a graph node endpoint');
|
|
1296
|
+
assert.equal(graphNode.sessionId, createdBody.sessionId);
|
|
1297
|
+
assert.equal(graphNode.kind, 'terminal-session');
|
|
1298
|
+
|
|
1299
|
+
registry.update(createdBody.sessionId, { status: 'running' });
|
|
1300
|
+
const workerSession = registry.get(createdBody.sessionId);
|
|
1301
|
+
registerPtyProcess(createdBody.sessionId, { write: data => written.push(String(data)) });
|
|
1302
|
+
|
|
1303
|
+
const edge = await postJson(localBaseUrl, started.token, '/api/workflow/edges', {
|
|
1304
|
+
from: mainAgentId,
|
|
1305
|
+
to: createdBody.graphNodeId,
|
|
1306
|
+
relation: 'delegation',
|
|
1307
|
+
});
|
|
1308
|
+
assert.equal(edge.status, 201, JSON.stringify(edge.body));
|
|
1309
|
+
assert.equal(edge.body.edge.relation, 'delegation');
|
|
1310
|
+
|
|
1311
|
+
const context = await runNode([
|
|
1312
|
+
script,
|
|
1313
|
+
'workflow-context',
|
|
1314
|
+
'--node',
|
|
1315
|
+
mainAgentId,
|
|
1316
|
+
'--project',
|
|
1317
|
+
root,
|
|
1318
|
+
], { cwd: root, timeout: 5000, env });
|
|
1319
|
+
assert.equal(context.status, 0, context.stderr || context.stdout);
|
|
1320
|
+
const contextBody = JSON.parse(context.stdout);
|
|
1321
|
+
const workerRef = contextBody.context.connectedAgentRefs.find(ref => ref.nodeId === createdBody.graphNodeId);
|
|
1322
|
+
assert.ok(workerRef, JSON.stringify(contextBody.context.connectedAgentRefs));
|
|
1323
|
+
assert.equal(workerRef.relation, 'delegation');
|
|
1324
|
+
assert.equal(workerRef.delegation.sendAction, 'agent.sendInput');
|
|
1325
|
+
|
|
1326
|
+
const delegated = await runNode([
|
|
1327
|
+
script,
|
|
1328
|
+
'delegate-agent',
|
|
1329
|
+
'--node',
|
|
1330
|
+
createdBody.graphNodeId,
|
|
1331
|
+
'--text',
|
|
1332
|
+
'W24_DELEGATE_PING',
|
|
1333
|
+
'--project',
|
|
1334
|
+
root,
|
|
1335
|
+
], { cwd: root, timeout: 5000, env });
|
|
1336
|
+
assert.equal(delegated.status, 0, delegated.stderr || delegated.stdout);
|
|
1337
|
+
const delegateBody = JSON.parse(delegated.stdout);
|
|
1338
|
+
assert.equal(delegateBody.action, 'agent.sendInput');
|
|
1339
|
+
assert.equal(delegateBody.result.sessionId, createdBody.sessionId);
|
|
1340
|
+
assert.equal(written[0], 'W', 'the delegated body must start typing synchronously');
|
|
1341
|
+
await waitFor(() => (written.join('') === 'W24_DELEGATE_PING\r' ? written : null), { timeout: 8000 });
|
|
1342
|
+
assert.equal(written.join(''), 'W24_DELEGATE_PING\r', 'the typed body must join to the exact text + \\r');
|
|
1343
|
+
assert.equal(written[written.length - 1], '\r', 'the delegated submit must end with a single \\r');
|
|
1344
|
+
assert.equal(written.join('').includes('\n'), false, 'no \\n may be injected');
|
|
1345
|
+
assert.equal(written.filter(w => w === '\r').length, 1, 'exactly one \\r (0x0D)');
|
|
1346
|
+
|
|
1347
|
+
appendTerminalData(root, workerSession, 'W24_WORKER_OK\n', 'stdout');
|
|
1348
|
+
const read = await runNode([
|
|
1349
|
+
script,
|
|
1350
|
+
'read-agent',
|
|
1351
|
+
'--node',
|
|
1352
|
+
createdBody.graphNodeId,
|
|
1353
|
+
'--action',
|
|
1354
|
+
'transcript',
|
|
1355
|
+
'--tail',
|
|
1356
|
+
'20',
|
|
1357
|
+
'--project',
|
|
1358
|
+
root,
|
|
1359
|
+
], { cwd: root, timeout: 5000, env });
|
|
1360
|
+
assert.equal(read.status, 0, read.stderr || read.stdout);
|
|
1361
|
+
const readBody = JSON.parse(read.stdout);
|
|
1362
|
+
assert.equal(readBody.action, 'agent.readTranscript');
|
|
1363
|
+
assert.equal(readBody.result.sessionId, createdBody.sessionId);
|
|
1364
|
+
assert.ok(readBody.result.entries.some(entry => entry.stream === 'stdin' && entry.data.includes('W24_DELEGATE_PING')), JSON.stringify(readBody.result.entries));
|
|
1365
|
+
assert.ok(readBody.result.entries.some(entry => entry.stream === 'stdout' && entry.data.includes('W24_WORKER_OK')), JSON.stringify(readBody.result.entries));
|
|
1366
|
+
} finally {
|
|
1367
|
+
for (const session of registry.getAll()) unregisterPtyProcess(session.sessionId);
|
|
1368
|
+
await stopServer(started.server);
|
|
1369
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
test('W25-ENV-NODE-INIT graph start records env/node init only and preserves explicit initialInput', async () => {
|
|
1374
|
+
const root = makeHarnessTempRoot('wf-ui-env-node-init-test-');
|
|
1375
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
1376
|
+
const registry = new SessionRegistry();
|
|
1377
|
+
const graphNodeId = 'session-w25-bootstrap-agent';
|
|
1378
|
+
const explicitGraphNodeId = 'session-w25-explicit-agent';
|
|
1379
|
+
persistSession(root, {
|
|
1380
|
+
sessionId: 'w25-bootstrap-previous',
|
|
1381
|
+
runtime: 'codex',
|
|
1382
|
+
agentKind: 'subagent',
|
|
1383
|
+
role: 'W25 Bootstrap Agent',
|
|
1384
|
+
objective: 'Read connected graph context before acting',
|
|
1385
|
+
projectRoot: root,
|
|
1386
|
+
cwd: root,
|
|
1387
|
+
graphNodeId,
|
|
1388
|
+
status: 'stopped',
|
|
1389
|
+
});
|
|
1390
|
+
persistSession(root, {
|
|
1391
|
+
sessionId: 'w25-explicit-previous',
|
|
1392
|
+
runtime: 'codex',
|
|
1393
|
+
agentKind: 'subagent',
|
|
1394
|
+
role: 'W25 Explicit Agent',
|
|
1395
|
+
objective: 'Use explicit startup input',
|
|
1396
|
+
projectRoot: root,
|
|
1397
|
+
cwd: root,
|
|
1398
|
+
graphNodeId: explicitGraphNodeId,
|
|
1399
|
+
status: 'stopped',
|
|
1400
|
+
});
|
|
1401
|
+
writeWorkflowGraphMap(root, {
|
|
1402
|
+
nodes: [
|
|
1403
|
+
{
|
|
1404
|
+
nodeId: graphNodeId,
|
|
1405
|
+
sessionId: 'w25-bootstrap-previous',
|
|
1406
|
+
runtime: 'codex',
|
|
1407
|
+
agentKind: 'subagent',
|
|
1408
|
+
role: 'W25 Bootstrap Agent',
|
|
1409
|
+
objective: 'Read connected graph context before acting',
|
|
1410
|
+
status: 'stopped',
|
|
1411
|
+
},
|
|
1412
|
+
{
|
|
1413
|
+
nodeId: explicitGraphNodeId,
|
|
1414
|
+
sessionId: 'w25-explicit-previous',
|
|
1415
|
+
runtime: 'codex',
|
|
1416
|
+
agentKind: 'subagent',
|
|
1417
|
+
role: 'W25 Explicit Agent',
|
|
1418
|
+
objective: 'Use explicit startup input',
|
|
1419
|
+
status: 'stopped',
|
|
1420
|
+
},
|
|
1421
|
+
],
|
|
1422
|
+
positions: {
|
|
1423
|
+
[graphNodeId]: { x: 80, y: 120 },
|
|
1424
|
+
[explicitGraphNodeId]: { x: 360, y: 120 },
|
|
1425
|
+
},
|
|
1426
|
+
});
|
|
1427
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
|
|
1428
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
1429
|
+
try {
|
|
1430
|
+
const bootstrap = await postJson(localBaseUrl, started.token, `/api/a2a/nodes/${graphNodeId}/start`, {});
|
|
1431
|
+
assert.equal(bootstrap.status, 200, JSON.stringify(bootstrap.body));
|
|
1432
|
+
assert.equal(bootstrap.body.graphNodeId, graphNodeId);
|
|
1433
|
+
assert.equal(bootstrap.body.started.status, 'blocked');
|
|
1434
|
+
|
|
1435
|
+
const bootstrapState = listTerminalSessions(root).find(session => session.sessionId === bootstrap.body.sessionId);
|
|
1436
|
+
assert.equal(bootstrapState.pendingInitialInputMode, undefined);
|
|
1437
|
+
assert.equal(bootstrapState.pendingInitialInputBytes, undefined);
|
|
1438
|
+
assert.equal(fs.existsSync(bootstrapState.nodeInitPath), true);
|
|
1439
|
+
const init = fs.readFileSync(bootstrapState.nodeInitPath, 'utf8');
|
|
1440
|
+
assert.match(init, /HARNESS_NODE_INIT/);
|
|
1441
|
+
assert.match(init, /workflow-ontology/);
|
|
1442
|
+
assert.match(init, /## Working Method — discovery first/);
|
|
1443
|
+
assert.match(init, /hydrate EVERY turn, mandatory/);
|
|
1444
|
+
assert.match(init, /workflow-context --project \./);
|
|
1445
|
+
assert.match(init, /## Invariant Rules/);
|
|
1446
|
+
assert.doesNotMatch(init, /short bootstrap prompt/i);
|
|
1447
|
+
|
|
1448
|
+
const bootstrapEvents = readJsonlFile(path.join(root, 'Harness', 'a2a', 'sessions', bootstrap.body.sessionId, 'events.jsonl'));
|
|
1449
|
+
const pendingBootstrap = bootstrapEvents.find(event => event.type === 'terminal.input.pending');
|
|
1450
|
+
assert.equal(pendingBootstrap, undefined);
|
|
1451
|
+
|
|
1452
|
+
const explicitInput = 'CUSTOM_INITIAL_INPUT\r';
|
|
1453
|
+
const explicit = await postJson(localBaseUrl, started.token, `/api/a2a/nodes/${explicitGraphNodeId}/start`, {
|
|
1454
|
+
initialInput: explicitInput,
|
|
1455
|
+
});
|
|
1456
|
+
assert.equal(explicit.status, 200, JSON.stringify(explicit.body));
|
|
1457
|
+
assert.equal(explicit.body.graphNodeId, explicitGraphNodeId);
|
|
1458
|
+
|
|
1459
|
+
const explicitState = listTerminalSessions(root).find(session => session.sessionId === explicit.body.sessionId);
|
|
1460
|
+
assert.equal(explicitState.pendingInitialInputMode, 'explicit');
|
|
1461
|
+
assert.equal(explicitState.pendingInitialInputBytes, explicitInput.length);
|
|
1462
|
+
const explicitEvents = readJsonlFile(path.join(root, 'Harness', 'a2a', 'sessions', explicit.body.sessionId, 'events.jsonl'));
|
|
1463
|
+
const pendingExplicit = explicitEvents.find(event => event.type === 'terminal.input.pending');
|
|
1464
|
+
assert.equal(pendingExplicit.mode, 'explicit');
|
|
1465
|
+
assert.equal(pendingExplicit.bytes, explicitInput.length);
|
|
1466
|
+
} finally {
|
|
1467
|
+
await stopServer(started.server);
|
|
1468
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
test('W26-INITIAL-INPUT waits for terminal ready output before injecting explicit initial input', () => {
|
|
1473
|
+
assert.equal(terminalReadyForInitialInput('\u001b[1t', 'claude'), false);
|
|
1474
|
+
assert.equal(
|
|
1475
|
+
terminalReadyForInitialInput('\u001b[HClaude Code v2.1.215\r\n❯ \r\n⏵⏵ bypass permissions on', 'claude'),
|
|
1476
|
+
true,
|
|
1477
|
+
);
|
|
1478
|
+
assert.equal(terminalReadyForInitialInput('Codex ready ›', 'codex'), true);
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
test('codex update prompt response writes selected menu input and clears pending request', async () => {
|
|
1482
|
+
const root = makeHarnessTempRoot('wf-ui-codex-update-test-');
|
|
1483
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
1484
|
+
const registry = new SessionRegistry();
|
|
1485
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
|
|
1486
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
1487
|
+
const written = [];
|
|
1488
|
+
try {
|
|
1489
|
+
const session = registry.create({ runtime: 'codex' });
|
|
1490
|
+
registry.update(session.sessionId, {
|
|
1491
|
+
status: 'running',
|
|
1492
|
+
controlRequest: {
|
|
1493
|
+
requestId: 'codex-update-test',
|
|
1494
|
+
type: 'codex:update-prompt',
|
|
1495
|
+
status: 'pending',
|
|
1496
|
+
detectedAt: new Date().toISOString(),
|
|
1497
|
+
},
|
|
1498
|
+
});
|
|
1499
|
+
registerPtyProcess(session.sessionId, { write: (data) => written.push(data) });
|
|
1500
|
+
|
|
1501
|
+
const first = await postJson(localBaseUrl, started.token, `/api/sessions/${session.sessionId}/codex-update-prompt`, { choice: 'skip-session' });
|
|
1502
|
+
assert.equal(first.status, 200);
|
|
1503
|
+
assert.equal(first.body.ok, true);
|
|
1504
|
+
assert.equal(written.at(-1), CODEX_UPDATE_SKIP_SESSION_KEYS);
|
|
1505
|
+
assert.equal(registry.get(session.sessionId).controlRequest, null);
|
|
1506
|
+
|
|
1507
|
+
registry.update(session.sessionId, {
|
|
1508
|
+
controlRequest: {
|
|
1509
|
+
requestId: 'codex-update-test-2',
|
|
1510
|
+
type: 'codex:update-prompt',
|
|
1511
|
+
status: 'pending',
|
|
1512
|
+
detectedAt: new Date().toISOString(),
|
|
1513
|
+
},
|
|
1514
|
+
});
|
|
1515
|
+
const second = await postJson(localBaseUrl, started.token, `/api/sessions/${session.sessionId}/codex-update-prompt`, { choice: 'skip-until-next-version' });
|
|
1516
|
+
assert.equal(second.status, 200);
|
|
1517
|
+
assert.equal(second.body.ok, true);
|
|
1518
|
+
assert.equal(written.at(-1), CODEX_UPDATE_SKIP_UNTIL_NEXT_VERSION_KEYS);
|
|
1519
|
+
assert.equal(registry.get(session.sessionId).controlRequest, null);
|
|
1520
|
+
} finally {
|
|
1521
|
+
for (const session of registry.getAll()) unregisterPtyProcess(session.sessionId);
|
|
1522
|
+
await stopServer(started.server);
|
|
1523
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
test('codex update prompt response is single-use under concurrent requests', async () => {
|
|
1528
|
+
const root = makeHarnessTempRoot('wf-ui-codex-update-lock-test-');
|
|
1529
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
1530
|
+
const registry = new SessionRegistry();
|
|
1531
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
|
|
1532
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
1533
|
+
const written = [];
|
|
1534
|
+
try {
|
|
1535
|
+
const session = registry.create({ runtime: 'codex' });
|
|
1536
|
+
registry.update(session.sessionId, {
|
|
1537
|
+
status: 'running',
|
|
1538
|
+
controlRequest: {
|
|
1539
|
+
requestId: 'codex-update-concurrent-test',
|
|
1540
|
+
type: 'codex:update-prompt',
|
|
1541
|
+
status: 'pending',
|
|
1542
|
+
detectedAt: new Date().toISOString(),
|
|
1543
|
+
},
|
|
1544
|
+
});
|
|
1545
|
+
registerPtyProcess(session.sessionId, { write: (data) => written.push(data) });
|
|
1546
|
+
|
|
1547
|
+
const results = await Promise.all([
|
|
1548
|
+
postJson(localBaseUrl, started.token, `/api/sessions/${session.sessionId}/codex-update-prompt`, { choice: 'skip-session' }),
|
|
1549
|
+
postJson(localBaseUrl, started.token, `/api/sessions/${session.sessionId}/codex-update-prompt`, { choice: 'skip-session' }),
|
|
1550
|
+
]);
|
|
1551
|
+
|
|
1552
|
+
assert.deepEqual(results.map(result => result.status).sort(), [200, 400]);
|
|
1553
|
+
assert.deepEqual(written, [CODEX_UPDATE_SKIP_SESSION_KEYS]);
|
|
1554
|
+
assert.equal(registry.get(session.sessionId).controlRequest, null);
|
|
1555
|
+
} finally {
|
|
1556
|
+
for (const session of registry.getAll()) unregisterPtyProcess(session.sessionId);
|
|
1557
|
+
await stopServer(started.server);
|
|
1558
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
test('AC-CLEANUP-003 cleanup API previews and prunes eligible stopped sessions', async () => {
|
|
1563
|
+
const root = makeHarnessTempRoot('wf-ui-cleanup-api-test-');
|
|
1564
|
+
fs.mkdirSync(path.join(root, 'Harness', 'tasks'), { recursive: true });
|
|
1565
|
+
const registry = new SessionRegistry();
|
|
1566
|
+
const started = await startServer({ projectRoot: root, host: '127.0.0.1', port: 0, sessionRegistry: registry });
|
|
1567
|
+
const localBaseUrl = `http://127.0.0.1:${started.port}`;
|
|
1568
|
+
const sessionDir = path.join(root, 'Harness', 'a2a', 'sessions', 'session-cleanup-old');
|
|
1569
|
+
try {
|
|
1570
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
1571
|
+
fs.writeFileSync(path.join(sessionDir, 'STATE.json'), JSON.stringify({
|
|
1572
|
+
sessionId: 'session-cleanup-old',
|
|
1573
|
+
runtime: 'codex',
|
|
1574
|
+
status: 'saved',
|
|
1575
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
1576
|
+
terminalSeq: 1,
|
|
1577
|
+
}, null, 2));
|
|
1578
|
+
fs.writeFileSync(path.join(sessionDir, 'terminal.jsonl'), '{"seq":1,"data":"done"}\n');
|
|
1579
|
+
|
|
1580
|
+
const summary = await fetchJson(localBaseUrl, started.token, '/api/cleanup/summary');
|
|
1581
|
+
assert.equal(summary.status, 200);
|
|
1582
|
+
assert.equal(Object.hasOwn(summary.body, '_internal'), false);
|
|
1583
|
+
assert.ok(summary.body.sessions.eligibleCount >= 1);
|
|
1584
|
+
assert.ok(summary.body.targets.sessions.some(target => target.sessionId === 'session-cleanup-old'));
|
|
1585
|
+
|
|
1586
|
+
const result = await postJson(localBaseUrl, started.token, '/api/cleanup/prune', {
|
|
1587
|
+
apply: true,
|
|
1588
|
+
policy: {
|
|
1589
|
+
stoppedSessionRetentionDays: 1,
|
|
1590
|
+
keepStoppedSessions: 20,
|
|
1591
|
+
includeTaskSessions: false,
|
|
1592
|
+
detachedLogRetentionHours: 24,
|
|
1593
|
+
},
|
|
1594
|
+
});
|
|
1595
|
+
assert.equal(result.status, 200);
|
|
1596
|
+
assert.equal(result.body.applied, true);
|
|
1597
|
+
assert.equal(fs.existsSync(sessionDir), false);
|
|
1598
|
+
} finally {
|
|
1599
|
+
await stopServer(started.server);
|
|
1600
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1601
|
+
}
|
|
1602
|
+
});
|