@scalequality/cli 0.4.9 → 0.4.11

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "sdkVersion": "0.3.281",
3
- "sha256": "411f9d73e135bdcb7327703ee340f0966eda86ac60193d4a545950a4b1f7071d",
4
- "sourceCommit": "c85fa2362e7a802197cca13e9438f8cd90bda4c0"
5
- }
3
+ "sha256": "c2e9fefe31fcd4f9b46e4f5323336571b20380b6756c90b494584c1727d9f787",
4
+ "sourceCommit": "6437124ad656162948b2c83adfd87e5afc2d82a7"
5
+ }
package/dist/connect.cjs CHANGED
@@ -1290,13 +1290,14 @@ async function decideToolUse(toolName, input, ctx) {
1290
1290
  }
1291
1291
 
1292
1292
  // src/application/services/workspaceSandbox/protections.ts
1293
- var NO_PROTECTIONS = Object.freeze({ secretFiles: false, protectedPush: false, deployApproval: false, destructiveCommands: false, unknownPackage: null });
1293
+ var NO_PROTECTIONS = Object.freeze({ secretFiles: false, protectedPush: false, deployApproval: false, destructiveCommands: false, unknownPackage: null, toolApproval: false });
1294
1294
  var KNOWN_RULES = {
1295
1295
  "secret-files": ["block"],
1296
1296
  "protected-push": ["require"],
1297
1297
  "deploy-approval": ["require"],
1298
1298
  "destructive-commands": ["require"],
1299
- "unknown-package": ["block", "warn"]
1299
+ "unknown-package": ["block", "warn"],
1300
+ "tool-approval": ["require"]
1300
1301
  };
1301
1302
  function protectionsWireOf(raw) {
1302
1303
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
@@ -1318,7 +1319,8 @@ function parseProtections(raw) {
1318
1319
  protectedPush: r["protected-push"] === "require",
1319
1320
  deployApproval: r["deploy-approval"] === "require",
1320
1321
  destructiveCommands: r["destructive-commands"] === "require",
1321
- unknownPackage: r["unknown-package"] === "block" || r["unknown-package"] === "warn" ? r["unknown-package"] : null
1322
+ unknownPackage: r["unknown-package"] === "block" || r["unknown-package"] === "warn" ? r["unknown-package"] : null,
1323
+ toolApproval: r["tool-approval"] === "require"
1322
1324
  };
1323
1325
  }
1324
1326
  var SECRET_FILE_MESSAGE = "Blocked by your organization's protection: secret files are never sent to the model. Do not try to read this file another way; ask the user for the specific value your task needs, or use the example file (such as .env.example).";
@@ -1857,6 +1859,7 @@ var RegistryChecker = class {
1857
1859
  }
1858
1860
  };
