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 +9 -0
- package/dist/src/commands/run.d.ts +8 -0
- package/dist/src/commands/run.js +145 -17
- package/package.json +1 -1
- package/templates/scripts/new-feature.sh +40 -2
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
|
package/dist/src/commands/run.js
CHANGED
|
@@ -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
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
|
162
|
-
createResult = spawnSync("git", ["worktree", "add", worktreePath, "-b", branch,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -162,8 +162,46 @@ fi
|
|
|
162
162
|
|
|
163
163
|
# Install dependencies if needed
|
|
164
164
|
if [ ! -d "node_modules" ]; then
|
|
165
|
-
|
|
166
|
-
|
|
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 ""
|