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.
@@ -4227,7 +4227,7 @@ ${summarizeReport(report2)}`).join("\n\n")
4227
4227
  import { readFile as readFile27 } from "fs/promises";
4228
4228
  import { createHash as createHash21, randomUUID as randomUUID25 } from "crypto";
4229
4229
  import { homedir as homedir2, release as osRelease, tmpdir as tmpdir4, version as osVersion } from "os";
4230
- import { basename as basename8, join as join32, relative as relative23, resolve as resolve32, sep as sep23 } from "path";
4230
+ import { basename as basename8, join as join33, relative as relative23, resolve as resolve33, sep as sep23 } from "path";
4231
4231
 
4232
4232
  // src/agent/subagents.ts
4233
4233
  import { z as z10 } from "zod";
@@ -19684,6 +19684,232 @@ function formatReviewReport(report) {
19684
19684
  return lines.join("\n");
19685
19685
  }
19686
19686
 
19687
+ // src/explain/service.ts
19688
+ async function resolveExplainTarget(graph, query) {
19689
+ const trimmed = query.trim();
19690
+ if (trimmed.includes("#")) {
19691
+ const hit = await graph.relations(trimmed).catch(() => void 0);
19692
+ if (hit?.origin !== void 0) return { kind: "resolved", node: hit.origin };
19693
+ }
19694
+ const hits = await graph.search(trimmed, 10);
19695
+ if (hits.length === 0) return { kind: "not-found", query: trimmed };
19696
+ if (hits.length === 1) return { kind: "resolved", node: hits[0] };
19697
+ return { kind: "ambiguous", candidates: hits };
19698
+ }
19699
+ function explainCandidateLabel(candidate) {
19700
+ return candidate.id;
19701
+ }
19702
+ async function selectExplainCandidate(candidates, questions, signal) {
19703
+ const shown = candidates.slice(0, 3);
19704
+ const answers = await questions.ask([{
19705
+ header: "Symbol",
19706
+ question: "Multiple symbols match. Which one should /explain cover? (Pick one, or type a more exact name.)",
19707
+ options: [
19708
+ ...shown.map((candidate) => ({
19709
+ label: explainCandidateLabel(candidate),
19710
+ description: `${candidate.kind} \xB7 ${candidate.filePath}:${candidate.startLine}-${candidate.endLine}`
19711
+ })),
19712
+ { label: "Cancel", description: "Abort /explain without choosing." }
19713
+ ]
19714
+ }], signal);
19715
+ const answer = (answers[0] ?? "").trim();
19716
+ if (answer === "Cancel" || answer === "") return { kind: "cancelled" };
19717
+ const picked = shown.find((candidate) => explainCandidateLabel(candidate) === answer);
19718
+ if (picked !== void 0) return { kind: "picked", node: picked };
19719
+ return { kind: "typed", query: answer };
19720
+ }
19721
+ var MAX_SOURCE_CHARS = 2e4;
19722
+ function buildExplainPrompt(input) {
19723
+ const source = input.anchorSource.length > MAX_SOURCE_CHARS ? `${input.anchorSource.slice(0, MAX_SOURCE_CHARS)}
19724
+ \u2026 (truncated)` : input.anchorSource;
19725
+ const line = (n) => `- ${n.name} \u2014 ${n.filePath}:${n.startLine}`;
19726
+ return [
19727
+ "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.",
19728
+ `Answer in ${input.language}.`,
19729
+ "Structure the answer with short markdown sections:",
19730
+ "1. \u5B83\u662F\u505A\u4EC0\u4E48\u7684 \u2014 one paragraph, plain language, beginner first.",
19731
+ "2. \u5173\u952E\u5B9E\u73B0\u70B9 \u2014 2-5 bullets tied to concrete lines/branches in the source.",
19732
+ "3. \u8C01\u8C03\u7528\u5B83 / \u5B83\u8C03\u7528\u8C01 \u2014 use the relation lists; explain the data flow in one or two sentences.",
19733
+ "4. \u4E3A\u4EC0\u4E48\u8FD9\u6837\u5199 \u2014 infer from the recent commit subjects when possible; say '\u4E0D\u786E\u5B9A' instead of guessing.",
19734
+ "5. \u65B0\u4EBA\u6CE8\u610F\u4E8B\u9879 \u2014 pitfalls, guards or invariants worth knowing.",
19735
+ ...input.focus === void 0 || input.focus.trim() === "" ? [] : [`The user specifically asks about: ${input.focus.trim()}`],
19736
+ "",
19737
+ `Symbol: ${input.anchor.id} (${input.anchor.kind}${input.anchor.signature ? `, signature: ${input.anchor.signature}` : ""})`,
19738
+ "",
19739
+ `Source (${input.anchor.filePath}:${input.anchor.startLine}-${input.anchor.endLine}):`,
19740
+ source,
19741
+ "",
19742
+ `Callers:
19743
+ ${input.callers.map(line).join("\n") || "- (none)"}`,
19744
+ `Callees:
19745
+ ${input.callees.map(line).join("\n") || "- (none)"}`,
19746
+ "",
19747
+ `Recent commits touching the file:
19748
+ ${input.history.map((h) => `- ${h.date} ${h.author}: ${h.subject}`).join("\n") || "- (no history)"}`
19749
+ ].join("\n");
19750
+ }
19751
+ async function explainWithModel(options, prompt, signal, onText) {
19752
+ const { adapter, model } = options.registry.get(options.modelId());
19753
+ let text = "";
19754
+ for await (const event of adapter.stream({
19755
+ model,
19756
+ messages: [{ role: "user", content: prompt }],
19757
+ tools: [],
19758
+ signal
19759
+ })) {
19760
+ if (event.type === "text") {
19761
+ text += event.text;
19762
+ onText?.(event.text);
19763
+ } else if (event.type === "error") throw new Error(`explain generation failed: ${event.error.message}`);
19764
+ else if (event.type === "done") break;
19765
+ }
19766
+ const cleaned = text.trim();
19767
+ if (cleaned === "") throw new Error("explain generation returned no text");
19768
+ return cleaned;
19769
+ }
19770
+ var HISTORY_LIMIT = 8;
19771
+ var MAX_RESOLVE_ROUNDS = 2;
19772
+ async function runExplain(deps, query, focus, signal) {
19773
+ if (query === void 0) {
19774
+ return "Usage: /explain <symbol | file.ts#symbol> [what to focus on]. Example: /explain src/order.ts#cancelOrder error handling";
19775
+ }
19776
+ if (!(await deps.graph.status()).available) {
19777
+ return "The code graph is not built yet \u2014 run /ast init, then try /explain again.";
19778
+ }
19779
+ let target = await resolveExplainTarget(deps.graph, query);
19780
+ for (let round = 1; round <= MAX_RESOLVE_ROUNDS; round += 1) {
19781
+ if (target.kind !== "ambiguous") break;
19782
+ if (deps.questions === void 0) {
19783
+ return `"${query}" is ambiguous. Use the exact node id, e.g. /explain ${target.candidates[0]?.id ?? "path/file.ts#name"}.`;
19784
+ }
19785
+ const selection = await selectExplainCandidate(target.candidates, deps.questions, signal);
19786
+ if (selection.kind === "cancelled") return "/explain cancelled \u2014 no symbol was selected.";
19787
+ if (selection.kind === "picked") {
19788
+ target = { kind: "resolved", node: selection.node };
19789
+ break;
19790
+ }
19791
+ target = await resolveExplainTarget(deps.graph, selection.query);
19792
+ if (round === MAX_RESOLVE_ROUNDS && target.kind === "ambiguous") {
19793
+ return `"${selection.query}" is still ambiguous. Pick one node id explicitly: /explain ${target.candidates[0]?.id ?? "path/file.ts#name"}`;
19794
+ }
19795
+ }
19796
+ if (target.kind === "not-found") {
19797
+ return `No matching symbol for "${target.query}" \u2014 check the name, or refresh the graph with /ast sync.`;
19798
+ }
19799
+ if (target.kind !== "resolved") {
19800
+ return `"${query}" is ambiguous. Pick one node id explicitly: /explain ${target.candidates[0]?.id ?? "path/file.ts#name"}`;
19801
+ }
19802
+ const anchor = target.node;
19803
+ signal.throwIfAborted();
19804
+ deps.notify?.(`Explaining ${anchor.id} \u2026`);
19805
+ const { callers, callees } = await deps.graph.relations(anchor.id, 1);
19806
+ const raw = await deps.readFile(anchor.filePath).catch(() => "");
19807
+ const lines = raw.split("\n");
19808
+ const anchorSource = lines.slice(Math.max(0, anchor.startLine - 1), anchor.endLine).join("\n") || raw;
19809
+ const history = deps.history === void 0 ? [] : await deps.history(anchor.filePath, HISTORY_LIMIT).catch(() => []);
19810
+ try {
19811
+ if (deps.onText !== void 0) deps.onText(`Explain ${anchor.id}
19812
+
19813
+ `);
19814
+ const explanation = await explainWithModel(
19815
+ { registry: deps.registry, modelId: deps.modelId },
19816
+ buildExplainPrompt({
19817
+ anchor,
19818
+ anchorSource,
19819
+ callers: callers.map((n) => ({ name: n.name, filePath: n.filePath, startLine: n.startLine })),
19820
+ callees: callees.map((n) => ({ name: n.name, filePath: n.filePath, startLine: n.startLine })),
19821
+ history,
19822
+ ...focus === void 0 ? {} : { focus },
19823
+ language: deps.language
19824
+ }),
19825
+ signal,
19826
+ deps.onText
19827
+ );
19828
+ return deps.onText === void 0 ? `Explain ${anchor.id}
19829
+
19830
+ ${explanation}` : "";
19831
+ } catch (error) {
19832
+ if (signal.aborted) throw error;
19833
+ return `/explain failed: ${error instanceof Error ? error.message : String(error)}`;
19834
+ }
19835
+ }
19836
+
19837
+ // src/desktop/astgraph-service.ts
19838
+ import { stat as stat10 } from "fs/promises";
19839
+ import { createRequire as nodeCreateRequire } from "module";
19840
+ import { join as join27, resolve as resolve26 } from "path";
19841
+ var FIELDS = "id, kind, name, qualified_name, file_path, language, start_line, end_line, signature";
19842
+ var DesktopAstGraphService = class {
19843
+ #path;
19844
+ constructor(workspace) {
19845
+ this.#path = join27(resolve26(workspace), ".flavor", "astgraph", "index.db");
19846
+ }
19847
+ async status() {
19848
+ if ((await stat10(this.#path).catch(() => void 0))?.isFile() !== true) {
19849
+ return { available: false, path: this.#path, files: 0, nodes: 0, edges: 0 };
19850
+ }
19851
+ return this.#read((db) => {
19852
+ const count = (table) => Number(db.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get().count);
19853
+ const metadata = db.prepare("SELECT value FROM project_metadata WHERE key = 'last_index'").get();
19854
+ let indexedAt;
19855
+ try {
19856
+ const at = Number(JSON.parse(metadata?.value ?? "{}").at);
19857
+ if (Number.isFinite(at)) indexedAt = new Date(at).toISOString();
19858
+ } catch {
19859
+ }
19860
+ return { available: true, path: this.#path, files: count("files"), nodes: count("nodes"), edges: count("edges"), ...indexedAt === void 0 ? {} : { indexedAt } };
19861
+ });
19862
+ }
19863
+ async search(query, limit = 40) {
19864
+ const value = query.trim().slice(0, 200);
19865
+ if (value.length === 0 || !(await this.status()).available) return [];
19866
+ 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));
19867
+ }
19868
+ async relations(id, hops = 2) {
19869
+ if (!/^[A-Za-z0-9._:/#@+~-]{1,512}$/.test(id)) throw new Error("Invalid AST node id");
19870
+ if (!(await this.status()).available) return { callers: [], callees: [], impact: [] };
19871
+ return this.#read((db) => {
19872
+ const originRow = db.prepare(`SELECT ${FIELDS} FROM nodes WHERE id = ?`).get(id);
19873
+ const linked = (direction) => {
19874
+ const source = direction === "callers" ? "e.source" : "e.target";
19875
+ const predicate = direction === "callers" ? "e.target = ?" : "e.source = ?";
19876
+ 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);
19877
+ };
19878
+ const seen = /* @__PURE__ */ new Map();
19879
+ let frontier = [id];
19880
+ for (let hop = 1; hop <= Math.min(Math.max(hops, 1), 4) && frontier.length > 0 && seen.size < 200; hop += 1) {
19881
+ const next = [];
19882
+ for (const current of frontier) {
19883
+ 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);
19884
+ for (const row of rows) if (row.other !== id && !seen.has(row.other)) {
19885
+ seen.set(row.other, hop);
19886
+ next.push(row.other);
19887
+ }
19888
+ }
19889
+ frontier = next;
19890
+ }
19891
+ const impact = [...seen].flatMap(([nodeId, hop]) => {
19892
+ const row = db.prepare(`SELECT ${FIELDS} FROM nodes WHERE id = ?`).get(nodeId);
19893
+ return row === void 0 ? [] : [{ ...mapNode(row), hop }];
19894
+ });
19895
+ return { ...originRow === void 0 ? {} : { origin: mapNode(originRow) }, callers: linked("callers"), callees: linked("callees"), impact };
19896
+ });
19897
+ }
19898
+ #read(run) {
19899
+ const sqlite = nodeCreateRequire(import.meta.url)("node:sqlite");
19900
+ const db = new sqlite.DatabaseSync(this.#path, { readOnly: true });
19901
+ try {
19902
+ return run(db);
19903
+ } finally {
19904
+ db.close();
19905
+ }
19906
+ }
19907
+ };
19908
+ function mapNode(value) {
19909
+ const row = value;
19910
+ 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) } };
19911
+ }
19912
+
19687
19913
  // src/git/tools.ts
19688
19914
  import { z as z34 } from "zod";
19689
19915
  var GitHistoryInput = z34.object({
@@ -19885,7 +20111,7 @@ function createTodoWriteTool() {
19885
20111
  // src/tools/managed.ts
19886
20112
  import { randomUUID as randomUUID18 } from "crypto";
19887
20113
  import { link, lstat as lstat11, mkdir as mkdir20, readFile as readFile22, readdir as readdir9, unlink as unlink3, writeFile as writeFile12 } from "fs/promises";
19888
- import { dirname as dirname21, join as join27, resolve as resolve26 } from "path";
20114
+ import { dirname as dirname21, join as join28, resolve as resolve27 } from "path";
19889
20115
  import { z as z37 } from "zod";
19890
20116
  var MANAGED_TOOL_VERSION = 1;
19891
20117
  var MAX_RECORD_BYTES = 256 * 1024;
@@ -19927,9 +20153,9 @@ var ManagedToolStore = class {
19927
20153
  #entries = /* @__PURE__ */ new Map();
19928
20154
  #diagnostics = [];
19929
20155
  constructor(options) {
19930
- this.#workspace = resolve26(options.workspace);
19931
- this.#projectRoot = join27(this.#workspace, ".flavor", "tools");
19932
- this.#globalRoot = join27(resolve26(options.home), ".flavor-code", "tools");
20156
+ this.#workspace = resolve27(options.workspace);
20157
+ this.#projectRoot = join28(this.#workspace, ".flavor", "tools");
20158
+ this.#globalRoot = join28(resolve27(options.home), ".flavor-code", "tools");
19933
20159
  this.#now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
19934
20160
  }
19935
20161
  get diagnostics() {
@@ -19976,7 +20202,7 @@ var ManagedToolStore = class {
19976
20202
  version: MANAGED_TOOL_VERSION,
19977
20203
  createdAt: this.#now()
19978
20204
  });
19979
- const path = join27(this.directory(input.scope), filename(input.name));
20205
+ const path = join28(this.directory(input.scope), filename(input.name));
19980
20206
  const definition = compileDefinition(record3, input.scope, this.#workspace);
19981
20207
  await writeExclusive(path, `${JSON.stringify(record3, void 0, 2)}
