circuschief 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/package.json +1 -1
  2. package/packages/server/src/agents/adapters/CodexAdapter.js +5 -4
  3. package/packages/server/src/api/canvas.js +22 -57
  4. package/packages/server/src/api/index.js +2 -0
  5. package/packages/server/src/api/kanban.js +4 -2
  6. package/packages/server/src/api/projects-helpers.js +20 -3
  7. package/packages/server/src/api/projects-session-helpers.js +35 -4
  8. package/packages/server/src/api/projects.js +11 -0
  9. package/packages/server/src/api/providers.js +11 -1
  10. package/packages/server/src/api/sessions-commands.js +35 -17
  11. package/packages/server/src/api/sessions-draft.js +1 -0
  12. package/packages/server/src/api/sessions-lifecycle.js +10 -10
  13. package/packages/server/src/api/sessions-patch.js +4 -0
  14. package/packages/server/src/api/sessions.js +6 -5
  15. package/packages/server/src/api/settings.js +52 -4
  16. package/packages/server/src/database.js +0 -2
  17. package/packages/server/src/db/ConversationRepository.js +16 -3
  18. package/packages/server/src/db/DatabaseManager.js +5 -1
  19. package/packages/server/src/db/ProjectDefaultsRepository.js +50 -40
  20. package/packages/server/src/db/ProviderRepository.js +87 -32
  21. package/packages/server/src/db/SessionRepository.js +13 -8
  22. package/packages/server/src/db/SettingsRepository.js +44 -16
  23. package/packages/server/src/db/conversation-helpers.js +1 -0
  24. package/packages/server/src/db/index.js +0 -3
  25. package/packages/server/src/db/migrations/index.js +36 -200
  26. package/packages/server/src/db/seedBaselineData.js +137 -0
  27. package/packages/server/src/db/session-helpers.js +9 -3
  28. package/packages/server/src/middleware/sessionLookup.js +81 -8
  29. package/packages/server/src/schema.sql +157 -132
  30. package/packages/server/src/scripts/backupDatabase.js +21 -0
  31. package/packages/server/src/scripts/dbUtils.js +81 -0
  32. package/packages/server/src/scripts/inspectDatabaseSchema.js +81 -0
  33. package/packages/server/src/scripts/validateDatabaseBaseline.js +212 -0
  34. package/packages/server/src/services/agentCallLogger.js +1 -1
  35. package/packages/server/src/services/codexSpawnHelper.js +9 -0
  36. package/packages/server/src/services/commandButtonPrompts.js +48 -0
  37. package/packages/server/src/services/commandRunner.js +7 -1
  38. package/packages/server/src/services/draftSessionService.js +2 -0
  39. package/packages/server/src/services/e2eSpawnCapture.js +147 -0
  40. package/packages/server/src/services/gitCommitAttribution.js +120 -0
  41. package/packages/server/src/services/gitService.js +11 -2
  42. package/packages/server/src/services/gitSessionSetup.js +11 -1
  43. package/packages/server/src/services/kanbanTriggers.js +6 -3
  44. package/packages/server/src/services/nodeSpawnHelper.js +9 -0
  45. package/packages/server/src/services/prUrlService.js +3 -3
  46. package/packages/server/src/services/queryParamBuilder.js +90 -0
  47. package/packages/server/src/services/sessionDuplicator.js +1 -5
  48. package/packages/server/src/services/sessionExecution.js +56 -106
  49. package/packages/server/src/services/sessionPrompts.js +16 -47
  50. package/packages/server/src/services/sessionProvider.js +16 -8
  51. package/packages/server/src/services/streamEventCallbacks.js +72 -40
  52. package/packages/server/src/services/streamEventHandler.js +13 -2
  53. package/packages/server/src/services/streamUsageHandler.js +6 -0
  54. package/packages/server/src/services/summaryClaudeClient.js +37 -12
  55. package/packages/server/src/services/summaryModelClient.js +154 -0
  56. package/packages/server/src/services/summaryModelResolver.js +148 -0
  57. package/packages/server/src/services/summaryService.js +11 -7
  58. package/packages/server/src/services/summaryStaleCheck.js +23 -4
  59. package/packages/server/src/services/templateTriggerService.js +3 -1
  60. package/packages/server/src/services/usageTracker.js +5 -1
  61. package/packages/server/src/services/visibleFinalErrorMessage.js +123 -0
  62. package/packages/shared/src/constants.js +4 -2
  63. package/packages/shared/src/contracts/commandButtons.js +16 -2
  64. package/packages/shared/src/contracts/projects.js +4 -2
  65. package/packages/shared/src/contracts/providers.js +60 -0
  66. package/packages/shared/src/contracts/sessions.js +2 -1
  67. package/packages/shared/src/contracts/templates.js +2 -2
  68. package/packages/shared/src/types.js +1 -9
  69. package/packages/shared/src/utils.js +11 -19
  70. package/packages/web/dist/assets/ActiveSessionsView-B0XHqLmv.js +1 -0
  71. package/packages/web/dist/assets/{AgentLogsView-Cdw4nmvd.js → AgentLogsView-DmsjUMlB.js} +2 -2
  72. package/packages/web/dist/assets/ApiClient-C3ztI9s9.js +1 -0
  73. package/packages/web/dist/assets/ArchiveConfirmModal-BlCyn5Vt.js +1 -0
  74. package/packages/web/dist/assets/ArchiveConfirmModal-DeoCVGXt.css +1 -0
  75. package/packages/web/dist/assets/{CommandButtonDetailView-DnFhJY5A.js → CommandButtonDetailView-CdSCPp78.js} +1 -1
  76. package/packages/web/dist/assets/EffortLevelSelector-hc2MNKg6.js +1 -0
  77. package/packages/web/dist/assets/{GeneralSettingsView-CQkmdczf.js → GeneralSettingsView-D1nI8_zk.js} +1 -1
  78. package/packages/web/dist/assets/InputWithButton-CAkttyqx.js +1 -0
  79. package/packages/web/dist/assets/{InputWithButton-cYdrEmTs.css → InputWithButton-D9HMvfR5.css} +1 -1
  80. package/packages/web/dist/assets/{InterpolationHelp-PfYR3KJo.js → InterpolationHelp-BO1j9Z3_.js} +1 -1
  81. package/packages/web/dist/assets/MarkdownEditor-ucRAP_UM.js +2 -0
  82. package/packages/web/dist/assets/{ModelSelector-BZOT1Jc6.css → ModelSelector-BSxKUSus.css} +1 -1
  83. package/packages/web/dist/assets/ModelSelector-CwTz8ZWO.js +1 -0
  84. package/packages/web/dist/assets/NewSessionView-BDPb-1qr.css +1 -0
  85. package/packages/web/dist/assets/NewSessionView-BsDrp8mj.js +3 -0
  86. package/packages/web/dist/assets/ProjectEditView-CwTOeSun.js +1 -0
  87. package/packages/web/dist/assets/ProjectEditView-J15mcsWz.css +1 -0
  88. package/packages/web/dist/assets/{ProjectListView-CuYMmd3O.js → ProjectListView-DcNyuINs.js} +1 -1
  89. package/packages/web/dist/assets/{ProjectNewView-CNaA4Maf.js → ProjectNewView-B5YV62hv.js} +1 -1
  90. package/packages/web/dist/assets/ProvidersView-bZemq_Rv.css +1 -0
  91. package/packages/web/dist/assets/ProvidersView-nY9GnDdO.js +1 -0
  92. package/packages/web/dist/assets/QuickResponseSettings-B352c75l.css +1 -0
  93. package/packages/web/dist/assets/QuickResponseSettings-BQwQXuL7.js +1 -0
  94. package/packages/web/dist/assets/{QuickResponsesPanel-DIBQFj0W.css → QuickResponsesPanel-BlFDvnZ2.css} +1 -1
  95. package/packages/web/dist/assets/{QuickResponsesPanel-BqMYSHb0.js → QuickResponsesPanel-BzSYcCSP.js} +1 -1
  96. package/packages/web/dist/assets/{ResizableTextarea-wYF3K2RO.js → ResizableTextarea-B3YIdIXv.js} +1 -1
  97. package/packages/web/dist/assets/{SessionCard-bLaQEWWX.js → SessionCard-CjE1tXiT.js} +1 -1
  98. package/packages/web/dist/assets/SessionDetailView-3cPZrbS3.js +36 -0
  99. package/packages/web/dist/assets/SessionDetailView-CZRZMrfM.css +1 -0
  100. package/packages/web/dist/assets/SessionFormOptions-B6AxyREh.js +1 -0
  101. package/packages/web/dist/assets/SessionFormOptions-BpUALRKn.css +1 -0
  102. package/packages/web/dist/assets/SessionListView-B5_6gW49.css +1 -0
  103. package/packages/web/dist/assets/SessionListView-CLXBfLcq.js +1 -0
  104. package/packages/web/dist/assets/{SessionLogStream-DTnDAF95.js → SessionLogStream-LlZ3z_Xj.js} +1 -1
  105. package/packages/web/dist/assets/{SettingsView-DNLUSsHV.js → SettingsView-CTGiGvR2.js} +1 -1
  106. package/packages/web/dist/assets/{SlashCommandWizard-CRGFaO8t.js → SlashCommandWizard-Cy04d7-o.js} +1 -1
  107. package/packages/web/dist/assets/{SlashCommandWizard-Dn7sNaBd.css → SlashCommandWizard-DJzw3LP5.css} +1 -1
  108. package/packages/web/dist/assets/SummarySettingsView-BR2ZjEa3.js +1 -0
  109. package/packages/web/dist/assets/SummarySettingsView-l2bxHmZZ.css +1 -0
  110. package/packages/web/dist/assets/TemplateDetailView-DH6Oswsp.js +1 -0
  111. package/packages/web/dist/assets/{commandButtons-Bbjf3fCt.js → commandButtons-BfqR-fqq.js} +1 -1
  112. package/packages/web/dist/assets/index-1zziPL6l.js +1 -0
  113. package/packages/web/dist/assets/index-7kzHPxSF.js +1 -0
  114. package/packages/web/dist/assets/index-B0N_obMc.js +1 -0
  115. package/packages/web/dist/assets/index-BNk_gdfI.js +1 -0
  116. package/packages/web/dist/assets/{index-gmCCsCQ1.css → index-BY174HVJ.css} +1 -1
  117. package/packages/web/dist/assets/index-CSqaAH-0.js +1 -0
  118. package/packages/web/dist/assets/index-C_q4WlK8.js +1 -0
  119. package/packages/web/dist/assets/index-D1wpU4y0.js +7 -0
  120. package/packages/web/dist/assets/index-D5zCA8sD.js +1 -0
  121. package/packages/web/dist/assets/index-DGR8ELWY.js +1 -0
  122. package/packages/web/dist/assets/index-DHga8pXo.js +1 -0
  123. package/packages/web/dist/assets/index-DSby02Wl.js +1 -0
  124. package/packages/web/dist/assets/{index-Cf6vdW-B.js → index-DgkC10TW.js} +3 -3
  125. package/packages/web/dist/assets/index-DqjXJTVI.js +1 -0
  126. package/packages/web/dist/assets/{index-Bs7Qf5D6.js → index-DtfUt785.js} +2 -2
  127. package/packages/web/dist/assets/index-_4S2uLDI.js +1 -0
  128. package/packages/web/dist/assets/{index-BQL_L4gL.js → index-fK8FIZgP.js} +15 -14
  129. package/packages/web/dist/assets/index-gmiZeFXN.js +1 -0
  130. package/packages/web/dist/assets/index-irD539ZM.js +3 -0
  131. package/packages/web/dist/assets/index-yq-E1Y00.js +1 -0
  132. package/packages/web/dist/assets/{projects-CPt3AB7U.js → projects-DXYQNJIi.js} +1 -1
  133. package/packages/web/dist/assets/{providers-ChfeMvUq.js → providers-1bnH-exJ.js} +1 -1
  134. package/packages/web/dist/assets/sessions-6zGUlFrt.js +1 -0
  135. package/packages/web/dist/assets/settings-MbfRir0d.js +1 -0
  136. package/packages/web/dist/index.html +2 -2
  137. package/packages/server/src/api/sessions-notes.js +0 -51
  138. package/packages/server/src/db/SessionNoteRepository.js +0 -60
  139. package/packages/server/src/db/migrations/canvasItemsMigrations.js +0 -109
  140. package/packages/server/src/db/migrations/conversationsMigrations.js +0 -183
  141. package/packages/server/src/db/migrations/kanbanMigrations.js +0 -99
  142. package/packages/server/src/db/migrations/miscMigrations.js +0 -369
  143. package/packages/server/src/db/migrations/projectsMigrations.js +0 -99
  144. package/packages/server/src/db/migrations/sessionsMigrations.js +0 -282
  145. package/packages/web/dist/assets/ActiveSessionsView-UCbQrF1b.js +0 -1
  146. package/packages/web/dist/assets/ApiClient-CWbXWDUY.js +0 -1
  147. package/packages/web/dist/assets/ArchiveConfirmModal-BQ-4gI0R.css +0 -1
  148. package/packages/web/dist/assets/ArchiveConfirmModal-J48eh3zw.js +0 -1
  149. package/packages/web/dist/assets/EffortLevelSelector-bXbPo4Zw.js +0 -1
  150. package/packages/web/dist/assets/InputWithButton-XyM3k6lN.js +0 -1
  151. package/packages/web/dist/assets/MarkdownEditor-P8F5kO-o.js +0 -2
  152. package/packages/web/dist/assets/ModelSelector-CowKfGMP.js +0 -1
  153. package/packages/web/dist/assets/NewSessionView-D_Hi7M9g.css +0 -1
  154. package/packages/web/dist/assets/NewSessionView-DkjFLvHU.js +0 -3
  155. package/packages/web/dist/assets/ProjectEditView-CpeKj-_w.css +0 -1
  156. package/packages/web/dist/assets/ProjectEditView-embVT7NC.js +0 -1
  157. package/packages/web/dist/assets/ProvidersView-C7rydtOd.js +0 -1
  158. package/packages/web/dist/assets/ProvidersView-DE82G_5W.css +0 -1
  159. package/packages/web/dist/assets/QuickResponseSettings-B8188A1D.css +0 -1
  160. package/packages/web/dist/assets/QuickResponseSettings-BTQEKhwJ.js +0 -1
  161. package/packages/web/dist/assets/SessionDetailView-Cv-xMzXp.css +0 -1
  162. package/packages/web/dist/assets/SessionDetailView-CvQOUsW2.js +0 -36
  163. package/packages/web/dist/assets/SessionFormOptions-3pzbgI2Q.js +0 -1
  164. package/packages/web/dist/assets/SessionFormOptions-DhhIkjIS.css +0 -1
  165. package/packages/web/dist/assets/SessionListView-Dranfb72.js +0 -1
  166. package/packages/web/dist/assets/SessionListView-fHlQyecX.css +0 -1
  167. package/packages/web/dist/assets/SummarySettingsView-C7G_suHp.js +0 -1
  168. package/packages/web/dist/assets/SummarySettingsView-DcsmSVJI.css +0 -1
  169. package/packages/web/dist/assets/TemplateDetailView-B78_DLMR.js +0 -1
  170. package/packages/web/dist/assets/index--V7c-VZf.js +0 -1
  171. package/packages/web/dist/assets/index-8Q04yd7H.js +0 -1
  172. package/packages/web/dist/assets/index-B47XRBDH.js +0 -1
  173. package/packages/web/dist/assets/index-BXbgZrhS.js +0 -1
  174. package/packages/web/dist/assets/index-CGhDVPen.js +0 -1
  175. package/packages/web/dist/assets/index-CKcRO1A6.js +0 -1
  176. package/packages/web/dist/assets/index-CTq-SLIW.js +0 -1
  177. package/packages/web/dist/assets/index-CYyos3iC.js +0 -1
  178. package/packages/web/dist/assets/index-CsCREAxF.js +0 -1
  179. package/packages/web/dist/assets/index-DJTTk_8T.js +0 -3
  180. package/packages/web/dist/assets/index-DPqUJ5JK.js +0 -1
  181. package/packages/web/dist/assets/index-EwAe1dKg.js +0 -1
  182. package/packages/web/dist/assets/index-JBA8axyA.js +0 -1
  183. package/packages/web/dist/assets/index-JkVHFtK5.js +0 -7
  184. package/packages/web/dist/assets/index-gMPUwT55.js +0 -1
  185. package/packages/web/dist/assets/index-wadc_0zT.js +0 -1
  186. package/packages/web/dist/assets/sessions-CwPsJOb1.js +0 -1
  187. package/packages/web/dist/assets/settings-BOj6wq6t.js +0 -1
