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,374 @@
1
+ /**
2
+ * Ready gate engine (#683).
3
+ *
4
+ * Drives the `sequant ready <issue>` pipeline: a full-weight `qa → loop → qa`
5
+ * loop that reproduces the maintainer's manual fresh-session A+ pass
6
+ * deterministically, then STOPS at a human merge gate — it never merges.
7
+ *
8
+ * The loop's exit threshold is set by a **gate policy**:
9
+ *
10
+ * - `ac` (default): stop once no `AC_NOT_MET` verdict remains. Remaining
11
+ * quality/polish gaps are surfaced in the report but NOT auto-fixed. Findings
12
+ * that touch the issue's Non-Goals are report-only. Predictable, scope-
13
+ * respecting behavior for a team engineer with a fixed agenda.
14
+ * - `a-plus` (opt-in): loop toward `READY_FOR_MERGE`, auto-fixing quality gaps.
15
+ *
16
+ * Both policies are additionally bounded by `maxIterations`, an optional token
17
+ * budget, and the `LOOP_NO_DIFF` stagnation guard. The #534 class (zero-diff
18
+ * exec / null QA verdict) is never reported as ready.
19
+ *
20
+ * This module is the reusable engine — a future `sequant run --ready-gate`
21
+ * (out of scope for #683) can reuse `runReadyGate` directly. The command shell
22
+ * lives in `src/commands/ready.ts`.
23
+ */
24
+ import { snapshotLoopProgress, compareLoopProgress, } from "./qa-stagnation.js";
25
+ import { hasExecChanges } from "./phase-executor.js";
26
+ import { readTokenUsageFiles, aggregateTokenUsage, TOKEN_USAGE_DIR, } from "./token-utils.js";
27
+ import * as path from "path";
28
+ /**
29
+ * Pure exit predicate. Given a policy and a QA verdict, has the loop reached
30
+ * its stopping threshold?
31
+ *
32
+ * - `READY_FOR_MERGE` always stops (both policies).
33
+ * - `ac`: `AC_MET_BUT_NOT_A_PLUS` also stops — ACs are objectively met; the
34
+ * remaining gaps are quality-only and `ac` reports rather than fixes them.
35
+ * - `a-plus`: only `READY_FOR_MERGE` stops.
36
+ * - `AC_NOT_MET` / `NEEDS_VERIFICATION` never stop in either policy.
37
+ */
38
+ export function isAtThreshold(policy, verdict) {
39
+ if (verdict === "READY_FOR_MERGE")
40
+ return true;
41
+ if (policy === "ac")
42
+ return verdict === "AC_MET_BUT_NOT_A_PLUS";
43
+ return false;
44
+ }
45
+ const STOPWORDS = new Set([
46
+ "the",
47
+ "and",
48
+ "for",
49
+ "with",
50
+ "that",
51
+ "this",
52
+ "from",
53
+ "into",
54
+ "are",
55
+ "was",
56
+ "has",
57
+ "have",
58
+ "not",
59
+ "but",
60
+ "its",
61
+ "via",
62
+ "any",
63
+ "all",
64
+ "out",
65
+ "should",
66
+ "would",
67
+ "could",
68
+ "when",
69
+ "then",
70
+ "than",
71
+ "must",
72
+ "will",
73
+ ]);
74
+ function significantTokens(text) {
75
+ return new Set(text
76
+ .toLowerCase()
77
+ .replace(/[^a-z0-9\s-]/g, " ")
78
+ .split(/[\s-]+/)
79
+ .filter((w) => w.length > 3 && !STOPWORDS.has(w)));
80
+ }
81
+ /**
82
+ * Does a gap description overlap a Non-Goal? Conservative token-overlap
83
+ * heuristic: ≥2 shared significant words marks the gap as Non-Goal-touching.
84
+ *
85
+ * @internal Exported for testing only.
86
+ */
87
+ export function gapTouchesNonGoals(gap, nonGoals) {
88
+ if (nonGoals.length === 0)
89
+ return false;
90
+ const gapTokens = significantTokens(gap);
91
+ if (gapTokens.size === 0)
92
+ return false;
93
+ for (const ng of nonGoals) {
94
+ const ngTokens = significantTokens(ng);
95
+ let overlap = 0;
96
+ for (const t of ngTokens) {
97
+ if (gapTokens.has(t))
98
+ overlap++;
99
+ if (overlap >= 2)
100
+ return true;
101
+ }
102
+ }
103
+ return false;
104
+ }
105
+ /**
106
+ * Parse the issue body's Non-Goals section into a list of bullet items.
107
+ *
108
+ * Recognizes `## Non-goals`, `## Non-Goals`, `### Out of scope`, etc. Captures
109
+ * markdown bullet items until the next heading. Returns `[]` when no section is
110
+ * present.
111
+ *
112
+ * @internal Exported for testing only.
113
+ */
114
+ export function parseNonGoals(issueBody) {
115
+ if (!issueBody)
116
+ return [];
117
+ const lines = issueBody.split("\n");
118
+ const items = [];
119
+ let inSection = false;
120
+ const headingRe = /^#{1,6}\s+(.*)$/;
121
+ const nonGoalHeadingRe = /^(non-?goals?|out[ -]of[ -]scope)\b/i;
122
+ for (const line of lines) {
123
+ const heading = line.match(headingRe);
124
+ if (heading) {
125
+ inSection = nonGoalHeadingRe.test(heading[1].trim());
126
+ continue;
127
+ }
128
+ if (!inSection)
129
+ continue;
130
+ const bullet = line.match(/^\s*[-*]\s+(.+)$/);
131
+ if (bullet) {
132
+ // Strip surrounding markdown emphasis/backticks for cleaner matching.
133
+ const text = bullet[1].replace(/[`*]/g, "").trim();
134
+ if (text)
135
+ items.push(text);
136
+ }
137
+ }
138
+ return items;
139
+ }
140
+ function classifyGaps(gaps, nonGoals) {
141
+ return gaps.map((g) => ({
142
+ description: g,
143
+ nonGoal: gapTouchesNonGoals(g, nonGoals),
144
+ }));
145
+ }
146
+ function defaultReadTokensUsed(worktreePath) {
147
+ const dir = path.join(worktreePath, TOKEN_USAGE_DIR);
148
+ // Read without cleanup — the engine polls cumulatively across phases.
149
+ const files = readTokenUsageFiles(dir);
150
+ return aggregateTokenUsage(files).tokensUsed;
151
+ }
152
+ /**
153
+ * Build a minimal ExecutionConfig for a single ready-gate phase.
154
+ */
155
+ function buildPhaseConfig(opts, extra) {
156
+ return {
157
+ phases: [],
158
+ phaseTimeout: opts.phaseTimeout,
159
+ qualityLoop: false,
160
+ maxIterations: opts.maxIterations,
161
+ skipVerification: false,
162
+ sequential: true,
163
+ concurrency: 1,
164
+ parallel: false,
165
+ verbose: opts.verbose ?? false,
166
+ noSmartTests: false,
167
+ dryRun: false,
168
+ mcp: opts.mcp,
169
+ retry: true,
170
+ ...extra,
171
+ };
172
+ }
173
+ /**
174
+ * Render the structured gap report (AC-4).
175
+ *
176
+ * @internal Exported for testing only.
177
+ */
178
+ export function formatReadyReport(result) {
179
+ const headline = result.ready
180
+ ? "✅ READY — awaiting human merge decision"
181
+ : result.reason === "NO_IMPLEMENTATION"
182
+ ? "⛔ NOT READY — no implementation detected"
183
+ : "⚠️ NOT READY — needs human intervention";
184
+ const reasonText = {
185
+ AC_MET: "Acceptance Criteria are objectively met (no AC_NOT_MET). Remaining gaps are quality-only and reported below (policy `ac` does not auto-fix them).",
186
+ READY_FOR_MERGE: "QA returned READY_FOR_MERGE.",
187
+ MAX_ITERATIONS: "Hit the iteration cap before reaching the policy threshold. A human should review the remaining gaps.",
188
+ TOKEN_BUDGET: "Token budget exhausted before reaching the policy threshold. A human should review the remaining gaps.",
189
+ LOOP_NO_DIFF: "The fix loop made no diff (stagnation guard). Manual intervention required.",
190
+ LOOP_FAILED: "The fix loop phase failed. A human should investigate before merging.",
191
+ NO_IMPLEMENTATION: "Zero-diff worktree or null/unparseable QA verdict — there is nothing to certify (#534 guard).",
192
+ };
193
+ const lines = [];
194
+ lines.push(`## sequant ready — Issue #${result.issueNumber}`);
195
+ lines.push("");
196
+ lines.push(`**${headline}**`);
197
+ lines.push("");
198
+ lines.push(`- **Policy:** \`${result.policy}\``);
199
+ lines.push(`- **Final verdict:** ${result.finalVerdict ?? "(none)"}`);
200
+ lines.push(`- **Stop reason:** ${result.reason} — ${reasonText[result.reason]}`);
201
+ lines.push(`- **QA passes:** ${result.iterations}`);
202
+ lines.push(`- **Tokens used:** ${result.tokensUsed.toLocaleString()}`);
203
+ lines.push(`- **Final state:** \`${result.issueStatus}\` (never auto-merged)`);
204
+ lines.push("");
205
+ lines.push("### Auto-fixed");
206
+ if (result.autoFixed.length === 0) {
207
+ lines.push("- None");
208
+ }
209
+ else {
210
+ for (const item of result.autoFixed)
211
+ lines.push(`- ${item}`);
212
+ }
213
+ lines.push("");
214
+ lines.push("### Remaining / accepted gaps");
215
+ if (result.remaining.length === 0) {
216
+ lines.push("- None");
217
+ }
218
+ else {
219
+ for (const item of result.remaining) {
220
+ const tag = item.nonGoal ? " _(Non-Goal — report-only)_" : "";
221
+ lines.push(`- ${item.description}${tag}`);
222
+ }
223
+ }
224
+ lines.push("");
225
+ lines.push("> The human merge gate is intentional: `sequant ready` never merges. Review the gaps above, then merge manually when satisfied.");
226
+ return lines.join("\n");
227
+ }
228
+ /**
229
+ * Drive the policy-bounded `qa → loop → qa` ready gate.
230
+ */
231
+ export async function runReadyGate(opts) {
232
+ const { issueNumber, worktreePath, policy, maxIterations, tokenBudget, nonGoals = [], } = opts;
233
+ const readTokensUsed = opts.readTokensUsed ?? defaultReadTokensUsed;
234
+ const hasChangesFn = opts.hasChangesFn ?? hasExecChanges;
235
+ const snapshotFn = opts.snapshotFn ?? snapshotLoopProgress;
236
+ // #697: run a phase while emitting live-progress events around it. `iteration`
237
+ // is the 1-based QA-pass index so the renderer can render `loop N/M`. Emits
238
+ // `failed` (and re-throws) if the runner itself throws so the catch path in
239
+ // ready.ts disposes a renderer that already reflects the failed cell.
240
+ const runPhaseTracked = async (phase, config, iteration) => {
241
+ opts.onProgress?.(opts.issueNumber, phase, "start", { iteration });
242
+ let phaseResult;
243
+ try {
244
+ phaseResult = await opts.runPhase(phase, config, worktreePath);
245
+ }
246
+ catch (err) {
247
+ opts.onProgress?.(opts.issueNumber, phase, "failed", {
248
+ iteration,
249
+ error: err instanceof Error ? err.message : String(err),
250
+ });
251
+ throw err;
252
+ }
253
+ opts.onProgress?.(opts.issueNumber, phase, phaseResult.success === false ? "failed" : "complete", {
254
+ iteration,
255
+ durationSeconds: phaseResult.durationSeconds,
256
+ error: phaseResult.success === false ? phaseResult.error : undefined,
257
+ });
258
+ return phaseResult;
259
+ };
260
+ let iterations = 0;
261
+ let finalVerdict = null;
262
+ const autoFixed = [];
263
+ let remaining = [];
264
+ let tokensUsed = 0;
265
+ const finish = (reason) => {
266
+ const ready = reason === "AC_MET" || reason === "READY_FOR_MERGE";
267
+ const issueStatus = ready
268
+ ? "waiting_for_human_merge"
269
+ : "blocked";
270
+ const result = {
271
+ issueNumber,
272
+ policy,
273
+ ready,
274
+ reason,
275
+ issueStatus,
276
+ iterations,
277
+ finalVerdict,
278
+ autoFixed,
279
+ remaining,
280
+ tokensUsed,
281
+ report: "",
282
+ };
283
+ result.report = formatReadyReport(result);
284
+ return result;
285
+ };
286
+ const budgetExceeded = () => typeof tokenBudget === "number" &&
287
+ tokenBudget > 0 &&
288
+ tokensUsed >= tokenBudget;
289
+ // Loop is bounded by maxIterations QA passes. Each iteration: run QA, check
290
+ // the policy threshold + #534 guards, and (if not stopping) run one fix loop.
291
+ while (iterations < maxIterations) {
292
+ if (budgetExceeded()) {
293
+ return finish("TOKEN_BUDGET");
294
+ }
295
+ iterations++;
296
+ const qaResult = await runPhaseTracked("qa", buildPhaseConfig(opts, { fullQa: true }), iterations);
297
+ tokensUsed = readTokensUsed(worktreePath);
298
+ const verdict = qaResult.verdict ?? null;
299
+ // #534 guard: a null/unparseable verdict is never "ready".
300
+ if (!verdict) {
301
+ return finish("NO_IMPLEMENTATION");
302
+ }
303
+ // #534 guard: an empty worktree (no commits, no uncommitted work) is never
304
+ // "ready" — replays the #529/#570 empty-branch class.
305
+ if (!hasChangesFn(worktreePath)) {
306
+ return finish("NO_IMPLEMENTATION");
307
+ }
308
+ finalVerdict = verdict;
309
+ const gaps = qaResult.summary?.gaps ?? [];
310
+ remaining = classifyGaps(gaps, nonGoals);
311
+ // Policy threshold reached → stop at the human merge gate.
312
+ if (isAtThreshold(policy, verdict)) {
313
+ return finish(verdict === "READY_FOR_MERGE" ? "READY_FOR_MERGE" : "AC_MET");
314
+ }
315
+ // Not at threshold and out of iterations → clean halt, needs human.
316
+ if (iterations >= maxIterations) {
317
+ return finish("MAX_ITERATIONS");
318
+ }
319
+ if (budgetExceeded()) {
320
+ return finish("TOKEN_BUDGET");
321
+ }
322
+ // Run one fix loop. In `ac` mode we only reach here on AC_NOT_MET, so the
323
+ // gaps are AC gaps — feeding them via failedAcs keeps the loop scoped to
324
+ // the AC boundary (quality gaps are never fixed under `ac`). Non-Goal-
325
+ // touching findings are excluded from what we ask the loop to fix.
326
+ const fixableGaps = remaining
327
+ .filter((g) => !g.nonGoal)
328
+ .map((g) => g.description);
329
+ const before = snapshotFn(worktreePath);
330
+ const loopResult = await runPhaseTracked("loop", buildPhaseConfig(opts, {
331
+ lastVerdict: verdict,
332
+ failedAcs: fixableGaps.join("; ") || undefined,
333
+ promptContext: buildLoopContext(policy, verdict, fixableGaps),
334
+ }), iterations);
335
+ tokensUsed = readTokensUsed(worktreePath);
336
+ if (!loopResult.success) {
337
+ return finish("LOOP_FAILED");
338
+ }
339
+ const after = snapshotFn(worktreePath);
340
+ const progress = compareLoopProgress(before, after);
341
+ if (!progress.progressed) {
342
+ return finish("LOOP_NO_DIFF");
343
+ }
344
+ // The loop produced a diff — record what it was asked to fix and re-QA.
345
+ for (const g of fixableGaps) {
346
+ if (!autoFixed.includes(g))
347
+ autoFixed.push(g);
348
+ }
349
+ }
350
+ // Iteration cap reached without an explicit stop above (defensive).
351
+ return finish("MAX_ITERATIONS");
352
+ }
353
+ /**
354
+ * Build the prompt context handed to the `/loop` phase. Mirrors the
355
+ * batch-executor's `buildLoopContext` shape but scopes the instruction to the
356
+ * gate policy so `ac` runs do not chase quality-only gaps.
357
+ */
358
+ function buildLoopContext(policy, verdict, fixableGaps) {
359
+ const parts = [];
360
+ parts.push(`Ready gate (#683) — policy: ${policy}`);
361
+ parts.push(`QA Verdict: ${verdict}`);
362
+ if (policy === "ac") {
363
+ parts.push("Scope: fix ONLY the unmet Acceptance Criteria below. Do NOT address quality/polish gaps or anything touching the issue's Non-Goals — those are deliberately deferred under the `ac` policy.");
364
+ }
365
+ else {
366
+ parts.push("Scope: drive the work toward READY_FOR_MERGE by addressing the gaps below.");
367
+ }
368
+ if (fixableGaps.length > 0) {
369
+ parts.push("Gaps to address:");
370
+ for (const g of fixableGaps)
371
+ parts.push(`- ${g}`);
372
+ }
373
+ return parts.join("\n");
374
+ }
@@ -95,6 +95,12 @@ export function getNextActionHint(issue) {
95
95
  }
96
96
  case "waiting_for_qa_gate":
97
97
  return `sequant run ${issue.number} --phase qa`;
98
+ case "waiting_for_human_merge":
99
+ // `sequant ready` certified the work; a human reviews + merges manually.
100
+ if (issue.pr?.number) {
101
+ return `review gaps, then gh pr merge ${issue.pr.number}`;
102
+ }
103
+ return `review gaps, then merge manually`;
98
104
  case "ready_for_merge":
99
105
  if (issue.pr?.number) {
100
106
  return `gh pr merge ${issue.pr.number}`;
@@ -113,17 +113,7 @@ export type QaSummary = z.infer<typeof QaSummarySchema>;
113
113
  * Log entry for a single phase execution
114
114
  */
115
115
  export declare const PhaseLogSchema: z.ZodObject<{
116
- phase: z.ZodEnum<{
117
- qa: "qa";
118
- loop: "loop";
119
- verify: "verify";
120
- spec: "spec";
121
- "security-review": "security-review";
122
- exec: "exec";
123
- testgen: "testgen";
124
- test: "test";
125
- merger: "merger";
126
- }>;
116
+ phase: z.ZodString;
127
117
  issueNumber: z.ZodNumber;
128
118
  startTime: z.ZodString;
129
119
  endTime: z.ZodString;
@@ -199,17 +189,7 @@ export declare const IssueLogSchema: z.ZodObject<{
199
189
  partial: "partial";
200
190
  }>;
201
191
  phases: z.ZodArray<z.ZodObject<{
202
- phase: z.ZodEnum<{
203
- qa: "qa";
204
- loop: "loop";
205
- verify: "verify";
206
- spec: "spec";
207
- "security-review": "security-review";
208
- exec: "exec";
209
- testgen: "testgen";
210
- test: "test";
211
- merger: "merger";
212
- }>;
192
+ phase: z.ZodString;
213
193
  issueNumber: z.ZodNumber;
214
194
  startTime: z.ZodString;
215
195
  endTime: z.ZodString;
@@ -280,17 +260,7 @@ export type IssueLog = z.infer<typeof IssueLogSchema>;
280
260
  * Run configuration
281
261
  */
282
262
  export declare const RunConfigSchema: z.ZodObject<{
283
- phases: z.ZodArray<z.ZodEnum<{
284
- qa: "qa";
285
- loop: "loop";
286
- verify: "verify";
287
- spec: "spec";
288
- "security-review": "security-review";
289
- exec: "exec";
290
- testgen: "testgen";
291
- test: "test";
292
- merger: "merger";
293
- }>>;
263
+ phases: z.ZodArray<z.ZodString>;
294
264
  sequential: z.ZodBoolean;
295
265
  qualityLoop: z.ZodBoolean;
296
266
  maxIterations: z.ZodNumber;
@@ -319,17 +289,7 @@ export declare const RunLogSchema: z.ZodObject<{
319
289
  startTime: z.ZodString;
320
290
  endTime: z.ZodString;
321
291
  config: z.ZodObject<{
322
- phases: z.ZodArray<z.ZodEnum<{
323
- qa: "qa";
324
- loop: "loop";
325
- verify: "verify";
326
- spec: "spec";
327
- "security-review": "security-review";
328
- exec: "exec";
329
- testgen: "testgen";
330
- test: "test";
331
- merger: "merger";
332
- }>>;
292
+ phases: z.ZodArray<z.ZodString>;
333
293
  sequential: z.ZodBoolean;
334
294
  qualityLoop: z.ZodBoolean;
335
295
  maxIterations: z.ZodNumber;
@@ -346,17 +306,7 @@ export declare const RunLogSchema: z.ZodObject<{
346
306
  partial: "partial";
347
307
  }>;
348
308
  phases: z.ZodArray<z.ZodObject<{
349
- phase: z.ZodEnum<{
350
- qa: "qa";
351
- loop: "loop";
352
- verify: "verify";
353
- spec: "spec";
354
- "security-review": "security-review";
355
- exec: "exec";
356
- testgen: "testgen";
357
- test: "test";
358
- merger: "merger";
359
- }>;
309
+ phase: z.ZodString;
360
310
  issueNumber: z.ZodNumber;
361
311
  startTime: z.ZodString;
362
312
  endTime: z.ZodString;
@@ -6,13 +6,14 @@
6
6
  *
7
7
  * @module
8
8
  */
9
- import type { ExecutionConfig, IssueResult, RunOptions, ProgressCallback } from "./types.js";
9
+ import type { ExecutionConfig, IssueResult, RunOptions, ProgressCallback, PhasePlanCallback, PhasePauseHandle } from "./types.js";
10
10
  import type { RunSnapshot } from "./run-state.js";
11
11
  import type { WorktreeInfo } from "./worktree-manager.js";
12
12
  import { LogWriter } from "./log-writer.js";
13
13
  import { StateManager } from "./state-manager.js";
14
14
  import { ShutdownManager } from "../shutdown.js";
15
15
  import type { LockFile } from "../locks/index.js";
16
+ import { WorkflowEventEmitter } from "./event-emitter.js";
16
17
  import type { SequantSettings } from "../settings.js";
17
18
  /**
18
19
  * Build the stack-manifest line emitted into PR bodies under --stacked.
@@ -56,6 +57,14 @@ export interface OrchestratorConfig {
56
57
  baseBranch?: string;
57
58
  /** Per-phase progress callback (parallel mode) */
58
59
  onProgress?: ProgressCallback;
60
+ /** #672 AC-2: phase-plan callback forwarded into per-issue contexts. */
61
+ onPhasePlan?: PhasePlanCallback;
62
+ /**
63
+ * Optional live-zone pause handle (#656). Forwarded to every issue's
64
+ * batch context so `executePhaseWithRetry` can quiesce the renderer
65
+ * around verbose Claude streaming.
66
+ */
67
+ phasePauseHandle?: PhasePauseHandle;
59
68
  }
60
69
  /**
61
70
  * High-level init config for full lifecycle execution.
@@ -75,6 +84,15 @@ export interface RunInit {
75
84
  baseBranch?: string;
76
85
  /** Per-phase progress callback */
77
86
  onProgress?: ProgressCallback;
87
+ /** #672 AC-2: phase-plan callback. Fired once per issue once the executor
88
+ * has resolved the final phase pipeline. */
89
+ onPhasePlan?: PhasePlanCallback;
90
+ /**
91
+ * Optional live-zone pause handle (#656). Threaded through to the
92
+ * `OrchestratorConfig` so verbose Claude streaming pauses the renderer's
93
+ * live zone instead of redrawing over it.
94
+ */
95
+ phasePauseHandle?: PhasePauseHandle;
78
96
  /**
79
97
  * Invoked once the orchestrator is constructed but before execution begins.
80
98
  * Used by the experimental TUI to attach a snapshot poller to the active
@@ -146,8 +164,16 @@ export declare class RunOrchestrator {
146
164
  private readonly cfg;
147
165
  private readonly issueStates;
148
166
  private readonly phaseStartTimes;
167
+ private readonly emitter;
149
168
  private done;
150
169
  constructor(config: OrchestratorConfig);
170
+ /**
171
+ * Returns the workflow event emitter. External consumers (TUI, MCP server,
172
+ * future webhooks) call `getEmitter().on(...)` to subscribe to lifecycle
173
+ * events. Subscribing is opt-in — the orchestrator runs unaware of who is
174
+ * listening (#504, AC-3).
175
+ */
176
+ getEmitter(): WorkflowEventEmitter;
151
177
  /**
152
178
  * Point-in-time view of the entire run.
153
179
  *
@@ -157,7 +183,11 @@ export declare class RunOrchestrator {
157
183
  * observing torn writes.
158
184
  */
159
185
  getSnapshot(): RunSnapshot;
160
- /** Mark the run as completed so the dashboard can unmount. */
186
+ /**
187
+ * Mark the run as completed so the dashboard can unmount and drop event
188
+ * subscribers. Drains the emitter to prevent leaks across multiple
189
+ * `run()` invocations in the same process (e.g. the MCP server).
190
+ */
161
191
  markDone(): void;
162
192
  private initIssueStates;
163
193
  private wrapProgress;