mergestorm 0.3.13 → 0.3.16

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 (66) hide show
  1. package/README.md +16 -17
  2. package/dist/api.d.ts +103 -6
  3. package/dist/api.js +195 -9
  4. package/dist/automation-catalog.d.ts +37 -0
  5. package/dist/automation-catalog.js +13 -0
  6. package/dist/client/index.d.ts +3 -2
  7. package/dist/client/index.js +3 -2
  8. package/dist/commands/branches.d.ts +6 -2
  9. package/dist/commands/branches.js +29 -14
  10. package/dist/commands/browse.d.ts +39 -0
  11. package/dist/commands/browse.js +252 -0
  12. package/dist/commands/credits.js +7 -10
  13. package/dist/commands/help.d.ts +5 -0
  14. package/dist/commands/help.js +93 -0
  15. package/dist/commands/jobs.d.ts +11 -2
  16. package/dist/commands/jobs.js +64 -15
  17. package/dist/commands/login.js +4 -3
  18. package/dist/commands/logout.js +2 -1
  19. package/dist/commands/pr.d.ts +22 -0
  20. package/dist/commands/pr.js +127 -0
  21. package/dist/commands/queue.d.ts +12 -0
  22. package/dist/commands/queue.js +160 -0
  23. package/dist/commands/registry.js +98 -21
  24. package/dist/commands/review-client.d.ts +13 -0
  25. package/dist/commands/review-client.js +110 -0
  26. package/dist/commands/review.d.ts +6 -0
  27. package/dist/commands/review.js +18 -7
  28. package/dist/commands/settings.d.ts +17 -0
  29. package/dist/commands/settings.js +82 -0
  30. package/dist/commands/skill.d.ts +20 -0
  31. package/dist/commands/skill.js +145 -0
  32. package/dist/commands/stack.d.ts +57 -2
  33. package/dist/commands/stack.js +352 -87
  34. package/dist/commands/status.d.ts +2 -0
  35. package/dist/commands/status.js +30 -15
  36. package/dist/commands/whoami.d.ts +3 -1
  37. package/dist/commands/whoami.js +21 -7
  38. package/dist/errors.d.ts +1 -1
  39. package/dist/repo-identity.d.ts +1 -1
  40. package/dist/repo-identity.js +13 -2
  41. package/dist/shell.js +39 -31
  42. package/dist/stack-dto.d.ts +92 -1
  43. package/dist/stack-meta.d.ts +12 -0
  44. package/dist/stack-meta.js +32 -3
  45. package/dist/ui/ansi.d.ts +2 -0
  46. package/dist/ui/ansi.js +2 -0
  47. package/dist/ui/banner.d.ts +34 -2
  48. package/dist/ui/banner.js +166 -79
  49. package/dist/ui/findings.d.ts +2 -1
  50. package/dist/ui/findings.js +13 -7
  51. package/dist/ui/line-tabs.d.ts +27 -0
  52. package/dist/ui/line-tabs.js +170 -0
  53. package/dist/ui/logo.d.ts +10 -2
  54. package/dist/ui/logo.js +13 -9
  55. package/dist/ui/present.d.ts +10 -0
  56. package/dist/ui/present.js +13 -0
  57. package/dist/ui/prompt.d.ts +58 -1
  58. package/dist/ui/prompt.js +225 -42
  59. package/dist/ui/select.js +1 -1
  60. package/dist/ui/tabs.d.ts +125 -0
  61. package/dist/ui/tabs.js +480 -0
  62. package/dist/ui/usage.d.ts +4 -0
  63. package/dist/ui/usage.js +22 -2
  64. package/package.json +2 -1
  65. package/skill/SKILL.md +98 -0
  66. package/skill/mergestorm-pr-loop/SKILL.md +84 -0
@@ -1,4 +1,4 @@
1
- import { adoptStack, ensureUpperPark, landNextStack, listStacks, restackStack, setStackAutoPromote, } from "../api.js";
1
+ import { adoptStack, ensureUpperPark, landNextStack, listStacks, restackStack, setStackPolicy, } from "../api.js";
2
2
  import { loadConfig } from "../config.js";
3
3
  import { CommandError } from "../errors.js";
4
4
  import { createPr, findOpenPrNumber, requireGh } from "../gh.js";
@@ -6,19 +6,26 @@ import { parseGithubOriginRepo } from "../git.js";
6
6
  import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
7
7
  import { buildPrBodyFromCommit } from "../pr-body.js";
8
8
  import { gitTopLevel } from "../repo-identity.js";
9
- import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, } from "../stack-meta.js";
9
+ import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, setActivePolicy, } from "../stack-meta.js";
10
10
  import { isMgParkBranch, planSubmitLayerBases } from "../submit-pr-base.js";
