@cldmv/gh-broker 0.7.0 → 0.9.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/README.md +165 -41
  2. package/config.example.json +38 -9
  3. package/config.sample.json +45 -0
  4. package/deploy/README.md +76 -17
  5. package/deploy/install.mjs +171 -104
  6. package/deploy/install.sh +0 -0
  7. package/deploy/lib/config-merge.mjs +174 -16
  8. package/deploy/lib/credentials.mjs +143 -0
  9. package/deploy/lib/install-args.mjs +144 -0
  10. package/deploy/lib/npm-setup.mjs +164 -0
  11. package/deploy/lib/secret-store.mjs +20 -2
  12. package/deploy/lib/system.mjs +26 -2
  13. package/dist/index.mjs +5864 -1809
  14. package/dist/index.mjs.map +4 -4
  15. package/package.json +18 -8
  16. package/.configs/fix-headers.json +0 -3
  17. package/.configs/vitest.config.mjs +0 -44
  18. package/.github/dependabot.yml +0 -111
  19. package/.github/workflows/branch-retention.yml +0 -44
  20. package/.github/workflows/bundle-size.yml +0 -52
  21. package/.github/workflows/ci.yml +0 -389
  22. package/.github/workflows/cla.yml +0 -66
  23. package/.github/workflows/codeql.yml +0 -87
  24. package/.github/workflows/dependabot-auto-merge.yml +0 -62
  25. package/.github/workflows/dependabot-recreate.yml +0 -67
  26. package/.github/workflows/dependency-review.yml +0 -46
  27. package/.github/workflows/feature-pr.yml +0 -75
  28. package/.github/workflows/hotfix-redirector.yml +0 -67
  29. package/.github/workflows/hotfixes-release.yml +0 -70
  30. package/.github/workflows/labeler.yml +0 -48
  31. package/.github/workflows/master-commit-audit.yml +0 -56
  32. package/.github/workflows/member-auto-merge.yml +0 -75
  33. package/.github/workflows/next-release.yml +0 -76
  34. package/.github/workflows/next-reset.yml +0 -50
  35. package/.github/workflows/pr-notify.yml +0 -44
  36. package/.github/workflows/pr-title-normalizer.yml +0 -52
  37. package/.github/workflows/provenance.yml +0 -51
  38. package/.github/workflows/publish.yml +0 -146
  39. package/.github/workflows/release-merge.yml +0 -104
  40. package/.github/workflows/release-notify.yml +0 -41
  41. package/.github/workflows/scorecard.yml +0 -64
  42. package/.github/workflows/stale.yml +0 -50
  43. package/.github/workflows/tag-health.yml +0 -70
  44. package/.github/workflows/update-major-version-tags.yml +0 -94
  45. package/.github/workflows/v4-bootstrap.yml +0 -115
  46. package/.github/workflows/welcome.yml +0 -42
  47. package/docs/TOOLS.md +0 -347
  48. package/docs/agent-user-model.md +0 -89
  49. package/docs/changelog/v0/v0.2.0.md +0 -26
  50. package/docs/changelog/v0/v0.2.1.md +0 -23
  51. package/docs/changelog/v0/v0.2.2.md +0 -23
  52. package/docs/changelog/v0/v0.2.3.md +0 -33
  53. package/docs/changelog/v0/v0.2.4.md +0 -25
  54. package/docs/changelog/v0/v0.2.5.md +0 -29
  55. package/docs/changelog/v0/v0.3.0.md +0 -84
  56. package/docs/changelog/v0/v0.3.1.md +0 -30
  57. package/docs/changelog/v0/v0.3.2.md +0 -24
  58. package/docs/changelog/v0/v0.3.3.md +0 -28
  59. package/docs/changelog/v0/v0.4.0.md +0 -58
  60. package/docs/changelog/v0/v0.4.1.md +0 -33
  61. package/docs/changelog/v0/v0.4.2.md +0 -45
  62. package/docs/changelog/v0/v0.5.0.md +0 -39
  63. package/docs/changelog/v0/v0.5.1.md +0 -28
  64. package/docs/changelog/v0/v0.6.0.md +0 -52
  65. package/docs/changelog/v0/v0.6.1.md +0 -48
  66. package/docs/changelog/v0/v0.7.0.md +0 -71
  67. package/docs/deploy-architecture.md +0 -82
  68. package/src/appauth.ts +0 -56
  69. package/src/audit.ts +0 -34
  70. package/src/catalog.ts +0 -165
  71. package/src/config.ts +0 -130
  72. package/src/index.ts +0 -107
  73. package/src/paths.ts +0 -60
  74. package/src/router.ts +0 -258
  75. package/src/secrets.ts +0 -137
  76. package/src/server.ts +0 -91
  77. package/src/types.ts +0 -104
  78. package/src/upstream.ts +0 -85
  79. package/src/verbs/issue_labels.ts +0 -329
  80. package/src/verbs/link_issue.ts +0 -84
  81. package/src/verbs/release-analysis.ts +0 -210
  82. package/src/verbs/repo-tier.ts +0 -90
  83. package/src/verbs/report_ci_status.ts +0 -358
  84. package/src/verbs/report_release_prs.ts +0 -238
  85. package/src/verbs/report_security_alerts.ts +0 -442
  86. package/src/verbs/report_unlabeled.ts +0 -194
  87. package/src/verbs/rest-client.ts +0 -181
  88. package/src/verbs/review_thread.ts +0 -138
  89. package/tests/appauth.test.vitest.mts +0 -76
  90. package/tests/audit.test.vitest.mts +0 -48
  91. package/tests/catalog.test.vitest.mts +0 -180
  92. package/tests/config-merge.test.vitest.mts +0 -58
  93. package/tests/config.test.vitest.mts +0 -314
  94. package/tests/issue-labels.test.vitest.mts +0 -347
  95. package/tests/paths.test.vitest.mts +0 -52
  96. package/tests/release-prs.test.vitest.mts +0 -237
  97. package/tests/rest-client.test.vitest.mts +0 -115
  98. package/tests/router.test.vitest.mts +0 -399
  99. package/tests/run-vitest.mjs +0 -51
  100. package/tests/secret-store.test.vitest.mts +0 -106
  101. package/tests/secrets.test.vitest.mts +0 -155
  102. package/tests/security-alerts.test.vitest.mts +0 -328
  103. package/tests/server.test.vitest.mts +0 -20
  104. package/tests/upstream.test.vitest.mts +0 -91
  105. package/tests/verb-helpers.test.vitest.mts +0 -331
  106. package/tests/verbs.test.vitest.mts +0 -624
  107. package/tsconfig.json +0 -21
