circuschief 0.6.0 → 0.8.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.
Files changed (187) hide show
  1. package/package.json +1 -1
  2. package/packages/server/src/agents/adapters/CodexAdapter.js +5 -4
  3. package/packages/server/src/api/canvas.js +22 -57
  4. package/packages/server/src/api/index.js +2 -0
  5. package/packages/server/src/api/kanban.js +4 -2
  6. package/packages/server/src/api/projects-helpers.js +20 -3
  7. package/packages/server/src/api/projects-session-helpers.js +35 -4
  8. package/packages/server/src/api/projects.js +11 -0
  9. package/packages/server/src/api/providers.js +11 -1
  10. package/packages/server/src/api/sessions-commands.js +35 -17
  11. package/packages/server/src/api/sessions-draft.js +1 -0
  12. package/packages/server/src/api/sessions-lifecycle.js +10 -10
  13. package/packages/server/src/api/sessions-patch.js +4 -0
  14. package/packages/server/src/api/sessions.js +6 -5
  15. package/packages/server/src/api/settings.js +52 -4
  16. package/packages/server/src/database.js +0 -2
  17. package/packages/server/src/db/ConversationRepository.js +16 -3
  18. package/packages/server/src/db/DatabaseManager.js +5 -1
  19. package/packages/server/src/db/ProjectDefaultsRepository.js +50 -40
  20. package/packages/server/src/db/ProviderRepository.js +87 -32
  21. package/packages/server/src/db/SessionRepository.js +13 -8
  22. package/packages/server/src/db/SettingsRepository.js +44 -16
  23. package/packages/server/src/db/conversation-helpers.js +1 -0
  24. package/packages/server/src/db/index.js +0 -3
  25. package/packages/server/src/db/migrations/index.js +36 -200
  26. package/packages/server/src/db/seedBaselineData.js +137 -0
  27. package/packages/server/src/db/session-helpers.js +9 -3
  28. package/packages/server/src/middleware/sessionLookup.js +81 -8
  29. package/packages/server/src/schema.sql +157 -132
  30. package/packages/server/src/scripts/backupDatabase.js +21 -0
  31. package/packages/server/src/scripts/dbUtils.js +81 -0
  32. package/packages/server/src/scripts/inspectDatabaseSchema.js +81 -0
  33. package/packages/server/src/scripts/validateDatabaseBaseline.js +212 -0
  34. package/packages/server/src/services/agentCallLogger.js +1 -1
  35. package/packages/server/src/services/codexSpawnHelper.js +9 -0
  36. package/packages/server/src/services/commandButtonPrompts.js +48 -0
  37. package/packages/server/src/services/commandRunner.js +7 -1
  38. package/packages/server/src/services/draftSessionService.js +2 -0
  39. package/packages/server/src/services/e2eSpawnCapture.js +147 -0
  40. package/packages/server/src/services/gitCommitAttribution.js +120 -0
  41. package/packages/server/src/services/gitService.js +11 -2
  42. package/packages/server/src/services/gitSessionSetup.js +11 -1
  43. package/packages/server/src/services/kanbanTriggers.js +6 -3
  44. package/packages/server/src/services/nodeSpawnHelper.js +9 -0
  45. package/packages/server/src/services/prUrlService.js +3 -3
  46. package/packages/server/src/services/queryParamBuilder.js +90 -0
  47. package/packages/server/src/services/sessionDuplicator.js +1 -5
  48. package/packages/server/src/services/sessionExecution.js +56 -106
  49. package/packages/server/src/services/sessionPrompts.js +16 -47
  50. package/packages/server/src/services/sessionProvider.js +16 -8
  51. package/packages/server/src/services/streamEventCallbacks.js +72 -40
  52. package/packages/server/src/services/streamEventHandler.js +13 -2
  53. package/packages/server/src/services/streamUsageHandler.js +6 -0
  54. package/packages/server/src/services/summaryClaudeClient.js +37 -12
  55. package/packages/server/src/services/summaryModelClient.js +154 -0
  56. package/packages/server/src/services/summaryModelResolver.js +148 -0
  57. package/packages/server/src/services/summaryService.js +11 -7
  58. package/packages/server/src/services/summaryStaleCheck.js +23 -4
  59. package/packages/server/src/services/templateTriggerService.js +3 -1
  60. package/packages/server/src/services/usageTracker.js +5 -1
  61. package/packages/server/src/services/visibleFinalErrorMessage.js +123 -0
  62. package/packages/shared/src/constants.js +4 -2
  63. package/packages/shared/src/contracts/commandButtons.js +16 -2
  64. package/packages/shared/src/contracts/projects.js +4 -2
  65. package/packages/shared/src/contracts/providers.js +60 -0
  66. package/packages/shared/src/contracts/sessions.js +2 -1
  67. package/packages/shared/src/contracts/templates.js +2 -2
  68. package/packages/shared/src/types.js +1 -9
  69. package/packages/shared/src/utils.js +11 -19
  70. package/packages/web/dist/assets/ActiveSessionsView-B0XHqLmv.js +1 -0
  71. package/packages/web/dist/assets/{AgentLogsView-Cdw4nmvd.js → AgentLogsView-DmsjUMlB.js} +2 -2
  72. package/packages/web/dist/assets/ApiClient-C3ztI9s9.js +1 -0
  73. package/packages/web/dist/assets/ArchiveConfirmModal-BlCyn5Vt.js +1 -0
  74. package/packages/web/dist/assets/ArchiveConfirmModal-DeoCVGXt.css +1 -0
  75. package/packages/web/dist/assets/{CommandButtonDetailView-DnFhJY5A.js → CommandButtonDetailView-CdSCPp78.js} +1 -1
  76. package/packages/web/dist/assets/EffortLevelSelector-hc2MNKg6.js +1 -0
  77. package/packages/web/dist/assets/{GeneralSettingsView-CQkmdczf.js → GeneralSettingsView-D1nI8_zk.js} +1 -1
  78. package/packages/web/dist/assets/InputWithButton-CAkttyqx.js +1 -0
  79. package/packages/web/dist/assets/{InputWithButton-cYdrEmTs.css → InputWithButton-D9HMvfR5.css} +1 -1
  80. package/packages/web/dist/assets/{InterpolationHelp-PfYR3KJo.js → InterpolationHelp-BO1j9Z3_.js} +1 -1
  81. package/packages/web/dist/assets/MarkdownEditor-ucRAP_UM.js +2 -0
  82. package/packages/web/dist/assets/{ModelSelector-BZOT1Jc6.css → ModelSelector-BSxKUSus.css} +1 -1
  83. package/packages/web/dist/assets/ModelSelector-CwTz8ZWO.js +1 -0
  84. package/packages/web/dist/assets/NewSessionView-BDPb-1qr.css +1 -0
  85. package/packages/web/dist/assets/NewSessionView-BsDrp8mj.js +3 -0
  86. package/packages/web/dist/assets/ProjectEditView-CwTOeSun.js +1 -0
  87. package/packages/web/dist/assets/ProjectEditView-J15mcsWz.css +1 -0
  88. package/packages/web/dist/assets/{ProjectListView-CuYMmd3O.js → ProjectListView-DcNyuINs.js} +1 -1
  89. package/packages/web/dist/assets/{ProjectNewView-CNaA4Maf.js → ProjectNewView-B5YV62hv.js} +1 -1
  90. package/packages/web/dist/assets/ProvidersView-bZemq_Rv.css +1 -0
  91. package/packages/web/dist/assets/ProvidersView-nY9GnDdO.js +1 -0
  92. package/packages/web/dist/assets/QuickResponseSettings-B352c75l.css +1 -0
  93. package/packages/web/dist/assets/QuickResponseSettings-BQwQXuL7.js +1 -0
  94. package/packages/web/dist/assets/{QuickResponsesPanel-DIBQFj0W.css → QuickResponsesPanel-BlFDvnZ2.css} +1 -1
  95. package/packages/web/dist/assets/{QuickResponsesPanel-BqMYSHb0.js → QuickResponsesPanel-BzSYcCSP.js} +1 -1
  96. package/packages/web/dist/assets/{ResizableTextarea-wYF3K2RO.js → ResizableTextarea-B3YIdIXv.js} +1 -1
  97. package/packages/web/dist/assets/{SessionCard-bLaQEWWX.js → SessionCard-CjE1tXiT.js} +1 -1
  98. package/packages/web/dist/assets/SessionDetailView-3cPZrbS3.js +36 -0
  99. package/packages/web/dist/assets/SessionDetailView-CZRZMrfM.css +1 -0
  100. package/packages/web/dist/assets/SessionFormOptions-B6AxyREh.js +1 -0
  101. package/packages/web/dist/assets/SessionFormOptions-BpUALRKn.css +1 -0
  102. package/packages/web/dist/assets/SessionListView-B5_6gW49.css +1 -0
  103. package/packages/web/dist/assets/SessionListView-CLXBfLcq.js +1 -0
  104. package/packages/web/dist/assets/{SessionLogStream-DTnDAF95.js → SessionLogStream-LlZ3z_Xj.js} +1 -1
  105. package/packages/web/dist/assets/{SettingsView-DNLUSsHV.js → SettingsView-CTGiGvR2.js} +1 -1
  106. package/packages/web/dist/assets/{SlashCommandWizard-CRGFaO8t.js → SlashCommandWizard-Cy04d7-o.js} +1 -1
  107. package/packages/web/dist/assets/{SlashCommandWizard-Dn7sNaBd.css → SlashCommandWizard-DJzw3LP5.css} +1 -1
  108. package/packages/web/dist/assets/SummarySettingsView-BR2ZjEa3.js +1 -0
  109. package/packages/web/dist/assets/SummarySettingsView-l2bxHmZZ.css +1 -0
  110. package/packages/web/dist/assets/TemplateDetailView-DH6Oswsp.js +1 -0
  111. package/packages/web/dist/assets/{commandButtons-Bbjf3fCt.js → commandButtons-BfqR-fqq.js} +1 -1
  112. package/packages/web/dist/assets/index-1zziPL6l.js +1 -0
  113. package/packages/web/dist/assets/index-7kzHPxSF.js +1 -0
  114. package/packages/web/dist/assets/index-B0N_obMc.js +1 -0
  115. package/packages/web/dist/assets/index-BNk_gdfI.js +1 -0
  116. package/packages/web/dist/assets/{index-gmCCsCQ1.css → index-BY174HVJ.css} +1 -1
  117. package/packages/web/dist/assets/index-CSqaAH-0.js +1 -0
  118. package/packages/web/dist/assets/index-C_q4WlK8.js +1 -0
  119. package/packages/web/dist/assets/index-D1wpU4y0.js +7 -0
  120. package/packages/web/dist/assets/index-D5zCA8sD.js +1 -0
  121. package/packages/web/dist/assets/index-DGR8ELWY.js +1 -0
  122. package/packages/web/dist/assets/index-DHga8pXo.js +1 -0
  123. package/packages/web/dist/assets/index-DSby02Wl.js +1 -0
  124. package/packages/web/dist/assets/{index-Cf6vdW-B.js → index-DgkC10TW.js} +3 -3
  125. package/packages/web/dist/assets/index-DqjXJTVI.js +1 -0
  126. package/packages/web/dist/assets/{index-Bs7Qf5D6.js → index-DtfUt785.js} +2 -2
  127. package/packages/web/dist/assets/index-_4S2uLDI.js +1 -0
  128. package/packages/web/dist/assets/{index-BQL_L4gL.js → index-fK8FIZgP.js} +15 -14
  129. package/packages/web/dist/assets/index-gmiZeFXN.js +1 -0
  130. package/packages/web/dist/assets/index-irD539ZM.js +3 -0
  131. package/packages/web/dist/assets/index-yq-E1Y00.js +1 -0
  132. package/packages/web/dist/assets/{projects-CPt3AB7U.js → projects-DXYQNJIi.js} +1 -1
  133. package/packages/web/dist/assets/{providers-ChfeMvUq.js → providers-1bnH-exJ.js} +1 -1
  134. package/packages/web/dist/assets/sessions-6zGUlFrt.js +1 -0
  135. package/packages/web/dist/assets/settings-MbfRir0d.js +1 -0
  136. package/packages/web/dist/index.html +2 -2
  137. package/packages/server/src/api/sessions-notes.js +0 -51
  138. package/packages/server/src/db/SessionNoteRepository.js +0 -60
  139. package/packages/server/src/db/migrations/canvasItemsMigrations.js +0 -109
  140. package/packages/server/src/db/migrations/conversationsMigrations.js +0 -183
  141. package/packages/server/src/db/migrations/kanbanMigrations.js +0 -99
  142. package/packages/server/src/db/migrations/miscMigrations.js +0 -369
  143. package/packages/server/src/db/migrations/projectsMigrations.js +0 -99
  144. package/packages/server/src/db/migrations/sessionsMigrations.js +0 -282
  145. package/packages/web/dist/assets/ActiveSessionsView-UCbQrF1b.js +0 -1
  146. package/packages/web/dist/assets/ApiClient-CWbXWDUY.js +0 -1
  147. package/packages/web/dist/assets/ArchiveConfirmModal-BQ-4gI0R.css +0 -1
  148. package/packages/web/dist/assets/ArchiveConfirmModal-J48eh3zw.js +0 -1
  149. package/packages/web/dist/assets/EffortLevelSelector-bXbPo4Zw.js +0 -1
  150. package/packages/web/dist/assets/InputWithButton-XyM3k6lN.js +0 -1
  151. package/packages/web/dist/assets/MarkdownEditor-P8F5kO-o.js +0 -2
  152. package/packages/web/dist/assets/ModelSelector-CowKfGMP.js +0 -1
  153. package/packages/web/dist/assets/NewSessionView-D_Hi7M9g.css +0 -1
  154. package/packages/web/dist/assets/NewSessionView-DkjFLvHU.js +0 -3
  155. package/packages/web/dist/assets/ProjectEditView-CpeKj-_w.css +0 -1
  156. package/packages/web/dist/assets/ProjectEditView-embVT7NC.js +0 -1
  157. package/packages/web/dist/assets/ProvidersView-C7rydtOd.js +0 -1
  158. package/packages/web/dist/assets/ProvidersView-DE82G_5W.css +0 -1
  159. package/packages/web/dist/assets/QuickResponseSettings-B8188A1D.css +0 -1
  160. package/packages/web/dist/assets/QuickResponseSettings-BTQEKhwJ.js +0 -1
  161. package/packages/web/dist/assets/SessionDetailView-Cv-xMzXp.css +0 -1
  162. package/packages/web/dist/assets/SessionDetailView-CvQOUsW2.js +0 -36
  163. package/packages/web/dist/assets/SessionFormOptions-3pzbgI2Q.js +0 -1
  164. package/packages/web/dist/assets/SessionFormOptions-DhhIkjIS.css +0 -1
  165. package/packages/web/dist/assets/SessionListView-Dranfb72.js +0 -1
  166. package/packages/web/dist/assets/SessionListView-fHlQyecX.css +0 -1
  167. package/packages/web/dist/assets/SummarySettingsView-C7G_suHp.js +0 -1
  168. package/packages/web/dist/assets/SummarySettingsView-DcsmSVJI.css +0 -1
  169. package/packages/web/dist/assets/TemplateDetailView-B78_DLMR.js +0 -1
  170. package/packages/web/dist/assets/index--V7c-VZf.js +0 -1
  171. package/packages/web/dist/assets/index-8Q04yd7H.js +0 -1
  172. package/packages/web/dist/assets/index-B47XRBDH.js +0 -1
  173. package/packages/web/dist/assets/index-BXbgZrhS.js +0 -1
  174. package/packages/web/dist/assets/index-CGhDVPen.js +0 -1
  175. package/packages/web/dist/assets/index-CKcRO1A6.js +0 -1
  176. package/packages/web/dist/assets/index-CTq-SLIW.js +0 -1
  177. package/packages/web/dist/assets/index-CYyos3iC.js +0 -1
  178. package/packages/web/dist/assets/index-CsCREAxF.js +0 -1
  179. package/packages/web/dist/assets/index-DJTTk_8T.js +0 -3
  180. package/packages/web/dist/assets/index-DPqUJ5JK.js +0 -1
  181. package/packages/web/dist/assets/index-EwAe1dKg.js +0 -1
  182. package/packages/web/dist/assets/index-JBA8axyA.js +0 -1
  183. package/packages/web/dist/assets/index-JkVHFtK5.js +0 -7
  184. package/packages/web/dist/assets/index-gMPUwT55.js +0 -1
  185. package/packages/web/dist/assets/index-wadc_0zT.js +0 -1
  186. package/packages/web/dist/assets/sessions-CwPsJOb1.js +0 -1
  187. package/packages/web/dist/assets/settings-BOj6wq6t.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circuschief",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Local-first web UI for managing Claude Code sessions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,16 +14,17 @@ let codexCliUnavailable = false;
