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,290 @@
1
+ /**
2
+ * `sequant locks` — inspect and clear per-issue concurrency locks (#625).
3
+ */
4
+ import chalk from "chalk";
5
+ import { LockManager, formatLockedMessage, } from "../lib/locks/index.js";
6
+ /** Human-readable line for the `--signal-other` log output (#637). */
7
+ function formatSignalLine(issue, pid, result) {
8
+ switch (result.reason) {
9
+ case "sent":
10
+ return `Signaled PID ${pid} (SIGTERM) for #${issue}`;
11
+ case "cross-host":
12
+ return `Could not signal PID ${pid} for #${issue} (cross-host holder)`;
13
+ case "self-or-parent":
14
+ return `Refused to signal PID ${pid} for #${issue} (matches this process or its parent)`;
15
+ case "pid-dead":
16
+ return `Could not signal PID ${pid} for #${issue} (already exited)`;
17
+ case "kill-failed":
18
+ return `Could not signal PID ${pid} for #${issue} (kill syscall failed)`;
19
+ case "orchestrator":
20
+ return `Skipped signal for #${issue} (orchestrator mode)`;
21
+ }
22
+ }
23
+ function parseIssue(arg) {
24
+ const issue = Number.parseInt(arg, 10);
25
+ if (!Number.isInteger(issue) || issue <= 0) {
26
+ console.error(chalk.red(`Invalid issue number: ${arg}`));
27
+ process.exitCode = 1;
28
+ return null;
29
+ }
30
+ return issue;
31
+ }
32
+ /** `sequant locks list` — print every active lock with staleness metadata. */
33
+ export async function locksListCommand(options = {}) {
34
+ const manager = new LockManager();
35
+ if (manager.isNoop) {
36
+ if (options.json) {
37
+ console.log(JSON.stringify({ locks: [], orchestratorMode: true }));
38
+ }
39
+ else {
40
+ console.log(chalk.gray("Lock operations are disabled (SEQUANT_ORCHESTRATOR set)."));
41
+ }
42
+ return;
43
+ }
44
+ const listings = manager.list();
45
+ if (options.json) {
46
+ console.log(JSON.stringify({ locks: listings }, null, 2));
47
+ return;
48
+ }
49
+ if (listings.length === 0) {
50
+ console.log(chalk.gray("No active locks."));
51
+ return;
52
+ }
53
+ console.log(chalk.bold(`Active locks (${listings.length}):`));
54
+ console.log("");
55
+ for (const l of listings) {
56
+ const ageMinutes = Math.floor(l.ageMs / 60_000);
57
+ const staleTag = l.stale ? chalk.yellow(` (stale: ${l.staleReason})`) : "";
58
+ console.log(` #${l.issue} pid=${l.holder.pid} host=${l.holder.hostname} ` +
59
+ `age=${ageMinutes}m started=${l.holder.startedAt}${staleTag}`);
60
+ console.log(` command: ${l.holder.command}`);
61
+ }
62
+ }
63
+ /**
64
+ * `sequant locks clear <issue>` — remove a lock manually.
65
+ * By default refuses to clear a fresh same-host lock whose PID is alive;
66
+ * pass `--force` to override.
67
+ */
68
+ export async function locksClearCommand(issueArg, options = {}) {
69
+ const issue = Number.parseInt(issueArg, 10);
70
+ if (!Number.isInteger(issue) || issue <= 0) {
71
+ console.error(chalk.red(`Invalid issue number: ${issueArg}`));
72
+ process.exitCode = 1;
73
+ return;
74
+ }
75
+ const manager = new LockManager();
76
+ if (manager.isNoop) {
77
+ console.log(chalk.gray("Lock operations are disabled (SEQUANT_ORCHESTRATOR set)."));
78
+ return;
79
+ }
80
+ const result = manager.clearLock(issue, { safetyCheck: !options.force });
81
+ if (options.json) {
82
+ console.log(JSON.stringify({ issue, ...result }));
83
+ return;
84
+ }
85
+ if (result.cleared) {
86
+ console.log(chalk.green(`✓ Cleared lock for #${issue}`));
87
+ return;
88
+ }
89
+ switch (result.reason) {
90
+ case "no-lock":
91
+ console.log(chalk.gray(`No lock found for #${issue}`));
92
+ return;
93
+ case "fresh-same-host-alive": {
94
+ const holder = manager.check(issue);
95
+ console.log(chalk.yellow(`Refusing to clear fresh lock on #${issue}` +
96
+ (holder ? ` (PID ${holder.pid} appears alive on this host)` : "") +
97
+ `. Re-run with --force if you really want to clear it.`));
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+ default:
102
+ console.log(chalk.gray(`No-op (${result.reason})`));
103
+ }
104
+ }
105
+ /**
106
+ * `sequant locks acquire <issue>` — claim the lock from a shell context
107
+ * (e.g. a skill SKILL.md). Use `--skip-pid-check` for skill shells whose
108
+ * Node PID dies between acquire and release; stale recovery then falls back
109
+ * to age-only (2h).
110
+ *
111
+ * Exit codes:
112
+ * 0 — acquired
113
+ * 1 — locked by another holder (printed to stderr unless --json)
114
+ * 2 — invalid arguments
115
+ */
116
+ export async function locksAcquireCommand(issueArg, options = {}) {
117
+ const issue = parseIssue(issueArg);
118
+ if (issue === null)
119
+ return;
120
+ const command = options.command ?? "unknown";
121
+ const manager = new LockManager();
122
+ if (manager.isNoop) {
123
+ if (options.json) {
124
+ console.log(JSON.stringify({ acquired: true, orchestratorMode: true }));
125
+ }
126
+ else {
127
+ console.log(chalk.gray("Lock operations are disabled (SEQUANT_ORCHESTRATOR set)."));
128
+ }
129
+ return;
130
+ }
131
+ if (options.force) {
132
+ const { previous } = manager.forceAcquire(issue, command, {
133
+ skipPidCheck: options.skipPidCheck,
134
+ });
135
+ if (previous && options.signalOther) {
136
+ const result = manager.signalOther(previous);
137
+ if (!options.json) {
138
+ console.log(chalk.gray(formatSignalLine(issue, previous.pid, result)));
139
+ }
140
+ }
141
+ if (options.json) {
142
+ console.log(JSON.stringify({
143
+ acquired: true,
144
+ forced: true,
145
+ previousHolder: previous,
146
+ }));
147
+ }
148
+ else {
149
+ console.log(chalk.green(`✓ Acquired lock for #${issue} (forced)`));
150
+ }
151
+ return;
152
+ }
153
+ const result = manager.acquire(issue, command, {
154
+ skipPidCheck: options.skipPidCheck,
155
+ });
156
+ if (result.acquired) {
157
+ if (options.json) {
158
+ console.log(JSON.stringify({ acquired: true, lockPath: result.lockPath }));
159
+ }
160
+ else {
161
+ console.log(chalk.green(`✓ Acquired lock for #${issue}`));
162
+ }
163
+ return;
164
+ }
165
+ // Blocked.
166
+ process.exitCode = 1;
167
+ if (options.json) {
168
+ console.log(JSON.stringify({
169
+ acquired: false,
170
+ holder: result.holder,
171
+ lockPath: result.lockPath,
172
+ }));
173
+ }
174
+ else {
175
+ console.error(chalk.yellow(formatLockedMessage(issue, result.holder)));
176
+ }
177
+ }
178
+ /**
179
+ * `sequant locks release <issue>` — release a lock previously acquired by
180
+ * a skill shell on this host. Refuses to release locks held by a foreign
181
+ * host or by a different process (use `locks clear --force` for that).
182
+ */
183
+ export async function locksReleaseCommand(issueArg, options = {}) {
184
+ const issue = parseIssue(issueArg);
185
+ if (issue === null)
186
+ return;
187
+ const manager = new LockManager();
188
+ if (manager.isNoop) {
189
+ if (options.json) {
190
+ console.log(JSON.stringify({ issue, released: false, orchestratorMode: true }));
191
+ }
192
+ else {
193
+ console.log(chalk.gray("Lock operations are disabled (SEQUANT_ORCHESTRATOR set)."));
194
+ }
195
+ return;
196
+ }
197
+ const released = manager.releaseExternal(issue);
198
+ if (options.json) {
199
+ console.log(JSON.stringify({ issue, released }));
200
+ return;
201
+ }
202
+ if (released) {
203
+ console.log(chalk.green(`✓ Released lock for #${issue}`));
204
+ }
205
+ else {
206
+ console.log(chalk.gray(`No releasable lock for #${issue}`));
207
+ }
208
+ }
209
+ /**
210
+ * `sequant locks check <issue>` — read-only lock probe for `/assess`-style
211
+ * skills. Prints holder info if any, exit code 0 when free, 1 when held.
212
+ */
213
+ export async function locksCheckCommand(issueArg, options = {}) {
214
+ const issue = parseIssue(issueArg);
215
+ if (issue === null)
216
+ return;
217
+ const manager = new LockManager();
218
+ if (manager.isNoop) {
219
+ if (options.json) {
220
+ console.log(JSON.stringify({ locked: false, orchestratorMode: true }));
221
+ }
222
+ else {
223
+ console.log(chalk.gray("Lock operations are disabled (SEQUANT_ORCHESTRATOR set)."));
224
+ }
225
+ return;
226
+ }
227
+ const holder = manager.check(issue);
228
+ if (!holder) {
229
+ if (options.json) {
230
+ console.log(JSON.stringify({ issue, locked: false }));
231
+ }
232
+ else {
233
+ console.log(chalk.gray(`#${issue} is not locked`));
234
+ }
235
+ return;
236
+ }
237
+ process.exitCode = 1;
238
+ if (options.json) {
239
+ console.log(JSON.stringify({ issue, locked: true, holder }));
240
+ }
241
+ else {
242
+ console.log(chalk.yellow(formatLockedMessage(issue, holder)));
243
+ }
244
+ }
245
+ /**
246
+ * `sequant locks check-batch <issue1> <issue2> ...` — read-only batch probe
247
+ * used by `/assess`. Text mode emits one canonical warning line per held
248
+ * issue (nothing for unheld), so the skill can paste the output directly
249
+ * above its dashboard. JSON mode emits a structured `{ warnings: [...] }`.
250
+ *
251
+ * Exit code is always 0 — `/assess` is read-only and should never abort
252
+ * on locked issues; the warning is informational.
253
+ */
254
+ export async function locksCheckBatchCommand(issueArgs, options = {}) {
255
+ const issues = [];
256
+ for (const arg of issueArgs) {
257
+ const issue = Number.parseInt(arg, 10);
258
+ if (!Number.isInteger(issue) || issue <= 0) {
259
+ console.error(chalk.red(`Invalid issue number: ${arg}`));
260
+ process.exitCode = 2;
261
+ return;
262
+ }
263
+ issues.push(issue);
264
+ }
265
+ const manager = new LockManager();
266
+ if (manager.isNoop) {
267
+ if (options.json) {
268
+ console.log(JSON.stringify({ warnings: [], orchestratorMode: true, checked: 0 }));
269
+ }
270
+ // Non-JSON: silent in orchestrator mode (matches `acquire`/`release`
271
+ // semantics — no spurious output for /assess in MCP-driven runs).
272
+ return;
273
+ }
274
+ const warnings = [];
275
+ for (const issue of issues) {
276
+ const holder = manager.check(issue);
277
+ if (holder)
278
+ warnings.push({ issue, holder });
279
+ }
280
+ if (options.json) {
281
+ console.log(JSON.stringify({ warnings, checked: issues.length }, null, 2));
282
+ return;
283
+ }
284
+ // Text mode: one line per held issue (canonical `⚠` format that /assess
285
+ // pastes verbatim into its dashboard). Empty output when nothing is held.
286
+ for (const { issue, holder } of warnings) {
287
+ console.log(`⚠ #${issue} held by PID ${holder.pid} on ${holder.hostname} ` +
288
+ `since ${holder.startedAt} (${holder.command})`);
289
+ }
290
+ }
@@ -14,6 +14,7 @@
14
14
  import { spawnSync } from "child_process";
15
15
  import { ui, colors } from "../lib/cli-ui.js";
16
16
  import { runMergeChecks, formatReportMarkdown, } from "../lib/merge-check/index.js";
17
+ import { LockManager, formatLockedMessage } from "../lib/locks/index.js";
17
18
  /**
18
19
  * Determine exit code from batch verdict
19
20
  */
@@ -68,6 +69,16 @@ export async function mergeCommand(issues, options) {
68
69
  console.log(colors.muted(issueNumbers.length > 0
69
70
  ? `Checking issues: ${issueNumbers.map((i) => `#${i}`).join(", ")} (mode: ${mode})`
70
71
  : `Auto-detecting issues from most recent run (mode: ${mode})`));
72
+ // #625: read-only lock awareness — warn but proceed.
73
+ const lockManager = new LockManager();
74
+ if (!lockManager.isNoop) {
75
+ for (const issue of issueNumbers) {
76
+ const holder = lockManager.check(issue);
77
+ if (holder) {
78
+ console.log(colors.muted(` ! ${formatLockedMessage(issue, holder)}`));
79
+ }
80
+ }
81
+ }
71
82
  console.log("");
72
83
  }
73
84
  try {
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `sequant prompt <issue> "<message>"` — send a message into a running
3
+ * headless `sequant run` session via the interactive relay (#383).
4
+ */
5
+ import { type RelayMessageType } from "../lib/relay/types.js";
6
+ import type { IssueState } from "../lib/workflow/state-schema.js";
7
+ export interface PromptCommandOptions {
8
+ type?: string;
9
+ json?: boolean;
10
+ }
11
+ export interface ParsedPromptArgs {
12
+ issue: number | null;
13
+ message: string;
14
+ type: RelayMessageType;
15
+ }
16
+ /** Validate raw CLI args. Throws on invalid type or empty message. */
17
+ export declare function parseRelayPromptArgs(args: string[], options?: {
18
+ type?: string;
19
+ }): ParsedPromptArgs;
20
+ /** Identify candidate active runs for auto-resolution (AC-17b). */
21
+ export declare function findActiveIssues(issues: IssueState[], isAlive: (pid: number) => boolean, cwd?: string): number[];
22
+ /**
23
+ * Resolve which issue to target.
24
+ * - Explicit `issue` arg: use as-is.
25
+ * - Single active run: auto-resolve.
26
+ * - Zero active runs: error.
27
+ * - Multiple active runs: error with usage hint.
28
+ */
29
+ export declare function resolveTargetIssue(args: {
30
+ explicit: number | null;
31
+ activeIssues: number[];
32
+ }): {
33
+ issue: number;
34
+ reason: "explicit" | "single-active";
35
+ };
36
+ export declare function promptCommand(argsAndOptions: {
37
+ args: string[];
38
+ options: PromptCommandOptions;
39
+ }): Promise<void>;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * `sequant prompt <issue> "<message>"` — send a message into a running
3
+ * headless `sequant run` session via the interactive relay (#383).
4
+ */
5
+ import chalk from "chalk";
6
+ import { RelayMessageTypeSchema, } from "../lib/relay/types.js";
7
+ import { appendInboxMessage } from "../lib/relay/writer.js";
8
+ import { cleanupStalePid, readPidFile } from "../lib/relay/pid.js";
9
+ import { StateManager } from "../lib/workflow/state-manager.js";
10
+ /** Validate raw CLI args. Throws on invalid type or empty message. */
11
+ export function parseRelayPromptArgs(args, options = {}) {
12
+ // Allowed shapes:
13
+ // ["<message>"] — single arg, auto-resolve issue
14
+ // ["<issue>", "<message>"] — both positional
15
+ let issueArg;
16
+ let messageArg;
17
+ if (args.length === 1) {
18
+ messageArg = args[0];
19
+ }
20
+ else if (args.length >= 2) {
21
+ issueArg = args[0];
22
+ messageArg = args.slice(1).join(" ");
23
+ }
24
+ else {
25
+ throw new Error('Usage: sequant prompt [issue] "<message>" [--type TYPE]');
26
+ }
27
+ if (!messageArg || messageArg.trim() === "") {
28
+ throw new Error("Message cannot be empty");
29
+ }
30
+ let issue = null;
31
+ if (issueArg !== undefined) {
32
+ const n = Number.parseInt(issueArg, 10);
33
+ if (!Number.isInteger(n) || n <= 0) {
34
+ throw new Error(`Invalid issue number: '${issueArg}'`);
35
+ }
36
+ issue = n;
37
+ }
38
+ const rawType = options.type ?? "query";
39
+ const parsedType = RelayMessageTypeSchema.safeParse(rawType);
40
+ if (!parsedType.success) {
41
+ throw new Error(`Invalid --type '${rawType}'. Valid values: query, directive, abort.`);
42
+ }
43
+ return { issue, message: messageArg, type: parsedType.data };
44
+ }
45
+ /** Identify candidate active runs for auto-resolution (AC-17b). */
46
+ export function findActiveIssues(issues, isAlive, cwd = process.cwd()) {
47
+ const active = [];
48
+ for (const issue of issues) {
49
+ if (issue.status !== "in_progress")
50
+ continue;
51
+ const pid = readPidFile(issue.number, cwd);
52
+ if (pid !== null && isAlive(pid)) {
53
+ active.push(issue.number);
54
+ }
55
+ }
56
+ return active;
57
+ }
58
+ /**
59
+ * Resolve which issue to target.
60
+ * - Explicit `issue` arg: use as-is.
61
+ * - Single active run: auto-resolve.
62
+ * - Zero active runs: error.
63
+ * - Multiple active runs: error with usage hint.
64
+ */
65
+ export function resolveTargetIssue(args) {
66
+ if (args.explicit !== null) {
67
+ return { issue: args.explicit, reason: "explicit" };
68
+ }
69
+ if (args.activeIssues.length === 0) {
70
+ throw new Error("No active sequant runs found. Start one with `sequant run <issue>` first.");
71
+ }
72
+ if (args.activeIssues.length > 1) {
73
+ const list = args.activeIssues.map((n) => `#${n}`).join(", ");
74
+ throw new Error(`Multiple active runs: ${list}. Specify an issue number.\n` +
75
+ `Usage: sequant prompt ${args.activeIssues[0]} "your message"`);
76
+ }
77
+ return { issue: args.activeIssues[0], reason: "single-active" };
78
+ }
79
+ export async function promptCommand(argsAndOptions) {
80
+ const { args, options } = argsAndOptions;
81
+ const parsed = parseRelayPromptArgs(args, { type: options.type });
82
+ // Resolve target issue.
83
+ const stateManager = new StateManager();
84
+ let issueState;
85
+ let issueNumber;
86
+ if (parsed.issue !== null) {
87
+ issueNumber = parsed.issue;
88
+ issueState = await stateManager.getIssueState(issueNumber);
89
+ }
90
+ else {
91
+ const all = stateManager.stateExists()
92
+ ? Object.values(await stateManager.getAllIssueStates())
93
+ : [];
94
+ const { defaultIsPidAlive } = await import("../lib/locks/lock-manager.js");
95
+ const active = findActiveIssues(all, defaultIsPidAlive);
96
+ const target = resolveTargetIssue({
97
+ explicit: null,
98
+ activeIssues: active,
99
+ });
100
+ issueNumber = target.issue;
101
+ issueState = await stateManager.getIssueState(issueNumber);
102
+ }
103
+ // Liveness check — refuse to write to a dead session.
104
+ const cleanup = cleanupStalePid(issueNumber);
105
+ if (cleanup.warning) {
106
+ // Also reflect deactivation in state.
107
+ if (issueState?.relay) {
108
+ try {
109
+ await stateManager.setRelayState(issueNumber, null);
110
+ }
111
+ catch {
112
+ /* swallow */
113
+ }
114
+ }
115
+ if (options.json) {
116
+ console.log(JSON.stringify({
117
+ ok: false,
118
+ issue: issueNumber,
119
+ error: cleanup.warning,
120
+ }));
121
+ }
122
+ else {
123
+ console.error(chalk.yellow(cleanup.warning));
124
+ }
125
+ process.exitCode = 1;
126
+ return;
127
+ }
128
+ if (!cleanup.alive) {
129
+ const msg = `No relay PID found for #${issueNumber}. Is the run active?`;
130
+ if (options.json) {
131
+ console.log(JSON.stringify({ ok: false, issue: issueNumber, error: msg }));
132
+ }
133
+ else {
134
+ console.error(chalk.yellow(msg));
135
+ }
136
+ process.exitCode = 1;
137
+ return;
138
+ }
139
+ // Write to inbox (the worktree path is recorded in IssueState).
140
+ const message = appendInboxMessage(issueNumber, {
141
+ type: parsed.type,
142
+ ...(parsed.type === "abort" && parsed.message.trim() === ""
143
+ ? {}
144
+ : { message: parsed.message }),
145
+ }, { worktreePath: issueState?.worktree });
146
+ // Increment relay messageCount in state.
147
+ try {
148
+ await stateManager.incrementRelayMessageCount(issueNumber, 1);
149
+ }
150
+ catch {
151
+ /* swallow */
152
+ }
153
+ // Build confirmation with current phase + elapsed time.
154
+ const phase = issueState?.currentPhase ?? "unknown";
155
+ let elapsedSegment = "";
156
+ if (issueState?.relay?.startedAt) {
157
+ const ms = Date.now() - new Date(issueState.relay.startedAt).getTime();
158
+ elapsedSegment = `, ${formatElapsed(ms)} elapsed`;
159
+ }
160
+ const confirmation = `Message sent to #${issueNumber} (${phase} phase${elapsedSegment})`;
161
+ if (options.json) {
162
+ console.log(JSON.stringify({
163
+ ok: true,
164
+ issue: issueNumber,
165
+ messageId: message.id,
166
+ type: parsed.type,
167
+ phase,
168
+ }));
169
+ }
170
+ else {
171
+ console.log(chalk.green(confirmation));
172
+ }
173
+ }
174
+ function formatElapsed(ms) {
175
+ const totalSec = Math.max(0, Math.floor(ms / 1000));
176
+ const minutes = Math.floor(totalSec / 60);
177
+ const seconds = totalSec % 60;
178
+ return `${minutes}m ${seconds}s`;
179
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Display helpers for `sequant run` — pre-run config block + post-run summary.
3
+ *
4
+ * Kept separate from run.ts so the adapter stays thin (see AC-2 of #503).
5
+ *
6
+ * As of #618, the post-run summary delegates to the unified RunRenderer when
7
+ * one is provided. The renderless path (used by --experimental-tui and tests)
8
+ * falls back to `renderRunSummary` so output stays consistent across modes.
9
+ */
10
+ import type { RunRenderer } from "../lib/cli-ui/run-renderer-types.js";
11
+ import type { ResolvedRun, RunResult } from "../lib/workflow/run-orchestrator.js";
12
+ /**
13
+ * Print pre-run config block.
14
+ *
15
+ * Columnar alignment via 15-char label padding. Conditional rows only
16
+ * appear when non-default, matching the pre-#503 format.
17
+ */
18
+ export declare function displayConfig(r: ResolvedRun): void;
19
+ /**
20
+ * Print post-run summary: per-issue grid, log path, reflection, tips.
21
+ *
22
+ * If a renderer is provided (default path), delegate to its `renderSummary`
23
+ * so the live zone is torn down cleanly first. Otherwise, fall back to the
24
+ * shared `renderRunSummary` helper (used by tests and TUI mode).
25
+ */
26
+ export declare function displaySummary(result: RunResult, renderer?: RunRenderer | null): void;