1859
1861
  function mayNeedProtection(command, p) {
1862
+ if (p.toolApproval) return true;
1860
1863
  if (p.destructiveCommands && isDestructiveCommand(command)) return true;
1861
1864
  if (p.deployApproval && isDeployCommand(command)) return true;
1862
1865
  if (p.protectedPush && gitPushes(command).length > 0) return true;
@@ -1889,6 +1892,7 @@ async function protectionVerdict(command, p, deps) {
1889
1892
  if (p.destructiveCommands && isDestructiveCommand(command)) reasons.push("PROTECTION_DESTRUCTIVE_COMMAND");
1890
1893
  if (p.deployApproval && isDeployCommand(command)) reasons.push("PROTECTION_DEPLOY");
1891
1894
  if (p.protectedPush && await isProtectedPush(command, deps.currentBranch)) reasons.push("PROTECTION_PROTECTED_PUSH");
1895
+ if (p.toolApproval && !reasons.length) reasons.push("PROTECTION_TOOL_APPROVAL");
1892
1896
  if (!reasons.length) return { kind: "allow" };
1893
1897
  return { kind: "ask", reasons, ...pkg ? { package: pkg.name, packageReason: pkg.reason } : {} };
1894
1898
  }
@@ -1896,7 +1900,12 @@ var PROTECTION_MESSAGES = {
1896
1900
  rejected: "Your organization's protection requires a person to approve this command, and the user denied it. It did not run. Do not run it again or another way unless the user asks.",
1897
1901
  timedOut: (minutes) => `Your organization's protection requires a person to approve this command, and nobody answered within ${minutes} minutes. It did not run. Ask the user before trying it again.`,
1898
1902
  stopped: "The request was stopped before the command ran.",
1899
- unavailable: "Your organization's protection requires a person to approve this command, and the approval could not be requested. It did not run. Tell the user; do not try to run it another way."
1903
+ unavailable: "Your organization's protection requires a person to approve this command, and the approval could not be requested. It did not run. Tell the user; do not try to run it another way.",
1904
+ // A file change under tool-approval.
1905
+ editRejected: (path) => `Your organization's protection requires a person to approve every change, and the user denied this change to ${path}. The file was not changed. Do not change it another way unless the user asks.`,
1906
+ editTimedOut: (path, minutes) => `Your organization's protection requires a person to approve every change, and nobody answered about ${path} within ${minutes} minutes. The file was not changed. Ask the user before trying again.`,
1907
+ editStopped: "The request was stopped before the file was changed.",
1908
+ editUnavailable: (path) => `Your organization's protection requires a person to approve every change, and the approval for ${path} could not be requested. The file was not changed. Tell the user; do not change it another way.`
1900
1909
  };
1901
1910
 
1902
1911
  // src/application/services/workspaceSandbox/SessionTransport.ts
@@ -2010,8 +2019,9 @@ var HttpSessionTransport = class {
2010
2019
  async cancelCommandApproval(approvalId, reason) {
2011
2020
  await this.post(`/command-approvals/${encodeURIComponent(approvalId)}/cancel`, { reason }, this.opts.requestTimeoutMs ?? 3e4);
2012
2021
  }
2013
- async refreshRuntime() {
2014
- const raw = await this.post("/runtime", {}, this.opts.requestTimeoutMs ?? 3e4);
2022
+ /** `current`: the model this engine works with now, which the API keeps for the session when the new one is refused. */
2023
+ async refreshRuntime(current) {
2024
+ const raw = await this.post("/runtime", current?.model ? { current: current.model } : {}, this.opts.requestTimeoutMs ?? 3e4);
2015
2025
  const boot = toSessionBootstrap({ session: { model: raw?.model, reasoning: raw?.reasoning }, runtime: raw?.runtime, repository: null });
2016
2026
  return { model: boot.model, runtime: boot.runtime, reasoning: boot.reasoning ?? null };
2017
2027
  }
@@ -2129,7 +2139,13 @@ function toSessionBootstrap(raw) {
2129
2139
  kind,
2130
2140
  teamId: typeof rawScope?.teamId === "string" ? rawScope.teamId : null,
2131
2141
  projectIds: Array.isArray(rawScope?.projectIds) ? rawScope.projectIds.filter((p) => typeof p === "string") : typeof session.projectId === "string" && session.projectId ? [session.projectId] : [],
2132
- repos: repositories
2142
+ repos: repositories,
2143
+ // An area: its name, the chosen initiative's name and the project that measures it (an older API sends none).
2144
+ ...kind === "BUSINESS_AREA" ? {
2145
+ areaName: text3(rawScope?.areaName) || null,
2146
+ initiativeName: text3(rawScope?.initiativeName) || null,
2147
+ initiativeProjectId: text3(rawScope?.initiativeProjectId) || null
2148
+ } : {}
2133
2149
  },
2134
2150
  branch: text3(session.branch, repo2 ? defaultBranch : ""),
2135
2151
  projectId: text3(session.projectId),
@@ -3024,6 +3040,15 @@ var BrowserCommandGate = class {
3024
3040
  this.chain = next.catch(() => void 0);
3025
3041
  return next;
3026
3042
  }
3043
+ checkProtectedEdit(path, opts = {}) {
3044
+ const next = this.chain.then(() => this.ask(`edit ${path}`, {
3045
+ signal: opts.signal,
3046
+ edit: { path, ...opts.preview ? { preview: opts.preview } : {} },
3047
+ protection: { reasons: ["PROTECTION_TOOL_APPROVAL"] }
3048
+ }));
3049
+ this.chain = next.catch(() => void 0);
3050
+ return next;
3051
+ }
3027
3052
  checkEdit(path, opts = {}) {
3028
3053
  const next = this.chain.then(() => this.ask(`edit ${path}`, { signal: opts.signal, edit: { path, ...opts.preview ? { preview: opts.preview } : {} } }));
3029
3054
  this.chain = next.catch(() => void 0);
@@ -3040,7 +3065,7 @@ var BrowserCommandGate = class {
3040
3065
  command,
3041
3066
  cwd: this.o.root,
3042
3067
  ...opts.description ? { description: opts.description.slice(0, 1e3) } : {},
3043
- reasons: opts.edit ? [] : protection ? [...protection.reasons, ...approvalReasons(command)] : approvalReasons(command),
3068
+ reasons: protection ? [...protection.reasons, ...opts.edit ? [] : approvalReasons(command)] : opts.edit ? [] : approvalReasons(command),
3044
3069
  suggestedRule: opts.edit || protection ? null : suggestRule(command),
3045
3070
  ...opts.edit ? { edit: { path: opts.edit.path, ...opts.edit.preview ? { preview: opts.edit.preview.slice(0, 4e3) } : {} } } : {},
3046
3071
  ...protection?.package ? { package: protection.package, ...protection.packageReason ? { packageReason: protection.packageReason } : {} } : {}
@@ -3048,6 +3073,7 @@ var BrowserCommandGate = class {
3048
3073
  } catch (e) {
3049
3074
  this.o.log?.(`command approval could not be requested: ${e.message}`);
3050
3075
  const code = e instanceof CommandApprovalError ? e.code : null;
3076
+ if (protection && opts.edit) return { allow: false, message: PROTECTION_MESSAGES.editUnavailable(opts.edit.path) };
3051
3077
  if (protection && code !== "COMMAND_NOT_ALLOWED") return { allow: false, message: PROTECTION_MESSAGES.unavailable };
3052
3078
  return {
3053
3079
  allow: false,
@@ -3065,10 +3091,12 @@ var BrowserCommandGate = class {
3065
3091
  if (!decision) {
3066
3092
  const reason = opts.signal?.aborted ? "STOPPED" : "TIMED_OUT";
3067
3093
  void this.o.api.cancel(approvalId, reason).catch(() => void 0);
3094
+ if (protection && opts.edit) return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.editStopped : PROTECTION_MESSAGES.editTimedOut(opts.edit.path, Math.round(timeoutMs / 6e4)) };
3068
3095
  if (protection) return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.stopped : PROTECTION_MESSAGES.timedOut(Math.round(timeoutMs / 6e4)) };
3069
3096
  return reason === "STOPPED" ? { allow: false, message: STOPPED } : { allow: false, message: `The user did not answer the approval for this command within ${Math.round(timeoutMs / 6e4)} minutes, so it did not run. Ask the user before trying it again.` };
3070
3097
  }
3071
3098
  if (decision.decision === "REJECT") {
3099
+ if (protection && opts.edit) return { allow: false, message: PROTECTION_MESSAGES.editRejected(opts.edit.path) };
3072
3100
  if (protection) return { allow: false, message: PROTECTION_MESSAGES.rejected };
3073
3101
  return opts.edit ? { allow: false, message: `The user declined this change to ${opts.edit.path} in the browser. The file was not changed. Ask the user what to do differently.` } : { allow: false, message: "The user denied this command in the browser. It did not run. Do not run the same command again unless the user asks; continue another way or ask the user." };
3074
3102
  }
@@ -4814,6 +4842,8 @@ function sandboxTestEnv() {
4814
4842
  "BUNDLE_PATH",
4815
4843
  "PUB_CACHE",
4816
4844
  "FLUTTER_ROOT",
4845
+ "MIX_HOME",
4846
+ "HEX_HOME",
4817
4847
  "MISE_DATA_DIR",
4818
4848
  "MISE_CACHE_DIR",
4819
4849
  "ELIXIR_ERL_OPTIONS"
@@ -4911,6 +4941,9 @@ var ENGINE_MESSAGES = {
4911
4941
  MODEL_SWITCH_FAILED: "The model could not be changed now; this request uses {model}.",
4912
4942
  MODEL_SWITCHED: "Now using {model}.",
4913
4943
  GATEWAY_REFUSED: "The AI gateway refused this request ({code}).",
4944
+ // The AI Workspace's own credit (gateway BUDGET codes): said as credit, not as a budget the organization set.
4945
+ MANAGED_ALLOWANCE_EXHAUSTED: "The AI Workspace credit ran out ({code}).",
4946
+ MANAGED_PREPAID_REQUIRED: "This model uses purchased credit, and the purchased credit ran out ({code}).",
4914
4947
  // Files, rewind and hunks.
4915
4948
  DISCARD_FAILED: "The change to that file could not be discarded.",
4916
4949
  DISCARD_REPOSITORY_REQUIRED: "Say which repository the file belongs to.",
@@ -4933,6 +4966,8 @@ var ENGINE_MESSAGES = {
4933
4966
  // Pull requests.
4934
4967
  BASE_ADVANCED: "The pull request could not be opened: the base branch in the repository is not at the commit this change was made on.",
4935
4968
  PR_FAILED: "The pull request could not be opened.",
4969
+ PROVIDER_CONNECTION_REQUIRED: "The pull request could not be opened: the provider connection of {repoFullName} needs to be reconnected in ScaleQuality.",
4970
+ REPOSITORY_NOT_IN_PROJECT: "The pull request could not be opened: {repoFullName} is no longer a repository of this project.",
4936
4971
  // Attachments and mentions.
4937
4972
  ATTACHMENT_SKIPPED: "An attached image could not be read and was left out.",
4938
4973
  MENTION_SKIPPED: "{path} could not be attached ({reason}).",
@@ -4958,10 +4993,66 @@ function engineMessage(code, params) {
4958
4993
  return text3.replace(/\{(\w+)\}/g, (_m, k) => k === "code" && !(params && k in params) ? code : params && params[k] !== void 0 && params[k] !== null ? String(params[k]) : "");
4959
4994
  }
4960
4995
  var SQ_CODE = /"scalequality_code"\s*:\s*"([A-Z][A-Z0-9_]{2,79})"/;
4996
+ var GATEWAY_CODE_CATEGORIES = Object.freeze({
4997
+ ...Object.fromEntries([
4998
+ "BUDGET_EXHAUSTED",
4999
+ "ORGANIZATION_BUDGET_EXHAUSTED",
5000
+ "ENVIRONMENT_BUDGET_EXHAUSTED",
5001
+ "TEAM_BUDGET_EXHAUSTED",
5002
+ "BUSINESS_UNIT_BUDGET_EXHAUSTED",
5003
+ "AREA_BUDGET_EXHAUSTED",
5004
+ "MODEL_BUDGET_EXHAUSTED",
5005
+ "MANAGED_ALLOWANCE_EXHAUSTED",
5006
+ "MANAGED_PREPAID_REQUIRED"
5007
+ ].map((c) => [c, "BUDGET"])),
5008
+ PERSON_BUDGET_EXHAUSTED: "PERSON_BUDGET",
5009
+ CONCURRENCY_LIMIT: "CONCURRENCY",
5010
+ ORGANIZATION_CONCURRENCY_LIMIT: "CONCURRENCY",
5011
+ ...Object.fromEntries([
5012
+ "INPUT_POLICY_BLOCK",
5013
+ "OUTPUT_POLICY_BLOCK",
5014
+ "SEMANTIC_INPUT_BLOCK",
5015
+ "SEMANTIC_OUTPUT_BLOCK",
5016
+ "TOOL_OUTPUT_POLICY_BLOCK",
5017
+ "TOOL_OUTPUT_NOT_ALLOWED",
5018
+ "TOOL_OUTPUT_INVALID",
5019
+ "PROTECTION_PROMPT_ATTACK",
5020
+ "PROTECTION_INDIRECT_INJECTION",
5021
+ "PROTECTION_HARMFUL_CONTENT",
5022
+ "PROTECTION_PROHIBITED_PRACTICE",
5023
+ "PROTECTION_SENSITIVE_INFERENCE",
5024
+ "PROTECTION_COMPANY_TOPIC",
5025
+ "PROTECTION_UNGROUNDED_ANSWER"
5026
+ ].map((c) => [c, "POLICY"])),
5027
+ ...Object.fromEntries([
5028
+ "MODEL_ALIAS_NOT_ALLOWED",
5029
+ "NO_PUBLISHED_POLICY",
5030
+ "MODEL_DISABLED_BY_ORGANIZATION",
5031
+ "MODEL_EXCLUDED_FROM_AUTO",
5032
+ "MANAGED_MODEL_PLAN_RESTRICTED",
5033
+ "MANAGED_ROUTE_NOT_ALLOWED",
5034
+ "MANAGED_CODE_PROFILE_REQUIRED",
5035
+ "CONNECTION_NOT_AVAILABLE"
5036
+ ].map((c) => [c, "MODEL_ACCESS"])),
5037
+ ...Object.fromEntries([
5038
+ "MANAGED_NOT_AVAILABLE",
5039
+ "WORKSPACE_ENTITLEMENT_REQUIRED",
5040
+ "MANAGED_ENTITLEMENT_REQUIRED",
5041
+ "TRIAL_UPGRADE_REQUIRED",
5042
+ "WORKSPACE_ACTIVE_PEOPLE_LIMIT"
5043
+ ].map((c) => [c, "ENTITLEMENT"])),
5044
+ PROVIDER_RATE_LIMITED: "PROVIDER"
5045
+ });
5046
+ var BARE_CODE = /API Error:\s*(\d{3})\s+([A-Z][A-Z0-9_]{2,79})(?![A-Za-z0-9_])/;
5047
+ function bareGatewayError(text3) {
5048
+ const m = BARE_CODE.exec(text3);
5049
+ const category = m ? GATEWAY_CODE_CATEGORIES[m[2]] : void 0;
5050
+ return m && category ? { code: m[2], params: { status: Number(m[1]), category } } : null;
5051
+ }
4961
5052
  function gatewayError(text3) {
4962
5053
  if (!text3) return null;
4963
5054
  const m = SQ_CODE.exec(text3);
4964
- if (!m) return null;
5055
+ if (!m) return bareGatewayError(text3);
4965
5056
  const params = {};
4966
5057
  const status = /API Error:\s*(\d{3})/.exec(text3);
4967
5058
  if (status) params.status = Number(status[1]);
@@ -9934,12 +10025,16 @@ var TurnTiming = class {
9934
10025
  // src/application/services/workspaceSandbox/protectionApproval.ts
9935
10026
  function gateProtectionApprover(gate) {
9936
10027
  if (!gate.checkProtected) return null;
9937
- return { ask: (command, check) => gate.checkProtected(command, check) };
10028
+ return {
10029
+ ask: (command, check) => gate.checkProtected(command, check),
10030
+ ...gate.checkProtectedEdit ? { askEdit: (path, check) => gate.checkProtectedEdit(path, check) } : {}
10031
+ };
9938
10032
  }
9939
10033
  function cloudProtectionApprover(o) {
9940
10034
  let chain = Promise.resolve();
9941
10035
  const ask = async (command, check) => {
9942
- if (check.signal?.aborted) return { allow: false, message: PROTECTION_MESSAGES.stopped };
10036
+ const edit = check.edit;
10037
+ if (check.signal?.aborted) return { allow: false, message: edit ? PROTECTION_MESSAGES.editStopped : PROTECTION_MESSAGES.stopped };
9943
10038
  let approvalId;
9944
10039
  try {
9945
10040
  ({ approvalId } = await o.calls.requestCommandApproval({
@@ -9948,11 +10043,12 @@ function cloudProtectionApprover(o) {
9948
10043
  ...check.description ? { description: check.description.slice(0, 1e3) } : {},
9949
10044
  reasons: [...check.reasons],
9950
10045
  suggestedRule: null,
10046
+ ...edit ? { edit: { path: edit.path, ...edit.preview ? { preview: edit.preview.slice(0, 4e3) } : {} } } : {},
9951
10047
  ...check.package ? { package: check.package, ...check.packageReason ? { packageReason: check.packageReason } : {} } : {}
9952
10048
  }));
9953
10049
  } catch (e) {
9954
10050
  o.log?.(`protection approval could not be requested: ${e.message}`);
9955
- return { allow: false, message: PROTECTION_MESSAGES.unavailable };
10051
+ return { allow: false, message: edit ? PROTECTION_MESSAGES.editUnavailable(edit.path) : PROTECTION_MESSAGES.unavailable };
9956
10052
  }
9957
10053
  const timeoutMs = o.timeoutMs ?? COMMAND_APPROVAL_TIMEOUT_MS;
9958
10054
  const timer = new AbortController();
@@ -9963,16 +10059,21 @@ function cloudProtectionApprover(o) {
9963
10059
  if (!decision) {
9964
10060
  const reason = check.signal?.aborted ? "STOPPED" : "TIMED_OUT";
9965
10061
  void o.calls.cancelCommandApproval(approvalId, reason).catch(() => void 0);
9966
- return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.stopped : PROTECTION_MESSAGES.timedOut(Math.round(timeoutMs / 6e4)) };
10062
+ const minutes = Math.round(timeoutMs / 6e4);
10063
+ if (edit) return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.editStopped : PROTECTION_MESSAGES.editTimedOut(edit.path, minutes) };
10064
+ return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.stopped : PROTECTION_MESSAGES.timedOut(minutes) };
9967
10065
  }
9968
- return decision.decision === "APPROVE" ? { allow: true } : { allow: false, message: PROTECTION_MESSAGES.rejected };
10066
+ if (decision.decision === "APPROVE") return { allow: true };
10067
+ return { allow: false, message: edit ? PROTECTION_MESSAGES.editRejected(edit.path) : PROTECTION_MESSAGES.rejected };
10068
+ };
10069
+ const queued = (run2) => {
10070
+ const next = chain.then(run2);
10071
+ chain = next.catch(() => void 0);
10072
+ return next;
9969
10073
  };
9970
10074
  return {
9971
- ask(command, check) {
9972
- const next = chain.then(() => ask(command, check));
9973
- chain = next.catch(() => void 0);
9974
- return next;
9975
- }
10075
+ ask: (command, check) => queued(() => ask(command, check)),
10076
+ askEdit: (path, check) => queued(() => ask(`edit ${path}`, { reasons: ["PROTECTION_TOOL_APPROVAL"], signal: check.signal, edit: { path, ...check.preview ? { preview: check.preview } : {} } }))
9976
10077
  };
9977
10078
  }
9978
10079
 
@@ -9997,10 +10098,15 @@ function layoutLines(root, l) {
9997
10098
  }
9998
10099
  var LISTED = 30;
9999
10100
  function scopeLine(c) {
10000
- const what = c.scope.kind === "ALL" ? "everything the user can access in the organization" : c.scope.kind === "TEAM" ? "the projects of one team" : c.scope.kind === "BUSINESS_AREA" ? "the projects of one business area" : c.scope.projectIds.length === 1 ? `project ${c.scope.projectIds[0]}` : `${c.scope.projectIds.length} projects`;
10101
+ const what = c.scope.kind === "ALL" ? "everything the user can access in the organization" : c.scope.kind === "TEAM" ? "the projects of one team" : c.scope.kind === "BUSINESS_AREA" ? `the projects of the business area${c.scope.areaName ? ` "${c.scope.areaName}"` : ""} (the ones associated with it and the ones that measure its initiatives)` : c.scope.projectIds.length === 1 ? `project ${c.scope.projectIds[0]}` : `${c.scope.projectIds.length} projects`;
10001
10102
  const n = c.scope.repos.length;
10002
10103
  const names = c.scope.repos.slice(0, LISTED).map((r) => `${r.repoFullName} (${r.provider})`).join(", ");
10003
- return `The session scope is ${what}: ${n === 0 ? "no repository" : `${n} repositor${n === 1 ? "y" : "ies"}: ${names}${n > LISTED ? ", ... (call list_repositories for all)" : ""}`}. ScaleQuality tools only act on projects and repositories of this scope.`;
10104
+ return `The session scope is ${what}: ${n === 0 ? "no repository" : `${n} repositor${n === 1 ? "y" : "ies"}: ${names}${n > LISTED ? ", ... (call list_repositories for all)" : ""}`}. ScaleQuality tools only act on projects and repositories of this scope.${initiativeLine(c)}`;
10105
+ }
10106
+ function initiativeLine(c) {
10107
+ if (c.scope.kind !== "BUSINESS_AREA" || !c.scope.initiativeName) return "";
10108
+ const own = c.scope.initiativeProjectId ? c.scope.repos.filter((r) => r.projectId === c.scope.initiativeProjectId).map((r) => r.repoFullName) : [];
10109
+ return ` The user is working on the initiative "${c.scope.initiativeName}"${own.length ? `; its code is ${own.join(", ")} (the project that measures the initiative). Start there when the user talks about the initiative's code.` : "; no project measures this initiative yet."}`;
10004
10110
  }
10005
10111
  function openLine(c) {
10006
10112
  if (!c.open.length) return "No repository is open yet.";
@@ -11242,11 +11348,15 @@ var WorkspaceEngine = class {
11242
11348
  })) : null;
11243
11349
  if (!repos) return;
11244
11350
  const kind = p.kind === "ALL" || p.kind === "TEAM" || p.kind === "BUSINESS_AREA" ? p.kind : "PROJECTS";
11351
+ const named = (v) => typeof v === "string" && v ? v : null;
11245
11352
  this.scope = {
11246
11353
  kind,
11247
11354
  teamId: typeof p.teamId === "string" ? p.teamId : null,
11248
11355
  projectIds: Array.isArray(p.projectIds) ? p.projectIds.filter((x) => typeof x === "string") : [],
11249
- repos
11356
+ repos,
11357
+ areaName: named(p.areaName),
11358
+ initiativeName: named(p.initiativeName),
11359
+ initiativeProjectId: named(p.initiativeProjectId)
11250
11360
  };
11251
11361
  for (const repo2 of this.repos.values()) {
11252
11362
  if (repo2.localId) {
@@ -11575,7 +11685,7 @@ ${String(item.content)}` : String(item.content);
11575
11685
  if (!model || model === boot.model || !this.deps.transport.refreshRuntime) return;
11576
11686
  const step = this.ownStep("model", "tool", `Switching to ${model}`, "STEP_MODEL_SWITCH", { model });
11577
11687
  try {
11578
- const fresh = await this.deps.transport.refreshRuntime();
11688
+ const fresh = await this.deps.transport.refreshRuntime({ model: boot.model });
11579
11689
  if (!fresh.runtime.token || !fresh.runtime.baseUrl) throw new Error("RUNTIME_INCOMPLETE");
11580
11690
  this.redactor.add(fresh.runtime.token);
11581
11691
  boot.runtime = fresh.runtime;
@@ -11632,10 +11742,11 @@ ${String(item.content)}` : String(item.content);
11632
11742
  async renewCredential() {
11633
11743
  if (!this.deps.transport.refreshRuntime) return;
11634
11744
  try {
11635
- const fresh = await this.deps.transport.refreshRuntime();
11745
+ const fresh = await this.deps.transport.refreshRuntime({ model: this.boot.model });
11636
11746
  if (!fresh.runtime.token || !fresh.runtime.baseUrl) throw new Error("RUNTIME_INCOMPLETE");
11637
11747
  this.redactor.add(fresh.runtime.token);
11638
11748
  this.boot.runtime = fresh.runtime;
11749
+ if (fresh.model) this.boot.model = fresh.model;
11639
11750
  this.reasoningCapability = parseReasoningCapability(fresh.runtime.reasoning ?? null);
11640
11751
  this.reasoningLevel = effectiveReasoning(fresh.reasoning ?? this.reasoningLevel, this.reasoningCapability);
11641
11752
  } catch (e) {
@@ -13074,16 +13185,33 @@ _Measured on an earlier version of this change._`);
13074
13185
  if (e instanceof TransportError && e.code === REPOSITORY_NOT_IN_SCOPE) {
13075
13186
  return text(`${REPOSITORY_NOT_IN_SCOPE}: ${target} left this session's scope before the pull request was opened. It was not opened.`, true);
13076
13187
  }
13077
- if (status === 409) {
13078
- const where = this.local ? " Update this folder to the latest commit of the base branch (git pull), then ask again." : "";
13079
- this.error("BASE_ADVANCED", { repoFullName: target, local: this.local });
13080
- return text(`The pull request could not be opened: the base branch in the repository is not at the commit this change was made on.${where} Tell the user; do not retry without being asked.`, true);
13081
- }
13082
- this.error("PR_FAILED", { repoFullName: target });
13083
- return text("The pull request could not be opened. Tell the user; do not retry without being asked.", true);
13188
+ const failure = pullRequestFailure(status, e instanceof TransportError ? e.code : void 0, target, this.local);
13189
+ this.error(failure.code, failure.params);
13190
+ return text(`${failure.say} Tell the user; do not retry without being asked.`, true);
13084
13191
  }
13085
13192
  }
13086
13193
  };
13194
+ function pullRequestFailure(status, code, repoFullName, local) {
13195
+ if (status === 409 && code === "PROVIDER_CONNECTION_REQUIRED") return {
13196
+ code: "PROVIDER_CONNECTION_REQUIRED",
13197
+ params: { repoFullName },
13198
+ say: `The pull request could not be opened: the provider connection of ${repoFullName} needs to be reconnected in ScaleQuality.`
13199
+ };
13200
+ if (status === 409 && code === "REPOSITORY_NOT_IN_PROJECT") return {
13201
+ code: "REPOSITORY_NOT_IN_PROJECT",
13202
+ params: { repoFullName },
13203
+ say: `The pull request could not be opened: ${repoFullName} is no longer a repository of this project.`
13204
+ };
13205
+ if (status === 409 && (!code || code === "BASE_ADVANCED")) {
13206
+ const where = local ? " Update this folder to the latest commit of the base branch (git pull), then ask again." : "";
13207
+ return {
13208
+ code: "BASE_ADVANCED",
13209
+ params: { repoFullName, local },
13210
+ say: `The pull request could not be opened: the base branch in the repository is not at the commit this change was made on.${where}`
13211
+ };
13212
+ }
13213
+ return { code: "PR_FAILED", params: { repoFullName, ...code ? { reason: code } : {} }, say: "The pull request could not be opened." };
13214
+ }
13087
13215
  var LOCAL_ENV_PASSTHROUGH = [
13088
13216
  "LOGNAME",
13089
13217
  "COLORTERM",
@@ -13236,11 +13364,11 @@ function buildQueryOptions(o) {
13236
13364
  }
13237
13365
  const protections = o.protections?.() ?? NO_PROTECTIONS;
13238
13366
  const d = await decideToolUse(name, input.tool_input ?? {}, { ...o.policy, protections });
13239
- const guarded = d.behavior === "allow" && name === "Bash" && mayNeedProtection(String(d.updatedInput.command ?? ""), protections);
13367
+ const guarded = d.behavior === "allow" && (name === "Bash" && mayNeedProtection(String(d.updatedInput.command ?? ""), protections) || protections.toolApproval && !o.plan && name in WRITE_TOOL_PATHS);
13240
13368
  const gated = guarded || d.behavior === "allow" && !!gate && (name === "Bash" || !!gate.checkEdit && !o.plan && name in WRITE_TOOL_PATHS);
13241
13369
  if (d.behavior === "allow" && !gated) await beforeWrite(name, d.updatedInput);
13242
13370
  if (gated) {
13243
- return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: guarded ? "This command follows the organization's protections." : "Commands and changes on this machine follow the person's permission mode." } };
13371
+ return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: guarded ? "This action follows the organization's protections." : "Commands and changes on this machine follow the person's permission mode." } };
13244
13372
  }
13245
13373
  return d.behavior === "allow" ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: d.updatedInput } } : { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: d.message } };
13246
13374
  };
@@ -13278,10 +13406,18 @@ function buildQueryOptions(o) {
13278
13406
  protectionApproved = true;
13279
13407
  }
13280
13408
  }
13281
- if (gate?.checkEdit && !o.plan && toolName in WRITE_TOOL_PATHS) {
13409
+ const changes = !o.plan && toolName in WRITE_TOOL_PATHS;
13410
+ const changePath = () => {
13282
13411
  const field = WRITE_TOOL_PATHS[toolName];
13283
- const path = typeof d.updatedInput[field] === "string" ? (0, import_path15.relative)(o.root, d.updatedInput[field]) || String(d.updatedInput[field]) : "";
13284
- const r = await gate.checkEdit(path, { preview: editPreview(toolName, d.updatedInput), signal: opts?.signal ?? o.abortController.signal });
13412
+ return typeof d.updatedInput[field] === "string" ? (0, import_path15.relative)(o.root, d.updatedInput[field]) || String(d.updatedInput[field]) : "";
13413
+ };
13414
+ if (changes && protections.toolApproval) {
13415
+ const path = changePath(), askEdit = o.protection?.approver?.askEdit;
13416
+ if (!askEdit) return { behavior: "deny", message: PROTECTION_MESSAGES.editUnavailable(path) };
13417
+ const r = await askEdit(path, { preview: editPreview(toolName, d.updatedInput), signal: opts?.signal ?? o.abortController.signal });
13418
+ if (!r.allow) return { behavior: "deny", message: r.message };
13419
+ } else if (gate?.checkEdit && changes) {
13420
+ const r = await gate.checkEdit(changePath(), { preview: editPreview(toolName, d.updatedInput), signal: opts?.signal ?? o.abortController.signal });
13285
13421
  if (!r.allow) return { behavior: "deny", message: r.message };
13286
13422
  }
13287
13423
  await beforeWrite(toolName, d.updatedInput);
@@ -13344,7 +13480,8 @@ function timedGate(gate, took) {
13344
13480
  return {
13345
13481
  check: (command, opts) => timed(() => gate.check(command, opts)),
13346
13482
  ...gate.checkEdit ? { checkEdit: (path, opts) => timed(() => gate.checkEdit(path, opts)) } : {},
13347
- ...gate.checkProtected ? { checkProtected: (command, opts) => timed(() => gate.checkProtected(command, opts)) } : {}
13483
+ ...gate.checkProtected ? { checkProtected: (command, opts) => timed(() => gate.checkProtected(command, opts)) } : {},
13484
+ ...gate.checkProtectedEdit ? { checkProtectedEdit: (path, opts) => timed(() => gate.checkProtectedEdit(path, opts)) } : {}
13348
13485
  };
13349
13486
  }
13350
13487
  async function hasLocalTranscript(configDir, sessionId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalequality/cli",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "description": "ScaleQuality CLI. Connect your computer to the ScaleQuality AI Workspace (`scalequality login`), run its coding engine in any folder of your computer, and import your Claude Code and Codex conversations.",
5
5
  "type": "module",
6
6
  "bin": {