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
@@ -6,8 +6,8 @@ export type StackBlocker = {
6
6
  blocker: string;
7
7
  bounceKind: string | null;
8
8
  };
9
- export type StackBlockerReason = "restack_conflict" | "restack_failed" | "draft_pr" | "merge_conflict" | "ci_failed" | "vortex_failed" | "vortex_out_of_quota" | "vortex_skipped" | "vortex_findings_need_person" | "seam_failed" | "seam_findings" | "seam_not_running" | "seam_review_stuck" | "seam_rereview_not_running" | "seam_verdict_stale" | "unit_abandoned" | "land_gate" | "bounce" | "auto_land_off" | "auto_land_switched_off" | "auto_land_ci_pending" | "auto_land_members_remaining" | "auto_land_vortex_wait" | "auto_land_promote_failed" | "auto_land_ci_unreadable" | "auto_land_waited" | "queue_stalled" | "land_pr_missing";
10
- export type ClassifiedStackBlocker = StackBlocker & {
9
+ export type StackBlockerReason = "restack_conflict" | "restack_failed" | "draft_pr" | "merge_conflict" | "ci_failed" | "vortex_failed" | "vortex_out_of_quota" | "vortex_skipped" | "vortex_findings_need_person" | "seam_failed" | "seam_findings" | "seam_not_running" | "seam_review_stuck" | "seam_rereview_not_running" | "seam_verdict_stale" | "unit_abandoned" | "land_gate" | "bounce" | "auto_land_off" | "auto_land_switched_off" | "auto_land_ci_pending" | "auto_land_members_remaining" | "auto_land_vortex_wait" | "auto_land_promote_failed" | "auto_land_ci_unreadable" | "auto_land_waited" | "queue_stalled" | "cyclone_stalled" | "land_pr_missing";
10
+ type ClassifiedStackBlocker = StackBlocker & {
11
11
  reason: StackBlockerReason;
12
12
  };
13
13
  export type StackAgentBusy = {
@@ -113,6 +113,14 @@ export type StackRepairHint = {
113
113
  since: string;
114
114
  reason: string;
115
115
  steps: string;
116
+ } | {
117
+ kind: "cyclone_stalled";
118
+ prNumber: number;
119
+ headSha: string | null;
120
+ branch: string;
121
+ runSha: string | null;
122
+ since: string | null;
123
+ steps: string;
116
124
  } | {
117
125
  kind: "queue_stalled";
118
126
  prNumber: number;
@@ -159,42 +167,14 @@ export type StackLandGatePending = {
159
167
  export declare function landGateIsPending(reason: string | null | undefined): boolean;
160
168
  export declare function layerAgentsBusy(layer: StackLayerDto): StackAgentsBusy;
161
169
  export declare function layerAgents(layer: StackLayerDto): StackLayerAgents;
162
- export declare function isMgParkBase(branch: string | null | undefined): boolean;
163
- /** GitHub DIRTY vs an mg-park-* freeze is not a merge conflict, verified or not. */
164
- export declare function boundDirty(layer: StackLayerDto): boolean;
165
- export declare function mergeabilityPending(layer: StackLayerDto): boolean;
166
170
  export declare function sameHead(a: string | null | undefined, b: string | null | undefined): boolean;
167
171
  export declare function currentLayer(stack: StackDto): StackLayerDto | undefined;
168
- export declare const RESTACK_FAILED = "Restack failed";
169
- export declare const SEAM_REVIEW_NOT_RUNNING = "Seam review pending, no review running";
170
- export declare const VORTEX_REVIEW_OUT_OF_QUOTA = "Review skipped, out of review quota";
171
- export declare const SEAM_REVIEW_STUCK = "Seam review stuck in reviewing, no review running";
172
- export declare const SEAM_REREVIEW_NOT_RUNNING = "Seam re-review pending, no review running";
173
- export declare const SEAM_VERDICT_STALE = "Seam verdict is for an older head, no review running";
174
172
  export declare const VORTEX_FINDINGS_NEED_PERSON = "Vortex findings need a person";
175
- export declare const CYCLONE_NOT_RUN_ATTENTION_MS: number;
176
- export declare const CYCLONE_NOT_RUN_REASON = "not_run";
177
173
  export declare function cycloneStatusWords(layer: Pick<StackLayerDto, "cycloneStatus" | "cycloneHandoff" | "headSha">): string | null;
178
174
  export declare function autoResolveActive(stack: StackDto, layer: StackLayerDto): boolean;
179
175
  export declare function autoFixCiActive(stack: StackDto, layer: StackLayerDto): boolean;
180
176
  export declare const CI_DID_NOT_FINISH_BLOCKER = "CI did not finish \u2014 left for a person";
181
- export declare function pendingCiLeftForPerson(stack: StackDto, layer: StackLayerDto): boolean;
182
- export declare const VORTEX_AUTO_REVIEW_OFF = "Vortex auto-review is off, so this head was not reviewed";
183
- export declare const VORTEX_BILLING_SKIPPED = "Vortex skipped this head: it could not confirm billing";
184
- export declare const VORTEX_REVIEW_INCOMPLETE = "Vortex reviewed only part of this PR";
185
- export declare const VORTEX_IGNORED_AUTHOR = "Vortex skipped this head: the PR author is on the ignored authors list";
186
- export declare const VORTEX_REPO_DISABLED = "Vortex skipped this head: reviews are turned off for this repository";
187
- export declare const VORTEX_RETRY_SCHEDULED = "Vortex hit a temporary error on this head and will retry by itself";
188
- export declare const VORTEX_REVIEW_FAILED = "Review failed";
189
- export declare function vortexRetryScheduled(layer: StackLayerDto): boolean;
190
- export declare const AUTO_LAND_CI_PENDING_ATTENTION_MS: number;
191
- export declare const AUTO_LAND_VORTEX_WAIT_ATTENTION_MS: number;
192
- export declare const AUTO_LAND_PROMOTE_FAILED_ATTEMPTS = 3;
193
- export declare const AUTO_LAND_CI_FAILED_BLOCKER = "CI failed \u2014 Auto land reads a failing check on GitHub at this head";
194
- export declare const AUTO_LAND_WAIT_ATTENTION_MS: number;
195
- export declare const AUTO_LAND_CI_UNREADABLE_ATTENTION_MS: number;
196
- export declare function autoLandWaitLayer(stack: StackDto): StackLayerDto | undefined;
197
- export declare const MERGE_QUEUE_STALL_ATTENTION_MS: number;
177
+ export declare const CYCLONE_STALLED_PREFIX = "Cyclone stalled: ";
198
178
  export type StackQueueWait = {
199
179
  prNumber: number;
200
180
  headSha: string | null;
@@ -216,15 +196,8 @@ export declare function mergeQueueWait(queue: readonly MergeQueueEntryDto[], now
216
196
  export declare function conflictLiveParent(stack: StackDto, layer: StackLayerDto): string | null;
217
197
  export declare function conflictRepairSteps(branch: string, liveParent: string | null): string;
218
198
  export declare const BREEZE_BRANCH_PREFIX = "breeze/";
219
- export declare function isBreezeBranch(branch: string | null | undefined): boolean;
220
- export declare function breezeConflictRepairSteps(branch: string, liveParent: string | null, prNumber: number): string;
221
- export declare function restackFailedRepairSteps(branch: string, liveParent: string | null, failure: {
222
- kind: string;
223
- detail?: string | null;
224
- }, prNumber?: number): string;
225
199
  export declare function agentsCannotClear(reason: StackBlockerReason, stack?: StackDto, layer?: StackLayerDto): boolean;
226
200
  export declare function stackRepair(stack: StackDto, attention: ClassifiedStackBlocker | null, layer: StackLayerDto | undefined, bounce: MergeQueueEntryDto | undefined, queueWait?: MergeQueueWait | null, nowMs?: number): StackRepairHint | null;
227
- export declare const LAND_PR_MISSING_PREFIX = "No open land PR for ";
228
201
  /** Pair-gate attention plus live hard blocks on every other open layer. */
229
202
  export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntryDto[], cursor?: Pick<StackWatchCursor, "bounceId" | "afterFinishedAt">, nowMs?: number): {
230
203
  attention: StackBlocker | null;
@@ -243,3 +216,4 @@ export declare function stackBlockers(stack: StackDto, entries?: MergeQueueEntry
243
216
  };
244
217
  /** Compact contract text shared by status and wait summaries. */
245
218
  export declare function stackBlockersSummary(attention: Pick<StackBlocker, "prNumber" | "blocker"> | null, issues: StackBlocker[], busy?: readonly StackAgentBusy[], landGatePending?: Pick<StackLandGatePending, "prNumber" | "reason"> | null, actAfter?: StackHeldBlocker["actAfter"] | null, queueWait?: Pick<StackQueueWait, "prNumber" | "text"> | null): string;
219
+ export {};
@@ -1,4 +1,4 @@
1
- import { CYCLONE_CI_FIX_HELD_REASON, CYCLONE_FIX_HELD_REASON, deriveStackAgentsBusy, mergeQueueBounceLabel, restackRetryPending, } from "./stack-dto.js";
1
+ import { CYCLONE_CI_FIX_HELD_REASON, CYCLONE_FIX_HELD_REASON, deriveStackAgentsBusy, mergeQueueBounceLabel, restackRetryPending, STACK_AGENT_PATCHING_STALE_MS, stalledCycloneRun, } from "./stack-dto.js";
2
2
  export function landGateIsPending(reason) {
3
3
  const token = reason?.trim().replace(/^landing blocked:\s*/i, "").match(/^[a-z_]+/i)?.[0] ?? "";
4
4
  return /_(?:pending|running)$/i.test(token);
@@ -18,11 +18,11 @@ export function layerAgents(layer) {
18
18
  busy: { vortex: busy.vortex, cyclone: busy.cyclone },
19
19
  };
20
20
  }
21
- export function isMgParkBase(branch) {
21
+ function isMgParkBase(branch) {
22
22
  return (branch ?? "").trim().toLowerCase().startsWith("mg-park-");
23
23
  }
24
24
  /** GitHub DIRTY vs an mg-park-* freeze is not a merge conflict, verified or not. */
25
- export function boundDirty(layer) {
25
+ function boundDirty(layer) {
26
26
  if (isMgParkBase(layer.parentBranch))
27
27
  return false;
28
28
  if (layer.mergeable == null)
@@ -30,7 +30,7 @@ export function boundDirty(layer) {
30
30
  return (!layer.headSha || layer.mergeableHeadSha === layer.headSha) &&
31
31
  (layer.mergeable === false || layer.mergeableState?.toLowerCase() === "dirty");
32
32
  }
33
- export function mergeabilityPending(layer) {
33
+ function mergeabilityPending(layer) {
34
34
  if (isMgParkBase(layer.parentBranch))
35
35
  return false;
36
36
  return layer.mergeable == null && !!layer.headSha && layer.mergeableHeadSha === layer.headSha &&
@@ -51,12 +51,12 @@ export function currentLayer(stack) {
51
51
  return promote[0] ?? open.find((layer) => layer.prNumber === stack.unit?.landPrNumber) ??
52
52
  (land && land.prNumber > 0 && land.state !== "merged" && land.state !== "closed" ? land : undefined);
53
53
  }
54
- export const RESTACK_FAILED = "Restack failed";
55
- export const SEAM_REVIEW_NOT_RUNNING = "Seam review pending, no review running";
56
- export const VORTEX_REVIEW_OUT_OF_QUOTA = "Review skipped, out of review quota";
57
- export const SEAM_REVIEW_STUCK = "Seam review stuck in reviewing, no review running";
58
- export const SEAM_REREVIEW_NOT_RUNNING = "Seam re-review pending, no review running";
59
- export const SEAM_VERDICT_STALE = "Seam verdict is for an older head, no review running";
54
+ const RESTACK_FAILED = "Restack failed";
55
+ const SEAM_REVIEW_NOT_RUNNING = "Seam review pending, no review running";
56
+ const VORTEX_REVIEW_OUT_OF_QUOTA = "Review skipped, out of review quota";
57
+ const SEAM_REVIEW_STUCK = "Seam review stuck in reviewing, no review running";
58
+ const SEAM_REREVIEW_NOT_RUNNING = "Seam re-review pending, no review running";
59
+ const SEAM_VERDICT_STALE = "Seam verdict is for an older head, no review running";
60
60
  const STUCK_SEAM_REASONS = ["seam_review_stuck", "seam_rereview_not_running", "seam_verdict_stale"];
61
61
  function coreReviewDoneAtHead(layer) {
62
62
  const review = layer.vortexReview;
@@ -72,8 +72,8 @@ function vortexFindingsRestAtHead(layer) {
72
72
  function vortexChangesRequestedAtHead(layer) {
73
73
  return vortexFindingsRestAtHead(layer) && layer.reviewStatus === "changes_requested";
74
74
  }
75
- export const CYCLONE_NOT_RUN_ATTENTION_MS = 30 * 60_000;
76
- export const CYCLONE_NOT_RUN_REASON = "not_run";
75
+ const CYCLONE_NOT_RUN_ATTENTION_MS = 30 * 60_000;
76
+ const CYCLONE_NOT_RUN_REASON = "not_run";
77
77
  function cycloneNotRunAtHead(layer, nowMs) {
78
78
  const review = layer.vortexReview;
79
79
  if (layer.cycloneFindingsTakenAtHead !== false || layer.vortexStatus !== "findings")
@@ -109,6 +109,7 @@ const CYCLONE_HANDOFF_WORDS = {
109
109
  unsupported_language: "the findings are in a language Cyclone does not patch",
110
110
  policy_owner_changed: "the stack's owner changed while the patch was queued",
111
111
  patch_job_lost: "Cyclone's patch job was lost before it finished",
112
+ run_deadline: "Cyclone's patch ran past its time limit and stopped without pushing",
112
113
  pr_state_unavailable: "Cyclone could not read the PR's state, so it pushed nothing",
113
114
  left_for_human: "Cyclone did not patch these findings and pushed nothing",
114
115
  hold: "the review told Cyclone to hold, so it did not patch these findings",
@@ -175,7 +176,7 @@ export function autoFixCiActive(stack, layer) {
175
176
  !sameHead(layer.cycloneCiHandoff?.headSha, layer.headSha);
176
177
  }
177
178
  export const CI_DID_NOT_FINISH_BLOCKER = "CI did not finish — left for a person";
178
- export function pendingCiLeftForPerson(stack, layer) {
179
+ function pendingCiLeftForPerson(stack, layer) {
179
180
  return layer.ciStatus === "pending" && layer.checks?.cancelledOnly === true &&
180
181
  sameHead(layer.checks.namedRunsCompleteHeadSha, layer.headSha) && !isLandPrLayer(stack, layer) &&
181
182
  sameHead(layer.cycloneCiHandoff?.headSha, layer.headSha);
@@ -200,6 +201,7 @@ const CYCLONE_HELD_FINDINGS_REASONS = [
200
201
  "pr_state_unavailable",
201
202
  "vortex_patch_cap",
202
203
  "patch_job_lost",
204
+ "run_deadline",
203
205
  "unsupported_language",
204
206
  "credits_exhausted",
205
207
  "no_billing",
@@ -227,13 +229,13 @@ function quotaSkippedReview(layer) {
227
229
  return review?.status === "skipped" && review.skip_reason === "quota_exceeded" &&
228
230
  sameHead(review.head_sha, layer.headSha);
229
231
  }
230
- export const VORTEX_AUTO_REVIEW_OFF = "Vortex auto-review is off, so this head was not reviewed";
231
- export const VORTEX_BILLING_SKIPPED = "Vortex skipped this head: it could not confirm billing";
232
- export const VORTEX_REVIEW_INCOMPLETE = "Vortex reviewed only part of this PR";
233
- export const VORTEX_IGNORED_AUTHOR = "Vortex skipped this head: the PR author is on the ignored authors list";
234
- export const VORTEX_REPO_DISABLED = "Vortex skipped this head: reviews are turned off for this repository";
235
- export const VORTEX_RETRY_SCHEDULED = "Vortex hit a temporary error on this head and will retry by itself";
236
- export const VORTEX_REVIEW_FAILED = "Review failed";
232
+ const VORTEX_AUTO_REVIEW_OFF = "Vortex auto-review is off, so this head was not reviewed";
233
+ const VORTEX_BILLING_SKIPPED = "Vortex skipped this head: it could not confirm billing";
234
+ const VORTEX_REVIEW_INCOMPLETE = "Vortex reviewed only part of this PR";
235
+ const VORTEX_IGNORED_AUTHOR = "Vortex skipped this head: the PR author is on the ignored authors list";
236
+ const VORTEX_REPO_DISABLED = "Vortex skipped this head: reviews are turned off for this repository";
237
+ const VORTEX_RETRY_SCHEDULED = "Vortex hit a temporary error on this head and will retry by itself";
238
+ const VORTEX_REVIEW_FAILED = "Review failed";
237
239
  const VORTEX_SKIP_BLOCKERS = {
238
240
  auto_review_off: VORTEX_AUTO_REVIEW_OFF,
239
241
  billing_blocked: VORTEX_BILLING_SKIPPED,
@@ -241,7 +243,7 @@ const VORTEX_SKIP_BLOCKERS = {
241
243
  ignored_author: VORTEX_IGNORED_AUTHOR,
242
244
  repo_disabled: VORTEX_REPO_DISABLED,
243
245
  };
244
- export function vortexRetryScheduled(layer) {
246
+ function vortexRetryScheduled(layer) {
245
247
  const review = layer.vortexReview;
246
248
  const reason = review?.skip_reason?.trim();
247
249
  return (reason === "retry_scheduled" || reason === "post_delayed") && sameHead(review?.head_sha, layer.headSha);
@@ -388,6 +390,9 @@ function layerAttention(stack, layer, queue, cursor, nowMs) {
388
390
  if (landing && !landGateIsPending(landing))
389
391
  return hardBlock(landing, "land_gate");
390
392
  }
393
+ const stalled = stalledCycloneRun(layer, nowMs);
394
+ if (stalled)
395
+ return hardBlock(cycloneStalledBlocker(stalled, nowMs), "cyclone_stalled");
391
396
  if (vortexFindingsLeftForPerson(stack, layer, nowMs))
392
397
  return hardBlock(VORTEX_FINDINGS_NEED_PERSON, "vortex_findings_need_person");
393
398
  }
@@ -408,6 +413,11 @@ function layerAttention(stack, layer, queue, cursor, nowMs) {
408
413
  return { blocker: label, reason: "bounce", bounceKind: kind, bounce };
409
414
  }
410
415
  const UNIT_ABANDONED_BLOCKER_PREFIX = "Review unit abandoned: based on ";
416
+ export const CYCLONE_STALLED_PREFIX = "Cyclone stalled: ";
417
+ function cycloneStalledBlocker(run, nowMs) {
418
+ const minutes = Math.floor((nowMs - Date.parse(run.startedAt ?? "")) / 60_000);
419
+ return `${CYCLONE_STALLED_PREFIX}patching ${run.sha?.slice(0, 7) ?? "this PR"} for ${minutes}m with no push or result`;
420
+ }
411
421
  function unitAbandonedUnder(stack, layer) {
412
422
  if (stack.unit?.state !== "abandoned")
413
423
  return false;
@@ -447,15 +457,15 @@ function switchedOffByAnotherActor(stack) {
447
457
  const reason = stack.autoLandOff?.reason;
448
458
  return reason && AUTO_LAND_SWITCHED_OFF_BY[reason] ? reason : undefined;
449
459
  }
450
- export const AUTO_LAND_CI_PENDING_ATTENTION_MS = 45 * 60_000;
451
- export const AUTO_LAND_VORTEX_WAIT_ATTENTION_MS = 30 * 60_000;
452
- export const AUTO_LAND_PROMOTE_FAILED_ATTEMPTS = 3;
453
- export const AUTO_LAND_CI_FAILED_BLOCKER = "CI failed — Auto land reads a failing check on GitHub at this head";
460
+ const AUTO_LAND_CI_PENDING_ATTENTION_MS = 45 * 60_000;
461
+ const AUTO_LAND_VORTEX_WAIT_ATTENTION_MS = 30 * 60_000;
462
+ const AUTO_LAND_PROMOTE_FAILED_ATTEMPTS = 3;
463
+ const AUTO_LAND_CI_FAILED_BLOCKER = "CI failed — Auto land reads a failing check on GitHub at this head";
454
464
  const AUTO_LAND_CI_PENDING_PREFIX = "CI pending for ";
455
465
  const AUTO_LAND_VORTEX_WAIT_PREFIX = "No green Vortex review for ";
456
466
  const AUTO_LAND_PROMOTE_FAILED_PREFIX = "Promote failed ";
457
- export const AUTO_LAND_WAIT_ATTENTION_MS = 45 * 60_000;
458
- export const AUTO_LAND_CI_UNREADABLE_ATTENTION_MS = 10 * 60_000;
467
+ const AUTO_LAND_WAIT_ATTENTION_MS = 45 * 60_000;
468
+ const AUTO_LAND_CI_UNREADABLE_ATTENTION_MS = 10 * 60_000;
459
469
  const AUTO_LAND_CI_UNREADABLE_PREFIX = "Mergestorm cannot read ";
460
470
  const AUTO_LAND_WAITED_PREFIX = "Auto land has waited ";
461
471
  const AUTO_LAND_MEMBERS_REMAINING_PREFIX = "The land PR cannot be queued: ";
@@ -510,7 +520,7 @@ function lastPrLayer(stack) {
510
520
  return land;
511
521
  return stack.layers.filter((layer) => layer.prNumber > 0).sort((a, b) => b.position - a.position)[0];
512
522
  }
513
- export function autoLandWaitLayer(stack) {
523
+ function autoLandWaitLayer(stack) {
514
524
  const wait = armedAutoLandWait(stack);
515
525
  if (!wait)
516
526
  return undefined;
@@ -578,7 +588,7 @@ function autoLandWaitNamed(stack, layer, nowMs) {
578
588
  ? { blocker: `${AUTO_LAND_WAITED_PREFIX}${minutes}m: ${autoLandWaitWords(wait.reason)}`, reason: "auto_land_waited" }
579
589
  : null;
580
590
  }
581
- export const MERGE_QUEUE_STALL_ATTENTION_MS = 45 * 60_000;
591
+ const MERGE_QUEUE_STALL_ATTENTION_MS = 45 * 60_000;
582
592
  const MERGE_QUEUE_STALLED_PREFIX = "Merge queue stuck: ";
583
593
  const LIVE_QUEUE_STATES = ["queued", "running", "waiting"];
584
594
  function minutesOf(ms) {
@@ -680,15 +690,15 @@ export function conflictRepairSteps(branch, liveParent) {
680
690
  return `Merge ${liveParent} into ${branch}: git fetch origin, check out ${branch} at its live remote head, git merge origin/${liveParent}, resolve the conflicts, run the tests, then an ordinary git push (no force). Do not rebase onto trunk, main, or mg-park-*, and do not retarget the PR base.`;
681
691
  }
682
692
  export const BREEZE_BRANCH_PREFIX = "breeze/";
683
- export function isBreezeBranch(branch) {
693
+ function isBreezeBranch(branch) {
684
694
  return typeof branch === "string" && branch.startsWith(BREEZE_BRANCH_PREFIX);
685
695
  }
686
- export function breezeConflictRepairSteps(branch, liveParent, prNumber) {
696
+ function breezeConflictRepairSteps(branch, liveParent, prNumber) {
687
697
  if (!liveParent)
688
698
  return conflictRepairSteps(branch, liveParent);
689
699
  return `${branch} is a Breeze branch: Breeze updates it only with open_pull_request, which publishes single-parent commits, so it cannot publish a merge of ${liveParent}. Rebase it in the Breeze chat that opened #${prNumber}: fetch ${liveParent} with fetch_repository, git rebase origin/${liveParent}, resolve the conflicts, run the tests, then call open_pull_request again. Or have Cyclone resolve it: cyclone_start with task resolve_conflicts and pr_number ${prNumber}. Do not retarget the PR base.`;
690
700
  }
691
- export function restackFailedRepairSteps(branch, liveParent, failure, prNumber) {
701
+ function restackFailedRepairSteps(branch, liveParent, failure, prNumber) {
692
702
  const why = `Mergestorm could not restack ${branch}${liveParent ? ` onto ${liveParent}` : ""} (${failure.kind}${failure.detail?.trim() ? `: ${failure.detail.trim()}` : ""}) and will not try this head again.`;
693
703
  if (!liveParent) {
694
704
  return `${why} The live parent of ${branch} is unknown (it is never an mg-park-* freeze). Do not merge or rebase anything; stop and tell the human.`;
@@ -815,6 +825,18 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null, n
815
825
  steps: `Auto land is off for this stack (autoLandOff reason ${offReason}: ${AUTO_LAND_SWITCHED_OFF_BY[offReason] ?? offReason}), so nothing will enqueue it and it will not land by itself. No bounce did this and nothing in the layers needs a change: do not patch or push for it. A person or another agent session may have switched it off on purpose, so do not turn Auto land back on yourself. Tell the human that Auto land is off and why, and turn it back on (mg stack set ${stack.id} --auto-land on) only when they say so.`,
816
826
  };
817
827
  }
828
+ if (reason === "cyclone_stalled") {
829
+ const run = stalledCycloneRun(layer, nowMs);
830
+ return {
831
+ kind: "cyclone_stalled",
832
+ prNumber: layer.prNumber,
833
+ headSha: layer.headSha ?? null,
834
+ branch: layer.branch,
835
+ runSha: run?.sha ?? null,
836
+ since: run?.startedAt ?? null,
837
+ steps: `Cyclone has been patching #${layer.prNumber}${run?.sha ? ` (a run for ${run.sha.slice(0, 7)})` : ""} since ${run?.startedAt ?? "an unknown time"} without pushing or finishing, past the ${STACK_AGENT_PATCHING_STALE_MS / 60_000}-minute limit, so the watch no longer waits for it. Patch the open Vortex findings at ${layer.headSha ?? "the head"} yourself: verify each one against a checkout of ${layer.branch}, fix the real ones with the smallest patch and an ordinary push, and dismiss the ones you judge wrong with mg dismiss. If ${layer.branch} moves before you push, Cyclone pushed after all: re-read stack_status instead of pushing. Tell the human Cyclone stalled on this PR.`,
838
+ };
839
+ }
818
840
  if (reason === "vortex_failed") {
819
841
  return {
820
842
  kind: "vortex_failed",
@@ -933,7 +955,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null, n
933
955
  }
934
956
  return null;
935
957
  }
936
- export const LAND_PR_MISSING_PREFIX = "No open land PR for ";
958
+ const LAND_PR_MISSING_PREFIX = "No open land PR for ";
937
959
  function landPrMissing(stack, candidate) {
938
960
  const unit = stack.unit;
939
961
  if (!unit || candidate || stack.archivedAt || unit.state === "landed" || unit.state === "abandoned")
@@ -120,7 +120,9 @@ export type StackAgentsBusy = {
120
120
  headSha: string | null;
121
121
  };
122
122
  export declare const STACK_AGENT_QUEUED_STALE_MS: number;
123
+ export declare const STACK_AGENT_PATCHING_STALE_MS: number;
123
124
  export declare const AUTO_RESOLVE_ENQUEUE_GRACE_MS: number;
125
+ export declare function stalledCycloneRun(layer: Pick<StackLayerDto, "agentRuns">, nowMs?: number): StackAgentRun | null;
124
126
  export declare function deriveStackAgentsBusy(layer: Pick<StackLayerDto, "headSha" | "vortexStatus" | "cycloneStatus" | "agentRuns" | "vortexReview" | "cycloneConflictPendingSince" | "cycloneCiPendingSince">, opts?: {
125
127
  cycloneClaimed?: boolean;
126
128
  nowMs?: number;
@@ -238,6 +240,7 @@ export type StackLayerDto = {
238
240
  headSha?: string | null;
239
241
  /** Head SHA that produced mergeable / mergeableState. */
240
242
  mergeableHeadSha?: string | null;
243
+ staleBaseMergeable?: boolean;
241
244
  /** GitHub PR draft flag from enrich; the auto-land watcher never enqueues a draft. */
242
245
  draft?: boolean | null;
243
246
  /**
package/dist/stack-dto.js CHANGED
@@ -26,6 +26,7 @@ export const AUTO_LAND_OFF_REASONS = ["user", "archive", "watcher", "cancelled",
26
26
  export const CYCLONE_FIX_HELD_REASON = "fix_held";
27
27
  export const CYCLONE_CI_FIX_HELD_REASON = "ci_fix_held";
28
28
  export const STACK_AGENT_QUEUED_STALE_MS = 15 * 60 * 1000;
29
+ export const STACK_AGENT_PATCHING_STALE_MS = 60 * 60 * 1000;
29
30
  export const AUTO_RESOLVE_ENQUEUE_GRACE_MS = 2 * 60_000;
30
31
  function agentShaMayBeHead(sha, head) {
31
32
  const left = sha?.trim().toLowerCase();
@@ -34,6 +35,17 @@ function agentShaMayBeHead(sha, head) {
34
35
  return false;
35
36
  return left.startsWith(right) || right.startsWith(left);
36
37
  }
38
+ export function stalledCycloneRun(layer, nowMs = Date.now()) {
39
+ const patching = (layer.agentRuns ?? [])
40
+ .filter((run) => !run.finishedAt && run.agent === "cyclone" && run.status === "patching")
41
+ .map((run) => ({ run, startedMs: Date.parse(run.startedAt ?? "") }));
42
+ if (!patching.length)
43
+ return null;
44
+ if (!patching.every(({ startedMs }) => Number.isFinite(startedMs) && nowMs - startedMs > STACK_AGENT_PATCHING_STALE_MS)) {
45
+ return null;
46
+ }
47
+ return patching.reduce((newest, entry) => (entry.startedMs > newest.startedMs ? entry : newest)).run;
48
+ }
37
49
  export function deriveStackAgentsBusy(layer, opts = {}) {
38
50
  const headSha = layer.headSha?.trim() || null;
39
51
  const nowMs = opts.nowMs ?? Date.now();
@@ -54,8 +66,9 @@ export function deriveStackAgentsBusy(layer, opts = {}) {
54
66
  const sinceMs = Date.parse(since ?? "");
55
67
  return Number.isFinite(sinceMs) && nowMs - sinceMs <= AUTO_RESOLVE_ENQUEUE_GRACE_MS;
56
68
  };
57
- const cyclone = opts.cycloneClaimed === true || layer.cycloneStatus === "patching" ||
58
- runs.some((run) => run.agent === "cyclone" && run.status === "patching") ||
69
+ const stalled = stalledCycloneRun(layer, nowMs) !== null;
70
+ const cyclone = (!stalled && (opts.cycloneClaimed === true || layer.cycloneStatus === "patching" ||
71
+ runs.some((run) => run.agent === "cyclone" && run.status === "patching"))) ||
59
72
  runs.some((run) => run.agent === "cyclone" && agentShaMayBeHead(run.sha, headSha) && queuedFresh(run)) ||
60
73
  withinGrace(layer.cycloneConflictPendingSince) ||
61
74
  withinGrace(layer.cycloneCiPendingSince);
@@ -3,7 +3,7 @@ export type StackLayerMeta = {
3
3
  /** Parent branch name (trunk for the bottom layer). */
4
4
  parentBranch: string;
5
5
  };
6
- export type LocalStack = {
6
+ type LocalStack = {
7
7
  layers: StackLayerMeta[];
8
8
  autoEnqueueWhenReady?: boolean;
9
9
  /** Per-open override carried from `stack create` to `stack submit`; absent = account flag. */
@@ -22,7 +22,6 @@ export type StackMeta = {
22
22
  export declare function stackMetaPath(cwd?: string, stacksRoot?: string): string;
23
23
  export declare function loadStackMeta(cwd?: string, stacksRoot?: string): Promise<StackMeta | null>;
24
24
  export declare function emptyStackMeta(trunk: string): StackMeta;
25
- export declare function activeStack(meta: StackMeta): LocalStack;
26
25
  export declare function activeLayers(meta: StackMeta): StackLayerMeta[];
27
26
  export declare function setActiveAutoEnqueue(meta: StackMeta, enabled: boolean | undefined): StackMeta;
28
27
  /** Per-open automation flags remembered on the active local stack until submit. */
@@ -45,3 +44,4 @@ export declare function appendLayer(meta: StackMeta, layer: StackLayerMeta): Sta
45
44
  export declare function selectStackForCreate(meta: StackMeta, parentBranch: string): StackMeta;
46
45
  /** Drop the active stack after a successful submit. */
47
46
  export declare function removeActiveStack(meta: StackMeta): StackMeta;
47
+ export {};
@@ -102,7 +102,7 @@ export function emptyStackMeta(trunk) {
102
102
  active: 0,
103
103
  };
104
104
  }
105
- export function activeStack(meta) {
105
+ function activeStack(meta) {
106
106
  const stack = meta.stacks[meta.active];
107
107
  if (!stack) {
108
108
  throw new Error("Stack authoring state has an out-of-range active stack; run `mg stack reset --force` to clear it.");
@@ -1,21 +1,14 @@
1
1
  import type { Config } from "./config.js";
2
2
  import { readLandedQueueEntry } from "./api.js";
3
3
  import { pollStackWatch, type StackWatchCursor, type StackWatchEnvelope } from "./stack-watch.js";
4
- export declare const STACK_WATCH_MARKER = "MS-WATCH";
5
4
  export declare const STACK_WATCH_EXIT: {
6
5
  readonly landed: 0;
7
6
  readonly attention: 3;
8
7
  readonly timeout: 5;
9
8
  };
10
9
  export declare const STACK_WATCH_SLICE_MS = 300000;
11
- export declare const STACK_WATCH_RECHECK_MS = 30000;
12
- export declare const STACK_WATCH_FAILED_RETRY_MS = 15000;
13
- export declare const STACK_WATCH_RATE_LIMIT_WAIT_MS = 30000;
14
- export declare const STACK_WATCH_TRANSIENT_WINDOW_MS: number;
15
- export declare const STACK_WATCH_TRANSIENT_MAX_WAIT_MS = 60000;
16
- export declare function isTransientWatchFailure(err: unknown): boolean;
17
10
  export type StackWatchUntil = "attention" | "landed";
18
- export type StackWatchOutcome = "attention" | "landed" | "failed" | "timeout" | "aborted";
11
+ type StackWatchOutcome = "attention" | "landed" | "failed" | "timeout" | "aborted";
19
12
  export type RunStackWatchOptions = {
20
13
  until?: StackWatchUntil;
21
14
  ignore?: readonly string[];
@@ -35,10 +28,6 @@ export type StackWatchLoopResult = {
35
28
  exitCode: number;
36
29
  envelope: StackWatchEnvelope | null;
37
30
  };
38
- export declare function blockerWithoutElapsedMinutes(blocker: string | null | undefined): string;
39
- export declare function stackWatchAttentionKey(envelope: Pick<StackWatchEnvelope, "prNumber" | "headSha" | "blocker">): string;
40
- export declare function stackWatchAttentionLine(envelope: StackWatchEnvelope): string;
41
- export declare function stackWatchLandedLine(envelope: StackWatchEnvelope): string;
42
31
  export declare function stackWatchFailedLine(stackId: string, message: string): string;
43
- export declare function stackWatchTimeoutLine(stackId: string, maxMs: number): string;
44
32
  export declare function runStackWatch(cfg: Config, stackId: string, opts?: RunStackWatchOptions): Promise<StackWatchLoopResult>;
33
+ export {};
@@ -3,16 +3,16 @@ import { API_TIMEOUT_PREFIX, DEFAULT_API_TIMEOUT_MS, LandedQueueReadError, readL
3
3
  import { isTransientReviewPollError } from "./commands/review-client.js";
4
4
  import { stackWatchBackgroundCommand } from "./stack-watch-obligation.js";
5
5
  import { pollStackWatch, StackWatchError, StackWatchTimeoutError, } from "./stack-watch.js";
6
- export const STACK_WATCH_MARKER = "MS-WATCH";
6
+ const STACK_WATCH_MARKER = "MS-WATCH";
7
7
  export const STACK_WATCH_EXIT = { landed: 0, attention: 3, timeout: 5 };
8
8
  export const STACK_WATCH_SLICE_MS = 300_000;
9
- export const STACK_WATCH_RECHECK_MS = 30_000;
10
- export const STACK_WATCH_FAILED_RETRY_MS = 15_000;
11
- export const STACK_WATCH_RATE_LIMIT_WAIT_MS = 30_000;
12
- export const STACK_WATCH_TRANSIENT_WINDOW_MS = 15 * 60_000;
13
- export const STACK_WATCH_TRANSIENT_MAX_WAIT_MS = 60_000;
9
+ const STACK_WATCH_RECHECK_MS = 30_000;
10
+ const STACK_WATCH_FAILED_RETRY_MS = 15_000;
11
+ const STACK_WATCH_RATE_LIMIT_WAIT_MS = 30_000;
12
+ const STACK_WATCH_TRANSIENT_WINDOW_MS = 15 * 60_000;
13
+ const STACK_WATCH_TRANSIENT_MAX_WAIT_MS = 60_000;
14
14
  const TRANSIENT_WATCH_FAILURE = /HTTP (?:408|429|5\d\d)|snapshot missing|fetch failed|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|socket hang up|network|connection/i;
15
- export function isTransientWatchFailure(err) {
15
+ function isTransientWatchFailure(err) {
16
16
  if (!(err instanceof Error))
17
17
  return false;
18
18
  if (err instanceof LandedQueueReadError)
@@ -24,24 +24,24 @@ export function isTransientWatchFailure(err) {
24
24
  return err.message.startsWith(API_TIMEOUT_PREFIX) || TRANSIENT_WATCH_FAILURE.test(err.message);
25
25
  }
26
26
  const ELAPSED_MINUTES = /\b\d+m\b/g;
27
- export function blockerWithoutElapsedMinutes(blocker) {
27
+ function blockerWithoutElapsedMinutes(blocker) {
28
28
  return (blocker ?? "").replace(ELAPSED_MINUTES, "Nm");
29
29
  }
30
- export function stackWatchAttentionKey(envelope) {
30
+ function stackWatchAttentionKey(envelope) {
31
31
  return `${envelope.prNumber ?? ""}|${envelope.headSha ?? ""}|${blockerWithoutElapsedMinutes(envelope.blocker)}`;
32
32
  }
33
- export function stackWatchAttentionLine(envelope) {
33
+ function stackWatchAttentionLine(envelope) {
34
34
  const restart = envelope.cursor.bounceId
35
35
  ? ` restart=${JSON.stringify(stackWatchBackgroundCommand(envelope.stackId, envelope.cursor))}` : "";
36
36
  return `${STACK_WATCH_MARKER} ATTENTION pr=${envelope.prNumber ?? "none"} head=${envelope.headSha ?? "none"} blocker=${JSON.stringify(envelope.blocker ?? "")} repair=${envelope.repair?.kind ?? "none"}${restart}`;
37
37
  }
38
- export function stackWatchLandedLine(envelope) {
38
+ function stackWatchLandedLine(envelope) {
39
39
  return `${STACK_WATCH_MARKER} LANDED stack=${envelope.stackId} reason=${envelope.watch.reason}`;
40
40
  }
41
41
  export function stackWatchFailedLine(stackId, message) {
42
42
  return `${STACK_WATCH_MARKER} ATTENTION failed stack=${stackId} error=${JSON.stringify(message)}`;
43
43
  }
44
- export function stackWatchTimeoutLine(stackId, maxMs) {
44
+ function stackWatchTimeoutLine(stackId, maxMs) {
45
45
  return `${STACK_WATCH_MARKER} TIMEOUT stack=${stackId} after=${Math.round(maxMs / 60_000)}m`;
46
46
  }
47
47
  function isAbort(err, signal) {
@@ -6,7 +6,6 @@
6
6
  * `opened` and double-wake Vortex.
7
7
  */
8
8
  import type { StackDto } from "./api.js";
9
- export declare const MG_PARK_PREFIX = "mg-park-";
10
9
  export declare function isMgParkBranch(branch: string): boolean;
11
10
  /** Live freeze already used as a GitHub base on this stack, if any. */
12
11
  export declare function parkFreezeBranchFromStack(stack: StackDto): string | null;
@@ -13,7 +13,7 @@ function registeredParentHit(stacks, parentBranch, owner, repo) {
13
13
  }
14
14
  return null;
15
15
  }
16
- export const MG_PARK_PREFIX = "mg-park-";
16
+ const MG_PARK_PREFIX = "mg-park-";
17
17
  export function isMgParkBranch(branch) {
18
18
  return branch.trim().toLowerCase().startsWith(MG_PARK_PREFIX);
19
19
  }
package/dist/ui/ansi.d.ts CHANGED
@@ -1,14 +1,4 @@
1
- /**
2
- * Brand ink. xterm-256 77 (#5FAF5F) � muted green that Terminal.app
3
- * actually paints. 24-bit `38;2;61;�` was read as palette 61 (purple).
4
- */
5
- export declare const BRAND_GREEN: {
6
- readonly hex: "#5FAF5F";
7
- readonly index: 77;
8
- };
9
1
  export declare const BRAND_GREEN_SGR: string;
10
- /** Full-cell fill. Glyphs like ? leave a hairline; a space with this bg does not. */
11
- export declare const BRAND_FILL_SGR: string;
12
2
  export declare const ansi: {
13
3
  readonly enabled: boolean;
14
4
  green: (t: string) => string;
package/dist/ui/ansi.js CHANGED
@@ -15,10 +15,10 @@ function wrap(code, text) {
15
15
  * Brand ink. xterm-256 77 (#5FAF5F) � muted green that Terminal.app
16
16
  * actually paints. 24-bit `38;2;61;�` was read as palette 61 (purple).
17
17
  */
18
- export const BRAND_GREEN = { hex: "#5FAF5F", index: 77 };
18
+ const BRAND_GREEN = { hex: "#5FAF5F", index: 77 };
19
19
  export const BRAND_GREEN_SGR = `38;5;${BRAND_GREEN.index}`;
20
20
  /** Full-cell fill. Glyphs like ? leave a hairline; a space with this bg does not. */
21
- export const BRAND_FILL_SGR = `${BRAND_GREEN_SGR};48;5;${BRAND_GREEN.index}`;
21
+ const BRAND_FILL_SGR = `${BRAND_GREEN_SGR};48;5;${BRAND_GREEN.index}`;
22
22
  function brandGreen(text, bold = false) {
23
23
  if (!computeEnabled())
24
24
  return text;
@@ -7,11 +7,12 @@ export type BannerState = {
7
7
  me: MeResponse | null;
8
8
  key: string | undefined;
9
9
  keyInvalid: boolean;
10
+ account?: string | null;
10
11
  };
11
12
  /** Full welcome, mid pane, or Claude-style short chrome that still shows the mark. */
12
13
  export type BannerDensity = "full" | "compact" | "mini" | "nano";
13
14
  /** `true` = compact. Strings pick the short-pane densities. */
14
- export type BannerVariant = boolean | "mini" | "nano";
15
+ type BannerVariant = boolean | "mini" | "nano";
15
16
  /** Handle returned by {@link printBannerHeader}; `rows` reflows for a resize. */
16
17
  export type BannerHandle = {
17
18
  rows(columns?: number, variant?: BannerVariant, frame?: number): string[];
@@ -35,3 +36,4 @@ export declare function printBannerHeader(opts?: {
35
36
  paint?: boolean;
36
37
  }): Promise<BannerHandle>;
37
38
  export declare function shellPrompt(_loggedIn: boolean): string;
39
+ export {};
package/dist/ui/banner.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getMe } from "../api.js";
2
2
  import { CommandError } from "../errors.js";
3
- import { keyDisplay, loadConfig, resolveApiKey } from "../config.js";
3
+ import { keyDisplay, loadConfig, resolveApiKey, settleAccountName } from "../config.js";
4
4
  import { cliVersion } from "../version.js";
5
5
  import { ansi } from "./ansi.js";
6
6
  import { frameWidth, padVisible, preferBoxedUi, roundedBox, sideBySide, terminalColumns, visibleWidth, } from "./box.js";
@@ -230,6 +230,12 @@ const WHATS_NEW = {
230
230
  "0.3.64": [
231
231
  "Memory commands and Overdrive merge batches",
232
232
  ],
233
+ "0.3.65": [
234
+ "Up to 3 accounts: `account use`, `--account <name>`",
235
+ ],
236
+ "0.3.66": [
237
+ "`issue wait`; submit needs an approved issue",
238
+ ],
233
239
  };
234
240
  function densityFromVariant(variant) {
235
241
  if (variant === "mini" || variant === "nano")
@@ -247,7 +253,7 @@ function statusLine(state) {
247
253
  if (state.keyInvalid) {
248
254
  return `${ansi.dim("○")} API key invalid or revoked · type ${ansi.brightGreen("login")} to sign in again`;
249
255
  }
250
- const display = state.me?.key.prefix ?? keyDisplay(state.key) ?? "msk_live_…";
256
+ const display = state.account ?? state.me?.key.prefix ?? keyDisplay(state.key) ?? "msk_live_…";
251
257
  if (!state.me) {
252
258
  return `${ansi.brightGreen("●")} ${ansi.bold(display)} ${ansi.dim("· account details unavailable")}`;
253
259
  }
@@ -373,7 +379,8 @@ export async function printBannerHeader(opts) {
373
379
  }
374
380
  }
375
381
  }
376
- const state = { version: cliVersion(), me, key, keyInvalid };
382
+ const account = me ? await settleAccountName(cfg, me) : cfg.account;
383
+ const state = { version: cliVersion(), me, key, keyInvalid, account: account ?? null };
377
384
  if (opts?.paint !== false) {
378
385
  for (const line of buildBannerRows(state)) {
379
386
  console.log(line);
package/dist/ui/box.d.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  import { padVisible, sliceVisible, visibleWidth } from "./width.js";
2
2
  export { padVisible, sliceVisible, visibleWidth };
3
- /** Below this width, boxed chrome wraps on real TTYs — prefer plain output. */
4
- export declare const MIN_BOXED_COLUMNS = 48;
5
3
  /**
6
4
  * Usable terminal columns (TTY), with a sensible fallback for pipes/tests.
7
5
  * Pass `columns` to measure a stream other than process.stdout (or a
package/dist/ui/box.js CHANGED
@@ -5,7 +5,7 @@ import { padVisible, sliceVisible, visibleWidth } from "./width.js";
5
5
  // importers (banner, prompt) keep working.
6
6
  export { padVisible, sliceVisible, visibleWidth };
7
7
  /** Below this width, boxed chrome wraps on real TTYs — prefer plain output. */
8
- export const MIN_BOXED_COLUMNS = 48;
8
+ const MIN_BOXED_COLUMNS = 48;
9
9
  /**
10
10
  * Usable terminal columns (TTY), with a sensible fallback for pipes/tests.
11
11
  * Pass `columns` to measure a stream other than process.stdout (or a