sequant 1.15.1 → 1.15.3
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/.claude-plugin/marketplace.json +4 -4
- package/.claude-plugin/plugin.json +3 -3
- package/README.md +3 -3
- package/dist/bin/cli.js +3 -0
- package/dist/src/commands/init.js +1 -1
- package/dist/src/commands/logs.js +15 -0
- package/dist/src/commands/run.d.ts +114 -1
- package/dist/src/commands/run.js +513 -31
- package/dist/src/commands/stats.js +48 -0
- package/dist/src/lib/scope/index.d.ts +1 -0
- package/dist/src/lib/scope/index.js +2 -0
- package/dist/src/lib/scope/settings-converter.d.ts +28 -0
- package/dist/src/lib/scope/settings-converter.js +53 -0
- package/dist/src/lib/settings.d.ts +45 -0
- package/dist/src/lib/settings.js +30 -0
- package/dist/src/lib/test-tautology-detector.d.ts +122 -0
- package/dist/src/lib/test-tautology-detector.js +488 -0
- package/dist/src/lib/upstream/issues.js +5 -5
- package/dist/src/lib/workflow/git-diff-utils.d.ts +39 -0
- package/dist/src/lib/workflow/git-diff-utils.js +142 -0
- package/dist/src/lib/workflow/log-writer.d.ts +9 -2
- package/dist/src/lib/workflow/log-writer.js +9 -3
- package/dist/src/lib/workflow/metrics-schema.d.ts +9 -0
- package/dist/src/lib/workflow/metrics-schema.js +10 -1
- package/dist/src/lib/workflow/phase-detection.d.ts +3 -0
- package/dist/src/lib/workflow/phase-detection.js +27 -1
- package/dist/src/lib/workflow/qa-cache.d.ts +3 -1
- package/dist/src/lib/workflow/qa-cache.js +2 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts +90 -3
- package/dist/src/lib/workflow/run-log-schema.js +44 -2
- package/dist/src/lib/workflow/state-utils.d.ts +46 -0
- package/dist/src/lib/workflow/state-utils.js +167 -0
- package/dist/src/lib/workflow/token-utils.d.ts +92 -0
- package/dist/src/lib/workflow/token-utils.js +170 -0
- package/dist/src/lib/workflow/types.d.ts +6 -0
- package/dist/src/lib/workflow/types.js +1 -0
- package/package.json +4 -4
- package/templates/hooks/pre-tool.sh +4 -0
- package/templates/skills/assess/SKILL.md +1 -1
- package/templates/skills/exec/SKILL.md +5 -4
- package/templates/skills/improve/SKILL.md +37 -24
- package/templates/skills/loop/SKILL.md +3 -3
- package/templates/skills/qa/SKILL.md +66 -1
- package/templates/skills/qa/references/code-review-checklist.md +10 -11
- package/templates/skills/qa/scripts/quality-checks.sh +16 -0
- package/templates/skills/security-review/references/security-checklists.md +89 -36
- package/templates/skills/solve/SKILL.md +3 -1
- package/templates/skills/spec/SKILL.md +8 -4
package/dist/src/commands/run.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { spawnSync } from "child_process";
|
|
9
|
-
import { existsSync } from "fs";
|
|
9
|
+
import { existsSync, readFileSync } from "fs";
|
|
10
10
|
import path from "path";
|
|
11
11
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
12
12
|
import { getManifest } from "../lib/manifest.js";
|
|
@@ -23,6 +23,9 @@ import { determineOutcome, } from "../lib/workflow/metrics-schema.js";
|
|
|
23
23
|
import { getResumablePhasesForIssue } from "../lib/workflow/phase-detection.js";
|
|
24
24
|
import { ui, colors } from "../lib/cli-ui.js";
|
|
25
25
|
import { PhaseSpinner } from "../lib/phase-spinner.js";
|
|
26
|
+
import { getGitDiffStats, getCommitHash, } from "../lib/workflow/git-diff-utils.js";
|
|
27
|
+
import { getTokenUsageForRun } from "../lib/workflow/token-utils.js";
|
|
28
|
+
import { reconcileStateAtStartup } from "../lib/workflow/state-utils.js";
|
|
26
29
|
/**
|
|
27
30
|
* Slugify a title for branch naming
|
|
28
31
|
*/
|
|
@@ -94,6 +97,92 @@ function findExistingWorktree(branch) {
|
|
|
94
97
|
}
|
|
95
98
|
return null;
|
|
96
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Check if a worktree is stale (behind origin/main) and should be recreated
|
|
102
|
+
*
|
|
103
|
+
* @param worktreePath - Path to the worktree
|
|
104
|
+
* @param verbose - Enable verbose output
|
|
105
|
+
* @returns Freshness check result
|
|
106
|
+
*/
|
|
107
|
+
export function checkWorktreeFreshness(worktreePath, verbose) {
|
|
108
|
+
const result = {
|
|
109
|
+
isStale: false,
|
|
110
|
+
commitsBehind: 0,
|
|
111
|
+
hasUncommittedChanges: false,
|
|
112
|
+
hasUnpushedCommits: false,
|
|
113
|
+
};
|
|
114
|
+
// Fetch latest main to ensure accurate comparison
|
|
115
|
+
spawnSync("git", ["-C", worktreePath, "fetch", "origin", "main"], {
|
|
116
|
+
stdio: "pipe",
|
|
117
|
+
timeout: 30000,
|
|
118
|
+
});
|
|
119
|
+
// Check for uncommitted changes
|
|
120
|
+
const statusResult = spawnSync("git", ["-C", worktreePath, "status", "--porcelain"], { stdio: "pipe" });
|
|
121
|
+
if (statusResult.status === 0) {
|
|
122
|
+
result.hasUncommittedChanges =
|
|
123
|
+
statusResult.stdout.toString().trim().length > 0;
|
|
124
|
+
}
|
|
125
|
+
// Get merge base with origin/main
|
|
126
|
+
const mergeBaseResult = spawnSync("git", ["-C", worktreePath, "merge-base", "HEAD", "origin/main"], { stdio: "pipe" });
|
|
127
|
+
if (mergeBaseResult.status !== 0) {
|
|
128
|
+
// Can't determine merge base - not stale
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
const mergeBase = mergeBaseResult.stdout.toString().trim();
|
|
132
|
+
// Get origin/main HEAD
|
|
133
|
+
const mainHeadResult = spawnSync("git", ["-C", worktreePath, "rev-parse", "origin/main"], { stdio: "pipe" });
|
|
134
|
+
if (mainHeadResult.status !== 0) {
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
const mainHead = mainHeadResult.stdout.toString().trim();
|
|
138
|
+
// Count commits behind main
|
|
139
|
+
if (mergeBase !== mainHead) {
|
|
140
|
+
const countResult = spawnSync("git", ["-C", worktreePath, "rev-list", "--count", `${mergeBase}..${mainHead}`], { stdio: "pipe" });
|
|
141
|
+
if (countResult.status === 0) {
|
|
142
|
+
result.commitsBehind = parseInt(countResult.stdout.toString().trim(), 10);
|
|
143
|
+
// Consider stale if more than 5 commits behind (configurable threshold)
|
|
144
|
+
result.isStale = result.commitsBehind > 5;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Check for unpushed commits (work in progress)
|
|
148
|
+
const unpushedResult = spawnSync("git", ["-C", worktreePath, "log", "--oneline", "@{u}..HEAD"], { stdio: "pipe" });
|
|
149
|
+
if (unpushedResult.status === 0) {
|
|
150
|
+
result.hasUnpushedCommits =
|
|
151
|
+
unpushedResult.stdout.toString().trim().length > 0;
|
|
152
|
+
}
|
|
153
|
+
if (verbose && result.isStale) {
|
|
154
|
+
console.log(chalk.gray(` 📊 Worktree is ${result.commitsBehind} commits behind origin/main`));
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Remove and recreate a stale worktree
|
|
160
|
+
*
|
|
161
|
+
* @param existingPath - Path to existing worktree
|
|
162
|
+
* @param branch - Branch name
|
|
163
|
+
* @param verbose - Enable verbose output
|
|
164
|
+
* @returns true if worktree was removed
|
|
165
|
+
*/
|
|
166
|
+
export function removeStaleWorktree(existingPath, branch, verbose) {
|
|
167
|
+
if (verbose) {
|
|
168
|
+
console.log(chalk.gray(` 🗑️ Removing stale worktree...`));
|
|
169
|
+
}
|
|
170
|
+
// Remove the worktree
|
|
171
|
+
const removeResult = spawnSync("git", ["worktree", "remove", "--force", existingPath], { stdio: "pipe" });
|
|
172
|
+
if (removeResult.status !== 0) {
|
|
173
|
+
const error = removeResult.stderr.toString();
|
|
174
|
+
console.log(chalk.yellow(` ⚠️ Could not remove worktree: ${error}`));
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
// Delete the branch so it can be recreated fresh
|
|
178
|
+
const deleteResult = spawnSync("git", ["branch", "-D", branch], {
|
|
179
|
+
stdio: "pipe",
|
|
180
|
+
});
|
|
181
|
+
if (deleteResult.status !== 0 && verbose) {
|
|
182
|
+
console.log(chalk.gray(` ℹ️ Branch ${branch} not deleted (may not exist locally)`));
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
97
186
|
/**
|
|
98
187
|
* List all active worktrees with their branches
|
|
99
188
|
*/
|
|
@@ -160,6 +249,37 @@ export function getWorktreeDiffStats(worktreePath) {
|
|
|
160
249
|
linesAdded: insertionsMatch ? parseInt(insertionsMatch[1], 10) : 0,
|
|
161
250
|
};
|
|
162
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Read cache metrics from QA phase (AC-7)
|
|
254
|
+
*
|
|
255
|
+
* @param worktreePath - Path to the worktree
|
|
256
|
+
* @returns CacheMetrics or undefined if not available
|
|
257
|
+
*/
|
|
258
|
+
function readCacheMetrics(worktreePath) {
|
|
259
|
+
const cacheMetricsPath = worktreePath
|
|
260
|
+
? path.join(worktreePath, ".sequant/.cache/qa/cache-metrics.json")
|
|
261
|
+
: ".sequant/.cache/qa/cache-metrics.json";
|
|
262
|
+
if (!existsSync(cacheMetricsPath)) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const content = readFileSync(cacheMetricsPath, "utf-8");
|
|
267
|
+
const data = JSON.parse(content);
|
|
268
|
+
if (typeof data.hits === "number" &&
|
|
269
|
+
typeof data.misses === "number" &&
|
|
270
|
+
typeof data.skipped === "number") {
|
|
271
|
+
return {
|
|
272
|
+
hits: data.hits,
|
|
273
|
+
misses: data.misses,
|
|
274
|
+
skipped: data.skipped,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Ignore parse errors
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
163
283
|
/**
|
|
164
284
|
* Filter phases based on resume status.
|
|
165
285
|
*
|
|
@@ -196,11 +316,76 @@ async function ensureWorktree(issueNumber, title, verbose, packageManager, baseB
|
|
|
196
316
|
const worktreesDir = path.join(path.dirname(gitRoot), "worktrees");
|
|
197
317
|
const worktreePath = path.join(worktreesDir, branch);
|
|
198
318
|
// Check if worktree already exists
|
|
199
|
-
|
|
319
|
+
let existingPath = findExistingWorktree(branch);
|
|
320
|
+
if (existingPath) {
|
|
321
|
+
// AC-3: Check if worktree is stale and needs recreation
|
|
322
|
+
const freshness = checkWorktreeFreshness(existingPath, verbose);
|
|
323
|
+
if (freshness.isStale) {
|
|
324
|
+
// AC-3: Handle stale worktrees - check for work in progress
|
|
325
|
+
if (freshness.hasUncommittedChanges) {
|
|
326
|
+
console.log(chalk.yellow(` ⚠️ Worktree is ${freshness.commitsBehind} commits behind main but has uncommitted changes`));
|
|
327
|
+
console.log(chalk.yellow(` ℹ️ Keeping existing worktree. Commit or stash changes, then re-run.`));
|
|
328
|
+
// Continue with existing worktree
|
|
329
|
+
}
|
|
330
|
+
else if (freshness.hasUnpushedCommits) {
|
|
331
|
+
console.log(chalk.yellow(` ⚠️ Worktree is ${freshness.commitsBehind} commits behind main but has unpushed commits`));
|
|
332
|
+
console.log(chalk.yellow(` ℹ️ Keeping existing worktree with WIP commits.`));
|
|
333
|
+
// Continue with existing worktree
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
// Safe to recreate - no uncommitted/unpushed work
|
|
337
|
+
console.log(chalk.yellow(` ⚠️ Worktree is ${freshness.commitsBehind} commits behind main — recreating fresh`));
|
|
338
|
+
if (removeStaleWorktree(existingPath, branch, verbose)) {
|
|
339
|
+
existingPath = null; // Will fall through to create new worktree
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
200
344
|
if (existingPath) {
|
|
201
345
|
if (verbose) {
|
|
202
346
|
console.log(chalk.gray(` 📂 Reusing existing worktree: ${existingPath}`));
|
|
203
347
|
}
|
|
348
|
+
// In chain mode, rebase existing worktree onto previous chain link
|
|
349
|
+
if (chainMode && baseBranch) {
|
|
350
|
+
if (verbose) {
|
|
351
|
+
console.log(chalk.gray(` 🔄 Rebasing existing worktree onto chain base (${baseBranch})...`));
|
|
352
|
+
}
|
|
353
|
+
const rebaseResult = spawnSync("git", ["-C", existingPath, "rebase", baseBranch], { stdio: "pipe" });
|
|
354
|
+
if (rebaseResult.status !== 0) {
|
|
355
|
+
const rebaseError = rebaseResult.stderr.toString();
|
|
356
|
+
// Check if it's a conflict
|
|
357
|
+
if (rebaseError.includes("CONFLICT") ||
|
|
358
|
+
rebaseError.includes("could not apply")) {
|
|
359
|
+
console.log(chalk.yellow(` ⚠️ Rebase conflict detected. Aborting rebase and keeping original branch state.`));
|
|
360
|
+
console.log(chalk.yellow(` ℹ️ Branch ${branch} is not properly chained. Manual rebase may be required.`));
|
|
361
|
+
// Abort the rebase to restore branch state
|
|
362
|
+
spawnSync("git", ["-C", existingPath, "rebase", "--abort"], {
|
|
363
|
+
stdio: "pipe",
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
console.log(chalk.yellow(` ⚠️ Rebase failed: ${rebaseError.trim()}`));
|
|
368
|
+
console.log(chalk.yellow(` ℹ️ Continuing with branch in its original state.`));
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
issue: issueNumber,
|
|
372
|
+
path: existingPath,
|
|
373
|
+
branch,
|
|
374
|
+
existed: true,
|
|
375
|
+
rebased: false,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
if (verbose) {
|
|
379
|
+
console.log(chalk.green(` ✅ Existing worktree rebased onto ${baseBranch}`));
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
issue: issueNumber,
|
|
383
|
+
path: existingPath,
|
|
384
|
+
branch,
|
|
385
|
+
existed: true,
|
|
386
|
+
rebased: true,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
204
389
|
return {
|
|
205
390
|
issue: issueNumber,
|
|
206
391
|
path: existingPath,
|
|
@@ -444,9 +629,7 @@ export function createCheckpointCommit(worktreePath, issueNumber, verbose) {
|
|
|
444
629
|
const commitMessage = `checkpoint(#${issueNumber}): QA passed
|
|
445
630
|
|
|
446
631
|
This is an automatic checkpoint commit created after issue #${issueNumber}
|
|
447
|
-
passed QA in chain mode. It serves as a recovery point if later issues fail
|
|
448
|
-
|
|
449
|
-
Co-Authored-By: Sequant <noreply@sequant.dev>`;
|
|
632
|
+
passed QA in chain mode. It serves as a recovery point if later issues fail.`;
|
|
450
633
|
const commitResult = spawnSync("git", ["-C", worktreePath, "commit", "-m", commitMessage], { stdio: "pipe" });
|
|
451
634
|
if (commitResult.status !== 0) {
|
|
452
635
|
const error = commitResult.stderr.toString();
|
|
@@ -458,6 +641,137 @@ Co-Authored-By: Sequant <noreply@sequant.dev>`;
|
|
|
458
641
|
console.log(chalk.green(` 📌 Checkpoint commit created for #${issueNumber}`));
|
|
459
642
|
return true;
|
|
460
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* Lockfile names for different package managers
|
|
646
|
+
*/
|
|
647
|
+
const LOCKFILES = [
|
|
648
|
+
"package-lock.json",
|
|
649
|
+
"pnpm-lock.yaml",
|
|
650
|
+
"bun.lock",
|
|
651
|
+
"yarn.lock",
|
|
652
|
+
];
|
|
653
|
+
/**
|
|
654
|
+
* Check if any lockfile changed during a rebase and re-run install if needed.
|
|
655
|
+
* This prevents dependency drift when the lockfile was updated on main.
|
|
656
|
+
* @param worktreePath Path to the worktree
|
|
657
|
+
* @param packageManager Package manager to use for install
|
|
658
|
+
* @param verbose Whether to show verbose output
|
|
659
|
+
* @param preRebaseRef Git ref pointing to pre-rebase HEAD (defaults to ORIG_HEAD,
|
|
660
|
+
* which git sets automatically after rebase). Using ORIG_HEAD captures all
|
|
661
|
+
* lockfile changes across multi-commit rebases, unlike HEAD~1 which only
|
|
662
|
+
* checks the last commit.
|
|
663
|
+
* @returns true if reinstall was performed, false otherwise
|
|
664
|
+
* @internal Exported for testing
|
|
665
|
+
*/
|
|
666
|
+
export function reinstallIfLockfileChanged(worktreePath, packageManager, verbose, preRebaseRef = "ORIG_HEAD") {
|
|
667
|
+
// Compare pre-rebase state to current HEAD to detect all lockfile changes
|
|
668
|
+
// introduced by the rebase (including changes from main that were pulled in)
|
|
669
|
+
let lockfileChanged = false;
|
|
670
|
+
for (const lockfile of LOCKFILES) {
|
|
671
|
+
const result = spawnSync("git", [
|
|
672
|
+
"-C",
|
|
673
|
+
worktreePath,
|
|
674
|
+
"diff",
|
|
675
|
+
"--name-only",
|
|
676
|
+
`${preRebaseRef}..HEAD`,
|
|
677
|
+
"--",
|
|
678
|
+
lockfile,
|
|
679
|
+
], { stdio: "pipe" });
|
|
680
|
+
if (result.status === 0 && result.stdout.toString().trim().length > 0) {
|
|
681
|
+
lockfileChanged = true;
|
|
682
|
+
if (verbose) {
|
|
683
|
+
console.log(chalk.gray(` 📦 Lockfile changed: ${lockfile}`));
|
|
684
|
+
}
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (!lockfileChanged) {
|
|
689
|
+
if (verbose) {
|
|
690
|
+
console.log(chalk.gray(` 📦 No lockfile changes detected`));
|
|
691
|
+
}
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
// Re-run install to sync node_modules with updated lockfile
|
|
695
|
+
console.log(chalk.blue(` 📦 Reinstalling dependencies (lockfile changed)...`));
|
|
696
|
+
const pm = packageManager || "npm";
|
|
697
|
+
const pmConfig = PM_CONFIG[pm];
|
|
698
|
+
const [cmd, ...args] = pmConfig.installSilent.split(" ");
|
|
699
|
+
const installResult = spawnSync(cmd, args, {
|
|
700
|
+
cwd: worktreePath,
|
|
701
|
+
stdio: "pipe",
|
|
702
|
+
});
|
|
703
|
+
if (installResult.status !== 0) {
|
|
704
|
+
const error = installResult.stderr.toString();
|
|
705
|
+
console.log(chalk.yellow(` ⚠️ Dependency reinstall failed: ${error.trim()}`));
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
console.log(chalk.green(` ✅ Dependencies reinstalled`));
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Rebase the worktree branch onto origin/main before PR creation.
|
|
713
|
+
* This ensures the branch is up-to-date and prevents lockfile drift.
|
|
714
|
+
*
|
|
715
|
+
* @param worktreePath Path to the worktree
|
|
716
|
+
* @param issueNumber Issue number (for logging)
|
|
717
|
+
* @param packageManager Package manager to use if reinstall needed
|
|
718
|
+
* @param verbose Whether to show verbose output
|
|
719
|
+
* @returns RebaseResult indicating success/failure and whether reinstall was performed
|
|
720
|
+
* @internal Exported for testing
|
|
721
|
+
*/
|
|
722
|
+
export function rebaseBeforePR(worktreePath, issueNumber, packageManager, verbose) {
|
|
723
|
+
if (verbose) {
|
|
724
|
+
console.log(chalk.gray(` 🔄 Rebasing #${issueNumber} onto origin/main before PR...`));
|
|
725
|
+
}
|
|
726
|
+
// Fetch latest main to ensure we're rebasing onto fresh state
|
|
727
|
+
const fetchResult = spawnSync("git", ["-C", worktreePath, "fetch", "origin", "main"], {
|
|
728
|
+
stdio: "pipe",
|
|
729
|
+
});
|
|
730
|
+
if (fetchResult.status !== 0) {
|
|
731
|
+
const error = fetchResult.stderr.toString();
|
|
732
|
+
console.log(chalk.yellow(` ⚠️ Could not fetch origin/main: ${error.trim()}`));
|
|
733
|
+
// Continue anyway - might work with local state
|
|
734
|
+
}
|
|
735
|
+
// Perform the rebase
|
|
736
|
+
const rebaseResult = spawnSync("git", ["-C", worktreePath, "rebase", "origin/main"], { stdio: "pipe" });
|
|
737
|
+
if (rebaseResult.status !== 0) {
|
|
738
|
+
const rebaseError = rebaseResult.stderr.toString();
|
|
739
|
+
// Check if it's a conflict
|
|
740
|
+
if (rebaseError.includes("CONFLICT") ||
|
|
741
|
+
rebaseError.includes("could not apply")) {
|
|
742
|
+
console.log(chalk.yellow(` ⚠️ Rebase conflict detected. Aborting rebase and keeping original branch state.`));
|
|
743
|
+
console.log(chalk.yellow(` ℹ️ PR will be created without rebase. Manual rebase may be required before merge.`));
|
|
744
|
+
// Abort the rebase to restore branch state
|
|
745
|
+
spawnSync("git", ["-C", worktreePath, "rebase", "--abort"], {
|
|
746
|
+
stdio: "pipe",
|
|
747
|
+
});
|
|
748
|
+
return {
|
|
749
|
+
performed: true,
|
|
750
|
+
success: false,
|
|
751
|
+
reinstalled: false,
|
|
752
|
+
error: "Rebase conflict - manual resolution required",
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
console.log(chalk.yellow(` ⚠️ Rebase failed: ${rebaseError.trim()}`));
|
|
757
|
+
console.log(chalk.yellow(` ℹ️ Continuing with branch in its original state.`));
|
|
758
|
+
return {
|
|
759
|
+
performed: true,
|
|
760
|
+
success: false,
|
|
761
|
+
reinstalled: false,
|
|
762
|
+
error: rebaseError.trim(),
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
console.log(chalk.green(` ✅ Branch rebased onto origin/main`));
|
|
767
|
+
// Check if lockfile changed and reinstall if needed
|
|
768
|
+
const reinstalled = reinstallIfLockfileChanged(worktreePath, packageManager, verbose);
|
|
769
|
+
return {
|
|
770
|
+
performed: true,
|
|
771
|
+
success: true,
|
|
772
|
+
reinstalled,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
461
775
|
/**
|
|
462
776
|
* Natural language prompts for each phase
|
|
463
777
|
* These prompts will invoke the corresponding skills via natural language
|
|
@@ -671,6 +985,11 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
671
985
|
// Get MCP servers config if enabled
|
|
672
986
|
// Reads from Claude Desktop config and passes to SDK for headless MCP support
|
|
673
987
|
const mcpServers = config.mcp ? getMcpServersConfig() : undefined;
|
|
988
|
+
// Track whether we're actively streaming verbose output
|
|
989
|
+
// Pausing spinner once per streaming session prevents truncation from rapid pause/resume cycles
|
|
990
|
+
// (Issue #283: ora's stop() clears the current line, which can truncate output when
|
|
991
|
+
// pause/resume is called for every chunk in rapid succession)
|
|
992
|
+
let verboseStreamingActive = false;
|
|
674
993
|
const queryInstance = query({
|
|
675
994
|
prompt,
|
|
676
995
|
options: {
|
|
@@ -693,10 +1012,14 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
693
1012
|
// Capture stderr for debugging (helps diagnose early exit failures)
|
|
694
1013
|
stderr: (data) => {
|
|
695
1014
|
capturedStderr += data;
|
|
1015
|
+
// Write stderr in verbose mode
|
|
696
1016
|
if (config.verbose) {
|
|
697
|
-
spinner
|
|
1017
|
+
// Pause spinner once to avoid truncation (Issue #283)
|
|
1018
|
+
if (!verboseStreamingActive) {
|
|
1019
|
+
spinner?.pause();
|
|
1020
|
+
verboseStreamingActive = true;
|
|
1021
|
+
}
|
|
698
1022
|
process.stderr.write(chalk.red(data));
|
|
699
|
-
spinner?.resume();
|
|
700
1023
|
}
|
|
701
1024
|
},
|
|
702
1025
|
},
|
|
@@ -719,10 +1042,13 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
719
1042
|
capturedOutput += textContent;
|
|
720
1043
|
// Show streaming output in verbose mode
|
|
721
1044
|
if (config.verbose) {
|
|
722
|
-
// Pause spinner
|
|
723
|
-
|
|
1045
|
+
// Pause spinner once at start of streaming to avoid truncation
|
|
1046
|
+
// (Issue #283: repeated pause/resume causes ora to clear lines between chunks)
|
|
1047
|
+
if (!verboseStreamingActive) {
|
|
1048
|
+
spinner?.pause();
|
|
1049
|
+
verboseStreamingActive = true;
|
|
1050
|
+
}
|
|
724
1051
|
process.stdout.write(chalk.gray(textContent));
|
|
725
|
-
spinner?.resume();
|
|
726
1052
|
}
|
|
727
1053
|
}
|
|
728
1054
|
}
|
|
@@ -731,6 +1057,11 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
731
1057
|
resultMessage = message;
|
|
732
1058
|
}
|
|
733
1059
|
}
|
|
1060
|
+
// Resume spinner after streaming completes (if we paused it)
|
|
1061
|
+
if (verboseStreamingActive) {
|
|
1062
|
+
spinner?.resume();
|
|
1063
|
+
verboseStreamingActive = false;
|
|
1064
|
+
}
|
|
734
1065
|
clearTimeout(timeoutId);
|
|
735
1066
|
// Clear abort controller from shutdown manager
|
|
736
1067
|
if (shutdownManager) {
|
|
@@ -844,14 +1175,30 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
844
1175
|
const COLD_START_THRESHOLD_SECONDS = 60;
|
|
845
1176
|
const COLD_START_MAX_RETRIES = 2;
|
|
846
1177
|
/**
|
|
847
|
-
* Execute a phase with automatic retry for cold-start failures.
|
|
848
|
-
*
|
|
849
|
-
*
|
|
1178
|
+
* Execute a phase with automatic retry for cold-start failures and MCP fallback.
|
|
1179
|
+
*
|
|
1180
|
+
* Retry strategy:
|
|
1181
|
+
* 1. If phase fails within COLD_START_THRESHOLD_SECONDS, retry up to COLD_START_MAX_RETRIES times
|
|
1182
|
+
* 2. If still failing and MCP is enabled, retry once with MCP disabled (npx-based MCP servers
|
|
1183
|
+
* can fail on first run due to cold-cache issues)
|
|
1184
|
+
*
|
|
1185
|
+
* The MCP fallback is safe because MCP servers are optional enhancements, not required
|
|
1186
|
+
* for core functionality.
|
|
1187
|
+
*/
|
|
1188
|
+
/**
|
|
1189
|
+
* @internal Exported for testing only
|
|
850
1190
|
*/
|
|
851
|
-
async function executePhaseWithRetry(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, spinner
|
|
1191
|
+
export async function executePhaseWithRetry(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, spinner,
|
|
1192
|
+
/** @internal Injected for testing — defaults to module-level executePhase */
|
|
1193
|
+
executePhaseFn = executePhase) {
|
|
1194
|
+
// Skip retry logic if explicitly disabled
|
|
1195
|
+
if (config.retry === false) {
|
|
1196
|
+
return executePhaseFn(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, spinner);
|
|
1197
|
+
}
|
|
852
1198
|
let lastResult;
|
|
1199
|
+
// Phase 1: Cold-start retry attempts (with MCP enabled if configured)
|
|
853
1200
|
for (let attempt = 0; attempt <= COLD_START_MAX_RETRIES; attempt++) {
|
|
854
|
-
lastResult = await
|
|
1201
|
+
lastResult = await executePhaseFn(issueNumber, phase, config, sessionId, worktreePath, shutdownManager, spinner);
|
|
855
1202
|
const duration = lastResult.durationSeconds ?? 0;
|
|
856
1203
|
// Success or genuine failure (took long enough to be real work)
|
|
857
1204
|
if (lastResult.success || duration >= COLD_START_THRESHOLD_SECONDS) {
|
|
@@ -864,6 +1211,28 @@ async function executePhaseWithRetry(issueNumber, phase, config, sessionId, work
|
|
|
864
1211
|
}
|
|
865
1212
|
}
|
|
866
1213
|
}
|
|
1214
|
+
// Capture the original error for better diagnostics
|
|
1215
|
+
const originalError = lastResult.error;
|
|
1216
|
+
// Phase 2: MCP fallback - if MCP is enabled and we're still failing, try without MCP
|
|
1217
|
+
// This handles npx-based MCP servers that fail on first run due to cold-cache issues
|
|
1218
|
+
if (config.mcp && !lastResult.success) {
|
|
1219
|
+
console.log(chalk.yellow(`\n ⚠️ Phase failed with MCP enabled, retrying without MCP...`));
|
|
1220
|
+
// Create config copy with MCP disabled
|
|
1221
|
+
const configWithoutMcp = {
|
|
1222
|
+
...config,
|
|
1223
|
+
mcp: false,
|
|
1224
|
+
};
|
|
1225
|
+
const retryResult = await executePhaseFn(issueNumber, phase, configWithoutMcp, sessionId, worktreePath, shutdownManager, spinner);
|
|
1226
|
+
if (retryResult.success) {
|
|
1227
|
+
console.log(chalk.green(` ✓ Phase succeeded without MCP (MCP cold-start issue detected)`));
|
|
1228
|
+
return retryResult;
|
|
1229
|
+
}
|
|
1230
|
+
// Both attempts failed - return original error for better diagnostics
|
|
1231
|
+
return {
|
|
1232
|
+
...lastResult,
|
|
1233
|
+
error: originalError,
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
867
1236
|
return lastResult;
|
|
868
1237
|
}
|
|
869
1238
|
/**
|
|
@@ -1160,6 +1529,10 @@ export async function runCommand(issues, options) {
|
|
|
1160
1529
|
const mcpEnabled = mergedOptions.noMcp
|
|
1161
1530
|
? false
|
|
1162
1531
|
: (settings.run.mcp ?? DEFAULT_CONFIG.mcp);
|
|
1532
|
+
// Resolve retry setting: CLI flag → settings.run.retry → default (true)
|
|
1533
|
+
const retryEnabled = mergedOptions.noRetry
|
|
1534
|
+
? false
|
|
1535
|
+
: (settings.run.retry ?? true);
|
|
1163
1536
|
const config = {
|
|
1164
1537
|
...DEFAULT_CONFIG,
|
|
1165
1538
|
phases: explicitPhases ?? DEFAULT_PHASES,
|
|
@@ -1171,6 +1544,7 @@ export async function runCommand(issues, options) {
|
|
|
1171
1544
|
maxIterations: mergedOptions.maxIterations ?? DEFAULT_CONFIG.maxIterations,
|
|
1172
1545
|
noSmartTests: mergedOptions.noSmartTests ?? false,
|
|
1173
1546
|
mcp: mcpEnabled,
|
|
1547
|
+
retry: retryEnabled,
|
|
1174
1548
|
};
|
|
1175
1549
|
// Propagate verbose mode to UI config so spinners use text-only mode.
|
|
1176
1550
|
// This prevents animated spinner control characters from colliding with
|
|
@@ -1190,12 +1564,24 @@ export async function runCommand(issues, options) {
|
|
|
1190
1564
|
sequential: config.sequential,
|
|
1191
1565
|
qualityLoop: config.qualityLoop,
|
|
1192
1566
|
maxIterations: config.maxIterations,
|
|
1567
|
+
chain: mergedOptions.chain,
|
|
1568
|
+
qaGate: mergedOptions.qaGate,
|
|
1193
1569
|
};
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1570
|
+
try {
|
|
1571
|
+
logWriter = new LogWriter({
|
|
1572
|
+
logPath: mergedOptions.logPath ?? settings.run.logPath,
|
|
1573
|
+
verbose: config.verbose,
|
|
1574
|
+
startCommit: getCommitHash(process.cwd()),
|
|
1575
|
+
});
|
|
1576
|
+
await logWriter.initialize(runConfig);
|
|
1577
|
+
}
|
|
1578
|
+
catch (err) {
|
|
1579
|
+
// Log initialization failure is non-fatal - warn and continue without logging
|
|
1580
|
+
// Common causes: permissions issues, disk full, invalid path
|
|
1581
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1582
|
+
console.log(chalk.yellow(` ⚠️ Log initialization failed, continuing without logging: ${errorMessage}`));
|
|
1583
|
+
logWriter = null;
|
|
1584
|
+
}
|
|
1199
1585
|
}
|
|
1200
1586
|
// Initialize state manager for persistent workflow state tracking
|
|
1201
1587
|
// State tracking is always enabled (unless dry run)
|
|
@@ -1239,7 +1625,62 @@ export async function runCommand(issues, options) {
|
|
|
1239
1625
|
if (stateManager) {
|
|
1240
1626
|
console.log(chalk.gray(` State tracking: enabled`));
|
|
1241
1627
|
}
|
|
1628
|
+
if (mergedOptions.force) {
|
|
1629
|
+
console.log(chalk.yellow(` Force mode: enabled (bypass state guard)`));
|
|
1630
|
+
}
|
|
1242
1631
|
console.log(chalk.gray(` Issues: ${issueNumbers.map((n) => `#${n}`).join(", ")}`));
|
|
1632
|
+
// ============================================================================
|
|
1633
|
+
// Pre-flight State Guard (#305)
|
|
1634
|
+
// ============================================================================
|
|
1635
|
+
// AC-5: Auto-cleanup at run start - reconcile stale ready_for_merge states
|
|
1636
|
+
if (stateManager && !config.dryRun) {
|
|
1637
|
+
try {
|
|
1638
|
+
const reconcileResult = await reconcileStateAtStartup({
|
|
1639
|
+
verbose: config.verbose,
|
|
1640
|
+
});
|
|
1641
|
+
if (reconcileResult.success && reconcileResult.advanced.length > 0) {
|
|
1642
|
+
console.log(chalk.gray(` State reconciled: ${reconcileResult.advanced.map((n) => `#${n}`).join(", ")} → merged`));
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
catch {
|
|
1646
|
+
// AC-8: Graceful degradation - don't block execution on reconciliation failure
|
|
1647
|
+
if (config.verbose) {
|
|
1648
|
+
console.log(chalk.yellow(` ⚠️ State reconciliation failed, continuing...`));
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
// AC-1 & AC-2: Pre-flight state guard - skip completed issues unless --force
|
|
1653
|
+
if (stateManager && !config.dryRun && !mergedOptions.force) {
|
|
1654
|
+
const skippedIssues = [];
|
|
1655
|
+
const activeIssues = [];
|
|
1656
|
+
for (const issueNumber of issueNumbers) {
|
|
1657
|
+
try {
|
|
1658
|
+
const issueState = await stateManager.getIssueState(issueNumber);
|
|
1659
|
+
if (issueState &&
|
|
1660
|
+
(issueState.status === "ready_for_merge" ||
|
|
1661
|
+
issueState.status === "merged")) {
|
|
1662
|
+
skippedIssues.push(issueNumber);
|
|
1663
|
+
console.log(chalk.yellow(` ⚠️ #${issueNumber}: already ${issueState.status} — skipping (use --force to re-run)`));
|
|
1664
|
+
}
|
|
1665
|
+
else {
|
|
1666
|
+
activeIssues.push(issueNumber);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
catch {
|
|
1670
|
+
// AC-8: Graceful degradation - if state check fails, include the issue
|
|
1671
|
+
activeIssues.push(issueNumber);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
// Update issueNumbers to only include active issues
|
|
1675
|
+
if (skippedIssues.length > 0) {
|
|
1676
|
+
issueNumbers = activeIssues;
|
|
1677
|
+
if (issueNumbers.length === 0) {
|
|
1678
|
+
console.log(chalk.yellow(`\n All issues already completed. Use --force to re-run.`));
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
console.log(chalk.gray(` Active issues: ${issueNumbers.map((n) => `#${n}`).join(", ")}`));
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1243
1684
|
// Worktree isolation is enabled by default for multi-issue runs
|
|
1244
1685
|
const useWorktreeIsolation = mergedOptions.worktreeIsolation !== false && issueNumbers.length > 0;
|
|
1245
1686
|
if (useWorktreeIsolation) {
|
|
@@ -1297,7 +1738,7 @@ export async function runCommand(issues, options) {
|
|
|
1297
1738
|
for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
|
|
1298
1739
|
const batch = batches[batchIdx];
|
|
1299
1740
|
console.log(chalk.blue(`\n Batch ${batchIdx + 1}/${batches.length}: Issues ${batch.map((n) => `#${n}`).join(", ")}`));
|
|
1300
|
-
const batchResults = await executeBatch(batch, config, logWriter, stateManager, mergedOptions, issueInfoMap, worktreeMap, shutdown);
|
|
1741
|
+
const batchResults = await executeBatch(batch, config, logWriter, stateManager, mergedOptions, issueInfoMap, worktreeMap, shutdown, manifest.packageManager);
|
|
1301
1742
|
results.push(...batchResults);
|
|
1302
1743
|
// Check if batch failed and we should stop
|
|
1303
1744
|
const batchFailed = batchResults.some((r) => !r.success);
|
|
@@ -1309,7 +1750,8 @@ export async function runCommand(issues, options) {
|
|
|
1309
1750
|
}
|
|
1310
1751
|
else if (config.sequential) {
|
|
1311
1752
|
// Sequential execution
|
|
1312
|
-
for (
|
|
1753
|
+
for (let i = 0; i < issueNumbers.length; i++) {
|
|
1754
|
+
const issueNumber = issueNumbers[i];
|
|
1313
1755
|
const issueInfo = issueInfoMap.get(issueNumber) ?? {
|
|
1314
1756
|
title: `Issue #${issueNumber}`,
|
|
1315
1757
|
labels: [],
|
|
@@ -1319,7 +1761,10 @@ export async function runCommand(issues, options) {
|
|
|
1319
1761
|
if (logWriter) {
|
|
1320
1762
|
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
1321
1763
|
}
|
|
1322
|
-
const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, mergedOptions, worktreeInfo?.path, worktreeInfo?.branch, shutdown, mergedOptions.chain
|
|
1764
|
+
const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, mergedOptions, worktreeInfo?.path, worktreeInfo?.branch, shutdown, mergedOptions.chain, // Enable checkpoint commits in chain mode
|
|
1765
|
+
manifest.packageManager,
|
|
1766
|
+
// In chain mode, only the last issue should trigger pre-PR rebase
|
|
1767
|
+
mergedOptions.chain ? i === issueNumbers.length - 1 : undefined);
|
|
1323
1768
|
results.push(result);
|
|
1324
1769
|
// Complete issue logging
|
|
1325
1770
|
if (logWriter) {
|
|
@@ -1374,7 +1819,8 @@ export async function runCommand(issues, options) {
|
|
|
1374
1819
|
if (logWriter) {
|
|
1375
1820
|
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
1376
1821
|
}
|
|
1377
|
-
const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, mergedOptions, worktreeInfo?.path, worktreeInfo?.branch, shutdown, false
|
|
1822
|
+
const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, mergedOptions, worktreeInfo?.path, worktreeInfo?.branch, shutdown, false, // Parallel mode doesn't support chain
|
|
1823
|
+
manifest.packageManager);
|
|
1378
1824
|
results.push(result);
|
|
1379
1825
|
// Complete issue logging
|
|
1380
1826
|
if (logWriter) {
|
|
@@ -1385,7 +1831,9 @@ export async function runCommand(issues, options) {
|
|
|
1385
1831
|
// Finalize log
|
|
1386
1832
|
let logPath = null;
|
|
1387
1833
|
if (logWriter) {
|
|
1388
|
-
logPath = await logWriter.finalize(
|
|
1834
|
+
logPath = await logWriter.finalize({
|
|
1835
|
+
endCommit: getCommitHash(process.cwd()),
|
|
1836
|
+
});
|
|
1389
1837
|
}
|
|
1390
1838
|
// Calculate success/failure counts
|
|
1391
1839
|
const passed = results.filter((r) => r.success).length;
|
|
@@ -1443,6 +1891,8 @@ export async function runCommand(issues, options) {
|
|
|
1443
1891
|
cliFlags.push("--quality-loop");
|
|
1444
1892
|
if (mergedOptions.testgen)
|
|
1445
1893
|
cliFlags.push("--testgen");
|
|
1894
|
+
// Read token usage from SessionEnd hook files (AC-5, AC-6)
|
|
1895
|
+
const tokenUsage = getTokenUsageForRun(undefined, true); // cleanup after reading
|
|
1446
1896
|
// Record the run
|
|
1447
1897
|
await metricsWriter.recordRun({
|
|
1448
1898
|
issues: issueNumbers,
|
|
@@ -1452,11 +1902,15 @@ export async function runCommand(issues, options) {
|
|
|
1452
1902
|
model: process.env.ANTHROPIC_MODEL ?? "opus",
|
|
1453
1903
|
flags: cliFlags,
|
|
1454
1904
|
metrics: {
|
|
1455
|
-
tokensUsed:
|
|
1905
|
+
tokensUsed: tokenUsage.tokensUsed,
|
|
1456
1906
|
filesChanged: totalFilesChanged,
|
|
1457
1907
|
linesAdded: totalLinesAdded,
|
|
1458
1908
|
acceptanceCriteria: 0, // Would need to parse from issue
|
|
1459
1909
|
qaIterations: totalQaIterations,
|
|
1910
|
+
// Token breakdown (AC-6)
|
|
1911
|
+
inputTokens: tokenUsage.inputTokens || undefined,
|
|
1912
|
+
outputTokens: tokenUsage.outputTokens || undefined,
|
|
1913
|
+
cacheTokens: tokenUsage.cacheTokens || undefined,
|
|
1460
1914
|
},
|
|
1461
1915
|
});
|
|
1462
1916
|
if (config.verbose) {
|
|
@@ -1514,7 +1968,7 @@ export async function runCommand(issues, options) {
|
|
|
1514
1968
|
/**
|
|
1515
1969
|
* Execute a batch of issues
|
|
1516
1970
|
*/
|
|
1517
|
-
async function executeBatch(issueNumbers, config, logWriter, stateManager, options, issueInfoMap, worktreeMap, shutdownManager) {
|
|
1971
|
+
async function executeBatch(issueNumbers, config, logWriter, stateManager, options, issueInfoMap, worktreeMap, shutdownManager, packageManager) {
|
|
1518
1972
|
const results = [];
|
|
1519
1973
|
for (const issueNumber of issueNumbers) {
|
|
1520
1974
|
// Check if shutdown was triggered
|
|
@@ -1530,7 +1984,8 @@ async function executeBatch(issueNumbers, config, logWriter, stateManager, optio
|
|
|
1530
1984
|
if (logWriter) {
|
|
1531
1985
|
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
1532
1986
|
}
|
|
1533
|
-
const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, options, worktreeInfo?.path, worktreeInfo?.branch, shutdownManager, false
|
|
1987
|
+
const result = await runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueInfo.title, issueInfo.labels, options, worktreeInfo?.path, worktreeInfo?.branch, shutdownManager, false, // Batch mode doesn't support chain
|
|
1988
|
+
packageManager);
|
|
1534
1989
|
results.push(result);
|
|
1535
1990
|
// Complete issue logging
|
|
1536
1991
|
if (logWriter) {
|
|
@@ -1542,7 +1997,7 @@ async function executeBatch(issueNumbers, config, logWriter, stateManager, optio
|
|
|
1542
1997
|
/**
|
|
1543
1998
|
* Execute all phases for a single issue with logging and quality loop
|
|
1544
1999
|
*/
|
|
1545
|
-
async function runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueTitle, labels, options, worktreePath, branch, shutdownManager, chainMode) {
|
|
2000
|
+
async function runIssueWithLogging(issueNumber, config, logWriter, stateManager, issueTitle, labels, options, worktreePath, branch, shutdownManager, chainMode, packageManager, isLastInChain) {
|
|
1546
2001
|
const startTime = Date.now();
|
|
1547
2002
|
const phaseResults = [];
|
|
1548
2003
|
let loopTriggered = false;
|
|
@@ -1630,6 +2085,7 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
|
|
|
1630
2085
|
phaseResults.push(specResult);
|
|
1631
2086
|
specAlreadyRan = true;
|
|
1632
2087
|
// Log spec phase result
|
|
2088
|
+
// Note: Spec runs in main repo, not worktree, so no git diff stats
|
|
1633
2089
|
if (logWriter) {
|
|
1634
2090
|
const phaseLog = createPhaseLogFromTiming("spec", issueNumber, specStartTime, specEndTime, specResult.success
|
|
1635
2091
|
? "success"
|
|
@@ -1771,16 +2227,30 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
|
|
|
1771
2227
|
}
|
|
1772
2228
|
}
|
|
1773
2229
|
phaseResults.push(result);
|
|
1774
|
-
// Log phase result
|
|
2230
|
+
// Log phase result with observability data (AC-1, AC-2, AC-3, AC-7)
|
|
1775
2231
|
if (logWriter) {
|
|
2232
|
+
// Capture git diff stats for worktree phases (AC-1, AC-3)
|
|
2233
|
+
const diffStats = worktreePath
|
|
2234
|
+
? getGitDiffStats(worktreePath)
|
|
2235
|
+
: undefined;
|
|
2236
|
+
// Capture commit hash after phase (AC-2)
|
|
2237
|
+
const commitHash = worktreePath
|
|
2238
|
+
? getCommitHash(worktreePath)
|
|
2239
|
+
: undefined;
|
|
2240
|
+
// Read cache metrics for QA phase (AC-7)
|
|
2241
|
+
const cacheMetrics = phase === "qa" ? readCacheMetrics(worktreePath) : undefined;
|
|
1776
2242
|
const phaseLog = createPhaseLogFromTiming(phase, issueNumber, phaseStartTime, phaseEndTime, result.success
|
|
1777
2243
|
? "success"
|
|
1778
2244
|
: result.error?.includes("Timeout")
|
|
1779
2245
|
? "timeout"
|
|
1780
2246
|
: "failure", {
|
|
1781
2247
|
error: result.error,
|
|
1782
|
-
// Include verdict for QA phase (AC-6)
|
|
1783
2248
|
verdict: result.verdict,
|
|
2249
|
+
// Observability fields (AC-1, AC-2, AC-3, AC-7)
|
|
2250
|
+
filesModified: diffStats?.filesModified,
|
|
2251
|
+
fileDiffStats: diffStats?.fileDiffStats,
|
|
2252
|
+
commitHash,
|
|
2253
|
+
cacheMetrics,
|
|
1784
2254
|
});
|
|
1785
2255
|
logWriter.logPhase(phaseLog);
|
|
1786
2256
|
}
|
|
@@ -1861,6 +2331,18 @@ async function runIssueWithLogging(issueNumber, config, logWriter, stateManager,
|
|
|
1861
2331
|
if (success && chainMode && worktreePath) {
|
|
1862
2332
|
createCheckpointCommit(worktreePath, issueNumber, config.verbose);
|
|
1863
2333
|
}
|
|
2334
|
+
// Rebase onto origin/main before PR creation (unless --no-rebase)
|
|
2335
|
+
// This ensures the branch is up-to-date and prevents lockfile drift
|
|
2336
|
+
// AC-1: Non-chain mode rebases onto origin/main before PR
|
|
2337
|
+
// AC-2: Chain mode rebases only the final branch onto origin/main before PR
|
|
2338
|
+
// (intermediate branches must stay based on their predecessor)
|
|
2339
|
+
const shouldRebase = success &&
|
|
2340
|
+
worktreePath &&
|
|
2341
|
+
!options.noRebase &&
|
|
2342
|
+
(!chainMode || isLastInChain);
|
|
2343
|
+
if (shouldRebase) {
|
|
2344
|
+
rebaseBeforePR(worktreePath, issueNumber, packageManager, config.verbose);
|
|
2345
|
+
}
|
|
1864
2346
|
return {
|
|
1865
2347
|
issueNumber,
|
|
1866
2348
|
success,
|