sequant 2.2.0 → 2.4.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +81 -5
- package/dist/bin/cli.js +140 -13
- package/dist/src/commands/abort.d.ts +36 -0
- package/dist/src/commands/abort.js +138 -0
- package/dist/src/commands/doctor.d.ts +25 -0
- package/dist/src/commands/doctor.js +36 -1
- package/dist/src/commands/locks.d.ts +67 -0
- package/dist/src/commands/locks.js +290 -0
- package/dist/src/commands/merge.js +11 -0
- package/dist/src/commands/prompt.d.ts +46 -0
- package/dist/src/commands/prompt.js +273 -0
- package/dist/src/commands/run-display.d.ts +11 -2
- package/dist/src/commands/run-display.js +62 -28
- package/dist/src/commands/run-progress.d.ts +42 -0
- package/dist/src/commands/run-progress.js +93 -0
- package/dist/src/commands/run.js +90 -18
- package/dist/src/commands/stats.d.ts +2 -0
- package/dist/src/commands/stats.js +94 -8
- package/dist/src/commands/status.js +12 -0
- package/dist/src/commands/watch.d.ts +18 -0
- package/dist/src/commands/watch.js +211 -0
- package/dist/src/lib/ac-linter.d.ts +1 -1
- package/dist/src/lib/ac-linter.js +81 -0
- package/dist/src/lib/assess-collision-detect.d.ts +91 -0
- package/dist/src/lib/assess-collision-detect.js +217 -0
- package/dist/src/lib/assess-comment-parser.d.ts +59 -1
- package/dist/src/lib/assess-comment-parser.js +124 -2
- package/dist/src/lib/cli-ui/format.d.ts +19 -0
- package/dist/src/lib/cli-ui/format.js +34 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +220 -0
- package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +265 -0
- package/dist/src/lib/cli-ui/run-renderer.js +1390 -0
- 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/heuristics/behavior-rule-detector.d.ts +94 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
- package/dist/src/lib/locks/index.d.ts +7 -0
- package/dist/src/lib/locks/index.js +5 -0
- package/dist/src/lib/locks/lock-manager.d.ts +168 -0
- package/dist/src/lib/locks/lock-manager.js +433 -0
- package/dist/src/lib/locks/types.d.ts +59 -0
- package/dist/src/lib/locks/types.js +31 -0
- package/dist/src/lib/merge-check/types.js +1 -1
- package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
- package/dist/src/lib/qa/markdown-only-ci.js +74 -0
- package/dist/src/lib/relay/activation.d.ts +60 -0
- package/dist/src/lib/relay/activation.js +122 -0
- package/dist/src/lib/relay/archive.d.ts +34 -0
- package/dist/src/lib/relay/archive.js +112 -0
- package/dist/src/lib/relay/frame.d.ts +20 -0
- package/dist/src/lib/relay/frame.js +76 -0
- package/dist/src/lib/relay/index.d.ts +13 -0
- package/dist/src/lib/relay/index.js +13 -0
- package/dist/src/lib/relay/paths.d.ts +43 -0
- package/dist/src/lib/relay/paths.js +59 -0
- package/dist/src/lib/relay/pid.d.ts +34 -0
- package/dist/src/lib/relay/pid.js +72 -0
- package/dist/src/lib/relay/reader.d.ts +35 -0
- package/dist/src/lib/relay/reader.js +115 -0
- package/dist/src/lib/relay/types.d.ts +70 -0
- package/dist/src/lib/relay/types.js +85 -0
- package/dist/src/lib/relay/writer.d.ts +48 -0
- package/dist/src/lib/relay/writer.js +113 -0
- package/dist/src/lib/settings.d.ts +31 -1
- package/dist/src/lib/settings.js +18 -3
- package/dist/src/lib/version-check.d.ts +60 -5
- package/dist/src/lib/version-check.js +97 -9
- package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
- package/dist/src/lib/workflow/batch-executor.js +274 -185
- package/dist/src/lib/workflow/config-resolver.js +4 -0
- 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/heartbeat.d.ts +71 -0
- package/dist/src/lib/workflow/heartbeat.js +194 -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 +58 -16
- package/dist/src/lib/workflow/phase-executor.js +244 -130
- package/dist/src/lib/workflow/phase-mapper.d.ts +27 -13
- package/dist/src/lib/workflow/phase-mapper.js +70 -51
- 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 +1 -1
- package/dist/src/lib/workflow/platforms/github.js +20 -3
- package/dist/src/lib/workflow/pr-status.d.ts +18 -2
- package/dist/src/lib/workflow/pr-status.js +41 -9
- package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
- package/dist/src/lib/workflow/qa-stagnation.js +179 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts +5 -55
- package/dist/src/lib/workflow/run-orchestrator.d.ts +70 -1
- package/dist/src/lib/workflow/run-orchestrator.js +464 -25
- package/dist/src/lib/workflow/run-reflect.js +1 -1
- package/dist/src/lib/workflow/run-state.d.ts +71 -0
- package/dist/src/lib/workflow/run-state.js +14 -0
- package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
- package/dist/src/lib/workflow/state-cleanup.js +17 -5
- package/dist/src/lib/workflow/state-manager.d.ts +31 -2
- package/dist/src/lib/workflow/state-manager.js +64 -1
- package/dist/src/lib/workflow/state-schema.d.ts +82 -35
- package/dist/src/lib/workflow/state-schema.js +63 -4
- package/dist/src/lib/workflow/types.d.ts +139 -16
- package/dist/src/lib/workflow/types.js +18 -13
- package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
- package/dist/src/lib/workflow/worktree-manager.js +15 -6
- package/dist/src/mcp/tools/run.d.ts +44 -0
- package/dist/src/mcp/tools/run.js +104 -13
- package/dist/src/ui/tui/App.d.ts +14 -0
- package/dist/src/ui/tui/App.js +41 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
- package/dist/src/ui/tui/ElapsedTimer.js +31 -0
- package/dist/src/ui/tui/Header.d.ts +6 -0
- package/dist/src/ui/tui/Header.js +15 -0
- package/dist/src/ui/tui/IssueBox.d.ts +16 -0
- package/dist/src/ui/tui/IssueBox.js +68 -0
- package/dist/src/ui/tui/Spinner.d.ts +9 -0
- package/dist/src/ui/tui/Spinner.js +18 -0
- package/dist/src/ui/tui/index.d.ts +15 -0
- package/dist/src/ui/tui/index.js +29 -0
- package/dist/src/ui/tui/theme.d.ts +29 -0
- package/dist/src/ui/tui/theme.js +52 -0
- package/dist/src/ui/tui/truncate.d.ts +11 -0
- package/dist/src/ui/tui/truncate.js +31 -0
- package/package.json +14 -6
- package/templates/agents/sequant-explorer.md +1 -0
- package/templates/agents/sequant-qa-checker.md +2 -1
- package/templates/agents/sequant-testgen.md +1 -0
- package/templates/hooks/post-tool.sh +92 -0
- package/templates/hooks/pre-tool.sh +18 -9
- package/templates/hooks/relay-check.sh +107 -0
- package/templates/relay/frame.txt +11 -0
- package/templates/scripts/cleanup-worktree.sh +25 -3
- package/templates/scripts/new-feature.sh +6 -0
- package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
- package/templates/skills/_shared/references/subagent-types.md +21 -8
- package/templates/skills/assess/SKILL.md +122 -68
- package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
- package/templates/skills/docs/SKILL.md +141 -22
- package/templates/skills/exec/SKILL.md +10 -8
- package/templates/skills/fullsolve/SKILL.md +79 -5
- package/templates/skills/loop/SKILL.md +28 -0
- package/templates/skills/merger/SKILL.md +621 -0
- package/templates/skills/qa/SKILL.md +727 -8
- package/templates/skills/setup/SKILL.md +12 -6
- package/templates/skills/spec/SKILL.md +52 -0
- package/templates/skills/spec/references/parallel-groups.md +7 -0
- package/templates/skills/spec/references/recommended-workflow.md +4 -2
- package/templates/skills/testgen/SKILL.md +24 -17
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
* Display helpers for `sequant run` — pre-run config block + post-run summary.
|
|
3
3
|
*
|
|
4
4
|
* Kept separate from run.ts so the adapter stays thin (see AC-2 of #503).
|
|
5
|
+
*
|
|
6
|
+
* As of #618, the post-run summary delegates to the unified RunRenderer when
|
|
7
|
+
* one is provided. The renderless path (used by --experimental-tui and tests)
|
|
8
|
+
* falls back to `renderRunSummary` so output stays consistent across modes.
|
|
5
9
|
*/
|
|
10
|
+
import type { RunRenderer } from "../lib/cli-ui/run-renderer-types.js";
|
|
6
11
|
import type { ResolvedRun, RunResult } from "../lib/workflow/run-orchestrator.js";
|
|
7
12
|
/**
|
|
8
13
|
* Print pre-run config block.
|
|
@@ -12,6 +17,10 @@ import type { ResolvedRun, RunResult } from "../lib/workflow/run-orchestrator.js
|
|
|
12
17
|
*/
|
|
13
18
|
export declare function displayConfig(r: ResolvedRun): void;
|
|
14
19
|
/**
|
|
15
|
-
* Print post-run summary: per-issue
|
|
20
|
+
* Print post-run summary: per-issue grid, log path, reflection, tips.
|
|
21
|
+
*
|
|
22
|
+
* If a renderer is provided (default path), delegate to its `renderSummary`
|
|
23
|
+
* so the live zone is torn down cleanly first. Otherwise, fall back to the
|
|
24
|
+
* shared `renderRunSummary` helper (used by tests and TUI mode).
|
|
16
25
|
*/
|
|
17
|
-
export declare function displaySummary(result: RunResult): void;
|
|
26
|
+
export declare function displaySummary(result: RunResult, renderer?: RunRenderer | null): void;
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* Display helpers for `sequant run` — pre-run config block + post-run summary.
|
|
3
3
|
*
|
|
4
4
|
* Kept separate from run.ts so the adapter stays thin (see AC-2 of #503).
|
|
5
|
+
*
|
|
6
|
+
* As of #618, the post-run summary delegates to the unified RunRenderer when
|
|
7
|
+
* one is provided. The renderless path (used by --experimental-tui and tests)
|
|
8
|
+
* falls back to `renderRunSummary` so output stays consistent across modes.
|
|
5
9
|
*/
|
|
6
10
|
import chalk from "chalk";
|
|
7
11
|
import { ui, colors } from "../lib/cli-ui.js";
|
|
8
|
-
import {
|
|
12
|
+
import { renderRunSummary } from "../lib/cli-ui/run-renderer.js";
|
|
9
13
|
import { analyzeRun, formatReflection } from "../lib/workflow/run-reflect.js";
|
|
10
14
|
/**
|
|
11
15
|
* Print pre-run config block.
|
|
@@ -21,7 +25,7 @@ export function displayConfig(r) {
|
|
|
21
25
|
row("Phases", "auto-detect from labels");
|
|
22
26
|
}
|
|
23
27
|
else {
|
|
24
|
-
row("Phases", r.config.phases.join("
|
|
28
|
+
row("Phases", r.config.phases.join(" → "));
|
|
25
29
|
}
|
|
26
30
|
row("Mode", r.config.sequential
|
|
27
31
|
? "sequential (stop-on-failure)"
|
|
@@ -60,36 +64,61 @@ export function displayConfig(r) {
|
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
63
|
-
*
|
|
67
|
+
* Convert workflow `IssueResult` to renderer `IssueSummary`.
|
|
68
|
+
*/
|
|
69
|
+
function toIssueSummary(r) {
|
|
70
|
+
const failedPhase = r.phaseResults.find((p) => !p.success);
|
|
71
|
+
const summary = {
|
|
72
|
+
issueNumber: r.issueNumber,
|
|
73
|
+
success: r.success,
|
|
74
|
+
durationSeconds: r.durationSeconds,
|
|
75
|
+
phases: r.phaseResults.map((p) => ({ name: p.phase, success: p.success })),
|
|
76
|
+
loopTriggered: r.loopTriggered,
|
|
77
|
+
prNumber: r.prNumber,
|
|
78
|
+
prUrl: r.prUrl,
|
|
79
|
+
};
|
|
80
|
+
if (!r.success) {
|
|
81
|
+
summary.failureReason =
|
|
82
|
+
failedPhase?.error ??
|
|
83
|
+
r.abortReason ??
|
|
84
|
+
`${failedPhase?.phase ?? "phase"} failed`;
|
|
85
|
+
if (failedPhase?.verdict) {
|
|
86
|
+
summary.qaVerdict = String(failedPhase.verdict);
|
|
87
|
+
}
|
|
88
|
+
if (failedPhase?.summary?.gaps?.length !== undefined) {
|
|
89
|
+
summary.unmetCount = failedPhase.summary.gaps.length;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return summary;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Print post-run summary: per-issue grid, log path, reflection, tips.
|
|
96
|
+
*
|
|
97
|
+
* If a renderer is provided (default path), delegate to its `renderSummary`
|
|
98
|
+
* so the live zone is torn down cleanly first. Otherwise, fall back to the
|
|
99
|
+
* shared `renderRunSummary` helper (used by tests and TUI mode).
|
|
64
100
|
*/
|
|
65
|
-
export function displaySummary(result) {
|
|
101
|
+
export function displaySummary(result, renderer) {
|
|
66
102
|
const { results, logPath, config, mergedOptions } = result;
|
|
67
103
|
if (results.length === 0)
|
|
68
104
|
return;
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
|
|
86
|
-
const prInfo = r.prUrl ? colors.muted(` → PR #${r.prNumber}`) : "";
|
|
87
|
-
console.log(` ${status} #${r.issueNumber}: ${phases}${loopInfo}${prInfo}${duration}`);
|
|
88
|
-
}
|
|
89
|
-
console.log("");
|
|
90
|
-
if (logPath) {
|
|
91
|
-
console.log(colors.muted(` Log: ${logPath}`));
|
|
92
|
-
console.log("");
|
|
105
|
+
const issueSummaries = results.map(toIssueSummary);
|
|
106
|
+
const totalSeconds = results.reduce((sum, r) => sum + (r.durationSeconds ?? 0), 0);
|
|
107
|
+
if (renderer) {
|
|
108
|
+
renderer.renderSummary({
|
|
109
|
+
issues: issueSummaries,
|
|
110
|
+
totalDurationSeconds: totalSeconds,
|
|
111
|
+
logPath,
|
|
112
|
+
dryRun: config.dryRun,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
renderRunSummary({
|
|
117
|
+
issues: issueSummaries,
|
|
118
|
+
totalDurationSeconds: totalSeconds,
|
|
119
|
+
logPath,
|
|
120
|
+
dryRun: config.dryRun,
|
|
121
|
+
});
|
|
93
122
|
}
|
|
94
123
|
if (mergedOptions.reflect && results.length > 0) {
|
|
95
124
|
const reflection = analyzeRun({
|
|
@@ -104,6 +133,7 @@ export function displaySummary(result) {
|
|
|
104
133
|
console.log("");
|
|
105
134
|
}
|
|
106
135
|
}
|
|
136
|
+
const passed = results.filter((r) => r.success).length;
|
|
107
137
|
if (results.length > 1 && passed > 0 && !config.dryRun) {
|
|
108
138
|
console.log(colors.muted(" Tip: Verify batch integration before merging:"));
|
|
109
139
|
console.log(colors.muted(" sequant merge --check"));
|
|
@@ -113,4 +143,8 @@ export function displaySummary(result) {
|
|
|
113
143
|
console.log(colors.warning(" ℹ️ This was a dry run. Use without --dry-run to execute."));
|
|
114
144
|
console.log("");
|
|
115
145
|
}
|
|
146
|
+
// Reference imported `ui` so existing tests that depend on side-effect-only
|
|
147
|
+
// imports still pass; explicit retention to keep ui in scope for future
|
|
148
|
+
// formatting reuse.
|
|
149
|
+
void ui;
|
|
116
150
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run progress wiring — keeps run.ts thin (#503 AC-2: <200 LOC).
|
|
3
|
+
*
|
|
4
|
+
* Builds the appropriate `onProgress` callback for the run mode:
|
|
5
|
+
* - tui : no callback (the ink dashboard owns its own state)
|
|
6
|
+
* - quiet : LivenessHeartbeat-driven (TTY liveness + stall warning)
|
|
7
|
+
* - default: RunRenderer-driven (live grid + events log, #618)
|
|
8
|
+
*/
|
|
9
|
+
import type { RunRenderer } from "../lib/cli-ui/run-renderer-types.js";
|
|
10
|
+
import { LivenessHeartbeat } from "../lib/workflow/heartbeat.js";
|
|
11
|
+
import type { ProgressCallback, PhasePlanCallback } from "../lib/workflow/types.js";
|
|
12
|
+
export interface ProgressWiring {
|
|
13
|
+
renderer: RunRenderer | null;
|
|
14
|
+
heartbeat: LivenessHeartbeat | null;
|
|
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;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Construct the renderer + heartbeat + onProgress callback for a run.
|
|
22
|
+
*
|
|
23
|
+
* `tuiEnabled` and `quiet` are mutually exclusive with the renderer path.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildProgressWiring(args: {
|
|
26
|
+
tuiEnabled: boolean;
|
|
27
|
+
quiet: boolean;
|
|
28
|
+
issueNumbers: number[];
|
|
29
|
+
phaseTimeoutSeconds: number;
|
|
30
|
+
/** AC-23: when auto-detect mode is on, the renderer shows `Phase: detecting…`
|
|
31
|
+
* while spec runs (before the resolved plan is known). */
|
|
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[];
|
|
40
|
+
/** #624 Item 3 / D2: total allowed quality-loop iterations (from settings). */
|
|
41
|
+
maxLoopIterations?: number;
|
|
42
|
+
}): ProgressWiring;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run progress wiring — keeps run.ts thin (#503 AC-2: <200 LOC).
|
|
3
|
+
*
|
|
4
|
+
* Builds the appropriate `onProgress` callback for the run mode:
|
|
5
|
+
* - tui : no callback (the ink dashboard owns its own state)
|
|
6
|
+
* - quiet : LivenessHeartbeat-driven (TTY liveness + stall warning)
|
|
7
|
+
* - default: RunRenderer-driven (live grid + events log, #618)
|
|
8
|
+
*/
|
|
9
|
+
import { createRunRenderer } from "../lib/cli-ui/run-renderer.js";
|
|
10
|
+
import { LivenessHeartbeat } from "../lib/workflow/heartbeat.js";
|
|
11
|
+
/**
|
|
12
|
+
* Construct the renderer + heartbeat + onProgress callback for a run.
|
|
13
|
+
*
|
|
14
|
+
* `tuiEnabled` and `quiet` are mutually exclusive with the renderer path.
|
|
15
|
+
*/
|
|
16
|
+
export function buildProgressWiring(args) {
|
|
17
|
+
const { tuiEnabled, quiet, issueNumbers, phaseTimeoutSeconds, autoDetectPhases, basePhases, maxLoopIterations, } = args;
|
|
18
|
+
const heartbeat = quiet && !tuiEnabled
|
|
19
|
+
? new LivenessHeartbeat({ phaseTimeoutSeconds })
|
|
20
|
+
: null;
|
|
21
|
+
// RunRenderer (#618) — single owner of stdout, replaces legacy
|
|
22
|
+
// PhaseSpinner (#244) + parallel-mode `▸/✔` lines (#458).
|
|
23
|
+
// AC-26: derive a stall threshold from the configured phase timeout. Half
|
|
24
|
+
// the timeout is a conservative "expected duration" proxy — phases that
|
|
25
|
+
// routinely take longer would have failed timeout already.
|
|
26
|
+
// #624 Item 1: pass terminal rows so the live zone can cap its height.
|
|
27
|
+
// #624 Item 3 / D2: thread the configured maxLoopIterations through so
|
|
28
|
+
// all three retry-suffix sites display the correct denominator.
|
|
29
|
+
const renderer = !tuiEnabled && !quiet
|
|
30
|
+
? createRunRenderer({
|
|
31
|
+
stallThresholdMs: phaseTimeoutSeconds > 0
|
|
32
|
+
? (phaseTimeoutSeconds * 1000) / 2
|
|
33
|
+
: undefined,
|
|
34
|
+
rows: process.stdout.rows,
|
|
35
|
+
maxLoopIterations,
|
|
36
|
+
})
|
|
37
|
+
: null;
|
|
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;
|
|
47
|
+
for (const issueNumber of issueNumbers) {
|
|
48
|
+
renderer.registerIssue({
|
|
49
|
+
issueNumber,
|
|
50
|
+
autoDetect: autoDetectPhases,
|
|
51
|
+
plannedPhases: seedPlan,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
let onProgress;
|
|
56
|
+
if (renderer) {
|
|
57
|
+
onProgress = (issue, phase, event, extra) => {
|
|
58
|
+
// #543: activity events only feed the TUI's nowLine — skip the line renderer.
|
|
59
|
+
if (event === "activity")
|
|
60
|
+
return;
|
|
61
|
+
// #624 Item 3: pass the outer-loop iteration through so the renderer can
|
|
62
|
+
// render `(attempt N/M)` / `loop N/M`.
|
|
63
|
+
renderer.onEvent({
|
|
64
|
+
issue,
|
|
65
|
+
phase,
|
|
66
|
+
event,
|
|
67
|
+
durationSeconds: extra?.durationSeconds,
|
|
68
|
+
error: extra?.error,
|
|
69
|
+
iteration: extra?.iteration,
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
else if (heartbeat) {
|
|
74
|
+
onProgress = (issue, phase, event) => {
|
|
75
|
+
if (event === "activity")
|
|
76
|
+
return;
|
|
77
|
+
if (event === "start")
|
|
78
|
+
heartbeat.start({
|
|
79
|
+
issueNumber: issue,
|
|
80
|
+
phase,
|
|
81
|
+
startedAt: Date.now(),
|
|
82
|
+
});
|
|
83
|
+
else
|
|
84
|
+
heartbeat.stop({ issueNumber: issue, phase });
|
|
85
|
+
};
|
|
86
|
+
}
|
|
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 };
|
|
93
|
+
}
|
package/dist/src/commands/run.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/** sequant run — Thin CLI adapter that delegates to RunOrchestrator. */
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { getManifest } from "../lib/manifest.js";
|
|
4
|
-
import { formatElapsedTime } from "../lib/phase-spinner.js";
|
|
5
4
|
import { getSettings } from "../lib/settings.js";
|
|
6
5
|
import { checkVersionCached, getVersionWarning } from "../lib/version-check.js";
|
|
7
|
-
import { ui
|
|
6
|
+
import { ui } from "../lib/cli-ui.js";
|
|
8
7
|
import { parseBatches } from "../lib/workflow/batch-executor.js";
|
|
9
8
|
import { RunOrchestrator } from "../lib/workflow/run-orchestrator.js";
|
|
10
9
|
import { displayConfig, displaySummary } from "./run-display.js";
|
|
10
|
+
import { buildProgressWiring } from "./run-progress.js";
|
|
11
11
|
// Re-export public API for backwards compatibility
|
|
12
12
|
export * from "./run-compat.js";
|
|
13
13
|
/** Parse CLI args → validate → delegate to RunOrchestrator.run() → display summary. */
|
|
@@ -31,6 +31,15 @@ export async function runCommand(issues, options) {
|
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
const settings = await getSettings();
|
|
34
|
+
// #605: --stacked implies --chain; reject explicit --no-chain combo before
|
|
35
|
+
// we evaluate any --chain-dependent constraint below.
|
|
36
|
+
if (options.stacked && options.chain === false) {
|
|
37
|
+
console.log(chalk.red("❌ --stacked cannot be combined with --no-chain"));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (options.stacked) {
|
|
41
|
+
options.chain = true;
|
|
42
|
+
}
|
|
34
43
|
// Validate constraints
|
|
35
44
|
if (options.chain && options.batch?.length) {
|
|
36
45
|
console.log(chalk.red("❌ --chain cannot be used with --batch"));
|
|
@@ -60,22 +69,85 @@ export async function runCommand(issues, options) {
|
|
|
60
69
|
};
|
|
61
70
|
const resolved = RunOrchestrator.resolveConfig(init, issues, batches);
|
|
62
71
|
displayConfig(resolved);
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
const tuiEnabled = Boolean(options.experimentalTui) && Boolean(process.stdout.isTTY);
|
|
73
|
+
// RunRenderer (#618) + LivenessHeartbeat (#574) wiring lives in
|
|
74
|
+
// run-progress.ts to keep this adapter under the 200-LOC cap (#503 AC-2).
|
|
75
|
+
const { renderer, heartbeat, onProgress, onPhasePlan } = buildProgressWiring({
|
|
76
|
+
tuiEnabled,
|
|
77
|
+
quiet: Boolean(options.quiet),
|
|
78
|
+
issueNumbers: resolved.issueNumbers,
|
|
79
|
+
phaseTimeoutSeconds: settings.run.timeout,
|
|
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,
|
|
84
|
+
// #624 Item 3 / D2: route the resolved maxIterations into the renderer so
|
|
85
|
+
// `(attempt N/M)` and `loop N/M` reflect actual configured limits.
|
|
86
|
+
maxLoopIterations: resolved.config.maxIterations,
|
|
87
|
+
});
|
|
88
|
+
if (tuiEnabled) {
|
|
89
|
+
const { renderTui } = await import("../ui/tui/index.js");
|
|
90
|
+
let tuiHandle = null;
|
|
91
|
+
// Unmount the TUI before ShutdownManager writes its shutdown banner so
|
|
92
|
+
// the two don't race on stdout / leave the terminal in alt-screen buffer.
|
|
93
|
+
// `process.once` fires listeners in registration order, so this runs
|
|
94
|
+
// before ShutdownManager's SIGINT handler registered inside run().
|
|
95
|
+
const sigintHandler = () => {
|
|
96
|
+
tuiHandle?.unmount();
|
|
97
|
+
};
|
|
98
|
+
process.once("SIGINT", sigintHandler);
|
|
99
|
+
try {
|
|
100
|
+
const result = await RunOrchestrator.run({
|
|
101
|
+
...init,
|
|
102
|
+
onProgress,
|
|
103
|
+
onPhasePlan,
|
|
104
|
+
phasePauseHandle: renderer ?? undefined,
|
|
105
|
+
onOrchestratorReady: (orch) => {
|
|
106
|
+
tuiHandle = renderTui(orch);
|
|
107
|
+
},
|
|
108
|
+
}, issues, batches);
|
|
109
|
+
if (tuiHandle) {
|
|
110
|
+
await tuiHandle.done;
|
|
72
111
|
}
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
displaySummary(result);
|
|
113
|
+
if (result.exitCode !== 0)
|
|
114
|
+
process.exit(result.exitCode);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
process.off("SIGINT", sigintHandler);
|
|
75
119
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
120
|
+
}
|
|
121
|
+
// SIGINT handler: clear the live zone before ShutdownManager writes its
|
|
122
|
+
// cleanup banner so the two don't collide. See AC-29.
|
|
123
|
+
const sigintHandler = () => {
|
|
124
|
+
renderer?.dispose();
|
|
125
|
+
};
|
|
126
|
+
if (renderer)
|
|
127
|
+
process.once("SIGINT", sigintHandler);
|
|
128
|
+
try {
|
|
129
|
+
const result = await RunOrchestrator.run({
|
|
130
|
+
...init,
|
|
131
|
+
onProgress,
|
|
132
|
+
onPhasePlan,
|
|
133
|
+
phasePauseHandle: renderer ?? undefined,
|
|
134
|
+
}, issues, batches);
|
|
135
|
+
// Record PR info in renderer state before summary so done rows show PR #s.
|
|
136
|
+
if (renderer) {
|
|
137
|
+
for (const r of result.results) {
|
|
138
|
+
if (r.prNumber && r.prUrl) {
|
|
139
|
+
renderer.setPullRequest(r.issueNumber, r.prNumber, r.prUrl);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
displaySummary(result, renderer);
|
|
144
|
+
renderer?.dispose();
|
|
145
|
+
if (result.exitCode !== 0)
|
|
146
|
+
process.exit(result.exitCode);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
heartbeat?.dispose();
|
|
150
|
+
if (renderer)
|
|
151
|
+
process.off("SIGINT", sigintHandler);
|
|
152
|
+
}
|
|
81
153
|
}
|
|
@@ -55,6 +55,37 @@ function parseLogFile(filePath) {
|
|
|
55
55
|
return null;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Filter runs by label and/or since date.
|
|
60
|
+
*
|
|
61
|
+
* Applied after parseLogFile, before calculateStats / calculateDetailedAnalytics.
|
|
62
|
+
* Metrics file is bypassed when either filter is set — its schema carries only
|
|
63
|
+
* issue numbers (not labels), so per-label filtering is impossible there.
|
|
64
|
+
*
|
|
65
|
+
* Inclusion rules (AND when both filters set):
|
|
66
|
+
* --label <name> — keep a run iff some issue in log.issues carries the label
|
|
67
|
+
* (mirrors the per-issue label scan in calculateDetailedAnalytics)
|
|
68
|
+
* --since YYYY-MM-DD — keep a run iff log.startTime >= <since>T00:00:00Z
|
|
69
|
+
*
|
|
70
|
+
* Caller is responsible for validating `since` format up-front (see statsCommand).
|
|
71
|
+
*/
|
|
72
|
+
function filterLogs(logs, filters) {
|
|
73
|
+
const { label, since } = filters;
|
|
74
|
+
const sinceMs = since !== undefined ? Date.parse(`${since}T00:00:00Z`) : undefined;
|
|
75
|
+
return logs.filter((log) => {
|
|
76
|
+
if (label !== undefined) {
|
|
77
|
+
const hasLabel = log.issues.some((issue) => issue.labels.includes(label));
|
|
78
|
+
if (!hasLabel)
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (sinceMs !== undefined) {
|
|
82
|
+
const startMs = Date.parse(log.startTime);
|
|
83
|
+
if (Number.isNaN(startMs) || startMs < sinceMs)
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
58
89
|
/**
|
|
59
90
|
* Calculate aggregate statistics from logs
|
|
60
91
|
*/
|
|
@@ -566,12 +597,51 @@ function displayDetailedAnalytics(detailed) {
|
|
|
566
597
|
}
|
|
567
598
|
}
|
|
568
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* Emit the "no matching runs" empty branch in the active output format.
|
|
602
|
+
* Reuses the shape of the existing empty-data branches (AC-4).
|
|
603
|
+
*/
|
|
604
|
+
function emitNoMatchingRuns(options) {
|
|
605
|
+
if (options.json) {
|
|
606
|
+
console.log(JSON.stringify({ error: "No matching runs", runs: [] }));
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (options.csv) {
|
|
610
|
+
console.log("runId,startTime,duration,issues,passed,failed,phases");
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
console.log(ui.headerBox("SEQUANT ANALYTICS"));
|
|
614
|
+
console.log(colors.muted("\n Local data only - no telemetry\n"));
|
|
615
|
+
console.log(colors.warning(" No matching runs."));
|
|
616
|
+
console.log("");
|
|
617
|
+
}
|
|
569
618
|
/**
|
|
570
619
|
* Main stats command
|
|
571
620
|
*/
|
|
572
621
|
export async function statsCommand(options) {
|
|
573
|
-
//
|
|
574
|
-
|
|
622
|
+
// Validate --since up-front so an invalid date errors before any output.
|
|
623
|
+
// Two-stage check: a strict YYYY-MM-DD regex (rejects 2026/01/01, 2026-1-1,
|
|
624
|
+
// Jan 1 2026 — all of which Date.parse would otherwise accept with engine-
|
|
625
|
+
// dependent UTC interpretation), then Date.parse for semantic validity
|
|
626
|
+
// (rejects 2026-13-45 etc.).
|
|
627
|
+
if (options.since !== undefined) {
|
|
628
|
+
const SINCE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
629
|
+
const sinceMs = SINCE_RE.test(options.since)
|
|
630
|
+
? Date.parse(`${options.since}T00:00:00Z`)
|
|
631
|
+
: NaN;
|
|
632
|
+
if (Number.isNaN(sinceMs)) {
|
|
633
|
+
console.error(colors.error(`Invalid --since date: ${options.since}. Expected YYYY-MM-DD.`));
|
|
634
|
+
process.exitCode = 1;
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// --label / --since force log-mode: the metrics file has no per-issue labels
|
|
639
|
+
// (metrics-schema.ts MetricRun.issues is number[]), so filtering is impossible
|
|
640
|
+
// against metrics. Bypass the metrics-first path entirely when filters are set.
|
|
641
|
+
const useFilters = options.label !== undefined || options.since !== undefined;
|
|
642
|
+
const filters = { label: options.label, since: options.since };
|
|
643
|
+
// Try to load local metrics first (skipped when filters force log-mode)
|
|
644
|
+
const metrics = useFilters ? null : loadMetrics();
|
|
575
645
|
// If JSON output requested
|
|
576
646
|
if (options.json) {
|
|
577
647
|
if (metrics && metrics.runs.length > 0) {
|
|
@@ -611,16 +681,21 @@ export async function statsCommand(options) {
|
|
|
611
681
|
console.log(JSON.stringify({ error: "No data found", runs: [] }));
|
|
612
682
|
return;
|
|
613
683
|
}
|
|
614
|
-
const
|
|
684
|
+
const allLogs = logFiles
|
|
615
685
|
.map((filename) => {
|
|
616
686
|
const filePath = path.join(logDir, filename);
|
|
617
687
|
return parseLogFile(filePath);
|
|
618
688
|
})
|
|
619
689
|
.filter((log) => log !== null);
|
|
620
|
-
if (
|
|
690
|
+
if (allLogs.length === 0) {
|
|
621
691
|
console.log(JSON.stringify({ error: "No valid logs found", runs: [] }));
|
|
622
692
|
return;
|
|
623
693
|
}
|
|
694
|
+
const logs = useFilters ? filterLogs(allLogs, filters) : allLogs;
|
|
695
|
+
if (useFilters && logs.length === 0) {
|
|
696
|
+
emitNoMatchingRuns(options);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
624
699
|
const stats = calculateStats(logs);
|
|
625
700
|
const output = {
|
|
626
701
|
source: "logs",
|
|
@@ -646,12 +721,17 @@ export async function statsCommand(options) {
|
|
|
646
721
|
console.log("runId,startTime,duration,issues,passed,failed,phases");
|
|
647
722
|
return;
|
|
648
723
|
}
|
|
649
|
-
const
|
|
724
|
+
const allLogs = logFiles
|
|
650
725
|
.map((filename) => {
|
|
651
726
|
const filePath = path.join(logDir, filename);
|
|
652
727
|
return parseLogFile(filePath);
|
|
653
728
|
})
|
|
654
729
|
.filter((log) => log !== null);
|
|
730
|
+
const logs = useFilters ? filterLogs(allLogs, filters) : allLogs;
|
|
731
|
+
if (useFilters && logs.length === 0) {
|
|
732
|
+
emitNoMatchingRuns(options);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
655
735
|
console.log(generateCsv(logs));
|
|
656
736
|
return;
|
|
657
737
|
}
|
|
@@ -672,16 +752,21 @@ export async function statsCommand(options) {
|
|
|
672
752
|
console.log("");
|
|
673
753
|
return;
|
|
674
754
|
}
|
|
675
|
-
const
|
|
755
|
+
const allLogs = logFiles
|
|
676
756
|
.map((filename) => {
|
|
677
757
|
const filePath = path.join(logDir, filename);
|
|
678
758
|
return parseLogFile(filePath);
|
|
679
759
|
})
|
|
680
760
|
.filter((log) => log !== null);
|
|
681
|
-
if (
|
|
761
|
+
if (allLogs.length === 0) {
|
|
682
762
|
console.log(colors.warning("\n No valid log files found.\n"));
|
|
683
763
|
return;
|
|
684
764
|
}
|
|
765
|
+
const logs = useFilters ? filterLogs(allLogs, filters) : allLogs;
|
|
766
|
+
if (useFilters && logs.length === 0) {
|
|
767
|
+
emitNoMatchingRuns(options);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
685
770
|
const stats = calculateStats(logs);
|
|
686
771
|
displayStats(stats, logDir);
|
|
687
772
|
}
|
|
@@ -689,12 +774,13 @@ export async function statsCommand(options) {
|
|
|
689
774
|
if (options.detailed) {
|
|
690
775
|
const logDir = resolveLogPath(options.path);
|
|
691
776
|
const logFiles = listLogFiles(logDir);
|
|
692
|
-
const
|
|
777
|
+
const allLogs = logFiles
|
|
693
778
|
.map((filename) => {
|
|
694
779
|
const filePath = path.join(logDir, filename);
|
|
695
780
|
return parseLogFile(filePath);
|
|
696
781
|
})
|
|
697
782
|
.filter((log) => log !== null);
|
|
783
|
+
const logs = useFilters ? filterLogs(allLogs, filters) : allLogs;
|
|
698
784
|
if (logs.length > 0) {
|
|
699
785
|
const detailed = calculateDetailedAnalytics(logs);
|
|
700
786
|
displayDetailedAnalytics(detailed);
|
|
@@ -11,6 +11,7 @@ import { rebuildStateFromLogs, cleanupStaleEntries, } from "../lib/workflow/stat
|
|
|
11
11
|
import { reconcileState, getNextActionHint, formatRelativeTime, } from "../lib/workflow/reconcile.js";
|
|
12
12
|
import { getSettingsWithWarnings } from "../lib/settings.js";
|
|
13
13
|
import { getSkillVersions } from "../lib/skill-version.js";
|
|
14
|
+
import { LockManager, formatLockedMessage } from "../lib/locks/index.js";
|
|
14
15
|
/**
|
|
15
16
|
* Run reconciliation and display warnings.
|
|
16
17
|
* Returns the reconcile result for use in display.
|
|
@@ -182,11 +183,16 @@ function displayIssueSummary(issues) {
|
|
|
182
183
|
const hintDisplay = hint
|
|
183
184
|
? chalk.gray(`→ ${hint.length > 30 ? hint.substring(0, 27) + "..." : hint}`)
|
|
184
185
|
: "";
|
|
186
|
+
// Relay column (#383). Shows ✓ when active, "-" otherwise.
|
|
187
|
+
const relayDisplay = issue.relay?.enabled
|
|
188
|
+
? chalk.green("on")
|
|
189
|
+
: chalk.gray("-");
|
|
185
190
|
rows.push([
|
|
186
191
|
`#${issue.number}`,
|
|
187
192
|
title,
|
|
188
193
|
colorStatus(issue.status, issue.resolvedAt),
|
|
189
194
|
issue.currentPhase || "-",
|
|
195
|
+
relayDisplay,
|
|
190
196
|
hintDisplay,
|
|
191
197
|
]);
|
|
192
198
|
}
|
|
@@ -199,6 +205,7 @@ function displayIssueSummary(issues) {
|
|
|
199
205
|
{ header: "Title", width: 32 },
|
|
200
206
|
{ header: "Status", width: 20 },
|
|
201
207
|
{ header: "Phase", width: 10 },
|
|
208
|
+
{ header: "Relay", width: 5 },
|
|
202
209
|
{ header: "Next", width: 34 },
|
|
203
210
|
],
|
|
204
211
|
}));
|
|
@@ -356,6 +363,11 @@ async function displayIssueState(options) {
|
|
|
356
363
|
else if (issueState) {
|
|
357
364
|
console.log(chalk.bold(`\nIssue #${options.issue} State\n`));
|
|
358
365
|
console.log(formatIssueState(issueState));
|
|
366
|
+
const lockManager = new LockManager();
|
|
367
|
+
const holder = lockManager.check(options.issue);
|
|
368
|
+
if (holder) {
|
|
369
|
+
console.log(chalk.yellow(` ! ${formatLockedMessage(options.issue, holder)}`));
|
|
370
|
+
}
|
|
359
371
|
const hint = getNextActionHint(issueState);
|
|
360
372
|
if (hint) {
|
|
361
373
|
console.log(chalk.cyan(`\n Next: ${hint}`));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sequant watch <issue>` — tail the relay outbox for replies from a running
|
|
3
|
+
* headless session (#383). Uses `fs.watch()` when available, falls back to
|
|
4
|
+
* polling on platforms where `fs.watch` is unreliable (NFS, some WSL setups).
|
|
5
|
+
*/
|
|
6
|
+
export interface WatchCommandOptions {
|
|
7
|
+
json?: boolean;
|
|
8
|
+
/** Poll interval (ms); used when fs.watch is unavailable. Default: 200. */
|
|
9
|
+
pollIntervalMs?: number;
|
|
10
|
+
/** Abort signal for clean shutdown (tests). */
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
/** Override cwd for resolving the pid file + archive root (test seam). */
|
|
13
|
+
cwd?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function watchCommand(argsAndOptions: {
|
|
16
|
+
args: string[];
|
|
17
|
+
options: WatchCommandOptions;
|
|
18
|
+
}): Promise<void>;
|