circuschief 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/package.json +1 -1
  2. package/packages/server/src/agents/adapters/CodexAdapter.js +5 -4
  3. package/packages/server/src/api/canvas.js +22 -57
  4. package/packages/server/src/api/index.js +2 -0
  5. package/packages/server/src/api/kanban.js +4 -2
  6. package/packages/server/src/api/projects-helpers.js +20 -3
  7. package/packages/server/src/api/projects-session-helpers.js +10 -4
  8. package/packages/server/src/api/projects.js +10 -0
  9. package/packages/server/src/api/providers.js +11 -1
  10. package/packages/server/src/api/sessions-commands.js +35 -17
  11. package/packages/server/src/api/sessions-lifecycle.js +10 -10
  12. package/packages/server/src/api/sessions-patch.js +4 -0
  13. package/packages/server/src/api/sessions.js +6 -5
  14. package/packages/server/src/database.js +0 -2
  15. package/packages/server/src/db/DatabaseManager.js +5 -1
  16. package/packages/server/src/db/ProjectDefaultsRepository.js +3 -3
  17. package/packages/server/src/db/ProviderRepository.js +87 -32
  18. package/packages/server/src/db/SessionRepository.js +1 -0
  19. package/packages/server/src/db/index.js +0 -3
  20. package/packages/server/src/db/migrations/index.js +36 -202
  21. package/packages/server/src/db/seedBaselineData.js +137 -0
  22. package/packages/server/src/db/session-helpers.js +6 -3
  23. package/packages/server/src/middleware/sessionLookup.js +81 -8
  24. package/packages/server/src/schema.sql +149 -132
  25. package/packages/server/src/scripts/backupDatabase.js +21 -0
  26. package/packages/server/src/scripts/dbUtils.js +81 -0
  27. package/packages/server/src/scripts/inspectDatabaseSchema.js +81 -0
  28. package/packages/server/src/scripts/validateDatabaseBaseline.js +212 -0
  29. package/packages/server/src/services/codexSpawnHelper.js +9 -0
  30. package/packages/server/src/services/commandButtonPrompts.js +8 -8
  31. package/packages/server/src/services/commandRunner.js +7 -1
  32. package/packages/server/src/services/e2eSpawnCapture.js +147 -0
  33. package/packages/server/src/services/gitCommitAttribution.js +120 -0
  34. package/packages/server/src/services/gitService.js +11 -2
  35. package/packages/server/src/services/gitSessionSetup.js +11 -1
  36. package/packages/server/src/services/kanbanTriggers.js +6 -3
  37. package/packages/server/src/services/nodeSpawnHelper.js +9 -0
  38. package/packages/server/src/services/prUrlService.js +3 -3
  39. package/packages/server/src/services/queryParamBuilder.js +90 -0
  40. package/packages/server/src/services/sessionDuplicator.js +1 -5
  41. package/packages/server/src/services/sessionExecution.js +56 -108
  42. package/packages/server/src/services/sessionPrompts.js +12 -47
  43. package/packages/server/src/services/sessionProvider.js +10 -0
  44. package/packages/server/src/services/summaryService.js +5 -3
  45. package/packages/server/src/services/summaryStaleCheck.js +23 -4
  46. package/packages/server/src/services/templateTriggerService.js +3 -1
  47. package/packages/shared/src/constants.js +3 -0
  48. package/packages/shared/src/contracts/commandButtons.js +16 -2
  49. package/packages/shared/src/contracts/projects.js +2 -2
  50. package/packages/shared/src/contracts/providers.js +60 -0
  51. package/packages/shared/src/contracts/sessions.js +2 -1
  52. package/packages/shared/src/contracts/templates.js +2 -2
  53. package/packages/shared/src/types.js +1 -9
  54. package/packages/shared/src/utils.js +2 -2
  55. package/packages/web/dist/assets/{ActiveSessionsView-UJsCILDL.js → ActiveSessionsView-B0XHqLmv.js} +1 -1
  56. package/packages/web/dist/assets/{AgentLogsView-BGFPLjLa.js → AgentLogsView-DmsjUMlB.js} +2 -2
  57. package/packages/web/dist/assets/ApiClient-C3ztI9s9.js +1 -0
  58. package/packages/web/dist/assets/ArchiveConfirmModal-BlCyn5Vt.js +1 -0
  59. package/packages/web/dist/assets/ArchiveConfirmModal-DeoCVGXt.css +1 -0
  60. package/packages/web/dist/assets/{CommandButtonDetailView-D8S258uP.js → CommandButtonDetailView-CdSCPp78.js} +1 -1
  61. package/packages/web/dist/assets/EffortLevelSelector-hc2MNKg6.js +1 -0
  62. package/packages/web/dist/assets/{GeneralSettingsView-DsHChEhv.js → GeneralSettingsView-D1nI8_zk.js} +1 -1
  63. package/packages/web/dist/assets/InputWithButton-CAkttyqx.js +1 -0
  64. package/packages/web/dist/assets/{InputWithButton-cYdrEmTs.css → InputWithButton-D9HMvfR5.css} +1 -1
  65. package/packages/web/dist/assets/{InterpolationHelp-CIkOSkWX.js → InterpolationHelp-BO1j9Z3_.js} +1 -1
  66. package/packages/web/dist/assets/MarkdownEditor-ucRAP_UM.js +2 -0
  67. package/packages/web/dist/assets/{ModelSelector-D8hbTRIt.css → ModelSelector-BSxKUSus.css} +1 -1
  68. package/packages/web/dist/assets/{ModelSelector-BMpR0DPr.js → ModelSelector-CwTz8ZWO.js} +1 -1
  69. package/packages/web/dist/assets/{NewSessionView-CUUdHkfv.css → NewSessionView-BDPb-1qr.css} +1 -1
  70. package/packages/web/dist/assets/NewSessionView-BsDrp8mj.js +3 -0
  71. package/packages/web/dist/assets/ProjectEditView-CwTOeSun.js +1 -0
  72. package/packages/web/dist/assets/{ProjectEditView-D9sK0fdH.css → ProjectEditView-J15mcsWz.css} +1 -1
  73. package/packages/web/dist/assets/{ProjectListView-B9FuWESY.js → ProjectListView-DcNyuINs.js} +1 -1
  74. package/packages/web/dist/assets/{ProjectNewView-D62jYlBL.js → ProjectNewView-B5YV62hv.js} +1 -1
  75. package/packages/web/dist/assets/ProvidersView-bZemq_Rv.css +1 -0
  76. package/packages/web/dist/assets/ProvidersView-nY9GnDdO.js +1 -0
  77. package/packages/web/dist/assets/QuickResponseSettings-B352c75l.css +1 -0
  78. package/packages/web/dist/assets/{QuickResponseSettings-CDm5vwP7.js → QuickResponseSettings-BQwQXuL7.js} +1 -1
  79. package/packages/web/dist/assets/{QuickResponsesPanel-DZ_Lre_l.js → QuickResponsesPanel-BzSYcCSP.js} +1 -1
  80. package/packages/web/dist/assets/{ResizableTextarea-DiIOEGjN.js → ResizableTextarea-B3YIdIXv.js} +1 -1
  81. package/packages/web/dist/assets/{SessionCard-DmjnVYWn.js → SessionCard-CjE1tXiT.js} +1 -1
  82. package/packages/web/dist/assets/SessionDetailView-3cPZrbS3.js +36 -0
  83. package/packages/web/dist/assets/SessionDetailView-CZRZMrfM.css +1 -0
  84. package/packages/web/dist/assets/{SessionFormOptions-DYUISplS.js → SessionFormOptions-B6AxyREh.js} +1 -1
  85. package/packages/web/dist/assets/SessionListView-B5_6gW49.css +1 -0
  86. package/packages/web/dist/assets/SessionListView-CLXBfLcq.js +1 -0
  87. package/packages/web/dist/assets/{SessionLogStream-DpUE6Xsh.js → SessionLogStream-LlZ3z_Xj.js} +1 -1
  88. package/packages/web/dist/assets/{SettingsView-BC055tIA.js → SettingsView-CTGiGvR2.js} +1 -1
  89. package/packages/web/dist/assets/{SlashCommandWizard-DmTyNG9O.js → SlashCommandWizard-Cy04d7-o.js} +1 -1
  90. package/packages/web/dist/assets/{SlashCommandWizard-Dn7sNaBd.css → SlashCommandWizard-DJzw3LP5.css} +1 -1
  91. package/packages/web/dist/assets/{SummarySettingsView-BgnRCwlq.js → SummarySettingsView-BR2ZjEa3.js} +1 -1
  92. package/packages/web/dist/assets/{TemplateDetailView-BlhOmLUX.js → TemplateDetailView-DH6Oswsp.js} +1 -1
  93. package/packages/web/dist/assets/{commandButtons-D4RPpLiu.js → commandButtons-BfqR-fqq.js} +1 -1
  94. package/packages/web/dist/assets/{index-CfL84oGW.js → index-1zziPL6l.js} +1 -1
  95. package/packages/web/dist/assets/{index-OfCywayk.js → index-7kzHPxSF.js} +1 -1
  96. package/packages/web/dist/assets/{index-PDesaJc6.js → index-B0N_obMc.js} +1 -1
  97. package/packages/web/dist/assets/{index-Cpy4-yv3.js → index-BNk_gdfI.js} +1 -1
  98. package/packages/web/dist/assets/{index-Cs2nxhrT.css → index-BY174HVJ.css} +1 -1
  99. package/packages/web/dist/assets/{index-9vb2KaAd.js → index-CSqaAH-0.js} +1 -1
  100. package/packages/web/dist/assets/{index-CNwkdB0T.js → index-C_q4WlK8.js} +1 -1
  101. package/packages/web/dist/assets/{index-B0CvZXuN.js → index-D1wpU4y0.js} +1 -1
  102. package/packages/web/dist/assets/{index-4rhEeO0B.js → index-D5zCA8sD.js} +1 -1
  103. package/packages/web/dist/assets/{index-CkmxO8Mm.js → index-DGR8ELWY.js} +1 -1
  104. package/packages/web/dist/assets/{index-CrAQJmoZ.js → index-DHga8pXo.js} +1 -1
  105. package/packages/web/dist/assets/{index-BUhvkAdF.js → index-DSby02Wl.js} +1 -1
  106. package/packages/web/dist/assets/{index-BGwH4Cfn.js → index-DgkC10TW.js} +3 -3
  107. package/packages/web/dist/assets/{index-DfrE0gAC.js → index-DqjXJTVI.js} +1 -1
  108. package/packages/web/dist/assets/{index-Bn5xdGFM.js → index-DtfUt785.js} +1 -1
  109. package/packages/web/dist/assets/{index-KwEyz0F3.js → index-_4S2uLDI.js} +1 -1
  110. package/packages/web/dist/assets/{index-B6G18FqB.js → index-fK8FIZgP.js} +15 -14
  111. package/packages/web/dist/assets/{index-BcnkUk2o.js → index-gmiZeFXN.js} +1 -1
  112. package/packages/web/dist/assets/{index-D6Ky9vJe.js → index-irD539ZM.js} +1 -1
  113. package/packages/web/dist/assets/{index-uB6nhSvz.js → index-yq-E1Y00.js} +1 -1
  114. package/packages/web/dist/assets/{projects-BUiOGmmb.js → projects-DXYQNJIi.js} +1 -1
  115. package/packages/web/dist/assets/{providers-Bh1ZiiJi.js → providers-1bnH-exJ.js} +1 -1
  116. package/packages/web/dist/assets/sessions-6zGUlFrt.js +1 -0
  117. package/packages/web/dist/assets/{settings-Z4AVVmkJ.js → settings-MbfRir0d.js} +1 -1
  118. package/packages/web/dist/index.html +2 -2
  119. package/packages/server/src/api/sessions-notes.js +0 -51
  120. package/packages/server/src/db/SessionNoteRepository.js +0 -60
  121. package/packages/server/src/db/migrations/canvasItemsMigrations.js +0 -109
  122. package/packages/server/src/db/migrations/conversationsMigrations.js +0 -187
  123. package/packages/server/src/db/migrations/kanbanMigrations.js +0 -99
  124. package/packages/server/src/db/migrations/miscMigrations.js +0 -369
  125. package/packages/server/src/db/migrations/projectsMigrations.js +0 -99
  126. package/packages/server/src/db/migrations/sessionsMigrations.js +0 -287
  127. package/packages/web/dist/assets/ApiClient-B4YTtyY4.js +0 -1
  128. package/packages/web/dist/assets/ArchiveConfirmModal-BQ-4gI0R.css +0 -1
  129. package/packages/web/dist/assets/ArchiveConfirmModal-OFaj_uX5.js +0 -1
  130. package/packages/web/dist/assets/EffortLevelSelector-C2378L8e.js +0 -1
  131. package/packages/web/dist/assets/InputWithButton-Ci15ox0a.js +0 -1
  132. package/packages/web/dist/assets/MarkdownEditor-5-bexzUT.js +0 -2
  133. package/packages/web/dist/assets/NewSessionView-BCqtIgWH.js +0 -3
  134. package/packages/web/dist/assets/ProjectEditView-RFaxHhAX.js +0 -1
  135. package/packages/web/dist/assets/ProvidersView-DDKMIQWZ.js +0 -1
  136. package/packages/web/dist/assets/ProvidersView-DE82G_5W.css +0 -1
  137. package/packages/web/dist/assets/QuickResponseSettings-B8188A1D.css +0 -1
  138. package/packages/web/dist/assets/SessionDetailView-CL7nmfiB.js +0 -36
  139. package/packages/web/dist/assets/SessionDetailView-CupIkI7u.css +0 -1
  140. package/packages/web/dist/assets/SessionListView-BcxGz4aC.js +0 -1
  141. package/packages/web/dist/assets/SessionListView-fHlQyecX.css +0 -1
  142. package/packages/web/dist/assets/sessions-DH1R-NhV.js +0 -1
