mergestorm 0.3.15 → 0.3.17

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.
@@ -1,4 +1,4 @@
1
- import { adoptStack, ensureUpperPark, landNextStack, listStacks, restackStack, } 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,7 +6,7 @@ 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
12
  import { runLineTabsBrowser } from "../ui/line-tabs.js";
@@ -14,14 +14,18 @@ import { present } from "../ui/present.js";
14
14
  import { canBrowse } from "./browse.js";
15
15
  import { openHelpBrowser } from "./help.js";
16
16
  const STACK_USAGE = `usage:
17
- mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--json]
18
- 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]
19
19
  mergestorm stack reset --force
20
20
  mergestorm stack list [--json]
21
- 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]
22
23
  mergestorm stack restack <stack-id> [--json]
23
24
  mergestorm stack land <stack-id> [--json]
24
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.
25
29
  stack submit opens PRs with a body generated from each layer tip commit.
26
30
  Layers 1–2 use the git parent as GitHub base. Layer 3+ opens onto the
27
31
  review-unit park freeze (mg-park-*) so adopt does not retarget after open.
@@ -126,9 +130,25 @@ export function buildStackListLines(stacks) {
126
130
  lines.push(ansi.dim(" create · submit · land <id> · q to close"));
127
131
  return lines;
128
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
+ }
129
146
  function formatStackHuman(s) {
130
147
  const lines = [];
131
- lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}`);
148
+ lines.push(` ${ansi.bold(`${s.owner}/${s.repo}`)} trunk=${s.trunkBranch}` +
149
+ stackPolicyLabels(s)
150
+ .map((label) => ` ${label}`)
151
+ .join(""));
132
152
  lines.push(` ${ansi.dim(s.id)}`);
133
153
  if (s.layers.length === 0) {
134
154
  lines.push(ansi.dim(" (no layers)"));
@@ -141,12 +161,45 @@ function formatStackHuman(s) {
141
161
  }
142
162
  return lines;
143
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
+ }
144
196
  /** Parse `stack create` argv into options. Exported for tests. */
145
197
  export function parseStackCreateArgs(args) {
146
198
  let name;
147
199
  let onto;
148
200
  let trunk;
149
201
  let extend = false;
202
+ const policy = {};
150
203
  let asJson = false;
151
204
  for (let i = 0; i < args.length; i++) {
152
205
  const a = args[i];
@@ -158,6 +211,11 @@ export function parseStackCreateArgs(args) {
158
211
  extend = true;
159
212
  continue;
160
213
  }
214
+ const next = takeOpenPolicyFlag(args, i, policy, "stack create");
215
+ if (next !== null) {
216
+ i = next - 1;
217
+ continue;
218
+ }
161
219
  if (a === "--onto") {
162
220
  const v = args[++i];
163
221
  if (!v) {
@@ -190,13 +248,24 @@ export function parseStackCreateArgs(args) {
190
248
  }
191
249
  name = a;
192
250
  }
193
- 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
+ };
194
261
  }
195
262
  /** Parse `stack submit` argv. Exported for tests. */
196
263
  export function parseStackSubmitArgs(args) {
197
264
  let extend = false;
265
+ const policy = {};
198
266
  let asJson = false;
199
- for (const a of args) {
267
+ for (let i = 0; i < args.length; i++) {
268
+ const a = args[i];
200
269
  if (a === "--json") {
201
270
  asJson = true;
202
271
  continue;
@@ -205,43 +274,159 @@ export function parseStackSubmitArgs(args) {
205
274
  extend = true;
206
275
  continue;
207
276
  }
208
- 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]");
209
283
  }
210
- return { extend, asJson };
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);
378
+ }
379
+ return {
380
+ stackId: requireStackId(stackId, usage),
381
+ ...(autoLand !== undefined ? { autoLand } : {}),
382
+ ...(autoReview !== undefined ? { autoReview } : {}),
383
+ ...(autoPatch !== undefined ? { autoPatch } : {}),
384
+ asJson,
385
+ };
211
386
  }
212
387
  export function parseStackResetArgs(args) {
213
388
  if (args.length !== 1 || args[0] !== "--force") {
214
389
  throw new CommandError("usage: mergestorm stack reset --force\nClears only CLI-managed pre-submit state; branches, PRs, and registered stacks are unchanged.");
215
390
  }
216
391
  }