@@ -1,94 +0,0 @@
1
- #
2
- # @Project: @cldmv/gh-broker
3
- # @Filename: /.github/workflows/update-major-version-tags.yml
4
- # @Date: 2026-05-20T00:00:00-07:00 (1779260400)
5
- # @Author: Nate Corcoran <CLDMV>
6
- # @Email: <Shinrai@users.noreply.github.com>
7
- # -----
8
- # @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
9
- # @Last modified time: 2026-10-02T11:30:58-07:00 (1790965858)
10
- # -----
11
- # @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
12
- #
13
-
14
- # Individual repo: .github/workflows/update-major-version-tags.yml
15
- name: 🏷️ Update Major Version Tags
16
-
17
- on:
18
- release:
19
- types: [published]
20
- workflow_dispatch:
21
- inputs:
22
- debug:
23
- description: "Enable debug logging for troubleshooting"
24
- type: boolean
25
- required: false
26
- default: false
27
- create_documentation:
28
- description: "Whether to create/update VERSION_TAGS.md documentation"
29
- type: boolean
30
- required: false
31
- default: false
32
- use_gpg:
33
- description: "Enable GPG signing (if GPG secrets provided)"
34
- type: boolean
35
- required: false
36
- default: true
37
- # Tag health configuration
38
- max_tags:
39
- description: "Maximum number of tags to process (safety limit)"
40
- required: false
41
- default: "100"
42
- max_major_versions:
43
- description: "Maximum number of major versions to process"
44
- required: false
45
- default: "10"
46
- max_minor_versions:
47
- description: "Maximum number of minor versions per major to process"
48
- required: false
49
- default: "10"
50
- bot_patterns:
51
- description: "JSON array of bot name patterns to identify bot signatures"
52
- required: false
53
- default: '["CLDMV Bot", "cldmv-bot", "github-actions[bot]"]'
54
- include_patterns:
55
- description: "JSON array of tag patterns to include (e.g. ['v*', 'release-*'])"
56
- required: false
57
- default: '["v*"]'
58
- exclude_patterns:
59
- description: "JSON array of tag patterns to exclude"
60
- required: false
61
- default: "[]"
62
-
63
- permissions:
64
- contents: read
65
-
66
- jobs:
67
- update-tags:
68
- # Skip release events fired without a tag_name (e.g. "untagged-<sha>" runs
69
- # the bot or a prior code path can produce). The reusable workflow has its
70
- # own tag-readiness polling for forward-facing prevention; this guard
71
- # protects against legacy / external sources of untagged release events.
72
- # Batch 1.2 from tmp/plan-future-workflows.md.
73
- if: github.event_name != 'release' || github.event.release.tag_name != ''
74
- uses: CLDMV/.github/.github/workflows/workflow-update-major-version-tags.yml@v4
75
- permissions:
76
- contents: write
77
- with:
78
- debug: ${{ github.event.inputs.debug == 'true' }}
79
- create_documentation: ${{ github.event.inputs.create_documentation == 'true' }}
80
- use_gpg: ${{ github.event.inputs.use_gpg != 'false' }}
81
- max_tags: ${{ github.event.inputs.max_tags || '100' }}
82
- max_major_versions: ${{ github.event.inputs.max_major_versions || '10' }}
83
- max_minor_versions: ${{ github.event.inputs.max_minor_versions || '10' }}
84
- bot_patterns: ${{ github.event.inputs.bot_patterns || '["CLDMV Bot", "cldmv-bot", "github-actions[bot]"]' }}
85
- include_patterns: ${{ github.event.inputs.include_patterns || '["v*"]' }}
86
- exclude_patterns: ${{ github.event.inputs.exclude_patterns || '[]' }}
87
- secrets:
88
- # Map your repo/org secrets to the expected names
89
- TAGGER_NAME: ${{ secrets.CLDMV_BOT_NAME }}
90
- TAGGER_EMAIL: ${{ secrets.CLDMV_BOT_EMAIL }}
91
- GPG_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_GPG_PRIVATE_KEY }}
92
- GPG_PASSPHRASE: ${{ secrets.CLDMV_BOT_GPG_PASSPHRASE }}
93
- BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }}
94
- BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }}
@@ -1,115 +0,0 @@
1
- #
2
- # @Project: @cldmv/gh-broker
3
- # @Filename: /.github/workflows/v4-bootstrap.yml
4
- # @Date: 2026-05-26T00:00:00-07:00 (1779778800)
5
- # @Author: Nate Corcoran <CLDMV>
6
- # @Email: <Shinrai@users.noreply.github.com>
7
- # -----
8
- # @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
9
- # @Last modified time: 2026-10-02T11:30:58-07:00 (1790965858)
10
- # -----
11
- # @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
12
- #
13
-
14
- # Individual repo: .github/workflows/v4-bootstrap.yml
15
- #
16
- # Per-repo v4 bootstrap — thin wrapper around the shared
17
- # `org-bootstrap-repo@v4` action. Run once per repo from the Actions tab
18
- # (or, for org-wide rollout, prefer `local-org-onboarding.yml` in
19
- # CLDMV/.github which fans out across many repos in parallel).
20
- #
21
- # What gets applied (overwrite-with-warn — divergences are surfaced in the
22
- # run summary):
23
- # - `next` + `hotfixes` branches created from master HEAD if missing
24
- # - repo settings: allow_auto_merge=true, delete_branch_on_merge=false,
25
- # allow_squash_merge=true, allow_merge_commit=true,
26
- # allow_rebase_merge=false, allow_update_branch=true; plus PR-merge
27
- # dialog defaults (merge_commit_title / squash_merge_commit_title =
28
- # PR_TITLE, merge_commit_message / squash_merge_commit_message =
29
- # PR_BODY) so the resulting commit captures the PR title + body
30
- # verbatim (release-PR body = the categorized changelog → lands on
31
- # master). Per-branch ruleset allowed_merge_methods picks the method.
32
- # - security toggles: dependabot alerts + security updates, secret
33
- # scanning + push protection, private vulnerability reporting
34
- # - rulesets: replaces the three rulesets (Protect Master/Next/Hotfixes)
35
- # with the org canonical defaults
36
- #
37
- # What is NOT applied (GitHub doesn't expose it via REST / GraphQL / gh CLI
38
- # — confirmed against community/community#188598; the bootstrap surfaces
39
- # this as a 'Manual one-time toggles' line in the run summary):
40
- # - Settings → General → Pull Requests → "Auto-close issues with merged
41
- # linked pull requests" (recommended ON). Toggle in the repo UI once.
42
- #
43
- # Idempotent — re-running is safe. Default `dry_run: true` previews
44
- # everything before applying.
45
- #
46
- # Full design: CLDMV/.github docs/conventions/release-flow-v4.md
47
- # Migration checklist: CLDMV/.github docs/migration/v3-to-v4.md
48
- name: 🚀 v4 Bootstrap
49
-
50
- on:
51
- workflow_dispatch:
52
- inputs:
53
- dry_run:
54
- description: "Dry-run: preview every mutation without firing it. Default `true` — set to `false` to actually apply changes."
55
- type: boolean
56
- required: false
57
- default: true
58
- code_security:
59
- description: "Code Security policy. off = disable. public-only = enable only if this repo is public (free). all = enable (paid on private)."
60
- type: choice
61
- required: false
62
- default: "off"
63
- options:
64
- - "off"
65
- - "public-only"
66
- - "all"
67
- secret_protection:
68
- description: "Secret Protection (scanning + push protection) policy. Same shape as code_security."
69
- type: choice
70
- required: false
71
- default: "off"
72
- options:
73
- - "off"
74
- - "public-only"
75
- - "all"
76
- steps:
77
- description: "Subset of phases to run, comma-separated."
78
- required: false
79
- default: "branches,settings,security,rulesets,variables"
80
- code_scanning_config:
81
- description: "Override for the variables-phase GHAS config (scan/skip lists): inline JSON or a workspace path. Empty = the data/code-scanning-skips.json bundled in CLDMV/.github — third-party orgs supply their own here."
82
- required: false
83
- default: ""
84
-
85
- permissions:
86
- contents: read
87
-
88
- jobs:
89
- bootstrap:
90
- name: "🚀 Bootstrap v4 (this repo)"
91
- runs-on: ubuntu-latest
92
- steps:
93
- - name: Create App token
94
- id: app-token
95
- # Full-permission App token — bootstrap needs administration:write
96
- # for security toggles + ruleset import, plus contents:write for
97
- # branch creation.
98
- uses: CLDMV/.github/.github/actions/github/steps/create-app-token@v4
99
- with:
100
- client_id: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }}
101
- private_key: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }}
102
- env:
103
- BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }}
104
- BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }}
105
-
106
- - name: Bootstrap
107
- uses: CLDMV/.github/.github/actions/github/jobs/org-bootstrap-repo@v4
108
- with:
109
- # target_repo defaults to GITHUB_REPOSITORY (this repo).
110
- github_token: ${{ steps.app-token.outputs.token }}
111
- dry_run: ${{ github.event.inputs.dry_run }}
112
- steps: ${{ github.event.inputs.steps }}
113
- code_security: ${{ github.event.inputs.code_security }}
114
- secret_protection: ${{ github.event.inputs.secret_protection }}
115
- code_scanning_config: ${{ github.event.inputs.code_scanning_config }}
@@ -1,42 +0,0 @@
1
- #
2
- # @Project: @cldmv/gh-broker
3
- # @Filename: /.github/workflows/welcome.yml
4
- # @Date: 2026-05-20T00:00:00-07:00 (1779260400)
5
- # @Author: Nate Corcoran <CLDMV>
6
- # @Email: <Shinrai@users.noreply.github.com>
7
- # -----
8
- # @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
9
- # @Last modified time: 2026-10-02T11:30:58-07:00 (1790965858)
10
- # -----
11
- # @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
12
- #
13
-
14
- # Individual repo: .github/workflows/welcome.yml
15
- #
16
- # Batch 5.3 from tmp/plan-future-workflows.md.
17
- name: 👋 Welcome Contributor
18
-
19
- # SECURITY NOTE: pull_request_target runs in the BASE repo's context with
20
- # WRITE permissions and access to secrets. SAFE for THIS workflow because:
21
- # - We never checkout the PR head ref
22
- # - We never run code from the PR (no `run:` step uses PR data)
23
- # - We only call REST APIs to read prior interactions and post a comment
24
- # DO NOT add a checkout step or any step that executes PR-supplied content.
25
- on:
26
- issues:
27
- types: [opened]
28
- pull_request_target:
29
- types: [opened]
30
-
31
- permissions:
32
- issues: write
33
- pull-requests: write
34
-
35
- jobs:
36
- welcome:
37
- uses: CLDMV/.github/.github/workflows/reusable-welcome.yml@v4
38
- # Optional. Without these, the welcome comment is posted by
39
- # github-actions[bot]. With these, it's posted by your CLDMV bot App.
40
- secrets:
41
- BOT_APP_CLIENT_ID: ${{ secrets.CLDMV_BOT_APP_CLIENT_ID }}
42
- BOT_APP_PRIVATE_KEY: ${{ secrets.CLDMV_BOT_APP_PRIVATE_KEY }}
package/docs/TOOLS.md DELETED
@@ -1,347 +0,0 @@
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.