mergestorm 0.3.64 → 0.3.66

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 (77) hide show
  1. package/README.md +28 -9
  2. package/dist/api.d.ts +63 -11
  3. package/dist/api.js +195 -6
  4. package/dist/automation-catalog.d.ts +5 -1
  5. package/dist/automation-catalog.js +1 -0
  6. package/dist/cli.js +21 -19
  7. package/dist/client/index.d.ts +3 -2
  8. package/dist/client/index.js +3 -2
  9. package/dist/commands/account.d.ts +3 -0
  10. package/dist/commands/account.js +75 -0
  11. package/dist/commands/browse.d.ts +1 -9
  12. package/dist/commands/browse.js +10 -8
  13. package/dist/commands/dismiss.d.ts +0 -2
  14. package/dist/commands/dismiss.js +2 -2
  15. package/dist/commands/help.js +3 -2
  16. package/dist/commands/issue.d.ts +24 -6
  17. package/dist/commands/issue.js +153 -31
  18. package/dist/commands/login.d.ts +2 -1
  19. package/dist/commands/login.js +50 -14
  20. package/dist/commands/logout.js +6 -7
  21. package/dist/commands/memory.d.ts +2 -3
  22. package/dist/commands/memory.js +8 -6
  23. package/dist/commands/registry.d.ts +1 -0
  24. package/dist/commands/registry.js +35 -11
  25. package/dist/commands/review-client.d.ts +0 -3
  26. package/dist/commands/review-client.js +3 -3
  27. package/dist/commands/review.d.ts +1 -1
  28. package/dist/commands/review.js +1 -1
  29. package/dist/commands/settings.d.ts +3 -5
  30. package/dist/commands/settings.js +4 -3
  31. package/dist/commands/stack.d.ts +19 -5
  32. package/dist/commands/stack.js +78 -4
  33. package/dist/commands/whoami.js +14 -10
  34. package/dist/config.d.ts +55 -2
  35. package/dist/config.js +264 -13
  36. package/dist/errors.d.ts +1 -1
  37. package/dist/gh.d.ts +0 -4
  38. package/dist/gh.js +4 -4
  39. package/dist/git.d.ts +1 -1
  40. package/dist/git.js +2 -2
  41. package/dist/memory-api.d.ts +1 -0
  42. package/dist/memory-api.js +2 -2
  43. package/dist/node-version.d.ts +0 -2
  44. package/dist/node-version.js +1 -1
  45. package/dist/pr-body.d.ts +4 -0
  46. package/dist/pr-body.js +15 -0
  47. package/dist/repo-identity.d.ts +2 -1
  48. package/dist/repo-identity.js +8 -4
  49. package/dist/server-publish.d.ts +1 -24
  50. package/dist/server-publish.js +9 -9
  51. package/dist/stack-blockers.d.ts +12 -38
  52. package/dist/stack-blockers.js +55 -33
  53. package/dist/stack-dto.d.ts +3 -0
  54. package/dist/stack-dto.js +15 -2
  55. package/dist/stack-meta.d.ts +2 -2
  56. package/dist/stack-meta.js +1 -1
  57. package/dist/stack-watch-loop.d.ts +2 -13
  58. package/dist/stack-watch-loop.js +12 -12
  59. package/dist/submit-pr-base.d.ts +0 -1
  60. package/dist/submit-pr-base.js +1 -1
  61. package/dist/ui/ansi.d.ts +0 -10
  62. package/dist/ui/ansi.js +2 -2
  63. package/dist/ui/banner.d.ts +3 -1
  64. package/dist/ui/banner.js +10 -3
  65. package/dist/ui/box.d.ts +0 -2
  66. package/dist/ui/box.js +1 -1
  67. package/dist/ui/prompt.d.ts +0 -11
  68. package/dist/ui/prompt.js +7 -7
  69. package/dist/ui/tabs.d.ts +2 -2
  70. package/dist/ui/tabs.js +1 -1
  71. package/dist/ui/width.d.ts +0 -9
  72. package/dist/ui/width.js +3 -3
  73. package/dist/web-api.d.ts +17 -0
  74. package/dist/web-api.js +30 -0
  75. package/package.json +1 -1
  76. package/skill/SKILL.md +4 -0
  77. package/skill/mergestorm-bounce-watch/SKILL.md +1 -1
@@ -1,16 +1,15 @@
1
1
  import { type Config } from "../config.js";
2
2
  import { deleteMemory, getMemoryMode, listMemories, saveMemory, searchMemories, setMemoryMode, updateMemory, type MemoryItem, type MemoryKind, type MemoryScope } from "../memory-api.js";
3
- export declare const MEMORY_USAGE: string;
4
- export declare const MEMORY_SUBCOMMANDS: readonly ["list", "search", "add", "update", "delete", "mode"];
5
3
  type Flags = {
6
4
  positional: string[];
7
5
  json: boolean;
6
+ keepBoth?: boolean;
8
7
  repo?: string;
9
8
  kind?: MemoryKind;
10
9
  scope?: MemoryScope;
11
10
  limit?: number;
12
11
  };
13
- export declare function parseMemoryFlags(args: string[], allowed: ReadonlyArray<"repo" | "kind" | "scope" | "limit">): Flags;
12
+ export declare function parseMemoryFlags(args: string[], allowed: ReadonlyArray<"repo" | "kind" | "scope" | "limit" | "keep-both">): Flags;
14
13
  export declare function formatMemoryLine(memory: MemoryItem): string;
