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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +125 -160
- package/dist/bin/cli.js +59 -4
- package/dist/dashboard/server.js +1 -0
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +2 -2
- package/dist/marketplace/external_plugins/sequant/README.md +6 -3
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +92 -0
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +18 -9
- package/dist/marketplace/external_plugins/sequant/hooks/relay-check.sh +107 -0
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/behavior-rule-detection.md +205 -0
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +21 -8
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +302 -86
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +109 -0
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +141 -22
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +83 -78
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +377 -137
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +28 -0
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +621 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +741 -232
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +47 -1
- package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +12 -6
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +217 -964
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/parallel-groups.md +7 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/quality-checklist.md +75 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +4 -2
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +0 -27
- package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +24 -44
- package/dist/src/commands/abort.d.ts +36 -0
- package/dist/src/commands/abort.js +138 -0
- package/dist/src/commands/prompt.d.ts +7 -0
- package/dist/src/commands/prompt.js +101 -7
- package/dist/src/commands/ready-tui-adapter.d.ts +59 -0
- package/dist/src/commands/ready-tui-adapter.js +130 -0
- package/dist/src/commands/ready.d.ts +49 -0
- package/dist/src/commands/ready.js +243 -0
- package/dist/src/commands/run-progress.d.ts +11 -1
- package/dist/src/commands/run-progress.js +20 -3
- package/dist/src/commands/run.js +12 -2
- package/dist/src/commands/status.js +4 -0
- package/dist/src/commands/watch.d.ts +2 -0
- package/dist/src/commands/watch.js +67 -3
- package/dist/src/lib/assess-collision-detect.js +1 -1
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +39 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +34 -2
- package/dist/src/lib/cli-ui/run-renderer.js +250 -33
- package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
- package/dist/src/lib/cli-ui/scrollback-harness.js +294 -0
- package/dist/src/lib/merge-check/types.js +1 -1
- package/dist/src/lib/relay/archive.js +6 -0
- package/dist/src/lib/relay/types.d.ts +2 -0
- package/dist/src/lib/relay/types.js +9 -0
- package/dist/src/lib/settings.d.ts +34 -0
- package/dist/src/lib/settings.js +23 -1
- package/dist/src/lib/workflow/batch-executor.js +34 -18
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
- package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
- package/dist/src/lib/workflow/drivers/aider.js +9 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
- package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
- package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
- package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
- package/dist/src/lib/workflow/event-emitter.js +102 -0
- package/dist/src/lib/workflow/notice.d.ts +32 -0
- package/dist/src/lib/workflow/notice.js +38 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +9 -21
- package/dist/src/lib/workflow/phase-executor.js +105 -117
- package/dist/src/lib/workflow/phase-mapper.d.ts +26 -13
- package/dist/src/lib/workflow/phase-mapper.js +55 -33
- package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
- package/dist/src/lib/workflow/phase-registry.js +233 -0
- package/dist/src/lib/workflow/platforms/github.d.ts +6 -0
- package/dist/src/lib/workflow/platforms/github.js +17 -0
- package/dist/src/lib/workflow/ready-gate.d.ts +155 -0
- package/dist/src/lib/workflow/ready-gate.js +374 -0
- package/dist/src/lib/workflow/reconcile.js +6 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts +5 -55
- package/dist/src/lib/workflow/run-orchestrator.d.ts +32 -2
- package/dist/src/lib/workflow/run-orchestrator.js +125 -11
- package/dist/src/lib/workflow/state-manager.d.ts +19 -1
- package/dist/src/lib/workflow/state-manager.js +27 -1
- package/dist/src/lib/workflow/state-schema.d.ts +23 -35
- package/dist/src/lib/workflow/state-schema.js +29 -3
- package/dist/src/lib/workflow/types.d.ts +74 -15
- package/dist/src/lib/workflow/types.js +18 -13
- package/dist/src/ui/tui/App.js +8 -2
- package/dist/src/ui/tui/IssueBox.js +3 -4
- package/dist/src/ui/tui/index.d.ts +13 -4
- package/dist/src/ui/tui/index.js +19 -5
- package/dist/src/ui/tui/row-cap.d.ts +51 -0
- package/dist/src/ui/tui/row-cap.js +76 -0
- package/dist/src/ui/tui/teardown.d.ts +20 -0
- package/dist/src/ui/tui/teardown.js +29 -0
- package/dist/src/ui/tui/theme.d.ts +3 -0
- package/dist/src/ui/tui/theme.js +3 -0
- package/package.json +23 -11
- package/templates/hooks/post-tool.sh +81 -0
- package/templates/skills/assess/SKILL.md +28 -28
- package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
- package/templates/skills/qa/SKILL.md +5 -2
- 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({
|
|
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
|
-
|
|
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
|
}
|
package/dist/src/commands/run.js
CHANGED
|
@@ -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({
|
|
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[];
|