@proletariat/cli 0.3.35 → 0.3.40
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/README.md +37 -2
- package/bin/dev.js +0 -0
- package/dist/commands/agent/auth.d.ts +12 -2
- package/dist/commands/agent/auth.js +128 -4
- package/dist/commands/agent/list.js +16 -7
- package/dist/commands/agent/status.js +32 -4
- package/dist/commands/board/watch.js +6 -0
- package/dist/commands/branch/list.d.ts +1 -0
- package/dist/commands/branch/list.js +43 -12
- package/dist/commands/branch/where.js +9 -19
- package/dist/commands/category/list.d.ts +2 -1
- package/dist/commands/category/list.js +38 -13
- package/dist/commands/{claude.d.ts → claude/index.d.ts} +1 -1
- package/dist/commands/{claude.js → claude/index.js} +12 -12
- package/dist/commands/claude/open.d.ts +13 -0
- package/dist/commands/claude/open.js +175 -0
- package/dist/commands/diet.js +18 -2
- package/dist/commands/docker/logs.js +7 -3
- package/dist/commands/docker/shell.js +6 -0
- package/dist/commands/docker/start.js +20 -4
- package/dist/commands/docker/sync.d.ts +4 -0
- package/dist/commands/docker/sync.js +30 -2
- package/dist/commands/epic/show.d.ts +13 -0
- package/dist/commands/epic/show.js +16 -0
- package/dist/commands/epic/ticket.js +7 -24
- package/dist/commands/epic/view.js +27 -0
- package/dist/commands/execution/config.d.ts +0 -4
- package/dist/commands/execution/config.js +14 -46
- package/dist/commands/execution/index.js +2 -1
- package/dist/commands/execution/logs.js +7 -1
- package/dist/commands/execution/stop.js +2 -1
- package/dist/commands/execution/view.js +30 -26
- package/dist/commands/init.js +2 -19
- package/dist/commands/label/create.js +2 -1
- package/dist/commands/label/delete.js +2 -1
- package/dist/commands/label/group/create.js +2 -1
- package/dist/commands/label/group/list.js +2 -1
- package/dist/commands/label/list.js +2 -1
- package/dist/commands/mcp-server.js +27 -1
- package/dist/commands/phase/template/list.js +2 -1
- package/dist/commands/pmo/init.js +12 -40
- package/dist/commands/project/create.js +3 -4
- package/dist/commands/project/update.js +5 -6
- package/dist/commands/pull.js +24 -0
- package/dist/commands/qa/index.d.ts +54 -0
- package/dist/commands/qa/index.js +762 -0
- package/dist/commands/repo/view.js +2 -8
- package/dist/commands/session/attach.js +4 -4
- package/dist/commands/session/create.d.ts +19 -0
- package/dist/commands/session/create.js +102 -0
- package/dist/commands/session/health.js +4 -23
- package/dist/commands/session/index.js +14 -1
- package/dist/commands/session/list.js +9 -8
- package/dist/commands/session/peek.d.ts +38 -0
- package/dist/commands/session/peek.js +316 -0
- package/dist/commands/session/poke.d.ts +27 -0
- package/dist/commands/session/poke.js +219 -0
- package/dist/commands/spec/view.js +29 -0
- package/dist/commands/template/list.js +2 -1
- package/dist/commands/theme/add-names.d.ts +4 -0
- package/dist/commands/theme/add-names.js +11 -1
- package/dist/commands/theme/create.d.ts +2 -0
- package/dist/commands/theme/create.js +8 -0
- package/dist/commands/ticket/bulk.js +2 -2
- package/dist/commands/ticket/complete.js +2 -2
- package/dist/commands/ticket/create.js +21 -0
- package/dist/commands/ticket/delete.js +8 -0
- package/dist/commands/ticket/edit.js +25 -0
- package/dist/commands/ticket/epic.js +17 -43
- package/dist/commands/ticket/index.js +2 -2
- package/dist/commands/ticket/move.js +25 -2
- package/dist/commands/ticket/resolve.js +3 -4
- package/dist/commands/ticket/show.d.ts +13 -0
- package/dist/commands/ticket/show.js +16 -0
- package/dist/commands/ticket/template/list.js +2 -1
- package/dist/commands/ticket/view.d.ts +0 -1
- package/dist/commands/ticket/view.js +30 -1
- package/dist/commands/work/index.js +4 -0
- package/dist/commands/work/spawn-all.js +1 -1
- package/dist/commands/work/spawn.js +15 -4
- package/dist/commands/work/start.js +186 -103
- package/dist/commands/work/status.d.ts +14 -0
- package/dist/commands/work/status.js +60 -0
- package/dist/commands/work/watch.js +1 -1
- package/dist/commands/workflow/index.js +2 -1
- package/dist/commands/workflow/show.d.ts +13 -0
- package/dist/commands/workflow/show.js +16 -0
- package/dist/commands/workspace/add.js +15 -0
- package/dist/commands/workspace/list.js +2 -1
- package/dist/commands/workspace/prune.js +7 -7
- package/dist/hooks/init.js +10 -2
- package/dist/lib/agents/commands.d.ts +5 -0
- package/dist/lib/agents/commands.js +143 -97
- package/dist/lib/branch/index.d.ts +1 -0
- package/dist/lib/database/drizzle-schema.d.ts +465 -0
- package/dist/lib/database/drizzle-schema.js +53 -0
- package/dist/lib/database/index.d.ts +47 -1
- package/dist/lib/database/index.js +138 -20
- package/dist/lib/execution/config.d.ts +15 -1
- package/dist/lib/execution/config.js +28 -0
- package/dist/lib/execution/runners.d.ts +45 -0
- package/dist/lib/execution/runners.js +187 -26
- package/dist/lib/execution/session-utils.d.ts +16 -1
- package/dist/lib/execution/session-utils.js +71 -4
- package/dist/lib/execution/spawner.js +15 -2
- package/dist/lib/execution/storage.d.ts +6 -1
- package/dist/lib/execution/storage.js +35 -5
- package/dist/lib/execution/types.d.ts +3 -0
- package/dist/lib/mcp/tools/board.js +4 -6
- package/dist/lib/mcp/tools/cli-passthrough.js +25 -6
- package/dist/lib/mcp/tools/epic.js +8 -3
- package/dist/lib/mcp/tools/index.d.ts +1 -0
- package/dist/lib/mcp/tools/index.js +1 -0
- package/dist/lib/mcp/tools/spec.js +1 -1
- package/dist/lib/mcp/tools/ticket.js +11 -9
- package/dist/lib/mcp/tools/tmux.d.ts +16 -0
- package/dist/lib/mcp/tools/tmux.js +182 -0
- package/dist/lib/mcp/tools/work.js +148 -6
- package/dist/lib/mcp/types.d.ts +10 -0
- package/dist/lib/multiline-input.js +2 -1
- package/dist/lib/pmo/base-command.js +4 -4
- package/dist/lib/pmo/schema.d.ts +1 -1
- package/dist/lib/pmo/schema.js +1 -0
- package/dist/lib/pmo/storage/actions.js +1 -1
- package/dist/lib/pmo/storage/base.js +402 -50
- package/dist/lib/pmo/storage/dependencies.d.ts +1 -0
- package/dist/lib/pmo/storage/dependencies.js +11 -3
- package/dist/lib/pmo/storage/epics.js +1 -1
- package/dist/lib/pmo/storage/helpers.d.ts +4 -4
- package/dist/lib/pmo/storage/helpers.js +36 -26
- package/dist/lib/pmo/storage/projects.d.ts +2 -0
- package/dist/lib/pmo/storage/projects.js +207 -119
- package/dist/lib/pmo/storage/specs.d.ts +2 -0
- package/dist/lib/pmo/storage/specs.js +274 -188
- package/dist/lib/pmo/storage/tickets.d.ts +2 -0
- package/dist/lib/pmo/storage/tickets.js +350 -290
- package/dist/lib/pmo/storage/types.d.ts +1 -0
- package/dist/lib/pmo/storage/views.d.ts +2 -0
- package/dist/lib/pmo/storage/views.js +183 -130
- package/dist/lib/prompt-command.d.ts +20 -0
- package/dist/lib/prompt-command.js +38 -2
- package/dist/lib/prompt-json.d.ts +41 -4
- package/dist/lib/prompt-json.js +138 -7
- package/dist/lib/styles.d.ts +37 -0
- package/dist/lib/styles.js +73 -0
- package/oclif.manifest.json +4046 -3385
- package/package.json +11 -6
- package/LICENSE +0 -190
|
@@ -424,29 +424,56 @@ export function addAgentsToDatabase(workspacePath, agentNames, themeId, mountMod
|
|
|
424
424
|
db.close();
|
|
425
425
|
}
|
|
426
426
|
/**
|
|
427
|
-
* Add an ephemeral agent to the database
|
|
427
|
+
* Add an ephemeral agent to the database.
|
|
428
|
+
* Throws on name collision — use tryAddEphemeralAgentToDatabase for
|
|
429
|
+
* concurrency-safe insertion with conflict detection.
|
|
428
430
|
*/
|
|
429
431
|
export function addEphemeralAgentToDatabase(workspacePath, agentName, baseName, themeId, mountMode = 'worktree') {
|
|
432
|
+
const result = tryAddEphemeralAgentToDatabase(workspacePath, agentName, baseName, themeId, mountMode);
|
|
433
|
+
if (!result) {
|
|
434
|
+
throw new Error(`Agent name "${agentName}" already exists (UNIQUE constraint failed: agents.name)`);
|
|
435
|
+
}
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Try to add an ephemeral agent to the database.
|
|
440
|
+
* Returns the Agent on success, or null if the name already exists
|
|
441
|
+
* (SQLITE_CONSTRAINT_PRIMARYKEY). This is concurrency-safe: parallel
|
|
442
|
+
* processes that generate the same name will not crash — the loser
|
|
443
|
+
* simply gets null and can retry with a different name.
|
|
444
|
+
*/
|
|
445
|
+
export function tryAddEphemeralAgentToDatabase(workspacePath, agentName, baseName, themeId, mountMode = 'worktree') {
|
|
430
446
|
const db = openWorkspaceDatabase(workspacePath);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
447
|
+
try {
|
|
448
|
+
const now = new Date().toISOString();
|
|
449
|
+
const worktreePath = `agents/temp/${agentName}`;
|
|
450
|
+
db.prepare(`
|
|
451
|
+
INSERT INTO agents (name, type, status, base_name, theme_id, worktree_path, mount_mode, created_at)
|
|
452
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
453
|
+
`).run(agentName, 'ephemeral', 'active', baseName, themeId || null, worktreePath, mountMode, now);
|
|
454
|
+
const agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentName);
|
|
455
|
+
return {
|
|
456
|
+
name: agent.name,
|
|
457
|
+
type: agent.type,
|
|
458
|
+
status: agent.status,
|
|
459
|
+
base_name: agent.base_name,
|
|
460
|
+
theme_id: agent.theme_id,
|
|
461
|
+
worktree_path: agent.worktree_path,
|
|
462
|
+
mount_mode: (agent.mount_mode || 'clone'),
|
|
463
|
+
created_at: agent.created_at,
|
|
464
|
+
cleaned_at: agent.cleaned_at,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
const sqliteErr = err;
|
|
469
|
+
if (sqliteErr.code === 'SQLITE_CONSTRAINT_PRIMARYKEY' || sqliteErr.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
throw err;
|
|
473
|
+
}
|
|
474
|
+
finally {
|
|
475
|
+
db.close();
|
|
476
|
+
}
|
|
450
477
|
}
|
|
451
478
|
/**
|
|
452
479
|
* Get all ephemeral agent names from the database
|
|
@@ -669,6 +696,34 @@ export function getAgentWorktrees(workspacePath, agentName) {
|
|
|
669
696
|
db.close();
|
|
670
697
|
return worktrees;
|
|
671
698
|
}
|
|
699
|
+
/**
|
|
700
|
+
* Find agent worktrees matching a branch pattern (case-insensitive LIKE).
|
|
701
|
+
*/
|
|
702
|
+
export function findWorktreesByBranch(workspacePath, branchPattern) {
|
|
703
|
+
const db = openWorkspaceDatabase(workspacePath);
|
|
704
|
+
const worktrees = db.prepare('SELECT * FROM agent_worktrees WHERE LOWER(branch) LIKE ?').all(branchPattern);
|
|
705
|
+
db.close();
|
|
706
|
+
return worktrees;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Get agent worktrees for a specific repository.
|
|
710
|
+
*/
|
|
711
|
+
export function getWorktreesForRepo(workspacePath, repoName) {
|
|
712
|
+
const db = openWorkspaceDatabase(workspacePath);
|
|
713
|
+
const worktrees = db.prepare('SELECT agent_name, is_clean, commits_ahead, branch FROM agent_worktrees WHERE repo_name = ?').all(repoName);
|
|
714
|
+
db.close();
|
|
715
|
+
return worktrees;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Upsert a workspace setting (key-value pair).
|
|
719
|
+
*/
|
|
720
|
+
export function upsertWorkspaceSetting(db, key, value) {
|
|
721
|
+
db.prepare(`
|
|
722
|
+
INSERT INTO workspace_settings (key, value)
|
|
723
|
+
VALUES (?, ?)
|
|
724
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
725
|
+
`).run(key, value);
|
|
726
|
+
}
|
|
672
727
|
/**
|
|
673
728
|
* Remove agents from database
|
|
674
729
|
*/
|
|
@@ -685,6 +740,69 @@ export function removeAgentsFromDatabase(workspacePath, agentNames) {
|
|
|
685
740
|
db.close();
|
|
686
741
|
}
|
|
687
742
|
// =============================================================================
|
|
743
|
+
// PMO Bootstrapping Operations
|
|
744
|
+
// =============================================================================
|
|
745
|
+
/**
|
|
746
|
+
* Check if PMO tables exist and get basic stats.
|
|
747
|
+
* Used by pmo init to detect existing PMO before storage layer is available.
|
|
748
|
+
*/
|
|
749
|
+
export function checkPMOExists(dbPath) {
|
|
750
|
+
const db = new Database(dbPath);
|
|
751
|
+
try {
|
|
752
|
+
const result = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='pmo_projects'").get();
|
|
753
|
+
if (result === undefined) {
|
|
754
|
+
return { exists: false, projectCount: 0, ticketCount: 0 };
|
|
755
|
+
}
|
|
756
|
+
const projectCountResult = db.prepare('SELECT COUNT(*) as count FROM pmo_projects').get();
|
|
757
|
+
const ticketCountResult = db.prepare('SELECT COUNT(*) as count FROM pmo_tickets').get();
|
|
758
|
+
return {
|
|
759
|
+
exists: true,
|
|
760
|
+
projectCount: projectCountResult.count,
|
|
761
|
+
ticketCount: ticketCountResult.count,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
finally {
|
|
765
|
+
db.close();
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Get a PMO setting from the pmo_settings table.
|
|
770
|
+
* Used for bootstrapping queries before storage layer is available.
|
|
771
|
+
*/
|
|
772
|
+
export function getPMOSetting(dbPath, key) {
|
|
773
|
+
const db = new Database(dbPath);
|
|
774
|
+
try {
|
|
775
|
+
const result = db.prepare('SELECT value FROM pmo_settings WHERE key = ?').get(key);
|
|
776
|
+
return result?.value ?? null;
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
finally {
|
|
782
|
+
db.close();
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Drop PMO tables from the database.
|
|
787
|
+
* Used during PMO reinitialization.
|
|
788
|
+
*/
|
|
789
|
+
export function dropPMOTables(dbPath, tables) {
|
|
790
|
+
const db = new Database(dbPath);
|
|
791
|
+
try {
|
|
792
|
+
for (const table of tables) {
|
|
793
|
+
try {
|
|
794
|
+
db.prepare(`DROP TABLE IF EXISTS ${table}`).run();
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
// Ignore errors - table might not exist
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
finally {
|
|
802
|
+
db.close();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
// =============================================================================
|
|
688
806
|
// Theme CRUD Operations
|
|
689
807
|
// =============================================================================
|
|
690
808
|
/**
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Uses the workspace_settings table (not pmo_settings - execution is workspace-level).
|
|
6
6
|
*/
|
|
7
7
|
import Database from 'better-sqlite3';
|
|
8
|
-
import { ExecutionConfig, TerminalApp, Shell, DisplayMode, OutputMode, ExecutionEnvironment } from './types.js';
|
|
8
|
+
import { ExecutionConfig, TerminalApp, Shell, DisplayMode, OutputMode, ExecutionEnvironment, AuthMethod } from './types.js';
|
|
9
9
|
import { type JsonFlags } from '../prompt-json.js';
|
|
10
10
|
declare const CONFIG_KEYS: {
|
|
11
11
|
terminalApp: string;
|
|
@@ -28,6 +28,7 @@ declare const CONFIG_KEYS: {
|
|
|
28
28
|
vmKeyPath: string;
|
|
29
29
|
vmSyncMethod: string;
|
|
30
30
|
coderName: string;
|
|
31
|
+
authMethod: string;
|
|
31
32
|
};
|
|
32
33
|
/**
|
|
33
34
|
* Load execution config from database, merging with defaults
|
|
@@ -55,6 +56,19 @@ export declare function saveTmuxControlMode(db: Database.Database, enabled: bool
|
|
|
55
56
|
* When enabled, new terminal tabs open without stealing focus from current window.
|
|
56
57
|
*/
|
|
57
58
|
export declare function saveTerminalOpenInBackground(db: Database.Database, enabled: boolean): void;
|
|
59
|
+
/**
|
|
60
|
+
* Save auth method preference (oauth or apikey)
|
|
61
|
+
*/
|
|
62
|
+
export declare function saveAuthMethod(db: Database.Database, method: AuthMethod): void;
|
|
63
|
+
/**
|
|
64
|
+
* Get saved auth method preference.
|
|
65
|
+
* Returns null if no preference has been saved (user should be prompted).
|
|
66
|
+
*/
|
|
67
|
+
export declare function getAuthMethod(db: Database.Database): AuthMethod | null;
|
|
68
|
+
/**
|
|
69
|
+
* Clear saved auth method preference (will prompt again next time)
|
|
70
|
+
*/
|
|
71
|
+
export declare function clearAuthMethod(db: Database.Database): void;
|
|
58
72
|
/**
|
|
59
73
|
* Check if terminal app preference has been set
|
|
60
74
|
*/
|
|
@@ -32,6 +32,7 @@ const CONFIG_KEYS = {
|
|
|
32
32
|
vmKeyPath: 'execution.vm.key_path',
|
|
33
33
|
vmSyncMethod: 'execution.vm.sync_method',
|
|
34
34
|
coderName: 'coder.name',
|
|
35
|
+
authMethod: 'execution.auth_method',
|
|
35
36
|
};
|
|
36
37
|
/**
|
|
37
38
|
* Get a setting value from the database
|
|
@@ -97,6 +98,11 @@ export function loadExecutionConfig(db) {
|
|
|
97
98
|
if (sandboxed !== null) {
|
|
98
99
|
config.sandboxed = sandboxed === 'true';
|
|
99
100
|
}
|
|
101
|
+
// Load auth method preference
|
|
102
|
+
const authMethod = getSetting(db, CONFIG_KEYS.authMethod);
|
|
103
|
+
if (authMethod) {
|
|
104
|
+
config.authMethod = authMethod;
|
|
105
|
+
}
|
|
100
106
|
// Load tmux settings
|
|
101
107
|
const tmuxSession = getSetting(db, CONFIG_KEYS.tmuxSession);
|
|
102
108
|
if (tmuxSession) {
|
|
@@ -178,6 +184,28 @@ export function saveTmuxControlMode(db, enabled) {
|
|
|
178
184
|
export function saveTerminalOpenInBackground(db, enabled) {
|
|
179
185
|
setSetting(db, CONFIG_KEYS.terminalOpenInBackground, enabled.toString());
|
|
180
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Save auth method preference (oauth or apikey)
|
|
189
|
+
*/
|
|
190
|
+
export function saveAuthMethod(db, method) {
|
|
191
|
+
setSetting(db, CONFIG_KEYS.authMethod, method);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get saved auth method preference.
|
|
195
|
+
* Returns null if no preference has been saved (user should be prompted).
|
|
196
|
+
*/
|
|
197
|
+
export function getAuthMethod(db) {
|
|
198
|
+
const value = getSetting(db, CONFIG_KEYS.authMethod);
|
|
199
|
+
if (value === 'oauth' || value === 'apikey')
|
|
200
|
+
return value;
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Clear saved auth method preference (will prompt again next time)
|
|
205
|
+
*/
|
|
206
|
+
export function clearAuthMethod(db) {
|
|
207
|
+
db.prepare(`DELETE FROM ${SETTINGS_TABLE} WHERE key = ?`).run(CONFIG_KEYS.authMethod);
|
|
208
|
+
}
|
|
181
209
|
/**
|
|
182
210
|
* Check if terminal app preference has been set
|
|
183
211
|
*/
|
|
@@ -41,6 +41,17 @@ export declare function buildTmuxAttachCommand(useControlMode: boolean, includeU
|
|
|
41
41
|
*/
|
|
42
42
|
export declare function configureITermTmuxPreferences(mode: 'tab' | 'window'): void;
|
|
43
43
|
export declare function configureITermTmuxWindowMode(mode: 'tab' | 'window'): void;
|
|
44
|
+
/**
|
|
45
|
+
* Build the tmux script that runs inside the container.
|
|
46
|
+
* In background mode: kills PID 1 (sleep infinity) after Claude exits to stop/remove container.
|
|
47
|
+
* In terminal/foreground mode: drops into exec bash for user inspection.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildTmuxScript(sessionName: string, claudeCmd: string, displayMode: DisplayMode): string;
|
|
50
|
+
/**
|
|
51
|
+
* Get the auto-remove flags for docker run based on display mode.
|
|
52
|
+
* Background mode containers get --rm so Docker removes them when they stop.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getDockerAutoRemoveFlags(displayMode: DisplayMode): string[];
|
|
44
55
|
/**
|
|
45
56
|
* Check if the claude-credentials Docker volume exists.
|
|
46
57
|
*/
|
|
@@ -63,6 +74,40 @@ export declare function getDockerCredentialInfo(): {
|
|
|
63
74
|
expiresAt: Date;
|
|
64
75
|
subscriptionType?: string;
|
|
65
76
|
} | null;
|
|
77
|
+
/**
|
|
78
|
+
* Preflight result indicating whether the executor is ready to run.
|
|
79
|
+
*/
|
|
80
|
+
export interface PreflightResult {
|
|
81
|
+
ok: boolean;
|
|
82
|
+
error?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Check if an executor binary is available on the host.
|
|
86
|
+
* Returns a PreflightResult with ok=true if the binary is found,
|
|
87
|
+
* or ok=false with a descriptive error and remediation hint.
|
|
88
|
+
*/
|
|
89
|
+
export declare function checkExecutorOnHost(executor: ExecutorType): PreflightResult;
|
|
90
|
+
/**
|
|
91
|
+
* Check if an executor binary is available inside a Docker container.
|
|
92
|
+
* Returns a PreflightResult with ok=true if the binary is found,
|
|
93
|
+
* or ok=false with a descriptive error and remediation hint.
|
|
94
|
+
*/
|
|
95
|
+
export declare function checkExecutorInContainer(executor: ExecutorType, containerId: string): PreflightResult;
|
|
96
|
+
/**
|
|
97
|
+
* Run preflight checks for a given execution environment and executor.
|
|
98
|
+
* Validates that the executor binary is available before spawning.
|
|
99
|
+
*
|
|
100
|
+
* Checks performed per environment:
|
|
101
|
+
* - host: Verify binary on PATH
|
|
102
|
+
* - devcontainer: Verify binary inside container (if container running)
|
|
103
|
+
* - docker: Verify binary on host (used in docker run command)
|
|
104
|
+
* - vm: Verify binary on host (will be checked on remote separately)
|
|
105
|
+
*/
|
|
106
|
+
export declare function runExecutorPreflight(executor: ExecutorType, environment: ExecutionEnvironment, containerId?: string): PreflightResult;
|
|
107
|
+
export declare function getExecutorCommand(executor: ExecutorType, prompt: string, skipPermissions?: boolean): {
|
|
108
|
+
cmd: string;
|
|
109
|
+
args: string[];
|
|
110
|
+
};
|
|
66
111
|
export interface RunnerResult {
|
|
67
112
|
success: boolean;
|
|
68
113
|
pid?: string;
|
|
@@ -87,6 +87,47 @@ export function configureITermTmuxWindowMode(mode) {
|
|
|
87
87
|
configureITermTmuxPreferences(mode);
|
|
88
88
|
}
|
|
89
89
|
// =============================================================================
|
|
90
|
+
// Background Mode Cleanup Helpers (TKT-988)
|
|
91
|
+
// =============================================================================
|
|
92
|
+
/**
|
|
93
|
+
* Build the tmux script that runs inside the container.
|
|
94
|
+
* In background mode: kills PID 1 (sleep infinity) after Claude exits to stop/remove container.
|
|
95
|
+
* In terminal/foreground mode: drops into exec bash for user inspection.
|
|
96
|
+
*/
|
|
97
|
+
export function buildTmuxScript(sessionName, claudeCmd, displayMode) {
|
|
98
|
+
if (displayMode === 'background') {
|
|
99
|
+
return `#!/bin/bash
|
|
100
|
+
export TERM=xterm-256color
|
|
101
|
+
export COLORTERM=truecolor
|
|
102
|
+
unset CI
|
|
103
|
+
echo "🚀 Starting: ${sessionName}"
|
|
104
|
+
echo ""
|
|
105
|
+
${claudeCmd}
|
|
106
|
+
echo ""
|
|
107
|
+
echo "✅ Agent work complete. Cleaning up container..."
|
|
108
|
+
kill 1
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
return `#!/bin/bash
|
|
112
|
+
export TERM=xterm-256color
|
|
113
|
+
export COLORTERM=truecolor
|
|
114
|
+
unset CI
|
|
115
|
+
echo "🚀 Starting: ${sessionName}"
|
|
116
|
+
echo ""
|
|
117
|
+
${claudeCmd}
|
|
118
|
+
echo ""
|
|
119
|
+
echo "✅ Agent work complete. Press Enter to close or run more commands."
|
|
120
|
+
exec bash
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get the auto-remove flags for docker run based on display mode.
|
|
125
|
+
* Background mode containers get --rm so Docker removes them when they stop.
|
|
126
|
+
*/
|
|
127
|
+
export function getDockerAutoRemoveFlags(displayMode) {
|
|
128
|
+
return displayMode === 'background' ? ['--rm'] : [];
|
|
129
|
+
}
|
|
130
|
+
// =============================================================================
|
|
90
131
|
// Docker Credential Helpers
|
|
91
132
|
// =============================================================================
|
|
92
133
|
const CLAUDE_CREDENTIALS_VOLUME = 'claude-credentials';
|
|
@@ -147,10 +188,125 @@ export function getDockerCredentialInfo() {
|
|
|
147
188
|
return null;
|
|
148
189
|
}
|
|
149
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Get the binary name for an executor type.
|
|
193
|
+
*/
|
|
194
|
+
function getExecutorBinary(executor) {
|
|
195
|
+
switch (executor) {
|
|
196
|
+
case 'claude-code':
|
|
197
|
+
return 'claude';
|
|
198
|
+
case 'codex':
|
|
199
|
+
return 'codex';
|
|
200
|
+
case 'aider':
|
|
201
|
+
return 'aider';
|
|
202
|
+
case 'custom':
|
|
203
|
+
return 'echo'; // placeholder
|
|
204
|
+
default:
|
|
205
|
+
return 'claude';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get install/setup hints for a missing executor binary.
|
|
210
|
+
*/
|
|
211
|
+
function getExecutorRemediationHint(executor) {
|
|
212
|
+
switch (executor) {
|
|
213
|
+
case 'claude-code':
|
|
214
|
+
return 'Install Claude Code: npm install -g @anthropic-ai/claude-code\n' +
|
|
215
|
+
' Then authenticate: claude login';
|
|
216
|
+
case 'codex':
|
|
217
|
+
return 'Install Codex: npm install -g @openai/codex\n' +
|
|
218
|
+
' Then authenticate: set OPENAI_API_KEY or run codex --login';
|
|
219
|
+
case 'aider':
|
|
220
|
+
return 'Install aider: pip install aider-chat\n' +
|
|
221
|
+
' Then authenticate: set OPENAI_API_KEY or ANTHROPIC_API_KEY';
|
|
222
|
+
case 'custom':
|
|
223
|
+
return 'Configure a custom executor command in your execution settings.';
|
|
224
|
+
default:
|
|
225
|
+
return 'Install the required executor and ensure it is on your PATH.';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Check if an executor binary is available on the host.
|
|
230
|
+
* Returns a PreflightResult with ok=true if the binary is found,
|
|
231
|
+
* or ok=false with a descriptive error and remediation hint.
|
|
232
|
+
*/
|
|
233
|
+
export function checkExecutorOnHost(executor) {
|
|
234
|
+
const binary = getExecutorBinary(executor);
|
|
235
|
+
try {
|
|
236
|
+
execSync(`${binary} --version`, {
|
|
237
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
238
|
+
timeout: 10000,
|
|
239
|
+
});
|
|
240
|
+
return { ok: true };
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
error: `Executor "${executor}" not found: "${binary}" is not installed or not on PATH.\n\n` +
|
|
246
|
+
`Remediation:\n ${getExecutorRemediationHint(executor)}`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Check if an executor binary is available inside a Docker container.
|
|
252
|
+
* Returns a PreflightResult with ok=true if the binary is found,
|
|
253
|
+
* or ok=false with a descriptive error and remediation hint.
|
|
254
|
+
*/
|
|
255
|
+
export function checkExecutorInContainer(executor, containerId) {
|
|
256
|
+
const binary = getExecutorBinary(executor);
|
|
257
|
+
try {
|
|
258
|
+
execSync(`docker exec ${containerId} which ${binary}`, {
|
|
259
|
+
stdio: 'pipe',
|
|
260
|
+
timeout: 10000,
|
|
261
|
+
});
|
|
262
|
+
return { ok: true };
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
error: `Executor "${executor}" not found in container ${containerId}: "${binary}" is not installed.\n\n` +
|
|
268
|
+
`Remediation:\n Ensure "${binary}" is installed in the devcontainer image.\n ` +
|
|
269
|
+
getExecutorRemediationHint(executor),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Run preflight checks for a given execution environment and executor.
|
|
275
|
+
* Validates that the executor binary is available before spawning.
|
|
276
|
+
*
|
|
277
|
+
* Checks performed per environment:
|
|
278
|
+
* - host: Verify binary on PATH
|
|
279
|
+
* - devcontainer: Verify binary inside container (if container running)
|
|
280
|
+
* - docker: Verify binary on host (used in docker run command)
|
|
281
|
+
* - vm: Verify binary on host (will be checked on remote separately)
|
|
282
|
+
*/
|
|
283
|
+
export function runExecutorPreflight(executor, environment, containerId) {
|
|
284
|
+
switch (environment) {
|
|
285
|
+
case 'host':
|
|
286
|
+
return checkExecutorOnHost(executor);
|
|
287
|
+
case 'devcontainer':
|
|
288
|
+
// For devcontainer, check inside the container if it's already running
|
|
289
|
+
if (containerId) {
|
|
290
|
+
return checkExecutorInContainer(executor, containerId);
|
|
291
|
+
}
|
|
292
|
+
// Container not yet running - will be checked after container start
|
|
293
|
+
return { ok: true };
|
|
294
|
+
case 'docker':
|
|
295
|
+
// Docker runner builds the command on host, executor runs inside container
|
|
296
|
+
// Can't check until container is created, so skip for now
|
|
297
|
+
return { ok: true };
|
|
298
|
+
case 'vm':
|
|
299
|
+
// VM executor runs remotely - can't check from host
|
|
300
|
+
// Could add SSH-based check in the future
|
|
301
|
+
return { ok: true };
|
|
302
|
+
default:
|
|
303
|
+
return { ok: true };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
150
306
|
// =============================================================================
|
|
151
307
|
// Executor Commands
|
|
152
308
|
// =============================================================================
|
|
153
|
-
function getExecutorCommand(executor, prompt, skipPermissions = true) {
|
|
309
|
+
export function getExecutorCommand(executor, prompt, skipPermissions = true) {
|
|
154
310
|
switch (executor) {
|
|
155
311
|
case 'claude-code':
|
|
156
312
|
if (skipPermissions) {
|
|
@@ -740,7 +896,7 @@ function imageExists(imageName) {
|
|
|
740
896
|
* Create and start a Docker container for an agent.
|
|
741
897
|
* Uses raw Docker commands instead of devcontainer CLI.
|
|
742
898
|
*/
|
|
743
|
-
function createDockerContainer(context, containerName, imageName, config) {
|
|
899
|
+
function createDockerContainer(context, containerName, imageName, config, displayMode = 'terminal') {
|
|
744
900
|
// Build mount flags
|
|
745
901
|
// KEY: Use a named Docker volume for Claude credentials - this is how devcontainer.json
|
|
746
902
|
// was handling it. The volume persists across containers, so login once = logged in everywhere.
|
|
@@ -794,12 +950,16 @@ function createDockerContainer(context, containerName, imageName, config) {
|
|
|
794
950
|
'--cap-add=NET_RAW', // For firewall setup
|
|
795
951
|
// Note: After firewall is set up, the container is network-restricted
|
|
796
952
|
];
|
|
953
|
+
// Auto-remove container on stop for background mode (R5)
|
|
954
|
+
// Background containers should be cleaned up after work completes — nobody will attach to inspect
|
|
955
|
+
const autoRemoveFlags = getDockerAutoRemoveFlags(displayMode);
|
|
797
956
|
try {
|
|
798
957
|
const createCmd = [
|
|
799
958
|
'docker run -d',
|
|
800
959
|
`--name ${containerName}`,
|
|
801
960
|
'--user node',
|
|
802
961
|
'-w /workspace',
|
|
962
|
+
...autoRemoveFlags,
|
|
803
963
|
...mounts,
|
|
804
964
|
...envVars,
|
|
805
965
|
...resourceFlags,
|
|
@@ -898,7 +1058,7 @@ function runContainerSetup(containerId, sandboxed = true) {
|
|
|
898
1058
|
* Builds image and creates container if needed.
|
|
899
1059
|
* Returns the container ID if successful, null otherwise.
|
|
900
1060
|
*/
|
|
901
|
-
function ensureDockerContainer(context, config) {
|
|
1061
|
+
function ensureDockerContainer(context, config, displayMode = 'terminal') {
|
|
902
1062
|
const containerName = getContainerName(context.agentName);
|
|
903
1063
|
const imageName = getImageName(context.agentName);
|
|
904
1064
|
// Always create fresh container to ensure mounts are up-to-date
|
|
@@ -927,7 +1087,7 @@ function ensureDockerContainer(context, config) {
|
|
|
927
1087
|
}
|
|
928
1088
|
// Create and start container
|
|
929
1089
|
console.debug(`[runners:docker] Creating container ${containerName}`);
|
|
930
|
-
if (!createDockerContainer(context, containerName, imageName, config)) {
|
|
1090
|
+
if (!createDockerContainer(context, containerName, imageName, config, displayMode)) {
|
|
931
1091
|
return null;
|
|
932
1092
|
}
|
|
933
1093
|
const containerId = getContainerId(containerName);
|
|
@@ -1105,13 +1265,21 @@ export async function runDevcontainer(context, executor, config, displayMode = '
|
|
|
1105
1265
|
copyClaudeCredentials(context.agentDir);
|
|
1106
1266
|
// Start or reuse container using raw Docker commands
|
|
1107
1267
|
// No devcontainer CLI required!
|
|
1108
|
-
const containerId = ensureDockerContainer(context, config);
|
|
1268
|
+
const containerId = ensureDockerContainer(context, config, displayMode);
|
|
1109
1269
|
if (!containerId) {
|
|
1110
1270
|
return {
|
|
1111
1271
|
success: false,
|
|
1112
1272
|
error: 'Failed to start Docker container. Check Docker logs for details.',
|
|
1113
1273
|
};
|
|
1114
1274
|
}
|
|
1275
|
+
// Executor preflight check (TKT-1082): verify executor binary is available inside container
|
|
1276
|
+
const preflight = checkExecutorInContainer(executor, containerId);
|
|
1277
|
+
if (!preflight.ok) {
|
|
1278
|
+
return {
|
|
1279
|
+
success: false,
|
|
1280
|
+
error: preflight.error,
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1115
1283
|
// Write prompt to file in worktree (accessible by container)
|
|
1116
1284
|
const { hostPath: promptHostPath, containerPath: promptFile } = writePromptFile(context);
|
|
1117
1285
|
// Inject fresh GitHub token into container (containers may be reused with stale/empty tokens)
|
|
@@ -1456,21 +1624,10 @@ async function runDevcontainerInTmux(context, devcontainerCmd, config, displayMo
|
|
|
1456
1624
|
// Extract the claude command from the devcontainer command
|
|
1457
1625
|
const cmdMatch = devcontainerCmd.match(/bash -c '(.+)'$/);
|
|
1458
1626
|
const claudeCmd = cmdMatch ? cmdMatch[1] : devcontainerCmd;
|
|
1459
|
-
// Create a script inside the container that runs claude
|
|
1460
|
-
//
|
|
1461
|
-
//
|
|
1462
|
-
|
|
1463
|
-
const tmuxScript = `#!/bin/bash
|
|
1464
|
-
export TERM=xterm-256color
|
|
1465
|
-
export COLORTERM=truecolor
|
|
1466
|
-
unset CI
|
|
1467
|
-
echo "🚀 Starting: ${sessionName}"
|
|
1468
|
-
echo ""
|
|
1469
|
-
${claudeCmd}
|
|
1470
|
-
echo ""
|
|
1471
|
-
echo "✅ Agent work complete. Press Enter to close or run more commands."
|
|
1472
|
-
exec bash
|
|
1473
|
-
`;
|
|
1627
|
+
// Create a script inside the container that runs claude
|
|
1628
|
+
// Background mode (R1): kills PID 1 to stop container after completion
|
|
1629
|
+
// Terminal/foreground mode (R2): drops into exec bash for user inspection
|
|
1630
|
+
const tmuxScript = buildTmuxScript(sessionName, claudeCmd, displayMode);
|
|
1474
1631
|
const scriptPath = `/tmp/prlt-${sessionName}.sh`;
|
|
1475
1632
|
// Write script and start tmux session inside container
|
|
1476
1633
|
// IMPORTANT: We create the session with bash first, then send keys to run the script.
|
|
@@ -1740,6 +1897,8 @@ exec $SHELL
|
|
|
1740
1897
|
export async function runDocker(context, executor, config) {
|
|
1741
1898
|
const prompt = buildPrompt(context);
|
|
1742
1899
|
const containerName = `work-${context.ticketId}-${Date.now()}`;
|
|
1900
|
+
// Get the correct executor command (claude, codex, aider, etc.)
|
|
1901
|
+
const { cmd, args } = getExecutorCommand(executor, prompt, !config.sandboxed);
|
|
1743
1902
|
try {
|
|
1744
1903
|
// Check if docker is available
|
|
1745
1904
|
execSync('which docker', { stdio: 'pipe' });
|
|
@@ -1764,10 +1923,10 @@ export async function runDocker(context, executor, config) {
|
|
|
1764
1923
|
if (config.docker.cpus) {
|
|
1765
1924
|
dockerCmd += ` --cpus ${config.docker.cpus}`;
|
|
1766
1925
|
}
|
|
1767
|
-
//
|
|
1768
|
-
const
|
|
1926
|
+
// Build executor command with properly escaped args
|
|
1927
|
+
const escapedArgs = args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ');
|
|
1769
1928
|
dockerCmd += ` ${config.docker.image}`;
|
|
1770
|
-
dockerCmd += `
|
|
1929
|
+
dockerCmd += ` ${cmd} ${escapedArgs}`;
|
|
1771
1930
|
const containerId = execSync(dockerCmd, { encoding: 'utf-8' }).trim();
|
|
1772
1931
|
return {
|
|
1773
1932
|
success: true,
|
|
@@ -1796,6 +1955,8 @@ export async function runVm(context, executor, config, host) {
|
|
|
1796
1955
|
const user = config.vm.user;
|
|
1797
1956
|
const keyPath = config.vm.keyPath;
|
|
1798
1957
|
const remoteWorkspace = `/workspace/${context.agentName}`;
|
|
1958
|
+
// Get the correct executor command (claude, codex, aider, etc.)
|
|
1959
|
+
const { cmd, args } = getExecutorCommand(executor, prompt, !config.sandboxed);
|
|
1799
1960
|
try {
|
|
1800
1961
|
// Build SSH options
|
|
1801
1962
|
let sshOpts = '';
|
|
@@ -1817,9 +1978,9 @@ export async function runVm(context, executor, config, host) {
|
|
|
1817
1978
|
const gitPullCmd = `cd ${remoteWorkspace} && git fetch && git checkout ${context.branch}`;
|
|
1818
1979
|
execSync(`ssh ${sshOpts} ${user}@${targetHost} "${gitPullCmd}"`, { stdio: 'pipe' });
|
|
1819
1980
|
}
|
|
1820
|
-
// Execute on remote
|
|
1821
|
-
const
|
|
1822
|
-
const remoteCmd = `cd ${remoteWorkspace} &&
|
|
1981
|
+
// Execute on remote using the correct executor
|
|
1982
|
+
const escapedArgs = args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ');
|
|
1983
|
+
const remoteCmd = `cd ${remoteWorkspace} && ${cmd} ${escapedArgs}`;
|
|
1823
1984
|
const sshCmd = `ssh ${sshOpts} ${user}@${targetHost} "nohup ${remoteCmd} > /tmp/work-${context.ticketId}.log 2>&1 &"`;
|
|
1824
1985
|
execSync(sshCmd, { stdio: 'pipe' });
|
|
1825
1986
|
return {
|
|
@@ -2,8 +2,18 @@
|
|
|
2
2
|
* Session Utilities
|
|
3
3
|
*
|
|
4
4
|
* Shared utilities for tmux session naming, parsing, and discovery.
|
|
5
|
-
* Used by session/list.ts and session/
|
|
5
|
+
* Used by session/list.ts, session/attach.ts, session/health.ts, and session/peek.ts commands.
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* Capture the last N lines from a tmux pane.
|
|
9
|
+
* Supports both host tmux sessions and container tmux sessions (via docker exec).
|
|
10
|
+
*
|
|
11
|
+
* @param sessionId - The tmux session ID to capture from
|
|
12
|
+
* @param lines - Number of scrollback lines to capture
|
|
13
|
+
* @param containerId - Optional container ID for container-based sessions
|
|
14
|
+
* @returns The captured pane content, or null if capture fails
|
|
15
|
+
*/
|
|
16
|
+
export declare function captureTmuxPane(sessionId: string, lines: number, containerId?: string): string | null;
|
|
7
17
|
/**
|
|
8
18
|
* Known action names used in session naming.
|
|
9
19
|
* These are the actions defined in pmo/actions/ that may be used when spawning agents.
|
|
@@ -50,6 +60,11 @@ export declare function flattenContainerSessions(containerTmuxSessions: Map<stri
|
|
|
50
60
|
sessionName: string;
|
|
51
61
|
containerId: string;
|
|
52
62
|
}>;
|
|
63
|
+
/**
|
|
64
|
+
* Find container sessions using prefix matching.
|
|
65
|
+
* Handles short vs full container ID mismatches between DB records and docker output.
|
|
66
|
+
*/
|
|
67
|
+
export declare function findContainerSessionsByPrefix(containerTmuxSessions: Map<string, string[]>, containerId: string): string[];
|
|
53
68
|
/**
|
|
54
69
|
* Try to find a matching tmux session for an execution with NULL sessionId.
|
|
55
70
|
* First tries exact matches with known action names, then falls back to
|