sequant 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +81 -5
  4. package/dist/bin/cli.js +140 -13
  5. package/dist/src/commands/abort.d.ts +36 -0
  6. package/dist/src/commands/abort.js +138 -0
  7. package/dist/src/commands/doctor.d.ts +25 -0
  8. package/dist/src/commands/doctor.js +36 -1
  9. package/dist/src/commands/locks.d.ts +67 -0
  10. package/dist/src/commands/locks.js +290 -0
  11. package/dist/src/commands/merge.js +11 -0
  12. package/dist/src/commands/prompt.d.ts +46 -0
  13. package/dist/src/commands/prompt.js +273 -0
  14. package/dist/src/commands/run-display.d.ts +11 -2
  15. package/dist/src/commands/run-display.js +62 -28
  16. package/dist/src/commands/run-progress.d.ts +42 -0
  17. package/dist/src/commands/run-progress.js +93 -0
  18. package/dist/src/commands/run.js +90 -18
  19. package/dist/src/commands/stats.d.ts +2 -0
  20. package/dist/src/commands/stats.js +94 -8
  21. package/dist/src/commands/status.js +12 -0
  22. package/dist/src/commands/watch.d.ts +18 -0
  23. package/dist/src/commands/watch.js +211 -0
  24. package/dist/src/lib/ac-linter.d.ts +1 -1
  25. package/dist/src/lib/ac-linter.js +81 -0
  26. package/dist/src/lib/assess-collision-detect.d.ts +91 -0
  27. package/dist/src/lib/assess-collision-detect.js +217 -0
  28. package/dist/src/lib/assess-comment-parser.d.ts +59 -1
  29. package/dist/src/lib/assess-comment-parser.js +124 -2
  30. package/dist/src/lib/cli-ui/format.d.ts +19 -0
  31. package/dist/src/lib/cli-ui/format.js +34 -0
  32. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +220 -0
  33. package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
  34. package/dist/src/lib/cli-ui/run-renderer.d.ts +265 -0
  35. package/dist/src/lib/cli-ui/run-renderer.js +1390 -0
  36. package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
  37. package/dist/src/lib/cli-ui/scrollback-harness.js +294 -0
  38. package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
  39. package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
  40. package/dist/src/lib/locks/index.d.ts +7 -0
  41. package/dist/src/lib/locks/index.js +5 -0
  42. package/dist/src/lib/locks/lock-manager.d.ts +168 -0
  43. package/dist/src/lib/locks/lock-manager.js +433 -0
  44. package/dist/src/lib/locks/types.d.ts +59 -0
  45. package/dist/src/lib/locks/types.js +31 -0
  46. package/dist/src/lib/merge-check/types.js +1 -1
  47. package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
  48. package/dist/src/lib/qa/markdown-only-ci.js +74 -0
  49. package/dist/src/lib/relay/activation.d.ts +60 -0
  50. package/dist/src/lib/relay/activation.js +122 -0
  51. package/dist/src/lib/relay/archive.d.ts +34 -0
  52. package/dist/src/lib/relay/archive.js +112 -0
  53. package/dist/src/lib/relay/frame.d.ts +20 -0
  54. package/dist/src/lib/relay/frame.js +76 -0
  55. package/dist/src/lib/relay/index.d.ts +13 -0
  56. package/dist/src/lib/relay/index.js +13 -0
  57. package/dist/src/lib/relay/paths.d.ts +43 -0
  58. package/dist/src/lib/relay/paths.js +59 -0
  59. package/dist/src/lib/relay/pid.d.ts +34 -0
  60. package/dist/src/lib/relay/pid.js +72 -0
  61. package/dist/src/lib/relay/reader.d.ts +35 -0
  62. package/dist/src/lib/relay/reader.js +115 -0
  63. package/dist/src/lib/relay/types.d.ts +70 -0
  64. package/dist/src/lib/relay/types.js +85 -0
  65. package/dist/src/lib/relay/writer.d.ts +48 -0
  66. package/dist/src/lib/relay/writer.js +113 -0
  67. package/dist/src/lib/settings.d.ts +31 -1
  68. package/dist/src/lib/settings.js +18 -3
  69. package/dist/src/lib/version-check.d.ts +60 -5
  70. package/dist/src/lib/version-check.js +97 -9
  71. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  72. package/dist/src/lib/workflow/batch-executor.js +274 -185
  73. package/dist/src/lib/workflow/config-resolver.js +4 -0
  74. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
  75. package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
  76. package/dist/src/lib/workflow/drivers/aider.js +9 -0
  77. package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
  78. package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
  79. package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
  80. package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
  81. package/dist/src/lib/workflow/event-emitter.js +102 -0
  82. package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
  83. package/dist/src/lib/workflow/heartbeat.js +194 -0
  84. package/dist/src/lib/workflow/notice.d.ts +32 -0
  85. package/dist/src/lib/workflow/notice.js +38 -0
  86. package/dist/src/lib/workflow/phase-executor.d.ts +58 -16
  87. package/dist/src/lib/workflow/phase-executor.js +244 -130
  88. package/dist/src/lib/workflow/phase-mapper.d.ts +27 -13
  89. package/dist/src/lib/workflow/phase-mapper.js +70 -51
  90. package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
  91. package/dist/src/lib/workflow/phase-registry.js +233 -0
  92. package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
  93. package/dist/src/lib/workflow/platforms/github.js +20 -3
  94. package/dist/src/lib/workflow/pr-status.d.ts +18 -2
  95. package/dist/src/lib/workflow/pr-status.js +41 -9
  96. package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
  97. package/dist/src/lib/workflow/qa-stagnation.js +179 -0
  98. package/dist/src/lib/workflow/run-log-schema.d.ts +5 -55
  99. package/dist/src/lib/workflow/run-orchestrator.d.ts +70 -1
  100. package/dist/src/lib/workflow/run-orchestrator.js +464 -25
  101. package/dist/src/lib/workflow/run-reflect.js +1 -1
  102. package/dist/src/lib/workflow/run-state.d.ts +71 -0
  103. package/dist/src/lib/workflow/run-state.js +14 -0
  104. package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
  105. package/dist/src/lib/workflow/state-cleanup.js +17 -5
  106. package/dist/src/lib/workflow/state-manager.d.ts +31 -2
  107. package/dist/src/lib/workflow/state-manager.js +64 -1
  108. package/dist/src/lib/workflow/state-schema.d.ts +82 -35
  109. package/dist/src/lib/workflow/state-schema.js +63 -4
  110. package/dist/src/lib/workflow/types.d.ts +139 -16
  111. package/dist/src/lib/workflow/types.js +18 -13
  112. package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
  113. package/dist/src/lib/workflow/worktree-manager.js +15 -6
  114. package/dist/src/mcp/tools/run.d.ts +44 -0
  115. package/dist/src/mcp/tools/run.js +104 -13
  116. package/dist/src/ui/tui/App.d.ts +14 -0
  117. package/dist/src/ui/tui/App.js +41 -0
  118. package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
  119. package/dist/src/ui/tui/ElapsedTimer.js +31 -0
  120. package/dist/src/ui/tui/Header.d.ts +6 -0
  121. package/dist/src/ui/tui/Header.js +15 -0
  122. package/dist/src/ui/tui/IssueBox.d.ts +16 -0
  123. package/dist/src/ui/tui/IssueBox.js +68 -0
  124. package/dist/src/ui/tui/Spinner.d.ts +9 -0
  125. package/dist/src/ui/tui/Spinner.js +18 -0
  126. package/dist/src/ui/tui/index.d.ts +15 -0
  127. package/dist/src/ui/tui/index.js +29 -0
  128. package/dist/src/ui/tui/theme.d.ts +29 -0
  129. package/dist/src/ui/tui/theme.js +52 -0
  130. package/dist/src/ui/tui/truncate.d.ts +11 -0
  131. package/dist/src/ui/tui/truncate.js +31 -0
  132. package/package.json +14 -6
  133. package/templates/agents/sequant-explorer.md +1 -0
  134. package/templates/agents/sequant-qa-checker.md +2 -1
  135. package/templates/agents/sequant-testgen.md +1 -0
  136. package/templates/hooks/post-tool.sh +92 -0
  137. package/templates/hooks/pre-tool.sh +18 -9
  138. package/templates/hooks/relay-check.sh +107 -0
  139. package/templates/relay/frame.txt +11 -0
  140. package/templates/scripts/cleanup-worktree.sh +25 -3
  141. package/templates/scripts/new-feature.sh +6 -0
  142. package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
  143. package/templates/skills/_shared/references/subagent-types.md +21 -8
  144. package/templates/skills/assess/SKILL.md +122 -68
  145. package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
  146. package/templates/skills/docs/SKILL.md +141 -22
  147. package/templates/skills/exec/SKILL.md +10 -8
  148. package/templates/skills/fullsolve/SKILL.md +79 -5
  149. package/templates/skills/loop/SKILL.md +28 -0
  150. package/templates/skills/merger/SKILL.md +621 -0
  151. package/templates/skills/qa/SKILL.md +727 -8
  152. package/templates/skills/setup/SKILL.md +12 -6
  153. package/templates/skills/spec/SKILL.md +52 -0
  154. package/templates/skills/spec/references/parallel-groups.md +7 -0
  155. package/templates/skills/spec/references/recommended-workflow.md +4 -2
  156. package/templates/skills/testgen/SKILL.md +24 -17
