mergestorm 0.3.18 → 0.3.20

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.
@@ -7,14 +7,16 @@ import { gitTopLevel } from "../repo-identity.js";
7
7
  import { present } from "../ui/present.js";
8
8
  export const SKILL_NAME = "mergestorm-review";
9
9
  export const PR_LOOP_SKILL_NAME = "mergestorm-pr-loop";
10
- export const SKILL_NAMES = [SKILL_NAME, PR_LOOP_SKILL_NAME];
10
+ export const BOUNCE_WATCH_SKILL_NAME = "mergestorm-bounce-watch";
11
+ export const SKILL_NAMES = [SKILL_NAME, PR_LOOP_SKILL_NAME, BOUNCE_WATCH_SKILL_NAME];
11
12
  export const SKILL_USAGE = `usage:
12
- mergestorm skill install --claude|--cursor [--json]
13
+ mergestorm skill install --claude|--cursor|--agents [--json]
13
14
 
14
- Copies mergestorm-review and mergestorm-pr-loop into this repo (git toplevel, else cwd):
15
+ Copies ${SKILL_NAMES.join(", ")} into this repo (git toplevel, else cwd):
15
16
  --claude .claude/skills/<name>/SKILL.md
16
17
  --cursor .cursor/skills/<name>/SKILL.md
17
- Pass both flags to write both trees. Overwrites existing copies.`;
18
+ --agents .agents/skills/<name>/SKILL.md
19
+ Pass multiple flags to write multiple trees. Overwrites existing copies.`;
18
20
  export function parseSkillArgs(args) {
19
21
  const [sub, ...rest] = args;
20
22
  if (!sub || sub === "-h" || sub === "--help" || sub === "help") {
@@ -40,13 +42,18 @@ export function parseSkillArgs(args) {
40
42
  targets.push("cursor");
41
43
  continue;
42
44
  }
45
+ if (arg === "--agents") {
46
+ if (!targets.includes("agents"))
47
+ targets.push("agents");
48
+ continue;
49
+ }
43
50
  if (arg === "-h" || arg === "--help" || arg === "help") {
44
51
  return { help: true, json: false, targets: [] };
45
52
  }
46
53
  throw new CommandError(`${SKILL_USAGE}\nunknown flag: ${arg}`, 1, "usage");
47
54
  }
48
55
  if (targets.length === 0) {
49
- throw new CommandError(`${SKILL_USAGE}\npass --claude and/or --cursor`, 1, "usage");
56
+ throw new CommandError(`${SKILL_USAGE}\npass --claude, --cursor, and/or --agents`, 1, "usage");
50
57
  }
51
58
  return { help: false, json, targets };
52
59
  }
@@ -65,6 +72,9 @@ export function skillDestination(root, target, name = SKILL_NAME) {
65
72
  if (target === "claude") {
66
73
  return path.join(root, ".claude", "skills", name, "SKILL.md");
67
74
  }
75
+ if (target === "agents") {
76
+ return path.join(root, ".agents", "skills", name, "SKILL.md");
77
+ }
68
78
  return path.join(root, ".cursor", "skills", name, "SKILL.md");
69
79
  }
70
80
  export function resolveBundledSkillPath(fromFile = import.meta.url, name = SKILL_NAME) {
@@ -90,7 +100,7 @@ export function resolveBundledSkillPath(fromFile = import.meta.url, name = SKILL
90
100
  }
91
101
  async function assertNoSymlink(dest) {
92
102
  const parts = dest.split(path.sep);
93
- const marker = parts.findIndex((p) => p === ".claude" || p === ".cursor");
103
+ const marker = parts.findIndex((p) => p === ".claude" || p === ".cursor" || p === ".agents");
94
104
  const first = marker === -1 ? parts.length - 1 : marker;
95
105
  for (let i = first; i < parts.length; i++) {
96
106
  const candidate = parts.slice(0, i + 1).join(path.sep);
@@ -1,11 +1,12 @@
1
- import { adoptStack, ensureUpperPark, listStacks, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
1
+ import { adoptStack, ensureUpperPark, getEnrichedStack, listStacks, setStackPolicy, type StackDto, type StackPolicyPatch } from "../api.js";
2
2
  import { loadConfig } from "../config.js";
3
- import { createPr, findOpenPrNumber } from "../gh.js";
3
+ import { createPrs, findOpenPrNumber } from "../gh.js";
4
4
  import { parseGithubOriginRepo } from "../git.js";
5
5
  import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty } from "../git-stack.js";
6
6
  import { buildPrBodyFromCommit } from "../pr-body.js";
7
7
  import { gitTopLevel } from "../repo-identity.js";
8
8
  import { loadStackMeta, saveStackMeta } from "../stack-meta.js";
9
+ import { pollStackWatch } from "../stack-watch.js";
9
10
  /** A registered stack layer that would become the parent of a new layer. */
10
11
  export type RegisteredParentHit = {
11
12
  stackId: string;
@@ -58,6 +59,7 @@ export declare function parseStackSubmitArgs(args: string[]): {
58
59
  autoLand?: boolean;
59
60
  autoReview?: boolean;
60
61
  autoPatch?: boolean;
62
+ rest: boolean;
61
63
  asJson: boolean;
62
64
  };
63
65
  export declare function parseStackAdoptArgs(args: string[]): {
@@ -113,13 +115,23 @@ export type StackSubmitDeps = {
113
115
  tipCommitSubject?: typeof tipCommitSubject;
114
116
  tipCommitMessage?: typeof tipCommitMessage;
115
117
  buildPrBodyFromCommit?: typeof buildPrBodyFromCommit;
116
- createPr?: typeof createPr;
118
+ createPrs?: typeof createPrs;
117
119
  loadConfig?: typeof loadConfig;
118
120
  listStacks?: typeof listStacks;
119
121
  adoptStack?: typeof adoptStack;
120
122
  ensureUpperPark?: typeof ensureUpperPark;
123
+ env?: NodeJS.ProcessEnv;
121
124
  };
122
125
  export declare function cmdStackSubmit(args: string[], deps?: StackSubmitDeps): Promise<void>;
126
+ export declare function cmdStackWait(args: string[], deps?: {
127
+ loadConfig?: typeof loadConfig;
128
+ pollStackWatch?: typeof pollStackWatch;
129
+ signal?: AbortSignal;
130
+ }): Promise<void>;
131
+ export declare function cmdStackStatus(args: string[], deps?: {
132
+ loadConfig?: typeof loadConfig;
133
+ getEnrichedStack?: typeof getEnrichedStack;
134
+ }): Promise<void>;
123
135
  export type StackSetDeps = {
124
136
  loadConfig?: typeof loadConfig;
125
137
  setStackPolicy?: typeof setStackPolicy;
@@ -127,4 +139,6 @@ export type StackSetDeps = {
127
139
  /** Human summary lines for a `stack set` write, one per flag. */
128
140
  export declare function stackSetSummary(parsed: StackSetArgs): string[];
129
141
  export declare function cmdStackSet(args: string[], deps?: StackSetDeps): Promise<void>;
130
- export declare function cmdStack(args: string[]): Promise<void>;
142
+ export declare function cmdStack(args: string[], options?: {
143
+ signal?: AbortSignal;
144
+ }): Promise<void>;
@@ -1,12 +1,14 @@
1
- import { adoptStack, ensureUpperPark, landNextStack, listStacks, restackStack, setStackPolicy, } from "../api.js";
1
+ import { stackBlockersSummary } from "../stack-blockers.js";
2
+ import { adoptStack, ensureUpperPark, getEnrichedStack, landNextStack, listStacks, restackStack, setStackPolicy, } from "../api.js";
2
3
  import { loadConfig } from "../config.js";
3
- import { CommandError } from "../errors.js";
4
- import { createPr, findOpenPrNumber, requireGh } from "../gh.js";
4
+ import { rateLimitedMessage, REVIEW_EXIT, CommandError } from "../errors.js";
5
+ import { createPrs, findOpenPrNumber, githubApiFromEnv, requireGh, } from "../gh.js";
5
6
  import { parseGithubOriginRepo } from "../git.js";
6
7
  import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
7
8
  import { buildPrBodyFromCommit } from "../pr-body.js";
8
9
  import { gitTopLevel } from "../repo-identity.js";
9
10
  import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, setActivePolicy, } from "../stack-meta.js";
11
+ import { pollStackWatch, StackWatchError, StackWatchTimeoutError, } from "../stack-watch.js";
10
12
  import { isMgParkBranch, planSubmitLayerBases } from "../submit-pr-base.js";
11
13
  import { ansi } from "../ui/ansi.js";
12
14
  import { runLineTabsBrowser } from "../ui/line-tabs.js";
@@ -15,9 +17,11 @@ import { canBrowse } from "./browse.js";
15
17
  import { openHelpBrowser } from "./help.js";
16
18
  const STACK_USAGE = `usage:
