@proletariat/cli 0.3.47 → 0.3.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/commands/caffeinate/index.d.ts +10 -0
  2. package/dist/commands/caffeinate/index.js +64 -0
  3. package/dist/commands/caffeinate/start.d.ts +14 -0
  4. package/dist/commands/caffeinate/start.js +86 -0
  5. package/dist/commands/caffeinate/status.d.ts +10 -0
  6. package/dist/commands/caffeinate/status.js +55 -0
  7. package/dist/commands/caffeinate/stop.d.ts +10 -0
  8. package/dist/commands/caffeinate/stop.js +47 -0
  9. package/dist/commands/commit.js +10 -8
  10. package/dist/commands/config/index.js +2 -3
  11. package/dist/commands/init.js +9 -1
  12. package/dist/commands/orchestrator/attach.js +64 -14
  13. package/dist/commands/orchestrator/start.d.ts +4 -4
  14. package/dist/commands/orchestrator/start.js +26 -16
  15. package/dist/commands/orchestrator/status.js +64 -23
  16. package/dist/commands/orchestrator/stop.js +44 -12
  17. package/dist/commands/session/attach.js +23 -0
  18. package/dist/commands/session/poke.js +1 -1
  19. package/dist/commands/work/index.js +4 -0
  20. package/dist/commands/work/linear.d.ts +24 -0
  21. package/dist/commands/work/linear.js +195 -0
  22. package/dist/commands/work/spawn.js +28 -19
  23. package/dist/commands/work/start.js +12 -2
  24. package/dist/hooks/init.js +8 -0
  25. package/dist/lib/caffeinate.d.ts +64 -0
  26. package/dist/lib/caffeinate.js +146 -0
  27. package/dist/lib/execution/codex-adapter.d.ts +96 -0
  28. package/dist/lib/execution/codex-adapter.js +148 -0
  29. package/dist/lib/execution/index.d.ts +1 -0
  30. package/dist/lib/execution/index.js +1 -0
  31. package/dist/lib/execution/runners.js +50 -6
  32. package/dist/lib/external-issues/index.d.ts +1 -1
  33. package/dist/lib/external-issues/index.js +1 -1
  34. package/dist/lib/external-issues/linear.d.ts +37 -0
  35. package/dist/lib/external-issues/linear.js +198 -0
  36. package/dist/lib/external-issues/types.d.ts +67 -0
  37. package/dist/lib/external-issues/types.js +41 -0
  38. package/dist/lib/init/index.d.ts +4 -0
  39. package/dist/lib/init/index.js +11 -1
  40. package/dist/lib/machine-config.d.ts +1 -0
  41. package/dist/lib/machine-config.js +6 -3
  42. package/dist/lib/pmo/storage/actions.js +3 -3
  43. package/dist/lib/pmo/storage/base.js +85 -6
  44. package/dist/lib/pmo/storage/epics.js +1 -1
  45. package/dist/lib/pmo/storage/tickets.js +2 -2
  46. package/dist/lib/pmo/storage/types.d.ts +2 -1
  47. package/oclif.manifest.json +4363 -4037
  48. package/package.json +1 -1
@@ -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
+ }
@@ -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
  */
@@ -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
- return path.join(os.homedir(), '.proletariat');
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(os.homedir(), inputPath.slice(1))
40
+ ? path.join(home, inputPath.slice(1))
38
41
  : inputPath;
39
42
  // Make absolute
40
43
  resolved = path.resolve(resolved);
@@ -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.default_category
169
- ? JSON.parse(row.default_category)
168
+ suggestedForCategories: row.suggested_for_categories
169
+ ? JSON.parse(row.suggested_for_categories)
170
170
  : undefined,
171
- defaultMoveToCategory: row.default_category,
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
- Do NOT modify any code. Do NOT attempt to fix any issues. This is a read-only review — report your findings only.
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
- No commits are needed for code review.
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: 5,
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: 6,
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: 8,
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: 7,
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 (changes.specId !== undefined) {
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 (changes.specId !== undefined)
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
- default_category: string | null;
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;