circuschief 0.7.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/package.json +1 -1
  2. package/packages/server/src/agents/adapters/CodexAdapter.js +5 -4
  3. package/packages/server/src/api/canvas.js +22 -57
  4. package/packages/server/src/api/commandButtons.js +16 -15
  5. package/packages/server/src/api/index.js +2 -0
  6. package/packages/server/src/api/kanban.js +4 -2
  7. package/packages/server/src/api/projects-commandButtons.js +6 -6
  8. package/packages/server/src/api/projects-helpers.js +20 -3
  9. package/packages/server/src/api/projects-session-create.js +109 -0
  10. package/packages/server/src/api/projects-session-defaults.js +51 -0
  11. package/packages/server/src/api/projects-session-helpers.js +57 -5
  12. package/packages/server/src/api/projects-templates.js +38 -0
  13. package/packages/server/src/api/projects.js +28 -170
  14. package/packages/server/src/api/providers.js +11 -1
  15. package/packages/server/src/api/sessions-commands.js +46 -25
  16. package/packages/server/src/api/sessions-lifecycle.js +10 -10
  17. package/packages/server/src/api/sessions-patch.js +45 -1
  18. package/packages/server/src/api/sessions.js +6 -5
  19. package/packages/server/src/database.js +0 -2
  20. package/packages/server/src/db/DatabaseManager.js +5 -1
  21. package/packages/server/src/db/ProjectDefaultsRepository.js +3 -3
  22. package/packages/server/src/db/ProviderRepository.js +87 -32
  23. package/packages/server/src/db/SessionRepository.js +2 -1
  24. package/packages/server/src/db/SessionTemplateRepository.js +23 -2
  25. package/packages/server/src/db/index.js +0 -3
  26. package/packages/server/src/db/migrations/index.js +60 -7
  27. package/packages/server/src/db/migrations/kanbanMigrations.js +1 -1
  28. package/packages/server/src/db/migrations/miscMigrations.js +59 -184
  29. package/packages/server/src/db/migrations/projectsMigrations.js +31 -0
  30. package/packages/server/src/db/migrations/providerCommitAttributionMigrations.js +30 -0
  31. package/packages/server/src/db/migrations/providerMigrations.js +165 -0
  32. package/packages/server/src/db/migrations/sessionTableRecreate.js +136 -0
  33. package/packages/server/src/db/migrations/sessionsMigrations.js +18 -5
  34. package/packages/server/src/db/seedBaselineData.js +137 -0
  35. package/packages/server/src/db/session-helpers.js +32 -4
  36. package/packages/server/src/middleware/sessionLookup.js +81 -8
  37. package/packages/server/src/schema.sql +153 -132
  38. package/packages/server/src/scripts/backupDatabase.js +21 -0
  39. package/packages/server/src/scripts/dbUtils.js +81 -0
  40. package/packages/server/src/scripts/inspectDatabaseSchema.js +81 -0
  41. package/packages/server/src/scripts/validateDatabaseBaseline.js +212 -0
  42. package/packages/server/src/services/codexSpawnHelper.js +9 -0
  43. package/packages/server/src/services/commandButtonPrompts.js +14 -12
  44. package/packages/server/src/services/commandRunner.js +7 -1
  45. package/packages/server/src/services/e2eSpawnCapture.js +147 -0
  46. package/packages/server/src/services/gitCommitAttribution.js +150 -0
  47. package/packages/server/src/services/gitDiff.js +132 -0
  48. package/packages/server/src/services/gitRepoUrl.js +174 -0
  49. package/packages/server/src/services/gitService.js +48 -311
  50. package/packages/server/src/services/gitSessionSetup.js +11 -1
  51. package/packages/server/src/services/gitWorktree.js +127 -0
  52. package/packages/server/src/services/kanbanTriggers.js +6 -3
  53. package/packages/server/src/services/nodeSpawnHelper.js +9 -0
  54. package/packages/server/src/services/prUrlService.js +3 -3
  55. package/packages/server/src/services/queryParamBuilder.js +90 -0
  56. package/packages/server/src/services/sessionDuplicator.js +1 -5
  57. package/packages/server/src/services/sessionExecution.js +56 -108
  58. package/packages/server/src/services/sessionPrompts.js +12 -47
  59. package/packages/server/src/services/sessionProvider.js +10 -0
  60. package/packages/server/src/services/summaryService.js +5 -3
  61. package/packages/server/src/services/summaryStaleCheck.js +23 -4
  62. package/packages/server/src/services/templateTriggerService.js +3 -1
  63. package/packages/shared/src/constants.js +3 -0
  64. package/packages/shared/src/contracts/commandButtons.js +16 -2
  65. package/packages/shared/src/contracts/projects.js +2 -2
  66. package/packages/shared/src/contracts/providers.js +60 -0
  67. package/packages/shared/src/contracts/sessions.js +29 -2
  68. package/packages/shared/src/contracts/templates.js +12 -2
  69. package/packages/shared/src/types.js +1 -9
  70. package/packages/shared/src/utils.js +2 -2
  71. package/packages/web/dist/assets/{ActiveSessionsView-UJsCILDL.js → ActiveSessionsView-Cxh8mHmB.js} +1 -1
  72. package/packages/web/dist/assets/{AgentLogsView-BGFPLjLa.js → AgentLogsView-xdfI2bR6.js} +2 -2
  73. package/packages/web/dist/assets/ApiClient-DfbJwzpz.js +1 -0
  74. package/packages/web/dist/assets/ArchiveConfirmModal-DXZYdzHR.js +1 -0
  75. package/packages/web/dist/assets/ArchiveConfirmModal-DeoCVGXt.css +1 -0
  76. package/packages/web/dist/assets/CommandButtonDetailView-D8xfqLAp.js +1 -0
  77. package/packages/web/dist/assets/CommandButtonDetailView-D9zjx9ME.css +1 -0
  78. package/packages/web/dist/assets/EffortLevelSelector-D2Hdzc_8.js +1 -0
  79. package/packages/web/dist/assets/{GeneralSettingsView-DsHChEhv.js → GeneralSettingsView-sPXkLlLy.js} +1 -1
  80. package/packages/web/dist/assets/InputWithButton-B-o0DgMH.js +1 -0
  81. package/packages/web/dist/assets/{InputWithButton-cYdrEmTs.css → InputWithButton-D9HMvfR5.css} +1 -1
  82. package/packages/web/dist/assets/{InterpolationHelp-CIkOSkWX.js → InterpolationHelp-Dxn1li4l.js} +1 -1
  83. package/packages/web/dist/assets/MarkdownEditor-D4Kbb-9l.js +2 -0
  84. package/packages/web/dist/assets/ModelSelector-72C7MUH4.js +1 -0
  85. package/packages/web/dist/assets/{ModelSelector-D8hbTRIt.css → ModelSelector-BNYKujL-.css} +1 -1
  86. package/packages/web/dist/assets/NewSessionView-BR_COfgW.js +3 -0
  87. package/packages/web/dist/assets/NewSessionView-DBl7T2Xp.css +1 -0
  88. package/packages/web/dist/assets/ProjectEditView-DbqTbA0q.css +1 -0
  89. package/packages/web/dist/assets/ProjectEditView-WImU7sNd.js +1 -0
  90. package/packages/web/dist/assets/{ProjectListView-B9FuWESY.js → ProjectListView-CYmmAcBD.js} +1 -1
  91. package/packages/web/dist/assets/{ProjectNewView-D62jYlBL.js → ProjectNewView-DEhqw3Jv.js} +1 -1
  92. package/packages/web/dist/assets/ProvidersView-XZh3jkmH.js +1 -0
  93. package/packages/web/dist/assets/ProvidersView-bZemq_Rv.css +1 -0
  94. package/packages/web/dist/assets/QuickResponsesPanel-BqmnTd-D.js +1 -0
  95. package/packages/web/dist/assets/QuickResponsesPanel-dk-Rj8xx.css +1 -0
  96. package/packages/web/dist/assets/ResizableTextarea-BQNw5e0C.css +1 -0
  97. package/packages/web/dist/assets/ResizableTextarea-DpWdIAP6.js +1 -0
  98. package/packages/web/dist/assets/SessionCard-Bw77-KwD.js +1 -0
  99. package/packages/web/dist/assets/SessionDetailView-B59TEkr-.js +36 -0
  100. package/packages/web/dist/assets/SessionDetailView-CKVBnR4T.css +1 -0
  101. package/packages/web/dist/assets/{SessionFormOptions-DYUISplS.js → SessionFormOptions-hqijxc0S.js} +1 -1
  102. package/packages/web/dist/assets/SessionListView-3-xx6EVs.css +1 -0
  103. package/packages/web/dist/assets/SessionListView-DYXHM9I-.js +1 -0
  104. package/packages/web/dist/assets/{SessionLogStream-DpUE6Xsh.js → SessionLogStream-5NfVr9pF.js} +6 -6
  105. package/packages/web/dist/assets/{SettingsView-BC055tIA.js → SettingsView-DI8ncOAV.js} +1 -1
  106. package/packages/web/dist/assets/{SlashCommandWizard-DmTyNG9O.js → SlashCommandWizard-BQ_rMzn-.js} +1 -1
  107. package/packages/web/dist/assets/{SlashCommandWizard-Dn7sNaBd.css → SlashCommandWizard-DJzw3LP5.css} +1 -1
  108. package/packages/web/dist/assets/{SummarySettingsView-BgnRCwlq.js → SummarySettingsView-C2Qs35mm.js} +1 -1
  109. package/packages/web/dist/assets/TemplateDetailView-B5NI2oTR.css +1 -0
  110. package/packages/web/dist/assets/TemplateDetailView-zVkIvgtu.js +1 -0
  111. package/packages/web/dist/assets/{commandButtons-D4RPpLiu.js → commandButtons-CoU3G4zK.js} +1 -1
  112. package/packages/web/dist/assets/index-9yF1uCCA.js +1 -0
  113. package/packages/web/dist/assets/index-BKstCaYU.js +1 -0
  114. package/packages/web/dist/assets/index-BhbH7eOk.js +1 -0
  115. package/packages/web/dist/assets/{index-BGwH4Cfn.js → index-BjuRttEY.js} +3 -3
  116. package/packages/web/dist/assets/index-Bo7PdwM5.js +1 -0
  117. package/packages/web/dist/assets/index-C2QFVD7d.js +83 -0
  118. package/packages/web/dist/assets/index-C7Ww2auW.js +1 -0
  119. package/packages/web/dist/assets/index-CAGdsDh7.js +1 -0
  120. package/packages/web/dist/assets/index-CLRsVASf.js +3 -0
  121. package/packages/web/dist/assets/{index-Bn5xdGFM.js → index-CP-SxOlV.js} +1 -1
  122. package/packages/web/dist/assets/index-CslU0psO.js +1 -0
  123. package/packages/web/dist/assets/index-DI4NxaWD.js +1 -0
  124. package/packages/web/dist/assets/index-DOzONENy.js +1 -0
  125. package/packages/web/dist/assets/index-DUa7adFh.js +1 -0
  126. package/packages/web/dist/assets/index-DZBpETI5.js +1 -0
  127. package/packages/web/dist/assets/index-DsjWqc6R.js +7 -0
  128. package/packages/web/dist/assets/index-c99Bo3JV.js +1 -0
  129. package/packages/web/dist/assets/index-mT1JpxDc.js +1 -0
  130. package/packages/web/dist/assets/index-rkQx2tso.js +1 -0
  131. package/packages/web/dist/assets/{index-Cs2nxhrT.css → index-uySCcnA_.css} +1 -1
  132. package/packages/web/dist/assets/projectDefaults-B8esIcYq.js +1 -0
  133. package/packages/web/dist/assets/{projects-BUiOGmmb.js → projects-C-8PSxKi.js} +1 -1
  134. package/packages/web/dist/assets/{providers-Bh1ZiiJi.js → providers-oXifvvqN.js} +1 -1
  135. package/packages/web/dist/assets/sessions-Nq5VafSf.js +1 -0
  136. package/packages/web/dist/assets/{settings-Z4AVVmkJ.js → settings-DtpuiyT6.js} +1 -1
  137. package/packages/web/dist/index.html +2 -2
  138. package/packages/server/src/api/sessions-notes.js +0 -51
  139. package/packages/server/src/db/SessionNoteRepository.js +0 -60
  140. package/packages/web/dist/assets/ApiClient-B4YTtyY4.js +0 -1
  141. package/packages/web/dist/assets/ArchiveConfirmModal-BQ-4gI0R.css +0 -1
  142. package/packages/web/dist/assets/ArchiveConfirmModal-OFaj_uX5.js +0 -1
  143. package/packages/web/dist/assets/CommandButtonDetailView-D8S258uP.js +0 -1
  144. package/packages/web/dist/assets/CommandButtonDetailView-DBm3rzhw.css +0 -1
  145. package/packages/web/dist/assets/EffortLevelSelector-C2378L8e.js +0 -1
  146. package/packages/web/dist/assets/InputWithButton-Ci15ox0a.js +0 -1
  147. package/packages/web/dist/assets/MarkdownEditor-5-bexzUT.js +0 -2
  148. package/packages/web/dist/assets/ModelSelector-BMpR0DPr.js +0 -1
  149. package/packages/web/dist/assets/NewSessionView-BCqtIgWH.js +0 -3
  150. package/packages/web/dist/assets/NewSessionView-CUUdHkfv.css +0 -1
  151. package/packages/web/dist/assets/ProjectEditView-D9sK0fdH.css +0 -1
  152. package/packages/web/dist/assets/ProjectEditView-RFaxHhAX.js +0 -1
  153. package/packages/web/dist/assets/ProvidersView-DDKMIQWZ.js +0 -1
  154. package/packages/web/dist/assets/ProvidersView-DE82G_5W.css +0 -1
  155. package/packages/web/dist/assets/QuickResponseSettings-B8188A1D.css +0 -1
  156. package/packages/web/dist/assets/QuickResponseSettings-CDm5vwP7.js +0 -1
  157. package/packages/web/dist/assets/QuickResponsesPanel-BlFDvnZ2.css +0 -1
  158. package/packages/web/dist/assets/QuickResponsesPanel-DZ_Lre_l.js +0 -1
  159. package/packages/web/dist/assets/ResizableTextarea-DiIOEGjN.js +0 -1
  160. package/packages/web/dist/assets/ResizableTextarea-DsU3TVwF.css +0 -1
  161. package/packages/web/dist/assets/SessionCard-DmjnVYWn.js +0 -1
  162. package/packages/web/dist/assets/SessionDetailView-CL7nmfiB.js +0 -36
  163. package/packages/web/dist/assets/SessionDetailView-CupIkI7u.css +0 -1
  164. package/packages/web/dist/assets/SessionListView-BcxGz4aC.js +0 -1
  165. package/packages/web/dist/assets/SessionListView-fHlQyecX.css +0 -1
  166. package/packages/web/dist/assets/TemplateDetailView-BlhOmLUX.js +0 -1
  167. package/packages/web/dist/assets/TemplateDetailView-DT2m06W7.css +0 -1
  168. package/packages/web/dist/assets/index-4rhEeO0B.js +0 -1
  169. package/packages/web/dist/assets/index-9vb2KaAd.js +0 -1
  170. package/packages/web/dist/assets/index-B0CvZXuN.js +0 -7
  171. package/packages/web/dist/assets/index-B6G18FqB.js +0 -82
  172. package/packages/web/dist/assets/index-BUhvkAdF.js +0 -1
  173. package/packages/web/dist/assets/index-BcnkUk2o.js +0 -1
  174. package/packages/web/dist/assets/index-CNwkdB0T.js +0 -1
  175. package/packages/web/dist/assets/index-CfL84oGW.js +0 -1
  176. package/packages/web/dist/assets/index-CkmxO8Mm.js +0 -1
  177. package/packages/web/dist/assets/index-Cpy4-yv3.js +0 -1
  178. package/packages/web/dist/assets/index-CrAQJmoZ.js +0 -1
  179. package/packages/web/dist/assets/index-D6Ky9vJe.js +0 -3
  180. package/packages/web/dist/assets/index-DfrE0gAC.js +0 -1
  181. package/packages/web/dist/assets/index-KwEyz0F3.js +0 -1
  182. package/packages/web/dist/assets/index-OfCywayk.js +0 -1
  183. package/packages/web/dist/assets/index-PDesaJc6.js +0 -1
  184. package/packages/web/dist/assets/index-uB6nhSvz.js +0 -1
  185. package/packages/web/dist/assets/sessions-DH1R-NhV.js +0 -1
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ import Database from 'better-sqlite3';
3
+ import { existsSync } from 'node:fs';
4
+ import {
5
+ createFreshBaselineDb,
6
+ getActiveDbPath,
7
+ getIndexColumns,
8
+ getSchemaObjects,
9
+ getTableColumns,
10
+ normalizeSql,
11
+ } from './dbUtils.js';
12
+ import {
13
+ BUILT_IN_ANTHROPIC_MODELS,
14
+ BUILT_IN_OPENAI_MODELS,
15
+ BUILT_IN_ANTHROPIC_PROVIDER,
16
+ BUILT_IN_OPENAI_PROVIDER,
17
+ } from '../db/seedBaselineData.js';
18
+
19
+ export const DRIFT_SENSITIVE_TABLES = [
20
+ 'sessions',
21
+ 'project_session_defaults',
22
+ 'session_templates',
23
+ 'canvas_items',
24
+ 'providers',
25
+ 'provider_models',
26
+ 'agent_call_logs',
27
+ 'kanban_lanes',
28
+ ];
29
+
30
+ export const REQUIRED_INDEXES = {
31
+ idx_sessions_project: ['project_id'],
32
+ idx_sessions_status: ['status'],
33
+ idx_sessions_archived: ['archived'],
34
+ idx_sessions_starred: ['archived', 'starred'],
35
+ idx_sessions_next_template: ['next_template_id'],
36
+ idx_sessions_parent: ['parent_session_id'],
37
+ idx_sessions_scheduled: ['scheduled_at'],
38
+ idx_messages_conversation: ['conversation_id'],
39
+ idx_canvas_deleted: ['deleted_at'],
40
+ idx_todos_conversation: ['conversation_id'],
41
+ idx_project_defaults_projectId: ['project_id'],
42
+ idx_conversations_parent: ['parent_conversation_id'],
43
+ idx_agent_call_logs_agent_type: ['agent_type'],
44
+ idx_agent_call_logs_call_type: ['call_type'],
45
+ idx_agent_call_logs_status: ['status'],
46
+ idx_agent_call_logs_model: ['model'],
47
+ };
48
+
49
+ function byName(rows) {
50
+ return new Map(rows.map((row) => [row.name, row]));
51
+ }
52
+
53
+ function compareTables(actualDb, expectedDb, errors) {
54
+ const actualObjects = byName(getSchemaObjects(actualDb).filter((row) => row.type === 'table'));
55
+ const expectedObjects = getSchemaObjects(expectedDb).filter((row) => row.type === 'table');
56
+
57
+ for (const expected of expectedObjects) {
58
+ const actual = actualObjects.get(expected.name);
59
+ if (!actual) {
60
+ errors.push(`Missing table: ${expected.name}`);
61
+ continue;
62
+ }
63
+
64
+ const actualColumns = new Map(
65
+ getTableColumns(actualDb, expected.name).map((col) => [col.name, col])
66
+ );
67
+ const expectedColumns = getTableColumns(expectedDb, expected.name);
68
+
69
+ for (const expectedColumn of expectedColumns) {
70
+ const actualColumn = actualColumns.get(expectedColumn.name);
71
+ if (!actualColumn) {
72
+ errors.push(`Missing column: ${expected.name}.${expectedColumn.name}`);
73
+ continue;
74
+ }
75
+
76
+ const actualSummary = {
77
+ type: actualColumn.type,
78
+ notnull: actualColumn.notnull,
79
+ dflt_value: actualColumn.dflt_value,
80
+ pk: actualColumn.pk,
81
+ };
82
+ const expectedSummary = {
83
+ type: expectedColumn.type,
84
+ notnull: expectedColumn.notnull,
85
+ dflt_value: expectedColumn.dflt_value,
86
+ pk: expectedColumn.pk,
87
+ };
88
+ if (JSON.stringify(actualSummary) !== JSON.stringify(expectedSummary)) {
89
+ errors.push(`Column mismatch for ${expected.name}.${expectedColumn.name}`);
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ function compareIndexes(actualDb, expectedDb, errors) {
96
+ const actualObjects = byName(getSchemaObjects(actualDb).filter((row) => row.type === 'index'));
97
+ const expectedObjects = byName(getSchemaObjects(expectedDb).filter((row) => row.type === 'index'));
98
+
99
+ for (const [name, expectedColumns] of Object.entries(REQUIRED_INDEXES)) {
100
+ const actual = actualObjects.get(name);
101
+ if (!actual) {
102
+ errors.push(`Missing index: ${name}`);
103
+ continue;
104
+ }
105
+ const actualColumns = getIndexColumns(actualDb, name);
106
+ if (JSON.stringify(actualColumns) !== JSON.stringify(expectedColumns)) {
107
+ errors.push(`Index column mismatch for ${name}: ${actualColumns.join(',')} !== ${expectedColumns.join(',')}`);
108
+ }
109
+ }
110
+
111
+ const expectedScheduledSql = normalizeSql(expectedObjects.get('idx_sessions_scheduled')?.sql);
112
+ const actualScheduledSql = normalizeSql(actualObjects.get('idx_sessions_scheduled')?.sql);
113
+ if (expectedScheduledSql !== actualScheduledSql) {
114
+ errors.push('Partial index SQL mismatch for idx_sessions_scheduled');
115
+ }
116
+ }
117
+
118
+ function validateProviders(actualDb, errors) {
119
+ for (const provider of [BUILT_IN_ANTHROPIC_PROVIDER, BUILT_IN_OPENAI_PROVIDER]) {
120
+ const row = actualDb.prepare(
121
+ 'SELECT id, name, kind, base_url, auth_token, commit_attribution_override FROM providers WHERE id = ?'
122
+ ).get(provider.id);
123
+ if (!row) {
124
+ errors.push(`Missing provider seed row: ${provider.id}`);
125
+ continue;
126
+ }
127
+ if (row.name !== provider.name || row.kind !== provider.kind) {
128
+ errors.push(`Provider seed mismatch: ${provider.id}`);
129
+ }
130
+ if (row.base_url !== null || row.auth_token !== null) {
131
+ errors.push(`Provider nullable seed columns mismatch: ${provider.id}`);
132
+ }
133
+ }
134
+ }
135
+
136
+ function validateProviderModels(actualDb, errors) {
137
+ for (const model of [...BUILT_IN_ANTHROPIC_MODELS, ...BUILT_IN_OPENAI_MODELS]) {
138
+ const row = actualDb.prepare(
139
+ 'SELECT provider_id, model_id, display_name, description, tier FROM provider_models WHERE id = ?'
140
+ ).get(model.id);
141
+ if (!row) {
142
+ errors.push(`Missing provider model seed row: ${model.id}`);
143
+ continue;
144
+ }
145
+ const expected = {
146
+ provider_id: model.providerId,
147
+ model_id: model.modelId,
148
+ display_name: model.displayName,
149
+ description: model.description,
150
+ tier: model.tier,
151
+ };
152
+ if (JSON.stringify(row) !== JSON.stringify(expected)) {
153
+ errors.push(`Provider model seed mismatch: ${model.id}`);
154
+ }
155
+ }
156
+ }
157
+
158
+ function compareSeedRows(actualDb, errors) {
159
+ const tables = new Set(actualDb.prepare(
160
+ "SELECT name FROM sqlite_master WHERE type = 'table'"
161
+ ).all().map((row) => row.name));
162
+ if (!tables.has('providers') || !tables.has('provider_models') || !tables.has('quick_responses') || !tables.has('session_templates')) {
163
+ errors.push('Required seed tables are missing');
164
+ return;
165
+ }
166
+ validateProviders(actualDb, errors);
167
+ validateProviderModels(actualDb, errors);
168
+ }
169
+
170
+ export function validateDatabaseBaseline(actualDb) {
171
+ const fresh = createFreshBaselineDb();
172
+ const errors = [];
173
+
174
+ try {
175
+ compareTables(actualDb, fresh.db, errors);
176
+ compareIndexes(actualDb, fresh.db, errors);
177
+ compareSeedRows(actualDb, errors);
178
+ } finally {
179
+ fresh.close();
180
+ }
181
+
182
+ return errors;
183
+ }
184
+
185
+ export function main() {
186
+ const dbPath = getActiveDbPath();
187
+ if (!existsSync(dbPath)) {
188
+ console.log(`No database found at ${dbPath}; nothing to validate.`);
189
+ return 0;
190
+ }
191
+
192
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
193
+ try {
194
+ const errors = validateDatabaseBaseline(db);
195
+ if (errors.length === 0) {
196
+ console.log(`Database baseline validation passed: ${dbPath}`);
197
+ return 0;
198
+ }
199
+
200
+ console.error(`Database baseline validation failed: ${dbPath}`);
201
+ for (const error of errors) {
202
+ console.error(`- ${error}`);
203
+ }
204
+ return 1;
205
+ } finally {
206
+ db.close();
207
+ }
208
+ }
209
+
210
+ if (import.meta.url === `file://${process.argv[1]}`) {
211
+ process.exit(main());
212
+ }
@@ -1,5 +1,10 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { createRobustEnv } from './nodeSpawnHelper.js';
3
+ import {
4
+ captureSpawnAttempt,
5
+ createCapturedSpawnProcess,
6
+ isE2ESpawnCaptureEnabled,
7
+ } from './e2eSpawnCapture.js';
3
8
 
4
9
  /**
5
10
  * Create a custom spawn function for the Codex CLI.
@@ -19,6 +24,10 @@ import { createRobustEnv } from './nodeSpawnHelper.js';
19
24
  export function createCodexSpawner() {
20
25
  return (options) => {
21
26
  const { command, args, cwd, env, signal } = options;
27
+ if (isE2ESpawnCaptureEnabled()) {
28
+ captureSpawnAttempt('codex', options);
29
+ return createCapturedSpawnProcess('codex');
30
+ }
22
31
 
23
32
  // Replace 'node' with the absolute path to the current Node executable
24
33
  const actualCommand = command === 'node' ? process.execPath : command;
@@ -1,11 +1,11 @@
1
1
  import { commandButtons } from '../database.js';
2
2
 
3
3
  /**
4
- * Build Command Button API instructions for system prompt if the project has command buttons.
4
+ * Build Command API instructions for system prompt if the project has commands.
5
5
  * @param {string} apiUrl - Base API URL
6
6
  * @param {string} sessionId - Current session ID
7
7
  * @param {string} projectId - Current project ID
8
- * @returns {string} Command button instructions or empty string if no buttons configured
8
+ * @returns {string} Command instructions or empty string if no commands configured
9
9
  */
10
10
  export function buildCommandButtonApiInstructions(apiUrl, sessionId, projectId) {
11
11
  const buttons = commandButtons.getByProjectId(projectId);
@@ -13,36 +13,38 @@ export function buildCommandButtonApiInstructions(apiUrl, sessionId, projectId)
13
13
  return '';
14
14
  }
15
15
 
16
- return `## Command Buttons API
16
+ return `## Circus Commands
17
17
 
18
- This project has command buttons 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
19
 
20
- ### List Available Buttons
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.
21
+
22
+ ### List Available Commands
21
23
  \`\`\`bash
22
- curl ${apiUrl}/api/projects/${projectId}/command-buttons
24
+ curl ${apiUrl}/api/sessions/${sessionId}/circus-commands
23
25
  \`\`\`
24
26
 
25
- ### Run a Button
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
- ### List All Runs for This Session
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
  }
@@ -7,6 +7,12 @@ import { TerminalOutputProcessor } from './terminalOutput.js';
7
7
  // Re-export for backward compatibility
8
8
  export { stripAnsiCodes, TerminalOutputProcessor } from './terminalOutput.js';
9
9
 
10
+ export function createCommandRunnerEnv(baseEnv = process.env) {
11
+ const env = createRobustEnv(baseEnv);
12
+ delete env.CIRCUSCHIEF_COMMIT_ATTRIBUTION;
13
+ return env;
14
+ }
15
+
10
16
  /**
11
17
  * Service for running commands and managing their execution
12
18
  */
@@ -141,7 +147,7 @@ export class CommandRunner {
141
147
  cwd: workingDirectory,
142
148
  stdio: ['pipe', 'pipe', 'pipe'],
143
149
  detached: true,
144
- env: createRobustEnv(),
150
+ env: createCommandRunnerEnv(),
145
151
  });
146
152
 
147
153
  const entry = this.#createProcessEntry(child, sessionId, buttonId);
@@ -0,0 +1,147 @@
1
+ import { appendFileSync } from 'fs';
2
+ import { EventEmitter } from 'events';
3
+ import { PassThrough } from 'stream';
4
+
5
+ export function isE2ESpawnCaptureEnabled() {
6
+ return Boolean(process.env.E2E_AGENT_SPAWN_CAPTURE_FILE);
7
+ }
8
+
9
+ export function captureSpawnAttempt(agentType, spawnOptions) {
10
+ const filePath = process.env.E2E_AGENT_SPAWN_CAPTURE_FILE;
11
+ if (!filePath) return;
12
+
13
+ const record = {
14
+ agentType,
15
+ command: spawnOptions.command,
16
+ args: spawnOptions.args || [],
17
+ cwd: spawnOptions.cwd || null,
18
+ env: summarizeSpawnEnv(spawnOptions.env),
19
+ options: summarizeSpawnOptions(agentType, spawnOptions),
20
+ capturedAt: new Date().toISOString(),
21
+ };
22
+
23
+ appendFileSync(filePath, `${JSON.stringify(record)}\n`, 'utf8');
24
+ }
25
+
26
+ export function createCapturedSpawnProcess(agentType) {
27
+ const processStub = new EventEmitter();
28
+ const stdin = new PassThrough();
29
+ const stdout = new PassThrough();
30
+ const stderr = new PassThrough();
31
+
32
+ processStub.stdin = stdin;
33
+ processStub.stdout = stdout;
34
+ processStub.stderr = stderr;
35
+ processStub.killed = false;
36
+ processStub.exitCode = null;
37
+ processStub.kill = (signal = 'SIGTERM') => {
38
+ if (processStub.killed || processStub.exitCode !== null) return true;
39
+ processStub.killed = true;
40
+ finishProcess({ processStub, stdout, stderr, code: null, signal });
41
+ return true;
42
+ };
43
+
44
+ const complete = () => {
45
+ setImmediate(() => {
46
+ if (processStub.killed || processStub.exitCode !== null) return;
47
+ writeCapturedAgentEvents(agentType, stdout);
48
+ finishProcess({ processStub, stdout, stderr, code: 0, signal: null });
49
+ });
50
+ };
51
+
52
+ if (agentType === 'claude-code') {
53
+ setTimeout(complete, 10);
54
+ } else {
55
+ stdin.once('finish', complete);
56
+ }
57
+
58
+ return processStub;
59
+ }
60
+
61
+ function summarizeSpawnEnv(env = {}) {
62
+ if (!Object.prototype.hasOwnProperty.call(env, 'CIRCUSCHIEF_COMMIT_ATTRIBUTION')) {
63
+ return {};
64
+ }
65
+ return {
66
+ CIRCUSCHIEF_COMMIT_ATTRIBUTION: env.CIRCUSCHIEF_COMMIT_ATTRIBUTION,
67
+ };
68
+ }
69
+
70
+ function summarizeSpawnOptions(agentType, spawnOptions) {
71
+ const args = spawnOptions.args || [];
72
+ if (agentType === 'claude-code') {
73
+ return {
74
+ model: valueAfter(args, '--model'),
75
+ settings: valueAfter(args, '--settings'),
76
+ permissionMode: valueAfter(args, '--permission-mode'),
77
+ settingSources: valueAfter(args, '--setting-sources'),
78
+ };
79
+ }
80
+
81
+ return {
82
+ model: valueAfter(args, '-m'),
83
+ sandbox: valueAfter(args, '--sandbox'),
84
+ config: valuesAfter(args, '-c'),
85
+ };
86
+ }
87
+
88
+ function valueAfter(args, flag) {
89
+ const index = args.indexOf(flag);
90
+ if (index === -1) return null;
91
+ return args[index + 1] ?? null;
92
+ }
93
+
94
+ function valuesAfter(args, flag) {
95
+ const values = [];
96
+ for (let index = 0; index < args.length; index += 1) {
97
+ if (args[index] === flag && args[index + 1] !== undefined) {
98
+ values.push(args[index + 1]);
99
+ }
100
+ }
101
+ return values;
102
+ }
103
+
104
+ function writeCapturedAgentEvents(agentType, stdout) {
105
+ if (agentType === 'codex') {
106
+ writeJsonLine(stdout, { type: 'thread.started', thread_id: `e2e-codex-${Date.now()}` });
107
+ writeJsonLine(stdout, { type: 'turn.started' });
108
+ writeJsonLine(stdout, {
109
+ type: 'item.completed',
110
+ item: { type: 'agent_message', text: 'E2E spawn capture response.' },
111
+ });
112
+ writeJsonLine(stdout, {
113
+ type: 'turn.completed',
114
+ usage: { input_tokens: 0, cached_input_tokens: 0, output_tokens: 0 },
115
+ });
116
+ return;
117
+ }
118
+
119
+ writeJsonLine(stdout, {
120
+ type: 'system',
121
+ subtype: 'init',
122
+ session_id: `e2e-claude-${Date.now()}`,
123
+ });
124
+ writeJsonLine(stdout, {
125
+ type: 'assistant',
126
+ message: { content: [{ type: 'text', text: 'E2E spawn capture response.' }] },
127
+ });
128
+ writeJsonLine(stdout, {
129
+ type: 'result',
130
+ subtype: 'success',
131
+ usage: { input_tokens: 0, output_tokens: 0 },
132
+ });
133
+ }
134
+
135
+ function writeJsonLine(stream, value) {
136
+ stream.write(`${JSON.stringify(value)}\n`);
137
+ }
138
+
139
+ function finishProcess({ processStub, stdout, stderr, code, signal }) {
140
+ Object.assign(processStub, { exitCode: code });
141
+ stdout.end();
142
+ stderr.end();
143
+ setImmediate(() => {
144
+ processStub.emit('exit', code, signal);
145
+ processStub.emit('close', code, signal);
146
+ });
147
+ }
@@ -0,0 +1,150 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { chmod, mkdir, writeFile } from 'fs/promises';
6
+
7
+ const execAsync = promisify(exec);
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
+
36
+ const ATTRIBUTION_CONFIG_KEY = 'circuschief.commitAttribution';
37
+ const ATTRIBUTION_ENV_KEY = 'CIRCUSCHIEF_COMMIT_ATTRIBUTION';
38
+
39
+ async function git(directory, command) {
40
+ const { stdout } = await execAsync(`git ${command}`, { cwd: directory });
41
+ return stdout.trim();
42
+ }
43
+
44
+ async function gitConfigValue(directory, key) {
45
+ try {
46
+ return await git(directory, `config --get ${key}`);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function shellQuote(value) {
53
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
54
+ }
55
+
56
+ function buildCommitMsgHook() {
57
+ return `#!/bin/sh
58
+ set -eu
59
+
60
+ msg_file="$1"
61
+ trailer="\${${ATTRIBUTION_ENV_KEY}:-}"
62
+
63
+ [ -n "$trailer" ] || exit 0
64
+ [ -n "$(printf '%s' "$trailer" | tr -d '[:space:]')" ] || exit 0
65
+
66
+ if printf '%s' "$trailer" | grep '[[:cntrl:]]' >/dev/null 2>&1; then
67
+ echo "${ATTRIBUTION_ENV_KEY} must be a canonical Co-authored-by: Name <email> trailer." >&2
68
+ exit 1
69
+ fi
70
+
71
+ if ! printf '%s\\n' "$trailer" | grep -E '^Co-authored-by: [^<>[:space:]][^<>]* <[^[:space:]<>@]+@[^[:space:]<>@]+\\.[^[:space:]<>@]+>$' >/dev/null 2>&1; then
72
+ echo "${ATTRIBUTION_ENV_KEY} must be a canonical Co-authored-by: Name <email> trailer." >&2
73
+ exit 1
74
+ fi
75
+
76
+ if grep -F -x -i -- "$trailer" "$msg_file" >/dev/null 2>&1; then
77
+ exit 0
78
+ fi
79
+
80
+ git interpret-trailers --trailer "$trailer" --in-place "$msg_file"
81
+ `;
82
+ }
83
+
84
+ export async function clearWorktreeCommitAttribution(worktreePath) {
85
+ const managedHooksPath = getManagedHooksPath();
86
+ const currentAttribution = await gitConfigValue(worktreePath, ATTRIBUTION_CONFIG_KEY);
87
+ const currentHooksPath = await gitConfigValue(worktreePath, 'core.hooksPath');
88
+ if (!currentAttribution && currentHooksPath !== managedHooksPath) {
89
+ return false;
90
+ }
91
+
92
+ await git(worktreePath, 'config extensions.worktreeConfig true');
93
+
94
+ if (currentAttribution) {
95
+ try {
96
+ await git(worktreePath, `config --worktree --unset ${ATTRIBUTION_CONFIG_KEY}`);
97
+ } catch {
98
+ // Unset is idempotent for callers that are clearing a value that was never set.
99
+ }
100
+ }
101
+
102
+ if (currentHooksPath === managedHooksPath) {
103
+ try {
104
+ await git(worktreePath, 'config --worktree --unset core.hooksPath');
105
+ } catch {
106
+ // Unset is idempotent for callers that are clearing stale managed hook config.
107
+ }
108
+ }
109
+ return false;
110
+ }
111
+
112
+ /**
113
+ * Install/update managed commit attribution enforcement for a worktree.
114
+ *
115
+ * Runtime attribution is process-scoped: the hook reads
116
+ * CIRCUSCHIEF_COMMIT_ATTRIBUTION from the `git commit` process environment.
117
+ *
118
+ * @param {string} worktreePath - The worktree directory
119
+ * @returns {Promise<boolean>} True when a hook is installed or updated
120
+ */
121
+ export async function ensureWorktreeCommitAttributionHook(worktreePath) {
122
+ const managedHooksPath = getManagedHooksPath();
123
+
124
+ await git(worktreePath, 'config extensions.worktreeConfig true');
125
+
126
+ const currentHooksPath = await gitConfigValue(worktreePath, 'core.hooksPath');
127
+ if (currentHooksPath && currentHooksPath !== managedHooksPath && currentHooksPath !== LEGACY_MANAGED_HOOKS_PATH) {
128
+ throw new Error(
129
+ `Cannot install managed commit attribution hook: worktree already has core.hooksPath set to "${currentHooksPath}"`
130
+ );
131
+ }
132
+
133
+ try {
134
+ await git(worktreePath, `config --worktree --unset ${ATTRIBUTION_CONFIG_KEY}`);
135
+ } catch {
136
+ // Unset is idempotent for stale worktrees that never stored attribution.
137
+ }
138
+
139
+ await git(worktreePath, `config --worktree core.hooksPath ${shellQuote(managedHooksPath)}`);
140
+
141
+ const hookPath = path.join(managedHooksPath, 'commit-msg');
142
+ await mkdir(managedHooksPath, { recursive: true });
143
+ await writeFile(hookPath, buildCommitMsgHook(), 'utf8');
144
+ await chmod(hookPath, 0o755);
145
+ return true;
146
+ }
147
+
148
+ export async function configureWorktreeCommitAttribution(worktreePath, _commitAttribution) {
149
+ return ensureWorktreeCommitAttributionHook(worktreePath);
150
+ }