@@ -0,0 +1,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
+ }
@@ -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,16 +13,16 @@ export function buildCommandButtonApiInstructions(apiUrl, sessionId, projectId)
13
13
  return '';
14
14
  }
15
15
 
16
- return `## Command Buttons API
16
+ return `## Commands API
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 commands configured - reusable shell commands you can execute. Use the Bash tool to run these curl commands.
19
19
 
20
- ### List Available Buttons
20
+ ### List Available Commands
21
21
  \`\`\`bash
22
- curl ${apiUrl}/api/projects/${projectId}/command-buttons
22
+ curl ${apiUrl}/api/sessions/${sessionId}/command-buttons
23
23
  \`\`\`
24
24
 
25
- ### Run a Button
25
+ ### Run a Command
26
26
  \`\`\`bash
27
27
  curl -X POST ${apiUrl}/api/sessions/${sessionId}/command-buttons/<button_id>/run
28
28
  \`\`\`
@@ -36,7 +36,7 @@ curl ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs/<run_id>
36
36
 
37
37
  Response: { runId, buttonId, status, exitCode, output, startedAt, completedAt }
38
38
 
39
- ### List All Runs for This Session
39
+ ### List Command Runs
40
40
  \`\`\`bash
41
41
  curl ${apiUrl}/api/sessions/${sessionId}/command-buttons/runs
42
42
  \`\`\`
@@ -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
+ }