17
19
  mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
18
- mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
20
+ mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--rest] [--json]
19
21
  mergestorm stack reset --force
20
22
  mergestorm stack list [--json]
23
+ mergestorm stack status <stack-id> [--json]
24
+ mergestorm stack wait <stack-id> [--json] [--timeout <s>]
21
25
  mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]
22
26
  mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
23
27
  mergestorm stack restack <stack-id> [--json]
@@ -29,6 +33,8 @@ const STACK_USAGE = `usage:
29
33
  stack submit opens PRs with a body generated from each layer tip commit.
30
34
  Layers 1–2 use the git parent as GitHub base. Layer 3+ opens onto the
31
35
  review-unit park freeze (mg-park-*) so adopt does not retarget after open.
36
+ stack submit talks to GitHub GraphQL and falls back to the REST API when
37
+ GraphQL is blocked; --rest (or MERGESTORM_GITHUB_API=rest) forces REST.
32
38
  Parenting onto a branch that already belongs to a registered (submitted)
33
39
  stack requires --extend on create and submit — otherwise mg starts a new
34
40
  stack from trunk. stack land promotes into the review unit when one exists;
@@ -143,7 +149,7 @@ export function stackPolicyLabels(s) {
143
149
  }
144
150
  return labels;
145
151
  }
146
- function formatStackHuman(s) {
152
+ function formatStackHuman(s, includeLayers = true) {
147
153
  const lines = [];
148
154
  lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}` +
