@proletariat/cli 0.3.46 → 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/bin/validate-better-sqlite3.cjs +55 -0
- 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.d.ts +1 -0
- package/dist/commands/orchestrator/attach.js +104 -24
- package/dist/commands/orchestrator/index.js +2 -2
- package/dist/commands/orchestrator/start.d.ts +13 -1
- package/dist/commands/orchestrator/start.js +115 -34
- package/dist/commands/orchestrator/status.d.ts +1 -0
- package/dist/commands/orchestrator/status.js +68 -22
- package/dist/commands/orchestrator/stop.d.ts +1 -0
- package/dist/commands/orchestrator/stop.js +50 -13
- package/dist/commands/session/attach.js +55 -9
- package/dist/commands/session/poke.js +1 -1
- package/dist/commands/work/index.js +8 -0
- package/dist/commands/work/linear.d.ts +24 -0
- package/dist/commands/work/linear.js +195 -0
- package/dist/commands/work/review.d.ts +45 -0
- package/dist/commands/work/review.js +401 -0
- package/dist/commands/work/spawn.js +28 -19
- package/dist/commands/work/start.js +12 -2
- package/dist/hooks/init.js +26 -5
- package/dist/lib/caffeinate.d.ts +64 -0
- package/dist/lib/caffeinate.js +146 -0
- package/dist/lib/database/native-validation.d.ts +21 -0
- package/dist/lib/database/native-validation.js +49 -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 +56 -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/mcp/tools/work.js +36 -0
- 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 +4158 -3651
- package/package.json +2 -2
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { ExternalIssueAdapterError, toNormalizedEnvelope, } from './types.js';
|
|
2
|
+
const DEFAULT_LINEAR_API_URL = 'https://api.linear.app/graphql';
|
|
3
|
+
const LINEAR_ISSUES_QUERY = `
|
|
4
|
+
query IssuesForSpawn($teamKey: String!, $first: Int!) {
|
|
5
|
+
issues(
|
|
6
|
+
first: $first
|
|
7
|
+
filter: {
|
|
8
|
+
team: { key: { eq: $teamKey } }
|
|
9
|
+
state: { type: { nin: ["completed", "canceled"] } }
|
|
10
|
+
}
|
|
11
|
+
orderBy: updatedAt
|
|
12
|
+
) {
|
|
13
|
+
nodes {
|
|
14
|
+
id
|
|
15
|
+
identifier
|
|
16
|
+
title
|
|
17
|
+
description
|
|
18
|
+
url
|
|
19
|
+
priority
|
|
20
|
+
labels {
|
|
21
|
+
nodes {
|
|
22
|
+
name
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
state {
|
|
26
|
+
name
|
|
27
|
+
type
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
function priorityFromLinear(value) {
|
|
34
|
+
switch (value) {
|
|
35
|
+
case 1:
|
|
36
|
+
return 'P0';
|
|
37
|
+
case 2:
|
|
38
|
+
return 'P1';
|
|
39
|
+
case 3:
|
|
40
|
+
return 'P2';
|
|
41
|
+
case 4:
|
|
42
|
+
return 'P3';
|
|
43
|
+
default:
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function ensureLinearConfig(config) {
|
|
48
|
+
const apiKey = config.apiKey || process.env.LINEAR_API_KEY || process.env.PRLT_LINEAR_API_KEY;
|
|
49
|
+
const team = config.team || process.env.PRLT_LINEAR_TEAM || process.env.LINEAR_TEAM_KEY;
|
|
50
|
+
const apiUrl = config.apiUrl || process.env.PRLT_LINEAR_API_URL || DEFAULT_LINEAR_API_URL;
|
|
51
|
+
if (!apiKey) {
|
|
52
|
+
throw new ExternalIssueAdapterError('MISSING_CONFIG', 'Missing Linear API key. Set LINEAR_API_KEY or PRLT_LINEAR_API_KEY.');
|
|
53
|
+
}
|
|
54
|
+
if (!team) {
|
|
55
|
+
throw new ExternalIssueAdapterError('MISSING_CONFIG', 'Missing Linear team key. Pass --team or set PRLT_LINEAR_TEAM.');
|
|
56
|
+
}
|
|
57
|
+
return { apiKey, team, apiUrl };
|
|
58
|
+
}
|
|
59
|
+
function ensureLinearIssueShape(issue) {
|
|
60
|
+
if (!issue.id || !issue.identifier || !issue.title || !issue.url) {
|
|
61
|
+
throw new ExternalIssueAdapterError('BAD_PAYLOAD', 'Linear issue payload is missing required fields (id, identifier, title, url).', issue);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Normalize a raw Linear API issue node into a canonical IssueEnvelope.
|
|
66
|
+
*/
|
|
67
|
+
export function normalizeLinearIssue(rawIssue) {
|
|
68
|
+
if (!rawIssue || typeof rawIssue !== 'object') {
|
|
69
|
+
throw new ExternalIssueAdapterError('BAD_PAYLOAD', 'Linear issue payload is invalid.', rawIssue);
|
|
70
|
+
}
|
|
71
|
+
const issue = rawIssue;
|
|
72
|
+
ensureLinearIssueShape(issue);
|
|
73
|
+
const labels = (issue.labels?.nodes || [])
|
|
74
|
+
.map(label => label.name?.trim())
|
|
75
|
+
.filter((name) => Boolean(name));
|
|
76
|
+
const [projectKey] = issue.identifier.split('-');
|
|
77
|
+
return {
|
|
78
|
+
source: 'linear',
|
|
79
|
+
external_id: issue.id,
|
|
80
|
+
external_key: issue.identifier,
|
|
81
|
+
title: issue.title,
|
|
82
|
+
description: issue.description || '',
|
|
83
|
+
labels,
|
|
84
|
+
priority: priorityFromLinear(issue.priority),
|
|
85
|
+
status: issue.state?.name || 'Unknown',
|
|
86
|
+
url: issue.url,
|
|
87
|
+
project_key: projectKey || 'UNKNOWN',
|
|
88
|
+
assignee: null,
|
|
89
|
+
item_type: 'issue',
|
|
90
|
+
raw: rawIssue,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Normalize a raw Linear issue into a PMO-ready NormalizedIssueEnvelope.
|
|
95
|
+
*/
|
|
96
|
+
export function normalizeLinearIssueToEnvelope(rawIssue) {
|
|
97
|
+
const envelope = normalizeLinearIssue(rawIssue);
|
|
98
|
+
return toNormalizedEnvelope(envelope, 'feature');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build a PMO ticket description from a NormalizedIssueEnvelope.
|
|
102
|
+
*/
|
|
103
|
+
export function buildLinearTicketDescription(envelope) {
|
|
104
|
+
const body = envelope.description.trim();
|
|
105
|
+
const metadataLines = [
|
|
106
|
+
`- Source: ${envelope.source.name}`,
|
|
107
|
+
`- External key: ${envelope.source.externalKey}`,
|
|
108
|
+
`- External id: ${envelope.source.externalId}`,
|
|
109
|
+
`- URL: ${envelope.source.url}`,
|
|
110
|
+
`- Status: ${envelope.status}`,
|
|
111
|
+
`- Priority: ${envelope.priority || 'Unset'}`,
|
|
112
|
+
`- Labels: ${envelope.labels.length > 0 ? envelope.labels.join(', ') : 'None'}`,
|
|
113
|
+
];
|
|
114
|
+
const parts = [
|
|
115
|
+
body,
|
|
116
|
+
'## External Issue Context',
|
|
117
|
+
metadataLines.join('\n'),
|
|
118
|
+
].filter(Boolean);
|
|
119
|
+
return parts.join('\n\n');
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build ticket metadata from a NormalizedIssueEnvelope for traceability.
|
|
123
|
+
*/
|
|
124
|
+
export function buildLinearMetadata(envelope) {
|
|
125
|
+
return {
|
|
126
|
+
external_source: envelope.source.name,
|
|
127
|
+
external_key: envelope.source.externalKey,
|
|
128
|
+
external_id: envelope.source.externalId,
|
|
129
|
+
external_url: envelope.source.url,
|
|
130
|
+
external_raw: JSON.stringify(envelope.source.raw),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Build a spawn context message from a NormalizedIssueEnvelope.
|
|
135
|
+
*/
|
|
136
|
+
export function buildLinearSpawnContextMessage(envelope, additionalMessage) {
|
|
137
|
+
const lines = [
|
|
138
|
+
`External issue source: ${envelope.source.name}`,
|
|
139
|
+
`External issue key: ${envelope.source.externalKey}`,
|
|
140
|
+
`External issue id: ${envelope.source.externalId}`,
|
|
141
|
+
`External issue URL: ${envelope.source.url}`,
|
|
142
|
+
];
|
|
143
|
+
if (additionalMessage?.trim()) {
|
|
144
|
+
lines.push('', additionalMessage.trim());
|
|
145
|
+
}
|
|
146
|
+
return lines.join('\n');
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Build a CLI command string for selecting a specific Linear issue.
|
|
150
|
+
*/
|
|
151
|
+
export function buildLinearIssueChoiceCommand(issueIdentifier, projectId) {
|
|
152
|
+
let command = `prlt work linear --issue ${issueIdentifier} --json`;
|
|
153
|
+
if (projectId) {
|
|
154
|
+
command += ` -P ${projectId}`;
|
|
155
|
+
}
|
|
156
|
+
return command;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Fetch and normalize Linear issues into NormalizedIssueEnvelopes.
|
|
160
|
+
*/
|
|
161
|
+
export async function listLinearIssues(configInput, options) {
|
|
162
|
+
const config = ensureLinearConfig(configInput);
|
|
163
|
+
const fetchImpl = options?.fetchImpl || fetch;
|
|
164
|
+
const limit = Math.max(1, Math.min(options?.limit ?? 20, 100));
|
|
165
|
+
const response = await fetchImpl(config.apiUrl, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: {
|
|
168
|
+
'Content-Type': 'application/json',
|
|
169
|
+
Authorization: config.apiKey,
|
|
170
|
+
},
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
query: LINEAR_ISSUES_QUERY,
|
|
173
|
+
variables: {
|
|
174
|
+
teamKey: config.team,
|
|
175
|
+
first: limit,
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
if (response.status === 401 || response.status === 403) {
|
|
180
|
+
throw new ExternalIssueAdapterError('AUTH_FAILED', 'Linear authentication failed. Verify your LINEAR_API_KEY token.');
|
|
181
|
+
}
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
throw new ExternalIssueAdapterError('REQUEST_FAILED', `Linear request failed with status ${response.status}.`);
|
|
184
|
+
}
|
|
185
|
+
const payload = await response.json();
|
|
186
|
+
if (payload.errors && payload.errors.length > 0) {
|
|
187
|
+
const message = payload.errors[0]?.message || 'Unknown Linear API error.';
|
|
188
|
+
if (/auth|token|forbidden|unauthorized/i.test(message)) {
|
|
189
|
+
throw new ExternalIssueAdapterError('AUTH_FAILED', `Linear authentication failed: ${message}`);
|
|
190
|
+
}
|
|
191
|
+
throw new ExternalIssueAdapterError('REQUEST_FAILED', `Linear API error: ${message}`);
|
|
192
|
+
}
|
|
193
|
+
const nodes = payload.data?.issues?.nodes;
|
|
194
|
+
if (!Array.isArray(nodes)) {
|
|
195
|
+
throw new ExternalIssueAdapterError('BAD_PAYLOAD', 'Linear response payload was missing issues.nodes.', payload);
|
|
196
|
+
}
|
|
197
|
+
return nodes.map(normalizeLinearIssueToEnvelope);
|
|
198
|
+
}
|
|
@@ -142,3 +142,70 @@ export declare class ExternalIssueError extends Error {
|
|
|
142
142
|
validationErrors?: IssueValidationError[] | undefined;
|
|
143
143
|
constructor(code: ExternalIssueErrorCode, message: string, source?: IssueSource | undefined, validationErrors?: IssueValidationError[] | undefined);
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Error codes for adapter-level operations (config, auth, payload, request).
|
|
147
|
+
*/
|
|
148
|
+
export type ExternalIssueAdapterErrorCode = 'MISSING_CONFIG' | 'AUTH_FAILED' | 'BAD_PAYLOAD' | 'REQUEST_FAILED';
|
|
149
|
+
/**
|
|
150
|
+
* Typed error for external issue adapter operations.
|
|
151
|
+
*
|
|
152
|
+
* Covers config, auth, payload, and request failures that occur
|
|
153
|
+
* when interacting with a specific external issue source.
|
|
154
|
+
*/
|
|
155
|
+
export declare class ExternalIssueAdapterError extends Error {
|
|
156
|
+
readonly code: ExternalIssueAdapterErrorCode;
|
|
157
|
+
readonly causeDetail?: unknown | undefined;
|
|
158
|
+
constructor(code: ExternalIssueAdapterErrorCode, message: string, causeDetail?: unknown | undefined);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Source metadata nested object for traceability.
|
|
162
|
+
*/
|
|
163
|
+
export interface IssueSourceMetadata {
|
|
164
|
+
/** Which external system the issue came from */
|
|
165
|
+
name: IssueSource;
|
|
166
|
+
/** Unique identifier in the external system (e.g., Linear UUID) */
|
|
167
|
+
externalId: string;
|
|
168
|
+
/** Human-readable key in the external system (e.g., "ENG-123") */
|
|
169
|
+
externalKey: string;
|
|
170
|
+
/** URL to view the issue in the external system */
|
|
171
|
+
url: string;
|
|
172
|
+
/** Original raw payload from the external system */
|
|
173
|
+
raw: Record<string, unknown>;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* PMO-ready normalized issue envelope.
|
|
177
|
+
*
|
|
178
|
+
* Wraps a canonical IssueEnvelope with PMO-oriented fields (e.g., category)
|
|
179
|
+
* and a nested source metadata object for ergonomic access in commands.
|
|
180
|
+
*
|
|
181
|
+
* Produced by normalizing an IssueEnvelope into the shape expected by
|
|
182
|
+
* PMO ticket creation and the spawn context pipeline.
|
|
183
|
+
*/
|
|
184
|
+
export interface NormalizedIssueEnvelope {
|
|
185
|
+
/** Nested source metadata for traceability */
|
|
186
|
+
source: IssueSourceMetadata;
|
|
187
|
+
/** Issue title / summary */
|
|
188
|
+
title: string;
|
|
189
|
+
/** Issue description (markdown or plain text) */
|
|
190
|
+
description: string;
|
|
191
|
+
/** Labels / tags applied to the issue */
|
|
192
|
+
labels: string[];
|
|
193
|
+
/** Priority level (normalized to P0-P3 scale, or null) */
|
|
194
|
+
priority: string | null;
|
|
195
|
+
/** Current status name in the external system */
|
|
196
|
+
status: string;
|
|
197
|
+
/** Project key in the external system */
|
|
198
|
+
projectKey: string;
|
|
199
|
+
/** Assignee display name or identifier */
|
|
200
|
+
assignee: string | null;
|
|
201
|
+
/** PMO ticket category derived from source metadata (e.g., "feature") */
|
|
202
|
+
category: string | null;
|
|
203
|
+
/** Source-native work item kind when available */
|
|
204
|
+
itemType?: string | null;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Convert a canonical IssueEnvelope to a NormalizedIssueEnvelope.
|
|
208
|
+
*
|
|
209
|
+
* Deterministic: same input always produces same output.
|
|
210
|
+
*/
|
|
211
|
+
export declare function toNormalizedEnvelope(envelope: IssueEnvelope, category?: string | null): NormalizedIssueEnvelope;
|
|
@@ -24,3 +24,44 @@ export class ExternalIssueError extends Error {
|
|
|
24
24
|
this.name = 'ExternalIssueError';
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Typed error for external issue adapter operations.
|
|
29
|
+
*
|
|
30
|
+
* Covers config, auth, payload, and request failures that occur
|
|
31
|
+
* when interacting with a specific external issue source.
|
|
32
|
+
*/
|
|
33
|
+
export class ExternalIssueAdapterError extends Error {
|
|
34
|
+
code;
|
|
35
|
+
causeDetail;
|
|
36
|
+
constructor(code, message, causeDetail) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.causeDetail = causeDetail;
|
|
40
|
+
this.name = 'ExternalIssueAdapterError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Convert a canonical IssueEnvelope to a NormalizedIssueEnvelope.
|
|
45
|
+
*
|
|
46
|
+
* Deterministic: same input always produces same output.
|
|
47
|
+
*/
|
|
48
|
+
export function toNormalizedEnvelope(envelope, category) {
|
|
49
|
+
return {
|
|
50
|
+
source: {
|
|
51
|
+
name: envelope.source,
|
|
52
|
+
externalId: envelope.external_id,
|
|
53
|
+
externalKey: envelope.external_key,
|
|
54
|
+
url: envelope.url,
|
|
55
|
+
raw: envelope.raw,
|
|
56
|
+
},
|
|
57
|
+
title: envelope.title,
|
|
58
|
+
description: envelope.description,
|
|
59
|
+
labels: envelope.labels,
|
|
60
|
+
priority: envelope.priority,
|
|
61
|
+
status: envelope.status,
|
|
62
|
+
projectKey: envelope.project_key,
|
|
63
|
+
assignee: envelope.assignee,
|
|
64
|
+
category: category ?? null,
|
|
65
|
+
itemType: envelope.item_type ?? null,
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/lib/init/index.d.ts
CHANGED
|
@@ -35,6 +35,10 @@ export declare function validateHQLocation(location: string): {
|
|
|
35
35
|
valid: boolean;
|
|
36
36
|
reason?: string;
|
|
37
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* Check if an HQ name is already in use by another headquarters on this machine.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isHQNameTaken(name: string): boolean;
|
|
38
42
|
/**
|
|
39
43
|
* Prompt user for HQ name
|
|
40
44
|
*/
|
package/dist/lib/init/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createAgentWorktrees } from '../agents/index.js';
|
|
|
8
8
|
import { addRepositoriesToHQ, isInGitRepo } from '../repos/index.js';
|
|
9
9
|
import { createPMO, } from '../pmo/index.js';
|
|
10
10
|
import { createWorkspaceDatabase, addRepositoriesToDatabase, addAgentsToDatabase, createTheme, addThemeNames, setActiveTheme } from '../database/index.js';
|
|
11
|
-
import { ensureMachineConfigDir, registerHeadquarters, getOrganizations, createOrganization, } from '../machine-config.js';
|
|
11
|
+
import { ensureMachineConfigDir, registerHeadquarters, getOrganizations, createOrganization, findHeadquartersByName, } from '../machine-config.js';
|
|
12
12
|
import { hasGitHubRemote } from '../repos/git.js';
|
|
13
13
|
import { isGHInstalled, isGHAuthenticated } from '../pr/index.js';
|
|
14
14
|
/**
|
|
@@ -55,6 +55,13 @@ export function validateHQLocation(location) {
|
|
|
55
55
|
}
|
|
56
56
|
return { valid: true };
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Check if an HQ name is already in use by another headquarters on this machine.
|
|
60
|
+
*/
|
|
61
|
+
export function isHQNameTaken(name) {
|
|
62
|
+
const existing = findHeadquartersByName(name);
|
|
63
|
+
return existing.length > 0;
|
|
64
|
+
}
|
|
58
65
|
/**
|
|
59
66
|
* Prompt user for HQ name
|
|
60
67
|
*/
|
|
@@ -74,6 +81,9 @@ export async function promptForHQName() {
|
|
|
74
81
|
if (!/^[a-zA-Z0-9-_]+$/.test(input)) {
|
|
75
82
|
return 'Name can only contain letters, numbers, hyphens, and underscores';
|
|
76
83
|
}
|
|
84
|
+
if (isHQNameTaken(input.trim())) {
|
|
85
|
+
return `HQ name "${input.trim()}" is already in use on this machine. Pick another name.`;
|
|
86
|
+
}
|
|
77
87
|
return true;
|
|
78
88
|
},
|
|
79
89
|
}]);
|
|
@@ -40,6 +40,7 @@ export interface MachineConfig {
|
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
42
|
* Get the path to the machine-level config directory (~/.proletariat/).
|
|
43
|
+
* Uses process.env.HOME when set (for testability), falling back to os.homedir().
|
|
43
44
|
*/
|
|
44
45
|
export declare function getMachineConfigDir(): string;
|
|
45
46
|
/**
|
|
@@ -5,9 +5,11 @@ import { isValidHQ } from './workspace.js';
|
|
|
5
5
|
const CONFIG_VERSION = '1.0.0';
|
|
6
6
|
/**
|
|
7
7
|
* Get the path to the machine-level config directory (~/.proletariat/).
|
|
8
|
+
* Uses process.env.HOME when set (for testability), falling back to os.homedir().
|
|
8
9
|
*/
|
|
9
10
|
export function getMachineConfigDir() {
|
|
10
|
-
|
|
11
|
+
const home = process.env.HOME || os.homedir();
|
|
12
|
+
return path.join(home, '.proletariat');
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* Get the path to the machine-level config file (~/.proletariat/config.json).
|
|
@@ -32,9 +34,10 @@ export function ensureMachineConfigDir() {
|
|
|
32
34
|
* - Removes trailing slashes
|
|
33
35
|
*/
|
|
34
36
|
export function normalizePath(inputPath) {
|
|
35
|
-
// Expand ~ to home directory
|
|
37
|
+
// Expand ~ to home directory (respect process.env.HOME for testability)
|
|
38
|
+
const home = process.env.HOME || os.homedir();
|
|
36
39
|
let resolved = inputPath.startsWith('~')
|
|
37
|
-
? path.join(
|
|
40
|
+
? path.join(home, inputPath.slice(1))
|
|
38
41
|
: inputPath;
|
|
39
42
|
// Make absolute
|
|
40
43
|
resolved = path.resolve(resolved);
|
|
@@ -219,6 +219,42 @@ export function registerWorkTools(server, ctx) {
|
|
|
219
219
|
return errorResponse(error);
|
|
220
220
|
}
|
|
221
221
|
});
|
|
222
|
+
strictTool(server, 'work_review', 'Run automated review-fix pipeline on a ticket: spawns review agent, checks results, auto-spawns fix agent if issues found, re-reviews until clean or max cycles reached. Requires ticket to have a PR.', {
|
|
223
|
+
ticket_id: z.string().describe('Ticket ID to review'),
|
|
224
|
+
max_cycles: z.number().optional().describe('Maximum review-fix cycles (default: 3)'),
|
|
225
|
+
skip_permissions: z.boolean().optional().describe('Skip permission prompts (default: false)'),
|
|
226
|
+
environment: z.enum(['devcontainer', 'host']).optional().describe('Execution environment (default: devcontainer)'),
|
|
227
|
+
}, async (params) => {
|
|
228
|
+
try {
|
|
229
|
+
const args = [params.ticket_id, '--auto'];
|
|
230
|
+
if (params.max_cycles) {
|
|
231
|
+
args.push('--max-cycles', String(params.max_cycles));
|
|
232
|
+
}
|
|
233
|
+
if (params.skip_permissions) {
|
|
234
|
+
args.push('--skip-permissions');
|
|
235
|
+
}
|
|
236
|
+
if (params.environment === 'host') {
|
|
237
|
+
args.push('--run-on-host');
|
|
238
|
+
}
|
|
239
|
+
const cmd = `prlt work review ${args.join(' ')}`;
|
|
240
|
+
const output = ctx.runCommand(cmd);
|
|
241
|
+
return {
|
|
242
|
+
content: [{
|
|
243
|
+
type: 'text',
|
|
244
|
+
text: JSON.stringify({
|
|
245
|
+
success: true,
|
|
246
|
+
ticketId: params.ticket_id,
|
|
247
|
+
command: cmd,
|
|
248
|
+
output,
|
|
249
|
+
message: `Review pipeline started for ${params.ticket_id}`,
|
|
250
|
+
}, null, 2),
|
|
251
|
+
}],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
return errorResponse(error);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
222
258
|
strictTool(server, 'work_spawn', 'Spawn work on a ticket using the full CLI pipeline (agent selection, Docker build, container creation, branch setup, tmux session). Shells out to "prlt work spawn" — works whenever prlt is installed, no workspace context needed in-process.', {
|
|
223
259
|
ticket_id: z.string().describe('Ticket ID to spawn work for'),
|
|
224
260
|
action: z.string().optional().describe('Action to perform (e.g., implement, groom, review, custom). Defaults to implement.'),
|
|
@@ -165,10 +165,10 @@ export class ActionStorage {
|
|
|
165
165
|
description: row.description || undefined,
|
|
166
166
|
prompt: row.prompt,
|
|
167
167
|
endPrompt: row.end_prompt || undefined,
|
|
168
|
-
suggestedForCategories: row.
|
|
169
|
-
? JSON.parse(row.
|
|
168
|
+
suggestedForCategories: row.suggested_for_categories
|
|
169
|
+
? JSON.parse(row.suggested_for_categories)
|
|
170
170
|
: undefined,
|
|
171
|
-
defaultMoveToCategory: row.
|
|
171
|
+
defaultMoveToCategory: row.default_move_to_category,
|
|
172
172
|
modifiesCode: row.modifies_code === 1,
|
|
173
173
|
isBuiltin: row.is_builtin === 1,
|
|
174
174
|
createdAt: new Date(row.created_at),
|
|
@@ -817,7 +817,16 @@ After reviewing, determine your verdict:
|
|
|
817
817
|
- **REQUEST_CHANGES**: There are issues that must be fixed before merging
|
|
818
818
|
- **COMMENT**: General feedback, no blocking issues but some suggestions
|
|
819
819
|
|
|
820
|
-
|
|
820
|
+
## STRICT RULES - READ CAREFULLY
|
|
821
|
+
|
|
822
|
+
- **DO NOT** merge the PR (\`gh pr merge\` is FORBIDDEN)
|
|
823
|
+
- **DO NOT** push any code (\`git push\` is FORBIDDEN)
|
|
824
|
+
- **DO NOT** run tests or test suites
|
|
825
|
+
- **DO NOT** modify, edit, or write any code files
|
|
826
|
+
- **DO NOT** create commits
|
|
827
|
+
- Your ONLY output should be a \`gh pr review\` comment
|
|
828
|
+
- This is a **read-only** review — read the diff, analyze it, post your review, and stop
|
|
829
|
+
|
|
821
830
|
If you identify issues that need fixing, describe them in your review. A separate action (Review & Fix) will handle fixes.
|
|
822
831
|
|
|
823
832
|
${PRLT_COMMANDS_COMMON}
|
|
@@ -872,7 +881,7 @@ COMMENT - Some suggestions but no blocking issues."
|
|
|
872
881
|
|
|
873
882
|
Format the body with: what looks good, concerns (if any), suggested improvements (if any), and your verdict.
|
|
874
883
|
|
|
875
|
-
|
|
884
|
+
**REMINDER:** Do NOT merge the PR. Do NOT run tests. Do NOT modify code. Only post the review comment above.
|
|
876
885
|
|
|
877
886
|
**After posting your review**, if you found issues that need fixing, log them on the ticket so another action can address them:
|
|
878
887
|
\`\`\`bash
|
|
@@ -884,6 +893,76 @@ prlt ticket edit <TICKET_ID> --add-subtask "Fix: <description of issue>"
|
|
|
884
893
|
modifiesCode: false,
|
|
885
894
|
position: 4,
|
|
886
895
|
},
|
|
896
|
+
{
|
|
897
|
+
id: 'review-comment',
|
|
898
|
+
name: 'Review Comment',
|
|
899
|
+
description: 'Post review comments on a PR without merging, testing, or modifying code',
|
|
900
|
+
prompt: `${PRLT_USAGE_RULE}
|
|
901
|
+
|
|
902
|
+
---
|
|
903
|
+
|
|
904
|
+
# Action: Review Comment
|
|
905
|
+
|
|
906
|
+
Read the PR diff, analyze the changes, and post a GitHub review comment. That is your ONLY job.
|
|
907
|
+
|
|
908
|
+
## STRICT RULES - READ CAREFULLY
|
|
909
|
+
|
|
910
|
+
You are a **read-only** reviewer. You MUST follow these rules:
|
|
911
|
+
|
|
912
|
+
- **DO NOT** run \`gh pr merge\` — merging is FORBIDDEN
|
|
913
|
+
- **DO NOT** run \`git push\` — pushing is FORBIDDEN
|
|
914
|
+
- **DO NOT** run tests or test suites of any kind
|
|
915
|
+
- **DO NOT** modify, edit, or write any code files
|
|
916
|
+
- **DO NOT** create commits or branches
|
|
917
|
+
- **DO NOT** run any commands that change repository state
|
|
918
|
+
- Your **ONLY** permitted write operation is \`gh pr review\` to post your comment
|
|
919
|
+
|
|
920
|
+
## What To Do
|
|
921
|
+
|
|
922
|
+
1. Read the PR diff to understand the changes
|
|
923
|
+
2. Analyze the code for bugs, issues, style, and correctness
|
|
924
|
+
3. Post your review using \`gh pr review\` with the appropriate verdict
|
|
925
|
+
4. **STOP** — do nothing else after posting the review
|
|
926
|
+
|
|
927
|
+
${PRLT_COMMANDS_COMMON}
|
|
928
|
+
${PRLT_COMMANDS_REVIEW}`,
|
|
929
|
+
endPrompt: `Post your review on the PR using \`gh pr review\`. This is the ONLY action you should take.
|
|
930
|
+
|
|
931
|
+
**If approving:**
|
|
932
|
+
\`\`\`bash
|
|
933
|
+
gh pr review --approve --body "## Review
|
|
934
|
+
|
|
935
|
+
### Summary
|
|
936
|
+
- ...
|
|
937
|
+
|
|
938
|
+
APPROVED - Looks good to merge."
|
|
939
|
+
\`\`\`
|
|
940
|
+
|
|
941
|
+
**If requesting changes:**
|
|
942
|
+
\`\`\`bash
|
|
943
|
+
gh pr review --request-changes --body "## Review
|
|
944
|
+
|
|
945
|
+
### Issues
|
|
946
|
+
- ...
|
|
947
|
+
|
|
948
|
+
REQUEST CHANGES - Please address the above."
|
|
949
|
+
\`\`\`
|
|
950
|
+
|
|
951
|
+
**If commenting:**
|
|
952
|
+
\`\`\`bash
|
|
953
|
+
gh pr review --comment --body "## Review
|
|
954
|
+
|
|
955
|
+
### Feedback
|
|
956
|
+
- ...
|
|
957
|
+
|
|
958
|
+
COMMENT - Some suggestions, no blockers."
|
|
959
|
+
\`\`\`
|
|
960
|
+
|
|
961
|
+
**CRITICAL REMINDER:** After posting your review, STOP. Do NOT merge the PR. Do NOT run tests. Do NOT modify code. Do NOT push anything. Your job is done after the \`gh pr review\` command.`,
|
|
962
|
+
suggestedForCategories: ['completed'],
|
|
963
|
+
modifiesCode: false,
|
|
964
|
+
position: 5,
|
|
965
|
+
},
|
|
887
966
|
{
|
|
888
967
|
id: 'review-fix',
|
|
889
968
|
name: 'Review & Fix',
|
|
@@ -943,7 +1022,7 @@ ${PRLT_COMMANDS_REVIEW}`,
|
|
|
943
1022
|
\`\`\``,
|
|
944
1023
|
suggestedForCategories: ['started', 'completed'],
|
|
945
1024
|
modifiesCode: true,
|
|
946
|
-
position:
|
|
1025
|
+
position: 6,
|
|
947
1026
|
},
|
|
948
1027
|
{
|
|
949
1028
|
id: 'revise',
|
|
@@ -1007,7 +1086,7 @@ The PR will be updated automatically with your pushed changes.`,
|
|
|
1007
1086
|
suggestedForCategories: ['completed'],
|
|
1008
1087
|
defaultMoveToCategory: 'started',
|
|
1009
1088
|
modifiesCode: true,
|
|
1010
|
-
position:
|
|
1089
|
+
position: 7,
|
|
1011
1090
|
},
|
|
1012
1091
|
{
|
|
1013
1092
|
id: 'explore-cli',
|
|
@@ -1168,7 +1247,7 @@ tmux_kill_session({ session: "qa-test" })
|
|
|
1168
1247
|
\`\`\``,
|
|
1169
1248
|
suggestedForCategories: [],
|
|
1170
1249
|
modifiesCode: false,
|
|
1171
|
-
position:
|
|
1250
|
+
position: 9,
|
|
1172
1251
|
},
|
|
1173
1252
|
{
|
|
1174
1253
|
id: 'test',
|
|
@@ -1213,7 +1292,7 @@ ${PRLT_COMMANDS_CODE}`,
|
|
|
1213
1292
|
**IMPORTANT:** Use the global \`prlt\` command.`,
|
|
1214
1293
|
suggestedForCategories: ['started', 'completed'],
|
|
1215
1294
|
modifiesCode: true,
|
|
1216
|
-
position:
|
|
1295
|
+
position: 8,
|
|
1217
1296
|
},
|
|
1218
1297
|
];
|
|
1219
1298
|
// Use INSERT OR REPLACE to always update builtin actions with latest prompts
|
|
@@ -139,7 +139,7 @@ export class EpicStorage {
|
|
|
139
139
|
updates.push('file_path = ?');
|
|
140
140
|
params.push(changes.filePath);
|
|
141
141
|
}
|
|
142
|
-
if (
|
|
142
|
+
if ('specId' in changes) {
|
|
143
143
|
updates.push('spec_id = ?');
|
|
144
144
|
params.push(changes.specId || null);
|
|
145
145
|
}
|
|
@@ -301,8 +301,8 @@ export class TicketStorage {
|
|
|
301
301
|
updates.assignee = changes.assignee;
|
|
302
302
|
if (changes.branch !== undefined)
|
|
303
303
|
updates.branch = changes.branch;
|
|
304
|
-
if (
|
|
305
|
-
updates.specId = changes.specId;
|
|
304
|
+
if ('specId' in changes)
|
|
305
|
+
updates.specId = changes.specId ?? null;
|
|
306
306
|
if (changes.lastSyncedFromSpec !== undefined) {
|
|
307
307
|
updates.lastSyncedFromSpec = changes.lastSyncedFromSpec;
|
|
308
308
|
}
|
|
@@ -168,7 +168,8 @@ export interface WorkActionRow {
|
|
|
168
168
|
description: string | null;
|
|
169
169
|
prompt: string;
|
|
170
170
|
end_prompt: string | null;
|
|
171
|
-
|
|
171
|
+
suggested_for_categories: string | null;
|
|
172
|
+
default_move_to_category: string | null;
|
|
172
173
|
modifies_code: number;
|
|
173
174
|
is_builtin: number;
|
|
174
175
|
position: number;
|