sequant 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +81 -5
- package/dist/bin/cli.js +140 -13
- package/dist/src/commands/abort.d.ts +36 -0
- package/dist/src/commands/abort.js +138 -0
- package/dist/src/commands/doctor.d.ts +25 -0
- package/dist/src/commands/doctor.js +36 -1
- package/dist/src/commands/locks.d.ts +67 -0
- package/dist/src/commands/locks.js +290 -0
- package/dist/src/commands/merge.js +11 -0
- package/dist/src/commands/prompt.d.ts +46 -0
- package/dist/src/commands/prompt.js +273 -0
- package/dist/src/commands/run-display.d.ts +11 -2
- package/dist/src/commands/run-display.js +62 -28
- package/dist/src/commands/run-progress.d.ts +42 -0
- package/dist/src/commands/run-progress.js +93 -0
- package/dist/src/commands/run.js +90 -18
- package/dist/src/commands/stats.d.ts +2 -0
- package/dist/src/commands/stats.js +94 -8
- package/dist/src/commands/status.js +12 -0
- package/dist/src/commands/watch.d.ts +18 -0
- package/dist/src/commands/watch.js +211 -0
- package/dist/src/lib/ac-linter.d.ts +1 -1
- package/dist/src/lib/ac-linter.js +81 -0
- package/dist/src/lib/assess-collision-detect.d.ts +91 -0
- package/dist/src/lib/assess-collision-detect.js +217 -0
- package/dist/src/lib/assess-comment-parser.d.ts +59 -1
- package/dist/src/lib/assess-comment-parser.js +124 -2
- package/dist/src/lib/cli-ui/format.d.ts +19 -0
- package/dist/src/lib/cli-ui/format.js +34 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +220 -0
- package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +265 -0
- package/dist/src/lib/cli-ui/run-renderer.js +1390 -0
- package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
- package/dist/src/lib/cli-ui/scrollback-harness.js +294 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
- package/dist/src/lib/locks/index.d.ts +7 -0
- package/dist/src/lib/locks/index.js +5 -0
- package/dist/src/lib/locks/lock-manager.d.ts +168 -0
- package/dist/src/lib/locks/lock-manager.js +433 -0
- package/dist/src/lib/locks/types.d.ts +59 -0
- package/dist/src/lib/locks/types.js +31 -0
- package/dist/src/lib/merge-check/types.js +1 -1
- package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
- package/dist/src/lib/qa/markdown-only-ci.js +74 -0
- package/dist/src/lib/relay/activation.d.ts +60 -0
- package/dist/src/lib/relay/activation.js +122 -0
- package/dist/src/lib/relay/archive.d.ts +34 -0
- package/dist/src/lib/relay/archive.js +112 -0
- package/dist/src/lib/relay/frame.d.ts +20 -0
- package/dist/src/lib/relay/frame.js +76 -0
- package/dist/src/lib/relay/index.d.ts +13 -0
- package/dist/src/lib/relay/index.js +13 -0
- package/dist/src/lib/relay/paths.d.ts +43 -0
- package/dist/src/lib/relay/paths.js +59 -0
- package/dist/src/lib/relay/pid.d.ts +34 -0
- package/dist/src/lib/relay/pid.js +72 -0
- package/dist/src/lib/relay/reader.d.ts +35 -0
- package/dist/src/lib/relay/reader.js +115 -0
- package/dist/src/lib/relay/types.d.ts +70 -0
- package/dist/src/lib/relay/types.js +85 -0
- package/dist/src/lib/relay/writer.d.ts +48 -0
- package/dist/src/lib/relay/writer.js +113 -0
- package/dist/src/lib/settings.d.ts +31 -1
- package/dist/src/lib/settings.js +18 -3
- package/dist/src/lib/version-check.d.ts +60 -5
- package/dist/src/lib/version-check.js +97 -9
- package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
- package/dist/src/lib/workflow/batch-executor.js +274 -185
- package/dist/src/lib/workflow/config-resolver.js +4 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
- package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
- package/dist/src/lib/workflow/drivers/aider.js +9 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
- package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
- package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
- package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
- package/dist/src/lib/workflow/event-emitter.js +102 -0
- package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
- package/dist/src/lib/workflow/heartbeat.js +194 -0
- package/dist/src/lib/workflow/notice.d.ts +32 -0
- package/dist/src/lib/workflow/notice.js +38 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +58 -16
- package/dist/src/lib/workflow/phase-executor.js +244 -130
- package/dist/src/lib/workflow/phase-mapper.d.ts +27 -13
- package/dist/src/lib/workflow/phase-mapper.js +70 -51
- package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
- package/dist/src/lib/workflow/phase-registry.js +233 -0
- package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
- package/dist/src/lib/workflow/platforms/github.js +20 -3
- package/dist/src/lib/workflow/pr-status.d.ts +18 -2
- package/dist/src/lib/workflow/pr-status.js +41 -9
- package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
- package/dist/src/lib/workflow/qa-stagnation.js +179 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts +5 -55
- package/dist/src/lib/workflow/run-orchestrator.d.ts +70 -1
- package/dist/src/lib/workflow/run-orchestrator.js +464 -25
- package/dist/src/lib/workflow/run-reflect.js +1 -1
- package/dist/src/lib/workflow/run-state.d.ts +71 -0
- package/dist/src/lib/workflow/run-state.js +14 -0
- package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
- package/dist/src/lib/workflow/state-cleanup.js +17 -5
- package/dist/src/lib/workflow/state-manager.d.ts +31 -2
- package/dist/src/lib/workflow/state-manager.js +64 -1
- package/dist/src/lib/workflow/state-schema.d.ts +82 -35
- package/dist/src/lib/workflow/state-schema.js +63 -4
- package/dist/src/lib/workflow/types.d.ts +139 -16
- package/dist/src/lib/workflow/types.js +18 -13
- package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
- package/dist/src/lib/workflow/worktree-manager.js +15 -6
- package/dist/src/mcp/tools/run.d.ts +44 -0
- package/dist/src/mcp/tools/run.js +104 -13
- package/dist/src/ui/tui/App.d.ts +14 -0
- package/dist/src/ui/tui/App.js +41 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
- package/dist/src/ui/tui/ElapsedTimer.js +31 -0
- package/dist/src/ui/tui/Header.d.ts +6 -0
- package/dist/src/ui/tui/Header.js +15 -0
- package/dist/src/ui/tui/IssueBox.d.ts +16 -0
- package/dist/src/ui/tui/IssueBox.js +68 -0
- package/dist/src/ui/tui/Spinner.d.ts +9 -0
- package/dist/src/ui/tui/Spinner.js +18 -0
- package/dist/src/ui/tui/index.d.ts +15 -0
- package/dist/src/ui/tui/index.js +29 -0
- package/dist/src/ui/tui/theme.d.ts +29 -0
- package/dist/src/ui/tui/theme.js +52 -0
- package/dist/src/ui/tui/truncate.d.ts +11 -0
- package/dist/src/ui/tui/truncate.js +31 -0
- package/package.json +14 -6
- package/templates/agents/sequant-explorer.md +1 -0
- package/templates/agents/sequant-qa-checker.md +2 -1
- package/templates/agents/sequant-testgen.md +1 -0
- package/templates/hooks/post-tool.sh +92 -0
- package/templates/hooks/pre-tool.sh +18 -9
- package/templates/hooks/relay-check.sh +107 -0
- package/templates/relay/frame.txt +11 -0
- package/templates/scripts/cleanup-worktree.sh +25 -3
- package/templates/scripts/new-feature.sh +6 -0
- package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
- package/templates/skills/_shared/references/subagent-types.md +21 -8
- package/templates/skills/assess/SKILL.md +122 -68
- package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
- package/templates/skills/docs/SKILL.md +141 -22
- package/templates/skills/exec/SKILL.md +10 -8
- package/templates/skills/fullsolve/SKILL.md +79 -5
- package/templates/skills/loop/SKILL.md +28 -0
- package/templates/skills/merger/SKILL.md +621 -0
- package/templates/skills/qa/SKILL.md +727 -8
- package/templates/skills/setup/SKILL.md +12 -6
- package/templates/skills/spec/SKILL.md +52 -0
- package/templates/skills/spec/references/parallel-groups.md +7 -0
- package/templates/skills/spec/references/recommended-workflow.md +4 -2
- package/templates/skills/testgen/SKILL.md +24 -17
|
@@ -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.
|
|
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": ">=
|
|
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.
|
|
79
|
-
"@hono/node-server": "^
|
|
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": "^
|
|
87
|
+
"diff": "^9.0.0",
|
|
86
88
|
"gradient-string": "^3.0.0",
|
|
87
89
|
"hono": "^4.12.1",
|
|
88
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|