@@ -0,0 +1,81 @@
1
+ import { existsSync, mkdirSync, copyFileSync, mkdtempSync, rmSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { homedir, tmpdir } from 'node:os';
4
+ import { getDefaultDbPath } from '../config.js';
5
+ import { DatabaseManager } from '../db/DatabaseManager.js';
6
+
7
+ export function getActiveDbPath() {
8
+ return process.env.DB_PATH || getDefaultDbPath();
9
+ }
10
+
11
+ export function getSqliteSidecarPaths(dbPath) {
12
+ return [`${dbPath}-wal`, `${dbPath}-shm`];
13
+ }
14
+
15
+ export function getBackupDir() {
16
+ return join(homedir(), '.circuschief', 'backups');
17
+ }
18
+
19
+ export function copyDatabaseBackups(dbPath = getActiveDbPath(), timestamp = new Date()) {
20
+ if (!existsSync(dbPath)) {
21
+ return { dbPath, backupDir: getBackupDir(), copied: [], missing: [dbPath] };
22
+ }
23
+
24
+ const backupDir = getBackupDir();
25
+ mkdirSync(backupDir, { recursive: true });
26
+
27
+ const stamp = timestamp.toISOString().replace(/[:.]/g, '-');
28
+ const candidates = [dbPath, ...getSqliteSidecarPaths(dbPath)];
29
+ const copied = [];
30
+ const missing = [];
31
+
32
+ for (const source of candidates) {
33
+ if (!existsSync(source)) {
34
+ missing.push(source);
35
+ continue;
36
+ }
37
+ const target = join(backupDir, `${basename(source)}.${stamp}.bak`);
38
+ copyFileSync(source, target);
39
+ copied.push({ source, target });
40
+ }
41
+
42
+ return { dbPath, backupDir, copied, missing };
43
+ }
44
+
45
+ export function createFreshBaselineDb() {
46
+ const tempDir = mkdtempSync(join(tmpdir(), 'circuschief-baseline-'));
47
+ const dbPath = join(tempDir, 'baseline.db');
48
+ const manager = new DatabaseManager();
49
+ const db = manager.init(dbPath);
50
+
51
+ return {
52
+ db,
53
+ dbPath,
54
+ close() {
55
+ manager.close();
56
+ rmSync(tempDir, { recursive: true, force: true });
57
+ },
58
+ };
59
+ }
60
+
61
+ export function getSchemaObjects(db) {
62
+ return db.prepare(`
63
+ SELECT type, name, tbl_name, sql
64
+ FROM sqlite_master
65
+ WHERE type IN ('table', 'index', 'trigger', 'view')
66
+ AND name NOT LIKE 'sqlite_%'
67
+ ORDER BY type, name
68
+ `).all();
69
+ }
70
+
71
+ export function getTableColumns(db, tableName) {
72
+ return db.prepare(`PRAGMA table_info(${tableName})`).all();
73
+ }
74
+
75
+ export function getIndexColumns(db, indexName) {
76
+ return db.prepare(`PRAGMA index_info(${indexName})`).all().map((row) => row.name);
77
+ }
78
+
79
+ export function normalizeSql(sql) {
80
+ return (sql || '').replace(/\s+/g, ' ').trim();
81
+ }
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ import Database from 'better-sqlite3';
3
+ import { existsSync } from 'node:fs';
4
+ import {
5
+ createFreshBaselineDb,
6
+ getActiveDbPath,
7
+ getSchemaObjects,
8
+ } from './dbUtils.js';
9
+
10
+ const BASELINE_TABLES = [
11
+ 'sessions',
12
+ 'projects',
13
+ 'project_session_defaults',
14
+ 'session_templates',
15
+ 'canvas_items',
16
+ 'providers',
17
+ 'provider_models',
18
+ 'kanban_lanes',
19
+ 'agent_call_logs',
20
+ ];
21
+
22
+ function printDatabase(db, label) {
23
+ console.log(`# ${label}`);
24
+
25
+ for (const row of getSchemaObjects(db)) {
26
+ console.log(`\n-- ${row.type} ${row.name} (${row.tbl_name})`);
27
+ console.log(row.sql || '');
28
+ }
29
+
30
+ for (const table of BASELINE_TABLES) {
31
+ const exists = db.prepare(
32
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?"
33
+ ).get(table);
34
+ if (!exists) continue;
35
+
36
+ console.log(`\n-- PRAGMA table_info(${table})`);
37
+ console.table(db.prepare(`PRAGMA table_info(${table})`).all());
38
+ console.log(`-- PRAGMA foreign_key_list(${table})`);
39
+ console.table(db.prepare(`PRAGMA foreign_key_list(${table})`).all());
40
+ console.log(`-- PRAGMA index_list(${table})`);
41
+ const indexes = db.prepare(`PRAGMA index_list(${table})`).all();
42
+ console.table(indexes);
43
+
44
+ for (const index of indexes) {
45
+ console.log(`-- PRAGMA index_info(${index.name})`);
46
+ console.table(db.prepare(`PRAGMA index_info(${index.name})`).all());
47
+ console.log(`-- PRAGMA index_xinfo(${index.name})`);
48
+ console.table(db.prepare(`PRAGMA index_xinfo(${index.name})`).all());
49
+ }
50
+ }
51
+ }
52
+
53
+ export function main(argv = process.argv.slice(2)) {
54
+ if (argv.includes('--fresh')) {
55
+ const fresh = createFreshBaselineDb();
56
+ try {
57
+ printDatabase(fresh.db, `fresh baseline ${fresh.dbPath}`);
58
+ } finally {
59
+ fresh.close();
60
+ }
61
+ return 0;
62
+ }
63
+
64
+ const dbPath = getActiveDbPath();
65
+ if (!existsSync(dbPath)) {
66
+ console.log(`No database found at ${dbPath}; use --fresh to inspect a fresh baseline.`);
67
+ return 0;
68
+ }
69
+
70
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
71
+ try {
72
+ printDatabase(db, `active database ${dbPath}`);
73
+ } finally {
74
+ db.close();
75
+ }
76
+ return 0;
77
+ }
78
+
79
+ if (import.meta.url === `file://${process.argv[1]}`) {
80
+ process.exit(main());
81
+ }
@@ -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
+ }
@@ -20,7 +20,7 @@ export class AgentCallLogger {
20
20
  const callId = nanoid();
21
21
 
22
22
  // Build metadata object - only include keys with defined values
23
- const metadata = {};
23
+ const metadata = { ...(meta.metadata || {}) };
24
24
  if (meta.effortLevel !== undefined && meta.effortLevel !== null) {
25
25
  metadata.effortLevel = meta.effortLevel;
26
26
  }
@@ -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;
@@ -0,0 +1,48 @@
1
+ import { commandButtons } from '../database.js';
2
+
3
+ /**
4
+ * Build Command API instructions for system prompt if the project has commands.
5
+ * @param {string} apiUrl - Base API URL
6
+ * @param {string} sessionId - Current session ID
7
+ * @param {string} projectId - Current project ID
8
+ * @returns {string} Command instructions or empty string if no commands configured
9
+ */
10
+ export function buildCommandButtonApiInstructions(apiUrl, sessionId, projectId) {
11
+ const buttons = commandButtons.getByProjectId(projectId);
12
+ if (!buttons || buttons.length === 0) {
13
+ return '';
14
+ }
15
+
16
+ return `## Commands API
17
+
18
+ This project has commands configured - reusable shell commands you can execute. Use the Bash tool to run these curl commands.
19
+
20
+ ### List Available Commands
21
+ \`\`\`bash
22
+ curl ${apiUrl}/api/sessions/${sessionId}/command-buttons
23
+ \`\`\`
24
+
25
+ ### Run a Command
26
+ \`\`\`bash
27
+ curl -X POST ${apiUrl}/api/sessions/${sessionId}/command-buttons/<button_id>/run
28
+ \`\`\`
29
+
30
+ Response: { runId, buttonId, status: "running", output: "" }
31
+
32
+ ### Check Run Status & Output
33
+ \`\`\`bash
34
+ curl ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs/<run_id>
35
+ \`\`\`
36
+
37
+ Response: { runId, buttonId, status, exitCode, output, startedAt, completedAt }
38
+
39
+ ### List Command Runs
40
+ \`\`\`bash
41
+ curl ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs
42
+ \`\`\`
43
+
44
+ ### Kill a Running Command
45
+ \`\`\`bash
46
+ curl -X POST ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs/<run_id>/kill
47
+ \`\`\``;
48
+ }
@@ -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);
@@ -112,6 +112,7 @@ function getOrCreateInitialMessage(session, options) {
112
112
  * @param {object} options
113
113
  * @param {string} [options.prompt] - Optional new prompt to use/override
114
114
  * @param {string} [options.model] - Optional model override
115
+ * @param {string|null} [options.providerId] - Optional provider override
115
116
  * @returns {Promise<object>} The updated session
116
117
  */
117
118
  export async function startDraft(session, options = {}) {
@@ -144,6 +145,7 @@ export async function startDraft(session, options = {}) {
144
145
  status: 'starting',
145
146
  pendingModel: null,
146
147
  ...(model ? { model, agentType } : {}),
148
+ ...(options.providerId !== undefined ? { providerId: options.providerId } : {}),
147
149
  });
148
150
 
149
151
  // Resolve skill/command invocations so skill body goes into system prompt
@@ -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
+ }