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.
Files changed (52) hide show
  1. package/README.md +112 -10
  2. package/dist/bin/cli.js +6 -1
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/src/commands/doctor.d.ts.map +1 -1
  5. package/dist/src/commands/doctor.js +33 -2
  6. package/dist/src/commands/doctor.js.map +1 -1
  7. package/dist/src/commands/doctor.test.js +63 -1
  8. package/dist/src/commands/doctor.test.js.map +1 -1
  9. package/dist/src/commands/init.d.ts +1 -0
  10. package/dist/src/commands/init.d.ts.map +1 -1
  11. package/dist/src/commands/init.js +40 -10
  12. package/dist/src/commands/init.js.map +1 -1
  13. package/dist/src/commands/init.test.js +100 -1
  14. package/dist/src/commands/init.test.js.map +1 -1
  15. package/dist/src/commands/logs.js +1 -1
  16. package/dist/src/commands/logs.js.map +1 -1
  17. package/dist/src/commands/run.d.ts +16 -0
  18. package/dist/src/commands/run.d.ts.map +1 -1
  19. package/dist/src/commands/run.js +395 -37
  20. package/dist/src/commands/run.js.map +1 -1
  21. package/dist/src/commands/run.test.d.ts +2 -0
  22. package/dist/src/commands/run.test.d.ts.map +1 -0
  23. package/dist/src/commands/run.test.js +155 -0
  24. package/dist/src/commands/run.test.js.map +1 -0
  25. package/dist/src/commands/update.d.ts.map +1 -1
  26. package/dist/src/commands/update.js +9 -5
  27. package/dist/src/commands/update.js.map +1 -1
  28. package/dist/src/lib/manifest.d.ts +3 -1
  29. package/dist/src/lib/manifest.d.ts.map +1 -1
  30. package/dist/src/lib/manifest.js +2 -1
  31. package/dist/src/lib/manifest.js.map +1 -1
  32. package/dist/src/lib/stacks.d.ts +27 -0
  33. package/dist/src/lib/stacks.d.ts.map +1 -1
  34. package/dist/src/lib/stacks.js +154 -17
  35. package/dist/src/lib/stacks.js.map +1 -1
  36. package/dist/src/lib/stacks.test.js +343 -1
  37. package/dist/src/lib/stacks.test.js.map +1 -1
  38. package/dist/src/lib/system.d.ts +8 -0
  39. package/dist/src/lib/system.d.ts.map +1 -1
  40. package/dist/src/lib/system.js +23 -0
  41. package/dist/src/lib/system.js.map +1 -1
  42. package/dist/src/lib/tty.d.ts +31 -0
  43. package/dist/src/lib/tty.d.ts.map +1 -0
  44. package/dist/src/lib/tty.js +81 -0
  45. package/dist/src/lib/tty.js.map +1 -0
  46. package/dist/src/lib/tty.test.d.ts +2 -0
  47. package/dist/src/lib/tty.test.d.ts.map +1 -0
  48. package/dist/src/lib/tty.test.js +227 -0
  49. package/dist/src/lib/tty.test.js.map +1 -0
  50. package/package.json +1 -1
  51. package/templates/hooks/post-tool.sh +4 -2
  52. package/templates/scripts/new-feature.sh +33 -9
@@ -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: process.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
- ...(sessionId ? { resume: sessionId } : {}),
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 output = result.stdout.toString().trim().replace(/^"|"$/g, "");
288
- const [title, labelsStr] = output.split("|");
501
+ const data = JSON.parse(result.stdout.toString());
289
502
  return {
290
- title: title || `Issue #${issueNumber}`,
291
- labels: labelsStr ? labelsStr.split(",").filter(Boolean) : [],
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 = await getIssueInfo(issueNumber);
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 = await getIssueInfo(issueNumber);
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 = await getIssueInfo(issueNumber);
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
- const specResult = await executePhase(issueNumber, "spec", config, sessionId);
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;