@proletariat/cli 0.3.47 → 0.3.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/caffeinate/index.d.ts +10 -0
- package/dist/commands/caffeinate/index.js +64 -0
- package/dist/commands/caffeinate/start.d.ts +14 -0
- package/dist/commands/caffeinate/start.js +86 -0
- package/dist/commands/caffeinate/status.d.ts +10 -0
- package/dist/commands/caffeinate/status.js +55 -0
- package/dist/commands/caffeinate/stop.d.ts +10 -0
- package/dist/commands/caffeinate/stop.js +47 -0
- package/dist/commands/claude/index.js +21 -21
- package/dist/commands/claude/open.js +1 -1
- package/dist/commands/commit.js +10 -8
- package/dist/commands/config/index.js +4 -5
- package/dist/commands/execution/config.d.ts +2 -2
- package/dist/commands/execution/config.js +18 -18
- package/dist/commands/execution/list.js +2 -2
- package/dist/commands/execution/view.js +2 -2
- package/dist/commands/init.js +9 -1
- package/dist/commands/orchestrator/attach.js +64 -14
- package/dist/commands/orchestrator/start.d.ts +5 -5
- package/dist/commands/orchestrator/start.js +45 -35
- package/dist/commands/orchestrator/status.js +64 -23
- package/dist/commands/orchestrator/stop.js +44 -12
- package/dist/commands/qa/index.js +12 -12
- package/dist/commands/session/attach.js +23 -0
- package/dist/commands/session/poke.js +1 -1
- package/dist/commands/staff/add.js +1 -1
- package/dist/commands/work/index.js +4 -0
- package/dist/commands/work/linear.d.ts +24 -0
- package/dist/commands/work/linear.js +218 -0
- package/dist/commands/work/revise.js +8 -8
- package/dist/commands/work/spawn.js +29 -20
- package/dist/commands/work/start.js +22 -12
- package/dist/commands/work/watch.js +3 -3
- package/dist/hooks/init.js +8 -0
- package/dist/lib/agents/index.js +2 -2
- package/dist/lib/caffeinate.d.ts +64 -0
- package/dist/lib/caffeinate.js +146 -0
- package/dist/lib/database/drizzle-schema.d.ts +7 -7
- package/dist/lib/database/drizzle-schema.js +1 -1
- package/dist/lib/execution/codex-adapter.d.ts +96 -0
- package/dist/lib/execution/codex-adapter.js +148 -0
- package/dist/lib/execution/config.d.ts +6 -6
- package/dist/lib/execution/config.js +17 -10
- package/dist/lib/execution/devcontainer.d.ts +3 -3
- package/dist/lib/execution/devcontainer.js +3 -3
- package/dist/lib/execution/index.d.ts +1 -0
- package/dist/lib/execution/index.js +1 -0
- package/dist/lib/execution/runners.d.ts +2 -2
- package/dist/lib/execution/runners.js +69 -26
- package/dist/lib/execution/spawner.js +3 -3
- package/dist/lib/execution/storage.d.ts +2 -2
- package/dist/lib/execution/storage.js +3 -3
- package/dist/lib/execution/types.d.ts +2 -2
- package/dist/lib/execution/types.js +1 -1
- package/dist/lib/external-issues/index.d.ts +1 -1
- package/dist/lib/external-issues/index.js +1 -1
- package/dist/lib/external-issues/linear.d.ts +43 -0
- package/dist/lib/external-issues/linear.js +261 -0
- package/dist/lib/external-issues/types.d.ts +67 -0
- package/dist/lib/external-issues/types.js +41 -0
- package/dist/lib/init/index.d.ts +4 -0
- package/dist/lib/init/index.js +11 -1
- package/dist/lib/machine-config.d.ts +1 -0
- package/dist/lib/machine-config.js +6 -3
- package/dist/lib/pmo/schema.d.ts +1 -1
- package/dist/lib/pmo/schema.js +1 -1
- package/dist/lib/pmo/storage/actions.js +3 -3
- package/dist/lib/pmo/storage/base.js +116 -6
- package/dist/lib/pmo/storage/epics.js +1 -1
- package/dist/lib/pmo/storage/tickets.js +2 -2
- package/dist/lib/pmo/storage/types.d.ts +2 -1
- package/dist/lib/repos/index.js +1 -1
- package/oclif.manifest.json +3052 -2721
- package/package.json +1 -1
|
@@ -0,0 +1,261 @@
|
|
|
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
|
+
const LINEAR_ISSUE_BY_IDENTIFIER_QUERY = `
|
|
34
|
+
query IssueForSpawn($id: String!) {
|
|
35
|
+
issue(id: $id) {
|
|
36
|
+
id
|
|
37
|
+
identifier
|
|
38
|
+
title
|
|
39
|
+
description
|
|
40
|
+
url
|
|
41
|
+
priority
|
|
42
|
+
labels {
|
|
43
|
+
nodes {
|
|
44
|
+
name
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
state {
|
|
48
|
+
name
|
|
49
|
+
type
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
function priorityFromLinear(value) {
|
|
55
|
+
switch (value) {
|
|
56
|
+
case 1:
|
|
57
|
+
return 'P0';
|
|
58
|
+
case 2:
|
|
59
|
+
return 'P1';
|
|
60
|
+
case 3:
|
|
61
|
+
return 'P2';
|
|
62
|
+
case 4:
|
|
63
|
+
return 'P3';
|
|
64
|
+
default:
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function ensureLinearConfig(config) {
|
|
69
|
+
const apiKey = config.apiKey || process.env.LINEAR_API_KEY || process.env.PRLT_LINEAR_API_KEY;
|
|
70
|
+
const team = config.team || process.env.PRLT_LINEAR_TEAM || process.env.LINEAR_TEAM_KEY;
|
|
71
|
+
const apiUrl = config.apiUrl || process.env.PRLT_LINEAR_API_URL || DEFAULT_LINEAR_API_URL;
|
|
72
|
+
if (!apiKey) {
|
|
73
|
+
throw new ExternalIssueAdapterError('MISSING_CONFIG', 'Missing Linear API key. Set LINEAR_API_KEY or PRLT_LINEAR_API_KEY.');
|
|
74
|
+
}
|
|
75
|
+
if (!team) {
|
|
76
|
+
throw new ExternalIssueAdapterError('MISSING_CONFIG', 'Missing Linear team key. Pass --team or set PRLT_LINEAR_TEAM.');
|
|
77
|
+
}
|
|
78
|
+
return { apiKey, team, apiUrl };
|
|
79
|
+
}
|
|
80
|
+
function ensureLinearIssueShape(issue) {
|
|
81
|
+
if (!issue.id || !issue.identifier || !issue.title || !issue.url) {
|
|
82
|
+
throw new ExternalIssueAdapterError('BAD_PAYLOAD', 'Linear issue payload is missing required fields (id, identifier, title, url).', issue);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Normalize a raw Linear API issue node into a canonical IssueEnvelope.
|
|
87
|
+
*/
|
|
88
|
+
export function normalizeLinearIssue(rawIssue) {
|
|
89
|
+
if (!rawIssue || typeof rawIssue !== 'object') {
|
|
90
|
+
throw new ExternalIssueAdapterError('BAD_PAYLOAD', 'Linear issue payload is invalid.', rawIssue);
|
|
91
|
+
}
|
|
92
|
+
const issue = rawIssue;
|
|
93
|
+
ensureLinearIssueShape(issue);
|
|
94
|
+
const labels = (issue.labels?.nodes || [])
|
|
95
|
+
.map(label => label.name?.trim())
|
|
96
|
+
.filter((name) => Boolean(name));
|
|
97
|
+
const [projectKey] = issue.identifier.split('-');
|
|
98
|
+
return {
|
|
99
|
+
source: 'linear',
|
|
100
|
+
external_id: issue.id,
|
|
101
|
+
external_key: issue.identifier,
|
|
102
|
+
title: issue.title,
|
|
103
|
+
description: issue.description || '',
|
|
104
|
+
labels,
|
|
105
|
+
priority: priorityFromLinear(issue.priority),
|
|
106
|
+
status: issue.state?.name || 'Unknown',
|
|
107
|
+
url: issue.url,
|
|
108
|
+
project_key: projectKey || 'UNKNOWN',
|
|
109
|
+
assignee: null,
|
|
110
|
+
item_type: 'issue',
|
|
111
|
+
raw: rawIssue,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Normalize a raw Linear issue into a PMO-ready NormalizedIssueEnvelope.
|
|
116
|
+
*/
|
|
117
|
+
export function normalizeLinearIssueToEnvelope(rawIssue) {
|
|
118
|
+
const envelope = normalizeLinearIssue(rawIssue);
|
|
119
|
+
return toNormalizedEnvelope(envelope, 'feature');
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build a PMO ticket description from a NormalizedIssueEnvelope.
|
|
123
|
+
*/
|
|
124
|
+
export function buildLinearTicketDescription(envelope) {
|
|
125
|
+
const body = envelope.description.trim();
|
|
126
|
+
const metadataLines = [
|
|
127
|
+
`- Source: ${envelope.source.name}`,
|
|
128
|
+
`- External key: ${envelope.source.externalKey}`,
|
|
129
|
+
`- External id: ${envelope.source.externalId}`,
|
|
130
|
+
`- URL: ${envelope.source.url}`,
|
|
131
|
+
`- Status: ${envelope.status}`,
|
|
132
|
+
`- Priority: ${envelope.priority || 'Unset'}`,
|
|
133
|
+
`- Labels: ${envelope.labels.length > 0 ? envelope.labels.join(', ') : 'None'}`,
|
|
134
|
+
];
|
|
135
|
+
const parts = [
|
|
136
|
+
body,
|
|
137
|
+
'## External Issue Context',
|
|
138
|
+
metadataLines.join('\n'),
|
|
139
|
+
].filter(Boolean);
|
|
140
|
+
return parts.join('\n\n');
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Build ticket metadata from a NormalizedIssueEnvelope for traceability.
|
|
144
|
+
*/
|
|
145
|
+
export function buildLinearMetadata(envelope) {
|
|
146
|
+
return {
|
|
147
|
+
external_source: envelope.source.name,
|
|
148
|
+
external_key: envelope.source.externalKey,
|
|
149
|
+
external_id: envelope.source.externalId,
|
|
150
|
+
external_url: envelope.source.url,
|
|
151
|
+
external_raw: JSON.stringify(envelope.source.raw),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Build a spawn context message from a NormalizedIssueEnvelope.
|
|
156
|
+
*/
|
|
157
|
+
export function buildLinearSpawnContextMessage(envelope, additionalMessage) {
|
|
158
|
+
const lines = [
|
|
159
|
+
`External issue source: ${envelope.source.name}`,
|
|
160
|
+
`External issue key: ${envelope.source.externalKey}`,
|
|
161
|
+
`External issue id: ${envelope.source.externalId}`,
|
|
162
|
+
`External issue URL: ${envelope.source.url}`,
|
|
163
|
+
];
|
|
164
|
+
if (additionalMessage?.trim()) {
|
|
165
|
+
lines.push('', additionalMessage.trim());
|
|
166
|
+
}
|
|
167
|
+
return lines.join('\n');
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Build a CLI command string for selecting a specific Linear issue.
|
|
171
|
+
*/
|
|
172
|
+
export function buildLinearIssueChoiceCommand(issueIdentifier, projectId) {
|
|
173
|
+
let command = `prlt work linear --issue ${issueIdentifier} --json`;
|
|
174
|
+
if (projectId) {
|
|
175
|
+
command += ` -P ${projectId}`;
|
|
176
|
+
}
|
|
177
|
+
return command;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Fetch a single Linear issue by identifier (for example, ENG-123) and normalize it.
|
|
181
|
+
*/
|
|
182
|
+
export async function getLinearIssueByIdentifier(configInput, identifier, options) {
|
|
183
|
+
const config = ensureLinearConfig(configInput);
|
|
184
|
+
const fetchImpl = options?.fetchImpl || fetch;
|
|
185
|
+
const response = await fetchImpl(config.apiUrl, {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers: {
|
|
188
|
+
'Content-Type': 'application/json',
|
|
189
|
+
Authorization: config.apiKey,
|
|
190
|
+
},
|
|
191
|
+
body: JSON.stringify({
|
|
192
|
+
query: LINEAR_ISSUE_BY_IDENTIFIER_QUERY,
|
|
193
|
+
variables: {
|
|
194
|
+
id: identifier,
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
if (response.status === 401 || response.status === 403) {
|
|
199
|
+
throw new ExternalIssueAdapterError('AUTH_FAILED', 'Linear authentication failed. Verify your LINEAR_API_KEY token.');
|
|
200
|
+
}
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
throw new ExternalIssueAdapterError('REQUEST_FAILED', `Linear request failed with status ${response.status}.`);
|
|
203
|
+
}
|
|
204
|
+
const payload = await response.json();
|
|
205
|
+
if (payload.errors && payload.errors.length > 0) {
|
|
206
|
+
const message = payload.errors[0]?.message || 'Unknown Linear API error.';
|
|
207
|
+
if (/auth|token|forbidden|unauthorized/i.test(message)) {
|
|
208
|
+
throw new ExternalIssueAdapterError('AUTH_FAILED', `Linear authentication failed: ${message}`);
|
|
209
|
+
}
|
|
210
|
+
// "not found" should be treated as null, not hard failure.
|
|
211
|
+
if (/not found/i.test(message)) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
throw new ExternalIssueAdapterError('REQUEST_FAILED', `Linear API error: ${message}`);
|
|
215
|
+
}
|
|
216
|
+
const node = payload.data?.issue;
|
|
217
|
+
if (!node)
|
|
218
|
+
return null;
|
|
219
|
+
return normalizeLinearIssueToEnvelope(node);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Fetch and normalize Linear issues into NormalizedIssueEnvelopes.
|
|
223
|
+
*/
|
|
224
|
+
export async function listLinearIssues(configInput, options) {
|
|
225
|
+
const config = ensureLinearConfig(configInput);
|
|
226
|
+
const fetchImpl = options?.fetchImpl || fetch;
|
|
227
|
+
const limit = Math.max(1, Math.min(options?.limit ?? 20, 100));
|
|
228
|
+
const response = await fetchImpl(config.apiUrl, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: {
|
|
231
|
+
'Content-Type': 'application/json',
|
|
232
|
+
Authorization: config.apiKey,
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
query: LINEAR_ISSUES_QUERY,
|
|
236
|
+
variables: {
|
|
237
|
+
teamKey: config.team,
|
|
238
|
+
first: limit,
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
});
|
|
242
|
+
if (response.status === 401 || response.status === 403) {
|
|
243
|
+
throw new ExternalIssueAdapterError('AUTH_FAILED', 'Linear authentication failed. Verify your LINEAR_API_KEY token.');
|
|
244
|
+
}
|
|
245
|
+
if (!response.ok) {
|
|
246
|
+
throw new ExternalIssueAdapterError('REQUEST_FAILED', `Linear request failed with status ${response.status}.`);
|
|
247
|
+
}
|
|
248
|
+
const payload = await response.json();
|
|
249
|
+
if (payload.errors && payload.errors.length > 0) {
|
|
250
|
+
const message = payload.errors[0]?.message || 'Unknown Linear API error.';
|
|
251
|
+
if (/auth|token|forbidden|unauthorized/i.test(message)) {
|
|
252
|
+
throw new ExternalIssueAdapterError('AUTH_FAILED', `Linear authentication failed: ${message}`);
|
|
253
|
+
}
|
|
254
|
+
throw new ExternalIssueAdapterError('REQUEST_FAILED', `Linear API error: ${message}`);
|
|
255
|
+
}
|
|
256
|
+
const nodes = payload.data?.issues?.nodes;
|
|
257
|
+
if (!Array.isArray(nodes)) {
|
|
258
|
+
throw new ExternalIssueAdapterError('BAD_PAYLOAD', 'Linear response payload was missing issues.nodes.', payload);
|
|
259
|
+
}
|
|
260
|
+
return nodes.map(normalizeLinearIssueToEnvelope);
|
|
261
|
+
}
|
|
@@ -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);
|
package/dist/lib/pmo/schema.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ export declare const PMO_TABLE_SCHEMAS: {
|
|
|
67
67
|
readonly project_specs: "\n CREATE TABLE IF NOT EXISTS pmo_project_specs (\n project_id TEXT NOT NULL REFERENCES pmo_projects(id) ON DELETE CASCADE,\n spec_id TEXT NOT NULL REFERENCES pmo_specs(id) ON DELETE CASCADE,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (project_id, spec_id)\n )";
|
|
68
68
|
readonly cache_metadata: "\n CREATE TABLE IF NOT EXISTS pmo_cache_metadata (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n )";
|
|
69
69
|
readonly settings: "\n CREATE TABLE IF NOT EXISTS pmo_settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n )";
|
|
70
|
-
readonly agent_work: "\n CREATE TABLE IF NOT EXISTS agent_work (\n id TEXT PRIMARY KEY,\n ticket_id TEXT NOT NULL,\n agent_name TEXT NOT NULL,\n executor TEXT NOT NULL,\n environment TEXT NOT NULL DEFAULT 'host',\n display_mode TEXT NOT NULL DEFAULT 'terminal',\n
|
|
70
|
+
readonly agent_work: "\n CREATE TABLE IF NOT EXISTS agent_work (\n id TEXT PRIMARY KEY,\n ticket_id TEXT NOT NULL,\n agent_name TEXT NOT NULL,\n executor TEXT NOT NULL,\n environment TEXT NOT NULL DEFAULT 'host',\n display_mode TEXT NOT NULL DEFAULT 'terminal',\n permission_mode TEXT NOT NULL DEFAULT 'safe',\n status TEXT NOT NULL DEFAULT 'starting',\n branch TEXT,\n pid TEXT,\n container_id TEXT,\n session_id TEXT,\n host TEXT,\n log_path TEXT,\n started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n completed_at TIMESTAMP,\n exit_code INTEGER,\n error_message TEXT,\n FOREIGN KEY (ticket_id) REFERENCES pmo_tickets(id) ON DELETE CASCADE\n )";
|
|
71
71
|
readonly containers: "\n CREATE TABLE IF NOT EXISTS containers (\n id TEXT PRIMARY KEY,\n agent_name TEXT NOT NULL,\n docker_id TEXT NOT NULL,\n docker_name TEXT,\n image TEXT,\n status TEXT NOT NULL DEFAULT 'unknown',\n current_execution_id TEXT,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n last_seen_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (agent_name) REFERENCES agents(name) ON DELETE CASCADE,\n FOREIGN KEY (current_execution_id) REFERENCES agent_work(id) ON DELETE SET NULL\n )";
|
|
72
72
|
readonly id_sequences: "\n CREATE TABLE IF NOT EXISTS id_sequences (\n table_name TEXT PRIMARY KEY,\n next_id INTEGER NOT NULL DEFAULT 1\n )";
|
|
73
73
|
readonly statuses: "\n CREATE TABLE IF NOT EXISTS pmo_statuses (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL,\n name TEXT NOT NULL,\n category TEXT NOT NULL,\n position INTEGER NOT NULL DEFAULT 0,\n color TEXT,\n description TEXT,\n is_default INTEGER NOT NULL DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (project_id) REFERENCES pmo_projects(id) ON DELETE CASCADE,\n UNIQUE(project_id, name)\n )";
|
package/dist/lib/pmo/schema.js
CHANGED
|
@@ -328,7 +328,7 @@ export const PMO_TABLE_SCHEMAS = {
|
|
|
328
328
|
executor TEXT NOT NULL,
|
|
329
329
|
environment TEXT NOT NULL DEFAULT 'host',
|
|
330
330
|
display_mode TEXT NOT NULL DEFAULT 'terminal',
|
|
331
|
-
|
|
331
|
+
permission_mode TEXT NOT NULL DEFAULT 'safe',
|
|
332
332
|
status TEXT NOT NULL DEFAULT 'starting',
|
|
333
333
|
branch TEXT,
|
|
334
334
|
pid TEXT,
|
|
@@ -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),
|
|
@@ -345,6 +345,37 @@ export function runMigrations(db) {
|
|
|
345
345
|
// Non-critical migration - don't fail initialization
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
|
+
// Migration: Rename sandboxed column to permission_mode in agent_work table
|
|
349
|
+
if (tableExists(T.agent_work)) {
|
|
350
|
+
const awColumns = db.pragma(`table_info(${T.agent_work})`);
|
|
351
|
+
const awColumnNames = new Set(awColumns.map(c => c.name));
|
|
352
|
+
if (awColumnNames.has('sandboxed') && !awColumnNames.has('permission_mode')) {
|
|
353
|
+
try {
|
|
354
|
+
db.exec(`ALTER TABLE ${T.agent_work} ADD COLUMN permission_mode TEXT NOT NULL DEFAULT 'safe'`);
|
|
355
|
+
// Migrate existing data: sandboxed=1 → 'safe', sandboxed=0 → 'danger'
|
|
356
|
+
db.exec(`UPDATE ${T.agent_work} SET permission_mode = CASE WHEN sandboxed = 1 THEN 'safe' ELSE 'danger' END`);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// Column may already exist
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Migration: Rename execution.sandboxed setting to execution.permission_mode
|
|
364
|
+
if (tableExists('workspace_settings')) {
|
|
365
|
+
try {
|
|
366
|
+
const oldSetting = db.prepare(`SELECT value FROM workspace_settings WHERE key = 'execution.sandboxed'`).get();
|
|
367
|
+
if (oldSetting) {
|
|
368
|
+
const permMode = oldSetting.value === 'true' ? 'safe' : 'danger';
|
|
369
|
+
db.prepare(`
|
|
370
|
+
INSERT INTO workspace_settings (key, value) VALUES ('execution.permission_mode', ?)
|
|
371
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
372
|
+
`).run(permMode);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// Non-critical migration
|
|
377
|
+
}
|
|
378
|
+
}
|
|
348
379
|
}
|
|
349
380
|
/**
|
|
350
381
|
* Seed built-in workflows from BUILTIN_TEMPLATES (single source of truth).
|
|
@@ -817,7 +848,16 @@ After reviewing, determine your verdict:
|
|
|
817
848
|
- **REQUEST_CHANGES**: There are issues that must be fixed before merging
|
|
818
849
|
- **COMMENT**: General feedback, no blocking issues but some suggestions
|
|
819
850
|
|
|
820
|
-
|
|
851
|
+
## STRICT RULES - READ CAREFULLY
|
|
852
|
+
|
|
853
|
+
- **DO NOT** merge the PR (\`gh pr merge\` is FORBIDDEN)
|
|
854
|
+
- **DO NOT** push any code (\`git push\` is FORBIDDEN)
|
|
855
|
+
- **DO NOT** run tests or test suites
|
|
856
|
+
- **DO NOT** modify, edit, or write any code files
|
|
857
|
+
- **DO NOT** create commits
|
|
858
|
+
- Your ONLY output should be a \`gh pr review\` comment
|
|
859
|
+
- This is a **read-only** review — read the diff, analyze it, post your review, and stop
|
|
860
|
+
|
|
821
861
|
If you identify issues that need fixing, describe them in your review. A separate action (Review & Fix) will handle fixes.
|
|
822
862
|
|
|
823
863
|
${PRLT_COMMANDS_COMMON}
|
|
@@ -872,7 +912,7 @@ COMMENT - Some suggestions but no blocking issues."
|
|
|
872
912
|
|
|
873
913
|
Format the body with: what looks good, concerns (if any), suggested improvements (if any), and your verdict.
|
|
874
914
|
|
|
875
|
-
|
|
915
|
+
**REMINDER:** Do NOT merge the PR. Do NOT run tests. Do NOT modify code. Only post the review comment above.
|
|
876
916
|
|
|
877
917
|
**After posting your review**, if you found issues that need fixing, log them on the ticket so another action can address them:
|
|
878
918
|
\`\`\`bash
|
|
@@ -884,6 +924,76 @@ prlt ticket edit <TICKET_ID> --add-subtask "Fix: <description of issue>"
|
|
|
884
924
|
modifiesCode: false,
|
|
885
925
|
position: 4,
|
|
886
926
|
},
|
|
927
|
+
{
|
|
928
|
+
id: 'review-comment',
|
|
929
|
+
name: 'Review Comment',
|
|
930
|
+
description: 'Post review comments on a PR without merging, testing, or modifying code',
|
|
931
|
+
prompt: `${PRLT_USAGE_RULE}
|
|
932
|
+
|
|
933
|
+
---
|
|
934
|
+
|
|
935
|
+
# Action: Review Comment
|
|
936
|
+
|
|
937
|
+
Read the PR diff, analyze the changes, and post a GitHub review comment. That is your ONLY job.
|
|
938
|
+
|
|
939
|
+
## STRICT RULES - READ CAREFULLY
|
|
940
|
+
|
|
941
|
+
You are a **read-only** reviewer. You MUST follow these rules:
|
|
942
|
+
|
|
943
|
+
- **DO NOT** run \`gh pr merge\` — merging is FORBIDDEN
|
|
944
|
+
- **DO NOT** run \`git push\` — pushing is FORBIDDEN
|
|
945
|
+
- **DO NOT** run tests or test suites of any kind
|
|
946
|
+
- **DO NOT** modify, edit, or write any code files
|
|
947
|
+
- **DO NOT** create commits or branches
|
|
948
|
+
- **DO NOT** run any commands that change repository state
|
|
949
|
+
- Your **ONLY** permitted write operation is \`gh pr review\` to post your comment
|
|
950
|
+
|
|
951
|
+
## What To Do
|
|
952
|
+
|
|
953
|
+
1. Read the PR diff to understand the changes
|
|
954
|
+
2. Analyze the code for bugs, issues, style, and correctness
|
|
955
|
+
3. Post your review using \`gh pr review\` with the appropriate verdict
|
|
956
|
+
4. **STOP** — do nothing else after posting the review
|
|
957
|
+
|
|
958
|
+
${PRLT_COMMANDS_COMMON}
|
|
959
|
+
${PRLT_COMMANDS_REVIEW}`,
|
|
960
|
+
endPrompt: `Post your review on the PR using \`gh pr review\`. This is the ONLY action you should take.
|
|
961
|
+
|
|
962
|
+
**If approving:**
|
|
963
|
+
\`\`\`bash
|
|
964
|
+
gh pr review --approve --body "## Review
|
|
965
|
+
|
|
966
|
+
### Summary
|
|
967
|
+
- ...
|
|
968
|
+
|
|
969
|
+
APPROVED - Looks good to merge."
|
|
970
|
+
\`\`\`
|
|
971
|
+
|
|
972
|
+
**If requesting changes:**
|
|
973
|
+
\`\`\`bash
|
|
974
|
+
gh pr review --request-changes --body "## Review
|
|
975
|
+
|
|
976
|
+
### Issues
|
|
977
|
+
- ...
|
|
978
|
+
|
|
979
|
+
REQUEST CHANGES - Please address the above."
|
|
980
|
+
\`\`\`
|
|
981
|
+
|
|
982
|
+
**If commenting:**
|
|
983
|
+
\`\`\`bash
|
|
984
|
+
gh pr review --comment --body "## Review
|
|
985
|
+
|
|
986
|
+
### Feedback
|
|
987
|
+
- ...
|
|
988
|
+
|
|
989
|
+
COMMENT - Some suggestions, no blockers."
|
|
990
|
+
\`\`\`
|
|
991
|
+
|
|
992
|
+
**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.`,
|
|
993
|
+
suggestedForCategories: ['completed'],
|
|
994
|
+
modifiesCode: false,
|
|
995
|
+
position: 5,
|
|
996
|
+
},
|
|
887
997
|
{
|
|
888
998
|
id: 'review-fix',
|
|
889
999
|
name: 'Review & Fix',
|
|
@@ -943,7 +1053,7 @@ ${PRLT_COMMANDS_REVIEW}`,
|
|
|
943
1053
|
\`\`\``,
|
|
944
1054
|
suggestedForCategories: ['started', 'completed'],
|
|
945
1055
|
modifiesCode: true,
|
|
946
|
-
position:
|
|
1056
|
+
position: 6,
|
|
947
1057
|
},
|
|
948
1058
|
{
|
|
949
1059
|
id: 'revise',
|
|
@@ -1007,7 +1117,7 @@ The PR will be updated automatically with your pushed changes.`,
|
|
|
1007
1117
|
suggestedForCategories: ['completed'],
|
|
1008
1118
|
defaultMoveToCategory: 'started',
|
|
1009
1119
|
modifiesCode: true,
|
|
1010
|
-
position:
|
|
1120
|
+
position: 7,
|
|
1011
1121
|
},
|
|
1012
1122
|
{
|
|
1013
1123
|
id: 'explore-cli',
|
|
@@ -1168,7 +1278,7 @@ tmux_kill_session({ session: "qa-test" })
|
|
|
1168
1278
|
\`\`\``,
|
|
1169
1279
|
suggestedForCategories: [],
|
|
1170
1280
|
modifiesCode: false,
|
|
1171
|
-
position:
|
|
1281
|
+
position: 9,
|
|
1172
1282
|
},
|
|
1173
1283
|
{
|
|
1174
1284
|
id: 'test',
|
|
@@ -1213,7 +1323,7 @@ ${PRLT_COMMANDS_CODE}`,
|
|
|
1213
1323
|
**IMPORTANT:** Use the global \`prlt\` command.`,
|
|
1214
1324
|
suggestedForCategories: ['started', 'completed'],
|
|
1215
1325
|
modifiesCode: true,
|
|
1216
|
-
position:
|
|
1326
|
+
position: 8,
|
|
1217
1327
|
},
|
|
1218
1328
|
];
|
|
1219
1329
|
// 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
|
}
|