19982
20208
  `);
@@ -20001,7 +20227,7 @@ var ManagedToolStore = class {
20001
20227
  if (!parsed.success) return [];
20002
20228
  const matches = this.#matching(parsed.data.name).filter((entry2) => parsed.data.scope === void 0 || entry2.scope === parsed.data.scope);
20003
20229
  if (matches.length > 0) return matches.map((entry2) => entry2.path);
20004
- return [join27(this.directory(parsed.data.scope ?? "project"), filename(parsed.data.name))];
20230
+ return [join28(this.directory(parsed.data.scope ?? "project"), filename(parsed.data.name))];
20005
20231
  }
20006
20232
  async #loadDirectory(scope2) {
20007
20233
  const root = this.directory(scope2);
@@ -20014,7 +20240,7 @@ var ManagedToolStore = class {
20014
20240
  return;
20015
20241
  }
20016
20242
  for (const name of names) {
20017
- const path = join27(root, name);
20243
+ const path = join28(root, name);
20018
20244
  try {
20019
20245
  const info = await lstat11(path);
20020
20246
  if (!info.isFile() || info.isSymbolicLink()) throw new Error("Managed tool record must be a regular file");
@@ -20603,6 +20829,7 @@ var MVP_COMMANDS = [
20603
20829
  "mcp",
20604
20830
  "commit",
20605
20831
  "review",
20832
+ "explain",
20606
20833
  "ide",
20607
20834
  "memory",
20608
20835
  "remember",
@@ -20678,6 +20905,12 @@ function parseSlashCommand(input, dynamicCommands = [], skillCommands = [], mana
20678
20905
  const focus = args.join(" ").trim();
20679
20906
  return focus.length === 0 ? { name } : { name, focus };
20680
20907
  }
20908
+ if (name === "explain") {
20909
+ const [query, ...focusParts] = args;
20910
+ if (query === void 0) return { name };
20911
+ const focus = focusParts.join(" ").trim();
20912
+ return focus.length === 0 ? { name, query } : { name, query, focus };
20913
+ }
20681
20914
  if (name === "loop") {
20682
20915
  const goal = args.join(" ").trim();
20683
20916
  if (!goal) return { name: "invalid", command: name, message: "Use /loop <goal>." };
@@ -21281,6 +21514,14 @@ var FlavorSession = class {
21281
21514
  this.#notice(await required(this.#services.gitCommit, "commit")(command.hint, signal));
21282
21515
  } else if (command.name === "review") {
21283
21516
  this.#notice(await required(this.#services.gitReview, "review")(command.focus, signal));
21517
+ } else if (command.name === "explain") {
21518
+ const result = await required(this.#services.explain, "explain")(
21519
+ command.query,
21520
+ command.focus,
21521
+ signal,
21522
+ (chunk) => this.#services.output({ type: "text", text: chunk })
21523
+ );
21524
+ if (result !== "") this.#notice(result);
21284
21525
  } else if (command.name === "mcp") {
21285
21526
  this.#notice(await this.#services.mcp(command, signal));
21286
21527
  } else if (command.name === "pals") {
@@ -22317,7 +22558,7 @@ ${total}`;
22317
22558
  // src/memory/store.ts
22318
22559
  import { createHash as createHash17 } from "crypto";
22319
22560
  import { readFile as readFile23, rm as rm12 } from "fs/promises";
22320
- import { resolve as resolve27, join as join28, relative as relative19, sep as sep19 } from "path";
22561
+ import { resolve as resolve28, join as join29, relative as relative19, sep as sep19 } from "path";
22321
22562
 
22322
22563
  // src/memory/similarity.ts
