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.
Files changed (58) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/README.md +125 -163
  4. package/dist/bin/cli.js +13 -0
  5. package/dist/dashboard/server.js +1 -0
  6. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +2 -2
  7. package/dist/marketplace/external_plugins/sequant/README.md +6 -3
  8. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +92 -0
  9. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +18 -9
  10. package/dist/marketplace/external_plugins/sequant/hooks/relay-check.sh +107 -0
  11. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/behavior-rule-detection.md +205 -0
  12. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +21 -8
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +302 -86
  14. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +109 -0
  15. package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +141 -22
  16. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +83 -78
  17. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +377 -137
  18. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +28 -0
  19. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +621 -0
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +741 -232
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +47 -1
  22. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +12 -6
  23. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +217 -964
  24. package/dist/marketplace/external_plugins/sequant/skills/spec/references/parallel-groups.md +7 -0
  25. package/dist/marketplace/external_plugins/sequant/skills/spec/references/quality-checklist.md +75 -0
  26. package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +4 -2
  27. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +0 -27
  28. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +24 -44
  29. package/dist/src/commands/ready-tui-adapter.d.ts +59 -0
  30. package/dist/src/commands/ready-tui-adapter.js +130 -0
  31. package/dist/src/commands/ready.d.ts +49 -0
  32. package/dist/src/commands/ready.js +243 -0
  33. package/dist/src/commands/status.js +4 -0
  34. package/dist/src/lib/cli-ui/run-renderer.d.ts +7 -1
  35. package/dist/src/lib/cli-ui/run-renderer.js +28 -28
  36. package/dist/src/lib/settings.d.ts +34 -0
  37. package/dist/src/lib/settings.js +23 -1
  38. package/dist/src/lib/workflow/phase-executor.js +17 -2
  39. package/dist/src/lib/workflow/platforms/github.d.ts +6 -0
  40. package/dist/src/lib/workflow/platforms/github.js +17 -0
  41. package/dist/src/lib/workflow/ready-gate.d.ts +155 -0
  42. package/dist/src/lib/workflow/ready-gate.js +374 -0
  43. package/dist/src/lib/workflow/reconcile.js +6 -0
  44. package/dist/src/lib/workflow/state-schema.d.ts +3 -0
  45. package/dist/src/lib/workflow/state-schema.js +1 -0
  46. package/dist/src/lib/workflow/types.d.ts +9 -0
  47. package/dist/src/ui/tui/App.js +8 -2
  48. package/dist/src/ui/tui/IssueBox.js +3 -4
  49. package/dist/src/ui/tui/index.d.ts +13 -4
  50. package/dist/src/ui/tui/index.js +19 -5
  51. package/dist/src/ui/tui/row-cap.d.ts +51 -0
  52. package/dist/src/ui/tui/row-cap.js +76 -0
  53. package/dist/src/ui/tui/teardown.d.ts +20 -0
  54. package/dist/src/ui/tui/teardown.js +29 -0
  55. package/dist/src/ui/tui/theme.d.ts +3 -0
  56. package/dist/src/ui/tui/theme.js +3 -0
  57. package/package.json +19 -8
  58. 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
- private drawKeyValueTable;
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: Single-issue runs use a key:value full-grid table.
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
- // #647 AC-3: cap at 78 (not 110) so the rendered grid stays narrower than
930
- // any standard 80-col terminal even when the reported `cols` is wider than
931
- // the actual terminal (e.g. cached `process.stdout.columns`, TTY emulation
932
- // layers, `npx` piped stdout). Total drawn width is
933
- // `labelWidth + valueWidth + 9` (2 leading spaces + 3 box-drawing
934
- // intersection chars + 4 cell-padding spaces); the prior `- 7` formula
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.drawKeyValueTable(rows, labelWidth, valueWidth));
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
- drawKeyValueTable(rows, labelW, valueW) {
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 dim = c.dim;
1178
- const total = labelW + valueW + 3;
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
- const valuePadded = padEndVisible(line, valueW);
1188
- out.push(` ${dim("│")} ${labelCell} ${dim("│")} ${valuePadded} ${dim("│")}`);
1192
+ out.push(` ${labelCell} ${line}`);
1189
1193
  });
1190
- if (i < rows.length - 1)
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
  */
@@ -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
- // Case insensitive, handles optional markdown formatting
110
- const verdictMatch = output.match(/(?:###?\s*)?(?:\*\*)?Verdict:?\*?\*?\s*\*?\*?\s*(READY_FOR_MERGE|AC_MET_BUT_NOT_A_PLUS|AC_NOT_MET|NEEDS_VERIFICATION)\*?\*?/i);
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.