sequant 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/README.md +125 -160
  4. package/dist/bin/cli.js +59 -4
  5. package/dist/dashboard/server.js +1 -0
  6. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +2 -2
  7. package/dist/marketplace/external_plugins/sequant/README.md +6 -3
  8. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +92 -0
  9. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +18 -9
  10. package/dist/marketplace/external_plugins/sequant/hooks/relay-check.sh +107 -0
  11. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/behavior-rule-detection.md +205 -0
  12. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +21 -8
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +302 -86
  14. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +109 -0
  15. package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +141 -22
  16. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +83 -78
  17. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +377 -137
  18. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +28 -0
  19. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +621 -0
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +741 -232
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +47 -1
  22. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +12 -6
  23. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +217 -964
  24. package/dist/marketplace/external_plugins/sequant/skills/spec/references/parallel-groups.md +7 -0
  25. package/dist/marketplace/external_plugins/sequant/skills/spec/references/quality-checklist.md +75 -0
  26. package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +4 -2
  27. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +0 -27
  28. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +24 -44
  29. package/dist/src/commands/abort.d.ts +36 -0
  30. package/dist/src/commands/abort.js +138 -0
  31. package/dist/src/commands/prompt.d.ts +7 -0
  32. package/dist/src/commands/prompt.js +101 -7
  33. package/dist/src/commands/ready-tui-adapter.d.ts +59 -0
  34. package/dist/src/commands/ready-tui-adapter.js +130 -0
  35. package/dist/src/commands/ready.d.ts +49 -0
  36. package/dist/src/commands/ready.js +243 -0
  37. package/dist/src/commands/run-progress.d.ts +11 -1
  38. package/dist/src/commands/run-progress.js +20 -3
  39. package/dist/src/commands/run.js +12 -2
  40. package/dist/src/commands/status.js +4 -0
  41. package/dist/src/commands/watch.d.ts +2 -0
  42. package/dist/src/commands/watch.js +67 -3
  43. package/dist/src/lib/assess-collision-detect.js +1 -1
  44. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +39 -0
  45. package/dist/src/lib/cli-ui/run-renderer.d.ts +34 -2
  46. package/dist/src/lib/cli-ui/run-renderer.js +250 -33
  47. package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
  48. package/dist/src/lib/cli-ui/scrollback-harness.js +294 -0
  49. package/dist/src/lib/merge-check/types.js +1 -1
  50. package/dist/src/lib/relay/archive.js +6 -0
  51. package/dist/src/lib/relay/types.d.ts +2 -0
  52. package/dist/src/lib/relay/types.js +9 -0
  53. package/dist/src/lib/settings.d.ts +34 -0
  54. package/dist/src/lib/settings.js +23 -1
  55. package/dist/src/lib/workflow/batch-executor.js +34 -18
  56. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
  57. package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
  58. package/dist/src/lib/workflow/drivers/aider.js +9 -0
  59. package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
  60. package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
  61. package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
  62. package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
  63. package/dist/src/lib/workflow/event-emitter.js +102 -0
  64. package/dist/src/lib/workflow/notice.d.ts +32 -0
  65. package/dist/src/lib/workflow/notice.js +38 -0
  66. package/dist/src/lib/workflow/phase-executor.d.ts +9 -21
  67. package/dist/src/lib/workflow/phase-executor.js +105 -117
  68. package/dist/src/lib/workflow/phase-mapper.d.ts +26 -13
  69. package/dist/src/lib/workflow/phase-mapper.js +55 -33
  70. package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
  71. package/dist/src/lib/workflow/phase-registry.js +233 -0
  72. package/dist/src/lib/workflow/platforms/github.d.ts +6 -0
  73. package/dist/src/lib/workflow/platforms/github.js +17 -0
  74. package/dist/src/lib/workflow/ready-gate.d.ts +155 -0
  75. package/dist/src/lib/workflow/ready-gate.js +374 -0
  76. package/dist/src/lib/workflow/reconcile.js +6 -0
  77. package/dist/src/lib/workflow/run-log-schema.d.ts +5 -55
  78. package/dist/src/lib/workflow/run-orchestrator.d.ts +32 -2
  79. package/dist/src/lib/workflow/run-orchestrator.js +125 -11
  80. package/dist/src/lib/workflow/state-manager.d.ts +19 -1
  81. package/dist/src/lib/workflow/state-manager.js +27 -1
  82. package/dist/src/lib/workflow/state-schema.d.ts +23 -35
  83. package/dist/src/lib/workflow/state-schema.js +29 -3
  84. package/dist/src/lib/workflow/types.d.ts +74 -15
  85. package/dist/src/lib/workflow/types.js +18 -13
  86. package/dist/src/ui/tui/App.js +8 -2
  87. package/dist/src/ui/tui/IssueBox.js +3 -4
  88. package/dist/src/ui/tui/index.d.ts +13 -4
  89. package/dist/src/ui/tui/index.js +19 -5
  90. package/dist/src/ui/tui/row-cap.d.ts +51 -0
  91. package/dist/src/ui/tui/row-cap.js +76 -0
  92. package/dist/src/ui/tui/teardown.d.ts +20 -0
  93. package/dist/src/ui/tui/teardown.js +29 -0
  94. package/dist/src/ui/tui/theme.d.ts +3 -0
  95. package/dist/src/ui/tui/theme.js +3 -0
  96. package/package.json +23 -11
  97. package/templates/hooks/post-tool.sh +81 -0
  98. package/templates/skills/assess/SKILL.md +28 -28
  99. package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
  100. package/templates/skills/qa/SKILL.md +5 -2
  101. package/templates/skills/setup/SKILL.md +6 -6
