sequant 2.1.2 → 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.
Files changed (146) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +73 -0
  4. package/dist/bin/cli.js +95 -9
  5. package/dist/src/commands/doctor.d.ts +25 -0
  6. package/dist/src/commands/doctor.js +36 -1
  7. package/dist/src/commands/init.d.ts +1 -0
  8. package/dist/src/commands/init.js +118 -0
  9. package/dist/src/commands/locks.d.ts +67 -0
  10. package/dist/src/commands/locks.js +290 -0
  11. package/dist/src/commands/merge.js +11 -0
  12. package/dist/src/commands/prompt.d.ts +39 -0
  13. package/dist/src/commands/prompt.js +179 -0
  14. package/dist/src/commands/run-display.d.ts +26 -0
  15. package/dist/src/commands/run-display.js +150 -0
  16. package/dist/src/commands/run-progress.d.ts +32 -0
  17. package/dist/src/commands/run-progress.js +76 -0
  18. package/dist/src/commands/run.js +83 -73
  19. package/dist/src/commands/stats.d.ts +2 -0
  20. package/dist/src/commands/stats.js +94 -8
  21. package/dist/src/commands/status.js +27 -1
  22. package/dist/src/commands/watch.d.ts +16 -0
  23. package/dist/src/commands/watch.js +147 -0
  24. package/dist/src/lib/ac-linter.d.ts +1 -1
  25. package/dist/src/lib/ac-linter.js +81 -0
  26. package/dist/src/lib/assess-collision-detect.d.ts +91 -0
  27. package/dist/src/lib/assess-collision-detect.js +217 -0
  28. package/dist/src/lib/assess-comment-parser.d.ts +59 -1
  29. package/dist/src/lib/assess-comment-parser.js +124 -2
  30. package/dist/src/lib/cli-ui/format.d.ts +19 -0
  31. package/dist/src/lib/cli-ui/format.js +34 -0
  32. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +181 -0
  33. package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
  34. package/dist/src/lib/cli-ui/run-renderer.d.ts +239 -0
  35. package/dist/src/lib/cli-ui/run-renderer.js +1173 -0
  36. package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
  37. package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
  38. package/dist/src/lib/locks/index.d.ts +7 -0
  39. package/dist/src/lib/locks/index.js +5 -0
  40. package/dist/src/lib/locks/lock-manager.d.ts +168 -0
  41. package/dist/src/lib/locks/lock-manager.js +433 -0
  42. package/dist/src/lib/locks/types.d.ts +59 -0
  43. package/dist/src/lib/locks/types.js +31 -0
  44. package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
  45. package/dist/src/lib/qa/markdown-only-ci.js +74 -0
  46. package/dist/src/lib/relay/activation.d.ts +60 -0
  47. package/dist/src/lib/relay/activation.js +122 -0
  48. package/dist/src/lib/relay/archive.d.ts +34 -0
  49. package/dist/src/lib/relay/archive.js +106 -0
  50. package/dist/src/lib/relay/frame.d.ts +20 -0
  51. package/dist/src/lib/relay/frame.js +76 -0
  52. package/dist/src/lib/relay/index.d.ts +13 -0
  53. package/dist/src/lib/relay/index.js +13 -0
  54. package/dist/src/lib/relay/paths.d.ts +43 -0
  55. package/dist/src/lib/relay/paths.js +59 -0
  56. package/dist/src/lib/relay/pid.d.ts +34 -0
  57. package/dist/src/lib/relay/pid.js +72 -0
  58. package/dist/src/lib/relay/reader.d.ts +35 -0
  59. package/dist/src/lib/relay/reader.js +115 -0
  60. package/dist/src/lib/relay/types.d.ts +68 -0
  61. package/dist/src/lib/relay/types.js +76 -0
  62. package/dist/src/lib/relay/writer.d.ts +48 -0
  63. package/dist/src/lib/relay/writer.js +113 -0
  64. package/dist/src/lib/settings.d.ts +31 -1
  65. package/dist/src/lib/settings.js +18 -3
  66. package/dist/src/lib/skill-version.d.ts +19 -0
  67. package/dist/src/lib/skill-version.js +68 -0
  68. package/dist/src/lib/templates.d.ts +1 -0
  69. package/dist/src/lib/templates.js +1 -1
  70. package/dist/src/lib/version-check.d.ts +60 -5
  71. package/dist/src/lib/version-check.js +97 -9
  72. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  73. package/dist/src/lib/workflow/batch-executor.js +249 -176
  74. package/dist/src/lib/workflow/config-resolver.js +4 -0
  75. package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
  76. package/dist/src/lib/workflow/heartbeat.js +194 -0
  77. package/dist/src/lib/workflow/phase-executor.d.ts +88 -3
  78. package/dist/src/lib/workflow/phase-executor.js +276 -52
  79. package/dist/src/lib/workflow/phase-mapper.d.ts +3 -2
  80. package/dist/src/lib/workflow/phase-mapper.js +17 -20
  81. package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
  82. package/dist/src/lib/workflow/platforms/github.js +20 -3
  83. package/dist/src/lib/workflow/pr-status.d.ts +18 -2
  84. package/dist/src/lib/workflow/pr-status.js +41 -9
  85. package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
  86. package/dist/src/lib/workflow/qa-stagnation.js +179 -0
  87. package/dist/src/lib/workflow/run-orchestrator.d.ts +76 -0
  88. package/dist/src/lib/workflow/run-orchestrator.js +382 -29
  89. package/dist/src/lib/workflow/run-reflect.js +1 -1
  90. package/dist/src/lib/workflow/run-state.d.ts +71 -0
  91. package/dist/src/lib/workflow/run-state.js +14 -0
  92. package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
  93. package/dist/src/lib/workflow/state-cleanup.js +17 -5
  94. package/dist/src/lib/workflow/state-manager.d.ts +12 -1
  95. package/dist/src/lib/workflow/state-manager.js +37 -0
  96. package/dist/src/lib/workflow/state-schema.d.ts +62 -0
  97. package/dist/src/lib/workflow/state-schema.js +35 -1
  98. package/dist/src/lib/workflow/types.d.ts +74 -1
  99. package/dist/src/lib/workflow/worktree-manager.d.ts +12 -4
  100. package/dist/src/lib/workflow/worktree-manager.js +76 -17
  101. package/dist/src/mcp/tools/run.d.ts +44 -0
  102. package/dist/src/mcp/tools/run.js +104 -13
  103. package/dist/src/ui/tui/App.d.ts +14 -0
  104. package/dist/src/ui/tui/App.js +41 -0
  105. package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
  106. package/dist/src/ui/tui/ElapsedTimer.js +31 -0
  107. package/dist/src/ui/tui/Header.d.ts +6 -0
  108. package/dist/src/ui/tui/Header.js +15 -0
  109. package/dist/src/ui/tui/IssueBox.d.ts +16 -0
  110. package/dist/src/ui/tui/IssueBox.js +68 -0
  111. package/dist/src/ui/tui/Spinner.d.ts +9 -0
  112. package/dist/src/ui/tui/Spinner.js +18 -0
  113. package/dist/src/ui/tui/index.d.ts +15 -0
  114. package/dist/src/ui/tui/index.js +29 -0
  115. package/dist/src/ui/tui/theme.d.ts +29 -0
  116. package/dist/src/ui/tui/theme.js +52 -0
  117. package/dist/src/ui/tui/truncate.d.ts +11 -0
  118. package/dist/src/ui/tui/truncate.js +31 -0
  119. package/package.json +10 -3
  120. package/templates/agents/sequant-explorer.md +1 -0
  121. package/templates/agents/sequant-qa-checker.md +2 -1
  122. package/templates/agents/sequant-testgen.md +1 -0
  123. package/templates/hooks/post-tool.sh +11 -0
  124. package/templates/hooks/pre-tool.sh +18 -9
  125. package/templates/hooks/relay-check.sh +107 -0
  126. package/templates/relay/frame.txt +11 -0
  127. package/templates/scripts/cleanup-worktree.sh +25 -3
  128. package/templates/scripts/new-feature.sh +6 -0
  129. package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
  130. package/templates/skills/_shared/references/subagent-types.md +21 -8
  131. package/templates/skills/assess/SKILL.md +261 -94
  132. package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
  133. package/templates/skills/docs/SKILL.md +141 -22
  134. package/templates/skills/exec/SKILL.md +10 -49
  135. package/templates/skills/fullsolve/SKILL.md +80 -32
  136. package/templates/skills/loop/SKILL.md +28 -0
  137. package/templates/skills/merger/SKILL.md +621 -0
  138. package/templates/skills/qa/SKILL.md +746 -8
  139. package/templates/skills/qa/scripts/quality-checks.sh +47 -1
  140. package/templates/skills/setup/SKILL.md +6 -0
  141. package/templates/skills/spec/SKILL.md +217 -964
  142. package/templates/skills/spec/references/parallel-groups.md +7 -0
  143. package/templates/skills/spec/references/quality-checklist.md +75 -0
  144. package/templates/skills/spec/references/recommended-workflow.md +4 -2
  145. package/templates/skills/test/SKILL.md +0 -27
  146. package/templates/skills/testgen/SKILL.md +24 -44
@@ -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 */