co-maintainer 0.4.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/dist/main.d.ts +2 -0
  4. package/dist/main.js +9 -0
  5. package/dist/mod.d.ts +4 -0
  6. package/dist/mod.js +2 -0
  7. package/dist/package.json +54 -0
  8. package/dist/src/ai/batch.d.ts +16 -0
  9. package/dist/src/ai/batch.js +117 -0
  10. package/dist/src/ai/fake.d.ts +7 -0
  11. package/dist/src/ai/fake.js +40 -0
  12. package/dist/src/ai/hetzner.d.ts +16 -0
  13. package/dist/src/ai/hetzner.js +69 -0
  14. package/dist/src/ai/mermaid.d.ts +27 -0
  15. package/dist/src/ai/mermaid.js +457 -0
  16. package/dist/src/ai/mermaid_loop.d.ts +12 -0
  17. package/dist/src/ai/mermaid_loop.js +100 -0
  18. package/dist/src/ai/openrouter.d.ts +9 -0
  19. package/dist/src/ai/openrouter.js +29 -0
  20. package/dist/src/ai/provider.d.ts +4 -0
  21. package/dist/src/ai/provider.js +63 -0
  22. package/dist/src/cli/args.d.ts +5 -0
  23. package/dist/src/cli/args.js +252 -0
  24. package/dist/src/cli/commands/probe.d.ts +2 -0
  25. package/dist/src/cli/commands/probe.js +122 -0
  26. package/dist/src/cli/commands/review.d.ts +3 -0
  27. package/dist/src/cli/commands/review.js +84 -0
  28. package/dist/src/cli/commands/serve.d.ts +18 -0
  29. package/dist/src/cli/commands/serve.js +183 -0
  30. package/dist/src/cli/commands/set.d.ts +7 -0
  31. package/dist/src/cli/commands/set.js +171 -0
  32. package/dist/src/cli/main.d.ts +1 -0
  33. package/dist/src/cli/main.js +27 -0
  34. package/dist/src/cli/prompt.d.ts +4 -0
  35. package/dist/src/cli/prompt.js +26 -0
  36. package/dist/src/cli/review_args.d.ts +28 -0
  37. package/dist/src/cli/review_args.js +84 -0
  38. package/dist/src/cli/review_output.d.ts +2 -0
  39. package/dist/src/cli/review_output.js +10 -0
  40. package/dist/src/cli/review_result.d.ts +81 -0
  41. package/dist/src/cli/review_result.js +281 -0
  42. package/dist/src/config.d.ts +108 -0
  43. package/dist/src/config.js +138 -0
  44. package/dist/src/git/default_branch.d.ts +5 -0
  45. package/dist/src/git/default_branch.js +22 -0
  46. package/dist/src/github/app.d.ts +43 -0
  47. package/dist/src/github/app.js +120 -0
  48. package/dist/src/github/client.d.ts +17 -0
  49. package/dist/src/github/client.js +66 -0
  50. package/dist/src/github/collect.d.ts +7 -0
  51. package/dist/src/github/collect.js +368 -0
  52. package/dist/src/github/gh.d.ts +8 -0
  53. package/dist/src/github/gh.js +63 -0
  54. package/dist/src/github/jwt.d.ts +12 -0
  55. package/dist/src/github/jwt.js +84 -0
  56. package/dist/src/github/oauth.d.ts +9 -0
  57. package/dist/src/github/oauth.js +51 -0
  58. package/dist/src/github/pat.d.ts +10 -0
  59. package/dist/src/github/pat.js +60 -0
  60. package/dist/src/knowledge/facts.d.ts +3 -0
  61. package/dist/src/knowledge/facts.js +325 -0
  62. package/dist/src/knowledge/guide.d.ts +6 -0
  63. package/dist/src/knowledge/guide.js +82 -0
  64. package/dist/src/knowledge/probe.d.ts +12 -0
  65. package/dist/src/knowledge/probe.js +144 -0
  66. package/dist/src/knowledge/sections.d.ts +2 -0
  67. package/dist/src/knowledge/sections.js +13 -0
  68. package/dist/src/knowledge/skill.d.ts +10 -0
  69. package/dist/src/knowledge/skill.js +106 -0
  70. package/dist/src/knowledge/synthesis.d.ts +7 -0
  71. package/dist/src/knowledge/synthesis.js +315 -0
  72. package/dist/src/knowledge/types.d.ts +49 -0
  73. package/dist/src/knowledge/types.js +1 -0
  74. package/dist/src/knowledge/validate.d.ts +6 -0
  75. package/dist/src/knowledge/validate.js +134 -0
  76. package/dist/src/local/carry_over_store.d.ts +18 -0
  77. package/dist/src/local/carry_over_store.js +40 -0
  78. package/dist/src/local/codegraph_exclude.d.ts +3 -0
  79. package/dist/src/local/codegraph_exclude.js +26 -0
  80. package/dist/src/local/codegraph_prepare.d.ts +17 -0
  81. package/dist/src/local/codegraph_prepare.js +51 -0
  82. package/dist/src/local/git_ops.d.ts +12 -0
  83. package/dist/src/local/git_ops.js +98 -0
  84. package/dist/src/local/git_parse.d.ts +27 -0
  85. package/dist/src/local/git_parse.js +131 -0
  86. package/dist/src/local/git_revision.d.ts +15 -0
  87. package/dist/src/local/git_revision.js +141 -0
  88. package/dist/src/local/git_untracked.d.ts +21 -0
  89. package/dist/src/local/git_untracked.js +79 -0
  90. package/dist/src/local/review_local.d.ts +4 -0
  91. package/dist/src/local/review_local.js +273 -0
  92. package/dist/src/local/review_lock.d.ts +7 -0
  93. package/dist/src/local/review_lock.js +45 -0
  94. package/dist/src/pr/checkout.d.ts +25 -0
  95. package/dist/src/pr/checkout.js +136 -0
  96. package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
  97. package/dist/src/pr/codegraph_tool_args.js +34 -0
  98. package/dist/src/pr/codegraph_tools.d.ts +15 -0
  99. package/dist/src/pr/codegraph_tools.js +408 -0
  100. package/dist/src/pr/diff_summary.d.ts +6 -0
  101. package/dist/src/pr/diff_summary.js +52 -0
  102. package/dist/src/pr/findings.d.ts +14 -0
  103. package/dist/src/pr/findings.js +94 -0
  104. package/dist/src/pr/github_revision.d.ts +4 -0
  105. package/dist/src/pr/github_revision.js +39 -0
  106. package/dist/src/pr/hunks.d.ts +18 -0
  107. package/dist/src/pr/hunks.js +73 -0
  108. package/dist/src/pr/reviewer.d.ts +40 -0
  109. package/dist/src/pr/reviewer.js +582 -0
  110. package/dist/src/pr/rounds.d.ts +17 -0
  111. package/dist/src/pr/rounds.js +43 -0
  112. package/dist/src/pr/scope.d.ts +35 -0
  113. package/dist/src/pr/scope.js +84 -0
  114. package/dist/src/pr/snapshot.d.ts +11 -0
  115. package/dist/src/pr/snapshot.js +1 -0
  116. package/dist/src/pr/suggestion.d.ts +17 -0
  117. package/dist/src/pr/suggestion.js +70 -0
  118. package/dist/src/remote/client.d.ts +4 -0
  119. package/dist/src/remote/client.js +199 -0
  120. package/dist/src/remote/client_tools.d.ts +13 -0
  121. package/dist/src/remote/client_tools.js +30 -0
  122. package/dist/src/remote/fixtures.d.ts +1 -0
  123. package/dist/src/remote/fixtures.js +30 -0
  124. package/dist/src/remote/revision_from_submit.d.ts +2 -0
  125. package/dist/src/remote/revision_from_submit.js +26 -0
  126. package/dist/src/remote/schema.d.ts +14 -0
  127. package/dist/src/remote/schema.js +30 -0
  128. package/dist/src/remote/server/body.d.ts +18 -0
  129. package/dist/src/remote/server/body.js +73 -0
  130. package/dist/src/remote/server/routes.d.ts +1 -0
  131. package/dist/src/remote/server/routes.js +150 -0
  132. package/dist/src/remote/server/sessions.d.ts +16 -0
  133. package/dist/src/remote/server/sessions.js +48 -0
  134. package/dist/src/remote/server/sync.d.ts +3 -0
  135. package/dist/src/remote/server/sync.js +90 -0
  136. package/dist/src/remote/tool_bridge.d.ts +15 -0
  137. package/dist/src/remote/tool_bridge.js +84 -0
  138. package/dist/src/remote/validate.d.ts +8 -0
  139. package/dist/src/remote/validate.js +248 -0
  140. package/dist/src/review/carry_over.d.ts +63 -0
  141. package/dist/src/review/carry_over.js +372 -0
  142. package/dist/src/review/engine.d.ts +13 -0
  143. package/dist/src/review/engine.js +4 -0
  144. package/dist/src/review/guides.d.ts +12 -0
  145. package/dist/src/review/guides.js +44 -0
  146. package/dist/src/review/revision.d.ts +26 -0
  147. package/dist/src/review/revision.js +57 -0
  148. package/dist/src/server/api/activity.d.ts +1 -0
  149. package/dist/src/server/api/activity.js +12 -0
  150. package/dist/src/server/api/analytics.d.ts +1 -0
  151. package/dist/src/server/api/analytics.js +9 -0
  152. package/dist/src/server/api/installations.d.ts +4 -0
  153. package/dist/src/server/api/installations.js +24 -0
  154. package/dist/src/server/api/jobs.d.ts +1 -0
  155. package/dist/src/server/api/jobs.js +86 -0
  156. package/dist/src/server/api/json_body.d.ts +1 -0
  157. package/dist/src/server/api/json_body.js +14 -0
  158. package/dist/src/server/api/remote_tokens.d.ts +1 -0
  159. package/dist/src/server/api/remote_tokens.js +71 -0
  160. package/dist/src/server/api/repos.d.ts +4 -0
  161. package/dist/src/server/api/repos.js +136 -0
  162. package/dist/src/server/api/settings.d.ts +1 -0
  163. package/dist/src/server/api/settings.js +210 -0
  164. package/dist/src/server/app.d.ts +27 -0
  165. package/dist/src/server/app.js +147 -0
  166. package/dist/src/server/assets.d.ts +3 -0
  167. package/dist/src/server/assets.js +21 -0
  168. package/dist/src/server/auth.d.ts +44 -0
  169. package/dist/src/server/auth.js +188 -0
  170. package/dist/src/server/errors.d.ts +8 -0
  171. package/dist/src/server/errors.js +10 -0
  172. package/dist/src/server/http.d.ts +8 -0
  173. package/dist/src/server/http.js +117 -0
  174. package/dist/src/server/logo.d.ts +1 -0
  175. package/dist/src/server/logo.js +2 -0
  176. package/dist/src/server/pages/activity.d.ts +4 -0
  177. package/dist/src/server/pages/activity.js +200 -0
  178. package/dist/src/server/pages/add_repo.d.ts +9 -0
  179. package/dist/src/server/pages/add_repo.js +56 -0
  180. package/dist/src/server/pages/analytics.d.ts +2 -0
  181. package/dist/src/server/pages/analytics.js +193 -0
  182. package/dist/src/server/pages/client.d.ts +2 -0
  183. package/dist/src/server/pages/client.js +2 -0
  184. package/dist/src/server/pages/home.d.ts +16 -0
  185. package/dist/src/server/pages/home.js +87 -0
  186. package/dist/src/server/pages/knowledge.d.ts +2 -0
  187. package/dist/src/server/pages/knowledge.js +64 -0
  188. package/dist/src/server/pages/layout.d.ts +32 -0
  189. package/dist/src/server/pages/layout.js +230 -0
  190. package/dist/src/server/pages/pr.d.ts +2 -0
  191. package/dist/src/server/pages/pr.js +109 -0
  192. package/dist/src/server/pages/repo.d.ts +2 -0
  193. package/dist/src/server/pages/repo.js +92 -0
  194. package/dist/src/server/pages/repo_prs.d.ts +2 -0
  195. package/dist/src/server/pages/repo_prs.js +75 -0
  196. package/dist/src/server/pages/repo_remote.d.ts +2 -0
  197. package/dist/src/server/pages/repo_remote.js +38 -0
  198. package/dist/src/server/pages/repo_settings.d.ts +3 -0
  199. package/dist/src/server/pages/repo_settings.js +127 -0
  200. package/dist/src/server/pages/router.d.ts +17 -0
  201. package/dist/src/server/pages/router.js +312 -0
  202. package/dist/src/server/pages/settings.d.ts +2 -0
  203. package/dist/src/server/pages/settings.js +337 -0
  204. package/dist/src/server/pages/setup.d.ts +5 -0
  205. package/dist/src/server/pages/setup.js +38 -0
  206. package/dist/src/server/pages/styles.d.ts +2 -0
  207. package/dist/src/server/pages/styles.js +2 -0
  208. package/dist/src/server/webhook/index.d.ts +1 -0
  209. package/dist/src/server/webhook/index.js +64 -0
  210. package/dist/src/server/webhook/signature.d.ts +7 -0
  211. package/dist/src/server/webhook/signature.js +41 -0
  212. package/dist/src/services/credentials.d.ts +19 -0
  213. package/dist/src/services/credentials.js +146 -0
  214. package/dist/src/services/dashboard.d.ts +165 -0
  215. package/dist/src/services/dashboard.js +200 -0
  216. package/dist/src/services/drift.d.ts +13 -0
  217. package/dist/src/services/drift.js +76 -0
  218. package/dist/src/services/jobs.d.ts +34 -0
  219. package/dist/src/services/jobs.js +222 -0
  220. package/dist/src/services/remote_review.d.ts +10 -0
  221. package/dist/src/services/remote_review.js +274 -0
  222. package/dist/src/services/remote_review_abort.d.ts +3 -0
  223. package/dist/src/services/remote_review_abort.js +11 -0
  224. package/dist/src/services/remote_token_jobs.d.ts +1 -0
  225. package/dist/src/services/remote_token_jobs.js +20 -0
  226. package/dist/src/services/remote_tokens.d.ts +13 -0
  227. package/dist/src/services/remote_tokens.js +57 -0
  228. package/dist/src/services/replies.d.ts +7 -0
  229. package/dist/src/services/replies.js +248 -0
  230. package/dist/src/services/review.d.ts +35 -0
  231. package/dist/src/services/review.js +671 -0
  232. package/dist/src/services/setup.d.ts +29 -0
  233. package/dist/src/services/setup.js +364 -0
  234. package/dist/src/services/webhook.d.ts +22 -0
  235. package/dist/src/services/webhook.js +401 -0
  236. package/dist/src/store/app_db.d.ts +10 -0
  237. package/dist/src/store/app_db.js +85 -0
  238. package/dist/src/store/cache_db.d.ts +4 -0
  239. package/dist/src/store/cache_db.js +63 -0
  240. package/dist/src/store/deliveries.d.ts +12 -0
  241. package/dist/src/store/deliveries.js +23 -0
  242. package/dist/src/store/drift.d.ts +4 -0
  243. package/dist/src/store/drift.js +22 -0
  244. package/dist/src/store/findings.d.ts +31 -0
  245. package/dist/src/store/findings.js +64 -0
  246. package/dist/src/store/installations.d.ts +9 -0
  247. package/dist/src/store/installations.js +35 -0
  248. package/dist/src/store/job_logs.d.ts +5 -0
  249. package/dist/src/store/job_logs.js +17 -0
  250. package/dist/src/store/jobs.d.ts +27 -0
  251. package/dist/src/store/jobs.js +86 -0
  252. package/dist/src/store/migrations.d.ts +5 -0
  253. package/dist/src/store/migrations.js +258 -0
  254. package/dist/src/store/remote_review_inputs.d.ts +11 -0
  255. package/dist/src/store/remote_review_inputs.js +25 -0
  256. package/dist/src/store/remote_tokens.d.ts +10 -0
  257. package/dist/src/store/remote_tokens.js +45 -0
  258. package/dist/src/store/replies.d.ts +24 -0
  259. package/dist/src/store/replies.js +79 -0
  260. package/dist/src/store/repos.d.ts +11 -0
  261. package/dist/src/store/repos.js +59 -0
  262. package/dist/src/store/reviews.d.ts +86 -0
  263. package/dist/src/store/reviews.js +220 -0
  264. package/dist/src/store/rows.d.ts +179 -0
  265. package/dist/src/store/rows.js +4 -0
  266. package/dist/src/store/sessions.d.ts +8 -0
  267. package/dist/src/store/sessions.js +34 -0
  268. package/dist/src/store/skill_state.d.ts +3 -0
  269. package/dist/src/store/skill_state.js +24 -0
  270. package/dist/src/store/sqlite.d.ts +39 -0
  271. package/dist/src/store/sqlite.js +77 -0
  272. package/dist/src/store/subjects.d.ts +15 -0
  273. package/dist/src/store/subjects.js +78 -0
  274. package/dist/src/tools/codegraph.d.ts +58 -0
  275. package/dist/src/tools/codegraph.js +165 -0
  276. package/dist/src/tools/codegraph_exec.d.ts +13 -0
  277. package/dist/src/tools/codegraph_exec.js +66 -0
  278. package/dist/src/types.d.ts +89 -0
  279. package/dist/src/types.js +1 -0
  280. package/dist/src/util/atomic.d.ts +4 -0
  281. package/dist/src/util/atomic.js +78 -0
  282. package/dist/src/util/keyed_lock.d.ts +3 -0
  283. package/dist/src/util/keyed_lock.js +33 -0
  284. package/dist/src/util/log.d.ts +11 -0
  285. package/dist/src/util/log.js +42 -0
  286. package/dist/src/util/redact.d.ts +5 -0
  287. package/dist/src/util/redact.js +30 -0
  288. package/dist/src/util/runtime.d.ts +90 -0
  289. package/dist/src/util/runtime.js +206 -0
  290. package/dist/src/util/time.d.ts +9 -0
  291. package/dist/src/util/time.js +36 -0
  292. package/dist/src/version.d.ts +1 -0
  293. package/dist/src/version.js +5 -0
  294. package/package.json +54 -0
