circuschief 0.7.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 (142) 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 +10 -4
  8. package/packages/server/src/api/projects.js +10 -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-lifecycle.js +10 -10
  12. package/packages/server/src/api/sessions-patch.js +4 -0
  13. package/packages/server/src/api/sessions.js +6 -5
  14. package/packages/server/src/database.js +0 -2
  15. package/packages/server/src/db/DatabaseManager.js +5 -1
  16. package/packages/server/src/db/ProjectDefaultsRepository.js +3 -3
  17. package/packages/server/src/db/ProviderRepository.js +87 -32
  18. package/packages/server/src/db/SessionRepository.js +1 -0
  19. package/packages/server/src/db/index.js +0 -3
  20. package/packages/server/src/db/migrations/index.js +36 -202
  21. package/packages/server/src/db/seedBaselineData.js +137 -0
  22. package/packages/server/src/db/session-helpers.js +6 -3
  23. package/packages/server/src/middleware/sessionLookup.js +81 -8
  24. package/packages/server/src/schema.sql +149 -132
  25. package/packages/server/src/scripts/backupDatabase.js +21 -0
  26. package/packages/server/src/scripts/dbUtils.js +81 -0
  27. package/packages/server/src/scripts/inspectDatabaseSchema.js +81 -0
  28. package/packages/server/src/scripts/validateDatabaseBaseline.js +212 -0
  29. package/packages/server/src/services/codexSpawnHelper.js +9 -0
  30. package/packages/server/src/services/commandButtonPrompts.js +8 -8
  31. package/packages/server/src/services/commandRunner.js +7 -1
  32. package/packages/server/src/services/e2eSpawnCapture.js +147 -0
  33. package/packages/server/src/services/gitCommitAttribution.js +120 -0
  34. package/packages/server/src/services/gitService.js +11 -2
  35. package/packages/server/src/services/gitSessionSetup.js +11 -1
  36. package/packages/server/src/services/kanbanTriggers.js +6 -3
  37. package/packages/server/src/services/nodeSpawnHelper.js +9 -0
  38. package/packages/server/src/services/prUrlService.js +3 -3
  39. package/packages/server/src/services/queryParamBuilder.js +90 -0
  40. package/packages/server/src/services/sessionDuplicator.js +1 -5
  41. package/packages/server/src/services/sessionExecution.js +56 -108
  42. package/packages/server/src/services/sessionPrompts.js +12 -47
  43. package/packages/server/src/services/sessionProvider.js +10 -0
  44. package/packages/server/src/services/summaryService.js +5 -3
  45. package/packages/server/src/services/summaryStaleCheck.js +23 -4
  46. package/packages/server/src/services/templateTriggerService.js +3 -1
  47. package/packages/shared/src/constants.js +3 -0
  48. package/packages/shared/src/contracts/commandButtons.js +16 -2
  49. package/packages/shared/src/contracts/projects.js +2 -2
  50. package/packages/shared/src/contracts/providers.js +60 -0
  51. package/packages/shared/src/contracts/sessions.js +2 -1
  52. package/packages/shared/src/contracts/templates.js +2 -2
  53. package/packages/shared/src/types.js +1 -9
  54. package/packages/shared/src/utils.js +2 -2
  55. package/packages/web/dist/assets/{ActiveSessionsView-UJsCILDL.js → ActiveSessionsView-B0XHqLmv.js} +1 -1
  56. package/packages/web/dist/assets/{AgentLogsView-BGFPLjLa.js → AgentLogsView-DmsjUMlB.js} +2 -2
  57. package/packages/web/dist/assets/ApiClient-C3ztI9s9.js +1 -0
  58. package/packages/web/dist/assets/ArchiveConfirmModal-BlCyn5Vt.js +1 -0
  59. package/packages/web/dist/assets/ArchiveConfirmModal-DeoCVGXt.css +1 -0
  60. package/packages/web/dist/assets/{CommandButtonDetailView-D8S258uP.js → CommandButtonDetailView-CdSCPp78.js} +1 -1
  61. package/packages/web/dist/assets/EffortLevelSelector-hc2MNKg6.js +1 -0
  62. package/packages/web/dist/assets/{GeneralSettingsView-DsHChEhv.js → GeneralSettingsView-D1nI8_zk.js} +1 -1
  63. package/packages/web/dist/assets/InputWithButton-CAkttyqx.js +1 -0
  64. package/packages/web/dist/assets/{InputWithButton-cYdrEmTs.css → InputWithButton-D9HMvfR5.css} +1 -1
  65. package/packages/web/dist/assets/{InterpolationHelp-CIkOSkWX.js → InterpolationHelp-BO1j9Z3_.js} +1 -1
  66. package/packages/web/dist/assets/MarkdownEditor-ucRAP_UM.js +2 -0
  67. package/packages/web/dist/assets/{ModelSelector-D8hbTRIt.css → ModelSelector-BSxKUSus.css} +1 -1
  68. package/packages/web/dist/assets/{ModelSelector-BMpR0DPr.js → ModelSelector-CwTz8ZWO.js} +1 -1
  69. package/packages/web/dist/assets/{NewSessionView-CUUdHkfv.css → NewSessionView-BDPb-1qr.css} +1 -1
  70. package/packages/web/dist/assets/NewSessionView-BsDrp8mj.js +3 -0
  71. package/packages/web/dist/assets/ProjectEditView-CwTOeSun.js +1 -0
  72. package/packages/web/dist/assets/{ProjectEditView-D9sK0fdH.css → ProjectEditView-J15mcsWz.css} +1 -1
  73. package/packages/web/dist/assets/{ProjectListView-B9FuWESY.js → ProjectListView-DcNyuINs.js} +1 -1
  74. package/packages/web/dist/assets/{ProjectNewView-D62jYlBL.js → ProjectNewView-B5YV62hv.js} +1 -1
  75. package/packages/web/dist/assets/ProvidersView-bZemq_Rv.css +1 -0
  76. package/packages/web/dist/assets/ProvidersView-nY9GnDdO.js +1 -0
  77. package/packages/web/dist/assets/QuickResponseSettings-B352c75l.css +1 -0
  78. package/packages/web/dist/assets/{QuickResponseSettings-CDm5vwP7.js → QuickResponseSettings-BQwQXuL7.js} +1 -1
  79. package/packages/web/dist/assets/{QuickResponsesPanel-DZ_Lre_l.js → QuickResponsesPanel-BzSYcCSP.js} +1 -1
  80. package/packages/web/dist/assets/{ResizableTextarea-DiIOEGjN.js → ResizableTextarea-B3YIdIXv.js} +1 -1
  81. package/packages/web/dist/assets/{SessionCard-DmjnVYWn.js → SessionCard-CjE1tXiT.js} +1 -1
  82. package/packages/web/dist/assets/SessionDetailView-3cPZrbS3.js +36 -0
  83. package/packages/web/dist/assets/SessionDetailView-CZRZMrfM.css +1 -0
  84. package/packages/web/dist/assets/{SessionFormOptions-DYUISplS.js → SessionFormOptions-B6AxyREh.js} +1 -1
  85. package/packages/web/dist/assets/SessionListView-B5_6gW49.css +1 -0
  86. package/packages/web/dist/assets/SessionListView-CLXBfLcq.js +1 -0
  87. package/packages/web/dist/assets/{SessionLogStream-DpUE6Xsh.js → SessionLogStream-LlZ3z_Xj.js} +1 -1
  88. package/packages/web/dist/assets/{SettingsView-BC055tIA.js → SettingsView-CTGiGvR2.js} +1 -1
  89. package/packages/web/dist/assets/{SlashCommandWizard-DmTyNG9O.js → SlashCommandWizard-Cy04d7-o.js} +1 -1
  90. package/packages/web/dist/assets/{SlashCommandWizard-Dn7sNaBd.css → SlashCommandWizard-DJzw3LP5.css} +1 -1
  91. package/packages/web/dist/assets/{SummarySettingsView-BgnRCwlq.js → SummarySettingsView-BR2ZjEa3.js} +1 -1
  92. package/packages/web/dist/assets/{TemplateDetailView-BlhOmLUX.js → TemplateDetailView-DH6Oswsp.js} +1 -1
  93. package/packages/web/dist/assets/{commandButtons-D4RPpLiu.js → commandButtons-BfqR-fqq.js} +1 -1
  94. package/packages/web/dist/assets/{index-CfL84oGW.js → index-1zziPL6l.js} +1 -1
  95. package/packages/web/dist/assets/{index-OfCywayk.js → index-7kzHPxSF.js} +1 -1
  96. package/packages/web/dist/assets/{index-PDesaJc6.js → index-B0N_obMc.js} +1 -1
  97. package/packages/web/dist/assets/{index-Cpy4-yv3.js → index-BNk_gdfI.js} +1 -1
  98. package/packages/web/dist/assets/{index-Cs2nxhrT.css → index-BY174HVJ.css} +1 -1
  99. package/packages/web/dist/assets/{index-9vb2KaAd.js → index-CSqaAH-0.js} +1 -1
  100. package/packages/web/dist/assets/{index-CNwkdB0T.js → index-C_q4WlK8.js} +1 -1
  101. package/packages/web/dist/assets/{index-B0CvZXuN.js → index-D1wpU4y0.js} +1 -1
  102. package/packages/web/dist/assets/{index-4rhEeO0B.js → index-D5zCA8sD.js} +1 -1
  103. package/packages/web/dist/assets/{index-CkmxO8Mm.js → index-DGR8ELWY.js} +1 -1
  104. package/packages/web/dist/assets/{index-CrAQJmoZ.js → index-DHga8pXo.js} +1 -1
  105. package/packages/web/dist/assets/{index-BUhvkAdF.js → index-DSby02Wl.js} +1 -1
  106. package/packages/web/dist/assets/{index-BGwH4Cfn.js → index-DgkC10TW.js} +3 -3
  107. package/packages/web/dist/assets/{index-DfrE0gAC.js → index-DqjXJTVI.js} +1 -1
  108. package/packages/web/dist/assets/{index-Bn5xdGFM.js → index-DtfUt785.js} +1 -1
  109. package/packages/web/dist/assets/{index-KwEyz0F3.js → index-_4S2uLDI.js} +1 -1
  110. package/packages/web/dist/assets/{index-B6G18FqB.js → index-fK8FIZgP.js} +15 -14
  111. package/packages/web/dist/assets/{index-BcnkUk2o.js → index-gmiZeFXN.js} +1 -1
  112. package/packages/web/dist/assets/{index-D6Ky9vJe.js → index-irD539ZM.js} +1 -1
  113. package/packages/web/dist/assets/{index-uB6nhSvz.js → index-yq-E1Y00.js} +1 -1
  114. package/packages/web/dist/assets/{projects-BUiOGmmb.js → projects-DXYQNJIi.js} +1 -1
  115. package/packages/web/dist/assets/{providers-Bh1ZiiJi.js → providers-1bnH-exJ.js} +1 -1
  116. package/packages/web/dist/assets/sessions-6zGUlFrt.js +1 -0
  117. package/packages/web/dist/assets/{settings-Z4AVVmkJ.js → settings-MbfRir0d.js} +1 -1
  118. package/packages/web/dist/index.html +2 -2
  119. package/packages/server/src/api/sessions-notes.js +0 -51
  120. package/packages/server/src/db/SessionNoteRepository.js +0 -60
  121. package/packages/server/src/db/migrations/canvasItemsMigrations.js +0 -109
  122. package/packages/server/src/db/migrations/conversationsMigrations.js +0 -187
  123. package/packages/server/src/db/migrations/kanbanMigrations.js +0 -99
  124. package/packages/server/src/db/migrations/miscMigrations.js +0 -369
  125. package/packages/server/src/db/migrations/projectsMigrations.js +0 -99
  126. package/packages/server/src/db/migrations/sessionsMigrations.js +0 -287
  127. package/packages/web/dist/assets/ApiClient-B4YTtyY4.js +0 -1
  128. package/packages/web/dist/assets/ArchiveConfirmModal-BQ-4gI0R.css +0 -1
  129. package/packages/web/dist/assets/ArchiveConfirmModal-OFaj_uX5.js +0 -1
  130. package/packages/web/dist/assets/EffortLevelSelector-C2378L8e.js +0 -1
  131. package/packages/web/dist/assets/InputWithButton-Ci15ox0a.js +0 -1
  132. package/packages/web/dist/assets/MarkdownEditor-5-bexzUT.js +0 -2
  133. package/packages/web/dist/assets/NewSessionView-BCqtIgWH.js +0 -3
  134. package/packages/web/dist/assets/ProjectEditView-RFaxHhAX.js +0 -1
  135. package/packages/web/dist/assets/ProvidersView-DDKMIQWZ.js +0 -1
  136. package/packages/web/dist/assets/ProvidersView-DE82G_5W.css +0 -1
  137. package/packages/web/dist/assets/QuickResponseSettings-B8188A1D.css +0 -1
  138. package/packages/web/dist/assets/SessionDetailView-CL7nmfiB.js +0 -36
  139. package/packages/web/dist/assets/SessionDetailView-CupIkI7u.css +0 -1
  140. package/packages/web/dist/assets/SessionListView-BcxGz4aC.js +0 -1
  141. package/packages/web/dist/assets/SessionListView-fHlQyecX.css +0 -1
  142. package/packages/web/dist/assets/sessions-DH1R-NhV.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,97 +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
