@cldmv/gh-broker 0.10.0 → 0.10.1

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
@@ -19,19 +19,18 @@ It runs as a dedicated `gh-broker` system user, holds a read-scoped and a write-
19
19
 
20
20
  ## ✨ What's New
21
21
 
22
- ### Latest: v0.10.0 (November 2026)
22
+ ### Latest: v0.10.1 (November 2026)
23
23
 
24
- - **OTP gate for risky write tools.** Gated tools need a current code from an authenticator app, so the agent has to ask you. The first good code opens a five-minute window for that agent, and bad codes lock it out, then hard-lock it until `sudo gh-broker otp unlock <user>`.
25
- - **One socket per agent and the `gh-broker` command.** Each agent user gets its own socket, and `gh-broker agent add|remove|list|sync` writes its MCP config. `gh-broker otp status|list|unlock|rotate` manages the gate, and `gh-broker install` runs the installer.
26
- - **Eleven new write tools and a deny list.** These include merging, branch updates, repo create and fork, `delete_branch`, `dismiss_alert` and `minimize_comment`, with the risky ones gated by default and the Copilot coding-agent tools disabled. A built-in deny list refuses file writes, repo deletion and repo settings at config load. Breaking: the socket path, the `bin` and `npm_scaffold`'s `npm_otp`; see the upgrade notes.
27
- - [View full v0.10.0 Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.10.0.md)
24
+ - **The OTP gate says why it refused a code.** A wrong code now reads "not accepted", with the bad-code count and the lockout length, and a reused code says it's spent, so the agent can tell you what happened instead of just asking again.
25
+ - **`report_dependents` lists dev branches on large orgs.** The GraphQL listing pages at 25 repos and backs off when GitHub times out, instead of failing with a raw 502.
26
+ - [View full v0.10.1 Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.10.1.md)
28
27
 
29
28
  ### Recent Releases
30
29
 
30
+ - **v0.10.0** (November 2026) — OTP gate for risky write tools, one socket per agent with the `gh-broker` command, eleven new write tools and a built-in deny list ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.10.0.md))
31
31
  - **v0.9.0** (November 2026) — `report_alert_status`, `report_release_changes` and `report_dependents` for the release and fleet skills, `ciVerbosity` on `list_pull_requests`, and install from npm ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.9.0.md))
32
32
  - **v0.8.0** (November 2026) — `npm_scaffold` creates an npm package and its trusted publisher, `ghcr_push` pushes images without an agent-held token, and optional credentials hide their tools ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.8.0.md))
33
33
  - **v0.7.0** (October 2026) — `issue_labels` adds, removes or swaps single labels without replacing the set, and `issue_write` warns that its `labels` does ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.7.0.md))