11
11
  import { ansi } from "../ui/ansi.js";
12
+ import { runLineTabsBrowser } from "../ui/line-tabs.js";
13
+ import { present } from "../ui/present.js";
14
+ import { canBrowse } from "./browse.js";
15
+ import { openHelpBrowser } from "./help.js";
12
16
  const STACK_USAGE = `usage:
13
- mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--json]
14
- mergestorm stack submit [--extend] [--json]
17
+ 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]
15
19
  mergestorm stack reset --force
16
20
  mergestorm stack list [--json]
17
- mergestorm stack adopt <owner/repo>#<pr>
21
+ mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]
22
+ mergestorm stack adopt <owner/repo>#<pr> [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]
18
23
  mergestorm stack restack <stack-id> [--json]
19
24
  mergestorm stack land <stack-id> [--json]
20
- mergestorm stack auto-promote on|off <stack-id> [--json]
21
25
 
26
+ --auto-review / --auto-patch pin Vortex auto-review / Cyclone auto-patch for
27
+ one stack in either direction. Absent, or \`default\` on stack set, follows
28
+ the account setting.
22
29
  stack submit opens PRs with a body generated from each layer tip commit.
23
30
  Layers 1–2 use the git parent as GitHub base. Layer 3+ opens onto the
24
31
  review-unit park freeze (mg-park-*) so adopt does not retarget after open.
@@ -97,10 +104,51 @@ export function parseAdoptTarget(args) {
97
104
  }
98
105
  throw new CommandError(`usage: mergestorm stack adopt <owner/repo>#<pr>`);
99
106
  }
107
+ const stackCmd = (name, blurb) => ` ${ansi.brightGreen(name.padEnd(26))}${blurb}`;
108
+ /** Empty + populated bodies for the TTY `stack list` panel. */
109
+ export function buildStackListLines(stacks) {
110
+ if (stacks.length === 0) {
111
+ return [
112
+ ` ${ansi.bold("None yet")}`,
113
+ " No registered stacks for this key.",
114
+ "",
115
+ ` ${ansi.bold("Author one")}`,
116
+ stackCmd("stack create", "new layer on this branch"),
117
+ " commit, then stack submit",
118
+ "",
119
+ ` ${ansi.bold("Or import")}`,
120
+ stackCmd("stack adopt org/repo#12", "open GitHub chain"),
121
+ "",
122
+ ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"),
123
+ ];
124
+ }
125
+ const lines = [];
126
+ for (const s of stacks) {
127
+ lines.push(...formatStackHuman(s));
128
+ lines.push("");
129
+ }
130
+ lines.push(ansi.dim(" create · submit · land <id> · q to close"));
131
+ return lines;
132
+ }
133
+ /** Short per-stack policy suffixes: Auto land when on, overrides when pinned. */
134
+ export function stackPolicyLabels(s) {
135
+ const labels = [];
136
+ if (s.autoEnqueueWhenReady)
137
+ labels.push("auto-land on");
138
+ if (typeof s.autoReviewOverride === "boolean") {
139
+ labels.push(`auto-review ${s.autoReviewOverride ? "on" : "off"}`);
140
+ }
141
+ if (typeof s.autoPatchOverride === "boolean") {
142
+ labels.push(`auto-patch ${s.autoPatchOverride ? "on" : "off"}`);
143
+ }
144
+ return labels;
145
+ }
100
146
  function formatStackHuman(s) {
101
147
  const lines = [];
102
- const auto = s.autoPromoteWhenGreen ? " · auto-promote" : "";
103
- lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}${auto}`);
148
+ lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}` +
149
+ stackPolicyLabels(s)
150
+ .map((label) => ` ${label}`)
151
+ .join(""));
104
152
  lines.push(` ${ansi.dim(s.id)}`);
105
153
  if (s.layers.length === 0) {
106
154
  lines.push(ansi.dim(" (no layers)"));
@@ -113,12 +161,45 @@ function formatStackHuman(s) {
113
161
  }
114
162
  return lines;
115
163
  }
