@proletariat/cli 0.3.46 → 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.
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ const SUPPORTED_NODE_MAJORS = [20, 22, 23, 24, 25]
4
+
5
+ function parseNodeMajor(version) {
6
+ const match = /^v?(\d+)/.exec(version)
7
+ return match ? Number.parseInt(match[1], 10) : null
8
+ }
9
+
10
+ function runtimeInfo() {
11
+ return {
12
+ nodeVersion: process.version,
13
+ nodeMajor: parseNodeMajor(process.version),
14
+ abi: process.versions.modules,
15
+ platform: process.platform,
16
+ arch: process.arch,
17
+ }
18
+ }
19
+
20
+ function buildMessage(error, context) {
21
+ const info = runtimeInfo()
22
+ const nodeMajorHint = info.nodeMajor === null || SUPPORTED_NODE_MAJORS.includes(info.nodeMajor)
23
+ ? ''
24
+ : `\n- Unsupported Node major for this CLI: ${info.nodeMajor} (supported: ${SUPPORTED_NODE_MAJORS.join(', ')})`
25
+ const reason = error instanceof Error ? error.message : String(error)
26
+
27
+ return [
28
+ `better-sqlite3 native module validation failed (${context}).`,
29
+ `Runtime: node ${info.nodeVersion} (ABI ${info.abi}) on ${info.platform}-${info.arch}.${nodeMajorHint}`,
30
+ `Load error: ${reason}`,
31
+ '',
32
+ 'Fix steps:',
33
+ '1. Rebuild native bindings for this runtime: npm rebuild better-sqlite3',
34
+ '2. Verify runtime architecture: node -p "process.platform + \'-\' + process.arch + \' abi=\' + process.versions.modules"',
35
+ '3. Reinstall with your active Node version if needed: npm uninstall -g @proletariat/cli && npm install -g @proletariat/cli',
36
+ ].join('\n')
37
+ }
38
+
39
+ function validate(context) {
40
+ try {
41
+ const Database = require('better-sqlite3')
42
+ const db = new Database(':memory:')
43
+ db.pragma('foreign_keys = ON')
44
+ db.close()
45
+ } catch (error) {
46
+ throw new Error(buildMessage(error, context))
47
+ }
48
+ }
49
+
50
+ try {
51
+ validate('postinstall')
52
+ } catch (error) {
53
+ console.error(error instanceof Error ? error.message : String(error))
54
+ process.exit(1)
55
+ }
@@ -9,6 +9,7 @@ export default class OrchestratorAttach extends PromptCommand {
9
9
  static description: string;
10
10
  static examples: string[];
11
11
  static flags: {
12
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  'new-tab': import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
14
  terminal: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
15
  'current-terminal': import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -3,12 +3,15 @@ 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';
12
15
  /**
13
16
  * Detect the terminal emulator from environment variables.
14
17
  * Returns a terminal app name suitable for AppleScript tab creation,
@@ -48,6 +51,10 @@ export default class OrchestratorAttach extends PromptCommand {
48
51
  ];
49
52
  static flags = {
50
53
  ...machineOutputFlags,
54
+ name: Flags.string({
55
+ char: 'n',
56
+ description: 'Name of the orchestrator session to attach to (default: main)',
57
+ }),
51
58
  'new-tab': Flags.boolean({
52
59
  description: 'Open in a new terminal tab instead of attaching in the current terminal',
53
60
  default: false,
@@ -66,9 +73,10 @@ export default class OrchestratorAttach extends PromptCommand {
66
73
  async run() {
67
74
  const { flags } = await this.parse(OrchestratorAttach);
68
75
  const jsonMode = shouldOutputJson(flags);
76
+ const sessionName = buildOrchestratorSessionName(flags.name || 'main');
69
77
  // Check if orchestrator session exists
70
78
  const hostSessions = getHostTmuxSessionNames();
71
- if (!hostSessions.includes(ORCHESTRATOR_SESSION_NAME)) {
79
+ if (!hostSessions.includes(sessionName)) {
72
80
  if (jsonMode) {
73
81
  outputErrorAsJson('NOT_RUNNING', 'Orchestrator is not running. Start it with: prlt orchestrator start', createMetadata('orchestrator attach', flags));
74
82
  return;
@@ -81,7 +89,7 @@ export default class OrchestratorAttach extends PromptCommand {
81
89
  }
82
90
  if (jsonMode) {
83
91
  outputSuccessAsJson({
84
- sessionId: ORCHESTRATOR_SESSION_NAME,
92
+ sessionId: sessionName,
85
93
  status: 'attaching',
86
94
  }, createMetadata('orchestrator attach', flags));
87
95
  return;
@@ -93,7 +101,27 @@ export default class OrchestratorAttach extends PromptCommand {
93
101
  this.log(styles.warning('--terminal has no effect without --new-tab. Ignoring.'));
94
102
  }
95
103
  this.log('');
96
- this.log(styles.info(`Attaching to orchestrator session: ${ORCHESTRATOR_SESSION_NAME}`));
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);
111
+ try {
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();
120
+ }
121
+ }
122
+ catch {
123
+ // Not in a workspace or DB not available - fall back to no control mode
124
+ }
97
125
  if (flags['new-tab']) {
98
126
  // Determine terminal app: explicit flag > auto-detect > error
99
127
  const terminalApp = flags.terminal ?? detectTerminalApp();
@@ -102,26 +130,28 @@ export default class OrchestratorAttach extends PromptCommand {
102
130
  this.log(styles.muted('Falling back to direct tmux attach in current terminal.'));
103
131
  this.log(styles.muted('Tip: Use --terminal <app> to specify your terminal (iTerm, Terminal, Ghostty).'));
104
132
  this.log('');
105
- this.attachInCurrentTerminal();
133
+ this.attachInCurrentTerminal(useControlMode, sessionName);
106
134
  return;
107
135
  }
108
- await this.openInNewTab(terminalApp);
136
+ await this.openInNewTab(terminalApp, useControlMode, sessionName);
109
137
  }
110
138
  else {
111
- this.attachInCurrentTerminal();
139
+ this.attachInCurrentTerminal(useControlMode, sessionName);
112
140
  }
113
141
  }
114
- attachInCurrentTerminal() {
142
+ attachInCurrentTerminal(useControlMode, sessionName) {
115
143
  try {
116
- execSync(`tmux attach -t "${ORCHESTRATOR_SESSION_NAME}"`, { stdio: 'inherit' });
144
+ const tmuxAttach = buildTmuxAttachCommand(useControlMode);
145
+ execSync(`${tmuxAttach} -t "${sessionName}"`, { stdio: 'inherit' });
117
146
  }
118
147
  catch {
119
- this.error(`Failed to attach to orchestrator session "${ORCHESTRATOR_SESSION_NAME}"`);
148
+ this.error(`Failed to attach to orchestrator session "${sessionName}"`);
120
149
  }
121
150
  }
122
- async openInNewTab(terminalApp) {
151
+ async openInNewTab(terminalApp, useControlMode, sessionName) {
123
152
  const title = 'Orchestrator';
124
- const attachCmd = `tmux attach -t "${ORCHESTRATOR_SESSION_NAME}"`;
153
+ const tmuxAttach = buildTmuxAttachCommand(useControlMode);
154
+ const attachCmd = `${tmuxAttach} -t "${sessionName}"`;
125
155
  const baseDir = path.join(os.homedir(), '.proletariat', 'scripts');
126
156
  fs.mkdirSync(baseDir, { recursive: true });
127
157
  const scriptPath = path.join(baseDir, `attach-orch-${Date.now()}.sh`);
@@ -130,7 +160,7 @@ export default class OrchestratorAttach extends PromptCommand {
130
160
  echo -ne "\\033]0;${title}\\007"
131
161
  echo -ne "\\033]1;${title}\\007"
132
162
 
133
- echo "Attaching to: ${ORCHESTRATOR_SESSION_NAME}"
163
+ echo "Attaching to: ${sessionName}"
134
164
  ${attachCmd}
135
165
 
136
166
  # Clean up
@@ -1,7 +1,7 @@
1
1
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
2
2
  import { shouldOutputJson } from '../../lib/prompt-json.js';
3
3
  import { getHostTmuxSessionNames } from '../../lib/execution/session-utils.js';
4
- import { ORCHESTRATOR_SESSION_NAME } from './start.js';
4
+ import { findRunningOrchestratorSessions } from './start.js';
5
5
  export default class Orchestrator extends PMOCommand {
6
6
  static description = 'Manage the orchestrator agent (start, attach, status, stop)';
7
7
  static examples = [
@@ -22,7 +22,7 @@ export default class Orchestrator extends PMOCommand {
22
22
  const jsonModeConfig = shouldOutputJson(flags) ? { flags, commandName: 'orchestrator' } : null;
23
23
  // Check if orchestrator is currently running to offer contextual options
24
24
  const hostSessions = getHostTmuxSessionNames();
25
- const isRunning = hostSessions.includes(ORCHESTRATOR_SESSION_NAME);
25
+ const isRunning = findRunningOrchestratorSessions(hostSessions).length > 0;
26
26
  // When running, show "Attach to running session" first since that's the likely intent
27
27
  const choices = isRunning
28
28
  ? [
@@ -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
  };
@@ -11,8 +11,26 @@ import { DEFAULT_EXECUTION_CONFIG, } from '../../lib/execution/types.js';
11
11
  import { runExecution } from '../../lib/execution/runners.js';
12
12
  import { getHostTmuxSessionNames } from '../../lib/execution/session-utils.js';
13
13
  import { ExecutionStorage } from '../../lib/execution/storage.js';
14
- import { loadExecutionConfig, getTerminalApp, promptTerminalPreference, getShell, promptShellPreference, hasTerminalPreference, hasShellPreference, } from '../../lib/execution/config.js';
15
- export const ORCHESTRATOR_SESSION_NAME = 'prlt-orchestrator-main';
14
+ import { loadExecutionConfig, getTerminalApp, getShell, detectShell, detectTerminalApp, } from '../../lib/execution/config.js';
15
+ /**
16
+ * Build orchestrator tmux session name.
17
+ * Default: 'prlt-orchestrator-main'
18
+ * With --name: 'prlt-orchestrator-{name}'
19
+ */
20
+ export function buildOrchestratorSessionName(name = 'main') {
21
+ const safeName = name
22
+ .replace(/[^a-zA-Z0-9._-]/g, '-')
23
+ .replace(/-+/g, '-')
24
+ .replace(/^-|-$/g, '');
25
+ return `prlt-orchestrator-${safeName || 'main'}`;
26
+ }
27
+ /**
28
+ * Find running orchestrator session(s) by prefix match.
29
+ * Returns all tmux session names that start with 'prlt-orchestrator-'.
30
+ */
31
+ export function findRunningOrchestratorSessions(hostSessions) {
32
+ return hostSessions.filter(s => s.startsWith('prlt-orchestrator-'));
33
+ }
16
34
  export default class OrchestratorStart extends PromptCommand {
17
35
  static description = 'Start the orchestrator agent in a tmux session';
18
36
  static examples = [
@@ -47,36 +65,50 @@ export default class OrchestratorStart extends PromptCommand {
47
65
  default: false,
48
66
  exclusive: ['skip-permissions'],
49
67
  }),
68
+ name: Flags.string({
69
+ char: 'n',
70
+ description: 'Name for the orchestrator session (default: main)',
71
+ }),
50
72
  background: Flags.boolean({
51
73
  char: 'b',
52
74
  description: 'Start detached (don\'t open terminal tab)',
53
75
  default: false,
76
+ exclusive: ['foreground'],
77
+ }),
78
+ foreground: Flags.boolean({
79
+ char: 'f',
80
+ description: 'Attach to the tmux session in the current terminal (blocking)',
81
+ default: false,
82
+ exclusive: ['background'],
54
83
  }),
55
84
  };
56
85
  async run() {
57
86
  const { flags } = await this.parse(OrchestratorStart);
58
87
  const jsonMode = shouldOutputJson(flags);
88
+ const orchestratorName = flags.name || 'main';
89
+ const sessionName = buildOrchestratorSessionName(orchestratorName);
59
90
  // Check if orchestrator is already running
60
91
  const hostSessions = getHostTmuxSessionNames();
61
- if (hostSessions.includes(ORCHESTRATOR_SESSION_NAME)) {
92
+ if (hostSessions.includes(sessionName)) {
62
93
  if (jsonMode) {
63
- outputErrorAsJson('ALREADY_RUNNING', `Orchestrator is already running (session: ${ORCHESTRATOR_SESSION_NAME}). Use "prlt orchestrator attach" to reattach.`, createMetadata('orchestrator start', flags));
94
+ outputErrorAsJson('ALREADY_RUNNING', `Orchestrator is already running (session: ${sessionName}). Use "prlt orchestrator attach${flags.name ? ` --name ${flags.name}` : ''}" to reattach.`, createMetadata('orchestrator start', flags));
64
95
  return;
65
96
  }
66
97
  this.log('');
67
- this.log(styles.warning(`Orchestrator is already running (session: ${ORCHESTRATOR_SESSION_NAME})`));
98
+ this.log(styles.warning(`Orchestrator is already running (session: ${sessionName})`));
68
99
  this.log('');
100
+ const attachArgs = flags.name ? ['--name', flags.name] : [];
69
101
  const { choice } = await this.prompt([{
70
102
  type: 'list',
71
103
  name: 'choice',
72
104
  message: 'What would you like to do?',
73
105
  choices: [
74
- { name: 'Attach to running orchestrator', value: 'attach', command: 'prlt orchestrator attach --json' },
106
+ { name: 'Attach to running orchestrator', value: 'attach', command: `prlt orchestrator attach${flags.name ? ` --name ${flags.name}` : ''} --json` },
75
107
  { name: 'Cancel', value: 'cancel' },
76
108
  ],
77
109
  }], jsonMode ? { flags, commandName: 'orchestrator start' } : null);
78
110
  if (choice === 'attach') {
79
- await this.config.runCommand('orchestrator:attach', []);
111
+ await this.config.runCommand('orchestrator:attach', attachArgs);
80
112
  }
81
113
  return;
82
114
  }
@@ -169,12 +201,12 @@ export default class OrchestratorStart extends PromptCommand {
169
201
  }
170
202
  }
171
203
  // Build execution context
172
- // Use ticketId='prlt', actionName='orchestrator', agentName='main'
173
- // so buildSessionName produces 'prlt-orchestrator-main'
204
+ // Use ticketId='prlt', actionName='orchestrator', agentName=orchestratorName
205
+ // so buildSessionName produces 'prlt-orchestrator-{name}'
174
206
  const context = {
175
207
  ticketId: 'prlt',
176
208
  ticketTitle: 'Orchestrator',
177
- agentName: 'main',
209
+ agentName: orchestratorName,
178
210
  agentDir: hqPath,
179
211
  worktreePath: hqPath,
180
212
  branch: 'main',
@@ -202,19 +234,54 @@ export default class OrchestratorStart extends PromptCommand {
202
234
  catch {
203
235
  // Ignore config loading errors, use defaults
204
236
  }
205
- // If no terminal preference saved, prompt for it (first run only)
206
- if (!jsonMode && db) {
207
- if (!hasTerminalPreference(db)) {
208
- executionConfig.terminal.app = await promptTerminalPreference(db);
237
+ // Auto-detect shell (never prompt for orchestrator)
238
+ if (db) {
239
+ executionConfig.shell = await getShell(db);
240
+ }
241
+ else {
242
+ executionConfig.shell = detectShell() || 'zsh';
243
+ }
244
+ // Determine display mode
245
+ let displayMode;
246
+ if (flags.background) {
247
+ displayMode = 'background';
248
+ }
249
+ else if (flags.foreground) {
250
+ displayMode = 'foreground';
251
+ }
252
+ else {
253
+ const displayChoices = [
254
+ { name: 'New terminal tab — opens attached to the tmux session', value: 'terminal', command: `prlt orchestrator start${flags.name ? ` --name ${flags.name}` : ''} --json` },
255
+ { name: 'Current session — attach to tmux here (foreground, blocking)', value: 'foreground', command: `prlt orchestrator start${flags.name ? ` --name ${flags.name}` : ''} --foreground --json` },
256
+ { name: 'Background — start detached, attach later', value: 'background', command: `prlt orchestrator start${flags.name ? ` --name ${flags.name}` : ''} --background --json` },
257
+ ];
258
+ const displayMessage = 'How do you want to view the orchestrator?';
259
+ if (jsonMode) {
260
+ outputPromptAsJson(buildPromptConfig('list', 'displayMode', displayMessage, displayChoices), createMetadata('orchestrator start', flags));
261
+ return;
209
262
  }
210
- else {
263
+ const { displayMode: selectedMode } = await this.prompt([{
264
+ type: 'list',
265
+ name: 'displayMode',
266
+ message: displayMessage,
267
+ choices: displayChoices,
268
+ }], jsonMode ? { flags, commandName: 'orchestrator start' } : null);
269
+ displayMode = selectedMode;
270
+ }
271
+ // For 'terminal' display mode, auto-detect terminal app
272
+ if (displayMode === 'terminal') {
273
+ if (db) {
211
274
  executionConfig.terminal.app = await getTerminalApp(db);
212
275
  }
213
- if (!hasShellPreference(db)) {
214
- executionConfig.shell = await promptShellPreference(db);
215
- }
216
276
  else {
217
- executionConfig.shell = await getShell(db);
277
+ const detected = detectTerminalApp();
278
+ if (detected) {
279
+ executionConfig.terminal.app = detected;
280
+ }
281
+ else {
282
+ // Can't detect terminal and no db to prompt — fall back to foreground
283
+ displayMode = 'foreground';
284
+ }
218
285
  }
219
286
  }
220
287
  // Show what we're doing
@@ -223,14 +290,17 @@ export default class OrchestratorStart extends PromptCommand {
223
290
  this.log(styles.muted(` Starting orchestrator...`));
224
291
  this.log(styles.muted(` Executor: ${selectedExecutor}`));
225
292
  this.log(styles.muted(` Permission mode: ${sandboxed ? 'sandboxed' : 'skip-permissions'}`));
293
+ this.log(styles.muted(` Display mode: ${displayMode}`));
226
294
  this.log(styles.muted(` Directory: ${hqPath}`));
295
+ if (orchestratorName !== 'main') {
296
+ this.log(styles.muted(` Name: ${orchestratorName}`));
297
+ }
227
298
  if (actionPrompt) {
228
299
  this.log(styles.muted(` Prompt: "${actionPrompt.substring(0, 60)}${actionPrompt.length > 60 ? '...' : ''}"`));
229
300
  }
230
301
  this.log('');
231
302
  }
232
303
  // Launch orchestrator
233
- const displayMode = flags.background ? 'background' : 'terminal';
234
304
  const result = await runExecution('host', context, selectedExecutor, executionConfig, {
235
305
  displayMode,
236
306
  });
@@ -246,7 +316,7 @@ export default class OrchestratorStart extends PromptCommand {
246
316
  environment: 'host',
247
317
  displayMode,
248
318
  sandboxed,
249
- sessionId: result.sessionId || ORCHESTRATOR_SESSION_NAME,
319
+ sessionId: result.sessionId || sessionName,
250
320
  });
251
321
  }
252
322
  catch {
@@ -255,17 +325,18 @@ export default class OrchestratorStart extends PromptCommand {
255
325
  }
256
326
  if (jsonMode) {
257
327
  outputSuccessAsJson({
258
- sessionId: result.sessionId || ORCHESTRATOR_SESSION_NAME,
328
+ sessionId: result.sessionId || sessionName,
259
329
  executor: selectedExecutor,
260
330
  sandboxed,
261
331
  displayMode,
262
332
  directory: hqPath,
333
+ name: orchestratorName,
263
334
  }, createMetadata('orchestrator start', flags));
264
335
  }
265
- if (flags.background) {
336
+ if (displayMode === 'background') {
266
337
  this.log(styles.success(`Orchestrator started in background`));
267
- this.log(styles.muted(` Session: ${result.sessionId || ORCHESTRATOR_SESSION_NAME}`));
268
- this.log(styles.muted(` Attach with: prlt orchestrator attach`));
338
+ this.log(styles.muted(` Session: ${result.sessionId || sessionName}`));
339
+ this.log(styles.muted(` Attach with: prlt orchestrator attach${flags.name ? ` --name ${flags.name}` : ''}`));
269
340
  }
270
341
  else {
271
342
  this.log(styles.success(`Orchestrator started`));
@@ -3,6 +3,7 @@ export default class OrchestratorStatus extends PromptCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
7
  peek: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
8
  lines: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
8
9
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -4,7 +4,7 @@ import { machineOutputFlags } from '../../lib/pmo/index.js';
4
4
  import { shouldOutputJson, outputSuccessAsJson, createMetadata, } from '../../lib/prompt-json.js';
5
5
  import { styles } from '../../lib/styles.js';
6
6
  import { getHostTmuxSessionNames, captureTmuxPane } from '../../lib/execution/session-utils.js';
7
- import { ORCHESTRATOR_SESSION_NAME } from './start.js';
7
+ import { buildOrchestratorSessionName } from './start.js';
8
8
  export default class OrchestratorStatus extends PromptCommand {
9
9
  static description = 'Check if the orchestrator is running';
10
10
  static examples = [
@@ -14,6 +14,10 @@ export default class OrchestratorStatus extends PromptCommand {
14
14
  ];
15
15
  static flags = {
16
16
  ...machineOutputFlags,
17
+ name: Flags.string({
18
+ char: 'n',
19
+ description: 'Name of the orchestrator session to check (default: main)',
20
+ }),
17
21
  peek: Flags.boolean({
18
22
  description: 'Show recent output from the orchestrator',
19
23
  default: false,
@@ -26,16 +30,17 @@ export default class OrchestratorStatus extends PromptCommand {
26
30
  async run() {
27
31
  const { flags } = await this.parse(OrchestratorStatus);
28
32
  const jsonMode = shouldOutputJson(flags);
33
+ const sessionName = buildOrchestratorSessionName(flags.name || 'main');
29
34
  const hostSessions = getHostTmuxSessionNames();
30
- const isRunning = hostSessions.includes(ORCHESTRATOR_SESSION_NAME);
35
+ const isRunning = hostSessions.includes(sessionName);
31
36
  let recentOutput = null;
32
37
  if (isRunning && flags.peek) {
33
- recentOutput = captureTmuxPane(ORCHESTRATOR_SESSION_NAME, flags.lines);
38
+ recentOutput = captureTmuxPane(sessionName, flags.lines);
34
39
  }
35
40
  if (jsonMode) {
36
41
  outputSuccessAsJson({
37
42
  running: isRunning,
38
- sessionId: isRunning ? ORCHESTRATOR_SESSION_NAME : null,
43
+ sessionId: isRunning ? sessionName : null,
39
44
  ...(recentOutput !== null && { recentOutput }),
40
45
  }, createMetadata('orchestrator status', flags));
41
46
  return;
@@ -43,7 +48,7 @@ export default class OrchestratorStatus extends PromptCommand {
43
48
  this.log('');
44
49
  if (isRunning) {
45
50
  this.log(styles.success(`Orchestrator is running`));
46
- this.log(styles.muted(` Session: ${ORCHESTRATOR_SESSION_NAME}`));
51
+ this.log(styles.muted(` Session: ${sessionName}`));
47
52
  this.log(styles.muted(` Attach: prlt orchestrator attach`));
48
53
  this.log(styles.muted(` Poke: prlt session poke orchestrator "message"`));
49
54
  if (recentOutput) {
@@ -3,6 +3,7 @@ export default class OrchestratorStop extends PromptCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
7
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
8
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
9
  machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
@@ -10,7 +10,7 @@ import { shouldOutputJson, outputErrorAsJson, outputSuccessAsJson, createMetadat
10
10
  import { styles } from '../../lib/styles.js';
11
11
  import { getHostTmuxSessionNames } from '../../lib/execution/session-utils.js';
12
12
  import { ExecutionStorage } from '../../lib/execution/storage.js';
13
- import { ORCHESTRATOR_SESSION_NAME } from './start.js';
13
+ import { buildOrchestratorSessionName } from './start.js';
14
14
  export default class OrchestratorStop extends PromptCommand {
15
15
  static description = 'Stop the running orchestrator';
16
16
  static examples = [
@@ -19,6 +19,10 @@ export default class OrchestratorStop extends PromptCommand {
19
19
  ];
20
20
  static flags = {
21
21
  ...machineOutputFlags,
22
+ name: Flags.string({
23
+ char: 'n',
24
+ description: 'Name of the orchestrator session to stop (default: main)',
25
+ }),
22
26
  force: Flags.boolean({
23
27
  char: 'f',
24
28
  description: 'Skip confirmation',
@@ -28,9 +32,10 @@ export default class OrchestratorStop extends PromptCommand {
28
32
  async run() {
29
33
  const { flags } = await this.parse(OrchestratorStop);
30
34
  const jsonMode = shouldOutputJson(flags);
35
+ const sessionName = buildOrchestratorSessionName(flags.name || 'main');
31
36
  // Check if orchestrator session exists
32
37
  const hostSessions = getHostTmuxSessionNames();
33
- if (!hostSessions.includes(ORCHESTRATOR_SESSION_NAME)) {
38
+ if (!hostSessions.includes(sessionName)) {
34
39
  if (jsonMode) {
35
40
  outputErrorAsJson('NOT_RUNNING', 'Orchestrator is not running.', createMetadata('orchestrator stop', flags));
36
41
  return;
@@ -58,7 +63,7 @@ export default class OrchestratorStop extends PromptCommand {
58
63
  }
59
64
  // Kill the tmux session
60
65
  try {
61
- execSync(`tmux kill-session -t "${ORCHESTRATOR_SESSION_NAME}"`, { stdio: 'pipe' });
66
+ execSync(`tmux kill-session -t "${sessionName}"`, { stdio: 'pipe' });
62
67
  }
63
68
  catch (error) {
64
69
  if (jsonMode) {
@@ -92,7 +97,7 @@ export default class OrchestratorStop extends PromptCommand {
92
97
  }
93
98
  if (jsonMode) {
94
99
  outputSuccessAsJson({
95
- sessionId: ORCHESTRATOR_SESSION_NAME,
100
+ sessionId: sessionName,
96
101
  status: 'stopped',
97
102
  }, createMetadata('orchestrator stop', flags));
98
103
  return;
@@ -6,7 +6,8 @@ import * as os from 'node:os';
6
6
  import Database from 'better-sqlite3';
7
7
  import { styles } from '../../lib/styles.js';
8
8
  import { getWorkspaceInfo } from '../../lib/agents/commands.js';
9
- import { ExecutionStorage } from '../../lib/execution/index.js';
9
+ import { ExecutionStorage, loadExecutionConfig, shouldUseControlMode, buildTmuxAttachCommand } from '../../lib/execution/index.js';
10
+ import { detectTerminalApp } from '../orchestrator/attach.js';
10
11
  import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findContainerSessionsByPrefix, findSessionForExecution, } from '../../lib/execution/session-utils.js';
11
12
  import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
12
13
  import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
@@ -106,12 +107,32 @@ export default class SessionAttach extends PMOCommand {
106
107
  // Attach to the session
107
108
  this.log('');
108
109
  this.log(styles.info(`Attaching to session: ${selectedSession.sessionId}`));
110
+ // Determine if we should use tmux control mode (-u -CC) for iTerm
111
+ let useControlMode = false;
112
+ try {
113
+ const workspaceInfo = getWorkspaceInfo();
114
+ const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
115
+ const db = new Database(dbPath);
116
+ try {
117
+ const config = loadExecutionConfig(db);
118
+ const termApp = detectTerminalApp();
119
+ if (termApp === 'iTerm') {
120
+ useControlMode = shouldUseControlMode('iTerm', config.tmux.controlMode);
121
+ }
122
+ }
123
+ finally {
124
+ db.close();
125
+ }
126
+ }
127
+ catch {
128
+ // Not in a workspace or DB not available - fall back to no control mode
129
+ }
109
130
  // Default to new tab unless --current-terminal is specified
110
131
  if (flags['current-terminal']) {
111
- await this.attachInCurrentTerminal(selectedSession);
132
+ await this.attachInCurrentTerminal(selectedSession, useControlMode);
112
133
  }
113
134
  else {
114
- await this.attachInNewTab(selectedSession, flags.terminal);
135
+ await this.attachInNewTab(selectedSession, flags.terminal, useControlMode);
115
136
  }
116
137
  }
117
138
  /**
@@ -247,13 +268,14 @@ export default class SessionAttach extends PMOCommand {
247
268
  /**
248
269
  * Attach to session in current terminal
249
270
  */
250
- async attachInCurrentTerminal(session) {
271
+ async attachInCurrentTerminal(session, useControlMode) {
251
272
  try {
273
+ const tmuxAttach = buildTmuxAttachCommand(useControlMode, session.type === 'container');
252
274
  if (session.type === 'container' && session.containerId) {
253
- execSync(`docker exec -it ${session.containerId} tmux attach -t "${session.sessionId}"`, { stdio: 'inherit' });
275
+ execSync(`docker exec -it ${session.containerId} ${tmuxAttach} -t "${session.sessionId}"`, { stdio: 'inherit' });
254
276
  }
255
277
  else {
256
- execSync(`tmux attach -t "${session.sessionId}"`, { stdio: 'inherit' });
278
+ execSync(`${tmuxAttach} -t "${session.sessionId}"`, { stdio: 'inherit' });
257
279
  }
258
280
  }
259
281
  catch {
@@ -263,7 +285,7 @@ export default class SessionAttach extends PMOCommand {
263
285
  /**
264
286
  * Attach to session in a new terminal tab
265
287
  */
266
- async attachInNewTab(session, terminalApp) {
288
+ async attachInNewTab(session, terminalApp, useControlMode) {
267
289
  // Build a readable title for the tab
268
290
  const title = `${session.ticketId} (${session.agentName})`;
269
291
  // Create a script that sets tab title and attaches to tmux
@@ -271,9 +293,10 @@ export default class SessionAttach extends PMOCommand {
271
293
  fs.mkdirSync(baseDir, { recursive: true });
272
294
  const scriptPath = path.join(baseDir, `attach-${Date.now()}.sh`);
273
295
  // Different attach command for container vs host sessions
296
+ const tmuxAttach = buildTmuxAttachCommand(useControlMode, session.type === 'container');
274
297
  const attachCmd = session.type === 'container' && session.containerId
275
- ? `docker exec -it ${session.containerId} tmux -u attach -t "${session.sessionId}"`
276
- : `tmux attach -t "${session.sessionId}"`;
298
+ ? `docker exec -it ${session.containerId} ${tmuxAttach} -t "${session.sessionId}"`
299
+ : `${tmuxAttach} -t "${session.sessionId}"`;
277
300
  const script = `#!/bin/bash
278
301
  # Set terminal tab title
279
302
  echo -ne "\\033]0;${title}\\007"