mergestorm 0.3.62 → 0.3.64

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.
@@ -4,7 +4,7 @@ import { adoptStack, ensureUpperPark, findStackPull, getEnrichedStack, getStackP
4
4
  import { loadConfig } from "../config.js";
5
5
  import { createPrs, findOpenPrNumber } from "../gh.js";
6
6
  import { parseGithubOriginRepo } from "../git.js";
7
- import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty } from "../git-stack.js";
7
+ import { branchExists, commitsAheadOf, createBranchFrom, createBranchFromHead, trunkStartRef, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty } from "../git-stack.js";
8
8
  import { buildPrBodyFromCommit } from "../pr-body.js";
9
9
  import { publishBranchServerSide, type WorkingBranchDeps } from "../server-publish.js";
10
10
  import { gitTopLevel } from "../repo-identity.js";
@@ -33,6 +33,11 @@ export declare function findRegisteredParent(stacks: StackDto[], parentBranch: s
33
33
  * Local unsubmitted parents are fine; only API-listed layers trip this.
34
34
  */
35
35
  export declare function assertMayParentOntoRegistered(parentBranch: string, stacks: StackDto[], extend: boolean, verb: "create" | "submit", owner?: string, repo?: string): RegisteredParentHit | null;
36
+ export declare function stackShapeLine(input: {
37
+ layers: number;
38
+ base: string;
39
+ extendedStack: boolean;
40
+ }): string;
36
41
  export declare function requireStackId(raw: string | undefined, usage: string): string;
37
42
  /** Parse `owner/repo#12` or `owner/repo 12`. */
38
43
  export declare function parseAdoptTarget(args: string[]): {
@@ -110,6 +115,8 @@ export type StackCreateDeps = {
110
115
  currentBranch?: typeof currentBranch;
111
116
  branchExists?: typeof branchExists;
112
117
  createBranchFromHead?: typeof createBranchFromHead;
118
+ createBranchFrom?: typeof createBranchFrom;
119
+ trunkStartRef?: typeof trunkStartRef;
113
120
  deleteBranch?: typeof deleteBranch;
114
121
  discoverTrunk?: typeof discoverTrunk;
115
122
  defaultLayerBranchName?: typeof defaultLayerBranchName;
@@ -156,6 +163,7 @@ export declare function parseStackPublishArgs(args: string[]): {
156
163
  title?: string;
157
164
  replaceHead?: string;
158
165
  autoLand?: boolean;
166
+ extend: boolean;
159
167
  status: boolean;
160
168
  asJson: boolean;
161
169
  };
@@ -172,6 +180,7 @@ export declare function cmdStackWatch(args: string[], deps?: {
172
180
  loadConfig?: typeof loadConfig;
173
181
  runStackWatch?: typeof runStackWatch;
174
182
  signal?: AbortSignal;
183
+ writeMarker?: (line: string) => void;
175
184
  }): Promise<void>;
176
185
  export declare function cmdStackStatus(args: string[], deps?: {
177
186
  loadConfig?: typeof loadConfig;
@@ -5,13 +5,13 @@ import { loadConfig } from "../config.js";
5
5
  import { rateLimitedMessage, REVIEW_EXIT, CommandError } from "../errors.js";
6
6
  import { createPrs, findOpenPrNumber, ghReady, githubApiFromEnv, } from "../gh.js";
7
7
  import { parseGithubOriginRepo } from "../git.js";
8
- import { branchExists, commitsAheadOf, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
8
+ import { branchExists, commitsAheadOf, createBranchFrom, createBranchFromHead, trunkStartRef, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, fetchRemoteBranch, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
9
9
  import { buildPrBodyFromCommit } from "../pr-body.js";
10
- import { publishBranchServerSide, publishWorkingBranch, workingBranchPublication, } from "../server-publish.js";
10
+ import { publishBranchServerSide, assertExplicitStacking, publishWorkingBranch, workingBranchPublication, } from "../server-publish.js";
11
11
  import { gitTopLevel } from "../repo-identity.js";
12
12
  import { activeLayers, appendLayer, emptyStackMeta, loadStackMeta, removeActiveStack, resetStackMeta, saveStackMeta, selectStackForCreate, setActivePolicy, } from "../stack-meta.js";
13
13
  import { pollStackWatch, StackWatchError, StackWatchTimeoutError, } from "../stack-watch.js";
14
- import { runStackWatch, STACK_WATCH_EXIT } from "../stack-watch-loop.js";
14
+ import { runStackWatch, STACK_WATCH_EXIT, stackWatchFailedLine } from "../stack-watch-loop.js";
15
15
  import { isMgParkBranch, planSubmitLayerBases } from "../submit-pr-base.js";
16
16
  import { ansi } from "../ui/ansi.js";
17
17
  import { runLineTabsBrowser } from "../ui/line-tabs.js";
@@ -52,7 +52,7 @@ export function openStackWatch(stackId, opts = {}) {
52
52
  const STACK_USAGE = `usage:
53
53
  mergestorm stack create [name] [--onto <branch>] [--trunk <branch>] [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--auto-resolve-conflicts on|off] [--auto-fix-ci on|off] [--json]
54
54
  mergestorm stack submit [--extend] [--auto-land on|off] [--auto-review on|off] [--auto-patch on|off] [--auto-resolve-conflicts on|off] [--auto-fix-ci on|off] [--rest] [--server-side] [--json]
55
- mergestorm stack publish [--branch <branch>] [--base <branch>] [--title <title>] [--replace-head <sha>] [--auto-land on|off] [--status] [--json]
55
+ mergestorm stack publish [--branch <branch>] [--base <branch>] [--extend] [--title <title>] [--replace-head <sha>] [--auto-land on|off] [--status] [--json]
56
56
  mergestorm stack reset --force
57
57
  mergestorm stack list [--json]
58
58
  mergestorm stack status <stack-id> [--json]
@@ -62,7 +62,8 @@ const STACK_USAGE = `usage:
62
62
  mergestorm stack watch <stack-id> [--until attention|landed] [--ignore <blocker-substring>] [--max <minutes>] [--head <sha>] [--bounce-id <id>] [--after-finished-at <iso>] [--json]
63
63
  Loops stack wait with the cursor and stays silent while the stack is waiting or in progress.
64
64
  Prints one MS-WATCH ATTENTION line and exits 3 on attention or on reads that keep failing (a permanent
65
- error twice, or 15 minutes of timeouts and 5xx, which it retries with backoff); prints MS-WATCH LANDED
65
+ error twice, or 15 minutes with no stack read: timeouts, 5xx and rate limits, which it retries with backoff).
66
+ It also prints MS-WATCH ATTENTION failed when it cannot start (bad flags, no config), so it never exits without an MS-WATCH line unless you stop it; prints MS-WATCH LANDED
66
67
  and exits 0 only for a confirmed landing, including a stack Mergestorm deleted after its queue entry landed. Other terminal reasons exit 3 with ATTENTION. Run it as a background command.
67
68
  An ATTENTION line for a merge queue bounce ends with restart="mg stack watch <id> --bounce-id <id> --after-finished-at <iso>":
68
69
  restart with that command (plus --head <sha> after a push) so a bounce you already handled is not reported again.
@@ -95,10 +96,13 @@ const STACK_USAGE = `usage:
95
96
  you name that head with --replace-head. stack publish --status reads how far
96
97
  a branch got (absent, pushed, opened, registered) without changing anything;
97
98
  after a lost response or a failure, run the same command again to resume.
98
- Parenting onto a branch that already belongs to a registered (submitted)
99
- stack requires --extend on create and submit — otherwise mg starts a new
100
- stack from trunk. stack land promotes into the review unit when one exists;
101
- otherwise lands the bottom open PR (unit-less stacks).`;
99
+ stack create starts a new single PR from trunk, even on another PR's
100
+ branch. --extend stacks it on the checked-out branch (or --onto <branch>)
101
+ instead; --onto or publish --base a branch other than trunk without
102
+ --extend is refused. A layer on a registered stack also needs --extend on
103
+ submit. create, submit and publish say which one they made. stack land
104
+ promotes into the review unit when one exists; otherwise lands the bottom
105
+ open PR (unit-less stacks).`;
102
106
  /** Cap the registered-stack guard's API probe so it cannot stall local authoring. */
103
107
  const STACK_GUARD_TIMEOUT_MS = 8_000;
104
108
  async function listRegisteredStacksRetryingOnce(list, cfg) {
@@ -152,6 +156,13 @@ export function assertMayParentOntoRegistered(parentBranch, stacks, extend, verb
152
156
  `• New independent stack: check out trunk and pass \`--onto <trunk>\` (e.g. \`mg stack create --onto main\`).\n` +
153
157
  `• Intentionally grow that stack: pass \`--extend\` on \`stack ${verb}\`.`, 1, "registered_parent");
154
158
  }
159
+ export function stackShapeLine(input) {
160
+ if (input.extendedStack)
161
+ return ` Added ${input.layers === 1 ? "1 layer" : `${input.layers} layers`} to an existing stack (--extend).`;
162
+ if (input.layers === 1)
163
+ return ` A new single PR onto ${input.base}.`;
164
+ return ` A new stack of ${input.layers} layers onto ${input.base}.`;
165
+ }
155
166
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
156
167
  export function requireStackId(raw, usage) {
157
168
  const id = raw?.trim() ?? "";
@@ -525,6 +536,8 @@ export async function cmdStackCreate(args, deps = {}) {
525
536
  const currentBranchFn = deps.currentBranch ?? currentBranch;
526
537
  const branchExistsFn = deps.branchExists ?? branchExists;
527
538
  const createBranchFromHeadFn = deps.createBranchFromHead ?? createBranchFromHead;
539
+ const createBranchFromFn = deps.createBranchFrom ?? createBranchFrom;
540
+ const trunkStartRefFn = deps.trunkStartRef ?? trunkStartRef;
528
541
  const deleteBranchFn = deps.deleteBranch ?? deleteBranch;
529
542
  const discoverTrunkFn = deps.discoverTrunk ?? discoverTrunk;
530
543
  const defaultLayerBranchNameFn = deps.defaultLayerBranchName ?? defaultLayerBranchName;
@@ -542,17 +555,6 @@ export async function cmdStackCreate(args, deps = {}) {
542
555
  if (worktreeDirtyFn(cwd)) {
543
556
  throw new CommandError("Working tree is dirty. Commit or stash changes before `stack create`.");
544
557
  }
545
- const parentBranch = onto?.trim() || currentBranchFn(cwd);
546
- if (!parentBranch) {
547
- throw new CommandError("Detached HEAD. Check out a branch (or pass --onto <branch>) before `stack create`.");
548
- }
549
- if (!branchExistsFn(parentBranch, cwd)) {
550
- throw new CommandError(`Parent branch not found: ${parentBranch}`);
551
- }
552
- const branch = defaultLayerBranchNameFn(name);
553
- if (branchExistsFn(branch, cwd)) {
554
- throw new CommandError(`Branch already exists: ${branch}`);
555
- }
556
558
  let meta = await loadStackMetaFn(cwd);
557
559
  const trunk = trunkFlag?.trim() || meta?.trunk || discoverTrunkFn(cwd);
558
560
  if (!meta) {
@@ -561,8 +563,34 @@ export async function cmdStackCreate(args, deps = {}) {
561
563
  else if (trunkFlag?.trim() && meta.trunk !== trunk) {
562
564
  throw new CommandError(`Stack trunk is ${meta.trunk}; refusing --trunk ${trunk}. Run \`mg stack reset --force\` to discard local pre-submit state.`);
563
565
  }
564
- // --trunk only sets metadata; parent defaults to current branch. Call that out
565
- // when someone passes --trunk while sitting on a different branch without --onto.
566
+ const checkedOut = currentBranchFn(cwd);
567
+ if (extend && !onto?.trim() && !checkedOut) {
568
+ throw new CommandError("Detached HEAD. Check out a branch (or pass --onto <branch>) to stack with --extend.");
569
+ }
570
+ const parentBranch = onto?.trim() || (extend && checkedOut ? checkedOut : trunk);
571
+ assertExplicitStacking({
572
+ parent: parentBranch,
573
+ trunk,
574
+ extend,
575
+ stackCommand: `\`mg stack create ${name ?? "<name>"} --onto ${parentBranch} --extend\``,
576
+ trunkCommand: `\`mg stack create ${name ?? "<name>"}\``,
577
+ });
578
+ let startRef = null;
579
+ if (parentBranch === trunk && checkedOut !== trunk) {
580
+ startRef = trunkStartRefFn(trunk, cwd);
581
+ if (!startRef)
582
+ throw new CommandError(`Trunk branch not found: ${trunk}`);
583
+ }
584
+ else if (!branchExistsFn(parentBranch, cwd)) {
585
+ throw new CommandError(`Parent branch not found: ${parentBranch}`);
586
+ }
587
+ else if (parentBranch !== checkedOut) {
588
+ startRef = parentBranch;
589
+ }
590
+ const branch = defaultLayerBranchNameFn(name);
591
+ if (branchExistsFn(branch, cwd)) {
592
+ throw new CommandError(`Branch already exists: ${branch}`);
593
+ }
566
594
  if (trunkFlag?.trim() && !onto?.trim() && parentBranch !== trunk) {
567
595
  console.error(ansi.dim(` note: --trunk ${trunk} does not change the parent; new layer parents onto \`${parentBranch}\`. ` +
568
596
  `Pass \`--onto ${trunk}\` for a fresh stack from trunk.`));
@@ -606,7 +634,10 @@ export async function cmdStackCreate(args, deps = {}) {
606
634
  throw new CommandError(err instanceof Error ? err.message : String(err));
607
635
  }
608
636
  try {
609
- createBranchFromHeadFn(branch, cwd);
637
+ if (startRef)
638
+ createBranchFromFn(branch, startRef, cwd);
639
+ else
640
+ createBranchFromHeadFn(branch, cwd);
610
641
  }
611
642
  catch (err) {
612
643
  throw new CommandError(err instanceof Error ? err.message : String(err));
@@ -637,7 +668,9 @@ export async function cmdStackCreate(args, deps = {}) {
637
668
  return;
638
669
  }
639
670
  const created = [
640
- ansi.brightGreen(` Created ${branch} (onto ${parentBranch})`),
671
+ ansi.brightGreen(parentBranch === meta.trunk
672
+ ? ` Created ${branch}: a new single PR onto ${parentBranch}${startRef ? ` (branched from ${startRef})` : ""}`
673
+ : ` Created ${branch}: a layer stacked on ${parentBranch} (--extend)`),
641
674
  ];
642
675
  if (registeredHit) {
643
676
  created.push(ansi.dim(` extending registered stack ${registeredHit.stackId}` +
@@ -847,6 +880,7 @@ export async function cmdStackSubmit(args, deps = {}) {
847
880
  return;
848
881
  }
849
882
  const lines = published.map((row) => ansi.brightGreen(` #${row.prNumber} ${row.branch} → ${row.base} (${row.created ? "opened" : "exists"}, head ${row.head.slice(0, 12)})`));
883
+ lines.push(ansi.brightGreen(stackShapeLine({ layers: layers.length, base: plans[0]?.githubBase ?? meta.trunk, extendedStack: registeredHits > 0 })));
850
884
  if (publishedStackId)
851
885
  lines.push(ansi.brightGreen(` Registered stack ${publishedStackId}`));
852
886
  lines.push(ansi.dim(" Dashboard: https://mergestorm.ai/dashboard"));
@@ -987,6 +1021,7 @@ export async function cmdStackSubmit(args, deps = {}) {
987
1021
  const verb = row.created ? "opened" : "exists";
988
1022
  summary.push(ansi.brightGreen(` #${row.prNumber} ${row.branch} → ${row.base} (${verb})`));
989
1023
  }
1024
+ summary.push(ansi.brightGreen(stackShapeLine({ layers: layers.length, base: plans[0]?.githubBase ?? meta.trunk, extendedStack: registeredHits > 0 })));
990
1025
  const adoptedTrunk = data.stack?.trunkBranch;
991
1026
  if (adoptedTrunk?.startsWith("mg-stack-")) {
992
1027
  summary.push(ansi.dim(` Bottom base is now ${adoptedTrunk} (Mergestorm-owned trunk). Leave it.`));
@@ -1003,8 +1038,8 @@ export async function cmdStackSubmit(args, deps = {}) {
1003
1038
  await present("Stack", summary);
1004
1039
  }
1005
1040
  export function parseStackPublishArgs(args) {
1006
- const usage = "usage: mergestorm stack publish [--branch <branch>] [--base <branch>] [--title <title>] [--replace-head <sha>] [--auto-land on|off] [--status] [--json]";
1007
- const out = { status: false, asJson: false };
1041
+ const usage = "usage: mergestorm stack publish [--branch <branch>] [--base <branch>] [--extend] [--title <title>] [--replace-head <sha>] [--auto-land on|off] [--status] [--json]";
1042
+ const out = { extend: false, status: false, asJson: false };
1008
1043
  const valued = {
1009
1044
  "--branch": "branch",
1010
1045
  "--base": "base",
@@ -1021,6 +1056,10 @@ export function parseStackPublishArgs(args) {
1021
1056
  out.status = true;
1022
1057
  continue;
1023
1058
  }
1059
+ if (a === "--extend") {
1060
+ out.extend = true;
1061
+ continue;
1062
+ }
1024
1063
  if (a === "--auto-land") {
1025
1064
  out.autoLand = parseOnOff(args[++i], "--auto-land", "stack publish");
1026
1065
  continue;
@@ -1066,6 +1105,7 @@ export async function cmdStackPublish(args, deps = {}) {
1066
1105
  ...(parsed.title ? { title: parsed.title } : {}),
1067
1106
  ...(typeof parsed.autoLand === "boolean" ? { autoLand: parsed.autoLand } : {}),
1068
1107
  ...(parsed.replaceHead ? { replaceHead: parsed.replaceHead } : {}),
1108
+ extend: parsed.extend,
1069
1109
  }, publishDeps);
1070
1110
  const watch = openStackWatch(result.stack.id);
1071
1111
  if (parsed.asJson) {
@@ -1076,6 +1116,7 @@ export async function cmdStackPublish(args, deps = {}) {
1076
1116
  await present("Stack", [
1077
1117
  ansi.brightGreen(` ${result.branch} ${moved} at ${result.head.sha.slice(0, 12)} (tree ${result.head.tree.slice(0, 12)})`),
1078
1118
  ansi.brightGreen(` #${result.pr.number} ${result.branch} → ${result.baseBranch} (${result.pr.created ? "opened" : "exists"}) ${result.pr.url}`),
1119
+ ansi.brightGreen(result.stacked ? ` A layer stacked on ${result.baseBranch}.` : ` A single PR onto ${result.baseBranch}.`),
1079
1120
  ansi.brightGreen(` Registered stack ${result.stack.id}`),
1080
1121
  ...stackWatchTextLines(watch).map((line) => ` ${line}`),
1081
1122
  ]);
@@ -1173,7 +1214,7 @@ export async function cmdStackWait(args, deps = {}) {
1173
1214
  throw new CommandError(err.message, REVIEW_EXIT.timeout, "review_timeout");
1174
1215
  }
1175
1216
  }
1176
- export async function cmdStackWatch(args, deps = {}) {
1217
+ async function startStackWatch(args, deps) {
1177
1218
  const usage = "usage: mergestorm stack watch <stack-id> [--until attention|landed] [--ignore <blocker-substring>] [--max <minutes>] [--head <sha>] [--bounce-id <id>] [--after-finished-at <iso>] [--json]";
1178
1219
  const fail = (detail) => new CommandError(detail ? `${usage}\n${detail}` : usage, REVIEW_EXIT.usage, "usage");
1179
1220
  const positional = [];
@@ -1246,6 +1287,30 @@ export async function cmdStackWatch(args, deps = {}) {
1246
1287
  ...(afterFinishedAt ? { afterFinishedAt } : {}),
1247
1288
  } } : {}),
1248
1289
  });
1290
+ return { stackId, result };
1291
+ }
1292
+ export async function cmdStackWatch(args, deps = {}) {
1293
+ let started;
1294
+ try {
1295
+ started = await startStackWatch(args, deps);
1296
+ }
1297
+ catch (err) {
1298
+ const valueFlags = new Set(["--until", "--ignore", "--max", "--head", "--bounce-id", "--after-finished-at"]);
1299
+ let json = false;
1300
+ for (let i = 0; i < args.length; i++) {
1301
+ if (args[i] === "--json")
1302
+ json = true;
1303
+ else if (valueFlags.has(args[i]))
1304
+ i++;
1305
+ }
1306
+ const marker = deps.writeMarker ?? (json ? console.error : console.log);
1307
+ const stackId = args.find((arg, i) => UUID_RE.test(arg.trim()) && !valueFlags.has(args[i - 1]))?.trim() ?? "none";
1308
+ marker(stackWatchFailedLine(stackId, err instanceof Error ? err.message : String(err)));
1309
+ if (json)
1310
+ console.log(JSON.stringify({ stackId, outcome: "failed" }));
1311
+ throw err;
1312
+ }
1313
+ const { stackId, result } = started;
1249
1314
  if (result.exitCode === STACK_WATCH_EXIT.landed)
1250
1315
  return;
1251
1316
  const reason = result.outcome === "timeout"
package/dist/config.d.ts CHANGED
@@ -2,6 +2,7 @@ export declare const DEFAULT_API = "https://api.mergestorm.ai";
2
2
  export type Config = {
3
3
  apiKey?: string;
4
4
  apiBase?: string;
5
+ ignoreEnv?: boolean;
5
6
  };
6
7
  export declare function mergestormHome(env?: NodeJS.ProcessEnv): string;
7
8
  export declare function configPath(): string;
@@ -9,6 +10,7 @@ export declare function loadConfig(): Promise<Config>;
9
10
  export declare function saveConfig(cfg: Config): Promise<void>;
10
11
  export declare function clearConfig(): Promise<void>;
11
12
  export declare function apiBase(cfg: Config): string;
13
+ export declare function resolveChatId(cfg: Config): string | undefined;
12
14
  export declare function resolveApiKey(cfg: Config): string | undefined;
13
15
  /** Short display form of a stored key (prefix only). */
14
16
  export declare function keyDisplay(key: string | undefined): string | null;
package/dist/config.js CHANGED
@@ -30,9 +30,19 @@ export async function clearConfig() {
30
30
  await rm(configPath(), { force: true });
31
31
  }
32
32
  export function apiBase(cfg) {
33
- return (process.env.MERGESTORM_API_URL?.trim() || cfg.apiBase || DEFAULT_API).replace(/\/$/, "");
33
+ const fromEnv = cfg.ignoreEnv ? undefined : process.env.MERGESTORM_API_URL?.trim();
34
+ return (fromEnv || cfg.apiBase || DEFAULT_API).replace(/\/$/, "");
35
+ }
36
+ const CHAT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
37
+ export function resolveChatId(cfg) {
38
+ if (cfg.ignoreEnv)
39
+ return undefined;
40
+ const chatId = process.env.MERGESTORM_CHAT_ID?.trim() ?? "";
41
+ return CHAT_ID.test(chatId) ? chatId.toLowerCase() : undefined;
34
42
  }
35
43
  export function resolveApiKey(cfg) {
44
+ if (cfg.ignoreEnv)
45
+ return cfg.apiKey;
36
46
  return process.env.MERGESTORM_API_KEY?.trim() || cfg.apiKey;
37
47
  }
38
48
  /** Short display form of a stored key (prefix only). */
package/dist/errors.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Stable machine codes for CommandError control flow.
3
3
  * UX copy in `message` may change freely; match on `code`, never on message text.
4
4
  */
5
- export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited" | "cyclone_not_installed" | "pr_merged" | "pr_closed" | "not_stacked" | "draft" | "reserved_branch" | "supabase_unconfigured" | "cyclone_not_connected" | "cyclone_permission_missing" | "busy" | "adopt_failed" | "github_unreadable" | "stack_attention" | "head_changed" | "base_changed" | "tree_mismatch" | "protected_branch" | "payload_too_large" | "unsupported_publication" | "publication_failed" | "unknown_label";
5
+ export type CommandErrorCode = "auth_invalid" | "api_timeout" | "not_a_repo" | "not_found" | "missing_api_key" | "registered_parent" | "implicit_stack" | "usage" | "review_quota" | "review_failed" | "review_timeout" | "rate_limited" | "cyclone_not_installed" | "pr_merged" | "pr_closed" | "not_stacked" | "draft" | "reserved_branch" | "supabase_unconfigured" | "cyclone_not_connected" | "cyclone_permission_missing" | "busy" | "adopt_failed" | "github_unreadable" | "stack_attention" | "head_changed" | "base_changed" | "tree_mismatch" | "protected_branch" | "payload_too_large" | "unsupported_publication" | "publication_failed" | "git_failed" | "unknown_label" | "invalid_input" | "memory_off" | "memory_full" | "memory_unavailable";
6
6
  /** Stable one-shot exit codes for machine callers of `mergestorm review`. */
7
7
  export declare const REVIEW_EXIT: {
8
8
  readonly success: 0;
@@ -15,6 +15,8 @@ export declare function discoverTrunk(cwd?: string): string;
15
15
  export declare function branchExists(name: string, cwd?: string): boolean;
16
16
  export declare function deleteBranch(name: string, cwd?: string): void;
17
17
  export declare function createBranchFromHead(name: string, cwd?: string): void;
18
+ export declare function createBranchFrom(name: string, start: string, cwd?: string): void;
19
+ export declare function trunkStartRef(trunk: string, cwd?: string): string | null;
18
20
  /** Slugify a free-form name into a safe branch segment. */
19
21
  export declare function slugifyBranchName(raw: string): string;
20
22
  export declare function defaultLayerBranchName(explicit?: string): string;
package/dist/git-stack.js CHANGED
@@ -78,6 +78,21 @@ export function deleteBranch(name, cwd = process.cwd()) {
78
78
  export function createBranchFromHead(name, cwd = process.cwd()) {
79
79
  git(["checkout", "-b", name], cwd);
80
80
  }
81
+ export function createBranchFrom(name, start, cwd = process.cwd()) {
82
+ git(["checkout", "-b", name, start], cwd);
83
+ }
84
+ export function trunkStartRef(trunk, cwd = process.cwd()) {
85
+ for (const ref of [`refs/remotes/origin/${trunk}`, `refs/heads/${trunk}`]) {
86
+ try {
87
+ git(["rev-parse", "--verify", "--quiet", ref], cwd);
88
+ return ref === `refs/heads/${trunk}` ? trunk : `origin/${trunk}`;
89
+ }
90
+ catch {
91
+ continue;
92
+ }
93
+ }
94
+ return null;
95
+ }
81
96
  /** Slugify a free-form name into a safe branch segment. */
82
97
  export function slugifyBranchName(raw) {
83
98
  const s = raw
@@ -0,0 +1,73 @@
1
+ import { type Config } from "./config.js";
2
+ export declare const MEMORY_MODES: readonly ["off", "on_request", "all"];
3
+ export declare const MEMORY_SCOPES: readonly ["user", "repo"];
4
+ export declare const MEMORY_KINDS: readonly ["rule", "style", "preference", "fact", "project"];
5
+ export declare const MEMORY_SEARCH_LIMIT_MAX = 20;
6
+ export type MemoryMode = (typeof MEMORY_MODES)[number];
7
+ export type MemoryScope = (typeof MEMORY_SCOPES)[number];
8
+ export type MemoryKind = (typeof MEMORY_KINDS)[number];
9
+ export type MemoryItem = {
10
+ id: string;
11
+ scope: MemoryScope;
12
+ repo: string | null;
13
+ kind: MemoryKind;
14
+ text: string;
15
+ source: string | null;
16
+ source_url: string | null;
17
+ created_at: string;
18
+ updated_at: string;
19
+ };
20
+ export type MemoryList = {
21
+ memory_mode: MemoryMode;
22
+ items: MemoryItem[];
23
+ };
24
+ export type MemorySearch = {
25
+ memory_mode: MemoryMode;
26
+ rules: MemoryItem[];
27
+ items: MemoryItem[];
28
+ total: number;
29
+ };
30
+ export type MemorySaved = {
31
+ id: string;
32
+ merged: boolean;
33
+ };
34
+ export type MemoryUpdated = {
35
+ id: string;
36
+ text: string;
37
+ kind?: MemoryKind;
38
+ };
39
+ export declare function memoryChatId(value?: string | null | undefined): string | null;
40
+ export declare function isMemoryMode(value: unknown): value is MemoryMode;
41
+ export declare function isMemoryKind(value: unknown): value is MemoryKind;
42
+ export declare function isMemoryScope(value: unknown): value is MemoryScope;
43
+ export declare function listMemories(filter?: {
44
+ scope?: MemoryScope;
45
+ repo?: string;
46
+ kind?: MemoryKind;
47
+ }, cfg?: Config): Promise<MemoryList>;
48
+ export declare function searchMemories(input: {
49
+ query: string;
50
+ repo?: string;
51
+ limit?: number;
52
+ chatId?: string | null;
53
+ }, cfg?: Config): Promise<MemorySearch>;
54
+ export declare function saveMemory(input: {
55
+ text: string;
56
+ kind: MemoryKind;
57
+ repo?: string | null;
58
+ chatId?: string | null;
59
+ }, cfg?: Config): Promise<MemorySaved>;
60
+ export declare function updateMemory(id: string, change: {
61
+ text: string;
62
+ kind?: MemoryKind;
63
+ }, cfg?: Config): Promise<MemoryUpdated>;
64
+ export declare function deleteMemory(id: string, cfg?: Config): Promise<{
65
+ id: string;
66
+ deleted: true;
67
+ }>;
68
+ export declare function getMemoryMode(cfg?: Config): Promise<{
69
+ memory_mode: MemoryMode;
70
+ }>;
71
+ export declare function setMemoryMode(mode: MemoryMode, cfg?: Config): Promise<{
72
+ memory_mode: MemoryMode;
73
+ }>;
@@ -0,0 +1,69 @@
1
+ import { apiFetch } from "./api.js";
2
+ import { loadConfig } from "./config.js";
3
+ import { CommandError, REVIEW_EXIT, rateLimitedMessage } from "./errors.js";
4
+ export const MEMORY_MODES = ["off", "on_request", "all"];
5
+ export const MEMORY_SCOPES = ["user", "repo"];
6
+ export const MEMORY_KINDS = ["rule", "style", "preference", "fact", "project"];
7
+ export const MEMORY_SEARCH_LIMIT_MAX = 20;
8
+ const CHAT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
+ export function memoryChatId(value = process.env.MERGESTORM_CHAT_ID) {
10
+ const trimmed = value?.trim() ?? "";
11
+ return CHAT_ID.test(trimmed) ? trimmed.toLowerCase() : null;
12
+ }
13
+ export function isMemoryMode(value) {
14
+ return typeof value === "string" && MEMORY_MODES.includes(value);
15
+ }
16
+ export function isMemoryKind(value) {
17
+ return typeof value === "string" && MEMORY_KINDS.includes(value);
18
+ }
19
+ export function isMemoryScope(value) {
20
+ return typeof value === "string" && MEMORY_SCOPES.includes(value);
21
+ }
22
+ const KNOWN_CODES = new Set(["invalid_input", "not_found", "memory_off", "memory_full", "memory_unavailable", "payload_too_large"]);
23
+ function memoryFailure(status, body, retryAfterSeconds, action) {
24
+ const row = (body && typeof body === "object" ? body : {});
25
+ if (status === 429 || row.error === "rate_limited") {
26
+ return new CommandError(rateLimitedMessage(retryAfterSeconds), REVIEW_EXIT.rate_limited, "rate_limited", retryAfterSeconds !== undefined ? { retryAfterSeconds } : undefined);
27
+ }
28
+ const message = typeof row.message === "string" && row.message ? row.message : null;
29
+ const code = typeof row.error === "string" && KNOWN_CODES.has(row.error) ? row.error : undefined;
30
+ if (status === 404 && !code)
31
+ return new CommandError("This Mergestorm server does not manage memories yet.", 1, "not_found");
32
+ return new CommandError(message ?? `${action} (HTTP ${status})`, 1, code);
33
+ }
34
+ async function call(cfg, route, init, ok, action) {
35
+ const { status, body, retryAfterSeconds } = await apiFetch(cfg ?? (await loadConfig()), route, init);
36
+ if (!ok.includes(status))
37
+ throw memoryFailure(status, body, retryAfterSeconds, action);
38
+ return body;
39
+ }
40
+ function query(params) {
41
+ const search = new URLSearchParams();
42
+ for (const [key, value] of Object.entries(params))
43
+ if (value !== undefined && value !== null && value !== "")
44
+ search.set(key, String(value));
45
+ const text = search.toString();
46
+ return text ? `?${text}` : "";
47
+ }
48
+ export function listMemories(filter = {}, cfg) {
49
+ return call(cfg, `/api/v1/memories${query(filter)}`, {}, [200], "Could not list memories");
50
+ }
51
+ export function searchMemories(input, cfg) {
52
+ return call(cfg, `/api/v1/memories/search${query({ query: input.query, repo: input.repo, limit: input.limit, chat_id: input.chatId })}`, {}, [200], "Could not search memories");
53
+ }
54
+ export function saveMemory(input, cfg) {
55
+ const json = { text: input.text, kind: input.kind, ...(input.repo ? { repo: input.repo } : {}), ...(input.chatId ? { chat_id: input.chatId } : {}) };
56
+ return call(cfg, "/api/v1/memories", { method: "POST", json }, [200, 201], "Could not save the memory");
57
+ }
58
+ export function updateMemory(id, change, cfg) {
59
+ return call(cfg, `/api/v1/memories/${encodeURIComponent(id)}`, { method: "PATCH", json: change }, [200], "Could not update the memory");
60
+ }
61
+ export function deleteMemory(id, cfg) {
62
+ return call(cfg, `/api/v1/memories/${encodeURIComponent(id)}`, { method: "DELETE" }, [200], "Could not delete the memory");
63
+ }
64
+ export function getMemoryMode(cfg) {
65
+ return call(cfg, "/api/v1/memories/mode", {}, [200], "Could not read the memory mode");
66
+ }
67
+ export function setMemoryMode(mode, cfg) {
68
+ return call(cfg, "/api/v1/memories/mode", { method: "PUT", json: { memory_mode: mode } }, [200], "Could not set the memory mode");
69
+ }
@@ -1,5 +1,6 @@
1
1
  import { getStackPublication, submitStackPublication, type StackPublication, type StackPublicationChange, type StackPublicationCommit, type StackPublicationIdentity, type StackPublicationStatus } from "./api.js";
2
2
  import { loadConfig, type Config } from "./config.js";
3
+ import { discoverTrunk } from "./git-stack.js";
3
4
  export declare const SERVER_PUBLISH_MAX_COMMITS = 20;
4
5
  export declare const SERVER_PUBLISH_MAX_CHANGES = 500;
5
6
  export declare const SERVER_PUBLISH_MAX_FILE_BYTES: number;
@@ -7,26 +8,28 @@ export declare const SERVER_PUBLISH_MAX_TOTAL_BYTES: number;
7
8
  export declare const SERVER_PUBLISH_MAX_COMMIT_BYTES: number;
8
9
  export declare const SERVER_PUBLISH_FILE_MODES: readonly ["100644", "100755", "120000"];
9
10
  export declare const SERVER_PUBLISH_HISTORY_SCAN = 500;
10
- export type GitRead = (args: string[], cwd: string) => Buffer;
11
+ export type GitRead = (args: string[], cwd: string) => Buffer | Promise<Buffer>;
12
+ export declare const READ_ONLY_GIT: Set<string>;
11
13
  export declare function readGit(args: string[], cwd: string): Buffer;
12
- export declare function readCommitHeader(read: GitRead, sha: string, cwd: string): {
14
+ export declare function readCommitHeader(read: GitRead, sha: string, cwd: string): Promise<{
13
15
  tree: string;
14
16
  parents: string[];
15
17
  author: StackPublicationIdentity;
16
18
  committer: StackPublicationIdentity;
17
19
  message: string;
18
- };
20
+ }>;
19
21
  export declare function readCommitChanges(read: GitRead, parent: string, sha: string, cwd: string, budget: {
20
22
  changes: number;
21
23
  bytes: number;
22
- }): StackPublicationChange[];
23
- export declare function readPublicationCommits(read: GitRead, from: string, to: string, cwd: string): StackPublicationCommit[];
24
+ }): Promise<StackPublicationChange[]>;
25
+ export declare function readPublicationCommits(read: GitRead, from: string, to: string, cwd: string): Promise<StackPublicationCommit[]>;
24
26
  export type ServerPublishInput = {
25
27
  cfg: Config;
26
28
  cwd: string;
27
29
  owner: string;
28
30
  repo: string;
29
31
  branch: string;
32
+ localRef?: string;
30
33
  baseBranch: string;
31
34
  localBaseRef?: string;
32
35
  remoteBaseSha?: string;
@@ -39,6 +42,7 @@ export type ServerPublishDeps = {
39
42
  read?: GitRead;
40
43
  getStackPublication?: typeof getStackPublication;
41
44
  submitStackPublication?: typeof submitStackPublication;
45
+ assertBase?: (base: string) => void;
42
46
  };
43
47
  export type ServerPublishResult = StackPublication & {
44
48
  baseBranch: string;
@@ -54,17 +58,28 @@ export type WorkingBranchInput = {
54
58
  body?: string;
55
59
  autoLand?: boolean;
56
60
  replaceHead?: string;
61
+ extend?: boolean;
57
62
  cfg?: Config;
58
63
  };
59
64
  export type WorkingBranchDeps = ServerPublishDeps & {
60
65
  publishBranchServerSide?: typeof publishBranchServerSide;
61
66
  loadConfig?: typeof loadConfig;
67
+ discoverTrunk?: typeof discoverTrunk;
62
68
  };
69
+ export declare function assertExplicitStacking(input: {
70
+ parent: string;
71
+ trunk: string;
72
+ extend: boolean;
73
+ stackCommand: string;
74
+ trunkCommand: string;
75
+ }): void;
63
76
  export declare function workingBranchTarget(input: WorkingBranchInput): {
64
77
  cwd: string;
65
78
  owner: string;
66
79
  repo: string;
67
80
  branch: string;
68
81
  };
69
- export declare function publishWorkingBranch(input: WorkingBranchInput, deps?: WorkingBranchDeps): Promise<ServerPublishResult>;
82
+ export declare function publishWorkingBranch(input: WorkingBranchInput, deps?: WorkingBranchDeps): Promise<ServerPublishResult & {
83
+ stacked: boolean;
84
+ }>;
70
85
  export declare function workingBranchPublication(input: WorkingBranchInput, deps?: Pick<WorkingBranchDeps, "getStackPublication" | "loadConfig">): Promise<StackPublicationStatus>;