@cldmv/gh-broker 0.0.0-stage → 0.7.0

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.
Files changed (107) hide show
  1. package/.configs/fix-headers.json +3 -0
  2. package/.configs/vitest.config.mjs +44 -0
  3. package/.github/dependabot.yml +111 -0
  4. package/.github/workflows/branch-retention.yml +44 -0
  5. package/.github/workflows/bundle-size.yml +52 -0
  6. package/.github/workflows/ci.yml +389 -0
  7. package/.github/workflows/cla.yml +66 -0
  8. package/.github/workflows/codeql.yml +87 -0
  9. package/.github/workflows/dependabot-auto-merge.yml +62 -0
  10. package/.github/workflows/dependabot-recreate.yml +67 -0
  11. package/.github/workflows/dependency-review.yml +46 -0
  12. package/.github/workflows/feature-pr.yml +75 -0
  13. package/.github/workflows/hotfix-redirector.yml +67 -0
  14. package/.github/workflows/hotfixes-release.yml +70 -0
  15. package/.github/workflows/labeler.yml +48 -0
  16. package/.github/workflows/master-commit-audit.yml +56 -0
  17. package/.github/workflows/member-auto-merge.yml +75 -0
  18. package/.github/workflows/next-release.yml +76 -0
  19. package/.github/workflows/next-reset.yml +50 -0
  20. package/.github/workflows/pr-notify.yml +44 -0
  21. package/.github/workflows/pr-title-normalizer.yml +52 -0
  22. package/.github/workflows/provenance.yml +51 -0
  23. package/.github/workflows/publish.yml +146 -0
  24. package/.github/workflows/release-merge.yml +104 -0
  25. package/.github/workflows/release-notify.yml +41 -0
  26. package/.github/workflows/scorecard.yml +64 -0
  27. package/.github/workflows/stale.yml +50 -0
  28. package/.github/workflows/tag-health.yml +70 -0
  29. package/.github/workflows/update-major-version-tags.yml +94 -0
  30. package/.github/workflows/v4-bootstrap.yml +115 -0
  31. package/.github/workflows/welcome.yml +42 -0
  32. package/LICENSE +202 -0
  33. package/README.md +185 -2
  34. package/config.example.json +281 -0
  35. package/deploy/README.md +92 -0
  36. package/deploy/gh-broker.service +57 -0
  37. package/deploy/install.mjs +344 -0
  38. package/deploy/install.sh +29 -0
  39. package/deploy/lib/config-merge.mjs +56 -0
  40. package/deploy/lib/prompt.mjs +98 -0
  41. package/deploy/lib/secret-store.mjs +114 -0
  42. package/deploy/lib/smoke.mjs +137 -0
  43. package/deploy/lib/system.mjs +222 -0
  44. package/dist/index.mjs +22970 -0
  45. package/dist/index.mjs.map +7 -0
  46. package/docs/TOOLS.md +347 -0
  47. package/docs/agent-user-model.md +89 -0
  48. package/docs/changelog/v0/v0.2.0.md +26 -0
  49. package/docs/changelog/v0/v0.2.1.md +23 -0
  50. package/docs/changelog/v0/v0.2.2.md +23 -0
  51. package/docs/changelog/v0/v0.2.3.md +33 -0
  52. package/docs/changelog/v0/v0.2.4.md +25 -0
  53. package/docs/changelog/v0/v0.2.5.md +29 -0
  54. package/docs/changelog/v0/v0.3.0.md +84 -0
  55. package/docs/changelog/v0/v0.3.1.md +30 -0
  56. package/docs/changelog/v0/v0.3.2.md +24 -0
  57. package/docs/changelog/v0/v0.3.3.md +28 -0
  58. package/docs/changelog/v0/v0.4.0.md +58 -0
  59. package/docs/changelog/v0/v0.4.1.md +33 -0
  60. package/docs/changelog/v0/v0.4.2.md +45 -0
  61. package/docs/changelog/v0/v0.5.0.md +39 -0
  62. package/docs/changelog/v0/v0.5.1.md +28 -0
  63. package/docs/changelog/v0/v0.6.0.md +52 -0
  64. package/docs/changelog/v0/v0.6.1.md +48 -0
  65. package/docs/changelog/v0/v0.7.0.md +71 -0
  66. package/docs/deploy-architecture.md +82 -0
  67. package/package.json +71 -5
  68. package/src/appauth.ts +56 -0
  69. package/src/audit.ts +34 -0
  70. package/src/catalog.ts +165 -0
  71. package/src/config.ts +130 -0
  72. package/src/index.ts +107 -0
  73. package/src/paths.ts +60 -0
  74. package/src/router.ts +258 -0
  75. package/src/secrets.ts +137 -0
  76. package/src/server.ts +91 -0
  77. package/src/types.ts +104 -0
  78. package/src/upstream.ts +85 -0
  79. package/src/verbs/issue_labels.ts +329 -0
  80. package/src/verbs/link_issue.ts +84 -0
  81. package/src/verbs/release-analysis.ts +210 -0
  82. package/src/verbs/repo-tier.ts +90 -0
  83. package/src/verbs/report_ci_status.ts +358 -0
  84. package/src/verbs/report_release_prs.ts +238 -0
  85. package/src/verbs/report_security_alerts.ts +442 -0
  86. package/src/verbs/report_unlabeled.ts +194 -0
  87. package/src/verbs/rest-client.ts +181 -0
  88. package/src/verbs/review_thread.ts +138 -0
  89. package/tests/appauth.test.vitest.mts +76 -0
  90. package/tests/audit.test.vitest.mts +48 -0
  91. package/tests/catalog.test.vitest.mts +180 -0
  92. package/tests/config-merge.test.vitest.mts +58 -0
  93. package/tests/config.test.vitest.mts +314 -0
  94. package/tests/issue-labels.test.vitest.mts +347 -0
  95. package/tests/paths.test.vitest.mts +52 -0
  96. package/tests/release-prs.test.vitest.mts +237 -0
  97. package/tests/rest-client.test.vitest.mts +115 -0
  98. package/tests/router.test.vitest.mts +399 -0
  99. package/tests/run-vitest.mjs +51 -0
  100. package/tests/secret-store.test.vitest.mts +106 -0
  101. package/tests/secrets.test.vitest.mts +155 -0
  102. package/tests/security-alerts.test.vitest.mts +328 -0
  103. package/tests/server.test.vitest.mts +20 -0
  104. package/tests/upstream.test.vitest.mts +91 -0
  105. package/tests/verb-helpers.test.vitest.mts +331 -0
  106. package/tests/verbs.test.vitest.mts +624 -0
  107. package/tsconfig.json +21 -0
