@brainervirus/workit-core 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/skills/workit-babysit/SKILL.md +20 -26
  3. package/skills/workit-behavioral-tdd/SKILL.md +0 -5
  4. package/skills/workit-blast-radius/SKILL.md +0 -5
  5. package/skills/workit-challenge/SKILL.md +30 -47
  6. package/skills/workit-debug/SKILL.md +5 -27
  7. package/skills/workit-deslop/SKILL.md +0 -5
  8. package/skills/workit-diagram/SKILL.md +0 -5
  9. package/skills/workit-green-run/SKILL.md +0 -5
  10. package/skills/workit-handoff/SKILL.md +2 -8
  11. package/skills/workit-implement/SKILL.md +14 -20
  12. package/skills/workit-mockup/SKILL.md +0 -5
  13. package/skills/workit-plan/SKILL.md +30 -63
  14. package/skills/workit-review/SKILL.md +0 -5
  15. package/skills/workit-steer/SKILL.md +27 -27
  16. package/src/core/authority.ts +6 -1
  17. package/src/core/auto-approval.ts +34 -14
  18. package/src/core/branch.ts +293 -42
  19. package/src/core/config-conversion.ts +4 -4
  20. package/src/core/config-guard.ts +1 -7
  21. package/src/core/config.ts +1 -1
  22. package/src/core/cutover.ts +1207 -140
  23. package/src/core/doctor.ts +155 -82
  24. package/src/core/external-action-effects.ts +886 -172
  25. package/src/core/external-action.ts +214 -63
  26. package/src/core/init.ts +25 -115
  27. package/src/core/legacy-ownership.ts +47 -0
  28. package/src/core/methods.ts +56 -52
  29. package/src/core/ports/pr-create.ts +23 -15
  30. package/src/core/pr-create.ts +374 -84
  31. package/src/core/registration.ts +0 -15
  32. package/src/core/repo-tools.ts +0 -1
  33. package/src/core/route-intent.ts +34 -82
  34. package/src/core/safe-write.ts +66 -1
  35. package/src/core/setup-state.ts +7 -0
  36. package/src/core/setup.ts +175 -82
  37. package/src/core/task-contract.ts +7 -1
  38. package/src/core/task-engine.ts +47 -2
  39. package/src/core/task-store.ts +159 -1
  40. package/src/core/tracker-issues.ts +55 -118
  41. package/src/core/vcs-config.ts +178 -187
  42. package/src/core/workspaces.ts +548 -46
  43. package/src/core.ts +1 -2
  44. package/src/core/ports/vcs-token-create-urls.ts +0 -4
@@ -2,12 +2,9 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { configDir, PRESETS } from "./config";
5
- import { resolveWorkspace } from "./workspaces";
5
+ import { resolveRuntimeWorkspaceVcs } from "./workspaces";
6
6
  import { resolveBranchPolicyFor } from "./branch";
7
- // Ports of scripts/vcs/config.sh + verify-token.sh + token-create-urls.sh + merged-style.sh.
8
- // Token never printed.
9
-
10
- const TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
7
+ // VCS resolution and CLI-backed identity/style reads.
11
8
 
12
9
  export const vcsConfigPath = (): string =>
13
10
  process.env.WORKFLOW_VCS_CONFIG ?? path.join(configDir(), "vcs.json");
@@ -30,6 +27,95 @@ const remoteProvider = (cwd: string): string | null => {
30
27
  return null;
31
28
  };
32
29
 
