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,130 @@
1
+ /**
2
+ * Single-issue snapshot adapter for `sequant ready` (#699 Part A).
3
+ *
4
+ * The Ink TUI is pull-based: `App` polls `getSnapshot(): RunSnapshot` at 10 Hz
5
+ * (see `src/ui/tui/index.ts`). But `ready` has no `RunOrchestrator` — its
6
+ * progress arrives push-style via the gate's `onProgress` hook (#697). This
7
+ * adapter bridges the two: a small mutable tracker the gate feeds, exposing a
8
+ * `getSnapshot()` that returns a one-issue `RunSnapshot` for the TUI to mount.
9
+ *
10
+ * The gate fires `start`/`complete`/`failed` around each `qa`/`loop` pass; we
11
+ * model those passes as the phase row, with a coarse `nowLine`
12
+ * (`formatCoarseNowLine`) — no agent-stream enrichment (a stated Non-Goal).
13
+ */
14
+ import { formatCoarseNowLine, } from "../lib/workflow/run-state.js";
15
+ /**
16
+ * Mutable single-issue runtime tracker that doubles as a TUI snapshot provider.
17
+ *
18
+ * Lifecycle: construct → mount `renderTui(adapter)` → pass `adapter.onProgress`
19
+ * to the gate → on gate resolution call `markDone(ready)` so the polling `App`
20
+ * sees `done` and unmounts.
21
+ */
22
+ export class ReadySnapshotAdapter {
23
+ issueNumber;
24
+ title;
25
+ branch;
26
+ qualityLoop;
27
+ status = "queued";
28
+ phases = [];
29
+ currentPhase;
30
+ startedAt;
31
+ completedAt;
32
+ finished = false;
33
+ constructor(opts) {
34
+ this.issueNumber = opts.issueNumber;
35
+ this.title = opts.title;
36
+ this.branch = opts.branch;
37
+ this.qualityLoop = opts.qualityLoop ?? true;
38
+ }
39
+ /**
40
+ * `ProgressCallback`-shaped sink wired into the gate's `onProgress`.
41
+ *
42
+ * - `start` → append a running phase + set `currentPhase` (coarse nowLine).
43
+ * - `complete` → mark the active phase done, record elapsed, clear nowLine.
44
+ * - `failed` → mark the active phase failed, flip issue status to failed.
45
+ * - `activity` → refresh the activity stamp / nowLine if a finer signal lands.
46
+ */
47
+ onProgress = (_issue, phase, event, extra) => {
48
+ const now = new Date();
49
+ switch (event) {
50
+ case "start": {
51
+ if (!this.startedAt)
52
+ this.startedAt = now;
53
+ this.status = "running";
54
+ this.phases.push({ name: phase, status: "running", startedAt: now });
55
+ this.currentPhase = {
56
+ name: phase,
57
+ startedAt: now,
58
+ lastActivityAt: now,
59
+ nowLine: formatCoarseNowLine(phase),
60
+ };
61
+ break;
62
+ }
63
+ case "complete":
64
+ case "failed": {
65
+ const active = this.phases[this.phases.length - 1];
66
+ if (active && active.status === "running") {
67
+ active.status = event === "failed" ? "failed" : "done";
68
+ active.elapsedMs =
69
+ extra?.durationSeconds != null
70
+ ? Math.round(extra.durationSeconds * 1000)
71
+ : active.startedAt
72
+ ? now.getTime() - active.startedAt.getTime()
73
+ : undefined;
74
+ }
75
+ this.currentPhase = undefined;
76
+ if (event === "failed")
77
+ this.status = "failed";
78
+ break;
79
+ }
80
+ case "activity": {
81
+ if (this.currentPhase) {
82
+ this.currentPhase = {
83
+ ...this.currentPhase,
84
+ lastActivityAt: now,
85
+ nowLine: extra?.text?.trim()
86
+ ? extra.text.trim()
87
+ : this.currentPhase.nowLine,
88
+ };
89
+ }
90
+ break;
91
+ }
92
+ }
93
+ };
94
+ /**
95
+ * Mark the run finished after `runReadyGate` resolves. Flips the snapshot's
96
+ * `done` flag so the polling `App` unmounts, and sets a terminal status
97
+ * (failed wins if a phase already failed).
98
+ */
99
+ markDone(ready) {
100
+ this.completedAt = new Date();
101
+ this.currentPhase = undefined;
102
+ if (this.status !== "failed") {
103
+ this.status = ready ? "passed" : "failed";
104
+ }
105
+ this.finished = true;
106
+ }
107
+ /** Pull-based snapshot consumed by the TUI's 10 Hz poll loop. */
108
+ getSnapshot() {
109
+ const issue = {
110
+ number: this.issueNumber,
111
+ title: this.title,
112
+ branch: this.branch,
113
+ status: this.status,
114
+ phases: this.phases.map((p) => ({ ...p })),
115
+ currentPhase: this.currentPhase ? { ...this.currentPhase } : undefined,
116
+ startedAt: this.startedAt,
117
+ completedAt: this.completedAt,
118
+ };
119
+ return {
120
+ config: {
121
+ concurrency: 1,
122
+ baseBranch: this.branch,
123
+ qualityLoop: this.qualityLoop,
124
+ },
125
+ issues: [issue],
126
+ done: this.finished,
127
+ capturedAt: new Date(),
128
+ };
129
+ }
130
+ }
@@ -0,0 +1,49 @@
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 { type ReadyPolicy } from "../lib/settings.js";
18
+ import { type ReadyResult } from "../lib/workflow/ready-gate.js";
19
+ export interface ReadyCommandOptions {
20
+ policy?: string;
21
+ maxIterations?: number;
22
+ budget?: number;
23
+ timeout?: number;
24
+ /** Commander surfaces `--no-mcp` as `mcp === false`. */
25
+ mcp?: boolean;
26
+ json?: boolean;
27
+ verbose?: boolean;
28
+ }
29
+ /**
30
+ * Exit code from a ready result.
31
+ * - 0: ready (awaiting human merge)
32
+ * - 1: not ready — needs human intervention (budget/iterations/stagnation)
33
+ * - 2: not ready — no implementation (#534) or hard error
34
+ */
35
+ export declare function getReadyExitCode(result: ReadyResult): number;
36
+ /**
37
+ * Resolve the gate policy: `--policy` flag > settings.ready.policy > "ac".
38
+ * Invalid flag values fall back to the settings/default value.
39
+ *
40
+ * @internal Exported for testing only.
41
+ */
42
+ export declare function resolvePolicy(flag: string | undefined, settingsPolicy: ReadyPolicy): ReadyPolicy;
43
+ /**
44
+ * Locate the worktree path for an issue from `git worktree list`.
45
+ *
46
+ * @internal Exported for testing only.
47
+ */
48
+ export declare function resolveWorktreePath(issueNumber: number): string | null;
49
+ export declare function readyCommand(issueArg: string, options: ReadyCommandOptions): Promise<void>;
@@ -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
+ }
@@ -8,11 +8,14 @@
8
8
  */