15
14
  export type MemoryDeps = {
16
15
  loadConfig?: () => Promise<Config>;
@@ -1,18 +1,18 @@
1
1
  import { loadConfig } from "../config.js";
2
2
  import { CommandError, REVIEW_EXIT } from "../errors.js";
3
3
  import { MEMORY_KINDS, MEMORY_SEARCH_LIMIT_MAX, deleteMemory, getMemoryMode, isMemoryKind, isMemoryMode, isMemoryScope, listMemories, memoryChatId, saveMemory, searchMemories, setMemoryMode, updateMemory, } from "../memory-api.js";
4
- export const MEMORY_USAGE = [
4
+ const MEMORY_USAGE = [
5
5
  "usage: mergestorm memory list [--scope user|repo] [--repo <owner/repo>] [--kind <kind>] [--json]",
6
6
  " mergestorm memory search <query> [--repo <owner/repo>] [--limit <1-20>] [--json]",
7
- " mergestorm memory add <text> --kind <kind> [--repo <owner/repo>] [--json]",
7
+ " mergestorm memory add <text> --kind <kind> [--repo <owner/repo>] [--keep-both] [--json]",
8
8
  " mergestorm memory update <id> <text> [--kind <kind>] [--json]",
9
9
  " mergestorm memory delete <id> [--json]",
10
10
  " mergestorm memory mode [off|on_request|all] [--json]",
11
11
  ` kind is one of ${MEMORY_KINDS.join(", ")}. With --repo a memory is about that repository, otherwise about you.`,
12
+ " add refuses text close to a memory you have; update that memory, or add again with --keep-both when they are different things.",
12
13
  " search covers your memories, and with --repo that repository's memories and standing rules too.",
13
14
  " Inside a Breeze chat (MERGESTORM_CHAT_ID), add and search follow that chat's memory setting.",
14
15
  ].join("\n");
15
- export const MEMORY_SUBCOMMANDS = ["list", "search", "add", "update", "delete", "mode"];
16
16
  const REPO = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9._-]{1,100}$/;
17
17
  function usage(message) {
18
18
  return new CommandError(message ? `${message}\n${MEMORY_USAGE}` : MEMORY_USAGE, REVIEW_EXIT.usage, "usage");
@@ -29,6 +29,8 @@ export function parseMemoryFlags(args, allowed) {
29
29
  const name = arg.startsWith("--") ? arg.slice(2) : null;
30
30
  if (arg === "--json")
31
31
  flags.json = true;
32
+ else if (name === "keep-both" && allowed.includes("keep-both"))
33
+ flags.keepBoth = true;
32
34
  else if (name === "repo" && allowed.includes("repo")) {
33
35
  const repo = value(arg, args[++i]).replace(/^https?:\/\/github\.com\//i, "").replace(/\.git$/i, "");
34
36
  if (!REPO.test(repo))
@@ -108,14 +110,14 @@ export async function cmdMemory(args, deps = {}) {
108
110
  return;
109
111
  }
110
112
  if (sub === "add" || sub === "save" || sub === "remember") {
111
- const flags = parseMemoryFlags(rest, ["repo", "kind"]);
113
+ const flags = parseMemoryFlags(rest, ["repo", "kind", "keep-both"]);
112
114
  const text = flags.positional.join(" ").trim();
113
115
  if (!text)
114
116
  throw usage("add needs the text to remember");
115
117
  if (!flags.kind)
116
118
  throw usage(`add needs --kind (${MEMORY_KINDS.join(", ")})`);
117
- const saved = await api.saveMemory({ text, kind: flags.kind, ...(flags.repo ? { repo: flags.repo } : {}), chatId }, await config());
118
- emit(flags.json, saved, () => [saved.merged ? `That matched memory ${saved.id}, which now says this.` : `Saved to memory as ${saved.id}.`]);
119
+ const saved = await api.saveMemory({ text, kind: flags.kind, ...(flags.repo ? { repo: flags.repo } : {}), chatId, ...(flags.keepBoth ? { keepBoth: true } : {}) }, await config());
120
+ emit(flags.json, saved, () => [saved.merged ? `Memory ${saved.id} already says this.` : `Saved to memory as ${saved.id}.`]);
119
121
  return;
120
122
  }
121
123
  if (sub === "update" || sub === "edit") {
@@ -3,6 +3,7 @@ import type { CommandSpec } from "../ui/prompt.js";
3
3
  export type CommandContext = {
4
4
  mode: "oneshot" | "shell";
5
5
  signal?: AbortSignal;
6
+ account?: string;
6
7
  /** Shell: refresh banner after login/logout. */
7
8
  afterAuth?: () => Promise<void>;
8
9
  };
@@ -1,5 +1,6 @@
1
- import { DEFAULT_API } from "../config.js";
1
+ import { DEFAULT_API, splitAccountFlag, withAccount } from "../config.js";
2
2
  import { CommandError } from "../errors.js";
3
+ import { cmdAccount } from "./account.js";
3
4
  import { cmdBranches, cmdChain } from "./branches.js";
4
5
  import { canBrowse, openTabsBrowser } from "./browse.js";
5
6
  import { cmdCredits } from "./credits.js";
@@ -24,9 +25,9 @@ import { cmdWhoami } from "./whoami.js";
24
25
  export const COMMAND_REGISTRY = [
25
26
  {
26
27
  name: "login",
27
- summary: "Sign in via browser (or paste a key)",
28
+ summary: "Sign in via browser (or paste a key); adds an account, up to 3",
28
29
  usage: [
29
- "mergestorm login Sign in via browser and store an API key",
30
+ "mergestorm login Sign in via browser and add the account (up to 3 saved)",
30
31
  "mergestorm login --start Print a sign-in URL and code, then exit (agents)",
31
32
  "mergestorm login --finish Store the API key once the code from --start is approved",
32
33
  "mergestorm login --key Paste an existing API key instead (headless/CI)",
@@ -39,14 +40,28 @@ export const COMMAND_REGISTRY = [
39
40
  },
40
41
  {
41
42
  name: "logout",
42
- summary: "Remove the stored API key",
43
- usage: ["mergestorm logout Remove the stored API key"],
43
+ summary: "Log out of the active account and revoke its key",
44
+ usage: ["mergestorm logout Log out of the active account (or --account <name>) and revoke its key"],
44
45
  async run(_args, ctx) {
45
46
  await cmdLogout();
46
47
  if (ctx.afterAuth)
47
48
  await ctx.afterAuth();
48
49
  },
49
50
  },
51
+ {
52
+ name: "account",
53
+ summary: "Saved accounts: list, use <name>, remove <name>",
54
+ usage: [
55
+ "mergestorm account list [--json] Saved accounts (up to 3), the active one marked",
56
+ "mergestorm account use <name> Make <name> the active account",
57
+ "mergestorm account remove <name> Log out of <name> and revoke its key",
58
+ ],
59
+ async run(args, ctx) {
60
+ await cmdAccount(args);
61
+ if (ctx.afterAuth)
62
+ await ctx.afterAuth();
63
+ },
64
+ },
50
65
  {
51
66
  name: "review",
52
67
  summary: "Review git diff (default origin/HEAD or main or master)",
@@ -102,11 +117,15 @@ export const COMMAND_REGISTRY = [
102
117
  },
103
118
  {
104
119
  name: "issue",
105
- summary: "Open a GitHub issue through Mergestorm, no gh needed",
120
+ summary: "Open a GitHub issue through Mergestorm and wait for its Vortex review",
106
121
  usage: [
107
- "mergestorm issue create --title <title> [--body <text> | --body-file <path>] [--label <name>]… [--repo <owner/repo>] [--json]",
122
+ "mergestorm issue create --title <title> [--body <text> | --body-file <path>] [--label <name>]… [--repo <owner/repo>] [--wait [--timeout <s>]] [--json]",
108
123
  " Opens the issue as the Surge App; needs write access on GitHub, uses existing labels only",
109
124
  " The same title again returns the open issue it already made when it is among the newest 100 open issues; an older one, or two calls at once, can still create a duplicate",
125
+ " --wait then waits for the Vortex issue review, like issue wait",
126
+ "mergestorm issue wait <n> [--repo <owner/repo>] [--timeout <s>] [--json]",
127
+ " Waits for the Vortex issue review (default 900s); exits 0 once approved or when issue reviews are off",
128
+ " Exits 4 with the points to answer: reply in a comment on the issue, wait again, and open the PR only after approval",
110
129
  ],
111
130
  async run(args) {
112
131
  await cmdIssue(args);
@@ -186,7 +205,7 @@ export const COMMAND_REGISTRY = [
186
205
  {
187
206
  name: "whoami",
188
207
  summary: "Key + plan (Status tab on a TTY)",
189
- usage: ["mergestorm whoami [--json] Key + plan + API base"],
208
+ usage: ["mergestorm whoami [--json] Account + key + plan + API base"],
190
209
  async run(args, ctx) {
191
210
  await cmdWhoami(args, { mode: ctx.mode });
192
211
  },
@@ -201,6 +220,7 @@ export const COMMAND_REGISTRY = [
201
220
  " [--cyclone-skip-ci on|off]",
202
221
  " [--cyclone-patch-unverified on|off] Patch languages we cannot typecheck",
203
222
  " [--vortex-seam on|off] [--vortex-skip-all-clear on|off]",
223
+ " [--issue-review on|off] Account default for Vortex issue reviews; --repo overrides it per repository",
204
224
  " [--auto-land on|off] Auto land default for new stacks",
205
225
  " [--auto-land-settle <seconds>] How long Auto land waits before queueing (15-300, default 60)",
206
226
  " [--merge-queue-batch on|off] Test several queued pull requests on one CI run",
@@ -225,7 +245,7 @@ export const COMMAND_REGISTRY = [
225
245
  usage: [
226
246
  "mergestorm memory list [--scope user|repo] [--repo <owner/repo>] [--kind <kind>] [--json]",
227
247
  "mergestorm memory search <query> [--repo <owner/repo>] [--limit <1-20>] [--json]",
228
- "mergestorm memory add <text> --kind rule|style|preference|fact|project [--repo <owner/repo>] [--json]",
248
+ "mergestorm memory add <text> --kind rule|style|preference|fact|project [--repo <owner/repo>] [--keep-both] [--json]",
229
249
  "mergestorm memory update <id> <text> [--kind <kind>] [--json]",
230
250
  "mergestorm memory delete <id> [--json]",
231
251
  "mergestorm memory mode [off|on_request|all] [--json] Read or set whether Breeze remembers",
@@ -348,9 +368,12 @@ export function formatUsageBody() {
348
368
  return `${cmdLines}
349
369
 
350
370
  mg is a short alias for mergestorm (same binary).
371
+ --account <name> before or after any command runs it as that saved account
372
+ without changing the active one; MERGESTORM_ACCOUNT=<name> does the same.
351
373
 
352
374
  Env:
353
- MERGESTORM_API_KEY API key (overrides config)
375
+ MERGESTORM_API_KEY API key (overrides every saved account)
376
+ MERGESTORM_ACCOUNT Saved account to run as (default: the active one)
354
377
  MERGESTORM_API_URL API base (default ${DEFAULT_API})
355
378
  MERGESTORM_HOME State directory for login and stack state (default ~/.mergestorm)
356
379
  `;
@@ -366,6 +389,7 @@ export async function dispatchCommand(name, args, ctx) {
366
389
  // `shell` as a subcommand is oneshot-only.
367
390
  if (cmd.name === "shell" && ctx.mode === "shell")
368
391
  return false;
369
- await cmd.run(args, ctx);
392
+ const split = splitAccountFlag(args);
393
+ await withAccount(split.account ?? ctx.account, () => cmd.run(split.args, ctx));
370
394
  return true;
371
395
  }
@@ -10,10 +10,8 @@ export declare const REVIEW_POLL_STRETCH_AFTER_MS = 60000;
10
10
  export declare const REVIEW_POLL_STRETCH_SPAN_MS = 60000;
11
11
  /** Same thread slug contract as POST /api/v1/reviews. */
12
12
  export declare const REVIEW_THREAD_SLUG_RE: RegExp;
13
- export declare const MAX_CONTEXT_TEXT_CHARS = 60000;
14
13
  export declare const MAX_CONTEXT_FILES = 10;
15
14
  export declare const MAX_CONTEXT_FILE_CHARS = 16000;
16
- export declare const MAX_CONTEXT_FILES_TOTAL = 60000;
17
15
  export declare const MAX_IDEMPOTENCY_KEY_CHARS = 128;
18
16
  export type ReviewInput = {
19
17
  thread: string;
@@ -57,7 +55,6 @@ export type LoadReviewContextOptions = {
57
55
  };
58
56
  /** Load --context / --context-file and enforce the API caps client-side. */
59
57
  export declare function loadReviewContext(opts: LoadReviewContextOptions): Promise<ReviewContextPayload>;
60
- export declare function reviewIdempotencyKey(input: ReviewInput, opts?: SubmitReviewOptions, afterJobId?: string | null): string;
61
58
  export declare function submitReview(cfg: Config, input: ReviewInput, opts?: SubmitReviewOptions): Promise<{
62
59
  status: number;
63
60
  row: ReviewJobRow;
@@ -13,10 +13,10 @@ export const REVIEW_POLL_STRETCH_AFTER_MS = 60_000;
13
13
  export const REVIEW_POLL_STRETCH_SPAN_MS = 60_000;
14
14
  /** Same thread slug contract as POST /api/v1/reviews. */
15
15
  export const REVIEW_THREAD_SLUG_RE = /^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,119}$/;
16
- export const MAX_CONTEXT_TEXT_CHARS = 60_000;
16
+ const MAX_CONTEXT_TEXT_CHARS = 60_000;
17
17
  export const MAX_CONTEXT_FILES = 10;
18
18
  export const MAX_CONTEXT_FILE_CHARS = 16_000;
19
- export const MAX_CONTEXT_FILES_TOTAL = 60_000;
19
+ const MAX_CONTEXT_FILES_TOTAL = 60_000;
20
20
  export const MAX_IDEMPOTENCY_KEY_CHARS = 128;
21
21
  export async function collectReviewInput(base, head, cwd = process.cwd(), sandboxRoot) {
22
22
  let diff;
@@ -129,7 +129,7 @@ export async function loadReviewContext(opts) {
129
129
  }
130
130
  const REPLAYED_JOB_RESUBMIT_STATUSES = ["failed", "quota_exceeded"];
131
131
  const MAX_REPLAYED_JOB_RESUBMITS = 5;
132
- export function reviewIdempotencyKey(input, opts = {}, afterJobId = null) {
132
+ function reviewIdempotencyKey(input, opts = {}, afterJobId = null) {
133
133
  const request = [
134
134
  input.thread, input.branch, input.repo ?? null, input.baseLabel, input.headLabel, input.diff, input.files,
135
135
  opts.routerMode ?? null, [...new Set(opts.specialists ?? [])].sort(), opts.context ?? null, opts.contextFiles ?? [], opts.webhookUrl ?? null,
@@ -10,7 +10,7 @@ export declare const VORTEX_ROUTER_CAPS: {
10
10
  /**
11
11
  * Pinnable L0 lanes only. Keep this list byte-identical to
12
12
  * `pinnableVortexSpecialistIds(VORTEX_SPECIALIST_IDS)` in review-findings
13
- * (`cli-lockstep.test.ts`). Do not add `governance` (round-gated) or `seam`
13
+ * (`index.test.ts`). Do not add `governance` (round-gated) or `seam`
14
14
  * (phase-gated). Those L1 lanes are not local pins.
15
15
  */
16
16
  export declare const VORTEX_SPECIALIST_IDS: readonly ["security", "performance", "architecture", "tests", "data", "api", "frontend"];
@@ -16,7 +16,7 @@ export const VORTEX_ROUTER_CAPS = { standard: 3, max: 5 };
16
16
  /**
17
17
  * Pinnable L0 lanes only. Keep this list byte-identical to
18
18
  * `pinnableVortexSpecialistIds(VORTEX_SPECIALIST_IDS)` in review-findings
19
- * (`cli-lockstep.test.ts`). Do not add `governance` (round-gated) or `seam`
19
+ * (`index.test.ts`). Do not add `governance` (round-gated) or `seam`
20
20
  * (phase-gated). Those L1 lanes are not local pins.
21
21
  */
22
22
  export const VORTEX_SPECIALIST_IDS = [
@@ -1,16 +1,16 @@
1
- import { type RepoSettings, type RepoSettingsPatch, type SettingsPatch, type SettingsResponse } from "../api.js";
1
+ import { type RepoSettingsPatch, type SettingsPatch, type SettingsResponse } from "../api.js";
2
2
  /**
3
3
  * CLI flag → Bearer settings key. Boolean flags take on|off. The connected flags are
4
4
  * read-only on the API and deliberately have no flag here.
5
5
  */
6
- export declare const SETTINGS_FLAGS: Record<string, "auto_review_enabled" | "auto_patch_enabled" | "auto_resolve_conflicts_enabled" | "auto_fix_ci_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "merge_queue_lands_alone_paths" | "merge_queue_overdrive" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check">;
6
+ export declare const SETTINGS_FLAGS: Record<string, "auto_review_enabled" | "auto_patch_enabled" | "auto_resolve_conflicts_enabled" | "auto_fix_ci_enabled" | "vortex_show_thinking_traces" | "repo_overview_enabled" | "cyclone_skip_ci_enabled" | "cyclone_patch_unverified_languages" | "vortex_auto_overflow_enabled" | "vortex_skip_all_clear_comments" | "vortex_seam_specialist_enabled" | "issue_review_enabled" | "auto_land_default" | "auto_land_settle_seconds" | "merge_queue_batch_enabled" | "merge_queue_batch_size" | "merge_queue_lands_alone_paths" | "merge_queue_overdrive" | "ignored_bot_logins" | "vortex_bot_skip_check" | "vortex_findings_check" | "cyclone_patch_failure_check">;
7
7
  type IgnoreBotOperation = {
8
8
  action: "add" | "remove";
9
9
  login: string;
10
10
  } | {
11
11
  action: "clear";
12
12
  };
13
- export type RepoSettingsScope = {
13
+ type RepoSettingsScope = {
14
14
  owner: string;
15
15
  repo: string;
16
16
  patch: RepoSettingsPatch;
@@ -22,8 +22,6 @@ export type SettingsArgs = {
22
22
  repo?: RepoSettingsScope;
23
23
  listRepos?: boolean;
24
24
  };
25
- export declare function parsePathsFlagValue(flag: string, value: string | undefined, defaults: readonly string[]): string[];
26
- export declare function formatRepoSettingsLines(repos: readonly RepoSettings[]): string[];
27
25
  /** Parse boolean and enum flags plus ordered ignore-list operations. */
28
26
  export declare function parseSettingsArgs(rawArgs: string[]): SettingsArgs;
29
27
  /** Static printout includes the Config toggles plus enum and login settings. */
@@ -18,8 +18,9 @@ const REPO_FLAG_LABELS = {
18
18
  merge_queue_batch_size: "Merge queue batch size (pull requests)",
19
19
  merge_queue_lands_alone_paths: "Merge queue lands-alone paths",
20
20
  merge_queue_overdrive: "Merge queue overdrive",
21
+ issue_review_enabled: "Vortex issue reviews",
21
22
  };
22
- export function parsePathsFlagValue(flag, value, defaults) {
23
+ function parsePathsFlagValue(flag, value, defaults) {
23
24
  if (value === undefined || value === "" || value.startsWith("--")) {
24
25
  throw new CommandError(`${flag} takes a comma-separated list of path patterns, default, or none.`, 2, "usage");
25
26
  }
@@ -79,7 +80,7 @@ function parseRepoSettingsArgs(slug, args) {
79
80
  throw new CommandError(`Unknown settings flag: ${flag}. See \`mergestorm --help\`.`, 2, "usage");
80
81
  }
81
82
  if (!REPO_SETTINGS_KEYS.includes(key)) {
82
- throw new CommandError(`${flag} is an account setting. With --repo only --auto-patch, --auto-review, --auto-land, --merge-queue-batch, --merge-queue-batch-size, --merge-queue-lands-alone and --merge-queue-overdrive can be set.`, 2, "usage");
83
+ throw new CommandError(`${flag} is an account setting. With --repo only --auto-patch, --auto-review, --auto-land, --merge-queue-batch, --merge-queue-batch-size, --merge-queue-lands-alone, --merge-queue-overdrive and --issue-review can be set.`, 2, "usage");
83
84
  }
84
85
  const value = eq >= 0 ? arg.slice(eq + 1) : args[++i];
85
86
  const row = BEARER_SETTINGS.find((row) => row.key === key);
@@ -115,7 +116,7 @@ function repoSettingWords(value) {
115
116
  return String(value);
116
117
  return value ? "on" : "off";
117
118
  }
118
- export function formatRepoSettingsLines(repos) {
119
+ function formatRepoSettingsLines(repos) {
119
120
  if (repos.length === 0)
120
121
  return [" No repositories are connected to this account."];
121
122
  const width = Math.max(...Object.values(REPO_FLAG_LABELS).map((label) => label.length));
@@ -1,6 +1,6 @@
1
- import { type StackHeldBlocker, type StackRepairHint } from "../stack-blockers.js";
1
+ import { type StackRepairHint } from "../stack-blockers.js";
2
2
  import { type StackWatchObligation } from "../stack-watch-obligation.js";
3
- import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getStackPublication, listMergeQueueEntries, listStacks, openStackPull, claimStackPulls, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
3
+ import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getIssueReview, getStackPublication, listMergeQueueEntries, listStacks, openStackPull, claimStackPulls, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
4
4
  import { loadConfig } from "../config.js";
5
5
  import { createPrs, findOpenPrNumber } from "../gh.js";
6
6
  import { parseGithubOriginRepo } from "../git.js";
@@ -11,9 +11,7 @@ import { gitTopLevel } from "../repo-identity.js";
11
11
  import { loadStackMeta, saveStackMeta } from "../stack-meta.js";
12
12
  import { pollStackWatch } from "../stack-watch.js";
13
13
  import { runStackWatch } from "../stack-watch-loop.js";
14
- export declare function stackHeldLine(held: StackHeldBlocker | null | undefined): string[];
15
14
  export declare function stackRepairLine(repair: StackRepairHint | null | undefined): string[];
16
- export declare function stackWatchTextLines(watch: StackWatchObligation | null | undefined): string[];
17
15
  export declare function openStackWatch(stackId: string, opts?: {
18
16
  unread?: boolean;
19
17
  }): StackWatchObligation;
@@ -128,6 +126,22 @@ export type StackCreateDeps = {
128
126
  };
129
127
  export declare function cmdStackCreate(args: string[], deps?: StackCreateDeps): Promise<void>;
130
128
  /** Injectable seams for unit tests (STRUCT-04). Production callers omit deps. */
129
+ export declare function issueReviewsEnabledFor(cfg: Awaited<ReturnType<typeof loadConfig>>, owner: string, repo: string): Promise<boolean>;
130
+ export type IssueGateLayer = {
131
+ branch: string;
132
+ texts: string[];
133
+ };
134
+ export type IssueGateDeps = {
135
+ getIssueReview?: typeof getIssueReview;
136
+ issueReviewsEnabled?: typeof issueReviewsEnabledFor;
137
+ };
138
+ export declare function assertLinkedIssuesApproved(input: {
139
+ cfg: Awaited<ReturnType<typeof loadConfig>>;
140
+ owner: string;
141
+ repo: string;
142
+ layers: readonly IssueGateLayer[];
143
+ warn: (line: string) => void;
144
+ }, deps?: IssueGateDeps): Promise<void>;
131
145
  export type StackSubmitDeps = {
132
146
  cwd?: string;
133
147
  gitTopLevel?: (cwd?: string) => string;
@@ -155,7 +169,7 @@ export type StackSubmitDeps = {
155
169
  adoptStack?: typeof adoptStack;
156
170
  ensureUpperPark?: typeof ensureUpperPark;
157
171
  env?: NodeJS.ProcessEnv;
158
- };
172
+ } & IssueGateDeps;
159
173
  export declare function cmdStackSubmit(args: string[], deps?: StackSubmitDeps): Promise<void>;
160
174
  export declare function parseStackPublishArgs(args: string[]): {
161
175
  branch?: string;
@@ -1,12 +1,12 @@
1
1
  import { cycloneStatusWords, stackBlockers, stackBlockersSummary } from "../stack-blockers.js";
2
2
  import { STACK_WATCH_NOT_DONE_SENTENCE, STACK_WATCH_NOTIFY_PATTERN, stackTerminalReason, stackWatchObligation, } from "../stack-watch-obligation.js";
3
- import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getStackPublication, landNextStack, listMergeQueueEntries, listStacks, openStackPull, claimStackPulls, restackStack, setStackPolicy, } from "../api.js";
3
+ import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getIssueReview, getSettings, getStackPublication, issueNotApprovedError, landNextStack, listRepoSettings, listMergeQueueEntries, listStacks, openStackPull, claimStackPulls, restackStack, setStackPolicy, } from "../api.js";
4
4
  import { loadConfig } from "../config.js";
5
5
  import { rateLimitedMessage, REVIEW_EXIT, CommandError } from "../errors.js";
6
6
  import { createPrs, findOpenPrNumber, ghReady, githubApiFromEnv, } from "../gh.js";
7
7
  import { parseGithubOriginRepo } from "../git.js";
8
8
  import { branchExists, commitsAheadOf, createBranchFrom, createBranchFromHead, trunkStartRef, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
9
- import { buildPrBodyFromCommit } from "../pr-body.js";
9
+ import { buildPrBodyFromCommit, linkedIssueNumbers } from "../pr-body.js";
10
10
  import { publishBranchServerSide, assertExplicitStacking, publishWorkingBranch, workingBranchPublication, } from "../server-publish.js";
11
11
  import { gitTopLevel } from "../repo-identity.js";
12
12
  import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, setActivePolicy, } from "../stack-meta.js";
@@ -19,7 +19,7 @@ import { present } from "../ui/present.js";
19
19
  import { canBrowse } from "./browse.js";
20
20
  import { isTransientReviewPollError } from "./review-client.js";
21
21
  import { openHelpBrowser } from "./help.js";
22
- export function stackHeldLine(held) {
22
+ function stackHeldLine(held) {
23
23
  if (!held)
24
24
  return [];
25
25
  const agents = held.waitingOn.map((agent) => agent === "cyclone" ? "Cyclone" : "Vortex").join(" and ");
@@ -32,7 +32,7 @@ export function stackRepairLine(repair) {
32
32
  const note = "note" in repair && repair.note ? ` ${repair.note}` : "";
33
33
  return [`Repair #${repair.prNumber} ${repair.kind}${files}: ${repair.steps}${note}`];
34
34
  }
35
- export function stackWatchTextLines(watch) {
35
+ function stackWatchTextLines(watch) {
36
36
  if (!watch)
37
37
  return [];
38
38
  if (watch.done)
@@ -680,6 +680,65 @@ export async function cmdStackCreate(args, deps = {}) {
680
680
  created.push(ansi.dim(" authoring state: managed automatically outside the repository"));
681
681
  await present("Stack", created);
682
682
  }
683
+ /** Injectable seams for unit tests (STRUCT-04). Production callers omit deps. */
684
+ export async function issueReviewsEnabledFor(cfg, owner, repo) {
685
+ const repos = await listRepoSettings(cfg, { timeoutMs: STACK_GUARD_TIMEOUT_MS });
686
+ const row = repos.find((r) => r.owner.toLowerCase() === owner.toLowerCase() && r.repo.toLowerCase() === repo.toLowerCase());
687
+ if (typeof row?.issue_review_enabled === "boolean")
688
+ return row.issue_review_enabled;
689
+ const settings = await getSettings(cfg, { timeoutMs: STACK_GUARD_TIMEOUT_MS });
690
+ if (!settings)
691
+ throw new CommandError("Could not read the account settings");
692
+ return settings.issue_review_enabled === true;
693
+ }
694
+ export async function assertLinkedIssuesApproved(input, deps = {}) {
695
+ const { cfg, owner, repo, layers, warn } = input;
696
+ if (layers.length === 0)
697
+ return;
698
+ const getIssueReviewFn = deps.getIssueReview ?? getIssueReview;
699
+ const unverified = (err) => {
700
+ if (err instanceof CommandError && (err.code === "auth_invalid" || err.code === "missing_api_key" || err.code === "rate_limited"))
701
+ return err;
702
+ return new CommandError(`Could not verify the Vortex reviews of the linked issues (${err instanceof Error ? err.message : String(err)}); ` +
703
+ "refusing to submit. Retry when the API is reachable.");
704
+ };
705
+ const linked = layers.map((layer) => ({ branch: layer.branch, numbers: linkedIssueNumbers({ owner, repo }, ...layer.texts) }));
706
+ const statuses = [];
707
+ for (const number of [...new Set(linked.flatMap((layer) => layer.numbers))].sort((a, b) => a - b)) {
708
+ let status;
709
+ try {
710
+ status = await getIssueReviewFn(cfg, { owner, repo, number }, { timeoutMs: STACK_GUARD_TIMEOUT_MS });
711
+ }
712
+ catch (err) {
713
+ throw unverified(err);
714
+ }
715
+ if (!status) {
716
+ warn(ansi.dim(" This Mergestorm server does not check issue reviews yet; submitting without the issue gate."));
717
+ continue;
718
+ }
719
+ statuses.push(status);
720
+ }
721
+ const unlinked = linked.filter((layer) => layer.numbers.length === 0);
722
+ if (unlinked.length > 0) {
723
+ let enabled;
724
+ try {
725
+ enabled = await (deps.issueReviewsEnabled ?? issueReviewsEnabledFor)(cfg, owner, repo);
726
+ }
727
+ catch (err) {
728
+ throw unverified(err);
729
+ }
730
+ if (enabled) {
731
+ throw new CommandError([
732
+ `Issue reviews are on for ${owner}/${repo}, and ${unlinked.map((layer) => layer.branch).join(", ")} links no issue.`,
733
+ "Open one with `mergestorm issue create --title <problem> --wait`, answer the Vortex review until it is approved,",
734
+ "then add `Fixes #N` to the tip commit message and submit again.",
735
+ ].join("\n"), REVIEW_EXIT.failed, "issue_not_approved");
736
+ }
737
+ }
738
+ const blocking = statuses.filter((status) => status.state !== "approved" && status.state !== "off");
739
+ if (blocking.length > 0)
740
+ throw issueNotApprovedError(blocking);
741
+ }
683
742
  export async function cmdStackSubmit(args, deps = {}) {
684
743
  const { extend, autoLand, autoReview, autoPatch, autoResolveConflicts, autoFixCi, rest, serverSide, asJson } = parseStackSubmitArgs(args);
685
744
  const api = rest ? "rest" : githubApiFromEnv(deps.env ?? process.env);
@@ -803,6 +862,21 @@ export async function cmdStackSubmit(args, deps = {}) {
803
862
  for (const plan of plans) {
804
863
  existingPrs.set(plan.branch, await opener.findOpen(plan.branch));
805
864
  }
865
+ await assertLinkedIssuesApproved({
866
+ cfg: await loadConfigFn(),
867
+ owner,
868
+ repo,
869
+ layers: plans
870
+ .filter((plan) => existingPrs.get(plan.branch) == null)
871
+ .map((plan) => {
872
+ if (!branchExistsFn(plan.branch, cwd)) {
873
+ throw new CommandError(`Stack layer branch missing locally: ${plan.branch}`);
874
+ }
875
+ const message = tipCommitMessageFn(plan.branch, cwd);
876
+ return { branch: plan.branch, texts: [message, buildPrBodyFn(message)] };
877
+ }),
878
+ warn: progress,
879
+ }, deps);
806
880
  const newHeads = plans
807
881
  .filter((plan) => existingPrs.get(plan.branch) == null)
808
882
  .map((plan) => plan.branch);
@@ -1,9 +1,8 @@
1
- import { getMe } from "../api.js";
2
- import { apiBase, configPath, loadConfig, resolveApiKey } from "../config.js";
1
+ import { accountLabel, getMe } from "../api.js";
2
+ import { apiBase, configPath, listAccounts, loadConfig, resolveApiKey, settleAccountName } from "../config.js";
3
3
  import { CommandError } from "../errors.js";
4
4
  import { ansi } from "../ui/ansi.js";
5
5
  import { present } from "../ui/present.js";
6
- import { canBrowse, openTabsBrowser } from "./browse.js";
7
6
  export async function cmdWhoami(args, opts = {}) {
8
7
  const asJson = args.includes("--json");
9
8
  const cfg = await loadConfig();
@@ -11,15 +10,17 @@ export async function cmdWhoami(args, opts = {}) {
11
10
  if (!key) {
12
11
  throw new CommandError("Not logged in. Run `mergestorm login` (agents: `mergestorm login --start`, then `--finish`).");
13
12
  }
14
- if (!asJson && canBrowse()) {
15
- await openTabsBrowser("status");
16
- return;
17
- }
18
13
  const me = await getMe(cfg);
19
14
  if (!me) {
20
15
  throw new CommandError("Live account details unavailable. Could not verify account with /me.");
21
16
  }
17
+ const account = (await settleAccountName(cfg, me)) ?? accountLabel(me.account);
18
+ const accounts = (await listAccounts()).map((a) => a.name);
22
19
  const payload = {
20
+ account,
21
+ accounts,
22
+ github_login: me.account?.github_login ?? null,
23
+ email: me.account?.email ?? null,
23
24
  key_prefix: me.key.prefix,
24
25
  key_name: me.key.name ?? null,
25
26
  plan_key: me.plan_key ?? null,
@@ -37,9 +38,12 @@ export async function cmdWhoami(args, opts = {}) {
37
38
  console.log(text);
38
39
  return;
39
40
  }
40
- const lines = [
41
- ` Key ${ansi.bold(String(payload.key_prefix))}${payload.key_name ? ` (${payload.key_name})` : ""}`,
42
- ];
41
+ const lines = [` Account ${account ? ansi.bold(account) : "unknown (API too old to say)"}`];
42
+ lines.push(` Key ${ansi.bold(String(payload.key_prefix))}${payload.key_name ? ` (${payload.key_name})` : ""}`);
43
+ const others = accounts.filter((name) => name !== account);
44
+ if (others.length > 0) {
45
+ lines.push(` Others ${others.join(", ")} ${ansi.dim("· mergestorm account use <name>")}`);
46
+ }
43
47
  if (payload.plan_key) {
44
48
  lines.push(` Plan ${payload.plan_label_key ?? payload.plan_key}`);
45
49
  }
package/dist/config.d.ts CHANGED
@@ -1,16 +1,69 @@
1
1
  export declare const DEFAULT_API = "https://api.mergestorm.ai";
2
+ export declare const ACCOUNT_LIMIT = 3;
2
3
  export type Config = {
3
4
  apiKey?: string;
4
5
  apiBase?: string;
5
6
  ignoreEnv?: boolean;
7
+ account?: string;
8
+ };
9
+ export type StoredAccount = {
10
+ apiKey: string;
11
+ apiBase?: string;
12
+ };
13
+ export type AccountStore = {
14
+ accounts: Record<string, StoredAccount>;
15
+ activeAccount?: string;
16
+ apiBase?: string;
17
+ ignoreEnv?: boolean;
18
+ };
19
+ type StoreFile = AccountStore & {
20
+ apiKey?: string;
21
+ };
22
+ export type AccountSummary = {
23
+ name: string;
24
+ active: boolean;
25
+ keyPrefix: string;
26
+ apiBase: string;
27
+ };
28
+ export type NamedMe = {
29
+ account?: {
30
+ github_login?: string | null;
31
+ email?: string | null;
32
+ } | null;
6
33
  };
7
34
  export declare function mergestormHome(env?: NodeJS.ProcessEnv): string;
8
35
  export declare function configPath(): string;
36
+ export declare function provisionalAccountName(apiKey: string): string;
37
+ export declare function accountNameFromMe(me: NamedMe | null | undefined): string | null;
38
+ export declare function serializeStore(store: AccountStore): StoreFile;
39
+ export declare function migrateStore(raw: unknown): {
40
+ store: AccountStore;
41
+ migrated: boolean;
42
+ };
43
+ export declare function writeStore(store: AccountStore): Promise<void>;
44
+ export declare function readStore(): Promise<AccountStore>;
45
+ export declare function withAccount<T>(name: string | undefined, fn: () => Promise<T>): Promise<T>;
46
+ export declare function requestedAccount(store: AccountStore, env?: NodeJS.ProcessEnv): string | undefined;
47
+ export declare function selectAccountName(store: AccountStore, requested: string | undefined): string | undefined;
48
+ export declare function configFor(store: AccountStore, name: string | undefined): Config;
9
49
  export declare function loadConfig(): Promise<Config>;
10
- export declare function saveConfig(cfg: Config): Promise<void>;
11
- export declare function clearConfig(): Promise<void>;
50
+ export declare function accountLimitMessage(store: AccountStore): string;
51
+ export declare function saveAccount(name: string, account: StoredAccount, opts?: {
52
+ activate?: boolean;
53
+ }): Promise<AccountStore>;
54
+ export declare function useAccount(name: string): Promise<AccountStore>;
55
+ export declare function removeAccount(name: string): Promise<AccountStore>;
56
+ export declare function renameAccount(from: string, to: string): Promise<AccountStore>;
57
+ export declare function summarizeAccounts(store: AccountStore): AccountSummary[];
58
+ export declare function listAccounts(): Promise<AccountSummary[]>;
59
+ export declare function settleAccountName(cfg: Config, me: NamedMe | null | undefined): Promise<string | undefined>;
60
+ export declare function splitAccountFlag(args: readonly string[]): {
61
+ account?: string;
62
+ args: string[];
63
+ };
12
64
  export declare function apiBase(cfg: Config): string;
13
65
  export declare function resolveChatId(cfg: Config): string | undefined;
14
66
  export declare function resolveApiKey(cfg: Config): string | undefined;
15
67
  /** Short display form of a stored key (prefix only). */
16
68
  export declare function keyDisplay(key: string | undefined): string | null;
69
+ export {};