@proletariat/cli 0.3.24 → 0.3.25
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/dist/commands/action/create.js +3 -3
- package/dist/commands/action/update.js +3 -3
- package/dist/commands/epic/activate.js +9 -17
- package/dist/commands/epic/archive.js +13 -24
- package/dist/commands/epic/create.js +7 -6
- package/dist/commands/epic/move.js +28 -47
- package/dist/commands/epic/progress.js +10 -14
- package/dist/commands/epic/project.js +42 -59
- package/dist/commands/epic/reorder.js +25 -30
- package/dist/commands/epic/spec.d.ts +1 -0
- package/dist/commands/epic/spec.js +39 -40
- package/dist/commands/epic/ticket.d.ts +2 -0
- package/dist/commands/epic/ticket.js +63 -37
- package/dist/commands/feedback/index.d.ts +10 -0
- package/dist/commands/feedback/index.js +60 -0
- package/dist/commands/feedback/list.d.ts +12 -0
- package/dist/commands/feedback/list.js +126 -0
- package/dist/commands/feedback/submit.d.ts +16 -0
- package/dist/commands/feedback/submit.js +220 -0
- package/dist/commands/feedback/view.d.ts +15 -0
- package/dist/commands/feedback/view.js +109 -0
- package/dist/commands/gh/index.js +4 -0
- package/dist/commands/repo/create.d.ts +38 -0
- package/dist/commands/repo/create.js +283 -0
- package/dist/commands/repo/index.js +7 -0
- package/dist/commands/roadmap/add-project.js +9 -22
- package/dist/commands/roadmap/create.d.ts +0 -1
- package/dist/commands/roadmap/create.js +46 -40
- package/dist/commands/roadmap/delete.js +10 -24
- package/dist/commands/roadmap/generate.d.ts +1 -0
- package/dist/commands/roadmap/generate.js +21 -22
- package/dist/commands/roadmap/remove-project.js +14 -34
- package/dist/commands/roadmap/reorder.js +19 -26
- package/dist/commands/roadmap/update.js +27 -26
- package/dist/commands/roadmap/view.js +5 -12
- package/dist/commands/session/attach.d.ts +1 -8
- package/dist/commands/session/attach.js +93 -59
- package/dist/commands/session/list.d.ts +0 -8
- package/dist/commands/session/list.js +130 -81
- package/dist/commands/spec/create.js +1 -1
- package/dist/commands/spec/edit.js +63 -33
- package/dist/commands/support/book.d.ts +10 -0
- package/dist/commands/support/book.js +54 -0
- package/dist/commands/support/discord.d.ts +10 -0
- package/dist/commands/support/discord.js +54 -0
- package/dist/commands/support/docs.d.ts +10 -0
- package/dist/commands/support/docs.js +54 -0
- package/dist/commands/support/index.d.ts +19 -0
- package/dist/commands/support/index.js +81 -0
- package/dist/commands/support/issues.d.ts +11 -0
- package/dist/commands/support/issues.js +77 -0
- package/dist/commands/support/logs.d.ts +18 -0
- package/dist/commands/support/logs.js +247 -0
- package/dist/commands/ticket/create.js +21 -13
- package/dist/commands/ticket/edit.js +44 -13
- package/dist/commands/ticket/move.d.ts +7 -0
- package/dist/commands/ticket/move.js +132 -0
- package/dist/commands/work/spawn.d.ts +1 -0
- package/dist/commands/work/spawn.js +71 -7
- package/dist/commands/work/start.js +6 -0
- package/dist/lib/execution/runners.js +21 -17
- package/dist/lib/execution/session-utils.d.ts +60 -0
- package/dist/lib/execution/session-utils.js +162 -0
- package/dist/lib/execution/spawner.d.ts +2 -0
- package/dist/lib/execution/spawner.js +42 -0
- package/dist/lib/flags/resolver.d.ts +2 -2
- package/dist/lib/flags/resolver.js +15 -0
- package/dist/lib/init/index.js +18 -0
- package/dist/lib/multiline-input.d.ts +63 -0
- package/dist/lib/multiline-input.js +360 -0
- package/dist/lib/prompt-json.d.ts +5 -5
- package/dist/lib/repos/git.d.ts +7 -0
- package/dist/lib/repos/git.js +20 -0
- package/oclif.manifest.json +2206 -1607
- package/package.json +1 -1
|
@@ -19,6 +19,7 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
19
19
|
'<%= config.bin %> <%= command.id %> TKT-001 TKT-002 # Spawn specific tickets by ID',
|
|
20
20
|
'<%= config.bin %> <%= command.id %> --dry-run # Preview without executing',
|
|
21
21
|
'<%= config.bin %> <%= command.id %> --many --json # Output ticket choices as JSON (for agents)',
|
|
22
|
+
'<%= config.bin %> <%= command.id %> TKT-001 --action custom --message "Add unit tests" # Custom prompt',
|
|
22
23
|
];
|
|
23
24
|
static flags = {
|
|
24
25
|
...pmoBaseFlags,
|
|
@@ -101,7 +102,10 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
101
102
|
default: false,
|
|
102
103
|
}),
|
|
103
104
|
action: Flags.string({
|
|
104
|
-
description: 'Action to perform (e.g., groom, implement, review). Prompts if not provided.',
|
|
105
|
+
description: 'Action to perform (e.g., groom, implement, review, custom). Prompts if not provided.',
|
|
106
|
+
}),
|
|
107
|
+
message: Flags.string({
|
|
108
|
+
description: 'Custom prompt/message for the agent (use with --action custom)',
|
|
105
109
|
}),
|
|
106
110
|
session: Flags.string({
|
|
107
111
|
description: 'Session manager inside container (tmux runs agent in tmux inside container)',
|
|
@@ -522,6 +526,8 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
522
526
|
let batchNoPr = flags['no-pr'];
|
|
523
527
|
let batchRunOnHost = flags['run-on-host'];
|
|
524
528
|
let batchAction = flags.action;
|
|
529
|
+
// Track custom message for custom action (needs to be outside the if block)
|
|
530
|
+
let batchCustomMessage = flags.message;
|
|
525
531
|
// Track display mode separately for devcontainer (needs to be outside the if block)
|
|
526
532
|
let batchDisplayMode;
|
|
527
533
|
// For ephemeral agents, we'll create devcontainers on-demand
|
|
@@ -542,12 +548,16 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
542
548
|
name: `${a.id.padEnd(12)} - ${a.description || a.name}`,
|
|
543
549
|
value: a.id,
|
|
544
550
|
}));
|
|
545
|
-
// Add adhoc
|
|
551
|
+
// Add custom and adhoc options at the end
|
|
552
|
+
actionChoices.push({
|
|
553
|
+
name: 'custom - Enter a custom prompt/instruction',
|
|
554
|
+
value: '__custom__',
|
|
555
|
+
});
|
|
546
556
|
actionChoices.push({
|
|
547
557
|
name: 'adhoc - Unstructured exploration/debugging',
|
|
548
558
|
value: '__adhoc__',
|
|
549
559
|
});
|
|
550
|
-
// Use FlagResolver for action selection
|
|
560
|
+
// Use FlagResolver for action selection with optional custom input
|
|
551
561
|
const actionResolver = new FlagResolver({
|
|
552
562
|
commandName: 'work spawn',
|
|
553
563
|
baseCommand: 'prlt work spawn',
|
|
@@ -561,12 +571,54 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
561
571
|
default: 'implement',
|
|
562
572
|
choices: () => actionChoices,
|
|
563
573
|
});
|
|
574
|
+
actionResolver.addPrompt({
|
|
575
|
+
flagName: 'customInput',
|
|
576
|
+
type: 'input',
|
|
577
|
+
message: 'Enter custom prompt for the agent:',
|
|
578
|
+
when: (ctx) => ctx.flags.selectedAction === '__custom__',
|
|
579
|
+
validate: (value) => value.trim() ? true : 'Prompt cannot be empty',
|
|
580
|
+
});
|
|
564
581
|
const actionResult = await actionResolver.resolve();
|
|
565
582
|
const selectedAction = actionResult.selectedAction;
|
|
566
|
-
|
|
583
|
+
if (selectedAction === '__custom__') {
|
|
584
|
+
batchAction = 'custom';
|
|
585
|
+
batchCustomMessage = actionResult.customInput.trim();
|
|
586
|
+
}
|
|
587
|
+
else if (selectedAction === '__adhoc__') {
|
|
588
|
+
batchAction = 'adhoc';
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
batchAction = selectedAction;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
else if (flags.action === 'custom') {
|
|
595
|
+
// Custom action specified via flag - require --message
|
|
596
|
+
if (!flags.message) {
|
|
597
|
+
db.close();
|
|
598
|
+
return handleError('MISSING_MESSAGE', '--action custom requires --message flag with the custom prompt');
|
|
599
|
+
}
|
|
600
|
+
batchAction = 'custom';
|
|
601
|
+
batchCustomMessage = flags.message;
|
|
602
|
+
}
|
|
603
|
+
else if (flags.message && flags.action !== 'custom') {
|
|
604
|
+
// --message provided without --action custom - warn user
|
|
605
|
+
this.warn('--message flag is only used with --action custom, ignoring');
|
|
567
606
|
}
|
|
568
607
|
// Now fetch action details after selection is made
|
|
569
|
-
if (batchAction === '
|
|
608
|
+
if (batchAction === 'custom') {
|
|
609
|
+
// Custom action - user provides their own prompt
|
|
610
|
+
selectedActionDetails = {
|
|
611
|
+
id: 'custom',
|
|
612
|
+
name: 'Custom',
|
|
613
|
+
description: 'Custom prompt/instruction',
|
|
614
|
+
prompt: batchCustomMessage || '',
|
|
615
|
+
modifiesCode: true, // Assume custom prompts may modify code
|
|
616
|
+
defaultMoveToCategory: 'started',
|
|
617
|
+
isBuiltin: false,
|
|
618
|
+
createdAt: new Date(),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
else if (batchAction === 'adhoc') {
|
|
570
622
|
// Adhoc is a synthetic action, not stored in database
|
|
571
623
|
selectedActionDetails = {
|
|
572
624
|
id: 'adhoc',
|
|
@@ -899,6 +951,13 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
899
951
|
startArgs.push('--force');
|
|
900
952
|
if (flags.focus)
|
|
901
953
|
startArgs.push('--focus');
|
|
954
|
+
// Pass action/prompt - custom action uses --prompt, others use --action
|
|
955
|
+
if (batchAction === 'custom' && batchCustomMessage) {
|
|
956
|
+
startArgs.push('--prompt', batchCustomMessage);
|
|
957
|
+
}
|
|
958
|
+
else if (batchAction) {
|
|
959
|
+
startArgs.push('--action', batchAction);
|
|
960
|
+
}
|
|
902
961
|
}
|
|
903
962
|
else {
|
|
904
963
|
// Batch mode: pass all settings to skip prompts
|
|
@@ -920,8 +979,13 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
920
979
|
startArgs.push('--create-pr');
|
|
921
980
|
if (batchNoPr)
|
|
922
981
|
startArgs.push('--no-pr');
|
|
923
|
-
// Pass action
|
|
924
|
-
|
|
982
|
+
// Pass action/prompt - custom action uses --prompt, others use --action
|
|
983
|
+
if (batchAction === 'custom' && batchCustomMessage) {
|
|
984
|
+
startArgs.push('--prompt', batchCustomMessage);
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
startArgs.push('--action', batchAction || 'implement');
|
|
988
|
+
}
|
|
925
989
|
// Pass session manager (tmux inside container by default)
|
|
926
990
|
if (flags.session)
|
|
927
991
|
startArgs.push('--session', flags.session);
|
|
@@ -76,6 +76,7 @@ export default class WorkStart extends PMOCommand {
|
|
|
76
76
|
'<%= config.bin %> <%= command.id %> TKT-001 --mode terminal',
|
|
77
77
|
'<%= config.bin %> <%= command.id %> # Interactive mode',
|
|
78
78
|
'<%= config.bin %> <%= command.id %> --all # Spawn all backlog tickets',
|
|
79
|
+
'<%= config.bin %> <%= command.id %> TKT-001 --prompt "Add unit tests for the API" # Custom prompt',
|
|
79
80
|
];
|
|
80
81
|
static args = {
|
|
81
82
|
ticketId: Args.string({
|
|
@@ -572,6 +573,11 @@ export default class WorkStart extends PMOCommand {
|
|
|
572
573
|
customPrompt = flags.prompt;
|
|
573
574
|
}
|
|
574
575
|
else if (flags.action) {
|
|
576
|
+
// Handle special "custom" action - requires --prompt flag
|
|
577
|
+
if (flags.action === 'custom') {
|
|
578
|
+
db.close();
|
|
579
|
+
this.error('--action custom requires --prompt flag.\nUsage: prlt work start TKT-001 --action custom --prompt "your custom instructions"');
|
|
580
|
+
}
|
|
575
581
|
// Specific action requested
|
|
576
582
|
selectedAction = await this.storage.getAction(flags.action);
|
|
577
583
|
if (!selectedAction) {
|
|
@@ -262,6 +262,8 @@ function buildPrompt(context) {
|
|
|
262
262
|
prompt += `When you have completed the task, provide a summary of what you did.`;
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
|
+
// Universal stop instruction - prevents Claude Code from making additional API calls after task completion
|
|
266
|
+
prompt += `\n\n---\n\n**STOP:** After providing your final summary, your task is complete. Do not take any further actions, do not verify your work again, and do not continue the conversation. Simply output your summary and stop.`;
|
|
265
267
|
return prompt;
|
|
266
268
|
}
|
|
267
269
|
// =============================================================================
|
|
@@ -1479,9 +1481,11 @@ exec bash
|
|
|
1479
1481
|
error: `Failed to write script to container: ${error instanceof Error ? error.message : error}`,
|
|
1480
1482
|
};
|
|
1481
1483
|
}
|
|
1482
|
-
// Step 2: Create tmux session
|
|
1483
|
-
//
|
|
1484
|
-
|
|
1484
|
+
// Step 2: Create tmux session running the script directly
|
|
1485
|
+
// Pass the script as the session command (like host runner does) instead of using send-keys.
|
|
1486
|
+
// The send-keys approach had a race condition where keys could be lost if bash hadn't
|
|
1487
|
+
// fully initialized, causing background mode to create empty sessions without running claude.
|
|
1488
|
+
const createSessionCmd = `tmux new-session -d -s "${sessionName}" -n "${sessionName}" "bash ${scriptPath}"${mouseOption} \\; set-option -g set-titles on \\; set-option -g set-titles-string "#{window_name}"`;
|
|
1485
1489
|
try {
|
|
1486
1490
|
execSync(`docker exec ${actualContainerId} bash -c '${createSessionCmd}'`, { stdio: 'pipe' });
|
|
1487
1491
|
}
|
|
@@ -1491,28 +1495,28 @@ exec bash
|
|
|
1491
1495
|
error: `Failed to create tmux session inside container: ${error instanceof Error ? error.message : error}`,
|
|
1492
1496
|
};
|
|
1493
1497
|
}
|
|
1494
|
-
// Step 3:
|
|
1495
|
-
|
|
1496
|
-
try {
|
|
1497
|
-
execSync(`docker exec ${actualContainerId} bash -c '${sendKeysCmd}'`, { stdio: 'pipe' });
|
|
1498
|
-
}
|
|
1499
|
-
catch (error) {
|
|
1500
|
-
return {
|
|
1501
|
-
success: false,
|
|
1502
|
-
error: `Failed to start script in tmux session: ${error instanceof Error ? error.message : error}`,
|
|
1503
|
-
};
|
|
1504
|
-
}
|
|
1505
|
-
// Step 2: Open iTerm tab that attaches directly to container's tmux
|
|
1506
|
-
// Skip this step for background mode - just return success after tmux session is created
|
|
1498
|
+
// Step 3: Handle display mode
|
|
1499
|
+
// For background mode, return success after tmux session is created
|
|
1507
1500
|
// User can reattach later with `prlt session attach`
|
|
1508
1501
|
if (displayMode === 'background') {
|
|
1502
|
+
// Verify the tmux session was actually created (brief delay to let tmux start)
|
|
1503
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1504
|
+
try {
|
|
1505
|
+
execSync(`docker exec ${actualContainerId} tmux has-session -t "${sessionName}" 2>&1`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1506
|
+
}
|
|
1507
|
+
catch (err) {
|
|
1508
|
+
return {
|
|
1509
|
+
success: false,
|
|
1510
|
+
error: `Failed to verify tmux session "${sessionName}" inside container. The session may not have started correctly.`,
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1509
1513
|
return {
|
|
1510
1514
|
success: true,
|
|
1511
1515
|
containerId: actualContainerId,
|
|
1512
1516
|
sessionId: sessionName, // Container tmux session name for tracking
|
|
1513
1517
|
};
|
|
1514
1518
|
}
|
|
1515
|
-
//
|
|
1519
|
+
// For foreground mode: attach to container's tmux session in current terminal (blocking)
|
|
1516
1520
|
if (displayMode === 'foreground') {
|
|
1517
1521
|
try {
|
|
1518
1522
|
// Clear screen and attach - this blocks until user detaches or claude exits
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for tmux session naming, parsing, and discovery.
|
|
5
|
+
* Used by session/list.ts and session/attach.ts commands.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Known action names used in session naming.
|
|
9
|
+
* These are the actions defined in pmo/actions/ that may be used when spawning agents.
|
|
10
|
+
*/
|
|
11
|
+
export declare const KNOWN_ACTIONS: readonly ["Implement", "Review", "Fix", "Refactor", "Test", "Document", "work"];
|
|
12
|
+
/**
|
|
13
|
+
* Parse a tmux session name following prlt naming convention.
|
|
14
|
+
* Format: {ticketId}-{action}-{agentName}
|
|
15
|
+
*
|
|
16
|
+
* Note: Agent names can contain hyphens (like "stout-page"), so we match
|
|
17
|
+
* from the end using known action names to correctly split the components.
|
|
18
|
+
*
|
|
19
|
+
* Example: "TKT-878-Implement-stout-page" -> { ticketId: "TKT-878", action: "Implement", agentName: "stout-page" }
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseSessionName(sessionName: string): {
|
|
22
|
+
ticketId: string;
|
|
23
|
+
action: string;
|
|
24
|
+
agentName: string;
|
|
25
|
+
} | null;
|
|
26
|
+
/**
|
|
27
|
+
* Build expected session name from execution data.
|
|
28
|
+
* Format: {ticketId}-{action}-{agentName}
|
|
29
|
+
* This is the same format used by runners.ts buildSessionName()
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildExpectedSessionName(ticketId: string, agentName: string, action?: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Check if a session name matches the expected pattern for a ticket and agent.
|
|
34
|
+
* Parses the session name and verifies the ticket ID and agent name match exactly.
|
|
35
|
+
*/
|
|
36
|
+
export declare function sessionMatchesExecution(sessionName: string, ticketId: string, agentName: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Get list of host tmux session names.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getHostTmuxSessionNames(): string[];
|
|
41
|
+
/**
|
|
42
|
+
* Get map of containerId -> tmux session names.
|
|
43
|
+
* Only checks containers with the devcontainer.local_folder label.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getContainerTmuxSessionMap(): Map<string, string[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Flatten container sessions map into an array for easier iteration.
|
|
48
|
+
*/
|
|
49
|
+
export declare function flattenContainerSessions(containerTmuxSessions: Map<string, string[]>): Array<{
|
|
50
|
+
sessionName: string;
|
|
51
|
+
containerId: string;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Try to find a matching tmux session for an execution with NULL sessionId.
|
|
55
|
+
* First tries exact matches with known action names, then falls back to
|
|
56
|
+
* partial matching with agent name verification.
|
|
57
|
+
*
|
|
58
|
+
* @returns The matched session name, or null if no match found
|
|
59
|
+
*/
|
|
60
|
+
export declare function findSessionForExecution(ticketId: string, agentName: string, availableSessions: string[]): string | null;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for tmux session naming, parsing, and discovery.
|
|
5
|
+
* Used by session/list.ts and session/attach.ts commands.
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
/**
|
|
9
|
+
* Known action names used in session naming.
|
|
10
|
+
* These are the actions defined in pmo/actions/ that may be used when spawning agents.
|
|
11
|
+
*/
|
|
12
|
+
export const KNOWN_ACTIONS = [
|
|
13
|
+
'Implement',
|
|
14
|
+
'Review',
|
|
15
|
+
'Fix',
|
|
16
|
+
'Refactor',
|
|
17
|
+
'Test',
|
|
18
|
+
'Document',
|
|
19
|
+
'work', // Default fallback
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* Parse a tmux session name following prlt naming convention.
|
|
23
|
+
* Format: {ticketId}-{action}-{agentName}
|
|
24
|
+
*
|
|
25
|
+
* Note: Agent names can contain hyphens (like "stout-page"), so we match
|
|
26
|
+
* from the end using known action names to correctly split the components.
|
|
27
|
+
*
|
|
28
|
+
* Example: "TKT-878-Implement-stout-page" -> { ticketId: "TKT-878", action: "Implement", agentName: "stout-page" }
|
|
29
|
+
*/
|
|
30
|
+
export function parseSessionName(sessionName) {
|
|
31
|
+
// First, extract the ticket ID (format: TKT-### or PROJECT-###)
|
|
32
|
+
const ticketMatch = sessionName.match(/^(TKT-\d+|[A-Z]+-\d+)-/);
|
|
33
|
+
if (!ticketMatch) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const ticketId = ticketMatch[1];
|
|
37
|
+
const remainder = sessionName.slice(ticketMatch[0].length);
|
|
38
|
+
// Try to match known actions (case-insensitive) at the start of the remainder
|
|
39
|
+
for (const action of KNOWN_ACTIONS) {
|
|
40
|
+
const actionLower = action.toLowerCase();
|
|
41
|
+
const remainderLower = remainder.toLowerCase();
|
|
42
|
+
// Check if remainder starts with this action followed by a hyphen
|
|
43
|
+
if (remainderLower.startsWith(actionLower + '-')) {
|
|
44
|
+
const agentName = remainder.slice(action.length + 1);
|
|
45
|
+
if (agentName) {
|
|
46
|
+
return {
|
|
47
|
+
ticketId,
|
|
48
|
+
action: remainder.slice(0, action.length), // Preserve original casing
|
|
49
|
+
agentName,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Fallback: Split on first hyphen (original behavior for unknown actions)
|
|
55
|
+
const parts = remainder.split('-');
|
|
56
|
+
if (parts.length >= 2) {
|
|
57
|
+
return {
|
|
58
|
+
ticketId,
|
|
59
|
+
action: parts[0],
|
|
60
|
+
agentName: parts.slice(1).join('-'),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build expected session name from execution data.
|
|
67
|
+
* Format: {ticketId}-{action}-{agentName}
|
|
68
|
+
* This is the same format used by runners.ts buildSessionName()
|
|
69
|
+
*/
|
|
70
|
+
export function buildExpectedSessionName(ticketId, agentName, action = 'work') {
|
|
71
|
+
return `${ticketId}-${action}-${agentName}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Check if a session name matches the expected pattern for a ticket and agent.
|
|
75
|
+
* Parses the session name and verifies the ticket ID and agent name match exactly.
|
|
76
|
+
*/
|
|
77
|
+
export function sessionMatchesExecution(sessionName, ticketId, agentName) {
|
|
78
|
+
const parsed = parseSessionName(sessionName);
|
|
79
|
+
if (!parsed)
|
|
80
|
+
return false;
|
|
81
|
+
return parsed.ticketId === ticketId && parsed.agentName === agentName;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get list of host tmux session names.
|
|
85
|
+
*/
|
|
86
|
+
export function getHostTmuxSessionNames() {
|
|
87
|
+
try {
|
|
88
|
+
execSync('which tmux', { stdio: 'pipe' });
|
|
89
|
+
const output = execSync('tmux list-sessions -F "#{session_name}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
90
|
+
if (!output)
|
|
91
|
+
return [];
|
|
92
|
+
return output.split('\n');
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get map of containerId -> tmux session names.
|
|
100
|
+
* Only checks containers with the devcontainer.local_folder label.
|
|
101
|
+
*/
|
|
102
|
+
export function getContainerTmuxSessionMap() {
|
|
103
|
+
const sessionMap = new Map();
|
|
104
|
+
try {
|
|
105
|
+
const containersOutput = execSync('docker ps --filter "label=devcontainer.local_folder" --format "{{.ID}}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
106
|
+
if (!containersOutput)
|
|
107
|
+
return sessionMap;
|
|
108
|
+
for (const containerId of containersOutput.split('\n')) {
|
|
109
|
+
try {
|
|
110
|
+
const tmuxOutput = execSync(`docker exec ${containerId} tmux list-sessions -F "#{session_name}" 2>/dev/null`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
111
|
+
if (tmuxOutput) {
|
|
112
|
+
sessionMap.set(containerId, tmuxOutput.split('\n'));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Container has no tmux sessions
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Docker not available
|
|
122
|
+
}
|
|
123
|
+
return sessionMap;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Flatten container sessions map into an array for easier iteration.
|
|
127
|
+
*/
|
|
128
|
+
export function flattenContainerSessions(containerTmuxSessions) {
|
|
129
|
+
const result = [];
|
|
130
|
+
containerTmuxSessions.forEach((sessions, containerId) => {
|
|
131
|
+
for (const sessionName of sessions) {
|
|
132
|
+
result.push({ sessionName, containerId });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Try to find a matching tmux session for an execution with NULL sessionId.
|
|
139
|
+
* First tries exact matches with known action names, then falls back to
|
|
140
|
+
* partial matching with agent name verification.
|
|
141
|
+
*
|
|
142
|
+
* @returns The matched session name, or null if no match found
|
|
143
|
+
*/
|
|
144
|
+
export function findSessionForExecution(ticketId, agentName, availableSessions) {
|
|
145
|
+
// First, try exact matches with known action names
|
|
146
|
+
for (const action of KNOWN_ACTIONS) {
|
|
147
|
+
const expectedName = buildExpectedSessionName(ticketId, agentName, action);
|
|
148
|
+
if (availableSessions.includes(expectedName)) {
|
|
149
|
+
return expectedName;
|
|
150
|
+
}
|
|
151
|
+
// Also try lowercase variant
|
|
152
|
+
const expectedNameLower = buildExpectedSessionName(ticketId, agentName, action.toLowerCase());
|
|
153
|
+
if (availableSessions.includes(expectedNameLower)) {
|
|
154
|
+
return expectedNameLower;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Fallback: partial match with agent name verification
|
|
158
|
+
// This catches sessions with unknown action names while preventing
|
|
159
|
+
// false matches when multiple agents work on the same ticket
|
|
160
|
+
const match = availableSessions.find(s => sessionMatchesExecution(s, ticketId, agentName));
|
|
161
|
+
return match || null;
|
|
162
|
+
}
|
|
@@ -10,6 +10,7 @@ import { execSync } from 'node:child_process';
|
|
|
10
10
|
import { autoExportToBoard } from '../pmo/index.js';
|
|
11
11
|
import { getWorkColumnSetting, findColumnByName } from '../pmo/utils.js';
|
|
12
12
|
import { findHQRoot } from '../repos/index.js';
|
|
13
|
+
import { hasGitHubRemote } from '../repos/git.js';
|
|
13
14
|
import { hasDevcontainerConfig } from './devcontainer.js';
|
|
14
15
|
import { loadExecutionConfig, getOrPromptCoderName } from './config.js';
|
|
15
16
|
import { runExecution, isDockerRunning, isGitHubTokenAvailable, isDevcontainerCliInstalled } from './runners.js';
|
|
@@ -30,6 +31,21 @@ function tryGitCommand(cmd, cwd) {
|
|
|
30
31
|
return false;
|
|
31
32
|
}
|
|
32
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if any of the given repos have a GitHub remote
|
|
36
|
+
*/
|
|
37
|
+
function checkReposForRemote(repoPaths) {
|
|
38
|
+
const reposWithoutRemote = [];
|
|
39
|
+
for (const repoPath of repoPaths) {
|
|
40
|
+
if (isGitRepo(repoPath) && !hasGitHubRemote(repoPath)) {
|
|
41
|
+
reposWithoutRemote.push(repoPath);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
hasRemote: reposWithoutRemote.length === 0,
|
|
46
|
+
reposWithoutRemote,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
33
49
|
/**
|
|
34
50
|
* Check if a directory is a git repository
|
|
35
51
|
*/
|
|
@@ -272,6 +288,32 @@ export async function spawnAgentForTicket(ticket, agentName, storage, executionS
|
|
|
272
288
|
const gitRepos = repoWorktrees.length > 0
|
|
273
289
|
? repoWorktrees.map(r => path.join(agentDir, r))
|
|
274
290
|
: [worktreePath];
|
|
291
|
+
// Check for GitHub remote if PR creation is enabled
|
|
292
|
+
if (!options.skipRemoteCheck) {
|
|
293
|
+
const remoteCheck = checkReposForRemote(gitRepos);
|
|
294
|
+
if (!remoteCheck.hasRemote) {
|
|
295
|
+
const repoNames = remoteCheck.reposWithoutRemote.map(r => path.basename(r)).join(', ');
|
|
296
|
+
if (options.createPR) {
|
|
297
|
+
// If PR creation is requested, we must have a remote
|
|
298
|
+
return {
|
|
299
|
+
success: false,
|
|
300
|
+
ticketId: ticket.id,
|
|
301
|
+
agentName,
|
|
302
|
+
error: `No GitHub remote found for: ${repoNames}\n\n` +
|
|
303
|
+
'Cannot create PRs without a GitHub remote.\n' +
|
|
304
|
+
'Options:\n' +
|
|
305
|
+
' 1. Run "prlt repo create" to create a GitHub repo and set up remote\n' +
|
|
306
|
+
' 2. Manually add a remote: git remote add origin <url>\n' +
|
|
307
|
+
' 3. Use --skip-remote-check to spawn without PR support',
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
// Just warn if not creating PRs
|
|
312
|
+
log(`⚠️ No GitHub remote found for: ${repoNames}. PRs cannot be created.`);
|
|
313
|
+
log(' Run "prlt repo create" to set up a GitHub remote.');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
275
317
|
// Always fetch latest from origin before branch operations
|
|
276
318
|
// This ensures all spawn actions work with the latest code
|
|
277
319
|
for (const repoPath of gitRepos) {
|
|
@@ -74,8 +74,8 @@ export interface ResolverChoice<T = string> {
|
|
|
74
74
|
export interface PromptDefinition<TValue = unknown, TFlags = Record<string, unknown>> {
|
|
75
75
|
/** The flag name this prompt resolves (e.g., 'column', 'title') */
|
|
76
76
|
flagName: string;
|
|
77
|
-
/** Prompt type */
|
|
78
|
-
type: 'list' | 'checkbox' | 'input' | 'confirm' | 'editor';
|
|
77
|
+
/** Prompt type. Use 'multiline' for inline multi-line text input (replaces 'editor') */
|
|
78
|
+
type: 'list' | 'checkbox' | 'input' | 'confirm' | 'editor' | 'multiline';
|
|
79
79
|
/** User-facing prompt message */
|
|
80
80
|
message: string | ((ctx: ResolverContext<TFlags>) => string);
|
|
81
81
|
/**
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
import inquirer from 'inquirer';
|
|
42
42
|
import { outputPromptAsJson, createMetadata, } from '../prompt-json.js';
|
|
43
|
+
import { multiLineInput } from '../multiline-input.js';
|
|
43
44
|
/**
|
|
44
45
|
* FlagResolver handles unified flag resolution for both human and machine modes.
|
|
45
46
|
*
|
|
@@ -228,6 +229,20 @@ export class FlagResolver {
|
|
|
228
229
|
* Prompt for value in interactive mode
|
|
229
230
|
*/
|
|
230
231
|
async promptInteractive(prompt, message, choices, defaultValue) {
|
|
232
|
+
// Handle multiline type specially - use our custom multiLineInput
|
|
233
|
+
if (prompt.type === 'multiline') {
|
|
234
|
+
const result = await multiLineInput({
|
|
235
|
+
message,
|
|
236
|
+
default: typeof defaultValue === 'string' ? defaultValue : '',
|
|
237
|
+
validate: prompt.validate
|
|
238
|
+
? (value) => prompt.validate(value, this.resolverContext)
|
|
239
|
+
: undefined,
|
|
240
|
+
});
|
|
241
|
+
if (result.cancelled) {
|
|
242
|
+
throw new Error('Input cancelled');
|
|
243
|
+
}
|
|
244
|
+
return result.value;
|
|
245
|
+
}
|
|
231
246
|
// Build inquirer prompt config as a single question object
|
|
232
247
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
233
248
|
const question = {
|
package/dist/lib/init/index.js
CHANGED
|
@@ -9,6 +9,8 @@ import { addRepositoriesToHQ, isInGitRepo } from '../repos/index.js';
|
|
|
9
9
|
import { createPMO, } from '../pmo/index.js';
|
|
10
10
|
import { createWorkspaceDatabase, addRepositoriesToDatabase, addAgentsToDatabase, createTheme, addThemeNames, setActiveTheme } from '../database/index.js';
|
|
11
11
|
import { ensureMachineConfigDir, registerHeadquarters, getOrganizations, createOrganization, } from '../machine-config.js';
|
|
12
|
+
import { hasGitHubRemote } from '../repos/git.js';
|
|
13
|
+
import { isGHInstalled, isGHAuthenticated } from '../pr/index.js';
|
|
12
14
|
/**
|
|
13
15
|
* Validate that HQ path is not inside a git repository or another HQ
|
|
14
16
|
* Returns: { valid: true } or { valid: false, reason: string }
|
|
@@ -291,6 +293,22 @@ export async function initializeHQ(options) {
|
|
|
291
293
|
};
|
|
292
294
|
});
|
|
293
295
|
addRepositoriesToDatabase(hqPath, dbRepos);
|
|
296
|
+
// Check for repos without GitHub remotes (only in interactive mode)
|
|
297
|
+
if (!quiet && addedRepos.length > 0) {
|
|
298
|
+
const reposPath = path.join(hqPath, 'repos');
|
|
299
|
+
const reposWithoutRemote = [];
|
|
300
|
+
for (const repoName of addedRepos) {
|
|
301
|
+
const repoFullPath = path.join(reposPath, repoName);
|
|
302
|
+
if (fs.existsSync(repoFullPath) && !hasGitHubRemote(repoFullPath)) {
|
|
303
|
+
reposWithoutRemote.push(repoName);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (reposWithoutRemote.length > 0 && isGHInstalled() && isGHAuthenticated()) {
|
|
307
|
+
log(chalk.yellow(`\n⚠️ The following repos have no GitHub remote: ${reposWithoutRemote.join(', ')}`));
|
|
308
|
+
log(chalk.gray(' Without a remote, agents cannot create pull requests.'));
|
|
309
|
+
log(chalk.gray(' You can create GitHub repos later with: prlt repo create'));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
294
312
|
// Create PMO if requested
|
|
295
313
|
if (pmoSetup.includePMO) {
|
|
296
314
|
await createPMO({
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-line text input utility for CLI.
|
|
3
|
+
*
|
|
4
|
+
* Provides inline TTY text input without opening external editors.
|
|
5
|
+
* Handles paste safely, supports cursor navigation, and provides
|
|
6
|
+
* clear visual feedback.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const text = await multiLineInput({
|
|
11
|
+
* message: 'Enter description:',
|
|
12
|
+
* default: 'Existing content...',
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Options for multiLineInput
|
|
18
|
+
*/
|
|
19
|
+
export interface MultiLineInputOptions {
|
|
20
|
+
/** Prompt message displayed above the input area */
|
|
21
|
+
message: string;
|
|
22
|
+
/** Default/initial content to populate the input with */
|
|
23
|
+
default?: string;
|
|
24
|
+
/** Hint text shown below the input area (e.g., key bindings) */
|
|
25
|
+
hint?: string;
|
|
26
|
+
/** Whether input is required (empty not allowed) */
|
|
27
|
+
required?: boolean;
|
|
28
|
+
/** Validation function - returns true if valid, or error message */
|
|
29
|
+
validate?: (value: string) => boolean | string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Result of multiLineInput
|
|
33
|
+
*/
|
|
34
|
+
export interface MultiLineInputResult {
|
|
35
|
+
/** The entered text */
|
|
36
|
+
value: string;
|
|
37
|
+
/** Whether input was cancelled (Ctrl+C) */
|
|
38
|
+
cancelled: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Collect multi-line input from the user with an inline TTY editor.
|
|
42
|
+
*
|
|
43
|
+
* Features:
|
|
44
|
+
* - Arrow key navigation
|
|
45
|
+
* - Backspace/delete
|
|
46
|
+
* - Copy-paste handling (escapes special characters)
|
|
47
|
+
* - Ctrl+D to finish, Ctrl+C to cancel
|
|
48
|
+
* - Pre-populated content support
|
|
49
|
+
* - Real-time visual feedback
|
|
50
|
+
*
|
|
51
|
+
* @param options Input options
|
|
52
|
+
* @returns The entered text and cancellation status
|
|
53
|
+
*/
|
|
54
|
+
export declare function multiLineInput(options: MultiLineInputOptions): Promise<MultiLineInputResult>;
|
|
55
|
+
/**
|
|
56
|
+
* Convenience wrapper that returns just the value string.
|
|
57
|
+
* Throws if cancelled.
|
|
58
|
+
*/
|
|
59
|
+
export declare function promptMultiLine(options: MultiLineInputOptions): Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Integration with FlagResolver - creates a prompt-compatible function
|
|
62
|
+
*/
|
|
63
|
+
export declare function createMultiLinePrompt(message: string, defaultValue?: string, hint?: string): () => Promise<string>;
|