flavor-code 1.3.12 → 1.3.13

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.
@@ -66,12 +66,12 @@ function normalizeProviderError(error) {
66
66
 
67
67
  // src/utils/async.ts
68
68
  function withTimeout(promise, ms) {
69
- return new Promise((resolve28, reject) => {
69
+ return new Promise((resolve29, reject) => {
70
70
  let settled = false;
71
71
  const timer = setTimeout(() => {
72
72
  if (settled) return;
73
73
  settled = true;
74
- resolve28({ timedOut: true, value: void 0 });
74
+ resolve29({ timedOut: true, value: void 0 });
75
75
  }, ms);
76
76
  timer.unref?.();
77
77
  promise.then(
@@ -79,7 +79,7 @@ function withTimeout(promise, ms) {
79
79
  if (!settled) {
80
80
  settled = true;
81
81
  clearTimeout(timer);
82
- resolve28({ timedOut: false, value });
82
+ resolve29({ timedOut: false, value });
83
83
  }
84
84
  },
85
85
  (error) => {
@@ -94,13 +94,13 @@ function withTimeout(promise, ms) {
94
94
  }
95
95
  function awaitWithSignal(promise, signal) {
96
96
  signal.throwIfAborted();
97
- return new Promise((resolve28, reject) => {
97
+ return new Promise((resolve29, reject) => {
98
98
  const onAbort = () => reject(signal.reason);
99
99
  signal.addEventListener("abort", onAbort, { once: true });
100
100
  promise.then(
101
101
  (value) => {
102
102
  signal.removeEventListener("abort", onAbort);
103
- resolve28(value);
103
+ resolve29(value);
104
104
  },
105
105
  (error) => {
106
106
  signal.removeEventListener("abort", onAbort);
@@ -3224,6 +3224,7 @@ var MVP_COMMANDS = [
3224
3224
  "mcp",
3225
3225
  "commit",
3226
3226
  "review",
3227
+ "explain",
3227
3228
  "ide",
3228
3229
  "memory",
3229
3230
  "remember",
@@ -3262,6 +3263,7 @@ var COMMAND_DESCRIPTIONS = {
3262
3263
  evolve: "Self-improvement loop: capture failures, suggest fixes, verify with tests",
3263
3264
  commit: "Generate a commit message for staged changes and commit after confirmation",
3264
3265
  review: "Review uncommitted changes for bugs and risks before committing",
3266
+ explain: "Explain a symbol for newcomers (AST graph + source + git history)",
3265
3267
  mcp: "Manage MCP servers",
3266
3268
  ide: "Show the connected IDE and editor context",
3267
3269
  memory: "Show long-term project memory",
@@ -3338,6 +3340,12 @@ function parseSlashCommand(input, dynamicCommands = [], skillCommands = [], mana
3338
3340
  const focus = args.join(" ").trim();
3339
3341
  return focus.length === 0 ? { name } : { name, focus };
3340
3342
  }
3343
+ if (name === "explain") {
3344
+ const [query, ...focusParts] = args;
3345
+ if (query === void 0) return { name };
3346
+ const focus = focusParts.join(" ").trim();
3347
+ return focus.length === 0 ? { name, query } : { name, query, focus };
3348
+ }
3341
3349
  if (name === "loop") {
3342
3350
  const goal = args.join(" ").trim();
3343
3351
  if (!goal) return { name: "invalid", command: name, message: "Use /loop <goal>." };
@@ -3941,6 +3949,14 @@ var FlavorSession = class {
3941
3949
  this.#notice(await required(this.#services.gitCommit, "commit")(command.hint, signal));
3942
3950
  } else if (command.name === "review") {
3943
3951
  this.#notice(await required(this.#services.gitReview, "review")(command.focus, signal));
3952
+ } else if (command.name === "explain") {
3953
+ const result = await required(this.#services.explain, "explain")(
3954
+ command.query,
3955
+ command.focus,
3956
+ signal,
3957
+ (chunk) => this.#services.output({ type: "text", text: chunk })
3958
+ );
3959
+ if (result !== "") this.#notice(result);
3944
3960
  } else if (command.name === "mcp") {
3945
3961
  this.#notice(await this.#services.mcp(command, signal));
3946
3962
  } else if (command.name === "pals") {
@@ -4107,9 +4123,9 @@ var FlavorSession = class {
4107
4123
  }
4108
4124
  #waitForPlanningIdle(key) {
4109
4125
  if (!this.#activePlanning.has(key)) return Promise.resolve();
4110
- return new Promise((resolve28) => {
4126
+ return new Promise((resolve29) => {
4111
4127
  const check = () => {
4112
- if (!this.#activePlanning.has(key)) resolve28();
4128
+ if (!this.#activePlanning.has(key)) resolve29();
4113
4129
  else this.#planningIdleWaiters.add(check);
4114
4130
  };
4115
4131
  this.#planningIdleWaiters.add(check);
@@ -4457,12 +4473,12 @@ var BrokerStateError = class extends Error {
4457
4473
  };
4458
4474
  var WINDOWS_PIPE_PREFIX = "\\\\.\\pipe\\";
4459
4475
  function canConnect(address) {
4460
- return new Promise((resolve28) => {
4476
+ return new Promise((resolve29) => {
4461
4477
  const socket = new Socket();
4462
4478
  const finish = (connected) => {
4463
4479
  socket.removeAllListeners();
4464
4480
  socket.destroy();
4465
- resolve28(connected);
4481
+ resolve29(connected);
4466
4482
  };
4467
4483
  socket.once("connect", () => finish(true));
4468
4484
  socket.once("error", () => finish(false));
@@ -4511,7 +4527,7 @@ var PalBrokerServer = class {
4511
4527
  this.authToken ??= await loadOrCreatePalAuthToken(this.authHome === void 0 ? {} : { home: this.authHome });
4512
4528
  try {
4513
4529
  await this.prepareAddress();
4514
- await new Promise((resolve28, reject) => {
4530
+ await new Promise((resolve29, reject) => {
4515
4531
  const onError = (error) => {
4516
4532
  this.server.off("listening", onListening);
4517
4533
  reject(error);
@@ -4520,7 +4536,7 @@ var PalBrokerServer = class {
4520
4536
  this.server.off("error", onError);
4521
4537
  this.started = true;
4522
4538
  this.ownsAddress = true;
4523
- resolve28();
4539
+ resolve29();
4524
4540
  };
4525
4541
  this.server.once("error", onError);
4526
4542
  this.server.once("listening", onListening);
@@ -4548,7 +4564,7 @@ var PalBrokerServer = class {
4548
4564
  const ownsAddress = this.ownsAddress;
4549
4565
  for (const connection of this.connections) connection.socket.destroy();
4550
4566
  if (this.started) {
4551
- await new Promise((resolve28) => this.server.close(() => resolve28()));
4567
+ await new Promise((resolve29) => this.server.close(() => resolve29()));
4552
4568
  this.started = false;
4553
4569
  }
4554
4570
  this.ownsAddress = false;
@@ -4567,8 +4583,8 @@ var PalBrokerServer = class {
4567
4583
  });
4568
4584
  if (this.endpointLock === void 0) throw new Error(`Pals endpoint '${this.address}' is active or owned`);
4569
4585
  try {
4570
- const stat9 = await lstat3(this.address);
4571
- if (!stat9.isSocket()) throw new Error(`Refusing to unlink '${this.address}' because it is not a socket`);
4586
+ const stat10 = await lstat3(this.address);
4587
+ if (!stat10.isSocket()) throw new Error(`Refusing to unlink '${this.address}' because it is not a socket`);
4572
4588
  await unlink3(this.address);
4573
4589
  } catch (error) {
4574
4590
  if (error.code !== "ENOENT") throw error;
@@ -4581,8 +4597,8 @@ var PalBrokerServer = class {
4581
4597
  }
4582
4598
  async unlinkKnownSocket() {
4583
4599
  try {
4584
- const stat9 = await lstat3(this.address);
4585
- if (stat9.isSocket()) await unlink3(this.address);
4600
+ const stat10 = await lstat3(this.address);
4601
+ if (stat10.isSocket()) await unlink3(this.address);
4586
4602
  } catch (error) {
4587
4603
  if (error.code !== "ENOENT") throw error;
4588
4604
  }
@@ -5217,15 +5233,15 @@ var PalBrokerState = class {
5217
5233
  // src/pals/broker-cli.ts
5218
5234
  var startupAttempts = /* @__PURE__ */ new Map();
5219
5235
  function delay(milliseconds) {
5220
- return new Promise((resolve28) => setTimeout(resolve28, milliseconds));
5236
+ return new Promise((resolve29) => setTimeout(resolve29, milliseconds));
5221
5237
  }
5222
5238
  function canConnect2(address) {
5223
- return new Promise((resolve28) => {
5239
+ return new Promise((resolve29) => {
5224
5240
  const socket = new Socket2();
5225
5241
  const finish = (connected) => {
5226
5242
  socket.removeAllListeners();
5227
5243
  socket.destroy();
5228
- resolve28(connected);
5244
+ resolve29(connected);
5229
5245
  };
5230
5246
  socket.once("connect", () => finish(true));
5231
5247
  socket.once("error", () => finish(false));
@@ -5268,8 +5284,8 @@ async function runPalBroker(options) {
5268
5284
  const sweepIntervalMs = options.sweepIntervalMs ?? 5e3;
5269
5285
  const idleShutdownMs = options.idleShutdownMs ?? 3e4;
5270
5286
  let resolveClosed;
5271
- const closed = new Promise((resolve28) => {
5272
- resolveClosed = resolve28;
5287
+ const closed = new Promise((resolve29) => {
5288
+ resolveClosed = resolve29;
5273
5289
  });
5274
5290
  let closing;
5275
5291
  const timer = setInterval(() => {
@@ -5312,7 +5328,7 @@ async function ensureOnce(options) {
5312
5328
  import { readFile as readFile22 } from "fs/promises";
5313
5329
  import { createHash as createHash18, randomUUID as randomUUID20 } from "crypto";
5314
5330
  import { homedir as homedir2, release as osRelease, tmpdir as tmpdir4, version as osVersion } from "os";
5315
- import { basename as basename6, join as join25, relative as relative20, resolve as resolve27, sep as sep20 } from "path";
5331
+ import { basename as basename6, join as join26, relative as relative20, resolve as resolve28, sep as sep20 } from "path";
5316
5332
 
5317
5333
  // src/agent/subagents.ts
5318
5334
  import { z as z5 } from "zod";
@@ -7164,10 +7180,10 @@ function sanitize(message2, secrets) {
7164
7180
  }
7165
7181
  function waitForRetry(delayMs, signal) {
7166
7182
  if (signal?.aborted) return Promise.reject(signal.reason);
7167
- return new Promise((resolve28, reject) => {
7183
+ return new Promise((resolve29, reject) => {
7168
7184
  const timer = setTimeout(() => {
7169
7185
  signal?.removeEventListener("abort", onAbort);
7170
- resolve28();
7186
+ resolve29();
7171
7187
  }, delayMs);
7172
7188
  const onAbort = () => {
7173
7189
  clearTimeout(timer);
@@ -7778,10 +7794,10 @@ function serializeToolInput(input) {
7778
7794
  }
7779
7795
  function waitForRetry2(delayMs, signal) {
7780
7796
  if (signal?.aborted) return Promise.reject(signal.reason);
7781
- return new Promise((resolve28, reject) => {
7797
+ return new Promise((resolve29, reject) => {
7782
7798
  const timer = setTimeout(() => {
7783
7799
  signal?.removeEventListener("abort", onAbort);
7784
- resolve28();
7800
+ resolve29();
7785
7801
  }, delayMs);
7786
7802
  const onAbort = () => {
7787
7803
  clearTimeout(timer);
@@ -8987,7 +9003,7 @@ function decoratePayload(payload, envelope) {
8987
9003
  return envelope === void 0 ? payload : { ...payload, ...envelope };
8988
9004
  }
8989
9005
  function runShellHandler(descriptor, event, signal) {
8990
- return new Promise((resolve28, reject) => {
9006
+ return new Promise((resolve29, reject) => {
8991
9007
  const child = spawn4(descriptor.command, [...descriptor.args ?? []], {
8992
9008
  env: { ...process.env, ...descriptor.env },
8993
9009
  shell: false,
@@ -9030,7 +9046,7 @@ function runShellHandler(descriptor, event, signal) {
9030
9046
  return;
9031
9047
  }
9032
9048
  try {
9033
- resolve28(HookDecisionSchema.parse(JSON.parse(stdout)));
9049
+ resolve29(HookDecisionSchema.parse(JSON.parse(stdout)));
9034
9050
  } catch (error) {
9035
9051
  reject(error);
9036
9052
  }
@@ -9227,14 +9243,14 @@ async function createIslandControlServer(options) {
9227
9243
  void dispatch(line, socket, options, token);
9228
9244
  });
9229
9245
  });
9230
- await new Promise((resolve28, reject) => {
9246
+ await new Promise((resolve29, reject) => {
9231
9247
  const onError = (error) => {
9232
9248
  server.removeListener("listening", onListening);
9233
9249
  reject(error);
9234
9250
  };
9235
9251
  const onListening = () => {
9236
9252
  server.removeListener("error", onError);
9237
- resolve28();
9253
+ resolve29();
9238
9254
  };
9239
9255
  server.once("error", onError);
9240
9256
  server.once("listening", onListening);
@@ -9305,11 +9321,11 @@ function write(socket, payload) {
9305
9321
  }
9306
9322
  }
9307
9323
  function closeServer(server) {
9308
- return new Promise((resolve28) => {
9324
+ return new Promise((resolve29) => {
9309
9325
  try {
9310
- server.close(() => resolve28());
9326
+ server.close(() => resolve29());
9311
9327
  } catch {
9312
- resolve28();
9328
+ resolve29();
9313
9329
  }
9314
9330
  });
9315
9331
  }
@@ -10494,7 +10510,7 @@ var USAGE = [
10494
10510
  " clear reset signals, done markers, and verified markers"
10495
10511
  ].join("\n");
10496
10512
  function runCommand(command, cwd, timeoutMs) {
10497
- return new Promise((resolve28) => {
10513
+ return new Promise((resolve29) => {
10498
10514
  const isWindows = process.platform === "win32";
10499
10515
  const child = spawn5(isWindows ? "cmd.exe" : "/bin/sh", isWindows ? ["/d", "/s", "/c", command] : ["-c", command], {
10500
10516
  cwd,
@@ -10504,7 +10520,7 @@ function runCommand(command, cwd, timeoutMs) {
10504
10520
  let stderr = "";
10505
10521
  const timer = setTimeout(() => {
10506
10522
  child.kill();
10507
- resolve28({ ok: false, stdout, stderr: `${stderr}
10523
+ resolve29({ ok: false, stdout, stderr: `${stderr}
10508
10524
  [killed: exceeded ${timeoutMs}ms]`, code: "timeout" });
10509
10525
  }, timeoutMs);
10510
10526
  child.stdout.on("data", (chunk) => {
@@ -10515,12 +10531,12 @@ function runCommand(command, cwd, timeoutMs) {
10515
10531
  });
10516
10532
  child.on("error", (error) => {
10517
10533
  clearTimeout(timer);
10518
- resolve28({ ok: false, stdout, stderr: `${stderr}
10534
+ resolve29({ ok: false, stdout, stderr: `${stderr}
10519
10535
  ${error.message}`, code: "spawn" });
10520
10536
  });
10521
10537
  child.on("close", (code) => {
10522
10538
  clearTimeout(timer);
10523
- resolve28({ ok: code === 0, stdout, stderr, code: code ?? "unknown" });
10539
+ resolve29({ ok: code === 0, stdout, stderr, code: code ?? "unknown" });
10524
10540
  });
10525
10541
  });
10526
10542
  }
@@ -14036,8 +14052,8 @@ var OAuthCallbackAuthProvider = class {
14036
14052
  async #startCallbackServer(expectedState, signal) {
14037
14053
  let serverResolve;
14038
14054
  let serverReject;
14039
- const codePromise = new Promise((resolve28, reject) => {
14040
- serverResolve = resolve28;
14055
+ const codePromise = new Promise((resolve29, reject) => {
14056
+ serverResolve = resolve29;
14041
14057
  serverReject = reject;
14042
14058
  });
14043
14059
  const server = createServer3((req, res) => {
@@ -14071,7 +14087,7 @@ var OAuthCallbackAuthProvider = class {
14071
14087
  );
14072
14088
  serverResolve(code);
14073
14089
  });
14074
- return new Promise((resolve28, reject) => {
14090
+ return new Promise((resolve29, reject) => {
14075
14091
  server.once("error", (error) => {
14076
14092
  reject(new Error(`OAuth callback server failed: ${error.message}`));
14077
14093
  });
@@ -14088,7 +14104,7 @@ var OAuthCallbackAuthProvider = class {
14088
14104
  signal.addEventListener("abort", () => {
14089
14105
  serverReject(new Error("OAuth flow aborted"));
14090
14106
  }, { once: true });
14091
- resolve28({ redirectUri, promise: codePromise });
14107
+ resolve29({ redirectUri, promise: codePromise });
14092
14108
  });
14093
14109
  });
14094
14110
  }
@@ -17227,8 +17243,8 @@ var LspConnection = class {
17227
17243
  const id = this.#nextId++;
17228
17244
  const request = { jsonrpc: "2.0", id, method, params };
17229
17245
  const timeout = AbortSignal.timeout(LSP_REQUEST_TIMEOUT_MS);
17230
- const promise = new Promise((resolve28, reject) => {
17231
- this.#pending.set(id, { resolve: resolve28, reject });
17246
+ const promise = new Promise((resolve29, reject) => {
17247
+ this.#pending.set(id, { resolve: resolve29, reject });
17232
17248
  timeout.addEventListener("abort", () => {
17233
17249
  if (this.#pending.has(id)) {
17234
17250
  this.#pending.delete(id);
@@ -17632,19 +17648,19 @@ async function changeSummary(workspace) {
17632
17648
  };
17633
17649
  }
17634
17650
  async function stagedDiff(workspace, maxChars = 96e3) {
17635
- const stat9 = await git2(workspace, ["diff", "--staged", "--stat"]);
17651
+ const stat10 = await git2(workspace, ["diff", "--staged", "--stat"]);
17636
17652
  const diff = await git2(workspace, ["diff", "--staged", "--no-ext-diff", "--no-color"]);
17637
17653
  if (!diff.ok) throw gitFailure(diff, "diff --staged");
17638
17654
  const text = diff.stdout;
17639
17655
  return {
17640
17656
  diff: text.length > maxChars ? `${text.slice(0, maxChars)}
17641
17657
  \u2026[diff truncated]` : text,
17642
- stat: stat9.stdout.trim(),
17658
+ stat: stat10.stdout.trim(),
17643
17659
  truncated: text.length > maxChars
17644
17660
  };
17645
17661
  }
17646
17662
  async function uncommittedDiff(workspace, maxChars = 96e3) {
17647
- const stat9 = await git2(workspace, ["diff", "HEAD", "--stat"]);
17663
+ const stat10 = await git2(workspace, ["diff", "HEAD", "--stat"]);
17648
17664
  const diff = await git2(workspace, ["diff", "HEAD", "--no-ext-diff", "--no-color"]);
17649
17665
  if (!diff.ok && !diff.stderr.includes("unknown revision")) throw gitFailure(diff, "diff HEAD");
17650
17666
  const summary2 = await changeSummary(workspace);
@@ -17652,7 +17668,7 @@ async function uncommittedDiff(workspace, maxChars = 96e3) {
17652
17668
  return {
17653
17669
  diff: text.length > maxChars ? `${text.slice(0, maxChars)}
17654
17670
  \u2026[diff truncated]` : text,
17655
- stat: stat9.stdout.trim(),
17671
+ stat: stat10.stdout.trim(),
17656
17672
  untracked: summary2.untrackedFiles
17657
17673
  };
17658
17674
  }
@@ -17793,6 +17809,232 @@ function formatReviewReport(report) {
17793
17809
  return lines.join("\n");
17794
17810
  }
17795
17811
 
17812
+ // src/explain/service.ts
17813
+ async function resolveExplainTarget(graph, query) {
17814
+ const trimmed = query.trim();
17815
+ if (trimmed.includes("#")) {
17816
+ const hit = await graph.relations(trimmed).catch(() => void 0);
17817
+ if (hit?.origin !== void 0) return { kind: "resolved", node: hit.origin };
17818
+ }
17819
+ const hits = await graph.search(trimmed, 10);
17820
+ if (hits.length === 0) return { kind: "not-found", query: trimmed };
17821
+ if (hits.length === 1) return { kind: "resolved", node: hits[0] };
17822
+ return { kind: "ambiguous", candidates: hits };
17823
+ }
17824
+ function explainCandidateLabel(candidate) {
17825
+ return candidate.id;
17826
+ }
17827
+ async function selectExplainCandidate(candidates, questions, signal) {
17828
+ const shown = candidates.slice(0, 3);
17829
+ const answers = await questions.ask([{
17830
+ header: "Symbol",
17831
+ question: "Multiple symbols match. Which one should /explain cover? (Pick one, or type a more exact name.)",
17832
+ options: [
17833
+ ...shown.map((candidate) => ({
17834
+ label: explainCandidateLabel(candidate),
17835
+ description: `${candidate.kind} \xB7 ${candidate.filePath}:${candidate.startLine}-${candidate.endLine}`
17836
+ })),
17837
+ { label: "Cancel", description: "Abort /explain without choosing." }
17838
+ ]
17839
+ }], signal);
17840
+ const answer = (answers[0] ?? "").trim();
17841
+ if (answer === "Cancel" || answer === "") return { kind: "cancelled" };
17842
+ const picked = shown.find((candidate) => explainCandidateLabel(candidate) === answer);
17843
+ if (picked !== void 0) return { kind: "picked", node: picked };
17844
+ return { kind: "typed", query: answer };
17845
+ }
17846
+ var MAX_SOURCE_CHARS = 2e4;
17847
+ function buildExplainPrompt(input) {
17848
+ const source = input.anchorSource.length > MAX_SOURCE_CHARS ? `${input.anchorSource.slice(0, MAX_SOURCE_CHARS)}
17849
+ \u2026 (truncated)` : input.anchorSource;
17850
+ const line = (n) => `- ${n.name} \u2014 ${n.filePath}:${n.startLine}`;
17851
+ return [
17852
+ "You are a senior engineer onboarding a new team member. Explain the following code symbol clearly and concretely; never invent behavior that is not in the shown evidence.",
17853
+ `Answer in ${input.language}.`,
17854
+ "Structure the answer with short markdown sections:",
17855
+ "1. \u5B83\u662F\u505A\u4EC0\u4E48\u7684 \u2014 one paragraph, plain language, beginner first.",
17856
+ "2. \u5173\u952E\u5B9E\u73B0\u70B9 \u2014 2-5 bullets tied to concrete lines/branches in the source.",
17857
+ "3. \u8C01\u8C03\u7528\u5B83 / \u5B83\u8C03\u7528\u8C01 \u2014 use the relation lists; explain the data flow in one or two sentences.",
17858
+ "4. \u4E3A\u4EC0\u4E48\u8FD9\u6837\u5199 \u2014 infer from the recent commit subjects when possible; say '\u4E0D\u786E\u5B9A' instead of guessing.",
17859
+ "5. \u65B0\u4EBA\u6CE8\u610F\u4E8B\u9879 \u2014 pitfalls, guards or invariants worth knowing.",
17860
+ ...input.focus === void 0 || input.focus.trim() === "" ? [] : [`The user specifically asks about: ${input.focus.trim()}`],
17861
+ "",
17862
+ `Symbol: ${input.anchor.id} (${input.anchor.kind}${input.anchor.signature ? `, signature: ${input.anchor.signature}` : ""})`,
17863
+ "",
17864
+ `Source (${input.anchor.filePath}:${input.anchor.startLine}-${input.anchor.endLine}):`,
17865
+ source,
17866
+ "",
17867
+ `Callers:
17868
+ ${input.callers.map(line).join("\n") || "- (none)"}`,
17869
+ `Callees:
17870
+ ${input.callees.map(line).join("\n") || "- (none)"}`,
17871
+ "",
17872
+ `Recent commits touching the file:
17873
+ ${input.history.map((h) => `- ${h.date} ${h.author}: ${h.subject}`).join("\n") || "- (no history)"}`
17874
+ ].join("\n");
17875
+ }
17876
+ async function explainWithModel(options, prompt, signal, onText) {
17877
+ const { adapter, model } = options.registry.get(options.modelId());
17878
+ let text = "";
17879
+ for await (const event of adapter.stream({
17880
+ model,
17881
+ messages: [{ role: "user", content: prompt }],
17882
+ tools: [],
17883
+ signal
17884
+ })) {
17885
+ if (event.type === "text") {
17886
+ text += event.text;
17887
+ onText?.(event.text);
17888
+ } else if (event.type === "error") throw new Error(`explain generation failed: ${event.error.message}`);
17889
+ else if (event.type === "done") break;
17890
+ }
17891
+ const cleaned = text.trim();
17892
+ if (cleaned === "") throw new Error("explain generation returned no text");
17893
+ return cleaned;
17894
+ }
17895
+ var HISTORY_LIMIT = 8;
17896
+ var MAX_RESOLVE_ROUNDS = 2;
17897
+ async function runExplain(deps, query, focus, signal) {
17898
+ if (query === void 0) {
17899
+ return "Usage: /explain <symbol | file.ts#symbol> [what to focus on]. Example: /explain src/order.ts#cancelOrder error handling";
17900
+ }
17901
+ if (!(await deps.graph.status()).available) {
17902
+ return "The code graph is not built yet \u2014 run /ast init, then try /explain again.";
17903
+ }
17904
+ let target = await resolveExplainTarget(deps.graph, query);
17905
+ for (let round = 1; round <= MAX_RESOLVE_ROUNDS; round += 1) {
17906
+ if (target.kind !== "ambiguous") break;
17907
+ if (deps.questions === void 0) {
17908
+ return `"${query}" is ambiguous. Use the exact node id, e.g. /explain ${target.candidates[0]?.id ?? "path/file.ts#name"}.`;
17909
+ }
17910
+ const selection = await selectExplainCandidate(target.candidates, deps.questions, signal);
17911
+ if (selection.kind === "cancelled") return "/explain cancelled \u2014 no symbol was selected.";
17912
+ if (selection.kind === "picked") {
17913
+ target = { kind: "resolved", node: selection.node };
17914
+ break;
17915
+ }
17916
+ target = await resolveExplainTarget(deps.graph, selection.query);
17917
+ if (round === MAX_RESOLVE_ROUNDS && target.kind === "ambiguous") {
17918
+ return `"${selection.query}" is still ambiguous. Pick one node id explicitly: /explain ${target.candidates[0]?.id ?? "path/file.ts#name"}`;
17919
+ }
17920
+ }
17921
+ if (target.kind === "not-found") {
17922
+ return `No matching symbol for "${target.query}" \u2014 check the name, or refresh the graph with /ast sync.`;
17923
+ }
17924
+ if (target.kind !== "resolved") {
17925
+ return `"${query}" is ambiguous. Pick one node id explicitly: /explain ${target.candidates[0]?.id ?? "path/file.ts#name"}`;
17926
+ }
17927
+ const anchor = target.node;
17928
+ signal.throwIfAborted();
17929
+ deps.notify?.(`Explaining ${anchor.id} \u2026`);
17930
+ const { callers, callees } = await deps.graph.relations(anchor.id, 1);
17931
+ const raw = await deps.readFile(anchor.filePath).catch(() => "");
17932
+ const lines = raw.split("\n");
17933
+ const anchorSource = lines.slice(Math.max(0, anchor.startLine - 1), anchor.endLine).join("\n") || raw;
17934
+ const history = deps.history === void 0 ? [] : await deps.history(anchor.filePath, HISTORY_LIMIT).catch(() => []);
17935
+ try {
17936
+ if (deps.onText !== void 0) deps.onText(`Explain ${anchor.id}
17937
+
17938
+ `);
17939
+ const explanation = await explainWithModel(
17940
+ { registry: deps.registry, modelId: deps.modelId },
17941
+ buildExplainPrompt({
17942
+ anchor,
17943
+ anchorSource,
17944
+ callers: callers.map((n) => ({ name: n.name, filePath: n.filePath, startLine: n.startLine })),
17945
+ callees: callees.map((n) => ({ name: n.name, filePath: n.filePath, startLine: n.startLine })),
17946
+ history,
17947
+ ...focus === void 0 ? {} : { focus },
17948
+ language: deps.language
17949
+ }),
17950
+ signal,
17951
+ deps.onText
17952
+ );
17953
+ return deps.onText === void 0 ? `Explain ${anchor.id}
17954
+
17955
+ ${explanation}` : "";
17956
+ } catch (error) {
17957
+ if (signal.aborted) throw error;
17958
+ return `/explain failed: ${error instanceof Error ? error.message : String(error)}`;
17959
+ }
17960
+ }
17961
+
17962
+ // src/desktop/astgraph-service.ts
17963
+ import { stat as stat8 } from "fs/promises";
17964
+ import { createRequire as nodeCreateRequire } from "module";
17965
+ import { join as join22, resolve as resolve22 } from "path";
17966
+ var FIELDS = "id, kind, name, qualified_name, file_path, language, start_line, end_line, signature";
17967
+ var DesktopAstGraphService = class {
17968
+ #path;
17969
+ constructor(workspace) {
17970
+ this.#path = join22(resolve22(workspace), ".flavor", "astgraph", "index.db");
17971
+ }
17972
+ async status() {
17973
+ if ((await stat8(this.#path).catch(() => void 0))?.isFile() !== true) {
17974
+ return { available: false, path: this.#path, files: 0, nodes: 0, edges: 0 };
17975
+ }
17976
+ return this.#read((db) => {
17977
+ const count = (table) => Number(db.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get().count);
17978
+ const metadata = db.prepare("SELECT value FROM project_metadata WHERE key = 'last_index'").get();
17979
+ let indexedAt;
17980
+ try {
17981
+ const at = Number(JSON.parse(metadata?.value ?? "{}").at);
17982
+ if (Number.isFinite(at)) indexedAt = new Date(at).toISOString();
17983
+ } catch {
17984
+ }
17985
+ return { available: true, path: this.#path, files: count("files"), nodes: count("nodes"), edges: count("edges"), ...indexedAt === void 0 ? {} : { indexedAt } };
17986
+ });
17987
+ }
17988
+ async search(query, limit = 40) {
17989
+ const value = query.trim().slice(0, 200);
17990
+ if (value.length === 0 || !(await this.status()).available) return [];
17991
+ return this.#read((db) => db.prepare(`SELECT ${FIELDS} FROM nodes WHERE lower(name) LIKE ? OR lower(qualified_name) LIKE ? ORDER BY CASE WHEN lower(name) = ? THEN 0 ELSE 1 END, name LIMIT ?`).all(`%${value.toLowerCase()}%`, `%${value.toLowerCase()}%`, value.toLowerCase(), Math.min(Math.max(limit, 1), 100)).map(mapNode));
17992
+ }
17993
+ async relations(id, hops = 2) {
17994
+ if (!/^[A-Za-z0-9._:/#@+~-]{1,512}$/.test(id)) throw new Error("Invalid AST node id");
17995
+ if (!(await this.status()).available) return { callers: [], callees: [], impact: [] };
17996
+ return this.#read((db) => {
17997
+ const originRow = db.prepare(`SELECT ${FIELDS} FROM nodes WHERE id = ?`).get(id);
17998
+ const linked = (direction) => {
17999
+ const source = direction === "callers" ? "e.source" : "e.target";
18000
+ const predicate = direction === "callers" ? "e.target = ?" : "e.source = ?";
18001
+ return db.prepare(`SELECT DISTINCT n.${FIELDS.split(", ").join(", n.")} FROM edges e JOIN nodes n ON n.id = ${source} WHERE ${predicate} AND e.kind IN ('calls','imports') LIMIT 100`).all(id).map(mapNode);
18002
+ };
18003
+ const seen = /* @__PURE__ */ new Map();
18004
+ let frontier = [id];
18005
+ for (let hop = 1; hop <= Math.min(Math.max(hops, 1), 4) && frontier.length > 0 && seen.size < 200; hop += 1) {
18006
+ const next = [];
18007
+ for (const current of frontier) {
18008
+ const rows = db.prepare("SELECT source AS other FROM edges WHERE target = ? UNION SELECT target AS other FROM edges WHERE source = ? LIMIT 200").all(current, current);
18009
+ for (const row of rows) if (row.other !== id && !seen.has(row.other)) {
18010
+ seen.set(row.other, hop);
18011
+ next.push(row.other);
18012
+ }
18013
+ }
18014
+ frontier = next;
18015
+ }
18016
+ const impact = [...seen].flatMap(([nodeId, hop]) => {
18017
+ const row = db.prepare(`SELECT ${FIELDS} FROM nodes WHERE id = ?`).get(nodeId);
18018
+ return row === void 0 ? [] : [{ ...mapNode(row), hop }];
18019
+ });
18020
+ return { ...originRow === void 0 ? {} : { origin: mapNode(originRow) }, callers: linked("callers"), callees: linked("callees"), impact };
18021
+ });
18022
+ }
18023
+ #read(run) {
18024
+ const sqlite = nodeCreateRequire(import.meta.url)("node:sqlite");
18025
+ const db = new sqlite.DatabaseSync(this.#path, { readOnly: true });
18026
+ try {
18027
+ return run(db);
18028
+ } finally {
18029
+ db.close();
18030
+ }
18031
+ }
18032
+ };
18033
+ function mapNode(value) {
18034
+ const row = value;
18035
+ return { id: String(row.id), kind: String(row.kind), name: String(row.name), qualifiedName: String(row.qualified_name), filePath: String(row.file_path), language: String(row.language), startLine: Number(row.start_line), endLine: Number(row.end_line), ...row.signature == null ? {} : { signature: String(row.signature) } };
18036
+ }
18037
+
17796
18038
  // src/git/tools.ts
17797
18039
  import { z as z28 } from "zod";
17798
18040
  var GitHistoryInput = z28.object({
@@ -17887,7 +18129,7 @@ var QuestionBridge = class {
17887
18129
  }
17888
18130
  }
17889
18131
  #askLocally(questions, signal) {
17890
- return new Promise((resolve28, reject) => {
18132
+ return new Promise((resolve29, reject) => {
17891
18133
  const onAbort = () => {
17892
18134
  this.#pending = void 0;
17893
18135
  this.#active = false;
@@ -17901,7 +18143,7 @@ var QuestionBridge = class {
17901
18143
  this.#pending = void 0;
17902
18144
  this.#active = false;
17903
18145
  this.#onChange?.();
17904
- resolve28(answers);
18146
+ resolve29(answers);
17905
18147
  },
17906
18148
  reject: (error) => {
17907
18149
  signal.removeEventListener("abort", onAbort);
@@ -17994,7 +18236,7 @@ function createTodoWriteTool() {
17994
18236
  // src/tools/managed.ts
17995
18237
  import { randomUUID as randomUUID16 } from "crypto";
17996
18238
  import { link as link2, lstat as lstat11, mkdir as mkdir17, readFile as readFile19, readdir as readdir8, unlink as unlink5, writeFile as writeFile10 } from "fs/promises";
17997
- import { dirname as dirname18, join as join22, resolve as resolve22 } from "path";
18239
+ import { dirname as dirname18, join as join23, resolve as resolve23 } from "path";
17998
18240
  import { z as z31 } from "zod";
17999
18241
  var MANAGED_TOOL_VERSION = 1;
18000
18242
  var MAX_RECORD_BYTES = 256 * 1024;
@@ -18036,9 +18278,9 @@ var ManagedToolStore = class {
18036
18278
  #entries = /* @__PURE__ */ new Map();
18037
18279
  #diagnostics = [];
18038
18280
  constructor(options) {
18039
- this.#workspace = resolve22(options.workspace);
18040
- this.#projectRoot = join22(this.#workspace, ".flavor", "tools");
18041
- this.#globalRoot = join22(resolve22(options.home), ".flavor-code", "tools");
18281
+ this.#workspace = resolve23(options.workspace);
18282
+ this.#projectRoot = join23(this.#workspace, ".flavor", "tools");
18283
+ this.#globalRoot = join23(resolve23(options.home), ".flavor-code", "tools");
18042
18284
  this.#now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
18043
18285
  }
18044
18286
  get diagnostics() {
@@ -18085,7 +18327,7 @@ var ManagedToolStore = class {
18085
18327
  version: MANAGED_TOOL_VERSION,
18086
18328
  createdAt: this.#now()
18087
18329
  });
18088
- const path = join22(this.directory(input.scope), filename(input.name));
18330
+ const path = join23(this.directory(input.scope), filename(input.name));
18089
18331
  const definition = compileDefinition(record, input.scope, this.#workspace);
18090
18332
  await writeExclusive(path, `${JSON.stringify(record, void 0, 2)}
18091
18333
  `);
@@ -18110,7 +18352,7 @@ var ManagedToolStore = class {
18110
18352
  if (!parsed.success) return [];
18111
18353
  const matches = this.#matching(parsed.data.name).filter((entry2) => parsed.data.scope === void 0 || entry2.scope === parsed.data.scope);
18112
18354
  if (matches.length > 0) return matches.map((entry2) => entry2.path);
18113
- return [join22(this.directory(parsed.data.scope ?? "project"), filename(parsed.data.name))];
18355
+ return [join23(this.directory(parsed.data.scope ?? "project"), filename(parsed.data.name))];
18114
18356
  }
18115
18357
  async #loadDirectory(scope2) {
18116
18358
  const root = this.directory(scope2);
@@ -18123,7 +18365,7 @@ var ManagedToolStore = class {
18123
18365
  return;
18124
18366
  }
18125
18367
  for (const name of names) {
18126
- const path = join22(root, name);
18368
+ const path = join23(root, name);
18127
18369
  try {
18128
18370
  const info = await lstat11(path);
18129
18371
  if (!info.isFile() || info.isSymbolicLink()) throw new Error("Managed tool record must be a regular file");
@@ -19042,7 +19284,7 @@ ${total}`;
19042
19284
  // src/memory/store.ts
19043
19285
  import { createHash as createHash16 } from "crypto";
19044
19286
  import { readFile as readFile20, rm as rm9 } from "fs/promises";
19045
- import { resolve as resolve23, join as join23, relative as relative17, sep as sep17 } from "path";
19287
+ import { resolve as resolve24, join as join24, relative as relative17, sep as sep17 } from "path";
19046
19288
 
19047
19289
  // src/memory/similarity.ts
19048
19290
  function normalizeForSimilarity(value) {
@@ -19144,10 +19386,10 @@ var MemoryStore = class {
19144
19386
  constructor(options) {
19145
19387
  if (!Number.isSafeInteger(options.maxEntries) || options.maxEntries < 1) throw new Error("maxEntries must be an integer of at least 1");
19146
19388
  if (!Number.isSafeInteger(options.maxEntryChars) || options.maxEntryChars < 1) throw new Error("maxEntryChars must be an integer of at least 1");
19147
- this.workspace = resolve23(options.workspace);
19148
- this.#memoryRoot = join23(this.workspace, ".flavor", "memory");
19149
- this.path = join23(this.#memoryRoot, "MEMORY.md");
19150
- this.#behaviorPath = join23(this.#memoryRoot, "behavior.json");
19389
+ this.workspace = resolve24(options.workspace);
19390
+ this.#memoryRoot = join24(this.workspace, ".flavor", "memory");
19391
+ this.path = join24(this.#memoryRoot, "MEMORY.md");
19392
+ this.#behaviorPath = join24(this.#memoryRoot, "behavior.json");
19151
19393
  this.#maxEntries = options.maxEntries;
19152
19394
  this.#maxEntryChars = options.maxEntryChars;
19153
19395
  }
@@ -19370,7 +19612,7 @@ var MemoryStore = class {
19370
19612
  }
19371
19613
  #resolveContentPath(contentPath) {
19372
19614
  if (!/^tasks\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.md$/.test(contentPath)) throw new Error("Invalid memory content path");
19373
- const path = resolve23(this.#memoryRoot, ...contentPath.split("/"));
19615
+ const path = resolve24(this.#memoryRoot, ...contentPath.split("/"));
19374
19616
  const rel = relative17(this.#memoryRoot, path);
19375
19617
  if (rel === ".." || rel.startsWith(`..${sep17}`)) throw new Error("Memory content path escapes the memory directory");
19376
19618
  return path;
@@ -19841,8 +20083,8 @@ function createExecutionEnvironment(workspace, config) {
19841
20083
  }
19842
20084
 
19843
20085
  // src/ide/client.ts
19844
- import { readFile as readFile21, readdir as readdir9, stat as stat8 } from "fs/promises";
19845
- import { basename as basename5, join as join24, relative as relative18, resolve as resolve24, sep as sep18 } from "path";
20086
+ import { readFile as readFile21, readdir as readdir9, stat as stat9 } from "fs/promises";
20087
+ import { basename as basename5, join as join25, relative as relative18, resolve as resolve25, sep as sep18 } from "path";
19846
20088
  var FlavorIdeClient = class {
19847
20089
  #workspace;
19848
20090
  #home;
@@ -19853,8 +20095,8 @@ var FlavorIdeClient = class {
19853
20095
  #sessionId;
19854
20096
  #eventTail = Promise.resolve();
19855
20097
  constructor(options) {
19856
- this.#workspace = resolve24(options.workspace);
19857
- this.#home = resolve24(options.home);
20098
+ this.#workspace = resolve25(options.workspace);
20099
+ this.#home = resolve25(options.home);
19858
20100
  this.#environment = options.environment ?? process.env;
19859
20101
  this.#fetch = options.fetch ?? fetch;
19860
20102
  }
@@ -19938,7 +20180,7 @@ var FlavorIdeClient = class {
19938
20180
  };
19939
20181
  if (await this.#healthy(configured)) return configured;
19940
20182
  }
19941
- const directory = join24(this.#home, ".flavor-code", "ide");
20183
+ const directory = join25(this.#home, ".flavor-code", "ide");
19942
20184
  let entries;
19943
20185
  try {
19944
20186
  entries = (await readdir9(directory)).filter((name) => name.endsWith(".lock"));
@@ -19946,9 +20188,9 @@ var FlavorIdeClient = class {
19946
20188
  return void 0;
19947
20189
  }
19948
20190
  const candidates = (await Promise.all(entries.map(async (name) => {
19949
- const path = join24(directory, name);
20191
+ const path = join25(directory, name);
19950
20192
  try {
19951
- const [contents, info] = await Promise.all([readFile21(path, "utf8"), stat8(path)]);
20193
+ const [contents, info] = await Promise.all([readFile21(path, "utf8"), stat9(path)]);
19952
20194
  const lock = parseLockfile(contents);
19953
20195
  return lock === void 0 ? void 0 : { lock, modified: info.mtimeMs };
19954
20196
  } catch {
@@ -20052,8 +20294,8 @@ function parsePort(value) {
20052
20294
  return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
20053
20295
  }
20054
20296
  function isWithin8(parent, child) {
20055
- const base = normalize(resolve24(parent));
20056
- const target = normalize(resolve24(child));
20297
+ const base = normalize(resolve25(parent));
20298
+ const target = normalize(resolve25(child));
20057
20299
  return target === base || target.startsWith(`${base}${sep18}`);
20058
20300
  }
20059
20301
  function normalize(path) {
@@ -20083,8 +20325,8 @@ var JobRegistry = class {
20083
20325
  const id = `job-${randomUUID17().slice(0, 12)}`;
20084
20326
  const now = (/* @__PURE__ */ new Date()).toISOString();
20085
20327
  let settle;
20086
- const done = new Promise((resolve28) => {
20087
- settle = resolve28;
20328
+ const done = new Promise((resolve29) => {
20329
+ settle = resolve29;
20088
20330
  });
20089
20331
  const record = {
20090
20332
  id,
@@ -20207,8 +20449,8 @@ function snapshot2(job) {
20207
20449
 
20208
20450
  // src/terminal/service.ts
20209
20451
  import { randomUUID as randomUUID18 } from "crypto";
20210
- import { createRequire as nodeCreateRequire } from "module";
20211
- import { isAbsolute as isAbsolute11, relative as relative19, resolve as resolve25, sep as sep19 } from "path";
20452
+ import { createRequire as nodeCreateRequire2 } from "module";
20453
+ import { isAbsolute as isAbsolute11, relative as relative19, resolve as resolve26, sep as sep19 } from "path";
20212
20454
  var TerminalService = class {
20213
20455
  #root;
20214
20456
  #factory;
@@ -20216,7 +20458,7 @@ var TerminalService = class {
20216
20458
  #maxOutputChars;
20217
20459
  #sessions = /* @__PURE__ */ new Map();
20218
20460
  constructor(workspace, options = {}) {
20219
- this.#root = resolve25(workspace);
20461
+ this.#root = resolve26(workspace);
20220
20462
  this.#factory = options.factory ?? defaultFactory;
20221
20463
  this.#jobs = options.jobs;
20222
20464
  this.#maxOutputChars = options.maxOutputChars ?? 2e5;
@@ -20321,7 +20563,7 @@ function snapshot3(record) {
20321
20563
  }
20322
20564
  function defaultFactory(shell, args, options) {
20323
20565
  try {
20324
- const pty = nodeCreateRequire(import.meta.url)("node-pty");
20566
+ const pty = nodeCreateRequire2(import.meta.url)("node-pty");
20325
20567
  return pty.spawn(shell, args, { ...options, name: "xterm-256color" });
20326
20568
  } catch (error) {
20327
20569
  throw new Error(`Pseudo-terminal backend is unavailable: ${error instanceof Error ? error.message : String(error)}`);
@@ -20334,7 +20576,7 @@ function cleanEnvironment(environment) {
20334
20576
  return Object.fromEntries(Object.entries(environment).filter((entry2) => entry2[1] !== void 0));
20335
20577
  }
20336
20578
  function workspacePath(root, input) {
20337
- const path = resolve25(root, input);
20579
+ const path = resolve26(root, input);
20338
20580
  const delta = relative19(root, path);
20339
20581
  if (delta === ".." || delta.startsWith(`..${sep19}`) || isAbsolute11(delta)) throw new Error("Terminal cwd is outside the workspace");
20340
20582
  return path;
@@ -20342,7 +20584,7 @@ function workspacePath(root, input) {
20342
20584
 
20343
20585
  // src/tools/terminal.ts
20344
20586
  import { z as z33 } from "zod";
20345
- import { resolve as resolve26 } from "path";
20587
+ import { resolve as resolve27 } from "path";
20346
20588
  var Id = z33.string().regex(/^term-[a-f0-9-]+$/);
20347
20589
  var OpenInput = z33.object({ cwd: z33.string().optional(), shell: z33.string().optional(), args: z33.array(z33.string()).optional(), columns: z33.coerce.number().int().min(10).max(500).optional(), rows: z33.coerce.number().int().min(2).max(300).optional() });
20348
20590
  var OptionalBoolean = z33.union([z33.boolean(), z33.string().refine((value) => value === "true" || value === "false")]);
@@ -20356,8 +20598,8 @@ function createTerminalTools(terminals, workspace = process.cwd()) {
20356
20598
  name: "TerminalOpen",
20357
20599
  description: "Open a persistent interactive pseudo-terminal in the workspace",
20358
20600
  inputSchema: OpenInput,
20359
- paths: (input) => [resolve26(workspace, input.cwd ?? ".")],
20360
- permissions: (input) => ({ paths: [resolve26(workspace, input.cwd ?? ".")], command: input.shell ?? "system-shell", args: input.args ?? [], cwd: resolve26(workspace, input.cwd ?? ".") }),
20601
+ paths: (input) => [resolve27(workspace, input.cwd ?? ".")],
20602
+ permissions: (input) => ({ paths: [resolve27(workspace, input.cwd ?? ".")], command: input.shell ?? "system-shell", args: input.args ?? [], cwd: resolve27(workspace, input.cwd ?? ".") }),
20361
20603
  execute: async (input, _signal, context) => terminals.open({
20362
20604
  owner: owner(context),
20363
20605
  ...input.cwd === void 0 ? {} : { cwd: input.cwd },
@@ -20583,8 +20825,8 @@ var PalClient = class {
20583
20825
  let disconnectTimer;
20584
20826
  await Promise.race([
20585
20827
  this.request({ version: 1, type: "disconnect", requestId: randomUUID19() }).catch(() => void 0),
20586
- new Promise((resolve28) => {
20587
- disconnectTimer = setTimeout(resolve28, Math.min(this.requestTimeoutMs, 250));
20828
+ new Promise((resolve29) => {
20829
+ disconnectTimer = setTimeout(resolve29, Math.min(this.requestTimeoutMs, 250));
20588
20830
  })
20589
20831
  ]);
20590
20832
  if (disconnectTimer !== void 0) clearTimeout(disconnectTimer);
@@ -20630,7 +20872,7 @@ var PalClient = class {
20630
20872
  return presence;
20631
20873
  }
20632
20874
  openSocket() {
20633
- return new Promise((resolve28, reject) => {
20875
+ return new Promise((resolve29, reject) => {
20634
20876
  let settled = false;
20635
20877
  const rejectConnection = (error) => {
20636
20878
  if (settled) return;
@@ -20653,7 +20895,7 @@ var PalClient = class {
20653
20895
  settled = true;
20654
20896
  if (this.connectingReject === rejectConnection) this.connectingReject = void 0;
20655
20897
  socket.off("error", onInitialError);
20656
- resolve28();
20898
+ resolve29();
20657
20899
  };
20658
20900
  const onInitialError = (error) => {
20659
20901
  socket.off("connect", onConnect);
@@ -20684,12 +20926,12 @@ var PalClient = class {
20684
20926
  if (this.closed || socket === void 0 || socket.destroyed || !socket.writable) {
20685
20927
  return Promise.reject(new Error(this.closed ? "Pal client is closed" : "Pal client is not connected"));
20686
20928
  }
20687
- return new Promise((resolve28, reject) => {
20929
+ return new Promise((resolve29, reject) => {
20688
20930
  const timer = setTimeout(() => {
20689
20931
  this.pending.delete(request.requestId);
20690
20932
  reject(new Error(`Pal request '${request.type}' timed out`));
20691
20933
  }, this.requestTimeoutMs);
20692
- this.pending.set(request.requestId, { resolve: resolve28, reject, timer });
20934
+ this.pending.set(request.requestId, { resolve: resolve29, reject, timer });
20693
20935
  let frame;
20694
20936
  try {
20695
20937
  frame = encodeControlFrame(BrokerRequestSchema.parse(request));
@@ -21093,8 +21335,8 @@ var ApprovalBridge = class {
21093
21335
  }
21094
21336
  };
21095
21337
  async function createProductionRuntime(options) {
21096
- const workspace = resolve27(options.workspace ?? process.cwd());
21097
- const home = resolve27(options.home ?? homedir2());
21338
+ const workspace = resolve28(options.workspace ?? process.cwd());
21339
+ const home = resolve28(options.home ?? homedir2());
21098
21340
  const environment = options.environment ?? process.env;
21099
21341
  const ide = new FlavorIdeClient({ workspace, home, environment });
21100
21342
  const ideReady = ide.initialize();
@@ -21318,8 +21560,8 @@ async function createProductionRuntime(options) {
21318
21560
  const registeredProviders = registration.registered;
21319
21561
  let effectiveLlm = registration.effectiveLlm;
21320
21562
  const pluginHost = new PluginHost({
21321
- globalPluginDirs: [join25(home, ".flavor-code", "plugins")],
21322
- projectPluginDirs: [join25(workspace, ".flavor", "plugins")],
21563
+ globalPluginDirs: [join26(home, ".flavor-code", "plugins")],
21564
+ projectPluginDirs: [join26(workspace, ".flavor", "plugins")],
21323
21565
  config,
21324
21566
  // Existing project plugins (including astgraph and superharness) use Node.js
21325
21567
  // built-ins. The Worker/vm sandbox deliberately blocks those imports, so it
@@ -21427,7 +21669,7 @@ async function createProductionRuntime(options) {
21427
21669
  const address = palSocketAddress({
21428
21670
  platform: process.platform,
21429
21671
  userScope: home,
21430
- runtimeDir: join25(tmpdir4(), `flavor-code-pals-${createHash18("sha256").update(home).digest("hex").slice(0, 16)}`)
21672
+ runtimeDir: join26(tmpdir4(), `flavor-code-pals-${createHash18("sha256").update(home).digest("hex").slice(0, 16)}`)
21431
21673
  });
21432
21674
  const baseCollaborationClient = collaboration.client ?? collaboration.createClient?.({
21433
21675
  instanceId: collaboration.instanceId,
@@ -21538,8 +21780,8 @@ async function createProductionRuntime(options) {
21538
21780
  diagnostics.push(`MCP servers could not start: ${message(error)}`);
21539
21781
  });
21540
21782
  const skills = new SkillRegistry({
21541
- globalRoots: [join25(home, ".flavor-code", "skills")],
21542
- projectRoots: [join25(workspace, ".flavor", "skills"), ...pluginSkillRoots],
21783
+ globalRoots: [join26(home, ".flavor-code", "skills")],
21784
+ projectRoots: [join26(workspace, ".flavor", "skills"), ...pluginSkillRoots],
21543
21785
  authorizeResource: async () => true,
21544
21786
  disabledNames: config.skills.disabled
21545
21787
  });
@@ -21547,7 +21789,7 @@ async function createProductionRuntime(options) {
21547
21789
  skillsReady.catch(() => void 0);
21548
21790
  tools.push(createSkillTool(skills), createSkillResourceTool(skills));
21549
21791
  if (options.extraTools !== void 0) tools.push(...options.extraTools);
21550
- const flavor = await optionalText2(join25(workspace, "FLAVOR.md"));
21792
+ const flavor = await optionalText2(join26(workspace, "FLAVOR.md"));
21551
21793
  const instructionBaseline = await workspaceInstructions.baseline();
21552
21794
  let memoryContext;
21553
21795
  if (memoryStore !== void 0) {
@@ -22125,7 +22367,7 @@ async function createProductionRuntime(options) {
22125
22367
  evolveService.toolDefinition(),
22126
22368
  ...mcpTools
22127
22369
  ];
22128
- const loopFlavor = await optionalText2(join25(input.workspace, "FLAVOR.md"));
22370
+ const loopFlavor = await optionalText2(join26(input.workspace, "FLAVOR.md"));
22129
22371
  const loopInstructionBaseline = await loopInstructions.baseline();
22130
22372
  const loopEnvironment = createPromptEnvironment({
22131
22373
  now: /* @__PURE__ */ new Date(),
@@ -22485,6 +22727,22 @@ async function createProductionRuntime(options) {
22485
22727
  modelId: () => harness.subagentModelId,
22486
22728
  notify: (message2) => emitOutput({ type: "notice", message: message2 })
22487
22729
  }, focus, signal),
22730
+ explain: (query, focus, signal, onText) => runExplain({
22731
+ graph: new DesktopAstGraphService(workspace),
22732
+ readFile: async (path) => {
22733
+ const root = resolve28(workspace);
22734
+ const abs = resolve28(root, path);
22735
+ if (abs !== root && !abs.startsWith(root + sep20)) throw new Error(`path outside workspace: ${path}`);
22736
+ return readFile22(abs, "utf8");
22737
+ },
22738
+ history: async (path, limit) => fileHistory(workspace, path, limit),
22739
+ questions,
22740
+ registry,
22741
+ modelId: () => harness.subagentModelId,
22742
+ language,
22743
+ notify: (message2) => emitOutput({ type: "notice", message: message2 }),
22744
+ ...onText === void 0 ? {} : { onText }
22745
+ }, query, focus, signal),
22488
22746
  audit: async (toolFilter) => {
22489
22747
  try {
22490
22748
  const raw = await readFile22(auditLogger.path, "utf8");
@@ -22674,7 +22932,7 @@ ${renderMemoryDocument(entries)}`;
22674
22932
  return `Provider "${providerName}" is missing authorizationUrl, tokenUrl, or clientId.`;
22675
22933
  }
22676
22934
  try {
22677
- const tokenStore = createFileTokenStore(join25(home, ".flavor-code", "auth.json"));
22935
+ const tokenStore = createFileTokenStore(join26(home, ".flavor-code", "auth.json"));
22678
22936
  const credentialId = oauthCredentialId(oauthConfig.tokenUrl, oauthConfig.clientId);
22679
22937
  await migrateLegacyOAuthToken(tokenStore, providerName, credentialId);
22680
22938
  const oauth = new OAuthCallbackAuthProvider({
@@ -22721,7 +22979,7 @@ ${renderMemoryDocument(entries)}`;
22721
22979
  }
22722
22980
  },
22723
22981
  async logout() {
22724
- const tokenStore = createFileTokenStore(join25(home, ".flavor-code", "auth.json"));
22982
+ const tokenStore = createFileTokenStore(join26(home, ".flavor-code", "auth.json"));
22725
22983
  const tokens = await tokenStore.load();
22726
22984
  const hadCredentials = Object.keys(tokens).length > 0 || effectiveLlm !== void 0;
22727
22985
  if (!hadCredentials) {
@@ -22997,8 +23255,8 @@ async function workspaceFingerprint(workspace) {
22997
23255
  let remaining = 5 * 1024 * 1024;
22998
23256
  for (const name of untracked.stdout.split("\0").filter(Boolean).sort()) {
22999
23257
  if (name === ".flavor/loops" || name.startsWith(".flavor/loops/")) continue;
23000
- const path = resolve27(workspace, name);
23001
- const relativePath = relative20(resolve27(workspace), path);
23258
+ const path = resolve28(workspace, name);
23259
+ const relativePath = relative20(resolve28(workspace), path);
23002
23260
  if (relativePath === ".." || relativePath.startsWith(`..${sep20}`)) continue;
23003
23261
  hash.update(name);
23004
23262
  if (remaining <= 0) continue;
@@ -23216,7 +23474,7 @@ async function registerConfiguredAdapters(providers, registry, environment, diag
23216
23474
  const configured = { ...providers };
23217
23475
  if (configured.openai === void 0 && environment.OPENAI_API_KEY) configured.openai = { type: "openai", apiKey: environment.OPENAI_API_KEY };
23218
23476
  if (configured.anthropic === void 0 && environment.ANTHROPIC_API_KEY) configured.anthropic = { type: "anthropic", apiKey: environment.ANTHROPIC_API_KEY };
23219
- const oauthTokenStore = createFileTokenStore(join25(home, ".flavor-code", "auth.json"));
23477
+ const oauthTokenStore = createFileTokenStore(join26(home, ".flavor-code", "auth.json"));
23220
23478
  const registered = [];
23221
23479
  let effectiveLlm;
23222
23480
  for (const [name, provider] of Object.entries(configured)) {