specrails-desktop 2.3.0 → 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-Dyyz1ht3.js → AnalyticsPage-BUd3gWYC.js} +1 -1
- package/client/dist/assets/{BarChart-CMdLa6Es.js → BarChart-HDe_YoUD.js} +2 -2
- package/client/dist/assets/CodePage-CqPPND47.js +2 -0
- package/client/dist/assets/{DesktopAnalyticsPage-CTNwb639.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-DuoZcdYN.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/{cssMode-Cc6ozl-J.js → cssMode-DzNPAYFh.js} +1 -1
- package/client/dist/assets/{dist-js-H6hyhSuv.js → dist-js-COfIfLRE.js} +1 -1
- package/client/dist/assets/{dist-js-4UEGaKhD.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-Cs5FrUJI.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-BZWYV-w-.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 +12 -11
- package/docs/jira-integration-plan.md +321 -0
- package/package.json +1 -1
- package/server/dist/db.js +80 -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/project-registry.js +43 -1
- package/server/dist/project-router-tickets.js +49 -1
- package/server/dist/project-router.js +4 -0
- package/server/dist/rails-router.js +12 -0
- package/client/dist/assets/ActivityFeedPage-3Veccrvk.js +0 -1
- package/client/dist/assets/AgentsPage-2mFPghP4.js +0 -86
- package/client/dist/assets/CodePage-D7Xwjhut.js +0 -2
- package/client/dist/assets/DocsDialog-D8yoyZDD.js +0 -11
- package/client/dist/assets/DocsPage-CeO-fAxy.js +0 -11
- package/client/dist/assets/IntegrationsPage-iIZ0UEzf.js +0 -3
- package/client/dist/assets/JobDetailPage-DgJHAH2m.js +0 -16
- package/client/dist/assets/JobsPage-Bv_RpRAE.js +0 -1
- package/client/dist/assets/index-CGHKpC-N.js +0 -142
- package/client/dist/assets/index-D17R4Cjc.css +0 -2
- 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/{code-zCwBt3Uu.js → code-BwIz8agY.js} +0 -0
- /package/client/dist/assets/{code-g0qFMzyg.js → code-CD7yNSK0.js} +0 -0
- /package/client/dist/assets/{code-DDU0CRS0.js → code-CDFlxUFC.js} +0 -0
- /package/client/dist/assets/{code-L35Loak_.js → code-Cp3Fdng-.js} +0 -0
- /package/client/dist/assets/{code-D1z-YDt-.js → code-D24e1Crx.js} +0 -0
- /package/client/dist/assets/{code-BtsmPQLV.js → code-DtZBQTi9.js} +0 -0
- /package/client/dist/assets/{code-Coa8f2Sh.js → code-nKa0fkm_.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,321 @@
|
|
|
1
|
+
# Jira como fuente de Specs — Estudio e implementación
|
|
2
|
+
|
|
3
|
+
> **Estado: v1 IMPLEMENTADO en specrails-desktop** (Fase 0 + Fase 1). Subsistema `server/jira/` + UI wizard + tests; core y companion sin cambios. Ver la sección "Jira integration" en `CLAUDE.md` para el mapa de código. Las Fases 2–3 (OAuth 3LO, keychain OS, conflictos bidireccionales, sink de comentarios Jira→local, relay de webhooks) siguen pendientes. Producto: Specrails (core + desktop + companion).
|
|
4
|
+
> Objetivo: que una Spec **pueda ser** un ticket de Jira, con **hot‑swap** por proyecto entre tickets locales y tickets de Jira, siguiendo todo el ciclo de vida (crear → To Do → In Progress → Done / revertir), y dejando un comentario en Jira al terminar el job del rail.
|
|
5
|
+
> Conclusión de cabecera: **specrails‑core no necesita ningún cambio**; el 95 % del trabajo vive en specrails‑desktop; specrails‑companion necesita ~0 (campos aditivos opcionales).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Veredicto ejecutivo
|
|
10
|
+
|
|
11
|
+
La feature es viable, de alto valor enterprise, y — crucialmente — se puede construir **sin tocar el contrato congelado de core ni el de la app móvil**. La arquitectura ganadora (validada por un panel de 3 arquitectos + 3 jueces y un barrido adversarial de 87 corner cases) es:
|
|
12
|
+
|
|
13
|
+
> **Desktop es la capa de sincronización. El store local `local-tickets.json` sigue siendo la caché de lectura canónica. Jira es el sistema de registro. El hot‑swap es un *cambio de destino de escritura*, no un refactor del camino de lectura. Toda la escritura a Jira pasa por un *outbox transaccional durable* en SQLite.**
|
|
14
|
+
|
|
15
|
+
Tres decisiones la sostienen:
|
|
16
|
+
|
|
17
|
+
1. **Core intacto.** Desktop materializa Jira → `.specrails/local-tickets.json` (lo que core ya lee) y escribe `.specrails/backlog-config.json` con `provider:"local"` + `write_access:false`. Eso mete a core en su rama **read‑only**: lee tickets de la caché pero **nunca** muta estado ni habla con Jira. Desktop (`applyJobOutcomeToTickets`) queda como **única autoridad de estado**.
|
|
18
|
+
2. **No refactorizar el store en v1.** No introducimos todavía una interfaz `TicketProvider` que obligue a reescribir los ~27 importadores de `ticket-store.ts` (el módulo más sensible: advisory‑lock, atomic‑rename, supresión de eco del watcher, regla de no‑resurrección de draft/cancelled). Eso se difiere hasta que exista un **segundo** backend (GitHub Issues/Linear) que justifique la abstracción. En v1, un `JiraSyncManager` *al lado* de los managers existentes en `ProjectContext`.
|
|
19
|
+
3. **Durabilidad desde el día 1.** Escritura caché + fila de outbox en **una sola transacción SQLite** → es imposible perder una transición o un comentario por crash/offline. Idempotencia en cada operación. Esto es lo verdaderamente "load‑bearing" de cualquier write‑back a Jira y es barato incluirlo desde el principio.
|
|
20
|
+
|
|
21
|
+
Lo que **se descarta para v1** (con motivo): webhooks entrantes (el server liga a `127.0.0.1`, sin ingress público); OAuth 2.0 3LO (el token endpoint de Atlassian exige `client_secret`, sin device‑flow → necesitaría un *token‑broker* hospedado que no existe); resolución de conflictos campo‑a‑campo bidireccional; interfaz `TicketProvider` genérica; sink de comentarios Jira→local.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 2. Respuesta directa a tus preguntas
|
|
26
|
+
|
|
27
|
+
### ¿Cómo impacta a cada repo?
|
|
28
|
+
|
|
29
|
+
| Repo | Impacto | Detalle |
|
|
30
|
+
|------|---------|---------|
|
|
31
|
+
| **specrails-core** | **Cero cambios** | Core nunca lee tickets por código: cada lectura/escritura es un agente LLM siguiendo instrucciones markdown contra `.specrails/local-tickets.json`, gateado por `.specrails/backlog-config.json` (`{provider, write_access, git_auto}`). La ruta del fichero está *hardcodeada* en cada skill, así que redirigir la **ubicación** sí requeriría tocar core — pero redirigir el **contenido** (materializar Jira en esa ruta fija) no requiere nada. Con `write_access:false` core entra en su rama read‑only (`implement.md:1307`: "Do NOT create, modify, or comment on any issues/tickets") e imprime una tabla de actualización manual en vez de mutar. |
|
|
32
|
+
| **specrails-desktop** | **~95 % del trabajo** | Nuevo subsistema `server/jira/`: cliente HTTP, materializador (poll → `local-tickets.json`), outbox durable, resolver de estados/transiciones, store de credenciales cifrado, router REST + eventos WS, y UI cliente (toggle de fuente, badge Jira, panel de sync/dead‑letter, settings de conexión). Dos *hooks* en código existente (lanzamiento de rail + salida de job). |
|
|
33
|
+
| **specrails-companion** | **~0 (aditivo opcional)** | Los deserializadores Dart son **tolerantes** (`models.dart:1-4`: "parsers are deliberately tolerant … so a schema tweak never crashes the app"). Añadir `jiraKey`/`source`/`externalStatus`/`externalUrl` al JSON REST de `/tickets` es ignorado sin crash. Opción A: cero cambios y la spec Jira ya se ve. Opción B (recomendada): 2 ediciones aditivas para un badge "PROJ‑123" de primera clase. Ningún string del contrato congelado (`hub.*`, `specrailshub` mDNS, campos de pairing) se toca. |
|
|
34
|
+
|
|
35
|
+
### El ciclo de vida que describiste, mapeado
|
|
36
|
+
|
|
37
|
+
Tu descripción actual:
|
|
38
|
+
- En el listado de Specs = **To Do**; al pasar al rail = **In Progress**; al terminar = **Done**; cancelación/fallo = vuelve a **To Do**.
|
|
39
|
+
|
|
40
|
+
Cómo se implementa hoy en desktop (verificado):
|
|
41
|
+
- `todo → in_progress`: **no se escribe en servidor**. Lo escribe el agente CLI de core en `local-tickets.json`, o lo infiere el tablero por pertenencia al rail. (`rails-router.ts:293-298` solo hace `enqueue` + `railJobs.set` + broadcast `rail.job_started`; `QueueManager` nunca escribe estado de ticket.)
|
|
42
|
+
- `in_progress → done` / `→ todo` / `needs_review`: un único sitio, `applyJobOutcomeToTickets` (`ticket-store.ts:324-353`), invocado solo desde `onJobFinished` en `project-registry.ts:315-317` (envuelto en `mutateStore`).
|
|
43
|
+
|
|
44
|
+
Con Jira, cada transición local se refleja en Jira vía outbox, en **dos chokepoints exactos**:
|
|
45
|
+
|
|
46
|
+
| Transición Specrails | Dónde se emite | Acción Jira (encolada en outbox) |
|
|
47
|
+
|----------------------|----------------|----------------------------------|
|
|
48
|
+
| `todo → in_progress` (lanzar rail) | `rails-router.ts:293-298`, justo tras `enqueue` (con `rail.ticketIds` en scope) | Transición a categoría `indeterminate` + escribir `in_progress` en la caché local (porque `write_access:false` quita la escritura de core) |
|
|
49
|
+
| `in_progress/todo → done` (job OK) | `project-registry.ts:315-327`, tras el broadcast loop | Transición a categoría `done` + **comentario de finalización** |
|
|
50
|
+
| `in_progress → todo` (fallo/cancel/zombie) | mismo sitio | Transición *best‑effort* hacia categoría `new` (puede no existir camino — ver §6) + comentario |
|
|
51
|
+
| `done + needs_review` (murió tras Ship) | mismo sitio | **NO** transicionar a Done; en su lugar comentario ("run terminó anormalmente, revisar") + label `specrails:needs-review` |
|
|
52
|
+
|
|
53
|
+
> Punto crítico verificado: el `in_progress` de lanzamiento **NUNCA** lo emite la app server‑side, y con `write_access:false` el agente CLI **tampoco** lo escribirá. Por eso el push de `In Progress` a Jira **debe** emitirse explícitamente desde el hook de lanzamiento — no se puede depender del file‑watcher ni del agente.
|
|
54
|
+
|
|
55
|
+
### Crear el ticket y comentar al final
|
|
56
|
+
|
|
57
|
+
- **Crear**: el flujo Add Spec gana un destino. Si la fuente del proyecto es Jira (o el usuario elige "crear en Jira"), `POST /rest/api/3/issue` crea el issue (project + issuetype + summary + descripción en ADF), se mintea un `#id` local estable y se inserta la fila en `jira_links`. Si es local, ruta actual sin cambios.
|
|
58
|
+
- **Comentar al terminar**: el comentario de finalización ("Implementado por Specrails — job N, coste $X, duración Z, PR #…") se compone en el chokepoint de `onJobFinished` (que ya tiene `status`, `costUsd`, `jobRow.duration_ms`, `completedTicketIds` en scope) y se encola como op de outbox **independiente** de la transición (para que si la transición falla por workflow, el comentario igualmente se publique).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 3. Arquitectura
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
┌────────────────────────── specrails-desktop ──────────────────────────┐
|
|
66
|
+
│ │
|
|
67
|
+
Jira Cloud/DC ◀──▶│ server/jira/ │
|
|
68
|
+
REST v3 / v2 │ ├─ jira-client.ts (HTTP: Basic Cloud / Bearer DC, ADF vs wiki) │
|
|
69
|
+
(poll + outbox) │ ├─ jira-sync-manager.ts (per-project; poll loop + outbox drainer) │
|
|
70
|
+
│ ├─ jira-outbox.ts (durable SQLite outbox + dead-letter) │
|
|
71
|
+
│ ├─ jira-status-resolver.ts (two-tier map + BFS transition walk) │
|
|
72
|
+
│ ├─ jira-materializer.ts (Jira issues → local-tickets.json, surgical) │
|
|
73
|
+
│ ├─ jira-links.ts (immutable-id ↔ local #id map) │
|
|
74
|
+
│ └─ jira-credential-store.ts (libsodium secretbox behind interface) │
|
|
75
|
+
│ │
|
|
76
|
+
│ Hooks (2): rails-router.ts:293 + project-registry.ts:315 │
|
|
77
|
+
│ Lee/escribe: .specrails/local-tickets.json (vía mutateStore, locked) │
|
|
78
|
+
│ Escribe: .specrails/backlog-config.json {provider:local, write:false} │
|
|
79
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
80
|
+
│ lee local-tickets.json (sin saber que viene de Jira)
|
|
81
|
+
▼
|
|
82
|
+
specrails-core (agente LLM, rama read-only, CERO cambios)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Principio rector — "al lado, no a través" (beside, not through):** el `JiraSyncManager` se monta en `ProjectContext` junto a `chatManager`, `ticketWatcher`, etc. (sitio de construcción en `project-registry.ts:380`). Los ~27 importadores de `ticket-store.ts` quedan intactos. La mutación local en `onJobFinished` sigue siendo **síncrona** (como hoy); solo el efecto secundario hacia Jira se hace **asíncrono y durable**. Esto evita el movimiento más peligroso (async‑ificar `applyJobOutcomeToTickets` en el chokepoint de salida de job, que gobierna la liberación de tickets del rail).
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 4. Modelo de datos
|
|
90
|
+
|
|
91
|
+
### 4.1 Config por proyecto (registro de proyectos, desktop-db)
|
|
92
|
+
|
|
93
|
+
Nueva columna `ticket_source TEXT DEFAULT 'local'` (`'local' | 'jira'`) en la tabla de proyectos (migración aditiva, mismo patrón que `provider`/`providers` migr. 10/11). Más una fila de conexión Jira (ver 4.4). **Invariante** (espejo del multi‑provider): cuando `ticket_source='local'` todo se comporta byte‑idéntico a hoy; ningún selector se renderiza, ninguna credencial se persiste.
|
|
94
|
+
|
|
95
|
+
### 4.2 `.specrails/backlog-config.json` (lo escribe desktop, lo lee core)
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{ "provider": "local", "write_access": false, "git_auto": false }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`provider:"local"` (no `"jira"`) es **deliberado**: mantiene a core fuera del camino `curl`‑a‑Jira; desktop es lo único que autentica contra Jira. `write_access:false` mete a core en read‑only.
|
|
102
|
+
|
|
103
|
+
### 4.3 `jira_links` (per-project `jobs.sqlite`, nueva migración)
|
|
104
|
+
|
|
105
|
+
La tabla más importante del diseño. **Clave: el id numérico INMUTABLE de Jira**, jamás la key mutable `PROJ-123`.
|
|
106
|
+
|
|
107
|
+
```sql
|
|
108
|
+
CREATE TABLE jira_links (
|
|
109
|
+
local_id INTEGER PRIMARY KEY, -- el #id que ve core; monotónico, NUNCA reusado
|
|
110
|
+
jira_issue_id TEXT NOT NULL UNIQUE, -- id numérico inmutable de Jira (sobrevive move/rename)
|
|
111
|
+
jira_key TEXT, -- 'PROJ-123' — solo display, re-resuelto por id si 404
|
|
112
|
+
jira_project_id TEXT NOT NULL,
|
|
113
|
+
deployment TEXT NOT NULL, -- 'cloud' | 'dc'
|
|
114
|
+
last_remote_hash TEXT, -- detección barata de divergencia inbound
|
|
115
|
+
status_category TEXT, -- última categoría conocida (new|indeterminate|done)
|
|
116
|
+
state TEXT DEFAULT 'linked', -- linked | orphaned | conflict
|
|
117
|
+
created_at TEXT, updated_at TEXT
|
|
118
|
+
);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Razones (de los corner cases data‑model): un issue **movido** de proyecto cambia la key pero no el id; un **rename de project key** reescribe todas las keys. Si la clave de join fuera la key, cada transición/comentario daría 404 y el poll filtrado por `project=` lo perdería. Los `local_id` se asignan una vez y se **tombstonean** al borrar (nunca se renumeran), para que un `/specrails:implement #42` capturado al lanzar siga resolviendo al mismo issue tras re‑materializaciones y reinicios.
|
|
122
|
+
|
|
123
|
+
### 4.4 `jira_connection` (per-project) + credencial cifrada
|
|
124
|
+
|
|
125
|
+
```sql
|
|
126
|
+
CREATE TABLE jira_connection (
|
|
127
|
+
project_id TEXT PRIMARY KEY,
|
|
128
|
+
base_url TEXT NOT NULL, -- https://acme.atlassian.net | https://jira.acme.com
|
|
129
|
+
deployment TEXT NOT NULL, -- 'cloud' | 'dc' (detectado al bind)
|
|
130
|
+
api_version TEXT NOT NULL, -- '3' | '2'
|
|
131
|
+
auth_scheme TEXT NOT NULL, -- 'basic' | 'bearer'
|
|
132
|
+
account_email TEXT, -- solo Cloud (Basic = base64(email:token))
|
|
133
|
+
jira_project_key TEXT NOT NULL,
|
|
134
|
+
jira_project_id TEXT NOT NULL, -- inmutable; la key puede renombrarse
|
|
135
|
+
status_map TEXT, -- JSON: override explícito spec-status → Jira status id
|
|
136
|
+
high_water_ms INTEGER, -- marca de poll (epoch ms; resolución real = minuto)
|
|
137
|
+
created_at TEXT, updated_at TEXT
|
|
138
|
+
);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
El **token** NO va en esta tabla en claro. Va en `jira-credential-store.ts` (ver §5): `crypto_secretbox` de libsodium sellado bajo un keyfile `0600`. En lectura se devuelve solo `hasToken: boolean` (espejo de la redacción de `publicWebhook`); el token jamás se devuelve al cliente ni al companion, nunca se loguea.
|
|
142
|
+
|
|
143
|
+
### 4.5 `jira_outbox` (per-project `jobs.sqlite`) — el corazón de la durabilidad
|
|
144
|
+
|
|
145
|
+
```sql
|
|
146
|
+
CREATE TABLE jira_outbox (
|
|
147
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
148
|
+
jira_issue_id TEXT NOT NULL, -- FIFO se serializa por este id
|
|
149
|
+
op_type TEXT NOT NULL, -- 'transition' | 'comment' | 'create'
|
|
150
|
+
idempotency_key TEXT NOT NULL UNIQUE, -- (job_id + ticket_id + op_type)
|
|
151
|
+
payload TEXT NOT NULL, -- JSON: target category / comment body / etc.
|
|
152
|
+
issue_version TEXT, -- versión/ETag capturada al encolar (freshness check)
|
|
153
|
+
state TEXT DEFAULT 'pending', -- pending | inflight | done | dead
|
|
154
|
+
attempts INTEGER DEFAULT 0,
|
|
155
|
+
next_attempt_at TEXT, last_error TEXT, dead_reason TEXT,
|
|
156
|
+
created_at TEXT, updated_at TEXT
|
|
157
|
+
);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**El invariante transaccional:** la fila de outbox se inserta en la **misma transacción SQLite** que registra el cambio de estado, *antes* de aplicar la mutación a `local-tickets.json` (que es JSON en disco, no SQLite). El outbox es la fuente de verdad durable de "qué debe llegar a Jira". Si el proceso muere entre escribir `done` en el JSON y encolar, no se pierde nada porque el outbox se persistió primero; al arrancar se drenan las filas no‑acked.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 5. Autenticación
|
|
165
|
+
|
|
166
|
+
### v1 — token‑paste (recomendado), cero backend
|
|
167
|
+
|
|
168
|
+
| | Cloud | Data Center / Server |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| Método | API token + email (**Basic**) | Personal Access Token (**Bearer**) |
|
|
171
|
+
| Header | `Authorization: Basic base64(email:token)` | `Authorization: Bearer <PAT>` |
|
|
172
|
+
| Base path | `/rest/api/3` | `/rest/api/2` |
|
|
173
|
+
| Body comentario/desc | **ADF** (JSON) | **wiki‑markup** (string plano) |
|
|
174
|
+
| Versión mínima | — | Jira 8.14+ (si no, basic auth) |
|
|
175
|
+
|
|
176
|
+
Por qué token‑paste y no OAuth en v1:
|
|
177
|
+
- OAuth 3LO de Atlassian **exige `client_secret`** (sin PKCE para public client, sin device flow), que no se puede embeber con seguridad en un binario distribuido → forzaría un **token‑broker hospedado** que veríamos y que dispara revisiones de seguridad enterprise ("¿vuestro servidor ve nuestros tokens?"). Token‑paste mantiene la credencial **en la máquina**.
|
|
178
|
+
- DC **no tiene** OAuth 3LO Cloud; el PAT es el único camino limpio. Token‑paste cubre ambos despliegues con un solo UX.
|
|
179
|
+
- Loopback redirect (`http://localhost`) sí está whitelisteado por Atlassian, así que **OAuth es el v2 correcto** para orgs que deshabilitan API tokens — pero detrás de la misma interfaz de credencial.
|
|
180
|
+
|
|
181
|
+
**Realidades operativas a manejar (de la investigación):**
|
|
182
|
+
- Tras 15‑dic‑2024 todo API token Cloud **expira** (máx 1 año, sin refresh). Tokens legacy se están **forzando a expirar** entre mar‑2025 y may‑2026. ⇒ UX consciente de expiración: detectar 401, pausar el outbox del proyecto, banner "token Jira expirado — re‑pega en Settings", reanudar drenaje tras re‑auth (la idempotencia hace el replay seguro).
|
|
183
|
+
- Muchas enterprises **deshabilitan** la creación de API tokens org‑wide o imponen expiraciones cortas → documentarlo y tener OAuth como ruta v2.
|
|
184
|
+
|
|
185
|
+
### Cifrado en reposo
|
|
186
|
+
|
|
187
|
+
`jira-credential-store.ts` expone una interfaz de un solo fichero. **v1**: libsodium `crypto_secretbox` bajo keyfile `0600` (estrictamente más fuerte que el listón actual de plaintext del webhook‑HMAC, **sin plugin nativo**). **v2**: swap a Tauri keychain/stronghold = cambio de un fichero (no toca `src-tauri`/Cargo/firma/notarización en v1). En Windows, donde los permisos POSIX de `secure-fs.ts` son no‑op, documentar que una cuenta‑OS compartida no es frontera de seguridad soportada en v1.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 6. Ciclo de vida y mapeo de estados (la parte difícil)
|
|
192
|
+
|
|
193
|
+
Los issues de Jira **no tienen un campo `status` asignable** — hay que **transicionar**, y las transiciones están **gateadas por el workflow** del cliente (que es arbitrario). Las 4 lógicas de Specrails (`todo/in_progress/done/cancelled`) deben caer sobre N estados de cliente repartidos en solo **3 categorías estables**: `new` / `indeterminate` / `done` (`statusCategory.key`).
|
|
194
|
+
|
|
195
|
+
### Resolver de dos niveles
|
|
196
|
+
|
|
197
|
+
1. **Mapa explícito por proyecto gana siempre.** En Settings el usuario elige, de la **lista real de estados** de su proyecto (fetched en vivo), el target para cada estado lógico. Esto resuelve la ambigüedad (p.ej. dos estados sobre categoría `done`: `Released` vs `Won't Do`).
|
|
198
|
+
2. **Fallback por categoría** cuando no hay mapa: anclar en `statusCategory.key` (nunca en el **nombre** localizable del estado). Para `cancelled`, preferir un *cancel‑lexicon* (`won't do`, `cancelled`, `rejected`, `abandoned`, `invalid`, `duplicate`) y fijar `resolution`; para `done`/éxito, preferir un *ship‑lexicon* (`done`, `closed`, `released`, `resolved`, `complete`) y **alejarse** del cancel‑lexicon.
|
|
199
|
+
|
|
200
|
+
### Camino de transición — BFS por saltos
|
|
201
|
+
|
|
202
|
+
Como solo ves las transiciones salientes del estado **actual**, un workflow `Backlog → Selected for Dev → In Progress → Done` no ofrece arista directa a `Done`. Algoritmo: `GET /transitions` del issue vivo → aplicar la arista que reduce la distancia a la categoría objetivo (orden `new < indeterminate < done`) → re‑`GET` → repetir. Cap ~5 saltos, dedup de estados visitados (evitar bucles), parar si ninguna transición reduce distancia. **Idempotency‑first**: si la categoría actual ya es la objetivo, **no‑op**. Si no hay camino en N saltos → **dead‑letter** no‑fatal ("mover estado manualmente en Jira") y **jamás** se hace error del rail.
|
|
203
|
+
|
|
204
|
+
### Pantallas de transición / campos requeridos
|
|
205
|
+
|
|
206
|
+
La transición a `done` puede tener `hasScreen:true` con `resolution` requerido. Siempre `GET /transitions?expand=transitions.fields` primero; incluir `resolution`/custom fields **solo** si aparecen en esa pantalla (`required && !hasDefaultValue`). Si un custom field requerido no tiene default programable, abortar **esa** transición con gracia (dead‑letter) en vez de adivinar. El **comentario** va como op separada para que un fallo de transición no se lo lleve por delante.
|
|
207
|
+
|
|
208
|
+
### `needs_review`
|
|
209
|
+
|
|
210
|
+
Flag app‑only sin equivalente Jira. Mapeo: cuando `needs_review=true`, **no** disparar Done; en su lugar comentario + label `specrails:needs-review`. La transición a Done queda condicionada a `status==='completed' && !needs_review`. Se limpia el label en la siguiente finalización limpia (`clearWarning` en `ticket-store.ts:336-339`).
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 7. Hot‑swap local ↔ Jira
|
|
215
|
+
|
|
216
|
+
- **`source` es por‑ticket, no por‑tablero.** Voltear el toggle Local↔Jira cambia solo el **destino de creación** y la **fuente de lectura**; jamás re‑homologa specs existentes.
|
|
217
|
+
- Las specs locales pre‑existentes (sin `jira_links`) siguen siendo locales; ofrecer una acción explícita opt‑in "empujar esta spec a Jira" (crea el issue + fila `jira_links`) en vez de migrar implícitamente.
|
|
218
|
+
- **Write‑back gateado por snapshot‑per‑job:** la op de outbox de un job lleva el `jira_issue_id` capturado **al lanzar** (mismo patrón que el snapshot de profile). Voltear el tablero a mitad de un job no afecta el write‑back en vuelo. Esto también cubre el reinicio del server (se pierde el `railJobs` Map en memoria → re‑parsear `#id` del comando como ya hace el camino local, y re‑resolver `jira_links` por id).
|
|
219
|
+
- **Gating de capacidades:** helper `sourceSupports(source, feature)` (espejo de `sectionVisibleForProviders`) para ocultar Drafts / SMASH / Contract‑Layer en specs de origen Jira, sin construir la interfaz `TicketProvider` completa.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 8. Sincronización
|
|
224
|
+
|
|
225
|
+
### Inbound — polling (forzado, no es un compromiso)
|
|
226
|
+
|
|
227
|
+
Webhooks descartados: el server liga `127.0.0.1` (`index.ts:552`), sin ingress público; los webhooks dinámicos de Jira Cloud exigen URL HTTPS pública + app Connect/OAuth (Basic no puede registrarlos) y **expiran cada 30 días**.
|
|
228
|
+
|
|
229
|
+
Diseño de poll:
|
|
230
|
+
- `POST /rest/api/3/search/jql` (el `GET /search` legacy fue **deprecado** 2024‑10‑31 y bloqueado fin‑oct‑2025). Enviar `fields` explícito, paginar con `nextPageToken`, parar en token ausente/`isLast`.
|
|
231
|
+
- JQL: `project = KEY AND (labels en filtro specrails) ORDER BY updated ASC`.
|
|
232
|
+
- **High‑water mark con solape de seguridad de 2 min** (`updated >= hw - 2min`), nunca avanzar `hw` más allá de `now - 1min`, dedup por `(issueId, updated)`. **La marca se deriva del `updated` máximo observado en los issues devueltos** (timestamps del *server de Jira*), **no** de `Date.now()` local — esto auto‑corrige el clock skew del desktop.
|
|
233
|
+
- `updated` tiene **resolución de minuto** y la búsqueda es eventualmente consistente → ventana ≥1 min + solape evita perder cambios del mismo minuto.
|
|
234
|
+
- **Reconcile completo horario** (JQL full por project+label) para detectar **borrados/moves** que no bumpean `updated`.
|
|
235
|
+
- **Read‑your‑write**: tras una escritura propia, usar `reconcileIssues:[id]` (consistencia fuerte para ese id) para evitar el flicker done→in_progress→done por lag de réplica.
|
|
236
|
+
- **El outbox es autoritativo:** el poll **no** sobrescribe un campo (sobre todo `status`) de un issue con op de outbox pendiente. Para specs en rail activo, congelar la materialización de `status` (es app‑owned durante el run); solo sincronizar campos no‑estado (description/labels).
|
|
237
|
+
|
|
238
|
+
### Outbound — outbox durable
|
|
239
|
+
|
|
240
|
+
- Drenaje en worker de fondo: **FIFO por‑issue** (una transición debe aterrizar antes que el comentario que la describe), **paralelo entre issues distintos**, con cap de concurrencia.
|
|
241
|
+
- **Idempotencia:** transiciones por no‑op‑si‑ya‑en‑categoría; **comentarios** con un *self‑marker* invisible embebido en el body ADF (`[specrails:job-<id>]`) — Jira no tiene idempotencia nativa de comentarios, así que antes de re‑postear se hace `GET .../comment` y se salta si el marker ya existe. El marker dobla como filtro de auto‑eco en el poll.
|
|
242
|
+
- **Rate limits:** honrar `Retry-After` en 429 exacto; si ausente, backoff exponencial con jitter (base 2s, cap 30s, ~4 reintentos); respetar el techo ~20 writes/2s por issue. Token‑bucket por debajo de los burst caps.
|
|
243
|
+
- **Clasificación de errores:** `401` = credencial → **pausar** outbox del proyecto + banner re‑auth (no reintentar en bucle); `403` = permiso de operación concreta → dead‑letter nombrando la operación ("tu cuenta no puede transicionar en PROJ"), sin inferir fallo global; `404` sobre issue conocido = terminal (issue borrado/movido) → marcar link `orphaned`, parar la op; solo `429/5xx/timeout` son reintentables.
|
|
244
|
+
- **Dead‑letter visible** con reintento manual: `GET /jira/outbox`, `POST /jira/outbox/:id/retry`, indicador `JiraSyncIndicator` en UI. Un workflow‑gap o un 403 **nunca** es un drop silencioso.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 9. Corner cases (87 catalogados; los críticos)
|
|
249
|
+
|
|
250
|
+
Distribución: **9 critical, 47 high, 29 medium, 2 low**. Categorías: sync (18), workflow‑mapping (16), data‑model (12), lifecycle (10), concurrency (8), auth (6), offline/permissions/rate‑limit (4 c/u), core (3), hot‑swap (2).
|
|
251
|
+
|
|
252
|
+
Los **9 críticos** y su mitigación (todos v1‑must‑handle):
|
|
253
|
+
|
|
254
|
+
1. **Sin camino de transición a la categoría objetivo** (workflow forward‑only) → BFS por saltos + dead‑letter no‑fatal, nunca error del rail.
|
|
255
|
+
2. **Transición Done con pantalla + `resolution` requerido** → `expand=transitions.fields`, incluir solo lo que está en pantalla; abortar con gracia si hay custom field sin default.
|
|
256
|
+
3. **Ambigüedad de categoría `done`** (`Released` vs `Won't Do`) → resolver de 2 niveles, mapa explícito + cancel/ship lexicon.
|
|
257
|
+
4. **`in_progress` de lanzamiento nunca llega a Jira** (la app no lo emite y `write_access:false` lo quita de core) → push explícito desde `rails-router.ts:293-298` + escribir `in_progress` en caché ahí mismo.
|
|
258
|
+
5. **Crash entre mutación de caché y enqueue de outbox** → outbox persistido **primero** en la misma txn SQLite; outbox = fuente de verdad; drenar no‑acked al arrancar.
|
|
259
|
+
6. **Offline en la escritura de estado al salir el job** → side‑effect Jira **nunca** inline en `onJobFinished`; solo el enqueue durable, en try/catch que no puede romper el rail; drena al reconectar.
|
|
260
|
+
7. **Partial write: caché en `done`, Jira sigue `In Progress`, sin fila de outbox** → mismo invariante transaccional (op registrada antes de mutar JSON) + reconcile de arranque.
|
|
261
|
+
8. **Proyecto Jira equivocado** (key con typo / multi‑proyecto) → validar `GET /project/{key}` en bind, mostrar nombre+lead+issue‑types para confirmación humana, scope de JQL a `project=KEY` + filtro label, guardar el **id** inmutable del project.
|
|
262
|
+
9. **Token expira a mitad de un rail multi‑hora** → outbox durable absorbe el 401; estado dead‑letter visible + evento WS `jira.auth_expired` + banner "Reconnect to sync N pending"; al re‑pegar token, auto‑drena (idempotencia = no‑op si ya en categoría).
|
|
263
|
+
|
|
264
|
+
Otros **high** notables: dos editores (Jira UI + TicketDetailModal) → precondición de versión/ETag, 409 no clobbea, banner "editado en Jira"; humano reabre el issue antes de que drene el `done` encolado → freshness check (si la versión cambió desde el enqueue, dead‑letter "superseded", no forzar — "el robot no pelea con el humano"); op de propose‑spec en terminal sobre proyecto Jira → materializador **quirúrgico** que nunca dropea tickets sin `jira_links`, reconcilia los `source:"propose-spec"` hacia Jira; ADF (Cloud v3) vs wiki‑string (DC v2) → branch del serializador en el límite del adapter por deployment detectado.
|
|
265
|
+
|
|
266
|
+
**Guardarraíl explícito:** **no** añadir un valor `'jira-sync'` al enum `ai_invocations.surface` — una op de sync no tiene modelo/tokens/coste y contaminaría el análisis de coste con filas `$0/null`. La telemetría de sync va en una tabla ops dedicada o solo eventos WS.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 10. Plan de implementación por fases
|
|
271
|
+
|
|
272
|
+
### Fase 0 — Andamiaje y contrato (sin UI)
|
|
273
|
+
- Migraciones desktop‑db (`ticket_source`) y per‑project (`jira_connection`, `jira_links`, `jira_outbox`).
|
|
274
|
+
- `server/jira/jira-credential-store.ts` (interfaz + backend libsodium).
|
|
275
|
+
- `server/jira/jira-client.ts`: detección Cloud/DC en bind, Basic/Bearer, v3/v2, ADF/wiki, `{ok,data,error,status}` (modelado como `specrails-tech-client.ts`, **no** sobre `WebhookManager`).
|
|
276
|
+
- Escritor de `.specrails/backlog-config.json` (`provider:local`, `write_access:false`).
|
|
277
|
+
- Tests: cliente (mock HTTP), credential store, migraciones. Cobertura ≥ umbrales (server 80 %).
|
|
278
|
+
|
|
279
|
+
### Fase 1 — MVP (read + status round‑trip + comentario) — **el v1 enviable**
|
|
280
|
+
- `jira-materializer.ts`: poll `POST /search/jql` → `local-tickets.json` (merge quirúrgico, vía `mutateStore` con lock), `jira-links` por id inmutable, `#id` monotónico tombstoneado, high‑water + solape + reconcile horario.
|
|
281
|
+
- `jira-status-resolver.ts`: resolver 2 niveles + BFS + manejo de pantalla/resolution.
|
|
282
|
+
- `jira-outbox.ts` + drainer: txn única, idempotencia, FIFO‑por‑issue, Retry‑After, clasificación de errores, dead‑letter.
|
|
283
|
+
- **Hook A** `rails-router.ts:293-298`: encolar transición `in_progress` + escribir caché.
|
|
284
|
+
- **Hook B** `project-registry.ts:315-327`: encolar transición de salida + comentario de finalización.
|
|
285
|
+
- `jira-router.ts` (REST, gateado por flag): bind/validar conexión, `GET/POST /jira/outbox`, reintento, `mypermissions` probe.
|
|
286
|
+
- Eventos WS: `jira.synced`, `jira.auth_expired`, `jira.outbox_changed`, `jira.degraded` (project‑scoped).
|
|
287
|
+
- Cliente: toggle `ticket_source` por proyecto (Settings), badge "PROJ‑123" + estado externo en `SpecCard`/views/`TicketDetailModal`, "Ver en Jira", panel de credenciales + mapa de estados, `JiraSyncIndicator` + dead‑letter. i18n estricto (sin strings hardcoded) + tokens de tema semánticos.
|
|
288
|
+
- **Feature flags**: `SPECRAILS_JIRA_SECTION` (server) + `VITE_FEATURE_JIRA` (cliente), off‑por‑defecto hasta GA.
|
|
289
|
+
- Companion: **nada obligatorio** (pass‑through tolerante).
|
|
290
|
+
|
|
291
|
+
### Fase 2 — Robustez enterprise
|
|
292
|
+
- OAuth 2.0 3LO (loopback + token‑broker hospedado) detrás de la interfaz de credencial; swap de credential‑store a Tauri keychain/stronghold.
|
|
293
|
+
- Conflictos campo‑a‑campo bidireccionales (precondición de versión, banners de conflicto).
|
|
294
|
+
- Sink de comentarios Jira→local (poblar el `comments[]` dormante).
|
|
295
|
+
- Companion Opción B (badge Jira de primera clase, ~2 ediciones).
|
|
296
|
+
- Probes proactivos de permisos/expiración.
|
|
297
|
+
|
|
298
|
+
### Fase 3 — Escala / plataforma
|
|
299
|
+
- Relay de ingress público (vía specrails‑tech) para webhooks Jira en tiempo real (sustituye polling donde se pueda).
|
|
300
|
+
- Abstracción `TicketProvider` (extraer `LocalTicketProvider` + `JiraTicketProvider`) **cuando** llegue un tercer backend (GitHub Issues/Linear) — entonces es un refactor mecánico y revisable por separado, no una apuesta especulativa.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## 11. Por qué esto aporta alto valor al cliente enterprise
|
|
305
|
+
|
|
306
|
+
- **Adopción sin fricción de seguridad:** token en la máquina, cero backend que vea credenciales, soporta Cloud **y** Data Center (muchas enterprises siguen en DC). Ninguna revisión de "vuestro server ve nuestros tokens".
|
|
307
|
+
- **Respeta el workflow del cliente:** no impone estados; mapea sobre el workflow real con override explícito + fallback por categoría + BFS. Nunca rompe un rail por una transición imposible.
|
|
308
|
+
- **Nunca pierde una actualización ni spamea:** outbox transaccional + idempotencia → exactamente‑una‑vez observable en comentarios y transiciones, incluso con crash/offline/reintentos. El "robot no pelea con el humano".
|
|
309
|
+
- **Visibilidad para PMs:** el issue Jira refleja el ciclo real (In Progress al lanzar, Done al terminar) y recibe un comentario con coste/duración/PR — los stakeholders no técnicos viven en Jira y lo ven sin entrar en Specrails.
|
|
310
|
+
- **Hot‑swap real por proyecto** sin migración forzada ni riesgo a los proyectos locales existentes.
|
|
311
|
+
- **Riesgo de producto mínimo:** core congelado intacto, app móvil en App Review intacta, módulo `ticket-store.ts` crítico sin refactor. Todo detrás de feature flags.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## 12. Decisiones abiertas para validar antes de construir
|
|
316
|
+
|
|
317
|
+
1. **Alcance de despliegue v1:** ¿Cloud‑first (ADF, Basic) y DC detrás del tipo detectado, o DC desde el día 1? (El branch de body ADF/wiki debe existir igual si se promete DC.)
|
|
318
|
+
2. **Granularidad del mapa de estados:** ¿UI de mapeo explícito obligatoria en el bind, o fallback‑por‑categoría con opción de afinar? (Recomendado: fallback funcional + afinado opcional.)
|
|
319
|
+
3. **Comportamiento en fallo con workflow forward‑only:** ¿dejar el issue en `In Progress` en Jira por defecto (config) o intentar siempre revertir? (Recomendado: best‑effort revert + dejar‑como‑está configurable.)
|
|
320
|
+
4. **Companion v1:** ¿cero cambios (pass‑through) o el badge de 2 ediciones?
|
|
321
|
+
```
|
package/package.json
CHANGED
package/server/dist/db.js
CHANGED
|
@@ -559,6 +559,86 @@ const MIGRATIONS = [
|
|
|
559
559
|
);
|
|
560
560
|
`);
|
|
561
561
|
},
|
|
562
|
+
// Migration 29: Jira integration (per-project). Each project syncs with its
|
|
563
|
+
// own Jira board, so every Jira table lives here in the per-project jobs.sqlite
|
|
564
|
+
// and is keyed by nothing but its own rows. See docs/jira-integration-plan.md.
|
|
565
|
+
// - jira_connection: one row, the connection config (token stored encrypted).
|
|
566
|
+
// - jira_links: spec↔issue map keyed on the IMMUTABLE Jira numeric id.
|
|
567
|
+
// - jira_outbox: durable transactional write-back queue (status + comments).
|
|
568
|
+
(db) => {
|
|
569
|
+
db.exec(`
|
|
570
|
+
CREATE TABLE IF NOT EXISTS jira_connection (
|
|
571
|
+
project_id TEXT PRIMARY KEY,
|
|
572
|
+
base_url TEXT NOT NULL,
|
|
573
|
+
deployment TEXT NOT NULL,
|
|
574
|
+
api_version TEXT NOT NULL,
|
|
575
|
+
auth_scheme TEXT NOT NULL,
|
|
576
|
+
account_email TEXT,
|
|
577
|
+
jira_project_key TEXT NOT NULL,
|
|
578
|
+
jira_project_id TEXT NOT NULL,
|
|
579
|
+
encrypted_token TEXT,
|
|
580
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
581
|
+
status_map TEXT,
|
|
582
|
+
high_water_ms INTEGER,
|
|
583
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
584
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
CREATE TABLE IF NOT EXISTS jira_links (
|
|
588
|
+
local_id INTEGER PRIMARY KEY,
|
|
589
|
+
jira_issue_id TEXT NOT NULL UNIQUE,
|
|
590
|
+
jira_key TEXT,
|
|
591
|
+
jira_project_id TEXT NOT NULL,
|
|
592
|
+
deployment TEXT NOT NULL,
|
|
593
|
+
status_category TEXT,
|
|
594
|
+
state TEXT NOT NULL DEFAULT 'linked',
|
|
595
|
+
tombstoned INTEGER NOT NULL DEFAULT 0,
|
|
596
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
597
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
598
|
+
);
|
|
599
|
+
CREATE INDEX IF NOT EXISTS idx_jira_links_issue ON jira_links(jira_issue_id);
|
|
600
|
+
|
|
601
|
+
CREATE TABLE IF NOT EXISTS jira_outbox (
|
|
602
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
603
|
+
jira_issue_id TEXT NOT NULL,
|
|
604
|
+
op_type TEXT NOT NULL,
|
|
605
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
606
|
+
payload TEXT NOT NULL,
|
|
607
|
+
state TEXT NOT NULL DEFAULT 'pending',
|
|
608
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
609
|
+
next_attempt_at TEXT,
|
|
610
|
+
last_error TEXT,
|
|
611
|
+
dead_reason TEXT,
|
|
612
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
613
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
614
|
+
);
|
|
615
|
+
CREATE INDEX IF NOT EXISTS idx_jira_outbox_state ON jira_outbox(state);
|
|
616
|
+
CREATE INDEX IF NOT EXISTS idx_jira_outbox_issue ON jira_outbox(jira_issue_id);
|
|
617
|
+
`);
|
|
618
|
+
},
|
|
619
|
+
// Migration 30: Jira sprint custom-field id. The field that holds an issue's
|
|
620
|
+
// sprint(s) is a custom field whose id varies per instance; we discover it
|
|
621
|
+
// (schema com.pyxis.greenhopper.jira:gh-sprint) and cache it here. NULL =
|
|
622
|
+
// not yet checked, 'none' = checked and no sprint field exists, '<id>' = found.
|
|
623
|
+
(db) => {
|
|
624
|
+
try {
|
|
625
|
+
db.exec(`ALTER TABLE jira_connection ADD COLUMN sprint_field_id TEXT`);
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
// Column may already exist on a partially-migrated DB — no-op.
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
// Migration 31: Jira discard target status. The user-configured status name to
|
|
632
|
+
// which a discarded spec's issue is transitioned (instead of being deleted) in
|
|
633
|
+
// a Jira-synced project. NULL/empty = not configured (delete behaves normally).
|
|
634
|
+
(db) => {
|
|
635
|
+
try {
|
|
636
|
+
db.exec(`ALTER TABLE jira_connection ADD COLUMN discard_status TEXT`);
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
// Column may already exist on a partially-migrated DB — no-op.
|
|
640
|
+
}
|
|
641
|
+
},
|
|
562
642
|
];
|
|
563
643
|
function applyMigrations(db) {
|
|
564
644
|
// Ensure the migrations table exists (migration 1 creates it, but we need
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isCodeExplorerEnabled = isCodeExplorerEnabled;
|
|
4
4
|
exports.isBrowserCaptureEnabled = isBrowserCaptureEnabled;
|
|
5
|
+
exports.isJiraEnabled = isJiraEnabled;
|
|
5
6
|
function isCodeExplorerEnabled() {
|
|
6
7
|
return process.env.SPECRAILS_CODE_EXPLORER !== 'false';
|
|
7
8
|
}
|
|
@@ -15,3 +16,13 @@ function isCodeExplorerEnabled() {
|
|
|
15
16
|
function isBrowserCaptureEnabled() {
|
|
16
17
|
return process.env.SPECRAILS_BROWSER_CAPTURE !== 'false';
|
|
17
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Jira integration ("spec = Jira issue", per-project hot-swap local↔Jira).
|
|
21
|
+
* Server-side default ON; set SPECRAILS_JIRA_SECTION="false" to 404 the routes
|
|
22
|
+
* and skip all sync (emergency rollback). The feature is inert until a project
|
|
23
|
+
* actually configures a Jira connection, so default-on is safe. The client gates
|
|
24
|
+
* separately on VITE_FEATURE_JIRA.
|
|
25
|
+
*/
|
|
26
|
+
function isJiraEnabled() {
|
|
27
|
+
return process.env.SPECRAILS_JIRA_SECTION !== 'false';
|
|
28
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Atlassian Document Format (ADF) helpers.
|
|
3
|
+
//
|
|
4
|
+
// Jira Cloud (REST v3) requires comment/description bodies in ADF JSON. Jira
|
|
5
|
+
// Server/Data Center (REST v2) expects a plain wiki-markup string. We keep a
|
|
6
|
+
// single internal "text" model and render it to either format at the client
|
|
7
|
+
// boundary (see jira-client.ts `bodyForDeployment`).
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.SPECRAILS_COMMENT_PROP_KEY = void 0;
|
|
10
|
+
exports.textToAdf = textToAdf;
|
|
11
|
+
exports.bodyForDeployment = bodyForDeployment;
|
|
12
|
+
exports.commentMarker = commentMarker;
|
|
13
|
+
exports.discardCommentMarker = discardCommentMarker;
|
|
14
|
+
exports.bodyContainsMarker = bodyContainsMarker;
|
|
15
|
+
exports.commentHasMarker = commentHasMarker;
|
|
16
|
+
exports.adfToText = adfToText;
|
|
17
|
+
/** Build a minimal ADF document from plain text (newlines → paragraphs). */
|
|
18
|
+
function textToAdf(text) {
|
|
19
|
+
const paragraphs = text.split('\n');
|
|
20
|
+
return {
|
|
21
|
+
type: 'doc',
|
|
22
|
+
version: 1,
|
|
23
|
+
content: paragraphs.map((line) => line.length === 0
|
|
24
|
+
? { type: 'paragraph' }
|
|
25
|
+
: { type: 'paragraph', content: [{ type: 'text', text: line }] }),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Render a body for the target deployment: ADF for Cloud v3, plain for DC v2. */
|
|
29
|
+
function bodyForDeployment(text, deployment) {
|
|
30
|
+
return deployment === 'cloud' ? textToAdf(text) : text;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Jira comment-property key under which we store the idempotency marker. Comment
|
|
34
|
+
* properties are metadata that NEVER render in the comment body, so the marker
|
|
35
|
+
* stays invisible to users while still letting us dedup on retry. Supported on
|
|
36
|
+
* both Cloud (v3) and Data Center (v2).
|
|
37
|
+
*/
|
|
38
|
+
exports.SPECRAILS_COMMENT_PROP_KEY = 'sh.specrails.marker';
|
|
39
|
+
/**
|
|
40
|
+
* Deterministic idempotency marker. Jira has no native comment idempotency, so
|
|
41
|
+
* before re-posting on retry we GET the issue comments and skip if one already
|
|
42
|
+
* carries this marker (now via an invisible comment property — see
|
|
43
|
+
* SPECRAILS_COMMENT_PROP_KEY — with a legacy body-scan fallback).
|
|
44
|
+
*/
|
|
45
|
+
function commentMarker(jobId, ticketId) {
|
|
46
|
+
return `[specrails:job=${jobId}:ticket=${ticketId}]`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Idempotency marker for a user-initiated "discard / move-to" comment. The
|
|
50
|
+
* `nonce` (captured at enqueue) makes each discard distinct so a later re-discard
|
|
51
|
+
* of the same spec posts a fresh comment instead of being deduped away.
|
|
52
|
+
*/
|
|
53
|
+
function discardCommentMarker(ticketId, nonce) {
|
|
54
|
+
return `[specrails:discard=${nonce}:ticket=${ticketId}]`;
|
|
55
|
+
}
|
|
56
|
+
/** True when an ADF doc or wiki string already contains the given marker. */
|
|
57
|
+
function bodyContainsMarker(body, marker) {
|
|
58
|
+
if (typeof body === 'string')
|
|
59
|
+
return body.includes(marker);
|
|
60
|
+
try {
|
|
61
|
+
return JSON.stringify(body).includes(marker);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* True when a fetched comment already carries the marker — preferring the
|
|
69
|
+
* invisible comment property, with a fallback to a legacy body-embedded marker
|
|
70
|
+
* (comments posted before the property move). `comment.properties` comes from
|
|
71
|
+
* `GET …/comment?expand=properties`.
|
|
72
|
+
*/
|
|
73
|
+
function commentHasMarker(comment, marker) {
|
|
74
|
+
const prop = comment.properties?.find((p) => p.key === exports.SPECRAILS_COMMENT_PROP_KEY);
|
|
75
|
+
if (prop) {
|
|
76
|
+
try {
|
|
77
|
+
if (JSON.stringify(prop.value ?? '').includes(marker))
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* fall through to body scan */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return bodyContainsMarker(comment.body, marker);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Flatten an ADF document (or plain string) back to text — used to read inbound
|
|
88
|
+
* Jira descriptions/comments into the local cache.
|
|
89
|
+
*/
|
|
90
|
+
function adfToText(body) {
|
|
91
|
+
if (body == null)
|
|
92
|
+
return '';
|
|
93
|
+
if (typeof body === 'string')
|
|
94
|
+
return body;
|
|
95
|
+
const out = [];
|
|
96
|
+
const walk = (node) => {
|
|
97
|
+
if (!node || typeof node !== 'object')
|
|
98
|
+
return;
|
|
99
|
+
if (node.type === 'text' && typeof node.text === 'string')
|
|
100
|
+
out.push(node.text);
|
|
101
|
+
if (node.type === 'hardBreak')
|
|
102
|
+
out.push('\n');
|
|
103
|
+
if (Array.isArray(node.content)) {
|
|
104
|
+
for (const child of node.content)
|
|
105
|
+
walk(child);
|
|
106
|
+
// paragraph / block separators
|
|
107
|
+
if (node.type === 'paragraph' || node.type === 'heading')
|
|
108
|
+
out.push('\n');
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
walk(body);
|
|
112
|
+
return out.join('').replace(/\n{3,}/g, '\n\n').trim();
|
|
113
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Writes `.specrails/backlog-config.json` so specrails-core treats the
|
|
3
|
+
// materialized `local-tickets.json` as a plain LOCAL backlog it READS but never
|
|
4
|
+
// mutates. This is the mechanism that keeps core at ZERO changes:
|
|
5
|
+
// - provider:"local" → core reads `.specrails/local-tickets.json` (the cache).
|
|
6
|
+
// - write_access:false → core's implement pipeline enters its read-only branch
|
|
7
|
+
// and never mutates ticket status nor talks to Jira; Desktop's
|
|
8
|
+
// applyJobOutcomeToTickets + the Jira outbox are the sole status authority.
|
|
9
|
+
//
|
|
10
|
+
// We deliberately write provider:"local" (NOT "jira") so core never authenticates
|
|
11
|
+
// to Jira — Desktop is the only thing that holds the credential.
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.backlogConfigPath = backlogConfigPath;
|
|
17
|
+
exports.readBacklogConfig = readBacklogConfig;
|
|
18
|
+
exports.writeJiraBacklogConfig = writeJiraBacklogConfig;
|
|
19
|
+
exports.writeLocalBacklogConfig = writeLocalBacklogConfig;
|
|
20
|
+
const fs_1 = __importDefault(require("fs"));
|
|
21
|
+
const path_1 = __importDefault(require("path"));
|
|
22
|
+
function backlogConfigPath(projectPath) {
|
|
23
|
+
return path_1.default.join(projectPath, '.specrails', 'backlog-config.json');
|
|
24
|
+
}
|
|
25
|
+
function readBacklogConfig(projectPath) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = fs_1.default.readFileSync(backlogConfigPath(projectPath), 'utf-8');
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Idempotently write the Jira-mode backlog config (local provider, read-only). */
|
|
35
|
+
function writeJiraBacklogConfig(projectPath) {
|
|
36
|
+
const target = backlogConfigPath(projectPath);
|
|
37
|
+
const desired = { provider: 'local', write_access: false, git_auto: false };
|
|
38
|
+
const existing = readBacklogConfig(projectPath);
|
|
39
|
+
if (existing && existing.provider === desired.provider && existing.write_access === desired.write_access) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
fs_1.default.mkdirSync(path_1.default.dirname(target), { recursive: true });
|
|
43
|
+
const tmp = `${target}.tmp`;
|
|
44
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify(desired, null, 2), 'utf-8');
|
|
45
|
+
fs_1.default.renameSync(tmp, target);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Restore write access (used when hot-swapping a project back to local specs, so
|
|
49
|
+
* core can manage the local backlog normally again).
|
|
50
|
+
*/
|
|
51
|
+
function writeLocalBacklogConfig(projectPath) {
|
|
52
|
+
const target = backlogConfigPath(projectPath);
|
|
53
|
+
const desired = { provider: 'local', write_access: true, git_auto: false };
|
|
54
|
+
fs_1.default.mkdirSync(path_1.default.dirname(target), { recursive: true });
|
|
55
|
+
const tmp = `${target}.tmp`;
|
|
56
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify(desired, null, 2), 'utf-8');
|
|
57
|
+
fs_1.default.renameSync(tmp, target);
|
|
58
|
+
}
|