@@ -0,0 +1,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,70 @@
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
+ inboxCount: z.ZodOptional<z.ZodNumber>;
68
+ outboxCount: z.ZodOptional<z.ZodNumber>;
69
+ }, z.core.$strip>;
70
+ export type RelayArchiveMeta = z.infer<typeof RelayArchiveMetaSchema>;
@@ -0,0 +1,85 @@
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
+ /** Total inbox + outbox messages exchanged during the run. */
76
+ messageCount: z.number().int().nonnegative(),
77
+ /**
78
+ * Inbox messages (user → agent). Split out from `messageCount` (#645, Gap 5)
79
+ * so post-hoc inspection can spot unanswered queries (inboxCount > outboxCount).
80
+ * Optional for backward compatibility with archives written before this split.
81
+ */
82
+ inboxCount: z.number().int().nonnegative().optional(),
83
+ /** Outbox replies (agent → user). See `inboxCount` for context. */
84
+ outboxCount: z.number().int().nonnegative().optional(),
85
+ });
@@ -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 */
@@ -57,6 +57,7 @@ export const RunSettingsSchema = z.object({
57
57
  devUrl: z.string().optional(),
58
58
  agent: z.string().optional(),
59
59
  aider: AiderSettingsSchema.optional(),
60
+ relay: z.boolean().default(true),
60
61
  });
