sequant 1.1.3 → 1.2.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/README.md +112 -10
- package/dist/bin/cli.js +6 -1
- package/dist/bin/cli.js.map +1 -1
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +33 -2
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/doctor.test.js +63 -1
- package/dist/src/commands/doctor.test.js.map +1 -1
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +40 -10
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/init.test.js +100 -1
- package/dist/src/commands/init.test.js.map +1 -1
- package/dist/src/commands/logs.js +1 -1
- package/dist/src/commands/logs.js.map +1 -1
- package/dist/src/commands/run.d.ts +16 -0
- package/dist/src/commands/run.d.ts.map +1 -1
- package/dist/src/commands/run.js +395 -37
- package/dist/src/commands/run.js.map +1 -1
- package/dist/src/commands/run.test.d.ts +2 -0
- package/dist/src/commands/run.test.d.ts.map +1 -0
- package/dist/src/commands/run.test.js +155 -0
- package/dist/src/commands/run.test.js.map +1 -0
- package/dist/src/commands/update.d.ts.map +1 -1
- package/dist/src/commands/update.js +9 -5
- package/dist/src/commands/update.js.map +1 -1
- package/dist/src/lib/manifest.d.ts +3 -1
- package/dist/src/lib/manifest.d.ts.map +1 -1
- package/dist/src/lib/manifest.js +2 -1
- package/dist/src/lib/manifest.js.map +1 -1
- package/dist/src/lib/stacks.d.ts +27 -0
- package/dist/src/lib/stacks.d.ts.map +1 -1
- package/dist/src/lib/stacks.js +154 -17
- package/dist/src/lib/stacks.js.map +1 -1
- package/dist/src/lib/stacks.test.js +343 -1
- package/dist/src/lib/stacks.test.js.map +1 -1
- package/dist/src/lib/system.d.ts +8 -0
- package/dist/src/lib/system.d.ts.map +1 -1
- package/dist/src/lib/system.js +23 -0
- package/dist/src/lib/system.js.map +1 -1
- package/dist/src/lib/tty.d.ts +31 -0
- package/dist/src/lib/tty.d.ts.map +1 -0
- package/dist/src/lib/tty.js +81 -0
- package/dist/src/lib/tty.js.map +1 -0
- package/dist/src/lib/tty.test.d.ts +2 -0
- package/dist/src/lib/tty.test.d.ts.map +1 -0
- package/dist/src/lib/tty.test.js +227 -0
- package/dist/src/lib/tty.test.js.map +1 -0
- package/package.json +1 -1
- package/templates/hooks/post-tool.sh +4 -2
- package/templates/scripts/new-feature.sh +33 -9
package/dist/src/commands/run.js
CHANGED
|
@@ -6,11 +6,213 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { spawnSync } from "child_process";
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import path from "path";
|
|
9
11
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
10
12
|
import { getManifest } from "../lib/manifest.js";
|
|
11
13
|
import { getSettings } from "../lib/settings.js";
|
|
14
|
+
import { PM_CONFIG } from "../lib/stacks.js";
|
|
12
15
|
import { LogWriter, createPhaseLogFromTiming, } from "../lib/workflow/log-writer.js";
|
|
13
16
|
import { DEFAULT_PHASES, DEFAULT_CONFIG, } from "../lib/workflow/types.js";
|
|
17
|
+
/**
|
|
18
|
+
* Slugify a title for branch naming
|
|
19
|
+
*/
|
|
20
|
+
function slugify(title) {
|
|
21
|
+
return title
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
24
|
+
.replace(/^-+|-+$/g, "")
|
|
25
|
+
.substring(0, 50);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get the git repository root directory
|
|
29
|
+
*/
|
|
30
|
+
function getGitRoot() {
|
|
31
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
32
|
+
stdio: "pipe",
|
|
33
|
+
});
|
|
34
|
+
if (result.status === 0) {
|
|
35
|
+
return result.stdout.toString().trim();
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if a worktree exists for a given branch
|
|
41
|
+
*/
|
|
42
|
+
function findExistingWorktree(branch) {
|
|
43
|
+
const result = spawnSync("git", ["worktree", "list", "--porcelain"], {
|
|
44
|
+
stdio: "pipe",
|
|
45
|
+
});
|
|
46
|
+
if (result.status !== 0)
|
|
47
|
+
return null;
|
|
48
|
+
const output = result.stdout.toString();
|
|
49
|
+
const lines = output.split("\n");
|
|
50
|
+
let currentPath = "";
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
if (line.startsWith("worktree ")) {
|
|
53
|
+
currentPath = line.substring(9);
|
|
54
|
+
}
|
|
55
|
+
else if (line.startsWith("branch refs/heads/") && line.includes(branch)) {
|
|
56
|
+
return currentPath;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* List all active worktrees with their branches
|
|
63
|
+
*/
|
|
64
|
+
export function listWorktrees() {
|
|
65
|
+
const result = spawnSync("git", ["worktree", "list", "--porcelain"], {
|
|
66
|
+
stdio: "pipe",
|
|
67
|
+
});
|
|
68
|
+
if (result.status !== 0)
|
|
69
|
+
return [];
|
|
70
|
+
const output = result.stdout.toString();
|
|
71
|
+
const lines = output.split("\n");
|
|
72
|
+
const worktrees = [];
|
|
73
|
+
let currentPath = "";
|
|
74
|
+
let currentBranch = "";
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
if (line.startsWith("worktree ")) {
|
|
77
|
+
currentPath = line.substring(9);
|
|
78
|
+
}
|
|
79
|
+
else if (line.startsWith("branch refs/heads/")) {
|
|
80
|
+
currentBranch = line.substring(18);
|
|
81
|
+
// Extract issue number from branch name (e.g., feature/123-some-title)
|
|
82
|
+
const issueMatch = currentBranch.match(/feature\/(\d+)-/);
|
|
83
|
+
const issue = issueMatch ? parseInt(issueMatch[1], 10) : null;
|
|
84
|
+
worktrees.push({ path: currentPath, branch: currentBranch, issue });
|
|
85
|
+
currentPath = "";
|
|
86
|
+
currentBranch = "";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return worktrees;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get changed files in a worktree compared to main
|
|
93
|
+
*/
|
|
94
|
+
export function getWorktreeChangedFiles(worktreePath) {
|
|
95
|
+
const result = spawnSync("git", ["-C", worktreePath, "diff", "--name-only", "main...HEAD"], { stdio: "pipe" });
|
|
96
|
+
if (result.status !== 0)
|
|
97
|
+
return [];
|
|
98
|
+
return result.stdout
|
|
99
|
+
.toString()
|
|
100
|
+
.trim()
|
|
101
|
+
.split("\n")
|
|
102
|
+
.filter((f) => f.length > 0);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Create or reuse a worktree for an issue
|
|
106
|
+
*/
|
|
107
|
+
async function ensureWorktree(issueNumber, title, verbose, packageManager) {
|
|
108
|
+
const gitRoot = getGitRoot();
|
|
109
|
+
if (!gitRoot) {
|
|
110
|
+
console.log(chalk.red(" ❌ Not in a git repository"));
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const slug = slugify(title);
|
|
114
|
+
const branch = `feature/${issueNumber}-${slug}`;
|
|
115
|
+
const worktreesDir = path.join(path.dirname(gitRoot), "worktrees");
|
|
116
|
+
const worktreePath = path.join(worktreesDir, branch);
|
|
117
|
+
// Check if worktree already exists
|
|
118
|
+
const existingPath = findExistingWorktree(branch);
|
|
119
|
+
if (existingPath) {
|
|
120
|
+
if (verbose) {
|
|
121
|
+
console.log(chalk.gray(` 📂 Reusing existing worktree: ${existingPath}`));
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
issue: issueNumber,
|
|
125
|
+
path: existingPath,
|
|
126
|
+
branch,
|
|
127
|
+
existed: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// Check if branch exists (but no worktree)
|
|
131
|
+
const branchCheck = spawnSync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { stdio: "pipe" });
|
|
132
|
+
const branchExists = branchCheck.status === 0;
|
|
133
|
+
if (verbose) {
|
|
134
|
+
console.log(chalk.gray(` 🌿 Creating worktree for #${issueNumber}...`));
|
|
135
|
+
}
|
|
136
|
+
// Ensure worktrees directory exists
|
|
137
|
+
if (!existsSync(worktreesDir)) {
|
|
138
|
+
spawnSync("mkdir", ["-p", worktreesDir], { stdio: "pipe" });
|
|
139
|
+
}
|
|
140
|
+
// Create the worktree
|
|
141
|
+
let createResult;
|
|
142
|
+
if (branchExists) {
|
|
143
|
+
// Use existing branch
|
|
144
|
+
createResult = spawnSync("git", ["worktree", "add", worktreePath, branch], {
|
|
145
|
+
stdio: "pipe",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Create new branch from main
|
|
150
|
+
createResult = spawnSync("git", ["worktree", "add", worktreePath, "-b", branch], { stdio: "pipe" });
|
|
151
|
+
}
|
|
152
|
+
if (createResult.status !== 0) {
|
|
153
|
+
const error = createResult.stderr.toString();
|
|
154
|
+
console.log(chalk.red(` ❌ Failed to create worktree: ${error}`));
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
// Copy .env.local if it exists
|
|
158
|
+
const envLocalSrc = path.join(gitRoot, ".env.local");
|
|
159
|
+
const envLocalDst = path.join(worktreePath, ".env.local");
|
|
160
|
+
if (existsSync(envLocalSrc) && !existsSync(envLocalDst)) {
|
|
161
|
+
spawnSync("cp", [envLocalSrc, envLocalDst], { stdio: "pipe" });
|
|
162
|
+
}
|
|
163
|
+
// Copy .claude/settings.local.json if it exists
|
|
164
|
+
const claudeSettingsSrc = path.join(gitRoot, ".claude", "settings.local.json");
|
|
165
|
+
const claudeSettingsDst = path.join(worktreePath, ".claude", "settings.local.json");
|
|
166
|
+
if (existsSync(claudeSettingsSrc) && !existsSync(claudeSettingsDst)) {
|
|
167
|
+
spawnSync("mkdir", ["-p", path.join(worktreePath, ".claude")], {
|
|
168
|
+
stdio: "pipe",
|
|
169
|
+
});
|
|
170
|
+
spawnSync("cp", [claudeSettingsSrc, claudeSettingsDst], { stdio: "pipe" });
|
|
171
|
+
}
|
|
172
|
+
// Install dependencies if needed
|
|
173
|
+
const nodeModulesPath = path.join(worktreePath, "node_modules");
|
|
174
|
+
if (!existsSync(nodeModulesPath)) {
|
|
175
|
+
if (verbose) {
|
|
176
|
+
console.log(chalk.gray(` 📦 Installing dependencies...`));
|
|
177
|
+
}
|
|
178
|
+
// Use detected package manager or default to npm
|
|
179
|
+
const pm = packageManager || "npm";
|
|
180
|
+
const pmConfig = PM_CONFIG[pm];
|
|
181
|
+
const [cmd, ...args] = pmConfig.installSilent.split(" ");
|
|
182
|
+
spawnSync(cmd, args, {
|
|
183
|
+
cwd: worktreePath,
|
|
184
|
+
stdio: "pipe",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (verbose) {
|
|
188
|
+
console.log(chalk.green(` ✅ Worktree ready: ${worktreePath}`));
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
issue: issueNumber,
|
|
192
|
+
path: worktreePath,
|
|
193
|
+
branch,
|
|
194
|
+
existed: false,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Ensure worktrees exist for all issues before execution
|
|
199
|
+
*/
|
|
200
|
+
async function ensureWorktrees(issues, verbose, packageManager) {
|
|
201
|
+
const worktrees = new Map();
|
|
202
|
+
console.log(chalk.blue("\n 📂 Preparing worktrees..."));
|
|
203
|
+
for (const issue of issues) {
|
|
204
|
+
const worktree = await ensureWorktree(issue.number, issue.title, verbose, packageManager);
|
|
205
|
+
if (worktree) {
|
|
206
|
+
worktrees.set(issue.number, worktree);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const created = Array.from(worktrees.values()).filter((w) => !w.existed).length;
|
|
210
|
+
const reused = Array.from(worktrees.values()).filter((w) => w.existed).length;
|
|
211
|
+
if (created > 0 || reused > 0) {
|
|
212
|
+
console.log(chalk.gray(` Worktrees: ${created} created, ${reused} reused`));
|
|
213
|
+
}
|
|
214
|
+
return worktrees;
|
|
215
|
+
}
|
|
14
216
|
/**
|
|
15
217
|
* Natural language prompts for each phase
|
|
16
218
|
* These prompts will invoke the corresponding skills via natural language
|
|
@@ -122,10 +324,15 @@ function formatDuration(seconds) {
|
|
|
122
324
|
function getPhasePrompt(phase, issueNumber) {
|
|
123
325
|
return PHASE_PROMPTS[phase].replace(/\{issue\}/g, String(issueNumber));
|
|
124
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Phases that require worktree isolation (exec, test, qa)
|
|
329
|
+
* Spec runs in main repo since it's planning-only
|
|
330
|
+
*/
|
|
331
|
+
const ISOLATED_PHASES = ["exec", "test", "qa"];
|
|
125
332
|
/**
|
|
126
333
|
* Execute a single phase for an issue using Claude Agent SDK
|
|
127
334
|
*/
|
|
128
|
-
async function executePhase(issueNumber, phase, config, sessionId) {
|
|
335
|
+
async function executePhase(issueNumber, phase, config, sessionId, worktreePath) {
|
|
129
336
|
const startTime = Date.now();
|
|
130
337
|
if (config.dryRun) {
|
|
131
338
|
// Dry run - just simulate
|
|
@@ -141,7 +348,13 @@ async function executePhase(issueNumber, phase, config, sessionId) {
|
|
|
141
348
|
const prompt = getPhasePrompt(phase, issueNumber);
|
|
142
349
|
if (config.verbose) {
|
|
143
350
|
console.log(chalk.gray(` Prompt: ${prompt}`));
|
|
351
|
+
if (worktreePath && ISOLATED_PHASES.includes(phase)) {
|
|
352
|
+
console.log(chalk.gray(` Worktree: ${worktreePath}`));
|
|
353
|
+
}
|
|
144
354
|
}
|
|
355
|
+
// Determine working directory and environment
|
|
356
|
+
const shouldUseWorktree = worktreePath && ISOLATED_PHASES.includes(phase);
|
|
357
|
+
const cwd = shouldUseWorktree ? worktreePath : process.cwd();
|
|
145
358
|
try {
|
|
146
359
|
// Create abort controller for timeout
|
|
147
360
|
const abortController = new AbortController();
|
|
@@ -152,12 +365,24 @@ async function executePhase(issueNumber, phase, config, sessionId) {
|
|
|
152
365
|
let resultMessage;
|
|
153
366
|
let lastError;
|
|
154
367
|
let capturedOutput = "";
|
|
368
|
+
// Build environment with worktree isolation variables
|
|
369
|
+
const env = {
|
|
370
|
+
...process.env,
|
|
371
|
+
CLAUDE_HOOKS_SMART_TESTS: config.noSmartTests ? "false" : "true",
|
|
372
|
+
};
|
|
373
|
+
// Set worktree isolation environment variables
|
|
374
|
+
if (shouldUseWorktree) {
|
|
375
|
+
env.SEQUANT_WORKTREE = worktreePath;
|
|
376
|
+
env.SEQUANT_ISSUE = String(issueNumber);
|
|
377
|
+
}
|
|
155
378
|
// Execute using Claude Agent SDK
|
|
379
|
+
// Note: Don't resume sessions when switching to worktree (different cwd breaks resume)
|
|
380
|
+
const canResume = sessionId && !shouldUseWorktree;
|
|
156
381
|
const queryInstance = query({
|
|
157
382
|
prompt,
|
|
158
383
|
options: {
|
|
159
384
|
abortController,
|
|
160
|
-
cwd
|
|
385
|
+
cwd,
|
|
161
386
|
// Load project settings including skills
|
|
162
387
|
settingSources: ["project"],
|
|
163
388
|
// Use Claude Code's system prompt and tools
|
|
@@ -166,13 +391,10 @@ async function executePhase(issueNumber, phase, config, sessionId) {
|
|
|
166
391
|
// Bypass permissions for headless execution
|
|
167
392
|
permissionMode: "bypassPermissions",
|
|
168
393
|
allowDangerouslySkipPermissions: true,
|
|
169
|
-
// Resume from previous session if provided
|
|
170
|
-
...(
|
|
171
|
-
// Configure smart tests via environment
|
|
172
|
-
env
|
|
173
|
-
...process.env,
|
|
174
|
-
CLAUDE_HOOKS_SMART_TESTS: config.noSmartTests ? "false" : "true",
|
|
175
|
-
},
|
|
394
|
+
// Resume from previous session if provided (but not when switching directories)
|
|
395
|
+
...(canResume ? { resume: sessionId } : {}),
|
|
396
|
+
// Configure smart tests and worktree isolation via environment
|
|
397
|
+
env,
|
|
176
398
|
},
|
|
177
399
|
});
|
|
178
400
|
// Stream and process messages
|
|
@@ -274,21 +496,14 @@ async function executePhase(issueNumber, phase, config, sessionId) {
|
|
|
274
496
|
*/
|
|
275
497
|
async function getIssueInfo(issueNumber) {
|
|
276
498
|
try {
|
|
277
|
-
const result = spawnSync("gh", [
|
|
278
|
-
"issue",
|
|
279
|
-
"view",
|
|
280
|
-
String(issueNumber),
|
|
281
|
-
"--json",
|
|
282
|
-
"title,labels",
|
|
283
|
-
"--jq",
|
|
284
|
-
'"\(.title)|\(.labels | map(.name) | join(","))"',
|
|
285
|
-
], { stdio: "pipe", shell: true });
|
|
499
|
+
const result = spawnSync("gh", ["issue", "view", String(issueNumber), "--json", "title,labels"], { stdio: "pipe" });
|
|
286
500
|
if (result.status === 0) {
|
|
287
|
-
const
|
|
288
|
-
const [title, labelsStr] = output.split("|");
|
|
501
|
+
const data = JSON.parse(result.stdout.toString());
|
|
289
502
|
return {
|
|
290
|
-
title: title || `Issue #${issueNumber}`,
|
|
291
|
-
labels:
|
|
503
|
+
title: data.title || `Issue #${issueNumber}`,
|
|
504
|
+
labels: Array.isArray(data.labels)
|
|
505
|
+
? data.labels.map((l) => l.name)
|
|
506
|
+
: [],
|
|
292
507
|
};
|
|
293
508
|
}
|
|
294
509
|
}
|
|
@@ -297,6 +512,105 @@ async function getIssueInfo(issueNumber) {
|
|
|
297
512
|
}
|
|
298
513
|
return { title: `Issue #${issueNumber}`, labels: [] };
|
|
299
514
|
}
|
|
515
|
+
/**
|
|
516
|
+
* Parse dependencies from issue body and labels
|
|
517
|
+
* Returns array of issue numbers this issue depends on
|
|
518
|
+
*/
|
|
519
|
+
function parseDependencies(issueNumber) {
|
|
520
|
+
try {
|
|
521
|
+
const result = spawnSync("gh", ["issue", "view", String(issueNumber), "--json", "body,labels"], { stdio: "pipe" });
|
|
522
|
+
if (result.status !== 0)
|
|
523
|
+
return [];
|
|
524
|
+
const data = JSON.parse(result.stdout.toString());
|
|
525
|
+
const dependencies = [];
|
|
526
|
+
// Parse from body: "Depends on: #123" or "**Depends on**: #123"
|
|
527
|
+
if (data.body) {
|
|
528
|
+
const bodyMatch = data.body.match(/\*?\*?depends\s+on\*?\*?:?\s*#?(\d+)/gi);
|
|
529
|
+
if (bodyMatch) {
|
|
530
|
+
for (const match of bodyMatch) {
|
|
531
|
+
const numMatch = match.match(/(\d+)/);
|
|
532
|
+
if (numMatch) {
|
|
533
|
+
dependencies.push(parseInt(numMatch[1], 10));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// Parse from labels: "depends-on/123" or "depends-on-123"
|
|
539
|
+
if (data.labels && Array.isArray(data.labels)) {
|
|
540
|
+
for (const label of data.labels) {
|
|
541
|
+
const labelName = label.name || label;
|
|
542
|
+
const labelMatch = labelName.match(/depends-on[-/](\d+)/i);
|
|
543
|
+
if (labelMatch) {
|
|
544
|
+
dependencies.push(parseInt(labelMatch[1], 10));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return [...new Set(dependencies)]; // Remove duplicates
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
return [];
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Sort issues by dependencies (topological sort)
|
|
556
|
+
* Issues with no dependencies come first, then issues that depend on them
|
|
557
|
+
*/
|
|
558
|
+
function sortByDependencies(issueNumbers) {
|
|
559
|
+
// Build dependency graph
|
|
560
|
+
const dependsOn = new Map();
|
|
561
|
+
for (const issue of issueNumbers) {
|
|
562
|
+
const deps = parseDependencies(issue);
|
|
563
|
+
// Only include dependencies that are in our issue list
|
|
564
|
+
dependsOn.set(issue, deps.filter((d) => issueNumbers.includes(d)));
|
|
565
|
+
}
|
|
566
|
+
// Topological sort using Kahn's algorithm
|
|
567
|
+
const inDegree = new Map();
|
|
568
|
+
for (const issue of issueNumbers) {
|
|
569
|
+
inDegree.set(issue, 0);
|
|
570
|
+
}
|
|
571
|
+
for (const deps of dependsOn.values()) {
|
|
572
|
+
for (const dep of deps) {
|
|
573
|
+
inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// Note: inDegree counts how many issues depend on each issue
|
|
577
|
+
// We want to process issues that nothing depends on last
|
|
578
|
+
// So we sort by: issues nothing depends on first, then dependent issues
|
|
579
|
+
const sorted = [];
|
|
580
|
+
const queue = [];
|
|
581
|
+
// Start with issues that have no dependencies
|
|
582
|
+
for (const issue of issueNumbers) {
|
|
583
|
+
const deps = dependsOn.get(issue) || [];
|
|
584
|
+
if (deps.length === 0) {
|
|
585
|
+
queue.push(issue);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const visited = new Set();
|
|
589
|
+
while (queue.length > 0) {
|
|
590
|
+
const issue = queue.shift();
|
|
591
|
+
if (visited.has(issue))
|
|
592
|
+
continue;
|
|
593
|
+
visited.add(issue);
|
|
594
|
+
sorted.push(issue);
|
|
595
|
+
// Find issues that depend on this one
|
|
596
|
+
for (const [other, deps] of dependsOn.entries()) {
|
|
597
|
+
if (deps.includes(issue) && !visited.has(other)) {
|
|
598
|
+
// Check if all dependencies of 'other' are satisfied
|
|
599
|
+
const allDepsSatisfied = deps.every((d) => visited.has(d));
|
|
600
|
+
if (allDepsSatisfied) {
|
|
601
|
+
queue.push(other);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// Add any remaining issues (circular dependencies or unvisited)
|
|
607
|
+
for (const issue of issueNumbers) {
|
|
608
|
+
if (!visited.has(issue)) {
|
|
609
|
+
sorted.push(issue);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return sorted;
|
|
613
|
+
}
|
|
300
614
|
/**
|
|
301
615
|
* Check if an issue has UI-related labels
|
|
302
616
|
*/
|
|
@@ -404,11 +718,20 @@ export async function runCommand(issues, options) {
|
|
|
404
718
|
}
|
|
405
719
|
if (issueNumbers.length === 0) {
|
|
406
720
|
console.log(chalk.red("❌ No valid issue numbers provided."));
|
|
407
|
-
console.log(chalk.gray("\nUsage: sequant run <issues...> [options]"));
|
|
408
|
-
console.log(chalk.gray("Example: sequant run 1 2 3 --sequential"));
|
|
409
|
-
console.log(chalk.gray('Batch example: sequant run --batch "1 2" --batch "3"'));
|
|
721
|
+
console.log(chalk.gray("\nUsage: npx sequant run <issues...> [options]"));
|
|
722
|
+
console.log(chalk.gray("Example: npx sequant run 1 2 3 --sequential"));
|
|
723
|
+
console.log(chalk.gray('Batch example: npx sequant run --batch "1 2" --batch "3"'));
|
|
410
724
|
return;
|
|
411
725
|
}
|
|
726
|
+
// Sort issues by dependencies (if more than one issue)
|
|
727
|
+
if (issueNumbers.length > 1 && !batches) {
|
|
728
|
+
const originalOrder = [...issueNumbers];
|
|
729
|
+
issueNumbers = sortByDependencies(issueNumbers);
|
|
730
|
+
const orderChanged = !originalOrder.every((n, i) => n === issueNumbers[i]);
|
|
731
|
+
if (orderChanged) {
|
|
732
|
+
console.log(chalk.gray(` Dependency order: ${issueNumbers.map((n) => `#${n}`).join(" → ")}`));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
412
735
|
// Build config
|
|
413
736
|
// Note: config.phases is only used when --phases is explicitly set or autoDetect fails
|
|
414
737
|
const explicitPhases = mergedOptions.phases
|
|
@@ -469,6 +792,25 @@ export async function runCommand(issues, options) {
|
|
|
469
792
|
console.log(chalk.gray(` Logging: JSON (run ${logWriter.getRunId()?.slice(0, 8)}...)`));
|
|
470
793
|
}
|
|
471
794
|
console.log(chalk.gray(` Issues: ${issueNumbers.map((n) => `#${n}`).join(", ")}`));
|
|
795
|
+
// Worktree isolation is enabled by default for multi-issue runs
|
|
796
|
+
const useWorktreeIsolation = mergedOptions.worktreeIsolation !== false && issueNumbers.length > 0;
|
|
797
|
+
if (useWorktreeIsolation) {
|
|
798
|
+
console.log(chalk.gray(` Worktree isolation: enabled`));
|
|
799
|
+
}
|
|
800
|
+
// Fetch issue info for all issues first
|
|
801
|
+
const issueInfoMap = new Map();
|
|
802
|
+
for (const issueNumber of issueNumbers) {
|
|
803
|
+
issueInfoMap.set(issueNumber, await getIssueInfo(issueNumber));
|
|
804
|
+
}
|
|
805
|
+
// Create worktrees for all issues before execution (if isolation enabled)
|
|
806
|
+
let worktreeMap = new Map();
|
|
807
|
+
if (useWorktreeIsolation && !config.dryRun) {
|
|
808
|
+
const issueData = issueNumbers.map((num) => ({
|
|
809
|
+
number: num,
|
|
810
|
+
title: issueInfoMap.get(num)?.title || `Issue #${num}`,
|
|
811
|
+
}));
|
|
812
|
+
worktreeMap = await ensureWorktrees(issueData, config.verbose, manifest.packageManager);
|
|
813
|
+
}
|
|
472
814
|
// Execute
|
|
473
815
|
const results = [];
|
|
474
816
|
if (batches) {
|
|
@@ -476,7 +818,7 @@ export async function runCommand(issues, options) {
|
|
|
476
818
|
for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
|
|
477
819
|
const batch = batches[batchIdx];
|
|
478
820
|
console.log(chalk.blue(`\n Batch ${batchIdx + 1}/${batches.length}: Issues ${batch.map((n) => `#${n}`).join(", ")}`));
|
|
479
|
-
const batchResults = await executeBatch(batch, config, logWriter, mergedOptions);
|
|
821
|
+
const batchResults = await executeBatch(batch, config, logWriter, mergedOptions, issueInfoMap, worktreeMap);
|
|
480
822
|
results.push(...batchResults);
|
|
481
823
|
// Check if batch failed and we should stop
|
|
482
824
|
const batchFailed = batchResults.some((r) => !r.success);
|
|
@@ -489,12 +831,16 @@ export async function runCommand(issues, options) {
|
|
|
489
831
|
else if (config.sequential) {
|
|
490
832
|
// Sequential execution
|
|
491
833
|
for (const issueNumber of issueNumbers) {
|
|
492
|
-
const issueInfo =
|
|
834
|
+
const issueInfo = issueInfoMap.get(issueNumber) ?? {
|
|
835
|
+
title: `Issue #${issueNumber}`,
|
|
836
|
+
labels: [],
|
|
837
|
+
};
|
|
838
|
+
const worktreeInfo = worktreeMap.get(issueNumber);
|
|
493
839
|
// Start issue logging
|
|
494
840
|
if (logWriter) {
|
|
495
841
|
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
496
842
|
}
|
|
497
|
-
const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions);
|
|
843
|
+
const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions, worktreeInfo?.path);
|
|
498
844
|
results.push(result);
|
|
499
845
|
// Complete issue logging
|
|
500
846
|
if (logWriter) {
|
|
@@ -510,12 +856,16 @@ export async function runCommand(issues, options) {
|
|
|
510
856
|
// Parallel execution (for now, just run sequentially but don't stop on failure)
|
|
511
857
|
// TODO: Add proper parallel execution with listr2
|
|
512
858
|
for (const issueNumber of issueNumbers) {
|
|
513
|
-
const issueInfo =
|
|
859
|
+
const issueInfo = issueInfoMap.get(issueNumber) ?? {
|
|
860
|
+
title: `Issue #${issueNumber}`,
|
|
861
|
+
labels: [],
|
|
862
|
+
};
|
|
863
|
+
const worktreeInfo = worktreeMap.get(issueNumber);
|
|
514
864
|
// Start issue logging
|
|
515
865
|
if (logWriter) {
|
|
516
866
|
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
517
867
|
}
|
|
518
|
-
const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions);
|
|
868
|
+
const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions, worktreeInfo?.path);
|
|
519
869
|
results.push(result);
|
|
520
870
|
// Complete issue logging
|
|
521
871
|
if (logWriter) {
|
|
@@ -563,15 +913,19 @@ export async function runCommand(issues, options) {
|
|
|
563
913
|
/**
|
|
564
914
|
* Execute a batch of issues
|
|
565
915
|
*/
|
|
566
|
-
async function executeBatch(issueNumbers, config, logWriter, options) {
|
|
916
|
+
async function executeBatch(issueNumbers, config, logWriter, options, issueInfoMap, worktreeMap) {
|
|
567
917
|
const results = [];
|
|
568
918
|
for (const issueNumber of issueNumbers) {
|
|
569
|
-
const issueInfo =
|
|
919
|
+
const issueInfo = issueInfoMap.get(issueNumber) ?? {
|
|
920
|
+
title: `Issue #${issueNumber}`,
|
|
921
|
+
labels: [],
|
|
922
|
+
};
|
|
923
|
+
const worktreeInfo = worktreeMap.get(issueNumber);
|
|
570
924
|
// Start issue logging
|
|
571
925
|
if (logWriter) {
|
|
572
926
|
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
573
927
|
}
|
|
574
|
-
const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, options);
|
|
928
|
+
const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, options, worktreeInfo?.path);
|
|
575
929
|
results.push(result);
|
|
576
930
|
// Complete issue logging
|
|
577
931
|
if (logWriter) {
|
|
@@ -583,12 +937,15 @@ async function executeBatch(issueNumbers, config, logWriter, options) {
|
|
|
583
937
|
/**
|
|
584
938
|
* Execute all phases for a single issue with logging and quality loop
|
|
585
939
|
*/
|
|
586
|
-
async function runIssueWithLogging(issueNumber, config, logWriter, labels, options) {
|
|
940
|
+
async function runIssueWithLogging(issueNumber, config, logWriter, labels, options, worktreePath) {
|
|
587
941
|
const startTime = Date.now();
|
|
588
942
|
const phaseResults = [];
|
|
589
943
|
let loopTriggered = false;
|
|
590
944
|
let sessionId;
|
|
591
945
|
console.log(chalk.blue(`\n Issue #${issueNumber}`));
|
|
946
|
+
if (worktreePath) {
|
|
947
|
+
console.log(chalk.gray(` Worktree: ${worktreePath}`));
|
|
948
|
+
}
|
|
592
949
|
// Determine phases for this specific issue
|
|
593
950
|
let phases;
|
|
594
951
|
let detectedQualityLoop = false;
|
|
@@ -607,7 +964,8 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
|
|
|
607
964
|
console.log(chalk.gray(` Running spec to determine workflow...`));
|
|
608
965
|
console.log(chalk.gray(` ⏳ spec...`));
|
|
609
966
|
const specStartTime = new Date();
|
|
610
|
-
|
|
967
|
+
// Note: spec runs in main repo (not worktree) for planning
|
|
968
|
+
const specResult = await executePhase(issueNumber, "spec", config, sessionId, worktreePath);
|
|
611
969
|
const specEndTime = new Date();
|
|
612
970
|
if (specResult.sessionId) {
|
|
613
971
|
sessionId = specResult.sessionId;
|
|
@@ -693,7 +1051,7 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
|
|
|
693
1051
|
for (const phase of phases) {
|
|
694
1052
|
console.log(chalk.gray(` ⏳ ${phase}...`));
|
|
695
1053
|
const phaseStartTime = new Date();
|
|
696
|
-
const result = await executePhase(issueNumber, phase, config, sessionId);
|
|
1054
|
+
const result = await executePhase(issueNumber, phase, config, sessionId, worktreePath);
|
|
697
1055
|
const phaseEndTime = new Date();
|
|
698
1056
|
// Capture session ID for subsequent phases
|
|
699
1057
|
if (result.sessionId) {
|
|
@@ -721,7 +1079,7 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
|
|
|
721
1079
|
// If quality loop enabled, run loop phase to fix issues
|
|
722
1080
|
if (useQualityLoop && iteration < maxIterations) {
|
|
723
1081
|
console.log(chalk.yellow(` Running /loop to fix issues...`));
|
|
724
|
-
const loopResult = await executePhase(issueNumber, "loop", config, sessionId);
|
|
1082
|
+
const loopResult = await executePhase(issueNumber, "loop", config, sessionId, worktreePath);
|
|
725
1083
|
phaseResults.push(loopResult);
|
|
726
1084
|
if (loopResult.sessionId) {
|
|
727
1085
|
sessionId = loopResult.sessionId;
|