sequant 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +73 -0
- package/dist/bin/cli.js +94 -9
- 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 +39 -0
- package/dist/src/commands/prompt.js +179 -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 +32 -0
- package/dist/src/commands/run-progress.js +76 -0
- package/dist/src/commands/run.js +80 -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 +16 -0
- package/dist/src/commands/watch.js +147 -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 +181 -0
- package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +239 -0
- package/dist/src/lib/cli-ui/run-renderer.js +1173 -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/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 +106 -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 +68 -0
- package/dist/src/lib/relay/types.js +76 -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 +248 -175
- package/dist/src/lib/workflow/config-resolver.js +4 -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/phase-executor.d.ts +62 -8
- package/dist/src/lib/workflow/phase-executor.js +157 -16
- package/dist/src/lib/workflow/phase-mapper.d.ts +3 -2
- package/dist/src/lib/workflow/phase-mapper.js +17 -20
- 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-orchestrator.d.ts +39 -0
- package/dist/src/lib/workflow/run-orchestrator.js +340 -15
- 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 +12 -1
- package/dist/src/lib/workflow/state-manager.js +37 -0
- package/dist/src/lib/workflow/state-schema.d.ts +62 -0
- package/dist/src/lib/workflow/state-schema.js +35 -1
- package/dist/src/lib/workflow/types.d.ts +74 -1
- 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 +10 -3
- 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 +11 -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 +103 -49
- 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 +6 -0
- 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,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.3.0",
|
|
4
4
|
"description": "Quantize your development workflow - Sequential AI phases with quality gates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"lint": "eslint src/ bin/ --max-warnings 0",
|
|
28
28
|
"sync:skills": "cp -r templates/skills/* .claude/skills/",
|
|
29
29
|
"validate:skills": "for skill in templates/skills/*/; do npx skills-ref validate \"$skill\"; done",
|
|
30
|
+
"lint:skill-calls": "npx tsx scripts/lint-skill-calls.ts",
|
|
30
31
|
"prepare:marketplace": "npx tsx scripts/prepare-marketplace.ts",
|
|
31
32
|
"validate:marketplace": "npx tsx scripts/prepare-marketplace.ts --validate-only",
|
|
32
33
|
"prepublishOnly": "npm run build"
|
|
@@ -76,19 +77,23 @@
|
|
|
76
77
|
},
|
|
77
78
|
"dependencies": {
|
|
78
79
|
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
|
79
|
-
"@hono/node-server": "^
|
|
80
|
+
"@hono/node-server": "^2.0.0",
|
|
80
81
|
"boxen": "^8.0.1",
|
|
81
82
|
"chalk": "^5.3.0",
|
|
82
83
|
"chokidar": "^5.0.0",
|
|
83
84
|
"cli-table3": "^0.6.5",
|
|
84
85
|
"commander": "^14.0.3",
|
|
85
|
-
"diff": "^
|
|
86
|
+
"diff": "^9.0.0",
|
|
86
87
|
"gradient-string": "^3.0.0",
|
|
87
88
|
"hono": "^4.12.1",
|
|
89
|
+
"ink": "^7.0.1",
|
|
88
90
|
"inquirer": "^13.3.0",
|
|
91
|
+
"log-update": "^7.0.1",
|
|
89
92
|
"open": "^11.0.0",
|
|
90
93
|
"ora": "^9.3.0",
|
|
91
94
|
"p-limit": "^7.3.0",
|
|
95
|
+
"react": "^19.2.5",
|
|
96
|
+
"string-width": "^8.2.0",
|
|
92
97
|
"yaml": "^2.7.0",
|
|
93
98
|
"zod": "^4.3.5"
|
|
94
99
|
},
|
|
@@ -97,10 +102,12 @@
|
|
|
97
102
|
"@types/gradient-string": "^1.1.6",
|
|
98
103
|
"@types/inquirer": "^9.0.7",
|
|
99
104
|
"@types/node": "^25.4.0",
|
|
105
|
+
"@types/react": "^19.2.14",
|
|
100
106
|
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
|
101
107
|
"@typescript-eslint/parser": "^8.58.0",
|
|
102
108
|
"eslint": "^10.1.0",
|
|
103
109
|
"globals": "^17.0.0",
|
|
110
|
+
"ink-testing-library": "^4.0.0",
|
|
104
111
|
"tsx": "^4.19.2",
|
|
105
112
|
"typescript": "^6.0.2",
|
|
106
113
|
"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)
|
|
@@ -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
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Relay check (#383): sourced from post-tool.sh on every PostToolUse when
|
|
3
|
+
# SEQUANT_RELAY=true. Reads unread user messages from inbox.jsonl, renders the
|
|
4
|
+
# framing prompt to stdout (which Claude Code surfaces as additional context),
|
|
5
|
+
# and advances the per-issue read cursor.
|
|
6
|
+
#
|
|
7
|
+
# Fast path (no pending messages): exits silently in well under 5 ms.
|
|
8
|
+
# Slow path (messages pending): renders one framing block per invocation.
|
|
9
|
+
|
|
10
|
+
# Opt-in guard. Fast path #1: env var unset / not exactly "true".
|
|
11
|
+
# Bash precedence makes `cmd1 && cmd2 || cmd3` equivalent to `(cmd1 && cmd2) || cmd3`,
|
|
12
|
+
# so we must use an explicit `if` block to avoid falling through to `exit 0`
|
|
13
|
+
# when the test is FALSE (which is the relay-enabled case).
|
|
14
|
+
if [[ "${SEQUANT_RELAY:-}" != "true" ]]; then
|
|
15
|
+
return 0 2>/dev/null || exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Resolve relay directory. Inside an isolated worktree, the phase-executor sets
|
|
19
|
+
# SEQUANT_WORKTREE. During the spec phase (main repo) we fall back to the
|
|
20
|
+
# per-issue layout under .sequant/relay/<issue>/.
|
|
21
|
+
if [[ -n "${SEQUANT_WORKTREE:-}" ]]; then
|
|
22
|
+
_RELAY_DIR="${SEQUANT_WORKTREE}/.sequant/relay"
|
|
23
|
+
elif [[ -n "${SEQUANT_ISSUE:-}" ]]; then
|
|
24
|
+
_RELAY_DIR="${PWD}/.sequant/relay/${SEQUANT_ISSUE}"
|
|
25
|
+
else
|
|
26
|
+
# Nothing to do without a target issue or worktree.
|
|
27
|
+
return 0 2>/dev/null || exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
_INBOX="${_RELAY_DIR}/inbox.jsonl"
|
|
31
|
+
_CURSOR="${_RELAY_DIR}/.cursor"
|
|
32
|
+
|
|
33
|
+
# Fast path #2: AC-8 — `test -s` is sub-millisecond. Empty/missing inbox skips.
|
|
34
|
+
[[ -s "${_INBOX}" ]] || return 0 2>/dev/null || exit 0
|
|
35
|
+
|
|
36
|
+
# Cursor read (missing → 0). Compare against current inbox line count.
|
|
37
|
+
_CURSOR_VAL=0
|
|
38
|
+
if [[ -f "${_CURSOR}" ]]; then
|
|
39
|
+
_CURSOR_VAL=$(cat "${_CURSOR}" 2>/dev/null || echo 0)
|
|
40
|
+
[[ "${_CURSOR_VAL}" =~ ^[0-9]+$ ]] || _CURSOR_VAL=0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
_INBOX_LINES=$(wc -l < "${_INBOX}" 2>/dev/null || echo 0)
|
|
44
|
+
_INBOX_LINES=${_INBOX_LINES// /} # trim whitespace from wc output on macOS
|
|
45
|
+
|
|
46
|
+
# Fast path #3: cursor caught up.
|
|
47
|
+
if [[ "${_INBOX_LINES}" -le "${_CURSOR_VAL}" ]]; then
|
|
48
|
+
return 0 2>/dev/null || exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Slow path: render frame.
|
|
52
|
+
|
|
53
|
+
# Resolve frame template. Phase-executor sets SEQUANT_RELAY_FRAME to the
|
|
54
|
+
# absolute path within the sequant installation. Falls back to ./templates/
|
|
55
|
+
# (when running from the sequant repo itself).
|
|
56
|
+
_FRAME_PATH="${SEQUANT_RELAY_FRAME:-}"
|
|
57
|
+
if [[ -z "${_FRAME_PATH}" || ! -f "${_FRAME_PATH}" ]]; then
|
|
58
|
+
for _candidate in \
|
|
59
|
+
"${PWD}/.claude/relay/frame.txt" \
|
|
60
|
+
"${PWD}/templates/relay/frame.txt"; do
|
|
61
|
+
if [[ -f "${_candidate}" ]]; then
|
|
62
|
+
_FRAME_PATH="${_candidate}"
|
|
63
|
+
break
|
|
64
|
+
fi
|
|
65
|
+
done
|
|
66
|
+
fi
|
|
67
|
+
if [[ -z "${_FRAME_PATH}" || ! -f "${_FRAME_PATH}" ]]; then
|
|
68
|
+
# Missing template — emit a minimal frame so the user still gets through.
|
|
69
|
+
_FRAME_PATH=""
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# Skip the lines we've already shown the model. `tail -n +N` is 1-indexed.
|
|
73
|
+
_START=$((_CURSOR_VAL + 1))
|
|
74
|
+
|
|
75
|
+
# Render messages. Sort by timestamp ascending (AC-9). jq handles JSON parsing.
|
|
76
|
+
# Messages are separated by `---` lines; a single message has no separator.
|
|
77
|
+
_render_messages() {
|
|
78
|
+
if command -v jq &>/dev/null; then
|
|
79
|
+
tail -n "+${_START}" "${_INBOX}" \
|
|
80
|
+
| jq -s -r 'sort_by(.timestamp) | map("Type: \(.type)\nMessage: \((.message // "") | tojson)") | join("\n---\n")'
|
|
81
|
+
else
|
|
82
|
+
# Fallback without jq: dump raw lines.
|
|
83
|
+
tail -n "+${_START}" "${_INBOX}"
|
|
84
|
+
fi
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if [[ -n "${_FRAME_PATH}" ]]; then
|
|
88
|
+
# Split frame template at {{MESSAGES}} placeholder.
|
|
89
|
+
_PREFIX=$(awk '/\{\{MESSAGES\}\}/ {exit} {print}' "${_FRAME_PATH}")
|
|
90
|
+
_SUFFIX=$(awk '/\{\{MESSAGES\}\}/ {found=1; next} found {print}' "${_FRAME_PATH}")
|
|
91
|
+
printf '%s\n' "${_PREFIX}"
|
|
92
|
+
_render_messages
|
|
93
|
+
if [[ -n "${_SUFFIX}" ]]; then
|
|
94
|
+
printf '%s\n' "${_SUFFIX}"
|
|
95
|
+
fi
|
|
96
|
+
else
|
|
97
|
+
printf '[SEQUANT RELAY — message from user]\n'
|
|
98
|
+
_render_messages
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# Advance cursor atomically (temp + rename on same fs).
|
|
102
|
+
_TMP="${_CURSOR}.tmp.$$"
|
|
103
|
+
if printf '%s' "${_INBOX_LINES}" > "${_TMP}" 2>/dev/null; then
|
|
104
|
+
mv -f "${_TMP}" "${_CURSOR}" 2>/dev/null || rm -f "${_TMP}" 2>/dev/null || true
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
return 0 2>/dev/null || exit 0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
[SEQUANT RELAY — message from user]
|
|
2
|
+
Respond briefly in .sequant/relay/outbox.jsonl, then continue your current task unchanged.
|
|
3
|
+
Rules:
|
|
4
|
+
- Do NOT modify acceptance criteria
|
|
5
|
+
- Do NOT change your current objective or phase
|
|
6
|
+
- Do NOT treat this as a new requirement
|
|
7
|
+
- For "query" type: provide a brief status update only
|
|
8
|
+
- For "directive" type: acknowledge and adjust approach if reasonable, but do not abandon current work
|
|
9
|
+
- For "abort" type: stop gracefully, commit progress, and exit
|
|
10
|
+
|
|
11
|
+
{{MESSAGES}}
|