@nanocollective/roster 0.1.0-alpha.53 → 0.1.0-alpha.54

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
@@ -5728,6 +5728,17 @@ async function act(req) {
5728
5728
  await run3("gh", ["issue", "reopen", n, "--repo", repo], { encoding: "utf8" });
5729
5729
  return { ok: true, action };
5730
5730
  }
5731
+ if (action === "approve") {
5732
+ const args = ["pr", "review", n, "--repo", repo, "--approve"];
5733
+ if (req.body?.trim()) args.push("--body", req.body.trim());
5734
+ try {
5735
+ await run3("gh", args, { encoding: "utf8" });
5736
+ } catch (err) {
5737
+ const line = errText(err).split("\n").map((l) => l.trim()).find((l) => l && !l.startsWith("Command failed"));
5738
+ throw new Error(`Not approved: ${line ?? "GitHub refused it without saying why."}`);
5739
+ }
5740
+ return { ok: true, action };
5741
+ }
5731
5742
  if (action === "merge") {
5732
5743
  const how = req.mergeMethod ?? await mergeMethodFor(repo);
5733
5744
  const flag = MERGE_FLAG[how];
@@ -5787,6 +5798,10 @@ function whyNotMerged(ghError, pr) {
5787
5798
  return `Not merged: checks failed (${list(failing)}).`;
5788
5799
  if (pr?.mergeStateStatus === "BLOCKED" && running.length)
5789
5800
  return `Not merged: checks are still running (${list(running)}). Merge again once they pass.`;
5801
+ if (pr?.mergeStateStatus === "BLOCKED" && pr.reviewDecision === "REVIEW_REQUIRED")
5802
+ return "Not merged: the branch rules need an approving review first. Press Approve, then Merge.";
5803
+ if (pr?.mergeStateStatus === "BLOCKED" && pr.reviewDecision === "CHANGES_REQUESTED")
5804
+ return "Not merged: a reviewer has asked for changes. Open it to see what.";
5790
5805
  if (pr?.mergeStateStatus === "BLOCKED")
5791
5806
  return "Not merged: the branch rules require something first, such as a review. Open it in GitHub to see what.";
5792
5807
  if (pr?.mergeStateStatus === "BEHIND")
@@ -5804,7 +5819,15 @@ async function prState(repo, n) {
5804
5819
  try {
5805
5820
  const { stdout } = await run3(
5806
5821
  "gh",
5807
- ["pr", "view", n, "--repo", repo, "--json", "mergeStateStatus,statusCheckRollup"],
5822
+ [
5823
+ "pr",
5824
+ "view",
5825
+ n,
5826
+ "--repo",
5827
+ repo,
5828
+ "--json",
5829
+ "mergeStateStatus,statusCheckRollup,reviewDecision"
5830
+ ],
5808
5831
  { encoding: "utf8" }
5809
5832
  );
5810
5833
  return JSON.parse(stdout);
@@ -6020,7 +6043,7 @@ var LIGHT = `number title url state createdAt updatedAt closedAt body
6020
6043
  closer: timelineItems(last:1, itemTypes:[CLOSED_EVENT]) {
6021
6044
  nodes { ... on ClosedEvent { actor { login } } }
6022
6045
  }`;
6023
- var LIGHT_PR = `${LIGHT} isDraft mergeable
6046
+ var LIGHT_PR = `${LIGHT} isDraft mergeable reviewDecision
6024
6047
  commits(last:1) { nodes { commit { statusCheckRollup { state } } } }`;
6025
6048
  var QUERY = `
6026
6049
  query($owner:String!, $name:String!) {
@@ -6053,7 +6076,7 @@ query($owner:String!, $name:String!, $number:Int!) {
6053
6076
  timelineItems(last:100, itemTypes:${ISSUE_TYPES}) { nodes { ${TIMELINE_COMMON} } }
6054
6077
  }
6055
6078
  ... on PullRequest {
6056
- number title body url state createdAt updatedAt isDraft mergeable ${ROW_EXTRAS}
6079
+ number title body url state createdAt updatedAt isDraft mergeable reviewDecision ${ROW_EXTRAS}
6057
6080
  author { login }
6058
6081
  ${REACTIONS}
6059
6082
  labels(first:12) { nodes { name } }
@@ -6079,6 +6102,7 @@ async function fetchInbox(repos) {
6079
6102
  item.draft = n.isDraft;
6080
6103
  item.checks = rollup(n.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state);
6081
6104
  item.mergeable = n.mergeable;
6105
+ if (n.reviewDecision) item.reviewDecision = n.reviewDecision;
6082
6106
  items.push(item);
6083
6107
  }
6084
6108
  const cutoff = Date.now() - CLOSED_DAYS * 864e5;
@@ -6293,6 +6317,7 @@ async function fetchThread(repo, number, kind) {
6293
6317
  item.draft = n.isDraft;
6294
6318
  item.checks = rollup(n.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state);
6295
6319
  item.mergeable = n.mergeable;
6320
+ if (n.reviewDecision) item.reviewDecision = n.reviewDecision;
6296
6321
  }
6297
6322
  return item;
6298
6323
  }
package/docs/portal.md CHANGED
@@ -96,6 +96,9 @@ Where the portal opens. An Ask box at the top, then these, in order.
96
96
  - a `review` has **Approve**.
97
97
  - a `chore` has **Done**, which closes it.
98
98
  - a pull request has **Merge**, unless its checks fail, are still running, or it conflicts.
99
+ Where the branch rules require an approving review, it also has **Approve**, and Merge waits
100
+ until you have pressed it. Approving is your review, given on purpose; Merge never gives it
101
+ for you.
99
102
 
100
103
  Answers go out as you, with the staff member's `@handle`, so they act on them straight away.
101
104
  Once you have had the last word, the card leaves Needs you for Your requests, since it is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanocollective/roster",
3
- "version": "0.1.0-alpha.53",
3
+ "version": "0.1.0-alpha.54",
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": {
@@ -357,6 +357,11 @@ jobs:
357
357
  PUBLIC_TOKEN: ${{ steps.public.outputs.token }}
358
358
  with:
359
359
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN || secrets.AGENT_TOKEN }}
360
+ # The action refuses to run for a bot unless it is named here. A peer's ask is opened
361
+ # by that peer's App, so on a peer run, and only then, the one bot that opened it is
362
+ # allowed. The caller has already checked it is an App, not this staff member's own,
363
+ # and that the issue carries a from- label. Every other run allows no bots.
364
+ allowed_bots: ${{ inputs.trigger == 'peer' && github.actor || '' }}
360
365
  # These repos are private and single-user, so the usual reason to hide a run's output does
361
366
  # not apply, and hiding it turns any failure into "is_error: true" with no message.
362
367
  # Registered secrets are still masked by the runner.
@@ -209,8 +209,31 @@ function needCard(e) {
209
209
  actions.append(button("Done", true, () => done(e, note, actions)));
210
210
  } else if (kind === "merge") {
211
211
  const blocked = item.checks === "failing" || item.mergeable === "CONFLICTING";
212
- const merge = button("Merge", true, () => merge_(e, note, actions));
213
- merge.disabled = blocked || item.checks === "pending";
212
+ const needsReview = item.reviewDecision === "REVIEW_REQUIRED";
213
+ const merge = button("Merge", !needsReview, () => merge_(e, note, actions));
214
+ merge.disabled = blocked || item.checks === "pending" || needsReview;
215
+ /* The branch rules want an approving review. Approve is the person's review, given on
216
+ purpose; Merge waits for it rather than approving on their behalf. */
217
+ if (needsReview) {
218
+ const approve = button("Approve", true, async () => {
219
+ approve.disabled = true;
220
+ note.className = "meta";
221
+ note.textContent = "Approving…";
222
+ try {
223
+ await post({ action: "approve", repo: item.repo, number: item.number });
224
+ item.reviewDecision = "APPROVED";
225
+ note.textContent = "Approved.";
226
+ approve.remove();
227
+ merge.className = "ghbtn primary";
228
+ merge.disabled = blocked || item.checks === "pending";
229
+ } catch (err) {
230
+ note.textContent = String(err.message || err);
231
+ note.className = "meta err";
232
+ approve.disabled = false;
233
+ }
234
+ });
235
+ actions.append(approve);
236
+ }
214
237
  actions.append(merge);
215
238
  }
216
239
  card.append(actions);
@@ -234,7 +257,9 @@ function checksLine(item) {
234
257
  pending: "Checks are running.",
235
258
  none: "No checks.",
236
259
  };
237
- const conflict = item.mergeable === "CONFLICTING" ? " Conflicts with its base." : "";
260
+ const conflict =
261
+ (item.mergeable === "CONFLICTING" ? " Conflicts with its base." : "") +
262
+ (item.reviewDecision === "REVIEW_REQUIRED" ? " Needs your approval to merge." : "");
238
263
  return el("p", {
239
264
  className: "homedue" + (item.checks === "failing" || conflict ? " late" : ""),
240
265
  textContent: (words[item.checks] ?? "") + conflict,
@@ -800,6 +800,23 @@ function inboxScreen(m, opts) {
800
800
  openThread({ repo: item.repo, number: item.number, kind: item.kind });
801
801
  } catch (e) { failed(e); }
802
802
  };
803
+ /* The branch rules want an approving review before a merge. Approve is your review,
804
+ given on purpose; Merge never gives it for you, so it waits until there is one. */
805
+ if (item.reviewDecision === "REVIEW_REQUIRED" && item.mergeable !== "CONFLICTING") {
806
+ merge.disabled = true;
807
+ merge.title = "Approve it first: the branch rules need an approving review";
808
+ const approve = el("button", { className: "ghbtn primary", textContent: "Approve" });
809
+ approve.onclick = async () => {
810
+ busy(true, "approving…");
811
+ try {
812
+ await post({ action: "approve", repo: item.repo, number: item.number });
813
+ item.reviewDecision = "APPROVED";
814
+ openThread({ repo: item.repo, number: item.number, kind: item.kind });
815
+ } catch (e) { failed(e); }
816
+ };
817
+ buttons.push(approve);
818
+ row.append(approve);
819
+ }
803
820
  buttons.push(merge);
804
821
  row.append(merge);
805
822
  }