mergestorm 0.3.9 → 0.3.13

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 (43) hide show
  1. package/README.md +57 -13
  2. package/dist/api.d.ts +27 -38
  3. package/dist/api.js +81 -4
  4. package/dist/client/index.d.ts +5 -0
  5. package/dist/client/index.js +5 -0
  6. package/dist/commands/credits.js +33 -12
  7. package/dist/commands/login.js +28 -7
  8. package/dist/commands/registry.d.ts +1 -1
  9. package/dist/commands/registry.js +26 -19
  10. package/dist/commands/review-client.d.ts +93 -0
  11. package/dist/commands/review-client.js +289 -0
  12. package/dist/commands/review.d.ts +30 -6
  13. package/dist/commands/review.js +385 -146
  14. package/dist/commands/stack.d.ts +34 -2
  15. package/dist/commands/stack.js +252 -58
  16. package/dist/commands/status.d.ts +16 -2
  17. package/dist/commands/status.js +221 -21
  18. package/dist/errors.d.ts +18 -2
  19. package/dist/errors.js +22 -1
  20. package/dist/git-stack.d.ts +2 -0
  21. package/dist/git-stack.js +4 -0
  22. package/dist/git.d.ts +12 -1
  23. package/dist/git.js +52 -7
  24. package/dist/shell.d.ts +1 -1
  25. package/dist/shell.js +6 -3
  26. package/dist/stack-dto.d.ts +185 -0
  27. package/dist/stack-dto.js +7 -0
  28. package/dist/submit-pr-base.d.ts +31 -0
  29. package/dist/submit-pr-base.js +69 -0
  30. package/dist/ui/banner.js +7 -0
  31. package/dist/ui/box.d.ts +13 -9
  32. package/dist/ui/box.js +28 -44
  33. package/dist/ui/findings.d.ts +2 -0
  34. package/dist/ui/findings.js +2 -1
  35. package/dist/ui/job-envelope.d.ts +68 -0
  36. package/dist/ui/job-envelope.js +35 -0
  37. package/dist/ui/prompt.d.ts +91 -0
  38. package/dist/ui/prompt.js +241 -62
  39. package/dist/ui/usage.d.ts +25 -1
  40. package/dist/ui/usage.js +50 -1
  41. package/dist/ui/width.d.ts +26 -0
  42. package/dist/ui/width.js +262 -0
  43. package/package.json +5 -1
@@ -1,28 +1,228 @@
1
- import { apiFetch } from "../api.js";
1
+ import { getReview } from "../api.js";
2
2
  import { loadConfig } from "../config.js";
3
- import { CommandError } from "../errors.js";
3
+ import { CommandError, DetachedError, REVIEW_EXIT, isCommandErrorCode, } from "../errors.js";
4
4
  import { ansi } from "../ui/ansi.js";
5
5
  import { renderFindings } from "../ui/findings.js";
