@proletariat/cli 0.3.47 → 0.3.49

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 (74) 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/claude/index.js +21 -21
  10. package/dist/commands/claude/open.js +1 -1
  11. package/dist/commands/commit.js +10 -8
  12. package/dist/commands/config/index.js +4 -5
  13. package/dist/commands/execution/config.d.ts +2 -2
  14. package/dist/commands/execution/config.js +18 -18
  15. package/dist/commands/execution/list.js +2 -2
  16. package/dist/commands/execution/view.js +2 -2
  17. package/dist/commands/init.js +9 -1
  18. package/dist/commands/orchestrator/attach.js +64 -14
  19. package/dist/commands/orchestrator/start.d.ts +5 -5
  20. package/dist/commands/orchestrator/start.js +45 -35
  21. package/dist/commands/orchestrator/status.js +64 -23
  22. package/dist/commands/orchestrator/stop.js +44 -12
  23. package/dist/commands/qa/index.js +12 -12
  24. package/dist/commands/session/attach.js +23 -0
  25. package/dist/commands/session/poke.js +1 -1
  26. package/dist/commands/staff/add.js +1 -1
  27. package/dist/commands/work/index.js +4 -0
  28. package/dist/commands/work/linear.d.ts +24 -0
  29. package/dist/commands/work/linear.js +218 -0
  30. package/dist/commands/work/revise.js +8 -8
  31. package/dist/commands/work/spawn.js +29 -20
  32. package/dist/commands/work/start.js +22 -12
  33. package/dist/commands/work/watch.js +3 -3
  34. package/dist/hooks/init.js +8 -0
  35. package/dist/lib/agents/index.js +2 -2
  36. package/dist/lib/caffeinate.d.ts +64 -0
  37. package/dist/lib/caffeinate.js +146 -0
  38. package/dist/lib/database/drizzle-schema.d.ts +7 -7
  39. package/dist/lib/database/drizzle-schema.js +1 -1
  40. package/dist/lib/execution/codex-adapter.d.ts +96 -0
  41. package/dist/lib/execution/codex-adapter.js +148 -0
  42. package/dist/lib/execution/config.d.ts +6 -6
  43. package/dist/lib/execution/config.js +17 -10
  44. package/dist/lib/execution/devcontainer.d.ts +3 -3
  45. package/dist/lib/execution/devcontainer.js +3 -3
  46. package/dist/lib/execution/index.d.ts +1 -0
  47. package/dist/lib/execution/index.js +1 -0
  48. package/dist/lib/execution/runners.d.ts +2 -2
  49. package/dist/lib/execution/runners.js +69 -26
  50. package/dist/lib/execution/spawner.js +3 -3
  51. package/dist/lib/execution/storage.d.ts +2 -2
  52. package/dist/lib/execution/storage.js +3 -3
  53. package/dist/lib/execution/types.d.ts +2 -2
  54. package/dist/lib/execution/types.js +1 -1
  55. package/dist/lib/external-issues/index.d.ts +1 -1
  56. package/dist/lib/external-issues/index.js +1 -1
  57. package/dist/lib/external-issues/linear.d.ts +43 -0
  58. package/dist/lib/external-issues/linear.js +261 -0
  59. package/dist/lib/external-issues/types.d.ts +67 -0
  60. package/dist/lib/external-issues/types.js +41 -0
  61. package/dist/lib/init/index.d.ts +4 -0
  62. package/dist/lib/init/index.js +11 -1
  63. package/dist/lib/machine-config.d.ts +1 -0
  64. package/dist/lib/machine-config.js +6 -3
  65. package/dist/lib/pmo/schema.d.ts +1 -1
  66. package/dist/lib/pmo/schema.js +1 -1
  67. package/dist/lib/pmo/storage/actions.js +3 -3
  68. package/dist/lib/pmo/storage/base.js +116 -6
  69. package/dist/lib/pmo/storage/epics.js +1 -1
  70. package/dist/lib/pmo/storage/tickets.js +2 -2
  71. package/dist/lib/pmo/storage/types.d.ts +2 -1
  72. package/dist/lib/repos/index.js +1 -1
  73. package/oclif.manifest.json +3052 -2721
  74. package/package.json +1 -1