@@ -6,6 +6,8 @@
6
6
  import { existsSync, statSync, createReadStream, watch } from "fs";
7
7
  import chalk from "chalk";
8
8
  import { outboxPathFor } from "../lib/relay/paths.js";
9
+ import { listArchives } from "../lib/relay/archive.js";
10
+ import { readPidFile } from "../lib/relay/pid.js";
9
11
  import { StateManager } from "../lib/workflow/state-manager.js";
10
12
  import { RelayResponseSchema } from "../lib/relay/types.js";
11
13
  function formatTimestamp(iso) {
@@ -82,8 +84,10 @@ export async function watchCommand(argsAndOptions) {
82
84
  }
83
85
  const stateManager = new StateManager();
84
86
  const issueState = await stateManager.getIssueState(issueNumber);
87
+ const cwd = options.cwd ?? process.cwd();
85
88
  const outboxPath = outboxPathFor(issueNumber, {
86
89
  worktreePath: issueState?.worktree,
90
+ cwd,
87
91
  });
88
92
  const pollIntervalMs = options.pollIntervalMs ?? 200;
89
93
  const tail = { offset: 0, partial: "" };
@@ -91,16 +95,43 @@ export async function watchCommand(argsAndOptions) {
91
95
  if (existsSync(outboxPath)) {
92
96
  tail.offset = statSync(outboxPath).size;
93
97
  }
98
+ // Dead-relay detection (#645, Gap 3). The pidfile is written by activateRelay
99
+ // and removed by deactivateRelay. If it's absent and the outbox is absent at
100
+ // startup, there is nothing alive to watch — print a useful pointer and exit.
101
+ const initialPidPresent = readPidFile(issueNumber, cwd) !== null;
102
+ const initialOutboxPresent = existsSync(outboxPath);
103
+ if (!initialPidPresent && !initialOutboxPresent) {
104
+ const archives = listArchives(issueNumber, cwd);
105
+ const summary = `No active relay for #${issueNumber}.`;
106
+ const hint = archives[0]
107
+ ? ` Most recent archive: ${archives[0]}`
108
+ : " (no archived runs found)";
109
+ if (options.json) {
110
+ console.log(JSON.stringify({
111
+ ok: false,
112
+ issue: issueNumber,
113
+ reason: "no-active-relay",
114
+ archive: archives[0] ?? null,
115
+ }));
116
+ }
117
+ else {
118
+ console.log(chalk.yellow(summary + hint));
119
+ }
120
+ return;
121
+ }
94
122
  if (!options.json) {
95
123
  console.log(chalk.gray(`Watching #${issueNumber} outbox — Ctrl+C to stop`));
96
124
  }
97
125
  let stopped = false;
98
- const stop = () => {
126
+ let endReason = null;
127
+ const stop = (reason = "signal") => {
99
128
  stopped = true;
129
+ if (!endReason)
130
+ endReason = reason;
100
131
  };
101
- options.signal?.addEventListener("abort", stop);
132
+ options.signal?.addEventListener("abort", () => stop("signal"));
102
133
  process.on("SIGINT", () => {
103
- stop();
134
+ stop("signal");
104
135
  if (!options.json)
105
136
  console.log(chalk.gray("\nStopped watching."));
106
137
  process.exit(0);
@@ -125,6 +156,7 @@ export async function watchCommand(argsAndOptions) {
125
156
  };
126
157
  // Polling loop — also used as a heartbeat when fs.watch is active so we
127
158
  // don't miss events on filesystems where watch is unreliable.
159
+ let sawLivePid = initialPidPresent;
128
160
  while (!stopped) {
129
161
  try {
130
162
  const replies = await readNewLines(outboxPath, tail);
@@ -133,6 +165,23 @@ export async function watchCommand(argsAndOptions) {
133
165
  catch {
134
166
  /* transient — try again next tick */
135
167
  }
168
+ // Dead-relay detection (#645, Gap 3). Once we've seen a live pidfile, its
169
+ // absence means the run has deactivated relay (archive complete). Drain
170
+ // one more poll for late writes, then exit cleanly.
171
+ const pidAlive = readPidFile(issueNumber, cwd) !== null;
172
+ if (sawLivePid && !pidAlive) {
173
+ try {
174
+ const finalReplies = await readNewLines(outboxPath, tail);
175
+ emit(finalReplies);
176
+ }
177
+ catch {
178
+ /* swallow */
179
+ }
180
+ stop("relay-ended");
181
+ break;
182
+ }
183
+ if (pidAlive)
184
+ sawLivePid = true;
136
185
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
137
186
  }
138
187
  if (watcher) {
@@ -144,4 +193,19 @@ export async function watchCommand(argsAndOptions) {
144
193
  }
145
194
  }
146
195
  void useWatcher; // currently unused beyond best-effort init
196
+ if (endReason === "relay-ended") {
197
+ const archives = listArchives(issueNumber, cwd);
198
+ if (options.json) {
199
+ console.log(JSON.stringify({
200
+ ok: true,
201
+ issue: issueNumber,
202
+ reason: "relay-ended",
203
+ archive: archives[0] ?? null,
204
+ }));
205
+ }
206
+ else {
207
+ const hint = archives[0] ? ` Archive: ${archives[0]}` : "";
208
+ console.log(chalk.gray(`Run for #${issueNumber} ended.${hint}`));
209
+ }
210
+ }
147
211
  }
@@ -208,7 +208,7 @@ export function formatCollisionAnnotations(results) {
208
208
  if (r.issues.length >= 3 && !chainSuggestion) {
209
209
  const ids = r.issues.join(" ");
210
210
  chainSuggestion =
211
- `Chain: npx sequant run ${ids} --chain --qa-gate -q ` +
211
+ `Chain: npx sequant run ${ids} --chain --qa-gate -Q ` +
212
212
  `# alternative — ${r.issues.length} issues modify ${r.file} ` +
213
213
  `(chain length≥3 historically 1/6 = 17%; see docs/reference/chain-mode-analysis-2026-05.md)`;
214
214
  }
@@ -76,6 +76,15 @@ export interface IssueRegistration {
76
76
  * is known) and switches to the normal phase header once spec completes.
77
77
  */
78
78
  autoDetect?: boolean;
79
+ /**
80
+ * #672 AC-2: the resolved phase pipeline for this issue. When set, the live
81
+ * zone seeds one pending cell per planned phase so users see the full
82
+ * roadmap before any phase fires. Cells transition pending → running → ✔/✘
83
+ * in place via subsequent `onEvent` calls (#672 AC-3). When the plan isn't
84
+ * known at registration time (auto-detect mode), call `setPhasePlan` once
85
+ * spec resolves it.
86
+ */
87
+ plannedPhases?: string[];
79
88
  }
80
89
  /** Per-issue summary fields used by the final summary table. */
81
90
  export interface IssueSummary {
@@ -106,12 +115,26 @@ export interface RunRenderer {
106
115
  registerIssue(reg: IssueRegistration): void;
107
116
  /** Feed a progress event from batch-executor. */
108
117
  onEvent(event: ProgressEvent): void;
118
+ /**
119
+ * #672 AC-2: set or replace the planned phase pipeline for an already-
120
+ * registered issue. Used by auto-detect mode after spec resolves the plan.
121
+ * No-op for unregistered issues (defensive — same as `setPullRequest`).
122
+ * An empty `phases` array clears the plan back to streaming-only behaviour.
123
+ */
124
+ setPhasePlan(issue: number, phases: string[]): void;
109
125
  /** Mark an issue as completed with PR info. Called by orchestrator. */
110
126
  setPullRequest(issue: number, prNumber: number, prUrl: string): void;
111
127
  /** Pause live updates so verbose streaming can write through. */
112
128
  pause(): void;
113
129
  /** Resume live updates after streaming ends. */
114
130
  resume(): void;
131
+ /**
132
+ * #647 AC-3: print a notice line above the live zone without breaking
133
+ * log-update's cursor model. Use for retry/fallback messages emitted
134
+ * from outside the renderer's event flow (e.g., phase-executor retry
135
+ * paths).
136
+ */
137
+ appendNotice(message: string): void;
115
138
  /** Render the final summary block. */
116
139
  renderSummary(input: SummaryRenderInput): void;
117
140
  /** Tear down timers, cursor state, signal listeners. */
@@ -173,6 +196,22 @@ export interface RenderOptions {
173
196
  * Default: false (matches existing displaySummary behaviour).
174
197
  */
175
198
  alwaysRenderSummary?: boolean;
199
+ /**
200
+ * #647: inject a `log-update` instance (typically built via
201
+ * `createLogUpdate(stream)` against a custom stream). Used by the
202
+ * scrollback-harness regression test to drive the real `log-update`
203
+ * through a virtual terminal that tracks scrollback. When set, this takes
204
+ * precedence over both `stdoutWrite` (for the log-update path) and the
205
+ * default `process.stdout`-bound `logUpdate` import.
206
+ *
207
+ * Production code never sets this. Tests that need to assert on
208
+ * `log-update`'s actual erase semantics use it to replace the test stub.
209
+ */
210
+ logUpdateInstance?: {
211
+ (text: string): void;
212
+ clear(): void;
213
+ done(): void;
214
+ };
176
215
  }
177
216
  /**
178
217
  * Mode the renderer should run in. Auto-detected by `createRunRenderer` from
@@ -13,6 +13,7 @@
13
13
  *
14
14
  * See issue #618.
15
15
  */
16
+ import type { PhasePauseHandle } from "../workflow/types.js";
16
17
  import type { IssueRegistration, IssueState, ProgressEvent, RenderOptions, RendererMode, RunRenderer, SummaryRenderInput } from "./run-renderer-types.js";
17
18
  /**
18
19
  * #624 Item 4: normalized failure signature for dedup decisions.
@@ -35,7 +36,7 @@ export declare function failureSignature(error: string | undefined): string;
35
36
  * (no iteration, iteration === 1, or non-positive).
36
37
  */
37
38
  export declare function formatRetrySuffix(iteration: number | undefined, maxIterations: number, kind: "events" | "header"): string;
38
- declare abstract class BaseRenderer implements RunRenderer {
39
+ declare abstract class BaseRenderer implements RunRenderer, PhasePauseHandle {
39
40
  protected readonly issues: Map<number, IssueState>;
40
41
  protected readonly stdoutWrite: (s: string) => void;
41
42
  protected readonly stderrWrite: (s: string) => void;
@@ -47,10 +48,18 @@ declare abstract class BaseRenderer implements RunRenderer {
47
48
  protected disposed: boolean;
48
49
  constructor(options: RenderOptions);
49
50
  registerIssue(reg: IssueRegistration): void;
51
+ setPhasePlan(issue: number, phases: string[]): void;
50
52
  onEvent(event: ProgressEvent): void;
51
53
  setPullRequest(issue: number, prNumber: number, prUrl: string): void;
52
54
  pause(): void;
53
55
  resume(): void;
56
+ /**
57
+ * #647 AC-3: default notice path — just write to the renderer's stdout
58
+ * channel. NonTTYRenderer keeps this default (no live zone to manage).
59
+ * TTYRenderer overrides to clear the live zone before writing so
60
+ * log-update's cursor model stays consistent with the actual terminal.
61
+ */
62
+ appendNotice(message: string): void;
54
63
  abstract renderSummary(input: SummaryRenderInput): void;
55
64
  dispose(): void;
56
65
  protected applyEvent(state: IssueState, event: ProgressEvent): void;
@@ -134,6 +143,14 @@ export declare class TTYRenderer extends BaseRenderer {
134
143
  /**
135
144
  * #624 Derived AC-D1: expose the test-only log-update stub. Returns `null`
136
145
  * when not in test mode (production renders go through real `log-update`).
146
+ *
147
+ * #647 AC-D3 warning: this stub does NOT model `log-update`'s ANSI cursor
148
+ * or scrollback semantics. Tests that assert on `stub.lastFrame` only see
149
+ * the most recent frame, not whether earlier frames remained stranded in
150
+ * scrollback. Header-count / duplicate-header assertions MUST use
151
+ * `scrollback-harness.ts` (real `createLogUpdate` + VirtualTerminal),
152
+ * otherwise they will pass green even when the production rendering is
153
+ * broken — see #624 for the precedent.
137
154
  */
138
155
  getTestStub(): TTYTestStub | null;
139
156
  private startLiveTimer;
@@ -150,6 +167,15 @@ export declare class TTYRenderer extends BaseRenderer {
150
167
  renderSummary(input: SummaryRenderInput): void;
151
168
  protected onPause(): void;
152
169
  protected onResume(): void;
170
+ /**
171
+ * #647 AC-3: TTYRenderer override. Writes the notice above the live zone
172
+ * the same way `appendEventLine` does (clear → write → redraw), so
173
+ * log-update's `previousLineCount` stays consistent with the actual
174
+ * terminal state. If the renderer is already paused (e.g., during
175
+ * verbose subprocess streaming), skip the clear/redraw and just write;
176
+ * the eventual `resume()` will redraw cleanly.
177
+ */
178
+ appendNotice(message: string): void;
153
179
  protected onDispose(): void;
154
180
  private getColumns;
155
181
  /**
@@ -210,7 +236,13 @@ export declare class TTYRenderer extends BaseRenderer {
210
236
  private statusCellLines;
211
237
  private runHeader;
212
238
  private rollupLine;
213
- private drawKeyValueTable;
239
+ /**
240
+ * Single-issue layout: indented `label value` lines, no box drawing. The
241
+ * label is cyan and padded to `labelW`; continuation lines (multi-line
242
+ * status cells) align under the value column with a blank label. See
243
+ * `renderSingleIssueFrame` for why the bordered grid was dropped.
244
+ */
245
+ private drawKeyValueLines;
214
246
  private drawIssueGrid;
215
247
  }
216
248
  export interface CreateRendererOptions extends RenderOptions {