@proletariat/cli 0.3.45 → 0.3.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/validate-better-sqlite3.cjs +55 -0
- package/dist/commands/config/index.js +39 -1
- package/dist/commands/linear/auth.d.ts +14 -0
- package/dist/commands/linear/auth.js +211 -0
- package/dist/commands/linear/import.d.ts +21 -0
- package/dist/commands/linear/import.js +260 -0
- package/dist/commands/linear/status.d.ts +11 -0
- package/dist/commands/linear/status.js +88 -0
- package/dist/commands/linear/sync.d.ts +15 -0
- package/dist/commands/linear/sync.js +233 -0
- package/dist/commands/orchestrator/attach.d.ts +10 -1
- package/dist/commands/orchestrator/attach.js +102 -18
- package/dist/commands/orchestrator/index.js +22 -7
- package/dist/commands/orchestrator/start.d.ts +13 -1
- package/dist/commands/orchestrator/start.js +96 -25
- package/dist/commands/orchestrator/status.d.ts +1 -0
- package/dist/commands/orchestrator/status.js +10 -5
- package/dist/commands/orchestrator/stop.d.ts +1 -0
- package/dist/commands/orchestrator/stop.js +9 -4
- package/dist/commands/session/attach.js +32 -9
- package/dist/commands/ticket/link/duplicates.d.ts +15 -0
- package/dist/commands/ticket/link/duplicates.js +95 -0
- package/dist/commands/ticket/link/index.js +14 -0
- package/dist/commands/ticket/link/relates.d.ts +15 -0
- package/dist/commands/ticket/link/relates.js +95 -0
- package/dist/commands/work/index.js +4 -0
- package/dist/commands/work/review.d.ts +45 -0
- package/dist/commands/work/review.js +401 -0
- package/dist/commands/work/revise.js +4 -3
- package/dist/commands/work/spawn.d.ts +5 -0
- package/dist/commands/work/spawn.js +195 -14
- package/dist/commands/work/start.js +75 -19
- package/dist/hooks/init.js +18 -5
- package/dist/lib/database/native-validation.d.ts +21 -0
- package/dist/lib/database/native-validation.js +49 -0
- package/dist/lib/execution/config.d.ts +15 -0
- package/dist/lib/execution/config.js +54 -0
- package/dist/lib/execution/devcontainer.d.ts +6 -3
- package/dist/lib/execution/devcontainer.js +39 -12
- package/dist/lib/execution/runners.d.ts +28 -32
- package/dist/lib/execution/runners.js +353 -277
- package/dist/lib/execution/spawner.js +62 -5
- package/dist/lib/execution/types.d.ts +4 -0
- package/dist/lib/execution/types.js +3 -0
- package/dist/lib/external-issues/adapters.d.ts +26 -0
- package/dist/lib/external-issues/adapters.js +251 -0
- package/dist/lib/external-issues/index.d.ts +10 -0
- package/dist/lib/external-issues/index.js +14 -0
- package/dist/lib/external-issues/mapper.d.ts +21 -0
- package/dist/lib/external-issues/mapper.js +86 -0
- package/dist/lib/external-issues/types.d.ts +144 -0
- package/dist/lib/external-issues/types.js +26 -0
- package/dist/lib/external-issues/validation.d.ts +34 -0
- package/dist/lib/external-issues/validation.js +219 -0
- package/dist/lib/linear/client.d.ts +55 -0
- package/dist/lib/linear/client.js +254 -0
- package/dist/lib/linear/config.d.ts +37 -0
- package/dist/lib/linear/config.js +100 -0
- package/dist/lib/linear/index.d.ts +11 -0
- package/dist/lib/linear/index.js +10 -0
- package/dist/lib/linear/mapper.d.ts +67 -0
- package/dist/lib/linear/mapper.js +219 -0
- package/dist/lib/linear/sync.d.ts +37 -0
- package/dist/lib/linear/sync.js +89 -0
- package/dist/lib/linear/types.d.ts +139 -0
- package/dist/lib/linear/types.js +34 -0
- package/dist/lib/mcp/helpers.d.ts +8 -0
- package/dist/lib/mcp/helpers.js +10 -0
- package/dist/lib/mcp/tools/board.js +63 -11
- package/dist/lib/mcp/tools/work.js +36 -0
- package/dist/lib/pmo/schema.d.ts +2 -0
- package/dist/lib/pmo/schema.js +20 -0
- package/dist/lib/pmo/storage/base.js +92 -13
- package/dist/lib/pmo/storage/dependencies.js +15 -0
- package/dist/lib/prompt-json.d.ts +4 -0
- package/oclif.manifest.json +3205 -2537
- package/package.json +3 -2
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { autoExportToBoard, PMOCommand, pmoBaseFlags } from '../../../lib/pmo/index.js';
|
|
3
|
+
import { styles } from '../../../lib/styles.js';
|
|
4
|
+
import { shouldOutputJson, outputPromptAsJson, outputSuccessAsJson, outputErrorAsJson, createMetadata, buildPromptConfig, } from '../../../lib/prompt-json.js';
|
|
5
|
+
export default class TicketLinkRelates extends PMOCommand {
|
|
6
|
+
static description = 'Add a relates-to dependency between two tickets';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> <%= command.id %> TKT-001 TKT-002',
|
|
9
|
+
'<%= config.bin %> <%= command.id %> TKT-001',
|
|
10
|
+
'<%= config.bin %> <%= command.id %> TKT-001 --json',
|
|
11
|
+
];
|
|
12
|
+
static args = {
|
|
13
|
+
ticket: Args.string({
|
|
14
|
+
description: 'First ticket',
|
|
15
|
+
required: true,
|
|
16
|
+
}),
|
|
17
|
+
related: Args.string({
|
|
18
|
+
description: 'Second ticket (related)',
|
|
19
|
+
required: false,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
static flags = {
|
|
23
|
+
...pmoBaseFlags,
|
|
24
|
+
};
|
|
25
|
+
async execute() {
|
|
26
|
+
const { args, flags } = await this.parse(TicketLinkRelates);
|
|
27
|
+
const jsonMode = shouldOutputJson(flags);
|
|
28
|
+
const projectId = await this.requireProject();
|
|
29
|
+
const handleError = (code, message) => {
|
|
30
|
+
if (jsonMode) {
|
|
31
|
+
outputErrorAsJson(code, message, createMetadata('ticket link relates', flags));
|
|
32
|
+
this.exit(1);
|
|
33
|
+
}
|
|
34
|
+
this.error(message);
|
|
35
|
+
};
|
|
36
|
+
// Verify the source ticket exists
|
|
37
|
+
const ticket = await this.storage.getTicket(args.ticket);
|
|
38
|
+
if (!ticket) {
|
|
39
|
+
return handleError('TICKET_NOT_FOUND', `Ticket not found: ${args.ticket}`);
|
|
40
|
+
}
|
|
41
|
+
// If related ticket not provided, prompt for selection
|
|
42
|
+
if (!args.related) {
|
|
43
|
+
const tickets = await this.storage.listTickets(projectId);
|
|
44
|
+
const otherTickets = tickets.filter(t => t.id !== args.ticket);
|
|
45
|
+
if (otherTickets.length === 0) {
|
|
46
|
+
return handleError('NO_TICKETS', 'No other tickets to select as related.');
|
|
47
|
+
}
|
|
48
|
+
const projectFlag = flags.project ? ` -P ${flags.project}` : '';
|
|
49
|
+
const choices = otherTickets.map(t => ({
|
|
50
|
+
name: `${t.id} - ${t.title}`,
|
|
51
|
+
value: t.id,
|
|
52
|
+
command: `prlt ticket link relates ${args.ticket} ${t.id}${projectFlag} --json`,
|
|
53
|
+
}));
|
|
54
|
+
const message = `Select ticket to relate to ${args.ticket}:`;
|
|
55
|
+
if (jsonMode) {
|
|
56
|
+
outputPromptAsJson(buildPromptConfig('list', 'related', message, choices), createMetadata('ticket link relates', flags));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const { selected } = await this.prompt([{
|
|
60
|
+
type: 'list',
|
|
61
|
+
name: 'selected',
|
|
62
|
+
message,
|
|
63
|
+
choices,
|
|
64
|
+
}], null);
|
|
65
|
+
args.related = selected;
|
|
66
|
+
}
|
|
67
|
+
// Verify related ticket exists
|
|
68
|
+
const relatedTicket = await this.storage.getTicket(args.related);
|
|
69
|
+
if (!relatedTicket) {
|
|
70
|
+
return handleError('RELATED_NOT_FOUND', `Related ticket not found: ${args.related}`);
|
|
71
|
+
}
|
|
72
|
+
// Create the relates_to dependency
|
|
73
|
+
try {
|
|
74
|
+
await this.storage.createTicketDependency(args.ticket, args.related, 'relates_to');
|
|
75
|
+
await autoExportToBoard(this.pmoPath, this.storage, (msg) => this.log(styles.muted(msg)));
|
|
76
|
+
if (jsonMode) {
|
|
77
|
+
outputSuccessAsJson({
|
|
78
|
+
ticketId: args.ticket,
|
|
79
|
+
relatedTicketId: args.related,
|
|
80
|
+
type: 'relates_to',
|
|
81
|
+
}, createMetadata('ticket link relates', flags));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.log(styles.success(`\n${args.ticket} now relates to ${args.related}`));
|
|
85
|
+
this.log(styles.muted(` ${ticket.title}`));
|
|
86
|
+
this.log(styles.muted(` relates to: ${relatedTicket.title}`));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof Error && error.message.includes('already exists')) {
|
|
90
|
+
return handleError('ALREADY_EXISTS', 'Relates-to dependency already exists.');
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -27,6 +27,7 @@ export default class Work extends PMOCommand {
|
|
|
27
27
|
{ id: 'resolve', name: 'Resolve questions (agent-assisted)', command: `prlt work resolve -P ${projectId} --json` },
|
|
28
28
|
{ id: 'spawn', name: 'Spawn work (batch by column)', command: `prlt work spawn -P ${projectId} --json` },
|
|
29
29
|
{ id: 'watch', name: 'Watch column (auto-spawn)', command: `prlt work watch -P ${projectId} --json` },
|
|
30
|
+
{ id: 'review', name: 'Review pipeline (review → fix → re-review)', command: `prlt work review -P ${projectId} --json` },
|
|
30
31
|
{ id: 'ready', name: 'Mark work ready for review', command: `prlt work ready -P ${projectId} --json` },
|
|
31
32
|
{ id: 'complete', name: 'Mark work complete', command: `prlt work complete -P ${projectId} --json` },
|
|
32
33
|
{ id: 'cancel', name: 'Cancel', command: '' },
|
|
@@ -62,6 +63,9 @@ export default class Work extends PMOCommand {
|
|
|
62
63
|
case 'watch':
|
|
63
64
|
await this.config.runCommand('work:watch', projectArgs);
|
|
64
65
|
break;
|
|
66
|
+
case 'review':
|
|
67
|
+
await this.config.runCommand('work:review', projectArgs);
|
|
68
|
+
break;
|
|
65
69
|
case 'ready':
|
|
66
70
|
await this.config.runCommand('work:ready', projectArgs);
|
|
67
71
|
break;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { PMOCommand } from '../../lib/pmo/index.js';
|
|
2
|
+
export default class WorkReview extends PMOCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static args: {
|
|
6
|
+
ticketId: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
'max-cycles': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
auto: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
executor: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'run-on-host': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
'skip-permissions': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
display: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
session: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
'poll-interval': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
|
+
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
+
};
|
|
22
|
+
execute(): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Build args for `work:start` command.
|
|
25
|
+
*/
|
|
26
|
+
private buildStartArgs;
|
|
27
|
+
/**
|
|
28
|
+
* Run the fix phase: spawn review-fix agent and wait for completion.
|
|
29
|
+
*/
|
|
30
|
+
private runFixPhase;
|
|
31
|
+
/**
|
|
32
|
+
* Wait for the most recent execution on a ticket to complete.
|
|
33
|
+
* Polls the execution storage and checks tmux session existence.
|
|
34
|
+
*/
|
|
35
|
+
private waitForAgentCompletion;
|
|
36
|
+
/**
|
|
37
|
+
* Check if a tmux session exists.
|
|
38
|
+
*/
|
|
39
|
+
private checkTmuxSession;
|
|
40
|
+
/**
|
|
41
|
+
* Get the review verdict from PR feedback.
|
|
42
|
+
*/
|
|
43
|
+
private getReviewVerdict;
|
|
44
|
+
private sleep;
|
|
45
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
6
|
+
import { styles } from '../../lib/styles.js';
|
|
7
|
+
import { getWorkspaceInfo, } from '../../lib/agents/commands.js';
|
|
8
|
+
import { ExecutionStorage } from '../../lib/execution/storage.js';
|
|
9
|
+
import { shouldOutputJson, outputErrorAsJson, outputSuccessAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
10
|
+
import { isGHInstalled, isGHAuthenticated, getPRFeedback, getPRForBranch, } from '../../lib/pr/index.js';
|
|
11
|
+
/**
|
|
12
|
+
* Maximum poll duration before giving up (30 minutes).
|
|
13
|
+
*/
|
|
14
|
+
const MAX_POLL_DURATION_MS = 30 * 60 * 1000;
|
|
15
|
+
export default class WorkReview extends PMOCommand {
|
|
16
|
+
static description = 'Automated review-fix pipeline: review → fix → re-review until clean';
|
|
17
|
+
static examples = [
|
|
18
|
+
'<%= config.bin %> <%= command.id %> TKT-001',
|
|
19
|
+
'<%= config.bin %> <%= command.id %> TKT-001 --max-cycles 5',
|
|
20
|
+
'<%= config.bin %> <%= command.id %> TKT-001 --auto # Skip confirmations between cycles',
|
|
21
|
+
'<%= config.bin %> <%= command.id %> # Interactive mode',
|
|
22
|
+
];
|
|
23
|
+
static args = {
|
|
24
|
+
ticketId: Args.string({
|
|
25
|
+
description: 'Ticket ID to review',
|
|
26
|
+
required: false,
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
static flags = {
|
|
30
|
+
...pmoBaseFlags,
|
|
31
|
+
'max-cycles': Flags.integer({
|
|
32
|
+
description: 'Maximum review-fix cycles before stopping (default: 3)',
|
|
33
|
+
default: 3,
|
|
34
|
+
}),
|
|
35
|
+
auto: Flags.boolean({
|
|
36
|
+
description: 'Skip confirmations between cycles (fully automated)',
|
|
37
|
+
default: false,
|
|
38
|
+
}),
|
|
39
|
+
executor: Flags.string({
|
|
40
|
+
char: 'e',
|
|
41
|
+
description: 'Override executor',
|
|
42
|
+
options: ['claude-code', 'codex', 'aider', 'custom'],
|
|
43
|
+
}),
|
|
44
|
+
'run-on-host': Flags.boolean({
|
|
45
|
+
description: 'Run on host even if devcontainer exists (bypasses sandbox)',
|
|
46
|
+
default: false,
|
|
47
|
+
}),
|
|
48
|
+
'skip-permissions': Flags.boolean({
|
|
49
|
+
description: 'Skip permission checks for agents',
|
|
50
|
+
default: false,
|
|
51
|
+
}),
|
|
52
|
+
display: Flags.string({
|
|
53
|
+
char: 'd',
|
|
54
|
+
description: 'Display mode for agents',
|
|
55
|
+
options: ['foreground', 'terminal', 'background'],
|
|
56
|
+
default: 'background',
|
|
57
|
+
}),
|
|
58
|
+
session: Flags.string({
|
|
59
|
+
char: 's',
|
|
60
|
+
description: 'Session manager inside container',
|
|
61
|
+
options: ['tmux', 'direct'],
|
|
62
|
+
default: 'tmux',
|
|
63
|
+
}),
|
|
64
|
+
'poll-interval': Flags.integer({
|
|
65
|
+
description: 'Polling interval in seconds to check agent completion (default: 10)',
|
|
66
|
+
default: 10,
|
|
67
|
+
}),
|
|
68
|
+
force: Flags.boolean({
|
|
69
|
+
char: 'f',
|
|
70
|
+
description: 'Force spawn even if ticket has running executions',
|
|
71
|
+
default: false,
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
async execute() {
|
|
75
|
+
const { args, flags } = await this.parse(WorkReview);
|
|
76
|
+
const projectId = flags.project;
|
|
77
|
+
const jsonMode = shouldOutputJson(flags);
|
|
78
|
+
const handleError = (code, message) => {
|
|
79
|
+
if (jsonMode) {
|
|
80
|
+
outputErrorAsJson(code, message, createMetadata('work review', flags));
|
|
81
|
+
this.exit(1);
|
|
82
|
+
}
|
|
83
|
+
this.error(message);
|
|
84
|
+
};
|
|
85
|
+
// Check gh CLI
|
|
86
|
+
if (!isGHInstalled() || !isGHAuthenticated()) {
|
|
87
|
+
return handleError('GH_NOT_AVAILABLE', 'GitHub CLI (gh) is required for the review pipeline.\nRun: prlt gh login');
|
|
88
|
+
}
|
|
89
|
+
// Get workspace info
|
|
90
|
+
let workspaceInfo;
|
|
91
|
+
try {
|
|
92
|
+
workspaceInfo = getWorkspaceInfo();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return handleError('NOT_IN_WORKSPACE', 'Not in a workspace. Run "prlt init" first.');
|
|
96
|
+
}
|
|
97
|
+
const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
|
|
98
|
+
const db = new Database(dbPath);
|
|
99
|
+
const executionStorage = new ExecutionStorage(db);
|
|
100
|
+
try {
|
|
101
|
+
// Get ticketId
|
|
102
|
+
let ticketId = args.ticketId;
|
|
103
|
+
if (!ticketId) {
|
|
104
|
+
// Show tickets that are in progress or review (have PRs)
|
|
105
|
+
const allTickets = await this.storage.listTickets(projectId);
|
|
106
|
+
const reviewableTickets = allTickets.filter(t => {
|
|
107
|
+
const hasPR = t.metadata?.pr_url;
|
|
108
|
+
const isInProgress = t.status === 'in_progress' || t.status === 'done';
|
|
109
|
+
return hasPR || isInProgress;
|
|
110
|
+
});
|
|
111
|
+
if (reviewableTickets.length === 0) {
|
|
112
|
+
db.close();
|
|
113
|
+
return handleError('NO_TICKETS', 'No reviewable tickets found. Tickets need a PR to be reviewed.');
|
|
114
|
+
}
|
|
115
|
+
const selected = await this.selectFromList({
|
|
116
|
+
message: 'Select ticket to review:',
|
|
117
|
+
items: reviewableTickets,
|
|
118
|
+
getName: (t) => `${t.id} - ${t.title} ${t.metadata?.pr_url ? '(has PR)' : ''}`,
|
|
119
|
+
getValue: (t) => t.id,
|
|
120
|
+
getCommand: (t) => `prlt work review ${t.id} --json`,
|
|
121
|
+
jsonMode: jsonMode ? { flags, commandName: 'work review' } : null,
|
|
122
|
+
});
|
|
123
|
+
if (!selected) {
|
|
124
|
+
db.close();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
ticketId = selected;
|
|
128
|
+
}
|
|
129
|
+
// Get ticket
|
|
130
|
+
const ticket = await this.storage.getTicket(ticketId);
|
|
131
|
+
if (!ticket) {
|
|
132
|
+
db.close();
|
|
133
|
+
return handleError('TICKET_NOT_FOUND', `Ticket "${ticketId}" not found.`);
|
|
134
|
+
}
|
|
135
|
+
// Find the PR - either from ticket metadata or by branch
|
|
136
|
+
let prUrl = ticket.metadata?.pr_url;
|
|
137
|
+
if (!prUrl && ticket.branch) {
|
|
138
|
+
// Try to find PR by branch
|
|
139
|
+
const prInfo = getPRForBranch(ticket.branch);
|
|
140
|
+
if (prInfo) {
|
|
141
|
+
prUrl = prInfo.url;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!prUrl) {
|
|
145
|
+
db.close();
|
|
146
|
+
return handleError('NO_PR', `Ticket "${ticketId}" has no PR. Create one first with "prlt work ready ${ticketId} --pr".`);
|
|
147
|
+
}
|
|
148
|
+
const maxCycles = flags['max-cycles'];
|
|
149
|
+
const autoMode = flags.auto;
|
|
150
|
+
const pollInterval = (flags['poll-interval'] || 10) * 1000;
|
|
151
|
+
this.log('');
|
|
152
|
+
this.log(styles.header('Review Pipeline'));
|
|
153
|
+
this.log(styles.muted(` Ticket: ${ticket.id} - ${ticket.title}`));
|
|
154
|
+
this.log(styles.muted(` PR: ${prUrl}`));
|
|
155
|
+
this.log(styles.muted(` Max cycles: ${maxCycles}`));
|
|
156
|
+
this.log(styles.muted(` Mode: ${autoMode ? 'fully automated' : 'interactive'}`));
|
|
157
|
+
this.log('');
|
|
158
|
+
// Pipeline loop
|
|
159
|
+
for (let cycle = 1; cycle <= maxCycles; cycle++) {
|
|
160
|
+
this.log(styles.header(`Cycle ${cycle}/${maxCycles}: Review Phase`));
|
|
161
|
+
this.log('');
|
|
162
|
+
// === REVIEW PHASE ===
|
|
163
|
+
// Spawn a review agent
|
|
164
|
+
const reviewArgs = this.buildStartArgs(ticketId, flags, 'review');
|
|
165
|
+
this.log(styles.muted('Spawning review agent...'));
|
|
166
|
+
try {
|
|
167
|
+
await this.config.runCommand('work:start', reviewArgs);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
this.log(styles.error(`Failed to spawn review agent: ${error instanceof Error ? error.message : error}`));
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
// Wait for the review agent to complete
|
|
174
|
+
this.log(styles.muted('Waiting for review agent to complete...'));
|
|
175
|
+
const reviewCompleted = await this.waitForAgentCompletion(ticketId, executionStorage, pollInterval);
|
|
176
|
+
if (!reviewCompleted) {
|
|
177
|
+
this.log(styles.warning('Review agent did not complete within timeout. Stopping pipeline.'));
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
this.log(styles.success('Review agent completed.'));
|
|
181
|
+
this.log('');
|
|
182
|
+
// Check PR feedback
|
|
183
|
+
this.log(styles.muted('Checking review results...'));
|
|
184
|
+
const feedback = getPRFeedback(prUrl);
|
|
185
|
+
if (!feedback) {
|
|
186
|
+
this.log(styles.warning('Could not fetch PR feedback. Stopping pipeline.'));
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
const verdict = this.getReviewVerdict(feedback);
|
|
190
|
+
if (verdict === 'APPROVED') {
|
|
191
|
+
this.log(styles.success('PR APPROVED! No fixes needed.'));
|
|
192
|
+
this.log('');
|
|
193
|
+
if (jsonMode) {
|
|
194
|
+
outputSuccessAsJson({
|
|
195
|
+
ticketId: ticketId,
|
|
196
|
+
prUrl,
|
|
197
|
+
verdict: 'APPROVED',
|
|
198
|
+
cycles: cycle,
|
|
199
|
+
}, createMetadata('work review', flags));
|
|
200
|
+
}
|
|
201
|
+
db.close();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
this.log(styles.warning(`Review verdict: ${verdict}`));
|
|
205
|
+
this.log(styles.muted(` Reviews: ${feedback.reviews.length}`));
|
|
206
|
+
// Show review summary
|
|
207
|
+
for (const review of feedback.reviews) {
|
|
208
|
+
if (review.state === 'CHANGES_REQUESTED' || review.state === 'COMMENTED') {
|
|
209
|
+
this.log(styles.muted(` ${review.author}: ${review.state}`));
|
|
210
|
+
if (review.comments.length > 0) {
|
|
211
|
+
this.log(styles.muted(` ${review.comments.length} comment(s)`));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
this.log('');
|
|
216
|
+
// Check if we've reached max cycles
|
|
217
|
+
if (cycle === maxCycles) {
|
|
218
|
+
this.log(styles.warning(`Reached maximum cycles (${maxCycles}). Stopping pipeline.`));
|
|
219
|
+
this.log(styles.muted('Review feedback remains on the PR for manual follow-up.'));
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
// === FIX PHASE ===
|
|
223
|
+
if (!autoMode) {
|
|
224
|
+
const shouldFix = await this.selectFromList({
|
|
225
|
+
message: `Issues found in cycle ${cycle}. Auto-fix and re-review?`,
|
|
226
|
+
items: [
|
|
227
|
+
{ id: 'yes', name: 'Yes, spawn fix agent and re-review' },
|
|
228
|
+
{ id: 'fix-only', name: 'Fix only (no re-review)' },
|
|
229
|
+
{ id: 'no', name: 'No, stop pipeline' },
|
|
230
|
+
],
|
|
231
|
+
getName: (item) => item.name,
|
|
232
|
+
getValue: (item) => item.id,
|
|
233
|
+
getCommand: () => '',
|
|
234
|
+
jsonMode: jsonMode ? { flags, commandName: 'work review' } : null,
|
|
235
|
+
});
|
|
236
|
+
if (shouldFix === 'no' || !shouldFix) {
|
|
237
|
+
this.log(styles.muted('Pipeline stopped by user.'));
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
if (shouldFix === 'fix-only') {
|
|
241
|
+
// Spawn fix agent but don't re-review
|
|
242
|
+
this.log(styles.header(`Cycle ${cycle}/${maxCycles}: Fix Phase (final)`));
|
|
243
|
+
await this.runFixPhase(ticketId, flags, executionStorage, pollInterval);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
this.log(styles.header(`Cycle ${cycle}/${maxCycles}: Fix Phase`));
|
|
248
|
+
this.log('');
|
|
249
|
+
const fixCompleted = await this.runFixPhase(ticketId, flags, executionStorage, pollInterval);
|
|
250
|
+
if (!fixCompleted) {
|
|
251
|
+
this.log(styles.warning('Fix agent did not complete. Stopping pipeline.'));
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
this.log(styles.success('Fix agent completed. Proceeding to re-review...'));
|
|
255
|
+
this.log('');
|
|
256
|
+
// Small delay before re-review to let GitHub update
|
|
257
|
+
await this.sleep(3000);
|
|
258
|
+
}
|
|
259
|
+
// Final status
|
|
260
|
+
this.log('');
|
|
261
|
+
const finalFeedback = getPRFeedback(prUrl);
|
|
262
|
+
const finalVerdict = finalFeedback ? this.getReviewVerdict(finalFeedback) : 'UNKNOWN';
|
|
263
|
+
this.log(styles.header('Pipeline Complete'));
|
|
264
|
+
this.log(styles.muted(` Final status: ${finalVerdict}`));
|
|
265
|
+
this.log(styles.muted(` PR: ${prUrl}`));
|
|
266
|
+
this.log('');
|
|
267
|
+
if (jsonMode) {
|
|
268
|
+
outputSuccessAsJson({
|
|
269
|
+
ticketId: ticketId,
|
|
270
|
+
prUrl,
|
|
271
|
+
verdict: finalVerdict,
|
|
272
|
+
}, createMetadata('work review', flags));
|
|
273
|
+
}
|
|
274
|
+
db.close();
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
db.close();
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Build args for `work:start` command.
|
|
283
|
+
*/
|
|
284
|
+
buildStartArgs(ticketId, flags, action) {
|
|
285
|
+
const startArgs = [
|
|
286
|
+
ticketId,
|
|
287
|
+
'--action', action,
|
|
288
|
+
'--ephemeral',
|
|
289
|
+
'--display', flags.display || 'background',
|
|
290
|
+
'--yes',
|
|
291
|
+
];
|
|
292
|
+
if (flags.executor)
|
|
293
|
+
startArgs.push('--executor', flags.executor);
|
|
294
|
+
if (flags['run-on-host'])
|
|
295
|
+
startArgs.push('--run-on-host');
|
|
296
|
+
if (flags['skip-permissions'])
|
|
297
|
+
startArgs.push('--skip-permissions');
|
|
298
|
+
else
|
|
299
|
+
startArgs.push('--permission-mode', 'danger');
|
|
300
|
+
if (flags.session)
|
|
301
|
+
startArgs.push('--session', flags.session);
|
|
302
|
+
// Always pass --force for pipeline (multiple agents may work on same ticket across cycles)
|
|
303
|
+
startArgs.push('--force');
|
|
304
|
+
// Pass project if available
|
|
305
|
+
const projectId = flags.project;
|
|
306
|
+
if (projectId)
|
|
307
|
+
startArgs.push('--project', projectId);
|
|
308
|
+
return startArgs;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Run the fix phase: spawn review-fix agent and wait for completion.
|
|
312
|
+
*/
|
|
313
|
+
async runFixPhase(ticketId, flags, executionStorage, pollInterval) {
|
|
314
|
+
const fixArgs = this.buildStartArgs(ticketId, flags, 'review-fix');
|
|
315
|
+
this.log(styles.muted('Spawning fix agent...'));
|
|
316
|
+
try {
|
|
317
|
+
await this.config.runCommand('work:start', fixArgs);
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
this.log(styles.error(`Failed to spawn fix agent: ${error instanceof Error ? error.message : error}`));
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
this.log(styles.muted('Waiting for fix agent to complete...'));
|
|
324
|
+
return this.waitForAgentCompletion(ticketId, executionStorage, pollInterval);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Wait for the most recent execution on a ticket to complete.
|
|
328
|
+
* Polls the execution storage and checks tmux session existence.
|
|
329
|
+
*/
|
|
330
|
+
async waitForAgentCompletion(ticketId, executionStorage, pollInterval) {
|
|
331
|
+
const startTime = Date.now();
|
|
332
|
+
while (Date.now() - startTime < MAX_POLL_DURATION_MS) {
|
|
333
|
+
// Clean up stale executions first
|
|
334
|
+
executionStorage.cleanupStaleExecutions();
|
|
335
|
+
// Check if there's still a running execution for this ticket
|
|
336
|
+
const runningExec = executionStorage.getRunningExecution(ticketId);
|
|
337
|
+
if (!runningExec) {
|
|
338
|
+
// No running execution - agent has completed (or was never started)
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
// Check if the tmux session still exists
|
|
342
|
+
if (runningExec.sessionId) {
|
|
343
|
+
const sessionExists = this.checkTmuxSession(runningExec.sessionId, runningExec.environment, runningExec.containerId);
|
|
344
|
+
if (!sessionExists) {
|
|
345
|
+
// Session is gone - mark as completed
|
|
346
|
+
executionStorage.updateStatus(runningExec.id, 'completed');
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Log progress periodically
|
|
351
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
352
|
+
if (elapsed > 0 && elapsed % 60 === 0) {
|
|
353
|
+
this.log(styles.muted(` Still waiting... (${elapsed}s elapsed)`));
|
|
354
|
+
}
|
|
355
|
+
await this.sleep(pollInterval);
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Check if a tmux session exists.
|
|
361
|
+
*/
|
|
362
|
+
checkTmuxSession(sessionId, environment, containerId) {
|
|
363
|
+
try {
|
|
364
|
+
if (environment === 'devcontainer' && containerId) {
|
|
365
|
+
execSync(`docker exec ${containerId} tmux has-session -t "${sessionId}"`, { stdio: 'pipe' });
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
execSync(`tmux has-session -t "${sessionId}"`, { stdio: 'pipe' });
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Get the review verdict from PR feedback.
|
|
378
|
+
*/
|
|
379
|
+
getReviewVerdict(feedback) {
|
|
380
|
+
// Check review decision first (aggregated by GitHub)
|
|
381
|
+
if (feedback.reviewDecision) {
|
|
382
|
+
return feedback.reviewDecision;
|
|
383
|
+
}
|
|
384
|
+
// Check individual reviews - most recent takes precedence
|
|
385
|
+
const sortedReviews = [...feedback.reviews].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
386
|
+
for (const review of sortedReviews) {
|
|
387
|
+
if (review.state === 'APPROVED')
|
|
388
|
+
return 'APPROVED';
|
|
389
|
+
if (review.state === 'CHANGES_REQUESTED')
|
|
390
|
+
return 'CHANGES_REQUESTED';
|
|
391
|
+
}
|
|
392
|
+
// If there are any comments, treat as needing attention
|
|
393
|
+
if (feedback.reviews.some(r => r.comments.length > 0) || feedback.comments.length > 0) {
|
|
394
|
+
return 'COMMENTED';
|
|
395
|
+
}
|
|
396
|
+
return 'UNKNOWN';
|
|
397
|
+
}
|
|
398
|
+
sleep(ms) {
|
|
399
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
@@ -8,7 +8,7 @@ import { getWorkColumnSetting, findColumnByName } from '../../lib/pmo/utils.js';
|
|
|
8
8
|
import { styles } from '../../lib/styles.js';
|
|
9
9
|
import { getWorkspaceInfo, resolveAgentDir } from '../../lib/agents/commands.js';
|
|
10
10
|
import { DEFAULT_EXECUTION_CONFIG, } from '../../lib/execution/types.js';
|
|
11
|
-
import { runExecution, isDockerRunning, isDevcontainerCliInstalled } from '../../lib/execution/runners.js';
|
|
11
|
+
import { runExecution, isDockerRunning, isDevcontainerCliInstalled, getExecutorDisplayName } from '../../lib/execution/runners.js';
|
|
12
12
|
import { ExecutionStorage } from '../../lib/execution/storage.js';
|
|
13
13
|
import { loadExecutionConfig, getTerminalApp, getShell, hasTerminalPreference, hasShellPreference } from '../../lib/execution/config.js';
|
|
14
14
|
import { hasDevcontainerConfig } from '../../lib/execution/devcontainer.js';
|
|
@@ -242,12 +242,14 @@ export default class WorkRevise extends PMOCommand {
|
|
|
242
242
|
// Host environment: terminal/background are display modes
|
|
243
243
|
displayMode = flags.mode;
|
|
244
244
|
}
|
|
245
|
+
const executor = flags.executor || DEFAULT_EXECUTION_CONFIG.defaultExecutor;
|
|
246
|
+
const executorName = getExecutorDisplayName(executor);
|
|
245
247
|
// Permission mode
|
|
246
248
|
const { permissionMode } = await this.prompt([
|
|
247
249
|
{
|
|
248
250
|
type: 'list',
|
|
249
251
|
name: 'permissionMode',
|
|
250
|
-
message:
|
|
252
|
+
message: `Permission mode for ${executorName}:`,
|
|
251
253
|
choices: [
|
|
252
254
|
{ name: 'danger - Skip permission checks (faster for revisions)', value: 'danger', command: `prlt work revise ${ticketId} --json` },
|
|
253
255
|
{ name: 'safe - Requires approval for dangerous operations', value: 'safe', command: `prlt work revise ${ticketId} --json` },
|
|
@@ -256,7 +258,6 @@ export default class WorkRevise extends PMOCommand {
|
|
|
256
258
|
},
|
|
257
259
|
], reviseJsonModeConfig);
|
|
258
260
|
sandboxed = permissionMode === 'safe';
|
|
259
|
-
const executor = flags.executor || DEFAULT_EXECUTION_CONFIG.defaultExecutor;
|
|
260
261
|
// Show execution info
|
|
261
262
|
this.log('');
|
|
262
263
|
this.log(styles.header(`Revising: ${ticket.id}: ${ticket.title}`));
|
|
@@ -31,6 +31,11 @@ export default class WorkSpawn extends PMOCommand {
|
|
|
31
31
|
priority: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
32
32
|
epic: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
33
33
|
status: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
34
|
+
'from-linear': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
35
|
+
'linear-team': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
36
|
+
'linear-state': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
37
|
+
'linear-label': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
38
|
+
'linear-limit': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
34
39
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
35
40
|
machine: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
36
41
|
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|