@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,478 @@
1
+ import { SecurityFilteredGitHubGateway } from "./security.js";
2
+ export class UnsecuredGhCliGitHubGateway {
3
+ commandRunner;
4
+ constructor(commandRunner) {
5
+ this.commandRunner = commandRunner;
6
+ }
7
+ async getRepositoryMetadata(owner, name) {
8
+ const data = await this.runGhJson([
9
+ "repo",
10
+ "view",
11
+ `${owner}/${name}`,
12
+ "--json",
13
+ "id,name,nameWithOwner,url,defaultBranchRef,owner"
14
+ ]);
15
+ const defaultBranch = data.defaultBranchRef?.name?.trim();
16
+ if (!defaultBranch) {
17
+ throw new Error(`gh did not return a default branch for ${owner}/${name}`);
18
+ }
19
+ return {
20
+ id: data.id,
21
+ owner: data.owner?.login?.trim() || owner,
22
+ name: data.name,
23
+ nameWithOwner: data.nameWithOwner,
24
+ url: data.url,
25
+ defaultBranch
26
+ };
27
+ }
28
+ async getRepositoryDefaultBranch(owner, name) {
29
+ const metadata = await this.getRepositoryMetadata(owner, name);
30
+ return metadata.defaultBranch;
31
+ }
32
+ async getIssue(owner, name, issueNumber) {
33
+ return this.runGhJson([
34
+ "issue",
35
+ "view",
36
+ String(issueNumber),
37
+ "--repo",
38
+ `${owner}/${name}`,
39
+ "--json",
40
+ "id,number,title,body,url,state,createdAt,updatedAt,author,labels"
41
+ ]);
42
+ }
43
+ async getPullRequest(owner, name, prNumber) {
44
+ const data = await this.runGhJson([
45
+ "pr",
46
+ "view",
47
+ String(prNumber),
48
+ "--repo",
49
+ `${owner}/${name}`,
50
+ "--json",
51
+ "id,number,title,body,url,state,createdAt,updatedAt,author,labels,headRefName,headRefOid,baseRefName,isDraft"
52
+ ]);
53
+ return { ...data, headSha: data.headRefOid || "" };
54
+ }
55
+ async findOpenPullRequestByHeadBranch(owner, name, headBranch) {
56
+ const prs = await this.listOpenPullRequests(owner, name);
57
+ return prs.find((pr) => pr.headRefName === headBranch) ?? null;
58
+ }
59
+ async createPullRequest(input) {
60
+ const result = await this.commandRunner.run("gh", [
61
+ "pr",
62
+ "create",
63
+ "--repo",
64
+ `${input.owner}/${input.name}`,
65
+ "--head",
66
+ input.headBranch,
67
+ "--base",
68
+ input.baseBranch,
69
+ "--title",
70
+ input.title,
71
+ "--body",
72
+ input.body
73
+ ]);
74
+ const created = await this.findOpenPullRequestByHeadBranch(input.owner, input.name, input.headBranch);
75
+ if (created) {
76
+ return created;
77
+ }
78
+ const createdUrl = extractFirstUrl(result.stdout);
79
+ if (createdUrl) {
80
+ return buildFallbackPullRequestSummary(input, createdUrl);
81
+ }
82
+ throw new Error(`gh pr create succeeded but the new pull request could not be resolved for branch ${input.headBranch}`);
83
+ }
84
+ async listOpenIssues(owner, name) {
85
+ return this.runGhJson([
86
+ "issue",
87
+ "list",
88
+ "--repo",
89
+ `${owner}/${name}`,
90
+ "--state",
91
+ "open",
92
+ "--limit",
93
+ "1000",
94
+ "--json",
95
+ "id,number,title,body,url,state,createdAt,updatedAt,author,labels"
96
+ ]);
97
+ }
98
+ async listOpenPullRequests(owner, name) {
99
+ const data = await this.runGhJson([
100
+ "pr",
101
+ "list",
102
+ "--repo",
103
+ `${owner}/${name}`,
104
+ "--state",
105
+ "open",
106
+ "--limit",
107
+ "1000",
108
+ "--json",
109
+ "id,number,title,body,url,state,createdAt,updatedAt,author,labels,headRefName,headRefOid,baseRefName,isDraft"
110
+ ]);
111
+ return data.map((pr) => ({ ...pr, headSha: pr.headRefOid || "" }));
112
+ }
113
+ async listIssueComments(owner, name, issueNumber) {
114
+ const data = await this.runGhJson([
115
+ "issue",
116
+ "view",
117
+ String(issueNumber),
118
+ "--repo",
119
+ `${owner}/${name}`,
120
+ "--json",
121
+ "comments"
122
+ ]);
123
+ return data.comments ?? [];
124
+ }
125
+ async listPullRequestComments(owner, name, prNumber) {
126
+ const data = await this.runGhJson([
127
+ "api",
128
+ "--paginate",
129
+ "--slurp",
130
+ `repos/${owner}/${name}/issues/${prNumber}/comments?per_page=100`
131
+ ]);
132
+ if (!Array.isArray(data)) {
133
+ return [];
134
+ }
135
+ const flat = (Array.isArray(data[0]) ? data.flat() : data);
136
+ return flat.map((c) => ({
137
+ id: String(c.id),
138
+ url: c.html_url,
139
+ body: c.body,
140
+ createdAt: c.created_at,
141
+ updatedAt: c.updated_at,
142
+ author: { login: c.user.login }
143
+ }));
144
+ }
145
+ async listPullRequestReviews(owner, name, prNumber) {
146
+ const data = await this.runGhJson([
147
+ "pr",
148
+ "view",
149
+ String(prNumber),
150
+ "--repo",
151
+ `${owner}/${name}`,
152
+ "--json",
153
+ "reviews"
154
+ ]);
155
+ return data.reviews ?? [];
156
+ }
157
+ async listPullRequestReviewComments(owner, name, prNumber) {
158
+ const data = await this.runGhJson([
159
+ "api",
160
+ "--paginate",
161
+ "--slurp",
162
+ `repos/${owner}/${name}/pulls/${prNumber}/comments?per_page=100`
163
+ ]);
164
+ if (!Array.isArray(data)) {
165
+ return [];
166
+ }
167
+ if (data.length === 0) {
168
+ return [];
169
+ }
170
+ if (Array.isArray(data[0])) {
171
+ return data.flat();
172
+ }
173
+ return data;
174
+ }
175
+ async listPullRequestChangedFiles(owner, name, prNumber) {
176
+ const data = await this.runGhJson([
177
+ "api",
178
+ "--paginate",
179
+ "--slurp",
180
+ `repos/${owner}/${name}/pulls/${prNumber}/files?per_page=100`
181
+ ]);
182
+ if (!Array.isArray(data)) {
183
+ return [];
184
+ }
185
+ const flat = (data.length > 0 && Array.isArray(data[0]) ? data.flat() : data);
186
+ return flat.map((file) => {
187
+ const { rightChangedLines, leftChangedLines, rightCommentableLines, leftCommentableLines } = parsePullRequestFilePatch(file.patch ?? "");
188
+ return {
189
+ path: file.filename ?? "",
190
+ rightChangedLines,
191
+ leftChangedLines,
192
+ rightCommentableLines,
193
+ leftCommentableLines
194
+ };
195
+ });
196
+ }
197
+ async listIssueEvents(owner, name, issueNumber) {
198
+ const data = await this.runGhJson([
199
+ "api",
200
+ "--paginate",
201
+ "--slurp",
202
+ `repos/${owner}/${name}/issues/${issueNumber}/events?per_page=100`
203
+ ]);
204
+ if (!Array.isArray(data)) {
205
+ return [];
206
+ }
207
+ const flat = (data.length > 0 && Array.isArray(data[0]) ? data.flat() : data);
208
+ return flat.map((item) => {
209
+ const eventObj = {
210
+ event: item.event,
211
+ createdAt: item.created_at,
212
+ actor: item.actor ? { login: item.actor.login } : null
213
+ };
214
+ if (item.label) {
215
+ eventObj.label = { name: item.label.name };
216
+ }
217
+ return eventObj;
218
+ });
219
+ }
220
+ async listPullRequestCheckRuns(owner, name, headSha) {
221
+ const data = await this.runGhJson([
222
+ "api",
223
+ "--paginate",
224
+ "--slurp",
225
+ `repos/${owner}/${name}/commits/${headSha}/check-runs?per_page=100`
226
+ ]);
227
+ if (!Array.isArray(data)) {
228
+ return [];
229
+ }
230
+ const checkRuns = data.flatMap((page) => page.check_runs ?? []);
231
+ return checkRuns.map((run) => ({
232
+ id: run.id,
233
+ name: run.name,
234
+ status: run.status,
235
+ conclusion: run.conclusion,
236
+ url: run.html_url,
237
+ startedAt: run.started_at,
238
+ completedAt: run.completed_at
239
+ }));
240
+ }
241
+ async searchCode(input) {
242
+ const searchTerms = [`repo:${input.owner}/${input.name}`, input.query.trim()];
243
+ if (input.pathPrefix) {
244
+ searchTerms.push(`path:${input.pathPrefix}`);
245
+ }
246
+ const search = new URLSearchParams({
247
+ q: searchTerms.join(" "),
248
+ per_page: String(input.limit)
249
+ });
250
+ const data = await this.runGhJson(["api", `search/code?${search.toString()}`]);
251
+ return {
252
+ totalCount: data.total_count ?? 0,
253
+ incompleteResults: data.incomplete_results ?? false,
254
+ items: (data.items ?? []).map((item) => ({
255
+ name: item.name ?? "",
256
+ path: item.path ?? "",
257
+ sha: item.sha ?? "",
258
+ url: item.url ?? "",
259
+ gitUrl: item.git_url ?? "",
260
+ htmlUrl: item.html_url ?? "",
261
+ repository: {
262
+ id: item.repository?.id ?? 0,
263
+ fullName: item.repository?.full_name ?? `${input.owner}/${input.name}`,
264
+ htmlUrl: item.repository?.html_url ?? `https://github.com/${input.owner}/${input.name}`
265
+ }
266
+ }))
267
+ };
268
+ }
269
+ async postIssueComment(input) {
270
+ const data = await this.runGhJson([
271
+ "api",
272
+ `repos/${input.owner}/${input.name}/issues/${input.issueNumber}/comments`,
273
+ "--method",
274
+ "POST",
275
+ "--field",
276
+ `body=${input.body}`
277
+ ]);
278
+ return {
279
+ id: data.id ?? 0,
280
+ url: data.html_url ?? "",
281
+ body: data.body ?? input.body,
282
+ createdAt: data.created_at ?? "",
283
+ updatedAt: data.updated_at ?? "",
284
+ author: data.user?.login ? { login: data.user.login } : null
285
+ };
286
+ }
287
+ async updateIssueComment(input) {
288
+ const data = await this.runGhJson([
289
+ "api",
290
+ `repos/${input.owner}/${input.name}/issues/comments/${input.commentId}`,
291
+ "--method",
292
+ "PATCH",
293
+ "--field",
294
+ `body=${input.body}`
295
+ ]);
296
+ return {
297
+ id: data.id ?? 0,
298
+ url: data.html_url ?? "",
299
+ body: data.body ?? input.body,
300
+ createdAt: data.created_at ?? "",
301
+ updatedAt: data.updated_at ?? "",
302
+ author: data.user?.login ? { login: data.user.login } : null
303
+ };
304
+ }
305
+ async addReaction(input) {
306
+ const slug = `${input.owner}/${input.name}`;
307
+ if (input.commentId !== undefined) {
308
+ const commentPath = input.commentKind === "pull_request_review_comment"
309
+ ? `/repos/${slug}/pulls/comments/${input.commentId}/reactions`
310
+ : `/repos/${slug}/issues/comments/${input.commentId}/reactions`;
311
+ await this.commandRunner.run("gh", [
312
+ "api",
313
+ "-X", "POST",
314
+ commentPath,
315
+ "-f", `content=${input.content}`
316
+ ]);
317
+ }
318
+ else if (input.issueNumber !== undefined) {
319
+ await this.commandRunner.run("gh", [
320
+ "api",
321
+ "-X", "POST",
322
+ `/repos/${slug}/issues/${input.issueNumber}/reactions`,
323
+ "-f", `content=${input.content}`
324
+ ]);
325
+ }
326
+ else {
327
+ throw new Error(`addReaction requires either issueNumber or commentId`);
328
+ }
329
+ }
330
+ async postPullRequestReviewCommentReply(input) {
331
+ const data = await this.runGhJson([
332
+ "api",
333
+ `repos/${input.owner}/${input.name}/pulls/${input.pullRequestNumber}/comments/${input.commentId}/replies`,
334
+ "--method",
335
+ "POST",
336
+ "--field",
337
+ `body=${input.body}`
338
+ ]);
339
+ return {
340
+ id: data.id ?? 0,
341
+ url: data.html_url ?? "",
342
+ body: data.body ?? input.body,
343
+ createdAt: data.created_at ?? "",
344
+ updatedAt: data.updated_at ?? "",
345
+ author: data.user?.login ? { login: data.user.login } : null
346
+ };
347
+ }
348
+ async submitPullRequestReview(input) {
349
+ const payload = {
350
+ event: input.event,
351
+ ...(input.body ? { body: input.body } : {}),
352
+ ...(input.commitId ? { commit_id: input.commitId } : {}),
353
+ ...(input.comments?.length
354
+ ? {
355
+ comments: input.comments.map((comment) => ({
356
+ path: comment.path,
357
+ line: comment.line,
358
+ side: comment.side ?? "RIGHT",
359
+ ...(comment.startLine ? { start_line: comment.startLine } : {}),
360
+ ...(comment.startSide ? { start_side: comment.startSide } : {}),
361
+ body: comment.body
362
+ }))
363
+ }
364
+ : {})
365
+ };
366
+ const data = await this.runGhJson([
367
+ "api",
368
+ `repos/${input.owner}/${input.name}/pulls/${input.pullRequestNumber}/reviews`,
369
+ "--method",
370
+ "POST",
371
+ "--input",
372
+ "-"
373
+ ], JSON.stringify(payload));
374
+ return {
375
+ id: data.id ?? 0,
376
+ url: data.html_url ?? "",
377
+ body: data.body ?? input.body ?? "",
378
+ state: data.state ?? input.event,
379
+ submittedAt: data.submitted_at ?? "",
380
+ author: data.user?.login ? { login: data.user.login } : null
381
+ };
382
+ }
383
+ async runGhJson(args, input) {
384
+ const result = await this.commandRunner.run("gh", args, input === undefined ? undefined : { input });
385
+ try {
386
+ return JSON.parse(result.stdout);
387
+ }
388
+ catch (error) {
389
+ const message = error instanceof Error ? error.message : String(error);
390
+ throw new Error(`gh ${args.join(" ")}: invalid JSON (${message})`);
391
+ }
392
+ }
393
+ }
394
+ function parsePullRequestFilePatch(patch) {
395
+ const rightChangedLines = [];
396
+ const leftChangedLines = [];
397
+ const rightCommentableLines = [];
398
+ const leftCommentableLines = [];
399
+ if (!patch) {
400
+ return {
401
+ rightChangedLines,
402
+ leftChangedLines,
403
+ rightCommentableLines,
404
+ leftCommentableLines
405
+ };
406
+ }
407
+ const lines = patch.split(/\r?\n/);
408
+ let currentOldLine = 0;
409
+ let currentNewLine = 0;
410
+ let inHunk = false;
411
+ for (const line of lines) {
412
+ if (line.startsWith("@@ ")) {
413
+ const headerMatch = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
414
+ if (headerMatch && headerMatch[1] && headerMatch[2]) {
415
+ currentOldLine = parseInt(headerMatch[1], 10);
416
+ currentNewLine = parseInt(headerMatch[2], 10);
417
+ inHunk = true;
418
+ }
419
+ continue;
420
+ }
421
+ if (!inHunk) {
422
+ continue;
423
+ }
424
+ if (line.startsWith("+")) {
425
+ rightChangedLines.push(currentNewLine);
426
+ rightCommentableLines.push(currentNewLine);
427
+ currentNewLine++;
428
+ }
429
+ else if (line.startsWith("-")) {
430
+ leftChangedLines.push(currentOldLine);
431
+ leftCommentableLines.push(currentOldLine);
432
+ currentOldLine++;
433
+ }
434
+ else if (line.startsWith(" ")) {
435
+ leftCommentableLines.push(currentOldLine);
436
+ rightCommentableLines.push(currentNewLine);
437
+ currentOldLine++;
438
+ currentNewLine++;
439
+ }
440
+ else if (line.startsWith("\\")) {
441
+ // No newline at end of file, ignore
442
+ }
443
+ }
444
+ return {
445
+ rightChangedLines,
446
+ leftChangedLines,
447
+ rightCommentableLines,
448
+ leftCommentableLines
449
+ };
450
+ }
451
+ function extractFirstUrl(value) {
452
+ const match = value.match(/https:\/\/github\.com\/[^\s]+/);
453
+ return match?.[0];
454
+ }
455
+ function buildFallbackPullRequestSummary(input, url) {
456
+ const numberMatch = url.match(/\/pull\/(\d+)(?:\/?|$)/);
457
+ return {
458
+ id: `pull_request:${input.owner}/${input.name}:${numberMatch?.[1] ?? "unknown"}`,
459
+ number: numberMatch?.[1] ? Number(numberMatch[1]) : 0,
460
+ title: input.title,
461
+ body: input.body,
462
+ url,
463
+ state: "OPEN",
464
+ createdAt: "",
465
+ updatedAt: "",
466
+ author: null,
467
+ labels: [],
468
+ headRefName: input.headBranch,
469
+ headSha: "",
470
+ baseRefName: input.baseBranch,
471
+ isDraft: false
472
+ };
473
+ }
474
+ export class GhCliGitHubGateway extends SecurityFilteredGitHubGateway {
475
+ constructor(commandRunner, allowedUsers) {
476
+ super(new UnsecuredGhCliGitHubGateway(commandRunner), allowedUsers);
477
+ }
478
+ }
@@ -0,0 +1,222 @@
1
+ import { classifyPullRequestBase } from "./pr-classification.js";
2
+ export function normalizeIssue(issue, comments, context) {
3
+ const id = createEntityId("issue", context.repoMetadata.nameWithOwner, issue.number);
4
+ const previous = context.previousIssues.get(id);
5
+ return {
6
+ kind: "issue",
7
+ id,
8
+ repoId: context.repo.id,
9
+ repoKey: context.repoMetadata.nameWithOwner,
10
+ repoOwner: context.repoMetadata.owner,
11
+ repoName: context.repoMetadata.name,
12
+ repoSlug: context.repoMetadata.nameWithOwner,
13
+ number: issue.number,
14
+ key: createEntityKey("issue", context.repoMetadata.nameWithOwner, issue.number),
15
+ sourceId: String(issue.id),
16
+ url: issue.url,
17
+ title: issue.title,
18
+ body: issue.body ?? "",
19
+ labels: normalizeLabels(issue.labels),
20
+ author: normalizeActor(issue.author?.login),
21
+ createdAt: issue.createdAt,
22
+ updatedAt: issue.updatedAt,
23
+ ...withOptionalTimestamps(findLastCommentAt(comments), context.seenAt, previous?.lastHandledAt),
24
+ state: normalizeIssueState(issue.state)
25
+ };
26
+ }
27
+ export function normalizePullRequest(pr, comments, context) {
28
+ const id = createEntityId("pull_request", context.repoMetadata.nameWithOwner, pr.number);
29
+ const previous = context.previousPullRequests.get(id);
30
+ return {
31
+ kind: "pull_request",
32
+ id,
33
+ repoId: context.repo.id,
34
+ repoKey: context.repoMetadata.nameWithOwner,
35
+ repoOwner: context.repoMetadata.owner,
36
+ repoName: context.repoMetadata.name,
37
+ repoSlug: context.repoMetadata.nameWithOwner,
38
+ number: pr.number,
39
+ key: createEntityKey("pull_request", context.repoMetadata.nameWithOwner, pr.number),
40
+ sourceId: String(pr.id),
41
+ url: pr.url,
42
+ title: pr.title,
43
+ body: pr.body ?? "",
44
+ labels: normalizeLabels(pr.labels),
45
+ author: normalizeActor(pr.author?.login),
46
+ createdAt: pr.createdAt,
47
+ updatedAt: pr.updatedAt,
48
+ ...withOptionalTimestamps(findLastCommentAt(comments), context.seenAt, previous?.lastHandledAt),
49
+ state: normalizePullRequestState(pr.state),
50
+ headBranch: pr.headRefName,
51
+ headSha: pr.headSha ?? "",
52
+ baseBranch: pr.baseRefName,
53
+ defaultBranch: context.repoMetadata.defaultBranch,
54
+ baseClassification: classifyPullRequestBase({
55
+ pullRequestNumber: pr.number,
56
+ repoSlug: context.repoMetadata.nameWithOwner,
57
+ baseBranch: pr.baseRefName,
58
+ defaultBranch: context.repoMetadata.defaultBranch
59
+ }).kind,
60
+ isDraft: pr.isDraft
61
+ };
62
+ }
63
+ export function normalizeIssueComments(comments, parentNumber, parentType, commentKind, context) {
64
+ return comments.map((comment) => normalizeComment({
65
+ sourceId: String(comment.id),
66
+ url: comment.url,
67
+ body: comment.body ?? "",
68
+ createdAt: comment.createdAt,
69
+ updatedAt: comment.updatedAt,
70
+ author: normalizeActor(comment.author?.login),
71
+ parentNumber,
72
+ parentType,
73
+ commentKind,
74
+ context,
75
+ reactionTargetId: String(comment.id)
76
+ }));
77
+ }
78
+ export function normalizePullRequestReviews(reviews, parentNumber, context) {
79
+ return reviews
80
+ .filter((review) => (review.body ?? "").trim() !== "")
81
+ .map((review) => {
82
+ const createdAt = review.submittedAt ?? review.updatedAt;
83
+ const updatedAt = review.updatedAt ?? review.submittedAt;
84
+ if (!createdAt || !updatedAt) {
85
+ throw new Error(`PR review ${review.id} in ${context.repoMetadata.nameWithOwner} is missing timestamps`);
86
+ }
87
+ return normalizeComment({
88
+ sourceId: String(review.id),
89
+ url: review.url ?? "",
90
+ body: review.body ?? "",
91
+ createdAt,
92
+ updatedAt,
93
+ author: normalizeActor(review.author?.login),
94
+ parentNumber,
95
+ parentType: "pull_request",
96
+ commentKind: "pull_request_review",
97
+ context,
98
+ reactionTargetId: String(review.id)
99
+ });
100
+ });
101
+ }
102
+ export function normalizePullRequestReviewComments(comments, parentNumber, context) {
103
+ return comments.map((comment) => normalizeComment({
104
+ sourceId: String(comment.id),
105
+ url: comment.html_url ?? "",
106
+ body: comment.body ?? "",
107
+ createdAt: comment.created_at,
108
+ updatedAt: comment.updated_at,
109
+ author: normalizeActor(comment.user?.login),
110
+ parentNumber,
111
+ parentType: "pull_request",
112
+ commentKind: "pull_request_review_comment",
113
+ context,
114
+ reactionTargetId: String(comment.id),
115
+ pullRequestReviewLocation: buildPullRequestReviewLocation(comment)
116
+ }));
117
+ }
118
+ export function buildPullRequestReviewLocation(comment) {
119
+ const location = {
120
+ path: comment.path ?? undefined,
121
+ line: comment.line ?? undefined,
122
+ side: comment.side ?? undefined,
123
+ startLine: comment.start_line ?? undefined,
124
+ startSide: comment.start_side ?? undefined,
125
+ originalLine: comment.original_line ?? undefined,
126
+ originalStartLine: comment.original_start_line ?? undefined,
127
+ commitId: comment.commit_id ?? undefined,
128
+ originalCommitId: comment.original_commit_id ?? undefined,
129
+ diffHunk: comment.diff_hunk ?? undefined,
130
+ inReplyToId: comment.in_reply_to_id ?? undefined
131
+ };
132
+ function hasMeaningfulLocationValue(value) {
133
+ if (typeof value === "string") {
134
+ return value.trim().length > 0;
135
+ }
136
+ if (typeof value === "number") {
137
+ return true;
138
+ }
139
+ return false;
140
+ }
141
+ const hasMeaningfulField = [
142
+ comment.path,
143
+ comment.line,
144
+ comment.start_line,
145
+ comment.original_line,
146
+ comment.original_start_line,
147
+ comment.diff_hunk,
148
+ comment.commit_id,
149
+ comment.original_commit_id,
150
+ comment.in_reply_to_id,
151
+ comment.side,
152
+ comment.start_side
153
+ ].some(hasMeaningfulLocationValue);
154
+ return hasMeaningfulField ? location : undefined;
155
+ }
156
+ export function createEntityId(kind, repoSlug, number) {
157
+ return `${kind}:${repoSlug}:${number}`;
158
+ }
159
+ export function createEntityKey(kind, repoSlug, number) {
160
+ return `${repoSlug}:${kind}:${number}`;
161
+ }
162
+ export function createCommentId(repoSlug, commentKind, sourceId) {
163
+ return `comment:${repoSlug}:${commentKind}:${sourceId}`;
164
+ }
165
+ export function normalizeComment(input) {
166
+ const id = createCommentId(input.context.repoMetadata.nameWithOwner, input.commentKind, input.sourceId);
167
+ const previous = input.context.previousComments.get(id);
168
+ return {
169
+ ...previous,
170
+ kind: "comment",
171
+ commentKind: input.commentKind,
172
+ id,
173
+ sourceId: input.sourceId,
174
+ repoId: input.context.repo.id,
175
+ repoKey: input.context.repoMetadata.nameWithOwner,
176
+ repoOwner: input.context.repoMetadata.owner,
177
+ repoName: input.context.repoMetadata.name,
178
+ repoSlug: input.context.repoMetadata.nameWithOwner,
179
+ parentType: input.parentType,
180
+ parentNumber: input.parentNumber,
181
+ parentKey: createEntityKey(input.parentType === "issue" ? "issue" : "pull_request", input.context.repoMetadata.nameWithOwner, input.parentNumber),
182
+ url: input.url,
183
+ body: input.body,
184
+ author: input.author,
185
+ createdAt: input.createdAt,
186
+ updatedAt: input.updatedAt,
187
+ reactionTargetId: input.reactionTargetId,
188
+ lastSeenAt: input.context.seenAt,
189
+ ...(previous?.lastHandledAt ? { lastHandledAt: previous.lastHandledAt } : {}),
190
+ ...(input.pullRequestReviewLocation ? { pullRequestReviewLocation: input.pullRequestReviewLocation } : {}),
191
+ };
192
+ }
193
+ function normalizeActor(login) {
194
+ return login?.trim() || "unknown";
195
+ }
196
+ function normalizeLabels(labels) {
197
+ return (labels ?? []).map((label) => label.name).sort((left, right) => left.localeCompare(right));
198
+ }
199
+ function normalizeIssueState(state) {
200
+ return state === "OPEN" ? "open" : "closed";
201
+ }
202
+ function normalizePullRequestState(state) {
203
+ if (state === "MERGED") {
204
+ return "merged";
205
+ }
206
+ return state === "OPEN" ? "open" : "closed";
207
+ }
208
+ function findLastCommentAt(comments) {
209
+ return comments.reduce((latest, comment) => {
210
+ if (!latest) {
211
+ return comment.updatedAt;
212
+ }
213
+ return comment.updatedAt > latest ? comment.updatedAt : latest;
214
+ }, undefined);
215
+ }
216
+ function withOptionalTimestamps(lastCommentAt, lastSeenAt, lastHandledAt) {
217
+ return {
218
+ ...(lastCommentAt ? { lastCommentAt } : {}),
219
+ ...(lastSeenAt ? { lastSeenAt } : {}),
220
+ ...(lastHandledAt ? { lastHandledAt } : {})
221
+ };
222
+ }