9
9
  import type { RunRenderer } from "../lib/cli-ui/run-renderer-types.js";
10
10
  import { LivenessHeartbeat } from "../lib/workflow/heartbeat.js";
11
- import type { ProgressCallback } from "../lib/workflow/types.js";
11
+ import type { ProgressCallback, PhasePlanCallback } from "../lib/workflow/types.js";
12
12
  export interface ProgressWiring {
13
13
  renderer: RunRenderer | null;
14
14
  heartbeat: LivenessHeartbeat | null;
15
15
  onProgress: ProgressCallback | undefined;
16
+ /** #672 AC-2: forwarded to the orchestrator so batch-executor can hand the
17
+ * resolved phase pipeline back to the renderer once it's known. */
18
+ onPhasePlan: PhasePlanCallback | undefined;
16
19
  }
17
20
  /**
18
21
  * Construct the renderer + heartbeat + onProgress callback for a run.
@@ -27,6 +30,13 @@ export declare function buildProgressWiring(args: {
27
30
  /** AC-23: when auto-detect mode is on, the renderer shows `Phase: detecting…`
28
31
  * while spec runs (before the resolved plan is known). */
29
32
  autoDetectPhases?: boolean;
33
+ /** #672 AC-2: the base configured phase pipeline. In explicit-phase mode
34
+ * (not auto-detect) this is known upfront, so every issue — including those
35
+ * still queued behind the active one — can show its roadmap immediately
36
+ * rather than only once it starts running. `setPhasePlan` later refines it
37
+ * per issue (e.g. testgen/security-review insertion). Ignored in
38
+ * auto-detect mode, where the plan isn't known until spec resolves it. */
39
+ basePhases?: string[];
30
40
  /** #624 Item 3 / D2: total allowed quality-loop iterations (from settings). */
31
41
  maxLoopIterations?: number;
32
42
  }): ProgressWiring;
@@ -14,7 +14,7 @@ import { LivenessHeartbeat } from "../lib/workflow/heartbeat.js";
14
14
  * `tuiEnabled` and `quiet` are mutually exclusive with the renderer path.
15
15
  */
16
16
  export function buildProgressWiring(args) {
17
- const { tuiEnabled, quiet, issueNumbers, phaseTimeoutSeconds, autoDetectPhases, maxLoopIterations, } = args;
17
+ const { tuiEnabled, quiet, issueNumbers, phaseTimeoutSeconds, autoDetectPhases, basePhases, maxLoopIterations, } = args;
18
18
  const heartbeat = quiet && !tuiEnabled
19
19
  ? new LivenessHeartbeat({ phaseTimeoutSeconds })
20
20
  : null;
@@ -36,8 +36,20 @@ export function buildProgressWiring(args) {
36
36
  })