61
62
  /** Zod schema for ScopeThreshold (base — fields required, no defaults) */
62
63
  export const ScopeThresholdSchema = z.object({
@@ -121,6 +122,10 @@ export const ScopeAssessmentSettingsSchema = z.object({
121
122
  /** Zod schema for QASettings */
122
123
  export const QASettingsSchema = z.object({
123
124
  smallDiffThreshold: z.number().default(100),
125
+ markdownOnlyCiRelaxed: z.boolean().default(true),
126
+ markdownOnlySafeCiPatterns: z
127
+ .array(z.string())
128
+ .default(["build (*)", "Plugin Structure Validation"]),
124
129
  });
125
130
  /**
126
131
  * Zod schema for the full SequantSettings (AC-1, AC-5).
@@ -167,6 +172,7 @@ const KNOWN_KEYS = {
167
172
  "devUrl",
168
173
  "agent",
169
174
  "aider",
175
+ "relay",
170
176
  ]),
171
177
  agents: new Set(["parallel", "model", "isolateParallel"]),
172
178
  scopeAssessment: new Set([
@@ -175,7 +181,11 @@ const KNOWN_KEYS = {
175
181
  "trivialThresholds",
176
182
  "thresholds",
177
183
  ]),
178
- qa: new Set(["smallDiffThreshold"]),
184
+ qa: new Set([
185
+ "smallDiffThreshold",
186
+ "markdownOnlyCiRelaxed",
187
+ "markdownOnlySafeCiPatterns",
188
+ ]),
179
189
  "run.rotation": new Set(["enabled", "maxSizeMB", "maxFiles"]),
180
190
  "run.aider": new Set(["model", "editFormat", "extraArgs"]),
181
191
  "scopeAssessment.trivialThresholds": new Set([
@@ -310,6 +320,8 @@ export const DEFAULT_SCOPE_ASSESSMENT_SETTINGS = {
310
320
  */
311
321
  export const DEFAULT_QA_SETTINGS = {
312
322
  smallDiffThreshold: 100,
323
+ markdownOnlyCiRelaxed: true,
324
+ markdownOnlySafeCiPatterns: ["build (*)", "Plugin Structure Validation"],
313
325
  };
314
326
  /**
315
327
  * Default settings
@@ -331,6 +343,7 @@ export const DEFAULT_SETTINGS = {
331
343
  retry: true, // Enable automatic retry with MCP fallback by default
332
344
  staleBranchThreshold: 5, // Block QA/test if feature is >5 commits behind main
333
345
  resolvedIssueTTL: 7, // Auto-prune resolved issues after 7 days
346
+ relay: true, // Enable interactive relay (#383) by default
334
347
  },
335
348
  agents: DEFAULT_AGENT_SETTINGS,
336
349
  scopeAssessment: DEFAULT_SCOPE_ASSESSMENT_SETTINGS,
@@ -482,7 +495,7 @@ export function generateSettingsJsonc(settings) {
482
495
  lines.push(` "agents": {`);
483
496
  lines.push(` // Run agents in parallel (faster, higher token usage)`);
484
497
  lines.push(` "parallel": ${JSON.stringify(settings.agents.parallel)},`);
485
- lines.push(` // Default model for sub-agents ("haiku", "sonnet", "opus")`);
498
+ lines.push(` // Default model for sub-agents ("haiku", "sonnet", "opus") — currently inert per anthropics/claude-code#43869`);
486
499
  lines.push(` "model": ${JSON.stringify(settings.agents.model)},`);
487
500
  lines.push(` // Isolate parallel agent groups in separate worktrees`);
488
501
  lines.push(` "isolateParallel": ${JSON.stringify(settings.agents.isolateParallel)}`);
@@ -597,7 +610,7 @@ Generated by \`sequant init\`. See defaults below.
597
610
  | Key | Type | Default | Description |
598
611
  |-----|------|---------|-------------|
599
612
  | \`parallel\` | boolean | \`false\` | Run agents in parallel (faster, higher token usage) |
600
- | \`model\` | enum | \`"haiku"\` | Default model: \`"haiku"\`, \`"sonnet"\`, or \`"opus"\` |
613
+ | \`model\` | enum | \`"haiku"\` | Default model: \`"haiku"\`, \`"sonnet"\`, or \`"opus"\`. **Currently inert** per [anthropics/claude-code#43869](https://github.com/anthropics/claude-code/issues/43869) — subagents inherit the parent session's model. Kept for forward compatibility. |
601
614
  | \`isolateParallel\` | boolean | \`false\` | Isolate parallel agents in separate worktrees |
602
615
 
603
616
  ## \`scopeAssessment\` — Scope Assessment Settings
@@ -630,6 +643,8 @@ Each threshold has \`yellow\` (warning) and \`red\` (split recommended) values:
630
643
  | Key | Type | Default | Description |
631
644
  |-----|------|---------|-------------|
632
645
  | \`smallDiffThreshold\` | number | \`100\` | Diff size threshold for small-diff fast path |
646
+ | \`markdownOnlyCiRelaxed\` | boolean | \`true\` | When diff touches only \`.md\` files, treat pending CI checks matching \`markdownOnlySafeCiPatterns\` as informational |
647
+ | \`markdownOnlySafeCiPatterns\` | string[] | \`["build (*)", "Plugin Structure Validation"]\` | Glob patterns for CI checks that are safe to ignore when pending on a markdown-only diff |
633
648
 
634
649
  ---
635
650