specrails-desktop 2.2.1 → 2.4.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/client/dist/assets/ActivityFeedPage-DJJlZ3mF.js +1 -0
- package/client/dist/assets/AgentsPage-49JaEDjR.js +86 -0
- package/client/dist/assets/{AnalyticsPage-BD0paa75.js → AnalyticsPage-BUd3gWYC.js} +1 -1
- package/client/dist/assets/{BarChart-D8ZZRab3.js → BarChart-HDe_YoUD.js} +1 -1
- package/client/dist/assets/CodePage-CqPPND47.js +2 -0
- package/client/dist/assets/{DesktopAnalyticsPage-mwd8460_.js → DesktopAnalyticsPage-CgvmSvF0.js} +1 -1
- package/client/dist/assets/DocsDialog-hHFd3Ejs.js +11 -0
- package/client/dist/assets/DocsPage-B4R1aksg.js +11 -0
- package/client/dist/assets/{ExportDropdown-CLYmQhic.js → ExportDropdown-f4dwQjlT.js} +1 -1
- package/client/dist/assets/IntegrationsPage-CX2Ybxx0.js +3 -0
- package/client/dist/assets/JobDetailPage-DN2Jc8Ti.js +16 -0
- package/client/dist/assets/JobsPage-DmdpqijT.js +1 -0
- package/client/dist/assets/code-BwIz8agY.js +1 -0
- package/client/dist/assets/code-CD7yNSK0.js +1 -0
- package/client/dist/assets/code-CDFlxUFC.js +1 -0
- package/client/dist/assets/code-CY85RXZU.js +1 -0
- package/client/dist/assets/code-Cp3Fdng-.js +1 -0
- package/client/dist/assets/code-D24e1Crx.js +1 -0
- package/client/dist/assets/code-DtZBQTi9.js +1 -0
- package/client/dist/assets/code-nKa0fkm_.js +1 -0
- package/client/dist/assets/{cssMode-Cc6ozl-J.js → cssMode-DzNPAYFh.js} +1 -1
- package/client/dist/assets/{dist-js-D3MxtOYa.js → dist-js-COfIfLRE.js} +1 -1
- package/client/dist/assets/{dist-js-BOu_cXw3.js → dist-js-CvScGQU_.js} +1 -1
- package/client/dist/assets/{editor.main-CfXxHimg.js → editor.main-C7Rmw-hR.js} +2 -2
- package/client/dist/assets/{freemarker2-DP7J1gG3.js → freemarker2-Cszs4SVo.js} +1 -1
- package/client/dist/assets/{handlebars-BjRlucw6.js → handlebars-Dp7Lsuym.js} +1 -1
- package/client/dist/assets/{html-OumBQJ-U.js → html-BURidrEm.js} +1 -1
- package/client/dist/assets/{htmlMode-CStc3zXM.js → htmlMode--k5M7GjZ.js} +1 -1
- package/client/dist/assets/index-DBpvYrDK.css +2 -0
- package/client/dist/assets/index-DGIXKRHE.js +142 -0
- package/client/dist/assets/{integrations-Cublz3m6.js → integrations-2C7MkGT0.js} +1 -1
- package/client/dist/assets/{integrations-HIlUxXVs.js → integrations-BDC670cg.js} +1 -1
- package/client/dist/assets/integrations-BqUmRUef.js +1 -0
- package/client/dist/assets/{integrations-DmQYCUvN.js → integrations-C2jQtv-s.js} +1 -1
- package/client/dist/assets/{integrations-DRdbki5W.js → integrations-CB98NeH5.js} +1 -1
- package/client/dist/assets/{integrations-C3p12Ms6.js → integrations-CX4p_bij.js} +1 -1
- package/client/dist/assets/{integrations-DaC4SzzL.js → integrations-_SuVeQIG.js} +1 -1
- package/client/dist/assets/{integrations-Cr6hH7XR.js → integrations-eQPHAYsE.js} +1 -1
- package/client/dist/assets/{javascript-CMk--e7g.js → javascript-kJQz__44.js} +1 -1
- package/client/dist/assets/jira-C-ATCti0.js +1 -0
- package/client/dist/assets/jira-CmVfRM-b.js +1 -0
- package/client/dist/assets/jira-D7bkKAX8.js +1 -0
- package/client/dist/assets/jira-DKImM1YH.js +1 -0
- package/client/dist/assets/jira-DOw8bkIR.js +1 -0
- package/client/dist/assets/jira-DlA-wGp-.js +1 -0
- package/client/dist/assets/jira-Fob8EGxN.js +1 -0
- package/client/dist/assets/jira-xZA2lixb.js +1 -0
- package/client/dist/assets/{jsonMode-C2h3ZcjZ.js → jsonMode-v5JYPpnz.js} +1 -1
- package/client/dist/assets/{lib-DQ2hrj8m.js → lib-Bro9Z0gp.js} +1 -1
- package/client/dist/assets/{liquid-mI3KJrBE.js → liquid-Dl9I6gWt.js} +1 -1
- package/client/dist/assets/{lspLanguageFeatures-DU09ggWi.js → lspLanguageFeatures-CPlEe0NK.js} +1 -1
- package/client/dist/assets/{mdx-C41VDTR_.js → mdx-Byl7TtzQ.js} +1 -1
- package/client/dist/assets/{monaco.contribution-CPObAXMC.js → monaco.contribution-YMAkHQcQ.js} +2 -2
- package/client/dist/assets/{python-Y27rKQtk.js → python-jWQwT6j2.js} +1 -1
- package/client/dist/assets/{razor-Cd5-q9Bp.js → razor-BWS3sP-E.js} +1 -1
- package/client/dist/assets/setup-C0dzw8j4.js +1 -0
- package/client/dist/assets/setup-C1IA-9YS.js +1 -0
- package/client/dist/assets/setup-CpfjaNut.js +1 -0
- package/client/dist/assets/setup-D3rNZA9A.js +1 -0
- package/client/dist/assets/setup-UD2aanGs.js +1 -0
- package/client/dist/assets/setup-WP6WOYQh.js +1 -0
- package/client/dist/assets/setup-gzLG8T6F.js +1 -0
- package/client/dist/assets/setup-pjgmYHx6.js +1 -0
- package/client/dist/assets/specs-4lA_u79w.js +1 -0
- package/client/dist/assets/{specs-D2FzlLn9.js → specs-BHjxcjOf.js} +1 -1
- package/client/dist/assets/{specs-CZ1PsXsC.js → specs-CXNQzPk9.js} +1 -1
- package/client/dist/assets/{specs-Dyc5hYeE.js → specs-DFnc2Huj.js} +1 -1
- package/client/dist/assets/{specs-BFfu3u-a.js → specs-DZCLH2-l.js} +1 -1
- package/client/dist/assets/{specs-B__C8-8a.js → specs-DgmyAE3N.js} +1 -1
- package/client/dist/assets/{specs-DaUTrNF9.js → specs-DicWhvwi.js} +1 -1
- package/client/dist/assets/{specs-k0PyLDVt.js → specs-dkro6lSM.js} +1 -1
- package/client/dist/assets/{tsMode-B0y_xEci.js → tsMode-BbOGOuSV.js} +1 -1
- package/client/dist/assets/{typescript-BzK0OgwW.js → typescript-eBtFQJLs.js} +1 -1
- package/client/dist/assets/{useProjectCache-BxY4aTjd.js → useProjectCache-D9juBhsO.js} +1 -1
- package/client/dist/assets/{workers-rt--R2Qy.js → workers-BvicOoDf.js} +1 -1
- package/client/dist/assets/{xml-eX9QXAmI.js → xml-BJepAPyM.js} +1 -1
- package/client/dist/assets/{yaml-fcsNkpOt.js → yaml-DabgV-eA.js} +1 -1
- package/client/dist/index.html +13 -12
- package/docs/jira-integration-plan.md +321 -0
- package/package.json +1 -1
- package/server/dist/agent-refine-manager.js +128 -153
- package/server/dist/chat-manager.js +242 -0
- package/server/dist/code-explorer-router.js +78 -0
- package/server/dist/command-resolver.js +17 -0
- package/server/dist/contract-refine-runner.js +42 -10
- package/server/dist/db.js +86 -0
- package/server/dist/desktop-db.js +3 -0
- package/server/dist/explore-stdin-session.js +129 -0
- package/server/dist/feature-flags.js +11 -0
- package/server/dist/jira/jira-adf.js +113 -0
- package/server/dist/jira/jira-backlog-config.js +58 -0
- package/server/dist/jira/jira-client.js +279 -0
- package/server/dist/jira/jira-credential-store.js +103 -0
- package/server/dist/jira/jira-db.js +341 -0
- package/server/dist/jira/jira-issue-fields.js +428 -0
- package/server/dist/jira/jira-materializer.js +250 -0
- package/server/dist/jira/jira-status-resolver.js +211 -0
- package/server/dist/jira/jira-sync-manager.js +1014 -0
- package/server/dist/jira/types.js +9 -0
- package/server/dist/jira-router.js +304 -0
- package/server/dist/mobile/mobile-auth.js +16 -0
- package/server/dist/project-registry.js +43 -1
- package/server/dist/project-router-chat.js +218 -0
- package/server/dist/project-router-helpers.js +275 -0
- package/server/dist/project-router-jobs.js +389 -0
- package/server/dist/project-router-settings.js +312 -0
- package/server/dist/project-router-setup.js +456 -0
- package/server/dist/project-router-spending.js +320 -0
- package/server/dist/project-router-terminals.js +312 -0
- package/server/dist/project-router-tickets.js +1815 -0
- package/server/dist/project-router.js +31 -3950
- package/server/dist/providers/claude-adapter.js +23 -0
- package/server/dist/providers/codex-adapter.js +6 -0
- package/server/dist/rails-router.js +12 -0
- package/server/dist/spawn-lifecycle.js +117 -0
- package/client/dist/assets/ActivityFeedPage-BpjXuX2H.js +0 -1
- package/client/dist/assets/AgentsPage-D-7fDbTc.js +0 -86
- package/client/dist/assets/CodePage-B6q6CiYJ.js +0 -2
- package/client/dist/assets/DocsDialog-D_dyF2h9.js +0 -11
- package/client/dist/assets/DocsPage-C9-Ru8wE.js +0 -11
- package/client/dist/assets/IntegrationsPage-3WWtx9hi.js +0 -3
- package/client/dist/assets/JobDetailPage-DgN-79s-.js +0 -16
- package/client/dist/assets/JobsPage-Du8_w1ob.js +0 -1
- package/client/dist/assets/code-AL1rVIMb.js +0 -1
- package/client/dist/assets/code-C0BKpkht.js +0 -1
- package/client/dist/assets/code-C0FTS3ew.js +0 -1
- package/client/dist/assets/code-CPcHxzxw.js +0 -1
- package/client/dist/assets/code-D3ryDniw.js +0 -1
- package/client/dist/assets/code-D3zVVQTj.js +0 -1
- package/client/dist/assets/code-PCmfS3dn.js +0 -1
- package/client/dist/assets/code-exI0G5Wd.js +0 -1
- package/client/dist/assets/index-D17R4Cjc.css +0 -2
- package/client/dist/assets/index-D9G_K4L-.js +0 -142
- package/client/dist/assets/integrations-D28q1kF6.js +0 -1
- package/client/dist/assets/setup--FMCsnQS.js +0 -1
- package/client/dist/assets/setup-B19ZpBNi.js +0 -1
- package/client/dist/assets/setup-BZPmkjSN.js +0 -1
- package/client/dist/assets/setup-BqYA02rS.js +0 -1
- package/client/dist/assets/setup-ChKQDHN9.js +0 -1
- package/client/dist/assets/setup-D2n9jMfM.js +0 -1
- package/client/dist/assets/setup-P3r6YP1D.js +0 -1
- package/client/dist/assets/setup-fnfEbwlv.js +0 -1
- package/client/dist/assets/specs-cKEh2LXt.js +0 -1
- /package/client/dist/assets/{abap-Bw6f2wDG.js → abap-s65oMlhi.js} +0 -0
- /package/client/dist/assets/{activity-BdrPln96.js → activity-BqqwnH_h.js} +0 -0
- /package/client/dist/assets/{activity-BEIp_Y1A.js → activity-C8qqEIoP.js} +0 -0
- /package/client/dist/assets/{activity-CpkRS8Sx.js → activity-CZVM4nlJ.js} +0 -0
- /package/client/dist/assets/{activity-DOUVEjJi.js → activity-Cyy07Tgo.js} +0 -0
- /package/client/dist/assets/{activity-DRwkql_y.js → activity-DlbWCa4y.js} +0 -0
- /package/client/dist/assets/{activity-DKCpESPt.js → activity-Dwq0heud.js} +0 -0
- /package/client/dist/assets/{activity-DcDQ7tjw.js → activity-qFTcMyW9.js} +0 -0
- /package/client/dist/assets/{addon-image-3WCl5Vhd.js → addon-image-CpF0L0jM.js} +0 -0
- /package/client/dist/assets/{addon-ligatures-C5OdliKs.js → addon-ligatures-hXysGZrA.js} +0 -0
- /package/client/dist/assets/{addon-webgl-BbX6pSjl.js → addon-webgl-Cn1slavz.js} +0 -0
- /package/client/dist/assets/{addspec-D33ocMxf.js → addspec-B1FTtI2a.js} +0 -0
- /package/client/dist/assets/{addspec-DFswZ0jK.js → addspec-BCT9vm_c.js} +0 -0
- /package/client/dist/assets/{addspec-DVZ15Jp8.js → addspec-DeDOztDr.js} +0 -0
- /package/client/dist/assets/{addspec-Fkv91Opc.js → addspec-DpRgmfmx.js} +0 -0
- /package/client/dist/assets/{addspec-BEeF5-zc.js → addspec-Dw-0Dg-4.js} +0 -0
- /package/client/dist/assets/{addspec-B5yl4Loj.js → addspec-rp496P_F.js} +0 -0
- /package/client/dist/assets/{addspec-DRE-jZv7.js → addspec-v8j6A7CD.js} +0 -0
- /package/client/dist/assets/{agents-DK-Dlc0i.js → agents-23iPejcA.js} +0 -0
- /package/client/dist/assets/{agents-Q6Ldfpxx.js → agents-BDx1RXcl.js} +0 -0
- /package/client/dist/assets/{agents-TeOSy-ax.js → agents-BFr3kUhK.js} +0 -0
- /package/client/dist/assets/{agents-Bm9rPqnt.js → agents-B_1L9xRg.js} +0 -0
- /package/client/dist/assets/{agents-1nCDWRmP.js → agents-BlPnx-mz.js} +0 -0
- /package/client/dist/assets/{agents-iTqjRajS.js → agents-DcxZHzNr.js} +0 -0
- /package/client/dist/assets/{agents-s87sMGzL.js → agents-G3shOewU.js} +0 -0
- /package/client/dist/assets/{agentstudio-B6Wb59E7.js → agentstudio-B-CMAQqy.js} +0 -0
- /package/client/dist/assets/{agentstudio-D3I62TLJ.js → agentstudio-Bk1eZcv4.js} +0 -0
- /package/client/dist/assets/{agentstudio-DuH9TogZ.js → agentstudio-ChxNuGAu.js} +0 -0
- /package/client/dist/assets/{agentstudio-Kw88_dUF.js → agentstudio-DNlme601.js} +0 -0
- /package/client/dist/assets/{agentstudio-BdidyBzZ.js → agentstudio-DpP9caEE.js} +0 -0
- /package/client/dist/assets/{agentstudio-BSnWLR63.js → agentstudio-Y3G0ddJ2.js} +0 -0
- /package/client/dist/assets/{agentstudio-BADhZ41e.js → agentstudio-kk9RB7Se.js} +0 -0
- /package/client/dist/assets/{aiedit-DJMny-D5.js → aiedit-5ETerMK1.js} +0 -0
- /package/client/dist/assets/{aiedit-D2ji6Qy0.js → aiedit-BBCrOpHq.js} +0 -0
- /package/client/dist/assets/{aiedit-DAhZTvtk.js → aiedit-BMtcGYNE.js} +0 -0
- /package/client/dist/assets/{aiedit-DvrcbwGv.js → aiedit-D9ddlgkM.js} +0 -0
- /package/client/dist/assets/{aiedit-WBSjT_C1.js → aiedit-De0SOH6S.js} +0 -0
- /package/client/dist/assets/{aiedit-BWxHGsYA.js → aiedit-DrfzQroF.js} +0 -0
- /package/client/dist/assets/{aiedit-DOcxERkU.js → aiedit-fMltW101.js} +0 -0
- /package/client/dist/assets/{analytics-C9Zc-rkM.js → analytics-BeTyviO8.js} +0 -0
- /package/client/dist/assets/{analytics-CrPCZRJ-.js → analytics-C4eEO260.js} +0 -0
- /package/client/dist/assets/{analytics-CYj0tfj7.js → analytics-C67cIA1b.js} +0 -0
- /package/client/dist/assets/{analytics-C6EzgtdE.js → analytics-CAguvW28.js} +0 -0
- /package/client/dist/assets/{analytics-CVx3YOc0.js → analytics-DBtt8Mgk.js} +0 -0
- /package/client/dist/assets/{analytics-CnY4kNG3.js → analytics-DUPtODxX.js} +0 -0
- /package/client/dist/assets/{analytics-BIdr0YfL.js → analytics-YIpQvPAc.js} +0 -0
- /package/client/dist/assets/{apex-Cw8_REBo.js → apex-BLUBIldB.js} +0 -0
- /package/client/dist/assets/{attachments-DYHGA2Dj.js → attachments-CCWasu-P.js} +0 -0
- /package/client/dist/assets/{attachments-Dd92KpUH.js → attachments-CHaDUfjB.js} +0 -0
- /package/client/dist/assets/{attachments-DzdU6DV6.js → attachments-CVSAbGNl.js} +0 -0
- /package/client/dist/assets/{attachments-Bcf6BG6V.js → attachments-Chg5poG1.js} +0 -0
- /package/client/dist/assets/{attachments-BW4L3l2L.js → attachments-DazTVJbH.js} +0 -0
- /package/client/dist/assets/{attachments-COcrGRFz.js → attachments-Dn-JImAK.js} +0 -0
- /package/client/dist/assets/{attachments-Bke8sCU4.js → attachments-LDA9kp2X.js} +0 -0
- /package/client/dist/assets/{azcli-Cz6HAoOw.js → azcli-DuWxh9mO.js} +0 -0
- /package/client/dist/assets/{bat-CcJ-xyqL.js → bat-UKoTejQm.js} +0 -0
- /package/client/dist/assets/{bicep-z1WDCKYz.js → bicep-4sTT4B3D.js} +0 -0
- /package/client/dist/assets/{browser-DGITz3fC.js → browser-BDd1dbFa.js} +0 -0
- /package/client/dist/assets/{browser-JsAIGCEW.js → browser-BWSgbfdX.js} +0 -0
- /package/client/dist/assets/{browser-M5-rbPlw.js → browser-D2Y_UAKA.js} +0 -0
- /package/client/dist/assets/{browser-BlYF4OOq.js → browser-DH9SGVfM.js} +0 -0
- /package/client/dist/assets/{browser-Bc-YdlVg.js → browser-DWOVYMlg.js} +0 -0
- /package/client/dist/assets/{browser-CT-ReZGt.js → browser-Dxc_VIRK.js} +0 -0
- /package/client/dist/assets/{browser-5ErDlJoR.js → browser-lTQwcDCI.js} +0 -0
- /package/client/dist/assets/{cameligo-BRewOpfa.js → cameligo-CAAryRYO.js} +0 -0
- /package/client/dist/assets/{chat-DwUm6W9z.js → chat-BO9MvVID.js} +0 -0
- /package/client/dist/assets/{chat-BEGuC03z.js → chat-CPgmgZOj.js} +0 -0
- /package/client/dist/assets/{chat-CboQguCi.js → chat-CUrG1eVg.js} +0 -0
- /package/client/dist/assets/{chat-DRCa9pOt.js → chat-CvOOKB2s.js} +0 -0
- /package/client/dist/assets/{chat-BEW60P_u.js → chat-DIh3hr6y.js} +0 -0
- /package/client/dist/assets/{chat-yoXwguQu.js → chat-UVVZqA57.js} +0 -0
- /package/client/dist/assets/{chat-BQNMD0PL.js → chat-mPn3UlMl.js} +0 -0
- /package/client/dist/assets/{clojure-DBjRWN6g.js → clojure-BlMERO1w.js} +0 -0
- /package/client/dist/assets/{clsx-DnqN-uhr.js → clsx-CnH-HMk3.js} +0 -0
- /package/client/dist/assets/{coffee-Cfk_XHGR.js → coffee-Cj8D-Wl1.js} +0 -0
- /package/client/dist/assets/{commands-sqrqsxyE.js → commands-B-MVT-2F.js} +0 -0
- /package/client/dist/assets/{commands-UD1NzmwX.js → commands-B0yFTp7e.js} +0 -0
- /package/client/dist/assets/{commands-DLrvnPNg.js → commands-BR1kDkHQ.js} +0 -0
- /package/client/dist/assets/{commands-CJxCry-o.js → commands-Cb21pDlG.js} +0 -0
- /package/client/dist/assets/{commands-CfgY-_of.js → commands-DWgp-8W1.js} +0 -0
- /package/client/dist/assets/{commands-B772IyDa.js → commands-ddsl1V91.js} +0 -0
- /package/client/dist/assets/{commands-BDDp6xFG.js → commands-t4frzhB0.js} +0 -0
- /package/client/dist/assets/{common-Dmm1GhdD.js → common-5ilvMOcH.js} +0 -0
- /package/client/dist/assets/{common-DltqHaAe.js → common-B4sqsKp7.js} +0 -0
- /package/client/dist/assets/{common-GbpxfPG8.js → common-BKpVwUIf.js} +0 -0
- /package/client/dist/assets/{common-DeDELLZJ.js → common-BzEC3kJU.js} +0 -0
- /package/client/dist/assets/{common-DnjcgkPH.js → common-CALKUpYm.js} +0 -0
- /package/client/dist/assets/{common-Dard9UNH.js → common-CTEbWVZS.js} +0 -0
- /package/client/dist/assets/{common-DCr6VzJ7.js → common-DQiza2Xp.js} +0 -0
- /package/client/dist/assets/{cpp-BVob6BaP.js → cpp-BPfKnaj_.js} +0 -0
- /package/client/dist/assets/{csharp-C4fbRuOu.js → csharp-gX-x5uD6.js} +0 -0
- /package/client/dist/assets/{csp-DthFP_vT.js → csp-DKGVt8SM.js} +0 -0
- /package/client/dist/assets/{css-CGMH0hcW.js → css-CPMdnAVq.js} +0 -0
- /package/client/dist/assets/{cypher-Pnf68BRV.js → cypher-ClMDrj9S.js} +0 -0
- /package/client/dist/assets/{dart-PMMOtxZX.js → dart-C4zbzpVv.js} +0 -0
- /package/client/dist/assets/{dashboard-BZBADHSj.js → dashboard--Y6yzMlf.js} +0 -0
- /package/client/dist/assets/{dashboard-I19DXBxw.js → dashboard--a4-6oYE.js} +0 -0
- /package/client/dist/assets/{dashboard-CB6Le1yN.js → dashboard-BiJ3CDTG.js} +0 -0
- /package/client/dist/assets/{dashboard-B4ixDVk8.js → dashboard-CiXjk63Z.js} +0 -0
- /package/client/dist/assets/{dashboard-C1MfeUHs.js → dashboard-Cx5VjCea.js} +0 -0
- /package/client/dist/assets/{dashboard-C7SK6xu5.js → dashboard-D7jg25XR.js} +0 -0
- /package/client/dist/assets/{dashboard-CoTpMOBM.js → dashboard-DpGYK2s1.js} +0 -0
- /package/client/dist/assets/{dockerfile-di1nsJCc.js → dockerfile-D9xw73D1.js} +0 -0
- /package/client/dist/assets/{ecl-D_WVcB5M.js → ecl-gqO8tIR9.js} +0 -0
- /package/client/dist/assets/{editor.api2-XLGzZfbc.js → editor.api2-BPnIxMjz.js} +0 -0
- /package/client/dist/assets/{elixir-OAdJEMOn.js → elixir-DSAhVF3_.js} +0 -0
- /package/client/dist/assets/{explore-D2EFgt8J.js → explore-BE5UmlbD.js} +0 -0
- /package/client/dist/assets/{explore-BV5Xxlsn.js → explore-BmTaI8dX.js} +0 -0
- /package/client/dist/assets/{explore-A8Ltoblq.js → explore-CCwkqoWq.js} +0 -0
- /package/client/dist/assets/{explore-4mFpnrKU.js → explore-CMdEoPDx.js} +0 -0
- /package/client/dist/assets/{explore-C3FSE42C.js → explore-CtdCL4QU.js} +0 -0
- /package/client/dist/assets/{explore-B9A3iN2W.js → explore-DHjxSkqQ.js} +0 -0
- /package/client/dist/assets/{explore-BrBJvfjP.js → explore-__BeALjE.js} +0 -0
- /package/client/dist/assets/{flow9-D3QEZjgn.js → flow9-DeQCSPOd.js} +0 -0
- /package/client/dist/assets/{format-command-CwGuwzGA.js → format-command-2VNoNnMv.js} +0 -0
- /package/client/dist/assets/{fsharp-BF0k_8N8.js → fsharp-CEfaXL-S.js} +0 -0
- /package/client/dist/assets/{go-BAQO5Jsz.js → go-Xp1OkZCh.js} +0 -0
- /package/client/dist/assets/{graphql-hdFVFkiV.js → graphql-BwRXrUwe.js} +0 -0
- /package/client/dist/assets/{hcl-DWnl1o-X.js → hcl-u06DtVFk.js} +0 -0
- /package/client/dist/assets/{ini-CB-6OVu3.js → ini-AmeIpFND.js} +0 -0
- /package/client/dist/assets/{java-d1CmfiHX.js → java-CyDbRQjX.js} +0 -0
- /package/client/dist/assets/{jobs-DPPT6bV6.js → jobs-8viuHLDV.js} +0 -0
- /package/client/dist/assets/{jobs-3j3_npyo.js → jobs-AW2eB5D-.js} +0 -0
- /package/client/dist/assets/{jobs-2N3RXDAM.js → jobs-BSm89DL5.js} +0 -0
- /package/client/dist/assets/{jobs-BqEbCCxD.js → jobs-BZ3sQHjZ.js} +0 -0
- /package/client/dist/assets/{jobs-cHYInoau.js → jobs-Bd8AdOTb.js} +0 -0
- /package/client/dist/assets/{jobs-DRzjWI9u.js → jobs-CRtsq_u0.js} +0 -0
- /package/client/dist/assets/{jobs-2f6Hdc72.js → jobs-CSRwFQ6K.js} +0 -0
- /package/client/dist/assets/{jobs-vGfzIDQa.js → jobs-CbEl7WMI.js} +0 -0
- /package/client/dist/assets/{julia-Bgv08lKa.js → julia-BqialFRG.js} +0 -0
- /package/client/dist/assets/{kotlin-u98kaVTf.js → kotlin-Dzz8TWAt.js} +0 -0
- /package/client/dist/assets/{less-CjYwpgg5.js → less-DHRJD3TR.js} +0 -0
- /package/client/dist/assets/{lexon-YTjaAFBB.js → lexon-5Y3QgTmT.js} +0 -0
- /package/client/dist/assets/{lua-BzmkWv27.js → lua-sKvhfPn5.js} +0 -0
- /package/client/dist/assets/{m3-CFwk9fw0.js → m3-DWDVwkFG.js} +0 -0
- /package/client/dist/assets/{markdown-CR5iMpSZ.js → markdown-CD_aSBxW.js} +0 -0
- /package/client/dist/assets/{mips-CcEalc17.js → mips-687T03hg.js} +0 -0
- /package/client/dist/assets/{msdax-BQbkawnr.js → msdax-C1St-dIV.js} +0 -0
- /package/client/dist/assets/{mysql-GTlaaW_P.js → mysql-BG7r8oBS.js} +0 -0
- /package/client/dist/assets/{nav-C2YXcbZS.js → nav-B05EYB0b.js} +0 -0
- /package/client/dist/assets/{nav-D2bOGSEg.js → nav-BNGCq-0y.js} +0 -0
- /package/client/dist/assets/{nav-BEL3MTwK.js → nav-BRInPX8a.js} +0 -0
- /package/client/dist/assets/{nav-CtYwmMgu.js → nav-Bf87DRHD.js} +0 -0
- /package/client/dist/assets/{nav-iH1V5j6o.js → nav-BkVzzFpc.js} +0 -0
- /package/client/dist/assets/{nav-0fwkrgHt.js → nav-BzFLtS1W.js} +0 -0
- /package/client/dist/assets/{nav-ClzOE4mA.js → nav-DBDbQOYn.js} +0 -0
- /package/client/dist/assets/{nav-B_G-TJDW.js → nav-X9sVtUWC.js} +0 -0
- /package/client/dist/assets/{objective-c-Byu1T5if.js → objective-c-Ds1-m05L.js} +0 -0
- /package/client/dist/assets/{pascal-BrfzBfRm.js → pascal-BKK9FpIi.js} +0 -0
- /package/client/dist/assets/{pascaligo-BXXKFUeo.js → pascaligo-SRS3nwtO.js} +0 -0
- /package/client/dist/assets/{perl-B3OikKq-.js → perl-B2hTOlrF.js} +0 -0
- /package/client/dist/assets/{pgsql-CTsa0Acc.js → pgsql-DIQJYNpL.js} +0 -0
- /package/client/dist/assets/{php-DiQh3FUW.js → php-BEaVe8X2.js} +0 -0
- /package/client/dist/assets/{pla-92uH8Fzm.js → pla-oPLHpZ-Q.js} +0 -0
- /package/client/dist/assets/{postiats-BbeWkKUr.js → postiats-D_vzrAzD.js} +0 -0
- /package/client/dist/assets/{powerquery-DgDMzpsm.js → powerquery-BKG6w-FH.js} +0 -0
- /package/client/dist/assets/{powershell-BfdUUzaG.js → powershell-B3dLhDt4.js} +0 -0
- /package/client/dist/assets/{protobuf-BojW2ftW.js → protobuf-DC8SGjcl.js} +0 -0
- /package/client/dist/assets/{pug-BxqTg3IU.js → pug-D5E-4fI0.js} +0 -0
- /package/client/dist/assets/{qsharp-BX_A-MW9.js → qsharp-6vJAWv0x.js} +0 -0
- /package/client/dist/assets/{r-D9BMnxvJ.js → r-CDwsEcbM.js} +0 -0
- /package/client/dist/assets/{redis-5cJqEQJJ.js → redis-CuQbbESS.js} +0 -0
- /package/client/dist/assets/{redshift-d8BBqiwb.js → redshift-B9e1k-qI.js} +0 -0
- /package/client/dist/assets/{restructuredtext-C8a6yIcZ.js → restructuredtext-BiJ5IwaU.js} +0 -0
- /package/client/dist/assets/{ruby-egeh-6KX.js → ruby-B0UAHY9b.js} +0 -0
- /package/client/dist/assets/{rust-a3r9IInB.js → rust-Dg_spmFr.js} +0 -0
- /package/client/dist/assets/{sb-y8iRIDei.js → sb-DjU66I8Q.js} +0 -0
- /package/client/dist/assets/{scala-BPDK2AmK.js → scala-qvStIdfG.js} +0 -0
- /package/client/dist/assets/{scheme-BIWUEoOs.js → scheme-FstEk5Rh.js} +0 -0
- /package/client/dist/assets/{scss-CA-PSzwg.js → scss-w0U3rQLK.js} +0 -0
- /package/client/dist/assets/{settings-CTcwN9RE.js → settings-5tzo0Rn3.js} +0 -0
- /package/client/dist/assets/{settings-D_dujJZI.js → settings-BDAW3trC.js} +0 -0
- /package/client/dist/assets/{settings-Bg0A3zoS.js → settings-BEWv3VEu.js} +0 -0
- /package/client/dist/assets/{settings-BgPqg2nv.js → settings-BORg56um.js} +0 -0
- /package/client/dist/assets/{settings-BSze3_9q.js → settings-D3LurcR5.js} +0 -0
- /package/client/dist/assets/{settings-CSJ0ahZ8.js → settings-DcqWIEM6.js} +0 -0
- /package/client/dist/assets/{settings-DYIV89nV.js → settings-Dfz8QbZS.js} +0 -0
- /package/client/dist/assets/{settings-DDcfx_ca.js → settings-yMubjqYw.js} +0 -0
- /package/client/dist/assets/{shell--LiT1Bja.js → shell-DJ78wREd.js} +0 -0
- /package/client/dist/assets/{solidity-DdqZccZg.js → solidity-1aGIVsdX.js} +0 -0
- /package/client/dist/assets/{sophia-S6-YxNG_.js → sophia-40LqcGjB.js} +0 -0
- /package/client/dist/assets/{sparql-BSf5kMp2.js → sparql-Cz5dqG_g.js} +0 -0
- /package/client/dist/assets/{sql-D7KgjR8G.js → sql-64f62Ni4.js} +0 -0
- /package/client/dist/assets/{st-BnoDa-Ml.js → st-gJe2yG8J.js} +0 -0
- /package/client/dist/assets/{swift-DEUHTkUX.js → swift-C6ME22mv.js} +0 -0
- /package/client/dist/assets/{systemverilog-Tqb_KPnW.js → systemverilog-CEWz259w.js} +0 -0
- /package/client/dist/assets/{tcl-BmBFS2qq.js → tcl-CcLVIi3m.js} +0 -0
- /package/client/dist/assets/{terminal-Bje4ziIa.js → terminal-BYtreaaF.js} +0 -0
- /package/client/dist/assets/{terminal-CSONJOex.js → terminal-C0xx0SjA.js} +0 -0
- /package/client/dist/assets/{terminal-DeWzh6ys.js → terminal-CPpK58RC.js} +0 -0
- /package/client/dist/assets/{terminal-C2WYcFHF.js → terminal-CdxkpafL.js} +0 -0
- /package/client/dist/assets/{terminal-DEqzGtcr.js → terminal-Ciia0wh2.js} +0 -0
- /package/client/dist/assets/{terminal-80yDMgMF.js → terminal-DHIkiWcs.js} +0 -0
- /package/client/dist/assets/{terminal-lkZYR4wJ.js → terminal-DY42QANg.js} +0 -0
- /package/client/dist/assets/{terminal-YOlsJCQj.js → terminal-DoxtVdma.js} +0 -0
- /package/client/dist/assets/{tickets-DYvafSaY.js → tickets-0rM0lIXd.js} +0 -0
- /package/client/dist/assets/{tickets-DNOANUXr.js → tickets-1UIGf_oA.js} +0 -0
- /package/client/dist/assets/{tickets-DlpC_iTg.js → tickets-9kdPXInd.js} +0 -0
- /package/client/dist/assets/{tickets-CF2PYelu.js → tickets-C6pwZwt4.js} +0 -0
- /package/client/dist/assets/{tickets-CB7N30gm.js → tickets-DAjtxAVb.js} +0 -0
- /package/client/dist/assets/{tickets-DU1aqsbr.js → tickets-DNmXcAwu.js} +0 -0
- /package/client/dist/assets/{tickets-clefmXLv.js → tickets-n23kDqJT.js} +0 -0
- /package/client/dist/assets/{tickets-DucYgtdl.js → tickets-tGx5AR5b.js} +0 -0
- /package/client/dist/assets/{twig-BQV8igWC.js → twig-DvsO-WjW.js} +0 -0
- /package/client/dist/assets/{typespec-DlFroUGY.js → typespec-Brkt3IAA.js} +0 -0
- /package/client/dist/assets/{vb-BlrJpIMX.js → vb-r121Uzxt.js} +0 -0
- /package/client/dist/assets/{wgsl-BWgIc6FZ.js → wgsl-BRX8uYh4.js} +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Shared helpers and the deps contract for the project-router domain modules.
|
|
3
|
+
// Extracted verbatim from project-router.ts when that monolith was split into
|
|
4
|
+
// per-domain register functions. Behaviour-preserving — same exports, same code.
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.VALID_MODEL_ALIASES = exports.TERMINAL_PANEL_ENABLED = void 0;
|
|
10
|
+
exports.stripSpecMetadataSections = stripSpecMetadataSections;
|
|
11
|
+
exports.extractShortSummary = extractShortSummary;
|
|
12
|
+
exports.deriveFallbackShortSummary = deriveFallbackShortSummary;
|
|
13
|
+
exports.lightlyStructurePrompt = lightlyStructurePrompt;
|
|
14
|
+
exports.formatDescriptionWithCriteria = formatDescriptionWithCriteria;
|
|
15
|
+
exports.resolveDefaultSpecModel = resolveDefaultSpecModel;
|
|
16
|
+
exports.readAgentModels = readAgentModels;
|
|
17
|
+
exports.applyModelConfig = applyModelConfig;
|
|
18
|
+
exports.serializeInstallConfigYaml = serializeInstallConfigYaml;
|
|
19
|
+
const fs_1 = __importDefault(require("fs"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
const core_package_1 = require("./core-package");
|
|
22
|
+
const spec_models_1 = require("./spec-models");
|
|
23
|
+
const ticket_store_1 = require("./ticket-store");
|
|
24
|
+
const TERMINAL_PANEL_ENABLED = process.env.SPECRAILS_TERMINAL_PANEL !== 'false';
|
|
25
|
+
exports.TERMINAL_PANEL_ENABLED = TERMINAL_PANEL_ENABLED;
|
|
26
|
+
// ─── YAML helpers ─────────────────────────────────────────────────────────────
|
|
27
|
+
/**
|
|
28
|
+
* Serialize install config to YAML matching specrails-core's tui-installer format exactly.
|
|
29
|
+
*/
|
|
30
|
+
function serializeInstallConfigYaml(config) {
|
|
31
|
+
const c = config;
|
|
32
|
+
const overrides = c.models?.overrides ?? {};
|
|
33
|
+
const overridesEntries = Object.entries(overrides);
|
|
34
|
+
const overridesYaml = overridesEntries.length > 0
|
|
35
|
+
? '\n' + overridesEntries.map(([k, v]) => ` ${k}: ${v}`).join('\n')
|
|
36
|
+
: ' {}';
|
|
37
|
+
const lines = [
|
|
38
|
+
'# specrails install config — generated by Specrails',
|
|
39
|
+
`# Re-run: npx ${core_package_1.CORE_PACKAGE_SPEC} init to regenerate`,
|
|
40
|
+
`version: ${c.version ?? 1}`,
|
|
41
|
+
`provider: ${c.provider ?? 'claude'}`,
|
|
42
|
+
`tier: ${c.tier ?? 'quick'}`,
|
|
43
|
+
`agents:`,
|
|
44
|
+
` selected: [${(c.agents?.selected ?? []).join(', ')}]`,
|
|
45
|
+
` excluded: [${(c.agents?.excluded ?? []).join(', ')}]`,
|
|
46
|
+
`models:`,
|
|
47
|
+
` preset: ${c.models?.preset ?? 'balanced'}`,
|
|
48
|
+
` defaults: { model: ${c.models?.defaults?.model ?? 'sonnet'} }`,
|
|
49
|
+
` overrides:${overridesYaml}`,
|
|
50
|
+
`agent_teams: ${c.agent_teams ?? false}`,
|
|
51
|
+
'',
|
|
52
|
+
];
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
// ─── Agent model helpers ──────────────────────────────────────────────────────
|
|
56
|
+
const VALID_MODEL_ALIASES = ['sonnet', 'opus', 'haiku'];
|
|
57
|
+
exports.VALID_MODEL_ALIASES = VALID_MODEL_ALIASES;
|
|
58
|
+
/**
|
|
59
|
+
* Read installed agents from `.claude/agents/*.md` (top-level only, no subdirs).
|
|
60
|
+
* Extracts the `model:` field from YAML frontmatter.
|
|
61
|
+
*/
|
|
62
|
+
function readAgentModels(projectPath) {
|
|
63
|
+
const agentsDir = path_1.default.join(projectPath, '.claude', 'agents');
|
|
64
|
+
if (!fs_1.default.existsSync(agentsDir))
|
|
65
|
+
return [];
|
|
66
|
+
let entries;
|
|
67
|
+
try {
|
|
68
|
+
entries = fs_1.default.readdirSync(agentsDir);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const agents = [];
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
// Top-level .md files only — skip subdirs
|
|
76
|
+
if (!entry.endsWith('.md'))
|
|
77
|
+
continue;
|
|
78
|
+
const filePath = path_1.default.join(agentsDir, entry);
|
|
79
|
+
try {
|
|
80
|
+
const stat = fs_1.default.statSync(filePath);
|
|
81
|
+
if (!stat.isFile())
|
|
82
|
+
continue;
|
|
83
|
+
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
84
|
+
// Extract model from YAML frontmatter between --- markers
|
|
85
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
86
|
+
if (!frontmatterMatch)
|
|
87
|
+
continue;
|
|
88
|
+
const frontmatter = frontmatterMatch[1];
|
|
89
|
+
const modelMatch = frontmatter.match(/^model:\s*(.+)$/m);
|
|
90
|
+
const model = modelMatch ? modelMatch[1].trim() : 'sonnet';
|
|
91
|
+
agents.push({ name: entry.slice(0, -3), model });
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// skip unreadable files
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return agents;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Read `.specrails/install-config.yaml` and patch the `model:` line in each
|
|
101
|
+
* `.claude/agents/*.md` frontmatter to match the config's defaults/overrides.
|
|
102
|
+
* No-op if the config file does not exist.
|
|
103
|
+
*/
|
|
104
|
+
function applyModelConfig(projectPath) {
|
|
105
|
+
const configPath = path_1.default.join(projectPath, '.specrails', 'install-config.yaml');
|
|
106
|
+
if (!fs_1.default.existsSync(configPath))
|
|
107
|
+
return;
|
|
108
|
+
let configText;
|
|
109
|
+
try {
|
|
110
|
+
configText = fs_1.default.readFileSync(configPath, 'utf-8');
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Parse defaults.model
|
|
116
|
+
const defaultsMatch = configText.match(/defaults:\s*\{\s*model:\s*(\S+)\s*\}/);
|
|
117
|
+
const defaultModel = defaultsMatch ? defaultsMatch[1] : 'sonnet';
|
|
118
|
+
// Parse overrides block — lines like ` agentname: alias`
|
|
119
|
+
const overrides = {};
|
|
120
|
+
const overridesBlockMatch = configText.match(/overrides:([\s\S]*?)(?:\n\S|$)/);
|
|
121
|
+
if (overridesBlockMatch) {
|
|
122
|
+
const block = overridesBlockMatch[1];
|
|
123
|
+
const overrideLines = block.match(/^ {2,}(\S+):\s*(\S+)/gm) ?? [];
|
|
124
|
+
for (const line of overrideLines) {
|
|
125
|
+
const m = line.match(/^\s+(\S+):\s*(\S+)/);
|
|
126
|
+
if (m)
|
|
127
|
+
overrides[m[1]] = m[2];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const agentsDir = path_1.default.join(projectPath, '.claude', 'agents');
|
|
131
|
+
if (!fs_1.default.existsSync(agentsDir))
|
|
132
|
+
return;
|
|
133
|
+
let entries;
|
|
134
|
+
try {
|
|
135
|
+
entries = fs_1.default.readdirSync(agentsDir);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (!entry.endsWith('.md'))
|
|
142
|
+
continue;
|
|
143
|
+
const filePath = path_1.default.join(agentsDir, entry);
|
|
144
|
+
try {
|
|
145
|
+
const stat = fs_1.default.statSync(filePath);
|
|
146
|
+
if (!stat.isFile())
|
|
147
|
+
continue;
|
|
148
|
+
const agentName = entry.slice(0, -3);
|
|
149
|
+
const targetModel = overrides[agentName] ?? defaultModel;
|
|
150
|
+
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
151
|
+
const patched = content.replace(/^model: .+$/m, `model: ${targetModel}`);
|
|
152
|
+
if (patched !== content) {
|
|
153
|
+
fs_1.default.writeFileSync(filePath, patched, 'utf-8');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// skip unwritable files
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Strip the metadata sections (Spec Title, Labels, Estimated Complexity,
|
|
163
|
+
* Short Summary) from a generate-spec LLM response so they don't end up
|
|
164
|
+
* duplicated inside the ticket's description — they're already parsed into
|
|
165
|
+
* dedicated fields and re-rendered by the UI.
|
|
166
|
+
*/
|
|
167
|
+
function stripSpecMetadataSections(buffer) {
|
|
168
|
+
return buffer
|
|
169
|
+
.replace(/##\s*Spec Title\s*\n+[^\n]*\n*/i, '')
|
|
170
|
+
.replace(/##\s*Labels\s*\n+(?:[^\n]+(?:\n(?!##)[^\n]+)*)\n*/i, '')
|
|
171
|
+
.replace(/##\s*Estimated Complexity\s*\n+(?:[^\n]+(?:\n(?!##)[^\n]+)*)\n*/i, '')
|
|
172
|
+
.replace(/##\s*Short Summary\s*\n+(?:[^\n]+(?:\n(?!##)[^\n]+)*)\n*/i, '')
|
|
173
|
+
.trim();
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Extract the `## Short Summary` section body from a generate-spec response.
|
|
177
|
+
* Returns the raw multi-line string (the ticket-store `clampShortSummary`
|
|
178
|
+
* helper applies trim, control-strip, and the 240-char hard cap before
|
|
179
|
+
* persistence). Returns `null` when the section is missing or empty.
|
|
180
|
+
*/
|
|
181
|
+
function extractShortSummary(buffer) {
|
|
182
|
+
const m = buffer.match(/##\s*Short Summary\s*\n+((?:(?!##)[^\n]+(?:\n(?!##)[^\n]+)*))/i);
|
|
183
|
+
if (!m)
|
|
184
|
+
return null;
|
|
185
|
+
const body = m[1].trim();
|
|
186
|
+
return body.length > 0 ? body : null;
|
|
187
|
+
}
|
|
188
|
+
function deriveFallbackShortSummary(title, description) {
|
|
189
|
+
const plain = description
|
|
190
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
191
|
+
.replace(/^#{1,6}\s+.*$/gm, ' ')
|
|
192
|
+
.replace(/^\s*[-*]\s+/gm, '')
|
|
193
|
+
.replace(/\[[^\]]+\]\([^)]+\)/g, (m) => m.match(/\[([^\]]+)\]/)?.[1] ?? '')
|
|
194
|
+
.replace(/[`*_>]/g, '')
|
|
195
|
+
.replace(/\s+/g, ' ')
|
|
196
|
+
.trim();
|
|
197
|
+
const source = plain || title.trim();
|
|
198
|
+
if (!source)
|
|
199
|
+
return null;
|
|
200
|
+
const sentence = source.match(/^(.{24,}?[.!?])(?:\s|$)/)?.[1] ?? source;
|
|
201
|
+
const capped = sentence.length > 160 ? `${sentence.slice(0, 157).trimEnd()}...` : sentence;
|
|
202
|
+
return (0, ticket_store_1.clampShortSummary)(capped);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Lightly structure a raw free-form prompt (the "Raw" Add-Spec mode) when the
|
|
206
|
+
* user opts in. We deliberately do NOT fabricate spec sections — a raw prompt
|
|
207
|
+
* stays the user's own words. The only transform: when the body has no leading
|
|
208
|
+
* markdown heading, prefix a single neutral `## Overview` heading so the spec
|
|
209
|
+
* renders with a section title downstream. Returns the (trimmed) input
|
|
210
|
+
* unchanged when it already starts with a heading or is empty.
|
|
211
|
+
*/
|
|
212
|
+
function lightlyStructurePrompt(text) {
|
|
213
|
+
const trimmed = text.trim();
|
|
214
|
+
if (!trimmed)
|
|
215
|
+
return trimmed;
|
|
216
|
+
if (/^#{1,6}\s+/.test(trimmed))
|
|
217
|
+
return trimmed;
|
|
218
|
+
return `## Overview\n\n${trimmed}`;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Fold an `acceptanceCriteria` array into a ticket description body, writing
|
|
222
|
+
* (or replacing) a `## Acceptance Criteria` section.
|
|
223
|
+
*
|
|
224
|
+
* - `criteria.length > 0` → append/replace the section with one `- bullet` per item
|
|
225
|
+
* - `criteria.length === 0` → strip any existing `## Acceptance Criteria` section
|
|
226
|
+
*
|
|
227
|
+
* The match is case-insensitive on the heading text but requires `##` exactly
|
|
228
|
+
* to avoid matching other heading levels.
|
|
229
|
+
*
|
|
230
|
+
* Shared by `POST /tickets/from-draft` and `PATCH /tickets/:id`. See
|
|
231
|
+
* openspec/changes/replace-ai-edit-with-continue-editing/design.md D3+D4.
|
|
232
|
+
*/
|
|
233
|
+
function formatDescriptionWithCriteria(body, criteria) {
|
|
234
|
+
const sectionRegex = /\n*##\s*Acceptance Criteria\s*\n[\s\S]*?(?=\n##\s|\n*$)/i;
|
|
235
|
+
const withoutExisting = body.replace(sectionRegex, '').replace(/\s+$/, '');
|
|
236
|
+
if (criteria.length === 0)
|
|
237
|
+
return withoutExisting;
|
|
238
|
+
const section = `## Acceptance Criteria\n\n${criteria.map((c) => `- ${c}`).join('\n')}`;
|
|
239
|
+
if (withoutExisting === '')
|
|
240
|
+
return section;
|
|
241
|
+
return `${withoutExisting}\n\n${section}`;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Resolve the default model used by Add Spec for a project.
|
|
245
|
+
*
|
|
246
|
+
* Order:
|
|
247
|
+
* 1. `models.defaults.model` from `<project>/.specrails/install-config.yaml`,
|
|
248
|
+
* if it parses AND is in the provider allow-list.
|
|
249
|
+
* 2. Provider default from `PROVIDER_DEFAULT_MODEL` (`sonnet` / `gpt-5.5`).
|
|
250
|
+
*
|
|
251
|
+
* Logs a warning when the configured value exists but is not valid for the
|
|
252
|
+
* project's provider.
|
|
253
|
+
*/
|
|
254
|
+
function resolveDefaultSpecModel(args) {
|
|
255
|
+
const { projectPath, provider } = args;
|
|
256
|
+
const configPath = path_1.default.join(projectPath, '.specrails', 'install-config.yaml');
|
|
257
|
+
if (!fs_1.default.existsSync(configPath))
|
|
258
|
+
return (0, spec_models_1.getProviderDefault)(provider);
|
|
259
|
+
let configText;
|
|
260
|
+
try {
|
|
261
|
+
configText = fs_1.default.readFileSync(configPath, 'utf-8');
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return (0, spec_models_1.getProviderDefault)(provider);
|
|
265
|
+
}
|
|
266
|
+
const defaultsMatch = configText.match(/defaults:\s*\{\s*model:\s*(\S+?)\s*\}/);
|
|
267
|
+
const configured = defaultsMatch ? defaultsMatch[1] : null;
|
|
268
|
+
if (!configured)
|
|
269
|
+
return (0, spec_models_1.getProviderDefault)(provider);
|
|
270
|
+
if (!(0, spec_models_1.isValidModelForProvider)(configured, provider)) {
|
|
271
|
+
console.warn(`[project-router] resolveDefaultSpecModel: configured model "${configured}" is not valid for provider "${provider}" — falling back to provider default`);
|
|
272
|
+
return (0, spec_models_1.getProviderDefault)(provider);
|
|
273
|
+
}
|
|
274
|
+
return configured;
|
|
275
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerJobsRoutes = registerJobsRoutes;
|
|
4
|
+
const db_1 = require("./db");
|
|
5
|
+
const queue_manager_1 = require("./queue-manager");
|
|
6
|
+
const types_1 = require("./types");
|
|
7
|
+
const hooks_1 = require("./hooks");
|
|
8
|
+
const explore_smash_1 = require("./explore-smash");
|
|
9
|
+
const spec_models_1 = require("./spec-models");
|
|
10
|
+
const provider_selection_1 = require("./provider-selection");
|
|
11
|
+
const metrics_1 = require("./metrics");
|
|
12
|
+
const ticket_store_1 = require("./ticket-store");
|
|
13
|
+
const project_router_helpers_1 = require("./project-router-helpers");
|
|
14
|
+
function registerJobsRoutes(deps) {
|
|
15
|
+
const { router, registry, ctx, ticketPath } = deps;
|
|
16
|
+
// ─── Queue / Spawn routes ────────────────────────────────────────────────────
|
|
17
|
+
router.post('/:projectId/spawn', (req, res) => {
|
|
18
|
+
const { command, priority, dependsOnJobId, pipelineId, profileName, aiEngine } = req.body ?? {};
|
|
19
|
+
if (!command || typeof command !== 'string' || !command.trim()) {
|
|
20
|
+
res.status(400).json({ error: 'command is required' });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (priority !== undefined && !types_1.VALID_PRIORITIES.has(priority)) {
|
|
24
|
+
res.status(400).json({ error: 'priority must be one of: low, normal, high, critical' });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// profileName accepts: undefined (default resolution), null (force legacy), string (explicit)
|
|
28
|
+
const normalizedProfileName = profileName === null ? null
|
|
29
|
+
: typeof profileName === 'string' && profileName.trim() ? profileName.trim()
|
|
30
|
+
: undefined;
|
|
31
|
+
// aiEngine: optional per-job provider override; must be installed on the
|
|
32
|
+
// project. Omitting it runs on the project's primary provider.
|
|
33
|
+
const engineCheck = (0, provider_selection_1.validateRequestedProvider)(ctx(req).project, aiEngine);
|
|
34
|
+
if (!engineCheck.ok) {
|
|
35
|
+
res.status(400).json({ error: engineCheck.error });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const job = ctx(req).queueManager.enqueue(command, priority ?? 'normal', {
|
|
40
|
+
dependsOnJobId: dependsOnJobId || undefined,
|
|
41
|
+
pipelineId: pipelineId || undefined,
|
|
42
|
+
profileName: normalizedProfileName,
|
|
43
|
+
provider: aiEngine ? engineCheck.provider : undefined,
|
|
44
|
+
});
|
|
45
|
+
const position = job.queuePosition ?? 0;
|
|
46
|
+
res.status(202).json({ jobId: job.id, position });
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (err instanceof queue_manager_1.ClaudeNotFoundError) {
|
|
50
|
+
res.status(400).json({ error: err.message });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.error('[project-router] spawn error:', err);
|
|
54
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// ─── Pipeline routes ──────────────────────────────────────────────────────────
|
|
59
|
+
// NOTE: Ad-hoc pipeline creation removed — use rails (templates) instead.
|
|
60
|
+
// The GET route remains for viewing existing pipeline status.
|
|
61
|
+
router.get('/:projectId/pipelines/:pipelineId', (req, res) => {
|
|
62
|
+
const { db } = ctx(req);
|
|
63
|
+
const pipelineId = req.params.pipelineId;
|
|
64
|
+
const jobs = (0, db_1.getPipelineJobs)(db, pipelineId);
|
|
65
|
+
if (jobs.length === 0) {
|
|
66
|
+
res.status(404).json({ error: 'Pipeline not found' });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const allCompleted = jobs.every(j => j.status === 'completed');
|
|
70
|
+
const anyFailed = jobs.some(j => ['failed', 'skipped', 'canceled', 'zombie_terminated'].includes(j.status));
|
|
71
|
+
const status = allCompleted ? 'completed' : anyFailed ? 'failed' : 'running';
|
|
72
|
+
res.json({ pipelineId, status, jobs });
|
|
73
|
+
});
|
|
74
|
+
router.get('/:projectId/state', (req, res) => {
|
|
75
|
+
const { queueManager, project } = ctx(req);
|
|
76
|
+
res.json({
|
|
77
|
+
projectName: project.name,
|
|
78
|
+
projectId: project.id,
|
|
79
|
+
phases: (0, hooks_1.getPhaseStates)(),
|
|
80
|
+
busy: queueManager.getActiveJobId() !== null,
|
|
81
|
+
currentJobId: queueManager.getActiveJobId(),
|
|
82
|
+
featureFlags: {
|
|
83
|
+
smash: !(0, explore_smash_1.isSpecsSmashKillSwitchActive)(),
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
// Returns the resolved default model for Add Spec + the full provider
|
|
88
|
+
// allow-list so the modal can render its picker without maintaining its
|
|
89
|
+
// own copy of the model lists. Source of truth is `server/spec-models.ts`.
|
|
90
|
+
router.get('/:projectId/default-spec-model', (req, res) => {
|
|
91
|
+
const { project } = ctx(req);
|
|
92
|
+
// Multi-provider: an optional ?provider= query selects which engine's models
|
|
93
|
+
// to return. It must be one the project actually has installed; an invalid
|
|
94
|
+
// or omitted value falls back to the project's primary provider. The
|
|
95
|
+
// response also lists every installed provider so the Add Spec modal can
|
|
96
|
+
// render its AI Engine selector without a second round-trip.
|
|
97
|
+
const provider = (0, provider_selection_1.resolveProvider)(project, typeof req.query.provider === 'string' ? req.query.provider : undefined);
|
|
98
|
+
const model = (0, project_router_helpers_1.resolveDefaultSpecModel)({ projectPath: project.path, provider });
|
|
99
|
+
const allowed = (0, spec_models_1.getModelsForProvider)(provider);
|
|
100
|
+
res.json({ model, provider, allowed, providers: project.providers });
|
|
101
|
+
});
|
|
102
|
+
router.delete('/:projectId/jobs/:id', (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const result = ctx(req).queueManager.cancel(req.params.id);
|
|
105
|
+
res.json({ ok: true, status: result });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
if (err instanceof queue_manager_1.JobNotFoundError) {
|
|
109
|
+
res.status(404).json({ error: 'Job not found' });
|
|
110
|
+
}
|
|
111
|
+
else if (err instanceof queue_manager_1.JobAlreadyTerminalError) {
|
|
112
|
+
// Job already finished — delete it from the DB
|
|
113
|
+
(0, db_1.deleteJob)(ctx(req).db, req.params.id);
|
|
114
|
+
res.json({ ok: true, status: 'deleted' });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
router.patch('/:projectId/jobs/:id/priority', (req, res) => {
|
|
122
|
+
const { priority } = req.body ?? {};
|
|
123
|
+
if (!priority || !types_1.VALID_PRIORITIES.has(priority)) {
|
|
124
|
+
res.status(400).json({ error: 'priority must be one of: low, normal, high, critical' });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
ctx(req).queueManager.updatePriority(req.params.id, priority);
|
|
129
|
+
res.json({ ok: true });
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (err instanceof queue_manager_1.JobNotFoundError) {
|
|
133
|
+
res.status(404).json({ error: 'Job not found' });
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
res.status(400).json({ error: err.message });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
router.post('/:projectId/queue/pause', (req, res) => {
|
|
141
|
+
ctx(req).queueManager.pause();
|
|
142
|
+
res.json({ ok: true, paused: true });
|
|
143
|
+
});
|
|
144
|
+
router.post('/:projectId/queue/resume', (req, res) => {
|
|
145
|
+
ctx(req).queueManager.resume();
|
|
146
|
+
res.json({ ok: true, paused: false });
|
|
147
|
+
});
|
|
148
|
+
router.put('/:projectId/queue/reorder', (req, res) => {
|
|
149
|
+
const { jobIds } = req.body ?? {};
|
|
150
|
+
if (!Array.isArray(jobIds)) {
|
|
151
|
+
res.status(400).json({ error: 'jobIds must be an array' });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
ctx(req).queueManager.reorder(jobIds);
|
|
156
|
+
res.json({ ok: true, queue: jobIds });
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
res.status(400).json({ error: err.message });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
router.get('/:projectId/queue', (req, res) => {
|
|
163
|
+
const { queueManager } = ctx(req);
|
|
164
|
+
res.json({
|
|
165
|
+
jobs: queueManager.getJobs(),
|
|
166
|
+
paused: queueManager.isPaused(),
|
|
167
|
+
activeJobId: queueManager.getActiveJobId(),
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
router.get('/:projectId/jobs', (req, res) => {
|
|
171
|
+
// Clamp to [1, 200] (H-11): a negative limit is LIMIT -1 in SQLite, which
|
|
172
|
+
// means UNLIMITED — without the lower bound `?limit=-1` dumps the whole table.
|
|
173
|
+
const limit = Math.max(1, Math.min(parseInt(String(req.query.limit ?? '50'), 10) || 50, 200));
|
|
174
|
+
const offset = parseInt(String(req.query.offset ?? '0'), 10) || 0;
|
|
175
|
+
const status = req.query.status;
|
|
176
|
+
const from = req.query.from;
|
|
177
|
+
const to = req.query.to;
|
|
178
|
+
const { db } = ctx(req);
|
|
179
|
+
const result = (0, db_1.listJobs)(db, { limit, offset, status, from, to });
|
|
180
|
+
// Merge in-memory queued jobs that haven't been persisted to DB yet
|
|
181
|
+
const { queueManager } = ctx(req);
|
|
182
|
+
const dbIds = new Set(result.jobs.map((j) => j.id));
|
|
183
|
+
const queuedRows = queueManager
|
|
184
|
+
.getJobs()
|
|
185
|
+
.filter((j) => j.status === 'queued' && !dbIds.has(j.id))
|
|
186
|
+
.filter((j) => !status || j.status === status)
|
|
187
|
+
.map((j) => ({
|
|
188
|
+
id: j.id,
|
|
189
|
+
command: j.command,
|
|
190
|
+
started_at: j.startedAt ?? new Date().toISOString(),
|
|
191
|
+
finished_at: j.finishedAt,
|
|
192
|
+
status: j.status,
|
|
193
|
+
exit_code: j.exitCode,
|
|
194
|
+
queue_position: j.queuePosition,
|
|
195
|
+
priority: j.priority,
|
|
196
|
+
tokens_in: null,
|
|
197
|
+
tokens_out: null,
|
|
198
|
+
tokens_cache_read: null,
|
|
199
|
+
tokens_cache_create: null,
|
|
200
|
+
total_cost_usd: null,
|
|
201
|
+
num_turns: null,
|
|
202
|
+
model: null,
|
|
203
|
+
duration_ms: null,
|
|
204
|
+
duration_api_ms: null,
|
|
205
|
+
session_id: null,
|
|
206
|
+
depends_on_job_id: j.dependsOnJobId,
|
|
207
|
+
pipeline_id: j.pipelineId,
|
|
208
|
+
skip_reason: j.skipReason,
|
|
209
|
+
}));
|
|
210
|
+
if (queuedRows.length > 0) {
|
|
211
|
+
result.jobs = [...queuedRows, ...result.jobs];
|
|
212
|
+
result.total += queuedRows.length;
|
|
213
|
+
}
|
|
214
|
+
// Annotate each job with hasTelemetry so the client can show the
|
|
215
|
+
// Export diagnostic button without an extra round trip.
|
|
216
|
+
const jobsWithTelemetry = (0, db_1.getJobsWithTelemetry)(db);
|
|
217
|
+
const annotatedJobs = result.jobs.map((j) => ({
|
|
218
|
+
...j,
|
|
219
|
+
hasTelemetry: jobsWithTelemetry.has(j.id),
|
|
220
|
+
}));
|
|
221
|
+
res.json({ jobs: annotatedJobs, total: result.total });
|
|
222
|
+
});
|
|
223
|
+
// ─── CSV helper ──────────────────────────────────────────────────────────────
|
|
224
|
+
const toCsv = (headers, rows) => {
|
|
225
|
+
const escape = (v) => {
|
|
226
|
+
const s = v == null ? '' : String(v);
|
|
227
|
+
return s.includes(',') || s.includes('"') || s.includes('\n') ? `"${s.replace(/"/g, '""')}"` : s;
|
|
228
|
+
};
|
|
229
|
+
const lines = [headers.join(',')];
|
|
230
|
+
for (const row of rows) {
|
|
231
|
+
lines.push(headers.map(h => escape(row[h])).join(','));
|
|
232
|
+
}
|
|
233
|
+
return lines.join('\n');
|
|
234
|
+
};
|
|
235
|
+
// ─── Jobs export (must be before /:projectId/jobs/:id) ─────────────────────
|
|
236
|
+
router.get('/:projectId/jobs/export', (req, res) => {
|
|
237
|
+
const format = req.query.format || 'json';
|
|
238
|
+
if (format !== 'json' && format !== 'csv') {
|
|
239
|
+
res.status(400).json({ error: 'Invalid format. Must be json or csv' });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const from = req.query.from;
|
|
243
|
+
const to = req.query.to;
|
|
244
|
+
const { db } = ctx(req);
|
|
245
|
+
const conditions = [];
|
|
246
|
+
const params = [];
|
|
247
|
+
if (from) {
|
|
248
|
+
conditions.push('started_at >= ?');
|
|
249
|
+
params.push(from);
|
|
250
|
+
}
|
|
251
|
+
if (to) {
|
|
252
|
+
conditions.push('started_at <= ?');
|
|
253
|
+
params.push(to);
|
|
254
|
+
}
|
|
255
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
256
|
+
const jobs = db
|
|
257
|
+
.prepare(`SELECT * FROM jobs ${where} ORDER BY started_at DESC LIMIT 10000`)
|
|
258
|
+
.all(...params);
|
|
259
|
+
if (format === 'csv') {
|
|
260
|
+
const headers = ['id', 'command', 'status', 'started_at', 'finished_at', 'duration_ms', 'tokens_in', 'tokens_out', 'tokens_cache_read', 'total_cost_usd', 'model'];
|
|
261
|
+
const csv = toCsv(headers, jobs);
|
|
262
|
+
res.setHeader('Content-Type', 'text/csv');
|
|
263
|
+
res.setHeader('Content-Disposition', 'attachment; filename="jobs-export.csv"');
|
|
264
|
+
res.send(csv);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
res.json({ jobs });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
// Must be registered BEFORE /:projectId/jobs/:id, otherwise Express matches
|
|
271
|
+
// the parameterized route first with id='compare' and this never runs (the
|
|
272
|
+
// Job Comparison feature would always 404).
|
|
273
|
+
router.get('/:projectId/jobs/compare', (req, res) => {
|
|
274
|
+
const raw = req.query.jobIds;
|
|
275
|
+
if (!raw) {
|
|
276
|
+
res.status(400).json({ error: 'jobIds query param required (comma-separated, exactly 2)' });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const ids = raw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
280
|
+
if (ids.length !== 2) {
|
|
281
|
+
res.status(400).json({ error: 'Exactly 2 jobIds are required' });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const { db } = ctx(req);
|
|
285
|
+
const rows = ids.map((id) => {
|
|
286
|
+
const job = db.prepare('SELECT * FROM jobs WHERE id = ?').get(id);
|
|
287
|
+
if (!job)
|
|
288
|
+
return null;
|
|
289
|
+
const phases = db.prepare("SELECT phase FROM job_phases WHERE job_id = ? AND state = 'done' ORDER BY updated_at ASC").all(id);
|
|
290
|
+
return {
|
|
291
|
+
id: job.id,
|
|
292
|
+
command: job.command,
|
|
293
|
+
status: job.status,
|
|
294
|
+
startedAt: job.started_at,
|
|
295
|
+
finishedAt: job.finished_at,
|
|
296
|
+
durationMs: job.duration_ms,
|
|
297
|
+
tokensIn: job.tokens_in,
|
|
298
|
+
tokensOut: job.tokens_out,
|
|
299
|
+
tokensCacheRead: job.tokens_cache_read,
|
|
300
|
+
totalCostUsd: job.total_cost_usd,
|
|
301
|
+
model: job.model,
|
|
302
|
+
phasesCompleted: phases.map((p) => p.phase),
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
const missing = ids.filter((_, i) => rows[i] === null);
|
|
306
|
+
if (missing.length > 0) {
|
|
307
|
+
res.status(404).json({ error: `Jobs not found: ${missing.join(', ')}` });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
res.json({ jobs: rows });
|
|
311
|
+
});
|
|
312
|
+
router.get('/:projectId/jobs/:id', (req, res) => {
|
|
313
|
+
const { db, queueManager, project } = ctx(req);
|
|
314
|
+
const jobId = req.params.id;
|
|
315
|
+
const job = (0, db_1.getJob)(db, jobId);
|
|
316
|
+
if (!job) {
|
|
317
|
+
// Queued jobs live only in memory until spawn time (createJob runs on spawn,
|
|
318
|
+
// not enqueue). Fall back to the in-memory queue so /jobs/:id returns a
|
|
319
|
+
// usable payload instead of 404 — the detail page then renders a "queued"
|
|
320
|
+
// state and flips to live logs via WS once the job starts.
|
|
321
|
+
const inMemory = queueManager.getJobs().find((j) => j.id === jobId);
|
|
322
|
+
if (!inMemory) {
|
|
323
|
+
res.status(404).json({ error: 'Job not found' });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const synthetic = {
|
|
327
|
+
id: inMemory.id,
|
|
328
|
+
command: inMemory.command,
|
|
329
|
+
started_at: inMemory.startedAt ?? '',
|
|
330
|
+
finished_at: inMemory.finishedAt,
|
|
331
|
+
status: inMemory.status,
|
|
332
|
+
exit_code: inMemory.exitCode,
|
|
333
|
+
queue_position: inMemory.queuePosition,
|
|
334
|
+
priority: inMemory.priority,
|
|
335
|
+
tokens_in: null,
|
|
336
|
+
tokens_out: null,
|
|
337
|
+
tokens_cache_read: null,
|
|
338
|
+
tokens_cache_create: null,
|
|
339
|
+
total_cost_usd: null,
|
|
340
|
+
num_turns: null,
|
|
341
|
+
model: null,
|
|
342
|
+
duration_ms: null,
|
|
343
|
+
duration_api_ms: null,
|
|
344
|
+
session_id: null,
|
|
345
|
+
depends_on_job_id: inMemory.dependsOnJobId,
|
|
346
|
+
pipeline_id: inMemory.pipelineId,
|
|
347
|
+
skip_reason: inMemory.skipReason,
|
|
348
|
+
};
|
|
349
|
+
const phaseDefinitions = queueManager.phasesForCommand(synthetic.command);
|
|
350
|
+
const tickets = (0, ticket_store_1.resolveTicketsFromCommand)(project.path, synthetic.command);
|
|
351
|
+
res.json({ job: { ...synthetic, hasTelemetry: false, tickets }, events: [], phaseDefinitions });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const events = (0, db_1.getJobEvents)(db, jobId);
|
|
355
|
+
const phaseDefinitions = queueManager.phasesForCommand(job.command);
|
|
356
|
+
const tickets = (0, ticket_store_1.resolveTicketsFromCommand)(project.path, job.command);
|
|
357
|
+
const annotated = { ...job, hasTelemetry: (0, db_1.hasJobTelemetry)(db, jobId), tickets };
|
|
358
|
+
res.json({ job: annotated, events, phaseDefinitions });
|
|
359
|
+
});
|
|
360
|
+
router.delete('/:projectId/jobs', (req, res) => {
|
|
361
|
+
try {
|
|
362
|
+
const { from, to } = req.body ?? {};
|
|
363
|
+
const deleted = (0, db_1.purgeJobs)(ctx(req).db, { from, to });
|
|
364
|
+
res.json({ ok: true, deleted });
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
console.error('[project-router] purge error:', err);
|
|
368
|
+
res.status(500).json({ error: 'Failed to purge jobs' });
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
router.get('/:projectId/activity', (req, res) => {
|
|
372
|
+
const limit = Math.min(Math.max(1, parseInt(String(req.query.limit ?? '50'), 10) || 50), 100);
|
|
373
|
+
const before = req.query.before;
|
|
374
|
+
res.json((0, db_1.getProjectActivity)(ctx(req).db, { limit, before }));
|
|
375
|
+
});
|
|
376
|
+
router.get('/:projectId/stats', (req, res) => {
|
|
377
|
+
res.json((0, db_1.getStats)(ctx(req).db));
|
|
378
|
+
});
|
|
379
|
+
router.get('/:projectId/metrics', (req, res) => {
|
|
380
|
+
const { project, db } = ctx(req);
|
|
381
|
+
try {
|
|
382
|
+
res.json((0, metrics_1.getProjectMetrics)(project.path, db));
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
console.error('[project-router] metrics error:', err);
|
|
386
|
+
res.status(500).json({ error: 'Failed to compute metrics' });
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|