sequant 2.1.2 → 2.3.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 (146) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +73 -0
  4. package/dist/bin/cli.js +95 -9
  5. package/dist/src/commands/doctor.d.ts +25 -0
  6. package/dist/src/commands/doctor.js +36 -1
  7. package/dist/src/commands/init.d.ts +1 -0
  8. package/dist/src/commands/init.js +118 -0
  9. package/dist/src/commands/locks.d.ts +67 -0
  10. package/dist/src/commands/locks.js +290 -0
  11. package/dist/src/commands/merge.js +11 -0
  12. package/dist/src/commands/prompt.d.ts +39 -0
  13. package/dist/src/commands/prompt.js +179 -0
  14. package/dist/src/commands/run-display.d.ts +26 -0
  15. package/dist/src/commands/run-display.js +150 -0
  16. package/dist/src/commands/run-progress.d.ts +32 -0
  17. package/dist/src/commands/run-progress.js +76 -0
  18. package/dist/src/commands/run.js +83 -73
  19. package/dist/src/commands/stats.d.ts +2 -0
  20. package/dist/src/commands/stats.js +94 -8
  21. package/dist/src/commands/status.js +27 -1
  22. package/dist/src/commands/watch.d.ts +16 -0
  23. package/dist/src/commands/watch.js +147 -0
  24. package/dist/src/lib/ac-linter.d.ts +1 -1
  25. package/dist/src/lib/ac-linter.js +81 -0
  26. package/dist/src/lib/assess-collision-detect.d.ts +91 -0
  27. package/dist/src/lib/assess-collision-detect.js +217 -0
  28. package/dist/src/lib/assess-comment-parser.d.ts +59 -1
  29. package/dist/src/lib/assess-comment-parser.js +124 -2
  30. package/dist/src/lib/cli-ui/format.d.ts +19 -0
  31. package/dist/src/lib/cli-ui/format.js +34 -0
  32. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +181 -0
  33. package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
  34. package/dist/src/lib/cli-ui/run-renderer.d.ts +239 -0
  35. package/dist/src/lib/cli-ui/run-renderer.js +1173 -0
  36. package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
  37. package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
  38. package/dist/src/lib/locks/index.d.ts +7 -0
  39. package/dist/src/lib/locks/index.js +5 -0
  40. package/dist/src/lib/locks/lock-manager.d.ts +168 -0
  41. package/dist/src/lib/locks/lock-manager.js +433 -0
  42. package/dist/src/lib/locks/types.d.ts +59 -0
  43. package/dist/src/lib/locks/types.js +31 -0
  44. package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
  45. package/dist/src/lib/qa/markdown-only-ci.js +74 -0
  46. package/dist/src/lib/relay/activation.d.ts +60 -0
  47. package/dist/src/lib/relay/activation.js +122 -0
  48. package/dist/src/lib/relay/archive.d.ts +34 -0
  49. package/dist/src/lib/relay/archive.js +106 -0
  50. package/dist/src/lib/relay/frame.d.ts +20 -0
  51. package/dist/src/lib/relay/frame.js +76 -0
  52. package/dist/src/lib/relay/index.d.ts +13 -0
  53. package/dist/src/lib/relay/index.js +13 -0
  54. package/dist/src/lib/relay/paths.d.ts +43 -0
  55. package/dist/src/lib/relay/paths.js +59 -0
  56. package/dist/src/lib/relay/pid.d.ts +34 -0
  57. package/dist/src/lib/relay/pid.js +72 -0
  58. package/dist/src/lib/relay/reader.d.ts +35 -0
  59. package/dist/src/lib/relay/reader.js +115 -0
  60. package/dist/src/lib/relay/types.d.ts +68 -0
  61. package/dist/src/lib/relay/types.js +76 -0
  62. package/dist/src/lib/relay/writer.d.ts +48 -0
  63. package/dist/src/lib/relay/writer.js +113 -0
  64. package/dist/src/lib/settings.d.ts +31 -1
  65. package/dist/src/lib/settings.js +18 -3
  66. package/dist/src/lib/skill-version.d.ts +19 -0
  67. package/dist/src/lib/skill-version.js +68 -0
  68. package/dist/src/lib/templates.d.ts +1 -0
  69. package/dist/src/lib/templates.js +1 -1
  70. package/dist/src/lib/version-check.d.ts +60 -5
  71. package/dist/src/lib/version-check.js +97 -9
  72. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  73. package/dist/src/lib/workflow/batch-executor.js +249 -176
  74. package/dist/src/lib/workflow/config-resolver.js +4 -0
  75. package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
  76. package/dist/src/lib/workflow/heartbeat.js +194 -0
  77. package/dist/src/lib/workflow/phase-executor.d.ts +88 -3
  78. package/dist/src/lib/workflow/phase-executor.js +276 -52
  79. package/dist/src/lib/workflow/phase-mapper.d.ts +3 -2
  80. package/dist/src/lib/workflow/phase-mapper.js +17 -20
  81. package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
  82. package/dist/src/lib/workflow/platforms/github.js +20 -3
  83. package/dist/src/lib/workflow/pr-status.d.ts +18 -2
  84. package/dist/src/lib/workflow/pr-status.js +41 -9
  85. package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
  86. package/dist/src/lib/workflow/qa-stagnation.js +179 -0
  87. package/dist/src/lib/workflow/run-orchestrator.d.ts +76 -0
  88. package/dist/src/lib/workflow/run-orchestrator.js +382 -29
  89. package/dist/src/lib/workflow/run-reflect.js +1 -1
  90. package/dist/src/lib/workflow/run-state.d.ts +71 -0
  91. package/dist/src/lib/workflow/run-state.js +14 -0
  92. package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
  93. package/dist/src/lib/workflow/state-cleanup.js +17 -5
  94. package/dist/src/lib/workflow/state-manager.d.ts +12 -1
  95. package/dist/src/lib/workflow/state-manager.js +37 -0
  96. package/dist/src/lib/workflow/state-schema.d.ts +62 -0
  97. package/dist/src/lib/workflow/state-schema.js +35 -1
  98. package/dist/src/lib/workflow/types.d.ts +74 -1
  99. package/dist/src/lib/workflow/worktree-manager.d.ts +12 -4
  100. package/dist/src/lib/workflow/worktree-manager.js +76 -17
  101. package/dist/src/mcp/tools/run.d.ts +44 -0
  102. package/dist/src/mcp/tools/run.js +104 -13
  103. package/dist/src/ui/tui/App.d.ts +14 -0
  104. package/dist/src/ui/tui/App.js +41 -0
  105. package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
  106. package/dist/src/ui/tui/ElapsedTimer.js +31 -0
  107. package/dist/src/ui/tui/Header.d.ts +6 -0
  108. package/dist/src/ui/tui/Header.js +15 -0
  109. package/dist/src/ui/tui/IssueBox.d.ts +16 -0
  110. package/dist/src/ui/tui/IssueBox.js +68 -0
  111. package/dist/src/ui/tui/Spinner.d.ts +9 -0
  112. package/dist/src/ui/tui/Spinner.js +18 -0
  113. package/dist/src/ui/tui/index.d.ts +15 -0
  114. package/dist/src/ui/tui/index.js +29 -0
  115. package/dist/src/ui/tui/theme.d.ts +29 -0
  116. package/dist/src/ui/tui/theme.js +52 -0
  117. package/dist/src/ui/tui/truncate.d.ts +11 -0
  118. package/dist/src/ui/tui/truncate.js +31 -0
  119. package/package.json +10 -3
  120. package/templates/agents/sequant-explorer.md +1 -0
  121. package/templates/agents/sequant-qa-checker.md +2 -1
  122. package/templates/agents/sequant-testgen.md +1 -0
  123. package/templates/hooks/post-tool.sh +11 -0
  124. package/templates/hooks/pre-tool.sh +18 -9
  125. package/templates/hooks/relay-check.sh +107 -0
  126. package/templates/relay/frame.txt +11 -0
  127. package/templates/scripts/cleanup-worktree.sh +25 -3
  128. package/templates/scripts/new-feature.sh +6 -0
  129. package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
  130. package/templates/skills/_shared/references/subagent-types.md +21 -8
  131. package/templates/skills/assess/SKILL.md +261 -94
  132. package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
  133. package/templates/skills/docs/SKILL.md +141 -22
  134. package/templates/skills/exec/SKILL.md +10 -49
  135. package/templates/skills/fullsolve/SKILL.md +80 -32
  136. package/templates/skills/loop/SKILL.md +28 -0
  137. package/templates/skills/merger/SKILL.md +621 -0
  138. package/templates/skills/qa/SKILL.md +746 -8
  139. package/templates/skills/qa/scripts/quality-checks.sh +47 -1
  140. package/templates/skills/setup/SKILL.md +6 -0
  141. package/templates/skills/spec/SKILL.md +217 -964
  142. package/templates/skills/spec/references/parallel-groups.md +7 -0
  143. package/templates/skills/spec/references/quality-checklist.md +75 -0
  144. package/templates/skills/spec/references/recommended-workflow.md +4 -2
  145. package/templates/skills/test/SKILL.md +0 -27
  146. package/templates/skills/testgen/SKILL.md +24 -44
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Display helpers for `sequant run` — pre-run config block + post-run summary.
3
+ *
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.
9
+ */
10
+ import chalk from "chalk";
11
+ import { ui, colors } from "../lib/cli-ui.js";
12
+ import { renderRunSummary } from "../lib/cli-ui/run-renderer.js";
13
+ import { analyzeRun, formatReflection } from "../lib/workflow/run-reflect.js";
14
+ /**
15
+ * Print pre-run config block.
16
+ *
17
+ * Columnar alignment via 15-char label padding. Conditional rows only
18
+ * appear when non-default, matching the pre-#503 format.
19
+ */
20
+ export function displayConfig(r) {
21
+ const pad = (label) => label.padEnd(15);
22
+ const row = (label, value) => console.log(chalk.gray(` ${pad(label)}${value}`));
23
+ row("Stack", r.stack);
24
+ if (r.autoDetectPhases) {
25
+ row("Phases", "auto-detect from labels");
26
+ }
27
+ else {
28
+ row("Phases", r.config.phases.join(" → "));
29
+ }
30
+ row("Mode", r.config.sequential
31
+ ? "sequential (stop-on-failure)"
32
+ : `parallel (concurrency: ${r.config.concurrency})`);
33
+ if (r.config.qualityLoop) {
34
+ row("Quality loop", `enabled (max ${r.config.maxIterations} iterations)`);
35
+ }
36
+ if (r.mergedOptions.testgen)
37
+ row("Testgen", "enabled");
38
+ if (r.config.noSmartTests)
39
+ row("Smart tests", "disabled");
40
+ if (r.config.dryRun) {
41
+ console.log(chalk.yellow(` ! DRY RUN - no actual execution`));
42
+ }
43
+ if (r.logEnabled)
44
+ row("Logging", "JSON");
45
+ if (r.stateEnabled)
46
+ row("State", "enabled");
47
+ if (r.mergedOptions.force) {
48
+ console.log(chalk.yellow(` ${pad("Force")}enabled (bypass state guard)`));
49
+ }
50
+ if (r.issueNumbers.length > 0) {
51
+ row("Issues", r.issueNumbers.map((n) => `#${n}`).join(", "));
52
+ }
53
+ if (r.worktreeIsolationEnabled) {
54
+ console.log(chalk.gray(` Worktree isolation: enabled`));
55
+ }
56
+ if (r.baseBranch) {
57
+ console.log(chalk.gray(` Base branch: ${r.baseBranch}`));
58
+ }
59
+ if (r.mergedOptions.chain) {
60
+ console.log(chalk.gray(` Chain mode: enabled (each issue branches from previous)`));
61
+ }
62
+ if (r.mergedOptions.qaGate) {
63
+ console.log(chalk.gray(` QA gate: enabled (chain waits for QA pass)`));
64
+ }
65
+ }
66
+ /**
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).
100
+ */
101
+ export function displaySummary(result, renderer) {
102
+ const { results, logPath, config, mergedOptions } = result;
103
+ if (results.length === 0)
104
+ return;
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
+ });
122
+ }
123
+ if (mergedOptions.reflect && results.length > 0) {
124
+ const reflection = analyzeRun({
125
+ results,
126
+ issueInfoMap: result.issueInfoMap,
127
+ runLog: result.logWriter?.getRunLog() ?? null,
128
+ config: { phases: config.phases, qualityLoop: config.qualityLoop },
129
+ });
130
+ const reflectionOutput = formatReflection(reflection);
131
+ if (reflectionOutput) {
132
+ console.log(reflectionOutput);
133
+ console.log("");
134
+ }
135
+ }
136
+ const passed = results.filter((r) => r.success).length;
137
+ if (results.length > 1 && passed > 0 && !config.dryRun) {
138
+ console.log(colors.muted(" Tip: Verify batch integration before merging:"));
139
+ console.log(colors.muted(" sequant merge --check"));
140
+ console.log("");
141
+ }
142
+ if (config.dryRun) {
143
+ console.log(colors.warning(" ℹ️ This was a dry run. Use without --dry-run to execute."));
144
+ console.log("");
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;
150
+ }
@@ -0,0 +1,32 @@
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 } from "../lib/workflow/types.js";
12
+ export interface ProgressWiring {
13
+ renderer: RunRenderer | null;
14
+ heartbeat: LivenessHeartbeat | null;
15
+ onProgress: ProgressCallback | undefined;
16
+ }
17
+ /**
18
+ * Construct the renderer + heartbeat + onProgress callback for a run.
19
+ *
20
+ * `tuiEnabled` and `quiet` are mutually exclusive with the renderer path.
21
+ */
22
+ export declare function buildProgressWiring(args: {
23
+ tuiEnabled: boolean;
24
+ quiet: boolean;
25
+ issueNumbers: number[];
26
+ phaseTimeoutSeconds: number;
27
+ /** AC-23: when auto-detect mode is on, the renderer shows `Phase: detecting…`
28
+ * while spec runs (before the resolved plan is known). */
29
+ autoDetectPhases?: boolean;
30
+ /** #624 Item 3 / D2: total allowed quality-loop iterations (from settings). */
31
+ maxLoopIterations?: number;
32
+ }): ProgressWiring;
@@ -0,0 +1,76 @@
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, 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
+ for (const issueNumber of issueNumbers) {
40
+ renderer.registerIssue({ issueNumber, autoDetect: autoDetectPhases });
41
+ }
42
+ }
43
+ let onProgress;
44
+ if (renderer) {
45
+ onProgress = (issue, phase, event, extra) => {
46
+ // #543: activity events only feed the TUI's nowLine — skip the line renderer.
47
+ if (event === "activity")
48
+ return;
49
+ // #624 Item 3: pass the outer-loop iteration through so the renderer can
50
+ // render `(attempt N/M)` / `loop N/M`.
51
+ renderer.onEvent({
52
+ issue,
53
+ phase,
54
+ event,
55
+ durationSeconds: extra?.durationSeconds,
56
+ error: extra?.error,
57
+ iteration: extra?.iteration,
58
+ });
59
+ };
60
+ }
61
+ else if (heartbeat) {
62
+ onProgress = (issue, phase, event) => {
63
+ if (event === "activity")
64
+ return;
65
+ if (event === "start")
66
+ heartbeat.start({
67
+ issueNumber: issue,
68
+ phase,
69
+ startedAt: Date.now(),
70
+ });
71
+ else
72
+ heartbeat.stop({ issueNumber: issue, phase });
73
+ };
74
+ }
75
+ return { renderer, heartbeat, onProgress };
76
+ }
@@ -1,14 +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, colors } from "../lib/cli-ui.js";
8
- import { formatDuration } from "../lib/workflow/phase-executor.js";
6
+ import { ui } from "../lib/cli-ui.js";
9
7
  import { parseBatches } from "../lib/workflow/batch-executor.js";
