sequant 1.10.1 → 1.11.0
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/README.md +6 -1
- package/dist/bin/cli.js +55 -2
- package/dist/dashboard/server.d.ts +37 -0
- package/dist/dashboard/server.js +968 -0
- package/dist/src/commands/dashboard.d.ts +25 -0
- package/dist/src/commands/dashboard.js +44 -0
- package/dist/src/commands/doctor.d.ts +18 -1
- package/dist/src/commands/doctor.js +105 -2
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +26 -2
- package/dist/src/commands/run.d.ts +20 -0
- package/dist/src/commands/run.js +151 -3
- package/dist/src/commands/state.d.ts +60 -0
- package/dist/src/commands/state.js +267 -0
- package/dist/src/commands/stats.d.ts +3 -2
- package/dist/src/commands/stats.js +246 -38
- package/dist/src/commands/status.d.ts +2 -0
- package/dist/src/commands/status.js +28 -3
- package/dist/src/lib/ac-parser.d.ts +61 -0
- package/dist/src/lib/ac-parser.js +156 -0
- package/dist/src/lib/fs.d.ts +19 -0
- package/dist/src/lib/fs.js +58 -1
- package/dist/src/lib/settings.d.ts +7 -0
- package/dist/src/lib/settings.js +1 -0
- package/dist/src/lib/system.d.ts +19 -0
- package/dist/src/lib/system.js +26 -0
- package/dist/src/lib/templates.d.ts +34 -1
- package/dist/src/lib/templates.js +109 -5
- package/dist/src/lib/workflow/metrics-schema.d.ts +153 -0
- package/dist/src/lib/workflow/metrics-schema.js +138 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +102 -0
- package/dist/src/lib/workflow/metrics-writer.js +189 -0
- package/dist/src/lib/workflow/state-manager.d.ts +18 -1
- package/dist/src/lib/workflow/state-manager.js +61 -1
- package/dist/src/lib/workflow/state-schema.d.ts +152 -1
- package/dist/src/lib/workflow/state-schema.js +99 -0
- package/dist/src/lib/workflow/state-utils.d.ts +67 -3
- package/dist/src/lib/workflow/state-utils.js +289 -8
- package/dist/src/lib/workflow/types.d.ts +2 -0
- package/dist/src/lib/workflow/types.js +1 -0
- package/package.json +5 -1
|
@@ -18,6 +18,30 @@ import { spawnSync } from "child_process";
|
|
|
18
18
|
import { StateManager } from "./state-manager.js";
|
|
19
19
|
import { createEmptyState, createIssueState, createPhaseState, } from "./state-schema.js";
|
|
20
20
|
import { RunLogSchema, LOG_PATHS } from "./run-log-schema.js";
|
|
21
|
+
/**
|
|
22
|
+
* Check the merge status of a PR using the gh CLI
|
|
23
|
+
*
|
|
24
|
+
* @param prNumber - The PR number to check
|
|
25
|
+
* @returns "MERGED" | "CLOSED" | "OPEN" | null (null if PR not found or gh unavailable)
|
|
26
|
+
*/
|
|
27
|
+
export function checkPRMergeStatus(prNumber) {
|
|
28
|
+
try {
|
|
29
|
+
const result = spawnSync("gh", ["pr", "view", String(prNumber), "--json", "state", "-q", ".state"], { stdio: "pipe", timeout: 10000 });
|
|
30
|
+
if (result.status === 0 && result.stdout) {
|
|
31
|
+
const state = result.stdout.toString().trim().toUpperCase();
|
|
32
|
+
if (state === "MERGED")
|
|
33
|
+
return "MERGED";
|
|
34
|
+
if (state === "CLOSED")
|
|
35
|
+
return "CLOSED";
|
|
36
|
+
if (state === "OPEN")
|
|
37
|
+
return "OPEN";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// gh not available or error - return null
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
21
45
|
/**
|
|
22
46
|
* Rebuild workflow state from run logs
|
|
23
47
|
*
|
|
@@ -143,8 +167,11 @@ export async function rebuildStateFromLogs(options = {}) {
|
|
|
143
167
|
/**
|
|
144
168
|
* Clean up stale and orphaned entries from workflow state
|
|
145
169
|
*
|
|
146
|
-
* -
|
|
147
|
-
* -
|
|
170
|
+
* - Checks GitHub to detect if associated PR was merged
|
|
171
|
+
* - Orphaned entries with merged PRs get status "merged" and are removed automatically
|
|
172
|
+
* - Orphaned entries without merged PRs get status "abandoned" (kept for review)
|
|
173
|
+
* - Use removeAll to remove both merged and abandoned orphaned entries in one step
|
|
174
|
+
* - Use maxAgeDays to remove old merged/abandoned issues
|
|
148
175
|
*/
|
|
149
176
|
export async function cleanupStaleEntries(options = {}) {
|
|
150
177
|
const manager = new StateManager({
|
|
@@ -156,12 +183,14 @@ export async function cleanupStaleEntries(options = {}) {
|
|
|
156
183
|
success: true,
|
|
157
184
|
removed: [],
|
|
158
185
|
orphaned: [],
|
|
186
|
+
merged: [],
|
|
159
187
|
};
|
|
160
188
|
}
|
|
161
189
|
try {
|
|
162
190
|
const state = await manager.getState();
|
|
163
191
|
const removed = [];
|
|
164
192
|
const orphaned = [];
|
|
193
|
+
const merged = [];
|
|
165
194
|
// Get list of active worktrees
|
|
166
195
|
const activeWorktrees = getActiveWorktrees();
|
|
167
196
|
for (const [issueNumStr, issueState] of Object.entries(state.issues)) {
|
|
@@ -169,20 +198,61 @@ export async function cleanupStaleEntries(options = {}) {
|
|
|
169
198
|
// Check if worktree exists (if issue has one)
|
|
170
199
|
if (issueState.worktree &&
|
|
171
200
|
!activeWorktrees.includes(issueState.worktree)) {
|
|
172
|
-
orphaned.push(issueNum);
|
|
173
201
|
if (options.verbose) {
|
|
174
|
-
console.log(
|
|
202
|
+
console.log(`🔍 Orphaned: #${issueNum} (worktree not found: ${issueState.worktree})`);
|
|
203
|
+
}
|
|
204
|
+
// Check if this issue has a PR and if it's merged
|
|
205
|
+
let prMerged = false;
|
|
206
|
+
if (issueState.pr?.number) {
|
|
207
|
+
if (options.verbose) {
|
|
208
|
+
console.log(` Checking PR #${issueState.pr.number} status...`);
|
|
209
|
+
}
|
|
210
|
+
const prStatus = checkPRMergeStatus(issueState.pr.number);
|
|
211
|
+
prMerged = prStatus === "MERGED";
|
|
212
|
+
if (options.verbose) {
|
|
213
|
+
console.log(` PR status: ${prStatus ?? "unknown"}`);
|
|
214
|
+
}
|
|
175
215
|
}
|
|
176
216
|
if (!options.dryRun) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
217
|
+
if (prMerged || issueState.status === "merged") {
|
|
218
|
+
// Merged PRs are auto-removed
|
|
219
|
+
merged.push(issueNum);
|
|
180
220
|
removed.push(issueNum);
|
|
221
|
+
if (options.verbose) {
|
|
222
|
+
console.log(` ✓ Merged PR detected, removing entry`);
|
|
223
|
+
}
|
|
224
|
+
delete state.issues[issueNumStr];
|
|
225
|
+
}
|
|
226
|
+
else if (issueState.status === "abandoned" || options.removeAll) {
|
|
227
|
+
// Already abandoned or removeAll flag - remove it
|
|
228
|
+
orphaned.push(issueNum);
|
|
229
|
+
removed.push(issueNum);
|
|
230
|
+
if (options.verbose) {
|
|
231
|
+
console.log(` ✓ Removing abandoned entry`);
|
|
232
|
+
}
|
|
181
233
|
delete state.issues[issueNumStr];
|
|
182
234
|
}
|
|
183
235
|
else {
|
|
184
|
-
//
|
|
236
|
+
// Mark as abandoned (kept for review)
|
|
237
|
+
orphaned.push(issueNum);
|
|
185
238
|
issueState.status = "abandoned";
|
|
239
|
+
if (options.verbose) {
|
|
240
|
+
console.log(` → Marked as abandoned (kept for review)`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
// Dry run - report what would happen
|
|
246
|
+
if (prMerged || issueState.status === "merged") {
|
|
247
|
+
merged.push(issueNum);
|
|
248
|
+
removed.push(issueNum);
|
|
249
|
+
}
|
|
250
|
+
else if (issueState.status === "abandoned" || options.removeAll) {
|
|
251
|
+
orphaned.push(issueNum);
|
|
252
|
+
removed.push(issueNum);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
orphaned.push(issueNum);
|
|
186
256
|
}
|
|
187
257
|
}
|
|
188
258
|
continue;
|
|
@@ -211,6 +281,7 @@ export async function cleanupStaleEntries(options = {}) {
|
|
|
211
281
|
success: true,
|
|
212
282
|
removed,
|
|
213
283
|
orphaned,
|
|
284
|
+
merged,
|
|
214
285
|
};
|
|
215
286
|
}
|
|
216
287
|
catch (error) {
|
|
@@ -218,6 +289,7 @@ export async function cleanupStaleEntries(options = {}) {
|
|
|
218
289
|
success: false,
|
|
219
290
|
removed: [],
|
|
220
291
|
orphaned: [],
|
|
292
|
+
merged: [],
|
|
221
293
|
error: String(error),
|
|
222
294
|
};
|
|
223
295
|
}
|
|
@@ -241,3 +313,212 @@ function getActiveWorktrees() {
|
|
|
241
313
|
}
|
|
242
314
|
return paths;
|
|
243
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Parse issue number from a branch name
|
|
318
|
+
*
|
|
319
|
+
* Supports patterns:
|
|
320
|
+
* - feature/<number>-<slug>
|
|
321
|
+
* - issue-<number>
|
|
322
|
+
* - <number>-<slug>
|
|
323
|
+
*/
|
|
324
|
+
function parseIssueNumberFromBranch(branch) {
|
|
325
|
+
// Pattern: feature/123-description or feature/123
|
|
326
|
+
const featureMatch = branch.match(/^feature\/(\d+)(?:-|$)/);
|
|
327
|
+
if (featureMatch) {
|
|
328
|
+
return parseInt(featureMatch[1], 10);
|
|
329
|
+
}
|
|
330
|
+
// Pattern: issue-123
|
|
331
|
+
const issueMatch = branch.match(/^issue-(\d+)$/);
|
|
332
|
+
if (issueMatch) {
|
|
333
|
+
return parseInt(issueMatch[1], 10);
|
|
334
|
+
}
|
|
335
|
+
// Pattern: 123-description (bare number prefix)
|
|
336
|
+
const bareMatch = branch.match(/^(\d+)-/);
|
|
337
|
+
if (bareMatch) {
|
|
338
|
+
return parseInt(bareMatch[1], 10);
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Fetch issue title from GitHub using gh CLI
|
|
344
|
+
*
|
|
345
|
+
* Returns placeholder if gh is not available or fetch fails.
|
|
346
|
+
*/
|
|
347
|
+
function fetchIssueTitle(issueNumber) {
|
|
348
|
+
try {
|
|
349
|
+
const result = spawnSync("gh", ["issue", "view", String(issueNumber), "--json", "title", "-q", ".title"], { stdio: "pipe", timeout: 10000 });
|
|
350
|
+
if (result.status === 0 && result.stdout) {
|
|
351
|
+
const title = result.stdout.toString().trim();
|
|
352
|
+
if (title) {
|
|
353
|
+
return title;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// gh not available or error - use placeholder
|
|
359
|
+
}
|
|
360
|
+
return `(title unavailable for #${issueNumber})`;
|
|
361
|
+
}
|
|
362
|
+
function getWorktreeDetails() {
|
|
363
|
+
const result = spawnSync("git", ["worktree", "list", "--porcelain"], {
|
|
364
|
+
stdio: "pipe",
|
|
365
|
+
});
|
|
366
|
+
if (result.status !== 0) {
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
const output = result.stdout.toString();
|
|
370
|
+
const worktrees = [];
|
|
371
|
+
let current = {};
|
|
372
|
+
for (const line of output.split("\n")) {
|
|
373
|
+
if (line.startsWith("worktree ")) {
|
|
374
|
+
// Start of new worktree entry
|
|
375
|
+
if (current.path) {
|
|
376
|
+
worktrees.push(current);
|
|
377
|
+
}
|
|
378
|
+
current = { path: line.substring(9) };
|
|
379
|
+
}
|
|
380
|
+
else if (line.startsWith("HEAD ")) {
|
|
381
|
+
current.head = line.substring(5);
|
|
382
|
+
}
|
|
383
|
+
else if (line.startsWith("branch refs/heads/")) {
|
|
384
|
+
current.branch = line.substring(18);
|
|
385
|
+
}
|
|
386
|
+
else if (line === "" && current.path) {
|
|
387
|
+
// End of entry
|
|
388
|
+
worktrees.push(current);
|
|
389
|
+
current = {};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Don't forget the last entry
|
|
393
|
+
if (current.path && current.branch) {
|
|
394
|
+
worktrees.push(current);
|
|
395
|
+
}
|
|
396
|
+
return worktrees;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Infer the current phase for an issue by checking logs
|
|
400
|
+
*/
|
|
401
|
+
function inferPhaseFromLogs(issueNumber) {
|
|
402
|
+
const logPath = LOG_PATHS.project;
|
|
403
|
+
if (!fs.existsSync(logPath)) {
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const files = fs.readdirSync(logPath).filter((f) => f.endsWith(".json"));
|
|
408
|
+
// Sort by timestamp (newest first)
|
|
409
|
+
files.sort().reverse();
|
|
410
|
+
for (const file of files) {
|
|
411
|
+
try {
|
|
412
|
+
const content = fs.readFileSync(path.join(logPath, file), "utf-8");
|
|
413
|
+
const logData = JSON.parse(content);
|
|
414
|
+
const log = RunLogSchema.safeParse(logData);
|
|
415
|
+
if (!log.success)
|
|
416
|
+
continue;
|
|
417
|
+
// Find this issue in the log
|
|
418
|
+
const issueLog = log.data.issues.find((i) => i.issueNumber === issueNumber);
|
|
419
|
+
if (issueLog && issueLog.phases.length > 0) {
|
|
420
|
+
// Return the last executed phase
|
|
421
|
+
const lastPhase = issueLog.phases[issueLog.phases.length - 1];
|
|
422
|
+
return lastPhase.phase;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Discover worktrees that are not yet tracked in state
|
|
437
|
+
*
|
|
438
|
+
* Scans all git worktrees, identifies those with issue-related branch names,
|
|
439
|
+
* and returns information about worktrees not yet in the state file.
|
|
440
|
+
*/
|
|
441
|
+
export async function discoverUntrackedWorktrees(options = {}) {
|
|
442
|
+
try {
|
|
443
|
+
const worktrees = getWorktreeDetails();
|
|
444
|
+
const discovered = [];
|
|
445
|
+
const skipped = [];
|
|
446
|
+
let alreadyTracked = 0;
|
|
447
|
+
// Get existing state
|
|
448
|
+
const manager = new StateManager({
|
|
449
|
+
statePath: options.statePath,
|
|
450
|
+
verbose: options.verbose,
|
|
451
|
+
});
|
|
452
|
+
const state = await manager.getState();
|
|
453
|
+
const trackedIssues = new Set(Object.keys(state.issues).map((n) => parseInt(n, 10)));
|
|
454
|
+
for (const worktree of worktrees) {
|
|
455
|
+
// Skip if no branch (detached HEAD)
|
|
456
|
+
if (!worktree.branch) {
|
|
457
|
+
skipped.push({
|
|
458
|
+
path: worktree.path,
|
|
459
|
+
reason: "detached HEAD (no branch)",
|
|
460
|
+
});
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
// Skip main/master branches
|
|
464
|
+
if (worktree.branch === "main" || worktree.branch === "master") {
|
|
465
|
+
skipped.push({
|
|
466
|
+
path: worktree.path,
|
|
467
|
+
reason: "main/master branch (not a feature worktree)",
|
|
468
|
+
});
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
// Try to parse issue number from branch
|
|
472
|
+
const issueNumber = parseIssueNumberFromBranch(worktree.branch);
|
|
473
|
+
if (issueNumber === null) {
|
|
474
|
+
skipped.push({
|
|
475
|
+
path: worktree.path,
|
|
476
|
+
reason: `branch name doesn't match issue pattern: ${worktree.branch}`,
|
|
477
|
+
});
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
// Check if already tracked
|
|
481
|
+
if (trackedIssues.has(issueNumber)) {
|
|
482
|
+
alreadyTracked++;
|
|
483
|
+
if (options.verbose) {
|
|
484
|
+
console.log(` Already tracked: #${issueNumber} (${worktree.branch})`);
|
|
485
|
+
}
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
// Fetch title from GitHub
|
|
489
|
+
if (options.verbose) {
|
|
490
|
+
console.log(` Fetching title for #${issueNumber}...`);
|
|
491
|
+
}
|
|
492
|
+
const title = fetchIssueTitle(issueNumber);
|
|
493
|
+
// Try to infer phase from logs
|
|
494
|
+
const inferredPhase = inferPhaseFromLogs(issueNumber);
|
|
495
|
+
discovered.push({
|
|
496
|
+
issueNumber,
|
|
497
|
+
title,
|
|
498
|
+
worktreePath: worktree.path,
|
|
499
|
+
branch: worktree.branch,
|
|
500
|
+
inferredPhase,
|
|
501
|
+
});
|
|
502
|
+
if (options.verbose) {
|
|
503
|
+
console.log(` Discovered: #${issueNumber} - ${title}${inferredPhase ? ` (phase: ${inferredPhase})` : ""}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
success: true,
|
|
508
|
+
worktreesScanned: worktrees.length,
|
|
509
|
+
alreadyTracked,
|
|
510
|
+
discovered,
|
|
511
|
+
skipped,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
return {
|
|
516
|
+
success: false,
|
|
517
|
+
worktreesScanned: 0,
|
|
518
|
+
alreadyTracked: 0,
|
|
519
|
+
discovered: [],
|
|
520
|
+
skipped: [],
|
|
521
|
+
error: String(error),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
@@ -33,6 +33,8 @@ export interface ExecutionConfig {
|
|
|
33
33
|
noSmartTests: boolean;
|
|
34
34
|
/** Dry run mode - don't actually execute */
|
|
35
35
|
dryRun: boolean;
|
|
36
|
+
/** Enable MCP servers in headless mode (true by default, false if --no-mcp flag used) */
|
|
37
|
+
mcp: boolean;
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
38
40
|
* Default execution configuration
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sequant",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Quantize your development workflow - Sequential AI phases with quality gates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -55,10 +55,14 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
|
58
|
+
"@hono/node-server": "^1.19.9",
|
|
58
59
|
"chalk": "^5.3.0",
|
|
60
|
+
"chokidar": "^5.0.0",
|
|
59
61
|
"commander": "^12.1.0",
|
|
60
62
|
"diff": "^7.0.0",
|
|
63
|
+
"hono": "^4.11.4",
|
|
61
64
|
"inquirer": "^12.3.2",
|
|
65
|
+
"open": "^11.0.0",
|
|
62
66
|
"yaml": "^2.7.0",
|
|
63
67
|
"zod": "^4.3.5"
|
|
64
68
|
},
|