sequant 1.7.0 → 1.9.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/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,7 @@ 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)")
93
102
  .action(runCommand);
94
103
  program
95
104
  .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,8 @@ 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;
61
69
  }
62
70
  /**
63
71
  * Main run command
@@ -105,8 +105,9 @@ export function getWorktreeChangedFiles(worktreePath) {
105
105
  }
106
106
  /**
107
107
  * Create or reuse a worktree for an issue
108
+ * @param baseBranch - Optional branch to use as base instead of origin/main (for chain mode)
108
109
  */
109
- async function ensureWorktree(issueNumber, title, verbose, packageManager) {
110
+ async function ensureWorktree(issueNumber, title, verbose, packageManager, baseBranch) {
110
111
  const gitRoot = getGitRoot();
111
112
  if (!gitRoot) {
112
113
  console.log(chalk.red(" ❌ Not in a git repository"));
@@ -135,15 +136,22 @@ async function ensureWorktree(issueNumber, title, verbose, packageManager) {
135
136
  if (verbose) {
136
137
  console.log(chalk.gray(` 🌿 Creating worktree for #${issueNumber}...`));
137
138
  }
138
- // Fetch latest main to ensure worktree starts from fresh baseline
139
- if (verbose) {
140
- console.log(chalk.gray(` 🔄 Fetching latest main...`));
139
+ // 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) {
143
+ if (verbose) {
144
+ console.log(chalk.gray(` 🔄 Fetching latest main...`));
145
+ }
146
+ const fetchResult = spawnSync("git", ["fetch", "origin", "main"], {
147
+ stdio: "pipe",
148
+ });
149
+ if (fetchResult.status !== 0 && verbose) {
150
+ console.log(chalk.yellow(` ⚠️ Could not fetch origin/main, using local state`));
151
+ }
141
152
  }
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`));
153
+ else if (verbose) {
154
+ console.log(chalk.gray(` 🔗 Chaining from branch: ${baseBranch}`));
147
155
  }
148
156
  // Ensure worktrees directory exists
149
157
  if (!existsSync(worktreesDir)) {
@@ -158,8 +166,8 @@ async function ensureWorktree(issueNumber, title, verbose, packageManager) {
158
166
  });
159
167
  }
160
168
  else {
161
- // Create new branch from origin/main (fresh baseline)
162
- createResult = spawnSync("git", ["worktree", "add", worktreePath, "-b", branch, "origin/main"], { stdio: "pipe" });
169
+ // Create new branch from base reference (origin/main or previous branch in chain)
170
+ createResult = spawnSync("git", ["worktree", "add", worktreePath, "-b", branch, baseRef], { stdio: "pipe" });
163
171
  }
164
172
  if (createResult.status !== 0) {
165
173
  const error = createResult.stderr.toString();
@@ -225,6 +233,90 @@ async function ensureWorktrees(issues, verbose, packageManager) {
225
233
  }
226
234
  return worktrees;
227
235
  }
236
+ /**
237
+ * Ensure worktrees exist for all issues in chain mode
238
+ * Each issue branches from the previous issue's branch
239
+ */
240
+ async function ensureWorktreesChain(issues, verbose, packageManager) {
241
+ const worktrees = new Map();
242
+ console.log(chalk.blue("\n 🔗 Preparing chained worktrees..."));
243
+ let previousBranch;
244
+ for (const issue of issues) {
245
+ const worktree = await ensureWorktree(issue.number, issue.title, verbose, packageManager, previousBranch);
246
+ if (worktree) {
247
+ worktrees.set(issue.number, worktree);
248
+ previousBranch = worktree.branch; // Next issue will branch from this
249
+ }
250
+ else {
251
+ // If worktree creation fails, stop the chain
252
+ console.log(chalk.red(` ❌ Chain broken: could not create worktree for #${issue.number}`));
253
+ break;
254
+ }
255
+ }
256
+ const created = Array.from(worktrees.values()).filter((w) => !w.existed).length;
257
+ const reused = Array.from(worktrees.values()).filter((w) => w.existed).length;
258
+ if (created > 0 || reused > 0) {
259
+ console.log(chalk.gray(` Chained worktrees: ${created} created, ${reused} reused`));
260
+ }
261
+ // Show chain structure
262
+ if (worktrees.size > 0) {
263
+ const chainOrder = issues
264
+ .filter((i) => worktrees.has(i.number))
265
+ .map((i) => `#${i.number}`)
266
+ .join(" → ");
267
+ console.log(chalk.gray(` Chain: origin/main → ${chainOrder}`));
268
+ }
269
+ return worktrees;
270
+ }
271
+ /**
272
+ * Create a checkpoint commit in the worktree after QA passes
273
+ * This allows recovery in case later issues in the chain fail
274
+ * @internal Exported for testing
275
+ */
276
+ export function createCheckpointCommit(worktreePath, issueNumber, verbose) {
277
+ // Check if there are uncommitted changes
278
+ const statusResult = spawnSync("git", ["-C", worktreePath, "status", "--porcelain"], { stdio: "pipe" });
279
+ if (statusResult.status !== 0) {
280
+ if (verbose) {
281
+ console.log(chalk.yellow(` ⚠️ Could not check git status for checkpoint`));
282
+ }
283
+ return false;
284
+ }
285
+ const hasChanges = statusResult.stdout.toString().trim().length > 0;
286
+ if (!hasChanges) {
287
+ if (verbose) {
288
+ console.log(chalk.gray(` 📌 No changes to checkpoint (already committed)`));
289
+ }
290
+ return true;
291
+ }
292
+ // Stage all changes
293
+ const addResult = spawnSync("git", ["-C", worktreePath, "add", "-A"], {
294
+ stdio: "pipe",
295
+ });
296
+ if (addResult.status !== 0) {
297
+ if (verbose) {
298
+ console.log(chalk.yellow(` ⚠️ Could not stage changes for checkpoint`));
299
+ }
300
+ return false;
301
+ }
302
+ // Create checkpoint commit
303
+ const commitMessage = `checkpoint(#${issueNumber}): QA passed
304
+
305
+ This is an automatic checkpoint commit created after issue #${issueNumber}
306
+ passed QA in chain mode. It serves as a recovery point if later issues fail.
307
+
308
+ Co-Authored-By: Sequant <noreply@sequant.dev>`;
309
+ const commitResult = spawnSync("git", ["-C", worktreePath, "commit", "-m", commitMessage], { stdio: "pipe" });
310
+ if (commitResult.status !== 0) {
311
+ const error = commitResult.stderr.toString();
312
+ if (verbose) {
313
+ console.log(chalk.yellow(` ⚠️ Could not create checkpoint commit: ${error}`));
314
+ }
315
+ return false;
316
+ }
317
+ console.log(chalk.green(` 📌 Checkpoint commit created for #${issueNumber}`));
318
+ return true;
319
+ }
228
320
  /**
229
321
  * Natural language prompts for each phase
230
322
  * These prompts will invoke the corresponding skills via natural language
@@ -793,8 +885,30 @@ export async function runCommand(issues, options) {
793
885
  console.log(chalk.gray("\nUsage: npx sequant run <issues...> [options]"));
794
886
  console.log(chalk.gray("Example: npx sequant run 1 2 3 --sequential"));
795
887
  console.log(chalk.gray('Batch example: npx sequant run --batch "1 2" --batch "3"'));
888
+ console.log(chalk.gray("Chain example: npx sequant run 1 2 3 --sequential --chain"));
796
889
  return;
797
890
  }
891
+ // Validate chain mode requirements
892
+ if (mergedOptions.chain) {
893
+ if (!mergedOptions.sequential) {
894
+ console.log(chalk.red("❌ --chain requires --sequential flag"));
895
+ console.log(chalk.gray(" Chain mode executes issues sequentially, each branching from the previous."));
896
+ console.log(chalk.gray(" Usage: npx sequant run 1 2 3 --sequential --chain"));
897
+ return;
898
+ }
899
+ if (batches) {
900
+ console.log(chalk.red("❌ --chain cannot be used with --batch"));
901
+ console.log(chalk.gray(" Chain mode creates a linear dependency chain between issues."));
902
+ return;
903
+ }
904
+ // Warn about long chains
905
+ if (issueNumbers.length > 5) {
906
+ console.log(chalk.yellow(` ⚠️ Warning: Chain has ${issueNumbers.length} issues (recommended max: 5)`));
907
+ console.log(chalk.yellow(" Long chains increase merge complexity and review difficulty."));
908
+ console.log(chalk.yellow(" Consider breaking into smaller chains or using batch mode."));
909
+ console.log("");
910
+ }
911
+ }
798
912
  // Sort issues by dependencies (if more than one issue)
799
913
  if (issueNumbers.length > 1 && !batches) {
800
914
  const originalOrder = [...issueNumbers];
@@ -878,6 +992,9 @@ export async function runCommand(issues, options) {
878
992
  if (useWorktreeIsolation) {
879
993
  console.log(chalk.gray(` Worktree isolation: enabled`));
880
994
  }
995
+ if (mergedOptions.chain) {
996
+ console.log(chalk.gray(` Chain mode: enabled (each issue branches from previous)`));
997
+ }
881
998
  // Fetch issue info for all issues first
882
999
  const issueInfoMap = new Map();
883
1000
  for (const issueNumber of issueNumbers) {
@@ -890,7 +1007,13 @@ export async function runCommand(issues, options) {
890
1007
  number: num,
891
1008
  title: issueInfoMap.get(num)?.title || `Issue #${num}`,
892
1009
  }));
893
- worktreeMap = await ensureWorktrees(issueData, config.verbose, manifest.packageManager);
1010
+ // Use chain mode or standard worktree creation
1011
+ if (mergedOptions.chain) {
1012
+ worktreeMap = await ensureWorktreesChain(issueData, config.verbose, manifest.packageManager);
1013
+ }
1014
+ else {
1015
+ worktreeMap = await ensureWorktrees(issueData, config.verbose, manifest.packageManager);
1016
+ }
894
1017
  // Register cleanup tasks for newly created worktrees (not pre-existing ones)
895
1018
  for (const [issueNum, worktree] of worktreeMap.entries()) {
896
1019
  if (!worktree.existed) {
@@ -937,7 +1060,7 @@ export async function runCommand(issues, options) {
937
1060
  if (logWriter) {
938
1061
  logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
939
1062
  }
940
- const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions, worktreeInfo?.path, shutdown);
1063
+ const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions, worktreeInfo?.path, shutdown, mergedOptions.chain);
941
1064
  results.push(result);
942
1065
  // Complete issue logging
943
1066
  if (logWriter) {
@@ -948,7 +1071,8 @@ export async function runCommand(issues, options) {
948
1071
  break;
949
1072
  }
950
1073
  if (!result.success) {
951
- console.log(chalk.yellow(`\n ⚠️ Issue #${issueNumber} failed, stopping sequential execution`));
1074
+ const chainInfo = mergedOptions.chain ? " (chain stopped)" : "";
1075
+ console.log(chalk.yellow(`\n ⚠️ Issue #${issueNumber} failed, stopping sequential execution${chainInfo}`));
952
1076
  break;
953
1077
  }
954
1078
  }
@@ -970,7 +1094,7 @@ export async function runCommand(issues, options) {
970
1094
  if (logWriter) {
971
1095
  logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
972
1096
  }
973
- const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions, worktreeInfo?.path, shutdown);
1097
+ const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions, worktreeInfo?.path, shutdown, false);
974
1098
  results.push(result);
975
1099
  // Complete issue logging
976
1100
  if (logWriter) {
@@ -1043,7 +1167,7 @@ async function executeBatch(issueNumbers, config, logWriter, options, issueInfoM
1043
1167
  if (logWriter) {
1044
1168
  logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
1045
1169
  }
1046
- const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, options, worktreeInfo?.path, shutdownManager);
1170
+ const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, options, worktreeInfo?.path, shutdownManager, false);
1047
1171
  results.push(result);
1048
1172
  // Complete issue logging
1049
1173
  if (logWriter) {
@@ -1055,7 +1179,7 @@ async function executeBatch(issueNumbers, config, logWriter, options, issueInfoM
1055
1179
  /**
1056
1180
  * Execute all phases for a single issue with logging and quality loop
1057
1181
  */
1058
- async function runIssueWithLogging(issueNumber, config, logWriter, labels, options, worktreePath, shutdownManager) {
1182
+ async function runIssueWithLogging(issueNumber, config, logWriter, labels, options, worktreePath, shutdownManager, chainMode) {
1059
1183
  const startTime = Date.now();
1060
1184
  const phaseResults = [];
1061
1185
  let loopTriggered = false;
@@ -1231,6 +1355,10 @@ async function runIssueWithLogging(issueNumber, config, logWriter, labels, optio
1231
1355
  // Success is determined by whether all phases completed in any iteration,
1232
1356
  // not whether all accumulated phase results passed (which would fail after loop recovery)
1233
1357
  const success = completedSuccessfully;
1358
+ // Create checkpoint commit in chain mode after QA passes
1359
+ if (success && chainMode && worktreePath) {
1360
+ createCheckpointCommit(worktreePath, issueNumber, config.verbose);
1361
+ }
1234
1362
  return {
1235
1363
  issueNumber,
1236
1364
  success,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sequant",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Quantize your development workflow - Sequential AI phases with quality gates",
5
5
  "type": "module",
6
6
  "bin": {
@@ -162,8 +162,46 @@ fi
162
162
 
163
163
  # Install dependencies if needed
164
164
  if [ ! -d "node_modules" ]; then
165
- echo -e "${BLUE}📦 Installing dependencies...${NC}"
166
- npm install --silent
165
+ # Check for npm install cache optimization (opt-in via SEQUANT_NPM_CACHE=true)
166
+ if [ "${SEQUANT_NPM_CACHE:-false}" = "true" ]; then
167
+ CACHE_DIR="../worktrees/.npm-cache"
168
+ HASH_FILE="${CACHE_DIR}/.package-lock-hash"
169
+
170
+ # Calculate current package-lock hash (cross-platform)
171
+ if command -v md5sum &> /dev/null; then
172
+ CURRENT_HASH=$(md5sum "${MAIN_REPO_DIR}/package-lock.json" | cut -d' ' -f1)
173
+ elif command -v md5 &> /dev/null; then
174
+ CURRENT_HASH=$(md5 -q "${MAIN_REPO_DIR}/package-lock.json")
175
+ else
176
+ CURRENT_HASH=""
177
+ fi
178
+
179
+ # Check if cache is valid
180
+ if [ -n "$CURRENT_HASH" ] && [ -f "$HASH_FILE" ] && [ -d "${MAIN_REPO_DIR}/node_modules" ]; then
181
+ CACHED_HASH=$(cat "$HASH_FILE" 2>/dev/null || echo "")
182
+ if [ "$CURRENT_HASH" = "$CACHED_HASH" ]; then
183
+ echo -e "${GREEN}⚡ Using cached node_modules (package-lock unchanged)${NC}"
184
+ cp -r "${MAIN_REPO_DIR}/node_modules" ./node_modules
185
+ else
186
+ echo -e "${BLUE}📦 Installing dependencies (package-lock changed)...${NC}"
187
+ npm install --silent
188
+ # Update cache hash
189
+ mkdir -p "$CACHE_DIR"
190
+ echo "$CURRENT_HASH" > "$HASH_FILE"
191
+ fi
192
+ else
193
+ echo -e "${BLUE}📦 Installing dependencies (initializing cache)...${NC}"
194
+ npm install --silent
195
+ # Initialize cache hash
196
+ if [ -n "$CURRENT_HASH" ]; then
197
+ mkdir -p "$CACHE_DIR"
198
+ echo "$CURRENT_HASH" > "$HASH_FILE"
199
+ fi
200
+ fi
201
+ else
202
+ echo -e "${BLUE}📦 Installing dependencies...${NC}"
203
+ npm install --silent
204
+ fi
167
205
  fi
168
206
 
169
207
  echo ""