tmux-ide 1.3.1 → 2.1.0
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/bin/cli.js +4 -216
- package/bin/cli.ts +536 -0
- package/dashboard/out/404/index.html +1 -0
- package/dashboard/out/404.html +1 -0
- package/dashboard/out/__next.__PAGE__.txt +9 -0
- package/dashboard/out/__next._full.txt +21 -0
- package/dashboard/out/__next._head.txt +5 -0
- package/dashboard/out/__next._index.txt +7 -0
- package/dashboard/out/__next._tree.txt +4 -0
- package/dashboard/out/_next/static/WTHSCKfFb30xXXhXuq66f/_buildManifest.js +11 -0
- package/dashboard/out/_next/static/WTHSCKfFb30xXXhXuq66f/_clientMiddlewareManifest.json +1 -0
- package/dashboard/out/_next/static/WTHSCKfFb30xXXhXuq66f/_ssgManifest.js +1 -0
- package/dashboard/out/_next/static/chunks/048e2449530877e2.js +1 -0
- package/dashboard/out/_next/static/chunks/053c57bda6abb959.js +1 -0
- package/dashboard/out/_next/static/chunks/056760c6c2dd9172.js +1 -0
- package/dashboard/out/_next/static/chunks/06ab47357b6d3e35.js +1 -0
- package/dashboard/out/_next/static/chunks/0921cbe5dfd6738e.js +1 -0
- package/dashboard/out/_next/static/chunks/0b2599bf468cf5ea.js +1 -0
- package/dashboard/out/_next/static/chunks/0c012eac08ef2c4f.js +1 -0
- package/dashboard/out/_next/static/chunks/0c29e87fcdcffac3.js +1 -0
- package/dashboard/out/_next/static/chunks/0e1e98854430c693.js +1 -0
- package/dashboard/out/_next/static/chunks/0e24498976a88976.js +1 -0
- package/dashboard/out/_next/static/chunks/0e5bf11ff0b9ab90.js +1 -0
- package/dashboard/out/_next/static/chunks/0eae0a45ab8416c8.js +1 -0
- package/dashboard/out/_next/static/chunks/104797400d9d38ad.js +1 -0
- package/dashboard/out/_next/static/chunks/10cfcfce475f9d2d.js +1 -0
- package/dashboard/out/_next/static/chunks/11bac38e7cfb4ecc.js +1 -0
- package/dashboard/out/_next/static/chunks/120c5f6501ec3529.js +1 -0
- package/dashboard/out/_next/static/chunks/121fceb2b1dcd79f.js +1 -0
- package/dashboard/out/_next/static/chunks/13726b63e1be6b98.js +1 -0
- package/dashboard/out/_next/static/chunks/140c6ce631514660.js +1 -0
- package/dashboard/out/_next/static/chunks/1435ee3a51174c97.js +1 -0
- package/dashboard/out/_next/static/chunks/14dc927d2e2cca91.js +1 -0
- package/dashboard/out/_next/static/chunks/179c3031b292d98e.js +1 -0
- package/dashboard/out/_next/static/chunks/181e88957c47e5c1.js +1 -0
- package/dashboard/out/_next/static/chunks/18f70ba23873c432.js +1 -0
- package/dashboard/out/_next/static/chunks/1b58303b027fd135.js +1 -0
- package/dashboard/out/_next/static/chunks/1b5f49a358dcc71e.js +1 -0
- package/dashboard/out/_next/static/chunks/1bba44b81585099a.js +1 -0
- package/dashboard/out/_next/static/chunks/1bea8dee19a3e9ae.js +1 -0
- package/dashboard/out/_next/static/chunks/1cfad21646fc250e.js +1 -0
- package/dashboard/out/_next/static/chunks/1d053dc9e1e428ef.js +1 -0
- package/dashboard/out/_next/static/chunks/1e7d7639c6f0ee5e.js +1 -0
- package/dashboard/out/_next/static/chunks/1f42504ca81463f1.js +1 -0
- package/dashboard/out/_next/static/chunks/20c92047399305b4.js +1 -0
- package/dashboard/out/_next/static/chunks/214560e9514a9a4d.js +1 -0
- package/dashboard/out/_next/static/chunks/21d4ef106914e997.js +1 -0
- package/dashboard/out/_next/static/chunks/2586e83fc3dc2588.js +1 -0
- package/dashboard/out/_next/static/chunks/267136a09e94f5c2.js +1 -0
- package/dashboard/out/_next/static/chunks/271c50b329917c10.js +1 -0
- package/dashboard/out/_next/static/chunks/27fccecefffd88b5.js +1 -0
- package/dashboard/out/_next/static/chunks/28f058ed331315b6.js +1 -0
- package/dashboard/out/_next/static/chunks/28f704f2cc5713eb.js +1 -0
- package/dashboard/out/_next/static/chunks/290f67a6cd150a98.js +1 -0
- package/dashboard/out/_next/static/chunks/2ad12f4dcc30d30a.js +1 -0
- package/dashboard/out/_next/static/chunks/2b650b172078ab92.js +1 -0
- package/dashboard/out/_next/static/chunks/2bd91076019a6453.js +1 -0
- package/dashboard/out/_next/static/chunks/2e1278563a8e26be.js +1 -0
- package/dashboard/out/_next/static/chunks/309eb70780a3d67d.js +1 -0
- package/dashboard/out/_next/static/chunks/30f71950992e2307.js +1 -0
- package/dashboard/out/_next/static/chunks/31191873c2075784.js +1 -0
- package/dashboard/out/_next/static/chunks/31a790921a0c035e.js +1 -0
- package/dashboard/out/_next/static/chunks/34f8d7a5a06e8e32.js +1 -0
- package/dashboard/out/_next/static/chunks/36b3716436d9bc9d.js +1 -0
- package/dashboard/out/_next/static/chunks/3770b7d0ff004de3.js +1 -0
- package/dashboard/out/_next/static/chunks/381ab3bd4cffbf4d.js +1 -0
- package/dashboard/out/_next/static/chunks/38aa4eada5dcd1a4.js +1 -0
- package/dashboard/out/_next/static/chunks/39723132b78cb77e.js +1 -0
- package/dashboard/out/_next/static/chunks/399efdcc3fe56e09.js +1 -0
- package/dashboard/out/_next/static/chunks/3a8431a9221b0706.js +1 -0
- package/dashboard/out/_next/static/chunks/3a9b40810ce88cbf.js +1 -0
- package/dashboard/out/_next/static/chunks/3b322e3e9476ecdf.js +1 -0
- package/dashboard/out/_next/static/chunks/3bb14befeeb08c28.js +1 -0
- package/dashboard/out/_next/static/chunks/3c99637ebd462b20.js +1 -0
- package/dashboard/out/_next/static/chunks/3cbb9217d56459d8.js +1 -0
- package/dashboard/out/_next/static/chunks/3d438e0929a762f7.js +1 -0
- package/dashboard/out/_next/static/chunks/3e6d4834e0752f76.js +2 -0
- package/dashboard/out/_next/static/chunks/3e6f8cedaad4e89b.js +1 -0
- package/dashboard/out/_next/static/chunks/3e701259ce1b53e3.js +1 -0
- package/dashboard/out/_next/static/chunks/41562f7ab7c033eb.js +1 -0
- package/dashboard/out/_next/static/chunks/418fadd8a94e2fd7.js +1 -0
- package/dashboard/out/_next/static/chunks/42345437bb922e9c.js +1 -0
- package/dashboard/out/_next/static/chunks/434b6bd78c97b668.js +1 -0
- package/dashboard/out/_next/static/chunks/434febe897d33e2d.js +1 -0
- package/dashboard/out/_next/static/chunks/45d28707dd8ce1c1.js +1 -0
- package/dashboard/out/_next/static/chunks/4620a640e88cb1b7.js +1 -0
- package/dashboard/out/_next/static/chunks/468f8eb604502ea4.js +1 -0
- package/dashboard/out/_next/static/chunks/495f5cedf8afe823.js +1 -0
- package/dashboard/out/_next/static/chunks/49a89d1704d27740.js +1 -0
- package/dashboard/out/_next/static/chunks/49e5c37d15f63def.js +1 -0
- package/dashboard/out/_next/static/chunks/4a46ad186a8587ee.css +2 -0
- package/dashboard/out/_next/static/chunks/4a5c728574935d50.js +1 -0
- package/dashboard/out/_next/static/chunks/4b27ebd4d9304d8f.js +1 -0
- package/dashboard/out/_next/static/chunks/4b965aa320a9b358.js +1 -0
- package/dashboard/out/_next/static/chunks/4bb2da90bbe5794f.js +1 -0
- package/dashboard/out/_next/static/chunks/4d2f1c6f631e4b85.js +1 -0
- package/dashboard/out/_next/static/chunks/4d696d5a64dab42e.js +1 -0
- package/dashboard/out/_next/static/chunks/4f660dadb3f1e9e2.js +1 -0
- package/dashboard/out/_next/static/chunks/507b50d47f949333.js +1 -0
- package/dashboard/out/_next/static/chunks/50b1b7063bb044e2.js +1 -0
- package/dashboard/out/_next/static/chunks/50c0c2e3c4f02508.js +1 -0
- package/dashboard/out/_next/static/chunks/50d7ba9803e65b02.js +1 -0
- package/dashboard/out/_next/static/chunks/51535a6d4c98c7a4.js +1 -0
- package/dashboard/out/_next/static/chunks/526de6cc16c7dd2f.js +1 -0
- package/dashboard/out/_next/static/chunks/5307604f8632b3fd.js +1 -0
- package/dashboard/out/_next/static/chunks/5480bd722260f231.js +1 -0
- package/dashboard/out/_next/static/chunks/54c61c82e5f1dca5.js +1 -0
- package/dashboard/out/_next/static/chunks/555de6ac6bb9034c.js +1 -0
- package/dashboard/out/_next/static/chunks/562b89ee76a4b90c.js +1 -0
- package/dashboard/out/_next/static/chunks/5747f9a32edff429.js +1 -0
- package/dashboard/out/_next/static/chunks/57ee141430b4c05c.js +1 -0
- package/dashboard/out/_next/static/chunks/584c066a0837dd9a.js +1 -0
- package/dashboard/out/_next/static/chunks/5856aed4670a6253.js +1 -0
- package/dashboard/out/_next/static/chunks/58fe563414726511.js +1 -0
- package/dashboard/out/_next/static/chunks/5948196b2729b18c.js +1 -0
- package/dashboard/out/_next/static/chunks/5ab77c69e7306f97.js +1 -0
- package/dashboard/out/_next/static/chunks/5b3fa4c99d61611c.js +1 -0
- package/dashboard/out/_next/static/chunks/5cc87abaef616661.js +1 -0
- package/dashboard/out/_next/static/chunks/5dfa7913f99bb966.js +1 -0
- package/dashboard/out/_next/static/chunks/5e49b008432770f1.js +1 -0
- package/dashboard/out/_next/static/chunks/5e5a33dd81c45c47.js +1 -0
- package/dashboard/out/_next/static/chunks/5ee61bed638eb8a8.js +1 -0
- package/dashboard/out/_next/static/chunks/5eed557b9f45ae4a.js +1 -0
- package/dashboard/out/_next/static/chunks/60446823a7d30c1b.js +1 -0
- package/dashboard/out/_next/static/chunks/605150c490861442.js +1 -0
- package/dashboard/out/_next/static/chunks/609056d51c0bf332.js +1 -0
- package/dashboard/out/_next/static/chunks/60ec91dfeb50071b.js +1 -0
- package/dashboard/out/_next/static/chunks/61bc7be0bba6a33f.js +1 -0
- package/dashboard/out/_next/static/chunks/61f4a5c9ce23e55e.js +1 -0
- package/dashboard/out/_next/static/chunks/63aae3585be04e65.js +1 -0
- package/dashboard/out/_next/static/chunks/64e0e1ee7ef4f01b.js +1 -0
- package/dashboard/out/_next/static/chunks/66e9b4c26ff1aaaf.js +1 -0
- package/dashboard/out/_next/static/chunks/67068847aa1b5e98.js +1 -0
- package/dashboard/out/_next/static/chunks/68d7de1f2e202c26.js +1 -0
- package/dashboard/out/_next/static/chunks/698ca3ea1937ba4f.js +1 -0
- package/dashboard/out/_next/static/chunks/69eb97c24b5b6b9d.js +1 -0
- package/dashboard/out/_next/static/chunks/6b1ac098c228a479.js +1 -0
- package/dashboard/out/_next/static/chunks/6bb8020cb796bcc1.js +1 -0
- package/dashboard/out/_next/static/chunks/6cb5c58418022991.js +193 -0
- package/dashboard/out/_next/static/chunks/6cf8b1db17f2a155.js +1 -0
- package/dashboard/out/_next/static/chunks/6e7613643906ea88.js +1 -0
- package/dashboard/out/_next/static/chunks/6ea27ceb2e6feae5.js +1 -0
- package/dashboard/out/_next/static/chunks/6f95a8dbcf23dded.js +1 -0
- package/dashboard/out/_next/static/chunks/6fd44e759d82f64e.js +1 -0
- package/dashboard/out/_next/static/chunks/716ec29c268c38f6.js +1 -0
- package/dashboard/out/_next/static/chunks/717b399dfcf7c02c.js +1 -0
- package/dashboard/out/_next/static/chunks/72badd90e1c91348.js +1 -0
- package/dashboard/out/_next/static/chunks/72f9296dbb7ae3c4.js +1 -0
- package/dashboard/out/_next/static/chunks/73aa31e1144d8191.js +1 -0
- package/dashboard/out/_next/static/chunks/73f98ded8fe2e879.js +1 -0
- package/dashboard/out/_next/static/chunks/740a2e8ace15e13b.js +1 -0
- package/dashboard/out/_next/static/chunks/74e57c16e7304b6b.js +1 -0
- package/dashboard/out/_next/static/chunks/7628dead4d113df2.js +1 -0
- package/dashboard/out/_next/static/chunks/7717c88c07ed96e6.js +1 -0
- package/dashboard/out/_next/static/chunks/775581f7956d9a27.js +1 -0
- package/dashboard/out/_next/static/chunks/77d3b6661befdb98.js +1 -0
- package/dashboard/out/_next/static/chunks/78435c0e1c77fea7.js +1 -0
- package/dashboard/out/_next/static/chunks/791336748b9c9393.js +1 -0
- package/dashboard/out/_next/static/chunks/79ea68dfae65ef2f.js +1 -0
- package/dashboard/out/_next/static/chunks/7bae777a472eb13c.js +1 -0
- package/dashboard/out/_next/static/chunks/7fd847f47237a35c.js +1 -0
- package/dashboard/out/_next/static/chunks/804d718be0370058.css +1 -0
- package/dashboard/out/_next/static/chunks/807cb3c1923995ab.js +1 -0
- package/dashboard/out/_next/static/chunks/8286b39407d94447.js +1 -0
- package/dashboard/out/_next/static/chunks/84e4305b8dc830ed.js +1 -0
- package/dashboard/out/_next/static/chunks/850f99dbfabb53e0.js +1 -0
- package/dashboard/out/_next/static/chunks/85594127e66b6b7f.js +1 -0
- package/dashboard/out/_next/static/chunks/858c11169c183c9c.js +1 -0
- package/dashboard/out/_next/static/chunks/85e27005dfe4c3e3.js +1 -0
- package/dashboard/out/_next/static/chunks/8707bd3d8ddc1f1c.js +1 -0
- package/dashboard/out/_next/static/chunks/8851e5d7ef894973.js +1 -0
- package/dashboard/out/_next/static/chunks/88ba3782cc614989.js +1 -0
- package/dashboard/out/_next/static/chunks/8a0c9a181784c2ef.js +1 -0
- package/dashboard/out/_next/static/chunks/8ad01ae355a6e7f6.js +1 -0
- package/dashboard/out/_next/static/chunks/8d2ed93b0d0a4b4f.js +1 -0
- package/dashboard/out/_next/static/chunks/8dc352ebd5310d3c.js +1 -0
- package/dashboard/out/_next/static/chunks/8dd1d722c8c1a22c.js +1 -0
- package/dashboard/out/_next/static/chunks/8e60fe307e3e4986.js +1 -0
- package/dashboard/out/_next/static/chunks/8eea4dd71e25f32a.js +1 -0
- package/dashboard/out/_next/static/chunks/8f468a65bac34eaa.js +1 -0
- package/dashboard/out/_next/static/chunks/9057e5382d6a5059.js +1 -0
- package/dashboard/out/_next/static/chunks/931d056437805ecb.js +1 -0
- package/dashboard/out/_next/static/chunks/954eb11648dd8cff.js +1 -0
- package/dashboard/out/_next/static/chunks/955034d876d14bb3.js +1 -0
- package/dashboard/out/_next/static/chunks/966c4895773249f6.js +1 -0
- package/dashboard/out/_next/static/chunks/96aff410a6ab106c.js +1 -0
- package/dashboard/out/_next/static/chunks/99b396c821c1dd2d.js +1 -0
- package/dashboard/out/_next/static/chunks/9a255fd2cf4922ae.js +1 -0
- package/dashboard/out/_next/static/chunks/9b1f6aa73bc864b2.js +1 -0
- package/dashboard/out/_next/static/chunks/9b9ce582c533aa79.js +1 -0
- package/dashboard/out/_next/static/chunks/9c19bd3c773c6611.js +1 -0
- package/dashboard/out/_next/static/chunks/9ceeaa847eb15ca2.js +1 -0
- package/dashboard/out/_next/static/chunks/9d2e6e7b8b65f845.js +1 -0
- package/dashboard/out/_next/static/chunks/9de23802066128ff.js +1 -0
- package/dashboard/out/_next/static/chunks/9e327f94b3701170.js +1 -0
- package/dashboard/out/_next/static/chunks/9e7818421c2d455a.js +1 -0
- package/dashboard/out/_next/static/chunks/9e935acbcf2fb519.js +1 -0
- package/dashboard/out/_next/static/chunks/9ed9f68340343b51.js +1 -0
- package/dashboard/out/_next/static/chunks/9f94a0242455fc20.js +1 -0
- package/dashboard/out/_next/static/chunks/9fff46b5aaecf47e.js +1 -0
- package/dashboard/out/_next/static/chunks/a06d267a5c557657.js +1 -0
- package/dashboard/out/_next/static/chunks/a0eb8c81c54722fe.js +1 -0
- package/dashboard/out/_next/static/chunks/a16808e83106427d.js +1 -0
- package/dashboard/out/_next/static/chunks/a2215e2bc72739ef.js +1 -0
- package/dashboard/out/_next/static/chunks/a2de9e6696eb9c17.js +1 -0
- package/dashboard/out/_next/static/chunks/a33420b8c14090d6.js +1 -0
- package/dashboard/out/_next/static/chunks/a360bffef39d818b.js +1 -0
- package/dashboard/out/_next/static/chunks/a42852fdf5287cf5.js +1 -0
- package/dashboard/out/_next/static/chunks/a4efc88488322b67.js +1 -0
- package/dashboard/out/_next/static/chunks/a696a91d14101bdb.js +1 -0
- package/dashboard/out/_next/static/chunks/a6dad97d9634a72d.js +1 -0
- package/dashboard/out/_next/static/chunks/a6dad97d9634a72d.js.map +1 -0
- package/dashboard/out/_next/static/chunks/a705c7261712bd8e.js +1 -0
- package/dashboard/out/_next/static/chunks/a7d1e4b067ace180.js +1 -0
- package/dashboard/out/_next/static/chunks/a8590c121a7efaa5.js +1 -0
- package/dashboard/out/_next/static/chunks/a95953a83a6df6cf.js +1 -0
- package/dashboard/out/_next/static/chunks/a9d0a757d3b4afed.js +1 -0
- package/dashboard/out/_next/static/chunks/a9dc35514619c872.js +1 -0
- package/dashboard/out/_next/static/chunks/ab1821552066a74d.js +1 -0
- package/dashboard/out/_next/static/chunks/ac96bc0eccebe15a.js +1 -0
- package/dashboard/out/_next/static/chunks/acc03cfaaf6bb30f.js +1 -0
- package/dashboard/out/_next/static/chunks/acc298ae059b9fe4.js +1 -0
- package/dashboard/out/_next/static/chunks/ad076b6e996d250c.js +1 -0
- package/dashboard/out/_next/static/chunks/ae53b64af7ba16d5.js +1 -0
- package/dashboard/out/_next/static/chunks/ae5da7fa4bc5fa95.js +1 -0
- package/dashboard/out/_next/static/chunks/aefcf0eb033a155e.js +1 -0
- package/dashboard/out/_next/static/chunks/b12d43100ee10526.js +1 -0
- package/dashboard/out/_next/static/chunks/b137952dd5566a41.js +1 -0
- package/dashboard/out/_next/static/chunks/b2832d8a2d332a47.js +1 -0
- package/dashboard/out/_next/static/chunks/b417aa09c15f63c6.js +1 -0
- package/dashboard/out/_next/static/chunks/b48e152ba7bf6caf.js +1 -0
- package/dashboard/out/_next/static/chunks/b4cbef8e4714bf92.js +1 -0
- package/dashboard/out/_next/static/chunks/b568ff8383a37f53.js +1 -0
- package/dashboard/out/_next/static/chunks/b6e1495dd89a39e3.js +1 -0
- package/dashboard/out/_next/static/chunks/b73a21bd67528440.js +1 -0
- package/dashboard/out/_next/static/chunks/b77ee62bc71dcec7.js +1 -0
- package/dashboard/out/_next/static/chunks/b7f454615c11d4eb.js +1 -0
- package/dashboard/out/_next/static/chunks/b818ea32a839590f.js +1 -0
- package/dashboard/out/_next/static/chunks/b901ce78c1c65d66.js +1 -0
- package/dashboard/out/_next/static/chunks/b93d647b803be9fa.js +1 -0
- package/dashboard/out/_next/static/chunks/b960b09ff1ecd6bb.js +1 -0
- package/dashboard/out/_next/static/chunks/ba717c7779a778fc.js +1 -0
- package/dashboard/out/_next/static/chunks/bacb0ae7458d18b6.js +1 -0
- package/dashboard/out/_next/static/chunks/bb51f68b27346852.js +1 -0
- package/dashboard/out/_next/static/chunks/bb67f22e1ab632ae.js +1 -0
- package/dashboard/out/_next/static/chunks/bbae97ec58d1bb9d.js +1 -0
- package/dashboard/out/_next/static/chunks/bbaf38ee90307765.js +1 -0
- package/dashboard/out/_next/static/chunks/be01598fe1fce5db.js +1 -0
- package/dashboard/out/_next/static/chunks/bf4cbbc095987fec.js +1 -0
- package/dashboard/out/_next/static/chunks/c18f823797d57446.js +1 -0
- package/dashboard/out/_next/static/chunks/c1bcece34ec5b295.js +1 -0
- package/dashboard/out/_next/static/chunks/c1ccbc3a51adaf0a.js +1 -0
- package/dashboard/out/_next/static/chunks/c1f54a628f27791c.js +1 -0
- package/dashboard/out/_next/static/chunks/c21916bf5d232151.js +1 -0
- package/dashboard/out/_next/static/chunks/c27e570f0fc2f78b.js +1 -0
- package/dashboard/out/_next/static/chunks/c322b8005eebc0d8.js +1 -0
- package/dashboard/out/_next/static/chunks/c3900ac2b009b764.js +1 -0
- package/dashboard/out/_next/static/chunks/c3c597c8eb77ad6b.js +1 -0
- package/dashboard/out/_next/static/chunks/c58d45c1d88e208c.js +1 -0
- package/dashboard/out/_next/static/chunks/c63875998d7a655f.js +1 -0
- package/dashboard/out/_next/static/chunks/c6eaa7c8e7209c8e.js +1 -0
- package/dashboard/out/_next/static/chunks/c6f820f71162a59a.js +1 -0
- package/dashboard/out/_next/static/chunks/c7ce0941d1dfe7ed.js +1 -0
- package/dashboard/out/_next/static/chunks/c877259f784ec8e5.js +1 -0
- package/dashboard/out/_next/static/chunks/cb8057e86aaa1c68.js +1 -0
- package/dashboard/out/_next/static/chunks/ccb4e306ee4859b1.js +1 -0
- package/dashboard/out/_next/static/chunks/ccde62b0fa8112eb.js +1 -0
- package/dashboard/out/_next/static/chunks/cd421f0971c63c31.js +1 -0
- package/dashboard/out/_next/static/chunks/cde0317494d4adb7.js +1 -0
- package/dashboard/out/_next/static/chunks/cfae64434b92b8b6.js +1 -0
- package/dashboard/out/_next/static/chunks/cfc0e19310ec0a36.js +1 -0
- package/dashboard/out/_next/static/chunks/d0f344c7d0bcb59a.js +1 -0
- package/dashboard/out/_next/static/chunks/d1871c73e74e1f50.js +1 -0
- package/dashboard/out/_next/static/chunks/d18f658850711061.js +1 -0
- package/dashboard/out/_next/static/chunks/d25d9c48880697f6.js +1 -0
- package/dashboard/out/_next/static/chunks/d3f0a72bbc3060b0.js +1 -0
- package/dashboard/out/_next/static/chunks/d5fca429bc89060b.js +1 -0
- package/dashboard/out/_next/static/chunks/d6a6c75c9a4bef49.js +1 -0
- package/dashboard/out/_next/static/chunks/d6a8f93aa8f1e345.js +1 -0
- package/dashboard/out/_next/static/chunks/d7deab676b19ba0e.js +1 -0
- package/dashboard/out/_next/static/chunks/d80030dde88cec85.js +1 -0
- package/dashboard/out/_next/static/chunks/da84f9774c076ff1.js +1 -0
- package/dashboard/out/_next/static/chunks/da98ecc6b9b0ceb2.js +1 -0
- package/dashboard/out/_next/static/chunks/db550aabce60b912.js +4 -0
- package/dashboard/out/_next/static/chunks/dc87f77fcc029482.js +1 -0
- package/dashboard/out/_next/static/chunks/dd05f30acdb63a53.js +1 -0
- package/dashboard/out/_next/static/chunks/dea150155499b903.js +1 -0
- package/dashboard/out/_next/static/chunks/df4358754ff1d5e8.js +1 -0
- package/dashboard/out/_next/static/chunks/df8b21ff12d23b0e.js +1 -0
- package/dashboard/out/_next/static/chunks/df8fc68d77debf2c.js +1 -0
- package/dashboard/out/_next/static/chunks/df9ed1ffec90589f.js +1 -0
- package/dashboard/out/_next/static/chunks/dfa65ece1598f934.js +1 -0
- package/dashboard/out/_next/static/chunks/dfaab5cdd41a20d9.js +1 -0
- package/dashboard/out/_next/static/chunks/e00c03c942b5ae3e.js +1 -0
- package/dashboard/out/_next/static/chunks/e02d27cf679bece1.js +1 -0
- package/dashboard/out/_next/static/chunks/e0b747585f78513a.js +1 -0
- package/dashboard/out/_next/static/chunks/e19b71ca23fd7df0.js +1 -0
- package/dashboard/out/_next/static/chunks/e21cbe89d0cda120.js +1 -0
- package/dashboard/out/_next/static/chunks/e2a0386930edd34f.js +1 -0
- package/dashboard/out/_next/static/chunks/e375de19a36e2f38.js +1 -0
- package/dashboard/out/_next/static/chunks/e5241b4a6496a9e0.js +1 -0
- package/dashboard/out/_next/static/chunks/e569dd07c96eb4dd.js +1 -0
- package/dashboard/out/_next/static/chunks/e61c3ea91233f948.js +1 -0
- package/dashboard/out/_next/static/chunks/e73b42c5d34c4c28.js +1 -0
- package/dashboard/out/_next/static/chunks/e7c5b2105f651e87.js +1 -0
- package/dashboard/out/_next/static/chunks/e878e58a69bb01b6.js +1 -0
- package/dashboard/out/_next/static/chunks/e8f090a4dc571062.js +1 -0
- package/dashboard/out/_next/static/chunks/e98dc34dac91cdad.js +1 -0
- package/dashboard/out/_next/static/chunks/ea67cf7a0c5e7233.js +1 -0
- package/dashboard/out/_next/static/chunks/eacc98a1e3512fa9.js +1 -0
- package/dashboard/out/_next/static/chunks/eae9cd74d9bbed8b.js +1 -0
- package/dashboard/out/_next/static/chunks/eb187fd345852b34.js +1 -0
- package/dashboard/out/_next/static/chunks/ecea46df7a40b427.js +1 -0
- package/dashboard/out/_next/static/chunks/ecf48b83f4245d9c.js +1 -0
- package/dashboard/out/_next/static/chunks/eda41732bf966c6d.js +1 -0
- package/dashboard/out/_next/static/chunks/ee174a433049f43a.js +1 -0
- package/dashboard/out/_next/static/chunks/ee66b2d3b76de72e.js +1 -0
- package/dashboard/out/_next/static/chunks/ef058d3041c3d8ab.js +1 -0
- package/dashboard/out/_next/static/chunks/f3544b800b0d4969.js +1 -0
- package/dashboard/out/_next/static/chunks/f3b280e604b97509.js +1 -0
- package/dashboard/out/_next/static/chunks/f42e1d3693232993.js +1 -0
- package/dashboard/out/_next/static/chunks/f5a5fdd80166d304.js +1 -0
- package/dashboard/out/_next/static/chunks/f60adc5dd6169316.js +1 -0
- package/dashboard/out/_next/static/chunks/f6255bc2e6c68ce8.js +1 -0
- package/dashboard/out/_next/static/chunks/f6bb677d2d8840db.js +1 -0
- package/dashboard/out/_next/static/chunks/f6f3bf016239c33a.js +1 -0
- package/dashboard/out/_next/static/chunks/f6f57a46cd074bfe.js +1 -0
- package/dashboard/out/_next/static/chunks/f7feb9c1199e8cf6.js +1 -0
- package/dashboard/out/_next/static/chunks/f831087bf964cc3d.js +1 -0
- package/dashboard/out/_next/static/chunks/fa5a2f7244935aa2.js +1 -0
- package/dashboard/out/_next/static/chunks/fa73f36df4c38fa0.js +1 -0
- package/dashboard/out/_next/static/chunks/faa1461722d6a88d.js +1 -0
- package/dashboard/out/_next/static/chunks/fbf706b9a0edb7e6.js +1 -0
- package/dashboard/out/_next/static/chunks/fc2b8c8b7446bbc2.js +1 -0
- package/dashboard/out/_next/static/chunks/fd516f3930c128c1.js +1 -0
- package/dashboard/out/_next/static/chunks/fd663e7ff8de4887.js +1 -0
- package/dashboard/out/_next/static/chunks/fe5b656cc9bdd60e.js +1 -0
- package/dashboard/out/_next/static/chunks/turbopack-2f5dec4a537c0ee7.js +4 -0
- package/dashboard/out/_next/static/media/02263ebadd758ea4-s.8da66e7f.woff2 +0 -0
- package/dashboard/out/_next/static/media/2fe89d53234c61d4-s.cb53f04b.woff2 +0 -0
- package/dashboard/out/_next/static/media/59b15b4bcd7b1eb5-s.afa48be3.woff2 +0 -0
- package/dashboard/out/_next/static/media/5e05ae5b48faa55e-s.6b5ea6af.woff2 +0 -0
- package/dashboard/out/_next/static/media/68757d6cddeff913-s.e6cd1753.woff2 +0 -0
- package/dashboard/out/_next/static/media/99e609270109b47d-s.p.64b9304e.woff2 +0 -0
- package/dashboard/out/_next/static/media/a7afbb44bec2bb18-s.1dcddae6.woff2 +0 -0
- package/dashboard/out/_next/static/media/e390973e931a41c5-s.a82ecf4e.woff2 +0 -0
- package/dashboard/out/_next/static/media/e532fa1b9921e1cd-s.764b43eb.woff2 +0 -0
- package/dashboard/out/_next/static/media/effe91970fc4db64-s.p.19510058.woff2 +0 -0
- package/dashboard/out/_not-found/__next._full.txt +15 -0
- package/dashboard/out/_not-found/__next._head.txt +5 -0
- package/dashboard/out/_not-found/__next._index.txt +7 -0
- package/dashboard/out/_not-found/__next._not-found.__PAGE__.txt +5 -0
- package/dashboard/out/_not-found/__next._not-found.txt +4 -0
- package/dashboard/out/_not-found/__next._tree.txt +2 -0
- package/dashboard/out/_not-found/index.html +1 -0
- package/dashboard/out/_not-found/index.txt +15 -0
- package/dashboard/out/index.html +1 -0
- package/dashboard/out/index.txt +21 -0
- package/dashboard/out/project/__fallback/__next._full.txt +19 -0
- package/dashboard/out/project/__fallback/__next._head.txt +5 -0
- package/dashboard/out/project/__fallback/__next._index.txt +7 -0
- package/dashboard/out/project/__fallback/__next._tree.txt +5 -0
- package/dashboard/out/project/__fallback/__next.project.$d$name.__PAGE__.txt +7 -0
- package/dashboard/out/project/__fallback/__next.project.$d$name.txt +4 -0
- package/dashboard/out/project/__fallback/__next.project.txt +4 -0
- package/dashboard/out/project/__fallback/index.html +1 -0
- package/dashboard/out/project/__fallback/index.txt +19 -0
- package/package.json +31 -15
- package/src/__tests__/support.ts +186 -0
- package/src/attach.ts +20 -0
- package/src/cli.test.ts +314 -0
- package/src/command-center/discovery.test.ts +265 -0
- package/src/command-center/discovery.ts +296 -0
- package/src/command-center/index.ts +31 -0
- package/src/command-center/schemas.ts +57 -0
- package/src/command-center/server.test.ts +814 -0
- package/src/command-center/server.ts +1421 -0
- package/src/command-center/static.test.ts +105 -0
- package/src/command-center/static.ts +122 -0
- package/src/config-cli.test.ts +86 -0
- package/src/config.test.ts +205 -0
- package/src/config.ts +313 -0
- package/src/detect.test.ts +142 -0
- package/src/detect.ts +248 -0
- package/src/dispatch.ts +102 -0
- package/src/doctor.test.ts +119 -0
- package/src/doctor.ts +167 -0
- package/src/init.test.ts +101 -0
- package/src/init.ts +144 -0
- package/src/inspect.test.ts +67 -0
- package/src/inspect.ts +212 -0
- package/src/integration.test.ts.skip +276 -0
- package/src/js-yaml.d.ts +10 -0
- package/src/launch.test.ts +149 -0
- package/src/launch.ts +475 -0
- package/src/lib/auth/auth-service.test.ts +178 -0
- package/src/lib/auth/auth-service.ts +227 -0
- package/src/lib/auth/middleware.test.ts +119 -0
- package/src/lib/auth/middleware.ts +56 -0
- package/src/lib/auth/types.ts +13 -0
- package/src/lib/authorship.test.ts +242 -0
- package/src/lib/authorship.ts +280 -0
- package/src/lib/cast/recorder.test.ts +127 -0
- package/src/lib/cast/recorder.ts +148 -0
- package/src/lib/daemon-watchdog.ts +102 -0
- package/src/lib/daemon.ts +439 -0
- package/src/lib/dot-path.test.ts +66 -0
- package/src/lib/dot-path.ts +17 -0
- package/src/lib/errors.ts +44 -0
- package/src/lib/event-log.test.ts +189 -0
- package/src/lib/event-log.ts +237 -0
- package/src/lib/github-pr.ts +182 -0
- package/src/lib/hq/client.test.ts +154 -0
- package/src/lib/hq/client.ts +142 -0
- package/src/lib/hq/mdns.test.ts +54 -0
- package/src/lib/hq/mdns.ts +88 -0
- package/src/lib/hq/registry.test.ts +237 -0
- package/src/lib/hq/registry.ts +145 -0
- package/src/lib/hq/types.ts +44 -0
- package/src/lib/ipc/socket-protocol.test.ts +126 -0
- package/src/lib/ipc/socket-protocol.ts +324 -0
- package/src/lib/launch-plan.test.ts +119 -0
- package/src/lib/launch-plan.ts +90 -0
- package/src/lib/log.test.ts +39 -0
- package/src/lib/log.ts +71 -0
- package/src/lib/metrics.test.ts +213 -0
- package/src/lib/metrics.ts +345 -0
- package/src/lib/orchestrator.test.ts +2358 -0
- package/src/lib/orchestrator.ts +1790 -0
- package/src/lib/output.ts +76 -0
- package/src/lib/plan-store.test.ts +148 -0
- package/src/lib/plan-store.ts +131 -0
- package/src/lib/research.test.ts +208 -0
- package/src/lib/research.ts +475 -0
- package/src/lib/round-trip.test.ts +341 -0
- package/src/lib/session-monitor.test.ts +152 -0
- package/src/lib/session-monitor.ts +282 -0
- package/src/lib/session-options.test.ts +146 -0
- package/src/lib/session-options.ts +100 -0
- package/src/lib/shell.test.ts +20 -0
- package/src/lib/shell.ts +8 -0
- package/src/lib/sizes.test.ts +73 -0
- package/src/lib/sizes.ts +36 -0
- package/src/lib/skill-registry.test.ts +134 -0
- package/src/lib/skill-registry.ts +86 -0
- package/src/lib/slugify.test.ts +35 -0
- package/src/lib/slugify.ts +10 -0
- package/src/lib/task-store.ts +352 -0
- package/src/lib/tmux.test.ts +500 -0
- package/src/lib/tmux.ts +372 -0
- package/src/lib/token-tracker.test.ts +102 -0
- package/src/lib/token-tracker.ts +70 -0
- package/src/lib/tunnels/cloudflare.test.ts +73 -0
- package/src/lib/tunnels/cloudflare.ts +254 -0
- package/src/lib/tunnels/manager.test.ts +63 -0
- package/src/lib/tunnels/manager.ts +132 -0
- package/src/lib/tunnels/ngrok.test.ts +94 -0
- package/src/lib/tunnels/ngrok.ts +261 -0
- package/src/lib/tunnels/tailscale.test.ts +103 -0
- package/src/lib/tunnels/tailscale.ts +1048 -0
- package/src/lib/tunnels/types.ts +44 -0
- package/src/lib/validation.test.ts +289 -0
- package/src/lib/validation.ts +123 -0
- package/src/lib/webhook.ts +51 -0
- package/src/lib/workflow-store.ts +209 -0
- package/src/lib/ws-v3/protocol.test.ts +127 -0
- package/src/lib/ws-v3/protocol.ts +160 -0
- package/src/lib/yaml-io.test.ts +125 -0
- package/src/lib/yaml-io.ts +27 -0
- package/src/ls.test.ts +76 -0
- package/src/ls.ts +40 -0
- package/src/metrics-cli.ts +177 -0
- package/src/notify.ts +85 -0
- package/src/orchestrator-status.ts +228 -0
- package/src/plan.ts +117 -0
- package/src/postinstall.test.ts +79 -0
- package/src/remote.test.ts +115 -0
- package/src/remote.ts +148 -0
- package/src/restart.ts +24 -0
- package/src/schemas/domain.ts +422 -0
- package/src/schemas/ide-config.ts +148 -0
- package/src/schemas/index.ts +53 -0
- package/src/send.test.ts +70 -0
- package/src/send.ts +181 -0
- package/src/skill.test.ts +114 -0
- package/src/skill.ts +138 -0
- package/src/status.ts +66 -0
- package/src/stop.test.ts +151 -0
- package/src/stop.ts +28 -0
- package/src/task.test.ts +1466 -0
- package/src/task.ts +1143 -0
- package/src/tunnel.test.ts +80 -0
- package/src/tunnel.ts +133 -0
- package/src/types.ts +15 -0
- package/src/ui/index.ts +32 -0
- package/src/ui/terminal/index.ts +9 -0
- package/src/ui/types.ts +91 -0
- package/src/ui/web/base.css +80 -0
- package/src/ui/web/components/Box.tsx +59 -0
- package/src/ui/web/components/Input.tsx +32 -0
- package/src/ui/web/components/ScrollBox.tsx +60 -0
- package/src/ui/web/components/Text.tsx +28 -0
- package/src/ui/web/hooks.ts +106 -0
- package/src/ui/web/index.ts +27 -0
- package/src/ui/web/render.ts +77 -0
- package/src/ui/web/utils/color.test.ts +93 -0
- package/src/ui/web/utils/color.ts +27 -0
- package/src/validate.test.ts +385 -0
- package/src/validate.ts +217 -0
- package/src/widgets/changes/index.tsx +576 -0
- package/src/widgets/config/index.tsx +440 -0
- package/src/widgets/costs/index.tsx +216 -0
- package/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/src/widgets/explorer/footer.tsx +20 -0
- package/src/widgets/explorer/header.tsx +23 -0
- package/src/widgets/explorer/index.tsx +408 -0
- package/src/widgets/explorer/tree-model.test.ts +214 -0
- package/src/widgets/explorer/tree-model.ts +103 -0
- package/src/widgets/explorer/tree.tsx +156 -0
- package/src/widgets/lib/config-model.ts +116 -0
- package/src/widgets/lib/files.test.ts +103 -0
- package/src/widgets/lib/files.ts +88 -0
- package/src/widgets/lib/git.test.ts +151 -0
- package/src/widgets/lib/git.ts +88 -0
- package/src/widgets/lib/pane-comms.test.ts +178 -0
- package/src/widgets/lib/pane-comms.ts +207 -0
- package/src/widgets/lib/theme.ts +151 -0
- package/src/widgets/lib/watcher.ts +50 -0
- package/src/widgets/mission-control/activity-feed.tsx +59 -0
- package/src/widgets/mission-control/agent-panel.tsx +132 -0
- package/src/widgets/mission-control/command-bar.tsx +63 -0
- package/src/widgets/mission-control/index.tsx +751 -0
- package/src/widgets/mission-control/task-panel.tsx +238 -0
- package/src/widgets/preview/index.tsx +415 -0
- package/src/widgets/resolve.ts +40 -0
- package/src/widgets/setup/agent-naming.tsx +112 -0
- package/src/widgets/setup/config-tree.tsx +238 -0
- package/src/widgets/setup/detect-panel.tsx +72 -0
- package/src/widgets/setup/field-editor.tsx +265 -0
- package/src/widgets/setup/footer.tsx +107 -0
- package/src/widgets/setup/index.tsx +340 -0
- package/src/widgets/setup/layout-picker.tsx +96 -0
- package/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/src/widgets/setup/review-panel.tsx +140 -0
- package/src/widgets/setup/setup-model.test.ts +303 -0
- package/src/widgets/setup/setup-model.ts +188 -0
- package/src/widgets/tasks/index.tsx +129 -0
- package/src/widgets/tasks/task-detail.tsx +309 -0
- package/src/widgets/tasks/task-form.tsx +193 -0
- package/src/widgets/tasks/task-list.tsx +205 -0
- package/src/widgets/tasks/task-model.test.ts +473 -0
- package/src/widgets/tasks/task-model.ts +157 -0
- package/templates/AGENTS.md +20 -0
- package/templates/missions.yml +41 -0
- package/templates/skills/backend.md +30 -0
- package/templates/skills/frontend.md +30 -0
- package/templates/skills/general-worker.md +30 -0
- package/templates/skills/researcher.md +69 -0
- package/templates/skills/reviewer.md +30 -0
- package/dist/attach.d.ts +0 -3
- package/dist/attach.js +0 -14
- package/dist/config.d.ts +0 -5
- package/dist/config.js +0 -269
- package/dist/detect.d.ts +0 -15
- package/dist/detect.js +0 -228
- package/dist/doctor.d.ts +0 -3
- package/dist/doctor.js +0 -66
- package/dist/init.d.ts +0 -4
- package/dist/init.js +0 -67
- package/dist/inspect.d.ts +0 -54
- package/dist/inspect.js +0 -118
- package/dist/launch.d.ts +0 -22
- package/dist/launch.js +0 -174
- package/dist/lib/dot-path.d.ts +0 -2
- package/dist/lib/dot-path.js +0 -17
- package/dist/lib/errors.d.ts +0 -29
- package/dist/lib/errors.js +0 -37
- package/dist/lib/launch-plan.d.ts +0 -6
- package/dist/lib/launch-plan.js +0 -40
- package/dist/lib/output.d.ts +0 -9
- package/dist/lib/output.js +0 -65
- package/dist/lib/session-monitor.d.ts +0 -12
- package/dist/lib/session-monitor.js +0 -160
- package/dist/lib/session-options.d.ts +0 -13
- package/dist/lib/session-options.js +0 -85
- package/dist/lib/sizes.d.ts +0 -16
- package/dist/lib/sizes.js +0 -36
- package/dist/lib/tmux.d.ts +0 -42
- package/dist/lib/tmux.js +0 -225
- package/dist/lib/yaml-io.d.ts +0 -10
- package/dist/lib/yaml-io.js +0 -24
- package/dist/ls.d.ts +0 -3
- package/dist/ls.js +0 -35
- package/dist/restart.d.ts +0 -4
- package/dist/restart.js +0 -13
- package/dist/status.d.ts +0 -3
- package/dist/status.js +0 -29
- package/dist/stop.d.ts +0 -3
- package/dist/stop.js +0 -21
- package/dist/types.d.ts +0 -41
- package/dist/types.js +0 -1
- package/dist/validate.d.ts +0 -4
- package/dist/validate.js +0 -172
|
@@ -0,0 +1,2358 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach, expect } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
dispatch,
|
|
7
|
+
detectStalls,
|
|
8
|
+
detectCompletions,
|
|
9
|
+
buildTaskPrompt,
|
|
10
|
+
buildGoalPrompt,
|
|
11
|
+
runHook,
|
|
12
|
+
createOrchestrator,
|
|
13
|
+
isAgentPane,
|
|
14
|
+
isAgentBusy,
|
|
15
|
+
isIdleForDispatch,
|
|
16
|
+
saveOrchestratorState,
|
|
17
|
+
loadOrchestratorState,
|
|
18
|
+
syncClaims,
|
|
19
|
+
gracefulShutdown,
|
|
20
|
+
reloadConfig,
|
|
21
|
+
reconcile,
|
|
22
|
+
agentIdentifier,
|
|
23
|
+
normalizePaneTitle,
|
|
24
|
+
getPaneSpecialties,
|
|
25
|
+
dispatchGoals,
|
|
26
|
+
findBestAgent,
|
|
27
|
+
getCurrentMilestone,
|
|
28
|
+
isMilestoneReady,
|
|
29
|
+
checkMilestoneCompletion,
|
|
30
|
+
checkValidationResults,
|
|
31
|
+
buildValidationPrompt,
|
|
32
|
+
dispatchPlanning,
|
|
33
|
+
buildPlanningPrompt,
|
|
34
|
+
handleMissionComplete,
|
|
35
|
+
} from "./orchestrator.ts";
|
|
36
|
+
import { readEvents } from "./event-log.ts";
|
|
37
|
+
import { loadResearchState, saveResearchState } from "./research.ts";
|
|
38
|
+
import {
|
|
39
|
+
ensureTasksDir,
|
|
40
|
+
saveMission,
|
|
41
|
+
loadMission,
|
|
42
|
+
saveGoal,
|
|
43
|
+
saveTask,
|
|
44
|
+
loadTask,
|
|
45
|
+
loadTasks,
|
|
46
|
+
loadGoal,
|
|
47
|
+
type Goal,
|
|
48
|
+
type Mission,
|
|
49
|
+
} from "./task-store.ts";
|
|
50
|
+
import { loadValidationState, saveValidationState } from "./validation.ts";
|
|
51
|
+
import { _setExecutor, type PaneInfo } from "../widgets/lib/pane-comms.ts";
|
|
52
|
+
import {
|
|
53
|
+
makeTask,
|
|
54
|
+
makePane,
|
|
55
|
+
makeOrchestratorConfig,
|
|
56
|
+
makeOrchestratorState,
|
|
57
|
+
} from "../__tests__/support.ts";
|
|
58
|
+
|
|
59
|
+
let tmpDir: string;
|
|
60
|
+
let restoreTmux: () => void;
|
|
61
|
+
let tmuxCalls: { args: string[] }[];
|
|
62
|
+
let mockPanes: PaneInfo[];
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
tmpDir = mkdtempSync(join(tmpdir(), "tmux-ide-orch-test-"));
|
|
66
|
+
ensureTasksDir(tmpDir);
|
|
67
|
+
tmuxCalls = [];
|
|
68
|
+
mockPanes = [];
|
|
69
|
+
|
|
70
|
+
restoreTmux = _setExecutor((_cmd: string, args: string[]) => {
|
|
71
|
+
tmuxCalls.push({ args });
|
|
72
|
+
// Mock list-panes to return mockPanes as tab-separated output
|
|
73
|
+
if (args[0] === "list-panes") {
|
|
74
|
+
return mockPanes
|
|
75
|
+
.map(
|
|
76
|
+
(p) =>
|
|
77
|
+
`${p.id}\t${p.index}\t${p.title}\t${p.currentCommand}\t${p.width}\t${p.height}\t${p.active ? "1" : "0"}\t${p.role ?? ""}\t${p.name ?? ""}\t${p.type ?? ""}`,
|
|
78
|
+
)
|
|
79
|
+
.join("\n");
|
|
80
|
+
}
|
|
81
|
+
return "";
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
restoreTmux();
|
|
87
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("dispatch", () => {
|
|
91
|
+
it("assigns highest-priority task to idle agent", () => {
|
|
92
|
+
const task1 = makeTask({ id: "001", title: "Low priority", priority: 2 });
|
|
93
|
+
const task2 = makeTask({ id: "002", title: "High priority", priority: 1 });
|
|
94
|
+
saveTask(tmpDir, task1);
|
|
95
|
+
saveTask(tmpDir, task2);
|
|
96
|
+
|
|
97
|
+
const pane = makePane({ id: "%1", index: 0, title: "Agent 1", currentCommand: "zsh" });
|
|
98
|
+
const expectedName = agentIdentifier(pane);
|
|
99
|
+
const panes: PaneInfo[] = [pane];
|
|
100
|
+
mockPanes = panes;
|
|
101
|
+
|
|
102
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
103
|
+
const state = makeOrchestratorState();
|
|
104
|
+
|
|
105
|
+
dispatch(config, state, [task1, task2], panes);
|
|
106
|
+
|
|
107
|
+
// Task 002 (higher priority) should be assigned first
|
|
108
|
+
const assigned = loadTask(tmpDir, "002");
|
|
109
|
+
expect(assigned?.assignee).toBe(expectedName);
|
|
110
|
+
expect(assigned?.status).toBe("in-progress");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("skips master pane", () => {
|
|
114
|
+
const task = makeTask();
|
|
115
|
+
saveTask(tmpDir, task);
|
|
116
|
+
|
|
117
|
+
const agentPane = makePane({ id: "%1", index: 1, title: "Agent 1", currentCommand: "zsh" });
|
|
118
|
+
const expectedName = agentIdentifier(agentPane);
|
|
119
|
+
const panes: PaneInfo[] = [
|
|
120
|
+
makePane({ id: "%0", index: 0, title: "Master", currentCommand: "zsh", role: "lead" }),
|
|
121
|
+
agentPane,
|
|
122
|
+
];
|
|
123
|
+
mockPanes = panes;
|
|
124
|
+
|
|
125
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
126
|
+
const state = makeOrchestratorState();
|
|
127
|
+
|
|
128
|
+
dispatch(config, state, [task], panes);
|
|
129
|
+
|
|
130
|
+
const assigned = loadTask(tmpDir, "001");
|
|
131
|
+
expect(assigned?.assignee).toBe(expectedName);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("does not assign when no idle agents", () => {
|
|
135
|
+
const task = makeTask();
|
|
136
|
+
saveTask(tmpDir, task);
|
|
137
|
+
|
|
138
|
+
// Agent pane is busy (spinner in title indicates active work)
|
|
139
|
+
const panes: PaneInfo[] = [
|
|
140
|
+
makePane({ id: "%1", title: "⠙ Agent 1", currentCommand: "claude" }),
|
|
141
|
+
];
|
|
142
|
+
mockPanes = panes;
|
|
143
|
+
|
|
144
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
145
|
+
const state = makeOrchestratorState();
|
|
146
|
+
|
|
147
|
+
dispatch(config, state, [task], panes);
|
|
148
|
+
|
|
149
|
+
const notAssigned = loadTask(tmpDir, "001");
|
|
150
|
+
expect(notAssigned?.assignee).toBe(null);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("does not assign when no todo tasks", () => {
|
|
154
|
+
const task = makeTask({ status: "in-progress", assignee: "Agent 1" });
|
|
155
|
+
saveTask(tmpDir, task);
|
|
156
|
+
|
|
157
|
+
const panes: PaneInfo[] = [makePane({ id: "%2", title: "Agent 2", currentCommand: "zsh" })];
|
|
158
|
+
mockPanes = panes;
|
|
159
|
+
|
|
160
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
161
|
+
const state = makeOrchestratorState();
|
|
162
|
+
|
|
163
|
+
dispatch(config, state, [task], panes);
|
|
164
|
+
|
|
165
|
+
// No send-keys calls for Agent 2
|
|
166
|
+
const sendCalls = tmuxCalls.filter(
|
|
167
|
+
(c) => c.args.includes("send-keys") && c.args.includes("%2"),
|
|
168
|
+
);
|
|
169
|
+
expect(sendCalls.length).toBe(0);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("detectStalls", () => {
|
|
174
|
+
it("sends nudge after timeout", () => {
|
|
175
|
+
const pane = makePane({ id: "%1", index: 0, title: "Agent 1" });
|
|
176
|
+
const name = agentIdentifier(pane);
|
|
177
|
+
const task = makeTask({ status: "in-progress", assignee: name });
|
|
178
|
+
|
|
179
|
+
const panes: PaneInfo[] = [pane];
|
|
180
|
+
|
|
181
|
+
const config = makeOrchestratorConfig(tmpDir, { stallTimeout: 1000 });
|
|
182
|
+
const state = makeOrchestratorState({
|
|
183
|
+
lastActivity: new Map([["%1", Date.now() - 2000]]),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
detectStalls(config, state, [task], panes);
|
|
187
|
+
|
|
188
|
+
// Should have sent a nudge via send-keys
|
|
189
|
+
const sendCalls = tmuxCalls.filter((c) => c.args.includes("send-keys"));
|
|
190
|
+
expect(sendCalls.length > 0).toBeTruthy();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("does not nudge before timeout", () => {
|
|
194
|
+
const pane = makePane({ id: "%1", index: 0, title: "Agent 1" });
|
|
195
|
+
const name = agentIdentifier(pane);
|
|
196
|
+
const task = makeTask({ status: "in-progress", assignee: name });
|
|
197
|
+
|
|
198
|
+
const panes: PaneInfo[] = [pane];
|
|
199
|
+
|
|
200
|
+
const config = makeOrchestratorConfig(tmpDir, { stallTimeout: 300000 });
|
|
201
|
+
const state = makeOrchestratorState({
|
|
202
|
+
lastActivity: new Map([["%1", Date.now()]]),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
detectStalls(config, state, [task], panes);
|
|
206
|
+
|
|
207
|
+
const sendCalls = tmuxCalls.filter((c) => c.args.includes("send-keys"));
|
|
208
|
+
expect(sendCalls.length).toBe(0);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("detectCompletions", () => {
|
|
213
|
+
it("notifies master when task goes from in-progress to done", () => {
|
|
214
|
+
const task = makeTask({ status: "done", assignee: "Agent 1", proof: { notes: "tests pass" } });
|
|
215
|
+
|
|
216
|
+
const panes: PaneInfo[] = [makePane({ id: "%0", title: "Master" })];
|
|
217
|
+
|
|
218
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
219
|
+
const state = makeOrchestratorState({
|
|
220
|
+
previousTasks: new Map([["001", "in-progress"]]),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
detectCompletions(config, state, [task], panes);
|
|
224
|
+
|
|
225
|
+
// Should have sent notification to master
|
|
226
|
+
const sendCalls = tmuxCalls.filter(
|
|
227
|
+
(c) => c.args.includes("send-keys") && c.args.includes("%0"),
|
|
228
|
+
);
|
|
229
|
+
expect(sendCalls.length > 0).toBeTruthy();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("does not notify when task was already done", () => {
|
|
233
|
+
const task = makeTask({ status: "done" });
|
|
234
|
+
|
|
235
|
+
const panes: PaneInfo[] = [makePane({ id: "%0", title: "Master" })];
|
|
236
|
+
|
|
237
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
238
|
+
const state = makeOrchestratorState({
|
|
239
|
+
previousTasks: new Map([["001", "done"]]),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
detectCompletions(config, state, [task], panes);
|
|
243
|
+
|
|
244
|
+
const sendCalls = tmuxCalls.filter((c) => c.args.includes("send-keys"));
|
|
245
|
+
expect(sendCalls.length).toBe(0);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("processes completed research tasks into the library and clears active state", () => {
|
|
249
|
+
const task = makeTask({
|
|
250
|
+
id: "099",
|
|
251
|
+
title: "Research: periodic",
|
|
252
|
+
status: "done",
|
|
253
|
+
assignee: "Researcher",
|
|
254
|
+
tags: ["research", "periodic"],
|
|
255
|
+
updated: "2026-01-03T00:00:00Z",
|
|
256
|
+
salientSummary: "Found a contract gap",
|
|
257
|
+
});
|
|
258
|
+
saveResearchState(tmpDir, {
|
|
259
|
+
lastResearchAt: {},
|
|
260
|
+
missionStartAnalyzed: false,
|
|
261
|
+
milestoneTaskCounts: {},
|
|
262
|
+
activeResearchTaskId: "099",
|
|
263
|
+
retryWindow: [],
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const panes: PaneInfo[] = [makePane({ id: "%0", title: "Master" })];
|
|
267
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
268
|
+
const state = makeOrchestratorState({
|
|
269
|
+
previousTasks: new Map([["099", "in-progress"]]),
|
|
270
|
+
});
|
|
271
|
+
const researchState = loadResearchState(tmpDir);
|
|
272
|
+
|
|
273
|
+
detectCompletions(config, state, [task], panes, researchState);
|
|
274
|
+
|
|
275
|
+
expect(loadResearchState(tmpDir).activeResearchTaskId).toBe(null);
|
|
276
|
+
expect(existsSync(join(tmpDir, ".tmux-ide", "library", "research-findings.md"))).toBeTruthy();
|
|
277
|
+
expect(
|
|
278
|
+
readFileSync(join(tmpDir, ".tmux-ide", "library", "research-findings.md"), "utf-8"),
|
|
279
|
+
).toContain("Found a contract gap");
|
|
280
|
+
expect(readEvents(tmpDir).some((event) => event.type === "research_finding")).toBeTruthy();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("buildTaskPrompt", () => {
|
|
285
|
+
it("includes mission + goal + task context", () => {
|
|
286
|
+
saveMission(tmpDir, {
|
|
287
|
+
title: "Build auth",
|
|
288
|
+
description: "Before Q2 audit",
|
|
289
|
+
status: "active",
|
|
290
|
+
branch: null,
|
|
291
|
+
milestones: [],
|
|
292
|
+
created: "2026-01-01T00:00:00Z",
|
|
293
|
+
updated: "2026-01-01T00:00:00Z",
|
|
294
|
+
});
|
|
295
|
+
saveGoal(tmpDir, {
|
|
296
|
+
id: "01",
|
|
297
|
+
title: "Token storage",
|
|
298
|
+
description: "",
|
|
299
|
+
status: "in-progress",
|
|
300
|
+
acceptance: "All tokens encrypted",
|
|
301
|
+
priority: 1,
|
|
302
|
+
created: "2026-01-01T00:00:00Z",
|
|
303
|
+
updated: "2026-01-01T00:00:00Z",
|
|
304
|
+
assignee: null,
|
|
305
|
+
specialty: null,
|
|
306
|
+
milestone: null,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const task = makeTask({
|
|
310
|
+
title: "Fix middleware",
|
|
311
|
+
goal: "01",
|
|
312
|
+
tags: ["security"],
|
|
313
|
+
priority: 1,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const prompt = buildTaskPrompt(tmpDir, task);
|
|
317
|
+
|
|
318
|
+
expect(prompt.includes("Mission: Build auth")).toBeTruthy();
|
|
319
|
+
expect(prompt.includes("Goal: Token storage")).toBeTruthy();
|
|
320
|
+
expect(prompt.includes("Acceptance: All tokens encrypted")).toBeTruthy();
|
|
321
|
+
expect(prompt.includes("Your Task: Fix middleware")).toBeTruthy();
|
|
322
|
+
expect(prompt.includes("Priority: P1")).toBeTruthy();
|
|
323
|
+
expect(prompt.includes("Tags: security")).toBeTruthy();
|
|
324
|
+
expect(prompt.includes(`Workspace: ${tmpDir}`)).toBeTruthy();
|
|
325
|
+
expect(prompt.includes("tmux-ide task done 001")).toBeTruthy();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("works without mission or goal", () => {
|
|
329
|
+
const task = makeTask({ title: "Standalone task" });
|
|
330
|
+
const prompt = buildTaskPrompt(tmpDir, task);
|
|
331
|
+
expect(prompt.includes("Your Task: Standalone task")).toBeTruthy();
|
|
332
|
+
expect(!prompt.includes("Mission:")).toBeTruthy();
|
|
333
|
+
expect(!prompt.includes("Goal:")).toBeTruthy();
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("runHook", () => {
|
|
338
|
+
it("returns ok:true for successful command", () => {
|
|
339
|
+
const result = runHook("true", tmpDir);
|
|
340
|
+
expect(result).toEqual({ ok: true });
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("returns ok:false with error for failing command", () => {
|
|
344
|
+
const result = runHook("false", tmpDir);
|
|
345
|
+
expect(result.ok).toBe(false);
|
|
346
|
+
expect("error" in result && result.error.length > 0).toBeTruthy();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("runs command in the specified cwd", () => {
|
|
350
|
+
// Create a marker file, then check it exists via the hook
|
|
351
|
+
writeFileSync(join(tmpDir, "marker.txt"), "hello");
|
|
352
|
+
const result = runHook("test -f marker.txt", tmpDir);
|
|
353
|
+
expect(result).toEqual({ ok: true });
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it("fails when cwd does not contain expected file", () => {
|
|
357
|
+
const result = runHook("test -f nonexistent.txt", tmpDir);
|
|
358
|
+
expect(result.ok).toBe(false);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe("dispatch with hooks", () => {
|
|
363
|
+
it("skips task when before_run fails", () => {
|
|
364
|
+
const task = makeTask();
|
|
365
|
+
saveTask(tmpDir, task);
|
|
366
|
+
|
|
367
|
+
const panes: PaneInfo[] = [makePane({ id: "%1", title: "Agent 1", currentCommand: "zsh" })];
|
|
368
|
+
mockPanes = panes;
|
|
369
|
+
|
|
370
|
+
// before_run that always fails
|
|
371
|
+
const config = makeOrchestratorConfig(tmpDir, { beforeRun: "false" });
|
|
372
|
+
const state = makeOrchestratorState();
|
|
373
|
+
|
|
374
|
+
dispatch(config, state, [task], panes);
|
|
375
|
+
|
|
376
|
+
// Task should NOT be assigned
|
|
377
|
+
const loaded = loadTask(tmpDir, "001");
|
|
378
|
+
expect(loaded?.assignee).toBe(null);
|
|
379
|
+
expect(loaded?.status).toBe("todo");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("dispatches task when before_run succeeds", () => {
|
|
383
|
+
const task = makeTask();
|
|
384
|
+
saveTask(tmpDir, task);
|
|
385
|
+
|
|
386
|
+
const pane = makePane({ id: "%1", index: 0, title: "Agent 1", currentCommand: "zsh" });
|
|
387
|
+
const expectedName = agentIdentifier(pane);
|
|
388
|
+
const panes: PaneInfo[] = [pane];
|
|
389
|
+
mockPanes = panes;
|
|
390
|
+
|
|
391
|
+
// before_run that always succeeds
|
|
392
|
+
const config = makeOrchestratorConfig(tmpDir, { beforeRun: "true" });
|
|
393
|
+
const state = makeOrchestratorState();
|
|
394
|
+
|
|
395
|
+
dispatch(config, state, [task], panes);
|
|
396
|
+
|
|
397
|
+
const loaded = loadTask(tmpDir, "001");
|
|
398
|
+
expect(loaded?.assignee).toBe(expectedName);
|
|
399
|
+
expect(loaded?.status).toBe("in-progress");
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe("detectCompletions with after_run", () => {
|
|
404
|
+
it("runs after_run hook when task completes", () => {
|
|
405
|
+
const task = makeTask({
|
|
406
|
+
status: "done",
|
|
407
|
+
assignee: "Agent 1",
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const panes: PaneInfo[] = [makePane({ id: "%0", title: "Master" })];
|
|
411
|
+
|
|
412
|
+
// after_run creates a marker file — proves it ran in the project dir
|
|
413
|
+
const config = makeOrchestratorConfig(tmpDir, { afterRun: "touch after_run_marker" });
|
|
414
|
+
const state = makeOrchestratorState({
|
|
415
|
+
previousTasks: new Map([["001", "in-progress"]]),
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
detectCompletions(config, state, [task], panes);
|
|
419
|
+
|
|
420
|
+
expect(existsSync(join(tmpDir, "after_run_marker"))).toBeTruthy();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("does not crash when after_run fails", () => {
|
|
424
|
+
const task = makeTask({
|
|
425
|
+
status: "done",
|
|
426
|
+
assignee: "Agent 1",
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const panes: PaneInfo[] = [makePane({ id: "%0", title: "Master" })];
|
|
430
|
+
|
|
431
|
+
// after_run that fails — should not throw
|
|
432
|
+
const config = makeOrchestratorConfig(tmpDir, { afterRun: "false" });
|
|
433
|
+
const state = makeOrchestratorState({
|
|
434
|
+
previousTasks: new Map([["001", "in-progress"]]),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Should not throw
|
|
438
|
+
detectCompletions(config, state, [task], panes);
|
|
439
|
+
|
|
440
|
+
// Master should still get notified despite hook failure
|
|
441
|
+
const sendCalls = tmuxCalls.filter(
|
|
442
|
+
(c) => c.args.includes("send-keys") && c.args.includes("%0"),
|
|
443
|
+
);
|
|
444
|
+
expect(sendCalls.length > 0).toBeTruthy();
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe("isAgentPane", () => {
|
|
449
|
+
it("detects claude command", () => {
|
|
450
|
+
expect(isAgentPane(makePane({ currentCommand: "claude" }))).toBeTruthy();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("detects codex command", () => {
|
|
454
|
+
expect(isAgentPane(makePane({ currentCommand: "codex" }))).toBeTruthy();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("detects Claude Code via version string + title", () => {
|
|
458
|
+
expect(isAgentPane(makePane({ currentCommand: "2.1.80", title: "Claude Code" }))).toBeTruthy();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("detects Claude Code via title pattern", () => {
|
|
462
|
+
expect(isAgentPane(makePane({ currentCommand: "node", title: "Claude Code" }))).toBeTruthy();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("does not match a plain shell", () => {
|
|
466
|
+
expect(!isAgentPane(makePane({ currentCommand: "zsh", title: "Shell" }))).toBeTruthy();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("matches version string regardless of title (Claude Code reports version as command)", () => {
|
|
470
|
+
expect(isAgentPane(makePane({ currentCommand: "2.1.80", title: "Dev Server" }))).toBeTruthy();
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe("isAgentBusy", () => {
|
|
475
|
+
it("returns true when spinner in title", () => {
|
|
476
|
+
expect(isAgentBusy(makePane({ title: "⠙ Working..." }))).toBeTruthy();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("returns false for normal title", () => {
|
|
480
|
+
expect(!isAgentBusy(makePane({ title: "Claude Code" }))).toBeTruthy();
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe("isIdleForDispatch", () => {
|
|
485
|
+
it("returns true for shell command", () => {
|
|
486
|
+
expect(isIdleForDispatch(makePane({ currentCommand: "zsh" }))).toBeTruthy();
|
|
487
|
+
expect(isIdleForDispatch(makePane({ currentCommand: "bash" }))).toBeTruthy();
|
|
488
|
+
expect(isIdleForDispatch(makePane({ currentCommand: "fish" }))).toBeTruthy();
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("returns true for agent pane without spinner", () => {
|
|
492
|
+
expect(
|
|
493
|
+
isIdleForDispatch(makePane({ currentCommand: "claude", title: "Agent 1" })),
|
|
494
|
+
).toBeTruthy();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("returns true for Claude with version command and no spinner", () => {
|
|
498
|
+
expect(
|
|
499
|
+
isIdleForDispatch(makePane({ currentCommand: "2.1.80", title: "Claude Code" })),
|
|
500
|
+
).toBeTruthy();
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("returns false for agent pane with spinner", () => {
|
|
504
|
+
expect(
|
|
505
|
+
!isIdleForDispatch(makePane({ currentCommand: "claude", title: "⠹ Thinking..." })),
|
|
506
|
+
).toBeTruthy();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("returns false for non-agent non-shell command", () => {
|
|
510
|
+
expect(!isIdleForDispatch(makePane({ currentCommand: "vim", title: "Editor" }))).toBeTruthy();
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe("claim locking", () => {
|
|
515
|
+
it("prevents double-dispatch of same task across two calls", () => {
|
|
516
|
+
const task = makeTask();
|
|
517
|
+
saveTask(tmpDir, task);
|
|
518
|
+
|
|
519
|
+
const panes: PaneInfo[] = [
|
|
520
|
+
makePane({ id: "%1", title: "Agent 1", currentCommand: "zsh" }),
|
|
521
|
+
makePane({ id: "%2", title: "Agent 2", currentCommand: "zsh" }),
|
|
522
|
+
];
|
|
523
|
+
mockPanes = panes;
|
|
524
|
+
|
|
525
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
526
|
+
const state = makeOrchestratorState();
|
|
527
|
+
|
|
528
|
+
// First dispatch claims the task
|
|
529
|
+
dispatch(config, state, [task], panes);
|
|
530
|
+
expect(state.claimedTasks.has("001")).toBeTruthy();
|
|
531
|
+
|
|
532
|
+
// Reset task to todo to simulate a race (file not yet updated)
|
|
533
|
+
const raceTask = makeTask({ status: "todo", assignee: null });
|
|
534
|
+
|
|
535
|
+
// Second dispatch should skip claimed task
|
|
536
|
+
dispatch(config, state, [raceTask], panes);
|
|
537
|
+
|
|
538
|
+
// Only one send-keys call with the task prompt (not two)
|
|
539
|
+
const sendCalls = tmuxCalls.filter((c) => c.args.includes("send-keys"));
|
|
540
|
+
// First dispatch: send-keys text + send-keys Enter = 2 calls
|
|
541
|
+
expect(sendCalls.length).toBe(2);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("clears claim on task completion", () => {
|
|
545
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
546
|
+
const state = makeOrchestratorState({
|
|
547
|
+
previousTasks: new Map([["001", "in-progress"]]),
|
|
548
|
+
claimedTasks: new Set(["001"]),
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
const task = makeTask({
|
|
552
|
+
status: "done",
|
|
553
|
+
assignee: "Agent 1",
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const panes: PaneInfo[] = [makePane({ id: "%0", title: "Master" })];
|
|
557
|
+
|
|
558
|
+
detectCompletions(config, state, [task], panes);
|
|
559
|
+
|
|
560
|
+
expect(!state.claimedTasks.has("001")).toBeTruthy();
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it("releases claim when before_run hook fails", () => {
|
|
564
|
+
const task = makeTask();
|
|
565
|
+
saveTask(tmpDir, task);
|
|
566
|
+
|
|
567
|
+
const panes: PaneInfo[] = [makePane({ id: "%1", title: "Agent 1", currentCommand: "zsh" })];
|
|
568
|
+
mockPanes = panes;
|
|
569
|
+
|
|
570
|
+
const config = makeOrchestratorConfig(tmpDir, { beforeRun: "false" });
|
|
571
|
+
const state = makeOrchestratorState();
|
|
572
|
+
|
|
573
|
+
dispatch(config, state, [task], panes);
|
|
574
|
+
|
|
575
|
+
// Claim should be released so the task can be retried
|
|
576
|
+
expect(!state.claimedTasks.has("001")).toBeTruthy();
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
describe("createOrchestrator timer", () => {
|
|
581
|
+
it("auto-assigns a task within poll interval", async () => {
|
|
582
|
+
const task = makeTask();
|
|
583
|
+
saveTask(tmpDir, task);
|
|
584
|
+
|
|
585
|
+
const pane = makePane({ id: "%1", index: 0, title: "Agent 1", currentCommand: "zsh" });
|
|
586
|
+
const expectedName = agentIdentifier(pane);
|
|
587
|
+
mockPanes = [pane];
|
|
588
|
+
|
|
589
|
+
const config = makeOrchestratorConfig(tmpDir, { pollInterval: 50, masterPane: null });
|
|
590
|
+
|
|
591
|
+
const stop = createOrchestrator(config);
|
|
592
|
+
|
|
593
|
+
// Wait for a few ticks
|
|
594
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
595
|
+
|
|
596
|
+
stop();
|
|
597
|
+
|
|
598
|
+
const loaded = loadTask(tmpDir, "001");
|
|
599
|
+
expect(loaded?.status).toBe("in-progress");
|
|
600
|
+
expect(loaded?.assignee).toBe(expectedName);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it("stops polling when returned function is called", async () => {
|
|
604
|
+
mockPanes = [];
|
|
605
|
+
|
|
606
|
+
const config = makeOrchestratorConfig(tmpDir, { pollInterval: 50 });
|
|
607
|
+
|
|
608
|
+
const stop = createOrchestrator(config);
|
|
609
|
+
stop();
|
|
610
|
+
|
|
611
|
+
// Create a task after stopping — it should not be assigned
|
|
612
|
+
const task = makeTask();
|
|
613
|
+
saveTask(tmpDir, task);
|
|
614
|
+
|
|
615
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
616
|
+
|
|
617
|
+
const loaded = loadTask(tmpDir, "001");
|
|
618
|
+
expect(loaded?.status).toBe("todo");
|
|
619
|
+
expect(loaded?.assignee).toBe(null);
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
describe("dispatch with version-string agent", () => {
|
|
624
|
+
it("assigns task to agent reporting version string as command", () => {
|
|
625
|
+
const task = makeTask();
|
|
626
|
+
saveTask(tmpDir, task);
|
|
627
|
+
|
|
628
|
+
// Simulate Claude Code showing version as command
|
|
629
|
+
const pane = makePane({ id: "%1", index: 0, title: "Claude Code", currentCommand: "2.1.80" });
|
|
630
|
+
const expectedName = agentIdentifier(pane);
|
|
631
|
+
const panes: PaneInfo[] = [pane];
|
|
632
|
+
mockPanes = panes;
|
|
633
|
+
|
|
634
|
+
const config = makeOrchestratorConfig(tmpDir, { masterPane: null });
|
|
635
|
+
const state = makeOrchestratorState();
|
|
636
|
+
|
|
637
|
+
dispatch(config, state, [task], panes);
|
|
638
|
+
|
|
639
|
+
const loaded = loadTask(tmpDir, "001");
|
|
640
|
+
expect(loaded?.assignee).toBe(expectedName);
|
|
641
|
+
expect(loaded?.status).toBe("in-progress");
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("does not assign to busy agent with spinner", () => {
|
|
645
|
+
const task = makeTask();
|
|
646
|
+
saveTask(tmpDir, task);
|
|
647
|
+
|
|
648
|
+
const panes: PaneInfo[] = [
|
|
649
|
+
makePane({ id: "%1", title: "⠙ Thinking...", currentCommand: "2.1.80" }),
|
|
650
|
+
];
|
|
651
|
+
mockPanes = panes;
|
|
652
|
+
|
|
653
|
+
const config = makeOrchestratorConfig(tmpDir, { masterPane: null });
|
|
654
|
+
const state = makeOrchestratorState();
|
|
655
|
+
|
|
656
|
+
dispatch(config, state, [task], panes);
|
|
657
|
+
|
|
658
|
+
const loaded = loadTask(tmpDir, "001");
|
|
659
|
+
expect(loaded?.assignee).toBe(null);
|
|
660
|
+
expect(loaded?.status).toBe("todo");
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
describe("reloadConfig", () => {
|
|
665
|
+
it("updates pollInterval", () => {
|
|
666
|
+
const config = makeOrchestratorConfig(tmpDir, { pollInterval: 5000 });
|
|
667
|
+
reloadConfig(config, { pollInterval: 1000 });
|
|
668
|
+
expect(config.pollInterval).toBe(1000);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it("updates stallTimeout", () => {
|
|
672
|
+
const config = makeOrchestratorConfig(tmpDir, { stallTimeout: 300000 });
|
|
673
|
+
reloadConfig(config, { stallTimeout: 60000 });
|
|
674
|
+
expect(config.stallTimeout).toBe(60000);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it("updates maxConcurrentAgents", () => {
|
|
678
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
679
|
+
reloadConfig(config, { maxConcurrentAgents: 3 });
|
|
680
|
+
expect(config.maxConcurrentAgents).toBe(3);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it("updates multiple fields at once", () => {
|
|
684
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
685
|
+
reloadConfig(config, {
|
|
686
|
+
pollInterval: 2000,
|
|
687
|
+
stallTimeout: 120000,
|
|
688
|
+
autoDispatch: false,
|
|
689
|
+
});
|
|
690
|
+
expect(config.pollInterval).toBe(2000);
|
|
691
|
+
expect(config.stallTimeout).toBe(120000);
|
|
692
|
+
expect(config.autoDispatch).toBe(false);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("preserves fields not in the patch", () => {
|
|
696
|
+
const config = makeOrchestratorConfig(tmpDir, { pollInterval: 5000, stallTimeout: 300000 });
|
|
697
|
+
reloadConfig(config, { pollInterval: 1000 });
|
|
698
|
+
expect(config.stallTimeout).toBe(300000); // unchanged
|
|
699
|
+
expect(config.session).toBe("test"); // unchanged
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
describe("event logging integration", () => {
|
|
704
|
+
it("logs dispatch events", () => {
|
|
705
|
+
const task = makeTask();
|
|
706
|
+
saveTask(tmpDir, task);
|
|
707
|
+
|
|
708
|
+
const pane = makePane({ id: "%1", index: 0, title: "Agent 1", currentCommand: "zsh" });
|
|
709
|
+
const panes: PaneInfo[] = [pane];
|
|
710
|
+
mockPanes = panes;
|
|
711
|
+
|
|
712
|
+
const config = makeOrchestratorConfig(tmpDir, { masterPane: null });
|
|
713
|
+
const state = makeOrchestratorState();
|
|
714
|
+
|
|
715
|
+
dispatch(config, state, [task], panes);
|
|
716
|
+
|
|
717
|
+
const events = readEvents(tmpDir);
|
|
718
|
+
const dispatchEvent = events.find((e) => e.type === "dispatch");
|
|
719
|
+
expect(dispatchEvent).toBeTruthy();
|
|
720
|
+
expect(dispatchEvent!.taskId).toBe("001");
|
|
721
|
+
expect(dispatchEvent!.agent).toBe(pane.title);
|
|
722
|
+
expect(dispatchEvent!.message.includes("Test task")).toBeTruthy();
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it("logs stall events", () => {
|
|
726
|
+
const pane = makePane({ id: "%1", index: 0, title: "Agent 1" });
|
|
727
|
+
const name = agentIdentifier(pane);
|
|
728
|
+
const task = makeTask({ status: "in-progress", assignee: name });
|
|
729
|
+
const panes: PaneInfo[] = [pane];
|
|
730
|
+
|
|
731
|
+
const config = makeOrchestratorConfig(tmpDir, { stallTimeout: 1000 });
|
|
732
|
+
const state = makeOrchestratorState({
|
|
733
|
+
lastActivity: new Map([["%1", Date.now() - 2000]]),
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
detectStalls(config, state, [task], panes);
|
|
737
|
+
|
|
738
|
+
const events = readEvents(tmpDir);
|
|
739
|
+
const stallEvent = events.find((e) => e.type === "stall");
|
|
740
|
+
expect(stallEvent).toBeTruthy();
|
|
741
|
+
expect(stallEvent!.taskId).toBe("001");
|
|
742
|
+
expect(stallEvent!.agent).toBe(name);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it("logs completion events", () => {
|
|
746
|
+
const task = makeTask({
|
|
747
|
+
status: "done",
|
|
748
|
+
assignee: "Agent 1",
|
|
749
|
+
});
|
|
750
|
+
const panes: PaneInfo[] = [makePane({ id: "%0", title: "Master" })];
|
|
751
|
+
|
|
752
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
753
|
+
const state = makeOrchestratorState({
|
|
754
|
+
previousTasks: new Map([["001", "in-progress"]]),
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
detectCompletions(config, state, [task], panes);
|
|
758
|
+
|
|
759
|
+
const events = readEvents(tmpDir);
|
|
760
|
+
const completionEvent = events.find((e) => e.type === "completion");
|
|
761
|
+
expect(completionEvent).toBeTruthy();
|
|
762
|
+
expect(completionEvent!.taskId).toBe("001");
|
|
763
|
+
expect(completionEvent!.agent).toBe("Agent 1");
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it("logs reconcile events when agent vanishes", () => {
|
|
767
|
+
const task = makeTask({ status: "in-progress", assignee: "Agent 1" });
|
|
768
|
+
saveTask(tmpDir, task);
|
|
769
|
+
|
|
770
|
+
// No panes — agent has vanished
|
|
771
|
+
const panes: PaneInfo[] = [];
|
|
772
|
+
|
|
773
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
774
|
+
const state = makeOrchestratorState();
|
|
775
|
+
|
|
776
|
+
reconcile(config, state, [task], panes);
|
|
777
|
+
|
|
778
|
+
const events = readEvents(tmpDir);
|
|
779
|
+
const reconcileEvent = events.find((e) => e.type === "reconcile");
|
|
780
|
+
expect(reconcileEvent).toBeTruthy();
|
|
781
|
+
expect(reconcileEvent!.taskId).toBe("001");
|
|
782
|
+
expect(reconcileEvent!.agent).toBe("Agent 1");
|
|
783
|
+
expect(reconcileEvent!.message.includes("vanished")).toBeTruthy();
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
describe("saveOrchestratorState / loadOrchestratorState", () => {
|
|
788
|
+
it("round-trips claimed tasks and claim times", () => {
|
|
789
|
+
const state = makeOrchestratorState({
|
|
790
|
+
claimedTasks: new Set(["001", "003"]),
|
|
791
|
+
taskClaimTimes: new Map([
|
|
792
|
+
["001", 1700000000000],
|
|
793
|
+
["003", 1700000060000],
|
|
794
|
+
]),
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
saveOrchestratorState(tmpDir, state);
|
|
798
|
+
|
|
799
|
+
const restored = makeOrchestratorState();
|
|
800
|
+
loadOrchestratorState(tmpDir, restored);
|
|
801
|
+
|
|
802
|
+
expect(restored.claimedTasks.has("001")).toBeTruthy();
|
|
803
|
+
expect(restored.claimedTasks.has("003")).toBeTruthy();
|
|
804
|
+
expect(restored.taskClaimTimes.get("001")).toBe(1700000000000);
|
|
805
|
+
expect(restored.taskClaimTimes.get("003")).toBe(1700000060000);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it("handles missing state file gracefully", () => {
|
|
809
|
+
const state = makeOrchestratorState();
|
|
810
|
+
loadOrchestratorState(tmpDir, state);
|
|
811
|
+
expect(state.claimedTasks.size).toBe(0);
|
|
812
|
+
expect(state.taskClaimTimes.size).toBe(0);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it("handles corrupted state file gracefully", () => {
|
|
816
|
+
mkdirSync(join(tmpDir, ".tasks"), { recursive: true });
|
|
817
|
+
writeFileSync(join(tmpDir, ".tasks", "orchestrator-state.json"), "not json");
|
|
818
|
+
|
|
819
|
+
const state = makeOrchestratorState();
|
|
820
|
+
loadOrchestratorState(tmpDir, state);
|
|
821
|
+
expect(state.claimedTasks.size).toBe(0);
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
describe("syncClaims", () => {
|
|
826
|
+
it("removes stale claims for tasks no longer in-progress", () => {
|
|
827
|
+
saveTask(tmpDir, makeTask({ id: "001", status: "done", assignee: "Agent 1" }));
|
|
828
|
+
saveTask(tmpDir, makeTask({ id: "002", status: "todo" }));
|
|
829
|
+
saveTask(tmpDir, makeTask({ id: "003", status: "in-progress", assignee: "Agent 2" }));
|
|
830
|
+
|
|
831
|
+
const state = makeOrchestratorState({
|
|
832
|
+
claimedTasks: new Set(["001", "002", "003"]),
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
syncClaims(tmpDir, state);
|
|
836
|
+
|
|
837
|
+
expect(!state.claimedTasks.has("001")).toBeTruthy();
|
|
838
|
+
expect(!state.claimedTasks.has("002")).toBeTruthy();
|
|
839
|
+
expect(state.claimedTasks.has("003")).toBeTruthy();
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it("adds missing claims for in-progress tasks", () => {
|
|
843
|
+
saveTask(tmpDir, makeTask({ id: "001", status: "in-progress", assignee: "Agent 1" }));
|
|
844
|
+
saveTask(tmpDir, makeTask({ id: "002", status: "in-progress", assignee: "Agent 2" }));
|
|
845
|
+
|
|
846
|
+
const state = makeOrchestratorState({
|
|
847
|
+
claimedTasks: new Set(),
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
syncClaims(tmpDir, state);
|
|
851
|
+
|
|
852
|
+
expect(state.claimedTasks.has("001")).toBeTruthy();
|
|
853
|
+
expect(state.claimedTasks.has("002")).toBeTruthy();
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it("rebuilds claims from mixed task statuses", () => {
|
|
857
|
+
saveTask(tmpDir, makeTask({ id: "001", status: "done", assignee: "Agent 1" }));
|
|
858
|
+
saveTask(tmpDir, makeTask({ id: "002", status: "in-progress", assignee: "Agent 2" }));
|
|
859
|
+
saveTask(tmpDir, makeTask({ id: "003", status: "todo" }));
|
|
860
|
+
saveTask(tmpDir, makeTask({ id: "004", status: "in-progress", assignee: "Agent 3" }));
|
|
861
|
+
saveTask(tmpDir, makeTask({ id: "005", status: "review", assignee: "Agent 1" }));
|
|
862
|
+
|
|
863
|
+
// Stale state: claims 001 (done) and 003 (todo), missing 004 (in-progress)
|
|
864
|
+
const state = makeOrchestratorState({
|
|
865
|
+
claimedTasks: new Set(["001", "003"]),
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
syncClaims(tmpDir, state);
|
|
869
|
+
|
|
870
|
+
expect(state.claimedTasks.size).toBe(2);
|
|
871
|
+
expect(state.claimedTasks.has("002")).toBeTruthy();
|
|
872
|
+
expect(state.claimedTasks.has("004")).toBeTruthy();
|
|
873
|
+
expect(!state.claimedTasks.has("001")).toBeTruthy();
|
|
874
|
+
expect(!state.claimedTasks.has("003")).toBeTruthy();
|
|
875
|
+
expect(!state.claimedTasks.has("005")).toBeTruthy();
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
it("handles empty task store", () => {
|
|
879
|
+
const state = makeOrchestratorState({
|
|
880
|
+
claimedTasks: new Set(["001", "002"]),
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
syncClaims(tmpDir, state);
|
|
884
|
+
|
|
885
|
+
expect(state.claimedTasks.size).toBe(0);
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
describe("gracefulShutdown", () => {
|
|
890
|
+
it("releases in-progress tasks back to todo", () => {
|
|
891
|
+
const task = makeTask({
|
|
892
|
+
status: "in-progress",
|
|
893
|
+
assignee: "Agent 1",
|
|
894
|
+
});
|
|
895
|
+
saveTask(tmpDir, task);
|
|
896
|
+
|
|
897
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
898
|
+
const state = makeOrchestratorState({
|
|
899
|
+
claimedTasks: new Set(["001"]),
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
gracefulShutdown(config, state);
|
|
903
|
+
|
|
904
|
+
const loaded = loadTask(tmpDir, "001")!;
|
|
905
|
+
expect(loaded.status).toBe("todo");
|
|
906
|
+
expect(loaded.assignee).toBe(null);
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
it("saves orchestrator state to disk", () => {
|
|
910
|
+
saveTask(tmpDir, makeTask());
|
|
911
|
+
|
|
912
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
913
|
+
const state = makeOrchestratorState({
|
|
914
|
+
claimedTasks: new Set(["002"]),
|
|
915
|
+
taskClaimTimes: new Map([["002", Date.now()]]),
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
gracefulShutdown(config, state);
|
|
919
|
+
|
|
920
|
+
expect(existsSync(join(tmpDir, ".tasks", "orchestrator-state.json"))).toBeTruthy();
|
|
921
|
+
|
|
922
|
+
const restored = makeOrchestratorState();
|
|
923
|
+
loadOrchestratorState(tmpDir, restored);
|
|
924
|
+
expect(restored.claimedTasks.has("002")).toBeTruthy();
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
it("leaves done and todo tasks untouched", () => {
|
|
928
|
+
const done = makeTask({ id: "001", status: "done", assignee: "Agent 1" });
|
|
929
|
+
const todo = makeTask({ id: "002", status: "todo", assignee: null });
|
|
930
|
+
saveTask(tmpDir, done);
|
|
931
|
+
saveTask(tmpDir, todo);
|
|
932
|
+
|
|
933
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
934
|
+
const state = makeOrchestratorState();
|
|
935
|
+
|
|
936
|
+
gracefulShutdown(config, state);
|
|
937
|
+
|
|
938
|
+
expect(loadTask(tmpDir, "001")!.status).toBe("done");
|
|
939
|
+
expect(loadTask(tmpDir, "002")!.status).toBe("todo");
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
describe("normalizePaneTitle", () => {
|
|
944
|
+
it("strips spinner prefix from pane title", () => {
|
|
945
|
+
expect(normalizePaneTitle("⠂ Claude Code")).toBe("Claude Code");
|
|
946
|
+
expect(normalizePaneTitle("⠋ Working...")).toBe("Working...");
|
|
947
|
+
expect(normalizePaneTitle("✳ Claude Code")).toBe("Claude Code");
|
|
948
|
+
expect(normalizePaneTitle("◐ Thinking")).toBe("Thinking");
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it("returns title unchanged when no spinner prefix", () => {
|
|
952
|
+
expect(normalizePaneTitle("Agent 1")).toBe("Agent 1");
|
|
953
|
+
expect(normalizePaneTitle("Claude Code")).toBe("Claude Code");
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it("handles empty string", () => {
|
|
957
|
+
expect(normalizePaneTitle("")).toBe("");
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
describe("getPaneSpecialties", () => {
|
|
962
|
+
it("returns specialties from config map", () => {
|
|
963
|
+
const config = makeOrchestratorConfig(tmpDir, {
|
|
964
|
+
paneSpecialties: new Map([["Frontend Agent", ["frontend", "css", "react"]]]),
|
|
965
|
+
});
|
|
966
|
+
const pane = makePane({ title: "Frontend Agent" });
|
|
967
|
+
const specs = getPaneSpecialties(config, pane);
|
|
968
|
+
expect(specs).toEqual(["frontend", "css", "react"]);
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
it("returns empty array for pane with no specialties", () => {
|
|
972
|
+
const config = makeOrchestratorConfig(tmpDir, { paneSpecialties: new Map() });
|
|
973
|
+
const pane = makePane({ title: "Generic Agent" });
|
|
974
|
+
const specs = getPaneSpecialties(config, pane);
|
|
975
|
+
expect(specs).toEqual([]);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it("matches by pane title", () => {
|
|
979
|
+
const config = makeOrchestratorConfig(tmpDir, {
|
|
980
|
+
paneSpecialties: new Map([
|
|
981
|
+
["Backend", ["api", "database"]],
|
|
982
|
+
["Frontend", ["ui", "css"]],
|
|
983
|
+
]),
|
|
984
|
+
});
|
|
985
|
+
expect(getPaneSpecialties(config, makePane({ title: "Backend" }))).toEqual(["api", "database"]);
|
|
986
|
+
expect(getPaneSpecialties(config, makePane({ title: "Frontend" }))).toEqual(["ui", "css"]);
|
|
987
|
+
expect(getPaneSpecialties(config, makePane({ title: "Other" }))).toEqual([]);
|
|
988
|
+
});
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
describe("buildGoalPrompt", () => {
|
|
992
|
+
it("includes goal title, acceptance criteria, and planner name", () => {
|
|
993
|
+
saveMission(tmpDir, {
|
|
994
|
+
title: "Ship v2",
|
|
995
|
+
description: "Major release",
|
|
996
|
+
status: "active",
|
|
997
|
+
branch: null,
|
|
998
|
+
milestones: [],
|
|
999
|
+
created: "2026-01-01T00:00:00Z",
|
|
1000
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
const goal: Goal = {
|
|
1004
|
+
id: "01",
|
|
1005
|
+
title: "Build REST API",
|
|
1006
|
+
description: "Create CRUD endpoints",
|
|
1007
|
+
status: "todo",
|
|
1008
|
+
acceptance: "All endpoints return 200 with correct data",
|
|
1009
|
+
priority: 1,
|
|
1010
|
+
created: "2026-01-01T00:00:00Z",
|
|
1011
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1012
|
+
assignee: null,
|
|
1013
|
+
specialty: "backend",
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
const planner = makePane({ id: "%2", index: 1, title: "Backend Planner" });
|
|
1017
|
+
const prompt = buildGoalPrompt(tmpDir, goal, planner);
|
|
1018
|
+
|
|
1019
|
+
expect(prompt.includes("Build REST API")).toBeTruthy();
|
|
1020
|
+
expect(prompt.includes("All endpoints return 200 with correct data")).toBeTruthy();
|
|
1021
|
+
expect(prompt.includes("backend planner")).toBeTruthy();
|
|
1022
|
+
expect(prompt.includes("Ship v2")).toBeTruthy();
|
|
1023
|
+
expect(prompt.includes(agentIdentifier(planner))).toBeTruthy();
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
it("works without mission", () => {
|
|
1027
|
+
const goal: Goal = {
|
|
1028
|
+
id: "01",
|
|
1029
|
+
title: "Setup CI",
|
|
1030
|
+
description: "",
|
|
1031
|
+
status: "todo",
|
|
1032
|
+
acceptance: "CI passes",
|
|
1033
|
+
priority: 1,
|
|
1034
|
+
created: "2026-01-01T00:00:00Z",
|
|
1035
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1036
|
+
assignee: null,
|
|
1037
|
+
specialty: null,
|
|
1038
|
+
milestone: null,
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
const planner = makePane({ id: "%1", index: 0 });
|
|
1042
|
+
const prompt = buildGoalPrompt(tmpDir, goal, planner);
|
|
1043
|
+
|
|
1044
|
+
expect(prompt.includes("Setup CI")).toBeTruthy();
|
|
1045
|
+
expect(prompt.includes("general planner")).toBeTruthy();
|
|
1046
|
+
expect(!prompt.includes("Mission:")).toBeTruthy();
|
|
1047
|
+
});
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
describe("dispatchGoals", () => {
|
|
1051
|
+
it("assigns goal to idle planner pane", () => {
|
|
1052
|
+
const goal: Goal = {
|
|
1053
|
+
id: "01",
|
|
1054
|
+
title: "Build Frontend",
|
|
1055
|
+
description: "React components",
|
|
1056
|
+
status: "todo",
|
|
1057
|
+
acceptance: "UI renders",
|
|
1058
|
+
priority: 1,
|
|
1059
|
+
created: "2026-01-01T00:00:00Z",
|
|
1060
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1061
|
+
assignee: null,
|
|
1062
|
+
specialty: null,
|
|
1063
|
+
milestone: null,
|
|
1064
|
+
};
|
|
1065
|
+
saveGoal(tmpDir, goal);
|
|
1066
|
+
|
|
1067
|
+
const planner = makePane({ id: "%2", index: 1, title: "Planner", currentCommand: "claude" });
|
|
1068
|
+
const config = makeOrchestratorConfig(tmpDir, {
|
|
1069
|
+
paneSpecialties: new Map([["Planner", ["frontend"]]]),
|
|
1070
|
+
});
|
|
1071
|
+
const state = makeOrchestratorState();
|
|
1072
|
+
|
|
1073
|
+
dispatchGoals(config, state, [goal], [], [planner]);
|
|
1074
|
+
|
|
1075
|
+
const updated = loadGoal(tmpDir, "01")!;
|
|
1076
|
+
expect(updated.status).toBe("in-progress");
|
|
1077
|
+
expect(updated.assignee).toBe(agentIdentifier(planner));
|
|
1078
|
+
|
|
1079
|
+
const events = readEvents(tmpDir);
|
|
1080
|
+
expect(events.length).toBe(1);
|
|
1081
|
+
expect(events[0]!.type).toBe("dispatch");
|
|
1082
|
+
expect(events[0]!.message.includes("Build Frontend")).toBeTruthy();
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
it("skips master pane", () => {
|
|
1086
|
+
const goal: Goal = {
|
|
1087
|
+
id: "01",
|
|
1088
|
+
title: "Test Goal",
|
|
1089
|
+
description: "",
|
|
1090
|
+
status: "todo",
|
|
1091
|
+
acceptance: "",
|
|
1092
|
+
priority: 1,
|
|
1093
|
+
created: "2026-01-01T00:00:00Z",
|
|
1094
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1095
|
+
assignee: null,
|
|
1096
|
+
specialty: null,
|
|
1097
|
+
milestone: null,
|
|
1098
|
+
};
|
|
1099
|
+
saveGoal(tmpDir, goal);
|
|
1100
|
+
|
|
1101
|
+
// Only pane is the master — should not be used as planner
|
|
1102
|
+
const masterPane = makePane({
|
|
1103
|
+
id: "%1",
|
|
1104
|
+
index: 0,
|
|
1105
|
+
title: "Master",
|
|
1106
|
+
currentCommand: "claude",
|
|
1107
|
+
role: "lead",
|
|
1108
|
+
});
|
|
1109
|
+
const config = makeOrchestratorConfig(tmpDir, { masterPane: "Master" });
|
|
1110
|
+
const state = makeOrchestratorState();
|
|
1111
|
+
|
|
1112
|
+
dispatchGoals(config, state, [goal], [], [masterPane]);
|
|
1113
|
+
|
|
1114
|
+
const unchanged = loadGoal(tmpDir, "01")!;
|
|
1115
|
+
expect(unchanged.status).toBe("todo");
|
|
1116
|
+
expect(unchanged.assignee).toBe(null);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
it("does not assign already-assigned goals", () => {
|
|
1120
|
+
const goal: Goal = {
|
|
1121
|
+
id: "01",
|
|
1122
|
+
title: "Already assigned",
|
|
1123
|
+
description: "",
|
|
1124
|
+
status: "todo",
|
|
1125
|
+
acceptance: "",
|
|
1126
|
+
priority: 1,
|
|
1127
|
+
created: "2026-01-01T00:00:00Z",
|
|
1128
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1129
|
+
assignee: "Someone",
|
|
1130
|
+
specialty: null,
|
|
1131
|
+
milestone: null,
|
|
1132
|
+
};
|
|
1133
|
+
saveGoal(tmpDir, goal);
|
|
1134
|
+
|
|
1135
|
+
const planner = makePane({ id: "%2", index: 1, title: "Planner", currentCommand: "claude" });
|
|
1136
|
+
const config = makeOrchestratorConfig(tmpDir, {
|
|
1137
|
+
paneSpecialties: new Map([["Planner", []]]),
|
|
1138
|
+
});
|
|
1139
|
+
const state = makeOrchestratorState();
|
|
1140
|
+
|
|
1141
|
+
dispatchGoals(config, state, [goal], [], [planner]);
|
|
1142
|
+
|
|
1143
|
+
// Goal should remain unchanged — assignee filter excludes it
|
|
1144
|
+
const events = readEvents(tmpDir);
|
|
1145
|
+
expect(events.length).toBe(0);
|
|
1146
|
+
});
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
describe("findBestAgent", () => {
|
|
1150
|
+
it("returns first agent when task has no specialty", () => {
|
|
1151
|
+
const agents = [
|
|
1152
|
+
makePane({ id: "%1", title: "Agent A", currentCommand: "zsh" }),
|
|
1153
|
+
makePane({ id: "%2", title: "Agent B", currentCommand: "zsh" }),
|
|
1154
|
+
];
|
|
1155
|
+
const config = makeOrchestratorConfig(tmpDir, {
|
|
1156
|
+
paneSpecialties: new Map([["Agent A", ["frontend"]]]),
|
|
1157
|
+
});
|
|
1158
|
+
const task = makeTask({ specialty: null });
|
|
1159
|
+
const result = findBestAgent(config, agents, task, new Set());
|
|
1160
|
+
expect(result?.id).toBe("%1");
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
it("prefers specialist agent for matching specialty", () => {
|
|
1164
|
+
const agents = [
|
|
1165
|
+
makePane({ id: "%1", title: "Generalist", currentCommand: "zsh" }),
|
|
1166
|
+
makePane({ id: "%2", title: "Frontend", currentCommand: "zsh" }),
|
|
1167
|
+
];
|
|
1168
|
+
const config = makeOrchestratorConfig(tmpDir, {
|
|
1169
|
+
paneSpecialties: new Map([["Frontend", ["frontend", "css"]]]),
|
|
1170
|
+
});
|
|
1171
|
+
const task = makeTask({ specialty: "frontend" });
|
|
1172
|
+
const result = findBestAgent(config, agents, task, new Set());
|
|
1173
|
+
expect(result?.id).toBe("%2");
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
it("falls back to generalist when no specialist matches", () => {
|
|
1177
|
+
const agents = [
|
|
1178
|
+
makePane({ id: "%1", title: "Generalist", currentCommand: "zsh" }),
|
|
1179
|
+
makePane({ id: "%2", title: "Backend", currentCommand: "zsh" }),
|
|
1180
|
+
];
|
|
1181
|
+
const config = makeOrchestratorConfig(tmpDir, {
|
|
1182
|
+
paneSpecialties: new Map([["Backend", ["api", "database"]]]),
|
|
1183
|
+
});
|
|
1184
|
+
const task = makeTask({ specialty: "frontend" });
|
|
1185
|
+
const result = findBestAgent(config, agents, task, new Set());
|
|
1186
|
+
// Generalist has no specialties → takes anything
|
|
1187
|
+
expect(result?.id).toBe("%1");
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
it("returns null when no suitable agent (all are wrong specialists)", () => {
|
|
1191
|
+
const agents = [
|
|
1192
|
+
makePane({ id: "%1", title: "Backend", currentCommand: "zsh" }),
|
|
1193
|
+
makePane({ id: "%2", title: "DevOps", currentCommand: "zsh" }),
|
|
1194
|
+
];
|
|
1195
|
+
const config = makeOrchestratorConfig(tmpDir, {
|
|
1196
|
+
paneSpecialties: new Map([
|
|
1197
|
+
["Backend", ["api", "database"]],
|
|
1198
|
+
["DevOps", ["infra", "ci"]],
|
|
1199
|
+
]),
|
|
1200
|
+
});
|
|
1201
|
+
const task = makeTask({ specialty: "frontend" });
|
|
1202
|
+
const result = findBestAgent(config, agents, task, new Set());
|
|
1203
|
+
expect(result).toBeNull();
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
it("excludes agents in excludeIds set", () => {
|
|
1207
|
+
const agents = [
|
|
1208
|
+
makePane({ id: "%1", title: "Agent A", currentCommand: "zsh" }),
|
|
1209
|
+
makePane({ id: "%2", title: "Agent B", currentCommand: "zsh" }),
|
|
1210
|
+
];
|
|
1211
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
1212
|
+
const task = makeTask({ specialty: null });
|
|
1213
|
+
const result = findBestAgent(config, agents, task, new Set(["%1"]));
|
|
1214
|
+
expect(result?.id).toBe("%2");
|
|
1215
|
+
});
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
describe("skill-based dispatch integration", () => {
|
|
1219
|
+
it("dispatches task with specialty to matching specialist agent", () => {
|
|
1220
|
+
const task = makeTask({ id: "001", specialty: "frontend" });
|
|
1221
|
+
saveTask(tmpDir, task);
|
|
1222
|
+
|
|
1223
|
+
const generalist = makePane({ id: "%1", index: 0, title: "Generalist", currentCommand: "zsh" });
|
|
1224
|
+
const specialist = makePane({ id: "%2", index: 1, title: "Frontend", currentCommand: "zsh" });
|
|
1225
|
+
const panes = [generalist, specialist];
|
|
1226
|
+
mockPanes = panes;
|
|
1227
|
+
|
|
1228
|
+
const config = makeOrchestratorConfig(tmpDir, {
|
|
1229
|
+
masterPane: null,
|
|
1230
|
+
paneSpecialties: new Map([["Frontend", ["frontend", "css"]]]),
|
|
1231
|
+
});
|
|
1232
|
+
const state = makeOrchestratorState();
|
|
1233
|
+
|
|
1234
|
+
dispatch(config, state, [task], panes);
|
|
1235
|
+
|
|
1236
|
+
const loaded = loadTask(tmpDir, "001");
|
|
1237
|
+
expect(loaded?.assignee).toBe(agentIdentifier(specialist));
|
|
1238
|
+
expect(loaded?.status).toBe("in-progress");
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
it("dispatches task with no specialty to any idle agent", () => {
|
|
1242
|
+
const task = makeTask({ id: "001", specialty: null });
|
|
1243
|
+
saveTask(tmpDir, task);
|
|
1244
|
+
|
|
1245
|
+
const pane = makePane({ id: "%1", index: 0, title: "Agent 1", currentCommand: "zsh" });
|
|
1246
|
+
const panes = [pane];
|
|
1247
|
+
mockPanes = panes;
|
|
1248
|
+
|
|
1249
|
+
const config = makeOrchestratorConfig(tmpDir, {
|
|
1250
|
+
masterPane: null,
|
|
1251
|
+
paneSpecialties: new Map([["Agent 1", ["backend"]]]),
|
|
1252
|
+
});
|
|
1253
|
+
const state = makeOrchestratorState();
|
|
1254
|
+
|
|
1255
|
+
dispatch(config, state, [task], panes);
|
|
1256
|
+
|
|
1257
|
+
const loaded = loadTask(tmpDir, "001");
|
|
1258
|
+
expect(loaded?.assignee).toBe(agentIdentifier(pane));
|
|
1259
|
+
expect(loaded?.status).toBe("in-progress");
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
it("skips specialist task when no matching agent available", () => {
|
|
1263
|
+
const task = makeTask({ id: "001", specialty: "frontend" });
|
|
1264
|
+
saveTask(tmpDir, task);
|
|
1265
|
+
|
|
1266
|
+
// Only agent is a backend specialist — not a match and not a generalist
|
|
1267
|
+
const panes = [makePane({ id: "%1", index: 0, title: "Backend", currentCommand: "zsh" })];
|
|
1268
|
+
mockPanes = panes;
|
|
1269
|
+
|
|
1270
|
+
const config = makeOrchestratorConfig(tmpDir, {
|
|
1271
|
+
masterPane: null,
|
|
1272
|
+
paneSpecialties: new Map([["Backend", ["api", "database"]]]),
|
|
1273
|
+
});
|
|
1274
|
+
const state = makeOrchestratorState();
|
|
1275
|
+
|
|
1276
|
+
dispatch(config, state, [task], panes);
|
|
1277
|
+
|
|
1278
|
+
const loaded = loadTask(tmpDir, "001");
|
|
1279
|
+
expect(loaded?.assignee).toBe(null);
|
|
1280
|
+
expect(loaded?.status).toBe("todo");
|
|
1281
|
+
});
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
describe("buildTaskPrompt with skill injection", () => {
|
|
1285
|
+
it("includes skill body when task has specialty and skill file exists", () => {
|
|
1286
|
+
// Create skill file
|
|
1287
|
+
const skillsDir = join(tmpDir, ".tmux-ide", "skills");
|
|
1288
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
1289
|
+
writeFileSync(
|
|
1290
|
+
join(skillsDir, "frontend.md"),
|
|
1291
|
+
`---
|
|
1292
|
+
name: frontend
|
|
1293
|
+
specialties: [frontend, css]
|
|
1294
|
+
role: teammate
|
|
1295
|
+
description: Frontend specialist
|
|
1296
|
+
---
|
|
1297
|
+
You are a frontend expert. Focus on React components and CSS.`,
|
|
1298
|
+
);
|
|
1299
|
+
|
|
1300
|
+
const task = makeTask({ specialty: "frontend" });
|
|
1301
|
+
const prompt = buildTaskPrompt(tmpDir, task);
|
|
1302
|
+
|
|
1303
|
+
expect(prompt.includes("Your Role: frontend")).toBeTruthy();
|
|
1304
|
+
expect(prompt.includes("You are a frontend expert")).toBeTruthy();
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
it("does not include skill section when task has no specialty", () => {
|
|
1308
|
+
const task = makeTask({ specialty: null });
|
|
1309
|
+
const prompt = buildTaskPrompt(tmpDir, task);
|
|
1310
|
+
expect(!prompt.includes("Your Role:")).toBeTruthy();
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
it("does not include skill section when skill file is missing", () => {
|
|
1314
|
+
const task = makeTask({ specialty: "nonexistent" });
|
|
1315
|
+
const prompt = buildTaskPrompt(tmpDir, task);
|
|
1316
|
+
expect(!prompt.includes("Your Role:")).toBeTruthy();
|
|
1317
|
+
});
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
describe("milestone gating", () => {
|
|
1321
|
+
function setupMission(milestones: Mission["milestones"]): void {
|
|
1322
|
+
saveMission(tmpDir, {
|
|
1323
|
+
title: "Ship v2",
|
|
1324
|
+
description: "",
|
|
1325
|
+
status: "active",
|
|
1326
|
+
branch: null,
|
|
1327
|
+
milestones,
|
|
1328
|
+
created: "2026-01-01T00:00:00Z",
|
|
1329
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
it("getCurrentMilestone returns first non-done milestone by order", () => {
|
|
1334
|
+
setupMission([
|
|
1335
|
+
{
|
|
1336
|
+
id: "M2",
|
|
1337
|
+
title: "Phase 2",
|
|
1338
|
+
description: "",
|
|
1339
|
+
status: "locked",
|
|
1340
|
+
order: 2,
|
|
1341
|
+
created: "2026-01-01T00:00:00Z",
|
|
1342
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
id: "M1",
|
|
1346
|
+
title: "Phase 1",
|
|
1347
|
+
description: "",
|
|
1348
|
+
status: "active",
|
|
1349
|
+
order: 1,
|
|
1350
|
+
created: "2026-01-01T00:00:00Z",
|
|
1351
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1352
|
+
},
|
|
1353
|
+
]);
|
|
1354
|
+
const current = getCurrentMilestone(tmpDir);
|
|
1355
|
+
expect(current?.id).toBe("M1");
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
it("isMilestoneReady returns true when all predecessors are done", () => {
|
|
1359
|
+
setupMission([
|
|
1360
|
+
{
|
|
1361
|
+
id: "M1",
|
|
1362
|
+
title: "Phase 1",
|
|
1363
|
+
description: "",
|
|
1364
|
+
status: "done",
|
|
1365
|
+
order: 1,
|
|
1366
|
+
created: "2026-01-01T00:00:00Z",
|
|
1367
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
id: "M2",
|
|
1371
|
+
title: "Phase 2",
|
|
1372
|
+
description: "",
|
|
1373
|
+
status: "active",
|
|
1374
|
+
order: 2,
|
|
1375
|
+
created: "2026-01-01T00:00:00Z",
|
|
1376
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1377
|
+
},
|
|
1378
|
+
]);
|
|
1379
|
+
expect(isMilestoneReady(tmpDir, "M2")).toBe(true);
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
it("isMilestoneReady returns false when predecessor is not done", () => {
|
|
1383
|
+
setupMission([
|
|
1384
|
+
{
|
|
1385
|
+
id: "M1",
|
|
1386
|
+
title: "Phase 1",
|
|
1387
|
+
description: "",
|
|
1388
|
+
status: "active",
|
|
1389
|
+
order: 1,
|
|
1390
|
+
created: "2026-01-01T00:00:00Z",
|
|
1391
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
id: "M2",
|
|
1395
|
+
title: "Phase 2",
|
|
1396
|
+
description: "",
|
|
1397
|
+
status: "locked",
|
|
1398
|
+
order: 2,
|
|
1399
|
+
created: "2026-01-01T00:00:00Z",
|
|
1400
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1401
|
+
},
|
|
1402
|
+
]);
|
|
1403
|
+
expect(isMilestoneReady(tmpDir, "M2")).toBe(false);
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
it("dispatches task with active milestone M1", () => {
|
|
1407
|
+
setupMission([
|
|
1408
|
+
{
|
|
1409
|
+
id: "M1",
|
|
1410
|
+
title: "Phase 1",
|
|
1411
|
+
description: "",
|
|
1412
|
+
status: "active",
|
|
1413
|
+
order: 1,
|
|
1414
|
+
created: "2026-01-01T00:00:00Z",
|
|
1415
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1416
|
+
},
|
|
1417
|
+
{
|
|
1418
|
+
id: "M2",
|
|
1419
|
+
title: "Phase 2",
|
|
1420
|
+
description: "",
|
|
1421
|
+
status: "locked",
|
|
1422
|
+
order: 2,
|
|
1423
|
+
created: "2026-01-01T00:00:00Z",
|
|
1424
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1425
|
+
},
|
|
1426
|
+
]);
|
|
1427
|
+
|
|
1428
|
+
const task = makeTask({ id: "001", milestone: "M1" });
|
|
1429
|
+
saveTask(tmpDir, task);
|
|
1430
|
+
|
|
1431
|
+
const pane = makePane({ id: "%1", index: 0, title: "Agent 1", currentCommand: "zsh" });
|
|
1432
|
+
const panes = [pane];
|
|
1433
|
+
mockPanes = panes;
|
|
1434
|
+
|
|
1435
|
+
const config = makeOrchestratorConfig(tmpDir, { masterPane: null, dispatchMode: "missions" });
|
|
1436
|
+
const state = makeOrchestratorState();
|
|
1437
|
+
|
|
1438
|
+
dispatch(config, state, [task], panes);
|
|
1439
|
+
|
|
1440
|
+
const loaded = loadTask(tmpDir, "001");
|
|
1441
|
+
expect(loaded?.status).toBe("in-progress");
|
|
1442
|
+
expect(loaded?.assignee).toBe(agentIdentifier(pane));
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
it("skips task with locked milestone M2 when M1 is still active", () => {
|
|
1446
|
+
setupMission([
|
|
1447
|
+
{
|
|
1448
|
+
id: "M1",
|
|
1449
|
+
title: "Phase 1",
|
|
1450
|
+
description: "",
|
|
1451
|
+
status: "active",
|
|
1452
|
+
order: 1,
|
|
1453
|
+
created: "2026-01-01T00:00:00Z",
|
|
1454
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1455
|
+
},
|
|
1456
|
+
{
|
|
1457
|
+
id: "M2",
|
|
1458
|
+
title: "Phase 2",
|
|
1459
|
+
description: "",
|
|
1460
|
+
status: "locked",
|
|
1461
|
+
order: 2,
|
|
1462
|
+
created: "2026-01-01T00:00:00Z",
|
|
1463
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1464
|
+
},
|
|
1465
|
+
]);
|
|
1466
|
+
|
|
1467
|
+
const task = makeTask({ id: "001", milestone: "M2" });
|
|
1468
|
+
saveTask(tmpDir, task);
|
|
1469
|
+
|
|
1470
|
+
const pane = makePane({ id: "%1", index: 0, title: "Agent 1", currentCommand: "zsh" });
|
|
1471
|
+
const panes = [pane];
|
|
1472
|
+
mockPanes = panes;
|
|
1473
|
+
|
|
1474
|
+
const config = makeOrchestratorConfig(tmpDir, { masterPane: null, dispatchMode: "missions" });
|
|
1475
|
+
const state = makeOrchestratorState();
|
|
1476
|
+
|
|
1477
|
+
dispatch(config, state, [task], panes);
|
|
1478
|
+
|
|
1479
|
+
const loaded = loadTask(tmpDir, "001");
|
|
1480
|
+
expect(loaded?.status).toBe("todo");
|
|
1481
|
+
expect(loaded?.assignee).toBe(null);
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
it("tasks without milestone field always dispatch in missions mode", () => {
|
|
1485
|
+
setupMission([
|
|
1486
|
+
{
|
|
1487
|
+
id: "M1",
|
|
1488
|
+
title: "Phase 1",
|
|
1489
|
+
description: "",
|
|
1490
|
+
status: "active",
|
|
1491
|
+
order: 1,
|
|
1492
|
+
created: "2026-01-01T00:00:00Z",
|
|
1493
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1494
|
+
},
|
|
1495
|
+
]);
|
|
1496
|
+
|
|
1497
|
+
const task = makeTask({ id: "001", milestone: null });
|
|
1498
|
+
saveTask(tmpDir, task);
|
|
1499
|
+
|
|
1500
|
+
const pane = makePane({ id: "%1", index: 0, title: "Agent 1", currentCommand: "zsh" });
|
|
1501
|
+
const panes = [pane];
|
|
1502
|
+
mockPanes = panes;
|
|
1503
|
+
|
|
1504
|
+
const config = makeOrchestratorConfig(tmpDir, { masterPane: null, dispatchMode: "missions" });
|
|
1505
|
+
const state = makeOrchestratorState();
|
|
1506
|
+
|
|
1507
|
+
dispatch(config, state, [task], panes);
|
|
1508
|
+
|
|
1509
|
+
const loaded = loadTask(tmpDir, "001");
|
|
1510
|
+
expect(loaded?.status).toBe("in-progress");
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
it("checkMilestoneCompletion marks M1 done and activates M2 when all M1 tasks complete (no contract)", () => {
|
|
1514
|
+
setupMission([
|
|
1515
|
+
{
|
|
1516
|
+
id: "M1",
|
|
1517
|
+
title: "Phase 1",
|
|
1518
|
+
description: "",
|
|
1519
|
+
status: "active",
|
|
1520
|
+
order: 1,
|
|
1521
|
+
created: "2026-01-01T00:00:00Z",
|
|
1522
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1523
|
+
},
|
|
1524
|
+
{
|
|
1525
|
+
id: "M2",
|
|
1526
|
+
title: "Phase 2",
|
|
1527
|
+
description: "",
|
|
1528
|
+
status: "locked",
|
|
1529
|
+
order: 2,
|
|
1530
|
+
created: "2026-01-01T00:00:00Z",
|
|
1531
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1532
|
+
},
|
|
1533
|
+
]);
|
|
1534
|
+
|
|
1535
|
+
const task1 = makeTask({ id: "001", milestone: "M1", status: "done", assignee: "Agent 1" });
|
|
1536
|
+
const task2 = makeTask({ id: "002", milestone: "M1", status: "done", assignee: "Agent 2" });
|
|
1537
|
+
saveTask(tmpDir, task1);
|
|
1538
|
+
saveTask(tmpDir, task2);
|
|
1539
|
+
|
|
1540
|
+
const config = makeOrchestratorConfig(tmpDir, { dispatchMode: "missions" });
|
|
1541
|
+
const state = makeOrchestratorState();
|
|
1542
|
+
mockPanes = [];
|
|
1543
|
+
checkMilestoneCompletion(config, state, [task1, task2], []);
|
|
1544
|
+
|
|
1545
|
+
const mission = loadMission(tmpDir)!;
|
|
1546
|
+
const m1 = mission.milestones.find((m) => m.id === "M1")!;
|
|
1547
|
+
const m2 = mission.milestones.find((m) => m.id === "M2")!;
|
|
1548
|
+
expect(m1.status).toBe("done");
|
|
1549
|
+
expect(m2.status).toBe("active");
|
|
1550
|
+
|
|
1551
|
+
// Check event logged
|
|
1552
|
+
const events = readEvents(tmpDir);
|
|
1553
|
+
const milestoneEvent = events.find((e) => e.type === "milestone_complete");
|
|
1554
|
+
expect(milestoneEvent).toBeTruthy();
|
|
1555
|
+
expect(milestoneEvent!.message.includes("Phase 1")).toBeTruthy();
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
it("checkMilestoneCompletion does not advance when tasks are still in progress", () => {
|
|
1559
|
+
setupMission([
|
|
1560
|
+
{
|
|
1561
|
+
id: "M1",
|
|
1562
|
+
title: "Phase 1",
|
|
1563
|
+
description: "",
|
|
1564
|
+
status: "active",
|
|
1565
|
+
order: 1,
|
|
1566
|
+
created: "2026-01-01T00:00:00Z",
|
|
1567
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1568
|
+
},
|
|
1569
|
+
{
|
|
1570
|
+
id: "M2",
|
|
1571
|
+
title: "Phase 2",
|
|
1572
|
+
description: "",
|
|
1573
|
+
status: "locked",
|
|
1574
|
+
order: 2,
|
|
1575
|
+
created: "2026-01-01T00:00:00Z",
|
|
1576
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1577
|
+
},
|
|
1578
|
+
]);
|
|
1579
|
+
|
|
1580
|
+
const task1 = makeTask({ id: "001", milestone: "M1", status: "done", assignee: "Agent 1" });
|
|
1581
|
+
const task2 = makeTask({
|
|
1582
|
+
id: "002",
|
|
1583
|
+
milestone: "M1",
|
|
1584
|
+
status: "in-progress",
|
|
1585
|
+
assignee: "Agent 2",
|
|
1586
|
+
});
|
|
1587
|
+
saveTask(tmpDir, task1);
|
|
1588
|
+
saveTask(tmpDir, task2);
|
|
1589
|
+
|
|
1590
|
+
const config = makeOrchestratorConfig(tmpDir, { dispatchMode: "missions" });
|
|
1591
|
+
const state = makeOrchestratorState();
|
|
1592
|
+
checkMilestoneCompletion(config, state, [task1, task2], []);
|
|
1593
|
+
|
|
1594
|
+
const mission = loadMission(tmpDir)!;
|
|
1595
|
+
const m1 = mission.milestones.find((m) => m.id === "M1")!;
|
|
1596
|
+
const m2 = mission.milestones.find((m) => m.id === "M2")!;
|
|
1597
|
+
expect(m1.status).toBe("active");
|
|
1598
|
+
expect(m2.status).toBe("locked");
|
|
1599
|
+
});
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
describe("validation flow", () => {
|
|
1603
|
+
function setupMission(milestones: Mission["milestones"]): void {
|
|
1604
|
+
saveMission(tmpDir, {
|
|
1605
|
+
title: "Ship v2",
|
|
1606
|
+
description: "",
|
|
1607
|
+
status: "active",
|
|
1608
|
+
branch: null,
|
|
1609
|
+
milestones,
|
|
1610
|
+
created: "2026-01-01T00:00:00Z",
|
|
1611
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
function writeContract(content: string): void {
|
|
1616
|
+
mkdirSync(join(tmpDir, ".tasks"), { recursive: true });
|
|
1617
|
+
writeFileSync(join(tmpDir, ".tasks", "validation-contract.md"), content);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
it("milestone completion triggers validating state when contract exists", () => {
|
|
1621
|
+
setupMission([
|
|
1622
|
+
{
|
|
1623
|
+
id: "M1",
|
|
1624
|
+
title: "Phase 1",
|
|
1625
|
+
description: "",
|
|
1626
|
+
status: "active",
|
|
1627
|
+
order: 1,
|
|
1628
|
+
created: "2026-01-01T00:00:00Z",
|
|
1629
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1630
|
+
},
|
|
1631
|
+
{
|
|
1632
|
+
id: "M2",
|
|
1633
|
+
title: "Phase 2",
|
|
1634
|
+
description: "",
|
|
1635
|
+
status: "locked",
|
|
1636
|
+
order: 2,
|
|
1637
|
+
created: "2026-01-01T00:00:00Z",
|
|
1638
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1639
|
+
},
|
|
1640
|
+
]);
|
|
1641
|
+
writeContract("# Assertions\n- A1: Auth works\n- A2: Tests pass");
|
|
1642
|
+
|
|
1643
|
+
const task1 = makeTask({ id: "001", milestone: "M1", status: "done", fulfills: ["A1"] });
|
|
1644
|
+
const task2 = makeTask({ id: "002", milestone: "M1", status: "done", fulfills: ["A2"] });
|
|
1645
|
+
saveTask(tmpDir, task1);
|
|
1646
|
+
saveTask(tmpDir, task2);
|
|
1647
|
+
|
|
1648
|
+
const validatorPane = makePane({
|
|
1649
|
+
id: "%3",
|
|
1650
|
+
index: 2,
|
|
1651
|
+
title: "Validator",
|
|
1652
|
+
currentCommand: "zsh",
|
|
1653
|
+
role: "validator",
|
|
1654
|
+
});
|
|
1655
|
+
mockPanes = [validatorPane];
|
|
1656
|
+
|
|
1657
|
+
const config = makeOrchestratorConfig(tmpDir, { dispatchMode: "missions", masterPane: null });
|
|
1658
|
+
const state = makeOrchestratorState();
|
|
1659
|
+
|
|
1660
|
+
checkMilestoneCompletion(config, state, [task1, task2], [validatorPane]);
|
|
1661
|
+
|
|
1662
|
+
// Milestone should be in validating, not done
|
|
1663
|
+
const mission = loadMission(tmpDir)!;
|
|
1664
|
+
const m1 = mission.milestones.find((m) => m.id === "M1")!;
|
|
1665
|
+
expect(m1.status).toBe("validating");
|
|
1666
|
+
|
|
1667
|
+
// Validation state should have pending entries
|
|
1668
|
+
const valState = loadValidationState(tmpDir)!;
|
|
1669
|
+
expect(valState.assertions["A1"]?.status).toBe("pending");
|
|
1670
|
+
expect(valState.assertions["A2"]?.status).toBe("pending");
|
|
1671
|
+
|
|
1672
|
+
// Should have dispatched to validator pane
|
|
1673
|
+
const sendCalls = tmuxCalls.filter((c) => c.args.includes("send-keys"));
|
|
1674
|
+
expect(sendCalls.length).toBeGreaterThan(0);
|
|
1675
|
+
|
|
1676
|
+
// Check event
|
|
1677
|
+
const events = readEvents(tmpDir);
|
|
1678
|
+
expect(events.find((e) => e.type === "milestone_validating")).toBeTruthy();
|
|
1679
|
+
expect(events.find((e) => e.type === "validation_dispatch")).toBeTruthy();
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
it("all assertions passing marks milestone done and activates next", () => {
|
|
1683
|
+
setupMission([
|
|
1684
|
+
{
|
|
1685
|
+
id: "M1",
|
|
1686
|
+
title: "Phase 1",
|
|
1687
|
+
description: "",
|
|
1688
|
+
status: "validating",
|
|
1689
|
+
order: 1,
|
|
1690
|
+
created: "2026-01-01T00:00:00Z",
|
|
1691
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1692
|
+
},
|
|
1693
|
+
{
|
|
1694
|
+
id: "M2",
|
|
1695
|
+
title: "Phase 2",
|
|
1696
|
+
description: "",
|
|
1697
|
+
status: "locked",
|
|
1698
|
+
order: 2,
|
|
1699
|
+
created: "2026-01-01T00:00:00Z",
|
|
1700
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1701
|
+
},
|
|
1702
|
+
]);
|
|
1703
|
+
|
|
1704
|
+
const task1 = makeTask({ id: "001", milestone: "M1", status: "done", fulfills: ["A1"] });
|
|
1705
|
+
const task2 = makeTask({ id: "002", milestone: "M1", status: "done", fulfills: ["A2"] });
|
|
1706
|
+
saveTask(tmpDir, task1);
|
|
1707
|
+
saveTask(tmpDir, task2);
|
|
1708
|
+
|
|
1709
|
+
// All assertions passing
|
|
1710
|
+
saveValidationState(tmpDir, {
|
|
1711
|
+
assertions: {
|
|
1712
|
+
A1: {
|
|
1713
|
+
status: "passing",
|
|
1714
|
+
verifiedBy: "Validator",
|
|
1715
|
+
verifiedAt: "2026-01-01T00:00:00Z",
|
|
1716
|
+
evidence: "works",
|
|
1717
|
+
blockedBy: null,
|
|
1718
|
+
},
|
|
1719
|
+
A2: {
|
|
1720
|
+
status: "passing",
|
|
1721
|
+
verifiedBy: "Validator",
|
|
1722
|
+
verifiedAt: "2026-01-01T00:00:00Z",
|
|
1723
|
+
evidence: "tests pass",
|
|
1724
|
+
blockedBy: null,
|
|
1725
|
+
},
|
|
1726
|
+
},
|
|
1727
|
+
lastVerified: "2026-01-01T00:00:00Z",
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
const config = makeOrchestratorConfig(tmpDir, { dispatchMode: "missions" });
|
|
1731
|
+
checkValidationResults(config, [task1, task2]);
|
|
1732
|
+
|
|
1733
|
+
const mission = loadMission(tmpDir)!;
|
|
1734
|
+
const m1 = mission.milestones.find((m) => m.id === "M1")!;
|
|
1735
|
+
const m2 = mission.milestones.find((m) => m.id === "M2")!;
|
|
1736
|
+
expect(m1.status).toBe("done");
|
|
1737
|
+
expect(m2.status).toBe("active");
|
|
1738
|
+
|
|
1739
|
+
const events = readEvents(tmpDir);
|
|
1740
|
+
expect(events.find((e) => e.type === "milestone_complete")).toBeTruthy();
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
it("zero-assertion milestone advances to done", () => {
|
|
1744
|
+
setupMission([
|
|
1745
|
+
{
|
|
1746
|
+
id: "M1",
|
|
1747
|
+
title: "Phase 1",
|
|
1748
|
+
description: "",
|
|
1749
|
+
status: "validating",
|
|
1750
|
+
order: 1,
|
|
1751
|
+
created: "2026-01-01T00:00:00Z",
|
|
1752
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1753
|
+
},
|
|
1754
|
+
{
|
|
1755
|
+
id: "M2",
|
|
1756
|
+
title: "Phase 2",
|
|
1757
|
+
description: "",
|
|
1758
|
+
status: "locked",
|
|
1759
|
+
order: 2,
|
|
1760
|
+
created: "2026-01-01T00:00:00Z",
|
|
1761
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1762
|
+
},
|
|
1763
|
+
]);
|
|
1764
|
+
|
|
1765
|
+
const task1 = makeTask({ id: "001", milestone: "M1", status: "done", fulfills: [] });
|
|
1766
|
+
saveTask(tmpDir, task1);
|
|
1767
|
+
saveValidationState(tmpDir, { assertions: {}, lastVerified: "2026-01-01T00:00:00Z" });
|
|
1768
|
+
|
|
1769
|
+
const config = makeOrchestratorConfig(tmpDir, { dispatchMode: "missions" });
|
|
1770
|
+
checkValidationResults(config, [task1]);
|
|
1771
|
+
|
|
1772
|
+
const mission = loadMission(tmpDir)!;
|
|
1773
|
+
expect(mission.milestones.find((m) => m.id === "M1")?.status).toBe("done");
|
|
1774
|
+
expect(mission.milestones.find((m) => m.id === "M2")?.status).toBe("active");
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
it("failed assertion creates remediation task and resets milestone to active", () => {
|
|
1778
|
+
setupMission([
|
|
1779
|
+
{
|
|
1780
|
+
id: "M1",
|
|
1781
|
+
title: "Phase 1",
|
|
1782
|
+
description: "",
|
|
1783
|
+
status: "validating",
|
|
1784
|
+
order: 1,
|
|
1785
|
+
created: "2026-01-01T00:00:00Z",
|
|
1786
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1787
|
+
},
|
|
1788
|
+
]);
|
|
1789
|
+
|
|
1790
|
+
const task1 = makeTask({ id: "001", milestone: "M1", status: "done", fulfills: ["A1", "A2"] });
|
|
1791
|
+
saveTask(tmpDir, task1);
|
|
1792
|
+
|
|
1793
|
+
saveValidationState(tmpDir, {
|
|
1794
|
+
assertions: {
|
|
1795
|
+
A1: {
|
|
1796
|
+
status: "passing",
|
|
1797
|
+
verifiedBy: "V",
|
|
1798
|
+
verifiedAt: "2026-01-01T00:00:00Z",
|
|
1799
|
+
evidence: "ok",
|
|
1800
|
+
blockedBy: null,
|
|
1801
|
+
},
|
|
1802
|
+
A2: {
|
|
1803
|
+
status: "failing",
|
|
1804
|
+
verifiedBy: "V",
|
|
1805
|
+
verifiedAt: "2026-01-01T00:00:00Z",
|
|
1806
|
+
evidence: "auth broken",
|
|
1807
|
+
blockedBy: null,
|
|
1808
|
+
},
|
|
1809
|
+
},
|
|
1810
|
+
lastVerified: "2026-01-01T00:00:00Z",
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
const config = makeOrchestratorConfig(tmpDir, { dispatchMode: "missions" });
|
|
1814
|
+
checkValidationResults(config, [task1]);
|
|
1815
|
+
|
|
1816
|
+
// Milestone should be back to active
|
|
1817
|
+
const mission = loadMission(tmpDir)!;
|
|
1818
|
+
const m1 = mission.milestones.find((m) => m.id === "M1")!;
|
|
1819
|
+
expect(m1.status).toBe("active");
|
|
1820
|
+
|
|
1821
|
+
// Remediation task should exist
|
|
1822
|
+
const allTasks = loadTasks(tmpDir);
|
|
1823
|
+
const remediation = allTasks.find((t) => t.tags.includes("remediation"));
|
|
1824
|
+
expect(remediation).toBeTruthy();
|
|
1825
|
+
expect(remediation!.milestone).toBe("M1");
|
|
1826
|
+
expect(remediation!.fulfills).toContain("A2");
|
|
1827
|
+
expect(remediation!.description).toContain("auth broken");
|
|
1828
|
+
|
|
1829
|
+
// Failed assertion should be reset to pending
|
|
1830
|
+
const valState = loadValidationState(tmpDir)!;
|
|
1831
|
+
expect(valState.assertions["A2"]?.status).toBe("pending");
|
|
1832
|
+
|
|
1833
|
+
// Events
|
|
1834
|
+
const events = readEvents(tmpDir);
|
|
1835
|
+
expect(events.find((e) => e.type === "remediation")).toBeTruthy();
|
|
1836
|
+
expect(events.find((e) => e.type === "validation_failed")).toBeTruthy();
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
it("remediation skips missing assertion entries without crashing", () => {
|
|
1840
|
+
setupMission([
|
|
1841
|
+
{
|
|
1842
|
+
id: "M1",
|
|
1843
|
+
title: "Phase 1",
|
|
1844
|
+
description: "",
|
|
1845
|
+
status: "validating",
|
|
1846
|
+
order: 1,
|
|
1847
|
+
created: "2026-01-01T00:00:00Z",
|
|
1848
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1849
|
+
},
|
|
1850
|
+
]);
|
|
1851
|
+
|
|
1852
|
+
const task1 = makeTask({ id: "001", milestone: "M1", status: "done", fulfills: ["A1", "A2"] });
|
|
1853
|
+
saveTask(tmpDir, task1);
|
|
1854
|
+
writeFileSync(
|
|
1855
|
+
join(tmpDir, ".tasks", "validation-state.json"),
|
|
1856
|
+
JSON.stringify({
|
|
1857
|
+
assertions: {
|
|
1858
|
+
A1: {
|
|
1859
|
+
status: "failing",
|
|
1860
|
+
verifiedBy: "V",
|
|
1861
|
+
verifiedAt: "2026-01-01T00:00:00Z",
|
|
1862
|
+
evidence: "broken",
|
|
1863
|
+
blockedBy: null,
|
|
1864
|
+
},
|
|
1865
|
+
A2: null,
|
|
1866
|
+
},
|
|
1867
|
+
lastVerified: "2026-01-01T00:00:00Z",
|
|
1868
|
+
}),
|
|
1869
|
+
);
|
|
1870
|
+
|
|
1871
|
+
const config = makeOrchestratorConfig(tmpDir, { dispatchMode: "missions" });
|
|
1872
|
+
expect(() => checkValidationResults(config, [task1])).not.toThrow();
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
it("buildValidationPrompt includes contract and task details", () => {
|
|
1876
|
+
writeContract("# Assertions\n- A1: Auth endpoint returns 200");
|
|
1877
|
+
|
|
1878
|
+
const milestone = {
|
|
1879
|
+
id: "M1",
|
|
1880
|
+
title: "Phase 1",
|
|
1881
|
+
description: "",
|
|
1882
|
+
status: "validating" as const,
|
|
1883
|
+
order: 1,
|
|
1884
|
+
created: "2026-01-01T00:00:00Z",
|
|
1885
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1886
|
+
};
|
|
1887
|
+
const tasks = [
|
|
1888
|
+
makeTask({
|
|
1889
|
+
id: "001",
|
|
1890
|
+
title: "Fix auth",
|
|
1891
|
+
fulfills: ["A1"],
|
|
1892
|
+
proof: { notes: "done" },
|
|
1893
|
+
salientSummary: "Patched JWT",
|
|
1894
|
+
}),
|
|
1895
|
+
];
|
|
1896
|
+
|
|
1897
|
+
const prompt = buildValidationPrompt(tmpDir, milestone, tasks);
|
|
1898
|
+
expect(prompt.includes("Validation: Phase 1")).toBeTruthy();
|
|
1899
|
+
expect(prompt.includes("Auth endpoint returns 200")).toBeTruthy();
|
|
1900
|
+
expect(prompt.includes("Fix auth")).toBeTruthy();
|
|
1901
|
+
expect(prompt.includes("A1")).toBeTruthy();
|
|
1902
|
+
expect(prompt.includes("Patched JWT")).toBeTruthy();
|
|
1903
|
+
expect(prompt.includes("tmux-ide validate assert")).toBeTruthy();
|
|
1904
|
+
});
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
describe("structured handoff", () => {
|
|
1908
|
+
it("logs salientSummary in completion event", () => {
|
|
1909
|
+
const task = makeTask({
|
|
1910
|
+
status: "done",
|
|
1911
|
+
assignee: "Agent 1",
|
|
1912
|
+
salientSummary: "Discovered JWT needs rotation",
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
const panes: PaneInfo[] = [makePane({ id: "%0", title: "Master" })];
|
|
1916
|
+
|
|
1917
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
1918
|
+
const state = makeOrchestratorState({
|
|
1919
|
+
previousTasks: new Map([["001", "in-progress"]]),
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
detectCompletions(config, state, [task], panes);
|
|
1923
|
+
|
|
1924
|
+
const events = readEvents(tmpDir);
|
|
1925
|
+
const completionEvent = events.find((e) => e.type === "completion");
|
|
1926
|
+
expect(completionEvent).toBeTruthy();
|
|
1927
|
+
expect(completionEvent!.message.includes("Discovered JWT needs rotation")).toBeTruthy();
|
|
1928
|
+
});
|
|
1929
|
+
|
|
1930
|
+
it("auto-creates follow-up tasks for discoveredIssues", () => {
|
|
1931
|
+
const task = makeTask({
|
|
1932
|
+
status: "done",
|
|
1933
|
+
assignee: "Agent 1",
|
|
1934
|
+
discoveredIssues: ["CSS z-index conflict in modal", "Missing rate limiting on /api/auth"],
|
|
1935
|
+
});
|
|
1936
|
+
saveTask(tmpDir, task);
|
|
1937
|
+
|
|
1938
|
+
const panes: PaneInfo[] = [makePane({ id: "%0", title: "Master" })];
|
|
1939
|
+
|
|
1940
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
1941
|
+
const state = makeOrchestratorState({
|
|
1942
|
+
previousTasks: new Map([["001", "in-progress"]]),
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
detectCompletions(config, state, [task], panes);
|
|
1946
|
+
|
|
1947
|
+
// Two follow-up tasks should be created
|
|
1948
|
+
const allTasks = loadTasks(tmpDir);
|
|
1949
|
+
const followUps = allTasks.filter((t) => t.tags.includes("discovered-issue"));
|
|
1950
|
+
expect(followUps.length).toBe(2);
|
|
1951
|
+
expect(followUps[0]!.description).toContain("CSS z-index");
|
|
1952
|
+
expect(followUps[1]!.description).toContain("rate limiting");
|
|
1953
|
+
|
|
1954
|
+
// Events
|
|
1955
|
+
const events = readEvents(tmpDir);
|
|
1956
|
+
const issueEvents = events.filter((e) => e.type === "discovered_issue");
|
|
1957
|
+
expect(issueEvents.length).toBe(2);
|
|
1958
|
+
});
|
|
1959
|
+
});
|
|
1960
|
+
|
|
1961
|
+
describe("rich dispatch prompts", () => {
|
|
1962
|
+
it("includes milestone context when task has milestone", () => {
|
|
1963
|
+
saveMission(tmpDir, {
|
|
1964
|
+
title: "Ship v2",
|
|
1965
|
+
description: "",
|
|
1966
|
+
status: "active",
|
|
1967
|
+
branch: null,
|
|
1968
|
+
milestones: [
|
|
1969
|
+
{
|
|
1970
|
+
id: "M1",
|
|
1971
|
+
title: "Foundation",
|
|
1972
|
+
description: "Core infra setup",
|
|
1973
|
+
status: "active",
|
|
1974
|
+
order: 1,
|
|
1975
|
+
created: "2026-01-01T00:00:00Z",
|
|
1976
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1977
|
+
},
|
|
1978
|
+
],
|
|
1979
|
+
created: "2026-01-01T00:00:00Z",
|
|
1980
|
+
updated: "2026-01-01T00:00:00Z",
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
const task = makeTask({ milestone: "M1" });
|
|
1984
|
+
const prompt = buildTaskPrompt(tmpDir, task);
|
|
1985
|
+
|
|
1986
|
+
expect(prompt.includes("Milestone: Foundation (M1)")).toBeTruthy();
|
|
1987
|
+
expect(prompt.includes("Core infra setup")).toBeTruthy();
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
it("includes AGENTS.md content when file exists", () => {
|
|
1991
|
+
writeFileSync(join(tmpDir, "AGENTS.md"), "Do not modify the auth module.\nStay in your lane.");
|
|
1992
|
+
|
|
1993
|
+
const task = makeTask();
|
|
1994
|
+
const prompt = buildTaskPrompt(tmpDir, task);
|
|
1995
|
+
|
|
1996
|
+
expect(prompt.includes("Agent Guidelines")).toBeTruthy();
|
|
1997
|
+
expect(prompt.includes("Do not modify the auth module")).toBeTruthy();
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
it("includes recent completions from same milestone", () => {
|
|
2001
|
+
saveMission(tmpDir, {
|
|
2002
|
+
title: "Ship v2",
|
|
2003
|
+
description: "",
|
|
2004
|
+
status: "active",
|
|
2005
|
+
branch: null,
|
|
2006
|
+
milestones: [
|
|
2007
|
+
{
|
|
2008
|
+
id: "M1",
|
|
2009
|
+
title: "Phase 1",
|
|
2010
|
+
description: "",
|
|
2011
|
+
status: "active",
|
|
2012
|
+
order: 1,
|
|
2013
|
+
created: "2026-01-01T00:00:00Z",
|
|
2014
|
+
updated: "2026-01-01T00:00:00Z",
|
|
2015
|
+
},
|
|
2016
|
+
],
|
|
2017
|
+
created: "2026-01-01T00:00:00Z",
|
|
2018
|
+
updated: "2026-01-01T00:00:00Z",
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
// Save a completed task in the same milestone
|
|
2022
|
+
saveTask(
|
|
2023
|
+
tmpDir,
|
|
2024
|
+
makeTask({
|
|
2025
|
+
id: "099",
|
|
2026
|
+
title: "Setup database",
|
|
2027
|
+
milestone: "M1",
|
|
2028
|
+
status: "done",
|
|
2029
|
+
assignee: "Agent 1",
|
|
2030
|
+
salientSummary: "Used Postgres with pgvector",
|
|
2031
|
+
}),
|
|
2032
|
+
);
|
|
2033
|
+
|
|
2034
|
+
const task = makeTask({ id: "100", milestone: "M1" });
|
|
2035
|
+
const prompt = buildTaskPrompt(tmpDir, task);
|
|
2036
|
+
|
|
2037
|
+
expect(prompt.includes("Recent Completions")).toBeTruthy();
|
|
2038
|
+
expect(prompt.includes("Setup database")).toBeTruthy();
|
|
2039
|
+
expect(prompt.includes("Used Postgres with pgvector")).toBeTruthy();
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
it("includes library architecture.md when present", () => {
|
|
2043
|
+
const libraryDir = join(tmpDir, ".tmux-ide", "library");
|
|
2044
|
+
mkdirSync(libraryDir, { recursive: true });
|
|
2045
|
+
writeFileSync(join(libraryDir, "architecture.md"), "Monorepo with pnpm workspaces");
|
|
2046
|
+
|
|
2047
|
+
const task = makeTask();
|
|
2048
|
+
const prompt = buildTaskPrompt(tmpDir, task);
|
|
2049
|
+
|
|
2050
|
+
expect(prompt.includes("Architecture")).toBeTruthy();
|
|
2051
|
+
expect(prompt.includes("Monorepo with pnpm workspaces")).toBeTruthy();
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
it("includes library files matching task tags", () => {
|
|
2055
|
+
const libraryDir = join(tmpDir, ".tmux-ide", "library");
|
|
2056
|
+
mkdirSync(libraryDir, { recursive: true });
|
|
2057
|
+
writeFileSync(join(libraryDir, "security.md"), "Always sanitize inputs");
|
|
2058
|
+
|
|
2059
|
+
const task = makeTask({ tags: ["security"] });
|
|
2060
|
+
const prompt = buildTaskPrompt(tmpDir, task);
|
|
2061
|
+
|
|
2062
|
+
expect(prompt.includes("Reference: security.md")).toBeTruthy();
|
|
2063
|
+
expect(prompt.includes("Always sanitize inputs")).toBeTruthy();
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
it("truncates large library files", () => {
|
|
2067
|
+
const libraryDir = join(tmpDir, ".tmux-ide", "library");
|
|
2068
|
+
mkdirSync(libraryDir, { recursive: true });
|
|
2069
|
+
writeFileSync(join(libraryDir, "architecture.md"), "x".repeat(600));
|
|
2070
|
+
|
|
2071
|
+
const task = makeTask();
|
|
2072
|
+
const prompt = buildTaskPrompt(tmpDir, task);
|
|
2073
|
+
|
|
2074
|
+
expect(prompt.includes("... (see full file at")).toBeTruthy();
|
|
2075
|
+
});
|
|
2076
|
+
|
|
2077
|
+
it("includes services when config provides them", () => {
|
|
2078
|
+
const task = makeTask();
|
|
2079
|
+
const config = makeOrchestratorConfig(tmpDir, {
|
|
2080
|
+
services: {
|
|
2081
|
+
dev: { command: "pnpm dev", port: 3000 },
|
|
2082
|
+
test: { command: "pnpm test" },
|
|
2083
|
+
},
|
|
2084
|
+
});
|
|
2085
|
+
const prompt = buildTaskPrompt(tmpDir, task, config);
|
|
2086
|
+
|
|
2087
|
+
expect(prompt.includes("Available Services")).toBeTruthy();
|
|
2088
|
+
expect(prompt.includes("pnpm dev")).toBeTruthy();
|
|
2089
|
+
expect(prompt.includes("port 3000")).toBeTruthy();
|
|
2090
|
+
expect(prompt.includes("pnpm test")).toBeTruthy();
|
|
2091
|
+
});
|
|
2092
|
+
|
|
2093
|
+
it("includes fulfills in task details", () => {
|
|
2094
|
+
const task = makeTask({ fulfills: ["A1", "A2"] });
|
|
2095
|
+
const prompt = buildTaskPrompt(tmpDir, task);
|
|
2096
|
+
expect(prompt.includes("Fulfills assertions: A1, A2")).toBeTruthy();
|
|
2097
|
+
});
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
describe("knowledge library", () => {
|
|
2101
|
+
it("appends salientSummary to learnings.md on task completion", () => {
|
|
2102
|
+
const task = makeTask({
|
|
2103
|
+
status: "done",
|
|
2104
|
+
assignee: "Agent 1",
|
|
2105
|
+
salientSummary: "JWT refresh tokens need 15min expiry",
|
|
2106
|
+
});
|
|
2107
|
+
|
|
2108
|
+
const panes: PaneInfo[] = [makePane({ id: "%0", title: "Master" })];
|
|
2109
|
+
|
|
2110
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
2111
|
+
const state = makeOrchestratorState({
|
|
2112
|
+
previousTasks: new Map([["001", "in-progress"]]),
|
|
2113
|
+
});
|
|
2114
|
+
|
|
2115
|
+
detectCompletions(config, state, [task], panes);
|
|
2116
|
+
|
|
2117
|
+
const learningsPath = join(tmpDir, ".tmux-ide", "library", "learnings.md");
|
|
2118
|
+
expect(existsSync(learningsPath)).toBeTruthy();
|
|
2119
|
+
const content = readFileSync(learningsPath, "utf-8");
|
|
2120
|
+
expect(content.includes("Task 001: Test task")).toBeTruthy();
|
|
2121
|
+
expect(content.includes("JWT refresh tokens need 15min expiry")).toBeTruthy();
|
|
2122
|
+
});
|
|
2123
|
+
|
|
2124
|
+
it("does not create learnings.md when no salientSummary", () => {
|
|
2125
|
+
const task = makeTask({
|
|
2126
|
+
status: "done",
|
|
2127
|
+
assignee: "Agent 1",
|
|
2128
|
+
salientSummary: null,
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
const panes: PaneInfo[] = [makePane({ id: "%0", title: "Master" })];
|
|
2132
|
+
|
|
2133
|
+
const config = makeOrchestratorConfig(tmpDir);
|
|
2134
|
+
const state = makeOrchestratorState({
|
|
2135
|
+
previousTasks: new Map([["001", "in-progress"]]),
|
|
2136
|
+
});
|
|
2137
|
+
|
|
2138
|
+
detectCompletions(config, state, [task], panes);
|
|
2139
|
+
|
|
2140
|
+
const learningsPath = join(tmpDir, ".tmux-ide", "library", "learnings.md");
|
|
2141
|
+
expect(existsSync(learningsPath)).toBe(false);
|
|
2142
|
+
});
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
describe("mission lifecycle", () => {
|
|
2146
|
+
it("dispatches planning when mission has no milestones", () => {
|
|
2147
|
+
saveMission(tmpDir, {
|
|
2148
|
+
title: "Ship v2",
|
|
2149
|
+
description: "Major release",
|
|
2150
|
+
status: "planning",
|
|
2151
|
+
branch: null,
|
|
2152
|
+
milestones: [],
|
|
2153
|
+
created: "2026-01-01T00:00:00Z",
|
|
2154
|
+
updated: "2026-01-01T00:00:00Z",
|
|
2155
|
+
});
|
|
2156
|
+
|
|
2157
|
+
const leadPane = makePane({
|
|
2158
|
+
id: "%0",
|
|
2159
|
+
index: 0,
|
|
2160
|
+
title: "Lead",
|
|
2161
|
+
currentCommand: "zsh",
|
|
2162
|
+
role: "lead",
|
|
2163
|
+
});
|
|
2164
|
+
mockPanes = [leadPane];
|
|
2165
|
+
|
|
2166
|
+
const config = makeOrchestratorConfig(tmpDir, { dispatchMode: "missions", masterPane: "Lead" });
|
|
2167
|
+
const state = makeOrchestratorState();
|
|
2168
|
+
|
|
2169
|
+
dispatchPlanning(config, state, [leadPane]);
|
|
2170
|
+
|
|
2171
|
+
// Should have claimed __planning__
|
|
2172
|
+
expect(state.claimedTasks.has("__planning__")).toBeTruthy();
|
|
2173
|
+
|
|
2174
|
+
// Should have sent command to lead pane
|
|
2175
|
+
const sendCalls = tmuxCalls.filter((c) => c.args.includes("send-keys"));
|
|
2176
|
+
expect(sendCalls.length).toBeGreaterThan(0);
|
|
2177
|
+
|
|
2178
|
+
// Dispatch file should exist
|
|
2179
|
+
expect(existsSync(join(tmpDir, ".tasks", "dispatch", "planning.md"))).toBeTruthy();
|
|
2180
|
+
|
|
2181
|
+
// Event should be logged
|
|
2182
|
+
const events = readEvents(tmpDir);
|
|
2183
|
+
expect(events.find((e) => e.type === "planning")).toBeTruthy();
|
|
2184
|
+
});
|
|
2185
|
+
|
|
2186
|
+
it("does not dispatch planning twice", () => {
|
|
2187
|
+
saveMission(tmpDir, {
|
|
2188
|
+
title: "Ship v2",
|
|
2189
|
+
description: "",
|
|
2190
|
+
status: "planning",
|
|
2191
|
+
branch: null,
|
|
2192
|
+
milestones: [],
|
|
2193
|
+
created: "2026-01-01T00:00:00Z",
|
|
2194
|
+
updated: "2026-01-01T00:00:00Z",
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
const leadPane = makePane({
|
|
2198
|
+
id: "%0",
|
|
2199
|
+
index: 0,
|
|
2200
|
+
title: "Lead",
|
|
2201
|
+
currentCommand: "zsh",
|
|
2202
|
+
role: "lead",
|
|
2203
|
+
});
|
|
2204
|
+
mockPanes = [leadPane];
|
|
2205
|
+
|
|
2206
|
+
const config = makeOrchestratorConfig(tmpDir, { dispatchMode: "missions" });
|
|
2207
|
+
const state = makeOrchestratorState();
|
|
2208
|
+
|
|
2209
|
+
dispatchPlanning(config, state, [leadPane]);
|
|
2210
|
+
const callCount1 = tmuxCalls.filter((c) => c.args.includes("send-keys")).length;
|
|
2211
|
+
|
|
2212
|
+
dispatchPlanning(config, state, [leadPane]);
|
|
2213
|
+
const callCount2 = tmuxCalls.filter((c) => c.args.includes("send-keys")).length;
|
|
2214
|
+
|
|
2215
|
+
// Second call should not generate additional send-keys
|
|
2216
|
+
expect(callCount2).toBe(callCount1);
|
|
2217
|
+
});
|
|
2218
|
+
|
|
2219
|
+
it("logs an error when planning cannot be dispatched", () => {
|
|
2220
|
+
saveMission(tmpDir, {
|
|
2221
|
+
title: "Ship v2",
|
|
2222
|
+
description: "",
|
|
2223
|
+
status: "planning",
|
|
2224
|
+
branch: null,
|
|
2225
|
+
milestones: [],
|
|
2226
|
+
created: "2026-01-01T00:00:00Z",
|
|
2227
|
+
updated: "2026-01-01T00:00:00Z",
|
|
2228
|
+
});
|
|
2229
|
+
|
|
2230
|
+
const config = makeOrchestratorConfig(tmpDir, { dispatchMode: "missions" });
|
|
2231
|
+
const state = makeOrchestratorState();
|
|
2232
|
+
|
|
2233
|
+
dispatchPlanning(config, state, []);
|
|
2234
|
+
|
|
2235
|
+
const events = readEvents(tmpDir);
|
|
2236
|
+
const errorEvent = events.find((e) => e.type === "error");
|
|
2237
|
+
expect(errorEvent).toBeTruthy();
|
|
2238
|
+
expect(errorEvent?.message).toContain("no planner pane available");
|
|
2239
|
+
expect(state.claimedTasks.has("__planning__")).toBe(false);
|
|
2240
|
+
});
|
|
2241
|
+
|
|
2242
|
+
it("buildPlanningPrompt includes mission context and instructions", () => {
|
|
2243
|
+
saveMission(tmpDir, {
|
|
2244
|
+
title: "Ship v2",
|
|
2245
|
+
description: "Complete rewrite of auth",
|
|
2246
|
+
status: "planning",
|
|
2247
|
+
branch: null,
|
|
2248
|
+
milestones: [],
|
|
2249
|
+
created: "2026-01-01T00:00:00Z",
|
|
2250
|
+
updated: "2026-01-01T00:00:00Z",
|
|
2251
|
+
});
|
|
2252
|
+
|
|
2253
|
+
const prompt = buildPlanningPrompt(tmpDir);
|
|
2254
|
+
|
|
2255
|
+
expect(prompt.includes("Mission Planning")).toBeTruthy();
|
|
2256
|
+
expect(prompt.includes("Ship v2")).toBeTruthy();
|
|
2257
|
+
expect(prompt.includes("Complete rewrite of auth")).toBeTruthy();
|
|
2258
|
+
expect(prompt.includes("milestone create")).toBeTruthy();
|
|
2259
|
+
expect(prompt.includes("validation-contract.md")).toBeTruthy();
|
|
2260
|
+
expect(prompt.includes("plan-complete")).toBeTruthy();
|
|
2261
|
+
});
|
|
2262
|
+
|
|
2263
|
+
it("handleMissionComplete sets status and logs event", () => {
|
|
2264
|
+
const mission = {
|
|
2265
|
+
title: "Ship v2",
|
|
2266
|
+
description: "Major release",
|
|
2267
|
+
status: "active" as const,
|
|
2268
|
+
branch: null,
|
|
2269
|
+
milestones: [
|
|
2270
|
+
{
|
|
2271
|
+
id: "M1",
|
|
2272
|
+
title: "Phase 1",
|
|
2273
|
+
description: "",
|
|
2274
|
+
status: "done" as const,
|
|
2275
|
+
order: 1,
|
|
2276
|
+
created: "2026-01-01T00:00:00Z",
|
|
2277
|
+
updated: "2026-01-01T00:00:00Z",
|
|
2278
|
+
},
|
|
2279
|
+
],
|
|
2280
|
+
created: "2026-01-01T00:00:00Z",
|
|
2281
|
+
updated: "2026-01-01T00:00:00Z",
|
|
2282
|
+
};
|
|
2283
|
+
saveMission(tmpDir, mission);
|
|
2284
|
+
|
|
2285
|
+
const masterPane = makePane({ id: "%0", title: "Master", role: "lead" });
|
|
2286
|
+
mockPanes = [masterPane];
|
|
2287
|
+
|
|
2288
|
+
const config = makeOrchestratorConfig(tmpDir, { dispatchMode: "missions" });
|
|
2289
|
+
handleMissionComplete(config, mission, [masterPane]);
|
|
2290
|
+
|
|
2291
|
+
// Mission should be complete
|
|
2292
|
+
const updated = loadMission(tmpDir)!;
|
|
2293
|
+
expect(updated.status).toBe("complete");
|
|
2294
|
+
|
|
2295
|
+
// Dispatch file should exist
|
|
2296
|
+
expect(existsSync(join(tmpDir, ".tasks", "dispatch", "mission-complete.md"))).toBeTruthy();
|
|
2297
|
+
|
|
2298
|
+
// Event should be logged
|
|
2299
|
+
const events = readEvents(tmpDir);
|
|
2300
|
+
const missionEvent = events.find((e) => e.type === "mission_complete");
|
|
2301
|
+
expect(missionEvent).toBeTruthy();
|
|
2302
|
+
expect(missionEvent!.message.includes("Ship v2")).toBeTruthy();
|
|
2303
|
+
|
|
2304
|
+
// Master pane should be notified
|
|
2305
|
+
const sendCalls = tmuxCalls.filter(
|
|
2306
|
+
(c) => c.args.includes("send-keys") && c.args.includes("%0"),
|
|
2307
|
+
);
|
|
2308
|
+
expect(sendCalls.length).toBeGreaterThan(0);
|
|
2309
|
+
});
|
|
2310
|
+
|
|
2311
|
+
it("handleMissionComplete returns early when milestones are not done", () => {
|
|
2312
|
+
const mission = {
|
|
2313
|
+
title: "Ship v2",
|
|
2314
|
+
description: "Major release",
|
|
2315
|
+
status: "active" as const,
|
|
2316
|
+
branch: null,
|
|
2317
|
+
milestones: [
|
|
2318
|
+
{
|
|
2319
|
+
id: "M1",
|
|
2320
|
+
title: "Phase 1",
|
|
2321
|
+
description: "",
|
|
2322
|
+
status: "active" as const,
|
|
2323
|
+
order: 1,
|
|
2324
|
+
created: "2026-01-01T00:00:00Z",
|
|
2325
|
+
updated: "2026-01-01T00:00:00Z",
|
|
2326
|
+
},
|
|
2327
|
+
],
|
|
2328
|
+
created: "2026-01-01T00:00:00Z",
|
|
2329
|
+
updated: "2026-01-01T00:00:00Z",
|
|
2330
|
+
};
|
|
2331
|
+
saveMission(tmpDir, mission);
|
|
2332
|
+
|
|
2333
|
+
const config = makeOrchestratorConfig(tmpDir, { dispatchMode: "missions" });
|
|
2334
|
+
handleMissionComplete(config, mission, []);
|
|
2335
|
+
|
|
2336
|
+
expect(loadMission(tmpDir)?.status).toBe("active");
|
|
2337
|
+
expect(existsSync(join(tmpDir, ".tasks", "dispatch", "mission-complete.md"))).toBe(false);
|
|
2338
|
+
});
|
|
2339
|
+
|
|
2340
|
+
it("mission create sets status to planning", () => {
|
|
2341
|
+
// This tests the data model — mission create in task.ts sets status: "planning"
|
|
2342
|
+
// Verify via direct mission construction (CLI test would need process spawn)
|
|
2343
|
+
const now = new Date().toISOString();
|
|
2344
|
+
saveMission(tmpDir, {
|
|
2345
|
+
title: "New Project",
|
|
2346
|
+
description: "Big thing",
|
|
2347
|
+
status: "planning",
|
|
2348
|
+
branch: null,
|
|
2349
|
+
milestones: [],
|
|
2350
|
+
created: now,
|
|
2351
|
+
updated: now,
|
|
2352
|
+
});
|
|
2353
|
+
|
|
2354
|
+
const mission = loadMission(tmpDir)!;
|
|
2355
|
+
expect(mission.status).toBe("planning");
|
|
2356
|
+
expect(mission.milestones.length).toBe(0);
|
|
2357
|
+
});
|
|
2358
|
+
});
|