@coreplane/switchboard 1.204.0 → 1.206.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/dist/assets/Dockerfile +6 -3
- package/dist/assets/config/config.example.yaml +56 -4
- package/dist/assets/deploy/cloudflare/coordinator.ts +86 -0
- package/dist/assets/deploy/cloudflare/shared.ts +9 -0
- package/dist/assets/deploy/cloudflare/worker.ts +142 -6
- package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +9 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +381 -7
- package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +22 -2
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +89 -3
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +81 -10
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +4 -1
- package/dist/assets/deploy/secrets.manifest.json +1 -1
- package/dist/assets/package-lock.json +291 -97
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +517 -0
- package/dist/assets/src/config/profile.ts +229 -0
- package/dist/assets/src/core/authz/policy.ts +15 -0
- package/dist/assets/src/core/coordinator/contract.ts +239 -0
- package/dist/assets/src/core/coordinator/driver.ts +501 -0
- package/dist/assets/src/core/coordinator/instancesRoute.ts +181 -0
- package/dist/assets/src/core/delivery.ts +829 -0
- package/dist/assets/src/core/deliverySnapshotStore.ts +260 -0
- package/dist/assets/src/core/reviewVerdict.ts +296 -0
- package/dist/assets/src/core/reviewedHead.ts +77 -0
- package/dist/assets/src/core/runEvents.ts +4 -2
- package/dist/assets/src/core/runLedger/decisions.ts +2 -1
- package/dist/assets/src/core/runLedger/types.ts +23 -1
- package/dist/assets/src/core/runRecord.ts +100 -0
- package/dist/assets/src/core/ship/contract.ts +502 -0
- package/dist/assets/src/core/ship/coordinator.ts +1039 -0
- package/dist/assets/src/core/workerError.ts +12 -0
- package/dist/assets/web/dist/.vite/manifest.json +2770 -70
- package/dist/assets/web/dist/assets/AppShell-lYVcj-k7.js +1 -0
- package/dist/assets/web/dist/assets/{CostsPage-XQ-fQzdz.js → CostsPage-DGSE48l1.js} +1 -1
- package/dist/assets/web/dist/assets/DeliveryPage-CvlWP7Eq.js +1 -0
- package/dist/assets/web/dist/assets/NotFoundPage-BzWS4Ca1.js +1 -0
- package/dist/assets/web/dist/assets/ResidentDetailPage-D3P21yeI.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-DOYqnZ1q.js +1 -0
- package/dist/assets/web/dist/assets/RunRoutePage-OmvrvPXY.js +12 -0
- package/dist/assets/web/dist/assets/RunRoutePage-f6TSNPeW.css +1 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-DWbSQtL4.js +1 -0
- package/dist/assets/web/dist/assets/RunsTabs-DEICQ4FY.js +1 -0
- package/dist/assets/web/dist/assets/ScheduledPage-CPKfJ4mR.js +1 -0
- package/dist/assets/web/dist/assets/StatusDot-COr8jTyM.js +1 -0
- package/dist/assets/web/dist/assets/Tooltip-fOqTZkNT.js +1 -0
- package/dist/assets/web/dist/assets/abap-CLvhMVsD.js +1 -0
- package/dist/assets/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
- package/dist/assets/web/dist/assets/ada-C5qYipkI.js +1 -0
- package/dist/assets/web/dist/assets/ahk-CA9gdOdF.js +1 -0
- package/dist/assets/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
- package/dist/assets/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
- package/dist/assets/web/dist/assets/angular-html-Cw130Zyi.js +1 -0
- package/dist/assets/web/dist/assets/angular-ts-DY1m4C2s.js +1 -0
- package/dist/assets/web/dist/assets/apache-U0d_L8uA.js +1 -0
- package/dist/assets/web/dist/assets/apex-KFGFyKwG.js +1 -0
- package/dist/assets/web/dist/assets/apl-W6Ty7v05.js +1 -0
- package/dist/assets/web/dist/assets/applescript-CCn79oCD.js +1 -0
- package/dist/assets/web/dist/assets/ara-4CJ0cIlV.js +1 -0
- package/dist/assets/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
- package/dist/assets/web/dist/assets/asm-Cmm7eHzH.js +1 -0
- package/dist/assets/web/dist/assets/astro-DlLVHwVk.js +1 -0
- package/dist/assets/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
- package/dist/assets/web/dist/assets/awk-BWXHIvNe.js +1 -0
- package/dist/assets/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/dist/assets/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
- package/dist/assets/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/dist/assets/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
- package/dist/assets/web/dist/assets/bat-qdxAdBWQ.js +1 -0
- package/dist/assets/web/dist/assets/beancount-D-usSTwE.js +1 -0
- package/dist/assets/web/dist/assets/berry-DKpUyyne.js +1 -0
- package/dist/assets/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
- package/dist/assets/web/dist/assets/bicep-CUHmPFLl.js +1 -0
- package/dist/assets/web/dist/assets/bird2-CYIUgYqB.js +1 -0
- package/dist/assets/web/dist/assets/blade-Bco3tDh4.js +1 -0
- package/dist/assets/web/dist/assets/bsl-_VjlB9mv.js +1 -0
- package/dist/assets/web/dist/assets/c-1LaFY_cj.js +1 -0
- package/dist/assets/web/dist/assets/c3-D0apG41V.js +1 -0
- package/dist/assets/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
- package/dist/assets/web/dist/assets/cairo-DLTphjLi.js +1 -0
- package/dist/assets/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
- package/dist/assets/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
- package/dist/assets/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
- package/dist/assets/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
- package/dist/assets/web/dist/assets/chapel-B-OvrOL5.js +1 -0
- package/dist/assets/web/dist/assets/clarity-CtfBBspg.js +1 -0
- package/dist/assets/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
- package/dist/assets/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
- package/dist/assets/web/dist/assets/cobol-Ch3EPlHu.js +1 -0
- package/dist/assets/web/dist/assets/codeowners-C8r90Shi.js +1 -0
- package/dist/assets/web/dist/assets/codeql-oeQT6MSM.js +1 -0
- package/dist/assets/web/dist/assets/coffee-hZuITnto.js +1 -0
- package/dist/assets/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/dist/assets/web/dist/assets/coq-DuBRPxYg.js +1 -0
- package/dist/assets/web/dist/assets/cpp-B8iRVo8m.js +1 -0
- package/dist/assets/web/dist/assets/crystal-DKveI7lr.js +1 -0
- package/dist/assets/web/dist/assets/csharp-oqKa8noW.js +1 -0
- package/dist/assets/web/dist/assets/css-_2_CD-rr.js +1 -0
- package/dist/assets/web/dist/assets/csv-Dx-8-gkx.js +1 -0
- package/dist/assets/web/dist/assets/cue-CE9AQfxI.js +1 -0
- package/dist/assets/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
- package/dist/assets/web/dist/assets/d-qD-0Kul2.js +1 -0
- package/dist/assets/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
- package/dist/assets/web/dist/assets/dart-CnvKMtbv.js +1 -0
- package/dist/assets/web/dist/assets/dax-BkyTk9wS.js +1 -0
- package/dist/assets/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
- package/dist/assets/web/dist/assets/diff-woXpYk--.js +1 -0
- package/dist/assets/web/dist/assets/dist-BVjAWgkb.js +1547 -0
- package/dist/assets/web/dist/assets/docker-IyjqRm3v.js +1 -0
- package/dist/assets/web/dist/assets/dotenv-_5a1GRtc.js +1 -0
- package/dist/assets/web/dist/assets/dracula-BHWKrbxM.js +1 -0
- package/dist/assets/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
- package/dist/assets/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/dist/assets/web/dist/assets/edge-BenMDPct.js +1 -0
- package/dist/assets/web/dist/assets/elixir-CBbdEqG6.js +1 -0
- package/dist/assets/web/dist/assets/elm-DMub_2lY.js +1 -0
- package/dist/assets/web/dist/assets/emacs-lisp-c_oH4hRZ.js +1 -0
- package/dist/assets/web/dist/assets/erb-CI9He_Up.js +1 -0
- package/dist/assets/web/dist/assets/erlang-Cphh6RMH.js +1 -0
- package/dist/assets/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/dist/assets/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
- package/dist/assets/web/dist/assets/{favicon-CFfbl2AI.js → favicon-CWPcvWvp.js} +1 -1
- package/dist/assets/web/dist/assets/fennel-DQxkIbk2.js +1 -0
- package/dist/assets/web/dist/assets/fish-BJitypiv.js +1 -0
- package/dist/assets/web/dist/assets/fluent-C03EYrpw.js +1 -0
- package/dist/assets/web/dist/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
- package/dist/assets/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/dist/assets/web/dist/assets/fsharp-D13ZGOAj.js +1 -0
- package/dist/assets/web/dist/assets/gdresource-xMC7jr7x.js +1 -0
- package/dist/assets/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
- package/dist/assets/web/dist/assets/gdshader-CBce3t8t.js +1 -0
- package/dist/assets/web/dist/assets/genie-CV2tkWYe.js +1 -0
- package/dist/assets/web/dist/assets/gherkin-DExj1W_8.js +1 -0
- package/dist/assets/web/dist/assets/git-commit-BSykSTBG.js +1 -0
- package/dist/assets/web/dist/assets/git-rebase-BBfhnyF8.js +1 -0
- package/dist/assets/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
- package/dist/assets/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/dist/assets/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/dist/assets/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/dist/assets/web/dist/assets/github-light-EUqPIrTm.js +1 -0
- package/dist/assets/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
- package/dist/assets/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/dist/assets/web/dist/assets/gleam-CSRkHgEL.js +1 -0
- package/dist/assets/web/dist/assets/glimmer-js-CR9Fxrau.js +1 -0
- package/dist/assets/web/dist/assets/glimmer-ts-CwFwX_XS.js +1 -0
- package/dist/assets/web/dist/assets/glsl-Daiodp1r.js +1 -0
- package/dist/assets/web/dist/assets/gn-ilITqXS6.js +1 -0
- package/dist/assets/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
- package/dist/assets/web/dist/assets/go-rLFTqkRN.js +1 -0
- package/dist/assets/web/dist/assets/graphql-DZvrsEkB.js +1 -0
- package/dist/assets/web/dist/assets/groovy-CacY0gHj.js +1 -0
- package/dist/assets/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/dist/assets/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/dist/assets/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/dist/assets/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/dist/assets/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/dist/assets/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/dist/assets/web/dist/assets/hack-BaXOzdg7.js +1 -0
- package/dist/assets/web/dist/assets/haml-CMskq_VA.js +1 -0
- package/dist/assets/web/dist/assets/handlebars-Dhw_f7jD.js +1 -0
- package/dist/assets/web/dist/assets/haskell-D8IpX4py.js +1 -0
- package/dist/assets/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
- package/dist/assets/web/dist/assets/hcl-Dh228itO.js +1 -0
- package/dist/assets/web/dist/assets/hjson-CxZEssPk.js +1 -0
- package/dist/assets/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
- package/dist/assets/web/dist/assets/horizon-CE9ld1lL.js +1 -0
- package/dist/assets/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
- package/dist/assets/web/dist/assets/houston-CsvMBhTu.js +1 -0
- package/dist/assets/web/dist/assets/html-C9k99z0z.js +1 -0
- package/dist/assets/web/dist/assets/html-derivative-VckPItXN.js +1 -0
- package/dist/assets/web/dist/assets/http-DXU_3h9v.js +1 -0
- package/dist/assets/web/dist/assets/hurl-KU24JNdp.js +1 -0
- package/dist/assets/web/dist/assets/hxml-Dcm88srW.js +1 -0
- package/dist/assets/web/dist/assets/hy-CZbG8q4J.js +1 -0
- package/dist/assets/web/dist/assets/imba-DsUTQ-LC.js +1 -0
- package/dist/assets/web/dist/assets/indexFormat-B-pd4r7Y.js +1 -0
- package/dist/assets/web/dist/assets/ini-B5eOa1yu.js +1 -0
- package/dist/assets/web/dist/assets/java-BbZjTNgf.js +1 -0
- package/dist/assets/web/dist/assets/javascript-DXCdqcZZ.js +1 -0
- package/dist/assets/web/dist/assets/jinja-pDlsjTVp.js +1 -0
- package/dist/assets/web/dist/assets/jison-DsXDk66e.js +1 -0
- package/dist/assets/web/dist/assets/json-tCxXfRgG.js +1 -0
- package/dist/assets/web/dist/assets/json5-BR5RXkoi.js +1 -0
- package/dist/assets/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/dist/assets/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
- package/dist/assets/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/dist/assets/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
- package/dist/assets/web/dist/assets/jsx-B0ZYkCmi.js +1 -0
- package/dist/assets/web/dist/assets/julia-BLzzeI6x.js +1 -0
- package/dist/assets/web/dist/assets/just-DvD2_60c.js +1 -0
- package/dist/assets/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
- package/dist/assets/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
- package/dist/assets/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
- package/dist/assets/web/dist/assets/kdl-CsD5j6eV.js +1 -0
- package/dist/assets/web/dist/assets/kotlin-DhhofPvG.js +1 -0
- package/dist/assets/web/dist/assets/kusto-C7mF5XQf.js +1 -0
- package/dist/assets/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
- package/dist/assets/web/dist/assets/latex-511zn3h7.js +1 -0
- package/dist/assets/web/dist/assets/lean-CewbzKMR.js +1 -0
- package/dist/assets/web/dist/assets/less-DVTAwKKz.js +1 -0
- package/dist/assets/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
- package/dist/assets/web/dist/assets/liquid-BD0BkMox.js +1 -0
- package/dist/assets/web/dist/assets/llvm-DqHOMS-s.js +1 -0
- package/dist/assets/web/dist/assets/log-BNLmms1o.js +1 -0
- package/dist/assets/web/dist/assets/logo-Cluzi2Zq.js +1 -0
- package/dist/assets/web/dist/assets/lua-NFicKm6N.js +1 -0
- package/dist/assets/web/dist/assets/luau-C2pBCSgt.js +1 -0
- package/dist/assets/web/dist/assets/main-CuENKPdD.css +1 -0
- package/dist/assets/web/dist/assets/main-DZbJaqUb.js +28 -0
- package/dist/assets/web/dist/assets/make-Dixweg8N.js +1 -0
- package/dist/assets/web/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/dist/assets/web/dist/assets/marko-1-G_nEXK.js +1 -0
- package/dist/assets/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
- package/dist/assets/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/dist/assets/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/dist/assets/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/dist/assets/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/dist/assets/web/dist/assets/matlab-D7qyCx1q.js +1 -0
- package/dist/assets/web/dist/assets/mdc-DUe3AQLp.js +1 -0
- package/dist/assets/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
- package/dist/assets/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
- package/dist/assets/web/dist/assets/min-dark-BSWPekZh.js +1 -0
- package/dist/assets/web/dist/assets/min-light-DDpmG2fV.js +1 -0
- package/dist/assets/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
- package/dist/assets/web/dist/assets/mojo-fg40_ntQ.js +1 -0
- package/dist/assets/web/dist/assets/monokai-B_bV9PQ9.js +1 -0
- package/dist/assets/web/dist/assets/moonbit-DgjMoLWr.js +1 -0
- package/dist/assets/web/dist/assets/move-BYnGhZoM.js +1 -0
- package/dist/assets/web/dist/assets/narrat-_X_XdTYD.js +1 -0
- package/dist/assets/web/dist/assets/nextflow-BJtWHP5T.js +1 -0
- package/dist/assets/web/dist/assets/nextflow-groovy-DJMQeKeT.js +1 -0
- package/dist/assets/web/dist/assets/nginx-Dg379_bA.js +1 -0
- package/dist/assets/web/dist/assets/night-owl-DhmEMT88.js +1 -0
- package/dist/assets/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/dist/assets/web/dist/assets/nim-BnFq97ZO.js +1 -0
- package/dist/assets/web/dist/assets/nix-IvuFDN5E.js +1 -0
- package/dist/assets/web/dist/assets/nord-Cb4Vim4T.js +1 -0
- package/dist/assets/web/dist/assets/nsis-CjNLYCLg.js +1 -0
- package/dist/assets/web/dist/assets/nushell-DHYfO66s.js +1 -0
- package/dist/assets/web/dist/assets/objective-c-D1A_Heim.js +1 -0
- package/dist/assets/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/dist/assets/web/dist/assets/ocaml-O90oeIOV.js +1 -0
- package/dist/assets/web/dist/assets/odin-B1RWQWA5.js +1 -0
- package/dist/assets/web/dist/assets/one-dark-pro-CLwyXe_n.js +1 -0
- package/dist/assets/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
- package/dist/assets/web/dist/assets/openscad-BUDT5pXO.js +1 -0
- package/dist/assets/web/dist/assets/org-F0uiwGvq.js +1 -0
- package/dist/assets/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
- package/dist/assets/web/dist/assets/perl-B0euczkl.js +1 -0
- package/dist/assets/web/dist/assets/php-DkL3k_n7.js +1 -0
- package/dist/assets/web/dist/assets/pierre-dark-CpLgRqie.js +1 -0
- package/dist/assets/web/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
- package/dist/assets/web/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
- package/dist/assets/web/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
- package/dist/assets/web/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
- package/dist/assets/web/dist/assets/pierre-light-CoaEpmwp.js +1 -0
- package/dist/assets/web/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
- package/dist/assets/web/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
- package/dist/assets/web/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
- package/dist/assets/web/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
- package/dist/assets/web/dist/assets/pkl-ot-7Btpt.js +1 -0
- package/dist/assets/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
- package/dist/assets/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
- package/dist/assets/web/dist/assets/po-BiJDBrnU.js +1 -0
- package/dist/assets/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
- package/dist/assets/web/dist/assets/polar-C7UOKdEL.js +1 -0
- package/dist/assets/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
- package/dist/assets/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
- package/dist/assets/web/dist/assets/powershell-CCSX75yM.js +1 -0
- package/dist/assets/web/dist/assets/prisma-DMgNhnYS.js +1 -0
- package/dist/assets/web/dist/assets/prolog-iXnhIJG7.js +1 -0
- package/dist/assets/web/dist/assets/proto-DB4EqR-F.js +1 -0
- package/dist/assets/web/dist/assets/pug-L_OjjZP4.js +1 -0
- package/dist/assets/web/dist/assets/puppet-CDv2pdJW.js +1 -0
- package/dist/assets/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
- package/dist/assets/web/dist/assets/python-gzcpVVnB.js +1 -0
- package/dist/assets/web/dist/assets/qml-BMjB00Zz.js +1 -0
- package/dist/assets/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
- package/dist/assets/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
- package/dist/assets/web/dist/assets/r-DoeLdnqR.js +1 -0
- package/dist/assets/web/dist/assets/racket-DcIDlBhZ.js +1 -0
- package/dist/assets/web/dist/assets/raku-B3gFvitq.js +1 -0
- package/dist/assets/web/dist/assets/razor-BzYNkAWy.js +1 -0
- package/dist/assets/web/dist/assets/rbs-C0MpOrj4.js +1 -0
- package/dist/assets/web/dist/assets/red-DX1BQOs8.js +1 -0
- package/dist/assets/web/dist/assets/reg-CRGYupPL.js +1 -0
- package/dist/assets/web/dist/assets/regexp-CPmElMk3.js +1 -0
- package/dist/assets/web/dist/assets/rel-BtDbiS_P.js +1 -0
- package/dist/assets/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
- package/dist/assets/web/dist/assets/ron-VUp2lXgN.js +1 -0
- package/dist/assets/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
- package/dist/assets/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
- package/dist/assets/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
- package/dist/assets/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
- package/dist/assets/web/dist/assets/rst-DrsjgfI7.js +1 -0
- package/dist/assets/web/dist/assets/ruby-cB42ppvx.js +1 -0
- package/dist/assets/web/dist/assets/rust-Cfkwpbl8.js +1 -0
- package/dist/assets/web/dist/assets/sas-R5Y3NM_I.js +1 -0
- package/dist/assets/web/dist/assets/sass-DXrisJhu.js +1 -0
- package/dist/assets/web/dist/assets/scala-BiFt_7mu.js +1 -0
- package/dist/assets/web/dist/assets/scheme-DQCgrYNe.js +1 -0
- package/dist/assets/web/dist/assets/scss-C-zWxV9s.js +1 -0
- package/dist/assets/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
- package/dist/assets/web/dist/assets/seed-DeKSHrt2.js +6 -0
- package/dist/assets/web/dist/assets/shaderlab-TOUzSsQk.js +1 -0
- package/dist/assets/web/dist/assets/shellscript-oJF96aAU.js +1 -0
- package/dist/assets/web/dist/assets/shellsession-CznECKyi.js +1 -0
- package/dist/assets/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
- package/dist/assets/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/dist/assets/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
- package/dist/assets/web/dist/assets/smithy-Dgaz7aur.js +1 -0
- package/dist/assets/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/dist/assets/web/dist/assets/solarized-dark-UdsmMDCL.js +1 -0
- package/dist/assets/web/dist/assets/solarized-light-C1IUL_tW.js +1 -0
- package/dist/assets/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
- package/dist/assets/web/dist/assets/soy-wjHLSRag.js +1 -0
- package/dist/assets/web/dist/assets/sparql-D_iOobhT.js +1 -0
- package/dist/assets/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
- package/dist/assets/web/dist/assets/sql-C_BM8IOW.js +1 -0
- package/dist/assets/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
- package/dist/assets/web/dist/assets/stata-D31HO8aQ.js +1 -0
- package/dist/assets/web/dist/assets/stylus-B6D30XZt.js +1 -0
- package/dist/assets/web/dist/assets/surrealql-CKCLyplA.js +1 -0
- package/dist/assets/web/dist/assets/svelte-3geWk2iu.js +1 -0
- package/dist/assets/web/dist/assets/swift-CyEgAFGc.js +1 -0
- package/dist/assets/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/dist/assets/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
- package/dist/assets/web/dist/assets/systemd-BxMlprV5.js +1 -0
- package/dist/assets/web/dist/assets/talonscript-CohzipZa.js +1 -0
- package/dist/assets/web/dist/assets/tasl-DMoTqEGO.js +1 -0
- package/dist/assets/web/dist/assets/tcl-CZd0xW_V.js +1 -0
- package/dist/assets/web/dist/assets/templ-DiJr_hTD.js +1 -0
- package/dist/assets/web/dist/assets/terraform-DswuEJGm.js +1 -0
- package/dist/assets/web/dist/assets/tex-BEJGWyeF.js +1 -0
- package/dist/assets/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/dist/assets/web/dist/assets/toml-CcmNWLt0.js +1 -0
- package/dist/assets/web/dist/assets/ts-tags-C8Mzdpxx.js +1 -0
- package/dist/assets/web/dist/assets/tsv-sltzmVWM.js +1 -0
- package/dist/assets/web/dist/assets/tsx-UQiSm_p3.js +1 -0
- package/dist/assets/web/dist/assets/turtle-ByJddavk.js +1 -0
- package/dist/assets/web/dist/assets/twig-_Pvzkeo9.js +1 -0
- package/dist/assets/web/dist/assets/typescript-BWMkINKK.js +1 -0
- package/dist/assets/web/dist/assets/typespec-BRdr0IET.js +1 -0
- package/dist/assets/web/dist/assets/typst-Bdg9m-e7.js +1 -0
- package/dist/assets/web/dist/assets/v-DBj4K9d_.js +1 -0
- package/dist/assets/web/dist/assets/vala-zf12oZj6.js +1 -0
- package/dist/assets/web/dist/assets/vb-DOwzaHZ0.js +1 -0
- package/dist/assets/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
- package/dist/assets/web/dist/assets/vesper-DdrHHSXu.js +1 -0
- package/dist/assets/web/dist/assets/vhdl-BroJfC0k.js +1 -0
- package/dist/assets/web/dist/assets/viml-DvXPmvsu.js +1 -0
- package/dist/assets/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/dist/assets/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/dist/assets/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
- package/dist/assets/web/dist/assets/vue-CDZrrAZ8.js +1 -0
- package/dist/assets/web/dist/assets/vue-html-tAlpNhfN.js +1 -0
- package/dist/assets/web/dist/assets/vue-vine-C27UF_rh.js +1 -0
- package/dist/assets/web/dist/assets/vyper-CgoNMtux.js +1 -0
- package/dist/assets/web/dist/assets/wallClock-61NpVkY6.js +1 -0
- package/dist/assets/web/dist/assets/wasm-BnjxR4X6.js +1 -0
- package/dist/assets/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
- package/dist/assets/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
- package/dist/assets/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
- package/dist/assets/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
- package/dist/assets/web/dist/assets/wit-DdvCle-K.js +1 -0
- package/dist/assets/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
- package/dist/assets/web/dist/assets/xml-CxeDr9Zh.js +1 -0
- package/dist/assets/web/dist/assets/xsl-Bpi0uUnM.js +1 -0
- package/dist/assets/web/dist/assets/yaml-BtUgZ1GN.js +1 -0
- package/dist/assets/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
- package/dist/assets/web/dist/assets/zig-CMLA9XwU.js +1 -0
- package/dist/cli.js +6936 -3347
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/AppShell-Bw3-_TTE.js +0 -1
- package/dist/assets/web/dist/assets/DeliveryPage-BRwcQyr7.js +0 -1
- package/dist/assets/web/dist/assets/NotFoundPage-RjH-9dPy.js +0 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-BRy5wkv9.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-DAEVLN8j.js +0 -1
- package/dist/assets/web/dist/assets/RunRoutePage-B1KHmkZ9.js +0 -126
- package/dist/assets/web/dist/assets/RunRoutePage-CdbrbqU2.css +0 -1
- package/dist/assets/web/dist/assets/RunsIndexPage-3hUWFpFV.js +0 -1
- package/dist/assets/web/dist/assets/RunsTabs-Qn_5TVJU.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-DysVvVm1.js +0 -1
- package/dist/assets/web/dist/assets/StatusDot-Bug2a6T6.js +0 -1
- package/dist/assets/web/dist/assets/Tooltip-C2eEUbwn.js +0 -1
- package/dist/assets/web/dist/assets/main-0uhp-taL.js +0 -28
- package/dist/assets/web/dist/assets/main-Xickfv8V.css +0 -1
- package/dist/assets/web/dist/assets/seed-B8mQEhhw.js +0 -6
- package/dist/assets/web/dist/assets/wallClock-DI4HIEN5.js +0 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { REPO_SLUG, type DeliveryRange, type PullRequestFacts } from "./delivery.js";
|
|
2
|
+
import { errorSuffix } from "./workerError.js";
|
|
3
|
+
|
|
4
|
+
// Where a repository's delivery snapshot lives (docs/reference/specs/delivery.md item 10):
|
|
5
|
+
// the merged pull requests' facts over the snapshot window, as GitHub gave
|
|
6
|
+
// them, and when they were read. Two implementations of one seam (AGENTS.md
|
|
7
|
+
// invariant 2):
|
|
8
|
+
//
|
|
9
|
+
// - `WorkerDeliverySnapshotStore` — the production choice — an HTTPS client
|
|
10
|
+
// to the `DeliveryDO` on the state Worker (deploy/cloudflare-memory/, one
|
|
11
|
+
// SQLite Durable Object, the same MEMORY_TOKEN bearer as memory, run history
|
|
12
|
+
// and the schedule store), so a bot restart keeps the snapshot (invariant 6)
|
|
13
|
+
// and the first request after one is served, not computed.
|
|
14
|
+
// - `InMemoryDeliverySnapshotStore` — tests, and the process without a state
|
|
15
|
+
// Worker: the snapshot lives as long as the process does.
|
|
16
|
+
//
|
|
17
|
+
// Node-free on purpose: the state Worker imports `isDeliverySnapshot` by
|
|
18
|
+
// relative path so the bot and the Worker validate ONE shape.
|
|
19
|
+
//
|
|
20
|
+
// A snapshot is written whole once — the first read of a repository — and
|
|
21
|
+
// patched by every refresh after: the rows the refresh re-read, the rows that
|
|
22
|
+
// aged out of the window, the new meta. A busy repository's window is many MB
|
|
23
|
+
// and its hourly change a few rows, so the write follows the change.
|
|
24
|
+
//
|
|
25
|
+
// Route contract (JSON in/out, bearer = the Worker's MEMORY_TOKEN):
|
|
26
|
+
// POST /delivery/get {repo} → {snapshot: DeliverySnapshot | null}
|
|
27
|
+
// POST /delivery/put {snapshot} → {ok: true, prs} replace the repository's snapshot whole
|
|
28
|
+
// POST /delivery/merge {patch} → {ok: true, prs} | 404 apply a refresh; 404 with no snapshot to merge into
|
|
29
|
+
|
|
30
|
+
/** What a snapshot says about itself: everything but the rows. */
|
|
31
|
+
export interface DeliverySnapshotMeta {
|
|
32
|
+
/** `owner/name`. */
|
|
33
|
+
repo: string;
|
|
34
|
+
/** ISO 8601 — when the read from GitHub began. */
|
|
35
|
+
snapshotAt: string;
|
|
36
|
+
/** The window the facts were read for: Monday-start weeks ending on the snapshot day. */
|
|
37
|
+
range: DeliveryRange;
|
|
38
|
+
/** The read stopped at its page cap before the window's start — the newest pull requests only. */
|
|
39
|
+
truncated: boolean;
|
|
40
|
+
/** ISO 8601 — every pull request merged at or after this instant is in `prs`: the window's
|
|
41
|
+
* start on a complete read, the oldest update a capped listing reached otherwise. */
|
|
42
|
+
completeFrom: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One repository's facts as a source assembled them, and when. */
|
|
46
|
+
export interface DeliverySnapshot extends DeliverySnapshotMeta {
|
|
47
|
+
/** Every pull request merged inside the window that the reads reached, in number order. */
|
|
48
|
+
prs: PullRequestFacts[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** What one refresh changes in a stored snapshot: the new meta, the rows the read re-read — they
|
|
52
|
+
* replace the stored ones by number — and the numbers of the rows that aged out of the window. */
|
|
53
|
+
export interface DeliverySnapshotPatch extends DeliverySnapshotMeta {
|
|
54
|
+
upsert: PullRequestFacts[];
|
|
55
|
+
drop: number[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
59
|
+
const isInstant = (v: unknown): v is string => typeof v === "string" && Number.isFinite(Date.parse(v));
|
|
60
|
+
|
|
61
|
+
/** The fields the arithmetic reads, present and of the right kind; the arrays' rows are the source's. */
|
|
62
|
+
export function isPullRequestFacts(v: unknown): v is PullRequestFacts {
|
|
63
|
+
if (typeof v !== "object" || v === null) return false;
|
|
64
|
+
const p = v as Record<string, unknown>;
|
|
65
|
+
return (
|
|
66
|
+
typeof p.number === "number" &&
|
|
67
|
+
Number.isFinite(p.number) &&
|
|
68
|
+
typeof p.title === "string" &&
|
|
69
|
+
typeof p.author === "string" &&
|
|
70
|
+
isInstant(p.createdAt) &&
|
|
71
|
+
isInstant(p.mergedAt) &&
|
|
72
|
+
(p.updatedAt === undefined || isInstant(p.updatedAt)) &&
|
|
73
|
+
(p.firstHeadSha === undefined || typeof p.firstHeadSha === "string") &&
|
|
74
|
+
Array.isArray(p.ci) &&
|
|
75
|
+
Array.isArray(p.reviews) &&
|
|
76
|
+
Array.isArray(p.pushes) &&
|
|
77
|
+
(p.issue === undefined || (typeof p.issue === "object" && p.issue !== null))
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The fields every snapshot document carries, present and of the right kind. */
|
|
82
|
+
function hasSnapshotMeta(s: Record<string, unknown>): boolean {
|
|
83
|
+
if (typeof s.repo !== "string" || !REPO_SLUG.test(s.repo)) return false;
|
|
84
|
+
if (!isInstant(s.snapshotAt) || !isInstant(s.completeFrom)) return false;
|
|
85
|
+
if (typeof s.truncated !== "boolean") return false;
|
|
86
|
+
const range = s.range as Record<string, unknown> | null | undefined;
|
|
87
|
+
return (
|
|
88
|
+
typeof range === "object" &&
|
|
89
|
+
range !== null &&
|
|
90
|
+
typeof range.since === "string" &&
|
|
91
|
+
ISO_DAY.test(range.since) &&
|
|
92
|
+
typeof range.until === "string" &&
|
|
93
|
+
ISO_DAY.test(range.until) &&
|
|
94
|
+
typeof range.weeks === "number" &&
|
|
95
|
+
Number.isFinite(range.weeks)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function isDeliverySnapshot(v: unknown): v is DeliverySnapshot {
|
|
100
|
+
if (typeof v !== "object" || v === null) return false;
|
|
101
|
+
const s = v as Record<string, unknown>;
|
|
102
|
+
return hasSnapshotMeta(s) && Array.isArray(s.prs) && s.prs.every(isPullRequestFacts);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function isDeliverySnapshotPatch(v: unknown): v is DeliverySnapshotPatch {
|
|
106
|
+
if (typeof v !== "object" || v === null) return false;
|
|
107
|
+
const { upsert, drop, ...meta } = v as Record<string, unknown>;
|
|
108
|
+
return (
|
|
109
|
+
hasSnapshotMeta(meta) &&
|
|
110
|
+
Array.isArray(upsert) &&
|
|
111
|
+
upsert.every(isPullRequestFacts) &&
|
|
112
|
+
Array.isArray(drop) &&
|
|
113
|
+
drop.every((n) => typeof n === "number" && Number.isFinite(n))
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The snapshot a patch leaves behind when applied to `stored`: rows by number, in number order. */
|
|
118
|
+
export function applyPatch(stored: DeliverySnapshot, patch: DeliverySnapshotPatch): DeliverySnapshot {
|
|
119
|
+
const { upsert, drop, ...meta } = patch;
|
|
120
|
+
const rows = new Map(stored.prs.map((p) => [p.number, p]));
|
|
121
|
+
for (const p of upsert) rows.set(p.number, p);
|
|
122
|
+
for (const n of drop) rows.delete(n);
|
|
123
|
+
return { ...meta, prs: [...rows.values()].sort((a, b) => a.number - b.number) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface DeliverySnapshotStore {
|
|
127
|
+
/** The repository's stored snapshot, or undefined when none was ever stored. Throws on failure. */
|
|
128
|
+
get(repo: string): Promise<DeliverySnapshot | undefined>;
|
|
129
|
+
/** Replace the repository's snapshot whole. Throws on failure; callers warn, never crash. */
|
|
130
|
+
put(snapshot: DeliverySnapshot): Promise<void>;
|
|
131
|
+
/** Apply a refresh to the repository's stored snapshot. False when nothing is stored to merge
|
|
132
|
+
* into — the caller then writes the snapshot whole. Throws on failure. */
|
|
133
|
+
merge(patch: DeliverySnapshotPatch): Promise<boolean>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class InMemoryDeliverySnapshotStore implements DeliverySnapshotStore {
|
|
137
|
+
private readonly byRepo = new Map<string, DeliverySnapshot>();
|
|
138
|
+
|
|
139
|
+
async get(repo: string): Promise<DeliverySnapshot | undefined> {
|
|
140
|
+
const s = this.byRepo.get(repo);
|
|
141
|
+
return s ? structuredClone(s) : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async put(snapshot: DeliverySnapshot): Promise<void> {
|
|
145
|
+
this.byRepo.set(snapshot.repo, structuredClone(snapshot));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async merge(patch: DeliverySnapshotPatch): Promise<boolean> {
|
|
149
|
+
const stored = this.byRepo.get(patch.repo);
|
|
150
|
+
if (!stored) return false;
|
|
151
|
+
this.byRepo.set(patch.repo, applyPatch(stored, structuredClone(patch)));
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Per-request ceiling: a snapshot of a busy repository is a few MB each way. */
|
|
157
|
+
export const SNAPSHOT_WORKER_TIMEOUT_MS = 30_000;
|
|
158
|
+
|
|
159
|
+
export interface WorkerDeliverySnapshotStoreOptions {
|
|
160
|
+
/** Base URL of the state Worker. */
|
|
161
|
+
baseUrl: string;
|
|
162
|
+
/** Bearer secret (MEMORY_TOKEN on the Worker). */
|
|
163
|
+
token: string;
|
|
164
|
+
/** Injectable for tests; defaults to global fetch. */
|
|
165
|
+
fetch?: typeof fetch;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export class WorkerDeliverySnapshotStore implements DeliverySnapshotStore {
|
|
169
|
+
private readonly baseUrl: string;
|
|
170
|
+
private readonly fetchImpl: typeof fetch;
|
|
171
|
+
|
|
172
|
+
constructor(private readonly opts: WorkerDeliverySnapshotStoreOptions) {
|
|
173
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
174
|
+
this.fetchImpl = opts.fetch ?? fetch;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async get(repo: string): Promise<DeliverySnapshot | undefined> {
|
|
178
|
+
const res = await this.post("/delivery/get", { repo });
|
|
179
|
+
if (!res.ok) throw new Error(`state Worker /delivery/get HTTP ${res.status}${await errorSuffix(res)}`);
|
|
180
|
+
const data = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
181
|
+
if (data.snapshot === null || data.snapshot === undefined) return undefined;
|
|
182
|
+
if (!isDeliverySnapshot(data.snapshot))
|
|
183
|
+
throw new Error(`state Worker /delivery/get: the answer for ${repo} is not a delivery snapshot`);
|
|
184
|
+
return data.snapshot;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async put(snapshot: DeliverySnapshot): Promise<void> {
|
|
188
|
+
const res = await this.post("/delivery/put", { snapshot });
|
|
189
|
+
if (!res.ok) throw new Error(`state Worker /delivery/put HTTP ${res.status}${await errorSuffix(res)}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async merge(patch: DeliverySnapshotPatch): Promise<boolean> {
|
|
193
|
+
const res = await this.post("/delivery/merge", { patch });
|
|
194
|
+
// Nothing stored to merge into — or a Worker from before the route: either way the caller writes whole.
|
|
195
|
+
if (res.status === 404) return false;
|
|
196
|
+
if (!res.ok) throw new Error(`state Worker /delivery/merge HTTP ${res.status}${await errorSuffix(res)}`);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private post(path: string, body: unknown): Promise<Response> {
|
|
201
|
+
return this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${this.opts.token}` },
|
|
204
|
+
body: JSON.stringify(body),
|
|
205
|
+
signal: AbortSignal.timeout(SNAPSHOT_WORKER_TIMEOUT_MS),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** A `{ baseUrl, tokenEnv? }` reference to the state Worker, as every `*.worker` config block spells it. */
|
|
211
|
+
export interface StateWorkerRef {
|
|
212
|
+
baseUrl: string;
|
|
213
|
+
/** Env var holding the bearer; default MEMORY_TOKEN. */
|
|
214
|
+
tokenEnv?: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The config blocks that may name the state Worker — one Worker, referenced from each capability that uses it. */
|
|
218
|
+
export interface StateWorkerBlocks {
|
|
219
|
+
runtimeOverrides?: { worker?: StateWorkerRef };
|
|
220
|
+
runHistory?: { worker?: StateWorkerRef };
|
|
221
|
+
schedules?: { worker?: StateWorkerRef };
|
|
222
|
+
memory?: { worker?: StateWorkerRef };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const DEFAULT_TOKEN_ENV = "MEMORY_TOKEN";
|
|
226
|
+
|
|
227
|
+
/** The slice of the process's `Secrets` the builder reads (src/secrets.ts; named
|
|
228
|
+
* structurally so this module stays free of the bot's Node-only imports for the Worker). */
|
|
229
|
+
export interface SecretReader {
|
|
230
|
+
named(name: string): { reveal(): string } | undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Process-startup store selection (src/index.ts, the CLI): the Worker store
|
|
234
|
+
* behind whichever `*.worker` block names the state Worker — the snapshot is
|
|
235
|
+
* a cache, so it earns no block of its own — else the in-memory store with a
|
|
236
|
+
* warning naming what a restart then costs. Pure w.r.t. the environment. */
|
|
237
|
+
export function buildDeliverySnapshotStore(
|
|
238
|
+
config: StateWorkerBlocks,
|
|
239
|
+
secrets: SecretReader,
|
|
240
|
+
warn: (message: string) => void,
|
|
241
|
+
): DeliverySnapshotStore {
|
|
242
|
+
const worker = [config.runtimeOverrides, config.runHistory, config.schedules, config.memory].find(
|
|
243
|
+
(block) => block?.worker?.baseUrl,
|
|
244
|
+
)?.worker;
|
|
245
|
+
if (!worker) {
|
|
246
|
+
warn(
|
|
247
|
+
"delivery snapshots are kept in memory — a restart loses them and the first request after one reads GitHub live. Name the state Worker (runHistory.worker, runtimeOverrides.worker, schedules.worker or memory.worker) to keep them on it.",
|
|
248
|
+
);
|
|
249
|
+
return new InMemoryDeliverySnapshotStore();
|
|
250
|
+
}
|
|
251
|
+
const tokenEnv = worker.tokenEnv ?? DEFAULT_TOKEN_ENV;
|
|
252
|
+
const token = secrets.named(tokenEnv);
|
|
253
|
+
if (!token) {
|
|
254
|
+
warn(
|
|
255
|
+
`the state Worker is configured but ${tokenEnv} is unset — delivery snapshots are kept in memory and a restart loses them. Set ${tokenEnv} to the state Worker's bearer.`,
|
|
256
|
+
);
|
|
257
|
+
return new InMemoryDeliverySnapshotStore();
|
|
258
|
+
}
|
|
259
|
+
return new WorkerDeliverySnapshotStore({ baseUrl: worker.baseUrl, token: token.reveal() });
|
|
260
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// Deterministic review verdict → GitHub comment body.
|
|
2
|
+
//
|
|
3
|
+
// Downstream automation (a repository's opt-in `auto-approve-review-lgtm.yml`
|
|
4
|
+
// workflow) approves a PR when the review App's review body STARTS WITH the
|
|
5
|
+
// exact token `LGTM:`. That token must therefore never depend on how the model happens to
|
|
6
|
+
// phrase its opening line. The model states its judgement through the
|
|
7
|
+
// structured `submit_verdict` tool (src/tools/workspace.ts); this module turns
|
|
8
|
+
// that structured value into the first line of the posted body:
|
|
9
|
+
//
|
|
10
|
+
// approve → "LGTM: <summary>"
|
|
11
|
+
// request_changes → "Changes requested: <summary>"
|
|
12
|
+
// (no verdict) → "No verdict submitted — not approving." (fail-closed)
|
|
13
|
+
//
|
|
14
|
+
// The model's prose follows after a blank line. Whatever the prose says, only
|
|
15
|
+
// an explicit `approve` verdict can produce a body that begins with "LGTM".
|
|
16
|
+
//
|
|
17
|
+
// The verdict also carries typed findings (docs/reference/specs/agent-ship.md item 6):
|
|
18
|
+
// one compact entry per issue, rendered as a list under the verdict line, so
|
|
19
|
+
// a fix round can reference each finding by its stable id and answer it with
|
|
20
|
+
// a typed disposition (`parseDispositionsInput` below). Findings validate
|
|
21
|
+
// fail-closed PER finding — a malformed finding drops with a note, a
|
|
22
|
+
// malformed array drops the whole field — and an `approve` carrying a
|
|
23
|
+
// self-declared `blocking` finding is downgraded to `request_changes`, so the
|
|
24
|
+
// posted body can never begin with `LGTM:` over a defect the reviewer itself
|
|
25
|
+
// called blocking.
|
|
26
|
+
|
|
27
|
+
import { normalizeHead } from "./reviewedHead.js";
|
|
28
|
+
import { redactSecrets } from "./redact.js";
|
|
29
|
+
|
|
30
|
+
export type ReviewVerdictKind = "approve" | "request_changes";
|
|
31
|
+
|
|
32
|
+
export const FINDING_SEVERITIES = ["blocking", "major", "minor", "nit"] as const;
|
|
33
|
+
export type FindingSeverity = (typeof FINDING_SEVERITIES)[number];
|
|
34
|
+
|
|
35
|
+
export interface Finding {
|
|
36
|
+
/** Stable id the reviewer assigned in order ("F1", "F2", …) — dispositions
|
|
37
|
+
* reference findings by this id. */
|
|
38
|
+
id: string;
|
|
39
|
+
severity: FindingSeverity;
|
|
40
|
+
/** Repo-relative file the finding points at. */
|
|
41
|
+
file: string;
|
|
42
|
+
/** Optional 1-based line. An unusable value is dropped like a malformed
|
|
43
|
+
* `head` — the finding stands without it. */
|
|
44
|
+
line?: number;
|
|
45
|
+
/** One line naming the issue; the full explanation lives in the prose. */
|
|
46
|
+
title: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ReviewVerdict {
|
|
50
|
+
verdict: ReviewVerdictKind;
|
|
51
|
+
/** One line: why. Newlines are collapsed so the token line stays one line. */
|
|
52
|
+
summary: string;
|
|
53
|
+
/** The commit the agent says it reviewed (`git rev-parse HEAD` in its
|
|
54
|
+
* checkout), 7–40 lowercase hex. The dispatcher's reviewed-head guard
|
|
55
|
+
* (reviewedHead.ts) compares it to the PR head when the workspace HEAD
|
|
56
|
+
* could not be observed directly. Absent when not supplied or malformed. */
|
|
57
|
+
head?: string;
|
|
58
|
+
/** Typed findings enumerated through `submit_verdict`. Present only when
|
|
59
|
+
* the input carried a findings array (possibly empty after drops); absent
|
|
60
|
+
* when no array was supplied or the array itself was malformed. */
|
|
61
|
+
findings?: Finding[];
|
|
62
|
+
/** Parse notes naming what was dropped from `findings` (ids/indices with
|
|
63
|
+
* reasons) — surfaced in the tool ack so the model can resubmit, never
|
|
64
|
+
* rendered into the posted body. */
|
|
65
|
+
droppedFindings?: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const LGTM_TOKEN = "LGTM:";
|
|
69
|
+
export const CHANGES_TOKEN = "Changes requested:";
|
|
70
|
+
export const NO_VERDICT_LINE = "No verdict submitted — not approving.";
|
|
71
|
+
|
|
72
|
+
/** Parse an arbitrary tool input into a verdict, or null when it is not one. */
|
|
73
|
+
export function parseVerdictInput(input: Record<string, unknown>): ReviewVerdict | null {
|
|
74
|
+
const verdict = input.verdict;
|
|
75
|
+
if (verdict !== "approve" && verdict !== "request_changes") return null;
|
|
76
|
+
const summary = typeof input.summary === "string" ? oneLine(input.summary) : "";
|
|
77
|
+
const head = normalizeHead(input.head);
|
|
78
|
+
const out: ReviewVerdict = { verdict, summary };
|
|
79
|
+
if (head) out.head = head;
|
|
80
|
+
if (input.findings !== undefined) {
|
|
81
|
+
const parsed = parseFindings(input.findings);
|
|
82
|
+
if (parsed.findings) out.findings = parsed.findings;
|
|
83
|
+
if (parsed.dropped.length) out.droppedFindings = parsed.dropped;
|
|
84
|
+
// Verdict/severity consistency, fail-closed: an approve carrying a
|
|
85
|
+
// blocking finding downgrades so the posted body can never start with
|
|
86
|
+
// `LGTM:` over a self-declared blocking defect. Keyed on the RAW entries'
|
|
87
|
+
// declared severity, not the validated survivors — a blocking entry that
|
|
88
|
+
// itself failed validation and dropped still poisons the approve.
|
|
89
|
+
if (out.verdict === "approve" && parsed.blocking.length > 0) {
|
|
90
|
+
out.verdict = "request_changes";
|
|
91
|
+
out.summary = oneLine(
|
|
92
|
+
`${out.summary} [downgraded from approve: blocking finding${parsed.blocking.length === 1 ? "" : "s"} ${parsed.blocking.join(", ")}]`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Fail-closed per finding: each malformed entry drops with a note naming its
|
|
100
|
+
* index (and id when readable); a non-array drops the whole field.
|
|
101
|
+
* `blocking` labels every raw entry that declared severity "blocking" —
|
|
102
|
+
* valid or not — for the approve downgrade above. */
|
|
103
|
+
function parseFindings(value: unknown): { findings?: Finding[]; dropped: string[]; blocking: string[] } {
|
|
104
|
+
if (!Array.isArray(value)) {
|
|
105
|
+
return { dropped: ["findings: dropped — not an array"], blocking: [] };
|
|
106
|
+
}
|
|
107
|
+
const findings: Finding[] = [];
|
|
108
|
+
const dropped: string[] = [];
|
|
109
|
+
const blocking: string[] = [];
|
|
110
|
+
value.forEach((raw, i) => {
|
|
111
|
+
const parsed = parseFinding(raw);
|
|
112
|
+
if (parsed.finding) {
|
|
113
|
+
findings.push(parsed.finding);
|
|
114
|
+
} else {
|
|
115
|
+
dropped.push(`findings[${i}]${parsed.id ? ` (${parsed.id})` : ""}: dropped — ${parsed.reason}`);
|
|
116
|
+
}
|
|
117
|
+
const label = parsed.finding?.id ?? parsed.id ?? `findings[${i}]`;
|
|
118
|
+
const r = typeof raw === "object" && raw !== null ? (raw as Record<string, unknown>) : undefined;
|
|
119
|
+
if (r?.severity === "blocking") blocking.push(label);
|
|
120
|
+
});
|
|
121
|
+
return { findings, dropped, blocking };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseFinding(
|
|
125
|
+
raw: unknown,
|
|
126
|
+
): { finding: Finding; id?: never; reason?: never } | { finding?: never; id?: string; reason: string } {
|
|
127
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return { reason: "not an object" };
|
|
128
|
+
const r = raw as Record<string, unknown>;
|
|
129
|
+
const id = typeof r.id === "string" ? oneLine(r.id) : "";
|
|
130
|
+
if (!id) return { reason: "missing or empty id" };
|
|
131
|
+
const severity = r.severity;
|
|
132
|
+
if (!(FINDING_SEVERITIES as readonly unknown[]).includes(severity)) {
|
|
133
|
+
return { id, reason: `invalid severity ${JSON.stringify(r.severity)} (expected ${FINDING_SEVERITIES.join("|")})` };
|
|
134
|
+
}
|
|
135
|
+
const file = typeof r.file === "string" ? oneLine(r.file) : "";
|
|
136
|
+
if (!file) return { id, reason: "missing or empty file" };
|
|
137
|
+
const title = typeof r.title === "string" ? oneLine(r.title) : "";
|
|
138
|
+
if (!title) return { id, reason: "missing or empty title" };
|
|
139
|
+
const finding: Finding = { id, severity: severity as FindingSeverity, file, title };
|
|
140
|
+
// Like `head`: an unusable optional locator is dropped, the finding stands —
|
|
141
|
+
// dropping the whole finding over a bad line would also drop the severity
|
|
142
|
+
// that the approve→request_changes downgrade keys on.
|
|
143
|
+
if (typeof r.line === "number" && Number.isInteger(r.line) && r.line >= 1) finding.line = r.line;
|
|
144
|
+
return { finding };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function oneLine(s: string): string {
|
|
148
|
+
return s.replace(/\s*\n+\s*/g, " ").trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The exact first line of the posted body for a verdict (or its absence). */
|
|
152
|
+
export function verdictLine(verdict: ReviewVerdict | undefined): string {
|
|
153
|
+
if (!verdict) return NO_VERDICT_LINE;
|
|
154
|
+
const token = verdict.verdict === "approve" ? LGTM_TOKEN : CHANGES_TOKEN;
|
|
155
|
+
const summary = oneLine(verdict.summary);
|
|
156
|
+
return summary ? `${token} ${summary}` : token;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** One compact finding line — `[severity] id file[:line] — title` — shared by
|
|
160
|
+
* the posted body's list (bulleted below) and ship's synthesized child turns. */
|
|
161
|
+
export function formatFinding(f: Finding): string {
|
|
162
|
+
const location = f.line !== undefined ? `${f.file}:${f.line}` : f.file;
|
|
163
|
+
return `[${f.severity}] ${f.id} ${location} — ${f.title}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Build the body posted to GitHub: the deterministic verdict line, the
|
|
168
|
+
* compact findings list (when present), a blank line, then the model's
|
|
169
|
+
* review text. Never starts with "LGTM" unless the verdict is `approve`.
|
|
170
|
+
*/
|
|
171
|
+
export function buildReviewPostBody(answer: string, verdict: ReviewVerdict | undefined): string {
|
|
172
|
+
const head = [verdictLine(verdict), ...(verdict?.findings ?? []).map((f) => `- ${formatFinding(f)}`)];
|
|
173
|
+
return `${head.join("\n")}\n\n${answer.trim()}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --- Dispositions (the coding side's answer to findings) -------------------
|
|
177
|
+
//
|
|
178
|
+
// A fix round records one disposition per finding through the
|
|
179
|
+
// `submit_dispositions` tool; the ship orchestrator keeps the last valid set
|
|
180
|
+
// and splits a cap report into declined (disposition recorded) vs unaddressed
|
|
181
|
+
// (none). Validation mirrors the findings above: a malformed entry drops with
|
|
182
|
+
// a note; a non-array input rejects the whole call.
|
|
183
|
+
|
|
184
|
+
export type DispositionKind = "fixed" | "declined";
|
|
185
|
+
|
|
186
|
+
export interface FindingDisposition {
|
|
187
|
+
/** The stable finding id from the review verdict this disposition answers. */
|
|
188
|
+
findingId: string;
|
|
189
|
+
disposition: DispositionKind;
|
|
190
|
+
/** One line: what was done, or why the finding was declined. */
|
|
191
|
+
note: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Parse a `submit_dispositions` input. Null when `dispositions` is not an
|
|
196
|
+
* array (the call is rejected, nothing recorded); otherwise each malformed
|
|
197
|
+
* entry drops with a note naming its index (and findingId when readable),
|
|
198
|
+
* mirroring the per-finding validation above. A missing note is tolerated as
|
|
199
|
+
* "" (the summary convention), never a dropped entry.
|
|
200
|
+
*/
|
|
201
|
+
export function parseDispositionsInput(
|
|
202
|
+
input: Record<string, unknown>,
|
|
203
|
+
): { dispositions: FindingDisposition[]; dropped: string[] } | null {
|
|
204
|
+
const value = input.dispositions;
|
|
205
|
+
if (!Array.isArray(value)) return null;
|
|
206
|
+
const dispositions: FindingDisposition[] = [];
|
|
207
|
+
const dropped: string[] = [];
|
|
208
|
+
value.forEach((raw, i) => {
|
|
209
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
210
|
+
dropped.push(`dispositions[${i}]: dropped — not an object`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const r = raw as Record<string, unknown>;
|
|
214
|
+
const findingId = typeof r.findingId === "string" ? oneLine(r.findingId) : "";
|
|
215
|
+
if (!findingId) {
|
|
216
|
+
dropped.push(`dispositions[${i}]: dropped — missing or empty findingId`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const disposition = r.disposition;
|
|
220
|
+
if (disposition !== "fixed" && disposition !== "declined") {
|
|
221
|
+
dropped.push(
|
|
222
|
+
`dispositions[${i}] (${findingId}): dropped — invalid disposition ${JSON.stringify(r.disposition)} (expected fixed|declined)`,
|
|
223
|
+
);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const note = typeof r.note === "string" ? oneLine(r.note) : "";
|
|
227
|
+
dispositions.push({ findingId, disposition, note });
|
|
228
|
+
});
|
|
229
|
+
return { dispositions, dropped };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---- the stored shapes (docs/reference/specs/run-history.md items 2 and 3) ---------------------------
|
|
233
|
+
|
|
234
|
+
const VERDICT_KINDS: readonly string[] = ["approve", "request_changes"];
|
|
235
|
+
const DISPOSITION_KINDS: readonly string[] = ["fixed", "declined"];
|
|
236
|
+
|
|
237
|
+
const isRecordLike = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null;
|
|
238
|
+
|
|
239
|
+
function isFindingShape(v: unknown): v is Finding {
|
|
240
|
+
if (!isRecordLike(v)) return false;
|
|
241
|
+
return (
|
|
242
|
+
typeof v.id === "string" &&
|
|
243
|
+
(FINDING_SEVERITIES as readonly string[]).includes(v.severity as string) &&
|
|
244
|
+
typeof v.file === "string" &&
|
|
245
|
+
typeof v.title === "string" &&
|
|
246
|
+
(v.line === undefined || typeof v.line === "number")
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Structural check on a verdict read back from a stored record: the kind, a
|
|
251
|
+
* string summary, an optional string head, findings each with an id, a known
|
|
252
|
+
* severity, a file and a title. Shape only, no bounds — what a record carries
|
|
253
|
+
* was validated by `parseVerdictInput` on the way in and redacted since. */
|
|
254
|
+
export function isReviewVerdictShape(v: unknown): v is ReviewVerdict {
|
|
255
|
+
if (!isRecordLike(v)) return false;
|
|
256
|
+
if (!VERDICT_KINDS.includes(v.verdict as string) || typeof v.summary !== "string") return false;
|
|
257
|
+
if (v.head !== undefined && typeof v.head !== "string") return false;
|
|
258
|
+
if (v.findings !== undefined && !(Array.isArray(v.findings) && v.findings.every(isFindingShape))) return false;
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Structural check on a disposition set read back from a stored record. */
|
|
263
|
+
export function isFindingDispositionsShape(v: unknown): v is FindingDisposition[] {
|
|
264
|
+
return (
|
|
265
|
+
Array.isArray(v) &&
|
|
266
|
+
v.every(
|
|
267
|
+
(d) =>
|
|
268
|
+
isRecordLike(d) &&
|
|
269
|
+
typeof d.findingId === "string" &&
|
|
270
|
+
DISPOSITION_KINDS.includes(d.disposition as string) &&
|
|
271
|
+
typeof d.note === "string",
|
|
272
|
+
)
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Every string leaf of the verdict through the redaction seam — the summary,
|
|
277
|
+
* each finding's file and title — the input untouched: what the one record
|
|
278
|
+
* assembly writes. `droppedFindings` are parse notes for the model and never
|
|
279
|
+
* ride a record. */
|
|
280
|
+
export function redactVerdict(v: ReviewVerdict, redact: (s: string) => string = redactSecrets): ReviewVerdict {
|
|
281
|
+
return {
|
|
282
|
+
verdict: v.verdict,
|
|
283
|
+
summary: redact(v.summary),
|
|
284
|
+
...(v.head !== undefined ? { head: v.head } : {}),
|
|
285
|
+
...(v.findings !== undefined
|
|
286
|
+
? { findings: v.findings.map((f) => ({ ...f, file: redact(f.file), title: redact(f.title) })) }
|
|
287
|
+
: {}),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function redactDispositions(
|
|
292
|
+
dispositions: readonly FindingDisposition[],
|
|
293
|
+
redact: (s: string) => string = redactSecrets,
|
|
294
|
+
): FindingDisposition[] {
|
|
295
|
+
return dispositions.map((d) => ({ findingId: d.findingId, disposition: d.disposition, note: redact(d.note) }));
|
|
296
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Reviewed-head guard for the review post-step (docs/reference/specs/agent-review.md item 8).
|
|
2
|
+
//
|
|
3
|
+
// A review may only be posted to a PR when the commit the agent ACTUALLY
|
|
4
|
+
// reviewed is the PR head resolved for the run (`RepoContext.headSha`). The
|
|
5
|
+
// failure this guards against: the agent's own worktree is on the right
|
|
6
|
+
// branch, but it fetches and checks out another PR's branch because the PR
|
|
7
|
+
// body references it, reviews that, calls `submit_verdict approve`, and the
|
|
8
|
+
// deterministic post-step puts `LGTM:` on the PR under review — which an
|
|
9
|
+
// auto-approve workflow then approves. Every layer has done its job except the
|
|
10
|
+
// one that asks "is this review OF this PR?".
|
|
11
|
+
//
|
|
12
|
+
// Two sources for the reviewed head, in authority order:
|
|
13
|
+
// observed — `git rev-parse HEAD` run by the dispatcher in the run's
|
|
14
|
+
// workspace AFTER the model finished and BEFORE the workspace is
|
|
15
|
+
// released (a resident worktree; authoritative). Absent when the
|
|
16
|
+
// cwd is not a git repo (the cold sandbox's workspace root — the
|
|
17
|
+
// clone lives in a subdirectory the agent chose).
|
|
18
|
+
// reported — the `head` the agent passed to `submit_verdict` (it is told to
|
|
19
|
+
// run `git rev-parse HEAD` in the checkout it reviewed). Consulted
|
|
20
|
+
// only when nothing was observed; an observed mismatch is never
|
|
21
|
+
// rescued by a matching report.
|
|
22
|
+
//
|
|
23
|
+
// Fail-closed: unknown PR head, or no head from either source → no post.
|
|
24
|
+
|
|
25
|
+
export type HeadCheck = { ok: true; head: string; source: "observed" | "reported" } | { ok: false; reason: string };
|
|
26
|
+
|
|
27
|
+
const FULL_SHA = /^[0-9a-f]{40}$/;
|
|
28
|
+
const SHA_PREFIX = /^[0-9a-f]{7,40}$/;
|
|
29
|
+
|
|
30
|
+
/** Normalize a reported/observed head: trimmed, lowercased, 7–40 hex; else undefined. */
|
|
31
|
+
export function normalizeHead(value: unknown): string | undefined {
|
|
32
|
+
if (typeof value !== "string") return undefined;
|
|
33
|
+
const h = value.trim().toLowerCase();
|
|
34
|
+
return SHA_PREFIX.test(h) ? h : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The 40-hex commit from `git rev-parse HEAD` output, or undefined when the
|
|
38
|
+
* command failed (not a git repo, git missing) or printed nothing usable. */
|
|
39
|
+
export function parseRevParseOutput(output: string): string | undefined {
|
|
40
|
+
for (const line of output.split(/\r?\n/)) {
|
|
41
|
+
const t = line.trim().toLowerCase();
|
|
42
|
+
if (FULL_SHA.test(t)) return t;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Two normalized heads name the same commit when one is a ≥7-hex prefix of
|
|
48
|
+
* the other (the dispatcher's pre-run attach check reuses this). */
|
|
49
|
+
export function sameCommit(a: string, b: string): boolean {
|
|
50
|
+
const n = Math.min(a.length, b.length);
|
|
51
|
+
return n >= 7 && a.slice(0, n) === b.slice(0, n);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Decide whether the reviewed head is the PR head. `expected` is the PR head
|
|
56
|
+
* from repo resolution; `observed` the dispatcher's own `git rev-parse HEAD`
|
|
57
|
+
* (40-hex or undefined); `reported` whatever the agent passed to submit_verdict.
|
|
58
|
+
*/
|
|
59
|
+
export function checkReviewedHead(input: { expected?: string; observed?: string; reported?: string }): HeadCheck {
|
|
60
|
+
const expected = normalizeHead(input.expected);
|
|
61
|
+
if (!expected) return { ok: false, reason: "PR head unknown at resolution time — cannot verify what was reviewed" };
|
|
62
|
+
const observed = normalizeHead(input.observed);
|
|
63
|
+
if (observed) {
|
|
64
|
+
return sameCommit(observed, expected)
|
|
65
|
+
? { ok: true, head: observed, source: "observed" }
|
|
66
|
+
: { ok: false, reason: `reviewed head ${observed.slice(0, 7)} is not the PR head ${expected.slice(0, 7)}` };
|
|
67
|
+
}
|
|
68
|
+
const reported = normalizeHead(input.reported);
|
|
69
|
+
if (!reported)
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
reason: "reviewed head unknown — the workspace HEAD could not be read and no head was reported with the verdict",
|
|
73
|
+
};
|
|
74
|
+
return sameCommit(reported, expected)
|
|
75
|
+
? { ok: true, head: reported, source: "reported" }
|
|
76
|
+
: { ok: false, reason: `reviewed head ${reported.slice(0, 7)} is not the PR head ${expected.slice(0, 7)}` };
|
|
77
|
+
}
|
|
@@ -319,8 +319,10 @@ export type RunEvent =
|
|
|
319
319
|
text: string;
|
|
320
320
|
/** Where the request came from, for the Request block: the channel and
|
|
321
321
|
* user display names and a link back to the triggering message —
|
|
322
|
-
* whatever the adapter supplied (all optional)
|
|
323
|
-
|
|
322
|
+
* whatever the adapter supplied (all optional) — and, for a follow-up a
|
|
323
|
+
* run sent rather than a person (a parent's `send_to_run`), that run's
|
|
324
|
+
* id (docs/reference/specs/agent-conductor.md item 8). */
|
|
325
|
+
source?: { url?: string; channel?: string; user?: string; run?: string };
|
|
324
326
|
seq?: number;
|
|
325
327
|
at?: number;
|
|
326
328
|
}
|
|
@@ -9,7 +9,7 @@ import type { ClaimResult, FenceResult, LivePhase } from "./types.js";
|
|
|
9
9
|
* after a lost response), anything else is refused with what the steer
|
|
10
10
|
* message needs. */
|
|
11
11
|
export function decideClaim(
|
|
12
|
-
existing: { runId: string; agent?: string; startedAt: number; ownerGen: string } | undefined,
|
|
12
|
+
existing: { runId: string; agent?: string; startedAt: number; ownerGen: string; idempotencyKey?: string } | undefined,
|
|
13
13
|
req: { runId: string; gen: string },
|
|
14
14
|
): ClaimResult {
|
|
15
15
|
if (!existing) return { ok: true };
|
|
@@ -21,6 +21,7 @@ export function decideClaim(
|
|
|
21
21
|
runId: existing.runId,
|
|
22
22
|
...(existing.agent !== undefined ? { agent: existing.agent } : {}),
|
|
23
23
|
startedAt: existing.startedAt,
|
|
24
|
+
...(existing.idempotencyKey !== undefined ? { idempotencyKey: existing.idempotencyKey } : {}),
|
|
24
25
|
},
|
|
25
26
|
};
|
|
26
27
|
}
|