@repo-os/repoos 0.5.45
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.md +110 -0
- package/README.md +272 -0
- package/dist/.build-info.json +4 -0
- package/dist/.build-stamp.json +3 -0
- package/dist/cli/colors.d.ts +13 -0
- package/dist/cli/colors.js +37 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +221 -0
- package/dist/commands/check.d.ts +170 -0
- package/dist/commands/check.js +1227 -0
- package/dist/commands/docs.d.ts +2 -0
- package/dist/commands/docs.js +70 -0
- package/dist/commands/gc.d.ts +1 -0
- package/dist/commands/gc.js +57 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +699 -0
- package/dist/commands/screenshots.d.ts +7 -0
- package/dist/commands/screenshots.js +77 -0
- package/dist/commands/serve.d.ts +55 -0
- package/dist/commands/serve.js +204 -0
- package/dist/commands/status.d.ts +140 -0
- package/dist/commands/status.js +569 -0
- package/dist/commands/stop.d.ts +1 -0
- package/dist/commands/stop.js +59 -0
- package/dist/commands/tasks.d.ts +47 -0
- package/dist/commands/tasks.js +334 -0
- package/dist/commands/tunnel.d.ts +1 -0
- package/dist/commands/tunnel.js +1271 -0
- package/dist/commands/ui-harness.d.ts +53 -0
- package/dist/commands/ui-harness.js +62 -0
- package/dist/commands/ui-smoke.d.ts +6 -0
- package/dist/commands/ui-smoke.js +220 -0
- package/dist/commands/upgrade.d.ts +15 -0
- package/dist/commands/upgrade.js +188 -0
- package/dist/core/auth-store.d.ts +81 -0
- package/dist/core/auth-store.js +530 -0
- package/dist/core/auth.d.ts +75 -0
- package/dist/core/auth.js +124 -0
- package/dist/core/bootstrap.d.ts +41 -0
- package/dist/core/bootstrap.js +236 -0
- package/dist/core/build.d.ts +92 -0
- package/dist/core/build.js +249 -0
- package/dist/core/canary.d.ts +10 -0
- package/dist/core/canary.js +14 -0
- package/dist/core/check-failure-summary.d.ts +10 -0
- package/dist/core/check-failure-summary.js +34 -0
- package/dist/core/config.d.ts +160 -0
- package/dist/core/config.js +1288 -0
- package/dist/core/context-pack.d.ts +74 -0
- package/dist/core/context-pack.js +778 -0
- package/dist/core/db.d.ts +236 -0
- package/dist/core/db.js +747 -0
- package/dist/core/detect.d.ts +69 -0
- package/dist/core/detect.js +251 -0
- package/dist/core/docs.d.ts +76 -0
- package/dist/core/docs.js +225 -0
- package/dist/core/frontmatter.d.ts +29 -0
- package/dist/core/frontmatter.js +215 -0
- package/dist/core/git.d.ts +505 -0
- package/dist/core/git.js +1453 -0
- package/dist/core/indexer.d.ts +33 -0
- package/dist/core/indexer.js +282 -0
- package/dist/core/input.d.ts +66 -0
- package/dist/core/input.js +222 -0
- package/dist/core/logger.d.ts +70 -0
- package/dist/core/logger.js +138 -0
- package/dist/core/models.d.ts +45 -0
- package/dist/core/models.js +291 -0
- package/dist/core/net-probe.d.ts +4 -0
- package/dist/core/net-probe.js +20 -0
- package/dist/core/providers/deepinfra.d.ts +23 -0
- package/dist/core/providers/deepinfra.js +44 -0
- package/dist/core/providers/index.d.ts +24 -0
- package/dist/core/providers/index.js +59 -0
- package/dist/core/providers/openrouter.d.ts +28 -0
- package/dist/core/providers/openrouter.js +82 -0
- package/dist/core/providers/spend.d.ts +86 -0
- package/dist/core/providers/spend.js +286 -0
- package/dist/core/providers/types.d.ts +56 -0
- package/dist/core/providers/types.js +39 -0
- package/dist/core/repoos.d.ts +59 -0
- package/dist/core/repoos.js +221 -0
- package/dist/core/runtime.d.ts +35 -0
- package/dist/core/runtime.js +167 -0
- package/dist/core/skills-registry.d.ts +39 -0
- package/dist/core/skills-registry.js +148 -0
- package/dist/core/skills-registry.test.d.ts +1 -0
- package/dist/core/skills-registry.test.js +52 -0
- package/dist/core/sysmem.d.ts +7 -0
- package/dist/core/sysmem.js +55 -0
- package/dist/core/tailscale.d.ts +2 -0
- package/dist/core/tailscale.js +19 -0
- package/dist/core/task.d.ts +53 -0
- package/dist/core/task.js +310 -0
- package/dist/core/toml-line.d.ts +20 -0
- package/dist/core/toml-line.js +52 -0
- package/dist/core/toml-validate.d.ts +44 -0
- package/dist/core/toml-validate.js +335 -0
- package/dist/core/tunnel-assistant.d.ts +30 -0
- package/dist/core/tunnel-assistant.js +65 -0
- package/dist/core/tunnel-registry.d.ts +96 -0
- package/dist/core/tunnel-registry.js +267 -0
- package/dist/core/tunnel-registry.test.d.ts +1 -0
- package/dist/core/tunnel-registry.test.js +309 -0
- package/dist/core/tunnel.d.ts +103 -0
- package/dist/core/tunnel.js +320 -0
- package/dist/core/types.d.ts +811 -0
- package/dist/core/types.js +27 -0
- package/dist/core/worktree-gc.d.ts +57 -0
- package/dist/core/worktree-gc.js +194 -0
- package/dist/server/__tests__/integration-job.test.d.ts +1 -0
- package/dist/server/__tests__/integration-job.test.js +129 -0
- package/dist/server/agents.d.ts +1020 -0
- package/dist/server/agents.js +4070 -0
- package/dist/server/attachments.d.ts +51 -0
- package/dist/server/attachments.js +130 -0
- package/dist/server/auto-engineering.d.ts +45 -0
- package/dist/server/auto-engineering.js +275 -0
- package/dist/server/branchless-release.d.ts +22 -0
- package/dist/server/branchless-release.js +67 -0
- package/dist/server/built-in-agents.d.ts +305 -0
- package/dist/server/built-in-agents.js +1838 -0
- package/dist/server/cto-monitor.d.ts +35 -0
- package/dist/server/cto-monitor.js +281 -0
- package/dist/server/cto.d.ts +125 -0
- package/dist/server/cto.js +590 -0
- package/dist/server/deployments.d.ts +119 -0
- package/dist/server/deployments.js +325 -0
- package/dist/server/diff-snapshot.d.ts +20 -0
- package/dist/server/diff-snapshot.js +73 -0
- package/dist/server/done.d.ts +160 -0
- package/dist/server/done.js +488 -0
- package/dist/server/freeform.d.ts +55 -0
- package/dist/server/freeform.js +179 -0
- package/dist/server/handoff.d.ts +99 -0
- package/dist/server/handoff.js +575 -0
- package/dist/server/hetzner.d.ts +45 -0
- package/dist/server/hetzner.js +117 -0
- package/dist/server/icons.d.ts +8 -0
- package/dist/server/icons.js +195 -0
- package/dist/server/integration-job.d.ts +75 -0
- package/dist/server/integration-job.js +144 -0
- package/dist/server/integration-orchestrator.d.ts +211 -0
- package/dist/server/integration-orchestrator.js +1299 -0
- package/dist/server/integration-status.d.ts +48 -0
- package/dist/server/integration-status.js +71 -0
- package/dist/server/live-index.d.ts +295 -0
- package/dist/server/live-index.js +452 -0
- package/dist/server/model-test.d.ts +20 -0
- package/dist/server/model-test.js +100 -0
- package/dist/server/ntfy.d.ts +57 -0
- package/dist/server/ntfy.js +112 -0
- package/dist/server/pm-attachments.d.ts +35 -0
- package/dist/server/pm-attachments.js +181 -0
- package/dist/server/pm-runs.d.ts +58 -0
- package/dist/server/pm-runs.js +80 -0
- package/dist/server/preview.d.ts +174 -0
- package/dist/server/preview.js +641 -0
- package/dist/server/release.d.ts +88 -0
- package/dist/server/release.js +449 -0
- package/dist/server/reload.d.ts +194 -0
- package/dist/server/reload.js +586 -0
- package/dist/server/remote-validation.d.ts +132 -0
- package/dist/server/remote-validation.js +478 -0
- package/dist/server/repo-lock.d.ts +36 -0
- package/dist/server/repo-lock.js +141 -0
- package/dist/server/review-guard.d.ts +14 -0
- package/dist/server/review-guard.js +159 -0
- package/dist/server/review.d.ts +243 -0
- package/dist/server/review.js +1211 -0
- package/dist/server/routes/agents.d.ts +6 -0
- package/dist/server/routes/agents.js +28 -0
- package/dist/server/routes/auth.d.ts +60 -0
- package/dist/server/routes/auth.js +755 -0
- package/dist/server/routes/config.d.ts +20 -0
- package/dist/server/routes/config.js +347 -0
- package/dist/server/routes/debugger.d.ts +33 -0
- package/dist/server/routes/debugger.js +360 -0
- package/dist/server/routes/deployments.d.ts +3 -0
- package/dist/server/routes/deployments.js +16 -0
- package/dist/server/routes/docs.d.ts +8 -0
- package/dist/server/routes/docs.js +135 -0
- package/dist/server/routes/helpers.d.ts +21 -0
- package/dist/server/routes/helpers.js +171 -0
- package/dist/server/routes/index.d.ts +19 -0
- package/dist/server/routes/index.js +18 -0
- package/dist/server/routes/info.d.ts +17 -0
- package/dist/server/routes/info.js +177 -0
- package/dist/server/routes/inputs.d.ts +7 -0
- package/dist/server/routes/inputs.js +125 -0
- package/dist/server/routes/model-providers.d.ts +27 -0
- package/dist/server/routes/model-providers.js +94 -0
- package/dist/server/routes/models.d.ts +3 -0
- package/dist/server/routes/models.js +36 -0
- package/dist/server/routes/notify.d.ts +2 -0
- package/dist/server/routes/notify.js +17 -0
- package/dist/server/routes/playground.d.ts +29 -0
- package/dist/server/routes/playground.js +81 -0
- package/dist/server/routes/release.d.ts +20 -0
- package/dist/server/routes/release.js +86 -0
- package/dist/server/routes/router.d.ts +12 -0
- package/dist/server/routes/router.js +32 -0
- package/dist/server/routes/skill-registry.d.ts +5 -0
- package/dist/server/routes/skill-registry.js +51 -0
- package/dist/server/routes/tasks.d.ts +58 -0
- package/dist/server/routes/tasks.js +1442 -0
- package/dist/server/routes/transcribe.d.ts +2 -0
- package/dist/server/routes/transcribe.js +124 -0
- package/dist/server/routes/types.d.ts +68 -0
- package/dist/server/routes/types.js +1 -0
- package/dist/server/routes/ui.d.ts +5 -0
- package/dist/server/routes/ui.js +81 -0
- package/dist/server/routes/utils.d.ts +3 -0
- package/dist/server/routes/utils.js +23 -0
- package/dist/server/serve-reaper.d.ts +135 -0
- package/dist/server/serve-reaper.js +491 -0
- package/dist/server/server.d.ts +49 -0
- package/dist/server/server.js +2171 -0
- package/dist/server/supervisor.d.ts +89 -0
- package/dist/server/supervisor.js +231 -0
- package/dist/server/system.d.ts +198 -0
- package/dist/server/system.js +480 -0
- package/dist/server/task-check.d.ts +45 -0
- package/dist/server/task-check.js +71 -0
- package/dist/server/task-transitions.d.ts +51 -0
- package/dist/server/task-transitions.js +78 -0
- package/dist/server/task-watchdog.d.ts +158 -0
- package/dist/server/task-watchdog.js +681 -0
- package/dist/server/test-run.d.ts +28 -0
- package/dist/server/test-run.js +81 -0
- package/dist/server/watcher.d.ts +21 -0
- package/dist/server/watcher.js +194 -0
- package/dist/server/write.d.ts +94 -0
- package/dist/server/write.js +345 -0
- package/dist/ui/assets/AgentsView-CF4wRvHh.css +1 -0
- package/dist/ui/assets/AgentsView-D-myL-kA.js +1 -0
- package/dist/ui/assets/ContextView-D3TC-RSp.css +1 -0
- package/dist/ui/assets/ContextView-DsOqAdyE.js +4 -0
- package/dist/ui/assets/DashboardView-BOhsIJqO.js +1 -0
- package/dist/ui/assets/DashboardView-Ck9o0RP2.css +1 -0
- package/dist/ui/assets/DeploymentsView--iIUVfEH.css +1 -0
- package/dist/ui/assets/DeploymentsView-DXu_FYR2.js +1 -0
- package/dist/ui/assets/InputsView-24HVbVPk.js +1 -0
- package/dist/ui/assets/InputsView-BLb7GHtx.css +1 -0
- package/dist/ui/assets/LoginView-6QlGg6cx.css +1 -0
- package/dist/ui/assets/LoginView-BqupI0Lt.js +1 -0
- package/dist/ui/assets/ReleasesView-BEG1A0YH.css +1 -0
- package/dist/ui/assets/ReleasesView-CAgMuxpe.js +3 -0
- package/dist/ui/assets/SettingsView-CA-NYSMK.css +1 -0
- package/dist/ui/assets/SettingsView-O3iZSnjg.js +3 -0
- package/dist/ui/assets/WorkView-D8Tl6qwy.js +1 -0
- package/dist/ui/assets/WorkView-DMYL2s3W.css +1 -0
- package/dist/ui/assets/abnfDiagram-VCTEODGH-J5lkSnEE.js +1 -0
- package/dist/ui/assets/arc-BHZnmhhM.js +1 -0
- package/dist/ui/assets/architecture-7GRP2DOG-pLdFt-xS.js +1 -0
- package/dist/ui/assets/architectureDiagram-5GKGNRK7-C3k89uKX.js +36 -0
- package/dist/ui/assets/array-BifhSqXX.js +1 -0
- package/dist/ui/assets/blockDiagram-I7D4REHJ-AWNoBzop.js +129 -0
- package/dist/ui/assets/button-lbikqSph.js +1 -0
- package/dist/ui/assets/c4Diagram-7LVT6UL2-DKG1D5gd.js +38 -0
- package/dist/ui/assets/channel-gA4qBv3A.js +1 -0
- package/dist/ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/ui/assets/chunk-4HAMMTFA-D1YR8lF_.js +62 -0
- package/dist/ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/ui/assets/chunk-75Z2AOVW-5GuBxfsL.js +2 -0
- package/dist/ui/assets/chunk-DU6HZSFF-DfdNNqwa.js +127 -0
- package/dist/ui/assets/chunk-F27PBJKO-BPZzinQ4.js +1 -0
- package/dist/ui/assets/chunk-FOHPRMQF-l2z4K2jD.js +161 -0
- package/dist/ui/assets/chunk-GMAD6QVW-DGWvSKj5.js +72 -0
- package/dist/ui/assets/chunk-GVQU2GXP-D6Aw75wJ.js +1 -0
- package/dist/ui/assets/chunk-IMKFNOWR-B8mMBKCW.js +231 -0
- package/dist/ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/ui/assets/chunk-L3NEJ4N5-D5OJuXYE.js +1 -0
- package/dist/ui/assets/chunk-OSK3NFVY-B7r9w2UW.js +10 -0
- package/dist/ui/assets/chunk-P2QGCYS3-C74X4AQX.js +1 -0
- package/dist/ui/assets/chunk-POPQ4Y6H-C5_u1gUS.js +1 -0
- package/dist/ui/assets/chunk-PWAF6VOD-DfoTLtxn.js +1 -0
- package/dist/ui/assets/chunk-SHT3W25Y-D40m2eqz.js +168 -0
- package/dist/ui/assets/chunk-SVP7TREG-euxi0Kap.js +88 -0
- package/dist/ui/assets/chunk-TICWLB2K-fkdxngzN.js +206 -0
- package/dist/ui/assets/chunk-XXDRQBXY-DFBUG-OT.js +1 -0
- package/dist/ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/ui/assets/classDiagram-ZZMXUADV-BzW8bq2P.js +1 -0
- package/dist/ui/assets/classDiagram-v2-VYDZK3BY-BzW8bq2P.js +1 -0
- package/dist/ui/assets/cose-bilkent-JH36ORCC-BTcgMon1.js +1 -0
- package/dist/ui/assets/cynefin-OW5HDTMX-B5m_COsS.js +1 -0
- package/dist/ui/assets/cynefinDiagram-5FMLGOSQ-C-AG6CGe.js +62 -0
- package/dist/ui/assets/cytoscape.esm-FBntH-9t.js +321 -0
- package/dist/ui/assets/dagre-GXQ25YYZ-DDfDnp8L.js +4 -0
- package/dist/ui/assets/dagre-PrKaheQc.js +1 -0
- package/dist/ui/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/ui/assets/diagram-S7CK7UJ4-DjQZAlEY.js +30 -0
- package/dist/ui/assets/diagram-UQ7AKVKN-DAZ9yp1B.js +41 -0
- package/dist/ui/assets/diagram-VSXAHHWV-BWepUKJD.js +3 -0
- package/dist/ui/assets/diagram-VX7I27RA-EcV1J81b.js +24 -0
- package/dist/ui/assets/diagram-Z3DM3KII-Dbz7XU_K.js +24 -0
- package/dist/ui/assets/dist-DTg6UBE_.js +1 -0
- package/dist/ui/assets/ebnfDiagram-PWID7BFC-BUyQw8Mm.js +1 -0
- package/dist/ui/assets/erDiagram-RLTQ6QDP-WVf8ukKt.js +99 -0
- package/dist/ui/assets/eventmodeling-NTZA5JFV-DHqyLOAn.js +1 -0
- package/dist/ui/assets/flowDiagram-HODETNUW-CYk_ze5K.js +1 -0
- package/dist/ui/assets/ganttDiagram-EL5Y4UJY-rB4MZuB-.js +292 -0
- package/dist/ui/assets/gitGraph-4MIJSDKK-BOTWM8dY.js +1 -0
- package/dist/ui/assets/gitGraphDiagram-WWUBYQGX-eYscuhtP.js +106 -0
- package/dist/ui/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/ui/assets/index-BACBfnga.css +1 -0
- package/dist/ui/assets/index-BOQ-ust6.js +26 -0
- package/dist/ui/assets/info-A6RAGUB7-_67wOORe.js +1 -0
- package/dist/ui/assets/infoDiagram-27XIBGKW-XVXbddTA.js +2 -0
- package/dist/ui/assets/init-C-OQMol4.js +1 -0
- package/dist/ui/assets/ishikawaDiagram-5VMMS53U-ClLNiEru.js +70 -0
- package/dist/ui/assets/journeyDiagram-3NMN7TZE-NRawjR6i.js +139 -0
- package/dist/ui/assets/kanban-definition-UXKFOSKX-BgSzBfIx.js +89 -0
- package/dist/ui/assets/katex-DolUETbr.js +257 -0
- package/dist/ui/assets/line-KAyfuul4.js +1 -0
- package/dist/ui/assets/linear-cCCPOpu7.js +1 -0
- package/dist/ui/assets/mermaid-parser.core-CgXZk1iN.js +7 -0
- package/dist/ui/assets/mermaid.core-BUUkChHW.js +44 -0
- package/dist/ui/assets/mindmap-definition-YA3MSWOX-Cpn7vW4r.js +96 -0
- package/dist/ui/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/ui/assets/packet-AYTQ26CC-0lZwM-d6.js +1 -0
- package/dist/ui/assets/path-COt_16Va.js +1 -0
- package/dist/ui/assets/pegDiagram-XKGWAZYB-impy0inn.js +1 -0
- package/dist/ui/assets/pie-WAS4IAKB-CqfdGHYg.js +1 -0
- package/dist/ui/assets/pieDiagram-E7YTZNPT-CxjL4I1h.js +39 -0
- package/dist/ui/assets/quadrantDiagram-AXDQQJYC-Cjd1QMIe.js +7 -0
- package/dist/ui/assets/radar-RG4KPBEZ-CRkQIwiv.js +1 -0
- package/dist/ui/assets/railroad-74A4TZTK-i67XSWTB.js +1 -0
- package/dist/ui/assets/railroad-abnf-HS5TGJTU-Cv9m5aS3.js +1 -0
- package/dist/ui/assets/railroad-ebnf-LZEXJU2U-C5ZIn-Jh.js +1 -0
- package/dist/ui/assets/railroad-peg-WCYAUIDC-CGra9Oq-.js +1 -0
- package/dist/ui/assets/railroadDiagram-O6MQD6OU-_pK0qyaY.js +1 -0
- package/dist/ui/assets/releases-B0WNV-SY.js +1 -0
- package/dist/ui/assets/repoos-cto-square.webp +0 -0
- package/dist/ui/assets/repoos-helpful-pm-square.webp +0 -0
- package/dist/ui/assets/repoos-orchestrator-square.webp +0 -0
- package/dist/ui/assets/repoos-performance-agent-square.webp +0 -0
- package/dist/ui/assets/repoos-ross-from-friends-square.webp +0 -0
- package/dist/ui/assets/repoos-tech-debt-agent-square.webp +0 -0
- package/dist/ui/assets/requirementDiagram-BXWQKSXE-CRrFX-yw.js +84 -0
- package/dist/ui/assets/rough.esm-By172zw-.js +1 -0
- package/dist/ui/assets/sankeyDiagram-P5KCCOFB-X_xFRj9E.js +40 -0
- package/dist/ui/assets/sequenceDiagram-WJ2MYXX4-Byb49lKf.js +162 -0
- package/dist/ui/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
- package/dist/ui/assets/src-BH-TyZbA.js +1 -0
- package/dist/ui/assets/stateDiagram-D77RDMKH-CzaPrqYF.js +1 -0
- package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-BCXckz7K.js +1 -0
- package/dist/ui/assets/swimlanes-42K2YHIH-Cx_1lnqr.js +1 -0
- package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-54qsKi6A.js +8 -0
- package/dist/ui/assets/timeline-definition-24CTP7MA-B1IbZPQ-.js +120 -0
- package/dist/ui/assets/treeView-Q6P3EWNA-DFWhVE3d.js +1 -0
- package/dist/ui/assets/treemap-WGGIJYW6-BmbYPThn.js +1 -0
- package/dist/ui/assets/vendor-ui-DPm7zLxi.js +3 -0
- package/dist/ui/assets/vendor-vue-CobgyEJp.js +1 -0
- package/dist/ui/assets/vennDiagram-4TSXK5OY-Ae2AtRpt.js +34 -0
- package/dist/ui/assets/wardley-WFR3VGLG-BtD4X4tL.js +1 -0
- package/dist/ui/assets/wardleyDiagram-VM6X3IG4-B3YN2fTd.js +78 -0
- package/dist/ui/assets/xychartDiagram-S5SC5T6Z-CIzJfMKA.js +7 -0
- package/dist/ui/favicon.svg +11 -0
- package/dist/ui/index.html +49 -0
- package/dist/ui/sw.js +48 -0
- package/package.json +91 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live index. Stage 1's indexer rebuilds everything from disk on demand;
|
|
3
|
+
* for a long-lived server that's wasteful and loses the "what changed" signal.
|
|
4
|
+
*
|
|
5
|
+
* LiveIndex holds the parsed tasks in memory, applies INCREMENTAL updates when
|
|
6
|
+
* individual files change, and emits typed events describing each change. It is
|
|
7
|
+
* still backed entirely by the files on disk — it is a cache with a heartbeat,
|
|
8
|
+
* never a source of truth. A full rebuild (`refreshAll`) is always available
|
|
9
|
+
* and always correct.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, existsSync, realpathSync } from "node:fs";
|
|
12
|
+
import { join, extname } from "node:path";
|
|
13
|
+
import { STATUSES, PRIORITIES } from "../core/types.js";
|
|
14
|
+
import { parseTask } from "../core/task.js";
|
|
15
|
+
import { isGitRepo, localBranches, lastCommitForFile, worktreeStatus, worktreePaths, worktreeList, currentBranch, emptyGitInfo, resolveWorktreeStatuses, } from "../core/git.js";
|
|
16
|
+
import { buildIndex, buildIndexAsync, readWorktreeDirtyCache, writeWorktreeDirtyCache, } from "../core/indexer.js";
|
|
17
|
+
import { patchTaskFile } from "./write.js";
|
|
18
|
+
function priorityRank(p) {
|
|
19
|
+
const i = PRIORITIES.indexOf(p);
|
|
20
|
+
return i === -1 ? PRIORITIES.length : i;
|
|
21
|
+
}
|
|
22
|
+
function statusRank(s) {
|
|
23
|
+
return STATUSES.indexOf(s);
|
|
24
|
+
}
|
|
25
|
+
export class LiveIndex {
|
|
26
|
+
config;
|
|
27
|
+
/** task id -> Task */
|
|
28
|
+
byId = new Map();
|
|
29
|
+
/** abs path -> task id, so a file change maps back to a task */
|
|
30
|
+
pathToId = new Map();
|
|
31
|
+
listeners = new Set();
|
|
32
|
+
useGit = false;
|
|
33
|
+
branchCache = new Set();
|
|
34
|
+
/**
|
|
35
|
+
* Guard invoked by `applyFileChange` for a transition INTO `review` that
|
|
36
|
+
* did not already pass through the trusted PATCH route. #0210: a direct
|
|
37
|
+
* task-file edit must be held to the same commit/vacuity gate as the trusted
|
|
38
|
+
* handoff path. Returns false to reject the transition (the file is reverted
|
|
39
|
+
* to its previous status); true/fire-and-forget to accept it.
|
|
40
|
+
*/
|
|
41
|
+
reviewGuard = null;
|
|
42
|
+
/** Wire the #0210 review-transition guard. The server owns the logic. */
|
|
43
|
+
setReviewGuard(fn) {
|
|
44
|
+
this.reviewGuard = fn;
|
|
45
|
+
}
|
|
46
|
+
constructor(config) {
|
|
47
|
+
this.config = config;
|
|
48
|
+
}
|
|
49
|
+
/** Build from scratch. Safe to call any time; always authoritative. */
|
|
50
|
+
refreshAll() {
|
|
51
|
+
const idx = buildIndex(this.config);
|
|
52
|
+
this.byId.clear();
|
|
53
|
+
this.pathToId.clear();
|
|
54
|
+
this.useGit = isGitRepo(this.config.root);
|
|
55
|
+
this.branchCache = this.useGit ? localBranches(this.config.root) : new Set();
|
|
56
|
+
for (const t of idx.tasks) {
|
|
57
|
+
this.byId.set(t.id, t);
|
|
58
|
+
this.pathToId.set(t.absPath, t.id);
|
|
59
|
+
}
|
|
60
|
+
this.emit({
|
|
61
|
+
type: "index.rebuilt",
|
|
62
|
+
taskCount: this.byId.size,
|
|
63
|
+
at: now(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Async counterpart of `refreshAll`, used only at server startup: it lets
|
|
68
|
+
* `listen()` proceed while the (git-heavy, otherwise multi-second with
|
|
69
|
+
* 200+ tasks) index build runs in the background — see `buildIndexAsync`.
|
|
70
|
+
* Not a drop-in replacement for `refreshAll` in general: a file-watcher
|
|
71
|
+
* update landing mid-build is overwritten by this call's final swap-in,
|
|
72
|
+
* same as it would be by a concurrent `refreshAll`, but this call's window
|
|
73
|
+
* is wider. Fine for the one-shot boot case; callers that need to
|
|
74
|
+
* interleave safely with live updates should keep using `refreshAll`.
|
|
75
|
+
*/
|
|
76
|
+
async refreshAllAsync() {
|
|
77
|
+
const idx = await buildIndexAsync(this.config, { fastWorktreeStatus: true });
|
|
78
|
+
this.byId.clear();
|
|
79
|
+
this.pathToId.clear();
|
|
80
|
+
this.useGit = isGitRepo(this.config.root);
|
|
81
|
+
this.branchCache = this.useGit ? localBranches(this.config.root) : new Set();
|
|
82
|
+
for (const t of idx.tasks) {
|
|
83
|
+
this.byId.set(t.id, t);
|
|
84
|
+
this.pathToId.set(t.absPath, t.id);
|
|
85
|
+
}
|
|
86
|
+
this.emit({
|
|
87
|
+
type: "index.rebuilt",
|
|
88
|
+
taskCount: this.byId.size,
|
|
89
|
+
at: now(),
|
|
90
|
+
});
|
|
91
|
+
// NOTE: `fastWorktreeStatus` skipped `git status`, so `git.dirty` reflects
|
|
92
|
+
// only each branch's ahead count until the server's post-listen
|
|
93
|
+
// `reconcileWorktreeStatus()` sweep fills in the working-tree state. That
|
|
94
|
+
// sweep is scheduled by the caller, deliberately NOT here — starting git
|
|
95
|
+
// subprocesses from inside the boot path is exactly what this async build
|
|
96
|
+
// exists to avoid.
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Refresh every task's worktree `dirty`/`worktreePath` with a real
|
|
100
|
+
* `git status` sweep (batched — see `resolveWorktreeStatuses`), persist the
|
|
101
|
+
* result, and emit `task.updated` for any that changed. Called after the
|
|
102
|
+
* fast boot build and safe to call periodically. No-op without git.
|
|
103
|
+
*/
|
|
104
|
+
async reconcileWorktreeStatus() {
|
|
105
|
+
if (!this.useGit)
|
|
106
|
+
return;
|
|
107
|
+
const baseBranch = currentBranch(this.config.root);
|
|
108
|
+
const branches = [...this.byId.values()].map((t) => t.branch).filter((b) => !!b);
|
|
109
|
+
const { statuses, cache, changed } = await resolveWorktreeStatuses(this.config.root, branches, {
|
|
110
|
+
baseBranch,
|
|
111
|
+
cache: readWorktreeDirtyCache(this.config),
|
|
112
|
+
});
|
|
113
|
+
if (changed)
|
|
114
|
+
writeWorktreeDirtyCache(this.config, cache);
|
|
115
|
+
for (const [id, t] of this.byId) {
|
|
116
|
+
if (!t.branch)
|
|
117
|
+
continue;
|
|
118
|
+
const wt = statuses.get(t.branch);
|
|
119
|
+
if (!wt)
|
|
120
|
+
continue;
|
|
121
|
+
if (t.git.dirty === wt.dirty && t.git.worktreePath === wt.path)
|
|
122
|
+
continue;
|
|
123
|
+
const next = {
|
|
124
|
+
...t,
|
|
125
|
+
git: { ...t.git, dirty: wt.dirty, worktreePath: wt.path },
|
|
126
|
+
};
|
|
127
|
+
this.byId.set(id, next);
|
|
128
|
+
this.emit({ type: "task.updated", task: next, prev: {}, at: now() });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
isTaskFile(absPath) {
|
|
132
|
+
const workRoot = join(this.config.root, this.config.workDir);
|
|
133
|
+
if (!absPath.startsWith(workRoot))
|
|
134
|
+
return false;
|
|
135
|
+
return this.config.taskExtensions.includes(extname(absPath));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Re-parse a single file after a create/modify. Emits created or updated.
|
|
139
|
+
*
|
|
140
|
+
* `opts.guarded` marks a change that already passed the #0210 review gate
|
|
141
|
+
* (the trusted PATCH route and the trusted handoff path run the gate
|
|
142
|
+
* themselves), so it is applied synchronously without re-invoking the guard.
|
|
143
|
+
* Unmarked transitions INTO `review` — a direct task-file edit picked up by
|
|
144
|
+
* the watcher — are deferred to the async guard, which reverts the file to
|
|
145
|
+
* its previous status if the transition is rejected. Returns a Promise for
|
|
146
|
+
* that deferred case so callers (and tests) can await its settlement.
|
|
147
|
+
*/
|
|
148
|
+
applyFileChange(absPath, opts = {}) {
|
|
149
|
+
if (!this.isTaskFile(absPath))
|
|
150
|
+
return;
|
|
151
|
+
if (!existsSync(absPath)) {
|
|
152
|
+
this.applyFileDelete(absPath);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let content;
|
|
156
|
+
try {
|
|
157
|
+
content = readFileSync(absPath, "utf8");
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return; // transient read error (editor mid-write); next event will catch it
|
|
161
|
+
}
|
|
162
|
+
const task = parseTask({
|
|
163
|
+
content,
|
|
164
|
+
absPath,
|
|
165
|
+
root: this.config.root,
|
|
166
|
+
defaultStatus: this.config.defaultStatus,
|
|
167
|
+
defaultAssignee: this.config.defaultAssignee,
|
|
168
|
+
git: emptyGitInfo(),
|
|
169
|
+
});
|
|
170
|
+
if (this.useGit) {
|
|
171
|
+
const { subject, date } = lastCommitForFile(this.config.root, task.path);
|
|
172
|
+
const wt = task.branch
|
|
173
|
+
? worktreeStatus(this.config.root, task.branch)
|
|
174
|
+
: { path: null, dirty: false };
|
|
175
|
+
task.git = {
|
|
176
|
+
branchExists: task.branch ? this.branchCache.has(task.branch) : false,
|
|
177
|
+
worktreeExists: task.branch ? worktreePaths(this.config.root).has(task.branch) : false,
|
|
178
|
+
lastCommit: subject,
|
|
179
|
+
lastCommitAt: date,
|
|
180
|
+
worktreePath: wt.path,
|
|
181
|
+
dirty: wt.dirty,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const existing = this.byId.get(task.id);
|
|
185
|
+
// If the id moved to a different file, treat the old path as stale.
|
|
186
|
+
const priorPath = [...this.pathToId.entries()].find(([, id]) => id === task.id)?.[0];
|
|
187
|
+
if (priorPath && priorPath !== absPath)
|
|
188
|
+
this.pathToId.delete(priorPath);
|
|
189
|
+
// #0210: a transition INTO `review` that did not already pass through the
|
|
190
|
+
// trusted PATCH/handoff paths must be held to the same commit/vacuity gate
|
|
191
|
+
// before the index reflects it. Defer so a rejected transition never
|
|
192
|
+
// surfaces as a review event (no misleading auto-review/preview launch).
|
|
193
|
+
if (!opts.guarded &&
|
|
194
|
+
this.reviewGuard &&
|
|
195
|
+
existing &&
|
|
196
|
+
existing.status !== "review" &&
|
|
197
|
+
task.status === "review") {
|
|
198
|
+
return this.runReviewGuard(absPath, task, existing);
|
|
199
|
+
}
|
|
200
|
+
this.applyParsed(absPath, task, existing);
|
|
201
|
+
}
|
|
202
|
+
/** Apply a fully-parsed task to the in-memory index and emit. */
|
|
203
|
+
applyParsed(absPath, task, existing) {
|
|
204
|
+
this.byId.set(task.id, task);
|
|
205
|
+
this.pathToId.set(absPath, task.id);
|
|
206
|
+
if (!existing) {
|
|
207
|
+
this.emit({ type: "task.created", task, at: now() });
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
const prev = diff(existing, task);
|
|
211
|
+
// Only emit if something user-visible actually changed.
|
|
212
|
+
if (Object.keys(prev).length > 0) {
|
|
213
|
+
this.emit({ type: "task.updated", task, prev, at: now() });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Run the #0210 review guard for a deferred transition into `review`. On
|
|
219
|
+
* acceptance the task is applied normally (the worktree was committed by the
|
|
220
|
+
* guard). On rejection the task file is reverted to its previous status —
|
|
221
|
+
* using the same safe write path as a server edit — so the bypass can never
|
|
222
|
+
* leave a task sitting in `review` with uncommitted work.
|
|
223
|
+
*/
|
|
224
|
+
async runReviewGuard(absPath, task, existing) {
|
|
225
|
+
if (!this.reviewGuard) {
|
|
226
|
+
this.applyParsed(absPath, task, existing);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
let allowed;
|
|
230
|
+
try {
|
|
231
|
+
allowed = await this.reviewGuard(task, existing);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
allowed = false; // a failing guard must not leak the transition through
|
|
235
|
+
}
|
|
236
|
+
if (!allowed) {
|
|
237
|
+
try {
|
|
238
|
+
patchTaskFile(this.config, absPath, { status: existing.status });
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// If the revert fails, reflect what git actually shows rather than the
|
|
242
|
+
// unvalidated review state.
|
|
243
|
+
}
|
|
244
|
+
// Re-read whatever is now on disk (reverted, or unchanged) and apply it.
|
|
245
|
+
if (!existsSync(absPath))
|
|
246
|
+
return;
|
|
247
|
+
this.applyFileChange(absPath, { guarded: true });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this.applyParsed(absPath, task, existing);
|
|
251
|
+
}
|
|
252
|
+
/** Handle a file removal. Emits deleted if it mapped to a known task. */
|
|
253
|
+
applyFileDelete(absPath) {
|
|
254
|
+
const id = this.pathToId.get(absPath);
|
|
255
|
+
if (!id)
|
|
256
|
+
return;
|
|
257
|
+
const task = this.byId.get(id);
|
|
258
|
+
this.pathToId.delete(absPath);
|
|
259
|
+
this.byId.delete(id);
|
|
260
|
+
this.emit({
|
|
261
|
+
type: "task.deleted",
|
|
262
|
+
id,
|
|
263
|
+
path: task?.path ?? absPath,
|
|
264
|
+
at: now(),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Re-scan local branches / worktrees and re-apply the cheap git facts
|
|
269
|
+
* (`branchExists`, `worktreeExists`, `worktreePath`) to every indexed task.
|
|
270
|
+
* Called after an agent launch, branch sync, or hotfix so the UI's branch/
|
|
271
|
+
* worktree dots are right without a full rebuild.
|
|
272
|
+
*
|
|
273
|
+
* `dirty` is NOT recomputed here — that needs a `git status` per worktree,
|
|
274
|
+
* which this method used to run synchronously for every task on the event
|
|
275
|
+
* loop (~2.5s at 40 worktrees, on every task mutation). It is delegated to
|
|
276
|
+
* the batched `reconcileWorktreeStatus()` sweep, kicked off below and
|
|
277
|
+
* arriving via `task.updated` within ~100ms.
|
|
278
|
+
*/
|
|
279
|
+
refreshBranches() {
|
|
280
|
+
this.useGit = isGitRepo(this.config.root);
|
|
281
|
+
this.branchCache = this.useGit ? localBranches(this.config.root) : new Set();
|
|
282
|
+
const worktrees = this.useGit
|
|
283
|
+
? worktreeList(this.config.root)
|
|
284
|
+
: new Map();
|
|
285
|
+
let realRoot = this.config.root;
|
|
286
|
+
try {
|
|
287
|
+
realRoot = realpathSync(this.config.root);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
/* keep the configured root */
|
|
291
|
+
}
|
|
292
|
+
for (const [id, t] of this.byId) {
|
|
293
|
+
let worktreePath = t.branch ? (worktrees.get(t.branch)?.path ?? null) : null;
|
|
294
|
+
if (worktreePath) {
|
|
295
|
+
let rp = worktreePath;
|
|
296
|
+
try {
|
|
297
|
+
rp = realpathSync(worktreePath);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
/* keep the reported path */
|
|
301
|
+
}
|
|
302
|
+
// git lists the main checkout as a worktree too; it is never a task one.
|
|
303
|
+
if (rp === realRoot)
|
|
304
|
+
worktreePath = null;
|
|
305
|
+
}
|
|
306
|
+
this.byId.set(id, {
|
|
307
|
+
...t,
|
|
308
|
+
git: {
|
|
309
|
+
...t.git,
|
|
310
|
+
branchExists: t.branch ? this.branchCache.has(t.branch) : false,
|
|
311
|
+
worktreeExists: t.branch ? worktrees.has(t.branch) : false,
|
|
312
|
+
worktreePath,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
void this.reconcileWorktreeStatus();
|
|
317
|
+
}
|
|
318
|
+
// ---- reads ----
|
|
319
|
+
getTasks(status) {
|
|
320
|
+
const all = [...this.byId.values()];
|
|
321
|
+
const filtered = status ? all.filter((t) => t.status === status) : all;
|
|
322
|
+
return filtered.sort((a, b) => {
|
|
323
|
+
const s = statusRank(a.status) - statusRank(b.status);
|
|
324
|
+
if (s !== 0)
|
|
325
|
+
return s;
|
|
326
|
+
const p = priorityRank(a.priority) - priorityRank(b.priority);
|
|
327
|
+
if (p !== 0)
|
|
328
|
+
return p;
|
|
329
|
+
return a.id.localeCompare(b.id);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
getTask(id) {
|
|
333
|
+
return this.byId.get(id) ?? null;
|
|
334
|
+
}
|
|
335
|
+
counts() {
|
|
336
|
+
const c = Object.fromEntries(STATUSES.map((s) => [s, 0]));
|
|
337
|
+
for (const t of this.byId.values())
|
|
338
|
+
c[t.status]++;
|
|
339
|
+
return c;
|
|
340
|
+
}
|
|
341
|
+
snapshot() {
|
|
342
|
+
const tasks = this.getTasks();
|
|
343
|
+
return {
|
|
344
|
+
version: 1,
|
|
345
|
+
generatedAt: now(),
|
|
346
|
+
root: this.config.root,
|
|
347
|
+
taskCount: tasks.length,
|
|
348
|
+
tasks,
|
|
349
|
+
counts: this.counts(),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/** Lightweight board snapshot — skips body, extra, agent overrides. */
|
|
353
|
+
boardSnapshot() {
|
|
354
|
+
const tasks = this.getTasks();
|
|
355
|
+
return {
|
|
356
|
+
version: 1,
|
|
357
|
+
generatedAt: now(),
|
|
358
|
+
root: this.config.root,
|
|
359
|
+
taskCount: tasks.length,
|
|
360
|
+
tasks: tasks.map(toBoardTask),
|
|
361
|
+
counts: this.counts(),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
// ---- events ----
|
|
365
|
+
on(fn) {
|
|
366
|
+
this.listeners.add(fn);
|
|
367
|
+
return () => this.listeners.delete(fn);
|
|
368
|
+
}
|
|
369
|
+
emit(e) {
|
|
370
|
+
for (const fn of this.listeners) {
|
|
371
|
+
try {
|
|
372
|
+
fn(e);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
/* a bad listener must not break the index */
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function now() {
|
|
381
|
+
return new Date().toISOString();
|
|
382
|
+
}
|
|
383
|
+
/** Strip a Task down to what the board renders. */
|
|
384
|
+
function toBoardTask(t) {
|
|
385
|
+
return {
|
|
386
|
+
id: t.id,
|
|
387
|
+
title: t.title,
|
|
388
|
+
type: t.type,
|
|
389
|
+
status: t.status,
|
|
390
|
+
needsInput: t.needsInput,
|
|
391
|
+
needsInputReason: t.needsInputReason,
|
|
392
|
+
needsMerge: t.needsMerge,
|
|
393
|
+
priority: t.priority,
|
|
394
|
+
area: t.area,
|
|
395
|
+
assignee: t.assignee,
|
|
396
|
+
assignedTo: t.assignedTo,
|
|
397
|
+
createdBy: t.createdBy,
|
|
398
|
+
branch: t.branch,
|
|
399
|
+
tags: t.tags,
|
|
400
|
+
created_at: t.created_at,
|
|
401
|
+
updated_at: t.updated_at,
|
|
402
|
+
releasedAt: t.releasedAt ?? null,
|
|
403
|
+
bodyPreview: t.body?.slice(0, 500) ?? "",
|
|
404
|
+
path: t.path,
|
|
405
|
+
absPath: t.absPath,
|
|
406
|
+
git: t.git,
|
|
407
|
+
preview: null,
|
|
408
|
+
checkRetryCount: typeof t.extra?.check_retry_count === "number" ? t.extra.check_retry_count : 0,
|
|
409
|
+
mergeConflictRetryCount: typeof t.extra?.merge_conflict_retry_count === "number"
|
|
410
|
+
? t.extra.merge_conflict_retry_count
|
|
411
|
+
: 0,
|
|
412
|
+
handoffSignalRetryCount: typeof t.extra?.handoff_signal_retry_count === "number"
|
|
413
|
+
? t.extra.handoff_signal_retry_count
|
|
414
|
+
: 0,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/** Shallow diff of the fields we care about, for the `prev` payload. */
|
|
418
|
+
function diff(a, b) {
|
|
419
|
+
const fields = [
|
|
420
|
+
"title",
|
|
421
|
+
"status",
|
|
422
|
+
"priority",
|
|
423
|
+
"area",
|
|
424
|
+
"assignee",
|
|
425
|
+
"assignedTo",
|
|
426
|
+
"branch",
|
|
427
|
+
"type",
|
|
428
|
+
"needsInput",
|
|
429
|
+
"needsInputReason",
|
|
430
|
+
"needsMerge",
|
|
431
|
+
"agentOverride",
|
|
432
|
+
"cliOverride",
|
|
433
|
+
"modelOverride",
|
|
434
|
+
"pmAgentOverride",
|
|
435
|
+
"pmCliOverride",
|
|
436
|
+
"pmModelOverride",
|
|
437
|
+
"reviewAgentOverride",
|
|
438
|
+
"reviewCliOverride",
|
|
439
|
+
"reviewModelOverride",
|
|
440
|
+
"created_at",
|
|
441
|
+
"updated_at",
|
|
442
|
+
];
|
|
443
|
+
const out = {};
|
|
444
|
+
for (const f of fields) {
|
|
445
|
+
if (a[f] !== b[f])
|
|
446
|
+
out[f] = a[f];
|
|
447
|
+
}
|
|
448
|
+
// body change is common and worth signalling, but don't ship the whole body
|
|
449
|
+
if (a.body !== b.body)
|
|
450
|
+
out.body = "(changed)";
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ModelSourceResult } from "../core/models.js";
|
|
2
|
+
export declare const MODEL_TEST_SENTINEL = "REPOOS_MODEL_OK";
|
|
3
|
+
export declare const MODEL_TEST_TIMEOUT_MS = 8000;
|
|
4
|
+
export type ModelTestStatus = "passed" | "failed" | "timed_out" | "not_testable";
|
|
5
|
+
export interface ModelTestResult {
|
|
6
|
+
cli: string;
|
|
7
|
+
model: string;
|
|
8
|
+
status: ModelTestStatus;
|
|
9
|
+
durationMs: number;
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ModelTestOptions {
|
|
13
|
+
cwd: string;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
concurrency?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function sanitizeDiagnostic(text: string): string;
|
|
18
|
+
export declare function testModelCombination(cli: string, model: string, opts: ModelTestOptions): Promise<ModelTestResult>;
|
|
19
|
+
/** Test supported per-CLI model sources with a bounded worker pool. */
|
|
20
|
+
export declare function testModelCombinations(byCli: Record<string, ModelSourceResult>, opts: ModelTestOptions): Promise<ModelTestResult[]>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { promptCommand } from "./agents.js";
|
|
3
|
+
export const MODEL_TEST_SENTINEL = "REPOOS_MODEL_OK";
|
|
4
|
+
export const MODEL_TEST_TIMEOUT_MS = 8_000;
|
|
5
|
+
const OUTPUT_LIMIT = 4 * 1024;
|
|
6
|
+
export function sanitizeDiagnostic(text) {
|
|
7
|
+
return text
|
|
8
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
9
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "")
|
|
10
|
+
.trim()
|
|
11
|
+
.slice(0, OUTPUT_LIMIT);
|
|
12
|
+
}
|
|
13
|
+
export function testModelCombination(cli, model, opts) {
|
|
14
|
+
const started = Date.now();
|
|
15
|
+
const finish = (status, error) => ({
|
|
16
|
+
cli,
|
|
17
|
+
model,
|
|
18
|
+
status,
|
|
19
|
+
durationMs: Date.now() - started,
|
|
20
|
+
...(error ? { error: sanitizeDiagnostic(error) } : {}),
|
|
21
|
+
});
|
|
22
|
+
const agent = { name: "model-test", cli, model, enabled: true };
|
|
23
|
+
const { cmd, args } = promptCommand(agent, `Reply with exactly ${MODEL_TEST_SENTINEL}. Do not use tools or modify files.`);
|
|
24
|
+
// Compatibility probes may run from a configured repo root that Codex has
|
|
25
|
+
// not marked trusted yet. The prompt cannot modify files, so bypass only the
|
|
26
|
+
// repository trust preflight for this disposable probe.
|
|
27
|
+
if (cli === "codex")
|
|
28
|
+
args.splice(1, 0, "--skip-git-repo-check");
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
let proc;
|
|
31
|
+
try {
|
|
32
|
+
proc = spawn(cmd, args, { cwd: opts.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
resolve(finish("failed", err instanceof Error ? err.message : String(err)));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
let stdout = "";
|
|
39
|
+
let stderr = "";
|
|
40
|
+
let settled = false;
|
|
41
|
+
const done = (result) => {
|
|
42
|
+
if (settled)
|
|
43
|
+
return;
|
|
44
|
+
settled = true;
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
resolve(result);
|
|
47
|
+
};
|
|
48
|
+
const timer = setTimeout(() => {
|
|
49
|
+
try {
|
|
50
|
+
proc.kill("SIGKILL");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* already exited */
|
|
54
|
+
}
|
|
55
|
+
done(finish("timed_out", `Timed out after ${opts.timeoutMs ?? MODEL_TEST_TIMEOUT_MS}ms`));
|
|
56
|
+
}, opts.timeoutMs ?? MODEL_TEST_TIMEOUT_MS);
|
|
57
|
+
proc.stdout?.on("data", (chunk) => {
|
|
58
|
+
if (stdout.length < OUTPUT_LIMIT)
|
|
59
|
+
stdout += chunk.toString("utf8");
|
|
60
|
+
});
|
|
61
|
+
proc.stderr?.on("data", (chunk) => {
|
|
62
|
+
if (stderr.length < OUTPUT_LIMIT)
|
|
63
|
+
stderr += chunk.toString("utf8");
|
|
64
|
+
});
|
|
65
|
+
proc.on("error", (err) => done(finish("failed", `Could not launch ${cmd}: ${err.message}`)));
|
|
66
|
+
proc.on("close", (code) => {
|
|
67
|
+
if (code === 0 && sanitizeDiagnostic(stdout).includes(MODEL_TEST_SENTINEL)) {
|
|
68
|
+
done(finish("passed"));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const detail = stderr || stdout || `${cmd} exited with code ${code ?? "unknown"}`;
|
|
72
|
+
done(finish("failed", detail));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Test supported per-CLI model sources with a bounded worker pool. */
|
|
78
|
+
export async function testModelCombinations(byCli, opts) {
|
|
79
|
+
const queued = [];
|
|
80
|
+
const results = [];
|
|
81
|
+
for (const [cli, source] of Object.entries(byCli)) {
|
|
82
|
+
const models = [...new Set(source.models.length ? source.models : ["default"])];
|
|
83
|
+
if (!source.supported) {
|
|
84
|
+
for (const model of models)
|
|
85
|
+
results.push({ cli, model, status: "not_testable", durationMs: 0 });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
for (const model of models)
|
|
89
|
+
queued.push({ cli, model });
|
|
90
|
+
}
|
|
91
|
+
let next = 0;
|
|
92
|
+
const workers = Array.from({ length: Math.min(Math.max(1, opts.concurrency ?? 2), queued.length) }, async () => {
|
|
93
|
+
while (next < queued.length) {
|
|
94
|
+
const item = queued[next++];
|
|
95
|
+
results.push(await testModelCombination(item.cli, item.model, opts));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
await Promise.all(workers);
|
|
99
|
+
return results.sort((a, b) => a.cli.localeCompare(b.cli) || a.model.localeCompare(b.model));
|
|
100
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional ntfy push notifications for task lifecycle events (#0161).
|
|
3
|
+
*
|
|
4
|
+
* Redesigned to be scannable one-line notifications using the grammar:
|
|
5
|
+
* [emoji] [short human event] · [task title]
|
|
6
|
+
*
|
|
7
|
+
* Fire-and-forget HTTP POSTs to a ntfy topic. Best-effort by design: a network
|
|
8
|
+
* error, bad topic, or offline server must never block or fail the status
|
|
9
|
+
* transition that triggered the notification — callers just call
|
|
10
|
+
* `notifyStatusChange` and ignore the result.
|
|
11
|
+
*
|
|
12
|
+
* No runtime dependencies: publishing uses the platform's native `fetch`
|
|
13
|
+
* (Node >= 18 / Bun). If the topic is empty or the toggle is off, nothing is
|
|
14
|
+
* ever sent.
|
|
15
|
+
*/
|
|
16
|
+
import type { RepoOSConfig, Status, Task } from "../core/types.js";
|
|
17
|
+
type NotificationPriority = "min" | "low" | "default" | "high" | "max";
|
|
18
|
+
interface NotificationSpec {
|
|
19
|
+
/** Emoji + short verb, e.g. "▶️ Started" */
|
|
20
|
+
headline: string;
|
|
21
|
+
/** ntfy priority header: "Needs you", "Blocked", "Failed" = high/max; "Done", "Started" = low */
|
|
22
|
+
priority: NotificationPriority;
|
|
23
|
+
/** Optional second line for "Needs you" notifications, e.g. "Agent is waiting for your decision" */
|
|
24
|
+
subtitle?: string;
|
|
25
|
+
}
|
|
26
|
+
/** The ntfy server base URL: env var wins, then config, then the default. */
|
|
27
|
+
export declare function ntfyBaseUrl(config: RepoOSConfig): string;
|
|
28
|
+
/**
|
|
29
|
+
* Build a notification spec for a status transition, or null if no notification applies.
|
|
30
|
+
* For ready → active, returns "Started" (lower priority to avoid noise).
|
|
31
|
+
* For active/review → done, returns "Done" (lower priority).
|
|
32
|
+
* Other transitions do not currently trigger notifications.
|
|
33
|
+
*/
|
|
34
|
+
export declare function notificationForStatusChange(prev: Status, next: Status): NotificationSpec | null;
|
|
35
|
+
/**
|
|
36
|
+
* Build a notification spec for task creation.
|
|
37
|
+
* Returns "New" with low priority (informational).
|
|
38
|
+
*/
|
|
39
|
+
export declare function notificationForTaskCreated(): NotificationSpec;
|
|
40
|
+
/**
|
|
41
|
+
* Build a notification spec when a task needs human input.
|
|
42
|
+
* Returns "Needs you" with high priority and a subtitle explaining the situation.
|
|
43
|
+
*/
|
|
44
|
+
export declare function notificationForNeedsInput(): NotificationSpec;
|
|
45
|
+
/** Format a notification spec into the final message string. */
|
|
46
|
+
export declare function formatNotification(spec: NotificationSpec, title: string): string;
|
|
47
|
+
/** True when notifications are enabled AND a topic is configured. */
|
|
48
|
+
export declare function shouldSend(config: RepoOSConfig): boolean;
|
|
49
|
+
/** Best-effort publish of a message to the configured topic. Never throws. */
|
|
50
|
+
export declare function publish(config: RepoOSConfig, message: string, priority?: NotificationPriority): void;
|
|
51
|
+
/** Fire a notification for a task status transition, if one applies. */
|
|
52
|
+
export declare function notifyStatusChange(config: RepoOSConfig, task: Task, prev: Status, next: Status): void;
|
|
53
|
+
/** Fire a notification when a task is created (stretch from #0134). */
|
|
54
|
+
export declare function notifyTaskCreated(config: RepoOSConfig, task: Task): void;
|
|
55
|
+
/** Fire a notification when a task needs human input. */
|
|
56
|
+
export declare function notifyNeedsInput(config: RepoOSConfig, task: Task): void;
|
|
57
|
+
export {};
|