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
@@ -0,0 +1,120 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import path from 'path';
4
+ import { chmod, mkdir, writeFile } from 'fs/promises';
5
+
6
+ const execAsync = promisify(exec);
7
+ const MANAGED_HOOKS_PATH = '.circuschief-hooks';
8
+ const ATTRIBUTION_CONFIG_KEY = 'circuschief.commitAttribution';
9
+ const ATTRIBUTION_ENV_KEY = 'CIRCUSCHIEF_COMMIT_ATTRIBUTION';
10
+
11
+ async function git(directory, command) {
12
+ const { stdout } = await execAsync(`git ${command}`, { cwd: directory });
13
+ return stdout.trim();
14
+ }
15
+
16
+ async function gitConfigValue(directory, key) {
17
+ try {
18
+ return await git(directory, `config --get ${key}`);
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ function shellQuote(value) {
25
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
26
+ }
27
+
28
+ function buildCommitMsgHook() {
29
+ return `#!/bin/sh
30
+ set -eu
31
+
32
+ msg_file="$1"
33
+ trailer="\${${ATTRIBUTION_ENV_KEY}:-}"
34
+
35
+ [ -n "$trailer" ] || exit 0
36
+ [ -n "$(printf '%s' "$trailer" | tr -d '[:space:]')" ] || exit 0
37
+
38
+ if printf '%s' "$trailer" | grep '[[:cntrl:]]' >/dev/null 2>&1; then
39
+ echo "${ATTRIBUTION_ENV_KEY} must be a canonical Co-authored-by: Name <email> trailer." >&2
40
+ exit 1
41
+ fi
42
+
43
+ if ! printf '%s\\n' "$trailer" | grep -E '^Co-authored-by: [^<>[:space:]][^<>]* <[^[:space:]<>@]+@[^[:space:]<>@]+\\.[^[:space:]<>@]+>$' >/dev/null 2>&1; then
44
+ echo "${ATTRIBUTION_ENV_KEY} must be a canonical Co-authored-by: Name <email> trailer." >&2
45
+ exit 1
46
+ fi
47
+
48
+ if grep -F -x -i -- "$trailer" "$msg_file" >/dev/null 2>&1; then
49
+ exit 0
50
+ fi
51
+
52
+ git interpret-trailers --trailer "$trailer" --in-place "$msg_file"
53
+ `;
54
+ }
55
+
56
+ export async function clearWorktreeCommitAttribution(worktreePath) {
57
+ const currentAttribution = await gitConfigValue(worktreePath, ATTRIBUTION_CONFIG_KEY);
58
+ const currentHooksPath = await gitConfigValue(worktreePath, 'core.hooksPath');
59
+ if (!currentAttribution && currentHooksPath !== MANAGED_HOOKS_PATH) {
60
+ return false;
61
+ }
62
+
63
+ await git(worktreePath, 'config extensions.worktreeConfig true');
64
+
65
+ if (currentAttribution) {
66
+ try {
67
+ await git(worktreePath, `config --worktree --unset ${ATTRIBUTION_CONFIG_KEY}`);
68
+ } catch {
69
+ // Unset is idempotent for callers that are clearing a value that was never set.
70
+ }
71
+ }
72
+
73
+ if (currentHooksPath === MANAGED_HOOKS_PATH) {
74
+ try {
75
+ await git(worktreePath, 'config --worktree --unset core.hooksPath');
76
+ } catch {
77
+ // Unset is idempotent for callers that are clearing stale managed hook config.
78
+ }
79
+ }
80
+ return false;
81
+ }
82
+
83
+ /**
84
+ * Install/update managed commit attribution enforcement for a worktree.
85
+ *
86
+ * Runtime attribution is process-scoped: the hook reads
87
+ * CIRCUSCHIEF_COMMIT_ATTRIBUTION from the `git commit` process environment.
88
+ *
89
+ * @param {string} worktreePath - The worktree directory
90
+ * @returns {Promise<boolean>} True when a hook is installed or updated
91
+ */
92
+ export async function ensureWorktreeCommitAttributionHook(worktreePath) {
93
+ await git(worktreePath, 'config extensions.worktreeConfig true');
94
+
95
+ const currentHooksPath = await gitConfigValue(worktreePath, 'core.hooksPath');
96
+ if (currentHooksPath && currentHooksPath !== MANAGED_HOOKS_PATH) {
97
+ throw new Error(
98
+ `Cannot install managed commit attribution hook: worktree already has core.hooksPath set to "${currentHooksPath}"`
99
+ );
100
+ }
101
+
102
+ try {
103
+ await git(worktreePath, `config --worktree --unset ${ATTRIBUTION_CONFIG_KEY}`);
104
+ } catch {
105
+ // Unset is idempotent for stale worktrees that never stored attribution.
106
+ }
107
+
108
+ await git(worktreePath, `config --worktree core.hooksPath ${shellQuote(MANAGED_HOOKS_PATH)}`);
109
+
110
+ const hooksDir = path.join(worktreePath, MANAGED_HOOKS_PATH);
111
+ const hookPath = path.join(hooksDir, 'commit-msg');
112
+ await mkdir(hooksDir, { recursive: true });
113
+ await writeFile(hookPath, buildCommitMsgHook(), 'utf8');
114
+ await chmod(hookPath, 0o755);
115
+ return true;
116
+ }
117
+
118
+ export async function configureWorktreeCommitAttribution(worktreePath, _commitAttribution) {
119
+ return ensureWorktreeCommitAttributionHook(worktreePath);
120
+ }
@@ -2,6 +2,11 @@ import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  import path from 'path';
4
4
  import { realpath } from 'fs/promises';
5
+ export {
6
+ clearWorktreeCommitAttribution,
7
+ configureWorktreeCommitAttribution,
8
+ ensureWorktreeCommitAttributionHook,
9
+ } from './gitCommitAttribution.js';
5
10
 
6
11
  const execAsync = promisify(exec);
7
12
 
@@ -76,6 +81,10 @@ async function git(directory, command, opts = {}) {
76
81
  return stdout.trim();
77
82
  }
78
83
 
84
+ function shellQuote(value) {
85
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
86
+ }
87
+
79
88
  /**
80
89
  * Detect the default branch using git commands (symbolic-ref, rev-parse).
81
90
  * @param {string} directory
@@ -515,8 +524,8 @@ export async function pinAuthorInWorktree(worktreePath, projectDir, { env } = {}
515
524
 
516
525
  // Pin the human's identity in the worktree config so they are always
517
526
  // the commit Author, regardless of what the session does later
518
- await git(worktreePath, `config --worktree user.name "${author.name}"`);
519
- await git(worktreePath, `config --worktree user.email "${author.email}"`);
527
+ await git(worktreePath, `config --worktree user.name ${shellQuote(author.name)}`);
528
+ await git(worktreePath, `config --worktree user.email ${shellQuote(author.email)}`);
520
529
 
521
530
  return true;
522
531
  }
@@ -9,9 +9,17 @@ import * as gitService from './gitService.js';
9
9
  * @param {string|null} options.gitBranch - Branch name
10
10
  * @param {string} options.sessionId - Session ID
11
11
  * @param {string|null} [options.worktreeBasePath] - Custom base path for worktrees (overrides default .worktrees)
12
+ * @param {string|null} [options.commitAttributionOverride] - Deprecated; attribution is process-scoped at agent launch
12
13
  * @returns {Promise<{workingDirectory: string, gitWorktree: string|null}>}
13
14
  */
14
- export async function setupGitForSession({ projectDir, gitMode, gitBranch, sessionId, worktreeBasePath }) {
15
+ export async function setupGitForSession({
16
+ projectDir,
17
+ gitMode,
18
+ gitBranch,
19
+ sessionId,
20
+ worktreeBasePath,
21
+ commitAttributionOverride = null,
22
+ }) {
15
23
  // No git operations if gitMode is not specified
16
24
  if (!gitMode || !gitBranch) {
17
25
  return {
@@ -35,6 +43,8 @@ export async function setupGitForSession({ projectDir, gitMode, gitBranch, sessi
35
43
  await gitService.createWorktreeForBranch(projectDir, gitBranch, worktreePath);
36
44
  // Pin the human developer's git identity so they are the commit Author
37
45
  await gitService.pinAuthorInWorktree(worktreePath, projectDir);
46
+ void commitAttributionOverride;
47
+ await gitService.ensureWorktreeCommitAttributionHook(worktreePath);
38
48
  return {
39
49
  workingDirectory: worktreePath,
40
50
  gitWorktree: worktreePath,
@@ -5,11 +5,11 @@ import {
5
5
  projects,
6
6
  } from '../database.js';
7
7
  import { broadcastToProject } from '../websocket.js';
8
- import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
8
+ import { WS_MESSAGE_TYPES, DEFAULT_RESCHEDULE_DELAY_MINUTES } from '../../../shared/src/index.js';
9
9
  import { renderTemplatePrompt, getRootSession } from './templateTriggerService.js';
10
10
  import { setupGitForSession } from './gitSessionSetup.js';
11
11
  import { runSession } from './sessionManager.js';
12
- import { resolveAgentTypeFromModel } from './sessionProvider.js';
12
+ import { resolveAgentTypeFromModel, resolveProviderMetadataFromModel } from './sessionProvider.js';
13
13
 
14
14
  // Maximum depth for recursive lane-entry template triggers
15
15
  export const MAX_LANE_TRIGGER_DEPTH = 5;
@@ -56,6 +56,8 @@ export async function determineWorkingDirectory(parentSession, project, gitOptio
56
56
  gitBranch: gitOptions.gitBranch || null,
57
57
  sessionId: gitOptions.sessionId,
58
58
  worktreeBasePath: project.worktreePath || null,
59
+ commitAttributionOverride:
60
+ resolveProviderMetadataFromModel(gitOptions.model)?.commitAttributionOverride ?? null,
59
61
  });
60
62
  return { workingDirectory: gitSetup.workingDirectory, gitWorktree: gitSetup.gitWorktree };
61
63
  }
@@ -190,6 +192,7 @@ export async function triggerOnEnterTemplate(sessionId, lane, options = {}) {
190
192
  gitMode: settings.gitMode,
191
193
  gitBranch: settings.gitBranch,
192
194
  sessionId: newSession.id,
195
+ model: settings.model,
193
196
  });
194
197
  if (gitWorktree) {
195
198
  sessions.update(newSession.id, { gitWorktree });
@@ -250,7 +253,7 @@ async function buildChildSessionFromPrompt(lane, session, depth) {
250
253
  if (lane.onEnterAutoRescheduleEnabled) {
251
254
  Object.assign(sessionUpdates, {
252
255
  autoRescheduleEnabled: true,
253
- rescheduleDelayMinutes: lane.onEnterRescheduleDelayMinutes || 15,
256
+ rescheduleDelayMinutes: lane.onEnterRescheduleDelayMinutes || DEFAULT_RESCHEDULE_DELAY_MINUTES,
254
257
  rescheduleOnTokenLimit: lane.onEnterRescheduleOnTokenLimit ?? true,
255
258
  rescheduleOnServiceError: lane.onEnterRescheduleOnServiceError ?? true,
256
259
  maxRescheduleCount: lane.onEnterMaxRescheduleCount || null,
@@ -1,5 +1,10 @@
1
1
  import { spawn } from 'child_process';
2
2
  import path from 'path';
3
+ import {
4
+ captureSpawnAttempt,
5
+ createCapturedSpawnProcess,
6
+ isE2ESpawnCaptureEnabled,
7
+ } from './e2eSpawnCapture.js';
3
8
 
4
9
  /**
5
10
  * Get the directory containing the current Node.js executable.
@@ -42,6 +47,10 @@ export function createRobustEnv(baseEnv = process.env) {
42
47
  export function createClaudeCodeSpawner() {
43
48
  return (options) => {
44
49
  const { command, args, cwd, env, signal } = options;
50
+ if (isE2ESpawnCaptureEnabled()) {
51
+ captureSpawnAttempt('claude-code', options);
52
+ return createCapturedSpawnProcess('claude-code');
53
+ }
45
54
 
46
55
  // Replace 'node' with the absolute path to the current Node executable
47
56
  // This ensures we use the same Node that's running our app
@@ -126,8 +126,8 @@ export async function extractPrUrlIfNeeded(sessionId) {
126
126
  const session = sessions.getById(sessionId);
127
127
  if (!session) return;
128
128
 
129
- // Skip if session already has a PR URL
130
- if (session.prUrl) return;
129
+ // Skip if session already has a PR URL or the user cleared automatic PR linking.
130
+ if (session.prUrl || session.prUrlAutoLinkDisabled) return;
131
131
 
132
132
  // Extract PR URL from messages
133
133
  const prUrl = extractPrUrlFromMessages(sessionId);
@@ -145,7 +145,7 @@ export async function extractPrUrlIfNeeded(sessionId) {
145
145
  if (session.parentSessionId) {
146
146
  const rootId = sessions.getRootSessionId(sessionId);
147
147
  const root = rootId && rootId !== sessionId ? sessions.getById(rootId) : null;
148
- if (root && !root.prUrl) {
148
+ if (root && !root.prUrl && !root.prUrlAutoLinkDisabled) {
149
149
  sessions.update(root.id, { prUrl });
150
150
  console.log(`[PrUrlService] Propagated PR URL from session ${sessionId} to root ${root.id}: ${prUrl}`);
151
151
 
@@ -0,0 +1,90 @@
1
+ import { createClaudeCodeSpawner } from './nodeSpawnHelper.js';
2
+ import {
3
+ buildSystemPromptConfig,
4
+ getPermissionModeForSession,
5
+ getSandboxModeForSession,
6
+ } from './sessionPrompts.js';
7
+
8
+ /**
9
+ * Build query parameters for the Claude Code adapter.
10
+ * @returns {Object}
11
+ */
12
+ function buildClaudeCodeQueryParams({
13
+ prompt, workingDirectory, controller, session, sessionId, systemPrompt,
14
+ model, sessionEnv, resumeSessionId = null,
15
+ }) {
16
+ const isVCR = Boolean(process.env.VCR_MODE);
17
+ const effectiveModel = isVCR ? 'claude-haiku-4-5-20251001' : model;
18
+
19
+ return {
20
+ prompt,
21
+ options: {
22
+ cwd: workingDirectory,
23
+ abortController: controller,
24
+ includePartialMessages: true,
25
+ permissionMode: getPermissionModeForSession(session.mode),
26
+ // Match normal Claude Code CLI behavior: load user-level settings
27
+ // such as configured MCP servers, then project/local overrides.
28
+ settingSources: ['user', 'project', 'local'],
29
+ ...(resumeSessionId && { resume: resumeSessionId }),
30
+ env: sessionEnv,
31
+ spawnClaudeCodeProcess: createClaudeCodeSpawner(),
32
+ model: effectiveModel,
33
+ systemPrompt: buildSystemPromptConfig(sessionId, session.projectId, systemPrompt, session.mode),
34
+ },
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Build query parameters for the Codex adapter.
40
+ *
41
+ * Codex in v1 is a simple Chat-Completions-shaped executor. It does not need
42
+ * or accept Claude-specific options such as permissionMode, settingSources,
43
+ * includePartialMessages, spawnClaudeCodeProcess, or resume.
44
+ *
45
+ * @returns {Object}
46
+ */
47
+ function buildCodexQueryParams({
48
+ prompt, workingDirectory, controller, session, sessionId, systemPrompt, model, sessionEnv,
49
+ }) {
50
+ const isVCR = Boolean(process.env.VCR_MODE);
51
+ const effectiveModel = isVCR ? 'gpt-4o-mini' : model;
52
+
53
+ return {
54
+ prompt,
55
+ options: {
56
+ cwd: workingDirectory,
57
+ abortController: controller,
58
+ env: sessionEnv,
59
+ model: effectiveModel,
60
+ effortLevel: session?.effortLevel ?? null,
61
+ systemPrompt: buildSystemPromptConfig(sessionId, session.projectId, systemPrompt, session.mode),
62
+ sandboxMode: getSandboxModeForSession(session?.mode),
63
+ },
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Build query parameters for executing a session via the configured agent.
69
+ * Shared by runSession, continueSession, and continueSessionWithExistingMessage.
70
+ *
71
+ * @param {Object} options
72
+ * @param {string} options.prompt - The prompt text to send
73
+ * @param {string} options.workingDirectory - Session working directory
74
+ * @param {AbortController} options.controller - Abort controller for the session
75
+ * @param {Object} options.session - Session object from DB
76
+ * @param {string} options.sessionId - Session ID
77
+ * @param {string|null} options.systemPrompt - Custom system prompt from project settings
78
+ * @param {string|null} options.model - Model to use
79
+ * @param {Object} options.sessionEnv - Environment variables for the session
80
+ * @param {string|null} [options.resumeSessionId] - Session ID to resume (null for new session)
81
+ * @param {string} [options.agentType] - 'claude-code' (default) | 'codex'
82
+ * @returns {Object} Query parameters for agent.execute()
83
+ */
84
+ export function buildQueryParams(options) {
85
+ const { agentType = 'claude-code' } = options || {};
86
+ if (agentType === 'codex') {
87
+ return buildCodexQueryParams(options);
88
+ }
89
+ return buildClaudeCodeQueryParams(options);
90
+ }
@@ -3,7 +3,6 @@ import {
3
3
  conversations,
4
4
  messages,
5
5
  canvasItems,
6
- sessionNotes,
7
6
  sessionSummaries,
8
7
  projects,
9
8
  } from '../db/index.js';
@@ -47,10 +46,7 @@ export async function duplicateSession(sourceSessionId, options = {}) {
47
46
  // 5. Duplicate canvas items
48
47
  canvasItems.duplicateForSession(sourceSessionId, newSession.id);
49
48
 
50
- // 6. Duplicate session notes
51
- sessionNotes.duplicateForSession(sourceSessionId, newSession.id);
52
-
53
- // 7. Duplicate session summary (if exists)
49
+ // 6. Duplicate session summary (if exists)
54
50
  sessionSummaries.duplicateForSession(sourceSessionId, newSession.id);
55
51
 
56
52
  // Return the updated session
@@ -1,14 +1,13 @@
1
1
  import { sessions, messages, attachments, conversations } from '../database.js';
2
- import { createClaudeCodeSpawner } from './nodeSpawnHelper.js';
3
2
  import { createCodexSpawner } from './codexSpawnHelper.js';
4
- import { resolveProviderFromModel, buildSessionEnv } from './sessionProvider.js';
3
+ import { resolveProviderFromModel, resolveProviderMetadataFromModel, buildSessionEnv } from './sessionProvider.js';
5
4
  import { agentGateway } from '../agents/AgentGateway.js';
6
5
  import { LoggingAgentWrapper } from '../agents/LoggingAgentWrapper.js';
7
6
  import { VCRAgentAdapter } from '../agents/vcr/VCRAgentAdapter.js';
7
+ import { isE2ESpawnCaptureEnabled } from './e2eSpawnCapture.js';
8
+ export { buildQueryParams } from './queryParamBuilder.js';
9
+ import { buildQueryParams } from './queryParamBuilder.js';
8
10
  import {
9
- buildSystemPromptConfig,
10
- getPermissionModeForSession,
11
- getSandboxModeForSession,
12
11
  buildPromptWithAttachments,
13
12
  } from './sessionPrompts.js';
14
13
  import {
@@ -23,6 +22,7 @@ import {
23
22
  import { shouldRescheduleOnError, _checkProactiveReschedule } from './sessionErrors.js';
24
23
  import { schedulerService } from './schedulerService.js';
25
24
  import { buildConversationContextForModelSwitch, buildConversationContextForContinuation } from './conversationContext.js';
25
+ import { ensureWorktreeCommitAttributionHook } from './gitService.js';
26
26
  import { broadcastToSession } from '../websocket.js';
27
27
  import { WS_MESSAGE_TYPES } from '../../../shared/src/index.js';
28
28
 
@@ -40,6 +40,34 @@ function buildAgentConfig(agentType) {
40
40
  return {};
41
41
  }
42
42
 
43
+ export function buildAgentEnv(sessionEnv, commitAttributionOverride) {
44
+ const env = { ...(sessionEnv || {}) };
45
+ if (commitAttributionOverride) {
46
+ env.CIRCUSCHIEF_COMMIT_ATTRIBUTION = commitAttributionOverride;
47
+ } else {
48
+ delete env.CIRCUSCHIEF_COMMIT_ATTRIBUTION;
49
+ }
50
+ return env;
51
+ }
52
+
53
+ async function resolveInitialSessionModelEnv(session, model) {
54
+ const effectiveModel = model || session.model;
55
+ const provider = resolveProviderFromModel(effectiveModel);
56
+ const providerMetadata = resolveProviderMetadataFromModel(effectiveModel);
57
+ const commitAttributionOverride = providerMetadata?.commitAttributionOverride ?? null;
58
+
59
+ if (session.gitWorktree && commitAttributionOverride) {
60
+ await ensureWorktreeCommitAttributionHook(session.gitWorktree);
61
+ }
62
+
63
+ const baseSessionEnv = buildSessionEnv(provider, session.thinkingEnabled, session.effortLevel);
64
+ return {
65
+ effectiveModel,
66
+ sessionEnv: buildAgentEnv(baseSessionEnv, commitAttributionOverride),
67
+ commitAttributionOverride,
68
+ };
69
+ }
70
+
43
71
  /**
44
72
  * Create the agent for a session, using gateway + logging + VCR.
45
73
  *
@@ -56,7 +84,7 @@ export function createAgentForSession(agentType = 'claude-code', config = {}) {
56
84
  const baseAgent = agentGateway.createAgent(agentType, mergedConfig);
57
85
 
58
86
  // Wrap with VCR adapter if in VCR mode
59
- const agent = process.env.VCR_MODE
87
+ const agent = process.env.VCR_MODE && !isE2ESpawnCaptureEnabled()
60
88
  ? new VCRAgentAdapter(baseAgent, { cassetteDir: 'tests/e2e/cassettes' })
61
89
  : baseAgent;
62
90
 
@@ -64,95 +92,6 @@ export function createAgentForSession(agentType = 'claude-code', config = {}) {
64
92
  return new LoggingAgentWrapper(agent);
65
93
  }
66
94
 
67
- /**
68
- * Build query parameters for the Claude Code adapter.
69
- * @returns {Object}
70
- */
71
- function buildClaudeCodeQueryParams({
72
- prompt, workingDirectory, controller, session, sessionId, systemPrompt,
73
- model, sessionEnv, resumeSessionId = null,
74
- }) {
75
- const isVCR = Boolean(process.env.VCR_MODE);
76
- const effectiveModel = isVCR ? 'claude-haiku-4-5-20251001' : model;
77
-
78
- return {
79
- prompt,
80
- options: {
81
- cwd: workingDirectory,
82
- abortController: controller,
83
- includePartialMessages: true,
84
- permissionMode: getPermissionModeForSession(session.mode),
85
- settingSources: ['project'],
86
- ...(resumeSessionId && { resume: resumeSessionId }),
87
- env: sessionEnv,
88
- spawnClaudeCodeProcess: createClaudeCodeSpawner(),
89
- model: effectiveModel,
90
- systemPrompt: buildSystemPromptConfig(sessionId, session.projectId, systemPrompt, session.mode),
91
- },
92
- };
93
- }
94
-
95
- /**
96
- * Build query parameters for the Codex adapter.
97
- *
98
- * Codex in v1 is a simple Chat-Completions-shaped executor — it doesn't need
99
- * or accept Claude-specific options (permissionMode, settingSources,
100
- * includePartialMessages, spawnClaudeCodeProcess, resume).
101
- *
102
- * Codex does have its own sandboxing model, driven from {@code session.mode}
103
- * via {@link getSandboxModeForSession}. Codex CLI v0.124.0 also supports
104
- * resume via `codex resume` / `codex exec resume`, but Circus Chief v1
105
- * intentionally does NOT pass a resume token — wiring is deferred to a
106
- * later phase (see canvas plan §Phase 4.5).
107
- *
108
- * @returns {Object}
109
- */
110
- function buildCodexQueryParams({
111
- prompt, workingDirectory, controller, session, sessionId, systemPrompt, model, sessionEnv,
112
- }) {
113
- const isVCR = Boolean(process.env.VCR_MODE);
114
- // In VCR mode, force the cheapest commonly-cassetted OpenAI model.
115
- const effectiveModel = isVCR ? 'gpt-4o-mini' : model;
116
-
117
- return {
118
- prompt,
119
- options: {
120
- cwd: workingDirectory,
121
- abortController: controller,
122
- env: sessionEnv,
123
- model: effectiveModel,
124
- effortLevel: session?.effortLevel ?? null,
125
- systemPrompt: buildSystemPromptConfig(sessionId, session.projectId, systemPrompt, session.mode),
126
- sandboxMode: getSandboxModeForSession(session?.mode),
127
- },
128
- };
129
- }
130
-
131
- /**
132
- * Build query parameters for executing a session via the configured agent.
133
- * Shared by runSession, continueSession, and continueSessionWithExistingMessage.
134
- *
135
- * @param {Object} options
136
- * @param {string} options.prompt - The prompt text to send
137
- * @param {string} options.workingDirectory - Session working directory
138
- * @param {AbortController} options.controller - Abort controller for the session
139
- * @param {Object} options.session - Session object from DB
140
- * @param {string} options.sessionId - Session ID
141
- * @param {string|null} options.systemPrompt - Custom system prompt from project settings
142
- * @param {string|null} options.model - Model to use
143
- * @param {Object} options.sessionEnv - Environment variables for the session
144
- * @param {string|null} [options.resumeSessionId] - Session ID to resume (null for new session)
145
- * @param {string} [options.agentType] - 'claude-code' (default) | 'codex'
146
- * @returns {Object} Query parameters for agent.execute()
147
- */
148
- export function buildQueryParams(options) {
149
- const { agentType = 'claude-code' } = options || {};
150
- if (agentType === 'codex') {
151
- return buildCodexQueryParams(options);
152
- }
153
- return buildClaudeCodeQueryParams(options);
154
- }
155
-
156
95
  /**
157
96
  * Execute the agent stream loop and handle post-turn completion, errors, and cleanup.
158
97
  * This is the shared core of runSession, continueSession, and continueSessionWithExistingMessage.
@@ -258,7 +197,12 @@ function buildContinueModelAndEnv(session, sessionId, model) {
258
197
 
259
198
  // Derive provider from the effective model ID (returns null for Anthropic/SDK defaults)
260
199
  const provider = resolveProviderFromModel(effectiveModel);
261
- const sessionEnv = buildSessionEnv(provider, session.thinkingEnabled, session.effortLevel);
200
+ const providerMetadata = resolveProviderMetadataFromModel(effectiveModel);
201
+ const commitAttributionOverride = providerMetadata?.commitAttributionOverride ?? null;
202
+ const sessionEnv = buildAgentEnv(
203
+ buildSessionEnv(provider, session.thinkingEnabled, session.effortLevel),
204
+ commitAttributionOverride
205
+ );
262
206
 
263
207
  // Check if model changed from the session's last requested model
264
208
  // When model changes, we can't resume the previous session - thinking blocks and
@@ -273,7 +217,13 @@ function buildContinueModelAndEnv(session, sessionId, model) {
273
217
  updatedSession = sessions.getById(sessionId); // refresh
274
218
  }
275
219
 
276
- return { effectiveModel, sessionEnv, modelChanged, session: updatedSession };
220
+ return {
221
+ effectiveModel,
222
+ sessionEnv,
223
+ commitAttributionOverride,
224
+ modelChanged,
225
+ session: updatedSession,
226
+ };
277
227
  }
278
228
 
279
229
  /**
@@ -284,7 +234,7 @@ function buildContinueModelAndEnv(session, sessionId, model) {
284
234
  async function buildContinueParams({
285
235
  sessionId, session, model, systemPrompt, effectiveModel, sessionEnv,
286
236
  modelChanged, activeConversation, promptWithAttachments,
287
- workingDirectory, controller, agentType, agent,
237
+ workingDirectory, controller, agentType, agent, commitAttributionOverride,
288
238
  }) {
289
239
  // Only resume if we have a session ID AND model hasn't changed AND the
290
240
  // agent supports resume.
@@ -306,6 +256,7 @@ async function buildContinueParams({
306
256
  sessionEnv,
307
257
  resumeSessionId: canResume ? activeConversation.claudeSessionId : null,
308
258
  agentType,
259
+ commitAttributionOverride,
309
260
  });
310
261
 
311
262
  // Logging metadata for agent call tracking
@@ -392,11 +343,15 @@ export async function continueSessionCore(sessionId, content, workingDirectory,
392
343
  // Resolve model/provider and detect model changes
393
344
  const modelEnv = buildContinueModelAndEnv(session, sessionId, model);
394
345
  session = modelEnv.session;
346
+ if (session.gitWorktree && modelEnv.commitAttributionOverride) {
347
+ await ensureWorktreeCommitAttributionHook(session.gitWorktree);
348
+ }
395
349
 
396
350
  // Build query params and agent call meta
397
351
  const { queryParams, agentCallMeta } = await buildContinueParams({
398
352
  sessionId, session, model, systemPrompt,
399
353
  effectiveModel: modelEnv.effectiveModel, sessionEnv: modelEnv.sessionEnv,
354
+ commitAttributionOverride: modelEnv.commitAttributionOverride,
400
355
  modelChanged: modelEnv.modelChanged, activeConversation, promptWithAttachments,
401
356
  workingDirectory, controller, agentType, agent,
402
357
  });
@@ -457,14 +412,8 @@ export async function runSessionCore(sessionId, prompt, workingDirectory, config
457
412
  const agentType = session.agentType || 'claude-code';
458
413
  const agent = createAgentForSession(agentType);
459
414
 
460
- // Resolve the effective model: fall back to session.model as defense-in-depth
461
- // (draftSessionService already resolves the model upstream, but this ensures
462
- // correctness if called directly).
463
- const effectiveModel = model || session.model;
464
-
465
- // Derive provider from the effective model ID (returns null for Anthropic/SDK defaults)
466
- const provider = resolveProviderFromModel(effectiveModel);
467
- const sessionEnv = buildSessionEnv(provider, session.thinkingEnabled, session.effortLevel);
415
+ const { effectiveModel, sessionEnv, commitAttributionOverride } =
416
+ await resolveInitialSessionModelEnv(session, model);
468
417
 
469
418
  const queryParams = buildQueryParams({
470
419
  prompt: promptWithAttachments,
@@ -476,6 +425,7 @@ export async function runSessionCore(sessionId, prompt, workingDirectory, config
476
425
  model: effectiveModel,
477
426
  sessionEnv,
478
427
  agentType,
428
+ commitAttributionOverride,
479
429
  });
480
430
 
481
431
  // Log query params for debugging third-party provider issues