specrails-desktop 2.0.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/.claude/commands/specrails/batch-implement.md +287 -0
- package/.claude/commands/specrails/compat-check.md +271 -0
- package/.claude/commands/specrails/doctor.md +62 -0
- package/.claude/commands/specrails/enrich.md +1635 -0
- package/.claude/commands/specrails/explore-spec.md +173 -0
- package/.claude/commands/specrails/health-check.md +527 -0
- package/.claude/commands/specrails/implement.md +1457 -0
- package/.claude/commands/specrails/memory-inspect.md +259 -0
- package/.claude/commands/specrails/opsx-diff.md +419 -0
- package/.claude/commands/specrails/propose-spec.md +102 -0
- package/.claude/commands/specrails/reconfig.md +89 -0
- package/.claude/commands/specrails/refactor-recommender.md +212 -0
- package/.claude/commands/specrails/retry.md +363 -0
- package/.claude/commands/specrails/telemetry.md +552 -0
- package/.claude/commands/specrails/why.md +96 -0
- package/LICENSE +21 -0
- package/README.md +290 -0
- package/cli/dist/specrails-desktop.js +1098 -0
- package/client/dist/assets/ActivityFeedPage-Gy4x8dBt.js +1 -0
- package/client/dist/assets/AgentsPage-CPgu--Fb.js +86 -0
- package/client/dist/assets/AnalyticsPage-B5sJEee2.js +1 -0
- package/client/dist/assets/BarChart-7IMQ8HY1.js +33 -0
- package/client/dist/assets/CodePage-CBdFvbwe.js +2 -0
- package/client/dist/assets/DesktopAnalyticsPage-w0rdTq4w.js +1 -0
- package/client/dist/assets/DocsDialog-BZUYM7wm.js +11 -0
- package/client/dist/assets/DocsPage-9QglWl46.js +11 -0
- package/client/dist/assets/ExportDropdown-BLZFXtNi.js +1 -0
- package/client/dist/assets/IntegrationsPage-BxBE4y99.js +3 -0
- package/client/dist/assets/JobDetailPage-DydWx_5S.js +16 -0
- package/client/dist/assets/JobsPage-20ibw0IO.js +1 -0
- package/client/dist/assets/abap-Bw6f2wDG.js +1 -0
- package/client/dist/assets/activity-BEIp_Y1A.js +1 -0
- package/client/dist/assets/activity-BdrPln96.js +1 -0
- package/client/dist/assets/activity-CpkRS8Sx.js +1 -0
- package/client/dist/assets/activity-DKCpESPt.js +1 -0
- package/client/dist/assets/activity-DOUVEjJi.js +1 -0
- package/client/dist/assets/activity-DRwkql_y.js +1 -0
- package/client/dist/assets/activity-DcDQ7tjw.js +1 -0
- package/client/dist/assets/activity-Dv6H7wEr.js +1 -0
- package/client/dist/assets/addon-image-3WCl5Vhd.js +1 -0
- package/client/dist/assets/addon-ligatures-C5OdliKs.js +2 -0
- package/client/dist/assets/addon-webgl-BbX6pSjl.js +44 -0
- package/client/dist/assets/addspec-B5yl4Loj.js +1 -0
- package/client/dist/assets/addspec-BEeF5-zc.js +1 -0
- package/client/dist/assets/addspec-D33ocMxf.js +1 -0
- package/client/dist/assets/addspec-DFswZ0jK.js +1 -0
- package/client/dist/assets/addspec-DRE-jZv7.js +1 -0
- package/client/dist/assets/addspec-DVZ15Jp8.js +1 -0
- package/client/dist/assets/addspec-Fkv91Opc.js +1 -0
- package/client/dist/assets/addspec-GWm4ffKl.js +1 -0
- package/client/dist/assets/agents-1nCDWRmP.js +1 -0
- package/client/dist/assets/agents-Bm9rPqnt.js +1 -0
- package/client/dist/assets/agents-CMxtJMLD.js +1 -0
- package/client/dist/assets/agents-DK-Dlc0i.js +1 -0
- package/client/dist/assets/agents-Q6Ldfpxx.js +1 -0
- package/client/dist/assets/agents-TeOSy-ax.js +1 -0
- package/client/dist/assets/agents-iTqjRajS.js +1 -0
- package/client/dist/assets/agents-s87sMGzL.js +1 -0
- package/client/dist/assets/agentstudio-B6Wb59E7.js +1 -0
- package/client/dist/assets/agentstudio-BADhZ41e.js +1 -0
- package/client/dist/assets/agentstudio-BSnWLR63.js +1 -0
- package/client/dist/assets/agentstudio-BdidyBzZ.js +1 -0
- package/client/dist/assets/agentstudio-CxlUllqI.js +1 -0
- package/client/dist/assets/agentstudio-D3I62TLJ.js +1 -0
- package/client/dist/assets/agentstudio-DuH9TogZ.js +1 -0
- package/client/dist/assets/agentstudio-Kw88_dUF.js +1 -0
- package/client/dist/assets/aiedit-BWxHGsYA.js +1 -0
- package/client/dist/assets/aiedit-D2ji6Qy0.js +1 -0
- package/client/dist/assets/aiedit-DAhZTvtk.js +1 -0
- package/client/dist/assets/aiedit-DJMny-D5.js +1 -0
- package/client/dist/assets/aiedit-DOcxERkU.js +1 -0
- package/client/dist/assets/aiedit-DvrcbwGv.js +1 -0
- package/client/dist/assets/aiedit-TTwzL1TS.js +1 -0
- package/client/dist/assets/aiedit-WBSjT_C1.js +1 -0
- package/client/dist/assets/analytics-BIdr0YfL.js +1 -0
- package/client/dist/assets/analytics-C6EzgtdE.js +1 -0
- package/client/dist/assets/analytics-C9Zc-rkM.js +1 -0
- package/client/dist/assets/analytics-CVx3YOc0.js +1 -0
- package/client/dist/assets/analytics-CYj0tfj7.js +1 -0
- package/client/dist/assets/analytics-CnY4kNG3.js +1 -0
- package/client/dist/assets/analytics-CrPCZRJ-.js +1 -0
- package/client/dist/assets/analytics-DMCto-TF.js +1 -0
- package/client/dist/assets/apex-Cw8_REBo.js +1 -0
- package/client/dist/assets/atom-one-dark-B-oHczHB.css +1 -0
- package/client/dist/assets/attachments-BIsSSnHJ.js +1 -0
- package/client/dist/assets/attachments-BW4L3l2L.js +1 -0
- package/client/dist/assets/attachments-Bcf6BG6V.js +1 -0
- package/client/dist/assets/attachments-Bke8sCU4.js +1 -0
- package/client/dist/assets/attachments-COcrGRFz.js +1 -0
- package/client/dist/assets/attachments-DYHGA2Dj.js +1 -0
- package/client/dist/assets/attachments-Dd92KpUH.js +1 -0
- package/client/dist/assets/attachments-DzdU6DV6.js +1 -0
- package/client/dist/assets/azcli-Cz6HAoOw.js +1 -0
- package/client/dist/assets/bat-CcJ-xyqL.js +1 -0
- package/client/dist/assets/bicep-z1WDCKYz.js +2 -0
- package/client/dist/assets/browser-5ErDlJoR.js +1 -0
- package/client/dist/assets/browser-Bc-YdlVg.js +1 -0
- package/client/dist/assets/browser-BlYF4OOq.js +1 -0
- package/client/dist/assets/browser-CT-ReZGt.js +1 -0
- package/client/dist/assets/browser-DGITz3fC.js +1 -0
- package/client/dist/assets/browser-JsAIGCEW.js +1 -0
- package/client/dist/assets/browser-M5-rbPlw.js +1 -0
- package/client/dist/assets/browser-Qya9cARy.js +1 -0
- package/client/dist/assets/cameligo-BRewOpfa.js +1 -0
- package/client/dist/assets/chat-BEGuC03z.js +1 -0
- package/client/dist/assets/chat-BEW60P_u.js +1 -0
- package/client/dist/assets/chat-BQNMD0PL.js +1 -0
- package/client/dist/assets/chat-BsbNGPW9.js +1 -0
- package/client/dist/assets/chat-CboQguCi.js +1 -0
- package/client/dist/assets/chat-DRCa9pOt.js +1 -0
- package/client/dist/assets/chat-DwUm6W9z.js +1 -0
- package/client/dist/assets/chat-yoXwguQu.js +1 -0
- package/client/dist/assets/chunk-CilyBKbf.js +1 -0
- package/client/dist/assets/clojure-DBjRWN6g.js +1 -0
- package/client/dist/assets/clsx-DnqN-uhr.js +1 -0
- package/client/dist/assets/code-AL1rVIMb.js +1 -0
- package/client/dist/assets/code-C0BKpkht.js +1 -0
- package/client/dist/assets/code-C0FTS3ew.js +1 -0
- package/client/dist/assets/code-CPcHxzxw.js +1 -0
- package/client/dist/assets/code-D3ryDniw.js +1 -0
- package/client/dist/assets/code-D3zVVQTj.js +1 -0
- package/client/dist/assets/code-PCmfS3dn.js +1 -0
- package/client/dist/assets/code-exI0G5Wd.js +1 -0
- package/client/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/client/dist/assets/coffee-Cfk_XHGR.js +1 -0
- package/client/dist/assets/commands-B772IyDa.js +1 -0
- package/client/dist/assets/commands-BDDp6xFG.js +1 -0
- package/client/dist/assets/commands-CJxCry-o.js +1 -0
- package/client/dist/assets/commands-CfgY-_of.js +1 -0
- package/client/dist/assets/commands-DLrvnPNg.js +1 -0
- package/client/dist/assets/commands-IXMOKBYt.js +1 -0
- package/client/dist/assets/commands-UD1NzmwX.js +1 -0
- package/client/dist/assets/commands-sqrqsxyE.js +1 -0
- package/client/dist/assets/common-DCr6VzJ7.js +1 -0
- package/client/dist/assets/common-Dard9UNH.js +1 -0
- package/client/dist/assets/common-DeDELLZJ.js +1 -0
- package/client/dist/assets/common-DltqHaAe.js +1 -0
- package/client/dist/assets/common-Dmm1GhdD.js +1 -0
- package/client/dist/assets/common-DnjcgkPH.js +1 -0
- package/client/dist/assets/common-GbpxfPG8.js +1 -0
- package/client/dist/assets/common-wA36jmj1.js +1 -0
- package/client/dist/assets/cpp-BVob6BaP.js +1 -0
- package/client/dist/assets/csharp-C4fbRuOu.js +1 -0
- package/client/dist/assets/csp-DthFP_vT.js +1 -0
- package/client/dist/assets/css-CGMH0hcW.js +3 -0
- package/client/dist/assets/css.worker-Wv5dxAWO.js +89 -0
- package/client/dist/assets/cssMode-Cc6ozl-J.js +1 -0
- package/client/dist/assets/cypher-Pnf68BRV.js +1 -0
- package/client/dist/assets/dart-PMMOtxZX.js +1 -0
- package/client/dist/assets/dashboard-B4ixDVk8.js +1 -0
- package/client/dist/assets/dashboard-BZBADHSj.js +1 -0
- package/client/dist/assets/dashboard-C1MfeUHs.js +1 -0
- package/client/dist/assets/dashboard-C7SK6xu5.js +1 -0
- package/client/dist/assets/dashboard-CB6Le1yN.js +1 -0
- package/client/dist/assets/dashboard-CoTpMOBM.js +1 -0
- package/client/dist/assets/dashboard-Duo4DDCW.js +1 -0
- package/client/dist/assets/dashboard-I19DXBxw.js +1 -0
- package/client/dist/assets/dist-js-BY-Fv_fg.js +1 -0
- package/client/dist/assets/dist-js-Bakc4uxT.js +1 -0
- package/client/dist/assets/dockerfile-di1nsJCc.js +1 -0
- package/client/dist/assets/ecl-D_WVcB5M.js +1 -0
- package/client/dist/assets/editor-Br_kD0ds.css +1 -0
- package/client/dist/assets/editor.api2-XLGzZfbc.js +872 -0
- package/client/dist/assets/editor.main-CfXxHimg.js +6 -0
- package/client/dist/assets/editor.worker-Bd9IXS8d.js +26 -0
- package/client/dist/assets/elixir-OAdJEMOn.js +1 -0
- package/client/dist/assets/explore-4mFpnrKU.js +1 -0
- package/client/dist/assets/explore-A8Ltoblq.js +1 -0
- package/client/dist/assets/explore-B9A3iN2W.js +1 -0
- package/client/dist/assets/explore-BV5Xxlsn.js +1 -0
- package/client/dist/assets/explore-BrBJvfjP.js +1 -0
- package/client/dist/assets/explore-C3FSE42C.js +1 -0
- package/client/dist/assets/explore-D2EFgt8J.js +1 -0
- package/client/dist/assets/explore-hFc3HFcp.js +1 -0
- package/client/dist/assets/flow9-D3QEZjgn.js +1 -0
- package/client/dist/assets/format-command-CwGuwzGA.js +1 -0
- package/client/dist/assets/freemarker2-DP7J1gG3.js +3 -0
- package/client/dist/assets/fsharp-BF0k_8N8.js +1 -0
- package/client/dist/assets/go-BAQO5Jsz.js +1 -0
- package/client/dist/assets/graphql-hdFVFkiV.js +1 -0
- package/client/dist/assets/handlebars-BjRlucw6.js +1 -0
- package/client/dist/assets/hcl-DWnl1o-X.js +1 -0
- package/client/dist/assets/html-OumBQJ-U.js +1 -0
- package/client/dist/assets/html.worker-CQP8QQsS.js +502 -0
- package/client/dist/assets/htmlMode-CStc3zXM.js +1 -0
- package/client/dist/assets/index-CimDRRi7.css +2 -0
- package/client/dist/assets/index-XGZaKl_u.js +142 -0
- package/client/dist/assets/ini-CB-6OVu3.js +1 -0
- package/client/dist/assets/integrations-C3p12Ms6.js +1 -0
- package/client/dist/assets/integrations-Cr6hH7XR.js +1 -0
- package/client/dist/assets/integrations-Cublz3m6.js +1 -0
- package/client/dist/assets/integrations-D28q1kF6.js +1 -0
- package/client/dist/assets/integrations-DRdbki5W.js +1 -0
- package/client/dist/assets/integrations-DaC4SzzL.js +1 -0
- package/client/dist/assets/integrations-DmQYCUvN.js +1 -0
- package/client/dist/assets/integrations-HIlUxXVs.js +1 -0
- package/client/dist/assets/java-d1CmfiHX.js +1 -0
- package/client/dist/assets/javascript-CMk--e7g.js +1 -0
- package/client/dist/assets/jobs-BE1siB0M.js +1 -0
- package/client/dist/assets/jobs-BHcQ_Faf.js +1 -0
- package/client/dist/assets/jobs-CFfc2dNX.js +1 -0
- package/client/dist/assets/jobs-CSi5n8X_.js +1 -0
- package/client/dist/assets/jobs-Dc3X86PY.js +1 -0
- package/client/dist/assets/jobs-De5tASex.js +1 -0
- package/client/dist/assets/jobs-DsoXEdo7.js +1 -0
- package/client/dist/assets/jobs-Wl-ApPMb.js +1 -0
- package/client/dist/assets/json.worker-DzV-CpCQ.js +58 -0
- package/client/dist/assets/jsonMode-C2h3ZcjZ.js +7 -0
- package/client/dist/assets/julia-Bgv08lKa.js +1 -0
- package/client/dist/assets/kotlin-u98kaVTf.js +1 -0
- package/client/dist/assets/less-CjYwpgg5.js +2 -0
- package/client/dist/assets/lexon-YTjaAFBB.js +1 -0
- package/client/dist/assets/lib-CPxTMOAq.js +7 -0
- package/client/dist/assets/liquid-mI3KJrBE.js +1 -0
- package/client/dist/assets/lspLanguageFeatures-DU09ggWi.js +4 -0
- package/client/dist/assets/lua-BzmkWv27.js +1 -0
- package/client/dist/assets/m3-CFwk9fw0.js +1 -0
- package/client/dist/assets/markdown-CR5iMpSZ.js +1 -0
- package/client/dist/assets/mdx-C41VDTR_.js +1 -0
- package/client/dist/assets/mips-CcEalc17.js +1 -0
- package/client/dist/assets/monaco.contribution-CPObAXMC.js +2 -0
- package/client/dist/assets/msdax-BQbkawnr.js +1 -0
- package/client/dist/assets/mysql-GTlaaW_P.js +1 -0
- package/client/dist/assets/nav-0fwkrgHt.js +1 -0
- package/client/dist/assets/nav-BEL3MTwK.js +1 -0
- package/client/dist/assets/nav-B_G-TJDW.js +1 -0
- package/client/dist/assets/nav-C2YXcbZS.js +1 -0
- package/client/dist/assets/nav-ClzOE4mA.js +1 -0
- package/client/dist/assets/nav-CtYwmMgu.js +1 -0
- package/client/dist/assets/nav-D2bOGSEg.js +1 -0
- package/client/dist/assets/nav-iH1V5j6o.js +1 -0
- package/client/dist/assets/objective-c-Byu1T5if.js +1 -0
- package/client/dist/assets/pascal-BrfzBfRm.js +1 -0
- package/client/dist/assets/pascaligo-BXXKFUeo.js +1 -0
- package/client/dist/assets/perl-B3OikKq-.js +1 -0
- package/client/dist/assets/pgsql-CTsa0Acc.js +1 -0
- package/client/dist/assets/php-DiQh3FUW.js +1 -0
- package/client/dist/assets/pla-92uH8Fzm.js +1 -0
- package/client/dist/assets/postiats-BbeWkKUr.js +1 -0
- package/client/dist/assets/powerquery-DgDMzpsm.js +1 -0
- package/client/dist/assets/powershell-BfdUUzaG.js +1 -0
- package/client/dist/assets/preload-helper-DSXbuxSR.js +1 -0
- package/client/dist/assets/protobuf-BojW2ftW.js +2 -0
- package/client/dist/assets/pug-BxqTg3IU.js +1 -0
- package/client/dist/assets/python-Y27rKQtk.js +1 -0
- package/client/dist/assets/qsharp-BX_A-MW9.js +1 -0
- package/client/dist/assets/r-D9BMnxvJ.js +1 -0
- package/client/dist/assets/razor-Cd5-q9Bp.js +1 -0
- package/client/dist/assets/redis-5cJqEQJJ.js +1 -0
- package/client/dist/assets/redshift-d8BBqiwb.js +1 -0
- package/client/dist/assets/restructuredtext-C8a6yIcZ.js +1 -0
- package/client/dist/assets/ruby-egeh-6KX.js +1 -0
- package/client/dist/assets/rust-a3r9IInB.js +1 -0
- package/client/dist/assets/sb-y8iRIDei.js +1 -0
- package/client/dist/assets/scala-BPDK2AmK.js +1 -0
- package/client/dist/assets/scheme-BIWUEoOs.js +1 -0
- package/client/dist/assets/scss-CA-PSzwg.js +3 -0
- package/client/dist/assets/settings-55oDcbSh.js +1 -0
- package/client/dist/assets/settings-Bd4Tq1RB.js +1 -0
- package/client/dist/assets/settings-CCSM-Fhn.js +1 -0
- package/client/dist/assets/settings-D3e_bDoW.js +1 -0
- package/client/dist/assets/settings-DKbTkbn7.js +1 -0
- package/client/dist/assets/settings-Dxpo6_w7.js +1 -0
- package/client/dist/assets/settings-bt84e3Aa.js +1 -0
- package/client/dist/assets/settings-nu68QukM.js +1 -0
- package/client/dist/assets/setup-BMqwfbW9.js +1 -0
- package/client/dist/assets/setup-Bb5LcG28.js +1 -0
- package/client/dist/assets/setup-BeEx2_da.js +1 -0
- package/client/dist/assets/setup-CCCrB53Q.js +1 -0
- package/client/dist/assets/setup-CJA0ATmd.js +1 -0
- package/client/dist/assets/setup-CeiDbZcb.js +1 -0
- package/client/dist/assets/setup-Cus7TApA.js +1 -0
- package/client/dist/assets/setup-D9qOs2Xo.js +1 -0
- package/client/dist/assets/shell--LiT1Bja.js +1 -0
- package/client/dist/assets/solidity-DdqZccZg.js +1 -0
- package/client/dist/assets/sophia-S6-YxNG_.js +1 -0
- package/client/dist/assets/sparql-BSf5kMp2.js +1 -0
- package/client/dist/assets/specs-BFfu3u-a.js +1 -0
- package/client/dist/assets/specs-B__C8-8a.js +1 -0
- package/client/dist/assets/specs-CZ1PsXsC.js +1 -0
- package/client/dist/assets/specs-D2FzlLn9.js +1 -0
- package/client/dist/assets/specs-DaUTrNF9.js +1 -0
- package/client/dist/assets/specs-Dyc5hYeE.js +1 -0
- package/client/dist/assets/specs-cKEh2LXt.js +1 -0
- package/client/dist/assets/specs-k0PyLDVt.js +1 -0
- package/client/dist/assets/sql-D7KgjR8G.js +1 -0
- package/client/dist/assets/st-BnoDa-Ml.js +1 -0
- package/client/dist/assets/swift-DEUHTkUX.js +1 -0
- package/client/dist/assets/systemverilog-Tqb_KPnW.js +1 -0
- package/client/dist/assets/tcl-BmBFS2qq.js +1 -0
- package/client/dist/assets/terminal-80yDMgMF.js +1 -0
- package/client/dist/assets/terminal-Bje4ziIa.js +1 -0
- package/client/dist/assets/terminal-C2WYcFHF.js +1 -0
- package/client/dist/assets/terminal-CSONJOex.js +1 -0
- package/client/dist/assets/terminal-DEqzGtcr.js +1 -0
- package/client/dist/assets/terminal-DeWzh6ys.js +1 -0
- package/client/dist/assets/terminal-YOlsJCQj.js +1 -0
- package/client/dist/assets/terminal-lkZYR4wJ.js +1 -0
- package/client/dist/assets/tickets-CB7N30gm.js +1 -0
- package/client/dist/assets/tickets-CF2PYelu.js +1 -0
- package/client/dist/assets/tickets-DNOANUXr.js +1 -0
- package/client/dist/assets/tickets-DU1aqsbr.js +1 -0
- package/client/dist/assets/tickets-DYvafSaY.js +1 -0
- package/client/dist/assets/tickets-DlpC_iTg.js +1 -0
- package/client/dist/assets/tickets-DucYgtdl.js +1 -0
- package/client/dist/assets/tickets-clefmXLv.js +1 -0
- package/client/dist/assets/ts.worker-METxwbDZ.js +67719 -0
- package/client/dist/assets/tsMode-B0y_xEci.js +11 -0
- package/client/dist/assets/twig-BQV8igWC.js +1 -0
- package/client/dist/assets/typescript-BzK0OgwW.js +1 -0
- package/client/dist/assets/typespec-DlFroUGY.js +1 -0
- package/client/dist/assets/useProjectCache-DSaiGFjV.js +1 -0
- package/client/dist/assets/vb-BlrJpIMX.js +1 -0
- package/client/dist/assets/wgsl-BWgIc6FZ.js +298 -0
- package/client/dist/assets/workers-rt--R2Qy.js +1 -0
- package/client/dist/assets/xml-eX9QXAmI.js +1 -0
- package/client/dist/assets/yaml-fcsNkpOt.js +1 -0
- package/client/dist/index.html +246 -0
- package/docs/README.md +54 -0
- package/docs/cli.md +198 -0
- package/docs/codex.md +210 -0
- package/docs/creating-specs.md +197 -0
- package/docs/customizing.md +197 -0
- package/docs/getting-started.md +140 -0
- package/docs/internals/README.md +25 -0
- package/docs/internals/adding-a-provider.md +238 -0
- package/docs/internals/api-reference.md +634 -0
- package/docs/internals/architecture.md +332 -0
- package/docs/internals/configuration.md +172 -0
- package/docs/internals/openspec-workflow.md +282 -0
- package/docs/internals/operations-runbook.md +198 -0
- package/docs/internals/profiles.md +152 -0
- package/docs/platforms/macos.md +130 -0
- package/docs/platforms/windows.md +81 -0
- package/docs/running-pipelines.md +240 -0
- package/docs/terminal.md +138 -0
- package/docs/tracking-cost.md +155 -0
- package/package.json +82 -0
- package/server/dist/agent-generator.js +232 -0
- package/server/dist/agent-refine-db.js +124 -0
- package/server/dist/agent-refine-manager.js +526 -0
- package/server/dist/ai-invocations.js +111 -0
- package/server/dist/attachment-manager.js +299 -0
- package/server/dist/auth.js +207 -0
- package/server/dist/binary-probe.js +35 -0
- package/server/dist/browser-capture-manager.js +576 -0
- package/server/dist/browser-capture-types.js +28 -0
- package/server/dist/browser-network.js +149 -0
- package/server/dist/browser-playwright.js +888 -0
- package/server/dist/build-dirs.js +44 -0
- package/server/dist/changes-reader.js +120 -0
- package/server/dist/chat-manager.js +1060 -0
- package/server/dist/chromium-resolver.js +311 -0
- package/server/dist/code-explorer-router.js +788 -0
- package/server/dist/codex-otel-bridge.js +235 -0
- package/server/dist/command-resolver.js +102 -0
- package/server/dist/config.js +306 -0
- package/server/dist/context-budget.js +113 -0
- package/server/dist/context-scope.js +279 -0
- package/server/dist/contract-refine-runner.js +521 -0
- package/server/dist/core-compat.js +207 -0
- package/server/dist/core-package.js +14 -0
- package/server/dist/db.js +1034 -0
- package/server/dist/desktop-analytics.js +156 -0
- package/server/dist/desktop-db.js +456 -0
- package/server/dist/desktop-router.js +735 -0
- package/server/dist/docs-router.js +207 -0
- package/server/dist/explore-contract-refine.js +421 -0
- package/server/dist/explore-cwd-manager.js +242 -0
- package/server/dist/explore-draft-title.js +47 -0
- package/server/dist/explore-smash.js +450 -0
- package/server/dist/feature-flags.js +17 -0
- package/server/dist/file-provenance.js +382 -0
- package/server/dist/file-summary-generator.js +221 -0
- package/server/dist/file-summary-manager.js +689 -0
- package/server/dist/hooks.js +102 -0
- package/server/dist/ids.js +7 -0
- package/server/dist/index.js +586 -0
- package/server/dist/metrics.js +136 -0
- package/server/dist/mobile/index.js +16 -0
- package/server/dist/mobile/mobile-admin-router.js +84 -0
- package/server/dist/mobile/mobile-auth.js +67 -0
- package/server/dist/mobile/mobile-devices.js +80 -0
- package/server/dist/mobile/mobile-event-bus.js +39 -0
- package/server/dist/mobile/mobile-gateway.js +285 -0
- package/server/dist/mobile/mobile-mdns.js +81 -0
- package/server/dist/mobile/mobile-pairing.js +179 -0
- package/server/dist/mobile/mobile-redact.js +53 -0
- package/server/dist/mobile/mobile-router.js +411 -0
- package/server/dist/mobile/mobile-tls.js +86 -0
- package/server/dist/mobile/mobile-types.js +9 -0
- package/server/dist/mobile/mobile-ws.js +275 -0
- package/server/dist/path-resolver.js +298 -0
- package/server/dist/plugin-manager.js +617 -0
- package/server/dist/plugins/claude-approval.js +179 -0
- package/server/dist/plugins/claude-md-mutation.js +146 -0
- package/server/dist/plugins/codex-mcp.js +108 -0
- package/server/dist/plugins/contributors.js +72 -0
- package/server/dist/plugins/drift.js +58 -0
- package/server/dist/plugins/index.js +14 -0
- package/server/dist/plugins/json-mutation.js +120 -0
- package/server/dist/plugins/manager.js +32 -0
- package/server/dist/plugins/ownership.js +86 -0
- package/server/dist/plugins/paths.js +37 -0
- package/server/dist/plugins/prereq-installer.js +104 -0
- package/server/dist/plugins/rail-integration.js +79 -0
- package/server/dist/plugins/serena/index.js +13 -0
- package/server/dist/plugins/serena/install.js +91 -0
- package/server/dist/plugins/serena/instructions-content.js +21 -0
- package/server/dist/plugins/serena/manifest.js +111 -0
- package/server/dist/plugins/serena/verify.js +78 -0
- package/server/dist/plugins-router.js +215 -0
- package/server/dist/pricing.js +89 -0
- package/server/dist/profile-manager.js +310 -0
- package/server/dist/profiles-router.js +759 -0
- package/server/dist/project-registry.js +443 -0
- package/server/dist/project-router.js +4016 -0
- package/server/dist/proposal-manager.js +291 -0
- package/server/dist/provider-selection.js +69 -0
- package/server/dist/providers/claude-adapter.js +281 -0
- package/server/dist/providers/codex-adapter.js +264 -0
- package/server/dist/providers/index.js +23 -0
- package/server/dist/providers/registry.js +37 -0
- package/server/dist/providers/types.js +22 -0
- package/server/dist/queue-manager.js +1511 -0
- package/server/dist/rails-router.js +362 -0
- package/server/dist/rails-store.js +116 -0
- package/server/dist/result-event.js +106 -0
- package/server/dist/schemas/profile.v1.json +151 -0
- package/server/dist/setup-manager.js +1165 -0
- package/server/dist/setup-prerequisites.js +372 -0
- package/server/dist/smash-runner.js +663 -0
- package/server/dist/spec-draft-parser.js +133 -0
- package/server/dist/spec-launcher-manager.js +174 -0
- package/server/dist/spec-models.js +32 -0
- package/server/dist/specrails-tech-client.js +82 -0
- package/server/dist/spending.js +448 -0
- package/server/dist/telemetry-compactor.js +180 -0
- package/server/dist/telemetry-export.js +317 -0
- package/server/dist/telemetry-receiver.js +224 -0
- package/server/dist/terminal-manager.js +633 -0
- package/server/dist/terminal-marks-store.js +117 -0
- package/server/dist/terminal-osc-parser.js +159 -0
- package/server/dist/terminal-settings.js +282 -0
- package/server/dist/terminal-shell-integration.js +196 -0
- package/server/dist/ticket-broadcast.js +47 -0
- package/server/dist/ticket-store.js +397 -0
- package/server/dist/ticket-watcher.js +117 -0
- package/server/dist/types.js +10 -0
- package/server/dist/user-mcp-config.js +117 -0
- package/server/dist/util/cli-prompt.js +181 -0
- package/server/dist/util/secure-fs.js +50 -0
- package/server/dist/util/win-spawn.js +43 -0
- package/server/dist/webhook-manager.js +89 -0
- package/server/dist/ws-routing.js +47 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.FileSummaryManager = void 0;
|
|
40
|
+
exports.summariesDir = summariesDir;
|
|
41
|
+
exports.pathHash = pathHash;
|
|
42
|
+
exports.summaryFilePath = summaryFilePath;
|
|
43
|
+
exports.computeFileHash = computeFileHash;
|
|
44
|
+
exports.readSummary = readSummary;
|
|
45
|
+
exports.writeSummary = writeSummary;
|
|
46
|
+
exports.ensureGitignoreLine = ensureGitignoreLine;
|
|
47
|
+
exports.sweepOrphans = sweepOrphans;
|
|
48
|
+
exports.__resetDesktopSummaryStateForTests = __resetDesktopSummaryStateForTests;
|
|
49
|
+
const crypto_1 = require("crypto");
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const chokidar_1 = __importDefault(require("chokidar"));
|
|
53
|
+
const build_dirs_1 = require("./build-dirs");
|
|
54
|
+
const ai_invocations_1 = require("./ai-invocations");
|
|
55
|
+
const SUMMARIES_REL = path.join('.specrails', 'file-summaries');
|
|
56
|
+
// Current summary prompt version. Bump when buildSystemPrompt changes materially
|
|
57
|
+
// so existing summaries are treated as stale and regenerated on next request.
|
|
58
|
+
const CURRENT_PROMPT_VERSION = 1;
|
|
59
|
+
// Defensive bound on the per-job counter map so it cannot grow without limit
|
|
60
|
+
// across a long-lived app session (the per-job cap is best-effort).
|
|
61
|
+
const MAX_JOB_COUNTERS = 2000;
|
|
62
|
+
const TOKEN_CHARS_PER_TOKEN = 4;
|
|
63
|
+
const TOKEN_LIMIT = 8000;
|
|
64
|
+
const TRUNCATE_HEAD_CHARS = 16000;
|
|
65
|
+
const TRUNCATE_TAIL_CHARS = 8000;
|
|
66
|
+
const TRUNCATE_MARKER = '\n// … truncated … //\n';
|
|
67
|
+
function summariesDir(projectPath) {
|
|
68
|
+
return path.join(projectPath, SUMMARIES_REL);
|
|
69
|
+
}
|
|
70
|
+
function pathHash(relPath) {
|
|
71
|
+
return (0, crypto_1.createHash)('sha256').update(Buffer.from(relPath, 'utf8')).digest('hex');
|
|
72
|
+
}
|
|
73
|
+
function summaryFilePath(projectPath, relPath) {
|
|
74
|
+
return path.join(summariesDir(projectPath), `${pathHash(relPath)}.json`);
|
|
75
|
+
}
|
|
76
|
+
async function computeFileHash(absolutePath) {
|
|
77
|
+
// Use streaming hash so very large files do not balloon memory.
|
|
78
|
+
return await new Promise((resolve, reject) => {
|
|
79
|
+
const hash = (0, crypto_1.createHash)('sha256');
|
|
80
|
+
const stream = fs.createReadStream(absolutePath);
|
|
81
|
+
stream.on('error', reject);
|
|
82
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
83
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function readSummary(projectPath, relPath) {
|
|
87
|
+
const file = summaryFilePath(projectPath, relPath);
|
|
88
|
+
try {
|
|
89
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
90
|
+
return JSON.parse(raw);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (err.code === 'ENOENT')
|
|
94
|
+
return null;
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function writeSummary(projectPath, relPath, payload) {
|
|
99
|
+
const dir = summariesDir(projectPath);
|
|
100
|
+
const firstWrite = !fs.existsSync(dir);
|
|
101
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
102
|
+
const final = summaryFilePath(projectPath, relPath);
|
|
103
|
+
// Atomic write: temp file in the same directory, then rename.
|
|
104
|
+
const tmp = `${final}.tmp.${(0, crypto_1.randomBytes)(6).toString('hex')}`;
|
|
105
|
+
fs.writeFileSync(tmp, JSON.stringify(payload), { encoding: 'utf8', mode: 0o600 });
|
|
106
|
+
fs.renameSync(tmp, final);
|
|
107
|
+
if (firstWrite) {
|
|
108
|
+
// The app appends `.specrails/file-summaries/` to the project `.gitignore`
|
|
109
|
+
// on first write. Idempotent: only appends when the line is absent.
|
|
110
|
+
try {
|
|
111
|
+
ensureGitignoreLine(projectPath, '.specrails/file-summaries/');
|
|
112
|
+
}
|
|
113
|
+
catch { /* non-fatal */ }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function ensureGitignoreLine(projectPath, line) {
|
|
117
|
+
const gi = path.join(projectPath, '.gitignore');
|
|
118
|
+
let existing = '';
|
|
119
|
+
try {
|
|
120
|
+
existing = fs.readFileSync(gi, 'utf8');
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
if (err.code !== 'ENOENT')
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
const hasLine = existing.split(/\r?\n/).some((l) => l.trim() === line.trim());
|
|
127
|
+
if (hasLine)
|
|
128
|
+
return false;
|
|
129
|
+
const sep = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
|
|
130
|
+
fs.writeFileSync(gi, `${existing}${sep}${line}\n`, 'utf8');
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
function sweepOrphans(projectPath, cap = 200) {
|
|
134
|
+
const dir = summariesDir(projectPath);
|
|
135
|
+
let deleted = 0;
|
|
136
|
+
let remaining = 0;
|
|
137
|
+
let entries;
|
|
138
|
+
try {
|
|
139
|
+
entries = fs.readdirSync(dir);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (err.code === 'ENOENT')
|
|
143
|
+
return { deleted: 0, remaining: 0 };
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (!entry.endsWith('.json'))
|
|
148
|
+
continue;
|
|
149
|
+
const full = path.join(dir, entry);
|
|
150
|
+
let payload;
|
|
151
|
+
try {
|
|
152
|
+
payload = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const sourceAbs = path.join(projectPath, payload.path);
|
|
158
|
+
if (fs.existsSync(sourceAbs))
|
|
159
|
+
continue;
|
|
160
|
+
if (deleted >= cap) {
|
|
161
|
+
remaining += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
fs.unlinkSync(full);
|
|
166
|
+
deleted += 1;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// best-effort sweep
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { deleted, remaining };
|
|
173
|
+
}
|
|
174
|
+
// App-wide concurrency is shared across EVERY FileSummaryManager instance.
|
|
175
|
+
// Production constructs one manager per project (each with its own db/broadcast/
|
|
176
|
+
// generate), so a per-instance counter would only ever cap per-project. This
|
|
177
|
+
// module-level state makes the documented "app-wide 8" ceiling real: all live
|
|
178
|
+
// managers share one in-flight count, and freeing a slot re-pumps every live
|
|
179
|
+
// manager so a blocked project's queue advances.
|
|
180
|
+
const DESKTOP = {
|
|
181
|
+
inFlight: 0,
|
|
182
|
+
managers: new Set(),
|
|
183
|
+
};
|
|
184
|
+
/** Test-only: reset the shared app-wide counter/registry between unit tests so
|
|
185
|
+
* module-level state never leaks across cases. */
|
|
186
|
+
function __resetDesktopSummaryStateForTests() {
|
|
187
|
+
DESKTOP.inFlight = 0;
|
|
188
|
+
DESKTOP.managers.clear();
|
|
189
|
+
}
|
|
190
|
+
class FileSummaryManager {
|
|
191
|
+
deps;
|
|
192
|
+
perProjectConcurrency;
|
|
193
|
+
desktopConcurrency;
|
|
194
|
+
perJobCap;
|
|
195
|
+
queueTtlMs;
|
|
196
|
+
maxJobCounters;
|
|
197
|
+
// Per-project queue, per-project in-flight count. App-wide in-flight lives in
|
|
198
|
+
// the module-level DESKTOP so it is shared across all per-project instances.
|
|
199
|
+
queues = new Map();
|
|
200
|
+
inFlightPerProject = new Map();
|
|
201
|
+
jobCounter = new Map();
|
|
202
|
+
watchers = new Map();
|
|
203
|
+
// Dedupe key (`projectId:relPath`) → in-flight enqueue promise. A second
|
|
204
|
+
// enqueue for the same file rides the first instead of double-spawning the
|
|
205
|
+
// provider and double-billing ai_invocations.
|
|
206
|
+
inFlightByKey = new Map();
|
|
207
|
+
// AbortControllers for in-flight generations so dispose() can tear down the
|
|
208
|
+
// provider child instead of orphaning it past project removal.
|
|
209
|
+
activeControllers = new Set();
|
|
210
|
+
_disposed = false;
|
|
211
|
+
// Per-project SUPERSET of relPaths that have a summary on disk. Seeded from
|
|
212
|
+
// the summaries dir at attachWatcher and only ever added to (on write), so a
|
|
213
|
+
// path absent from the set provably has no summary — letting the chokidar
|
|
214
|
+
// 'change' handler skip the readSummary disk hit for the common no-summary
|
|
215
|
+
// file. A stale entry (after sweep) just costs one harmless failed read.
|
|
216
|
+
knownSummaries = new Map();
|
|
217
|
+
// Tracks pending generation promises so flush() can await them in tests.
|
|
218
|
+
pending = new Set();
|
|
219
|
+
constructor(deps, opts = {}) {
|
|
220
|
+
this.deps = deps;
|
|
221
|
+
this.perProjectConcurrency = opts.perProjectConcurrency ?? 2;
|
|
222
|
+
this.desktopConcurrency = opts.desktopConcurrency ?? 8;
|
|
223
|
+
this.perJobCap = opts.perJobCap ?? 50;
|
|
224
|
+
this.queueTtlMs = opts.queueTtlMs ?? 5 * 60 * 1000;
|
|
225
|
+
this.maxJobCounters = opts.maxJobCounters ?? MAX_JOB_COUNTERS;
|
|
226
|
+
DESKTOP.managers.add(this);
|
|
227
|
+
}
|
|
228
|
+
// NOT async: returning the in-flight promise verbatim is what makes the dedupe
|
|
229
|
+
// a true coalesce (the second caller gets the SAME promise, not a wrapper).
|
|
230
|
+
enqueue(req) {
|
|
231
|
+
// Per-(project,relPath) in-flight dedupe: a second enqueue for the same file
|
|
232
|
+
// while one is still running coalesces onto the first promise, so the
|
|
233
|
+
// provider is spawned once and ai_invocations is billed once (fixes
|
|
234
|
+
// concurrent-regenerate double-billing across tabs/clients).
|
|
235
|
+
const dedupeKey = `${req.projectId}:${req.relPath}`;
|
|
236
|
+
const existing = this.inFlightByKey.get(dedupeKey);
|
|
237
|
+
if (existing)
|
|
238
|
+
return existing;
|
|
239
|
+
const p = this._enqueueInner(req);
|
|
240
|
+
this.inFlightByKey.set(dedupeKey, p);
|
|
241
|
+
void p.catch(() => undefined).finally(() => {
|
|
242
|
+
if (this.inFlightByKey.get(dedupeKey) === p)
|
|
243
|
+
this.inFlightByKey.delete(dedupeKey);
|
|
244
|
+
});
|
|
245
|
+
return p;
|
|
246
|
+
}
|
|
247
|
+
async _enqueueInner(req) {
|
|
248
|
+
const absolutePath = path.join(req.projectPath, req.relPath);
|
|
249
|
+
// Step 1: file readability check.
|
|
250
|
+
let newHash;
|
|
251
|
+
try {
|
|
252
|
+
const stat = fs.statSync(absolutePath);
|
|
253
|
+
if (!stat.isFile()) {
|
|
254
|
+
this.emitSkipped(req, 'not-found');
|
|
255
|
+
return 'skipped:not-found';
|
|
256
|
+
}
|
|
257
|
+
newHash = await computeFileHash(absolutePath);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
this.emitSkipped(req, 'not-found');
|
|
261
|
+
return 'skipped:not-found';
|
|
262
|
+
}
|
|
263
|
+
// Step 2: hash gate. Skip regeneration only when content, language AND prompt
|
|
264
|
+
// version all match — and never when the caller forced it. Without the
|
|
265
|
+
// language check an app language switch (en↔es) would never refresh existing
|
|
266
|
+
// summaries; without `force` an explicit "Regenerate" of an unchanged file
|
|
267
|
+
// would be a silent no-op.
|
|
268
|
+
const currentLang = this.deps.language?.() ?? 'en';
|
|
269
|
+
const existing = readSummary(req.projectPath, req.relPath);
|
|
270
|
+
if (!req.force &&
|
|
271
|
+
existing &&
|
|
272
|
+
existing.fileHash === newHash &&
|
|
273
|
+
existing.language === currentLang &&
|
|
274
|
+
existing.generatedBy?.promptVersion === CURRENT_PROMPT_VERSION) {
|
|
275
|
+
this.deps.broadcast(buildSummaryUpdated(req.projectId, existing, false));
|
|
276
|
+
return 'skipped:hash';
|
|
277
|
+
}
|
|
278
|
+
// Step 3: per-job cap — PRE-CHECK ONLY. The counter is incremented in pump()
|
|
279
|
+
// when a generation actually STARTS, so budget-skipped / TTL-dropped / failed
|
|
280
|
+
// requests never consume a per-job slot (the cap counts generations, not
|
|
281
|
+
// attempts). pump() re-checks the cap at start to catch the case where
|
|
282
|
+
// several requests passed this pre-check before any started.
|
|
283
|
+
if (req.jobId) {
|
|
284
|
+
const count = this.jobCounter.get(req.jobId) ?? 0;
|
|
285
|
+
if (count >= this.perJobCap) {
|
|
286
|
+
this.emitSkipped(req, 'per-job-cap');
|
|
287
|
+
return 'skipped:per-job-cap';
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Step 4: budget cap. Applies to job- AND user-triggered requests; the only
|
|
291
|
+
// bypass is an explicit overrideBudget (the "Override the budget cap?"
|
|
292
|
+
// confirmation in the UI). Previously only job-triggered requests were
|
|
293
|
+
// gated, which left the manual-regenerate budget prompt unreachable.
|
|
294
|
+
if (!req.overrideBudget) {
|
|
295
|
+
const spend = this.deps.monthToDateSpend(req.projectId);
|
|
296
|
+
const budget = this.deps.monthlyBudgetUsd();
|
|
297
|
+
if (spend >= budget) {
|
|
298
|
+
this.emitSkipped(req, 'budget');
|
|
299
|
+
return 'skipped:budget';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Step 5: enqueue or run.
|
|
303
|
+
const result = await new Promise((resolve, reject) => {
|
|
304
|
+
const entry = {
|
|
305
|
+
req: { ...req },
|
|
306
|
+
enqueuedAt: (this.deps.now ?? Date.now)(),
|
|
307
|
+
newHash,
|
|
308
|
+
resolve,
|
|
309
|
+
reject,
|
|
310
|
+
};
|
|
311
|
+
const queue = this.queues.get(req.projectId) ?? [];
|
|
312
|
+
queue.push(entry);
|
|
313
|
+
this.queues.set(req.projectId, queue);
|
|
314
|
+
this.pump(req.projectId);
|
|
315
|
+
});
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
pump(projectId) {
|
|
319
|
+
if (this._disposed)
|
|
320
|
+
return;
|
|
321
|
+
const queue = this.queues.get(projectId) ?? [];
|
|
322
|
+
while (queue.length > 0) {
|
|
323
|
+
if (DESKTOP.inFlight >= this.desktopConcurrency)
|
|
324
|
+
break;
|
|
325
|
+
const perProject = this.inFlightPerProject.get(projectId) ?? 0;
|
|
326
|
+
if (perProject >= this.perProjectConcurrency)
|
|
327
|
+
break;
|
|
328
|
+
const entry = queue.shift();
|
|
329
|
+
const now = (this.deps.now ?? Date.now)();
|
|
330
|
+
// TTL drop before starting. Distinct 'skipped:ttl' (not 'skipped:hash') so
|
|
331
|
+
// the regenerate route can tell the user it was dropped, not silently 202.
|
|
332
|
+
if (now - entry.enqueuedAt > this.queueTtlMs) {
|
|
333
|
+
this.emitSkipped(entry.req, 'ttl');
|
|
334
|
+
entry.resolve('skipped:ttl');
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
// Budget re-check at dequeue: an entry that crossed the monthly cap while
|
|
338
|
+
// waiting in the queue is skipped instead of spending, bounding the
|
|
339
|
+
// concurrent-overshoot window to the in-flight set.
|
|
340
|
+
if (!entry.req.overrideBudget) {
|
|
341
|
+
const spend = this.deps.monthToDateSpend(entry.req.projectId);
|
|
342
|
+
const budget = this.deps.monthlyBudgetUsd();
|
|
343
|
+
if (spend >= budget) {
|
|
344
|
+
this.emitSkipped(entry.req, 'budget');
|
|
345
|
+
entry.resolve('skipped:budget');
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Per-job cap re-check + increment at START, so the counter measures
|
|
350
|
+
// generations actually run (not enqueue attempts) and several requests that
|
|
351
|
+
// passed the enqueue pre-check can't collectively exceed the cap.
|
|
352
|
+
if (entry.req.jobId) {
|
|
353
|
+
const count = this.jobCounter.get(entry.req.jobId) ?? 0;
|
|
354
|
+
if (count >= this.perJobCap) {
|
|
355
|
+
this.emitSkipped(entry.req, 'per-job-cap');
|
|
356
|
+
entry.resolve('skipped:per-job-cap');
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (!this.jobCounter.has(entry.req.jobId) && this.jobCounter.size >= this.maxJobCounters) {
|
|
360
|
+
const oldest = this.jobCounter.keys().next().value;
|
|
361
|
+
if (oldest !== undefined)
|
|
362
|
+
this.jobCounter.delete(oldest);
|
|
363
|
+
}
|
|
364
|
+
this.jobCounter.set(entry.req.jobId, count + 1);
|
|
365
|
+
}
|
|
366
|
+
this.inFlightPerProject.set(projectId, perProject + 1);
|
|
367
|
+
DESKTOP.inFlight += 1;
|
|
368
|
+
const p = this.runOne(entry)
|
|
369
|
+
.catch((err) => entry.reject(err))
|
|
370
|
+
.finally(() => {
|
|
371
|
+
this.inFlightPerProject.set(projectId, (this.inFlightPerProject.get(projectId) ?? 1) - 1);
|
|
372
|
+
DESKTOP.inFlight -= 1;
|
|
373
|
+
this.pending.delete(p);
|
|
374
|
+
this.pump(projectId);
|
|
375
|
+
// A freed app-wide slot may unblock OTHER projects' managers.
|
|
376
|
+
for (const m of DESKTOP.managers)
|
|
377
|
+
if (m !== this)
|
|
378
|
+
m._drainAll();
|
|
379
|
+
});
|
|
380
|
+
this.pending.add(p);
|
|
381
|
+
}
|
|
382
|
+
if (queue.length === 0)
|
|
383
|
+
this.queues.delete(projectId);
|
|
384
|
+
else
|
|
385
|
+
this.queues.set(projectId, queue);
|
|
386
|
+
}
|
|
387
|
+
/** Pump every project queue this manager owns (used when an app-wide slot frees
|
|
388
|
+
* up in a different manager). */
|
|
389
|
+
_drainAll() {
|
|
390
|
+
for (const pid of [...this.queues.keys()])
|
|
391
|
+
this.pump(pid);
|
|
392
|
+
}
|
|
393
|
+
async runOne(entry) {
|
|
394
|
+
const { req } = entry;
|
|
395
|
+
const absolutePath = path.join(req.projectPath, req.relPath);
|
|
396
|
+
const startedIso = new Date((this.deps.now ?? Date.now)()).toISOString();
|
|
397
|
+
let contents;
|
|
398
|
+
let fileHash;
|
|
399
|
+
try {
|
|
400
|
+
contents = fs.readFileSync(absolutePath, 'utf8');
|
|
401
|
+
// Use the hash captured for THIS entry at enqueue time (never another
|
|
402
|
+
// entry's). Fall back to a recompute only if it was somehow not set.
|
|
403
|
+
fileHash = entry.newHash || (await computeFileHash(absolutePath));
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
this.emitSkipped(req, 'not-found');
|
|
407
|
+
entry.resolve('skipped:not-found');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const tokens = Math.ceil(contents.length / TOKEN_CHARS_PER_TOKEN);
|
|
411
|
+
let truncated = false;
|
|
412
|
+
let promptContents = contents;
|
|
413
|
+
if (tokens > TOKEN_LIMIT) {
|
|
414
|
+
truncated = true;
|
|
415
|
+
const head = contents.slice(0, TRUNCATE_HEAD_CHARS);
|
|
416
|
+
const tail = contents.slice(contents.length - TRUNCATE_TAIL_CHARS);
|
|
417
|
+
promptContents = head + TRUNCATE_MARKER + tail;
|
|
418
|
+
}
|
|
419
|
+
const lang = (this.deps.language?.() ?? 'en');
|
|
420
|
+
const controller = new AbortController();
|
|
421
|
+
this.activeControllers.add(controller);
|
|
422
|
+
try {
|
|
423
|
+
const out = await this.deps.generate({
|
|
424
|
+
relPath: req.relPath,
|
|
425
|
+
contents: promptContents,
|
|
426
|
+
truncated,
|
|
427
|
+
language: lang,
|
|
428
|
+
}, controller.signal);
|
|
429
|
+
// The project may have been removed (and its DB closed) while the provider
|
|
430
|
+
// ran. Skip all DB/disk/broadcast work so we never touch a closed handle.
|
|
431
|
+
if (this._disposed) {
|
|
432
|
+
entry.resolve('failed');
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const payload = {
|
|
436
|
+
schemaVersion: 1,
|
|
437
|
+
path: req.relPath,
|
|
438
|
+
fileHash,
|
|
439
|
+
summary: out.summary,
|
|
440
|
+
language: lang,
|
|
441
|
+
generatedAt: new Date((this.deps.now ?? Date.now)()).toISOString(),
|
|
442
|
+
generatedBy: { model: out.model, promptVersion: CURRENT_PROMPT_VERSION, truncated },
|
|
443
|
+
triggeredBy: req.triggeredBy,
|
|
444
|
+
};
|
|
445
|
+
writeSummary(req.projectPath, req.relPath, payload);
|
|
446
|
+
// Keep the watcher's negative-cache a correct superset.
|
|
447
|
+
this.knownSummaries.get(req.projectId)?.add(req.relPath);
|
|
448
|
+
try {
|
|
449
|
+
(0, ai_invocations_1.recordInvocation)(this.deps.db, {
|
|
450
|
+
id: (0, crypto_1.randomUUID)(),
|
|
451
|
+
project_id: req.projectId,
|
|
452
|
+
provider: out.provider,
|
|
453
|
+
surface: 'file-summary',
|
|
454
|
+
surface_ref_id: req.jobId ?? null,
|
|
455
|
+
ticket_id: req.triggeredBy.ticketId,
|
|
456
|
+
status: 'success',
|
|
457
|
+
started_at: startedIso,
|
|
458
|
+
finished_at: new Date((this.deps.now ?? Date.now)()).toISOString(),
|
|
459
|
+
model: out.model,
|
|
460
|
+
total_cost_usd: out.costUsd,
|
|
461
|
+
tokens_in: out.tokensIn,
|
|
462
|
+
tokens_out: out.tokensOut,
|
|
463
|
+
tokens_cache_read: out.tokensCacheRead,
|
|
464
|
+
tokens_cache_create: out.tokensCacheCreate,
|
|
465
|
+
duration_ms: out.durationMs,
|
|
466
|
+
num_turns: 1,
|
|
467
|
+
total_cost_usd_estimated: !!out.costEstimated,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
catch (err) {
|
|
471
|
+
// An ai_invocations write failure must never crash the summary queue,
|
|
472
|
+
// but it should not be silent either — a swallowed failure here means
|
|
473
|
+
// spending under-counts with no trace.
|
|
474
|
+
console.error('[file-summary] recordInvocation (success) failed:', err);
|
|
475
|
+
}
|
|
476
|
+
this.deps.broadcast(buildSummaryUpdated(req.projectId, payload, false));
|
|
477
|
+
this.deps.broadcast({ type: 'spending.invalidated', projectId: req.projectId });
|
|
478
|
+
entry.resolve('enqueued');
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
// Disposed mid-flight (project removed) — DB is closed; skip all writes.
|
|
482
|
+
if (this._disposed) {
|
|
483
|
+
entry.resolve('failed');
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
487
|
+
// A timeout/abort kills the child AFTER the provider may have billed tokens.
|
|
488
|
+
// The generator attaches whatever usage it captured so the failed row (and
|
|
489
|
+
// therefore the monthly-budget reader) accounts for that real spend instead
|
|
490
|
+
// of recording a misleading $0.
|
|
491
|
+
const partial = err.partial;
|
|
492
|
+
try {
|
|
493
|
+
(0, ai_invocations_1.recordInvocation)(this.deps.db, {
|
|
494
|
+
id: (0, crypto_1.randomUUID)(),
|
|
495
|
+
project_id: req.projectId,
|
|
496
|
+
provider: partial?.provider ?? this.deps.providerId?.() ?? 'claude',
|
|
497
|
+
surface: 'file-summary',
|
|
498
|
+
surface_ref_id: req.jobId ?? null,
|
|
499
|
+
ticket_id: req.triggeredBy.ticketId,
|
|
500
|
+
status: 'failed',
|
|
501
|
+
started_at: startedIso,
|
|
502
|
+
finished_at: new Date((this.deps.now ?? Date.now)()).toISOString(),
|
|
503
|
+
model: partial?.model,
|
|
504
|
+
total_cost_usd: partial?.costUsd ?? 0,
|
|
505
|
+
tokens_in: partial?.tokensIn ?? 0,
|
|
506
|
+
tokens_out: partial?.tokensOut ?? 0,
|
|
507
|
+
tokens_cache_read: partial?.tokensCacheRead,
|
|
508
|
+
tokens_cache_create: partial?.tokensCacheCreate,
|
|
509
|
+
duration_ms: partial?.durationMs ?? 0,
|
|
510
|
+
num_turns: 1,
|
|
511
|
+
total_cost_usd_estimated: !!partial?.costEstimated,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
catch (recErr) {
|
|
515
|
+
// ai_invocations write failures must not crash the manager, but log so
|
|
516
|
+
// a persistent DB problem is visible.
|
|
517
|
+
console.error('[file-summary] recordInvocation (failure) failed:', recErr);
|
|
518
|
+
}
|
|
519
|
+
const failedMsg = {
|
|
520
|
+
type: 'file.summary_failed',
|
|
521
|
+
projectId: req.projectId,
|
|
522
|
+
path: req.relPath,
|
|
523
|
+
reason,
|
|
524
|
+
};
|
|
525
|
+
this.deps.broadcast(failedMsg);
|
|
526
|
+
// Resolve with 'failed' (not 'enqueued') so a caller awaiting enqueue()
|
|
527
|
+
// can distinguish a failed generation from a successful one.
|
|
528
|
+
entry.resolve('failed');
|
|
529
|
+
}
|
|
530
|
+
finally {
|
|
531
|
+
this.activeControllers.delete(controller);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
markStale(projectPath, projectId, relPath) {
|
|
535
|
+
const existing = readSummary(projectPath, relPath);
|
|
536
|
+
if (!existing)
|
|
537
|
+
return;
|
|
538
|
+
this.deps.broadcast(buildSummaryUpdated(projectId, existing, true));
|
|
539
|
+
}
|
|
540
|
+
attachWatcher(projectId, projectPath) {
|
|
541
|
+
if (this.watchers.has(projectId))
|
|
542
|
+
return;
|
|
543
|
+
this.knownSummaries.set(projectId, this.scanKnownSummaries(projectPath));
|
|
544
|
+
// Reclaim summary JSON files whose source file was renamed/deleted since the
|
|
545
|
+
// last session. Runs once per project per session (attachWatcher is
|
|
546
|
+
// idempotent) and is capped at 200/pass inside sweepOrphans. The chokidar
|
|
547
|
+
// watcher only sees 'change', never 'unlink', so this is the only reaper.
|
|
548
|
+
try {
|
|
549
|
+
sweepOrphans(projectPath);
|
|
550
|
+
}
|
|
551
|
+
catch { /* best effort */ }
|
|
552
|
+
// CRITICAL: prune build/dep trees (node_modules, dist, target, src-tauri/target,
|
|
553
|
+
// dot-dirs, …) from the recursive watch. Watching a Rust/Tauri `target/` tree
|
|
554
|
+
// opened ~10k file descriptors and broke terminal spawning under fd pressure.
|
|
555
|
+
// The predicate is tested against each path relative to the project root so a
|
|
556
|
+
// dot-segment in the absolute prefix (the user's home dir) can't false-positive.
|
|
557
|
+
const watcher = chokidar_1.default.watch(projectPath, {
|
|
558
|
+
ignored: (p) => {
|
|
559
|
+
const rel = path.relative(projectPath, p);
|
|
560
|
+
if (!rel || rel.startsWith('..'))
|
|
561
|
+
return false; // the root itself — never ignore
|
|
562
|
+
return (0, build_dirs_1.isInBuildDir)(rel);
|
|
563
|
+
},
|
|
564
|
+
ignoreInitial: true,
|
|
565
|
+
persistent: true,
|
|
566
|
+
// Coalesce partial-write churn so one logical edit fires one event.
|
|
567
|
+
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 100 },
|
|
568
|
+
});
|
|
569
|
+
watcher.on('change', (changed) => {
|
|
570
|
+
const rel = path.relative(projectPath, changed);
|
|
571
|
+
if (!rel || rel.startsWith('..'))
|
|
572
|
+
return;
|
|
573
|
+
// Skip the readSummary disk hit when this file provably has no summary.
|
|
574
|
+
const known = this.knownSummaries.get(projectId);
|
|
575
|
+
if (known && !known.has(rel))
|
|
576
|
+
return;
|
|
577
|
+
this.markStale(projectPath, projectId, rel);
|
|
578
|
+
});
|
|
579
|
+
this.watchers.set(projectId, { projectPath, watcher });
|
|
580
|
+
}
|
|
581
|
+
/** Read the relPaths of every summary on disk into a set (one-time at attach). */
|
|
582
|
+
scanKnownSummaries(projectPath) {
|
|
583
|
+
const set = new Set();
|
|
584
|
+
const dir = summariesDir(projectPath);
|
|
585
|
+
let files = [];
|
|
586
|
+
try {
|
|
587
|
+
files = fs.readdirSync(dir);
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
return set;
|
|
591
|
+
}
|
|
592
|
+
for (const f of files) {
|
|
593
|
+
if (!f.endsWith('.json'))
|
|
594
|
+
continue;
|
|
595
|
+
try {
|
|
596
|
+
const payload = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
597
|
+
if (payload?.path)
|
|
598
|
+
set.add(payload.path);
|
|
599
|
+
}
|
|
600
|
+
catch { /* skip unreadable/partial files */ }
|
|
601
|
+
}
|
|
602
|
+
return set;
|
|
603
|
+
}
|
|
604
|
+
detachWatcher(projectId) {
|
|
605
|
+
const state = this.watchers.get(projectId);
|
|
606
|
+
if (!state)
|
|
607
|
+
return;
|
|
608
|
+
void state.watcher.close();
|
|
609
|
+
this.watchers.delete(projectId);
|
|
610
|
+
this.knownSummaries.delete(projectId);
|
|
611
|
+
}
|
|
612
|
+
/** Full teardown for a single manager: stop accepting work, abort any in-flight
|
|
613
|
+
* provider child (so it is not orphaned past project removal), reject queued
|
|
614
|
+
* entries, close watchers, and leave the shared app-wide registry. Call from
|
|
615
|
+
* ProjectRegistry.removeProject (before db.close) and from shutdown(). After
|
|
616
|
+
* this, runOne's `_disposed` guard skips all DB/disk writes. Idempotent. */
|
|
617
|
+
dispose() {
|
|
618
|
+
if (this._disposed)
|
|
619
|
+
return;
|
|
620
|
+
this._disposed = true;
|
|
621
|
+
DESKTOP.managers.delete(this);
|
|
622
|
+
// Abort in-flight generations → generator treeKills the provider child.
|
|
623
|
+
for (const c of this.activeControllers) {
|
|
624
|
+
try {
|
|
625
|
+
c.abort();
|
|
626
|
+
}
|
|
627
|
+
catch { /* best effort */ }
|
|
628
|
+
}
|
|
629
|
+
this.activeControllers.clear();
|
|
630
|
+
// Reject still-queued entries so awaiting callers settle (skipped, not hung).
|
|
631
|
+
for (const [, queue] of this.queues) {
|
|
632
|
+
for (const entry of queue) {
|
|
633
|
+
try {
|
|
634
|
+
this.emitSkipped(entry.req, 'not-found');
|
|
635
|
+
entry.resolve('skipped:not-found');
|
|
636
|
+
}
|
|
637
|
+
catch { /* best effort */ }
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
this.queues.clear();
|
|
641
|
+
this.inFlightByKey.clear();
|
|
642
|
+
for (const [, state] of this.watchers) {
|
|
643
|
+
try {
|
|
644
|
+
void state.watcher.close();
|
|
645
|
+
}
|
|
646
|
+
catch { /* best effort */ }
|
|
647
|
+
}
|
|
648
|
+
this.watchers.clear();
|
|
649
|
+
this.knownSummaries.clear();
|
|
650
|
+
}
|
|
651
|
+
/** Close every watcher. Called on graceful server shutdown so chokidar's
|
|
652
|
+
* underlying fsevents/inotify handles are released, never leaked. */
|
|
653
|
+
disposeAll() {
|
|
654
|
+
this.dispose();
|
|
655
|
+
}
|
|
656
|
+
async flush() {
|
|
657
|
+
// Drain until no pending work remains.
|
|
658
|
+
while (this.pending.size > 0 || this.hasQueued()) {
|
|
659
|
+
await Promise.allSettled(Array.from(this.pending));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
hasQueued() {
|
|
663
|
+
for (const q of this.queues.values())
|
|
664
|
+
if (q.length > 0)
|
|
665
|
+
return true;
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
emitSkipped(req, reason) {
|
|
669
|
+
const msg = {
|
|
670
|
+
type: 'file.summary_skipped',
|
|
671
|
+
projectId: req.projectId,
|
|
672
|
+
path: req.relPath,
|
|
673
|
+
reason,
|
|
674
|
+
};
|
|
675
|
+
this.deps.broadcast(msg);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
exports.FileSummaryManager = FileSummaryManager;
|
|
679
|
+
function buildSummaryUpdated(projectId, payload, stale) {
|
|
680
|
+
const msg = {
|
|
681
|
+
type: 'file.summary_updated',
|
|
682
|
+
projectId,
|
|
683
|
+
path: payload.path,
|
|
684
|
+
summaryAvailable: true,
|
|
685
|
+
stale,
|
|
686
|
+
generatedAt: payload.generatedAt,
|
|
687
|
+
};
|
|
688
|
+
return msg;
|
|
689
|
+
}
|