@odla-ai/cli 0.34.1 → 0.35.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.
package/dist/index.cjs CHANGED
@@ -135,7 +135,7 @@ function approvalLines(prompt) {
135
135
  return lines;
136
136
  }
137
137
  function printApproval(out, prompt) {
138
- for (const line of approvalLines(prompt)) out.error(line);
138
+ for (const line2 of approvalLines(prompt)) out.error(line2);
139
139
  }
140
140
  function reminderLines(prompt) {
141
141
  return [
@@ -221,12 +221,12 @@ function approvalHint(pending) {
221
221
  }
222
222
  function approvalReminder(out, pending, periodMs = 3e4) {
223
223
  const timer = setInterval(() => {
224
- for (const line of reminderLines({
224
+ for (const line2 of reminderLines({
225
225
  userCode: pending.userCode,
226
226
  approvalUrl: pending.approvalUrl,
227
227
  minutesLeft: minutesLeft(pending.expiresAt)
228
228
  }))
229
- out.log(line);
229
+ out.log(line2);
230
230
  }, periodMs);
231
231
  timer.unref?.();
232
232
  return () => clearInterval(timer);
@@ -287,9 +287,9 @@ function mergeCredential(current, update) {
287
287
  function ensureGitignore(rootDir, localPaths = []) {
288
288
  const path = (0, import_node_path2.resolve)(rootDir, ".gitignore");
289
289
  const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
290
- const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
290
+ const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
291
291
  const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
292
- const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
292
+ const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
293
293
  if (missing.length === 0) return;
294
294
  const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
295
295
  (0, import_node_fs2.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
@@ -321,7 +321,7 @@ function writeDevVars(path, credentials, env, o11y) {
321
321
  if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
322
322
  }
323
323
  const existing = (0, import_node_fs2.existsSync)(path) ? (0, import_node_fs2.readFileSync)(path, "utf8") : "";
324
- const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
324
+ const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
325
325
  while (retained.at(-1) === "") retained.pop();
326
326
  const prefix = retained.length ? `${retained.join("\n")}
327
327
 
@@ -341,8 +341,8 @@ var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
341
341
  "ODLA_O11Y_VERSION",
342
342
  "ODLA_O11Y_TOKEN"
343
343
  ]);
344
- function isManagedDevVar(line) {
345
- const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
344
+ function isManagedDevVar(line2) {
345
+ const match = line2.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
346
346
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
347
347
  }
348
348
  function writePrivateText(path, text3) {
@@ -2196,7 +2196,7 @@ function chooseIdMode(options, rows) {
2196
2196
  async function appImport(options) {
2197
2197
  const cfg = await loadProjectConfig(options.configPath);
2198
2198
  const out = options.stdout ?? console;
2199
- const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
2199
+ const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
2200
2200
  const { tenant } = resolveTenant(cfg, options.env);
2201
2201
  const text3 = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs7.readFileSync)(0, "utf8")))() : (0, import_node_fs7.readFileSync)(options.file, "utf8");
2202
2202
  const { format, sources } = (0, import_import.parseImport)(text3, options.ns);
@@ -2521,7 +2521,7 @@ async function designUnpack(parsed, deps) {
2521
2521
  );
2522
2522
  return;
2523
2523
  }
2524
- for (const line of describeUnpack(result, outDir)) out.log(line);
2524
+ for (const line2 of describeUnpack(result, outDir)) out.log(line2);
2525
2525
  }
2526
2526
  async function brandCommand(parsed, deps) {
2527
2527
  const subject = parsed.positionals[1];
@@ -4482,7 +4482,7 @@ async function doctor(options) {
4482
4482
  out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
4483
4483
  const contract = resolveSecretContract(cfg);
4484
4484
  out.log(`secrets: ${contract.length ? `${contract.length} declared` : "none declared"}`);
4485
- for (const line of formatSecretContract(contract)) out.log(line);
4485
+ for (const line2 of formatSecretContract(contract)) out.log(line2);
4486
4486
  if (cfg.services.includes("calendar")) {
4487
4487
  const calendar = cfg.envs.map((env) => {
4488
4488
  const resolved = calendarServiceConfig(cfg, env);
@@ -4851,10 +4851,10 @@ async function secretsStatus(options) {
4851
4851
  }
4852
4852
  const body = await res.json();
4853
4853
  const stored = new Set((body.secrets ?? []).map((entry) => String(entry.name)));
4854
- const report4 = buildReport(cfg.app.id, options.env, tenantId, contract, stored);
4855
- if (options.json) out.log(JSON.stringify(report4, null, 2));
4856
- else printReport(report4, out);
4857
- return report4;
4854
+ const report5 = buildReport(cfg.app.id, options.env, tenantId, contract, stored);
4855
+ if (options.json) out.log(JSON.stringify(report5, null, 2));
4856
+ else printReport(report5, out);
4857
+ return report5;
4858
4858
  }
4859
4859
  function buildReport(appId, env, tenant, contract, stored) {
4860
4860
  const declared = new Set(contract.map((secret) => secret.name));
@@ -4871,25 +4871,25 @@ function buildReport(appId, env, tenant, contract, stored) {
4871
4871
  const ok = rows.every((row) => row.state !== "missing" || !row.required);
4872
4872
  return { app: appId, env, tenant, secrets: rows, ok };
4873
4873
  }
4874
- function printReport(report4, out) {
4875
- out.log(`${report4.app} (${report4.tenant})`);
4876
- if (report4.secrets.length === 0) {
4874
+ function printReport(report5, out) {
4875
+ out.log(`${report5.app} (${report5.tenant})`);
4876
+ if (report5.secrets.length === 0) {
4877
4877
  out.log(" no secrets declared and none stored");
4878
4878
  return;
4879
4879
  }
4880
- for (const row of report4.secrets) {
4880
+ for (const row of report5.secrets) {
4881
4881
  const label = row.state === "missing" && !row.required ? "missing (optional)" : row.state;
4882
4882
  const suffix = row.sources.length ? ` \u2014 ${row.sources.join(", ")}` : "";
4883
4883
  out.log(` ${label.padEnd(18)} ${row.name}${suffix}`);
4884
4884
  }
4885
- const missing = report4.secrets.filter((row) => row.state === "missing" && row.required);
4885
+ const missing = report5.secrets.filter((row) => row.state === "missing" && row.required);
4886
4886
  if (missing.length) {
4887
4887
  out.log("");
4888
4888
  for (const row of missing) {
4889
- out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${report4.env} --stdin"`);
4889
+ out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${report5.env} --stdin"`);
4890
4890
  }
4891
4891
  }
4892
- if (report4.secrets.some((row) => row.state === "reserved")) {
4892
+ if (report5.secrets.some((row) => row.state === "reserved")) {
4893
4893
  out.log("");
4894
4894
  out.log('"reserved" slots are never enumerated by the vault; presence cannot be confirmed here.');
4895
4895
  }
@@ -4967,12 +4967,12 @@ function claudeAdapter(skill, canonical2) {
4967
4967
  const lines = match[1].split(/\r?\n/);
4968
4968
  const frontmatter = [];
4969
4969
  let keepIndented = false;
4970
- for (const line of lines) {
4971
- if (line.startsWith("name:") || line.startsWith("description:")) {
4972
- frontmatter.push(line);
4973
- keepIndented = line.startsWith("description:");
4974
- } else if (keepIndented && /^\s+/.test(line)) {
4975
- frontmatter.push(line);
4970
+ for (const line2 of lines) {
4971
+ if (line2.startsWith("name:") || line2.startsWith("description:")) {
4972
+ frontmatter.push(line2);
4973
+ keepIndented = line2.startsWith("description:");
4974
+ } else if (keepIndented && /^\s+/.test(line2)) {
4975
+ frontmatter.push(line2);
4976
4976
  } else {
4977
4977
  keepIndented = false;
4978
4978
  }
@@ -6293,13 +6293,13 @@ async function createConversionRegistry(config) {
6293
6293
  policies.set(policy.conversionId, Object.freeze(policy));
6294
6294
  }
6295
6295
  const outputCounts = /* @__PURE__ */ new Map();
6296
- const get = (id2, kind) => {
6296
+ const get2 = (id2, kind) => {
6297
6297
  const policy = policies.get(id2);
6298
6298
  if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
6299
6299
  return policy;
6300
6300
  };
6301
6301
  const checked = (source, id2, kind) => {
6302
- const policy = get(id2, kind);
6302
+ const policy = get2(id2, kind);
6303
6303
  if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
6304
6304
  return policy;
6305
6305
  };
@@ -7130,7 +7130,7 @@ var PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
7130
7130
  var FORBIDDEN = /^(?:GIT binary patch|Binary files |rename (?:from|to) |copy (?:from|to) |similarity index |old mode |new mode |deleted file mode 160000|new file mode 160000)/m;
7131
7131
  function stripPatchEnvelope(patch2) {
7132
7132
  if (!/^\*\*\* (?:Begin|End) Patch\s*$/m.test(patch2)) return patch2;
7133
- const kept = patch2.split("\n").filter((line) => !/^\*\*\* (?:Begin|End) Patch\s*$/.test(line));
7133
+ const kept = patch2.split("\n").filter((line2) => !/^\*\*\* (?:Begin|End) Patch\s*$/.test(line2));
7134
7134
  const stripped = kept.join("\n");
7135
7135
  return /^diff --git /m.test(stripped) ? stripped : patch2;
7136
7136
  }
@@ -7151,9 +7151,9 @@ function validateCodePatch(rawPatch, maxBytes) {
7151
7151
  const paths = [];
7152
7152
  const lines = patch2.split("\n");
7153
7153
  for (let index = 0; index < lines.length; index += 1) {
7154
- const line = lines[index];
7155
- if (!line.startsWith("diff --git ")) continue;
7156
- const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(line);
7154
+ const line2 = lines[index];
7155
+ if (!line2.startsWith("diff --git ")) continue;
7156
+ const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(line2);
7157
7157
  const path = match?.[1];
7158
7158
  if (!path || !match?.[2] || path !== match[2]) throw new TypeError("patch must use one unquoted relative path per diff");
7159
7159
  validateRelativePath(path);
@@ -7186,9 +7186,9 @@ function resolveCodePath(workspaceDir, path) {
7186
7186
  return target;
7187
7187
  }
7188
7188
  function describePatchFailure(patch2, detail) {
7189
- const hunks = patch2.split("\n").filter((line) => line.startsWith("@@"));
7189
+ const hunks = patch2.split("\n").filter((line2) => line2.startsWith("@@"));
7190
7190
  const bodies = patch2.split(/^@@.*$/m).slice(1);
7191
- const contextless = bodies.some((body) => !body.split("\n").some((line) => line.startsWith(" ") && line.trim().length > 0));
7191
+ const contextless = bodies.some((body) => !body.split("\n").some((line2) => line2.startsWith(" ") && line2.trim().length > 0));
7192
7192
  const hint = hunks.length > 0 && contextless ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
7193
7193
  return `patch did not apply: ${detail}${hint}`;
7194
7194
  }
@@ -9749,11 +9749,237 @@ function record6(value2) {
9749
9749
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
9750
9750
  }
9751
9751
 
9752
+ // src/code-grant-command.ts
9753
+ var ACTIONS = ["request", "list", "approve", "revoke"];
9754
+ var isAction = (value2) => ACTIONS.includes(value2 ?? "");
9755
+ async function fail2(response2, what) {
9756
+ const body = redactSecrets((await response2.text()).slice(0, 1e3));
9757
+ throw new Error(`${what} failed (${response2.status}): ${body}`);
9758
+ }
9759
+ function line(grant) {
9760
+ const who = grant.status === "granted" ? `granted by ${grant.approvedBy ?? "?"}` : `requested by ${grant.requestedBy}`;
9761
+ return `${grant.grantId} ${grant.appEnv} ${grant.owner}/${grant.name} ${grant.status} ${who}`;
9762
+ }
9763
+ function report(grants, out, json) {
9764
+ if (json) return out.log(JSON.stringify({ grants }, null, 2));
9765
+ if (!grants.length) return out.log("no repository grants for this app");
9766
+ out.log("GRANT ENV REPOSITORY STATUS DECIDED");
9767
+ for (const grant of grants) out.log(line(grant));
9768
+ const pending = grants.filter((grant) => grant.status === "requested");
9769
+ if (pending.length) {
9770
+ out.log(`
9771
+ ${pending.length} awaiting approval. A human owner runs:`);
9772
+ for (const grant of pending) out.log(` odla-ai code grant approve ${grant.grantId} --env ${grant.appEnv}`);
9773
+ }
9774
+ }
9775
+ function grantsUrl(cfg, suffix = "") {
9776
+ return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/code/repository-grants${suffix}`;
9777
+ }
9778
+ async function codeGrantCommand(parsed, deps = {}) {
9779
+ const action2 = parsed.positionals[2];
9780
+ if (!isAction(action2)) {
9781
+ throw new Error(
9782
+ `unknown code grant action "${action2 ?? ""}". Try "odla-ai code grant list --env dev".`
9783
+ );
9784
+ }
9785
+ const decides = action2 === "approve" || action2 === "revoke";
9786
+ assertArgs(parsed, ["config", "env", "json", "token", "email", "open"], decides ? 4 : 3);
9787
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
9788
+ const doFetch = deps.fetch ?? fetch;
9789
+ const out = deps.stdout ?? console;
9790
+ const env = stringOpt(parsed.options.env) ?? "dev";
9791
+ const json = parsed.options.json === true;
9792
+ const token = await getDeveloperToken(cfg, {
9793
+ configPath: cfg.configPath,
9794
+ token: stringOpt(parsed.options.token),
9795
+ email: stringOpt(parsed.options.email),
9796
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
9797
+ openApprovalUrl: deps.openUrl
9798
+ // Every verb here administers grants, which the registry gates on
9799
+ // app.manage — a token without it is refused as "not your app", which reads
9800
+ // like the wrong app rather than the wrong authority.
9801
+ //
9802
+ // Only `request` additionally asks for code.session. Approving is a human
9803
+ // act, and a token minted to approve has no business also carrying the
9804
+ // authority the approval is about.
9805
+ }, doFetch, out, {
9806
+ optionalProjectCapabilities: action2 === "request" ? ["app.manage", "code.session"] : ["app.manage"]
9807
+ });
9808
+ const auth = { authorization: `Bearer ${token}` };
9809
+ if (action2 === "list") {
9810
+ const response3 = await doFetch(`${grantsUrl(cfg)}?env=${encodeURIComponent(env)}`, { headers: auth });
9811
+ if (!response3.ok) await fail2(response3, "repository grant list");
9812
+ const body2 = await response3.json();
9813
+ return report(body2.grants, out, json);
9814
+ }
9815
+ if (action2 === "request") {
9816
+ const response3 = await doFetch(grantsUrl(cfg), {
9817
+ method: "POST",
9818
+ headers: { ...auth, "content-type": "application/json" },
9819
+ body: JSON.stringify({ env })
9820
+ });
9821
+ if (!response3.ok) await fail2(response3, "repository grant request");
9822
+ const { grant } = await response3.json();
9823
+ if (json) return out.log(JSON.stringify({ grant }, null, 2));
9824
+ out.log(`requested ${grant.owner}/${grant.name} (${grant.appEnv}) \u2014 ${grant.grantId}`);
9825
+ return out.log(grant.status === "granted" ? "already granted; nothing to approve" : `a human owner of ${cfg.app.id} approves it with:
9826
+ odla-ai code grant approve ${grant.grantId} --env ${env}`);
9827
+ }
9828
+ const grantId = parsed.positionals[3];
9829
+ if (!grantId) throw new Error(`code grant ${action2} requires the grant id from "odla-ai code grant list"`);
9830
+ const url = `${grantsUrl(cfg, `/${encodeURIComponent(grantId)}`)}`;
9831
+ const response2 = action2 === "approve" ? await doFetch(url, {
9832
+ method: "POST",
9833
+ headers: { ...auth, "content-type": "application/json" },
9834
+ body: JSON.stringify({ env })
9835
+ }) : await doFetch(`${url}?env=${encodeURIComponent(env)}`, { method: "DELETE", headers: auth });
9836
+ if (!response2.ok) await fail2(response2, `repository grant ${action2}`);
9837
+ const body = await response2.json();
9838
+ if (json) return out.log(JSON.stringify(body, null, 2));
9839
+ out.log(action2 === "approve" && body.grant ? `granted ${body.grant.owner}/${body.grant.name} (${body.grant.appEnv}) to ${cfg.app.id}` : `revoked ${grantId}`);
9840
+ }
9841
+
9842
+ // src/code-repository-command.ts
9843
+ var ACTIONS2 = ["show", "list", "bind"];
9844
+ var isAction2 = (value2) => ACTIONS2.includes(value2 ?? "");
9845
+ async function get(doFetch, url, token, what) {
9846
+ const response2 = await doFetch(url, { headers: { authorization: `Bearer ${token}` } });
9847
+ if (!response2.ok) {
9848
+ const body = redactSecrets((await response2.text()).slice(0, 1e3));
9849
+ if (response2.status === 403 && body.includes("human_session_required")) {
9850
+ throw new Error(
9851
+ `selecting a repository needs a signed-in human session, and every CLI handshake token is a delegated agent credential by design \u2014 so this cannot be done with one, however it was approved. Connect the repository in Studio (Apps \u2192 this app \u2192 Code), then run "odla-ai code grant request".`
9852
+ );
9853
+ }
9854
+ throw new Error(`${what} failed (${response2.status}): ${body}`);
9855
+ }
9856
+ return await response2.json();
9857
+ }
9858
+ async function catalogue(doFetch, platformUrl, token) {
9859
+ const account = await get(
9860
+ doFetch,
9861
+ `${platformUrl}/registry/code/github/account`,
9862
+ token,
9863
+ "GitHub account read"
9864
+ );
9865
+ const active = account.installations.filter((install2) => install2.status === "active");
9866
+ const found = [];
9867
+ for (const install2 of active) {
9868
+ const { repositories } = await get(
9869
+ doFetch,
9870
+ `${platformUrl}/registry/code/github/installations/${install2.installationId}/repositories`,
9871
+ token,
9872
+ `repository catalog for ${install2.accountLogin}`
9873
+ );
9874
+ for (const repository of repositories) {
9875
+ found.push({ ...repository, installationId: install2.installationId });
9876
+ }
9877
+ }
9878
+ if (!active.length) {
9879
+ throw new Error(
9880
+ "no active GitHub App installation. Install odla's GitHub App on the account that owns the repository first, from Studio \u2192 Code."
9881
+ );
9882
+ }
9883
+ return found;
9884
+ }
9885
+ function selectRepository(found, full) {
9886
+ const wanted = full.toLowerCase();
9887
+ const matches = found.filter((repository) => repository.fullName.toLowerCase() === wanted);
9888
+ if (matches.length === 1) return matches[0];
9889
+ if (!matches.length) {
9890
+ const near = found.filter((repository) => repository.name.toLowerCase() === wanted.split("/").pop());
9891
+ throw new Error(near.length ? `no installation exposes ${full}. Did you mean ${near.map((repository) => repository.fullName).join(", ")}?` : `no installation exposes ${full}. Run "odla-ai code repository list" to see what is reachable.`);
9892
+ }
9893
+ throw new Error(
9894
+ `${full} is reachable through ${matches.length} installations (${matches.map((match) => match.installationId).join(", ")}); revoke the one you do not mean.`
9895
+ );
9896
+ }
9897
+ function repositoryUrl(cfg, suffix = "") {
9898
+ return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/code/repository${suffix}`;
9899
+ }
9900
+ async function codeRepositoryCommand(parsed, deps = {}) {
9901
+ const action2 = parsed.positionals[2];
9902
+ if (!isAction2(action2)) {
9903
+ throw new Error(
9904
+ `unknown code repository action "${action2 ?? ""}". Try "odla-ai code repository show --env dev".`
9905
+ );
9906
+ }
9907
+ assertArgs(parsed, ["config", "env", "repo", "json", "token", "email", "open"], 3);
9908
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
9909
+ const doFetch = deps.fetch ?? fetch;
9910
+ const out = deps.stdout ?? console;
9911
+ const env = stringOpt(parsed.options.env) ?? "dev";
9912
+ const json = parsed.options.json === true;
9913
+ const token = await getDeveloperToken(cfg, {
9914
+ configPath: cfg.configPath,
9915
+ token: stringOpt(parsed.options.token),
9916
+ email: stringOpt(parsed.options.email),
9917
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
9918
+ openApprovalUrl: deps.openUrl
9919
+ }, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
9920
+ if (action2 === "show") {
9921
+ const body2 = await get(
9922
+ doFetch,
9923
+ `${repositoryUrl(cfg)}?env=${encodeURIComponent(env)}`,
9924
+ token,
9925
+ "repository read"
9926
+ );
9927
+ if (json) return out.log(JSON.stringify(body2, null, 2));
9928
+ const bound = body2.environmentRepository;
9929
+ return out.log(bound ? `${cfg.app.id} (${env}) \u2192 ${bound.owner}/${bound.name}` : `${cfg.app.id} (${env}) has no connected repository. Connect one with "odla-ai code repository bind --repo owner/name --env ${env}".`);
9930
+ }
9931
+ const found = await catalogue(doFetch, cfg.platformUrl, token);
9932
+ if (action2 === "list") {
9933
+ if (json) return out.log(JSON.stringify({ repositories: found }, null, 2));
9934
+ if (!found.length) return out.log("no repositories reachable through your installations");
9935
+ out.log("REPOSITORY INSTALL DEFAULT READY");
9936
+ for (const repository of found) {
9937
+ out.log(`${repository.fullName} ${repository.installationId} ${repository.defaultBranch ?? "\u2014"} ${repository.ready ? "yes" : "empty"}`);
9938
+ }
9939
+ return;
9940
+ }
9941
+ const repo = stringOpt(parsed.options.repo);
9942
+ if (!repo?.includes("/")) throw new Error("code repository bind requires --repo owner/name");
9943
+ const selected = selectRepository(found, repo);
9944
+ if (!selected.ready) {
9945
+ throw new Error(`${selected.fullName} has no commits yet; GitHub cannot serve a default branch for an empty repository.`);
9946
+ }
9947
+ const response2 = await doFetch(repositoryUrl(cfg, "/bind"), {
9948
+ method: "POST",
9949
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
9950
+ body: JSON.stringify({ env, installationId: selected.installationId, repositoryId: selected.repositoryId })
9951
+ });
9952
+ if (!response2.ok) {
9953
+ throw new Error(`repository bind failed (${response2.status}): ${redactSecrets((await response2.text()).slice(0, 1e3))}`);
9954
+ }
9955
+ const body = await response2.json();
9956
+ if (json) return out.log(JSON.stringify(body, null, 2));
9957
+ out.log(`connected ${cfg.app.id} (${env}) to ${selected.fullName} @ ${selected.defaultBranch}`);
9958
+ out.log(`Next, so it may be worked on unattended:
9959
+ odla-ai code grant request --env ${env}`);
9960
+ }
9961
+
9752
9962
  // src/code-command.ts
9753
9963
  async function codeCommand(parsed, dependencies) {
9754
9964
  const sub = parsed.positionals[1];
9965
+ if (sub === "grant") {
9966
+ return await codeGrantCommand(parsed, {
9967
+ fetch: dependencies.fetch,
9968
+ stdout: dependencies.stdout,
9969
+ openUrl: dependencies.openUrl
9970
+ });
9971
+ }
9972
+ if (sub === "repository") {
9973
+ return await codeRepositoryCommand(parsed, {
9974
+ fetch: dependencies.fetch,
9975
+ stdout: dependencies.stdout,
9976
+ openUrl: dependencies.openUrl
9977
+ });
9978
+ }
9755
9979
  if (sub !== "connect") {
9756
- throw new Error(`unknown code subcommand "${sub ?? ""}". Try "odla-ai code connect --env dev".`);
9980
+ throw new Error(
9981
+ `unknown code subcommand "${sub ?? ""}". Try "odla-ai code connect --env dev" or "odla-ai code grant list --env dev".`
9982
+ );
9757
9983
  }
9758
9984
  assertArgs(parsed, [
9759
9985
  "config",
@@ -10195,6 +10421,9 @@ Commands:
10195
10421
  \u2014 no secret is ever copied between people.
10196
10422
  capabilities Show what the CLI automates vs agent edits and human checkpoints.
10197
10423
  code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
10424
+ "code repository show" reports the repository an app works on (Studio or a
10425
+ human session connects one); "code grant request|list|approve|revoke" then
10426
+ governs unattended access: an agent may request, only a human may approve.
10198
10427
  admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
10199
10428
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
10200
10429
  pm Project management (via @odla-ai/pm): Products contain Projects;
@@ -10295,6 +10524,17 @@ Safety:
10295
10524
  approval and credential hashes live in odla-ai/db. The host
10296
10525
  credential is never written under .odla/; it exists only in the foreground
10297
10526
  "code connect" process and is rotated by the next approved connection.
10527
+ "code repository bind" takes owner/name and resolves the two GitHub integers the
10528
+ bind route wants across every installation you have, refusing an ambiguous match
10529
+ rather than choosing one \u2014 the same repository name under two organizations is
10530
+ ordinary, and picking the first would connect an app to source nobody chose. It
10531
+ needs a signed-in human session; a CLI handshake token is a delegated agent
10532
+ credential by design, so an agent cannot choose the source it will be given.
10533
+ Unattended access is per repository and revocable on its own, but the connection
10534
+ IS the authorization: a repository is recorded for one exact app and environment
10535
+ only by a signed-in human choosing it. "code grant list" shows what is authorized;
10536
+ "revoke" stops unattended access without disconnecting the repository, and
10537
+ re-binding never resurrects it. Connecting sandbox never authorizes live.
10298
10538
  Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
10299
10539
  OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
10300
10540
  GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
@@ -10661,7 +10901,7 @@ async function discussWatch(ctx, topicId, parsed) {
10661
10901
  throw new WatchRemoteError(cursor, error);
10662
10902
  }
10663
10903
  if (deadline !== void 0 && now() >= deadline) {
10664
- const result = report(ctx, parsed, { found: false, cursor: cursor ?? "" });
10904
+ const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
10665
10905
  throw new WatchTimeoutError(result.cursor);
10666
10906
  }
10667
10907
  const base = Math.min(intervalMs, 1e3);
@@ -10702,7 +10942,7 @@ async function discussWatch(ctx, topicId, parsed) {
10702
10942
  });
10703
10943
  const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
10704
10944
  const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
10705
- return report(ctx, parsed, {
10945
+ return report2(ctx, parsed, {
10706
10946
  found: true,
10707
10947
  cursor,
10708
10948
  events: matching,
@@ -10729,13 +10969,13 @@ async function discussWatch(ctx, topicId, parsed) {
10729
10969
  }
10730
10970
  if (page2.hasMore) continue;
10731
10971
  if (deadline !== void 0 && now() >= deadline) {
10732
- return report(ctx, parsed, { found: false, cursor });
10972
+ return report2(ctx, parsed, { found: false, cursor });
10733
10973
  }
10734
10974
  const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
10735
10975
  await sleep(wait2);
10736
10976
  }
10737
10977
  }
10738
- function report(ctx, parsed, result) {
10978
+ function report2(ctx, parsed, result) {
10739
10979
  if (ctx.json) {
10740
10980
  ctx.out.log(JSON.stringify(result, null, 2));
10741
10981
  } else if (parsed.options.jsonl !== true && result.found) {
@@ -11332,7 +11572,7 @@ function eventLabel(event) {
11332
11572
  const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
11333
11573
  return body || event.payload.entityId;
11334
11574
  }
11335
- function report2(ctx, parsed, result) {
11575
+ function report3(ctx, parsed, result) {
11336
11576
  if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
11337
11577
  else if (parsed.options.jsonl !== true && result.found) {
11338
11578
  for (const event of result.events ?? []) {
@@ -11392,7 +11632,7 @@ async function pmWatch(ctx, parsed) {
11392
11632
  });
11393
11633
  if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
11394
11634
  if (deadline !== void 0 && now() >= deadline) {
11395
- return report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
11635
+ return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
11396
11636
  }
11397
11637
  const backoff = Math.min(
11398
11638
  MAX_BACKOFF_MS2,
@@ -11433,7 +11673,7 @@ async function pmWatch(ctx, parsed) {
11433
11673
  cursor,
11434
11674
  serverTime: current.serverTime
11435
11675
  });
11436
- return report2(ctx, parsed, { found: true, cursor, events: matching });
11676
+ return report3(ctx, parsed, { found: true, cursor, events: matching });
11437
11677
  }
11438
11678
  if (current.events.length > 0) {
11439
11679
  jsonl2(ctx, parsed, {
@@ -11452,7 +11692,7 @@ async function pmWatch(ctx, parsed) {
11452
11692
  }
11453
11693
  if (current.hasMore) continue;
11454
11694
  if (deadline !== void 0 && now() >= deadline) {
11455
- return report2(ctx, parsed, { found: false, cursor });
11695
+ return report3(ctx, parsed, { found: false, cursor });
11456
11696
  }
11457
11697
  await sleep(
11458
11698
  deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
@@ -11989,8 +12229,8 @@ function printO11yStatus(status, out) {
11989
12229
  out.log(
11990
12230
  `cloudflare ${status.provider.httpStatus} ${String(status.provider.body.status ?? status.provider.body.error ?? "unavailable")} ${numeric3(providerMetrics.requests)} invocations ${numeric3(providerMetrics.errors)} runtime errors ${optionalBytes(workerMemory.headroomBytes)} isolate memory headroom`
11991
12231
  );
11992
- for (const line of providerCapacityLines(status.providerCapacity)) {
11993
- out.log(line);
12232
+ for (const line2 of providerCapacityLines(status.providerCapacity)) {
12233
+ out.log(line2);
11994
12234
  }
11995
12235
  const coverage = record9(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
11996
12236
  const coverageCounts = record9(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
@@ -13033,7 +13273,11 @@ var COMMAND_SURFACE = {
13033
13273
  bug: { create: {}, list: {}, report: {} },
13034
13274
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
13035
13275
  capabilities: {},
13036
- code: { connect: {} },
13276
+ code: {
13277
+ connect: {},
13278
+ grant: { request: {}, list: {}, approve: {}, revoke: {} },
13279
+ repository: { show: {}, list: {}, bind: {} }
13280
+ },
13037
13281
  config: { diff: {}, plan: {}, apply: {} },
13038
13282
  context: { show: {}, list: {}, save: {}, remove: {} },
13039
13283
  credentials: { list: {}, revoke: {} },
@@ -13348,8 +13592,8 @@ function parseRunbook(text3, slug) {
13348
13592
  const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text3);
13349
13593
  if (fm) {
13350
13594
  rest = text3.slice(fm[0].length);
13351
- for (const line of fm[1].split(/\r?\n/)) {
13352
- const pair = /^(\w+)\s*:\s*(.+)$/.exec(line.trim());
13595
+ for (const line2 of fm[1].split(/\r?\n/)) {
13596
+ const pair = /^(\w+)\s*:\s*(.+)$/.exec(line2.trim());
13353
13597
  if (!pair) continue;
13354
13598
  const value2 = pair[2].trim().replace(/^["']|["']$/g, "");
13355
13599
  if (pair[1] === "summary") meta.summary = value2;
@@ -13489,18 +13733,18 @@ function areaOf(path) {
13489
13733
  return parts[src + 2] !== void 0 ? parts[src + 1] ?? null : null;
13490
13734
  }
13491
13735
  function scanHunk(lines, into) {
13492
- for (const [i, line] of lines.entries()) {
13493
- const decl = DECL.exec(line);
13736
+ for (const [i, line2] of lines.entries()) {
13737
+ const decl = DECL.exec(line2);
13494
13738
  if (decl?.[1]) {
13495
13739
  into.add(decl[1]);
13496
13740
  continue;
13497
13741
  }
13498
- const named = NAMED.exec(line);
13742
+ const named = NAMED.exec(line2);
13499
13743
  if (named?.[1]) {
13500
13744
  for (const name of namedExports(named[1])) into.add(name);
13501
13745
  continue;
13502
13746
  }
13503
- if (!JSDOC.test(line)) continue;
13747
+ if (!JSDOC.test(line2)) continue;
13504
13748
  for (let j = i + 1; j < lines.length && j < i + 40; j++) {
13505
13749
  const found = ANY_DECL.exec(lines[j]);
13506
13750
  if (found?.[1]) {
@@ -13518,8 +13762,8 @@ function parseDiff(diff) {
13518
13762
  if (current && hunk.length) scanHunk(hunk, current.exports);
13519
13763
  hunk = [];
13520
13764
  };
13521
- for (const line of diff.split("\n")) {
13522
- const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(line);
13765
+ for (const line2 of diff.split("\n")) {
13766
+ const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(line2);
13523
13767
  if (header) {
13524
13768
  flush();
13525
13769
  const path = header[2] ?? header[1];
@@ -13527,11 +13771,11 @@ function parseDiff(diff) {
13527
13771
  files.set(path, current);
13528
13772
  continue;
13529
13773
  }
13530
- if (line.startsWith("@@")) {
13774
+ if (line2.startsWith("@@")) {
13531
13775
  flush();
13532
13776
  continue;
13533
13777
  }
13534
- if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(line);
13778
+ if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(line2);
13535
13779
  }
13536
13780
  flush();
13537
13781
  return [...files.values()];
@@ -13594,7 +13838,7 @@ function untrackedDiff(runGit, read3) {
13594
13838
  return "";
13595
13839
  }
13596
13840
  let out = "";
13597
- for (const path of listed.split("\n").map((line) => line.trim()).filter(Boolean)) {
13841
+ for (const path of listed.split("\n").map((line2) => line2.trim()).filter(Boolean)) {
13598
13842
  out += `diff --git a/${path} b/${path}
13599
13843
  --- /dev/null
13600
13844
  +++ b/${path}
@@ -13607,7 +13851,7 @@ function untrackedDiff(runGit, read3) {
13607
13851
  continue;
13608
13852
  }
13609
13853
  out += `@@ -0,0 +1,${body.split("\n").length} @@
13610
- ${body.split("\n").map((line) => `+${line}`).join("\n")}
13854
+ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
13611
13855
  `;
13612
13856
  }
13613
13857
  return out;
@@ -13655,7 +13899,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
13655
13899
  return out;
13656
13900
  }
13657
13901
  var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
13658
- function report3(ctx, impacts) {
13902
+ function report4(ctx, impacts) {
13659
13903
  const covered = impacts.filter((i) => i.runbooks.length);
13660
13904
  ctx.out.log(
13661
13905
  `${impacts.length} changed surface${impacts.length === 1 ? "" : "s"}; ${covered.length} covered by a runbook. Reread each one and fix any step this change made wrong.`
@@ -13693,7 +13937,7 @@ async function runbookImpact(ctx, options, deps = {}) {
13693
13937
  }
13694
13938
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
13695
13939
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
13696
- report3(ctx, impacts);
13940
+ report4(ctx, impacts);
13697
13941
  }
13698
13942
 
13699
13943
  // src/runbook-lint.ts
@@ -13769,7 +14013,7 @@ async function runbookSearch(ctx, query, all, limit) {
13769
14013
  for (const [i, hit] of result.hits.entries()) {
13770
14014
  if (i) ctx.out.log("");
13771
14015
  ctx.out.log(`${hit.slug}${hit.heading ? ` \xA7 ${hit.heading}` : ""} (v${hit.version})`);
13772
- for (const line of hit.excerpt.split("\n")) ctx.out.log(` ${line}`);
14016
+ for (const line2 of hit.excerpt.split("\n")) ctx.out.log(` ${line2}`);
13773
14017
  ctx.out.log(` source: ${hit.slug}${hit.anchor ? `#${hit.anchor}` : ""} \xB7 ${hit.command}`);
13774
14018
  }
13775
14019
  }
@@ -14144,31 +14388,31 @@ function printHostedJob(out, job, platform, appId) {
14144
14388
  url.searchParams.set("job", job.jobId);
14145
14389
  out.log(` Studio: ${url.toString()}`);
14146
14390
  }
14147
- function printHostedReport(out, report4) {
14148
- out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
14149
- out.log(` coverage: ${report4.coverageStatus} cells=${report4.metrics.coverageCells} shallow=${report4.metrics.shallowCells} blocked=${report4.metrics.blockedCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
14150
- out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
14151
- out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
14152
- out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
14153
- for (const finding of report4.findings) {
14391
+ function printHostedReport(out, report5) {
14392
+ out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
14393
+ out.log(` coverage: ${report5.coverageStatus} cells=${report5.metrics.coverageCells} shallow=${report5.metrics.shallowCells} blocked=${report5.metrics.blockedCells} unscheduled=${report5.metrics.unscheduledCells} budget_exhausted=${report5.metrics.budgetExhaustedCells}`);
14394
+ out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
14395
+ out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
14396
+ out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
14397
+ for (const finding of report5.findings) {
14154
14398
  const location = finding.locations[0];
14155
14399
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
14156
14400
  }
14157
- for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
14401
+ for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
14158
14402
  }
14159
- function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
14403
+ function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
14160
14404
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
14161
14405
  const candidateValue = parsed.options["fail-on-candidates"];
14162
14406
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
14163
14407
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
14164
- const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
14165
- const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
14166
- const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
14408
+ const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
14409
+ const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
14410
+ const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
14167
14411
  if (confirmed.length || leads.length || incomplete) {
14168
- throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
14412
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
14169
14413
  }
14170
14414
  if (emitSuccess) {
14171
- out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
14415
+ out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
14172
14416
  }
14173
14417
  }
14174
14418
  function printHostedSecurityPlanRoute(out, label, route3) {
@@ -14251,17 +14495,17 @@ async function runHostedSecurity(options) {
14251
14495
  allowNetwork: false
14252
14496
  }
14253
14497
  });
14254
- const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
14255
- await (0, import_node3.writeSecurityArtifacts)(output, report4);
14256
- const reportDigest = await (0, import_security.securityFingerprint)(report4);
14498
+ const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
14499
+ await (0, import_node3.writeSecurityArtifacts)(output, report5);
14500
+ const reportDigest = await (0, import_security.securityFingerprint)(report5);
14257
14501
  await hosted.complete({
14258
14502
  reportDigest,
14259
- coverageStatus: report4.coverageStatus,
14260
- confirmed: report4.metrics.confirmed,
14261
- candidates: report4.metrics.candidates
14503
+ coverageStatus: report5.coverageStatus,
14504
+ confirmed: report5.metrics.confirmed,
14505
+ candidates: report5.metrics.candidates
14262
14506
  }, { signal: options.signal });
14263
- printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
14264
- return Object.freeze({ report: report4, run: hosted.run, output });
14507
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
14508
+ return Object.freeze({ report: report5, run: hosted.run, output });
14265
14509
  }
14266
14510
  function selectEnv(requested, declared, configPath, rootDir) {
14267
14511
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -14287,14 +14531,14 @@ function profileFor(name, maxHuntTasks) {
14287
14531
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
14288
14532
  return { ...profile, maxHuntTasks };
14289
14533
  }
14290
- function printSummary(out, appId, env, run, report4, output) {
14291
- const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
14534
+ function printSummary(out, appId, env, run, report5, output) {
14535
+ const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
14292
14536
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
14293
14537
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
14294
14538
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
14295
- out.log(` coverage: ${report4.coverageStatus} ${complete}/${report4.coverage.length} blocked=${report4.metrics.blockedCells} shallow=${report4.metrics.shallowCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
14296
- if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
14297
- out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
14539
+ out.log(` coverage: ${report5.coverageStatus} ${complete}/${report5.coverage.length} blocked=${report5.metrics.blockedCells} shallow=${report5.metrics.shallowCells} unscheduled=${report5.metrics.unscheduledCells} budget_exhausted=${report5.metrics.budgetExhaustedCells}`);
14540
+ if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
14541
+ out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
14298
14542
  out.log(` report: ${(0, import_node_path19.resolve)(output, "REPORT.md")}`);
14299
14543
  }
14300
14544
  function formatBudget(usage) {
@@ -14541,13 +14785,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
14541
14785
  }
14542
14786
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
14543
14787
  }
14544
- const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
14788
+ const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
14545
14789
  if (parsed.options.json === true) {
14546
- context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
14790
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
14547
14791
  } else {
14548
- printHostedReport(context.stdout, report4);
14792
+ printHostedReport(context.stdout, report5);
14549
14793
  }
14550
- enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
14794
+ enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
14551
14795
  }
14552
14796
  async function runLocalSecurityCommand(parsed, dependencies) {
14553
14797
  if (parsed.options.source === true) {
@@ -14615,13 +14859,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
14615
14859
  });
14616
14860
  enforceLocalGate(result.report, parsed);
14617
14861
  }
14618
- function enforceLocalGate(report4, parsed) {
14862
+ function enforceLocalGate(report5, parsed) {
14619
14863
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
14620
14864
  const candidateValue = parsed.options["fail-on-candidates"];
14621
14865
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
14622
- const confirmed = (0, import_security2.findingsAtOrAbove)(report4, failOn);
14623
- const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
14624
- const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
14866
+ const confirmed = (0, import_security2.findingsAtOrAbove)(report5, failOn);
14867
+ const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
14868
+ const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
14625
14869
  if (confirmed.length || leads.length || incomplete) {
14626
14870
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
14627
14871
  }
@@ -14660,9 +14904,9 @@ async function securityCommand(parsed, dependencies) {
14660
14904
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
14661
14905
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
14662
14906
  const context = await hostedSecurityContext(parsed, dependencies);
14663
- const report4 = await getHostedSecurityReport({ ...context, jobId });
14664
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
14665
- else printHostedReport(context.stdout, report4);
14907
+ const report5 = await getHostedSecurityReport({ ...context, jobId });
14908
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
14909
+ else printHostedReport(context.stdout, report5);
14666
14910
  return;
14667
14911
  }
14668
14912
  if (sub !== "run") {