@nanocollective/roster 0.1.0-alpha.42 → 0.1.0-alpha.43

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/dist/cli.js CHANGED
@@ -5356,11 +5356,56 @@ async function act(req) {
5356
5356
  if (!flag) throw new Error(`unknown merge method "${req.mergeMethod}"`);
5357
5357
  const args = ["pr", "merge", n, "--repo", repo, flag];
5358
5358
  if (req.body?.trim() && how !== "rebase") args.push("--body", req.body.trim());
5359
- await run3("gh", args, { encoding: "utf8" });
5359
+ try {
5360
+ await run3("gh", args, { encoding: "utf8" });
5361
+ } catch (err) {
5362
+ throw new Error(whyNotMerged(errText(err), await prState(repo, n)));
5363
+ }
5360
5364
  return { ok: true, action, mergedBy: how };
5361
5365
  }
5362
5366
  throw new Error(`unknown action "${action}"`);
5363
5367
  }
5368
+ function whyNotMerged(ghError, pr) {
5369
+ const checks = pr?.statusCheckRollup ?? [];
5370
+ const name = (c) => c.name || c.context || "a check";
5371
+ const running = checks.filter(
5372
+ (c) => c.status && c.status !== "COMPLETED" || c.state === "PENDING" || c.state === "EXPECTED"
5373
+ );
5374
+ const failing = checks.filter(
5375
+ (c) => ["FAILURE", "TIMED_OUT", "CANCELLED", "ACTION_REQUIRED", "ERROR"].includes(
5376
+ String(c.conclusion || c.state || "")
5377
+ )
5378
+ );
5379
+ const list = (cs) => cs.map(name).join(", ");
5380
+ if (pr?.mergeStateStatus === "BLOCKED" && failing.length)
5381
+ return `Not merged: checks failed (${list(failing)}).`;
5382
+ if (pr?.mergeStateStatus === "BLOCKED" && running.length)
5383
+ return `Not merged: checks are still running (${list(running)}). Merge again once they pass.`;
5384
+ if (pr?.mergeStateStatus === "BLOCKED")
5385
+ return "Not merged: the branch rules require something first, such as a review. Open it in GitHub to see what.";
5386
+ if (pr?.mergeStateStatus === "BEHIND")
5387
+ return "Not merged: the branch is behind its base, and the branch rules require it to be up to date.";
5388
+ if (pr?.mergeStateStatus === "DIRTY") return "Not merged: the branch conflicts with its base.";
5389
+ const line = ghError.split("\n").map((l) => l.replace(/^[!X✗]\s*/, "").trim()).find((l) => l && !l.startsWith("Command failed") && !/--auto|--admin/.test(l));
5390
+ return `Not merged: ${line ?? "GitHub refused it without saying why."}`;
5391
+ }
5392
+ function errText(err) {
5393
+ const e = err;
5394
+ return `${e?.stderr ?? ""}
5395
+ ${e?.message ?? ""}`;
5396
+ }
5397
+ async function prState(repo, n) {
5398
+ try {
5399
+ const { stdout } = await run3(
5400
+ "gh",
5401
+ ["pr", "view", n, "--repo", repo, "--json", "mergeStateStatus,statusCheckRollup"],
5402
+ { encoding: "utf8" }
5403
+ );
5404
+ return JSON.parse(stdout);
5405
+ } catch {
5406
+ return null;
5407
+ }
5408
+ }
5364
5409
  async function mergeMethodFor(repo) {
5365
5410
  try {
5366
5411
  const { stdout } = await run3(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanocollective/roster",
3
- "version": "0.1.0-alpha.42",
3
+ "version": "0.1.0-alpha.43",
4
4
  "description": "An agent-run org, powered by GitHub. Scaffold AI staff members whose brain is a repo.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -627,10 +627,11 @@ function inboxScreen(m, opts) {
627
627
  status.textContent = msg ?? "";
628
628
  status.className = "meta";
629
629
  };
630
+ // busy(false) clears the status line, so it goes first: the other way round wiped the
631
+ // error as it was written, and a refused merge looked like nothing happening at all.
630
632
  const failed = (e) => {
631
- status.textContent = e.message;
632
- status.className = "meta err";
633
633
  busy(false);
634
+ status.textContent = e.message;
634
635
  status.className = "meta err";
635
636
  };
636
637