@blockspool/cli 0.3.3 → 0.4.1

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 (149) hide show
  1. package/package.json +1 -1
  2. package/dist/bin/blockspool.d.ts +0 -16
  3. package/dist/bin/blockspool.d.ts.map +0 -1
  4. package/dist/bin/blockspool.js +0 -45
  5. package/dist/bin/blockspool.js.map +0 -1
  6. package/dist/commands/solo-auto.d.ts +0 -6
  7. package/dist/commands/solo-auto.d.ts.map +0 -1
  8. package/dist/commands/solo-auto.js +0 -418
  9. package/dist/commands/solo-auto.js.map +0 -1
  10. package/dist/commands/solo-exec.d.ts +0 -6
  11. package/dist/commands/solo-exec.d.ts.map +0 -1
  12. package/dist/commands/solo-exec.js +0 -656
  13. package/dist/commands/solo-exec.js.map +0 -1
  14. package/dist/commands/solo-inspect.d.ts +0 -6
  15. package/dist/commands/solo-inspect.d.ts.map +0 -1
  16. package/dist/commands/solo-inspect.js +0 -690
  17. package/dist/commands/solo-inspect.js.map +0 -1
  18. package/dist/commands/solo-lifecycle.d.ts +0 -6
  19. package/dist/commands/solo-lifecycle.d.ts.map +0 -1
  20. package/dist/commands/solo-lifecycle.js +0 -188
  21. package/dist/commands/solo-lifecycle.js.map +0 -1
  22. package/dist/commands/solo-nudge.d.ts +0 -6
  23. package/dist/commands/solo-nudge.d.ts.map +0 -1
  24. package/dist/commands/solo-nudge.js +0 -49
  25. package/dist/commands/solo-nudge.js.map +0 -1
  26. package/dist/commands/solo-qa.d.ts +0 -6
  27. package/dist/commands/solo-qa.d.ts.map +0 -1
  28. package/dist/commands/solo-qa.js +0 -254
  29. package/dist/commands/solo-qa.js.map +0 -1
  30. package/dist/commands/solo.d.ts +0 -11
  31. package/dist/commands/solo.d.ts.map +0 -1
  32. package/dist/commands/solo.js +0 -43
  33. package/dist/commands/solo.js.map +0 -1
  34. package/dist/index.d.ts +0 -18
  35. package/dist/index.d.ts.map +0 -1
  36. package/dist/index.js +0 -18
  37. package/dist/index.js.map +0 -1
  38. package/dist/lib/artifacts.d.ts +0 -136
  39. package/dist/lib/artifacts.d.ts.map +0 -1
  40. package/dist/lib/artifacts.js +0 -146
  41. package/dist/lib/artifacts.js.map +0 -1
  42. package/dist/lib/doctor.d.ts +0 -45
  43. package/dist/lib/doctor.d.ts.map +0 -1
  44. package/dist/lib/doctor.js +0 -383
  45. package/dist/lib/doctor.js.map +0 -1
  46. package/dist/lib/exec.d.ts +0 -24
  47. package/dist/lib/exec.d.ts.map +0 -1
  48. package/dist/lib/exec.js +0 -295
  49. package/dist/lib/exec.js.map +0 -1
  50. package/dist/lib/formulas.d.ts +0 -78
  51. package/dist/lib/formulas.d.ts.map +0 -1
  52. package/dist/lib/formulas.js +0 -295
  53. package/dist/lib/formulas.js.map +0 -1
  54. package/dist/lib/git.d.ts +0 -9
  55. package/dist/lib/git.d.ts.map +0 -1
  56. package/dist/lib/git.js +0 -60
  57. package/dist/lib/git.js.map +0 -1
  58. package/dist/lib/guidelines.d.ts +0 -43
  59. package/dist/lib/guidelines.d.ts.map +0 -1
  60. package/dist/lib/guidelines.js +0 -195
  61. package/dist/lib/guidelines.js.map +0 -1
  62. package/dist/lib/logger.d.ts +0 -17
  63. package/dist/lib/logger.d.ts.map +0 -1
  64. package/dist/lib/logger.js +0 -42
  65. package/dist/lib/logger.js.map +0 -1
  66. package/dist/lib/retention.d.ts +0 -62
  67. package/dist/lib/retention.d.ts.map +0 -1
  68. package/dist/lib/retention.js +0 -285
  69. package/dist/lib/retention.js.map +0 -1
  70. package/dist/lib/run-history.d.ts +0 -52
  71. package/dist/lib/run-history.d.ts.map +0 -1
  72. package/dist/lib/run-history.js +0 -116
  73. package/dist/lib/run-history.js.map +0 -1
  74. package/dist/lib/run-state.d.ts +0 -58
  75. package/dist/lib/run-state.d.ts.map +0 -1
  76. package/dist/lib/run-state.js +0 -119
  77. package/dist/lib/run-state.js.map +0 -1
  78. package/dist/lib/scope.d.ts +0 -95
  79. package/dist/lib/scope.d.ts.map +0 -1
  80. package/dist/lib/scope.js +0 -291
  81. package/dist/lib/scope.js.map +0 -1
  82. package/dist/lib/selection.d.ts +0 -35
  83. package/dist/lib/selection.d.ts.map +0 -1
  84. package/dist/lib/selection.js +0 -110
  85. package/dist/lib/selection.js.map +0 -1
  86. package/dist/lib/solo-auto.d.ts +0 -87
  87. package/dist/lib/solo-auto.d.ts.map +0 -1
  88. package/dist/lib/solo-auto.js +0 -1230
  89. package/dist/lib/solo-auto.js.map +0 -1
  90. package/dist/lib/solo-ci.d.ts +0 -84
  91. package/dist/lib/solo-ci.d.ts.map +0 -1
  92. package/dist/lib/solo-ci.js +0 -300
  93. package/dist/lib/solo-ci.js.map +0 -1
  94. package/dist/lib/solo-config.d.ts +0 -153
  95. package/dist/lib/solo-config.d.ts.map +0 -1
  96. package/dist/lib/solo-config.js +0 -236
  97. package/dist/lib/solo-config.js.map +0 -1
  98. package/dist/lib/solo-git.d.ts +0 -44
  99. package/dist/lib/solo-git.d.ts.map +0 -1
  100. package/dist/lib/solo-git.js +0 -174
  101. package/dist/lib/solo-git.js.map +0 -1
  102. package/dist/lib/solo-hints.d.ts +0 -32
  103. package/dist/lib/solo-hints.d.ts.map +0 -1
  104. package/dist/lib/solo-hints.js +0 -98
  105. package/dist/lib/solo-hints.js.map +0 -1
  106. package/dist/lib/solo-remote.d.ts +0 -14
  107. package/dist/lib/solo-remote.d.ts.map +0 -1
  108. package/dist/lib/solo-remote.js +0 -48
  109. package/dist/lib/solo-remote.js.map +0 -1
  110. package/dist/lib/solo-stdin.d.ts +0 -13
  111. package/dist/lib/solo-stdin.d.ts.map +0 -1
  112. package/dist/lib/solo-stdin.js +0 -33
  113. package/dist/lib/solo-stdin.js.map +0 -1
  114. package/dist/lib/solo-ticket.d.ts +0 -213
  115. package/dist/lib/solo-ticket.d.ts.map +0 -1
  116. package/dist/lib/solo-ticket.js +0 -850
  117. package/dist/lib/solo-ticket.js.map +0 -1
  118. package/dist/lib/solo-utils.d.ts +0 -133
  119. package/dist/lib/solo-utils.d.ts.map +0 -1
  120. package/dist/lib/solo-utils.js +0 -300
  121. package/dist/lib/solo-utils.js.map +0 -1
  122. package/dist/lib/spindle.d.ts +0 -144
  123. package/dist/lib/spindle.d.ts.map +0 -1
  124. package/dist/lib/spindle.js +0 -388
  125. package/dist/lib/spindle.js.map +0 -1
  126. package/dist/tui/app.d.ts +0 -17
  127. package/dist/tui/app.d.ts.map +0 -1
  128. package/dist/tui/app.js +0 -139
  129. package/dist/tui/app.js.map +0 -1
  130. package/dist/tui/index.d.ts +0 -8
  131. package/dist/tui/index.d.ts.map +0 -1
  132. package/dist/tui/index.js +0 -7
  133. package/dist/tui/index.js.map +0 -1
  134. package/dist/tui/poller.d.ts +0 -42
  135. package/dist/tui/poller.d.ts.map +0 -1
  136. package/dist/tui/poller.js +0 -62
  137. package/dist/tui/poller.js.map +0 -1
  138. package/dist/tui/screens/overview.d.ts +0 -9
  139. package/dist/tui/screens/overview.d.ts.map +0 -1
  140. package/dist/tui/screens/overview.js +0 -189
  141. package/dist/tui/screens/overview.js.map +0 -1
  142. package/dist/tui/state.d.ts +0 -93
  143. package/dist/tui/state.d.ts.map +0 -1
  144. package/dist/tui/state.js +0 -169
  145. package/dist/tui/state.js.map +0 -1
  146. package/dist/tui/types.d.ts +0 -18
  147. package/dist/tui/types.d.ts.map +0 -1
  148. package/dist/tui/types.js +0 -5
  149. package/dist/tui/types.js.map +0 -1
