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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +14 -2
- package/README.md +2 -0
- package/dist/bin/cli.js +2 -1
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +21 -0
- package/dist/marketplace/external_plugins/sequant/README.md +38 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +292 -0
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +463 -0
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/prompt-templates.md +350 -0
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +131 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +474 -0
- package/dist/marketplace/external_plugins/sequant/skills/clean/SKILL.md +211 -0
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +337 -0
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +807 -0
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +678 -0
- package/dist/marketplace/external_plugins/sequant/skills/improve/SKILL.md +668 -0
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +374 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +570 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-quality-exemplars.md +107 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +65 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +179 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/semgrep-rules.md +207 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +109 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +622 -0
- package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +175 -0
- package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +70 -0
- package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +95 -0
- package/dist/marketplace/external_plugins/sequant/skills/security-review/SKILL.md +358 -0
- package/dist/marketplace/external_plugins/sequant/skills/security-review/references/security-checklists.md +432 -0
- package/dist/marketplace/external_plugins/sequant/skills/solve/SKILL.md +697 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +754 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/parallel-groups.md +72 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +92 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +104 -0
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +600 -0
- package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +576 -0
- package/dist/marketplace/external_plugins/sequant/skills/verify/SKILL.md +281 -0
- package/dist/src/commands/run.d.ts +13 -274
- package/dist/src/commands/run.js +43 -1958
- package/dist/src/commands/sync.js +3 -0
- package/dist/src/commands/update.js +3 -0
- package/dist/src/lib/plugin-version-sync.d.ts +2 -1
- package/dist/src/lib/plugin-version-sync.js +28 -7
- package/dist/src/lib/solve-comment-parser.d.ts +26 -0
- package/dist/src/lib/solve-comment-parser.js +63 -7
- package/dist/src/lib/upstream/assessment.js +6 -3
- package/dist/src/lib/upstream/relevance.d.ts +5 -0
- package/dist/src/lib/upstream/relevance.js +24 -0
- package/dist/src/lib/upstream/report.js +18 -46
- package/dist/src/lib/upstream/types.d.ts +2 -0
- package/dist/src/lib/workflow/batch-executor.d.ts +117 -0
- package/dist/src/lib/workflow/batch-executor.js +574 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +40 -0
- package/dist/src/lib/workflow/phase-executor.js +381 -0
- package/dist/src/lib/workflow/phase-mapper.d.ts +65 -0
- package/dist/src/lib/workflow/phase-mapper.js +147 -0
- package/dist/src/lib/workflow/pr-operations.d.ts +86 -0
- package/dist/src/lib/workflow/pr-operations.js +326 -0
- package/dist/src/lib/workflow/pr-status.d.ts +49 -0
- package/dist/src/lib/workflow/pr-status.js +131 -0
- package/dist/src/lib/workflow/run-reflect.d.ts +32 -0
- package/dist/src/lib/workflow/run-reflect.js +191 -0
- package/dist/src/lib/workflow/run-summary.d.ts +36 -0
- package/dist/src/lib/workflow/run-summary.js +142 -0
- package/dist/src/lib/workflow/state-cleanup.d.ts +79 -0
- package/dist/src/lib/workflow/state-cleanup.js +250 -0
- package/dist/src/lib/workflow/state-rebuild.d.ts +38 -0
- package/dist/src/lib/workflow/state-rebuild.js +140 -0
- package/dist/src/lib/workflow/state-utils.d.ts +14 -162
- package/dist/src/lib/workflow/state-utils.js +10 -677
- package/dist/src/lib/workflow/worktree-discovery.d.ts +61 -0
- package/dist/src/lib/workflow/worktree-discovery.js +229 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +205 -0
- package/dist/src/lib/workflow/worktree-manager.js +918 -0
- package/package.json +4 -2
- package/templates/skills/exec/SKILL.md +2 -2
- package/templates/skills/fullsolve/SKILL.md +15 -5
- package/templates/skills/loop/SKILL.md +1 -1
- package/templates/skills/qa/SKILL.md +47 -7
- package/templates/skills/solve/SKILL.md +92 -6
- package/templates/skills/spec/SKILL.md +57 -4
- package/templates/skills/test/SKILL.md +10 -0
- 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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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";
|