specrails-desktop 2.26.0 → 2.26.2

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.
Files changed (91) hide show
  1. package/client/dist/assets/{ActivityFeedPage-1eXnoNAo.js → ActivityFeedPage-bDe4VitF.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-BHW_6Vm0.js → AgentBrowserCapture-DQyeEm2V.js} +1 -1
  3. package/client/dist/assets/{AgentModeCodePane-C89SAJzI.js → AgentModeCodePane-Ci39DcOo.js} +2 -2
  4. package/client/dist/assets/{AgentModeJobsPane-BG_4r4Dm.js → AgentModeJobsPane-C-g1WNVG.js} +1 -1
  5. package/client/dist/assets/{AgentsPage-BrkWfc9s.js → AgentsPage-BWgglK7V.js} +3 -3
  6. package/client/dist/assets/{AnalyticsPage-CMZRUPrT.js → AnalyticsPage-Da21wWWw.js} +1 -1
  7. package/client/dist/assets/{BarChart-BtF3zJHa.js → BarChart-BiLTq3uC.js} +1 -1
  8. package/client/dist/assets/{CodePage-Cime5FRs.js → CodePage-BUcAp7SC.js} +1 -1
  9. package/client/dist/assets/{DesktopAnalyticsPage-CSKHSBBF.js → DesktopAnalyticsPage-COkzgMmd.js} +1 -1
  10. package/client/dist/assets/{DocsDialog-ClTq6Fzj.js → DocsDialog-PZHMN18B.js} +1 -1
  11. package/client/dist/assets/{DocsPage-DTxfOpTu.js → DocsPage-D3fChfkj.js} +1 -1
  12. package/client/dist/assets/{ExportDropdown-Ho9zdPvK.js → ExportDropdown-DDhJH-Jr.js} +1 -1
  13. package/client/dist/assets/{IntegrationsPage-DWNyo6jk.js → IntegrationsPage-F87uxxVH.js} +1 -1
  14. package/client/dist/assets/{InteractiveJobComposer-CYxXOKld.js → InteractiveJobComposer-DqDCiCa0.js} +5 -5
  15. package/client/dist/assets/{JobDetailModal-DrNafHY5.js → JobDetailModal-BWJR60fi.js} +1 -1
  16. package/client/dist/assets/{JobDetailPage-4VKw33GW.js → JobDetailPage-CjWE_jyd.js} +1 -1
  17. package/client/dist/assets/{JobsPage-cFr6wWaH.js → JobsPage-LmnXkyCa.js} +1 -1
  18. package/client/dist/assets/{LoopBuilderPage-Bk8zVvEX.js → LoopBuilderPage-DsLLrtHa.js} +2 -2
  19. package/client/dist/assets/{LoopPreviewModal-XFSO2Vcs.js → LoopPreviewModal-C-fDEgnu.js} +1 -1
  20. package/client/dist/assets/{LoopsPage-DWMwDj9g.js → LoopsPage-DDNO9h9T.js} +1 -1
  21. package/client/dist/assets/{ProjectSettingsDialog-DnSe_yWm.js → ProjectSettingsDialog-CX854C8e.js} +1 -1
  22. package/client/dist/assets/{TemplatePreviewModal-B30-TY_v.js → TemplatePreviewModal-DcHFbLA8.js} +1 -1
  23. package/client/dist/assets/agent-BI6vr-To.js +1 -0
  24. package/client/dist/assets/{agent-D5QNn9np2.js → agent-BUGXI3Ok2.js} +1 -1
  25. package/client/dist/assets/agent-Bph_-xt7.js +1 -0
  26. package/client/dist/assets/agent-Bvy2Csyd.js +1 -0
  27. package/client/dist/assets/{agent-DgFoLgrF2.js → agent-Bwkyg2oX2.js} +1 -1
  28. package/client/dist/assets/agent-CLCyDaZh.js +1 -0
  29. package/client/dist/assets/{agent-6mhkV25_2.js → agent-Cqs14QMr2.js} +1 -1
  30. package/client/dist/assets/{agent-CRYdi9-t.js → agent-DJ0EBUuP.js} +1 -1
  31. package/client/dist/assets/{brain-DcXkQwzO.js → brain-BBNUUrUI.js} +1 -1
  32. package/client/dist/assets/dashboard-BjjS1OrL.js +1 -0
  33. package/client/dist/assets/dashboard-Bk6oSk1H2.js +1 -0
  34. package/client/dist/assets/dashboard-BrH-GdOg2.js +1 -0
  35. package/client/dist/assets/dashboard-BurhUI8_.js +1 -0
  36. package/client/dist/assets/dashboard-C6w0rICe.js +1 -0
  37. package/client/dist/assets/dashboard-CAUnfDQL2.js +1 -0
  38. package/client/dist/assets/dashboard-C_ljpeYO.js +1 -0
  39. package/client/dist/assets/dashboard-D6OqDdj6.js +1 -0
  40. package/client/dist/assets/{dist-js-BAWwLSIj.js → dist-js-BxjTIvdT.js} +1 -1
  41. package/client/dist/assets/{dist-js-6U6hyD6z.js → dist-js-DdP_Gl3p.js} +1 -1
  42. package/client/dist/assets/{index-cvOunQkD.css → index-C695aPdV.css} +1 -1
  43. package/client/dist/assets/index-Dw1N2-ci.js +156 -0
  44. package/client/dist/assets/{lib-Bsny0zZp.js → lib-BviHZ_gK.js} +1 -1
  45. package/client/dist/assets/{settings-DHd1lQ1k.js → settings-BHdPchJP.js} +1 -1
  46. package/client/dist/assets/{settings-FJXpJ3k92.js → settings-BeCczBFS2.js} +1 -1
  47. package/client/dist/assets/{settings-BWIf3vv4.js → settings-BlLi719f.js} +1 -1
  48. package/client/dist/assets/{settings-DTYQ5COE.js → settings-C_nY14Yz.js} +1 -1
  49. package/client/dist/assets/{settings-DE8t6aYv.js → settings-CaZ26a0S.js} +1 -1
  50. package/client/dist/assets/{settings-Dgv_q4qf2.js → settings-D54_cgl12.js} +1 -1
  51. package/client/dist/assets/{settings-DXgPh9-k.js → settings-D9bDDe2O.js} +1 -1
  52. package/client/dist/assets/{settings-BhuXe7ek2.js → settings-cn0tbve62.js} +1 -1
  53. package/client/dist/assets/{setup-iY8vH8pB.js → setup-7J8TjG4O.js} +1 -1
  54. package/client/dist/assets/{setup-D674kWQH.js → setup-BXpw07r-.js} +1 -1
  55. package/client/dist/assets/{setup-B590KsTX.js → setup-BiduSX9d.js} +1 -1
  56. package/client/dist/assets/{setup-cnqZsQSX2.js → setup-BuQNKztt2.js} +1 -1
  57. package/client/dist/assets/{setup-CEj6uY3-2.js → setup-BwMovM9z2.js} +1 -1
  58. package/client/dist/assets/{setup-BkyGoby6.js → setup-D84y3wx-.js} +1 -1
  59. package/client/dist/assets/{setup-CbIerFGf2.js → setup-DU0ULX8q2.js} +1 -1
  60. package/client/dist/assets/{setup-VNqXyB6Y.js → setup-RiUHzq7E.js} +1 -1
  61. package/client/dist/assets/{upload-49vX83IZ.js → upload-CRuuzzvp.js} +1 -1
  62. package/client/dist/assets/{value-Bo8EiTZE.js → value-CIr5hDMt.js} +1 -1
  63. package/client/dist/index.html +21 -8
  64. package/package.json +1 -1
  65. package/server/dist/agent-operator-prompt.js +45 -5
  66. package/server/dist/desktop-router.js +9 -2
  67. package/server/dist/mcp/guide.js +33 -4
  68. package/server/dist/mcp/tools/app-settings.js +18 -2
  69. package/server/dist/mcp/tools/catalog.js +2 -0
  70. package/server/dist/mcp/tools/rails.js +27 -6
  71. package/server/dist/mcp/tools/support.js +324 -0
  72. package/server/dist/pr-publisher.js +17 -15
  73. package/server/dist/project-git.js +57 -0
  74. package/server/dist/rail-isolated-launch.js +27 -3
  75. package/server/dist/rail-pr-decision.js +69 -1
  76. package/server/dist/rail-worktree-release.js +25 -0
  77. package/server/dist/rail-worktrees-store.js +2 -2
  78. package/server/dist/rails-router.js +136 -7
  79. package/client/dist/assets/agent-BZqfQhI_.js +0 -1
  80. package/client/dist/assets/agent-Bjy1SlT5.js +0 -1
  81. package/client/dist/assets/agent-Bn6ZmjHj.js +0 -1
  82. package/client/dist/assets/agent-DgwPsxWQ.js +0 -1
  83. package/client/dist/assets/dashboard-CuGuy2__2.js +0 -1
  84. package/client/dist/assets/dashboard-CxP3TZ3I2.js +0 -1
  85. package/client/dist/assets/dashboard-DWHVfP7U.js +0 -1
  86. package/client/dist/assets/dashboard-D_Pgs6QH2.js +0 -1
  87. package/client/dist/assets/dashboard-Dwnssayh.js +0 -1
  88. package/client/dist/assets/dashboard-S_pT1yxN.js +0 -1
  89. package/client/dist/assets/dashboard-tCxOFOxx.js +0 -1
  90. package/client/dist/assets/dashboard-zefbBNyG.js +0 -1
  91. package/client/dist/assets/index-DnkfABxY.js +0 -156
