co-maintainer 0.5.0 → 0.5.1-beta.2

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 (177) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +5 -3
  3. package/dist/package.json +1 -1
  4. package/dist/src/ai/anthropic.d.ts +55 -0
  5. package/dist/src/ai/anthropic.js +262 -0
  6. package/dist/src/ai/batch.d.ts +2 -2
  7. package/dist/src/ai/batch.js +1 -1
  8. package/dist/src/ai/estimate.d.ts +4 -0
  9. package/dist/src/ai/estimate.js +17 -2
  10. package/dist/src/ai/mermaid_loop.js +7 -4
  11. package/dist/src/ai/openai.d.ts +45 -0
  12. package/dist/src/ai/openai.js +206 -0
  13. package/dist/src/ai/opencode.d.ts +10 -0
  14. package/dist/src/ai/opencode.js +76 -0
  15. package/dist/src/ai/openrouter.d.ts +23 -7
  16. package/dist/src/ai/openrouter.js +39 -23
  17. package/dist/src/ai/pricing.js +1 -1
  18. package/dist/src/ai/provider.d.ts +9 -0
  19. package/dist/src/ai/provider.js +42 -3
  20. package/dist/src/ai/verify.d.ts +9 -1
  21. package/dist/src/ai/verify.js +81 -3
  22. package/dist/src/cli/args.d.ts +1 -2
  23. package/dist/src/cli/args.js +38 -26
  24. package/dist/src/cli/commands/config.d.ts +1 -1
  25. package/dist/src/cli/commands/config.js +10 -4
  26. package/dist/src/cli/commands/probe.js +3 -11
  27. package/dist/src/cli/commands/registry.d.ts +1 -9
  28. package/dist/src/cli/commands/registry.js +75 -10
  29. package/dist/src/cli/commands/review.js +18 -13
  30. package/dist/src/cli/commands/rollback.d.ts +1 -0
  31. package/dist/src/cli/commands/rollback.js +42 -0
  32. package/dist/src/cli/commands/serve.js +2 -3
  33. package/dist/src/cli/commands/set.js +71 -7
  34. package/dist/src/cli/commands/view.js +10 -9
  35. package/dist/src/cli/error.d.ts +4 -4
  36. package/dist/src/cli/error.js +4 -4
  37. package/dist/src/cli/main.d.ts +4 -4
  38. package/dist/src/cli/main.js +13 -8
  39. package/dist/src/cli/prompt.js +1 -1
  40. package/dist/src/cli/review_args.d.ts +9 -3
  41. package/dist/src/cli/review_args.js +28 -5
  42. package/dist/src/cli/review_github.d.ts +31 -0
  43. package/dist/src/cli/review_github.js +128 -0
  44. package/dist/src/cli/review_result.d.ts +27 -16
  45. package/dist/src/cli/review_result.js +7 -7
  46. package/dist/src/config.d.ts +28 -6
  47. package/dist/src/config.js +15 -1
  48. package/dist/src/git/default_branch.d.ts +1 -1
  49. package/dist/src/git/default_branch.js +1 -1
  50. package/dist/src/github/app.js +2 -2
  51. package/dist/src/github/app_manifest.d.ts +1 -7
  52. package/dist/src/github/app_manifest.js +2 -7
  53. package/dist/src/github/client.d.ts +4 -1
  54. package/dist/src/github/client.js +14 -2
  55. package/dist/src/github/gh.js +10 -7
  56. package/dist/src/github/pat.js +4 -4
  57. package/dist/src/github/permissions.d.ts +36 -0
  58. package/dist/src/github/permissions.js +334 -0
  59. package/dist/src/knowledge/facts.js +1 -1
  60. package/dist/src/knowledge/guide.d.ts +3 -3
  61. package/dist/src/knowledge/guide.js +5 -5
  62. package/dist/src/knowledge/probe.js +1 -2
  63. package/dist/src/knowledge/sections.d.ts +3 -3
  64. package/dist/src/knowledge/sections.js +3 -3
  65. package/dist/src/knowledge/skill.d.ts +1 -1
  66. package/dist/src/knowledge/skill.js +4 -4
  67. package/dist/src/knowledge/synthesis.d.ts +1 -1
  68. package/dist/src/knowledge/synthesis.js +5 -5
  69. package/dist/src/knowledge/types.d.ts +1 -1
  70. package/dist/src/local/codegraph_exclude.d.ts +1 -1
  71. package/dist/src/local/codegraph_exclude.js +1 -1
  72. package/dist/src/local/codegraph_prepare.js +1 -1
  73. package/dist/src/local/git_ops.d.ts +1 -1
  74. package/dist/src/local/git_ops.js +1 -1
  75. package/dist/src/local/git_parse.d.ts +2 -2
  76. package/dist/src/local/git_parse.js +2 -2
  77. package/dist/src/local/git_revision.d.ts +1 -1
  78. package/dist/src/local/git_revision.js +1 -1
  79. package/dist/src/local/git_untracked.d.ts +1 -1
  80. package/dist/src/local/git_untracked.js +1 -1
  81. package/dist/src/local/review_local.js +17 -14
  82. package/dist/src/local/review_lock.d.ts +1 -1
  83. package/dist/src/local/review_lock.js +1 -1
  84. package/dist/src/pr/checkout.js +2 -2
  85. package/dist/src/pr/findings.js +1 -1
  86. package/dist/src/pr/findings_json.d.ts +2 -2
  87. package/dist/src/pr/findings_json.js +1 -1
  88. package/dist/src/pr/github_revision.js +1 -1
  89. package/dist/src/pr/review_copy.d.ts +2 -2
  90. package/dist/src/pr/review_copy.js +2 -2
  91. package/dist/src/pr/reviewer.d.ts +6 -0
  92. package/dist/src/pr/reviewer.js +49 -39
  93. package/dist/src/remote/client.d.ts +4 -0
  94. package/dist/src/remote/client.js +84 -23
  95. package/dist/src/remote/fixtures.js +1 -1
  96. package/dist/src/remote/http.d.ts +10 -1
  97. package/dist/src/remote/http.js +15 -3
  98. package/dist/src/remote/schema.d.ts +3 -3
  99. package/dist/src/remote/schema.js +3 -3
  100. package/dist/src/remote/server/body.d.ts +1 -1
  101. package/dist/src/remote/server/body.js +1 -1
  102. package/dist/src/remote/server/byok_keys.d.ts +15 -0
  103. package/dist/src/remote/server/byok_keys.js +40 -0
  104. package/dist/src/remote/server/guides.js +2 -2
  105. package/dist/src/remote/server/routes.js +8 -1
  106. package/dist/src/remote/validate.js +15 -1
  107. package/dist/src/review/blocking.d.ts +2 -2
  108. package/dist/src/review/blocking.js +2 -2
  109. package/dist/src/review/carry_over.d.ts +2 -2
  110. package/dist/src/review/carry_over.js +2 -2
  111. package/dist/src/review/guides.js +1 -1
  112. package/dist/src/server/api/remote_tokens.js +2 -0
  113. package/dist/src/server/api/repos.js +51 -0
  114. package/dist/src/server/api/settings.js +10 -0
  115. package/dist/src/server/errors.d.ts +1 -1
  116. package/dist/src/server/http.js +1 -1
  117. package/dist/src/server/pages/activity.d.ts +2 -0
  118. package/dist/src/server/pages/activity.js +18 -4
  119. package/dist/src/server/pages/add_repo.js +61 -1
  120. package/dist/src/server/pages/analytics.js +18 -8
  121. package/dist/src/server/pages/cost.d.ts +21 -0
  122. package/dist/src/server/pages/cost.js +72 -0
  123. package/dist/src/server/pages/pr.js +5 -4
  124. package/dist/src/server/pages/repo.js +3 -2
  125. package/dist/src/server/pages/repo_prs.js +3 -2
  126. package/dist/src/server/pages/repo_remote.js +3 -2
  127. package/dist/src/server/pages/repo_settings.js +326 -12
  128. package/dist/src/server/pages/router.js +3 -2
  129. package/dist/src/server/pages/settings.js +50 -6
  130. package/dist/src/server/pages/styles.d.ts +1 -1
  131. package/dist/src/server/pages/styles.js +1 -1
  132. package/dist/src/services/credentials.js +8 -11
  133. package/dist/src/services/dashboard.d.ts +22 -9
  134. package/dist/src/services/dashboard.js +16 -2
  135. package/dist/src/services/probe.js +1 -1
  136. package/dist/src/services/remote_review.js +142 -35
  137. package/dist/src/services/remote_review_abort.d.ts +5 -1
  138. package/dist/src/services/remote_review_abort.js +9 -1
  139. package/dist/src/services/review.d.ts +1 -1
  140. package/dist/src/services/review.js +19 -5
  141. package/dist/src/services/review_policy.d.ts +116 -0
  142. package/dist/src/services/review_policy.js +489 -0
  143. package/dist/src/services/setup.d.ts +4 -3
  144. package/dist/src/services/setup.js +18 -16
  145. package/dist/src/services/setup_checklist.js +2 -2
  146. package/dist/src/services/webhook.d.ts +23 -0
  147. package/dist/src/services/webhook.js +141 -34
  148. package/dist/src/store/app_db.d.ts +5 -0
  149. package/dist/src/store/app_db.js +69 -13
  150. package/dist/src/store/backup.d.ts +31 -0
  151. package/dist/src/store/backup.js +158 -0
  152. package/dist/src/store/migrations.d.ts +1 -1
  153. package/dist/src/store/migrations.js +21 -1
  154. package/dist/src/store/remote_review_inputs.d.ts +3 -0
  155. package/dist/src/store/remote_review_inputs.js +3 -3
  156. package/dist/src/store/remote_tokens.d.ts +1 -1
  157. package/dist/src/store/remote_tokens.js +1 -1
  158. package/dist/src/store/repos.d.ts +2 -2
  159. package/dist/src/store/repos.js +1 -1
  160. package/dist/src/store/reviews.d.ts +21 -13
  161. package/dist/src/store/reviews.js +60 -15
  162. package/dist/src/store/rows.d.ts +10 -0
  163. package/dist/src/tools/codegraph.d.ts +4 -4
  164. package/dist/src/tools/codegraph.js +5 -5
  165. package/dist/src/tools/codegraph_exec.d.ts +1 -1
  166. package/dist/src/tools/codegraph_exec.js +1 -1
  167. package/dist/src/types.d.ts +12 -6
  168. package/dist/src/util/cost.d.ts +52 -0
  169. package/dist/src/util/cost.js +70 -0
  170. package/dist/src/util/log.d.ts +1 -1
  171. package/dist/src/util/log.js +2 -2
  172. package/dist/src/util/redact.js +2 -0
  173. package/dist/src/util/run_summary.d.ts +7 -2
  174. package/dist/src/util/run_summary.js +28 -2
  175. package/dist/src/util/time.d.ts +1 -1
  176. package/dist/src/util/time.js +1 -1
  177. package/package.json +1 -1
