mergestorm 0.3.64 → 0.3.67

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 (76) hide show
  1. package/README.md +28 -9
  2. package/dist/api.d.ts +77 -11
  3. package/dist/api.js +228 -6
  4. package/dist/automation-catalog.d.ts +9 -1
  5. package/dist/automation-catalog.js +2 -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 +157 -31
  18. package/dist/commands/login.js +46 -14
  19. package/dist/commands/logout.js +6 -7
  20. package/dist/commands/memory.d.ts +2 -3
  21. package/dist/commands/memory.js +8 -6
  22. package/dist/commands/registry.d.ts +1 -0
  23. package/dist/commands/registry.js +35 -11
  24. package/dist/commands/review-client.d.ts +0 -3
  25. package/dist/commands/review-client.js +3 -3
  26. package/dist/commands/review.d.ts +1 -1
  27. package/dist/commands/review.js +1 -1
  28. package/dist/commands/settings.d.ts +3 -5
  29. package/dist/commands/settings.js +5 -3
  30. package/dist/commands/stack.d.ts +19 -5
  31. package/dist/commands/stack.js +88 -4
  32. package/dist/commands/whoami.js +14 -10
  33. package/dist/config.d.ts +55 -2
  34. package/dist/config.js +264 -13
  35. package/dist/errors.d.ts +1 -1
  36. package/dist/gh.d.ts +0 -4
  37. package/dist/gh.js +4 -4
  38. package/dist/git.d.ts +1 -1
  39. package/dist/git.js +2 -2
  40. package/dist/memory-api.d.ts +1 -0
  41. package/dist/memory-api.js +2 -2
  42. package/dist/node-version.d.ts +0 -2
  43. package/dist/node-version.js +1 -1
  44. package/dist/pr-body.d.ts +4 -0
  45. package/dist/pr-body.js +15 -0
  46. package/dist/repo-identity.d.ts +2 -1
  47. package/dist/repo-identity.js +8 -4
  48. package/dist/server-publish.d.ts +1 -24
  49. package/dist/server-publish.js +9 -9
  50. package/dist/stack-blockers.d.ts +12 -38
  51. package/dist/stack-blockers.js +57 -33
  52. package/dist/stack-dto.d.ts +3 -0
  53. package/dist/stack-dto.js +15 -2
  54. package/dist/stack-meta.d.ts +2 -2
  55. package/dist/stack-meta.js +1 -1
  56. package/dist/stack-watch-loop.d.ts +2 -13
  57. package/dist/stack-watch-loop.js +12 -12
  58. package/dist/submit-pr-base.d.ts +0 -1
  59. package/dist/submit-pr-base.js +1 -1
  60. package/dist/ui/ansi.d.ts +0 -10
  61. package/dist/ui/ansi.js +2 -2
  62. package/dist/ui/banner.d.ts +3 -1
  63. package/dist/ui/banner.js +13 -3
  64. package/dist/ui/box.d.ts +0 -2
  65. package/dist/ui/box.js +1 -1
  66. package/dist/ui/prompt.d.ts +0 -11
  67. package/dist/ui/prompt.js +7 -7
  68. package/dist/ui/tabs.d.ts +2 -2
  69. package/dist/ui/tabs.js +1 -1
  70. package/dist/ui/width.d.ts +0 -9
  71. package/dist/ui/width.js +3 -3
  72. package/dist/web-api.d.ts +17 -0
  73. package/dist/web-api.js +30 -0
  74. package/package.json +1 -1
  75. package/skill/SKILL.md +4 -0
  76. package/skill/mergestorm-bounce-watch/SKILL.md +1 -1
@@ -1,27 +1,10 @@
1
- import { getStackPublication, submitStackPublication, type StackPublication, type StackPublicationChange, type StackPublicationCommit, type StackPublicationIdentity, type StackPublicationStatus } from "./api.js";
1
+ import { getStackPublication, submitStackPublication, type StackPublication, type StackPublicationCommit, type StackPublicationStatus } from "./api.js";
2
2
  import { loadConfig, type Config } from "./config.js";
3
3
  import { discoverTrunk } from "./git-stack.js";
4
- export declare const SERVER_PUBLISH_MAX_COMMITS = 20;
5
- export declare const SERVER_PUBLISH_MAX_CHANGES = 500;
6
4
  export declare const SERVER_PUBLISH_MAX_FILE_BYTES: number;
7
- export declare const SERVER_PUBLISH_MAX_TOTAL_BYTES: number;
8
- export declare const SERVER_PUBLISH_MAX_COMMIT_BYTES: number;
9
- export declare const SERVER_PUBLISH_FILE_MODES: readonly ["100644", "100755", "120000"];
10
- export declare const SERVER_PUBLISH_HISTORY_SCAN = 500;
11
5
  export type GitRead = (args: string[], cwd: string) => Buffer | Promise<Buffer>;
