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,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-issue PID tracking for relay liveness checks (AC-20/21/22).
|
|
3
|
+
*
|
|
4
|
+
* Reuses `defaultIsPidAlive` from `LockManager` — no duplicate `kill(pid, 0)`
|
|
5
|
+
* implementations (AC-21).
|
|
6
|
+
*/
|
|
7
|
+
/** Re-export so callers don't need to import from `locks` directly. */
|
|
8
|
+
export { defaultIsPidAlive as isPidAlive } from "../locks/lock-manager.js";
|
|
9
|
+
/** Write `<cwd>/.sequant/pids/<issue>.pid` containing the current PID. */
|
|
10
|
+
export declare function writePidFile(issue: number, pid?: number, cwd?: string): string;
|
|
11
|
+
/** Read the PID stored in the pidfile. Returns `null` if missing/unparseable. */
|
|
12
|
+
export declare function readPidFile(issue: number, cwd?: string): number | null;
|
|
13
|
+
/** Remove the pidfile for an issue if it exists. */
|
|
14
|
+
export declare function removePidFile(issue: number, cwd?: string): boolean;
|
|
15
|
+
export interface StaleCleanupResult {
|
|
16
|
+
/** Was a stale pidfile removed? */
|
|
17
|
+
cleaned: boolean;
|
|
18
|
+
/** Is the run still alive? */
|
|
19
|
+
alive: boolean;
|
|
20
|
+
/** PID we observed, if any. */
|
|
21
|
+
pid: number | null;
|
|
22
|
+
/** Human warning when a stale entry was cleared. */
|
|
23
|
+
warning: string | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Inspect the pidfile and clean it up if the PID is dead.
|
|
27
|
+
*
|
|
28
|
+
* Returns a structured result so callers can decide what to do (e.g.
|
|
29
|
+
* `sequant prompt` refuses to send to a dead run; warn the user).
|
|
30
|
+
*/
|
|
31
|
+
export declare function cleanupStalePid(issue: number, options?: {
|
|
32
|
+
cwd?: string;
|
|
33
|
+
isAlive?: (pid: number) => boolean;
|
|
34
|
+
}): StaleCleanupResult;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-issue PID tracking for relay liveness checks (AC-20/21/22).
|
|
3
|
+
*
|
|
4
|
+
* Reuses `defaultIsPidAlive` from `LockManager` — no duplicate `kill(pid, 0)`
|
|
5
|
+
* implementations (AC-21).
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "fs";
|
|
8
|
+
import { dirname } from "path";
|
|
9
|
+
import { defaultIsPidAlive } from "../locks/lock-manager.js";
|
|
10
|
+
import { pidPathFor } from "./paths.js";
|
|
11
|
+
/** Re-export so callers don't need to import from `locks` directly. */
|
|
12
|
+
export { defaultIsPidAlive as isPidAlive } from "../locks/lock-manager.js";
|
|
13
|
+
/** Write `<cwd>/.sequant/pids/<issue>.pid` containing the current PID. */
|
|
14
|
+
export function writePidFile(issue, pid = process.pid, cwd = process.cwd()) {
|
|
15
|
+
const path = pidPathFor(issue, cwd);
|
|
16
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
17
|
+
writeFileSync(path, String(pid), "utf-8");
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
20
|
+
/** Read the PID stored in the pidfile. Returns `null` if missing/unparseable. */
|
|
21
|
+
export function readPidFile(issue, cwd = process.cwd()) {
|
|
22
|
+
const path = pidPathFor(issue, cwd);
|
|
23
|
+
if (!existsSync(path))
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const raw = readFileSync(path, "utf-8").trim();
|
|
27
|
+
const pid = Number.parseInt(raw, 10);
|
|
28
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
29
|
+
return null;
|
|
30
|
+
return pid;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Remove the pidfile for an issue if it exists. */
|
|
37
|
+
export function removePidFile(issue, cwd = process.cwd()) {
|
|
38
|
+
const path = pidPathFor(issue, cwd);
|
|
39
|
+
if (!existsSync(path))
|
|
40
|
+
return false;
|
|
41
|
+
try {
|
|
42
|
+
unlinkSync(path);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Inspect the pidfile and clean it up if the PID is dead.
|
|
51
|
+
*
|
|
52
|
+
* Returns a structured result so callers can decide what to do (e.g.
|
|
53
|
+
* `sequant prompt` refuses to send to a dead run; warn the user).
|
|
54
|
+
*/
|
|
55
|
+
export function cleanupStalePid(issue, options = {}) {
|
|
56
|
+
const cwd = options.cwd ?? process.cwd();
|
|
57
|
+
const alive = options.isAlive ?? defaultIsPidAlive;
|
|
58
|
+
const pid = readPidFile(issue, cwd);
|
|
59
|
+
if (pid === null) {
|
|
60
|
+
return { cleaned: false, alive: false, pid: null, warning: null };
|
|
61
|
+
}
|
|
62
|
+
if (alive(pid)) {
|
|
63
|
+
return { cleaned: false, alive: true, pid, warning: null };
|
|
64
|
+
}
|
|
65
|
+
removePidFile(issue, cwd);
|
|
66
|
+
return {
|
|
67
|
+
cleaned: true,
|
|
68
|
+
alive: false,
|
|
69
|
+
pid,
|
|
70
|
+
warning: `Run for #${issue} is no longer active (process exited). Message not sent.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for the relay inbox. Tracks the last-read line via a `.cursor` file
|
|
3
|
+
* so each PostToolUse hook invocation only sees new messages (AC-11).
|
|
4
|
+
*
|
|
5
|
+
* Atomic cursor update: write to a temp file in the same directory, then
|
|
6
|
+
* rename. A crash mid-write never leaves a half-written cursor.
|
|
7
|
+
*/
|
|
8
|
+
import { type RelayMessage } from "./types.js";
|
|
9
|
+
import { type RelayPathOptions } from "./paths.js";
|
|
10
|
+
export interface ReadResult {
|
|
11
|
+
/** Newly read inbox messages, ordered by file appearance (timestamp). */
|
|
12
|
+
messages: RelayMessage[];
|
|
13
|
+
/** Cursor value after this read. */
|
|
14
|
+
cursor: number;
|
|
15
|
+
/** Total line count of inbox.jsonl after the read. */
|
|
16
|
+
inboxLineCount: number;
|
|
17
|
+
/** Malformed lines that were skipped (for logging). */
|
|
18
|
+
skipped: number;
|
|
19
|
+
}
|
|
20
|
+
/** Read the persisted cursor; missing/unparseable → 0 (AC-11 edge case). */
|
|
21
|
+
export declare function readCursor(issue: number, options?: RelayPathOptions): number;
|
|
22
|
+
/** Write the cursor atomically (temp file + rename). */
|
|
23
|
+
export declare function writeCursor(issue: number, value: number, options?: RelayPathOptions): void;
|
|
24
|
+
/**
|
|
25
|
+
* Read unread inbox messages and advance the cursor.
|
|
26
|
+
*
|
|
27
|
+
* - Missing or empty inbox → empty result (fast path).
|
|
28
|
+
* - Malformed JSON lines are logged via the `onMalformed` callback (if any)
|
|
29
|
+
* and skipped; the cursor still advances past them so we don't loop.
|
|
30
|
+
* - If the cursor points past EOF (inbox was truncated/rotated), reset to the
|
|
31
|
+
* current line count and return no messages.
|
|
32
|
+
*/
|
|
33
|
+
export declare function readUnreadMessages(issue: number, options?: RelayPathOptions & {
|
|
34
|
+
onMalformed?: (line: string, index: number) => void;
|
|
35
|
+
}): ReadResult;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for the relay inbox. Tracks the last-read line via a `.cursor` file
|
|
3
|
+
* so each PostToolUse hook invocation only sees new messages (AC-11).
|
|
4
|
+
*
|
|
5
|
+
* Atomic cursor update: write to a temp file in the same directory, then
|
|
6
|
+
* rename. A crash mid-write never leaves a half-written cursor.
|
|
7
|
+
*/
|
|
8
|
+
import { randomBytes } from "crypto";
|
|
9
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeSync, } from "fs";
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
import { RelayMessageSchema } from "./types.js";
|
|
12
|
+
import { cursorPathFor, inboxPathFor } from "./paths.js";
|
|
13
|
+
/** Read the persisted cursor; missing/unparseable → 0 (AC-11 edge case). */
|
|
14
|
+
export function readCursor(issue, options = {}) {
|
|
15
|
+
const path = cursorPathFor(issue, options);
|
|
16
|
+
if (!existsSync(path))
|
|
17
|
+
return 0;
|
|
18
|
+
try {
|
|
19
|
+
const raw = readFileSync(path, "utf-8").trim();
|
|
20
|
+
const n = Number.parseInt(raw, 10);
|
|
21
|
+
if (!Number.isInteger(n) || n < 0)
|
|
22
|
+
return 0;
|
|
23
|
+
return n;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Write the cursor atomically (temp file + rename). */
|
|
30
|
+
export function writeCursor(issue, value, options = {}) {
|
|
31
|
+
const path = cursorPathFor(issue, options);
|
|
32
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
33
|
+
const tmp = join(dirname(path), `.cursor.${process.pid}.${Date.now()}.${randomBytes(2).toString("hex")}.tmp`);
|
|
34
|
+
try {
|
|
35
|
+
const fd = openSync(tmp, "w");
|
|
36
|
+
try {
|
|
37
|
+
writeSync(fd, String(value));
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
closeSync(fd);
|
|
41
|
+
}
|
|
42
|
+
renameSync(tmp, path);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
if (existsSync(tmp)) {
|
|
46
|
+
try {
|
|
47
|
+
unlinkSync(tmp);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* swallow */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read unread inbox messages and advance the cursor.
|
|
58
|
+
*
|
|
59
|
+
* - Missing or empty inbox → empty result (fast path).
|
|
60
|
+
* - Malformed JSON lines are logged via the `onMalformed` callback (if any)
|
|
61
|
+
* and skipped; the cursor still advances past them so we don't loop.
|
|
62
|
+
* - If the cursor points past EOF (inbox was truncated/rotated), reset to the
|
|
63
|
+
* current line count and return no messages.
|
|
64
|
+
*/
|
|
65
|
+
export function readUnreadMessages(issue, options = {}) {
|
|
66
|
+
const inboxPath = inboxPathFor(issue, options);
|
|
67
|
+
if (!existsSync(inboxPath)) {
|
|
68
|
+
return { messages: [], cursor: 0, inboxLineCount: 0, skipped: 0 };
|
|
69
|
+
}
|
|
70
|
+
const st = statSync(inboxPath);
|
|
71
|
+
if (st.size === 0) {
|
|
72
|
+
return { messages: [], cursor: 0, inboxLineCount: 0, skipped: 0 };
|
|
73
|
+
}
|
|
74
|
+
const text = readFileSync(inboxPath, "utf-8");
|
|
75
|
+
const lines = text.split("\n").filter((l, idx, arr) => {
|
|
76
|
+
// Keep all but the final empty element from a trailing newline.
|
|
77
|
+
return !(idx === arr.length - 1 && l === "");
|
|
78
|
+
});
|
|
79
|
+
const totalLines = lines.length;
|
|
80
|
+
let cursor = readCursor(issue, options);
|
|
81
|
+
// If cursor is past EOF (file was rotated/truncated), reset to current end.
|
|
82
|
+
if (cursor > totalLines) {
|
|
83
|
+
writeCursor(issue, totalLines, options);
|
|
84
|
+
return {
|
|
85
|
+
messages: [],
|
|
86
|
+
cursor: totalLines,
|
|
87
|
+
inboxLineCount: totalLines,
|
|
88
|
+
skipped: 0,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const messages = [];
|
|
92
|
+
let skipped = 0;
|
|
93
|
+
for (let i = cursor; i < totalLines; i++) {
|
|
94
|
+
const raw = lines[i];
|
|
95
|
+
if (raw.trim() === "")
|
|
96
|
+
continue;
|
|
97
|
+
try {
|
|
98
|
+
const obj = JSON.parse(raw);
|
|
99
|
+
const parsed = RelayMessageSchema.safeParse(obj);
|
|
100
|
+
if (!parsed.success) {
|
|
101
|
+
skipped++;
|
|
102
|
+
options.onMalformed?.(raw, i);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
messages.push(parsed.data);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
skipped++;
|
|
109
|
+
options.onMalformed?.(raw, i);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
cursor = totalLines;
|
|
113
|
+
writeCursor(issue, cursor, options);
|
|
114
|
+
return { messages, cursor, inboxLineCount: totalLines, skipped };
|
|
115
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions and Zod schemas for the interactive relay (#383).
|
|
3
|
+
*
|
|
4
|
+
* The relay is a file-based IPC channel that allows a user terminal to send
|
|
5
|
+
* messages into a running headless Claude session. Messages flow through two
|
|
6
|
+
* JSONL files in `<worktree>/.sequant/relay/`:
|
|
7
|
+
*
|
|
8
|
+
* - `inbox.jsonl`: user → Claude (consumed by the PostToolUse hook)
|
|
9
|
+
* - `outbox.jsonl`: Claude → user (tailed by `sequant watch`)
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
/** Maximum size (bytes) of a single relay message body. */
|
|
13
|
+
export declare const MAX_MESSAGE_BYTES: number;
|
|
14
|
+
/** Relay message types. `query` asks for status, `directive` nudges behavior, `abort` stops. */
|
|
15
|
+
export declare const RelayMessageTypeSchema: z.ZodEnum<{
|
|
16
|
+
abort: "abort";
|
|
17
|
+
query: "query";
|
|
18
|
+
directive: "directive";
|
|
19
|
+
}>;
|
|
20
|
+
export type RelayMessageType = z.infer<typeof RelayMessageTypeSchema>;
|
|
21
|
+
/** Inbox message id format: `msg_<hex>`. */
|
|
22
|
+
export declare const MESSAGE_ID_PATTERN: RegExp;
|
|
23
|
+
/** Outbox reply id format: `reply_<hex>`. */
|
|
24
|
+
export declare const REPLY_ID_PATTERN: RegExp;
|
|
25
|
+
/**
|
|
26
|
+
* Discriminated union over `type`. `query` and `directive` require a non-empty
|
|
27
|
+
* `message` body; `abort` allows an optional explanatory message but it is not
|
|
28
|
+
* required.
|
|
29
|
+
*/
|
|
30
|
+
export declare const RelayMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
31
|
+
type: z.ZodLiteral<"query">;
|
|
32
|
+
message: z.ZodString;
|
|
33
|
+
id: z.ZodString;
|
|
34
|
+
timestamp: z.ZodString;
|
|
35
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
36
|
+
type: z.ZodLiteral<"directive">;
|
|
37
|
+
message: z.ZodString;
|
|
38
|
+
id: z.ZodString;
|
|
39
|
+
timestamp: z.ZodString;
|
|
40
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
41
|
+
type: z.ZodLiteral<"abort">;
|
|
42
|
+
message: z.ZodOptional<z.ZodString>;
|
|
43
|
+
id: z.ZodString;
|
|
44
|
+
timestamp: z.ZodString;
|
|
45
|
+
}, z.core.$strip>], "type">;
|
|
46
|
+
export type RelayMessage = z.infer<typeof RelayMessageSchema>;
|
|
47
|
+
/** Outbox reply. `inReplyTo` is mandatory — every reply references an inbox id. */
|
|
48
|
+
export declare const RelayResponseSchema: z.ZodObject<{
|
|
49
|
+
id: z.ZodString;
|
|
50
|
+
inReplyTo: z.ZodString;
|
|
51
|
+
timestamp: z.ZodString;
|
|
52
|
+
message: z.ZodString;
|
|
53
|
+
}, z.core.$strip>;
|
|
54
|
+
export type RelayResponse = z.infer<typeof RelayResponseSchema>;
|
|
55
|
+
export { RelayStateSchema, type RelayState } from "../workflow/state-schema.js";
|
|
56
|
+
/**
|
|
57
|
+
* `meta.json` written alongside archived inbox/outbox in
|
|
58
|
+
* `.sequant/logs/relay/<issue>-<phase>-<ts>/`. Captures the run boundary so
|
|
59
|
+
* post-hoc inspection knows which phase/issue the messages belonged to.
|
|
60
|
+
*/
|
|
61
|
+
export declare const RelayArchiveMetaSchema: z.ZodObject<{
|
|
62
|
+
issue: z.ZodNumber;
|
|
63
|
+
phase: z.ZodString;
|
|
64
|
+
startedAt: z.ZodString;
|
|
65
|
+
endedAt: z.ZodString;
|
|
66
|
+
messageCount: z.ZodNumber;
|
|
67
|
+
}, z.core.$strip>;
|
|
68
|
+
export type RelayArchiveMeta = z.infer<typeof RelayArchiveMetaSchema>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions and Zod schemas for the interactive relay (#383).
|
|
3
|
+
*
|
|
4
|
+
* The relay is a file-based IPC channel that allows a user terminal to send
|
|
5
|
+
* messages into a running headless Claude session. Messages flow through two
|
|
6
|
+
* JSONL files in `<worktree>/.sequant/relay/`:
|
|
7
|
+
*
|
|
8
|
+
* - `inbox.jsonl`: user → Claude (consumed by the PostToolUse hook)
|
|
9
|
+
* - `outbox.jsonl`: Claude → user (tailed by `sequant watch`)
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
/** Maximum size (bytes) of a single relay message body. */
|
|
13
|
+
export const MAX_MESSAGE_BYTES = 16 * 1024; // 16 KB
|
|
14
|
+
/** Relay message types. `query` asks for status, `directive` nudges behavior, `abort` stops. */
|
|
15
|
+
export const RelayMessageTypeSchema = z.enum(["query", "directive", "abort"]);
|
|
16
|
+
/** Inbox message id format: `msg_<hex>`. */
|
|
17
|
+
export const MESSAGE_ID_PATTERN = /^msg_[0-9a-f]+$/;
|
|
18
|
+
/** Outbox reply id format: `reply_<hex>`. */
|
|
19
|
+
export const REPLY_ID_PATTERN = /^reply_[0-9a-f]+$/;
|
|
20
|
+
const baseInboxFields = {
|
|
21
|
+
id: z.string().regex(MESSAGE_ID_PATTERN, "id must match /^msg_[0-9a-f]+$/"),
|
|
22
|
+
timestamp: z.string().datetime(),
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Discriminated union over `type`. `query` and `directive` require a non-empty
|
|
26
|
+
* `message` body; `abort` allows an optional explanatory message but it is not
|
|
27
|
+
* required.
|
|
28
|
+
*/
|
|
29
|
+
export const RelayMessageSchema = z.discriminatedUnion("type", [
|
|
30
|
+
z.object({
|
|
31
|
+
...baseInboxFields,
|
|
32
|
+
type: z.literal("query"),
|
|
33
|
+
message: z
|
|
34
|
+
.string()
|
|
35
|
+
.min(1, "message is required for query")
|
|
36
|
+
.max(MAX_MESSAGE_BYTES, `message exceeds ${MAX_MESSAGE_BYTES} bytes`),
|
|
37
|
+
}),
|
|
38
|
+
z.object({
|
|
39
|
+
...baseInboxFields,
|
|
40
|
+
type: z.literal("directive"),
|
|
41
|
+
message: z
|
|
42
|
+
.string()
|
|
43
|
+
.min(1, "message is required for directive")
|
|
44
|
+
.max(MAX_MESSAGE_BYTES, `message exceeds ${MAX_MESSAGE_BYTES} bytes`),
|
|
45
|
+
}),
|
|
46
|
+
z.object({
|
|
47
|
+
...baseInboxFields,
|
|
48
|
+
type: z.literal("abort"),
|
|
49
|
+
message: z.string().max(MAX_MESSAGE_BYTES).optional(),
|
|
50
|
+
}),
|
|
51
|
+
]);
|
|
52
|
+
/** Outbox reply. `inReplyTo` is mandatory — every reply references an inbox id. */
|
|
53
|
+
export const RelayResponseSchema = z.object({
|
|
54
|
+
id: z.string().regex(REPLY_ID_PATTERN, "id must match /^reply_[0-9a-f]+$/"),
|
|
55
|
+
inReplyTo: z
|
|
56
|
+
.string()
|
|
57
|
+
.min(1, "inReplyTo is required")
|
|
58
|
+
.regex(MESSAGE_ID_PATTERN, "inReplyTo must match /^msg_[0-9a-f]+$/"),
|
|
59
|
+
timestamp: z.string().datetime(),
|
|
60
|
+
message: z.string(),
|
|
61
|
+
});
|
|
62
|
+
// `RelayState` is defined in `src/lib/workflow/state-schema.ts` (canonical
|
|
63
|
+
// location alongside `IssueState`). Re-exported here as a convenience.
|
|
64
|
+
export { RelayStateSchema } from "../workflow/state-schema.js";
|
|
65
|
+
/**
|
|
66
|
+
* `meta.json` written alongside archived inbox/outbox in
|
|
67
|
+
* `.sequant/logs/relay/<issue>-<phase>-<ts>/`. Captures the run boundary so
|
|
68
|
+
* post-hoc inspection knows which phase/issue the messages belonged to.
|
|
69
|
+
*/
|
|
70
|
+
export const RelayArchiveMetaSchema = z.object({
|
|
71
|
+
issue: z.number().int().positive(),
|
|
72
|
+
phase: z.string(),
|
|
73
|
+
startedAt: z.string().datetime(),
|
|
74
|
+
endedAt: z.string().datetime(),
|
|
75
|
+
messageCount: z.number().int().nonnegative(),
|
|
76
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only writer for relay inbox/outbox JSONL files.
|
|
3
|
+
*
|
|
4
|
+
* Single-line appends use `O_APPEND` so concurrent writes interleave cleanly
|
|
5
|
+
* at the line boundary (POSIX guarantees atomicity for `write()` calls smaller
|
|
6
|
+
* than `PIPE_BUF`/4 KiB on local filesystems). Multi-line payloads use the
|
|
7
|
+
* temp-file + `rename()` pattern so partial reads never observe a half-written
|
|
8
|
+
* file (AC-5).
|
|
9
|
+
*/
|
|
10
|
+
import { type RelayMessage, type RelayMessageType, type RelayResponse } from "./types.js";
|
|
11
|
+
import { type RelayPathOptions } from "./paths.js";
|
|
12
|
+
/** Generate a `msg_<hex>` id with enough entropy to stay unique within a run. */
|
|
13
|
+
export declare function generateMessageId(): string;
|
|
14
|
+
/** Generate a `reply_<hex>` id. */
|
|
15
|
+
export declare function generateReplyId(): string;
|
|
16
|
+
/** Build an inbox message, validating against the schema. */
|
|
17
|
+
export declare function buildInboxMessage(input: {
|
|
18
|
+
type: RelayMessageType;
|
|
19
|
+
message?: string;
|
|
20
|
+
id?: string;
|
|
21
|
+
timestamp?: string;
|
|
22
|
+
}): RelayMessage;
|
|
23
|
+
/** Build an outbox reply, validating against the schema. */
|
|
24
|
+
export declare function buildOutboxReply(input: {
|
|
25
|
+
inReplyTo: string;
|
|
26
|
+
message: string;
|
|
27
|
+
id?: string;
|
|
28
|
+
timestamp?: string;
|
|
29
|
+
}): RelayResponse;
|
|
30
|
+
/**
|
|
31
|
+
* Append a single inbox message. Uses `O_APPEND` for crash-safe concurrent
|
|
32
|
+
* writes (AC-5). Throws on body sizes that exceed `MAX_MESSAGE_BYTES`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function appendInboxMessage(issue: number, input: {
|
|
35
|
+
type: RelayMessageType;
|
|
36
|
+
message?: string;
|
|
37
|
+
}, options?: RelayPathOptions): RelayMessage;
|
|
38
|
+
/** Append a single outbox reply via `O_APPEND`. */
|
|
39
|
+
export declare function appendOutboxReply(issue: number, input: {
|
|
40
|
+
inReplyTo: string;
|
|
41
|
+
message: string;
|
|
42
|
+
}, options?: RelayPathOptions): RelayResponse;
|
|
43
|
+
/**
|
|
44
|
+
* Write multiple inbox messages atomically (temp file + rename).
|
|
45
|
+
* Useful when seeding a relay dir from a backup or replaying. Single-message
|
|
46
|
+
* writes should use `appendInboxMessage` instead.
|
|
47
|
+
*/
|
|
48
|
+
export declare function writeInboxBatchAtomic(issue: number, messages: RelayMessage[], options?: RelayPathOptions): void;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only writer for relay inbox/outbox JSONL files.
|
|
3
|
+
*
|
|
4
|
+
* Single-line appends use `O_APPEND` so concurrent writes interleave cleanly
|
|
5
|
+
* at the line boundary (POSIX guarantees atomicity for `write()` calls smaller
|
|
6
|
+
* than `PIPE_BUF`/4 KiB on local filesystems). Multi-line payloads use the
|
|
7
|
+
* temp-file + `rename()` pattern so partial reads never observe a half-written
|
|
8
|
+
* file (AC-5).
|
|
9
|
+
*/
|
|
10
|
+
import { randomBytes } from "crypto";
|
|
11
|
+
import { closeSync, existsSync, mkdirSync, openSync, renameSync, unlinkSync, writeSync, } from "fs";
|
|
12
|
+
import { dirname, join } from "path";
|
|
13
|
+
import { MAX_MESSAGE_BYTES, RelayMessageSchema, RelayResponseSchema, } from "./types.js";
|
|
14
|
+
import { inboxPathFor, outboxPathFor } from "./paths.js";
|
|
15
|
+
/** Generate a `msg_<hex>` id with enough entropy to stay unique within a run. */
|
|
16
|
+
export function generateMessageId() {
|
|
17
|
+
return `msg_${randomBytes(8).toString("hex")}`;
|
|
18
|
+
}
|
|
19
|
+
/** Generate a `reply_<hex>` id. */
|
|
20
|
+
export function generateReplyId() {
|
|
21
|
+
return `reply_${randomBytes(8).toString("hex")}`;
|
|
22
|
+
}
|
|
23
|
+
function ensureDir(path) {
|
|
24
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
/** Build an inbox message, validating against the schema. */
|
|
27
|
+
export function buildInboxMessage(input) {
|
|
28
|
+
const candidate = {
|
|
29
|
+
id: input.id ?? generateMessageId(),
|
|
30
|
+
timestamp: input.timestamp ?? new Date().toISOString(),
|
|
31
|
+
type: input.type,
|
|
32
|
+
...(input.message !== undefined ? { message: input.message } : {}),
|
|
33
|
+
};
|
|
34
|
+
return RelayMessageSchema.parse(candidate);
|
|
35
|
+
}
|
|
36
|
+
/** Build an outbox reply, validating against the schema. */
|
|
37
|
+
export function buildOutboxReply(input) {
|
|
38
|
+
return RelayResponseSchema.parse({
|
|
39
|
+
id: input.id ?? generateReplyId(),
|
|
40
|
+
inReplyTo: input.inReplyTo,
|
|
41
|
+
timestamp: input.timestamp ?? new Date().toISOString(),
|
|
42
|
+
message: input.message,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Append a single inbox message. Uses `O_APPEND` for crash-safe concurrent
|
|
47
|
+
* writes (AC-5). Throws on body sizes that exceed `MAX_MESSAGE_BYTES`.
|
|
48
|
+
*/
|
|
49
|
+
export function appendInboxMessage(issue, input, options = {}) {
|
|
50
|
+
if (input.message &&
|
|
51
|
+
Buffer.byteLength(input.message, "utf-8") > MAX_MESSAGE_BYTES) {
|
|
52
|
+
throw new Error(`relay: message body exceeds max size of ${MAX_MESSAGE_BYTES} bytes`);
|
|
53
|
+
}
|
|
54
|
+
const message = buildInboxMessage(input);
|
|
55
|
+
const path = inboxPathFor(issue, options);
|
|
56
|
+
appendJsonLine(path, message);
|
|
57
|
+
return message;
|
|
58
|
+
}
|
|
59
|
+
/** Append a single outbox reply via `O_APPEND`. */
|
|
60
|
+
export function appendOutboxReply(issue, input, options = {}) {
|
|
61
|
+
const reply = buildOutboxReply(input);
|
|
62
|
+
const path = outboxPathFor(issue, options);
|
|
63
|
+
appendJsonLine(path, reply);
|
|
64
|
+
return reply;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Write multiple inbox messages atomically (temp file + rename).
|
|
68
|
+
* Useful when seeding a relay dir from a backup or replaying. Single-message
|
|
69
|
+
* writes should use `appendInboxMessage` instead.
|
|
70
|
+
*/
|
|
71
|
+
export function writeInboxBatchAtomic(issue, messages, options = {}) {
|
|
72
|
+
const path = inboxPathFor(issue, options);
|
|
73
|
+
writeJsonLinesAtomic(path, messages);
|
|
74
|
+
}
|
|
75
|
+
function appendJsonLine(path, payload) {
|
|
76
|
+
ensureDir(path);
|
|
77
|
+
const line = JSON.stringify(payload) + "\n";
|
|
78
|
+
const fd = openSync(path, "a");
|
|
79
|
+
try {
|
|
80
|
+
writeSync(fd, line);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
closeSync(fd);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function writeJsonLinesAtomic(path, items) {
|
|
87
|
+
ensureDir(path);
|
|
88
|
+
const body = items.map((it) => JSON.stringify(it)).join("\n") +
|
|
89
|
+
(items.length ? "\n" : "");
|
|
90
|
+
// Temp file must live on the SAME filesystem as `path` for rename() to be atomic.
|
|
91
|
+
const tmp = join(dirname(path), `.relay-tmp.${process.pid}.${Date.now()}.${randomBytes(4).toString("hex")}`);
|
|
92
|
+
try {
|
|
93
|
+
const fd = openSync(tmp, "w");
|
|
94
|
+
try {
|
|
95
|
+
writeSync(fd, body);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
closeSync(fd);
|
|
99
|
+
}
|
|
100
|
+
renameSync(tmp, path);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
if (existsSync(tmp)) {
|
|
104
|
+
try {
|
|
105
|
+
unlinkSync(tmp);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
/* swallow */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
throw err;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -42,7 +42,10 @@ export interface AgentSettings {
|
|
|
42
42
|
/**
|
|
43
43
|
* Default model for sub-agents.
|
|
44
44
|
* Options: "haiku" (cheapest), "sonnet" (balanced), "opus" (most capable)
|
|
45
|
-
* Default: "haiku"
|
|
45
|
+
* Default: "haiku" — currently inert per anthropics/claude-code#43869.
|
|
46
|
+
* @deprecated currently inert; see anthropics/claude-code#43869. Subagents
|
|
47
|
+
* inherit the parent session's model regardless of this value. Kept so
|
|
48
|
+
* existing user settings.json files continue to parse without error.
|
|
46
49
|
*/
|
|
47
50
|
model: "haiku" | "sonnet" | "opus";
|
|
48
51
|
/**
|
|
@@ -144,6 +147,13 @@ export interface RunSettings {
|
|
|
144
147
|
* Aider-specific configuration. Only used when agent is "aider".
|
|
145
148
|
*/
|
|
146
149
|
aider?: AiderSettings;
|
|
150
|
+
/**
|
|
151
|
+
* Enable interactive relay (#383): file-based IPC that lets a user terminal
|
|
152
|
+
* send `query`/`directive`/`abort` messages into a running headless session
|
|
153
|
+
* via the PostToolUse hook. Disable with `--no-relay`.
|
|
154
|
+
* Default: true.
|
|
155
|
+
*/
|
|
156
|
+
relay?: boolean;
|
|
147
157
|
}
|
|
148
158
|
/**
|
|
149
159
|
* Scope assessment threshold configuration
|
|
@@ -210,6 +220,20 @@ export interface QASettings {
|
|
|
210
220
|
* Default: 100
|
|
211
221
|
*/
|
|
212
222
|
smallDiffThreshold: number;
|
|
223
|
+
/**
|
|
224
|
+
* When a diff touches only markdown files, treat pending CI checks that match
|
|
225
|
+
* `markdownOnlySafeCiPatterns` as informational instead of forcing
|
|
226
|
+
* `NEEDS_VERIFICATION`. Failed checks always gate regardless of this setting.
|
|
227
|
+
* Default: true
|
|
228
|
+
*/
|
|
229
|
+
markdownOnlyCiRelaxed: boolean;
|
|
230
|
+
/**
|
|
231
|
+
* Glob patterns for CI check names that are safe to ignore when pending on a
|
|
232
|
+
* markdown-only diff (e.g., build matrix, plugin structure validation).
|
|
233
|
+
* Consumer projects should override these to match their CI step names.
|
|
234
|
+
* Default: ["build (*)", "Plugin Structure Validation"]
|
|
235
|
+
*/
|
|
236
|
+
markdownOnlySafeCiPatterns: string[];
|
|
213
237
|
}
|
|
214
238
|
/**
|
|
215
239
|
* Full settings schema
|
|
@@ -277,6 +301,7 @@ export declare const RunSettingsSchema: z.ZodObject<{
|
|
|
277
301
|
editFormat: z.ZodOptional<z.ZodString>;
|
|
278
302
|
extraArgs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
279
303
|
}, z.core.$strip>>;
|
|
304
|
+
relay: z.ZodDefault<z.ZodBoolean>;
|
|
280
305
|
}, z.core.$strip>;
|
|
281
306
|
/** Zod schema for ScopeThreshold (base — fields required, no defaults) */
|
|
282
307
|
export declare const ScopeThresholdSchema: z.ZodObject<{
|
|
@@ -318,6 +343,8 @@ export declare const ScopeAssessmentSettingsSchema: z.ZodObject<{
|
|
|
318
343
|
/** Zod schema for QASettings */
|
|
319
344
|
export declare const QASettingsSchema: z.ZodObject<{
|
|
320
345
|
smallDiffThreshold: z.ZodDefault<z.ZodNumber>;
|
|
346
|
+
markdownOnlyCiRelaxed: z.ZodDefault<z.ZodBoolean>;
|
|
347
|
+
markdownOnlySafeCiPatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
321
348
|
}, z.core.$strip>;
|
|
322
349
|
/**
|
|
323
350
|
* Zod schema for the full SequantSettings (AC-1, AC-5).
|
|
@@ -359,6 +386,7 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
359
386
|
editFormat: z.ZodOptional<z.ZodString>;
|
|
360
387
|
extraArgs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
361
388
|
}, z.core.$strip>>;
|
|
389
|
+
relay: z.ZodDefault<z.ZodBoolean>;
|
|
362
390
|
}, z.core.$strip>>;
|
|
363
391
|
agents: z.ZodDefault<z.ZodObject<{
|
|
364
392
|
parallel: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -397,6 +425,8 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
397
425
|
}, z.core.$strip>>;
|
|
398
426
|
qa: z.ZodDefault<z.ZodObject<{
|
|
399
427
|
smallDiffThreshold: z.ZodDefault<z.ZodNumber>;
|
|
428
|
+
markdownOnlyCiRelaxed: z.ZodDefault<z.ZodBoolean>;
|
|
429
|
+
markdownOnlySafeCiPatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
400
430
|
}, z.core.$strip>>;
|
|
401
431
|
}, z.core.$loose>;
|
|
402
432
|
/** A single validation warning about an unknown or invalid setting */
|