mergestorm 0.3.4 → 0.3.5

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.
package/README.md CHANGED
@@ -48,14 +48,14 @@ mergestorm # uses your local build
48
48
  On a TTY, bare `mergestorm` (or `mergestorm shell`) opens a branded REPL: a **full-width** welcome panel (logo + status on the left, tips / what's new on the right) and a **full-width** bordered input box with a live `/` slash-command dropdown. Non-TTY (CI/pipes) prints usage instead — no hanging prompt.
49
49
 
50
50
  ```
51
- ╭─ mergestorm v0.3.4 ──────────────────────────────────────────────────────────╮
51
+ ╭─ mergestorm v0.3.5 ──────────────────────────────────────────────────────────╮
52
52
  │ ▟██████████▛ Tips for getting started │
53
53
  │ ▜████████▛ review review main...HEAD │
54
54
  │ ▝▜████▛▘ stack create → submit → restack → land │
55
55
  │ ▜██▛ /help list all commands │
56
56
  │ ██ │
57
- │ ▝▘ What's new in v0.3.4 │
58
- │ ● msk… · maelstrom • stack create / submit (author with mg) │
57
+ │ ▝▘ What's new in v0.3.5 │
58
+ │ ● msk… · maelstrom • stack submit fills PR body from tip commit │
59
59
  │ [████░░░░░░░░░░] 12% used │
60
60
  ╰──────────────────────────────────────────────────────────────────────────────╯
61
61
  mergestorm
@@ -105,7 +105,7 @@ mergestorm stack auto-land on|off <stack-id> Toggle auto-land when green
105
105
 
106
106
  `stack create` is local-only: it checks out a new branch from the current tip (or `--onto <branch>`), discovers trunk (`main` / `master` / `origin/HEAD`, overridable with `--trunk`), and appends `{ branch, parentBranch }` under **`.mergestorm/stack.json`**. No GitHub or API calls.
107
107
 
108
- `stack submit` walks that meta: `git push -u origin` each layer, opens a PR with `gh` (`--base` = parent or trunk; skips heads that already have an open PR), then registers the stack with `POST /api/v1/stacks/adopt` internally (Mergestorm API key). Push/PR auth is your local `git` + `gh auth` — not a Cyclone app token.
108
+ `stack submit` walks that meta: `git push -u origin` each layer, opens a PR with `gh` (`--base` = parent or trunk; skips heads that already have an open PR; body from the tip commit — Summary + Test plan, preserving Fixes/Closes/Resolves), then registers the stack with `POST /api/v1/stacks/adopt` internally (Mergestorm API key). Push/PR auth is your local `git` + `gh auth` — not a Cyclone app token.
109
109
 
110
110
  ### `login`
111
111
 
package/dist/api.d.ts CHANGED
@@ -91,6 +91,13 @@ export type StackLayerDto = {
91
91
  htmlUrl: string | null;
92
92
  ciStatus: string;
93
93
  reviewStatus: string;
94
+ checks: {
95
+ total: number;
96
+ success: number;
97
+ pending: number;
98
+ failure: number;
99
+ failingName: string | null;
100
+ } | null;
94
101
  conflictDetail: string | null;
95
102
  lastRestackedSha: string | null;
96
103
  };
@@ -109,7 +116,9 @@ export declare function adoptStack(owner: string, repo: string, prNumber: number
109
116
  /** Restack descendants (Bearer POST /api/v1/stacks/:id/restack). */
110
117
  export declare function restackStack(stackId: string, cfg?: Config): Promise<unknown>;
111
118
  /** Land the bottom open layer (Bearer POST /api/v1/stacks/:id/land-next). */
112
- export declare function landNextStack(stackId: string, cfg?: Config): Promise<unknown>;
119
+ export declare function landNextStack(stackId: string, cfg?: Config, opts?: {
120
+ prNumber?: number;
121
+ }): Promise<unknown>;
113
122
  /** Toggle auto-land when green (Bearer PATCH /api/v1/stacks/:id). */
114
123
  export declare function setStackAutoLand(stackId: string, autoLandWhenGreen: boolean, cfg?: Config): Promise<unknown>;
115
124
  /** Recently reviewed branches/threads (Bearer GET /api/v1/threads). */
package/dist/api.js CHANGED
@@ -119,8 +119,11 @@ export async function restackStack(stackId, cfg) {
119
119
  return stackMutation(stackId, "/restack", "POST", {}, "Failed to restack", cfg);
120
120
  }
121
121
  /** Land the bottom open layer (Bearer POST /api/v1/stacks/:id/land-next). */
122
- export async function landNextStack(stackId, cfg) {
123
- return stackMutation(stackId, "/land-next", "POST", {}, "Failed to land next", cfg);
122
+ export async function landNextStack(stackId, cfg, opts) {
123
+ const json = opts?.prNumber != null && Number.isInteger(opts.prNumber) && opts.prNumber > 0
124
+ ? { prNumber: opts.prNumber }
125
+ : {};
126
+ return stackMutation(stackId, "/land-next", "POST", json, "Failed to land next", cfg);
124
127
  }
125
128
  /** Toggle auto-land when green (Bearer PATCH /api/v1/stacks/:id). */
126
129
  export async function setStackAutoLand(stackId, autoLandWhenGreen, cfg) {
package/dist/cli.js CHANGED
@@ -35,7 +35,7 @@ function usage() {
35
35
  mergestorm stack list [--json] List registered stacks
36
36
  mergestorm stack adopt <owner/repo>#<pr> Import an existing open PR chain
37
37
  mergestorm stack restack <stack-id> Restack stack descendants
38
- mergestorm stack land <stack-id> Land the bottom open PR
38
+ mergestorm stack land <stack-id> Land bottom PR (or promote into unit)
39
39
  mergestorm stack auto-land on|off <stack-id> Toggle auto-land when green
40
40
 
41
41
  mg is a short alias for mergestorm (same binary).
@@ -2,7 +2,8 @@ import { adoptStack, landNextStack, listStacks, restackStack, setStackAutoLand,
2
2
  import { loadConfig } from "../config.js";
3
3
  import { CommandError } from "../errors.js";
4
4
  import { createPr, findOpenPrNumber, requireGh } from "../gh.js";
5
- import { branchExists, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, pushBranch, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
5
+ import { branchExists, createBranchFromHead, currentBranch, defaultLayerBranchName, deleteBranch, discoverTrunk, pushBranch, tipCommitMessage, tipCommitSubject, worktreeDirty, } from "../git-stack.js";
6
+ import { buildPrBodyFromCommit } from "../pr-body.js";
6
7
  import { appendLayer, loadStackMeta, saveStackMeta, } from "../stack-meta.js";
7
8
  import { ansi } from "../ui/ansi.js";
8
9
  import { parseGithubOriginRepo } from "./review.js";
@@ -13,7 +14,11 @@ const STACK_USAGE = `usage:
13
14
  mergestorm stack adopt <owner/repo>#<pr>
14
15
  mergestorm stack restack <stack-id> [--json]
15
16
  mergestorm stack land <stack-id> [--json]
16
- mergestorm stack auto-land on|off <stack-id> [--json]`;
17
+ mergestorm stack auto-land on|off <stack-id> [--json]
18
+
19
+ stack submit opens PRs with a body generated from each layer tip commit.
20
+ stack land promotes into the review unit when one exists; otherwise lands
21
+ the bottom open PR (unit-less stacks).`;
17
22
  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;
18
23
  export function requireStackId(raw, usage) {
19
24
  const id = raw?.trim() ?? "";
@@ -192,6 +197,7 @@ async function cmdStackSubmit(args) {
192
197
  let created = false;
193
198
  if (prNumber == null) {
194
199
  const title = tipCommitSubject(layer.branch, cwd);
200
+ const body = buildPrBodyFromCommit(tipCommitMessage(layer.branch, cwd));
195
201
  console.log(ansi.dim(` Opening PR ${layer.branch} → ${base} …`));
196
202
  prNumber = createPr({
197
203
  owner,
@@ -199,7 +205,7 @@ async function cmdStackSubmit(args) {
199
205
  base,
200
206
  head: layer.branch,
201
207
  title,
202
- body: "",
208
+ body,
203
209
  cwd,
204
210
  });
205
211
  created = true;
@@ -311,6 +317,7 @@ async function cmdStackLand(args) {
311
317
  const asJson = args.includes("--json");
312
318
  const id = requireStackId(args.find((a) => a !== "--json"), "usage: mergestorm stack land <stack-id>");
313
319
  const cfg = await loadConfig();
320
+ // Server routes unit stacks to promote-next (#244 / AUD-07).
314
321
  const body = await landNextStack(id, cfg);
315
322
  if (asJson) {
316
323
  console.log(JSON.stringify(body, null, 2));
@@ -318,6 +325,15 @@ async function cmdStackLand(args) {
318
325
  }
319
326
  const pr = body.mergedPrNumber;
320
327
  const branch = body.mergedBranch;
328
+ const uNumber = body.uNumber;
329
+ if (typeof uNumber === "number" && uNumber > 0) {
330
+ console.log(ansi.green(` Promoted` +
331
+ (pr != null ? ` #${pr}` : "") +
332
+ ` into U${uNumber}` +
333
+ (branch ? ` (${branch})` : "") +
334
+ ` on ${id}`));
335
+ return;
336
+ }
321
337
  console.log(ansi.green(` Landed` +
322
338
  (pr != null ? ` #${pr}` : "") +
323
339
  (branch ? ` (${branch})` : "") +
@@ -1,6 +1,10 @@
1
1
  /** Current branch name, or null when detached. */
2
2
  export declare function currentBranch(cwd?: string): string | null;
3
- /** True when the worktree has staged/unstaged/untracked changes. */
3
+ /**
4
+ * True when the worktree has staged/unstaged/untracked changes that should
5
+ * block `stack create`. Ignores `.mergestorm/stack.json` so local stack meta
6
+ * written by create does not prevent the next layer.
7
+ */
4
8
  export declare function worktreeDirty(cwd?: string): boolean;
5
9
  /**
6
10
  * Resolve trunk branch: --trunk flag, then origin/HEAD, then main/master if present.
@@ -16,3 +20,5 @@ export declare function defaultLayerBranchName(explicit?: string): string;
16
20
  export declare function pushBranch(branch: string, cwd?: string): void;
17
21
  /** First line of the tip commit on `branch` (for PR title). */
18
22
  export declare function tipCommitSubject(branch: string, cwd?: string): string;
23
+ /** Full tip commit message on `branch` (subject + body; for PR description). */
24
+ export declare function tipCommitMessage(branch: string, cwd?: string): string;
package/dist/git-stack.js CHANGED
@@ -11,10 +11,28 @@ export function currentBranch(cwd = process.cwd()) {
11
11
  return null;
12
12
  }
13
13
  }
14
- /** True when the worktree has staged/unstaged/untracked changes. */
14
+ /**
15
+ * True when the worktree has staged/unstaged/untracked changes that should
16
+ * block `stack create`. Ignores `.mergestorm/stack.json` so local stack meta
17
+ * written by create does not prevent the next layer.
18
+ */
15
19
  export function worktreeDirty(cwd = process.cwd()) {
16
20
  const out = git(["status", "--porcelain"], cwd);
17
- return out.trim().length > 0;
21
+ for (const line of out.split("\n")) {
22
+ const trimmed = line.trimEnd();
23
+ if (!trimmed)
24
+ continue;
25
+ // porcelain: XY PATH or XY ORIG -> PATH (renames only)
26
+ const status = trimmed.slice(0, 2);
27
+ const pathPart = (status.includes("R") || status.includes("C"))
28
+ ? trimmed.slice(3).split(" -> ").pop()?.trim() ?? ""
29
+ : trimmed.slice(3).trim();
30
+ if (pathPart === ".mergestorm/stack.json" || pathPart.endsWith("/.mergestorm/stack.json")) {
31
+ continue;
32
+ }
33
+ return true;
34
+ }
35
+ return false;
18
36
  }
19
37
  /**
20
38
  * Resolve trunk branch: --trunk flag, then origin/HEAD, then main/master if present.
@@ -97,3 +115,15 @@ export function tipCommitSubject(branch, cwd = process.cwd()) {
97
115
  return branch;
98
116
  }
99
117
  }
118
+ /** Full tip commit message on `branch` (subject + body; for PR description). */
119
+ export function tipCommitMessage(branch, cwd = process.cwd()) {
120
+ try {
121
+ const s = git(["log", "-1", "--format=%B", branch], cwd);
122
+ if (s.trim())
123
+ return s;
124
+ return tipCommitSubject(branch, cwd);
125
+ }
126
+ catch {
127
+ return tipCommitSubject(branch, cwd);
128
+ }
129
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Build a GitHub PR body from a tip commit message (#217).
3
+ * Pure — no git / network.
4
+ */
5
+ /**
6
+ * Turn `git log -1 --format=%B` output into a non-empty PR description.
7
+ */
8
+ export declare function buildPrBodyFromCommit(fullMessage: string): string;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Build a GitHub PR body from a tip commit message (#217).
3
+ * Pure — no git / network.
4
+ */
5
+ const TRAILER_RE = /^(fixes|closes|resolves)\s+#(\d+)\s*$/i;
6
+ /**
7
+ * Turn `git log -1 --format=%B` output into a non-empty PR description.
8
+ */
9
+ export function buildPrBodyFromCommit(fullMessage) {
10
+ const lines = fullMessage.replace(/\r\n/g, "\n").split("\n");
11
+ const subject = (lines[0] ?? "").trim() || "Update";
12
+ const trailers = [];
13
+ const bodyLines = [];
14
+ for (const raw of lines.slice(1)) {
15
+ const line = raw.trim();
16
+ if (!line)
17
+ continue;
18
+ const m = TRAILER_RE.exec(line);
19
+ if (m) {
20
+ const verb = m[1][0].toUpperCase() + m[1].slice(1).toLowerCase();
21
+ trailers.push(`${verb} #${m[2]}`);
22
+ continue;
23
+ }
24
+ bodyLines.push(line);
25
+ }
26
+ // Also pick trailers that appear as the whole subject (unusual but cheap).
27
+ const subjectTrailer = TRAILER_RE.exec(subject);
28
+ const summaryBullets = [];
29
+ if (subjectTrailer) {
30
+ const verb = subjectTrailer[1][0].toUpperCase() + subjectTrailer[1].slice(1).toLowerCase();
31
+ trailers.unshift(`${verb} #${subjectTrailer[2]}`);
32
+ summaryBullets.push(`- ${subject}`);
33
+ }
34
+ else {
35
+ summaryBullets.push(`- ${subject}`);
36
+ }
37
+ for (const para of bodyLines) {
38
+ summaryBullets.push(`- ${para}`);
39
+ }
40
+ const parts = [
41
+ "## Summary",
42
+ ...summaryBullets,
43
+ "",
44
+ "## Test plan",
45
+ "- [ ] CI green",
46
+ ];
47
+ if (trailers.length > 0) {
48
+ parts.push("", ...uniqueTrailers(trailers));
49
+ }
50
+ return parts.join("\n") + "\n";
51
+ }
52
+ function uniqueTrailers(trailers) {
53
+ const seen = new Set();
54
+ const out = [];
55
+ for (const t of trailers) {
56
+ const key = t.toLowerCase();
57
+ if (seen.has(key))
58
+ continue;
59
+ seen.add(key);
60
+ out.push(t);
61
+ }
62
+ return out;
63
+ }
package/dist/ui/banner.js CHANGED
@@ -42,6 +42,10 @@ const WHATS_NEW = {
42
42
  "`stack create` / `submit` — author stacks with `mg` (not Graphite)",
43
43
  "Adopt is import-only; happy path is create → submit → restack → land",
44
44
  ],
45
+ "0.3.5": [
46
+ "`stack submit` fills PR body from the tip commit (keeps Fixes/Closes)",
47
+ "Unit-stack `stack land` promotes (matches server gates)",
48
+ ],
45
49
  };
46
50
  function formatStatusLines(me, key, barWidth) {
47
51
  if (!key) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "MergeStorm CLI — interactive shell and one-shot review commands",
5
5
  "type": "module",
6
6
  "bin": {