@proletariat/cli 0.3.50 → 0.3.52

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 (87) hide show
  1. package/dist/commands/agent/status.js +1 -0
  2. package/dist/commands/asana/connect.d.ts +15 -0
  3. package/dist/commands/asana/connect.js +267 -0
  4. package/dist/commands/asana/sync.d.ts +15 -0
  5. package/dist/commands/asana/sync.js +189 -0
  6. package/dist/commands/config/index.js +7 -1
  7. package/dist/commands/execution/list.js +3 -0
  8. package/dist/commands/execution/view.js +10 -0
  9. package/dist/commands/monday/connect.d.ts +16 -0
  10. package/dist/commands/monday/connect.js +212 -0
  11. package/dist/commands/monday/sync.d.ts +14 -0
  12. package/dist/commands/monday/sync.js +178 -0
  13. package/dist/commands/orchestrator/start.d.ts +6 -0
  14. package/dist/commands/orchestrator/start.js +149 -11
  15. package/dist/commands/session/list.js +6 -5
  16. package/dist/commands/work/index.js +7 -0
  17. package/dist/commands/work/jira.d.ts +28 -0
  18. package/dist/commands/work/jira.js +225 -0
  19. package/dist/commands/work/source/set.d.ts +12 -0
  20. package/dist/commands/work/source/set.js +52 -0
  21. package/dist/commands/work/source.d.ts +11 -0
  22. package/dist/commands/work/source.js +53 -0
  23. package/dist/commands/work/spawn.d.ts +1 -0
  24. package/dist/commands/work/spawn.js +73 -8
  25. package/dist/commands/work/start.d.ts +8 -0
  26. package/dist/commands/work/start.js +241 -3
  27. package/dist/lib/asana/client.d.ts +15 -0
  28. package/dist/lib/asana/client.js +120 -0
  29. package/dist/lib/asana/config.d.ts +9 -0
  30. package/dist/lib/asana/config.js +61 -0
  31. package/dist/lib/asana/index.d.ts +5 -0
  32. package/dist/lib/asana/index.js +4 -0
  33. package/dist/lib/asana/mapper.d.ts +13 -0
  34. package/dist/lib/asana/mapper.js +70 -0
  35. package/dist/lib/asana/sync.d.ts +13 -0
  36. package/dist/lib/asana/sync.js +36 -0
  37. package/dist/lib/asana/types.d.ts +40 -0
  38. package/dist/lib/asana/types.js +1 -0
  39. package/dist/lib/database/drizzle-schema.d.ts +393 -0
  40. package/dist/lib/database/drizzle-schema.js +45 -0
  41. package/dist/lib/execution/config.d.ts +10 -0
  42. package/dist/lib/execution/config.js +19 -0
  43. package/dist/lib/execution/runners.d.ts +10 -0
  44. package/dist/lib/execution/runners.js +110 -1
  45. package/dist/lib/execution/spawner.js +26 -0
  46. package/dist/lib/execution/storage.d.ts +4 -0
  47. package/dist/lib/execution/storage.js +8 -3
  48. package/dist/lib/execution/types.d.ts +4 -0
  49. package/dist/lib/external-issues/adapters.d.ts +18 -1
  50. package/dist/lib/external-issues/adapters.js +49 -1
  51. package/dist/lib/external-issues/index.d.ts +4 -1
  52. package/dist/lib/external-issues/index.js +5 -0
  53. package/dist/lib/external-issues/jira.d.ts +23 -0
  54. package/dist/lib/external-issues/jira.js +223 -0
  55. package/dist/lib/external-issues/linear.js +4 -3
  56. package/dist/lib/external-issues/mapper.d.ts +3 -2
  57. package/dist/lib/external-issues/mapper.js +5 -2
  58. package/dist/lib/external-issues/mapping-store.d.ts +12 -0
  59. package/dist/lib/external-issues/mapping-store.js +164 -0
  60. package/dist/lib/external-issues/types.d.ts +34 -0
  61. package/dist/lib/external-issues/validation.js +11 -0
  62. package/dist/lib/external-issues/work-start.d.ts +10 -0
  63. package/dist/lib/external-issues/work-start.js +12 -0
  64. package/dist/lib/linear/mapper.d.ts +2 -0
  65. package/dist/lib/linear/mapper.js +66 -2
  66. package/dist/lib/monday/client.d.ts +14 -0
  67. package/dist/lib/monday/client.js +113 -0
  68. package/dist/lib/monday/config.d.ts +10 -0
  69. package/dist/lib/monday/config.js +64 -0
  70. package/dist/lib/monday/index.d.ts +5 -0
  71. package/dist/lib/monday/index.js +4 -0
  72. package/dist/lib/monday/mapper.d.ts +14 -0
  73. package/dist/lib/monday/mapper.js +89 -0
  74. package/dist/lib/monday/sync.d.ts +13 -0
  75. package/dist/lib/monday/sync.js +45 -0
  76. package/dist/lib/monday/types.d.ts +38 -0
  77. package/dist/lib/monday/types.js +4 -0
  78. package/dist/lib/pmo/schema.d.ts +10 -1
  79. package/dist/lib/pmo/schema.js +73 -0
  80. package/dist/lib/pmo/storage/base.js +32 -0
  81. package/dist/lib/prompt-json.d.ts +11 -0
  82. package/dist/lib/work-source/config.d.ts +14 -0
  83. package/dist/lib/work-source/config.js +70 -0
  84. package/dist/lib/work-source/index.d.ts +1 -0
  85. package/dist/lib/work-source/index.js +1 -0
  86. package/oclif.manifest.json +2531 -1964
  87. package/package.json +1 -1
