@backburner/cli 0.1.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 (202) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +329 -0
  3. package/dist/src/agents/antigravity-launch.js +162 -0
  4. package/dist/src/agents/antigravity.js +251 -0
  5. package/dist/src/agents/claude-launch.js +117 -0
  6. package/dist/src/agents/claude.js +271 -0
  7. package/dist/src/agents/codex-launch.js +82 -0
  8. package/dist/src/agents/codex.js +261 -0
  9. package/dist/src/agents/composite-runner.js +13 -0
  10. package/dist/src/agents/extraction.js +442 -0
  11. package/dist/src/agents/failure-feedback.js +132 -0
  12. package/dist/src/agents/gemini-launch.js +96 -0
  13. package/dist/src/agents/gemini.js +392 -0
  14. package/dist/src/agents/job-agent-assignments.js +180 -0
  15. package/dist/src/agents/lifecycle-feedback.js +147 -0
  16. package/dist/src/agents/limit-detector.js +83 -0
  17. package/dist/src/agents/opencode-launch.js +138 -0
  18. package/dist/src/agents/opencode.js +156 -0
  19. package/dist/src/agents/plan-breakdown-feedback.js +97 -0
  20. package/dist/src/agents/pool.js +168 -0
  21. package/dist/src/agents/prompt.js +462 -0
  22. package/dist/src/agents/provider-sessions.js +105 -0
  23. package/dist/src/agents/review-feedback.js +255 -0
  24. package/dist/src/agents/selection.js +113 -0
  25. package/dist/src/agents/service.js +1087 -0
  26. package/dist/src/agents/types.js +1 -0
  27. package/dist/src/agents/wave-assessment-feedback.js +69 -0
  28. package/dist/src/attention/derive.js +337 -0
  29. package/dist/src/capabilities/index.js +2 -0
  30. package/dist/src/capabilities/projection.js +183 -0
  31. package/dist/src/capabilities/types.js +1 -0
  32. package/dist/src/capabilities/validator.js +87 -0
  33. package/dist/src/cli/commands/broker-smoke.js +96 -0
  34. package/dist/src/cli/commands/broker.js +56 -0
  35. package/dist/src/cli/commands/init.js +4 -0
  36. package/dist/src/cli/commands/journal.js +43 -0
  37. package/dist/src/cli/commands/run-loop.js +56 -0
  38. package/dist/src/cli/commands/run.js +1241 -0
  39. package/dist/src/cli/commands/tui.js +187 -0
  40. package/dist/src/cli/dispatcher.js +51 -0
  41. package/dist/src/cli/errors.js +2 -0
  42. package/dist/src/cli/init.js +92 -0
  43. package/dist/src/cli/options.js +86 -0
  44. package/dist/src/cli/output.js +231 -0
  45. package/dist/src/cli/run-journal.js +342 -0
  46. package/dist/src/cli/run-types.js +1 -0
  47. package/dist/src/cli/run.js +32 -0
  48. package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
  49. package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
  50. package/dist/src/cli/runtime/provider-runtime.js +152 -0
  51. package/dist/src/cli/runtime/provider-tools.js +73 -0
  52. package/dist/src/cli/runtime/providers/antigravity.js +26 -0
  53. package/dist/src/cli/runtime/providers/claude.js +20 -0
  54. package/dist/src/cli/runtime/providers/codex.js +20 -0
  55. package/dist/src/cli/runtime/providers/gemini.js +20 -0
  56. package/dist/src/cli/runtime/providers/opencode.js +20 -0
  57. package/dist/src/cli/runtime/run-context.js +131 -0
  58. package/dist/src/cli/tui/index.js +3 -0
  59. package/dist/src/cli/tui/screen.js +388 -0
  60. package/dist/src/cli/tui/tracker.js +558 -0
  61. package/dist/src/cli/tui/types.js +1 -0
  62. package/dist/src/cli/tui/views.js +1006 -0
  63. package/dist/src/config/loader.js +136 -0
  64. package/dist/src/config/schemas.js +403 -0
  65. package/dist/src/config/types.js +1 -0
  66. package/dist/src/context/types.js +331 -0
  67. package/dist/src/domain/entities.js +1 -0
  68. package/dist/src/git/backburner-git-tool-service.js +680 -0
  69. package/dist/src/git/gitops-lifecycle.js +56 -0
  70. package/dist/src/git/merge-prep-workspace.js +357 -0
  71. package/dist/src/git/types.js +484 -0
  72. package/dist/src/github/broker-model-content.js +215 -0
  73. package/dist/src/github/broker-server.js +237 -0
  74. package/dist/src/github/broker.js +638 -0
  75. package/dist/src/github/composed.js +53 -0
  76. package/dist/src/github/gateway.js +478 -0
  77. package/dist/src/github/normalize.js +222 -0
  78. package/dist/src/github/pr-classification.js +73 -0
  79. package/dist/src/github/scoped-broker.js +35 -0
  80. package/dist/src/github/security.js +126 -0
  81. package/dist/src/github/service.js +332 -0
  82. package/dist/src/github/types.js +1 -0
  83. package/dist/src/github/utils.js +16 -0
  84. package/dist/src/journal/index.js +4 -0
  85. package/dist/src/journal/reader.js +62 -0
  86. package/dist/src/journal/renderer.js +50 -0
  87. package/dist/src/journal/service.js +72 -0
  88. package/dist/src/journal/types.js +1 -0
  89. package/dist/src/journal/writer.js +19 -0
  90. package/dist/src/mcps/bridge.js +166 -0
  91. package/dist/src/mcps/index.js +5 -0
  92. package/dist/src/mcps/reconcile.js +113 -0
  93. package/dist/src/mcps/smithery-client.js +186 -0
  94. package/dist/src/mcps/smithery.js +164 -0
  95. package/dist/src/mcps/types.js +1 -0
  96. package/dist/src/memory/json-store.js +71 -0
  97. package/dist/src/memory/prompt.js +16 -0
  98. package/dist/src/memory/scope.js +31 -0
  99. package/dist/src/memory/store.js +1 -0
  100. package/dist/src/memory/tool-service.js +142 -0
  101. package/dist/src/memory/types.js +12 -0
  102. package/dist/src/onboarding/config-writer.js +96 -0
  103. package/dist/src/onboarding/engine.js +86 -0
  104. package/dist/src/onboarding/prompt.js +102 -0
  105. package/dist/src/onboarding/services/provider-discovery.js +58 -0
  106. package/dist/src/onboarding/services/repo-discovery.js +246 -0
  107. package/dist/src/onboarding/steps/check-github-auth.js +32 -0
  108. package/dist/src/onboarding/steps/check-requirements.js +34 -0
  109. package/dist/src/onboarding/steps/configure-models.js +105 -0
  110. package/dist/src/onboarding/steps/configure-paths.js +33 -0
  111. package/dist/src/onboarding/steps/discover-providers.js +38 -0
  112. package/dist/src/onboarding/steps/discover-repos.js +39 -0
  113. package/dist/src/onboarding/steps/generate-config.js +106 -0
  114. package/dist/src/onboarding/steps/readiness-validation.js +158 -0
  115. package/dist/src/onboarding/steps/select-repos.js +76 -0
  116. package/dist/src/onboarding/steps/show-summary.js +97 -0
  117. package/dist/src/onboarding/steps/welcome.js +27 -0
  118. package/dist/src/onboarding/types.js +1 -0
  119. package/dist/src/onboarding/validators/git-access.js +63 -0
  120. package/dist/src/onboarding/validators/github-auth.js +85 -0
  121. package/dist/src/onboarding/validators/tool-checker.js +153 -0
  122. package/dist/src/onboarding/validators/workspace.js +83 -0
  123. package/dist/src/prompts/resolver.js +106 -0
  124. package/dist/src/retrospectives/candidates.js +224 -0
  125. package/dist/src/retrospectives/derive.js +169 -0
  126. package/dist/src/retrospectives/ingest.js +321 -0
  127. package/dist/src/retrospectives/management-prs.js +380 -0
  128. package/dist/src/retrospectives/proposals.js +199 -0
  129. package/dist/src/retrospectives/scopes.js +272 -0
  130. package/dist/src/retrospectives/scoring.js +171 -0
  131. package/dist/src/retrospectives/types.js +1 -0
  132. package/dist/src/state/loader.js +473 -0
  133. package/dist/src/state/types.js +15 -0
  134. package/dist/src/tasks/derivation/builders.js +70 -0
  135. package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
  136. package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
  137. package/dist/src/tasks/derivation/handlers/index.js +54 -0
  138. package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
  139. package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
  140. package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
  141. package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
  142. package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
  143. package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
  144. package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
  145. package/dist/src/tasks/derivation/handlers/shared.js +24 -0
  146. package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
  147. package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
  148. package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
  149. package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
  150. package/dist/src/tasks/derivation/runner.js +18 -0
  151. package/dist/src/tasks/derivation/types.js +1 -0
  152. package/dist/src/tasks/derive.js +810 -0
  153. package/dist/src/tasks/mcp-skip.js +10 -0
  154. package/dist/src/tasks/retry.js +10 -0
  155. package/dist/src/tasks/task.js +184 -0
  156. package/dist/src/tasks/types.js +1 -0
  157. package/dist/src/utils/command-runner.js +192 -0
  158. package/dist/src/utils/json.js +38 -0
  159. package/dist/src/utils/logger.js +43 -0
  160. package/dist/src/utils/paths.js +38 -0
  161. package/dist/src/utils/slug.js +10 -0
  162. package/dist/src/workflows/registry.js +174 -0
  163. package/dist/src/workflows/triage.js +90 -0
  164. package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
  165. package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
  166. package/dist/src/workflows/workstream-machine/events.js +1 -0
  167. package/dist/src/workflows/workstream-machine/machine.js +355 -0
  168. package/dist/src/workflows/workstream-machine/selectors.js +19 -0
  169. package/dist/src/workflows/workstream-machine/types.js +1 -0
  170. package/dist/src/workstreams/branch-sync.js +87 -0
  171. package/dist/src/workstreams/derive.js +629 -0
  172. package/dist/src/workstreams/mcp-blockers.js +420 -0
  173. package/dist/src/workstreams/packet-candidates.js +42 -0
  174. package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
  175. package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
  176. package/dist/src/workstreams/packet-plan.js +597 -0
  177. package/dist/src/workstreams/packet-projections.js +190 -0
  178. package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
  179. package/dist/src/workstreams/parent-branch-sync.js +148 -0
  180. package/dist/src/workstreams/phases.js +4 -0
  181. package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
  182. package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
  183. package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
  184. package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
  185. package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
  186. package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
  187. package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
  188. package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
  189. package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
  190. package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
  191. package/dist/src/workstreams/utils.js +15 -0
  192. package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
  193. package/dist/src/workstreams/wave-assessment/index.js +3 -0
  194. package/dist/src/workstreams/wave-assessment/session.js +84 -0
  195. package/dist/src/workstreams/wave-assessment/store.js +51 -0
  196. package/dist/src/workstreams/wave-assessment/tools.js +419 -0
  197. package/dist/src/workstreams/wave-assessment/types.js +1 -0
  198. package/dist/src/workstreams/wave-assessment/validate.js +78 -0
  199. package/dist/src/worktrees/service.js +839 -0
  200. package/dist/src/worktrees/workspace-ownership.js +34 -0
  201. package/dist/src/worktrees/workspace-recovery.js +177 -0
  202. package/package.json +61 -0
