@proletariat/cli 0.3.36 → 0.3.41
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/branch/where.js +6 -17
- package/dist/commands/epic/ticket.js +7 -24
- package/dist/commands/execution/config.js +4 -14
- package/dist/commands/execution/logs.js +6 -0
- package/dist/commands/execution/view.js +8 -0
- package/dist/commands/init.js +4 -8
- package/dist/commands/mcp-server.js +2 -1
- package/dist/commands/pmo/init.js +12 -40
- 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/health.js +4 -4
- package/dist/commands/session/list.js +1 -19
- package/dist/commands/session/peek.js +6 -6
- package/dist/commands/session/poke.js +2 -2
- package/dist/commands/ticket/epic.js +17 -43
- package/dist/commands/work/spawn-all.js +1 -1
- package/dist/commands/work/spawn.js +15 -4
- package/dist/commands/work/start.js +17 -9
- package/dist/commands/work/watch.js +1 -1
- package/dist/commands/workspace/prune.js +3 -3
- package/dist/hooks/init.js +21 -10
- package/dist/lib/agents/commands.d.ts +5 -0
- package/dist/lib/agents/commands.js +143 -97
- 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/runners.d.ts +34 -0
- package/dist/lib/execution/runners.js +134 -7
- package/dist/lib/execution/session-utils.d.ts +5 -0
- package/dist/lib/execution/session-utils.js +45 -3
- package/dist/lib/execution/spawner.js +15 -2
- package/dist/lib/execution/storage.d.ts +1 -1
- package/dist/lib/execution/storage.js +17 -2
- package/dist/lib/execution/types.d.ts +1 -0
- package/dist/lib/mcp/tools/index.d.ts +1 -0
- package/dist/lib/mcp/tools/index.js +1 -0
- 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 +52 -0
- package/dist/lib/pmo/schema.d.ts +1 -1
- package/dist/lib/pmo/schema.js +1 -0
- package/dist/lib/pmo/storage/base.js +207 -0
- 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/views.d.ts +2 -0
- package/dist/lib/pmo/storage/views.js +183 -130
- package/dist/lib/prompt-json.d.ts +5 -0
- package/dist/lib/prompt-json.js +9 -0
- package/oclif.manifest.json +3293 -3190
- 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
|
/**
|
|
@@ -74,6 +74,40 @@ export declare function getDockerCredentialInfo(): {
|
|
|
74
74
|
expiresAt: Date;
|
|
75
75
|
subscriptionType?: string;
|
|
76
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
|
+
};
|
|
77
111
|
export interface RunnerResult {
|
|
78
112
|
success: boolean;
|
|
79
113
|
pid?: string;
|
|
@@ -188,10 +188,125 @@ export function getDockerCredentialInfo() {
|
|
|
188
188
|
return null;
|
|
189
189
|
}
|
|
190
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
|
+
}
|
|
191
306
|
// =============================================================================
|
|
192
307
|
// Executor Commands
|
|
193
308
|
// =============================================================================
|
|
194
|
-
function getExecutorCommand(executor, prompt, skipPermissions = true) {
|
|
309
|
+
export function getExecutorCommand(executor, prompt, skipPermissions = true) {
|
|
195
310
|
switch (executor) {
|
|
196
311
|
case 'claude-code':
|
|
197
312
|
if (skipPermissions) {
|
|
@@ -1157,6 +1272,14 @@ export async function runDevcontainer(context, executor, config, displayMode = '
|
|
|
1157
1272
|
error: 'Failed to start Docker container. Check Docker logs for details.',
|
|
1158
1273
|
};
|
|
1159
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
|
+
}
|
|
1160
1283
|
// Write prompt to file in worktree (accessible by container)
|
|
1161
1284
|
const { hostPath: promptHostPath, containerPath: promptFile } = writePromptFile(context);
|
|
1162
1285
|
// Inject fresh GitHub token into container (containers may be reused with stale/empty tokens)
|
|
@@ -1774,6 +1897,8 @@ exec $SHELL
|
|
|
1774
1897
|
export async function runDocker(context, executor, config) {
|
|
1775
1898
|
const prompt = buildPrompt(context);
|
|
1776
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);
|
|
1777
1902
|
try {
|
|
1778
1903
|
// Check if docker is available
|
|
1779
1904
|
execSync('which docker', { stdio: 'pipe' });
|
|
@@ -1798,10 +1923,10 @@ export async function runDocker(context, executor, config) {
|
|
|
1798
1923
|
if (config.docker.cpus) {
|
|
1799
1924
|
dockerCmd += ` --cpus ${config.docker.cpus}`;
|
|
1800
1925
|
}
|
|
1801
|
-
//
|
|
1802
|
-
const
|
|
1926
|
+
// Build executor command with properly escaped args
|
|
1927
|
+
const escapedArgs = args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ');
|
|
1803
1928
|
dockerCmd += ` ${config.docker.image}`;
|
|
1804
|
-
dockerCmd += `
|
|
1929
|
+
dockerCmd += ` ${cmd} ${escapedArgs}`;
|
|
1805
1930
|
const containerId = execSync(dockerCmd, { encoding: 'utf-8' }).trim();
|
|
1806
1931
|
return {
|
|
1807
1932
|
success: true,
|
|
@@ -1830,6 +1955,8 @@ export async function runVm(context, executor, config, host) {
|
|
|
1830
1955
|
const user = config.vm.user;
|
|
1831
1956
|
const keyPath = config.vm.keyPath;
|
|
1832
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);
|
|
1833
1960
|
try {
|
|
1834
1961
|
// Build SSH options
|
|
1835
1962
|
let sshOpts = '';
|
|
@@ -1851,9 +1978,9 @@ export async function runVm(context, executor, config, host) {
|
|
|
1851
1978
|
const gitPullCmd = `cd ${remoteWorkspace} && git fetch && git checkout ${context.branch}`;
|
|
1852
1979
|
execSync(`ssh ${sshOpts} ${user}@${targetHost} "${gitPullCmd}"`, { stdio: 'pipe' });
|
|
1853
1980
|
}
|
|
1854
|
-
// Execute on remote
|
|
1855
|
-
const
|
|
1856
|
-
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}`;
|
|
1857
1984
|
const sshCmd = `ssh ${sshOpts} ${user}@${targetHost} "nohup ${remoteCmd} > /tmp/work-${context.ticketId}.log 2>&1 &"`;
|
|
1858
1985
|
execSync(sshCmd, { stdio: 'pipe' });
|
|
1859
1986
|
return {
|
|
@@ -60,6 +60,11 @@ export declare function flattenContainerSessions(containerTmuxSessions: Map<stri
|
|
|
60
60
|
sessionName: string;
|
|
61
61
|
containerId: string;
|
|
62
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[];
|
|
63
68
|
/**
|
|
64
69
|
* Try to find a matching tmux session for an execution with NULL sessionId.
|
|
65
70
|
* First tries exact matches with known action names, then falls back to
|
|
@@ -127,10 +127,37 @@ export function getHostTmuxSessionNames() {
|
|
|
127
127
|
export function getContainerTmuxSessionMap() {
|
|
128
128
|
const sessionMap = new Map();
|
|
129
129
|
try {
|
|
130
|
-
const
|
|
131
|
-
|
|
130
|
+
const containerIds = new Set();
|
|
131
|
+
// Primary: devcontainer-labeled containers (historical behavior).
|
|
132
|
+
try {
|
|
133
|
+
const labeledOutput = execSync('docker ps --filter "label=devcontainer.local_folder" --format "{{.ID}}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
134
|
+
if (labeledOutput) {
|
|
135
|
+
for (const id of labeledOutput.split('\n')) {
|
|
136
|
+
if (id.trim())
|
|
137
|
+
containerIds.add(id.trim());
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Ignore and continue with broad fallback discovery.
|
|
143
|
+
}
|
|
144
|
+
// Fallback: include all running containers so prlt-managed devcontainers
|
|
145
|
+
// without the devcontainer.local_folder label are still discoverable.
|
|
146
|
+
try {
|
|
147
|
+
const allOutput = execSync('docker ps --format "{{.ID}}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
148
|
+
if (allOutput) {
|
|
149
|
+
for (const id of allOutput.split('\n')) {
|
|
150
|
+
if (id.trim())
|
|
151
|
+
containerIds.add(id.trim());
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Docker not available.
|
|
157
|
+
}
|
|
158
|
+
if (containerIds.size === 0)
|
|
132
159
|
return sessionMap;
|
|
133
|
-
for (const containerId of
|
|
160
|
+
for (const containerId of containerIds) {
|
|
134
161
|
try {
|
|
135
162
|
const tmuxOutput = execSync(`docker exec ${containerId} tmux list-sessions -F "#{session_name}" 2>/dev/null`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
136
163
|
if (tmuxOutput) {
|
|
@@ -159,6 +186,21 @@ export function flattenContainerSessions(containerTmuxSessions) {
|
|
|
159
186
|
});
|
|
160
187
|
return result;
|
|
161
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Find container sessions using prefix matching.
|
|
191
|
+
* Handles short vs full container ID mismatches between DB records and docker output.
|
|
192
|
+
*/
|
|
193
|
+
export function findContainerSessionsByPrefix(containerTmuxSessions, containerId) {
|
|
194
|
+
const exact = containerTmuxSessions.get(containerId);
|
|
195
|
+
if (exact)
|
|
196
|
+
return exact;
|
|
197
|
+
for (const [key, sessions] of containerTmuxSessions) {
|
|
198
|
+
if (key.startsWith(containerId) || containerId.startsWith(key)) {
|
|
199
|
+
return sessions;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
162
204
|
/**
|
|
163
205
|
* Try to find a matching tmux session for an execution with NULL sessionId.
|
|
164
206
|
* First tries exact matches with known action names, then falls back to
|
|
@@ -13,7 +13,7 @@ import { findHQRoot } from '../repos/index.js';
|
|
|
13
13
|
import { hasGitHubRemote } from '../repos/git.js';
|
|
14
14
|
import { hasDevcontainerConfig } from './devcontainer.js';
|
|
15
15
|
import { loadExecutionConfig, getOrPromptCoderName } from './config.js';
|
|
16
|
-
import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled } from './runners.js';
|
|
16
|
+
import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled, runExecutorPreflight } from './runners.js';
|
|
17
17
|
import { detectRepoWorktrees, resolveWorktreePath } from './context.js';
|
|
18
18
|
import { generateBranchName, DEFAULT_EXECUTION_CONFIG, } from './types.js';
|
|
19
19
|
// =============================================================================
|
|
@@ -282,6 +282,19 @@ export async function spawnAgentForTicket(ticket, agentName, storage, executionS
|
|
|
282
282
|
}
|
|
283
283
|
const displayMode = options.displayMode || 'terminal';
|
|
284
284
|
const sandboxed = !(options.skipPermissions ?? false);
|
|
285
|
+
// Executor preflight check (TKT-1082): verify binary is available before proceeding
|
|
286
|
+
// For host environment, check immediately. For devcontainer, check happens after container start.
|
|
287
|
+
if (environment === 'host') {
|
|
288
|
+
const preflight = runExecutorPreflight(executor, environment);
|
|
289
|
+
if (!preflight.ok) {
|
|
290
|
+
return {
|
|
291
|
+
success: false,
|
|
292
|
+
ticketId: ticket.id,
|
|
293
|
+
agentName,
|
|
294
|
+
error: preflight.error,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
285
298
|
// Create branch in worktree(s)
|
|
286
299
|
// For devcontainer environments, run git commands inside the container
|
|
287
300
|
// because the worktree .git file has container paths, not host paths
|
|
@@ -438,7 +451,7 @@ export async function spawnAgentForTicket(ticket, agentName, storage, executionS
|
|
|
438
451
|
};
|
|
439
452
|
}
|
|
440
453
|
else {
|
|
441
|
-
executionStorage.updateStatus(execution.id, 'failed');
|
|
454
|
+
executionStorage.updateStatus(execution.id, 'failed', undefined, result.error);
|
|
442
455
|
return {
|
|
443
456
|
success: false,
|
|
444
457
|
executionId: execution.id,
|
|
@@ -33,7 +33,7 @@ export declare class ExecutionStorage {
|
|
|
33
33
|
/**
|
|
34
34
|
* Update execution status
|
|
35
35
|
*/
|
|
36
|
-
updateStatus(id: string, status: ExecutionStatus, exitCode?: number): void;
|
|
36
|
+
updateStatus(id: string, status: ExecutionStatus, exitCode?: number, errorMessage?: string): void;
|
|
37
37
|
/**
|
|
38
38
|
* Update execution with process info
|
|
39
39
|
*/
|
|
@@ -29,6 +29,7 @@ function rowToAgentWork(row) {
|
|
|
29
29
|
startedAt: new Date(row.started_at),
|
|
30
30
|
completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
|
|
31
31
|
exitCode: row.exit_code ?? undefined,
|
|
32
|
+
errorMessage: row.error_message || undefined,
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
// =============================================================================
|
|
@@ -68,14 +69,28 @@ export class ExecutionStorage {
|
|
|
68
69
|
/**
|
|
69
70
|
* Update execution status
|
|
70
71
|
*/
|
|
71
|
-
updateStatus(id, status, exitCode) {
|
|
72
|
+
updateStatus(id, status, exitCode, errorMessage) {
|
|
72
73
|
const completedAt = ['completed', 'failed', 'stopped'].includes(status) ? Date.now() : null;
|
|
73
|
-
if (exitCode !== undefined) {
|
|
74
|
+
if (exitCode !== undefined && errorMessage) {
|
|
75
|
+
this.db.prepare(`
|
|
76
|
+
UPDATE ${T.agent_work}
|
|
77
|
+
SET status = ?, completed_at = ?, exit_code = ?, error_message = ?
|
|
78
|
+
WHERE id = ?
|
|
79
|
+
`).run(status, completedAt, exitCode, errorMessage, id);
|
|
80
|
+
}
|
|
81
|
+
else if (exitCode !== undefined) {
|
|
74
82
|
this.db.prepare(`
|
|
75
83
|
UPDATE ${T.agent_work}
|
|
76
84
|
SET status = ?, completed_at = ?, exit_code = ?
|
|
77
85
|
WHERE id = ?
|
|
78
86
|
`).run(status, completedAt, exitCode, id);
|
|
87
|
+
}
|
|
88
|
+
else if (errorMessage) {
|
|
89
|
+
this.db.prepare(`
|
|
90
|
+
UPDATE ${T.agent_work}
|
|
91
|
+
SET status = ?, completed_at = ?, error_message = ?
|
|
92
|
+
WHERE id = ?
|
|
93
|
+
`).run(status, completedAt, errorMessage, id);
|
|
79
94
|
}
|
|
80
95
|
else {
|
|
81
96
|
this.db.prepare(`
|
|
@@ -17,4 +17,5 @@ export { registerTemplateTools } from './template.js';
|
|
|
17
17
|
export { registerViewTools } from './view.js';
|
|
18
18
|
export { registerDietTools } from './diet.js';
|
|
19
19
|
export { registerLabelTools } from './label.js';
|
|
20
|
+
export { registerTmuxTools } from './tmux.js';
|
|
20
21
|
export { registerAgentTools, registerDockerTools, registerRepoTools, registerBranchTools, registerGitHubTools, registerInitTools, registerUtilityTools, } from './cli-passthrough.js';
|
|
@@ -17,5 +17,6 @@ export { registerTemplateTools } from './template.js';
|
|
|
17
17
|
export { registerViewTools } from './view.js';
|
|
18
18
|
export { registerDietTools } from './diet.js';
|
|
19
19
|
export { registerLabelTools } from './label.js';
|
|
20
|
+
export { registerTmuxTools } from './tmux.js';
|
|
20
21
|
// CLI passthrough tools
|
|
21
22
|
export { registerAgentTools, registerDockerTools, registerRepoTools, registerBranchTools, registerGitHubTools, registerInitTools, registerUtilityTools, } from './cli-passthrough.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tmux Tools
|
|
3
|
+
*
|
|
4
|
+
* Provides tmux interaction tools for AI agents to drive interactive CLI sessions.
|
|
5
|
+
* Used by the explore-cli action to perform exploratory QA testing.
|
|
6
|
+
*
|
|
7
|
+
* Tools:
|
|
8
|
+
* - tmux_send_keys: Send keystrokes to a tmux session
|
|
9
|
+
* - tmux_capture_pane: Capture current terminal screen from a tmux session
|
|
10
|
+
* - tmux_start_session: Start a new tmux session with an optional command
|
|
11
|
+
* - tmux_list_sessions: List active tmux sessions
|
|
12
|
+
* - tmux_kill_session: Kill a tmux session
|
|
13
|
+
*/
|
|
14
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
|
+
import type { McpToolContext } from '../types.js';
|
|
16
|
+
export declare function registerTmuxTools(server: McpServer, _ctx: McpToolContext): void;
|