@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.
Files changed (75) hide show
  1. package/dist/commands/action/create.js +3 -3
  2. package/dist/commands/action/update.js +3 -3
  3. package/dist/commands/epic/activate.js +9 -17
  4. package/dist/commands/epic/archive.js +13 -24
  5. package/dist/commands/epic/create.js +7 -6
  6. package/dist/commands/epic/move.js +28 -47
  7. package/dist/commands/epic/progress.js +10 -14
  8. package/dist/commands/epic/project.js +42 -59
  9. package/dist/commands/epic/reorder.js +25 -30
  10. package/dist/commands/epic/spec.d.ts +1 -0
  11. package/dist/commands/epic/spec.js +39 -40
  12. package/dist/commands/epic/ticket.d.ts +2 -0
  13. package/dist/commands/epic/ticket.js +63 -37
  14. package/dist/commands/feedback/index.d.ts +10 -0
  15. package/dist/commands/feedback/index.js +60 -0
  16. package/dist/commands/feedback/list.d.ts +12 -0
  17. package/dist/commands/feedback/list.js +126 -0
  18. package/dist/commands/feedback/submit.d.ts +16 -0
  19. package/dist/commands/feedback/submit.js +220 -0
  20. package/dist/commands/feedback/view.d.ts +15 -0
  21. package/dist/commands/feedback/view.js +109 -0
  22. package/dist/commands/gh/index.js +4 -0
  23. package/dist/commands/repo/create.d.ts +38 -0
  24. package/dist/commands/repo/create.js +283 -0
  25. package/dist/commands/repo/index.js +7 -0
  26. package/dist/commands/roadmap/add-project.js +9 -22
  27. package/dist/commands/roadmap/create.d.ts +0 -1
  28. package/dist/commands/roadmap/create.js +46 -40
  29. package/dist/commands/roadmap/delete.js +10 -24
  30. package/dist/commands/roadmap/generate.d.ts +1 -0
  31. package/dist/commands/roadmap/generate.js +21 -22
  32. package/dist/commands/roadmap/remove-project.js +14 -34
  33. package/dist/commands/roadmap/reorder.js +19 -26
  34. package/dist/commands/roadmap/update.js +27 -26
  35. package/dist/commands/roadmap/view.js +5 -12
  36. package/dist/commands/session/attach.d.ts +1 -8
  37. package/dist/commands/session/attach.js +93 -59
  38. package/dist/commands/session/list.d.ts +0 -8
  39. package/dist/commands/session/list.js +130 -81
  40. package/dist/commands/spec/create.js +1 -1
  41. package/dist/commands/spec/edit.js +63 -33
  42. package/dist/commands/support/book.d.ts +10 -0
  43. package/dist/commands/support/book.js +54 -0
  44. package/dist/commands/support/discord.d.ts +10 -0
  45. package/dist/commands/support/discord.js +54 -0
  46. package/dist/commands/support/docs.d.ts +10 -0
  47. package/dist/commands/support/docs.js +54 -0
  48. package/dist/commands/support/index.d.ts +19 -0
  49. package/dist/commands/support/index.js +81 -0
  50. package/dist/commands/support/issues.d.ts +11 -0
  51. package/dist/commands/support/issues.js +77 -0
  52. package/dist/commands/support/logs.d.ts +18 -0
  53. package/dist/commands/support/logs.js +247 -0
  54. package/dist/commands/ticket/create.js +21 -13
  55. package/dist/commands/ticket/edit.js +44 -13
  56. package/dist/commands/ticket/move.d.ts +7 -0
  57. package/dist/commands/ticket/move.js +132 -0
  58. package/dist/commands/work/spawn.d.ts +1 -0
  59. package/dist/commands/work/spawn.js +71 -7
  60. package/dist/commands/work/start.js +6 -0
  61. package/dist/lib/execution/runners.js +21 -17
  62. package/dist/lib/execution/session-utils.d.ts +60 -0
  63. package/dist/lib/execution/session-utils.js +162 -0
  64. package/dist/lib/execution/spawner.d.ts +2 -0
  65. package/dist/lib/execution/spawner.js +42 -0
  66. package/dist/lib/flags/resolver.d.ts +2 -2
  67. package/dist/lib/flags/resolver.js +15 -0
  68. package/dist/lib/init/index.js +18 -0
  69. package/dist/lib/multiline-input.d.ts +63 -0
  70. package/dist/lib/multiline-input.js +360 -0
  71. package/dist/lib/prompt-json.d.ts +5 -5
  72. package/dist/lib/repos/git.d.ts +7 -0
  73. package/dist/lib/repos/git.js +20 -0
  74. package/oclif.manifest.json +2206 -1607
  75. 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 option at the end
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
- batchAction = selectedAction === '__adhoc__' ? 'adhoc' : selectedAction;
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 === 'adhoc') {
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 flag (from prompt or flag)
924
- startArgs.push('--action', batchAction || 'implement');
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 with bash explicitly (not default shell which may be zsh)
1483
- // Using bash avoids zsh-newuser-install prompt that blocks the session
1484
- const createSessionCmd = `tmux new-session -d -s "${sessionName}" -n "${sessionName}" bash${mouseOption} \\; set-option -g set-titles on \\; set-option -g set-titles-string "#{window_name}"`;
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: Send keys to run the script (this runs in the interactive bash)
1495
- const sendKeysCmd = `tmux send-keys -t "${sessionName}" "source ${scriptPath}" Enter`;
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
- // Foreground mode: attach to container's tmux session in current terminal (blocking)
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
+ }
@@ -31,6 +31,8 @@ export interface SpawnOptions {
31
31
  executionConfig?: ExecutionConfig;
32
32
  /** Logging callback */
33
33
  log?: (msg: string) => void;
34
+ /** Skip GitHub remote check */
35
+ skipRemoteCheck?: boolean;
34
36
  }
35
37
  export interface SpawnResult {
36
38
  success: boolean;
@@ -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 = {
@@ -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>;