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.
Files changed (156) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +81 -5
  4. package/dist/bin/cli.js +140 -13
  5. package/dist/src/commands/abort.d.ts +36 -0
  6. package/dist/src/commands/abort.js +138 -0
  7. package/dist/src/commands/doctor.d.ts +25 -0
  8. package/dist/src/commands/doctor.js +36 -1
  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 +46 -0
  13. package/dist/src/commands/prompt.js +273 -0
  14. package/dist/src/commands/run-display.d.ts +11 -2
  15. package/dist/src/commands/run-display.js +62 -28
  16. package/dist/src/commands/run-progress.d.ts +42 -0
  17. package/dist/src/commands/run-progress.js +93 -0
  18. package/dist/src/commands/run.js +90 -18
  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 +12 -0
  22. package/dist/src/commands/watch.d.ts +18 -0
  23. package/dist/src/commands/watch.js +211 -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 +220 -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 +265 -0
  35. package/dist/src/lib/cli-ui/run-renderer.js +1390 -0
  36. package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
  37. package/dist/src/lib/cli-ui/scrollback-harness.js +294 -0
  38. package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
  39. package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
  40. package/dist/src/lib/locks/index.d.ts +7 -0
  41. package/dist/src/lib/locks/index.js +5 -0
  42. package/dist/src/lib/locks/lock-manager.d.ts +168 -0
  43. package/dist/src/lib/locks/lock-manager.js +433 -0
  44. package/dist/src/lib/locks/types.d.ts +59 -0
  45. package/dist/src/lib/locks/types.js +31 -0
  46. package/dist/src/lib/merge-check/types.js +1 -1
  47. package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
  48. package/dist/src/lib/qa/markdown-only-ci.js +74 -0
  49. package/dist/src/lib/relay/activation.d.ts +60 -0
  50. package/dist/src/lib/relay/activation.js +122 -0
  51. package/dist/src/lib/relay/archive.d.ts +34 -0
  52. package/dist/src/lib/relay/archive.js +112 -0
  53. package/dist/src/lib/relay/frame.d.ts +20 -0
  54. package/dist/src/lib/relay/frame.js +76 -0
  55. package/dist/src/lib/relay/index.d.ts +13 -0
  56. package/dist/src/lib/relay/index.js +13 -0
  57. package/dist/src/lib/relay/paths.d.ts +43 -0
  58. package/dist/src/lib/relay/paths.js +59 -0
  59. package/dist/src/lib/relay/pid.d.ts +34 -0
  60. package/dist/src/lib/relay/pid.js +72 -0
  61. package/dist/src/lib/relay/reader.d.ts +35 -0
  62. package/dist/src/lib/relay/reader.js +115 -0
  63. package/dist/src/lib/relay/types.d.ts +70 -0
  64. package/dist/src/lib/relay/types.js +85 -0
  65. package/dist/src/lib/relay/writer.d.ts +48 -0
  66. package/dist/src/lib/relay/writer.js +113 -0
  67. package/dist/src/lib/settings.d.ts +31 -1
  68. package/dist/src/lib/settings.js +18 -3
  69. package/dist/src/lib/version-check.d.ts +60 -5
  70. package/dist/src/lib/version-check.js +97 -9
  71. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  72. package/dist/src/lib/workflow/batch-executor.js +274 -185
  73. package/dist/src/lib/workflow/config-resolver.js +4 -0
  74. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
  75. package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
  76. package/dist/src/lib/workflow/drivers/aider.js +9 -0
  77. package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
  78. package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
  79. package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
  80. package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
  81. package/dist/src/lib/workflow/event-emitter.js +102 -0
  82. package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
  83. package/dist/src/lib/workflow/heartbeat.js +194 -0
  84. package/dist/src/lib/workflow/notice.d.ts +32 -0
  85. package/dist/src/lib/workflow/notice.js +38 -0
  86. package/dist/src/lib/workflow/phase-executor.d.ts +58 -16
  87. package/dist/src/lib/workflow/phase-executor.js +244 -130
  88. package/dist/src/lib/workflow/phase-mapper.d.ts +27 -13
  89. package/dist/src/lib/workflow/phase-mapper.js +70 -51
  90. package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
  91. package/dist/src/lib/workflow/phase-registry.js +233 -0
  92. package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
  93. package/dist/src/lib/workflow/platforms/github.js +20 -3
  94. package/dist/src/lib/workflow/pr-status.d.ts +18 -2
  95. package/dist/src/lib/workflow/pr-status.js +41 -9
  96. package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
  97. package/dist/src/lib/workflow/qa-stagnation.js +179 -0
  98. package/dist/src/lib/workflow/run-log-schema.d.ts +5 -55
  99. package/dist/src/lib/workflow/run-orchestrator.d.ts +70 -1
  100. package/dist/src/lib/workflow/run-orchestrator.js +464 -25
  101. package/dist/src/lib/workflow/run-reflect.js +1 -1
  102. package/dist/src/lib/workflow/run-state.d.ts +71 -0
  103. package/dist/src/lib/workflow/run-state.js +14 -0
  104. package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
  105. package/dist/src/lib/workflow/state-cleanup.js +17 -5
  106. package/dist/src/lib/workflow/state-manager.d.ts +31 -2
  107. package/dist/src/lib/workflow/state-manager.js +64 -1
  108. package/dist/src/lib/workflow/state-schema.d.ts +82 -35
  109. package/dist/src/lib/workflow/state-schema.js +63 -4
  110. package/dist/src/lib/workflow/types.d.ts +139 -16
  111. package/dist/src/lib/workflow/types.js +18 -13
  112. package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
  113. package/dist/src/lib/workflow/worktree-manager.js +15 -6
  114. package/dist/src/mcp/tools/run.d.ts +44 -0
  115. package/dist/src/mcp/tools/run.js +104 -13
  116. package/dist/src/ui/tui/App.d.ts +14 -0
  117. package/dist/src/ui/tui/App.js +41 -0
  118. package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
  119. package/dist/src/ui/tui/ElapsedTimer.js +31 -0
  120. package/dist/src/ui/tui/Header.d.ts +6 -0
  121. package/dist/src/ui/tui/Header.js +15 -0
  122. package/dist/src/ui/tui/IssueBox.d.ts +16 -0
  123. package/dist/src/ui/tui/IssueBox.js +68 -0
  124. package/dist/src/ui/tui/Spinner.d.ts +9 -0
  125. package/dist/src/ui/tui/Spinner.js +18 -0
  126. package/dist/src/ui/tui/index.d.ts +15 -0
  127. package/dist/src/ui/tui/index.js +29 -0
  128. package/dist/src/ui/tui/theme.d.ts +29 -0
  129. package/dist/src/ui/tui/theme.js +52 -0
  130. package/dist/src/ui/tui/truncate.d.ts +11 -0
  131. package/dist/src/ui/tui/truncate.js +31 -0
  132. package/package.json +14 -6
  133. package/templates/agents/sequant-explorer.md +1 -0
  134. package/templates/agents/sequant-qa-checker.md +2 -1
  135. package/templates/agents/sequant-testgen.md +1 -0
  136. package/templates/hooks/post-tool.sh +92 -0
  137. package/templates/hooks/pre-tool.sh +18 -9
  138. package/templates/hooks/relay-check.sh +107 -0
  139. package/templates/relay/frame.txt +11 -0
  140. package/templates/scripts/cleanup-worktree.sh +25 -3
  141. package/templates/scripts/new-feature.sh +6 -0
  142. package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
  143. package/templates/skills/_shared/references/subagent-types.md +21 -8
  144. package/templates/skills/assess/SKILL.md +122 -68
  145. package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
  146. package/templates/skills/docs/SKILL.md +141 -22
  147. package/templates/skills/exec/SKILL.md +10 -8
  148. package/templates/skills/fullsolve/SKILL.md +79 -5
  149. package/templates/skills/loop/SKILL.md +28 -0
  150. package/templates/skills/merger/SKILL.md +621 -0
  151. package/templates/skills/qa/SKILL.md +727 -8
  152. package/templates/skills/setup/SKILL.md +12 -6
  153. package/templates/skills/spec/SKILL.md +52 -0
  154. package/templates/skills/spec/references/parallel-groups.md +7 -0
  155. package/templates/skills/spec/references/recommended-workflow.md +4 -2
  156. package/templates/skills/testgen/SKILL.md +24 -17