34
- - **v0.6.1** (October 2026) — MCP SDK 1.32.0 (same-origin redirects only) and a refreshed vitest toolchain; no broker source changes ([Changelog](https://github.com/CLDMV/gh-broker/blob/master/docs/changelog/v0/v0.6.1.md))
35
34
 
36
35
  📚 For the complete version history, see the [docs/changelog/](https://github.com/CLDMV/gh-broker/tree/master/docs/changelog/) folder.
37
36
 
package/dist/index.mjs CHANGED
@@ -21468,14 +21468,17 @@ var SEARCH_PAGE_SIZE = 100;
21468
21468
  var SEARCH_MAX_PAGES = 10;
21469
21469
  var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml"];
21470
21470
  var NAME_PATTERN = /^(@[a-z0-9~-][a-z0-9._~-]*\/)?[a-z0-9~-][a-z0-9._~-]*$/;
21471
- var DEV_LIST_MAX_PAGES = 20;
21471
+ var DEV_LIST_PAGE_SIZE = 25;
21472
+ var DEV_LIST_MIN_PAGE_SIZE = 5;
21473
+ var DEV_LIST_MAX_PAGES = 80;
21474
+ var TIMEOUT_STATUSES = /* @__PURE__ */ new Set([502, 503, 504]);
21472
21475
  var DEV_BRANCH = "dev";
21473
21476
  var SEARCH_NOTE = "Candidates come from GitHub code search, which lags recent commits and only indexes default branches; each candidate was verified against its default-branch package.json. A repo that changed very recently can be missing.";
21474
21477
  var DEV_NOTE = " Every `dev` branch that differs from its default branch was also read in full, so dev-trunk repos are included; a `dev` whose root package.json version is below the default branch's is treated as abandoned and skipped (see `abandonedDev`).";
21475
21478
  var NO_DEV_NOTE = " Dev branches were not read, so a dev-trunk repo whose real manifests live on `dev` is missing.";
21476
- var DEV_BRANCHES_QUERY = `query($owner: String!, $after: String) {
21479
+ var DEV_BRANCHES_QUERY = `query($owner: String!, $after: String, $first: Int!) {
21477
21480
  repositoryOwner(login: $owner) {
21478
- repositories(first: 100, after: $after, orderBy: { field: NAME, direction: ASC }) {
21481
+ repositories(first: $first, after: $after, orderBy: { field: NAME, direction: ASC }) {
21479
21482
  pageInfo { hasNextPage endCursor }
21480
21483
  nodes {
21481
21484
  name
@@ -21620,8 +21623,17 @@ async function listDevBranches(octokit, budget, owner2) {
21620
21623
  const repos = [];
21621
21624
  const abandoned = [];
21622
21625
  let after = null;
21626
+ let first = DEV_LIST_PAGE_SIZE;
21623
21627
  for (let page = 1; page <= DEV_LIST_MAX_PAGES; page++) {
21624
- const res = await reqOne(octokit, budget, "POST /graphql", { query: DEV_BRANCHES_QUERY, variables: { owner: owner2, after } });
21628
+ let res;
21629
+ try {
21630
+ res = await reqOne(octokit, budget, "POST /graphql", { query: DEV_BRANCHES_QUERY, variables: { owner: owner2, after, first } });
21631
+ } catch (err) {
21632
+ if (!TIMEOUT_STATUSES.has(err.status)) throw err;
21633
+ if (first === DEV_LIST_MIN_PAGE_SIZE) return { repos, abandoned, complete: false, timedOut: true };
21634
+ first = Math.max(DEV_LIST_MIN_PAGE_SIZE, Math.ceil(first / 2));
21635
+ continue;
21636
+ }
21625
21637
  const conn = res.data?.repositoryOwner?.repositories;
21626
21638
  if (!conn) throw new Error(`report_dependents: listing dev branches failed: ${res.errors?.map((e) => e.message).join("; ") ?? `no owner "${owner2}"`}`);
21627
21639
  for (const r of conn.nodes) {
@@ -21725,8 +21737,9 @@ async function reportDependents(octokit, input, deps = {}) {
21725
21737
  dependents.push(entry);
21726
21738
  }
21727
21739
  if (devBranches) {
21728
- const { repos, abandoned, complete } = await listDevBranches(octokit, budget, owner2);
21740
+ const { repos, abandoned, complete, timedOut } = await listDevBranches(octokit, budget, owner2);
21729
21741
  abandonedDev.push(...abandoned);
21742
+ if (timedOut) truncatedReason ??= "github-timeout";
21730
21743
  if (!complete) truncatedReason ??= "max-repos";
21731
21744
  const room = Math.max(0, maxRepos - Math.min(repoNames.length, maxRepos));
21732
21745
  if (repos.length > room) truncatedReason ??= "max-repos";
@@ -25030,11 +25043,30 @@ var OtpGate = class {
25030
25043
  this.windows.push({ user, code, expiresAt, id });
25031
25044
  return { otp: "opened", window: id, until: new Date(expiresAt).toISOString() };
25032
25045
  }
25033
- if (step !== void 0) throw refusal();
25034
- this.recordFailure(tool, state, mine, user, now);
25035
- throw refusal();
25046
+ if (step !== void 0) {
25047
+ throw new OtpRefusal(
25048
+ `OTP code for ${tool} is spent (it, or a newer code, was already used): ask the user for the next code from their authenticator app and pass it as "${OTP_ARG}". Nothing was run.`,
25049
+ { otp: "refused" }
25050
+ );
25051
+ }
25052
+ throw this.rejection(tool, user, this.recordFailure(tool, state, mine, user, now));
25053
+ }
25054
+ /** The refusal for a wrong code, saying how close `user` now is to a lockout. */
25055
+ rejection(tool, user, after) {
25056
+ const lead = `OTP code for ${tool} was not accepted`;
25057
+ if (after.hardLock) {
25058
+ return new OtpRefusal(`${lead}, and that was the last lockout allowed: gh-broker is now locked for ${user}. Ask the user to run "${unlockCommand(user)}" on ${this.host}. Nothing was run.`, {
25059
+ otp: "refused"
25060
+ });
25061
+ }
25062
+ const lockout = windowText(this.settings.lockoutSeconds);
25063
+ if (after.failures === 0) return new OtpRefusal(`${lead}, and that was too many bad codes: OTP is locked for ${lockout}. Nothing was run.`, { otp: "refused" });
25064
+ return new OtpRefusal(
25065
+ `${lead} (${after.failures} of ${this.settings.maxFailures} bad codes before a lockout of ${lockout}): ask the user for a fresh code from their authenticator app and pass it as "${OTP_ARG}". Nothing was run.`,
25066
+ { otp: "refused" }
25067
+ );
25036
25068
  }
25037
- /** Count one bad code for `user`, starting a short lockout (and maybe the hard lock) at the limit. */
25069
+ /** Count one bad code for `user`, starting a short lockout (and maybe the hard lock) at the limit. Returns the user's new counters. */
25038
25070
  recordFailure(tool, state, mine, user, now) {
25039
25071
  const next = { ...mine, lockouts: mine.lockouts.filter((t) => Date.parse(t) > now - DAY_MS) };
25040
25072
  next.failures += 1;
@@ -25054,6 +25086,7 @@ var OtpGate = class {
25054
25086
  this.unsaved.add(user);
25055
25087
  console.error(`gh-broker: ${err.message}`);
25056
25088
  }
25089
+ return next;
25057
25090
  }
25058
25091
  /**
25059
25092
  * `state` tidied for a write: every user's lockouts pruned to the last 24 h, expired short