sequant 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +125 -160
- package/dist/bin/cli.js +59 -4
- package/dist/dashboard/server.js +1 -0
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +2 -2
- package/dist/marketplace/external_plugins/sequant/README.md +6 -3
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +92 -0
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +18 -9
- package/dist/marketplace/external_plugins/sequant/hooks/relay-check.sh +107 -0
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/behavior-rule-detection.md +205 -0
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +21 -8
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +302 -86
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +109 -0
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +141 -22
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +83 -78
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +377 -137
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +28 -0
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +621 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +741 -232
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +47 -1
- package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +12 -6
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +217 -964
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/parallel-groups.md +7 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/quality-checklist.md +75 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +4 -2
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +0 -27
- package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +24 -44
- package/dist/src/commands/abort.d.ts +36 -0
- package/dist/src/commands/abort.js +138 -0
- package/dist/src/commands/prompt.d.ts +7 -0
- package/dist/src/commands/prompt.js +101 -7
- package/dist/src/commands/ready-tui-adapter.d.ts +59 -0
- package/dist/src/commands/ready-tui-adapter.js +130 -0
- package/dist/src/commands/ready.d.ts +49 -0
- package/dist/src/commands/ready.js +243 -0
- package/dist/src/commands/run-progress.d.ts +11 -1
- package/dist/src/commands/run-progress.js +20 -3
- package/dist/src/commands/run.js +12 -2
- package/dist/src/commands/status.js +4 -0
- package/dist/src/commands/watch.d.ts +2 -0
- package/dist/src/commands/watch.js +67 -3
- package/dist/src/lib/assess-collision-detect.js +1 -1
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +39 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +34 -2
- package/dist/src/lib/cli-ui/run-renderer.js +250 -33
- 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/merge-check/types.js +1 -1
- package/dist/src/lib/relay/archive.js +6 -0
- package/dist/src/lib/relay/types.d.ts +2 -0
- package/dist/src/lib/relay/types.js +9 -0
- package/dist/src/lib/settings.d.ts +34 -0
- package/dist/src/lib/settings.js +23 -1
- package/dist/src/lib/workflow/batch-executor.js +34 -18
- 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/notice.d.ts +32 -0
- package/dist/src/lib/workflow/notice.js +38 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +9 -21
- package/dist/src/lib/workflow/phase-executor.js +105 -117
- package/dist/src/lib/workflow/phase-mapper.d.ts +26 -13
- package/dist/src/lib/workflow/phase-mapper.js +55 -33
- 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 +6 -0
- package/dist/src/lib/workflow/platforms/github.js +17 -0
- package/dist/src/lib/workflow/ready-gate.d.ts +155 -0
- package/dist/src/lib/workflow/ready-gate.js +374 -0
- package/dist/src/lib/workflow/reconcile.js +6 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts +5 -55
- package/dist/src/lib/workflow/run-orchestrator.d.ts +32 -2
- package/dist/src/lib/workflow/run-orchestrator.js +125 -11
- package/dist/src/lib/workflow/state-manager.d.ts +19 -1
- package/dist/src/lib/workflow/state-manager.js +27 -1
- package/dist/src/lib/workflow/state-schema.d.ts +23 -35
- package/dist/src/lib/workflow/state-schema.js +29 -3
- package/dist/src/lib/workflow/types.d.ts +74 -15
- package/dist/src/lib/workflow/types.js +18 -13
- package/dist/src/ui/tui/App.js +8 -2
- package/dist/src/ui/tui/IssueBox.js +3 -4
- package/dist/src/ui/tui/index.d.ts +13 -4
- package/dist/src/ui/tui/index.js +19 -5
- package/dist/src/ui/tui/row-cap.d.ts +51 -0
- package/dist/src/ui/tui/row-cap.js +76 -0
- package/dist/src/ui/tui/teardown.d.ts +20 -0
- package/dist/src/ui/tui/teardown.js +29 -0
- package/dist/src/ui/tui/theme.d.ts +3 -0
- package/dist/src/ui/tui/theme.js +3 -0
- package/package.json +23 -11
- package/templates/hooks/post-tool.sh +81 -0
- package/templates/skills/assess/SKILL.md +28 -28
- package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
- package/templates/skills/qa/SKILL.md +5 -2
- package/templates/skills/setup/SKILL.md +6 -6
|
@@ -13,77 +13,21 @@ import { readAgentsMd } from "../agents-md.js";
|
|
|
13
13
|
import { getDriver } from "./drivers/index.js";
|
|
14
14
|
import { classifyError } from "./error-classifier.js";
|
|
15
15
|
import { ApiError } from "../errors.js";
|
|
16
|
+
import { phaseRegistry } from "./phase-registry.js";
|
|
17
|
+
import { bracketedConsoleLog } from "./notice.js";
|
|
16
18
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
testgen: "Generate test stubs for GitHub issue #{issue} based on the specification. Run the /testgen {issue} workflow.",
|
|
24
|
-
exec: "Implement the feature for GitHub issue #{issue} following the spec. Run the /exec {issue} workflow.",
|
|
25
|
-
test: "Execute structured browser-based testing for GitHub issue #{issue}. Run the /test {issue} workflow.",
|
|
26
|
-
verify: "Verify the implementation for GitHub issue #{issue} by running commands and capturing output. Run the /verify {issue} workflow.",
|
|
27
|
-
qa: "Review the implementation for GitHub issue #{issue} against acceptance criteria. Run the /qa {issue} workflow.",
|
|
28
|
-
loop: "Parse test/QA findings for GitHub issue #{issue} and iterate until quality gates pass. Run the /loop {issue} workflow.",
|
|
29
|
-
merger: "Integrate and merge completed worktrees for GitHub issue #{issue}. Run the /merger {issue} workflow.",
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Self-contained prompts for non-Claude agents (Aider, Codex, etc.).
|
|
33
|
-
* These agents don't have a skill system, so prompts must include
|
|
34
|
-
* full instructions rather than skill invocations.
|
|
35
|
-
*/
|
|
36
|
-
const AIDER_PHASE_PROMPTS = {
|
|
37
|
-
spec: `Read GitHub issue #{issue} using 'gh issue view #{issue}'.
|
|
38
|
-
Create a spec comment on the issue with:
|
|
39
|
-
1. Implementation plan
|
|
40
|
-
2. Acceptance criteria as a checklist
|
|
41
|
-
3. Risk assessment
|
|
42
|
-
Post the comment using 'gh issue comment #{issue} --body "<comment>"'.`,
|
|
43
|
-
"security-review": `Perform a security review for GitHub issue #{issue}.
|
|
44
|
-
Read the issue with 'gh issue view #{issue}'.
|
|
45
|
-
Check for auth, permissions, injection, and sensitive data issues.
|
|
46
|
-
Post findings as a comment on the issue.`,
|
|
47
|
-
testgen: `Generate test stubs for GitHub issue #{issue}.
|
|
48
|
-
Read the spec comments on the issue with 'gh issue view #{issue} --comments'.
|
|
49
|
-
Create test files with describe/it blocks covering the acceptance criteria.
|
|
50
|
-
Use the project's existing test framework.`,
|
|
51
|
-
exec: `Implement the feature described in GitHub issue #{issue}.
|
|
52
|
-
Read the issue and any spec comments with 'gh issue view #{issue} --comments'.
|
|
53
|
-
Follow the implementation plan from the spec.
|
|
54
|
-
Write tests for new functionality.
|
|
55
|
-
Ensure the build passes with 'npm test' and 'npm run build'.`,
|
|
56
|
-
test: `Test the implementation for GitHub issue #{issue}.
|
|
57
|
-
Run 'npm test' and verify all tests pass.
|
|
58
|
-
Check for edge cases and error handling.`,
|
|
59
|
-
verify: `Verify the implementation for GitHub issue #{issue}.
|
|
60
|
-
Run relevant commands and capture their output for review.`,
|
|
61
|
-
qa: `Review the changes for GitHub issue #{issue}.
|
|
62
|
-
Run 'npm test' and 'npm run build' to verify everything works.
|
|
63
|
-
Check each acceptance criterion from the issue comments.
|
|
64
|
-
Output a verdict: READY_FOR_MERGE, AC_MET_BUT_NOT_A_PLUS, or AC_NOT_MET
|
|
65
|
-
with format "### Verdict: <VERDICT>" followed by an explanation.`,
|
|
66
|
-
loop: `Review test and QA findings for GitHub issue #{issue}.
|
|
67
|
-
Fix any issues identified in the QA feedback.
|
|
68
|
-
Re-run 'npm test' and 'npm run build' until all quality gates pass.`,
|
|
69
|
-
merger: `Integrate and merge completed worktrees for GitHub issue #{issue}.
|
|
70
|
-
Ensure all branches are up to date and merge cleanly.`,
|
|
71
|
-
};
|
|
72
|
-
/**
|
|
73
|
-
* Phases that require worktree isolation.
|
|
74
|
-
* Only `spec` runs in the main repo (planning-only, no file changes).
|
|
75
|
-
* All other phases must run in the worktree because:
|
|
76
|
-
* 1. They need to read/modify the worktree code
|
|
77
|
-
* 2. Resuming a session created in a different cwd crashes the SDK
|
|
19
|
+
* Determine whether a phase's session must run inside the issue worktree.
|
|
20
|
+
*
|
|
21
|
+
* Sourced from `phaseRegistry.get(phase).requiresWorktree` — replaces the
|
|
22
|
+
* previous hardcoded `ISOLATED_PHASES` array. Phases must:
|
|
23
|
+
* 1. Read/modify worktree code
|
|
24
|
+
* 2. Resume a session from the same cwd it was created in (SDK constraint)
|
|
78
25
|
*/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"qa",
|
|
85
|
-
"loop",
|
|
86
|
-
];
|
|
26
|
+
function phaseRequiresWorktree(phase) {
|
|
27
|
+
return phaseRegistry.has(phase)
|
|
28
|
+
? phaseRegistry.get(phase).requiresWorktree
|
|
29
|
+
: false;
|
|
30
|
+
}
|
|
87
31
|
/**
|
|
88
32
|
* Cold-start retry threshold in seconds.
|
|
89
33
|
* Failures under this duration are likely Claude Code subprocess initialization
|
|
@@ -137,15 +81,23 @@ export function createThrottledReporter(fn, intervalMs) {
|
|
|
137
81
|
return { report, cancel };
|
|
138
82
|
}
|
|
139
83
|
/**
|
|
140
|
-
* Spec-specific retry configuration.
|
|
84
|
+
* Spec-specific retry configuration. Sourced from the phase registry's
|
|
85
|
+
* `retryStrategy` field — `phase-registry.ts` is the source of truth.
|
|
86
|
+
*
|
|
141
87
|
* Spec failures have a higher failure rate (~8.6%) than other phases due to
|
|
142
88
|
* transient GitHub API issues and rate limits. One extra retry with backoff
|
|
143
89
|
* recovers most of these without user intervention.
|
|
90
|
+
*
|
|
91
|
+
* Fallback literals (5000 / 1) match the legacy hardcoded values and only
|
|
92
|
+
* fire if the spec registration is removed or its `retryStrategy` is unset,
|
|
93
|
+
* which would be a misconfiguration. Tests pin these at 5000 / 1, so any
|
|
94
|
+
* drift surfaces immediately.
|
|
144
95
|
*/
|
|
96
|
+
const SPEC_RETRY_STRATEGY = phaseRegistry.get("spec").retryStrategy;
|
|
145
97
|
/** @internal Exported for testing only */
|
|
146
|
-
export const SPEC_RETRY_BACKOFF_MS = 5000;
|
|
98
|
+
export const SPEC_RETRY_BACKOFF_MS = SPEC_RETRY_STRATEGY?.backoffMs ?? 5000;
|
|
147
99
|
/** @internal Exported for testing only */
|
|
148
|
-
export const SPEC_EXTRA_RETRIES = 1;
|
|
100
|
+
export const SPEC_EXTRA_RETRIES = SPEC_RETRY_STRATEGY?.extraRetries ?? 1;
|
|
149
101
|
export function parseQaVerdict(output) {
|
|
150
102
|
if (!output)
|
|
151
103
|
return null;
|
|
@@ -154,8 +106,16 @@ export function parseQaVerdict(output) {
|
|
|
154
106
|
// - "**Verdict:** X" (bold label with colon inside)
|
|
155
107
|
// - "**Verdict:** **X**" (bold label and bold value)
|
|
156
108
|
// - "Verdict: X" (plain)
|
|
157
|
-
//
|
|
158
|
-
|
|
109
|
+
// - "Verdict: ✅ X" (emoji-prefixed value — QA agents commonly write this)
|
|
110
|
+
// The gap between "Verdict:" and the token tolerates any run of
|
|
111
|
+
// non-alphanumeric characters (emoji, ✅/❌/⚠️, asterisks, whitespace). A
|
|
112
|
+
// negated ASCII class (not an emoji literal class) keeps this ReDoS-safe and
|
|
113
|
+
// avoids the no-misleading-character-class lint, matching parseQaSummary's
|
|
114
|
+
// approach below. Without this, `Verdict: ✅ READY_FOR_MERGE` parsed as null
|
|
115
|
+
// and a genuine PASS was recorded as "completed without a parseable verdict"
|
|
116
|
+
// (live repro: `sequant run 687 --phases exec,qa`, 2026-06-01).
|
|
117
|
+
// Case insensitive, handles optional markdown formatting.
|
|
118
|
+
const verdictMatch = output.match(/(?:###?\s*)?(?:\*\*)?Verdict:?[^A-Za-z0-9_]*(READY_FOR_MERGE|AC_MET_BUT_NOT_A_PLUS|AC_NOT_MET|NEEDS_VERIFICATION)\*?\*?/i);
|
|
159
119
|
if (!verdictMatch)
|
|
160
120
|
return null;
|
|
161
121
|
// Normalize to uppercase with underscores
|
|
@@ -377,6 +337,10 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
|
|
|
377
337
|
stdoutTail: agentResult.stdoutTail,
|
|
378
338
|
exitCode: agentResult.exitCode,
|
|
379
339
|
};
|
|
340
|
+
const resume = {
|
|
341
|
+
sessionId: agentResult.sessionId,
|
|
342
|
+
resumeHandle: agentResult.resumeHandle,
|
|
343
|
+
};
|
|
380
344
|
if (phase === "qa") {
|
|
381
345
|
const verdict = agentResult.output
|
|
382
346
|
? parseQaVerdict(agentResult.output)
|
|
@@ -392,7 +356,7 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
|
|
|
392
356
|
success: false,
|
|
393
357
|
durationSeconds,
|
|
394
358
|
error: `QA verdict: ${verdict}`,
|
|
395
|
-
|
|
359
|
+
...resume,
|
|
396
360
|
output: agentResult.output,
|
|
397
361
|
verdict,
|
|
398
362
|
summary,
|
|
@@ -406,7 +370,7 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
|
|
|
406
370
|
success: false,
|
|
407
371
|
durationSeconds,
|
|
408
372
|
error: "QA completed without a parseable verdict",
|
|
409
|
-
|
|
373
|
+
...resume,
|
|
410
374
|
output: agentResult.output,
|
|
411
375
|
summary,
|
|
412
376
|
...tails,
|
|
@@ -416,7 +380,7 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
|
|
|
416
380
|
phase,
|
|
417
381
|
success: true,
|
|
418
382
|
durationSeconds,
|
|
419
|
-
|
|
383
|
+
...resume,
|
|
420
384
|
output: agentResult.output,
|
|
421
385
|
verdict,
|
|
422
386
|
summary,
|
|
@@ -430,7 +394,7 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
|
|
|
430
394
|
success: false,
|
|
431
395
|
durationSeconds,
|
|
432
396
|
error: "exec produced no changes (no commits, no uncommitted work)",
|
|
433
|
-
|
|
397
|
+
...resume,
|
|
434
398
|
output: agentResult.output,
|
|
435
399
|
...tails,
|
|
436
400
|
};
|
|
@@ -439,7 +403,7 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
|
|
|
439
403
|
phase,
|
|
440
404
|
success: true,
|
|
441
405
|
durationSeconds,
|
|
442
|
-
|
|
406
|
+
...resume,
|
|
443
407
|
output: agentResult.output,
|
|
444
408
|
...tails,
|
|
445
409
|
};
|
|
@@ -453,8 +417,14 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
|
|
|
453
417
|
* @internal Exported for testing only
|
|
454
418
|
*/
|
|
455
419
|
export async function getPhasePrompt(phase, issueNumber, agent, promptContext) {
|
|
456
|
-
const
|
|
457
|
-
|
|
420
|
+
const definition = phaseRegistry.get(phase);
|
|
421
|
+
// Non-claude drivers consult driverOverrides[<driver>] first; fall back to
|
|
422
|
+
// the default promptTemplate when no override is registered for the driver.
|
|
423
|
+
const driverPrompt = agent && agent !== "claude-code"
|
|
424
|
+
? definition.driverOverrides?.[agent]?.promptTemplate
|
|
425
|
+
: undefined;
|
|
426
|
+
const template = driverPrompt ?? definition.promptTemplate;
|
|
427
|
+
let basePrompt = template.replace(/\{issue\}/g, String(issueNumber));
|
|
458
428
|
// Append phase-specific context (e.g., QA findings for loop phase)
|
|
459
429
|
if (promptContext) {
|
|
460
430
|
basePrompt += `\n\n---\n\n${promptContext}`;
|
|
@@ -471,14 +441,14 @@ export async function getPhasePrompt(phase, issueNumber, agent, promptContext) {
|
|
|
471
441
|
/**
|
|
472
442
|
* Execute a single phase for an issue using the configured AgentDriver.
|
|
473
443
|
*/
|
|
474
|
-
async function executePhase(issueNumber, phase, config,
|
|
444
|
+
async function executePhase(issueNumber, phase, config, resumeHandle, worktreePath, shutdownManager, spinner) {
|
|
475
445
|
const startTime = Date.now();
|
|
476
446
|
const prompt = await getPhasePrompt(phase, issueNumber, config.agent, config.promptContext);
|
|
477
447
|
if (config.dryRun) {
|
|
478
448
|
// Dry run - show the prompt that would be sent, then return
|
|
479
449
|
if (config.verbose) {
|
|
480
|
-
|
|
481
|
-
|
|
450
|
+
bracketedConsoleLog(spinner, chalk.gray(` Would execute: /${phase} ${issueNumber}`));
|
|
451
|
+
bracketedConsoleLog(spinner, chalk.gray(` Prompt: ${prompt}`));
|
|
482
452
|
}
|
|
483
453
|
return {
|
|
484
454
|
phase,
|
|
@@ -488,13 +458,13 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
488
458
|
};
|
|
489
459
|
}
|
|
490
460
|
if (config.verbose) {
|
|
491
|
-
|
|
492
|
-
if (worktreePath &&
|
|
493
|
-
|
|
461
|
+
bracketedConsoleLog(spinner, chalk.gray(` Prompt: ${prompt}`));
|
|
462
|
+
if (worktreePath && phaseRequiresWorktree(phase)) {
|
|
463
|
+
bracketedConsoleLog(spinner, chalk.gray(` Worktree: ${worktreePath}`));
|
|
494
464
|
}
|
|
495
465
|
}
|
|
496
466
|
// Determine working directory and environment
|
|
497
|
-
const shouldUseWorktree = worktreePath &&
|
|
467
|
+
const shouldUseWorktree = worktreePath && phaseRequiresWorktree(phase);
|
|
498
468
|
const cwd = shouldUseWorktree ? worktreePath : process.cwd();
|
|
499
469
|
// Resolve file context for file-oriented drivers (e.g., Aider --file)
|
|
500
470
|
let files;
|
|
@@ -546,6 +516,13 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
546
516
|
// Skills can check these to skip redundant pre-flight checks
|
|
547
517
|
env.SEQUANT_ORCHESTRATOR = "sequant-run";
|
|
548
518
|
env.SEQUANT_PHASE = phase;
|
|
519
|
+
// #683: force full-weight QA. `sequant ready` sets config.fullQa so its QA
|
|
520
|
+
// pass runs the standalone branch-freshness / process-state pre-flight checks
|
|
521
|
+
// even though SEQUANT_ORCHESTRATOR is set unconditionally above. Scoped to the
|
|
522
|
+
// qa phase — the loop/exec phases don't have a git-trust skip to override.
|
|
523
|
+
if (config.fullQa && phase === "qa") {
|
|
524
|
+
env.SEQUANT_FULL_QA = "1";
|
|
525
|
+
}
|
|
549
526
|
// Propagate issue type for skills to adapt behavior (e.g., lighter QA for docs)
|
|
550
527
|
if (config.issueType) {
|
|
551
528
|
env.SEQUANT_ISSUE_TYPE = config.issueType;
|
|
@@ -599,12 +576,19 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
599
576
|
}, ACTIVITY_THROTTLE_MS)
|
|
600
577
|
: undefined;
|
|
601
578
|
const reportActivity = throttle ? throttle.report : undefined;
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
|
|
579
|
+
// Resolve driver before the resume check — eligibility is now driver-owned
|
|
580
|
+
// (#674). Each driver's `canResume(handle, cwd)` enforces its own contract:
|
|
581
|
+
// Claude Code requires byte-equal cwd match (session storage is
|
|
582
|
+
// cwd-namespaced); Aider declines all resume (no session concept); Codex
|
|
583
|
+
// (when added in #497) folds in AGENTS.md parity. Replacing the prior
|
|
584
|
+
// `sessionId && !worktreePath` heuristic also unblocks same-worktree resume
|
|
585
|
+
// across phases.
|
|
586
|
+
const driver = getDriver(config.agent, {
|
|
587
|
+
aiderSettings: config.aiderSettings,
|
|
588
|
+
});
|
|
589
|
+
const eligibleHandle = resumeHandle && driver.canResume(resumeHandle, cwd)
|
|
590
|
+
? resumeHandle
|
|
591
|
+
: undefined;
|
|
608
592
|
// Build AgentExecutionConfig for the driver
|
|
609
593
|
const agentConfig = {
|
|
610
594
|
cwd,
|
|
@@ -613,7 +597,8 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
613
597
|
phaseTimeout: config.phaseTimeout,
|
|
614
598
|
verbose: config.verbose,
|
|
615
599
|
mcp: config.mcp,
|
|
616
|
-
|
|
600
|
+
resumeHandle: eligibleHandle,
|
|
601
|
+
sessionId: eligibleHandle?.token,
|
|
617
602
|
files,
|
|
618
603
|
onOutput: config.verbose || reportActivity
|
|
619
604
|
? (text) => {
|
|
@@ -622,6 +607,7 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
622
607
|
spinner?.pause();
|
|
623
608
|
verboseStreamingActive = true;
|
|
624
609
|
}
|
|
610
|
+
// eslint-disable-next-line no-restricted-syntax -- spinner is paused above; verbose subprocess streaming bypasses log-update intentionally.
|
|
625
611
|
process.stdout.write(chalk.gray(text));
|
|
626
612
|
}
|
|
627
613
|
reportActivity?.(text);
|
|
@@ -633,14 +619,11 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
633
619
|
spinner?.pause();
|
|
634
620
|
verboseStreamingActive = true;
|
|
635
621
|
}
|
|
622
|
+
// eslint-disable-next-line no-restricted-syntax -- spinner is paused above; verbose subprocess streaming bypasses log-update intentionally.
|
|
636
623
|
process.stderr.write(chalk.red(data));
|
|
637
624
|
}
|
|
638
625
|
: undefined,
|
|
639
626
|
};
|
|
640
|
-
// Resolve driver from config or default
|
|
641
|
-
const driver = getDriver(config.agent, {
|
|
642
|
-
aiderSettings: config.aiderSettings,
|
|
643
|
-
});
|
|
644
627
|
const agentResult = await driver.executePhase(prompt, agentConfig);
|
|
645
628
|
// Cancel any pending trailing activity fire — phase is done; the
|
|
646
629
|
// orchestrator's stale-phase guard would no-op a late call anyway, but
|
|
@@ -665,6 +648,7 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
665
648
|
durationSeconds,
|
|
666
649
|
error: agentResult.error,
|
|
667
650
|
sessionId: agentResult.sessionId,
|
|
651
|
+
resumeHandle: agentResult.resumeHandle,
|
|
668
652
|
stderrTail: agentResult.stderrTail,
|
|
669
653
|
stdoutTail: agentResult.stdoutTail,
|
|
670
654
|
exitCode: agentResult.exitCode,
|
|
@@ -684,24 +668,28 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
684
668
|
/**
|
|
685
669
|
* @internal Exported for testing only
|
|
686
670
|
*/
|
|
687
|
-
export async function executePhaseWithRetry(issueNumber, phase, config,
|
|
671
|
+
export async function executePhaseWithRetry(issueNumber, phase, config, resumeHandle, worktreePath, shutdownManager, spinner,
|
|
688
672
|
/** @internal Injected for testing — defaults to module-level executePhase */
|
|
689
673
|
executePhaseFn = executePhase,
|
|
690
674
|
/** @internal Injected for testing — defaults to setTimeout-based delay */
|
|
691
675
|
delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
692
676
|
// Skip retry logic if explicitly disabled
|
|
693
677
|
if (config.retry === false) {
|
|
694
|
-
return executePhaseFn(issueNumber, phase, config,
|
|
695
|
-
}
|
|
696
|
-
// Skip cold-start retries for
|
|
697
|
-
//
|
|
698
|
-
// Failures at 47-51s are genuine skill failures, not cold-start
|
|
699
|
-
// Without this guard, 2 cold-start retries + 1 MCP fallback = 3 wasted
|
|
700
|
-
|
|
678
|
+
return executePhaseFn(issueNumber, phase, config, resumeHandle, worktreePath, shutdownManager, spinner);
|
|
679
|
+
}
|
|
680
|
+
// Skip cold-start retries for phases registered with `retryStrategy.maxRetries: 0`.
|
|
681
|
+
// `loop` is the canonical user (#488) — it's always a re-run after a failed QA,
|
|
682
|
+
// never a first boot. Failures at 47-51s are genuine skill failures, not cold-start
|
|
683
|
+
// issues. Without this guard, 2 cold-start retries + 1 MCP fallback = 3 wasted
|
|
684
|
+
// spawns per loop. Sourcing the decision from the registry makes the rule
|
|
685
|
+
// data-driven — any future phase registered with `maxRetries: 0` inherits the
|
|
686
|
+
// same behavior without a code change here.
|
|
687
|
+
const skipColdStartRetry = phaseRegistry.has(phase) &&
|
|
688
|
+
phaseRegistry.get(phase).retryStrategy?.maxRetries === 0;
|
|
701
689
|
let lastResult;
|
|
702
690
|
if (skipColdStartRetry) {
|
|
703
691
|
// Single attempt — no cold-start retry loop
|
|
704
|
-
lastResult = await executePhaseFn(issueNumber, phase, config,
|
|
692
|
+
lastResult = await executePhaseFn(issueNumber, phase, config, resumeHandle, worktreePath, shutdownManager, spinner);
|
|
705
693
|
if (lastResult.success) {
|
|
706
694
|
return lastResult;
|
|
707
695
|
}
|
|
@@ -709,7 +697,7 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
|
709
697
|
else {
|
|
710
698
|
// Phase 1: Cold-start retry attempts (with MCP enabled if configured)
|
|
711
699
|
for (let attempt = 0; attempt <= COLD_START_MAX_RETRIES; attempt++) {
|
|
712
|
-
lastResult = await executePhaseFn(issueNumber, phase, config,
|
|
700
|
+
lastResult = await executePhaseFn(issueNumber, phase, config, resumeHandle, worktreePath, shutdownManager, spinner);
|
|
713
701
|
const duration = lastResult.durationSeconds ?? 0;
|
|
714
702
|
// Success → return immediately
|
|
715
703
|
if (lastResult.success) {
|
|
@@ -725,7 +713,7 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
|
725
713
|
const label = typedError instanceof ApiError
|
|
726
714
|
? `API error (status ${typedError.metadata.statusCode ?? "unknown"})`
|
|
727
715
|
: typedError.name;
|
|
728
|
-
|
|
716
|
+
bracketedConsoleLog(spinner, chalk.yellow(`\n ⟳ Retryable error: ${label}, retrying... (attempt ${attempt + 2}/${COLD_START_MAX_RETRIES + 1})`));
|
|
729
717
|
}
|
|
730
718
|
continue;
|
|
731
719
|
}
|
|
@@ -737,7 +725,7 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
|
737
725
|
// Cold-start failure detected — retry
|
|
738
726
|
if (attempt < COLD_START_MAX_RETRIES) {
|
|
739
727
|
if (config.verbose) {
|
|
740
|
-
|
|
728
|
+
bracketedConsoleLog(spinner, chalk.yellow(`\n ⟳ Cold-start failure detected (${duration.toFixed(1)}s), retrying... (attempt ${attempt + 2}/${COLD_START_MAX_RETRIES + 1})`));
|
|
741
729
|
}
|
|
742
730
|
}
|
|
743
731
|
}
|
|
@@ -748,15 +736,15 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
|
748
736
|
// This handles npx-based MCP servers that fail on first run due to cold-cache issues.
|
|
749
737
|
// Skip for `loop` phase — MCP is never the cause of loop failures (#488).
|
|
750
738
|
if (config.mcp && !lastResult.success && !skipColdStartRetry) {
|
|
751
|
-
|
|
739
|
+
bracketedConsoleLog(spinner, chalk.yellow(`\n ! Phase failed with MCP enabled, retrying without MCP...`));
|
|
752
740
|
// Create config copy with MCP disabled
|
|
753
741
|
const configWithoutMcp = {
|
|
754
742
|
...config,
|
|
755
743
|
mcp: false,
|
|
756
744
|
};
|
|
757
|
-
const retryResult = await executePhaseFn(issueNumber, phase, configWithoutMcp,
|
|
745
|
+
const retryResult = await executePhaseFn(issueNumber, phase, configWithoutMcp, resumeHandle, worktreePath, shutdownManager, spinner);
|
|
758
746
|
if (retryResult.success) {
|
|
759
|
-
|
|
747
|
+
bracketedConsoleLog(spinner, chalk.green(` ✓ Phase succeeded without MCP (MCP cold-start issue detected)`));
|
|
760
748
|
return retryResult;
|
|
761
749
|
}
|
|
762
750
|
// Update lastResult for Phase 3 (spec retry)
|
|
@@ -773,11 +761,11 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
|
773
761
|
// than other phases (~8.6%), so one extra retry with backoff recovers most cases.
|
|
774
762
|
if (phase === "spec" && !lastResult.success) {
|
|
775
763
|
for (let i = 0; i < SPEC_EXTRA_RETRIES; i++) {
|
|
776
|
-
|
|
764
|
+
bracketedConsoleLog(spinner, chalk.yellow(`\n ⟳ Spec phase failed, retrying with ${SPEC_RETRY_BACKOFF_MS}ms backoff... (spec retry ${i + 1}/${SPEC_EXTRA_RETRIES})`));
|
|
777
765
|
await delayFn(SPEC_RETRY_BACKOFF_MS);
|
|
778
|
-
const specRetryResult = await executePhaseFn(issueNumber, phase, config,
|
|
766
|
+
const specRetryResult = await executePhaseFn(issueNumber, phase, config, resumeHandle, worktreePath, shutdownManager, spinner);
|
|
779
767
|
if (specRetryResult.success) {
|
|
780
|
-
|
|
768
|
+
bracketedConsoleLog(spinner, chalk.green(` ✓ Spec phase succeeded on retry`));
|
|
781
769
|
return specRetryResult;
|
|
782
770
|
}
|
|
783
771
|
lastResult = specRetryResult;
|
|
@@ -17,27 +17,35 @@ interface PhaseMapperOptions {
|
|
|
17
17
|
securityReview?: boolean;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*
|
|
20
|
+
* Bug-related labels (used by downstream metadata consumers).
|
|
21
|
+
*
|
|
22
|
+
* Issue-type metadata — NOT phase-trigger rules. The registry-driven
|
|
23
|
+
* `detectPhasesFromLabels` below does not consult this list. It stays
|
|
24
|
+
* here because `batch-executor.ts` and other modules read it for
|
|
25
|
+
* `issueType` propagation and similar non-phase concerns.
|
|
25
26
|
*/
|
|
26
27
|
export declare const BUG_LABELS: string[];
|
|
27
28
|
/**
|
|
28
|
-
* Documentation labels (used for issueType propagation and downstream metadata)
|
|
29
|
+
* Documentation labels (used for issueType propagation and downstream metadata).
|
|
30
|
+
*
|
|
31
|
+
* Issue-type metadata — NOT phase-trigger rules. See BUG_LABELS comment.
|
|
29
32
|
*/
|
|
30
33
|
export declare const DOCS_LABELS: string[];
|
|
31
34
|
/**
|
|
32
|
-
* Complex labels that enable quality loop
|
|
35
|
+
* Complex labels that enable quality loop.
|
|
36
|
+
*
|
|
37
|
+
* Quality-loop trigger — NOT a phase-trigger rule (does not add the loop
|
|
38
|
+
* *phase*; only flips the `qualityLoop` flag on the run config). Kept
|
|
39
|
+
* out of the phase registry by design.
|
|
33
40
|
*/
|
|
34
41
|
export declare const COMPLEX_LABELS: string[];
|
|
35
42
|
/**
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
*
|
|
43
|
+
* Detect phases based on issue labels (like /assess logic).
|
|
44
|
+
*
|
|
45
|
+
* Label → phase mapping now lives in `PhaseDefinition.detect.labels`. Only
|
|
46
|
+
* the *insertion position* of detected phases remains baked in here, because
|
|
47
|
+
* pipeline ordering depends on the phase's role (security-review goes after
|
|
48
|
+
* spec; test goes before qa).
|
|
41
49
|
*/
|
|
42
50
|
export declare function detectPhasesFromLabels(labels: string[]): {
|
|
43
51
|
phases: Phase[];
|
|
@@ -56,7 +64,12 @@ export declare function parseRecommendedWorkflow(output: string): {
|
|
|
56
64
|
qualityLoop: boolean;
|
|
57
65
|
} | null;
|
|
58
66
|
/**
|
|
59
|
-
* Check if an issue has UI-related labels
|
|
67
|
+
* Check if an issue has UI-related labels.
|
|
68
|
+
*
|
|
69
|
+
* Sources the label list from the `test` phase's `detect.labels` entry in
|
|
70
|
+
* the registry — same data as `detectPhasesFromLabels` consults, just
|
|
71
|
+
* exposed as a boolean for callers that only need the yes/no answer
|
|
72
|
+
* (e.g. test phase insertion in `determinePhasesForIssue`).
|
|
60
73
|
*/
|
|
61
74
|
export declare function hasUILabels(labels: string[]): boolean;
|
|
62
75
|
/**
|
|
@@ -7,43 +7,62 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module phase-mapper
|
|
9
9
|
*/
|
|
10
|
+
import { phaseRegistry } from "./phase-registry.js";
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*
|
|
12
|
+
* Bug-related labels (used by downstream metadata consumers).
|
|
13
|
+
*
|
|
14
|
+
* Issue-type metadata — NOT phase-trigger rules. The registry-driven
|
|
15
|
+
* `detectPhasesFromLabels` below does not consult this list. It stays
|
|
16
|
+
* here because `batch-executor.ts` and other modules read it for
|
|
17
|
+
* `issueType` propagation and similar non-phase concerns.
|
|
16
18
|
*/
|
|
17
19
|
export const BUG_LABELS = ["bug", "fix", "hotfix", "patch"];
|
|
18
20
|
/**
|
|
19
|
-
* Documentation labels (used for issueType propagation and downstream metadata)
|
|
21
|
+
* Documentation labels (used for issueType propagation and downstream metadata).
|
|
22
|
+
*
|
|
23
|
+
* Issue-type metadata — NOT phase-trigger rules. See BUG_LABELS comment.
|
|
20
24
|
*/
|
|
21
25
|
export const DOCS_LABELS = ["docs", "documentation", "readme"];
|
|
22
26
|
/**
|
|
23
|
-
* Complex labels that enable quality loop
|
|
27
|
+
* Complex labels that enable quality loop.
|
|
28
|
+
*
|
|
29
|
+
* Quality-loop trigger — NOT a phase-trigger rule (does not add the loop
|
|
30
|
+
* *phase*; only flips the `qualityLoop` flag on the run config). Kept
|
|
31
|
+
* out of the phase registry by design.
|
|
24
32
|
*/
|
|
25
33
|
export const COMPLEX_LABELS = ["complex", "refactor", "breaking", "major"];
|
|
26
34
|
/**
|
|
27
|
-
*
|
|
35
|
+
* Look up label-based detect rules from the registry, returning the set
|
|
36
|
+
* of phases whose `detect.labels` intersect the issue's labels. Comparison
|
|
37
|
+
* is case-insensitive (labels lowercased at the call site).
|
|
28
38
|
*/
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
function detectPhasesFromRegistry(lowerLabels) {
|
|
40
|
+
const matched = new Set();
|
|
41
|
+
for (const def of phaseRegistry.list()) {
|
|
42
|
+
const triggers = def.detect?.labels;
|
|
43
|
+
if (!triggers || triggers.length === 0)
|
|
44
|
+
continue;
|
|
45
|
+
const hit = triggers.some((t) => lowerLabels.includes(t.toLowerCase()));
|
|
46
|
+
if (hit)
|
|
47
|
+
matched.add(def.name);
|
|
48
|
+
}
|
|
49
|
+
return matched;
|
|
50
|
+
}
|
|
36
51
|
/**
|
|
37
|
-
* Detect phases based on issue labels (like /assess logic)
|
|
52
|
+
* Detect phases based on issue labels (like /assess logic).
|
|
53
|
+
*
|
|
54
|
+
* Label → phase mapping now lives in `PhaseDefinition.detect.labels`. Only
|
|
55
|
+
* the *insertion position* of detected phases remains baked in here, because
|
|
56
|
+
* pipeline ordering depends on the phase's role (security-review goes after
|
|
57
|
+
* spec; test goes before qa).
|
|
38
58
|
*/
|
|
39
59
|
export function detectPhasesFromLabels(labels) {
|
|
40
60
|
const lowerLabels = labels.map((l) => l.toLowerCase());
|
|
41
|
-
//
|
|
42
|
-
const isUI = lowerLabels.some((label) => UI_LABELS.some((uiLabel) => label === uiLabel));
|
|
43
|
-
// Check for complex labels → enable quality loop
|
|
61
|
+
// Quality loop is a registry-independent label trigger (see COMPLEX_LABELS).
|
|
44
62
|
const isComplex = lowerLabels.some((label) => COMPLEX_LABELS.some((complexLabel) => label === complexLabel));
|
|
45
|
-
|
|
46
|
-
const
|
|
63
|
+
const matched = detectPhasesFromRegistry(lowerLabels);
|
|
64
|
+
const isUI = matched.has("test");
|
|
65
|
+
const isSecurity = matched.has("security-review");
|
|
47
66
|
// Build phase list — spec is always included by default (#533).
|
|
48
67
|
// Bug/docs labels no longer short-circuit spec; downstream consumers
|
|
49
68
|
// (e.g. `issueType: "docs"` propagation) still use DOCS_LABELS for
|
|
@@ -78,18 +97,10 @@ export function parseRecommendedWorkflow(output) {
|
|
|
78
97
|
.split(/\s*→\s*|\s*->\s*|\s*,\s*/)
|
|
79
98
|
.map((p) => p.trim().toLowerCase())
|
|
80
99
|
.filter((p) => p.length > 0);
|
|
81
|
-
// Validate
|
|
100
|
+
// Validate against the registry — accepts any registered phase.
|
|
82
101
|
const validPhases = [];
|
|
83
102
|
for (const name of phaseNames) {
|
|
84
|
-
if (
|
|
85
|
-
"spec",
|
|
86
|
-
"security-review",
|
|
87
|
-
"testgen",
|
|
88
|
-
"exec",
|
|
89
|
-
"test",
|
|
90
|
-
"qa",
|
|
91
|
-
"loop",
|
|
92
|
-
].includes(name)) {
|
|
103
|
+
if (phaseRegistry.has(name)) {
|
|
93
104
|
validPhases.push(name);
|
|
94
105
|
}
|
|
95
106
|
}
|
|
@@ -104,10 +115,21 @@ export function parseRecommendedWorkflow(output) {
|
|
|
104
115
|
return { phases: validPhases, qualityLoop };
|
|
105
116
|
}
|
|
106
117
|
/**
|
|
107
|
-
* Check if an issue has UI-related labels
|
|
118
|
+
* Check if an issue has UI-related labels.
|
|
119
|
+
*
|
|
120
|
+
* Sources the label list from the `test` phase's `detect.labels` entry in
|
|
121
|
+
* the registry — same data as `detectPhasesFromLabels` consults, just
|
|
122
|
+
* exposed as a boolean for callers that only need the yes/no answer
|
|
123
|
+
* (e.g. test phase insertion in `determinePhasesForIssue`).
|
|
108
124
|
*/
|
|
109
125
|
export function hasUILabels(labels) {
|
|
110
|
-
|
|
126
|
+
const testTriggers = phaseRegistry.has("test")
|
|
127
|
+
? (phaseRegistry.get("test").detect?.labels ?? [])
|
|
128
|
+
: [];
|
|
129
|
+
if (testTriggers.length === 0)
|
|
130
|
+
return false;
|
|
131
|
+
const lowered = new Set(testTriggers.map((t) => t.toLowerCase()));
|
|
132
|
+
return labels.some((label) => lowered.has(label.toLowerCase()));
|
|
111
133
|
}
|
|
112
134
|
/**
|
|
113
135
|
* Determine phases to run based on options and issue labels
|