sequant 2.4.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 -163
- package/dist/bin/cli.js +13 -0
- 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/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/status.js +4 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +7 -1
- package/dist/src/lib/cli-ui/run-renderer.js +28 -28
- package/dist/src/lib/settings.d.ts +34 -0
- package/dist/src/lib/settings.js +23 -1
- package/dist/src/lib/workflow/phase-executor.js +17 -2
- 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/state-schema.d.ts +3 -0
- package/dist/src/lib/workflow/state-schema.js +1 -0
- package/dist/src/lib/workflow/types.d.ts +9 -0
- 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 +19 -8
- package/templates/skills/qa/SKILL.md +5 -2
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sequant ready <issue> — post-resolve A+ QA gate (#683)
|
|
3
|
+
*
|
|
4
|
+
* Runs a full-weight `qa → loop → qa` pipeline against an issue's existing
|
|
5
|
+
* worktree, reproducing the maintainer's manual fresh-session A+ pass
|
|
6
|
+
* deterministically, then STOPS at a human merge gate. It NEVER merges.
|
|
7
|
+
*
|
|
8
|
+
* Gate policy (flag > settings.ready.policy > "ac"):
|
|
9
|
+
* - `ac` (default) — loop until ACs are objectively met; report (not fix)
|
|
10
|
+
* quality gaps and Non-Goal-touching findings.
|
|
11
|
+
* - `a-plus` (opt-in) — loop toward READY_FOR_MERGE, auto-fixing quality gaps.
|
|
12
|
+
*
|
|
13
|
+
* Terminates in `waiting_for_human_merge` (when ready) or `blocked` (needs
|
|
14
|
+
* human / no implementation), persists that state, and emits a structured gap
|
|
15
|
+
* report. See `src/lib/workflow/ready-gate.ts` for the engine.
|
|
16
|
+
*/
|
|
17
|
+
import { ui, colors } from "../lib/cli-ui.js";
|
|
18
|
+
import { getSettings } from "../lib/settings.js";
|
|
19
|
+
import { listWorktrees } from "../lib/workflow/worktree-manager.js";
|
|
20
|
+
import { GitHubProvider } from "../lib/workflow/platforms/github.js";
|
|
21
|
+
import { getStateManager } from "../lib/workflow/state-manager.js";
|
|
22
|
+
import { executePhaseWithRetry } from "../lib/workflow/phase-executor.js";
|
|
23
|
+
import { buildProgressWiring } from "./run-progress.js";
|
|
24
|
+
import { ReadySnapshotAdapter } from "./ready-tui-adapter.js";
|
|
25
|
+
import { runReadyGate, parseNonGoals, } from "../lib/workflow/ready-gate.js";
|
|
26
|
+
/**
|
|
27
|
+
* Exit code from a ready result.
|
|
28
|
+
* - 0: ready (awaiting human merge)
|
|
29
|
+
* - 1: not ready — needs human intervention (budget/iterations/stagnation)
|
|
30
|
+
* - 2: not ready — no implementation (#534) or hard error
|
|
31
|
+
*/
|
|
32
|
+
export function getReadyExitCode(result) {
|
|
33
|
+
if (result.ready)
|
|
34
|
+
return 0;
|
|
35
|
+
if (result.reason === "NO_IMPLEMENTATION")
|
|
36
|
+
return 2;
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the gate policy: `--policy` flag > settings.ready.policy > "ac".
|
|
41
|
+
* Invalid flag values fall back to the settings/default value.
|
|
42
|
+
*
|
|
43
|
+
* @internal Exported for testing only.
|
|
44
|
+
*/
|
|
45
|
+
export function resolvePolicy(flag, settingsPolicy) {
|
|
46
|
+
if (flag === "ac" || flag === "a-plus")
|
|
47
|
+
return flag;
|
|
48
|
+
return settingsPolicy;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Locate the worktree path for an issue from `git worktree list`.
|
|
52
|
+
*
|
|
53
|
+
* @internal Exported for testing only.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveWorktreePath(issueNumber) {
|
|
56
|
+
const match = listWorktrees().find((w) => w.issue === issueNumber);
|
|
57
|
+
return match?.path ?? null;
|
|
58
|
+
}
|
|
59
|
+
export async function readyCommand(issueArg, options) {
|
|
60
|
+
const issueNumber = parseInt(issueArg, 10);
|
|
61
|
+
if (isNaN(issueNumber)) {
|
|
62
|
+
if (options.json) {
|
|
63
|
+
console.log(JSON.stringify({ error: `Invalid issue: ${issueArg}` }));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.error(ui.errorBox("Invalid issue", `"${issueArg}" is not a number`));
|
|
67
|
+
}
|
|
68
|
+
process.exitCode = 2;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const settings = await getSettings();
|
|
72
|
+
const policy = resolvePolicy(options.policy, settings.ready.policy);
|
|
73
|
+
const maxIterations = typeof options.maxIterations === "number" && options.maxIterations > 0
|
|
74
|
+
? options.maxIterations
|
|
75
|
+
: settings.run.maxIterations;
|
|
76
|
+
const tokenBudget = typeof options.budget === "number" && options.budget > 0
|
|
77
|
+
? options.budget
|
|
78
|
+
: undefined;
|
|
79
|
+
const phaseTimeout = typeof options.timeout === "number" && options.timeout > 0
|
|
80
|
+
? options.timeout
|
|
81
|
+
: settings.run.timeout;
|
|
82
|
+
const mcp = options.mcp !== false;
|
|
83
|
+
// Resolve the issue's existing worktree (reuses run/state worktree infra).
|
|
84
|
+
const worktreePath = resolveWorktreePath(issueNumber);
|
|
85
|
+
if (!worktreePath) {
|
|
86
|
+
const msg = `No worktree found for issue #${issueNumber}. ` +
|
|
87
|
+
`Run \`sequant run ${issueNumber}\` first (or create one with ./scripts/new-feature.sh).`;
|
|
88
|
+
if (options.json) {
|
|
89
|
+
console.log(JSON.stringify({ error: msg }));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.error(ui.errorBox("No worktree", msg));
|
|
93
|
+
}
|
|
94
|
+
process.exitCode = 2;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Parse the issue's Non-Goals so `ac` mode can mark touching findings
|
|
98
|
+
// report-only. Best-effort: an unavailable body just yields no Non-Goals.
|
|
99
|
+
const gh = new GitHubProvider();
|
|
100
|
+
const body = gh.fetchIssueBodySync(String(issueNumber));
|
|
101
|
+
const nonGoals = body ? parseNonGoals(body) : [];
|
|
102
|
+
// Live progress UI (non-`--json` only, so no live writes corrupt piped JSON):
|
|
103
|
+
// - TTY → #699: the boxed Ink TUI, driven by a single-issue snapshot
|
|
104
|
+
// adapter fed by the gate's `onProgress` (supersedes #697's
|
|
105
|
+
// plain renderer on this path).
|
|
106
|
+
// - non-TTY → #697: the plain phase-matrix renderer, which degrades to
|
|
107
|
+
// line mode off a TTY. Static-report fallback, unchanged.
|
|
108
|
+
const useTui = !options.json && Boolean(process.stdout.isTTY);
|
|
109
|
+
let renderer = null;
|
|
110
|
+
let heartbeat = null;
|
|
111
|
+
let onProgress;
|
|
112
|
+
let adapter = null;
|
|
113
|
+
let tuiHandle = null;
|
|
114
|
+
if (!options.json) {
|
|
115
|
+
console.log(ui.headerBox("SEQUANT READY"));
|
|
116
|
+
console.log("");
|
|
117
|
+
console.log(colors.muted(`Issue #${issueNumber} · policy: ${policy} · max iterations: ${maxIterations}` +
|
|
118
|
+
(tokenBudget
|
|
119
|
+
? ` · budget: ${tokenBudget.toLocaleString()} tokens`
|
|
120
|
+
: "") +
|
|
121
|
+
`\nWorktree: ${worktreePath}`));
|
|
122
|
+
console.log(colors.muted("Full-weight QA (pre-flight checks ON). Never merges — stops at the human gate."));
|
|
123
|
+
console.log("");
|
|
124
|
+
}
|
|
125
|
+
if (useTui) {
|
|
126
|
+
// Build the snapshot adapter and mount the Ink TUI against it. The gate's
|
|
127
|
+
// `onProgress` events drive the single box; `markDone` (below) flips the
|
|
128
|
+
// snapshot's `done` flag so the polling `App` unmounts.
|
|
129
|
+
const title = gh.fetchIssueTitleSync(String(issueNumber)) ?? `Issue #${issueNumber}`;
|
|
130
|
+
const branch = listWorktrees().find((w) => w.issue === issueNumber)?.branch ?? "";
|
|
131
|
+
adapter = new ReadySnapshotAdapter({ issueNumber, title, branch });
|
|
132
|
+
onProgress = adapter.onProgress;
|
|
133
|
+
const { renderTui } = await import("../ui/tui/index.js");
|
|
134
|
+
tuiHandle = renderTui(adapter);
|
|
135
|
+
}
|
|
136
|
+
else if (!options.json) {
|
|
137
|
+
// Stream phases as they fire (no `basePhases`): the ready pipeline length
|
|
138
|
+
// is dynamic (1–N qa/loop passes), so a fixed seed would leave a stuck-
|
|
139
|
+
// pending `loop` cell when the gate stops after the first qa.
|
|
140
|
+
({ renderer, heartbeat, onProgress } = buildProgressWiring({
|
|
141
|
+
tuiEnabled: false,
|
|
142
|
+
quiet: false,
|
|
143
|
+
issueNumbers: [issueNumber],
|
|
144
|
+
phaseTimeoutSeconds: phaseTimeout,
|
|
145
|
+
maxLoopIterations: maxIterations,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
// SIGINT: tear down the live zone (TUI unmount or renderer dispose) before
|
|
149
|
+
// ShutdownManager writes its cleanup banner so the two don't collide on
|
|
150
|
+
// stdout (mirror run.ts SIGINT ordering).
|
|
151
|
+
const sigintHandler = () => {
|
|
152
|
+
tuiHandle?.unmount();
|
|
153
|
+
renderer?.dispose();
|
|
154
|
+
};
|
|
155
|
+
if (renderer || tuiHandle)
|
|
156
|
+
process.once("SIGINT", sigintHandler);
|
|
157
|
+
// Real phase runner: wraps executePhaseWithRetry against the worktree. The
|
|
158
|
+
// renderer doubles as the PhasePauseHandle (7th arg) so `--verbose` streaming
|
|
159
|
+
// pauses/resumes the live zone instead of double-rendering (AC-5).
|
|
160
|
+
const runPhase = (phase, config, wt) => executePhaseWithRetry(issueNumber, phase, config, undefined, wt, undefined, renderer ?? undefined);
|
|
161
|
+
let result;
|
|
162
|
+
try {
|
|
163
|
+
result = await runReadyGate({
|
|
164
|
+
issueNumber,
|
|
165
|
+
worktreePath,
|
|
166
|
+
policy,
|
|
167
|
+
maxIterations,
|
|
168
|
+
tokenBudget,
|
|
169
|
+
nonGoals,
|
|
170
|
+
phaseTimeout,
|
|
171
|
+
mcp,
|
|
172
|
+
verbose: options.verbose,
|
|
173
|
+
runPhase,
|
|
174
|
+
onProgress,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
// Tear down the live zone on the error path too — not just the happy path
|
|
179
|
+
// (Derived AC: cleanup on ALL exit paths). For the TUI, mark done + unmount
|
|
180
|
+
// so ink restores the terminal before the error box prints.
|
|
181
|
+
adapter?.markDone(false);
|
|
182
|
+
tuiHandle?.unmount();
|
|
183
|
+
renderer?.dispose();
|
|
184
|
+
heartbeat?.dispose();
|
|
185
|
+
if (renderer || tuiHandle)
|
|
186
|
+
process.off("SIGINT", sigintHandler);
|
|
187
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
188
|
+
if (options.json) {
|
|
189
|
+
console.log(JSON.stringify({ error: message }));
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.error(ui.errorBox("Ready gate failed", message));
|
|
193
|
+
}
|
|
194
|
+
process.exitCode = 2;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Persist the terminal state so `sequant status` reflects it (Derived AC).
|
|
198
|
+
// Best-effort: initialize the issue in state if a prior run didn't track it.
|
|
199
|
+
try {
|
|
200
|
+
const stateManager = getStateManager();
|
|
201
|
+
const existing = await stateManager.getIssueState(issueNumber);
|
|
202
|
+
if (!existing) {
|
|
203
|
+
const title = gh.fetchIssueTitleSync(String(issueNumber)) ?? `Issue #${issueNumber}`;
|
|
204
|
+
await stateManager.initializeIssue(issueNumber, title, {
|
|
205
|
+
worktree: worktreePath,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
await stateManager.updateIssueStatus(issueNumber, result.issueStatus);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// State persistence is non-fatal — the report is the primary output.
|
|
212
|
+
}
|
|
213
|
+
if (options.json) {
|
|
214
|
+
console.log(JSON.stringify({
|
|
215
|
+
issue: result.issueNumber,
|
|
216
|
+
policy: result.policy,
|
|
217
|
+
ready: result.ready,
|
|
218
|
+
reason: result.reason,
|
|
219
|
+
status: result.issueStatus,
|
|
220
|
+
iterations: result.iterations,
|
|
221
|
+
finalVerdict: result.finalVerdict,
|
|
222
|
+
autoFixed: result.autoFixed,
|
|
223
|
+
remaining: result.remaining,
|
|
224
|
+
tokensUsed: result.tokensUsed,
|
|
225
|
+
}, null, 2));
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// #699 AC-3 / #697 AC-6: tear the live zone DOWN before printing the report
|
|
229
|
+
// so the markdown lands in clean scrollback. For the TUI, flip `done` so the
|
|
230
|
+
// polling App unmounts, await that unmount (which also emits the durable
|
|
231
|
+
// teardown summary, AC-5), then print the report below it.
|
|
232
|
+
if (tuiHandle) {
|
|
233
|
+
adapter?.markDone(result.ready);
|
|
234
|
+
await tuiHandle.done;
|
|
235
|
+
}
|
|
236
|
+
renderer?.dispose();
|
|
237
|
+
console.log(result.report);
|
|
238
|
+
}
|
|
239
|
+
heartbeat?.dispose();
|
|
240
|
+
if (renderer || tuiHandle)
|
|
241
|
+
process.off("SIGINT", sigintHandler);
|
|
242
|
+
process.exitCode = getReadyExitCode(result);
|
|
243
|
+
}
|
|
@@ -65,6 +65,8 @@ function colorStatus(status, resolvedAt) {
|
|
|
65
65
|
return chalk.blue(status);
|
|
66
66
|
case "waiting_for_qa_gate":
|
|
67
67
|
return chalk.yellow(status);
|
|
68
|
+
case "waiting_for_human_merge":
|
|
69
|
+
return chalk.green(status);
|
|
68
70
|
case "ready_for_merge":
|
|
69
71
|
return chalk.green(status);
|
|
70
72
|
case "merged":
|
|
@@ -152,6 +154,7 @@ function displayIssueSummary(issues) {
|
|
|
152
154
|
const byStatus = {
|
|
153
155
|
in_progress: [],
|
|
154
156
|
waiting_for_qa_gate: [],
|
|
157
|
+
waiting_for_human_merge: [],
|
|
155
158
|
ready_for_merge: [],
|
|
156
159
|
blocked: [],
|
|
157
160
|
not_started: [],
|
|
@@ -165,6 +168,7 @@ function displayIssueSummary(issues) {
|
|
|
165
168
|
const statusOrder = [
|
|
166
169
|
"in_progress",
|
|
167
170
|
"waiting_for_qa_gate",
|
|
171
|
+
"waiting_for_human_merge",
|
|
168
172
|
"ready_for_merge",
|
|
169
173
|
"blocked",
|
|
170
174
|
"not_started",
|
|
@@ -236,7 +236,13 @@ export declare class TTYRenderer extends BaseRenderer {
|
|
|
236
236
|
private statusCellLines;
|
|
237
237
|
private runHeader;
|
|
238
238
|
private rollupLine;
|
|
239
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Single-issue layout: indented `label value` lines, no box drawing. The
|
|
241
|
+
* label is cyan and padded to `labelW`; continuation lines (multi-line
|
|
242
|
+
* status cells) align under the value column with a blank label. See
|
|
243
|
+
* `renderSingleIssueFrame` for why the bordered grid was dropped.
|
|
244
|
+
*/
|
|
245
|
+
private drawKeyValueLines;
|
|
240
246
|
private drawIssueGrid;
|
|
241
247
|
}
|
|
242
248
|
export interface CreateRendererOptions extends RenderOptions {
|
|
@@ -922,21 +922,25 @@ export class TTYRenderer extends BaseRenderer {
|
|
|
922
922
|
return lines.join("\n");
|
|
923
923
|
}
|
|
924
924
|
renderSingleIssueFrame(cols) {
|
|
925
|
-
// AC-11:
|
|
925
|
+
// AC-11: single-issue runs render as indented `label value` lines — not a
|
|
926
|
+
// box-drawing grid.
|
|
927
|
+
//
|
|
928
|
+
// The grid was the dominant source of `log-update` `eraseLines` stranding
|
|
929
|
+
// (#647 / #655): a multi-line bordered frame whose top survives in
|
|
930
|
+
// scrollback when the erase undershoots, leaving a frozen first paint (the
|
|
931
|
+
// classic "0s elapsed" ghost with no bottom border). Indented labels keep
|
|
932
|
+
// the same information at a shorter, border-free height that clears
|
|
933
|
+
// cleanly, and match the repo's move away from box-drawing in human output
|
|
934
|
+
// (see feedback_llm_hostile_formatting). Multi-issue still uses the grid.
|
|
926
935
|
const state = [...this.issues.values()][0];
|
|
927
936
|
const c = colorize(this.noColor);
|
|
928
937
|
const header = `SEQUANT WORKFLOW · #${state.issueNumber} · ${formatElapsedTime((this.now() - this.runStartedAt) / 1000)} elapsed`;
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
// the
|
|
932
|
-
//
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
// additionally produced rows 2 chars wider than `cols`, compounding the
|
|
936
|
-
// wrap. Both errors removed.
|
|
937
|
-
const labelWidth = 10;
|
|
938
|
-
const innerWidth = Math.max(40, Math.min(cols, 78) - labelWidth - 9);
|
|
939
|
-
const valueWidth = innerWidth;
|
|
938
|
+
// Label column fits the widest label ("Worktree"); value column is the
|
|
939
|
+
// remaining width after the 2-space indent + 2-space gap. Capped the same
|
|
940
|
+
// way the grid was so wide / misreported terminals can't push values past a
|
|
941
|
+
// standard 80-col reader.
|
|
942
|
+
const labelWidth = 8;
|
|
943
|
+
const valueWidth = Math.max(40, Math.min(cols, 100) - labelWidth - 4);
|
|
940
944
|
const rows = [];
|
|
941
945
|
const titleSuffix = state.title ? ` — ${state.title}` : "";
|
|
942
946
|
rows.push([
|
|
@@ -953,7 +957,7 @@ export class TTYRenderer extends BaseRenderer {
|
|
|
953
957
|
const lines = [c.bold(header), ""];
|
|
954
958
|
if (this.banner)
|
|
955
959
|
lines.push(c.yellow(this.banner), "");
|
|
956
|
-
lines.push(this.
|
|
960
|
+
lines.push(this.drawKeyValueLines(rows, labelWidth));
|
|
957
961
|
return lines.join("\n");
|
|
958
962
|
}
|
|
959
963
|
renderMultiIssueFrame(cols) {
|
|
@@ -1172,26 +1176,22 @@ export class TTYRenderer extends BaseRenderer {
|
|
|
1172
1176
|
return c.dim(`${done} done · ${running} running · ${queued} queued · ${failed} failed`);
|
|
1173
1177
|
}
|
|
1174
1178
|
// ---------------- Box drawing ----------------
|
|
1175
|
-
|
|
1179
|
+
/**
|
|
1180
|
+
* Single-issue layout: indented `label value` lines, no box drawing. The
|
|
1181
|
+
* label is cyan and padded to `labelW`; continuation lines (multi-line
|
|
1182
|
+
* status cells) align under the value column with a blank label. See
|
|
1183
|
+
* `renderSingleIssueFrame` for why the bordered grid was dropped.
|
|
1184
|
+
*/
|
|
1185
|
+
drawKeyValueLines(rows, labelW) {
|
|
1176
1186
|
const c = colorize(this.noColor);
|
|
1177
|
-
const
|
|
1178
|
-
const
|
|
1179
|
-
const top = dim(" ┌" + "─".repeat(labelW + 2) + "┬" + "─".repeat(valueW + 2) + "┐");
|
|
1180
|
-
const sep = dim(" ├" + "─".repeat(labelW + 2) + "┼" + "─".repeat(valueW + 2) + "┤");
|
|
1181
|
-
const bottom = dim(" └" + "─".repeat(labelW + 2) + "┴" + "─".repeat(valueW + 2) + "┘");
|
|
1182
|
-
const out = [top];
|
|
1183
|
-
rows.forEach(([label, lines], i) => {
|
|
1187
|
+
const out = [];
|
|
1188
|
+
for (const [label, lines] of rows) {
|
|
1184
1189
|
const labelPadded = padEndVisible(label, labelW);
|
|
1185
1190
|
lines.forEach((line, idx) => {
|
|
1186
1191
|
const labelCell = idx === 0 ? c.cyan(labelPadded) : " ".repeat(labelW);
|
|
1187
|
-
|
|
1188
|
-
out.push(` ${dim("│")} ${labelCell} ${dim("│")} ${valuePadded} ${dim("│")}`);
|
|
1192
|
+
out.push(` ${labelCell} ${line}`);
|
|
1189
1193
|
});
|
|
1190
|
-
|
|
1191
|
-
out.push(sep);
|
|
1192
|
-
});
|
|
1193
|
-
out.push(bottom);
|
|
1194
|
-
void total;
|
|
1194
|
+
}
|
|
1195
1195
|
return out.join("\n");
|
|
1196
1196
|
}
|
|
1197
1197
|
drawIssueGrid(rows, issueW, statusW) {
|
|
@@ -235,6 +235,24 @@ export interface QASettings {
|
|
|
235
235
|
*/
|
|
236
236
|
markdownOnlySafeCiPatterns: string[];
|
|
237
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Gate policy for the `sequant ready` post-resolve A+ QA gate (#683).
|
|
240
|
+
*
|
|
241
|
+
* - `ac` (default): loop stops once no `AC_NOT_MET` verdict remains (ACs
|
|
242
|
+
* objectively met). Remaining quality/polish gaps are documented in the gap
|
|
243
|
+
* report but NOT auto-fixed — predictable, scope-respecting behavior for a
|
|
244
|
+
* team engineer with a fixed agenda.
|
|
245
|
+
* - `a-plus` (opt-in): loop continues until `READY_FOR_MERGE`, auto-fixing
|
|
246
|
+
* quality gaps along the way — max-quality behavior for a solo maintainer.
|
|
247
|
+
*/
|
|
248
|
+
export type ReadyPolicy = "ac" | "a-plus";
|
|
249
|
+
/**
|
|
250
|
+
* Settings for the `sequant ready` command (#683).
|
|
251
|
+
*/
|
|
252
|
+
export interface ReadySettings {
|
|
253
|
+
/** Default gate policy. Overridable per-invocation with `--policy`. */
|
|
254
|
+
policy: ReadyPolicy;
|
|
255
|
+
}
|
|
238
256
|
/**
|
|
239
257
|
* Full settings schema
|
|
240
258
|
*/
|
|
@@ -249,6 +267,8 @@ export interface SequantSettings {
|
|
|
249
267
|
scopeAssessment: ScopeAssessmentSettings;
|
|
250
268
|
/** QA skill settings */
|
|
251
269
|
qa: QASettings;
|
|
270
|
+
/** `sequant ready` gate settings (#683) */
|
|
271
|
+
ready: ReadySettings;
|
|
252
272
|
}
|
|
253
273
|
/** Zod schema for RotationSettings */
|
|
254
274
|
export declare const RotationSettingsSchema: z.ZodObject<{
|
|
@@ -346,6 +366,13 @@ export declare const QASettingsSchema: z.ZodObject<{
|
|
|
346
366
|
markdownOnlyCiRelaxed: z.ZodDefault<z.ZodBoolean>;
|
|
347
367
|
markdownOnlySafeCiPatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
348
368
|
}, z.core.$strip>;
|
|
369
|
+
/** Zod schema for ReadySettings (#683) */
|
|
370
|
+
export declare const ReadySettingsSchema: z.ZodObject<{
|
|
371
|
+
policy: z.ZodDefault<z.ZodEnum<{
|
|
372
|
+
ac: "ac";
|
|
373
|
+
"a-plus": "a-plus";
|
|
374
|
+
}>>;
|
|
375
|
+
}, z.core.$strip>;
|
|
349
376
|
/**
|
|
350
377
|
* Zod schema for the full SequantSettings (AC-1, AC-5).
|
|
351
378
|
*
|
|
@@ -428,6 +455,12 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
428
455
|
markdownOnlyCiRelaxed: z.ZodDefault<z.ZodBoolean>;
|
|
429
456
|
markdownOnlySafeCiPatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
430
457
|
}, z.core.$strip>>;
|
|
458
|
+
ready: z.ZodDefault<z.ZodObject<{
|
|
459
|
+
policy: z.ZodDefault<z.ZodEnum<{
|
|
460
|
+
ac: "ac";
|
|
461
|
+
"a-plus": "a-plus";
|
|
462
|
+
}>>;
|
|
463
|
+
}, z.core.$strip>>;
|
|
431
464
|
}, z.core.$loose>;
|
|
432
465
|
/** A single validation warning about an unknown or invalid setting */
|
|
433
466
|
export interface SettingsWarning {
|
|
@@ -477,6 +510,7 @@ export declare const DEFAULT_SCOPE_ASSESSMENT_SETTINGS: ScopeAssessmentSettings;
|
|
|
477
510
|
* Default QA settings
|
|
478
511
|
*/
|
|
479
512
|
export declare const DEFAULT_QA_SETTINGS: QASettings;
|
|
513
|
+
export declare const DEFAULT_READY_SETTINGS: ReadySettings;
|
|
480
514
|
/**
|
|
481
515
|
* Default settings
|
|
482
516
|
*/
|
package/dist/src/lib/settings.js
CHANGED
|
@@ -127,6 +127,10 @@ export const QASettingsSchema = z.object({
|
|
|
127
127
|
.array(z.string())
|
|
128
128
|
.default(["build (*)", "Plugin Structure Validation"]),
|
|
129
129
|
});
|
|
130
|
+
/** Zod schema for ReadySettings (#683) */
|
|
131
|
+
export const ReadySettingsSchema = z.object({
|
|
132
|
+
policy: z.enum(["ac", "a-plus"]).default("ac"),
|
|
133
|
+
});
|
|
130
134
|
/**
|
|
131
135
|
* Zod schema for the full SequantSettings (AC-1, AC-5).
|
|
132
136
|
*
|
|
@@ -144,6 +148,7 @@ export const SettingsSchema = z
|
|
|
144
148
|
agents: AgentSettingsSchema.default(() => AgentSettingsSchema.parse({})),
|
|
145
149
|
scopeAssessment: ScopeAssessmentSettingsSchema.default(() => ScopeAssessmentSettingsSchema.parse({})),
|
|
146
150
|
qa: QASettingsSchema.default(() => QASettingsSchema.parse({})),
|
|
151
|
+
ready: ReadySettingsSchema.default(() => ReadySettingsSchema.parse({})),
|
|
147
152
|
})
|
|
148
153
|
.passthrough();
|
|
149
154
|
/**
|
|
@@ -151,7 +156,7 @@ export const SettingsSchema = z
|
|
|
151
156
|
* Used to detect unknown/misspelled keys and produce warnings.
|
|
152
157
|
*/
|
|
153
158
|
const KNOWN_KEYS = {
|
|
154
|
-
"": new Set(["version", "run", "agents", "scopeAssessment", "qa"]),
|
|
159
|
+
"": new Set(["version", "run", "agents", "scopeAssessment", "qa", "ready"]),
|
|
155
160
|
run: new Set([
|
|
156
161
|
"logJson",
|
|
157
162
|
"logPath",
|
|
@@ -186,6 +191,7 @@ const KNOWN_KEYS = {
|
|
|
186
191
|
"markdownOnlyCiRelaxed",
|
|
187
192
|
"markdownOnlySafeCiPatterns",
|
|
188
193
|
]),
|
|
194
|
+
ready: new Set(["policy"]),
|
|
189
195
|
"run.rotation": new Set(["enabled", "maxSizeMB", "maxFiles"]),
|
|
190
196
|
"run.aider": new Set(["model", "editFormat", "extraArgs"]),
|
|
191
197
|
"scopeAssessment.trivialThresholds": new Set([
|
|
@@ -323,6 +329,9 @@ export const DEFAULT_QA_SETTINGS = {
|
|
|
323
329
|
markdownOnlyCiRelaxed: true,
|
|
324
330
|
markdownOnlySafeCiPatterns: ["build (*)", "Plugin Structure Validation"],
|
|
325
331
|
};
|
|
332
|
+
export const DEFAULT_READY_SETTINGS = {
|
|
333
|
+
policy: "ac",
|
|
334
|
+
};
|
|
326
335
|
/**
|
|
327
336
|
* Default settings
|
|
328
337
|
*/
|
|
@@ -348,6 +357,7 @@ export const DEFAULT_SETTINGS = {
|
|
|
348
357
|
agents: DEFAULT_AGENT_SETTINGS,
|
|
349
358
|
scopeAssessment: DEFAULT_SCOPE_ASSESSMENT_SETTINGS,
|
|
350
359
|
qa: DEFAULT_QA_SETTINGS,
|
|
360
|
+
ready: DEFAULT_READY_SETTINGS,
|
|
351
361
|
};
|
|
352
362
|
/**
|
|
353
363
|
* Validate aider-specific settings.
|
|
@@ -499,6 +509,12 @@ export function generateSettingsJsonc(settings) {
|
|
|
499
509
|
lines.push(` "model": ${JSON.stringify(settings.agents.model)},`);
|
|
500
510
|
lines.push(` // Isolate parallel agent groups in separate worktrees`);
|
|
501
511
|
lines.push(` "isolateParallel": ${JSON.stringify(settings.agents.isolateParallel)}`);
|
|
512
|
+
lines.push(` },`);
|
|
513
|
+
lines.push("");
|
|
514
|
+
lines.push(` // sequant ready — post-resolve A+ QA gate (#683)`);
|
|
515
|
+
lines.push(` "ready": {`);
|
|
516
|
+
lines.push(` // Gate policy: "ac" (stop at ACs met, report quality gaps) or "a-plus" (loop to READY_FOR_MERGE)`);
|
|
517
|
+
lines.push(` "policy": ${JSON.stringify(settings.ready.policy)}`);
|
|
502
518
|
lines.push(` }`);
|
|
503
519
|
lines.push("}");
|
|
504
520
|
lines.push("");
|
|
@@ -646,6 +662,12 @@ Each threshold has \`yellow\` (warning) and \`red\` (split recommended) values:
|
|
|
646
662
|
| \`markdownOnlyCiRelaxed\` | boolean | \`true\` | When diff touches only \`.md\` files, treat pending CI checks matching \`markdownOnlySafeCiPatterns\` as informational |
|
|
647
663
|
| \`markdownOnlySafeCiPatterns\` | string[] | \`["build (*)", "Plugin Structure Validation"]\` | Glob patterns for CI checks that are safe to ignore when pending on a markdown-only diff |
|
|
648
664
|
|
|
665
|
+
## \`ready\` — \`sequant ready\` Gate Settings (#683)
|
|
666
|
+
|
|
667
|
+
| Key | Type | Default | Description |
|
|
668
|
+
|-----|------|---------|-------------|
|
|
669
|
+
| \`policy\` | enum | \`"ac"\` | Gate policy. \`"ac"\` loops until ACs are objectively met (no \`AC_NOT_MET\`), reporting but not auto-fixing quality gaps. \`"a-plus"\` loops until \`READY_FOR_MERGE\`, auto-fixing quality gaps. Override per-run with \`--policy ac\\|a-plus\`. |
|
|
670
|
+
|
|
649
671
|
---
|
|
650
672
|
|
|
651
673
|
*Unknown keys are preserved but logged as warnings. This allows forward compatibility
|
|
@@ -106,8 +106,16 @@ export function parseQaVerdict(output) {
|
|
|
106
106
|
// - "**Verdict:** X" (bold label with colon inside)
|
|
107
107
|
// - "**Verdict:** **X**" (bold label and bold value)
|
|
108
108
|
// - "Verdict: X" (plain)
|
|
109
|
-
//
|
|
110
|
-
|
|
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);
|
|
111
119
|
if (!verdictMatch)
|
|
112
120
|
return null;
|
|
113
121
|
// Normalize to uppercase with underscores
|
|
@@ -508,6 +516,13 @@ async function executePhase(issueNumber, phase, config, resumeHandle, worktreePa
|
|
|
508
516
|
// Skills can check these to skip redundant pre-flight checks
|
|
509
517
|
env.SEQUANT_ORCHESTRATOR = "sequant-run";
|
|
510
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
|
+
}
|
|
511
526
|
// Propagate issue type for skills to adapt behavior (e.g., lighter QA for docs)
|
|
512
527
|
if (config.issueType) {
|
|
513
528
|
env.SEQUANT_ISSUE_TYPE = config.issueType;
|
|
@@ -105,6 +105,12 @@ export declare class GitHubProvider implements PlatformProvider {
|
|
|
105
105
|
* Used by merge-check and worktree-discovery.
|
|
106
106
|
*/
|
|
107
107
|
fetchIssueTitleSync(issueId: string): string | null;
|
|
108
|
+
/**
|
|
109
|
+
* Fetch an issue's raw body markdown. Used by `sequant ready` (#683) to parse
|
|
110
|
+
* the Non-Goals section for report-only gap classification. Returns null when
|
|
111
|
+
* gh is unavailable, the issue can't be fetched, or the body is empty.
|
|
112
|
+
*/
|
|
113
|
+
fetchIssueBodySync(issueId: string): string | null;
|
|
108
114
|
/**
|
|
109
115
|
* Check if the `gh` CLI binary is installed (not auth, just available).
|
|
110
116
|
* Used by upstream/assessment.ts for pre-flight checks.
|
|
@@ -249,6 +249,23 @@ export class GitHubProvider {
|
|
|
249
249
|
return null;
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Fetch an issue's raw body markdown. Used by `sequant ready` (#683) to parse
|
|
254
|
+
* the Non-Goals section for report-only gap classification. Returns null when
|
|
255
|
+
* gh is unavailable, the issue can't be fetched, or the body is empty.
|
|
256
|
+
*/
|
|
257
|
+
fetchIssueBodySync(issueId) {
|
|
258
|
+
try {
|
|
259
|
+
const result = spawnSync("gh", ["issue", "view", issueId, "--json", "body", "--jq", ".body"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10000 });
|
|
260
|
+
if (result.status !== 0)
|
|
261
|
+
return null;
|
|
262
|
+
const body = result.stdout ?? "";
|
|
263
|
+
return body.trim() ? body : null;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
252
269
|
/**
|
|
253
270
|
* Check if the `gh` CLI binary is installed (not auth, just available).
|
|
254
271
|
* Used by upstream/assessment.ts for pre-flight checks.
|