149
155
  stackPolicyLabels(s)
@@ -154,6 +160,8 @@ function formatStackHuman(s) {
154
160
  lines.push(ansi.dim(" (no layers)"));
155
161
  return lines;
156
162
  }
163
+ if (!includeLayers)
164
+ return lines;
157
165
  for (const layer of s.layers) {
158
166
  const title = layer.title ? ` ${layer.title}` : "";
159
167
  const pr = layer.prNumber > 0 ? `#${layer.prNumber}` : "—";
@@ -262,6 +270,7 @@ export function parseStackCreateArgs(args) {
262
270
  /** Parse `stack submit` argv. Exported for tests. */
263
271
  export function parseStackSubmitArgs(args) {
264
272
  let extend = false;
273
+ let rest = false;
265
274
  const policy = {};
266
275
  let asJson = false;
267
276
  for (let i = 0; i < args.length; i++) {
@@ -274,18 +283,23 @@ export function parseStackSubmitArgs(args) {
274
283
  extend = true;
275
284
  continue;
276
285
  }
286
+ if (a === "--rest") {
287
+ rest = true;
288
+ continue;
289
+ }
277
290
  const next = takeOpenPolicyFlag(args, i, policy, "stack submit");
278
291
  if (next !== null) {
279
292
  i = next - 1;
280
293
  continue;
281
294
  }
282
- throw new CommandError("usage: mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]");
295
+ throw new CommandError("usage: mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--rest] [--json]");
283
296
  }
284
297
  return {
285
298
  extend,
286
299
  autoLand: policy.autoLand,
287
300
  autoReview: policy.autoReview,
288
301
  autoPatch: policy.autoPatch,
302
+ rest,
289
303
  asJson,
290
304
  };
291
305
  }
@@ -518,7 +532,8 @@ export async function cmdStackCreate(args, deps = {}) {
518
532
  await present("Stack", created);
519
533
  }
520
534
  export async function cmdStackSubmit(args, deps = {}) {
521
- const { extend, autoLand, autoReview, autoPatch, asJson } = parseStackSubmitArgs(args);
535
+ const { extend, autoLand, autoReview, autoPatch, rest, asJson } = parseStackSubmitArgs(args);
536
+ const api = rest ? "rest" : githubApiFromEnv(deps.env ?? process.env);
522
537
  const progress = (message) => {
523
538
  if (asJson)
524
539
  console.error(message);
@@ -539,7 +554,7 @@ export async function cmdStackSubmit(args, deps = {}) {
539
554
  const tipCommitSubjectFn = deps.tipCommitSubject ?? tipCommitSubject;
540
555
  const tipCommitMessageFn = deps.tipCommitMessage ?? tipCommitMessage;
541
556
  const buildPrBodyFn = deps.buildPrBodyFromCommit ?? buildPrBodyFromCommit;
542
- const createPrFn = deps.createPr ?? createPr;
557
+ const createPrsFn = deps.createPrs ?? createPrs;
543
558
  const loadConfigFn = deps.loadConfig ?? loadConfig;
544
559
  const listStacksFn = deps.listStacks ?? listStacks;
545
560
  const adoptStackFn = deps.adoptStack ?? adoptStack;
@@ -624,16 +639,11 @@ export async function cmdStackSubmit(args, deps = {}) {
624
639
  }
625
640
  const opened = [];
626
641
  const pendingPark = plans.filter((plan) => {
627
- if (findOpenPrNumberFn(owner, repo, plan.branch, cwd) != null)
642
+ if (findOpenPrNumberFn(owner, repo, plan.branch, cwd, { api }) != null)
628
643
  return false;
629
644
  return plan.githubBase == null;
630
645
  });
631
- const openLayerPr = (branch, base) => {
632
- const existing = findOpenPrNumberFn(owner, repo, branch, cwd);
633
- if (existing != null) {
634
- progress(ansi.dim(` PR #${existing} already open for ${branch}`));
635
- return { prNumber: existing, created: false };
636
- }
646
+ const prepareLayerPr = (branch, base) => {
637
647
  const aheadRef = isMgParkBranch(base) ? `origin/${base}` : base;
638
648
  if (isMgParkBranch(base)) {
639
649
  try {
@@ -657,30 +667,44 @@ export async function cmdStackSubmit(args, deps = {}) {
657
667
  throw new CommandError(`No commits on \`${branch}\` ahead of \`${base}\`. ` +
658
668
  `Commit your changes, then retry \`mg stack submit\`.`);
659
669
  }
660
- const title = tipCommitSubjectFn(branch, cwd);
661
- const body = buildPrBodyFn(tipCommitMessageFn(branch, cwd));
662
- progress(ansi.dim(` Opening PR ${branch} → ${base} …`));
663
670
  return {
664
- prNumber: createPrFn({
665
- owner,
666
- repo,
667
- base,
668
- head: branch,
669
- title,
670
- body,
671
- cwd,
672
- }),
673
- created: true,
671
+ base,
672
+ head: branch,
673
+ title: tipCommitSubjectFn(branch, cwd),
674
+ body: buildPrBodyFn(tipCommitMessageFn(branch, cwd)),
674
675
  };
675
676
  };
676
- for (const plan of plans) {
677
- if (plan.githubBase == null && findOpenPrNumberFn(owner, repo, plan.branch, cwd) == null) {
678
- continue;
677
+ const openLayerWave = (wave) => {
678
+ const rows = [];
679
+ const pending = [];
680
+ for (const { branch, base } of wave) {
681
+ const existing = findOpenPrNumberFn(owner, repo, branch, cwd, { api });
682
+ if (existing != null) {
683
+ progress(ansi.dim(` PR #${existing} already open for ${branch}`));
684
+ rows.push({ branch, base, prNumber: existing, created: false });
685
+ continue;
686
+ }
687
+ pending.push({ index: rows.length, pr: prepareLayerPr(branch, base) });
688
+ rows.push({ branch, base, prNumber: 0, created: true });
679
689
  }
680
- const base = plan.githubBase ?? plan.gitParent;
681
- const { prNumber, created } = openLayerPr(plan.branch, base);
682
- opened.push({ branch: plan.branch, base, prNumber, created });
683
- }
690
+ if (pending.length === 0)
691
+ return rows;
692
+ for (const { pr } of pending) {
693
+ progress(ansi.dim(` Opening PR ${pr.head} → ${pr.base} …`));
694
+ }
695
+ const numbers = createPrsFn({ owner, repo, prs: pending.map((p) => p.pr), cwd, api });
696
+ if (numbers.length !== pending.length) {
697
+ throw new CommandError(`GitHub returned ${numbers.length} PR numbers for ${pending.length} new pull requests.`);
698
+ }
699
+ pending.forEach(({ index }, i) => {
700
+ rows[index].prNumber = numbers[i];
701
+ });
702
+ return rows;
703
+ };
704
+ opened.push(...openLayerWave(plans
705
+ .filter((plan) => plan.githubBase != null ||
706
+ findOpenPrNumberFn(owner, repo, plan.branch, cwd, { api }) != null)
707
+ .map((plan) => ({ branch: plan.branch, base: plan.githubBase ?? plan.gitParent }))));
684
708
  const cfg = await loadConfigFn();
685
709
  let adoptData = null;
686
710
  let stackId = plans.find((plan) => plan.stackId)?.stackId ?? null;
@@ -711,10 +735,7 @@ export async function cmdStackSubmit(args, deps = {}) {
711
735
  }
712
736
  progress(ansi.dim(` Ensuring upper-park freeze for stack ${stackId} …`));
713
737
  const park = await ensureUpperParkFn(stackId, cfg);
714
- for (const plan of pendingPark) {
715
- const { prNumber, created } = openLayerPr(plan.branch, park.freezeBranch);
716
- opened.push({ branch: plan.branch, base: park.freezeBranch, prNumber, created });
717
- }
738
+ opened.push(...openLayerWave(pendingPark.map((plan) => ({ branch: plan.branch, base: park.freezeBranch }))));
718
739
  await registerPr(opened[opened.length - 1].prNumber);
719
740
  }
720
741
  else {
@@ -787,6 +808,86 @@ async function cmdStackList(args) {
787
808
  listed.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
788
809
  await present("Stacks", listed);
789
810
  }
811
+ export async function cmdStackWait(args, deps = {}) {
812
+ const usage = "usage: mergestorm stack wait <stack-id> [--json] [--timeout <s>]";
813
+ const positional = [];
814
+ let timeoutS = 45;
815
+ for (let i = 0; i < args.length; i++) {
816
+ const arg = args[i];
817
+ if (arg === "--json")
818
+ continue;
819
+ if (arg === "--timeout" || arg.startsWith("--timeout=")) {
820
+ const value = arg === "--timeout" ? args[++i] : arg.slice("--timeout=".length);
821
+ timeoutS = value?.trim() ? Number(value) : NaN;
822
+ if (!Number.isFinite(timeoutS) || timeoutS < 0 || timeoutS > 300) {
823
+ throw new CommandError(`${usage}\n--timeout must be between 0 and 300 seconds`, REVIEW_EXIT.usage, "usage");
824
+ }
825
+ }
826
+ else if (arg.startsWith("-")) {
827
+ throw new CommandError(usage, REVIEW_EXIT.usage, "usage");
828
+ }
829
+ else {
830
+ positional.push(arg);
831
+ }
832
+ }
833
+ if (positional.length !== 1)
834
+ throw new CommandError(usage, REVIEW_EXIT.usage, "usage");
835
+ const stackId = requireStackId(positional[0], usage);
836
+ const cfg = await (deps.loadConfig ?? loadConfig)();
837
+ const print = (envelope, retryAfterSeconds) => console.log(args.includes("--json")
838
+ ? JSON.stringify({
839
+ ...envelope,
840
+ ...(retryAfterSeconds !== undefined ? { retry_after_seconds: retryAfterSeconds } : {}),
841
+ }, null, 2)
842
+ : `Stack ${envelope.stackId} · ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues)}${envelope.assessment === "unavailable" ? " · assessment unavailable" : ""}`);
843
+ try {
844
+ print(await (deps.pollStackWatch ?? pollStackWatch)(cfg, stackId, {
845
+ timeoutMs: timeoutS * 1000,
846
+ signal: deps.signal,
847
+ }));
848
+ }
849
+ catch (err) {
850
+ if (err instanceof StackWatchError && err.lastEnvelope.status === "rate_limited") {
851
+ print(err.lastEnvelope, err.retryAfterSeconds);
852
+ throw new CommandError(rateLimitedMessage(err.retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", { retryAfterSeconds: err.retryAfterSeconds });
853
+ }
854
+ if (err instanceof StackWatchError) {
855
+ print(err.lastEnvelope);
856
+ throw err;
857
+ }
858
+ if (!(err instanceof StackWatchTimeoutError))
859
+ throw err;
860
+ print(err.lastEnvelope);
861
+ throw new CommandError(err.message, REVIEW_EXIT.timeout, "review_timeout");
862
+ }
863
+ }
864
+ export async function cmdStackStatus(args, deps = {}) {
865
+ const usage = "usage: mergestorm stack status <stack-id> [--json]";
866
+ const positional = args.filter((arg) => arg !== "--json");
867
+ if (positional.length !== 1)
868
+ throw new CommandError(usage, 2, "usage");
869
+ const stackId = requireStackId(positional[0], usage);
870
+ const cfg = await (deps.loadConfig ?? loadConfig)();
871
+ const stack = await (deps.getEnrichedStack ?? getEnrichedStack)(stackId, cfg);
872
+ if (!stack) {
873
+ throw new CommandError(`Stack not found or not owned by the current user: ${stackId}`, 1, "not_found");
874
+ }
875
+ if (args.includes("--json")) {
876
+ console.log(JSON.stringify(stack, null, 2));
877
+ return;
878
+ }
879
+ const lines = formatStackHuman(stack, false);
880
+ for (const layer of stack.layers) {
881
+ const head = layer.headSha ? `@${layer.headSha.slice(0, 7)}` : "";
882
+ const pr = layer.prNumber > 0 ? `#${layer.prNumber}` : "—";
883
+ lines.push(` ${String(layer.position).padStart(2)} ${pr}${head} ${layer.state.padEnd(14)} ${layer.branch}` +
884
+ ` CI: ${layer.ciStatus ?? "unknown"} review: ${layer.reviewStatus ?? "unknown"}` +
885
+ (layer.vortexStatus ? ` Vortex: ${layer.vortexStatus}` : "") +
886
+ (layer.cycloneStatus ? ` Cyclone: ${layer.cycloneStatus}` : "") +
887
+ (layer.tempestStatus ? ` Tempest: ${layer.tempestStatus}` : ""));
888
+ }
889
+ await present("Stack status", lines);
890
+ }
790
891
  /** Human summary lines for a `stack set` write, one per flag. */
791
892
  export function stackSetSummary(parsed) {
792
893
  const tri = (value) => value === null ? "default (account setting)" : value ? "on" : "off";
@@ -885,7 +986,7 @@ async function cmdStackLand(args) {
885
986
  ` on ${id}`),
886
987
  ]);
887
988
  }
888
- export async function cmdStack(args) {
989
+ export async function cmdStack(args, options = {}) {
889
990
  const sub = args[0]?.toLowerCase();
890
991
  const rest = args.slice(1);
891
992
  // Help must exit 0 — print usage, do not throw CommandError.
@@ -905,6 +1006,10 @@ export async function cmdStack(args) {
905
1006
  return cmdStackReset(rest);
906
1007
  if (sub === "list" || sub === "ls")
907
1008
  return cmdStackList(rest);
1009
+ if (sub === "status")
1010
+ return cmdStackStatus(rest);
1011
+ if (sub === "wait")
1012
+ return cmdStackWait(rest, { signal: options.signal });
908
1013
  if (sub === "set")
909
1014
  return cmdStackSet(rest);
910
1015
  if (sub === "adopt")
package/dist/errors.d.ts CHANGED
@@ -2,7 +2,7 @@
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" | "not_found" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited";
5
+ export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited" | "cyclone_not_installed" | "pr_merged" | "pr_closed" | "not_stacked" | "draft" | "reserved_branch" | "supabase_unconfigured" | "cyclone_not_connected" | "busy" | "adopt_failed";
6
6
  /** Stable one-shot exit codes for machine callers of `mergestorm review`. */
7
7
  export declare const REVIEW_EXIT: {
8
8
  readonly success: 0;
package/dist/gh.d.ts CHANGED
@@ -1,20 +1,82 @@
1
- export declare function runGh(args: string[], cwd?: string): {
1
+ import { CommandError } from "./errors.js";
2
+ export type GhResult = {
2
3
  ok: true;
3
4
  stdout: string;
4
5
  } | {
5
6
  ok: false;
6
7
  stderr: string;
7
8
  status: number | null;
9
+ stdout?: string;
8
10
  };
11
+ export type GhRunner = (args: string[], input?: string) => GhResult;
12
+ export type GithubApi = "graphql" | "rest";
13
+ export declare const GITHUB_API_ENV = "MERGESTORM_GITHUB_API";
14
+ export declare function githubApiFromEnv(env?: NodeJS.ProcessEnv): GithubApi;
15
+ export declare function isGraphqlEndpointBlocked(text: string): boolean;
16
+ export declare class GraphqlBlockedError extends CommandError {
17
+ readonly opened: readonly number[];
18
+ constructor(message: string, opened?: readonly number[]);
19
+ }
20
+ type GhSpawn = (command: string, args: string[], options: {
21
+ cwd: string;
22
+ encoding: "utf8";
23
+ input?: string;
24
+ }) => {
25
+ error?: Error;
26
+ stdout?: string | null;
27
+ stderr?: string | null;
28
+ status: number | null;
29
+ };
30
+ export declare function runGh(args: string[], cwd?: string, input?: string, spawn?: GhSpawn): GhResult;
9
31
  export declare function requireGh(cwd?: string): void;
10
32
  /** Open PR number for `head` branch in owner/repo, or null. */
11
- export declare function findOpenPrNumber(owner: string, repo: string, headBranch: string, cwd?: string): number | null;
12
- export declare function createPr(input: {
13
- owner: string;
14
- repo: string;
33
+ export declare function findOpenPrNumber(owner: string, repo: string, headBranch: string, cwd?: string, options?: {
34
+ api?: GithubApi;
35
+ gh?: GhRunner;
36
+ }): number | null;
37
+ export declare function findOpenPrNumberViaRest(owner: string, repo: string, headBranch: string, gh: GhRunner): number | null;
38
+ export type NewPr = {
15
39
  base: string;
16
40
  head: string;
17
41
  title: string;
18
- body?: string;
42
+ body: string;
43
+ };
44
+ export type GraphqlRequest = {
45
+ query: string;
46
+ variables: Record<string, unknown>;
47
+ };
48
+ export type GraphqlResponse = {
49
+ data?: Record<string, unknown> | null;
50
+ errors?: {
51
+ message?: string;
52
+ }[];
53
+ };
54
+ export type GraphqlRunner = (request: GraphqlRequest) => GraphqlResponse;
55
+ type GraphqlSpawnResult = {
56
+ error?: Error;
57
+ stdout?: string | null;
58
+ stderr?: string | null;
59
+ status: number | null;
60
+ };
61
+ type GraphqlSpawn = (command: string, args: string[], options: {
62
+ cwd: string;
63
+ encoding: "utf8";
64
+ input: string;
65
+ }) => GraphqlSpawnResult;
66
+ export declare const CREATE_PR_BATCH_SIZE = 5;
67
+ export declare function ghGraphql(request: GraphqlRequest, cwd?: string, spawn?: GraphqlSpawn): GraphqlResponse;
68
+ export declare function chunkCreatePrs<T>(items: readonly T[], size?: number): T[][];
69
+ export declare function buildCreatePrsMutation(repositoryId: string, prs: readonly NewPr[]): GraphqlRequest;
70
+ export declare function fetchRepositoryId(owner: string, repo: string, graphql: GraphqlRunner): string;
71
+ export declare function createPrsOnRepository(repositoryId: string, prs: readonly NewPr[], graphql: GraphqlRunner): number[];
72
+ export declare function createPrsViaRest(owner: string, repo: string, prs: readonly NewPr[], gh: GhRunner, opened?: readonly number[]): number[];
73
+ export declare function createPrs(input: {
74
+ owner: string;
75
+ repo: string;
76
+ prs: readonly NewPr[];
19
77
  cwd?: string;
20
- }): number;
78
+ api?: GithubApi;
79
+ graphql?: GraphqlRunner;
80
+ gh?: GhRunner;
81
+ }): number[];
82
+ export {};