sequant 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/README.md +125 -160
  4. package/dist/bin/cli.js +59 -4
  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/abort.d.ts +36 -0
  30. package/dist/src/commands/abort.js +138 -0
  31. package/dist/src/commands/prompt.d.ts +7 -0
  32. package/dist/src/commands/prompt.js +101 -7
  33. package/dist/src/commands/ready-tui-adapter.d.ts +59 -0
  34. package/dist/src/commands/ready-tui-adapter.js +130 -0
  35. package/dist/src/commands/ready.d.ts +49 -0
  36. package/dist/src/commands/ready.js +243 -0
  37. package/dist/src/commands/run-progress.d.ts +11 -1
  38. package/dist/src/commands/run-progress.js +20 -3
  39. package/dist/src/commands/run.js +12 -2
  40. package/dist/src/commands/status.js +4 -0
  41. package/dist/src/commands/watch.d.ts +2 -0
  42. package/dist/src/commands/watch.js +67 -3
  43. package/dist/src/lib/assess-collision-detect.js +1 -1
  44. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +39 -0
  45. package/dist/src/lib/cli-ui/run-renderer.d.ts +34 -2
  46. package/dist/src/lib/cli-ui/run-renderer.js +250 -33
  47. package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
  48. package/dist/src/lib/cli-ui/scrollback-harness.js +294 -0
  49. package/dist/src/lib/merge-check/types.js +1 -1
  50. package/dist/src/lib/relay/archive.js +6 -0
  51. package/dist/src/lib/relay/types.d.ts +2 -0
  52. package/dist/src/lib/relay/types.js +9 -0
  53. package/dist/src/lib/settings.d.ts +34 -0
  54. package/dist/src/lib/settings.js +23 -1
  55. package/dist/src/lib/workflow/batch-executor.js +34 -18
  56. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
  57. package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
  58. package/dist/src/lib/workflow/drivers/aider.js +9 -0
  59. package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
  60. package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
  61. package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
  62. package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
  63. package/dist/src/lib/workflow/event-emitter.js +102 -0
  64. package/dist/src/lib/workflow/notice.d.ts +32 -0
  65. package/dist/src/lib/workflow/notice.js +38 -0
  66. package/dist/src/lib/workflow/phase-executor.d.ts +9 -21
  67. package/dist/src/lib/workflow/phase-executor.js +105 -117
  68. package/dist/src/lib/workflow/phase-mapper.d.ts +26 -13
  69. package/dist/src/lib/workflow/phase-mapper.js +55 -33
  70. package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
  71. package/dist/src/lib/workflow/phase-registry.js +233 -0
  72. package/dist/src/lib/workflow/platforms/github.d.ts +6 -0
  73. package/dist/src/lib/workflow/platforms/github.js +17 -0
  74. package/dist/src/lib/workflow/ready-gate.d.ts +155 -0
  75. package/dist/src/lib/workflow/ready-gate.js +374 -0
  76. package/dist/src/lib/workflow/reconcile.js +6 -0
  77. package/dist/src/lib/workflow/run-log-schema.d.ts +5 -55
  78. package/dist/src/lib/workflow/run-orchestrator.d.ts +32 -2
  79. package/dist/src/lib/workflow/run-orchestrator.js +125 -11
  80. package/dist/src/lib/workflow/state-manager.d.ts +19 -1
  81. package/dist/src/lib/workflow/state-manager.js +27 -1
  82. package/dist/src/lib/workflow/state-schema.d.ts +23 -35
  83. package/dist/src/lib/workflow/state-schema.js +29 -3
  84. package/dist/src/lib/workflow/types.d.ts +74 -15
  85. package/dist/src/lib/workflow/types.js +18 -13
  86. package/dist/src/ui/tui/App.js +8 -2
  87. package/dist/src/ui/tui/IssueBox.js +3 -4
  88. package/dist/src/ui/tui/index.d.ts +13 -4
  89. package/dist/src/ui/tui/index.js +19 -5
  90. package/dist/src/ui/tui/row-cap.d.ts +51 -0
  91. package/dist/src/ui/tui/row-cap.js +76 -0
  92. package/dist/src/ui/tui/teardown.d.ts +20 -0
  93. package/dist/src/ui/tui/teardown.js +29 -0
  94. package/dist/src/ui/tui/theme.d.ts +3 -0
  95. package/dist/src/ui/tui/theme.js +3 -0
  96. package/package.json +23 -11
  97. package/templates/hooks/post-tool.sh +81 -0
  98. package/templates/skills/assess/SKILL.md +28 -28
  99. package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
  100. package/templates/skills/qa/SKILL.md +5 -2
  101. package/templates/skills/setup/SKILL.md +6 -6
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Virtual-terminal harness for renderer regression tests (#647).
3
+ *
4
+ * The test stub embedded in TTYRenderer (see {@link
5
+ * ./run-renderer.ts#TTYTestStub}) mocks `log-update` itself — it cannot reveal
6
+ * whether the real `log-update` actually erases prior frames once the terminal
7
+ * scrolls. That gap is what allowed #624's fix to ship green while the
8
+ * underlying duplicate-header bug remained.
9
+ *
10
+ * This harness models a real terminal:
11
+ * - bounded visible viewport (rows × cols)
12
+ * - unbounded scrollback that captures every line that scrolls off the top
13
+ * - the ANSI escape vocabulary that `log-update@7` + `ansi-escapes`
14
+ * actually emit (cursor up/down/forward/back, eraseLine variants,
15
+ * SGR colour stripping, private mode set/reset, save/restore)
16
+ *
17
+ * With it, a test can wire the production renderer through a real
18
+ * `createLogUpdate` instance, replay an event sequence, and assert on
19
+ * `(visible + scrollback)` to catch any duplicate-header rendering — the
20
+ * exact regression #647 was opened for.
21
+ */
22
+ import { createLogUpdate } from "log-update";
23
+ const ESC = "";
24
+ /**
25
+ * Minimal vt100 model: visible grid + scrollback + cursor. Strips SGR colour
26
+ * codes (they're styling, not content) and ignores private-mode toggles
27
+ * (cursor hide/show). Implements the cursor and erase escapes that
28
+ * `log-update@7` actually emits.
29
+ */
30
+ export class VirtualTerminal {
31
+ rows;
32
+ cols;
33
+ onlcr;
34
+ /** visible[row][col] = char (always single-codepoint slot). */
35
+ visible;
36
+ /** Scrollback grows oldest-first as rows shift off the top. */
37
+ scrollback = [];
38
+ cursorRow = 0;
39
+ cursorCol = 0;
40
+ constructor(opts) {
41
+ this.rows = opts.rows;
42
+ this.cols = opts.cols;
43
+ this.onlcr = opts.onlcr ?? true;
44
+ this.visible = Array.from({ length: this.rows }, () => Array(this.cols).fill(" "));
45
+ }
46
+ // ---------------------------------------------------------------- input
47
+ write(text) {
48
+ let i = 0;
49
+ while (i < text.length) {
50
+ const ch = text[i];
51
+ if (ch === ESC) {
52
+ i = this.handleEscape(text, i);
53
+ continue;
54
+ }
55
+ if (ch === "\n") {
56
+ this.linefeed();
57
+ i++;
58
+ continue;
59
+ }
60
+ if (ch === "\r") {
61
+ this.cursorCol = 0;
62
+ i++;
63
+ continue;
64
+ }
65
+ if (ch === "\b") {
66
+ if (this.cursorCol > 0)
67
+ this.cursorCol--;
68
+ i++;
69
+ continue;
70
+ }
71
+ this.putChar(ch);
72
+ i++;
73
+ }
74
+ }
75
+ // ---------------------------------------------------------------- output
76
+ /** Visible viewport as a list of trimmed-right rows. */
77
+ getVisibleLines() {
78
+ return this.visible.map((row) => row.join("").replace(/\s+$/, ""));
79
+ }
80
+ /** Single multi-line string of (scrollback + visible). */
81
+ getAllText() {
82
+ const visibleText = this.getVisibleLines().join("\n");
83
+ if (this.scrollback.length === 0)
84
+ return visibleText;
85
+ return this.scrollback.join("\n") + "\n" + visibleText;
86
+ }
87
+ /** Match count of the regex against (scrollback + visible). */
88
+ countOccurrences(pattern) {
89
+ const text = this.getAllText();
90
+ const flags = pattern.flags.includes("g")
91
+ ? pattern.flags
92
+ : pattern.flags + "g";
93
+ const globalPattern = new RegExp(pattern.source, flags);
94
+ const matches = text.match(globalPattern);
95
+ return matches?.length ?? 0;
96
+ }
97
+ // ------------------------------------------------------- internal: text
98
+ putChar(ch) {
99
+ if (this.cursorCol >= this.cols) {
100
+ // Auto-wrap into the next row. Most terminals do this; log-update wraps
101
+ // upstream so this rarely triggers in practice.
102
+ this.cursorCol = 0;
103
+ this.linefeed();
104
+ }
105
+ this.visible[this.cursorRow][this.cursorCol] = ch;
106
+ this.cursorCol++;
107
+ }
108
+ linefeed() {
109
+ if (this.cursorRow + 1 < this.rows) {
110
+ this.cursorRow++;
111
+ }
112
+ else {
113
+ // Bottom of viewport: scroll the top row into scrollback.
114
+ const top = this.visible.shift();
115
+ this.scrollback.push(top.join("").replace(/\s+$/, ""));
116
+ this.visible.push(Array(this.cols).fill(" "));
117
+ // Cursor stays clamped at last visible row.
118
+ }
119
+ if (this.onlcr)
120
+ this.cursorCol = 0;
121
+ }
122
+ // --------------------------------------------------- internal: escapes
123
+ /** Returns the index AFTER the consumed escape sequence. */
124
+ handleEscape(text, start) {
125
+ // Bare ESC at end → consume.
126
+ if (start + 1 >= text.length)
127
+ return text.length;
128
+ const next = text[start + 1];
129
+ // CSI: ESC [ ... <final>
130
+ if (next === "[") {
131
+ return this.handleCSI(text, start + 2);
132
+ }
133
+ // OSC: ESC ] ... BEL or ESC \
134
+ if (next === "]") {
135
+ let i = start + 2;
136
+ while (i < text.length) {
137
+ if (text[i] === "")
138
+ return i + 1;
139
+ if (text[i] === ESC && text[i + 1] === "\\")
140
+ return i + 2;
141
+ i++;
142
+ }
143
+ return text.length;
144
+ }
145
+ // 2-byte non-CSI escapes: ESC 7 / ESC 8 (save/restore — cursor only,
146
+ // safe to ignore for our uses).
147
+ return start + 2;
148
+ }
149
+ handleCSI(text, start) {
150
+ let i = start;
151
+ let isPrivate = false;
152
+ if (text[i] === "?" || text[i] === ">" || text[i] === "<") {
153
+ isPrivate = true;
154
+ i++;
155
+ }
156
+ let params = "";
157
+ while (i < text.length && /[0-9;]/.test(text[i])) {
158
+ params += text[i];
159
+ i++;
160
+ }
161
+ if (i >= text.length)
162
+ return text.length;
163
+ const final = text[i];
164
+ i++;
165
+ this.executeCSI(params, final, isPrivate);
166
+ return i;
167
+ }
168
+ executeCSI(params, final, isPrivate) {
169
+ const parts = params.length === 0 ? [] : params.split(";").map((p) => parseInt(p, 10));
170
+ const n = (idx, def) => {
171
+ const v = parts[idx];
172
+ return v === undefined || isNaN(v) ? def : v;
173
+ };
174
+ // Private modes (e.g. ?25l/?25h cursor hide/show) — ignore.
175
+ if (isPrivate)
176
+ return;
177
+ switch (final) {
178
+ case "A": // cursor up
179
+ this.cursorRow = Math.max(0, this.cursorRow - n(0, 1));
180
+ return;
181
+ case "B": // cursor down (no scroll)
182
+ this.cursorRow = Math.min(this.rows - 1, this.cursorRow + n(0, 1));
183
+ return;
184
+ case "C": // cursor forward
185
+ this.cursorCol = Math.min(this.cols - 1, this.cursorCol + n(0, 1));
186
+ return;
187
+ case "D": // cursor back
188
+ this.cursorCol = Math.max(0, this.cursorCol - n(0, 1));
189
+ return;
190
+ case "E": // cursor next line
191
+ this.cursorRow = Math.min(this.rows - 1, this.cursorRow + n(0, 1));
192
+ this.cursorCol = 0;
193
+ return;
194
+ case "F": // cursor prev line
195
+ this.cursorRow = Math.max(0, this.cursorRow - n(0, 1));
196
+ this.cursorCol = 0;
197
+ return;
198
+ case "G": // cursor absolute column (1-based)
199
+ this.cursorCol = Math.min(this.cols - 1, Math.max(0, n(0, 1) - 1));
200
+ return;
201
+ case "H": // cursor position (1-based row;col)
202
+ case "f":
203
+ this.cursorRow = Math.min(this.rows - 1, Math.max(0, n(0, 1) - 1));
204
+ this.cursorCol = Math.min(this.cols - 1, Math.max(0, n(1, 1) - 1));
205
+ return;
206
+ case "J": {
207
+ // erase in display
208
+ const mode = n(0, 0);
209
+ if (mode === 0)
210
+ this.eraseFromCursorToEndOfScreen();
211
+ else if (mode === 1)
212
+ this.eraseFromStartOfScreenToCursor();
213
+ else if (mode === 2 || mode === 3)
214
+ this.eraseScreen();
215
+ return;
216
+ }
217
+ case "K": {
218
+ // erase in line
219
+ const mode = n(0, 0);
220
+ if (mode === 0)
221
+ this.eraseFromCursorToEndOfLine();
222
+ else if (mode === 1)
223
+ this.eraseFromStartOfLineToCursor();
224
+ else if (mode === 2)
225
+ this.eraseLine();
226
+ return;
227
+ }
228
+ case "S": // scroll up
229
+ case "T": // scroll down
230
+ case "m": // SGR colour — ignore (we don't model styling)
231
+ case "s": // save cursor
232
+ case "u": // restore cursor
233
+ case "n": // device status report — ignore
234
+ case "h": // set mode — ignore
235
+ case "l": // reset mode — ignore
236
+ return;
237
+ }
238
+ }
239
+ eraseLine() {
240
+ for (let c = 0; c < this.cols; c++)
241
+ this.visible[this.cursorRow][c] = " ";
242
+ }
243
+ eraseFromCursorToEndOfLine() {
244
+ for (let c = this.cursorCol; c < this.cols; c++)
245
+ this.visible[this.cursorRow][c] = " ";
246
+ }
247
+ eraseFromStartOfLineToCursor() {
248
+ for (let c = 0; c <= this.cursorCol; c++)
249
+ this.visible[this.cursorRow][c] = " ";
250
+ }
251
+ eraseFromCursorToEndOfScreen() {
252
+ this.eraseFromCursorToEndOfLine();
253
+ for (let r = this.cursorRow + 1; r < this.rows; r++) {
254
+ for (let c = 0; c < this.cols; c++)
255
+ this.visible[r][c] = " ";
256
+ }
257
+ }
258
+ eraseFromStartOfScreenToCursor() {
259
+ for (let r = 0; r < this.cursorRow; r++) {
260
+ for (let c = 0; c < this.cols; c++)
261
+ this.visible[r][c] = " ";
262
+ }
263
+ this.eraseFromStartOfLineToCursor();
264
+ }
265
+ eraseScreen() {
266
+ for (let r = 0; r < this.rows; r++) {
267
+ for (let c = 0; c < this.cols; c++)
268
+ this.visible[r][c] = " ";
269
+ }
270
+ }
271
+ }
272
+ export function createTerminalHarness(opts) {
273
+ const vt = new VirtualTerminal(opts);
274
+ const stream = {
275
+ write: (chunk) => {
276
+ vt.write(chunk);
277
+ return true;
278
+ },
279
+ columns: opts.streamColumns ?? opts.cols,
280
+ rows: opts.streamRows ?? opts.rows,
281
+ isTTY: true,
282
+ };
283
+ // log-update reads `stream.columns` / `stream.rows` defensively; the cast is
284
+ // safe because we exercise only those fields plus `write`.
285
+ const lu = createLogUpdate(stream, {
286
+ showCursor: true,
287
+ });
288
+ return {
289
+ vt,
290
+ logUpdate: lu,
291
+ stdoutWrite: (s) => vt.write(s),
292
+ stderrWrite: (s) => vt.write(s),
293
+ };
294
+ }
@@ -19,7 +19,7 @@ export const BatchVerdictSchema = z.enum(BATCH_VERDICTS);
19
19
  */
20
20
  export const DEFAULT_MIRROR_PAIRS = [
21
21
  { source: ".claude/skills", target: "templates/skills" },
22
- { source: "hooks", target: "templates/hooks" },
22
+ { source: ".claude/hooks", target: "templates/hooks" },
23
23
  ];
24
24
  /**
25
25
  * Get the git ref to use for diff/merge operations on a branch.
@@ -57,6 +57,10 @@ export function archiveRelayDir(issue, options) {
57
57
  copyFileSync(src, join(destDir, name));
58
58
  }
59
59
  }
60
+ // Split inbox/outbox counts (#645, Gap 5). Surfaces unanswered queries
61
+ // (inboxCount > outboxCount) when inspecting archives post-hoc.
62
+ const inboxCount = countLines(join(srcDir, RELAY_INBOX));
63
+ const outboxCount = countLines(join(srcDir, RELAY_OUTBOX));
60
64
  // Write meta.json.
61
65
  const meta = RelayArchiveMetaSchema.parse({
62
66
  issue,
@@ -64,6 +68,8 @@ export function archiveRelayDir(issue, options) {
64
68
  startedAt: options.startedAt,
65
69
  endedAt,
66
70
  messageCount: options.messageCount,
71
+ inboxCount,
72
+ outboxCount,
67
73
  });
68
74
  writeFileSync(join(destDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
69
75
  // Clear the working relay dir (inbox/outbox/cursor).
@@ -64,5 +64,7 @@ export declare const RelayArchiveMetaSchema: z.ZodObject<{
64
64
  startedAt: z.ZodString;
65
65
  endedAt: z.ZodString;
66
66
  messageCount: z.ZodNumber;
67
+ inboxCount: z.ZodOptional<z.ZodNumber>;
68
+ outboxCount: z.ZodOptional<z.ZodNumber>;
67
69
  }, z.core.$strip>;
68
70
  export type RelayArchiveMeta = z.infer<typeof RelayArchiveMetaSchema>;
@@ -72,5 +72,14 @@ export const RelayArchiveMetaSchema = z.object({
72
72
  phase: z.string(),
73
73
  startedAt: z.string().datetime(),
74
74
  endedAt: z.string().datetime(),
75
+ /** Total inbox + outbox messages exchanged during the run. */
75
76
  messageCount: z.number().int().nonnegative(),
77
+ /**
78
+ * Inbox messages (user → agent). Split out from `messageCount` (#645, Gap 5)
79
+ * so post-hoc inspection can spot unanswered queries (inboxCount > outboxCount).
80
+ * Optional for backward compatibility with archives written before this split.
81
+ */
82
+ inboxCount: z.number().int().nonnegative().optional(),
83
+ /** Outbox replies (agent → user). See `inboxCount` for context. */
84
+ outboxCount: z.number().int().nonnegative().optional(),
76
85
  });
@@ -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
@@ -260,7 +260,7 @@ export function getEnvConfig() {
260
260
  return config;
261
261
  }
262
262
  export async function executeBatch(issueNumbers, batchCtx) {
263
- const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, } = batchCtx;
263
+ const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, } = batchCtx;
264
264
  const results = [];
