circuschief 0.1.4 → 0.2.1
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.
- package/package.json +1 -1
- package/packages/server/src/api/commands.js +50 -55
- package/packages/server/src/api/projects-helpers.js +13 -4
- package/packages/server/src/api/projects.js +33 -18
- package/packages/server/src/cli.js +82 -0
- package/packages/server/src/db/AgentCallLogRepository.js +30 -31
- package/packages/server/src/db/ConversationRepository.js +27 -16
- package/packages/server/src/db/ProjectRepository.js +21 -31
- package/packages/server/src/db/QuickResponseRepository.js +14 -19
- package/packages/server/src/db/migrations/sessionsMigrations.js +61 -61
- package/packages/server/src/index.js +42 -29
- package/packages/server/src/services/commandRunner.js +52 -99
- package/packages/server/src/services/kanbanTriggers.js +83 -56
- package/packages/server/src/services/schedulerService.js +68 -44
- package/packages/server/src/services/sessionExecution.js +102 -61
- package/packages/server/src/services/sessionManager.js +63 -37
- package/packages/server/src/services/summaryService.js +56 -53
- package/packages/server/src/services/templateTriggerService.js +58 -31
- package/packages/server/src/ws/WebSocketManager.js +5 -0
- package/packages/web/dist/assets/ActiveSessionsView-3697sD8N.js +1 -0
- package/packages/web/dist/assets/ActiveSessionsView-DfYXc6dz.css +1 -0
- package/packages/web/dist/assets/{AgentLogsView-c42v_j_5.js → AgentLogsView-D4l0N9ZA.js} +1 -1
- package/packages/web/dist/assets/{ArchiveConfirmModal-DBuOmtXu.js → ArchiveConfirmModal-Bv3vGOMM.js} +1 -1
- package/packages/web/dist/assets/{CommandButtonDetailView-CkKJ3Htz.js → CommandButtonDetailView-Bk_SHxpu.js} +1 -1
- package/packages/web/dist/assets/{EffortLevelSelector-BHJHSqul.js → EffortLevelSelector-VfBEelvO.js} +1 -1
- package/packages/web/dist/assets/{GeneralSettingsView-CdxfteZ2.js → GeneralSettingsView-BqCzCX-z.js} +1 -1
- package/packages/web/dist/assets/{InterpolationHelp-DabnHhE4.js → InterpolationHelp-Dc1Y0T6v.js} +1 -1
- package/packages/web/dist/assets/MarkdownEditor-DwBQkZbs.js +2 -0
- package/packages/web/dist/assets/{ModelSelector-BWIU4ud7.js → ModelSelector-DSxaZWBL.js} +1 -1
- package/packages/web/dist/assets/{NewSessionView-BIZl8QlH.js → NewSessionView-BsI7JtO9.js} +2 -2
- package/packages/web/dist/assets/{PathChooser-nhat_Pz4.js → PathChooser-CXFxb8Oj.js} +1 -1
- package/packages/web/dist/assets/{ProjectEditView-DD-2_VrW.js → ProjectEditView-Bes4Mib4.js} +1 -1
- package/packages/web/dist/assets/{ProjectListView-BOWbfoXQ.js → ProjectListView-DzEu-C36.js} +1 -1
- package/packages/web/dist/assets/{ProjectNewView-DC4uvSn2.js → ProjectNewView-Cv-iEAgl.js} +1 -1
- package/packages/web/dist/assets/ProvidersView-CgAr0qms.js +1 -0
- package/packages/web/dist/assets/{QuickResponseSettings-Bk9mq96x.js → QuickResponseSettings-uDDpwaza.js} +1 -1
- package/packages/web/dist/assets/{QuickResponsesPanel-BRvcnkQr.js → QuickResponsesPanel-D0qs0Fm_.js} +1 -1
- package/packages/web/dist/assets/{ResizableTextarea-CwGM4P3c.js → ResizableTextarea-_kHi1Mg3.js} +1 -1
- package/packages/web/dist/assets/{SessionCard-BGDVHU9u.js → SessionCard-Be1-bK0C.js} +1 -1
- package/packages/web/dist/assets/{SessionCard-D20G3bX8.css → SessionCard-CcqIjL8q.css} +1 -1
- package/packages/web/dist/assets/{SessionDetailView-CHYrx2Ab.js → SessionDetailView-DUYb7qTA.js} +17 -17
- package/packages/web/dist/assets/{SessionDetailView-7bWgC7Es.css → SessionDetailView-mnGRMaLY.css} +1 -1
- package/packages/web/dist/assets/{SessionFormOptions-8qvL25ca.js → SessionFormOptions-DvhOyP6z.js} +1 -1
- package/packages/web/dist/assets/{SessionListView-BAIBtJF7.css → SessionListView-78k6TTz6.css} +1 -1
- package/packages/web/dist/assets/SessionListView-CuHsWj85.js +1 -0
- package/packages/web/dist/assets/{SessionLogStream-B-w3n4c3.js → SessionLogStream-Da_GniUZ.js} +1 -1
- package/packages/web/dist/assets/{SettingsView-Dd0ZJ4Nv.js → SettingsView-5RDCXNUa.js} +1 -1
- package/packages/web/dist/assets/{SlashCommandWizard-CzyLjsdJ.js → SlashCommandWizard-B_8ifpxN.js} +1 -1
- package/packages/web/dist/assets/{SummarySettingsView-DTbh7uAF.js → SummarySettingsView-KvgSGHdd.js} +1 -1
- package/packages/web/dist/assets/{TemplateDetailView-BOnhkdtH.js → TemplateDetailView-BhOjYIvS.js} +1 -1
- package/packages/web/dist/assets/{commandButtons-CY87n64i.js → commandButtons-B4OYZP0J.js} +1 -1
- package/packages/web/dist/assets/{index-DxboI9i-.js → index-80Qu7W6P.js} +1 -1
- package/packages/web/dist/assets/{index-NzLFVaCi.js → index-B8_Iqwcq.js} +1 -1
- package/packages/web/dist/assets/{index-aCw-iXPX.js → index-B9JErft2.js} +1 -1
- package/packages/web/dist/assets/{index-Ce6sL47U.js → index-BHVnr8MO.js} +1 -1
- package/packages/web/dist/assets/{index-BXUcbV4K.js → index-BarVnQIj.js} +1 -1
- package/packages/web/dist/assets/{index--OtPwBbF.js → index-BqVgX_Jy.js} +3 -3
- package/packages/web/dist/assets/{index-gMpnPf1V.js → index-BsvRdU0B.js} +1 -1
- package/packages/web/dist/assets/{index-BRUlEEHm.js → index-Bugg2M-E.js} +1 -1
- package/packages/web/dist/assets/{index-Dx0sYW7H.js → index-C2Pjy-M8.js} +1 -1
- package/packages/web/dist/assets/{index-DkLkDgig.js → index-CSOPrlmq.js} +23 -23
- package/packages/web/dist/assets/{index-CO4EBOFw.js → index-CS_wb_Vj.js} +1 -1
- package/packages/web/dist/assets/{index-CjHb9rXv.js → index-ClzNIdCp.js} +1 -1
- package/packages/web/dist/assets/{index-DPwwgloE.js → index-Cn9Ajkye.js} +1 -1
- package/packages/web/dist/assets/{index-i1o916sk.js → index-CucpVX4L.js} +1 -1
- package/packages/web/dist/assets/{index-C6m-WfqP.js → index-D9hZYvW3.js} +1 -1
- package/packages/web/dist/assets/{index-jGjvGBfk.js → index-DA0dK_PG.js} +1 -1
- package/packages/web/dist/assets/{index-Bi4bQ_UB.js → index-DgpSn-jR.js} +1 -1
- package/packages/web/dist/assets/{index-DcA6pqXV.js → index-HZwIyC9t.js} +1 -1
- package/packages/web/dist/assets/{index-BshkV3r5.js → index-SS3wA2sI.js} +1 -1
- package/packages/web/dist/assets/{projects-C2Y29PSJ.js → projects-B2du-GX8.js} +1 -1
- package/packages/web/dist/assets/{providers-CeJXuo0Q.js → providers-B__J6FX0.js} +1 -1
- package/packages/web/dist/assets/sessions-VDrd87yA.js +1 -0
- package/packages/web/dist/assets/{settings-BplIxCbi.js → settings-CZ7Pc-Pt.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/assets/ActiveSessionsView-BryJ-V3f.js +0 -1
- package/packages/web/dist/assets/ActiveSessionsView-ofSvx-K1.css +0 -1
- package/packages/web/dist/assets/MarkdownEditor-k4zBLGqU.js +0 -2
- package/packages/web/dist/assets/ProvidersView-DT5afh1V.js +0 -1
- package/packages/web/dist/assets/SessionListView-927Yq6Il.js +0 -1
- package/packages/web/dist/assets/sessions-CMby7ij3.js +0 -1
|
@@ -10,6 +10,65 @@ const TABLE_SESSIONS = 'sessions';
|
|
|
10
10
|
// Column type constants
|
|
11
11
|
const COL_INTEGER_DEFAULT_0 = 'INTEGER DEFAULT 0';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* SQL column definition for the sessions table with updated status CHECK constraint.
|
|
15
|
+
*/
|
|
16
|
+
const SESSIONS_BASE_COLUMNS = `
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
19
|
+
name TEXT NOT NULL,
|
|
20
|
+
status TEXT NOT NULL DEFAULT 'starting' CHECK (status IN ('starting', 'running', 'waiting', 'stopped', 'completed', 'error', 'scheduled')),
|
|
21
|
+
mode TEXT NOT NULL DEFAULT 'standard' CHECK (mode IN ('plan', 'standard', 'yolo')),
|
|
22
|
+
thinking_enabled INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
git_branch TEXT,
|
|
24
|
+
git_worktree TEXT,
|
|
25
|
+
pr_url TEXT,
|
|
26
|
+
error TEXT,
|
|
27
|
+
effort_level TEXT CHECK(effort_level IN ('low', 'medium', 'high', 'max', 'auto')),
|
|
28
|
+
cost_usd REAL DEFAULT 0,
|
|
29
|
+
claude_session_id TEXT,
|
|
30
|
+
model TEXT,
|
|
31
|
+
next_template_id TEXT REFERENCES session_templates(id) ON DELETE SET NULL,
|
|
32
|
+
parent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
|
|
33
|
+
input_tokens INTEGER DEFAULT 0,
|
|
34
|
+
output_tokens INTEGER DEFAULT 0,
|
|
35
|
+
cache_read_input_tokens INTEGER DEFAULT 0,
|
|
36
|
+
cache_creation_input_tokens INTEGER DEFAULT 0,
|
|
37
|
+
web_search_requests INTEGER DEFAULT 0,
|
|
38
|
+
context_window INTEGER DEFAULT 200000,
|
|
39
|
+
archived INTEGER NOT NULL DEFAULT 0,
|
|
40
|
+
starred INTEGER NOT NULL DEFAULT 0,
|
|
41
|
+
manually_named INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
scheduled_at INTEGER DEFAULT NULL,
|
|
43
|
+
reschedule_delay_minutes INTEGER DEFAULT 15,
|
|
44
|
+
auto_reschedule_enabled INTEGER DEFAULT 0,
|
|
45
|
+
reschedule_on_token_limit INTEGER DEFAULT 1,
|
|
46
|
+
reschedule_on_service_error INTEGER DEFAULT 1,
|
|
47
|
+
max_reschedule_count INTEGER DEFAULT NULL,
|
|
48
|
+
max_total_tokens INTEGER DEFAULT NULL,
|
|
49
|
+
reschedule_count INTEGER DEFAULT 0,
|
|
50
|
+
reschedule_at_token_count INTEGER DEFAULT NULL,
|
|
51
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
52
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* All possible column names that may exist in the sessions table for migration SELECT.
|
|
57
|
+
*/
|
|
58
|
+
const SESSIONS_ALL_COLUMNS = [
|
|
59
|
+
'id', 'project_id', 'name', 'status', 'mode', 'thinking_enabled',
|
|
60
|
+
'git_branch', 'git_worktree', 'pr_url', 'error', 'effort_level',
|
|
61
|
+
'cost_usd', 'claude_session_id', 'model', 'next_template_id',
|
|
62
|
+
'parent_session_id', 'input_tokens', 'output_tokens',
|
|
63
|
+
'cache_read_input_tokens', 'cache_creation_input_tokens',
|
|
64
|
+
'web_search_requests', 'context_window', 'archived', 'starred',
|
|
65
|
+
'manually_named', 'scheduled_at', 'reschedule_delay_minutes',
|
|
66
|
+
'auto_reschedule_enabled', 'reschedule_on_token_limit',
|
|
67
|
+
'reschedule_on_service_error', 'max_reschedule_count',
|
|
68
|
+
'max_total_tokens', 'reschedule_count', 'reschedule_at_token_count',
|
|
69
|
+
'created_at', 'updated_at',
|
|
70
|
+
];
|
|
71
|
+
|
|
13
72
|
/**
|
|
14
73
|
* Migrate sessions table to include 'stopped' and 'scheduled' in status CHECK constraint.
|
|
15
74
|
* SQLite doesn't support ALTER TABLE to modify constraints, so we recreate the table.
|
|
@@ -22,76 +81,17 @@ function migrateSessionsStatusConstraint(db) {
|
|
|
22
81
|
return;
|
|
23
82
|
}
|
|
24
83
|
|
|
25
|
-
// Get all columns from the current table to preserve data
|
|
26
84
|
const columnNames = getColumns(db, TABLE_SESSIONS);
|
|
27
|
-
|
|
28
|
-
const baseColumns = `
|
|
29
|
-
id TEXT PRIMARY KEY,
|
|
30
|
-
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
31
|
-
name TEXT NOT NULL,
|
|
32
|
-
status TEXT NOT NULL DEFAULT 'starting' CHECK (status IN ('starting', 'running', 'waiting', 'stopped', 'completed', 'error', 'scheduled')),
|
|
33
|
-
mode TEXT NOT NULL DEFAULT 'standard' CHECK (mode IN ('plan', 'standard', 'yolo')),
|
|
34
|
-
thinking_enabled INTEGER NOT NULL DEFAULT 0,
|
|
35
|
-
git_branch TEXT,
|
|
36
|
-
git_worktree TEXT,
|
|
37
|
-
pr_url TEXT,
|
|
38
|
-
error TEXT,
|
|
39
|
-
effort_level TEXT CHECK(effort_level IN ('low', 'medium', 'high', 'max', 'auto')),
|
|
40
|
-
cost_usd REAL DEFAULT 0,
|
|
41
|
-
claude_session_id TEXT,
|
|
42
|
-
model TEXT,
|
|
43
|
-
next_template_id TEXT REFERENCES session_templates(id) ON DELETE SET NULL,
|
|
44
|
-
parent_session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL,
|
|
45
|
-
input_tokens INTEGER DEFAULT 0,
|
|
46
|
-
output_tokens INTEGER DEFAULT 0,
|
|
47
|
-
cache_read_input_tokens INTEGER DEFAULT 0,
|
|
48
|
-
cache_creation_input_tokens INTEGER DEFAULT 0,
|
|
49
|
-
web_search_requests INTEGER DEFAULT 0,
|
|
50
|
-
context_window INTEGER DEFAULT 200000,
|
|
51
|
-
archived INTEGER NOT NULL DEFAULT 0,
|
|
52
|
-
starred INTEGER NOT NULL DEFAULT 0,
|
|
53
|
-
manually_named INTEGER NOT NULL DEFAULT 0,
|
|
54
|
-
scheduled_at INTEGER DEFAULT NULL,
|
|
55
|
-
reschedule_delay_minutes INTEGER DEFAULT 15,
|
|
56
|
-
auto_reschedule_enabled INTEGER DEFAULT 0,
|
|
57
|
-
reschedule_on_token_limit INTEGER DEFAULT 1,
|
|
58
|
-
reschedule_on_service_error INTEGER DEFAULT 1,
|
|
59
|
-
max_reschedule_count INTEGER DEFAULT NULL,
|
|
60
|
-
max_total_tokens INTEGER DEFAULT NULL,
|
|
61
|
-
reschedule_count INTEGER DEFAULT 0,
|
|
62
|
-
reschedule_at_token_count INTEGER DEFAULT NULL,
|
|
63
|
-
created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
64
|
-
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
65
|
-
`;
|
|
66
|
-
|
|
67
|
-
const selectColumns = [
|
|
68
|
-
'id', 'project_id', 'name', 'status', 'mode', 'thinking_enabled',
|
|
69
|
-
'git_branch', 'git_worktree', 'pr_url', 'error', 'effort_level',
|
|
70
|
-
'cost_usd', 'claude_session_id', 'model', 'next_template_id',
|
|
71
|
-
'parent_session_id', 'input_tokens', 'output_tokens',
|
|
72
|
-
'cache_read_input_tokens', 'cache_creation_input_tokens',
|
|
73
|
-
'web_search_requests', 'context_window', 'archived', 'starred',
|
|
74
|
-
'manually_named', 'scheduled_at', 'reschedule_delay_minutes',
|
|
75
|
-
'auto_reschedule_enabled', 'reschedule_on_token_limit',
|
|
76
|
-
'reschedule_on_service_error', 'max_reschedule_count',
|
|
77
|
-
'max_total_tokens', 'reschedule_count', 'reschedule_at_token_count',
|
|
78
|
-
'created_at', 'updated_at',
|
|
79
|
-
]
|
|
85
|
+
const selectColumns = SESSIONS_ALL_COLUMNS
|
|
80
86
|
.filter((col) => columnNames.includes(col))
|
|
81
87
|
.join(', ');
|
|
82
88
|
|
|
83
89
|
db.exec(`
|
|
84
|
-
CREATE TABLE sessions_new (
|
|
85
|
-
${baseColumns}
|
|
86
|
-
);
|
|
87
|
-
|
|
90
|
+
CREATE TABLE sessions_new (${SESSIONS_BASE_COLUMNS});
|
|
88
91
|
INSERT INTO sessions_new (${selectColumns})
|
|
89
92
|
SELECT ${selectColumns} FROM sessions;
|
|
90
|
-
|
|
91
93
|
DROP TABLE sessions;
|
|
92
|
-
|
|
93
94
|
ALTER TABLE sessions_new RENAME TO sessions;
|
|
94
|
-
|
|
95
95
|
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
96
96
|
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
|
97
97
|
CREATE INDEX IF NOT EXISTS idx_sessions_archived ON sessions(archived);
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
|
-
import { parseArgs } from 'node:util';
|
|
3
2
|
import { execSync } from 'child_process';
|
|
4
3
|
import { createApp } from './app.js';
|
|
5
4
|
import { initDatabase } from './database.js';
|
|
6
|
-
import { initWebSocket } from './websocket.js';
|
|
7
|
-
import {
|
|
5
|
+
import { initWebSocket, webSocketManager } from './websocket.js';
|
|
6
|
+
import { parseCliOptions } from './cli.js';
|
|
7
|
+
import { settings } from './db/index.js';
|
|
8
8
|
import * as prStatusService from './services/prStatusService.js';
|
|
9
9
|
import * as systemMonitor from './services/systemMonitor.js';
|
|
10
10
|
import { schedulerService } from './services/schedulerService.js';
|
|
11
11
|
import * as sessionManager from './services/sessionManager.js';
|
|
12
|
+
import { clearScheduledTimers } from './services/summaryService.js';
|
|
13
|
+
import { commandRunner } from './services/commandRunner.js';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Validate Node.js environment at startup.
|
|
@@ -27,17 +29,7 @@ function validateNodeEnvironment() {
|
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
const {
|
|
31
|
-
options: {
|
|
32
|
-
port: {
|
|
33
|
-
type: 'string',
|
|
34
|
-
short: 'p',
|
|
35
|
-
default: String(DEFAULT_SERVER_PORT),
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const port = parseInt(values.port, 10);
|
|
32
|
+
const { port, disableAnalytics } = parseCliOptions();
|
|
41
33
|
process.env.PORT = String(port);
|
|
42
34
|
const production = process.env.NODE_ENV === 'production';
|
|
43
35
|
const dbPath = process.env.DB_PATH || 'circuschief.db';
|
|
@@ -57,6 +49,12 @@ validateNodeEnvironment();
|
|
|
57
49
|
initDatabase(dbPath);
|
|
58
50
|
console.log(`Database initialized: ${dbPath}`);
|
|
59
51
|
|
|
52
|
+
// Apply --no-analytics flag to persisted settings
|
|
53
|
+
if (disableAnalytics) {
|
|
54
|
+
settings.setGeneralSettings({ disableAnalytics: true });
|
|
55
|
+
console.log('Analytics disabled via --no-analytics flag');
|
|
56
|
+
}
|
|
57
|
+
|
|
60
58
|
// Create Express app
|
|
61
59
|
const app = createApp({ production });
|
|
62
60
|
|
|
@@ -77,30 +75,45 @@ prStatusService.start();
|
|
|
77
75
|
systemMonitor.start();
|
|
78
76
|
|
|
79
77
|
// Graceful shutdown
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
let shuttingDown = false;
|
|
79
|
+
function shutdown(signal) {
|
|
80
|
+
if (shuttingDown) return;
|
|
81
|
+
shuttingDown = true;
|
|
82
|
+
console.log(`${signal} received, shutting down gracefully`);
|
|
83
|
+
|
|
84
|
+
// Safety net: force exit after 5 seconds
|
|
85
|
+
const forceTimeout = setTimeout(() => {
|
|
86
|
+
console.error('Graceful shutdown timed out, forcing exit');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}, 5000);
|
|
89
|
+
forceTimeout.unref();
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
console.log('SIGINT received, shutting down gracefully');
|
|
91
|
+
// Stop periodic services
|
|
93
92
|
schedulerService.stop();
|
|
94
93
|
prStatusService.stop();
|
|
95
94
|
systemMonitor.stop();
|
|
95
|
+
|
|
96
|
+
// Clear dangling timers from summary service
|
|
97
|
+
clearScheduledTimers();
|
|
98
|
+
|
|
99
|
+
// Kill child processes spawned by commandRunner
|
|
100
|
+
commandRunner.shutdownAll();
|
|
101
|
+
|
|
102
|
+
// Close all WebSocket connections (must happen before server.close())
|
|
103
|
+
webSocketManager.close();
|
|
104
|
+
|
|
105
|
+
// Close HTTP server (now unblocked since WS clients are terminated)
|
|
96
106
|
server.close(() => {
|
|
97
107
|
console.log('Server closed');
|
|
98
108
|
process.exit(0);
|
|
99
109
|
});
|
|
100
|
-
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
113
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
101
114
|
|
|
102
115
|
// Start server on all interfaces
|
|
103
116
|
server.listen(port, '0.0.0.0', () => {
|
|
104
|
-
console.log(`
|
|
105
|
-
console.log(`WebSocket available at ws://
|
|
117
|
+
console.log(`Circus Chief running on http://localhost:${port}`);
|
|
118
|
+
console.log(`WebSocket available at ws://localhost:${port}/ws`);
|
|
106
119
|
});
|
|
@@ -33,11 +33,10 @@ export class CommandRunner {
|
|
|
33
33
|
* Wrap command with platform-specific TTY allocation.
|
|
34
34
|
*/
|
|
35
35
|
#wrapCommandForPlatform(command) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return `script -q /dev/null sh -c ${JSON.stringify(command)}`;
|
|
36
|
+
const cmd = JSON.stringify(command);
|
|
37
|
+
return platform() === 'linux'
|
|
38
|
+
? `script -q -e -c ${cmd} /dev/null`
|
|
39
|
+
: `script -q /dev/null sh -c ${cmd}`;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
/**
|
|
@@ -62,8 +61,7 @@ export class CommandRunner {
|
|
|
62
61
|
*/
|
|
63
62
|
#flushOutputBuffer(entryInput, runId) {
|
|
64
63
|
const entry = entryInput;
|
|
65
|
-
if (!entry.outputBuffer) return;
|
|
66
|
-
if (!entry.sessionId || !entry.buttonId) return;
|
|
64
|
+
if (!entry.outputBuffer || !entry.sessionId || !entry.buttonId) return;
|
|
67
65
|
if (!commandRuns || typeof commandRuns.appendOutput !== 'function') return;
|
|
68
66
|
try {
|
|
69
67
|
commandRuns.appendOutput(runId, entry.outputBuffer);
|
|
@@ -104,21 +102,31 @@ export class CommandRunner {
|
|
|
104
102
|
|
|
105
103
|
this.processes.delete(runId);
|
|
106
104
|
if (onComplete) onComplete(exitCode, entry.output);
|
|
107
|
-
//
|
|
108
|
-
// - Exit codes >128 indicate signal termination (convention: 128 + signal number)
|
|
109
|
-
// - Normalizing to 1 simplifies error handling for consumers
|
|
110
|
-
// - The signal information is already logged above for debugging
|
|
105
|
+
// Normalize to 1 on signal termination (signal info already logged above)
|
|
111
106
|
return exitCode ?? 1;
|
|
112
107
|
}
|
|
113
108
|
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
109
|
+
/** Handle a run failure (process error or setup exception). Flushes output, marks failed, resolves. */
|
|
110
|
+
#handleRunFailure({ entry: entryParam, runId, err, onError, resolve }) {
|
|
111
|
+
const entry = entryParam;
|
|
112
|
+
let errorOutput = `[Error] Failed to execute command: ${err.message}`;
|
|
113
|
+
if (entry) {
|
|
114
|
+
const remaining = entry.outputProcessor.flush();
|
|
115
|
+
if (remaining) { entry.output += remaining; entry.outputBuffer += remaining; }
|
|
116
|
+
this.#flushOutputBuffer(entry, runId);
|
|
117
|
+
errorOutput = entry.output;
|
|
118
|
+
}
|
|
119
|
+
const msg = entry ? `Failed to execute command: ${err.message}` : `Error running command: ${err.message}`;
|
|
120
|
+
console.error(`[commandRunner.run] Error for runId: ${runId}`, err);
|
|
121
|
+
if (onError) onError(msg);
|
|
122
|
+
if (commandRuns && typeof commandRuns.complete === 'function') {
|
|
123
|
+
try { commandRuns.complete(runId, 1, errorOutput); } catch (dbErr) { console.warn(`[commandRunner.run] DB error for runId: ${runId}`, dbErr.message); }
|
|
124
|
+
}
|
|
125
|
+
this.processes.delete(runId);
|
|
126
|
+
resolve(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Run a command and stream output via callback. Returns exit code. */
|
|
122
130
|
async run(params, callbacks = {}, metadata = {}) {
|
|
123
131
|
const { runId, command, workingDirectory } = params;
|
|
124
132
|
const { onOutput, onComplete, onError } = callbacks;
|
|
@@ -139,7 +147,6 @@ export class CommandRunner {
|
|
|
139
147
|
const entry = this.#createProcessEntry(child, sessionId, buttonId);
|
|
140
148
|
this.processes.set(runId, entry);
|
|
141
149
|
|
|
142
|
-
// Buffer timer management
|
|
143
150
|
const clearBufferTimer = () => {
|
|
144
151
|
if (entry.bufferFlushTimer) {
|
|
145
152
|
clearInterval(entry.bufferFlushTimer);
|
|
@@ -148,7 +155,6 @@ export class CommandRunner {
|
|
|
148
155
|
};
|
|
149
156
|
entry.bufferFlushTimer = setInterval(() => this.#flushOutputBuffer(entry, runId), this.outputBufferFlushInterval);
|
|
150
157
|
|
|
151
|
-
// Data handler for both stdout and stderr
|
|
152
158
|
const handleData = (data) => {
|
|
153
159
|
const text = entry.outputProcessor.process(data.toString());
|
|
154
160
|
if (text) {
|
|
@@ -163,20 +169,7 @@ export class CommandRunner {
|
|
|
163
169
|
|
|
164
170
|
child.on('error', (err) => {
|
|
165
171
|
clearBufferTimer();
|
|
166
|
-
|
|
167
|
-
if (remainingText) {
|
|
168
|
-
entry.output += remainingText;
|
|
169
|
-
entry.outputBuffer += remainingText;
|
|
170
|
-
}
|
|
171
|
-
this.#flushOutputBuffer(entry, runId);
|
|
172
|
-
const msg = `Failed to execute command: ${err.message}`;
|
|
173
|
-
console.error(`[commandRunner.run] Error for runId: ${runId}`, err);
|
|
174
|
-
if (onError) onError(msg);
|
|
175
|
-
if (commandRuns && typeof commandRuns.complete === 'function') {
|
|
176
|
-
try { commandRuns.complete(runId, 1, entry.output); } catch (dbErr) { console.warn('[commandRunner.run] Warning: Error completing run in database for runId:', runId, dbErr.message); }
|
|
177
|
-
}
|
|
178
|
-
this.processes.delete(runId);
|
|
179
|
-
resolve(1);
|
|
172
|
+
this.#handleRunFailure({ entry, runId, err, onError, resolve });
|
|
180
173
|
});
|
|
181
174
|
|
|
182
175
|
child.on('close', (exitCode, signal) => {
|
|
@@ -190,28 +183,12 @@ export class CommandRunner {
|
|
|
190
183
|
resolve(this.#handleProcessClose({ entry, runId, exitCode, signal }, onComplete));
|
|
191
184
|
});
|
|
192
185
|
} catch (err) {
|
|
193
|
-
|
|
194
|
-
console.error(`[commandRunner.run] Exception for runId: ${runId}`, err);
|
|
195
|
-
if (onError) onError(msg);
|
|
196
|
-
// Mark as error in database (if available) and persist the error message
|
|
197
|
-
if (commandRuns && typeof commandRuns.complete === 'function') {
|
|
198
|
-
try {
|
|
199
|
-
commandRuns.complete(runId, 1, `[Error] ${msg}`);
|
|
200
|
-
} catch (dbErr) {
|
|
201
|
-
console.warn(`[commandRunner.run] Warning: Error marking failed run in database for runId: ${runId}`, dbErr.message);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
this.processes.delete(runId);
|
|
205
|
-
resolve(1);
|
|
186
|
+
this.#handleRunFailure({ entry: null, runId, err, onError, resolve });
|
|
206
187
|
}
|
|
207
188
|
});
|
|
208
189
|
}
|
|
209
190
|
|
|
210
|
-
/**
|
|
211
|
-
* Kill a running process
|
|
212
|
-
* @param {string} runId
|
|
213
|
-
* @returns {boolean} True if process was killed, false if not found
|
|
214
|
-
*/
|
|
191
|
+
/** Kill a running process. Returns true if killed, false if not found. */
|
|
215
192
|
kill(runId) {
|
|
216
193
|
const entry = this.processes.get(runId);
|
|
217
194
|
if (!entry) {
|
|
@@ -234,22 +211,10 @@ export class CommandRunner {
|
|
|
234
211
|
|
|
235
212
|
// Give it a moment to terminate gracefully, then force kill
|
|
236
213
|
setTimeout(() => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
process.kill(-pid, 'SIGKILL');
|
|
242
|
-
} catch (e) {
|
|
243
|
-
// Fallback to killing just the process if process group kill fails
|
|
244
|
-
try {
|
|
245
|
-
entry.process.kill('SIGKILL');
|
|
246
|
-
} catch (err) {
|
|
247
|
-
// Process may have already exited
|
|
248
|
-
console.log(`[commandRunner.kill] SIGKILL failed, process may have exited: ${err.message}`);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
} else {
|
|
252
|
-
console.log(`[commandRunner.kill] Process already exited for runId: ${runId}`);
|
|
214
|
+
if (!this.processes.has(runId)) return;
|
|
215
|
+
console.log(`[commandRunner.kill] Process still running, sending SIGKILL to runId: ${runId}`);
|
|
216
|
+
try { process.kill(-pid, 'SIGKILL'); } catch {
|
|
217
|
+
try { entry.process.kill('SIGKILL'); } catch { /* already dead */ }
|
|
253
218
|
}
|
|
254
219
|
}, 1000);
|
|
255
220
|
|
|
@@ -279,30 +244,30 @@ export class CommandRunner {
|
|
|
279
244
|
}
|
|
280
245
|
}
|
|
281
246
|
|
|
282
|
-
/**
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
247
|
+
/** Terminate all active child processes (called during graceful shutdown). */
|
|
248
|
+
shutdownAll() {
|
|
249
|
+
const sendSignal = (sig) => {
|
|
250
|
+
for (const [, entry] of this.processes) {
|
|
251
|
+
try { process.kill(-entry.process.pid, sig); } catch {
|
|
252
|
+
try { entry.process.kill(sig); } catch { /* already dead */ }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
sendSignal('SIGTERM');
|
|
257
|
+
setTimeout(() => sendSignal('SIGKILL'), 1000).unref();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Get all active runs as a new Map. */
|
|
286
261
|
getActiveRuns() {
|
|
287
262
|
return new Map(this.processes);
|
|
288
263
|
}
|
|
289
264
|
|
|
290
|
-
/**
|
|
291
|
-
* Check if a run is active
|
|
292
|
-
* @param {string} runId
|
|
293
|
-
* @returns {boolean}
|
|
294
|
-
*/
|
|
265
|
+
/** Check if a run is active. */
|
|
295
266
|
isRunning(runId) {
|
|
296
267
|
return this.processes.has(runId);
|
|
297
268
|
}
|
|
298
269
|
|
|
299
|
-
/**
|
|
300
|
-
* Get all running commands for a project
|
|
301
|
-
* Used for merging in-memory running commands with completed runs from the database
|
|
302
|
-
* @param {string} projectId
|
|
303
|
-
* @param {Function} getSessionById - Function to look up session by ID
|
|
304
|
-
* @returns {Array} Running command runs
|
|
305
|
-
*/
|
|
270
|
+
/** Get all running commands for a project (merges in-memory with completed DB runs). */
|
|
306
271
|
getRunningByProjectId(projectId, getSessionById) {
|
|
307
272
|
const results = [];
|
|
308
273
|
for (const [runId, entry] of this.processes.entries()) {
|
|
@@ -324,15 +289,7 @@ export class CommandRunner {
|
|
|
324
289
|
return results;
|
|
325
290
|
}
|
|
326
291
|
|
|
327
|
-
/**
|
|
328
|
-
* Get all active runs for a specific session (both running and recent completed)
|
|
329
|
-
* @param {string} sessionId
|
|
330
|
-
* @returns {Array} Array of run info objects
|
|
331
|
-
*/
|
|
332
|
-
/**
|
|
333
|
-
* Mark an orphaned run as error in the database
|
|
334
|
-
* @param {Object} dbRun - The database run record
|
|
335
|
-
*/
|
|
292
|
+
/** Mark an orphaned run as error in the database. */
|
|
336
293
|
#markOrphanedRunAsError(dbRun) {
|
|
337
294
|
console.log(
|
|
338
295
|
`[commandRunner.getRunsBySession] Orphaned run detected: ${dbRun.id}, marking as error`
|
|
@@ -348,11 +305,7 @@ export class CommandRunner {
|
|
|
348
305
|
}
|
|
349
306
|
}
|
|
350
307
|
|
|
351
|
-
/**
|
|
352
|
-
* Process a database run record and return a normalized run object
|
|
353
|
-
* @param {Object} dbRun - The database run record
|
|
354
|
-
* @returns {Object} Normalized run object
|
|
355
|
-
*/
|
|
308
|
+
/** Process a database run record and return a normalized run object. */
|
|
356
309
|
#processDbRun(dbRun) {
|
|
357
310
|
let status = dbRun.status;
|
|
358
311
|
let exitCode = dbRun.exitCode;
|
|
@@ -120,6 +120,45 @@ export function getTemplateSessionSettings(template, session) {
|
|
|
120
120
|
* @param {Object} [options] - Options
|
|
121
121
|
* @param {number} [options.depth=0] - Current recursion depth
|
|
122
122
|
*/
|
|
123
|
+
/**
|
|
124
|
+
* Create and configure a child session from a template for lane entry.
|
|
125
|
+
* @param {Object} template
|
|
126
|
+
* @param {Object} session - Parent session
|
|
127
|
+
* @param {Object} lane
|
|
128
|
+
* @param {number} depth - Current trigger depth
|
|
129
|
+
* @returns {{ newSession: Object, renderedPrompt: string, settings: Object }}
|
|
130
|
+
*/
|
|
131
|
+
async function buildChildSessionFromTemplate(template, session, lane, depth) {
|
|
132
|
+
// Render prompt with session context
|
|
133
|
+
const parentSummary = sessionSummaries.getBySessionId(session.id);
|
|
134
|
+
const rootSession = getRootSession(session);
|
|
135
|
+
const rootSummary = sessionSummaries.getBySessionId(rootSession.id);
|
|
136
|
+
const renderedPrompt = await renderTemplatePrompt(
|
|
137
|
+
template.prompt,
|
|
138
|
+
{ parentSession: session, parentSummary, rootSession, rootSummary }
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Get settings and create session
|
|
142
|
+
const settings = getTemplateSessionSettings(template, session);
|
|
143
|
+
const newSession = sessions.create(session.projectId, `${template.name} (lane: ${lane.name})`, renderedPrompt, {
|
|
144
|
+
mode: settings.mode,
|
|
145
|
+
thinkingEnabled: settings.thinkingEnabled,
|
|
146
|
+
gitBranch: settings.gitBranch,
|
|
147
|
+
status: 'starting',
|
|
148
|
+
model: settings.model,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Configure session
|
|
152
|
+
sessions.update(newSession.id, {
|
|
153
|
+
parentSessionId: session.id,
|
|
154
|
+
nextTemplateId: template.nextTemplateId || null,
|
|
155
|
+
targetLaneId: template.targetLaneId || null,
|
|
156
|
+
laneTriggerDepth: depth + 1,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return { newSession, renderedPrompt, settings };
|
|
160
|
+
}
|
|
161
|
+
|
|
123
162
|
export async function triggerOnEnterTemplate(sessionId, lane, options = {}) {
|
|
124
163
|
const { depth = 0 } = options;
|
|
125
164
|
|
|
@@ -141,32 +180,7 @@ export async function triggerOnEnterTemplate(sessionId, lane, options = {}) {
|
|
|
141
180
|
console.log(`Kanban: Triggering on-enter template "${template.name}" for session "${session.name}" entering lane "${lane.name}"`);
|
|
142
181
|
|
|
143
182
|
try {
|
|
144
|
-
|
|
145
|
-
const parentSummary = sessionSummaries.getBySessionId(sessionId);
|
|
146
|
-
const rootSession = getRootSession(session);
|
|
147
|
-
const rootSummary = sessionSummaries.getBySessionId(rootSession.id);
|
|
148
|
-
const renderedPrompt = await renderTemplatePrompt(
|
|
149
|
-
template.prompt,
|
|
150
|
-
{ parentSession: session, parentSummary, rootSession, rootSummary }
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
// Get settings and create session
|
|
154
|
-
const settings = getTemplateSessionSettings(template, session);
|
|
155
|
-
const newSession = sessions.create(session.projectId, `${template.name} (lane: ${lane.name})`, renderedPrompt, {
|
|
156
|
-
mode: settings.mode,
|
|
157
|
-
thinkingEnabled: settings.thinkingEnabled,
|
|
158
|
-
gitBranch: settings.gitBranch,
|
|
159
|
-
status: 'starting',
|
|
160
|
-
model: settings.model,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// Configure session
|
|
164
|
-
sessions.update(newSession.id, {
|
|
165
|
-
parentSessionId: session.id,
|
|
166
|
-
nextTemplateId: template.nextTemplateId || null,
|
|
167
|
-
targetLaneId: template.targetLaneId || null,
|
|
168
|
-
laneTriggerDepth: depth + 1,
|
|
169
|
-
});
|
|
183
|
+
const { newSession, renderedPrompt, settings } = await buildChildSessionFromTemplate(template, session, lane, depth);
|
|
170
184
|
|
|
171
185
|
// Determine working directory
|
|
172
186
|
const { workingDirectory, gitWorktree } = await determineWorkingDirectory(session, project, {
|
|
@@ -203,6 +217,48 @@ export async function triggerOnEnterTemplate(sessionId, lane, options = {}) {
|
|
|
203
217
|
* @param {Object} [options] - Options
|
|
204
218
|
* @param {number} [options.depth=0] - Current recursion depth
|
|
205
219
|
*/
|
|
220
|
+
/**
|
|
221
|
+
* Create and configure a child session from a lane's on-enter prompt.
|
|
222
|
+
* @param {Object} lane
|
|
223
|
+
* @param {Object} session - Parent session
|
|
224
|
+
* @param {number} depth - Current trigger depth
|
|
225
|
+
* @returns {Promise<{ newSession: Object, renderedPrompt: string, settings: Object }>}
|
|
226
|
+
*/
|
|
227
|
+
async function buildChildSessionFromPrompt(lane, session, depth) {
|
|
228
|
+
// Render prompt with session context
|
|
229
|
+
const parentSummary = sessionSummaries.getBySessionId(session.id);
|
|
230
|
+
const rootSession = getRootSession(session);
|
|
231
|
+
const rootSummary = sessionSummaries.getBySessionId(rootSession.id);
|
|
232
|
+
const renderedPrompt = await renderTemplatePrompt(
|
|
233
|
+
lane.onEnterPrompt,
|
|
234
|
+
{ parentSession: session, parentSummary, rootSession, rootSummary }
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Get settings and create session
|
|
238
|
+
const settings = getLaneSessionSettings(lane, session);
|
|
239
|
+
const newSession = sessions.create(session.projectId, `Lane prompt (lane: ${lane.name})`, renderedPrompt, {
|
|
240
|
+
...settings,
|
|
241
|
+
status: 'starting',
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Configure session
|
|
245
|
+
const sessionUpdates = { parentSessionId: session.id, laneTriggerDepth: depth + 1 };
|
|
246
|
+
if (lane.onEnterAutoRescheduleEnabled) {
|
|
247
|
+
Object.assign(sessionUpdates, {
|
|
248
|
+
autoRescheduleEnabled: true,
|
|
249
|
+
rescheduleDelayMinutes: lane.onEnterRescheduleDelayMinutes || 15,
|
|
250
|
+
rescheduleOnTokenLimit: lane.onEnterRescheduleOnTokenLimit ?? true,
|
|
251
|
+
rescheduleOnServiceError: lane.onEnterRescheduleOnServiceError ?? true,
|
|
252
|
+
maxRescheduleCount: lane.onEnterMaxRescheduleCount || null,
|
|
253
|
+
maxTotalTokens: lane.onEnterMaxTotalTokens || null,
|
|
254
|
+
rescheduleAtTokenCount: lane.onEnterRescheduleAtTokenCount || null,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
sessions.update(newSession.id, sessionUpdates);
|
|
258
|
+
|
|
259
|
+
return { newSession, renderedPrompt, settings };
|
|
260
|
+
}
|
|
261
|
+
|
|
206
262
|
export async function triggerOnEnterPrompt(sessionId, lane, options = {}) {
|
|
207
263
|
const { depth = 0 } = options;
|
|
208
264
|
|
|
@@ -218,36 +274,7 @@ export async function triggerOnEnterPrompt(sessionId, lane, options = {}) {
|
|
|
218
274
|
console.log(`Kanban: Triggering on-enter prompt for session "${session.name}" entering lane "${lane.name}"`);
|
|
219
275
|
|
|
220
276
|
try {
|
|
221
|
-
|
|
222
|
-
const parentSummary = sessionSummaries.getBySessionId(sessionId);
|
|
223
|
-
const rootSession = getRootSession(session);
|
|
224
|
-
const rootSummary = sessionSummaries.getBySessionId(rootSession.id);
|
|
225
|
-
const renderedPrompt = await renderTemplatePrompt(
|
|
226
|
-
lane.onEnterPrompt,
|
|
227
|
-
{ parentSession: session, parentSummary, rootSession, rootSummary }
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
// Get settings and create session
|
|
231
|
-
const settings = getLaneSessionSettings(lane, session);
|
|
232
|
-
const newSession = sessions.create(session.projectId, `Lane prompt (lane: ${lane.name})`, renderedPrompt, {
|
|
233
|
-
...settings,
|
|
234
|
-
status: 'starting',
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// Configure session
|
|
238
|
-
const sessionUpdates = { parentSessionId: session.id, laneTriggerDepth: depth + 1 };
|
|
239
|
-
if (lane.onEnterAutoRescheduleEnabled) {
|
|
240
|
-
Object.assign(sessionUpdates, {
|
|
241
|
-
autoRescheduleEnabled: true,
|
|
242
|
-
rescheduleDelayMinutes: lane.onEnterRescheduleDelayMinutes || 15,
|
|
243
|
-
rescheduleOnTokenLimit: lane.onEnterRescheduleOnTokenLimit ?? true,
|
|
244
|
-
rescheduleOnServiceError: lane.onEnterRescheduleOnServiceError ?? true,
|
|
245
|
-
maxRescheduleCount: lane.onEnterMaxRescheduleCount || null,
|
|
246
|
-
maxTotalTokens: lane.onEnterMaxTotalTokens || null,
|
|
247
|
-
rescheduleAtTokenCount: lane.onEnterRescheduleAtTokenCount || null,
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
sessions.update(newSession.id, sessionUpdates);
|
|
277
|
+
const { newSession, renderedPrompt, settings } = await buildChildSessionFromPrompt(lane, session, depth);
|
|
251
278
|
|
|
252
279
|
// Determine working directory
|
|
253
280
|
const { workingDirectory, gitWorktree } = await determineWorkingDirectory(session, project);
|