circuschief 0.8.0 → 1.1.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 (155) hide show
  1. package/package.json +1 -1
  2. package/packages/server/src/agents/AgentGateway.js +2 -0
  3. package/packages/server/src/agents/adapters/GeminiAdapter.js +105 -0
  4. package/packages/server/src/agents/adapters/cliUtils.js +15 -0
  5. package/packages/server/src/agents/adapters/codexCliRunner.js +1 -8
  6. package/packages/server/src/agents/adapters/geminiCliRunner.js +183 -0
  7. package/packages/server/src/agents/adapters/geminiEventMapper.js +195 -0
  8. package/packages/server/src/api/commandButtons.js +16 -15
  9. package/packages/server/src/api/projects-commandButtons.js +6 -6
  10. package/packages/server/src/api/projects-session-create.js +109 -0
  11. package/packages/server/src/api/projects-session-defaults.js +51 -0
  12. package/packages/server/src/api/projects-session-helpers.js +47 -1
  13. package/packages/server/src/api/projects-templates.js +38 -0
  14. package/packages/server/src/api/projects.js +28 -180
  15. package/packages/server/src/api/sessions-commands.js +21 -18
  16. package/packages/server/src/api/sessions-patch.js +41 -1
  17. package/packages/server/src/db/ProviderRepository.js +4 -2
  18. package/packages/server/src/db/SessionRepository.js +1 -1
  19. package/packages/server/src/db/SessionTemplateRepository.js +23 -2
  20. package/packages/server/src/db/migrations/canvasItemsMigrations.js +109 -0
  21. package/packages/server/src/db/migrations/conversationsMigrations.js +187 -0
  22. package/packages/server/src/db/migrations/index.js +234 -6
  23. package/packages/server/src/db/migrations/kanbanMigrations.js +99 -0
  24. package/packages/server/src/db/migrations/miscMigrations.js +244 -0
  25. package/packages/server/src/db/migrations/projectsMigrations.js +130 -0
  26. package/packages/server/src/db/migrations/providerCommitAttributionMigrations.js +30 -0
  27. package/packages/server/src/db/migrations/providerMigrations.js +250 -0
  28. package/packages/server/src/db/migrations/sessionTableRecreate.js +136 -0
  29. package/packages/server/src/db/migrations/sessionsMigrations.js +300 -0
  30. package/packages/server/src/db/seedBaselineData.js +23 -1
  31. package/packages/server/src/db/session-helpers.js +26 -1
  32. package/packages/server/src/schema.sql +5 -1
  33. package/packages/server/src/services/commandButtonPrompts.js +9 -7
  34. package/packages/server/src/services/e2eSpawnCapture.js +47 -6
  35. package/packages/server/src/services/geminiSpawnHelper.js +47 -0
  36. package/packages/server/src/services/gitCommitAttribution.js +38 -8
  37. package/packages/server/src/services/gitDiff.js +107 -0
  38. package/packages/server/src/services/gitRepoUrl.js +174 -0
  39. package/packages/server/src/services/gitService.js +43 -311
  40. package/packages/server/src/services/gitWorktree.js +127 -0
  41. package/packages/server/src/services/providerTestService.js +59 -1
  42. package/packages/server/src/services/queryParamBuilder.js +33 -1
  43. package/packages/server/src/services/sessionExecution.js +4 -0
  44. package/packages/server/src/services/sessionPrompts.js +23 -1
  45. package/packages/server/src/services/sessionProvider.js +41 -1
  46. package/packages/shared/src/constants.js +1 -1
  47. package/packages/shared/src/contracts/providers.js +1 -1
  48. package/packages/shared/src/contracts/sessions.js +27 -1
  49. package/packages/shared/src/contracts/templates.js +10 -0
  50. package/packages/shared/src/types.js +7 -0
  51. package/packages/web/dist/assets/{ActiveSessionsView-B0XHqLmv.js → ActiveSessionsView-EdNxmPmZ.js} +1 -1
  52. package/packages/web/dist/assets/{AgentLogsView-DmsjUMlB.js → AgentLogsView-C2wX0JPP.js} +2 -2
  53. package/packages/web/dist/assets/ApiClient-DfbJwzpz.js +1 -0
  54. package/packages/web/dist/assets/ArchiveConfirmModal-DJERn5XO.js +1 -0
  55. package/packages/web/dist/assets/CommandButtonDetailView-CBPI8-US.js +1 -0
  56. package/packages/web/dist/assets/CommandButtonDetailView-D9zjx9ME.css +1 -0
  57. package/packages/web/dist/assets/EffortLevelSelector-PaBpUveC.js +1 -0
  58. package/packages/web/dist/assets/{GeneralSettingsView-D1nI8_zk.js → GeneralSettingsView-Dw-x83R0.js} +1 -1
  59. package/packages/web/dist/assets/{InputWithButton-CAkttyqx.js → InputWithButton-CHHcpF4I.js} +1 -1
  60. package/packages/web/dist/assets/{InterpolationHelp-BO1j9Z3_.js → InterpolationHelp-CLNPz8s8.js} +1 -1
  61. package/packages/web/dist/assets/MarkdownEditor-DYi1igfT.js +2 -0
  62. package/packages/web/dist/assets/ModelSelector-Cko_yTO5.js +1 -0
  63. package/packages/web/dist/assets/{ModelSelector-BSxKUSus.css → ModelSelector-Dtwe5xLH.css} +1 -1
  64. package/packages/web/dist/assets/{NewSessionView-BDPb-1qr.css → NewSessionView-DBl7T2Xp.css} +1 -1
  65. package/packages/web/dist/assets/NewSessionView-DwUfBg70.js +3 -0
  66. package/packages/web/dist/assets/ProjectEditView-CSbsea3U.js +1 -0
  67. package/packages/web/dist/assets/ProjectEditView-DbqTbA0q.css +1 -0
  68. package/packages/web/dist/assets/{ProjectListView-DcNyuINs.js → ProjectListView-CEc_LWZL.js} +1 -1
  69. package/packages/web/dist/assets/{ProjectNewView-B5YV62hv.js → ProjectNewView-D4U0uRlp.js} +1 -1
  70. package/packages/web/dist/assets/ProvidersView-2KCOiY6Q.css +1 -0
  71. package/packages/web/dist/assets/ProvidersView-CD1j8BOv.js +1 -0
  72. package/packages/web/dist/assets/QuickResponsesPanel-Dp39f12o.js +1 -0
  73. package/packages/web/dist/assets/QuickResponsesPanel-dk-Rj8xx.css +1 -0
  74. package/packages/web/dist/assets/ResizableTextarea-BWywIqOv.js +1 -0
  75. package/packages/web/dist/assets/ResizableTextarea-DERSH3Wz.css +1 -0
  76. package/packages/web/dist/assets/SessionCard-B6d5ijDW.js +1 -0
  77. package/packages/web/dist/assets/SessionDetailView-DWbXdx7A.js +36 -0
  78. package/packages/web/dist/assets/SessionDetailView-ULeIkWS0.css +1 -0
  79. package/packages/web/dist/assets/{SessionFormOptions-B6AxyREh.js → SessionFormOptions-Dz9ik4Fo.js} +1 -1
  80. package/packages/web/dist/assets/{SessionListView-B5_6gW49.css → SessionListView-3-xx6EVs.css} +1 -1
  81. package/packages/web/dist/assets/SessionListView-C129buBe.js +1 -0
  82. package/packages/web/dist/assets/{SessionLogStream-LlZ3z_Xj.js → SessionLogStream-BvXUNNBZ.js} +6 -6
  83. package/packages/web/dist/assets/{SettingsView-CTGiGvR2.js → SettingsView-DW1NvpX_.js} +1 -1
  84. package/packages/web/dist/assets/SlashCommandWizard-DleYBxrE.js +1 -0
  85. package/packages/web/dist/assets/{SummarySettingsView-BR2ZjEa3.js → SummarySettingsView-CLUfcWvf.js} +1 -1
  86. package/packages/web/dist/assets/TemplateDetailView-B5NI2oTR.css +1 -0
  87. package/packages/web/dist/assets/TemplateDetailView-Cukb205e.js +1 -0
  88. package/packages/web/dist/assets/{commandButtons-BfqR-fqq.js → commandButtons-DejH0rVN.js} +1 -1
  89. package/packages/web/dist/assets/index-BD7Y3rBE.js +3 -0
  90. package/packages/web/dist/assets/{index-BY174HVJ.css → index-Bd20AzX1.css} +1 -1
  91. package/packages/web/dist/assets/index-BgJiarKe.js +1 -0
  92. package/packages/web/dist/assets/index-Bk32fSSG.js +1 -0
  93. package/packages/web/dist/assets/index-BkA6pF2Z.js +1 -0
  94. package/packages/web/dist/assets/index-Cltr-Ldt.js +7 -0
  95. package/packages/web/dist/assets/index-Co-46Tp3.js +1 -0
  96. package/packages/web/dist/assets/index-Cpykk857.js +1 -0
  97. package/packages/web/dist/assets/index-CtABl0D1.js +1 -0
  98. package/packages/web/dist/assets/index-Cuqk5m9S.js +1 -0
  99. package/packages/web/dist/assets/{index-fK8FIZgP.js → index-CvXApbVC.js} +15 -15
  100. package/packages/web/dist/assets/index-D2gN-xEH.js +1 -0
  101. package/packages/web/dist/assets/index-Dd3WpmyQ.js +1 -0
  102. package/packages/web/dist/assets/index-Dk6--9rj.js +1 -0
  103. package/packages/web/dist/assets/{index-DgkC10TW.js → index-MZf7MlPX.js} +3 -3
  104. package/packages/web/dist/assets/{index-DtfUt785.js → index-NShCcwfj.js} +1 -1
  105. package/packages/web/dist/assets/index-hA3VEuSq.js +1 -0
  106. package/packages/web/dist/assets/index-p0mp3nca.js +1 -0
  107. package/packages/web/dist/assets/index-qntNa5r_.js +1 -0
  108. package/packages/web/dist/assets/index-qq9ceNSK.js +1 -0
  109. package/packages/web/dist/assets/projectDefaults-D9xkp2XR.js +1 -0
  110. package/packages/web/dist/assets/{projects-DXYQNJIi.js → projects-BvLADGKx.js} +1 -1
  111. package/packages/web/dist/assets/{providers-1bnH-exJ.js → providers-DZ-fOa4G.js} +1 -1
  112. package/packages/web/dist/assets/{sessions-6zGUlFrt.js → sessions-DETEyjPI.js} +1 -1
  113. package/packages/web/dist/assets/{settings-MbfRir0d.js → settings-TWfbahn5.js} +1 -1
  114. package/packages/web/dist/index.html +2 -2
  115. package/packages/web/dist/assets/ApiClient-C3ztI9s9.js +0 -1
  116. package/packages/web/dist/assets/ArchiveConfirmModal-BlCyn5Vt.js +0 -1
  117. package/packages/web/dist/assets/CommandButtonDetailView-CdSCPp78.js +0 -1
  118. package/packages/web/dist/assets/CommandButtonDetailView-DBm3rzhw.css +0 -1
  119. package/packages/web/dist/assets/EffortLevelSelector-hc2MNKg6.js +0 -1
  120. package/packages/web/dist/assets/MarkdownEditor-ucRAP_UM.js +0 -2
  121. package/packages/web/dist/assets/ModelSelector-CwTz8ZWO.js +0 -1
  122. package/packages/web/dist/assets/NewSessionView-BsDrp8mj.js +0 -3
  123. package/packages/web/dist/assets/ProjectEditView-CwTOeSun.js +0 -1
  124. package/packages/web/dist/assets/ProjectEditView-J15mcsWz.css +0 -1
  125. package/packages/web/dist/assets/ProvidersView-bZemq_Rv.css +0 -1
  126. package/packages/web/dist/assets/ProvidersView-nY9GnDdO.js +0 -1
  127. package/packages/web/dist/assets/QuickResponseSettings-B352c75l.css +0 -1
  128. package/packages/web/dist/assets/QuickResponseSettings-BQwQXuL7.js +0 -1
  129. package/packages/web/dist/assets/QuickResponsesPanel-BlFDvnZ2.css +0 -1
  130. package/packages/web/dist/assets/QuickResponsesPanel-BzSYcCSP.js +0 -1
  131. package/packages/web/dist/assets/ResizableTextarea-B3YIdIXv.js +0 -1
  132. package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +0 -1
  133. package/packages/web/dist/assets/SessionCard-CjE1tXiT.js +0 -1
  134. package/packages/web/dist/assets/SessionDetailView-3cPZrbS3.js +0 -36
  135. package/packages/web/dist/assets/SessionDetailView-CZRZMrfM.css +0 -1
  136. package/packages/web/dist/assets/SessionListView-CLXBfLcq.js +0 -1
  137. package/packages/web/dist/assets/SlashCommandWizard-Cy04d7-o.js +0 -1
  138. package/packages/web/dist/assets/TemplateDetailView-DH6Oswsp.js +0 -1
  139. package/packages/web/dist/assets/TemplateDetailView-DT2m06W7.css +0 -1
  140. package/packages/web/dist/assets/index-1zziPL6l.js +0 -1
  141. package/packages/web/dist/assets/index-7kzHPxSF.js +0 -1
  142. package/packages/web/dist/assets/index-B0N_obMc.js +0 -1
  143. package/packages/web/dist/assets/index-BNk_gdfI.js +0 -1
  144. package/packages/web/dist/assets/index-CSqaAH-0.js +0 -1
  145. package/packages/web/dist/assets/index-C_q4WlK8.js +0 -1
  146. package/packages/web/dist/assets/index-D1wpU4y0.js +0 -7
  147. package/packages/web/dist/assets/index-D5zCA8sD.js +0 -1
  148. package/packages/web/dist/assets/index-DGR8ELWY.js +0 -1
  149. package/packages/web/dist/assets/index-DHga8pXo.js +0 -1
  150. package/packages/web/dist/assets/index-DSby02Wl.js +0 -1
  151. package/packages/web/dist/assets/index-DqjXJTVI.js +0 -1
  152. package/packages/web/dist/assets/index-_4S2uLDI.js +0 -1
  153. package/packages/web/dist/assets/index-gmiZeFXN.js +0 -1
  154. package/packages/web/dist/assets/index-irD539ZM.js +0 -3
  155. package/packages/web/dist/assets/index-yq-E1Y00.js +0 -1
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Migrations for the sessions table and closely related session tables.
3
+ * Each export is an array of { name, up(db) } migration objects.
4
+ */
5
+ import { addColumnIfMissing, getColumns, getTableSql } from './migrationUtils.js';
6
+ import { migrateSessionsDefaultModeAndThinking } from './sessionTableRecreate.js';
7
+
8
+ // Table name constants for migrations
9
+ const TABLE_SESSIONS = 'sessions';
10
+
11
+ // Column type constants
12
+ const COL_INTEGER_DEFAULT_0 = 'INTEGER DEFAULT 0';
13
+
14
+ /**
15
+ * SQL column definition for the sessions table with updated status CHECK constraint.
16
+ */
17
+ const SESSIONS_BASE_COLUMNS = `
18
+ id TEXT PRIMARY KEY,
19
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
20
+ name TEXT NOT NULL,
21
+ status TEXT NOT NULL DEFAULT 'starting' CHECK (status IN ('starting', 'running', 'waiting', 'stopped', 'completed', 'error', 'scheduled')),
22
+ mode TEXT NOT NULL DEFAULT 'yolo' CHECK (mode IN ('plan', 'standard', 'yolo')),
23
+ thinking_enabled INTEGER NOT NULL DEFAULT 1,
24
+ git_branch TEXT,
25
+ git_worktree TEXT,
26
+ pr_url TEXT,
27
+ pr_url_auto_link_disabled INTEGER NOT NULL DEFAULT 0,
28
+ error TEXT,
29
+ effort_level TEXT CHECK(effort_level IN ('low', 'medium', 'high', 'max', 'auto')),
30
+ cost_usd REAL DEFAULT 0,
31
+ claude_session_id TEXT,
32
+ model TEXT,
33
+ next_template_id TEXT REFERENCES session_templates(id) ON DELETE SET NULL,
34
+ parent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
35
+ input_tokens INTEGER DEFAULT 0,
36
+ output_tokens INTEGER DEFAULT 0,
37
+ thinking_tokens INTEGER DEFAULT 0,
38
+ cache_read_input_tokens INTEGER DEFAULT 0,
39
+ cache_creation_input_tokens INTEGER DEFAULT 0,
40
+ web_search_requests INTEGER DEFAULT 0,
41
+ context_window INTEGER DEFAULT 200000,
42
+ archived INTEGER NOT NULL DEFAULT 0,
43
+ starred INTEGER NOT NULL DEFAULT 0,
44
+ manually_named INTEGER NOT NULL DEFAULT 0,
45
+ scheduled_at INTEGER DEFAULT NULL,
46
+ reschedule_delay_minutes INTEGER DEFAULT 60, -- keep in sync with DEFAULT_RESCHEDULE_DELAY_MINUTES in shared/constants.js
47
+ auto_reschedule_enabled INTEGER DEFAULT 0,
48
+ reschedule_on_token_limit INTEGER DEFAULT 1,
49
+ reschedule_on_service_error INTEGER DEFAULT 1,
50
+ max_reschedule_count INTEGER DEFAULT NULL,
51
+ max_total_tokens INTEGER DEFAULT NULL,
52
+ reschedule_count INTEGER DEFAULT 0,
53
+ reschedule_at_token_count INTEGER DEFAULT NULL,
54
+ created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
55
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
56
+ `;
57
+
58
+ /**
59
+ * All possible column names that may exist in the sessions table for migration SELECT.
60
+ */
61
+ const SESSIONS_ALL_COLUMNS = [
62
+ 'id', 'project_id', 'name', 'status', 'mode', 'thinking_enabled',
63
+ 'git_branch', 'git_worktree', 'pr_url', 'pr_url_auto_link_disabled', 'error', 'effort_level',
64
+ 'cost_usd', 'claude_session_id', 'model', 'next_template_id',
65
+ 'parent_session_id', 'input_tokens', 'output_tokens', 'thinking_tokens',
66
+ 'cache_read_input_tokens', 'cache_creation_input_tokens',
67
+ 'web_search_requests', 'context_window', 'archived', 'starred',
68
+ 'manually_named', 'scheduled_at', 'reschedule_delay_minutes',
69
+ 'auto_reschedule_enabled', 'reschedule_on_token_limit',
70
+ 'reschedule_on_service_error', 'max_reschedule_count',
71
+ 'max_total_tokens', 'reschedule_count', 'reschedule_at_token_count',
72
+ 'created_at', 'updated_at',
73
+ ];
74
+
75
+ /**
76
+ * Migrate sessions table to include 'stopped' and 'scheduled' in status CHECK constraint.
77
+ * SQLite doesn't support ALTER TABLE to modify constraints, so we recreate the table.
78
+ */
79
+ function migrateSessionsStatusConstraint(db) {
80
+ const tableSql = getTableSql(db, TABLE_SESSIONS);
81
+
82
+ // If schema already includes 'scheduled', no migration needed
83
+ if (tableSql?.includes("'scheduled'")) {
84
+ return;
85
+ }
86
+
87
+ const columnNames = getColumns(db, TABLE_SESSIONS);
88
+ const selectColumns = SESSIONS_ALL_COLUMNS
89
+ .filter((col) => columnNames.includes(col))
90
+ .join(', ');
91
+
92
+ db.exec(`
93
+ CREATE TABLE sessions_new (${SESSIONS_BASE_COLUMNS});
94
+ INSERT INTO sessions_new (${selectColumns})
95
+ SELECT ${selectColumns} FROM sessions;
96
+ DROP TABLE sessions;
97
+ ALTER TABLE sessions_new RENAME TO sessions;
98
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
99
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
100
+ CREATE INDEX IF NOT EXISTS idx_sessions_archived ON sessions(archived);
101
+ CREATE INDEX IF NOT EXISTS idx_sessions_starred ON sessions(archived, starred);
102
+ CREATE INDEX IF NOT EXISTS idx_sessions_next_template ON sessions(next_template_id);
103
+ CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id);
104
+ CREATE INDEX IF NOT EXISTS idx_sessions_scheduled ON sessions(scheduled_at) WHERE scheduled_at IS NOT NULL;
105
+ `);
106
+ }
107
+
108
+ /** @type {Array<{name: string, up: (db: import('better-sqlite3').Database) => void}>} */
109
+ export const sessionsMigrations = [
110
+ // --- Initial sessions columns ---
111
+ {
112
+ name: 'sessions-add-cost_usd',
113
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'cost_usd', 'REAL DEFAULT 0'); },
114
+ },
115
+ {
116
+ name: 'sessions-add-claude_session_id',
117
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'claude_session_id', 'TEXT'); },
118
+ },
119
+ {
120
+ name: 'sessions-add-model',
121
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'model', 'TEXT'); },
122
+ },
123
+ {
124
+ name: 'sessions-add-provider_id-early',
125
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'provider_id', 'TEXT'); },
126
+ },
127
+ {
128
+ name: 'sessions-add-effort_level',
129
+ up(db) {
130
+ addColumnIfMissing(
131
+ db, TABLE_SESSIONS, 'effort_level',
132
+ "TEXT CHECK(effort_level IN ('low', 'medium', 'high', 'max', 'auto'))"
133
+ );
134
+ },
135
+ },
136
+
137
+ // --- Scheduling columns ---
138
+ {
139
+ name: 'sessions-add-scheduled_at',
140
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'scheduled_at', 'INTEGER DEFAULT NULL'); },
141
+ },
142
+ {
143
+ name: 'sessions-add-reschedule_delay_minutes',
144
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'reschedule_delay_minutes', 'INTEGER DEFAULT 60'); /* keep in sync with DEFAULT_RESCHEDULE_DELAY_MINUTES */ },
145
+ },
146
+ {
147
+ name: 'sessions-add-auto_reschedule_enabled',
148
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'auto_reschedule_enabled', COL_INTEGER_DEFAULT_0); },
149
+ },
150
+ {
151
+ name: 'sessions-add-reschedule_on_token_limit',
152
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'reschedule_on_token_limit', 'INTEGER DEFAULT 1'); },
153
+ },
154
+ {
155
+ name: 'sessions-add-reschedule_on_service_error',
156
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'reschedule_on_service_error', 'INTEGER DEFAULT 1'); },
157
+ },
158
+ {
159
+ name: 'sessions-add-max_reschedule_count',
160
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'max_reschedule_count', 'INTEGER DEFAULT NULL'); },
161
+ },
162
+ {
163
+ name: 'sessions-add-max_total_tokens',
164
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'max_total_tokens', 'INTEGER DEFAULT NULL'); },
165
+ },
166
+ {
167
+ name: 'sessions-add-reschedule_count',
168
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'reschedule_count', COL_INTEGER_DEFAULT_0); },
169
+ },
170
+ {
171
+ name: 'sessions-add-reschedule_at_token_count',
172
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'reschedule_at_token_count', 'INTEGER DEFAULT NULL'); },
173
+ },
174
+
175
+ // --- Status constraint migration (table recreation) ---
176
+ {
177
+ name: 'sessions-migrate-status-constraint',
178
+ up(db) { migrateSessionsStatusConstraint(db); },
179
+ },
180
+
181
+ // --- Template chaining ---
182
+ {
183
+ name: 'sessions-add-next_template_id',
184
+ up(db) {
185
+ addColumnIfMissing(
186
+ db, TABLE_SESSIONS, 'next_template_id',
187
+ 'TEXT REFERENCES session_templates(id) ON DELETE SET NULL'
188
+ );
189
+ },
190
+ },
191
+ {
192
+ name: 'sessions-add-parent_session_id',
193
+ up(db) {
194
+ addColumnIfMissing(
195
+ db, TABLE_SESSIONS, 'parent_session_id',
196
+ 'TEXT REFERENCES sessions(id) ON DELETE SET NULL'
197
+ );
198
+ },
199
+ },
200
+ {
201
+ name: 'sessions-template-chaining-indexes',
202
+ up(db) {
203
+ db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_next_template ON sessions(next_template_id)');
204
+ db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id)');
205
+ },
206
+ },
207
+
208
+ // --- Token usage columns ---
209
+ {
210
+ name: 'sessions-add-input_tokens',
211
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'input_tokens', COL_INTEGER_DEFAULT_0); },
212
+ },
213
+ {
214
+ name: 'sessions-add-output_tokens',
215
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'output_tokens', COL_INTEGER_DEFAULT_0); },
216
+ },
217
+ {
218
+ name: 'sessions-add-thinking_tokens',
219
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'thinking_tokens', COL_INTEGER_DEFAULT_0); },
220
+ },
221
+ {
222
+ name: 'sessions-add-cache_read_input_tokens',
223
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'cache_read_input_tokens', COL_INTEGER_DEFAULT_0); },
224
+ },
225
+ {
226
+ name: 'sessions-add-cache_creation_input_tokens',
227
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'cache_creation_input_tokens', COL_INTEGER_DEFAULT_0); },
228
+ },
229
+ {
230
+ name: 'sessions-add-web_search_requests',
231
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'web_search_requests', COL_INTEGER_DEFAULT_0); },
232
+ },
233
+ {
234
+ name: 'sessions-add-context_window',
235
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'context_window', 'INTEGER DEFAULT 200000'); },
236
+ },
237
+
238
+ // --- Archived / starred / manually_named ---
239
+ {
240
+ name: 'sessions-add-archived',
241
+ up(db) {
242
+ addColumnIfMissing(db, TABLE_SESSIONS, 'archived', 'INTEGER NOT NULL DEFAULT 0');
243
+ db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_archived ON sessions(archived)');
244
+ },
245
+ },
246
+ {
247
+ name: 'sessions-add-starred',
248
+ up(db) {
249
+ addColumnIfMissing(db, TABLE_SESSIONS, 'starred', 'INTEGER NOT NULL DEFAULT 0');
250
+ db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_starred ON sessions(archived, starred)');
251
+ },
252
+ },
253
+ {
254
+ name: 'sessions-add-manually_named',
255
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'manually_named', 'INTEGER NOT NULL DEFAULT 0'); },
256
+ },
257
+ {
258
+ name: 'sessions-add-pr_url_auto_link_disabled',
259
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'pr_url_auto_link_disabled', 'INTEGER NOT NULL DEFAULT 0'); },
260
+ },
261
+
262
+ // --- Pending prompt / slash commands / pending model / auto send ---
263
+ {
264
+ name: 'sessions-add-pending_prompt',
265
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'pending_prompt', 'TEXT'); },
266
+ },
267
+ {
268
+ name: 'sessions-add-slash_commands',
269
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'slash_commands', 'TEXT'); },
270
+ },
271
+ {
272
+ name: 'sessions-add-pending_model',
273
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'pending_model', 'TEXT'); },
274
+ },
275
+ {
276
+ name: 'sessions-add-auto_send_pending_prompt',
277
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'auto_send_pending_prompt', COL_INTEGER_DEFAULT_0); },
278
+ },
279
+
280
+ // --- Provider ID (from providers table, added later in sequence) ---
281
+ {
282
+ name: 'sessions-add-provider_id-from-providers',
283
+ up(db) {
284
+ addColumnIfMissing(db, TABLE_SESSIONS, 'provider_id', 'TEXT REFERENCES providers(id)');
285
+ },
286
+ },
287
+
288
+ // --- Agent type ---
289
+ {
290
+ name: 'sessions-add-agent_type',
291
+ up(db) { addColumnIfMissing(db, TABLE_SESSIONS, 'agent_type', "TEXT DEFAULT 'claude-code'"); },
292
+ },
293
+
294
+ // --- Default mode / thinking changes ---
295
+ {
296
+ name: 'sessions-migrate-default-mode-thinking',
297
+ up(db) { migrateSessionsDefaultModeAndThinking(db); },
298
+ },
299
+
300
+ ];
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { OPENAI_MODELS } from '../../../shared/src/index.js';
2
+ import { OPENAI_MODELS, GEMINI_MODELS } from '../../../shared/src/index.js';
3
3
 