37
37
  : null;
38
38
  if (renderer) {
39
+ // #672 AC-2: seed the planned pipeline at registration when it's known
40
+ // upfront (explicit-phase mode). This makes queued issues render their
41
+ // roadmap before they start, matching the issue's multi-row matrix
42
+ // mock-up. In auto-detect mode the plan isn't known yet, so we leave
43
+ // `plannedPhases` undefined and rely on `setPhasePlan` once spec resolves.
44
+ const seedPlan = !autoDetectPhases && basePhases && basePhases.length > 0
45
+ ? basePhases
46
+ : undefined;
39
47
  for (const issueNumber of issueNumbers) {
40
- renderer.registerIssue({ issueNumber, autoDetect: autoDetectPhases });
48
+ renderer.registerIssue({
49
+ issueNumber,
50
+ autoDetect: autoDetectPhases,
51
+ plannedPhases: seedPlan,
52
+ });
41
53
  }
42
54
  }
43
55
  let onProgress;
@@ -72,5 +84,10 @@ export function buildProgressWiring(args) {
72
84
  heartbeat.stop({ issueNumber: issue, phase });
73
85
  };
74
86
  }
75
- return { renderer, heartbeat, onProgress };
87
+ // #672 AC-2: only the renderer path consumes a phase plan; quiet / TUI
88
+ // modes leave this undefined and the orchestrator no-ops the callback.
89
+ const onPhasePlan = renderer
90
+ ? (issue, phases) => renderer.setPhasePlan(issue, phases)
91
+ : undefined;
92
+ return { renderer, heartbeat, onProgress, onPhasePlan };
76
93
  }
@@ -72,12 +72,15 @@ export async function runCommand(issues, options) {
72
72
  const tuiEnabled = Boolean(options.experimentalTui) && Boolean(process.stdout.isTTY);
73
73
  // RunRenderer (#618) + LivenessHeartbeat (#574) wiring lives in
74
74
  // run-progress.ts to keep this adapter under the 200-LOC cap (#503 AC-2).
75
- const { renderer, heartbeat, onProgress } = buildProgressWiring({
75
+ const { renderer, heartbeat, onProgress, onPhasePlan } = buildProgressWiring({
76
76
  tuiEnabled,
77
77
  quiet: Boolean(options.quiet),
78
78
  issueNumbers: resolved.issueNumbers,
79
79
  phaseTimeoutSeconds: settings.run.timeout,
80
80
  autoDetectPhases: resolved.autoDetectPhases,
81
+ // #672 AC-2: base pipeline so queued issues show their roadmap upfront in
82
+ // explicit-phase mode (ignored when auto-detect resolves the plan later).
83
+ basePhases: resolved.config.phases,
81
84
  // #624 Item 3 / D2: route the resolved maxIterations into the renderer so
82
85
  // `(attempt N/M)` and `loop N/M` reflect actual configured limits.
83
86
  maxLoopIterations: resolved.config.maxIterations,
@@ -97,6 +100,8 @@ export async function runCommand(issues, options) {
97
100
  const result = await RunOrchestrator.run({
98
101
  ...init,
99
102
  onProgress,
103
+ onPhasePlan,
104
+ phasePauseHandle: renderer ?? undefined,
100
105
  onOrchestratorReady: (orch) => {
101
106
  tuiHandle = renderTui(orch);
102
107
  },
@@ -121,7 +126,12 @@ export async function runCommand(issues, options) {
121
126
  if (renderer)
122
127
  process.once("SIGINT", sigintHandler);
123
128
  try {
124
- const result = await RunOrchestrator.run({ ...init, onProgress }, issues, batches);
129
+ const result = await RunOrchestrator.run({
130
+ ...init,
131
+ onProgress,
132
+ onPhasePlan,
133
+ phasePauseHandle: renderer ?? undefined,
134
+ }, issues, batches);
125
135
  // Record PR info in renderer state before summary so done rows show PR #s.
126
136
  if (renderer) {
127
137
  for (const r of result.results) {
@@ -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",
@@ -9,6 +9,8 @@ export interface WatchCommandOptions {
9
9
  pollIntervalMs?: number;
10
10
  /** Abort signal for clean shutdown (tests). */
11
11
  signal?: AbortSignal;
12
+ /** Override cwd for resolving the pid file + archive root (test seam). */
13
+ cwd?: string;
12
14
  }
13
15
  export declare function watchCommand(argsAndOptions: {
14
16
  args: string[];