circuschief 0.7.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) 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/commandButtons.js +16 -15
  5. package/packages/server/src/api/index.js +2 -0
  6. package/packages/server/src/api/kanban.js +4 -2
  7. package/packages/server/src/api/projects-commandButtons.js +6 -6
  8. package/packages/server/src/api/projects-helpers.js +20 -3
  9. package/packages/server/src/api/projects-session-create.js +109 -0
  10. package/packages/server/src/api/projects-session-defaults.js +51 -0
  11. package/packages/server/src/api/projects-session-helpers.js +57 -5
  12. package/packages/server/src/api/projects-templates.js +38 -0
  13. package/packages/server/src/api/projects.js +28 -170
  14. package/packages/server/src/api/providers.js +11 -1
  15. package/packages/server/src/api/sessions-commands.js +46 -25
  16. package/packages/server/src/api/sessions-lifecycle.js +10 -10
  17. package/packages/server/src/api/sessions-patch.js +45 -1
  18. package/packages/server/src/api/sessions.js +6 -5
  19. package/packages/server/src/database.js +0 -2
  20. package/packages/server/src/db/DatabaseManager.js +5 -1
  21. package/packages/server/src/db/ProjectDefaultsRepository.js +3 -3
  22. package/packages/server/src/db/ProviderRepository.js +87 -32
  23. package/packages/server/src/db/SessionRepository.js +2 -1
  24. package/packages/server/src/db/SessionTemplateRepository.js +23 -2
  25. package/packages/server/src/db/index.js +0 -3
  26. package/packages/server/src/db/migrations/index.js +60 -7
  27. package/packages/server/src/db/migrations/kanbanMigrations.js +1 -1
  28. package/packages/server/src/db/migrations/miscMigrations.js +59 -184
  29. package/packages/server/src/db/migrations/projectsMigrations.js +31 -0
  30. package/packages/server/src/db/migrations/providerCommitAttributionMigrations.js +30 -0
  31. package/packages/server/src/db/migrations/providerMigrations.js +165 -0
  32. package/packages/server/src/db/migrations/sessionTableRecreate.js +136 -0
  33. package/packages/server/src/db/migrations/sessionsMigrations.js +18 -5
  34. package/packages/server/src/db/seedBaselineData.js +137 -0
  35. package/packages/server/src/db/session-helpers.js +32 -4
  36. package/packages/server/src/middleware/sessionLookup.js +81 -8
  37. package/packages/server/src/schema.sql +153 -132
  38. package/packages/server/src/scripts/backupDatabase.js +21 -0
  39. package/packages/server/src/scripts/dbUtils.js +81 -0
  40. package/packages/server/src/scripts/inspectDatabaseSchema.js +81 -0
  41. package/packages/server/src/scripts/validateDatabaseBaseline.js +212 -0
  42. package/packages/server/src/services/codexSpawnHelper.js +9 -0
  43. package/packages/server/src/services/commandButtonPrompts.js +14 -12
  44. package/packages/server/src/services/commandRunner.js +7 -1
  45. package/packages/server/src/services/e2eSpawnCapture.js +147 -0
  46. package/packages/server/src/services/gitCommitAttribution.js +150 -0
  47. package/packages/server/src/services/gitDiff.js +132 -0
  48. package/packages/server/src/services/gitRepoUrl.js +174 -0
  49. package/packages/server/src/services/gitService.js +48 -311
  50. package/packages/server/src/services/gitSessionSetup.js +11 -1
  51. package/packages/server/src/services/gitWorktree.js +127 -0
  52. package/packages/server/src/services/kanbanTriggers.js +6 -3
  53. package/packages/server/src/services/nodeSpawnHelper.js +9 -0
  54. package/packages/server/src/services/prUrlService.js +3 -3
  55. package/packages/server/src/services/queryParamBuilder.js +90 -0
  56. package/packages/server/src/services/sessionDuplicator.js +1 -5
  57. package/packages/server/src/services/sessionExecution.js +56 -108
  58. package/packages/server/src/services/sessionPrompts.js +12 -47
  59. package/packages/server/src/services/sessionProvider.js +10 -0
  60. package/packages/server/src/services/summaryService.js +5 -3
  61. package/packages/server/src/services/summaryStaleCheck.js +23 -4
  62. package/packages/server/src/services/templateTriggerService.js +3 -1
  63. package/packages/shared/src/constants.js +3 -0
  64. package/packages/shared/src/contracts/commandButtons.js +16 -2
  65. package/packages/shared/src/contracts/projects.js +2 -2
  66. package/packages/shared/src/contracts/providers.js +60 -0
  67. package/packages/shared/src/contracts/sessions.js +29 -2
  68. package/packages/shared/src/contracts/templates.js +12 -2
  69. package/packages/shared/src/types.js +1 -9
  70. package/packages/shared/src/utils.js +2 -2
  71. package/packages/web/dist/assets/{ActiveSessionsView-UJsCILDL.js → ActiveSessionsView-Cxh8mHmB.js} +1 -1
  72. package/packages/web/dist/assets/{AgentLogsView-BGFPLjLa.js → AgentLogsView-xdfI2bR6.js} +2 -2
  73. package/packages/web/dist/assets/ApiClient-DfbJwzpz.js +1 -0
  74. package/packages/web/dist/assets/ArchiveConfirmModal-DXZYdzHR.js +1 -0
  75. package/packages/web/dist/assets/ArchiveConfirmModal-DeoCVGXt.css +1 -0
  76. package/packages/web/dist/assets/CommandButtonDetailView-D8xfqLAp.js +1 -0
  77. package/packages/web/dist/assets/CommandButtonDetailView-D9zjx9ME.css +1 -0
  78. package/packages/web/dist/assets/EffortLevelSelector-D2Hdzc_8.js +1 -0
  79. package/packages/web/dist/assets/{GeneralSettingsView-DsHChEhv.js → GeneralSettingsView-sPXkLlLy.js} +1 -1
  80. package/packages/web/dist/assets/InputWithButton-B-o0DgMH.js +1 -0
  81. package/packages/web/dist/assets/{InputWithButton-cYdrEmTs.css → InputWithButton-D9HMvfR5.css} +1 -1
  82. package/packages/web/dist/assets/{InterpolationHelp-CIkOSkWX.js → InterpolationHelp-Dxn1li4l.js} +1 -1
  83. package/packages/web/dist/assets/MarkdownEditor-D4Kbb-9l.js +2 -0
  84. package/packages/web/dist/assets/ModelSelector-72C7MUH4.js +1 -0
  85. package/packages/web/dist/assets/{ModelSelector-D8hbTRIt.css → ModelSelector-BNYKujL-.css} +1 -1
  86. package/packages/web/dist/assets/NewSessionView-BR_COfgW.js +3 -0
  87. package/packages/web/dist/assets/NewSessionView-DBl7T2Xp.css +1 -0
  88. package/packages/web/dist/assets/ProjectEditView-DbqTbA0q.css +1 -0
  89. package/packages/web/dist/assets/ProjectEditView-WImU7sNd.js +1 -0
  90. package/packages/web/dist/assets/{ProjectListView-B9FuWESY.js → ProjectListView-CYmmAcBD.js} +1 -1
  91. package/packages/web/dist/assets/{ProjectNewView-D62jYlBL.js → ProjectNewView-DEhqw3Jv.js} +1 -1
  92. package/packages/web/dist/assets/ProvidersView-XZh3jkmH.js +1 -0
  93. package/packages/web/dist/assets/ProvidersView-bZemq_Rv.css +1 -0
  94. package/packages/web/dist/assets/QuickResponsesPanel-BqmnTd-D.js +1 -0
  95. package/packages/web/dist/assets/QuickResponsesPanel-dk-Rj8xx.css +1 -0
  96. package/packages/web/dist/assets/ResizableTextarea-BQNw5e0C.css +1 -0
  97. package/packages/web/dist/assets/ResizableTextarea-DpWdIAP6.js +1 -0
  98. package/packages/web/dist/assets/SessionCard-Bw77-KwD.js +1 -0
  99. package/packages/web/dist/assets/SessionDetailView-B59TEkr-.js +36 -0
  100. package/packages/web/dist/assets/SessionDetailView-CKVBnR4T.css +1 -0
  101. package/packages/web/dist/assets/{SessionFormOptions-DYUISplS.js → SessionFormOptions-hqijxc0S.js} +1 -1
  102. package/packages/web/dist/assets/SessionListView-3-xx6EVs.css +1 -0
  103. package/packages/web/dist/assets/SessionListView-DYXHM9I-.js +1 -0
  104. package/packages/web/dist/assets/{SessionLogStream-DpUE6Xsh.js → SessionLogStream-5NfVr9pF.js} +6 -6
  105. package/packages/web/dist/assets/{SettingsView-BC055tIA.js → SettingsView-DI8ncOAV.js} +1 -1
  106. package/packages/web/dist/assets/{SlashCommandWizard-DmTyNG9O.js → SlashCommandWizard-BQ_rMzn-.js} +1 -1
  107. package/packages/web/dist/assets/{SlashCommandWizard-Dn7sNaBd.css → SlashCommandWizard-DJzw3LP5.css} +1 -1
  108. package/packages/web/dist/assets/{SummarySettingsView-BgnRCwlq.js → SummarySettingsView-C2Qs35mm.js} +1 -1
  109. package/packages/web/dist/assets/TemplateDetailView-B5NI2oTR.css +1 -0
  110. package/packages/web/dist/assets/TemplateDetailView-zVkIvgtu.js +1 -0
  111. package/packages/web/dist/assets/{commandButtons-D4RPpLiu.js → commandButtons-CoU3G4zK.js} +1 -1
  112. package/packages/web/dist/assets/index-9yF1uCCA.js +1 -0
  113. package/packages/web/dist/assets/index-BKstCaYU.js +1 -0
  114. package/packages/web/dist/assets/index-BhbH7eOk.js +1 -0
  115. package/packages/web/dist/assets/{index-BGwH4Cfn.js → index-BjuRttEY.js} +3 -3
  116. package/packages/web/dist/assets/index-Bo7PdwM5.js +1 -0
  117. package/packages/web/dist/assets/index-C2QFVD7d.js +83 -0
  118. package/packages/web/dist/assets/index-C7Ww2auW.js +1 -0
  119. package/packages/web/dist/assets/index-CAGdsDh7.js +1 -0
  120. package/packages/web/dist/assets/index-CLRsVASf.js +3 -0
  121. package/packages/web/dist/assets/{index-Bn5xdGFM.js → index-CP-SxOlV.js} +1 -1
  122. package/packages/web/dist/assets/index-CslU0psO.js +1 -0
  123. package/packages/web/dist/assets/index-DI4NxaWD.js +1 -0
  124. package/packages/web/dist/assets/index-DOzONENy.js +1 -0
  125. package/packages/web/dist/assets/index-DUa7adFh.js +1 -0
  126. package/packages/web/dist/assets/index-DZBpETI5.js +1 -0
  127. package/packages/web/dist/assets/index-DsjWqc6R.js +7 -0
  128. package/packages/web/dist/assets/index-c99Bo3JV.js +1 -0
  129. package/packages/web/dist/assets/index-mT1JpxDc.js +1 -0
  130. package/packages/web/dist/assets/index-rkQx2tso.js +1 -0
  131. package/packages/web/dist/assets/{index-Cs2nxhrT.css → index-uySCcnA_.css} +1 -1
  132. package/packages/web/dist/assets/projectDefaults-B8esIcYq.js +1 -0
  133. package/packages/web/dist/assets/{projects-BUiOGmmb.js → projects-C-8PSxKi.js} +1 -1
  134. package/packages/web/dist/assets/{providers-Bh1ZiiJi.js → providers-oXifvvqN.js} +1 -1
  135. package/packages/web/dist/assets/sessions-Nq5VafSf.js +1 -0
  136. package/packages/web/dist/assets/{settings-Z4AVVmkJ.js → settings-DtpuiyT6.js} +1 -1
  137. package/packages/web/dist/index.html +2 -2
  138. package/packages/server/src/api/sessions-notes.js +0 -51
  139. package/packages/server/src/db/SessionNoteRepository.js +0 -60
  140. package/packages/web/dist/assets/ApiClient-B4YTtyY4.js +0 -1
  141. package/packages/web/dist/assets/ArchiveConfirmModal-BQ-4gI0R.css +0 -1
  142. package/packages/web/dist/assets/ArchiveConfirmModal-OFaj_uX5.js +0 -1
  143. package/packages/web/dist/assets/CommandButtonDetailView-D8S258uP.js +0 -1
  144. package/packages/web/dist/assets/CommandButtonDetailView-DBm3rzhw.css +0 -1
  145. package/packages/web/dist/assets/EffortLevelSelector-C2378L8e.js +0 -1
  146. package/packages/web/dist/assets/InputWithButton-Ci15ox0a.js +0 -1
  147. package/packages/web/dist/assets/MarkdownEditor-5-bexzUT.js +0 -2
  148. package/packages/web/dist/assets/ModelSelector-BMpR0DPr.js +0 -1
  149. package/packages/web/dist/assets/NewSessionView-BCqtIgWH.js +0 -3
  150. package/packages/web/dist/assets/NewSessionView-CUUdHkfv.css +0 -1
  151. package/packages/web/dist/assets/ProjectEditView-D9sK0fdH.css +0 -1
  152. package/packages/web/dist/assets/ProjectEditView-RFaxHhAX.js +0 -1
  153. package/packages/web/dist/assets/ProvidersView-DDKMIQWZ.js +0 -1
  154. package/packages/web/dist/assets/ProvidersView-DE82G_5W.css +0 -1
  155. package/packages/web/dist/assets/QuickResponseSettings-B8188A1D.css +0 -1
  156. package/packages/web/dist/assets/QuickResponseSettings-CDm5vwP7.js +0 -1
  157. package/packages/web/dist/assets/QuickResponsesPanel-BlFDvnZ2.css +0 -1
  158. package/packages/web/dist/assets/QuickResponsesPanel-DZ_Lre_l.js +0 -1
  159. package/packages/web/dist/assets/ResizableTextarea-DiIOEGjN.js +0 -1
  160. package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +0 -1
  161. package/packages/web/dist/assets/SessionCard-DmjnVYWn.js +0 -1
  162. package/packages/web/dist/assets/SessionDetailView-CL7nmfiB.js +0 -36
  163. package/packages/web/dist/assets/SessionDetailView-CupIkI7u.css +0 -1
  164. package/packages/web/dist/assets/SessionListView-BcxGz4aC.js +0 -1
  165. package/packages/web/dist/assets/SessionListView-fHlQyecX.css +0 -1
  166. package/packages/web/dist/assets/TemplateDetailView-BlhOmLUX.js +0 -1
  167. package/packages/web/dist/assets/TemplateDetailView-DT2m06W7.css +0 -1
  168. package/packages/web/dist/assets/index-4rhEeO0B.js +0 -1
  169. package/packages/web/dist/assets/index-9vb2KaAd.js +0 -1
  170. package/packages/web/dist/assets/index-B0CvZXuN.js +0 -7
  171. package/packages/web/dist/assets/index-B6G18FqB.js +0 -82
  172. package/packages/web/dist/assets/index-BUhvkAdF.js +0 -1
  173. package/packages/web/dist/assets/index-BcnkUk2o.js +0 -1
  174. package/packages/web/dist/assets/index-CNwkdB0T.js +0 -1
  175. package/packages/web/dist/assets/index-CfL84oGW.js +0 -1
  176. package/packages/web/dist/assets/index-CkmxO8Mm.js +0 -1
  177. package/packages/web/dist/assets/index-Cpy4-yv3.js +0 -1
  178. package/packages/web/dist/assets/index-CrAQJmoZ.js +0 -1
  179. package/packages/web/dist/assets/index-D6Ky9vJe.js +0 -3
  180. package/packages/web/dist/assets/index-DfrE0gAC.js +0 -1
  181. package/packages/web/dist/assets/index-KwEyz0F3.js +0 -1
  182. package/packages/web/dist/assets/index-OfCywayk.js +0 -1
  183. package/packages/web/dist/assets/index-PDesaJc6.js +0 -1
  184. package/packages/web/dist/assets/index-uB6nhSvz.js +0 -1
  185. package/packages/web/dist/assets/sessions-DH1R-NhV.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circuschief",
