@proletariat/cli 0.3.47 → 0.3.48

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 (48) hide show
  1. package/dist/commands/caffeinate/index.d.ts +10 -0
  2. package/dist/commands/caffeinate/index.js +64 -0
  3. package/dist/commands/caffeinate/start.d.ts +14 -0
  4. package/dist/commands/caffeinate/start.js +86 -0
  5. package/dist/commands/caffeinate/status.d.ts +10 -0
  6. package/dist/commands/caffeinate/status.js +55 -0
  7. package/dist/commands/caffeinate/stop.d.ts +10 -0
  8. package/dist/commands/caffeinate/stop.js +47 -0
  9. package/dist/commands/commit.js +10 -8
  10. package/dist/commands/config/index.js +2 -3
  11. package/dist/commands/init.js +9 -1
  12. package/dist/commands/orchestrator/attach.js +64 -14
  13. package/dist/commands/orchestrator/start.d.ts +4 -4
  14. package/dist/commands/orchestrator/start.js +26 -16
  15. package/dist/commands/orchestrator/status.js +64 -23
  16. package/dist/commands/orchestrator/stop.js +44 -12
  17. package/dist/commands/session/attach.js +23 -0
  18. package/dist/commands/session/poke.js +1 -1
  19. package/dist/commands/work/index.js +4 -0
  20. package/dist/commands/work/linear.d.ts +24 -0
  21. package/dist/commands/work/linear.js +195 -0
  22. package/dist/commands/work/spawn.js +28 -19
  23. package/dist/commands/work/start.js +12 -2
  24. package/dist/hooks/init.js +8 -0
  25. package/dist/lib/caffeinate.d.ts +64 -0
  26. package/dist/lib/caffeinate.js +146 -0
  27. package/dist/lib/execution/codex-adapter.d.ts +96 -0
  28. package/dist/lib/execution/codex-adapter.js +148 -0
  29. package/dist/lib/execution/index.d.ts +1 -0
  30. package/dist/lib/execution/index.js +1 -0
  31. package/dist/lib/execution/runners.js +50 -6
  32. package/dist/lib/external-issues/index.d.ts +1 -1
  33. package/dist/lib/external-issues/index.js +1 -1
  34. package/dist/lib/external-issues/linear.d.ts +37 -0
  35. package/dist/lib/external-issues/linear.js +198 -0
  36. package/dist/lib/external-issues/types.d.ts +67 -0
  37. package/dist/lib/external-issues/types.js +41 -0
  38. package/dist/lib/init/index.d.ts +4 -0
  39. package/dist/lib/init/index.js +11 -1
  40. package/dist/lib/machine-config.d.ts +1 -0
  41. package/dist/lib/machine-config.js +6 -3
  42. package/dist/lib/pmo/storage/actions.js +3 -3
  43. package/dist/lib/pmo/storage/base.js +85 -6
  44. package/dist/lib/pmo/storage/epics.js +1 -1
  45. package/dist/lib/pmo/storage/tickets.js +2 -2
  46. package/dist/lib/pmo/storage/types.d.ts +2 -1
  47. package/oclif.manifest.json +4363 -4037
  48. package/package.json +1 -1
