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
@@ -1,8 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from "react";
3
- import { Box, useStdout } from "ink";
3
+ import { Box, Text, useStdout } from "ink";
4
4
  import { Header } from "./Header.js";
5
5
  import { IssueBox } from "./IssueBox.js";
6
+ import { selectVisibleIssues } from "./row-cap.js";
7
+ import { ROLLUP_COLOR } from "./theme.js";
6
8
  const POLL_MS = 100; // 10 Hz
7
9
  /**
8
10
  * Root TUI component.
@@ -37,5 +39,9 @@ export function App({ getSnapshot, onDone, }) {
37
39
  }, []);
38
40
  const columns = stdout?.columns ?? 80;
39
41
  const boxWidth = Math.min(columns - 2, 100);
40
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { snapshot: snapshot }), snapshot.issues.map((issue, i) => (_jsx(IssueBox, { state: issue, slot: i, width: boxWidth, now: now }, issue.number)))] }));
42
+ // #699 AC-4: clamp the number of boxes to the terminal height so a large
43
+ // batch on a short terminal can't overflow the frame (parity with the plain
44
+ // renderer's #624 row cap). Older completed issues collapse into `✔ N done`.
45
+ const { visible, rolledUpDoneCount } = selectVisibleIssues(snapshot.issues, stdout?.rows);
46
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { snapshot: snapshot }), visible.map((issue, i) => (_jsx(IssueBox, { state: issue, slot: i, width: boxWidth, now: now }, issue.number))), rolledUpDoneCount > 0 ? (_jsx(Text, { color: ROLLUP_COLOR, children: `✔ ${rolledUpDoneCount} done` })) : null] }));
41
47
  }
@@ -20,7 +20,7 @@ export function IssueBox({ state, slot, width, now, }) {
20
20
  const displayPhaseN = activePhaseIndex >= 0 ? activePhaseIndex + 1 : doneCount;
21
21
  const total = state.phases.length;
22
22
  const headerTitle = truncateToWidth(`#${state.number} ${state.title}`, Math.max(10, innerWidth - 20));
23
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: border, paddingX: 1, marginBottom: 1, width: width, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: border, children: headerTitle }), _jsxs(Text, { color: DIVIDER_COLOR, children: ["phase ", displayPhaseN, "/", total, " \u2022", " ", _jsx(ElapsedTimer, { startedAt: state.startedAt })] })] }), _jsx(Divider, { width: innerWidth, borderColor: border }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "branch " }), _jsx(Text, { children: truncateToWidth(state.branch, innerWidth - 8) })] }), _jsx(PhaseProgression, { phases: state.phases, borderColor: border }), state.currentPhase?.logPath ? (_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "log " }), _jsx(Text, { children: truncateToWidth(state.currentPhase.logPath, innerWidth - 8) })] })) : null] }), _jsx(Divider, { width: innerWidth, borderColor: border }), _jsx(Box, { flexDirection: "column", children: state.currentPhase ? (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "now " }), _jsx(Spinner, { color: border }), _jsxs(Text, { children: [" ", truncateToWidth(state.currentPhase.nowLine, innerWidth - 12)] })] }), _jsx(Box, { children: _jsxs(Text, { color: DIVIDER_COLOR, children: [" └ last activity ", formatSinceActivity(now, state.currentPhase.lastActivityAt)] }) })] })) : (_jsx(Text, { color: DIVIDER_COLOR, children: statusLine(state) })) })] }));
23
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: border, paddingX: 1, marginBottom: 1, width: width, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: border, children: headerTitle }), _jsxs(Text, { color: DIVIDER_COLOR, children: ["phase ", displayPhaseN, "/", total, " \u2022", " ", _jsx(ElapsedTimer, { startedAt: state.startedAt })] })] }), _jsx(Divider, { width: innerWidth }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "branch " }), _jsx(Text, { children: truncateToWidth(state.branch, innerWidth - 8) })] }), _jsx(PhaseProgression, { phases: state.phases, borderColor: border }), state.currentPhase?.logPath ? (_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "log " }), _jsx(Text, { children: truncateToWidth(state.currentPhase.logPath, innerWidth - 8) })] })) : null] }), _jsx(Divider, { width: innerWidth }), _jsx(Box, { flexDirection: "column", children: state.currentPhase ? (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "now " }), _jsx(Spinner, { color: border }), _jsxs(Text, { children: [" ", truncateToWidth(state.currentPhase.nowLine, innerWidth - 12)] })] }), _jsx(Box, { children: _jsxs(Text, { color: DIVIDER_COLOR, children: [" └ last activity ", formatSinceActivity(now, state.currentPhase.lastActivityAt)] }) })] })) : (_jsx(Text, { color: DIVIDER_COLOR, children: statusLine(state) })) })] }));
24
24
  }
25
25
  function statusLine(state) {
26
26
  switch (state.status) {
@@ -34,9 +34,8 @@ function statusLine(state) {
34
34
  return "failed";
35
35
  }
36
36
  }
37
- function Divider({ width, borderColor, }) {
38
- const mid = "─".repeat(Math.max(0, width - 2));
39
- return (_jsxs(Text, { children: [_jsx(Text, { color: borderColor, children: "\u251C" }), _jsx(Text, { color: DIVIDER_COLOR, children: mid }), _jsx(Text, { color: borderColor, children: "\u2524" })] }));
37
+ function Divider({ width }) {
38
+ return _jsx(Text, { color: DIVIDER_COLOR, children: "─".repeat(Math.max(0, width)) });
40
39
  }
41
40
  function PhaseProgression({ phases, borderColor, }) {
42
41
  return (_jsxs(Box, { flexWrap: "wrap", children: [_jsx(Text, { color: DIVIDER_COLOR, children: "phases " }), phases.map((p, i) => {
@@ -1,15 +1,24 @@
1
1
  /**
2
2
  * Experimental multi-issue TUI entry point.
3
3
  *
4
- * Mounts an `ink` app that polls `RunOrchestrator.getSnapshot()` at 10 Hz.
5
- * Unmounts when the orchestrator reports `done` so the shell returns
4
+ * Mounts an `ink` app that polls a snapshot provider's `getSnapshot()` at
5
+ * 10 Hz. Unmounts when the snapshot reports `done` so the shell returns
6
6
  * cleanly. Only safe to call when `process.stdout.isTTY` is true.
7
+ *
8
+ * The provider is structural (`{ getSnapshot(): RunSnapshot }`) so any source
9
+ * of run state can drive the TUI — `RunOrchestrator` for `sequant run`, or the
10
+ * single-issue adapter `sequant ready` owns (#699). The TUI only ever reads
11
+ * `getSnapshot()`, never the orchestrator's batch lifecycle.
7
12
  */