@@ -295,7 +295,7 @@ Clean up your tmux session when done.`,
295
295
  }
296
296
  }
297
297
  // Resolve permission mode (default to danger for QA since it's in a container)
298
- const sandboxed = await this.resolvePermissionMode(flags, jsonMode, jsonModeConfig, environment, displayMode);
298
+ const permissionMode = await this.resolvePermissionMode(flags, jsonMode, jsonModeConfig, environment, displayMode);
299
299
  if (jsonMode && !flags['permission-mode']) {
300
300
  db.close();
301
301
  return;
@@ -359,14 +359,14 @@ Clean up your tmux session when done.`,
359
359
  executor: 'claude-code',
360
360
  environment,
361
361
  displayMode,
362
- sandboxed,
362
+ permissionMode,
363
363
  branch: 'main',
364
364
  });
365
365
  // Update ticket assignee
366
366
  await storage.updateTicket(ticket.id, { assignee: agentName });
367
367
  // Load execution config
368
368
  const executionConfig = loadExecutionConfig(db);
369
- executionConfig.sandboxed = sandboxed;
369
+ executionConfig.permissionMode = permissionMode;
370
370
  executionConfig.outputMode = 'interactive';
371
371
  // For terminal mode, ensure terminal preference is set
372
372
  if (displayMode === 'terminal' && !jsonMode) {
@@ -390,7 +390,7 @@ Clean up your tmux session when done.`,
390
390
  this.log(styles.muted(` Work ID: ${execution.id}`));
391
391
  this.log(styles.muted(` Environment: ${environment === 'devcontainer' ? '🐳' : '💻'} ${environment}`));
392
392
  this.log(styles.muted(` Display: ${displayMode}${flags.watch ? ' (watch mode)' : ''}`));
393
- this.log(styles.muted(` Permissions: ${sandboxed ? '🔒 safe' : '⚠️ danger'}`));
393
+ this.log(styles.muted(` Permissions: ${permissionMode === 'safe' ? '🔒 safe' : '⚠️ danger'}`));
394
394
  this.log('');
395
395
  // Run execution
396
396
  this.log(styles.muted('Starting QA agent...'));
@@ -457,7 +457,7 @@ Clean up your tmux session when done.`,
457
457
  return;
458
458
  }
459
459
  // Resolve permission mode
460
- const sandboxed = await this.resolvePermissionMode(flags, jsonMode, jsonModeConfig, environment, displayMode);
460
+ const permissionMode = await this.resolvePermissionMode(flags, jsonMode, jsonModeConfig, environment, displayMode);
461
461
  if (jsonMode && !flags['permission-mode'])
462
462
  return;
463
463
  const sessionName = `qa-explore-${Date.now().toString(36)}`;
@@ -501,7 +501,7 @@ Clean up your tmux session when done.`,
501
501
  };
502
502
  // Load execution config
503
503
  const executionConfig = { ...DEFAULT_EXECUTION_CONFIG };
504
- executionConfig.sandboxed = sandboxed;
504
+ executionConfig.permissionMode = permissionMode;
505
505
  executionConfig.outputMode = 'interactive';
506
506
  // For terminal mode, prompt for terminal preference
507
507
  if (displayMode === 'terminal' && !jsonMode) {
@@ -535,7 +535,7 @@ Clean up your tmux session when done.`,
535
535
  this.log(styles.muted(` Directory: ${workDir}`));
536
536
  this.log(styles.muted(` Environment: ${environment === 'devcontainer' ? '🐳' : '💻'} ${environment}`));
537
537
  this.log(styles.muted(` Display: ${displayMode}${flags.watch ? ' (watch mode)' : ''}`));
538
- this.log(styles.muted(` Permissions: ${sandboxed ? '🔒 safe' : '⚠️ danger'}`));
538
+ this.log(styles.muted(` Permissions: ${permissionMode === 'safe' ? '🔒 safe' : '⚠️ danger'}`));
539
539
  this.log('');
540
540
  // Run execution