- // Match normal Claude Code CLI behavior: load user-level settings
86
- // such as configured MCP servers, then project/local overrides.
87
- settingSources: ['user', 'project', 'local'],
88
- ...(resumeSessionId && { resume: resumeSessionId }),
89
- env: sessionEnv,
90
- spawnClaudeCodeProcess: createClaudeCodeSpawner(),
91
- model: effectiveModel,
92
- systemPrompt: buildSystemPromptConfig(sessionId, session.projectId, systemPrompt, session.mode),
93
- },
94
- };
95
- }
96
-
97
- /**
98
- * Build query parameters for the Codex adapter.
99
- *
100
- * Codex in v1 is a simple Chat-Completions-shaped executor — it doesn't need
101
- * or accept Claude-specific options (permissionMode, settingSources,
102
- * includePartialMessages, spawnClaudeCodeProcess, resume).
103
- *
104
- * Codex does have its own sandboxing model, driven from {@code session.mode}
105
- * via {@link getSandboxModeForSession}. Codex CLI v0.124.0 also supports
106
- * resume via `codex resume` / `codex exec resume`, but Circus Chief v1
107
- * intentionally does NOT pass a resume token — wiring is deferred to a
108
- * later phase (see canvas plan §Phase 4.5).
109
- *
110
- * @returns {Object}
111
- */
112
- function buildCodexQueryParams({
113
- prompt, workingDirectory, controller, session, sessionId, systemPrompt, model, sessionEnv,
114
- }) {
115
- const isVCR = Boolean(process.env.VCR_MODE);
116
- // In VCR mode, force the cheapest commonly-cassetted OpenAI model.
117
- const effectiveModel = isVCR ? 'gpt-4o-mini' : model;
118
-
119
- return {
120
- prompt,
121
- options: {
122
- cwd: workingDirectory,
123
- abortController: controller,
124
- env: sessionEnv,
125
- model: effectiveModel,
126
- effortLevel: session?.effortLevel ?? null,
127
- systemPrompt: buildSystemPromptConfig(sessionId, session.projectId, systemPrompt, session.mode),
128
- sandboxMode: getSandboxModeForSession(session?.mode),
129
- },
130
- };
131
- }
132
-
133
- /**
134
- * Build query parameters for executing a session via the configured agent.
135
- * Shared by runSession, continueSession, and continueSessionWithExistingMessage.
136
- *
137
- * @param {Object} options
138
- * @param {string} options.prompt - The prompt text to send
139
- * @param {string} options.workingDirectory - Session working directory
140
- * @param {AbortController} options.controller - Abort controller for the session
141
- * @param {Object} options.session - Session object from DB
142
- * @param {string} options.sessionId - Session ID
143
- * @param {string|null} options.systemPrompt - Custom system prompt from project settings
144
- * @param {string|null} options.model - Model to use
145
- * @param {Object} options.sessionEnv - Environment variables for the session
146
- * @param {string|null} [options.resumeSessionId] - Session ID to resume (null for new session)
147
- * @param {string} [options.agentType] - 'claude-code' (default) | 'codex'
148
- * @returns {Object} Query parameters for agent.execute()
149
- */
150
- export function buildQueryParams(options) {
151
- const { agentType = 'claude-code' } = options || {};
152
- if (agentType === 'codex') {
153
- return buildCodexQueryParams(options);
154
- }
155
- return buildClaudeCodeQueryParams(options);
156
- }
157
-
158
95
  /**
159
96
  * Execute the agent stream loop and handle post-turn completion, errors, and cleanup.
160
97
  * This is the shared core of runSession, continueSession, and continueSessionWithExistingMessage.
@@ -260,7 +197,12 @@ function buildContinueModelAndEnv(session, sessionId, model) {
260
197
 
261
198
  // Derive provider from the effective model ID (returns null for Anthropic/SDK defaults)
262
199
  const provider = resolveProviderFromModel(effectiveModel);
263
- 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
+ );
264
206
 
265
207
  // Check if model changed from the session's last requested model
266
208
  // When model changes, we can't resume the previous session - thinking blocks and
@@ -275,7 +217,13 @@ function buildContinueModelAndEnv(session, sessionId, model) {
275
217
  updatedSession = sessions.getById(sessionId); // refresh
276
218
  }
277
219
 
278
- return { effectiveModel, sessionEnv, modelChanged, session: updatedSession };
220
+ return {
221
+ effectiveModel,
222
+ sessionEnv,
223
+ commitAttributionOverride,
224
+ modelChanged,
225
+ session: updatedSession,
226
+ };
279
227
  }
280
228
 
281
229
  /**
@@ -286,7 +234,7 @@ function buildContinueModelAndEnv(session, sessionId, model) {
286
234
  async function buildContinueParams({
287
235
  sessionId, session, model, systemPrompt, effectiveModel, sessionEnv,
288
236
  modelChanged, activeConversation, promptWithAttachments,
289
- workingDirectory, controller, agentType, agent,
237
+ workingDirectory, controller, agentType, agent, commitAttributionOverride,
290
238
  }) {
291
239
  // Only resume if we have a session ID AND model hasn't changed AND the
292
240
  // agent supports resume.
@@ -308,6 +256,7 @@ async function buildContinueParams({
308
256
  sessionEnv,
309
257
  resumeSessionId: canResume ? activeConversation.claudeSessionId : null,
310
258
  agentType,
259
+ commitAttributionOverride,
311
260
  });
312
261
 
313
262
  // Logging metadata for agent call tracking
@@ -394,11 +343,15 @@ export async function continueSessionCore(sessionId, content, workingDirectory,
394
343
  // Resolve model/provider and detect model changes
395
344
  const modelEnv = buildContinueModelAndEnv(session, sessionId, model);
396
345
  session = modelEnv.session;
346
+ if (session.gitWorktree && modelEnv.commitAttributionOverride) {
347
+ await ensureWorktreeCommitAttributionHook(session.gitWorktree);
348
+ }
397
349
 
398
350
  // Build query params and agent call meta
399
351
  const { queryParams, agentCallMeta } = await buildContinueParams({
400
352
  sessionId, session, model, systemPrompt,
401
353
  effectiveModel: modelEnv.effectiveModel, sessionEnv: modelEnv.sessionEnv,
354
+ commitAttributionOverride: modelEnv.commitAttributionOverride,
402
355
  modelChanged: modelEnv.modelChanged, activeConversation, promptWithAttachments,
403
356
  workingDirectory, controller, agentType, agent,
404
357
  });
@@ -459,14 +412,8 @@ export async function runSessionCore(sessionId, prompt, workingDirectory, config
459
412
  const agentType = session.agentType || 'claude-code';
460
413
  const agent = createAgentForSession(agentType);
461
414
 
462
- // Resolve the effective model: fall back to session.model as defense-in-depth
463
- // (draftSessionService already resolves the model upstream, but this ensures
464
- // correctness if called directly).
465
- const effectiveModel = model || session.model;
466
-
467
- // Derive provider from the effective model ID (returns null for Anthropic/SDK defaults)
468
- const provider = resolveProviderFromModel(effectiveModel);
469
- const sessionEnv = buildSessionEnv(provider, session.thinkingEnabled, session.effortLevel);
415
+ const { effectiveModel, sessionEnv, commitAttributionOverride } =
416
+ await resolveInitialSessionModelEnv(session, model);
470
417
 
471
418
  const queryParams = buildQueryParams({
472
419
  prompt: promptWithAttachments,
@@ -478,6 +425,7 @@ export async function runSessionCore(sessionId, prompt, workingDirectory, config
478
425
  model: effectiveModel,
479
426
  sessionEnv,
480
427
  agentType,
428
+ commitAttributionOverride,
481
429
  });
482
430
 
483
431
  // Log query params for debugging third-party provider issues