8
- import type { RunOrchestrator } from "../../lib/workflow/run-orchestrator.js";
13
+ import type { RunSnapshot } from "../../lib/workflow/run-state.js";
14
+ /** Minimal structural contract the TUI needs from its state source. */
15
+ export interface SnapshotProvider {
16
+ getSnapshot(): RunSnapshot;
17
+ }
9
18
  export interface TuiHandle {
10
19
  /** Promise that resolves when the TUI unmounts. */
11
20
  done: Promise<void>;
12
21
  /** Force-unmount (e.g., on SIGINT fallback). */
13
22
  unmount: () => void;
14
23
  }
15
- export declare function renderTui(orchestrator: RunOrchestrator): TuiHandle;
24
+ export declare function renderTui(provider: SnapshotProvider): TuiHandle;
@@ -1,25 +1,39 @@
1
1
  /**
2
2
  * Experimental multi-issue TUI entry point.
3
3
  *
4
- * Mounts an `ink` app that polls `RunOrchestrator.getSnapshot()` at 10 Hz.
5
- * Unmounts when the orchestrator reports `done` so the shell returns
4
+ * Mounts an `ink` app that polls a snapshot provider's `getSnapshot()` at
5
+ * 10 Hz. Unmounts when the snapshot reports `done` so the shell returns
6
6
  * cleanly. Only safe to call when `process.stdout.isTTY` is true.
7
+ *
8
+ * The provider is structural (`{ getSnapshot(): RunSnapshot }`) so any source
9
+ * of run state can drive the TUI — `RunOrchestrator` for `sequant run`, or the
10
+ * single-issue adapter `sequant ready` owns (#699). The TUI only ever reads
11
+ * `getSnapshot()`, never the orchestrator's batch lifecycle.
7
12
  */
8
13
  import { createElement } from "react";
