sequant 1.16.1 → 1.18.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.
Files changed (83) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +14 -2
  3. package/README.md +2 -0
  4. package/dist/bin/cli.js +2 -1
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +21 -0
  6. package/dist/marketplace/external_plugins/sequant/README.md +38 -0
  7. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +292 -0
  8. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +463 -0
  9. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/prompt-templates.md +350 -0
  10. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +131 -0
  11. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +474 -0
  12. package/dist/marketplace/external_plugins/sequant/skills/clean/SKILL.md +211 -0
  13. package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +337 -0
  14. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +807 -0
  15. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +678 -0
  16. package/dist/marketplace/external_plugins/sequant/skills/improve/SKILL.md +668 -0
  17. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +374 -0
  18. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +570 -0
  19. package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-quality-exemplars.md +107 -0
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +65 -0
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +179 -0
  22. package/dist/marketplace/external_plugins/sequant/skills/qa/references/semgrep-rules.md +207 -0
  23. package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +109 -0
  24. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +622 -0
  25. package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +175 -0
  26. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +70 -0
  27. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +95 -0
  28. package/dist/marketplace/external_plugins/sequant/skills/security-review/SKILL.md +358 -0
  29. package/dist/marketplace/external_plugins/sequant/skills/security-review/references/security-checklists.md +432 -0
  30. package/dist/marketplace/external_plugins/sequant/skills/solve/SKILL.md +697 -0
  31. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +754 -0
  32. package/dist/marketplace/external_plugins/sequant/skills/spec/references/parallel-groups.md +72 -0
  33. package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +92 -0
  34. package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +104 -0
  35. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +600 -0
  36. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +576 -0
  37. package/dist/marketplace/external_plugins/sequant/skills/verify/SKILL.md +281 -0
  38. package/dist/src/commands/run.d.ts +13 -274
  39. package/dist/src/commands/run.js +43 -1958
  40. package/dist/src/commands/sync.js +3 -0
  41. package/dist/src/commands/update.js +3 -0
  42. package/dist/src/lib/plugin-version-sync.d.ts +2 -1
  43. package/dist/src/lib/plugin-version-sync.js +28 -7
  44. package/dist/src/lib/solve-comment-parser.d.ts +26 -0
  45. package/dist/src/lib/solve-comment-parser.js +63 -7
  46. package/dist/src/lib/upstream/assessment.js +6 -3
  47. package/dist/src/lib/upstream/relevance.d.ts +5 -0
  48. package/dist/src/lib/upstream/relevance.js +24 -0
  49. package/dist/src/lib/upstream/report.js +18 -46
  50. package/dist/src/lib/upstream/types.d.ts +2 -0
  51. package/dist/src/lib/workflow/batch-executor.d.ts +117 -0
  52. package/dist/src/lib/workflow/batch-executor.js +574 -0
  53. package/dist/src/lib/workflow/phase-executor.d.ts +40 -0
  54. package/dist/src/lib/workflow/phase-executor.js +381 -0
  55. package/dist/src/lib/workflow/phase-mapper.d.ts +65 -0
  56. package/dist/src/lib/workflow/phase-mapper.js +147 -0
  57. package/dist/src/lib/workflow/pr-operations.d.ts +86 -0
  58. package/dist/src/lib/workflow/pr-operations.js +326 -0
  59. package/dist/src/lib/workflow/pr-status.d.ts +49 -0
  60. package/dist/src/lib/workflow/pr-status.js +131 -0
  61. package/dist/src/lib/workflow/run-reflect.d.ts +32 -0
  62. package/dist/src/lib/workflow/run-reflect.js +191 -0
  63. package/dist/src/lib/workflow/run-summary.d.ts +36 -0
  64. package/dist/src/lib/workflow/run-summary.js +142 -0
  65. package/dist/src/lib/workflow/state-cleanup.d.ts +79 -0
  66. package/dist/src/lib/workflow/state-cleanup.js +250 -0
  67. package/dist/src/lib/workflow/state-rebuild.d.ts +38 -0
  68. package/dist/src/lib/workflow/state-rebuild.js +140 -0
  69. package/dist/src/lib/workflow/state-utils.d.ts +14 -162
  70. package/dist/src/lib/workflow/state-utils.js +10 -677
  71. package/dist/src/lib/workflow/worktree-discovery.d.ts +61 -0
  72. package/dist/src/lib/workflow/worktree-discovery.js +229 -0
  73. package/dist/src/lib/workflow/worktree-manager.d.ts +205 -0
  74. package/dist/src/lib/workflow/worktree-manager.js +918 -0
  75. package/package.json +4 -2
  76. package/templates/skills/exec/SKILL.md +2 -2
  77. package/templates/skills/fullsolve/SKILL.md +15 -5
  78. package/templates/skills/loop/SKILL.md +1 -1
  79. package/templates/skills/qa/SKILL.md +47 -7
  80. package/templates/skills/solve/SKILL.md +92 -6
  81. package/templates/skills/spec/SKILL.md +57 -4
  82. package/templates/skills/test/SKILL.md +10 -0
  83. package/templates/skills/testgen/SKILL.md +1 -1
