@proletariat/cli 0.3.45 → 0.3.47

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 (77) hide show
  1. package/bin/validate-better-sqlite3.cjs +55 -0
  2. package/dist/commands/config/index.js +39 -1
  3. package/dist/commands/linear/auth.d.ts +14 -0
  4. package/dist/commands/linear/auth.js +211 -0
  5. package/dist/commands/linear/import.d.ts +21 -0
  6. package/dist/commands/linear/import.js +260 -0
  7. package/dist/commands/linear/status.d.ts +11 -0
  8. package/dist/commands/linear/status.js +88 -0
  9. package/dist/commands/linear/sync.d.ts +15 -0
  10. package/dist/commands/linear/sync.js +233 -0
  11. package/dist/commands/orchestrator/attach.d.ts +10 -1
  12. package/dist/commands/orchestrator/attach.js +102 -18
  13. package/dist/commands/orchestrator/index.js +22 -7
  14. package/dist/commands/orchestrator/start.d.ts +13 -1
  15. package/dist/commands/orchestrator/start.js +96 -25
  16. package/dist/commands/orchestrator/status.d.ts +1 -0
  17. package/dist/commands/orchestrator/status.js +10 -5
  18. package/dist/commands/orchestrator/stop.d.ts +1 -0
  19. package/dist/commands/orchestrator/stop.js +9 -4
  20. package/dist/commands/session/attach.js +32 -9
  21. package/dist/commands/ticket/link/duplicates.d.ts +15 -0
  22. package/dist/commands/ticket/link/duplicates.js +95 -0
  23. package/dist/commands/ticket/link/index.js +14 -0
  24. package/dist/commands/ticket/link/relates.d.ts +15 -0
  25. package/dist/commands/ticket/link/relates.js +95 -0
  26. package/dist/commands/work/index.js +4 -0
  27. package/dist/commands/work/review.d.ts +45 -0
  28. package/dist/commands/work/review.js +401 -0
  29. package/dist/commands/work/revise.js +4 -3
  30. package/dist/commands/work/spawn.d.ts +5 -0
  31. package/dist/commands/work/spawn.js +195 -14
  32. package/dist/commands/work/start.js +75 -19
  33. package/dist/hooks/init.js +18 -5
  34. package/dist/lib/database/native-validation.d.ts +21 -0
  35. package/dist/lib/database/native-validation.js +49 -0
  36. package/dist/lib/execution/config.d.ts +15 -0
  37. package/dist/lib/execution/config.js +54 -0
  38. package/dist/lib/execution/devcontainer.d.ts +6 -3
  39. package/dist/lib/execution/devcontainer.js +39 -12
  40. package/dist/lib/execution/runners.d.ts +28 -32
  41. package/dist/lib/execution/runners.js +353 -277
  42. package/dist/lib/execution/spawner.js +62 -5
  43. package/dist/lib/execution/types.d.ts +4 -0
  44. package/dist/lib/execution/types.js +3 -0
  45. package/dist/lib/external-issues/adapters.d.ts +26 -0
  46. package/dist/lib/external-issues/adapters.js +251 -0
  47. package/dist/lib/external-issues/index.d.ts +10 -0
  48. package/dist/lib/external-issues/index.js +14 -0
  49. package/dist/lib/external-issues/mapper.d.ts +21 -0
  50. package/dist/lib/external-issues/mapper.js +86 -0
  51. package/dist/lib/external-issues/types.d.ts +144 -0
  52. package/dist/lib/external-issues/types.js +26 -0
  53. package/dist/lib/external-issues/validation.d.ts +34 -0
  54. package/dist/lib/external-issues/validation.js +219 -0
  55. package/dist/lib/linear/client.d.ts +55 -0
  56. package/dist/lib/linear/client.js +254 -0
  57. package/dist/lib/linear/config.d.ts +37 -0
  58. package/dist/lib/linear/config.js +100 -0
  59. package/dist/lib/linear/index.d.ts +11 -0
  60. package/dist/lib/linear/index.js +10 -0
  61. package/dist/lib/linear/mapper.d.ts +67 -0
  62. package/dist/lib/linear/mapper.js +219 -0
  63. package/dist/lib/linear/sync.d.ts +37 -0
  64. package/dist/lib/linear/sync.js +89 -0
  65. package/dist/lib/linear/types.d.ts +139 -0
  66. package/dist/lib/linear/types.js +34 -0
  67. package/dist/lib/mcp/helpers.d.ts +8 -0
  68. package/dist/lib/mcp/helpers.js +10 -0
  69. package/dist/lib/mcp/tools/board.js +63 -11
  70. package/dist/lib/mcp/tools/work.js +36 -0
  71. package/dist/lib/pmo/schema.d.ts +2 -0
  72. package/dist/lib/pmo/schema.js +20 -0
  73. package/dist/lib/pmo/storage/base.js +92 -13
  74. package/dist/lib/pmo/storage/dependencies.js +15 -0
  75. package/dist/lib/prompt-json.d.ts +4 -0
  76. package/oclif.manifest.json +3205 -2537
  77. package/package.json +3 -2