@@ -4,7 +4,9 @@ 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 { buildOrchestratorSessionName } from './start.js';
7
+ import { findHQRoot } from '../../lib/workspace.js';
8
+ import { getHeadquartersNameFromPath } from '../../lib/machine-config.js';
9
+ import { buildOrchestratorSessionName, findRunningOrchestratorSessions } from './start.js';
8
10
  export default class OrchestratorStatus extends PromptCommand {
9
11
  static description = 'Check if the orchestrator is running';
10
12
  static examples = [
@@ -30,38 +32,77 @@ export default class OrchestratorStatus extends PromptCommand {
30
32
  async run() {
31
33
  const { flags } = await this.parse(OrchestratorStatus);
32
34
  const jsonMode = shouldOutputJson(flags);
33
- const sessionName = buildOrchestratorSessionName(flags.name || 'main');
34
35
  const hostSessions = getHostTmuxSessionNames();
35
- const isRunning = hostSessions.includes(sessionName);
36
- let recentOutput = null;
37
- if (isRunning && flags.peek) {
38
- recentOutput = captureTmuxPane(sessionName, flags.lines);
36
+ // Resolve session name: try HQ-scoped first, fall back to discovery
37
+ let sessionName;
38
+ const hqPath = findHQRoot(process.cwd());
39
+ if (hqPath) {
40
+ const hqName = getHeadquartersNameFromPath(hqPath);
41
+ sessionName = buildOrchestratorSessionName(hqName, flags.name || 'main');
39
42
  }
43
+ // If in HQ, check specifically for this HQ's session
44
+ if (sessionName) {
45
+ const isRunning = hostSessions.includes(sessionName);
46
+ let recentOutput = null;
47
+ if (isRunning && flags.peek) {
48
+ recentOutput = captureTmuxPane(sessionName, flags.lines);
49
+ }
50
+ if (jsonMode) {
51
+ outputSuccessAsJson({
52
+ running: isRunning,
53
+ sessionId: isRunning ? sessionName : null,
54
+ ...(recentOutput !== null && { recentOutput }),
55
+ }, createMetadata('orchestrator status', flags));
56
+ return;
57
+ }
58
+ this.log('');
59
+ if (isRunning) {
60
+ this.log(styles.success(`Orchestrator is running`));
61
+ this.log(styles.muted(` Session: ${sessionName}`));
62
+ this.log(styles.muted(` Attach: prlt orchestrator attach`));
63
+ this.log(styles.muted(` Poke: prlt session poke orchestrator "message"`));
64
+ if (recentOutput) {
65
+ this.log('');
66
+ this.log(styles.header('Recent output:'));
67
+ this.log(styles.muted('─'.repeat(60)));
68
+ this.log(recentOutput);
69
+ this.log(styles.muted('─'.repeat(60)));
70
+ }
71
+ }
72
+ else {
73
+ this.log(styles.muted('Orchestrator is not running.'));
74
+ this.log(styles.muted('Start it with: prlt orchestrator start'));
75
+ }
76
+ this.log('');
77
+ return;
78
+ }
79
+ // Not in HQ — discover all running orchestrator sessions
80
+ const runningSessions = findRunningOrchestratorSessions(hostSessions);
40
81
  if (jsonMode) {
41
82
  outputSuccessAsJson({
42
- running: isRunning,
43
- sessionId: isRunning ? sessionName : null,
44
- ...(recentOutput !== null && { recentOutput }),
83
+ running: runningSessions.length > 0,
84
+ sessions: runningSessions,
45
85
  }, createMetadata('orchestrator status', flags));
46
86
  return;
47
87
  }
48
88
  this.log('');
49
- if (isRunning) {
50
- this.log(styles.success(`Orchestrator is running`));
51
- this.log(styles.muted(` Session: ${sessionName}`));
52
- this.log(styles.muted(` Attach: prlt orchestrator attach`));
53
- this.log(styles.muted(` Poke: prlt session poke orchestrator "message"`));
54
- if (recentOutput) {
55
- this.log('');
56
- this.log(styles.header('Recent output:'));
57
- this.log(styles.muted('─'.repeat(60)));
58
- this.log(recentOutput);
59
- this.log(styles.muted('─'.repeat(60)));
60
- }
89
+ if (runningSessions.length === 0) {
90
+ this.log(styles.muted('No orchestrator sessions running.'));
91
+ this.log(styles.muted('Start one with: prlt orchestrator start'));
61
92
  }
62
93
  else {
63
- this.log(styles.muted('Orchestrator is not running.'));
64
- this.log(styles.muted('Start it with: prlt orchestrator start'));
94
+ this.log(styles.success(`${runningSessions.length} orchestrator session(s) running:`));
95
+ for (const s of runningSessions) {
96
+ this.log(styles.muted(` ${s}`));
97
+ if (flags.peek) {
98
+ const output = captureTmuxPane(s, flags.lines);
99
+ if (output) {
100
+ this.log(styles.muted('─'.repeat(60)));
101
+ this.log(output);
102
+ this.log(styles.muted('─'.repeat(60)));
103
+ }
104
+ }
105
+ }
65
106
  }
66
107
  this.log('');
67
108
  }
@@ -10,7 +10,8 @@ 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 { buildOrchestratorSessionName } from './start.js';
13
+ import { buildOrchestratorSessionName, findRunningOrchestratorSessions } from './start.js';
14
+ import { getHeadquartersNameFromPath } from '../../lib/machine-config.js';
14
15
  export default class OrchestratorStop extends PromptCommand {
15
16
  static description = 'Stop the running orchestrator';
16
17
  static examples = [
@@ -32,17 +33,49 @@ export default class OrchestratorStop extends PromptCommand {
32
33
  async run() {
33
34
  const { flags } = await this.parse(OrchestratorStop);
34
35
  const jsonMode = shouldOutputJson(flags);
35
- const sessionName = buildOrchestratorSessionName(flags.name || 'main');
36
- // Check if orchestrator session exists
37
36
  const hostSessions = getHostTmuxSessionNames();
38
- if (!hostSessions.includes(sessionName)) {
39
- if (jsonMode) {
40
- outputErrorAsJson('NOT_RUNNING', 'Orchestrator is not running.', createMetadata('orchestrator stop', flags));
37
+ // Resolve session name: try HQ-scoped first, fall back to discovery
38
+ let sessionName;
39
+ const hqPath = findHQRoot(process.cwd());
40
+ if (hqPath) {
41
+ const hqName = getHeadquartersNameFromPath(hqPath);
42
+ sessionName = buildOrchestratorSessionName(hqName, flags.name || 'main');
43
+ if (!hostSessions.includes(sessionName)) {
44
+ sessionName = undefined;
45
+ }
46
+ }
47
+ // If not in HQ or session not found, discover running orchestrator sessions
48
+ if (!sessionName) {
49
+ const runningSessions = findRunningOrchestratorSessions(hostSessions);
50
+ if (runningSessions.length === 0) {
51
+ if (jsonMode) {
52
+ outputErrorAsJson('NOT_RUNNING', 'Orchestrator is not running.', createMetadata('orchestrator stop', flags));
53
+ return;
54
+ }
55
+ this.log('');
56
+ this.log(styles.muted('Orchestrator is not running.'));
57
+ this.log('');
41
58
  return;
42
59
  }
43
- this.log('');
44
- this.log(styles.muted('Orchestrator is not running.'));
45
- this.log('');
60
+ else if (runningSessions.length === 1) {
61
+ sessionName = runningSessions[0];
62
+ }
63
+ else {
64
+ // Multiple sessions — let user pick
65
+ const { session } = await this.prompt([{
66
+ type: 'list',
67
+ name: 'session',
68
+ message: 'Multiple orchestrator sessions found. Select one to stop:',
69
+ choices: runningSessions.map(s => ({
70
+ name: s,
71
+ value: s,
72
+ command: `prlt orchestrator stop --name "${s}" --force --json`,
73
+ })),
74
+ }], jsonMode ? { flags, commandName: 'orchestrator stop' } : null);
75
+ sessionName = session;
76
+ }
77
+ }
78
+ if (!sessionName) {
46
79
  return;
47
80
  }
48
81
  // Confirm unless --force
@@ -50,7 +83,7 @@ export default class OrchestratorStop extends PromptCommand {
50
83
  const { confirmed } = await this.prompt([{
51
84
  type: 'list',
52
85
  name: 'confirmed',
53
- message: 'Stop the orchestrator?',
86
+ message: `Stop the orchestrator (${sessionName})?`,
54
87
  choices: [
55
88
  { name: 'Yes', value: true },
56
89
  { name: 'No', value: false },
@@ -72,8 +105,7 @@ export default class OrchestratorStop extends PromptCommand {
72
105
  }
73
106
  this.error(`Failed to stop orchestrator: ${error instanceof Error ? error.message : error}`);
74
107
  }
75
- // Update execution record to stopped
76
- const hqPath = findHQRoot(process.cwd());
108
+ // Update execution record to stopped (only if in HQ)
77
109
  if (hqPath) {
78
110
  const dbPath = path.join(hqPath, '.proletariat', 'workspace.db');
79
111
  if (fs.existsSync(dbPath)) {
@@ -270,6 +270,21 @@ export default class SessionAttach extends PMOCommand {
270
270
  */
271
271
  async attachInCurrentTerminal(session, useControlMode) {
272
272
  try {
273
+ // Set mouse mode based on attach type:
274
+ // - Plain terminal: mouse on (enables scroll in tmux; hold Shift/Option to bypass)
275
+ // - iTerm -CC: mouse off (iTerm handles scrolling natively)
276
+ const mouseMode = useControlMode ? 'off' : 'on';
277
+ try {
278
+ if (session.type === 'container' && session.containerId) {
279
+ execSync(`docker exec ${session.containerId} tmux set-option -t "${session.sessionId}" mouse ${mouseMode}`, { stdio: 'pipe' });
280
+ }
281
+ else {
282
+ execSync(`tmux set-option -t "${session.sessionId}" mouse ${mouseMode}`, { stdio: 'pipe' });
283
+ }
284
+ }
285
+ catch {
286
+ // Non-fatal: mouse mode is a convenience, don't block attach
287
+ }
273
288
  const tmuxAttach = buildTmuxAttachCommand(useControlMode, session.type === 'container');
274
289
  if (session.type === 'container' && session.containerId) {
275
290
  execSync(`docker exec -it ${session.containerId} ${tmuxAttach} -t "${session.sessionId}"`, { stdio: 'inherit' });
@@ -297,11 +312,19 @@ export default class SessionAttach extends PMOCommand {
297
312
  const attachCmd = session.type === 'container' && session.containerId
298
313
  ? `docker exec -it ${session.containerId} ${tmuxAttach} -t "${session.sessionId}"`
299
314
  : `${tmuxAttach} -t "${session.sessionId}"`;
315
+ // Set mouse mode based on attach type
316
+ const mouseMode = useControlMode ? 'off' : 'on';
317
+ const mouseCmd = session.type === 'container' && session.containerId
318
+ ? `docker exec ${session.containerId} tmux set-option -t "${session.sessionId}" mouse ${mouseMode} 2>/dev/null || true`
319
+ : `tmux set-option -t "${session.sessionId}" mouse ${mouseMode} 2>/dev/null || true`;
300
320
  const script = `#!/bin/bash
301
321
  # Set terminal tab title
302
322
  echo -ne "\\033]0;${title}\\007"
303
323
  echo -ne "\\033]1;${title}\\007"
304
324
 
325
+ # Set mouse mode before attaching
326
+ ${mouseCmd}
327
+
305
328
  echo "Attaching to: ${session.sessionId} (${session.type})"
306
329
  ${attachCmd}
307
330
 
@@ -176,7 +176,7 @@ export default class SessionPoke extends PMOCommand {
176
176
  * Resolve the tmux session for a specific execution record.
177
177
  */
178
178
  resolveSessionForExecution(exec, jsonMode, flags) {
179
- const isContainer = exec.environment === 'devcontainer';
179
+ const isContainer = !!exec.containerId;
180
180
  let actualSessionId = exec.sessionId;
181
181
  let containerId = isContainer ? exec.containerId : undefined;
182
182
  // If sessionId is NULL, try to discover it from tmux
@@ -24,6 +24,7 @@ export default class Work extends PMOCommand {
24
24
  const menuChoices = [
25
25
  { id: 'status', name: 'View work status (in-progress tickets)', command: `prlt work status -P ${projectId} --json` },
26
26
  { id: 'start', name: 'Start work (launch single agent)', command: `prlt work start -P ${projectId} --json` },
27
+ { id: 'linear', name: 'Spawn from Linear issue', command: `prlt work linear -P ${projectId} --json` },
27
28
  { id: 'resolve', name: 'Resolve questions (agent-assisted)', command: `prlt work resolve -P ${projectId} --json` },
28
29
  { id: 'spawn', name: 'Spawn work (batch by column)', command: `prlt work spawn -P ${projectId} --json` },
29
30
  { id: 'watch', name: 'Watch column (auto-spawn)', command: `prlt work watch -P ${projectId} --json` },
@@ -54,6 +55,9 @@ export default class Work extends PMOCommand {
54
55
  case 'start':
55
56
  await this.config.runCommand('work:start', projectArgs);
56
57
  break;
58
+ case 'linear':
59
+ await this.config.runCommand('work:linear', projectArgs);
60
+ break;
57
61
  case 'resolve':
58
62
  await this.config.runCommand('work:resolve', projectArgs);
59
63
  break;
@@ -0,0 +1,24 @@
1
+ import { PMOCommand } from '../../lib/pmo/index.js';
2
+ export default class WorkLinear extends PMOCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ team: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ issue: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
9
+ executor: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ display: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ action: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ message: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'run-on-host': import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ 'skip-permissions': import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ 'create-pr': import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
+ project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ };
21
+ private findLinkedTicket;
22
+ private createOrUpdateLinkedTicket;
23
+ execute(): Promise<void>;
24
+ }
@@ -0,0 +1,195 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { PMOCommand, pmoBaseFlags, autoExportToBoard, } from '../../lib/pmo/index.js';
3
+ import { shouldOutputJson, } from '../../lib/prompt-json.js';
4
+ import { ExternalIssueAdapterError, } from '../../lib/external-issues/types.js';
5
+ import { listLinearIssues, buildLinearIssueChoiceCommand, buildLinearTicketDescription, buildLinearMetadata, buildLinearSpawnContextMessage, } from '../../lib/external-issues/linear.js';
6
+ function buildWorkStartArgs(options) {
7
+ const args = [options.ticketId, '--project', options.projectId, '--ephemeral'];
8
+ if (options.executor)
9
+ args.push('--executor', options.executor);
10
+ if (options.display)
11
+ args.push('--display', options.display);
12
+ if (options.runOnHost)
13
+ args.push('--run-on-host');
14
+ if (options.skipPermissions)
15
+ args.push('--skip-permissions');
16
+ if (options.createPr)
17
+ args.push('--create-pr');
18
+ if (options.action)
19
+ args.push('--action', options.action);
20
+ if (options.message)
21
+ args.push('--message', options.message);
22
+ if (options.json)
23
+ args.push('--json');
24
+ if (options.machine)
25
+ args.push('--machine');
26
+ if (options.yes)
27
+ args.push('--yes');
28
+ return args;
29
+ }
30
+ export default class WorkLinear extends PMOCommand {
31
+ static description = 'List/select Linear issues and spawn work using the existing work-start flow';
32
+ static examples = [
33
+ '<%= config.bin %> <%= command.id %> --team ENG',
34
+ '<%= config.bin %> <%= command.id %> --team ENG --issue ENG-123',
35
+ '<%= config.bin %> <%= command.id %> --team ENG --issue ENG-123 --yes --skip-permissions --display terminal',
36
+ ];
37
+ static flags = {
38
+ ...pmoBaseFlags,
39
+ team: Flags.string({
40
+ description: 'Linear team key (fallback: PRLT_LINEAR_TEAM)',
41
+ }),
42
+ issue: Flags.string({
43
+ description: 'Linear issue identifier (for example: ENG-123)',
44
+ }),
45
+ limit: Flags.integer({
46
+ char: 'l',
47
+ description: 'Maximum number of Linear issues to fetch',
48
+ default: 20,
49
+ min: 1,
50
+ max: 100,
51
+ }),
52
+ executor: Flags.string({
53
+ char: 'e',
54
+ description: 'Override executor',
55
+ options: ['claude-code', 'codex', 'aider', 'custom'],
56
+ }),
57
+ display: Flags.string({
58
+ char: 'd',
59
+ description: 'Display mode',
60
+ options: ['terminal', 'background', 'foreground'],
61
+ }),
62
+ action: Flags.string({
63
+ char: 'A',
64
+ description: 'Action to run in work start (default: implement)',
65
+ default: 'implement',
66
+ }),
67
+ message: Flags.string({
68
+ description: 'Additional instructions appended to spawn context',
69
+ }),
70
+ 'run-on-host': Flags.boolean({
71
+ description: 'Run on host even if devcontainer exists',
72
+ default: false,
73
+ }),
74
+ 'skip-permissions': Flags.boolean({
75
+ description: 'Skip permission prompts (danger mode)',
76
+ default: false,
77
+ }),
78
+ 'create-pr': Flags.boolean({
79
+ description: 'Create PR when work is ready',
80
+ default: false,
81
+ }),
82
+ yes: Flags.boolean({
83
+ char: 'y',
84
+ description: 'Skip confirmation prompts in downstream work start',
85
+ default: false,
86
+ }),
87
+ };
88
+ async findLinkedTicket(projectId, envelope) {
89
+ const tickets = await this.storage.listTickets(projectId);
90
+ return tickets.find((ticket) => {
91
+ const source = ticket.metadata?.external_source;
92
+ const key = ticket.metadata?.external_key;
93
+ const id = ticket.metadata?.external_id;
94
+ return source === 'linear'
95
+ && (key === envelope.source.externalKey || id === envelope.source.externalId);
96
+ });
97
+ }
98
+ async createOrUpdateLinkedTicket(projectId, envelope) {
99
+ const existing = await this.findLinkedTicket(projectId, envelope);
100
+ const description = buildLinearTicketDescription(envelope);
101
+ const metadata = buildLinearMetadata(envelope);
102
+ if (existing) {
103
+ const updated = await this.storage.updateTicket(existing.id, {
104
+ title: envelope.title,
105
+ description,
106
+ priority: envelope.priority ?? undefined,
107
+ category: envelope.category ?? undefined,
108
+ labels: envelope.labels,
109
+ metadata: {
110
+ ...existing.metadata,
111
+ ...metadata,
112
+ },
113
+ });
114
+ return updated;
115
+ }
116
+ return this.storage.createTicket(projectId, {
117
+ title: envelope.title,
118
+ description,
119
+ priority: envelope.priority ?? undefined,
120
+ category: envelope.category ?? undefined,
121
+ labels: envelope.labels,
122
+ metadata,
123
+ });
124
+ }
125
+ async execute() {
126
+ const { flags } = await this.parse(WorkLinear);
127
+ const jsonMode = shouldOutputJson(flags);
128
+ const projectId = await this.requireProject({
129
+ jsonMode: {
130
+ flags,
131
+ commandName: 'work linear',
132
+ baseCommand: 'prlt work linear',
133
+ },
134
+ });
135
+ const team = flags.team || process.env.PRLT_LINEAR_TEAM;
136
+ let issues;
137
+ try {
138
+ issues = await listLinearIssues({
139
+ team,
140
+ }, { limit: flags.limit });
141
+ }
142
+ catch (error) {
143
+ if (error instanceof ExternalIssueAdapterError) {
144
+ return this.handleError(error.code, error.message, { jsonMode, commandName: 'work linear', flags });
145
+ }
146
+ const msg = error instanceof Error ? error.message : 'Failed to fetch Linear issues.';
147
+ return this.handleError('LINEAR_REQUEST_FAILED', msg, { jsonMode, commandName: 'work linear', flags });
148
+ }
149
+ if (issues.length === 0) {
150
+ return this.handleError('NO_LINEAR_ISSUES', 'No active Linear issues found for the configured team.', { jsonMode, commandName: 'work linear', flags });
151
+ }
152
+ let selectedIssue = issues.find(issue => issue.source.externalKey === flags.issue);
153
+ if (!selectedIssue) {
154
+ if (flags.issue) {
155
+ return this.handleError('LINEAR_ISSUE_NOT_FOUND', `Linear issue "${flags.issue}" was not found.`, { jsonMode, commandName: 'work linear', flags });
156
+ }
157
+ const selectedKey = await this.selectFromList({
158
+ message: 'Select Linear issue to spawn:',
159
+ items: issues,
160
+ getName: (issue) => {
161
+ const priority = issue.priority || 'None';
162
+ return `[${priority}] ${issue.source.externalKey} - ${issue.title}`;
163
+ },
164
+ getValue: issue => issue.source.externalKey,
165
+ getCommand: issue => buildLinearIssueChoiceCommand(issue.source.externalKey, projectId),
166
+ jsonMode: jsonMode ? { flags, commandName: 'work linear' } : null,
167
+ });
168
+ if (!selectedKey) {
169
+ return;
170
+ }
171
+ selectedIssue = issues.find(issue => issue.source.externalKey === selectedKey);
172
+ if (!selectedIssue) {
173
+ return this.handleError('LINEAR_ISSUE_NOT_FOUND', `Linear issue "${selectedKey}" was not found.`, { jsonMode, commandName: 'work linear', flags });
174
+ }
175
+ }
176
+ const ticket = await this.createOrUpdateLinkedTicket(projectId, selectedIssue);
177
+ await autoExportToBoard(this.pmoPath, this.storage);
178
+ const contextMessage = buildLinearSpawnContextMessage(selectedIssue, flags.message);
179
+ const args = buildWorkStartArgs({
180
+ ticketId: ticket.id,
181
+ projectId,
182
+ executor: flags.executor,
183
+ display: flags.display,
184
+ runOnHost: flags['run-on-host'],
185
+ skipPermissions: flags['skip-permissions'],
186
+ createPr: flags['create-pr'],
187
+ action: flags.action,
188
+ message: contextMessage,
189
+ json: flags.json,
190
+ machine: flags.machine,
191
+ yes: flags.yes,
192
+ });
193
+ await this.config.runCommand('work:start', args);
194
+ }
195
+ }
@@ -1357,26 +1357,35 @@ export default class WorkSpawn extends PMOCommand {
1357
1357
  batchOutput = 'interactive';
1358
1358
  }
1359
1359
  // Prompt for permissions mode if not explicitly set via --skip-permissions flag
1360
+ // Non-code-modifying actions (review, review-comment, groom) default to safe mode
1361
+ // to prevent agents from performing destructive operations like merging PRs
1362
+ const spawnActionModifiesCode = selectedActionDetails?.modifiesCode ?? true;
1360
1363
  if (!flags['skip-permissions']) {
1361
- // Use FlagResolver for permission mode
1362
- const permissionResolver = new FlagResolver({
1363
- commandName: 'work spawn',
1364
- baseCommand: 'prlt work spawn',
1365
- jsonMode,
1366
- flags: {},
1367
- });
1368
- permissionResolver.addPrompt({
1369
- flagName: 'permissionMode',
1370
- type: 'list',
1371
- message: `Permission mode for ${executorName}:`,
1372
- default: 'danger',
1373
- choices: () => [
1374
- { name: '⚠️ danger - Skip permission checks (faster, container provides isolation)', value: 'danger' },
1375
- { name: '🔒 safe - Requires approval for dangerous operations', value: 'safe' },
1376
- ],
1377
- });
1378
- const permissionResult = await permissionResolver.resolve();
1379
- batchPermissionMode = permissionResult.permissionMode;
1364
+ if (!spawnActionModifiesCode) {
1365
+ // Non-code-modifying actions automatically use safe mode
1366
+ batchPermissionMode = 'safe';
1367
+ }
1368
+ else {
1369
+ // Use FlagResolver for permission mode
1370
+ const permissionResolver = new FlagResolver({
1371
+ commandName: 'work spawn',
1372
+ baseCommand: 'prlt work spawn',
1373
+ jsonMode,
1374
+ flags: {},
1375
+ });
1376
+ permissionResolver.addPrompt({
1377
+ flagName: 'permissionMode',
1378
+ type: 'list',
1379
+ message: `Permission mode for ${executorName}:`,
1380
+ default: 'danger',
1381
+ choices: () => [
1382
+ { name: '⚠️ danger - Skip permission checks (faster, container provides isolation)', value: 'danger' },
1383
+ { name: '🔒 safe - Requires approval for dangerous operations', value: 'safe' },
1384
+ ],
1385
+ });
1386
+ const permissionResult = await permissionResolver.resolve();
1387
+ batchPermissionMode = permissionResult.permissionMode;
1388
+ }
1380
1389
  }
1381
1390
  // Prompt for PR creation if not provided AND action modifies code
1382
1391
  // Resolution order: explicit flags > workspace config default > interactive prompt
@@ -1206,9 +1206,17 @@ export default class WorkStart extends PMOCommand {
1206
1206
  }
1207
1207
  // Prompt for permissions mode (all environments)
1208
1208
  // Use FlagResolver to handle both JSON mode and interactive prompts consistently
1209
+ // Non-code-modifying actions (review, review-comment, groom) default to safe mode
1210
+ // to prevent agents from performing destructive operations like merging PRs
1211
+ const actionModifiesCode = context.modifiesCode !== false;
1212
+ const defaultPermissionMode = actionModifiesCode ? 'danger' : 'safe';
1209
1213
  if (flags['permission-mode']) {
1210
1214
  sandboxed = flags['permission-mode'] === 'safe';
1211
1215
  }
1216
+ else if (!actionModifiesCode) {
1217
+ // Non-code-modifying actions automatically use safe mode
1218
+ sandboxed = true;
1219
+ }
1212
1220
  else {
1213
1221
  const containerNote = environment === 'devcontainer'
1214
1222
  ? ' (container provides additional isolation)'
@@ -1225,7 +1233,7 @@ export default class WorkStart extends PMOCommand {
1225
1233
  flagName: 'permission-mode',
1226
1234
  type: 'list',
1227
1235
  message: `Permission mode for ${executorName}${containerNote}:`,
1228
- default: 'danger',
1236
+ default: defaultPermissionMode,
1229
1237
  choices: () => [
1230
1238
  { name: '⚠️ danger - Skip permission checks (faster, container provides isolation)', value: 'danger' },
1231
1239
  { name: '🔒 safe - Requires approval for dangerous operations', value: 'safe' },
@@ -1980,9 +1988,11 @@ export default class WorkStart extends PMOCommand {
1980
1988
  const hasDevcontainer = hasDevcontainerConfig(agentDir);
1981
1989
  const useDevcontainer = hasDevcontainer && !flags['run-on-host'];
1982
1990
  // Non-interactive defaults
1991
+ // Non-code-modifying actions default to safe mode to prevent destructive operations
1983
1992
  const environment = useDevcontainer ? 'devcontainer' : 'host';
1984
1993
  const displayMode = 'terminal';
1985
- const sandboxed = flags['permission-mode'] === 'safe';
1994
+ const actionModifiesCode = context.modifiesCode !== false;
1995
+ const sandboxed = flags['permission-mode'] === 'safe' || (!flags['permission-mode'] && !actionModifiesCode);
1986
1996
  const executor = flags.executor || DEFAULT_EXECUTION_CONFIG.defaultExecutor;
1987
1997
  const outputMode = 'interactive';
1988
1998
  // Handle git branch - only if action modifies code
@@ -18,6 +18,14 @@ const hook = async function ({ id, argv, config }) {
18
18
  if (process.env.OCLIF_COMPILATION || process.argv[1]?.includes('oclif')) {
19
19
  return;
20
20
  }
21
+ // Skip when in test environments that provide their own HQ
22
+ if (process.env.PRLT_HQ_PATH && process.env.PRLT_TEST_ENV) {
23
+ return;
24
+ }
25
+ // Skip init redirect when explicitly disabled (e.g., e2e test isolation)
26
+ if (process.env.PRLT_SKIP_INIT_REDIRECT === '1') {
27
+ return;
28
+ }
21
29
  // Skip when --help or --version flags are present - these should always be available
22
30
  // Check both process.argv (production CLI) and the oclif-provided argv
23
31
  // (programmatic invocation via @oclif/test runCommand)