541
541
  this.log(styles.muted('Starting QA agent...'));
@@ -575,8 +575,8 @@ Clean up your tmux session when done.`,
575
575
  }
576
576
  const hasProjectDevcontainer = hasDevcontainerConfig(workDir);
577
577
  const devcontainerLabel = hasProjectDevcontainer
578
- ? '🐳 devcontainer (uses project config, sandboxed)'
579
- : '🐳 devcontainer (uses catch-all container, sandboxed)';
578
+ ? '🐳 devcontainer (uses project config, isolated)'
579
+ : '🐳 devcontainer (uses catch-all container, isolated)';
580
580
  if (jsonMode) {
581
581
  await this.prompt([
582
582
  {
@@ -680,7 +680,7 @@ Clean up your tmux session when done.`,
680
680
  */
681
681
  async resolvePermissionMode(flags, jsonMode, jsonModeConfig, environment, displayMode) {
682
682
  if (flags['permission-mode']) {
683
- return flags['permission-mode'] === 'safe';
683
+ return (flags['permission-mode'] || 'safe');
684
684
  }
685
685
  const containerNote = environment === 'devcontainer' ? ' (container provides isolation)' : '';
686
686
  const { permissionMode } = await this.prompt([
@@ -696,8 +696,8 @@ Clean up your tmux session when done.`,
696
696
  },
697
697
  ], jsonModeConfig);
698
698
  if (jsonMode)
699
- return true; // unreachable
700
- return permissionMode === 'safe';
699
+ return 'safe'; // unreachable
700
+ return permissionMode;
701
701
  }
702
702
  /**
703
703
  * Set up catch-all devcontainer for directories without one.
@@ -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
@@ -264,7 +264,7 @@ export default class Add extends PromptCommand {
264
264
  this.log(chalk.blue(` From theme: ${theme?.display_name || themeId}`));
265
265
  }
266
266
  if (!flags['no-container']) {
267
- this.log(chalk.blue(' Devcontainer config created for sandboxed execution'));
267
+ this.log(chalk.blue(' Devcontainer config created for isolated execution'));
268
268
  }
269
269
  }
270
270
  catch (error) {
@@ -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,218 @@
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, getLinearIssueByIdentifier, buildLinearIssueChoiceCommand, buildLinearTicketDescription, buildLinearMetadata, buildLinearSpawnContextMessage, } from '../../lib/external-issues/linear.js';
6
+ import { getLinearApiKey, loadLinearConfig } from '../../lib/linear/index.js';
7
+ function buildWorkStartArgs(options) {
8
+ const args = [options.ticketId, '--project', options.projectId, '--ephemeral'];
9
+ if (options.executor)
10
+ args.push('--executor', options.executor);
11
+ if (options.display)
12
+ args.push('--display', options.display);
13
+ if (options.runOnHost)
14
+ args.push('--run-on-host');
15
+ if (options.skipPermissions)
16
+ args.push('--skip-permissions');
17
+ if (options.createPr)
18
+ args.push('--create-pr');
19
+ if (options.action)
20
+ args.push('--action', options.action);
21
+ if (options.message)
22
+ args.push('--message', options.message);
23
+ if (options.json)
24
+ args.push('--json');
25
+ if (options.machine)
26
+ args.push('--machine');
27
+ if (options.yes)
28
+ args.push('--yes');
29
+ return args;
30
+ }
31
+ export default class WorkLinear extends PMOCommand {
32
+ static description = 'List/select Linear issues and spawn work using the existing work-start flow';
33
+ static examples = [
34
+ '<%= config.bin %> <%= command.id %> --team ENG',
35
+ '<%= config.bin %> <%= command.id %> --team ENG --issue ENG-123',
36
+ '<%= config.bin %> <%= command.id %> --team ENG --issue ENG-123 --yes --skip-permissions --display terminal',
37
+ ];
38
+ static flags = {
39
+ ...pmoBaseFlags,
40
+ team: Flags.string({
41
+ description: 'Linear team key (fallback: PRLT_LINEAR_TEAM)',
42
+ }),
43
+ issue: Flags.string({
44
+ description: 'Linear issue identifier (for example: ENG-123)',
45
+ }),
46
+ limit: Flags.integer({
47
+ char: 'l',
48
+ description: 'Maximum number of Linear issues to fetch',
49
+ default: 20,
50
+ min: 1,
51
+ max: 100,
52
+ }),
53
+ executor: Flags.string({
54
+ char: 'e',
55
+ description: 'Override executor',
56
+ options: ['claude-code', 'codex', 'aider', 'custom'],
57
+ }),
58
+ display: Flags.string({
59
+ char: 'd',
60
+ description: 'Display mode',
61
+ options: ['terminal', 'background', 'foreground'],
62
+ }),
63
+ action: Flags.string({
64
+ char: 'A',
65
+ description: 'Action to run in work start (default: implement)',
66
+ default: 'implement',
67
+ }),
68
+ message: Flags.string({
69
+ description: 'Additional instructions appended to spawn context',
70
+ }),
71
+ 'run-on-host': Flags.boolean({
72
+ description: 'Run on host even if devcontainer exists',
73
+ default: false,
74
+ }),
75
+ 'skip-permissions': Flags.boolean({
76
+ description: 'Skip permission prompts (danger mode)',
77
+ default: false,
78
+ }),
79
+ 'create-pr': Flags.boolean({
80
+ description: 'Create PR when work is ready',
81
+ default: false,
82
+ }),
83
+ yes: Flags.boolean({
84
+ char: 'y',
85
+ description: 'Skip confirmation prompts in downstream work start',
86
+ default: false,
87
+ }),
88
+ };
89
+ async findLinkedTicket(projectId, envelope) {
90
+ const tickets = await this.storage.listTickets(projectId);
91
+ return tickets.find((ticket) => {
92
+ const source = ticket.metadata?.external_source;
93
+ const key = ticket.metadata?.external_key;
94
+ const id = ticket.metadata?.external_id;
95
+ return source === 'linear'
96
+ && (key === envelope.source.externalKey || id === envelope.source.externalId);
97
+ });
98
+ }
99
+ async createOrUpdateLinkedTicket(projectId, envelope) {
100
+ const existing = await this.findLinkedTicket(projectId, envelope);
101
+ const description = buildLinearTicketDescription(envelope);
102
+ const metadata = buildLinearMetadata(envelope);
103
+ if (existing) {
104
+ const updated = await this.storage.updateTicket(existing.id, {
105
+ title: envelope.title,
106
+ description,
107
+ priority: envelope.priority ?? undefined,
108
+ category: envelope.category ?? undefined,
109
+ labels: envelope.labels,
110
+ metadata: {
111
+ ...existing.metadata,
112
+ ...metadata,
113
+ },
114
+ });
115
+ return updated;
116
+ }
117
+ return this.storage.createTicket(projectId, {
118
+ title: envelope.title,
119
+ description,
120
+ priority: envelope.priority ?? undefined,
121
+ category: envelope.category ?? undefined,
122
+ labels: envelope.labels,
123
+ metadata,
124
+ });
125
+ }
126
+ async execute() {
127
+ const { flags } = await this.parse(WorkLinear);
128
+ const jsonMode = shouldOutputJson(flags);
129
+ const db = this.storage.getDatabase();
130
+ const linearConfig = loadLinearConfig(db);
131
+ const projectId = await this.requireProject({
132
+ jsonMode: {
133
+ flags,
134
+ commandName: 'work linear',
135
+ baseCommand: 'prlt work linear',
136
+ },
137
+ });
138
+ const apiKey = getLinearApiKey(db) || undefined;
139
+ const team = flags.team || linearConfig?.defaultTeamKey || process.env.PRLT_LINEAR_TEAM;
140
+ let issues;
141
+ try {
142
+ issues = await listLinearIssues({
143
+ apiKey,
144
+ team,
145
+ }, { limit: flags.limit });
146
+ }
147
+ catch (error) {
148
+ if (error instanceof ExternalIssueAdapterError) {
149
+ return this.handleError(error.code, error.message, { jsonMode, commandName: 'work linear', flags });
150
+ }
151
+ const msg = error instanceof Error ? error.message : 'Failed to fetch Linear issues.';
152
+ return this.handleError('LINEAR_REQUEST_FAILED', msg, { jsonMode, commandName: 'work linear', flags });
153
+ }
154
+ if (issues.length === 0) {
155
+ return this.handleError('NO_LINEAR_ISSUES', 'No active Linear issues found for the configured team.', { jsonMode, commandName: 'work linear', flags });
156
+ }
157
+ let selectedIssue = issues.find(issue => issue.source.externalKey === flags.issue);
158
+ if (!selectedIssue && flags.issue) {
159
+ try {
160
+ selectedIssue = await getLinearIssueByIdentifier({
161
+ apiKey,
162
+ team,
163
+ }, flags.issue) ?? undefined;
164
+ }
165
+ catch (error) {
166
+ if (error instanceof ExternalIssueAdapterError) {
167
+ return this.handleError(error.code, error.message, { jsonMode, commandName: 'work linear', flags });
168
+ }
169
+ const msg = error instanceof Error ? error.message : 'Failed to fetch Linear issue.';
170
+ return this.handleError('LINEAR_REQUEST_FAILED', msg, { jsonMode, commandName: 'work linear', flags });
171
+ }
172
+ if (!selectedIssue) {
173
+ return this.handleError('LINEAR_ISSUE_NOT_FOUND', `Linear issue "${flags.issue}" was not found.`, { jsonMode, commandName: 'work linear', flags });
174
+ }
175
+ }
176
+ if (!selectedIssue) {
177
+ const selectedKey = await this.selectFromList({
178
+ message: 'Select Linear issue to spawn:',
179
+ items: issues,
180
+ getName: (issue) => {
181
+ const priority = issue.priority || 'None';
182
+ return `[${priority}] ${issue.source.externalKey} - ${issue.title}`;
183
+ },
184
+ getValue: issue => issue.source.externalKey,
185
+ getCommand: issue => {
186
+ const base = buildLinearIssueChoiceCommand(issue.source.externalKey, projectId);
187
+ return team ? `${base} --team ${team}` : base;
188
+ },
189
+ jsonMode: jsonMode ? { flags, commandName: 'work linear' } : null,
190
+ });
191
+ if (!selectedKey) {
192
+ return;
193
+ }
194
+ selectedIssue = issues.find(issue => issue.source.externalKey === selectedKey);
195
+ if (!selectedIssue) {
196
+ return this.handleError('LINEAR_ISSUE_NOT_FOUND', `Linear issue "${selectedKey}" was not found.`, { jsonMode, commandName: 'work linear', flags });
197
+ }
198
+ }
199
+ const ticket = await this.createOrUpdateLinkedTicket(projectId, selectedIssue);
200
+ await autoExportToBoard(this.pmoPath, this.storage);
201
+ const contextMessage = buildLinearSpawnContextMessage(selectedIssue, flags.message);
202
+ const args = buildWorkStartArgs({
203
+ ticketId: ticket.id,
204
+ projectId,
205
+ executor: flags.executor,
206
+ display: flags.display,
207
+ runOnHost: flags['run-on-host'],
208
+ skipPermissions: flags['skip-permissions'],
209
+ createPr: flags['create-pr'],
210
+ action: flags.action,
211
+ message: contextMessage,
212
+ json: flags.json,
213
+ machine: flags.machine,
214
+ yes: flags.yes,
215
+ });
216
+ await this.config.runCommand('work:start', args);
217
+ }
218
+ }
@@ -76,7 +76,7 @@ export default class WorkRevise extends PMOCommand {
76
76
  // Early Docker check
77
77
  if (!flags['run-on-host'] && !isDockerRunning()) {
78
78
  return handleError('DOCKER_NOT_RUNNING', 'Docker is not running.\n\n' +
79
- 'Docker is required for devcontainer execution (recommended for agent sandboxing).\n' +
79
+ 'Docker is required for devcontainer execution (recommended for agent isolation).\n' +
80
80
  'Please start Docker Desktop and try again.\n\n' +
81
81
  'Alternatively, use --run-on-host to run directly on your machine (bypasses sandbox).');
82
82
  }
@@ -220,7 +220,7 @@ export default class WorkRevise extends PMOCommand {
220
220
  const hasDevcontainer = hasDevcontainerConfig(agentDir);
221
221
  let environment = 'host';
222
222
  let displayMode = 'terminal';
223
- let sandboxed = false;
223
+ let permissionMode = 'danger';
224
224
  const reviseJsonModeConfig = jsonMode ? { flags: flags, commandName: 'work revise' } : null;
225
225
  if (hasDevcontainer && !flags['run-on-host']) {
226
226
  environment = 'devcontainer';
@@ -245,19 +245,19 @@ export default class WorkRevise extends PMOCommand {
245
245
  const executor = flags.executor || DEFAULT_EXECUTION_CONFIG.defaultExecutor;
246
246
  const executorName = getExecutorDisplayName(executor);
247
247
  // Permission mode
248
- const { permissionMode } = await this.prompt([
248
+ const { permissionMode: selectedPermMode } = await this.prompt([
249
249
  {
250
250
  type: 'list',
251
251
  name: 'permissionMode',
252
252
  message: `Permission mode for ${executorName}:`,
253
253
  choices: [
254
- { name: 'danger - Skip permission checks (faster for revisions)', value: 'danger', command: `prlt work revise ${ticketId} --json` },
255
- { name: 'safe - Requires approval for dangerous operations', value: 'safe', command: `prlt work revise ${ticketId} --json` },
254
+ { name: '⚠️ danger - Skip permission checks (faster for revisions)', value: 'danger', command: `prlt work revise ${ticketId} --json` },
255
+ { name: '🔒 safe - Requires approval for dangerous operations', value: 'safe', command: `prlt work revise ${ticketId} --json` },
256
256
  ],
257
257
  default: 'danger',
258
258
  },
259
259
  ], reviseJsonModeConfig);
260
- sandboxed = permissionMode === 'safe';
260
+ permissionMode = selectedPermMode;
261
261
  // Show execution info
262
262
  this.log('');
263
263
  this.log(styles.header(`Revising: ${ticket.id}: ${ticket.title}`));
@@ -292,7 +292,7 @@ export default class WorkRevise extends PMOCommand {
292
292
  executor,
293
293
  environment,
294
294
  displayMode,
295
- sandboxed,
295
+ permissionMode,
296
296
  branch,
297
297
  });
298
298
  this.log(styles.muted(` Work ID: ${execution.id}`));
@@ -327,7 +327,7 @@ export default class WorkRevise extends PMOCommand {
327
327
  executionConfig.shell = shell;
328
328
  }
329
329
  executionConfig.outputMode = 'interactive';
330
- executionConfig.sandboxed = sandboxed;
330
+ executionConfig.permissionMode = permissionMode;
331
331
  // Run execution
332
332
  this.log(styles.muted('Starting agent to address feedback...'));
333
333
  const sessionManager = (flags.session || 'tmux');
@@ -1198,7 +1198,7 @@ export default class WorkSpawn extends PMOCommand {
1198
1198
  }
1199
1199
  // Prompt for environment (devcontainer vs host) if devcontainer available and not already set
1200
1200
  if (hasDevcontainer && !batchRunOnHost && !batchDisplay) {
1201
- const devcontainerLabel = '🐳 devcontainer (sandboxed, recommended)';
1201
+ const devcontainerLabel = '🐳 devcontainer (isolated, recommended)';
1202
1202
  const envChoices = [
1203
1203
  { name: devcontainerLabel, value: 'devcontainer' },
1204
1204
  { name: '💻 host (runs directly on your machine)', value: 'host' },
@@ -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
@@ -805,10 +805,10 @@ export default class WorkStart extends PMOCommand {
805
805
  // Determine execution environment and display mode
806
806
  let environment = 'host';
807
807
  let displayMode = 'terminal';
808
- let sandboxed = false; // Whether --dangerously-skip-permissions is NOT used
808
+ let permissionMode = 'danger';
809
809
  if (hasDevcontainer && !flags.display && !flags['run-on-host']) {
810
810
  // Agent has devcontainer - prompt for environment choice
811
- const devcontainerLabel = '🐳 devcontainer (sandboxed, recommended)';
811
+ const devcontainerLabel = '🐳 devcontainer (isolated, recommended)';
812
812
  const envChoices = [
813
813
  { name: devcontainerLabel, value: 'devcontainer' },
814
814
  { name: '💻 host (runs directly on your machine)', value: 'host' },
@@ -1206,8 +1206,16 @@ 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
- sandboxed = flags['permission-mode'] === 'safe';
1214
+ permissionMode = (flags['permission-mode'] || 'danger');
1215
+ }
1216
+ else if (!actionModifiesCode) {
1217
+ // Non-code-modifying actions automatically use safe mode
1218
+ permissionMode = 'safe';
1211
1219
  }
1212
1220
  else {
1213
1221
  const containerNote = environment === 'devcontainer'
@@ -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' },
@@ -1233,7 +1241,7 @@ export default class WorkStart extends PMOCommand {
1233
1241
  when: (ctx) => !ctx.flags['permission-mode'],
1234
1242
  });
1235
1243
  const resolvedPermission = await permissionResolver.resolve();
1236
- sandboxed = resolvedPermission['permission-mode'] === 'safe';
1244
+ permissionMode = (resolvedPermission['permission-mode'] || 'danger');
1237
1245
  }
1238
1246
  // Prompt for PR creation when work is complete
1239
1247
  // Resolution order: explicit flags > workspace config default > interactive prompt
@@ -1304,7 +1312,7 @@ export default class WorkStart extends PMOCommand {
1304
1312
  this.log(styles.muted(` Environment: ${envIcon} ${environment}`));
1305
1313
  this.log(styles.muted(` Display: ${displayMode}`));
1306
1314
  // Permissions info
1307
- if (sandboxed) {
1315
+ if (permissionMode === 'safe') {
1308
1316
  this.log(styles.success(` Permissions: 🔒 safe`));
1309
1317
  }
1310
1318
  else {
@@ -1521,7 +1529,7 @@ export default class WorkStart extends PMOCommand {
1521
1529
  executor,
1522
1530
  environment,
1523
1531
  displayMode,
1524
- sandboxed,
1532
+ permissionMode,
1525
1533
  branch,
1526
1534
  });
1527
1535
  if (!jsonMode) {
@@ -1561,8 +1569,8 @@ export default class WorkStart extends PMOCommand {
1561
1569
  }
1562
1570
  // Set output mode from user selection
1563
1571
  executionConfig.outputMode = outputMode;
1564
- // Set sandboxed mode (determines whether --dangerously-skip-permissions is used)
1565
- executionConfig.sandboxed = sandboxed;
1572
+ // Set permission mode (determines whether --dangerously-skip-permissions is used)
1573
+ executionConfig.permissionMode = permissionMode;
1566
1574
  // Handle --focus flag: when set, bring terminal to foreground instead of opening in background
1567
1575
  if (flags.focus) {
1568
1576
  executionConfig.terminal.openInBackground = false;
@@ -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 permissionMode = flags['permission-mode'] || (!actionModifiesCode ? 'safe' : 'danger');
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
@@ -2023,14 +2033,14 @@ export default class WorkStart extends PMOCommand {
2023
2033
  executor,
2024
2034
  environment,
2025
2035
  displayMode,
2026
- sandboxed,
2036
+ permissionMode,
2027
2037
  branch,
2028
2038
  });
2029
2039
  // Note: Ticket status update moved to after successful spawn
2030
2040
  // Load execution config
2031
2041
  const executionConfig = loadExecutionConfig(db);
2032
2042
  executionConfig.outputMode = outputMode;
2033
- executionConfig.sandboxed = sandboxed;
2043
+ executionConfig.permissionMode = permissionMode;
2034
2044
  // Run execution
2035
2045
  this.log(styles.muted(` Starting ${ticket.id} → ${agentName}...`));
2036
2046
  const batchSessionManager = (flags.session || 'tmux');