10
8
  import { RunOrchestrator } from "../lib/workflow/run-orchestrator.js";
11
- import { analyzeRun, formatReflection } from "../lib/workflow/run-reflect.js";
9
+ import { displayConfig, displaySummary } from "./run-display.js";
10
+ import { buildProgressWiring } from "./run-progress.js";
12
11
  // Re-export public API for backwards compatibility
13
12
  export * from "./run-compat.js";
14
13
  /** Parse CLI args → validate → delegate to RunOrchestrator.run() → display summary. */
@@ -32,6 +31,15 @@ export async function runCommand(issues, options) {
32
31
  return;
33
32
  }
34
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
+ }
35
43
  // Validate constraints
36
44
  if (options.chain && options.batch?.length) {
37
45
  console.log(chalk.red("❌ --chain cannot be used with --batch"));
@@ -51,83 +59,85 @@ export async function runCommand(issues, options) {
51
59
  batches = parseBatches(options.batch);
52
60
  console.log(chalk.gray(` Batch mode: ${batches.map((b) => `[${b.join(", ")}]`).join(" → ")}`));
53
61
  }
54
- console.log(chalk.gray(` ${"Stack".padEnd(15)}${manifest.stack}`));
55
- const onProgress = !options.quiet
56
- ? (issue, phase, event, extra) => {
57
- if (event === "start")
58
- console.log(` ${colors.running("▸")} #${issue} ${phase}`);
59
- else if (event === "complete") {
60
- const dur = extra?.durationSeconds != null
61
- ? ` ${formatElapsedTime(extra.durationSeconds)}`
62
- : "";
63
- console.log(` ${colors.success("✔")} #${issue} ${phase}${dur}`);
64
- }
65
- else
66
- console.log(` ${colors.error("✖")} #${issue} ${phase}`);
67
- }
68
- : undefined;
69
- const result = await RunOrchestrator.run({
62
+ const init = {
70
63
  options,
71
64
  settings,
72
65
  manifest: {
73
66
  stack: manifest.stack,
74
67
  packageManager: manifest.packageManager ?? "npm",
75
68
  },
76
- onProgress,
77
- }, issues, batches);
78
- displaySummary(result);
79
- if (result.exitCode !== 0)
80
- process.exit(result.exitCode);
81
- }
82
- function displaySummary(result) {
83
- const { results, logPath, config, mergedOptions } = result;
84
- if (results.length === 0)
85
- return;
86
- const passed = results.filter((r) => r.success).length;
87
- const failed = results.filter((r) => !r.success).length;
88
- console.log("\n" + ui.divider());
89
- console.log(colors.info(" Summary"));
90
- console.log(ui.divider());
91
- console.log(`\n ${colors.success(`${passed} passed`)} ${colors.muted("·")} ${colors.error(`${failed} failed`)}`);
92
- for (const r of results) {
93
- const status = r.success
94
- ? ui.statusIcon("success")
95
- : ui.statusIcon("error");
96
- const duration = r.durationSeconds
97
- ? colors.muted(` (${formatDuration(r.durationSeconds)})`)
98
- : "";
99
- const phases = r.phaseResults
100
- .map((p) => (p.success ? colors.success(p.phase) : colors.error(p.phase)))
101
- .join(" → ");
102
- const loopInfo = r.loopTriggered ? colors.warning(" [loop]") : "";
103
- const prInfo = r.prUrl ? colors.muted(` → PR #${r.prNumber}`) : "";
104
- console.log(` ${status} #${r.issueNumber}: ${phases}${loopInfo}${prInfo}${duration}`);
105
- }
106
- console.log("");
107
- if (logPath) {
108
- console.log(colors.muted(` Log: ${logPath}`));
109
- console.log("");
110
- }
111
- if (mergedOptions.reflect && results.length > 0) {
112
- const reflection = analyzeRun({
113
- results,
114
- issueInfoMap: result.issueInfoMap,
115
- runLog: result.logWriter?.getRunLog() ?? null,
116
- config: { phases: config.phases, qualityLoop: config.qualityLoop },
117
- });
118
- const reflectionOutput = formatReflection(reflection);
119
- if (reflectionOutput) {
120
- console.log(reflectionOutput);
121
- console.log("");
69
+ };
70
+ const resolved = RunOrchestrator.resolveConfig(init, issues, batches);
71
+ displayConfig(resolved);
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 } = buildProgressWiring({
76
+ tuiEnabled,
77
+ quiet: Boolean(options.quiet),
78
+ issueNumbers: resolved.issueNumbers,
79
+ phaseTimeoutSeconds: settings.run.timeout,
80
+ autoDetectPhases: resolved.autoDetectPhases,
81
+ // #624 Item 3 / D2: route the resolved maxIterations into the renderer so
82
+ // `(attempt N/M)` and `loop N/M` reflect actual configured limits.
83
+ maxLoopIterations: resolved.config.maxIterations,
84
+ });
85
+ if (tuiEnabled) {
86
+ const { renderTui } = await import("../ui/tui/index.js");
87
+ let tuiHandle = null;
88
+ // Unmount the TUI before ShutdownManager writes its shutdown banner so
89
+ // the two don't race on stdout / leave the terminal in alt-screen buffer.
90
+ // `process.once` fires listeners in registration order, so this runs
91
+ // before ShutdownManager's SIGINT handler registered inside run().
92
+ const sigintHandler = () => {
93
+ tuiHandle?.unmount();
94
+ };
95
+ process.once("SIGINT", sigintHandler);
96
+ try {
97
+ const result = await RunOrchestrator.run({
98
+ ...init,
99
+ onProgress,
100
+ onOrchestratorReady: (orch) => {
101
+ tuiHandle = renderTui(orch);
102
+ },
103
+ }, issues, batches);
104
+ if (tuiHandle) {
105
+ await tuiHandle.done;
106
+ }
107
+ displaySummary(result);
108
+ if (result.exitCode !== 0)
109
+ process.exit(result.exitCode);
110
+ return;
111
+ }
112
+ finally {
113
+ process.off("SIGINT", sigintHandler);
122
114
  }
123
115
  }
124
- if (results.length > 1 && passed > 0 && !config.dryRun) {
125
- console.log(colors.muted(" Tip: Verify batch integration before merging:"));
126
- console.log(colors.muted(" sequant merge --check"));
127
- console.log("");
116
+ // SIGINT handler: clear the live zone before ShutdownManager writes its
117
+ // cleanup banner so the two don't collide. See AC-29.
118
+ const sigintHandler = () => {
119
+ renderer?.dispose();
120
+ };
121
+ if (renderer)
122
+ process.once("SIGINT", sigintHandler);
123
+ try {
124
+ const result = await RunOrchestrator.run({ ...init, onProgress }, issues, batches);
125
+ // Record PR info in renderer state before summary so done rows show PR #s.
126
+ if (renderer) {
127
+ for (const r of result.results) {
128
+ if (r.prNumber && r.prUrl) {
129
+ renderer.setPullRequest(r.issueNumber, r.prNumber, r.prUrl);
130
+ }
131
+ }
132
+ }
133
+ displaySummary(result, renderer);
134
+ renderer?.dispose();
135
+ if (result.exitCode !== 0)
136
+ process.exit(result.exitCode);
128
137
  }
129
- if (config.dryRun) {
130
- console.log(colors.warning(" ℹ️ This was a dry run. Use without --dry-run to execute."));
131
- console.log("");
138
+ finally {
139
+ heartbeat?.dispose();
140
+ if (renderer)
141
+ process.off("SIGINT", sigintHandler);
132
142
  }
133
143
  }
@@ -9,6 +9,8 @@ interface StatsOptions {
9
9
  csv?: boolean;
10
10
  json?: boolean;
11
11
  detailed?: boolean;
12
+ label?: string;
13
+ since?: string;
12
14
  }
13
15
  /**
14
16
  * Main stats command
@@ -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
- // Try to load local metrics first
574
- const metrics = loadMetrics();
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 logs = logFiles
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 (logs.length === 0) {
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 logs = logFiles
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 logs = logFiles
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 (logs.length === 0) {
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 logs = logFiles
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);