22323
22564
  function normalizeForSimilarity(value) {
@@ -22419,10 +22660,10 @@ var MemoryStore = class {
22419
22660
  constructor(options) {
22420
22661
  if (!Number.isSafeInteger(options.maxEntries) || options.maxEntries < 1) throw new Error("maxEntries must be an integer of at least 1");
22421
22662
  if (!Number.isSafeInteger(options.maxEntryChars) || options.maxEntryChars < 1) throw new Error("maxEntryChars must be an integer of at least 1");
22422
- this.workspace = resolve27(options.workspace);
22423
- this.#memoryRoot = join28(this.workspace, ".flavor", "memory");
22424
- this.path = join28(this.#memoryRoot, "MEMORY.md");
22425
- this.#behaviorPath = join28(this.#memoryRoot, "behavior.json");
22663
+ this.workspace = resolve28(options.workspace);
22664
+ this.#memoryRoot = join29(this.workspace, ".flavor", "memory");
22665
+ this.path = join29(this.#memoryRoot, "MEMORY.md");
22666
+ this.#behaviorPath = join29(this.#memoryRoot, "behavior.json");
22426
22667
  this.#maxEntries = options.maxEntries;
22427
22668
  this.#maxEntryChars = options.maxEntryChars;
22428
22669
  }
@@ -22645,7 +22886,7 @@ var MemoryStore = class {
22645
22886
  }
22646
22887
  #resolveContentPath(contentPath) {
22647
22888
  if (!/^tasks\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.md$/.test(contentPath)) throw new Error("Invalid memory content path");
22648
- const path = resolve27(this.#memoryRoot, ...contentPath.split("/"));
22889
+ const path = resolve28(this.#memoryRoot, ...contentPath.split("/"));
22649
22890
  const rel = relative19(this.#memoryRoot, path);
22650
22891
  if (rel === ".." || rel.startsWith(`..${sep19}`)) throw new Error("Memory content path escapes the memory directory");
22651
22892
  return path;
@@ -23105,12 +23346,12 @@ var MemoryReviewBridge = class {
23105
23346
 
23106
23347
  // src/execution/docker.ts
23107
23348
  import { spawn as spawn6 } from "child_process";
23108
- import { relative as relative20, resolve as resolve28, sep as sep20, win32 } from "path";
23349
+ import { relative as relative20, resolve as resolve29, sep as sep20, win32 } from "path";
23109
23350
  function buildDockerInvocation(options) {
23110
23351
  const workspace = options.workspace;
23111
23352
  const cwd = options.request.cwd;
23112
23353
  const windows = /^[A-Za-z]:[\\/]/.test(workspace);
23113
- const delta = windows ? win32.relative(workspace, cwd) : relative20(resolve28(workspace), resolve28(cwd));
23354
+ const delta = windows ? win32.relative(workspace, cwd) : relative20(resolve29(workspace), resolve29(cwd));
23114
23355
  if (delta === ".." || delta.startsWith(`..${windows ? "\\" : sep20}`)) throw new Error("Docker cwd escapes workspace");
23115
23356
  const containerCwd = delta.length === 0 ? "/workspace" : `/workspace/${delta.replaceAll("\\", "/")}`;
23116
23357
  const args = [
@@ -23210,8 +23451,8 @@ function createExecutionEnvironment(workspace, config) {
23210
23451
  }
23211
23452
 
23212
23453
  // src/ide/client.ts
23213
- import { readFile as readFile24, readdir as readdir10, stat as stat10 } from "fs/promises";
23214
- import { basename as basename7, join as join29, relative as relative21, resolve as resolve29, sep as sep21 } from "path";
23454
+ import { readFile as readFile24, readdir as readdir10, stat as stat11 } from "fs/promises";
23455
+ import { basename as basename7, join as join30, relative as relative21, resolve as resolve30, sep as sep21 } from "path";
23215
23456
  var FlavorIdeClient = class {
23216
23457
  #workspace;
23217
23458
  #home;
@@ -23222,8 +23463,8 @@ var FlavorIdeClient = class {
23222
23463
  #sessionId;
23223
23464
  #eventTail = Promise.resolve();
23224
23465
  constructor(options) {
23225
- this.#workspace = resolve29(options.workspace);
23226
- this.#home = resolve29(options.home);
23466
+ this.#workspace = resolve30(options.workspace);
23467
+ this.#home = resolve30(options.home);
23227
23468
  this.#environment = options.environment ?? process.env;
23228
23469
  this.#fetch = options.fetch ?? fetch;
23229
23470
  }
@@ -23307,7 +23548,7 @@ var FlavorIdeClient = class {
23307
23548
  };
23308
23549
  if (await this.#healthy(configured)) return configured;
23309
23550
  }
23310
- const directory = join29(this.#home, ".flavor-code", "ide");
23551
+ const directory = join30(this.#home, ".flavor-code", "ide");
23311
23552
  let entries;
23312
23553
  try {
23313
23554
  entries = (await readdir10(directory)).filter((name) => name.endsWith(".lock"));
@@ -23315,9 +23556,9 @@ var FlavorIdeClient = class {
23315
23556
  return void 0;
23316
23557
  }
23317
23558
  const candidates = (await Promise.all(entries.map(async (name) => {
23318
- const path = join29(directory, name);
23559
+ const path = join30(directory, name);
23319
23560
  try {
23320
- const [contents, info] = await Promise.all([readFile24(path, "utf8"), stat10(path)]);
23561
+ const [contents, info] = await Promise.all([readFile24(path, "utf8"), stat11(path)]);
23321
23562
  const lock = parseLockfile(contents);
23322
23563
  return lock === void 0 ? void 0 : { lock, modified: info.mtimeMs };
23323
23564
  } catch {
@@ -23421,8 +23662,8 @@ function parsePort(value) {
23421
23662
  return Number.isInteger(port2) && port2 > 0 && port2 <= 65535 ? port2 : void 0;
23422
23663
  }
23423
23664
  function isWithin8(parent, child) {
23424
- const base = normalize(resolve29(parent));
23425
- const target = normalize(resolve29(child));
23665
+ const base = normalize(resolve30(parent));
23666
+ const target = normalize(resolve30(child));
23426
23667
  return target === base || target.startsWith(`${base}${sep21}`);
23427
23668
  }
23428
23669
  function normalize(path) {
@@ -23576,8 +23817,8 @@ function snapshot2(job) {
23576
23817
 
23577
23818
  // src/terminal/service.ts
23578
23819
  import { randomUUID as randomUUID20 } from "crypto";
23579
- import { createRequire as nodeCreateRequire } from "module";
23580
- import { isAbsolute as isAbsolute14, relative as relative22, resolve as resolve30, sep as sep22 } from "path";
23820
+ import { createRequire as nodeCreateRequire2 } from "module";
23821
+ import { isAbsolute as isAbsolute14, relative as relative22, resolve as resolve31, sep as sep22 } from "path";
23581
23822
  var TerminalService = class {
23582
23823
  #root;
23583
23824
  #factory;
@@ -23585,7 +23826,7 @@ var TerminalService = class {
23585
23826
  #maxOutputChars;
23586
23827
  #sessions = /* @__PURE__ */ new Map();
23587
23828
  constructor(workspace, options = {}) {
23588
- this.#root = resolve30(workspace);
23829
+ this.#root = resolve31(workspace);
23589
23830
  this.#factory = options.factory ?? defaultFactory;
23590
23831
  this.#jobs = options.jobs;
23591
23832
  this.#maxOutputChars = options.maxOutputChars ?? 2e5;
@@ -23690,7 +23931,7 @@ function snapshot3(record3) {
23690
23931
  }
23691
23932
  function defaultFactory(shell2, args, options) {
23692
23933
  try {
23693
- const pty = nodeCreateRequire(import.meta.url)("node-pty");
23934
+ const pty = nodeCreateRequire2(import.meta.url)("node-pty");
23694
23935
  return pty.spawn(shell2, args, { ...options, name: "xterm-256color" });
23695
23936
  } catch (error) {
23696
23937
  throw new Error(`Pseudo-terminal backend is unavailable: ${error instanceof Error ? error.message : String(error)}`);
@@ -23703,7 +23944,7 @@ function cleanEnvironment(environment) {
23703
23944
  return Object.fromEntries(Object.entries(environment).filter((entry2) => entry2[1] !== void 0));
23704
23945
  }
23705
23946
  function workspacePath(root, input) {
23706
- const path = resolve30(root, input);
23947
+ const path = resolve31(root, input);
23707
23948
  const delta = relative22(root, path);
23708
23949
  if (delta === ".." || delta.startsWith(`..${sep22}`) || isAbsolute14(delta)) throw new Error("Terminal cwd is outside the workspace");
23709
23950
  return path;
@@ -23711,7 +23952,7 @@ function workspacePath(root, input) {
23711
23952
 
23712
23953
  // src/tools/terminal.ts
23713
23954
  import { z as z40 } from "zod";
23714
- import { resolve as resolve31 } from "path";
23955
+ import { resolve as resolve32 } from "path";
23715
23956
  var Id = z40.string().regex(/^term-[a-f0-9-]+$/);
23716
23957
  var OpenInput = z40.object({ cwd: z40.string().optional(), shell: z40.string().optional(), args: z40.array(z40.string()).optional(), columns: z40.coerce.number().int().min(10).max(500).optional(), rows: z40.coerce.number().int().min(2).max(300).optional() });
23717
23958
  var OptionalBoolean = z40.union([z40.boolean(), z40.string().refine((value) => value === "true" || value === "false")]);
@@ -23725,8 +23966,8 @@ function createTerminalTools(terminals, workspace = process.cwd()) {
23725
23966
  name: "TerminalOpen",
23726
23967
  description: "Open a persistent interactive pseudo-terminal in the workspace",
23727
23968
  inputSchema: OpenInput,
23728
- paths: (input) => [resolve31(workspace, input.cwd ?? ".")],
23729
- permissions: (input) => ({ paths: [resolve31(workspace, input.cwd ?? ".")], command: input.shell ?? "system-shell", args: input.args ?? [], cwd: resolve31(workspace, input.cwd ?? ".") }),
23969
+ paths: (input) => [resolve32(workspace, input.cwd ?? ".")],
23970
+ permissions: (input) => ({ paths: [resolve32(workspace, input.cwd ?? ".")], command: input.shell ?? "system-shell", args: input.args ?? [], cwd: resolve32(workspace, input.cwd ?? ".") }),
23730
23971
  execute: async (input, _signal, context) => terminals.open({
23731
23972
  owner: owner(context),
23732
23973
  ...input.cwd === void 0 ? {} : { cwd: input.cwd },
@@ -23822,7 +24063,7 @@ function palSocketAddress(options) {
23822
24063
  import { createHash as createHash20 } from "crypto";
23823
24064
  import { Socket as Socket2 } from "net";
23824
24065
  import { tmpdir as tmpdir3 } from "os";
23825
- import { join as join31 } from "path";
24066
+ import { join as join32 } from "path";
23826
24067
  import { spawn as spawn7 } from "child_process";
23827
24068
 
23828
24069
  // src/pals/broker.ts
@@ -23835,16 +24076,16 @@ import { dirname as dirname23 } from "path";
23835
24076
  import { randomBytes as randomBytes3, randomUUID as randomUUID21 } from "crypto";
23836
24077
  import { chmod as chmod2, link as link2, mkdir as mkdir21, open as open12, readFile as readFile25, unlink as unlink4 } from "fs/promises";
23837
24078
  import { homedir } from "os";
23838
- import { join as join30 } from "path";
24079
+ import { join as join31 } from "path";
23839
24080
  var PAL_AUTH_TOKEN_BYTES = 32;
23840
24081
  var TOKEN_PATTERN = /^[a-f0-9]{64}$/;
23841
24082
  async function loadOrCreatePalAuthToken(options = {}) {
23842
- const directory = join30(options.home ?? homedir(), ".flavor-code", "pals");
23843
- const tokenPath = join30(directory, "auth-token");
24083
+ const directory = join31(options.home ?? homedir(), ".flavor-code", "pals");
24084
+ const tokenPath = join31(directory, "auth-token");
23844
24085
  await mkdir21(directory, { recursive: true, mode: 448 });
23845
24086
  await chmod2(directory, 448);
23846
24087
  const token = (options.random ?? randomBytes3)(PAL_AUTH_TOKEN_BYTES).toString("hex");
23847
- const temporaryPath = join30(directory, `.auth-token-${process.pid}-${randomUUID21()}.tmp`);
24088
+ const temporaryPath = join31(directory, `.auth-token-${process.pid}-${randomUUID21()}.tmp`);
23848
24089
  let handle;
23849
24090
  try {
23850
24091
  handle = await open12(temporaryPath, "wx", 384);
@@ -23879,7 +24120,7 @@ function validatePalAuthToken(token) {
23879
24120
 
23880
24121
  // src/pals/lifecycle.ts
23881
24122
  import { randomUUID as randomUUID22 } from "crypto";
23882
- import { mkdir as mkdir22, open as open13, readFile as readFile26, rename as rename11, stat as stat11, unlink as unlink5 } from "fs/promises";
24123
+ import { mkdir as mkdir22, open as open13, readFile as readFile26, rename as rename11, stat as stat12, unlink as unlink5 } from "fs/promises";
23883
24124
  import { dirname as dirname22 } from "path";
23884
24125
  function defaultProcessAlive(pid) {
23885
24126
  if (!Number.isSafeInteger(pid) || pid <= 0) return true;
@@ -23911,7 +24152,7 @@ async function acquirePalFileLock(options) {
23911
24152
  handle = await open13(options.path, "wx", 384);
23912
24153
  } catch (error) {
23913
24154
  if (error.code !== "EEXIST") throw error;
23914
- const [contents, info] = await Promise.all([readFile26(options.path, "utf8").catch(() => ""), stat11(options.path).catch(() => void 0)]);
24155
+ const [contents, info] = await Promise.all([readFile26(options.path, "utf8").catch(() => ""), stat12(options.path).catch(() => void 0)]);
23915
24156
  if (await options.endpointLive()) return void 0;
23916
24157
  const existing = parseMetadata(contents);
23917
24158
  if (existing !== void 0 && (options.processAlive ?? defaultProcessAlive)(existing.pid)) return void 0;
@@ -23974,7 +24215,7 @@ function canConnect(address) {
23974
24215
  }
23975
24216
  function startupLockPath(address) {
23976
24217
  const digest = createHash20("sha256").update(address, "utf8").digest("hex").slice(0, 24);
23977
- return join31(tmpdir3(), `flavor-code-pals-${digest}.startup.lock`);
24218
+ return join32(tmpdir3(), `flavor-code-pals-${digest}.startup.lock`);
23978
24219
  }
23979
24220
  async function spawnProductionBroker(address) {
23980
24221
  const entry2 = process.argv[1];
@@ -24666,8 +24907,8 @@ var ApprovalBridge = class {
24666
24907
  }
24667
24908
  };
24668
24909
  async function createProductionRuntime(options) {
24669
- const workspace = resolve32(options.workspace ?? process.cwd());
24670
- const home = resolve32(options.home ?? homedir2());
24910
+ const workspace = resolve33(options.workspace ?? process.cwd());
24911
+ const home = resolve33(options.home ?? homedir2());
24671
24912
  const environment = options.environment ?? process.env;
24672
24913
  const ide = new FlavorIdeClient({ workspace, home, environment });
24673
24914
  const ideReady = ide.initialize();
@@ -24891,8 +25132,8 @@ async function createProductionRuntime(options) {
24891
25132
  const registeredProviders = registration.registered;
24892
25133
  let effectiveLlm = registration.effectiveLlm;
24893
25134
  const pluginHost = new PluginHost({
24894
- globalPluginDirs: [join32(home, ".flavor-code", "plugins")],
24895
- projectPluginDirs: [join32(workspace, ".flavor", "plugins")],
25135
+ globalPluginDirs: [join33(home, ".flavor-code", "plugins")],
25136
+ projectPluginDirs: [join33(workspace, ".flavor", "plugins")],
24896
25137
  config,
24897
25138
  // Existing project plugins (including astgraph and superharness) use Node.js
24898
25139
  // built-ins. The Worker/vm sandbox deliberately blocks those imports, so it
@@ -25000,7 +25241,7 @@ async function createProductionRuntime(options) {
25000
25241
  const address = palSocketAddress({
25001
25242
  platform: process.platform,
25002
25243
  userScope: home,
25003
- runtimeDir: join32(tmpdir4(), `flavor-code-pals-${createHash21("sha256").update(home).digest("hex").slice(0, 16)}`)
25244
+ runtimeDir: join33(tmpdir4(), `flavor-code-pals-${createHash21("sha256").update(home).digest("hex").slice(0, 16)}`)
25004
25245
  });
25005
25246
  const baseCollaborationClient = collaboration.client ?? collaboration.createClient?.({
25006
25247
  instanceId: collaboration.instanceId,
@@ -25111,8 +25352,8 @@ async function createProductionRuntime(options) {
25111
25352
  diagnostics.push(`MCP servers could not start: ${message(error)}`);
25112
25353
  });
25113
25354
  const skills = new SkillRegistry({
25114
- globalRoots: [join32(home, ".flavor-code", "skills")],
25115
- projectRoots: [join32(workspace, ".flavor", "skills"), ...pluginSkillRoots],
25355
+ globalRoots: [join33(home, ".flavor-code", "skills")],
25356
+ projectRoots: [join33(workspace, ".flavor", "skills"), ...pluginSkillRoots],
25116
25357
  authorizeResource: async () => true,
25117
25358
  disabledNames: config.skills.disabled
25118
25359
  });
@@ -25120,7 +25361,7 @@ async function createProductionRuntime(options) {
25120
25361
  skillsReady.catch(() => void 0);
25121
25362
  tools.push(createSkillTool(skills), createSkillResourceTool(skills));
25122
25363
  if (options.extraTools !== void 0) tools.push(...options.extraTools);
25123
- const flavor = await optionalText2(join32(workspace, "FLAVOR.md"));
25364
+ const flavor = await optionalText2(join33(workspace, "FLAVOR.md"));
25124
25365
  const instructionBaseline = await workspaceInstructions.baseline();
25125
25366
  let memoryContext;
25126
25367
  if (memoryStore !== void 0) {
@@ -25698,7 +25939,7 @@ async function createProductionRuntime(options) {
25698
25939
  evolveService.toolDefinition(),
25699
25940
  ...mcpTools
25700
25941
  ];
25701
- const loopFlavor = await optionalText2(join32(input.workspace, "FLAVOR.md"));
25942
+ const loopFlavor = await optionalText2(join33(input.workspace, "FLAVOR.md"));
25702
25943
  const loopInstructionBaseline = await loopInstructions.baseline();
25703
25944
  const loopEnvironment = createPromptEnvironment({
25704
25945
  now: /* @__PURE__ */ new Date(),
@@ -26058,6 +26299,22 @@ async function createProductionRuntime(options) {
26058
26299
  modelId: () => harness.subagentModelId,
26059
26300
  notify: (message2) => emitOutput({ type: "notice", message: message2 })
26060
26301
  }, focus, signal),
26302
+ explain: (query, focus, signal, onText) => runExplain({
26303
+ graph: new DesktopAstGraphService(workspace),
26304
+ readFile: async (path) => {
26305
+ const root = resolve33(workspace);
26306
+ const abs = resolve33(root, path);
26307
+ if (abs !== root && !abs.startsWith(root + sep23)) throw new Error(`path outside workspace: ${path}`);
26308
+ return readFile27(abs, "utf8");
26309
+ },
26310
+ history: async (path, limit) => fileHistory(workspace, path, limit),
26311
+ questions,
26312
+ registry,
26313
+ modelId: () => harness.subagentModelId,
26314
+ language,
26315
+ notify: (message2) => emitOutput({ type: "notice", message: message2 }),
26316
+ ...onText === void 0 ? {} : { onText }
26317
+ }, query, focus, signal),
26061
26318
  audit: async (toolFilter) => {
26062
26319
  try {
26063
26320
  const raw = await readFile27(auditLogger.path, "utf8");
@@ -26247,7 +26504,7 @@ ${renderMemoryDocument(entries)}`;
26247
26504
  return `Provider "${providerName}" is missing authorizationUrl, tokenUrl, or clientId.`;
26248
26505
  }
26249
26506
  try {
26250
- const tokenStore = createFileTokenStore(join32(home, ".flavor-code", "auth.json"));
26507
+ const tokenStore = createFileTokenStore(join33(home, ".flavor-code", "auth.json"));
26251
26508
  const credentialId = oauthCredentialId(oauthConfig.tokenUrl, oauthConfig.clientId);
26252
26509
  await migrateLegacyOAuthToken(tokenStore, providerName, credentialId);
26253
26510
  const oauth = new OAuthCallbackAuthProvider({
@@ -26294,7 +26551,7 @@ ${renderMemoryDocument(entries)}`;
26294
26551
  }
26295
26552
  },
26296
26553
  async logout() {
26297
- const tokenStore = createFileTokenStore(join32(home, ".flavor-code", "auth.json"));
26554
+ const tokenStore = createFileTokenStore(join33(home, ".flavor-code", "auth.json"));
26298
26555
  const tokens2 = await tokenStore.load();
26299
26556
  const hadCredentials = Object.keys(tokens2).length > 0 || effectiveLlm !== void 0;
26300
26557
  if (!hadCredentials) {
@@ -26570,8 +26827,8 @@ async function workspaceFingerprint(workspace) {
26570
26827
  let remaining = 5 * 1024 * 1024;
26571
26828
  for (const name of untracked.stdout.split("\0").filter(Boolean).sort()) {
26572
26829
  if (name === ".flavor/loops" || name.startsWith(".flavor/loops/")) continue;
26573
- const path = resolve32(workspace, name);
26574
- const relativePath = relative23(resolve32(workspace), path);
26830
+ const path = resolve33(workspace, name);
26831
+ const relativePath = relative23(resolve33(workspace), path);
26575
26832
  if (relativePath === ".." || relativePath.startsWith(`..${sep23}`)) continue;
26576
26833
  hash.update(name);
26577
26834
  if (remaining <= 0) continue;
@@ -26789,7 +27046,7 @@ async function registerConfiguredAdapters(providers, registry, environment, diag
26789
27046
  const configured = { ...providers };
26790
27047
  if (configured.openai === void 0 && environment.OPENAI_API_KEY) configured.openai = { type: "openai", apiKey: environment.OPENAI_API_KEY };
26791
27048
  if (configured.anthropic === void 0 && environment.ANTHROPIC_API_KEY) configured.anthropic = { type: "anthropic", apiKey: environment.ANTHROPIC_API_KEY };
26792
- const oauthTokenStore = createFileTokenStore(join32(home, ".flavor-code", "auth.json"));
27049
+ const oauthTokenStore = createFileTokenStore(join33(home, ".flavor-code", "auth.json"));
26793
27050
  const registered = [];
26794
27051
  let effectiveLlm;
26795
27052
  for (const [name, provider] of Object.entries(configured)) {
@@ -27220,13 +27477,13 @@ function createMemoryTaskId() {
27220
27477
 
27221
27478
  // src/desktop/runtime-controller.ts
27222
27479
  import { createHash as createHash27 } from "crypto";
27223
- import { mkdir as mkdir27, readFile as readFile35, readdir as readdir12, rm as rm15, stat as stat14, writeFile as writeFile17 } from "fs/promises";
27480
+ import { mkdir as mkdir27, readFile as readFile35, readdir as readdir12, rm as rm15, stat as stat15, writeFile as writeFile17 } from "fs/promises";
27224
27481
  import { homedir as homedir4 } from "os";
27225
- import { basename as basename9, join as join42, resolve as resolve35 } from "path";
27482
+ import { basename as basename9, join as join43, resolve as resolve36 } from "path";
27226
27483
 
27227
27484
  // src/d2c/workflow.ts
27228
27485
  import { createHash as createHash22 } from "crypto";
27229
- import { join as join33 } from "path";
27486
+ import { join as join34 } from "path";
27230
27487
  import { z as z42 } from "zod";
27231
27488
 
27232
27489
  // src/d2c/workflow-shared.ts
@@ -27372,7 +27629,7 @@ function reviewsFor(report, previous) {
27372
27629
  function implementationDirectory(report) {
27373
27630
  const source = report.implementation.source;
27374
27631
  if (!/^https?:\/\//i.test(source)) return source;
27375
- return join33("src", "d2c-output", report.task);
27632
+ return join34("src", "d2c-output", report.task);
27376
27633
  }
27377
27634
  function createWorkflow(report, framework) {
27378
27635
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -27496,7 +27753,7 @@ function applyQualityIssueDecision(workflow, issueId2, decision) {
27496
27753
  };
27497
27754
  }
27498
27755
  function workflowPath(workspace, task) {
27499
- return join33(taskDir(workspace, task), "workflow.json");
27756
+ return join34(taskDir(workspace, task), "workflow.json");
27500
27757
  }
27501
27758
  async function readWorkflow(workspace, task) {
27502
27759
  return (await readRecoverableFile(workflowPath(workspace, task), decodeWorkflow))?.value;
@@ -27750,7 +28007,7 @@ function confirmApiMapping(mapping, operationKey, operations) {
27750
28007
 
27751
28008
  // src/d2c/modules.ts
27752
28009
  import { readFile as readFile28 } from "fs/promises";
27753
- import { isAbsolute as isAbsolute15, join as join34, normalize as normalize2, posix, sep as sep24, win32 as win322 } from "path";
28010
+ import { isAbsolute as isAbsolute15, join as join35, normalize as normalize2, posix, sep as sep24, win32 as win322 } from "path";
27754
28011
  import { z as z43 } from "zod";
27755
28012
  var ModuleSchema = z43.object({
27756
28013
  id: z43.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/),
@@ -27769,13 +28026,13 @@ function safeSource(path) {
27769
28026
  }
27770
28027
  function d2cOutputDirectory(workspace, task) {
27771
28028
  if (!D2C_TASK_PATTERN.test(task)) throw new Error(`Invalid D2C task: ${task}`);
27772
- return join34(workspace, "src", "d2c-output", task);
28029
+ return join35(workspace, "src", "d2c-output", task);
27773
28030
  }
27774
28031
  async function readD2cModules(workspace, task) {
27775
28032
  const project = d2cOutputDirectory(workspace, task);
27776
28033
  let raw;
27777
28034
  try {
27778
- raw = await readFile28(join34(project, "d2c.modules.json"), "utf8");
28035
+ raw = await readFile28(join35(project, "d2c.modules.json"), "utf8");
27779
28036
  } catch {
27780
28037
  return [{ id: "page", label: task, sourceFiles: ["src/App.vue", "src/App.jsx"], keywords: [task] }];
27781
28038
  }
@@ -27796,7 +28053,7 @@ async function readD2cModules(workspace, task) {
27796
28053
 
27797
28054
  // src/d2c/integration.ts
27798
28055
  import { mkdir as mkdir24, readFile as readFile29, writeFile as writeFile13 } from "fs/promises";
27799
- import { join as join35 } from "path";
28056
+ import { join as join36 } from "path";
27800
28057
  function record2(value) {
27801
28058
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
27802
28059
  }
@@ -27932,7 +28189,7 @@ async def reset_acceptance(request: Request):
27932
28189
  }
27933
28190
  async function generateIntegrationArtifacts(projectDir, document, mappings, options = {}) {
27934
28191
  if (mappings.some((mapping) => mapping.status === "needs-confirmation")) throw new Error("Confirm every uncertain OpenAPI mapping before generating integration code");
27935
- const packagePath = join35(projectDir, "package.json");
28192
+ const packagePath = join36(projectDir, "package.json");
27936
28193
  let pkg;
27937
28194
  try {
27938
28195
  pkg = JSON.parse(await readFile29(packagePath, "utf8"));
@@ -27942,9 +28199,9 @@ async function generateIntegrationArtifacts(projectDir, document, mappings, opti
27942
28199
  const scripts = options.pythonServer ? { ...record2(pkg.scripts) ?? {} } : { ...record2(pkg.scripts) ?? {}, mock: "node mock/server.mjs" };
27943
28200
  const dependencies2 = options.pythonServer ? { ...record2(pkg.dependencies) ?? {}, axios: "^1.7.9" } : { ...record2(pkg.dependencies) ?? {}, axios: "^1.7.9", express: "^5.2.1" };
27944
28201
  const selected = [...new Set(mappings.map((mapping) => mapping.operationKey))].map((key) => document.operations.find((operation) => operation.id === key)).filter((operation) => operation !== void 0);
27945
- const apiDir = join35(projectDir, "src", "api");
27946
- const mockDir = join35(projectDir, "mock");
27947
- const serverDir = join35(projectDir, "server");
28202
+ const apiDir = join36(projectDir, "src", "api");
28203
+ const mockDir = join36(projectDir, "mock");
28204
+ const serverDir = join36(projectDir, "server");
27948
28205
  await Promise.all([
27949
28206
  mkdir24(apiDir, { recursive: true }),
27950
28207
  ...options.pythonServer ? [] : [mkdir24(mockDir, { recursive: true })],
@@ -27954,23 +28211,23 @@ async function generateIntegrationArtifacts(projectDir, document, mappings, opti
27954
28211
  if (options.pythonServer) files.push("server/main.py", "server/requirements.txt", "server/README.md", "server/flavor-runtime.json");
27955
28212
  else files.push("mock/server.mjs");
27956
28213
  await Promise.all([
27957
- writeFile13(join35(apiDir, "http.js"), `import axios from "axios";
28214
+ writeFile13(join36(apiDir, "http.js"), `import axios from "axios";
27958
28215
 
27959
28216
  export const http = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || ${JSON.stringify(document.baseUrl ?? "")}, timeout: 15000 });
27960
28217
 
27961
28218
  http.interceptors.response.use((response) => response, (error) => Promise.reject({ name: "ApiError", status: error.response?.status, message: error.response?.data?.message || error.message, cause: error }));
27962
28219
  `),
27963
- writeFile13(join35(apiDir, "d2c-api.js"), apiModule(selected)),
27964
- writeFile13(join35(apiDir, "d2c-bindings.json"), `${JSON.stringify({ schema: 1, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), mappings }, null, 2)}
28220
+ writeFile13(join36(apiDir, "d2c-api.js"), apiModule(selected)),
28221
+ writeFile13(join36(apiDir, "d2c-bindings.json"), `${JSON.stringify({ schema: 1, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), mappings }, null, 2)}
27965
28222
  `),
27966
- ...options.pythonServer ? [] : [writeFile13(join35(mockDir, "server.mjs"), mockModule({ ...document, operations: selected }))],
28223
+ ...options.pythonServer ? [] : [writeFile13(join36(mockDir, "server.mjs"), mockModule({ ...document, operations: selected }))],
27967
28224
  writeFile13(packagePath, `${JSON.stringify({ ...pkg, scripts, dependencies: dependencies2 }, null, 2)}
27968
28225
  `),
27969
28226
  ...options.pythonServer ? [
27970
- writeFile13(join35(serverDir, "main.py"), pythonServerModule({ ...document, operations: selected })),
27971
- writeFile13(join35(serverDir, "requirements.txt"), "fastapi>=0.116,<1\nuvicorn[standard]>=0.35,<1\nsqlalchemy>=2.0,<3\n"),
27972
- writeFile13(join35(serverDir, "README.md"), "# Python API service\n\nThis is the real integration service, not a mock. Development defaults to SQLite through `DATABASE_URL=sqlite:///./data/app.db`. Set the same environment variable to a SQLAlchemy MySQL or PostgreSQL URL for deployment; keep schema changes in migrations instead of database-specific SQL. Flavor Code starts the service automatically for acceptance.\n"),
27973
- writeFile13(join35(serverDir, "flavor-runtime.json"), `${JSON.stringify({
28227
+ writeFile13(join36(serverDir, "main.py"), pythonServerModule({ ...document, operations: selected })),
28228
+ writeFile13(join36(serverDir, "requirements.txt"), "fastapi>=0.116,<1\nuvicorn[standard]>=0.35,<1\nsqlalchemy>=2.0,<3\n"),
28229
+ writeFile13(join36(serverDir, "README.md"), "# Python API service\n\nThis is the real integration service, not a mock. Development defaults to SQLite through `DATABASE_URL=sqlite:///./data/app.db`. Set the same environment variable to a SQLAlchemy MySQL or PostgreSQL URL for deployment; keep schema changes in migrations instead of database-specific SQL. Flavor Code starts the service automatically for acceptance.\n"),
28230
+ writeFile13(join36(serverDir, "flavor-runtime.json"), `${JSON.stringify({
27974
28231
  schema: 1,
27975
28232
  kind: "python-fastapi",
27976
28233
  cwd: "server",
@@ -27991,7 +28248,7 @@ http.interceptors.response.use((response) => response, (error) => Promise.reject
27991
28248
  import { spawn as spawn8 } from "child_process";
27992
28249
  import { access as access3, readFile as readFile30 } from "fs/promises";
27993
28250
  import { createServer as createServer5 } from "net";
27994
- import { join as join36 } from "path";
28251
+ import { join as join37 } from "path";
27995
28252
  async function exists3(path) {
27996
28253
  try {
27997
28254
  await access3(path);
@@ -28099,7 +28356,7 @@ ${output.trim()}` : ""}`);
28099
28356
  }
28100
28357
  }
28101
28358
  async function runPythonBackend(projectDir, options) {
28102
- const serverDir = join36(projectDir, "server");
28359
+ const serverDir = join37(projectDir, "server");
28103
28360
  if (options.installDependencies !== false) await installPython(serverDir, options.signal);
28104
28361
  const selectedPort = await port();
28105
28362
  const url = `http://127.0.0.1:${selectedPort}`;
@@ -28145,7 +28402,7 @@ ${managed.output().slice(-4e3)}`);
28145
28402
  };
28146
28403
  }
28147
28404
  async function runD2cMockServer(projectDir, options = {}) {
28148
- const runtimePath = join36(projectDir, "server", "flavor-runtime.json");
28405
+ const runtimePath = join37(projectDir, "server", "flavor-runtime.json");
28149
28406
  if (await exists3(runtimePath)) {
28150
28407
  let runtime;
28151
28408
  try {
@@ -28156,9 +28413,9 @@ async function runD2cMockServer(projectDir, options = {}) {
28156
28413
  if (runtime.schema !== 1 || runtime.kind !== "python-fastapi") throw new Error("Unsupported E2E backend runtime manifest");
28157
28414
  return runPythonBackend(projectDir, options);
28158
28415
  }
28159
- const serverPath = join36(projectDir, "mock", "server.mjs");
28416
+ const serverPath = join37(projectDir, "mock", "server.mjs");
28160
28417
  if (!await exists3(serverPath)) throw new Error(`D2C mock server is not generated at ${serverPath}`);
28161
- if (options.installDependencies === false && !await exists3(join36(projectDir, "node_modules", "express", "package.json"))) {
28418
+ if (options.installDependencies === false && !await exists3(join37(projectDir, "node_modules", "express", "package.json"))) {
28162
28419
  throw new Error("D2C mock server dependencies are not installed");
28163
28420
  }
28164
28421
  if (options.installDependencies !== false) await install(projectDir, options.signal);
@@ -28200,8 +28457,8 @@ ${managed.output().slice(-4e3)}`);
28200
28457
 
28201
28458
  // src/d2c/product.ts
28202
28459
  import { createHash as createHash24, randomUUID as randomUUID27 } from "crypto";
28203
- import { copyFile as copyFile2, mkdir as mkdir25, readFile as readFile32, rename as rename13, stat as stat12, unlink as unlink7, writeFile as writeFile15 } from "fs/promises";
28204
- import { join as join37 } from "path";
28460
+ import { copyFile as copyFile2, mkdir as mkdir25, readFile as readFile32, rename as rename13, stat as stat13, unlink as unlink7, writeFile as writeFile15 } from "fs/promises";
28461
+ import { join as join38 } from "path";
28205
28462
  import { z as z44 } from "zod";
28206
28463
 
28207
28464
  // src/e2e/prd-governance.ts
@@ -28356,13 +28613,13 @@ var PlanSchema2 = z44.object({
28356
28613
  }).strict();
28357
28614
  function d2cProductDirectory(workspace, task) {
28358
28615
  if (!D2C_TASK_PATTERN.test(task)) throw new Error("Invalid D2C task name");
28359
- return join37(taskDir(workspace, task), "product");
28616
+ return join38(taskDir(workspace, task), "product");
28360
28617
  }
28361
28618
  function d2cProductPrototypeDirectory(workspace, task) {
28362
- return join37(d2cProductDirectory(workspace, task), "prototype");
28619
+ return join38(d2cProductDirectory(workspace, task), "prototype");
28363
28620
  }
28364
28621
  function planPath(workspace, task) {
28365
- return join37(taskDir(workspace, task), "product-plan.json");
28622
+ return join38(taskDir(workspace, task), "product-plan.json");
28366
28623
  }
28367
28624
  function resolveD2cProductTechnology(requirement, fallbackFramework = "vue") {
28368
28625
  const frontendCandidates = [
@@ -28450,7 +28707,7 @@ function buildD2cProductOpenApi(task, modules) {
28450
28707
  `;
28451
28708
  }
28452
28709
  async function fileTimestamp(path) {
28453
- const info = await stat12(path).catch(() => void 0);
28710
+ const info = await stat13(path).catch(() => void 0);
28454
28711
  return info?.isFile() ? info.mtime.toISOString() : void 0;
28455
28712
  }
28456
28713
  async function fileHash(path) {
@@ -28545,16 +28802,16 @@ async function readD2cProductPlan(workspace, task) {
28545
28802
  }
28546
28803
  async function discoverD2cProductArtifacts(workspace, plan) {
28547
28804
  const product = d2cProductDirectory(workspace, plan.task);
28548
- const prdUpdatedAt = await fileTimestamp(join37(product, "prd.md"));
28549
- const prdHash = await fileHash(join37(product, "prd.md"));
28550
- const prototypeUpdatedAt = await fileTimestamp(join37(product, "prototype", "index.html"));
28551
- const prototypeHash = await fileHash(join37(product, "prototype", "index.html"));
28552
- const expectedManifest = join37(product, "prototype", "interaction-manifest.json");
28805
+ const prdUpdatedAt = await fileTimestamp(join38(product, "prd.md"));
28806
+ const prdHash = await fileHash(join38(product, "prd.md"));
28807
+ const prototypeUpdatedAt = await fileTimestamp(join38(product, "prototype", "index.html"));
28808
+ const prototypeHash = await fileHash(join38(product, "prototype", "index.html"));
28809
+ const expectedManifest = join38(product, "prototype", "interaction-manifest.json");
28553
28810
  let manifestUpdatedAt = await fileTimestamp(expectedManifest);
28554
28811
  if (manifestUpdatedAt === void 0) {
28555
- const misplacedManifest = join37(product, "interaction-manifest.json");
28812
+ const misplacedManifest = join38(product, "interaction-manifest.json");
28556
28813
  if (await fileTimestamp(misplacedManifest) !== void 0) {
28557
- await mkdir25(join37(product, "prototype"), { recursive: true });
28814
+ await mkdir25(join38(product, "prototype"), { recursive: true });
28558
28815
  await copyFile2(misplacedManifest, expectedManifest);
28559
28816
  manifestUpdatedAt = await fileTimestamp(expectedManifest);
28560
28817
  }
@@ -28587,8 +28844,8 @@ async function readD2cProductPlanView(workspace, task) {
28587
28844
  if (stored === void 0) return void 0;
28588
28845
  const plan = await discoverD2cProductArtifacts(workspace, stored);
28589
28846
  if (plan.revision !== stored.revision) await writeD2cProductPlan(workspace, plan);
28590
- const prdMarkdown = plan.prd === void 0 ? void 0 : await readFile32(join37(taskDir(workspace, task), plan.prd.path), "utf8");
28591
- const manifestPath = join37(d2cProductDirectory(workspace, task), "prototype", "interaction-manifest.json");
28847
+ const prdMarkdown = plan.prd === void 0 ? void 0 : await readFile32(join38(taskDir(workspace, task), plan.prd.path), "utf8");
28848
+ const manifestPath = join38(d2cProductDirectory(workspace, task), "prototype", "interaction-manifest.json");
28592
28849
  const validationError = plan.phase === "design-generating" && await fileTimestamp(manifestPath) !== void 0 ? await validateAndNormalizePrototypeManifest(manifestPath) : void 0;
28593
28850
  return {
28594
28851
  plan,
@@ -28747,7 +29004,7 @@ function createAcceptanceEvidence(coverage, run, now = /* @__PURE__ */ new Date(
28747
29004
 
28748
29005
  // src/e2e/delivery-run.ts
28749
29006
  import { createHash as createHash26 } from "crypto";
28750
- import { join as join38 } from "path";
29007
+ import { join as join39 } from "path";
28751
29008
  import { z as z45 } from "zod";
28752
29009
  var DELIVERY_NODE_IDS = ["requirement", "prd", "design", "d2c", "api", "acceptance", "delivery"];
28753
29010
  var ArtifactSchema = z45.object({
@@ -28864,7 +29121,7 @@ function completeDeliveryNode(run, node, outputs, now = /* @__PURE__ */ new Date
28864
29121
  return { ...run, nodes, updatedAt: timestamp2 };
28865
29122
  }
28866
29123
  function deliveryRunPath(workspace, task) {
28867
- return join38(taskDir(workspace, task), "delivery-run.json");
29124
+ return join39(taskDir(workspace, task), "delivery-run.json");
28868
29125
  }
28869
29126
  function decodeDeliveryRun(raw) {
28870
29127
  return DeliveryRunSchema.parse(JSON.parse(raw));
@@ -28907,14 +29164,14 @@ async function updateDeliveryRun(workspace, task, expectedRevision, mutation) {
28907
29164
  // src/skills/manager.ts
28908
29165
  import { lstat as lstat13, mkdir as mkdir26, readFile as readFile34, readdir as readdir11, realpath as realpath13, rm as rm14, writeFile as writeFile16 } from "fs/promises";
28909
29166
  import { homedir as homedir3 } from "os";
28910
- import { isAbsolute as isAbsolute16, join as join39, relative as relative24, resolve as resolve33, sep as sep25 } from "path";
29167
+ import { isAbsolute as isAbsolute16, join as join40, relative as relative24, resolve as resolve34, sep as sep25 } from "path";
28911
29168
  import { stringify } from "yaml";
28912
29169
  var SkillManager = class {
28913
29170
  #workspace;
28914
29171
  #home;
28915
29172
  constructor(options) {
28916
- this.#workspace = resolve33(options.workspace);
28917
- this.#home = resolve33(options.home ?? homedir3());
29173
+ this.#workspace = resolve34(options.workspace);
29174
+ this.#home = resolve34(options.home ?? homedir3());
28918
29175
  }
28919
29176
  async list() {
28920
29177
  const { registry, disabled } = await this.#registry();
@@ -28930,7 +29187,7 @@ var SkillManager = class {
28930
29187
  async create(draft) {
28931
29188
  const value = parseDraft(draft);
28932
29189
  const projectRoot = this.#projectRoot();
28933
- const directory = join39(projectRoot, value.name);
29190
+ const directory = join40(projectRoot, value.name);
28934
29191
  await mkdir26(projectRoot, { recursive: true, mode: 448 });
28935
29192
  try {
28936
29193
  await mkdir26(directory, { mode: 448 });
@@ -28939,7 +29196,7 @@ var SkillManager = class {
28939
29196
  throw error;
28940
29197
  }
28941
29198
  try {
28942
- await writeFile16(join39(directory, "SKILL.md"), renderSkill(value), { encoding: "utf8", flag: "wx", mode: 384 });
29199
+ await writeFile16(join40(directory, "SKILL.md"), renderSkill(value), { encoding: "utf8", flag: "wx", mode: 384 });
28943
29200
  await this.#persistEnabled(value.name, true);
28944
29201
  return this.get(value.name);
28945
29202
  } catch (error) {
@@ -28957,7 +29214,7 @@ var SkillManager = class {
28957
29214
  throw error;
28958
29215
  });
28959
29216
  if (!info.isDirectory() || info.isSymbolicLink()) throw new Error(`Project skill path is not a real directory: ${validName}`);
28960
- const skillFile = join39(directory, "SKILL.md");
29217
+ const skillFile = join40(directory, "SKILL.md");
28961
29218
  const fileInfo = await lstat13(skillFile);
28962
29219
  if (!fileInfo.isFile() || fileInfo.isSymbolicLink()) throw new Error(`Project skill file is not a real file: ${validName}`);
28963
29220
  await writeFile16(skillFile, renderSkill(value), { encoding: "utf8", mode: 384 });
@@ -28989,12 +29246,12 @@ var SkillManager = class {
28989
29246
  async #registry() {
28990
29247
  const loaded = await loadConfig({ cwd: this.#workspace, home: this.#home });
28991
29248
  const [globalPluginRoots, projectPluginRoots] = await Promise.all([
28992
- discoverPluginSkillRoots(join39(this.#home, ".flavor-code", "plugins")),
28993
- discoverPluginSkillRoots(join39(this.#workspace, ".flavor", "plugins"))
29249
+ discoverPluginSkillRoots(join40(this.#home, ".flavor-code", "plugins")),
29250
+ discoverPluginSkillRoots(join40(this.#workspace, ".flavor", "plugins"))
28994
29251
  ]);
28995
29252
  return {
28996
29253
  registry: new SkillRegistry({
28997
- globalRoots: [join39(this.#home, ".flavor-code", "skills"), ...globalPluginRoots],
29254
+ globalRoots: [join40(this.#home, ".flavor-code", "skills"), ...globalPluginRoots],
28998
29255
  projectRoots: [this.#projectRoot(), ...projectPluginRoots]
28999
29256
  }),
29000
29257
  disabled: new Set(loaded.config.skills.disabled)
@@ -29010,10 +29267,10 @@ var SkillManager = class {
29010
29267
  };
29011
29268
  }
29012
29269
  #projectRoot() {
29013
- return join39(this.#workspace, ".flavor", "skills");
29270
+ return join40(this.#workspace, ".flavor", "skills");
29014
29271
  }
29015
29272
  #projectSkillDirectory(name) {
29016
- return join39(this.#projectRoot(), name);
29273
+ return join40(this.#projectRoot(), name);
29017
29274
  }
29018
29275
  };
29019
29276
  function parseName(value) {
@@ -29054,11 +29311,11 @@ async function discoverPluginSkillRoots(directory) {
29054
29311
  const roots = [];
29055
29312
  for (const entry2 of entries) {
29056
29313
  if (!entry2.isDirectory() || entry2.isSymbolicLink()) continue;
29057
- const pluginRoot = await realpath13(join39(directory, entry2.name));
29314
+ const pluginRoot = await realpath13(join40(directory, entry2.name));
29058
29315
  try {
29059
- const manifest = PluginManifestSchema.parse(JSON.parse(await readFile34(join39(pluginRoot, "flavor-plugin.json"), "utf8")));
29316
+ const manifest = PluginManifestSchema.parse(JSON.parse(await readFile34(join40(pluginRoot, "flavor-plugin.json"), "utf8")));
29060
29317
  for (const contribution of manifest.contributes.skillRoots) {
29061
- const candidate = resolve33(pluginRoot, contribution.path);
29318
+ const candidate = resolve34(pluginRoot, contribution.path);
29062
29319
  const info = await lstat13(candidate);
29063
29320
  if (!info.isDirectory() || info.isSymbolicLink()) continue;
29064
29321
  const physical = await realpath13(candidate);
@@ -29131,11 +29388,11 @@ async function createProjectMemoryManager(options) {
29131
29388
  }
29132
29389
 
29133
29390
  // src/mcp/config-manager.ts
29134
- import { join as join40 } from "path";
29391
+ import { join as join41 } from "path";
29135
29392
  var ProjectMcpConfigManager = class {
29136
29393
  path;
29137
29394
  constructor(workspace) {
29138
- this.path = join40(workspace, ".flavor", "flavor.json");
29395
+ this.path = join41(workspace, ".flavor", "flavor.json");
29139
29396
  }
29140
29397
  async list() {
29141
29398
  const document = await readRecoverableFile(this.path, (raw) => parseDocument2(this.path, raw));
@@ -29232,7 +29489,7 @@ function isRecord7(value) {
29232
29489
  }
29233
29490
 
29234
29491
  // src/desktop/model-config.ts
29235
- import { join as join41 } from "path";
29492
+ import { join as join42 } from "path";
29236
29493
  var DEFAULT_DESKTOP_MODELS = [
29237
29494
  {
29238
29495
  id: "anthropic:deepseek-v4-pro",
@@ -29253,7 +29510,7 @@ var DEFAULT_DESKTOP_MODELS = [
29253
29510
  ];
29254
29511
  async function loadDesktopModels(workspace, home) {
29255
29512
  try {
29256
- const tokens2 = await createFileTokenStore(join41(home, ".flavor-code", "auth.json")).load();
29513
+ const tokens2 = await createFileTokenStore(join42(home, ".flavor-code", "auth.json")).load();
29257
29514
  const pkceModels = [];
29258
29515
  const knownPkce = /* @__PURE__ */ new Set();
29259
29516
  for (const token of Object.values(tokens2)) {
@@ -29321,9 +29578,9 @@ function protocolLabel(type) {
29321
29578
 
29322
29579
  // src/desktop/d2c-product-preview.ts
29323
29580
  import { createReadStream as createReadStream2 } from "fs";
29324
- import { realpath as realpath14, stat as stat13 } from "fs/promises";
29581
+ import { realpath as realpath14, stat as stat14 } from "fs/promises";
29325
29582
  import { createServer as createServer6 } from "http";
29326
- import { extname as extname3, isAbsolute as isAbsolute17, relative as relative25, resolve as resolve34, sep as sep26 } from "path";
29583
+ import { extname as extname3, isAbsolute as isAbsolute17, relative as relative25, resolve as resolve35, sep as sep26 } from "path";
29327
29584
  var MIME_TYPES = {
29328
29585
  ".css": "text/css; charset=utf-8",
29329
29586
  ".gif": "image/gif",
@@ -29345,8 +29602,8 @@ function isInside(root, candidate) {
29345
29602
  return delta === "" || !delta.startsWith(`..${sep26}`) && delta !== ".." && !isAbsolute17(delta);
29346
29603
  }
29347
29604
  async function startD2cProductPreview(rootDirectory) {
29348
- const root = await realpath14(resolve34(rootDirectory));
29349
- const rootInfo = await stat13(root);
29605
+ const root = await realpath14(resolve35(rootDirectory));
29606
+ const rootInfo = await stat14(root);
29350
29607
  if (!rootInfo.isDirectory()) throw new Error("D2C product prototype directory is unavailable");
29351
29608
  const server = createServer6(async (request, response2) => {
29352
29609
  response2.setHeader("cache-control", "no-store");
@@ -29357,11 +29614,11 @@ async function startD2cProductPreview(rootDirectory) {
29357
29614
  let pathname = decodeURIComponent(url.pathname);
29358
29615
  if (pathname === "/") pathname = "/index.html";
29359
29616
  if (pathname.includes("\0")) return sendNotFound(response2);
29360
- const unresolved = resolve34(root, `.${pathname}`);
29617
+ const unresolved = resolve35(root, `.${pathname}`);
29361
29618
  if (!isInside(root, unresolved)) return sendNotFound(response2);
29362
29619
  const target = await realpath14(unresolved).catch(() => void 0);
29363
29620
  if (target === void 0 || !isInside(root, target)) return sendNotFound(response2);
29364
- const info = await stat13(target);
29621
+ const info = await stat14(target);
29365
29622
  if (!info.isFile()) return sendNotFound(response2);
29366
29623
  response2.writeHead(200, {
29367
29624
  "content-length": info.size,
@@ -29398,33 +29655,33 @@ function pngDataUrl(png) {
29398
29655
  }
29399
29656
  async function detectD2cFramework(workspace, task) {
29400
29657
  try {
29401
- const pkg = JSON.parse(await readFile35(join42(d2cOutputDirectory(workspace, task), "package.json"), "utf8"));
29658
+ const pkg = JSON.parse(await readFile35(join43(d2cOutputDirectory(workspace, task), "package.json"), "utf8"));
29402
29659
  return pkg.dependencies?.react === void 0 ? "vue" : "react";
29403
29660
  } catch {
29404
29661
  return "vue";
29405
29662
  }
29406
29663
  }
29407
29664
  function integrationDirectory(workspace, task) {
29408
- return join42(workspace, ".flavor", "d2c", task, "integration");
29665
+ return join43(workspace, ".flavor", "d2c", task, "integration");
29409
29666
  }
29410
29667
  var D2C_BACKEND_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".cfg", ".env", ".ini", ".js", ".json", ".lock", ".mjs", ".py", ".toml", ".txt", ".yaml", ".yml"]);
29411
29668
  var D2C_BACKEND_IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([".git", ".venv", "__pycache__", "data", "node_modules", "venv"]);
29412
29669
  async function d2cBackendSourceFingerprint(project) {
29413
- const runtimeManifest = join42(project, "server", "flavor-runtime.json");
29414
- const realBackend = (await stat14(runtimeManifest).catch(() => void 0))?.isFile() === true;
29415
- const root = realBackend ? join42(project, "server") : join42(project, "mock");
29416
- if ((await stat14(root).catch(() => void 0))?.isDirectory() !== true) return void 0;
29670
+ const runtimeManifest = join43(project, "server", "flavor-runtime.json");
29671
+ const realBackend = (await stat15(runtimeManifest).catch(() => void 0))?.isFile() === true;
29672
+ const root = realBackend ? join43(project, "server") : join43(project, "mock");
29673
+ if ((await stat15(root).catch(() => void 0))?.isDirectory() !== true) return void 0;
29417
29674
  const files = [];
29418
29675
  const visit = async (directory) => {
29419
29676
  const entries = await readdir12(directory, { withFileTypes: true });
29420
29677
  for (const entry2 of entries) {
29421
29678
  if (entry2.isDirectory()) {
29422
- if (!D2C_BACKEND_IGNORED_DIRECTORIES.has(entry2.name)) await visit(join42(directory, entry2.name));
29679
+ if (!D2C_BACKEND_IGNORED_DIRECTORIES.has(entry2.name)) await visit(join43(directory, entry2.name));
29423
29680
  continue;
29424
29681
  }
29425
29682
  if (!entry2.isFile()) continue;
29426
29683
  const extension2 = entry2.name.includes(".") ? `.${entry2.name.split(".").pop().toLowerCase()}` : "";
29427
- if (D2C_BACKEND_SOURCE_EXTENSIONS.has(extension2)) files.push(join42(directory, entry2.name));
29684
+ if (D2C_BACKEND_SOURCE_EXTENSIONS.has(extension2)) files.push(join43(directory, entry2.name));
29428
29685
  }
29429
29686
  };
29430
29687
  await visit(root);
@@ -29476,7 +29733,7 @@ var DesktopRuntimeController = class {
29476
29733
  /** Backend source digest observed when the managed process was started. */
29477
29734
  #d2cMockSourceFingerprints = /* @__PURE__ */ new Map();
29478
29735
  constructor(options) {
29479
- this.#home = resolve35(options.home ?? homedir4());
29736
+ this.#home = resolve36(options.home ?? homedir4());
29480
29737
  this.#createRuntime = options.createRuntime ?? (async (runtimeOptions) => createProductionRuntime(runtimeOptions));
29481
29738
  this.#createTerminalService = options.createTerminalService ?? ((workspace) => new TerminalService(workspace));
29482
29739
  this.#listSessions = options.listSessions ?? (async (workspace) => {
@@ -29551,7 +29808,7 @@ var DesktopRuntimeController = class {
29551
29808
  };
29552
29809
  }
29553
29810
  async openWorkspace(path) {
29554
- const workspace = resolve35(path);
29811
+ const workspace = resolve36(path);
29555
29812
  if (workspace !== this.#workspace) {
29556
29813
  await this.#disposeRuntime();
29557
29814
  await this.#stopAllD2cPreviews();
@@ -29978,8 +30235,8 @@ var DesktopRuntimeController = class {
29978
30235
  if (workflow === void 0 || !reviewProgress(workflow).complete || workflow.stage === "visual-review") {
29979
30236
  throw new Error("Complete visual review before importing OpenAPI");
29980
30237
  }
29981
- const moduleManifest = join42(d2cOutputDirectory(workspace, task), "d2c.modules.json");
29982
- const moduleManifestInfo = await stat14(moduleManifest).catch(() => void 0);
30238
+ const moduleManifest = join43(d2cOutputDirectory(workspace, task), "d2c.modules.json");
30239
+ const moduleManifestInfo = await stat15(moduleManifest).catch(() => void 0);
29983
30240
  if (moduleManifestInfo === void 0 || !moduleManifestInfo.isFile()) {
29984
30241
  throw new Error("D2C implementation must write d2c.modules.json before API mapping; ask the agent to split the project into modules first");
29985
30242
  }
@@ -29991,8 +30248,8 @@ var DesktopRuntimeController = class {
29991
30248
  await mkdir27(dir, { recursive: true });
29992
30249
  const hash = createHash27("sha256").update(raw).digest("hex");
29993
30250
  await Promise.all([
29994
- writeFile17(join42(dir, "swagger.json"), raw),
29995
- writeFile17(join42(dir, "openapi.normalized.json"), `${JSON.stringify(document, null, 2)}
30251
+ writeFile17(join43(dir, "swagger.json"), raw),
30252
+ writeFile17(join43(dir, "openapi.normalized.json"), `${JSON.stringify(document, null, 2)}
29996
30253
  `)
29997
30254
  ]);
29998
30255
  const next = {
@@ -30016,7 +30273,7 @@ var DesktopRuntimeController = class {
30016
30273
  const workspace = this.#workspace;
30017
30274
  if (workspace === void 0) throw new Error("Open a project before importing OpenAPI");
30018
30275
  await this.#assertProductPrdLocked(workspace, task);
30019
- const info = await stat14(sourcePath);
30276
+ const info = await stat15(sourcePath);
30020
30277
  if (!info.isFile() || info.size > 8 * 1024 * 1024) throw new Error("OpenAPI JSON must be a file no larger than 8 MiB");
30021
30278
  return this.#storeD2cOpenApi(workspace, task, basename9(sourcePath), await readFile35(sourcePath, "utf8"), false);
30022
30279
  }
@@ -30027,7 +30284,7 @@ var DesktopRuntimeController = class {
30027
30284
  let workflow = await readWorkflow(workspace, task);
30028
30285
  if (workflow?.openapi === void 0 && workflow !== void 0 && reviewProgress(workflow).complete && await readD2cProductPlanView(workspace, task) !== void 0) {
30029
30286
  const modules = await readD2cModules(workspace, task);
30030
- const productOpenApiPath = join42(taskDir(workspace, task), "product", "openapi.json");
30287
+ const productOpenApiPath = join43(taskDir(workspace, task), "product", "openapi.json");
30031
30288
  const authored = await readFile35(productOpenApiPath, "utf8").catch(() => void 0);
30032
30289
  const generated = authored === void 0;
30033
30290
  const prepared = await this.#storeD2cOpenApi(
@@ -30040,7 +30297,7 @@ var DesktopRuntimeController = class {
30040
30297
  return prepared;
30041
30298
  }
30042
30299
  if (workflow?.openapi === void 0 || workflow.mappings === void 0) return void 0;
30043
- const document = JSON.parse(await readFile35(join42(integrationDirectory(workspace, task), "openapi.normalized.json"), "utf8"));
30300
+ const document = JSON.parse(await readFile35(join43(integrationDirectory(workspace, task), "openapi.normalized.json"), "utf8"));
30044
30301
  return { workflow, document, mappings: workflow.mappings };
30045
30302
  }
30046
30303
  async confirmD2cMapping(task, moduleId, operationKey) {
@@ -30107,7 +30364,7 @@ var DesktopRuntimeController = class {
30107
30364
  ].join("\n");
30108
30365
  const delivery = await readDeliveryRun(workspace, task);
30109
30366
  if (delivery?.nodes.api.status === "running") {
30110
- const apiArtifacts = await Promise.all(generated.files.map(async (file) => artifactRef(file, await readFile35(join42(project, file)))));
30367
+ const apiArtifacts = await Promise.all(generated.files.map(async (file) => artifactRef(file, await readFile35(join43(project, file)))));
30111
30368
  const completed = await updateDeliveryRun(
30112
30369
  workspace,
30113
30370
  task,
@@ -30134,7 +30391,7 @@ var DesktopRuntimeController = class {
30134
30391
  try {
30135
30392
  const fingerprint2 = await d2cBackendSourceFingerprint(project);
30136
30393
  if (fingerprint2 !== void 0) this.#d2cMockSourceFingerprints.set(task, fingerprint2);
30137
- const environmentPath = join42(project, ".env.local");
30394
+ const environmentPath = join43(project, ".env.local");
30138
30395
  const existingEnvironment = await readFile35(environmentPath, "utf8").catch(() => "");
30139
30396
  const environment = `${existingEnvironment.split(/\r?\n/).filter((line) => !/^\s*VITE_API_BASE_URL\s*=/.test(line)).join("\n").trim()}
30140
30397
  VITE_API_BASE_URL=${running.url}
@@ -30215,7 +30472,7 @@ VITE_API_BASE_URL=${running.url}
30215
30472
  const readyPreview = this.#d2cPreviews.get(task);
30216
30473
  const readyMock = this.#d2cMocks.get(task);
30217
30474
  if (readyPreview === void 0 || readyMock === void 0) throw new Error("D2C preview or backend service is unavailable after synchronization");
30218
- const manifestPath = join42(taskDir(workspace, task), "design", "interaction-manifest.json");
30475
+ const manifestPath = join43(taskDir(workspace, task), "design", "interaction-manifest.json");
30219
30476
  const manifest = parseInteractionManifest(await readFile35(manifestPath, "utf8"));
30220
30477
  const baseline = await this.#verifyAcceptanceBaseline(workspace, task);
30221
30478
  const coverage = baseline === void 0 ? void 0 : verifyRequirementCoverage(baseline.approvedPrd.criteria, manifest);
@@ -30227,7 +30484,7 @@ VITE_API_BASE_URL=${running.url}
30227
30484
  await mkdir27(planDir, { recursive: true });
30228
30485
  try {
30229
30486
  const observations = await this.#observeD2cPages(manifest, readyPreview.url);
30230
- const prd = await readFile35(join42(taskDir(workspace, task), "product", "prd.md"), "utf8").catch(() => void 0);
30487
+ const prd = await readFile35(join43(taskDir(workspace, task), "product", "prd.md"), "utf8").catch(() => void 0);
30231
30488
  const apiContext = JSON.stringify({ openapi: workflow.openapi, mappings: workflow.mappings }, null, 2);
30232
30489
  const plan = await this.#d2cJudge.planInteractions({ task, seed: manifest, observations, ...prd === void 0 ? {} : { prd }, apiContext });
30233
30490
  executableManifest = mergeInteractionManifests(manifest, plan.manifest);
@@ -30238,9 +30495,9 @@ VITE_API_BASE_URL=${running.url}
30238
30495
  plannedScenarios: executableManifest.pages.reduce((total, page) => total + page.scenarios.length, 0),
30239
30496
  pageAnalyses: plan.pageAnalyses
30240
30497
  };
30241
- await writeFile17(join42(planDir, "autonomous-interaction-plan.json"), `${JSON.stringify({ ...plan, manifest: executableManifest }, null, 2)}
30498
+ await writeFile17(join43(planDir, "autonomous-interaction-plan.json"), `${JSON.stringify({ ...plan, manifest: executableManifest }, null, 2)}
30242
30499
  `, { encoding: "utf8", mode: 384 });
30243
- await rm15(join42(planDir, "autonomous-interaction-diagnostic.json"), { force: true });
30500
+ await rm15(join43(planDir, "autonomous-interaction-diagnostic.json"), { force: true });
30244
30501
  } catch (cause) {
30245
30502
  const detail = message(cause);
30246
30503
  review = {
@@ -30248,8 +30505,8 @@ VITE_API_BASE_URL=${running.url}
30248
30505
  plannedScenarios: manifest.pages.reduce((total, page) => total + page.scenarios.length, 0),
30249
30506
  warning: `\u591A\u6A21\u6001\u81EA\u4E3B\u89C4\u5212\u5931\u8D25\uFF0C\u5DF2\u7EE7\u7EED\u6267\u884C\u5DF2\u786E\u8BA4\u7684\u8BBE\u8BA1\u5951\u7EA6\uFF1A${detail}`
30250
30507
  };
30251
- await rm15(join42(planDir, "autonomous-interaction-plan.json"), { force: true });
30252
- await writeFile17(join42(planDir, "autonomous-interaction-diagnostic.json"), `${JSON.stringify({
30508
+ await rm15(join43(planDir, "autonomous-interaction-plan.json"), { force: true });
30509
+ await writeFile17(join43(planDir, "autonomous-interaction-diagnostic.json"), `${JSON.stringify({
30253
30510
  schema: 1,
30254
30511
  stage: "planning",
30255
30512
  occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -30263,13 +30520,13 @@ VITE_API_BASE_URL=${running.url}
30263
30520
  let result = { ...rawResult, evidenceMode: review.mode };
30264
30521
  if (coverage !== void 0) {
30265
30522
  const evidence = createAcceptanceEvidence(coverage, result);
30266
- await writeFile17(join42(taskDir(workspace, task), "acceptance-evidence.json"), `${JSON.stringify({ ...evidence, baseline }, null, 2)}
30523
+ await writeFile17(join43(taskDir(workspace, task), "acceptance-evidence.json"), `${JSON.stringify({ ...evidence, baseline }, null, 2)}
30267
30524
  `, { encoding: "utf8", mode: 384 });
30268
30525
  if (!evidence.passed) result = { ...result, passed: false, failures: Math.max(1, result.failures) };
30269
30526
  }
30270
30527
  const dir = integrationDirectory(workspace, task);
30271
30528
  await mkdir27(dir, { recursive: true });
30272
- await writeFile17(join42(dir, "interaction-results.json"), `${JSON.stringify(result, null, 2)}
30529
+ await writeFile17(join43(dir, "interaction-results.json"), `${JSON.stringify(result, null, 2)}
30273
30530
  `);
30274
30531
  const next = await updateWorkflow(workspace, task, workflow.revision, (current) => applyInteractionRun(current, result));
30275
30532
  return { workflow: next, result, review };
@@ -30282,7 +30539,7 @@ VITE_API_BASE_URL=${running.url}
30282
30539
  if (mock === void 0 || !mock.exited() || result.failures === 0) return result;
30283
30540
  const output = mock.output().trim();
30284
30541
  try {
30285
- const dir = join42(d2cOutputDirectory(workspace, task), "mock-server.log");
30542
+ const dir = join43(d2cOutputDirectory(workspace, task), "mock-server.log");
30286
30543
  await writeFile17(dir, `${output}
30287
30544
  `);
30288
30545
  } catch {
@@ -30377,7 +30634,7 @@ VITE_API_BASE_URL=${running.url}
30377
30634
  if (current === void 0) throw new Error("D2C product plan is unavailable");
30378
30635
  if (current.plan.approvedPrd !== void 0) throw new Error("The approved PRD is immutable and cannot be edited");
30379
30636
  if (current.plan.phase !== "prd-review") throw new Error("PRD sections can only be edited during PRD review");
30380
- const path = join42(taskDir(workspace, task), "product", "prd.md");
30637
+ const path = join43(taskDir(workspace, task), "product", "prd.md");
30381
30638
  const updated = await updatePrdSectionFile(path, { sectionId, body, expectedHash });
30382
30639
  const now = (/* @__PURE__ */ new Date()).toISOString();
30383
30640
  await writeD2cProductPlan(workspace, {
@@ -30397,33 +30654,33 @@ VITE_API_BASE_URL=${running.url}
30397
30654
  async #assertProductPrdLocked(workspace, task) {
30398
30655
  const current = await readD2cProductPlanView(workspace, task);
30399
30656
  if (current?.plan.approvedPrd === void 0) return;
30400
- await assertApprovedPrd(join42(taskDir(workspace, task), "product", "prd.md"), current.plan.approvedPrd);
30657
+ await assertApprovedPrd(join43(taskDir(workspace, task), "product", "prd.md"), current.plan.approvedPrd);
30401
30658
  }
30402
30659
  async #acceptancePaths(workspace, task) {
30403
- const product = join42(taskDir(workspace, task), "product");
30404
- const openapi = join42(product, "openapi.json");
30660
+ const product = join43(taskDir(workspace, task), "product");
30661
+ const openapi = join43(product, "openapi.json");
30405
30662
  return {
30406
- prd: join42(product, "prd.md"),
30407
- prototype: join42(product, "prototype", "index.html"),
30408
- interaction: join42(product, "prototype", "interaction-manifest.json"),
30409
- ...await stat14(openapi).catch(() => void 0) === void 0 ? {} : { openapi }
30663
+ prd: join43(product, "prd.md"),
30664
+ prototype: join43(product, "prototype", "index.html"),
30665
+ interaction: join43(product, "prototype", "interaction-manifest.json"),
30666
+ ...await stat15(openapi).catch(() => void 0) === void 0 ? {} : { openapi }
30410
30667
  };
30411
30668
  }
30412
30669
  async #verifyAcceptanceBaseline(workspace, task) {
30413
30670
  const product = await readD2cProductPlanView(workspace, task);
30414
30671
  if (product?.plan.approvedPrd === void 0) return void 0;
30415
30672
  await this.#assertProductPrdLocked(workspace, task);
30416
- const baseline = JSON.parse(await readFile35(join42(taskDir(workspace, task), "acceptance-baseline.json"), "utf8"));
30673
+ const baseline = JSON.parse(await readFile35(join43(taskDir(workspace, task), "acceptance-baseline.json"), "utf8"));
30417
30674
  await verifyAcceptanceBaseline(await this.#acceptancePaths(workspace, task), baseline);
30418
- const design = join42(taskDir(workspace, task), "design");
30675
+ const design = join43(taskDir(workspace, task), "design");
30419
30676
  await Promise.all([
30420
- verifyAcceptanceArtifact(join42(design, "index.html"), baseline.artifacts.prototype, "runtime prototype"),
30421
- verifyAcceptanceArtifact(join42(design, "interaction-manifest.json"), baseline.artifacts.interaction, "runtime interaction contract")
30677
+ verifyAcceptanceArtifact(join43(design, "index.html"), baseline.artifacts.prototype, "runtime prototype"),
30678
+ verifyAcceptanceArtifact(join43(design, "interaction-manifest.json"), baseline.artifacts.interaction, "runtime interaction contract")
30422
30679
  ]);
30423
30680
  const workflow = await readWorkflow(workspace, task);
30424
30681
  if (workflow?.openapi !== void 0) {
30425
30682
  await verifyAcceptanceArtifact(
30426
- join42(taskDir(workspace, task), "integration", "swagger.json"),
30683
+ join43(taskDir(workspace, task), "integration", "swagger.json"),
30427
30684
  { hash: workflow.openapi.hash },
30428
30685
  "runtime OpenAPI contract"
30429
30686
  );
@@ -30444,14 +30701,14 @@ VITE_API_BASE_URL=${running.url}
30444
30701
  throw new Error("D2C PRD is empty and cannot be accepted");
30445
30702
  }
30446
30703
  if (stage === "design" && accepted) {
30447
- const manifest = join42(d2cProductPrototypeDirectory(workspace, task), "interaction-manifest.json");
30704
+ const manifest = join43(d2cProductPrototypeDirectory(workspace, task), "interaction-manifest.json");
30448
30705
  const normalizedManifest = parseInteractionManifest(await readFile35(manifest, "utf8"));
30449
30706
  if (current.plan.approvedPrd === void 0) throw new Error("Approve the PRD before confirming the design");
30450
30707
  verifyRequirementCoverage(current.plan.approvedPrd.criteria, normalizedManifest);
30451
30708
  await writeFile17(manifest, `${JSON.stringify(normalizedManifest, null, 2)}
30452
30709
  `, { encoding: "utf8", mode: 384 });
30453
30710
  const baseline = await captureAcceptanceBaseline(await this.#acceptancePaths(workspace, task), current.plan.approvedPrd);
30454
- await writeFile17(join42(taskDir(workspace, task), "acceptance-baseline.json"), `${JSON.stringify(baseline, null, 2)}
30711
+ await writeFile17(join43(taskDir(workspace, task), "acceptance-baseline.json"), `${JSON.stringify(baseline, null, 2)}
30455
30712
  `, { encoding: "utf8", mode: 384 });
30456
30713
  }
30457
30714
  const plan = await writeD2cProductPlan(
@@ -30485,8 +30742,8 @@ VITE_API_BASE_URL=${running.url}
30485
30742
  const imported = await this.importD2cDesign(task, d2cProductPrototypeDirectory(workspace, task));
30486
30743
  const delivery = await readDeliveryRun(workspace, task);
30487
30744
  if (delivery !== void 0) {
30488
- const prototype = await readFile35(join42(d2cProductPrototypeDirectory(workspace, task), "index.html"), "utf8");
30489
- const interaction = await readFile35(join42(d2cProductPrototypeDirectory(workspace, task), "interaction-manifest.json"), "utf8");
30745
+ const prototype = await readFile35(join43(d2cProductPrototypeDirectory(workspace, task), "index.html"), "utf8");
30746
+ const interaction = await readFile35(join43(d2cProductPrototypeDirectory(workspace, task), "interaction-manifest.json"), "utf8");
30490
30747
  const completed = await updateDeliveryRun(workspace, task, delivery.revision, (run) => completeDeliveryNode(run, "design", [
30491
30748
  artifactRef("product/prototype/index.html", prototype),
30492
30749
  artifactRef("product/prototype/interaction-manifest.json", interaction)
@@ -30570,10 +30827,10 @@ VITE_API_BASE_URL=${running.url}
30570
30827
  const next = await updateWorkflow(workspace, task, workflow.revision, (current) => applyQualityJudgment(current, judgment));
30571
30828
  const qualityContent = `${JSON.stringify(judgment, null, 2)}
30572
30829
  `;
30573
- await writeFile17(join42(taskDir(workspace, task), "quality-judge.json"), qualityContent, { encoding: "utf8", mode: 384 });
30830
+ await writeFile17(join43(taskDir(workspace, task), "quality-judge.json"), qualityContent, { encoding: "utf8", mode: 384 });
30574
30831
  const delivery = await readDeliveryRun(workspace, task);
30575
30832
  if (next.stage === "completed" && delivery?.nodes.acceptance.status === "running") {
30576
- const interactionContent = await readFile35(join42(integrationDirectory(workspace, task), "interaction-results.json"), "utf8");
30833
+ const interactionContent = await readFile35(join43(integrationDirectory(workspace, task), "interaction-results.json"), "utf8");
30577
30834
  const accepted = await updateDeliveryRun(workspace, task, delivery.revision, (run) => completeDeliveryNode(run, "acceptance", [
30578
30835
  artifactRef("integration/interaction-results.json", interactionContent),
30579
30836
  artifactRef("quality-judge.json", qualityContent)
@@ -30755,14 +31012,14 @@ function desktopWindowChrome(platform = process.platform) {
30755
31012
 
30756
31013
  // src/utils/crash-guard.ts
30757
31014
  import { mkdirSync as mkdirSync3, writeFileSync } from "fs";
30758
- import { join as join43 } from "path";
31015
+ import { join as join44 } from "path";
30759
31016
  var installed = false;
30760
31017
  function installCrashGuard(options = {}) {
30761
31018
  if (installed) return;
30762
31019
  installed = true;
30763
31020
  const crashLogDir = () => {
30764
31021
  try {
30765
- return join43((options.workspace ?? process.cwd)(), ".flavor");
31022
+ return join44((options.workspace ?? process.cwd)(), ".flavor");
30766
31023
  } catch {
30767
31024
  return process.cwd();
30768
31025
  }
@@ -30771,7 +31028,7 @@ function installCrashGuard(options = {}) {
30771
31028
  try {
30772
31029
  const dir = crashLogDir();
30773
31030
  mkdirSync3(dir, { recursive: true });
30774
- const path = join43(dir, `crash-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "")}-${process.pid}.log`);
31031
+ const path = join44(dir, `crash-${(/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "")}-${process.pid}.log`);
30775
31032
  writeFileSync(path, [
30776
31033
  `kind: ${kind}`,
30777
31034
  `version: ${process.version}`,
@@ -30816,7 +31073,7 @@ ${reason.stack ?? ""}`;
30816
31073
 
30817
31074
  // src/desktop/git-manager.ts
30818
31075
  import { readFile as readFile36, rm as rm16 } from "fs/promises";
30819
- import { isAbsolute as isAbsolute18, relative as relative26, resolve as resolve36 } from "path";
31076
+ import { isAbsolute as isAbsolute18, relative as relative26, resolve as resolve37 } from "path";
30820
31077
  function failure(action, stderr, stdout) {
30821
31078
  return new Error(`${action}\u5931\u8D25\uFF1A${(stderr.trim() || stdout.trim() || "\u672A\u77E5 Git \u9519\u8BEF").split("\n")[0]}`);
30822
31079
  }
@@ -30909,13 +31166,13 @@ async function desktopLastTurnDiff(workspace, sessionId, paths = []) {
30909
31166
  return path.replaceAll("\\", "/");
30910
31167
  }));
30911
31168
  try {
30912
- const tree = JSON.parse(await readFile36(resolve36(workspace, ".flavor", "session-trees", sessionId, "tree.json"), "utf8"));
31169
+ const tree = JSON.parse(await readFile36(resolve37(workspace, ".flavor", "session-trees", sessionId, "tree.json"), "utf8"));
30913
31170
  const leaf = tree.nodes?.find((node) => node.id === tree.leafId);
30914
31171
  const parent = leaf?.parentId == null ? void 0 : tree.nodes?.find((node) => node.id === leaf.parentId);
30915
31172
  if (leaf === void 0 || parent === void 0) return desktopGitReviewDiff(workspace, { scope: "last-turn", paths });
30916
31173
  const load = async (id) => {
30917
31174
  if (!/^checkpoint-[A-Za-z0-9-]+$/.test(id)) throw new Error("Invalid checkpoint id");
30918
- return JSON.parse(await readFile36(resolve36(workspace, ".flavor", "checkpoints", "manifests", `${id}.json`), "utf8"));
31175
+ return JSON.parse(await readFile36(resolve37(workspace, ".flavor", "checkpoints", "manifests", `${id}.json`), "utf8"));
30919
31176
  };
30920
31177
  const [before, after] = await Promise.all([load(parent.checkpointId), load(leaf.checkpointId)]);
30921
31178
  const oldFiles = new Map(before.files.map((file) => [file.path, file.digest]));
@@ -30924,7 +31181,7 @@ async function desktopLastTurnDiff(workspace, sessionId, paths = []) {
30924
31181
  const object = async (digest) => {
30925
31182
  if (digest === void 0) return "";
30926
31183
  if (!/^[a-f0-9]{64}$/.test(digest)) throw new Error("Invalid checkpoint object");
30927
- const value = await readFile36(resolve36(workspace, ".flavor", "checkpoints", "objects", digest));
31184
+ const value = await readFile36(resolve37(workspace, ".flavor", "checkpoints", "objects", digest));
30928
31185
  return value.includes(0) ? "[binary content]" : value.toString("utf8");
30929
31186
  };
30930
31187
  const chunks = await Promise.all(changed.map(async (path) => wholeFileDiff(path, await object(oldFiles.get(path)), await object(newFiles.get(path)))));
@@ -30949,8 +31206,8 @@ async function assertListed(workspace, path) {
30949
31206
  }
30950
31207
  function safeWorkspacePath(workspace, path) {
30951
31208
  if (isAbsolute18(path)) throw new Error("Git \u6587\u4EF6\u8DEF\u5F84\u5FC5\u987B\u662F\u9879\u76EE\u5185\u7684\u76F8\u5BF9\u8DEF\u5F84");
30952
- const root = resolve36(workspace);
30953
- const target = resolve36(root, path);
31209
+ const root = resolve37(workspace);
31210
+ const target = resolve37(root, path);
30954
31211
  const child = relative26(root, target);
30955
31212
  if (child === "" || child.startsWith("..") || isAbsolute18(child)) throw new Error("\u62D2\u7EDD\u8BBF\u95EE\u9879\u76EE\u4E4B\u5916\u7684\u8DEF\u5F84");
30956
31213
  return target;
@@ -30963,14 +31220,14 @@ function safeGitTarget(value) {
30963
31220
 
30964
31221
  // src/desktop/worktree-manager.ts
30965
31222
  import { randomUUID as randomUUID28 } from "crypto";
30966
- import { mkdir as mkdir28, readFile as readFile37, readdir as readdir13, rm as rm17, stat as stat15, writeFile as writeFile18 } from "fs/promises";
30967
- import { basename as basename10, isAbsolute as isAbsolute19, join as join44, relative as relative27, resolve as resolve37, sep as sep27 } from "path";
31223
+ import { mkdir as mkdir28, readFile as readFile37, readdir as readdir13, rm as rm17, stat as stat16, writeFile as writeFile18 } from "fs/promises";
31224
+ import { basename as basename10, isAbsolute as isAbsolute19, join as join45, relative as relative27, resolve as resolve38, sep as sep27 } from "path";
30968
31225
  var DesktopWorktreeManager = class {
30969
31226
  #repository;
30970
31227
  #storage;
30971
31228
  constructor(options) {
30972
- this.#repository = resolve37(options.repository);
30973
- this.#storage = resolve37(options.storage);
31229
+ this.#repository = resolve38(options.repository);
31230
+ this.#storage = resolve38(options.storage);
30974
31231
  }
30975
31232
  async create(label) {
30976
31233
  if (!await isGitRepository(this.#repository)) throw new Error("Task worktrees require a Git repository");
@@ -31010,8 +31267,8 @@ var DesktopWorktreeManager = class {
31010
31267
  }
31011
31268
  const values = await Promise.all(names.filter((name) => name.endsWith(".json")).map(async (name) => {
31012
31269
  try {
31013
- const stored = validateStored(JSON.parse(await readFile37(join44(this.#storage, name), "utf8")), this.#repository, this.#storage);
31014
- if ((await stat15(stored.path).catch(() => void 0))?.isDirectory() !== true) return void 0;
31270
+ const stored = validateStored(JSON.parse(await readFile37(join45(this.#storage, name), "utf8")), this.#repository, this.#storage);
31271
+ if ((await stat16(stored.path).catch(() => void 0))?.isDirectory() !== true) return void 0;
31015
31272
  const status = await git2(stored.path, ["status", "--porcelain"]);
31016
31273
  const merged = await git2(this.#repository, ["merge-base", "--is-ancestor", stored.branch, "HEAD"]);
31017
31274
  const { version: _version, ...item } = stored;
@@ -31051,7 +31308,7 @@ var DesktopWorktreeManager = class {
31051
31308
  return this.#safeStoragePath(`${id}.json`);
31052
31309
  }
31053
31310
  #safeStoragePath(name) {
31054
- const target = resolve37(this.#storage, name);
31311
+ const target = resolve38(this.#storage, name);
31055
31312
  const delta = relative27(this.#storage, target);
31056
31313
  if (delta === "" || delta === ".." || delta.startsWith(`..${sep27}`) || isAbsolute19(delta)) throw new Error("Worktree path escapes storage");
31057
31314
  return target;
@@ -31060,10 +31317,10 @@ var DesktopWorktreeManager = class {
31060
31317
  function validateStored(value, repository, storage) {
31061
31318
  if (typeof value !== "object" || value === null) throw new Error("Invalid worktree metadata");
31062
31319
  const item = value;
31063
- if (item.version !== 1 || item.mode !== "worktree" || typeof item.id !== "string" || typeof item.path !== "string" || typeof item.branch !== "string" || typeof item.createdAt !== "string" || resolve37(item.repository ?? "") !== repository) {
31320
+ if (item.version !== 1 || item.mode !== "worktree" || typeof item.id !== "string" || typeof item.path !== "string" || typeof item.branch !== "string" || typeof item.createdAt !== "string" || resolve38(item.repository ?? "") !== repository) {
31064
31321
  throw new Error("Invalid worktree metadata");
31065
31322
  }
31066
- const path = resolve37(item.path);
31323
+ const path = resolve38(item.path);
31067
31324
  const delta = relative27(storage, path);
31068
31325
  if (delta === "" || delta === ".." || delta.startsWith(`..${sep27}`) || isAbsolute19(delta) || basename10(path) !== item.id) {
31069
31326
  throw new Error("Worktree metadata escapes storage");
@@ -31072,17 +31329,17 @@ function validateStored(value, repository, storage) {
31072
31329
  }
31073
31330
 
31074
31331
  // src/desktop/workbench-service.ts
31075
- import { readFile as readFile38, readdir as readdir14, stat as stat16 } from "fs/promises";
31332
+ import { readFile as readFile38, readdir as readdir14, stat as stat17 } from "fs/promises";
31076
31333
  import { homedir as homedir5 } from "os";
31077
- import { join as join45, resolve as resolve38 } from "path";
31334
+ import { join as join46, resolve as resolve39 } from "path";
31078
31335
  var MAX_TEXT = 128e3;
31079
31336
  var MAX_ITEMS = 200;
31080
31337
  var DesktopWorkbenchService = class {
31081
31338
  #workspace;
31082
31339
  #home;
31083
31340
  constructor(workspace, home = homedir5()) {
31084
- this.#workspace = resolve38(workspace);
31085
- this.#home = resolve38(home);
31341
+ this.#workspace = resolve39(workspace);
31342
+ this.#home = resolve39(home);
31086
31343
  }
31087
31344
  async inspect(sessionId) {
31088
31345
  const [goals, instructions, permissionFiles, audit, context] = await Promise.all([
@@ -31107,11 +31364,11 @@ var DesktopWorkbenchService = class {
31107
31364
  return url.toString().replace(/\/$/, input.trim().endsWith("/") ? "/" : "");
31108
31365
  }
31109
31366
  async #goals() {
31110
- const root = join45(this.#workspace, ".flavor", "goals");
31367
+ const root = join46(this.#workspace, ".flavor", "goals");
31111
31368
  const names = await readdir14(root).catch((error) => error.code === "ENOENT" ? [] : Promise.reject(error));
31112
31369
  const values = await Promise.all(names.filter((name) => /^goal-[A-Za-z0-9._-]+\.json$/.test(name)).slice(0, MAX_ITEMS).map(async (name) => {
31113
31370
  try {
31114
- return GoalStateSchema.parse(JSON.parse(await boundedRead2(join45(root, name))));
31371
+ return GoalStateSchema.parse(JSON.parse(await boundedRead2(join46(root, name))));
31115
31372
  } catch {
31116
31373
  return void 0;
31117
31374
  }
@@ -31121,7 +31378,7 @@ var DesktopWorkbenchService = class {
31121
31378
  async #instructions() {
31122
31379
  const names = ["FLAVOR.md", "AGENTS.md", "CLAUDE.md", "AGENTS.local.md", "CLAUDE.local.md"];
31123
31380
  const items = await Promise.all(names.map(async (name) => {
31124
- const path = join45(this.#workspace, name);
31381
+ const path = join46(this.#workspace, name);
31125
31382
  try {
31126
31383
  return { name, path, content: redactErrorText(await boundedRead2(path)) };
31127
31384
  } catch (error) {
@@ -31133,10 +31390,10 @@ var DesktopWorkbenchService = class {
31133
31390
  }
31134
31391
  async #permissionFiles() {
31135
31392
  const candidates = [
31136
- ...process.env.FLAVOR_MANAGED_PERMISSIONS ? [{ tier: "managed", path: resolve38(process.env.FLAVOR_MANAGED_PERMISSIONS), content: "" }] : [],
31137
- { tier: "user", path: join45(this.#home, ".flavor-code", "permissions.json"), content: "" },
31138
- { tier: "project", path: join45(this.#workspace, ".flavor", "permissions.json"), content: "" },
31139
- { tier: "local", path: join45(this.#workspace, ".flavor", "permissions.local.json"), content: "" }
31393
+ ...process.env.FLAVOR_MANAGED_PERMISSIONS ? [{ tier: "managed", path: resolve39(process.env.FLAVOR_MANAGED_PERMISSIONS), content: "" }] : [],
31394
+ { tier: "user", path: join46(this.#home, ".flavor-code", "permissions.json"), content: "" },
31395
+ { tier: "project", path: join46(this.#workspace, ".flavor", "permissions.json"), content: "" },
31396
+ { tier: "local", path: join46(this.#workspace, ".flavor", "permissions.local.json"), content: "" }
31140
31397
  ];
31141
31398
  const values = await Promise.all(candidates.map(async (item) => {
31142
31399
  try {
@@ -31151,7 +31408,7 @@ var DesktopWorkbenchService = class {
31151
31408
  async #audit() {
31152
31409
  let raw;
31153
31410
  try {
31154
- raw = await boundedRead2(join45(this.#workspace, ".flavor", "audit.jsonl"));
31411
+ raw = await boundedRead2(join46(this.#workspace, ".flavor", "audit.jsonl"));
31155
31412
  } catch (error) {
31156
31413
  if (error.code === "ENOENT") return [];
31157
31414
  throw error;
@@ -31166,7 +31423,7 @@ var DesktopWorkbenchService = class {
31166
31423
  }).reverse();
31167
31424
  }
31168
31425
  async #context(sessionId) {
31169
- const usage = await readJsonLines2(join45(this.#workspace, ".flavor", "usage.jsonl"));
31426
+ const usage = await readJsonLines2(join46(this.#workspace, ".flavor", "usage.jsonl"));
31170
31427
  if (sessionId === void 0) return { visibility: [], usage };
31171
31428
  try {
31172
31429
  const document = await new SessionStore({ workspace: this.#workspace }).load(sessionId);
@@ -31177,7 +31434,7 @@ var DesktopWorkbenchService = class {
31177
31434
  }
31178
31435
  };
31179
31436
  async function boundedRead2(path) {
31180
- const info = await stat16(path);
31437
+ const info = await stat17(path);
31181
31438
  if (!info.isFile()) throw new Error(`Expected a file: ${path}`);
31182
31439
  if (info.size > MAX_TEXT) throw new Error(`Workbench file exceeds ${MAX_TEXT} bytes: ${path}`);
31183
31440
  return readFile38(path, "utf8");
@@ -31204,82 +31461,6 @@ function redact2(value, key = "") {
31204
31461
  return typeof value === "string" && value.length > MAX_TEXT ? `${value.slice(0, MAX_TEXT)}\u2026` : value;
31205
31462
  }
31206
31463
 
31207
- // src/desktop/astgraph-service.ts
31208
- import { stat as stat17 } from "fs/promises";
31209
- import { createRequire as nodeCreateRequire2 } from "module";
31210
- import { join as join46, resolve as resolve39 } from "path";
31211
- var FIELDS = "id, kind, name, qualified_name, file_path, language, start_line, end_line, signature";
31212
- var DesktopAstGraphService = class {
31213
- #path;
31214
- constructor(workspace) {
31215
- this.#path = join46(resolve39(workspace), ".flavor", "astgraph", "index.db");
31216
- }
31217
- async status() {
31218
- if ((await stat17(this.#path).catch(() => void 0))?.isFile() !== true) {
31219
- return { available: false, path: this.#path, files: 0, nodes: 0, edges: 0 };
31220
- }
31221
- return this.#read((db) => {
31222
- const count = (table) => Number(db.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get().count);
31223
- const metadata = db.prepare("SELECT value FROM project_metadata WHERE key = 'last_index'").get();
31224
- let indexedAt;
31225
- try {
31226
- const at = Number(JSON.parse(metadata?.value ?? "{}").at);
31227
- if (Number.isFinite(at)) indexedAt = new Date(at).toISOString();
31228
- } catch {
31229
- }
31230
- return { available: true, path: this.#path, files: count("files"), nodes: count("nodes"), edges: count("edges"), ...indexedAt === void 0 ? {} : { indexedAt } };
31231
- });
31232
- }
31233
- async search(query, limit = 40) {
31234
- const value = query.trim().slice(0, 200);
31235
- if (value.length === 0 || !(await this.status()).available) return [];
31236
- 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));
31237
- }
31238
- async relations(id, hops = 2) {
31239
- if (!/^[A-Za-z0-9._:/#@+~-]{1,512}$/.test(id)) throw new Error("Invalid AST node id");
31240
- if (!(await this.status()).available) return { callers: [], callees: [], impact: [] };
31241
- return this.#read((db) => {
31242
- const originRow = db.prepare(`SELECT ${FIELDS} FROM nodes WHERE id = ?`).get(id);
31243
- const linked = (direction) => {
31244
- const source = direction === "callers" ? "e.source" : "e.target";
31245
- const predicate = direction === "callers" ? "e.target = ?" : "e.source = ?";
31246
- 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);
31247
- };
31248
- const seen = /* @__PURE__ */ new Map();
31249
- let frontier = [id];
31250
- for (let hop = 1; hop <= Math.min(Math.max(hops, 1), 4) && frontier.length > 0 && seen.size < 200; hop += 1) {
31251
- const next = [];
31252
- for (const current of frontier) {
31253
- 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);
31254
- for (const row of rows) if (row.other !== id && !seen.has(row.other)) {
31255
- seen.set(row.other, hop);
31256
- next.push(row.other);
31257
- }
31258
- }
31259
- frontier = next;
31260
- }
31261
- const impact = [...seen].flatMap(([nodeId, hop]) => {
31262
- const row = db.prepare(`SELECT ${FIELDS} FROM nodes WHERE id = ?`).get(nodeId);
31263
- return row === void 0 ? [] : [{ ...mapNode(row), hop }];
31264
- });
31265
- return { ...originRow === void 0 ? {} : { origin: mapNode(originRow) }, callers: linked("callers"), callees: linked("callees"), impact };
31266
- });
31267
- }
31268
- #read(run) {
31269
- const sqlite = nodeCreateRequire2(import.meta.url)("node:sqlite");
31270
- const db = new sqlite.DatabaseSync(this.#path, { readOnly: true });
31271
- try {
31272
- return run(db);
31273
- } finally {
31274
- db.close();
31275
- }
31276
- }
31277
- };
31278
- function mapNode(value) {
31279
- const row = value;
31280
- 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) } };
31281
- }
31282
-
31283
31464
  // src/desktop/main.ts
31284
31465
  installCrashGuard();
31285
31466
  var moduleDirectory = dirname24(fileURLToPath4(import.meta.url));