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
@@ -1,9 +1,13 @@
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';
8
+
9
+ const SCHEDULED_AT_FORMAT_MESSAGE = 'scheduledAt must be a valid ISO 8601 date-time string with a timezone, for example "2026-06-12T14:00:00Z".';
10
+ const ISO_8601_DATE_TIME_WITH_TIMEZONE = /^(\d{4})-(\d{2})-(\d{2})T([01]\d|2[0-3]):([0-5]\d):([0-5]\d)(?:\.\d+)?(Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
7
11
 
8
12
  /**
9
13
  * Generate an initial session name from the prompt
@@ -82,16 +86,59 @@ export function resolveThinkingEnabled(body, projectDefs, systemDefaults) {
82
86
  return systemDefaults.thinkingEnabled;
83
87
  }
84
88
 
89
+ function hasValidDateParts(year, month, day) {
90
+ const parsed = new Date(Date.UTC(year, month - 1, day));
91
+ return parsed.getUTCFullYear() === year
92
+ && parsed.getUTCMonth() === month - 1
93
+ && parsed.getUTCDate() === day;
94
+ }
95
+
96
+ /**
97
+ * Parse scheduledAt from the public API contract.
98
+ * @param {*} value - Raw scheduledAt value
99
+ * @returns {{ value: number|undefined, error: string|null }}
100
+ */
101
+ export function parseScheduledAt(value) {
102
+ if (value === undefined || value === null || value === '') {
103
+ return { value: undefined, error: null };
104
+ }
105
+
106
+ if (typeof value !== 'string') {
107
+ return { value: undefined, error: SCHEDULED_AT_FORMAT_MESSAGE };
108
+ }
109
+
110
+ const match = ISO_8601_DATE_TIME_WITH_TIMEZONE.exec(value);
111
+ if (!match) {
112
+ return { value: undefined, error: SCHEDULED_AT_FORMAT_MESSAGE };
113
+ }
114
+
115
+ const year = Number(match[1]);
116
+ const month = Number(match[2]);
117
+ const day = Number(match[3]);
118
+ if (!hasValidDateParts(year, month, day)) {
119
+ return { value: undefined, error: SCHEDULED_AT_FORMAT_MESSAGE };
120
+ }
121
+
122
+ const timestamp = Date.parse(value);
123
+ if (!Number.isFinite(timestamp)) {
124
+ return { value: undefined, error: SCHEDULED_AT_FORMAT_MESSAGE };
125
+ }
126
+
127
+ return { value: timestamp, error: null };
128
+ }
129
+
85
130
  /**
86
131
  * Parse scheduling fields from request body.
87
132
  * @param {object} body - Request body
88
133
  * @returns {object} Scheduling configuration
89
134
  */
90
135
  export function parseSchedulingConfig(body) {
136
+ const scheduledAt = parseScheduledAt(body.scheduledAt);
91
137
  return {
92
- scheduledAt: body.scheduledAt ? parseInt(body.scheduledAt, 10) : undefined,
138
+ scheduledAt: scheduledAt.value,
139
+ schedulingError: scheduledAt.error,
93
140
  autoRescheduleEnabled: body.autoRescheduleEnabled === true || body.autoRescheduleEnabled === 'true',
94
- rescheduleDelayMinutes: body.rescheduleDelayMinutes ? parseInt(body.rescheduleDelayMinutes, 10) : 15,
141
+ rescheduleDelayMinutes: body.rescheduleDelayMinutes ? parseInt(body.rescheduleDelayMinutes, 10) : DEFAULT_RESCHEDULE_DELAY_MINUTES,
95
142
  rescheduleOnTokenLimit: body.rescheduleOnTokenLimit !== false && body.rescheduleOnTokenLimit !== 'false',
96
143
  rescheduleOnServiceError: body.rescheduleOnServiceError !== false && body.rescheduleOnServiceError !== 'false',
97
144
  maxRescheduleCount: body.maxRescheduleCount ? parseInt(body.maxRescheduleCount, 10) : null,
@@ -146,7 +193,7 @@ export function prepareSessionConfig(body, projectDefs, systemDefaults) {
146
193
  providerId: resolveProviderDefault(explicitProviderId, projectProviderId, systemProviderId),
147
194
  effortLevel,
148
195
  gitBranch: resolveDefault(body.gitBranch, projectDefs?.gitBranch, null),
149
- gitMode: resolveDefault(body.gitMode, projectDefs?.gitMode, null),
196
+ gitMode: resolveDefault(body.gitMode, projectDefs?.gitMode, systemDefaults.gitMode),
150
197
  templateId: body.templateId,
151
198
  parentSessionId: body.parentSessionId || null,
152
199
  files: [],
@@ -260,12 +307,17 @@ async function resolveSessionWorkingDirectory({ session, config, project }) {
260
307
  };
261
308
  }
262
309
 
310
+ // Normalize 'current' mode to null (no git isolation) for setupGitForSession
311
+ const normalizedGitMode = (config.gitMode === 'current') ? null : (config.gitMode || null);
312
+
263
313
  const gitSetup = await setupGitForSession({
264
314
  projectDir: project.workingDirectory,
265
- gitMode: config.gitMode || null,
315
+ gitMode: normalizedGitMode,
266
316
  gitBranch: config.gitBranch || null,
267
317
  sessionId: session.id,
268
318
  worktreeBasePath: project.worktreePath || null,
319
+ commitAttributionOverride:
320
+ resolveProviderMetadataFromModel(config.model)?.commitAttributionOverride ?? null,
269
321
  });
270
322
  return { workingDirectory: gitSetup.workingDirectory, gitWorktree: gitSetup.gitWorktree };
271
323
  }
@@ -0,0 +1,38 @@
1
+ import { Router } from 'express';
2
+ import { projects, sessionTemplates } from '../database.js';
3
+ import { CreateSessionTemplateRequest } from '../../../shared/src/contracts/templates.js';
4
+
5
+ const ERR_PROJECT_NOT_FOUND = 'Project not found';
6
+ const router = Router({ mergeParams: true });
7
+
8
+ // GET - List available templates for project (project + global)
9
+ router.get('/', (req, res) => {
10
+ const project = projects.getById(req.params.id);
11
+ if (!project) {
12
+ return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
13
+ }
14
+
15
+ const available = sessionTemplates.getAvailableForProject(req.params.id);
16
+ res.json(available);
17
+ });
18
+
19
+ // POST - Create project template
20
+ router.post('/', (req, res) => {
21
+ const project = projects.getById(req.params.id);
22
+ if (!project) {
23
+ return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
24
+ }
25
+
26
+ const result = CreateSessionTemplateRequest.safeParse(req.body);
27
+ if (!result.success) {
28
+ return res.status(400).json({ error: result.error.issues[0].message });
29
+ }
30
+
31
+ const template = sessionTemplates.create({
32
+ projectId: req.params.id,
33
+ ...result.data,
34
+ });
35
+ res.status(201).json(template);
36
+ });
37
+
38
+ export default router;
@@ -1,26 +1,18 @@
1
1
  import { Router } from 'express';
2
- import { projects, sessions, sessionTemplates, projectDefaults, commandRuns } from '../database.js';
2
+ import { projects, sessions, commandRuns } from '../database.js';
3
3
  import { commandRunner } from '../services/commandRunner.js';
4
- import { CreateProjectRequest, UpdateProjectRequest, ProjectSessionDefaultsRequest } from '../../../shared/src/contracts/projects.js';
5
- import { ProjectDefaultsRepository } from '../db/ProjectDefaultsRepository.js';
6
- import { CreateSessionTemplateRequest } from '../../../shared/src/contracts/templates.js';
7
- import { broadcastToProject } from '../websocket.js';
4
+ import { CreateProjectRequest, UpdateProjectRequest } from '../../../shared/src/contracts/projects.js';
8
5
  import projectCommandButtonsRouter from './projects-commandButtons.js';
9
- import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
6
+ import projectSessionDefaultsRouter from './projects-session-defaults.js';
7
+ import projectTemplatesRouter from './projects-templates.js';
10
8
  import { handleUploadError, uploadMiddleware } from '../middleware/upload.js';
11
- import {
12
- generateInitialName,
13
- prepareSessionConfig,
14
- applyTemplateOverrides,
15
- resolveNextTemplateId,
16
- determineInitialStatus,
17
- buildSchedulingUpdate,
18
- setupAndStartSession,
19
- } from './projects-session-helpers.js';
20
- import { validateGitSettings, buildRunsBySession } from './projects-helpers.js';
9
+ import { determineInitialStatus } from './projects-session-helpers.js';
10
+ import { buildRunsBySession } from './projects-helpers.js';
21
11
  import { resolveAgentTypeFromModel } from '../services/sessionProvider.js';
22
12
  import { access, constants } from 'fs/promises';
23
13
  import { dirname, isAbsolute } from 'path';
14
+ import { getRepositoryUrl } from '../services/gitService.js';
15
+ import { validateAndPrepareSessionConfig, createSessionRow, startSessionOrFail } from './projects-session-create.js';
24
16
 
25
17
  // Error message constants
26
18
  const ERR_PROJECT_NOT_FOUND = 'Project not found';
@@ -65,17 +57,31 @@ router.post('/', async (req, res) => {
65
57
  return res.status(400).json({ error: result.error.issues[0].message });
66
58
  }
67
59
 
68
- const { name, workingDirectory, systemPrompt, onSessionCreated, onSessionDeleted, worktreePath, kanbanEnabled } = result.data;
60
+ const { name, workingDirectory, systemPrompt, onSessionCreated, onSessionDeleted, worktreePath, kanbanEnabled, repoUrl } = result.data;
69
61
 
70
62
  const pathError = await validateWorktreePath(worktreePath);
71
63
  if (pathError) {
72
64
  return res.status(400).json({ error: pathError });
73
65
  }
74
66
 
67
+ // Resolve repoUrl:
68
+ // - string: use as-is (explicitly provided)
69
+ // - null: suppress detection (explicitly sent as null)
70
+ // - undefined: auto-detect from git remote
71
+ let resolvedRepoUrl = repoUrl;
72
+ if (resolvedRepoUrl === undefined) {
73
+ try {
74
+ resolvedRepoUrl = await getRepositoryUrl(workingDirectory);
75
+ } catch {
76
+ resolvedRepoUrl = null;
77
+ }
78
+ }
79
+
75
80
  const createOptions = {
76
81
  onSessionCreated: onSessionCreated || null,
77
82
  onSessionDeleted: onSessionDeleted || null,
78
83
  worktreePath: worktreePath || null,
84
+ repoUrl: resolvedRepoUrl,
79
85
  };
80
86
  if (kanbanEnabled !== undefined) {
81
87
  createOptions.kanbanEnabled = kanbanEnabled;
@@ -198,88 +204,6 @@ router.get('/:id/sessions', (req, res) => {
198
204
  }
199
205
  });
200
206
 
201
- /**
202
- * Validate and prepare the session configuration from the request body.
203
- * Returns { config, nextTemplateId } on success, or { error, status } on failure.
204
- */
205
- async function validateAndPrepareSessionConfig(reqBody, reqFiles, projectId, project) {
206
- const projectDefs = projectDefaults.getByProjectId(projectId);
207
- const systemDefaults = ProjectDefaultsRepository.getSystemDefaults();
208
- const config = prepareSessionConfig(reqBody, projectDefs, systemDefaults);
209
- config.files = reqFiles || [];
210
-
211
- if (!config.prompt) {
212
- return { error: 'Prompt is required', status: 400 };
213
- }
214
-
215
- // Apply template overrides and resolve nextTemplateId
216
- applyTemplateOverrides(config);
217
- const { nextTemplateId, error: nextTemplateError } = resolveNextTemplateId(reqBody, config.nextTemplateId || null);
218
- if (nextTemplateError) {
219
- return { error: nextTemplateError, status: 400 };
220
- }
221
- config.nextTemplateId = nextTemplateId;
222
-
223
- // Validate git settings for git repos
224
- const { config: updatedConfig, error: gitError } = await validateGitSettings(config, project);
225
- if (gitError) {
226
- return { error: gitError, status: 400 };
227
- }
228
- Object.assign(config, updatedConfig);
229
-
230
- return { config, nextTemplateId };
231
- }
232
-
233
- /**
234
- * Create the session row and apply any post-create updates.
235
- * Returns the created session (already persisted in DB).
236
- */
237
- function createSessionRow(projectId, config, nextTemplateId, initialStatus) {
238
- const sessionName = config.name || generateInitialName(config.prompt);
239
- const session = sessions.create(projectId, sessionName, config.prompt, {
240
- mode: config.mode,
241
- thinkingEnabled: config.thinkingEnabled,
242
- gitBranch: config.gitBranch,
243
- parentSessionId: config.parentSessionId,
244
- status: initialStatus,
245
- model: config.model,
246
- providerId: config.providerId,
247
- effortLevel: config.effortLevel,
248
- agentType: config.agentType,
249
- });
250
-
251
- const postCreateUpdate = {
252
- ...(nextTemplateId ? { nextTemplateId } : {}),
253
- ...buildSchedulingUpdate(config, initialStatus),
254
- };
255
- if (Object.keys(postCreateUpdate).length > 0) {
256
- sessions.update(session.id, postCreateUpdate);
257
- }
258
- return session;
259
- }
260
-
261
- /**
262
- * Run setupAndStartSession and translate any failure into an error response,
263
- * marking the session as errored and broadcasting the update.
264
- */
265
- async function startSessionOrFail(req, res, { session, config, project }) {
266
- try {
267
- const { updatedSession } = await setupAndStartSession({
268
- session, config, project, projectId: req.params.id, files: config.files,
269
- });
270
- return res.status(201).json(updatedSession);
271
- } catch (error) {
272
- console.error('Git setup error:', error);
273
- const updatedSession = sessions.update(session.id, { status: 'error', error: error.message });
274
- broadcastToProject(req.params.id, WS_MESSAGE_TYPES.SESSION_UPDATED, {
275
- projectId: req.params.id,
276
- sessionId: session.id,
277
- session: updatedSession,
278
- });
279
- return res.status(500).json({ error: `Git setup failed: ${error.message}` });
280
- }
281
- }
282
-
283
207
  // POST /api/projects/:id/sessions - Create session
284
208
  // Supports both JSON and multipart/form-data (for file attachments)
285
209
  router.post('/:id/sessions', uploadMiddleware('files', 10), handleUploadError, async (req, res) => {
@@ -325,79 +249,13 @@ router.post('/:id/sessions', uploadMiddleware('files', 10), handleUploadError, a
325
249
  }
326
250
  });
327
251
 
328
- // GET /api/projects/:id/templates - List available templates for project (project + global)
329
- router.get('/:id/templates', (req, res) => {
330
- const project = projects.getById(req.params.id);
331
- if (!project) {
332
- return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
333
- }
334
-
335
- const available = sessionTemplates.getAvailableForProject(req.params.id);
336
- res.json(available);
337
- });
338
-
339
- // POST /api/projects/:id/templates - Create project template
340
- router.post('/:id/templates', (req, res) => {
341
- const project = projects.getById(req.params.id);
342
- if (!project) {
343
- return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
344
- }
345
-
346
- const result = CreateSessionTemplateRequest.safeParse(req.body);
347
- if (!result.success) {
348
- return res.status(400).json({ error: result.error.issues[0].message });
349
- }
350
-
351
- const template = sessionTemplates.create({
352
- projectId: req.params.id,
353
- ...result.data,
354
- });
355
- res.status(201).json(template);
356
- });
252
+ // Template routes are mounted as a sub-router
253
+ router.use('/:id/templates', projectTemplatesRouter);
357
254
 
358
255
  // Command button routes are mounted as a sub-router
359
- router.use('/:id/command-buttons', projectCommandButtonsRouter);
256
+ router.use('/:id/circus-commands', projectCommandButtonsRouter);
360
257
 
361
- // GET /api/projects/:id/session-defaults - Get session defaults for project
362
- router.get('/:id/session-defaults', (req, res) => {
363
- const project = projects.getById(req.params.id);
364
- if (!project) {
365
- return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
366
- }
367
-
368
- const defaults = projectDefaults.getByProjectId(req.params.id);
369
- if (!defaults) {
370
- return res.json(null);
371
- }
372
-
373
- res.json(defaults);
374
- });
375
-
376
- // POST /api/projects/:id/session-defaults - Update/create session defaults for project
377
- router.post('/:id/session-defaults', (req, res) => {
378
- const project = projects.getById(req.params.id);
379
- if (!project) {
380
- return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
381
- }
382
-
383
- const result = ProjectSessionDefaultsRequest.safeParse(req.body);
384
- if (!result.success) {
385
- return res.status(400).json({ error: result.error.issues[0].message });
386
- }
387
-
388
- const updated = projectDefaults.upsert(req.params.id, result.data);
389
- res.status(200).json(updated);
390
- });
391
-
392
- // DELETE /api/projects/:id/session-defaults - Reset session defaults for project
393
- router.delete('/:id/session-defaults', (req, res) => {
394
- const project = projects.getById(req.params.id);
395
- if (!project) {
396
- return res.status(404).json({ error: ERR_PROJECT_NOT_FOUND });
397
- }
398
-
399
- projectDefaults.resetToDefaults(req.params.id);
400
- res.json({ message: 'Session defaults reset to system defaults' });
401
- });
258
+ // Session defaults routes are mounted as a sub-router
259
+ router.use('/:id/session-defaults', projectSessionDefaultsRouter);
402
260
 
403
261
  export default router;
@@ -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,10 +2,13 @@ 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
 
9
+ // Error message constants
10
+ const ERR_BUTTON_NOT_FOUND = 'Circus Command not found';
11
+
9
12
  const router = Router();
10
13
 
11
14
  /**
@@ -46,16 +49,24 @@ function broadcastCommandError(ctx, errorMessage) {
46
49
  broadcastToProject(projectId, WS_MESSAGE_TYPES.COMMAND_RUN_ERROR, { projectId, sessionId, runId, buttonId, error: errorMessage });
47
50
  }
48
51
 
49
- // 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;
52
+ // GET /api/sessions/:id/circus-commands - List command buttons for the workflow project
53
+ router.get('/:id/circus-commands', requireRootSessionAndProject, (req, res) => {
54
+ res.json(commandButtons.getByProjectId(req.rootSession_.projectId));
55
+ });
56
+
57
+ // POST /api/sessions/:id/circus-commands/:buttonId/run - Execute button command
58
+ router.post('/:id/circus-commands/:buttonId/run', requireRootSessionAndProject, (req, res) => {
59
+ const sessionId = req.rootSessionId;
52
60
  const buttonId = req.params.buttonId;
53
61
 
54
62
  console.log(`[RUN] Starting command for buttonId: ${buttonId}, sessionId: ${sessionId}`);
55
63
 
56
64
  const button = commandButtons.getById(buttonId);
57
65
  if (!button) {
58
- return res.status(404).json({ error: 'Command button not found' });
66
+ return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
67
+ }
68
+ if (button.projectId !== req.rootSession_.projectId) {
69
+ return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
59
70
  }
60
71
 
61
72
  // Generate run ID
@@ -67,8 +78,8 @@ router.post('/:id/command-buttons/:buttonId/run', requireSessionAndProject, (req
67
78
  res.json({ runId, buttonId, status: 'running', output: '' });
68
79
 
69
80
  // Capture middleware values for use in async callbacks
70
- const projectId = req.session_.projectId;
71
- const workingDirectory = req.workingDirectory;
81
+ const projectId = req.rootSession_.projectId;
82
+ const workingDirectory = req.rootWorkingDirectory;
72
83
  const ctx = { sessionId, projectId, runId, buttonId };
73
84
 
74
85
  // Broadcast initial "running" status immediately so session list can show the running indicator
@@ -97,17 +108,18 @@ router.post('/:id/command-buttons/:buttonId/run', requireSessionAndProject, (req
97
108
  })();
98
109
  });
99
110
 
100
- // 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;
111
+ // GET /api/sessions/:id/circus-commands/runs - Get active runs for session
112
+ router.get('/:id/circus-commands/runs', requireRootSessionAndProject, (req, res) => {
113
+ const sessionId = req.rootSessionId;
103
114
 
104
115
  const activeRuns = commandRunner.getRunsBySession(sessionId);
105
116
  res.json(activeRuns);
106
117
  });
107
118
 
108
- // 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;
119
+ // GET /api/sessions/:id/circus-commands/runs/:runId - Get single run by ID
120
+ router.get('/:id/circus-commands/runs/:runId', requireRootSessionAndProject, (req, res) => {
121
+ const { runId } = req.params;
122
+ const sessionId = req.rootSessionId;
111
123
 
112
124
  // Check if run is currently running (in memory)
113
125
  if (commandRunner.isRunning(runId)) {
@@ -135,9 +147,9 @@ router.get('/:id/command-buttons/runs/:runId', requireSession, (req, res) => {
135
147
  });
136
148
  });
137
149
 
138
- // 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;
150
+ // DELETE /api/sessions/:id/circus-commands/runs/:runId - Delete a command run record
151
+ router.delete('/:id/circus-commands/runs/:runId', requireRootSessionAndProject, (req, res) => {
152
+ const sessionId = req.rootSessionId;
141
153
  const { runId } = req.params;
142
154
 
143
155
  const run = commandRuns.getById(runId);
@@ -151,7 +163,7 @@ router.delete('/:id/command-buttons/runs/:runId', requireSessionAndProject, (req
151
163
 
152
164
  commandRuns.deleteById(runId);
153
165
 
154
- const projectId = req.session_.projectId;
166
+ const projectId = req.rootSession_.projectId;
155
167
 
156
168
  // Broadcast deletion to session and project subscribers
157
169
  broadcastToSession(sessionId, WS_MESSAGE_TYPES.COMMAND_RUN_DELETED, {
@@ -169,19 +181,22 @@ router.delete('/:id/command-buttons/runs/:runId', requireSessionAndProject, (req
169
181
  res.status(204).send();
170
182
  });
171
183
 
172
- // 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;
184
+ // DELETE /api/sessions/:id/circus-commands/:buttonId/runs/all - Delete all runs for a button in a session
185
+ router.delete('/:id/circus-commands/:buttonId/runs/all', requireRootSessionAndProject, (req, res) => {
186
+ const sessionId = req.rootSessionId;
175
187
  const { buttonId } = req.params;
176
188
 
177
189
  const button = commandButtons.getById(buttonId);
178
190
  if (!button) {
179
- return res.status(404).json({ error: 'Command button not found' });
191
+ return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
192
+ }
193
+ if (button.projectId !== req.rootSession_.projectId) {
194
+ return res.status(404).json({ error: ERR_BUTTON_NOT_FOUND });
180
195
  }
181
196
 
182
197
  const { deletedRuns } = commandRuns.deleteByButtonAndSession(buttonId, sessionId);
183
198
 
184
- const projectId = req.session_.projectId;
199
+ const projectId = req.rootSession_.projectId;
185
200
 
186
201
  // Broadcast individual COMMAND_RUN_DELETED events for each deleted run
187
202
  for (const run of deletedRuns) {
@@ -201,13 +216,19 @@ router.delete('/:id/command-buttons/:buttonId/runs/all', requireSessionAndProjec
201
216
  res.status(204).send();
202
217
  });
203
218
 
204
- // 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;
219
+ // POST /api/sessions/:id/circus-commands/runs/:runId/kill - Kill running command
220
+ router.post('/:id/circus-commands/runs/:runId/kill', requireRootSessionAndProject, (req, res) => {
221
+ const sessionId = req.rootSessionId;
207
222
  const runId = req.params.runId;
208
223
 
209
224
  console.log(`[KILL] Kill request for runId: ${runId}, sessionId: ${sessionId}`);
210
225
 
226
+ const activeRuns = commandRunner.getRunsBySession(sessionId);
227
+ const activeRun = activeRuns.find((run) => run.runId === runId);
228
+ if (!activeRun) {
229
+ return res.status(404).json({ error: 'Run not found or already completed' });
230
+ }
231
+
211
232
  const killed = commandRunner.kill(runId);
212
233
  console.log(`[KILL] Kill result: ${killed} for runId: ${runId}`);
213
234
  if (!killed) {
@@ -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 });