@@ -0,0 +1,212 @@
1
+ import { Flags } from '@oclif/core';
2
+ import inquirer from 'inquirer';
3
+ import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
4
+ import { colors } from '../../lib/colors.js';
5
+ import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
6
+ import { MondayClient, isMondayConfigured, loadMondayConfig, saveMondayApiToken, saveMondayBoard, saveMondayAccountName, clearMondayConfig, getMondayApiToken, getMondayBoardId, } from '../../lib/monday/index.js';
7
+ export default class MondayConnect extends PMOCommand {
8
+ static description = 'Connect PRLT to Monday.com and store workspace credentials';
9
+ static examples = [
10
+ '<%= config.bin %> <%= command.id %>',
11
+ '<%= config.bin %> <%= command.id %> --board 1234567890',
12
+ '<%= config.bin %> <%= command.id %> --check',
13
+ '<%= config.bin %> <%= command.id %> --disconnect',
14
+ ];
15
+ static flags = {
16
+ ...pmoBaseFlags,
17
+ board: Flags.string({
18
+ description: 'Monday board ID to sync tickets to',
19
+ }),
20
+ check: Flags.boolean({
21
+ description: 'Only check if Monday configuration is valid',
22
+ default: false,
23
+ }),
24
+ force: Flags.boolean({
25
+ description: 'Force re-authentication even if already configured',
26
+ default: false,
27
+ }),
28
+ disconnect: Flags.boolean({
29
+ description: 'Remove stored Monday credentials',
30
+ default: false,
31
+ }),
32
+ };
33
+ async execute() {
34
+ const { flags } = await this.parse(MondayConnect);
35
+ const jsonMode = shouldOutputJson(flags);
36
+ const db = this.storage.getDatabase();
37
+ if (flags.disconnect) {
38
+ clearMondayConfig(db);
39
+ if (jsonMode) {
40
+ outputSuccessAsJson({
41
+ disconnected: true,
42
+ message: 'Monday configuration removed.',
43
+ }, createMetadata('monday connect', flags));
44
+ return;
45
+ }
46
+ this.log(colors.success('Monday configuration removed.'));
47
+ return;
48
+ }
49
+ if (flags.check) {
50
+ await this.checkConnection(flags, jsonMode);
51
+ return;
52
+ }
53
+ const existingConfig = loadMondayConfig(db);
54
+ if (existingConfig && !flags.force) {
55
+ try {
56
+ const client = new MondayClient(existingConfig.apiToken);
57
+ const info = await client.verify();
58
+ if (jsonMode) {
59
+ outputSuccessAsJson({
60
+ authenticated: true,
61
+ account: info.accountName,
62
+ user: info.userName,
63
+ boardId: existingConfig.boardId ?? null,
64
+ boardName: existingConfig.boardName ?? null,
65
+ message: 'Already authenticated. Use --force to re-authenticate.',
66
+ }, createMetadata('monday connect', flags));
67
+ return;
68
+ }
69
+ this.log(colors.success('Already connected to Monday.com'));
70
+ this.log(colors.textMuted(` Account: ${info.accountName}`));
71
+ this.log(colors.textMuted(` User: ${info.userName}`));
72
+ if (existingConfig.boardId) {
73
+ this.log(colors.textMuted(` Board: ${existingConfig.boardName ?? 'Unknown'} (${existingConfig.boardId})`));
74
+ }
75
+ this.log('');
76
+ this.log(colors.textMuted('Use --force to re-authenticate.'));
77
+ return;
78
+ }
79
+ catch {
80
+ // Stored token is invalid, proceed with re-authentication.
81
+ }
82
+ }
83
+ let apiToken = getMondayApiToken(db);
84
+ if (!apiToken) {
85
+ if (jsonMode) {
86
+ outputErrorAsJson('API_TOKEN_REQUIRED', 'Monday API token required. Set MONDAY_API_TOKEN or PRLT_MONDAY_API_TOKEN, or run interactively.', createMetadata('monday connect', flags));
87
+ this.exit(1);
88
+ }
89
+ const answers = await inquirer.prompt([{
90
+ type: 'password',
91
+ name: 'token',
92
+ message: 'Enter your Monday API token:',
93
+ mask: '*',
94
+ validate: (value) => value.trim().length > 0 || 'API token is required',
95
+ }]);
96
+ apiToken = answers.token;
97
+ }
98
+ if (!apiToken) {
99
+ this.error('Monday API token is required.');
100
+ }
101
+ const client = new MondayClient(apiToken);
102
+ let boardId = flags.board ?? getMondayBoardId(db);
103
+ if (!boardId && !jsonMode) {
104
+ const answers = await inquirer.prompt([{
105
+ type: 'input',
106
+ name: 'boardId',
107
+ message: 'Enter Monday board ID for ticket sync:',
108
+ validate: (value) => value.trim().length > 0 || 'Board ID is required',
109
+ }]);
110
+ boardId = answers.boardId;
111
+ }
112
+ try {
113
+ const info = await client.verify();
114
+ saveMondayApiToken(db, apiToken);
115
+ saveMondayAccountName(db, info.accountName);
116
+ let boardName = null;
117
+ if (boardId) {
118
+ const board = await client.getBoard(boardId);
119
+ if (!board) {
120
+ if (jsonMode) {
121
+ outputErrorAsJson('BOARD_NOT_FOUND', `Monday board ${boardId} was not found or is inaccessible.`, createMetadata('monday connect', flags));
122
+ this.exit(1);
123
+ }
124
+ this.error(`Monday board ${boardId} was not found or is inaccessible.`);
125
+ }
126
+ boardName = board.name;
127
+ saveMondayBoard(db, board.id, board.name);
128
+ boardId = board.id;
129
+ }
130
+ if (jsonMode) {
131
+ outputSuccessAsJson({
132
+ authenticated: true,
133
+ account: info.accountName,
134
+ user: info.userName,
135
+ email: info.email ?? null,
136
+ boardId: boardId ?? null,
137
+ boardName,
138
+ }, createMetadata('monday connect', flags));
139
+ return;
140
+ }
141
+ this.log(colors.success(`Connected to Monday account: ${info.accountName}`));
142
+ this.log(colors.textMuted(` Signed in as ${info.userName}${info.email ? ` (${info.email})` : ''}`));
143
+ if (boardId) {
144
+ this.log(colors.textMuted(` Default board: ${boardName ?? 'Unknown'} (${boardId})`));
145
+ }
146
+ else {
147
+ this.log(colors.warning('No board selected yet.'));
148
+ this.log(colors.textMuted(' Run "prlt monday connect --board <board-id>" to set one.'));
149
+ }
150
+ }
151
+ catch (error) {
152
+ const message = error instanceof Error ? error.message : String(error);
153
+ if (jsonMode) {
154
+ outputErrorAsJson('MONDAY_AUTH_FAILED', `Authentication failed: ${message}`, createMetadata('monday connect', flags));
155
+ this.exit(1);
156
+ }
157
+ this.error(`Authentication failed: ${message}`);
158
+ }
159
+ }
160
+ async checkConnection(flags, jsonMode) {
161
+ const db = this.storage.getDatabase();
162
+ if (!isMondayConfigured(db)) {
163
+ if (jsonMode) {
164
+ outputSuccessAsJson({
165
+ configured: false,
166
+ message: 'Monday is not configured. Run "prlt monday connect" first.',
167
+ }, createMetadata('monday connect', flags));
168
+ return;
169
+ }
170
+ this.log(colors.warning('Monday is not configured.'));
171
+ this.log(colors.textMuted('Run "prlt monday connect" first.'));
172
+ this.exit(1);
173
+ }
174
+ const config = loadMondayConfig(db);
175
+ try {
176
+ const client = new MondayClient(config.apiToken);
177
+ const info = await client.verify();
178
+ let board = null;
179
+ if (config.boardId) {
180
+ const fetched = await client.getBoard(config.boardId);
181
+ board = fetched ? { id: fetched.id, name: fetched.name } : null;
182
+ }
183
+ if (jsonMode) {
184
+ outputSuccessAsJson({
185
+ configured: true,
186
+ connected: true,
187
+ account: info.accountName,
188
+ user: info.userName,
189
+ boardId: config.boardId ?? null,
190
+ boardName: board?.name ?? config.boardName ?? null,
191
+ }, createMetadata('monday connect', flags));
192
+ return;
193
+ }
194
+ this.log(colors.success('Monday connection is active'));
195
+ this.log(colors.textMuted(` Account: ${info.accountName}`));
196
+ this.log(colors.textMuted(` User: ${info.userName}`));
197
+ if (config.boardId) {
198
+ this.log(colors.textMuted(` Board: ${board?.name ?? config.boardName ?? 'Unknown'} (${config.boardId})`));
199
+ }
200
+ }
201
+ catch (error) {
202
+ const message = error instanceof Error ? error.message : String(error);
203
+ if (jsonMode) {
204
+ outputErrorAsJson('MONDAY_AUTH_INVALID', `Stored Monday token is invalid or expired: ${message}`, createMetadata('monday connect', flags));
205
+ this.exit(1);
206
+ }
207
+ this.log(colors.error('Stored Monday token is invalid or expired.'));
208
+ this.log(colors.textMuted('Run "prlt monday connect --force" to re-authenticate.'));
209
+ this.exit(1);
210
+ }
211
+ }
212
+ }
@@ -0,0 +1,14 @@
1
+ import { PMOCommand } from '../../lib/pmo/index.js';
2
+ export default class MondaySyncCommand extends PMOCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ ticket: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ execute(): Promise<void>;
13
+ private syncSingleTicket;
14
+ }
@@ -0,0 +1,178 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
3
+ import { colors } from '../../lib/colors.js';
4
+ import { shouldOutputJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
5
+ import { MondayClient, MondayMapper, MondaySync, isMondayConfigured, loadMondayConfig, } from '../../lib/monday/index.js';
6
+ export default class MondaySyncCommand extends PMOCommand {
7
+ static description = 'Sync PMO tickets to Monday.com board items';
8
+ static examples = [
9
+ '<%= config.bin %> <%= command.id %> # Sync project tickets to Monday board',
10
+ '<%= config.bin %> <%= command.id %> --ticket TKT-001 # Sync one ticket',
11
+ '<%= config.bin %> <%= command.id %> --dry-run # Preview sync operations',
12
+ ];
13
+ static flags = {
14
+ ...pmoBaseFlags,
15
+ ticket: Flags.string({
16
+ description: 'PMO ticket ID to sync',
17
+ }),
18
+ 'dry-run': Flags.boolean({
19
+ description: 'Preview what would be synced without writing to Monday',
20
+ default: false,
21
+ }),
22
+ };
23
+ async execute() {
24
+ const { flags } = await this.parse(MondaySyncCommand);
25
+ const jsonMode = shouldOutputJson(flags);
26
+ const db = this.storage.getDatabase();
27
+ if (!isMondayConfigured(db)) {
28
+ if (jsonMode) {
29
+ outputErrorAsJson('MONDAY_NOT_CONFIGURED', 'Monday is not configured. Run "prlt monday connect" first.', createMetadata('monday sync', flags));
30
+ this.exit(1);
31
+ }
32
+ this.error('Monday is not configured. Run "prlt monday connect" first.');
33
+ }
34
+ const config = loadMondayConfig(db);
35
+ if (!config.boardId) {
36
+ if (jsonMode) {
37
+ outputErrorAsJson('MONDAY_BOARD_REQUIRED', 'No Monday board configured. Run "prlt monday connect --board <id>" first.', createMetadata('monday sync', flags));
38
+ this.exit(1);
39
+ }
40
+ this.error('No Monday board configured. Run "prlt monday connect --board <id>" first.');
41
+ }
42
+ const client = new MondayClient(config.apiToken);
43
+ const mapper = new MondayMapper(db);
44
+ const sync = new MondaySync(client, mapper);
45
+ if (flags.ticket) {
46
+ await this.syncSingleTicket(flags.ticket, config.boardId, sync, jsonMode, flags);
47
+ return;
48
+ }
49
+ const projectId = await this.requireProject({
50
+ jsonMode: jsonMode ? {
51
+ flags,
52
+ commandName: 'monday sync',
53
+ baseCommand: `${this.config.bin} monday sync`,
54
+ } : undefined,
55
+ });
56
+ const tickets = await this.storage.listTickets(projectId);
57
+ if (tickets.length === 0) {
58
+ if (jsonMode) {
59
+ outputSuccessAsJson({
60
+ synced: 0,
61
+ created: 0,
62
+ skipped: 0,
63
+ message: `No tickets found for project ${projectId}.`,
64
+ }, createMetadata('monday sync', flags));
65
+ return;
66
+ }
67
+ this.log(colors.warning(`No tickets found for project ${projectId}.`));
68
+ return;
69
+ }
70
+ if (flags['dry-run']) {
71
+ if (jsonMode) {
72
+ outputSuccessAsJson({
73
+ dryRun: true,
74
+ action: 'sync-all',
75
+ projectId,
76
+ boardId: config.boardId,
77
+ tickets: tickets.map((ticket) => ({
78
+ ticketId: ticket.id,
79
+ itemName: sync.ticketToItemName(ticket),
80
+ mapped: mapper.getByTicketId(ticket.id) !== null,
81
+ })),
82
+ }, createMetadata('monday sync', flags));
83
+ return;
84
+ }
85
+ this.log(colors.textMuted(`Would sync ${tickets.length} ticket(s) from ${projectId} to Monday board ${config.boardId}.`));
86
+ return;
87
+ }
88
+ let syncedCount = 0;
89
+ let createdCount = 0;
90
+ const errors = [];
91
+ for (const ticket of tickets) {
92
+ try {
93
+ // eslint-disable-next-line no-await-in-loop
94
+ const result = await sync.syncTicket(ticket, config.boardId);
95
+ syncedCount++;
96
+ if (result.created)
97
+ createdCount++;
98
+ }
99
+ catch (error) {
100
+ errors.push({
101
+ ticketId: ticket.id,
102
+ error: error instanceof Error ? error.message : String(error),
103
+ });
104
+ }
105
+ }
106
+ if (jsonMode) {
107
+ outputSuccessAsJson({
108
+ action: 'sync-all',
109
+ projectId,
110
+ boardId: config.boardId,
111
+ synced: syncedCount,
112
+ created: createdCount,
113
+ skipped: tickets.length - syncedCount,
114
+ errors,
115
+ }, createMetadata('monday sync', flags));
116
+ return;
117
+ }
118
+ this.log(colors.success(`Synced ${syncedCount}/${tickets.length} ticket(s) to Monday`));
119
+ this.log(colors.textMuted(` Created new items: ${createdCount}`));
120
+ if (errors.length > 0) {
121
+ this.log(colors.warning(` Errors: ${errors.length}`));
122
+ for (const error of errors.slice(0, 5)) {
123
+ this.log(colors.textMuted(` ${error.ticketId}: ${error.error}`));
124
+ }
125
+ }
126
+ }
127
+ async syncSingleTicket(ticketId, boardId, sync, jsonMode, flags) {
128
+ const ticket = await this.storage.getTicket(ticketId);
129
+ if (!ticket) {
130
+ if (jsonMode) {
131
+ outputErrorAsJson('TICKET_NOT_FOUND', `Ticket ${ticketId} not found.`, createMetadata('monday sync', flags));
132
+ this.exit(1);
133
+ }
134
+ this.error(`Ticket ${ticketId} not found.`);
135
+ }
136
+ if (flags['dry-run']) {
137
+ if (jsonMode) {
138
+ outputSuccessAsJson({
139
+ dryRun: true,
140
+ action: 'sync-ticket',
141
+ ticketId,
142
+ boardId,
143
+ itemName: sync.ticketToItemName(ticket),
144
+ }, createMetadata('monday sync', flags));
145
+ return;
146
+ }
147
+ this.log(colors.textMuted(`Would sync ${ticketId} to Monday board ${boardId}.`));
148
+ return;
149
+ }
150
+ try {
151
+ const result = await sync.syncTicket(ticket, boardId);
152
+ if (jsonMode) {
153
+ outputSuccessAsJson({
154
+ action: 'sync-ticket',
155
+ ticketId,
156
+ boardId,
157
+ itemId: result.itemId,
158
+ created: result.created,
159
+ }, createMetadata('monday sync', flags));
160
+ return;
161
+ }
162
+ if (result.created) {
163
+ this.log(colors.success(`Created Monday item for ${ticketId}`));
164
+ }
165
+ else {
166
+ this.log(colors.success(`Updated Monday item for ${ticketId}`));
167
+ }
168
+ }
169
+ catch (error) {
170
+ const message = error instanceof Error ? error.message : String(error);
171
+ if (jsonMode) {
172
+ outputErrorAsJson('MONDAY_SYNC_FAILED', `Failed to sync ${ticketId}: ${message}`, createMetadata('monday sync', flags));
173
+ this.exit(1);
174
+ }
175
+ this.error(`Failed to sync ${ticketId}: ${message}`);
176
+ }
177
+ }
178
+ }
@@ -10,6 +10,12 @@ export declare function buildOrchestratorSessionName(hqName: string, name?: stri
10
10
  * Returns all tmux session names that start with 'prlt-orchestrator-'.
11
11
  */
12
12
  export declare function findRunningOrchestratorSessions(hostSessions: string[]): string[];
13
+ export declare function resolveOrchestratorName(name?: string): string;
14
+ export declare function buildOrchestratorAttachCommand(name: string): string;
15
+ export declare function extractOrchestratorNameFromSession(sessionName: string, hqName: string): string | null;
16
+ export declare function collectReservedOrchestratorNames(agentNames: string[], hostSessions: string[], hqName: string): Set<string>;
17
+ export declare function buildAvailableOrchestratorNames(reserved: Set<string>, maxNames?: number): string[];
18
+ export declare function findGlobalOrchestratorNameConflict(name: string, reserved: Set<string>): string | null;
13
19
  export default class OrchestratorStart extends PromptCommand {
14
20
  static description: string;
15
21
  static examples: string[];
@@ -9,9 +9,10 @@ import { getHeadquartersNameFromPath } from '../../lib/machine-config.js';
9
9
  import { shouldOutputJson, outputErrorAsJson, outputSuccessAsJson, createMetadata, buildPromptConfig, outputPromptAsJson, } from '../../lib/prompt-json.js';
10
10
  import { styles } from '../../lib/styles.js';
11
11
  import { DEFAULT_EXECUTION_CONFIG, } from '../../lib/execution/types.js';
12
- import { runExecution } from '../../lib/execution/runners.js';
12
+ import { runExecution, hostCredentialsExist } from '../../lib/execution/runners.js';
13
13
  import { getHostTmuxSessionNames } from '../../lib/execution/session-utils.js';
14
14
  import { ExecutionStorage } from '../../lib/execution/storage.js';
15
+ import { getWorkspaceInfo } from '../../lib/agents/commands.js';
15
16
  import { loadExecutionConfig, getTerminalApp, getShell, detectShell, detectTerminalApp, } from '../../lib/execution/config.js';
16
17
  /**
17
18
  * Sanitize a name segment for use in tmux session names.
@@ -39,6 +40,68 @@ export function buildOrchestratorSessionName(hqName, name = 'main') {
39
40
  export function findRunningOrchestratorSessions(hostSessions) {
40
41
  return hostSessions.filter(s => s.startsWith('prlt-orchestrator-'));
41
42
  }
43
+ export function resolveOrchestratorName(name) {
44
+ const normalized = name?.trim();
45
+ return normalized && normalized.length > 0 ? normalized : 'main';
46
+ }
47
+ export function buildOrchestratorAttachCommand(name) {
48
+ return name === 'main'
49
+ ? 'prlt orchestrator attach'
50
+ : `prlt orchestrator attach --name ${name}`;
51
+ }
52
+ export function extractOrchestratorNameFromSession(sessionName, hqName) {
53
+ const prefix = `prlt-orchestrator-${sanitizeName(hqName) || 'default'}-`;
54
+ if (!sessionName.startsWith(prefix)) {
55
+ return null;
56
+ }
57
+ const extracted = sessionName.slice(prefix.length);
58
+ return extracted.length > 0 ? extracted : null;
59
+ }
60
+ export function collectReservedOrchestratorNames(agentNames, hostSessions, hqName) {
61
+ const reserved = new Set();
62
+ for (const agentName of agentNames) {
63
+ const normalized = resolveOrchestratorName(sanitizeName(agentName).toLowerCase());
64
+ reserved.add(normalized);
65
+ }
66
+ for (const session of hostSessions) {
67
+ const extracted = extractOrchestratorNameFromSession(session, hqName);
68
+ if (!extracted)
69
+ continue;
70
+ reserved.add(resolveOrchestratorName(extracted.toLowerCase()));
71
+ }
72
+ return reserved;
73
+ }
74
+ function nextAvailableName(baseName, reserved) {
75
+ const normalizedBase = resolveOrchestratorName(baseName.toLowerCase());
76
+ if (!reserved.has(normalizedBase)) {
77
+ return normalizedBase;
78
+ }
79
+ let suffix = 2;
80
+ while (reserved.has(`${normalizedBase}-${suffix}`)) {
81
+ suffix += 1;
82
+ }
83
+ return `${normalizedBase}-${suffix}`;
84
+ }
85
+ export function buildAvailableOrchestratorNames(reserved, maxNames = 8) {
86
+ const orderedBases = ['main', ...Array.from(reserved).sort()];
87
+ const suggestions = [];
88
+ const taken = new Set(reserved);
89
+ for (const base of orderedBases) {
90
+ const candidate = nextAvailableName(base, taken);
91
+ if (suggestions.includes(candidate))
92
+ continue;
93
+ suggestions.push(candidate);
94
+ taken.add(candidate);
95
+ if (suggestions.length >= maxNames) {
96
+ break;
97
+ }
98
+ }
99
+ return suggestions;
100
+ }
101
+ export function findGlobalOrchestratorNameConflict(name, reserved) {
102
+ const normalized = resolveOrchestratorName(sanitizeName(name).toLowerCase());
103
+ return reserved.has(normalized) ? normalized : null;
104
+ }
42
105
  export default class OrchestratorStart extends PromptCommand {
43
106
  static description = 'Start the orchestrator agent in a tmux session';
44
107
  static examples = [
@@ -93,7 +156,6 @@ export default class OrchestratorStart extends PromptCommand {
93
156
  async run() {
94
157
  const { flags } = await this.parse(OrchestratorStart);
95
158
  const jsonMode = shouldOutputJson(flags);
96
- const orchestratorName = flags.name || 'main';
97
159
  // Resolve HQ path first (needed for scoped session name)
98
160
  const hqPath = findHQRoot(process.cwd());
99
161
  if (!hqPath) {
@@ -103,26 +165,68 @@ export default class OrchestratorStart extends PromptCommand {
103
165
  }
104
166
  this.error('Not in an HQ workspace. Run "prlt init" first.');
105
167
  }
106
- // Build session name scoped to this HQ
168
+ // Resolve orchestrator name (interactive prompt when --name is omitted)
169
+ const workspaceInfo = getWorkspaceInfo();
107
170
  const hqName = getHeadquartersNameFromPath(hqPath);
171
+ const hostSessions = getHostTmuxSessionNames();
172
+ const reservedAgentNames = new Set(workspaceInfo.agents.map(agent => resolveOrchestratorName(sanitizeName(agent.name).toLowerCase())));
173
+ const reservedNames = collectReservedOrchestratorNames(workspaceInfo.agents.map(agent => agent.name), hostSessions, hqName);
174
+ let orchestratorName = resolveOrchestratorName(flags.name);
175
+ if (!flags.name && !jsonMode) {
176
+ const availableNames = buildAvailableOrchestratorNames(reservedNames);
177
+ const { selectedName } = await this.prompt([{
178
+ type: 'list',
179
+ name: 'selectedName',
180
+ message: 'Select orchestrator name:',
181
+ choices: [
182
+ ...availableNames.map(name => ({
183
+ name,
184
+ value: name,
185
+ command: `prlt orchestrator start --name ${name} --json`,
186
+ })),
187
+ { name: 'Custom...', value: '__custom__' },
188
+ ],
189
+ }]);
190
+ if (selectedName === '__custom__') {
191
+ const defaultCustomName = availableNames[0] || 'main';
192
+ const { customName } = await this.prompt([{
193
+ type: 'input',
194
+ name: 'customName',
195
+ message: 'Enter orchestrator name:',
196
+ default: defaultCustomName,
197
+ validate: (input) => {
198
+ const normalized = resolveOrchestratorName(sanitizeName(String(input ?? '')).toLowerCase());
199
+ if (reservedNames.has(normalized)) {
200
+ return `Name "${normalized}" is already in use by an agent or orchestrator session.`;
201
+ }
202
+ return true;
203
+ },
204
+ }]);
205
+ orchestratorName = resolveOrchestratorName(customName);
206
+ }
207
+ else {
208
+ orchestratorName = resolveOrchestratorName(selectedName);
209
+ }
210
+ }
211
+ const attachCommand = buildOrchestratorAttachCommand(orchestratorName);
212
+ const attachArgs = orchestratorName === 'main' ? [] : ['--name', orchestratorName];
213
+ // Build session name scoped to this HQ
108
214
  const sessionName = buildOrchestratorSessionName(hqName, orchestratorName);
109
215
  // Check if orchestrator is already running
110
- const hostSessions = getHostTmuxSessionNames();
111
216
  if (hostSessions.includes(sessionName)) {
112
217
  if (jsonMode) {
113
- outputErrorAsJson('ALREADY_RUNNING', `Orchestrator is already running (session: ${sessionName}). Use "prlt orchestrator attach${flags.name ? ` --name ${flags.name}` : ''}" to reattach.`, createMetadata('orchestrator start', flags));
218
+ outputErrorAsJson('ALREADY_RUNNING', `Orchestrator is already running (session: ${sessionName}). Use "${attachCommand}" to reattach.`, createMetadata('orchestrator start', flags));
114
219
  return;
115
220
  }
116
221
  this.log('');
117
222
  this.log(styles.warning(`Orchestrator is already running (session: ${sessionName})`));
118
223
  this.log('');
119
- const attachArgs = flags.name ? ['--name', flags.name] : [];
120
224
  const { choice } = await this.prompt([{
121
225
  type: 'list',
122
226
  name: 'choice',
123
227
  message: 'What would you like to do?',
124
228
  choices: [
125
- { name: 'Attach to running orchestrator', value: 'attach', command: `prlt orchestrator attach${flags.name ? ` --name ${flags.name}` : ''} --json` },
229
+ { name: 'Attach to running orchestrator', value: 'attach', command: `${attachCommand} --json` },
126
230
  { name: 'Cancel', value: 'cancel' },
127
231
  ],
128
232
  }], jsonMode ? { flags, commandName: 'orchestrator start' } : null);
@@ -131,6 +235,14 @@ export default class OrchestratorStart extends PromptCommand {
131
235
  }
132
236
  return;
133
237
  }
238
+ const conflict = findGlobalOrchestratorNameConflict(orchestratorName, reservedAgentNames);
239
+ if (conflict) {
240
+ if (jsonMode) {
241
+ outputErrorAsJson('NAME_CONFLICT', `Orchestrator name "${conflict}" is already in use by a staff/temp agent. Choose a unique name with --name.`, createMetadata('orchestrator start', flags));
242
+ return;
243
+ }
244
+ this.error(`Orchestrator name "${conflict}" is already in use by a staff/temp agent. Choose a different name.`);
245
+ }
134
246
  // Executor selection
135
247
  let selectedExecutor;
136
248
  if (flags.executor) {
@@ -156,6 +268,32 @@ export default class OrchestratorStart extends PromptCommand {
156
268
  }]);
157
269
  selectedExecutor = executor;
158
270
  }
271
+ // Validate Claude Code authentication for claude-code executor
272
+ if (selectedExecutor === 'claude-code' && !hostCredentialsExist()) {
273
+ const errorMsg = 'Claude Code authentication is not available. This usually happens when the macOS keychain is locked in SSH sessions.';
274
+ const remediation = [
275
+ '',
276
+ 'To fix this, choose one of the following:',
277
+ '',
278
+ '1. Unlock the keychain:',
279
+ ' security unlock-keychain',
280
+ '',
281
+ '2. Set the ANTHROPIC_API_KEY environment variable:',
282
+ ' export ANTHROPIC_API_KEY=your-api-key',
283
+ '',
284
+ '3. Login to Claude Code:',
285
+ ' claude /login',
286
+ '',
287
+ ].join('\n');
288
+ if (jsonMode) {
289
+ outputErrorAsJson('AUTH_UNAVAILABLE', errorMsg + '\n' + remediation, createMetadata('orchestrator start', flags));
290
+ return;
291
+ }
292
+ this.log('');
293
+ this.log(styles.error(errorMsg));
294
+ this.log(remediation);
295
+ return;
296
+ }
159
297
  // Permission mode selection
160
298
  let permissionMode;
161
299
  if (flags['skip-permissions']) {
@@ -261,9 +399,9 @@ export default class OrchestratorStart extends PromptCommand {
261
399
  }
262
400
  else {
263
401
  const displayChoices = [
264
- { name: 'New terminal tab — opens attached to the tmux session', value: 'terminal', command: `prlt orchestrator start${flags.name ? ` --name ${flags.name}` : ''} --json` },
265
- { name: 'Current session — attach to tmux here (foreground, blocking)', value: 'foreground', command: `prlt orchestrator start${flags.name ? ` --name ${flags.name}` : ''} --foreground --json` },
266
- { name: 'Background — start detached, attach later', value: 'background', command: `prlt orchestrator start${flags.name ? ` --name ${flags.name}` : ''} --background --json` },
402
+ { name: 'New terminal tab — opens attached to the tmux session', value: 'terminal', command: `prlt orchestrator start${orchestratorName !== 'main' ? ` --name ${orchestratorName}` : ''} --json` },
403
+ { name: 'Current session — attach to tmux here (foreground, blocking)', value: 'foreground', command: `prlt orchestrator start${orchestratorName !== 'main' ? ` --name ${orchestratorName}` : ''} --foreground --json` },
404
+ { name: 'Background — start detached, attach later', value: 'background', command: `prlt orchestrator start${orchestratorName !== 'main' ? ` --name ${orchestratorName}` : ''} --background --json` },
267
405
  ];
268
406
  const displayMessage = 'How do you want to view the orchestrator?';
269
407
  if (jsonMode) {
@@ -346,7 +484,7 @@ export default class OrchestratorStart extends PromptCommand {
346
484
  if (displayMode === 'background') {
347
485
  this.log(styles.success(`Orchestrator started in background`));
348
486
  this.log(styles.muted(` Session: ${result.sessionId || sessionName}`));
349
- this.log(styles.muted(` Attach with: prlt orchestrator attach${flags.name ? ` --name ${flags.name}` : ''}`));
487
+ this.log(styles.muted(` Attach with: ${attachCommand}`));
350
488
  }
351
489
  else {
352
490
  this.log(styles.success(`Orchestrator started`));