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,29 @@
1
+ import type { AiResponse, GitHubClient, Options } from "../types.ts";
2
+ import type { LogFn } from "./jobs.ts";
3
+ import type { JobRow } from "../store/rows.ts";
4
+ export type AiMetrics = {
5
+ calls: number;
6
+ tokensIn: number;
7
+ tokensOut: number;
8
+ cost: number;
9
+ costKnown: boolean;
10
+ };
11
+ export declare function emptyAiMetrics(): AiMetrics;
12
+ export declare function recordAiCost(repo: string, job: string, response: AiResponse): Promise<void>;
13
+ export declare function clientFor(options: Options): GitHubClient;
14
+ export declare function runInitOrRemake(options: Options): Promise<void>;
15
+ /** Builds `Options` for a server-triggered `init`/`remake` from config
16
+ * alone — no CLI flags, no interactive prompt, there is no terminal here. */
17
+ export declare function optionsFromConfig(repo: string, command: "init" | "remake"): Options;
18
+ /** `runner` defaults to the real `runInitOrRemake`; tests substitute a fake
19
+ * to check the option merging and log wiring without touching the network. */
20
+ export declare function buildSetupHandler(runner?: typeof runInitOrRemake): {
21
+ run(job: JobRow, jobLog: LogFn): Promise<void>;
22
+ };
23
+ /** No `reconcile` hook: `run` is idempotent, so an orphan from a crash is
24
+ * simply requeued and run again from the top. */
25
+ export declare function registerSetupJobHandler(): void;
26
+ export declare function enqueueSetup(repo: string, command: "init" | "remake", overrides?: Partial<Options>): {
27
+ id: string;
28
+ debounced: boolean;
29
+ };
@@ -0,0 +1,364 @@
1
+ /** `init`/`remake` orchestration. The dashboard calls `runInitOrRemake`
2
+ * directly; it used to re-spawn the CLI as a subprocess, a second
3
+ * execution path with its own failure modes. */
4
+ import { createAiProvider } from "../ai/provider.js";
5
+ import { enrichFacts, synthesizeSections } from "../knowledge/synthesis.js";
6
+ import { collectSource } from "../github/collect.js";
7
+ import { GhClient } from "../github/gh.js";
8
+ import { PatClient } from "../github/pat.js";
9
+ import { extractFacts } from "../knowledge/facts.js";
10
+ import { buildReviewDocuments } from "../knowledge/guide.js";
11
+ import { assembleSkill, extractSections, factSectionHashes, } from "../knowledge/skill.js";
12
+ import { validateSkill } from "../knowledge/validate.js";
13
+ import { readConfig, reposDir, writeRepoConfig } from "../config.js";
14
+ import { readState, writeState } from "../store/skill_state.js";
15
+ import { cacheSet } from "../store/cache_db.js";
16
+ import { log, timed, withLogSink } from "../util/log.js";
17
+ import { enqueue, registerHandler } from "./jobs.js";
18
+ import { getRepo, markKnowledgeBuilt } from "../store/repos.js";
19
+ import { nowIso } from "../util/time.js";
20
+ import { mkdir, readTextFile, remove } from "../util/runtime.js";
21
+ export function emptyAiMetrics() {
22
+ return { calls: 0, tokensIn: 0, tokensOut: 0, cost: 0, costKnown: true };
23
+ }
24
+ export async function recordAiCost(repo, job, response) {
25
+ await cacheSet("cost", `${repo}:${crypto.randomUUID()}`, JSON.stringify({
26
+ at: new Date().toISOString(),
27
+ job,
28
+ provider: response.provider,
29
+ model: response.model,
30
+ tokensIn: response.tokensIn,
31
+ tokensOut: response.tokensOut,
32
+ usd: response.provider === "hetzner" ? 0 : (response.cost ?? null),
33
+ }));
34
+ }
35
+ export function clientFor(options) {
36
+ if (options.auth === "pat") {
37
+ return new PatClient(options.githubPat ?? "");
38
+ }
39
+ return new GhClient();
40
+ }
41
+ function optionsForState(options) {
42
+ const { command: _, aiToken: __, githubPat: ___, ...rest } = options;
43
+ return rest;
44
+ }
45
+ function emptyState(options) {
46
+ const source = {
47
+ repo: {},
48
+ tree: [],
49
+ treeSha: {},
50
+ files: {},
51
+ pullRequests: [],
52
+ commits: [],
53
+ };
54
+ return {
55
+ version: 1,
56
+ repo: options.repo,
57
+ options: optionsForState(options),
58
+ source,
59
+ facts: [],
60
+ sectionHashes: {},
61
+ scanDone: {
62
+ pullRequests: 0,
63
+ commits: 0,
64
+ updatedAt: new Date().toISOString(),
65
+ },
66
+ updatedAt: new Date().toISOString(),
67
+ };
68
+ }
69
+ async function skillPath(repo) {
70
+ const path = `${reposDir()}/${repo}/SKILL.md`;
71
+ await mkdir(`${reposDir()}/${repo}`, { recursive: true });
72
+ return path;
73
+ }
74
+ async function writeReviewDocuments(repo, documents) {
75
+ const directory = `${reposDir()}/${repo}`;
76
+ if (!documents) {
77
+ await Promise.all([
78
+ remove(`${directory}/PR_REVIEW_GUIDE.md`).catch(() => { }),
79
+ remove(`${directory}/PR_REVIEW_DETAILED_GUIDE.md`).catch(() => { }),
80
+ ]);
81
+ return;
82
+ }
83
+ const { writeTextFileAtomic } = await import("../util/atomic.js");
84
+ await writeTextFileAtomic(`${directory}/PR_REVIEW_GUIDE.md`, documents.guide);
85
+ if (documents.detailed) {
86
+ await writeTextFileAtomic(`${directory}/PR_REVIEW_DETAILED_GUIDE.md`, documents.detailed);
87
+ }
88
+ else {
89
+ await remove(`${directory}/PR_REVIEW_DETAILED_GUIDE.md`).catch(() => { });
90
+ }
91
+ }
92
+ /** Splits the codebase-description sections (layout/style/tests/devloop) out
93
+ * of the assembled skill into their own file, so `review` can check a pull
94
+ * request against how this repository's code actually looks, not just the
95
+ * review-bar checklist mined from past PR comments. */
96
+ async function writeCodebaseDocument(repo, skillMarkdown) {
97
+ const path = `${reposDir()}/${repo}/CODEBASE.md`;
98
+ const sections = extractSections(skillMarkdown);
99
+ const body = ["layout", "style", "tests", "devloop"]
100
+ .map((key) => sections[key])
101
+ .filter(Boolean)
102
+ .join("\n\n");
103
+ if (!body) {
104
+ await remove(path).catch(() => { });
105
+ return;
106
+ }
107
+ const { writeTextFileAtomic } = await import("../util/atomic.js");
108
+ await writeTextFileAtomic(path, `# Codebase conventions for ${repo}\n\nHow this repository's code is actually structured and written. A pull request that departs from these observed conventions is worth flagging even without a matching review-bar rule.\n\n${body}\n`);
109
+ }
110
+ function addReviewLink(markdown, documents) {
111
+ if (!documents)
112
+ return markdown;
113
+ const detailedLink = documents.detailed
114
+ ? " See [PR_REVIEW_DETAILED_GUIDE.md](PR_REVIEW_DETAILED_GUIDE.md) for evidence."
115
+ : "";
116
+ return `${markdown.trimEnd()}\n\n## Pull request review guides\n\n- Read [PR_REVIEW_GUIDE.md](PR_REVIEW_GUIDE.md).${detailedLink}\n`;
117
+ }
118
+ export async function runInitOrRemake(options) {
119
+ const operationStarted = performance.now();
120
+ const aiMetrics = emptyAiMetrics();
121
+ const previous = await readState(options.repo);
122
+ if (options.command === "remake" && !previous) {
123
+ throw new Error("remake requires a previous init or remake for this repository");
124
+ }
125
+ if (options.command === "remake" && previous) {
126
+ options.maxCommits ??= previous.options.maxCommits;
127
+ options.maxPrMonths ??= previous.options.maxPrMonths;
128
+ options.maxPullRequestChangeLines ??=
129
+ previous.options.maxPullRequestChangeLines;
130
+ }
131
+ const checkpoint = previous ?? emptyState(options);
132
+ const client = clientFor(options);
133
+ const lowAi = createAiProvider(options, options.lowModel ?? "");
134
+ const highAi = createAiProvider(options, options.highModel ?? "");
135
+ if (lowAi || highAi) {
136
+ log("ai", `${options.ai} providers configured · low=${options.lowModel} · high=${options.highModel}`);
137
+ }
138
+ log("fetch", `${options.repo} via ${options.auth}`);
139
+ const source = await timed("fetch repository data", options.logTime, () => collectSource(client, options, previous, async (pullRequests) => {
140
+ checkpoint.source.pullRequests = pullRequests;
141
+ checkpoint.scanDone = {
142
+ ...checkpoint.scanDone,
143
+ pullRequests: pullRequests.length,
144
+ updatedAt: new Date().toISOString(),
145
+ };
146
+ await writeState(checkpoint);
147
+ }, async (codebase) => {
148
+ checkpoint.source = {
149
+ ...checkpoint.source,
150
+ tree: codebase.tree,
151
+ treeSha: codebase.treeSha,
152
+ files: codebase.files,
153
+ };
154
+ await writeState(checkpoint);
155
+ }));
156
+ log("fetch", `source ready · ${Object.keys(source.files).length} files · ${source.pullRequests.length} PRs · ${source.commits.length} commits`);
157
+ const path = await skillPath(options.repo);
158
+ let previousMarkdown;
159
+ try {
160
+ previousMarkdown = await readTextFile(path);
161
+ }
162
+ catch {
163
+ // init can create the first output.
164
+ }
165
+ const factsStarted = performance.now();
166
+ let facts = extractFacts(source, options);
167
+ if (options.logTime) {
168
+ log("time", `extract deterministic facts · ${((performance.now() - factsStarted) / 1000).toFixed(2)}s`);
169
+ }
170
+ let overrides = {};
171
+ if (lowAi) {
172
+ log("ai", "starting extract_unit jobs");
173
+ const usage = async (job, response) => {
174
+ aiMetrics.calls++;
175
+ aiMetrics.tokensIn += response.tokensIn;
176
+ aiMetrics.tokensOut += response.tokensOut;
177
+ if (response.provider === "hetzner")
178
+ aiMetrics.cost += 0;
179
+ else if (response.cost === undefined)
180
+ aiMetrics.costKnown = false;
181
+ else
182
+ aiMetrics.cost += response.cost;
183
+ await recordAiCost(options.repo, job, response);
184
+ };
185
+ facts = await timed("extract_unit AI", options.logTime, () => enrichFacts(lowAi, options.repo, facts, source, options, usage));
186
+ log("ai", `extract_unit complete · ${facts.length} facts`);
187
+ const hashes = await factSectionHashes(facts);
188
+ const synthesisChanged = previous &&
189
+ (previous.options.highModel !== options.highModel ||
190
+ previous.options.synthesisVersion !== options.synthesisVersion);
191
+ const dirtySections = previous
192
+ ? synthesisChanged
193
+ ? new Set(Object.keys(hashes))
194
+ : new Set(Object.entries(hashes)
195
+ .filter(([key, hash]) => previous.sectionHashes[key] !== hash)
196
+ .map(([key]) => key))
197
+ : undefined;
198
+ log("ai", `starting synth_section jobs${dirtySections ? ` · ${dirtySections.size} dirty sections` : ""}`);
199
+ if (highAi) {
200
+ overrides = await timed("synth_section AI", options.logTime, () => synthesizeSections(highAi, options.repo, facts, previousMarkdown, options.ai, options.highModel ?? "", options.aiConcurrent, usage, dirtySections));
201
+ }
202
+ log("ai", `synth_section complete · ${Object.keys(overrides).length} sections`);
203
+ }
204
+ let result = await timed("assemble skill", options.logTime, () => assembleSkill(options.repo, facts, previousMarkdown, previous?.sectionHashes ?? {}, overrides));
205
+ const reviewDocuments = buildReviewDocuments(facts);
206
+ result.markdown = addReviewLink(result.markdown, reviewDocuments);
207
+ const validation = await timed("validate skill", options.logTime, () => validateSkill(result.markdown, `${reposDir()}/${options.repo}`, source));
208
+ if (!validation.valid && Object.keys(overrides).length) {
209
+ log("validate", `AI output rejected: ${validation.errors.join("; ")}`);
210
+ overrides = {};
211
+ result = await timed("reassemble valid skill", options.logTime, () => assembleSkill(options.repo, facts, previousMarkdown, previous?.sectionHashes ?? {}, overrides));
212
+ result.markdown = addReviewLink(result.markdown, reviewDocuments);
213
+ }
214
+ const finalValidation = await timed("final skill validation", options.logTime, () => validateSkill(result.markdown, `${reposDir()}/${options.repo}`, source));
215
+ if (!finalValidation.valid) {
216
+ throw new Error(`generated skill is invalid: ${finalValidation.errors.join("; ")}`);
217
+ }
218
+ await timed("write skill and state", options.logTime, async () => {
219
+ const { withKnowledgeLock } = await import("../review/guides.js");
220
+ const { writeTextFileAtomic } = await import("../util/atomic.js");
221
+ await withKnowledgeLock(options.repo, async () => {
222
+ await writeTextFileAtomic(path, result.markdown);
223
+ await writeCodebaseDocument(options.repo, result.markdown);
224
+ await writeReviewDocuments(options.repo, reviewDocuments);
225
+ const head = source.commits[0];
226
+ const baseSha = head?.sha ? String(head.sha) : new Date().toISOString();
227
+ markKnowledgeBuilt(options.repo, baseSha);
228
+ });
229
+ await writeState({
230
+ version: 1,
231
+ repo: options.repo,
232
+ options: optionsForState(options),
233
+ source,
234
+ facts,
235
+ sectionHashes: result.hashes,
236
+ scanDone: {
237
+ pullRequests: source.pullRequests.length,
238
+ commits: source.commits.length,
239
+ updatedAt: new Date().toISOString(),
240
+ },
241
+ updatedAt: new Date().toISOString(),
242
+ });
243
+ // Remember everything but the token, so `remake owner/repo` alone
244
+ // (no flags, no prompts) reuses what this run resolved.
245
+ await writeRepoConfig(options.repo, {
246
+ auth: options.auth,
247
+ ai: options.ai,
248
+ lowModel: options.lowModel,
249
+ highModel: options.highModel,
250
+ maxCommits: options.maxCommits,
251
+ maxPrMonths: options.maxPrMonths,
252
+ maxPullRequestChangeLines: options.maxPullRequestChangeLines,
253
+ maxComments: options.maxComments,
254
+ includeCodebase: options.includeCodebase,
255
+ includePullRequests: options.includePullRequests,
256
+ includePullRequestChanges: options.includePullRequestChanges,
257
+ includeCommitHistory: options.includeCommitHistory,
258
+ includeHowRepoWorks: options.includeHowRepoWorks,
259
+ });
260
+ });
261
+ log("write", `${path} · ${result.changed.length
262
+ ? `updated ${result.changed.join(", ")}`
263
+ : "already current"}`);
264
+ log("done", `${facts.length} facts · ${source.pullRequests.length} pull requests · ${source.commits.length} commits`);
265
+ if (options.logTime) {
266
+ log("time", `AI total · calls=${aiMetrics.calls} · input=${aiMetrics.tokensIn} tokens · output=${aiMetrics.tokensOut} tokens · cost=${aiMetrics.costKnown ? aiMetrics.cost.toFixed(4) : "unknown"}`);
267
+ log("time", `total init/remake · ${((performance.now() - operationStarted) / 1000).toFixed(2)}s`);
268
+ }
269
+ }
270
+ /** Builds `Options` for a server-triggered `init`/`remake` from config
271
+ * alone — no CLI flags, no interactive prompt, there is no terminal here. */
272
+ export function optionsFromConfig(repo, command) {
273
+ const config = readConfig();
274
+ const repoConfig = config.repos?.[repo] ?? {};
275
+ const auth = repoConfig.auth ?? config.auth ?? "gh";
276
+ if (auth === "pat" && !config.githubPat) {
277
+ throw new Error("auth=pat requires a GitHub PAT; run: co-maintainer set --github-pat=...");
278
+ }
279
+ const ai = repoConfig.ai ?? config.ai ?? "none";
280
+ const lowModel = repoConfig.lowModel ?? config.lowModel;
281
+ const highModel = repoConfig.highModel ?? config.highModel;
282
+ if (ai !== "none" && (!config.token || !lowModel || !highModel)) {
283
+ throw new Error("AI is enabled but token/low-model/high-model are not fully configured; run: " +
284
+ "co-maintainer set --token=... --low-model=... --high-model=...");
285
+ }
286
+ return {
287
+ command,
288
+ repo,
289
+ debug: false,
290
+ logTime: true,
291
+ improveMatrix: 1,
292
+ ghConcurrent: 1,
293
+ aiConcurrent: 3,
294
+ auth,
295
+ githubPat: config.githubPat,
296
+ ai,
297
+ aiToken: config.token,
298
+ lowModel,
299
+ highModel,
300
+ synthesisVersion: 16,
301
+ includeCodebase: repoConfig.includeCodebase ?? true,
302
+ includePullRequests: repoConfig.includePullRequests ?? true,
303
+ includePullRequestChanges: repoConfig.includePullRequestChanges ?? true,
304
+ includeCommitHistory: repoConfig.includeCommitHistory ?? true,
305
+ includeHowRepoWorks: repoConfig.includeHowRepoWorks ?? true,
306
+ maxCommits: repoConfig.maxCommits ?? config.defaults?.maxCommits,
307
+ maxPrMonths: repoConfig.maxPrMonths ?? config.defaults?.maxPrMonths,
308
+ maxPullRequestChangeLines: repoConfig.maxPullRequestChangeLines ??
309
+ config.defaults?.maxPullRequestChangeLines,
310
+ maxComments: repoConfig.maxComments,
311
+ };
312
+ }
313
+ /** `runner` defaults to the real `runInitOrRemake`; tests substitute a fake
314
+ * to check the option merging and log wiring without touching the network. */
315
+ // ponytail: the AbortSignal the queue passes to `run` is not threaded
316
+ // through `runInitOrRemake`'s GitHub fetch loops, so `cancel()` on a
317
+ // running init/remake marks it canceled and stops it being picked up
318
+ // again, but does not interrupt in-flight fetches — a live job keeps
319
+ // running to natural completion. Wire the signal into `github/collect.ts`'s
320
+ // `mapPool` if truly stopping a large in-flight fetch turns out to matter.
321
+ export function buildSetupHandler(runner = runInitOrRemake) {
322
+ return {
323
+ async run(job, jobLog) {
324
+ const overrides = JSON.parse(job.args || "{}");
325
+ const options = {
326
+ ...optionsFromConfig(job.repo, job.type),
327
+ ...overrides,
328
+ command: job.type,
329
+ repo: job.repo,
330
+ };
331
+ if (options.command === "remake" && !(await readState(options.repo))) {
332
+ jobLog("info", "No previous init in the cache. Building knowledge from scratch.");
333
+ options.command = "init";
334
+ }
335
+ await withLogSink((phase, message) => jobLog("info", `[${phase}] ${message}`), () => runner(options));
336
+ if (!getRepo(job.repo)?.knowledge_built_at) {
337
+ markKnowledgeBuilt(job.repo, await builtBaseSha(job.repo));
338
+ }
339
+ },
340
+ };
341
+ }
342
+ /** The head commit of the default branch as of the fetch that just ran.
343
+ * Falls back to the build timestamp when commit history was not collected,
344
+ * which is what this column held before. */
345
+ async function builtBaseSha(repo) {
346
+ const state = await readState(repo);
347
+ const head = state?.source.commits[0];
348
+ return head?.sha ? String(head.sha) : nowIso();
349
+ }
350
+ /** No `reconcile` hook: `run` is idempotent, so an orphan from a crash is
351
+ * simply requeued and run again from the top. */
352
+ export function registerSetupJobHandler() {
353
+ const handler = buildSetupHandler();
354
+ registerHandler("init", handler);
355
+ registerHandler("remake", handler);
356
+ }
357
+ export function enqueueSetup(repo, command, overrides = {}) {
358
+ return enqueue({
359
+ type: command,
360
+ repo,
361
+ args: overrides,
362
+ queueKey: `setup:${repo}`,
363
+ });
364
+ }
@@ -0,0 +1,22 @@
1
+ export declare const REVIEW_DEBOUNCE_MS = 20000;
2
+ export type WebhookResult = {
3
+ outcome: "enqueued" | "skipped" | "ignored" | "error";
4
+ reason?: string;
5
+ repo?: string;
6
+ prNumber?: number;
7
+ jobId?: string;
8
+ };
9
+ export declare function maybeEnqueueReview(input: {
10
+ repo: string;
11
+ prNumber: number;
12
+ draft: boolean;
13
+ bot: boolean;
14
+ additions: number;
15
+ deletions: number;
16
+ changedFiles: number;
17
+ deliveryId: string;
18
+ trigger: string;
19
+ headSha?: string;
20
+ maxDiffLines?: number;
21
+ }): WebhookResult;
22
+ export declare function dispatchGithubEvent(event: string, payload: Record<string, unknown>, deliveryId: string, maxDiffLines?: number): WebhookResult;