sequant 2.2.0 → 2.3.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +73 -0
- package/dist/bin/cli.js +94 -9
- package/dist/src/commands/doctor.d.ts +25 -0
- package/dist/src/commands/doctor.js +36 -1
- package/dist/src/commands/locks.d.ts +67 -0
- package/dist/src/commands/locks.js +290 -0
- package/dist/src/commands/merge.js +11 -0
- package/dist/src/commands/prompt.d.ts +39 -0
- package/dist/src/commands/prompt.js +179 -0
- package/dist/src/commands/run-display.d.ts +11 -2
- package/dist/src/commands/run-display.js +62 -28
- package/dist/src/commands/run-progress.d.ts +32 -0
- package/dist/src/commands/run-progress.js +76 -0
- package/dist/src/commands/run.js +80 -18
- package/dist/src/commands/stats.d.ts +2 -0
- package/dist/src/commands/stats.js +94 -8
- package/dist/src/commands/status.js +12 -0
- package/dist/src/commands/watch.d.ts +16 -0
- package/dist/src/commands/watch.js +147 -0
- package/dist/src/lib/ac-linter.d.ts +1 -1
- package/dist/src/lib/ac-linter.js +81 -0
- package/dist/src/lib/assess-collision-detect.d.ts +91 -0
- package/dist/src/lib/assess-collision-detect.js +217 -0
- package/dist/src/lib/assess-comment-parser.d.ts +59 -1
- package/dist/src/lib/assess-comment-parser.js +124 -2
- package/dist/src/lib/cli-ui/format.d.ts +19 -0
- package/dist/src/lib/cli-ui/format.js +34 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +181 -0
- package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +239 -0
- package/dist/src/lib/cli-ui/run-renderer.js +1173 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
- package/dist/src/lib/locks/index.d.ts +7 -0
- package/dist/src/lib/locks/index.js +5 -0
- package/dist/src/lib/locks/lock-manager.d.ts +168 -0
- package/dist/src/lib/locks/lock-manager.js +433 -0
- package/dist/src/lib/locks/types.d.ts +59 -0
- package/dist/src/lib/locks/types.js +31 -0
- package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
- package/dist/src/lib/qa/markdown-only-ci.js +74 -0
- package/dist/src/lib/relay/activation.d.ts +60 -0
- package/dist/src/lib/relay/activation.js +122 -0
- package/dist/src/lib/relay/archive.d.ts +34 -0
- package/dist/src/lib/relay/archive.js +106 -0
- package/dist/src/lib/relay/frame.d.ts +20 -0
- package/dist/src/lib/relay/frame.js +76 -0
- package/dist/src/lib/relay/index.d.ts +13 -0
- package/dist/src/lib/relay/index.js +13 -0
- package/dist/src/lib/relay/paths.d.ts +43 -0
- package/dist/src/lib/relay/paths.js +59 -0
- package/dist/src/lib/relay/pid.d.ts +34 -0
- package/dist/src/lib/relay/pid.js +72 -0
- package/dist/src/lib/relay/reader.d.ts +35 -0
- package/dist/src/lib/relay/reader.js +115 -0
- package/dist/src/lib/relay/types.d.ts +68 -0
- package/dist/src/lib/relay/types.js +76 -0
- package/dist/src/lib/relay/writer.d.ts +48 -0
- package/dist/src/lib/relay/writer.js +113 -0
- package/dist/src/lib/settings.d.ts +31 -1
- package/dist/src/lib/settings.js +18 -3
- package/dist/src/lib/version-check.d.ts +60 -5
- package/dist/src/lib/version-check.js +97 -9
- package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
- package/dist/src/lib/workflow/batch-executor.js +248 -175
- package/dist/src/lib/workflow/config-resolver.js +4 -0
- package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
- package/dist/src/lib/workflow/heartbeat.js +194 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +62 -8
- package/dist/src/lib/workflow/phase-executor.js +157 -16
- package/dist/src/lib/workflow/phase-mapper.d.ts +3 -2
- package/dist/src/lib/workflow/phase-mapper.js +17 -20
- package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
- package/dist/src/lib/workflow/platforms/github.js +20 -3
- package/dist/src/lib/workflow/pr-status.d.ts +18 -2
- package/dist/src/lib/workflow/pr-status.js +41 -9
- package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
- package/dist/src/lib/workflow/qa-stagnation.js +179 -0
- package/dist/src/lib/workflow/run-orchestrator.d.ts +39 -0
- package/dist/src/lib/workflow/run-orchestrator.js +340 -15
- package/dist/src/lib/workflow/run-reflect.js +1 -1
- package/dist/src/lib/workflow/run-state.d.ts +71 -0
- package/dist/src/lib/workflow/run-state.js +14 -0
- package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
- package/dist/src/lib/workflow/state-cleanup.js +17 -5
- package/dist/src/lib/workflow/state-manager.d.ts +12 -1
- package/dist/src/lib/workflow/state-manager.js +37 -0
- package/dist/src/lib/workflow/state-schema.d.ts +62 -0
- package/dist/src/lib/workflow/state-schema.js +35 -1
- package/dist/src/lib/workflow/types.d.ts +74 -1
- package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
- package/dist/src/lib/workflow/worktree-manager.js +15 -6
- package/dist/src/mcp/tools/run.d.ts +44 -0
- package/dist/src/mcp/tools/run.js +104 -13
- package/dist/src/ui/tui/App.d.ts +14 -0
- package/dist/src/ui/tui/App.js +41 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
- package/dist/src/ui/tui/ElapsedTimer.js +31 -0
- package/dist/src/ui/tui/Header.d.ts +6 -0
- package/dist/src/ui/tui/Header.js +15 -0
- package/dist/src/ui/tui/IssueBox.d.ts +16 -0
- package/dist/src/ui/tui/IssueBox.js +68 -0
- package/dist/src/ui/tui/Spinner.d.ts +9 -0
- package/dist/src/ui/tui/Spinner.js +18 -0
- package/dist/src/ui/tui/index.d.ts +15 -0
- package/dist/src/ui/tui/index.js +29 -0
- package/dist/src/ui/tui/theme.d.ts +29 -0
- package/dist/src/ui/tui/theme.js +52 -0
- package/dist/src/ui/tui/truncate.d.ts +11 -0
- package/dist/src/ui/tui/truncate.js +31 -0
- package/package.json +10 -3
- package/templates/agents/sequant-explorer.md +1 -0
- package/templates/agents/sequant-qa-checker.md +2 -1
- package/templates/agents/sequant-testgen.md +1 -0
- package/templates/hooks/post-tool.sh +11 -0
- package/templates/hooks/pre-tool.sh +18 -9
- package/templates/hooks/relay-check.sh +107 -0
- package/templates/relay/frame.txt +11 -0
- package/templates/scripts/cleanup-worktree.sh +25 -3
- package/templates/scripts/new-feature.sh +6 -0
- package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
- package/templates/skills/_shared/references/subagent-types.md +21 -8
- package/templates/skills/assess/SKILL.md +103 -49
- package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
- package/templates/skills/docs/SKILL.md +141 -22
- package/templates/skills/exec/SKILL.md +10 -8
- package/templates/skills/fullsolve/SKILL.md +79 -5
- package/templates/skills/loop/SKILL.md +28 -0
- package/templates/skills/merger/SKILL.md +621 -0
- package/templates/skills/qa/SKILL.md +727 -8
- package/templates/skills/setup/SKILL.md +6 -0
- package/templates/skills/spec/SKILL.md +52 -0
- package/templates/skills/spec/references/parallel-groups.md +7 -0
- package/templates/skills/spec/references/recommended-workflow.md +4 -2
- package/templates/skills/testgen/SKILL.md +24 -17
|
@@ -11,11 +11,11 @@ import chalk from "chalk";
|
|
|
11
11
|
import { spawnSync } from "child_process";
|
|
12
12
|
import { createPhaseLogFromTiming } from "./log-writer.js";
|
|
13
13
|
import { classifyError, errorTypeToCategory } from "./error-classifier.js";
|
|
14
|
-
import { PhaseSpinner } from "../phase-spinner.js";
|
|
15
14
|
import { getGitDiffStats, getCommitHash } from "./git-diff-utils.js";
|
|
16
15
|
import { createCheckpointCommit, rebaseBeforePR, createPR, readCacheMetrics, filterResumedPhases, } from "./worktree-manager.js";
|
|
17
16
|
import { executePhaseWithRetry } from "./phase-executor.js";
|
|
18
|
-
import { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIssue,
|
|
17
|
+
import { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIssue, DOCS_LABELS, } from "./phase-mapper.js";
|
|
18
|
+
import { activateRelay, deactivateRelay, } from "../relay/activation.js";
|
|
19
19
|
/**
|
|
20
20
|
* Emit a structured progress line to stderr for MCP progress notifications.
|
|
21
21
|
* Only emits when running under an orchestrator (e.g., MCP server).
|
|
@@ -26,6 +26,30 @@ import { detectPhasesFromLabels, parseRecommendedWorkflow, determinePhasesForIss
|
|
|
26
26
|
* @param event - Phase lifecycle event: "start", "complete", or "failed"
|
|
27
27
|
* @param extra - Optional fields: durationSeconds (on complete), error (on failed)
|
|
28
28
|
*/
|
|
29
|
+
/**
|
|
30
|
+
* Wrap an `ExecutionConfig` with an `onActivity` hook that re-emits each
|
|
31
|
+
* agent-output ping as a `"activity"` progress event for the dashboard (#543).
|
|
32
|
+
*
|
|
33
|
+
* Returns the input config unchanged when no `onProgress` callback is set,
|
|
34
|
+
* so non-TUI runs pay no overhead.
|
|
35
|
+
*
|
|
36
|
+
* @internal Exported for testing only
|
|
37
|
+
*/
|
|
38
|
+
export function withActivityHook(base, issueNumber, phase, onProgress) {
|
|
39
|
+
if (!onProgress)
|
|
40
|
+
return base;
|
|
41
|
+
return {
|
|
42
|
+
...base,
|
|
43
|
+
onActivity: (text) => {
|
|
44
|
+
try {
|
|
45
|
+
onProgress(issueNumber, phase, "activity", { text });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Activity events must never disrupt the run.
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
29
53
|
/**
|
|
30
54
|
* Build enriched prompt context for the /loop phase from a failed phase result (#488).
|
|
31
55
|
* Passes QA verdict, failed ACs, and error directly so the /loop skill doesn't need
|
|
@@ -64,9 +88,26 @@ export function emitProgressLine(issue, phase, event = "start", extra) {
|
|
|
64
88
|
if (extra?.error !== undefined) {
|
|
65
89
|
payload.error = extra.error;
|
|
66
90
|
}
|
|
91
|
+
// #624 Item 3: surface the outer-loop iteration so MCP consumers (and the
|
|
92
|
+
// renderer) can label retried events as `(attempt N/M)` / `loop N/M`.
|
|
93
|
+
if (extra?.iteration !== undefined) {
|
|
94
|
+
payload.iteration = extra.iteration;
|
|
95
|
+
}
|
|
67
96
|
const line = `SEQUANT_PROGRESS:${JSON.stringify(payload)}\n`;
|
|
68
97
|
process.stderr.write(line);
|
|
69
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Emit the current run's UUID on stderr so MCP callers can look up the exact
|
|
101
|
+
* log file produced by this subprocess instead of relying on a fuzzy time
|
|
102
|
+
* filter (#631). Gated on `SEQUANT_ORCHESTRATOR` so CLI users see nothing.
|
|
103
|
+
*
|
|
104
|
+
* Must be called before `emitProgressLine` to satisfy AC-1.
|
|
105
|
+
*/
|
|
106
|
+
export function emitRunIdLine(runId) {
|
|
107
|
+
if (!process.env.SEQUANT_ORCHESTRATOR)
|
|
108
|
+
return;
|
|
109
|
+
process.stderr.write(`SEQUANT_RUN_ID:${runId}\n`);
|
|
110
|
+
}
|
|
70
111
|
export async function getIssueInfo(issueNumber) {
|
|
71
112
|
try {
|
|
72
113
|
const result = spawnSync("gh", ["issue", "view", String(issueNumber), "--json", "title,labels"], { stdio: "pipe" });
|
|
@@ -213,6 +254,9 @@ export function getEnvConfig() {
|
|
|
213
254
|
if (process.env.SEQUANT_TESTGEN === "true") {
|
|
214
255
|
config.testgen = true;
|
|
215
256
|
}
|
|
257
|
+
if (process.env.SEQUANT_SECURITY_REVIEW === "true") {
|
|
258
|
+
config.securityReview = true;
|
|
259
|
+
}
|
|
216
260
|
return config;
|
|
217
261
|
}
|
|
218
262
|
export async function executeBatch(issueNumbers, batchCtx) {
|
|
@@ -303,162 +347,174 @@ export async function runIssueWithLogging(ctx) {
|
|
|
303
347
|
}
|
|
304
348
|
}
|
|
305
349
|
}
|
|
350
|
+
// Activate relay (#383) if enabled. Tolerates errors — relay must never
|
|
351
|
+
// block the underlying run.
|
|
352
|
+
let relayActivation = null;
|
|
353
|
+
if (config.relayEnabled && !config.dryRun) {
|
|
354
|
+
try {
|
|
355
|
+
relayActivation = await activateRelay(issueNumber, {
|
|
356
|
+
worktreePath,
|
|
357
|
+
stateManager: stateManager ?? null,
|
|
358
|
+
});
|
|
359
|
+
if (relayActivation.warning && config.verbose) {
|
|
360
|
+
log(chalk.yellow(` ! Relay: ${relayActivation.warning}`));
|
|
361
|
+
}
|
|
362
|
+
else if (relayActivation.activated && config.verbose) {
|
|
363
|
+
log(chalk.gray(` Relay active — use \`sequant prompt ${issueNumber} "<msg>"\` to nudge`));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
if (config.verbose) {
|
|
368
|
+
log(chalk.yellow(` ! Relay activation failed: ${err}`));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
306
372
|
// Determine phases for this specific issue
|
|
307
373
|
let phases;
|
|
308
374
|
let detectedQualityLoop = false;
|
|
309
375
|
let specAlreadyRan = false;
|
|
310
376
|
if (options.autoDetectPhases) {
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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(" → ")}`));
|
|
377
|
+
// #533: Always run spec to get recommended workflow.
|
|
378
|
+
// The prior bug/docs shortcut (skip spec → exec → qa) was removed because
|
|
379
|
+
// bug and docs issues often contain design decisions (scope boundaries,
|
|
380
|
+
// edge cases, test-strategy shifts) that benefit from a spec pass.
|
|
381
|
+
log(chalk.gray(` Running spec to determine workflow...`));
|
|
382
|
+
// RunRenderer (#618) owns spec progress via emitProgressLine + onProgress.
|
|
383
|
+
// The legacy PhaseSpinner produced duplicate lines for single-issue runs.
|
|
384
|
+
emitProgressLine(issueNumber, "spec", "start");
|
|
385
|
+
try {
|
|
386
|
+
onProgress?.(issueNumber, "spec", "start");
|
|
325
387
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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");
|
|
388
|
+
catch {
|
|
389
|
+
/* progress errors must not halt */
|
|
390
|
+
}
|
|
391
|
+
// Track spec phase start in state
|
|
392
|
+
if (stateManager) {
|
|
340
393
|
try {
|
|
341
|
-
|
|
394
|
+
await stateManager.updatePhaseStatus(issueNumber, "spec", "in_progress");
|
|
342
395
|
}
|
|
343
396
|
catch {
|
|
344
|
-
|
|
397
|
+
// State tracking errors shouldn't stop execution
|
|
345
398
|
}
|
|
346
|
-
|
|
399
|
+
}
|
|
400
|
+
const specStartTime = new Date();
|
|
401
|
+
// Note: spec runs in main repo (not worktree) for planning
|
|
402
|
+
const specResult = await executePhaseWithRetry(issueNumber, "spec", withActivityHook(config, issueNumber, "spec", onProgress), sessionId, worktreePath, // Will be ignored for spec (non-isolated phase)
|
|
403
|
+
shutdownManager);
|
|
404
|
+
const specEndTime = new Date();
|
|
405
|
+
if (specResult.sessionId) {
|
|
406
|
+
sessionId = specResult.sessionId;
|
|
407
|
+
// Update session ID in state for resume capability
|
|
347
408
|
if (stateManager) {
|
|
348
409
|
try {
|
|
349
|
-
await stateManager.
|
|
410
|
+
await stateManager.updateSessionId(issueNumber, specResult.sessionId);
|
|
350
411
|
}
|
|
351
412
|
catch {
|
|
352
413
|
// State tracking errors shouldn't stop execution
|
|
353
414
|
}
|
|
354
415
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
await stateManager.updateSessionId(issueNumber, specResult.sessionId);
|
|
366
|
-
}
|
|
367
|
-
catch {
|
|
368
|
-
// State tracking errors shouldn't stop execution
|
|
369
|
-
}
|
|
370
|
-
}
|
|
416
|
+
}
|
|
417
|
+
phaseResults.push(specResult);
|
|
418
|
+
specAlreadyRan = true;
|
|
419
|
+
// Emit completion/failure progress event (AC-8)
|
|
420
|
+
const specDurationSec = Math.round((specEndTime.getTime() - specStartTime.getTime()) / 1000);
|
|
421
|
+
if (specResult.success) {
|
|
422
|
+
const extra = { durationSeconds: specDurationSec };
|
|
423
|
+
emitProgressLine(issueNumber, "spec", "complete", extra);
|
|
424
|
+
try {
|
|
425
|
+
onProgress?.(issueNumber, "spec", "complete", extra);
|
|
371
426
|
}
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
}
|
|
427
|
+
catch {
|
|
428
|
+
/* progress errors must not halt */
|
|
385
429
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
catch {
|
|
393
|
-
/* progress errors must not halt */
|
|
394
|
-
}
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
const extra = { error: specResult.error ?? "unknown" };
|
|
433
|
+
emitProgressLine(issueNumber, "spec", "failed", extra);
|
|
434
|
+
try {
|
|
435
|
+
onProgress?.(issueNumber, "spec", "failed", extra);
|
|
395
436
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (logWriter) {
|
|
399
|
-
// Build errorContext from captured stderr/stdout tails (#447)
|
|
400
|
-
let specErrorContext;
|
|
401
|
-
if (!specResult.success && specResult.stderrTail) {
|
|
402
|
-
const specError = classifyError(specResult.stderrTail ?? [], specResult.exitCode);
|
|
403
|
-
specErrorContext = {
|
|
404
|
-
stderrTail: specResult.stderrTail ?? [],
|
|
405
|
-
stdoutTail: specResult.stdoutTail ?? [],
|
|
406
|
-
exitCode: specResult.exitCode,
|
|
407
|
-
category: errorTypeToCategory(specError),
|
|
408
|
-
errorType: specError.name,
|
|
409
|
-
errorMetadata: specError.metadata,
|
|
410
|
-
isRetryable: specError.isRetryable,
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
const phaseLog = createPhaseLogFromTiming("spec", issueNumber, specStartTime, specEndTime, specResult.success
|
|
414
|
-
? "success"
|
|
415
|
-
: specResult.error?.includes("Timeout")
|
|
416
|
-
? "timeout"
|
|
417
|
-
: "failure", { error: specResult.error, errorContext: specErrorContext });
|
|
418
|
-
logWriter.logPhase(phaseLog);
|
|
437
|
+
catch {
|
|
438
|
+
/* progress errors must not halt */
|
|
419
439
|
}
|
|
420
|
-
|
|
421
|
-
|
|
440
|
+
}
|
|
441
|
+
// Log spec phase result
|
|
442
|
+
// Note: Spec runs in main repo, not worktree, so no git diff stats
|
|
443
|
+
if (logWriter) {
|
|
444
|
+
// Build errorContext from captured stderr/stdout tails (#447)
|
|
445
|
+
let specErrorContext;
|
|
446
|
+
if (!specResult.success && specResult.stderrTail) {
|
|
447
|
+
const specError = classifyError(specResult.stderrTail ?? [], specResult.exitCode);
|
|
448
|
+
specErrorContext = {
|
|
449
|
+
stderrTail: specResult.stderrTail ?? [],
|
|
450
|
+
stdoutTail: specResult.stdoutTail ?? [],
|
|
451
|
+
exitCode: specResult.exitCode,
|
|
452
|
+
category: errorTypeToCategory(specError),
|
|
453
|
+
errorType: specError.name,
|
|
454
|
+
errorMetadata: specError.metadata,
|
|
455
|
+
isRetryable: specError.isRetryable,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const phaseLog = createPhaseLogFromTiming("spec", issueNumber, specStartTime, specEndTime, specResult.success
|
|
459
|
+
? "success"
|
|
460
|
+
: specResult.error?.includes("Timeout")
|
|
461
|
+
? "timeout"
|
|
462
|
+
: "failure", { error: specResult.error, errorContext: specErrorContext });
|
|
463
|
+
logWriter.logPhase(phaseLog);
|
|
464
|
+
}
|
|
465
|
+
// Track spec phase completion in state
|
|
466
|
+
if (stateManager) {
|
|
467
|
+
try {
|
|
468
|
+
const phaseStatus = specResult.success ? "completed" : "failed";
|
|
469
|
+
await stateManager.updatePhaseStatus(issueNumber, "spec", phaseStatus, {
|
|
470
|
+
error: specResult.error,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
// State tracking errors shouldn't stop execution
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (!specResult.success) {
|
|
478
|
+
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
479
|
+
// Archive relay state on early exit (spec failure).
|
|
480
|
+
if (relayActivation) {
|
|
422
481
|
try {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
482
|
+
await deactivateRelay(issueNumber, {
|
|
483
|
+
phase: "spec",
|
|
484
|
+
startedAt: relayActivation.startedAt,
|
|
485
|
+
worktreePath,
|
|
486
|
+
stateManager: stateManager ?? null,
|
|
426
487
|
});
|
|
427
488
|
}
|
|
428
489
|
catch {
|
|
429
|
-
|
|
490
|
+
/* swallow */
|
|
430
491
|
}
|
|
431
492
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const detected = detectPhasesFromLabels(labels);
|
|
458
|
-
phases = detected.phases.filter((p) => p !== "spec");
|
|
459
|
-
detectedQualityLoop = detected.qualityLoop;
|
|
460
|
-
log(chalk.gray(` Fallback: ${phases.join(" → ")}`));
|
|
461
|
-
}
|
|
493
|
+
return {
|
|
494
|
+
issueNumber,
|
|
495
|
+
success: false,
|
|
496
|
+
phaseResults,
|
|
497
|
+
durationSeconds,
|
|
498
|
+
loopTriggered: false,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
// Parse recommended workflow from spec output
|
|
502
|
+
const parsedWorkflow = specResult.output
|
|
503
|
+
? parseRecommendedWorkflow(specResult.output)
|
|
504
|
+
: null;
|
|
505
|
+
if (parsedWorkflow) {
|
|
506
|
+
// Remove spec from phases since we already ran it
|
|
507
|
+
phases = parsedWorkflow.phases.filter((p) => p !== "spec");
|
|
508
|
+
detectedQualityLoop = parsedWorkflow.qualityLoop;
|
|
509
|
+
log(chalk.gray(` Spec recommends: ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
// Fall back to label-based detection
|
|
513
|
+
log(chalk.yellow(` Could not parse spec recommendation, using label-based detection`));
|
|
514
|
+
const detected = detectPhasesFromLabels(labels);
|
|
515
|
+
phases = detected.phases.filter((p) => p !== "spec");
|
|
516
|
+
detectedQualityLoop = detected.qualityLoop;
|
|
517
|
+
log(chalk.gray(` Fallback: ${phases.join(" → ")}`));
|
|
462
518
|
}
|
|
463
519
|
}
|
|
464
520
|
else {
|
|
@@ -497,6 +553,21 @@ export async function runIssueWithLogging(ctx) {
|
|
|
497
553
|
}
|
|
498
554
|
}
|
|
499
555
|
}
|
|
556
|
+
// Add security-review phase if requested (and spec was in the phases).
|
|
557
|
+
// Idempotent vs label-based auto-detection — only inserts if not present.
|
|
558
|
+
if (options.securityReview &&
|
|
559
|
+
(phases.includes("spec") || specAlreadyRan) &&
|
|
560
|
+
!phases.includes("security-review")) {
|
|
561
|
+
if (specAlreadyRan) {
|
|
562
|
+
phases.unshift("security-review");
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
const specIndex = phases.indexOf("spec");
|
|
566
|
+
if (specIndex !== -1) {
|
|
567
|
+
phases.splice(specIndex + 1, 0, "security-review");
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
500
571
|
// Build per-issue config with issue type metadata for skill env propagation
|
|
501
572
|
const lowerLabelsForType = labels.map((l) => l.toLowerCase());
|
|
502
573
|
const issueIsDocs = lowerLabelsForType.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
|
|
@@ -514,27 +585,17 @@ export async function runIssueWithLogging(ctx) {
|
|
|
514
585
|
loopTriggered = true;
|
|
515
586
|
}
|
|
516
587
|
let phasesFailed = false;
|
|
517
|
-
// Calculate total phases for progress indicator
|
|
518
|
-
// If spec already ran in auto-detect mode, it's counted separately
|
|
519
|
-
const totalPhases = specAlreadyRan ? phases.length + 1 : phases.length;
|
|
520
|
-
const phaseIndexOffset = specAlreadyRan ? 1 : 0;
|
|
521
588
|
for (let phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {
|
|
522
589
|
const phase = phases[phaseIdx];
|
|
523
|
-
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
totalPhases,
|
|
531
|
-
shutdownManager,
|
|
532
|
-
iteration: useQualityLoop ? iteration : undefined,
|
|
533
|
-
});
|
|
534
|
-
phaseSpinner?.start();
|
|
535
|
-
emitProgressLine(issueNumber, phase, "start");
|
|
590
|
+
// RunRenderer (#618) owns phase progress via emitProgressLine + onProgress.
|
|
591
|
+
// #624 Item 3: surface the outer-loop iteration on every retried phase
|
|
592
|
+
// event so the renderer can label them `(attempt N/M)`. First-attempt
|
|
593
|
+
// events still get `iteration: 1` so the data flow is uniform; the
|
|
594
|
+
// renderer's `formatRetrySuffix` suppresses the suffix when iteration ≤ 1.
|
|
595
|
+
const phaseExtra = { iteration };
|
|
596
|
+
emitProgressLine(issueNumber, phase, "start", phaseExtra);
|
|
536
597
|
try {
|
|
537
|
-
onProgress?.(issueNumber, phase, "start");
|
|
598
|
+
onProgress?.(issueNumber, phase, "start", phaseExtra);
|
|
538
599
|
}
|
|
539
600
|
catch {
|
|
540
601
|
/* progress errors must not halt */
|
|
@@ -549,7 +610,7 @@ export async function runIssueWithLogging(ctx) {
|
|
|
549
610
|
}
|
|
550
611
|
}
|
|
551
612
|
const phaseStartTime = new Date();
|
|
552
|
-
const result = await executePhaseWithRetry(issueNumber, phase, issueConfig, sessionId, worktreePath, shutdownManager
|
|
613
|
+
const result = await executePhaseWithRetry(issueNumber, phase, withActivityHook(issueConfig, issueNumber, phase, onProgress), sessionId, worktreePath, shutdownManager);
|
|
553
614
|
const phaseEndTime = new Date();
|
|
554
615
|
// Capture session ID for subsequent phases
|
|
555
616
|
if (result.sessionId) {
|
|
@@ -568,7 +629,7 @@ export async function runIssueWithLogging(ctx) {
|
|
|
568
629
|
// Emit completion/failure progress event (AC-8)
|
|
569
630
|
const phaseDurationSec = Math.round((phaseEndTime.getTime() - phaseStartTime.getTime()) / 1000);
|
|
570
631
|
if (result.success) {
|
|
571
|
-
const extra = { durationSeconds: phaseDurationSec };
|
|
632
|
+
const extra = { durationSeconds: phaseDurationSec, iteration };
|
|
572
633
|
emitProgressLine(issueNumber, phase, "complete", extra);
|
|
573
634
|
try {
|
|
574
635
|
onProgress?.(issueNumber, phase, "complete", extra);
|
|
@@ -578,7 +639,7 @@ export async function runIssueWithLogging(ctx) {
|
|
|
578
639
|
}
|
|
579
640
|
}
|
|
580
641
|
else {
|
|
581
|
-
const extra = { error: result.error ?? "unknown" };
|
|
642
|
+
const extra = { error: result.error ?? "unknown", iteration };
|
|
582
643
|
emitProgressLine(issueNumber, phase, "failed", extra);
|
|
583
644
|
try {
|
|
584
645
|
onProgress?.(issueNumber, phase, "failed", extra);
|
|
@@ -645,27 +706,18 @@ export async function runIssueWithLogging(ctx) {
|
|
|
645
706
|
}
|
|
646
707
|
}
|
|
647
708
|
if (result.success) {
|
|
648
|
-
|
|
709
|
+
// Phase succeeded — RunRenderer (#618) updates state via onProgress.
|
|
649
710
|
}
|
|
650
711
|
else {
|
|
651
|
-
phaseSpinner?.fail(result.error);
|
|
652
712
|
phasesFailed = true;
|
|
653
713
|
// If quality loop enabled, run loop phase to fix issues
|
|
654
714
|
if (useQualityLoop && iteration < maxIterations) {
|
|
655
|
-
//
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
phase: "loop",
|
|
660
|
-
phaseIndex: phaseNumber,
|
|
661
|
-
totalPhases,
|
|
662
|
-
shutdownManager,
|
|
663
|
-
iteration,
|
|
664
|
-
});
|
|
665
|
-
loopSpinner?.start();
|
|
666
|
-
emitProgressLine(issueNumber, "loop", "start");
|
|
715
|
+
// #624 Item 3 (AC-3.3): the loop phase carries the current outer
|
|
716
|
+
// iteration so the live-zone status cell can show `loop N/M`.
|
|
717
|
+
const loopStartExtra = { iteration };
|
|
718
|
+
emitProgressLine(issueNumber, "loop", "start", loopStartExtra);
|
|
667
719
|
try {
|
|
668
|
-
onProgress?.(issueNumber, "loop", "start");
|
|
720
|
+
onProgress?.(issueNumber, "loop", "start", loopStartExtra);
|
|
669
721
|
}
|
|
670
722
|
catch {
|
|
671
723
|
/* progress errors must not halt */
|
|
@@ -680,13 +732,13 @@ export async function runIssueWithLogging(ctx) {
|
|
|
680
732
|
promptContext: buildLoopContext(result),
|
|
681
733
|
};
|
|
682
734
|
const loopStartTime = new Date();
|
|
683
|
-
const loopResult = await executePhaseWithRetry(issueNumber, "loop", loopConfig, sessionId, worktreePath, shutdownManager
|
|
735
|
+
const loopResult = await executePhaseWithRetry(issueNumber, "loop", withActivityHook(loopConfig, issueNumber, "loop", onProgress), sessionId, worktreePath, shutdownManager);
|
|
684
736
|
const loopEndTime = new Date();
|
|
685
737
|
phaseResults.push(loopResult);
|
|
686
738
|
// Emit loop completion/failure progress event (AC-8)
|
|
687
739
|
const loopDurationSec = Math.round((loopEndTime.getTime() - loopStartTime.getTime()) / 1000);
|
|
688
740
|
if (loopResult.success) {
|
|
689
|
-
const extra = { durationSeconds: loopDurationSec };
|
|
741
|
+
const extra = { durationSeconds: loopDurationSec, iteration };
|
|
690
742
|
emitProgressLine(issueNumber, "loop", "complete", extra);
|
|
691
743
|
try {
|
|
692
744
|
onProgress?.(issueNumber, "loop", "complete", extra);
|
|
@@ -696,7 +748,7 @@ export async function runIssueWithLogging(ctx) {
|
|
|
696
748
|
}
|
|
697
749
|
}
|
|
698
750
|
else {
|
|
699
|
-
const extra = { error: loopResult.error ?? "unknown" };
|
|
751
|
+
const extra = { error: loopResult.error ?? "unknown", iteration };
|
|
700
752
|
emitProgressLine(issueNumber, "loop", "failed", extra);
|
|
701
753
|
try {
|
|
702
754
|
onProgress?.(issueNumber, "loop", "failed", extra);
|
|
@@ -709,13 +761,9 @@ export async function runIssueWithLogging(ctx) {
|
|
|
709
761
|
sessionId = loopResult.sessionId;
|
|
710
762
|
}
|
|
711
763
|
if (loopResult.success) {
|
|
712
|
-
loopSpinner?.succeed();
|
|
713
764
|
// Continue to next iteration
|
|
714
765
|
break;
|
|
715
766
|
}
|
|
716
|
-
else {
|
|
717
|
-
loopSpinner?.fail(loopResult.error);
|
|
718
|
-
}
|
|
719
767
|
}
|
|
720
768
|
// Stop on first failure (if not in quality loop or loop failed)
|
|
721
769
|
break;
|
|
@@ -766,7 +814,15 @@ export async function runIssueWithLogging(ctx) {
|
|
|
766
814
|
let prUrl;
|
|
767
815
|
const shouldCreatePR = success && worktreePath && branch && !options.noPr;
|
|
768
816
|
if (shouldCreatePR) {
|
|
769
|
-
|
|
817
|
+
// #605: under --stacked, target predecessor branch (only for non-first,
|
|
818
|
+
// non-last issues). Last PR keeps `main` so partial progress can land.
|
|
819
|
+
const stackOptions = chain?.predecessorBranch || chain?.stackManifest
|
|
820
|
+
? {
|
|
821
|
+
prBase: chain.predecessorBranch,
|
|
822
|
+
stackManifest: chain.stackManifest,
|
|
823
|
+
}
|
|
824
|
+
: undefined;
|
|
825
|
+
const prResult = createPR(worktreePath, issueNumber, issueTitle, branch, config.verbose, labels, stackOptions);
|
|
770
826
|
if (prResult.success && prResult.prNumber && prResult.prUrl) {
|
|
771
827
|
prNumber = prResult.prNumber;
|
|
772
828
|
prUrl = prResult.prUrl;
|
|
@@ -784,6 +840,23 @@ export async function runIssueWithLogging(ctx) {
|
|
|
784
840
|
}
|
|
785
841
|
}
|
|
786
842
|
}
|
|
843
|
+
// Deactivate relay (#383) — archive inbox/outbox transcripts to
|
|
844
|
+
// .sequant/logs/relay/ before worktree teardown (AC-D2). Never throws.
|
|
845
|
+
if (relayActivation) {
|
|
846
|
+
try {
|
|
847
|
+
await deactivateRelay(issueNumber, {
|
|
848
|
+
phase: phaseResults[phaseResults.length - 1]?.phase ?? "exec",
|
|
849
|
+
startedAt: relayActivation.startedAt,
|
|
850
|
+
worktreePath,
|
|
851
|
+
stateManager: stateManager ?? null,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
catch (err) {
|
|
855
|
+
if (config.verbose) {
|
|
856
|
+
log(chalk.yellow(` ! Relay deactivation failed: ${err}`));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
787
860
|
return {
|
|
788
861
|
issueNumber,
|
|
789
862
|
success,
|
|
@@ -146,6 +146,9 @@ export function buildExecutionConfig(mergedOptions, settings, issueCount) {
|
|
|
146
146
|
: (settings.run.retry ?? true);
|
|
147
147
|
const isSequential = mergedOptions.sequential ?? false;
|
|
148
148
|
const isParallel = !isSequential && issueCount > 1;
|
|
149
|
+
// `--no-relay` arrives from Commander as `mergedOptions.relay === false`;
|
|
150
|
+
// explicit `false` overrides settings, otherwise settings/default win.
|
|
151
|
+
const relayEnabled = mergedOptions.relay === false ? false : (settings.run.relay ?? true);
|
|
149
152
|
return {
|
|
150
153
|
...DEFAULT_CONFIG,
|
|
151
154
|
phases: explicitPhases ?? DEFAULT_PHASES,
|
|
@@ -163,5 +166,6 @@ export function buildExecutionConfig(mergedOptions, settings, issueCount) {
|
|
|
163
166
|
agent: mergedOptions.agent ?? settings.run.agent,
|
|
164
167
|
aiderSettings: settings.run.aider,
|
|
165
168
|
isolateParallel: mergedOptions.isolateParallel,
|
|
169
|
+
relayEnabled,
|
|
166
170
|
};
|
|
167
171
|
}
|