@proletariat/cli 0.3.24 → 0.3.26

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 (134) hide show
  1. package/dist/commands/action/create.js +3 -3
  2. package/dist/commands/action/index.js +2 -2
  3. package/dist/commands/action/update.js +3 -3
  4. package/dist/commands/agent/auth.js +1 -1
  5. package/dist/commands/agent/cleanup.js +6 -6
  6. package/dist/commands/agent/discover.js +1 -1
  7. package/dist/commands/agent/remove.js +4 -4
  8. package/dist/commands/autocomplete/setup.d.ts +2 -2
  9. package/dist/commands/autocomplete/setup.js +5 -5
  10. package/dist/commands/branch/create.js +31 -30
  11. package/dist/commands/category/create.js +4 -5
  12. package/dist/commands/category/delete.js +2 -3
  13. package/dist/commands/category/rename.js +2 -3
  14. package/dist/commands/claude.d.ts +2 -8
  15. package/dist/commands/claude.js +26 -26
  16. package/dist/commands/commit.d.ts +2 -8
  17. package/dist/commands/commit.js +4 -26
  18. package/dist/commands/config/index.d.ts +2 -10
  19. package/dist/commands/config/index.js +8 -34
  20. package/dist/commands/docker/index.d.ts +2 -2
  21. package/dist/commands/docker/index.js +8 -8
  22. package/dist/commands/epic/activate.js +9 -17
  23. package/dist/commands/epic/archive.js +13 -24
  24. package/dist/commands/epic/create.js +7 -6
  25. package/dist/commands/epic/delete.js +4 -5
  26. package/dist/commands/epic/move.js +28 -47
  27. package/dist/commands/epic/progress.js +10 -14
  28. package/dist/commands/epic/project.js +42 -59
  29. package/dist/commands/epic/reorder.js +25 -30
  30. package/dist/commands/epic/spec.d.ts +1 -0
  31. package/dist/commands/epic/spec.js +39 -40
  32. package/dist/commands/epic/ticket.d.ts +2 -0
  33. package/dist/commands/epic/ticket.js +63 -37
  34. package/dist/commands/feedback/index.d.ts +10 -0
  35. package/dist/commands/feedback/index.js +60 -0
  36. package/dist/commands/feedback/list.d.ts +12 -0
  37. package/dist/commands/feedback/list.js +126 -0
  38. package/dist/commands/feedback/submit.d.ts +16 -0
  39. package/dist/commands/feedback/submit.js +220 -0
  40. package/dist/commands/feedback/view.d.ts +15 -0
  41. package/dist/commands/feedback/view.js +109 -0
  42. package/dist/commands/gh/index.js +4 -0
  43. package/dist/commands/link/index.js +2 -2
  44. package/dist/commands/pmo/init.d.ts +2 -2
  45. package/dist/commands/pmo/init.js +7 -7
  46. package/dist/commands/project/spec.js +6 -6
  47. package/dist/commands/repo/create.d.ts +38 -0
  48. package/dist/commands/repo/create.js +283 -0
  49. package/dist/commands/repo/index.js +7 -0
  50. package/dist/commands/roadmap/add-project.js +9 -22
  51. package/dist/commands/roadmap/create.d.ts +0 -1
  52. package/dist/commands/roadmap/create.js +46 -40
  53. package/dist/commands/roadmap/delete.js +10 -24
  54. package/dist/commands/roadmap/generate.d.ts +1 -0
  55. package/dist/commands/roadmap/generate.js +21 -22
  56. package/dist/commands/roadmap/remove-project.js +14 -34
  57. package/dist/commands/roadmap/reorder.js +19 -26
  58. package/dist/commands/roadmap/update.js +27 -26
  59. package/dist/commands/roadmap/view.js +5 -12
  60. package/dist/commands/session/attach.d.ts +1 -8
  61. package/dist/commands/session/attach.js +93 -59
  62. package/dist/commands/session/health.d.ts +29 -0
  63. package/dist/commands/session/health.js +495 -0
  64. package/dist/commands/session/index.js +4 -0
  65. package/dist/commands/session/list.d.ts +0 -8
  66. package/dist/commands/session/list.js +130 -81
  67. package/dist/commands/spec/create.js +1 -1
  68. package/dist/commands/spec/edit.js +64 -35
  69. package/dist/commands/staff/add.d.ts +2 -2
  70. package/dist/commands/staff/add.js +15 -14
  71. package/dist/commands/staff/index.js +2 -2
  72. package/dist/commands/staff/remove.js +4 -4
  73. package/dist/commands/status/index.js +6 -7
  74. package/dist/commands/support/book.d.ts +10 -0
  75. package/dist/commands/support/book.js +54 -0
  76. package/dist/commands/support/discord.d.ts +10 -0
  77. package/dist/commands/support/discord.js +54 -0
  78. package/dist/commands/support/docs.d.ts +10 -0
  79. package/dist/commands/support/docs.js +54 -0
  80. package/dist/commands/support/index.d.ts +19 -0
  81. package/dist/commands/support/index.js +81 -0
  82. package/dist/commands/support/issues.d.ts +11 -0
  83. package/dist/commands/support/issues.js +77 -0
  84. package/dist/commands/support/logs.d.ts +18 -0
  85. package/dist/commands/support/logs.js +247 -0
  86. package/dist/commands/template/apply.js +10 -11
  87. package/dist/commands/template/create.js +18 -17
  88. package/dist/commands/template/index.d.ts +2 -2
  89. package/dist/commands/template/index.js +6 -6
  90. package/dist/commands/template/save.js +8 -7
  91. package/dist/commands/template/update.js +6 -7
  92. package/dist/commands/terminal/title.d.ts +2 -26
  93. package/dist/commands/terminal/title.js +4 -33
  94. package/dist/commands/theme/index.d.ts +2 -2
  95. package/dist/commands/theme/index.js +19 -18
  96. package/dist/commands/theme/set.d.ts +2 -2
  97. package/dist/commands/theme/set.js +5 -5
  98. package/dist/commands/ticket/create.js +52 -26
  99. package/dist/commands/ticket/delete.js +15 -13
  100. package/dist/commands/ticket/edit.js +59 -20
  101. package/dist/commands/ticket/epic.js +12 -10
  102. package/dist/commands/ticket/move.d.ts +7 -0
  103. package/dist/commands/ticket/move.js +132 -0
  104. package/dist/commands/ticket/project.js +11 -9
  105. package/dist/commands/ticket/reassign.js +23 -19
  106. package/dist/commands/ticket/spec.js +7 -5
  107. package/dist/commands/ticket/update.js +55 -53
  108. package/dist/commands/whoami.js +1 -0
  109. package/dist/commands/work/ready.js +7 -7
  110. package/dist/commands/work/revise.js +13 -11
  111. package/dist/commands/work/spawn.d.ts +1 -0
  112. package/dist/commands/work/spawn.js +225 -64
  113. package/dist/commands/work/start.d.ts +1 -0
  114. package/dist/commands/work/start.js +301 -173
  115. package/dist/hooks/init.js +4 -0
  116. package/dist/lib/execution/runners.js +21 -17
  117. package/dist/lib/execution/session-utils.d.ts +60 -0
  118. package/dist/lib/execution/session-utils.js +162 -0
  119. package/dist/lib/execution/spawner.d.ts +2 -0
  120. package/dist/lib/execution/spawner.js +42 -0
  121. package/dist/lib/flags/resolver.d.ts +2 -2
  122. package/dist/lib/flags/resolver.js +15 -0
  123. package/dist/lib/init/index.js +18 -0
  124. package/dist/lib/multiline-input.d.ts +63 -0
  125. package/dist/lib/multiline-input.js +360 -0
  126. package/dist/lib/pr/index.d.ts +4 -0
  127. package/dist/lib/pr/index.js +32 -14
  128. package/dist/lib/prompt-command.d.ts +3 -0
  129. package/dist/lib/prompt-json.d.ts +77 -6
  130. package/dist/lib/prompt-json.js +46 -0
  131. package/dist/lib/repos/git.d.ts +7 -0
  132. package/dist/lib/repos/git.js +20 -0
  133. package/oclif.manifest.json +2913 -2246
  134. package/package.json +1 -1
@@ -13,6 +13,10 @@ const hook = async function ({ id, config }) {
13
13
  if (id === 'init') {
14
14
  return;
15
15
  }
16
+ // Skip when --help flag is present - help should always be available
17
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
18
+ return;
19
+ }
16
20
  // Skip for help-related commands/flags
17
21
  // When user runs just `prlt` with no args, id is undefined
18
22
  if (!id || id === 'help') {
@@ -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>;