@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.
- package/dist/commands/caffeinate/index.d.ts +10 -0
- package/dist/commands/caffeinate/index.js +64 -0
- package/dist/commands/caffeinate/start.d.ts +14 -0
- package/dist/commands/caffeinate/start.js +86 -0
- package/dist/commands/caffeinate/status.d.ts +10 -0
- package/dist/commands/caffeinate/status.js +55 -0
- package/dist/commands/caffeinate/stop.d.ts +10 -0
- package/dist/commands/caffeinate/stop.js +47 -0
- package/dist/commands/claude/index.js +21 -21
- package/dist/commands/claude/open.js +1 -1
- package/dist/commands/commit.js +10 -8
- package/dist/commands/config/index.js +4 -5
- package/dist/commands/execution/config.d.ts +2 -2
- package/dist/commands/execution/config.js +18 -18
- package/dist/commands/execution/list.js +2 -2
- package/dist/commands/execution/view.js +2 -2
- package/dist/commands/init.js +9 -1
- package/dist/commands/orchestrator/attach.js +64 -14
- package/dist/commands/orchestrator/start.d.ts +5 -5
- package/dist/commands/orchestrator/start.js +45 -35
- package/dist/commands/orchestrator/status.js +64 -23
- package/dist/commands/orchestrator/stop.js +44 -12
- package/dist/commands/qa/index.js +12 -12
- package/dist/commands/session/attach.js +23 -0
- package/dist/commands/session/poke.js +1 -1
- package/dist/commands/staff/add.js +1 -1
- package/dist/commands/work/index.js +4 -0
- package/dist/commands/work/linear.d.ts +24 -0
- package/dist/commands/work/linear.js +218 -0
- package/dist/commands/work/revise.js +8 -8
- package/dist/commands/work/spawn.js +29 -20
- package/dist/commands/work/start.js +22 -12
- package/dist/commands/work/watch.js +3 -3
- package/dist/hooks/init.js +8 -0
- package/dist/lib/agents/index.js +2 -2
- package/dist/lib/caffeinate.d.ts +64 -0
- package/dist/lib/caffeinate.js +146 -0
- package/dist/lib/database/drizzle-schema.d.ts +7 -7
- package/dist/lib/database/drizzle-schema.js +1 -1
- package/dist/lib/execution/codex-adapter.d.ts +96 -0
- package/dist/lib/execution/codex-adapter.js +148 -0
- package/dist/lib/execution/config.d.ts +6 -6
- package/dist/lib/execution/config.js +17 -10
- package/dist/lib/execution/devcontainer.d.ts +3 -3
- package/dist/lib/execution/devcontainer.js +3 -3
- package/dist/lib/execution/index.d.ts +1 -0
- package/dist/lib/execution/index.js +1 -0
- package/dist/lib/execution/runners.d.ts +2 -2
- package/dist/lib/execution/runners.js +69 -26
- package/dist/lib/execution/spawner.js +3 -3
- package/dist/lib/execution/storage.d.ts +2 -2
- package/dist/lib/execution/storage.js +3 -3
- package/dist/lib/execution/types.d.ts +2 -2
- package/dist/lib/execution/types.js +1 -1
- package/dist/lib/external-issues/index.d.ts +1 -1
- package/dist/lib/external-issues/index.js +1 -1
- package/dist/lib/external-issues/linear.d.ts +43 -0
- package/dist/lib/external-issues/linear.js +261 -0
- package/dist/lib/external-issues/types.d.ts +67 -0
- package/dist/lib/external-issues/types.js +41 -0
- package/dist/lib/init/index.d.ts +4 -0
- package/dist/lib/init/index.js +11 -1
- package/dist/lib/machine-config.d.ts +1 -0
- package/dist/lib/machine-config.js +6 -3
- package/dist/lib/pmo/schema.d.ts +1 -1
- package/dist/lib/pmo/schema.js +1 -1
- package/dist/lib/pmo/storage/actions.js +3 -3
- package/dist/lib/pmo/storage/base.js +116 -6
- package/dist/lib/pmo/storage/epics.js +1 -1
- package/dist/lib/pmo/storage/tickets.js +2 -2
- package/dist/lib/pmo/storage/types.d.ts +2 -1
- package/dist/lib/repos/index.js +1 -1
- package/oclif.manifest.json +3052 -2721
- 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
|
|
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
|
-
|
|
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.
|
|
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: ${
|
|
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
|
|
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.
|
|
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: ${
|
|
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,
|
|
579
|
-
: '🐳 devcontainer (uses catch-all container,
|
|
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']
|
|
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
|
|
700
|
-
return permissionMode
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
1565
|
-
executionConfig.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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');
|