@@ -1,6 +1,12 @@
1
1
  /**
2
2
  * State utilities for rebuilding and cleaning up workflow state
3
3
  *
4
+ * This module re-exports focused utilities from dedicated modules:
5
+ * - pr-status: PR merge detection and branch status
6
+ * - state-rebuild: State reconstruction from run logs
7
+ * - worktree-discovery: Worktree discovery for state bootstrapping
8
+ * - state-cleanup: Cleanup of stale entries and startup reconciliation
9
+ *
4
10
  * @example
5
11
  * ```typescript
6
12
  * import { rebuildStateFromLogs, cleanupStaleEntries } from './state-utils';
@@ -12,680 +18,7 @@
12
18
  * const result = await cleanupStaleEntries({ dryRun: true });
13
19
  * ```
14
20
  */
15
- import * as fs from "fs";
16
- import * as path from "path";
17
- import { spawnSync } from "child_process";
18
- import { StateManager } from "./state-manager.js";
19
- import { createEmptyState, createIssueState, createPhaseState, } from "./state-schema.js";
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
- }
45
- /**
46
- * Rebuild workflow state from run logs
47
- *
48
- * Scans all run logs in .sequant/logs/ and reconstructs state
49
- * based on the most recent activity for each issue.
50
- */
51
- export async function rebuildStateFromLogs(options = {}) {
52
- const logPath = options.logPath ?? LOG_PATHS.project;
53
- if (!fs.existsSync(logPath)) {
54
- return {
55
- success: false,
56
- logsProcessed: 0,
57
- issuesFound: 0,
58
- error: `Log directory not found: ${logPath}`,
59
- };
60
- }
61
- try {
62
- // Find all log files
63
- const files = fs.readdirSync(logPath).filter((f) => f.endsWith(".json"));
64
- if (files.length === 0) {
65
- return {
66
- success: true,
67
- logsProcessed: 0,
68
- issuesFound: 0,
69
- };
70
- }
71
- // Sort by timestamp (newest first)
72
- files.sort().reverse();
73
- // Build state from logs
74
- const state = createEmptyState();
75
- const issueMap = new Map();
76
- for (const file of files) {
77
- const filePath = path.join(logPath, file);
78
- try {
79
- const content = fs.readFileSync(filePath, "utf-8");
80
- const logData = JSON.parse(content);
81
- const log = RunLogSchema.safeParse(logData);
82
- if (!log.success) {
83
- if (options.verbose) {
84
- console.log(`⚠️ Invalid log format: ${file}`);
85
- }
86
- continue;
87
- }
88
- const runLog = log.data;
89
- // Process each issue in the log
90
- for (const issueLog of runLog.issues) {
91
- // Skip if we already have newer data for this issue
92
- if (issueMap.has(issueLog.issueNumber)) {
93
- continue;
94
- }
95
- // Create issue state from log
96
- const issueState = createIssueState(issueLog.issueNumber, issueLog.title);
97
- // Determine status from log
98
- if (issueLog.status === "success") {
99
- issueState.status = "ready_for_merge";
100
- }
101
- else if (issueLog.status === "failure") {
102
- issueState.status = "in_progress";
103
- }
104
- else {
105
- issueState.status = "in_progress";
106
- }
107
- // Add phase states from log
108
- for (const phaseLog of issueLog.phases) {
109
- const phaseState = createPhaseState(phaseLog.status === "success"
110
- ? "completed"
111
- : phaseLog.status === "failure"
112
- ? "failed"
113
- : phaseLog.status === "skipped"
114
- ? "skipped"
115
- : "completed");
116
- phaseState.startedAt = phaseLog.startTime;
117
- phaseState.completedAt = phaseLog.endTime;
118
- if (phaseLog.error) {
119
- phaseState.error = phaseLog.error;
120
- }
121
- issueState.phases[phaseLog.phase] = phaseState;
122
- // Update current phase to last executed
123
- issueState.currentPhase = phaseLog.phase;
124
- }
125
- // Set last activity from most recent phase
126
- const lastPhase = issueLog.phases[issueLog.phases.length - 1];
127
- if (lastPhase) {
128
- issueState.lastActivity = lastPhase.endTime;
129
- }
130
- issueMap.set(issueLog.issueNumber, issueState);
131
- }
132
- if (options.verbose) {
133
- console.log(`✓ Processed: ${file}`);
134
- }
135
- }
136
- catch (err) {
137
- if (options.verbose) {
138
- console.log(`⚠️ Error reading ${file}: ${err}`);
139
- }
140
- }
141
- }
142
- // Copy issues to state
143
- for (const [num, issueState] of issueMap) {
144
- state.issues[String(num)] = issueState;
145
- }
146
- // Save rebuilt state
147
- const manager = new StateManager({
148
- statePath: options.statePath,
149
- verbose: options.verbose,
150
- });
151
- await manager.saveState(state);
152
- return {
153
- success: true,
154
- logsProcessed: files.length,
155
- issuesFound: issueMap.size,
156
- };
157
- }
158
- catch (error) {
159
- return {
160
- success: false,
161
- logsProcessed: 0,
162
- issuesFound: 0,
163
- error: String(error),
164
- };
165
- }
166
- }
167
- /**
168
- * Clean up stale and orphaned entries from workflow state
169
- *
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
175
- */
176
- export async function cleanupStaleEntries(options = {}) {
177
- const manager = new StateManager({
178
- statePath: options.statePath,
179
- verbose: options.verbose,
180
- });
181
- if (!manager.stateExists()) {
182
- return {
183
- success: true,
184
- removed: [],
185
- orphaned: [],
186
- merged: [],
187
- };
188
- }
189
- try {
190
- const state = await manager.getState();
191
- const removed = [];
192
- const orphaned = [];
193
- const merged = [];
194
- // Get list of active worktrees
195
- const activeWorktrees = getActiveWorktrees();
196
- for (const [issueNumStr, issueState] of Object.entries(state.issues)) {
197
- const issueNum = parseInt(issueNumStr, 10);
198
- // Check if worktree exists (if issue has one)
199
- if (issueState.worktree &&
200
- !activeWorktrees.includes(issueState.worktree)) {
201
- if (options.verbose) {
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
- }
215
- }
216
- if (!options.dryRun) {
217
- if (prMerged || issueState.status === "merged") {
218
- // Merged PRs are auto-removed
219
- merged.push(issueNum);
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
- }
233
- delete state.issues[issueNumStr];
234
- }
235
- else {
236
- // Mark as abandoned (kept for review)
237
- orphaned.push(issueNum);
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);
256
- }
257
- }
258
- continue;
259
- }
260
- // Check age for merged/abandoned issues
261
- if (options.maxAgeDays &&
262
- (issueState.status === "merged" || issueState.status === "abandoned")) {
263
- const lastActivity = new Date(issueState.lastActivity);
264
- const ageDays = (Date.now() - lastActivity.getTime()) / (1000 * 60 * 60 * 24);
265
- if (ageDays > options.maxAgeDays) {
266
- removed.push(issueNum);
267
- if (options.verbose) {
268
- console.log(`🗑️ Stale: #${issueNum} (${Math.floor(ageDays)} days old)`);
269
- }
270
- if (!options.dryRun) {
271
- delete state.issues[issueNumStr];
272
- }
273
- }
274
- }
275
- }
276
- // Save updated state
277
- if (!options.dryRun && (removed.length > 0 || orphaned.length > 0)) {
278
- await manager.saveState(state);
279
- }
280
- return {
281
- success: true,
282
- removed,
283
- orphaned,
284
- merged,
285
- };
286
- }
287
- catch (error) {
288
- return {
289
- success: false,
290
- removed: [],
291
- orphaned: [],
292
- merged: [],
293
- error: String(error),
294
- };
295
- }
296
- }
297
- /**
298
- * Get list of active worktree paths
299
- */
300
- function getActiveWorktrees() {
301
- const result = spawnSync("git", ["worktree", "list", "--porcelain"], {
302
- stdio: "pipe",
303
- });
304
- if (result.status !== 0) {
305
- return [];
306
- }
307
- const output = result.stdout.toString();
308
- const paths = [];
309
- for (const line of output.split("\n")) {
310
- if (line.startsWith("worktree ")) {
311
- paths.push(line.substring(9));
312
- }
313
- }
314
- return paths;
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
- }
525
- /**
526
- * Check if a branch has been merged into main using git
527
- *
528
- * @param branchName - The branch name to check (e.g., "feature/33-some-title")
529
- * @returns true if the branch is merged into main, false otherwise
530
- */
531
- export function isBranchMergedIntoMain(branchName) {
532
- try {
533
- // Get branches merged into main
534
- const result = spawnSync("git", ["branch", "--merged", "main"], {
535
- stdio: "pipe",
536
- timeout: 10000,
537
- });
538
- if (result.status === 0 && result.stdout) {
539
- const mergedBranches = result.stdout.toString();
540
- // Check if our branch is in the list (handle both local and remote refs)
541
- return (mergedBranches.includes(branchName) ||
542
- mergedBranches.includes(`remotes/origin/${branchName}`));
543
- }
544
- }
545
- catch {
546
- // git command failed - return false
547
- }
548
- return false;
549
- }
550
- /**
551
- * Check if a feature branch for an issue is merged into main
552
- *
553
- * Tries multiple detection methods:
554
- * 1. Check if branch exists and is merged via `git branch --merged main`
555
- * 2. Check for merge commits mentioning the issue
556
- *
557
- * @param issueNumber - The issue number to check
558
- * @returns true if the issue's work is merged into main
559
- */
560
- export function isIssueMergedIntoMain(issueNumber) {
561
- try {
562
- // Method 1: Check if any feature branch for this issue is merged
563
- const listResult = spawnSync("git", ["branch", "-a"], {
564
- stdio: "pipe",
565
- timeout: 10000,
566
- });
567
- if (listResult.status === 0 && listResult.stdout) {
568
- const branches = listResult.stdout.toString();
569
- // Find branches matching feature/<issue>-*
570
- const branchPattern = new RegExp(`feature/${issueNumber}-[^\\s]+`, "g");
571
- const matchedBranches = branches.match(branchPattern);
572
- if (matchedBranches) {
573
- for (const branch of matchedBranches) {
574
- const cleanBranch = branch.replace(/^\*?\s*/, "").trim();
575
- if (isBranchMergedIntoMain(cleanBranch)) {
576
- return true;
577
- }
578
- }
579
- }
580
- }
581
- // Method 2: Check for merge commits mentioning the issue
582
- // Use specific merge patterns to avoid false positives from
583
- // unrelated commits that merely reference the issue number
584
- const logResult = spawnSync("git", [
585
- "log",
586
- "main",
587
- "--oneline",
588
- "-20",
589
- "--grep",
590
- `Merge #${issueNumber}`,
591
- "--grep",
592
- `Merge.*#${issueNumber}`,
593
- "--grep",
594
- `(#${issueNumber})`,
595
- ], {
596
- stdio: "pipe",
597
- timeout: 10000,
598
- });
599
- if (logResult.status === 0 && logResult.stdout) {
600
- const commits = logResult.stdout.toString().trim();
601
- if (commits.length > 0) {
602
- return true;
603
- }
604
- }
605
- }
606
- catch {
607
- // git command failed - return false
608
- }
609
- return false;
610
- }
611
- /**
612
- * Lightweight state reconciliation at run start
613
- *
614
- * Checks issues in `ready_for_merge` state and advances them to `merged`
615
- * if their PRs are merged or their branches are in main.
616
- *
617
- * This prevents re-running already completed issues.
618
- *
619
- * @param options - Reconciliation options
620
- * @returns Result with lists of advanced and still-pending issues
621
- */
622
- export async function reconcileStateAtStartup(options = {}) {
623
- const manager = new StateManager({
624
- statePath: options.statePath,
625
- verbose: options.verbose,
626
- });
627
- // Graceful degradation: if state file doesn't exist, skip
628
- if (!manager.stateExists()) {
629
- return {
630
- success: true,
631
- advanced: [],
632
- stillPending: [],
633
- };
634
- }
635
- try {
636
- const state = await manager.getState();
637
- const advanced = [];
638
- const stillPending = [];
639
- // Find issues in ready_for_merge state
640
- for (const [issueNumStr, issueState] of Object.entries(state.issues)) {
641
- if (issueState.status !== "ready_for_merge") {
642
- continue;
643
- }
644
- const issueNum = parseInt(issueNumStr, 10);
645
- let isMerged = false;
646
- // Check 1: If we have PR info, check PR status via gh
647
- if (issueState.pr?.number) {
648
- const prStatus = checkPRMergeStatus(issueState.pr.number);
649
- if (prStatus === "MERGED") {
650
- isMerged = true;
651
- if (options.verbose) {
652
- console.log(` #${issueNum}: PR #${issueState.pr.number} is merged`);
653
- }
654
- }
655
- }
656
- // Check 2: If no PR or PR check failed, check git for merged branch
657
- if (!isMerged) {
658
- isMerged = isIssueMergedIntoMain(issueNum);
659
- if (isMerged && options.verbose) {
660
- console.log(` #${issueNum}: Branch merged into main (git check)`);
661
- }
662
- }
663
- if (isMerged) {
664
- // Advance state to merged
665
- issueState.status = "merged";
666
- issueState.lastActivity = new Date().toISOString();
667
- advanced.push(issueNum);
668
- }
669
- else {
670
- stillPending.push(issueNum);
671
- }
672
- }
673
- // Save state if any issues were advanced
674
- if (advanced.length > 0) {
675
- await manager.saveState(state);
676
- }
677
- return {
678
- success: true,
679
- advanced,
680
- stillPending,
681
- };
682
- }
683
- catch (error) {
684
- return {
685
- success: false,
686
- advanced: [],
687
- stillPending: [],
688
- error: String(error),
689
- };
690
- }
691
- }
21
+ export { checkPRMergeStatus, isBranchMergedIntoMain, isIssueMergedIntoMain, } from "./pr-status.js";
22
+ export { rebuildStateFromLogs } from "./state-rebuild.js";
23
+ export { discoverUntrackedWorktrees } from "./worktree-discovery.js";
24
+ export { cleanupStaleEntries, reconcileStateAtStartup, } from "./state-cleanup.js";