@mutmutco/cli 2.10.1 → 2.11.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 (2) hide show
  1. package/dist/index.cjs +451 -344
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -3480,8 +3480,8 @@ function parseHookInput(stdin) {
3480
3480
  }
3481
3481
 
3482
3482
  // src/index.ts
3483
- var import_node_child_process5 = require("node:child_process");
3484
- var import_node_util5 = require("node:util");
3483
+ var import_node_child_process6 = require("node:child_process");
3484
+ var import_node_util6 = require("node:util");
3485
3485
  var import_node_path5 = require("node:path");
3486
3486
  var import_node_os = require("node:os");
3487
3487
 
@@ -3495,7 +3495,7 @@ var HEAD_PROMPT_ACTION_LIMIT = 50;
3495
3495
  var HEAD_PROMPT_DECISION_LIMIT = 80;
3496
3496
  function resolveEngine(platform, custom) {
3497
3497
  if (custom) return { cmd: custom, args: [], shell: true };
3498
- return { cmd: "claude", args: ["-p"], shell: platform === "win32" };
3498
+ return { cmd: "claude", args: ["-p", "--no-session-persistence"], shell: platform === "win32" };
3499
3499
  }
3500
3500
  function headTsPath(key) {
3501
3501
  const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "_");
@@ -4025,32 +4025,153 @@ ${buildReportBody(body, sourceRepo)}`;
4025
4025
  }
4026
4026
 
4027
4027
  // src/board.ts
4028
+ var import_node_child_process4 = require("node:child_process");
4029
+ var import_node_util4 = require("node:util");
4030
+
4031
+ // src/github-client.ts
4028
4032
  var import_node_child_process3 = require("node:child_process");
4029
4033
  var import_node_util3 = require("node:util");
4034
+ var rawExecFileP = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
4035
+ var execFileP = (file, args, options = {}) => rawExecFileP(file, args, { encoding: "utf8", windowsHide: true, timeout: 1e4, killSignal: "SIGTERM", ...options });
4036
+ var cachedGhCliToken;
4037
+ async function githubToken() {
4038
+ if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
4039
+ if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
4040
+ cachedGhCliToken ??= execFileP("gh", ["auth", "token"]).then(({ stdout }) => stdout.trim() || void 0).catch(() => void 0);
4041
+ return cachedGhCliToken;
4042
+ }
4043
+ var GitHubApiError = class extends Error {
4044
+ status;
4045
+ stderr;
4046
+ graphqlErrors;
4047
+ constructor(message, opts = {}) {
4048
+ super(message);
4049
+ this.name = "GitHubApiError";
4050
+ this.status = opts.status ?? 0;
4051
+ this.stderr = message;
4052
+ this.graphqlErrors = opts.graphqlErrors;
4053
+ }
4054
+ };
4055
+ var DEFAULT_TIMEOUT_MS = 2e4;
4056
+ function joinUrl(base, path2) {
4057
+ if (path2.startsWith("http://") || path2.startsWith("https://")) return path2;
4058
+ return `${base.replace(/\/+$/, "")}/${path2.replace(/^\/+/, "")}`;
4059
+ }
4060
+ function withPerPage(url) {
4061
+ if (/[?&]per_page=/.test(url)) return url;
4062
+ return url.includes("?") ? `${url}&per_page=100` : `${url}?per_page=100`;
4063
+ }
4064
+ function nextLink(linkHeader) {
4065
+ if (!linkHeader) return void 0;
4066
+ for (const part of linkHeader.split(",")) {
4067
+ const match = part.match(/<([^>]+)>\s*;\s*rel="next"/);
4068
+ if (match) return match[1];
4069
+ }
4070
+ return void 0;
4071
+ }
4072
+ async function errorFromResponse(res) {
4073
+ let detail = "";
4074
+ try {
4075
+ const text = await res.text();
4076
+ try {
4077
+ const parsed = JSON.parse(text);
4078
+ detail = parsed.message ?? text;
4079
+ } catch {
4080
+ detail = text;
4081
+ }
4082
+ } catch {
4083
+ detail = "";
4084
+ }
4085
+ const suffix = detail ? `: ${detail.trim()}` : "";
4086
+ return new GitHubApiError(`HTTP ${res.status}${suffix} (${res.url})`, { status: res.status });
4087
+ }
4088
+ function createGitHubClient(options = {}) {
4089
+ const baseUrl = options.baseUrl ?? process.env.GITHUB_API_URL ?? "https://api.github.com";
4090
+ const token = options.token ?? githubToken;
4091
+ const defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
4092
+ const fetchImpl = options.fetchImpl ?? fetch;
4093
+ async function request(method, url, init = {}) {
4094
+ const t = await token();
4095
+ const headers = {
4096
+ Accept: "application/vnd.github+json",
4097
+ "X-GitHub-Api-Version": "2022-11-28",
4098
+ "User-Agent": "mmi-cli",
4099
+ ...t ? { Authorization: `Bearer ${t}` } : {},
4100
+ ...init.body !== void 0 ? { "Content-Type": "application/json" } : {},
4101
+ ...init.headers
4102
+ };
4103
+ const res = await fetchImpl(url, {
4104
+ method,
4105
+ headers,
4106
+ body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
4107
+ signal: AbortSignal.timeout(init.timeoutMs ?? defaultTimeoutMs)
4108
+ });
4109
+ if (!res.ok) throw await errorFromResponse(res);
4110
+ return res;
4111
+ }
4112
+ async function parseJson(res) {
4113
+ if (res.status === 204) return void 0;
4114
+ const text = await res.text();
4115
+ if (!text) return void 0;
4116
+ return JSON.parse(text);
4117
+ }
4118
+ return {
4119
+ async rest(method, path2, init) {
4120
+ const res = await request(method, joinUrl(baseUrl, path2), init);
4121
+ return parseJson(res);
4122
+ },
4123
+ async restPaginate(path2, init) {
4124
+ const items = [];
4125
+ let url = withPerPage(joinUrl(baseUrl, path2));
4126
+ while (url) {
4127
+ const res = await request("GET", url, init);
4128
+ const page = await parseJson(res);
4129
+ if (Array.isArray(page)) items.push(...page);
4130
+ url = nextLink(res.headers.get("link"));
4131
+ }
4132
+ return items;
4133
+ },
4134
+ async graphql(query, variables, init) {
4135
+ const res = await request("POST", joinUrl(baseUrl, "graphql"), {
4136
+ ...init,
4137
+ body: { query, ...variables ? { variables } : {} }
4138
+ });
4139
+ const parsed = await parseJson(res);
4140
+ if (parsed?.errors?.length) {
4141
+ const message = parsed.errors.map((e) => e.message ?? e.type ?? "unknown GraphQL error").join("; ");
4142
+ throw new GitHubApiError(`GraphQL: ${message}`, { status: 200, graphqlErrors: parsed.errors });
4143
+ }
4144
+ if (!parsed || parsed.data === void 0 || parsed.data === null) {
4145
+ throw new GitHubApiError("GraphQL response did not include data", { status: 200 });
4146
+ }
4147
+ return parsed.data;
4148
+ }
4149
+ };
4150
+ }
4151
+ var cachedDefaultClient;
4152
+ function defaultGitHubClient() {
4153
+ cachedDefaultClient ??= createGitHubClient();
4154
+ return cachedDefaultClient;
4155
+ }
4030
4156
 
4031
4157
  // ../infra/board-vocab.mjs
4032
4158
  var BOARD_STATUSES = ["Todo", "In Progress", "In Review", "Done"];
4033
4159
 
4034
4160
  // src/board.ts
4035
- var rawExecFileP = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
4036
- var BOARD_GH_TIMEOUT_MS = 2e4;
4161
+ var rawExecFileP2 = (0, import_node_util4.promisify)(import_node_child_process4.execFile);
4037
4162
  var BOARD_GIT_TIMEOUT_MS = 1e4;
4038
4163
  var WRITE_PROBE_CONCURRENCY = 8;
4039
- var execFileP = (file, args, options = {}) => (
4164
+ var execFileP2 = (file, args, options = {}) => (
4040
4165
  // encoding 'utf8' guarantees string output at runtime; the cast pins the type (promisify's
4041
4166
  // overloads widen to string|Buffer when options is spread in).
4042
- rawExecFileP(file, args, { encoding: "utf8", windowsHide: true, timeout: BOARD_GIT_TIMEOUT_MS, killSignal: "SIGTERM", ...options })
4167
+ rawExecFileP2(file, args, { encoding: "utf8", windowsHide: true, timeout: BOARD_GIT_TIMEOUT_MS, killSignal: "SIGTERM", ...options })
4043
4168
  );
4044
4169
  var ACTIVE_STATUSES = /* @__PURE__ */ new Set(["Todo", "In Progress", "In Review"]);
4045
4170
  var STATUS_ORDER = new Map(BOARD_STATUSES.map((s, i) => [s, i]));
4046
4171
  var TYPE_LABELS = ["bug", "feature", "task"];
4047
- var defaultGh = async (args) => {
4048
- const { stdout, stderr } = await execFileP("gh", args, { timeout: BOARD_GH_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024 });
4049
- return { stdout: String(stdout), stderr: String(stderr) };
4050
- };
4051
4172
  var defaultGit = async (args) => {
4052
4173
  try {
4053
- const { stdout } = await execFileP("git", args, { timeout: BOARD_GIT_TIMEOUT_MS });
4174
+ const { stdout } = await execFileP2("git", args, { timeout: BOARD_GIT_TIMEOUT_MS });
4054
4175
  return stdout.trim();
4055
4176
  } catch {
4056
4177
  return "";
@@ -4104,36 +4225,52 @@ query($owner: String!, $number: Int!, $after: String) {
4104
4225
  }
4105
4226
  }
4106
4227
  }`;
4107
- var ISSUE_PROJECT_ITEM_QUERY = `
4108
- query($repoOwner: String!, $repoName: String!, $number: Int!) {
4109
- repository(owner: $repoOwner, name: $repoName) {
4110
- issue(number: $number) {
4111
- id
4112
- number
4113
- title
4114
- url
4115
- state
4116
- repository { nameWithOwner }
4117
- labels(first: 10) { nodes { name } }
4118
- assignees(first: 10) { nodes { login } }
4119
- projectItems(first: 20) {
4120
- nodes {
4121
- id
4122
- project { id title }
4123
- fieldValues(first: 8) {
4124
- nodes {
4125
- ... on ProjectV2ItemFieldSingleSelectValue {
4126
- name
4127
- optionId
4128
- field { ... on ProjectV2SingleSelectField { id name } }
4228
+ var SINGLE_ITEM_CONTENT_FRAGMENT = `
4229
+ id
4230
+ number
4231
+ title
4232
+ url
4233
+ state
4234
+ repository { nameWithOwner }
4235
+ labels(first: 10) { nodes { name } }
4236
+ assignees(first: 10) { nodes { login } }
4237
+ projectItems(first: 20) {
4238
+ nodes {
4239
+ id
4240
+ project { id title }
4241
+ fieldValues(first: 8) {
4242
+ nodes {
4243
+ ... on ProjectV2ItemFieldSingleSelectValue {
4244
+ name
4245
+ optionId
4246
+ field { ... on ProjectV2SingleSelectField { id name } }
4247
+ }
4129
4248
  }
4130
4249
  }
4131
4250
  }
4132
- }
4251
+ }`;
4252
+ var ISSUE_PROJECT_ITEM_QUERY = `
4253
+ query($repoOwner: String!, $repoName: String!, $number: Int!) {
4254
+ viewer { login }
4255
+ repository(owner: $repoOwner, name: $repoName) {
4256
+ issueOrPullRequest(number: $number) {
4257
+ __typename
4258
+ ... on Issue {${SINGLE_ITEM_CONTENT_FRAGMENT}
4259
+ }
4260
+ ... on PullRequest {${SINGLE_ITEM_CONTENT_FRAGMENT}
4133
4261
  }
4134
4262
  }
4135
4263
  }
4136
4264
  }`;
4265
+ var UPDATE_ITEM_FIELD_MUTATION = `
4266
+ mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
4267
+ updateProjectV2ItemFieldValue(input: { projectId: $projectId, itemId: $itemId, fieldId: $fieldId, value: { singleSelectOptionId: $optionId } }) {
4268
+ projectV2Item { id }
4269
+ }
4270
+ }`;
4271
+ async function updateItemSingleSelect(client, projectId, itemId, fieldId, optionId) {
4272
+ await client.graphql(UPDATE_ITEM_FIELD_MUTATION, { projectId, itemId, fieldId, optionId });
4273
+ }
4137
4274
  function resolveBoardConfig(cfg) {
4138
4275
  const missing = [];
4139
4276
  if (!cfg.projectOwner) missing.push("projectOwner");
@@ -4256,7 +4393,7 @@ function renderBoardReport(report) {
4256
4393
  return lines.join("\n");
4257
4394
  }
4258
4395
  async function collectBoardItems(cfg, options, deps) {
4259
- const gh = deps.gh ?? defaultGh;
4396
+ const client = deps.client ?? defaultGitHubClient();
4260
4397
  const git = deps.git ?? defaultGit;
4261
4398
  const currentRepo = options.repo ?? repoFromGitRemote(await git(["remote", "get-url", "origin"])) ?? "";
4262
4399
  if (!currentRepo) throw new Error("could not resolve current GitHub repo; pass --repo owner/repo");
@@ -4269,7 +4406,7 @@ async function collectBoardItems(cfg, options, deps) {
4269
4406
  let partial = false;
4270
4407
  do {
4271
4408
  try {
4272
- const page = await fetchProjectPage(gh, cfg, after);
4409
+ const page = await fetchProjectPage(client, cfg, after);
4273
4410
  viewer ||= page.viewer.login;
4274
4411
  const project2 = page.organization?.projectV2;
4275
4412
  if (!project2) throw new Error(`project ${cfg.projectOwner}#${cfg.projectNumber} not found`);
