@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.
- 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/commit.js +10 -8
- package/dist/commands/config/index.js +2 -3
- package/dist/commands/init.js +9 -1
- package/dist/commands/orchestrator/attach.js +64 -14
- package/dist/commands/orchestrator/start.d.ts +4 -4
- package/dist/commands/orchestrator/start.js +26 -16
- package/dist/commands/orchestrator/status.js +64 -23
- package/dist/commands/orchestrator/stop.js +44 -12
- package/dist/commands/session/attach.js +23 -0
- package/dist/commands/session/poke.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 +195 -0
- package/dist/commands/work/spawn.js +28 -19
- package/dist/commands/work/start.js +12 -2
- package/dist/hooks/init.js +8 -0
- package/dist/lib/caffeinate.d.ts +64 -0
- package/dist/lib/caffeinate.js +146 -0
- package/dist/lib/execution/codex-adapter.d.ts +96 -0
- package/dist/lib/execution/codex-adapter.js +148 -0
- package/dist/lib/execution/index.d.ts +1 -0
- package/dist/lib/execution/index.js +1 -0
- package/dist/lib/execution/runners.js +50 -6
- 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 +37 -0
- package/dist/lib/external-issues/linear.js +198 -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/storage/actions.js +3 -3
- package/dist/lib/pmo/storage/base.js +85 -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/oclif.manifest.json +4363 -4037
- 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 {
|
|
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
|
-
|
|
36
|
-
let
|
|
37
|
-
|
|
38
|
-
|
|
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:
|
|
43
|
-
|
|
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 (
|
|
50
|
-
this.log(styles.
|
|
51
|
-
this.log(styles.muted(
|
|
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.
|
|
64
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
|
@@ -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:
|
|
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
|
|
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
|
package/dist/hooks/init.js
CHANGED
|
@@ -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)
|