sequant 1.8.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
@@ -10,6 +10,7 @@ import { fileURLToPath } from "url";
10
10
  import { dirname, resolve } from "path";
11
11
  import { readFileSync } from "fs";
12
12
  import { initCommand } from "../src/commands/init.js";
13
+ import { isLocalNodeModulesInstall } from "../src/lib/version-check.js";
13
14
  // Read version from package.json dynamically
14
15
  // Works from both source (bin/) and compiled (dist/bin/) locations
15
16
  function getVersion() {
@@ -43,6 +44,13 @@ const program = new Command();
43
44
  if (process.argv.includes("--no-color")) {
44
45
  process.env.FORCE_COLOR = "0";
45
46
  }
47
+ // Warn if running from local node_modules (not npx cache or global)
48
+ // This helps users who accidentally have a stale local install
49
+ if (!process.argv.includes("--quiet") && isLocalNodeModulesInstall()) {
50
+ console.warn(chalk.yellow("⚠️ Running sequant from local node_modules\n" +
51
+ " For latest version: npx sequant@latest\n" +
52
+ " To remove local: npm uninstall sequant\n"));
53
+ }
46
54
  program
47
55
  .name("sequant")
48
56
  .description("Quantize your development workflow - Sequential AI phases with quality gates")
@@ -90,6 +98,8 @@ program
90
98
  .option("--no-smart-tests", "Disable smart test detection")
91
99
  .option("--testgen", "Run testgen phase after spec")
92
100
  .option("--quiet", "Suppress version warnings and non-essential output")
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)")
93
103
  .action(runCommand);
94
104
  program
95
105
  .command("logs")