12
6
  export declare const READ_ONLY_GIT: Set<string>;
13
7
  export declare function readGit(args: string[], cwd: string): Buffer;
14
- export declare function readCommitHeader(read: GitRead, sha: string, cwd: string): Promise<{
15
- tree: string;
16
- parents: string[];
17
- author: StackPublicationIdentity;
18
- committer: StackPublicationIdentity;
19
- message: string;
20
- }>;
21
- export declare function readCommitChanges(read: GitRead, parent: string, sha: string, cwd: string, budget: {
22
- changes: number;
23
- bytes: number;
24
- }): Promise<StackPublicationChange[]>;
25
8
  export declare function readPublicationCommits(read: GitRead, from: string, to: string, cwd: string): Promise<StackPublicationCommit[]>;
26
9
  export type ServerPublishInput = {
27
10
  cfg: Config;
@@ -73,12 +56,6 @@ export declare function assertExplicitStacking(input: {
73
56
  stackCommand: string;
74
57
  trunkCommand: string;
75
58
  }): void;
76
- export declare function workingBranchTarget(input: WorkingBranchInput): {
77
- cwd: string;
78
- owner: string;
79
- repo: string;
80
- branch: string;
81
- };
82
59
  export declare function publishWorkingBranch(input: WorkingBranchInput, deps?: WorkingBranchDeps): Promise<ServerPublishResult & {
83
60
  stacked: boolean;
84
61
  }>;
@@ -5,13 +5,13 @@ import { CommandError, isCommandErrorCode } from "./errors.js";
5
5
  import { parseGithubOriginRepo } from "./git.js";
6
6
  import { currentBranch, discoverTrunk, tipCommitMessage, tipCommitSubject } from "./git-stack.js";
7
7
  import { buildPrBodyFromCommit } from "./pr-body.js";
8
- export const SERVER_PUBLISH_MAX_COMMITS = 20;
9
- export const SERVER_PUBLISH_MAX_CHANGES = 500;
8
+ const SERVER_PUBLISH_MAX_COMMITS = 20;
9
+ const SERVER_PUBLISH_MAX_CHANGES = 500;
10
10
  export const SERVER_PUBLISH_MAX_FILE_BYTES = 2 * 1024 * 1024;
11
- export const SERVER_PUBLISH_MAX_TOTAL_BYTES = 8 * 1024 * 1024;
12
- export const SERVER_PUBLISH_MAX_COMMIT_BYTES = 1024 * 1024;
13
- export const SERVER_PUBLISH_FILE_MODES = ["100644", "100755", "120000"];
14
- export const SERVER_PUBLISH_HISTORY_SCAN = 500;
11
+ const SERVER_PUBLISH_MAX_TOTAL_BYTES = 8 * 1024 * 1024;
12
+ const SERVER_PUBLISH_MAX_COMMIT_BYTES = 1024 * 1024;
13
+ const SERVER_PUBLISH_FILE_MODES = ["100644", "100755", "120000"];
14
+ const SERVER_PUBLISH_HISTORY_SCAN = 500;
15
15
  export const READ_ONLY_GIT = new Set(["cat-file", "diff-tree", "merge-base", "rev-list", "rev-parse", "symbolic-ref"]);
16
16
  export function readGit(args, cwd) {
17
17
  if (!READ_ONLY_GIT.has(args[0] ?? "")) {
@@ -56,7 +56,7 @@ function parseIdentity(line, sha) {
56
56
  }
57
57
  return { name: match[1].trim() ? match[1] : match[2], email: match[2], date };
58
58
  }
59
- export async function readCommitHeader(read, sha, cwd) {
59
+ async function readCommitHeader(read, sha, cwd) {
60
60
  const commitBytes = Number(await text(read, ["cat-file", "-s", sha], cwd));
61
61
  if (commitBytes > SERVER_PUBLISH_MAX_COMMIT_BYTES) {
62
62
  throw new CommandError(`Commit ${sha} is ${commitBytes} bytes; server-side publication reads commits up to ${SERVER_PUBLISH_MAX_COMMIT_BYTES} bytes.`, 1, "payload_too_large");
@@ -91,7 +91,7 @@ export async function readCommitHeader(read, sha, cwd) {
91
91
  }
92
92
  return { tree, parents, author: parseIdentity(author, sha), committer: parseIdentity(committer, sha), message };
93
93
  }
94
- export async function readCommitChanges(read, parent, sha, cwd, budget) {
94
+ async function readCommitChanges(read, parent, sha, cwd, budget) {
95
95
  const listing = await read(["diff-tree", "-r", "-z", "--raw", "--no-renames", "--no-commit-id", "--abbrev=40", parent, sha], cwd);
96
96
  let raw;
97
97
  try {
@@ -244,7 +244,7 @@ export function assertExplicitStacking(input) {
244
244
  `• Stack onto \`${input.parent}\`: ${input.stackCommand}\n` +
245
245
  `• New stack from \`${input.trunk}\`: ${input.trunkCommand}`, 1, "implicit_stack");
246
246
  }
247
- export function workingBranchTarget(input) {
247
+ function workingBranchTarget(input) {
248
248
  const cwd = input.cwd ?? process.cwd();
249
249
  const origin = input.owner && input.repo ? { owner: input.owner, repo: input.repo } : parseGithubOriginRepo(cwd);
250
250
  if (!origin) {
@@ -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: ";
@@ -477,6 +487,8 @@ const AUTO_LAND_WAIT_WORDS = {
477
487
  missing_head: "GitHub returned no head commit for this PR",
478
488
  pr_draft: "this PR is a draft",
479
489
  trunk_in_land_target_without_land_pr: "the stack's commits are already in the land target, but no merged land PR was found",
490
+ land_pr_push_unreviewed: "someone other than Stacks pushed to the stack trunk, and the land PR has no clean Vortex review at that push or at its head",
491
+ land_pr_head_unexplained: "GitHub's activity list for the stack trunk does not show the push that made the land PR head",
480
492
  conflict_unchanged: "the merge queue bounced this PR for a conflict, and neither the PR nor its base has changed since",
481
493
  land_pr_unavailable: "the land PR cannot be opened or found",
482
494
  github_unreadable: "Mergestorm cannot read this PR from GitHub",
@@ -510,7 +522,7 @@ function lastPrLayer(stack) {
510
522
  return land;
511
523
  return stack.layers.filter((layer) => layer.prNumber > 0).sort((a, b) => b.position - a.position)[0];
512
524
  }
513
- export function autoLandWaitLayer(stack) {
525
+ function autoLandWaitLayer(stack) {
514
526
  const wait = armedAutoLandWait(stack);
515
527
  if (!wait)
516
528
  return undefined;
@@ -578,7 +590,7 @@ function autoLandWaitNamed(stack, layer, nowMs) {
578
590
  ? { blocker: `${AUTO_LAND_WAITED_PREFIX}${minutes}m: ${autoLandWaitWords(wait.reason)}`, reason: "auto_land_waited" }
579
591
  : null;
580
592
  }
581
- export const MERGE_QUEUE_STALL_ATTENTION_MS = 45 * 60_000;
593
+ const MERGE_QUEUE_STALL_ATTENTION_MS = 45 * 60_000;
582
594
  const MERGE_QUEUE_STALLED_PREFIX = "Merge queue stuck: ";
583
595
  const LIVE_QUEUE_STATES = ["queued", "running", "waiting"];
584
596
  function minutesOf(ms) {
@@ -680,15 +692,15 @@ export function conflictRepairSteps(branch, liveParent) {
680
692
  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
693
  }
682
694
  export const BREEZE_BRANCH_PREFIX = "breeze/";
683
- export function isBreezeBranch(branch) {
695
+ function isBreezeBranch(branch) {
684
696
  return typeof branch === "string" && branch.startsWith(BREEZE_BRANCH_PREFIX);
685
697
  }
686
- export function breezeConflictRepairSteps(branch, liveParent, prNumber) {
698
+ function breezeConflictRepairSteps(branch, liveParent, prNumber) {
687
699
  if (!liveParent)
688
700
  return conflictRepairSteps(branch, liveParent);
689
701
  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
702
  }
691
- export function restackFailedRepairSteps(branch, liveParent, failure, prNumber) {
703
+ function restackFailedRepairSteps(branch, liveParent, failure, prNumber) {
692
704
  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
705
  if (!liveParent) {
694
706
  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 +827,18 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null, n
815
827
  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
828
  };
817
829
  }
830
+ if (reason === "cyclone_stalled") {
831
+ const run = stalledCycloneRun(layer, nowMs);
832
+ return {
833
+ kind: "cyclone_stalled",
834
+ prNumber: layer.prNumber,
835
+ headSha: layer.headSha ?? null,
836
+ branch: layer.branch,
837
+ runSha: run?.sha ?? null,
838
+ since: run?.startedAt ?? null,
839
+ 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.`,
840
+ };
841
+ }
818
842
  if (reason === "vortex_failed") {
819
843
  return {
820
844
  kind: "vortex_failed",
@@ -933,7 +957,7 @@ export function stackRepair(stack, attention, layer, bounce, queueWait = null, n
933
957
  }
934
958
  return null;
935
959
  }
936
- export const LAND_PR_MISSING_PREFIX = "No open land PR for ";
960
+ const LAND_PR_MISSING_PREFIX = "No open land PR for ";
937
961
  function landPrMissing(stack, candidate) {
938
962
  const unit = stack.unit;
939
963
  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;