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,134 @@
1
+ import { stat } from "../util/runtime.js";
2
+ export async function validateSkill(markdown, outputDirectory, source) {
3
+ const errors = [];
4
+ if (!markdown.startsWith("---\n"))
5
+ errors.push("missing YAML frontmatter");
6
+ if (!/^name:\s+\S+/m.test(markdown))
7
+ errors.push("missing frontmatter name");
8
+ if (!/^description:\s+\S+/m.test(markdown)) {
9
+ errors.push("missing frontmatter description");
10
+ }
11
+ if (!/^#\s+\S+/m.test(markdown))
12
+ errors.push("missing skill heading");
13
+ if (markdown.includes("```")) {
14
+ errors.push("skill contains an unrequested code fence");
15
+ }
16
+ if (!(markdown.match(/^##\s+.+$/gm) ?? []).length) {
17
+ errors.push("skill has no Markdown sections");
18
+ }
19
+ for (const section of markdown.split(/^##\s+/m).slice(1)) {
20
+ const bullets = section.match(/^-\s+/gm) ?? [];
21
+ if (bullets.length > 6) {
22
+ errors.push("section exceeds six bullets");
23
+ }
24
+ if (section.split("\n").some((line) => /^\s*[-*+]\s*$/.test(line))) {
25
+ errors.push("skill contains an incomplete bullet");
26
+ }
27
+ }
28
+ if ((markdown.match(/^\|.*\|$/gm) ?? []).length >= 3) {
29
+ errors.push("skill contains a raw Markdown table");
30
+ }
31
+ if (source) {
32
+ const paths = new Set([...source.tree, ...Object.keys(source.files)]);
33
+ const repositoryName = String(source.repo.full_name ?? "");
34
+ const commands = new Set();
35
+ for (const [path, content] of Object.entries(source.files)) {
36
+ if (path.endsWith("package.json")) {
37
+ try {
38
+ const scripts = JSON.parse(content)
39
+ .scripts ?? {};
40
+ const prefix = source.tree.some((item) => /pnpm-lock\.yaml$/.test(item))
41
+ ? "pnpm"
42
+ : source.tree.some((item) => /yarn\.lock$/.test(item))
43
+ ? "yarn"
44
+ : "npm";
45
+ for (const name of Object.keys(scripts)) {
46
+ commands.add(`${prefix} ${name}`);
47
+ commands.add(`${prefix} run ${name}`);
48
+ commands.add(String(scripts[name]));
49
+ }
50
+ }
51
+ catch {
52
+ // Ignore malformed manifests; syntax validation is outside this gate.
53
+ }
54
+ }
55
+ if (/deno\.jsonc?$/.test(path)) {
56
+ try {
57
+ const tasks = JSON.parse(content).tasks ??
58
+ {};
59
+ for (const [name, task] of Object.entries(tasks)) {
60
+ commands.add(`deno task ${name}`);
61
+ commands.add(String(task));
62
+ }
63
+ }
64
+ catch {
65
+ // Ignore malformed manifests; syntax validation is outside this gate.
66
+ }
67
+ }
68
+ for (const match of content.matchAll(/^\s*run:\s*([^\s#].*?)\s*$/gm)) {
69
+ commands.add(match[1].replace(/^['"]|['"]$/g, ""));
70
+ }
71
+ for (const match of content.matchAll(/`((?:npm|pnpm|yarn|bun|deno|cargo|make|go|python|node|git)\s+[^`\n]+)`/g)) {
72
+ commands.add(match[1]);
73
+ }
74
+ for (const match of content.matchAll(/^\s*((?:npm|pnpm|yarn|bun|deno|cargo|make|go|python|node|git)\s+[^\n`]+)$/gm)) {
75
+ commands.add(match[1].trim());
76
+ }
77
+ }
78
+ if (source.tree.some((path) => /Cargo\.toml$/.test(path))) {
79
+ commands.add("cargo test");
80
+ commands.add("cargo build");
81
+ }
82
+ const references = [...markdown.matchAll(/`([^`\n]+)`/g)].map((match) => match[1]);
83
+ for (const reference of references) {
84
+ const isQualifier = /^[a-z][a-z\d_-]*:/i.test(reference);
85
+ if (reference.includes("/") &&
86
+ !reference.startsWith("@") &&
87
+ !reference.startsWith("http") &&
88
+ !isQualifier &&
89
+ !reference.startsWith("./") &&
90
+ !reference.includes(" ")) {
91
+ const normalized = reference
92
+ .replace(/^\.\/+/, "")
93
+ .replace(/^\/+/, "")
94
+ .replace(/\/+$/, "");
95
+ if (normalized === repositoryName)
96
+ continue;
97
+ const wildcardPattern = normalized.includes("*")
98
+ ? new RegExp(`^${normalized
99
+ .split("*")
100
+ .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
101
+ .join(".*")}$`)
102
+ : undefined;
103
+ if (!paths.has(normalized) &&
104
+ ![...paths].some((path) => wildcardPattern
105
+ ? wildcardPattern.test(path)
106
+ : path.startsWith(`${normalized}/`))) {
107
+ errors.push(`referenced path is absent from source: ${reference}`);
108
+ }
109
+ }
110
+ if (/^(?:npm|pnpm|yarn|bun|deno|cargo|make|go|python|node|git)\s/.test(reference) &&
111
+ ![...commands].some((command) => reference === command ||
112
+ reference.startsWith(`${command} `) ||
113
+ command.startsWith(`${reference} `))) {
114
+ errors.push(`referenced command is absent from source: ${reference}`);
115
+ }
116
+ }
117
+ }
118
+ const links = [...markdown.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)].map((match) => match[1]);
119
+ for (const link of links) {
120
+ if (/^[a-z][a-z\d+.-]*:/i.test(link) ||
121
+ link.startsWith("/") ||
122
+ link.includes("..")) {
123
+ errors.push(`link is not relative: ${link}`);
124
+ continue;
125
+ }
126
+ try {
127
+ await stat(`${outputDirectory}/${link}`);
128
+ }
129
+ catch {
130
+ errors.push(`linked file does not exist: ${link}`);
131
+ }
132
+ }
133
+ return { valid: errors.length === 0, errors };
134
+ }
@@ -0,0 +1,18 @@
1
+ import type { RevisionFile } from "../review/revision.ts";
2
+ import type { StoredFinding } from "../review/carry_over.ts";
3
+ export type LocalCarrySnapshot = {
4
+ subjectId: string;
5
+ files: RevisionFile[];
6
+ visiblePaths: string[];
7
+ findings: StoredFinding[];
8
+ guideBuiltAt: string | null;
9
+ };
10
+ export declare function localSubjectId(repo: string, root: string, branch: string): string;
11
+ export declare function loadLocalCarry(repo: string, root: string, branch: string): Promise<LocalCarrySnapshot | null>;
12
+ /** Plan E163 — corrupt or locked cache must not abort review. */
13
+ export declare function tryLoadLocalCarry(repo: string, root: string, branch: string): Promise<{
14
+ data: LocalCarrySnapshot | null;
15
+ unavailable: boolean;
16
+ }>;
17
+ export declare function saveLocalCarry(snapshot: LocalCarrySnapshot): Promise<void>;
18
+ export declare function clearLocalCarry(repo: string, root: string, branch: string): Promise<void>;
@@ -0,0 +1,40 @@
1
+ import { cacheDelete, cacheGet, cacheSet } from "../store/cache_db.js";
2
+ const NS = "local-review";
3
+ function key(repo, root, branch) {
4
+ return `${repo}\0${root}\0${branch}`;
5
+ }
6
+ export function localSubjectId(repo, root, branch) {
7
+ return key(repo, root, branch);
8
+ }
9
+ export async function loadLocalCarry(repo, root, branch) {
10
+ const loaded = await tryLoadLocalCarry(repo, root, branch);
11
+ if (loaded.unavailable) {
12
+ throw new Error("local carry-over cache is unreadable");
13
+ }
14
+ return loaded.data;
15
+ }
16
+ /** Plan E163 — corrupt or locked cache must not abort review. */
17
+ export async function tryLoadLocalCarry(repo, root, branch) {
18
+ try {
19
+ const raw = await cacheGet(NS, key(repo, root, branch));
20
+ if (!raw)
21
+ return { data: null, unavailable: false };
22
+ return { data: JSON.parse(raw), unavailable: false };
23
+ }
24
+ catch {
25
+ return { data: null, unavailable: true };
26
+ }
27
+ }
28
+ export async function saveLocalCarry(snapshot) {
29
+ const { subjectId, ...rest } = snapshot;
30
+ const parts = subjectId.split("\0");
31
+ if (parts.length !== 3)
32
+ throw new Error("invalid local subject id");
33
+ await cacheSet(NS, key(parts[0], parts[1], parts[2]), JSON.stringify({
34
+ subjectId,
35
+ ...rest,
36
+ }));
37
+ }
38
+ export async function clearLocalCarry(repo, root, branch) {
39
+ await cacheDelete(NS, key(repo, root, branch));
40
+ }
@@ -0,0 +1,3 @@
1
+ import type { Run } from "../pr/checkout.ts";
2
+ /** Plan §13.2 — hide the local codegraph index from git status. */
3
+ export declare function ensureCodegraphGitExclude(gitRoot: string, run: Run): Promise<void>;
@@ -0,0 +1,26 @@
1
+ import { dirname } from "node:path";
2
+ import { isNotFound, mkdir, readTextFile, writeTextFile, } from "../util/runtime.js";
3
+ const EXCLUDE_LINE = "/.co-maintainer-codegraph/";
4
+ /** Plan §13.2 — hide the local codegraph index from git status. */
5
+ export async function ensureCodegraphGitExclude(gitRoot, run) {
6
+ const pathResult = await run("git", ["rev-parse", "--git-path", "info/exclude"], gitRoot);
7
+ if (pathResult.code !== 0)
8
+ return;
9
+ const excludePath = pathResult.stdout.trim();
10
+ if (!excludePath)
11
+ return;
12
+ let text = "";
13
+ try {
14
+ text = await readTextFile(excludePath);
15
+ }
16
+ catch (error) {
17
+ if (!isNotFound(error))
18
+ throw error;
19
+ }
20
+ const lines = text.split("\n").map((line) => line.trim());
21
+ if (lines.includes(EXCLUDE_LINE.trim()))
22
+ return;
23
+ const prefix = text.length > 0 && !text.endsWith("\n") ? "\n" : "";
24
+ await mkdir(dirname(excludePath), { recursive: true }).catch(() => { });
25
+ await writeTextFile(excludePath, `${text}${prefix}${EXCLUDE_LINE}\n`);
26
+ }
@@ -0,0 +1,17 @@
1
+ import type { ToolHandler } from "../ai/mermaid_loop.ts";
2
+ import { type Presence } from "../tools/codegraph.ts";
3
+ import { type Run } from "../pr/checkout.ts";
4
+ export type LocalCodegraphState = "used" | "disabled" | "unavailable";
5
+ export type LocalCodegraphPrepare = {
6
+ state: LocalCodegraphState;
7
+ reason: string | null;
8
+ tools: ToolHandler[];
9
+ };
10
+ export declare function prepareLocalCodegraph(input: {
11
+ gitRoot: string;
12
+ enabled: boolean;
13
+ allowInstall: boolean;
14
+ interactive: boolean;
15
+ run?: Run;
16
+ detect?: () => Promise<Presence>;
17
+ }): Promise<LocalCodegraphPrepare>;
@@ -0,0 +1,51 @@
1
+ import { codegraphTools, ensureCodegraphIndex } from "../pr/codegraph_tools.js";
2
+ import { ensureCodegraphForReview } from "../tools/codegraph.js";
3
+ import { createCodegraphRunner, LOCAL_CODEGRAPH_DIR, } from "../tools/codegraph_exec.js";
4
+ import { log, startHeartbeat } from "../util/log.js";
5
+ import { ensureCodegraphGitExclude } from "./codegraph_exclude.js";
6
+ import { pathExists, runCommand } from "../pr/checkout.js";
7
+ export async function prepareLocalCodegraph(input) {
8
+ if (!input.enabled) {
9
+ return { state: "disabled", reason: null, tools: [] };
10
+ }
11
+ const run = input.run ?? runCommand;
12
+ await ensureCodegraphGitExclude(input.gitRoot, run);
13
+ const resolved = await ensureCodegraphForReview({
14
+ allowInstall: input.allowInstall,
15
+ interactive: input.interactive,
16
+ log: (message) => log("codegraph", message.replace(/^\[codegraph\]\s*/, "")),
17
+ });
18
+ if (!("path" in resolved)) {
19
+ log("codegraph", `tools unavailable · ${resolved.reason}`);
20
+ return { state: "unavailable", reason: resolved.reason, tools: [] };
21
+ }
22
+ const binary = resolved.path;
23
+ const cgRun = createCodegraphRunner(LOCAL_CODEGRAPH_DIR);
24
+ try {
25
+ const fresh = !(await pathExists(`${input.gitRoot}/${LOCAL_CODEGRAPH_DIR}`));
26
+ if (fresh) {
27
+ log("codegraph", "building code index for the first time (this can take a few minutes)");
28
+ const stop = startHeartbeat("codegraph init");
29
+ try {
30
+ await ensureCodegraphIndex(binary, input.gitRoot, cgRun, LOCAL_CODEGRAPH_DIR);
31
+ }
32
+ finally {
33
+ stop();
34
+ }
35
+ }
36
+ else {
37
+ await ensureCodegraphIndex(binary, input.gitRoot, cgRun, LOCAL_CODEGRAPH_DIR);
38
+ }
39
+ }
40
+ catch (error) {
41
+ const reason = error instanceof Error ? error.message : String(error);
42
+ const line = reason.split("\n")[0] || "codegraph index failed";
43
+ log("codegraph", `tools unavailable · ${line}`);
44
+ return { state: "unavailable", reason: line, tools: [] };
45
+ }
46
+ return {
47
+ state: "used",
48
+ reason: null,
49
+ tools: codegraphTools(binary, input.gitRoot, cgRun),
50
+ };
51
+ }
@@ -0,0 +1,12 @@
1
+ import { type Run } from "../pr/checkout.ts";
2
+ export declare function gitRoot(cwd: string, run?: Run): Promise<string>;
3
+ export declare class ReviewCliError extends Error {
4
+ code: string;
5
+ hint?: string;
6
+ exitCode: number;
7
+ constructor(code: string, message: string, hint?: string, exitCode?: number);
8
+ }
9
+ export declare function assertGitQuiet(cwd: string, run?: Run): Promise<void>;
10
+ export declare function detectRemoteRepo(cwd: string, override?: string, run?: Run): Promise<string>;
11
+ export declare function currentBranch(cwd: string, override?: string, run?: Run): Promise<string>;
12
+ export declare function headSha(cwd: string, run?: Run): Promise<string>;
@@ -0,0 +1,98 @@
1
+ import { runCommand } from "../pr/checkout.js";
2
+ import { normalizeGithubRemote } from "./git_parse.js";
3
+ import { stat } from "../util/runtime.js";
4
+ export async function gitRoot(cwd, run = runCommand) {
5
+ const result = await run("git", ["rev-parse", "--show-toplevel"], cwd);
6
+ if (result.code !== 0) {
7
+ throw new ReviewCliError("not_a_git_repo", "This directory is not inside a git repository.");
8
+ }
9
+ return result.stdout.trim();
10
+ }
11
+ export class ReviewCliError extends Error {
12
+ code;
13
+ hint;
14
+ exitCode;
15
+ constructor(code, message, hint, exitCode = 2) {
16
+ super(message);
17
+ this.code = code;
18
+ this.hint = hint;
19
+ this.exitCode = exitCode;
20
+ }
21
+ }
22
+ export async function assertGitQuiet(cwd, run = runCommand) {
23
+ const heads = ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"];
24
+ for (const name of heads) {
25
+ const path = await run("git", ["rev-parse", "--git-path", name], cwd);
26
+ if (path.code !== 0)
27
+ continue;
28
+ try {
29
+ await stat(path.stdout.trim());
30
+ throw new ReviewCliError("git_operation_in_progress", "Finish the in-progress git operation before running review.");
31
+ }
32
+ catch (error) {
33
+ if (error instanceof ReviewCliError)
34
+ throw error;
35
+ }
36
+ }
37
+ for (const dir of ["rebase-merge", "rebase-apply"]) {
38
+ const path = await run("git", ["rev-parse", "--git-path", dir], cwd);
39
+ if (path.code !== 0)
40
+ continue;
41
+ try {
42
+ const info = await stat(path.stdout.trim());
43
+ if (info.isDirectory()) {
44
+ throw new ReviewCliError("git_operation_in_progress", "Finish the in-progress rebase before running review.");
45
+ }
46
+ }
47
+ catch (error) {
48
+ if (error instanceof ReviewCliError)
49
+ throw error;
50
+ }
51
+ }
52
+ const conflicts = await run("git", ["diff", "--name-only", "--diff-filter=U"], cwd);
53
+ if (conflicts.stdout.trim()) {
54
+ throw new ReviewCliError("git_operation_in_progress", "Resolve merge conflicts before running review.");
55
+ }
56
+ }
57
+ export async function detectRemoteRepo(cwd, override, run = runCommand) {
58
+ if (override) {
59
+ if (!/^[^/]+\/[^/]+$/.test(override)) {
60
+ throw new ReviewCliError("usage", "Repository must look like owner/repo");
61
+ }
62
+ return override;
63
+ }
64
+ const remotes = await run("git", ["remote"], cwd);
65
+ const names = remotes.stdout
66
+ .split("\n")
67
+ .map((l) => l.trim())
68
+ .filter(Boolean);
69
+ const pick = ["upstream", "origin"].find((n) => names.includes(n)) ??
70
+ (names.length === 1 ? names[0] : undefined);
71
+ if (!pick) {
72
+ throw new ReviewCliError("repo_not_detected", "Could not detect the GitHub repository from remotes.", "--repo=owner/repo");
73
+ }
74
+ const url = await run("git", ["remote", "get-url", pick], cwd);
75
+ if (url.code !== 0) {
76
+ throw new ReviewCliError("repo_not_detected", "Could not read git remote URL.");
77
+ }
78
+ const normalized = normalizeGithubRemote(url.stdout.trim());
79
+ if (!normalized.ok) {
80
+ throw new ReviewCliError("unsupported_host", "Only github.com remotes are supported for local review.");
81
+ }
82
+ return normalized.fullName;
83
+ }
84
+ export async function currentBranch(cwd, override, run = runCommand) {
85
+ if (override)
86
+ return override;
87
+ const result = await run("git", ["symbolic-ref", "--short", "-q", "HEAD"], cwd);
88
+ if (result.code !== 0 || !result.stdout.trim()) {
89
+ throw new ReviewCliError("detached_head", "Detached HEAD; pass --branch=<name> to review.");
90
+ }
91
+ return result.stdout.trim();
92
+ }
93
+ export async function headSha(cwd, run = runCommand) {
94
+ const result = await run("git", ["rev-parse", "HEAD"], cwd);
95
+ if (result.code !== 0)
96
+ throw new ReviewCliError("internal", "Could not read HEAD.");
97
+ return result.stdout.trim();
98
+ }
@@ -0,0 +1,27 @@
1
+ /** Pure parsers for git -z output and remote URL normalization (plan §10). */
2
+ export type NameStatusEntry = {
3
+ status: "added" | "modified" | "removed" | "renamed";
4
+ path: string;
5
+ previousPath?: string;
6
+ };
7
+ export type NumstatEntry = {
8
+ additions: number;
9
+ deletions: number;
10
+ binary: boolean;
11
+ };
12
+ export type NormalizeRemoteResult = {
13
+ ok: true;
14
+ fullName: string;
15
+ } | {
16
+ ok: false;
17
+ code: "unsupported_host";
18
+ };
19
+ export declare function normalizeGithubRemote(url: string): NormalizeRemoteResult;
20
+ /** Parse `git diff --name-status -z` records (NUL-separated). */
21
+ export declare function parseNameStatusZ(raw: string): NameStatusEntry[];
22
+ /** Parse `git diff --numstat -z` (`add\tdelete\tpath\0` per file). */
23
+ export declare function parseNumstatZ(raw: string): Map<string, NumstatEntry>;
24
+ /** Paths from name-status that have no entry in a bulk unified diff (plan §10.7). */
25
+ export declare function pathsMissingPatches(statuses: NameStatusEntry[], patches: Map<string, string>): string[];
26
+ /** Split unified diff output into per-file patch bodies (without diff --git header). */
27
+ export declare function splitDiffPatches(raw: string): Map<string, string>;
@@ -0,0 +1,131 @@
1
+ /** Pure parsers for git -z output and remote URL normalization (plan §10). */
2
+ export function normalizeGithubRemote(url) {
3
+ const trimmed = url.trim().replace(/\/+$/, "");
4
+ let host = "";
5
+ let path = "";
6
+ if (trimmed.startsWith("git@")) {
7
+ const match = /^git@([^:]+):(.+)$/i.exec(trimmed);
8
+ if (!match)
9
+ return { ok: false, code: "unsupported_host" };
10
+ host = match[1];
11
+ path = match[2];
12
+ }
13
+ else if (trimmed.startsWith("ssh://")) {
14
+ try {
15
+ const parsed = new URL(trimmed);
16
+ host = parsed.hostname;
17
+ path = parsed.pathname.replace(/^\//, "");
18
+ }
19
+ catch {
20
+ return { ok: false, code: "unsupported_host" };
21
+ }
22
+ }
23
+ else if (/^https?:\/\//i.test(trimmed)) {
24
+ try {
25
+ const parsed = new URL(trimmed);
26
+ host = parsed.hostname;
27
+ path = parsed.pathname.replace(/^\//, "");
28
+ }
29
+ catch {
30
+ return { ok: false, code: "unsupported_host" };
31
+ }
32
+ }
33
+ else {
34
+ return { ok: false, code: "unsupported_host" };
35
+ }
36
+ if (host.toLowerCase() !== "github.com") {
37
+ return { ok: false, code: "unsupported_host" };
38
+ }
39
+ if (path.endsWith(".git"))
40
+ path = path.slice(0, -4);
41
+ const parts = path.split("/").filter(Boolean);
42
+ if (parts.length < 2)
43
+ return { ok: false, code: "unsupported_host" };
44
+ const owner = parts[0];
45
+ const repo = parts[1];
46
+ return { ok: true, fullName: `${owner}/${repo}` };
47
+ }
48
+ /** Parse `git diff --name-status -z` records (NUL-separated). */
49
+ export function parseNameStatusZ(raw) {
50
+ if (!raw)
51
+ return [];
52
+ const parts = raw.split("\0").filter((p) => p.length > 0);
53
+ const entries = [];
54
+ for (let i = 0; i < parts.length; i++) {
55
+ const token = parts[i];
56
+ if (token.startsWith("R")) {
57
+ const path = parts[i + 2];
58
+ const previousPath = parts[i + 1];
59
+ if (!path || !previousPath)
60
+ break;
61
+ entries.push({ status: "renamed", path, previousPath });
62
+ i += 2;
63
+ continue;
64
+ }
65
+ if (token === "T") {
66
+ const path = parts[i + 1];
67
+ if (!path)
68
+ break;
69
+ entries.push({ status: "modified", path });
70
+ i += 1;
71
+ continue;
72
+ }
73
+ const path = parts[i + 1];
74
+ if (!path)
75
+ break;
76
+ if (token === "A")
77
+ entries.push({ status: "added", path });
78
+ else if (token === "M")
79
+ entries.push({ status: "modified", path });
80
+ else if (token === "D")
81
+ entries.push({ status: "removed", path });
82
+ i += 1;
83
+ }
84
+ return entries;
85
+ }
86
+ /** Parse `git diff --numstat -z` (`add\tdelete\tpath\0` per file). */
87
+ export function parseNumstatZ(raw) {
88
+ const map = new Map();
89
+ for (const record of raw.split("\0").filter((part) => part.length > 0)) {
90
+ const match = /^([^\t]*)\t([^\t]*)\t(.*)$/.exec(record);
91
+ if (!match)
92
+ continue;
93
+ const addRaw = match[1];
94
+ const delRaw = match[2];
95
+ const path = match[3];
96
+ const binary = addRaw === "-" && delRaw === "-";
97
+ map.set(path, {
98
+ additions: binary ? 0 : Number(addRaw),
99
+ deletions: binary ? 0 : Number(delRaw),
100
+ binary,
101
+ });
102
+ }
103
+ return map;
104
+ }
105
+ /** Paths from name-status that have no entry in a bulk unified diff (plan §10.7). */
106
+ export function pathsMissingPatches(statuses, patches) {
107
+ return statuses
108
+ .filter((entry) => entry.status !== "removed")
109
+ .map((entry) => entry.path)
110
+ .filter((path) => !patches.has(path) || patches.get(path) === "");
111
+ }
112
+ /** Split unified diff output into per-file patch bodies (without diff --git header). */
113
+ export function splitDiffPatches(raw) {
114
+ const map = new Map();
115
+ const chunks = raw.split(/^diff --git /m).filter(Boolean);
116
+ for (const chunk of chunks) {
117
+ const headerEnd = chunk.indexOf("\n");
118
+ if (headerEnd === -1)
119
+ continue;
120
+ const header = chunk.slice(0, headerEnd);
121
+ const pathMatch = /^a\/(.+?) b\/(.+)$/m.exec(header) ??
122
+ /^a\/(.+?) b\/(.+)$/m.exec(header.replace(/\t.*$/, ""));
123
+ const path = pathMatch?.[2] ?? pathMatch?.[1];
124
+ if (!path)
125
+ continue;
126
+ const body = chunk.slice(headerEnd + 1);
127
+ const patchStart = body.indexOf("@@");
128
+ map.set(path, patchStart === -1 ? "" : body.slice(patchStart));
129
+ }
130
+ return map;
131
+ }
@@ -0,0 +1,15 @@
1
+ import type { Run } from "../pr/checkout.ts";
2
+ import { type Revision } from "../review/revision.ts";
3
+ import { type UntrackedWarning } from "./git_untracked.ts";
4
+ /** Single-file unified patch when bulk `git diff` did not map a path (plan §10.7). */
5
+ export declare function perFileUnifiedPatch(cwd: string, base: string, path: string, run: Run): Promise<string>;
6
+ export declare function mergeBase(cwd: string, baseRef: string, remote: string, run: Run): Promise<string>;
7
+ export declare function resolveBaseRef(cwd: string, remote: string, toBranch: string | undefined, run: Run): Promise<{
8
+ ref: string;
9
+ label: string;
10
+ }>;
11
+ export type LocalRevisionBuild = {
12
+ revision: Revision;
13
+ warnings: UntrackedWarning[];
14
+ };
15
+ export declare function buildLocalRevision(cwd: string, mergeBaseSha: string, baseLabel: string, run: Run): Promise<LocalRevisionBuild>;