@@ -0,0 +1,638 @@
1
+ const MAX_COMMENT_BODY_LENGTH = 65_536;
2
+ const MAX_PR_BODY_LENGTH = 65_536;
3
+ const MAX_SEARCH_LIMIT = 25;
4
+ export class GitHubBrokerService {
5
+ gitHubGateway;
6
+ logger;
7
+ managedRepos;
8
+ allowedToolNames;
9
+ constructor(gitHubGateway, repos, logger, allowedToolNames = TOOL_DEFINITIONS.map((tool) => tool.name)) {
10
+ this.gitHubGateway = gitHubGateway;
11
+ this.logger = logger;
12
+ this.allowedToolNames = new Set(allowedToolNames);
13
+ this.managedRepos = new Map(repos
14
+ .filter((repo) => repo.enabled)
15
+ .map((repo) => {
16
+ const slug = `${repo.owner}/${repo.name}`;
17
+ return [
18
+ slug,
19
+ {
20
+ id: repo.id,
21
+ owner: repo.owner,
22
+ name: repo.name,
23
+ slug
24
+ }
25
+ ];
26
+ }));
27
+ }
28
+ listTools() {
29
+ return TOOL_DEFINITIONS.filter((tool) => this.allowedToolNames.has(tool.name));
30
+ }
31
+ async callTool(name, rawArguments) {
32
+ const startedAt = new Date().toISOString();
33
+ const sanitizedArguments = sanitizeArguments(name, rawArguments);
34
+ if (!this.allowedToolNames.has(name)) {
35
+ const repo = extractRepoSlug(rawArguments);
36
+ return {
37
+ ok: false,
38
+ tool: name,
39
+ error: {
40
+ code: "tool_not_found",
41
+ message: `GitHub broker tool "${name}" is not enabled for this run`
42
+ }
43
+ };
44
+ }
45
+ try {
46
+ switch (name) {
47
+ case "get_issue":
48
+ return await this.runReadTool(startedAt, name, rawArguments, sanitizedArguments, async () => {
49
+ const input = validateIssueLookup(rawArguments);
50
+ const repo = this.requireManagedRepo(input.repo);
51
+ const issue = await this.gitHubGateway.getIssue(repo.owner, repo.name, input.issueNumber);
52
+ return {
53
+ ok: true,
54
+ tool: name,
55
+ repo: repo.slug,
56
+ summary: `get_issue succeeded for ${repo.slug}`,
57
+ result: {
58
+ repo: repo.slug,
59
+ issue
60
+ }
61
+ };
62
+ });
63
+ case "get_issue_comments":
64
+ return await this.runReadTool(startedAt, name, rawArguments, sanitizedArguments, async () => {
65
+ const input = validateIssueLookup(rawArguments);
66
+ const repo = this.requireManagedRepo(input.repo);
67
+ const comments = await this.gitHubGateway.listIssueComments(repo.owner, repo.name, input.issueNumber);
68
+ return {
69
+ ok: true,
70
+ tool: name,
71
+ repo: repo.slug,
72
+ summary: `get_issue_comments succeeded for ${repo.slug}`,
73
+ result: {
74
+ repo: repo.slug,
75
+ issueNumber: input.issueNumber,
76
+ comments
77
+ }
78
+ };
79
+ });
80
+ case "get_pr":
81
+ return await this.runReadTool(startedAt, name, rawArguments, sanitizedArguments, async () => {
82
+ const input = validatePullRequestLookup(rawArguments);
83
+ const repo = this.requireManagedRepo(input.repo);
84
+ const pullRequest = await this.gitHubGateway.getPullRequest(repo.owner, repo.name, input.pullRequestNumber);
85
+ return {
86
+ ok: true,
87
+ tool: name,
88
+ repo: repo.slug,
89
+ summary: `get_pr succeeded for ${repo.slug}`,
90
+ result: {
91
+ repo: repo.slug,
92
+ pullRequest
93
+ }
94
+ };
95
+ });
96
+ case "get_pr_comments":
97
+ return await this.runReadTool(startedAt, name, rawArguments, sanitizedArguments, async () => {
98
+ const input = validatePullRequestLookup(rawArguments);
99
+ const repo = this.requireManagedRepo(input.repo);
100
+ const [comments, reviews, reviewComments] = await Promise.all([
101
+ this.gitHubGateway.listPullRequestComments(repo.owner, repo.name, input.pullRequestNumber),
102
+ this.gitHubGateway.listPullRequestReviews(repo.owner, repo.name, input.pullRequestNumber),
103
+ this.gitHubGateway.listPullRequestReviewComments(repo.owner, repo.name, input.pullRequestNumber)
104
+ ]);
105
+ return {
106
+ ok: true,
107
+ tool: name,
108
+ repo: repo.slug,
109
+ summary: `get_pr_comments succeeded for ${repo.slug}`,
110
+ result: {
111
+ repo: repo.slug,
112
+ pullRequestNumber: input.pullRequestNumber,
113
+ comments,
114
+ reviews,
115
+ reviewComments
116
+ }
117
+ };
118
+ });
119
+ case "get_pr_checks":
120
+ return await this.runReadTool(startedAt, name, rawArguments, sanitizedArguments, async () => {
121
+ const input = validatePullRequestLookup(rawArguments);
122
+ const repo = this.requireManagedRepo(input.repo);
123
+ const pullRequest = await this.gitHubGateway.getPullRequest(repo.owner, repo.name, input.pullRequestNumber);
124
+ if (!pullRequest.headSha) {
125
+ return {
126
+ ok: true,
127
+ tool: name,
128
+ repo: repo.slug,
129
+ summary: `get_pr_checks: head SHA unavailable for PR #${input.pullRequestNumber}`,
130
+ result: {
131
+ repo: repo.slug,
132
+ pullRequestNumber: input.pullRequestNumber,
133
+ headSha: "",
134
+ checkRuns: []
135
+ }
136
+ };
137
+ }
138
+ const checkRuns = await this.gitHubGateway.listPullRequestCheckRuns(repo.owner, repo.name, pullRequest.headSha);
139
+ return {
140
+ ok: true,
141
+ tool: name,
142
+ repo: repo.slug,
143
+ summary: `get_pr_checks succeeded for ${repo.slug}`,
144
+ result: {
145
+ repo: repo.slug,
146
+ pullRequestNumber: input.pullRequestNumber,
147
+ headSha: pullRequest.headSha,
148
+ checkRuns
149
+ }
150
+ };
151
+ });
152
+ case "search_code":
153
+ return await this.runReadTool(startedAt, name, rawArguments, sanitizedArguments, async () => {
154
+ const input = validateCodeSearch(rawArguments);
155
+ const repo = this.requireManagedRepo(input.repo);
156
+ const result = await this.gitHubGateway.searchCode(input.pathPrefix === undefined
157
+ ? {
158
+ owner: repo.owner,
159
+ name: repo.name,
160
+ query: input.query,
161
+ limit: input.limit
162
+ }
163
+ : {
164
+ owner: repo.owner,
165
+ name: repo.name,
166
+ query: input.query,
167
+ pathPrefix: input.pathPrefix,
168
+ limit: input.limit
169
+ });
170
+ return {
171
+ ok: true,
172
+ tool: name,
173
+ repo: repo.slug,
174
+ summary: `search_code succeeded for ${repo.slug}`,
175
+ result: {
176
+ repo: repo.slug,
177
+ query: input.query,
178
+ pathPrefix: input.pathPrefix ?? null,
179
+ totalCount: result.totalCount,
180
+ incompleteResults: result.incompleteResults,
181
+ items: result.items
182
+ }
183
+ };
184
+ });
185
+ case "post_issue_comment":
186
+ return await this.runReadTool(startedAt, name, rawArguments, sanitizedArguments, async () => {
187
+ const input = validatePostIssueComment(rawArguments);
188
+ const repo = this.requireManagedRepo(input.repo);
189
+ const comment = await this.gitHubGateway.postIssueComment({
190
+ owner: repo.owner,
191
+ name: repo.name,
192
+ issueNumber: input.issueNumber,
193
+ body: input.body
194
+ });
195
+ return {
196
+ ok: true,
197
+ tool: name,
198
+ repo: repo.slug,
199
+ summary: `post_issue_comment succeeded for ${repo.slug}`,
200
+ result: {
201
+ repo: repo.slug,
202
+ issueNumber: input.issueNumber,
203
+ comment
204
+ }
205
+ };
206
+ });
207
+ case "post_pr_review_comment_reply":
208
+ return await this.runReadTool(startedAt, name, rawArguments, sanitizedArguments, async () => {
209
+ const input = validatePostPullRequestReviewCommentReply(rawArguments);
210
+ const repo = this.requireManagedRepo(input.repo);
211
+ const comment = await this.gitHubGateway.postPullRequestReviewCommentReply({
212
+ owner: repo.owner,
213
+ name: repo.name,
214
+ pullRequestNumber: input.pullRequestNumber,
215
+ commentId: input.commentId,
216
+ body: input.body
217
+ });
218
+ return {
219
+ ok: true,
220
+ tool: name,
221
+ repo: repo.slug,
222
+ summary: `post_pr_review_comment_reply succeeded for ${repo.slug}`,
223
+ result: {
224
+ repo: repo.slug,
225
+ pullRequestNumber: input.pullRequestNumber,
226
+ commentId: input.commentId,
227
+ reply: comment
228
+ }
229
+ };
230
+ });
231
+ case "create_pr":
232
+ return await this.runReadTool(startedAt, name, rawArguments, sanitizedArguments, async () => {
233
+ const input = validateCreatePullRequest(rawArguments);
234
+ const repo = this.requireManagedRepo(input.repo);
235
+ const baseBranch = input.baseBranch ?? (await this.gitHubGateway.getRepositoryDefaultBranch(repo.owner, repo.name));
236
+ const pullRequest = await this.gitHubGateway.createPullRequest({
237
+ owner: repo.owner,
238
+ name: repo.name,
239
+ headBranch: input.headBranch,
240
+ baseBranch,
241
+ title: input.title,
242
+ body: input.body
243
+ });
244
+ return {
245
+ ok: true,
246
+ tool: name,
247
+ repo: repo.slug,
248
+ summary: `create_pr succeeded for ${repo.slug}`,
249
+ result: {
250
+ repo: repo.slug,
251
+ pullRequest
252
+ }
253
+ };
254
+ });
255
+ default:
256
+ return {
257
+ ok: false,
258
+ tool: name,
259
+ error: {
260
+ code: "tool_not_found",
261
+ message: `Unknown GitHub broker tool "${name}"`
262
+ }
263
+ };
264
+ }
265
+ }
266
+ catch (error) {
267
+ const failure = toFailure(name, rawArguments, error);
268
+ const repo = extractRepoSlug(rawArguments);
269
+ this.logInvocation({
270
+ timestamp: startedAt,
271
+ toolName: name,
272
+ status: "error",
273
+ arguments: sanitizedArguments,
274
+ ...(repo ? { repo } : {}),
275
+ message: failure.error.message
276
+ });
277
+ return failure;
278
+ }
279
+ }
280
+ requireManagedRepo(repoSlug) {
281
+ const repo = this.managedRepos.get(repoSlug);
282
+ if (!repo) {
283
+ throw new RepoNotManagedError(`Repository "${repoSlug}" is not enabled in management/repos.json`);
284
+ }
285
+ return repo;
286
+ }
287
+ async runReadTool(timestamp, toolName, rawArguments, sanitizedArguments, runner) {
288
+ const result = await runner();
289
+ this.logInvocation({
290
+ timestamp,
291
+ toolName,
292
+ repo: result.repo,
293
+ status: "ok",
294
+ arguments: sanitizedArguments
295
+ });
296
+ return result;
297
+ }
298
+ logInvocation(entry) {
299
+ this.logger.info(JSON.stringify({
300
+ component: "github-broker-mcp",
301
+ timestamp: entry.timestamp,
302
+ toolName: entry.toolName,
303
+ repo: entry.repo ?? "unknown",
304
+ resultStatus: entry.status,
305
+ arguments: entry.arguments,
306
+ ...(entry.message ? { message: entry.message } : {})
307
+ }));
308
+ }
309
+ }
310
+ class BrokerValidationError extends Error {
311
+ }
312
+ class RepoNotManagedError extends Error {
313
+ }
314
+ function toFailure(name, rawArguments, error) {
315
+ const repo = extractRepoSlug(rawArguments);
316
+ if (error instanceof BrokerValidationError) {
317
+ return {
318
+ ok: false,
319
+ tool: name,
320
+ ...(repo ? { repo } : {}),
321
+ error: {
322
+ code: "invalid_arguments",
323
+ message: error.message
324
+ }
325
+ };
326
+ }
327
+ if (error instanceof RepoNotManagedError) {
328
+ return {
329
+ ok: false,
330
+ tool: name,
331
+ ...(repo ? { repo } : {}),
332
+ error: {
333
+ code: "repo_not_managed",
334
+ message: error.message
335
+ }
336
+ };
337
+ }
338
+ return {
339
+ ok: false,
340
+ tool: name,
341
+ ...(repo ? { repo } : {}),
342
+ error: {
343
+ code: "github_error",
344
+ message: error instanceof Error ? error.message : String(error)
345
+ }
346
+ };
347
+ }
348
+ function validateIssueLookup(value) {
349
+ const object = expectObject(value, "tool arguments must be an object");
350
+ return {
351
+ repo: expectRepoSlug(object.repo, '"repo" must be an "owner/name" string'),
352
+ issueNumber: expectPositiveInteger(object.issueNumber, '"issueNumber" must be a positive integer')
353
+ };
354
+ }
355
+ function validatePullRequestLookup(value) {
356
+ const object = expectObject(value, "tool arguments must be an object");
357
+ return {
358
+ repo: expectRepoSlug(object.repo, '"repo" must be an "owner/name" string'),
359
+ pullRequestNumber: expectPositiveInteger(object.pullRequestNumber, '"pullRequestNumber" must be a positive integer')
360
+ };
361
+ }
362
+ function validateCodeSearch(value) {
363
+ const object = expectObject(value, "tool arguments must be an object");
364
+ const limitValue = object.limit === undefined ? 10 : object.limit;
365
+ const pathPrefix = optionalTrimmedString(object.pathPrefix, '"pathPrefix" must be a non-empty string when provided');
366
+ return {
367
+ repo: expectRepoSlug(object.repo, '"repo" must be an "owner/name" string'),
368
+ query: expectNonEmptyString(object.query, '"query" must be a non-empty string'),
369
+ ...(pathPrefix ? { pathPrefix } : {}),
370
+ limit: expectBoundedPositiveInteger(limitValue, 1, MAX_SEARCH_LIMIT, `"limit" must be between 1 and ${MAX_SEARCH_LIMIT}`)
371
+ };
372
+ }
373
+ function validatePostIssueComment(value) {
374
+ const object = expectObject(value, "tool arguments must be an object");
375
+ return {
376
+ repo: expectRepoSlug(object.repo, '"repo" must be an "owner/name" string'),
377
+ issueNumber: expectPositiveInteger(object.issueNumber, '"issueNumber" must be a positive integer'),
378
+ body: expectBoundedString(object.body, MAX_COMMENT_BODY_LENGTH, `"body" must be a non-empty string up to ${MAX_COMMENT_BODY_LENGTH} characters`)
379
+ };
380
+ }
381
+ function validateCreatePullRequest(value) {
382
+ const object = expectObject(value, "tool arguments must be an object");
383
+ const baseBranch = optionalTrimmedString(object.baseBranch, '"baseBranch" must be a non-empty string when provided');
384
+ return {
385
+ repo: expectRepoSlug(object.repo, '"repo" must be an "owner/name" string'),
386
+ headBranch: expectNonEmptyString(object.headBranch, '"headBranch" must be a non-empty string'),
387
+ ...(baseBranch ? { baseBranch } : {}),
388
+ title: expectNonEmptyString(object.title, '"title" must be a non-empty string'),
389
+ body: expectBoundedString(object.body, MAX_PR_BODY_LENGTH, `"body" must be a non-empty string up to ${MAX_PR_BODY_LENGTH} characters`)
390
+ };
391
+ }
392
+ function validatePostPullRequestReviewCommentReply(value) {
393
+ const object = expectObject(value, "tool arguments must be an object");
394
+ return {
395
+ repo: expectRepoSlug(object.repo, '"repo" must be an "owner/name" string'),
396
+ pullRequestNumber: expectPositiveInteger(object.pullRequestNumber, '"pullRequestNumber" must be a positive integer'),
397
+ commentId: expectPositiveInteger(object.commentId, '"commentId" must be a positive integer'),
398
+ body: expectBoundedString(object.body, MAX_COMMENT_BODY_LENGTH, `"body" must be a non-empty string up to ${MAX_COMMENT_BODY_LENGTH} characters`)
399
+ };
400
+ }
401
+ function sanitizeArguments(name, value) {
402
+ const object = isRecord(value) ? value : {};
403
+ switch (name) {
404
+ case "post_issue_comment":
405
+ case "post_pr_review_comment_reply":
406
+ return {
407
+ repo: safeString(object.repo),
408
+ ...(name === "post_issue_comment"
409
+ ? { issueNumber: safeNumber(object.issueNumber) }
410
+ : {
411
+ pullRequestNumber: safeNumber(object.pullRequestNumber),
412
+ commentId: safeNumber(object.commentId)
413
+ }),
414
+ bodyPreview: previewBody(object.body),
415
+ bodyLength: safeLength(object.body)
416
+ };
417
+ case "create_pr":
418
+ return {
419
+ repo: safeString(object.repo),
420
+ headBranch: safeString(object.headBranch),
421
+ baseBranch: safeString(object.baseBranch),
422
+ title: safeString(object.title),
423
+ bodyPreview: previewBody(object.body),
424
+ bodyLength: safeLength(object.body)
425
+ };
426
+ default:
427
+ return Object.fromEntries(Object.entries(object).map(([key, entry]) => [key, typeof entry === "string" ? entry : entry ?? null]));
428
+ }
429
+ }
430
+ function previewBody(value) {
431
+ if (typeof value !== "string") {
432
+ return null;
433
+ }
434
+ const trimmed = value.trim();
435
+ if (trimmed === "") {
436
+ return "";
437
+ }
438
+ return trimmed.slice(0, 120);
439
+ }
440
+ function safeLength(value) {
441
+ return typeof value === "string" ? value.length : null;
442
+ }
443
+ function safeString(value) {
444
+ return typeof value === "string" ? value : null;
445
+ }
446
+ function safeNumber(value) {
447
+ return typeof value === "number" ? value : null;
448
+ }
449
+ function extractRepoSlug(value) {
450
+ if (!isRecord(value) || typeof value.repo !== "string") {
451
+ return undefined;
452
+ }
453
+ return value.repo;
454
+ }
455
+ function expectObject(value, message) {
456
+ if (!isRecord(value)) {
457
+ throw new BrokerValidationError(message);
458
+ }
459
+ return value;
460
+ }
461
+ function expectRepoSlug(value, message) {
462
+ const repo = expectNonEmptyString(value, message);
463
+ if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) {
464
+ throw new BrokerValidationError(message);
465
+ }
466
+ return repo;
467
+ }
468
+ function expectNonEmptyString(value, message) {
469
+ if (typeof value !== "string" || value.trim() === "") {
470
+ throw new BrokerValidationError(message);
471
+ }
472
+ return value.trim();
473
+ }
474
+ function optionalTrimmedString(value, message) {
475
+ if (value === undefined) {
476
+ return undefined;
477
+ }
478
+ return expectNonEmptyString(value, message);
479
+ }
480
+ function expectPositiveInteger(value, message) {
481
+ if (!Number.isInteger(value) || value <= 0) {
482
+ throw new BrokerValidationError(message);
483
+ }
484
+ return value;
485
+ }
486
+ function expectBoundedPositiveInteger(value, minimum, maximum, message) {
487
+ const parsed = expectPositiveInteger(value, message);
488
+ if (parsed < minimum || parsed > maximum) {
489
+ throw new BrokerValidationError(message);
490
+ }
491
+ return parsed;
492
+ }
493
+ function expectBoundedString(value, maximumLength, message) {
494
+ const parsed = expectNonEmptyString(value, message);
495
+ if (parsed.length > maximumLength) {
496
+ throw new BrokerValidationError(message);
497
+ }
498
+ return parsed;
499
+ }
500
+ function isRecord(value) {
501
+ return value !== null && typeof value === "object" && !Array.isArray(value);
502
+ }
503
+ const TOOL_DEFINITIONS = [
504
+ {
505
+ name: "get_issue",
506
+ title: "Get GitHub Issue",
507
+ description: "Return a single managed GitHub issue with structured metadata.",
508
+ inputSchema: {
509
+ type: "object",
510
+ additionalProperties: false,
511
+ required: ["repo", "issueNumber"],
512
+ properties: {
513
+ repo: { type: "string", description: 'Managed repository slug in "owner/name" form.' },
514
+ issueNumber: { type: "integer", minimum: 1 }
515
+ }
516
+ }
517
+ },
518
+ {
519
+ name: "get_issue_comments",
520
+ title: "Get GitHub Issue Comments",
521
+ description: "Return top-level comments for a managed GitHub issue.",
522
+ inputSchema: {
523
+ type: "object",
524
+ additionalProperties: false,
525
+ required: ["repo", "issueNumber"],
526
+ properties: {
527
+ repo: { type: "string" },
528
+ issueNumber: { type: "integer", minimum: 1 }
529
+ }
530
+ }
531
+ },
532
+ {
533
+ name: "get_pr",
534
+ title: "Get GitHub Pull Request",
535
+ description: "Return a single managed GitHub pull request with structured metadata.",
536
+ inputSchema: {
537
+ type: "object",
538
+ additionalProperties: false,
539
+ required: ["repo", "pullRequestNumber"],
540
+ properties: {
541
+ repo: { type: "string" },
542
+ pullRequestNumber: { type: "integer", minimum: 1 }
543
+ }
544
+ }
545
+ },
546
+ {
547
+ name: "get_pr_comments",
548
+ title: "Get Pull Request Discussion",
549
+ description: "Return top-level PR comments, reviews, and inline review comments for a managed pull request.",
550
+ inputSchema: {
551
+ type: "object",
552
+ additionalProperties: false,
553
+ required: ["repo", "pullRequestNumber"],
554
+ properties: {
555
+ repo: { type: "string" },
556
+ pullRequestNumber: { type: "integer", minimum: 1 }
557
+ }
558
+ }
559
+ },
560
+ {
561
+ name: "get_pr_checks",
562
+ title: "Get Pull Request CI Check Results",
563
+ description: "Return CI check run results for the head commit of a managed pull request. Reports status (queued/in_progress/completed) and conclusion (success/failure/etc.) for each check.",
564
+ inputSchema: {
565
+ type: "object",
566
+ additionalProperties: false,
567
+ required: ["repo", "pullRequestNumber"],
568
+ properties: {
569
+ repo: { type: "string" },
570
+ pullRequestNumber: { type: "integer", minimum: 1 }
571
+ }
572
+ }
573
+ },
574
+ {
575
+ name: "search_code",
576
+ title: "Search Managed Repository Code",
577
+ description: "Run a bounded code search in a managed repository.",
578
+ inputSchema: {
579
+ type: "object",
580
+ additionalProperties: false,
581
+ required: ["repo", "query"],
582
+ properties: {
583
+ repo: { type: "string" },
584
+ query: { type: "string" },
585
+ pathPrefix: { type: "string" },
586
+ limit: { type: "integer", minimum: 1, maximum: MAX_SEARCH_LIMIT }
587
+ }
588
+ }
589
+ },
590
+ {
591
+ name: "post_issue_comment",
592
+ title: "Post GitHub Issue Comment",
593
+ description: "Post a comment on a managed issue or pull request thread.",
594
+ inputSchema: {
595
+ type: "object",
596
+ additionalProperties: false,
597
+ required: ["repo", "issueNumber", "body"],
598
+ properties: {
599
+ repo: { type: "string" },
600
+ issueNumber: { type: "integer", minimum: 1 },
601
+ body: { type: "string", minLength: 1, maxLength: MAX_COMMENT_BODY_LENGTH }
602
+ }
603
+ }
604
+ },
605
+ {
606
+ name: "post_pr_review_comment_reply",
607
+ title: "Reply To Inline Review Comment",
608
+ description: "Post a reply directly to an inline pull request review comment in a managed repository.",
609
+ inputSchema: {
610
+ type: "object",
611
+ additionalProperties: false,
612
+ required: ["repo", "pullRequestNumber", "commentId", "body"],
613
+ properties: {
614
+ repo: { type: "string" },
615
+ pullRequestNumber: { type: "integer", minimum: 1 },
616
+ commentId: { type: "integer", minimum: 1 },
617
+ body: { type: "string", minLength: 1, maxLength: MAX_COMMENT_BODY_LENGTH }
618
+ }
619
+ }
620
+ },
621
+ {
622
+ name: "create_pr",
623
+ title: "Create Pull Request",
624
+ description: "Create a pull request in a managed repository from an existing head branch.",
625
+ inputSchema: {
626
+ type: "object",
627
+ additionalProperties: false,
628
+ required: ["repo", "headBranch", "title", "body"],
629
+ properties: {
630
+ repo: { type: "string" },
631
+ headBranch: { type: "string", minLength: 1 },
632
+ baseBranch: { type: "string", minLength: 1 },
633
+ title: { type: "string", minLength: 1 },
634
+ body: { type: "string", minLength: 1, maxLength: MAX_PR_BODY_LENGTH }
635
+ }
636
+ }
637
+ }
638
+ ];
@@ -0,0 +1,53 @@
1
+ export class ComposedBrokerService {
2
+ services;
3
+ routes = new Map();
4
+ constructor(services) {
5
+ this.services = services;
6
+ for (const service of services) {
7
+ const namespace = service.namespace;
8
+ if (namespace !== undefined && (namespace === "" || namespace.includes("."))) {
9
+ throw new Error(`Invalid namespace "${namespace}". Namespaces must be non-empty and must not contain "."`);
10
+ }
11
+ for (const tool of service.listTools()) {
12
+ if (tool.name === "") {
13
+ throw new Error("Tool local name must be non-empty");
14
+ }
15
+ const publicName = namespace ? `${namespace}.${tool.name}` : tool.name;
16
+ if (this.routes.has(publicName)) {
17
+ throw new Error(`Duplicate tool registration for public name "${publicName}"`);
18
+ }
19
+ this.routes.set(publicName, {
20
+ publicName,
21
+ serviceToolName: tool.name,
22
+ service,
23
+ tool
24
+ });
25
+ }
26
+ }
27
+ }
28
+ listTools() {
29
+ return Array.from(this.routes.values()).map((route) => ({
30
+ ...route.tool,
31
+ name: route.publicName
32
+ }));
33
+ }
34
+ async callTool(name, rawArguments) {
35
+ const route = this.routes.get(name);
36
+ if (!route) {
37
+ return {
38
+ ok: false,
39
+ tool: name,
40
+ error: {
41
+ code: "tool_not_found",
42
+ message: `Tool "${name}" not found in any registered service`
43
+ }
44
+ };
45
+ }
46
+ const result = await route.service.callTool(route.serviceToolName, rawArguments);
47
+ // Ensure the returned tool name matches the public name used for the call
48
+ return {
49
+ ...result,
50
+ tool: name
51
+ };
52
+ }
53
+ }