9
14
  import { render } from "ink";
10
15
  import { App } from "./App.js";
11
- export function renderTui(orchestrator) {
16
+ import { composeTeardownSummary } from "./teardown.js";
17
+ export function renderTui(provider) {
12
18
  let resolveDone;
13
19
  const done = new Promise((resolve) => {
14
20
  resolveDone = resolve;
15
21
  });
16
22
  const instance = render(createElement(App, {
17
- getSnapshot: () => orchestrator.getSnapshot(),
23
+ getSnapshot: () => provider.getSnapshot(),
18
24
  onDone: () => {
19
25
  instance.unmount();
20
26
  },
21
27
  }), { exitOnCtrlC: false });
22
- instance.waitUntilExit().then(() => resolveDone());
28
+ instance.waitUntilExit().then(() => {
29
+ // #699 AC-5: ink leaves no per-issue history on unmount, so write a durable
30
+ // `✔/✘` transcript from the final snapshot into scrollback. Runs before
31
+ // `done` resolves so the caller's own report (e.g. `ready`'s) prints after.
32
+ const summary = composeTeardownSummary(provider.getSnapshot());
33
+ if (summary)
34
+ process.stdout.write(summary + "\n");
35
+ resolveDone();
36
+ });
23
37
  return {
24
38
  done,
25
39
  unmount: () => {
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Row cap + frame-height clamp for the Ink TUI (#699 AC-4).
3
+ *
4
+ * Parity with the plain renderer's #624 behavior (`run-renderer.ts`
5
+ * `applyRowCap` / `effectiveRowCap`): N issues must never render more boxes
6
+ * than the terminal can hold. Keep every active (queued/running) issue, fill
7
+ * the remaining slots with the most-recently-completed issues, and roll older
8
+ * completed issues into a single `✔ N done` summary line.
9
+ *
10
+ * The TUI difference is vertical density: each plain-grid issue is ~3 lines,
11
+ * but a full Ink box is ~9–11 lines (round border + 3 cells + 2 dividers +
12
+ * bottom margin). `LINES_PER_BOX` is sized accordingly so the dynamic cap
13
+ * reflects how many boxes actually fit.
14
+ */
15
+ import type { IssueRuntimeState } from "../../lib/workflow/run-state.js";
16
+ /** Static row cap (matches the plain renderer's default). */
17
+ export declare const DEFAULT_TUI_ROW_CAP = 10;
18
+ /**
19
+ * Approximate height of one rendered Ink box, in terminal rows: round border
20
+ * top/bottom (2) + header (1) + two dividers (2) + context cell (~3) +
21
+ * activity cell (~2) + bottom margin (1) ≈ 11.
22
+ */
23
+ export declare const TUI_LINES_PER_BOX = 11;
24
+ /**
25
+ * Effective cap: the smaller of the static cap and a dynamic terminal-height
26
+ * ceiling. Mirrors `run-renderer.ts` `effectiveRowCap`, but with a box-height
27
+ * `linesPerBox` rather than the plain grid's 3.
28
+ *
29
+ * When `rows` is unknown (no TTY size), trust the static cap directly rather
30
+ * than guessing a height — the same "don't over-clamp" intent as the plain
31
+ * renderer's tall default, without picking an arbitrary fallback row count.
32
+ *
33
+ * @internal Exported for testing.
34
+ */
35
+ export declare function effectiveTuiRowCap(rows: number | undefined, staticCap?: number, linesPerBox?: number): number;
36
+ export interface VisibleSelection {
37
+ /** Boxes to render, in order: active issues first, then recent done. */
38
+ visible: IssueRuntimeState[];
39
+ /** Older completed issues collapsed into the `✔ N done` summary (0 if none). */
40
+ rolledUpDoneCount: number;
41
+ }
42
+ /**
43
+ * Select which issue boxes to render so the frame never exceeds the terminal
44
+ * height. Parity with `run-renderer.ts` `applyRowCap`.
45
+ *
46
+ * - Under the cap → render everything, no rollup.
47
+ * - Over the cap → keep all active issues; fill remaining slots (minus one
48
+ * reserved for the rollup line) with the most-recently-completed issues;
49
+ * the rest roll up into `rolledUpDoneCount`.
50
+ */
51
+ export declare function selectVisibleIssues(issues: IssueRuntimeState[], rows: number | undefined, staticCap?: number, linesPerBox?: number): VisibleSelection;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Row cap + frame-height clamp for the Ink TUI (#699 AC-4).
3
+ *
4
+ * Parity with the plain renderer's #624 behavior (`run-renderer.ts`
5
+ * `applyRowCap` / `effectiveRowCap`): N issues must never render more boxes
6
+ * than the terminal can hold. Keep every active (queued/running) issue, fill
7
+ * the remaining slots with the most-recently-completed issues, and roll older
8
+ * completed issues into a single `✔ N done` summary line.
9
+ *
10
+ * The TUI difference is vertical density: each plain-grid issue is ~3 lines,
11
+ * but a full Ink box is ~9–11 lines (round border + 3 cells + 2 dividers +
12
+ * bottom margin). `LINES_PER_BOX` is sized accordingly so the dynamic cap
13
+ * reflects how many boxes actually fit.
14
+ */
15
+ /** Static row cap (matches the plain renderer's default). */
16
+ export const DEFAULT_TUI_ROW_CAP = 10;
17
+ /**
18
+ * Approximate height of one rendered Ink box, in terminal rows: round border
19
+ * top/bottom (2) + header (1) + two dividers (2) + context cell (~3) +
20
+ * activity cell (~2) + bottom margin (1) ≈ 11.
21
+ */
22
+ export const TUI_LINES_PER_BOX = 11;
23
+ /**
24
+ * Fixed vertical overhead outside the issue boxes: the Header block plus the
25
+ * rolled-up `✔ N done` summary line.
26
+ */
27
+ const FIXED_OVERHEAD = 4;
28
+ /** A queued or running issue is "active"; passed/failed are terminal. */
29
+ function isActive(issue) {
30
+ return issue.status === "queued" || issue.status === "running";
31
+ }
32
+ /**
33
+ * Effective cap: the smaller of the static cap and a dynamic terminal-height
34
+ * ceiling. Mirrors `run-renderer.ts` `effectiveRowCap`, but with a box-height
35
+ * `linesPerBox` rather than the plain grid's 3.
36
+ *
37
+ * When `rows` is unknown (no TTY size), trust the static cap directly rather
38
+ * than guessing a height — the same "don't over-clamp" intent as the plain
39
+ * renderer's tall default, without picking an arbitrary fallback row count.
40
+ *
41
+ * @internal Exported for testing.
42
+ */
43
+ export function effectiveTuiRowCap(rows, staticCap = DEFAULT_TUI_ROW_CAP, linesPerBox = TUI_LINES_PER_BOX) {
44
+ if (!rows || rows <= 0)
45
+ return staticCap;
46
+ const dynamicCap = Math.max(1, Math.floor((rows - FIXED_OVERHEAD) / Math.max(1, linesPerBox)));
47
+ return Math.min(staticCap, dynamicCap);
48
+ }
49
+ /**
50
+ * Select which issue boxes to render so the frame never exceeds the terminal
51
+ * height. Parity with `run-renderer.ts` `applyRowCap`.
52
+ *
53
+ * - Under the cap → render everything, no rollup.
54
+ * - Over the cap → keep all active issues; fill remaining slots (minus one
55
+ * reserved for the rollup line) with the most-recently-completed issues;
56
+ * the rest roll up into `rolledUpDoneCount`.
57
+ */
58
+ export function selectVisibleIssues(issues, rows, staticCap = DEFAULT_TUI_ROW_CAP, linesPerBox = TUI_LINES_PER_BOX) {
59
+ const cap = effectiveTuiRowCap(rows, staticCap, linesPerBox);
60
+ if (issues.length <= cap) {
61
+ return { visible: issues, rolledUpDoneCount: 0 };
62
+ }
63
+ const active = issues.filter(isActive);
64
+ const done = issues
65
+ .filter((i) => !isActive(i))
66
+ .sort((a, b) => (b.completedAt?.getTime() ?? 0) - (a.completedAt?.getTime() ?? 0));
67
+ // Reserve one slot for the rollup line; the rest go to visible boxes.
68
+ const visibleSlots = Math.max(1, cap - 1);
69
+ const remainingForDone = Math.max(0, visibleSlots - active.length);
70
+ const visibleDone = done.slice(0, remainingForDone);
71
+ const rolledUpDoneCount = done.length - visibleDone.length;
72
+ return {
73
+ visible: [...active, ...visibleDone],
74
+ rolledUpDoneCount,
75
+ };
76
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Durable teardown summary for the Ink TUI (#699 AC-5).
3
+ *
4
+ * Ink repaints a live region in place; unlike the plain renderer it appends no
5
+ * per-issue `✔/✘` history when it unmounts. So on teardown we compose a compact
6
+ * transcript line per issue from the final snapshot and write it to stdout
7
+ * (outside ink's managed region) so a completed run leaves a record in
8
+ * scrollback. Emitting it here in the shared entry point means both `run` and
9
+ * `sequant ready` inherit it.
10
+ */
11
+ import type { RunSnapshot } from "../../lib/workflow/run-state.js";
12
+ /**
13
+ * Compose the durable teardown summary from a final snapshot.
14
+ *
15
+ * Returns a newline-joined block of one line per issue, or an empty string when
16
+ * there are no issues (nothing to record).
17
+ *
18
+ * @internal Exported for testing.
19
+ */
20
+ export declare function composeTeardownSummary(snapshot: RunSnapshot): string;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Durable teardown summary for the Ink TUI (#699 AC-5).
3
+ *
4
+ * Ink repaints a live region in place; unlike the plain renderer it appends no
5
+ * per-issue `✔/✘` history when it unmounts. So on teardown we compose a compact
6
+ * transcript line per issue from the final snapshot and write it to stdout
7
+ * (outside ink's managed region) so a completed run leaves a record in
8
+ * scrollback. Emitting it here in the shared entry point means both `run` and
9
+ * `sequant ready` inherit it.
10
+ */
11
+ /** One transcript line per issue, e.g. `✔ #699 Upgrade ready to the Ink TUI`. */
12
+ function issueLine(issue) {
13
+ const glyph = issue.status === "failed" ? "✘" : "✔";
14
+ const title = issue.title ? ` ${issue.title}` : "";
15
+ return `${glyph} #${issue.number}${title}`;
16
+ }
17
+ /**
18
+ * Compose the durable teardown summary from a final snapshot.
19
+ *
20
+ * Returns a newline-joined block of one line per issue, or an empty string when
21
+ * there are no issues (nothing to record).
22
+ *
23
+ * @internal Exported for testing.
24
+ */
25
+ export function composeTeardownSummary(snapshot) {
26
+ if (!snapshot.issues.length)
27
+ return "";
28
+ return snapshot.issues.map(issueLine).join("\n");
29
+ }
@@ -11,6 +11,9 @@ export declare const BORDER_ROTATION: readonly ["cyan", "magenta", "blue", "yell
11
11
  export type BorderColor = (typeof BORDER_ROTATION)[number] | "green" | "red" | "gray";
12
12
  /** Gray used for horizontal dividers inside each box. */
13
13
  export declare const DIVIDER_COLOR: "gray";
14
+ /** Green used for the rolled-up `✔ N done` summary line (#699, parity with the
15
+ * plain renderer's #624 rollup). */
16
+ export declare const ROLLUP_COLOR: "green";
14
17
  /**
15
18
  * Pick the border color for an issue.
16
19
  * Failed / passed states win over rotation; otherwise rotate by slot.
@@ -9,6 +9,9 @@
9
9
  export const BORDER_ROTATION = ["cyan", "magenta", "blue", "yellow"];
10
10
  /** Gray used for horizontal dividers inside each box. */
11
11
  export const DIVIDER_COLOR = "gray";
12
+ /** Green used for the rolled-up `✔ N done` summary line (#699, parity with the
13
+ * plain renderer's #624 rollup). */
14
+ export const ROLLUP_COLOR = "green";
12
15
  /**
13
16
  * Pick the border color for an issue.
14
17
  * Failed / passed states win over rotation; otherwise rotate by slot.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sequant",
3
- "version": "2.3.0",
4
- "description": "Quantize your development workflow - Sequential AI phases with quality gates",
3
+ "version": "2.5.0",
4
+ "description": "AI coding agent orchestrator resolve GitHub issues end-to-end with isolated git worktrees, quality gates, and an MCP server. Works with Claude Code or Aider.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "sequant": "dist/bin/cli.js"
@@ -26,6 +26,7 @@
26
26
  "test": "vitest run",
27
27
  "lint": "eslint src/ bin/ --max-warnings 0",
28
28
  "sync:skills": "cp -r templates/skills/* .claude/skills/",
29
+ "sync:hooks": "bash scripts/sync-hooks.sh",
29
30
  "validate:skills": "for skill in templates/skills/*/; do npx skills-ref validate \"$skill\"; done",
30
31
  "lint:skill-calls": "npx tsx scripts/lint-skill-calls.ts",
31
32
  "prepare:marketplace": "npx tsx scripts/prepare-marketplace.ts",
@@ -33,26 +34,37 @@
33
34
  "prepublishOnly": "npm run build"
34
35
  },
35
36
  "keywords": [
36
- "ai",
37
37
  "claude",
38
38
  "claude-code",
39
+ "claude-code-plugin",
39
40
  "aider",
40
41
  "mcp",
42
+ "mcp-server",
41
43
  "cli",
44
+ "headless",
45
+ "ci",
42
46
  "workflow",
43
47
  "automation",
48
+ "parallel",
44
49
  "coding-agent",
45
- "agent",
50
+ "ai-coding-agent",
51
+ "agent-orchestrator",
52
+ "autonomous-agent",
53
+ "agentic",
46
54
  "github",
47
55
  "github-issues",
56
+ "pull-request",
57
+ "issue-resolution",
58
+ "spec-driven",
59
+ "acceptance-criteria",
60
+ "human-in-the-loop",
61
+ "code-review",
48
62
  "quality-gates",
49
63
  "code-quality",
50
64
  "orchestrator",
65
+ "git-worktree",
51
66
  "developer-tools",
52
- "anthropic",
53
- "llm",
54
- "ai-coding",
55
- "copilot"
67
+ "ai-coding"
56
68
  ],
57
69
  "author": "Sequant Contributors",
58
70
  "license": "MIT",
@@ -65,7 +77,7 @@
65
77
  },
66
78
  "homepage": "https://sequant.io",
67
79
  "engines": {
68
- "node": ">=20.19.0"
80
+ "node": ">=22.12.0"
69
81
  },
70
82
  "peerDependencies": {
71
83
  "@modelcontextprotocol/sdk": "^1.27.1"
@@ -76,7 +88,7 @@
76
88
  }
77
89
  },
78
90
  "dependencies": {
79
- "@anthropic-ai/claude-agent-sdk": "^0.2.11",
91
+ "@anthropic-ai/claude-agent-sdk": "^0.3.142",
80
92
  "@hono/node-server": "^2.0.0",
81
93
  "boxen": "^8.0.1",
82
94
  "chalk": "^5.3.0",
@@ -87,7 +99,7 @@
87
99
  "gradient-string": "^3.0.0",
88
100
  "hono": "^4.12.1",
89
101
  "ink": "^7.0.1",
90
- "inquirer": "^13.3.0",
102
+ "inquirer": "^14.0.1",
91
103
  "log-update": "^7.0.1",
92
104
  "open": "^11.0.0",
93
105
  "ora": "^9.3.0",
@@ -227,6 +227,60 @@ if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE '(npm run build
227
227
  fi
228
228
  fi
229
229
 
230
+ # === TEST COVERAGE ANALYSIS (P3) ===
231
+ # Opt-in: Set CLAUDE_HOOKS_COVERAGE=true to enable
232
+ # Automatically appends coverage analysis to npm test output
233
+ # Logs which changed files have/don't have corresponding tests
234
+ if [[ "${CLAUDE_HOOKS_COVERAGE:-}" == "true" ]]; then
235
+ if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE '(npm (test|run test)|bun (test|run test)|yarn (test|run test)|pnpm (test|run test))'; then
236
+ # Only run if tests passed (don't clutter failure output)
237
+ if ! echo "$TOOL_OUTPUT" | grep -qE '(FAIL|failed|Error:)'; then
238
+ COVERAGE_LOG="${_LOG_DIR}/claude-coverage.log"
239
+
240
+ # Get changed source files (excluding tests)
241
+ changed_files=$(git diff main...HEAD --name-only 2>/dev/null | grep -E '\.(ts|tsx|js|jsx)$' | grep -v -E '\.test\.|\.spec\.|__tests__' || true)
242
+
243
+ if [[ -n "$changed_files" ]]; then
244
+ echo "$(date +%H:%M:%S) COVERAGE_ANALYSIS: Checking test coverage for changed files" >> "$QUALITY_LOG"
245
+
246
+ files_with_tests=0
247
+ files_without_tests=0
248
+ critical_without_tests=""
249
+
250
+ while IFS= read -r file; do
251
+ [[ -z "$file" ]] && continue
252
+ base=$(basename "$file" .ts | sed 's/\.tsx$//')
253
+
254
+ # Check for test file
255
+ if find . -name "${base}.test.*" -o -name "${base}.spec.*" 2>/dev/null | grep -q .; then
256
+ ((files_with_tests++))
257
+ else
258
+ ((files_without_tests++))
259
+ # Check if critical path
260
+ if echo "$file" | grep -qE 'auth|payment|security|server-action|middleware|admin'; then
261
+ critical_without_tests="$critical_without_tests $file"
262
+ fi
263
+ fi
264
+ done <<< "$changed_files"
265
+
266
+ total=$((files_with_tests + files_without_tests))
267
+
268
+ # Log coverage summary
269
+ echo "$(date +%H:%M:%S) COVERAGE: $files_with_tests/$total changed files have tests" >> "$COVERAGE_LOG"
270
+
271
+ if [[ -n "$critical_without_tests" ]]; then
272
+ echo "$(date +%H:%M:%S) ⚠️ CRITICAL_NO_TESTS:$critical_without_tests" >> "$COVERAGE_LOG"
273
+ echo "$(date +%H:%M:%S) CRITICAL_NO_TESTS:$critical_without_tests" >> "$QUALITY_LOG"
274
+ fi
275
+
276
+ if [[ $files_without_tests -gt 0 ]]; then
277
+ echo "$(date +%H:%M:%S) COVERAGE_GAP: $files_without_tests files without tests" >> "$QUALITY_LOG"
278
+ fi
279
+ fi
280
+ fi
281
+ fi
282
+ fi
283
+
230
284
  # === SMART TEST RUNNING (P3) ===
231
285
  # Opt-in: Set CLAUDE_HOOKS_SMART_TESTS=true to enable
232
286
  # Runs related tests asynchronously after file edits
@@ -311,4 +365,31 @@ if [[ -n "${CLAUDE_HOOKS_WEBHOOK_URL:-}" ]]; then
311
365
  fi
312
366
  fi
313
367
 
368
+ # === POST-MERGE WORKTREE CLEANUP ===
369
+ # Clean up worktree AFTER `gh pr merge` succeeds (not before).
370
+ # Previous approach removed worktree pre-merge, which lost work if merge failed.
371
+ if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'gh pr merge'; then
372
+ # Only clean up if merge succeeded (output contains merge confirmation)
373
+ if echo "$TOOL_OUTPUT" | grep -qiE '(merged|Merged pull request|Pull request .* merged)'; then
374
+ PR_NUM=$(echo "$TOOL_INPUT" | grep -oE 'gh pr merge [0-9]+' | grep -oE '[0-9]+')
375
+
376
+ if [[ -n "$PR_NUM" ]]; then
377
+ BRANCH_NAME=$(gh pr view "$PR_NUM" --json headRefName --jq '.headRefName' 2>/dev/null || true)
378
+
379
+ if [[ -n "$BRANCH_NAME" ]]; then
380
+ # Note: worktree line is 2 lines before branch line in porcelain output
381
+ WORKTREE_PATH=$(git worktree list --porcelain 2>/dev/null | grep -B2 "branch refs/heads/$BRANCH_NAME" | grep "^worktree " | sed 's/^worktree //' || true)
382
+
383
+ if [[ -n "$WORKTREE_PATH" && -d "$WORKTREE_PATH" ]]; then
384
+ git worktree remove "$WORKTREE_PATH" --force 2>/dev/null || true
385
+ echo "POST-MERGE: Removed worktree $WORKTREE_PATH for branch $BRANCH_NAME" >> "${_LOG_DIR}/claude-hook.log"
386
+ fi
387
+
388
+ # Clean up local branch
389
+ git branch -D "$BRANCH_NAME" 2>/dev/null || true
390
+ fi
391
+ fi
392
+ fi
393
+ fi
394
+
314
395
  exit 0