sequant 1.20.2 → 2.0.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/.claude-plugin/marketplace.json +2 -4
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +29 -9
- package/dist/bin/cli.js +25 -2
- package/dist/src/commands/doctor.js +42 -9
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +52 -0
- package/dist/src/commands/logs.d.ts +1 -0
- package/dist/src/commands/logs.js +18 -2
- package/dist/src/commands/run.d.ts +7 -0
- package/dist/src/commands/run.js +235 -68
- package/dist/src/commands/serve.d.ts +13 -0
- package/dist/src/commands/serve.js +131 -0
- package/dist/src/commands/stats.d.ts +1 -0
- package/dist/src/commands/stats.js +185 -26
- package/dist/src/commands/status.d.ts +2 -0
- package/dist/src/commands/status.js +99 -50
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +4 -1
- package/dist/src/lib/ac-parser.d.ts +2 -0
- package/dist/src/lib/ac-parser.js +12 -2
- package/dist/src/lib/assess-comment-parser.d.ts +137 -0
- package/dist/src/lib/assess-comment-parser.js +344 -0
- package/dist/src/lib/ci/config.d.ts +22 -0
- package/dist/src/lib/ci/config.js +134 -0
- package/dist/src/lib/ci/index.d.ts +12 -0
- package/dist/src/lib/ci/index.js +10 -0
- package/dist/src/lib/ci/inputs.d.ts +29 -0
- package/dist/src/lib/ci/inputs.js +103 -0
- package/dist/src/lib/ci/labels.d.ts +34 -0
- package/dist/src/lib/ci/labels.js +101 -0
- package/dist/src/lib/ci/outputs.d.ts +25 -0
- package/dist/src/lib/ci/outputs.js +84 -0
- package/dist/src/lib/ci/triggers.d.ts +9 -0
- package/dist/src/lib/ci/triggers.js +86 -0
- package/dist/src/lib/ci/types.d.ts +131 -0
- package/dist/src/lib/ci/types.js +47 -0
- package/dist/src/lib/mcp-config.d.ts +54 -0
- package/dist/src/lib/mcp-config.js +172 -0
- package/dist/src/lib/merge-check/index.js +6 -12
- package/dist/src/lib/merge-check/types.d.ts +20 -7
- package/dist/src/lib/merge-check/types.js +11 -0
- package/dist/src/lib/phase-signal.d.ts +3 -3
- package/dist/src/lib/phase-signal.js +5 -3
- package/dist/src/lib/settings.d.ts +52 -0
- package/dist/src/lib/settings.js +41 -0
- package/dist/src/lib/shutdown.d.ts +16 -5
- package/dist/src/lib/shutdown.js +32 -12
- package/dist/src/lib/solve-comment-parser.d.ts +9 -102
- package/dist/src/lib/solve-comment-parser.js +13 -248
- package/dist/src/lib/stacks.d.ts +8 -0
- package/dist/src/lib/stacks.js +34 -0
- package/dist/src/lib/system.js +3 -7
- package/dist/src/lib/test-tautology-detector.d.ts +10 -0
- package/dist/src/lib/test-tautology-detector.js +43 -4
- package/dist/src/lib/upstream/assessment.js +9 -59
- package/dist/src/lib/upstream/issues.js +12 -75
- package/dist/src/lib/version-check.d.ts +2 -2
- package/dist/src/lib/version-check.js +6 -3
- package/dist/src/lib/version.d.ts +4 -0
- package/dist/src/lib/version.js +25 -0
- package/dist/src/lib/workflow/batch-executor.d.ts +18 -86
- package/dist/src/lib/workflow/batch-executor.js +232 -55
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +56 -0
- package/dist/src/lib/workflow/drivers/agent-driver.js +8 -0
- package/dist/src/lib/workflow/drivers/aider.d.ts +18 -0
- package/dist/src/lib/workflow/drivers/aider.js +160 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +165 -0
- package/dist/src/lib/workflow/drivers/index.d.ts +20 -0
- package/dist/src/lib/workflow/drivers/index.js +27 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +16 -0
- package/dist/src/lib/workflow/error-classifier.js +90 -0
- package/dist/src/lib/workflow/log-writer.d.ts +6 -3
- package/dist/src/lib/workflow/log-writer.js +57 -27
- package/dist/src/lib/workflow/metrics-schema.d.ts +9 -9
- package/dist/src/lib/workflow/phase-detection.d.ts +23 -0
- package/dist/src/lib/workflow/phase-detection.js +45 -29
- package/dist/src/lib/workflow/phase-executor.d.ts +42 -3
- package/dist/src/lib/workflow/phase-executor.js +345 -220
- package/dist/src/lib/workflow/phase-mapper.d.ts +1 -1
- package/dist/src/lib/workflow/phase-mapper.js +7 -7
- package/dist/src/lib/workflow/platforms/github.d.ts +157 -0
- package/dist/src/lib/workflow/platforms/github.js +466 -0
- package/dist/src/lib/workflow/platforms/index.d.ts +17 -0
- package/dist/src/lib/workflow/platforms/index.js +25 -0
- package/dist/src/lib/workflow/platforms/platform-provider.d.ts +67 -0
- package/dist/src/lib/workflow/platforms/platform-provider.js +8 -0
- package/dist/src/lib/workflow/pr-status.d.ts +2 -4
- package/dist/src/lib/workflow/pr-status.js +3 -16
- package/dist/src/lib/workflow/qa-cache.d.ts +58 -0
- package/dist/src/lib/workflow/qa-cache.js +88 -0
- package/dist/src/lib/workflow/reconcile.d.ts +69 -0
- package/dist/src/lib/workflow/reconcile.js +290 -0
- package/dist/src/lib/workflow/ring-buffer.d.ts +17 -0
- package/dist/src/lib/workflow/ring-buffer.js +37 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts +115 -24
- package/dist/src/lib/workflow/run-log-schema.js +47 -12
- package/dist/src/lib/workflow/run-reflect.js +1 -1
- package/dist/src/lib/workflow/state-cleanup.js +21 -0
- package/dist/src/lib/workflow/state-manager.d.ts +34 -3
- package/dist/src/lib/workflow/state-manager.js +278 -126
- package/dist/src/lib/workflow/state-schema.d.ts +34 -30
- package/dist/src/lib/workflow/state-schema.js +35 -25
- package/dist/src/lib/workflow/state-utils.d.ts +3 -1
- package/dist/src/lib/workflow/state-utils.js +1 -0
- package/dist/src/lib/workflow/types.d.ts +208 -6
- package/dist/src/lib/workflow/types.js +20 -1
- package/dist/src/lib/workflow/worktree-discovery.d.ts +1 -1
- package/dist/src/lib/workflow/worktree-discovery.js +6 -14
- package/dist/src/lib/workflow/worktree-manager.js +33 -51
- package/dist/src/mcp/index.d.ts +4 -0
- package/dist/src/mcp/index.js +4 -0
- package/dist/src/mcp/resources.d.ts +7 -0
- package/dist/src/mcp/resources.js +111 -0
- package/dist/src/mcp/run-registry.d.ts +34 -0
- package/dist/src/mcp/run-registry.js +42 -0
- package/dist/src/mcp/server.d.ts +12 -0
- package/dist/src/mcp/server.js +50 -0
- package/dist/src/mcp/tools/logs.d.ts +7 -0
- package/dist/src/mcp/tools/logs.js +149 -0
- package/dist/src/mcp/tools/run.d.ts +121 -0
- package/dist/src/mcp/tools/run.js +591 -0
- package/dist/src/mcp/tools/status.d.ts +7 -0
- package/dist/src/mcp/tools/status.js +127 -0
- package/package.json +10 -1
- package/templates/hooks/post-tool.sh +19 -8
- package/templates/hooks/pre-tool.sh +36 -49
- package/templates/mcp.json +6 -0
- package/templates/skills/assess/SKILL.md +354 -352
- package/templates/skills/exec/SKILL.md +64 -1
- package/templates/skills/fullsolve/SKILL.md +35 -4
- package/templates/skills/qa/SKILL.md +486 -9
- package/templates/skills/qa/scripts/quality-checks.sh +1 -1
- package/templates/skills/setup/SKILL.md +386 -0
- package/templates/skills/solve/SKILL.md +38 -664
- package/templates/skills/spec/SKILL.md +90 -31
|
@@ -10,11 +10,35 @@
|
|
|
10
10
|
import chalk from "chalk";
|
|
11
11
|
import { spawnSync } from "child_process";
|
|
12
12
|
import { createPhaseLogFromTiming } from "./log-writer.js";
|
|
13
|
+
import { classifyError } from "./error-classifier.js";
|
|
13
14
|
import { PhaseSpinner } from "../phase-spinner.js";
|
|
14
15
|
import { getGitDiffStats, getCommitHash } from "./git-diff-utils.js";
|
|
15
16
|
import { createCheckpointCommit, rebaseBeforePR, createPR, readCacheMetrics, filterResumedPhases, } from "./worktree-manager.js";
|
|
16
17
|
import { executePhaseWithRetry } from "./phase-executor.js";
|
|
17
|
-
import { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIssue, BUG_LABELS, } from "./phase-mapper.js";
|
|
18
|
+
import { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIssue, BUG_LABELS, DOCS_LABELS, } from "./phase-mapper.js";
|
|
19
|
+
/**
|
|
20
|
+
* Emit a structured progress line to stderr for MCP progress notifications.
|
|
21
|
+
* Only emits when running under an orchestrator (e.g., MCP server).
|
|
22
|
+
* The MCP handler parses these lines to send `notifications/progress`.
|
|
23
|
+
*
|
|
24
|
+
* @param issue - GitHub issue number
|
|
25
|
+
* @param phase - Phase name (e.g., "spec", "exec", "qa")
|
|
26
|
+
* @param event - Phase lifecycle event: "start", "complete", or "failed"
|
|
27
|
+
* @param extra - Optional fields: durationSeconds (on complete), error (on failed)
|
|
28
|
+
*/
|
|
29
|
+
export function emitProgressLine(issue, phase, event = "start", extra) {
|
|
30
|
+
if (!process.env.SEQUANT_ORCHESTRATOR)
|
|
31
|
+
return;
|
|
32
|
+
const payload = { issue, phase, event };
|
|
33
|
+
if (extra?.durationSeconds !== undefined) {
|
|
34
|
+
payload.durationSeconds = extra.durationSeconds;
|
|
35
|
+
}
|
|
36
|
+
if (extra?.error !== undefined) {
|
|
37
|
+
payload.error = extra.error;
|
|
38
|
+
}
|
|
39
|
+
const line = `SEQUANT_PROGRESS:${JSON.stringify(payload)}\n`;
|
|
40
|
+
process.stderr.write(line);
|
|
41
|
+
}
|
|
18
42
|
export async function getIssueInfo(issueNumber) {
|
|
19
43
|
try {
|
|
20
44
|
const result = spawnSync("gh", ["issue", "view", String(issueNumber), "--json", "title,labels"], { stdio: "pipe" });
|
|
@@ -95,8 +119,8 @@ export function sortByDependencies(issueNumbers) {
|
|
|
95
119
|
}
|
|
96
120
|
}
|
|
97
121
|
// Note: inDegree counts how many issues depend on each issue
|
|
98
|
-
// We want to process issues that
|
|
99
|
-
//
|
|
122
|
+
// We want to process issues that have no dependencies first,
|
|
123
|
+
// so dependent issues come after their prerequisites
|
|
100
124
|
const sorted = [];
|
|
101
125
|
const queue = [];
|
|
102
126
|
// Start with issues that have no dependencies
|
|
@@ -163,7 +187,8 @@ export function getEnvConfig() {
|
|
|
163
187
|
}
|
|
164
188
|
return config;
|
|
165
189
|
}
|
|
166
|
-
export async function executeBatch(issueNumbers,
|
|
190
|
+
export async function executeBatch(issueNumbers, batchCtx) {
|
|
191
|
+
const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, } = batchCtx;
|
|
167
192
|
const results = [];
|
|
168
193
|
for (const issueNumber of issueNumbers) {
|
|
169
194
|
// Check if shutdown was triggered
|
|
@@ -179,8 +204,21 @@ export async function executeBatch(issueNumbers, config, logWriter, stateManager
|
|
|
179
204
|
if (logWriter) {
|
|
180
205
|
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
181
206
|
}
|
|
182
|
-
const
|
|
183
|
-
|
|
207
|
+
const ctx = {
|
|
208
|
+
issueNumber,
|
|
209
|
+
title: issueInfo.title,
|
|
210
|
+
labels: issueInfo.labels,
|
|
211
|
+
config,
|
|
212
|
+
options,
|
|
213
|
+
services: { logWriter, stateManager, shutdownManager },
|
|
214
|
+
worktree: worktreeInfo
|
|
215
|
+
? { path: worktreeInfo.path, branch: worktreeInfo.branch }
|
|
216
|
+
: undefined,
|
|
217
|
+
packageManager,
|
|
218
|
+
baseBranch,
|
|
219
|
+
onProgress,
|
|
220
|
+
};
|
|
221
|
+
const result = await runIssueWithLogging(ctx);
|
|
184
222
|
results.push(result);
|
|
185
223
|
// Record PR info in log before completing issue
|
|
186
224
|
if (logWriter && result.prNumber && result.prUrl) {
|
|
@@ -193,14 +231,23 @@ export async function executeBatch(issueNumbers, config, logWriter, stateManager
|
|
|
193
231
|
}
|
|
194
232
|
return results;
|
|
195
233
|
}
|
|
196
|
-
export async function runIssueWithLogging(
|
|
234
|
+
export async function runIssueWithLogging(ctx) {
|
|
235
|
+
// Destructure context for use throughout the function
|
|
236
|
+
const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, } = ctx;
|
|
237
|
+
const worktreePath = worktree?.path;
|
|
238
|
+
const branch = worktree?.branch;
|
|
239
|
+
const chainMode = chain?.enabled;
|
|
240
|
+
const isLastInChain = chain?.isLast;
|
|
197
241
|
const startTime = Date.now();
|
|
198
242
|
const phaseResults = [];
|
|
199
243
|
let loopTriggered = false;
|
|
200
244
|
let sessionId;
|
|
201
|
-
|
|
245
|
+
// In parallel mode, suppress per-issue terminal output to prevent interleaving.
|
|
246
|
+
// The caller (run.ts) handles progress display via updateProgress().
|
|
247
|
+
const log = config.parallel ? () => { } : console.log.bind(console);
|
|
248
|
+
log(chalk.blue(`\n Issue #${issueNumber}`));
|
|
202
249
|
if (worktreePath) {
|
|
203
|
-
|
|
250
|
+
log(chalk.gray(` Worktree: ${worktreePath}`));
|
|
204
251
|
}
|
|
205
252
|
// Initialize state tracking for this issue
|
|
206
253
|
if (stateManager) {
|
|
@@ -224,7 +271,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
224
271
|
catch (error) {
|
|
225
272
|
// State tracking errors shouldn't stop execution
|
|
226
273
|
if (config.verbose) {
|
|
227
|
-
|
|
274
|
+
log(chalk.yellow(` ⚠️ State tracking error: ${error}`));
|
|
228
275
|
}
|
|
229
276
|
}
|
|
230
277
|
}
|
|
@@ -235,23 +282,39 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
235
282
|
if (options.autoDetectPhases) {
|
|
236
283
|
// Check if labels indicate a simple bug/fix (skip spec entirely)
|
|
237
284
|
const lowerLabels = labels.map((l) => l.toLowerCase());
|
|
238
|
-
const isSimpleBugFix = lowerLabels.some((label) => BUG_LABELS.some((bugLabel) => label
|
|
285
|
+
const isSimpleBugFix = lowerLabels.some((label) => BUG_LABELS.some((bugLabel) => label === bugLabel));
|
|
286
|
+
// Check if labels indicate documentation-only work (skip spec)
|
|
287
|
+
const isDocs = lowerLabels.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
|
|
239
288
|
if (isSimpleBugFix) {
|
|
240
289
|
// Simple bug fix: skip spec, go straight to exec → qa
|
|
241
290
|
phases = ["exec", "qa"];
|
|
242
|
-
|
|
291
|
+
log(chalk.gray(` Bug fix detected: ${phases.join(" → ")}`));
|
|
292
|
+
}
|
|
293
|
+
else if (isDocs) {
|
|
294
|
+
// Documentation issue: skip spec, lighter pipeline
|
|
295
|
+
phases = ["exec", "qa"];
|
|
296
|
+
log(chalk.gray(` Docs issue detected: ${phases.join(" → ")}`));
|
|
243
297
|
}
|
|
244
298
|
else {
|
|
245
299
|
// Run spec first to get recommended workflow
|
|
246
|
-
|
|
247
|
-
// Create spinner for spec phase (
|
|
248
|
-
const specSpinner =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
300
|
+
log(chalk.gray(` Running spec to determine workflow...`));
|
|
301
|
+
// Create spinner for spec phase (suppressed in parallel mode to prevent interleaving)
|
|
302
|
+
const specSpinner = config.parallel
|
|
303
|
+
? undefined
|
|
304
|
+
: new PhaseSpinner({
|
|
305
|
+
phase: "spec",
|
|
306
|
+
phaseIndex: 1,
|
|
307
|
+
totalPhases: 3, // Estimate; will be refined after spec
|
|
308
|
+
shutdownManager,
|
|
309
|
+
});
|
|
310
|
+
specSpinner?.start();
|
|
311
|
+
emitProgressLine(issueNumber, "spec", "start");
|
|
312
|
+
try {
|
|
313
|
+
onProgress?.(issueNumber, "spec", "start");
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
/* progress errors must not halt */
|
|
317
|
+
}
|
|
255
318
|
// Track spec phase start in state
|
|
256
319
|
if (stateManager) {
|
|
257
320
|
try {
|
|
@@ -280,14 +343,46 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
280
343
|
}
|
|
281
344
|
phaseResults.push(specResult);
|
|
282
345
|
specAlreadyRan = true;
|
|
346
|
+
// Emit completion/failure progress event (AC-8)
|
|
347
|
+
const specDurationSec = Math.round((specEndTime.getTime() - specStartTime.getTime()) / 1000);
|
|
348
|
+
if (specResult.success) {
|
|
349
|
+
const extra = { durationSeconds: specDurationSec };
|
|
350
|
+
emitProgressLine(issueNumber, "spec", "complete", extra);
|
|
351
|
+
try {
|
|
352
|
+
onProgress?.(issueNumber, "spec", "complete", extra);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
/* progress errors must not halt */
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
const extra = { error: specResult.error ?? "unknown" };
|
|
360
|
+
emitProgressLine(issueNumber, "spec", "failed", extra);
|
|
361
|
+
try {
|
|
362
|
+
onProgress?.(issueNumber, "spec", "failed", extra);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
/* progress errors must not halt */
|
|
366
|
+
}
|
|
367
|
+
}
|
|
283
368
|
// Log spec phase result
|
|
284
369
|
// Note: Spec runs in main repo, not worktree, so no git diff stats
|
|
285
370
|
if (logWriter) {
|
|
371
|
+
// Build errorContext from captured stderr/stdout tails (#447)
|
|
372
|
+
let specErrorContext;
|
|
373
|
+
if (!specResult.success && specResult.stderrTail) {
|
|
374
|
+
specErrorContext = {
|
|
375
|
+
stderrTail: specResult.stderrTail ?? [],
|
|
376
|
+
stdoutTail: specResult.stdoutTail ?? [],
|
|
377
|
+
exitCode: specResult.exitCode,
|
|
378
|
+
category: classifyError(specResult.stderrTail ?? []),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
286
381
|
const phaseLog = createPhaseLogFromTiming("spec", issueNumber, specStartTime, specEndTime, specResult.success
|
|
287
382
|
? "success"
|
|
288
383
|
: specResult.error?.includes("Timeout")
|
|
289
384
|
? "timeout"
|
|
290
|
-
: "failure", { error: specResult.error });
|
|
385
|
+
: "failure", { error: specResult.error, errorContext: specErrorContext });
|
|
291
386
|
logWriter.logPhase(phaseLog);
|
|
292
387
|
}
|
|
293
388
|
// Track spec phase completion in state
|
|
@@ -303,7 +398,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
303
398
|
}
|
|
304
399
|
}
|
|
305
400
|
if (!specResult.success) {
|
|
306
|
-
specSpinner
|
|
401
|
+
specSpinner?.fail(specResult.error);
|
|
307
402
|
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
308
403
|
return {
|
|
309
404
|
issueNumber,
|
|
@@ -313,7 +408,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
313
408
|
loopTriggered: false,
|
|
314
409
|
};
|
|
315
410
|
}
|
|
316
|
-
specSpinner
|
|
411
|
+
specSpinner?.succeed();
|
|
317
412
|
// Parse recommended workflow from spec output
|
|
318
413
|
const parsedWorkflow = specResult.output
|
|
319
414
|
? parseRecommendedWorkflow(specResult.output)
|
|
@@ -322,15 +417,15 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
322
417
|
// Remove spec from phases since we already ran it
|
|
323
418
|
phases = parsedWorkflow.phases.filter((p) => p !== "spec");
|
|
324
419
|
detectedQualityLoop = parsedWorkflow.qualityLoop;
|
|
325
|
-
|
|
420
|
+
log(chalk.gray(` Spec recommends: ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
|
|
326
421
|
}
|
|
327
422
|
else {
|
|
328
423
|
// Fall back to label-based detection
|
|
329
|
-
|
|
424
|
+
log(chalk.yellow(` Could not parse spec recommendation, using label-based detection`));
|
|
330
425
|
const detected = detectPhasesFromLabels(labels);
|
|
331
426
|
phases = detected.phases.filter((p) => p !== "spec");
|
|
332
427
|
detectedQualityLoop = detected.qualityLoop;
|
|
333
|
-
|
|
428
|
+
log(chalk.gray(` Fallback: ${phases.join(" → ")}`));
|
|
334
429
|
}
|
|
335
430
|
}
|
|
336
431
|
}
|
|
@@ -338,21 +433,21 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
338
433
|
// Use explicit phases with adjustments
|
|
339
434
|
phases = determinePhasesForIssue(config.phases, labels, options);
|
|
340
435
|
if (phases.length !== config.phases.length) {
|
|
341
|
-
|
|
436
|
+
log(chalk.gray(` Phases adjusted: ${phases.join(" → ")}`));
|
|
342
437
|
}
|
|
343
438
|
}
|
|
344
439
|
// Resume: filter out completed phases if --resume flag is set
|
|
345
440
|
if (options.resume) {
|
|
346
441
|
const resumeResult = filterResumedPhases(issueNumber, phases, true);
|
|
347
442
|
if (resumeResult.skipped.length > 0) {
|
|
348
|
-
|
|
443
|
+
log(chalk.gray(` Resume: skipping completed phases: ${resumeResult.skipped.join(", ")}`));
|
|
349
444
|
phases = resumeResult.phases;
|
|
350
445
|
}
|
|
351
446
|
// Also skip spec if it was auto-detected as completed
|
|
352
447
|
if (specAlreadyRan &&
|
|
353
448
|
resumeResult.skipped.length === 0 &&
|
|
354
449
|
resumeResult.phases.length === 0) {
|
|
355
|
-
|
|
450
|
+
log(chalk.gray(` Resume: all phases already completed`));
|
|
356
451
|
}
|
|
357
452
|
}
|
|
358
453
|
// Add testgen phase if requested (and spec was in the phases)
|
|
@@ -370,6 +465,12 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
370
465
|
}
|
|
371
466
|
}
|
|
372
467
|
}
|
|
468
|
+
// Build per-issue config with issue type metadata for skill env propagation
|
|
469
|
+
const lowerLabelsForType = labels.map((l) => l.toLowerCase());
|
|
470
|
+
const issueIsDocs = lowerLabelsForType.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
|
|
471
|
+
const issueConfig = issueIsDocs
|
|
472
|
+
? { ...config, issueType: "docs" }
|
|
473
|
+
: config;
|
|
373
474
|
let iteration = 0;
|
|
374
475
|
const useQualityLoop = config.qualityLoop || detectedQualityLoop;
|
|
375
476
|
const maxIterations = useQualityLoop ? config.maxIterations : 1;
|
|
@@ -377,7 +478,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
377
478
|
while (iteration < maxIterations) {
|
|
378
479
|
iteration++;
|
|
379
480
|
if (useQualityLoop && iteration > 1) {
|
|
380
|
-
|
|
481
|
+
log(chalk.yellow(` Quality loop iteration ${iteration}/${maxIterations}`));
|
|
381
482
|
loopTriggered = true;
|
|
382
483
|
}
|
|
383
484
|
let phasesFailed = false;
|
|
@@ -388,15 +489,24 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
388
489
|
for (let phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {
|
|
389
490
|
const phase = phases[phaseIdx];
|
|
390
491
|
const phaseNumber = phaseIdx + 1 + phaseIndexOffset;
|
|
391
|
-
// Create spinner for this phase
|
|
392
|
-
const phaseSpinner =
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
492
|
+
// Create spinner for this phase (suppressed in parallel mode)
|
|
493
|
+
const phaseSpinner = config.parallel
|
|
494
|
+
? undefined
|
|
495
|
+
: new PhaseSpinner({
|
|
496
|
+
phase,
|
|
497
|
+
phaseIndex: phaseNumber,
|
|
498
|
+
totalPhases,
|
|
499
|
+
shutdownManager,
|
|
500
|
+
iteration: useQualityLoop ? iteration : undefined,
|
|
501
|
+
});
|
|
502
|
+
phaseSpinner?.start();
|
|
503
|
+
emitProgressLine(issueNumber, phase, "start");
|
|
504
|
+
try {
|
|
505
|
+
onProgress?.(issueNumber, phase, "start");
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
/* progress errors must not halt */
|
|
509
|
+
}
|
|
400
510
|
// Track phase start in state
|
|
401
511
|
if (stateManager) {
|
|
402
512
|
try {
|
|
@@ -407,7 +517,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
407
517
|
}
|
|
408
518
|
}
|
|
409
519
|
const phaseStartTime = new Date();
|
|
410
|
-
const result = await executePhaseWithRetry(issueNumber, phase,
|
|
520
|
+
const result = await executePhaseWithRetry(issueNumber, phase, issueConfig, sessionId, worktreePath, shutdownManager, phaseSpinner);
|
|
411
521
|
const phaseEndTime = new Date();
|
|
412
522
|
// Capture session ID for subsequent phases
|
|
413
523
|
if (result.sessionId) {
|
|
@@ -423,6 +533,28 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
423
533
|
}
|
|
424
534
|
}
|
|
425
535
|
phaseResults.push(result);
|
|
536
|
+
// Emit completion/failure progress event (AC-8)
|
|
537
|
+
const phaseDurationSec = Math.round((phaseEndTime.getTime() - phaseStartTime.getTime()) / 1000);
|
|
538
|
+
if (result.success) {
|
|
539
|
+
const extra = { durationSeconds: phaseDurationSec };
|
|
540
|
+
emitProgressLine(issueNumber, phase, "complete", extra);
|
|
541
|
+
try {
|
|
542
|
+
onProgress?.(issueNumber, phase, "complete", extra);
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
/* progress errors must not halt */
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
const extra = { error: result.error ?? "unknown" };
|
|
550
|
+
emitProgressLine(issueNumber, phase, "failed", extra);
|
|
551
|
+
try {
|
|
552
|
+
onProgress?.(issueNumber, phase, "failed", extra);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
/* progress errors must not halt */
|
|
556
|
+
}
|
|
557
|
+
}
|
|
426
558
|
// Log phase result with observability data (AC-1, AC-2, AC-3, AC-7)
|
|
427
559
|
if (logWriter) {
|
|
428
560
|
// Capture git diff stats for worktree phases (AC-1, AC-3)
|
|
@@ -435,6 +567,16 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
435
567
|
: undefined;
|
|
436
568
|
// Read cache metrics for QA phase (AC-7)
|
|
437
569
|
const cacheMetrics = phase === "qa" ? readCacheMetrics(worktreePath) : undefined;
|
|
570
|
+
// Build errorContext from captured stderr/stdout tails (#447)
|
|
571
|
+
let errorContext;
|
|
572
|
+
if (!result.success && result.stderrTail) {
|
|
573
|
+
errorContext = {
|
|
574
|
+
stderrTail: result.stderrTail ?? [],
|
|
575
|
+
stdoutTail: result.stdoutTail ?? [],
|
|
576
|
+
exitCode: result.exitCode,
|
|
577
|
+
category: classifyError(result.stderrTail ?? []),
|
|
578
|
+
};
|
|
579
|
+
}
|
|
438
580
|
const phaseLog = createPhaseLogFromTiming(phase, issueNumber, phaseStartTime, phaseEndTime, result.success
|
|
439
581
|
? "success"
|
|
440
582
|
: result.error?.includes("Timeout")
|
|
@@ -442,11 +584,13 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
442
584
|
: "failure", {
|
|
443
585
|
error: result.error,
|
|
444
586
|
verdict: result.verdict,
|
|
587
|
+
summary: result.summary,
|
|
445
588
|
// Observability fields (AC-1, AC-2, AC-3, AC-7)
|
|
446
589
|
filesModified: diffStats?.filesModified,
|
|
447
590
|
fileDiffStats: diffStats?.fileDiffStats,
|
|
448
591
|
commitHash,
|
|
449
592
|
cacheMetrics,
|
|
593
|
+
errorContext,
|
|
450
594
|
});
|
|
451
595
|
logWriter.logPhase(phaseLog);
|
|
452
596
|
}
|
|
@@ -465,34 +609,67 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
465
609
|
}
|
|
466
610
|
}
|
|
467
611
|
if (result.success) {
|
|
468
|
-
phaseSpinner
|
|
612
|
+
phaseSpinner?.succeed();
|
|
469
613
|
}
|
|
470
614
|
else {
|
|
471
|
-
phaseSpinner
|
|
615
|
+
phaseSpinner?.fail(result.error);
|
|
472
616
|
phasesFailed = true;
|
|
473
617
|
// If quality loop enabled, run loop phase to fix issues
|
|
474
618
|
if (useQualityLoop && iteration < maxIterations) {
|
|
475
|
-
// Create spinner for loop phase
|
|
476
|
-
const loopSpinner =
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
619
|
+
// Create spinner for loop phase (suppressed in parallel mode)
|
|
620
|
+
const loopSpinner = config.parallel
|
|
621
|
+
? undefined
|
|
622
|
+
: new PhaseSpinner({
|
|
623
|
+
phase: "loop",
|
|
624
|
+
phaseIndex: phaseNumber,
|
|
625
|
+
totalPhases,
|
|
626
|
+
shutdownManager,
|
|
627
|
+
iteration,
|
|
628
|
+
});
|
|
629
|
+
loopSpinner?.start();
|
|
630
|
+
emitProgressLine(issueNumber, "loop", "start");
|
|
631
|
+
try {
|
|
632
|
+
onProgress?.(issueNumber, "loop", "start");
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
/* progress errors must not halt */
|
|
636
|
+
}
|
|
637
|
+
const loopStartTime = new Date();
|
|
638
|
+
const loopResult = await executePhaseWithRetry(issueNumber, "loop", issueConfig, sessionId, worktreePath, shutdownManager, loopSpinner);
|
|
639
|
+
const loopEndTime = new Date();
|
|
485
640
|
phaseResults.push(loopResult);
|
|
641
|
+
// Emit loop completion/failure progress event (AC-8)
|
|
642
|
+
const loopDurationSec = Math.round((loopEndTime.getTime() - loopStartTime.getTime()) / 1000);
|
|
643
|
+
if (loopResult.success) {
|
|
644
|
+
const extra = { durationSeconds: loopDurationSec };
|
|
645
|
+
emitProgressLine(issueNumber, "loop", "complete", extra);
|
|
646
|
+
try {
|
|
647
|
+
onProgress?.(issueNumber, "loop", "complete", extra);
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
/* progress errors must not halt */
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
const extra = { error: loopResult.error ?? "unknown" };
|
|
655
|
+
emitProgressLine(issueNumber, "loop", "failed", extra);
|
|
656
|
+
try {
|
|
657
|
+
onProgress?.(issueNumber, "loop", "failed", extra);
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
/* progress errors must not halt */
|
|
661
|
+
}
|
|
662
|
+
}
|
|
486
663
|
if (loopResult.sessionId) {
|
|
487
664
|
sessionId = loopResult.sessionId;
|
|
488
665
|
}
|
|
489
666
|
if (loopResult.success) {
|
|
490
|
-
loopSpinner
|
|
667
|
+
loopSpinner?.succeed();
|
|
491
668
|
// Continue to next iteration
|
|
492
669
|
break;
|
|
493
670
|
}
|
|
494
671
|
else {
|
|
495
|
-
loopSpinner
|
|
672
|
+
loopSpinner?.fail(loopResult.error);
|
|
496
673
|
}
|
|
497
674
|
}
|
|
498
675
|
// Stop on first failure (if not in quality loop or loop failed)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentDriver interface — decouples workflow orchestration from agent execution.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code is the default implementation; alternatives (Aider, Codex CLI,
|
|
5
|
+
* Continue.dev, Copilot SDK, Cursor API) can be added by implementing this
|
|
6
|
+
* interface without touching orchestration logic.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Configuration passed to an agent for phase execution.
|
|
10
|
+
*/
|
|
11
|
+
export interface AgentExecutionConfig {
|
|
12
|
+
cwd: string;
|
|
13
|
+
env: Record<string, string>;
|
|
14
|
+
abortSignal?: AbortSignal;
|
|
15
|
+
phaseTimeout: number;
|
|
16
|
+
verbose: boolean;
|
|
17
|
+
mcp: boolean;
|
|
18
|
+
/** Resume a previous session (driver-specific; ignored if unsupported) */
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
/** Callback for streaming output */
|
|
21
|
+
onOutput?: (text: string) => void;
|
|
22
|
+
/** Callback for stderr */
|
|
23
|
+
onStderr?: (text: string) => void;
|
|
24
|
+
/** Relevant files for the phase (used by file-oriented drivers like Aider) */
|
|
25
|
+
files?: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Result returned by an agent after executing a phase.
|
|
29
|
+
*/
|
|
30
|
+
export interface AgentPhaseResult {
|
|
31
|
+
success: boolean;
|
|
32
|
+
output: string;
|
|
33
|
+
sessionId?: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
/** Last N lines of stderr captured via RingBuffer (#447) */
|
|
36
|
+
stderrTail?: string[];
|
|
37
|
+
/** Last N lines of stdout captured via RingBuffer (#447) */
|
|
38
|
+
stdoutTail?: string[];
|
|
39
|
+
/** Process exit code (undefined for SDK-based drivers) (#447) */
|
|
40
|
+
exitCode?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Interface that all agent backends must implement.
|
|
44
|
+
*
|
|
45
|
+
* The driver is responsible for executing a prompt and returning
|
|
46
|
+
* a structured result. Parsing (QA verdicts, etc.) stays in
|
|
47
|
+
* phase-executor.ts — the driver just captures text.
|
|
48
|
+
*/
|
|
49
|
+
export interface AgentDriver {
|
|
50
|
+
/** Human-readable name for logging */
|
|
51
|
+
name: string;
|
|
52
|
+
/** Execute a phase prompt and return structured result */
|
|
53
|
+
executePhase(prompt: string, config: AgentExecutionConfig): Promise<AgentPhaseResult>;
|
|
54
|
+
/** Check if this driver is available/configured */
|
|
55
|
+
isAvailable(): Promise<boolean>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentDriver interface — decouples workflow orchestration from agent execution.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code is the default implementation; alternatives (Aider, Codex CLI,
|
|
5
|
+
* Continue.dev, Copilot SDK, Cursor API) can be added by implementing this
|
|
6
|
+
* interface without touching orchestration logic.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AiderDriver — AgentDriver implementation wrapping the Aider CLI.
|
|
3
|
+
*
|
|
4
|
+
* Shells out to `aider --yes --no-auto-commits --message "<prompt>"`
|
|
5
|
+
* for fully non-interactive phase execution. Sequant manages git,
|
|
6
|
+
* not Aider.
|
|
7
|
+
*/
|
|
8
|
+
import type { AgentDriver, AgentExecutionConfig, AgentPhaseResult } from "./agent-driver.js";
|
|
9
|
+
import type { AiderSettings } from "../../settings.js";
|
|
10
|
+
export declare class AiderDriver implements AgentDriver {
|
|
11
|
+
name: string;
|
|
12
|
+
private settings?;
|
|
13
|
+
constructor(settings?: AiderSettings);
|
|
14
|
+
executePhase(prompt: string, config: AgentExecutionConfig): Promise<AgentPhaseResult>;
|
|
15
|
+
isAvailable(): Promise<boolean>;
|
|
16
|
+
/** Build the CLI argument list for aider. */
|
|
17
|
+
private buildArgs;
|
|
18
|
+
}
|