sequant 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,6 +84,7 @@ Run without Claude Code UI:
84
84
  npx sequant run 123 # Single issue
85
85
  npx sequant run 1 2 3 # Batch (parallel)
86
86
  npx sequant run 123 --quality-loop
87
+ npx sequant run 123 --base feature/dashboard # Custom base branch
87
88
  ```
88
89
 
89
90
  ---
@@ -148,7 +149,8 @@ See [Run Command Options](docs/run-command.md) for advanced usage.
148
149
  {
149
150
  "run": {
150
151
  "qualityLoop": false,
151
- "maxIterations": 3
152
+ "maxIterations": 3,
153
+ "defaultBase": "feature/dashboard" // Optional: custom default base branch
152
154
  }
153
155
  }
154
156
  ```
@@ -173,6 +175,7 @@ See [Customization Guide](docs/customization.md) for all options.
173
175
  - [Getting Started](docs/getting-started/installation.md)
174
176
  - [Workflow Concepts](docs/concepts/workflow-phases.md)
175
177
  - [Run Command](docs/run-command.md)
178
+ - [Feature Branch Workflows](docs/feature-branch-workflow.md)
176
179
  - [Customization](docs/customization.md)
177
180
  - [Troubleshooting](docs/troubleshooting.md)
178
181
 
package/dist/bin/cli.js CHANGED
@@ -99,6 +99,7 @@ program
99
99
  .option("--testgen", "Run testgen phase after spec")
100
100
  .option("--quiet", "Suppress version warnings and non-essential output")
101
101
  .option("--chain", "Chain issues: each branches from previous (requires --sequential)")
102
+ .option("--base <branch>", "Base branch for worktree creation (default: main or settings.run.defaultBase)")
102
103
  .action(runCommand);
103
104
  program
104
105
  .command("logs")
@@ -66,6 +66,11 @@ interface RunOptions {
66
66
  quiet?: boolean;
67
67
  /** Chain issues: each branches from previous (requires --sequential) */
68
68
  chain?: boolean;
69
+ /**
70
+ * Base branch for worktree creation.
71
+ * Resolution priority: this CLI flag → settings.run.defaultBase → 'main'
72
+ */
73
+ base?: string;
69
74
  }
70
75
  /**
71
76
  * Main run command
@@ -13,6 +13,7 @@ import { getManifest } from "../lib/manifest.js";
13
13
  import { getSettings } from "../lib/settings.js";
14
14
  import { PM_CONFIG } from "../lib/stacks.js";
15
15
  import { LogWriter, createPhaseLogFromTiming, } from "../lib/workflow/log-writer.js";
16
+ import { StateManager, } from "../lib/workflow/state-manager.js";
16
17
  import { DEFAULT_PHASES, DEFAULT_CONFIG, } from "../lib/workflow/types.js";
17
18
  import { ShutdownManager } from "../lib/shutdown.js";
18
19
  import { checkVersionCached, getVersionWarning } from "../lib/version-check.js";
@@ -137,17 +138,29 @@ async function ensureWorktree(issueNumber, title, verbose, packageManager, baseB
137
138
  console.log(chalk.gray(` 🌿 Creating worktree for #${issueNumber}...`));
138
139
  }
139
140
  // Determine the base for the new branch
140
- const baseRef = baseBranch || "origin/main";
141
- // Fetch latest main to ensure worktree starts from fresh baseline (unless using local branch)
142
- if (!baseBranch) {
141
+ // For custom base branches, use origin/<branch> if it's a remote-style reference
142
+ // For local branches (chain mode), use as-is
143
+ const isLocalBranch = baseBranch && !baseBranch.startsWith("origin/") && baseBranch !== "main";
144
+ const baseRef = baseBranch
145
+ ? isLocalBranch
146
+ ? baseBranch
147
+ : baseBranch.startsWith("origin/")
148
+ ? baseBranch
149
+ : `origin/${baseBranch}`
150
+ : "origin/main";
151
+ // Fetch the base branch to ensure worktree starts from fresh baseline
152
+ const branchToFetch = baseBranch
153
+ ? baseBranch.replace(/^origin\//, "")
154
+ : "main";
155
+ if (!isLocalBranch) {
143
156
  if (verbose) {
144
- console.log(chalk.gray(` 🔄 Fetching latest main...`));
157
+ console.log(chalk.gray(` 🔄 Fetching latest ${branchToFetch}...`));
145
158
  }
146
- const fetchResult = spawnSync("git", ["fetch", "origin", "main"], {
159
+ const fetchResult = spawnSync("git", ["fetch", "origin", branchToFetch], {
147
160
  stdio: "pipe",
148
161
  });
149
162
  if (fetchResult.status !== 0 && verbose) {
150
- console.log(chalk.yellow(` ⚠️ Could not fetch origin/main, using local state`));
163
+ console.log(chalk.yellow(` ⚠️ Could not fetch origin/${branchToFetch}, using local state`));
151
164
  }
152
165
  }
153
166
  else if (verbose) {
@@ -216,12 +229,14 @@ async function ensureWorktree(issueNumber, title, verbose, packageManager, baseB
216
229
  }
217
230
  /**
218
231
  * Ensure worktrees exist for all issues before execution
232
+ * @param baseBranch - Optional base branch for worktree creation (default: main)
219
233
  */