3
- "version": "0.7.0",
3
+ "version": "1.0.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);
@@ -8,6 +8,7 @@ import { databaseManager } from '../db/DatabaseManager.js';
8
8
 
9
9
  // Error message constants
10
10
  const ERR_SESSION_NOT_FOUND = 'Session not found';
11
+ const ERR_BUTTON_NOT_FOUND = 'Circus Command not found';
11
12
 
12
13
  const router = Router({ mergeParams: true });
13
14
 
@@ -64,14 +65,14 @@ function broadcastCommandRunError({ sessionId, projectId, runId, buttonId, error
64
65
  });
65
66
  }
66
67
 
67
- // GET /api/projects/:projectId/command-buttons - List all command buttons for project
68
+ // GET /api/projects/:projectId/circus-commands - List all command buttons for project
68
69
  router.get('/', (req, res) => {
69
70
  const { projectId } = req.params;
70
71
  const buttons = commandButtons.getByProjectId(projectId);
71
72
  res.json(buttons);
72
73
  });
73
74
 
74
- // GET /api/projects/:projectId/command-buttons/latest-runs - Get latest run for each button per session in project
75
+ // GET /api/projects/:projectId/circus-commands/latest-runs - Get latest run for each button per session in project
75
76
  router.get('/latest-runs', (req, res) => {
76
77
  const { projectId } = req.params;
77
78
 
@@ -85,7 +86,7 @@ router.get('/latest-runs', (req, res) => {
85
86
  res.json(latestRuns);
86
87
  });
87
88
 
88
- // POST /api/projects/:projectId/command-buttons - Create new command button
89
+ // POST /api/projects/:projectId/circus-commands - Create new command button
89
90
  router.post('/', (req, res) => {
90
91
  const result = CreateCommandButtonRequest.safeParse(req.body);
91
92
  if (!result.success) {
@@ -103,20 +104,20 @@ router.post('/', (req, res) => {
103
104
  res.status(201).json(button);
104
105
  });
105
106
 
106
- // GET /api/projects/:projectId/command-buttons/:id - Get single button
107
+ // GET /api/projects/:projectId/circus-commands/:id - Get single button
107
108
  router.get('/:id', (req, res) => {
108
109
  const button = commandButtons.getById(req.params.id);
109
110
  if (!button) {
110
- return res.status(404).json({ error: 'Command button not found' });
111
+ return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
111
112
  }
112
113
  res.json(button);
113
114
  });
114
115
 
115
- // PATCH /api/projects/:projectId/command-buttons/:id - Update button
116
+ // PATCH /api/projects/:projectId/circus-commands/:id - Update button
116
117
  router.patch('/:id', (req, res) => {
117
118
  const button = commandButtons.getById(req.params.id);
118
119
  if (!button) {
119
- return res.status(404).json({ error: 'Command button not found' });
120
+ return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
120
121
  }
121
122
 
122
123
  const result = UpdateCommandButtonRequest.safeParse(req.body);
@@ -135,18 +136,18 @@ router.patch('/:id', (req, res) => {
135
136
  res.json(updated);
136
137
  });
137
138
 
138
- // DELETE /api/projects/:projectId/command-buttons/:id - Delete button
139
+ // DELETE /api/projects/:projectId/circus-commands/:id - Delete button
139
140
  router.delete('/:id', (req, res) => {
140
141
  const button = commandButtons.getById(req.params.id);
141
142
  if (!button) {
142
- return res.status(404).json({ error: 'Command button not found' });
143
+ return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
143
144
  }
144
145
 
145
146
  commandButtons.delete(req.params.id);
146
147
  res.status(204).send();
147
148
  });
148
149
 
149
- // POST /api/sessions/:sessionId/command-buttons/:buttonId/run - Execute button command
150
+ // POST /api/sessions/:sessionId/circus-commands/:buttonId/run - Execute button command
150
151
  router.post('/run/:buttonId', (req, res) => {
151
152
  const { sessionId, buttonId } = req.params;
152
153
 
@@ -157,7 +158,7 @@ router.post('/run/:buttonId', (req, res) => {
157
158
 
158
159
  const button = commandButtons.getById(buttonId);
159
160
  if (!button) {
160
- return res.status(404).json({ error: 'Command button not found' });
161
+ return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
161
162
  }
162
163
 
163
164
  const workingDirectory = session.gitWorktree || session.project?.workingDirectory || process.cwd();
@@ -181,7 +182,7 @@ router.post('/run/:buttonId', (req, res) => {
181
182
  })();
182
183
  });
183
184
 
184
- // GET /api/sessions/:sessionId/command-buttons/runs - Get active runs for session
185
+ // GET /api/sessions/:sessionId/circus-commands/runs - Get active runs for session
185
186
  router.get('/runs', (req, res) => {
186
187
  const { sessionId } = req.params;
187
188
 
@@ -221,7 +222,7 @@ router.get('/runs', (req, res) => {
221
222
  res.json(Array.from(runMap.values()));
222
223
  });
223
224
 
224
- // GET /api/sessions/:sessionId/command-buttons/runs/:runId - Get single run by ID
225
+ // GET /api/sessions/:sessionId/circus-commands/runs/:runId - Get single run by ID
225
226
  router.get('/runs/:runId', (req, res) => {
226
227
  const { sessionId, runId } = req.params;
227
228
 
@@ -256,7 +257,7 @@ router.get('/runs/:runId', (req, res) => {
256
257
  });
257
258
  });
258
259
 
259
- // DELETE /api/sessions/:sessionId/command-buttons/runs/:runId - Delete a command run record
260
+ // DELETE /api/sessions/:sessionId/circus-commands/runs/:runId - Delete a command run record
260
261
  router.delete('/runs/:runId', (req, res) => {
261
262
  const { sessionId, runId } = req.params;
262
263
 
@@ -292,7 +293,7 @@ router.delete('/runs/:runId', (req, res) => {
292
293
  res.status(204).send();
293
294
  });
294
295
 
295
- // POST /api/sessions/:sessionId/command-buttons/runs/:runId/kill - Kill running command
296
+ // POST /api/sessions/:sessionId/circus-commands/runs/:runId/kill - Kill running command
296
297
  router.post('/runs/:runId/kill', (req, res) => {
297
298
  const { sessionId, runId } = req.params;
298
299
 
@@ -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);
@@ -4,11 +4,11 @@ import { CreateCommandButtonRequest, UpdateCommandButtonRequest } from '../../..
4
4
 
5
5
  // Error message constants
6
6
  const ERR_PROJECT_NOT_FOUND = 'Project not found';
7
- const ERR_BUTTON_NOT_FOUND = 'Command button not found';
7
+ const ERR_BUTTON_NOT_FOUND = 'Circus Command not found';
8
8
 
9
9
  const router = Router({ mergeParams: true });
10
10
 
11
- // GET /api/projects/:id/command-buttons - List all command buttons for project
11
+ // GET /api/projects/:id/circus-commands - List all command buttons for project
12
12
  router.get('/', (req, res) => {
13
13
  const project = projects.getById(req.params.id);
14
14
  if (!project) {
@@ -19,7 +19,7 @@ router.get('/', (req, res) => {
19
19
  res.json(buttons);
20
20
  });
21
21
 
22
- // POST /api/projects/:id/command-buttons - Create new command button
22
+ // POST /api/projects/:id/circus-commands - Create new command button
23
23
  router.post('/', (req, res) => {
24
24
  const project = projects.getById(req.params.id);
25
25
  if (!project) {
@@ -42,7 +42,7 @@ router.post('/', (req, res) => {
42
42
  res.status(201).json(button);
43
43
  });
44
44
 
45
- // GET /api/projects/:id/command-buttons/:buttonId - Get single button
45
+ // GET /api/projects/:id/circus-commands/:buttonId - Get single button
46
46
  router.get('/:buttonId', (req, res) => {
47
47
  const project = projects.getById(req.params.id);
48
48
  if (!project) {
@@ -56,7 +56,7 @@ router.get('/:buttonId', (req, res) => {
56
56
  res.json(button);
57
57
  });
58
58
 
59
- // PATCH /api/projects/:id/command-buttons/:buttonId - Update button
59
+ // PATCH /api/projects/:id/circus-commands/:buttonId - Update button
60
60
  router.patch('/:buttonId', (req, res) => {
61
61
  const project = projects.getById(req.params.id);
62
62
  if (!project) {
@@ -77,7 +77,7 @@ router.patch('/:buttonId', (req, res) => {
77
77
  res.json(updated);
78
78
  });
79
79
 
80
- // DELETE /api/projects/:id/command-buttons/:buttonId - Delete button
80
+ // DELETE /api/projects/:id/circus-commands/:buttonId - Delete button
81
81
  router.delete('/:buttonId', (req, res) => {
82
82
  const project = projects.getById(req.params.id);
83
83
  if (!project) {
@@ -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
  };
@@ -0,0 +1,109 @@
1
+ import { sessions, projectDefaults } from '../database.js';
2
+ import { ProjectDefaultsRepository } from '../db/ProjectDefaultsRepository.js';
3
+ import { broadcastToProject } from '../websocket.js';
4
+ import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
5
+ import {
6
+ generateInitialName,
7
+ prepareSessionConfig,
8
+ applyTemplateOverrides,
9
+ resolveNextTemplateId,
10
+ buildSchedulingUpdate,
11
+ setupAndStartSession,
12
+ } from './projects-session-helpers.js';
13
+ import { validateGitSettings } from './projects-helpers.js';
14
+
15
+ /**
16
+ * Validate and prepare the session configuration from the request body.
17
+ * Returns { config, nextTemplateId } on success, or { error, status } on failure.
18
+ */
19
+ export async function validateAndPrepareSessionConfig(reqBody, reqFiles, projectId, project) {
20
+ const projectDefs = projectDefaults.getByProjectId(projectId);
21
+ const systemDefaults = ProjectDefaultsRepository.getSystemDefaults();
22
+ const config = prepareSessionConfig(reqBody, projectDefs, systemDefaults);
23
+ config.files = reqFiles || [];
24
+
25
+ if (!config.prompt) {
26
+ return { error: 'Prompt is required', status: 400 };
27
+ }
28
+
29
+ if (config.schedulingError) {
30
+ return { error: config.schedulingError, status: 400 };
31
+ }
32
+
33
+ if (config.parentSessionId) {
34
+ const parentSession = sessions.getById(config.parentSessionId);
35
+ if (!parentSession) {
36
+ return { error: 'Parent session not found', status: 404 };
37
+ }
38
+ if (parentSession.projectId !== projectId) {
39
+ return { error: 'Parent session does not belong to this project', status: 400 };
40
+ }
41
+ }
42
+
43
+ // Apply template overrides and resolve nextTemplateId
44
+ applyTemplateOverrides(config);
45
+ const { nextTemplateId, error: nextTemplateError } = resolveNextTemplateId(reqBody, config.nextTemplateId || null);
46
+ if (nextTemplateError) {
47
+ return { error: nextTemplateError, status: 400 };
48
+ }
49
+ config.nextTemplateId = nextTemplateId;
50
+
51
+ // Validate git settings for git repos
52
+ const { config: updatedConfig, error: gitError } = await validateGitSettings(config, project);
53
+ if (gitError) {
54
+ return { error: gitError, status: 400 };
55
+ }
56
+ Object.assign(config, updatedConfig);
57
+
58
+ return { config, nextTemplateId };
59
+ }
60
+
61
+ /**
62
+ * Create the session row and apply any post-create updates.
63
+ * Returns the created session (already persisted in DB).
64
+ */
65
+ export function createSessionRow(projectId, config, nextTemplateId, initialStatus) {
66
+ const sessionName = config.name || generateInitialName(config.prompt);
67
+ const session = sessions.create(projectId, sessionName, config.prompt, {
68
+ mode: config.mode,
69
+ thinkingEnabled: config.thinkingEnabled,
70
+ gitBranch: config.gitBranch,
71
+ parentSessionId: config.parentSessionId,
72
+ status: initialStatus,
73
+ model: config.model,
74
+ providerId: config.providerId,
75
+ effortLevel: config.effortLevel,
76
+ agentType: config.agentType,
77
+ });
78
+
79
+ const postCreateUpdate = {
80
+ ...(nextTemplateId ? { nextTemplateId } : {}),
81
+ ...buildSchedulingUpdate(config, initialStatus),
82
+ };
83
+ if (Object.keys(postCreateUpdate).length > 0) {
84
+ sessions.update(session.id, postCreateUpdate);
85
+ }
86
+ return session;
87
+ }
88
+
89
+ /**
90
+ * Run setupAndStartSession and translate any failure into an error response,
91
+ * marking the session as errored and broadcasting the update.
92
+ */
93
+ export async function startSessionOrFail(req, res, { session, config, project }) {
94
+ try {
95
+ const { updatedSession } = await setupAndStartSession({
96
+ session, config, project, projectId: req.params.id, files: config.files,
97
+ });
98
+ return res.status(201).json(updatedSession);
99
+ } catch (error) {
100
+ console.error('Git setup error:', error);
101
+ const updatedSession = sessions.update(session.id, { status: 'error', error: error.message });
102
+ broadcastToProject(req.params.id, WS_MESSAGE_TYPES.SESSION_UPDATED, {
103
+ projectId: req.params.id,
104
+ sessionId: session.id,
105
+ session: updatedSession,
106
+ });
107
+ return res.status(500).json({ error: `Git setup failed: ${error.message}` });
108
+ }
109
+ }
@@ -0,0 +1,51 @@
1
+ import { Router } from 'express';
2
+ import { projects, projectDefaults } from '../database.js';
3
+ import { ProjectSessionDefaultsRequest } from '../../../shared/src/contracts/projects.js';
4
+
5
+ const ERR_PROJECT_NOT_FOUND = 'Project not found';
6
+
7
+ const router = Router({ mergeParams: true });
8
+
9
+ // GET /api/projects/:id/session-defaults - Get session defaults for project
10
+ router.get('/', (req, res) => {
11
+ const project = projects.getById(req.params.id);
12
+ if (!project) {
13
+ return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
14
+ }
15
+
16
+ const defaults = projectDefaults.getByProjectId(req.params.id);
17
+ if (!defaults) {
18
+ return res.json(null);
19
+ }
20
+
21
+ res.json(defaults);
22
+ });
23
+
24
+ // POST /api/projects/:id/session-defaults - Update/create session defaults for project
25
+ router.post('/', (req, res) => {
26
+ const project = projects.getById(req.params.id);
27
+ if (!project) {
28
+ return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
29
+ }
30
+
31
+ const result = ProjectSessionDefaultsRequest.safeParse(req.body);
32
+ if (!result.success) {
33
+ return res.status(400).json({ error: result.error.issues[0].message });
34
+ }
35
+
36
+ const updated = projectDefaults.upsert(req.params.id, result.data);
37
+ res.status(200).json(updated);
38
+ });
39
+
40
+ // DELETE /api/projects/:id/session-defaults - Reset session defaults for project
41
+ router.delete('/', (req, res) => {
42
+ const project = projects.getById(req.params.id);
43
+ if (!project) {
44
+ return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
45
+ }
46
+
47
+ projectDefaults.resetToDefaults(req.params.id);
48
+ res.json({ message: 'Session defaults reset to system defaults' });
49
+ });
50
+
51
+ export default router;