@@ -1,656 +0,0 @@
1
- /**
2
- * Solo execution commands: run, retry, pr
3
- */
4
- import * as path from 'node:path';
5
- import chalk from 'chalk';
6
- import { tickets, runs } from '@blockspool/core/repos';
7
- import { createGitService } from '../lib/git.js';
8
- import { isInitialized, initSolo, loadConfig, getAdapter, } from '../lib/solo-config.js';
9
- import { formatDuration, findConflictingTickets, regenerateAllowedPaths, runPreflightChecks, } from '../lib/solo-utils.js';
10
- import { cleanupWorktree } from '../lib/solo-git.js';
11
- import { soloRunTicket } from '../lib/solo-ticket.js';
12
- import { EXIT_CODES } from '../lib/solo-ticket.js';
13
- export function registerExecCommands(solo) {
14
- /**
15
- * solo run - Execute a ticket using Claude
16
- */
17
- solo
18
- .command('run <ticketId>')
19
- .description('Execute a ticket using Claude Code CLI')
20
- .addHelpText('after', `
21
- This command:
22
- 1. Creates an isolated git worktree
23
- 2. Runs Claude Code CLI with the ticket prompt
24
- 3. Validates changes with QA commands
25
- 4. Creates a PR (or commits to a feature branch)
26
-
27
- Rerun behavior:
28
- - ready/blocked tickets: runs normally
29
- - in_progress tickets: warns about possible crashed run, continues
30
- - done/in_review tickets: skips (use --force to override)
31
-
32
- Examples:
33
- blockspool solo run tkt_abc123 # Run ticket
34
- blockspool solo run tkt_abc123 --pr # Create PR after success
35
- blockspool solo run tkt_abc123 --force # Force rerun of completed ticket
36
- `)
37
- .option('-v, --verbose', 'Show detailed output')
38
- .option('--json', 'Output as JSON')
39
- .option('--pr', 'Create PR after successful run')
40
- .option('--no-qa', 'Skip QA validation')
41
- .option('--timeout <ms>', 'Claude execution timeout', '600000')
42
- .option('-f, --force', 'Force rerun of done/in_review tickets')
43
- .action(async (ticketId, options) => {
44
- const isJsonMode = options.json;
45
- const skipQa = options.qa === false;
46
- const createPr = options.pr ?? false;
47
- const timeoutMs = parseInt(options.timeout ?? '600000', 10);
48
- const forceRerun = options.force ?? false;
49
- if (!isJsonMode) {
50
- console.log(chalk.blue('🚀 BlockSpool Solo Run'));
51
- console.log();
52
- }
53
- const git = createGitService();
54
- const repoRoot = await git.findRepoRoot(process.cwd());
55
- if (!repoRoot) {
56
- if (isJsonMode) {
57
- console.log(JSON.stringify({ success: false, error: 'Not a git repository' }));
58
- }
59
- else {
60
- console.error(chalk.red('✗ Not a git repository'));
61
- }
62
- process.exit(1);
63
- }
64
- const preflight = await runPreflightChecks(repoRoot, { needsPr: createPr });
65
- if (!preflight.ok) {
66
- if (isJsonMode) {
67
- console.log(JSON.stringify({ success: false, error: preflight.error }));
68
- }
69
- else {
70
- console.error(chalk.red(`✗ ${preflight.error}`));
71
- }
72
- process.exit(1);
73
- }
74
- for (const warning of preflight.warnings) {
75
- if (!isJsonMode) {
76
- console.log(chalk.yellow(`⚠ ${warning}`));
77
- }
78
- }
79
- if (!isInitialized(repoRoot)) {
80
- if (!isJsonMode) {
81
- console.log(chalk.gray('Initializing local state...'));
82
- }
83
- await initSolo(repoRoot);
84
- }
85
- const config = loadConfig(repoRoot);
86
- const adapter = await getAdapter(repoRoot);
87
- let currentRunId = null;
88
- let interrupted = false;
89
- const sigintHandler = async () => {
90
- if (interrupted) {
91
- process.exit(1);
92
- }
93
- interrupted = true;
94
- if (!isJsonMode) {
95
- console.log(chalk.yellow('\n\nInterrupted. Cleaning up...'));
96
- }
97
- const worktreePath = path.join(repoRoot, '.blockspool', 'worktrees', ticketId);
98
- await cleanupWorktree(repoRoot, worktreePath);
99
- try {
100
- await tickets.updateStatus(adapter, ticketId, 'ready');
101
- if (currentRunId) {
102
- await runs.markFailure(adapter, currentRunId, 'Interrupted by user (SIGINT)');
103
- }
104
- }
105
- catch {
106
- // Ignore cleanup errors
107
- }
108
- if (!isJsonMode) {
109
- console.log(chalk.gray('Ticket reset to ready. You can retry with: blockspool solo run ' + ticketId));
110
- }
111
- await adapter.close();
112
- process.exit(130);
113
- };
114
- process.on('SIGINT', sigintHandler);
115
- try {
116
- const ticket = await tickets.getById(adapter, ticketId);
117
- if (!ticket) {
118
- if (isJsonMode) {
119
- console.log(JSON.stringify({ success: false, error: `Ticket not found: ${ticketId}` }));
120
- }
121
- else {
122
- console.error(chalk.red(`✗ Ticket not found: ${ticketId}`));
123
- }
124
- process.exit(1);
125
- }
126
- if (!isJsonMode) {
127
- console.log(`Ticket: ${chalk.bold(ticket.title)}`);
128
- console.log(chalk.gray(` ID: ${ticket.id}`));
129
- console.log(chalk.gray(` Status: ${ticket.status}`));
130
- console.log();
131
- }
132
- if (ticket.status === 'done' || ticket.status === 'in_review') {
133
- if (!forceRerun) {
134
- if (isJsonMode) {
135
- console.log(JSON.stringify({
136
- success: false,
137
- error: `Ticket already ${ticket.status}. Use --force to rerun.`,
138
- }));
139
- }
140
- else {
141
- console.log(chalk.yellow(`Ticket already ${ticket.status}. Use --force to rerun.`));
142
- }
143
- return;
144
- }
145
- if (!isJsonMode) {
146
- console.log(chalk.yellow(`⚠ Force rerunning ${ticket.status} ticket`));
147
- }
148
- }
149
- if (ticket.status === 'in_progress') {
150
- if (!isJsonMode) {
151
- console.log(chalk.yellow('⚠ Ticket was in_progress (previous run may have crashed)'));
152
- console.log(chalk.gray(' Cleaning up and retrying...'));
153
- console.log();
154
- }
155
- const worktreePath = path.join(repoRoot, '.blockspool', 'worktrees', ticketId);
156
- await cleanupWorktree(repoRoot, worktreePath);
157
- }
158
- const conflicts = await findConflictingTickets(adapter, ticket);
159
- if (conflicts.length > 0 && !forceRerun) {
160
- if (isJsonMode) {
161
- console.log(JSON.stringify({
162
- success: false,
163
- error: 'Conflicting tickets detected with overlapping paths',
164
- conflicts: conflicts.map(c => ({
165
- ticketId: c.ticket.id,
166
- title: c.ticket.title,
167
- overlappingPaths: c.overlappingPaths,
168
- })),
169
- }));
170
- }
171
- else {
172
- console.log(chalk.yellow('⚠ Conflicting tickets detected with overlapping paths:'));
173
- console.log();
174
- for (const conflict of conflicts) {
175
- console.log(chalk.yellow(` • ${conflict.ticket.id}: ${conflict.ticket.title}`));
176
- console.log(chalk.gray(` Status: ${conflict.ticket.status}`));
177
- console.log(chalk.gray(` Overlapping paths:`));
178
- for (const overlap of conflict.overlappingPaths.slice(0, 5)) {
179
- console.log(chalk.gray(` - ${overlap}`));
180
- }
181
- if (conflict.overlappingPaths.length > 5) {
182
- console.log(chalk.gray(` ... and ${conflict.overlappingPaths.length - 5} more`));
183
- }
184
- }
185
- console.log();
186
- console.log(chalk.yellow('Running tickets that modify the same files may cause merge conflicts.'));
187
- console.log(chalk.gray('Use --force to run anyway.'));
188
- }
189
- process.exit(1);
190
- }
191
- else if (conflicts.length > 0 && !isJsonMode) {
192
- console.log(chalk.yellow('⚠ Running despite conflicting tickets (--force):'));
193
- for (const conflict of conflicts) {
194
- console.log(chalk.gray(` • ${conflict.ticket.id}: ${conflict.ticket.title}`));
195
- }
196
- console.log();
197
- }
198
- await tickets.updateStatus(adapter, ticketId, 'in_progress');
199
- const run = await runs.create(adapter, {
200
- projectId: ticket.projectId,
201
- type: 'worker',
202
- ticketId: ticket.id,
203
- metadata: {
204
- skipQa,
205
- createPr,
206
- timeoutMs,
207
- },
208
- });
209
- currentRunId = run.id;
210
- if (!isJsonMode) {
211
- console.log(chalk.gray(`Run: ${run.id}`));
212
- console.log();
213
- }
214
- const result = await soloRunTicket({
215
- ticket,
216
- repoRoot,
217
- config,
218
- adapter,
219
- runId: run.id,
220
- skipQa,
221
- createPr,
222
- timeoutMs,
223
- verbose: options.verbose ?? false,
224
- onProgress: (msg) => {
225
- if (!isJsonMode && !interrupted) {
226
- console.log(chalk.gray(` ${msg}`));
227
- }
228
- },
229
- });
230
- if (interrupted) {
231
- return;
232
- }
233
- if (result.success) {
234
- await runs.markSuccess(adapter, run.id, {
235
- branchName: result.branchName,
236
- prUrl: result.prUrl,
237
- durationMs: result.durationMs,
238
- completionOutcome: result.completionOutcome,
239
- });
240
- await tickets.updateStatus(adapter, ticketId, result.prUrl ? 'in_review' : 'done');
241
- }
242
- else {
243
- await runs.markFailure(adapter, run.id, result.error ?? 'Unknown error', {
244
- durationMs: result.durationMs,
245
- branchName: result.branchName,
246
- });
247
- await tickets.updateStatus(adapter, ticketId, 'blocked');
248
- }
249
- if (isJsonMode) {
250
- const jsonOutput = {
251
- success: result.success,
252
- runId: run.id,
253
- ticketId: ticket.id,
254
- branchName: result.branchName,
255
- prUrl: result.prUrl,
256
- durationMs: result.durationMs,
257
- error: result.error,
258
- failureReason: result.failureReason,
259
- completionOutcome: result.completionOutcome,
260
- artifacts: result.artifacts,
261
- };
262
- if (result.failureReason === 'spindle_abort' && result.spindle) {
263
- jsonOutput.spindle = {
264
- trigger: result.spindle.trigger,
265
- estimatedTokens: result.spindle.estimatedTokens,
266
- threshold: result.spindle.thresholds.tokenBudgetAbort,
267
- iteration: result.spindle.iteration,
268
- confidence: result.spindle.confidence,
269
- };
270
- }
271
- console.log(JSON.stringify(jsonOutput, null, 2));
272
- }
273
- else {
274
- console.log();
275
- if (result.success && result.completionOutcome === 'no_changes_needed') {
276
- console.log(chalk.green('✓ Ticket completed - no changes needed'));
277
- console.log(chalk.gray(' Claude reviewed the ticket and determined no code changes were required'));
278
- }
279
- else if (result.success) {
280
- console.log(chalk.green('✓ Ticket completed successfully'));
281
- if (result.branchName) {
282
- console.log(chalk.gray(` Branch: ${result.branchName}`));
283
- }
284
- if (result.prUrl) {
285
- console.log(chalk.cyan(` PR: ${result.prUrl}`));
286
- }
287
- }
288
- else if (result.failureReason === 'spindle_abort' && result.spindle) {
289
- console.log(chalk.yellow('⚠ Execution stopped by Spindle (loop protection)'));
290
- console.log();
291
- console.log(chalk.bold('What happened:'));
292
- console.log(` Stopped execution to prevent ${result.spindle.trigger}`);
293
- console.log();
294
- console.log(chalk.bold('Why:'));
295
- if (result.spindle.trigger === 'token_budget') {
296
- console.log(` Token estimate ~${result.spindle.estimatedTokens.toLocaleString()} > abort limit ${result.spindle.thresholds.tokenBudgetAbort.toLocaleString()}`);
297
- }
298
- else if (result.spindle.trigger === 'stalling') {
299
- console.log(` ${result.spindle.metrics.iterationsWithoutChange} iterations without meaningful changes`);
300
- }
301
- else if (result.spindle.trigger === 'oscillation') {
302
- console.log(` Detected flip-flopping: ${result.spindle.metrics.oscillationPattern ?? 'add→remove→add pattern'}`);
303
- }
304
- else if (result.spindle.trigger === 'repetition') {
305
- console.log(` Similar outputs detected (${(result.spindle.confidence * 100).toFixed(0)}% similarity)`);
306
- }
307
- console.log();
308
- console.log(chalk.bold('What to do:'));
309
- for (const rec of result.spindle.recommendations.slice(0, 3)) {
310
- console.log(chalk.gray(` • ${rec}`));
311
- }
312
- console.log();
313
- console.log(chalk.gray(` Artifacts: ${result.spindle.artifactPath}`));
314
- }
315
- else {
316
- console.log(chalk.red(`✗ Ticket failed: ${result.error}`));
317
- if (result.branchName) {
318
- console.log(chalk.gray(` Branch preserved: ${result.branchName}`));
319
- console.log(chalk.gray(' Inspect with: git checkout ' + result.branchName));
320
- }
321
- console.log(chalk.gray(' Retry with: blockspool solo run ' + ticketId));
322
- }
323
- console.log(chalk.gray(` Duration: ${formatDuration(result.durationMs)}`));
324
- }
325
- if (!result.success) {
326
- if (result.failureReason === 'spindle_abort') {
327
- process.exitCode = EXIT_CODES.SPINDLE_ABORT;
328
- }
329
- else {
330
- process.exitCode = EXIT_CODES.FAILURE;
331
- }
332
- }
333
- }
334
- finally {
335
- process.removeListener('SIGINT', sigintHandler);
336
- await adapter.close();
337
- }
338
- });
339
- /**
340
- * solo retry - Reset a blocked ticket to ready status
341
- */
342
- solo
343
- .command('retry <ticketId>')
344
- .description('Reset a blocked ticket to ready status and regenerate allowed_paths')
345
- .addHelpText('after', `
346
- This command resets a blocked ticket so it can be run again.
347
-
348
- What it does:
349
- 1. Resets the ticket status to 'ready'
350
- 2. Regenerates allowed_paths using current scope expansion logic
351
- 3. Optionally allows updating the ticket description
352
-
353
- Use this when:
354
- - A ticket failed and is now blocked
355
- - You want to retry with regenerated scope
356
- - You want to update the ticket description before retrying
357
-
358
- Examples:
359
- blockspool solo retry tkt_abc123 # Reset blocked ticket
360
- blockspool solo retry tkt_abc123 -d "New desc" # Reset with new description
361
- blockspool solo retry tkt_abc123 --force # Reset even if not blocked
362
- `)
363
- .option('-v, --verbose', 'Show detailed output')
364
- .option('--json', 'Output as JSON')
365
- .option('-d, --description <text>', 'Update the ticket description')
366
- .option('-f, --force', 'Force reset even if ticket is not blocked')
367
- .action(async (ticketId, options) => {
368
- const isJsonMode = options.json;
369
- const forceReset = options.force ?? false;
370
- const newDescription = options.description;
371
- if (!isJsonMode) {
372
- console.log(chalk.blue('🔄 BlockSpool Solo Retry'));
373
- console.log();
374
- }
375
- const git = createGitService();
376
- const repoRoot = await git.findRepoRoot(process.cwd());
377
- if (!repoRoot) {
378
- if (isJsonMode) {
379
- console.log(JSON.stringify({ success: false, error: 'Not a git repository' }));
380
- }
381
- else {
382
- console.error(chalk.red('✗ Not a git repository'));
383
- }
384
- process.exit(1);
385
- }
386
- if (!isInitialized(repoRoot)) {
387
- if (isJsonMode) {
388
- console.log(JSON.stringify({ success: false, error: 'Not initialized. Run: blockspool solo init' }));
389
- }
390
- else {
391
- console.error(chalk.red('✗ Not initialized'));
392
- console.log(chalk.gray(' Run: blockspool solo init'));
393
- }
394
- process.exit(1);
395
- }
396
- const adapter = await getAdapter(repoRoot);
397
- try {
398
- const ticket = await tickets.getById(adapter, ticketId);
399
- if (!ticket) {
400
- if (isJsonMode) {
401
- console.log(JSON.stringify({ success: false, error: `Ticket not found: ${ticketId}` }));
402
- }
403
- else {
404
- console.error(chalk.red(`✗ Ticket not found: ${ticketId}`));
405
- }
406
- process.exit(1);
407
- }
408
- if (options.verbose) {
409
- console.log(chalk.gray(` Ticket: ${ticket.title}`));
410
- console.log(chalk.gray(` Current status: ${ticket.status}`));
411
- console.log(chalk.gray(` Category: ${ticket.category ?? 'none'}`));
412
- }
413
- if (ticket.status !== 'blocked' && !forceReset) {
414
- if (isJsonMode) {
415
- console.log(JSON.stringify({
416
- success: false,
417
- error: `Ticket is ${ticket.status}, not blocked. Use --force to reset anyway.`,
418
- }));
419
- }
420
- else {
421
- console.error(chalk.yellow(`⚠ Ticket is ${ticket.status}, not blocked`));
422
- console.log(chalk.gray(' Use --force to reset anyway'));
423
- }
424
- process.exit(1);
425
- }
426
- const newAllowedPaths = regenerateAllowedPaths(ticket);
427
- const updates = [];
428
- const params = [];
429
- let paramIndex = 1;
430
- updates.push(`status = $${paramIndex++}`);
431
- params.push('ready');
432
- updates.push(`allowed_paths = $${paramIndex++}`);
433
- params.push(JSON.stringify(newAllowedPaths));
434
- if (newDescription !== undefined) {
435
- updates.push(`description = $${paramIndex++}`);
436
- params.push(newDescription);
437
- }
438
- updates.push(`updated_at = datetime('now')`);
439
- params.push(ticketId);
440
- await adapter.query(`UPDATE tickets SET ${updates.join(', ')} WHERE id = $${paramIndex}`, params);
441
- const updatedTicket = await tickets.getById(adapter, ticketId);
442
- if (isJsonMode) {
443
- console.log(JSON.stringify({
444
- success: true,
445
- ticket: {
446
- id: updatedTicket.id,
447
- title: updatedTicket.title,
448
- status: updatedTicket.status,
449
- allowedPaths: updatedTicket.allowedPaths,
450
- description: updatedTicket.description,
451
- },
452
- changes: {
453
- statusFrom: ticket.status,
454
- statusTo: 'ready',
455
- allowedPathsCount: newAllowedPaths.length,
456
- descriptionUpdated: newDescription !== undefined,
457
- },
458
- }));
459
- }
460
- else {
461
- console.log(chalk.green('✓ Ticket reset successfully'));
462
- console.log();
463
- console.log(chalk.gray(` ID: ${updatedTicket.id}`));
464
- console.log(chalk.gray(` Title: ${updatedTicket.title}`));
465
- console.log(chalk.gray(` Status: ${ticket.status} → ready`));
466
- console.log(chalk.gray(` Allowed paths: ${newAllowedPaths.length} paths`));
467
- if (options.verbose && newAllowedPaths.length > 0) {
468
- for (const p of newAllowedPaths.slice(0, 5)) {
469
- console.log(chalk.gray(` - ${p}`));
470
- }
471
- if (newAllowedPaths.length > 5) {
472
- console.log(chalk.gray(` ... and ${newAllowedPaths.length - 5} more`));
473
- }
474
- }
475
- if (newDescription !== undefined) {
476
- console.log(chalk.gray(` Description: updated`));
477
- }
478
- console.log();
479
- console.log(chalk.blue('Next step:'));
480
- console.log(` blockspool solo run ${ticketId}`);
481
- }
482
- }
483
- finally {
484
- await adapter.close();
485
- }
486
- });
487
- /**
488
- * solo pr - Create PR for completed ticket
489
- */
490
- solo
491
- .command('pr <ticketId>')
492
- .description('Create a PR for a completed ticket branch')
493
- .addHelpText('after', `
494
- This command creates a PR for a ticket that was completed without --pr.
495
-
496
- Use this when:
497
- - A ticket ran successfully but --pr was not specified
498
- - The branch was pushed but PR creation was skipped
499
-
500
- Examples:
501
- blockspool solo pr tkt_abc123 # Create PR for ticket's branch
502
- `)
503
- .option('-v, --verbose', 'Show detailed output')
504
- .option('--json', 'Output as JSON')
505
- .action(async (ticketId, options) => {
506
- const isJsonMode = options.json;
507
- if (!isJsonMode) {
508
- console.log(chalk.blue('🔗 BlockSpool Solo PR'));
509
- console.log();
510
- }
511
- const git = createGitService();
512
- const repoRoot = await git.findRepoRoot(process.cwd());
513
- if (!repoRoot) {
514
- if (isJsonMode) {
515
- console.log(JSON.stringify({ success: false, error: 'Not a git repository' }));
516
- }
517
- else {
518
- console.error(chalk.red('✗ Not a git repository'));
519
- }
520
- process.exit(1);
521
- }
522
- const preflightResult = await runPreflightChecks(repoRoot, { needsPr: true });
523
- if (!preflightResult.ok) {
524
- if (isJsonMode) {
525
- console.log(JSON.stringify({ success: false, error: preflightResult.error }));
526
- }
527
- else {
528
- console.error(chalk.red(`✗ ${preflightResult.error}`));
529
- }
530
- process.exit(1);
531
- }
532
- if (!isInitialized(repoRoot)) {
533
- if (isJsonMode) {
534
- console.log(JSON.stringify({ success: false, error: 'Solo mode not initialized. Run: blockspool solo init' }));
535
- }
536
- else {
537
- console.error(chalk.red('✗ Solo mode not initialized. Run: blockspool solo init'));
538
- }
539
- process.exit(1);
540
- }
541
- const adapter = await getAdapter(repoRoot);
542
- try {
543
- const ticket = await tickets.getById(adapter, ticketId);
544
- if (!ticket) {
545
- if (isJsonMode) {
546
- console.log(JSON.stringify({ success: false, error: `Ticket not found: ${ticketId}` }));
547
- }
548
- else {
549
- console.error(chalk.red(`✗ Ticket not found: ${ticketId}`));
550
- }
551
- process.exit(1);
552
- }
553
- if (!isJsonMode) {
554
- console.log(`Ticket: ${chalk.bold(ticket.title)}`);
555
- console.log(chalk.gray(` ID: ${ticket.id}`));
556
- console.log(chalk.gray(` Status: ${ticket.status}`));
557
- console.log();
558
- }
559
- const result = await adapter.query(`SELECT id, metadata FROM runs
560
- WHERE ticket_id = $1 AND status = 'success' AND type = 'worker'
561
- ORDER BY completed_at DESC
562
- LIMIT 1`, [ticketId]);
563
- const runRow = result.rows[0];
564
- if (!runRow) {
565
- if (isJsonMode) {
566
- console.log(JSON.stringify({ success: false, error: 'No successful run found for this ticket' }));
567
- }
568
- else {
569
- console.error(chalk.red('✗ No successful run found for this ticket'));
570
- console.log(chalk.gray(' The ticket must have completed successfully before creating a PR.'));
571
- }
572
- process.exit(1);
573
- }
574
- const metadata = runRow.metadata ? JSON.parse(runRow.metadata) : {};
575
- const branchName = metadata.branchName;
576
- if (!branchName) {
577
- if (isJsonMode) {
578
- console.log(JSON.stringify({ success: false, error: 'No branch name found in run metadata' }));
579
- }
580
- else {
581
- console.error(chalk.red('✗ No branch name found in run metadata'));
582
- }
583
- process.exit(1);
584
- }
585
- if (metadata.prUrl) {
586
- if (isJsonMode) {
587
- console.log(JSON.stringify({ success: true, prUrl: metadata.prUrl, alreadyExists: true }));
588
- }
589
- else {
590
- console.log(chalk.yellow(`PR already exists: ${metadata.prUrl}`));
591
- }
592
- return;
593
- }
594
- if (!isJsonMode) {
595
- console.log(chalk.gray(`Branch: ${branchName}`));
596
- console.log(chalk.gray('Creating PR...'));
597
- }
598
- const { execSync } = await import('child_process');
599
- try {
600
- execSync(`git ls-remote --heads origin ${branchName}`, { cwd: repoRoot, encoding: 'utf-8', stdio: 'pipe' });
601
- }
602
- catch {
603
- if (isJsonMode) {
604
- console.log(JSON.stringify({ success: false, error: `Branch not found on remote: ${branchName}` }));
605
- }
606
- else {
607
- console.error(chalk.red(`✗ Branch not found on remote: ${branchName}`));
608
- console.log(chalk.gray(' The branch must be pushed to the remote before creating a PR.'));
609
- }
610
- process.exit(1);
611
- }
612
- try {
613
- const prBody = `## Summary\n\n${ticket.description ?? ticket.title}\n\n---\n_Created by BlockSpool_`;
614
- const prOutput = execSync(`gh pr create --title "${ticket.title.replace(/"/g, '\\"')}" --body "${prBody.replace(/"/g, '\\"')}" --head "${branchName}"`, { cwd: repoRoot, encoding: 'utf-8' }).trim();
615
- const urlMatch = prOutput.match(/https:\/\/github\.com\/[^\s]+/);
616
- const prUrl = urlMatch ? urlMatch[0] : undefined;
617
- if (prUrl) {
618
- const existingMetadata = runRow.metadata ? JSON.parse(runRow.metadata) : {};
619
- const updatedMetadata = { ...existingMetadata, prUrl };
620
- await adapter.query(`UPDATE runs SET metadata = $1 WHERE id = $2`, [JSON.stringify(updatedMetadata), runRow.id]);
621
- await tickets.updateStatus(adapter, ticketId, 'in_review');
622
- if (isJsonMode) {
623
- console.log(JSON.stringify({ success: true, prUrl, branchName }));
624
- }
625
- else {
626
- console.log();
627
- console.log(chalk.green('✓ PR created successfully'));
628
- console.log(chalk.cyan(` ${prUrl}`));
629
- }
630
- }
631
- else {
632
- if (isJsonMode) {
633
- console.log(JSON.stringify({ success: false, error: 'PR created but could not parse URL' }));
634
- }
635
- else {
636
- console.log(chalk.yellow('⚠ PR created but could not parse URL'));
637
- console.log(chalk.gray(` Output: ${prOutput}`));
638
- }
639
- }
640
- }
641
- catch (prError) {
642
- if (isJsonMode) {
643
- console.log(JSON.stringify({ success: false, error: prError instanceof Error ? prError.message : String(prError) }));
644
- }
645
- else {
646
- console.error(chalk.red(`✗ Failed to create PR: ${prError instanceof Error ? prError.message : prError}`));
647
- }
648
- process.exit(1);
649
- }
650
- }
651
- finally {
652
- await adapter.close();
653
- }
654
- });
655
- }
656
- //# sourceMappingURL=solo-exec.js.map