sequant 1.20.3 → 2.0.1
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 +36 -15
- 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 +26 -86
- package/dist/src/lib/workflow/batch-executor.js +269 -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 +375 -229
- 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 +224 -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 +26 -7
- 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,63 @@
|
|
|
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
|
+
/**
|
|
30
|
+
* Build enriched prompt context for the /loop phase from a failed phase result (#488).
|
|
31
|
+
* Passes QA verdict, failed ACs, and error directly so the /loop skill doesn't need
|
|
32
|
+
* to reconstruct context from GitHub comments (which fails in subprocess).
|
|
33
|
+
*
|
|
34
|
+
* @internal Exported for testing only
|
|
35
|
+
*/
|
|
36
|
+
export function buildLoopContext(failedResult) {
|
|
37
|
+
const parts = [`Previous phase "${failedResult.phase}" failed.`];
|
|
38
|
+
if (failedResult.verdict) {
|
|
39
|
+
parts.push(`QA Verdict: ${failedResult.verdict}`);
|
|
40
|
+
}
|
|
41
|
+
if (failedResult.summary?.gaps?.length) {
|
|
42
|
+
parts.push(`QA Gaps:\n${failedResult.summary.gaps.map((gap) => `- ${gap}`).join("\n")}`);
|
|
43
|
+
}
|
|
44
|
+
if (failedResult.summary?.suggestions?.length) {
|
|
45
|
+
parts.push(`Suggestions:\n${failedResult.summary.suggestions.map((s) => `- ${s}`).join("\n")}`);
|
|
46
|
+
}
|
|
47
|
+
if (failedResult.error) {
|
|
48
|
+
parts.push(`Error: ${failedResult.error}`);
|
|
49
|
+
}
|
|
50
|
+
// Include tail of output for additional context (truncated to avoid prompt bloat)
|
|
51
|
+
if (failedResult.output) {
|
|
52
|
+
const tail = failedResult.output.slice(-2000);
|
|
53
|
+
parts.push(`Last output:\n${tail}`);
|
|
54
|
+
}
|
|
55
|
+
return parts.join("\n\n");
|
|
56
|
+
}
|
|
57
|
+
export function emitProgressLine(issue, phase, event = "start", extra) {
|
|
58
|
+
if (!process.env.SEQUANT_ORCHESTRATOR)
|
|
59
|
+
return;
|
|
60
|
+
const payload = { issue, phase, event };
|
|
61
|
+
if (extra?.durationSeconds !== undefined) {
|
|
62
|
+
payload.durationSeconds = extra.durationSeconds;
|
|
63
|
+
}
|
|
64
|
+
if (extra?.error !== undefined) {
|
|
65
|
+
payload.error = extra.error;
|
|
66
|
+
}
|
|
67
|
+
const line = `SEQUANT_PROGRESS:${JSON.stringify(payload)}\n`;
|
|
68
|
+
process.stderr.write(line);
|
|
69
|
+
}
|
|
18
70
|
export async function getIssueInfo(issueNumber) {
|
|
19
71
|
try {
|
|
20
72
|
const result = spawnSync("gh", ["issue", "view", String(issueNumber), "--json", "title,labels"], { stdio: "pipe" });
|
|
@@ -95,8 +147,8 @@ export function sortByDependencies(issueNumbers) {
|
|
|
95
147
|
}
|
|
96
148
|
}
|
|
97
149
|
// Note: inDegree counts how many issues depend on each issue
|
|
98
|
-
// We want to process issues that
|
|
99
|
-
//
|
|
150
|
+
// We want to process issues that have no dependencies first,
|
|
151
|
+
// so dependent issues come after their prerequisites
|
|
100
152
|
const sorted = [];
|
|
101
153
|
const queue = [];
|
|
102
154
|
// Start with issues that have no dependencies
|
|
@@ -163,7 +215,8 @@ export function getEnvConfig() {
|
|
|
163
215
|
}
|
|
164
216
|
return config;
|
|
165
217
|
}
|
|
166
|
-
export async function executeBatch(issueNumbers,
|
|
218
|
+
export async function executeBatch(issueNumbers, batchCtx) {
|
|
219
|
+
const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, } = batchCtx;
|
|
167
220
|
const results = [];
|
|
168
221
|
for (const issueNumber of issueNumbers) {
|
|
169
222
|
// Check if shutdown was triggered
|
|
@@ -179,8 +232,21 @@ export async function executeBatch(issueNumbers, config, logWriter, stateManager
|
|
|
179
232
|
if (logWriter) {
|
|
180
233
|
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
181
234
|
}
|
|
182
|
-
const
|
|
183
|
-
|
|
235
|
+
const ctx = {
|
|
236
|
+
issueNumber,
|
|
237
|
+
title: issueInfo.title,
|
|
238
|
+
labels: issueInfo.labels,
|
|
239
|
+
config,
|
|
240
|
+
options,
|
|
241
|
+
services: { logWriter, stateManager, shutdownManager },
|
|
242
|
+
worktree: worktreeInfo
|
|
243
|
+
? { path: worktreeInfo.path, branch: worktreeInfo.branch }
|
|
244
|
+
: undefined,
|
|
245
|
+
packageManager,
|
|
246
|
+
baseBranch,
|
|
247
|
+
onProgress,
|
|
248
|
+
};
|
|
249
|
+
const result = await runIssueWithLogging(ctx);
|
|
184
250
|
results.push(result);
|
|
185
251
|
// Record PR info in log before completing issue
|
|
186
252
|
if (logWriter && result.prNumber && result.prUrl) {
|
|
@@ -193,14 +259,23 @@ export async function executeBatch(issueNumbers, config, logWriter, stateManager
|
|
|
193
259
|
}
|
|
194
260
|
return results;
|
|
195
261
|
}
|
|
196
|
-
export async function runIssueWithLogging(
|
|
262
|
+
export async function runIssueWithLogging(ctx) {
|
|
263
|
+
// Destructure context for use throughout the function
|
|
264
|
+
const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, } = ctx;
|
|
265
|
+
const worktreePath = worktree?.path;
|
|
266
|
+
const branch = worktree?.branch;
|
|
267
|
+
const chainMode = chain?.enabled;
|
|
268
|
+
const isLastInChain = chain?.isLast;
|
|
197
269
|
const startTime = Date.now();
|
|
198
270
|
const phaseResults = [];
|
|
199
271
|
let loopTriggered = false;
|
|
200
272
|
let sessionId;
|
|
201
|
-
|
|
273
|
+
// In parallel mode, suppress per-issue terminal output to prevent interleaving.
|
|
274
|
+
// The caller (run.ts) handles progress display via updateProgress().
|
|
275
|
+
const log = config.parallel ? () => { } : console.log.bind(console);
|
|
276
|
+
log(chalk.blue(`\n Issue #${issueNumber}`));
|
|
202
277
|
if (worktreePath) {
|
|
203
|
-
|
|
278
|
+
log(chalk.gray(` Worktree: ${worktreePath}`));
|
|
204
279
|
}
|
|
205
280
|
// Initialize state tracking for this issue
|
|
206
281
|
if (stateManager) {
|
|
@@ -224,7 +299,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
224
299
|
catch (error) {
|
|
225
300
|
// State tracking errors shouldn't stop execution
|
|
226
301
|
if (config.verbose) {
|
|
227
|
-
|
|
302
|
+
log(chalk.yellow(` ⚠️ State tracking error: ${error}`));
|
|
228
303
|
}
|
|
229
304
|
}
|
|
230
305
|
}
|
|
@@ -235,23 +310,39 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
235
310
|
if (options.autoDetectPhases) {
|
|
236
311
|
// Check if labels indicate a simple bug/fix (skip spec entirely)
|
|
237
312
|
const lowerLabels = labels.map((l) => l.toLowerCase());
|
|
238
|
-
const isSimpleBugFix = lowerLabels.some((label) => BUG_LABELS.some((bugLabel) => label
|
|
313
|
+
const isSimpleBugFix = lowerLabels.some((label) => BUG_LABELS.some((bugLabel) => label === bugLabel));
|
|
314
|
+
// Check if labels indicate documentation-only work (skip spec)
|
|
315
|
+
const isDocs = lowerLabels.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
|
|
239
316
|
if (isSimpleBugFix) {
|
|
240
317
|
// Simple bug fix: skip spec, go straight to exec → qa
|
|
241
318
|
phases = ["exec", "qa"];
|
|
242
|
-
|
|
319
|
+
log(chalk.gray(` Bug fix detected: ${phases.join(" → ")}`));
|
|
320
|
+
}
|
|
321
|
+
else if (isDocs) {
|
|
322
|
+
// Documentation issue: skip spec, lighter pipeline
|
|
323
|
+
phases = ["exec", "qa"];
|
|
324
|
+
log(chalk.gray(` Docs issue detected: ${phases.join(" → ")}`));
|
|
243
325
|
}
|
|
244
326
|
else {
|
|
245
327
|
// Run spec first to get recommended workflow
|
|
246
|
-
|
|
247
|
-
// Create spinner for spec phase (
|
|
248
|
-
const specSpinner =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
328
|
+
log(chalk.gray(` Running spec to determine workflow...`));
|
|
329
|
+
// Create spinner for spec phase (suppressed in parallel mode to prevent interleaving)
|
|
330
|
+
const specSpinner = config.parallel
|
|
331
|
+
? undefined
|
|
332
|
+
: new PhaseSpinner({
|
|
333
|
+
phase: "spec",
|
|
334
|
+
phaseIndex: 1,
|
|
335
|
+
totalPhases: 3, // Estimate; will be refined after spec
|
|
336
|
+
shutdownManager,
|
|
337
|
+
});
|
|
338
|
+
specSpinner?.start();
|
|
339
|
+
emitProgressLine(issueNumber, "spec", "start");
|
|
340
|
+
try {
|
|
341
|
+
onProgress?.(issueNumber, "spec", "start");
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
/* progress errors must not halt */
|
|
345
|
+
}
|
|
255
346
|
// Track spec phase start in state
|
|
256
347
|
if (stateManager) {
|
|
257
348
|
try {
|
|
@@ -280,14 +371,46 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
280
371
|
}
|
|
281
372
|
phaseResults.push(specResult);
|
|
282
373
|
specAlreadyRan = true;
|
|
374
|
+
// Emit completion/failure progress event (AC-8)
|
|
375
|
+
const specDurationSec = Math.round((specEndTime.getTime() - specStartTime.getTime()) / 1000);
|
|
376
|
+
if (specResult.success) {
|
|
377
|
+
const extra = { durationSeconds: specDurationSec };
|
|
378
|
+
emitProgressLine(issueNumber, "spec", "complete", extra);
|
|
379
|
+
try {
|
|
380
|
+
onProgress?.(issueNumber, "spec", "complete", extra);
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
/* progress errors must not halt */
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
const extra = { error: specResult.error ?? "unknown" };
|
|
388
|
+
emitProgressLine(issueNumber, "spec", "failed", extra);
|
|
389
|
+
try {
|
|
390
|
+
onProgress?.(issueNumber, "spec", "failed", extra);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
/* progress errors must not halt */
|
|
394
|
+
}
|
|
395
|
+
}
|
|
283
396
|
// Log spec phase result
|
|
284
397
|
// Note: Spec runs in main repo, not worktree, so no git diff stats
|
|
285
398
|
if (logWriter) {
|
|
399
|
+
// Build errorContext from captured stderr/stdout tails (#447)
|
|
400
|
+
let specErrorContext;
|
|
401
|
+
if (!specResult.success && specResult.stderrTail) {
|
|
402
|
+
specErrorContext = {
|
|
403
|
+
stderrTail: specResult.stderrTail ?? [],
|
|
404
|
+
stdoutTail: specResult.stdoutTail ?? [],
|
|
405
|
+
exitCode: specResult.exitCode,
|
|
406
|
+
category: classifyError(specResult.stderrTail ?? []),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
286
409
|
const phaseLog = createPhaseLogFromTiming("spec", issueNumber, specStartTime, specEndTime, specResult.success
|
|
287
410
|
? "success"
|
|
288
411
|
: specResult.error?.includes("Timeout")
|
|
289
412
|
? "timeout"
|
|
290
|
-
: "failure", { error: specResult.error });
|
|
413
|
+
: "failure", { error: specResult.error, errorContext: specErrorContext });
|
|
291
414
|
logWriter.logPhase(phaseLog);
|
|
292
415
|
}
|
|
293
416
|
// Track spec phase completion in state
|
|
@@ -303,7 +426,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
303
426
|
}
|
|
304
427
|
}
|
|
305
428
|
if (!specResult.success) {
|
|
306
|
-
specSpinner
|
|
429
|
+
specSpinner?.fail(specResult.error);
|
|
307
430
|
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
308
431
|
return {
|
|
309
432
|
issueNumber,
|
|
@@ -313,7 +436,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
313
436
|
loopTriggered: false,
|
|
314
437
|
};
|
|
315
438
|
}
|
|
316
|
-
specSpinner
|
|
439
|
+
specSpinner?.succeed();
|
|
317
440
|
// Parse recommended workflow from spec output
|
|
318
441
|
const parsedWorkflow = specResult.output
|
|
319
442
|
? parseRecommendedWorkflow(specResult.output)
|
|
@@ -322,15 +445,15 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
322
445
|
// Remove spec from phases since we already ran it
|
|
323
446
|
phases = parsedWorkflow.phases.filter((p) => p !== "spec");
|
|
324
447
|
detectedQualityLoop = parsedWorkflow.qualityLoop;
|
|
325
|
-
|
|
448
|
+
log(chalk.gray(` Spec recommends: ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
|
|
326
449
|
}
|
|
327
450
|
else {
|
|
328
451
|
// Fall back to label-based detection
|
|
329
|
-
|
|
452
|
+
log(chalk.yellow(` Could not parse spec recommendation, using label-based detection`));
|
|
330
453
|
const detected = detectPhasesFromLabels(labels);
|
|
331
454
|
phases = detected.phases.filter((p) => p !== "spec");
|
|
332
455
|
detectedQualityLoop = detected.qualityLoop;
|
|
333
|
-
|
|
456
|
+
log(chalk.gray(` Fallback: ${phases.join(" → ")}`));
|
|
334
457
|
}
|
|
335
458
|
}
|
|
336
459
|
}
|
|
@@ -338,21 +461,21 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
338
461
|
// Use explicit phases with adjustments
|
|
339
462
|
phases = determinePhasesForIssue(config.phases, labels, options);
|
|
340
463
|
if (phases.length !== config.phases.length) {
|
|
341
|
-
|
|
464
|
+
log(chalk.gray(` Phases adjusted: ${phases.join(" → ")}`));
|
|
342
465
|
}
|
|
343
466
|
}
|
|
344
467
|
// Resume: filter out completed phases if --resume flag is set
|
|
345
468
|
if (options.resume) {
|
|
346
469
|
const resumeResult = filterResumedPhases(issueNumber, phases, true);
|
|
347
470
|
if (resumeResult.skipped.length > 0) {
|
|
348
|
-
|
|
471
|
+
log(chalk.gray(` Resume: skipping completed phases: ${resumeResult.skipped.join(", ")}`));
|
|
349
472
|
phases = resumeResult.phases;
|
|
350
473
|
}
|
|
351
474
|
// Also skip spec if it was auto-detected as completed
|
|
352
475
|
if (specAlreadyRan &&
|
|
353
476
|
resumeResult.skipped.length === 0 &&
|
|
354
477
|
resumeResult.phases.length === 0) {
|
|
355
|
-
|
|
478
|
+
log(chalk.gray(` Resume: all phases already completed`));
|
|
356
479
|
}
|
|
357
480
|
}
|
|
358
481
|
// Add testgen phase if requested (and spec was in the phases)
|
|
@@ -370,6 +493,12 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
370
493
|
}
|
|
371
494
|
}
|
|
372
495
|
}
|
|
496
|
+
// Build per-issue config with issue type metadata for skill env propagation
|
|
497
|
+
const lowerLabelsForType = labels.map((l) => l.toLowerCase());
|
|
498
|
+
const issueIsDocs = lowerLabelsForType.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
|
|
499
|
+
const issueConfig = issueIsDocs
|
|
500
|
+
? { ...config, issueType: "docs" }
|
|
501
|
+
: config;
|
|
373
502
|
let iteration = 0;
|
|
374
503
|
const useQualityLoop = config.qualityLoop || detectedQualityLoop;
|
|
375
504
|
const maxIterations = useQualityLoop ? config.maxIterations : 1;
|
|
@@ -377,7 +506,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
377
506
|
while (iteration < maxIterations) {
|
|
378
507
|
iteration++;
|
|
379
508
|
if (useQualityLoop && iteration > 1) {
|
|
380
|
-
|
|
509
|
+
log(chalk.yellow(` Quality loop iteration ${iteration}/${maxIterations}`));
|
|
381
510
|
loopTriggered = true;
|
|
382
511
|
}
|
|
383
512
|
let phasesFailed = false;
|
|
@@ -388,15 +517,24 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
388
517
|
for (let phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {
|
|
389
518
|
const phase = phases[phaseIdx];
|
|
390
519
|
const phaseNumber = phaseIdx + 1 + phaseIndexOffset;
|
|
391
|
-
// Create spinner for this phase
|
|
392
|
-
const phaseSpinner =
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
520
|
+
// Create spinner for this phase (suppressed in parallel mode)
|
|
521
|
+
const phaseSpinner = config.parallel
|
|
522
|
+
? undefined
|
|
523
|
+
: new PhaseSpinner({
|
|
524
|
+
phase,
|
|
525
|
+
phaseIndex: phaseNumber,
|
|
526
|
+
totalPhases,
|
|
527
|
+
shutdownManager,
|
|
528
|
+
iteration: useQualityLoop ? iteration : undefined,
|
|
529
|
+
});
|
|
530
|
+
phaseSpinner?.start();
|
|
531
|
+
emitProgressLine(issueNumber, phase, "start");
|
|
532
|
+
try {
|
|
533
|
+
onProgress?.(issueNumber, phase, "start");
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
/* progress errors must not halt */
|
|
537
|
+
}
|
|
400
538
|
// Track phase start in state
|
|
401
539
|
if (stateManager) {
|
|
402
540
|
try {
|
|
@@ -407,7 +545,7 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
407
545
|
}
|
|
408
546
|
}
|
|
409
547
|
const phaseStartTime = new Date();
|
|
410
|
-
const result = await executePhaseWithRetry(issueNumber, phase,
|
|
548
|
+
const result = await executePhaseWithRetry(issueNumber, phase, issueConfig, sessionId, worktreePath, shutdownManager, phaseSpinner);
|
|
411
549
|
const phaseEndTime = new Date();
|
|
412
550
|
// Capture session ID for subsequent phases
|
|
413
551
|
if (result.sessionId) {
|
|
@@ -423,6 +561,28 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
423
561
|
}
|
|
424
562
|
}
|
|
425
563
|
phaseResults.push(result);
|
|
564
|
+
// Emit completion/failure progress event (AC-8)
|
|
565
|
+
const phaseDurationSec = Math.round((phaseEndTime.getTime() - phaseStartTime.getTime()) / 1000);
|
|
566
|
+
if (result.success) {
|
|
567
|
+
const extra = { durationSeconds: phaseDurationSec };
|
|
568
|
+
emitProgressLine(issueNumber, phase, "complete", extra);
|
|
569
|
+
try {
|
|
570
|
+
onProgress?.(issueNumber, phase, "complete", extra);
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
/* progress errors must not halt */
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
const extra = { error: result.error ?? "unknown" };
|
|
578
|
+
emitProgressLine(issueNumber, phase, "failed", extra);
|
|
579
|
+
try {
|
|
580
|
+
onProgress?.(issueNumber, phase, "failed", extra);
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
/* progress errors must not halt */
|
|
584
|
+
}
|
|
585
|
+
}
|
|
426
586
|
// Log phase result with observability data (AC-1, AC-2, AC-3, AC-7)
|
|
427
587
|
if (logWriter) {
|
|
428
588
|
// Capture git diff stats for worktree phases (AC-1, AC-3)
|
|
@@ -435,6 +595,16 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
435
595
|
: undefined;
|
|
436
596
|
// Read cache metrics for QA phase (AC-7)
|
|
437
597
|
const cacheMetrics = phase === "qa" ? readCacheMetrics(worktreePath) : undefined;
|
|
598
|
+
// Build errorContext from captured stderr/stdout tails (#447)
|
|
599
|
+
let errorContext;
|
|
600
|
+
if (!result.success && result.stderrTail) {
|
|
601
|
+
errorContext = {
|
|
602
|
+
stderrTail: result.stderrTail ?? [],
|
|
603
|
+
stdoutTail: result.stdoutTail ?? [],
|
|
604
|
+
exitCode: result.exitCode,
|
|
605
|
+
category: classifyError(result.stderrTail ?? []),
|
|
606
|
+
};
|
|
607
|
+
}
|
|
438
608
|
const phaseLog = createPhaseLogFromTiming(phase, issueNumber, phaseStartTime, phaseEndTime, result.success
|
|
439
609
|
? "success"
|
|
440
610
|
: result.error?.includes("Timeout")
|
|
@@ -442,11 +612,13 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
442
612
|
: "failure", {
|
|
443
613
|
error: result.error,
|
|
444
614
|
verdict: result.verdict,
|
|
615
|
+
summary: result.summary,
|
|
445
616
|
// Observability fields (AC-1, AC-2, AC-3, AC-7)
|
|
446
617
|
filesModified: diffStats?.filesModified,
|
|
447
618
|
fileDiffStats: diffStats?.fileDiffStats,
|
|
448
619
|
commitHash,
|
|
449
620
|
cacheMetrics,
|
|
621
|
+
errorContext,
|
|
450
622
|
});
|
|
451
623
|
logWriter.logPhase(phaseLog);
|
|
452
624
|
}
|
|
@@ -465,34 +637,76 @@ export async function runIssueWithLogging(issueNumber, config, logWriter, stateM
|
|
|
465
637
|
}
|
|
466
638
|
}
|
|
467
639
|
if (result.success) {
|
|
468
|
-
phaseSpinner
|
|
640
|
+
phaseSpinner?.succeed();
|
|
469
641
|
}
|
|
470
642
|
else {
|
|
471
|
-
phaseSpinner
|
|
643
|
+
phaseSpinner?.fail(result.error);
|
|
472
644
|
phasesFailed = true;
|
|
473
645
|
// If quality loop enabled, run loop phase to fix issues
|
|
474
646
|
if (useQualityLoop && iteration < maxIterations) {
|
|
475
|
-
// Create spinner for loop phase
|
|
476
|
-
const loopSpinner =
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
647
|
+
// Create spinner for loop phase (suppressed in parallel mode)
|
|
648
|
+
const loopSpinner = config.parallel
|
|
649
|
+
? undefined
|
|
650
|
+
: new PhaseSpinner({
|
|
651
|
+
phase: "loop",
|
|
652
|
+
phaseIndex: phaseNumber,
|
|
653
|
+
totalPhases,
|
|
654
|
+
shutdownManager,
|
|
655
|
+
iteration,
|
|
656
|
+
});
|
|
657
|
+
loopSpinner?.start();
|
|
658
|
+
emitProgressLine(issueNumber, "loop", "start");
|
|
659
|
+
try {
|
|
660
|
+
onProgress?.(issueNumber, "loop", "start");
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
/* progress errors must not halt */
|
|
664
|
+
}
|
|
665
|
+
// Build enriched config for loop phase with QA context (#488).
|
|
666
|
+
// Pass verdict, failed ACs, and error directly so the /loop skill
|
|
667
|
+
// doesn't need to reconstruct context from GitHub comments.
|
|
668
|
+
const loopConfig = {
|
|
669
|
+
...issueConfig,
|
|
670
|
+
lastVerdict: result.verdict ?? undefined,
|
|
671
|
+
failedAcs: result.summary?.gaps?.join("; ") ?? undefined,
|
|
672
|
+
promptContext: buildLoopContext(result),
|
|
673
|
+
};
|
|
674
|
+
const loopStartTime = new Date();
|
|
675
|
+
const loopResult = await executePhaseWithRetry(issueNumber, "loop", loopConfig, sessionId, worktreePath, shutdownManager, loopSpinner);
|
|
676
|
+
const loopEndTime = new Date();
|
|
485
677
|
phaseResults.push(loopResult);
|
|
678
|
+
// Emit loop completion/failure progress event (AC-8)
|
|
679
|
+
const loopDurationSec = Math.round((loopEndTime.getTime() - loopStartTime.getTime()) / 1000);
|
|
680
|
+
if (loopResult.success) {
|
|
681
|
+
const extra = { durationSeconds: loopDurationSec };
|
|
682
|
+
emitProgressLine(issueNumber, "loop", "complete", extra);
|
|
683
|
+
try {
|
|
684
|
+
onProgress?.(issueNumber, "loop", "complete", extra);
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
/* progress errors must not halt */
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
const extra = { error: loopResult.error ?? "unknown" };
|
|
692
|
+
emitProgressLine(issueNumber, "loop", "failed", extra);
|
|
693
|
+
try {
|
|
694
|
+
onProgress?.(issueNumber, "loop", "failed", extra);
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
/* progress errors must not halt */
|
|
698
|
+
}
|
|
699
|
+
}
|
|
486
700
|
if (loopResult.sessionId) {
|
|
487
701
|
sessionId = loopResult.sessionId;
|
|
488
702
|
}
|
|
489
703
|
if (loopResult.success) {
|
|
490
|
-
loopSpinner
|
|
704
|
+
loopSpinner?.succeed();
|
|
491
705
|
// Continue to next iteration
|
|
492
706
|
break;
|
|
493
707
|
}
|
|
494
708
|
else {
|
|
495
|
-
loopSpinner
|
|
709
|
+
loopSpinner?.fail(loopResult.error);
|
|
496
710
|
}
|
|
497
711
|
}
|
|
498
712
|
// 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
|
+
}
|