4
4
  export const BUILT_IN_ANTHROPIC_PROVIDER = {
5
5
  id: 'anthropic-default',
@@ -13,6 +13,12 @@ export const BUILT_IN_OPENAI_PROVIDER = {
13
13
  kind: 'openai',
14
14
  };
15
15
 
16
+ export const BUILT_IN_GOOGLE_PROVIDER = {
17
+ id: 'google-default',
18
+ name: 'Google (Official)',
19
+ kind: 'google',
20
+ };
21
+
16
22
  export const BUILT_IN_ANTHROPIC_MODELS = [
17
23
  { id: 'anthropic-haiku', providerId: BUILT_IN_ANTHROPIC_PROVIDER.id, modelId: 'claude-haiku-4-5-20251001', displayName: 'Haiku 4.5', description: 'Fast & lightweight', tier: 'haiku' },
18
24
  { id: 'anthropic-sonnet', providerId: BUILT_IN_ANTHROPIC_PROVIDER.id, modelId: 'claude-sonnet-4-6', displayName: 'Sonnet 4.6', description: 'Balanced', tier: 'sonnet' },
@@ -29,6 +35,15 @@ export const BUILT_IN_OPENAI_MODELS = OPENAI_MODELS.map((model) => ({
29
35
  tier: 'custom',
30
36
  }));
31
37
 
38
+ export const BUILT_IN_GOOGLE_MODELS = GEMINI_MODELS.map((model) => ({
39
+ id: model.seedId,
40
+ providerId: BUILT_IN_GOOGLE_PROVIDER.id,
41
+ modelId: model.id,
42
+ displayName: model.name,
43
+ description: model.description,
44
+ tier: 'custom',
45
+ }));
46
+
32
47
  export const DEFAULT_QUICK_RESPONSES = [
33
48
  { label: 'Put a plan on the canvas', content: 'Put a plan on the canvas to get this done', autoSubmit: false, sortOrder: 0 },
34
49
  { label: 'Yes', content: 'Yes', autoSubmit: true, sortOrder: 1 },
@@ -87,6 +102,13 @@ function seedBuiltInProviders(db) {
87
102
  VALUES (?, ?, ?, ?, ?, ?, ?)`
88
103
  );
89
104
 
105
+ // Note: Google provider and models are NOT seeded here because seedBaselineData
106
+ // runs before migrations. On existing databases the providers table still has
107
+ // CHECK(kind IN ('anthropic','openai')), so an INSERT with kind='google' would
108
+ // be silently ignored by INSERT OR IGNORE, and the subsequent model inserts
109
+ // would fail with a FOREIGN KEY constraint. The 'providers-seed-built-in-google'
110
+ // migration handles seeding Google for both fresh and existing databases after
111
+ // the 'providers-widen-kind-check-google' migration has widened the CHECK constraint.
90
112
  for (const model of [...BUILT_IN_ANTHROPIC_MODELS, ...BUILT_IN_OPENAI_MODELS]) {
91
113
  insertModel.run(model.id, model.providerId, model.modelId, model.displayName, model.description, model.tier, now);
92
114
  }
@@ -7,7 +7,32 @@ import { DEFAULT_RESCHEDULE_DELAY_MINUTES } from '../../../shared/src/index.js';
7
7
 
8
8
  /** Reusable SQL fragment for computed activity fields on sessions */
9
9
  export const ACTIVITY_FIELDS_SQL = `
10
- (SELECT MAX(cm.timestamp) FROM conversation_messages cm WHERE cm.session_id = s.id) AS last_activity_at,
10
+ (
11
+ SELECT MAX(activity_at)
12
+ FROM (
13
+ SELECT cm.timestamp AS activity_at
14
+ FROM conversation_messages cm
15
+ WHERE cm.session_id = s.id
16
+ UNION ALL
17
+ SELECT ss.generated_at AS activity_at
18
+ FROM session_summaries ss
19
+ WHERE ss.session_id = s.id
20
+ UNION ALL
21
+ SELECT ss.updated_at AS activity_at
22
+ FROM session_summaries ss
23
+ WHERE ss.session_id = s.id
24
+ UNION ALL
25
+ SELECT COALESCE(cr.completed_at, cr.started_at) AS activity_at
26
+ FROM command_runs cr
27
+ WHERE cr.session_id = s.id
28
+ )
29
+ WHERE activity_at IS NOT NULL
30
+ ) AS last_activity_at,
31
+ (
32
+ SELECT MAX(cm.timestamp)
33
+ FROM conversation_messages cm
34
+ WHERE cm.session_id = s.id
35
+ ) AS last_message_at,
11
36
  (CAST(
12
37
  CASE
13
38
  WHEN (SELECT MAX(cm2.timestamp) FROM conversation_messages cm2 WHERE cm2.session_id = s.id) IS NOT NULL
@@ -26,6 +26,10 @@ CREATE TABLE IF NOT EXISTS session_templates (
26
26
  mode TEXT DEFAULT 'yolo' CHECK(mode IN ('plan', 'standard', 'yolo')),
27
27
  effort_level TEXT CHECK(effort_level IN ('low', 'medium', 'high', 'max', 'auto')),
28
28
  target_lane_id TEXT REFERENCES kanban_lanes(id) ON DELETE SET NULL,
29
+ show_in_quick_responses INTEGER NOT NULL DEFAULT 0,
30
+ quick_response_auto_submit INTEGER NOT NULL DEFAULT 0,
31
+ quick_response_sort_order INTEGER NOT NULL DEFAULT 0,
32
+ legacy_quick_response_id TEXT UNIQUE,
29
33
  created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
30
34
  updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
31
35
  );
@@ -39,7 +43,7 @@ CREATE TABLE IF NOT EXISTS providers (
39
43
  additional_env_vars TEXT,
40
44
  commit_attribution_override TEXT,
41
45
  is_built_in INTEGER NOT NULL DEFAULT 0,
42
- kind TEXT NOT NULL DEFAULT 'anthropic' CHECK(kind IN ('anthropic','openai')),
46
+ kind TEXT NOT NULL DEFAULT 'anthropic' CHECK(kind IN ('anthropic','openai','google')),
43
47
  created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
44
48
  updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
45
49
  );
@@ -13,36 +13,38 @@ export function buildCommandButtonApiInstructions(apiUrl, sessionId, projectId)
13
13
  return '';
14
14
  }
15
15
 
16
- return `## Commands API
16
+ return `## Circus Commands
17
17
 
18
- This project has commands configured - reusable shell commands you can execute. Use the Bash tool to run these curl commands.
18
+ This project has Circus Commands configured - reusable shell commands you can execute. Use the Bash tool to run these curl commands.
19
+
20
+ > When the user asks to "run a command", "what commands are available", "list circus commands", or similar, use the Commands API below to discover and execute them.
19
21
 
20
22
  ### List Available Commands
21
23
  \`\`\`bash
22
- curl ${apiUrl}/api/sessions/${sessionId}/command-buttons
24
+ curl ${apiUrl}/api/sessions/${sessionId}/circus-commands
23
25
  \`\`\`
24
26
 
25
27
  ### Run a Command
26
28
  \`\`\`bash
27
- curl -X POST ${apiUrl}/api/sessions/${sessionId}/command-buttons/<button_id>/run
29
+ curl -X POST ${apiUrl}/api/sessions/${sessionId}/circus-commands/<button_id>/run
28
30
  \`\`\`
29
31
 
30
32
  Response: { runId, buttonId, status: "running", output: "" }
31
33
 
32
34
  ### Check Run Status & Output
33
35
  \`\`\`bash
34
- curl ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs/<run_id>
36
+ curl ${apiUrl}/api/sessions/${sessionId}/circus-commands/runs/<run_id>
35
37
  \`\`\`
36
38
 
37
39
  Response: { runId, buttonId, status, exitCode, output, startedAt, completedAt }
38
40
 
39
41
  ### List Command Runs
40
42
  \`\`\`bash
41
- curl ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs
43
+ curl ${apiUrl}/api/sessions/${sessionId}/circus-commands/runs
42
44
  \`\`\`
43
45
 
44
46
  ### Kill a Running Command
45
47
  \`\`\`bash
46
- curl -X POST ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs/<run_id>/kill
48
+ curl -X POST ${apiUrl}/api/sessions/${sessionId}/circus-commands/runs/<run_id>/kill
47
49
  \`\`\``;
48
50
  }
@@ -49,9 +49,12 @@ export function createCapturedSpawnProcess(agentType) {
49
49
  });
50
50
  };
51
51
 
52
- if (agentType === 'claude-code') {
52
+ if (agentType === 'claude-code' || agentType === 'gemini') {
53
+ // Claude Code and Gemini don't pass prompts via stdin (they use CLI args).
54
+ // Complete after a short delay to simulate process execution.
53
55
  setTimeout(complete, 10);
54
56
  } else {
57
+ // Codex passes the prompt via stdin; complete when stdin is closed.
55
58
  stdin.once('finish', complete);
56
59
  }
57
60
 
@@ -59,12 +62,15 @@ export function createCapturedSpawnProcess(agentType) {
59
62
  }
60
63
 
61
64
  function summarizeSpawnEnv(env = {}) {
62
- if (!Object.prototype.hasOwnProperty.call(env, 'CIRCUSCHIEF_COMMIT_ATTRIBUTION')) {
63
- return {};
65
+ const summary = {};
66
+ if (Object.prototype.hasOwnProperty.call(env, 'CIRCUSCHIEF_COMMIT_ATTRIBUTION')) {
67
+ summary.CIRCUSCHIEF_COMMIT_ATTRIBUTION = env.CIRCUSCHIEF_COMMIT_ATTRIBUTION;
64
68
  }
65
- return {
66
- CIRCUSCHIEF_COMMIT_ATTRIBUTION: env.CIRCUSCHIEF_COMMIT_ATTRIBUTION,
67
- };
69
+ if (Object.prototype.hasOwnProperty.call(env, 'GEMINI_CLI_TRUST_WORKSPACE')) {
70
+ summary.GEMINI_CLI_TRUST_WORKSPACE = env.GEMINI_CLI_TRUST_WORKSPACE;
71
+ }
72
+ if (Object.keys(summary).length === 0) return {};
73
+ return summary;
68
74
  }
69
75
 
70
76
  function summarizeSpawnOptions(agentType, spawnOptions) {
@@ -78,6 +84,15 @@ function summarizeSpawnOptions(agentType, spawnOptions) {
78
84
  };
79
85
  }
80
86
 
87
+ if (agentType === 'gemini') {
88
+ return {
89
+ model: valueAfter(args, '-m'),
90
+ outputFormat: valueAfter(args, '--output-format'),
91
+ approvalMode: optionValue(args, '--approval-mode'),
92
+ prompt: valueAfter(args, '-p'),
93
+ };
94
+ }
95
+
81
96
  return {
82
97
  model: valueAfter(args, '-m'),
83
98
  sandbox: valueAfter(args, '--sandbox'),
@@ -91,6 +106,14 @@ function valueAfter(args, flag) {
91
106
  return args[index + 1] ?? null;
92
107
  }
93
108
 
109
+ function optionValue(args, flag) {
110
+ const separateValue = valueAfter(args, flag);
111
+ if (separateValue !== null) return separateValue;
112
+ const prefix = `${flag}=`;
113
+ const arg = args.find((value) => value.startsWith(prefix));
114
+ return arg ? arg.slice(prefix.length) : null;
115
+ }
116
+
94
117
  function valuesAfter(args, flag) {
95
118
  const values = [];
96
119
  for (let index = 0; index < args.length; index += 1) {
@@ -116,6 +139,24 @@ function writeCapturedAgentEvents(agentType, stdout) {
116
139
  return;
117
140
  }
118
141
 
142
+ if (agentType === 'gemini') {
143
+ // Gemini CLI stream-json format: init → message → result
144
+ writeJsonLine(stdout, {
145
+ type: 'init',
146
+ session_id: `e2e-gemini-${Date.now()}`,
147
+ });
148
+ writeJsonLine(stdout, {
149
+ type: 'message',
150
+ role: 'assistant',
151
+ content: 'E2E spawn capture response.',
152
+ });
153
+ writeJsonLine(stdout, {
154
+ type: 'result',
155
+ stats: { input_tokens: 0, output_tokens: 0 },
156
+ });
157
+ return;
158
+ }
159
+
119
160
  writeJsonLine(stdout, {
120
161
  type: 'system',
121
162
  subtype: 'init',
@@ -0,0 +1,47 @@
1
+ import { spawn } from 'child_process';
2
+ import { createRobustEnv } from './nodeSpawnHelper.js';
3
+ import {
4
+ captureSpawnAttempt,
5
+ createCapturedSpawnProcess,
6
+ isE2ESpawnCaptureEnabled,
7
+ } from './e2eSpawnCapture.js';
8
+
9
+ /**
10
+ * Create a custom spawn function for the Gemini CLI.
11
+ *
12
+ * Mirrors {@link createCodexSpawner} but for the `gemini` command.
13
+ *
14
+ * As with other CLI helpers:
15
+ * - The command 'node' is replaced with {@link process.execPath} so child
16
+ * processes use the same Node binary.
17
+ * - `createRobustEnv` guarantees the Node bin directory is on PATH.
18
+ *
19
+ * @returns {Function} Spawn function of shape (options) => childProcess
20
+ */
21
+ export function createGeminiSpawner() {
22
+ return (options) => {
23
+ const { command, args, cwd, env, signal } = options;
24
+ // Replace 'node' with the absolute path to the current Node executable
25
+ const actualCommand = command === 'node' ? process.execPath : command;
26
+
27
+ // Ensure PATH includes the directory containing Node
28
+ const robustEnv = createRobustEnv(env);
29
+
30
+ // Trust the workspace automatically in headless/automated mode.
31
+ // Without this, Gemini CLI refuses to run in untrusted directories.
32
+ robustEnv.GEMINI_CLI_TRUST_WORKSPACE = 'true';
33
+
34
+ if (isE2ESpawnCaptureEnabled()) {
35
+ captureSpawnAttempt('gemini', { ...options, env: robustEnv });
36
+ return createCapturedSpawnProcess('gemini');
37
+ }
38
+
39
+ return spawn(actualCommand, args, {
40
+ cwd,
41
+ stdio: ['pipe', 'pipe', 'pipe'],
42
+ signal,
43
+ env: robustEnv,
44
+ windowsHide: true,
45
+ });
46
+ };
47
+ }
@@ -1,10 +1,38 @@
1
1
  import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
+ import os from 'os';
3
4
  import path from 'path';
4
5
  import { chmod, mkdir, writeFile } from 'fs/promises';
5
6
 
6
7
  const execAsync = promisify(exec);
7
- const MANAGED_HOOKS_PATH = '.circuschief-hooks';
8
+
9
+ const DEFAULT_MANAGED_HOOKS_PATH = path.join(os.homedir(), '.circuschief', 'hooks');
10
+ let _managedHooksPath = DEFAULT_MANAGED_HOOKS_PATH;
11
+
12
+ /**
13
+ * Get the managed hooks directory path.
14
+ * Production code uses the real home directory; tests can override via _setManagedHooksPath().
15
+ * @returns {string}
16
+ */
17
+ export function getManagedHooksPath() {
18
+ return _managedHooksPath;
19
+ }
20
+
21
+ /**
22
+ * Override the managed hooks path (for testing only).
23
+ * Restores the default when called with no arguments.
24
+ * @param {string} [overridePath]
25
+ */
26
+ export function _setManagedHooksPath(overridePath) {
27
+ _managedHooksPath = overridePath ?? DEFAULT_MANAGED_HOOKS_PATH;
28
+ }
29
+
30
+ /**
31
+ * Legacy managed hooks path (pre-migration). Used by ensureWorktreeCommitAttributionHook()
32
+ * to recognize and auto-upgrade worktrees that still have the old relative path set.
33
+ */
34
+ const LEGACY_MANAGED_HOOKS_PATH = '.circuschief-hooks';
35
+
8
36
  const ATTRIBUTION_CONFIG_KEY = 'circuschief.commitAttribution';
9
37
  const ATTRIBUTION_ENV_KEY = 'CIRCUSCHIEF_COMMIT_ATTRIBUTION';
10
38
 
@@ -54,9 +82,10 @@ git interpret-trailers --trailer "$trailer" --in-place "$msg_file"
54
82
  }
55
83
 
56
84
  export async function clearWorktreeCommitAttribution(worktreePath) {
85
+ const managedHooksPath = getManagedHooksPath();
57
86
  const currentAttribution = await gitConfigValue(worktreePath, ATTRIBUTION_CONFIG_KEY);
58
87
  const currentHooksPath = await gitConfigValue(worktreePath, 'core.hooksPath');
59
- if (!currentAttribution && currentHooksPath !== MANAGED_HOOKS_PATH) {
88
+ if (!currentAttribution && currentHooksPath !== managedHooksPath) {
60
89
  return false;
61
90
  }
62
91
 
@@ -70,7 +99,7 @@ export async function clearWorktreeCommitAttribution(worktreePath) {
70
99
  }
71
100
  }
72
101
 
73
- if (currentHooksPath === MANAGED_HOOKS_PATH) {
102
+ if (currentHooksPath === managedHooksPath) {
74
103
  try {
75
104
  await git(worktreePath, 'config --worktree --unset core.hooksPath');
76
105
  } catch {
@@ -90,10 +119,12 @@ export async function clearWorktreeCommitAttribution(worktreePath) {
90
119
  * @returns {Promise<boolean>} True when a hook is installed or updated
91
120
  */
92
121
  export async function ensureWorktreeCommitAttributionHook(worktreePath) {
122
+ const managedHooksPath = getManagedHooksPath();
123
+
93
124
  await git(worktreePath, 'config extensions.worktreeConfig true');
94
125
 
95
126
  const currentHooksPath = await gitConfigValue(worktreePath, 'core.hooksPath');
96
- if (currentHooksPath && currentHooksPath !== MANAGED_HOOKS_PATH) {
127
+ if (currentHooksPath && currentHooksPath !== managedHooksPath && currentHooksPath !== LEGACY_MANAGED_HOOKS_PATH) {
97
128
  throw new Error(
98
129
  `Cannot install managed commit attribution hook: worktree already has core.hooksPath set to "${currentHooksPath}"`
99
130
  );
@@ -105,11 +136,10 @@ export async function ensureWorktreeCommitAttributionHook(worktreePath) {
105
136
  // Unset is idempotent for stale worktrees that never stored attribution.
106
137
  }
107
138
 
108
- await git(worktreePath, `config --worktree core.hooksPath ${shellQuote(MANAGED_HOOKS_PATH)}`);
139
+ await git(worktreePath, `config --worktree core.hooksPath ${shellQuote(managedHooksPath)}`);
109
140
 
110
- const hooksDir = path.join(worktreePath, MANAGED_HOOKS_PATH);
111
- const hookPath = path.join(hooksDir, 'commit-msg');
112
- await mkdir(hooksDir, { recursive: true });
141
+ const hookPath = path.join(managedHooksPath, 'commit-msg');
142
+ await mkdir(managedHooksPath, { recursive: true });
113
143
  await writeFile(hookPath, buildCommitMsgHook(), 'utf8');
114
144
  await chmod(hookPath, 0o755);
115
145
  return true;