217
- async function cmdStackCreate(args, deps = {}) {
218
- 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);
219
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;
220
405
  const listStacksFn = deps.listStacks ?? listStacks;
221
406
  const loadConfigFn = deps.loadConfig ?? loadConfig;
222
407
  const parseOriginFn = deps.parseGithubOriginRepo ?? parseGithubOriginRepo;
223
408
  try {
224
- gitTopLevel(cwd);
409
+ gitTopLevelFn(cwd);
225
410
  }
226
411
  catch {
227
412
  throw new CommandError("Not in a git repository. Run `mg stack create` from inside the repository whose stack you want to author.");
228
413
  }
229
- if (worktreeDirty(cwd)) {
414
+ if (worktreeDirtyFn(cwd)) {
230
415
  throw new CommandError("Working tree is dirty. Commit or stash changes before `stack create`.");
231
416
  }
232
- const parentBranch = onto?.trim() || currentBranch(cwd);
417
+ const parentBranch = onto?.trim() || currentBranchFn(cwd);
233
418
  if (!parentBranch) {
234
419
  throw new CommandError("Detached HEAD. Check out a branch (or pass --onto <branch>) before `stack create`.");
235
420
  }
236
- if (!branchExists(parentBranch, cwd)) {
421
+ if (!branchExistsFn(parentBranch, cwd)) {
237
422
  throw new CommandError(`Parent branch not found: ${parentBranch}`);
238
423
  }
239
- const branch = defaultLayerBranchName(name);
240
- if (branchExists(branch, cwd)) {
424
+ const branch = defaultLayerBranchNameFn(name);
425
+ if (branchExistsFn(branch, cwd)) {
241
426
  throw new CommandError(`Branch already exists: ${branch}`);
242
427
  }
243
- let meta = await loadStackMeta(cwd);
244
- const trunk = trunkFlag?.trim() || meta?.trunk || discoverTrunk(cwd);
428
+ let meta = await loadStackMetaFn(cwd);
429
+ const trunk = trunkFlag?.trim() || meta?.trunk || discoverTrunkFn(cwd);
245
430
  if (!meta) {
246
431
  meta = emptyStackMeta(trunk);
247
432
  }
@@ -281,23 +466,28 @@ async function cmdStackCreate(args, deps = {}) {
281
466
  }
282
467
  try {
283
468
  meta = selectStackForCreate(meta, parentBranch);
469
+ meta = setActivePolicy(meta, {
470
+ autoEnqueueWhenReady: autoLand,
471
+ autoReviewOverride: autoReview,
472
+ autoPatchOverride: autoPatch,
473
+ });
284
474
  }
285
475
  catch (err) {
286
476
  throw new CommandError(err instanceof Error ? err.message : String(err));
287
477
  }
288
478
  try {
289
- createBranchFromHead(branch, cwd);
479
+ createBranchFromHeadFn(branch, cwd);
290
480
  }
291
481
  catch (err) {
292
482
  throw new CommandError(err instanceof Error ? err.message : String(err));
293
483
  }
294
484
  try {
295
485
  meta = appendLayer(meta, { branch, parentBranch });
296
- await saveStackMeta(meta, cwd);
486
+ await saveStackMetaFn(meta, cwd);
297
487
  }
298
488
  catch (err) {
299
489
  try {
300
- deleteBranch(branch, cwd);
490
+ deleteBranchFn(branch, cwd);
301
491
  }
302
492
  catch { /* rollback best-effort */ }
303
493
  throw new CommandError(err instanceof Error ? err.message : String(err));
@@ -328,7 +518,13 @@ async function cmdStackCreate(args, deps = {}) {
328
518
  await present("Stack", created);
329
519
  }
330
520
  export async function cmdStackSubmit(args, deps = {}) {
331
- const { extend, asJson } = parseStackSubmitArgs(args);
521
+ const { extend, autoLand, autoReview, autoPatch, asJson } = parseStackSubmitArgs(args);
522
+ const progress = (message) => {
523
+ if (asJson)
524
+ console.error(message);
525
+ else
526
+ console.log(message);
527
+ };
332
528
  const cwd = deps.cwd ?? process.cwd();
333
529
  const gitTopLevelFn = deps.gitTopLevel ?? gitTopLevel;
334
530
  const loadStackMetaFn = deps.loadStackMeta ?? loadStackMeta;
@@ -399,11 +595,26 @@ export async function cmdStackSubmit(args, deps = {}) {
399
595
  owner,
400
596
  repo,
401
597
  });
598
+ // Explicit submit flags win; otherwise a fresh (unregistered) stack carries
599
+ // the choices remembered at `stack create`. Joining a registered stack
600
+ // never replays stored create-time flags onto it.
601
+ const stored = meta.stacks[meta.active];
602
+ const freshStack = plans.every((plan) => plan.stackId === null);
603
+ const pick = (flag, remembered) => typeof flag === "boolean"
604
+ ? flag
605
+ : freshStack && typeof remembered === "boolean"
606
+ ? remembered
607
+ : undefined;
608
+ const policy = openPolicyPatch({
609
+ autoLand: pick(autoLand, stored?.autoEnqueueWhenReady),
610
+ autoReview: pick(autoReview, stored?.autoReviewOverride),
611
+ autoPatch: pick(autoPatch, stored?.autoPatchOverride),
612
+ });
402
613
  for (const layer of layers) {
403
614
  if (!branchExistsFn(layer.branch, cwd)) {
404
615
  throw new CommandError(`Stack layer branch missing locally: ${layer.branch}`);
405
616
  }
406
- console.log(ansi.dim(` Pushing ${layer.branch} …`));
617
+ progress(ansi.dim(` Pushing ${layer.branch} …`));
407
618
  try {
408
619
  pushBranchFn(layer.branch, cwd);
409
620
  }
@@ -420,7 +631,7 @@ export async function cmdStackSubmit(args, deps = {}) {
420
631
  const openLayerPr = (branch, base) => {
421
632
  const existing = findOpenPrNumberFn(owner, repo, branch, cwd);
422
633
  if (existing != null) {
423
- console.log(ansi.dim(` PR #${existing} already open for ${branch}`));
634
+ progress(ansi.dim(` PR #${existing} already open for ${branch}`));
424
635
  return { prNumber: existing, created: false };
425
636
  }
426
637
  const aheadRef = isMgParkBranch(base) ? `origin/${base}` : base;
@@ -448,7 +659,7 @@ export async function cmdStackSubmit(args, deps = {}) {
448
659
  }
449
660
  const title = tipCommitSubjectFn(branch, cwd);
450
661
  const body = buildPrBodyFn(tipCommitMessageFn(branch, cwd));
451
- console.log(ansi.dim(` Opening PR ${branch} → ${base} …`));
662
+ progress(ansi.dim(` Opening PR ${branch} → ${base} …`));
452
663
  return {
453
664
  prNumber: createPrFn({
454
665
  owner,
@@ -474,8 +685,8 @@ export async function cmdStackSubmit(args, deps = {}) {
474
685
  let adoptData = null;
475
686
  let stackId = plans.find((plan) => plan.stackId)?.stackId ?? null;
476
687
  const registerPr = async (prNumber) => {
477
- console.log(ansi.dim(` Registering stack via import (${owner}/${repo}#${prNumber}) …`));
478
- const body = await adoptStackFn(owner, repo, prNumber, cfg);
688
+ progress(ansi.dim(` Registering stack via import (${owner}/${repo}#${prNumber}) …`));
689
+ const body = await adoptStackFn(owner, repo, prNumber, cfg, policy);
479
690
  if (typeof body !== "object" || body === null) {
480
691
  throw new CommandError("Unexpected response shape from adopt API");
481
692
  }
@@ -498,7 +709,7 @@ export async function cmdStackSubmit(args, deps = {}) {
498
709
  if (!stackId) {
499
710
  throw new CommandError("Cannot mint a park freeze before the stack is registered. Open layers 1–2 first, then retry.");
500
711
  }
501
- console.log(ansi.dim(` Ensuring upper-park freeze for stack ${stackId} …`));
712
+ progress(ansi.dim(` Ensuring upper-park freeze for stack ${stackId} …`));
502
713
  const park = await ensureUpperParkFn(stackId, cfg);
503
714
  for (const plan of pendingPark) {
504
715
  const { prNumber, created } = openLayerPr(plan.branch, park.freezeBranch);
@@ -576,15 +787,39 @@ async function cmdStackList(args) {
576
787
  listed.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
577
788
  await present("Stacks", listed);
578
789
  }
579
- async function cmdStackAdopt(args) {
580
- const asJson = args.includes("--json");
581
- const targetArgs = args.filter((a) => a !== "--json");
582
- if (targetArgs.length === 0) {
583
- throw new CommandError(`usage: mergestorm stack adopt <owner/repo>#<pr>`);
790
+ /** Human summary lines for a `stack set` write, one per flag. */
791
+ export function stackSetSummary(parsed) {
792
+ const tri = (value) => value === null ? "default (account setting)" : value ? "on" : "off";
793
+ const lines = [];
794
+ if (parsed.autoLand !== undefined) {
795
+ lines.push(`Auto land ${parsed.autoLand ? "on" : "off"}`);
584
796
  }
585
- const { owner, repo, prNumber } = parseAdoptTarget(targetArgs);
797
+ if (parsed.autoReview !== undefined) {
798
+ lines.push(`Auto-review ${tri(parsed.autoReview)}`);
799
+ }
800
+ if (parsed.autoPatch !== undefined) {
801
+ lines.push(`Auto-patch ${tri(parsed.autoPatch)}`);
802
+ }
803
+ return lines;
804
+ }
805
+ export async function cmdStackSet(args, deps = {}) {
806
+ const parsed = parseStackSetArgs(args);
807
+ const cfg = await (deps.loadConfig ?? loadConfig)();
808
+ const body = await (deps.setStackPolicy ?? setStackPolicy)(parsed.stackId, {
809
+ ...(parsed.autoLand !== undefined ? { autoEnqueueWhenReady: parsed.autoLand } : {}),
810
+ ...(parsed.autoReview !== undefined ? { autoReviewOverride: parsed.autoReview } : {}),
811
+ ...(parsed.autoPatch !== undefined ? { autoPatchOverride: parsed.autoPatch } : {}),
812
+ }, cfg);
813
+ if (parsed.asJson) {
814
+ console.log(JSON.stringify(body, null, 2));
815
+ return;
816
+ }
817
+ await present("Stack", stackSetSummary(parsed).map((line) => ansi.brightGreen(` ${line} for ${parsed.stackId}`)));
818
+ }
819
+ async function cmdStackAdopt(args) {
820
+ const { owner, repo, prNumber, autoLand, autoReview, autoPatch, asJson } = parseStackAdoptArgs(args);
586
821
  const cfg = await loadConfig();
587
- const body = await adoptStack(owner, repo, prNumber, cfg);
822
+ const body = await adoptStack(owner, repo, prNumber, cfg, openPolicyPatch({ autoLand, autoReview, autoPatch }));
588
823
  if (asJson) {
589
824
  console.log(JSON.stringify(body, null, 2));
590
825
  return;
@@ -670,6 +905,8 @@ export async function cmdStack(args) {
670
905
  return cmdStackReset(rest);
671
906
  if (sub === "list" || sub === "ls")
672
907
  return cmdStackList(rest);
908
+ if (sub === "set")
909
+ return cmdStackSet(rest);
673
910
  if (sub === "adopt")
674
911
  return cmdStackAdopt(rest);
675
912
  if (sub === "restack")
@@ -5,6 +5,7 @@ import { canBrowse, formatJobDetailLines } from "./browse.js";
5
5
  import { CommandError, DetachedError, REVIEW_EXIT, isCommandErrorCode, } from "../errors.js";
6
6
  import { ansi } from "../ui/ansi.js";
7
7
  import { renderFindings } from "../ui/findings.js";
8
+ import { humanSummary } from "../ui/human-summary.js";
8
9
  import { toReviewJobEnvelope, } from "../ui/job-envelope.js";
9
10
  import { REVIEW_POLL_DEFAULT_TIMEOUT_MS, ReviewPollTimeoutError, pollReview, } from "./review-client.js";
10
11
  const STATUS_USAGE = "usage: mergestorm status <job_id> [--json] [--wait] [--timeout seconds]";
@@ -71,8 +72,9 @@ function printPretty(row) {
71
72
  if (envelope.credits) {
72
73
  console.log(`Credits: ${envelope.credits.standard} standard`);
73
74
  }
74
- if (envelope.summary)
75
- console.log(envelope.summary);
75
+ const summary = envelope.summary ? humanSummary(envelope.summary) : null;
76
+ if (summary)
77
+ console.log(summary);
76
78
  if (envelope.error)
77
79
  console.log(ansi.red(`Error: ${envelope.error}`));
78
80
  renderFindings(envelope.findings, {
@@ -1,5 +1,5 @@
1
1
  import { getMe } from "../api.js";
2
- import { apiBase, configPath, loadConfig, resolveApiKey, keyDisplay } from "../config.js";
2
+ import { apiBase, configPath, loadConfig, resolveApiKey } 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";
@@ -16,14 +16,17 @@ export async function cmdWhoami(args, opts = {}) {
16
16
  return;
17
17
  }
18
18
  const me = await getMe(cfg);
19
+ if (!me) {
20
+ throw new CommandError("Live account details unavailable. Could not verify account with /me.");
21
+ }
19
22
  const payload = {
20
- key_prefix: me?.key.prefix ?? keyDisplay(key),
21
- key_name: me?.key.name ?? null,
22
- plan_key: me?.plan_key ?? null,
23
- plan_label_key: me?.plan_label_key ?? null,
23
+ key_prefix: me.key.prefix,
24
+ key_name: me.key.name ?? null,
25
+ plan_key: me.plan_key ?? null,
26
+ plan_label_key: me.plan_label_key ?? null,
24
27
  api_base: apiBase(cfg),
25
28
  config_path: configPath(),
26
- usage: me?.usage ?? null,
29
+ usage: me.usage ?? null,
27
30
  };
28
31
  if (asJson) {
29
32
  const text = JSON.stringify(payload, null, 2);
@@ -42,8 +45,5 @@ export async function cmdWhoami(args, opts = {}) {
42
45
  }
43
46
  lines.push(` API ${payload.api_base}`);
44
47
  lines.push(` Config ${payload.config_path}`);
45
- if (!me) {
46
- lines.push(ansi.dim(" (live account details unavailable — older API or offline)"));
47
- }
48
48
  await present("Account", lines);
49
49
  }
package/dist/git.js CHANGED
@@ -48,7 +48,7 @@ export async function changedNamesInSandbox(base, head, cwd = process.cwd(), san
48
48
  // even though their contents cannot be read from the working tree for upload.
49
49
  // -z disables core.quotePath quoting and NUL-terminates names, so paths with
50
50
  // spaces or non-ASCII characters survive the split verbatim.
51
- const names = git(["diff", "--name-only", "-z", "--diff-filter=ACMRDT", `${base}...${head}`], cwd)
51
+ const names = git(["diff", "--no-ext-diff", "--no-textconv", "--name-only", "-z", "--diff-filter=ACMRDT", `${base}...${head}`], cwd)
52
52
  .split("\0")
53
53
  .filter(Boolean);
54
54
  const kept = [];
@@ -2,7 +2,7 @@ export declare const LEGACY_STACK_META_REL: string;
2
2
  export declare function gitTopLevel(cwd?: string): string;
3
3
  export declare function gitCommonDir(cwd?: string): string;
4
4
  /**
5
- * A physical clone and all its linked worktrees share one authoring state.
5
+ * Each worktree has its own authoring state.
6
6
  * Independent clones remain isolated even when they point at the same remote.
7
7
  */
8
8
  export declare function stableRepoId(cwd?: string): string;
@@ -41,11 +41,22 @@ export function gitCommonDir(cwd = process.cwd()) {
41
41
  }
42
42
  }
43
43
  /**
44
- * A physical clone and all its linked worktrees share one authoring state.
44
+ * Each worktree has its own authoring state.
45
45
  * Independent clones remain isolated even when they point at the same remote.
46
46
  */
47
47
  export function stableRepoId(cwd = process.cwd()) {
48
- return createHash("sha256").update(gitCommonDir(cwd)).digest("hex").slice(0, 20);
48
+ let raw;
49
+ try {
50
+ raw = git(["rev-parse", "--git-dir"], cwd).trim();
51
+ }
52
+ catch (err) {
53
+ rethrowIfNotARepo(err);
54
+ }
55
+ const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
56
+ return createHash("sha256")
57
+ .update(realpathSync(absolute))
58
+ .digest("hex")
59
+ .slice(0, 20);
49
60
  }
50
61
  export function cliStacksRoot() {
51
62
  return path.join(mergestormHome(), "stacks");