14
14
  *
15
15
  * Two execution paths:
16
16
  *
17
- * 1. CLI path (default) — spawns the `codex --json ...` CLI and parses its
17
+ * 1. CLI path (default) — spawns `codex exec --json ...` and parses its
18
18
  * line-delimited JSON stdout. Uses {@link createCodexEventMapper} to
19
19
  * normalize events into the SDK-shaped envelope the rest of the app
20
20
  * already understands.
21
21
  *
22
22
  * 2. Direct-API path — activated by {@code USE_CODEX_DIRECT_API=1}. Uses
23
23
  * the official {@code openai} SDK with Chat Completions streaming
24
- * against the provider's configured baseURL/apiKey. Intended for
25
- * environments where the Codex CLI isn't installable and for the
26
- * Step-0 contingency path in the implementation plan.
24
+ * against the provider's configured baseURL/apiKey. It bypasses
25
+ * CLI-specific behavior such as sandbox enforcement and Codex
26
+ * commit-attribution config. Intended for
27
+ * environments where the Codex CLI isn't installable.
27
28
  *
28
29
  * Capabilities in v1:
29
30
  * - streaming: true
@@ -2,10 +2,11 @@ import { Router } from 'express';
2
2
  import { readFileSync, existsSync, statSync } from 'fs';