164
+ const OPEN_POLICY_FLAGS = [
165
+ { flag: "--auto-land", key: "autoLand" },
166
+ { flag: "--auto-review", key: "autoReview" },
167
+ { flag: "--auto-patch", key: "autoPatch" },
168
+ ];
169
+ /**
170
+ * Consume one `--auto-* on|off` flag at `args[i]` (space or `=` form). Returns
171
+ * the next index to scan, or null when `args[i]` is not a policy flag.
172
+ */
173
+ function takeOpenPolicyFlag(args, i, into, verb) {
174
+ const a = args[i];
175
+ for (const { flag, key } of OPEN_POLICY_FLAGS) {
176
+ if (a === flag) {
177
+ into[key] = parseOnOff(args[i + 1], flag, verb);
178
+ return i + 2;
179
+ }
180
+ if (a.startsWith(`${flag}=`)) {
181
+ into[key] = parseOnOff(a.slice(flag.length + 1), flag, verb);
182
+ return i + 1;
183
+ }
184
+ }
185
+ return null;
186
+ }
187
+ /** Wire policy for adopt from the parsed `on|off` flags; absent keys stay off the body. */
188
+ export function openPolicyPatch(flags) {
189
+ const patch = {
190
+ ...(typeof flags.autoLand === "boolean" ? { autoEnqueueWhenReady: flags.autoLand } : {}),
191
+ ...(typeof flags.autoReview === "boolean" ? { autoReviewOverride: flags.autoReview } : {}),
192
+ ...(typeof flags.autoPatch === "boolean" ? { autoPatchOverride: flags.autoPatch } : {}),
193
+ };
194
+ return Object.keys(patch).length > 0 ? patch : undefined;
195
+ }
116
196
  /** Parse `stack create` argv into options. Exported for tests. */
