@scalequality/cli 0.4.8 → 0.4.9-rc.2

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": "2cc2b586649f475725d306b032e821c01721bcd6bfa560f8423ad9de3e189050",
4
- "sourceCommit": "b252300744f0d3132910cba3d700faf23a2c468b"
3
+ "sha256": "f9a1832f530428fd24a48cc17074549aab70faa7ba2f419f3f372321fbe81b0a",
4
+ "sourceCommit": "59a3d8bb1500daa81edb1d977bccf6a459954c5f"
5
5
  }
package/dist/connect.cjs CHANGED
@@ -2602,6 +2602,28 @@ var ConsoleLog = class {
2602
2602
  }
2603
2603
  }
2604
2604
  };
2605
+ function turnTimingLine(ctx) {
2606
+ if (!ctx || ctx.event !== "WORKSPACE_TURN_TIMING") return null;
2607
+ const ms = (k) => typeof ctx[k] === "number" ? ctx[k] : null;
2608
+ const secs = (v) => v >= 1e4 ? `${(v / 1e3).toFixed(1)} s` : `${Math.round(v)} ms`;
2609
+ const parts = [];
2610
+ const first = ms("firstModelEventMs");
2611
+ parts.push(`first model event ${first === null ? "none" : secs(first)}`);
2612
+ const phases = [
2613
+ ms("prepareMs") !== null ? `prepare ${secs(ms("prepareMs"))}` : null,
2614
+ ctx.warmStart === true ? "engine started ahead" : ctx.warmStart === false ? "engine started for it" : null,
2615
+ ms("engineReadyMs") !== null ? `engine ready ${secs(ms("engineReadyMs"))}` : null,
2616
+ ms("checkpointMs") !== null ? `saved point ${secs(ms("checkpointMs"))}` : null,
2617
+ ms("toolWaitMs") !== null ? `tools waited ${secs(ms("toolWaitMs"))}` : null
2618
+ ].filter(Boolean);
2619
+ if (phases.length) parts.push(`(${phases.join(", ")})`);
2620
+ const permissions = ms("permissions");
2621
+ if (permissions) parts.push(`; ${permissions} permission${permissions === 1 ? "" : "s"} ${secs(ms("permissionMs") ?? 0)}`);
2622
+ const total = ms("totalMs");
2623
+ if (total !== null) parts.push(`; total ${secs(total)}`);
2624
+ if (typeof ctx.outcome === "string" && ctx.outcome !== "completed") parts.push(`(${ctx.outcome})`);
2625
+ return `Timing: ${parts.join(" ").replace(/ ;/g, ";")}`;
2626
+ }
2605
2627
  function banner(boot, local, style2, warnings) {
2606
2628
  const model = boot.runtime.primaryModel || boot.model;
2607
2629
  const repos = bootScopeRepos(boot);
@@ -2965,6 +2987,7 @@ function removeStableCopies(home, keep = null) {
2965
2987
  function serviceArgs(env, bin, api) {
2966
2988
  return [env.nodePath, bin, "up", "--api", api, "--service"];
2967
2989
  }
2990
+ var LAUNCHD_PROCESS_TYPE = "Interactive";
2968
2991
  var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2969
2992
  function launchAgentPlist(env, bin, api) {
2970
2993
  const names = serviceNames(api);
@@ -2990,7 +3013,7 @@ ${args}
2990
3013
  <key>ThrottleInterval</key>
2991
3014
  <integer>10</integer>
2992
3015
  <key>ProcessType</key>
2993
- <string>Background</string>
3016
+ <string>${LAUNCHD_PROCESS_TYPE}</string>
2994
3017
  <key>WorkingDirectory</key>
2995
3018
  <string>${xml(env.home)}</string>
2996
3019
  <key>EnvironmentVariables</key>
@@ -3042,18 +3065,29 @@ function writeFileSafely(file, content, mode = 420) {
3042
3065
  (0, import_fs5.renameSync)(tmp, file);
3043
3066
  }
3044
3067
  var failed = (r) => `${r.stderr || r.stdout}`.trim().split("\n").slice(-1)[0]?.slice(0, 200) || `exit ${r.code}`;
3068
+ var LAUNCHD_REPLACE_WAIT_MS = 3e4;
3069
+ var LAUNCHD_POLL_MS = 500;
3070
+ var LAUNCHD_BOOTSTRAP_ATTEMPTS = 6;
3045
3071
  async function installLaunchd(env, bin, api) {
3046
3072
  const names = serviceNames(api);
3047
3073
  const plist = launchAgentPath(env, api);
3048
3074
  (0, import_fs5.mkdirSync)(logDir(env), { recursive: true });
3049
3075
  writeFileSafely(plist, launchAgentPlist(env, bin, api));
3050
3076
  const domain = `gui/${env.uid}`;
3051
- await env.run("launchctl", ["bootout", `${domain}/${names.label}`]);
3052
- await env.run("launchctl", ["enable", `${domain}/${names.label}`]);
3053
- const boot = await env.run("launchctl", ["bootstrap", domain, plist]);
3077
+ const sleep2 = env.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
3078
+ const target = `${domain}/${names.label}`;
3079
+ const loaded = async () => (await env.run("launchctl", ["print", target])).code === 0;
3080
+ await env.run("launchctl", ["bootout", target]);
3081
+ for (let waited = 0; waited < LAUNCHD_REPLACE_WAIT_MS && await loaded(); waited += LAUNCHD_POLL_MS) await sleep2(LAUNCHD_POLL_MS);
3082
+ await env.run("launchctl", ["enable", target]);
3083
+ let boot = await env.run("launchctl", ["bootstrap", domain, plist]);
3084
+ for (let attempt = 1; boot.code !== 0 && attempt < LAUNCHD_BOOTSTRAP_ATTEMPTS; attempt++) {
3085
+ await sleep2(LAUNCHD_POLL_MS);
3086
+ boot = await env.run("launchctl", ["bootstrap", domain, plist]);
3087
+ }
3054
3088
  if (boot.code === 0) return { ok: true, manager: "launchd", hints: [] };
3055
3089
  const legacy = await env.run("launchctl", ["load", "-w", plist]);
3056
- if (legacy.code === 0) return { ok: true, manager: "launchd", hints: [] };
3090
+ if (legacy.code === 0 && await loaded()) return { ok: true, manager: "launchd", hints: [] };
3057
3091
  (0, import_fs5.rmSync)(plist, { force: true });
3058
3092
  return { ok: false, code: "LAUNCHD_REFUSED", message: `macOS did not accept the background service (${failed(boot)}). A device management profile may block login items.` };
3059
3093
  }
@@ -4411,7 +4445,7 @@ function limitInEngineText(text3) {
4411
4445
  // src/application/services/workspaceSandbox/scalequalityTools.ts
4412
4446
  var import_crypto4 = require("crypto");
4413
4447
 
4414
- // ../aigov-code-queue/node_modules/zod/v3/external.js
4448
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/external.js
4415
4449
  var external_exports = {};
4416
4450
  __export(external_exports, {
4417
4451
  BRAND: () => BRAND,
@@ -4523,7 +4557,7 @@ __export(external_exports, {
4523
4557
  void: () => voidType
4524
4558
  });
4525
4559
 
4526
- // ../aigov-code-queue/node_modules/zod/v3/helpers/util.js
4560
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/helpers/util.js
4527
4561
  var util;
4528
4562
  (function(util2) {
4529
4563
  util2.assertEqual = (_) => {
@@ -4657,7 +4691,7 @@ var getParsedType = (data) => {
4657
4691
  }
4658
4692
  };
4659
4693
 
4660
- // ../aigov-code-queue/node_modules/zod/v3/ZodError.js
4694
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/ZodError.js
4661
4695
  var ZodIssueCode = util.arrayToEnum([
4662
4696
  "invalid_type",
4663
4697
  "invalid_literal",
@@ -4775,7 +4809,7 @@ ZodError.create = (issues) => {
4775
4809
  return error;
4776
4810
  };
4777
4811
 
4778
- // ../aigov-code-queue/node_modules/zod/v3/locales/en.js
4812
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/locales/en.js
4779
4813
  var errorMap = (issue, _ctx) => {
4780
4814
  let message;
4781
4815
  switch (issue.code) {
@@ -4878,7 +4912,7 @@ var errorMap = (issue, _ctx) => {
4878
4912
  };
4879
4913
  var en_default = errorMap;
4880
4914
 
4881
- // ../aigov-code-queue/node_modules/zod/v3/errors.js
4915
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/errors.js
4882
4916
  var overrideErrorMap = en_default;
4883
4917
  function setErrorMap(map) {
4884
4918
  overrideErrorMap = map;
@@ -4887,7 +4921,7 @@ function getErrorMap() {
4887
4921
  return overrideErrorMap;
4888
4922
  }
4889
4923
 
4890
- // ../aigov-code-queue/node_modules/zod/v3/helpers/parseUtil.js
4924
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/helpers/parseUtil.js
4891
4925
  var makeIssue = (params) => {
4892
4926
  const { data, path, errorMaps, issueData } = params;
4893
4927
  const fullPath = [...path, ...issueData.path || []];
@@ -4997,14 +5031,14 @@ var isDirty = (x) => x.status === "dirty";
4997
5031
  var isValid = (x) => x.status === "valid";
4998
5032
  var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
4999
5033
 
5000
- // ../aigov-code-queue/node_modules/zod/v3/helpers/errorUtil.js
5034
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/helpers/errorUtil.js
5001
5035
  var errorUtil;
5002
5036
  (function(errorUtil2) {
5003
5037
  errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
5004
5038
  errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
5005
5039
  })(errorUtil || (errorUtil = {}));
5006
5040
 
5007
- // ../aigov-code-queue/node_modules/zod/v3/types.js
5041
+ // ../../../../../../../Users/erikfernandes/GIT/load-n-scale/.codex-worktrees/aigov-code-queue/node_modules/zod/v3/types.js
5008
5042
  var ParseInputLazyPath = class {
5009
5043
  constructor(parent, value, path, key) {
5010
5044
  this._cachedPath = [];
@@ -9164,6 +9198,28 @@ var TurnTiming = class {
9164
9198
  calls = 0;
9165
9199
  answers = /* @__PURE__ */ new Set();
9166
9200
  done = false;
9201
+ firstFrame = null;
9202
+ phase = {};
9203
+ permissionTotal = 0;
9204
+ permissionCount = 0;
9205
+ toolWaitTotal = 0;
9206
+ /** Durations (ms) and flags of the turn's own phases. */
9207
+ phases(p) {
9208
+ Object.assign(this.phase, p);
9209
+ }
9210
+ /** Any frame from the engine process (the first one says it is up and has the message). */
9211
+ engineFrame() {
9212
+ if (this.firstFrame === null) this.firstFrame = this.now();
9213
+ }
9214
+ /** A command or change permission the engine asked for (the rules, the person's mode or the person answered). */
9215
+ permission(ms) {
9216
+ this.permissionTotal += ms;
9217
+ this.permissionCount += 1;
9218
+ }
9219
+ /** A tool waited for the turn's saved point. */
9220
+ toolWait(ms) {
9221
+ this.toolWaitTotal += ms;
9222
+ }
9167
9223
  /** Any frame of the main conversation that came from the model. */
9168
9224
  modelEvent() {
9169
9225
  if (this.firstModel === null) this.firstModel = this.now();
@@ -9200,6 +9256,10 @@ var TurnTiming = class {
9200
9256
  firstVisibleEventMs: since(this.firstVisible),
9201
9257
  totalMs: end - this.startedAt,
9202
9258
  modelCalls: this.calls,
9259
+ ...this.phase,
9260
+ engineReadyMs: since(this.firstFrame),
9261
+ ...this.permissionCount ? { permissions: this.permissionCount, permissionMs: this.permissionTotal } : {},
9262
+ ...this.toolWaitTotal ? { toolWaitMs: this.toolWaitTotal } : {},
9203
9263
  outcome,
9204
9264
  ...extra
9205
9265
  });
@@ -9836,6 +9896,7 @@ ${lines2.join("\n")}
9836
9896
  }
9837
9897
 
9838
9898
  // src/application/services/workspaceSandbox/WorkspaceEngine.ts
9899
+ var WARM_IDLE_MS = 15 * 6e4;
9839
9900
  var STOP_GRACE_MS = 8e3;
9840
9901
  var INTERRUPT_TIMEOUT_MS = 3e3;
9841
9902
  var MODEL_TOOLS = ["Read", "Write", "Edit", "NotebookEdit", "Glob", "Grep", "Bash", "TaskStop", "TodoWrite", "Agent", "WebFetch", "Skill"];
@@ -10032,6 +10093,14 @@ var WorkspaceEngine = class {
10032
10093
  envTrouble = [];
10033
10094
  /** The organization's approved MCP servers: their catalog and the in-process servers that forward to the API. */
10034
10095
  orgMcp = null;
10096
+ /**
10097
+ * The saved point of the turn the engine runs now: every tool waits for it (the PreToolUse hook and the permission
10098
+ * prompt), so the engine process starts while it is taken and no tool changes a file before it is recorded.
10099
+ */
10100
+ toolGate = Promise.resolve();
10101
+ /** The engine process started for the next turn (warmStart), and the start options it was given (their hash). */
10102
+ warm = null;
10103
+ prewarming = null;
10035
10104
  emit(e) {
10036
10105
  if (e.type === "terminal" && e.data.chunk === void 0 && this.folderBaseline) this.sweepPending = true;
10037
10106
  if (this.turnWatch) {
@@ -10221,6 +10290,7 @@ var WorkspaceEngine = class {
10221
10290
  if (this.resumedFromCheckpoint) await this.diffNow();
10222
10291
  this.phase("READY");
10223
10292
  await this.sink.flush();
10293
+ this.prewarm();
10224
10294
  return true;
10225
10295
  }
10226
10296
  /** Where a background command writes its output: inside the config dir, which Read may read. */
@@ -10556,6 +10626,7 @@ var WorkspaceEngine = class {
10556
10626
  } finally {
10557
10627
  this.turnRunning = null;
10558
10628
  }
10629
+ this.prewarm();
10559
10630
  })();
10560
10631
  }
10561
10632
  /** Waits for the current turn (test and shutdown helper). */
@@ -10839,14 +10910,16 @@ ${String(item.content)}` : String(item.content);
10839
10910
  this.recordCheckpoint(turnId, await this.takeSnapshot());
10840
10911
  }
10841
10912
  async takeSnapshot() {
10913
+ const sdkSessionId = this.sdkSessionId;
10914
+ const resumeAt = this.resumeAt ?? this.lastChainUuid;
10842
10915
  const trees = /* @__PURE__ */ new Map();
10843
- await this.folderBaseline;
10916
+ await this.folderBaseline?.catch(() => void 0);
10844
10917
  for (const repo2 of this.repos.values()) {
10845
10918
  if (repo2.dormant) continue;
10846
10919
  const tree = await repo2.tracker.snapshot().catch(() => null);
10847
10920
  if (tree) trees.set(repo2.root, tree);
10848
10921
  }
10849
- return { trees, sdkSessionId: this.sdkSessionId, resumeAt: this.resumeAt ?? this.lastChainUuid };
10922
+ return { trees, sdkSessionId, resumeAt };
10850
10923
  }
10851
10924
  recordCheckpoint(turnId, snap) {
10852
10925
  const at = iso2();
@@ -11220,9 +11293,16 @@ ${json}
11220
11293
  this.turnWatch = { lastExit: null, errored: false };
11221
11294
  if (!compact && typeof payload.turnId === "string" && payload.turnId) this.messageStatus(turnId, "started");
11222
11295
  this.setState("WORKING");
11223
- if (!compact) await this.checkpointTurn(turnId);
11296
+ let checkpointMs = null;
11297
+ const saved = compact ? Promise.resolve() : this.checkpointTurn(turnId).then(() => {
11298
+ checkpointMs = Date.now() - turnStart;
11299
+ }, (e) => {
11300
+ this.deps.log.warn("turn checkpoint failed", { error: this.redactor.text(e.message) });
11301
+ });
11302
+ this.toolGate = saved;
11224
11303
  if (!compact) {
11225
11304
  await this.refreshSkills();
11305
+ if (!this.local) await saved;
11226
11306
  this.startEnvSetups();
11227
11307
  await Promise.all(this.envSetups.values());
11228
11308
  }
@@ -11320,6 +11400,9 @@ ${text3}`;
11320
11400
  timing: new TurnTiming({ sessionId: this.deps.sessionId ?? null, turnId, model, reasoning: this.reasoningLevel }, typeof payload.receivedAt === "number" ? payload.receivedAt : null, turnStart)
11321
11401
  };
11322
11402
  this.live = live;
11403
+ void saved.then(() => {
11404
+ if (checkpointMs !== null) live.timing.phases({ checkpointMs });
11405
+ });
11323
11406
  const attempt = async (resume, at) => {
11324
11407
  let sawInit = false;
11325
11408
  let conversation = resume;
@@ -11356,41 +11439,26 @@ ${text3}`;
11356
11439
  live.turnEnded = false;
11357
11440
  live.headResult = null;
11358
11441
  channel.start(userMessage(turnPrompt, context.images, messageUuid(turnId)));
11359
- const options = buildQueryOptions({
11360
- root: this.deps.root,
11361
- model,
11362
- resume,
11363
- resumeAt: resume ? at : null,
11364
- abortController: ac,
11365
- reasoning: reasoning.options,
11366
- env: buildEngineEnv(boot, this.deps.configDir, model, { local: this.local, reasoning, output, extraEnv: this.projectEnv(), plan: planMode }),
11367
- mcpServer: this.mcpServer,
11368
- systemAppend: await this.systemAppend(),
11369
- policy: {
11370
- root: this.deps.root,
11371
- extraReadRoots: [this.deps.configDir],
11372
- deniedRoots: this.deps.privateDirs,
11373
- local: this.local,
11374
- plan: planMode,
11375
- planDir: (0, import_path15.join)(this.deps.configDir, "plans"),
11376
- orgMcpTools: orgMcpToolNames(Object.keys(off.mcpServers).length ? off.mcpCatalog : null)
11377
- },
11378
- plan: planMode,
11379
- onPlan: (text3) => this.proposePlan(live.turnId, text3),
11380
- skills: { dir: this.skillsDir(), enabled: enabledSkillNames(off.skillEntries), pluginDirs: off.pluginDirs },
11381
- ...Object.keys(off.mcpServers).length ? { orgMcpServers: off.mcpServers } : {},
11382
- pathToClaudeCodeExecutable: this.deps.pathToClaudeCodeExecutable,
11383
- commandGate: this.deps.commandGate,
11384
- ...this.localFolder?.kind === "folder" ? { beforeWrite: (path) => this.beforeFolderWrite(path) } : {}
11385
- });
11442
+ const inputs = await this.engineInputs({ model, reasoning, output, planMode, off, resume, resumeAt: resume ? at : null });
11386
11443
  if (!streaming) channel.close();
11387
11444
  const single = (text3) => context.images.length ? (async function* () {
11388
11445
  yield userMessage(text3, context.images);
11389
11446
  })() : text3;
11390
- const q = sdk.query({ prompt: streaming ? channel : single(turnPrompt), options });
11447
+ const warm = streaming ? await this.claimWarm(spawnKey(inputs)) : (this.dropWarm(), null);
11448
+ let q;
11449
+ if (warm) {
11450
+ const forward = () => warm.abort.abort();
11451
+ if (ac.signal.aborted) forward();
11452
+ else ac.signal.addEventListener("abort", forward, { once: true });
11453
+ q = warm.handle.query(channel);
11454
+ } else {
11455
+ q = sdk.query({ prompt: streaming ? channel : single(turnPrompt), options: this.queryOptions(inputs, ac, this.mcpServer) });
11456
+ }
11457
+ live.timing.phases({ prepareMs: Date.now() - turnStart, warmStart: !!warm });
11391
11458
  live.query = q;
11392
11459
  try {
11393
11460
  for await (const msg of q) {
11461
+ live.timing.engineFrame();
11394
11462
  const f = msg && typeof msg === "object" ? msg : {};
11395
11463
  const main2 = !f.parent_tool_use_id;
11396
11464
  if (f.type === "command_lifecycle") {
@@ -11445,6 +11513,7 @@ ${text3}`;
11445
11513
  }
11446
11514
  } finally {
11447
11515
  const stopped = ac.signal.aborted || live.interrupted;
11516
+ await saved;
11448
11517
  live.timing.end(this.deps.log, stopped ? "stopped" : "failed");
11449
11518
  this.live = null;
11450
11519
  this.turnAbort = null;
@@ -11585,6 +11654,7 @@ ${text3}`;
11585
11654
  }
11586
11655
  live.between = true;
11587
11656
  live.snapshot = this.takeSnapshot();
11657
+ this.toolGate = live.snapshot.then(() => void 0, () => void 0);
11588
11658
  }
11589
11659
  /** The query ended: its input closes, and a message it never read runs as the next turn (or is dropped by a rewind). */
11590
11660
  settleLive(live) {
@@ -11625,6 +11695,146 @@ ${text3}`;
11625
11695
  output: { limit: own ?? boot.runtime.maxOutputTokensCeiling ?? boot.runtime.maxOutputTokens ?? null, ceiling: own ?? boot.runtime.maxOutputTokensCeiling ?? boot.runtime.maxOutputTokens ?? null }
11626
11696
  };
11627
11697
  }
11698
+ /** The data a turn's engine process starts with (see EngineInputs). */
11699
+ async engineInputs(t) {
11700
+ const boot = this.boot;
11701
+ const org = Object.keys(t.off.mcpServers).length > 0;
11702
+ return {
11703
+ model: t.model,
11704
+ resume: t.resume,
11705
+ resumeAt: t.resumeAt,
11706
+ reasoning: t.reasoning.options,
11707
+ env: buildEngineEnv(boot, this.deps.configDir, t.model, { local: this.local, reasoning: t.reasoning, output: t.output, extraEnv: this.projectEnv(), plan: t.planMode }),
11708
+ systemAppend: await this.systemAppend(),
11709
+ policy: {
11710
+ root: this.deps.root,
11711
+ extraReadRoots: [this.deps.configDir],
11712
+ deniedRoots: this.deps.privateDirs,
11713
+ local: this.local,
11714
+ plan: t.planMode,
11715
+ planDir: (0, import_path15.join)(this.deps.configDir, "plans"),
11716
+ orgMcpTools: orgMcpToolNames(org ? t.off.mcpCatalog : null)
11717
+ },
11718
+ plan: t.planMode,
11719
+ skills: { dir: this.skillsDir(), enabled: enabledSkillNames(t.off.skillEntries), pluginDirs: t.off.pluginDirs },
11720
+ skillsKey: this.skillsSent,
11721
+ orgMcpServers: org ? t.off.mcpServers : {},
11722
+ folderWrites: this.localFolder?.kind === "folder"
11723
+ };
11724
+ }
11725
+ /**
11726
+ * The query options of a turn's engine process. The callbacks read the engine's current turn (never one captured
11727
+ * here), so a process started ahead serves the turn that takes it: its plan goes to that turn, and its tools wait
11728
+ * for that turn's saved point.
11729
+ */
11730
+ queryOptions(i, abortController, mcpServer) {
11731
+ const gate = this.deps.commandGate;
11732
+ return buildQueryOptions({
11733
+ root: this.deps.root,
11734
+ model: i.model,
11735
+ resume: i.resume,
11736
+ resumeAt: i.resumeAt,
11737
+ abortController,
11738
+ reasoning: i.reasoning,
11739
+ env: i.env,
11740
+ mcpServer,
11741
+ systemAppend: i.systemAppend,
11742
+ policy: i.policy,
11743
+ plan: i.plan,
11744
+ onPlan: (text3) => {
11745
+ const live = this.live;
11746
+ if (live) this.proposePlan(live.turnId, text3);
11747
+ },
11748
+ skills: i.skills,
11749
+ ...Object.keys(i.orgMcpServers).length ? { orgMcpServers: i.orgMcpServers } : {},
11750
+ pathToClaudeCodeExecutable: this.deps.pathToClaudeCodeExecutable,
11751
+ commandGate: gate ? timedGate(gate, (ms) => this.live?.timing.permission(ms)) : void 0,
11752
+ beforeTool: async () => {
11753
+ const t0 = Date.now();
11754
+ await this.toolGate;
11755
+ const waited = Date.now() - t0;
11756
+ if (waited > 0) this.live?.timing.toolWait(waited);
11757
+ },
11758
+ ...i.folderWrites ? { beforeWrite: (path) => this.beforeFolderWrite(path) } : {}
11759
+ });
11760
+ }
11761
+ /**
11762
+ * The inputs of the next turn as the person would most likely send it (same model, reasoning, mode and tools as the
11763
+ * session now, an everyday task), for the process started ahead. null when that process could not serve it safely:
11764
+ * the organization's MCP servers are in-process objects shared by every process, and one serves one process at a time.
11765
+ */
11766
+ async nextTurnInputs() {
11767
+ const boot = this.boot;
11768
+ const off = this.turnOff(void 0);
11769
+ if (Object.keys(off.mcpServers).length) return null;
11770
+ const primary = boot.runtime.primaryModel || boot.model;
11771
+ const route = routeAutoTurn(boot, {
11772
+ content: "",
11773
+ previousTrouble: this.previousTrouble,
11774
+ maxMode: this.reasoningLevel === "max" || isMaxMode(this.reasoningLevel, this.reasoningCapability)
11775
+ });
11776
+ const { model, reasoning, output } = this.turnModel(primary, route);
11777
+ const canResume = !!this.sdkSessionId && (this.knownSessions.has(this.sdkSessionId) || await hasLocalTranscript(this.deps.configDir, this.sdkSessionId));
11778
+ return this.engineInputs({ model, reasoning, output, planMode: false, off, resume: canResume ? this.sdkSessionId : null, resumeAt: canResume ? this.resumeAt : null });
11779
+ }
11780
+ /**
11781
+ * Starts the engine process of the next turn now (warmStart), while the session waits for the person: Claude Code
11782
+ * keeps its process up between messages, and here the next message then goes to a ready process. Only when nothing
11783
+ * runs or waits; a process already started with the same inputs is kept.
11784
+ */
11785
+ prewarm() {
11786
+ const sdk = this.sdk;
11787
+ if (!this.deps.warmStart || !sdk?.startup || !this.boot || this.stopping || this.turnRunning || this.queue.length || this.prewarming) return;
11788
+ this.prewarming = (async () => {
11789
+ const inputs = await this.nextTurnInputs();
11790
+ if (!inputs || this.stopping || this.turnRunning || this.queue.length) return;
11791
+ const key = spawnKey(inputs);
11792
+ if (this.warm?.key === key) return;
11793
+ this.dropWarm();
11794
+ const abort = new AbortController();
11795
+ const server = buildScaleQualityServer(sdk, this.toolHost());
11796
+ const ready = sdk.startup({ options: this.queryOptions(inputs, abort, server) }).catch((e) => {
11797
+ this.deps.log.warn("engine warm start failed", { error: this.redactor.text(e.message) });
11798
+ return null;
11799
+ });
11800
+ const timer = setTimeout(() => {
11801
+ if (this.warm?.abort === abort) this.dropWarm();
11802
+ }, this.deps.warmIdleMs ?? WARM_IDLE_MS);
11803
+ timer.unref?.();
11804
+ this.warm = { key, abort, ready, timer };
11805
+ })().catch((e) => {
11806
+ this.deps.log.warn("engine warm start failed", { error: this.redactor.text(e.message) });
11807
+ }).finally(() => {
11808
+ this.prewarming = null;
11809
+ });
11810
+ }
11811
+ /** The process started ahead, for a turn with this start key; any other is closed (the turn starts its own). */
11812
+ async claimWarm(key) {
11813
+ await this.prewarming;
11814
+ const w = this.warm;
11815
+ if (!w) return null;
11816
+ if (w.key !== key) {
11817
+ this.dropWarm();
11818
+ return null;
11819
+ }
11820
+ this.warm = null;
11821
+ clearTimeout(w.timer);
11822
+ const handle = await w.ready;
11823
+ return handle ? { handle, abort: w.abort } : null;
11824
+ }
11825
+ /** Closes the process started ahead, if any. */
11826
+ dropWarm() {
11827
+ const w = this.warm;
11828
+ if (!w) return;
11829
+ this.warm = null;
11830
+ clearTimeout(w.timer);
11831
+ void w.ready.then((h) => {
11832
+ try {
11833
+ h?.close();
11834
+ } catch {
11835
+ }
11836
+ }).finally(() => w.abort.abort());
11837
+ }
11628
11838
  /** The imported conversation as a context block (fetched once); null when there is none or it cannot be read. */
11629
11839
  importedHistoryBlock() {
11630
11840
  const info = this.boot?.imported;
@@ -11736,6 +11946,7 @@ ${patch}`;
11736
11946
  if (this.stopping) return;
11737
11947
  this.stopping = true;
11738
11948
  this.queue.length = 0;
11949
+ this.dropWarm();
11739
11950
  this.turnAbort?.abort();
11740
11951
  this.pollAbort.abort();
11741
11952
  this.envAbort.abort();
@@ -12261,6 +12472,7 @@ function buildQueryOptions(o) {
12261
12472
  };
12262
12473
  const preToolUse = async (input) => {
12263
12474
  if (input.hook_event_name !== "PreToolUse") return {};
12475
+ if (o.beforeTool) await o.beforeTool();
12264
12476
  const name = String(input.tool_name ?? "");
12265
12477
  if (name === "ExitPlanMode") return o.plan ? {} : { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: "Not in plan mode." } };
12266
12478
  const provenance = input.mcp_server;
@@ -12276,6 +12488,7 @@ function buildQueryOptions(o) {
12276
12488
  return d.behavior === "allow" ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: d.updatedInput } } : { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: d.message } };
12277
12489
  };
12278
12490
  const canUseTool = async (toolName, input, opts) => {
12491
+ if (o.beforeTool) await o.beforeTool();
12279
12492
  if (toolName.startsWith("mcp__") && opts?.mcpServer?.source && opts.mcpServer.source !== "sdk") {
12280
12493
  return { behavior: "deny", message: "Only ScaleQuality tools are available in this workspace." };
12281
12494
  }
@@ -12337,6 +12550,24 @@ function buildQueryOptions(o) {
12337
12550
  ...o.pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable: o.pathToClaudeCodeExecutable } : {}
12338
12551
  };
12339
12552
  }
12553
+ function spawnKey(i) {
12554
+ const { orgMcpServers, ...data } = i;
12555
+ return (0, import_crypto7.createHash)("sha256").update(JSON.stringify({ ...data, orgMcpServers: Object.keys(orgMcpServers).sort() })).digest("hex");
12556
+ }
12557
+ function timedGate(gate, took) {
12558
+ const timed = async (run2) => {
12559
+ const t0 = Date.now();
12560
+ try {
12561
+ return await run2();
12562
+ } finally {
12563
+ took(Date.now() - t0);
12564
+ }
12565
+ };
12566
+ return {
12567
+ check: (command, opts) => timed(() => gate.check(command, opts)),
12568
+ ...gate.checkEdit ? { checkEdit: (path, opts) => timed(() => gate.checkEdit(path, opts)) } : {}
12569
+ };
12570
+ }
12340
12571
  async function hasLocalTranscript(configDir, sessionId) {
12341
12572
  if (!/^[A-Za-z0-9-]{8,80}$/.test(sessionId)) return false;
12342
12573
  const projects = (0, import_path15.join)(configDir, "projects");
@@ -12390,6 +12621,10 @@ function createLocalEngine(o) {
12390
12621
  const log = {
12391
12622
  info: (msg, ctx) => {
12392
12623
  if (o.verbose) say(style.dim(`${o.prefix ?? ""}[info] ${msg} ${ctx ? JSON.stringify(ctx) : ""}`));
12624
+ else if (msg === "WORKSPACE_TURN_TIMING") {
12625
+ const line = turnTimingLine(ctx);
12626
+ if (line) say(`${o.prefix ?? ""}${style.dim(` ${line}`)}`);
12627
+ }
12393
12628
  },
12394
12629
  warn: (msg, ctx) => {
12395
12630
  if (o.verbose) say(style.dim(`${o.prefix ?? ""}[warn] ${msg} ${ctx ? JSON.stringify(ctx) : ""}`));
@@ -12465,7 +12700,9 @@ function createLocalEngine(o) {
12465
12700
  // An imported Claude Code conversation of this computer and folder resumes from its own transcript.
12466
12701
  resumeImported: (externalId) => copyClaudeTranscript(sources, externalId, o.root, configDir),
12467
12702
  exit: (code, reason) => o.exit(code, reason, startError),
12468
- pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE || void 0
12703
+ pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE || void 0,
12704
+ // The next message's engine process is started while the person reads, like Claude Code's process between messages.
12705
+ warmStart: true
12469
12706
  });
12470
12707
  }
12471
12708
  async function connectMain(argv) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalequality/cli",
3
- "version": "0.4.8",
3
+ "version": "0.4.9-rc.2",
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": {