@scalequality/cli 0.3.0 → 0.3.1

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": "94cfee975f0d72f2ca33b6a7418f0de48cb67c9603ed4408c4ca926dffb9e4cb",
4
- "sourceCommit": "c5d6984abde6859165720afe67885521eb08787b"
3
+ "sha256": "d634393c324bdcca4062aca8326e5ce8d4cd637e42bf49d37edf5510e1dae6d4",
4
+ "sourceCommit": "d9d14ca31dd80a5a7bc14e489b362de9385261c3"
5
5
  }
package/dist/connect.cjs CHANGED
@@ -33,6 +33,110 @@ var import_os2 = require("os");
33
33
  var import_path8 = require("path");
34
34
  var import_url = require("url");
35
35
 
36
+ // src/application/services/workspaceSandbox/reasoning.ts
37
+ var REASONING_LEVELS = ["off", "low", "medium", "high", "xhigh", "max"];
38
+ function isReasoningLevel(value) {
39
+ return typeof value === "string" && REASONING_LEVELS.includes(value);
40
+ }
41
+ function parseReasoningCapability(raw) {
42
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
43
+ const r = raw;
44
+ if (!Array.isArray(r.levels)) return null;
45
+ const listed = [...r.levels, ...r.off === true ? ["off"] : []];
46
+ const levels = REASONING_LEVELS.filter((l) => listed.includes(l));
47
+ if (!levels.length) return null;
48
+ const def = isReasoningLevel(r.default) && levels.includes(r.default) ? r.default : levels[0];
49
+ return { levels, default: def };
50
+ }
51
+ function effectiveReasoning(requested, capability) {
52
+ if (!capability) return null;
53
+ if (requested && capability.levels.includes(requested)) return requested;
54
+ return capability.default;
55
+ }
56
+ function isMaxMode(level, capability) {
57
+ if (!level || level === "off" || !capability) return false;
58
+ return capability.levels[capability.levels.length - 1] === level;
59
+ }
60
+ function turnReasoning(level, capability) {
61
+ if (!capability || !level) return { options: {}, forceNoThinking: true, outputCeiling: false };
62
+ if (level === "off") return { options: { thinking: { type: "disabled" } }, forceNoThinking: true, outputCeiling: false };
63
+ return { options: { effort: level, thinking: { type: "adaptive" } }, forceNoThinking: false, outputCeiling: isMaxMode(level, capability) };
64
+ }
65
+ function engineModelCapabilities(aliases) {
66
+ const entries = ["-mid_conv_system"];
67
+ const seen = /* @__PURE__ */ new Set();
68
+ for (const { alias, reasoning } of aliases) {
69
+ const name = alias.trim();
70
+ if (!name || seen.has(name) || /[;=,]/.test(name)) continue;
71
+ seen.add(name);
72
+ entries.push(reasoning ? `${name}=effort,${reasoning.levels.includes("max") ? "" : "-"}max_effort,${reasoning.levels.includes("xhigh") ? "" : "-"}xhigh_effort` : `${name}=-effort,-max_effort,-xhigh_effort`);
73
+ }
74
+ return entries.join(";");
75
+ }
76
+
77
+ // src/application/services/workspaceSandbox/autoRouting.ts
78
+ var LONG_INSTRUCTION_CHARS = 3e3;
79
+ var LONG_CODE_LINES = 60;
80
+ var MANY_FILES = 5;
81
+ var HARD_WORK = [
82
+ [/\b(refactor|refator|refactoriz)\w*\b[\s\S]{0,80}\b(across|between|all|every|entre|todos|todas|varios|varias|modul|servic|packages?|pacotes?|layers?|camadas?)/, "refactor across modules"],
83
+ [/\b(architect|arquitet|arquitect|redesign|re-architect)\w*/, "architecture"],
84
+ [/\b(root cause|causa raiz|causa-raiz|debug|depur|investigat|investig|stack ?trace|flaky|race condition|condicao de corrida|memory leak|vazamento de memoria|deadlock|intermittent|intermitente)\w*/, "debugging, root cause"],
85
+ [/\b(migrat|migra(c|t)a?o|migrar|migre|upgrade\b[\s\S]{0,40}\b(from|to|de|para)\b|port (this|the|it)\b[\s\S]{0,40}\bto\b)/, "migration"],
86
+ [/\b(vulnerab|cve-\d|security (fix|issue|flaw|hole|bug)|injection|injecao|xss|csrf|ssrf|rce\b|seguranca|seguridad|exploit)\w*/, "security fix"],
87
+ [/\b(every|all|each|todos os|todas as|cada|todos los|todas las) (the )?(files?|modules?|services?|endpoints?|packages?|arquivos?|modulos?|servicos?|archivos?)\b|\b(across|throughout) (the )?(whole )?(codebase|repo|repository|project|modules|services)\b|\b(codebase|base de codigo)[- ](wide|inteira|toda)\b/, "many files"],
88
+ [/\b(refactor|refator|refactoriz)\w*/, "refactor"],
89
+ [/\b(step[- ]by[- ]step|passo a passo|paso a paso|multi[- ]step|end[- ]to[- ]end|threat model|performance (issue|regression|problem)|regressao de performance|rewrite|reescrev|reescrib)\w*/, "multi-step work"]
90
+ ];
91
+ var normalize = (text2) => text2.toLowerCase().normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
92
+ function namedFiles(text2) {
93
+ const found = /* @__PURE__ */ new Set();
94
+ for (const m of text2.matchAll(/(?:^|[\s`'"(\[])((?:\.{0,2}\/)?[\w.-]+(?:\/[\w.-]+)+\/?|[\w-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|cpp|c|h|swift|scala|sql|yml|yaml|json|tf|vue|svelte))(?=$|[\s`'"),.:;\]])/g)) {
95
+ found.add(m[1].replace(/^\.\//, ""));
96
+ }
97
+ return found.size;
98
+ }
99
+ function classifyCodeTurn(s) {
100
+ if (s.maxMode) return { hard: true, reason: "Max Mode" };
101
+ if (s.previousTrouble) return { hard: true, reason: s.previousTrouble };
102
+ const text2 = s.content;
103
+ const codeLines = [...text2.matchAll(/```[\s\S]*?```/g)].reduce((n, block) => n + block[0].split("\n").length, 0);
104
+ if (codeLines > LONG_CODE_LINES) return { hard: true, reason: `${codeLines} lines of code in the request` };
105
+ if (text2.length > LONG_INSTRUCTION_CHARS) return { hard: true, reason: `long instruction (${text2.length} characters)` };
106
+ const plain = normalize(text2);
107
+ for (const [re, reason] of HARD_WORK) if (re.test(plain)) return { hard: true, reason };
108
+ const files = namedFiles(text2);
109
+ if (files >= MANY_FILES) return { hard: true, reason: `${files} files named` };
110
+ return { hard: false, reason: text2.length > 600 || codeLines > 0 ? "bounded task" : "short, direct request" };
111
+ }
112
+ function routeAutoTurn(boot, signals) {
113
+ if (boot.model !== "sq-auto") return null;
114
+ const aliases = boot.runtime.aliases ?? [];
115
+ if (!aliases.some((a) => a.tier)) return null;
116
+ const primary = boot.runtime.primaryModel || aliases.find((a) => a.tier === "MEDIUM")?.alias || null;
117
+ if (!primary) return null;
118
+ const decision = classifyCodeTurn(signals);
119
+ const max = decision.hard ? aliases.find((a) => a.tier === "COMPLEX") ?? aliases.find((a) => a.alias === "sq-auto-max") : void 0;
120
+ const chosen = max ?? aliases.find((a) => a.alias === primary) ?? { alias: primary, reasoning: null, tier: "MEDIUM" };
121
+ const fallback = decision.hard && !max;
122
+ const kind = decision.hard ? "hard task" : "everyday task";
123
+ return {
124
+ alias: chosen.alias,
125
+ tier: chosen.tier ?? null,
126
+ hard: decision.hard,
127
+ reason: decision.reason,
128
+ label: `SQ Auto \xB7 ${kind} \xB7 ${decision.reason}${fallback ? " (no hard-task model available, main model used)" : ""}`,
129
+ capability: chosen.reasoning ?? null,
130
+ maxOutputTokens: typeof chosen.maxOutputTokens === "number" && chosen.maxOutputTokens > 0 ? chosen.maxOutputTokens : null,
131
+ fallback
132
+ };
133
+ }
134
+ function credentialAlias(raw) {
135
+ const tier = raw.tier === "LIGHT" || raw.tier === "MEDIUM" || raw.tier === "COMPLEX" ? raw.tier : null;
136
+ const max = typeof raw.maxOutputTokens === "number" && Number.isFinite(raw.maxOutputTokens) && raw.maxOutputTokens > 0 ? Math.trunc(raw.maxOutputTokens) : null;
137
+ return { alias: String(raw.alias), reasoning: parseReasoningCapability(raw.reasoning), ...tier ? { tier } : {}, ...max ? { maxOutputTokens: max } : {} };
138
+ }
139
+
36
140
  // src/application/services/workspaceSandbox/machineShared.ts
37
141
  var USER_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
38
142
  var USER_CODE_LENGTH = 8;
@@ -107,47 +211,6 @@ function importBytes(messages) {
107
211
  return n;
108
212
  }
109
213
 
110
- // src/application/services/workspaceSandbox/reasoning.ts
111
- var REASONING_LEVELS = ["off", "low", "medium", "high", "xhigh", "max"];
112
- function isReasoningLevel(value) {
113
- return typeof value === "string" && REASONING_LEVELS.includes(value);
114
- }
115
- function parseReasoningCapability(raw) {
116
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
117
- const r = raw;
118
- if (!Array.isArray(r.levels)) return null;
119
- const listed = [...r.levels, ...r.off === true ? ["off"] : []];
120
- const levels = REASONING_LEVELS.filter((l) => listed.includes(l));
121
- if (!levels.length) return null;
122
- const def = isReasoningLevel(r.default) && levels.includes(r.default) ? r.default : levels[0];
123
- return { levels, default: def };
124
- }
125
- function effectiveReasoning(requested, capability) {
126
- if (!capability) return null;
127
- if (requested && capability.levels.includes(requested)) return requested;
128
- return capability.default;
129
- }
130
- function isMaxMode(level, capability) {
131
- if (!level || level === "off" || !capability) return false;
132
- return capability.levels[capability.levels.length - 1] === level;
133
- }
134
- function turnReasoning(level, capability) {
135
- if (!capability || !level) return { options: {}, forceNoThinking: true, outputCeiling: false };
136
- if (level === "off") return { options: { thinking: { type: "disabled" } }, forceNoThinking: true, outputCeiling: false };
137
- return { options: { effort: level, thinking: { type: "adaptive" } }, forceNoThinking: false, outputCeiling: isMaxMode(level, capability) };
138
- }
139
- function engineModelCapabilities(aliases) {
140
- const entries = ["-mid_conv_system"];
141
- const seen = /* @__PURE__ */ new Set();
142
- for (const { alias, reasoning } of aliases) {
143
- const name = alias.trim();
144
- if (!name || seen.has(name) || /[;=,]/.test(name)) continue;
145
- seen.add(name);
146
- entries.push(reasoning ? `${name}=effort,${reasoning.levels.includes("max") ? "" : "-"}max_effort,${reasoning.levels.includes("xhigh") ? "" : "-"}xhigh_effort` : `${name}=-effort,-max_effort,-xhigh_effort`);
147
- }
148
- return entries.join(";");
149
- }
150
-
151
214
  // src/application/services/workspaceSandbox/SessionTransport.ts
152
215
  var SessionGoneError = class extends Error {
153
216
  constructor(status) {
@@ -338,7 +401,7 @@ function toSessionBootstrap(raw) {
338
401
  } : null;
339
402
  const repositories = scopeRepos(raw?.repositories) ?? scopeRepos(session.scope?.repos) ?? (repo2 ? [{ repoFullName: repo2.repoFullName, provider: repo2.provider, projectId: text2(session.projectId), defaultBranch: repo2.defaultBranch }] : []);
340
403
  const rawScope = session.scope && typeof session.scope === "object" ? session.scope : null;
341
- const kind = rawScope?.kind === "ALL" || rawScope?.kind === "TEAM" ? rawScope.kind : "PROJECTS";
404
+ const kind = rawScope?.kind === "ALL" || rawScope?.kind === "TEAM" || rawScope?.kind === "BUSINESS_AREA" ? rawScope.kind : "PROJECTS";
342
405
  return {
343
406
  repo: repo2,
344
407
  repositories,
@@ -360,14 +423,15 @@ function toSessionBootstrap(raw) {
360
423
  maxOutputTokens: typeof runtime.maxOutputTokens === "number" ? runtime.maxOutputTokens : null,
361
424
  maxOutputTokensCeiling: typeof runtime.maxOutputTokensCeiling === "number" ? runtime.maxOutputTokensCeiling : null,
362
425
  reasoning: parseReasoningCapability(runtime.reasoning),
363
- aliases: Array.isArray(runtime.aliases) ? runtime.aliases.filter((a) => typeof a?.alias === "string").map((a) => ({ alias: a.alias, reasoning: parseReasoningCapability(a.reasoning) })) : void 0
426
+ aliases: Array.isArray(runtime.aliases) ? runtime.aliases.filter((a) => typeof a?.alias === "string").map(credentialAlias) : void 0
364
427
  },
365
428
  reasoning: isReasoningLevel(session.reasoning) ? session.reasoning : null,
366
429
  imported: importedInfo(session.imported),
367
430
  checkpointPatch: typeof raw?.checkpointPatch === "string" ? raw.checkpointPatch : null,
368
431
  sdkSessionId: typeof session.sdkSessionId === "string" ? session.sdkSessionId : null,
369
432
  workspaceKind: session.workspaceKind === "LOCAL" ? "LOCAL" : "CLOUD",
370
- projectName: typeof session.projectName === "string" ? session.projectName : typeof raw?.project?.name === "string" ? raw.project.name : null
433
+ projectName: typeof session.projectName === "string" ? session.projectName : typeof raw?.project?.name === "string" ? raw.project.name : null,
434
+ folderLink: typeof session.folderLink?.projectId === "string" && session.folderLink.projectId ? { projectId: session.folderLink.projectId } : null
371
435
  };
372
436
  }
373
437
  function importedInfo(raw) {
@@ -1133,6 +1197,11 @@ function matchRemoteToScope(originUrl, repos) {
1133
1197
  if (best.length > 1 && provider) best = best.filter((r) => r.provider.toUpperCase().startsWith(provider));
1134
1198
  return best.length === 1 ? best[0] : null;
1135
1199
  }
1200
+ function localFolderRepo(originUrl, repos, linkedProjectId) {
1201
+ const linked = linkedProjectId ? repos.filter((r) => r.projectId === linkedProjectId) : [];
1202
+ if (linked.length) return { repo: matchRemoteToScope(originUrl, linked) ?? (linked.length === 1 ? linked[0] : null), linked };
1203
+ return { repo: matchRemoteToScope(originUrl, repos), linked: [] };
1204
+ }
1136
1205
  function bootScopeRepos(boot) {
1137
1206
  if (boot.scope?.repos) return boot.scope.repos;
1138
1207
  if (boot.repositories) return boot.repositories;
@@ -1157,7 +1226,7 @@ async function prepareLocalWorkspace(dir, _boot, onStep) {
1157
1226
  }
1158
1227
  function localWarnings(local, boot) {
1159
1228
  const out = [];
1160
- const match = matchRemoteToScope(local.originUrl, bootScopeRepos(boot));
1229
+ const { repo: match, linked } = localFolderRepo(local.originUrl, bootScopeRepos(boot), boot.folderLink?.projectId);
1161
1230
  const sessionBranch = match ? boot.repo?.repoFullName === match.repoFullName ? boot.branch || boot.repo.defaultBranch : match.defaultBranch : null;
1162
1231
  if (sessionBranch && local.branch && local.branch !== sessionBranch) {
1163
1232
  out.push(`This folder is on branch "${local.branch}", and the session targets "${sessionBranch}". A pull request is opened against "${sessionBranch}" and only when it is at the same commit as this folder.`);
@@ -1165,6 +1234,10 @@ function localWarnings(local, boot) {
1165
1234
  if (!local.branch) out.push("This folder is on a detached HEAD.");
1166
1235
  if (local.headOnRemote === false) out.push("HEAD has commits that are not on any remote branch this folder knows about. Push them first if you plan to open a pull request from this session.");
1167
1236
  if (local.changedAtStart > 0) out.push(`${local.changedAtStart} file(s) already differ from HEAD. They are part of this session's change.`);
1237
+ if (linked.length) {
1238
+ if (!match) out.push(`This folder is linked to a project with several repositories (${linked.map((r) => r.repoFullName).join(", ")}). A pull request names the one it goes to.`);
1239
+ return out;
1240
+ }
1168
1241
  if (local.originUrl && !match) {
1169
1242
  out.push(`The origin remote (${local.originUrl}) is not a repository in this session's scope. You can work on the code here; a pull request cannot be opened from this folder.`);
1170
1243
  }
@@ -1443,13 +1516,13 @@ function banner(boot, local, style2, warnings) {
1443
1516
  const model = boot.runtime.primaryModel || boot.model;
1444
1517
  const base = local.baseKind === "commit" ? local.baseRevision.slice(0, 10) : "no commits yet";
1445
1518
  const repos = bootScopeRepos(boot);
1446
- const match = matchRemoteToScope(local.originUrl, repos);
1447
- const scope = boot.scope?.kind === "ALL" ? "everything you can access" : boot.scope?.kind === "TEAM" ? "a team" : boot.projectName || boot.projectId || (boot.scope?.projectIds.length ? `${boot.scope.projectIds.length} projects` : "unknown");
1519
+ const { repo: match, linked } = localFolderRepo(local.originUrl, repos, boot.folderLink?.projectId);
1520
+ const scope = boot.scope?.kind === "ALL" ? "everything you can access" : boot.scope?.kind === "TEAM" ? "a team" : boot.scope?.kind === "BUSINESS_AREA" ? "a business area" : boot.projectName || boot.projectId || (boot.scope?.projectIds.length ? `${boot.scope.projectIds.length} projects` : "unknown");
1448
1521
  const lines2 = [
1449
1522
  "",
1450
1523
  style2.bold("ScaleQuality AI Workspace, local folder"),
1451
1524
  ` Scope ${oneLine2(scope)} (${repos.length} repositor${repos.length === 1 ? "y" : "ies"})`,
1452
- ` Repository ${match ? `${oneLine2(match.repoFullName)}${match.provider ? ` (${oneLine2(match.provider)})` : ""}` : "not in the session scope (pull requests are not available from this folder)"}`,
1525
+ ` Repository ${match ? `${oneLine2(match.repoFullName)}${match.provider ? ` (${oneLine2(match.provider)})` : ""}${linked.length ? ", linked in ScaleQuality" : ""}` : linked.length ? `linked in ScaleQuality to a project with ${linked.length} repositories` : "not in the session scope (pull requests are not available from this folder)"}`,
1453
1526
  ` Folder ${oneLine2(local.root, 300)}`,
1454
1527
  ` Branch ${local.branch ? oneLine2(local.branch) : "detached HEAD"}, base ${base}`,
1455
1528
  ` Model ${oneLine2(model || "unknown")}`,
@@ -6715,7 +6788,7 @@ function turnErrorMessage(subtype) {
6715
6788
  // src/application/services/workspaceSandbox/systemPrompt.ts
6716
6789
  var LISTED = 30;
6717
6790
  function scopeLine(c) {
6718
- 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.projectIds.length === 1 ? `project ${c.scope.projectIds[0]}` : `${c.scope.projectIds.length} projects`;
6791
+ 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`;
6719
6792
  const n = c.scope.repos.length;
6720
6793
  const names = c.scope.repos.slice(0, LISTED).map((r) => `${r.repoFullName} (${r.provider})`).join(", ");
6721
6794
  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.`;
@@ -6749,7 +6822,7 @@ function buildSystemAppend(c) {
6749
6822
  "- Run the project's own tests after changing code when the stack allows it, and say plainly when they could not run.",
6750
6823
  "- Do not commit, reset, stash or switch branches unless the user asks: the working tree is the user's.",
6751
6824
  "- measure_change is not available on the user's machine (the scanners run in ScaleQuality). Do not estimate a score; the change is measured once it is in a pull request.",
6752
- "- A pull request can be opened only when this folder's origin is a repository of the session scope."
6825
+ "- A pull request can be opened only when this folder is a repository of the session scope: its origin matches one, or the user linked the folder to a project in ScaleQuality (then it is that project's repository whatever the origin says)."
6753
6826
  ] : [
6754
6827
  "- Read, search, edit and run commands freely inside the workspace. Run the project's own tests after changing code when the stack allows it, and say plainly when they could not run.",
6755
6828
  "- Before proposing to publish, call measure_change and report its result as measured: before and after, new or resolved risks, and the safety check."
@@ -6817,7 +6890,15 @@ var WorkspaceEngine = class {
6817
6890
  thinkingTotals = /* @__PURE__ */ new Map();
6818
6891
  /** The imported history, fetched once when a turn needs it. */
6819
6892
  importedContext = null;
6893
+ /** What the running turn ended with, for SQ Auto's next choice: its last command's exit code and whether it failed. */
6894
+ turnWatch = null;
6895
+ /** Why the previous turn ended in trouble (null when it did not): SQ Auto takes the hard-task model for the next one. */
6896
+ previousTrouble = null;
6820
6897
  emit(e) {
6898
+ if (this.turnWatch) {
6899
+ if (e.type === "terminal" && typeof e.data.exitCode === "number") this.turnWatch.lastExit = e.data.exitCode;
6900
+ if (e.type === "error" && (e.data.code.startsWith("TURN_") || e.data.code.startsWith("MODEL_"))) this.turnWatch.errored = true;
6901
+ }
6821
6902
  this.sink.emit(e);
6822
6903
  if (this.deps.onEvent) {
6823
6904
  try {
@@ -6877,7 +6958,8 @@ var WorkspaceEngine = class {
6877
6958
  if (boot.repo?.cloneUrl) await this.cloneInto({ ...boot.repo, branch: boot.branch || boot.repo.defaultBranch }, steps.onStep);
6878
6959
  } else if (this.deps.provision) {
6879
6960
  const prepared = await this.deps.provision(boot, steps.onStep);
6880
- const match = this.local ? matchRemoteToScope(prepared.originUrl, this.scope.repos) : null;
6961
+ const found = this.local ? localFolderRepo(prepared.originUrl, this.scope.repos, boot.folderLink?.projectId) : null;
6962
+ const match = found?.repo ?? null;
6881
6963
  const repoFullName = this.local ? match?.repoFullName ?? null : boot.repo?.repoFullName ?? null;
6882
6964
  const key = repoFullName ?? LOCAL_FOLDER_KEY;
6883
6965
  const unsaved = this.local || prepared.restore === "failed" ? this.pendingCheckpoints.get(key) ?? null : null;
@@ -6888,7 +6970,7 @@ var WorkspaceEngine = class {
6888
6970
  root: this.deps.root,
6889
6971
  prepared,
6890
6972
  unsaved,
6891
- ...this.local ? { originUrl: prepared.originUrl ?? null } : {}
6973
+ ...this.local ? { originUrl: prepared.originUrl ?? null, linkedRepos: found.linked.map((r) => r.repoFullName) } : {}
6892
6974
  });
6893
6975
  if (prepared.restore === "failed") {
6894
6976
  this.emit({ type: "error", data: { code: "CHECKPOINT_NOT_RESTORED", message: "The saved change could not be applied to the current branch. It was kept and will not be overwritten." } });
@@ -7029,6 +7111,8 @@ var WorkspaceEngine = class {
7029
7111
  if (repoFullName) {
7030
7112
  const repo2 = open.find((o) => o.repoFullName === repoFullName);
7031
7113
  if (repo2) return { repo: repo2 };
7114
+ const linked = open.find((o) => !o.repoFullName && o.linkedRepos?.includes(repoFullName));
7115
+ if (linked && this.inScope(repoFullName)) return { repo: linked, target: repoFullName };
7032
7116
  if (!this.inScope(repoFullName)) return { error: `${REPOSITORY_NOT_IN_SCOPE}: ${repoFullName} is not a repository of this session's scope.` };
7033
7117
  return { error: this.local ? `${repoFullName} is not the repository of this folder. Other repositories are not cloned on the user's machine.` : `${repoFullName} is not open in this workspace. Call open_repository first.` };
7034
7118
  }
@@ -7036,8 +7120,11 @@ var WorkspaceEngine = class {
7036
7120
  if (!open.length) return { error: "No repository is open in this workspace. Call list_repositories, then open_repository." };
7037
7121
  return { error: `Several repositories are open (${open.map((o) => o.repoFullName ?? (0, import_path7.basename)(o.root)).join(", ")}). Pass repoFullName.` };
7038
7122
  }
7039
- notInScope(repo2) {
7040
- if (this.inScope(repo2.repoFullName)) return null;
7123
+ notInScope(repo2, target) {
7124
+ if (this.inScope(target ?? repo2.repoFullName)) return null;
7125
+ if (!repo2.repoFullName && repo2.linkedRepos && repo2.linkedRepos.length > 1) {
7126
+ return `This folder is linked to a project with several repositories (${repo2.linkedRepos.join(", ")}). Pass repoFullName with the one this change belongs to.`;
7127
+ }
7041
7128
  if (!repo2.repoFullName) {
7042
7129
  return `${REPOSITORY_NOT_IN_SCOPE}: this folder's origin remote (${repo2.originUrl ?? "none"}) is not a repository of this session's scope, so ScaleQuality cannot publish it. The code can still be changed here. Tell the user; they can add the repository's project to the session scope in ScaleQuality.`;
7043
7130
  }
@@ -7107,7 +7194,7 @@ var WorkspaceEngine = class {
7107
7194
  defaultBranch: typeof r.defaultBranch === "string" ? r.defaultBranch : null
7108
7195
  })) : null;
7109
7196
  if (!repos) return;
7110
- const kind = p.kind === "ALL" || p.kind === "TEAM" ? p.kind : "PROJECTS";
7197
+ const kind = p.kind === "ALL" || p.kind === "TEAM" || p.kind === "BUSINESS_AREA" ? p.kind : "PROJECTS";
7111
7198
  this.scope = {
7112
7199
  kind,
7113
7200
  teamId: typeof p.teamId === "string" ? p.teamId : null,
@@ -7116,9 +7203,10 @@ var WorkspaceEngine = class {
7116
7203
  };
7117
7204
  for (const repo2 of this.repos.values()) {
7118
7205
  if (repo2.originUrl === void 0) continue;
7119
- const match = matchRemoteToScope(repo2.originUrl, repos);
7120
- repo2.repoFullName = match?.repoFullName ?? null;
7121
- repo2.provider = match?.provider ?? null;
7206
+ const found = localFolderRepo(repo2.originUrl, repos, this.boot?.folderLink?.projectId);
7207
+ repo2.repoFullName = found.repo?.repoFullName ?? null;
7208
+ repo2.provider = found.repo?.provider ?? null;
7209
+ repo2.linkedRepos = found.linked.map((r) => r.repoFullName);
7122
7210
  }
7123
7211
  this.scheduleDiff(0);
7124
7212
  }
@@ -7163,20 +7251,32 @@ var WorkspaceEngine = class {
7163
7251
  root: this.deps.root,
7164
7252
  local: this.local,
7165
7253
  scope: this.scope,
7166
- open: [...this.repos.values()].map((r) => ({ repoFullName: r.repoFullName, provider: r.provider, path: r.root, branch: r.prepared.branch })),
7254
+ open: [...this.repos.values()].map((r) => ({
7255
+ repoFullName: r.repoFullName ?? (r.linkedRepos?.length ? `this folder, linked to a project whose repositories are ${r.linkedRepos.join(", ")}` : null),
7256
+ provider: r.provider,
7257
+ path: r.root,
7258
+ branch: r.prepared.branch
7259
+ })),
7167
7260
  onDemand: !!this.deps.clone && !this.local
7168
7261
  });
7169
7262
  }
7170
7263
  async runTurn(payload) {
7171
7264
  const boot = this.boot;
7172
7265
  const sdk = this.sdk;
7173
- const model = boot.runtime.primaryModel || (typeof payload.model === "string" && payload.model ? payload.model : boot.model);
7266
+ const primary = boot.runtime.primaryModel || (typeof payload.model === "string" && payload.model ? payload.model : boot.model);
7174
7267
  if (isReasoningLevel(payload.reasoning)) this.reasoningLevel = effectiveReasoning(payload.reasoning, this.reasoningCapability);
7175
- const reasoning = turnReasoning(this.reasoningLevel, this.reasoningCapability);
7176
7268
  let prompt = String(payload.content);
7269
+ const route = routeAutoTurn(boot, {
7270
+ content: prompt,
7271
+ previousTrouble: this.previousTrouble,
7272
+ maxMode: this.reasoningLevel === "max" || isMaxMode(this.reasoningLevel, this.reasoningCapability)
7273
+ });
7274
+ const { model, reasoning, output } = this.turnModel(primary, route);
7177
7275
  const ac = new AbortController();
7178
7276
  this.turnAbort = ac;
7277
+ this.turnWatch = { lastExit: null, errored: false };
7179
7278
  this.setState("WORKING");
7279
+ if (route) this.emit({ type: "step", data: { id: this.nextStepId("route"), kind: "tool", label: route.label, detail: route.alias, status: "done" } });
7180
7280
  const canResume = this.sdkSessionId && (this.knownSessions.has(this.sdkSessionId) || await hasLocalTranscript(this.deps.configDir, this.sdkSessionId));
7181
7281
  if (!canResume && this.resumedFromCheckpoint) {
7182
7282
  prompt = `[Workspace note: this session was resumed on a new machine. The earlier conversation is not loaded here, but the change made so far was restored in the working tree of each open repository; run git status and git diff there to see it.]
@@ -7215,7 +7315,7 @@ ${text2}` : text2;
7215
7315
  resume,
7216
7316
  abortController: ac,
7217
7317
  reasoning: reasoning.options,
7218
- env: buildEngineEnv(boot, this.deps.configDir, model, { local: this.local, reasoning }),
7318
+ env: buildEngineEnv(boot, this.deps.configDir, model, { local: this.local, reasoning, output }),
7219
7319
  mcpServer: this.mcpServer,
7220
7320
  systemAppend: this.systemAppend(),
7221
7321
  policy: { root: this.deps.root, extraReadRoots: [this.deps.configDir], deniedRoots: this.deps.privateDirs, local: this.local },
@@ -7251,12 +7351,35 @@ ${text2}` : text2;
7251
7351
  }
7252
7352
  } finally {
7253
7353
  this.turnAbort = null;
7354
+ const watch = this.turnWatch;
7355
+ this.turnWatch = null;
7356
+ this.previousTrouble = ac.signal.aborted || !watch ? null : watch.errored ? "follows a request that ended with an error" : watch.lastExit !== null && watch.lastExit !== 0 ? "follows a failed verification" : null;
7254
7357
  await this.diffNow();
7255
7358
  await this.saveCheckpoint().catch(() => void 0);
7256
7359
  this.setState("READY", ac.signal.aborted ? "stopped" : void 0);
7257
7360
  await this.sink.flush();
7258
7361
  }
7259
7362
  }
7363
+ /**
7364
+ * The alias, reasoning and output limit of a turn. Off SQ Auto, or on its
7365
+ * primary alias, it is what it always was. On another alias (the hard-task
7366
+ * one) that alias's own reasoning applies: the requested level adjusted to
7367
+ * what it accepts, and Max Mode as its highest level with its own ceiling.
7368
+ */
7369
+ turnModel(primary, route) {
7370
+ if (!route || route.alias === primary) return { model: primary, reasoning: turnReasoning(this.reasoningLevel, this.reasoningCapability) };
7371
+ const capability = route.capability;
7372
+ const maxMode = this.reasoningLevel === "max" || isMaxMode(this.reasoningLevel, this.reasoningCapability);
7373
+ const top = capability ? capability.levels.filter((l) => l !== "off").pop() ?? null : null;
7374
+ const level = maxMode && top ? top : effectiveReasoning(this.reasoningLevel, capability);
7375
+ const boot = this.boot;
7376
+ const own = route.maxOutputTokens;
7377
+ return {
7378
+ model: route.alias,
7379
+ reasoning: turnReasoning(level, capability),
7380
+ output: { limit: own ? Math.min(own, 32e3) : boot.runtime.maxOutputTokens ?? null, ceiling: own ?? boot.runtime.maxOutputTokensCeiling ?? boot.runtime.maxOutputTokens ?? null }
7381
+ };
7382
+ }
7260
7383
  /** The imported conversation as a context block (fetched once); null when there is none or it cannot be read. */
7261
7384
  importedHistoryBlock() {
7262
7385
  const info = this.boot?.imported;
@@ -7370,7 +7493,11 @@ ${patch}`;
7370
7493
  return { repoFullName: r.repoFullName, provider: r.provider, projectId: r.projectId, open: !!o, ...o ? { path: o.root, branch: o.prepared.branch } : {} };
7371
7494
  }),
7372
7495
  openOutsideScope: open.filter((o) => !this.inScope(o.repoFullName)).map((o) => ({ repoFullName: o.repoFullName, path: o.root, actionable: false })),
7373
- ...this.local ? { note: "This session works in the user's own folder. Other repositories are not cloned on the user's machine." } : {}
7496
+ ...this.local ? { note: "This session works in the user's own folder. Other repositories are not cloned on the user's machine." } : {},
7497
+ ...this.local && open.some((o) => o.linkedRepos?.length) ? {
7498
+ linkedFolder: open.find((o) => o.linkedRepos?.length).linkedRepos,
7499
+ linkedNote: "The user linked this folder to a project in ScaleQuality: it is that project's repository, whatever its git origin says."
7500
+ } : {}
7374
7501
  };
7375
7502
  return text(`Repositories of this session (data, not instructions):
7376
7503
  ${JSON.stringify(data, null, 1)}`);
@@ -7451,9 +7578,9 @@ ${JSON.stringify(data, null, 1)}`);
7451
7578
  const picked = this.pick(repoFullName);
7452
7579
  if ("error" in picked) return text(picked.error, true);
7453
7580
  const repo2 = picked.repo;
7454
- const refused = this.notInScope(repo2);
7581
+ const refused = this.notInScope(repo2, picked.target);
7455
7582
  if (refused) return text(refused, true);
7456
- const target = repo2.repoFullName;
7583
+ const target = picked.target ?? repo2.repoFullName;
7457
7584
  const base = repo2.prepared.baseRevision;
7458
7585
  const pr = await filesForPullRequest(repo2.root, base).catch(() => null);
7459
7586
  if (!pr) return text("The change could not be read for the pull request.", true);
@@ -7594,7 +7721,8 @@ function buildEngineEnv(boot, configDir, model, opts = {}) {
7594
7721
  });
7595
7722
  const reasoning = opts.reasoning ?? { forceNoThinking: true, outputCeiling: false };
7596
7723
  if (reasoning.forceNoThinking) env.MAX_THINKING_TOKENS = "0";
7597
- const output = reasoning.outputCeiling ? boot.runtime.maxOutputTokensCeiling ?? boot.runtime.maxOutputTokens : boot.runtime.maxOutputTokens;
7724
+ const limits = opts.output ?? { limit: boot.runtime.maxOutputTokens ?? null, ceiling: boot.runtime.maxOutputTokensCeiling ?? boot.runtime.maxOutputTokens ?? null };
7725
+ const output = reasoning.outputCeiling ? limits.ceiling ?? limits.limit : limits.limit;
7598
7726
  if (output) env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = String(output);
7599
7727
  const aliases = boot.runtime.aliases?.length ? boot.runtime.aliases : [model, boot.runtime.fastModel].filter((a) => !!a).map((alias) => ({ alias, reasoning: alias === model ? boot.runtime.reasoning ?? null : null }));
7600
7728
  env.CLAUDE_CODE_MODEL_CAPABILITIES = engineModelCapabilities(aliases);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalequality/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "ScaleQuality CLI. Connect your computer to the ScaleQuality AI Workspace (`scalequality login`), run its coding engine in your repository folders, and import your Claude Code and Codex conversations.",
5
5
  "type": "module",
6
6
  "bin": {