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
|
@@ -8,82 +8,26 @@
|
|
|
8
8
|
* is agent-agnostic.
|
|
9
9
|
*/
|
|
10
10
|
import chalk from "chalk";
|
|
11
|
-
import { execSync } from "child_process";
|
|
11
|
+
import { execSync, execFileSync } from "child_process";
|
|
12
12
|
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
|
|
@@ -93,15 +37,67 @@ const ISOLATED_PHASES = [
|
|
|
93
37
|
const COLD_START_THRESHOLD_SECONDS = 60;
|
|
94
38
|
const COLD_START_MAX_RETRIES = 2;
|
|
95
39
|
/**
|
|
96
|
-
*
|
|
40
|
+
* Leading + trailing throttle. Fires the wrapped callback immediately on the
|
|
41
|
+
* first call, drops subsequent calls that arrive inside `intervalMs` but
|
|
42
|
+
* remembers the latest payload, and fires one final "trailing" call with that
|
|
43
|
+
* latest payload after the window closes. Used to bridge the agent driver's
|
|
44
|
+
* fine-grained `onOutput` stream (#543) to the TUI's `nowLine` without
|
|
45
|
+
* either burning the 10 Hz snapshot budget on every chunk or losing the last
|
|
46
|
+
* useful chunk before the agent goes idle.
|
|
47
|
+
*
|
|
48
|
+
* `cancel()` clears the pending timer + payload — call after the consuming
|
|
49
|
+
* phase finishes so a residual trailing fire doesn't outlive its phase
|
|
50
|
+
* context. (The orchestrator's stale-phase guard catches it anyway, but
|
|
51
|
+
* cleanup avoids holding even a no-op timer.)
|
|
52
|
+
*
|
|
53
|
+
* @internal Exported for testing only.
|
|
54
|
+
*/
|
|
55
|
+
export function createThrottledReporter(fn, intervalMs) {
|
|
56
|
+
let timer = null;
|
|
57
|
+
let pending = null;
|
|
58
|
+
const report = (text) => {
|
|
59
|
+
if (timer) {
|
|
60
|
+
// Inside the throttle window — stash the latest payload for the
|
|
61
|
+
// trailing fire and drop this call.
|
|
62
|
+
pending = text;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
fn(text);
|
|
66
|
+
timer = setTimeout(() => {
|
|
67
|
+
const trailing = pending;
|
|
68
|
+
pending = null;
|
|
69
|
+
timer = null;
|
|
70
|
+
if (trailing !== null)
|
|
71
|
+
report(trailing);
|
|
72
|
+
}, intervalMs);
|
|
73
|
+
timer.unref?.();
|
|
74
|
+
};
|
|
75
|
+
const cancel = () => {
|
|
76
|
+
if (timer)
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
timer = null;
|
|
79
|
+
pending = null;
|
|
80
|
+
};
|
|
81
|
+
return { report, cancel };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Spec-specific retry configuration. Sourced from the phase registry's
|
|
85
|
+
* `retryStrategy` field — `phase-registry.ts` is the source of truth.
|
|
86
|
+
*
|
|
97
87
|
* Spec failures have a higher failure rate (~8.6%) than other phases due to
|
|
98
88
|
* transient GitHub API issues and rate limits. One extra retry with backoff
|
|
99
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.
|
|
100
95
|
*/
|
|
96
|
+
const SPEC_RETRY_STRATEGY = phaseRegistry.get("spec").retryStrategy;
|
|
101
97
|
/** @internal Exported for testing only */
|
|
102
|
-
export const SPEC_RETRY_BACKOFF_MS = 5000;
|
|
98
|
+
export const SPEC_RETRY_BACKOFF_MS = SPEC_RETRY_STRATEGY?.backoffMs ?? 5000;
|
|
103
99
|
/** @internal Exported for testing only */
|
|
104
|
-
export const SPEC_EXTRA_RETRIES = 1;
|
|
100
|
+
export const SPEC_EXTRA_RETRIES = SPEC_RETRY_STRATEGY?.extraRetries ?? 1;
|
|
105
101
|
export function parseQaVerdict(output) {
|
|
106
102
|
if (!output)
|
|
107
103
|
return null;
|
|
@@ -218,29 +214,83 @@ export function formatDuration(seconds) {
|
|
|
218
214
|
const secs = seconds % 60;
|
|
219
215
|
return `${mins}m ${secs.toFixed(0)}s`;
|
|
220
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Resolve the base ref the zero-diff guard should compare against for
|
|
219
|
+
* this worktree.
|
|
220
|
+
*
|
|
221
|
+
* Reads `branch.<current>.sequantBase` — written by `scripts/new-feature.sh`
|
|
222
|
+
* when a worktree is created with `--base <branch>`. Returns `origin/<base>`
|
|
223
|
+
* (prepending `origin/` only when the recorded value does not already
|
|
224
|
+
* reference a remote). Falls back to `"origin/main"` on missing config,
|
|
225
|
+
* missing branch, or any git error — preserves the pre-#537 behavior
|
|
226
|
+
* for worktrees that predate this change or are managed outside
|
|
227
|
+
* `new-feature.sh`.
|
|
228
|
+
*
|
|
229
|
+
* Uses `execFileSync` (not `execSync`) so argv is passed directly to
|
|
230
|
+
* `execve` without shell interpretation — the recorded value originates
|
|
231
|
+
* from the user-supplied `--base` CLI flag, and shell-interpolating it
|
|
232
|
+
* would open a shell-injection vector. With `execFileSync`, a malicious
|
|
233
|
+
* value is at worst treated as an invalid revspec by git (triggering
|
|
234
|
+
* the fail-open path), never executed as shell.
|
|
235
|
+
*
|
|
236
|
+
* @internal Exported for testing only.
|
|
237
|
+
*/
|
|
238
|
+
export function resolveBaseRef(cwd) {
|
|
239
|
+
const fallback = "origin/main";
|
|
240
|
+
let branch;
|
|
241
|
+
try {
|
|
242
|
+
branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
243
|
+
cwd,
|
|
244
|
+
stdio: "pipe",
|
|
245
|
+
})
|
|
246
|
+
.toString()
|
|
247
|
+
.trim();
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return fallback;
|
|
251
|
+
}
|
|
252
|
+
// Guard against multi-line output (paranoid — should never happen) and
|
|
253
|
+
// the detached-HEAD case where we have no recorded base to look up.
|
|
254
|
+
if (!branch || branch === "HEAD" || branch.includes("\n"))
|
|
255
|
+
return fallback;
|
|
256
|
+
let recorded;
|
|
257
|
+
try {
|
|
258
|
+
recorded = execFileSync("git", ["config", "--get", `branch.${branch}.sequantBase`], { cwd, stdio: "pipe" })
|
|
259
|
+
.toString()
|
|
260
|
+
.trim();
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return fallback;
|
|
264
|
+
}
|
|
265
|
+
if (!recorded || recorded.includes("\n"))
|
|
266
|
+
return fallback;
|
|
267
|
+
return recorded.startsWith("origin/") ? recorded : `origin/${recorded}`;
|
|
268
|
+
}
|
|
221
269
|
/**
|
|
222
270
|
* Check whether the exec phase produced any changes in the worktree.
|
|
223
|
-
* Returns true if HEAD has commits unique to it relative to
|
|
224
|
-
* OR uncommitted work is present.
|
|
271
|
+
* Returns true if HEAD has commits unique to it relative to the resolved
|
|
272
|
+
* base ref (see {@link resolveBaseRef}) OR uncommitted work is present.
|
|
225
273
|
*
|
|
226
|
-
* Uses `git rev-list --count
|
|
227
|
-
* but not
|
|
228
|
-
* two-dot diff also fires in reverse when
|
|
274
|
+
* Uses `git rev-list --count <base>..HEAD` (commits reachable from HEAD
|
|
275
|
+
* but not the base) instead of `git diff <base>..HEAD`, because the
|
|
276
|
+
* two-dot diff also fires in reverse when the base has advanced past HEAD
|
|
229
277
|
* — on stale branches that would falsely report "has commits" even when the
|
|
230
278
|
* exec phase produced nothing, reintroducing the bug #534 is fixing.
|
|
231
279
|
*
|
|
280
|
+
* The base ref defaults to `origin/main` but is overridden to the worktree's
|
|
281
|
+
* recorded base (see #537) so zero-diff execs are still detected on
|
|
282
|
+
* custom-base worktrees (e.g. those created with `--base feature/epic`).
|
|
283
|
+
*
|
|
232
284
|
* Fails open (returns true) on git errors — a missing origin ref is better
|
|
233
285
|
* diagnosed as a real zero-diff run than as a false phase failure.
|
|
234
286
|
*
|
|
235
287
|
* @internal Exported for testing only.
|
|
236
288
|
*/
|
|
237
289
|
export function hasExecChanges(cwd) {
|
|
290
|
+
const baseRef = resolveBaseRef(cwd);
|
|
238
291
|
let commitsAhead;
|
|
239
292
|
try {
|
|
240
|
-
const count =
|
|
241
|
-
cwd,
|
|
242
|
-
stdio: "pipe",
|
|
243
|
-
})
|
|
293
|
+
const count = execFileSync("git", ["rev-list", "--count", `${baseRef}..HEAD`], { cwd, stdio: "pipe" })
|
|
244
294
|
.toString()
|
|
245
295
|
.trim();
|
|
246
296
|
commitsAhead = Number.parseInt(count, 10) > 0;
|
|
@@ -251,7 +301,10 @@ export function hasExecChanges(cwd) {
|
|
|
251
301
|
if (commitsAhead)
|
|
252
302
|
return true;
|
|
253
303
|
try {
|
|
254
|
-
const porcelain =
|
|
304
|
+
const porcelain = execFileSync("git", ["status", "--porcelain"], {
|
|
305
|
+
cwd,
|
|
306
|
+
stdio: "pipe",
|
|
307
|
+
})
|
|
255
308
|
.toString()
|
|
256
309
|
.trim();
|
|
257
310
|
return porcelain.length > 0;
|
|
@@ -276,6 +329,10 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
|
|
|
276
329
|
stdoutTail: agentResult.stdoutTail,
|
|
277
330
|
exitCode: agentResult.exitCode,
|
|
278
331
|
};
|
|
332
|
+
const resume = {
|
|
333
|
+
sessionId: agentResult.sessionId,
|
|
334
|
+
resumeHandle: agentResult.resumeHandle,
|
|
335
|
+
};
|
|
279
336
|
if (phase === "qa") {
|
|
280
337
|
const verdict = agentResult.output
|
|
281
338
|
? parseQaVerdict(agentResult.output)
|
|
@@ -291,7 +348,7 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
|
|
|
291
348
|
success: false,
|
|
292
349
|
durationSeconds,
|
|
293
350
|
error: `QA verdict: ${verdict}`,
|
|
294
|
-
|
|
351
|
+
...resume,
|
|
295
352
|
output: agentResult.output,
|
|
296
353
|
verdict,
|
|
297
354
|
summary,
|
|
@@ -305,7 +362,7 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
|
|
|
305
362
|
success: false,
|
|
306
363
|
durationSeconds,
|
|
307
364
|
error: "QA completed without a parseable verdict",
|
|
308
|
-
|
|
365
|
+
...resume,
|
|
309
366
|
output: agentResult.output,
|
|
310
367
|
summary,
|
|
311
368
|
...tails,
|
|
@@ -315,7 +372,7 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
|
|
|
315
372
|
phase,
|
|
316
373
|
success: true,
|
|
317
374
|
durationSeconds,
|
|
318
|
-
|
|
375
|
+
...resume,
|
|
319
376
|
output: agentResult.output,
|
|
320
377
|
verdict,
|
|
321
378
|
summary,
|
|
@@ -329,7 +386,7 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
|
|
|
329
386
|
success: false,
|
|
330
387
|
durationSeconds,
|
|
331
388
|
error: "exec produced no changes (no commits, no uncommitted work)",
|
|
332
|
-
|
|
389
|
+
...resume,
|
|
333
390
|
output: agentResult.output,
|
|
334
391
|
...tails,
|
|
335
392
|
};
|
|
@@ -338,7 +395,7 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
|
|
|
338
395
|
phase,
|
|
339
396
|
success: true,
|
|
340
397
|
durationSeconds,
|
|
341
|
-
|
|
398
|
+
...resume,
|
|
342
399
|
output: agentResult.output,
|
|
343
400
|
...tails,
|
|
344
401
|
};
|
|
@@ -352,8 +409,14 @@ export function mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds
|
|
|
352
409
|
* @internal Exported for testing only
|
|
353
410
|
*/
|
|
354
411
|
export async function getPhasePrompt(phase, issueNumber, agent, promptContext) {
|
|
355
|
-
const
|
|
356
|
-
|
|
412
|
+
const definition = phaseRegistry.get(phase);
|
|
413
|
+
// Non-claude drivers consult driverOverrides[<driver>] first; fall back to
|
|
414
|
+
// the default promptTemplate when no override is registered for the driver.
|
|
415
|
+
const driverPrompt = agent && agent !== "claude-code"
|
|
416
|
+
? definition.driverOverrides?.[agent]?.promptTemplate
|
|
417
|
+
: undefined;
|
|
418
|
+
const template = driverPrompt ?? definition.promptTemplate;
|
|
419
|
+
let basePrompt = template.replace(/\{issue\}/g, String(issueNumber));
|
|
357
420
|
// Append phase-specific context (e.g., QA findings for loop phase)
|
|
358
421
|
if (promptContext) {
|
|
359
422
|
basePrompt += `\n\n---\n\n${promptContext}`;
|
|
@@ -370,14 +433,14 @@ export async function getPhasePrompt(phase, issueNumber, agent, promptContext) {
|
|
|
370
433
|
/**
|
|
371
434
|
* Execute a single phase for an issue using the configured AgentDriver.
|
|
372
435
|
*/
|
|
373
|
-
async function executePhase(issueNumber, phase, config,
|
|
436
|
+
async function executePhase(issueNumber, phase, config, resumeHandle, worktreePath, shutdownManager, spinner) {
|
|
374
437
|
const startTime = Date.now();
|
|
375
438
|
const prompt = await getPhasePrompt(phase, issueNumber, config.agent, config.promptContext);
|
|
376
439
|
if (config.dryRun) {
|
|
377
440
|
// Dry run - show the prompt that would be sent, then return
|
|
378
441
|
if (config.verbose) {
|
|
379
|
-
|
|
380
|
-
|
|
442
|
+
bracketedConsoleLog(spinner, chalk.gray(` Would execute: /${phase} ${issueNumber}`));
|
|
443
|
+
bracketedConsoleLog(spinner, chalk.gray(` Prompt: ${prompt}`));
|
|
381
444
|
}
|
|
382
445
|
return {
|
|
383
446
|
phase,
|
|
@@ -387,13 +450,13 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
387
450
|
};
|
|
388
451
|
}
|
|
389
452
|
if (config.verbose) {
|
|
390
|
-
|
|
391
|
-
if (worktreePath &&
|
|
392
|
-
|
|
453
|
+
bracketedConsoleLog(spinner, chalk.gray(` Prompt: ${prompt}`));
|
|
454
|
+
if (worktreePath && phaseRequiresWorktree(phase)) {
|
|
455
|
+
bracketedConsoleLog(spinner, chalk.gray(` Worktree: ${worktreePath}`));
|
|
393
456
|
}
|
|
394
457
|
}
|
|
395
458
|
// Determine working directory and environment
|
|
396
|
-
const shouldUseWorktree = worktreePath &&
|
|
459
|
+
const shouldUseWorktree = worktreePath && phaseRequiresWorktree(phase);
|
|
397
460
|
const cwd = shouldUseWorktree ? worktreePath : process.cwd();
|
|
398
461
|
// Resolve file context for file-oriented drivers (e.g., Aider --file)
|
|
399
462
|
let files;
|
|
@@ -460,17 +523,57 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
460
523
|
if (config.isolateParallel) {
|
|
461
524
|
env.SEQUANT_ISOLATE_PARALLEL = "true";
|
|
462
525
|
}
|
|
526
|
+
// Activate interactive relay (#383) unless explicitly disabled.
|
|
527
|
+
// `relay-check.sh` (sourced from post-tool.sh) reads this env var on every
|
|
528
|
+
// tool call. Disabled by default in non-interactive scenarios — controlled
|
|
529
|
+
// via `settings.run.relay` (true by default).
|
|
530
|
+
if (config.relayEnabled) {
|
|
531
|
+
env.SEQUANT_RELAY = "true";
|
|
532
|
+
try {
|
|
533
|
+
const { resolveBundledFramePath } = await import("../relay/activation.js");
|
|
534
|
+
const framePath = resolveBundledFramePath();
|
|
535
|
+
if (framePath)
|
|
536
|
+
env.SEQUANT_RELAY_FRAME = framePath;
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
/* relay module unavailable — fall back to bash's search heuristic. */
|
|
540
|
+
}
|
|
541
|
+
}
|
|
463
542
|
// Track whether we're actively streaming verbose output
|
|
464
543
|
// Pausing spinner once per streaming session prevents truncation from rapid pause/resume cycles
|
|
465
544
|
// (Issue #283: ora's stop() clears the current line, which can truncate output when
|
|
466
545
|
// pause/resume is called for every chunk in rapid succession)
|
|
467
546
|
let verboseStreamingActive = false;
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
const
|
|
547
|
+
// Activity ping throttle (#543): the agent driver streams text in many small
|
|
548
|
+
// chunks; the TUI only polls at 10 Hz. Coalesce to ≤2 calls per ~100ms
|
|
549
|
+
// window (leading + trailing) so we don't burn the poll budget on snapshot
|
|
550
|
+
// churn but still surface the latest chunk before the agent goes idle.
|
|
551
|
+
const ACTIVITY_THROTTLE_MS = 100;
|
|
552
|
+
const onActivity = config.onActivity;
|
|
553
|
+
const throttle = onActivity
|
|
554
|
+
? createThrottledReporter((text) => {
|
|
555
|
+
try {
|
|
556
|
+
onActivity(text);
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
// Activity reporting must never disrupt the run.
|
|
560
|
+
}
|
|
561
|
+
}, ACTIVITY_THROTTLE_MS)
|
|
562
|
+
: undefined;
|
|
563
|
+
const reportActivity = throttle ? throttle.report : undefined;
|
|
564
|
+
// Resolve driver before the resume check — eligibility is now driver-owned
|
|
565
|
+
// (#674). Each driver's `canResume(handle, cwd)` enforces its own contract:
|
|
566
|
+
// Claude Code requires byte-equal cwd match (session storage is
|
|
567
|
+
// cwd-namespaced); Aider declines all resume (no session concept); Codex
|
|
568
|
+
// (when added in #497) folds in AGENTS.md parity. Replacing the prior
|
|
569
|
+
// `sessionId && !worktreePath` heuristic also unblocks same-worktree resume
|
|
570
|
+
// across phases.
|
|
571
|
+
const driver = getDriver(config.agent, {
|
|
572
|
+
aiderSettings: config.aiderSettings,
|
|
573
|
+
});
|
|
574
|
+
const eligibleHandle = resumeHandle && driver.canResume(resumeHandle, cwd)
|
|
575
|
+
? resumeHandle
|
|
576
|
+
: undefined;
|
|
474
577
|
// Build AgentExecutionConfig for the driver
|
|
475
578
|
const agentConfig = {
|
|
476
579
|
cwd,
|
|
@@ -479,15 +582,20 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
479
582
|
phaseTimeout: config.phaseTimeout,
|
|
480
583
|
verbose: config.verbose,
|
|
481
584
|
mcp: config.mcp,
|
|
482
|
-
|
|
585
|
+
resumeHandle: eligibleHandle,
|
|
586
|
+
sessionId: eligibleHandle?.token,
|
|
483
587
|
files,
|
|
484
|
-
onOutput: config.verbose
|
|
588
|
+
onOutput: config.verbose || reportActivity
|
|
485
589
|
? (text) => {
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
|
|
590
|
+
if (config.verbose) {
|
|
591
|
+
if (!verboseStreamingActive) {
|
|
592
|
+
spinner?.pause();
|
|
593
|
+
verboseStreamingActive = true;
|
|
594
|
+
}
|
|
595
|
+
// eslint-disable-next-line no-restricted-syntax -- spinner is paused above; verbose subprocess streaming bypasses log-update intentionally.
|
|
596
|
+
process.stdout.write(chalk.gray(text));
|
|
489
597
|
}
|
|
490
|
-
|
|
598
|
+
reportActivity?.(text);
|
|
491
599
|
}
|
|
492
600
|
: undefined,
|
|
493
601
|
onStderr: config.verbose
|
|
@@ -496,15 +604,16 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
496
604
|
spinner?.pause();
|
|
497
605
|
verboseStreamingActive = true;
|
|
498
606
|
}
|
|
607
|
+
// eslint-disable-next-line no-restricted-syntax -- spinner is paused above; verbose subprocess streaming bypasses log-update intentionally.
|
|
499
608
|
process.stderr.write(chalk.red(data));
|
|
500
609
|
}
|
|
501
610
|
: undefined,
|
|
502
611
|
};
|
|
503
|
-
// Resolve driver from config or default
|
|
504
|
-
const driver = getDriver(config.agent, {
|
|
505
|
-
aiderSettings: config.aiderSettings,
|
|
506
|
-
});
|
|
507
612
|
const agentResult = await driver.executePhase(prompt, agentConfig);
|
|
613
|
+
// Cancel any pending trailing activity fire — phase is done; the
|
|
614
|
+
// orchestrator's stale-phase guard would no-op a late call anyway, but
|
|
615
|
+
// clearing the timer is cheaper than letting it elapse.
|
|
616
|
+
throttle?.cancel();
|
|
508
617
|
// Resume spinner after execution completes (if we paused it)
|
|
509
618
|
if (verboseStreamingActive) {
|
|
510
619
|
spinner?.resume();
|
|
@@ -524,6 +633,7 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
524
633
|
durationSeconds,
|
|
525
634
|
error: agentResult.error,
|
|
526
635
|
sessionId: agentResult.sessionId,
|
|
636
|
+
resumeHandle: agentResult.resumeHandle,
|
|
527
637
|
stderrTail: agentResult.stderrTail,
|
|
528
638
|
stdoutTail: agentResult.stdoutTail,
|
|
529
639
|
exitCode: agentResult.exitCode,
|
|
@@ -543,24 +653,28 @@ async function executePhase(issueNumber, phase, config, sessionId, worktreePath,
|
|
|
543
653
|
/**
|
|
544
654
|
* @internal Exported for testing only
|
|
545
655
|
*/
|
|
546
|
-
export async function executePhaseWithRetry(issueNumber, phase, config,
|
|
656
|
+
export async function executePhaseWithRetry(issueNumber, phase, config, resumeHandle, worktreePath, shutdownManager, spinner,
|
|
547
657
|
/** @internal Injected for testing — defaults to module-level executePhase */
|
|
548
658
|
executePhaseFn = executePhase,
|
|
549
659
|
/** @internal Injected for testing — defaults to setTimeout-based delay */
|
|
550
660
|
delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
551
661
|
// Skip retry logic if explicitly disabled
|
|
552
662
|
if (config.retry === false) {
|
|
553
|
-
return executePhaseFn(issueNumber, phase, config,
|
|
663
|
+
return executePhaseFn(issueNumber, phase, config, resumeHandle, worktreePath, shutdownManager, spinner);
|
|
554
664
|
}
|
|
555
|
-
// Skip cold-start retries for
|
|
556
|
-
//
|
|
557
|
-
// Failures at 47-51s are genuine skill failures, not cold-start
|
|
558
|
-
// Without this guard, 2 cold-start retries + 1 MCP fallback = 3 wasted
|
|
559
|
-
|
|
665
|
+
// Skip cold-start retries for phases registered with `retryStrategy.maxRetries: 0`.
|
|
666
|
+
// `loop` is the canonical user (#488) — it's always a re-run after a failed QA,
|
|
667
|
+
// never a first boot. Failures at 47-51s are genuine skill failures, not cold-start
|
|
668
|
+
// issues. Without this guard, 2 cold-start retries + 1 MCP fallback = 3 wasted
|
|
669
|
+
// spawns per loop. Sourcing the decision from the registry makes the rule
|
|
670
|
+
// data-driven — any future phase registered with `maxRetries: 0` inherits the
|
|
671
|
+
// same behavior without a code change here.
|
|
672
|
+
const skipColdStartRetry = phaseRegistry.has(phase) &&
|
|
673
|
+
phaseRegistry.get(phase).retryStrategy?.maxRetries === 0;
|
|
560
674
|
let lastResult;
|
|
561
675
|
if (skipColdStartRetry) {
|
|
562
676
|
// Single attempt — no cold-start retry loop
|
|
563
|
-
lastResult = await executePhaseFn(issueNumber, phase, config,
|
|
677
|
+
lastResult = await executePhaseFn(issueNumber, phase, config, resumeHandle, worktreePath, shutdownManager, spinner);
|
|
564
678
|
if (lastResult.success) {
|
|
565
679
|
return lastResult;
|
|
566
680
|
}
|
|
@@ -568,7 +682,7 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
|
568
682
|
else {
|
|
569
683
|
// Phase 1: Cold-start retry attempts (with MCP enabled if configured)
|
|
570
684
|
for (let attempt = 0; attempt <= COLD_START_MAX_RETRIES; attempt++) {
|
|
571
|
-
lastResult = await executePhaseFn(issueNumber, phase, config,
|
|
685
|
+
lastResult = await executePhaseFn(issueNumber, phase, config, resumeHandle, worktreePath, shutdownManager, spinner);
|
|
572
686
|
const duration = lastResult.durationSeconds ?? 0;
|
|
573
687
|
// Success → return immediately
|
|
574
688
|
if (lastResult.success) {
|
|
@@ -584,7 +698,7 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
|
584
698
|
const label = typedError instanceof ApiError
|
|
585
699
|
? `API error (status ${typedError.metadata.statusCode ?? "unknown"})`
|
|
586
700
|
: typedError.name;
|
|
587
|
-
|
|
701
|
+
bracketedConsoleLog(spinner, chalk.yellow(`\n ⟳ Retryable error: ${label}, retrying... (attempt ${attempt + 2}/${COLD_START_MAX_RETRIES + 1})`));
|
|
588
702
|
}
|
|
589
703
|
continue;
|
|
590
704
|
}
|
|
@@ -596,7 +710,7 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
|
596
710
|
// Cold-start failure detected — retry
|
|
597
711
|
if (attempt < COLD_START_MAX_RETRIES) {
|
|
598
712
|
if (config.verbose) {
|
|
599
|
-
|
|
713
|
+
bracketedConsoleLog(spinner, chalk.yellow(`\n ⟳ Cold-start failure detected (${duration.toFixed(1)}s), retrying... (attempt ${attempt + 2}/${COLD_START_MAX_RETRIES + 1})`));
|
|
600
714
|
}
|
|
601
715
|
}
|
|
602
716
|
}
|
|
@@ -607,15 +721,15 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
|
607
721
|
// This handles npx-based MCP servers that fail on first run due to cold-cache issues.
|
|
608
722
|
// Skip for `loop` phase — MCP is never the cause of loop failures (#488).
|
|
609
723
|
if (config.mcp && !lastResult.success && !skipColdStartRetry) {
|
|
610
|
-
|
|
724
|
+
bracketedConsoleLog(spinner, chalk.yellow(`\n ! Phase failed with MCP enabled, retrying without MCP...`));
|
|
611
725
|
// Create config copy with MCP disabled
|
|
612
726
|
const configWithoutMcp = {
|
|
613
727
|
...config,
|
|
614
728
|
mcp: false,
|
|
615
729
|
};
|
|
616
|
-
const retryResult = await executePhaseFn(issueNumber, phase, configWithoutMcp,
|
|
730
|
+
const retryResult = await executePhaseFn(issueNumber, phase, configWithoutMcp, resumeHandle, worktreePath, shutdownManager, spinner);
|
|
617
731
|
if (retryResult.success) {
|
|
618
|
-
|
|
732
|
+
bracketedConsoleLog(spinner, chalk.green(` ✓ Phase succeeded without MCP (MCP cold-start issue detected)`));
|
|
619
733
|
return retryResult;
|
|
620
734
|
}
|
|
621
735
|
// Update lastResult for Phase 3 (spec retry)
|
|
@@ -632,11 +746,11 @@ delayFn = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
|
632
746
|
// than other phases (~8.6%), so one extra retry with backoff recovers most cases.
|
|
633
747
|
if (phase === "spec" && !lastResult.success) {
|
|
634
748
|
for (let i = 0; i < SPEC_EXTRA_RETRIES; i++) {
|
|
635
|
-
|
|
749
|
+
bracketedConsoleLog(spinner, chalk.yellow(`\n ⟳ Spec phase failed, retrying with ${SPEC_RETRY_BACKOFF_MS}ms backoff... (spec retry ${i + 1}/${SPEC_EXTRA_RETRIES})`));
|
|
636
750
|
await delayFn(SPEC_RETRY_BACKOFF_MS);
|
|
637
|
-
const specRetryResult = await executePhaseFn(issueNumber, phase, config,
|
|
751
|
+
const specRetryResult = await executePhaseFn(issueNumber, phase, config, resumeHandle, worktreePath, shutdownManager, spinner);
|
|
638
752
|
if (specRetryResult.success) {
|
|
639
|
-
|
|
753
|
+
bracketedConsoleLog(spinner, chalk.green(` ✓ Spec phase succeeded on retry`));
|
|
640
754
|
return specRetryResult;
|
|
641
755
|
}
|
|
642
756
|
lastResult = specRetryResult;
|
|
@@ -14,29 +14,38 @@ import type { Phase } from "./types.js";
|
|
|
14
14
|
*/
|
|
15
15
|
interface PhaseMapperOptions {
|
|
16
16
|
testgen?: boolean;
|
|
17
|
+
securityReview?: boolean;
|
|
17
18
|
}
|
|
18
19
|
/**
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
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.
|
|
24
26
|
*/
|
|
25
27
|
export declare const BUG_LABELS: string[];
|
|
26
28
|
/**
|
|
27
|
-
* Documentation labels
|
|
29
|
+
* Documentation labels (used for issueType propagation and downstream metadata).
|
|
30
|
+
*
|
|
31
|
+
* Issue-type metadata — NOT phase-trigger rules. See BUG_LABELS comment.
|
|
28
32
|
*/
|
|
29
33
|
export declare const DOCS_LABELS: string[];
|
|
30
34
|
/**
|
|
31
|
-
* 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.
|
|
32
40
|
*/
|
|
33
41
|
export declare const COMPLEX_LABELS: string[];
|
|
34
42
|
/**
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*
|
|
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).
|
|
40
49
|
*/
|
|
41
50
|
export declare function detectPhasesFromLabels(labels: string[]): {
|
|
42
51
|
phases: Phase[];
|
|
@@ -55,7 +64,12 @@ export declare function parseRecommendedWorkflow(output: string): {
|
|
|
55
64
|
qualityLoop: boolean;
|
|
56
65
|
} | null;
|
|
57
66
|
/**
|
|
58
|
-
* 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`).
|
|
59
73
|
*/
|
|
60
74
|
export declare function hasUILabels(labels: string[]): boolean;
|
|
61
75
|
/**
|