@@ -17,6 +17,12 @@ export declare function listWorktrees(): Array<{
17
17
  * Get changed files in a worktree compared to main
18
18
  */
19
19
  export declare function getWorktreeChangedFiles(worktreePath: string): string[];
20
+ /**
21
+ * Create a checkpoint commit in the worktree after QA passes
22
+ * This allows recovery in case later issues in the chain fail
23
+ * @internal Exported for testing
24
+ */
25
+ export declare function createCheckpointCommit(worktreePath: string, issueNumber: number, verbose: boolean): boolean;
20
26
  /**
21
27
  * Detect phases based on issue labels (like /solve logic)
22
28
  */
@@ -58,6 +64,13 @@ interface RunOptions {
58
64
  reuseWorktrees?: boolean;
59
65
  /** Suppress version warnings and non-essential output */
60
66
  quiet?: boolean;
67
+ /** Chain issues: each branches from previous (requires --sequential) */
68
+ chain?: boolean;
69
+ /**
70
+ * Base branch for worktree creation.
71
+ * Resolution priority: this CLI flag → settings.run.defaultBase → 'main'
72
+ */
73
+ base?: string;
61
74
  }
62
75
  /**
63
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";
@@ -105,8 +106,9 @@ export function getWorktreeChangedFiles(worktreePath) {
105
106
  }
106
107
  /**
107
108
  * Create or reuse a worktree for an issue
109
+ * @param baseBranch - Optional branch to use as base instead of origin/main (for chain mode)
108
110
  */
109
- async function ensureWorktree(issueNumber, title, verbose, packageManager) {
111
+ async function ensureWorktree(issueNumber, title, verbose, packageManager, baseBranch) {
110
112
  const gitRoot = getGitRoot();
111
113
  if (!gitRoot) {
112
114
  console.log(chalk.red(" ❌ Not in a git repository"));
@@ -135,15 +137,34 @@ async function ensureWorktree(issueNumber, title, verbose, packageManager) {
135
137
  if (verbose) {
136
138
  console.log(chalk.gray(` 🌿 Creating worktree for #${issueNumber}...`));
137
139
  }
138
- // Fetch latest main to ensure worktree starts from fresh baseline
139
- if (verbose) {
140
- console.log(chalk.gray(` 🔄 Fetching latest main...`));
140
+ // Determine the base for the new branch
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) {
156
+ if (verbose) {
157
+ console.log(chalk.gray(` 🔄 Fetching latest ${branchToFetch}...`));
158
+ }
159
+ const fetchResult = spawnSync("git", ["fetch", "origin", branchToFetch], {
160
+ stdio: "pipe",
161
+ });
162
+ if (fetchResult.status !== 0 && verbose) {
163
+ console.log(chalk.yellow(` ⚠️ Could not fetch origin/${branchToFetch}, using local state`));
164
+ }
141
165
  }
142
- const fetchResult = spawnSync("git", ["fetch", "origin", "main"], {
143
- stdio: "pipe",
144
- });
145
- if (fetchResult.status !== 0 && verbose) {
146
- console.log(chalk.yellow(` ⚠️ Could not fetch origin/main, using local state`));
166
+ else if (verbose) {
167
+ console.log(chalk.gray(` 🔗 Chaining from branch: ${baseBranch}`));
147
168
  }
148
169
  // Ensure worktrees directory exists
149
170
  if (!existsSync(worktreesDir)) {
@@ -158,8 +179,8 @@ async function ensureWorktree(issueNumber, title, verbose, packageManager) {
158
179
  });
159
180
  }
160
181
  else {
161
- // Create new branch from origin/main (fresh baseline)
162
- createResult = spawnSync("git", ["worktree", "add", worktreePath, "-b", branch, "origin/main"], { stdio: "pipe" });
182
+ // Create new branch from base reference (origin/main or previous branch in chain)
183
+ createResult = spawnSync("git", ["worktree", "add", worktreePath, "-b", branch, baseRef], { stdio: "pipe" });
163
184
  }
164
185
  if (createResult.status !== 0) {
165
186
  const error = createResult.stderr.toString();
@@ -208,12 +229,14 @@ async function ensureWorktree(issueNumber, title, verbose, packageManager) {
208
229
  }
209
230
  /**
210
231
  * Ensure worktrees exist for all issues before execution
232
+ * @param baseBranch - Optional base branch for worktree creation (default: main)
211
233
  */
212
- async function ensureWorktrees(issues, verbose, packageManager) {
234
+ async function ensureWorktrees(issues, verbose, packageManager, baseBranch) {
213
235
  const worktrees = new Map();
214
- console.log(chalk.blue("\n 📂 Preparing worktrees..."));
236
+ const baseDisplay = baseBranch || "main";
237
+ console.log(chalk.blue(`\n 📂 Preparing worktrees from ${baseDisplay}...`));
215
238
  for (const issue of issues) {
216
- const worktree = await ensureWorktree(issue.number, issue.title, verbose, packageManager);
239
+ const worktree = await ensureWorktree(issue.number, issue.title, verbose, packageManager, baseBranch);
217
240
  if (worktree) {
218
241
  worktrees.set(issue.number, worktree);
219
242
  }
@@ -225,6 +248,93 @@ async function ensureWorktrees(issues, verbose, packageManager) {
225
248
  }
226
249
  return worktrees;
227
250
  }
251
+ /**
252
+ * Ensure worktrees exist for all issues in chain mode
253
+ * Each issue branches from the previous issue's branch
254
+ * @param baseBranch - Optional starting base branch for the chain (default: main)
255
+ */
256
+ async function ensureWorktreesChain(issues, verbose, packageManager, baseBranch) {
257
+ const worktrees = new Map();
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;
262
+ for (const issue of issues) {
263
+ const worktree = await ensureWorktree(issue.number, issue.title, verbose, packageManager, previousBranch);
264
+ if (worktree) {
265
+ worktrees.set(issue.number, worktree);
266
+ previousBranch = worktree.branch; // Next issue will branch from this
267
+ }
268
+ else {
269
+ // If worktree creation fails, stop the chain
270
+ console.log(chalk.red(` ❌ Chain broken: could not create worktree for #${issue.number}`));
271
+ break;
272
+ }
273
+ }
274
+ const created = Array.from(worktrees.values()).filter((w) => !w.existed).length;
275
+ const reused = Array.from(worktrees.values()).filter((w) => w.existed).length;
276
+ if (created > 0 || reused > 0) {
277
+ console.log(chalk.gray(` Chained worktrees: ${created} created, ${reused} reused`));
278
+ }
279
+ // Show chain structure
280
+ if (worktrees.size > 0) {
281
+ const chainOrder = issues
282
+ .filter((i) => worktrees.has(i.number))
283
+ .map((i) => `#${i.number}`)
284
+ .join(" → ");
285
+ console.log(chalk.gray(` Chain: ${baseDisplay} → ${chainOrder}`));
286
+ }
287
+ return worktrees;
288
+ }
289
+ /**
290
+ * Create a checkpoint commit in the worktree after QA passes
291
+ * This allows recovery in case later issues in the chain fail
292
+ * @internal Exported for testing
293
+ */
294
+ export function createCheckpointCommit(worktreePath, issueNumber, verbose) {
295
+ // Check if there are uncommitted changes
296
+ const statusResult = spawnSync("git", ["-C", worktreePath, "status", "--porcelain"], { stdio: "pipe" });
297
+ if (statusResult.status !== 0) {
298
+ if (verbose) {
299
+ console.log(chalk.yellow(` ⚠️ Could not check git status for checkpoint`));
300
+ }
301
+ return false;
302
+ }
303
+ const hasChanges = statusResult.stdout.toString().trim().length > 0;
304
+ if (!hasChanges) {
305
+ if (verbose) {
306
+ console.log(chalk.gray(` 📌 No changes to checkpoint (already committed)`));
307
+ }
308
+ return true;
309
+ }
310
+ // Stage all changes
311
+ const addResult = spawnSync("git", ["-C", worktreePath, "add", "-A"], {
312
+ stdio: "pipe",
313
+ });
314
+ if (addResult.status !== 0) {
315
+ if (verbose) {
316
+ console.log(chalk.yellow(` ⚠️ Could not stage changes for checkpoint`));
317
+ }
318
+ return false;
319
+ }
320
+ // Create checkpoint commit
321
+ const commitMessage = `checkpoint(#${issueNumber}): QA passed
322
+
323
+ This is an automatic checkpoint commit created after issue #${issueNumber}
324
+ passed QA in chain mode. It serves as a recovery point if later issues fail.
325
+
326
+ Co-Authored-By: Sequant <noreply@sequant.dev>`;
327
+ const commitResult = spawnSync("git", ["-C", worktreePath, "commit", "-m", commitMessage], { stdio: "pipe" });
328
+ if (commitResult.status !== 0) {
329
+ const error = commitResult.stderr.toString();
330
+ if (verbose) {
331
+ console.log(chalk.yellow(` ⚠️ Could not create checkpoint commit: ${error}`));
332
+ }
333
+ return false;
334
+ }
335
+ console.log(chalk.green(` 📌 Checkpoint commit created for #${issueNumber}`));
336
+ return true;
337
+ }
228
338
  /**
229
339
  * Natural language prompts for each phase
230
340
  * These prompts will invoke the corresponding skills via natural language
@@ -777,6 +887,8 @@ export async function runCommand(issues, options) {
777
887
  // Determine if we should auto-detect phases from labels
778
888
  const autoDetectPhases = !options.phases && settings.run.autoDetectPhases;
779
889
  mergedOptions.autoDetectPhases = autoDetectPhases;
890
+ // Resolve base branch: CLI flag → settings.run.defaultBase → 'main'
891
+ const resolvedBaseBranch = options.base ?? settings.run.defaultBase ?? undefined;
780
892
  // Parse issue numbers (or use batch mode)
781
893
  let issueNumbers;
782
894
  let batches = null;
@@ -793,8 +905,30 @@ export async function runCommand(issues, options) {
793
905
  console.log(chalk.gray("\nUsage: npx sequant run <issues...> [options]"));
794
906
  console.log(chalk.gray("Example: npx sequant run 1 2 3 --sequential"));
795
907
  console.log(chalk.gray('Batch example: npx sequant run --batch "1 2" --batch "3"'));
908
+ console.log(chalk.gray("Chain example: npx sequant run 1 2 3 --sequential --chain"));
796
909
  return;
797
910
  }
911
+ // Validate chain mode requirements
912
+ if (mergedOptions.chain) {
913
+ if (!mergedOptions.sequential) {
914
+ console.log(chalk.red("❌ --chain requires --sequential flag"));
915
+ console.log(chalk.gray(" Chain mode executes issues sequentially, each branching from the previous."));
916
+ console.log(chalk.gray(" Usage: npx sequant run 1 2 3 --sequential --chain"));
917
+ return;
918
+ }
919
+ if (batches) {
920
+ console.log(chalk.red("❌ --chain cannot be used with --batch"));
921
+ console.log(chalk.gray(" Chain mode creates a linear dependency chain between issues."));
922
+ return;
923
+ }
924
+ // Warn about long chains
925
+ if (issueNumbers.length > 5) {
926
+ console.log(chalk.yellow(` ⚠️ Warning: Chain has ${issueNumbers.length} issues (recommended max: 5)`));
927
+ console.log(chalk.yellow(" Long chains increase merge complexity and review difficulty."));
928
+ console.log(chalk.yellow(" Consider breaking into smaller chains or using batch mode."));
929
+ console.log("");
930
+ }
931
+ }
798
932
  // Sort issues by dependencies (if more than one issue)
799
933
  if (issueNumbers.length > 1 && !batches) {
800
934
  const originalOrder = [...issueNumbers];
@@ -839,6 +973,12 @@ export async function runCommand(issues, options) {
839
973
  });
840
974
  await logWriter.initialize(runConfig);
841
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
+ }
842
982
  // Initialize shutdown manager for graceful interruption handling
843
983
  const shutdown = new ShutdownManager();
844
984
  // Register log writer finalization as cleanup task
@@ -872,12 +1012,21 @@ export async function runCommand(issues, options) {
872
1012
  if (logWriter) {
873
1013
  console.log(chalk.gray(` Logging: JSON (run ${logWriter.getRunId()?.slice(0, 8)}...)`));
874
1014
  }
1015
+ if (stateManager) {
1016
+ console.log(chalk.gray(` State tracking: enabled`));
1017
+ }
875
1018
  console.log(chalk.gray(` Issues: ${issueNumbers.map((n) => `#${n}`).join(", ")}`));
876
1019
  // Worktree isolation is enabled by default for multi-issue runs
877
1020
  const useWorktreeIsolation = mergedOptions.worktreeIsolation !== false && issueNumbers.length > 0;
878
1021
  if (useWorktreeIsolation) {
879
1022
  console.log(chalk.gray(` Worktree isolation: enabled`));
880
1023
  }
1024
+ if (resolvedBaseBranch) {
1025
+ console.log(chalk.gray(` Base branch: ${resolvedBaseBranch}`));
1026
+ }
1027
+ if (mergedOptions.chain) {
1028
+ console.log(chalk.gray(` Chain mode: enabled (each issue branches from previous)`));
1029
+ }
881
1030
  // Fetch issue info for all issues first
882
1031
  const issueInfoMap = new Map();
883
1032
  for (const issueNumber of issueNumbers) {
@@ -890,7 +1039,13 @@ export async function runCommand(issues, options) {
890
1039
  number: num,
891
1040
  title: issueInfoMap.get(num)?.title || `Issue #${num}`,
892
1041
  }));
893
- worktreeMap = await ensureWorktrees(issueData, config.verbose, manifest.packageManager);
1042
+ // Use chain mode or standard worktree creation
1043
+ if (mergedOptions.chain) {
1044
+ worktreeMap = await ensureWorktreesChain(issueData, config.verbose, manifest.packageManager, resolvedBaseBranch);
1045
+ }
1046
+ else {
1047
+ worktreeMap = await ensureWorktrees(issueData, config.verbose, manifest.packageManager, resolvedBaseBranch);
1048
+ }
894
1049
  // Register cleanup tasks for newly created worktrees (not pre-existing ones)
895
1050
  for (const [issueNum, worktree] of worktreeMap.entries()) {
896
1051
  if (!worktree.existed) {
@@ -915,7 +1070,7 @@ export async function runCommand(issues, options) {
915
1070
  for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
916
1071
  const batch = batches[batchIdx];
917
1072
  console.log(chalk.blue(`\n Batch ${batchIdx + 1}/${batches.length}: Issues ${batch.map((n) => `#${n}`).join(", ")}`));
918
- const batchResults = await executeBatch(batch, config, logWriter, mergedOptions, issueInfoMap, worktreeMap, shutdown);
1073
+ const batchResults = await executeBatch(batch, config, logWriter, stateManager, mergedOptions, issueInfoMap, worktreeMap, shutdown);
919
1074
  results.push(...batchResults);
920
1075
  // Check if batch failed and we should stop
921
1076
  const batchFailed = batchResults.some((r) => !r.success);
@@ -937,7 +1092,7 @@ export async function runCommand(issues, options) {
937
1092
  if (logWriter) {
938
1093
  logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
939
1094
  }
940
- const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions, worktreeInfo?.path, shutdown);
1095
+ const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, mergedOptions, worktreeInfo?.path, worktreeInfo?.branch, shutdown, mergedOptions.chain);
941
1096
  results.push(result);
942
1097
  // Complete issue logging
943
1098
  if (logWriter) {
@@ -948,7 +1103,8 @@ export async function runCommand(issues, options) {
948
1103
  break;
949
1104
  }
950
1105
  if (!result.success) {
951
- console.log(chalk.yellow(`\n ⚠️ Issue #${issueNumber} failed, stopping sequential execution`));
1106
+ const chainInfo = mergedOptions.chain ? " (chain stopped)" : "";
1107
+ console.log(chalk.yellow(`\n ⚠️ Issue #${issueNumber} failed, stopping sequential execution${chainInfo}`));
952
1108
  break;
953
1109
  }
954
1110
  }
@@ -970,7 +1126,7 @@ export async function runCommand(issues, options) {
970
1126
  if (logWriter) {
971
1127
  logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
972
1128
  }
973
- const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions, worktreeInfo?.path, shutdown);
1129
+ const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, mergedOptions, worktreeInfo?.path, worktreeInfo?.branch, shutdown, false);
974
1130
  results.push(result);
975
1131
  // Complete issue logging
976
1132
  if (logWriter) {
@@ -1027,7 +1183,7 @@ export async function runCommand(issues, options) {
1027
1183
  /**
1028
1184
  * Execute a batch of issues
1029
1185
  */
1030
- async function executeBatch(issueNumbers, config, logWriter, options, issueInfoMap, worktreeMap, shutdownManager) {
1186
+ async function executeBatch(issueNumbers, config, logWriter, stateManager, options, issueInfoMap, worktreeMap, shutdownManager) {
1031
1187
  const results = [];
1032
1188
  for (const issueNumber of issueNumbers) {
1033
1189
  // Check if shutdown was triggered
@@ -1043,7 +1199,7 @@ async function executeBatch(issueNumbers, config, logWriter, options, issueInfoM
1043
1199
  if (logWriter) {
1044
1200
  logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
1045
1201
  }
1046
- const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, options, worktreeInfo?.path, shutdownManager);
1202
+ const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, options, worktreeInfo?.path, worktreeInfo?.branch, shutdownManager, false);
1047
1203
  results.push(result);
1048
1204
  // Complete issue logging
1049
1205
  if (logWriter) {
@@ -1055,7 +1211,7 @@ async function executeBatch(issueNumbers, config, logWriter, options, issueInfoM
1055
1211
  /**
1056
1212
  * Execute all phases for a single issue with logging and quality loop
1057
1213
  */
1058
- async function runIssueWithLogging(issueNumber, config, logWriter, labels, options, worktreePath, shutdownManager) {
1214
+ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueTitle, labels, options, worktreePath, branch, shutdownManager, chainMode) {
1059
1215
  const startTime = Date.now();
1060
1216
  const phaseResults = [];
1061
1217
  let loopTriggered = false;
@@ -1064,6 +1220,32 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1064
1220
  if (worktreePath) {
1065
1221
  console.log(chalk.gray(` Worktree: ${worktreePath}`));
1066
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
+ }
1067
1249
  // Determine phases for this specific issue
1068
1250
  let phases;
1069
1251
  let detectedQualityLoop = false;
@@ -1081,6 +1263,15 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1081
1263
  // Run spec first to get recommended workflow
1082
1264
  console.log(chalk.gray(` Running spec to determine workflow...`));
1083
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
+ }
1084
1275
  const specStartTime = new Date();
1085
1276
  // Note: spec runs in main repo (not worktree) for planning
1086
1277
  const specResult = await executePhase(issueNumber, "spec", config, sessionId, worktreePath, // Will be ignored for spec (non-isolated phase)
@@ -1088,6 +1279,15 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1088
1279
  const specEndTime = new Date();
1089
1280
  if (specResult.sessionId) {
1090
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
+ }
1091
1291
  }
1092
1292
  phaseResults.push(specResult);
1093
1293
  specAlreadyRan = true;
@@ -1100,6 +1300,18 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1100
1300
  : "failure", { error: specResult.error });
1101
1301
  logWriter.logPhase(phaseLog);
1102
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
+ }
1103
1315
  if (!specResult.success) {
1104
1316
  console.log(chalk.red(` ✗ spec: ${specResult.error}`));
1105
1317
  const durationSeconds = (Date.now() - startTime) / 1000;
@@ -1170,12 +1382,30 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1170
1382
  let phasesFailed = false;
1171
1383
  for (const phase of phases) {
1172
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
+ }
1173
1394
  const phaseStartTime = new Date();
1174
1395
  const result = await executePhase(issueNumber, phase, config, sessionId, worktreePath, shutdownManager);
1175
1396
  const phaseEndTime = new Date();
1176
1397
  // Capture session ID for subsequent phases
1177
1398
  if (result.sessionId) {
1178
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
+ }
1179
1409
  }
1180
1410
  phaseResults.push(result);
1181
1411
  // Log phase result
@@ -1187,6 +1417,20 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1187
1417
  : "failure", { error: result.error });
1188
1418
  logWriter.logPhase(phaseLog);
1189
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
+ }
1190
1434
  if (result.success) {
1191
1435
  const duration = result.durationSeconds
1192
1436
  ? ` (${formatDuration(result.durationSeconds)})`
@@ -1231,6 +1475,20 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1231
1475
  // Success is determined by whether all phases completed in any iteration,
1232
1476
  // not whether all accumulated phase results passed (which would fail after loop recovery)
1233
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
+ }
1488
+ // Create checkpoint commit in chain mode after QA passes
1489
+ if (success && chainMode && worktreePath) {
1490
+ createCheckpointCommit(worktreePath, issueNumber, config.verbose);
1491
+ }
1234
1492
  return {
1235
1493
  issueNumber,
1236
1494
  success,
@@ -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>;