30
+ const remoteEndpoint = (
31
+ remote: string,
32
+ ): { host: string; path: string; protocol: string; port: string } | null => {
33
+ const raw = remote.trim();
34
+ if (!raw) return null;
35
+ const scp = /^(?:[^@\s]+@)?([^:]+):(.+)$/u.exec(raw);
36
+ if (scp && !raw.includes("://"))
37
+ return {
38
+ host: scp[1].toLowerCase(),
39
+ path: scp[2].replace(/^\//u, "").replace(/\.git$/u, ""),
40
+ protocol: "ssh:",
41
+ port: "",
42
+ };
43
+ try {
44
+ const url = new URL(raw);
45
+ return {
46
+ host: url.hostname.toLowerCase(),
47
+ path: url.pathname
48
+ .replace(/^\//u, "")
49
+ .replace(/\.git$/u, "")
50
+ .replace(/\/$/u, ""),
51
+ protocol: url.protocol,
52
+ port: url.port,
53
+ };
54
+ } catch {
55
+ return null;
56
+ }
57
+ };
58
+
59
+ const configuredSshEndpoint = (host: string): { host: string; port: string } | null => {
60
+ if (host.startsWith("-")) return null;
61
+ const resolved = spawnSync("ssh", ["-G", host], {
62
+ encoding: "utf8",
63
+ timeout: 5000,
64
+ maxBuffer: 1024 * 1024,
65
+ env: { ...process.env, PATH: process.env.PATH ?? "" },
66
+ });
67
+ if (resolved.status !== 0) return null;
68
+ const output = resolved.stdout ?? "";
69
+ const hostname = /^hostname\s+(\S+)/imu.exec(output)?.[1]?.toLowerCase();
70
+ if (!hostname) return null;
71
+ return { host: hostname, port: /^port\s+(\d+)/imu.exec(output)?.[1] ?? "22" };
72
+ };
73
+
74
+ const sshApiHostAliases: Record<string, Array<{ host: string; port: string }>> = {
75
+ "github.com": [{ host: "ssh.github.com", port: "443" }],
76
+ "gitlab.com": [{ host: "altssh.gitlab.com", port: "443" }],
77
+ };
78
+
79
+ /** Confirm that Git transport and authenticated provider API calls target the same host. */
80
+ export const hostingApiHostMatches = (
81
+ remote: string,
82
+ apiHost: string,
83
+ resolveSshEndpoint: (
84
+ host: string,
85
+ ) => { host: string; port: string } | null = configuredSshEndpoint,
86
+ ): boolean => {
87
+ const endpoint = remoteEndpoint(remote);
88
+ if (!endpoint) return false;
89
+ let expected: { host: string; port: string };
90
+ try {
91
+ const url = new URL(`https://${apiHost}`);
92
+ if (url.pathname !== "/" || url.search || url.hash || url.username || url.password)
93
+ return false;
94
+ expected = {
95
+ host: url.hostname.toLowerCase().replace(/\.$/u, ""),
96
+ port: url.port,
97
+ };
98
+ } catch {
99
+ return false;
100
+ }
101
+ if (endpoint.protocol !== "ssh:") {
102
+ return endpoint.host.replace(/\.$/u, "") === expected.host && endpoint.port === expected.port;
103
+ }
104
+ const resolved = resolveSshEndpoint(endpoint.host);
105
+ if (!resolved) return false;
106
+ const hostname = resolved.host.toLowerCase().replace(/\.$/u, "");
107
+ const port = endpoint.port || resolved.port;
108
+ if (hostname === expected.host) return port === (expected.port || "22");
109
+ return Boolean(
110
+ sshApiHostAliases[expected.host]?.some(
111
+ (alias) =>
112
+ alias.host === hostname &&
113
+ alias.port === port &&
114
+ (!expected.port || expected.port === port),
115
+ ),
116
+ );
117
+ };
118
+
33
119
  // RL-01: typed vcs.json reader. Missing is a legitimate unconfigured state;
34
120
  // malformed (parse failure or a non-object) is reported with the exact path so
35
121
  // risky consumers stop instead of silently reading defaults.
@@ -60,14 +146,9 @@ export const readVcsConfig = (): VcsConfigResult => {
60
146
  return { status: "malformed", path: file, config: {}, error: `${file} is not a JSON object` };
61
147
  };
62
148
 
63
- function readVcsJson(): { config: Record<string, any>; path: string; ok: boolean } {
64
- const { status, path: cfgPath, config } = readVcsConfig();
65
- return { config, path: cfgPath, ok: status === "valid" };
66
- }
67
-
68
149
  /** Port of scripts/vcs/config.sh — mode: load | summary | resolve. */
69
150
  export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): Record<string, any> {
70
- const ws = resolveWorkspace(vcsCwd(cwd));
151
+ const ws = resolveRuntimeWorkspaceVcs(vcsCwd(cwd));
71
152
  const wsVcs = (ws?.vcs ?? {}) as Record<string, any>;
72
153
  const wsYt = (ws?.youtrack ?? {}) as Record<string, any>;
73
154
  const wsIssues = (ws?.issues ?? {}) as Record<string, any>;
@@ -110,8 +191,10 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
110
191
  (hasWorkspacePolicy ? policyDefault : (cfg.defaultTargetBranch ?? policyDefault)) ??
111
192
  "develop",
112
193
  );
113
- const linkIssues = typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
114
- const youtrackBaseUrl = typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
194
+ const linkIssues =
195
+ provider === "gitlab" && typeof wsYt.link_issues === "boolean" ? wsYt.link_issues : null;
196
+ const youtrackBaseUrl =
197
+ provider === "gitlab" && typeof wsYt.baseUrl === "string" ? wsYt.baseUrl : null;
115
198
  // github issues path only when BOTH providers are github (mirrors WorkspaceConfig.issues).
116
199
  let issuesProvider: string | null = null;
117
200
  let linkOnPr: boolean | null = null;
@@ -144,11 +227,9 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
144
227
  if (cfgStatus === "malformed") {
145
228
  return { ok: false, error: cfgError, configPath: cfgPath };
146
229
  }
147
- if (cfgStatus === "missing") {
230
+ if (cfgStatus === "missing" && provider === null)
148
231
  return { ok: false, error: `vcs.json is missing: ${cfgPath}`, configPath: cfgPath };
149
- }
150
- // Credentials cannot resolve without a provider: fail closed instead of
151
- // assuming a host. Branch policy (resolve mode above) never needed one.
232
+ // Hosting cannot resolve without a provider; branch policy still can.
152
233
  if (provider === null) {
153
234
  return {
154
235
  ok: false,
@@ -158,33 +239,12 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
158
239
  }
159
240
 
160
241
  const prov = (cfg[provider] ?? {}) as Record<string, any>;
161
- // Workspace-tight tokens: an explicit workspace vcs.tokenFile wins (per-area
162
- // accounts without collisions), else the global provider tokenFile, else the
163
- // default <provider>.token path. Same order as the per-workspace
164
- // commitPolicy/branchPolicy overrides.
165
- const wsTokenFile =
166
- typeof wsVcs.tokenFile === "string" && wsVcs.tokenFile.trim() !== "" ? wsVcs.tokenFile : null;
167
- const tokenFile = String(
168
- wsTokenFile ?? prov.tokenFile ?? path.join(configDir(), `${provider}.token`),
169
- );
170
- const tokenPath = path.resolve(tokenFile);
171
- let tokenOk = false;
172
- if (fs.existsSync(tokenPath)) {
173
- const token = fs.readFileSync(tokenPath, "utf8").trim();
174
- const placeholder =
175
- !token || token === TOKEN_PLACEHOLDER || token.startsWith(TOKEN_PLACEHOLDER);
176
- tokenOk = !placeholder;
177
- }
178
-
179
242
  const out: Record<string, any> = {
180
243
  ok: true,
181
244
  configPath: path.resolve(cfgPath),
182
245
  provider,
183
246
  defaultTargetBranch: defaultTarget,
184
247
  pr: cfg.pr ?? {},
185
- tokenPath,
186
- tokenPresent: fs.existsSync(tokenPath),
187
- tokenReady: tokenOk,
188
248
  workspace_name: ws?.name ?? null,
189
249
  link_issues: linkIssues,
190
250
  youtrack_base_url: youtrackBaseUrl,
@@ -199,159 +259,69 @@ export function vcsConfig(mode: "load" | "summary" | "resolve", cwd?: string): R
199
259
  } else if (provider === "github") {
200
260
  out.github = { host: prov.host ?? "github.com" };
201
261
  }
202
- if (mode === "summary") delete out.tokenPath;
203
262
  return out;
204
263
  }
205
264
 
206
- /** Port of scripts/vcs/verify-token.sh — soft-fail JSON out, always exit 0. */
207
- export async function vcsVerifyToken(): Promise<Record<string, any>> {
208
- const cfg = vcsConfig("load");
265
+ /** Probe the CLI credential actually used for this checkout, never a Workit token file. */
266
+ export function vcsCliIdentity(cwd?: string): Record<string, any> {
267
+ const root = vcsCwd(cwd);
268
+ const cfg = vcsConfig("load", root);
209
269
  if (!cfg.ok) return { ok: false, error: cfg.error ?? "vcs config not ready" };
210
270
  const provider = cfg.provider as string;
211
- if (!cfg.tokenReady) {
212
- return {
213
- ok: false,
214
- provider,
215
- error: "token file still placeholder YOUR_TOKEN_HERE — edit locally, then /wk-status",
216
- path: cfg.tokenPath,
217
- };
218
- }
219
- const token = fs.readFileSync(cfg.tokenPath as string, "utf8").trim();
220
-
221
- if (provider === "gitlab") {
222
- const host = (cfg.gitlab as Record<string, any>)?.host ?? "gitlab.com";
223
- const api = ((cfg.gitlab as Record<string, any>)?.apiUrl ?? `https://${host}/api/v4`).replace(
224
- /\/+$/,
225
- "",
226
- );
227
- let user: Record<string, any>;
228
- try {
229
- const res = await fetch(`${api}/user`, {
230
- headers: { "PRIVATE-TOKEN": token },
231
- });
232
- if (!res.ok) {
233
- return {
234
- ok: false,
235
- provider,
236
- error: "GitLab API rejected token",
237
- detail: (await res.text()).slice(0, 200),
238
- };
239
- }
240
- user = JSON.parse(await res.text()) as Record<string, any>;
241
- } catch (err) {
242
- if (err instanceof SyntaxError) {
243
- return { ok: false, provider, error: "invalid JSON from GitLab /user" };
244
- }
245
- return {
246
- ok: false,
247
- provider,
248
- error: "GitLab API rejected token",
249
- detail: (err instanceof Error ? err.message : "network error").slice(0, 200),
250
- };
251
- }
252
- return { ok: true, provider, username: user.username ?? user.login, name: user.name };
253
- }
254
- if (provider === "github") {
255
- const result = spawnSync("gh", ["api", "user"], {
256
- encoding: "utf8",
257
- env: { ...process.env, GH_TOKEN: token },
258
- });
259
- if (result.status !== 0) {
271
+ if (provider !== "gitlab" && provider !== "github")
272
+ return { ok: false, error: `unsupported provider: ${provider}` };
273
+ const bin = provider === "gitlab" ? "glab" : "gh";
274
+ const host = String(
275
+ provider === "github" ? (cfg.github?.host ?? "github.com") : (cfg.gitlab?.host ?? "gitlab.com"),
276
+ ).toLowerCase();
277
+ const env = {
278
+ ...process.env,
279
+ PATH: process.env.PATH ?? "",
280
+ ...(provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }),
281
+ };
282
+ const result = spawnSync(bin, ["api", "user"], {
283
+ cwd: root,
284
+ encoding: "utf8",
285
+ env,
286
+ });
287
+ if (result.status !== 0)
288
+ return { ok: false, provider, error: `${bin} is missing or not authenticated` };
289
+ try {
290
+ const user = JSON.parse(result.stdout ?? "") as Record<string, unknown>;
291
+ const username = provider === "gitlab" ? user.username : user.login;
292
+ if (typeof username !== "string" || !username)
293
+ return { ok: false, provider, error: `invalid ${bin} identity response` };
294
+ const expected = resolveRuntimeWorkspaceVcs(root)?.vcs?.account;
295
+ if (expected && username.toLowerCase() !== expected.toLowerCase())
260
296
  return {
261
297
  ok: false,
262
298
  provider,
263
- error: "GitHub API rejected token",
264
- detail: (result.stderr ?? result.stdout ?? "").slice(0, 200),
299
+ error: `${bin} account ${username} does not match ${expected}`,
265
300
  };
266
- }
267
- try {
268
- const user = JSON.parse(result.stdout ?? "") as Record<string, any>;
269
- return { ok: true, provider, username: user.login, name: user.name };
270
- } catch {
271
- return { ok: false, provider, error: "invalid JSON from gh api user" };
272
- }
301
+ return { ok: true, provider, host, username, name: user.name };
302
+ } catch {
303
+ return { ok: false, provider, error: `invalid JSON from ${bin} api user` };
273
304
  }
274
- return { ok: false, error: `unsupported provider: ${provider}` };
275
305
  }
276
306
 
277
- /** Port of scripts/vcs/token-create-urls.sh — provider token-creation deep links. */
278
- export function vcsTokenCreateUrls(): Record<string, any> {
279
- const tokenName = process.env.WORKFLOW_VCS_TOKEN_NAME ?? "workit";
280
- const { config: cfg } = readVcsJson();
281
- const defaults = (cfg.tokenDefaults ?? {}) as Record<string, any>;
282
- const name = String(defaults.name ?? tokenName);
283
- const desc = String(defaults.description ?? "OpenCode workit — /wk-pr and glab/gh");
284
-
285
- const gitlab = (cfg.gitlab ?? {}) as Record<string, any>;
286
- const host = String(gitlab.host ?? "gitlab.com");
287
- const gitlabScopes = Array.isArray(defaults.gitlabScopes) ? defaults.gitlabScopes : ["api"];
288
- const gitlabParams = new URLSearchParams({
289
- name,
290
- description: desc,
291
- scopes: gitlabScopes.join(","),
292
- });
293
- const gitlabUrl = `https://${host}/-/user_settings/personal_access_tokens?${gitlabParams}`;
294
-
295
- const githubPerms = defaults.githubPermissions ?? {
296
- pull_requests: "write",
297
- contents: "write",
298
- metadata: "read",
299
- };
300
- const ghParams = new URLSearchParams({ name, description: desc, ...githubPerms });
301
- const githubFineUrl = `https://github.com/settings/personal-access-tokens/new?${ghParams}`;
302
-
303
- const classicScopes = Array.isArray(defaults.githubClassicScopes)
304
- ? defaults.githubClassicScopes
305
- : ["repo"];
306
- const githubClassicUrl = `https://github.com/settings/tokens/new?${new URLSearchParams({ description: name, scopes: classicScopes.join(",") })}`;
307
-
308
- const rawProvider = cfg.provider;
309
- // Display-only URL helper: both providers' URLs are always returned, but
310
- // nothing is preselected without an explicit provider — no silent default.
311
- const provider =
312
- typeof rawProvider === "string" && rawProvider.trim() ? rawProvider.toLowerCase() : null;
313
- const active =
314
- provider === null
315
- ? undefined
316
- : ({
317
- gitlab: {
318
- tokenFile: gitlab.tokenFile ?? path.join(configDir(), "gitlab.token"),
319
- createUrl: gitlabUrl,
320
- scopes: gitlabScopes,
321
- name,
322
- },
323
- github: {
324
- tokenFile:
325
- (cfg.github as Record<string, any>)?.tokenFile ??
326
- path.join(configDir(), "github.token"),
327
- createUrl: githubFineUrl,
328
- createUrlClassic: githubClassicUrl,
329
- permissions: githubPerms,
330
- name,
331
- },
332
- }[provider] ?? {});
333
-
334
- return {
335
- tokenName: name,
336
- tokenDescription: desc,
337
- activeProvider: provider,
338
- active,
339
- gitlab: { host, createUrl: gitlabUrl, scopes: gitlabScopes, tokenFile: gitlab.tokenFile },
340
- github: {
341
- createUrl: githubFineUrl,
342
- createUrlClassic: githubClassicUrl,
343
- permissions: githubPerms,
344
- tokenFile: (cfg.github as Record<string, any>)?.tokenFile,
345
- },
346
- };
307
+ /** Legacy command name, now verifies native CLI auth rather than a separate token. */
308
+ export async function vcsVerifyToken(): Promise<Record<string, any>> {
309
+ return vcsCliIdentity();
347
310
  }
348
311
 
349
312
  /** Port of scripts/vcs/merged-style.sh — recent merged MR/PR bodies for style reference. */
350
313
  export function mergedPrStyle(limit = 6, cwd?: string): Record<string, any> {
351
314
  const cfg = vcsConfig("load", cwd);
352
- if (!cfg.ok || !cfg.tokenReady) return { ok: false, error: "vcs not configured" };
315
+ if (!cfg.ok) return { ok: false, error: "vcs not configured" };
353
316
  const provider = cfg.provider as string;
354
- const token = fs.readFileSync(cfg.tokenPath as string, "utf8").trim();
317
+ const host = String(
318
+ provider === "github" ? (cfg.github?.host ?? "github.com") : (cfg.gitlab?.host ?? "gitlab.com"),
319
+ ).toLowerCase();
320
+ const env = {
321
+ ...process.env,
322
+ PATH: process.env.PATH ?? "",
323
+ ...(provider === "github" ? { GH_HOST: host } : { GITLAB_HOST: host }),
324
+ };
355
325
  const examples: Array<Record<string, any>> = [];
356
326
 
357
327
  const descInfo = (desc: string, caseInsensitiveNotes = false): Record<string, any> => ({
@@ -363,22 +333,32 @@ export function mergedPrStyle(limit = 6, cwd?: string): Record<string, any> {
363
333
  descriptionPreview: desc.slice(0, 600),
364
334
  });
365
335
 
336
+ const remoteResult = spawnSync("git", ["remote", "get-url", "--push", "--all", "origin"], {
337
+ cwd,
338
+ encoding: "utf8",
339
+ });
340
+ const remotes = (remoteResult.stdout ?? "")
341
+ .split(/\r?\n/u)
342
+ .map((value) => value.trim())
343
+ .filter(Boolean);
344
+ if (remotes.length === 0) return { ok: false, error: "no origin remote" };
345
+ if (remotes.length !== 1) return { ok: false, error: "origin has ambiguous push destinations" };
346
+ const remote = remotes[0];
347
+ const endpoint = remoteEndpoint(remote);
348
+ if (!endpoint?.path) return { ok: false, error: "hosting repository could not be resolved" };
349
+ if (!hostingApiHostMatches(remote, host))
350
+ return { ok: false, error: "PR style target does not match the provider host" };
351
+
366
352
  if (provider === "gitlab") {
367
- const remote = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
368
- if (remote.status !== 0) return { ok: false, error: "no origin remote" };
369
- const url = (remote.stdout ?? "").trim();
370
- const m = /gitlab\.com[:/](.+?)(?:\.git)?$/.exec(url);
371
- if (!m) return { ok: false, error: "not a gitlab.com origin" };
372
- const project = m[1];
373
- const env = { ...process.env, GITLAB_TOKEN: token };
374
353
  const run = (args: string[]) =>
375
- spawnSync("glab", ["api", ...args], { cwd, encoding: "utf8", env });
354
+ spawnSync("glab", ["api", ...args], {
355
+ cwd,
356
+ encoding: "utf8",
357
+ env,
358
+ });
376
359
  let r = run([
377
- `projects/${project.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`,
360
+ `projects/${endpoint.path.replaceAll("/", "%2F")}/merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`,
378
361
  ]);
379
- if (r.status !== 0) {
380
- r = run([`merge_requests?state=merged&per_page=${limit}&order_by=updated_at&sort=desc`]);
381
- }
382
362
  if (r.status !== 0) return { ok: false, error: "could not list merge requests" };
383
363
  for (const mr of JSON.parse(r.stdout ?? "[]") as Array<Record<string, any>>) {
384
364
  const desc = String(mr.description ?? "").trim();
@@ -392,8 +372,19 @@ export function mergedPrStyle(limit = 6, cwd?: string): Record<string, any> {
392
372
  } else if (provider === "github") {
393
373
  const r = spawnSync(
394
374
  "gh",
395
- ["pr", "list", "--state", "merged", "--limit", String(limit), "--json", "title,url,body"],
396
- { cwd, encoding: "utf8", env: { ...process.env, GH_TOKEN: token } },
375
+ [
376
+ "pr",
377
+ "list",
378
+ "--repo",
379
+ `${host}/${endpoint.path}`,
380
+ "--state",
381
+ "merged",
382
+ "--limit",
383
+ String(limit),
384
+ "--json",
385
+ "title,url,body",
386
+ ],
387
+ { cwd, encoding: "utf8", env },
397
388
  );
398
389
  if (r.status !== 0) return { ok: false, error: "could not list pull requests" };
399
390
  for (const pr of JSON.parse(r.stdout ?? "[]") as Array<Record<string, any>>) {