@@ -0,0 +1,281 @@
1
+ import { parseFindings } from "../pr/findings.js";
2
+ import { readSuggestion, stripSuggestion } from "../pr/suggestion.js";
3
+ import { anchorTextFromPatch, } from "../review/carry_over.js";
4
+ import { humanCopy } from "../services/review.js";
5
+ const SEVERITY_RANK = {
6
+ P0: 0,
7
+ P1: 1,
8
+ P2: 2,
9
+ P3: 3,
10
+ };
11
+ export function isBlockingFinding(title, severity) {
12
+ if (/\[P0\b/i.test(title) || /^P0$/i.test(severity))
13
+ return true;
14
+ if (/\[P\d\s*·\s*blocking\]/i.test(title))
15
+ return true;
16
+ return false;
17
+ }
18
+ export function resolvedFromFirstReview(parsed, filesByPath) {
19
+ return parsed.map((finding) => {
20
+ const file = finding.path ? filesByPath.get(finding.path) : undefined;
21
+ const anchor = file && finding.from
22
+ ? anchorTextFromPatch(file.patch, finding.from, finding.to ?? finding.from)
23
+ : null;
24
+ return {
25
+ id: crypto.randomUUID(),
26
+ state: "new",
27
+ path: finding.path,
28
+ lineFrom: finding.from,
29
+ lineTo: finding.to,
30
+ title: finding.heading || finding.path,
31
+ bodyMd: finding.excerpt,
32
+ anchorText: anchor,
33
+ severity: finding.severity ?? "P2",
34
+ carriedFromId: null,
35
+ firstSeenReviewId: null,
36
+ };
37
+ });
38
+ }
39
+ function compareFindings(a, b) {
40
+ const stateOrder = { new: 0, open: 1, closed: 2 };
41
+ const ds = stateOrder[a.state] - stateOrder[b.state];
42
+ if (ds !== 0)
43
+ return ds;
44
+ const sa = SEVERITY_RANK[a.severity] ?? 9;
45
+ const sb = SEVERITY_RANK[b.severity] ?? 9;
46
+ if (sa !== sb)
47
+ return sa - sb;
48
+ const pa = a.path ?? "";
49
+ const pb = b.path ?? "";
50
+ if (pa !== pb)
51
+ return pa.localeCompare(pb);
52
+ return (a.lineFrom ?? 0) - (b.lineFrom ?? 0);
53
+ }
54
+ export function sortResolvedFindings(findings) {
55
+ return [...findings].sort(compareFindings);
56
+ }
57
+ export function toJsonFinding(row) {
58
+ const suggestion = readSuggestion(row.bodyMd);
59
+ return {
60
+ id: row.id,
61
+ state: row.state,
62
+ closeReason: row.closeReason ?? null,
63
+ severity: row.severity,
64
+ blocking: isBlockingFinding(row.title, row.severity),
65
+ path: row.path,
66
+ lineFrom: row.lineFrom,
67
+ lineTo: row.lineTo,
68
+ title: humanCopy(row.title),
69
+ body: humanCopy(stripSuggestion(row.bodyMd)),
70
+ suggestion: suggestion
71
+ ? {
72
+ lineFrom: suggestion.from,
73
+ lineTo: suggestion.to,
74
+ text: suggestion.code,
75
+ }
76
+ : null,
77
+ };
78
+ }
79
+ export function revisionStats(revision) {
80
+ let additions = 0;
81
+ let deletions = 0;
82
+ for (const file of revision.files) {
83
+ additions += file.additions;
84
+ deletions += file.deletions;
85
+ }
86
+ return {
87
+ files: revision.files.length,
88
+ additions,
89
+ deletions,
90
+ truncated: false,
91
+ };
92
+ }
93
+ export function summaryCounts(findings) {
94
+ let blocking = 0;
95
+ for (const row of findings) {
96
+ if ((row.state === "new" || row.state === "open") &&
97
+ isBlockingFinding(row.title, row.severity)) {
98
+ blocking++;
99
+ }
100
+ }
101
+ return {
102
+ new: findings.filter((f) => f.state === "new").length,
103
+ open: findings.filter((f) => f.state === "open").length,
104
+ closed: findings.filter((f) => f.state === "closed").length,
105
+ blocking,
106
+ };
107
+ }
108
+ export function reviewExitCodeFromResolved(findings) {
109
+ return summaryCounts(findings).blocking > 0 ? 1 : 0;
110
+ }
111
+ function locationLabel(row) {
112
+ if (!row.path)
113
+ return row.title;
114
+ const from = row.lineFrom ?? 0;
115
+ const to = row.lineTo ?? from;
116
+ const span = from === to ? `${from}` : `${from}-${to}`;
117
+ return `${row.path}:${span}`;
118
+ }
119
+ function shortTitle(row) {
120
+ const title = humanCopy(row.title);
121
+ const dash = title.indexOf(" — ");
122
+ return dash === -1
123
+ ? title
124
+ : title
125
+ .slice(dash + 3)
126
+ .replace(/^`|`$/g, "")
127
+ .trim();
128
+ }
129
+ function impactLabel(row) {
130
+ const blocking = isBlockingFinding(row.title, row.severity);
131
+ return `[${row.severity} · ${blocking ? "blocking" : "non-blocking"}]`;
132
+ }
133
+ /** Plan §9.1 human-readable local review output. */
134
+ export function formatHumanLocalReview(header, revision, guideBuiltAt, codegraphState, findings, warnings) {
135
+ const stats = revisionStats(revision);
136
+ const guideBit = guideBuiltAt
137
+ ? `guide ${guideBuiltAt.slice(0, 10)}`
138
+ : "guide unknown";
139
+ const cgBit = codegraphState === "used"
140
+ ? "codegraph used"
141
+ : codegraphState === "disabled"
142
+ ? "codegraph disabled"
143
+ : "codegraph unavailable";
144
+ const lines = [
145
+ header,
146
+ `${stats.files} files · +${stats.additions} −${stats.deletions} · ${guideBit} · ${cgBit}`,
147
+ "",
148
+ ];
149
+ const sorted = sortResolvedFindings(findings);
150
+ const groups = [
151
+ { label: "Closed", state: "closed", bullet: "✓" },
152
+ { label: "Still open", state: "open", bullet: "•" },
153
+ { label: "New", state: "new", bullet: "•" },
154
+ ];
155
+ let any = false;
156
+ for (const group of groups) {
157
+ const rows = sorted.filter((f) => f.state === group.state);
158
+ if (rows.length === 0)
159
+ continue;
160
+ any = true;
161
+ lines.push(`${group.label} (${rows.length})`);
162
+ for (const row of rows) {
163
+ const loc = locationLabel(row);
164
+ const head = `${group.bullet} ${loc} ${impactLabel(row)} ${shortTitle(row)}`;
165
+ lines.push(` ${head}`);
166
+ if (row.state !== "closed") {
167
+ const body = humanCopy(stripSuggestion(row.bodyMd)).trim();
168
+ if (body) {
169
+ for (const part of body.split("\n")) {
170
+ lines.push(` ${part}`);
171
+ }
172
+ const suggestion = readSuggestion(row.bodyMd);
173
+ if (suggestion && row.path) {
174
+ lines.push(` Suggested replacement for ${row.path}:${suggestion.from}${suggestion.to !== suggestion.from ? `-${suggestion.to}` : ""}`);
175
+ lines.push(" ```");
176
+ for (const part of suggestion.code.split("\n")) {
177
+ lines.push(` ${part}`);
178
+ }
179
+ lines.push(" ```");
180
+ }
181
+ }
182
+ }
183
+ }
184
+ lines.push("");
185
+ }
186
+ if (!any) {
187
+ lines.push("No actionable findings.");
188
+ lines.push("");
189
+ }
190
+ const counts = summaryCounts(findings);
191
+ lines.push(`Summary: ${counts.new} new · ${counts.open} open · ${counts.closed} closed · ${counts.blocking} blocking`);
192
+ if (warnings.length) {
193
+ lines.push("Warnings:");
194
+ for (const w of warnings)
195
+ lines.push(` - ${w.message}`);
196
+ }
197
+ return lines.join("\n");
198
+ }
199
+ /** Human-readable findings block for remote sync JSON (same shape as `toJsonFinding`). */
200
+ export function formatHumanJsonFindings(findings) {
201
+ if (findings.length === 0) {
202
+ return "## Findings\n\nNo actionable findings.";
203
+ }
204
+ const lines = ["## Findings", ""];
205
+ const sorted = [...findings].sort((a, b) => (SEVERITY_RANK[a.severity] ?? 9) - (SEVERITY_RANK[b.severity] ?? 9));
206
+ for (const row of sorted) {
207
+ const blocking = row.blocking || isBlockingFinding(row.title, row.severity);
208
+ const impact = `[${row.severity} · ${blocking ? "blocking" : "non-blocking"}]`;
209
+ let prefix = "";
210
+ if (row.path) {
211
+ const from = row.lineFrom ?? 0;
212
+ const to = row.lineTo ?? from;
213
+ const span = from === to ? `${from}` : `${from}-${to}`;
214
+ prefix = `${row.path}:${span} `;
215
+ }
216
+ lines.push(`- ${prefix}${impact} ${row.title}`);
217
+ const body = row.body.trim();
218
+ if (body) {
219
+ for (const part of body.split("\n")) {
220
+ lines.push(` ${part}`);
221
+ }
222
+ }
223
+ if (row.suggestion && row.path) {
224
+ const { lineFrom, lineTo, text } = row.suggestion;
225
+ lines.push(` Suggested replacement for ${row.path}:${lineFrom}${lineTo !== lineFrom ? `-${lineTo}` : ""}`);
226
+ lines.push(" ```");
227
+ for (const part of text.split("\n")) {
228
+ lines.push(` ${part}`);
229
+ }
230
+ lines.push(" ```");
231
+ }
232
+ lines.push("");
233
+ }
234
+ return lines.join("\n").trimEnd();
235
+ }
236
+ export function reviewExitCodeFromJsonFindings(findings) {
237
+ return findings.some((f) => f.blocking || isBlockingFinding(f.title, f.severity))
238
+ ? 1
239
+ : 0;
240
+ }
241
+ export function buildPrReviewJson(input) {
242
+ const findings = resolvedFromFirstReview(parseFindings(input.markdown), new Map());
243
+ const sorted = sortResolvedFindings(findings);
244
+ return JSON.stringify({
245
+ schemaVersion: 1,
246
+ ok: true,
247
+ mode: "pr",
248
+ subject: { repo: input.repo, branch: null, prNumber: input.prNumber },
249
+ guide: { builtAt: input.guideBuiltAt },
250
+ codegraph: {
251
+ state: input.codegraphState,
252
+ reason: input.codegraphReason,
253
+ },
254
+ summary: summaryCounts(findings),
255
+ findings: sorted.map(toJsonFinding),
256
+ warnings: [],
257
+ usage: input.usage,
258
+ durationMs: input.durationMs,
259
+ });
260
+ }
261
+ export function buildLocalReviewJson(input) {
262
+ const sorted = sortResolvedFindings(input.findings);
263
+ return JSON.stringify({
264
+ schemaVersion: 1,
265
+ ok: true,
266
+ mode: "local",
267
+ subject: { repo: input.repo, branch: input.branch, prNumber: null },
268
+ base: { toBranch: input.baseLabel, label: input.baseLabel },
269
+ revision: revisionStats(input.revision),
270
+ guide: { builtAt: input.guideBuiltAt },
271
+ codegraph: {
272
+ state: input.codegraphState,
273
+ reason: input.codegraphReason,
274
+ },
275
+ summary: summaryCounts(input.findings),
276
+ findings: sorted.map(toJsonFinding),
277
+ warnings: input.warnings,
278
+ usage: input.usage,
279
+ durationMs: input.durationMs,
280
+ });
281
+ }
@@ -0,0 +1,108 @@
1
+ import { type Platform } from "./util/runtime.ts";
2
+ /** Everything init/remake resolved for one repo, except secrets — an API
3
+ * token is never written to config.json, only ever taken from --token or
4
+ * an env file. Remembering this lets `remake owner/repo` run with no
5
+ * flags and no interactive prompts, reusing what `init` was told. */
6
+ export type RepoConfig = {
7
+ auth?: "gh" | "pat";
8
+ ai?: "none" | "openrouter" | "hetzner";
9
+ lowModel?: string;
10
+ highModel?: string;
11
+ maxCommits?: number;
12
+ maxPrMonths?: number;
13
+ maxPullRequestChangeLines?: number;
14
+ maxComments?: number;
15
+ includeCodebase?: boolean;
16
+ includePullRequests?: boolean;
17
+ includePullRequestChanges?: boolean;
18
+ includeCommitHistory?: boolean;
19
+ includeHowRepoWorks?: boolean;
20
+ };
21
+ export type UserConfig = {
22
+ auth?: "gh" | "pat";
23
+ ai?: "none" | "openrouter" | "hetzner";
24
+ lowModel?: string;
25
+ highModel?: string;
26
+ /** Written only by `co-maintainer set --token=...`. Every other write
27
+ * path (writeRepoConfig, init/remake) must never put a secret here. */
28
+ token?: string;
29
+ /** GitHub Personal Access Token, used when `--auth=pat`. Only ever
30
+ * written by `set --github-pat=...`. */
31
+ githubPat?: string;
32
+ /** GitHub App credentials for `co-maintainer serve` — also only ever
33
+ * written by `set`. `githubWebhookSecret` is optional. */
34
+ githubAppId?: string;
35
+ githubAppPrivateKey?: string;
36
+ githubWebhookSecret?: string;
37
+ webhookUrl?: string;
38
+ /** "Sign in with GitHub" for the dashboard, reusing the App's own OAuth
39
+ * client — only `githubOAuthAllowedUser` may complete it. All three only
40
+ * ever written by `set`. */
41
+ githubOAuthClientId?: string;
42
+ githubOAuthClientSecret?: string;
43
+ githubOAuthAllowedUser?: string;
44
+ /** Which sign-in methods `serve` accepts by default; a `--disable-auth`/
45
+ * `--enable-auth` flag on the `serve` command overrides these per run. */
46
+ passwordAuthDisabled?: boolean;
47
+ githubAuthEnabled?: boolean;
48
+ defaults?: {
49
+ maxCommits?: number;
50
+ maxPrMonths?: number;
51
+ maxPullRequestChangeLines?: number;
52
+ };
53
+ /** Cap on jobs running at once across all types. Unset means no limit. */
54
+ maxConcurrentJobs?: number;
55
+ /** Remote review CLI target (`co-maintainer set`). */
56
+ remoteHost?: string;
57
+ /** Bearer token for `/api/remote/*`; only written by `set`. */
58
+ remoteToken?: string;
59
+ /** Hosts for which the unpublished-code notice was shown (plan §14.1 G13). */
60
+ remoteNoticeShownFor?: string[];
61
+ /** Remote client watchdog timeout; dashboard may override (plan §19.6). */
62
+ remoteSyncTimeoutSeconds?: number;
63
+ maxConcurrentRemoteReviewsPerToken?: number;
64
+ remoteToolOutputMaxChars?: number;
65
+ repos?: Record<string, RepoConfig>;
66
+ };
67
+ /** Platform/env injection keeps the OS branches testable (plan §0b): the
68
+ * migration's `"windows"` vs `"win32"` trap is invisible otherwise. */
69
+ export declare function getConfigDir(os?: Platform, env?: (name: string) => string | undefined): string;
70
+ export declare function getCacheDir(os?: Platform, env?: (name: string) => string | undefined): string;
71
+ /** `CM_CONFIG_PATH` overrides the path — tests point it at a temp file so
72
+ * they never read or write the real config.json. */
73
+ export declare function configPath(): string;
74
+ /** Where generated skills (SKILL.md, CODEBASE.md, review guides) live.
75
+ * Never relative to the current directory — a globally installed CLI can be
76
+ * invoked from anywhere, including directories it has no permission to
77
+ * write into (e.g. C:\Windows\System32). */
78
+ export declare function reposDir(): string;
79
+ /** Third-party binaries co-maintainer installs for itself, never onto the
80
+ * user's global PATH. Each tool lives under its own version directory so an
81
+ * upgrade installs alongside the old one instead of over it, and a rollback is
82
+ * just pointing at the previous directory. */
83
+ export declare function toolsDir(): string;
84
+ /** A short, stable directory name for a repository. Full `owner-repo` names
85
+ * blow the Windows 260 character path limit once a deep source tree is checked
86
+ * out under them: typescript-eslint's longest path is 169 characters on its
87
+ * own, which leaves under 91 for everything above it. */
88
+ export declare function repoSlug(repo: string): string;
89
+ /** Clones and worktrees are large, disposable and rebuildable in seconds, so
90
+ * they belong beside the cache rather than in the config directory, which on
91
+ * Windows is the roaming profile and gets synced across machines. */
92
+ export declare function clonesDir(): string;
93
+ export declare function cloneDir(repo: string): string;
94
+ export declare function worktreeDir(repo: string, pr: number): string;
95
+ export declare function cacheDbPath(): string;
96
+ export declare function loadEnvFile(path: string): void;
97
+ export declare function readConfig(): UserConfig;
98
+ /** Merges `patch` into `repos[repo]` in config.json, dropping undefined
99
+ * fields. Never call this with a token or other secret. */
100
+ export declare function writeRepoConfig(repo: string, patch: RepoConfig): Promise<void>;
101
+ /** Merges `patch` into the top-level (global, cross-repo) config —
102
+ * used by `co-maintainer set`. This is the only path allowed to persist
103
+ * a token. */
104
+ export declare function writeUserConfig(patch: Partial<UserConfig>): Promise<void>;
105
+ export declare function prepareConfig(args: string[]): {
106
+ config: UserConfig;
107
+ envPath?: string;
108
+ };
@@ -0,0 +1,138 @@
1
+ import { chmod, currentPlatform, getEnv, isNotFound, mkdir, readTextFileSync, setEnv, writeTextFile, } from "./util/runtime.js";
2
+ function homeDir(env) {
3
+ const home = env("HOME") ?? env("USERPROFILE");
4
+ if (!home)
5
+ throw new Error("Could not determine the home directory");
6
+ return home;
7
+ }
8
+ /** Platform/env injection keeps the OS branches testable (plan §0b): the
9
+ * migration's `"windows"` vs `"win32"` trap is invisible otherwise. */
10
+ export function getConfigDir(os = currentPlatform(), env = getEnv) {
11
+ const home = homeDir(env);
12
+ switch (os) {
13
+ case "windows":
14
+ return env("APPDATA") ?? `${home}\\AppData\\Roaming`;
15
+ case "darwin":
16
+ return `${home}/Library/Application Support`;
17
+ default:
18
+ return env("XDG_CONFIG_HOME") ?? `${home}/.config`;
19
+ }
20
+ }
21
+ export function getCacheDir(os = currentPlatform(), env = getEnv) {
22
+ const home = homeDir(env);
23
+ switch (os) {
24
+ case "windows":
25
+ return env("LOCALAPPDATA") ?? `${home}\\AppData\\Local`;
26
+ case "darwin":
27
+ return `${home}/Library/Caches`;
28
+ default:
29
+ return env("XDG_CACHE_HOME") ?? `${home}/.cache`;
30
+ }
31
+ }
32
+ /** `CM_CONFIG_PATH` overrides the path — tests point it at a temp file so
33
+ * they never read or write the real config.json. */
34
+ export function configPath() {
35
+ return (getEnv("CM_CONFIG_PATH") ?? `${getConfigDir()}/co-maintainer/config.json`);
36
+ }
37
+ /** Where generated skills (SKILL.md, CODEBASE.md, review guides) live.
38
+ * Never relative to the current directory — a globally installed CLI can be
39
+ * invoked from anywhere, including directories it has no permission to
40
+ * write into (e.g. C:\Windows\System32). */
41
+ export function reposDir() {
42
+ return getEnv("CM_REPOS_DIR") ?? `${getConfigDir()}/co-maintainer/repos`;
43
+ }
44
+ /** Third-party binaries co-maintainer installs for itself, never onto the
45
+ * user's global PATH. Each tool lives under its own version directory so an
46
+ * upgrade installs alongside the old one instead of over it, and a rollback is
47
+ * just pointing at the previous directory. */
48
+ export function toolsDir() {
49
+ return getEnv("CM_TOOLS_DIR") ?? `${getConfigDir()}/co-maintainer/tools`;
50
+ }
51
+ /** A short, stable directory name for a repository. Full `owner-repo` names
52
+ * blow the Windows 260 character path limit once a deep source tree is checked
53
+ * out under them: typescript-eslint's longest path is 169 characters on its
54
+ * own, which leaves under 91 for everything above it. */
55
+ export function repoSlug(repo) {
56
+ let hash = 2166136261;
57
+ for (let index = 0; index < repo.length; index++) {
58
+ hash ^= repo.charCodeAt(index);
59
+ hash = Math.imul(hash, 16777619) >>> 0;
60
+ }
61
+ return hash.toString(16).padStart(8, "0");
62
+ }
63
+ /** Clones and worktrees are large, disposable and rebuildable in seconds, so
64
+ * they belong beside the cache rather than in the config directory, which on
65
+ * Windows is the roaming profile and gets synced across machines. */
66
+ export function clonesDir() {
67
+ return getEnv("CM_CLONES_DIR") ?? `${getCacheDir()}/co-maintainer/clones`;
68
+ }
69
+ export function cloneDir(repo) {
70
+ return `${clonesDir()}/${repoSlug(repo)}`;
71
+ }
72
+ export function worktreeDir(repo, pr) {
73
+ return `${getCacheDir()}/co-maintainer/wt/${repoSlug(repo)}/${pr}`;
74
+ }
75
+ export function cacheDbPath() {
76
+ return `${getCacheDir()}/co-maintainer/cache.db`;
77
+ }
78
+ export function loadEnvFile(path) {
79
+ const contents = readTextFileSync(path);
80
+ for (const line of contents.split(/\r?\n/)) {
81
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_]\w*)\s*=\s*(.*)\s*$/);
82
+ if (!match)
83
+ continue;
84
+ let value = match[2].trim();
85
+ if ((value.startsWith('"') && value.endsWith('"')) ||
86
+ (value.startsWith("'") && value.endsWith("'"))) {
87
+ value = value.slice(1, -1);
88
+ }
89
+ if (getEnv(match[1]) === undefined)
90
+ setEnv(match[1], value);
91
+ }
92
+ }
93
+ export function readConfig() {
94
+ try {
95
+ return JSON.parse(readTextFileSync(configPath()));
96
+ }
97
+ catch (error) {
98
+ if (isNotFound(error))
99
+ return {};
100
+ throw new Error(`Could not read config ${configPath()}: ${String(error)}`);
101
+ }
102
+ }
103
+ async function writeConfig(config) {
104
+ await mkdir(`${getConfigDir()}/co-maintainer`, { recursive: true });
105
+ const path = configPath();
106
+ await writeTextFile(path, `${JSON.stringify(config, null, 2)}\n`);
107
+ // config.json can hold an API token (see UserConfig.token) — keep it
108
+ // readable only by the current user where the platform supports it.
109
+ try {
110
+ await chmod(path, 0o600);
111
+ }
112
+ catch {
113
+ // Windows has no POSIX chmod; NotSupported there is expected.
114
+ }
115
+ }
116
+ /** Merges `patch` into `repos[repo]` in config.json, dropping undefined
117
+ * fields. Never call this with a token or other secret. */
118
+ export async function writeRepoConfig(repo, patch) {
119
+ const config = readConfig();
120
+ const repos = { ...config.repos };
121
+ repos[repo] = Object.fromEntries(Object.entries({ ...repos[repo], ...patch }).filter(([, value]) => value !== undefined));
122
+ await writeConfig({ ...config, repos });
123
+ }
124
+ /** Merges `patch` into the top-level (global, cross-repo) config —
125
+ * used by `co-maintainer set`. This is the only path allowed to persist
126
+ * a token. */
127
+ export async function writeUserConfig(patch) {
128
+ const config = readConfig();
129
+ const merged = Object.fromEntries(Object.entries({ ...config, ...patch }).filter(([, value]) => value !== undefined));
130
+ await writeConfig(merged);
131
+ }
132
+ export function prepareConfig(args) {
133
+ const envArg = args.find((arg) => arg.startsWith("--env="));
134
+ const envPath = envArg?.slice("--env=".length);
135
+ if (envPath)
136
+ loadEnvFile(envPath);
137
+ return { config: readConfig(), envPath };
138
+ }
@@ -0,0 +1,5 @@
1
+ import type { Run } from "../pr/checkout.ts";
2
+ /** `refs/remotes/<remote>/…` for scope and PR algorithms (plan §10.5). */
3
+ export declare function detectDefaultBranchRef(cwd: string, remote: string, run: Run): Promise<string | undefined>;
4
+ /** Short branch name (`main`) for `refs/remotes/<remote>/<branch>`. */
5
+ export declare function resolveDefaultBranchName(cwd: string, remote: string, run: Run): Promise<string | undefined>;
@@ -0,0 +1,22 @@
1
+ /** `refs/remotes/<remote>/…` for scope and PR algorithms (plan §10.5). */
2
+ export async function detectDefaultBranchRef(cwd, remote, run) {
3
+ const symref = await run("git", ["symbolic-ref", "-q", `refs/remotes/${remote}/HEAD`], cwd);
4
+ if (symref.code === 0 && symref.stdout.trim()) {
5
+ return symref.stdout.trim();
6
+ }
7
+ for (const name of ["main", "master"]) {
8
+ const ref = `refs/remotes/${remote}/${name}`;
9
+ const probe = await run("git", ["rev-parse", "--verify", "-q", ref], cwd);
10
+ if (probe.code === 0)
11
+ return ref;
12
+ }
13
+ return undefined;
14
+ }
15
+ /** Short branch name (`main`) for `refs/remotes/<remote>/<branch>`. */
16
+ export async function resolveDefaultBranchName(cwd, remote, run) {
17
+ const ref = await detectDefaultBranchRef(cwd, remote, run);
18
+ if (!ref)
19
+ return undefined;
20
+ const parts = ref.split("/");
21
+ return parts[parts.length - 1];
22
+ }
@@ -0,0 +1,43 @@
1
+ import type { GitHubClient } from "../types.ts";
2
+ export type Installation = {
3
+ id: number;
4
+ account: {
5
+ login: string;
6
+ type: string;
7
+ } | null;
8
+ suspended_at: string | null;
9
+ permissions?: Record<string, string>;
10
+ };
11
+ /** App-level calls: list installations, mint an installation access token.
12
+ * Not scoped to one installation — `AppClient` below is. */
13
+ export declare class AppJwtClient {
14
+ private readonly appId;
15
+ private readonly privateKeyPem;
16
+ constructor(appId: string, privateKeyPem: string);
17
+ private jwt;
18
+ listInstallations(): Promise<Installation[]>;
19
+ /** Cached until 60s before the token's own `expires_at`. */
20
+ installationToken(installationId: number): Promise<string>;
21
+ }
22
+ /** A `GitHubClient` scoped to one installation. */
23
+ export declare class AppClient implements GitHubClient {
24
+ private readonly app;
25
+ private readonly installationId;
26
+ constructor(appId: string, privateKeyPem: string, installationId: number);
27
+ request<T>(endpoint: string): Promise<T>;
28
+ pages<T>(endpoint: string, limit?: number, progress?: (page: number, fetched: number) => void): Promise<T[]>;
29
+ write<T>(endpoint: string, body: unknown): Promise<T>;
30
+ createCheckRun<T>(endpoint: string, body: unknown): Promise<T>;
31
+ updateCheckRun<T>(endpoint: string, body: unknown): Promise<T>;
32
+ }
33
+ export type InstallationRepos = {
34
+ installation: Installation;
35
+ repos: {
36
+ fullName: string;
37
+ private: boolean;
38
+ }[];
39
+ };
40
+ /** For `GET /api/installations`: every installation the App has, each with
41
+ * the repos it can see. */
42
+ export declare function listInstallationsWithRepos(appId: string, privateKeyPem: string): Promise<InstallationRepos[]>;
43
+ export declare function findInstallationForRepo(appId: string, privateKeyPem: string, fullName: string): Promise<number | undefined>;