@@ -0,0 +1,42 @@
1
+ /** `co-maintainer rollback`: put back the backup an upgrade took.
2
+ *
3
+ * Only the previous version, and only while the version that made the backup
4
+ * is the one running, because that is the code that knows both the old and the
5
+ * new structure. Data written since the upgrade is lost, so it asks first. */
6
+ import { CliError, EXIT_USAGE } from "../error.js";
7
+ import { askConfirm } from "../prompt.js";
8
+ import { unknownOptionMessage } from "./registry.js";
9
+ import { canPrompt } from "../../tools/codegraph.js";
10
+ import { VERSION } from "../../version.js";
11
+ import { backupPaths, liveLockPid } from "../../store/app_db.js";
12
+ import { checkRollback, restoreBackup, rolledBackDir, } from "../../store/backup.js";
13
+ export async function runRollback(args) {
14
+ for (const arg of args) {
15
+ if (arg !== "--yes") {
16
+ throw new CliError("usage", unknownOptionMessage(arg, "rollback"));
17
+ }
18
+ }
19
+ const paths = backupPaths();
20
+ const manifest = await checkRollback(paths, VERSION);
21
+ const pid = await liveLockPid();
22
+ if (pid !== undefined) {
23
+ throw new CliError("serve_already_running", `A co-maintainer serve process (pid ${pid}) is using this data directory.`, "Stop it, then run rollback again.", EXIT_USAGE);
24
+ }
25
+ console.log(`Rolling back to the backup taken on ${manifest.createdAt}, before the upgrade to ${manifest.toVersion}.`);
26
+ console.log("Everything written since then is lost: reviews, settings and cache.");
27
+ console.log(`The current files are kept in ${rolledBackDir(paths)}.`);
28
+ if (!args.includes("--yes")) {
29
+ if (!canPrompt()) {
30
+ throw new CliError("confirmation_required", "Rollback needs your confirmation.", "Run it in a terminal, or pass --yes.", EXIT_USAGE);
31
+ }
32
+ if (!(await askConfirm("Roll back?"))) {
33
+ console.log("Nothing changed.");
34
+ return;
35
+ }
36
+ }
37
+ await restoreBackup(paths);
38
+ console.log("Restored.");
39
+ console.log(manifest.fromVersion
40
+ ? `Now install that version: npm i -g co-maintainer@${manifest.fromVersion}`
41
+ : "Now install the version you upgraded from.");
42
+ }
@@ -212,9 +212,8 @@ export async function runServe(args) {
212
212
  // Ctrl+C in an interactive terminal fires this reliably everywhere,
213
213
  // including Windows. A SIGTERM sent by another process (a process
214
214
  // manager, `kill`, `taskkill`) is reliable on Linux but not on Windows,
215
- // where it forcibly terminates the process before any handler can run —
216
- // see the P3 Live note in PLAN.md. Registering it anyway costs nothing
217
- // and is correct wherever it does work.
215
+ // where it forcibly terminates the process before any handler can run.
216
+ // Registering it anyway costs nothing and is correct wherever it does work.
218
217
  for (const signal of ["SIGINT", "SIGTERM"]) {
219
218
  process.on(signal, () => void shutdown(signal));
220
219
  }
@@ -1,8 +1,11 @@
1
1
  import { readConfig, configPath, writeUserConfig } from "../../config.js";
2
2
  import { hashPassword, passwordProblem } from "../../util/password.js";
3
3
  import { readTextFile } from "../../util/runtime.js";
4
- import { verifyOpenRouter } from "../../ai/verify.js";
4
+ import { verifyAnthropic, verifyOpenAi, verifyOpenRouter, } from "../../ai/verify.js";
5
+ import { verifyOpenCode } from "../../ai/opencode.js";
5
6
  import { die } from "../error.js";
7
+ import { parsePolicy } from "../../services/review_policy.js";
8
+ import { AI_PROVIDERS, rejectRetiredProvider } from "../../ai/provider.js";
6
9
  import { renderCommandHelp, renderGlobalHelp } from "./registry.js";
7
10
  function text(args, name) {
8
11
  return args
@@ -17,6 +20,7 @@ const secretFields = new Set([
17
20
  "githubOAuthClientSecret",
18
21
  "remoteToken",
19
22
  "dashboardPasswordHash",
23
+ "remoteByok",
20
24
  ]);
21
25
  /** `co-maintainer set --token=... --ai=... --low-model=... --high-model=...
22
26
  * --auth=... --github-app-id=... --github-app-private-key=... (or
@@ -50,6 +54,11 @@ export async function runSet(args) {
50
54
  "enable-auth",
51
55
  "remote-host",
52
56
  "remote-token",
57
+ "remote-byok-policy",
58
+ "remote-byok",
59
+ "remote-byok-default",
60
+ "review-policy",
61
+ "review-policy-file",
53
62
  "review-blocking",
54
63
  "password",
55
64
  "unset",
@@ -64,8 +73,9 @@ export async function runSet(args) {
64
73
  }
65
74
  }
66
75
  const ai = text(args, "ai");
67
- if (ai && !["none", "openrouter", "hetzner"].includes(ai)) {
68
- die("--ai must be one of: none, openrouter, hetzner");
76
+ rejectRetiredProvider(ai);
77
+ if (ai && !AI_PROVIDERS.includes(ai)) {
78
+ die(`--ai must be one of: ${AI_PROVIDERS.join(", ")}`);
69
79
  }
70
80
  const auth = text(args, "auth");
71
81
  if (auth && !["gh", "pat"].includes(auth)) {
@@ -98,6 +108,10 @@ export async function runSet(args) {
98
108
  "enable-auth": "githubAuthEnabled",
99
109
  "remote-host": "remoteHost",
100
110
  "remote-token": "remoteToken",
111
+ "remote-byok-policy": "remoteByokPolicy",
112
+ "remote-byok": "remoteByok",
113
+ "remote-byok-default": "remoteByokDefault",
114
+ "review-policy": "reviewPolicy",
101
115
  "review-blocking": "reviewBlocking",
102
116
  password: "dashboardPasswordHash",
103
117
  };
@@ -181,6 +195,47 @@ export async function runSet(args) {
181
195
  const remoteToken = text(args, "remote-token");
182
196
  if (remoteToken)
183
197
  patch.remoteToken = remoteToken;
198
+ const remoteByokPolicy = text(args, "remote-byok-policy");
199
+ if (remoteByokPolicy) {
200
+ if (!["off", "allow", "require"].includes(remoteByokPolicy)) {
201
+ die("--remote-byok-policy must be one of: off, allow, require");
202
+ }
203
+ patch.remoteByokPolicy = remoteByokPolicy;
204
+ }
205
+ const remoteByok = text(args, "remote-byok");
206
+ if (remoteByok)
207
+ patch.remoteByok = remoteByok;
208
+ const remoteByokDefault = text(args, "remote-byok-default");
209
+ if (remoteByokDefault) {
210
+ if (!["on", "off"].includes(remoteByokDefault)) {
211
+ die("--remote-byok-default must be one of: on, off");
212
+ }
213
+ patch.remoteByokDefault = remoteByokDefault === "on";
214
+ }
215
+ const reviewPolicy = text(args, "review-policy");
216
+ const reviewPolicyFile = text(args, "review-policy-file");
217
+ if (reviewPolicy && reviewPolicyFile) {
218
+ die("Pass only one of --review-policy or --review-policy-file");
219
+ }
220
+ if (reviewPolicy) {
221
+ const checked = parsePolicy(reviewPolicy);
222
+ if (!checked.ok)
223
+ die(`--review-policy: ${checked.problem}`);
224
+ patch.reviewPolicy = reviewPolicy;
225
+ }
226
+ if (reviewPolicyFile) {
227
+ let parsed;
228
+ try {
229
+ parsed = JSON.parse(await readTextFile(reviewPolicyFile));
230
+ }
231
+ catch (error) {
232
+ die(`Could not read a JSON policy from ${reviewPolicyFile}: ${String(error)}`);
233
+ }
234
+ const checked = parsePolicy(parsed);
235
+ if (!checked.ok)
236
+ die(`--review-policy-file: ${checked.problem}`);
237
+ patch.reviewPolicy = parsed;
238
+ }
184
239
  const reviewBlocking = text(args, "review-blocking");
185
240
  if (reviewBlocking) {
186
241
  if (!["model", "severity"].includes(reviewBlocking)) {
@@ -199,11 +254,12 @@ export async function runSet(args) {
199
254
  die("Nothing to set. Pass --token= (or --ai-key=), --ai=, --low-model=, --high-model=, --auth=, --github-pat=, " +
200
255
  "--github-app-id=, --github-app-private-key(-file|-path)=, --github-webhook-secret=, " +
201
256
  "--github-oauth-client-id=, --github-oauth-client-secret=, --github-oauth-allowed-user=, " +
202
- "--remote-host=, --remote-token=, --review-blocking=model|severity, " +
257
+ "--remote-host=, --remote-token=, --remote-byok-policy=off|allow|require, " +
258
+ "--review-blocking=model|severity, " +
203
259
  "--password=, --disable-auth=password, --enable-auth=github, or --unset=name");
204
260
  }
205
261
  // Verify the key and model before writing, so a typo fails here instead of
206
- // after a review has already fetched and cloned the pull request (CORE-22).
262
+ // after a review has already fetched and cloned the pull request.
207
263
  // A field present in the patch wins, including when its value is `undefined`
208
264
  // — that is an unset, and there is nothing left to verify.
209
265
  const before = readConfig();
@@ -211,8 +267,16 @@ export async function runSet(args) {
211
267
  const effectiveAi = effective("ai");
212
268
  const effectiveToken = effective("token");
213
269
  const effectiveHighModel = effective("highModel");
214
- if (verify && effectiveAi === "openrouter" && effectiveToken) {
215
- const result = await verifyOpenRouter(String(effectiveToken), effectiveHighModel === undefined ? undefined : String(effectiveHighModel));
270
+ const verifiers = {
271
+ openrouter: verifyOpenRouter,
272
+ openai: verifyOpenAi,
273
+ anthropic: verifyAnthropic,
274
+ "opencode-zen": (key, model) => verifyOpenCode("opencode-zen", key, model),
275
+ "opencode-go": (key, model) => verifyOpenCode("opencode-go", key, model),
276
+ };
277
+ const verifyProvider = typeof effectiveAi === "string" ? verifiers[effectiveAi] : undefined;
278
+ if (verify && verifyProvider && effectiveToken) {
279
+ const result = await verifyProvider(String(effectiveToken), effectiveHighModel === undefined ? undefined : String(effectiveHighModel));
216
280
  if (result.status === "rejected")
217
281
  throw result.error;
218
282
  if (result.status === "unreachable") {
@@ -1,13 +1,13 @@
1
- /** `co-maintainer view`: print the guides co-maintainer already generated
2
- * (CORE-23, F25). Read-only — the counterpart to `config` for the generated
1
+ /** `co-maintainer view`: print the guides co-maintainer already generated.
2
+ * Read-only — the counterpart to `config` for the generated
3
3
  * knowledge rather than the settings.
4
4
  *
5
- * Two sources, one presentation (CORE-44): the guide directory on this machine,
5
+ * Two sources, one presentation: the guide directory on this machine,
6
6
  * or the servers a remote review token can read (`--remote`). Local and remote
7
7
  * differ only in where the guide list comes from, so `--list`, a single guide
8
8
  * and the every-guide headers are all rendered by the same code.
9
9
  *
10
- * Naming: the plan calls the guide kinds `skill`, `codebase`,
10
+ * Naming: the guide kinds are `skill`, `codebase`,
11
11
  * `review-guide`, `detailed-guide`; `--list` and the headers show the real
12
12
  * file names, and a kind name is accepted too so a user never has to type
13
13
  * `PR_REVIEW_GUIDE.md` unless they want to. */
@@ -16,7 +16,7 @@ import { detectRemoteRepo } from "../../local/git_ops.js";
16
16
  import { CliError, EXIT_USAGE } from "../error.js";
17
17
  import { renderCommandHelp, unknownOptionMessage } from "./registry.js";
18
18
  import { readDir, readTextFile, stat } from "../../util/runtime.js";
19
- import { baseUrl, die, readApiError, remoteFetch } from "../../remote/http.js";
19
+ import { baseUrl, die, readApiError, remoteFetch, remoteFromEnv, } from "../../remote/http.js";
20
20
  /** The four generated guides, in the order a reader wants them: the
21
21
  * contribution skill first, then the codebase notes, then the two review
22
22
  * guides (short, then the evidence behind it). */
@@ -26,7 +26,7 @@ const GUIDES = [
26
26
  { kind: "review-guide", file: "PR_REVIEW_GUIDE.md" },
27
27
  { kind: "detailed-guide", file: "PR_REVIEW_DETAILED_GUIDE.md" },
28
28
  ];
29
- /** Resolves a user-supplied name to a guide. Accepts the plan's kind names
29
+ /** Resolves a user-supplied name to a guide. Accepts the kind names
30
30
  * (`review-guide`), the file name (`PR_REVIEW_GUIDE.md`), and the bare stem
31
31
  * (`PR_REVIEW_GUIDE`), case-insensitively. */
32
32
  function resolveGuide(name) {
@@ -88,13 +88,14 @@ async function readLocalGuides(repo) {
88
88
  }
89
89
  return guides;
90
90
  }
91
- /** The guides a remote review server holds for `repo` (CORE-44). The token is
91
+ /** The guides a remote review server holds for `repo`. The token is
92
92
  * the same one `review --remote` uses, so a host already configured for remote
93
93
  * review needs no extra setup. */
94
94
  async function readRemoteGuides(repo) {
95
95
  const config = readConfig();
96
- const host = config.remoteHost;
97
- const token = config.remoteToken;
96
+ const fromEnv = remoteFromEnv();
97
+ const host = fromEnv.host ?? config.remoteHost;
98
+ const token = fromEnv.token ?? config.remoteToken;
98
99
  if (!host || !token) {
99
100
  die("remote_not_configured", "Remote review is not configured.", "co-maintainer config set remote-host https://your-server " +
100
101
  "&& co-maintainer config set remote-token <token>");
@@ -1,4 +1,4 @@
1
- /** One error shape for every CLI failure (CORE-10).
1
+ /** One error shape for every CLI failure.
2
2
  *
3
3
  * The message is the first line the user reads, `hint` is an optional
4
4
  * follow-up line printed as `Hint: ...`, and `exitCode` is the documented
@@ -27,13 +27,13 @@ export declare function die(message: string, code?: string): never;
27
27
  * of the intended code. Every provider and remote call uses fetch, so this is
28
28
  * reachable from any error path after a request. Setting `exitCode` and letting
29
29
  * the loop empty is what the probe confirms is crash-free and immediate (the
30
- * undici pool does not keep the process alive). CORE-11. */
30
+ * undici pool does not keep the process alive). */
31
31
  export declare function exitWith(code: number): void;
32
32
  /** Renders an error the way the CLI prints it: the message, then `Hint: ...`
33
33
  * when there is one. Used where an error becomes a log line rather than the
34
- * top-level exit (CORE-12), so a hint is not lost just because the failure was
34
+ * top-level exit, so a hint is not lost just because the failure was
35
35
  * quarantined. */
36
36
  export declare function formatError(error: unknown): string;
37
- /** A failed `fetch` as a message that names the host and the cause (CORE-12).
37
+ /** A failed `fetch` as a message that names the host and the cause.
38
38
  * Node's own text is only `fetch failed`, which says neither. */
39
39
  export declare function networkFailure(url: string, error: unknown): CliError;
@@ -1,4 +1,4 @@
1
- /** One error shape for every CLI failure (CORE-10).
1
+ /** One error shape for every CLI failure.
2
2
  *
3
3
  * The message is the first line the user reads, `hint` is an optional
4
4
  * follow-up line printed as `Hint: ...`, and `exitCode` is the documented
@@ -35,13 +35,13 @@ export function die(message, code = "usage") {
35
35
  * of the intended code. Every provider and remote call uses fetch, so this is
36
36
  * reachable from any error path after a request. Setting `exitCode` and letting
37
37
  * the loop empty is what the probe confirms is crash-free and immediate (the
38
- * undici pool does not keep the process alive). CORE-11. */
38
+ * undici pool does not keep the process alive). */
39
39
  export function exitWith(code) {
40
40
  process.exitCode = code;
41
41
  }
42
42
  /** Renders an error the way the CLI prints it: the message, then `Hint: ...`
43
43
  * when there is one. Used where an error becomes a log line rather than the
44
- * top-level exit (CORE-12), so a hint is not lost just because the failure was
44
+ * top-level exit, so a hint is not lost just because the failure was
45
45
  * quarantined. */
46
46
  export function formatError(error) {
47
47
  if (error instanceof CliError) {
@@ -49,7 +49,7 @@ export function formatError(error) {
49
49
  }
50
50
  return error instanceof Error ? error.message : String(error);
51
51
  }
52
- /** A failed `fetch` as a message that names the host and the cause (CORE-12).
52
+ /** A failed `fetch` as a message that names the host and the cause.
53
53
  * Node's own text is only `fetch failed`, which says neither. */
54
54
  export function networkFailure(url, error) {
55
55
  const code = error.cause?.code;
@@ -1,8 +1,8 @@
1
1
  export declare function run(args: string[]): Promise<void>;
2
- /** The single place a CLI error turns into output and an exit code (CORE-10).
2
+ /** The single place a CLI error turns into output and an exit code.
3
3
  * The message's first line and the optional `Hint:` line keep the 0.4.13
4
4
  * shape. With `--json` the error goes to stdout as `schemaVersion 1` JSON, the
5
- * same shape local review already used. CORE-11 later replaces the direct
6
- * `process.exit` with `exitCode` plus handle cleanup, which is why that work is
7
- * separate. */
5
+ * same shape local review already used. The exit goes through `exitWith`,
6
+ * which sets `exitCode` and lets open handles drain instead of calling
7
+ * `process.exit`. */
8
8
  export declare function reportCliError(error: unknown): void;
@@ -1,7 +1,8 @@
1
1
  /** CLI dispatch only. Each branch parses nothing itself beyond `args[0]` and
2
- * calls straight into a command or a service — see PLAN.md Section 3. */
2
+ * calls straight into a command or a service. */
3
3
  import { parseArgs } from "./args.js";
4
4
  import { runServe } from "./commands/serve.js";
5
+ import { runRollback } from "./commands/rollback.js";
5
6
  import { runSet } from "./commands/set.js";
6
7
  import { runConfig } from "./commands/config.js";
7
8
  import { runView } from "./commands/view.js";
@@ -13,7 +14,7 @@ import { CliError, EXIT_RUNTIME, exitWith } from "./error.js";
13
14
  import { findCommand, renderCommandHelp, renderGlobalHelp, unknownCommandMessage, } from "./commands/registry.js";
14
15
  /** Every command's help is rendered from the registry, so a new flag cannot
15
16
  * drift out of it. `help <command>`, `<command> --help` and `<command> -h` all
16
- * land here and all exit 0 (CORE-20). */
17
+ * land here and all exit 0. */
17
18
  function printHelpFor(name) {
18
19
  const help = renderCommandHelp(name);
19
20
  console.log(help ?? renderGlobalHelp());
@@ -38,7 +39,7 @@ function handleHelp(args) {
38
39
  return true;
39
40
  }
40
41
  // `<command> --help` without a repo: `serve --help` and `set --help` must not
41
- // reach their handlers, which would start a server or demand a flag (CORE-20).
42
+ // reach their handlers, which would start a server or demand a flag.
42
43
  if (first && (args.includes("--help") || args.includes("-h"))) {
43
44
  const spec = findCommand(first);
44
45
  if (spec) {
@@ -71,12 +72,16 @@ export async function run(args) {
71
72
  await runServe(args.slice(1));
72
73
  return;
73
74
  }
75
+ if (args[0] === "rollback") {
76
+ await runRollback(args.slice(1));
77
+ return;
78
+ }
74
79
  if (args[0] === "review") {
75
80
  await runReviewFromCli(args.slice(1));
76
81
  return;
77
82
  }
78
83
  // An unknown command is a usage error with a suggestion, and it must resolve
79
- // through the registry so hidden aliases still work (CORE-20).
84
+ // through the registry so hidden aliases still work.
80
85
  if (args[0] && !args[0].startsWith("-") && !findCommand(args[0])) {
81
86
  throw new CliError("usage", unknownCommandMessage(args[0]));
82
87
  }
@@ -86,12 +91,12 @@ export async function run(args) {
86
91
  else
87
92
  await runInitOrRemake(options);
88
93
  }
89
- /** The single place a CLI error turns into output and an exit code (CORE-10).
94
+ /** The single place a CLI error turns into output and an exit code.
90
95
  * The message's first line and the optional `Hint:` line keep the 0.4.13
91
96
  * shape. With `--json` the error goes to stdout as `schemaVersion 1` JSON, the
92
- * same shape local review already used. CORE-11 later replaces the direct
93
- * `process.exit` with `exitCode` plus handle cleanup, which is why that work is
94
- * separate. */
97
+ * same shape local review already used. The exit goes through `exitWith`,
98
+ * which sets `exitCode` and lets open handles drain instead of calling
99
+ * `process.exit`. */
95
100
  export function reportCliError(error) {
96
101
  const cli = error instanceof CliError
97
102
  ? error
@@ -7,7 +7,7 @@ import { createInterface } from "node:readline/promises";
7
7
  /** Asks one question and always settles.
8
8
  *
9
9
  * `rl.question` never settles when stdin reaches EOF before an answer, which
10
- * is the F01 hang: `co-maintainer review </dev/null` awaited the answer
10
+ * is the hang: `co-maintainer review </dev/null` awaited the answer
11
11
  * forever, printed `Warning: Detected unsettled top-level await`, and exited
12
12
  * 13. Racing the question against `close`/`end` and treating those as "no
13
13
  * answer" keeps the caller's fallback path alive instead. */
@@ -10,10 +10,16 @@ type ReviewFlags = {
10
10
  branch?: string;
11
11
  repoOverride?: string;
12
12
  remakeBeforeReview: boolean;
13
- /** `--remote-host` override for this run only (CORE-25). */
13
+ /** `--remote-host` override for this run only. */
14
14
  remoteHost?: string;
15
- /** `--remote-token` override for this run only (CORE-25). */
15
+ /** `--remote-token` override for this run only. */
16
16
  remoteToken?: string;
17
+ /** `--remote-byok` (`true`) / `--no-remote-byok` (`false`) for this run
18
+ * only. Unset means "use the configured default". */
19
+ remoteByok?: boolean;
20
+ /** `--output=github`: workflow commands and a job summary instead of prose.
21
+ */
22
+ output?: "github";
17
23
  };
18
24
  export type ReviewCliArgs = ({
19
25
  mode: "pr";
@@ -27,6 +33,6 @@ export type ReviewCliArgs = ({
27
33
  } & ReviewFlags);
28
34
  /** Strip local-only flags before `parseArgs` for PR-style options. */
29
35
  export declare function filterReviewConfigArgs(raw: string[]): string[];
30
- /** Parses `co-maintainer review` after the `review` token (plan §8.1). */
36
+ /** Parses `co-maintainer review` after the `review` token. */
31
37
  export declare function parseReviewArgs(args: string[]): Promise<ReviewCliArgs>;
32
38
  export {};
@@ -3,8 +3,10 @@ import { die } from "./error.js";
3
3
  function reviewFlags(rest) {
4
4
  const text = (name) => rest.find((item) => item.startsWith(`--${name}=`))?.slice(name.length + 3);
5
5
  // `--remake-before-review` is the 0.4.13 spelling; both names mean the same
6
- // thing, and only the new one is documented (CORE-21).
6
+ // thing, and only the new one is documented.
7
7
  const syncBeforeReview = rest.some((arg) => arg === "--sync-before-review" || arg === "--remake-before-review");
8
+ const remoteByokOn = rest.includes("--remote-byok");
9
+ const remoteByokOff = rest.includes("--no-remote-byok");
8
10
  for (const arg of rest) {
9
11
  if (arg === "--codegraph") {
10
12
  die("Unknown option: --codegraph");
@@ -13,6 +15,16 @@ function reviewFlags(rest) {
13
15
  die("--sync-before-review cannot be used with --remote");
14
16
  }
15
17
  }
18
+ if (remoteByokOn && remoteByokOff) {
19
+ die("--remote-byok and --no-remote-byok cannot both be used");
20
+ }
21
+ const output = text("output");
22
+ if (output !== undefined && output !== "github") {
23
+ die("--output must be: github");
24
+ }
25
+ if (output !== undefined && rest.includes("--json")) {
26
+ die("--output=github cannot be used with --json");
27
+ }
16
28
  return {
17
29
  json: rest.includes("--json"),
18
30
  remote: rest.includes("--remote"),
@@ -25,6 +37,8 @@ function reviewFlags(rest) {
25
37
  remakeBeforeReview: syncBeforeReview,
26
38
  remoteHost: text("remote-host"),
27
39
  remoteToken: text("remote-token"),
40
+ remoteByok: remoteByokOn ? true : remoteByokOff ? false : undefined,
41
+ output,
28
42
  };
29
43
  }
30
44
  const LOCAL_ONLY_FLAGS = new Set([
@@ -35,6 +49,8 @@ const LOCAL_ONLY_FLAGS = new Set([
35
49
  "--fresh",
36
50
  "--remake-before-review",
37
51
  "--sync-before-review",
52
+ "--remote-byok",
53
+ "--no-remote-byok",
38
54
  ]);
39
55
  /** Strip local-only flags before `parseArgs` for PR-style options. */
40
56
  export function filterReviewConfigArgs(raw) {
@@ -45,22 +61,26 @@ export function filterReviewConfigArgs(raw) {
45
61
  arg.startsWith("--branch=") ||
46
62
  arg.startsWith("--repo=") ||
47
63
  arg.startsWith("--remote-host=") ||
48
- arg.startsWith("--remote-token=")) {
64
+ arg.startsWith("--remote-token=") ||
65
+ arg.startsWith("--output=")) {
49
66
  return false;
50
67
  }
51
68
  return true;
52
69
  });
53
70
  }
54
- /** Parses `co-maintainer review` after the `review` token (plan §8.1). */
71
+ /** Parses `co-maintainer review` after the `review` token. */
55
72
  export async function parseReviewArgs(args) {
56
73
  const positional = args.filter((a) => !a.startsWith("--"));
57
74
  const flags = reviewFlags(args);
58
75
  // `--remote-host` / `--remote-token` only mean something together with
59
- // `--remote`; silently dropping them is exactly the F33 surprise (CORE-25).
76
+ // `--remote`; silently dropping them is exactly the kind of surprise to avoid.
60
77
  if ((flags.remoteHost || flags.remoteToken) && !flags.remote) {
61
78
  die("--remote-host and --remote-token require --remote");
62
79
  }
63
- // `--json` must never prompt (plan §8.7). `parseArgs` only receives the
80
+ if (flags.remoteByok !== undefined && !flags.remote) {
81
+ die("--remote-byok requires --remote");
82
+ }
83
+ // `--json` must never prompt. `parseArgs` only receives the
64
84
  // filtered args, and `--json` is stripped before it sees them, so the flag
65
85
  // has to be applied here rather than after the parse returns.
66
86
  setCliInteractive(!flags.json);
@@ -71,6 +91,9 @@ export async function parseReviewArgs(args) {
71
91
  if (flags.remoteHost || flags.remoteToken) {
72
92
  die("--remote-host is only for remote review without a PR number");
73
93
  }
94
+ if (flags.remoteByok !== undefined) {
95
+ die("--remote-byok is only for remote review without a PR number");
96
+ }
74
97
  const options = await parseArgs([
75
98
  "review",
76
99
  positional[0],
@@ -0,0 +1,31 @@
1
+ import { type RunSummary } from "../util/run_summary.ts";
2
+ import { type HumanFinding } from "./review_result.ts";
3
+ /** GitHub shows at most this many error and this many warning annotations per
4
+ * step and drops the rest without a word. */
5
+ export declare const MAX_ANNOTATIONS_PER_LEVEL = 10;
6
+ /** The message part of a workflow command. GitHub's own toolkit escapes exactly
7
+ * these three, and a `:` or `,` here would show as `%3A` and `%2C` in the text. */
8
+ export declare function escapeData(value: string): string;
9
+ /** A property value (`file`, `title`) also cannot hold `:` or `,`, which end it. */
10
+ export declare function escapeProperty(value: string): string;
11
+ /** One workflow command per finding still standing, errors first. A finding
12
+ * that was closed since the last review needs no annotation. */
13
+ export declare function githubAnnotations(findings: HumanFinding[]): string[];
14
+ export type GithubSummaryInput = {
15
+ /** What was reviewed, for the line under the heading. */
16
+ title: string;
17
+ findings: HumanFinding[];
18
+ /** Cost and time, in the same words the terminal uses. Left out when the run
19
+ * cannot say. */
20
+ run?: RunSummary;
21
+ };
22
+ export declare function githubSummary(input: GithubSummaryInput): string;
23
+ /** Appends to `$GITHUB_STEP_SUMMARY` when it is set. A summary that cannot be
24
+ * written is reported on stderr and never fails the review. */
25
+ export declare function appendStepSummary(markdown: string, path?: string | undefined): void;
26
+ /** The whole `--output=github` output of one finished review. */
27
+ export declare function emitGithubOutput(input: GithubSummaryInput): void;
28
+ /** Progress lines that come from somewhere else (the server's job log) are
29
+ * printed on stderr, and the runner reads workflow commands from stderr too.
30
+ * A line that would start one is given a prefix so it stays plain text. */
31
+ export declare function neutralizeCommands(text: string): string;