117
197
  export function parseStackCreateArgs(args) {
118
198
  let name;
119
199
  let onto;
120
200
  let trunk;
121
201
  let extend = false;
202
+ const policy = {};
122
203
  let asJson = false;
123
204
  for (let i = 0; i < args.length; i++) {
124
205
  const a = args[i];
@@ -130,6 +211,11 @@ export function parseStackCreateArgs(args) {
130
211
  extend = true;
131
212
  continue;
132
213
  }
214
+ const next = takeOpenPolicyFlag(args, i, policy, "stack create");
215
+ if (next !== null) {
216
+ i = next - 1;
217
+ continue;
218
+ }
133
219
  if (a === "--onto") {
134
220
  const v = args[++i];
135
221
  if (!v) {
@@ -162,13 +248,24 @@ export function parseStackCreateArgs(args) {
162
248
  }
163
249
  name = a;
164
250
  }
165
- return { name, onto, trunk, extend, asJson };
251
+ return {
252
+ name,
253
+ onto,
254
+ trunk,
255
+ extend,
256
+ autoLand: policy.autoLand,
257
+ autoReview: policy.autoReview,
258
+ autoPatch: policy.autoPatch,
259
+ asJson,
260
+ };
166
261
  }
167
262
  /** Parse `stack submit` argv. Exported for tests. */
168
263
  export function parseStackSubmitArgs(args) {
169
264
  let extend = false;
265
+ const policy = {};
170
266
  let asJson = false;
171
- for (const a of args) {
267
+ for (let i = 0; i < args.length; i++) {
268
+ const a = args[i];
172
269
  if (a === "--json") {
173
270
  asJson = true;
174
271
  continue;
@@ -177,43 +274,159 @@ export function parseStackSubmitArgs(args) {
177
274
  extend = true;
178
275
  continue;
179
276
  }
180
- throw new CommandError("usage: mergestorm stack submit [--extend] [--json]");
277
+ const next = takeOpenPolicyFlag(args, i, policy, "stack submit");
278
+ if (next !== null) {
279
+ i = next - 1;
280
+ continue;
281
+ }
282
+ throw new CommandError("usage: mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--json]");
283
+ }
284
+ return {
285
+ extend,
286
+ autoLand: policy.autoLand,
287
+ autoReview: policy.autoReview,
288
+ autoPatch: policy.autoPatch,
289
+ asJson,
290
+ };
291
+ }
292
+ function parseOnOff(raw, flag, verb) {
293
+ if (raw === "on")
294
+ return true;
295
+ if (raw === "off")
296
+ return false;
297
+ throw new CommandError(`usage: mergestorm ${verb} [${flag} on|off]`);
298
+ }
299
+ /** `stack set` overrides are tri-state: `default` writes null (follow the account flag). */
300
+ function parseOnOffDefault(raw, flag, verb) {
301
+ if (raw === "on")
302
+ return true;
303
+ if (raw === "off")
304
+ return false;
305
+ if (raw === "default")
306
+ return null;
307
+ throw new CommandError(`usage: mergestorm ${verb} [${flag} on|off|default]`);
308
+ }
309
+ export function parseStackAdoptArgs(args) {
310
+ const targetArgs = [];
311
+ const policy = {};
312
+ let asJson = false;
313
+ for (let i = 0; i < args.length; i++) {
314
+ const a = args[i];
315
+ if (a === "--json") {
316
+ asJson = true;
317
+ continue;
318
+ }
319
+ const next = takeOpenPolicyFlag(args, i, policy, "stack adopt <owner/repo>#<pr>");
320
+ if (next !== null) {
321
+ i = next - 1;
322
+ continue;
323
+ }
324
+ if (a.startsWith("-")) {
325
+ throw new CommandError(`${STACK_USAGE}\nunknown flag: ${a}`);
326
+ }
327
+ targetArgs.push(a);
328
+ }
329
+ const target = parseAdoptTarget(targetArgs);
330
+ return {
331
+ ...target,
332
+ autoLand: policy.autoLand,
333
+ autoReview: policy.autoReview,
334
+ autoPatch: policy.autoPatch,
335
+ asJson,
336
+ };
337
+ }
338
+ export function parseStackSetArgs(args) {
339
+ const usage = "usage: mergestorm stack set <stack-id> [--auto-land on|off] [--auto-review on|off|default] [--auto-patch on|off|default] [--json]";
340
+ const verb = "stack set <stack-id>";
341
+ let stackId;
342
+ let autoLand;
343
+ let autoReview;
344
+ let autoPatch;
345
+ let asJson = false;
346
+ for (let i = 0; i < args.length; i++) {
347
+ const arg = args[i];
348
+ if (arg === "--json") {
349
+ asJson = true;
350
+ }
351
+ else if (arg === "--auto-land") {
352
+ autoLand = parseOnOff(args[++i], "--auto-land", verb);
353
+ }
354
+ else if (arg.startsWith("--auto-land=")) {
355
+ autoLand = parseOnOff(arg.slice("--auto-land=".length), "--auto-land", verb);
356
+ }
357
+ else if (arg === "--auto-review") {
358
+ autoReview = parseOnOffDefault(args[++i], "--auto-review", verb);
359
+ }
360
+ else if (arg.startsWith("--auto-review=")) {
361
+ autoReview = parseOnOffDefault(arg.slice("--auto-review=".length), "--auto-review", verb);
362
+ }
363
+ else if (arg === "--auto-patch") {
364
+ autoPatch = parseOnOffDefault(args[++i], "--auto-patch", verb);
365
+ }
366
+ else if (arg.startsWith("--auto-patch=")) {
367
+ autoPatch = parseOnOffDefault(arg.slice("--auto-patch=".length), "--auto-patch", verb);
368
+ }
369
+ else if (arg.startsWith("-") || stackId !== undefined) {
370
+ throw new CommandError(usage);
371
+ }
372
+ else {
373
+ stackId = arg;
374
+ }
375
+ }
376
+ if (autoLand === undefined && autoReview === undefined && autoPatch === undefined) {
377
+ throw new CommandError(usage);
181
378
  }
182
- return { extend, asJson };
379
+ return {
380
+ stackId: requireStackId(stackId, usage),
381
+ ...(autoLand !== undefined ? { autoLand } : {}),
382
+ ...(autoReview !== undefined ? { autoReview } : {}),
383
+ ...(autoPatch !== undefined ? { autoPatch } : {}),
384
+ asJson,
385
+ };
183
386
  }
184
387
  export function parseStackResetArgs(args) {
185
388
  if (args.length !== 1 || args[0] !== "--force") {
186
389
  throw new CommandError("usage: mergestorm stack reset --force\nClears only CLI-managed pre-submit state; branches, PRs, and registered stacks are unchanged.");
187
390
  }
188
391
  }
189
- async function cmdStackCreate(args, deps = {}) {
190
- const { name, onto, trunk: trunkFlag, extend, asJson } = parseStackCreateArgs(args);
392
+ export async function cmdStackCreate(args, deps = {}) {
393
+ const { name, onto, trunk: trunkFlag, extend, autoLand, autoReview, autoPatch, asJson, } = parseStackCreateArgs(args);
191
394
  const cwd = deps.cwd ?? process.cwd();
395
+ const gitTopLevelFn = deps.gitTopLevel ?? gitTopLevel;
396
+ const worktreeDirtyFn = deps.worktreeDirty ?? worktreeDirty;
397
+ const currentBranchFn = deps.currentBranch ?? currentBranch;
398
+ const branchExistsFn = deps.branchExists ?? branchExists;
399
+ const createBranchFromHeadFn = deps.createBranchFromHead ?? createBranchFromHead;
400
+ const deleteBranchFn = deps.deleteBranch ?? deleteBranch;
401
+ const discoverTrunkFn = deps.discoverTrunk ?? discoverTrunk;
402
+ const defaultLayerBranchNameFn = deps.defaultLayerBranchName ?? defaultLayerBranchName;
403
+ const loadStackMetaFn = deps.loadStackMeta ?? loadStackMeta;
404
+ const saveStackMetaFn = deps.saveStackMeta ?? saveStackMeta;
192
405
  const listStacksFn = deps.listStacks ?? listStacks;
193
406
  const loadConfigFn = deps.loadConfig ?? loadConfig;
194
407
  const parseOriginFn = deps.parseGithubOriginRepo ?? parseGithubOriginRepo;
195
408
  try {
196
- gitTopLevel(cwd);
409
+ gitTopLevelFn(cwd);
197
410
  }
198
411
  catch {
199
412
  throw new CommandError("Not in a git repository. Run `mg stack create` from inside the repository whose stack you want to author.");
200
413
  }
201
- if (worktreeDirty(cwd)) {
414
+ if (worktreeDirtyFn(cwd)) {
202
415
  throw new CommandError("Working tree is dirty. Commit or stash changes before `stack create`.");
203
416
  }
204
- const parentBranch = onto?.trim() || currentBranch(cwd);
417
+ const parentBranch = onto?.trim() || currentBranchFn(cwd);
205
418
  if (!parentBranch) {
206
419
  throw new CommandError("Detached HEAD. Check out a branch (or pass --onto <branch>) before `stack create`.");
207
420
  }
208
- if (!branchExists(parentBranch, cwd)) {
421
+ if (!branchExistsFn(parentBranch, cwd)) {
209
422
  throw new CommandError(`Parent branch not found: ${parentBranch}`);
210
423
  }
211
- const branch = defaultLayerBranchName(name);
212
- if (branchExists(branch, cwd)) {
424
+ const branch = defaultLayerBranchNameFn(name);
425
+ if (branchExistsFn(branch, cwd)) {
213
426
  throw new CommandError(`Branch already exists: ${branch}`);
214
427
  }
215
- let meta = await loadStackMeta(cwd);
216
- const trunk = trunkFlag?.trim() || meta?.trunk || discoverTrunk(cwd);
428
+ let meta = await loadStackMetaFn(cwd);
429
+ const trunk = trunkFlag?.trim() || meta?.trunk || discoverTrunkFn(cwd);
217
430
  if (!meta) {
218
431
  meta = emptyStackMeta(trunk);
219
432
  }
@@ -253,23 +466,28 @@ async function cmdStackCreate(args, deps = {}) {
253
466
  }
254
467
  try {
255
468
  meta = selectStackForCreate(meta, parentBranch);
469
+ meta = setActivePolicy(meta, {
470
+ autoEnqueueWhenReady: autoLand,
471
+ autoReviewOverride: autoReview,
472
+ autoPatchOverride: autoPatch,
473
+ });
256
474
  }
257
475
  catch (err) {
258
476
  throw new CommandError(err instanceof Error ? err.message : String(err));
259
477
  }
260
478
  try {
261
- createBranchFromHead(branch, cwd);
479
+ createBranchFromHeadFn(branch, cwd);
262
480
  }
263
481
  catch (err) {
264
482
  throw new CommandError(err instanceof Error ? err.message : String(err));
265
483
  }
266
484
  try {
267
485
  meta = appendLayer(meta, { branch, parentBranch });
268
- await saveStackMeta(meta, cwd);
486
+ await saveStackMetaFn(meta, cwd);
269
487
  }
270
488
  catch (err) {
271
489
  try {
272
- deleteBranch(branch, cwd);
490
+ deleteBranchFn(branch, cwd);
273
491
  }
274
492
  catch { /* rollback best-effort */ }
275
493
  throw new CommandError(err instanceof Error ? err.message : String(err));
@@ -288,16 +506,19 @@ async function cmdStackCreate(args, deps = {}) {
288
506
  }, null, 2));
289
507
  return;
290
508
  }
291
- console.log(ansi.green(` Created ${branch} (onto ${parentBranch})`));
509
+ const created = [
510
+ ansi.brightGreen(` Created ${branch} (onto ${parentBranch})`),
511
+ ];
292
512
  if (registeredHit) {
293
- console.log(ansi.dim(` extending registered stack ${registeredHit.stackId}` +
513
+ created.push(ansi.dim(` extending registered stack ${registeredHit.stackId}` +
294
514
  (registeredHit.prNumber > 0 ? ` (from #${registeredHit.prNumber})` : "")));
295
515
  }
296
- console.log(ansi.dim(` trunk: ${meta.trunk} · local stacks: ${meta.stacks.length} · active layers: ${layers.length}`));
297
- console.log(ansi.dim(" authoring state: managed automatically outside the repository"));
516
+ created.push(ansi.dim(` trunk: ${meta.trunk} · local stacks: ${meta.stacks.length} · active layers: ${layers.length}`));
517
+ created.push(ansi.dim(" authoring state: managed automatically outside the repository"));
518
+ await present("Stack", created);
298
519
  }
299
520
  export async function cmdStackSubmit(args, deps = {}) {
300
- const { extend, asJson } = parseStackSubmitArgs(args);
521
+ const { extend, autoLand, autoReview, autoPatch, asJson } = parseStackSubmitArgs(args);
301
522
  const cwd = deps.cwd ?? process.cwd();
302
523
  const gitTopLevelFn = deps.gitTopLevel ?? gitTopLevel;
303
524
  const loadStackMetaFn = deps.loadStackMeta ?? loadStackMeta;
@@ -368,6 +589,21 @@ export async function cmdStackSubmit(args, deps = {}) {
368
589
  owner,
369
590
  repo,
370
591
  });
592
+ // Explicit submit flags win; otherwise a fresh (unregistered) stack carries
593
+ // the choices remembered at `stack create`. Joining a registered stack
594
+ // never replays stored create-time flags onto it.
595
+ const stored = meta.stacks[meta.active];
596
+ const freshStack = plans.every((plan) => plan.stackId === null);
597
+ const pick = (flag, remembered) => typeof flag === "boolean"
598
+ ? flag
599
+ : freshStack && typeof remembered === "boolean"
600
+ ? remembered
601
+ : undefined;
602
+ const policy = openPolicyPatch({
603
+ autoLand: pick(autoLand, stored?.autoEnqueueWhenReady),
604
+ autoReview: pick(autoReview, stored?.autoReviewOverride),
605
+ autoPatch: pick(autoPatch, stored?.autoPatchOverride),
606
+ });
371
607
  for (const layer of layers) {
372
608
  if (!branchExistsFn(layer.branch, cwd)) {
373
609
  throw new CommandError(`Stack layer branch missing locally: ${layer.branch}`);
@@ -444,7 +680,7 @@ export async function cmdStackSubmit(args, deps = {}) {
444
680
  let stackId = plans.find((plan) => plan.stackId)?.stackId ?? null;
445
681
  const registerPr = async (prNumber) => {
446
682
  console.log(ansi.dim(` Registering stack via import (${owner}/${repo}#${prNumber}) …`));
447
- const body = await adoptStackFn(owner, repo, prNumber, cfg);
683
+ const body = await adoptStackFn(owner, repo, prNumber, cfg, policy);
448
684
  if (typeof body !== "object" || body === null) {
449
685
  throw new CommandError("Unexpected response shape from adopt API");
450
686
  }
@@ -495,22 +731,26 @@ export async function cmdStackSubmit(args, deps = {}) {
495
731
  }, null, 2));
496
732
  return;
497
733
  }
734
+ const summary = [];
498
735
  for (const row of opened) {
499
736
  const verb = row.created ? "opened" : "exists";
500
- console.log(ansi.green(` #${row.prNumber} ${row.branch} → ${row.base} (${verb})`));
737
+ summary.push(ansi.brightGreen(` #${row.prNumber} ${row.branch} → ${row.base} (${verb})`));
501
738
  }
502
739
  if (stackId) {
503
- console.log(ansi.green(` Registered stack ${stackId}`));
740
+ summary.push(ansi.brightGreen(` Registered stack ${stackId}`));
504
741
  }
505
742
  else {
506
- console.log(ansi.dim(" Stack registration returned no stack ID"));
743
+ summary.push(ansi.dim(" Stack registration returned no stack ID"));
507
744
  }
508
- console.log(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
745
+ summary.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
746
+ await present("Stack", summary);
509
747
  }
510
748
  async function cmdStackReset(args) {
511
749
  parseStackResetArgs(args);
512
750
  await resetStackMeta(process.cwd());
513
- console.log(ansi.green(" Cleared CLI-managed pre-submit stack state. Branches, PRs, and registered stacks were not changed."));
751
+ await present("Stack", [
752
+ ansi.brightGreen(" Cleared CLI-managed pre-submit stack state. Branches, PRs, and registered stacks were not changed."),
753
+ ]);
514
754
  }
515
755
  async function cmdStackList(args) {
516
756
  const asJson = args.includes("--json");
@@ -520,28 +760,60 @@ async function cmdStackList(args) {
520
760
  console.log(JSON.stringify({ stacks }, null, 2));
521
761
  return;
522
762
  }
763
+ if (canBrowse()) {
764
+ await runLineTabsBrowser({
765
+ tabs: [{ id: "yours", label: "Stacks", lines: buildStackListLines(stacks) }],
766
+ });
767
+ return;
768
+ }
523
769
  if (stacks.length === 0) {
524
- console.log(ansi.dim(" No stacks yet. Author with `stack create` → commit → `stack submit`."));
525
- console.log(ansi.dim(" Import an existing chain only: `stack adopt owner/repo#pr`"));
526
- console.log(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
770
+ await present("Stacks", [
771
+ ansi.dim(" No stacks yet. Author with `stack create` → commit → `stack submit`."),
772
+ ansi.dim(" Import an existing chain only: `stack adopt owner/repo#pr`"),
773
+ ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"),
774
+ ]);
527
775
  return;
528
776
  }
777
+ const listed = [];
529
778
  for (const s of stacks) {
530
- for (const line of formatStackHuman(s))
531
- console.log(line);
532
- console.log("");
779
+ listed.push(...formatStackHuman(s), "");
533
780
  }
534
- console.log(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
781
+ listed.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
782
+ await present("Stacks", listed);
535
783
  }
536
- async function cmdStackAdopt(args) {
537
- const asJson = args.includes("--json");
538
- const targetArgs = args.filter((a) => a !== "--json");
539
- if (targetArgs.length === 0) {
540
- throw new CommandError(`usage: mergestorm stack adopt <owner/repo>#<pr>`);
784
+ /** Human summary lines for a `stack set` write, one per flag. */
785
+ export function stackSetSummary(parsed) {
786
+ const tri = (value) => value === null ? "default (account setting)" : value ? "on" : "off";
787
+ const lines = [];
788
+ if (parsed.autoLand !== undefined) {
789
+ lines.push(`Auto land ${parsed.autoLand ? "on" : "off"}`);
541
790
  }
542
- const { owner, repo, prNumber } = parseAdoptTarget(targetArgs);
791
+ if (parsed.autoReview !== undefined) {
792
+ lines.push(`Auto-review ${tri(parsed.autoReview)}`);
793
+ }
794
+ if (parsed.autoPatch !== undefined) {
795
+ lines.push(`Auto-patch ${tri(parsed.autoPatch)}`);
796
+ }
797
+ return lines;
798
+ }
799
+ export async function cmdStackSet(args, deps = {}) {
800
+ const parsed = parseStackSetArgs(args);
801
+ const cfg = await (deps.loadConfig ?? loadConfig)();
802
+ const body = await (deps.setStackPolicy ?? setStackPolicy)(parsed.stackId, {
803
+ ...(parsed.autoLand !== undefined ? { autoEnqueueWhenReady: parsed.autoLand } : {}),
804
+ ...(parsed.autoReview !== undefined ? { autoReviewOverride: parsed.autoReview } : {}),
805
+ ...(parsed.autoPatch !== undefined ? { autoPatchOverride: parsed.autoPatch } : {}),
806
+ }, cfg);
807
+ if (parsed.asJson) {
808
+ console.log(JSON.stringify(body, null, 2));
809
+ return;
810
+ }
811
+ await present("Stack", stackSetSummary(parsed).map((line) => ansi.brightGreen(` ${line} for ${parsed.stackId}`)));
812
+ }
813
+ async function cmdStackAdopt(args) {
814
+ const { owner, repo, prNumber, autoLand, autoReview, autoPatch, asJson } = parseStackAdoptArgs(args);
543
815
  const cfg = await loadConfig();
544
- const body = await adoptStack(owner, repo, prNumber, cfg);
816
+ const body = await adoptStack(owner, repo, prNumber, cfg, openPolicyPatch({ autoLand, autoReview, autoPatch }));
545
817
  if (asJson) {
546
818
  console.log(JSON.stringify(body, null, 2));
547
819
  return;
@@ -552,13 +824,16 @@ async function cmdStackAdopt(args) {
552
824
  const data = body;
553
825
  const stack = data.stack;
554
826
  const chain = data.chain;
555
- console.log(ansi.green(` Imported ${owner}/${repo}#${prNumber}` +
556
- (stack?.id ? ` → ${stack.id}` : "") +
557
- (Array.isArray(chain) ? ` (${chain.length} layer(s))` : "")));
827
+ const imported = [
828
+ ansi.brightGreen(` Imported ${owner}/${repo}#${prNumber}` +
829
+ (stack?.id ? ` → ${stack.id}` : "") +
830
+ (Array.isArray(chain) ? ` (${chain.length} layer(s))` : "")),
831
+ ];
558
832
  if (stack?.trunkBranch) {
559
- console.log(ansi.dim(` trunk: ${stack.trunkBranch}`));
833
+ imported.push(ansi.dim(` trunk: ${stack.trunkBranch}`));
560
834
  }
561
- console.log(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
835
+ imported.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
836
+ await present("Stack", imported);
562
837
  }
563
838
  async function cmdStackRestack(args) {
564
839
  const asJson = args.includes("--json");
@@ -570,7 +845,9 @@ async function cmdStackRestack(args) {
570
845
  return;
571
846
  }
572
847
  const fromBranch = body.fromBranch;
573
- console.log(ansi.green(` Restack started` + (fromBranch ? ` from ${fromBranch}` : "") + ` (${id})`));
848
+ await present("Stack", [
849
+ ansi.brightGreen(` Restack started` + (fromBranch ? ` from ${fromBranch}` : "") + ` (${id})`),
850
+ ]);
574
851
  }
575
852
  async function cmdStackLand(args) {
576
853
  const asJson = args.includes("--json");
@@ -586,39 +863,31 @@ async function cmdStackLand(args) {
586
863
  const branch = body.mergedBranch;
587
864
  const uNumber = body.uNumber;
588
865
  if (typeof uNumber === "number" && uNumber > 0) {
589
- console.log(ansi.green(` Promoted` +
590
- (pr != null ? ` #${pr}` : "") +
591
- ` into U${uNumber}` +
592
- (branch ? ` (${branch})` : "") +
593
- ` on ${id}`));
866
+ await present("Stack", [
867
+ ansi.brightGreen(` Promoted` +
868
+ (pr != null ? ` #${pr}` : "") +
869
+ ` into U${uNumber}` +
870
+ (branch ? ` (${branch})` : "") +
871
+ ` on ${id}`),
872
+ ]);
594
873
  return;
595
874
  }
596
- console.log(ansi.green(` Landed` +
597
- (pr != null ? ` #${pr}` : "") +
598
- (branch ? ` (${branch})` : "") +
599
- ` on ${id}`));
600
- }
601
- async function cmdStackAutoLand(args) {
602
- const asJson = args.includes("--json");
603
- const filtered = args.filter((a) => a !== "--json");
604
- const mode = filtered[0]?.toLowerCase();
605
- const id = requireStackId(filtered[1], "usage: mergestorm stack auto-promote on|off <stack-id>");
606
- if (mode !== "on" && mode !== "off") {
607
- throw new CommandError("usage: mergestorm stack auto-promote on|off <stack-id>");
608
- }
609
- const cfg = await loadConfig();
610
- const body = await setStackAutoPromote(id, mode === "on", cfg);
611
- if (asJson) {
612
- console.log(JSON.stringify(body, null, 2));
613
- return;
614
- }
615
- console.log(ansi.green(` Auto-promote ${mode} for ${id}`));
875
+ await present("Stack", [
876
+ ansi.brightGreen(` Landed` +
877
+ (pr != null ? ` #${pr}` : "") +
878
+ (branch ? ` (${branch})` : "") +
879
+ ` on ${id}`),
880
+ ]);
616
881
  }
617
882
  export async function cmdStack(args) {
618
883
  const sub = args[0]?.toLowerCase();
619
884
  const rest = args.slice(1);
620
885
  // Help must exit 0 — print usage, do not throw CommandError.
621
886
  if (!sub || sub === "-h" || sub === "--help" || sub === "help") {
887
+ if (canBrowse()) {
888
+ await openHelpBrowser("stacks");
889
+ return;
890
+ }
622
891
  console.log(STACK_USAGE);
623
892
  return;
624
893
  }
@@ -630,17 +899,13 @@ export async function cmdStack(args) {
630
899
  return cmdStackReset(rest);
631
900
  if (sub === "list" || sub === "ls")
632
901
  return cmdStackList(rest);
902
+ if (sub === "set")
903
+ return cmdStackSet(rest);
633
904
  if (sub === "adopt")
634
905
  return cmdStackAdopt(rest);
635
906
  if (sub === "restack")
636
907
  return cmdStackRestack(rest);
637
908
  if (sub === "land" || sub === "land-next")
638
909
  return cmdStackLand(rest);
639
- if (sub === "auto-promote" ||
640
- sub === "autopromote" ||
641
- sub === "auto-land" ||
642
- sub === "autoland") {
643
- return cmdStackAutoLand(rest);
644
- }
645
910
  throw new CommandError(`${STACK_USAGE}\nunknown stack subcommand: ${sub}`);
646
911
  }
@@ -12,6 +12,8 @@ export type StatusOptions = {
12
12
  interactive?: boolean;
13
13
  /** Test seam; production uses the two-second poll interval. */
14
14
  pollIntervalMs?: number;
15
+ /** Shell: route --json through the hold-to-read panel instead of console.log. */
16
+ mode?: "oneshot" | "shell";
15
17
  };
16
18
  export declare function parseStatusArgs(args: string[], defaults?: {
17
19
  format: "json" | "pretty";