@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.
@@ -48,7 +48,7 @@ function approvalLines(prompt) {
48
48
  return lines;
49
49
  }
50
50
  function printApproval(out, prompt) {
51
- for (const line of approvalLines(prompt)) out.error(line);
51
+ for (const line2 of approvalLines(prompt)) out.error(line2);
52
52
  }
53
53
  function reminderLines(prompt) {
54
54
  return [
@@ -134,12 +134,12 @@ function approvalHint(pending) {
134
134
  }
135
135
  function approvalReminder(out, pending, periodMs = 3e4) {
136
136
  const timer = setInterval(() => {
137
- for (const line of reminderLines({
137
+ for (const line2 of reminderLines({
138
138
  userCode: pending.userCode,
139
139
  approvalUrl: pending.approvalUrl,
140
140
  minutesLeft: minutesLeft(pending.expiresAt)
141
141
  }))
142
- out.log(line);
142
+ out.log(line2);
143
143
  }, periodMs);
144
144
  timer.unref?.();
145
145
  return () => clearInterval(timer);
@@ -200,9 +200,9 @@ function mergeCredential(current, update) {
200
200
  function ensureGitignore(rootDir, localPaths = []) {
201
201
  const path = resolve(rootDir, ".gitignore");
202
202
  const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
203
- const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line) => !!line);
203
+ const configured = localPaths.map((localPath) => gitignoreEntry(rootDir, localPath)).filter((line2) => !!line2);
204
204
  const wanted = [.../* @__PURE__ */ new Set([...GITIGNORE_LINES, ...configured])];
205
- const missing = wanted.filter((line) => !existing.split(/\r?\n/).includes(line));
205
+ const missing = wanted.filter((line2) => !existing.split(/\r?\n/).includes(line2));
206
206
  if (missing.length === 0) return;
207
207
  const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
208
208
  writeFileSync(path, `${existing}${prefix}${missing.join("\n")}
@@ -234,7 +234,7 @@ function writeDevVars(path, credentials, env, o11y) {
234
234
  if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
235
235
  }
236
236
  const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
237
- const retained = existing.split(/\r?\n/).filter((line) => !isManagedDevVar(line));
237
+ const retained = existing.split(/\r?\n/).filter((line2) => !isManagedDevVar(line2));
238
238
  while (retained.at(-1) === "") retained.pop();
239
239
  const prefix = retained.length ? `${retained.join("\n")}
240
240
 
@@ -254,8 +254,8 @@ var MANAGED_DEV_VARS = /* @__PURE__ */ new Set([
254
254
  "ODLA_O11Y_VERSION",
255
255
  "ODLA_O11Y_TOKEN"
256
256
  ]);
257
- function isManagedDevVar(line) {
258
- const match = line.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
257
+ function isManagedDevVar(line2) {
258
+ const match = line2.match(/^\s*(?:export\s+)?([A-Z][A-Z0-9_]*)\s*=/);
259
259
  return !!match?.[1] && MANAGED_DEV_VARS.has(match[1]);
260
260
  }
261
261
  function writePrivateText(path, text3) {
@@ -2113,7 +2113,7 @@ function chooseIdMode(options, rows) {
2113
2113
  async function appImport(options) {
2114
2114
  const cfg = await loadProjectConfig(options.configPath);
2115
2115
  const out = options.stdout ?? console;
2116
- const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
2116
+ const say = options.json ? (line2) => out.error(line2) : (line2) => out.log(line2);
2117
2117
  const { tenant } = resolveTenant(cfg, options.env);
2118
2118
  const text3 = options.file === "-" ? (options.readStdin ?? (() => readFileSync4(0, "utf8")))() : readFileSync4(options.file, "utf8");
2119
2119
  const { format, sources } = parseImport(text3, options.ns);
@@ -2443,7 +2443,7 @@ async function designUnpack(parsed, deps) {
2443
2443
  );
2444
2444
  return;
2445
2445
  }
2446
- for (const line of describeUnpack(result, outDir)) out.log(line);
2446
+ for (const line2 of describeUnpack(result, outDir)) out.log(line2);
2447
2447
  }
2448
2448
  async function brandCommand(parsed, deps) {
2449
2449
  const subject = parsed.positionals[1];
@@ -4352,7 +4352,7 @@ async function doctor(options) {
4352
4352
  out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
4353
4353
  const contract = resolveSecretContract(cfg);
4354
4354
  out.log(`secrets: ${contract.length ? `${contract.length} declared` : "none declared"}`);
4355
- for (const line of formatSecretContract(contract)) out.log(line);
4355
+ for (const line2 of formatSecretContract(contract)) out.log(line2);
4356
4356
  if (cfg.services.includes("calendar")) {
4357
4357
  const calendar = cfg.envs.map((env) => {
4358
4358
  const resolved = calendarServiceConfig(cfg, env);
@@ -4721,10 +4721,10 @@ async function secretsStatus(options) {
4721
4721
  }
4722
4722
  const body = await res.json();
4723
4723
  const stored = new Set((body.secrets ?? []).map((entry) => String(entry.name)));
4724
- const report4 = buildReport(cfg.app.id, options.env, tenantId, contract, stored);
4725
- if (options.json) out.log(JSON.stringify(report4, null, 2));
4726
- else printReport(report4, out);
4727
- return report4;
4724
+ const report5 = buildReport(cfg.app.id, options.env, tenantId, contract, stored);
4725
+ if (options.json) out.log(JSON.stringify(report5, null, 2));
4726
+ else printReport(report5, out);
4727
+ return report5;
4728
4728
  }
4729
4729
  function buildReport(appId, env, tenant, contract, stored) {
4730
4730
  const declared = new Set(contract.map((secret) => secret.name));
@@ -4741,25 +4741,25 @@ function buildReport(appId, env, tenant, contract, stored) {
4741
4741
  const ok = rows.every((row) => row.state !== "missing" || !row.required);
4742
4742
  return { app: appId, env, tenant, secrets: rows, ok };
4743
4743
  }
4744
- function printReport(report4, out) {
4745
- out.log(`${report4.app} (${report4.tenant})`);
4746
- if (report4.secrets.length === 0) {
4744
+ function printReport(report5, out) {
4745
+ out.log(`${report5.app} (${report5.tenant})`);
4746
+ if (report5.secrets.length === 0) {
4747
4747
  out.log(" no secrets declared and none stored");
4748
4748
  return;
4749
4749
  }
4750
- for (const row of report4.secrets) {
4750
+ for (const row of report5.secrets) {
4751
4751
  const label = row.state === "missing" && !row.required ? "missing (optional)" : row.state;
4752
4752
  const suffix = row.sources.length ? ` \u2014 ${row.sources.join(", ")}` : "";
4753
4753
  out.log(` ${label.padEnd(18)} ${row.name}${suffix}`);
4754
4754
  }
4755
- const missing = report4.secrets.filter((row) => row.state === "missing" && row.required);
4755
+ const missing = report5.secrets.filter((row) => row.state === "missing" && row.required);
4756
4756
  if (missing.length) {
4757
4757
  out.log("");
4758
4758
  for (const row of missing) {
4759
- out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${report4.env} --stdin"`);
4759
+ out.log(`${row.name} is required but not set \u2014 "odla-ai secrets set ${row.name} --env ${report5.env} --stdin"`);
4760
4760
  }
4761
4761
  }
4762
- if (report4.secrets.some((row) => row.state === "reserved")) {
4762
+ if (report5.secrets.some((row) => row.state === "reserved")) {
4763
4763
  out.log("");
4764
4764
  out.log('"reserved" slots are never enumerated by the vault; presence cannot be confirmed here.');
4765
4765
  }
@@ -4837,12 +4837,12 @@ function claudeAdapter(skill, canonical2) {
4837
4837
  const lines = match[1].split(/\r?\n/);
4838
4838
  const frontmatter = [];
4839
4839
  let keepIndented = false;
4840
- for (const line of lines) {
4841
- if (line.startsWith("name:") || line.startsWith("description:")) {
4842
- frontmatter.push(line);
4843
- keepIndented = line.startsWith("description:");
4844
- } else if (keepIndented && /^\s+/.test(line)) {
4845
- frontmatter.push(line);
4840
+ for (const line2 of lines) {
4841
+ if (line2.startsWith("name:") || line2.startsWith("description:")) {
4842
+ frontmatter.push(line2);
4843
+ keepIndented = line2.startsWith("description:");
4844
+ } else if (keepIndented && /^\s+/.test(line2)) {
4845
+ frontmatter.push(line2);
4846
4846
  } else {
4847
4847
  keepIndented = false;
4848
4848
  }
@@ -6166,13 +6166,13 @@ async function createConversionRegistry(config) {
6166
6166
  policies.set(policy.conversionId, Object.freeze(policy));
6167
6167
  }
6168
6168
  const outputCounts = /* @__PURE__ */ new Map();
6169
- const get = (id2, kind) => {
6169
+ const get2 = (id2, kind) => {
6170
6170
  const policy = policies.get(id2);
6171
6171
  if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
6172
6172
  return policy;
6173
6173
  };
6174
6174
  const checked = (source, id2, kind) => {
6175
- const policy = get(id2, kind);
6175
+ const policy = get2(id2, kind);
6176
6176
  if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
6177
6177
  return policy;
6178
6178
  };
@@ -7003,7 +7003,7 @@ var PATH = /^[A-Za-z0-9_@+.,-]+(?:\/[A-Za-z0-9_@+.,-]+)*$/;
7003
7003
  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;
7004
7004
  function stripPatchEnvelope(patch2) {
7005
7005
  if (!/^\*\*\* (?:Begin|End) Patch\s*$/m.test(patch2)) return patch2;
7006
- const kept = patch2.split("\n").filter((line) => !/^\*\*\* (?:Begin|End) Patch\s*$/.test(line));
7006
+ const kept = patch2.split("\n").filter((line2) => !/^\*\*\* (?:Begin|End) Patch\s*$/.test(line2));
7007
7007
  const stripped = kept.join("\n");
7008
7008
  return /^diff --git /m.test(stripped) ? stripped : patch2;
7009
7009
  }
@@ -7024,9 +7024,9 @@ function validateCodePatch(rawPatch, maxBytes) {
7024
7024
  const paths = [];
7025
7025
  const lines = patch2.split("\n");
7026
7026
  for (let index = 0; index < lines.length; index += 1) {
7027
- const line = lines[index];
7028
- if (!line.startsWith("diff --git ")) continue;
7029
- const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(line);
7027
+ const line2 = lines[index];
7028
+ if (!line2.startsWith("diff --git ")) continue;
7029
+ const match = /^diff --git a\/(\S+) b\/(\S+)$/.exec(line2);
7030
7030
  const path = match?.[1];
7031
7031
  if (!path || !match?.[2] || path !== match[2]) throw new TypeError("patch must use one unquoted relative path per diff");
7032
7032
  validateRelativePath(path);
@@ -7059,9 +7059,9 @@ function resolveCodePath(workspaceDir, path) {
7059
7059
  return target;
7060
7060
  }
7061
7061
  function describePatchFailure(patch2, detail) {
7062
- const hunks = patch2.split("\n").filter((line) => line.startsWith("@@"));
7062
+ const hunks = patch2.split("\n").filter((line2) => line2.startsWith("@@"));
7063
7063
  const bodies = patch2.split(/^@@.*$/m).slice(1);
7064
- const contextless = bodies.some((body) => !body.split("\n").some((line) => line.startsWith(" ") && line.trim().length > 0));
7064
+ const contextless = bodies.some((body) => !body.split("\n").some((line2) => line2.startsWith(" ") && line2.trim().length > 0));
7065
7065
  const hint = hunks.length > 0 && contextless ? " A hunk has no context lines; include at least one unchanged line above or below each change." : "";
7066
7066
  return `patch did not apply: ${detail}${hint}`;
7067
7067
  }
@@ -9622,11 +9622,237 @@ function record6(value2) {
9622
9622
  return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
9623
9623
  }
9624
9624
 
9625
+ // src/code-grant-command.ts
9626
+ var ACTIONS = ["request", "list", "approve", "revoke"];
9627
+ var isAction = (value2) => ACTIONS.includes(value2 ?? "");
9628
+ async function fail2(response2, what) {
9629
+ const body = redactSecrets((await response2.text()).slice(0, 1e3));
9630
+ throw new Error(`${what} failed (${response2.status}): ${body}`);
9631
+ }
9632
+ function line(grant) {
9633
+ const who = grant.status === "granted" ? `granted by ${grant.approvedBy ?? "?"}` : `requested by ${grant.requestedBy}`;
9634
+ return `${grant.grantId} ${grant.appEnv} ${grant.owner}/${grant.name} ${grant.status} ${who}`;
9635
+ }
9636
+ function report(grants, out, json) {
9637
+ if (json) return out.log(JSON.stringify({ grants }, null, 2));
9638
+ if (!grants.length) return out.log("no repository grants for this app");
9639
+ out.log("GRANT ENV REPOSITORY STATUS DECIDED");
9640
+ for (const grant of grants) out.log(line(grant));
9641
+ const pending = grants.filter((grant) => grant.status === "requested");
9642
+ if (pending.length) {
9643
+ out.log(`
9644
+ ${pending.length} awaiting approval. A human owner runs:`);
9645
+ for (const grant of pending) out.log(` odla-ai code grant approve ${grant.grantId} --env ${grant.appEnv}`);
9646
+ }
9647
+ }
9648
+ function grantsUrl(cfg, suffix = "") {
9649
+ return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/code/repository-grants${suffix}`;
9650
+ }
9651
+ async function codeGrantCommand(parsed, deps = {}) {
9652
+ const action2 = parsed.positionals[2];
9653
+ if (!isAction(action2)) {
9654
+ throw new Error(
9655
+ `unknown code grant action "${action2 ?? ""}". Try "odla-ai code grant list --env dev".`
9656
+ );
9657
+ }
9658
+ const decides = action2 === "approve" || action2 === "revoke";
9659
+ assertArgs(parsed, ["config", "env", "json", "token", "email", "open"], decides ? 4 : 3);
9660
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
9661
+ const doFetch = deps.fetch ?? fetch;
9662
+ const out = deps.stdout ?? console;
9663
+ const env = stringOpt(parsed.options.env) ?? "dev";
9664
+ const json = parsed.options.json === true;
9665
+ const token = await getDeveloperToken(cfg, {
9666
+ configPath: cfg.configPath,
9667
+ token: stringOpt(parsed.options.token),
9668
+ email: stringOpt(parsed.options.email),
9669
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
9670
+ openApprovalUrl: deps.openUrl
9671
+ // Every verb here administers grants, which the registry gates on
9672
+ // app.manage — a token without it is refused as "not your app", which reads
9673
+ // like the wrong app rather than the wrong authority.
9674
+ //
9675
+ // Only `request` additionally asks for code.session. Approving is a human
9676
+ // act, and a token minted to approve has no business also carrying the
9677
+ // authority the approval is about.
9678
+ }, doFetch, out, {
9679
+ optionalProjectCapabilities: action2 === "request" ? ["app.manage", "code.session"] : ["app.manage"]
9680
+ });
9681
+ const auth = { authorization: `Bearer ${token}` };
9682
+ if (action2 === "list") {
9683
+ const response3 = await doFetch(`${grantsUrl(cfg)}?env=${encodeURIComponent(env)}`, { headers: auth });
9684
+ if (!response3.ok) await fail2(response3, "repository grant list");
9685
+ const body2 = await response3.json();
9686
+ return report(body2.grants, out, json);
9687
+ }
9688
+ if (action2 === "request") {
9689
+ const response3 = await doFetch(grantsUrl(cfg), {
9690
+ method: "POST",
9691
+ headers: { ...auth, "content-type": "application/json" },
9692
+ body: JSON.stringify({ env })
9693
+ });
9694
+ if (!response3.ok) await fail2(response3, "repository grant request");
9695
+ const { grant } = await response3.json();
9696
+ if (json) return out.log(JSON.stringify({ grant }, null, 2));
9697
+ out.log(`requested ${grant.owner}/${grant.name} (${grant.appEnv}) \u2014 ${grant.grantId}`);
9698
+ return out.log(grant.status === "granted" ? "already granted; nothing to approve" : `a human owner of ${cfg.app.id} approves it with:
9699
+ odla-ai code grant approve ${grant.grantId} --env ${env}`);
9700
+ }
9701
+ const grantId = parsed.positionals[3];
9702
+ if (!grantId) throw new Error(`code grant ${action2} requires the grant id from "odla-ai code grant list"`);
9703
+ const url = `${grantsUrl(cfg, `/${encodeURIComponent(grantId)}`)}`;
9704
+ const response2 = action2 === "approve" ? await doFetch(url, {
9705
+ method: "POST",
9706
+ headers: { ...auth, "content-type": "application/json" },
9707
+ body: JSON.stringify({ env })
9708
+ }) : await doFetch(`${url}?env=${encodeURIComponent(env)}`, { method: "DELETE", headers: auth });
9709
+ if (!response2.ok) await fail2(response2, `repository grant ${action2}`);
9710
+ const body = await response2.json();
9711
+ if (json) return out.log(JSON.stringify(body, null, 2));
9712
+ out.log(action2 === "approve" && body.grant ? `granted ${body.grant.owner}/${body.grant.name} (${body.grant.appEnv}) to ${cfg.app.id}` : `revoked ${grantId}`);
9713
+ }
9714
+
9715
+ // src/code-repository-command.ts
9716
+ var ACTIONS2 = ["show", "list", "bind"];
9717
+ var isAction2 = (value2) => ACTIONS2.includes(value2 ?? "");
9718
+ async function get(doFetch, url, token, what) {
9719
+ const response2 = await doFetch(url, { headers: { authorization: `Bearer ${token}` } });
9720
+ if (!response2.ok) {
9721
+ const body = redactSecrets((await response2.text()).slice(0, 1e3));
9722
+ if (response2.status === 403 && body.includes("human_session_required")) {
9723
+ throw new Error(
9724
+ `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".`
9725
+ );
9726
+ }
9727
+ throw new Error(`${what} failed (${response2.status}): ${body}`);
9728
+ }
9729
+ return await response2.json();
9730
+ }
9731
+ async function catalogue(doFetch, platformUrl, token) {
9732
+ const account = await get(
9733
+ doFetch,
9734
+ `${platformUrl}/registry/code/github/account`,
9735
+ token,
9736
+ "GitHub account read"
9737
+ );
9738
+ const active = account.installations.filter((install2) => install2.status === "active");
9739
+ const found = [];
9740
+ for (const install2 of active) {
9741
+ const { repositories } = await get(
9742
+ doFetch,
9743
+ `${platformUrl}/registry/code/github/installations/${install2.installationId}/repositories`,
9744
+ token,
9745
+ `repository catalog for ${install2.accountLogin}`
9746
+ );
9747
+ for (const repository of repositories) {
9748
+ found.push({ ...repository, installationId: install2.installationId });
9749
+ }
9750
+ }
9751
+ if (!active.length) {
9752
+ throw new Error(
9753
+ "no active GitHub App installation. Install odla's GitHub App on the account that owns the repository first, from Studio \u2192 Code."
9754
+ );
9755
+ }
9756
+ return found;
9757
+ }
9758
+ function selectRepository(found, full) {
9759
+ const wanted = full.toLowerCase();
9760
+ const matches = found.filter((repository) => repository.fullName.toLowerCase() === wanted);
9761
+ if (matches.length === 1) return matches[0];
9762
+ if (!matches.length) {
9763
+ const near = found.filter((repository) => repository.name.toLowerCase() === wanted.split("/").pop());
9764
+ 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.`);
9765
+ }
9766
+ throw new Error(
9767
+ `${full} is reachable through ${matches.length} installations (${matches.map((match) => match.installationId).join(", ")}); revoke the one you do not mean.`
9768
+ );
9769
+ }
9770
+ function repositoryUrl(cfg, suffix = "") {
9771
+ return `${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}/code/repository${suffix}`;
9772
+ }
9773
+ async function codeRepositoryCommand(parsed, deps = {}) {
9774
+ const action2 = parsed.positionals[2];
9775
+ if (!isAction2(action2)) {
9776
+ throw new Error(
9777
+ `unknown code repository action "${action2 ?? ""}". Try "odla-ai code repository show --env dev".`
9778
+ );
9779
+ }
9780
+ assertArgs(parsed, ["config", "env", "repo", "json", "token", "email", "open"], 3);
9781
+ const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
9782
+ const doFetch = deps.fetch ?? fetch;
9783
+ const out = deps.stdout ?? console;
9784
+ const env = stringOpt(parsed.options.env) ?? "dev";
9785
+ const json = parsed.options.json === true;
9786
+ const token = await getDeveloperToken(cfg, {
9787
+ configPath: cfg.configPath,
9788
+ token: stringOpt(parsed.options.token),
9789
+ email: stringOpt(parsed.options.email),
9790
+ open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
9791
+ openApprovalUrl: deps.openUrl
9792
+ }, doFetch, out, { optionalProjectCapabilities: ["app.manage"] });
9793
+ if (action2 === "show") {
9794
+ const body2 = await get(
9795
+ doFetch,
9796
+ `${repositoryUrl(cfg)}?env=${encodeURIComponent(env)}`,
9797
+ token,
9798
+ "repository read"
9799
+ );
9800
+ if (json) return out.log(JSON.stringify(body2, null, 2));
9801
+ const bound = body2.environmentRepository;
9802
+ 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}".`);
9803
+ }
9804
+ const found = await catalogue(doFetch, cfg.platformUrl, token);
9805
+ if (action2 === "list") {
9806
+ if (json) return out.log(JSON.stringify({ repositories: found }, null, 2));
9807
+ if (!found.length) return out.log("no repositories reachable through your installations");
9808
+ out.log("REPOSITORY INSTALL DEFAULT READY");
9809
+ for (const repository of found) {
9810
+ out.log(`${repository.fullName} ${repository.installationId} ${repository.defaultBranch ?? "\u2014"} ${repository.ready ? "yes" : "empty"}`);
9811
+ }
9812
+ return;
9813
+ }
9814
+ const repo = stringOpt(parsed.options.repo);
9815
+ if (!repo?.includes("/")) throw new Error("code repository bind requires --repo owner/name");
9816
+ const selected = selectRepository(found, repo);
9817
+ if (!selected.ready) {
9818
+ throw new Error(`${selected.fullName} has no commits yet; GitHub cannot serve a default branch for an empty repository.`);
9819
+ }
9820
+ const response2 = await doFetch(repositoryUrl(cfg, "/bind"), {
9821
+ method: "POST",
9822
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
9823
+ body: JSON.stringify({ env, installationId: selected.installationId, repositoryId: selected.repositoryId })
9824
+ });
9825
+ if (!response2.ok) {
9826
+ throw new Error(`repository bind failed (${response2.status}): ${redactSecrets((await response2.text()).slice(0, 1e3))}`);
9827
+ }
9828
+ const body = await response2.json();
9829
+ if (json) return out.log(JSON.stringify(body, null, 2));
9830
+ out.log(`connected ${cfg.app.id} (${env}) to ${selected.fullName} @ ${selected.defaultBranch}`);
9831
+ out.log(`Next, so it may be worked on unattended:
9832
+ odla-ai code grant request --env ${env}`);
9833
+ }
9834
+
9625
9835
  // src/code-command.ts
9626
9836
  async function codeCommand(parsed, dependencies) {
9627
9837
  const sub = parsed.positionals[1];
9838
+ if (sub === "grant") {
9839
+ return await codeGrantCommand(parsed, {
9840
+ fetch: dependencies.fetch,
9841
+ stdout: dependencies.stdout,
9842
+ openUrl: dependencies.openUrl
9843
+ });
9844
+ }
9845
+ if (sub === "repository") {
9846
+ return await codeRepositoryCommand(parsed, {
9847
+ fetch: dependencies.fetch,
9848
+ stdout: dependencies.stdout,
9849
+ openUrl: dependencies.openUrl
9850
+ });
9851
+ }
9628
9852
  if (sub !== "connect") {
9629
- throw new Error(`unknown code subcommand "${sub ?? ""}". Try "odla-ai code connect --env dev".`);
9853
+ throw new Error(
9854
+ `unknown code subcommand "${sub ?? ""}". Try "odla-ai code connect --env dev" or "odla-ai code grant list --env dev".`
9855
+ );
9630
9856
  }
9631
9857
  assertArgs(parsed, [
9632
9858
  "config",
@@ -10068,6 +10294,9 @@ Commands:
10068
10294
  \u2014 no secret is ever copied between people.
10069
10295
  capabilities Show what the CLI automates vs agent edits and human checkpoints.
10070
10296
  code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
10297
+ "code repository show" reports the repository an app works on (Studio or a
10298
+ human session connects one); "code grant request|list|approve|revoke" then
10299
+ governs unattended access: an agent may request, only a human may approve.
10071
10300
  admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
10072
10301
  security Connect GitHub sources and run commit-pinned hosted reviews, or scan a local snapshot.
10073
10302
  pm Project management (via @odla-ai/pm): Products contain Projects;
@@ -10168,6 +10397,17 @@ Safety:
10168
10397
  approval and credential hashes live in odla-ai/db. The host
10169
10398
  credential is never written under .odla/; it exists only in the foreground
10170
10399
  "code connect" process and is rotated by the next approved connection.
10400
+ "code repository bind" takes owner/name and resolves the two GitHub integers the
10401
+ bind route wants across every installation you have, refusing an ambiguous match
10402
+ rather than choosing one \u2014 the same repository name under two organizations is
10403
+ ordinary, and picking the first would connect an app to source nobody chose. It
10404
+ needs a signed-in human session; a CLI handshake token is a delegated agent
10405
+ credential by design, so an agent cannot choose the source it will be given.
10406
+ Unattended access is per repository and revocable on its own, but the connection
10407
+ IS the authorization: a repository is recorded for one exact app and environment
10408
+ only by a signed-in human choosing it. "code grant list" shows what is authorized;
10409
+ "revoke" stops unattended access without disconnecting the repository, and
10410
+ re-binding never resurrects it. Connecting sandbox never authorizes live.
10171
10411
  Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
10172
10412
  OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
10173
10413
  GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
@@ -10534,7 +10774,7 @@ async function discussWatch(ctx, topicId, parsed) {
10534
10774
  throw new WatchRemoteError(cursor, error);
10535
10775
  }
10536
10776
  if (deadline !== void 0 && now() >= deadline) {
10537
- const result = report(ctx, parsed, { found: false, cursor: cursor ?? "" });
10777
+ const result = report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
10538
10778
  throw new WatchTimeoutError(result.cursor);
10539
10779
  }
10540
10780
  const base = Math.min(intervalMs, 1e3);
@@ -10575,7 +10815,7 @@ async function discussWatch(ctx, topicId, parsed) {
10575
10815
  });
10576
10816
  const posts = topicId ? matching.filter((event) => event.type === "message").map((event) => event.payload) : void 0;
10577
10817
  const topics = topicId ? void 0 : matching.filter((event) => event.type === "activity").map((event) => event.payload);
10578
- return report(ctx, parsed, {
10818
+ return report2(ctx, parsed, {
10579
10819
  found: true,
10580
10820
  cursor,
10581
10821
  events: matching,
@@ -10602,13 +10842,13 @@ async function discussWatch(ctx, topicId, parsed) {
10602
10842
  }
10603
10843
  if (page2.hasMore) continue;
10604
10844
  if (deadline !== void 0 && now() >= deadline) {
10605
- return report(ctx, parsed, { found: false, cursor });
10845
+ return report2(ctx, parsed, { found: false, cursor });
10606
10846
  }
10607
10847
  const wait2 = deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()));
10608
10848
  await sleep(wait2);
10609
10849
  }
10610
10850
  }
10611
- function report(ctx, parsed, result) {
10851
+ function report2(ctx, parsed, result) {
10612
10852
  if (ctx.json) {
10613
10853
  ctx.out.log(JSON.stringify(result, null, 2));
10614
10854
  } else if (parsed.options.jsonl !== true && result.found) {
@@ -11205,7 +11445,7 @@ function eventLabel(event) {
11205
11445
  const body = event.payload.message?.body?.replace(/\s+/g, " ").trim();
11206
11446
  return body || event.payload.entityId;
11207
11447
  }
11208
- function report2(ctx, parsed, result) {
11448
+ function report3(ctx, parsed, result) {
11209
11449
  if (ctx.json) ctx.out.log(JSON.stringify(result, null, 2));
11210
11450
  else if (parsed.options.jsonl !== true && result.found) {
11211
11451
  for (const event of result.events ?? []) {
@@ -11265,7 +11505,7 @@ async function pmWatch(ctx, parsed) {
11265
11505
  });
11266
11506
  if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES2) throw error;
11267
11507
  if (deadline !== void 0 && now() >= deadline) {
11268
- return report2(ctx, parsed, { found: false, cursor: cursor ?? "" });
11508
+ return report3(ctx, parsed, { found: false, cursor: cursor ?? "" });
11269
11509
  }
11270
11510
  const backoff = Math.min(
11271
11511
  MAX_BACKOFF_MS2,
@@ -11306,7 +11546,7 @@ async function pmWatch(ctx, parsed) {
11306
11546
  cursor,
11307
11547
  serverTime: current.serverTime
11308
11548
  });
11309
- return report2(ctx, parsed, { found: true, cursor, events: matching });
11549
+ return report3(ctx, parsed, { found: true, cursor, events: matching });
11310
11550
  }
11311
11551
  if (current.events.length > 0) {
11312
11552
  jsonl2(ctx, parsed, {
@@ -11325,7 +11565,7 @@ async function pmWatch(ctx, parsed) {
11325
11565
  }
11326
11566
  if (current.hasMore) continue;
11327
11567
  if (deadline !== void 0 && now() >= deadline) {
11328
- return report2(ctx, parsed, { found: false, cursor });
11568
+ return report3(ctx, parsed, { found: false, cursor });
11329
11569
  }
11330
11570
  await sleep(
11331
11571
  deadline === void 0 ? intervalMs : Math.min(intervalMs, Math.max(0, deadline - now()))
@@ -11862,8 +12102,8 @@ function printO11yStatus(status, out) {
11862
12102
  out.log(
11863
12103
  `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`
11864
12104
  );
11865
- for (const line of providerCapacityLines(status.providerCapacity)) {
11866
- out.log(line);
12105
+ for (const line2 of providerCapacityLines(status.providerCapacity)) {
12106
+ out.log(line2);
11867
12107
  }
11868
12108
  const coverage = record9(status.providerReconciliation.body.comparison) ? status.providerReconciliation.body.comparison : {};
11869
12109
  const coverageCounts = record9(status.providerReconciliation.body.counts) ? status.providerReconciliation.body.counts : {};
@@ -12906,7 +13146,11 @@ var COMMAND_SURFACE = {
12906
13146
  bug: { create: {}, list: {}, report: {} },
12907
13147
  calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
12908
13148
  capabilities: {},
12909
- code: { connect: {} },
13149
+ code: {
13150
+ connect: {},
13151
+ grant: { request: {}, list: {}, approve: {}, revoke: {} },
13152
+ repository: { show: {}, list: {}, bind: {} }
13153
+ },
12910
13154
  config: { diff: {}, plan: {}, apply: {} },
12911
13155
  context: { show: {}, list: {}, save: {}, remove: {} },
12912
13156
  credentials: { list: {}, revoke: {} },
@@ -13172,8 +13416,8 @@ function parseRunbook(text3, slug) {
13172
13416
  const fm = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text3);
13173
13417
  if (fm) {
13174
13418
  rest = text3.slice(fm[0].length);
13175
- for (const line of fm[1].split(/\r?\n/)) {
13176
- const pair = /^(\w+)\s*:\s*(.+)$/.exec(line.trim());
13419
+ for (const line2 of fm[1].split(/\r?\n/)) {
13420
+ const pair = /^(\w+)\s*:\s*(.+)$/.exec(line2.trim());
13177
13421
  if (!pair) continue;
13178
13422
  const value2 = pair[2].trim().replace(/^["']|["']$/g, "");
13179
13423
  if (pair[1] === "summary") meta.summary = value2;
@@ -13313,18 +13557,18 @@ function areaOf(path) {
13313
13557
  return parts[src + 2] !== void 0 ? parts[src + 1] ?? null : null;
13314
13558
  }
13315
13559
  function scanHunk(lines, into) {
13316
- for (const [i, line] of lines.entries()) {
13317
- const decl = DECL.exec(line);
13560
+ for (const [i, line2] of lines.entries()) {
13561
+ const decl = DECL.exec(line2);
13318
13562
  if (decl?.[1]) {
13319
13563
  into.add(decl[1]);
13320
13564
  continue;
13321
13565
  }
13322
- const named = NAMED.exec(line);
13566
+ const named = NAMED.exec(line2);
13323
13567
  if (named?.[1]) {
13324
13568
  for (const name of namedExports(named[1])) into.add(name);
13325
13569
  continue;
13326
13570
  }
13327
- if (!JSDOC.test(line)) continue;
13571
+ if (!JSDOC.test(line2)) continue;
13328
13572
  for (let j = i + 1; j < lines.length && j < i + 40; j++) {
13329
13573
  const found = ANY_DECL.exec(lines[j]);
13330
13574
  if (found?.[1]) {
@@ -13342,8 +13586,8 @@ function parseDiff(diff) {
13342
13586
  if (current && hunk.length) scanHunk(hunk, current.exports);
13343
13587
  hunk = [];
13344
13588
  };
13345
- for (const line of diff.split("\n")) {
13346
- const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(line);
13589
+ for (const line2 of diff.split("\n")) {
13590
+ const header = /^diff --git a\/(\S+) b\/(\S+)/.exec(line2);
13347
13591
  if (header) {
13348
13592
  flush();
13349
13593
  const path = header[2] ?? header[1];
@@ -13351,11 +13595,11 @@ function parseDiff(diff) {
13351
13595
  files.set(path, current);
13352
13596
  continue;
13353
13597
  }
13354
- if (line.startsWith("@@")) {
13598
+ if (line2.startsWith("@@")) {
13355
13599
  flush();
13356
13600
  continue;
13357
13601
  }
13358
- if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(line);
13602
+ if (current && SOURCE2.test(current.path) && !TEST_PATH.test(current.path)) hunk.push(line2);
13359
13603
  }
13360
13604
  flush();
13361
13605
  return [...files.values()];
@@ -13418,7 +13662,7 @@ function untrackedDiff(runGit, read3) {
13418
13662
  return "";
13419
13663
  }
13420
13664
  let out = "";
13421
- for (const path of listed.split("\n").map((line) => line.trim()).filter(Boolean)) {
13665
+ for (const path of listed.split("\n").map((line2) => line2.trim()).filter(Boolean)) {
13422
13666
  out += `diff --git a/${path} b/${path}
13423
13667
  --- /dev/null
13424
13668
  +++ b/${path}
@@ -13431,7 +13675,7 @@ function untrackedDiff(runGit, read3) {
13431
13675
  continue;
13432
13676
  }
13433
13677
  out += `@@ -0,0 +1,${body.split("\n").length} @@
13434
- ${body.split("\n").map((line) => `+${line}`).join("\n")}
13678
+ ${body.split("\n").map((line2) => `+${line2}`).join("\n")}
13435
13679
  `;
13436
13680
  }
13437
13681
  return out;
@@ -13479,7 +13723,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
13479
13723
  return out;
13480
13724
  }
13481
13725
  var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
13482
- function report3(ctx, impacts) {
13726
+ function report4(ctx, impacts) {
13483
13727
  const covered = impacts.filter((i) => i.runbooks.length);
13484
13728
  ctx.out.log(
13485
13729
  `${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.`
@@ -13517,7 +13761,7 @@ async function runbookImpact(ctx, options, deps = {}) {
13517
13761
  }
13518
13762
  const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
13519
13763
  if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
13520
- report3(ctx, impacts);
13764
+ report4(ctx, impacts);
13521
13765
  }
13522
13766
 
13523
13767
  // src/runbook-lint.ts
@@ -13593,7 +13837,7 @@ async function runbookSearch(ctx, query, all, limit) {
13593
13837
  for (const [i, hit] of result.hits.entries()) {
13594
13838
  if (i) ctx.out.log("");
13595
13839
  ctx.out.log(`${hit.slug}${hit.heading ? ` \xA7 ${hit.heading}` : ""} (v${hit.version})`);
13596
- for (const line of hit.excerpt.split("\n")) ctx.out.log(` ${line}`);
13840
+ for (const line2 of hit.excerpt.split("\n")) ctx.out.log(` ${line2}`);
13597
13841
  ctx.out.log(` source: ${hit.slug}${hit.anchor ? `#${hit.anchor}` : ""} \xB7 ${hit.command}`);
13598
13842
  }
13599
13843
  }
@@ -13968,31 +14212,31 @@ function printHostedJob(out, job, platform, appId) {
13968
14212
  url.searchParams.set("job", job.jobId);
13969
14213
  out.log(` Studio: ${url.toString()}`);
13970
14214
  }
13971
- function printHostedReport(out, report4) {
13972
- out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
13973
- 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}`);
13974
- out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
13975
- out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
13976
- out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
13977
- for (const finding of report4.findings) {
14215
+ function printHostedReport(out, report5) {
14216
+ out.log(`security report ${report5.jobId}: ${report5.repository}@${report5.revision}`);
14217
+ 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}`);
14218
+ out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates} rejected=${report5.metrics.rejected}`);
14219
+ out.log(` discovery: ${report5.provenance.discovery?.provider ?? "unknown"}/${report5.provenance.discovery?.model ?? "unknown"}`);
14220
+ out.log(` validation: ${report5.provenance.validation?.provider ?? "unknown"}/${report5.provenance.validation?.model ?? "unknown"} independent=${String(report5.provenance.independentValidation)}`);
14221
+ for (const finding of report5.findings) {
13978
14222
  const location = finding.locations[0];
13979
14223
  out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
13980
14224
  }
13981
- for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
14225
+ for (const limitation of report5.limitations) out.log(` limitation: ${limitation}`);
13982
14226
  }
13983
- function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
14227
+ function enforceHostedReportGate(report5, parsed, out, emitSuccess) {
13984
14228
  const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
13985
14229
  const candidateValue = parsed.options["fail-on-candidates"];
13986
14230
  const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
13987
14231
  const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
13988
- const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
13989
- const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
13990
- const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
14232
+ const confirmed = report5.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
14233
+ const leads = failOnCandidates ? report5.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
14234
+ const incomplete = report5.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
13991
14235
  if (confirmed.length || leads.length || incomplete) {
13992
- throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
14236
+ throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report5.coverageStatus}` : ""}`);
13993
14237
  }
13994
14238
  if (emitSuccess) {
13995
- out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
14239
+ out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report5.coverageStatus}. This is not proof that the application is secure.`);
13996
14240
  }
13997
14241
  }
13998
14242
  function printHostedSecurityPlanRoute(out, label, route3) {
@@ -14082,17 +14326,17 @@ async function runHostedSecurity(options) {
14082
14326
  allowNetwork: false
14083
14327
  }
14084
14328
  });
14085
- const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
14086
- await writeSecurityArtifacts(output, report4);
14087
- const reportDigest = await securityFingerprint(report4);
14329
+ const report5 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
14330
+ await writeSecurityArtifacts(output, report5);
14331
+ const reportDigest = await securityFingerprint(report5);
14088
14332
  await hosted.complete({
14089
14333
  reportDigest,
14090
- coverageStatus: report4.coverageStatus,
14091
- confirmed: report4.metrics.confirmed,
14092
- candidates: report4.metrics.candidates
14334
+ coverageStatus: report5.coverageStatus,
14335
+ confirmed: report5.metrics.confirmed,
14336
+ candidates: report5.metrics.candidates
14093
14337
  }, { signal: options.signal });
14094
- printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
14095
- return Object.freeze({ report: report4, run: hosted.run, output });
14338
+ printSummary(options.stdout ?? console, appId, env, hosted.run, report5, output);
14339
+ return Object.freeze({ report: report5, run: hosted.run, output });
14096
14340
  }
14097
14341
  function selectEnv(requested, declared, configPath, rootDir) {
14098
14342
  const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
@@ -14118,14 +14362,14 @@ function profileFor(name, maxHuntTasks) {
14118
14362
  if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
14119
14363
  return { ...profile, maxHuntTasks };
14120
14364
  }
14121
- function printSummary(out, appId, env, run, report4, output) {
14122
- const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
14365
+ function printSummary(out, appId, env, run, report5, output) {
14366
+ const complete = report5.coverage.filter((cell) => cell.state === "complete").length;
14123
14367
  out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
14124
14368
  out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
14125
14369
  out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
14126
- 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}`);
14127
- if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
14128
- out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
14370
+ 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}`);
14371
+ if (report5.callBudget) out.log(` calls: discovery=${formatBudget(report5.callBudget.discovery)} validation=${formatBudget(report5.callBudget.validation)}`);
14372
+ out.log(` findings: confirmed=${report5.metrics.confirmed} needs_reproduction=${report5.metrics.needsReproduction} candidates=${report5.metrics.candidates}`);
14129
14373
  out.log(` report: ${resolve13(output, "REPORT.md")}`);
14130
14374
  }
14131
14375
  function formatBudget(usage) {
@@ -14372,13 +14616,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
14372
14616
  }
14373
14617
  throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
14374
14618
  }
14375
- const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
14619
+ const report5 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
14376
14620
  if (parsed.options.json === true) {
14377
- context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
14621
+ context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report5 }, null, 2));
14378
14622
  } else {
14379
- printHostedReport(context.stdout, report4);
14623
+ printHostedReport(context.stdout, report5);
14380
14624
  }
14381
- enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
14625
+ enforceHostedReportGate(report5, parsed, context.stdout, parsed.options.json !== true);
14382
14626
  }
14383
14627
  async function runLocalSecurityCommand(parsed, dependencies) {
14384
14628
  if (parsed.options.source === true) {
@@ -14446,13 +14690,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
14446
14690
  });
14447
14691
  enforceLocalGate(result.report, parsed);
14448
14692
  }
14449
- function enforceLocalGate(report4, parsed) {
14693
+ function enforceLocalGate(report5, parsed) {
14450
14694
  const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
14451
14695
  const candidateValue = parsed.options["fail-on-candidates"];
14452
14696
  const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
14453
- const confirmed = findingsAtOrAbove(report4, failOn);
14454
- const leads = failOnCandidates ? findingsAtOrAbove(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
14455
- const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
14697
+ const confirmed = findingsAtOrAbove(report5, failOn);
14698
+ const leads = failOnCandidates ? findingsAtOrAbove(report5, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
14699
+ const incomplete = report5.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
14456
14700
  if (confirmed.length || leads.length || incomplete) {
14457
14701
  throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
14458
14702
  }
@@ -14491,9 +14735,9 @@ async function securityCommand(parsed, dependencies) {
14491
14735
  assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
14492
14736
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
14493
14737
  const context = await hostedSecurityContext(parsed, dependencies);
14494
- const report4 = await getHostedSecurityReport({ ...context, jobId });
14495
- if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
14496
- else printHostedReport(context.stdout, report4);
14738
+ const report5 = await getHostedSecurityReport({ ...context, jobId });
14739
+ if (parsed.options.json === true) context.stdout.log(JSON.stringify(report5, null, 2));
14740
+ else printHostedReport(context.stdout, report5);
14497
14741
  return;
14498
14742
  }
14499
14743
  if (sub !== "run") {
@@ -14790,4 +15034,4 @@ export {
14790
15034
  isTerminalHostedSecurityStatus,
14791
15035
  runCli
14792
15036
  };
14793
- //# sourceMappingURL=chunk-RUEM7ZTA.js.map
15037
+ //# sourceMappingURL=chunk-EG23MPUC.js.map