6
- export async function cmdStatus(jobId, opts = {}) {
7
- const format = opts.format ?? "json";
8
- const cfg = await loadConfig();
9
- const { status, body } = await apiFetch(cfg, `/api/v1/reviews/${jobId}`, {
10
- signal: opts.signal,
6
+ import { toReviewJobEnvelope, } from "../ui/job-envelope.js";
7
+ import { REVIEW_POLL_DEFAULT_TIMEOUT_MS, ReviewPollTimeoutError, pollReview, } from "./review-client.js";
8
+ const STATUS_USAGE = "usage: mergestorm status <job_id> [--json] [--wait] [--timeout seconds]";
9
+ function usageError(message = STATUS_USAGE) {
10
+ return new CommandError(message, REVIEW_EXIT.usage, "usage");
11
+ }
12
+ export function parseStatusArgs(args, defaults = { format: "json" }) {
13
+ let jobId;
14
+ let format = defaults.format;
15
+ let wait = false;
16
+ let timeoutMs = REVIEW_POLL_DEFAULT_TIMEOUT_MS;
17
+ for (let i = 0; i < args.length; i += 1) {
18
+ const a = args[i];
19
+ if (a === "--json") {
20
+ format = "json";
21
+ continue;
22
+ }
23
+ if (a === "--pretty") {
24
+ format = "pretty";
25
+ continue;
26
+ }
27
+ if (a === "--wait") {
28
+ wait = true;
29
+ continue;
30
+ }
31
+ if (a === "--no-wait") {
32
+ wait = false;
33
+ continue;
34
+ }
35
+ if (a === "--timeout" || a.startsWith("--timeout=")) {
36
+ const value = a === "--timeout" ? args[++i] : a.slice("--timeout=".length);
37
+ const seconds = Number(value);
38
+ if (!value || !Number.isFinite(seconds) || seconds <= 0) {
39
+ throw usageError("--timeout must be a positive number of seconds");
40
+ }
41
+ timeoutMs = Math.round(seconds * 1_000);
42
+ wait = true;
43
+ continue;
44
+ }
45
+ if (a.startsWith("-")) {
46
+ throw usageError(`unknown flag: ${a}`);
47
+ }
48
+ if (jobId)
49
+ throw usageError();
50
+ jobId = a;
51
+ }
52
+ if (!jobId)
53
+ throw usageError();
54
+ return { jobId, format, wait, timeoutMs };
55
+ }
56
+ function printPretty(row) {
57
+ const envelope = toReviewJobEnvelope(row);
58
+ console.log(`Status: ${envelope.status}`);
59
+ if (envelope.verdict)
60
+ console.log(`Verdict: ${ansi.bold(envelope.verdict)}`);
61
+ if (envelope.router_mode)
62
+ console.log(`Router: ${envelope.router_mode}`);
63
+ if (envelope.specialists_requested.length > 0) {
64
+ console.log(`Requested: ${envelope.specialists_requested.join(", ")}`);
65
+ }
66
+ if (envelope.specialists_run.length > 0) {
67
+ console.log(`Run: ${envelope.specialists_run.join(", ")}`);
68
+ }
69
+ if (envelope.credits) {
70
+ console.log(`Credits: ${envelope.credits.standard} standard`);
71
+ }
72
+ if (envelope.summary)
73
+ console.log(envelope.summary);
74
+ if (envelope.error)
75
+ console.log(ansi.red(`Error: ${envelope.error}`));
76
+ renderFindings(envelope.findings, {
77
+ emptyMessage: envelope.status === "completed",
78
+ });
79
+ }
80
+ export async function cmdStatus(args, opts = {}) {
81
+ const parsed = parseStatusArgs(args, {
82
+ format: opts.defaultFormat ?? "json",
11
83
  });
12
- if (status !== 200) {
13
- throw new CommandError(JSON.stringify(body, null, 2));
84
+ const json = parsed.format === "json";
85
+ const interactive = Boolean(opts.interactive && !json);
86
+ const fallback = { jobId: parsed.jobId };
87
+ let jsonPrinted = false;
88
+ const printJson = (row, extra = {}) => {
89
+ console.log(JSON.stringify(toReviewJobEnvelope(row, { ...fallback, ...extra })));
90
+ jsonPrinted = true;
91
+ };
92
+ try {
93
+ const cfg = await loadConfig();
94
+ if (!parsed.wait) {
95
+ let body;
96
+ try {
97
+ body = await getReview(parsed.jobId, cfg, { signal: opts.signal });
98
+ }
99
+ catch (err) {
100
+ if (err instanceof CommandError &&
101
+ (err.code === "auth_invalid" || err.code === "missing_api_key")) {
102
+ throw err;
103
+ }
104
+ if (err instanceof Error && err.name === "AbortError")
105
+ throw err;
106
+ if (isCommandErrorCode(err, "api_timeout")) {
107
+ if (json)
108
+ printJson(null, { status: "in_progress", error: err.message });
109
+ throw new CommandError(err.message, REVIEW_EXIT.timeout, "review_timeout");
110
+ }
111
+ if (isCommandErrorCode(err, "rate_limited")) {
112
+ if (json) {
113
+ printJson(null, {
114
+ status: "rate_limited",
115
+ error: err.message,
116
+ retryAfterSeconds: err.retryAfterSeconds,
117
+ });
118
+ }
119
+ else {
120
+ console.log(`Status: rate_limited`);
121
+ console.log(err.message);
122
+ }
123
+ throw err;
124
+ }
125
+ throw new CommandError(err instanceof Error ? err.message : String(err), REVIEW_EXIT.failed, "review_failed");
126
+ }
127
+ const row = body && typeof body === "object"
128
+ ? { ...body, job_id: parsed.jobId }
129
+ : { job_id: parsed.jobId, status: "failed", error: "Invalid review response" };
130
+ if (row.status === "failed" || row.status === "quota_exceeded") {
131
+ if (json)
132
+ printJson(row);
133
+ else
134
+ printPretty(row);
135
+ throw new CommandError(`Review ${row.status}: ${row.error ?? ""}`, row.status === "quota_exceeded" ? REVIEW_EXIT.quota : REVIEW_EXIT.failed, row.status === "quota_exceeded" ? "review_quota" : "review_failed");
136
+ }
137
+ if (json)
138
+ printJson(row);
139
+ else
140
+ printPretty(row);
141
+ return;
142
+ }
143
+ if (!json) {
144
+ const waitHint = interactive
145
+ ? ` Waiting… (${ansi.dim("ctrl+c to detach — job keeps running")})`
146
+ : " Waiting…";
147
+ console.log(`Job ${parsed.jobId}.${waitHint}`);
148
+ }
149
+ let row;
150
+ try {
151
+ row = await pollReview(cfg, parsed.jobId, {
152
+ timeoutMs: parsed.timeoutMs,
153
+ intervalMs: opts.pollIntervalMs,
154
+ signal: opts.signal,
155
+ propagateTimeout: interactive,
156
+ onTick(kind) {
157
+ if (json)
158
+ console.error(kind === "retry" ? "?" : ".");
159
+ else
160
+ process.stdout.write(kind === "retry" ? "?" : ".");
161
+ },
162
+ });
163
+ }
164
+ catch (err) {
165
+ if (opts.interactive && err instanceof Error && err.name === "AbortError") {
166
+ throw new DetachedError(parsed.jobId);
167
+ }
168
+ if (interactive && isCommandErrorCode(err, "api_timeout")) {
169
+ throw new DetachedError(parsed.jobId);
170
+ }
171
+ if (err instanceof ReviewPollTimeoutError) {
172
+ const message = `Timed out waiting for review ${parsed.jobId}`;
173
+ if (json) {
174
+ printJson(err.lastRow, {
175
+ jobId: parsed.jobId,
176
+ status: err.lastRow.status ?? "in_progress",
177
+ error: "Timed out waiting for review.",
178
+ });
179
+ }
180
+ throw new CommandError(message, REVIEW_EXIT.timeout, "review_timeout");
181
+ }
182
+ if (isCommandErrorCode(err, "rate_limited")) {
183
+ if (json) {
184
+ printJson(null, {
185
+ status: "rate_limited",
186
+ error: err.message,
187
+ retryAfterSeconds: err.retryAfterSeconds,
188
+ });
189
+ }
190
+ else {
191
+ console.log(`Status: rate_limited`);
192
+ console.log(err.message);
193
+ }
194
+ throw err;
195
+ }
196
+ throw err;
197
+ }
198
+ if (!json)
199
+ console.log("");
200
+ if (row.status === "failed" || row.status === "quota_exceeded") {
201
+ const message = `Review ${row.status}: ${row.error ?? ""}`;
202
+ if (json)
203
+ printJson(row);
204
+ else
205
+ printPretty(row);
206
+ throw new CommandError(message, row.status === "quota_exceeded" ? REVIEW_EXIT.quota : REVIEW_EXIT.failed, row.status === "quota_exceeded" ? "review_quota" : "review_failed");
207
+ }
208
+ if (json)
209
+ printJson(row);
210
+ else
211
+ printPretty(row);
14
212
  }
15
- if (format === "json") {
16
- console.log(JSON.stringify(body, null, 2));
17
- return;
213
+ catch (err) {
214
+ if (err instanceof DetachedError)
215
+ throw err;
216
+ if (json &&
217
+ !jsonPrinted &&
218
+ !(err instanceof Error && err.name === "AbortError")) {
219
+ const message = err instanceof Error ? err.message : String(err);
220
+ printJson(null, { status: "failed", error: message });
221
+ }
222
+ if (err instanceof CommandError &&
223
+ (err.code === "auth_invalid" || err.code === "missing_api_key")) {
224
+ throw new CommandError(err.message, REVIEW_EXIT.auth, err.code);
225
+ }
226
+ throw err;
18
227
  }
19
- const row = body;
20
- console.log(`Status: ${row.status}`);
21
- if (row.verdict)
22
- console.log(`Verdict: ${ansi.bold(row.verdict)}`);
23
- if (row.summary)
24
- console.log(row.summary);
25
- if (row.error)
26
- console.log(ansi.red(`Error: ${row.error}`));
27
- renderFindings(row.findings, { emptyMessage: row.status === "completed" });
28
228
  }
package/dist/errors.d.ts CHANGED
@@ -2,13 +2,29 @@
2
2
  * Stable machine codes for CommandError control flow.
3
3
  * UX copy in `message` may change freely; match on `code`, never on message text.
4
4
  */
5
- export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "missing_api_key";
5
+ export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited";
6
+ /** Stable one-shot exit codes for machine callers of `mergestorm review`. */
7
+ export declare const REVIEW_EXIT: {
8
+ readonly success: 0;
9
+ readonly usage: 2;
10
+ readonly quota: 3;
11
+ readonly failed: 4;
12
+ readonly timeout: 5;
13
+ readonly auth: 6;
14
+ readonly rate_limited: 7;
15
+ };
16
+ export type CommandErrorExtras = {
17
+ retryAfterSeconds?: number;
18
+ };
6
19
  /** Thrown by command handlers; one-shot mode maps `exitCode` onto process.exitCode. */
7
20
  export declare class CommandError extends Error {
8
21
  readonly exitCode: number;
9
22
  readonly code?: CommandErrorCode;
10
- constructor(message: string, exitCode?: number, code?: CommandErrorCode);
23
+ readonly retryAfterSeconds?: number;
24
+ constructor(message: string, exitCode?: number, code?: CommandErrorCode, extras?: CommandErrorExtras);
11
25
  }
26
+ export declare const RATE_LIMITED_HINT = "Rate limited. Wait for a running review or mergestorm jobs.";
27
+ export declare function rateLimitedMessage(retryAfterSeconds?: number): string;
12
28
  export declare function isCommandErrorCode(err: unknown, code: CommandErrorCode): err is CommandError;
13
29
  /** Review/status poll aborted by Ctrl+C — shell returns to the prompt. */
14
30
  export declare class DetachedError extends Error {
package/dist/errors.js CHANGED
@@ -1,14 +1,35 @@
1
+ /** Stable one-shot exit codes for machine callers of `mergestorm review`. */
2
+ export const REVIEW_EXIT = {
3
+ success: 0,
4
+ usage: 2,
5
+ quota: 3,
6
+ failed: 4,
7
+ timeout: 5,
8
+ auth: 6,
9
+ rate_limited: 7,
10
+ };
1
11
  /** Thrown by command handlers; one-shot mode maps `exitCode` onto process.exitCode. */
2
12
  export class CommandError extends Error {
3
13
  exitCode;
4
14
  code;
5
- constructor(message, exitCode = 1, code) {
15
+ retryAfterSeconds;
16
+ constructor(message, exitCode = 1, code, extras) {
6
17
  super(message);
7
18
  this.name = "CommandError";
8
19
  this.exitCode = exitCode;
9
20
  this.code = code;
21
+ if (extras?.retryAfterSeconds !== undefined) {
22
+ this.retryAfterSeconds = extras.retryAfterSeconds;
23
+ }
10
24
  }
11
25
  }
26
+ export const RATE_LIMITED_HINT = "Rate limited. Wait for a running review or mergestorm jobs.";
27
+ export function rateLimitedMessage(retryAfterSeconds) {
28
+ if (retryAfterSeconds == null || !Number.isFinite(retryAfterSeconds)) {
29
+ return RATE_LIMITED_HINT;
30
+ }
31
+ return `${RATE_LIMITED_HINT} Retry after ${Math.ceil(retryAfterSeconds)}s.`;
32
+ }
12
33
  export function isCommandErrorCode(err, code) {
13
34
  return err instanceof CommandError && err.code === code;
14
35
  }
@@ -23,6 +23,8 @@ export declare function defaultLayerBranchName(explicit?: string): string;
23
23
  * (`git rev-list --count base..head`). Used by stack submit to refuse empty layers.
24
24
  */
25
25
  export declare function commitsAheadOf(base: string, head: string, cwd?: string): number;
26
+ /** Fetch one remote branch so submit can count commits ahead of a park freeze. */
27
+ export declare function fetchRemoteBranch(name: string, cwd?: string): void;
26
28
  /** Push branch to origin and set upstream. */
27
29
  export declare function pushBranch(branch: string, cwd?: string): void;
28
30
  /** First line of the tip commit on `branch` (for PR title). */
package/dist/git-stack.js CHANGED
@@ -107,6 +107,10 @@ export function commitsAheadOf(base, head, cwd = process.cwd()) {
107
107
  }
108
108
  return n;
109
109
  }
110
+ /** Fetch one remote branch so submit can count commits ahead of a park freeze. */
111
+ export function fetchRemoteBranch(name, cwd = process.cwd()) {
112
+ git(["fetch", "origin", name], cwd);
113
+ }
110
114
  /** Push branch to origin and set upstream. */
111
115
  export function pushBranch(branch, cwd = process.cwd()) {
112
116
  try {
package/dist/git.d.ts CHANGED
@@ -4,7 +4,18 @@ export declare function parseGithubOriginRepo(cwd?: string): {
4
4
  owner: string;
5
5
  repo: string;
6
6
  } | null;
7
- export declare function collectChangedFiles(base: string, head: string, cwd?: string): Promise<{
7
+ /**
8
+ * Repo-root absolute path for the current working tree.
9
+ * Kept local (not imported from repo-identity) to avoid a git ↔ repo-identity cycle.
10
+ */
11
+ export declare function repoRoot(cwd?: string): string;
12
+ /**
13
+ * Repo-root-relative names of files changed between base and head that resolve
14
+ * inside the sandbox root. Bounds both the uploaded file contents and the diff
15
+ * itself to the sandbox so tracked files outside it never reach the API.
16
+ */
17
+ export declare function changedNamesInSandbox(base: string, head: string, cwd?: string, sandboxRoot?: string): Promise<string[]>;
18
+ export declare function collectChangedFiles(base: string, head: string, cwd?: string, sandboxRoot?: string): Promise<{
8
19
  path: string;
9
20
  content: string;
10
21
  }[]>;
package/dist/git.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { realpathSync } from "node:fs";
3
- import { readFile, stat } from "node:fs/promises";
3
+ import { readFile, realpath, stat } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  export function git(args, cwd = process.cwd()) {
6
6
  try {
@@ -33,15 +33,61 @@ export function parseGithubOriginRepo(cwd = process.cwd()) {
33
33
  * Repo-root absolute path for the current working tree.
34
34
  * Kept local (not imported from repo-identity) to avoid a git ↔ repo-identity cycle.
35
35
  */
36
- function repoRoot(cwd = process.cwd()) {
36
+ export function repoRoot(cwd = process.cwd()) {
37
37
  return realpathSync(git(["rev-parse", "--show-toplevel"], cwd).trim());
38
38
  }
39
- export async function collectChangedFiles(base, head, cwd = process.cwd()) {
39
+ /**
40
+ * Repo-root-relative names of files changed between base and head that resolve
41
+ * inside the sandbox root. Bounds both the uploaded file contents and the diff
42
+ * itself to the sandbox so tracked files outside it never reach the API.
43
+ */
44
+ export async function changedNamesInSandbox(base, head, cwd = process.cwd(), sandboxRoot) {
40
45
  const root = repoRoot(cwd);
41
- const names = git(["diff", "--name-only", "--diff-filter=ACMR", `${base}...${head}`], cwd)
42
- .split("\n")
43
- .map((s) => s.trim())
46
+ const bound = sandboxRoot ? await realpath(path.resolve(sandboxRoot)) : root;
47
+ // D/T are included so deletions and type changes stay in the review diff,
48
+ // even though their contents cannot be read from the working tree for upload.
49
+ // -z disables core.quotePath quoting and NUL-terminates names, so paths with
50
+ // spaces or non-ASCII characters survive the split verbatim.
51
+ const names = git(["diff", "--name-only", "-z", "--diff-filter=ACMRDT", `${base}...${head}`], cwd)
52
+ .split("\0")
44
53
  .filter(Boolean);
54
+ const kept = [];
55
+ for (const name of names) {
56
+ const abs = path.join(root, name);
57
+ let real;
58
+ try {
59
+ real = await realpath(abs);
60
+ }
61
+ catch {
62
+ // Deleted from the working tree: resolve the nearest existing ancestor so
63
+ // the path can still be bounded against the sandbox for the diff pathspec.
64
+ let ancestor = path.dirname(abs);
65
+ let realAncestor;
66
+ while (!realAncestor) {
67
+ try {
68
+ realAncestor = await realpath(ancestor);
69
+ }
70
+ catch {
71
+ const up = path.dirname(ancestor);
72
+ if (up === ancestor)
73
+ throw new Error(`cannot resolve ${abs}`);
74
+ ancestor = up;
75
+ }
76
+ }
77
+ real = path.join(realAncestor, path.relative(ancestor, abs));
78
+ }
79
+ const relToBound = path.relative(bound, real);
80
+ if (relToBound === ".." || relToBound.startsWith(`..${path.sep}`)) {
81
+ console.warn(`Warning: skipping ${name} for review upload: it resolves outside the ${sandboxRoot ? "sandbox root" : "repo root"}`);
82
+ continue;
83
+ }
84
+ kept.push(name);
85
+ }
86
+ return kept;
87
+ }
88
+ export async function collectChangedFiles(base, head, cwd = process.cwd(), sandboxRoot) {
89
+ const root = repoRoot(cwd);
90
+ const names = await changedNamesInSandbox(base, head, cwd, sandboxRoot);
45
91
  const files = [];
46
92
  for (const name of names.slice(0, 40)) {
47
93
  const abs = path.join(root, name);
@@ -53,7 +99,6 @@ export async function collectChangedFiles(base, head, cwd = process.cwd()) {
53
99
  files.push({ path: name, content });
54
100
  }
55
101
  catch {
56
- // deleted or unreadable — do not silently drop without a hint
57
102
  console.warn(`Warning: could not read ${name} for review upload (resolved from repo root); skipping file contents`);
58
103
  }
59
104
  }
package/dist/shell.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** Shell autocomplete + `/help` — derived from the command registry (STRUCT-01). */
1
+ /** Shell autocomplete + `/help` � derived from the command registry (STRUCT-01). */
2
2
  export declare const COMMANDS: import("./ui/prompt.js").CommandSpec[];
3
3
  /** Strips an optional leading "/" so both "review" and "/review" work. */
4
4
  export declare function parseLine(line: string): {
package/dist/shell.js CHANGED
@@ -5,11 +5,14 @@ import { dispatchCommand, shellCommandSpecs, } from "./commands/registry.js";
5
5
  import { ansi } from "./ui/ansi.js";
6
6
  import { printBannerHeader, shellPrompt } from "./ui/banner.js";
7
7
  import { CTRL_C_EXIT_HINT, CtrlCExitGate } from "./ui/ctrl-c-exit.js";
8
- import { askLine, PromptClosedError } from "./ui/prompt.js";
9
- /** Shell autocomplete + `/help` — derived from the command registry (STRUCT-01). */
8
+ import { askLine, formatSlashLabel, PromptClosedError } from "./ui/prompt.js";
9
+ /** Shell autocomplete + `/help` � derived from the command registry (STRUCT-01). */
10
10
  export const COMMANDS = shellCommandSpecs();
11
11
  function printHelp() {
12
- const rows = COMMANDS.map((c) => ` ${ansi.green("/" + c.name.padEnd(10))} ${c.summary}`).join("\n");
12
+ const rows = COMMANDS.map((c) => {
13
+ const label = formatSlashLabel(c).slice(1);
14
+ return ` ${ansi.green("/" + label.padEnd(20))} ${c.summary}`;
15
+ }).join("\n");
13
16
  console.log(`
14
17
  ${ansi.bold("Commands")}
15
18
  ${rows}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Wire types returned by the stack list/enrich HTTP endpoints.
3
+ *
4
+ * `api/src/stacks.ts#loadStackDtos` is the producer. Keep this file
5
+ * dependency-free and byte-for-byte aligned with the canonical wire contract.
6
+ */
7
+ export type StackBranchState = "clean" | "needs_restack" | "restacking" | "conflict" | "merged" | "closed";
8
+ export type StackCiStatus = "success" | "pending" | "failure" | "neutral" | "none" | "unknown";
9
+ export type StackReviewStatus = "approved" | "changes_requested" | "reviewed" | "failed" | "none" | "unknown";
10
+ /** `failed` is the newest Vortex `pr_reviews` row. Work paints Review failed. */
11
+ export type StackVortexStatus = "reviewing" | "seam_pending" | "all_clear" | "findings" | "throttled" | "failed";
12
+ export type StackCycloneStatus = "patching" | "awaiting_fix";
13
+ export type StackTempestStatus = "reviewing" | "findings" | "clear" | "failed" | "stale";
14
+ export type StackAgentName = "vortex" | "cyclone" | "tempest";
15
+ /**
16
+ * One live-or-resting agent run for a PR (#1119): a dumb projection of one
17
+ * ledger row (webhook job / lease / review-state / pr_reviews). Two live
18
+ * Vortex jobs on two SHAs → two runs → two chips. Carries only safe facts —
19
+ * never job ids, holders, or error strings.
20
+ */
21
+ export type StackAgentRun = {
22
+ agent: StackAgentName;
23
+ /** Agent-specific status, e.g. "reviewing" / "patching" / "all_clear". */
24
+ status: string;
25
+ /** Full SHA when known. UI shortens to 7. */
26
+ sha: string | null;
27
+ startedAt?: string | null;
28
+ finishedAt?: string | null;
29
+ /**
30
+ * Vortex in-flight sub-state from pr_reviews.phase (#1189): "fanout" while
31
+ * Core + specialists run in parallel, "synthesizing" during consolidation.
32
+ * Only on live `reviewing` runs; unknown values fall back to plain Reviewing.
33
+ */
34
+ phase?: string | null;
35
+ /**
36
+ * Actionable finding count from the latest `pr_reviews.findings` payload
37
+ * (#1248). Info-only notes (Continue, Decision required) are excluded.
38
+ * Only on resting Vortex `findings` runs.
39
+ */
40
+ findingCount?: number | null;
41
+ };
42
+ export type StackLayerChecks = {
43
+ total: number;
44
+ success: number;
45
+ pending: number;
46
+ failure: number;
47
+ failingName: string | null;
48
+ };
49
+ /** Closing / Development-sidebar issue. Cap 3 on the wire. */
50
+ export type StackLinkedIssue = {
51
+ number: number;
52
+ title: string | null;
53
+ };
54
+ /** Cyclone merge-conflict specialist resolution for this PR (#973). */
55
+ export type StackLayerConflictResolution = {
56
+ resolvedAt: string;
57
+ /** Branch the conflicts were resolved against (stacks parent or GitHub base). */
58
+ baseBranch: string | null;
59
+ /** Conflicted paths the specialist resolved; the writer caps the list. */
60
+ files: string[];
61
+ /** Head commit of the pushed resolution, when known. */
62
+ headSha: string | null;
63
+ /**
64
+ * Chat dispatch that triggered the run (#973). Traces a mark to the job that
65
+ * pushed it; null when the row predates dispatch tracing (or had no job).
66
+ */
67
+ dispatchId: string | null;
68
+ };
69
+ export type StackLayerDto = {
70
+ branch: string;
71
+ parentBranch: string | null;
72
+ prNumber: number;
73
+ position: number;
74
+ state: StackBranchState;
75
+ title: string | null;
76
+ htmlUrl: string | null;
77
+ ciStatus: StackCiStatus;
78
+ reviewStatus: StackReviewStatus;
79
+ checks: StackLayerChecks | null;
80
+ /** null on the fast list or when no Vortex/seam signal exists. */
81
+ vortexStatus: StackVortexStatus | null;
82
+ /** null on the fast list or when no Cyclone signal exists. */
83
+ cycloneStatus: StackCycloneStatus | null;
84
+ /** null when no Tempest signal exists. Fast list and enrich both paint this from pr_reviews (#876). */
85
+ tempestStatus: StackTempestStatus | null;
86
+ /**
87
+ * Ledger-backed agent runs, one per live row (#1119). Fast list and enrich
88
+ * both paint this from our tables only — never GitHub. The per-agent status
89
+ * fields above stay as a rollup of the newest run for older clients.
90
+ * Undefined = producer predates runs (clients fall back to the rollup).
91
+ */
92
+ agentRuns?: StackAgentRun[];
93
+ conflictDetail: string | null;
94
+ lastRestackedSha: string | null;
95
+ /** GitHub PR mergeability from enrich; null when unknown. */
96
+ mergeable: boolean | null;
97
+ /** GitHub REST mergeable_state; null when unknown. */
98
+ mergeableState: string | null;
99
+ /**
100
+ * Vortex-style linked issues (keywords + Development sidebar), cap 3.
101
+ * null until first enrich finds any; kept on merged unit members (unlike CI).
102
+ */
103
+ linkedIssues?: StackLinkedIssue[] | null;
104
+ /**
105
+ * Specialist lanes Vortex launched on this PR (smart router / Manual).
106
+ * Fast list and enrich both fill from vortex_pr_review_state (#896 / #915).
107
+ * Durable after merge — terminal layers keep the last run so Work can grey
108
+ * the marks on a done-growing unit member instead of dropping them (#1021).
109
+ */
110
+ specialistsRun?: string[] | null;
111
+ /** Published invocation count per specialist lane. Frontend paint follows separately. */
112
+ specialistRunCounts?: Record<string, number> | null;
113
+ /** Number of published reviews Core participated in for this PR. */
114
+ coreRuns?: number | null;
115
+ /**
116
+ * Cyclone's merge-conflict specialist resolved this PR (#973). Fast list and
117
+ * enrich both paint from cyclone_conflict_resolutions; null on terminal
118
+ * layers and when the specialist never ran.
119
+ */
120
+ conflictResolution?: StackLayerConflictResolution | null;
121
+ };
122
+ export type StackUnitMemberDto = {
123
+ prNumber: number;
124
+ branch: string;
125
+ position: number;
126
+ seamState: string;
127
+ seamReviewedSha: string | null;
128
+ promotedHeadSha: string | null;
129
+ promotedAt: string;
130
+ /**
131
+ * GitHub PR title snapshotted at promote / enrich (#965).
132
+ * Survives stack_branches dropping the open-chain row. null until first write.
133
+ */
134
+ title?: string | null;
135
+ /**
136
+ * Linked issues snapshotted onto the member at promote / enrich (#936).
137
+ * Survives stack_branches dropping the open-chain row. null until first write.
138
+ */
139
+ linkedIssues?: StackLinkedIssue[] | null;
140
+ /**
141
+ * Vortex specialist lanes that ran on this PR (#1021). Painted from
142
+ * vortex_pr_review_state on the fast list — the open layer is gone after
143
+ * promote, so the member must carry the marks for the greyed-out row.
144
+ */
145
+ specialistsRun?: string[] | null;
146
+ /** Published invocation count per specialist lane. */
147
+ specialistRunCounts?: Record<string, number> | null;
148
+ /** Number of published reviews Core participated in for this PR. */
149
+ coreRuns?: number | null;
150
+ };
151
+ export type StackUnitDto = {
152
+ id: string;
153
+ uNumber: number;
154
+ state: string;
155
+ branch: string;
156
+ landTarget: string;
157
+ members: StackUnitMemberDto[];
158
+ landPrNumber: number | null;
159
+ tempestLandStatus: string | null;
160
+ landingBlockReason: string | null;
161
+ /**
162
+ * Land-PR chrome. Fast list hydrates last-known CI/title/mergeable/issues from
163
+ * `review_units` (#878 / #892) and may attach a tempest-only stub (#876). Enrich
164
+ * refreshes GitHub and writes back. Null when the land PR is an open stack
165
+ * layer (chips live on `layers`) or the unit has no land PR.
166
+ */
167
+ landPr: StackLayerDto | null;
168
+ };
169
+ export type StackDto = {
170
+ id: string;
171
+ owner: string;
172
+ repo: string;
173
+ trunkBranch: string;
174
+ /** Real trunk the review unit eventually lands into. */
175
+ landTarget: string;
176
+ autoPromoteWhenGreen: boolean;
177
+ /**
178
+ * Dashboard-only archive (#963): set hides the stack from the active Work
179
+ * list; GitHub PRs stay open. Only PATCH /stacks/:id toggles it.
180
+ */
181
+ archivedAt: string | null;
182
+ layers: StackLayerDto[];
183
+ /** Present when a review-unit row exists for this stack. */
184
+ unit?: StackUnitDto;
185
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Wire types returned by the stack list/enrich HTTP endpoints.
3
+ *
4
+ * `api/src/stacks.ts#loadStackDtos` is the producer. Keep this file
5
+ * dependency-free and byte-for-byte aligned with the canonical wire contract.
6
+ */
7
+ export {};