@blockspool/cli 0.4.0 → 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.
- package/package.json +1 -1
- package/dist/bin/blockspool.d.ts +0 -16
- package/dist/bin/blockspool.d.ts.map +0 -1
- package/dist/bin/blockspool.js +0 -45
- package/dist/bin/blockspool.js.map +0 -1
- package/dist/commands/solo-auto.d.ts +0 -6
- package/dist/commands/solo-auto.d.ts.map +0 -1
- package/dist/commands/solo-auto.js +0 -418
- package/dist/commands/solo-auto.js.map +0 -1
- package/dist/commands/solo-exec.d.ts +0 -6
- package/dist/commands/solo-exec.d.ts.map +0 -1
- package/dist/commands/solo-exec.js +0 -656
- package/dist/commands/solo-exec.js.map +0 -1
- package/dist/commands/solo-inspect.d.ts +0 -6
- package/dist/commands/solo-inspect.d.ts.map +0 -1
- package/dist/commands/solo-inspect.js +0 -690
- package/dist/commands/solo-inspect.js.map +0 -1
- package/dist/commands/solo-lifecycle.d.ts +0 -6
- package/dist/commands/solo-lifecycle.d.ts.map +0 -1
- package/dist/commands/solo-lifecycle.js +0 -188
- package/dist/commands/solo-lifecycle.js.map +0 -1
- package/dist/commands/solo-nudge.d.ts +0 -6
- package/dist/commands/solo-nudge.d.ts.map +0 -1
- package/dist/commands/solo-nudge.js +0 -49
- package/dist/commands/solo-nudge.js.map +0 -1
- package/dist/commands/solo-qa.d.ts +0 -6
- package/dist/commands/solo-qa.d.ts.map +0 -1
- package/dist/commands/solo-qa.js +0 -254
- package/dist/commands/solo-qa.js.map +0 -1
- package/dist/commands/solo.d.ts +0 -11
- package/dist/commands/solo.d.ts.map +0 -1
- package/dist/commands/solo.js +0 -43
- package/dist/commands/solo.js.map +0 -1
- package/dist/index.d.ts +0 -18
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -18
- package/dist/index.js.map +0 -1
- package/dist/lib/artifacts.d.ts +0 -136
- package/dist/lib/artifacts.d.ts.map +0 -1
- package/dist/lib/artifacts.js +0 -146
- package/dist/lib/artifacts.js.map +0 -1
- package/dist/lib/doctor.d.ts +0 -45
- package/dist/lib/doctor.d.ts.map +0 -1
- package/dist/lib/doctor.js +0 -383
- package/dist/lib/doctor.js.map +0 -1
- package/dist/lib/exec.d.ts +0 -24
- package/dist/lib/exec.d.ts.map +0 -1
- package/dist/lib/exec.js +0 -295
- package/dist/lib/exec.js.map +0 -1
- package/dist/lib/formulas.d.ts +0 -78
- package/dist/lib/formulas.d.ts.map +0 -1
- package/dist/lib/formulas.js +0 -295
- package/dist/lib/formulas.js.map +0 -1
- package/dist/lib/git.d.ts +0 -9
- package/dist/lib/git.d.ts.map +0 -1
- package/dist/lib/git.js +0 -60
- package/dist/lib/git.js.map +0 -1
- package/dist/lib/guidelines.d.ts +0 -43
- package/dist/lib/guidelines.d.ts.map +0 -1
- package/dist/lib/guidelines.js +0 -195
- package/dist/lib/guidelines.js.map +0 -1
- package/dist/lib/logger.d.ts +0 -17
- package/dist/lib/logger.d.ts.map +0 -1
- package/dist/lib/logger.js +0 -42
- package/dist/lib/logger.js.map +0 -1
- package/dist/lib/retention.d.ts +0 -62
- package/dist/lib/retention.d.ts.map +0 -1
- package/dist/lib/retention.js +0 -285
- package/dist/lib/retention.js.map +0 -1
- package/dist/lib/run-history.d.ts +0 -52
- package/dist/lib/run-history.d.ts.map +0 -1
- package/dist/lib/run-history.js +0 -116
- package/dist/lib/run-history.js.map +0 -1
- package/dist/lib/run-state.d.ts +0 -58
- package/dist/lib/run-state.d.ts.map +0 -1
- package/dist/lib/run-state.js +0 -119
- package/dist/lib/run-state.js.map +0 -1
- package/dist/lib/scope.d.ts +0 -95
- package/dist/lib/scope.d.ts.map +0 -1
- package/dist/lib/scope.js +0 -291
- package/dist/lib/scope.js.map +0 -1
- package/dist/lib/selection.d.ts +0 -35
- package/dist/lib/selection.d.ts.map +0 -1
- package/dist/lib/selection.js +0 -110
- package/dist/lib/selection.js.map +0 -1
- package/dist/lib/solo-auto.d.ts +0 -87
- package/dist/lib/solo-auto.d.ts.map +0 -1
- package/dist/lib/solo-auto.js +0 -1230
- package/dist/lib/solo-auto.js.map +0 -1
- package/dist/lib/solo-ci.d.ts +0 -84
- package/dist/lib/solo-ci.d.ts.map +0 -1
- package/dist/lib/solo-ci.js +0 -300
- package/dist/lib/solo-ci.js.map +0 -1
- package/dist/lib/solo-config.d.ts +0 -155
- package/dist/lib/solo-config.d.ts.map +0 -1
- package/dist/lib/solo-config.js +0 -236
- package/dist/lib/solo-config.js.map +0 -1
- package/dist/lib/solo-git.d.ts +0 -44
- package/dist/lib/solo-git.d.ts.map +0 -1
- package/dist/lib/solo-git.js +0 -174
- package/dist/lib/solo-git.js.map +0 -1
- package/dist/lib/solo-hints.d.ts +0 -32
- package/dist/lib/solo-hints.d.ts.map +0 -1
- package/dist/lib/solo-hints.js +0 -98
- package/dist/lib/solo-hints.js.map +0 -1
- package/dist/lib/solo-remote.d.ts +0 -14
- package/dist/lib/solo-remote.d.ts.map +0 -1
- package/dist/lib/solo-remote.js +0 -48
- package/dist/lib/solo-remote.js.map +0 -1
- package/dist/lib/solo-stdin.d.ts +0 -13
- package/dist/lib/solo-stdin.d.ts.map +0 -1
- package/dist/lib/solo-stdin.js +0 -33
- package/dist/lib/solo-stdin.js.map +0 -1
- package/dist/lib/solo-ticket.d.ts +0 -213
- package/dist/lib/solo-ticket.d.ts.map +0 -1
- package/dist/lib/solo-ticket.js +0 -850
- package/dist/lib/solo-ticket.js.map +0 -1
- package/dist/lib/solo-utils.d.ts +0 -133
- package/dist/lib/solo-utils.d.ts.map +0 -1
- package/dist/lib/solo-utils.js +0 -300
- package/dist/lib/solo-utils.js.map +0 -1
- package/dist/lib/spindle.d.ts +0 -144
- package/dist/lib/spindle.d.ts.map +0 -1
- package/dist/lib/spindle.js +0 -388
- package/dist/lib/spindle.js.map +0 -1
- package/dist/tui/app.d.ts +0 -17
- package/dist/tui/app.d.ts.map +0 -1
- package/dist/tui/app.js +0 -139
- package/dist/tui/app.js.map +0 -1
- package/dist/tui/index.d.ts +0 -8
- package/dist/tui/index.d.ts.map +0 -1
- package/dist/tui/index.js +0 -7
- package/dist/tui/index.js.map +0 -1
- package/dist/tui/poller.d.ts +0 -42
- package/dist/tui/poller.d.ts.map +0 -1
- package/dist/tui/poller.js +0 -62
- package/dist/tui/poller.js.map +0 -1
- package/dist/tui/screens/overview.d.ts +0 -9
- package/dist/tui/screens/overview.d.ts.map +0 -1
- package/dist/tui/screens/overview.js +0 -189
- package/dist/tui/screens/overview.js.map +0 -1
- package/dist/tui/state.d.ts +0 -93
- package/dist/tui/state.d.ts.map +0 -1
- package/dist/tui/state.js +0 -169
- package/dist/tui/state.js.map +0 -1
- package/dist/tui/types.d.ts +0 -18
- package/dist/tui/types.d.ts.map +0 -1
- package/dist/tui/types.js +0 -5
- 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
|