@@ -0,0 +1,324 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.supportTools = supportTools;
4
+ const zod_1 = require("zod");
5
+ const mcp_tiers_1 = require("../mcp-tiers");
6
+ const types_1 = require("./types");
7
+ const projects_1 = require("./projects");
8
+ const SUPPORT_TOPICS = [
9
+ 'installation',
10
+ 'project_setup',
11
+ 'provider_cli',
12
+ 'mcp_connection',
13
+ 'agent_chat',
14
+ 'framework_install',
15
+ 'specs_backlog',
16
+ 'rails_jobs',
17
+ 'git_pr',
18
+ 'analytics_costs',
19
+ 'plugins_integrations',
20
+ 'general_usage',
21
+ ];
22
+ /**
23
+ * Support triage and repair entry point. This is intentionally NOT a spec
24
+ * creation path: it keeps user-help requests in the conversation and routes
25
+ * framework repairs through explicit support actions.
26
+ */
27
+ function supportTools() {
28
+ return [
29
+ {
30
+ name: 'specrails_support',
31
+ title: 'Support & troubleshooting',
32
+ description: 'Support and troubleshooting for Specrails installation and usage problems. Use this when the user needs help setting up Specrails, adding/provisioning a project, fixing missing agents/skills/commands from the global specrails-core framework, provider CLI/MCP/agent-chat issues, rails/jobs/specs, or GitHub/PR/cost/plugin questions. ' +
33
+ 'Actions: triage (read-only support playbook + diagnostics), core_update_status/check/apply (global specrails-core framework update). ' +
34
+ 'This tool never creates specs; repair actions require the normal permission tier and explicit user confirmation.',
35
+ tier: (a) => (String(a.action) === 'core_update_apply' ? 'ai-spawn' : 'read'),
36
+ hintTier: 'read',
37
+ inputSchema: {
38
+ action: zod_1.z
39
+ .enum([
40
+ 'triage',
41
+ 'core_update_status',
42
+ 'core_update_check',
43
+ 'core_update_apply',
44
+ ])
45
+ .describe('Support operation to perform'),
46
+ question: zod_1.z
47
+ .string()
48
+ .optional()
49
+ .describe('The user-facing problem or question to triage, in the user wording when possible'),
50
+ topic: zod_1.z
51
+ .enum(SUPPORT_TOPICS)
52
+ .optional()
53
+ .describe('Optional explicit support topic; omit to infer from question'),
54
+ projectId: zod_1.z
55
+ .string()
56
+ .optional()
57
+ .describe('Optional project id when the issue is project-specific; defaults to the active project when selected'),
58
+ version: zod_1.z
59
+ .string()
60
+ .optional()
61
+ .describe('core_update_apply: optional target specrails-core version; omit to use the latest version found by core_update_check'),
62
+ includeDiagnostics: zod_1.z
63
+ .boolean()
64
+ .optional()
65
+ .describe('When true or omitted, include read-only local diagnostics such as prerequisites, providers, settings, and projects. Project setup checkpoints are skipped for specrails-core installation triage because they are not a core health signal.'),
66
+ },
67
+ async handler(ctx, args) {
68
+ const action = String(args.action);
69
+ switch (action) {
70
+ case 'core_update_status':
71
+ return (0, types_1.apiCall)(ctx, 'GET', '/core-update/status');
72
+ case 'core_update_check':
73
+ return (0, types_1.apiCall)(ctx, 'POST', '/core-update/check', {});
74
+ case 'core_update_apply': {
75
+ const body = typeof args.version === 'string' && args.version.trim()
76
+ ? { version: args.version.trim() }
77
+ : {};
78
+ const r = await (0, types_1.apiCall)(ctx, 'POST', '/core-update/update', body);
79
+ return {
80
+ ...(typeof r === 'object' && r !== null ? r : { result: r }),
81
+ hint: 'Core update is async and app-global. Progress streams as core_update.progress / framework.updated. This updates the global specrails-core framework; it is not a per-project setup/install action.',
82
+ };
83
+ }
84
+ case 'triage':
85
+ break;
86
+ default:
87
+ throw new Error(`Unknown action "${action}".`);
88
+ }
89
+ const question = typeof args.question === 'string' ? args.question.trim() : '';
90
+ const topic = isSupportTopic(args.topic) ? args.topic : inferTopic(question);
91
+ const includeDiagnostics = args.includeDiagnostics !== false;
92
+ const diagnostics = includeDiagnostics
93
+ ? await collectDiagnostics(ctx, args.projectId, topic)
94
+ : { skipped: true };
95
+ return {
96
+ mode: 'support-triage',
97
+ doNotCreateSpec: true,
98
+ topic,
99
+ question: question || null,
100
+ supportPrompt: [
101
+ 'Treat this as a help/support conversation, not product backlog intake.',
102
+ 'Answer in the user language and solve the immediate setup or usage problem with concrete steps.',
103
+ 'Do not create or propose a spec unless the user explicitly pivots from troubleshooting to new product work.',
104
+ 'Prefer read-only diagnostics first. Ask at most two targeted questions if the next step depends on missing local context.',
105
+ 'If a fix requires write, ai-spawn, or destructive permissions, explain the exact action and ask for confirmation before calling another tool.',
106
+ 'For missing agents, skills, slash commands, or specrails-core framework files during a job, explain that these definitions live in the app-global specrails-core framework. Do not use project setup checkpoints as evidence of core health or as a reason to run project setup.',
107
+ ],
108
+ localDiagnostics: diagnostics,
109
+ recommendedNextSteps: nextSteps(topic),
110
+ suggestedReadTools: suggestedTools(topic),
111
+ availableRepairActions: repairActions(topic),
112
+ responseChecklist: [
113
+ 'State what the diagnostics show, including any unknowns or failed probes.',
114
+ 'Give the smallest next action the user can take in the app or terminal.',
115
+ 'Name the exact Specrails surface when relevant: Add Project, Setup, Settings > MCP, Agent Chat, Rails board, Job Detail, Analytics, Integrations.',
116
+ 'If the issue is already resolved by state, say so and explain how to verify it.',
117
+ ],
118
+ };
119
+ },
120
+ },
121
+ ];
122
+ }
123
+ function isSupportTopic(value) {
124
+ return typeof value === 'string' && SUPPORT_TOPICS.includes(value);
125
+ }
126
+ function inferTopic(question) {
127
+ const q = question.toLowerCase();
128
+ if (/\b(mcp|claude desktop|cursor|external ai|token|settings.*mcp)\b/.test(q))
129
+ return 'mcp_connection';
130
+ if (/\b(agent chat|chat panel|shift\+tab|permission level|observe|operate|autonomous)\b/.test(q))
131
+ return 'agent_chat';
132
+ if (/\b(specrails-core|core framework|missing agents?|missing skills?|no agents?|no skills?|slash command|command not found|propose-feature|opsx|framework files?|reinstall)\b/.test(q))
133
+ return 'framework_install';
134
+ if (/\b(claude|codex|gemini|provider|engine|cli|auth|login|api key|path)\b/.test(q))
135
+ return 'provider_cli';
136
+ if (/\b(add project|project setup|setup wizard|provision|checkpoint|install config)\b/.test(q))
137
+ return 'project_setup';
138
+ if (/\b(install|installation|setup|prerequisite|node|npm|npx|git)\b/.test(q))
139
+ return 'installation';
140
+ if (/\b(rail|job|launch|run|queue|stuck|failed|logs?)\b/.test(q))
141
+ return 'rails_jobs';
142
+ if (/\b(github|git|pr|pull request|remote|branch|merge|integrate locally)\b/.test(q))
143
+ return 'git_pr';
144
+ if (/\b(cost|spend|budget|analytics|tokens?)\b/.test(q))
145
+ return 'analytics_costs';
146
+ if (/\b(plugin|integration|serena|marketplace)\b/.test(q))
147
+ return 'plugins_integrations';
148
+ if (/\b(spec|ticket|backlog|draft|todo|done|review)\b/.test(q))
149
+ return 'specs_backlog';
150
+ return 'general_usage';
151
+ }
152
+ async function collectDiagnostics(ctx, projectId, topic) {
153
+ const projects = ctx.registry.listContexts().map((c) => (0, projects_1.serializeProject)(c.project));
154
+ const selectedProjectId = projectId ?? (0, types_1.getActiveProject)(ctx) ?? undefined;
155
+ const out = {
156
+ mcp: {
157
+ enabled: (0, mcp_tiers_1.isMcpEnabled)(ctx.desktopDb),
158
+ tierWrite: (0, mcp_tiers_1.isTierEnabled)(ctx.desktopDb, 'write'),
159
+ tierAiSpawn: (0, mcp_tiers_1.isTierEnabled)(ctx.desktopDb, 'ai-spawn'),
160
+ tierDestructive: (0, mcp_tiers_1.isTierEnabled)(ctx.desktopDb, 'destructive'),
161
+ },
162
+ activeProjectId: (0, types_1.getActiveProject)(ctx),
163
+ projectCount: projects.length,
164
+ projects,
165
+ prerequisites: await safeDiagnostic(() => (0, types_1.apiCall)(ctx, 'GET', '/setup-prerequisites?diagnostic=1')),
166
+ availableProviders: await safeDiagnostic(() => (0, types_1.apiCall)(ctx, 'GET', '/available-providers')),
167
+ coreUpdate: await safeDiagnostic(() => (0, types_1.apiCall)(ctx, 'GET', '/core-update/status')),
168
+ };
169
+ if (!selectedProjectId) {
170
+ out.project = {
171
+ ok: false,
172
+ error: 'No active project or projectId was provided. Project-specific diagnostics need specrails_select_project or projectId.',
173
+ };
174
+ return out;
175
+ }
176
+ const project = safeLocal(() => (0, projects_1.serializeProject)((0, types_1.requireProject)(ctx, selectedProjectId).project));
177
+ out.project = project;
178
+ if (project.ok && topic !== 'framework_install') {
179
+ out.setupCheckpoints = await safeDiagnostic(() => (0, types_1.apiCall)(ctx, 'GET', `/projects/${selectedProjectId}/setup/checkpoints`));
180
+ }
181
+ else if (topic === 'framework_install') {
182
+ out.setupCheckpoints = {
183
+ skipped: true,
184
+ reason: 'Skipped intentionally: project setup checkpoints are not a specrails-core health check. Pending checkpoints or 0 agent/command counts must not be used to diagnose global core installation.',
185
+ };
186
+ }
187
+ return out;
188
+ }
189
+ async function safeDiagnostic(fn) {
190
+ try {
191
+ return { ok: true, data: await fn() };
192
+ }
193
+ catch (err) {
194
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
195
+ }
196
+ }
197
+ function safeLocal(fn) {
198
+ try {
199
+ return { ok: true, data: fn() };
200
+ }
201
+ catch (err) {
202
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
203
+ }
204
+ }
205
+ function nextSteps(topic) {
206
+ switch (topic) {
207
+ case 'installation':
208
+ return [
209
+ 'Read prerequisites and availableProviders from localDiagnostics.',
210
+ 'If Node/npm/npx/git are missing, tell the user to install them, restart Specrails, then recheck.',
211
+ 'If a provider CLI is missing, install and authenticate that CLI before retrying project setup.',
212
+ ];
213
+ case 'project_setup':
214
+ return [
215
+ 'Check whether the project is registered and whether setupCheckpoints.isInstalling is still true.',
216
+ 'If setup is running, tell the user to wait or poll checkpoints; if failed, surface the last log lines and retry only the failed setup step.',
217
+ 'If no project is selected, ask for the repo path and provider choices before using specrails_setup.',
218
+ ];
219
+ case 'provider_cli':
220
+ return [
221
+ 'Compare availableProviders with the providers installed on the project.',
222
+ 'Ask the user to verify the provider CLI in a fresh terminal only when the local probe is missing or ambiguous.',
223
+ 'For auth failures, direct them to the provider CLI login/API-key flow, then re-open or retry Specrails setup.',
224
+ ];
225
+ case 'mcp_connection':
226
+ return [
227
+ 'Check localDiagnostics.mcp.enabled and explain that external clients also need the Settings > MCP config and token.',
228
+ 'If MCP is off, tell the user to enable it in Settings > MCP; do not try to mutate the MCP enable flag through this tool.',
229
+ 'If a tier blocks an action, name the exact tier to enable in Settings > MCP.',
230
+ ];
231
+ case 'agent_chat':
232
+ return [
233
+ 'Explain the in-app permission ladder: observe, edit, operate, autonomous.',
234
+ 'If a tool was refused, tell the user which level to switch to with Shift+Tab.',
235
+ 'If Agent Chat cannot operate, check MCP enabled state and installed provider state first.',
236
+ ];
237
+ case 'framework_install':
238
+ return [
239
+ 'For specrails-core installation questions, evaluate only global core status and prerequisites. Do not treat project setup checkpoints as evidence of a core problem.',
240
+ 'If global core status is current/healthy and the user has not provided an actual job error, say the core installation looks healthy and ask for the concrete failing job/error before recommending repair.',
241
+ 'If a job explicitly reports missing agents, missing skills, unresolved slash commands, or missing /specrails:* commands, explain that those definitions come from the app-global specrails-core framework.',
242
+ 'If an update is available, offer core_update_check followed by core_update_apply with confirmation. If no update is available but the job still fails with missing core definitions, give the manual fallback: run `npx specrails-core@latest update` from the project root, then retry, or collect the job diagnostic for a framework-loading bug.',
243
+ 'Never recommend specrails_setup(install) or a project reinstall solely because setup/checkpoints show pending or 0 agents/commands.',
244
+ ];
245
+ case 'rails_jobs':
246
+ return [
247
+ 'Use read tools for rails/jobs status before guessing: specrails_rails, specrails_jobs, and specrails_watch only when the user asks to wait.',
248
+ 'For a failed or stuck job, inspect the Job Detail logs and report the real failure.',
249
+ 'Do not relaunch or stop anything without confirmation and the required permission level.',
250
+ ];
251
+ case 'git_pr':
252
+ return [
253
+ 'Use specrails_git read-only diagnostics for remote, status, branch, gh_auth, gh_repo, and PR questions.',
254
+ 'Explain that PR creation/integration belongs to the rail PR-decision surface, not this support tool.',
255
+ 'If there is no GitHub remote, explain local-only delivery and Integrate locally.',
256
+ ];
257
+ case 'analytics_costs':
258
+ return [
259
+ 'Use specrails_analytics(spending) for actual cost questions.',
260
+ 'Distinguish app-wide budget settings from per-project analytics budget caps.',
261
+ 'Prefix cost with ~ when the provider reports estimates.',
262
+ ];
263
+ case 'plugins_integrations':
264
+ return [
265
+ 'Use specrails_plugins read actions before suggesting install/uninstall.',
266
+ 'For degraded plugins, report health details and avoid blocking rail launches.',
267
+ 'Mutating plugin actions need explicit confirmation and the matching permission level.',
268
+ ];
269
+ case 'specs_backlog':
270
+ return [
271
+ 'Use specrails_specs list/get to explain current backlog state.',
272
+ 'Only create or edit specs if the user asks for backlog work, not when they are asking how Specrails works.',
273
+ 'Do not patch pipeline-managed statuses unless the user explicitly asks and understands the consequence.',
274
+ ];
275
+ case 'general_usage':
276
+ return [
277
+ 'Use specrails_guide and specrails_search to find the relevant domain.',
278
+ 'Explain the UI path first, then offer to perform safe read/write actions from chat when appropriate.',
279
+ 'Keep the answer short and operational; ask one targeted question if the requested surface is unclear.',
280
+ ];
281
+ }
282
+ }
283
+ function suggestedTools(topic) {
284
+ switch (topic) {
285
+ case 'installation':
286
+ return ['specrails_support', 'specrails_setup(prerequisites)', 'specrails_setup(available_providers)'];
287
+ case 'project_setup':
288
+ return ['specrails_projects(list|get)', 'specrails_setup(checkpoints)', 'specrails_setup(prerequisites)'];
289
+ case 'provider_cli':
290
+ return ['specrails_setup(available_providers)', 'specrails_setup(prerequisites)', 'specrails_projects(get)'];
291
+ case 'mcp_connection':
292
+ return ['specrails_settings(get)', 'specrails_guide', 'specrails_describe'];
293
+ case 'agent_chat':
294
+ return ['specrails_settings(get)', 'specrails_projects(list|get)', 'specrails_guide'];
295
+ case 'framework_install':
296
+ return ['specrails_support(triage|core_update_status|core_update_check)', 'specrails_jobs(get)', 'specrails_jobs(diagnostic)'];
297
+ case 'rails_jobs':
298
+ return ['specrails_rails', 'specrails_jobs', 'specrails_watch'];
299
+ case 'git_pr':
300
+ return ['specrails_git(remote|status|branch|gh_auth|gh_repo|gh_pr_list|gh_pr_view)'];
301
+ case 'analytics_costs':
302
+ return ['specrails_analytics(spending)', 'specrails_settings(get)'];
303
+ case 'plugins_integrations':
304
+ return ['specrails_plugins(list|health|preview)', 'specrails_projects(get)'];
305
+ case 'specs_backlog':
306
+ return ['specrails_specs(list|get)', 'specrails_guide'];
307
+ case 'general_usage':
308
+ return ['specrails_guide', 'specrails_search', 'specrails_describe'];
309
+ }
310
+ }
311
+ function repairActions(topic) {
312
+ if (topic === 'framework_install') {
313
+ return [
314
+ 'specrails_support(core_update_check) then specrails_support(core_update_apply) - apply requires operate/ai-spawn and only makes sense when a newer global specrails-core framework is available.',
315
+ 'Manual fallback when no update is available but a real job still cannot load core definitions: run `npx specrails-core@latest update` from the project root, then retry or collect a job diagnostic.',
316
+ ];
317
+ }
318
+ if (topic !== 'project_setup' && topic !== 'installation')
319
+ return [];
320
+ return [
321
+ 'specrails_setup(install, projectId) - operate/ai-spawn: use only for actual project setup/provisioning, not for specrails-core global installation questions.',
322
+ 'specrails_support(core_update_check) then specrails_support(core_update_apply) - apply requires operate/ai-spawn: updates the app-global specrails-core framework when a newer version is available.',
323
+ ];
324
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.defaultExec = void 0;
4
4
  exports.parsePrUrl = parsePrUrl;
5
+ exports.pushBranch = pushBranch;
5
6
  exports.publishDraftPr = publishDraftPr;
6
7
  /**
7
8
  * App-owned "deliver a mutating loop's result as a DRAFT PR" primitive.
@@ -46,32 +47,33 @@ function parsePrUrl(stdout) {
46
47
  const m = stdout.match(/https?:\/\/\S+\/pull\/\d+/);
47
48
  return m ? m[0] : undefined;
48
49
  }
49
- async function publishDraftPr(exec, input) {
50
+ /** Push a committed delivery branch, including the gh-credential retry used by
51
+ * draft PR creation. This is also used for follow-up commits on an existing PR:
52
+ * that path must update the same remote PR branch without trying to create a
53
+ * second PR. */
54
+ async function pushBranch(exec, input) {
50
55
  const remote = input.remote ?? 'origin';
51
56
  const { repoDir, branch, baseBranch } = input;
52
- // 1. Push the branch (set upstream). Failure → local-only, never throw.
53
- // Guardrail: never force-push, never push the integration branch itself.
54
57
  const pushArgs = ['push', '-u', remote, branch];
55
58
  (0, git_guardrails_1.assertGitAllowed)('git', pushArgs, { protectedBranch: baseBranch });
56
59
  let push = await exec.run('git', pushArgs, repoDir);
57
60
  if (push.code !== 0 && isCredentialFailure(push)) {
58
- // The push failed authenticating over HTTPS — typically the machine's
59
- // configured credential helper (e.g. git-credential-osxkeychain) is not on
60
- // the app's PATH (GUI-launch PATH divergence: the sidecar inherits launchd's
61
- // PATH, not the user's login shell). Retry borrowing gh's credentials — the
62
- // SAME auth we use for `gh pr create` below, so if the PR can be created the
63
- // push can succeed — instead of depending on a machine credential helper.
64
- // `-c credential.helper=` first RESETS the (broken) inherited helper list,
65
- // then adds gh's. Harmless for SSH remotes (git ignores credential.helper).
66
- // The semantic push (refspec/remote) is unchanged, so the guardrail asserted
67
- // above still holds; no-op when gh is absent (retry fails the same way).
68
61
  const retry = await exec.run('git', ['-c', 'credential.helper=', '-c', 'credential.helper=!gh auth git-credential', ...pushArgs], repoDir);
69
62
  if (retry.code === 0)
70
63
  push = retry;
71
64
  }
72
- if (push.code !== 0) {
65
+ if (push.code !== 0)
73
66
  return { state: 'local-only', branch, reason: reasonFrom(push) };
74
- }
67
+ return { state: 'pushed', branch };
68
+ }
69
+ async function publishDraftPr(exec, input) {
70
+ const remote = input.remote ?? 'origin';
71
+ const { repoDir, branch, baseBranch } = input;
72
+ // 1. Push the branch (set upstream). Failure → local-only, never throw.
73
+ // Guardrail: never force-push, never push the integration branch itself.
74
+ const pushed = await pushBranch(exec, { repoDir, branch, baseBranch, remote });
75
+ if (pushed.state === 'local-only')
76
+ return pushed;
75
77
  // 2. Open a DRAFT PR. Failure (no gh / not authed / perms) → pushed, never throw.
76
78
  const pr = await exec.run('gh', ['pr', 'create', '--draft', '--base', baseBranch, '--head', branch, '--title', input.title, '--body', input.body], repoDir);
77
79
  if (pr.code !== 0) {
@@ -4,8 +4,10 @@ exports.parseWorktreePorcelain = parseWorktreePorcelain;
4
4
  exports.getProjectGitInfo = getProjectGitInfo;
5
5
  exports.compactCheckoutError = compactCheckoutError;
6
6
  exports.checkoutProjectBranch = checkoutProjectBranch;
7
+ exports.checkoutProjectReviewBranch = checkoutProjectReviewBranch;
7
8
  const child_process_1 = require("child_process");
8
9
  const file_provenance_1 = require("./file-provenance");
10
+ const integration_branch_1 = require("./integration-branch");
9
11
  // ─── Project git info + branch switch (Agent-Mode git bar) ────────────────────
10
12
  //
11
13
  // Read the repo's current branch / last commit / local branches, and check out
@@ -145,3 +147,58 @@ async function checkoutProjectBranch(repoDir, branch) {
145
147
  return { ok: false, error: compactCheckoutError(err instanceof Error ? err.message : 'git checkout failed') };
146
148
  }
147
149
  }
150
+ async function remoteBranchExists(repoDir, branch) {
151
+ try {
152
+ await git(repoDir, ['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${branch}`]);
153
+ return true;
154
+ }
155
+ catch {
156
+ return false;
157
+ }
158
+ }
159
+ /** Checkout a PR/review branch into the user's main repo. Unlike the small
160
+ * AgentGitBar switcher, this path may materialize a local tracking branch from
161
+ * `origin/<branch>` because a PR delivery can be remote-only after worktree
162
+ * cleanup or a fresh clone. It never overwrites dirty local work. */
163
+ async function checkoutProjectReviewBranch(repoDir, branch) {
164
+ const target = branch.trim();
165
+ if (!(0, integration_branch_1.isValidBranchName)(target))
166
+ return { ok: false, error: `Invalid branch name: ${branch}` };
167
+ let dirty = false;
168
+ try {
169
+ dirty = (await git(repoDir, ['status', '--porcelain'])) !== '';
170
+ }
171
+ catch (err) {
172
+ return { ok: false, error: err instanceof Error ? err.message : 'git status failed' };
173
+ }
174
+ if (dirty)
175
+ return { ok: false, error: 'Working tree has uncommitted changes. Commit or stash them before checkout.' };
176
+ // Best effort: refresh remote-tracking refs before deciding whether a branch
177
+ // can be materialized. Failure degrades to local-only checkout.
178
+ await git(repoDir, ['fetch', 'origin']).catch(() => { });
179
+ let branches;
180
+ try {
181
+ branches = await listLocalBranches(repoDir);
182
+ }
183
+ catch (err) {
184
+ return { ok: false, error: err instanceof Error ? err.message : 'git failed' };
185
+ }
186
+ try {
187
+ if (branches.includes(target)) {
188
+ await git(repoDir, ['checkout', target]);
189
+ }
190
+ else if (await remoteBranchExists(repoDir, target)) {
191
+ await git(repoDir, ['checkout', '-b', target, '--track', `origin/${target}`]);
192
+ }
193
+ else {
194
+ return { ok: false, error: `Unknown branch: ${target}` };
195
+ }
196
+ }
197
+ catch (err) {
198
+ return { ok: false, error: compactCheckoutError(err instanceof Error ? err.message : 'git checkout failed') };
199
+ }
200
+ // A failed ff-only pull should not undo a successful checkout; the branch is
201
+ // already where the user can inspect/fix it. Surface only hard checkout errors.
202
+ await git(repoDir, ['pull', '--ff-only']).catch(() => { });
203
+ return { ok: true };
204
+ }
@@ -82,6 +82,7 @@ const file_story_1 = require("./file-story");
82
82
  const providers_1 = require("./providers");
83
83
  const pr_publisher_1 = require("./pr-publisher");
84
84
  const active_pr_continuation_1 = require("./active-pr-continuation");
85
+ const rail_worktree_release_1 = require("./rail-worktree-release");
85
86
  /**
86
87
  * Ticket data for the conventional branch name (pr-naming): title/labels from
87
88
  * the ticket-spec reader, the Jira key resolved with the authoritative
@@ -301,6 +302,7 @@ async function launchIsolatedRail(input, io = {}) {
301
302
  originSurface: input.originSurface ?? 'dashboard',
302
303
  originConversationId: input.originConversationId ?? null,
303
304
  }).id;
305
+ input.onPrDeliveryCreated?.(prDeliveryId);
304
306
  if (launchContinuation) {
305
307
  (0, rail_pr_store_1.transitionDecision)(ctx.db, prDeliveryId, 'building', 'building', {
306
308
  branch: launchContinuation.branch,
@@ -393,7 +395,10 @@ async function launchIsolatedRail(input, io = {}) {
393
395
  }
394
396
  // Close the just-inserted decision row (kept as 'discarded' for audit, never
395
397
  // deleted) so a wedged 'building' row can never block relaunching the slot.
396
- if (prDeliveryId && (0, rail_pr_store_1.transitionDecision)(ctx.db, prDeliveryId, 'building', 'discarded')) {
398
+ // Router-driven PR continuations may intentionally fall back to shared-cwd
399
+ // on the same PR branch; in that case the router preserves and updates the
400
+ // row as the live iteration card instead of showing a false discard.
401
+ if (!input.preservePrDeliveryOnAllocationFailure && prDeliveryId && (0, rail_pr_store_1.transitionDecision)(ctx.db, prDeliveryId, 'building', 'discarded')) {
397
402
  broadcastPrState();
398
403
  syncOriginCard('update');
399
404
  }
@@ -497,9 +502,25 @@ async function launchIsolatedRail(input, io = {}) {
497
502
  const worktreeIds = allocated.map((a) => a.ledgerId);
498
503
  const anySucceeded = results.some((r) => r.succeeded);
499
504
  const settledContinuation = anySucceeded ? launchContinuation : null;
505
+ let continuationPushFailed = false;
506
+ let continuationPushFailureReason = null;
507
+ if (settledContinuation) {
508
+ const pushed = await (0, pr_publisher_1.pushBranch)(exec, {
509
+ repoDir: baseRepo,
510
+ branch: settledContinuation.branch,
511
+ baseBranch: settledContinuation.baseBranch,
512
+ });
513
+ if (pushed.state === 'local-only') {
514
+ continuationPushFailed = true;
515
+ continuationPushFailureReason = pushed.reason;
516
+ console.warn(`[rail-isolated] existing PR follow-up push failed for ${settledContinuation.branch}: ${continuationPushFailureReason}`);
517
+ }
518
+ }
500
519
  const next = anySucceeded
501
520
  ? (settledContinuation
502
- ? (settledContinuation.isDraft === false ? 'pr_ready' : 'pr_draft')
521
+ ? (continuationPushFailed
522
+ ? 'pr_failed'
523
+ : (settledContinuation.isDraft === false ? 'pr_ready' : 'pr_draft'))
503
524
  : 'on_review')
504
525
  : 'discarded';
505
526
  const patch = settledContinuation
@@ -509,10 +530,13 @@ async function launchIsolatedRail(input, io = {}) {
509
530
  branch: settledContinuation.branch,
510
531
  prUrl: settledContinuation.prUrl,
511
532
  prNumber: settledContinuation.prNumber,
512
- prState: 'pr-created',
533
+ prState: continuationPushFailed ? 'local-only' : 'pr-created',
513
534
  }
514
535
  : { branches, worktreeIds };
515
536
  if (prDeliveryId && (0, rail_pr_store_1.transitionDecision)(ctx.db, prDeliveryId, 'building', next, patch)) {
537
+ if (settledContinuation && !continuationPushFailed) {
538
+ await (0, rail_worktree_release_1.releaseRailWorktrees)({ db: ctx.db, git, repoDir: baseRepo, worktreeIds });
539
+ }
516
540
  broadcastPrState();
517
541
  // Completion driver: refresh the origin conversation's card in place —
518
542
  // on_review asks the question, discarded informs the outcome.
@@ -67,7 +67,9 @@ exports.executePrDecision = executePrDecision;
67
67
  const path = __importStar(require("path"));
68
68
  const artifact_registry_1 = require("./artifact-registry");
69
69
  const worktree_manager_1 = require("./worktree-manager");
70
+ const pr_publisher_1 = require("./pr-publisher");
70
71
  const rail_pr_delivery_1 = require("./rail-pr-delivery");
72
+ const rail_worktree_release_1 = require("./rail-worktree-release");
71
73
  const pr_naming_1 = require("./pr-naming");
72
74
  const pr_body_1 = require("./pr-body");
73
75
  const jira_db_1 = require("./jira/jira-db");
@@ -171,6 +173,46 @@ function batchWorktreeRoot(slug) {
171
173
  // Mirrors rail-isolated-launch's worktreesRoot — derivable, never stored.
172
174
  return path.join((0, artifact_registry_1.resolveHome)(), '.specrails', 'projects', slug, 'worktrees');
173
175
  }
176
+ async function resolveExistingPrDecisionAfterPush(deps, row) {
177
+ if (!row.pr_url)
178
+ return 'pr_ready';
179
+ const r = await deps.exec.run('gh', ['pr', 'view', row.pr_url, '--json', 'isDraft,state'], deps.project.path);
180
+ if (r.code !== 0)
181
+ return 'pr_ready';
182
+ try {
183
+ const parsed = JSON.parse(r.stdout);
184
+ if (parsed.isDraft === true)
185
+ return 'pr_draft';
186
+ }
187
+ catch {
188
+ /* fall through to the conservative published/open state */
189
+ }
190
+ return 'pr_ready';
191
+ }
192
+ async function retryExistingPrFollowupPush(deps, row) {
193
+ if (!row.branch || !row.pr_url)
194
+ return { status: 502, body: { error: 'push_failed', detail: 'missing PR branch/url' } };
195
+ const pushed = await (0, pr_publisher_1.pushBranch)(deps.exec, {
196
+ repoDir: deps.project.path,
197
+ branch: row.branch,
198
+ baseBranch: row.base_branch,
199
+ });
200
+ if (pushed.state === 'local-only') {
201
+ const conflict = casTransition(deps, row, 'pr_failed', { prState: 'local-only' });
202
+ if (conflict)
203
+ return conflict;
204
+ finalizeTransition(deps, row.id);
205
+ return { status: 200, body: { ok: true, decision: 'pr_failed', prUrl: row.pr_url, detail: pushed.reason } };
206
+ }
207
+ const next = await resolveExistingPrDecisionAfterPush(deps, row);
208
+ const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
209
+ const conflict = casTransition(deps, row, next, { prState: 'pr-created' });
210
+ if (conflict)
211
+ return conflict;
212
+ await (0, rail_worktree_release_1.releaseRailWorktrees)({ db: deps.db, git: deps.git, repoDir: deps.project.path, worktreeIds: snap.worktreeIds });
213
+ const after = finalizeTransition(deps, row.id);
214
+ return { status: 200, body: { ok: true, decision: next, prUrl: after?.prUrl ?? row.pr_url, prState: 'pr-created' } };
215
+ }
174
216
  /**
175
217
  * Load the PR's ticket data (title/labels/description) from the ticket store,
176
218
  * with the Jira key resolved PER TICKET: the authoritative `jira_links` row
@@ -240,6 +282,9 @@ async function findRecoverableBranch(deps, unit, baseBranch) {
240
282
  return null;
241
283
  }
242
284
  async function runCreatePr(deps, row) {
285
+ if (row.decision === 'pr_failed' && row.pr_url && row.branch) {
286
+ return retryExistingPrFollowupPush(deps, row);
287
+ }
243
288
  const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
244
289
  // Pre-flight: every succeeded unit branch must actually exist before we push
245
290
  // it — a missing ref can only degrade to local-only again, so a retry on a
@@ -363,6 +408,9 @@ async function runCreatePr(deps, row) {
363
408
  const conflict = casTransition(deps, row, next, patch);
364
409
  if (conflict)
365
410
  return conflict;
411
+ if (next === 'pr_draft' && patch.prUrl) {
412
+ await (0, rail_worktree_release_1.releaseRailWorktrees)({ db: deps.db, git: deps.git, repoDir: deps.project.path, worktreeIds: snap.worktreeIds });
413
+ }
366
414
  const after = finalizeTransition(deps, row.id);
367
415
  // Tickets stay on_review — a draft PR is still awaiting the engineer's merge.
368
416
  return {
@@ -380,9 +428,11 @@ async function runPublish(deps, row) {
380
428
  const r = await deps.exec.run('gh', ['pr', 'ready', row.pr_url], deps.project.path);
381
429
  if (r.code !== 0)
382
430
  return ghFailed(r); // no transition — the draft is still a draft
431
+ const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
383
432
  const conflict = casTransition(deps, row, 'pr_ready');
384
433
  if (conflict)
385
434
  return conflict;
435
+ await (0, rail_worktree_release_1.releaseRailWorktrees)({ db: deps.db, git: deps.git, repoDir: deps.project.path, worktreeIds: snap.worktreeIds });
386
436
  finalizeTransition(deps, row.id);
387
437
  return { status: 200, body: { ok: true, decision: 'pr_ready', prUrl: row.pr_url } };
388
438
  }
@@ -467,11 +517,29 @@ async function runPollMerge(deps, row) {
467
517
  // Not merged (OPEN/CLOSED/unknown) → observation only, nothing changes.
468
518
  return { status: 200, body: { ok: true, decision: row.decision, merged: false } };
469
519
  }
520
+ const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
521
+ const branchSet = new Set(snap.branches.map((b) => b.branch));
522
+ if (row.branch)
523
+ branchSet.add(row.branch);
524
+ const succeededUnits = snap.branches.filter((b) => b.succeeded);
525
+ if (succeededUnits.length > 1) {
526
+ try {
527
+ branchSet.add((0, pr_naming_1.batchBranchNameFor)(loadPrTicketData(deps, succeededUnits.map((b) => b.ticketId))));
528
+ }
529
+ catch {
530
+ /* best-effort cleanup only */
531
+ }
532
+ }
533
+ await (0, rail_worktree_release_1.releaseRailWorktrees)({ db: deps.db, git: deps.git, repoDir: deps.project.path, worktreeIds: snap.worktreeIds, state: 'merged' });
534
+ for (const branch of branchSet) {
535
+ if (!branch || branch === row.base_branch)
536
+ continue;
537
+ await deps.git.run(['branch', '-D', branch], deps.project.path).catch(() => { });
538
+ }
470
539
  const conflict = casTransition(deps, row, 'merged');
471
540
  if (conflict)
472
541
  return conflict;
473
542
  finalizeTransition(deps, row.id);
474
- const snap = (0, rail_pr_store_1.toPrDeliverySnapshot)(row);
475
543
  const changed = applyTicketTransition(deps, snap.ticketIds, 'done');
476
544
  try {
477
545
  deps.jiraSyncManager?.onRailMerged(changed, row.id, row.pr_url);