@@ -0,0 +1,88 @@
1
+ import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
2
+ import { colors } from '../../lib/colors.js';
3
+ import { shouldOutputJson, outputSuccessAsJson, createMetadata, } from '../../lib/prompt-json.js';
4
+ import { LinearClient, isLinearConfigured, loadLinearConfig, } from '../../lib/linear/index.js';
5
+ import { LinearMapper } from '../../lib/linear/mapper.js';
6
+ export default class LinearStatus extends PMOCommand {
7
+ static description = 'Show Linear integration status and connection info';
8
+ static examples = [
9
+ '<%= config.bin %> <%= command.id %>',
10
+ '<%= config.bin %> <%= command.id %> --json',
11
+ ];
12
+ static flags = {
13
+ ...pmoBaseFlags,
14
+ };
15
+ async execute() {
16
+ const { flags } = await this.parse(LinearStatus);
17
+ const jsonMode = shouldOutputJson(flags);
18
+ const db = this.storage.getDatabase();
19
+ if (!isLinearConfigured(db)) {
20
+ if (jsonMode) {
21
+ outputSuccessAsJson({
22
+ configured: false,
23
+ message: 'Linear is not configured. Run "prlt linear auth" to connect.',
24
+ }, createMetadata('linear status', flags));
25
+ return;
26
+ }
27
+ this.log(colors.warning('Linear is not configured'));
28
+ this.log(colors.textMuted('Run "prlt linear auth" to connect your Linear workspace.'));
29
+ return;
30
+ }
31
+ const config = loadLinearConfig(db);
32
+ // Verify connection
33
+ let connectionInfo = null;
34
+ let connectionError = null;
35
+ try {
36
+ const client = new LinearClient(config.apiKey);
37
+ connectionInfo = await client.verify();
38
+ }
39
+ catch (error) {
40
+ connectionError = error instanceof Error ? error.message : String(error);
41
+ }
42
+ // Count mapped issues
43
+ const mapper = new LinearMapper(db);
44
+ const mappings = mapper.listMappings();
45
+ if (jsonMode) {
46
+ outputSuccessAsJson({
47
+ configured: true,
48
+ connected: connectionInfo !== null,
49
+ organization: connectionInfo?.organizationName ?? config.organizationName ?? null,
50
+ user: connectionInfo?.userName ?? null,
51
+ email: connectionInfo?.email ?? null,
52
+ defaultTeam: config.defaultTeamKey ?? null,
53
+ mappedIssues: mappings.length,
54
+ error: connectionError,
55
+ }, createMetadata('linear status', flags));
56
+ return;
57
+ }
58
+ this.log(colors.primary('Linear Integration Status'));
59
+ this.log('');
60
+ if (connectionInfo) {
61
+ this.log(` ${colors.success('Connected')}`);
62
+ this.log(colors.textMuted(` Organization: ${connectionInfo.organizationName}`));
63
+ this.log(colors.textMuted(` User: ${connectionInfo.userName} (${connectionInfo.email})`));
64
+ }
65
+ else {
66
+ this.log(` ${colors.error('Connection failed')}`);
67
+ if (connectionError) {
68
+ this.log(colors.textMuted(` Error: ${connectionError}`));
69
+ }
70
+ }
71
+ if (config.defaultTeamKey) {
72
+ this.log(colors.textMuted(` Default team: ${config.defaultTeamKey}`));
73
+ }
74
+ this.log('');
75
+ this.log(colors.textMuted(` Mapped issues: ${mappings.length}`));
76
+ if (mappings.length > 0) {
77
+ const recent = mappings.slice(0, 5);
78
+ this.log('');
79
+ this.log(colors.textMuted(' Recent mappings:'));
80
+ for (const m of recent) {
81
+ this.log(colors.textMuted(` ${m.linearIdentifier} → ${m.pmoTicketId}`));
82
+ }
83
+ if (mappings.length > 5) {
84
+ this.log(colors.textMuted(` ... and ${mappings.length - 5} more`));
85
+ }
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,15 @@
1
+ import { PMOCommand } from '../../lib/pmo/index.js';
2
+ export default class LinearSyncCommand 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
+ 'pr-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ 'pr-title': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ execute(): Promise<void>;
15
+ }
@@ -0,0 +1,233 @@
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 { LinearClient, LinearMapper, LinearSync, isLinearConfigured, loadLinearConfig, } from '../../lib/linear/index.js';
6
+ export default class LinearSyncCommand extends PMOCommand {
7
+ static description = 'Sync PMO ticket status and PR links back to Linear';
8
+ static examples = [
9
+ '<%= config.bin %> <%= command.id %> # Sync all mapped tickets',
10
+ '<%= config.bin %> <%= command.id %> --ticket TKT-001 # Sync a specific ticket',
11
+ '<%= config.bin %> <%= command.id %> --pr-url https://github.com/... --ticket TKT-001 # Attach PR to Linear issue',
12
+ '<%= config.bin %> <%= command.id %> --dry-run # Preview what would be synced',
13
+ ];
14
+ static flags = {
15
+ ...pmoBaseFlags,
16
+ ticket: Flags.string({
17
+ description: 'PMO ticket ID to sync (syncs all if omitted)',
18
+ }),
19
+ 'pr-url': Flags.string({
20
+ description: 'PR URL to attach to the Linear issue',
21
+ }),
22
+ 'pr-title': Flags.string({
23
+ description: 'PR title for the attachment (defaults to ticket title)',
24
+ }),
25
+ 'dry-run': Flags.boolean({
26
+ description: 'Preview what would be synced without making changes',
27
+ default: false,
28
+ }),
29
+ };
30
+ async execute() {
31
+ const { flags } = await this.parse(LinearSyncCommand);
32
+ const jsonMode = shouldOutputJson(flags);
33
+ const db = this.storage.getDatabase();
34
+ if (!isLinearConfigured(db)) {
35
+ if (jsonMode) {
36
+ outputErrorAsJson('LINEAR_NOT_CONFIGURED', 'Linear is not configured. Run "prlt linear auth" first.', createMetadata('linear sync', flags));
37
+ this.exit(1);
38
+ }
39
+ this.error('Linear is not configured. Run "prlt linear auth" first.');
40
+ }
41
+ const config = loadLinearConfig(db);
42
+ const client = new LinearClient(config.apiKey);
43
+ const mapper = new LinearMapper(db);
44
+ const sync = new LinearSync(client, mapper);
45
+ // Handle PR link attachment
46
+ if (flags['pr-url'] && flags.ticket) {
47
+ const prTitle = flags['pr-title'] ?? (await this.storage.getTicket(flags.ticket))?.title ?? 'Pull Request';
48
+ if (flags['dry-run']) {
49
+ if (jsonMode) {
50
+ outputSuccessAsJson({
51
+ dryRun: true,
52
+ action: 'attach-pr',
53
+ ticketId: flags.ticket,
54
+ prUrl: flags['pr-url'],
55
+ prTitle,
56
+ }, createMetadata('linear sync', flags));
57
+ return;
58
+ }
59
+ this.log(colors.textMuted(`Would attach PR to ${flags.ticket}: ${flags['pr-url']}`));
60
+ return;
61
+ }
62
+ const attached = await sync.syncPRLink(flags.ticket, flags['pr-url'], prTitle);
63
+ if (jsonMode) {
64
+ outputSuccessAsJson({
65
+ action: 'attach-pr',
66
+ ticketId: flags.ticket,
67
+ prUrl: flags['pr-url'],
68
+ synced: attached,
69
+ }, createMetadata('linear sync', flags));
70
+ return;
71
+ }
72
+ if (attached) {
73
+ this.log(colors.success(`PR linked to Linear issue for ${flags.ticket}`));
74
+ }
75
+ else {
76
+ this.log(colors.warning(`No Linear mapping found for ${flags.ticket}`));
77
+ }
78
+ return;
79
+ }
80
+ // Sync ticket status(es) back to Linear
81
+ if (flags.ticket) {
82
+ // Single ticket sync
83
+ const ticket = await this.storage.getTicket(flags.ticket);
84
+ if (!ticket) {
85
+ if (jsonMode) {
86
+ outputErrorAsJson('TICKET_NOT_FOUND', `Ticket ${flags.ticket} not found.`, createMetadata('linear sync', flags));
87
+ this.exit(1);
88
+ }
89
+ this.error(`Ticket ${flags.ticket} not found.`);
90
+ }
91
+ const mapping = mapper.getByTicketId(flags.ticket);
92
+ if (!mapping) {
93
+ if (jsonMode) {
94
+ outputSuccessAsJson({
95
+ synced: false,
96
+ message: `Ticket ${flags.ticket} has no Linear mapping.`,
97
+ }, createMetadata('linear sync', flags));
98
+ return;
99
+ }
100
+ this.log(colors.warning(`Ticket ${flags.ticket} has no Linear mapping.`));
101
+ return;
102
+ }
103
+ // Get Linear states for the team
104
+ const team = await client.getTeamByKey(mapping.linearTeamKey);
105
+ if (!team) {
106
+ if (jsonMode) {
107
+ outputErrorAsJson('TEAM_NOT_FOUND', `Linear team ${mapping.linearTeamKey} not found.`, createMetadata('linear sync', flags));
108
+ this.exit(1);
109
+ }
110
+ this.error(`Linear team ${mapping.linearTeamKey} not found.`);
111
+ }
112
+ const linearStates = await client.listStates(team.id);
113
+ if (flags['dry-run']) {
114
+ if (jsonMode) {
115
+ outputSuccessAsJson({
116
+ dryRun: true,
117
+ action: 'sync-status',
118
+ ticketId: flags.ticket,
119
+ linearIdentifier: mapping.linearIdentifier,
120
+ currentStatus: ticket.statusName,
121
+ statusCategory: ticket.statusCategory,
122
+ }, createMetadata('linear sync', flags));
123
+ return;
124
+ }
125
+ this.log(colors.textMuted(`Would sync ${flags.ticket} (${mapping.linearIdentifier}): status "${ticket.statusName}" (${ticket.statusCategory})`));
126
+ return;
127
+ }
128
+ const synced = await sync.syncTicketStatus(ticket, linearStates);
129
+ if (jsonMode) {
130
+ outputSuccessAsJson({
131
+ action: 'sync-status',
132
+ ticketId: flags.ticket,
133
+ linearIdentifier: mapping.linearIdentifier,
134
+ synced,
135
+ }, createMetadata('linear sync', flags));
136
+ return;
137
+ }
138
+ if (synced) {
139
+ this.log(colors.success(`Synced ${mapping.linearIdentifier} status to Linear`));
140
+ }
141
+ else {
142
+ this.log(colors.warning(`Could not find matching Linear state for ${ticket.statusCategory}`));
143
+ }
144
+ return;
145
+ }
146
+ // Bulk sync: sync all mapped tickets
147
+ const mappings = mapper.listMappings();
148
+ if (mappings.length === 0) {
149
+ if (jsonMode) {
150
+ outputSuccessAsJson({
151
+ synced: 0,
152
+ message: 'No mapped tickets to sync.',
153
+ }, createMetadata('linear sync', flags));
154
+ return;
155
+ }
156
+ this.log(colors.textMuted('No mapped tickets to sync.'));
157
+ return;
158
+ }
159
+ if (!jsonMode) {
160
+ this.log(colors.textMuted(`Syncing ${mappings.length} mapped ticket(s) to Linear...`));
161
+ }
162
+ // Group mappings by team to fetch states efficiently
163
+ const teamKeys = [...new Set(mappings.map((m) => m.linearTeamKey))];
164
+ const statesByTeam = {};
165
+ for (const teamKey of teamKeys) {
166
+ // eslint-disable-next-line no-await-in-loop
167
+ const team = await client.getTeamByKey(teamKey);
168
+ if (team) {
169
+ // eslint-disable-next-line no-await-in-loop
170
+ statesByTeam[teamKey] = await client.listStates(team.id);
171
+ }
172
+ }
173
+ if (flags['dry-run']) {
174
+ const preview = [];
175
+ for (const mapping of mappings) {
176
+ // eslint-disable-next-line no-await-in-loop
177
+ const ticket = await this.storage.getTicket(mapping.pmoTicketId);
178
+ preview.push({
179
+ ticketId: mapping.pmoTicketId,
180
+ linearIdentifier: mapping.linearIdentifier,
181
+ currentStatus: ticket?.statusName,
182
+ statusCategory: ticket?.statusCategory,
183
+ });
184
+ }
185
+ if (jsonMode) {
186
+ outputSuccessAsJson({
187
+ dryRun: true,
188
+ action: 'sync-all',
189
+ tickets: preview,
190
+ }, createMetadata('linear sync', flags));
191
+ return;
192
+ }
193
+ this.log('');
194
+ for (const item of preview) {
195
+ this.log(colors.textMuted(` ${item.linearIdentifier} → ${item.currentStatus} (${item.statusCategory})`));
196
+ }
197
+ return;
198
+ }
199
+ // Perform the sync for each team
200
+ let totalSynced = 0;
201
+ let totalSkipped = 0;
202
+ let totalErrors = 0;
203
+ for (const teamKey of teamKeys) {
204
+ const teamStates = statesByTeam[teamKey];
205
+ if (!teamStates) {
206
+ totalErrors += mappings.filter((m) => m.linearTeamKey === teamKey).length;
207
+ continue;
208
+ }
209
+ const teamMappings = mappings.filter((m) => m.linearTeamKey === teamKey);
210
+ // eslint-disable-next-line no-await-in-loop
211
+ const result = await sync.syncAllStatuses(this.storage, teamMappings, teamStates);
212
+ totalSynced += result.synced;
213
+ totalSkipped += result.skipped;
214
+ totalErrors += result.errors;
215
+ }
216
+ if (jsonMode) {
217
+ outputSuccessAsJson({
218
+ action: 'sync-all',
219
+ synced: totalSynced,
220
+ skipped: totalSkipped,
221
+ errors: totalErrors,
222
+ }, createMetadata('linear sync', flags));
223
+ return;
224
+ }
225
+ this.log('');
226
+ if (totalSynced > 0)
227
+ this.log(colors.success(`Synced: ${totalSynced}`));
228
+ if (totalSkipped > 0)
229
+ this.log(colors.textMuted(`Skipped: ${totalSkipped}`));
230
+ if (totalErrors > 0)
231
+ this.log(colors.error(`Errors: ${totalErrors}`));
232
+ }
233
+ }
@@ -1,13 +1,22 @@
1
1
  import { PromptCommand } from '../../lib/prompt-command.js';
2
+ /**
3
+ * Detect the terminal emulator from environment variables.
4
+ * Returns a terminal app name suitable for AppleScript tab creation,
5
+ * or null if detection fails or we're in a remote/headless environment.
6
+ */
7
+ export declare function detectTerminalApp(): string | null;
2
8
  export default class OrchestratorAttach extends PromptCommand {
3
9
  static description: string;
4
10
  static examples: string[];
5
11
  static flags: {
12
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'new-tab': import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ terminal: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
15
  'current-terminal': import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
- terminal: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
16
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
17
  machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
18
  };
11
19
  run(): Promise<void>;
20
+ private attachInCurrentTerminal;
12
21
  private openInNewTab;
13
22
  }
@@ -3,37 +3,80 @@ import { execSync } from 'node:child_process';
3
3
  import * as path from 'node:path';
4
4
  import * as fs from 'node:fs';
5
5
  import * as os from 'node:os';
6
+ import Database from 'better-sqlite3';
6
7
  import { PromptCommand } from '../../lib/prompt-command.js';
7
8
  import { machineOutputFlags } from '../../lib/pmo/index.js';
8
9
  import { shouldOutputJson, outputErrorAsJson, outputSuccessAsJson, createMetadata, } from '../../lib/prompt-json.js';
9
10
  import { styles } from '../../lib/styles.js';
10
11
  import { getHostTmuxSessionNames } from '../../lib/execution/session-utils.js';
11
- import { ORCHESTRATOR_SESSION_NAME } from './start.js';
12
+ import { getWorkspaceInfo } from '../../lib/agents/commands.js';
13
+ import { loadExecutionConfig, shouldUseControlMode, buildTmuxAttachCommand } from '../../lib/execution/index.js';
14
+ import { buildOrchestratorSessionName } from './start.js';
15
+ /**
16
+ * Detect the terminal emulator from environment variables.
17
+ * Returns a terminal app name suitable for AppleScript tab creation,
18
+ * or null if detection fails or we're in a remote/headless environment.
19
+ */
20
+ export function detectTerminalApp() {
21
+ // Remote sessions should never attempt AppleScript/GUI operations
22
+ if (process.env.SSH_TTY || process.env.SSH_CONNECTION) {
23
+ return null;
24
+ }
25
+ // Headless / no display — skip GUI attempts
26
+ if (process.platform !== 'darwin' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
27
+ return null;
28
+ }
29
+ const termProgram = process.env.TERM_PROGRAM;
30
+ if (!termProgram)
31
+ return null;
32
+ switch (termProgram) {
33
+ case 'iTerm.app':
34
+ return 'iTerm';
35
+ case 'ghostty':
36
+ return 'Ghostty';
37
+ case 'Apple_Terminal':
38
+ return 'Terminal';
39
+ case 'WezTerm':
40
+ return 'WezTerm';
41
+ default:
42
+ return null;
43
+ }
44
+ }
12
45
  export default class OrchestratorAttach extends PromptCommand {
13
46
  static description = 'Attach to the running orchestrator tmux session';
14
47
  static examples = [
15
48
  '<%= config.bin %> <%= command.id %>',
16
- '<%= config.bin %> <%= command.id %> --current-terminal',
49
+ '<%= config.bin %> <%= command.id %> --new-tab',
50
+ '<%= config.bin %> <%= command.id %> --new-tab --terminal Ghostty',
17
51
  ];
18
52
  static flags = {
19
53
  ...machineOutputFlags,
20
- 'current-terminal': Flags.boolean({
21
- char: 'c',
22
- description: 'Attach in current terminal instead of new tab',
54
+ name: Flags.string({
55
+ char: 'n',
56
+ description: 'Name of the orchestrator session to attach to (default: main)',
57
+ }),
58
+ 'new-tab': Flags.boolean({
59
+ description: 'Open in a new terminal tab instead of attaching in the current terminal',
23
60
  default: false,
24
61
  }),
25
62
  terminal: Flags.string({
26
63
  char: 't',
27
- description: 'Terminal app to use (iTerm, Terminal, Ghostty)',
28
- default: 'iTerm',
64
+ description: 'Terminal app to use for new tab (iTerm, Terminal, Ghostty). Auto-detected if not specified.',
65
+ }),
66
+ 'current-terminal': Flags.boolean({
67
+ char: 'c',
68
+ description: '[deprecated] Attach in current terminal (this is now the default behavior)',
69
+ hidden: true,
70
+ default: false,
29
71
  }),
30
72
  };
31
73
  async run() {
32
74
  const { flags } = await this.parse(OrchestratorAttach);
33
75
  const jsonMode = shouldOutputJson(flags);
76
+ const sessionName = buildOrchestratorSessionName(flags.name || 'main');
34
77
  // Check if orchestrator session exists
35
78
  const hostSessions = getHostTmuxSessionNames();
36
- if (!hostSessions.includes(ORCHESTRATOR_SESSION_NAME)) {
79
+ if (!hostSessions.includes(sessionName)) {
37
80
  if (jsonMode) {
38
81
  outputErrorAsJson('NOT_RUNNING', 'Orchestrator is not running. Start it with: prlt orchestrator start', createMetadata('orchestrator attach', flags));
39
82
  return;
@@ -46,28 +89,69 @@ export default class OrchestratorAttach extends PromptCommand {
46
89
  }
47
90
  if (jsonMode) {
48
91
  outputSuccessAsJson({
49
- sessionId: ORCHESTRATOR_SESSION_NAME,
92
+ sessionId: sessionName,
50
93
  status: 'attaching',
51
94
  }, createMetadata('orchestrator attach', flags));
52
95
  return;
53
96
  }
54
- this.log('');
55
- this.log(styles.info(`Attaching to orchestrator session: ${ORCHESTRATOR_SESSION_NAME}`));
56
97
  if (flags['current-terminal']) {
98
+ this.log(styles.warning('--current-terminal is deprecated. Direct tmux attach is now the default behavior.'));
99
+ }
100
+ if (flags.terminal && !flags['new-tab']) {
101
+ this.log(styles.warning('--terminal has no effect without --new-tab. Ignoring.'));
102
+ }
103
+ this.log('');
104
+ this.log(styles.info(`Attaching to orchestrator session: ${sessionName}`));
105
+ // Determine if we should use tmux control mode (-u -CC) for iTerm
106
+ let useControlMode = false;
107
+ try {
108
+ const workspaceInfo = getWorkspaceInfo();
109
+ const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
110
+ const db = new Database(dbPath);
57
111
  try {
58
- execSync(`tmux attach -t "${ORCHESTRATOR_SESSION_NAME}"`, { stdio: 'inherit' });
112
+ const config = loadExecutionConfig(db);
113
+ const termApp = detectTerminalApp();
114
+ if (termApp === 'iTerm') {
115
+ useControlMode = shouldUseControlMode('iTerm', config.tmux.controlMode);
116
+ }
117
+ }
118
+ finally {
119
+ db.close();
59
120
  }
60
- catch {
61
- this.error(`Failed to attach to orchestrator session "${ORCHESTRATOR_SESSION_NAME}"`);
121
+ }
122
+ catch {
123
+ // Not in a workspace or DB not available - fall back to no control mode
124
+ }
125
+ if (flags['new-tab']) {
126
+ // Determine terminal app: explicit flag > auto-detect > error
127
+ const terminalApp = flags.terminal ?? detectTerminalApp();
128
+ if (!terminalApp) {
129
+ this.log(styles.warning('Could not detect terminal emulator for new tab.'));
130
+ this.log(styles.muted('Falling back to direct tmux attach in current terminal.'));
131
+ this.log(styles.muted('Tip: Use --terminal <app> to specify your terminal (iTerm, Terminal, Ghostty).'));
132
+ this.log('');
133
+ this.attachInCurrentTerminal(useControlMode, sessionName);
134
+ return;
62
135
  }
136
+ await this.openInNewTab(terminalApp, useControlMode, sessionName);
63
137
  }
64
138
  else {
65
- await this.openInNewTab(flags.terminal);
139
+ this.attachInCurrentTerminal(useControlMode, sessionName);
140
+ }
141
+ }
142
+ attachInCurrentTerminal(useControlMode, sessionName) {
143
+ try {
144
+ const tmuxAttach = buildTmuxAttachCommand(useControlMode);
145
+ execSync(`${tmuxAttach} -t "${sessionName}"`, { stdio: 'inherit' });
146
+ }
147
+ catch {
148
+ this.error(`Failed to attach to orchestrator session "${sessionName}"`);
66
149
  }
67
150
  }
68
- async openInNewTab(terminalApp) {
151
+ async openInNewTab(terminalApp, useControlMode, sessionName) {
69
152
  const title = 'Orchestrator';
70
- const attachCmd = `tmux attach -t "${ORCHESTRATOR_SESSION_NAME}"`;
153
+ const tmuxAttach = buildTmuxAttachCommand(useControlMode);
154
+ const attachCmd = `${tmuxAttach} -t "${sessionName}"`;
71
155
  const baseDir = path.join(os.homedir(), '.proletariat', 'scripts');
72
156
  fs.mkdirSync(baseDir, { recursive: true });
73
157
  const scriptPath = path.join(baseDir, `attach-orch-${Date.now()}.sh`);
@@ -76,7 +160,7 @@ export default class OrchestratorAttach extends PromptCommand {
76
160
  echo -ne "\\033]0;${title}\\007"
77
161
  echo -ne "\\033]1;${title}\\007"
78
162
 
79
- echo "Attaching to: ${ORCHESTRATOR_SESSION_NAME}"
163
+ echo "Attaching to: ${sessionName}"
80
164
  ${attachCmd}
81
165
 
82
166
  # Clean up
@@ -1,5 +1,7 @@
1
1
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
2
2
  import { shouldOutputJson } from '../../lib/prompt-json.js';
3
+ import { getHostTmuxSessionNames } from '../../lib/execution/session-utils.js';
4
+ import { findRunningOrchestratorSessions } from './start.js';
3
5
  export default class Orchestrator extends PMOCommand {
4
6
  static description = 'Manage the orchestrator agent (start, attach, status, stop)';
5
7
  static examples = [
@@ -18,17 +20,30 @@ export default class Orchestrator extends PMOCommand {
18
20
  async execute() {
19
21
  const { flags } = await this.parse(Orchestrator);
20
22
  const jsonModeConfig = shouldOutputJson(flags) ? { flags, commandName: 'orchestrator' } : null;
23
+ // Check if orchestrator is currently running to offer contextual options
24
+ const hostSessions = getHostTmuxSessionNames();
25
+ const isRunning = findRunningOrchestratorSessions(hostSessions).length > 0;
26
+ // When running, show "Attach to running session" first since that's the likely intent
27
+ const choices = isRunning
28
+ ? [
29
+ { name: 'Attach to running session', value: 'attach', command: 'prlt orchestrator attach --json' },
30
+ { name: 'Start orchestrator', value: 'start', command: 'prlt orchestrator start --json' },
31
+ { name: 'Check orchestrator status', value: 'status', command: 'prlt orchestrator status --json' },
32
+ { name: 'Stop orchestrator', value: 'stop', command: 'prlt orchestrator stop --json' },
33
+ { name: 'Cancel', value: 'cancel' },
34
+ ]
35
+ : [
36
+ { name: 'Start orchestrator', value: 'start', command: 'prlt orchestrator start --json' },
37
+ { name: 'Attach to orchestrator', value: 'attach', command: 'prlt orchestrator attach --json' },
38
+ { name: 'Check orchestrator status', value: 'status', command: 'prlt orchestrator status --json' },
39
+ { name: 'Stop orchestrator', value: 'stop', command: 'prlt orchestrator stop --json' },
40
+ { name: 'Cancel', value: 'cancel' },
41
+ ];
21
42
  const { action } = await this.prompt([{
22
43
  type: 'list',
23
44
  name: 'action',
24
45
  message: 'Orchestrator - What would you like to do?',
25
- choices: [
26
- { name: 'Start orchestrator', value: 'start', command: 'prlt orchestrator start --json' },
27
- { name: 'Attach to orchestrator', value: 'attach', command: 'prlt orchestrator attach --json' },
28
- { name: 'Check orchestrator status', value: 'status', command: 'prlt orchestrator status --json' },
29
- { name: 'Stop orchestrator', value: 'stop', command: 'prlt orchestrator stop --json' },
30
- { name: 'Cancel', value: 'cancel' },
31
- ],
46
+ choices,
32
47
  }], jsonModeConfig);
33
48
  if (action === 'cancel') {
34
49
  return;
@@ -1,5 +1,15 @@
1
1
  import { PromptCommand } from '../../lib/prompt-command.js';
2
- export declare const ORCHESTRATOR_SESSION_NAME = "prlt-orchestrator-main";
2
+ /**
3
+ * Build orchestrator tmux session name.
4
+ * Default: 'prlt-orchestrator-main'
5
+ * With --name: 'prlt-orchestrator-{name}'
6
+ */
7
+ export declare function buildOrchestratorSessionName(name?: string): string;
8
+ /**
9
+ * Find running orchestrator session(s) by prefix match.
10
+ * Returns all tmux session names that start with 'prlt-orchestrator-'.
11
+ */
12
+ export declare function findRunningOrchestratorSessions(hostSessions: string[]): string[];
3
13
  export default class OrchestratorStart extends PromptCommand {
4
14
  static description: string;
5
15
  static examples: string[];
@@ -9,7 +19,9 @@ export default class OrchestratorStart extends PromptCommand {
9
19
  executor: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
20
  'skip-permissions': import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
21
  sandboxed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
22
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
23
  background: import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
+ foreground: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
25
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
26
  machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
27
  };