3
3
  import { mkdir } from 'fs/promises';
4
4
  import { extname, join, basename } from 'path';
5
- import { sessions, canvasItems } from '../database.js';
5
+ import { canvasItems } from '../database.js';
6
6
  import { broadcastToSession } from '../websocket.js';
7
7
  import { WS_MESSAGE_TYPES, UpdateCanvasItemRequest } from '../../../shared/src/index.js';
8
8
  import { upload, handleUploadError } from '../middleware/upload.js';
9
+ import { requireRootSessionAndProject } from '../middleware/sessionLookup.js';
9
10
  import {
10
11
  isBinaryContent,
11
12
  getTypeFromExtension,
@@ -19,8 +20,6 @@ import {
19
20
  import trashRoutes from './canvas-trash-routes.js';
20
21
 
21
22
  // Error message constants
22
- const ERR_SESSION_NOT_FOUND = 'Session not found';
23
-
24
23
  /**
25
24
  * Process multipart file upload (Mode 1)
26
25
  * @param {Express.Multer.File} file - The uploaded file from multer
@@ -84,12 +83,7 @@ router.use('/', trashRoutes);
84
83
  // 1. Multipart mode: FormData with 'file' field - from browser file uploads
85
84
  // 2. File mode: { filePath } - reads file from disk
86
85
  // 3. Inline mode: { type, content, filename } - uses provided content directly
87
- router.post('/:id/canvas', upload.single('file'), handleUploadError, (req, res) => {
88
- const session = sessions.getById(req.params.id);
89
- if (!session) {
90
- return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
91
- }
92
-
86
+ router.post('/:id/canvas', requireRootSessionAndProject, upload.single('file'), handleUploadError, (req, res) => {
93
87
  const { filePath, type, content, filename } = req.body;
94
88
  let result;
95
89
 
@@ -110,21 +104,16 @@ router.post('/:id/canvas', upload.single('file'), handleUploadError, (req, res)
110
104
  return res.status(400).json({ error: result.error });
111
105
  }
112
106
 
113
- const item = canvasItems.create(req.params.id, result.itemData);
107
+ const item = canvasItems.create(req.rootSessionId, result.itemData);
114
108
 
115
109
  // Broadcast to session subscribers
116
- broadcastCanvasUpdate(req.params.id, item);
110
+ broadcastCanvasUpdate(req.rootSessionId, item);
117
111
 
118
112
  res.status(201).json(item);
119
113
  });
120
114
 
121
115
  // PUT /api/sessions/:id/canvas/:itemId - Update canvas item content in-place
122
- router.put('/:id/canvas/:itemId', (req, res) => {
123
- const session = sessions.getById(req.params.id);
124
- if (!session) {
125
- return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
126
- }
127
-
116
+ router.put('/:id/canvas/:itemId', requireRootSessionAndProject, (req, res) => {
128
117
  const parsed = UpdateCanvasItemRequest.safeParse(req.body);
129
118
  if (!parsed.success) {
130
119
  return res.status(400).json({ error: 'Invalid request body', details: parsed.error.issues });
@@ -135,7 +124,7 @@ router.put('/:id/canvas/:itemId', (req, res) => {
135
124
  return res.status(404).json({ error: 'Canvas item not found' });
136
125
  }
137
126
 
138
- if (item.sessionId !== req.params.id) {
127
+ if (item.sessionId !== req.rootSessionId) {
139
128
  return res.status(400).json({ error: 'Canvas item does not belong to this session' });
140
129
  }
141
130
 
@@ -144,20 +133,15 @@ router.put('/:id/canvas/:itemId', (req, res) => {
144
133
  }
145
134
 
146
135
  const updatedItem = canvasItems.updateContent(req.params.itemId, parsed.data.content);
147
- broadcastToSession(req.params.id, WS_MESSAGE_TYPES.CANVAS_UPDATE, { item: updatedItem });
136
+ broadcastToSession(req.rootSessionId, WS_MESSAGE_TYPES.CANVAS_UPDATE, { item: updatedItem });
148
137
  res.json(updatedItem);
149
138
  });
150
139
 
151
140
  // GET /api/sessions/:id/canvas - List canvas items
152
141
  // Returns only latest version of each file (no version metadata exposed)
153
- router.get('/:id/canvas', (req, res) => {
154
- const session = sessions.getById(req.params.id);
155
- if (!session) {
156
- return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
157
- }
158
-
142
+ router.get('/:id/canvas', requireRootSessionAndProject, (req, res) => {
159
143
  // Get only latest versions (one per filename)
160
- const items = canvasItems.getLatestVersionsBySessionId(req.params.id);
144
+ const items = canvasItems.getLatestVersionsBySessionId(req.rootSessionId);
161
145
 
162
146
  // Strip content/data from list responses to reduce payload size.
163
147
  // Clients should use GET /canvas/file/:filename/content for inline content.
@@ -166,14 +150,9 @@ router.get('/:id/canvas', (req, res) => {
166
150
 
167
151
  // GET /api/sessions/:id/canvas/all - List ALL canvas items (including all versions)
168
152
  // Returns all versions of each file for the frontend UI
169
- router.get('/:id/canvas/all', (req, res) => {
170
- const session = sessions.getById(req.params.id);
171
- if (!session) {
172
- return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
173
- }
174
-
153
+ router.get('/:id/canvas/all', requireRootSessionAndProject, (req, res) => {
175
154
  // Get ALL versions (not just latest)
176
- const items = canvasItems.getBySessionId(req.params.id);
155
+ const items = canvasItems.getBySessionId(req.rootSessionId);
177
156
 
178
157
  // Strip content/data from list responses to reduce payload size.
179
158
  res.json(items.map(({ content: _content, data: _data, ...meta }) => meta));
@@ -182,15 +161,11 @@ router.get('/:id/canvas/all', (req, res) => {
182
161
  // GET /api/sessions/:id/canvas/file/:filename/history/:version - Get historical version of canvas file
183
162
  // Uses standard versioning: version 1 = oldest, version N = latest (where N = totalVersions)
184
163
  // NOTE: This route must be defined BEFORE the main file route to match correctly
185
- router.get('/:id/canvas/file/:filename/history/:version', async (req, res) => {
186
- const { id: sessionId, filename, version } = req.params;
164
+ router.get('/:id/canvas/file/:filename/history/:version', requireRootSessionAndProject, async (req, res) => {
165
+ const { filename, version } = req.params;
166
+ const sessionId = req.rootSessionId;
187
167
  const versionNum = parseInt(version, 10);
188
168
 
189
- const session = sessions.getById(sessionId);
190
- if (!session) {
191
- return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
192
- }
193
-
194
169
  // Get all versions of this file (newest first)
195
170
  const allVersions = canvasItems.getAllVersionsByFilename(sessionId, filename);
196
171
  if (allVersions.length === 0) {
@@ -247,11 +222,8 @@ router.get('/:id/canvas/file/:filename/history/:version', async (req, res) => {
247
222
  // GET /api/sessions/:id/canvas/file/:filename/content - Get canvas file content inline
248
223
  // Returns content/data inline in JSON for browser consumption (unlike the temp-file endpoint below)
249
224
  // Supports ?version=N query param (1-based, 1 = oldest)
250
- router.get('/:id/canvas/file/:filename/content', (req, res) => {
251
- const session = sessions.getById(req.params.id);
252
- if (!session) return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
253
-
254
- const allVersions = canvasItems.getAllVersionsByFilename(req.params.id, req.params.filename);
225
+ router.get('/:id/canvas/file/:filename/content', requireRootSessionAndProject, (req, res) => {
226
+ const allVersions = canvasItems.getAllVersionsByFilename(req.rootSessionId, req.params.filename);
255
227
  if (allVersions.length === 0) return res.status(404).json({ error: 'File not found' });
256
228
 
257
229
  // Support ?version=N (1-based, 1 = oldest)
@@ -280,13 +252,10 @@ router.get('/:id/canvas/file/:filename/content', (req, res) => {
280
252
  });
281
253
 
282
254
  // GET /api/sessions/:id/canvas/:itemId/content - Get one canvas item content inline
283
- router.get('/:id/canvas/:itemId/content', (req, res) => {
284
- const session = sessions.getById(req.params.id);
285
- if (!session) return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
286
-
255
+ router.get('/:id/canvas/:itemId/content', requireRootSessionAndProject, (req, res) => {
287
256
  const item = canvasItems.getById(req.params.itemId);
288
257
  if (!item) return res.status(404).json({ error: 'Canvas item not found' });
289
- if (item.sessionId !== req.params.id) {
258
+ if (item.sessionId !== req.rootSessionId) {
290
259
  return res.status(400).json({ error: 'Canvas item does not belong to this session' });
291
260
  }
292
261
 
@@ -302,13 +271,9 @@ router.get('/:id/canvas/:itemId/content', (req, res) => {
302
271
  // GET /api/sessions/:id/canvas/file/:filename - Get canvas file by filename
303
272
  // Writes the file to /tmp and returns the file path for Claude's Read tool
304
273
  // Always returns the latest version
305
- router.get('/:id/canvas/file/:filename', async (req, res) => {
306
- const { id: sessionId, filename } = req.params;
307
-
308
- const session = sessions.getById(sessionId);
309
- if (!session) {
310
- return res.status(404).json({ error: ERR_SESSION_NOT_FOUND });
311
- }
274
+ router.get('/:id/canvas/file/:filename', requireRootSessionAndProject, async (req, res) => {
275
+ const { filename } = req.params;
276
+ const sessionId = req.rootSessionId;
312
277
 
313
278
  // Get all versions of this file (newest first)
314
279
  const allVersions = canvasItems.getAllVersionsByFilename(sessionId, filename);
@@ -14,6 +14,7 @@ import kanbanRouter from './kanban.js';
14
14
  import agentsRouter from './agents.js';
15
15
  import { getDbPath } from '../database.js';
16
16
  import { schedulerService } from '../services/schedulerService.js';
17
+ import { isE2ESpawnCaptureEnabled } from '../services/e2eSpawnCapture.js';
17
18
 
18
19
  const router = Router();
19
20
 
@@ -29,6 +30,7 @@ router.get('/server-info', (_req, res) => {
29
30
  dbPath: getDbPath(),
30
31
  vcrMode: vcr && vcr.length > 0 ? vcr : null,
31
32
  schedulerRunning: schedulerService.isRunning(),
33
+ e2eSpawnCaptureEnabled: isE2ESpawnCaptureEnabled(),
32
34
  });
33
35
  });
34
36
 
@@ -11,6 +11,7 @@ import {
11
11
  ReorderKanbanCardsRequest,
12
12
  } from '../../../shared/src/contracts/kanban.js';
13
13
  import { moveCard as moveCardService } from '../services/kanbanService.js';
14
+ import { resolveBodyRootSessionForProject } from '../middleware/sessionLookup.js';
14
15
 
15
16
  const router = Router({ mergeParams: true });
16
17
 
@@ -215,7 +216,7 @@ router.put('/lanes/reorder', (req, res) => {
215
216
  * POST /api/projects/:projectId/kanban/cards
216
217
  * Add a session to the board (create card in a lane)
217
218
  */
218
- router.post('/cards', (req, res) => {
219
+ router.post('/cards', resolveBodyRootSessionForProject('projectId'), (req, res) => {
219
220
  const { projectId } = req.params;
220
221
 
221
222
  const result = CreateKanbanCardRequest.safeParse(req.body);
@@ -223,7 +224,8 @@ router.post('/cards', (req, res) => {
223
224
  return res.status(400).json({ error: result.error.issues[0].message });
224
225
  }
225
226
 
226
- const { sessionId, laneId } = result.data;
227
+ const { laneId } = result.data;
228
+ const sessionId = req.bodyRootSessionId;
227
229
 
228
230
  // Check if session already has a card
229
231
  const existingCard = kanbanCards.getBySessionId(sessionId);
@@ -1,22 +1,39 @@
1
+ import { generateWorktreeBranch } from '../../../shared/src/index.js';
1
2
  import { isGitRepo } from '../services/gitService.js';
2
3
 
3
4
  /**
4
5
  * Validate and default git settings for git-backed projects.
5
6
  * If gitMode or gitBranch are missing for a git project, defaults are applied
6
- * (gitMode: 'none', gitBranch: 'main') instead of rejecting the request.
7
+ * instead of rejecting the request. Worktree sessions receive a generated branch
8
+ * so they never try to check out an already-active branch such as main.
9
+ * Current-mode sessions skip branch generation entirely.
7
10
  * @param {Object} config - The session configuration
8
11
  * @param {Object} project - The project object
9
12
  * @returns {Promise<{config: Object, error: string|null}>} Updated config and error message if validation fails.
10
13
  */
11
14
  export async function validateGitSettings(config, project) {
15
+ // Current mode: no branch needed, pass through as-is
16
+ if (config.gitMode === 'current') {
17
+ return {
18
+ config: {
19
+ ...config,
20
+ gitBranch: null,
21
+ },
22
+ error: null,
23
+ };
24
+ }
25
+
12
26
  if (!config.gitMode || !config.gitBranch) {
13
27
  const isGit = await isGitRepo(project.workingDirectory);
14
28
  if (isGit) {
29
+ const gitMode = config.gitMode || 'none';
15
30
  return {
16
31
  config: {
17
32
  ...config,
18
- gitMode: config.gitMode || 'none',
19
- gitBranch: config.gitBranch || 'main',
33
+ gitMode,
34
+ gitBranch: config.gitBranch || (gitMode === 'worktree'
35
+ ? generateWorktreeBranch(config.name, config.prompt)
36
+ : 'main'),
20
37
  },
21
38
  error: null,
22
39
  };
@@ -1,9 +1,10 @@
1
1
  import { sessions, sessionTemplates, attachments } from '../database.js';
2
2
  import * as slashCommandService from '../services/slashCommandService.js';
3
3
  import { setupGitForSession } from '../services/gitSessionSetup.js';
4
+ import { resolveProviderMetadataFromModel } from '../services/sessionProvider.js';
4
5
  import { executeHookAsync } from '../services/hookService.js';
5
6
  import { broadcastToProject } from '../websocket.js';
6
- import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
7
+ import { WS_MESSAGE_TYPES, DEFAULT_RESCHEDULE_DELAY_MINUTES } from '../../../shared/src/index.js';
7
8
 
8
9
  /**
9
10
  * Generate an initial session name from the prompt
@@ -44,6 +45,26 @@ export function resolveDefault(explicit, projectDefault, systemDefault) {
44
45
  return systemDefault;
45
46
  }
46
47
 
48
+ /**
49
+ * Normalize provider IDs from JSON or multipart request fields.
50
+ * @param {*} value - Raw providerId value
51
+ * @returns {string|null|undefined}
52
+ */
53
+ export function normalizeProviderId(value) {
54
+ if (value === undefined) return undefined;
55
+ if (value === null || value === '') return null;
56
+ if (typeof value !== 'string') {
57
+ throw new TypeError('providerId must be a string or null');
58
+ }
59
+ return value;
60
+ }
61
+
62
+ function resolveProviderDefault(explicit, projectDefault, systemDefault) {
63
+ if (explicit !== undefined) return explicit;
64
+ if (projectDefault !== undefined && projectDefault !== null) return projectDefault;
65
+ return systemDefault ?? null;
66
+ }
67
+
47
68
  /**
48
69
  * Resolve thinkingEnabled with special boolean handling.
49
70
  * @param {object} body - Request body
@@ -71,7 +92,7 @@ export function parseSchedulingConfig(body) {
71
92
  return {
72
93
  scheduledAt: body.scheduledAt ? parseInt(body.scheduledAt, 10) : undefined,
73
94
  autoRescheduleEnabled: body.autoRescheduleEnabled === true || body.autoRescheduleEnabled === 'true',
74
- rescheduleDelayMinutes: body.rescheduleDelayMinutes ? parseInt(body.rescheduleDelayMinutes, 10) : 15,
95
+ rescheduleDelayMinutes: body.rescheduleDelayMinutes ? parseInt(body.rescheduleDelayMinutes, 10) : DEFAULT_RESCHEDULE_DELAY_MINUTES,
75
96
  rescheduleOnTokenLimit: body.rescheduleOnTokenLimit !== false && body.rescheduleOnTokenLimit !== 'false',
76
97
  rescheduleOnServiceError: body.rescheduleOnServiceError !== false && body.rescheduleOnServiceError !== 'false',
77
98
  maxRescheduleCount: body.maxRescheduleCount ? parseInt(body.maxRescheduleCount, 10) : null,
@@ -114,14 +135,19 @@ export function prepareSessionConfig(body, projectDefs, systemDefaults) {
114
135
  effortLevel = null;
115
136
  }
116
137
 
138
+ const explicitProviderId = normalizeProviderId(body.providerId);
139
+ const projectProviderId = normalizeProviderId(projectDefs?.providerId);
140
+ const systemProviderId = normalizeProviderId(systemDefaults.providerId);
141
+
117
142
  return {
118
143
  prompt: body.prompt,
119
144
  name: body.name,
120
145
  mode: resolveDefault(body.mode, projectDefs?.mode, systemDefaults.mode),
121
146
  model: resolveDefault(body.model, projectDefs?.model, systemDefaults.model || null),
147
+ providerId: resolveProviderDefault(explicitProviderId, projectProviderId, systemProviderId),
122
148
  effortLevel,
123
149
  gitBranch: resolveDefault(body.gitBranch, projectDefs?.gitBranch, null),
124
- gitMode: resolveDefault(body.gitMode, projectDefs?.gitMode, null),
150
+ gitMode: resolveDefault(body.gitMode, projectDefs?.gitMode, systemDefaults.gitMode),
125
151
  templateId: body.templateId,
126
152
  parentSessionId: body.parentSessionId || null,
127
153
  files: [],
@@ -235,12 +261,17 @@ async function resolveSessionWorkingDirectory({ session, config, project }) {
235
261
  };
236
262
  }
237
263
 
264
+ // Normalize 'current' mode to null (no git isolation) for setupGitForSession
265
+ const normalizedGitMode = (config.gitMode === 'current') ? null : (config.gitMode || null);
266
+
238
267
  const gitSetup = await setupGitForSession({
239
268
  projectDir: project.workingDirectory,
240
- gitMode: config.gitMode || null,
269
+ gitMode: normalizedGitMode,
241
270
  gitBranch: config.gitBranch || null,
242
271
  sessionId: session.id,
243
272
  worktreeBasePath: project.worktreePath || null,
273
+ commitAttributionOverride:
274
+ resolveProviderMetadataFromModel(config.model)?.commitAttributionOverride ?? null,
244
275
  });
245
276
  return { workingDirectory: gitSetup.workingDirectory, gitWorktree: gitSetup.gitWorktree };
246
277
  }
@@ -212,6 +212,16 @@ async function validateAndPrepareSessionConfig(reqBody, reqFiles, projectId, pro
212
212
  return { error: 'Prompt is required', status: 400 };
213
213
  }
214
214
 
215
+ if (config.parentSessionId) {
216
+ const parentSession = sessions.getById(config.parentSessionId);
217
+ if (!parentSession) {
218
+ return { error: 'Parent session not found', status: 404 };
219
+ }
220
+ if (parentSession.projectId !== projectId) {
221
+ return { error: 'Parent session does not belong to this project', status: 400 };
222
+ }
223
+ }
224
+
215
225
  // Apply template overrides and resolve nextTemplateId
216
226
  applyTemplateOverrides(config);
217
227
  const { nextTemplateId, error: nextTemplateError } = resolveNextTemplateId(reqBody, config.nextTemplateId || null);
@@ -243,6 +253,7 @@ function createSessionRow(projectId, config, nextTemplateId, initialStatus) {
243
253
  parentSessionId: config.parentSessionId,
244
254
  status: initialStatus,
245
255
  model: config.model,
256
+ providerId: config.providerId,
246
257
  effortLevel: config.effortLevel,
247
258
  agentType: config.agentType,
248
259
  });
@@ -1,6 +1,7 @@
1
1
  import { Router } from 'express';
2
2
  import { modelProviders } from '../database.js';
3
3
  import {
4
+ COMMIT_ATTRIBUTION_VALIDATION_MESSAGE,
4
5
  CreateProviderRequest,
5
6
  UpdateProviderRequest,
6
7
  CreateProviderModelRequest,
@@ -34,6 +35,9 @@ router.get('/', (_req, res) => {
34
35
  const sanitized = allProviders.map(redactAuthToken);
35
36
  res.json(sanitized);
36
37
  } catch (error) {
38
+ if (error.message === COMMIT_ATTRIBUTION_VALIDATION_MESSAGE) {
39
+ return res.status(400).json({ error: error.message });
40
+ }
37
41
  res.status(500).json({ error: error.message });
38
42
  }
39
43
  });
@@ -82,9 +86,15 @@ router.patch('/:id', (req, res) => {
82
86
  const updated = modelProviders.update(req.params.id, result.data);
83
87
  res.json(redactAuthToken(updated));
84
88
  } catch (error) {
85
- if (error.message === 'Cannot delete built-in provider') {
89
+ if (
90
+ error.message === 'Cannot delete built-in provider' ||
91
+ error.message.startsWith('Built-in providers can only update')
92
+ ) {
86
93
  return res.status(403).json({ error: error.message });
87
94
  }
95
+ if (error.message === COMMIT_ATTRIBUTION_VALIDATION_MESSAGE) {
96
+ return res.status(400).json({ error: error.message });
97
+ }
88
98
  res.status(500).json({ error: error.message });
89
99
  }
90
100
  });
@@ -2,7 +2,7 @@ import { Router } from 'express';
2
2
  import { commandButtons, commandRuns } from '../database.js';
3
3
  import { broadcastToSession, broadcastToProject } from '../websocket.js';
4
4
  import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
5
- import { requireSession, requireSessionAndProject } from '../middleware/sessionLookup.js';
5
+ import { requireRootSessionAndProject } from '../middleware/sessionLookup.js';
6
6
  import { commandRunner } from '../services/commandRunner.js';
7
7
  import { databaseManager } from '../db/DatabaseManager.js';
8
8
 
@@ -46,9 +46,14 @@ function broadcastCommandError(ctx, errorMessage) {
46
46
  broadcastToProject(projectId, WS_MESSAGE_TYPES.COMMAND_RUN_ERROR, { projectId, sessionId, runId, buttonId, error: errorMessage });
47
47
  }
48
48
 
49
+ // GET /api/sessions/:id/command-buttons - List command buttons for the workflow project
50
+ router.get('/:id/command-buttons', requireRootSessionAndProject, (req, res) => {
51
+ res.json(commandButtons.getByProjectId(req.rootSession_.projectId));
52
+ });
53
+
49
54
  // POST /api/sessions/:id/command-buttons/:buttonId/run - Execute button command
50
- router.post('/:id/command-buttons/:buttonId/run', requireSessionAndProject, (req, res) => {
51
- const sessionId = req.params.id;
55
+ router.post('/:id/command-buttons/:buttonId/run', requireRootSessionAndProject, (req, res) => {
56
+ const sessionId = req.rootSessionId;
52
57
  const buttonId = req.params.buttonId;
53
58
 
54
59
  console.log(`[RUN] Starting command for buttonId: ${buttonId}, sessionId: ${sessionId}`);
@@ -57,6 +62,9 @@ router.post('/:id/command-buttons/:buttonId/run', requireSessionAndProject, (req
57
62
  if (!button) {
58
63
  return res.status(404).json({ error: 'Command button not found' });
59
64
  }
65
+ if (button.projectId !== req.rootSession_.projectId) {
66
+ return res.status(404).json({ error: 'Command button not found' });
67
+ }
60
68
 
61
69
  // Generate run ID
62
70
  const runId = databaseManager.generateId();
@@ -67,8 +75,8 @@ router.post('/:id/command-buttons/:buttonId/run', requireSessionAndProject, (req
67
75
  res.json({ runId, buttonId, status: 'running', output: '' });
68
76
 
69
77
  // Capture middleware values for use in async callbacks
70
- const projectId = req.session_.projectId;
71
- const workingDirectory = req.workingDirectory;
78
+ const projectId = req.rootSession_.projectId;
79
+ const workingDirectory = req.rootWorkingDirectory;
72
80
  const ctx = { sessionId, projectId, runId, buttonId };
73
81
 
74
82
  // Broadcast initial "running" status immediately so session list can show the running indicator
@@ -98,16 +106,17 @@ router.post('/:id/command-buttons/:buttonId/run', requireSessionAndProject, (req
98
106
  });
99
107
 
100
108
  // GET /api/sessions/:id/command-buttons/runs - Get active runs for session
101
- router.get('/:id/command-buttons/runs', requireSession, (req, res) => {
102
- const sessionId = req.params.id;
109
+ router.get('/:id/command-buttons/runs', requireRootSessionAndProject, (req, res) => {
110
+ const sessionId = req.rootSessionId;
103
111
 
104
112
  const activeRuns = commandRunner.getRunsBySession(sessionId);
105
113
  res.json(activeRuns);
106
114
  });
107
115
 
108
116
  // GET /api/sessions/:id/command-buttons/runs/:runId - Get single run by ID
109
- router.get('/:id/command-buttons/runs/:runId', requireSession, (req, res) => {
110
- const { id: sessionId, runId } = req.params;
117
+ router.get('/:id/command-buttons/runs/:runId', requireRootSessionAndProject, (req, res) => {
118
+ const { runId } = req.params;
119
+ const sessionId = req.rootSessionId;
111
120
 
112
121
  // Check if run is currently running (in memory)
113
122
  if (commandRunner.isRunning(runId)) {
@@ -136,8 +145,8 @@ router.get('/:id/command-buttons/runs/:runId', requireSession, (req, res) => {
136
145
  });
137
146
 
138
147
  // DELETE /api/sessions/:id/command-buttons/runs/:runId - Delete a command run record
139
- router.delete('/:id/command-buttons/runs/:runId', requireSessionAndProject, (req, res) => {
140
- const sessionId = req.params.id;
148
+ router.delete('/:id/command-buttons/runs/:runId', requireRootSessionAndProject, (req, res) => {
149
+ const sessionId = req.rootSessionId;
141
150
  const { runId } = req.params;
142
151
 
143
152
  const run = commandRuns.getById(runId);
@@ -151,7 +160,7 @@ router.delete('/:id/command-buttons/runs/:runId', requireSessionAndProject, (req
151
160
 
152
161
  commandRuns.deleteById(runId);
153
162
 
154
- const projectId = req.session_.projectId;
163
+ const projectId = req.rootSession_.projectId;
155
164
 
156
165
  // Broadcast deletion to session and project subscribers
157
166
  broadcastToSession(sessionId, WS_MESSAGE_TYPES.COMMAND_RUN_DELETED, {
@@ -170,18 +179,21 @@ router.delete('/:id/command-buttons/runs/:runId', requireSessionAndProject, (req
170
179
  });
171
180
 
172
181
  // DELETE /api/sessions/:id/command-buttons/:buttonId/runs/all - Delete all runs for a button in a session
173
- router.delete('/:id/command-buttons/:buttonId/runs/all', requireSessionAndProject, (req, res) => {
174
- const sessionId = req.params.id;
182
+ router.delete('/:id/command-buttons/:buttonId/runs/all', requireRootSessionAndProject, (req, res) => {
183
+ const sessionId = req.rootSessionId;
175
184
  const { buttonId } = req.params;
176
185
 
177
186
  const button = commandButtons.getById(buttonId);
178
187
  if (!button) {
179
188
  return res.status(404).json({ error: 'Command button not found' });
180
189
  }
190
+ if (button.projectId !== req.rootSession_.projectId) {
191
+ return res.status(404).json({ error: 'Command button not found' });
192
+ }
181
193
 
182
194
  const { deletedRuns } = commandRuns.deleteByButtonAndSession(buttonId, sessionId);
183
195
 
184
- const projectId = req.session_.projectId;
196
+ const projectId = req.rootSession_.projectId;
185
197
 
186
198
  // Broadcast individual COMMAND_RUN_DELETED events for each deleted run
187
199
  for (const run of deletedRuns) {
@@ -202,12 +214,18 @@ router.delete('/:id/command-buttons/:buttonId/runs/all', requireSessionAndProjec
202
214
  });
203
215
 
204
216
  // POST /api/sessions/:id/command-buttons/runs/:runId/kill - Kill running command
205
- router.post('/:id/command-buttons/runs/:runId/kill', requireSession, (req, res) => {
206
- const sessionId = req.params.id;
217
+ router.post('/:id/command-buttons/runs/:runId/kill', requireRootSessionAndProject, (req, res) => {
218
+ const sessionId = req.rootSessionId;
207
219
  const runId = req.params.runId;
208
220
 
209
221
  console.log(`[KILL] Kill request for runId: ${runId}, sessionId: ${sessionId}`);
210
222
 
223
+ const activeRuns = commandRunner.getRunsBySession(sessionId);
224
+ const activeRun = activeRuns.find((run) => run.runId === runId);
225
+ if (!activeRun) {
226
+ return res.status(404).json({ error: 'Run not found or already completed' });
227
+ }
228
+
211
229
  const killed = commandRunner.kill(runId);
212
230
  console.log(`[KILL] Kill result: ${killed} for runId: ${runId}`);
213
231
  if (!killed) {
@@ -57,6 +57,7 @@ router.post('/:id/start', requireSession, async (req, res) => {
57
57
  const updatedSession = await startDraft(req.session_, {
58
58
  prompt: req.body.prompt,
59
59
  model: req.body.model,
60
+ providerId: req.body.providerId,
60
61
  });
61
62
 
62
63
  res.json({ success: true, session: updatedSession });
@@ -6,19 +6,19 @@ import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
6
6
  import * as gitService from '../services/gitService.js';
7
7
  import * as summaryService from '../services/summaryService.js';
8
8
  import { executeHookAsync } from '../services/hookService.js';
9
- import { requireSession, requireSessionAndProject } from '../middleware/sessionLookup.js';
9
+ import { requireRootSessionAndProject, requireSession, requireSessionAndProject } from '../middleware/sessionLookup.js';
10
10
  import { duplicateSession } from '../services/sessionDuplicator.js';
11
11
  import { configureSchedule, ScheduleError } from '../services/scheduleService.js';
12
12
 
13
13
  const router = Router();
14
14
 
15
- // GET /api/sessions/:id/summary - Get session summary
16
- router.get('/:id/summary', requireSession, async (req, res) => {
15
+ // GET /api/sessions/:id/summary - Get workflow summary
16
+ router.get('/:id/summary', requireRootSessionAndProject, async (req, res) => {
17
17
  // Check if generateIfMissing query param is set
18
18
  const generateIfMissing = req.query.generate === 'true';
19
19
 
20
20
  try {
21
- const summary = await summaryService.getSummary(req.params.id, generateIfMissing);
21
+ const summary = await summaryService.getSummary(req.rootSessionId, generateIfMissing);
22
22
  if (!summary) {
23
23
  return res.status(404).json({ error: 'Summary not found' });
24
24
  }
@@ -28,10 +28,10 @@ router.get('/:id/summary', requireSession, async (req, res) => {
28
28
  }
29
29
  });
30
30
 
31
- // POST /api/sessions/:id/summary - Generate/regenerate session summary
32
- router.post('/:id/summary', requireSession, async (req, res) => {
31
+ // POST /api/sessions/:id/summary - Generate/regenerate workflow summary
32
+ router.post('/:id/summary', requireRootSessionAndProject, async (req, res) => {
33
33
  try {
34
- const summary = await summaryService.regenerateSummary(req.params.id);
34
+ const summary = await summaryService.regenerateSummary(req.rootSessionId);
35
35
  if (!summary) {
36
36
  return res.status(500).json({ error: 'Failed to generate summary' });
37
37
  }
@@ -41,10 +41,10 @@ router.post('/:id/summary', requireSession, async (req, res) => {
41
41
  }
42
42
  });
43
43
 
44
- // PUT /api/sessions/:id/summary - Directly set summary data (for testing/seeding)
45
- router.put('/:id/summary', requireSession, async (req, res) => {
44
+ // PUT /api/sessions/:id/summary - Directly set workflow summary data (for testing/seeding)
45
+ router.put('/:id/summary', requireRootSessionAndProject, async (req, res) => {
46
46
  try {
47
- const summary = sessionSummaries.upsert(req.params.id, req.body);
47
+ const summary = sessionSummaries.upsert(req.rootSessionId, req.body);
48
48
  res.json(summary);
49
49
  } catch (error) {
50
50
  res.status(500).json({ error: error.message });
@@ -158,6 +158,10 @@ function buildUpdateData(body) {
158
158
  updateData.manuallyNamed = true;
159
159
  }
160
160
 
161
+ if (body.prUrl !== undefined) {
162
+ updateData.prUrlAutoLinkDisabled = updateData.prUrl === null;
163
+ }
164
+
161
165
  return { updateData };
162
166
  }
163
167