@armanckeser/downstream 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +133 -2
- package/SKILL.md +150 -0
- package/bin/downstream.mjs +19 -0
- package/dist/cli/downstream.js +543 -0
- package/dist/domain/agent.js +3 -0
- package/dist/domain/approval.js +190 -0
- package/dist/domain/delivery.js +21 -0
- package/dist/domain/home.js +1 -0
- package/dist/domain/inbox.js +245 -0
- package/dist/domain/model.js +180 -0
- package/dist/domain/next-move.js +34 -0
- package/dist/domain/pr-detail.js +45 -0
- package/dist/server/actions.js +564 -0
- package/dist/server/agent-host.js +230 -0
- package/dist/server/analyze/git.js +161 -0
- package/dist/server/analyze/graph.js +403 -0
- package/dist/server/analyze/py-graph.js +141 -0
- package/dist/server/analyze/py.js +346 -0
- package/dist/server/analyze/ts-decls.js +299 -0
- package/dist/server/analyze/ts-project.js +212 -0
- package/dist/server/gh.js +51 -0
- package/dist/server/github.js +264 -0
- package/dist/server/home-store.js +127 -0
- package/dist/server/home.js +325 -0
- package/dist/server/install.js +12 -0
- package/dist/server/main.js +228 -0
- package/dist/server/prompts.js +32 -0
- package/dist/server/publish.js +136 -0
- package/dist/server/repos.js +164 -0
- package/dist/server/stale.js +50 -0
- package/dist/server/store.js +269 -0
- package/dist/server/workspace.js +251 -0
- package/package.json +63 -4
- package/web/dist/assets/abap-CLvhMVsD.js +1 -0
- package/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
- package/web/dist/assets/ada-X3Y2mlq2.js +1 -0
- package/web/dist/assets/ahk-DwADkxyd.js +1 -0
- package/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
- package/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
- package/web/dist/assets/angular-html-iITFzleb.js +1 -0
- package/web/dist/assets/angular-ts-qNGaWUZr.js +1 -0
- package/web/dist/assets/apache-U0d_L8uA.js +1 -0
- package/web/dist/assets/apex-KFGFyKwG.js +1 -0
- package/web/dist/assets/apl-DAcxbtpZ.js +1 -0
- package/web/dist/assets/applescript-CCn79oCD.js +1 -0
- package/web/dist/assets/ara-4CJ0cIlV.js +1 -0
- package/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
- package/web/dist/assets/asm-Cmm7eHzH.js +1 -0
- package/web/dist/assets/astro-CxPQu5s8.js +1 -0
- package/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
- package/web/dist/assets/awk-BWXHIvNe.js +1 -0
- package/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
- package/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
- package/web/dist/assets/bat-qdxAdBWQ.js +1 -0
- package/web/dist/assets/beancount-C2IN4Vk8.js +1 -0
- package/web/dist/assets/berry-DKpUyyne.js +1 -0
- package/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
- package/web/dist/assets/bicep-CUHmPFLl.js +1 -0
- package/web/dist/assets/bird2-CYIUgYqB.js +1 -0
- package/web/dist/assets/blade-DJrnOqL4.js +1 -0
- package/web/dist/assets/bsl-_VjlB9mv.js +1 -0
- package/web/dist/assets/c-DJLzn8qa.js +1 -0
- package/web/dist/assets/c3-D0apG41V.js +1 -0
- package/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
- package/web/dist/assets/cairo-DLTphjLi.js +1 -0
- package/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
- package/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
- package/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
- package/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
- package/web/dist/assets/chapel-BHYX99F3.js +1 -0
- package/web/dist/assets/clarity-CtfBBspg.js +1 -0
- package/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
- package/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
- package/web/dist/assets/cobol-C_LDam8n.js +1 -0
- package/web/dist/assets/codeowners-C8r90Shi.js +1 -0
- package/web/dist/assets/codeql-oeQT6MSM.js +1 -0
- package/web/dist/assets/coffee-CcuifkxZ.js +1 -0
- package/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/web/dist/assets/coq-CGaSfhwR.js +1 -0
- package/web/dist/assets/cpp-E7p80IG9.js +1 -0
- package/web/dist/assets/crystal-iYHODIir.js +1 -0
- package/web/dist/assets/csharp-dmMxLVLW.js +1 -0
- package/web/dist/assets/css-Bk6njqO4.js +1 -0
- package/web/dist/assets/csv-Dx-8-gkx.js +1 -0
- package/web/dist/assets/cue-CE9AQfxI.js +1 -0
- package/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
- package/web/dist/assets/d-qD-0Kul2.js +1 -0
- package/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
- package/web/dist/assets/dart-CHZY9Xhz.js +1 -0
- package/web/dist/assets/dax-BkyTk9wS.js +1 -0
- package/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
- package/web/dist/assets/diff-woXpYk--.js +1 -0
- package/web/dist/assets/docker-IyjqRm3v.js +1 -0
- package/web/dist/assets/dotenv-CuH5my32.js +1 -0
- package/web/dist/assets/dracula-BHWKrbxM.js +1 -0
- package/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
- package/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/web/dist/assets/edge-Ct7_SkpN.js +1 -0
- package/web/dist/assets/elixir-YTzSQL2h.js +1 -0
- package/web/dist/assets/elm-bNkwlHr8.js +1 -0
- package/web/dist/assets/emacs-lisp-DR2vtWaj.js +1 -0
- package/web/dist/assets/erb-uFBspDdG.js +1 -0
- package/web/dist/assets/erlang-Cphh6RMH.js +1 -0
- package/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
- package/web/dist/assets/fennel-DQxkIbk2.js +1 -0
- package/web/dist/assets/fish-BJitypiv.js +1 -0
- package/web/dist/assets/fluent-C03EYrpw.js +1 -0
- package/web/dist/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
- package/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/web/dist/assets/fsharp-D13ZGOAj.js +1 -0
- package/web/dist/assets/gdresource-xMC7jr7x.js +1 -0
- package/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
- package/web/dist/assets/gdshader-CBce3t8t.js +1 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/genie-CV2tkWYe.js +1 -0
- package/web/dist/assets/gherkin-DExj1W_8.js +1 -0
- package/web/dist/assets/git-commit-BSykSTBG.js +1 -0
- package/web/dist/assets/git-rebase-CSzaMu4k.js +1 -0
- package/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
- package/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/web/dist/assets/github-light-EUqPIrTm.js +1 -0
- package/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
- package/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/web/dist/assets/gleam-CSRkHgEL.js +1 -0
- package/web/dist/assets/glimmer-js-WrpQIag3.js +1 -0
- package/web/dist/assets/glimmer-ts-BkR7s81M.js +1 -0
- package/web/dist/assets/glsl-r6ahr8UN.js +1 -0
- package/web/dist/assets/gn-CAXYAgV-.js +1 -0
- package/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
- package/web/dist/assets/go-rLFTqkRN.js +1 -0
- package/web/dist/assets/graphql-vcFzEvUM.js +1 -0
- package/web/dist/assets/groovy-CacY0gHj.js +1 -0
- package/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/web/dist/assets/hack-DV1TStQR.js +1 -0
- package/web/dist/assets/haml-BzEpEFnM.js +1 -0
- package/web/dist/assets/handlebars-Bw2ai2tn.js +1 -0
- package/web/dist/assets/haskell-D8IpX4py.js +1 -0
- package/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
- package/web/dist/assets/hcl-Dh228itO.js +1 -0
- package/web/dist/assets/hjson-CxZEssPk.js +1 -0
- package/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
- package/web/dist/assets/horizon-CE9ld1lL.js +1 -0
- package/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
- package/web/dist/assets/houston-CsvMBhTu.js +1 -0
- package/web/dist/assets/html-CVWJgEI4.js +1 -0
- package/web/dist/assets/html-derivative-kTFEcoss.js +1 -0
- package/web/dist/assets/http-DXC6PGgt.js +1 -0
- package/web/dist/assets/hurl-Bpd8P_ef.js +1 -0
- package/web/dist/assets/hxml-Dcm88srW.js +1 -0
- package/web/dist/assets/hy-CZbG8q4J.js +1 -0
- package/web/dist/assets/imba-DsUTQ-LC.js +1 -0
- package/web/dist/assets/index-DpYMsPxu.js +1673 -0
- package/web/dist/assets/index-Zh-Bd2u6.css +2 -0
- package/web/dist/assets/ini-B5eOa1yu.js +1 -0
- package/web/dist/assets/java-Bsmh9nXE.js +1 -0
- package/web/dist/assets/javascript-CeyaFN_P.js +1 -0
- package/web/dist/assets/jinja-BuqTDofy.js +1 -0
- package/web/dist/assets/jison-BYfmmaBZ.js +1 -0
- package/web/dist/assets/json-Bhl6cDM_.js +1 -0
- package/web/dist/assets/json5-BR5RXkoi.js +1 -0
- package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
- package/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
- package/web/dist/assets/jsx-BSUHjwDX.js +1 -0
- package/web/dist/assets/julia-CWrNF0wf.js +1 -0
- package/web/dist/assets/just-wLw7gWsD.js +1 -0
- package/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
- package/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
- package/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
- package/web/dist/assets/kdl-CsD5j6eV.js +1 -0
- package/web/dist/assets/kotlin-DhhofPvG.js +1 -0
- package/web/dist/assets/kusto-C7mF5XQf.js +1 -0
- package/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
- package/web/dist/assets/latex-DyWaeH0P.js +1 -0
- package/web/dist/assets/lean-CewbzKMR.js +1 -0
- package/web/dist/assets/less-DVTAwKKz.js +1 -0
- package/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
- package/web/dist/assets/liquid-DeCc5Rg2.js +1 -0
- package/web/dist/assets/llvm-DqHOMS-s.js +1 -0
- package/web/dist/assets/log-BNLmms1o.js +1 -0
- package/web/dist/assets/logo-Cluzi2Zq.js +1 -0
- package/web/dist/assets/lua-Ba-NltBp.js +1 -0
- package/web/dist/assets/luau-VGWVLoqX.js +1 -0
- package/web/dist/assets/make-Dixweg8N.js +1 -0
- package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/web/dist/assets/marko-DGb2HMVj.js +1 -0
- package/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
- package/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/web/dist/assets/matlab-D7qyCx1q.js +1 -0
- package/web/dist/assets/mdc-CL7bhRQ-.js +1 -0
- package/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
- package/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
- package/web/dist/assets/min-dark-BSWPekZh.js +1 -0
- package/web/dist/assets/min-light-DDpmG2fV.js +1 -0
- package/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
- package/web/dist/assets/mojo-fg40_ntQ.js +1 -0
- package/web/dist/assets/monokai-BscAjiQZ.js +1 -0
- package/web/dist/assets/moonbit-D9W784E_.js +1 -0
- package/web/dist/assets/move-BYnGhZoM.js +1 -0
- package/web/dist/assets/narrat-_X_XdTYD.js +1 -0
- package/web/dist/assets/nextflow-CSgcYZcx.js +1 -0
- package/web/dist/assets/nextflow-groovy-D7IRt6Pp.js +1 -0
- package/web/dist/assets/nginx-CbyuJibN.js +1 -0
- package/web/dist/assets/night-owl-DhmEMT88.js +1 -0
- package/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/web/dist/assets/nim-C7LemgDR.js +1 -0
- package/web/dist/assets/nix-IvuFDN5E.js +1 -0
- package/web/dist/assets/nord-Cb4Vim4T.js +1 -0
- package/web/dist/assets/nsis-CjNLYCLg.js +1 -0
- package/web/dist/assets/nushell--0fT3PI1.js +1 -0
- package/web/dist/assets/objective-c-D1A_Heim.js +1 -0
- package/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/web/dist/assets/ocaml-O90oeIOV.js +1 -0
- package/web/dist/assets/odin-B1RWQWA5.js +1 -0
- package/web/dist/assets/one-dark-pro-C6YyR8OD.js +1 -0
- package/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
- package/web/dist/assets/openscad-BUDT5pXO.js +1 -0
- package/web/dist/assets/org-wrbLazmP.js +1 -0
- package/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
- package/web/dist/assets/perl-DarHXyEu.js +1 -0
- package/web/dist/assets/php--54M0-pj.js +1 -0
- package/web/dist/assets/pierre-dark-CpLgRqie.js +1 -0
- package/web/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
- package/web/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
- package/web/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
- package/web/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
- package/web/dist/assets/pierre-light-CoaEpmwp.js +1 -0
- package/web/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
- package/web/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
- package/web/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
- package/web/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
- package/web/dist/assets/pkl-ot-7Btpt.js +1 -0
- package/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
- package/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
- package/web/dist/assets/po-BiJDBrnU.js +1 -0
- package/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
- package/web/dist/assets/polar-C7UOKdEL.js +1 -0
- package/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
- package/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
- package/web/dist/assets/powershell-QtSnmXcu.js +1 -0
- package/web/dist/assets/prisma-DMgNhnYS.js +1 -0
- package/web/dist/assets/prolog-iXnhIJG7.js +1 -0
- package/web/dist/assets/proto-DB4EqR-F.js +1 -0
- package/web/dist/assets/pug-CFrC1rDC.js +1 -0
- package/web/dist/assets/puppet-CDv2pdJW.js +1 -0
- package/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
- package/web/dist/assets/python-gzcpVVnB.js +1 -0
- package/web/dist/assets/qml-Go4B46qX.js +1 -0
- package/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
- package/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
- package/web/dist/assets/r-Bpu17F3f.js +1 -0
- package/web/dist/assets/racket-DcIDlBhZ.js +1 -0
- package/web/dist/assets/raku-B3gFvitq.js +1 -0
- package/web/dist/assets/razor-CeZTEIIg.js +1 -0
- package/web/dist/assets/rbs-C0MpOrj4.js +1 -0
- package/web/dist/assets/red-CYlAq2b0.js +1 -0
- package/web/dist/assets/reg-CRGYupPL.js +1 -0
- package/web/dist/assets/regexp-D4K6CiTG.js +1 -0
- package/web/dist/assets/rel-BtDbiS_P.js +1 -0
- package/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
- package/web/dist/assets/ron-VUp2lXgN.js +1 -0
- package/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
- package/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
- package/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
- package/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
- package/web/dist/assets/rst-Dy2Bl1Fg.js +1 -0
- package/web/dist/assets/ruby-CNqr2Wuz.js +1 -0
- package/web/dist/assets/rust-DpiIf5JO.js +1 -0
- package/web/dist/assets/sas-B6MsvXFb.js +1 -0
- package/web/dist/assets/sass-DXrisJhu.js +1 -0
- package/web/dist/assets/scala-BiFt_7mu.js +1 -0
- package/web/dist/assets/scheme-DQCgrYNe.js +1 -0
- package/web/dist/assets/scss-CHuFWGi8.js +1 -0
- package/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
- package/web/dist/assets/shaderlab-TOUzSsQk.js +1 -0
- package/web/dist/assets/shellscript-EEdMjEbs.js +1 -0
- package/web/dist/assets/shellsession-U_I5Q2cJ.js +1 -0
- package/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
- package/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
- package/web/dist/assets/smithy-Dgaz7aur.js +1 -0
- package/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/web/dist/assets/solarized-dark-b97czJOk.js +1 -0
- package/web/dist/assets/solarized-light-DFPxpIYQ.js +1 -0
- package/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
- package/web/dist/assets/soy-BnCBZkG3.js +1 -0
- package/web/dist/assets/sparql-D_iOobhT.js +1 -0
- package/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
- package/web/dist/assets/sql-5dStcFSw.js +1 -0
- package/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
- package/web/dist/assets/stata-B5iSQdtS.js +1 -0
- package/web/dist/assets/stylus-B6D30XZt.js +1 -0
- package/web/dist/assets/surrealql-D4K5msa4.js +1 -0
- package/web/dist/assets/svelte-Dn-_7BgE.js +1 -0
- package/web/dist/assets/swift-CyEgAFGc.js +1 -0
- package/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
- package/web/dist/assets/systemd-BxMlprV5.js +1 -0
- package/web/dist/assets/talonscript-CohzipZa.js +1 -0
- package/web/dist/assets/tasl-DMoTqEGO.js +1 -0
- package/web/dist/assets/tcl-CZd0xW_V.js +1 -0
- package/web/dist/assets/templ-BTe8yLW8.js +1 -0
- package/web/dist/assets/terraform-DswuEJGm.js +1 -0
- package/web/dist/assets/tex-BBG77enc.js +1 -0
- package/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/web/dist/assets/toml-CcmNWLt0.js +1 -0
- package/web/dist/assets/ts-tags-BzJXXnMD.js +1 -0
- package/web/dist/assets/tsv-sltzmVWM.js +1 -0
- package/web/dist/assets/tsx-Cj2inrAP.js +1 -0
- package/web/dist/assets/turtle-ByJddavk.js +1 -0
- package/web/dist/assets/twig-BH3HU2q4.js +1 -0
- package/web/dist/assets/typescript-DpDozqSz.js +1 -0
- package/web/dist/assets/typespec-BGhR2SqG.js +1 -0
- package/web/dist/assets/typst-GHzvrUad.js +1 -0
- package/web/dist/assets/v-DBj4K9d_.js +1 -0
- package/web/dist/assets/vala-zf12oZj6.js +1 -0
- package/web/dist/assets/vb-DOwzaHZ0.js +1 -0
- package/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
- package/web/dist/assets/vesper-DdrHHSXu.js +1 -0
- package/web/dist/assets/vhdl-BroJfC0k.js +1 -0
- package/web/dist/assets/viml-DvXPmvsu.js +1 -0
- package/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
- package/web/dist/assets/vue-CqkncxvB.js +1 -0
- package/web/dist/assets/vue-html-DrD64fqb.js +1 -0
- package/web/dist/assets/vue-vine-BJy37wv2.js +1 -0
- package/web/dist/assets/vyper-CgoNMtux.js +1 -0
- package/web/dist/assets/wasm-BnjxR4X6.js +1 -0
- package/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
- package/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
- package/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
- package/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
- package/web/dist/assets/wit-DdvCle-K.js +1 -0
- package/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
- package/web/dist/assets/xml-CtXDvb3g.js +1 -0
- package/web/dist/assets/xsl-9J2XPEqv.js +1 -0
- package/web/dist/assets/yaml-CKsrpaXo.js +1 -0
- package/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
- package/web/dist/assets/zig-CMLA9XwU.js +1 -0
- package/web/dist/favicon.svg +1 -0
- package/web/dist/index.html +15 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Which of the agent's tool calls run without asking you. Reading never needs
|
|
2
|
+
// permission; changing anything (files, git history, GitHub) does, and so does
|
|
3
|
+
// sending the review, which the agent must never do on its own.
|
|
4
|
+
/** Tools that only read. */
|
|
5
|
+
const READ_TOOLS = new Set(["Read", "Grep", "Glob", "LS", "NotebookRead", "TodoWrite", "WebFetch", "WebSearch", "Skill", "ToolSearch", "Agent", "Task", "TaskOutput"]);
|
|
6
|
+
/**
|
|
7
|
+
* Shell commands that only read, and the flags that would make them run or
|
|
8
|
+
* write something (`rg --pre`, `fd --exec`, `sort -o`, `sed -i`, `find -delete`).
|
|
9
|
+
*/
|
|
10
|
+
const READ_COMMANDS = {
|
|
11
|
+
cat: null, head: null, tail: null, wc: null, ls: null, grep: null, cut: null, tr: null, echo: null, printf: null, pwd: null,
|
|
12
|
+
which: null, file: null, stat: null, diff: null, basename: null, dirname: null, realpath: null, true: null, jq: null, nl: null, column: null, uniq: null,
|
|
13
|
+
read: null,
|
|
14
|
+
rg: /^--pre/,
|
|
15
|
+
fd: /^(-x|-X|--exec|--exec-batch)/,
|
|
16
|
+
sort: /^(-o|--output)/,
|
|
17
|
+
tree: /^-o$/,
|
|
18
|
+
sed: /^(-i|--in-place)/,
|
|
19
|
+
find: /^-(exec|execdir|delete|ok|fprint)/,
|
|
20
|
+
};
|
|
21
|
+
const GIT_READ = new Set(["log", "show", "diff", "status", "rev-parse", "merge-base", "ls-files", "blame", "grep", "fetch", "cat-file", "describe", "shortlog", "rev-list", "ls-tree", "remote"]);
|
|
22
|
+
const GH_READ = {
|
|
23
|
+
pr: new Set(["view", "diff", "checks", "list", "status"]),
|
|
24
|
+
repo: new Set(["view"]),
|
|
25
|
+
run: new Set(["view", "list"]),
|
|
26
|
+
issue: new Set(["view", "list"]),
|
|
27
|
+
};
|
|
28
|
+
/** Downstream subcommands that publish: always yours to approve. */
|
|
29
|
+
const DOWNSTREAM_PUBLISH = new Set(["send"]);
|
|
30
|
+
/**
|
|
31
|
+
* `skillDir` is where Downstream lives, so `node <skillDir>/bin/downstream.mjs …`
|
|
32
|
+
* is recognized as the Downstream CLI.
|
|
33
|
+
*/
|
|
34
|
+
export function autoApprove(tool, input, skillDir) {
|
|
35
|
+
if (READ_TOOLS.has(tool))
|
|
36
|
+
return { allow: true };
|
|
37
|
+
if (tool !== "Bash")
|
|
38
|
+
return { allow: false, reason: `${tool} can change things` };
|
|
39
|
+
const command = typeof input.command === "string" ? input.command : "";
|
|
40
|
+
return readOnlyShell(command, skillDir);
|
|
41
|
+
}
|
|
42
|
+
function readOnlyShell(command, skillDir) {
|
|
43
|
+
// Discard harmless stderr plumbing first, then refuse anything that could write or hide a second command.
|
|
44
|
+
const plain = command.replace(/\s2>&1/g, " ").replace(/\s2>\s*\/dev\/null/g, " ");
|
|
45
|
+
const substituted = substitutions(plain, skillDir);
|
|
46
|
+
if ("allow" in substituted)
|
|
47
|
+
return substituted;
|
|
48
|
+
if (/[>`]|\$\(|<\(|\bsudo\b/.test(substituted.rest))
|
|
49
|
+
return { allow: false, reason: "redirects, substitutions or sudo" };
|
|
50
|
+
for (const raw of substituted.rest.split(/&&|\|\||;|\||\n/)) {
|
|
51
|
+
const words = withoutLoopKeywords(raw.trim().split(/\s+/).filter(Boolean));
|
|
52
|
+
if (words.length === 0)
|
|
53
|
+
continue;
|
|
54
|
+
const verdict = readOnlyCommand(words, skillDir);
|
|
55
|
+
if (!verdict.allow)
|
|
56
|
+
return verdict;
|
|
57
|
+
}
|
|
58
|
+
return { allow: true };
|
|
59
|
+
}
|
|
60
|
+
/** Stands in for a `$(…)` already judged read-only: an expansion like `$f`, so it gets the same suspicion. */
|
|
61
|
+
const SUBSTITUTED = "$SUBSTITUTED";
|
|
62
|
+
/** Judge each `$(…)` on its own and replace it with an expansion; a refusal inside refuses the whole command. */
|
|
63
|
+
function substitutions(command, skillDir) {
|
|
64
|
+
let rest = command;
|
|
65
|
+
for (let start = rest.indexOf("$("); start !== -1; start = rest.indexOf("$(")) {
|
|
66
|
+
const end = closingParen(rest, start + 2);
|
|
67
|
+
if (end === -1)
|
|
68
|
+
return { allow: false, reason: "an unclosed substitution" };
|
|
69
|
+
const inner = readOnlyShell(rest.slice(start + 2, end), skillDir);
|
|
70
|
+
if (!inner.allow)
|
|
71
|
+
return inner;
|
|
72
|
+
rest = `${rest.slice(0, start)}${SUBSTITUTED}${rest.slice(end + 1)}`;
|
|
73
|
+
}
|
|
74
|
+
return { rest };
|
|
75
|
+
}
|
|
76
|
+
function closingParen(text, from) {
|
|
77
|
+
let depth = 1;
|
|
78
|
+
for (let i = from; i < text.length; i++) {
|
|
79
|
+
if (text[i] === "(")
|
|
80
|
+
depth++;
|
|
81
|
+
if (text[i] === ")" && --depth === 0)
|
|
82
|
+
return i;
|
|
83
|
+
}
|
|
84
|
+
return -1;
|
|
85
|
+
}
|
|
86
|
+
/** `for f in …; do cat $f; done`: the loop's words are syntax; what runs is the command after them. */
|
|
87
|
+
function withoutLoopKeywords(words) {
|
|
88
|
+
const [first] = words;
|
|
89
|
+
if (first === "done" || first === "fi")
|
|
90
|
+
return words.slice(1);
|
|
91
|
+
if (first === "for")
|
|
92
|
+
return [];
|
|
93
|
+
if (first === "do" || first === "then" || first === "else" || first === "while" || first === "until" || first === "if")
|
|
94
|
+
return withoutLoopKeywords(words.slice(1));
|
|
95
|
+
return words;
|
|
96
|
+
}
|
|
97
|
+
/** Programs people often call by path (`~/.local/bin/gh`, `/usr/bin/git`): judged by name, like the bare command. */
|
|
98
|
+
const BY_NAME = new Set(["gh", "git"]);
|
|
99
|
+
function readOnlyCommand(words, skillDir) {
|
|
100
|
+
const [first, ...args] = words;
|
|
101
|
+
const base = first.split("/").at(-1);
|
|
102
|
+
const cmd = first.includes("/") && BY_NAME.has(base) ? base : first;
|
|
103
|
+
if (cmd === "cd")
|
|
104
|
+
return { allow: true };
|
|
105
|
+
if (cmd.includes("$"))
|
|
106
|
+
return { allow: false, reason: "the program itself is an expansion" };
|
|
107
|
+
// An expanded value is unseen text: `git show $f` with f = `x --output=~/.zshrc` writes a file. Only programs no flag can turn into a write take one.
|
|
108
|
+
const expanded = args.some((a) => a.includes("$"));
|
|
109
|
+
if (Object.hasOwn(READ_COMMANDS, cmd)) {
|
|
110
|
+
const risky = READ_COMMANDS[cmd];
|
|
111
|
+
if (risky && expanded)
|
|
112
|
+
return { allow: false, reason: `an expanded argument to ${cmd} could be a flag` };
|
|
113
|
+
const flag = risky ? args.find((a) => risky.test(a)) : undefined;
|
|
114
|
+
return flag ? { allow: false, reason: `${cmd} ${flag} can run or write` } : { allow: true };
|
|
115
|
+
}
|
|
116
|
+
const downstreamArgs = downstreamCall(cmd, args, skillDir);
|
|
117
|
+
if ((cmd === "git" || cmd === "gh" || downstreamArgs) && expanded)
|
|
118
|
+
return { allow: false, reason: `an expanded argument to ${cmd} could be a flag or a send` };
|
|
119
|
+
if (cmd === "git")
|
|
120
|
+
return gitRead(args);
|
|
121
|
+
if (cmd === "gh")
|
|
122
|
+
return ghRead(args);
|
|
123
|
+
if (downstreamArgs) {
|
|
124
|
+
const [sub, ...rest] = downstreamArgs;
|
|
125
|
+
const publishes = (sub !== undefined && DOWNSTREAM_PUBLISH.has(sub)) || rest.some((a) => a.includes("review.publish"));
|
|
126
|
+
return publishes ? { allow: false, reason: "sending the review is yours to approve" } : { allow: true };
|
|
127
|
+
}
|
|
128
|
+
return { allow: false, reason: `${cmd} isn't known to be read-only` };
|
|
129
|
+
}
|
|
130
|
+
/** The args after `downstream`, if this is the Downstream CLI. */
|
|
131
|
+
function downstreamCall(cmd, args, skillDir) {
|
|
132
|
+
if (cmd === "downstream")
|
|
133
|
+
return args;
|
|
134
|
+
const script = args[0]?.replace(/^["']|["']$/g, "");
|
|
135
|
+
const ours = script === `${skillDir}/bin/downstream.mjs` || script?.endsWith("/bin/downstream.mjs");
|
|
136
|
+
return cmd === "node" && ours ? args.slice(1) : null;
|
|
137
|
+
}
|
|
138
|
+
function gitRead(args) {
|
|
139
|
+
const rest = args[0] === "-C" ? args.slice(2) : args;
|
|
140
|
+
const sub = rest[0];
|
|
141
|
+
if (rest.some((a) => a.startsWith("--output") || a.startsWith("--ext-diff")))
|
|
142
|
+
return { allow: false, reason: "git writing to a file or running a diff tool" };
|
|
143
|
+
if (sub === "branch" || sub === "worktree") {
|
|
144
|
+
const listing = sub === "branch" ? !rest.slice(1).some((a) => /^-[dDmMcC]|^--(delete|move|copy)/.test(a)) : rest[1] === "list";
|
|
145
|
+
return listing ? { allow: true } : { allow: false, reason: `git ${sub} can change things` };
|
|
146
|
+
}
|
|
147
|
+
if (sub === "remote" && rest.length > 2)
|
|
148
|
+
return { allow: false, reason: "git remote can change things" };
|
|
149
|
+
return sub && GIT_READ.has(sub) ? { allow: true } : { allow: false, reason: `git ${sub ?? ""} can change things`.trim() };
|
|
150
|
+
}
|
|
151
|
+
function ghRead(args) {
|
|
152
|
+
const [group, verb] = args;
|
|
153
|
+
if (group === "api") {
|
|
154
|
+
const at = args.findIndex((a) => a === "-X" || a === "--method");
|
|
155
|
+
const writes = at !== -1 && args[at + 1]?.toUpperCase() !== "GET";
|
|
156
|
+
const graphql = args[1] === "graphql";
|
|
157
|
+
const fields = args.some((a) => /^(-f|-F|--field|--raw-field|--input)$/.test(a));
|
|
158
|
+
if (writes)
|
|
159
|
+
return { allow: false, reason: "gh api with a write method" };
|
|
160
|
+
if (graphql)
|
|
161
|
+
return args.some((a) => /\bmutation\b/.test(a)) ? { allow: false, reason: "a GraphQL mutation" } : { allow: true };
|
|
162
|
+
return fields ? { allow: false, reason: "gh api with fields posts" } : { allow: true };
|
|
163
|
+
}
|
|
164
|
+
return group && verb && GH_READ[group]?.has(verb) ? { allow: true } : { allow: false, reason: `gh ${group ?? ""} ${verb ?? ""} can change things`.trim() };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Commands that reach other people: sending the review, posting or merging on
|
|
168
|
+
* GitHub, pushing. The agent runs in auto mode, where a classifier approves
|
|
169
|
+
* most calls; these always come to you, whatever the classifier would say.
|
|
170
|
+
* Matched across the whole command, so a push inside `$(…)` or after `&&` counts.
|
|
171
|
+
*/
|
|
172
|
+
const OUTWARD = [
|
|
173
|
+
[/\bdownstream(\.mjs)?["']?\s+send\b/, "sending the review"],
|
|
174
|
+
[/\breview\.publish\b/, "sending the review"],
|
|
175
|
+
[/\bgit\b(\s+-C\s+\S+)?\s+push\b/, "pushing to GitHub"],
|
|
176
|
+
[/\bgh\s+pr\s+(review|comment|merge|close|reopen|create|edit|ready)\b/, "changing a pull request on GitHub"],
|
|
177
|
+
[/\bgh\s+issue\s+(comment|create|close|reopen|edit)\b/, "changing an issue on GitHub"],
|
|
178
|
+
[/\bgh\s+(release|repo)\s+(create|delete|edit)\b/, "changing a repository on GitHub"],
|
|
179
|
+
[/\bgh\s+api\b[^|;&]*(-X|--method)[\s=]*(POST|PATCH|PUT|DELETE)\b/i, "writing through the GitHub API"],
|
|
180
|
+
[/\bgh\s+api\s+graphql\b[^|;&]*\bmutation\b/, "a GitHub GraphQL mutation"],
|
|
181
|
+
];
|
|
182
|
+
export function reachesOthers(tool, input) {
|
|
183
|
+
if (tool !== "Bash")
|
|
184
|
+
return null;
|
|
185
|
+
const command = typeof input.command === "string" ? input.command : "";
|
|
186
|
+
const restFields = /\bgh\s+api\s+(?!graphql\b)[^|;&]*\s(-f|-F|--field|--raw-field|--input)\s/.test(command) && !/(-X|--method)[\s=]*GET\b/i.test(command);
|
|
187
|
+
if (restFields)
|
|
188
|
+
return "writing through the GitHub API";
|
|
189
|
+
return OUTWARD.find(([pattern]) => pattern.test(command))?.[1] ?? null;
|
|
190
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Null when there's nothing to wait on: the thread is settled, or the agent spoke last. */
|
|
2
|
+
export function delivery(note, readThrough, presence) {
|
|
3
|
+
if (note.status !== "open")
|
|
4
|
+
return null;
|
|
5
|
+
const last = note.replies.at(-1) ?? note;
|
|
6
|
+
if (last.author !== "user")
|
|
7
|
+
return null;
|
|
8
|
+
const read = readThrough !== null && last.createdAt <= readThrough;
|
|
9
|
+
if (!read)
|
|
10
|
+
return "unread";
|
|
11
|
+
// A plain note or flag doesn't ask for an answer, so a busy agent isn't "answering" it.
|
|
12
|
+
const expectsAnswer = note.kind === "question" || note.replies.length > 0;
|
|
13
|
+
switch (presence.agent) {
|
|
14
|
+
case "working":
|
|
15
|
+
return expectsAnswer ? "answering" : "read";
|
|
16
|
+
case "listening":
|
|
17
|
+
return "read";
|
|
18
|
+
case "away":
|
|
19
|
+
return expectsAnswer ? "stalled" : "read";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// The inbox: every open pull request you're part of, reduced to the one thing
|
|
2
|
+
// it needs from you next (Getting Things Done: one next action each; what waits
|
|
3
|
+
// on someone else goes to its own list), then sorted into the Eisenhower matrix:
|
|
4
|
+
// urgent when a person is blocked on you, important when it's part of what
|
|
5
|
+
// you're working on. Downstream makes the first read; yours replaces it.
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
export const Checks = z.enum(["passing", "failing", "pending", "none"]);
|
|
8
|
+
export const Mergeable = z.enum(["mergeable", "conflicting", "unknown"]);
|
|
9
|
+
export const ReviewDecision = z.enum(["approved", "changes_requested", "review_required"]);
|
|
10
|
+
const TouchAt = z.object({ at: z.string(), how: z.enum(["reviewed", "commented", "opened", "pushed"]) });
|
|
11
|
+
/** What the author did after your last review that hands the PR back to you. */
|
|
12
|
+
const AuthorAct = z.object({ how: z.enum(["pushed", "replied"]), at: z.string() });
|
|
13
|
+
/** A pull request as GitHub describes it, from the point of view of one account. */
|
|
14
|
+
export const PrFacts = z.object({
|
|
15
|
+
repo: z.string(),
|
|
16
|
+
number: z.number().int(),
|
|
17
|
+
title: z.string(),
|
|
18
|
+
url: z.string(),
|
|
19
|
+
author: z.string(),
|
|
20
|
+
/** Bots (GitHub Apps) open PRs as your delegated work, not as someone waiting on you. */
|
|
21
|
+
authorKind: z.enum(["person", "bot"]),
|
|
22
|
+
/** What people call the author ("Annie Edison"), when GitHub knows it. */
|
|
23
|
+
authorName: z.string().nullable(),
|
|
24
|
+
authorAvatar: z.string().nullable(),
|
|
25
|
+
isDraft: z.boolean(),
|
|
26
|
+
updatedAt: z.string(),
|
|
27
|
+
baseRef: z.string(),
|
|
28
|
+
headRef: z.string(),
|
|
29
|
+
headSha: z.string(),
|
|
30
|
+
reviewDecision: ReviewDecision.nullable(),
|
|
31
|
+
checks: Checks,
|
|
32
|
+
mergeable: Mergeable,
|
|
33
|
+
unresolvedThreads: z.number().int(),
|
|
34
|
+
/** Whether this account wrote the PR or is (or was) asked to review it. */
|
|
35
|
+
role: z.enum(["author", "reviewer"]),
|
|
36
|
+
/** A review from this account is requested right now. */
|
|
37
|
+
reviewRequested: z.boolean(),
|
|
38
|
+
/** When this account last reviewed, if it ever did. */
|
|
39
|
+
myReviewAt: z.string().nullable(),
|
|
40
|
+
/** The author's latest push or reply after that review. Null when there is none, or no review. */
|
|
41
|
+
authorSince: AuthorAct.nullable(),
|
|
42
|
+
/** GitHub's native stacks (`gh stack`): position 1 sits on the base branch. */
|
|
43
|
+
stack: z.object({ number: z.number().int(), size: z.number().int(), position: z.number().int() }).nullable(),
|
|
44
|
+
/** The last time this account reviewed, commented on, opened or pushed to it, from its event feed. */
|
|
45
|
+
touched: TouchAt.nullable(),
|
|
46
|
+
});
|
|
47
|
+
/** Attach each PR's latest touch. `touches` is newest first, as GitHub returns it. */
|
|
48
|
+
export function withTouches(prs, touches) {
|
|
49
|
+
return prs.map((pr) => {
|
|
50
|
+
const hit = touches.find((t) => t.repo === pr.repo && (t.number === pr.number || (t.branch !== null && t.branch === pr.headRef)));
|
|
51
|
+
return hit ? { ...pr, touched: { at: hit.at, how: hit.how } } : pr;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* How each move reads and what pressing it does. `review` opens Downstream with
|
|
56
|
+
* the agent; `agent` starts the agent on the task; `github` opens the PR.
|
|
57
|
+
* Rank orders the action list: two-minute actions first (GTD's two-minute rule),
|
|
58
|
+
* then whoever is blocked on you, then your own fixes.
|
|
59
|
+
*/
|
|
60
|
+
export const MOVES = {
|
|
61
|
+
merge: { verb: "Merge", rank: 0, opens: "github" },
|
|
62
|
+
review: { verb: "Review", rank: 1, opens: "review" },
|
|
63
|
+
rereview: { verb: "Re-review", rank: 2, opens: "review" },
|
|
64
|
+
"address-feedback": { verb: "Address feedback", rank: 3, opens: "agent" },
|
|
65
|
+
"resolve-conflicts": { verb: "Resolve conflicts", rank: 4, opens: "agent" },
|
|
66
|
+
"fix-checks": { verb: "Fix checks", rank: 5, opens: "agent" },
|
|
67
|
+
"finish-draft": { verb: "Finish draft", rank: 6, opens: "agent" },
|
|
68
|
+
waiting: { verb: "Waiting", rank: 99, opens: "github" },
|
|
69
|
+
};
|
|
70
|
+
export const WAITING_ON = {
|
|
71
|
+
author: "the author",
|
|
72
|
+
reviewers: "reviewers",
|
|
73
|
+
checks: "checks",
|
|
74
|
+
"lower-layer": "the PR below it",
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The next action for a single PR, ignoring the stack it may sit in. Once you
|
|
78
|
+
* have reviewed, the PR is the author's turn until they push, reply or ask you
|
|
79
|
+
* again. Commit ids can't tell this: a review posted on a commit the author
|
|
80
|
+
* force-pushed away mid-review would look like new work forever.
|
|
81
|
+
*/
|
|
82
|
+
export function moveFor(pr) {
|
|
83
|
+
if (pr.role === "reviewer")
|
|
84
|
+
return reviewerMove(pr);
|
|
85
|
+
if (pr.mergeable === "conflicting")
|
|
86
|
+
return { kind: "resolve-conflicts" };
|
|
87
|
+
if (pr.checks === "failing")
|
|
88
|
+
return { kind: "fix-checks" };
|
|
89
|
+
if (pr.reviewDecision === "changes_requested" || pr.unresolvedThreads > 0)
|
|
90
|
+
return { kind: "address-feedback", threads: pr.unresolvedThreads };
|
|
91
|
+
if (pr.isDraft)
|
|
92
|
+
return { kind: "finish-draft" };
|
|
93
|
+
if (pr.checks === "pending")
|
|
94
|
+
return { kind: "waiting", on: "checks" };
|
|
95
|
+
if (pr.reviewDecision === "approved" && pr.mergeable === "mergeable")
|
|
96
|
+
return { kind: "merge" };
|
|
97
|
+
return { kind: "waiting", on: "reviewers" };
|
|
98
|
+
}
|
|
99
|
+
function reviewerMove(pr) {
|
|
100
|
+
if (pr.myReviewAt === null)
|
|
101
|
+
return pr.reviewRequested ? { kind: "review" } : { kind: "waiting", on: "author" };
|
|
102
|
+
// Reviewing fulfils a request, so a request standing after your review was made again.
|
|
103
|
+
if (pr.reviewRequested)
|
|
104
|
+
return { kind: "rereview", because: { how: "re-requested", at: pr.updatedAt } };
|
|
105
|
+
if (pr.authorSince)
|
|
106
|
+
return { kind: "rereview", because: pr.authorSince };
|
|
107
|
+
return { kind: "waiting", on: "author" };
|
|
108
|
+
}
|
|
109
|
+
export const itemKey = (pr) => (pr.stack ? `${pr.repo}@${pr.stack.number}` : `${pr.repo}#${pr.number}`);
|
|
110
|
+
/**
|
|
111
|
+
* Group PRs into items. In a stack only the lowest layer can merge, so an
|
|
112
|
+
* approved layer above an open one waits on it; the stack's move is the move
|
|
113
|
+
* of its lowest layer that needs something from you.
|
|
114
|
+
*/
|
|
115
|
+
export function toItems(prs) {
|
|
116
|
+
const groups = Map.groupBy(dedupe(prs), itemKey);
|
|
117
|
+
return [...groups.entries()].map(([key, group]) => {
|
|
118
|
+
const layers = [...group].sort((a, b) => (a.stack?.position ?? 0) - (b.stack?.position ?? 0));
|
|
119
|
+
const moves = layers.map((pr, i) => {
|
|
120
|
+
const move = moveFor(pr);
|
|
121
|
+
if (move.kind === "merge" && i > 0)
|
|
122
|
+
return { kind: "waiting", on: "lower-layer" };
|
|
123
|
+
return move;
|
|
124
|
+
});
|
|
125
|
+
const actionable = moves.findIndex((m) => m.kind !== "waiting");
|
|
126
|
+
const at = actionable === -1 ? 0 : actionable;
|
|
127
|
+
const first = layers[0];
|
|
128
|
+
return {
|
|
129
|
+
key,
|
|
130
|
+
repo: first.repo,
|
|
131
|
+
prs: layers,
|
|
132
|
+
lead: layers[at],
|
|
133
|
+
move: moves[at],
|
|
134
|
+
stack: first.stack ? { number: first.stack.number, size: first.stack.size } : null,
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The same PR can come back from several searches (requested, and reviewed
|
|
140
|
+
* before). Keep one: a pending request wins.
|
|
141
|
+
*/
|
|
142
|
+
function dedupe(prs) {
|
|
143
|
+
const byKey = new Map();
|
|
144
|
+
for (const pr of prs) {
|
|
145
|
+
const key = `${pr.repo}#${pr.number}`;
|
|
146
|
+
const seen = byKey.get(key);
|
|
147
|
+
byKey.set(key, seen
|
|
148
|
+
? { ...seen, reviewRequested: seen.reviewRequested || pr.reviewRequested, myReviewAt: seen.myReviewAt ?? pr.myReviewAt, authorSince: seen.authorSince ?? pr.authorSince }
|
|
149
|
+
: pr);
|
|
150
|
+
}
|
|
151
|
+
return [...byKey.values()];
|
|
152
|
+
}
|
|
153
|
+
export const Quadrant = z.enum(["do", "next", "delegate", "someday"]);
|
|
154
|
+
/** Touched in the last three days: what you're in the middle of. */
|
|
155
|
+
const FOCUS_WINDOW_MS = 3 * 24 * 3600_000;
|
|
156
|
+
/** No activity from anyone for two weeks: it has gone stale, whatever its state says, and needs a decision rather than a slot. */
|
|
157
|
+
const QUIET_AFTER_MS = 14 * 24 * 3600_000;
|
|
158
|
+
const latestTouch = (item) => item.prs.map((p) => p.touched).filter((t) => t !== null).sort((a, b) => b.at.localeCompare(a.at))[0] ?? null;
|
|
159
|
+
const lastActivity = (item) => [...item.prs.map((p) => p.updatedAt), latestTouch(item)?.at ?? ""].sort().at(-1);
|
|
160
|
+
/**
|
|
161
|
+
* Downstream's read of one item. Urgent: a person is blocked on you (their
|
|
162
|
+
* review, feedback on your PR) or it's done and only needs merging. Important:
|
|
163
|
+
* you're working on it or near it (touched lately, or you touched another PR by
|
|
164
|
+
* the same author), or it's yours. A PR handed back after your review is both.
|
|
165
|
+
* Drafts you haven't opened lately say "someday". Stale PRs never reach here.
|
|
166
|
+
*/
|
|
167
|
+
export function triage(item, context) {
|
|
168
|
+
const now = context.now.getTime();
|
|
169
|
+
const pr = item.lead;
|
|
170
|
+
// Before focus: "you reviewed 1h ago" would hide that the author answered since.
|
|
171
|
+
if (item.move.kind === "rereview")
|
|
172
|
+
return { quadrant: "do", why: { kind: "your-turn", who: pr.authorName ?? pr.author, ...item.move.because } };
|
|
173
|
+
const touch = latestTouch(item);
|
|
174
|
+
const inFocus = touch !== null && now - Date.parse(touch.at) <= FOCUS_WINDOW_MS;
|
|
175
|
+
if (inFocus)
|
|
176
|
+
return { quadrant: "do", why: { kind: "in-focus", at: touch.at, how: touch.how } };
|
|
177
|
+
if (pr.isDraft)
|
|
178
|
+
return { quadrant: "someday", why: { kind: "draft" } };
|
|
179
|
+
const yours = pr.role === "author";
|
|
180
|
+
if (yours && item.move.kind === "merge")
|
|
181
|
+
return { quadrant: "do", why: { kind: "ready-to-merge" } };
|
|
182
|
+
if (yours && item.move.kind === "address-feedback")
|
|
183
|
+
return { quadrant: "do", why: { kind: "feedback-on-yours" } };
|
|
184
|
+
if (yours)
|
|
185
|
+
return { quadrant: "next", why: { kind: "yours" } };
|
|
186
|
+
if (pr.authorKind === "bot")
|
|
187
|
+
return { quadrant: "next", why: { kind: "bot-work", who: pr.author } };
|
|
188
|
+
const waiting = { kind: "person-waiting", who: pr.authorName ?? pr.author, since: pr.updatedAt };
|
|
189
|
+
return { quadrant: context.focusAuthors.has(pr.author) ? "do" : "delegate", why: waiting };
|
|
190
|
+
}
|
|
191
|
+
const UNTIL_IT_CHANGES = new Set(["someday", "hidden"]);
|
|
192
|
+
/**
|
|
193
|
+
* Read every item, apply your choices over the reads, and order each quadrant.
|
|
194
|
+
* `touches` names who you've been working with: the authors of PRs you touched
|
|
195
|
+
* lately, including ones already merged.
|
|
196
|
+
*/
|
|
197
|
+
export function arrange(items, choices, now, touches) {
|
|
198
|
+
const standing = (item) => {
|
|
199
|
+
const chosen = choices[item.key];
|
|
200
|
+
const woke = chosen !== undefined && UNTIL_IT_CHANGES.has(chosen.quadrant) && lastActivity(item) > chosen.at;
|
|
201
|
+
return chosen && !woke ? chosen.quadrant : null;
|
|
202
|
+
};
|
|
203
|
+
const shown = items.filter((i) => standing(i) !== "hidden");
|
|
204
|
+
const isStale = (i) => now.getTime() - Date.parse(lastActivity(i)) > QUIET_AFTER_MS;
|
|
205
|
+
const stale = shown.filter(isStale).sort((a, b) => lastActivity(a).localeCompare(lastActivity(b)));
|
|
206
|
+
const fresh = shown.filter((i) => !isStale(i));
|
|
207
|
+
const actionable = fresh.filter((i) => i.move.kind !== "waiting");
|
|
208
|
+
const recent = touches.filter((t) => now.getTime() - Date.parse(t.at) <= FOCUS_WINDOW_MS);
|
|
209
|
+
const focusAuthors = new Set(recent.map((t) => t.author).filter((a) => a !== null));
|
|
210
|
+
const triaged = actionable.map((item) => {
|
|
211
|
+
const auto = triage(item, { now, focusAuthors });
|
|
212
|
+
const chosen = standing(item);
|
|
213
|
+
return chosen && chosen !== "hidden" && chosen !== auto.quadrant ? { ...item, quadrant: chosen, why: { kind: "you-chose" }, auto } : { ...item, ...auto, auto };
|
|
214
|
+
});
|
|
215
|
+
const of = (q) => triaged.filter((t) => t.quadrant === q).sort(ORDER[q]);
|
|
216
|
+
const inbox = { do: of("do"), next: of("next"), delegate: of("delegate"), someday: of("someday") };
|
|
217
|
+
return {
|
|
218
|
+
top: inbox.do[0] ?? inbox.next[0] ?? inbox.delegate[0] ?? null,
|
|
219
|
+
...inbox,
|
|
220
|
+
waiting: fresh.filter((i) => i.move.kind === "waiting").sort((a, b) => b.lead.updatedAt.localeCompare(a.lead.updatedAt)),
|
|
221
|
+
stale,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** When the person on the other side last handed it to you. */
|
|
225
|
+
const waitedLongest = (t) => (t.why.kind === "your-turn" ? t.why.at : t.lead.updatedAt);
|
|
226
|
+
const oldestFirst = (a, b) => a.lead.updatedAt.localeCompare(b.lead.updatedAt);
|
|
227
|
+
const newestFirst = (a, b) => b.lead.updatedAt.localeCompare(a.lead.updatedAt);
|
|
228
|
+
const byMove = (a, b) => MOVES[a.move.kind].rank - MOVES[b.move.kind].rank;
|
|
229
|
+
/** Do: people blocked on you first, longest wait first (they can't move without you); then what you're in the middle of, latest first. */
|
|
230
|
+
const DO_ORDER = { "your-turn": 0, "person-waiting": 0, "feedback-on-yours": 1, "ready-to-merge": 2, "you-chose": 3, "in-focus": 4, yours: 5, "bot-work": 5, draft: 6 };
|
|
231
|
+
const ORDER = {
|
|
232
|
+
do: (a, b) => DO_ORDER[a.why.kind] - DO_ORDER[b.why.kind] || (a.why.kind === "in-focus" && b.why.kind === "in-focus" ? b.why.at.localeCompare(a.why.at) : waitedLongest(a).localeCompare(waitedLongest(b))),
|
|
233
|
+
next: (a, b) => byMove(a, b) || newestFirst(a, b),
|
|
234
|
+
delegate: oldestFirst,
|
|
235
|
+
someday: newestFirst,
|
|
236
|
+
};
|
|
237
|
+
/** The choices a stale item offers, in the order they're shown. */
|
|
238
|
+
export function staleChoices(item) {
|
|
239
|
+
const pr = item.lead;
|
|
240
|
+
const hide = { kind: "hide", nextMove: item.move.kind === "waiting" ? "theirs" : "yours" };
|
|
241
|
+
if (pr.role === "author")
|
|
242
|
+
return [{ kind: "comment", to: "reviewers" }, { kind: "close" }, hide, { kind: "ask-agent" }];
|
|
243
|
+
const request = pr.reviewRequested ? [{ kind: "remove-request" }] : [];
|
|
244
|
+
return [...request, { kind: "comment", to: "author" }, hide, { kind: "ask-agent" }];
|
|
245
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// The review model. Shared by the server, the CLI and the browser so the three
|
|
2
|
+
// never disagree about what a symbol, a flow edge or a thread is.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export const Range = z.object({ start: z.number().int().min(1), end: z.number().int().min(1) });
|
|
5
|
+
/** What happened to a symbol in this change. `context` = unchanged, shown so the flow reads end to end. */
|
|
6
|
+
export const SymbolStatus = z.enum(["added", "modified", "removed", "context"]);
|
|
7
|
+
export const SymbolKind = z.enum([
|
|
8
|
+
"function",
|
|
9
|
+
"method",
|
|
10
|
+
"class",
|
|
11
|
+
"interface",
|
|
12
|
+
"type",
|
|
13
|
+
"route",
|
|
14
|
+
"component",
|
|
15
|
+
"const",
|
|
16
|
+
"module",
|
|
17
|
+
]);
|
|
18
|
+
export const CodeSymbol = z.object({
|
|
19
|
+
/** Stable id: `<file>#<Container.name>`. */
|
|
20
|
+
id: z.string(),
|
|
21
|
+
name: z.string(),
|
|
22
|
+
container: z.string().nullable().default(null),
|
|
23
|
+
kind: SymbolKind,
|
|
24
|
+
file: z.string(),
|
|
25
|
+
/** Lines on the new side. Null for removed symbols. */
|
|
26
|
+
range: Range.nullable(),
|
|
27
|
+
oldFile: z.string().nullable().default(null),
|
|
28
|
+
oldRange: Range.nullable().default(null),
|
|
29
|
+
status: SymbolStatus,
|
|
30
|
+
signature: z.string().nullable().default(null),
|
|
31
|
+
exported: z.boolean().default(false),
|
|
32
|
+
/** A door into the change: nothing else in the change calls it. */
|
|
33
|
+
entry: z.boolean().default(false),
|
|
34
|
+
/** Lives in a test file: shown as "tested by", never a door. */
|
|
35
|
+
test: z.boolean().default(false),
|
|
36
|
+
/** The agent's one-line answer to "what is this for?". */
|
|
37
|
+
summary: z.string().nullable().default(null),
|
|
38
|
+
/** Changed lines inside the symbol, new side. */
|
|
39
|
+
changedLines: z.number().int().default(0),
|
|
40
|
+
});
|
|
41
|
+
export const EdgeKind = z.enum(["calls", "renders", "implements", "extends", "uses"]);
|
|
42
|
+
/** Whether the relationship itself is new in this change. */
|
|
43
|
+
export const EdgeChange = z.enum(["added", "removed", "same"]);
|
|
44
|
+
export const Edge = z.object({
|
|
45
|
+
id: z.string(),
|
|
46
|
+
from: z.string(),
|
|
47
|
+
to: z.string(),
|
|
48
|
+
kind: EdgeKind,
|
|
49
|
+
change: EdgeChange.default("same"),
|
|
50
|
+
/** Call site line in `from`'s file (new side, or old side for removed edges). */
|
|
51
|
+
line: z.number().int().nullable().default(null),
|
|
52
|
+
label: z.string().nullable().default(null),
|
|
53
|
+
});
|
|
54
|
+
export const Step = z.object({
|
|
55
|
+
id: z.string(),
|
|
56
|
+
order: z.number().int(),
|
|
57
|
+
title: z.string(),
|
|
58
|
+
body: z.string().default(""),
|
|
59
|
+
symbolId: z.string().nullable().default(null),
|
|
60
|
+
lines: Range.nullable().default(null),
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* Everything said about the code is a thread.
|
|
64
|
+
* - why: rationale for something that looks odd
|
|
65
|
+
* - decision: a design choice with the alternatives that lost
|
|
66
|
+
* - finding: a problem the reviewer wants addressed
|
|
67
|
+
* - question: someone needs an answer from the other side
|
|
68
|
+
* - comment: anything else
|
|
69
|
+
*/
|
|
70
|
+
export const NoteKind = z.enum(["why", "decision", "finding", "question", "comment"]);
|
|
71
|
+
/** Shown as must fix / should fix / nice to have. */
|
|
72
|
+
export const Severity = z.enum(["blocker", "concern", "nit"]);
|
|
73
|
+
export const severityLabel = { blocker: "must fix", concern: "should fix", nit: "nice to have" };
|
|
74
|
+
/** What kind of problem a finding is, in the order a reviewer who gets paged cares about. */
|
|
75
|
+
export const FindingCategory = z.enum(["bug", "risk", "scale", "test", "speed", "lean"]);
|
|
76
|
+
/** The comment this thread becomes on the pull request, if it goes out at all. */
|
|
77
|
+
export const Outgoing = z.object({ include: z.boolean(), body: z.string() });
|
|
78
|
+
export const NoteStatus = z.enum(["open", "resolved", "dismissed"]);
|
|
79
|
+
export const Author = z.enum(["agent", "user"]);
|
|
80
|
+
export const Alternative = z.object({ option: z.string(), tradeoff: z.string() });
|
|
81
|
+
export const Suggestion = z.object({ code: z.string(), lines: Range });
|
|
82
|
+
export const Reply = z.object({
|
|
83
|
+
id: z.string(),
|
|
84
|
+
noteId: z.string(),
|
|
85
|
+
author: Author,
|
|
86
|
+
body: z.string(),
|
|
87
|
+
createdAt: z.string(),
|
|
88
|
+
});
|
|
89
|
+
export const Note = z.object({
|
|
90
|
+
id: z.string(),
|
|
91
|
+
/** Short handle for conversation: "fix 2 and 5". */
|
|
92
|
+
number: z.number().int().default(0),
|
|
93
|
+
kind: NoteKind,
|
|
94
|
+
severity: Severity.nullable().default(null),
|
|
95
|
+
category: FindingCategory.nullable().default(null),
|
|
96
|
+
title: z.string(),
|
|
97
|
+
/** For findings: the problem, with the concrete case that goes wrong. */
|
|
98
|
+
body: z.string().default(""),
|
|
99
|
+
/** For findings: the smallest fix. */
|
|
100
|
+
fix: z.string().default(""),
|
|
101
|
+
/** For findings: what happens if we ship it anyway. */
|
|
102
|
+
impact: z.string().default(""),
|
|
103
|
+
outgoing: Outgoing.nullable().default(null),
|
|
104
|
+
symbolId: z.string().nullable().default(null),
|
|
105
|
+
file: z.string().nullable().default(null),
|
|
106
|
+
lines: Range.nullable().default(null),
|
|
107
|
+
side: z.enum(["new", "old"]).default("new"),
|
|
108
|
+
alternatives: z.array(Alternative).default([]),
|
|
109
|
+
suggestion: Suggestion.nullable().default(null),
|
|
110
|
+
status: NoteStatus.default("open"),
|
|
111
|
+
author: Author,
|
|
112
|
+
createdAt: z.string(),
|
|
113
|
+
replies: z.array(Reply).default([]),
|
|
114
|
+
});
|
|
115
|
+
export const FileChange = z.object({
|
|
116
|
+
path: z.string(),
|
|
117
|
+
oldPath: z.string().nullable(),
|
|
118
|
+
status: z.enum(["added", "modified", "deleted", "renamed", "context"]),
|
|
119
|
+
additions: z.number().int(),
|
|
120
|
+
deletions: z.number().int(),
|
|
121
|
+
});
|
|
122
|
+
export const Verdict = z.object({
|
|
123
|
+
by: Author,
|
|
124
|
+
value: z.enum(["approve", "changes", "comment"]),
|
|
125
|
+
body: z.string().default(""),
|
|
126
|
+
at: z.string(),
|
|
127
|
+
});
|
|
128
|
+
export const Review = z.object({
|
|
129
|
+
id: z.string(),
|
|
130
|
+
title: z.string(),
|
|
131
|
+
mode: z.enum(["diff", "teach"]),
|
|
132
|
+
/** What the user asked to compare against (a branch name, a PR base). */
|
|
133
|
+
base: z.string().nullable(),
|
|
134
|
+
/** The merge base actually diffed against. */
|
|
135
|
+
baseSha: z.string().nullable().default(null),
|
|
136
|
+
/** A ref, or WORKTREE for uncommitted work. */
|
|
137
|
+
head: z.string(),
|
|
138
|
+
/** Teach mode: the paths being explained. */
|
|
139
|
+
paths: z.array(z.string()).default([]),
|
|
140
|
+
pr: z.number().int().nullable().default(null),
|
|
141
|
+
/** `owner/name` on GitHub, when the repo has a GitHub origin. */
|
|
142
|
+
repo: z.string().nullable().default(null),
|
|
143
|
+
/** The GitHub account this review reads and sends as. */
|
|
144
|
+
account: z.string().nullable().default(null),
|
|
145
|
+
summary: z.string().default(""),
|
|
146
|
+
status: z.enum(["drafting", "ready", "done"]),
|
|
147
|
+
createdAt: z.string(),
|
|
148
|
+
verdicts: z.array(Verdict).default([]),
|
|
149
|
+
/** The body of the review that goes to the pull request; the agent drafts it, the user edits it. */
|
|
150
|
+
outgoingBody: z.string().default(""),
|
|
151
|
+
published: z.object({ url: z.string(), at: z.string(), event: z.string(), comments: z.number().int() }).nullable().default(null),
|
|
152
|
+
});
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Application state: the UI tells the agent what the user sees; the agent can
|
|
155
|
+
// move the user's view. Same idea as agent-native's navigation/selection keys.
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
export const Navigation = z.object({
|
|
158
|
+
view: z.enum(["overview", "trail", "map"]),
|
|
159
|
+
trail: z.array(z.string()).default([]),
|
|
160
|
+
stepId: z.string().nullable().default(null),
|
|
161
|
+
noteId: z.string().nullable().default(null),
|
|
162
|
+
});
|
|
163
|
+
export const Selection = z.object({
|
|
164
|
+
symbolId: z.string().nullable(),
|
|
165
|
+
file: z.string(),
|
|
166
|
+
lines: Range,
|
|
167
|
+
side: z.enum(["new", "old"]).default("new"),
|
|
168
|
+
});
|
|
169
|
+
export const NavigateCommand = z.object({
|
|
170
|
+
symbolId: z.string().nullable().default(null),
|
|
171
|
+
stepId: z.string().nullable().default(null),
|
|
172
|
+
noteId: z.string().nullable().default(null),
|
|
173
|
+
view: z.enum(["overview", "trail", "map"]).nullable().default(null),
|
|
174
|
+
lines: Range.nullable().default(null),
|
|
175
|
+
nonce: z.string().optional(),
|
|
176
|
+
});
|
|
177
|
+
/** The pull request on GitHub, when the review is of one. */
|
|
178
|
+
export const prUrl = (review) => (review.repo && review.pr ? `https://github.com/${review.repo}/pull/${review.pr}` : null);
|
|
179
|
+
/** The status `review.publish` answers with when the PR changed since the review was opened. */
|
|
180
|
+
export const HEAD_MOVED = 409;
|