sequant 2.2.0 → 2.4.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 +81 -5
- package/dist/bin/cli.js +140 -13
- package/dist/src/commands/abort.d.ts +36 -0
- package/dist/src/commands/abort.js +138 -0
- 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 +46 -0
- package/dist/src/commands/prompt.js +273 -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 +42 -0
- package/dist/src/commands/run-progress.js +93 -0
- package/dist/src/commands/run.js +90 -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 +18 -0
- package/dist/src/commands/watch.js +211 -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 +220 -0
- package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +265 -0
- package/dist/src/lib/cli-ui/run-renderer.js +1390 -0
- package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
- package/dist/src/lib/cli-ui/scrollback-harness.js +294 -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/merge-check/types.js +1 -1
- 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 +112 -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 +70 -0
- package/dist/src/lib/relay/types.js +85 -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 +274 -185
- package/dist/src/lib/workflow/config-resolver.js +4 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
- package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
- package/dist/src/lib/workflow/drivers/aider.js +9 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
- package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
- package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
- package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
- package/dist/src/lib/workflow/event-emitter.js +102 -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/notice.d.ts +32 -0
- package/dist/src/lib/workflow/notice.js +38 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +58 -16
- package/dist/src/lib/workflow/phase-executor.js +244 -130
- package/dist/src/lib/workflow/phase-mapper.d.ts +27 -13
- package/dist/src/lib/workflow/phase-mapper.js +70 -51
- package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
- package/dist/src/lib/workflow/phase-registry.js +233 -0
- 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-log-schema.d.ts +5 -55
- package/dist/src/lib/workflow/run-orchestrator.d.ts +70 -1
- package/dist/src/lib/workflow/run-orchestrator.js +464 -25
- 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 +31 -2
- package/dist/src/lib/workflow/state-manager.js +64 -1
- package/dist/src/lib/workflow/state-schema.d.ts +82 -35
- package/dist/src/lib/workflow/state-schema.js +63 -4
- package/dist/src/lib/workflow/types.d.ts +139 -16
- package/dist/src/lib/workflow/types.js +18 -13
- 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 +14 -6
- 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 +92 -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 +122 -68
- 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 +12 -6
- 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,10 +254,13 @@ 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) {
|
|
219
|
-
const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, } = batchCtx;
|
|
263
|
+
const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, } = batchCtx;
|
|
220
264
|
const results = [];
|
|
221
265
|
for (const issueNumber of issueNumbers) {
|
|
222
266
|
// Check if shutdown was triggered
|
|
@@ -245,6 +289,8 @@ export async function executeBatch(issueNumbers, batchCtx) {
|
|
|
245
289
|
packageManager,
|
|
246
290
|
baseBranch,
|
|
247
291
|
onProgress,
|
|
292
|
+
onPhasePlan,
|
|
293
|
+
phasePauseHandle,
|
|
248
294
|
};
|
|
249
295
|
const result = await runIssueWithLogging(ctx);
|
|
250
296
|
results.push(result);
|
|
@@ -261,7 +307,7 @@ export async function executeBatch(issueNumbers, batchCtx) {
|
|
|
261
307
|
}
|
|
262
308
|
export async function runIssueWithLogging(ctx) {
|
|
263
309
|
// 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;
|
|
310
|
+
const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, } = ctx;
|
|
265
311
|
const worktreePath = worktree?.path;
|
|
266
312
|
const branch = worktree?.branch;
|
|
267
313
|
const chainMode = chain?.enabled;
|
|
@@ -269,7 +315,8 @@ export async function runIssueWithLogging(ctx) {
|
|
|
269
315
|
const startTime = Date.now();
|
|
270
316
|
const phaseResults = [];
|
|
271
317
|
let loopTriggered = false;
|
|
272
|
-
|
|
318
|
+
// Cross-phase resume token, driver-tagged and cwd-bound (#674).
|
|
319
|
+
let resumeHandle;
|
|
273
320
|
// In parallel mode, suppress per-issue terminal output to prevent interleaving.
|
|
274
321
|
// The caller (run.ts) handles progress display via updateProgress().
|
|
275
322
|
const log = config.parallel ? () => { } : console.log.bind(console);
|
|
@@ -303,162 +350,174 @@ export async function runIssueWithLogging(ctx) {
|
|
|
303
350
|
}
|
|
304
351
|
}
|
|
305
352
|
}
|
|
353
|
+
// Activate relay (#383) if enabled. Tolerates errors — relay must never
|
|
354
|
+
// block the underlying run.
|
|
355
|
+
let relayActivation = null;
|
|
356
|
+
if (config.relayEnabled && !config.dryRun) {
|
|
357
|
+
try {
|
|
358
|
+
relayActivation = await activateRelay(issueNumber, {
|
|
359
|
+
worktreePath,
|
|
360
|
+
stateManager: stateManager ?? null,
|
|
361
|
+
});
|
|
362
|
+
if (relayActivation.warning && config.verbose) {
|
|
363
|
+
log(chalk.yellow(` ! Relay: ${relayActivation.warning}`));
|
|
364
|
+
}
|
|
365
|
+
else if (relayActivation.activated && config.verbose) {
|
|
366
|
+
log(chalk.gray(` Relay active — use \`sequant prompt ${issueNumber} "<msg>"\` to nudge`));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
if (config.verbose) {
|
|
371
|
+
log(chalk.yellow(` ! Relay activation failed: ${err}`));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
306
375
|
// Determine phases for this specific issue
|
|
307
376
|
let phases;
|
|
308
377
|
let detectedQualityLoop = false;
|
|
309
378
|
let specAlreadyRan = false;
|
|
310
379
|
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(" → ")}`));
|
|
380
|
+
// #533: Always run spec to get recommended workflow.
|
|
381
|
+
// The prior bug/docs shortcut (skip spec → exec → qa) was removed because
|
|
382
|
+
// bug and docs issues often contain design decisions (scope boundaries,
|
|
383
|
+
// edge cases, test-strategy shifts) that benefit from a spec pass.
|
|
384
|
+
log(chalk.gray(` Running spec to determine workflow...`));
|
|
385
|
+
// RunRenderer (#618) owns spec progress via emitProgressLine + onProgress.
|
|
386
|
+
// The legacy PhaseSpinner produced duplicate lines for single-issue runs.
|
|
387
|
+
emitProgressLine(issueNumber, "spec", "start");
|
|
388
|
+
try {
|
|
389
|
+
onProgress?.(issueNumber, "spec", "start");
|
|
325
390
|
}
|
|
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");
|
|
391
|
+
catch {
|
|
392
|
+
/* progress errors must not halt */
|
|
393
|
+
}
|
|
394
|
+
// Track spec phase start in state
|
|
395
|
+
if (stateManager) {
|
|
340
396
|
try {
|
|
341
|
-
|
|
397
|
+
await stateManager.updatePhaseStatus(issueNumber, "spec", "in_progress");
|
|
342
398
|
}
|
|
343
399
|
catch {
|
|
344
|
-
|
|
400
|
+
// State tracking errors shouldn't stop execution
|
|
345
401
|
}
|
|
346
|
-
|
|
402
|
+
}
|
|
403
|
+
const specStartTime = new Date();
|
|
404
|
+
// Note: spec runs in main repo (not worktree) for planning
|
|
405
|
+
const specResult = await executePhaseWithRetry(issueNumber, "spec", withActivityHook(config, issueNumber, "spec", onProgress), resumeHandle, worktreePath, // Will be ignored for spec (non-isolated phase)
|
|
406
|
+
shutdownManager, phasePauseHandle);
|
|
407
|
+
const specEndTime = new Date();
|
|
408
|
+
if (specResult.resumeHandle) {
|
|
409
|
+
resumeHandle = specResult.resumeHandle;
|
|
410
|
+
// Persist resume token + originCwd for cross-process resume (#674).
|
|
347
411
|
if (stateManager) {
|
|
348
412
|
try {
|
|
349
|
-
await stateManager.
|
|
413
|
+
await stateManager.updateResumeHandle(issueNumber, specResult.resumeHandle);
|
|
350
414
|
}
|
|
351
415
|
catch {
|
|
352
416
|
// State tracking errors shouldn't stop execution
|
|
353
417
|
}
|
|
354
418
|
}
|
|
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
|
-
}
|
|
419
|
+
}
|
|
420
|
+
phaseResults.push(specResult);
|
|
421
|
+
specAlreadyRan = true;
|
|
422
|
+
// Emit completion/failure progress event (AC-8)
|
|
423
|
+
const specDurationSec = Math.round((specEndTime.getTime() - specStartTime.getTime()) / 1000);
|
|
424
|
+
if (specResult.success) {
|
|
425
|
+
const extra = { durationSeconds: specDurationSec };
|
|
426
|
+
emitProgressLine(issueNumber, "spec", "complete", extra);
|
|
427
|
+
try {
|
|
428
|
+
onProgress?.(issueNumber, "spec", "complete", extra);
|
|
371
429
|
}
|
|
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
|
-
}
|
|
430
|
+
catch {
|
|
431
|
+
/* progress errors must not halt */
|
|
385
432
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
catch {
|
|
393
|
-
/* progress errors must not halt */
|
|
394
|
-
}
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
const extra = { error: specResult.error ?? "unknown" };
|
|
436
|
+
emitProgressLine(issueNumber, "spec", "failed", extra);
|
|
437
|
+
try {
|
|
438
|
+
onProgress?.(issueNumber, "spec", "failed", extra);
|
|
395
439
|
}
|
|
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);
|
|
440
|
+
catch {
|
|
441
|
+
/* progress errors must not halt */
|
|
419
442
|
}
|
|
420
|
-
|
|
421
|
-
|
|
443
|
+
}
|
|
444
|
+
// Log spec phase result
|
|
445
|
+
// Note: Spec runs in main repo, not worktree, so no git diff stats
|
|
446
|
+
if (logWriter) {
|
|
447
|
+
// Build errorContext from captured stderr/stdout tails (#447)
|
|
448
|
+
let specErrorContext;
|
|
449
|
+
if (!specResult.success && specResult.stderrTail) {
|
|
450
|
+
const specError = classifyError(specResult.stderrTail ?? [], specResult.exitCode);
|
|
451
|
+
specErrorContext = {
|
|
452
|
+
stderrTail: specResult.stderrTail ?? [],
|
|
453
|
+
stdoutTail: specResult.stdoutTail ?? [],
|
|
454
|
+
exitCode: specResult.exitCode,
|
|
455
|
+
category: errorTypeToCategory(specError),
|
|
456
|
+
errorType: specError.name,
|
|
457
|
+
errorMetadata: specError.metadata,
|
|
458
|
+
isRetryable: specError.isRetryable,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
const phaseLog = createPhaseLogFromTiming("spec", issueNumber, specStartTime, specEndTime, specResult.success
|
|
462
|
+
? "success"
|
|
463
|
+
: specResult.error?.includes("Timeout")
|
|
464
|
+
? "timeout"
|
|
465
|
+
: "failure", { error: specResult.error, errorContext: specErrorContext });
|
|
466
|
+
logWriter.logPhase(phaseLog);
|
|
467
|
+
}
|
|
468
|
+
// Track spec phase completion in state
|
|
469
|
+
if (stateManager) {
|
|
470
|
+
try {
|
|
471
|
+
const phaseStatus = specResult.success ? "completed" : "failed";
|
|
472
|
+
await stateManager.updatePhaseStatus(issueNumber, "spec", phaseStatus, {
|
|
473
|
+
error: specResult.error,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// State tracking errors shouldn't stop execution
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (!specResult.success) {
|
|
481
|
+
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
482
|
+
// Archive relay state on early exit (spec failure).
|
|
483
|
+
if (relayActivation) {
|
|
422
484
|
try {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
485
|
+
await deactivateRelay(issueNumber, {
|
|
486
|
+
phase: "spec",
|
|
487
|
+
startedAt: relayActivation.startedAt,
|
|
488
|
+
worktreePath,
|
|
489
|
+
stateManager: stateManager ?? null,
|
|
426
490
|
});
|
|
427
491
|
}
|
|
428
492
|
catch {
|
|
429
|
-
|
|
493
|
+
/* swallow */
|
|
430
494
|
}
|
|
431
495
|
}
|
|
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
|
-
}
|
|
496
|
+
return {
|
|
497
|
+
issueNumber,
|
|
498
|
+
success: false,
|
|
499
|
+
phaseResults,
|
|
500
|
+
durationSeconds,
|
|
501
|
+
loopTriggered: false,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
// Parse recommended workflow from spec output
|
|
505
|
+
const parsedWorkflow = specResult.output
|
|
506
|
+
? parseRecommendedWorkflow(specResult.output)
|
|
507
|
+
: null;
|
|
508
|
+
if (parsedWorkflow) {
|
|
509
|
+
// Remove spec from phases since we already ran it
|
|
510
|
+
phases = parsedWorkflow.phases.filter((p) => p !== "spec");
|
|
511
|
+
detectedQualityLoop = parsedWorkflow.qualityLoop;
|
|
512
|
+
log(chalk.gray(` Spec recommends: ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
// Fall back to label-based detection
|
|
516
|
+
log(chalk.yellow(` Could not parse spec recommendation, using label-based detection`));
|
|
517
|
+
const detected = detectPhasesFromLabels(labels);
|
|
518
|
+
phases = detected.phases.filter((p) => p !== "spec");
|
|
519
|
+
detectedQualityLoop = detected.qualityLoop;
|
|
520
|
+
log(chalk.gray(` Fallback: ${phases.join(" → ")}`));
|
|
462
521
|
}
|
|
463
522
|
}
|
|
464
523
|
else {
|
|
@@ -497,6 +556,35 @@ export async function runIssueWithLogging(ctx) {
|
|
|
497
556
|
}
|
|
498
557
|
}
|
|
499
558
|
}
|
|
559
|
+
// Add security-review phase if requested (and spec was in the phases).
|
|
560
|
+
// Idempotent vs label-based auto-detection — only inserts if not present.
|
|
561
|
+
if (options.securityReview &&
|
|
562
|
+
(phases.includes("spec") || specAlreadyRan) &&
|
|
563
|
+
!phases.includes("security-review")) {
|
|
564
|
+
if (specAlreadyRan) {
|
|
565
|
+
phases.unshift("security-review");
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
const specIndex = phases.indexOf("spec");
|
|
569
|
+
if (specIndex !== -1) {
|
|
570
|
+
phases.splice(specIndex + 1, 0, "security-review");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
// #672 AC-2: surface the resolved phase pipeline to the renderer so it can
|
|
575
|
+
// seed pending cells for every phase before any one of them fires. This
|
|
576
|
+
// runs once per issue after all phase-list mutations (auto-detect, resume
|
|
577
|
+
// filter, testgen/security-review insertion). The full pipeline for the row
|
|
578
|
+
// is `spec` (if it already ran) plus the remaining `phases` array.
|
|
579
|
+
if (onPhasePlan) {
|
|
580
|
+
const fullPlan = specAlreadyRan ? ["spec", ...phases] : [...phases];
|
|
581
|
+
try {
|
|
582
|
+
onPhasePlan(issueNumber, fullPlan);
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
/* renderer wiring errors must not halt execution */
|
|
586
|
+
}
|
|
587
|
+
}
|
|
500
588
|
// Build per-issue config with issue type metadata for skill env propagation
|
|
501
589
|
const lowerLabelsForType = labels.map((l) => l.toLowerCase());
|
|
502
590
|
const issueIsDocs = lowerLabelsForType.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
|
|
@@ -514,27 +602,17 @@ export async function runIssueWithLogging(ctx) {
|
|
|
514
602
|
loopTriggered = true;
|
|
515
603
|
}
|
|
516
604
|
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
605
|
for (let phaseIdx = 0; phaseIdx < phases.length; phaseIdx++) {
|
|
522
606
|
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");
|
|
607
|
+
// RunRenderer (#618) owns phase progress via emitProgressLine + onProgress.
|
|
608
|
+
// #624 Item 3: surface the outer-loop iteration on every retried phase
|
|
609
|
+
// event so the renderer can label them `(attempt N/M)`. First-attempt
|
|
610
|
+
// events still get `iteration: 1` so the data flow is uniform; the
|
|
611
|
+
// renderer's `formatRetrySuffix` suppresses the suffix when iteration ≤ 1.
|
|
612
|
+
const phaseExtra = { iteration };
|
|
613
|
+
emitProgressLine(issueNumber, phase, "start", phaseExtra);
|
|
536
614
|
try {
|
|
537
|
-
onProgress?.(issueNumber, phase, "start");
|
|
615
|
+
onProgress?.(issueNumber, phase, "start", phaseExtra);
|
|
538
616
|
}
|
|
539
617
|
catch {
|
|
540
618
|
/* progress errors must not halt */
|
|
@@ -549,15 +627,14 @@ export async function runIssueWithLogging(ctx) {
|
|
|
549
627
|
}
|
|
550
628
|
}
|
|
551
629
|
const phaseStartTime = new Date();
|
|
552
|
-
const result = await executePhaseWithRetry(issueNumber, phase, issueConfig,
|
|
630
|
+
const result = await executePhaseWithRetry(issueNumber, phase, withActivityHook(issueConfig, issueNumber, phase, onProgress), resumeHandle, worktreePath, shutdownManager, phasePauseHandle);
|
|
553
631
|
const phaseEndTime = new Date();
|
|
554
|
-
// Capture
|
|
555
|
-
if (result.
|
|
556
|
-
|
|
557
|
-
// Update session ID in state for resume capability
|
|
632
|
+
// Capture resume handle for subsequent phases (#674).
|
|
633
|
+
if (result.resumeHandle) {
|
|
634
|
+
resumeHandle = result.resumeHandle;
|
|
558
635
|
if (stateManager) {
|
|
559
636
|
try {
|
|
560
|
-
await stateManager.
|
|
637
|
+
await stateManager.updateResumeHandle(issueNumber, result.resumeHandle);
|
|
561
638
|
}
|
|
562
639
|
catch {
|
|
563
640
|
// State tracking errors shouldn't stop execution
|
|
@@ -568,7 +645,7 @@ export async function runIssueWithLogging(ctx) {
|
|
|
568
645
|
// Emit completion/failure progress event (AC-8)
|
|
569
646
|
const phaseDurationSec = Math.round((phaseEndTime.getTime() - phaseStartTime.getTime()) / 1000);
|
|
570
647
|
if (result.success) {
|
|
571
|
-
const extra = { durationSeconds: phaseDurationSec };
|
|
648
|
+
const extra = { durationSeconds: phaseDurationSec, iteration };
|
|
572
649
|
emitProgressLine(issueNumber, phase, "complete", extra);
|
|
573
650
|
try {
|
|
574
651
|
onProgress?.(issueNumber, phase, "complete", extra);
|
|
@@ -578,7 +655,7 @@ export async function runIssueWithLogging(ctx) {
|
|
|
578
655
|
}
|
|
579
656
|
}
|
|
580
657
|
else {
|
|
581
|
-
const extra = { error: result.error ?? "unknown" };
|
|
658
|
+
const extra = { error: result.error ?? "unknown", iteration };
|
|
582
659
|
emitProgressLine(issueNumber, phase, "failed", extra);
|
|
583
660
|
try {
|
|
584
661
|
onProgress?.(issueNumber, phase, "failed", extra);
|
|
@@ -645,27 +722,18 @@ export async function runIssueWithLogging(ctx) {
|
|
|
645
722
|
}
|
|
646
723
|
}
|
|
647
724
|
if (result.success) {
|
|
648
|
-
|
|
725
|
+
// Phase succeeded — RunRenderer (#618) updates state via onProgress.
|
|
649
726
|
}
|
|
650
727
|
else {
|
|
651
|
-
phaseSpinner?.fail(result.error);
|
|
652
728
|
phasesFailed = true;
|
|
653
729
|
// If quality loop enabled, run loop phase to fix issues
|
|
654
730
|
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");
|
|
731
|
+
// #624 Item 3 (AC-3.3): the loop phase carries the current outer
|
|
732
|
+
// iteration so the live-zone status cell can show `loop N/M`.
|
|
733
|
+
const loopStartExtra = { iteration };
|
|
734
|
+
emitProgressLine(issueNumber, "loop", "start", loopStartExtra);
|
|
667
735
|
try {
|
|
668
|
-
onProgress?.(issueNumber, "loop", "start");
|
|
736
|
+
onProgress?.(issueNumber, "loop", "start", loopStartExtra);
|
|
669
737
|
}
|
|
670
738
|
catch {
|
|
671
739
|
/* progress errors must not halt */
|
|
@@ -680,13 +748,13 @@ export async function runIssueWithLogging(ctx) {
|
|
|
680
748
|
promptContext: buildLoopContext(result),
|
|
681
749
|
};
|
|
682
750
|
const loopStartTime = new Date();
|
|
683
|
-
const loopResult = await executePhaseWithRetry(issueNumber, "loop", loopConfig,
|
|
751
|
+
const loopResult = await executePhaseWithRetry(issueNumber, "loop", withActivityHook(loopConfig, issueNumber, "loop", onProgress), resumeHandle, worktreePath, shutdownManager, phasePauseHandle);
|
|
684
752
|
const loopEndTime = new Date();
|
|
685
753
|
phaseResults.push(loopResult);
|
|
686
754
|
// Emit loop completion/failure progress event (AC-8)
|
|
687
755
|
const loopDurationSec = Math.round((loopEndTime.getTime() - loopStartTime.getTime()) / 1000);
|
|
688
756
|
if (loopResult.success) {
|
|
689
|
-
const extra = { durationSeconds: loopDurationSec };
|
|
757
|
+
const extra = { durationSeconds: loopDurationSec, iteration };
|
|
690
758
|
emitProgressLine(issueNumber, "loop", "complete", extra);
|
|
691
759
|
try {
|
|
692
760
|
onProgress?.(issueNumber, "loop", "complete", extra);
|
|
@@ -696,7 +764,7 @@ export async function runIssueWithLogging(ctx) {
|
|
|
696
764
|
}
|
|
697
765
|
}
|
|
698
766
|
else {
|
|
699
|
-
const extra = { error: loopResult.error ?? "unknown" };
|
|
767
|
+
const extra = { error: loopResult.error ?? "unknown", iteration };
|
|
700
768
|
emitProgressLine(issueNumber, "loop", "failed", extra);
|
|
701
769
|
try {
|
|
702
770
|
onProgress?.(issueNumber, "loop", "failed", extra);
|
|
@@ -705,17 +773,13 @@ export async function runIssueWithLogging(ctx) {
|
|
|
705
773
|
/* progress errors must not halt */
|
|
706
774
|
}
|
|
707
775
|
}
|
|
708
|
-
if (loopResult.
|
|
709
|
-
|
|
776
|
+
if (loopResult.resumeHandle) {
|
|
777
|
+
resumeHandle = loopResult.resumeHandle;
|
|
710
778
|
}
|
|
711
779
|
if (loopResult.success) {
|
|
712
|
-
loopSpinner?.succeed();
|
|
713
780
|
// Continue to next iteration
|
|
714
781
|
break;
|
|
715
782
|
}
|
|
716
|
-
else {
|
|
717
|
-
loopSpinner?.fail(loopResult.error);
|
|
718
|
-
}
|
|
719
783
|
}
|
|
720
784
|
// Stop on first failure (if not in quality loop or loop failed)
|
|
721
785
|
break;
|
|
@@ -766,7 +830,15 @@ export async function runIssueWithLogging(ctx) {
|
|
|
766
830
|
let prUrl;
|
|
767
831
|
const shouldCreatePR = success && worktreePath && branch && !options.noPr;
|
|
768
832
|
if (shouldCreatePR) {
|
|
769
|
-
|
|
833
|
+
// #605: under --stacked, target predecessor branch (only for non-first,
|
|
834
|
+
// non-last issues). Last PR keeps `main` so partial progress can land.
|
|
835
|
+
const stackOptions = chain?.predecessorBranch || chain?.stackManifest
|
|
836
|
+
? {
|
|
837
|
+
prBase: chain.predecessorBranch,
|
|
838
|
+
stackManifest: chain.stackManifest,
|
|
839
|
+
}
|
|
840
|
+
: undefined;
|
|
841
|
+
const prResult = createPR(worktreePath, issueNumber, issueTitle, branch, config.verbose, labels, stackOptions);
|
|
770
842
|
if (prResult.success && prResult.prNumber && prResult.prUrl) {
|
|
771
843
|
prNumber = prResult.prNumber;
|
|
772
844
|
prUrl = prResult.prUrl;
|
|
@@ -784,6 +856,23 @@ export async function runIssueWithLogging(ctx) {
|
|
|
784
856
|
}
|
|
785
857
|
}
|
|
786
858
|
}
|
|
859
|
+
// Deactivate relay (#383) — archive inbox/outbox transcripts to
|
|
860
|
+
// .sequant/logs/relay/ before worktree teardown (AC-D2). Never throws.
|
|
861
|
+
if (relayActivation) {
|
|
862
|
+
try {
|
|
863
|
+
await deactivateRelay(issueNumber, {
|
|
864
|
+
phase: phaseResults[phaseResults.length - 1]?.phase ?? "exec",
|
|
865
|
+
startedAt: relayActivation.startedAt,
|
|
866
|
+
worktreePath,
|
|
867
|
+
stateManager: stateManager ?? null,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
catch (err) {
|
|
871
|
+
if (config.verbose) {
|
|
872
|
+
log(chalk.yellow(` ! Relay deactivation failed: ${err}`));
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
787
876
|
return {
|
|
788
877
|
issueNumber,
|
|
789
878
|
success,
|