@@ -0,0 +1,6 @@
1
+ import type { JSX } from "react";
2
+ import type { RunSnapshot } from "../../lib/workflow/run-state.js";
3
+ /** Top-of-dashboard summary: count, concurrency, base, quality-loop. */
4
+ export declare function Header({ snapshot }: {
5
+ snapshot: RunSnapshot;
6
+ }): JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { DIVIDER_COLOR } from "./theme.js";
4
+ /** Top-of-dashboard summary: count, concurrency, base, quality-loop. */
5
+ export function Header({ snapshot }) {
6
+ const { config, issues } = snapshot;
7
+ const concurrency = config.concurrency > 1
8
+ ? `parallel (${config.concurrency} concurrent)`
9
+ : "sequential";
10
+ const loop = config.qualityLoop ? "on" : "off";
11
+ const base = config.baseSha
12
+ ? `${config.baseBranch} @${config.baseSha.slice(0, 7)}`
13
+ : config.baseBranch;
14
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "sequant run" }), _jsxs(Text, { color: DIVIDER_COLOR, children: [" ─ ", issues.length, " issue", issues.length === 1 ? "" : "s", " • ", concurrency, " • ", "quality loop ", loop] })] }), _jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "base " }), _jsx(Text, { children: base })] })] }));
15
+ }
@@ -0,0 +1,16 @@
1
+ import type { JSX } from "react";
2
+ import type { IssueRuntimeState } from "../../lib/workflow/run-state.js";
3
+ /**
4
+ * Three-cell rendering of a single issue's runtime state.
5
+ *
6
+ * Cells:
7
+ * 1. header — id, title, phase N/total, elapsed
8
+ * 2. context — branch, phase progression, log path
9
+ * 3. activity — current `now` line, last-activity stamp
10
+ */
11
+ export declare function IssueBox({ state, slot, width, now, }: {
12
+ state: IssueRuntimeState;
13
+ slot: number;
14
+ width: number;
15
+ now: number;
16
+ }): JSX.Element;
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { DIVIDER_COLOR, PHASE_GLYPHS, borderColorForIssue, phaseStatusColor, } from "./theme.js";
4
+ import { Spinner } from "./Spinner.js";
5
+ import { ElapsedTimer, formatSinceActivity } from "./ElapsedTimer.js";
6
+ import { truncateToWidth } from "./truncate.js";
7
+ /**
8
+ * Three-cell rendering of a single issue's runtime state.
9
+ *
10
+ * Cells:
11
+ * 1. header — id, title, phase N/total, elapsed
12
+ * 2. context — branch, phase progression, log path
13
+ * 3. activity — current `now` line, last-activity stamp
14
+ */
15
+ export function IssueBox({ state, slot, width, now, }) {
16
+ const border = borderColorForIssue(state.status, slot);
17
+ const innerWidth = Math.max(10, width - 4);
18
+ const doneCount = state.phases.filter((p) => p.status === "done" || p.status === "failed").length;
19
+ const activePhaseIndex = state.phases.findIndex((p) => p.status === "running");
20
+ const displayPhaseN = activePhaseIndex >= 0 ? activePhaseIndex + 1 : doneCount;
21
+ const total = state.phases.length;
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) })) })] }));
24
+ }
25
+ function statusLine(state) {
26
+ switch (state.status) {
27
+ case "queued":
28
+ return "queued";
29
+ case "running":
30
+ return "working…";
31
+ case "passed":
32
+ return "completed";
33
+ case "failed":
34
+ return "failed";
35
+ }
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" })] }));
40
+ }
41
+ function PhaseProgression({ phases, borderColor, }) {
42
+ return (_jsxs(Box, { flexWrap: "wrap", children: [_jsx(Text, { color: DIVIDER_COLOR, children: "phases " }), phases.map((p, i) => {
43
+ const isLast = i === phases.length - 1;
44
+ return (_jsxs(Box, { children: [_jsx(PhaseGlyph, { status: p.status, label: p.name, activeColor: borderColor, elapsedMs: p.elapsedMs }), !isLast ? (_jsxs(Text, { color: DIVIDER_COLOR, children: [" ", PHASE_GLYPHS.separator, " "] })) : null] }, `${p.name}-${i}`));
45
+ })] }));
46
+ }
47
+ function PhaseGlyph({ status, label, activeColor, elapsedMs, }) {
48
+ if (status === "running") {
49
+ return (_jsxs(Box, { children: [_jsx(Spinner, { color: activeColor }), _jsxs(Text, { children: [" ", label] })] }));
50
+ }
51
+ const glyph = status === "done"
52
+ ? PHASE_GLYPHS.done
53
+ : status === "failed"
54
+ ? PHASE_GLYPHS.failed
55
+ : PHASE_GLYPHS.pending;
56
+ const glyphColor = phaseStatusColor(status);
57
+ return (_jsxs(Box, { children: [_jsx(Text, { color: glyphColor, children: glyph }), _jsxs(Text, { color: DIVIDER_COLOR, children: [" ", label, elapsedMs != null ? ` ${formatShortDuration(elapsedMs)}` : ""] })] }));
58
+ }
59
+ function formatShortDuration(ms) {
60
+ const secs = Math.max(0, Math.floor(ms / 1000));
61
+ if (secs < 60)
62
+ return `${secs.toString().padStart(2, "0")}s`;
63
+ const mm = Math.floor(secs / 60)
64
+ .toString()
65
+ .padStart(2, "0");
66
+ const ss = (secs % 60).toString().padStart(2, "0");
67
+ return `${mm}:${ss}`;
68
+ }
@@ -0,0 +1,9 @@
1
+ import { type JSX } from "react";
2
+ import { type BorderColor } from "./theme.js";
3
+ /**
4
+ * Braille spinner. Ticks at 10 Hz in its own component so the parent
5
+ * issue box is not forced to re-render on each frame.
6
+ */
7
+ export declare function Spinner({ color }: {
8
+ color?: BorderColor;
9
+ }): JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Text } from "ink";
4
+ import { SPINNER_FRAMES } from "./theme.js";
5
+ /**
6
+ * Braille spinner. Ticks at 10 Hz in its own component so the parent
7
+ * issue box is not forced to re-render on each frame.
8
+ */
9
+ export function Spinner({ color }) {
10
+ const [frame, setFrame] = useState(0);
11
+ useEffect(() => {
12
+ const id = setInterval(() => {
13
+ setFrame((f) => (f + 1) % SPINNER_FRAMES.length);
14
+ }, 100);
15
+ return () => clearInterval(id);
16
+ }, []);
17
+ return _jsx(Text, { color: color, children: SPINNER_FRAMES[frame] });
18
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Experimental multi-issue TUI entry point.
3
+ *
4
+ * Mounts an `ink` app that polls `RunOrchestrator.getSnapshot()` at 10 Hz.
5
+ * Unmounts when the orchestrator reports `done` so the shell returns
6
+ * cleanly. Only safe to call when `process.stdout.isTTY` is true.
7
+ */
8
+ import type { RunOrchestrator } from "../../lib/workflow/run-orchestrator.js";
9
+ export interface TuiHandle {
10
+ /** Promise that resolves when the TUI unmounts. */
11
+ done: Promise<void>;
12
+ /** Force-unmount (e.g., on SIGINT fallback). */
13
+ unmount: () => void;
14
+ }
15
+ export declare function renderTui(orchestrator: RunOrchestrator): TuiHandle;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Experimental multi-issue TUI entry point.
3
+ *
4
+ * Mounts an `ink` app that polls `RunOrchestrator.getSnapshot()` at 10 Hz.
5
+ * Unmounts when the orchestrator reports `done` so the shell returns
6
+ * cleanly. Only safe to call when `process.stdout.isTTY` is true.
7
+ */
8
+ import { createElement } from "react";
9
+ import { render } from "ink";
10
+ import { App } from "./App.js";
11
+ export function renderTui(orchestrator) {
12
+ let resolveDone;
13
+ const done = new Promise((resolve) => {
14
+ resolveDone = resolve;
15
+ });
16
+ const instance = render(createElement(App, {
17
+ getSnapshot: () => orchestrator.getSnapshot(),
18
+ onDone: () => {
19
+ instance.unmount();
20
+ },
21
+ }), { exitOnCtrlC: false });
22
+ instance.waitUntilExit().then(() => resolveDone());
23
+ return {
24
+ done,
25
+ unmount: () => {
26
+ instance.unmount();
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Theme tokens for the experimental multi-issue dashboard TUI.
3
+ *
4
+ * Palette rotates by start order (cyan → magenta → blue → yellow); issue
5
+ * status (failed / passed) overrides the rotation color where applicable.
6
+ * Respects `NO_COLOR` automatically via `ink`/`chalk`.
7
+ */
8
+ import type { IssueStatus, PhaseStatus } from "../../lib/workflow/run-state.js";
9
+ /** Border-color palette rotated by issue start order. */
10
+ export declare const BORDER_ROTATION: readonly ["cyan", "magenta", "blue", "yellow"];
11
+ export type BorderColor = (typeof BORDER_ROTATION)[number] | "green" | "red" | "gray";
12
+ /** Gray used for horizontal dividers inside each box. */
13
+ export declare const DIVIDER_COLOR: "gray";
14
+ /**
15
+ * Pick the border color for an issue.
16
+ * Failed / passed states win over rotation; otherwise rotate by slot.
17
+ */
18
+ export declare function borderColorForIssue(status: IssueStatus, slot: number): BorderColor;
19
+ /** Glyphs for the phase progression row. */
20
+ export declare const PHASE_GLYPHS: {
21
+ readonly pending: "○";
22
+ readonly done: "✓";
23
+ readonly failed: "✗";
24
+ readonly separator: "▸";
25
+ };
26
+ /** Braille spinner frames — 10 Hz rotation looks smooth at ~100ms tick. */
27
+ export declare const SPINNER_FRAMES: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
28
+ /** Color for a phase glyph based on its status. Active phase uses border color. */
29
+ export declare function phaseStatusColor(status: PhaseStatus): BorderColor;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Theme tokens for the experimental multi-issue dashboard TUI.
3
+ *
4
+ * Palette rotates by start order (cyan → magenta → blue → yellow); issue
5
+ * status (failed / passed) overrides the rotation color where applicable.
6
+ * Respects `NO_COLOR` automatically via `ink`/`chalk`.
7
+ */
8
+ /** Border-color palette rotated by issue start order. */
9
+ export const BORDER_ROTATION = ["cyan", "magenta", "blue", "yellow"];
10
+ /** Gray used for horizontal dividers inside each box. */
11
+ export const DIVIDER_COLOR = "gray";
12
+ /**
13
+ * Pick the border color for an issue.
14
+ * Failed / passed states win over rotation; otherwise rotate by slot.
15
+ */
16
+ export function borderColorForIssue(status, slot) {
17
+ if (status === "failed")
18
+ return "red";
19
+ if (status === "passed")
20
+ return "green";
21
+ const idx = ((slot % BORDER_ROTATION.length) + BORDER_ROTATION.length) %
22
+ BORDER_ROTATION.length;
23
+ return BORDER_ROTATION[idx];
24
+ }
25
+ /** Glyphs for the phase progression row. */
26
+ export const PHASE_GLYPHS = {
27
+ pending: "○",
28
+ done: "✓",
29
+ failed: "✗",
30
+ separator: "▸",
31
+ };
32
+ /** Braille spinner frames — 10 Hz rotation looks smooth at ~100ms tick. */
33
+ export const SPINNER_FRAMES = [
34
+ "⠋",
35
+ "⠙",
36
+ "⠹",
37
+ "⠸",
38
+ "⠼",
39
+ "⠴",
40
+ "⠦",
41
+ "⠧",
42
+ "⠇",
43
+ "⠏",
44
+ ];
45
+ /** Color for a phase glyph based on its status. Active phase uses border color. */
46
+ export function phaseStatusColor(status) {
47
+ if (status === "done")
48
+ return "green";
49
+ if (status === "failed")
50
+ return "red";
51
+ return "gray";
52
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Display-width-aware truncation for terminal output.
3
+ *
4
+ * Uses `string-width` so wide glyphs (CJK, emoji) and ANSI escapes are
5
+ * counted correctly. Cheaper than ink's own truncation in hot paths.
6
+ */
7
+ /**
8
+ * Truncate `text` so its visible width does not exceed `max` columns.
9
+ * If truncation happens, appends a single `…` (which itself counts as 1 col).
10
+ */
11
+ export declare function truncateToWidth(text: string, max: number): string;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Display-width-aware truncation for terminal output.
3
+ *
4
+ * Uses `string-width` so wide glyphs (CJK, emoji) and ANSI escapes are
5
+ * counted correctly. Cheaper than ink's own truncation in hot paths.
6
+ */
7
+ import stringWidth from "string-width";
8
+ /**
9
+ * Truncate `text` so its visible width does not exceed `max` columns.
10
+ * If truncation happens, appends a single `…` (which itself counts as 1 col).
11
+ */
12
+ export function truncateToWidth(text, max) {
13
+ if (max <= 0)
14
+ return "";
15
+ const width = stringWidth(text);
16
+ if (width <= max)
17
+ return text;
18
+ if (max === 1)
19
+ return "…";
20
+ let acc = "";
21
+ let accWidth = 0;
22
+ const budget = max - 1; // reserve one column for the ellipsis
23
+ for (const ch of text) {
24
+ const w = stringWidth(ch);
25
+ if (accWidth + w > budget)
26
+ break;
27
+ acc += ch;
28
+ accWidth += w;
29
+ }
30
+ return `${acc}…`;
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sequant",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "Quantize your development workflow - Sequential AI phases with quality gates",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,9 @@
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",
31
+ "lint:skill-calls": "npx tsx scripts/lint-skill-calls.ts",
30
32
  "prepare:marketplace": "npx tsx scripts/prepare-marketplace.ts",
31
33
  "validate:marketplace": "npx tsx scripts/prepare-marketplace.ts --validate-only",
32
34
  "prepublishOnly": "npm run build"
@@ -64,7 +66,7 @@
64
66
  },
65
67
  "homepage": "https://sequant.io",
66
68
  "engines": {
67
- "node": ">=20.19.0"
69
+ "node": ">=22.12.0"
68
70
  },
69
71
  "peerDependencies": {
70
72
  "@modelcontextprotocol/sdk": "^1.27.1"
@@ -75,20 +77,24 @@
75
77
  }
76
78
  },
77
79
  "dependencies": {
78
- "@anthropic-ai/claude-agent-sdk": "^0.2.11",
79
- "@hono/node-server": "^1.19.9",
80
+ "@anthropic-ai/claude-agent-sdk": "^0.3.142",
81
+ "@hono/node-server": "^2.0.0",
80
82
  "boxen": "^8.0.1",
81
83
  "chalk": "^5.3.0",
82
84
  "chokidar": "^5.0.0",
83
85
  "cli-table3": "^0.6.5",
84
86
  "commander": "^14.0.3",
85
- "diff": "^8.0.3",
87
+ "diff": "^9.0.0",
86
88
  "gradient-string": "^3.0.0",
87
89
  "hono": "^4.12.1",
88
- "inquirer": "^13.3.0",
90
+ "ink": "^7.0.1",
91
+ "inquirer": "^14.0.1",
92
+ "log-update": "^7.0.1",
89
93
  "open": "^11.0.0",
90
94
  "ora": "^9.3.0",
91
95
  "p-limit": "^7.3.0",
96
+ "react": "^19.2.5",
97
+ "string-width": "^8.2.0",
92
98
  "yaml": "^2.7.0",
93
99
  "zod": "^4.3.5"
94
100
  },
@@ -97,10 +103,12 @@
97
103
  "@types/gradient-string": "^1.1.6",
98
104
  "@types/inquirer": "^9.0.7",
99
105
  "@types/node": "^25.4.0",
106
+ "@types/react": "^19.2.14",
100
107
  "@typescript-eslint/eslint-plugin": "^8.58.0",
101
108
  "@typescript-eslint/parser": "^8.58.0",
102
109
  "eslint": "^10.1.0",
103
110
  "globals": "^17.0.0",
111
+ "ink-testing-library": "^4.0.0",
104
112
  "tsx": "^4.19.2",
105
113
  "typescript": "^6.0.2",
106
114
  "typescript-eslint": "^8.58.0",
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: sequant-explorer
3
3
  description: Codebase exploration agent for sequant /spec phase. Searches for existing patterns, components, database schemas, and file structures. Use when gathering context before planning a feature implementation.
4
+ # Note: per anthropics/claude-code#43869 this is currently a no-op; agent runs on parent's model
4
5
  model: haiku
5
6
  maxTurns: 15
6
7
  tools:
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  name: sequant-qa-checker
3
3
  description: Quality check agent for sequant /qa phase. Runs type safety, scope/size, security, and documentation checks on diffs. Use when spawned by the /qa skill to perform parallel or sequential quality checks.
4
- model: haiku
4
+ # Note: per anthropics/claude-code#43869 this is currently a no-op; agent runs on parent's model
5
+ model: sonnet
5
6
  permissionMode: bypassPermissions
6
7
  effort: low
7
8
  maxTurns: 15
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: sequant-testgen
3
3
  description: Test stub generator for sequant /testgen phase. Parses verification criteria from /spec comments and generates Jest/Vitest test stubs with Given/When/Then structure. Use when spawned by the /testgen skill.
4
+ # Note: per anthropics/claude-code#43869 this is currently a no-op; agent runs on parent's model
4
5
  model: haiku
5
6
  maxTurns: 25
6
7
  tools:
@@ -13,6 +13,17 @@ if [[ "${CLAUDE_HOOKS_DISABLED:-}" == "true" ]]; then
13
13
  exit 0
14
14
  fi
15
15
 
16
+ # === RELAY CHECK (#383) ===
17
+ # Sourced only when SEQUANT_RELAY=true. The check itself is a single env-var
18
+ # comparison when relay is disabled (default), so non-relay runs incur no cost.
19
+ if [[ "${SEQUANT_RELAY:-}" == "true" ]]; then
20
+ _RELAY_CHECK="$(dirname "${BASH_SOURCE[0]:-$0}")/relay-check.sh"
21
+ if [[ -f "${_RELAY_CHECK}" ]]; then
22
+ # shellcheck source=relay-check.sh disable=SC1091
23
+ source "${_RELAY_CHECK}" || true
24
+ fi
25
+ fi
26
+
16
27
  # === READ INPUT FROM STDIN ===
17
28
  # Claude Code passes tool data as JSON via stdin, not environment variables
18
29
  INPUT_JSON=$(cat)
@@ -216,6 +227,60 @@ if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE '(npm run build
216
227
  fi
217
228
  fi
218
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
+
219
284
  # === SMART TEST RUNNING (P3) ===
220
285
  # Opt-in: Set CLAUDE_HOOKS_SMART_TESTS=true to enable
221
286
  # Runs related tests asynchronously after file edits
@@ -300,4 +365,31 @@ if [[ -n "${CLAUDE_HOOKS_WEBHOOK_URL:-}" ]]; then
300
365
  fi
301
366
  fi
302
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
+
303
395
  exit 0
@@ -104,20 +104,23 @@ if echo "$TOOL_INPUT" | grep -qE '^(env|printenv|export)$'; then
104
104
  fi
105
105
 
106
106
  # Destructive system commands
107
- if echo "$TOOL_INPUT" | grep -qE 'sudo|rm -rf /|rm -rf ~|rm -rf \$HOME'; then
107
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#570)
108
+ if ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'sudo|rm -rf /|rm -rf ~|rm -rf \$HOME'; then
108
109
  echo "HOOK_BLOCKED: Destructive system command" | tee -a "$HOOK_LOG" >&2
109
110
  exit 2
110
111
  fi
111
112
 
112
113
  # Deployment (should never happen in issue automation)
113
- if echo "$TOOL_INPUT" | grep -qE 'vercel (deploy|--prod)|terraform (apply|destroy)|kubectl (apply|delete)'; then
114
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#570)
115
+ if ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'vercel (deploy|--prod)|terraform (apply|destroy)|kubectl (apply|delete)'; then
114
116
  echo "HOOK_BLOCKED: Deployment command" | tee -a "$HOOK_LOG" >&2
115
117
  exit 2
116
118
  fi
117
119
 
118
120
  # Force push
119
121
  # Pattern requires -f to be a standalone flag (not part of branch name like -fix)
120
- if echo "$TOOL_INPUT" | grep -qE 'git push.*(--force| -f($| ))'; then
122
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#564)
123
+ if ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'git push.*(--force| -f($| ))'; then
121
124
  echo "HOOK_BLOCKED: Force push" | tee -a "$HOOK_LOG" >&2
122
125
  exit 2
123
126
  fi
@@ -127,7 +130,8 @@ fi
127
130
  # - Unpushed commits on main/master
128
131
  # - Uncommitted changes (staged or unstaged)
129
132
  # - Unfinished merge in progress
130
- if echo "$TOOL_INPUT" | grep -qE 'git reset.*(--hard|origin)'; then
133
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#570)
134
+ if ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'git reset.*(--hard|origin)'; then
131
135
  CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
132
136
  BLOCK_REASONS=""
133
137
 
@@ -167,7 +171,8 @@ if echo "$TOOL_INPUT" | grep -qE 'git reset.*(--hard|origin)'; then
167
171
  fi
168
172
 
169
173
  # CI/CD triggers (automation shouldn't trigger more automation)
170
- if echo "$TOOL_INPUT" | grep -qE 'gh workflow run'; then
174
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#570)
175
+ if ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'gh workflow run'; then
171
176
  echo "HOOK_BLOCKED: Workflow trigger" | tee -a "$HOOK_LOG" >&2
172
177
  exit 2
173
178
  fi
@@ -225,7 +230,8 @@ check_sensitive_files() {
225
230
 
226
231
  if [[ "${CLAUDE_HOOKS_SECURITY:-true}" != "false" ]]; then
227
232
  # Security checks for git commit
228
- if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
233
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#564)
234
+ if [[ "$TOOL_NAME" == "Bash" ]] && ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
229
235
  # Skip security checks if --no-verify is used
230
236
  if ! echo "$TOOL_INPUT" | grep -qE -- '--no-verify'; then
231
237
  # Check staged files for secrets
@@ -257,7 +263,8 @@ fi
257
263
  # --- No-Changes Guard (AC-7) ---
258
264
  # Block commits when there are no staged or unstaged changes (prevents empty commits)
259
265
  # Skips for --amend since amending doesn't require new changes
260
- if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
266
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#564)
267
+ if [[ "$TOOL_NAME" == "Bash" ]] && ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
261
268
  if ! echo "$TOOL_INPUT" | grep -qE -- '--amend|--allow-empty'; then
262
269
  # Extract target directory from cd command if present (for worktree commits)
263
270
  # Handles: "cd /path && git commit" or "cd /path; git commit"
@@ -284,7 +291,8 @@ fi
284
291
  # Warn (but don't block) when committing outside a feature worktree
285
292
  # This catches accidental commits to main repo during feature work
286
293
  QUALITY_LOG="${_LOG_DIR}/claude-quality.log"
287
- if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
294
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#564)
295
+ if [[ "$TOOL_NAME" == "Bash" ]] && ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
288
296
  CWD=$(pwd)
289
297
  if ! echo "$CWD" | grep -qE 'worktrees/feature/'; then
290
298
  echo "$(date +%H:%M:%S) WORKTREE_WARNING: Committing outside feature worktree ($CWD)" >> "$QUALITY_LOG"
@@ -295,7 +303,8 @@ fi
295
303
  # --- Commit Message Validation (AC-3) ---
296
304
  # Enforce conventional commits format: type(scope): description
297
305
  # Types: feat|fix|docs|style|refactor|test|chore|ci|build|perf
298
- if [[ "$TOOL_NAME" == "Bash" ]] && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
306
+ # Skip for gh issue/pr commands body text may legitimately reference these tokens (#564)
307
+ if [[ "$TOOL_NAME" == "Bash" ]] && ! echo "$TOOL_INPUT" | grep -qE '^gh (issue|pr) ' && echo "$TOOL_INPUT" | grep -qE 'git commit'; then
299
308
  # Extract message from -m flag
300
309
  MSG=""
301
310