@@ -4287,19 +4424,15 @@ async function collectBoardItems(cfg, options, deps) {
4287
4424
  } while (after);
4288
4425
  return { items: nodesToItems(nodes, warnings, cfg), viewer, repo: currentRepo, projectId, projectTitle, warnings, partial };
4289
4426
  }
4290
- async function repoCanPush(repo, gh) {
4427
+ async function repoCanPush(repo, client) {
4291
4428
  try {
4292
- const { stdout } = await gh(["api", `repos/${repo}`, "--jq", ".permissions.push"]);
4293
- const value = stdout.trim();
4294
- if (value === "true") return true;
4295
- if (value === "false") return false;
4296
- const parsed = JSON.parse(value);
4297
- return typeof parsed.permissions?.push === "boolean" ? parsed.permissions.push : void 0;
4429
+ const parsed = await client.rest("GET", `repos/${repo}`);
4430
+ return typeof parsed?.permissions?.push === "boolean" ? parsed.permissions.push : void 0;
4298
4431
  } catch {
4299
4432
  return void 0;
4300
4433
  }
4301
4434
  }
4302
- async function resolveWritableReposForClaimables(items, gh, allowPartial) {
4435
+ async function resolveWritableReposForClaimables(items, client, allowPartial) {
4303
4436
  const candidateRepos = [...new Set(items.filter((item) => item.status === "Todo" && item.assignees.length === 0).map((item) => item.repository))];
4304
4437
  const repos = /* @__PURE__ */ new Set();
4305
4438
  const warnings = [];
@@ -4308,7 +4441,7 @@ async function resolveWritableReposForClaimables(items, gh, allowPartial) {
4308
4441
  const worker = async () => {
4309
4442
  while (next < candidateRepos.length) {
4310
4443
  const repo = candidateRepos[next++];
4311
- const canPush = await repoCanPush(repo, gh);
4444
+ const canPush = await repoCanPush(repo, client);
4312
4445
  if (canPush === true) {
4313
4446
  repos.add(repo.toLowerCase());
4314
4447
  continue;
@@ -4326,9 +4459,9 @@ async function resolveWritableReposForClaimables(items, gh, allowPartial) {
4326
4459
  }
4327
4460
  async function readBoard(options, deps = {}) {
4328
4461
  const cfg = resolveBoardConfig(options.config);
4329
- const gh = deps.gh ?? defaultGh;
4462
+ const client = deps.client ?? defaultGitHubClient();
4330
4463
  const collected = await collectBoardItems(cfg, options, deps);
4331
- const writable = await resolveWritableReposForClaimables(collected.items, gh, options.allowPartial ?? false);
4464
+ const writable = await resolveWritableReposForClaimables(collected.items, client, options.allowPartial ?? false);
4332
4465
  collected.warnings.push(...writable.warnings);
4333
4466
  collected.partial = collected.partial || writable.partial;
4334
4467
  const groups = partitionBoardItems(collected.items, collected.viewer, collected.repo, writable.repos);
@@ -4341,7 +4474,7 @@ async function readBoard(options, deps = {}) {
4341
4474
  partial: collected.partial
4342
4475
  };
4343
4476
  if (options.includeBundleDetails) {
4344
- await attachBundleDetails(report, gh, options.allowPartial ?? false);
4477
+ await attachBundleDetails(report, client, options.allowPartial ?? false);
4345
4478
  }
4346
4479
  return report;
4347
4480
  }
@@ -4352,67 +4485,50 @@ function findBoardItem(items, selector) {
4352
4485
  if (!found) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
4353
4486
  return found;
4354
4487
  }
4488
+ async function resolveCurrentRepo(options, deps) {
4489
+ const git = deps.git ?? defaultGit;
4490
+ const currentRepo = options.repo ?? repoFromGitRemote(await git(["remote", "get-url", "origin"])) ?? "";
4491
+ if (!currentRepo) throw new Error("could not resolve current GitHub repo; pass --repo owner/repo");
4492
+ return currentRepo;
4493
+ }
4355
4494
  async function moveBoardItem(options, deps = {}) {
4356
4495
  if (!BOARD_STATUSES.includes(options.status)) {
4357
4496
  throw new Error(`unknown status '${options.status}'; expected one of ${BOARD_STATUSES.join(", ")}`);
4358
4497
  }
4359
4498
  const cfg = resolveBoardConfig(options.config);
4360
- const gh = deps.gh ?? defaultGh;
4361
- const collected = await collectBoardItems(cfg, options, deps);
4362
- const selector = parseIssueSelector(options.selector, collected.repo);
4363
- const item = findBoardItem(collected.items, selector);
4499
+ const client = deps.client ?? defaultGitHubClient();
4500
+ const currentRepo = await resolveCurrentRepo(options, deps);
4501
+ const selector = parseIssueSelector(options.selector, currentRepo);
4502
+ const lookup = await fetchIssueProjectItem(client, cfg, selector);
4503
+ const item = lookup.item;
4504
+ if (!item) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
4364
4505
  if (item.contentType !== "Issue") throw new Error(`${item.ref} is not an issue`);
4365
4506
  const optionId = cfg.statusOptions[options.status];
4366
4507
  try {
4367
- await gh([
4368
- "project",
4369
- "item-edit",
4370
- "--id",
4371
- item.itemId,
4372
- "--project-id",
4373
- cfg.projectId,
4374
- "--field-id",
4375
- cfg.statusFieldId,
4376
- "--single-select-option-id",
4377
- optionId
4378
- ]);
4508
+ await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, optionId);
4379
4509
  } catch (e) {
4380
4510
  const warning = `partial move: ${item.ref} status was not changed to ${options.status} (${ghError(e)})`;
4381
4511
  if (!options.allowPartial) throw new Error(warning);
4382
- return { item, viewer: collected.viewer, repo: collected.repo, status: item.status, partial: true, warning };
4512
+ return { item, viewer: lookup.viewer, repo: currentRepo, status: item.status, partial: true, warning };
4383
4513
  }
4384
4514
  return {
4385
4515
  item: { ...item, status: options.status, statusOptionId: optionId },
4386
- viewer: collected.viewer,
4387
- repo: collected.repo,
4516
+ viewer: lookup.viewer,
4517
+ repo: currentRepo,
4388
4518
  status: options.status,
4389
4519
  partial: false
4390
4520
  };
4391
4521
  }
4392
4522
  async function showBoardItem(options, deps = {}) {
4393
4523
  const cfg = resolveBoardConfig(options.config);
4394
- const gh = deps.gh ?? defaultGh;
4395
- const collected = await collectBoardItems(cfg, options, deps);
4396
- const selector = parseIssueSelector(options.selector, collected.repo);
4397
- let item;
4398
- try {
4399
- item = findBoardItem(collected.items, selector);
4400
- } catch (e) {
4401
- const fallback = await fetchIssueProjectItem(gh, cfg, selector);
4402
- if (!fallback) throw e;
4403
- item = fallback;
4404
- }
4524
+ const client = deps.client ?? defaultGitHubClient();
4525
+ const currentRepo = await resolveCurrentRepo(options, deps);
4526
+ const selector = parseIssueSelector(options.selector, currentRepo);
4527
+ const { item } = await fetchIssueProjectItem(client, cfg, selector);
4528
+ if (!item) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
4405
4529
  if (item.contentType === "Issue") {
4406
4530
  try {
4407
- const { stdout } = await gh(["issue", "view", String(item.number), "--repo", item.repository, "--json", "body,comments"]);
4408
- const detail = JSON.parse(stdout);
4409
- item.details = {
4410
- body: detail.body ?? "",
4411
- comments: (detail.comments ?? []).map((comment) => ({
4412
- author: comment.author?.login ?? "",
4413
- body: comment.body ?? ""
4414
- }))
4415
- };
4531
+ item.details = await fetchIssueDetails(client, item.repository, item.number);
4416
4532
  } catch (e) {
4417
4533
  if (!options.allowPartial) throw new Error(`detail read failed: ${item.ref}: ${ghError(e)}`);
4418
4534
  }
@@ -4421,17 +4537,17 @@ async function showBoardItem(options, deps = {}) {
4421
4537
  }
4422
4538
  async function claimBoardIssue(options, deps = {}) {
4423
4539
  const cfg = resolveBoardConfig(options.config);
4424
- const gh = deps.gh ?? defaultGh;
4540
+ const client = deps.client ?? defaultGitHubClient();
4425
4541
  const collected = await collectBoardItems(cfg, { repo: options.repo, allowPartial: options.allowPartial }, deps);
4426
4542
  const selector = parseIssueSelector(options.selector, collected.repo);
4427
4543
  try {
4428
4544
  findBoardItem(collected.items, selector);
4429
4545
  } catch (e) {
4430
- const fallback = await fetchIssueProjectItem(gh, cfg, selector);
4546
+ const fallback = (await fetchIssueProjectItem(client, cfg, selector)).item;
4431
4547
  if (!fallback) throw e;
4432
4548
  collected.items.push(fallback);
4433
4549
  }
4434
- const writable = await resolveWritableReposForClaimables(collected.items, gh, options.allowPartial ?? false);
4550
+ const writable = await resolveWritableReposForClaimables(collected.items, client, options.allowPartial ?? false);
4435
4551
  collected.warnings.push(...writable.warnings);
4436
4552
  collected.partial = collected.partial || writable.partial;
4437
4553
  const report = {
@@ -4448,7 +4564,7 @@ async function claimBoardIssue(options, deps = {}) {
4448
4564
  }
4449
4565
  let item = findClaimableItem(report, selector);
4450
4566
  if (item.contentType !== "Issue") throw new Error(`${item.ref} is not an issue`);
4451
- const fresh = await fetchIssueProjectItem(gh, cfg, { repo: item.repository, number: item.number });
4567
+ const fresh = (await fetchIssueProjectItem(client, cfg, { repo: item.repository, number: item.number })).item;
4452
4568
  if (!fresh) throw new Error(`${item.ref} is not on this project board`);
4453
4569
  if (fresh.status !== "Todo") throw new Error(`${item.ref} is not claimable: Status is ${fresh.status}`);
4454
4570
  if (fresh.assignees.length) throw new Error(`${item.ref} is already assigned to @${fresh.assignees.join(", @")}`);
@@ -4456,23 +4572,12 @@ async function claimBoardIssue(options, deps = {}) {
4456
4572
  const assignee = options.assignee ?? "@me";
4457
4573
  const assignedLogin = assignee === "@me" ? report.viewer : assignee.replace(/^@/, "");
4458
4574
  try {
4459
- await gh(["issue", "edit", String(item.number), "--repo", item.repository, "--add-assignee", assignee]);
4575
+ await client.rest("POST", `repos/${item.repository}/issues/${item.number}/assignees`, { body: { assignees: [assignedLogin] } });
4460
4576
  } catch (e) {
4461
4577
  throw new Error(`claim failed before board status changed: ${ghError(e)}`);
4462
4578
  }
4463
4579
  try {
4464
- await gh([
4465
- "project",
4466
- "item-edit",
4467
- "--id",
4468
- item.itemId,
4469
- "--project-id",
4470
- cfg.projectId,
4471
- "--field-id",
4472
- cfg.statusFieldId,
4473
- "--single-select-option-id",
4474
- cfg.statusOptions["In Progress"]
4475
- ]);
4580
+ await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, cfg.statusOptions["In Progress"]);
4476
4581
  } catch (e) {
4477
4582
  const warning = `partial claim: ${item.ref} was assigned to @${assignedLogin}, but Status was not moved to In Progress (${ghError(e)})`;
4478
4583
  if (!options.allowPartial) throw new Error(warning);
@@ -4491,22 +4596,11 @@ async function claimBoardIssue(options, deps = {}) {
4491
4596
  partial: false
4492
4597
  };
4493
4598
  }
4494
- async function setBoardItemPriority(gh, cfg, itemId, priority) {
4599
+ async function setBoardItemPriority(client, cfg, itemId, priority) {
4495
4600
  if (!isPriorityFieldConfigured(cfg)) return void 0;
4496
4601
  const optionId = resolvePriorityOptionId(cfg, priority);
4497
4602
  if (!optionId || !cfg.priorityFieldId || !cfg.projectId) return void 0;
4498
- await gh([
4499
- "project",
4500
- "item-edit",
4501
- "--id",
4502
- itemId,
4503
- "--project-id",
4504
- cfg.projectId,
4505
- "--field-id",
4506
- cfg.priorityFieldId,
4507
- "--single-select-option-id",
4508
- optionId
4509
- ]);
4603
+ await updateItemSingleSelect(client, cfg.projectId, itemId, cfg.priorityFieldId, optionId);
4510
4604
  return cliPriorityToFieldName(priority);
4511
4605
  }
4512
4606
  async function backfillBoardPriorities(options, deps = {}) {
@@ -4514,7 +4608,7 @@ async function backfillBoardPriorities(options, deps = {}) {
4514
4608
  if (!isPriorityFieldConfigured(cfg)) {
4515
4609
  throw new Error("priority field is not configured in Hub registry META (priorityFieldId + priorityOptions)");
4516
4610
  }
4517
- const gh = deps.gh ?? defaultGh;
4611
+ const client = deps.client ?? defaultGitHubClient();
4518
4612
  const collected = await collectBoardItems(cfg, { repo: options.repo }, deps);
4519
4613
  const issues = collected.items.filter((item) => item.contentType === "Issue");
4520
4614
  const concurrency = Math.max(1, options.concurrency ?? 8);
@@ -4525,7 +4619,7 @@ async function backfillBoardPriorities(options, deps = {}) {
4525
4619
  return;
4526
4620
  }
4527
4621
  try {
4528
- const priority = await recoverIssuePriority(gh, item);
4622
+ const priority = await recoverIssuePriority(client, item);
4529
4623
  if (!priority) {
4530
4624
  result.skipped += 1;
4531
4625
  return;
@@ -4537,18 +4631,7 @@ async function backfillBoardPriorities(options, deps = {}) {
4537
4631
  }
4538
4632
  const optionId = cfg.priorityOptions?.[priority];
4539
4633
  if (!optionId) throw new Error(`no option id for ${priority}`);
4540
- await gh([
4541
- "project",
4542
- "item-edit",
4543
- "--id",
4544
- item.itemId,
4545
- "--project-id",
4546
- cfg.projectId,
4547
- "--field-id",
4548
- cfg.priorityFieldId,
4549
- "--single-select-option-id",
4550
- optionId
4551
- ]);
4634
+ await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.priorityFieldId, optionId);
4552
4635
  result.set += 1;
4553
4636
  result.details.push(`${item.ref} \u2192 ${priority}`);
4554
4637
  } catch (e) {
@@ -4561,88 +4644,65 @@ async function backfillBoardPriorities(options, deps = {}) {
4561
4644
  }
4562
4645
  return result;
4563
4646
  }
4564
- async function recoverIssuePriority(gh, item) {
4647
+ async function recoverIssuePriority(client, item) {
4565
4648
  for (const label of item.labels) {
4566
4649
  const fromLabel = labelToFieldPriority(label);
4567
4650
  if (fromLabel) return fromLabel;
4568
4651
  }
4569
- const { stdout } = await gh(["api", `repos/${item.repository}/issues/${item.number}/events`, "--paginate"]);
4570
- return recoverPriorityFromEvents(parsePaginatedEvents(stdout));
4652
+ const events = await client.restPaginate(
4653
+ `repos/${item.repository}/issues/${item.number}/events`
4654
+ );
4655
+ return recoverPriorityFromEvents(events);
4571
4656
  }
4572
- function parsePaginatedEvents(stdout) {
4573
- const trimmed = stdout.trim();
4574
- if (!trimmed) return [];
4575
- try {
4576
- const parsed = JSON.parse(trimmed);
4577
- if (Array.isArray(parsed)) return parsed;
4578
- } catch {
4579
- }
4580
- return trimmed.split(/\r?\n/).filter(Boolean).flatMap((line) => {
4581
- try {
4582
- const parsed = JSON.parse(line);
4583
- return Array.isArray(parsed) ? parsed : [];
4584
- } catch {
4585
- return [];
4586
- }
4657
+ async function fetchProjectPage(client, cfg, after) {
4658
+ return client.graphql(PROJECT_ITEMS_QUERY, {
4659
+ owner: cfg.projectOwner,
4660
+ number: cfg.projectNumber,
4661
+ ...after ? { after } : {}
4587
4662
  });
4588
4663
  }
4589
- async function fetchProjectPage(gh, cfg, after) {
4590
- const args = [
4591
- "api",
4592
- "graphql",
4593
- "-f",
4594
- `query=${PROJECT_ITEMS_QUERY}`,
4595
- "-f",
4596
- `owner=${cfg.projectOwner}`,
4597
- "-F",
4598
- `number=${cfg.projectNumber}`
4599
- ];
4600
- if (after) args.push("-f", `after=${after}`);
4601
- const { stdout } = await gh(args);
4602
- const parsed = JSON.parse(stdout);
4603
- if (!parsed.data) throw new Error("gh GraphQL response did not include data");
4604
- return parsed.data;
4664
+ function isGraphQLNotFound(e) {
4665
+ const errors = e.graphqlErrors;
4666
+ return Boolean(e instanceof GitHubApiError && errors?.length && errors.every((entry) => entry.type === "NOT_FOUND"));
4605
4667
  }
4606
- async function fetchIssueProjectItem(gh, cfg, selector) {
4668
+ async function fetchIssueProjectItem(client, cfg, selector) {
4607
4669
  const [repoOwner, repoName] = selector.repo.split("/");
4608
- if (!repoOwner || !repoName) return void 0;
4609
- const { stdout } = await gh([
4610
- "api",
4611
- "graphql",
4612
- "-f",
4613
- `query=${ISSUE_PROJECT_ITEM_QUERY}`,
4614
- "-f",
4615
- `repoOwner=${repoOwner}`,
4616
- "-f",
4617
- `repoName=${repoName}`,
4618
- "-F",
4619
- `number=${selector.number}`
4620
- ]);
4621
- const parsed = JSON.parse(stdout);
4622
- const issue2 = parsed.data?.repository?.issue;
4623
- if (!issue2) {
4624
- const pageNodes = parsed.data?.organization?.projectV2?.items.nodes ?? [];
4625
- return nodesToItems(pageNodes, [], cfg).find(
4626
- (item) => item.repository.toLowerCase() === selector.repo.toLowerCase() && item.number === selector.number
4627
- );
4670
+ if (!repoOwner || !repoName) return { viewer: "" };
4671
+ let data;
4672
+ try {
4673
+ data = await client.graphql(ISSUE_PROJECT_ITEM_QUERY, {
4674
+ repoOwner,
4675
+ repoName,
4676
+ number: selector.number
4677
+ });
4678
+ } catch (e) {
4679
+ if (isGraphQLNotFound(e)) return { viewer: "" };
4680
+ throw e;
4628
4681
  }
4629
- const projectItem = (issue2.projectItems?.nodes ?? []).find((item) => item.project?.id === cfg.projectId);
4630
- if (!projectItem) return void 0;
4631
- return nodeToItem({
4632
- id: projectItem.id,
4633
- fieldValues: projectItem.fieldValues,
4634
- content: {
4635
- __typename: "Issue",
4636
- id: issue2.id,
4637
- number: issue2.number,
4638
- title: issue2.title,
4639
- url: issue2.url,
4640
- state: issue2.state,
4641
- repository: issue2.repository,
4642
- labels: issue2.labels,
4643
- assignees: issue2.assignees
4644
- }
4645
- }, cfg);
4682
+ const viewer = data.viewer?.login ?? "";
4683
+ const content = data.repository?.issueOrPullRequest;
4684
+ if (!content) return { viewer };
4685
+ const projectItem = (content.projectItems?.nodes ?? []).find((item) => item.project?.id === cfg.projectId);
4686
+ if (!projectItem) return { viewer };
4687
+ const { projectItems: _projectItems, ...contentFields } = content;
4688
+ return {
4689
+ viewer,
4690
+ item: nodeToItem({
4691
+ id: projectItem.id,
4692
+ fieldValues: projectItem.fieldValues,
4693
+ content: contentFields
4694
+ }, cfg)
4695
+ };
4696
+ }
4697
+ async function fetchIssueDetails(client, repo, number) {
4698
+ const issue2 = await client.rest("GET", `repos/${repo}/issues/${number}`);
4699
+ const comments = await client.restPaginate(
4700
+ `repos/${repo}/issues/${number}/comments`
4701
+ );
4702
+ return {
4703
+ body: issue2?.body ?? "",
4704
+ comments: comments.map((comment) => ({ author: comment.user?.login ?? "", body: comment.body ?? "" }))
4705
+ };
4646
4706
  }
4647
4707
  function nodesToItems(nodes, warnings, cfg) {
4648
4708
  const items = [];
@@ -4701,20 +4761,12 @@ function nodeToItem(node, cfg) {
4701
4761
  function isSupportedContent(content) {
4702
4762
  return Boolean(content && (content.__typename === "Issue" || content.__typename === "PullRequest"));
4703
4763
  }
4704
- async function attachBundleDetails(report, gh, allowPartial) {
4764
+ async function attachBundleDetails(report, client, allowPartial) {
4705
4765
  const candidates = detailCandidates(report);
4706
4766
  if (candidates.length <= 1) return;
4707
4767
  for (const item of candidates) {
4708
4768
  try {
4709
- const { stdout } = await gh(["issue", "view", String(item.number), "--repo", item.repository, "--json", "body,comments"]);
4710
- const detail = JSON.parse(stdout);
4711
- item.details = {
4712
- body: detail.body ?? "",
4713
- comments: (detail.comments ?? []).map((comment) => ({
4714
- author: comment.author?.login ?? "",
4715
- body: comment.body ?? ""
4716
- }))
4717
- };
4769
+ item.details = await fetchIssueDetails(client, item.repository, item.number);
4718
4770
  } catch (e) {
4719
4771
  const warning = `partial detail read: ${item.ref}: ${ghError(e)}`;
4720
4772
  if (!allowPartial) throw new Error(warning);
@@ -5347,12 +5399,12 @@ function buildInstalledPluginVersionCheck(input) {
5347
5399
  }
5348
5400
 
5349
5401
  // src/stage-runner.ts
5350
- var import_node_child_process4 = require("node:child_process");
5402
+ var import_node_child_process5 = require("node:child_process");
5351
5403
  var import_node_fs3 = require("node:fs");
5352
5404
  var import_node_path3 = require("node:path");
5353
5405
  var import_node_net = require("node:net");
5354
- var import_node_util4 = require("node:util");
5355
- var execFileP2 = (0, import_node_util4.promisify)(import_node_child_process4.execFile);
5406
+ var import_node_util5 = require("node:util");
5407
+ var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process5.execFile);
5356
5408
  function stageStatePath(cwd = process.cwd()) {
5357
5409
  return (0, import_node_path3.join)(cwd, "tmp", "stage", "state.json");
5358
5410
  }
@@ -5387,7 +5439,7 @@ function isPortFree(port) {
5387
5439
  });
5388
5440
  }
5389
5441
  async function shell(command, cwd, timeoutMs) {
5390
- await execFileP2(command, [], {
5442
+ await execFileP3(command, [], {
5391
5443
  cwd,
5392
5444
  shell: true,
5393
5445
  timeout: timeoutMs,
@@ -5406,7 +5458,7 @@ function readState(path2) {
5406
5458
  async function killTree(pid) {
5407
5459
  if (!Number.isInteger(pid) || pid <= 0) return;
5408
5460
  if (process.platform === "win32") {
5409
- await execFileP2("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true }).catch(() => void 0);
5461
+ await execFileP3("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true }).catch(() => void 0);
5410
5462
  return;
5411
5463
  }
5412
5464
  try {
@@ -5469,7 +5521,7 @@ async function startStage(config = {}, opts = {}) {
5469
5521
  }
5470
5522
  const sub = (s) => s != null && stagePort != null ? s.replace(/\$\{?STAGE_PORT\}?/g, String(stagePort)) : s;
5471
5523
  const up = sub(config.up.trim());
5472
- const child = (0, import_node_child_process4.spawn)(up, {
5524
+ const child = (0, import_node_child_process5.spawn)(up, {
5473
5525
  cwd,
5474
5526
  shell: true,
5475
5527
  detached: true,
@@ -5759,22 +5811,29 @@ function safeJson(text, fallback) {
5759
5811
  return fallback;
5760
5812
  }
5761
5813
  }
5762
- async function ghJson(deps, args, fallback) {
5814
+ async function restJson(deps, path2, fallback) {
5763
5815
  try {
5764
- return safeJson((await deps.gh(args)).stdout, fallback);
5816
+ return await deps.client.rest("GET", path2) ?? fallback;
5817
+ } catch {
5818
+ return fallback;
5819
+ }
5820
+ }
5821
+ async function restPagedJson(deps, path2, fallback) {
5822
+ try {
5823
+ return await deps.client.restPaginate(path2);
5765
5824
  } catch {
5766
5825
  return fallback;
5767
5826
  }
5768
5827
  }
5769
5828
  async function resolveOwners(deps) {
5770
- const members = await ghJson(deps, ["api", `orgs/${OWNER}/members?role=admin`, "--paginate"], []);
5829
+ const members = await restPagedJson(deps, `orgs/${OWNER}/members?role=admin`, []);
5771
5830
  return members.map((m) => m.login);
5772
5831
  }
5773
5832
  function collaboratorRole(c) {
5774
5833
  return c.role_name ?? (c.permissions?.admin ? "admin" : c.permissions?.maintain ? "maintain" : "write");
5775
5834
  }
5776
5835
  async function auditRepoCollaborators(repo, owners, deps) {
5777
- const collabs = await ghJson(deps, ["api", `repos/${repo}/collaborators?affiliation=direct`, "--paginate"], []);
5836
+ const collabs = await restPagedJson(deps, `repos/${repo}/collaborators?affiliation=direct`, []);
5778
5837
  const findings = [];
5779
5838
  for (const c of collabs) {
5780
5839
  if (owners.has(c.login)) continue;
@@ -5795,7 +5854,7 @@ async function auditRepoCollaborators(repo, owners, deps) {
5795
5854
  async function auditTrainBranch(repo, branch, owners, deps, projectAdmins = /* @__PURE__ */ new Set()) {
5796
5855
  let restrictions = null;
5797
5856
  try {
5798
- restrictions = safeJson((await deps.gh(["api", `repos/${repo}/branches/${branch}/protection/restrictions`])).stdout, null);
5857
+ restrictions = await deps.client.rest("GET", `repos/${repo}/branches/${branch}/protection/restrictions`) ?? null;
5799
5858
  } catch {
5800
5859
  restrictions = null;
5801
5860
  }
@@ -5879,7 +5938,7 @@ async function auditRepoAccess(repo, repoClass, owners, deps, projectAdmins = /*
5879
5938
  return { repo, class: repoClass, ok: !findings.some((f) => f.severity === "high"), findings };
5880
5939
  }
5881
5940
  async function auditOrgBasePermission(deps) {
5882
- const org = await ghJson(deps, ["api", `orgs/${OWNER}`], {});
5941
+ const org = await restJson(deps, `orgs/${OWNER}`, {});
5883
5942
  const perm = org.default_repository_permission;
5884
5943
  if (perm && perm !== "read" && perm !== "none") {
5885
5944
  return [{
@@ -6022,9 +6081,16 @@ function safeJson2(text, fallback) {
6022
6081
  return fallback;
6023
6082
  }
6024
6083
  }
6025
- async function ghJson2(deps, args, fallback) {
6084
+ async function restJson2(deps, path2, fallback) {
6085
+ try {
6086
+ return await deps.client.rest("GET", path2) ?? fallback;
6087
+ } catch {
6088
+ return fallback;
6089
+ }
6090
+ }
6091
+ async function restPagedJson2(deps, path2, fallback) {
6026
6092
  try {
6027
- return safeJson2((await deps.gh(args)).stdout, fallback);
6093
+ return await deps.client.restPaginate(path2);
6028
6094
  } catch {
6029
6095
  return fallback;
6030
6096
  }
@@ -6032,7 +6098,7 @@ async function ghJson2(deps, args, fallback) {
6032
6098
  async function contentExists(deps, repo, branch, path2) {
6033
6099
  try {
6034
6100
  const encodedPath = path2.split("/").map(encodeURIComponent).join("/");
6035
- await deps.gh(["api", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`]);
6101
+ await deps.client.rest("GET", `repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(branch)}`);
6036
6102
  return true;
6037
6103
  } catch {
6038
6104
  return false;
@@ -6040,7 +6106,7 @@ async function contentExists(deps, repo, branch, path2) {
6040
6106
  }
6041
6107
  async function getProtection(deps, repo, branch) {
6042
6108
  try {
6043
- return safeJson2((await deps.gh(["api", `repos/${repo}/branches/${branch}/protection`])).stdout, {});
6109
+ return await deps.client.rest("GET", `repos/${repo}/branches/${branch}/protection`) ?? {};
6044
6110
  } catch {
6045
6111
  return null;
6046
6112
  }
@@ -6068,7 +6134,7 @@ async function rulesetDetails(deps, repo, list) {
6068
6134
  details.push(ruleset);
6069
6135
  continue;
6070
6136
  }
6071
- details.push(await ghJson2(deps, ["api", `repos/${repo}/rulesets/${ruleset.id}`], ruleset));
6137
+ details.push(await restJson2(deps, `repos/${repo}/rulesets/${ruleset.id}`, ruleset));
6072
6138
  }
6073
6139
  return details;
6074
6140
  }
@@ -6081,11 +6147,11 @@ async function verifyBootstrap(repo, repoClass, deps) {
6081
6147
  const branchesWanted = expectedBranches(repoClass);
6082
6148
  const baseBranch = repoClass === "content" ? "main" : "development";
6083
6149
  const checks = [];
6084
- const repoInfo = await ghJson2(deps, ["api", `repos/${repo}`], {});
6150
+ const repoInfo = await restJson2(deps, `repos/${repo}`, {});
6085
6151
  checks.push({ ok: Boolean(repoInfo.default_branch), label: "repo exists" });
6086
6152
  checks.push({ ok: repoInfo.default_branch === baseBranch, label: `default branch is ${baseBranch}`, detail: repoInfo.default_branch || "missing" });
6087
6153
  checks.push({ ok: repoInfo.has_wiki === true, label: "wiki enabled", detail: repoInfo.has_wiki === true ? void 0 : "has_wiki is false or unavailable" });
6088
- const branchList = await ghJson2(deps, ["api", `repos/${repo}/branches`, "--paginate"], []);
6154
+ const branchList = await restPagedJson2(deps, `repos/${repo}/branches`, []);
6089
6155
  const branchNames = new Set(branchList.map((b) => b.name));
6090
6156
  for (const branch of branchesWanted) {
6091
6157
  checks.push({ ok: branchNames.has(branch), label: `branch exists: ${branch}` });
@@ -6118,14 +6184,14 @@ async function verifyBootstrap(repo, repoClass, deps) {
6118
6184
  checks.push({ ok: await contentExists(deps, repo, baseBranch, trainScript), label: `train tooling script exists: ${trainScript}` });
6119
6185
  }
6120
6186
  checks.push({ ok: await contentExists(deps, repo, baseBranch, ".cursor/environment.json"), label: "Cursor environment committed" });
6121
- const labels = await ghJson2(deps, ["label", "list", "--repo", repo, "--limit", "200", "--json", "name"], []);
6187
+ const labels = await restPagedJson2(deps, `repos/${repo}/labels`, []);
6122
6188
  const labelNames = new Set(labels.map((l) => l.name));
6123
6189
  for (const label of requiredLabels) {
6124
6190
  checks.push({ ok: labelNames.has(label), label: `label exists: ${label}` });
6125
6191
  }
6126
6192
  const strays = strayDefaultLabels.filter((l) => labelNames.has(l));
6127
6193
  checks.push({ ok: strays.length === 0, label: "no stray GitHub-default labels", detail: presentDetail(strays) });
6128
- const actions = await ghJson2(deps, ["api", `repos/${repo}/actions/permissions`], {});
6194
+ const actions = await restJson2(deps, `repos/${repo}/actions/permissions`, {});
6129
6195
  checks.push({ ok: actions.enabled === true, label: "GitHub Actions enabled" });
6130
6196
  const config = deps.projectMeta ?? null;
6131
6197
  checks.push({
@@ -6133,12 +6199,18 @@ async function verifyBootstrap(repo, repoClass, deps) {
6133
6199
  label: "registry project board META exists"
6134
6200
  });
6135
6201
  if (config?.projectOwner && config.projectNumber != null) {
6136
- const project2 = await ghJson2(
6137
- deps,
6138
- ["project", "field-list", String(config.projectNumber), "--owner", config.projectOwner, "--format", "json"],
6139
- {}
6140
- );
6141
- const fields = project2.fields || [];
6202
+ const fieldsQuery = `query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { fields(first: 50) { nodes { ... on ProjectV2FieldCommon { id name } ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }`;
6203
+ const fields = await (async () => {
6204
+ try {
6205
+ const data = await deps.client.graphql(fieldsQuery, {
6206
+ login: config.projectOwner,
6207
+ number: config.projectNumber
6208
+ });
6209
+ return (data.organization?.projectV2?.fields?.nodes ?? []).filter((f) => Boolean(f?.id && f?.name));
6210
+ } catch {
6211
+ return [];
6212
+ }
6213
+ })();
6142
6214
  const statusField = fields.find((field) => field.name === "Status");
6143
6215
  const labelField = fields.find((field) => field.name === "Labels");
6144
6216
  checks.push({
@@ -6195,12 +6267,17 @@ async function verifyBootstrap(repo, repoClass, deps) {
6195
6267
  }
6196
6268
  }
6197
6269
  const workflowQuery = "query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { workflows(first: 30) { nodes { name enabled } } } } }";
6198
- const workflowResponse = await ghJson2(
6199
- deps,
6200
- ["api", "graphql", "-f", `query=${workflowQuery}`, "-f", `login=${config.projectOwner}`, "-F", `number=${config.projectNumber}`],
6201
- {}
6202
- );
6203
- const workflows = workflowResponse.data?.organization?.projectV2?.workflows?.nodes || [];
6270
+ const workflowResponse = await (async () => {
6271
+ try {
6272
+ return await deps.client.graphql(workflowQuery, {
6273
+ login: config.projectOwner,
6274
+ number: config.projectNumber
6275
+ });
6276
+ } catch {
6277
+ return {};
6278
+ }
6279
+ })();
6280
+ const workflows = workflowResponse.organization?.projectV2?.workflows?.nodes || [];
6204
6281
  for (const workflowName of requiredProjectWorkflows) {
6205
6282
  checks.push({
6206
6283
  ok: workflows.some((workflow) => workflow.name === workflowName && workflow.enabled === true),
@@ -6212,11 +6289,7 @@ async function verifyBootstrap(repo, repoClass, deps) {
6212
6289
  if (fanout != null) checks.push({ ok: fanout, label: `fanout target registered on ${baseBranch}` });
6213
6290
  const projectRegistry = localRegistryCheck(deps, "projects.json", (json) => Array.isArray(json?.projects) && json.projects.some((p) => (p.repos || []).includes(repo)));
6214
6291
  if (projectRegistry != null) checks.push({ ok: projectRegistry, label: "cloud-agent project registry includes repo" });
6215
- const rulesetList = await ghJson2(
6216
- deps,
6217
- ["api", `repos/${repo}/rulesets?includes_parents=true`],
6218
- []
6219
- );
6292
+ const rulesetList = await restJson2(deps, `repos/${repo}/rulesets?includes_parents=true`, []);
6220
6293
  const rulesets = await rulesetDetails(deps, repo, rulesetList);
6221
6294
  const activeOrgRulesets = rulesets.filter(
6222
6295
  (r) => r.source_type === "Organization" && r.target === "branch" && r.enforcement === "active"
@@ -6402,7 +6475,7 @@ var ORG_CONFIG_PATH = "/org/config";
6402
6475
  var PROJECTS_ENVELOPE_KEY = "projects";
6403
6476
 
6404
6477
  // src/registry-client.ts
6405
- var DEFAULT_TIMEOUT_MS = 8e3;
6478
+ var DEFAULT_TIMEOUT_MS2 = 8e3;
6406
6479
  async function fetchTrainAuthority(repo, deps) {
6407
6480
  if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
6408
6481
  const token = await deps.token();
@@ -6412,7 +6485,7 @@ async function fetchTrainAuthority(repo, deps) {
6412
6485
  const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}/train-authority?repo=${encodeURIComponent(repo)}`, {
6413
6486
  method: "GET",
6414
6487
  headers: { Authorization: `Bearer ${token}` },
6415
- signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS)
6488
+ signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
6416
6489
  });
6417
6490
  if (!res.ok) return { ok: false, error: `train-authority HTTP ${res.status}` };
6418
6491
  const body = await res.json();
@@ -6431,7 +6504,7 @@ async function fetchProjectsList(deps) {
6431
6504
  const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}${PROJECTS_LIST_PATH}`, {
6432
6505
  method: "GET",
6433
6506
  headers: { Authorization: `Bearer ${token}` },
6434
- signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS)
6507
+ signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
6435
6508
  });
6436
6509
  if (!res.ok) return null;
6437
6510
  const body = await res.json();
@@ -6455,7 +6528,7 @@ async function fetchProjectBySlugChecked(slug, deps) {
6455
6528
  const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}`, {
6456
6529
  method: "GET",
6457
6530
  headers: { Authorization: `Bearer ${token}` },
6458
- signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS)
6531
+ signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
6459
6532
  });
6460
6533
  if (res.status === 404) return { ok: true, project: null };
6461
6534
  if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
@@ -6477,7 +6550,7 @@ async function fetchDeployStatusBySlug(slug, deps) {
6477
6550
  const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}/deploy-status`, {
6478
6551
  method: "GET",
6479
6552
  headers: { Authorization: `Bearer ${token}` },
6480
- signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS)
6553
+ signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
6481
6554
  });
6482
6555
  if (!res.ok) return null;
6483
6556
  const body = await res.json();
@@ -6496,7 +6569,7 @@ async function fetchOrgConfig(deps) {
6496
6569
  const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}${ORG_CONFIG_PATH}`, {
6497
6570
  method: "GET",
6498
6571
  headers: { Authorization: `Bearer ${token}` },
6499
- signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS)
6572
+ signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
6500
6573
  });
6501
6574
  if (!res.ok) return null;
6502
6575
  return await res.json();
@@ -6514,7 +6587,7 @@ async function postJson(pathSuffix, payload, deps, method = "POST") {
6514
6587
  method,
6515
6588
  headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" },
6516
6589
  body: JSON.stringify(payload),
6517
- signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS)
6590
+ signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
6518
6591
  });
6519
6592
  let body = null;
6520
6593
  try {
@@ -6532,6 +6605,9 @@ async function registerProject(payload, deps) {
6532
6605
  async function upsertProject(slug, patch, deps) {
6533
6606
  return postJson(`/projects/${encodeURIComponent(slug)}`, patch, deps);
6534
6607
  }
6608
+ async function attestAppGaps(slug, repo, deps) {
6609
+ return postJson(`/projects/${encodeURIComponent(slug)}/attest-app`, { repo }, deps);
6610
+ }
6535
6611
  async function tenantControl(payload, deps) {
6536
6612
  return postJson("/tenant-control", payload, deps);
6537
6613
  }
@@ -6570,7 +6646,21 @@ function stageRequiredSecrets(stage2, meta) {
6570
6646
  function stageKey(stage2, key) {
6571
6647
  return key.includes("/") ? key : `${stage2}/${key}`;
6572
6648
  }
6649
+ function hasRuntimeSecretContract(contract) {
6650
+ return Boolean(contract) && !Array.isArray(contract);
6651
+ }
6652
+ function appAttestationOf(meta) {
6653
+ const a = meta?.appAttested;
6654
+ if (!a || typeof a !== "object" || Array.isArray(a)) return null;
6655
+ const { at, by } = a;
6656
+ return typeof at === "string" && at.length > 0 && typeof by === "string" && by.length > 0 ? { at, by } : null;
6657
+ }
6658
+ function attestedLine(att) {
6659
+ return `App-owned readiness attested by @${att.by} on ${att.at.slice(0, 10)} \u2014 the static checklist is cleared (the doctor reads no product repo files); re-run \`mmi-cli project attest\` after app-owned structural changes.`;
6660
+ }
6573
6661
  function appGapsFor(meta, model, slug, projectType) {
6662
+ const attested = appAttestationOf(meta);
6663
+ if (attested) return [attestedLine(attested)];
6574
6664
  if (projectType === "content" || model === "content") return ["Content/KB repo: keep app-owned work to docs/content changes; release train does not apply."];
6575
6665
  if (projectType === "desktop-game") {
6576
6666
  return [
@@ -6602,6 +6692,9 @@ function appGapsFor(meta, model, slug, projectType) {
6602
6692
  "Make app config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
6603
6693
  "Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
6604
6694
  ];
6695
+ if (meta && !hasRuntimeSecretContract(meta.requiredRuntimeSecrets)) {
6696
+ gaps.unshift("No runtime secrets declared \u2014 declare requiredRuntimeSecrets (a per-stage name map) in the registry META, or attest the app needs none with an explicit empty stage map ({ dev: [], rc: [], main: [] }).");
6697
+ }
6605
6698
  if (slug === "mmi-katip") {
6606
6699
  gaps.push("Katip-specific app plan: declare Google Workspace service-account requirements, use the service account numeric OAuth2 client ID for DWD, remove prod-hidden impersonation defaults, and make non-critical Google Workspace failures degrade instead of crash-looping.");
6607
6700
  }
@@ -6741,7 +6834,8 @@ async function buildV2Doctor(repoOrSlug, deps) {
6741
6834
  hubOwned: { meta: { ok: metaMissing.length === 0, missing: metaMissing }, deployCoords, deployState, secrets: secrets2 },
6742
6835
  secretsError,
6743
6836
  autoHealAvailable: Object.keys(autoHeal.patch),
6744
- appOwnedGaps: autoHeal.appOwnedGaps
6837
+ appOwnedGaps: autoHeal.appOwnedGaps,
6838
+ appAttested: appAttestationOf(meta) ?? void 0
6745
6839
  };
6746
6840
  }
6747
6841
  function renderReadinessIssueBody(existingBody, report, opts = {}) {
@@ -6834,6 +6928,10 @@ function buildProjectSetPatch(input) {
6834
6928
  }
6835
6929
  return patch;
6836
6930
  }
6931
+ function repoFromRemoteUrl(remoteUrl) {
6932
+ const m = remoteUrl.trim().match(/^(?:[a-z][a-z0-9+.-]*:\/\/)?(?:[^@\s/]+@)?github\.com[:/]([^/\s:]+)\/([^/\s]+?)(?:\.git)?\/?$/i);
6933
+ return m ? `${m[1]}/${m[2]}` : void 0;
6934
+ }
6837
6935
  function requireProjectTarget(commandName, explicitTarget, currentRepo) {
6838
6936
  const target = explicitTarget?.trim() || currentRepo?.trim();
6839
6937
  if (!target) {
@@ -7113,12 +7211,14 @@ function formatVaultPointer(p) {
7113
7211
  }
7114
7212
  var TIMEOUT_MS2 = 8e3;
7115
7213
  var repoOf = (slug) => `${OWNER2}/${slug}`;
7214
+ async function vaultSlug(deps, opts) {
7215
+ return (opts.repo ? opts.repo.split("/").pop() : await deps.slug()).toLowerCase();
7216
+ }
7116
7217
  async function targetRepo(deps, opts) {
7117
- return opts.repo ?? repoOf(await deps.slug());
7218
+ return opts.repo ?? repoOf(await vaultSlug(deps, opts));
7118
7219
  }
7119
7220
  async function secretsWhere(deps, opts) {
7120
- const slug = opts.repo ? opts.repo.split("/").pop().toLowerCase() : await deps.slug();
7121
- deps.log(formatVaultPointer(vaultPointer(slug)));
7221
+ deps.log(formatVaultPointer(vaultPointer(await vaultSlug(deps, opts))));
7122
7222
  }
7123
7223
  async function readErr(res) {
7124
7224
  try {
@@ -7300,7 +7400,7 @@ async function secretsSet(deps, key, opts) {
7300
7400
  );
7301
7401
  return;
7302
7402
  }
7303
- deps.log(`set ${key} (${classifyTier(await deps.slug(), key)} tier)`);
7403
+ deps.log(`set ${key} (${classifyTier(await vaultSlug(deps, opts), key)} tier)`);
7304
7404
  }
7305
7405
  async function secretsEdit(deps, key, opts) {
7306
7406
  return secretsSet(deps, key, opts);
@@ -7352,8 +7452,8 @@ async function secretsRevoke(deps, repo, login, key, _opts) {
7352
7452
  }
7353
7453
  deps.log(`revoked @${login}'s access to ${key} in ${repo}`);
7354
7454
  }
7355
- async function secretsUse(deps, key, _opts) {
7356
- const slug = await deps.slug();
7455
+ async function secretsUse(deps, key, opts) {
7456
+ const slug = await vaultSlug(deps, opts);
7357
7457
  const tier = classifyTier(slug, key);
7358
7458
  const path2 = secretParamName(slug, key);
7359
7459
  deps.log(
@@ -7438,30 +7538,23 @@ function authorizeBodyHasMismatch(body) {
7438
7538
  }
7439
7539
 
7440
7540
  // src/index.ts
7441
- var rawExecFileP2 = (0, import_node_util5.promisify)(import_node_child_process5.execFile);
7541
+ var rawExecFileP3 = (0, import_node_util6.promisify)(import_node_child_process6.execFile);
7442
7542
  var DEFAULT_EXEC_TIMEOUT_MS = 1e4;
7443
- var execFileP3 = (file, args, options = {}) => (
7543
+ var execFileP4 = (file, args, options = {}) => (
7444
7544
  // encoding 'utf8' guarantees string stdout/stderr at runtime; the cast pins the type because
7445
7545
  // promisify(execFile)'s overloads widen to string|Buffer when options is spread in.
7446
- rawExecFileP2(file, args, { encoding: "utf8", windowsHide: true, timeout: DEFAULT_EXEC_TIMEOUT_MS, killSignal: "SIGTERM", ...options })
7546
+ rawExecFileP3(file, args, { encoding: "utf8", windowsHide: true, timeout: DEFAULT_EXEC_TIMEOUT_MS, killSignal: "SIGTERM", ...options })
7447
7547
  );
7448
7548
  var GIT_TIMEOUT_MS = DEFAULT_EXEC_TIMEOUT_MS;
7449
7549
  var GC_GH_TIMEOUT_MS = 2e4;
7450
- var cachedGhCliToken;
7451
- async function githubToken() {
7452
- if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
7453
- if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN;
7454
- cachedGhCliToken ??= execFileP3("gh", ["auth", "token"]).then(({ stdout }) => stdout.trim() || void 0).catch(() => void 0);
7455
- return cachedGhCliToken;
7456
- }
7457
7550
  var cachedGithubLogin;
7458
7551
  async function githubLogin() {
7459
- cachedGithubLogin ??= execFileP3("gh", ["api", "user", "--jq", ".login"]).then(({ stdout }) => stdout.trim() || void 0).catch(() => void 0);
7552
+ cachedGithubLogin ??= execFileP4("gh", ["api", "user", "--jq", ".login"]).then(({ stdout }) => stdout.trim() || void 0).catch(() => void 0);
7460
7553
  return cachedGithubLogin;
7461
7554
  }
7462
7555
  async function awsCallerArn() {
7463
7556
  try {
7464
- const { stdout } = await execFileP3(
7557
+ const { stdout } = await execFileP4(
7465
7558
  "aws",
7466
7559
  ["sts", "get-caller-identity", "--query", "Arn", "--output", "text"],
7467
7560
  { timeout: GIT_TIMEOUT_MS }
@@ -7521,7 +7614,7 @@ var DEFAULT_RULES_SOURCE = "https://raw.githubusercontent.com/mutmutco/MMI-Hub/d
7521
7614
  var SESSION_FILE = ".mmi/.session";
7522
7615
  var gitOut = async (args) => {
7523
7616
  try {
7524
- return (await execFileP3("git", [...args])).stdout.trim();
7617
+ return (await execFileP4("git", [...args])).stdout.trim();
7525
7618
  } catch {
7526
7619
  return "";
7527
7620
  }
@@ -7593,18 +7686,18 @@ async function readStdin() {
7593
7686
  async function ghPrs(limit) {
7594
7687
  const args = (state) => ["pr", "list", "--state", state, "--limit", String(limit), "--json", "number,headRefName,state"];
7595
7688
  const [open, closed] = await Promise.all([
7596
- execFileP3("gh", args("open"), { timeout: GC_GH_TIMEOUT_MS }),
7597
- execFileP3("gh", args("closed"), { timeout: GC_GH_TIMEOUT_MS })
7689
+ execFileP4("gh", args("open"), { timeout: GC_GH_TIMEOUT_MS }),
7690
+ execFileP4("gh", args("closed"), { timeout: GC_GH_TIMEOUT_MS })
7598
7691
  ]);
7599
7692
  return [...JSON.parse(open.stdout || "[]"), ...JSON.parse(closed.stdout || "[]")];
7600
7693
  }
7601
7694
  async function worktreeBranches() {
7602
- const { stdout } = await execFileP3("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
7695
+ const { stdout } = await execFileP4("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
7603
7696
  const parsed = parseWorktreePorcelain(stdout);
7604
7697
  return await Promise.all(parsed.map(async (w) => {
7605
7698
  let dirty = true;
7606
7699
  try {
7607
- const { stdout: status } = await execFileP3("git", ["-C", w.path, "status", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
7700
+ const { stdout: status } = await execFileP4("git", ["-C", w.path, "status", "--porcelain"], { timeout: GIT_TIMEOUT_MS });
7608
7701
  dirty = status.trim().length > 0;
7609
7702
  } catch {
7610
7703
  dirty = true;
@@ -7616,7 +7709,7 @@ async function gcPlan(remote, limit) {
7616
7709
  const [branches, current, stale, prs, worktrees] = await Promise.all([
7617
7710
  gitOut(["branch", "--format=%(refname:short)"]),
7618
7711
  gitOut(["rev-parse", "--abbrev-ref", "HEAD"]),
7619
- execFileP3("git", ["remote", "prune", remote, "--dry-run"], { timeout: GIT_TIMEOUT_MS }).then((r) => parseRemotePruneDryRun(`${r.stdout}${r.stderr}`)).catch(() => []),
7712
+ execFileP4("git", ["remote", "prune", remote, "--dry-run"], { timeout: GIT_TIMEOUT_MS }).then((r) => parseRemotePruneDryRun(`${r.stdout}${r.stderr}`)).catch(() => []),
7620
7713
  ghPrs(limit),
7621
7714
  worktreeBranches()
7622
7715
  ]);
@@ -7644,8 +7737,8 @@ async function applyGcPlan(plan2, remote) {
7644
7737
  const result = { removedBranches: [], removedTrackingRefs: [], failed: [], pruned: false };
7645
7738
  for (const branch of plan2.branches) {
7646
7739
  try {
7647
- if (branch.worktreePath) await execFileP3("git", ["worktree", "remove", "--force", branch.worktreePath], { timeout: GIT_TIMEOUT_MS });
7648
- await execFileP3("git", ["branch", "-D", branch.branch], { timeout: GIT_TIMEOUT_MS });
7740
+ if (branch.worktreePath) await execFileP4("git", ["worktree", "remove", "--force", branch.worktreePath], { timeout: GIT_TIMEOUT_MS });
7741
+ await execFileP4("git", ["branch", "-D", branch.branch], { timeout: GIT_TIMEOUT_MS });
7649
7742
  result.removedBranches.push(branch.branch);
7650
7743
  } catch (e) {
7651
7744
  result.failed.push(`${branch.branch}: ${e.message.split("\n")[0]}`);
@@ -7653,7 +7746,7 @@ async function applyGcPlan(plan2, remote) {
7653
7746
  }
7654
7747
  for (const ref of plan2.trackingRefs) {
7655
7748
  try {
7656
- await execFileP3("git", ["update-ref", "-d", `refs/remotes/${remote}/${ref.branch}`], { timeout: GIT_TIMEOUT_MS });
7749
+ await execFileP4("git", ["update-ref", "-d", `refs/remotes/${remote}/${ref.branch}`], { timeout: GIT_TIMEOUT_MS });
7657
7750
  result.removedTrackingRefs.push(ref.branch);
7658
7751
  } catch (e) {
7659
7752
  result.failed.push(`${remote}/${ref.branch}: ${e.message.split("\n")[0]}`);
@@ -7661,7 +7754,7 @@ async function applyGcPlan(plan2, remote) {
7661
7754
  }
7662
7755
  if (plan2.branches.some((b) => b.worktreePath)) {
7663
7756
  try {
7664
- await execFileP3("git", ["worktree", "prune"], { timeout: GIT_TIMEOUT_MS });
7757
+ await execFileP4("git", ["worktree", "prune"], { timeout: GIT_TIMEOUT_MS });
7665
7758
  result.pruned = true;
7666
7759
  } catch (e) {
7667
7760
  result.failed.push(`worktree prune: ${e.message.split("\n")[0]}`);
@@ -7691,7 +7784,7 @@ function readRepoVersion() {
7691
7784
  }
7692
7785
  async function fetchReleasedVersion() {
7693
7786
  try {
7694
- const { stdout } = await execFileP3("gh", pluginManifestVersionArgs(), { timeout: 5e3 });
7787
+ const { stdout } = await execFileP4("gh", pluginManifestVersionArgs(), { timeout: 5e3 });
7695
7788
  return parseManifestVersion(stdout);
7696
7789
  } catch {
7697
7790
  return void 0;
@@ -7705,9 +7798,9 @@ async function applyVersionAutoUpdate(report, log) {
7705
7798
  const target = report.releasedVersion ?? "latest";
7706
7799
  if (action === "plugin-pull") {
7707
7800
  try {
7708
- const root = (await execFileP3("git", ["-C", process.env.CLAUDE_PLUGIN_ROOT, "rev-parse", "--show-toplevel"], { timeout: PLUGIN_PULL_TIMEOUT_MS })).stdout.trim();
7801
+ const root = (await execFileP4("git", ["-C", process.env.CLAUDE_PLUGIN_ROOT, "rev-parse", "--show-toplevel"], { timeout: PLUGIN_PULL_TIMEOUT_MS })).stdout.trim();
7709
7802
  log(` \u21BB refreshing MMI plugin ${report.currentVersion} \u2192 ${target} (effective next session)\u2026`);
7710
- await execFileP3("git", ["-C", root, "pull", "--ff-only"], { timeout: PLUGIN_PULL_TIMEOUT_MS });
7803
+ await execFileP4("git", ["-C", root, "pull", "--ff-only"], { timeout: PLUGIN_PULL_TIMEOUT_MS });
7711
7804
  return { ...report, ok: true };
7712
7805
  } catch {
7713
7806
  return report;
@@ -7734,7 +7827,7 @@ async function requireFreshTrainCli(commandName) {
7734
7827
  var consoleIo = { log: (m) => console.log(m), err: (m) => console.error(m) };
7735
7828
  var CLAUDE_PLUGIN_TIMEOUT_MS = 12e4;
7736
7829
  function runHostBin(bin, args, opts) {
7737
- return isWin ? execFileP3("cmd.exe", ["/c", bin, ...args], opts) : execFileP3(bin, args, opts);
7830
+ return isWin ? execFileP4("cmd.exe", ["/c", bin, ...args], opts) : execFileP4(bin, args, opts);
7738
7831
  }
7739
7832
  async function runClaudePlugin(args) {
7740
7833
  try {
@@ -7805,7 +7898,7 @@ async function runDocsSync(opts, io = consoleIo) {
7805
7898
  isDirty: async (f) => await gitOut(["status", "--porcelain", "--", f]) !== "",
7806
7899
  originContent: async (f) => {
7807
7900
  try {
7808
- return (await execFileP3("git", ["show", `origin/${def}:${f}`], { maxBuffer: 10 * 1024 * 1024 })).stdout;
7901
+ return (await execFileP4("git", ["show", `origin/${def}:${f}`], { maxBuffer: 10 * 1024 * 1024 })).stdout;
7809
7902
  } catch {
7810
7903
  return null;
7811
7904
  }
@@ -7838,14 +7931,17 @@ async function runSagaShow(opts, io = consoleIo) {
7838
7931
  try {
7839
7932
  const key = await sagaKey(cfg);
7840
7933
  const qs = opts.latestAnywhere ? "scope=anywhere" : new URLSearchParams({ project: key.project, branch: key.branch }).toString();
7841
- const res = await fetch(`${cfg.sagaApiUrl}/saga/head?${qs}`, { headers: await sagaHeaders(), signal: AbortSignal.timeout(8e3) });
7934
+ const res = await fetch(`${cfg.sagaApiUrl}/saga/head?${qs}`, { headers: await sagaHeaders(), signal: AbortSignal.timeout(3e3) });
7842
7935
  if (res.ok) {
7843
7936
  io.log(resumeCue());
7844
7937
  return io.log(await res.text());
7845
7938
  }
7846
7939
  if (!opts.quiet) io.log(`saga show failed: HTTP ${res.status}`);
7847
7940
  } catch (e) {
7848
- if (!opts.quiet) io.err(`saga show: ${e.message}`);
7941
+ if (!opts.quiet) {
7942
+ const reason = e.name === "TimeoutError" ? "backend unreachable (timed out after 3s)" : e.message;
7943
+ io.err(`saga show: ${reason} \u2014 continuing without saga; diagnose with \`mmi-cli saga health --json\``);
7944
+ }
7849
7945
  }
7850
7946
  }
7851
7947
  saga.command("show").option("--quiet", "no-op silently when unconfigured/unreachable (SessionStart hook)").option("--latest-anywhere", "resume the newest saga across all repos (default: current repo)").description("print your resume block \u2014 current repo HEAD + project memory (where you left off)").action((opts) => runSagaShow(opts));
@@ -7865,7 +7961,7 @@ saga.command("head-update").option("--run", "detached worker: fetch state, run t
7865
7961
  if (!headGateDue(tsPath)) return;
7866
7962
  markHeadRun(tsPath);
7867
7963
  try {
7868
- (0, import_node_child_process5.spawn)(process.execPath, [process.argv[1], "saga", "head-update", "--run"], {
7964
+ (0, import_node_child_process6.spawn)(process.execPath, [process.argv[1], "saga", "head-update", "--run"], {
7869
7965
  detached: true,
7870
7966
  stdio: "ignore",
7871
7967
  windowsHide: true
@@ -7982,7 +8078,7 @@ var kb = program2.command("kb").description("org knowledgebase (read-only)");
7982
8078
  kb.command("get <path>").description("print a KB document by path").action(async (path2) => {
7983
8079
  const src = resolveKbSource((await loadConfig()).kbSource);
7984
8080
  try {
7985
- const { stdout } = await execFileP3("gh", buildKbGetArgs(src, path2), { timeout: 1e4 });
8081
+ const { stdout } = await execFileP4("gh", buildKbGetArgs(src, path2), { timeout: 1e4 });
7986
8082
  process.stdout.write(stdout);
7987
8083
  } catch (e) {
7988
8084
  const err = e;
@@ -7992,7 +8088,7 @@ kb.command("get <path>").description("print a KB document by path").action(async
7992
8088
  kb.command("list [prefix]").description("list KB document paths (optionally under a prefix)").action(async (prefix) => {
7993
8089
  const src = resolveKbSource((await loadConfig()).kbSource);
7994
8090
  try {
7995
- const { stdout } = await execFileP3("gh", buildKbTreeArgs(src), { timeout: 1e4 });
8091
+ const { stdout } = await execFileP4("gh", buildKbTreeArgs(src), { timeout: 1e4 });
7996
8092
  const paths = parseKbTree(stdout, prefix);
7997
8093
  if (!paths.length) return fail(`kb list: no documents${prefix ? ` under ${prefix}` : ""}`);
7998
8094
  console.log(paths.join("\n"));
@@ -8003,21 +8099,23 @@ kb.command("list [prefix]").description("list KB document paths (optionally unde
8003
8099
  });
8004
8100
  async function ghCreate(args) {
8005
8101
  try {
8006
- const { stdout } = await execFileP3("gh", args);
8102
+ const { stdout } = await execFileP4("gh", args);
8007
8103
  return parseCreatedUrl(stdout);
8008
8104
  } catch (e) {
8009
8105
  const err = e;
8010
8106
  fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}`);
8011
8107
  }
8012
8108
  }
8013
- async function ghJson3(args, timeout = 1e4) {
8014
- const { stdout } = await execFileP3("gh", args, { timeout });
8109
+ async function ghJson(args, timeout = 1e4) {
8110
+ const { stdout } = await execFileP4("gh", args, { timeout });
8015
8111
  return JSON.parse(stdout);
8016
8112
  }
8017
8113
  async function resolveRepo(repo) {
8018
8114
  if (repo) return repo;
8115
+ const fromOrigin = repoFromRemoteUrl(await gitOut(["remote", "get-url", "origin"]));
8116
+ if (fromOrigin) return fromOrigin;
8019
8117
  try {
8020
- const { stdout } = await execFileP3("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { timeout: 5e3 });
8118
+ const { stdout } = await execFileP4("gh", ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], { timeout: 5e3 });
8021
8119
  return stdout.trim() || void 0;
8022
8120
  } catch {
8023
8121
  return void 0;
@@ -8033,19 +8131,14 @@ async function attachToProject(issueNumber, repo, priority) {
8033
8131
  try {
8034
8132
  const viewArgs = ["issue", "view", String(issueNumber), "--json", "id", "--jq", ".id"];
8035
8133
  if (targetRepo2) viewArgs.push("--repo", targetRepo2);
8036
- const { stdout: idOut } = await execFileP3("gh", viewArgs, { timeout: 1e4 });
8134
+ const { stdout: idOut } = await execFileP4("gh", viewArgs, { timeout: 1e4 });
8037
8135
  const contentId = idOut.trim();
8038
8136
  if (!contentId) throw new Error("could not resolve issue node id");
8039
- const { stdout } = await execFileP3("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout: 1e4 });
8137
+ const { stdout } = await execFileP4("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout: 1e4 });
8040
8138
  const projectItemId = parseAddedItemId(stdout);
8041
8139
  if (projectItemId && priority) {
8042
8140
  try {
8043
- await setBoardItemPriority(
8044
- async (args) => execFileP3("gh", args, { timeout: 1e4 }),
8045
- cfg,
8046
- projectItemId,
8047
- priority
8048
- );
8141
+ await setBoardItemPriority(defaultGitHubClient(), cfg, projectItemId, priority);
8049
8142
  } catch (e) {
8050
8143
  const err = e;
8051
8144
  process.stderr.write(`warning: issue #${issueNumber} board Priority not set: ${(err.stderr || err.message || String(e)).trim()}
@@ -8064,7 +8157,7 @@ function scheduleRelatedDiscovery(o) {
8064
8157
  try {
8065
8158
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
8066
8159
  if (o.repo) args.push("--repo", o.repo);
8067
- (0, import_node_child_process5.spawn)(process.execPath, [process.argv[1], ...args], {
8160
+ (0, import_node_child_process6.spawn)(process.execPath, [process.argv[1], ...args], {
8068
8161
  detached: true,
8069
8162
  stdio: "ignore",
8070
8163
  windowsHide: true,
@@ -8120,7 +8213,7 @@ function openInEditor(path2) {
8120
8213
  return;
8121
8214
  }
8122
8215
  try {
8123
- (0, import_node_child_process5.spawn)(editor, [path2], { stdio: "inherit" });
8216
+ (0, import_node_child_process6.spawn)(editor, [path2], { stdio: "inherit" });
8124
8217
  } catch {
8125
8218
  console.log(`open ${path2} manually`);
8126
8219
  }
@@ -8178,7 +8271,9 @@ function makeSecretsDeps(cfg) {
8178
8271
  apiUrl: cfg.sagaApiUrl,
8179
8272
  fetch: (url, init = {}) => fetch(url, { ...init, signal: init.signal ?? AbortSignal.timeout(1e4) }),
8180
8273
  headers: (extra) => sagaHeaders(extra),
8181
- slug: async () => (await sagaKey(cfg)).project,
8274
+ // Vault paths are lowercase kebab (AGENTS.md naming); sagaKey carries the repo name's original
8275
+ // casing, which leaked mixed-case into `secrets where` output (#681).
8276
+ slug: async () => (await sagaKey(cfg)).project.toLowerCase(),
8182
8277
  readSecretValue: () => readSecretStdin(),
8183
8278
  log: (m) => console.log(m),
8184
8279
  err: (m) => console.error(m)
@@ -8278,22 +8373,22 @@ async function v2ReadinessDeps(cfg) {
8278
8373
  async function updateV2ReadinessIssue(repo, report, healed) {
8279
8374
  const title = "v2 readiness: central deploy + secrets alignment";
8280
8375
  const freshBody = renderReadinessIssueBody("", report, { healed });
8281
- const list = await execFileP3("gh", ["issue", "list", "--repo", repo, "--state", "open", "--search", "v2 readiness in:title", "--json", "number,title", "--limit", "20"], { timeout: 2e4 });
8376
+ const list = await execFileP4("gh", ["issue", "list", "--repo", repo, "--state", "open", "--search", "v2 readiness in:title", "--json", "number,title", "--limit", "20"], { timeout: 2e4 });
8282
8377
  const issues = JSON.parse(list.stdout || "[]");
8283
8378
  const existing = issues.find((i) => i.title.toLowerCase().includes("v2 readiness"));
8284
8379
  if (!existing) {
8285
- const created = await execFileP3("gh", ["issue", "create", "--repo", repo, "--title", title, "--body", freshBody, "--label", "feature"], { timeout: 2e4 });
8380
+ const created = await execFileP4("gh", ["issue", "create", "--repo", repo, "--title", title, "--body", freshBody, "--label", "feature"], { timeout: 2e4 });
8286
8381
  const url = created.stdout.trim();
8287
8382
  const number = Number(url.match(/\/issues\/(\d+)$/)?.[1] ?? 0);
8288
8383
  return { number, url, action: "created" };
8289
8384
  }
8290
- const view = await execFileP3("gh", ["issue", "view", String(existing.number), "--repo", repo, "--json", "body,url", "--jq", "{body:.body,url:.url}"], { timeout: 2e4 });
8385
+ const view = await execFileP4("gh", ["issue", "view", String(existing.number), "--repo", repo, "--json", "body,url", "--jq", "{body:.body,url:.url}"], { timeout: 2e4 });
8291
8386
  const current = JSON.parse(view.stdout || "{}");
8292
8387
  const nextBody = renderReadinessIssueBody(current.body ?? "", report, { healed });
8293
- await execFileP3("gh", ["issue", "edit", String(existing.number), "--repo", repo, "--body", nextBody], { timeout: 2e4 });
8388
+ await execFileP4("gh", ["issue", "edit", String(existing.number), "--repo", repo, "--body", nextBody], { timeout: 2e4 });
8294
8389
  return { number: existing.number, url: current.url ?? `https://github.com/${repo}/issues/${existing.number}`, action: "updated" };
8295
8390
  }
8296
- var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); set is master-only");
8391
+ var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); attest is project-admin; set is master-only");
8297
8392
  async function projectTarget(commandName, explicitTarget) {
8298
8393
  return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
8299
8394
  }
@@ -8330,7 +8425,7 @@ project.command("resolve <owner/repo>").description("deploy coords for a stage \
8330
8425
  }
8331
8426
  fail(msg);
8332
8427
  });
8333
- project.command("doctor [owner/repo]").description("diagnose Hub v2 readiness for a repo without reading product repo files; defaults to the current repo").option("--v2", "compatibility flag; v2 readiness is the default").option("--json", "machine-readable output").action(async (repo, _o) => {
8428
+ project.command("doctor [owner/repo]").description("diagnose Hub v2 readiness for a repo without reading product repo files \u2014 appOwnedGaps are advisory templates; clear them with `project attest`; defaults to the current repo").option("--v2", "compatibility flag; v2 readiness is the default").option("--json", "machine-readable output").action(async (repo, _o) => {
8334
8429
  const cfg = await loadConfig();
8335
8430
  let target;
8336
8431
  try {
@@ -8380,6 +8475,18 @@ project.command("readiness [owner/repo]").description("update the repo v2 readin
8380
8475
  const issue2 = await updateV2ReadinessIssue(target, report, []);
8381
8476
  console.log(JSON.stringify({ ok: true, repo: target, issue: issue2, ready: report.ok }));
8382
8477
  });
8478
+ project.command("attest [owner/repo]").description("attest this repo's app-owned v2 readiness work is done (project-admin of the repo or master) \u2014 clears doctor's static appOwnedGaps; re-running overwrites; defaults to the current repo").option("--json", "machine-readable output").action(async (repoOrSlug) => {
8479
+ const cfg = await loadConfig();
8480
+ let target;
8481
+ try {
8482
+ target = await projectTarget("project attest", repoOrSlug);
8483
+ } catch (e) {
8484
+ return fail(e.message);
8485
+ }
8486
+ const repo = target.includes("/") ? target : `mutmutco/${slugOf(target)}`;
8487
+ const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
8488
+ reportWrite("project attest", res);
8489
+ });
8383
8490
  project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project META (idempotent merge; defaults to the current repo; no clobber of unspecified fields)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--var <KEY=VALUE...>", "META field to set (repeatable): name, division, projectId, branch, wikiRepo, vaultPath, kbPointer").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
8384
8491
  const cfg = await loadConfig();
8385
8492
  let target;
@@ -8494,7 +8601,7 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
8494
8601
  const la = ["label", "create", label, "--color", "ededed"];
8495
8602
  if (o.repo) la.push("--repo", o.repo);
8496
8603
  try {
8497
- await execFileP3("gh", la, { timeout: 1e4 });
8604
+ await execFileP4("gh", la, { timeout: 1e4 });
8498
8605
  } catch {
8499
8606
  }
8500
8607
  }
@@ -8509,7 +8616,7 @@ issue.command("discover-related").description("find related issues for an existi
8509
8616
  const repo = await resolveRepo(o.repo);
8510
8617
  if (!repo) return fail("issue discover-related: could not resolve repo");
8511
8618
  try {
8512
- const issues = await ghJson3([
8619
+ const issues = await ghJson([
8513
8620
  "issue",
8514
8621
  "list",
8515
8622
  "--repo",
@@ -8524,7 +8631,7 @@ issue.command("discover-related").description("find related issues for an existi
8524
8631
  const candidates = findRelatedIssues({ number, title: o.title, body: o.body }, issues);
8525
8632
  if (o.json) return console.log(JSON.stringify({ number, repo, candidates }, null, 2));
8526
8633
  if (!candidates.length) return;
8527
- const viewed = await ghJson3([
8634
+ const viewed = await ghJson([
8528
8635
  "issue",
8529
8636
  "view",
8530
8637
  String(number),
@@ -8534,11 +8641,11 @@ issue.command("discover-related").description("find related issues for an existi
8534
8641
  "comments"
8535
8642
  ]);
8536
8643
  if (viewed.comments.some((comment) => comment.body.includes(relatedMarker(number)))) return;
8537
- await execFileP3("gh", ["issue", "comment", String(number), "--repo", repo, "--body", buildRelatedComment(number, candidates)], { timeout: 1e4 });
8644
+ await execFileP4("gh", ["issue", "comment", String(number), "--repo", repo, "--body", buildRelatedComment(number, candidates)], { timeout: 1e4 });
8538
8645
  } catch {
8539
8646
  }
8540
8647
  });
8541
- program2.command("report").description("file a friction report on the Hub board (GitHub auth, dedups open reports) and print {number,url} JSON").requiredOption("--title <title>", "one-line friction summary").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").option("--type <type>", "bug | feature | task (sets the matching label)", "task").option("--priority <priority>", "urgent | high | medium | low (board Priority field when configured)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO})`).option("--force", "file a new issue even when an open report looks like a duplicate").action(async (o) => {
8648
+ program2.command("report").description("file a friction report on the Hub board (GitHub auth, dedups open reports) and print {number,url} JSON").requiredOption("--title <title>", "one-line friction summary").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").option("--type <type>", "bug | feature | task (sets the matching label)", "task").option("--priority <priority>", "urgent | high | medium | low (board Priority field when configured)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO})`).option("--force", "file a new issue even when an open report looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 report always prints JSON; #682)").action(async (o) => {
8542
8649
  let body;
8543
8650
  let priority;
8544
8651
  let args;
@@ -8561,7 +8668,7 @@ program2.command("report").description("file a friction report on the Hub board
8561
8668
  if (!o.force) {
8562
8669
  let openReports = [];
8563
8670
  try {
8564
- openReports = await ghJson3([
8671
+ openReports = await ghJson([
8565
8672
  "issue",
8566
8673
  "list",
8567
8674
  "--repo",
@@ -8580,7 +8687,7 @@ program2.command("report").description("file a friction report on the Hub board
8580
8687
  const dup = findDuplicateReport({ title: o.title, body }, openReports);
8581
8688
  if (dup) {
8582
8689
  try {
8583
- await execFileP3("gh", ["issue", "comment", String(dup.number), "--repo", targetRepo2, "--body", buildDupComment(dup.number, body, sourceRepo)], { timeout: 1e4 });
8690
+ await execFileP4("gh", ["issue", "comment", String(dup.number), "--repo", targetRepo2, "--body", buildDupComment(dup.number, body, sourceRepo)], { timeout: 1e4 });
8584
8691
  } catch (e) {
8585
8692
  const err = e;
8586
8693
  return fail(`report: duplicate of #${dup.number} (${dup.url}) but the +1 comment failed: ${(err.stderr || err.message || String(e)).trim()}`);
@@ -8589,7 +8696,7 @@ program2.command("report").description("file a friction report on the Hub board
8589
8696
  }
8590
8697
  }
8591
8698
  try {
8592
- await execFileP3("gh", ["label", "create", REPORT_LABEL, "--color", "ededed", "--repo", targetRepo2], { timeout: 1e4 });
8699
+ await execFileP4("gh", ["label", "create", REPORT_LABEL, "--color", "ededed", "--repo", targetRepo2], { timeout: 1e4 });
8593
8700
  } catch {
8594
8701
  }
8595
8702
  const created = await ghCreate(args);
@@ -8604,7 +8711,7 @@ pr.command("create").description("create a PR and print {number,url} JSON").requ
8604
8711
  async function remoteBranchExists(branch) {
8605
8712
  if (!branch) return void 0;
8606
8713
  try {
8607
- const { stdout } = await execFileP3("git", ["ls-remote", "--heads", "origin", branch], { timeout: GIT_TIMEOUT_MS });
8714
+ const { stdout } = await execFileP4("git", ["ls-remote", "--heads", "origin", branch], { timeout: GIT_TIMEOUT_MS });
8608
8715
  return stdout.trim().length > 0;
8609
8716
  } catch {
8610
8717
  return void 0;
@@ -8613,15 +8720,15 @@ async function remoteBranchExists(branch) {
8613
8720
  pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 no leftover local branch").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (number, o) => {
8614
8721
  const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
8615
8722
  const repoArgs = o.repo ? ["--repo", o.repo] : [];
8616
- const headRef = (await execFileP3("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName", "--jq", ".headRefName"], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
8617
- const startingPath = (await execFileP3("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
8723
+ const headRef = (await execFileP4("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName", "--jq", ".headRefName"], { timeout: GC_GH_TIMEOUT_MS })).stdout.trim();
8724
+ const startingPath = (await execFileP4("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
8618
8725
  const beforeWorktrees = parseWorktreePorcelain(
8619
- (await execFileP3("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
8726
+ (await execFileP4("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
8620
8727
  );
8621
8728
  const remoteBefore = repoArgs.length ? void 0 : await remoteBranchExists(headRef);
8622
8729
  let remoteDeleteAttempted = false;
8623
8730
  let remoteNotAttemptedReason = repoArgs.length ? "repo-option" : void 0;
8624
- await execFileP3("gh", ["pr", "merge", number, ...repoArgs, method, "--delete-branch"], { timeout: GC_GH_TIMEOUT_MS }).catch((e) => {
8731
+ await execFileP4("gh", ["pr", "merge", number, ...repoArgs, method, "--delete-branch"], { timeout: GC_GH_TIMEOUT_MS }).catch((e) => {
8625
8732
  const message = String(e.message || "");
8626
8733
  if (/already been merged/i.test(message)) {
8627
8734
  remoteNotAttemptedReason = "pr-already-merged";
@@ -8644,7 +8751,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
8644
8751
  } : await cleanupPrMergeLocalBranch(headRef, {
8645
8752
  beforeWorktrees,
8646
8753
  startingPath,
8647
- execGit: async (args) => (await execFileP3("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
8754
+ execGit: async (args) => (await execFileP4("git", args, { timeout: GIT_TIMEOUT_MS })).stdout
8648
8755
  });
8649
8756
  console.log(JSON.stringify({
8650
8757
  merged: number,
@@ -8769,7 +8876,7 @@ function stageKeepAlive() {
8769
8876
  program2.command("port-range <repo>").description("assign (idempotently) + print the repo's local stage port block via the atomic ORG#config.portCursor allocator (committed-file fallback)").option("--json", "machine-readable output").action(async (repo, o) => {
8770
8877
  const path2 = (0, import_node_path5.join)(process.cwd(), "infra", "port-ranges.json");
8771
8878
  const allocate = async (seed) => {
8772
- const { stdout } = await execFileP3("node", [(0, import_node_path5.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
8879
+ const { stdout } = await execFileP4("node", [(0, import_node_path5.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
8773
8880
  const parsed = JSON.parse(stdout);
8774
8881
  if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
8775
8882
  return parsed.range;
@@ -8875,8 +8982,8 @@ for (const commandName of ["rcand", "release", "hotfix"]) {
8875
8982
  if (commandName === "hotfix") return fail("hotfix: CLI apply is reserved; use the /hotfix skill PR path after explicit master-admin approval");
8876
8983
  try {
8877
8984
  const result = await runTrainApply(commandName, {
8878
- run: async (file, args) => (await execFileP3(file, args, { timeout: file === "gh" ? 3e4 : GIT_TIMEOUT_MS })).stdout,
8879
- runSelf: async (args) => (await execFileP3(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout,
8985
+ run: async (file, args) => (await execFileP4(file, args, { timeout: file === "gh" ? 3e4 : GIT_TIMEOUT_MS })).stdout,
8986
+ runSelf: async (args) => (await execFileP4(process.execPath, [process.argv[1], ...args], { timeout: 3e4 })).stdout,
8880
8987
  trainAuthority: async (repo) => {
8881
8988
  const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
8882
8989
  return verdict.ok ? { ok: true, role: verdict.authority.role, train: verdict.authority.train } : verdict;
@@ -8907,7 +9014,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
8907
9014
  const slug = (repo.includes("/") ? repo.split("/")[1] : repo).toLowerCase();
8908
9015
  const meta = await fetchProjectBySlug(slug, { baseUrl: cfg.sagaApiUrl, token: githubToken });
8909
9016
  const report = await verifyBootstrap(repo, o.class, {
8910
- gh: async (args) => execFileP3("gh", args, { timeout: 2e4 }),
9017
+ client: defaultGitHubClient(),
8911
9018
  projectMeta: meta,
8912
9019
  readLocalFile: (path2) => path2 === "projects.json" && apiProjects != null ? apiProjects : (0, import_node_fs5.existsSync)(path2) ? (0, import_node_fs5.readFileSync)(path2, "utf8") : null,
8913
9020
  // requiredGcpApis is stored as an array by a JSON write, but `project set --var KEY=VALUE` stores a raw
@@ -8920,7 +9027,7 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
8920
9027
  })(),
8921
9028
  listEnabledGcpApis: async (gcpProject) => {
8922
9029
  try {
8923
- const { stdout } = await execFileP3(
9030
+ const { stdout } = await execFileP4(
8924
9031
  "gcloud",
8925
9032
  ["services", "list", "--enabled", "--project", gcpProject, "--format", "value(config.name)"],
8926
9033
  { timeout: 3e4 }
@@ -8954,7 +9061,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
8954
9061
  const manifest = loadBootstrapSeeds((0, import_node_fs5.readFileSync)(manifestPath, "utf8"));
8955
9062
  const baseBranch = o.class === "content" ? "main" : "development";
8956
9063
  const slug = parsedRepo.slug;
8957
- const gh = async (args) => execFileP3("gh", args, { timeout: 2e4 });
9064
+ const gh = async (args) => execFileP4("gh", args, { timeout: 2e4 });
8958
9065
  const readFile2 = (p) => (0, import_node_fs5.existsSync)(p) ? (0, import_node_fs5.readFileSync)(p, "utf8") : null;
8959
9066
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
8960
9067
  const vars = {};
@@ -9071,7 +9178,7 @@ access.command("role [repo]").description("D14 train authority for a repo (serve
9071
9178
  });
9072
9179
  access.command("audit").description("audit collaborator roles + train-branch push allowlists vs the locked state; read-only, emits gh remediation, never applies").option("--json", "machine-readable output").option("--repo <owner/repo>", "audit a single repo instead of the whole org").option("--class <class>", "repo class for --repo (deployable | content)", "deployable").action(async () => {
9073
9180
  const o = { json: rawFlag("--json"), repo: rawValue("--repo", ""), class: rawValue("--class", "deployable") };
9074
- const deps = { gh: async (args) => execFileP3("gh", args, { timeout: 2e4 }) };
9181
+ const deps = { client: defaultGitHubClient() };
9075
9182
  let targets;
9076
9183
  const cfg = await loadConfig();
9077
9184
  const registryProjects = await fetchProjectsList(registryClientDeps(cfg));
@@ -9219,17 +9326,17 @@ async function runDoctor(opts, io = consoleIo) {
9219
9326
  const CLONE_FIX = 'run: git config --global url."https://github.com/".insteadOf "git@github.com:"';
9220
9327
  const [login, pathProbe, releasedVersion, cfg, callerArn, cloneProbe] = await Promise.all([
9221
9328
  githubLogin(),
9222
- execFileP3(isWin ? "where" : "which", ["mmi-cli"]).then(() => true).catch(() => false),
9329
+ execFileP4(isWin ? "where" : "which", ["mmi-cli"]).then(() => true).catch(() => false),
9223
9330
  fetchReleasedVersion(),
9224
9331
  loadConfig(),
9225
9332
  awsCallerArn(),
9226
- execFileP3("git", ["config", "--global", "--get-all", REWRITE_KEY]).then(({ stdout }) => stdout.split("\n").some((l) => l.trim() === "git@github.com:")).catch(() => false)
9333
+ execFileP4("git", ["config", "--global", "--get-all", REWRITE_KEY]).then(({ stdout }) => stdout.split("\n").some((l) => l.trim() === "git@github.com:")).catch(() => false)
9227
9334
  // unset → repair below
9228
9335
  ]);
9229
9336
  let ghInstalled = true;
9230
9337
  if (!login) {
9231
9338
  try {
9232
- await execFileP3("gh", ["--version"]);
9339
+ await execFileP4("gh", ["--version"]);
9233
9340
  } catch {
9234
9341
  ghInstalled = false;
9235
9342
  }
@@ -9256,7 +9363,7 @@ async function runDoctor(opts, io = consoleIo) {
9256
9363
  let cloneOk = cloneProbe;
9257
9364
  if (!cloneOk && !opts.banner && !opts.json) {
9258
9365
  try {
9259
- await execFileP3("git", ["config", "--global", "--add", REWRITE_KEY, "git@github.com:"]);
9366
+ await execFileP4("git", ["config", "--global", "--add", REWRITE_KEY, "git@github.com:"]);
9260
9367
  cloneOk = true;
9261
9368
  io.err(" \u21BB repaired: git insteadOf git@github.com \u2192 https (plugin clone over HTTPS)");
9262
9369
  } catch {