265
265
  for (const issueNumber of issueNumbers) {
266
266
  // Check if shutdown was triggered
@@ -289,6 +289,8 @@ export async function executeBatch(issueNumbers, batchCtx) {
289
289
  packageManager,
290
290
  baseBranch,
291
291
  onProgress,
292
+ onPhasePlan,
293
+ phasePauseHandle,
292
294
  };
293
295
  const result = await runIssueWithLogging(ctx);
294
296
  results.push(result);
@@ -305,7 +307,7 @@ export async function executeBatch(issueNumbers, batchCtx) {
305
307
  }
306
308
  export async function runIssueWithLogging(ctx) {
307
309
  // Destructure context for use throughout the function
308
- const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, } = ctx;
310
+ const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, } = ctx;
309
311
  const worktreePath = worktree?.path;
310
312
  const branch = worktree?.branch;
311
313
  const chainMode = chain?.enabled;
@@ -313,7 +315,8 @@ export async function runIssueWithLogging(ctx) {
313
315
  const startTime = Date.now();
314
316
  const phaseResults = [];
315
317
  let loopTriggered = false;
316
- let sessionId;
318
+ // Cross-phase resume token, driver-tagged and cwd-bound (#674).
319
+ let resumeHandle;
317
320
  // In parallel mode, suppress per-issue terminal output to prevent interleaving.
318
321
  // The caller (run.ts) handles progress display via updateProgress().
319
322
  const log = config.parallel ? () => { } : console.log.bind(console);
@@ -399,15 +402,15 @@ export async function runIssueWithLogging(ctx) {
399
402
  }
400
403
  const specStartTime = new Date();
401
404
  // Note: spec runs in main repo (not worktree) for planning
402
- const specResult = await executePhaseWithRetry(issueNumber, "spec", withActivityHook(config, issueNumber, "spec", onProgress), sessionId, worktreePath, // Will be ignored for spec (non-isolated phase)
403
- shutdownManager);
405
+ const specResult = await executePhaseWithRetry(issueNumber, "spec", withActivityHook(config, issueNumber, "spec", onProgress), resumeHandle, worktreePath, // Will be ignored for spec (non-isolated phase)
406
+ shutdownManager, phasePauseHandle);
404
407
  const specEndTime = new Date();
405
- if (specResult.sessionId) {
406
- sessionId = specResult.sessionId;
407
- // Update session ID in state for resume capability
408
+ if (specResult.resumeHandle) {
409
+ resumeHandle = specResult.resumeHandle;
410
+ // Persist resume token + originCwd for cross-process resume (#674).
408
411
  if (stateManager) {
409
412
  try {
410
- await stateManager.updateSessionId(issueNumber, specResult.sessionId);
413
+ await stateManager.updateResumeHandle(issueNumber, specResult.resumeHandle);
411
414
  }
412
415
  catch {
413
416
  // State tracking errors shouldn't stop execution
@@ -568,6 +571,20 @@ export async function runIssueWithLogging(ctx) {
568
571
  }
569
572
  }
570
573
  }
574
+ // #672 AC-2: surface the resolved phase pipeline to the renderer so it can
575
+ // seed pending cells for every phase before any one of them fires. This
576
+ // runs once per issue after all phase-list mutations (auto-detect, resume
577
+ // filter, testgen/security-review insertion). The full pipeline for the row
578
+ // is `spec` (if it already ran) plus the remaining `phases` array.
579
+ if (onPhasePlan) {
580
+ const fullPlan = specAlreadyRan ? ["spec", ...phases] : [...phases];
581
+ try {
582
+ onPhasePlan(issueNumber, fullPlan);
583
+ }
584
+ catch {
585
+ /* renderer wiring errors must not halt execution */
586
+ }
587
+ }
571
588
  // Build per-issue config with issue type metadata for skill env propagation
572
589
  const lowerLabelsForType = labels.map((l) => l.toLowerCase());
573
590
  const issueIsDocs = lowerLabelsForType.some((label) => DOCS_LABELS.some((docsLabel) => label === docsLabel));
@@ -610,15 +627,14 @@ export async function runIssueWithLogging(ctx) {
610
627
  }
611
628
  }
612
629
  const phaseStartTime = new Date();
613
- const result = await executePhaseWithRetry(issueNumber, phase, withActivityHook(issueConfig, issueNumber, phase, onProgress), sessionId, worktreePath, shutdownManager);
630
+ const result = await executePhaseWithRetry(issueNumber, phase, withActivityHook(issueConfig, issueNumber, phase, onProgress), resumeHandle, worktreePath, shutdownManager, phasePauseHandle);
614
631
  const phaseEndTime = new Date();
615
- // Capture session ID for subsequent phases
616
- if (result.sessionId) {
617
- sessionId = result.sessionId;
618
- // Update session ID in state for resume capability
632
+ // Capture resume handle for subsequent phases (#674).
633
+ if (result.resumeHandle) {
634
+ resumeHandle = result.resumeHandle;
619
635
  if (stateManager) {
620
636
  try {
621
- await stateManager.updateSessionId(issueNumber, result.sessionId);
637
+ await stateManager.updateResumeHandle(issueNumber, result.resumeHandle);
622
638
  }
623
639
  catch {
624
640
  // State tracking errors shouldn't stop execution
@@ -732,7 +748,7 @@ export async function runIssueWithLogging(ctx) {
732
748
  promptContext: buildLoopContext(result),
733
749
  };
734
750
  const loopStartTime = new Date();
735
- const loopResult = await executePhaseWithRetry(issueNumber, "loop", withActivityHook(loopConfig, issueNumber, "loop", onProgress), sessionId, worktreePath, shutdownManager);
751
+ const loopResult = await executePhaseWithRetry(issueNumber, "loop", withActivityHook(loopConfig, issueNumber, "loop", onProgress), resumeHandle, worktreePath, shutdownManager, phasePauseHandle);
736
752
  const loopEndTime = new Date();
737
753
  phaseResults.push(loopResult);
738
754
  // Emit loop completion/failure progress event (AC-8)
@@ -757,8 +773,8 @@ export async function runIssueWithLogging(ctx) {
757
773
  /* progress errors must not halt */
758
774
  }
759
775
  }
760
- if (loopResult.sessionId) {
761
- sessionId = loopResult.sessionId;
776
+ if (loopResult.resumeHandle) {
777
+ resumeHandle = loopResult.resumeHandle;
762
778
  }
763
779
  if (loopResult.success) {
764
780
  // Continue to next iteration