220
- async function ensureWorktrees(issues, verbose, packageManager) {
234
+ async function ensureWorktrees(issues, verbose, packageManager, baseBranch) {
221
235
  const worktrees = new Map();
222
- console.log(chalk.blue("\n 📂 Preparing worktrees..."));
236
+ const baseDisplay = baseBranch || "main";
237
+ console.log(chalk.blue(`\n 📂 Preparing worktrees from ${baseDisplay}...`));
223
238
  for (const issue of issues) {
224
- const worktree = await ensureWorktree(issue.number, issue.title, verbose, packageManager);
239
+ const worktree = await ensureWorktree(issue.number, issue.title, verbose, packageManager, baseBranch);
225
240
  if (worktree) {
226
241
  worktrees.set(issue.number, worktree);
227
242
  }
@@ -236,11 +251,14 @@ async function ensureWorktrees(issues, verbose, packageManager) {
236
251
  /**
237
252
  * Ensure worktrees exist for all issues in chain mode
238
253
  * Each issue branches from the previous issue's branch
254
+ * @param baseBranch - Optional starting base branch for the chain (default: main)
239
255
  */
240
- async function ensureWorktreesChain(issues, verbose, packageManager) {
256
+ async function ensureWorktreesChain(issues, verbose, packageManager, baseBranch) {
241
257
  const worktrees = new Map();
242
- console.log(chalk.blue("\n 🔗 Preparing chained worktrees..."));
243
- let previousBranch;
258
+ const baseDisplay = baseBranch || "main";
259
+ console.log(chalk.blue(`\n 🔗 Preparing chained worktrees from ${baseDisplay}...`));
260
+ // First issue starts from the specified base branch (or main)
261
+ let previousBranch = baseBranch;
244
262
  for (const issue of issues) {
245
263
  const worktree = await ensureWorktree(issue.number, issue.title, verbose, packageManager, previousBranch);
246
264
  if (worktree) {
@@ -264,7 +282,7 @@ async function ensureWorktreesChain(issues, verbose, packageManager) {
264
282
  .filter((i) => worktrees.has(i.number))
265
283
  .map((i) => `#${i.number}`)
266
284
  .join(" → ");
267
- console.log(chalk.gray(` Chain: origin/main → ${chainOrder}`));
285
+ console.log(chalk.gray(` Chain: ${baseDisplay} → ${chainOrder}`));
268
286
  }
269
287
  return worktrees;
270
288
  }
@@ -869,6 +887,8 @@ export async function runCommand(issues, options) {
869
887
  // Determine if we should auto-detect phases from labels
870
888
  const autoDetectPhases = !options.phases && settings.run.autoDetectPhases;
871
889
  mergedOptions.autoDetectPhases = autoDetectPhases;
890
+ // Resolve base branch: CLI flag → settings.run.defaultBase → 'main'
891
+ const resolvedBaseBranch = options.base ?? settings.run.defaultBase ?? undefined;
872
892
  // Parse issue numbers (or use batch mode)
873
893
  let issueNumbers;
874
894
  let batches = null;
@@ -953,6 +973,12 @@ export async function runCommand(issues, options) {
953
973
  });
954
974
  await logWriter.initialize(runConfig);
955
975
  }
976
+ // Initialize state manager for persistent workflow state tracking
977
+ // State tracking is always enabled (unless dry run)
978
+ let stateManager = null;
979
+ if (!config.dryRun) {
980
+ stateManager = new StateManager({ verbose: config.verbose });
981
+ }
956
982
  // Initialize shutdown manager for graceful interruption handling
957
983
  const shutdown = new ShutdownManager();
958
984
  // Register log writer finalization as cleanup task
@@ -986,12 +1012,18 @@ export async function runCommand(issues, options) {
986
1012
  if (logWriter) {
987
1013
  console.log(chalk.gray(` Logging: JSON (run ${logWriter.getRunId()?.slice(0, 8)}...)`));
988
1014
  }
1015
+ if (stateManager) {
1016
+ console.log(chalk.gray(` State tracking: enabled`));
1017
+ }
989
1018
  console.log(chalk.gray(` Issues: ${issueNumbers.map((n) => `#${n}`).join(", ")}`));
990
1019
  // Worktree isolation is enabled by default for multi-issue runs
991
1020
  const useWorktreeIsolation = mergedOptions.worktreeIsolation !== false && issueNumbers.length > 0;
992
1021
  if (useWorktreeIsolation) {
993
1022
  console.log(chalk.gray(` Worktree isolation: enabled`));
994
1023
  }
1024
+ if (resolvedBaseBranch) {
1025
+ console.log(chalk.gray(` Base branch: ${resolvedBaseBranch}`));
1026
+ }
995
1027
  if (mergedOptions.chain) {
996
1028
  console.log(chalk.gray(` Chain mode: enabled (each issue branches from previous)`));
997
1029
  }
@@ -1009,10 +1041,10 @@ export async function runCommand(issues, options) {
1009
1041
  }));
1010
1042
  // Use chain mode or standard worktree creation
1011
1043
  if (mergedOptions.chain) {
1012
- worktreeMap = await ensureWorktreesChain(issueData, config.verbose, manifest.packageManager);
1044
+ worktreeMap = await ensureWorktreesChain(issueData, config.verbose, manifest.packageManager, resolvedBaseBranch);
1013
1045
  }
1014
1046
  else {
1015
- worktreeMap = await ensureWorktrees(issueData, config.verbose, manifest.packageManager);
1047
+ worktreeMap = await ensureWorktrees(issueData, config.verbose, manifest.packageManager, resolvedBaseBranch);
1016
1048
  }
1017
1049
  // Register cleanup tasks for newly created worktrees (not pre-existing ones)
1018
1050
  for (const [issueNum, worktree] of worktreeMap.entries()) {
@@ -1038,7 +1070,7 @@ export async function runCommand(issues, options) {
1038
1070
  for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
1039
1071
  const batch = batches[batchIdx];
1040
1072
  console.log(chalk.blue(`\n Batch ${batchIdx + 1}/${batches.length}: Issues ${batch.map((n) => `#${n}`).join(", ")}`));
1041
- const batchResults = await executeBatch(batch, config, logWriter, mergedOptions, issueInfoMap, worktreeMap, shutdown);
1073
+ const batchResults = await executeBatch(batch, config, logWriter, stateManager, mergedOptions, issueInfoMap, worktreeMap, shutdown);
1042
1074
  results.push(...batchResults);
1043
1075
  // Check if batch failed and we should stop
1044
1076
  const batchFailed = batchResults.some((r) => !r.success);
@@ -1060,7 +1092,7 @@ export async function runCommand(issues, options) {
1060
1092
  if (logWriter) {
1061
1093
  logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
1062
1094
  }
1063
- const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions, worktreeInfo?.path, shutdown, mergedOptions.chain);
1095
+ const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, mergedOptions, worktreeInfo?.path, worktreeInfo?.branch, shutdown, mergedOptions.chain);
1064
1096
  results.push(result);
1065
1097
  // Complete issue logging
1066
1098
  if (logWriter) {
@@ -1094,7 +1126,7 @@ export async function runCommand(issues, options) {
1094
1126
  if (logWriter) {
1095
1127
  logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
1096
1128
  }
1097
- const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions, worktreeInfo?.path, shutdown, false);
1129
+ const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, mergedOptions, worktreeInfo?.path, worktreeInfo?.branch, shutdown, false);
1098
1130
  results.push(result);
1099
1131
  // Complete issue logging
1100
1132
  if (logWriter) {
@@ -1151,7 +1183,7 @@ export async function runCommand(issues, options) {
1151
1183
  /**
1152
1184
  * Execute a batch of issues
1153
1185
  */
1154
- async function executeBatch(issueNumbers, config, logWriter, options, issueInfoMap, worktreeMap, shutdownManager) {
1186
+ async function executeBatch(issueNumbers, config, logWriter, stateManager, options, issueInfoMap, worktreeMap, shutdownManager) {
1155
1187
  const results = [];
1156
1188
  for (const issueNumber of issueNumbers) {
1157
1189
  // Check if shutdown was triggered
@@ -1167,7 +1199,7 @@ async function executeBatch(issueNumbers, config, logWriter, options, issueInfoM
1167
1199
  if (logWriter) {
1168
1200
  logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
1169
1201
  }
1170
- const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, options, worktreeInfo?.path, shutdownManager, false);
1202
+ const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, options, worktreeInfo?.path, worktreeInfo?.branch, shutdownManager, false);
1171
1203
  results.push(result);
1172
1204
  // Complete issue logging
1173
1205
  if (logWriter) {
@@ -1179,7 +1211,7 @@ async function executeBatch(issueNumbers, config, logWriter, options, issueInfoM
1179
1211
  /**
1180
1212
  * Execute all phases for a single issue with logging and quality loop
1181
1213
  */
1182
- async function runIssueWithLogging(issueNumber, config, logWriter, labels, options, worktreePath, shutdownManager, chainMode) {
1214
+ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueTitle, labels, options, worktreePath, branch, shutdownManager, chainMode) {
1183
1215
  const startTime = Date.now();
1184
1216
  const phaseResults = [];
1185
1217
  let loopTriggered = false;
@@ -1188,6 +1220,32 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1188
1220
  if (worktreePath) {
1189
1221
  console.log(chalk.gray(` Worktree: ${worktreePath}`));
1190
1222
  }
1223
+ // Initialize state tracking for this issue
1224
+ if (stateManager) {
1225
+ try {
1226
+ const existingState = await stateManager.getIssueState(issueNumber);
1227
+ if (!existingState) {
1228
+ await stateManager.initializeIssue(issueNumber, issueTitle, {
1229
+ worktree: worktreePath,
1230
+ branch,
1231
+ qualityLoop: config.qualityLoop,
1232
+ maxIterations: config.maxIterations,
1233
+ });
1234
+ }
1235
+ else {
1236
+ // Update worktree info if it changed
1237
+ if (worktreePath && branch) {
1238
+ await stateManager.updateWorktreeInfo(issueNumber, worktreePath, branch);
1239
+ }
1240
+ }
1241
+ }
1242
+ catch (error) {
1243
+ // State tracking errors shouldn't stop execution
1244
+ if (config.verbose) {
1245
+ console.log(chalk.yellow(` ⚠️ State tracking error: ${error}`));
1246
+ }
1247
+ }
1248
+ }
1191
1249
  // Determine phases for this specific issue
1192
1250
  let phases;
1193
1251
  let detectedQualityLoop = false;
@@ -1205,6 +1263,15 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1205
1263
  // Run spec first to get recommended workflow
1206
1264
  console.log(chalk.gray(` Running spec to determine workflow...`));
1207
1265
  console.log(chalk.gray(` ⏳ spec...`));
1266
+ // Track spec phase start in state
1267
+ if (stateManager) {
1268
+ try {
1269
+ await stateManager.updatePhaseStatus(issueNumber, "spec", "in_progress");
1270
+ }
1271
+ catch {
1272
+ // State tracking errors shouldn't stop execution
1273
+ }
1274
+ }
1208
1275
  const specStartTime = new Date();
1209
1276
  // Note: spec runs in main repo (not worktree) for planning
1210
1277
  const specResult = await executePhase(issueNumber, "spec", config, sessionId, worktreePath, // Will be ignored for spec (non-isolated phase)
@@ -1212,6 +1279,15 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1212
1279
  const specEndTime = new Date();
1213
1280
  if (specResult.sessionId) {
1214
1281
  sessionId = specResult.sessionId;
1282
+ // Update session ID in state for resume capability
1283
+ if (stateManager) {
1284
+ try {
1285
+ await stateManager.updateSessionId(issueNumber, specResult.sessionId);
1286
+ }
1287
+ catch {
1288
+ // State tracking errors shouldn't stop execution
1289
+ }
1290
+ }
1215
1291
  }
1216
1292
  phaseResults.push(specResult);
1217
1293
  specAlreadyRan = true;
@@ -1224,6 +1300,18 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1224
1300
  : "failure", { error: specResult.error });
1225
1301
  logWriter.logPhase(phaseLog);
1226
1302
  }
1303
+ // Track spec phase completion in state
1304
+ if (stateManager) {
1305
+ try {
1306
+ const phaseStatus = specResult.success ? "completed" : "failed";
1307
+ await stateManager.updatePhaseStatus(issueNumber, "spec", phaseStatus, {
1308
+ error: specResult.error,
1309
+ });
1310
+ }
1311
+ catch {
1312
+ // State tracking errors shouldn't stop execution
1313
+ }
1314
+ }
1227
1315
  if (!specResult.success) {
1228
1316
  console.log(chalk.red(` ✗ spec: ${specResult.error}`));
1229
1317
  const durationSeconds = (Date.now() - startTime) / 1000;
@@ -1294,12 +1382,30 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1294
1382
  let phasesFailed = false;
1295
1383
  for (const phase of phases) {
1296
1384
  console.log(chalk.gray(` ⏳ ${phase}...`));
1385
+ // Track phase start in state
1386
+ if (stateManager) {
1387
+ try {
1388
+ await stateManager.updatePhaseStatus(issueNumber, phase, "in_progress");
1389
+ }
1390
+ catch {
1391
+ // State tracking errors shouldn't stop execution
1392
+ }
1393
+ }
1297
1394
  const phaseStartTime = new Date();
1298
1395
  const result = await executePhase(issueNumber, phase, config, sessionId, worktreePath, shutdownManager);
1299
1396
  const phaseEndTime = new Date();
1300
1397
  // Capture session ID for subsequent phases
1301
1398
  if (result.sessionId) {
1302
1399
  sessionId = result.sessionId;
1400
+ // Update session ID in state for resume capability
1401
+ if (stateManager) {
1402
+ try {
1403
+ await stateManager.updateSessionId(issueNumber, result.sessionId);
1404
+ }
1405
+ catch {
1406
+ // State tracking errors shouldn't stop execution
1407
+ }
1408
+ }
1303
1409
  }
1304
1410
  phaseResults.push(result);
1305
1411
  // Log phase result
@@ -1311,6 +1417,20 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1311
1417
  : "failure", { error: result.error });
1312
1418
  logWriter.logPhase(phaseLog);
1313
1419
  }
1420
+ // Track phase completion in state
1421
+ if (stateManager) {
1422
+ try {
1423
+ const phaseStatus = result.success
1424
+ ? "completed"
1425
+ : result.error?.includes("Timeout")
1426
+ ? "failed"
1427
+ : "failed";
1428
+ await stateManager.updatePhaseStatus(issueNumber, phase, phaseStatus, { error: result.error });
1429
+ }
1430
+ catch {
1431
+ // State tracking errors shouldn't stop execution
1432
+ }
1433
+ }
1314
1434
  if (result.success) {
1315
1435
  const duration = result.durationSeconds
1316
1436
  ? ` (${formatDuration(result.durationSeconds)})`
@@ -1355,6 +1475,16 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1355
1475
  // Success is determined by whether all phases completed in any iteration,
1356
1476
  // not whether all accumulated phase results passed (which would fail after loop recovery)
1357
1477
  const success = completedSuccessfully;
1478
+ // Update final issue status in state
1479
+ if (stateManager) {
1480
+ try {
1481
+ const finalStatus = success ? "ready_for_merge" : "in_progress";
1482
+ await stateManager.updateIssueStatus(issueNumber, finalStatus);
1483
+ }
1484
+ catch {
1485
+ // State tracking errors shouldn't stop execution
1486
+ }
1487
+ }
1358
1488
  // Create checkpoint commit in chain mode after QA passes
1359
1489
  if (success && chainMode && worktreePath) {
1360
1490
  createCheckpointCommit(worktreePath, issueNumber, config.verbose);
@@ -1,4 +1,20 @@
1
1
  /**
2
- * sequant status - Show version and configuration
2
+ * sequant status - Show version, configuration, and workflow state
3
3
  */
4
- export declare function statusCommand(): Promise<void>;
4
+ export interface StatusCommandOptions {
5
+ /** Show only issues state */
6
+ issues?: boolean;
7
+ /** Show details for a specific issue */
8
+ issue?: number;
9
+ /** Output as JSON */
10
+ json?: boolean;
11
+ /** Rebuild state from run logs */
12
+ rebuild?: boolean;
13
+ /** Clean up stale/orphaned entries */
14
+ cleanup?: boolean;
15
+ /** Only show what would be cleaned (used with --cleanup) */
16
+ dryRun?: boolean;
17
+ /** Remove entries older than this many days (used with --cleanup) */
18
+ maxAge?: number;
19
+ }
20
+ export declare function statusCommand(options?: StatusCommandOptions): Promise<void>;