package/docs/TOOLS.md ADDED
@@ -0,0 +1,347 @@
1
+ # Tools reference
2
+
3
+ Every tool the broker advertises falls into one of three categories, set per entry in `config.tools` (`src/types.ts` `ToolRule`) and dispatched accordingly in `router.ts`:
4
+
5
+ | Category | Config flag | What happens | Example |
6
+ |---|---|---|---|
7
+ | **Proxied** | (neither flag) | Forwarded to the hosted GitHub MCP untouched — the advertised schema and response are exactly what the hosted MCP defines (the one addition: `issue_write`'s description carries a warning that `labels` replaces the whole set). | `list_issues`, `pull_request_read` (base behavior) |
8
+ | **Custom** | `"custom": true` | Handled entirely locally — no call to the hosted MCP at all. The broker builds its own `Octokit` from the resolved PAT and does the work itself (raw REST/GraphQL, or composing several reads). | `report_unlabeled`, `report_security_alerts`, `report_release_prs`, `report_ci_status`, `issue_labels`, `resolve_review_thread`, `reply_review_thread` |
9
+ | **Augmented** | `"augment": true` | Forwarded to the hosted MCP like a plain proxy, then the broker post-processes the result before returning it. | `pull_request_read`'s `ciVerbosity` extension |
10
+
11
+ The broker is not obligated to make a proxied tool's advertised schema or response match the hosted MCP's exactly — see the `augment` category above. It presents its own MCP contract to the agent; a tool can extend what the hosted MCP offers.
12
+
13
+ This document covers the **custom** and **augmented** tools — the ones that don't exist in the hosted MCP at all, or that gh-broker extends beyond what it offers. For everything else, see the hosted GitHub MCP's own tool documentation; gh-broker just routes the credential.
14
+
15
+ ---
16
+
17
+ ## `report_unlabeled`
18
+
19
+ Issues and/or PRs with no labels, for one repo or every non-archived repo an owner has. Read-only, `mode: "read"`.
20
+
21
+ ```json
22
+ {
23
+ "owner": "cldmv",
24
+ "repo": "gh-broker",
25
+ "target": "both",
26
+ "state": "open"
27
+ }
28
+ ```
29
+
30
+ | Param | Type | Default | Notes |
31
+ |---|---|---|---|
32
+ | `owner` | string | — (required) | Selects the read credential. |
33
+ | `repo` | string | — | Omit to scan every non-archived repo visible to this owner's read PAT (enumerated via `GET /orgs/{owner}/repos`, falling back to `GET /users/{owner}/repos` on a 404). |
34
+ | `target` | `"issues"` \| `"pulls"` \| `"both"` | `"both"` | |
35
+ | `state` | `"open"` \| `"closed"` \| `"all"` | `"open"` | |
36
+ | `includeDrafts` | boolean | `false` | A draft PR with no labels isn't triage-ready by default; include it explicitly if wanted. |
37
+ | `maxRepos` | number | 50 (hard cap 200) | Only relevant when `repo` is omitted. |
38
+ | `perRepoLimit` | number | 200 (hard cap 1000) | Items scanned per repo, per resource type. |
39
+ | `maxApiCalls` | number | 250 (hard cap 1000) | Hard ceiling on total REST calls for one invocation, regardless of the two caps above. |
40
+
41
+ Real example — `CLDMV/gh-broker` scanned for unlabeled issues/PRs (`state: "all"`):
42
+
43
+ ```json
44
+ {
45
+ "scanned": { "repos": 1, "issues": 4, "pulls": 54 },
46
+ "unlabeled": [
47
+ { "repo": "CLDMV/gh-broker", "type": "issue", "number": 9, "title": "Deploy/runtime is Linux + systemd only, despite the app being portable Node", "url": "https://github.com/CLDMV/gh-broker/issues/9", "createdAt": "2026-08-03T02:04:22Z" }
48
+ ]
49
+ }
50
+ ```
51
+
52
+ **Fan-out safety.** Hitting a rate limit or the `maxApiCalls` ceiling mid-scan returns `truncated: true` plus a `truncatedReason` (`"rate-limited"` | `"max-api-calls"` | `"max-repos"`) and whatever was already found, rather than discarding partial results. The rate-limit floor is not a hardcoded number — every response's `x-ratelimit-limit` header is read and the scan backs off at 2% of that token's real limit (floored at an absolute minimum for low-limit tokens), so it's correct regardless of which token type is configured.
53
+
54
+ ---
55
+
56
+ ## `report_security_alerts`
57
+
58
+ Every open Dependabot, code-scanning (CodeQL) and secret-scanning alert, for one repo or everything an owner has. Read-only, `mode: "read"`, `custom: true`.
59
+
60
+ ```json
61
+ {
62
+ "owner": "cldmv",
63
+ "types": ["dependabot", "codeql", "secrets"]
64
+ }
65
+ ```
66
+
67
+ | Param | Type | Default | Notes |
68
+ |---|---|---|---|
69
+ | `owner` | string | — (required) | Selects the read credential. |
70
+ | `repo` | string | — | Omit to cover every non-archived repo the owner has. When given, only the per-repo endpoints are used. |
71
+ | `types` | array of `"dependabot"` \| `"codeql"` \| `"secrets"` | all three | |
72
+ | `maxRepos` | number | 200 (hard cap 500) | Only relevant to a per-repo sweep (see below). |
73
+ | `alertLimit` | number | 500 (hard cap 5000) | Alerts fetched per type in org mode, or per repo per type in a per-repo sweep. |
74
+ | `maxApiCalls` | number | 700 (hard cap 3000) | Hard ceiling on total REST calls for one invocation. |
75
+ | `summaryOnly` | boolean | `false` | Return only the per-repo roll-up and counts. An org run is about 700 KB with the alert list and a few KB without it; `alertsOmitted` says how many alerts were left out. |
76
+ | `minSeverity` | `"critical"` \| `"high"` \| `"medium"` \| `"low"` | — | Keep only alerts at or above this severity. Secret-scanning alerts have no severity and are always kept. |
77
+ | `repos` | string[] | — | Keep only these repos (names, with or without the owner prefix, case-insensitive). |
78
+ | `classify` | boolean | `false` | Add each repo's flow tier to its roll-up row. About 3 REST calls per repo that has alerts. |
79
+
80
+ **Org-level first, per-repo as the fallback.** For each requested type the verb asks the org-level endpoint (`/orgs/{owner}/dependabot/alerts`, `/code-scanning/alerts`, `/secret-scanning/alerts`) — a handful of calls for the whole owner. If that endpoint is not available to the token (a user-owned account answers 404, a PAT without org alert access answers 403), that type alone falls back to one call per repo. `scope` in the result says which path each type took, and `disabled` counts the repos where a type is switched off or unreadable (code scanning and secret scanning are off on many repos, and that is a normal answer rather than an error).
81
+
82
+ **Per-repo roll-up.** Every result carries `byRepo`: one row per repo with at least one alert (after the filters), worst first (criticals, then highs, then total), with `total`, per-type counts (`dependabot`, `codeql`, `secrets`) and `critical` / `high` / `medium` / `low`. With `classify: true` each row also has `tier` and `tierSignals`:
83
+
84
+ | `tier` | Meaning |
85
+ |---|---|
86
+ | `v4` | `.github/workflows/next-release.yml` exists on the default branch, and `next` carries its own work (or there is no `dev` branch). |
87
+ | `dev-trunk` | A v4 repo with a `dev` branch that differs from the default branch while `next` is missing or equal to the default branch (scaffold only). |
88
+ | `non-v4` | No `next-release.yml` on the default branch — even if `next` / `hotfixes` branches exist. |
89
+
90
+ The tier is a structural signal, not a verdict. A stale `dev` branch on a repo whose `next` happens to equal `master` right after a release reads as `dev-trunk`; `tierSignals` carries the raw facts (`v4Workflow`, `defaultBranch`, `next`, `dev`, `nextEqualsDefault`, `devDiffersFromDefault`) so a caller can cross-check.
91
+
92
+ Each alert is normalized to one shape: `repo`, `type`, `number`, `severity`, `summary`, `url`, `createdAt`, plus type-specific fields — Dependabot adds `package`, `ecosystem`, `manifest`, `scope`, `ghsa`, `cve`, `vulnerableRange` and `patchedVersion` (the patched version for the range that actually matches the alert); code scanning adds `rule`, `tool` and `location`; secret scanning adds `secretType` and `validity`. Results are sorted critical-first, then by repo and number. Secret scanning returns metadata only: the secret value GitHub sends back is dropped and never leaves the broker.
93
+
94
+ ```json
95
+ {
96
+ "scanned": {},
97
+ "scope": { "dependabot": "org", "codeql": "per-repo", "secrets": "per-repo" },
98
+ "counts": { "dependabot": { "high": 1 }, "codeql": {}, "secrets": {} },
99
+ "alerts": [
100
+ { "repo": "CLDMV/gh-broker", "type": "dependabot", "number": 19, "severity": "medium", "summary": "…", "url": "https://github.com/CLDMV/gh-broker/security/dependabot/19", "createdAt": "2026-10-02T20:10:30Z", "package": "ip-address", "ecosystem": "npm", "manifest": "package-lock.json", "scope": "runtime", "ghsa": "GHSA-j6r3-76f7-8jcv", "vulnerableRange": "<= 10.7.0", "patchedVersion": "10.7.1" }
101
+ ],
102
+ "disabled": { "codeql": 140, "secrets": 140 }
103
+ }
104
+ ```
105
+
106
+ **"Open" is the default-branch view.** An alert's state tracks the default branch, so one already fixed on `next` but not yet released still reads as open until the release lands.
107
+
108
+ **Fan-out safety** works like `report_unlabeled`'s: hitting `maxRepos`, `alertLimit`, `maxApiCalls` or GitHub's rate limit returns what was found so far with `truncated: true` and a `truncatedReason` (`"max-repos"` | `"max-alerts"` | `"max-api-calls"` | `"rate-limited"`). A type that was never reached is left out of `counts` rather than reported as zero. A permission or feature-disabled `403`/`404` from an alert endpoint is never mistaken for a rate limit — only a `403` that carries a `Retry-After`, an exhausted `x-ratelimit-remaining`, or a rate-limit/abuse message is.
109
+
110
+ ---
111
+
112
+ ## `report_release_prs`
113
+
114
+ Every open release PR for an owner (or one repo), and whether each one's per-version changelog file is already on the release branch. Built for mass updates: it tells an agent which repos have a release waiting and which of those still need a changelog written. Read-only, `mode: "read"`, `custom: true`.
115
+
116
+ ```json
117
+ {
118
+ "owner": "cldmv",
119
+ "onlyMissing": true
120
+ }
121
+ ```
122
+
123
+ | Param | Type | Default | Notes |
124
+ |---|---|---|---|
125
+ | `owner` | string | — (required) | Selects the read credential. |
126
+ | `repo` | string | — | Omit to search every repo the owner has. |
127
+ | `onlyMissing` | boolean | `false` | List only PRs that are not `ready`. `summary` still counts all of them. |
128
+ | `analyze` | boolean | `false` | Add an `analysis` block per release PR and a `verdicts` tally. About 5 more REST calls per release PR, so raise `maxApiCalls` for a whole org. With `onlyMissing`, "missing" then means "verdict other than `done`". |
129
+ | `maxApiCalls` | number | 300 (hard cap 1000) | Hard ceiling on total REST calls for one invocation. |
130
+
131
+ A release PR is an open PR carrying the `release` label, which the v4 release automation applies to both the `next → master` and the `hotfixes → master` PR. One search call covers the whole owner; each PR found then costs one call for its head/base and up to two to look for the changelog.
132
+
133
+ The version is read from the `release: vX.Y.Z - …` title. The changelog is looked up **at the PR's head commit** (not the default branch), because the file lands on `next` before the release PR merges. Both `docs/changelog/v<major>/vX.Y.Z.md` and `docs/changelogs/v<major>/vX.Y.Z.md` are checked. Each PR gets one `status`:
134
+
135
+ | `status` | Meaning |
136
+ |---|---|
137
+ | `ready` | The changelog file exists (`changelogPath` says where). |
138
+ | `needs-changelog` | The version is known but no changelog file exists (`checkedPaths` lists what was looked up). |
139
+ | `unknown-version` | The title has no `release: vX.Y.Z`, so there is nothing to look up. |
140
+
141
+ ```json
142
+ {
143
+ "scanned": { "releasePrs": 2 },
144
+ "summary": { "ready": 1, "needsChangelog": 1, "unknownVersion": 0 },
145
+ "releasePrs": [
146
+ { "repo": "CLDMV/gh-broker", "pull": 91, "url": "https://github.com/CLDMV/gh-broker/pull/91", "title": "release: v0.4.3 - …", "version": "0.4.3", "head": "next", "base": "master", "draft": false, "status": "needs-changelog", "checkedPaths": ["docs/changelog/v0/v0.4.3.md", "docs/changelogs/v0/v0.4.3.md"] }
147
+ ]
148
+ }
149
+ ```
150
+
151
+ **`analyze: true`.** Each release PR gains `analysis`: `openPrs` (everything open against its release branch — `next`, or `hotfixes` — as `doc`, `dependency`, `feature` or `draft`; a doc PR's title version is read), `freshness` when a changelog exists, a `verdict`, and a one-line `reason`.
152
+
153
+ `freshness.status` compares the changelog with the PRs that actually shipped in the release (read from the release PR's commits, first line of each; docs PRs and the version bump are ignored):
154
+
155
+ | `freshness.status` | Meaning |
156
+ |---|---|
157
+ | `current` | The heading carries the release's version and every shipped PR is mentioned. |
158
+ | `minor-gap` | Every user-facing PR is mentioned; only `ci` / `chore` / `style` / `test` PRs are not (`missingInternal`). |
159
+ | `stale` | At least one user-facing PR is not mentioned (`missingUserFacing`). |
160
+ | `version-drift` | The changelog's heading names a different version than the release PR's current title. |
161
+ | `thin` | Looks like a stub (very short, or no Overview section). |
162
+
163
+ `readmeMentionsVersion` reports whether the README's What's New block names the version; it does not change the status.
164
+
165
+ The `verdict` is the release-docs decision table:
166
+
167
+ | `verdict` | When |
168
+ |---|---|
169
+ | `done` | A changelog exists and is `current` or `minor-gap`. |
170
+ | `in-flight` | It needs work and a doc PR is already open against the release branch (`docVersionMismatch` is set if that PR names a different version). |
171
+ | `wait` | It needs work and feature/fix PRs are still open against the release branch. |
172
+ | `gray` | It needs work and only dependency PRs or drafts are open. |
173
+ | `write` | No changelog exists and nothing is open against the release branch. |
174
+ | `update` | A changelog exists but is stale, drifted or thin, and nothing is open against the release branch. |
175
+ | `inspect` | The release PR title has no readable version. |
176
+
177
+ The release PR's version can change while it is open (more PRs landing on `next` can raise the bump), so a `ready` changelog is only as current as the title it was matched against — re-run the report after `next` changes. Truncation follows the same pattern as the other report verbs; the extra reasons are `"max-results"` (more than one page of release PRs matched — GitHub's search returns 100 per page) and `"incomplete-search"` (GitHub reported the search timed out).
178
+
179
+ ---
180
+
181
+ ## `report_ci_status`
182
+
183
+ CI health for a PR, or every open PR in a repo, at a verbosity the caller chooses. Read-only, `mode: "read"`, `custom: true`.
184
+
185
+ ```json
186
+ {
187
+ "owner": "cldmv",
188
+ "repo": "droidsock",
189
+ "pull": 35,
190
+ "verbosity": 3
191
+ }
192
+ ```
193
+
194
+ | Param | Type | Default | Notes |
195
+ |---|---|---|---|
196
+ | `owner`, `repo` | string | — (required) | |
197
+ | `pull` | number | — | Omit to scan every open PR in the repo — only PRs with a failure are included in the result. |
198
+ | `verbosity` | `0`-`3` | `0` | See tiers below. |
199
+ | `logLines` | number | 200 (hard cap 2000) | Tier-3 only: tail lines per failing job's log. |
200
+ | `maxPulls` | number | 50 (hard cap 200) | Only relevant when `pull` is omitted. |
201
+ | `maxApiCalls` | number | 250 (hard cap 1000) | |
202
+
203
+ ### Verbosity tiers
204
+
205
+ | Tier | Adds |
206
+ |---|---|
207
+ | 0 (default) | `summary: { passed, failed, pending }` only |
208
+ | 1 | Which run/job failed: `workflow`, `job`, `runId`, `runUrl`, `conclusion` |
209
+ | 2 | + which step in that job failed, with `durationMs` |
210
+ | 3 | + a tail of the failing job's log (`logTail`) |
211
+
212
+ Detail beyond tier 0 is fetched **only for runs that are actually failing** — a healthy PR costs exactly one API call regardless of the requested verbosity.
213
+
214
+ **Classification.** A run, job, or step with `status !== "completed"` is `"pending"`. A completed one is `"failed"` unless its `conclusion` is `success`, `neutral`, or `skipped` — those three are the only non-blocking outcomes. Notably, a `cancelled` job (e.g. a matrix sibling killed after another leg failed) counts as **failed**, not skipped, matching how GitHub's own UI treats it as needing attention.
215
+
216
+ ### Real example (tier 3, `CLDMV/droidsock#35`)
217
+
218
+ A dependency-bump PR with a genuine `npm ERESOLVE` peer-dependency conflict:
219
+
220
+ ```json
221
+ {
222
+ "pull": 35,
223
+ "summary": { "passed": 6, "failed": 1, "pending": 0 },
224
+ "failures": [
225
+ { "workflow": "🧪 CI Tests & Build", "job": "Build & Test (24)", "runId": 34787050719, "conclusion": "cancelled", "steps": [{ "name": "Build and Test", "number": 2, "conclusion": "cancelled", "durationMs": 16000 }] },
226
+ { "workflow": "🧪 CI Tests & Build", "job": "Build & Test (22.12)", "runId": 34787050719, "conclusion": "failure", "steps": [{ "name": "Build and Test", "number": 2, "conclusion": "failure", "durationMs": 8000 }], "logTail": "npm error ERESOLVE could not resolve\nnpm error While resolving: @cldmv/slothlet@3.15.1\nnpm error Found: typescript@7.0.2\n…\nnpm error peerOptional typescript@\"^6.0.3\" from @cldmv/slothlet@3.15.1\n…" }
227
+ ]
228
+ }
229
+ ```
230
+
231
+ Note the first entry (a `cancelled` matrix leg) has no `logTail` even at tier 3 — the hosted MCP's `get_job_logs` (see below) only has log content for jobs whose conclusion is exactly `"failure"`, not `"cancelled"`. A lookup miss like this yields no `logTail` on that entry, never an error for the whole report.
232
+
233
+ ### Tier 3's log fetch: `get_job_logs`, not a hand-rolled REST call
234
+
235
+ Tier 3 does not call GitHub's job-logs REST endpoint directly. It calls the hosted MCP's own `get_job_logs` tool (already allowlisted, plain proxied) with `failed_only: true` and the failing run's `run_id` — which returns every failing job's log **in that run** in a single call, already tailed to `logLines`. One call per failing **run**, not per failing job. This is dispatched from `router.ts`'s `makeJobLogsFetcher`, which both `report_ci_status` and the `pull_request_read` augmentation (below) share.
236
+
237
+ Because this goes through the hosted MCP rather than a direct REST call, it is **not** counted by `report_ci_status`'s own `CallBudget`/rate-limit-floor (that machinery is REST-response-header-based, and an upstream-forwarded MCP call exposes no such headers). This is an accepted gap, not an oversight — the fan-out is already bounded by "one call per actually-failing run," so it can't grow unboundedly regardless.
238
+
239
+ ---
240
+
241
+ ## `pull_request_read` + `ciVerbosity` (augmented)
242
+
243
+ A normal `pull_request_read` call (`method: "get"`) can carry CI status inline by adding `ciVerbosity`:
244
+
245
+ ```json
246
+ {
247
+ "owner": "cldmv",
248
+ "repo": "droidsock",
249
+ "pullNumber": 35,
250
+ "method": "get",
251
+ "ciVerbosity": 1
252
+ }
253
+ ```
254
+
255
+ `ciVerbosity` (0-3, same tiers as `report_ci_status`) and `logLines` are gh-broker's own extension on top of the hosted MCP's real `pull_request_read` schema — they're stripped before the call is forwarded, since the upstream schema doesn't know them. The response comes back with a `ci` field appended, computed from the PR's head sha already present in the forwarded response (no redundant PR lookup):
256
+
257
+ ```json
258
+ {
259
+ "number": 35,
260
+ "title": "deps: bump typescript from 6.0.3 to 7.0.2",
261
+ "...": "…every other real pull_request_read field, untouched…",
262
+ "ci": { "pull": 35, "summary": { "passed": 6, "failed": 1, "pending": 0 }, "failures": [ "…same shape as report_ci_status…" ] }
263
+ }
264
+ ```
265
+
266
+ **Opt-in only.** Omitting `ciVerbosity` makes this call byte-identical to a plain forward — augmenting every `pull_request_read` by default would add an unrequested API call (and cost) to every ordinary PR read. Only `method: "get"` is augmentable; any other method (`get_diff`, `get_files`, …) forwards untouched even if `ciVerbosity` is present.
267
+
268
+ **Best-effort.** If the upstream response isn't the JSON-text shape expected — a format gh-broker doesn't control — the original result is returned unmodified rather than thrown. A shape mismatch degrades to "no CI data added," never a broken PR read.
269
+
270
+ ---
271
+
272
+ ## `issue_labels`
273
+
274
+ Edit the labels on one issue or PR, touching only the labels named. Write, `mode: "write"`, `custom: true`, runs on the write PAT (Issues: write).
275
+
276
+ The hosted MCP's `issue_write` takes `labels` as a **replacement**: every label not in the list is removed. Changing one status label that way silently drops the priority, area and type labels next to it. Reading the set and writing the merged set back has a race, since a label someone else adds between the two calls is lost. `issue_labels` uses GitHub's per-label endpoints instead, so nothing outside the call is touched. The broker also appends a warning to the advertised `issue_write` description pointing here.
277
+
278
+ ```json
279
+ {
280
+ "owner": "cldmv",
281
+ "repo": "gh-broker",
282
+ "issue_number": 117,
283
+ "swap": { "prefix": "status: ", "to": "status: implemented" }
284
+ }
285
+ ```
286
+
287
+ | Param | Type | Default | Notes |
288
+ |---|---|---|---|
289
+ | `owner` | string | — (required) | Selects the write credential. |
290
+ | `repo` | string | — (required) | |
291
+ | `issue_number` | integer | — (required) | An issue or a pull-request number. PRs use the same endpoints. |
292
+ | `add` | string \| string[] | — | Append one or more labels (`POST …/issues/{n}/labels`, server-side append). Labels already on the issue are a no-op. |
293
+ | `remove` | string \| string[] | — | Remove one or more labels (`DELETE …/issues/{n}/labels/{name}` each, using the spelling already on the issue). A label the issue doesn't have is skipped: no request, not an error. |
294
+ | `swap` | `{ prefix, to }` | — | Remove every current label whose name starts with `prefix`, then add `to` (one label or several). A `to` label already on the issue is left in place rather than removed and re-added. |
295
+ | `set` | string \| string[] | — | Replace the whole set (`PUT …/issues/{n}/labels`). Only as an explicit choice; cannot be combined with `add`, `remove` or `swap`. An empty `set` is refused, so it can never clear every label. |
296
+ | `create_missing` | boolean | `false` | Create add-side labels that don't exist in the repo instead of refusing the call. |
297
+ | `color` | string | `ededed` | 6-digit hex color (with or without `#`) for labels created by `create_missing`. Anything else is refused. |
298
+
299
+ `add`, `remove` and `swap` combine freely in one call. Naming the same label on both sides is refused, and a label on the add side is never removed by a `swap` prefix match (it stays put rather than being deleted and re-added).
300
+
301
+ **Strict arguments.** The handler enforces the schema itself: an unknown key (a typo such as `remvoe`), a non-integer `issue_number`, a non-string `owner`/`repo`, a non-boolean `create_missing`, an empty list or a bad `color` refuses the call before any request. Each list holds at most 100 labels (GitHub's per-issue limit), and so does the add side as a whole.
302
+
303
+ **Write order and partial failure.** Writes run in this order: create missing labels, one POST for the whole add side, then the DELETEs. Adding before removing means a failure part-way never leaves the issue with fewer labels than it should have; a `swap` whose POST is rate-limited keeps its old status label. If any write fails, the error names what was applied and what was not, and keeps the HTTP status:
304
+
305
+ ```
306
+ issue_labels: failed part-way on cldmv/gh-broker#117: API rate limit exceeded. Applied: nothing. Not applied: added [status: verified]; removed [status: in progress].
307
+ ```
308
+
309
+ A failure before the first write (the read of the issue's labels, or a label lookup) is passed through unchanged; nothing was written.
310
+
311
+ **Unknown labels are refused.** Every name the call would add (`add`, `swap.to`, `set`) is looked up in the repo first. If any is missing and `create_missing` is not set, the call fails with the missing names and **nothing is written**. Names to remove are not checked, since removing an absent label is already a no-op. Matching is case-insensitive, as GitHub's is, and the repo's own spelling is what gets applied.
312
+
313
+ **Result.** Always the issue's resulting label set, plus what this call changed:
314
+
315
+ ```json
316
+ {
317
+ "issue": "cldmv/gh-broker#117",
318
+ "labels": ["type: enhancement", "priority: medium", "status: implemented"],
319
+ "added": ["status: implemented"],
320
+ "removed": ["status: not started"],
321
+ "created": []
322
+ }
323
+ ```
324
+
325
+ `added` lists only labels this call asked for that were not already there; a label someone else added meanwhile shows in `labels` but not in `added`.
326
+
327
+ **Calls.** One read of the issue's current labels (which also fails a wrong issue number before anything is written), one lookup per add-side label, one create per missing label with `create_missing`, at most one POST, and one DELETE per label actually on the issue. With the 100-label caps a call makes at most a few hundred requests. `swap` and `remove` work from that first read, so a matching label added by someone else in the moments between that read and the DELETEs is not removed; nothing outside the call is ever affected.
328
+
329
+ ---
330
+
331
+ ## `resolve_review_thread` / `reply_review_thread`
332
+
333
+ Resolve/unresolve, or reply inside, a pull-request review thread (e.g. addressing a Copilot review comment) — gaps the hosted MCP has no tool for. Both run on the **write** PAT (`Pull requests: write`), via the `resolveReviewThread`/`unresolveReviewThread`/`addPullRequestReviewThreadReply` GraphQL mutations.
334
+
335
+ ```json
336
+ { "owner": "cldmv", "threadId": "PRRT_kwDO…", "resolve": true }
337
+ ```
338
+
339
+ `threadId` is the review-thread **node id**, taken verbatim from `pull_request_read`'s `get_review_comments` method (each returned thread carries it as `id`). `resolve_review_thread` defaults to resolving; pass `resolve: false` to unresolve. `reply_review_thread` takes the same `threadId` plus a `body` (Markdown).
340
+
341
+ ---
342
+
343
+ ## Extending this: adding a new custom verb or augmented tool
344
+
345
+ - **Custom verb**: implement in `src/verbs/<name>.ts` (export the function + a `*_SCHEMA` JSON Schema constant), add a `case` in `router.ts`'s `dispatchCustom`, advertise it in `catalog.ts`'s `if (rule.custom)` branch, and add a `{ "name": ..., "mode": ..., "custom": true }` entry to `config.example.json`.
346
+ - **Augmented tool**: add a `case` in `router.ts`'s `dispatchAugmented` that forwards via `this.upstream.forward(...)` and post-processes the result, layer the extra schema properties onto the real fetched schema in `catalog.ts` (see the `pull_request_read` branch for the pattern), and mark the allowlist entry `"augment": true` instead of `"custom": true` — the two are mutually exclusive.
347
+ - If the new verb needs data another proxied tool already provides (like `report_ci_status`'s tier 3 needing `get_job_logs`), give `dispatchCustom`/`dispatchAugmented` access to `this.upstream` and inject a fetcher function into the verb rather than reimplementing the underlying REST call — see `router.ts`'s `makeJobLogsFetcher` for the pattern.
@@ -0,0 +1,89 @@
1
+ # The agent-as-separate-user model
2
+
3
+ gh-broker's security rests on one structural decision: **the agent runs as a different, unprivileged OS user than the broker that holds the credentials.** The agent gets to *act* on GitHub through the broker; it never gets to *hold* a token. This document is the worked example of that layout — the users, the group, the permissions that make the boundary real, and how to reproduce it. It is a reference setup, not a one-off.
4
+
5
+ ## Why a separate user at all
6
+
7
+ The credentials (per-org fine-grained PATs, and optionally a GitHub App key) live in a secret store on a root-owned, agent-unreadable path, read into the broker process's memory at start. The only thing that ever crosses to the agent is MCP frames over a Unix socket — bytes in, bytes out, no token.
8
+
9
+ That isolation is the entire security property, not a garnish on top of one. If the agent and the broker were the same user, that user could read the secret store, connect to the socket, *and* read the broker process's environment and memory — the design would be theater. Running them as two users, with the OS enforcing the split, is what turns "the agent can't exfiltrate the secret" from a hope into a mechanism.
10
+
11
+ ## The three roles
12
+
13
+ - **Agent user** — the unprivileged account the agent (Claude, or any autonomous client) runs as. It is a member of the *bridge group* so it can open the socket, and nothing more. It is **not** in the service group and has no path to the credentials.
14
+ - **Service user** — the dedicated account the broker daemon runs as. It owns the socket and runtime directory and is the only identity that can read and hold the PATs. It runs nothing else.
15
+ - **Bridge group** — a group whose *sole* purpose is socket access. Both users belong to it; the socket is `group=<bridge>, mode 0660`. Membership grants exactly one capability — connect to the socket — and grants nothing over the credentials.
16
+
17
+ ## Example layout (this deployment)
18
+
19
+ | Role | User | uid / gid | Relevant group memberships | Runs as agent? |
20
+ |---|---|---|---|---|
21
+ | Agent | `repo` | 1003 / 1003 | `repo`, `users`, `docker` (986, **rootless**), `repos` (1004), `gh-broker-clients` (984) | yes |
22
+ | Service | `gh-broker` | 995 / 985 | `gh-broker` (985), `gh-broker-clients` (984) | no — daemon only |
23
+
24
+ Bridge group: `gh-broker-clients` (gid 984) → members `gh-broker, repo`.
25
+
26
+ Filesystem and socket:
27
+
28
+ | Path | Owner:Group | Mode | Effect |
29
+ |---|---|---|---|
30
+ | `/etc/gh-broker` | `root:gh-broker` | `0750` | Agent (not in `gh-broker`) can't even traverse into it. |
31
+ | the token store — `creds/*.cred` (systemd-creds) or `secrets.json` (file) | `root:root` / `root:gh-broker` | `0400` / `0640` | The tokens, in whichever backend was chosen at install. Readable only by root / the `gh-broker` group; the agent (not in it) can't. Referenced by `cred:` names in the config. |
32
+ | `/run/gh-broker` | `gh-broker:gh-broker` | `0755` | Traversable so the socket can be reached. |
33
+ | `/run/gh-broker/gh-broker.sock` | `gh-broker:gh-broker-clients` | `0660` | Agent connects (it's in the bridge group); non-members can't. |
34
+
35
+ ## What makes the boundary real
36
+
37
+ Read these as the capability matrix — what each user actually can and cannot do:
38
+
39
+ | | Read the token store | Read broker env / memory | Open the socket | Ever hold a PAT |
40
+ |---|---|---|---|---|
41
+ | **Agent** (`repo`) | no — not in `gh-broker`; dir is `0750 root:gh-broker`, tokens root-owned | no — separate user, plus the unit's `ProtectHome`/`NoNewPrivileges`/etc. | **yes** — member of `gh-broker-clients` | **never** |
42
+ | **Service** (`gh-broker`) | yes — via systemd-creds (memory-only `$CREDENTIALS_DIRECTORY`) or the file, into memory only | yes (it is the process) | yes — it owns it | yes — in memory only, never on an agent-readable path |
43
+
44
+ Three independent gates, each verifiable:
45
+
46
+ 1. **Credentials** — `/etc/gh-broker` is `0750 root:gh-broker`. The agent user is deliberately *not* in the `gh-broker` group, so it cannot list or traverse the directory, let alone read the token store. Verify: as the agent user, `cat /etc/gh-broker/secrets.json` (or a `creds/*.cred`) → *Permission denied*.
47
+ 2. **Socket** — `0660 gh-broker:gh-broker-clients`. The agent is in `gh-broker-clients`, so it can `connect()`; a user in neither group cannot. The socket speaks MCP frames only — no credential material is ever written to it.
48
+ 3. **Process** — the broker runs under the hardened unit (`ProtectHome`, `ProtectSystem=strict`, `PrivateTmp`, `NoNewPrivileges`, restricted syscalls). A separate user can't `ptrace` it or read `/proc/<pid>/environ`, so the in-memory token isn't reachable that way either.
49
+
50
+ ## Host hardening that backs the split
51
+
52
+ The two-user split only holds if the agent user has no *other* route to root or to the service user's files:
53
+
54
+ - **No passwordless sudo for the agent.** If the agent could `sudo`, every gate above is moot. Verify: as the agent user, `sudo -n true` → fails.
55
+ - **Rootless docker (mandatory if the agent has docker access).** The agent here is in the `docker` group — which on a **rootful** daemon is root-equivalent (a container can bind-mount host `/` and read any file, including the creds). It is safe *only because docker runs rootless*: the group grants access to the agent's own userns-mapped daemon, not to a host-root daemon. If you put the agent user in a rootful `docker` group, you have handed it the credentials. Keep docker rootless, or keep the agent out of the docker group.
56
+ - **The agent never has the credentials on any readable path** — not in `~/.claude.json`, not in an env var, not in a file it owns. The socket bridge (`socat STDIO UNIX-CONNECT:…`) is the whole of its access.
57
+
58
+ ## Setting up the agent user
59
+
60
+ The agent account is a prerequisite you provide; `deploy/provision.sh` only wires it into the bridge group. Set it up as a plain unprivileged user:
61
+
62
+ ```sh
63
+ # An ordinary login user with NO sudo. (Add to `docker` only if docker is rootless.)
64
+ sudo useradd --create-home --shell /bin/bash agent # or reuse an existing unprivileged user
65
+ # Make sure it has no sudo rights:
66
+ sudo -l -U agent # should report it may run nothing, or fail
67
+ ```
68
+
69
+ Then provisioning adds it to the bridge group and the socket becomes reachable after a re-login:
70
+
71
+ ```sh
72
+ sudo ./deploy/provision.sh agent # arg = the agent user; adds it to gh-broker-clients
73
+ # re-login (or `newgrp gh-broker-clients`) so the membership is active
74
+ ```
75
+
76
+ Point the agent's MCP config at the socket via the stdio bridge (see `deploy/README.md` §4). Nothing else about the agent needs the broker's internals.
77
+
78
+ ## Anti-patterns
79
+
80
+ - **Running the agent as root** (or with sudo) — collapses every gate; root reads the creds directly.
81
+ - **Running the broker as the agent user** — the agent then owns the secret store and the process; there is no boundary left.
82
+ - **Adding the agent to the *service* group (`gh-broker`)** — it only needs the *bridge* group (`gh-broker-clients`). Membership in `gh-broker` grants traversal into `/etc/gh-broker`. Keep these two groups distinct and put the agent in the bridge group only.
83
+ - **Granting the agent read on `/etc/gh-broker/creds`** "for convenience" — the whole point is that it can't.
84
+ - **A rootful `docker` group for the agent** — see above; it's a silent root-equivalent escape around the socket.
85
+
86
+ ## See also
87
+
88
+ - `deploy/README.md` — prerequisites, provisioning, the systemd unit, and agent wiring.
89
+ - The full design + threat model lives outside this repo (the maintainer's `designs/github-mcp-broker.md`).
@@ -0,0 +1,26 @@
1
+ # gh-broker v0.2.0 Changelog
2
+
3
+ **Release Date**: August 2026
4
+ **Release Type**: Minor
5
+ **Branch**: `release/0.2.0`
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ The first substantive release of the credential-routing MCP broker. GitHub reads — and the first writes — are routed through the daemon so the agent acts without ever holding a token, with per-owner read/write PATs resolved by repo owner and mode. Shipped in [#5](https://github.com/CLDMV/gh-broker/pull/5).
12
+
13
+ ---
14
+
15
+ ## ✨ Features
16
+
17
+ - **Custom review-thread verbs** — `resolve_review_thread` / `reply_review_thread`, so a PR review thread (e.g. Copilot's) can be replied to or resolved via the GraphQL mutations on the write PAT, filling a gap the hosted MCP has no tool for (`7a47c21`).
18
+ - **`create_pull_request` allowlisted; `deploy-app.sh` syncs `config.json`** (`8ce78f7`).
19
+
20
+ ## 🐛 Bug Fixes
21
+
22
+ - **All-toolsets upstream** — route to the all-toolsets hosted MCP and skip allowlisted tools the upstream doesn't expose, so the advertised toolset matches what actually resolves (`8f4302d`).
23
+
24
+ ## 🔧 Other
25
+
26
+ - `link_issue` disabled in the allowlist as dead (`92b3840`); the `link_issue` path refactored to the Octokit SDK + `@octokit/auth-app` App token instead of hand-rolled fetch (`4c1e88f`).
@@ -0,0 +1,23 @@
1
+ # gh-broker v0.2.1 Changelog
2
+
3
+ **Release Date**: August 2026
4
+ **Release Type**: Patch
5
+ **Branch**: `release/0.2.1`
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ A dependency-maintenance release — no runtime behavior changed. Shipped in [#8](https://github.com/CLDMV/gh-broker/pull/8).
12
+
13
+ ---
14
+
15
+ ## 🔧 Dependencies
16
+
17
+ - Bump `@modelcontextprotocol/sdk` to `1.30.0` (and `fast-uri` to `3.1.5`) ([#7](https://github.com/CLDMV/gh-broker/pull/7), `8fd7e21`).
18
+
19
+ ---
20
+
21
+ ## Upgrade notes
22
+
23
+ - No breaking changes — drop-in for v0.2.0.
@@ -0,0 +1,23 @@
1
+ # gh-broker v0.2.2 Changelog
2
+
3
+ **Release Date**: August 2026
4
+ **Release Type**: Patch
5
+ **Branch**: `release/0.2.2`
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ A dependency-maintenance release — no runtime behavior changed. Shipped in [#11](https://github.com/CLDMV/gh-broker/pull/11).
12
+
13
+ ---
14
+
15
+ ## 🔧 Dependencies
16
+
17
+ - Bump the `@hono/node-server` transitive dependency to `2.0.12` ([#11](https://github.com/CLDMV/gh-broker/pull/11)).
18
+
19
+ ---
20
+
21
+ ## Upgrade notes
22
+
23
+ - No breaking changes — drop-in for v0.2.1.
@@ -0,0 +1,33 @@
1
+ # gh-broker v0.2.3 Changelog
2
+
3
+ **Release Date**: September 2026
4
+ **Release Type**: Patch
5
+ **Branch**: `release/0.2.3`
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ A CI-routing fix plus routine dependency and workflow maintenance — no runtime behavior changed. Shipped in [#35](https://github.com/CLDMV/gh-broker/pull/35).
12
+
13
+ ---
14
+
15
+ ## 🐛 Bug Fixes
16
+
17
+ ### Route the Required PR Check mirror to `cldmv-runners` on private repos ([#22](https://github.com/CLDMV/gh-broker/pull/22))
18
+
19
+ The local `✅ Required PR Check` mirror job is routed to the self-hosted `cldmv-runners` on private repos, so the required check actually runs (and posts) where GitHub-hosted runners aren't used — otherwise the ruleset's required check never reports and the PR can't merge (`13a4e1b`).
20
+
21
+ ## 🔧 CI & tooling
22
+
23
+ - Pass `BOT_NAME` / `BOT_EMAIL` to the v4 release and feature-PR workflows so changelog contributor lists are accurate; retrigger `feature-pr` against the fixed `@v4` ([#34](https://github.com/CLDMV/gh-broker/pull/34), `cf559cd`, `97bb0f1`).
24
+
25
+ ## 🔧 Dependencies
26
+
27
+ - Bump `typescript` `6.0.3` → `7.0.2` ([#21](https://github.com/CLDMV/gh-broker/pull/21)), `@octokit/auth-app` `8.2.0` → `8.3.0` ([#20](https://github.com/CLDMV/gh-broker/pull/20)), `@octokit/core` `7.0.7` → `7.0.8` ([#37](https://github.com/CLDMV/gh-broker/pull/37)), `@types/node` `26.1.0` → `26.4.0` ([#38](https://github.com/CLDMV/gh-broker/pull/38)), and a patch group of 4 updates ([#36](https://github.com/CLDMV/gh-broker/pull/36)).
28
+
29
+ ---
30
+
31
+ ## Upgrade notes
32
+
33
+ - No breaking changes — drop-in for v0.2.2. The change is CI-only.
@@ -0,0 +1,25 @@
1
+ # gh-broker v0.2.4 Changelog
2
+
3
+ **Release Date**: September 2026
4
+ **Release Type**: Patch
5
+ **Branch**: `release/0.2.4`
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ A CI-signing fix — no runtime behavior changed. Shipped in [#42](https://github.com/CLDMV/gh-broker/pull/42).
12
+
13
+ ---
14
+
15
+ ## 🔧 CI & tooling
16
+
17
+ ### Sign redirected security PRs ([#41](https://github.com/CLDMV/gh-broker/pull/41))
18
+
19
+ The hotfix-redirector now maps the `CLDMV_BOT_*` GPG secrets so that security PRs it redirects are GPG-signed (and therefore "Verified"), instead of landing unsigned and being blocked by the verified-signature merge rule (`e754c70`).
20
+
21
+ ---
22
+
23
+ ## Upgrade notes
24
+
25
+ - No breaking changes — drop-in for v0.2.3. The change is CI-only.
@@ -0,0 +1,29 @@
1
+ # gh-broker v0.2.5 Changelog
2
+
3
+ **Release Date**: September 2026
4
+ **Release Type**: Patch
5
+ **Branch**: `hotfixes` (shipped via the hotfixes lane)
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ A security-maintenance **hotfix** — shipped directly through the `hotfixes → master` lane rather than the `next` queue. No runtime behavior changed. Shipped in [#46](https://github.com/CLDMV/gh-broker/pull/46).
12
+
13
+ ---
14
+
15
+ ## 🔒 Security & supply chain
16
+
17
+ ### `npm_and_yarn` group bump ([#40](https://github.com/CLDMV/gh-broker/pull/40))
18
+
19
+ Bump the `npm_and_yarn` dependency group across 1 directory with 4 updates, clearing the group's open security advisories (`a401571`).
20
+
21
+ ## 🔧 Dependencies
22
+
23
+ - `npm_and_yarn` group, 4 updates ([#40](https://github.com/CLDMV/gh-broker/pull/40)).
24
+
25
+ ---
26
+
27
+ ## Upgrade notes
28
+
29
+ - No breaking changes — drop-in for v0.2.4.