zelari-code 1.20.0 → 1.22.0

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.
Files changed (37) hide show
  1. package/dist/cli/app.js +2 -1
  2. package/dist/cli/app.js.map +1 -1
  3. package/dist/cli/budget/historySummary.js +127 -0
  4. package/dist/cli/budget/historySummary.js.map +1 -0
  5. package/dist/cli/budget/llmCompact.js +114 -0
  6. package/dist/cli/budget/llmCompact.js.map +1 -0
  7. package/dist/cli/budget/tokenBudget.js +111 -24
  8. package/dist/cli/budget/tokenBudget.js.map +1 -1
  9. package/dist/cli/components/StatusBar.js +4 -1
  10. package/dist/cli/components/StatusBar.js.map +1 -1
  11. package/dist/cli/hooks/conversationContext.js +7 -0
  12. package/dist/cli/hooks/conversationContext.js.map +1 -1
  13. package/dist/cli/hooks/historyCompaction.js +77 -14
  14. package/dist/cli/hooks/historyCompaction.js.map +1 -1
  15. package/dist/cli/hooks/permissionPicker.js +76 -0
  16. package/dist/cli/hooks/permissionPicker.js.map +1 -0
  17. package/dist/cli/hooks/useChatTurn.js +32 -5
  18. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  19. package/dist/cli/main.bundled.js +1483 -579
  20. package/dist/cli/main.bundled.js.map +4 -4
  21. package/dist/cli/plugins/registry.js +0 -13
  22. package/dist/cli/plugins/registry.js.map +1 -1
  23. package/dist/cli/runHeadless.js +30 -1
  24. package/dist/cli/runHeadless.js.map +1 -1
  25. package/dist/cli/safety/toolPermissions.js +123 -0
  26. package/dist/cli/safety/toolPermissions.js.map +1 -0
  27. package/dist/cli/sessionTodos.js +67 -0
  28. package/dist/cli/sessionTodos.js.map +1 -0
  29. package/dist/cli/toolRegistry.js +134 -43
  30. package/dist/cli/toolRegistry.js.map +1 -1
  31. package/dist/cli/tools/skillTool.js +78 -0
  32. package/dist/cli/tools/skillTool.js.map +1 -0
  33. package/dist/cli/tools/taskTool.js +81 -47
  34. package/dist/cli/tools/taskTool.js.map +1 -1
  35. package/dist/cli/tools/todoTools.js +68 -0
  36. package/dist/cli/tools/todoTools.js.map +1 -0
  37. package/package.json +2 -2
@@ -19175,6 +19175,55 @@ var init_paths = __esm({
19175
19175
  }
19176
19176
  });
19177
19177
 
19178
+ // src/cli/sessionTodos.ts
19179
+ function listSessionTodos() {
19180
+ return todos.map((t) => ({ ...t }));
19181
+ }
19182
+ function clearSessionTodos() {
19183
+ todos = [];
19184
+ }
19185
+ function writeSessionTodos(items, opts) {
19186
+ const merge2 = opts?.merge === true;
19187
+ const normalized = items.map((it, i) => ({
19188
+ id: (it.id?.trim() || `t${i + 1}`).slice(0, 64),
19189
+ content: it.content.trim().slice(0, 500),
19190
+ status: it.status ?? "pending"
19191
+ })).filter((t) => t.content.length > 0);
19192
+ if (!merge2) {
19193
+ todos = normalized.slice(0, 40);
19194
+ return listSessionTodos();
19195
+ }
19196
+ const byId = new Map(todos.map((t) => [t.id, t]));
19197
+ for (const t of normalized) {
19198
+ byId.set(t.id, t);
19199
+ }
19200
+ todos = [...byId.values()].slice(0, 40);
19201
+ return listSessionTodos();
19202
+ }
19203
+ function formatTodosForModel(list = todos) {
19204
+ if (list.length === 0) return "(no todos)";
19205
+ return list.map((t) => {
19206
+ const mark = t.status === "completed" ? "x" : t.status === "in_progress" ? ">" : t.status === "cancelled" ? "-" : " ";
19207
+ return `- [${mark}] ${t.id}: ${t.content} (${t.status})`;
19208
+ }).join("\n");
19209
+ }
19210
+ function formatTodoStatusSummary(list = todos) {
19211
+ if (list.length === 0) return null;
19212
+ const done = list.filter(
19213
+ (t) => t.status === "completed" || t.status === "cancelled"
19214
+ ).length;
19215
+ const active = list.filter((t) => t.status === "in_progress").length;
19216
+ const base = `todos ${done}/${list.length}`;
19217
+ return active > 0 ? `${base} \xB7 ${active} active` : base;
19218
+ }
19219
+ var todos;
19220
+ var init_sessionTodos = __esm({
19221
+ "src/cli/sessionTodos.ts"() {
19222
+ "use strict";
19223
+ todos = [];
19224
+ }
19225
+ });
19226
+
19178
19227
  // packages/core/dist/shared/events.js
19179
19228
  function createBrainEvent(type, sessionId, data) {
19180
19229
  return {
@@ -19577,7 +19626,7 @@ function extractToolObjects(text) {
19577
19626
  }
19578
19627
  return out;
19579
19628
  }
19580
- var AgentHarness;
19629
+ var AgentHarness, DOOM_LOOP_THRESHOLD;
19581
19630
  var init_AgentHarness = __esm({
19582
19631
  "packages/core/dist/core/AgentHarness.js"() {
19583
19632
  "use strict";
@@ -19603,6 +19652,12 @@ var init_AgentHarness = __esm({
19603
19652
  * real progress. Reset on every `run()` so it does not leak across turns.
19604
19653
  */
19605
19654
  toolCallCache = /* @__PURE__ */ new Map();
19655
+ /**
19656
+ * Count of identical tool+args invocations this run (incl. first).
19657
+ * v1.21.0 doom_loop: after DOOM_LOOP_THRESHOLD, return a hard error so the
19658
+ * model must change approach instead of soft-replaying forever.
19659
+ */
19660
+ toolCallCounts = /* @__PURE__ */ new Map();
19606
19661
  /**
19607
19662
  * How many times this run re-entered the provider because the model emitted
19608
19663
  * tool calls as a `---TOOLS---` text block (fallback format) instead of native
@@ -19690,21 +19745,30 @@ var init_AgentHarness = __esm({
19690
19745
  const maxParallel = Math.max(1, Number.parseInt(process.env.ZELARI_MAX_PARALLEL_TOOLS ?? "6", 10) || 6);
19691
19746
  const inflight = /* @__PURE__ */ new Map();
19692
19747
  const invokeOne = async (p3) => {
19693
- if (p3.cached !== void 0) {
19748
+ if (p3.skipped || !this.config.toolRegistry) {
19694
19749
  return {
19695
- content: p3.cached,
19696
- isError: p3.skipped || p3.cached.startsWith("[skipped]"),
19750
+ content: `[skipped] maxToolCallsPerTurn reached (limit=${maxToolCalls})`,
19751
+ isError: true,
19697
19752
  durationMs: 0
19698
19753
  };
19699
19754
  }
19700
- if (p3.skipped || !this.config.toolRegistry) {
19755
+ const callKey = hashToolCall(p3.toolName, p3.args);
19756
+ const nextCount = (this.toolCallCounts.get(callKey) ?? 0) + 1;
19757
+ this.toolCallCounts.set(callKey, nextCount);
19758
+ if (nextCount >= DOOM_LOOP_THRESHOLD) {
19701
19759
  return {
19702
- content: `[skipped] maxToolCallsPerTurn reached (limit=${maxToolCalls})`,
19760
+ content: `[doom_loop] Tool "${p3.toolName}" called ${nextCount} times with identical arguments. Stop repeating. Change approach: different path/args, another tool, or answer with what you already have.`,
19703
19761
  isError: true,
19704
19762
  durationMs: 0
19705
19763
  };
19706
19764
  }
19707
- const callKey = hashToolCall(p3.toolName, p3.args);
19765
+ if (p3.cached !== void 0) {
19766
+ return {
19767
+ content: p3.cached,
19768
+ isError: p3.cached.startsWith("[skipped]") || p3.cached.startsWith("[doom_loop]"),
19769
+ durationMs: 0
19770
+ };
19771
+ }
19708
19772
  const fromRunCache = this.toolCallCache.get(callKey);
19709
19773
  if (fromRunCache !== void 0) {
19710
19774
  return {
@@ -19857,6 +19921,7 @@ ${shared2.content}`,
19857
19921
  const startTime = Date.now();
19858
19922
  this.activeController = new AbortController();
19859
19923
  this.toolCallCache = /* @__PURE__ */ new Map();
19924
+ this.toolCallCounts = /* @__PURE__ */ new Map();
19860
19925
  this.textToolReentries = 0;
19861
19926
  const startEvent = createBrainEvent("agent_start", this.sessionId, {
19862
19927
  model: this.config.model,
@@ -20343,6 +20408,7 @@ ${cached2}`
20343
20408
  yield msgEnd;
20344
20409
  }
20345
20410
  };
20411
+ DOOM_LOOP_THRESHOLD = 3;
20346
20412
  }
20347
20413
  });
20348
20414
 
@@ -20492,6 +20558,7 @@ var init_sessionJsonl = __esm({
20492
20558
  var harness_exports = {};
20493
20559
  __export(harness_exports, {
20494
20560
  AgentHarness: () => AgentHarness,
20561
+ DOOM_LOOP_THRESHOLD: () => DOOM_LOOP_THRESHOLD,
20495
20562
  SessionJsonlWriter: () => SessionJsonlWriter,
20496
20563
  collapseLoopedAssistantText: () => collapseLoopedAssistantText,
20497
20564
  detectAssistantTextLoop: () => detectAssistantTextLoop,
@@ -20879,27 +20946,112 @@ var init_openai_compatible = __esm({
20879
20946
  }
20880
20947
  });
20881
20948
 
20949
+ // packages/core/dist/core/tools/toolOutputSpill.js
20950
+ import { createHash, randomBytes } from "node:crypto";
20951
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
20952
+ import { homedir as homedir3, tmpdir } from "node:os";
20953
+ import { join } from "node:path";
20954
+ function resolveToolOutputDir() {
20955
+ const fromEnv = process.env.ZELARI_TOOL_OUTPUT_DIR?.trim();
20956
+ if (fromEnv)
20957
+ return fromEnv;
20958
+ try {
20959
+ return join(homedir3(), ".tmp", "zelari-code", "tool-output");
20960
+ } catch {
20961
+ return join(tmpdir(), "zelari-code", "tool-output");
20962
+ }
20963
+ }
20964
+ function isToolSpillEnabled() {
20965
+ const v = process.env.ZELARI_TOOL_SPILL?.trim().toLowerCase();
20966
+ if (v === "0" || v === "false" || v === "off" || v === "no")
20967
+ return false;
20968
+ return true;
20969
+ }
20970
+ function spillToolOutput(fullText, meta3) {
20971
+ if (!isToolSpillEnabled())
20972
+ return null;
20973
+ if (!fullText)
20974
+ return null;
20975
+ try {
20976
+ const dir = resolveToolOutputDir();
20977
+ if (!existsSync7(dir)) {
20978
+ mkdirSync6(dir, { recursive: true });
20979
+ }
20980
+ const hash3 = createHash("sha256").update(fullText).digest("hex").slice(0, 12);
20981
+ const stamp = Date.now().toString(36);
20982
+ const rnd = randomBytes(3).toString("hex");
20983
+ const safeTool = (meta3?.toolName ?? "tool").replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 32);
20984
+ const file2 = `${stamp}-${safeTool}-${hash3}-${rnd}.txt`;
20985
+ const path38 = join(dir, file2);
20986
+ writeFileSync5(path38, fullText, "utf8");
20987
+ return path38;
20988
+ } catch {
20989
+ return null;
20990
+ }
20991
+ }
20992
+ var init_toolOutputSpill = __esm({
20993
+ "packages/core/dist/core/tools/toolOutputSpill.js"() {
20994
+ "use strict";
20995
+ }
20996
+ });
20997
+
20882
20998
  // packages/core/dist/core/tools/registry.js
20883
- function truncateToolResult(text, cap2 = TOOL_RESULT_LINE_CAP) {
20999
+ function truncateToolResult(text, capOrOpts = TOOL_RESULT_LINE_CAP) {
20884
21000
  if (text.length === 0)
20885
21001
  return text;
21002
+ const opts = typeof capOrOpts === "number" ? { cap: capOrOpts } : capOrOpts ?? {};
21003
+ const cap2 = typeof opts.cap === "number" && Number.isFinite(opts.cap) && opts.cap >= 10 ? opts.cap : TOOL_RESULT_LINE_CAP;
21004
+ const doSpill = opts.spill !== false;
20886
21005
  const lines = text.split("\n");
20887
- if (lines.length <= cap2)
21006
+ const charBudget = cap2 * 80;
21007
+ const overLines = lines.length > cap2;
21008
+ const overChars = text.length > charBudget && lines.length <= cap2;
21009
+ if (!overLines && !overChars)
20888
21010
  return text;
20889
- const half = Math.floor(cap2 / 2);
20890
- const head = lines.slice(0, half);
20891
- const tail = lines.slice(lines.length - half);
20892
- const omitted = lines.length - cap2;
20893
- return head.join("\n") + `
20894
- \u2026 [+${omitted} lines omitted \u2014 showing head:${half}, tail:${half} of ${lines.length} total] \u2026
21011
+ let preview;
21012
+ let marker;
21013
+ if (overLines) {
21014
+ const half = Math.floor(cap2 / 2);
21015
+ const head = lines.slice(0, half);
21016
+ const tail = lines.slice(lines.length - half);
21017
+ const omitted = lines.length - cap2;
21018
+ marker = `+${omitted} lines omitted \u2014 showing head:${half}, tail:${half} of ${lines.length} total`;
21019
+ preview = head.join("\n") + `
21020
+ \u2026 [${marker}] \u2026
20895
21021
  ` + tail.join("\n");
21022
+ } else {
21023
+ const half = Math.floor(charBudget / 2);
21024
+ const head = text.slice(0, half);
21025
+ const tail = text.slice(text.length - half);
21026
+ const omitted = text.length - charBudget;
21027
+ marker = `+${omitted} chars omitted \u2014 showing head/tail of ${text.length} total (line-sparse payload)`;
21028
+ preview = `${head}
21029
+ \u2026 [${marker}] \u2026
21030
+ ${tail}`;
21031
+ }
21032
+ if (doSpill) {
21033
+ const path38 = spillToolOutput(text, { toolName: opts.toolName });
21034
+ if (path38) {
21035
+ const spillNote = `
21036
+ \u2026 [full output spilled to: ${path38} \u2014 re-read with read_file if you need the complete text] \u2026`;
21037
+ if (preview.includes("] \u2026\n")) {
21038
+ preview = preview.replace("] \u2026\n", `] \u2026${spillNote}
21039
+ `);
21040
+ } else {
21041
+ preview = preview + spillNote;
21042
+ }
21043
+ }
21044
+ }
21045
+ return preview;
20896
21046
  }
20897
21047
  var TOOL_NAME_ALIASES, TOOL_RESULT_LINE_CAP, ToolRegistry;
20898
21048
  var init_registry = __esm({
20899
21049
  "packages/core/dist/core/tools/registry.js"() {
20900
21050
  "use strict";
20901
21051
  init_zodBridge();
21052
+ init_toolOutputSpill();
20902
21053
  init_toolTypes();
21054
+ init_toolOutputSpill();
20903
21055
  TOOL_NAME_ALIASES = {
20904
21056
  read: "read_file",
20905
21057
  readfile: "read_file",
@@ -20981,13 +21133,19 @@ var init_registry = __esm({
20981
21133
  })
20982
21134
  ]);
20983
21135
  if (result.ok) {
21136
+ const tName = options.toolName ?? name;
20984
21137
  if (typeof result.value === "string") {
20985
- return { ok: true, value: truncateToolResult(result.value) };
21138
+ return {
21139
+ ok: true,
21140
+ value: truncateToolResult(result.value, {
21141
+ toolName: tName
21142
+ })
21143
+ };
20986
21144
  }
20987
21145
  if (result.value && typeof result.value === "object") {
20988
21146
  const v = result.value;
20989
21147
  if (typeof v.content === "string") {
20990
- v.content = truncateToolResult(v.content);
21148
+ v.content = truncateToolResult(v.content, { toolName: tName });
20991
21149
  }
20992
21150
  }
20993
21151
  }
@@ -21213,7 +21371,7 @@ var init_cmdline = __esm({
21213
21371
 
21214
21372
  // src/cli/diagnostics/engine.ts
21215
21373
  import { spawn as spawn2 } from "node:child_process";
21216
- import { existsSync as existsSync7 } from "node:fs";
21374
+ import { existsSync as existsSync8 } from "node:fs";
21217
21375
  import path16 from "node:path";
21218
21376
  function parseEslintJson(stdout, _file2) {
21219
21377
  const json2 = safeJson(stdout);
@@ -21282,7 +21440,7 @@ function resolveBin(bin, cwd) {
21282
21440
  for (let i = 0; i < 6; i += 1) {
21283
21441
  for (const suffix of suffixes) {
21284
21442
  const candidate = path16.join(dir, "node_modules", ".bin", `${bin}${suffix}`);
21285
- if (existsSync7(candidate)) return candidate;
21443
+ if (existsSync8(candidate)) return candidate;
21286
21444
  }
21287
21445
  const parent = path16.dirname(dir);
21288
21446
  if (parent === dir) break;
@@ -21388,6 +21546,26 @@ var init_engine = __esm({
21388
21546
  });
21389
21547
 
21390
21548
  // src/cli/tools/taskTool.ts
21549
+ function systemPromptForAgent(agent) {
21550
+ if (agent === "general") return GENERAL_PROMPT;
21551
+ if (agent === "verify") return VERIFY_PROMPT;
21552
+ return EXPLORE_PROMPT;
21553
+ }
21554
+ function maxToolCallsForThoroughness(thoroughness, agent) {
21555
+ if (agent === "general") {
21556
+ if (thoroughness === "quick") return 8;
21557
+ if (thoroughness === "deep") return 20;
21558
+ return 12;
21559
+ }
21560
+ if (agent === "verify") {
21561
+ if (thoroughness === "quick") return 6;
21562
+ if (thoroughness === "deep") return 14;
21563
+ return 10;
21564
+ }
21565
+ if (thoroughness === "quick") return 4;
21566
+ if (thoroughness === "deep") return 12;
21567
+ return 6;
21568
+ }
21391
21569
  async function runSubAgent(harness) {
21392
21570
  let current = "";
21393
21571
  let lastCompleted = "";
@@ -21417,36 +21595,40 @@ async function runSubAgent(harness) {
21417
21595
  function createTaskTool(deps) {
21418
21596
  return {
21419
21597
  name: "task",
21420
- description: 'Delegate a focused, READ-ONLY research/exploration sub-task to an isolated sub-agent that has its own fresh context and returns only a concise conclusion. Use it to keep your own context lean \u2014 e.g. "find where feature X is implemented and summarize how it works" \u2014 on large codebases. The sub-agent cannot edit files or run shell commands. Provide a fully self-contained `prompt` (it cannot see this conversation).',
21421
- // The sub-agent reads the workspace and may hit the network (fetch/search),
21422
- // but never writes or runs shell commands.
21423
- permissions: ["read", "network"],
21424
- // Sub-agents run a full (bounded) agent loop — allow generous wall time.
21598
+ description: "Delegate a focused sub-task to an isolated sub-agent with its own context; returns only a concise conclusion (keeps parent context lean).\n- agent=explore (default): read-only research/search\n- agent=general: can edit files for one bounded unit of work\n- agent=verify: read + bash to run tests/checks\nProvide a fully self-contained `prompt` (sub-agent cannot see this conversation).",
21599
+ permissions: ["read", "network", "write", "execute"],
21425
21600
  timeoutMs: 3e5,
21426
21601
  inputSchema: TaskArgsSchema,
21427
21602
  execute: async (args, ctx) => {
21603
+ const agent = args.agent ?? "explore";
21604
+ const thoroughness = args.thoroughness ?? "medium";
21428
21605
  let sub;
21429
21606
  try {
21430
- sub = await deps.createSubAgentContext();
21607
+ sub = await deps.createSubAgentContext({ agent, thoroughness });
21431
21608
  } catch (err) {
21432
- return typedErr(`task: could not initialize sub-agent \u2014 ${err instanceof Error ? err.message : String(err)}`);
21609
+ return typedErr(
21610
+ `task: could not initialize sub-agent \u2014 ${err instanceof Error ? err.message : String(err)}`
21611
+ );
21433
21612
  }
21434
21613
  if (!sub) {
21435
- return typedErr("task: no provider configured for the sub-agent (set an API key / run /login).");
21614
+ return typedErr(
21615
+ "task: no provider configured for the sub-agent (set an API key / run /login)."
21616
+ );
21436
21617
  }
21618
+ const maxToolCalls = maxToolCallsForThoroughness(thoroughness, agent);
21437
21619
  const config2 = {
21438
21620
  model: sub.model,
21439
21621
  provider: sub.provider,
21440
21622
  messages: [
21441
- { role: "system", content: SUBAGENT_SYSTEM_PROMPT },
21623
+ { role: "system", content: systemPromptForAgent(agent) },
21442
21624
  { role: "user", content: args.prompt }
21443
21625
  ],
21444
21626
  tools: sub.tools,
21445
21627
  toolRegistry: sub.registry,
21446
21628
  providerStream: sub.providerStream,
21447
21629
  cwd: ctx.cwd,
21448
- // Guard against a single turn ballooning context on "explore" tasks.
21449
- maxToolCallsPerTurn: 5
21630
+ maxToolCallsPerTurn: maxToolCalls,
21631
+ maxToolLoopIterations: Math.max(12, maxToolCalls + 4)
21450
21632
  };
21451
21633
  let harness;
21452
21634
  try {
@@ -21457,40 +21639,58 @@ function createTaskTool(deps) {
21457
21639
  harness = new AgentHarness2(config2);
21458
21640
  }
21459
21641
  } catch (err) {
21460
- return typedErr(`task: failed to start sub-agent \u2014 ${err instanceof Error ? err.message : String(err)}`);
21642
+ return typedErr(
21643
+ `task: failed to start sub-agent \u2014 ${err instanceof Error ? err.message : String(err)}`
21644
+ );
21461
21645
  }
21462
21646
  const { result, error: error51 } = await runSubAgent(harness);
21463
21647
  if (!result) {
21464
- return typedErr(`task: sub-agent produced no output${error51 ? ` (${error51})` : ""}.`);
21648
+ return typedErr(
21649
+ `task: sub-agent (${agent}) produced no output${error51 ? ` (${error51})` : ""}.`
21650
+ );
21465
21651
  }
21466
- return typedOk({ result });
21652
+ return typedOk({
21653
+ result: `[sub-agent:${agent}/${thoroughness}]
21654
+ ${result}`,
21655
+ agent
21656
+ });
21467
21657
  }
21468
21658
  };
21469
21659
  }
21470
- var SUBAGENT_SYSTEM_PROMPT, TaskArgsSchema;
21660
+ var EXPLORE_PROMPT, GENERAL_PROMPT, VERIFY_PROMPT, TaskArgsSchema;
21471
21661
  var init_taskTool = __esm({
21472
21662
  "src/cli/tools/taskTool.ts"() {
21473
21663
  "use strict";
21474
21664
  init_zod();
21475
21665
  init_toolTypes();
21476
- SUBAGENT_SYSTEM_PROMPT = [
21477
- "You are a focused sub-agent spawned to answer ONE bounded question or",
21478
- "complete ONE small research task for a parent coding agent.",
21479
- "",
21480
- "You have READ-ONLY tools (read files, list files, search, fetch URLs).",
21481
- "You cannot edit files or run shell commands \u2014 do not attempt to.",
21482
- "",
21483
- "Work efficiently: gather only what you need, then STOP and reply with a",
21484
- "concise, self-contained conclusion the parent can act on. Include concrete",
21485
- "references (file paths, line numbers, symbol names) but do not paste large",
21486
- "file dumps. Do not ask the parent follow-up questions \u2014 deliver your best",
21487
- "answer with what you can find."
21666
+ EXPLORE_PROMPT = [
21667
+ "You are a focused EXPLORE sub-agent for a parent coding agent.",
21668
+ "READ-ONLY tools only (read, list, grep, fetch). No edits, no shell.",
21669
+ "Gather only what you need, then STOP with a concise conclusion:",
21670
+ "file paths, symbols, line refs, and how things connect. No large dumps.",
21671
+ "Do not ask follow-up questions."
21672
+ ].join("\n");
21673
+ GENERAL_PROMPT = [
21674
+ "You are a GENERAL sub-agent that can read AND modify the codebase for one",
21675
+ "bounded unit of work. Prefer small, correct edits. Run checks when needed.",
21676
+ "Return a short report: what changed, files touched, remaining risks.",
21677
+ "Do not spawn further sub-agents. Do not expand scope beyond the prompt."
21678
+ ].join("\n");
21679
+ VERIFY_PROMPT = [
21680
+ "You are a VERIFY sub-agent. Confirm whether work is correct on disk.",
21681
+ "You may read files and run test/build commands via bash. Prefer",
21682
+ "targeted checks over full suite when possible.",
21683
+ "Report: pass/fail, commands run, key output, and gaps."
21488
21684
  ].join("\n");
21489
21685
  TaskArgsSchema = external_exports.object({
21490
21686
  description: external_exports.string().min(1).describe("A 3-6 word label for the sub-task (for logs/UI)."),
21491
21687
  prompt: external_exports.string().min(1).describe(
21492
21688
  "The full, self-contained instruction for the sub-agent. It has no access to this conversation, so include all context it needs."
21493
- )
21689
+ ),
21690
+ agent: external_exports.enum(["explore", "general", "verify"]).optional().describe(
21691
+ "Sub-agent type: explore (read-only research, default), general (can edit), verify (read + bash tests). Prefer explore for search; general for isolated edits."
21692
+ ),
21693
+ thoroughness: external_exports.enum(["quick", "medium", "deep"]).optional().describe("How deep the sub-agent should go (tool budget). Default medium.")
21494
21694
  });
21495
21695
  }
21496
21696
  });
@@ -21554,6 +21754,253 @@ var init_askUser = __esm({
21554
21754
  }
21555
21755
  });
21556
21756
 
21757
+ // src/cli/skillsMd.ts
21758
+ import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync5 } from "node:fs";
21759
+ import { join as join2 } from "node:path";
21760
+ import { homedir as homedir4 } from "node:os";
21761
+ function skillMdSearchDirs(projectRoot = process.cwd()) {
21762
+ return [
21763
+ join2(projectRoot, ".zelari", "skills"),
21764
+ join2(projectRoot, ".claude", "skills"),
21765
+ join2(projectRoot, ".opencode", "skills"),
21766
+ join2(homedir4(), ".zelari-code", "skills")
21767
+ ];
21768
+ }
21769
+ function parseSkillMd(content, sourcePath) {
21770
+ const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(content);
21771
+ if (!fmMatch) return null;
21772
+ const [, fmRaw = "", body = ""] = fmMatch;
21773
+ const fields = {};
21774
+ for (const line of fmRaw.split(/\r?\n/)) {
21775
+ const kv = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(line);
21776
+ if (!kv) continue;
21777
+ const key = (kv[1] ?? "").toLowerCase();
21778
+ let value = (kv[2] ?? "").trim();
21779
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
21780
+ value = value.slice(1, -1);
21781
+ }
21782
+ fields[key] = value;
21783
+ }
21784
+ const name = (fields["name"] ?? "").trim().toLowerCase();
21785
+ const description = (fields["description"] ?? "").trim();
21786
+ if (!name || !description) return null;
21787
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(name)) return null;
21788
+ const rawTools = fields["tools"] ?? fields["requiredtools"] ?? fields["required_tools"] ?? "";
21789
+ const requiredTools = rawTools.replace(/^\[|\]$/g, "").split(",").map((t) => t.trim().replace(/^["']|["']$/g, "")).filter((t) => t.length > 0);
21790
+ const rawCategory = (fields["category"] ?? "").trim().toLowerCase();
21791
+ const category = CODING_CATEGORIES.has(rawCategory) ? rawCategory : "maint";
21792
+ const rawCost = (fields["cost"] ?? fields["estimatedcost"] ?? "").trim().toLowerCase();
21793
+ const estimatedCost = rawCost === "low" || rawCost === "high" ? rawCost : "medium";
21794
+ const trimmedBody = body.trim();
21795
+ if (!trimmedBody) return null;
21796
+ return { name, description, category, requiredTools, estimatedCost, body: trimmedBody, sourcePath };
21797
+ }
21798
+ function toCodingSkillDefinition(parsed) {
21799
+ return {
21800
+ id: parsed.name,
21801
+ name: parsed.name,
21802
+ description: parsed.description,
21803
+ version: "1.0.0",
21804
+ category: parsed.category,
21805
+ systemPromptFragment: parsed.body,
21806
+ requiredTools: parsed.requiredTools,
21807
+ enabledByDefault: true,
21808
+ builtin: false,
21809
+ requires: [],
21810
+ examples: [],
21811
+ triggers: [],
21812
+ antiPatterns: [],
21813
+ requiredRoles: [],
21814
+ estimatedCost: parsed.estimatedCost,
21815
+ outputSchema: "string",
21816
+ relatedSkills: [],
21817
+ tags: ["user", "skill-md"]
21818
+ };
21819
+ }
21820
+ function loadSkillMdSkills(projectRoot = process.cwd(), options = {}) {
21821
+ const summary = { loaded: [], skipped: [] };
21822
+ const seen = new Set(options.existingIds ?? []);
21823
+ for (const dir of skillMdSearchDirs(projectRoot)) {
21824
+ if (!existsSync9(dir)) continue;
21825
+ let entries;
21826
+ try {
21827
+ entries = readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
21828
+ } catch {
21829
+ continue;
21830
+ }
21831
+ for (const entry of entries) {
21832
+ const skillPath = join2(dir, entry, "SKILL.md");
21833
+ if (!existsSync9(skillPath)) continue;
21834
+ try {
21835
+ const parsed = parseSkillMd(readFileSync5(skillPath, "utf8"), skillPath);
21836
+ if (!parsed) {
21837
+ summary.skipped.push({ path: skillPath, reason: "missing/invalid frontmatter (name, description) or empty body" });
21838
+ continue;
21839
+ }
21840
+ if (seen.has(parsed.name)) {
21841
+ summary.skipped.push({ path: skillPath, reason: `name "${parsed.name}" already registered (earlier dir or builtin wins)` });
21842
+ continue;
21843
+ }
21844
+ registerCodingSkill(toCodingSkillDefinition(parsed));
21845
+ seen.add(parsed.name);
21846
+ summary.loaded.push(parsed.name);
21847
+ } catch (err) {
21848
+ summary.skipped.push({ path: skillPath, reason: err instanceof Error ? err.message : String(err) });
21849
+ }
21850
+ }
21851
+ }
21852
+ return summary;
21853
+ }
21854
+ var CODING_CATEGORIES;
21855
+ var init_skillsMd = __esm({
21856
+ "src/cli/skillsMd.ts"() {
21857
+ "use strict";
21858
+ init_skills2();
21859
+ CODING_CATEGORIES = /* @__PURE__ */ new Set([
21860
+ "plan",
21861
+ "refactor",
21862
+ "debug",
21863
+ "review",
21864
+ "test",
21865
+ "docs",
21866
+ "ops",
21867
+ "git",
21868
+ "db",
21869
+ "maint"
21870
+ ]);
21871
+ }
21872
+ });
21873
+
21874
+ // src/cli/tools/skillTool.ts
21875
+ function formatAvailableSkillsCatalog(cwd = process.cwd()) {
21876
+ try {
21877
+ loadSkillMdSkills(cwd);
21878
+ } catch {
21879
+ }
21880
+ const skills = listCodingSkills();
21881
+ if (skills.length === 0) {
21882
+ return "No skills registered.";
21883
+ }
21884
+ const lines = skills.slice(0, 80).map((s) => {
21885
+ const desc = (s.description || s.id).replace(/\s+/g, " ").trim().slice(0, 160);
21886
+ return `- ${s.id}: ${desc}`;
21887
+ });
21888
+ return lines.join("\n");
21889
+ }
21890
+ function createSkillTool(opts) {
21891
+ const cwd = opts?.cwd ?? process.cwd();
21892
+ const catalog = formatAvailableSkillsCatalog(cwd);
21893
+ return {
21894
+ name: "skill",
21895
+ description: "Load the full instructions for a named coding skill into this turn. Call when a skill matches the current task. Available skills:\n" + catalog,
21896
+ permissions: ["read"],
21897
+ timeoutMs: 1e4,
21898
+ inputSchema: SkillArgsSchema,
21899
+ execute: async (args) => {
21900
+ try {
21901
+ loadSkillMdSkills(cwd);
21902
+ } catch {
21903
+ }
21904
+ const id = args.name.trim();
21905
+ const skill = getCodingSkillById(id) ?? listCodingSkills().find(
21906
+ (s) => s.id.toLowerCase() === id.toLowerCase() || s.name?.toLowerCase() === id.toLowerCase()
21907
+ );
21908
+ if (!skill) {
21909
+ const known = listCodingSkills().map((s) => s.id).slice(0, 40).join(", ");
21910
+ return typedErr(
21911
+ `Unknown skill "${id}". Known: ${known || "(none)"}. Use /skill or pick from the list.`
21912
+ );
21913
+ }
21914
+ const body = skill.systemPromptFragment?.trim() || skill.description || `(skill ${skill.id} has no body)`;
21915
+ const header = `# Skill: ${skill.id}
21916
+ ${skill.description ? `> ${skill.description}
21917
+
21918
+ ` : ""}`;
21919
+ return typedOk({
21920
+ name: skill.id,
21921
+ content: header + body
21922
+ });
21923
+ }
21924
+ };
21925
+ }
21926
+ var SkillArgsSchema;
21927
+ var init_skillTool = __esm({
21928
+ "src/cli/tools/skillTool.ts"() {
21929
+ "use strict";
21930
+ init_zod();
21931
+ init_skills2();
21932
+ init_toolTypes();
21933
+ init_skillsMd();
21934
+ SkillArgsSchema = external_exports.object({
21935
+ name: external_exports.string().min(1).describe("Skill id/name to load (from the available skills list).")
21936
+ });
21937
+ }
21938
+ });
21939
+
21940
+ // src/cli/tools/todoTools.ts
21941
+ function createTodoWriteTool() {
21942
+ return {
21943
+ name: "todo_write",
21944
+ description: "Create or update the session todo list for this multi-step task. Use to track progress (pending \u2192 in_progress \u2192 completed). Prefer small, concrete items. Call todo_read to inspect current list. Not for durable product plans (.zelari/plan.json).",
21945
+ permissions: ["read"],
21946
+ timeoutMs: 5e3,
21947
+ inputSchema: WriteSchema,
21948
+ execute: async (input) => {
21949
+ const list = writeSessionTodos(
21950
+ input.todos.map((t) => ({
21951
+ id: t.id,
21952
+ content: t.content,
21953
+ status: t.status ?? "pending"
21954
+ })),
21955
+ { merge: input.merge === true }
21956
+ );
21957
+ return typedOk({
21958
+ todos: list,
21959
+ formatted: formatTodosForModel(list)
21960
+ });
21961
+ }
21962
+ };
21963
+ }
21964
+ function createTodoReadTool() {
21965
+ return {
21966
+ name: "todo_read",
21967
+ description: "Read the current session todo list. Use after todo_write or to recall open work mid-task.",
21968
+ permissions: ["read"],
21969
+ timeoutMs: 5e3,
21970
+ inputSchema: ReadSchema,
21971
+ execute: async () => {
21972
+ const list = listSessionTodos();
21973
+ return typedOk({
21974
+ todos: list,
21975
+ formatted: formatTodosForModel(list)
21976
+ });
21977
+ }
21978
+ };
21979
+ }
21980
+ var StatusSchema, TodoItemSchema, WriteSchema, ReadSchema;
21981
+ var init_todoTools = __esm({
21982
+ "src/cli/tools/todoTools.ts"() {
21983
+ "use strict";
21984
+ init_zod();
21985
+ init_toolTypes();
21986
+ init_sessionTodos();
21987
+ StatusSchema = external_exports.enum(["pending", "in_progress", "completed", "cancelled"]);
21988
+ TodoItemSchema = external_exports.object({
21989
+ id: external_exports.string().min(1).max(64).optional().describe("Stable id; auto-generated if omitted"),
21990
+ content: external_exports.string().min(1).max(500).describe("Short task description"),
21991
+ status: StatusSchema.optional().describe("Default pending")
21992
+ });
21993
+ WriteSchema = external_exports.object({
21994
+ todos: external_exports.array(TodoItemSchema).min(1).max(40).describe("Todo items to set (replace list unless merge=true)"),
21995
+ merge: external_exports.boolean().optional().describe("If true, upsert by id and keep unlisted items. Default false = replace.")
21996
+ });
21997
+ ReadSchema = external_exports.object({
21998
+ // empty object so models can call with {}
21999
+ _unused: external_exports.string().optional()
22000
+ });
22001
+ }
22002
+ });
22003
+
21557
22004
  // src/cli/lsp/protocol.ts
21558
22005
  function encodeMessage(message) {
21559
22006
  const json2 = JSON.stringify(message);
@@ -21857,7 +22304,7 @@ var init_servers = __esm({
21857
22304
 
21858
22305
  // src/cli/lsp/manager.ts
21859
22306
  import { spawn as spawn3 } from "node:child_process";
21860
- import { readFileSync as readFileSync5 } from "node:fs";
22307
+ import { readFileSync as readFileSync6 } from "node:fs";
21861
22308
  function processTransport(child) {
21862
22309
  return {
21863
22310
  send: (data) => {
@@ -22060,7 +22507,7 @@ var init_manager = __esm({
22060
22507
  const uri = pathToUri(file2);
22061
22508
  let text;
22062
22509
  try {
22063
- text = readFileSync5(file2, "utf8");
22510
+ text = readFileSync6(file2, "utf8");
22064
22511
  } catch {
22065
22512
  text = "";
22066
22513
  }
@@ -22382,13 +22829,13 @@ var init_store = __esm({
22382
22829
  });
22383
22830
 
22384
22831
  // src/cli/semantic/index.ts
22385
- import { promises as fs12, existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
22386
- import { homedir as homedir3 } from "node:os";
22832
+ import { promises as fs12, existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
22833
+ import { homedir as homedir5 } from "node:os";
22387
22834
  import path19 from "node:path";
22388
- import { createHash } from "node:crypto";
22835
+ import { createHash as createHash2 } from "node:crypto";
22389
22836
  function getIndexPath(root) {
22390
- const hash3 = createHash("sha1").update(path19.resolve(root)).digest("hex").slice(0, 16);
22391
- return process.env.ZELARI_SEMANTIC_FILE ?? path19.join(homedir3(), ".tmp", "zelari-code", "semantic", `${hash3}.json`);
22837
+ const hash3 = createHash2("sha1").update(path19.resolve(root)).digest("hex").slice(0, 16);
22838
+ return process.env.ZELARI_SEMANTIC_FILE ?? path19.join(homedir5(), ".tmp", "zelari-code", "semantic", `${hash3}.json`);
22392
22839
  }
22393
22840
  async function collectSourceFiles(root, maxFiles = 1500) {
22394
22841
  const out = [];
@@ -22466,9 +22913,9 @@ async function saveIndex(root, data) {
22466
22913
  }
22467
22914
  function loadIndex(root) {
22468
22915
  const file2 = getIndexPath(root);
22469
- if (!existsSync8(file2)) return null;
22916
+ if (!existsSync10(file2)) return null;
22470
22917
  try {
22471
- const parsed = JSON.parse(readFileSync6(file2, "utf8"));
22918
+ const parsed = JSON.parse(readFileSync7(file2, "utf8"));
22472
22919
  if (parsed && Array.isArray(parsed.chunks)) return parsed;
22473
22920
  } catch {
22474
22921
  }
@@ -22998,19 +23445,19 @@ __export(targets_exports, {
22998
23445
  });
22999
23446
  import {
23000
23447
  chmodSync,
23001
- existsSync as existsSync9,
23002
- mkdirSync as mkdirSync6,
23003
- readFileSync as readFileSync7,
23004
- writeFileSync as writeFileSync5
23448
+ existsSync as existsSync11,
23449
+ mkdirSync as mkdirSync7,
23450
+ readFileSync as readFileSync8,
23451
+ writeFileSync as writeFileSync6
23005
23452
  } from "node:fs";
23006
- import { dirname as dirname2, join } from "node:path";
23007
- import { homedir as homedir4 } from "node:os";
23453
+ import { dirname as dirname2, join as join3 } from "node:path";
23454
+ import { homedir as homedir6 } from "node:os";
23008
23455
  import { spawn as spawn4 } from "node:child_process";
23009
23456
  function getSshTargetsPath() {
23010
- return join(homedir4(), ".zelari-code", "ssh-targets.json");
23457
+ return join3(homedir6(), ".zelari-code", "ssh-targets.json");
23011
23458
  }
23012
23459
  function getSshSecretsPath() {
23013
- return join(homedir4(), ".zelari-code", "ssh-secrets.json");
23460
+ return join3(homedir6(), ".zelari-code", "ssh-secrets.json");
23014
23461
  }
23015
23462
  function normalizeAuth(auth) {
23016
23463
  if (auth === "keyPath") return "keyPath";
@@ -23019,17 +23466,17 @@ function normalizeAuth(auth) {
23019
23466
  }
23020
23467
  function readSecrets() {
23021
23468
  const path38 = getSshSecretsPath();
23022
- if (!existsSync9(path38)) return {};
23469
+ if (!existsSync11(path38)) return {};
23023
23470
  try {
23024
- return JSON.parse(readFileSync7(path38, "utf8"));
23471
+ return JSON.parse(readFileSync8(path38, "utf8"));
23025
23472
  } catch {
23026
23473
  return {};
23027
23474
  }
23028
23475
  }
23029
23476
  function writeSecrets(data) {
23030
23477
  const path38 = getSshSecretsPath();
23031
- mkdirSync6(dirname2(path38), { recursive: true });
23032
- writeFileSync5(path38, `${JSON.stringify(data, null, 2)}
23478
+ mkdirSync7(dirname2(path38), { recursive: true });
23479
+ writeFileSync6(path38, `${JSON.stringify(data, null, 2)}
23033
23480
  `, "utf8");
23034
23481
  try {
23035
23482
  chmodSync(path38, 384);
@@ -23062,9 +23509,9 @@ function deleteSshPassword(id) {
23062
23509
  }
23063
23510
  function readStore2() {
23064
23511
  const path38 = getSshTargetsPath();
23065
- if (!existsSync9(path38)) return [];
23512
+ if (!existsSync11(path38)) return [];
23066
23513
  try {
23067
- const parsed = JSON.parse(readFileSync7(path38, "utf8"));
23514
+ const parsed = JSON.parse(readFileSync8(path38, "utf8"));
23068
23515
  const list = Array.isArray(parsed.targets) ? parsed.targets : [];
23069
23516
  return list.filter(
23070
23517
  (t) => t && typeof t.id === "string" && typeof t.host === "string" && typeof t.user === "string"
@@ -23080,9 +23527,9 @@ function readStore2() {
23080
23527
  }
23081
23528
  function writeStore2(targets) {
23082
23529
  const path38 = getSshTargetsPath();
23083
- mkdirSync6(dirname2(path38), { recursive: true });
23530
+ mkdirSync7(dirname2(path38), { recursive: true });
23084
23531
  const clean = targets.map(({ hasPassword: _hp, ...t }) => t);
23085
- writeFileSync5(
23532
+ writeFileSync6(
23086
23533
  path38,
23087
23534
  `${JSON.stringify({ targets: clean }, null, 2)}
23088
23535
  `,
@@ -23183,17 +23630,17 @@ function buildSshBaseArgs(target) {
23183
23630
  return args;
23184
23631
  }
23185
23632
  function ensureAskpassHelper() {
23186
- const dir = join(homedir4(), ".zelari-code", "ssh-helpers");
23187
- mkdirSync6(dir, { recursive: true });
23188
- const cjs = join(dir, "askpass.cjs");
23189
- writeFileSync5(
23633
+ const dir = join3(homedir6(), ".zelari-code", "ssh-helpers");
23634
+ mkdirSync7(dir, { recursive: true });
23635
+ const cjs = join3(dir, "askpass.cjs");
23636
+ writeFileSync6(
23190
23637
  cjs,
23191
23638
  "process.stdout.write(process.env.ZELARI_SSH_ASKPASS_PASS || '');\n",
23192
23639
  "utf8"
23193
23640
  );
23194
23641
  if (process.platform === "win32") {
23195
- const cmd = join(dir, "askpass.cmd");
23196
- writeFileSync5(
23642
+ const cmd = join3(dir, "askpass.cmd");
23643
+ writeFileSync6(
23197
23644
  cmd,
23198
23645
  `@echo off\r
23199
23646
  node "%~dp0askpass.cjs"\r
@@ -23202,8 +23649,8 @@ node "%~dp0askpass.cjs"\r
23202
23649
  );
23203
23650
  return cmd;
23204
23651
  }
23205
- const sh = join(dir, "askpass.sh");
23206
- writeFileSync5(
23652
+ const sh = join3(dir, "askpass.sh");
23653
+ writeFileSync6(
23207
23654
  sh,
23208
23655
  `#!/bin/sh
23209
23656
  exec node "$(dirname "$0")/askpass.cjs"
@@ -23276,9 +23723,9 @@ function readSshPublicKey(keyOrPubPath) {
23276
23723
  if (!raw) return { ok: false, error: "Empty path" };
23277
23724
  const candidates = raw.endsWith(".pub") ? [raw] : [`${raw}.pub`, raw];
23278
23725
  for (const p3 of candidates) {
23279
- if (!existsSync9(p3)) continue;
23726
+ if (!existsSync11(p3)) continue;
23280
23727
  try {
23281
- const content = readFileSync7(p3, "utf8").trim();
23728
+ const content = readFileSync8(p3, "utf8").trim();
23282
23729
  if (!content) continue;
23283
23730
  if (/BEGIN .*PRIVATE KEY/i.test(content)) {
23284
23731
  return {
@@ -23756,6 +24203,87 @@ ${args.content}
23756
24203
  }
23757
24204
  });
23758
24205
 
24206
+ // src/cli/safety/toolPermissions.ts
24207
+ function grantSessionTool(toolName) {
24208
+ const n = toolName.trim();
24209
+ if (n) sessionToolGrants.add(n);
24210
+ }
24211
+ function grantSessionCategory(cat) {
24212
+ sessionCategoryGrants.add(cat);
24213
+ }
24214
+ function clearSessionPermissionGrants() {
24215
+ sessionToolGrants.clear();
24216
+ sessionCategoryGrants.clear();
24217
+ }
24218
+ function isSessionGranted(toolName, required2) {
24219
+ if (sessionToolGrants.has(toolName)) return true;
24220
+ if (!required2.length) return false;
24221
+ return required2.every((c) => sessionCategoryGrants.has(c));
24222
+ }
24223
+ function parseAction(raw, fallback) {
24224
+ const v = raw?.trim().toLowerCase();
24225
+ if (v === "allow" || v === "ask" || v === "deny") return v;
24226
+ return fallback;
24227
+ }
24228
+ function isAutoPermissions() {
24229
+ const v = process.env.ZELARI_AUTO?.trim().toLowerCase();
24230
+ return v === "1" || v === "true" || v === "yes" || v === "on";
24231
+ }
24232
+ function defaultPermissionPolicy(overrides) {
24233
+ return {
24234
+ read: parseAction(process.env.ZELARI_PERMISSION_READ, "allow"),
24235
+ write: parseAction(process.env.ZELARI_PERMISSION_WRITE, "allow"),
24236
+ execute: parseAction(process.env.ZELARI_PERMISSION_EXECUTE, "allow"),
24237
+ network: parseAction(process.env.ZELARI_PERMISSION_NETWORK, "allow"),
24238
+ ui: "allow",
24239
+ auto: isAutoPermissions(),
24240
+ ...overrides
24241
+ };
24242
+ }
24243
+ function resolveToolPermission(toolName, required2, policy) {
24244
+ if (!required2.length) {
24245
+ return { action: "allow", reason: "", categories: [] };
24246
+ }
24247
+ let worst = "allow";
24248
+ const hit = [];
24249
+ for (const cat of required2) {
24250
+ const a = cat === "read" ? policy.read : cat === "write" ? policy.write : cat === "execute" ? policy.execute : cat === "network" ? policy.network : cat === "ui" ? policy.ui : "allow";
24251
+ if (a === "deny") {
24252
+ worst = "deny";
24253
+ hit.push(cat);
24254
+ } else if (a === "ask" && worst !== "deny") {
24255
+ worst = "ask";
24256
+ hit.push(cat);
24257
+ }
24258
+ }
24259
+ if (worst === "allow") {
24260
+ return { action: "allow", reason: "", categories: [] };
24261
+ }
24262
+ if (worst === "deny") {
24263
+ return {
24264
+ action: "deny",
24265
+ reason: `Permission denied for tool "${toolName}" (${hit.join(", ")}). Policy forbids this action.`,
24266
+ categories: hit
24267
+ };
24268
+ }
24269
+ if (policy.auto || isSessionGranted(toolName, hit.length ? hit : required2)) {
24270
+ return { action: "allow", reason: "", categories: [] };
24271
+ }
24272
+ return {
24273
+ action: "ask",
24274
+ reason: `Tool "${toolName}" requires approval (${hit.join(", ")}).`,
24275
+ categories: hit
24276
+ };
24277
+ }
24278
+ var sessionToolGrants, sessionCategoryGrants;
24279
+ var init_toolPermissions = __esm({
24280
+ "src/cli/safety/toolPermissions.ts"() {
24281
+ "use strict";
24282
+ sessionToolGrants = /* @__PURE__ */ new Set();
24283
+ sessionCategoryGrants = /* @__PURE__ */ new Set();
24284
+ }
24285
+ });
24286
+
23759
24287
  // src/cli/toolRegistry.ts
23760
24288
  var toolRegistry_exports = {};
23761
24289
  __export(toolRegistry_exports, {
@@ -23780,35 +24308,54 @@ function createBuiltinToolRegistry(options = {}) {
23780
24308
  const safeFetchUrl = wrapWithAudit(fetchUrlTool, audit, sessionId);
23781
24309
  const safeWebSearch = wrapWithAudit(webSearchTool, audit, sessionId);
23782
24310
  const registry3 = new ToolRegistry();
23783
- const readOnly = options.readOnly === true || options.planMode === true;
23784
- registry3.register(safeReadFile);
23785
- registry3.register(safeGrepContent);
23786
- registry3.register(safeListFiles);
23787
- registry3.register(safeShowDiff);
23788
- registry3.register(safeFetchUrl);
23789
- registry3.register(safeWebSearch);
23790
- if (!readOnly) {
23791
- registry3.register(safeWriteFile);
23792
- registry3.register(safeEditFile);
23793
- registry3.register(safeBash);
23794
- registry3.register(safeApplyDiff);
23795
- }
23796
- const askUserTool = options.readOnly === true ? null : createAskUserTool(options.onAskUser);
24311
+ const profile = options.profile ?? "full";
24312
+ const readOnly = options.readOnly === true || options.planMode === true || profile === "explore";
24313
+ const verifyMode = profile === "verify";
24314
+ const allowMutators = !readOnly && !verifyMode;
24315
+ const allowBash = allowMutators || verifyMode;
24316
+ const permPolicy = options.permissionPolicy ?? defaultPermissionPolicy();
24317
+ const withPerm = (t) => wrapWithPermissions(t, permPolicy, options.onPermissionAsk);
24318
+ registry3.register(withPerm(safeReadFile));
24319
+ registry3.register(withPerm(safeGrepContent));
24320
+ registry3.register(withPerm(safeListFiles));
24321
+ registry3.register(withPerm(safeShowDiff));
24322
+ registry3.register(withPerm(safeFetchUrl));
24323
+ registry3.register(withPerm(safeWebSearch));
24324
+ if (allowMutators) {
24325
+ registry3.register(withPerm(safeWriteFile));
24326
+ registry3.register(withPerm(safeEditFile));
24327
+ registry3.register(withPerm(safeApplyDiff));
24328
+ }
24329
+ if (allowBash) {
24330
+ registry3.register(withPerm(safeBash));
24331
+ }
24332
+ const askUserTool = options.readOnly === true || profile === "explore" || profile === "verify" ? null : createAskUserTool(options.onAskUser);
23797
24333
  if (askUserTool) {
23798
- registry3.register(askUserTool);
23799
- }
23800
- const summary = readOnly ? [safeReadFile, safeGrepContent, safeListFiles, safeShowDiff, safeFetchUrl, safeWebSearch] : [
24334
+ registry3.register(withPerm(askUserTool));
24335
+ }
24336
+ const enableSkill = options.enableSkill !== false && options.readOnly !== true && profile !== "explore" && profile !== "verify";
24337
+ const skillTool = enableSkill ? withPerm(createSkillTool({ cwd: root })) : null;
24338
+ if (skillTool) {
24339
+ registry3.register(skillTool);
24340
+ }
24341
+ const enableTodos = options.enableTodos !== false && options.readOnly !== true && profile === "full";
24342
+ const todoWrite = enableTodos ? withPerm(createTodoWriteTool()) : null;
24343
+ const todoRead = enableTodos ? withPerm(createTodoReadTool()) : null;
24344
+ if (todoWrite) registry3.register(todoWrite);
24345
+ if (todoRead) registry3.register(todoRead);
24346
+ const summary = [
23801
24347
  safeReadFile,
23802
- safeWriteFile,
23803
- safeEditFile,
23804
- safeBash,
23805
24348
  safeGrepContent,
23806
24349
  safeListFiles,
23807
24350
  safeShowDiff,
23808
- safeApplyDiff,
23809
24351
  safeFetchUrl,
23810
24352
  safeWebSearch,
23811
- ...askUserTool ? [askUserTool] : []
24353
+ ...allowMutators ? [safeWriteFile, safeEditFile, safeApplyDiff] : [],
24354
+ ...allowBash ? [safeBash] : [],
24355
+ ...askUserTool ? [askUserTool] : [],
24356
+ ...skillTool ? [skillTool] : [],
24357
+ ...todoWrite ? [todoWrite] : [],
24358
+ ...todoRead ? [todoRead] : []
23812
24359
  ];
23813
24360
  const tools = summary.map((t) => ({
23814
24361
  name: t.name,
@@ -23860,17 +24407,23 @@ function createBuiltinToolRegistry(options = {}) {
23860
24407
  });
23861
24408
  }
23862
24409
  }
23863
- if (!readOnly && options.enableTask !== false) {
24410
+ const enableTask = options.enableTask !== false && !readOnly && !verifyMode && profile === "full" && !options.planMode;
24411
+ if (enableTask) {
23864
24412
  const taskTool = createTaskTool({
23865
- createSubAgentContext: async () => {
24413
+ createSubAgentContext: async ({ agent }) => {
23866
24414
  const cfg = await providerFromEnv();
23867
24415
  if (!cfg) return null;
24416
+ const subProfile = taskAgentToProfile(agent);
23868
24417
  const { registry: subRegistry } = createBuiltinToolRegistry({
23869
24418
  root,
23870
24419
  audit,
23871
24420
  sessionId,
23872
- readOnly: true,
23873
- diagnostics: false
24421
+ profile: subProfile,
24422
+ enableTask: false,
24423
+ enableSkill: agent === "general",
24424
+ diagnostics: false,
24425
+ lspProvider: null,
24426
+ permissionPolicy: defaultPermissionPolicy({ auto: true })
23874
24427
  });
23875
24428
  return {
23876
24429
  providerStream: openaiCompatibleProvider(cfg),
@@ -23881,11 +24434,12 @@ function createBuiltinToolRegistry(options = {}) {
23881
24434
  name: t.function.name,
23882
24435
  description: t.function.description,
23883
24436
  parameters: t.function.parameters
23884
- }))
24437
+ })),
24438
+ agent
23885
24439
  };
23886
24440
  }
23887
24441
  });
23888
- registry3.register(taskTool);
24442
+ registry3.register(withPerm(taskTool));
23889
24443
  tools.push({
23890
24444
  name: taskTool.name,
23891
24445
  description: taskTool.description,
@@ -23922,6 +24476,51 @@ function registerCliToolsIntoCouncilCatalog(registry3) {
23922
24476
  }
23923
24477
  }
23924
24478
  }
24479
+ function taskAgentToProfile(agent) {
24480
+ if (agent === "general") return "general";
24481
+ if (agent === "verify") return "verify";
24482
+ return "explore";
24483
+ }
24484
+ function wrapWithPermissions(original, policy, onAsk) {
24485
+ const required2 = original.permissions ?? [];
24486
+ const decisionProbe = resolveToolPermission(original.name, required2, policy);
24487
+ if (decisionProbe.action === "allow" && !required2.includes("write") && !required2.includes("execute")) {
24488
+ }
24489
+ return {
24490
+ ...original,
24491
+ execute: async (input, ctx) => {
24492
+ const decision = resolveToolPermission(original.name, required2, policy);
24493
+ if (decision.action === "deny") {
24494
+ return typedErr(`[permission] ${decision.reason}`);
24495
+ }
24496
+ if (decision.action === "ask") {
24497
+ if (!onAsk) {
24498
+ return typedErr(
24499
+ `[permission] ${decision.reason} No interactive approval available (set ZELARI_AUTO=1 to auto-allow, or configure onPermissionAsk).`
24500
+ );
24501
+ }
24502
+ try {
24503
+ const ok = await onAsk({
24504
+ toolName: original.name,
24505
+ reason: decision.reason,
24506
+ categories: decision.categories,
24507
+ args: input
24508
+ });
24509
+ if (!ok) {
24510
+ return typedErr(
24511
+ `[permission] User denied "${original.name}" (${decision.categories.join(", ")}).`
24512
+ );
24513
+ }
24514
+ } catch (err) {
24515
+ return typedErr(
24516
+ `[permission] Approval UI failed: ${err instanceof Error ? err.message : String(err)}`
24517
+ );
24518
+ }
24519
+ }
24520
+ return original.execute(input, ctx);
24521
+ }
24522
+ };
24523
+ }
23925
24524
  function wrapWithSandbox(original, pathArgs, root, audit, sessionId) {
23926
24525
  return {
23927
24526
  ...original,
@@ -24094,6 +24693,8 @@ var init_toolRegistry = __esm({
24094
24693
  init_engine();
24095
24694
  init_taskTool();
24096
24695
  init_askUser();
24696
+ init_skillTool();
24697
+ init_todoTools();
24097
24698
  init_tools2();
24098
24699
  init_manager();
24099
24700
  init_tools3();
@@ -24102,6 +24703,8 @@ var init_toolRegistry = __esm({
24102
24703
  init_tools6();
24103
24704
  init_worldModel();
24104
24705
  init_openai_compatible();
24706
+ init_toolPermissions();
24707
+ init_toolTypes();
24105
24708
  init_skills2();
24106
24709
  HARNESS_BUILTIN_NAMES = /* @__PURE__ */ new Set([
24107
24710
  "read_file",
@@ -24119,7 +24722,7 @@ var init_toolRegistry = __esm({
24119
24722
  });
24120
24723
 
24121
24724
  // src/cli/state/fileStateStore.ts
24122
- import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
24725
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
24123
24726
  import { promises as fs14 } from "node:fs";
24124
24727
  import * as path24 from "node:path";
24125
24728
  function shortId() {
@@ -24171,7 +24774,7 @@ async function getStateStore(projectRoot, env = process.env) {
24171
24774
  }
24172
24775
  }
24173
24776
  function hashStablePrompt(stable) {
24174
- return createHash2("sha256").update(stable, "utf8").digest("hex").slice(0, 16);
24777
+ return createHash3("sha256").update(stable, "utf8").digest("hex").slice(0, 16);
24175
24778
  }
24176
24779
  var DEFAULT_MATERIALIZE_CHARS, FileDurableStateStore, NoopDurableStateStore;
24177
24780
  var init_fileStateStore = __esm({
@@ -24874,8 +25477,8 @@ var init_parseCssMotion = __esm({
24874
25477
  });
24875
25478
 
24876
25479
  // packages/core/dist/council/verification/citeVerify.js
24877
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
24878
- import { join as join3 } from "node:path";
25480
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
25481
+ import { join as join5 } from "node:path";
24879
25482
  function extractCitations(text) {
24880
25483
  const out = [];
24881
25484
  const seen = /* @__PURE__ */ new Set();
@@ -24897,8 +25500,8 @@ function verifyCitations(projectRoot, synthesisText) {
24897
25500
  return [];
24898
25501
  const results = [];
24899
25502
  for (const cite of extractCitations(synthesisText)) {
24900
- const abs = join3(projectRoot, cite.file);
24901
- if (!existsSync10(abs)) {
25503
+ const abs = join5(projectRoot, cite.file);
25504
+ if (!existsSync12(abs)) {
24902
25505
  results.push({
24903
25506
  id: "synthesis.cite-invalid",
24904
25507
  severity: "error",
@@ -24911,7 +25514,7 @@ function verifyCitations(projectRoot, synthesisText) {
24911
25514
  });
24912
25515
  continue;
24913
25516
  }
24914
- const lines = readFileSync8(abs, "utf8").split(/\r?\n/);
25517
+ const lines = readFileSync9(abs, "utf8").split(/\r?\n/);
24915
25518
  if (cite.line > lines.length) {
24916
25519
  results.push({
24917
25520
  id: "synthesis.cite-invalid",
@@ -25174,14 +25777,14 @@ var init_synthesisAudit = __esm({
25174
25777
  });
25175
25778
 
25176
25779
  // packages/core/dist/council/verification/runChecks.js
25177
- import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
25178
- import { join as join4 } from "node:path";
25780
+ import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
25781
+ import { join as join6 } from "node:path";
25179
25782
  function loadNfrSpec(zelariRoot) {
25180
- const path38 = join4(zelariRoot, "nfr-spec.json");
25181
- if (!existsSync11(path38))
25783
+ const path38 = join6(zelariRoot, "nfr-spec.json");
25784
+ if (!existsSync13(path38))
25182
25785
  return null;
25183
25786
  try {
25184
- const raw = JSON.parse(readFileSync9(path38, "utf8"));
25787
+ const raw = JSON.parse(readFileSync10(path38, "utf8"));
25185
25788
  if (raw.version !== 1 || !Array.isArray(raw.targets))
25186
25789
  return null;
25187
25790
  return raw;
@@ -25192,11 +25795,11 @@ function loadNfrSpec(zelariRoot) {
25192
25795
  function resolveTargets(projectRoot, spec) {
25193
25796
  const found = [];
25194
25797
  for (const rel2 of spec.targets) {
25195
- if (existsSync11(join4(projectRoot, rel2))) {
25798
+ if (existsSync13(join6(projectRoot, rel2))) {
25196
25799
  found.push(rel2);
25197
25800
  }
25198
25801
  }
25199
- if (found.length === 0 && existsSync11(join4(projectRoot, "index.html"))) {
25802
+ if (found.length === 0 && existsSync13(join6(projectRoot, "index.html"))) {
25200
25803
  return ["index.html"];
25201
25804
  }
25202
25805
  return found;
@@ -25251,18 +25854,18 @@ function checkDeadCssHooks(html, relFile) {
25251
25854
  return results;
25252
25855
  }
25253
25856
  function checkPlanReality(projectRoot, zelariRoot, targets, keywords) {
25254
- const planPath = join4(zelariRoot, "plan.json");
25255
- if (!existsSync11(planPath) || keywords.length === 0)
25857
+ const planPath = join6(zelariRoot, "plan.json");
25858
+ if (!existsSync13(planPath) || keywords.length === 0)
25256
25859
  return [];
25257
25860
  let plan;
25258
25861
  try {
25259
- plan = JSON.parse(readFileSync9(planPath, "utf8"));
25862
+ plan = JSON.parse(readFileSync10(planPath, "utf8"));
25260
25863
  } catch {
25261
25864
  return [];
25262
25865
  }
25263
25866
  const milestoneText = (plan.milestones ?? []).map((m) => `${m.name ?? ""} ${m.description ?? ""}`).join(" ").toLowerCase();
25264
25867
  const results = [];
25265
- const targetContent = targets.map((t) => readFileSync9(join4(projectRoot, t), "utf8").toLowerCase()).join("\n");
25868
+ const targetContent = targets.map((t) => readFileSync10(join6(projectRoot, t), "utf8").toLowerCase()).join("\n");
25266
25869
  for (const kw of keywords) {
25267
25870
  const low = kw.toLowerCase();
25268
25871
  if (!milestoneText.includes(low))
@@ -25291,14 +25894,14 @@ function checkPlanReality(projectRoot, zelariRoot, targets, keywords) {
25291
25894
  return results;
25292
25895
  }
25293
25896
  function checkReadmeStale(projectRoot, targets) {
25294
- const readmePath = join4(projectRoot, "README.md");
25295
- if (!existsSync11(readmePath) || targets.length === 0)
25897
+ const readmePath = join6(projectRoot, "README.md");
25898
+ if (!existsSync13(readmePath) || targets.length === 0)
25296
25899
  return [];
25297
- const readme = readFileSync9(readmePath, "utf8");
25298
- const htmlPath = join4(projectRoot, targets[0]);
25299
- if (!existsSync11(htmlPath))
25900
+ const readme = readFileSync10(readmePath, "utf8");
25901
+ const htmlPath = join6(projectRoot, targets[0]);
25902
+ if (!existsSync13(htmlPath))
25300
25903
  return [];
25301
- const html = readFileSync9(htmlPath, "utf8");
25904
+ const html = readFileSync10(htmlPath, "utf8");
25302
25905
  const sectionCount = (html.match(/<section\s+id=/gi) ?? []).length;
25303
25906
  const readmeSections = readme.match(/(\d+)\s+sezioni/i);
25304
25907
  if (readmeSections) {
@@ -25336,7 +25939,7 @@ function runImplementationVerification(input) {
25336
25939
  forbidLayoutProps: anim.forbidLayoutProps ?? true
25337
25940
  };
25338
25941
  for (const rel2 of targets) {
25339
- const html = readFileSync9(join4(input.projectRoot, rel2), "utf8");
25942
+ const html = readFileSync10(join6(input.projectRoot, rel2), "utf8");
25340
25943
  for (const v of scanKeyframesViolations(html, scanOpts)) {
25341
25944
  results.push({
25342
25945
  id: "motion.keyframes",
@@ -25392,8 +25995,8 @@ function runImplementationVerification(input) {
25392
25995
  };
25393
25996
  }
25394
25997
  function writeVerificationReport(zelariRoot, report) {
25395
- const outPath = join4(zelariRoot, "verification-report.json");
25396
- writeFileSync6(outPath, JSON.stringify(report, null, 2), "utf8");
25998
+ const outPath = join6(zelariRoot, "verification-report.json");
25999
+ writeFileSync7(outPath, JSON.stringify(report, null, 2), "utf8");
25397
26000
  return outPath;
25398
26001
  }
25399
26002
  var DEFAULT_NFR_SPEC;
@@ -25422,8 +26025,8 @@ var init_runChecks = __esm({
25422
26025
  });
25423
26026
 
25424
26027
  // packages/core/dist/council/verification/microGate.js
25425
- import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
25426
- import { join as join5 } from "node:path";
26028
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
26029
+ import { join as join7 } from "node:path";
25427
26030
  function checkDeadHooksInHtml(html) {
25428
26031
  const warnings = [];
25429
26032
  const scripts = [];
@@ -25451,8 +26054,8 @@ function checkDeadHooksInHtml(html) {
25451
26054
  return warnings;
25452
26055
  }
25453
26056
  function runMicroVerificationOnFile(projectRoot, relPath, zelariRoot) {
25454
- const abs = join5(projectRoot, relPath);
25455
- if (!existsSync12(abs) || !/\.html?$/i.test(relPath))
26057
+ const abs = join7(projectRoot, relPath);
26058
+ if (!existsSync14(abs) || !/\.html?$/i.test(relPath))
25456
26059
  return [];
25457
26060
  const spec = (zelariRoot ? loadNfrSpec(zelariRoot) : null) ?? DEFAULT_NFR_SPEC;
25458
26061
  const anim = spec.animation ?? { compositorOnly: true, forbidLayoutProps: true };
@@ -25460,7 +26063,7 @@ function runMicroVerificationOnFile(projectRoot, relPath, zelariRoot) {
25460
26063
  compositorOnly: anim.compositorOnly ?? true,
25461
26064
  forbidLayoutProps: anim.forbidLayoutProps ?? true
25462
26065
  };
25463
- const html = readFileSync10(abs, "utf8");
26066
+ const html = readFileSync11(abs, "utf8");
25464
26067
  const warnings = [];
25465
26068
  for (const v of scanKeyframesViolations(html, scanOpts)) {
25466
26069
  warnings.push({
@@ -25816,8 +26419,8 @@ var init_implementationDelivery = __esm({
25816
26419
  });
25817
26420
 
25818
26421
  // packages/core/dist/council/verification/inlineJsAutofix.js
25819
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
25820
- import { join as join6 } from "node:path";
26422
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
26423
+ import { join as join8 } from "node:path";
25821
26424
  function minifyInlineJs(js) {
25822
26425
  let out = js.replace(/\/\*[\s\S]*?\*\//g, "");
25823
26426
  out = out.split("\n").map((line) => line.replace(/\/\/.*$/, "").trimEnd()).filter((line) => line.trim().length > 0).join("\n");
@@ -25860,10 +26463,10 @@ function applyInlineJsAutofix(projectRoot, report) {
25860
26463
  const fixes = [];
25861
26464
  for (const r of fails) {
25862
26465
  const rel2 = r.file ?? "index.html";
25863
- const abs = join6(projectRoot, rel2);
26466
+ const abs = join8(projectRoot, rel2);
25864
26467
  let html;
25865
26468
  try {
25866
- html = readFileSync11(abs, "utf8");
26469
+ html = readFileSync12(abs, "utf8");
25867
26470
  } catch {
25868
26471
  continue;
25869
26472
  }
@@ -25876,7 +26479,7 @@ function applyInlineJsAutofix(projectRoot, report) {
25876
26479
  if (!changed || afterBytes > maxBytes)
25877
26480
  continue;
25878
26481
  const nextHtml = html.replace(SCRIPT_RE, `<script>${after}</script>`);
25879
- writeFileSync7(abs, nextHtml, "utf8");
26482
+ writeFileSync8(abs, nextHtml, "utf8");
25880
26483
  filesChanged.push(rel2);
25881
26484
  fixes.push(`${rel2}: trimmed inline script ${Buffer.byteLength(before, "utf8")}\u2192${afterBytes} bytes`);
25882
26485
  }
@@ -25896,8 +26499,8 @@ var init_inlineJsAutofix = __esm({
25896
26499
  });
25897
26500
 
25898
26501
  // packages/core/dist/agents/councilApi.js
25899
- import { existsSync as existsSync13 } from "node:fs";
25900
- import { join as join7 } from "node:path";
26502
+ import { existsSync as existsSync15 } from "node:fs";
26503
+ import { join as join9 } from "node:path";
25901
26504
  function extractBalancedJsonObject(s) {
25902
26505
  const start = s.indexOf("{");
25903
26506
  if (start < 0)
@@ -26524,7 +27127,7 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
26524
27127
  const zelariRoot = `${chairmanProjectRoot}/.zelari`;
26525
27128
  const spec = loadNfrSpec(zelariRoot) ?? DEFAULT_NFR_SPEC;
26526
27129
  for (const rel2 of spec.targets) {
26527
- if (!existsSync13(join7(chairmanProjectRoot, rel2)))
27130
+ if (!existsSync15(join9(chairmanProjectRoot, rel2)))
26528
27131
  continue;
26529
27132
  changedTargetFiles.add(rel2);
26530
27133
  for (const w of runChairmanMicroGate({ projectRoot: chairmanProjectRoot, relPath: rel2, zelariRoot })) {
@@ -26544,7 +27147,7 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
26544
27147
  const zelariRootReplay = `${chairmanProjectRoot}/.zelari`;
26545
27148
  const specReplay = loadNfrSpec(zelariRootReplay) ?? DEFAULT_NFR_SPEC;
26546
27149
  for (const rel2 of specReplay.targets) {
26547
- if (!existsSync13(join7(chairmanProjectRoot, rel2)))
27150
+ if (!existsSync15(join9(chairmanProjectRoot, rel2)))
26548
27151
  continue;
26549
27152
  changedTargetFiles.add(rel2);
26550
27153
  for (const w of runChairmanMicroGate({
@@ -27156,8 +27759,8 @@ function inferStack(msg) {
27156
27759
  return [...found];
27157
27760
  }
27158
27761
  function firstSentence(msg, max = 160) {
27159
- const oneLine = msg.replace(/\s+/g, " ").trim();
27160
- return oneLine.length <= max ? oneLine : oneLine.slice(0, max - 1).trimEnd() + "\u2026";
27762
+ const oneLine2 = msg.replace(/\s+/g, " ").trim();
27763
+ return oneLine2.length <= max ? oneLine2 : oneLine2.slice(0, max - 1).trimEnd() + "\u2026";
27161
27764
  }
27162
27765
  function deliverableFor(intent, msg) {
27163
27766
  const s = firstSentence(msg);
@@ -27262,8 +27865,8 @@ var init_types2 = __esm({
27262
27865
  });
27263
27866
 
27264
27867
  // packages/core/dist/council/verification/motionAutofix.js
27265
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "node:fs";
27266
- import { join as join8 } from "node:path";
27868
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
27869
+ import { join as join10 } from "node:path";
27267
27870
  function sanitizeTransitionPart(part) {
27268
27871
  const tokens = part.trim().split(/\s+/);
27269
27872
  if (tokens.length === 0)
@@ -27338,10 +27941,10 @@ function applyMotionAutofix(projectRoot, report) {
27338
27941
  forbidLayoutProps: DEFAULT_NFR_SPEC.animation?.forbidLayoutProps ?? true
27339
27942
  };
27340
27943
  for (const rel2 of targets) {
27341
- const abs = join8(projectRoot, rel2);
27944
+ const abs = join10(projectRoot, rel2);
27342
27945
  let html;
27343
27946
  try {
27344
- html = readFileSync12(abs, "utf8");
27947
+ html = readFileSync13(abs, "utf8");
27345
27948
  } catch {
27346
27949
  continue;
27347
27950
  }
@@ -27356,7 +27959,7 @@ function applyMotionAutofix(projectRoot, report) {
27356
27959
  const afterK = scanKeyframesViolations(final, scanOpts).length;
27357
27960
  const afterT = scanTransitionViolations(final, scanOpts).length;
27358
27961
  if (final !== html || beforeK > afterK || beforeT > afterT) {
27359
- writeFileSync8(abs, final, "utf8");
27962
+ writeFileSync9(abs, final, "utf8");
27360
27963
  filesChanged.push(rel2);
27361
27964
  if (beforeK > afterK)
27362
27965
  fixes.push(`${rel2}: sanitized ${beforeK - afterK} keyframe violation(s)`);
@@ -27367,7 +27970,7 @@ function applyMotionAutofix(projectRoot, report) {
27367
27970
  if (!fixes.some((f) => f.startsWith(rel2)))
27368
27971
  fixes.push(`${rel2}: motion CSS sanitized`);
27369
27972
  } else if (motionChanged) {
27370
- writeFileSync8(abs, final, "utf8");
27973
+ writeFileSync9(abs, final, "utf8");
27371
27974
  filesChanged.push(rel2);
27372
27975
  fixes.push(`${rel2}: motion CSS sanitized`);
27373
27976
  }
@@ -27413,8 +28016,8 @@ var init_motionAutofix = __esm({
27413
28016
  });
27414
28017
 
27415
28018
  // packages/core/dist/council/verification/autofix.js
27416
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
27417
- import { join as join9 } from "node:path";
28019
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "node:fs";
28020
+ import { join as join11 } from "node:path";
27418
28021
  function applyDeterministicAutofix(projectRoot, report) {
27419
28022
  const motion = applyMotionAutofix(projectRoot, report);
27420
28023
  const inlineJs = applyInlineJsAutofix(projectRoot, report);
@@ -27428,8 +28031,8 @@ function applyDeterministicAutofix(projectRoot, report) {
27428
28031
  const m = r.evidence?.match(/classList\.add\(\s*['"]([\w-]+)['"]\s*\)/);
27429
28032
  if (!m || m[1] === "rm")
27430
28033
  continue;
27431
- const abs = join9(projectRoot, rel2);
27432
- let html = readFileSync13(abs, "utf8");
28034
+ const abs = join11(projectRoot, rel2);
28035
+ let html = readFileSync14(abs, "utf8");
27433
28036
  const snippet = m[0];
27434
28037
  if (!html.includes(snippet))
27435
28038
  continue;
@@ -27437,7 +28040,7 @@ function applyDeterministicAutofix(projectRoot, report) {
27437
28040
  const filtered = lines.filter((line) => !line.includes(snippet));
27438
28041
  if (filtered.length === lines.length)
27439
28042
  continue;
27440
- writeFileSync9(abs, filtered.join("\n"), "utf8");
28043
+ writeFileSync10(abs, filtered.join("\n"), "utf8");
27441
28044
  filesChanged.push(rel2);
27442
28045
  fixes.push(`removed dead hook line in ${rel2}: ${snippet}`);
27443
28046
  }
@@ -27484,12 +28087,12 @@ var init_types3 = __esm({
27484
28087
  });
27485
28088
 
27486
28089
  // packages/core/dist/council/lessons/io.js
27487
- import { readFileSync as readFileSync14 } from "node:fs";
27488
- import { join as join10 } from "node:path";
28090
+ import { readFileSync as readFileSync15 } from "node:fs";
28091
+ import { join as join12 } from "node:path";
27489
28092
  function readLessonsDeduped(zelariRoot) {
27490
- const path38 = join10(zelariRoot, LESSONS_FILE);
28093
+ const path38 = join12(zelariRoot, LESSONS_FILE);
27491
28094
  try {
27492
- const raw = readFileSync14(path38, "utf8");
28095
+ const raw = readFileSync15(path38, "utf8");
27493
28096
  const byId = /* @__PURE__ */ new Map();
27494
28097
  for (const line of raw.split(/\r?\n/)) {
27495
28098
  if (!line.trim())
@@ -27580,7 +28183,7 @@ var init_signatures = __esm({
27580
28183
 
27581
28184
  // packages/core/dist/council/lessons/recordFailure.js
27582
28185
  import { appendFileSync as appendFileSync2 } from "node:fs";
27583
- import { join as join11 } from "node:path";
28186
+ import { join as join13 } from "node:path";
27584
28187
  function methodologyFor(check2) {
27585
28188
  return METHODOLOGY[check2.id] ?? `When ${check2.id} fails, fix the underlying issue and cite grep/tool evidence before claiming PASS.`;
27586
28189
  }
@@ -27590,7 +28193,7 @@ function keywordsFrom(check2, signature) {
27590
28193
  return [.../* @__PURE__ */ new Set([...fromId, ...words])].slice(0, 12);
27591
28194
  }
27592
28195
  function writeLesson(zelariRoot, lesson) {
27593
- const path38 = join11(zelariRoot, LESSONS_FILE);
28196
+ const path38 = join13(zelariRoot, LESSONS_FILE);
27594
28197
  appendFileSync2(path38, `${JSON.stringify(lesson)}
27595
28198
  `, "utf8");
27596
28199
  }
@@ -27747,8 +28350,8 @@ var init_types4 = __esm({
27747
28350
  });
27748
28351
 
27749
28352
  // packages/core/dist/council/completion/buildCompletion.js
27750
- import { writeFileSync as writeFileSync10 } from "node:fs";
27751
- import { join as join12 } from "node:path";
28353
+ import { writeFileSync as writeFileSync11 } from "node:fs";
28354
+ import { join as join14 } from "node:path";
27752
28355
  function openFailsFromReport(report) {
27753
28356
  if (!report)
27754
28357
  return [];
@@ -27776,8 +28379,8 @@ function tierSummary(report) {
27776
28379
  function promptSummaryFrom(synthesisText) {
27777
28380
  if (!synthesisText?.trim())
27778
28381
  return void 0;
27779
- const oneLine = synthesisText.replace(/\s+/g, " ").trim();
27780
- return oneLine.length > 240 ? `${oneLine.slice(0, 237)}...` : oneLine;
28382
+ const oneLine2 = synthesisText.replace(/\s+/g, " ").trim();
28383
+ return oneLine2.length > 240 ? `${oneLine2.slice(0, 237)}...` : oneLine2;
27781
28384
  }
27782
28385
  function buildCouncilCompletion(input) {
27783
28386
  const openFails = openFailsFromReport(input.verification?.report);
@@ -27807,8 +28410,8 @@ function buildCouncilCompletion(input) {
27807
28410
  };
27808
28411
  }
27809
28412
  function writeCouncilCompletion(zelariRoot, completion) {
27810
- const outPath = join12(zelariRoot, "completion.json");
27811
- writeFileSync10(outPath, JSON.stringify(completion, null, 2), "utf8");
28413
+ const outPath = join14(zelariRoot, "completion.json");
28414
+ writeFileSync11(outPath, JSON.stringify(completion, null, 2), "utf8");
27812
28415
  return outPath;
27813
28416
  }
27814
28417
  var init_buildCompletion = __esm({
@@ -28134,33 +28737,241 @@ var init_council = __esm({
28134
28737
  init_systemPromptBuilder();
28135
28738
  init_promptModules();
28136
28739
  }
28137
- });
28138
-
28139
- // packages/core/dist/memory/types.js
28140
- var init_types5 = __esm({
28141
- "packages/core/dist/memory/types.js"() {
28740
+ });
28741
+
28742
+ // packages/core/dist/memory/types.js
28743
+ var init_types5 = __esm({
28744
+ "packages/core/dist/memory/types.js"() {
28745
+ "use strict";
28746
+ }
28747
+ });
28748
+
28749
+ // packages/core/dist/state/index.js
28750
+ var init_state = __esm({
28751
+ "packages/core/dist/state/index.js"() {
28752
+ "use strict";
28753
+ }
28754
+ });
28755
+
28756
+ // packages/core/dist/index.js
28757
+ var init_dist = __esm({
28758
+ "packages/core/dist/index.js"() {
28759
+ "use strict";
28760
+ init_events2();
28761
+ init_types();
28762
+ init_harness();
28763
+ init_council();
28764
+ init_skills2();
28765
+ init_types5();
28766
+ init_state();
28767
+ }
28768
+ });
28769
+
28770
+ // src/cli/budget/historySummary.ts
28771
+ function extractiveHistorySummary(dropped, opts) {
28772
+ const maxChars = opts?.maxChars ?? MAX_SUMMARY_CHARS;
28773
+ if (dropped.length === 0) return "No prior turns.";
28774
+ const userGoals = [];
28775
+ const assistantNotes = [];
28776
+ const tools = /* @__PURE__ */ new Map();
28777
+ const files = /* @__PURE__ */ new Set();
28778
+ let toolResults = 0;
28779
+ for (const m of dropped) {
28780
+ if (m.role === "user" && m.content.trim()) {
28781
+ userGoals.push(oneLine(m.content, 220));
28782
+ } else if (m.role === "assistant") {
28783
+ if (m.content.trim()) {
28784
+ assistantNotes.push(oneLine(m.content, 180));
28785
+ }
28786
+ if (m.toolCalls) {
28787
+ for (const tc of m.toolCalls) {
28788
+ tools.set(tc.name, (tools.get(tc.name) ?? 0) + 1);
28789
+ collectPaths(tc.args, files);
28790
+ }
28791
+ }
28792
+ } else if (m.role === "tool") {
28793
+ toolResults += 1;
28794
+ collectPathsFromText(m.content, files);
28795
+ }
28796
+ }
28797
+ const parts = [
28798
+ "[history-summary] Earlier turns were compacted to stay within the context budget.",
28799
+ `Dropped ${dropped.length} message(s) (${userGoals.length} user, ${assistantNotes.length} assistant notes, ${toolResults} tool results).`
28800
+ ];
28801
+ if (userGoals.length) {
28802
+ parts.push("## User goals / requests");
28803
+ for (const g of userGoals.slice(-6)) parts.push(`- ${g}`);
28804
+ }
28805
+ if (assistantNotes.length) {
28806
+ parts.push("## Assistant conclusions (truncated)");
28807
+ for (const a of assistantNotes.slice(-5)) parts.push(`- ${a}`);
28808
+ }
28809
+ if (tools.size) {
28810
+ const ranked = [...tools.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12).map(([n, c]) => `${n}\xD7${c}`);
28811
+ parts.push(`## Tools used: ${ranked.join(", ")}`);
28812
+ }
28813
+ if (files.size) {
28814
+ const list = [...files].slice(0, 24);
28815
+ parts.push(`## Paths mentioned: ${list.join(", ")}`);
28816
+ }
28817
+ parts.push(
28818
+ "Continue from the recent messages below; do not re-ask goals already answered above unless the user changes them."
28819
+ );
28820
+ let out = parts.join("\n");
28821
+ if (out.length > maxChars) {
28822
+ out = `${out.slice(0, maxChars - 1)}\u2026`;
28823
+ }
28824
+ return out;
28825
+ }
28826
+ function formatDroppedForLlm(dropped) {
28827
+ const lines = [];
28828
+ for (const m of dropped) {
28829
+ if (m.role === "user") {
28830
+ lines.push(`USER: ${oneLine(m.content, 400)}`);
28831
+ } else if (m.role === "assistant") {
28832
+ const tools = m.toolCalls?.map((t) => t.name).join(",") || "";
28833
+ const body2 = oneLine(m.content, 300);
28834
+ lines.push(
28835
+ tools ? `ASSISTANT(tools=${tools}): ${body2}` : `ASSISTANT: ${body2}`
28836
+ );
28837
+ } else if (m.role === "tool") {
28838
+ lines.push(`TOOL(${m.toolCallId ?? "?"}): ${oneLine(m.content, 160)}`);
28839
+ } else if (m.role === "system") {
28840
+ lines.push(`SYSTEM: ${oneLine(m.content, 200)}`);
28841
+ }
28842
+ }
28843
+ let body = lines.join("\n");
28844
+ if (body.length > MAX_LLM_INPUT_CHARS) {
28845
+ body = body.slice(body.length - MAX_LLM_INPUT_CHARS);
28846
+ }
28847
+ return body;
28848
+ }
28849
+ function oneLine(s, max) {
28850
+ const t = s.replace(/\s+/g, " ").trim();
28851
+ if (t.length <= max) return t;
28852
+ return `${t.slice(0, max - 1)}\u2026`;
28853
+ }
28854
+ function collectPaths(args, out) {
28855
+ if (!args || typeof args !== "object") return;
28856
+ const obj = args;
28857
+ for (const key of ["path", "file", "filepath", "filePath", "target", "cwd"]) {
28858
+ const v = obj[key];
28859
+ if (typeof v === "string" && v.length > 1 && v.length < 260) {
28860
+ out.add(v.replace(/\\/g, "/"));
28861
+ }
28862
+ }
28863
+ if (typeof obj.file_path === "string") out.add(String(obj.file_path));
28864
+ }
28865
+ function collectPathsFromText(text, out) {
28866
+ const re = /(?:^|[\s"'`])((?:[\w.-]+\/)+[\w.-]+\.\w{1,8})/g;
28867
+ let m;
28868
+ let n = 0;
28869
+ while ((m = re.exec(text)) !== null && n < 8) {
28870
+ out.add(m[1]);
28871
+ n += 1;
28872
+ }
28873
+ }
28874
+ var MAX_SUMMARY_CHARS, MAX_LLM_INPUT_CHARS;
28875
+ var init_historySummary = __esm({
28876
+ "src/cli/budget/historySummary.ts"() {
28142
28877
  "use strict";
28878
+ MAX_SUMMARY_CHARS = 3500;
28879
+ MAX_LLM_INPUT_CHARS = 24e3;
28143
28880
  }
28144
28881
  });
28145
28882
 
28146
- // packages/core/dist/state/index.js
28147
- var init_state = __esm({
28148
- "packages/core/dist/state/index.js"() {
28149
- "use strict";
28883
+ // src/cli/budget/llmCompact.ts
28884
+ function isLlmCompactEnabled() {
28885
+ const v = process.env.ZELARI_LLM_COMPACT?.trim().toLowerCase();
28886
+ if (v === "0" || v === "false" || v === "off" || v === "no") return false;
28887
+ return true;
28888
+ }
28889
+ async function llmSummarizeHistory(input) {
28890
+ if (!isLlmCompactEnabled()) return null;
28891
+ if (!input.droppedTranscript.trim()) return null;
28892
+ let config2 = null;
28893
+ try {
28894
+ config2 = await resolveCompactProviderConfig();
28895
+ } catch {
28896
+ return null;
28150
28897
  }
28151
- });
28898
+ if (!config2) return null;
28899
+ const model = process.env.ZELARI_COMPACT_MODEL?.trim() || config2.model;
28900
+ const controller = new AbortController();
28901
+ const timeout = setTimeout(() => controller.abort(), 45e3);
28902
+ const onOuterAbort = () => controller.abort();
28903
+ input.signal?.addEventListener("abort", onOuterAbort, { once: true });
28904
+ try {
28905
+ const url2 = `${config2.baseUrl.replace(/\/$/, "")}/chat/completions`;
28906
+ const res = await fetch(url2, {
28907
+ method: "POST",
28908
+ signal: controller.signal,
28909
+ headers: {
28910
+ "content-type": "application/json",
28911
+ authorization: `Bearer ${config2.apiKey}`
28912
+ },
28913
+ body: JSON.stringify({
28914
+ model,
28915
+ temperature: 0.1,
28916
+ max_tokens: 900,
28917
+ stream: false,
28918
+ messages: [
28919
+ { role: "system", content: COMPACT_SYSTEM },
28920
+ {
28921
+ role: "user",
28922
+ content: `Extractive sketch (may be incomplete):
28923
+ ${input.extractive.slice(0, 2500)}
28152
28924
 
28153
- // packages/core/dist/index.js
28154
- var init_dist = __esm({
28155
- "packages/core/dist/index.js"() {
28925
+ Transcript of dropped turns:
28926
+ ${input.droppedTranscript}`
28927
+ }
28928
+ ]
28929
+ })
28930
+ });
28931
+ if (!res.ok) return null;
28932
+ const json2 = await res.json();
28933
+ const text = json2.choices?.[0]?.message?.content?.trim();
28934
+ if (!text) return null;
28935
+ return "[history-summary \xB7 llm]\n" + text + "\n\nContinue from the recent messages below; honor decisions already made above.";
28936
+ } catch {
28937
+ return null;
28938
+ } finally {
28939
+ clearTimeout(timeout);
28940
+ input.signal?.removeEventListener("abort", onOuterAbort);
28941
+ }
28942
+ }
28943
+ async function resolveCompactProviderConfig() {
28944
+ const active = getProviderConfig().activeProviderId;
28945
+ const meta3 = await resolveApiKeyWithMeta(active);
28946
+ const apiKey = meta3?.apiKey;
28947
+ if (!apiKey) return null;
28948
+ const custom2 = getCustomEndpoint(active);
28949
+ let baseUrl = custom2 || (active === "openai-compatible" || active === "custom" ? process.env.OPENAI_BASE_URL ?? PROVIDER_ENDPOINTS[active] : PROVIDER_ENDPOINTS[active]);
28950
+ if (!baseUrl) return null;
28951
+ const model = getModelForProvider(active);
28952
+ return {
28953
+ apiKey,
28954
+ baseUrl,
28955
+ model,
28956
+ providerId: active
28957
+ };
28958
+ }
28959
+ var COMPACT_SYSTEM;
28960
+ var init_llmCompact = __esm({
28961
+ "src/cli/budget/llmCompact.ts"() {
28156
28962
  "use strict";
28157
- init_events2();
28158
- init_types();
28159
- init_harness();
28160
- init_council();
28161
- init_skills2();
28162
- init_types5();
28163
- init_state();
28963
+ init_providerConfig();
28964
+ init_keyStore();
28965
+ init_openai_compatible();
28966
+ COMPACT_SYSTEM = `You compress earlier turns of a coding-agent session into a dense continuity brief.
28967
+ Output plain text (no markdown fences) with these sections:
28968
+ 1) Goal \u2014 what the user wants
28969
+ 2) Decisions \u2014 choices already made
28970
+ 3) Done \u2014 completed work / files changed
28971
+ 4) Open \u2014 remaining tasks / blockers
28972
+ 5) Constraints \u2014 important rules the agent must keep
28973
+
28974
+ Be factual and concise. Max ~400 words. Do not invent work that was not present.`;
28164
28975
  }
28165
28976
  });
28166
28977
 
@@ -28231,24 +29042,77 @@ function findValidCutIndex(messages, naiveCut) {
28231
29042
  return cut;
28232
29043
  }
28233
29044
  function compactHistory(messages, opts) {
29045
+ return compactHistoryDetailed(messages, opts).messages;
29046
+ }
29047
+ function compactHistoryDetailed(messages, opts) {
28234
29048
  const maxMessages = resolveMaxMessages(opts);
28235
- if (maxMessages === 0) return [];
28236
- if (messages.length <= maxMessages * 2) return messages;
29049
+ if (maxMessages === 0) {
29050
+ return { messages: [], compacted: true, messagesRemoved: messages.length, summary: "" };
29051
+ }
29052
+ if (messages.length <= maxMessages * 2) {
29053
+ return {
29054
+ messages,
29055
+ compacted: false,
29056
+ messagesRemoved: 0,
29057
+ summary: ""
29058
+ };
29059
+ }
28237
29060
  const naiveCut = messages.length - maxMessages;
28238
29061
  const cut = findValidCutIndex(messages, naiveCut);
28239
- const dropped = cut;
28240
- if (dropped === 0) return messages;
29062
+ if (cut === 0) {
29063
+ return {
29064
+ messages,
29065
+ compacted: false,
29066
+ messagesRemoved: 0,
29067
+ summary: ""
29068
+ };
29069
+ }
29070
+ const droppedMsgs = messages.slice(0, cut);
28241
29071
  const kept = messages.slice(cut);
29072
+ const summaryText = extractiveHistorySummary(droppedMsgs);
28242
29073
  const summary = {
28243
29074
  role: "system",
28244
- content: `${COMPACT_MARKER} ${dropped} earlier message(s) dropped.`
29075
+ content: summaryText || `${COMPACT_MARKER} ${cut} earlier message(s) dropped.`
29076
+ };
29077
+ return {
29078
+ messages: [summary, ...kept],
29079
+ compacted: true,
29080
+ messagesRemoved: cut,
29081
+ summary: summary.content
29082
+ };
29083
+ }
29084
+ async function compactHistoryAsync(messages, opts) {
29085
+ const base = compactHistoryDetailed(messages, opts);
29086
+ if (!base.compacted || base.messagesRemoved === 0) return base;
29087
+ const cut = base.messagesRemoved;
29088
+ const droppedMsgs = messages.slice(0, cut);
29089
+ const extractive = extractiveHistorySummary(droppedMsgs);
29090
+ const droppedTranscript = formatDroppedForLlm(droppedMsgs);
29091
+ let summaryText = extractive;
29092
+ try {
29093
+ const llm = await llmSummarizeHistory({
29094
+ extractive,
29095
+ droppedTranscript,
29096
+ signal: opts?.signal
29097
+ });
29098
+ if (llm && llm.trim().length > 40) summaryText = llm.trim();
29099
+ } catch {
29100
+ }
29101
+ const kept = messages.slice(cut);
29102
+ const summary = { role: "system", content: summaryText };
29103
+ return {
29104
+ messages: [summary, ...kept],
29105
+ compacted: true,
29106
+ messagesRemoved: cut,
29107
+ summary: summaryText
28245
29108
  };
28246
- return [summary, ...kept];
28247
29109
  }
28248
29110
  var COMPACT_MARKER;
28249
29111
  var init_historyCompaction = __esm({
28250
29112
  "src/cli/hooks/historyCompaction.ts"() {
28251
29113
  "use strict";
29114
+ init_historySummary();
29115
+ init_llmCompact();
28252
29116
  init_envNumber();
28253
29117
  COMPACT_MARKER = "[history] Earlier turns were compacted to stay within the context budget.";
28254
29118
  }
@@ -28276,8 +29140,8 @@ __export(conversationContext_exports, {
28276
29140
  setHistory: () => setHistory,
28277
29141
  setLastClarification: () => setLastClarification
28278
29142
  });
28279
- import { existsSync as existsSync14 } from "node:fs";
28280
- import { join as join13 } from "node:path";
29143
+ import { existsSync as existsSync16 } from "node:fs";
29144
+ import { join as join15 } from "node:path";
28281
29145
  function getHistory() {
28282
29146
  return history;
28283
29147
  }
@@ -28285,7 +29149,7 @@ function setHistory(messages) {
28285
29149
  history = [...messages];
28286
29150
  }
28287
29151
  function compactInPlace(cwd = process.cwd()) {
28288
- const durableStatePresent = existsSync14(join13(cwd, ".zelari", "state", "HEAD.json"));
29152
+ const durableStatePresent = existsSync16(join15(cwd, ".zelari", "state", "HEAD.json"));
28289
29153
  history = compactHistory(history, { durableStatePresent });
28290
29154
  }
28291
29155
  function appendMessages(msgs) {
@@ -28295,6 +29159,8 @@ function appendMessages(msgs) {
28295
29159
  function clearHistory() {
28296
29160
  history = [];
28297
29161
  lastClarification = null;
29162
+ clearSessionTodos();
29163
+ clearSessionPermissionGrants();
28298
29164
  }
28299
29165
  function serializeHistory() {
28300
29166
  return [...history];
@@ -28441,11 +29307,15 @@ function truncate(s, max) {
28441
29307
  function _resetConversationContextForTests() {
28442
29308
  history = [];
28443
29309
  lastClarification = null;
29310
+ clearSessionTodos();
29311
+ clearSessionPermissionGrants();
28444
29312
  }
28445
29313
  var history, lastClarification, SHORT_CONTINUE, SHORT_CONTINUE_LOOSE;
28446
29314
  var init_conversationContext = __esm({
28447
29315
  "src/cli/hooks/conversationContext.ts"() {
28448
29316
  "use strict";
29317
+ init_toolPermissions();
29318
+ init_sessionTodos();
28449
29319
  init_historyCompaction();
28450
29320
  history = [];
28451
29321
  lastClarification = null;
@@ -28530,14 +29400,14 @@ var init_phase = __esm({
28530
29400
  });
28531
29401
 
28532
29402
  // src/cli/workspace/projectInstructions.ts
28533
- import { existsSync as existsSync15, readFileSync as readFileSync15 } from "node:fs";
28534
- import { join as join14 } from "node:path";
29403
+ import { existsSync as existsSync17, readFileSync as readFileSync16 } from "node:fs";
29404
+ import { join as join16 } from "node:path";
28535
29405
  function loadProjectInstructions(projectRoot = process.cwd(), maxChars = MAX_CHARS) {
28536
29406
  for (const name of CANDIDATES) {
28537
- const full = join14(projectRoot, name);
28538
- if (!existsSync15(full)) continue;
29407
+ const full = join16(projectRoot, name);
29408
+ if (!existsSync17(full)) continue;
28539
29409
  try {
28540
- let raw = readFileSync15(full, "utf8");
29410
+ let raw = readFileSync16(full, "utf8");
28541
29411
  raw = raw.replace(/\r\n/g, "\n").trim();
28542
29412
  if (!raw) continue;
28543
29413
  if (raw.length <= maxChars) {
@@ -28574,20 +29444,20 @@ var init_projectInstructions = __esm({
28574
29444
 
28575
29445
  // src/cli/workspace/paths.ts
28576
29446
  import {
28577
- mkdirSync as mkdirSync7,
28578
- writeFileSync as writeFileSync11,
28579
- existsSync as existsSync16,
29447
+ mkdirSync as mkdirSync8,
29448
+ writeFileSync as writeFileSync12,
29449
+ existsSync as existsSync18,
28580
29450
  accessSync,
28581
29451
  constants,
28582
29452
  realpathSync
28583
29453
  } from "node:fs";
28584
- import { join as join15, basename } from "node:path";
28585
- import { homedir as homedir5 } from "node:os";
28586
- import { createHash as createHash3 } from "node:crypto";
29454
+ import { join as join17, basename } from "node:path";
29455
+ import { homedir as homedir7 } from "node:os";
29456
+ import { createHash as createHash4 } from "node:crypto";
28587
29457
  function resolveWorkspaceRoot(projectRoot = process.cwd()) {
28588
29458
  const candidates = [
28589
- join15(projectRoot, ".zelari"),
28590
- join15(homedir5(), ".zelari-code", "workspace", hashProject(projectRoot))
29459
+ join17(projectRoot, ".zelari"),
29460
+ join17(homedir7(), ".zelari-code", "workspace", hashProject(projectRoot))
28591
29461
  ];
28592
29462
  for (const candidate of candidates) {
28593
29463
  if (isWritableDir(projectRoot) || candidate !== candidates[0]) {
@@ -28599,11 +29469,11 @@ function resolveWorkspaceRoot(projectRoot = process.cwd()) {
28599
29469
  return candidates[0];
28600
29470
  }
28601
29471
  function hashProject(projectPath) {
28602
- return createHash3("sha1").update(realpathSync(projectPath)).digest("hex").slice(0, 12);
29472
+ return createHash4("sha1").update(realpathSync(projectPath)).digest("hex").slice(0, 12);
28603
29473
  }
28604
29474
  function isWritableDir(dir) {
28605
29475
  try {
28606
- if (!existsSync16(dir)) return false;
29476
+ if (!existsSync18(dir)) return false;
28607
29477
  accessSync(dir, constants.W_OK);
28608
29478
  return true;
28609
29479
  } catch {
@@ -28611,26 +29481,26 @@ function isWritableDir(dir) {
28611
29481
  }
28612
29482
  }
28613
29483
  function ensureWorkspaceDir(workspaceDir) {
28614
- mkdirSync7(workspaceDir, { recursive: true });
28615
- if (workspaceDir.endsWith("/.zelari") && existsSync16(join15(workspaceDir, "..", ".git"))) {
28616
- const gitignorePath = join15(workspaceDir, ".gitignore");
28617
- if (!existsSync16(gitignorePath)) {
28618
- writeFileSync11(gitignorePath, "*\n!.gitignore\n");
29484
+ mkdirSync8(workspaceDir, { recursive: true });
29485
+ if (workspaceDir.endsWith("/.zelari") && existsSync18(join17(workspaceDir, "..", ".git"))) {
29486
+ const gitignorePath = join17(workspaceDir, ".gitignore");
29487
+ if (!existsSync18(gitignorePath)) {
29488
+ writeFileSync12(gitignorePath, "*\n!.gitignore\n");
28619
29489
  }
28620
29490
  }
28621
29491
  }
28622
29492
  function workspaceFile(rootDir, kind) {
28623
29493
  switch (kind) {
28624
29494
  case "plan":
28625
- return join15(rootDir, "plan.md");
29495
+ return join17(rootDir, "plan.md");
28626
29496
  case "risks":
28627
- return join15(rootDir, "risks.md");
29497
+ return join17(rootDir, "risks.md");
28628
29498
  case "index":
28629
- return join15(rootDir, "workspace.json");
29499
+ return join17(rootDir, "workspace.json");
28630
29500
  }
28631
29501
  }
28632
29502
  function workspaceArtifact(rootDir, subdir, slug) {
28633
- return join15(rootDir, subdir, `${slug}.md`);
29503
+ return join17(rootDir, subdir, `${slug}.md`);
28634
29504
  }
28635
29505
  function projectName(projectRoot = process.cwd()) {
28636
29506
  return basename(realpathSync(projectRoot));
@@ -28650,8 +29520,8 @@ __export(workspaceSummary_exports, {
28650
29520
  buildWorkspaceSummary: () => buildWorkspaceSummary,
28651
29521
  buildZelariReadHint: () => buildZelariReadHint
28652
29522
  });
28653
- import { existsSync as existsSync17, readFileSync as readFileSync16, readdirSync, statSync as statSync3 } from "node:fs";
28654
- import { join as join16, relative } from "node:path";
29523
+ import { existsSync as existsSync19, readFileSync as readFileSync17, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
29524
+ import { join as join18, relative } from "node:path";
28655
29525
  function buildWorkspaceSummary(projectRoot = process.cwd(), options = {}) {
28656
29526
  const { maxEntries = 30, maxChars = 3500, maxDeps = 24, maxScripts = 16 } = options;
28657
29527
  const name = safeProjectName(projectRoot);
@@ -28684,11 +29554,11 @@ function formatTaskLine(t) {
28684
29554
  }
28685
29555
  function buildPlanSummary(projectRoot = process.cwd(), options) {
28686
29556
  const zelariRoot = resolveWorkspaceRoot(projectRoot);
28687
- const planPath = join16(zelariRoot, "plan.json");
28688
- if (!existsSync17(planPath)) return null;
29557
+ const planPath = join18(zelariRoot, "plan.json");
29558
+ if (!existsSync19(planPath)) return null;
28689
29559
  let plan;
28690
29560
  try {
28691
- plan = JSON.parse(readFileSync16(planPath, "utf8"));
29561
+ plan = JSON.parse(readFileSync17(planPath, "utf8"));
28692
29562
  } catch {
28693
29563
  return null;
28694
29564
  }
@@ -28824,8 +29694,8 @@ function pickNextTask(open) {
28824
29694
  )[0];
28825
29695
  }
28826
29696
  function buildZelariReadHint(projectRoot = process.cwd()) {
28827
- const planPath = join16(resolveWorkspaceRoot(projectRoot), "plan.json");
28828
- if (!existsSync17(planPath)) return "";
29697
+ const planPath = join18(resolveWorkspaceRoot(projectRoot), "plan.json");
29698
+ if (!existsSync19(planPath)) return "";
28829
29699
  return [
28830
29700
  "# Council workspace detected (.zelari/) \u2014 DRAFT vault",
28831
29701
  "`.zelari/plan.json` and `.zelari/docs/` hold **design hypotheses**, not verified product state.",
@@ -28840,10 +29710,10 @@ function safeProjectName(root) {
28840
29710
  }
28841
29711
  }
28842
29712
  function readPackageJson(projectRoot) {
28843
- const p3 = join16(projectRoot, "package.json");
28844
- if (!existsSync17(p3)) return null;
29713
+ const p3 = join18(projectRoot, "package.json");
29714
+ if (!existsSync19(p3)) return null;
28845
29715
  try {
28846
- return JSON.parse(readFileSync16(p3, "utf8"));
29716
+ return JSON.parse(readFileSync17(p3, "utf8"));
28847
29717
  } catch {
28848
29718
  return null;
28849
29719
  }
@@ -28884,7 +29754,7 @@ function readBuildScripts(projectRoot, maxScripts = 16) {
28884
29754
  function listShallow(projectRoot, maxEntries) {
28885
29755
  const out = [];
28886
29756
  try {
28887
- const top = readdirSync(projectRoot, { withFileTypes: true }).filter(
29757
+ const top = readdirSync2(projectRoot, { withFileTypes: true }).filter(
28888
29758
  (e) => !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "dist"
28889
29759
  ).sort((a, b) => a.name.localeCompare(b.name));
28890
29760
  let count = 0;
@@ -28893,11 +29763,11 @@ function listShallow(projectRoot, maxEntries) {
28893
29763
  out.push(`\u2026 (+${top.length - count} more)`);
28894
29764
  break;
28895
29765
  }
28896
- const rel2 = relative(projectRoot, join16(projectRoot, entry.name));
29766
+ const rel2 = relative(projectRoot, join18(projectRoot, entry.name));
28897
29767
  if (entry.isDirectory()) {
28898
29768
  let inner = "";
28899
29769
  try {
28900
- const sub = readdirSync(join16(projectRoot, entry.name), {
29770
+ const sub = readdirSync2(join18(projectRoot, entry.name), {
28901
29771
  withFileTypes: true
28902
29772
  }).filter((e) => !e.name.startsWith(".")).slice(0, 4).map((e) => e.name);
28903
29773
  if (sub.length > 0)
@@ -28945,12 +29815,12 @@ var init_workspaceSummary = __esm({
28945
29815
  });
28946
29816
 
28947
29817
  // src/cli/workspace/buildLessonsSummary.ts
28948
- import { existsSync as existsSync18 } from "node:fs";
28949
- import { join as join17 } from "node:path";
29818
+ import { existsSync as existsSync20 } from "node:fs";
29819
+ import { join as join19 } from "node:path";
28950
29820
  function buildLessonsSummary(projectRoot = process.cwd(), taskText) {
28951
29821
  if (process.env["ZELARI_LESSONS"] === "0") return null;
28952
29822
  const zelariRoot = resolveWorkspaceRoot(projectRoot);
28953
- if (!existsSync18(join17(zelariRoot, "lessons.jsonl"))) return null;
29823
+ if (!existsSync20(join19(zelariRoot, "lessons.jsonl"))) return null;
28954
29824
  const lessons = recallLessons(zelariRoot, {
28955
29825
  maxLessons: 5,
28956
29826
  maxBytes: 2048,
@@ -28971,8 +29841,8 @@ var composeContext_exports = {};
28971
29841
  __export(composeContext_exports, {
28972
29842
  composeProjectContext: () => composeProjectContext
28973
29843
  });
28974
- import { existsSync as existsSync19, readdirSync as readdirSync2, readFileSync as readFileSync17 } from "node:fs";
28975
- import { join as join18 } from "node:path";
29844
+ import { existsSync as existsSync21, readdirSync as readdirSync3, readFileSync as readFileSync18 } from "node:fs";
29845
+ import { join as join20 } from "node:path";
28976
29846
  function cap(text, max, label) {
28977
29847
  if (!text || text.length <= max) return { text: text || "", truncated: false };
28978
29848
  return {
@@ -28984,19 +29854,19 @@ function cap(text, max, label) {
28984
29854
  }
28985
29855
  function buildDesignIndex(projectRoot, maxChars) {
28986
29856
  const root = resolveWorkspaceRoot(projectRoot);
28987
- if (!existsSync19(root)) return "";
29857
+ if (!existsSync21(root)) return "";
28988
29858
  const lines = [
28989
29859
  "# Design vault index (.zelari/) \u2014 HYPOTHESES only",
28990
29860
  "Full design docs are NOT product source of truth. Open with list_files / read_file / searchDocuments if needed."
28991
29861
  ];
28992
- const docsDir = join18(root, "docs");
28993
- if (existsSync19(docsDir)) {
29862
+ const docsDir = join20(root, "docs");
29863
+ if (existsSync21(docsDir)) {
28994
29864
  try {
28995
- const docs = readdirSync2(docsDir).filter((n) => n.endsWith(".md")).slice(0, 12);
29865
+ const docs = readdirSync3(docsDir).filter((n) => n.endsWith(".md")).slice(0, 12);
28996
29866
  if (docs.length > 0) {
28997
29867
  lines.push("", "## docs/ (titles only)");
28998
29868
  for (const d of docs) lines.push(`- .zelari/docs/${d}`);
28999
- if (readdirSync2(docsDir).filter((n) => n.endsWith(".md")).length > 12) {
29869
+ if (readdirSync3(docsDir).filter((n) => n.endsWith(".md")).length > 12) {
29000
29870
  lines.push("- \u2026 (more under .zelari/docs/)");
29001
29871
  }
29002
29872
  }
@@ -29004,14 +29874,14 @@ function buildDesignIndex(projectRoot, maxChars) {
29004
29874
  }
29005
29875
  }
29006
29876
  for (const name of ["risks.md", "plan.json", "nfr-spec.json"]) {
29007
- if (existsSync19(join18(root, name))) {
29877
+ if (existsSync21(join20(root, name))) {
29008
29878
  lines.push(`- .zelari/${name} present`);
29009
29879
  }
29010
29880
  }
29011
- const decisionsDir = join18(root, "decisions");
29012
- if (existsSync19(decisionsDir)) {
29881
+ const decisionsDir = join20(root, "decisions");
29882
+ if (existsSync21(decisionsDir)) {
29013
29883
  try {
29014
- const n = readdirSync2(decisionsDir).filter((f) => f.endsWith(".md")).length;
29884
+ const n = readdirSync3(decisionsDir).filter((f) => f.endsWith(".md")).length;
29015
29885
  if (n > 0) lines.push(`- .zelari/decisions/ (${n} ADR file(s) \u2014 treat proposed as non-binding)`);
29016
29886
  } catch {
29017
29887
  }
@@ -29110,17 +29980,17 @@ function composeProjectContext(input) {
29110
29980
  }
29111
29981
  function readDurableHeadSync(projectRoot) {
29112
29982
  try {
29113
- const headPath = join18(projectRoot, ".zelari", "state", "HEAD.json");
29114
- if (!existsSync19(headPath)) return "";
29115
- const head = JSON.parse(readFileSync17(headPath, "utf8"));
29983
+ const headPath = join20(projectRoot, ".zelari", "state", "HEAD.json");
29984
+ if (!existsSync21(headPath)) return "";
29985
+ const head = JSON.parse(readFileSync18(headPath, "utf8"));
29116
29986
  if (!head?.id) return "";
29117
- const metaPath = join18(projectRoot, ".zelari", "state", "commits", `${head.id}.json`);
29118
- if (!existsSync19(metaPath)) return "";
29119
- const meta3 = JSON.parse(readFileSync17(metaPath, "utf8"));
29120
- const discPath = meta3.artifactDir ? join18(projectRoot, ".zelari", "state", meta3.artifactDir, "discoveries.json") : join18(projectRoot, ".zelari", "state", "artifacts", head.id, "discoveries.json");
29987
+ const metaPath = join20(projectRoot, ".zelari", "state", "commits", `${head.id}.json`);
29988
+ if (!existsSync21(metaPath)) return "";
29989
+ const meta3 = JSON.parse(readFileSync18(metaPath, "utf8"));
29990
+ const discPath = meta3.artifactDir ? join20(projectRoot, ".zelari", "state", meta3.artifactDir, "discoveries.json") : join20(projectRoot, ".zelari", "state", "artifacts", head.id, "discoveries.json");
29121
29991
  let discoveries = [];
29122
- if (existsSync19(discPath)) {
29123
- discoveries = JSON.parse(readFileSync17(discPath, "utf8"));
29992
+ if (existsSync21(discPath)) {
29993
+ discoveries = JSON.parse(readFileSync18(discPath, "utf8"));
29124
29994
  }
29125
29995
  const reusable = discoveries.filter((d) => d.reusable !== false);
29126
29996
  const lines = [
@@ -29153,13 +30023,13 @@ var planDetect_exports = {};
29153
30023
  __export(planDetect_exports, {
29154
30024
  hasWorkspacePlan: () => hasWorkspacePlan
29155
30025
  });
29156
- import { existsSync as existsSync20, readFileSync as readFileSync18 } from "node:fs";
29157
- import { join as join19 } from "node:path";
30026
+ import { existsSync as existsSync22, readFileSync as readFileSync19 } from "node:fs";
30027
+ import { join as join21 } from "node:path";
29158
30028
  function hasWorkspacePlan(projectRoot = process.cwd()) {
29159
- const planPath = join19(resolveWorkspaceRoot(projectRoot), "plan.json");
29160
- if (!existsSync20(planPath)) return false;
30029
+ const planPath = join21(resolveWorkspaceRoot(projectRoot), "plan.json");
30030
+ if (!existsSync22(planPath)) return false;
29161
30031
  try {
29162
- const parsed = JSON.parse(readFileSync18(planPath, "utf8"));
30032
+ const parsed = JSON.parse(readFileSync19(planPath, "utf8"));
29163
30033
  return Array.isArray(parsed.phases) && parsed.phases.length > 0;
29164
30034
  } catch {
29165
30035
  return false;
@@ -29219,14 +30089,14 @@ __export(storage_exports, {
29219
30089
  workspaceMutex: () => workspaceMutex
29220
30090
  });
29221
30091
  import {
29222
- readFileSync as readFileSync19,
29223
- writeFileSync as writeFileSync12,
29224
- existsSync as existsSync21,
29225
- mkdirSync as mkdirSync8,
29226
- readdirSync as readdirSync3,
30092
+ readFileSync as readFileSync20,
30093
+ writeFileSync as writeFileSync13,
30094
+ existsSync as existsSync23,
30095
+ mkdirSync as mkdirSync9,
30096
+ readdirSync as readdirSync4,
29227
30097
  renameSync as renameSync2
29228
30098
  } from "node:fs";
29229
- import { dirname as dirname4, join as join20 } from "node:path";
30099
+ import { dirname as dirname4, join as join22 } from "node:path";
29230
30100
  function parseFrontmatter(md) {
29231
30101
  const m = FRONTMATTER_RE.exec(md);
29232
30102
  if (!m) return { meta: {}, body: md };
@@ -29481,15 +30351,15 @@ var init_storage = __esm({
29481
30351
  Storage = class {
29482
30352
  /** Read a Markdown file with frontmatter. Throws if not found. */
29483
30353
  read(path38) {
29484
- if (!existsSync21(path38)) {
30354
+ if (!existsSync23(path38)) {
29485
30355
  throw new Error(`File not found: ${path38}`);
29486
30356
  }
29487
- const md = readFileSync19(path38, "utf8");
30357
+ const md = readFileSync20(path38, "utf8");
29488
30358
  return parseFrontmatter(md);
29489
30359
  }
29490
30360
  /** Read a Markdown file; returns null if not found. */
29491
30361
  readIfExists(path38) {
29492
- if (!existsSync21(path38)) return null;
30362
+ if (!existsSync23(path38)) return null;
29493
30363
  return this.read(path38);
29494
30364
  }
29495
30365
  /**
@@ -29497,16 +30367,16 @@ var init_storage = __esm({
29497
30367
  * The meta object is serialized as YAML frontmatter; body as Markdown.
29498
30368
  */
29499
30369
  write(path38, meta3, body) {
29500
- mkdirSync8(dirname4(path38), { recursive: true });
30370
+ mkdirSync9(dirname4(path38), { recursive: true });
29501
30371
  const tmp = path38 + ".tmp-" + process.pid;
29502
30372
  const md = serializeFrontmatter(meta3, body);
29503
- writeFileSync12(tmp, md, "utf8");
30373
+ writeFileSync13(tmp, md, "utf8");
29504
30374
  renameSync2(tmp, path38);
29505
30375
  }
29506
30376
  /** List all .md files in a directory (non-recursive). */
29507
30377
  listMarkdown(dir) {
29508
- if (!existsSync21(dir)) return [];
29509
- return readdirSync3(dir).filter((f) => f.endsWith(".md") && !f.startsWith(".")).map((f) => join20(dir, f));
30378
+ if (!existsSync23(dir)) return [];
30379
+ return readdirSync4(dir).filter((f) => f.endsWith(".md") && !f.startsWith(".")).map((f) => join22(dir, f));
29510
30380
  }
29511
30381
  };
29512
30382
  KeyedMutex = class {
@@ -29545,14 +30415,14 @@ __export(stubs_exports, {
29545
30415
  resolveWorkspaceRoot: () => resolveWorkspaceRoot
29546
30416
  });
29547
30417
  import {
29548
- existsSync as existsSync22,
29549
- readdirSync as readdirSync4,
29550
- writeFileSync as writeFileSync13,
29551
- readFileSync as readFileSync20,
29552
- mkdirSync as mkdirSync9,
30418
+ existsSync as existsSync24,
30419
+ readdirSync as readdirSync5,
30420
+ writeFileSync as writeFileSync14,
30421
+ readFileSync as readFileSync21,
30422
+ mkdirSync as mkdirSync10,
29553
30423
  renameSync as renameSync3
29554
30424
  } from "node:fs";
29555
- import { join as join21, basename as basename2, dirname as dirname5, relative as relative2 } from "node:path";
30425
+ import { join as join23, basename as basename2, dirname as dirname5, relative as relative2 } from "node:path";
29556
30426
  function createWorkspaceContext(projectRoot = process.cwd()) {
29557
30427
  const rootDir = resolveWorkspaceRoot(projectRoot);
29558
30428
  return {
@@ -29562,14 +30432,14 @@ function createWorkspaceContext(projectRoot = process.cwd()) {
29562
30432
  };
29563
30433
  }
29564
30434
  function planJsonPath(ctx) {
29565
- return join21(ctx.rootDir, "plan.json");
30435
+ return join23(ctx.rootDir, "plan.json");
29566
30436
  }
29567
30437
  function readPlan(ctx) {
29568
30438
  const jsonPath = planJsonPath(ctx);
29569
- if (existsSync22(jsonPath)) {
30439
+ if (existsSync24(jsonPath)) {
29570
30440
  try {
29571
30441
  const parsed = JSON.parse(
29572
- readFileSync20(jsonPath, "utf8")
30442
+ readFileSync21(jsonPath, "utf8")
29573
30443
  );
29574
30444
  return {
29575
30445
  phases: Array.isArray(parsed.phases) ? parsed.phases : [],
@@ -29591,9 +30461,9 @@ function readPlan(ctx) {
29591
30461
  }
29592
30462
  function writePlan(ctx, summary) {
29593
30463
  const jsonPath = planJsonPath(ctx);
29594
- mkdirSync9(dirname5(jsonPath), { recursive: true });
30464
+ mkdirSync10(dirname5(jsonPath), { recursive: true });
29595
30465
  const tmp = jsonPath + ".tmp-" + process.pid;
29596
- writeFileSync13(tmp, JSON.stringify(summary, null, 2), "utf8");
30466
+ writeFileSync14(tmp, JSON.stringify(summary, null, 2), "utf8");
29597
30467
  renameSync3(tmp, jsonPath);
29598
30468
  const mdPath = workspaceFile(ctx.rootDir, "plan");
29599
30469
  const summaryMeta = {
@@ -29660,9 +30530,9 @@ function renderPlanBody(summary) {
29660
30530
  return lines.join("\n");
29661
30531
  }
29662
30532
  function nextAdrId(ctx) {
29663
- const decisionsDir = join21(ctx.rootDir, "decisions");
29664
- if (!existsSync22(decisionsDir)) return "001";
29665
- const existing = readdirSync4(decisionsDir).filter((f) => f.endsWith(".md")).map((f) => f.match(/^(\d+)-/)).filter((m) => !!m).map((m) => parseInt(m[1], 10));
30533
+ const decisionsDir = join23(ctx.rootDir, "decisions");
30534
+ if (!existsSync24(decisionsDir)) return "001";
30535
+ const existing = readdirSync5(decisionsDir).filter((f) => f.endsWith(".md")).map((f) => f.match(/^(\d+)-/)).filter((m) => !!m).map((m) => parseInt(m[1], 10));
29666
30536
  const max = existing.length === 0 ? 0 : Math.max(...existing);
29667
30537
  return String(max + 1).padStart(3, "0");
29668
30538
  }
@@ -29703,7 +30573,7 @@ function addTaskRecord(ctx, summary, phaseId, t, options) {
29703
30573
  status: "pending",
29704
30574
  priority: t.priority
29705
30575
  });
29706
- const taskPath = join21(ctx.rootDir, "plan-tasks", `${id}.md`);
30576
+ const taskPath = join23(ctx.rootDir, "plan-tasks", `${id}.md`);
29707
30577
  const meta3 = {
29708
30578
  kind: "task",
29709
30579
  id,
@@ -29745,7 +30615,7 @@ function addMilestoneRecord(ctx, summary, input) {
29745
30615
  dueDate: input.dueDate,
29746
30616
  targetVersion: version2
29747
30617
  });
29748
- const path38 = join21(ctx.rootDir, "milestones", `${id}.md`);
30618
+ const path38 = join23(ctx.rootDir, "milestones", `${id}.md`);
29749
30619
  const meta3 = {
29750
30620
  kind: "milestone",
29751
30621
  id,
@@ -30043,8 +30913,8 @@ function createNfrSpecStub(ctx) {
30043
30913
  },
30044
30914
  planFeatureKeywords: Array.isArray(args["planFeatureKeywords"]) ? args["planFeatureKeywords"] : void 0
30045
30915
  };
30046
- const outPath = join21(ctx.rootDir, "nfr-spec.json");
30047
- writeFileSync13(outPath, JSON.stringify(spec, null, 2), "utf8");
30916
+ const outPath = join23(ctx.rootDir, "nfr-spec.json");
30917
+ writeFileSync14(outPath, JSON.stringify(spec, null, 2), "utf8");
30048
30918
  return `NFR spec written to nfr-spec.json (${targets.length} target(s)).`;
30049
30919
  });
30050
30920
  }
@@ -30107,18 +30977,18 @@ function searchDocumentsStub(ctx) {
30107
30977
  (w) => w.length >= 3 && w !== "or" && w !== "and" && w !== "the"
30108
30978
  );
30109
30979
  const files = [
30110
- ...ctx.storage.listMarkdown(join21(ctx.rootDir, "decisions")),
30111
- ...ctx.storage.listMarkdown(join21(ctx.rootDir, "docs")),
30112
- ...ctx.storage.listMarkdown(join21(ctx.rootDir, "reviews")),
30113
- ...ctx.storage.listMarkdown(join21(ctx.rootDir, "plan-tasks")),
30114
- ...ctx.storage.listMarkdown(join21(ctx.rootDir, "milestones")),
30980
+ ...ctx.storage.listMarkdown(join23(ctx.rootDir, "decisions")),
30981
+ ...ctx.storage.listMarkdown(join23(ctx.rootDir, "docs")),
30982
+ ...ctx.storage.listMarkdown(join23(ctx.rootDir, "reviews")),
30983
+ ...ctx.storage.listMarkdown(join23(ctx.rootDir, "plan-tasks")),
30984
+ ...ctx.storage.listMarkdown(join23(ctx.rootDir, "milestones")),
30115
30985
  workspaceFile(ctx.rootDir, "plan"),
30116
30986
  workspaceFile(ctx.rootDir, "risks")
30117
30987
  ];
30118
30988
  const results = [];
30119
30989
  for (const file2 of files) {
30120
- if (!existsSync22(file2)) continue;
30121
- const raw = readFileSync20(file2, "utf8");
30990
+ if (!existsSync24(file2)) continue;
30991
+ const raw = readFileSync21(file2, "utf8");
30122
30992
  const content = raw.toLowerCase();
30123
30993
  let idx = -1;
30124
30994
  let matchLen = 0;
@@ -30169,9 +31039,9 @@ function linkDocumentsStub(ctx) {
30169
31039
  const toId = args["toId"] ?? args["targetId"] ?? args["targetPathOrTitle"];
30170
31040
  if (!fromId || !toId) return "linkDocuments requires fromId and toId.";
30171
31041
  const allFiles = [
30172
- ...ctx.storage.listMarkdown(join21(ctx.rootDir, "decisions")),
30173
- ...ctx.storage.listMarkdown(join21(ctx.rootDir, "docs")),
30174
- ...ctx.storage.listMarkdown(join21(ctx.rootDir, "reviews"))
31042
+ ...ctx.storage.listMarkdown(join23(ctx.rootDir, "decisions")),
31043
+ ...ctx.storage.listMarkdown(join23(ctx.rootDir, "docs")),
31044
+ ...ctx.storage.listMarkdown(join23(ctx.rootDir, "reviews"))
30175
31045
  ];
30176
31046
  const source = allFiles.find((f) => {
30177
31047
  try {
@@ -30202,9 +31072,9 @@ function getDocumentBacklinksStub(ctx) {
30202
31072
  const targetId = args["targetId"] ?? args["id"];
30203
31073
  if (!targetId) return "getDocumentBacklinks requires targetId.";
30204
31074
  const allFiles = [
30205
- ...ctx.storage.listMarkdown(join21(ctx.rootDir, "decisions")),
30206
- ...ctx.storage.listMarkdown(join21(ctx.rootDir, "docs")),
30207
- ...ctx.storage.listMarkdown(join21(ctx.rootDir, "reviews"))
31075
+ ...ctx.storage.listMarkdown(join23(ctx.rootDir, "decisions")),
31076
+ ...ctx.storage.listMarkdown(join23(ctx.rootDir, "docs")),
31077
+ ...ctx.storage.listMarkdown(join23(ctx.rootDir, "reviews"))
30208
31078
  ];
30209
31079
  const backlinks = [];
30210
31080
  for (const file2 of allFiles) {
@@ -30299,7 +31169,7 @@ __export(updater_exports, {
30299
31169
  });
30300
31170
  import { createRequire as createRequire2 } from "node:module";
30301
31171
  import { spawn as spawn6 } from "node:child_process";
30302
- import { existsSync as existsSync23 } from "node:fs";
31172
+ import { existsSync as existsSync25 } from "node:fs";
30303
31173
  import path25 from "node:path";
30304
31174
  import { fileURLToPath } from "node:url";
30305
31175
  function resolveBundledNpmCli(execPath = process.execPath) {
@@ -30312,7 +31182,7 @@ function resolveBundledNpmCli(execPath = process.execPath) {
30312
31182
  ];
30313
31183
  for (const candidate of candidates) {
30314
31184
  try {
30315
- if (existsSync23(candidate)) return candidate;
31185
+ if (existsSync25(candidate)) return candidate;
30316
31186
  } catch {
30317
31187
  }
30318
31188
  }
@@ -30612,23 +31482,23 @@ var init_mcpClient = __esm({
30612
31482
 
30613
31483
  // src/cli/mcp/mcpConfigIo.ts
30614
31484
  import {
30615
- existsSync as existsSync24,
30616
- mkdirSync as mkdirSync10,
30617
- readFileSync as readFileSync21,
30618
- writeFileSync as writeFileSync14
31485
+ existsSync as existsSync26,
31486
+ mkdirSync as mkdirSync11,
31487
+ readFileSync as readFileSync22,
31488
+ writeFileSync as writeFileSync15
30619
31489
  } from "node:fs";
30620
- import { dirname as dirname6, join as join22 } from "node:path";
30621
- import { homedir as homedir6 } from "node:os";
31490
+ import { dirname as dirname6, join as join24 } from "node:path";
31491
+ import { homedir as homedir8 } from "node:os";
30622
31492
  function getUserMcpPath() {
30623
- return join22(homedir6(), ".zelari-code", "mcp.json");
31493
+ return join24(homedir8(), ".zelari-code", "mcp.json");
30624
31494
  }
30625
31495
  function getProjectMcpPath(projectRoot) {
30626
- return join22(projectRoot, ".zelari", "mcp.json");
31496
+ return join24(projectRoot, ".zelari", "mcp.json");
30627
31497
  }
30628
31498
  function readFile2(path38) {
30629
- if (!existsSync24(path38)) return {};
31499
+ if (!existsSync26(path38)) return {};
30630
31500
  try {
30631
- const parsed = JSON.parse(readFileSync21(path38, "utf8"));
31501
+ const parsed = JSON.parse(readFileSync22(path38, "utf8"));
30632
31502
  const out = {};
30633
31503
  for (const [name, cfg] of Object.entries(parsed.mcpServers ?? {})) {
30634
31504
  if (!cfg || typeof cfg.command !== "string" || !cfg.command.trim()) continue;
@@ -30645,9 +31515,9 @@ function readFile2(path38) {
30645
31515
  }
30646
31516
  }
30647
31517
  function writeFile(path38, servers) {
30648
- mkdirSync10(dirname6(path38), { recursive: true });
31518
+ mkdirSync11(dirname6(path38), { recursive: true });
30649
31519
  const body = { mcpServers: servers };
30650
- writeFileSync14(path38, `${JSON.stringify(body, null, 2)}
31520
+ writeFileSync15(path38, `${JSON.stringify(body, null, 2)}
30651
31521
  `, "utf8");
30652
31522
  }
30653
31523
  function listMcpServers(projectRoot) {
@@ -30801,20 +31671,20 @@ __export(mcpManager_exports, {
30801
31671
  readMcpConfig: () => readMcpConfig,
30802
31672
  registerMcpTools: () => registerMcpTools
30803
31673
  });
30804
- import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
30805
- import { join as join23 } from "node:path";
30806
- import { homedir as homedir7 } from "node:os";
31674
+ import { existsSync as existsSync27, readFileSync as readFileSync23 } from "node:fs";
31675
+ import { join as join25 } from "node:path";
31676
+ import { homedir as homedir9 } from "node:os";
30807
31677
  function readMcpConfig(projectRoot = process.cwd()) {
30808
31678
  const merged = {};
30809
31679
  const paths = [];
30810
31680
  if (process.env["ZELARI_MCP_USER"] !== "0") {
30811
- paths.push(join23(homedir7(), ".zelari-code", "mcp.json"));
31681
+ paths.push(join25(homedir9(), ".zelari-code", "mcp.json"));
30812
31682
  }
30813
- paths.push(join23(projectRoot, ".zelari", "mcp.json"));
31683
+ paths.push(join25(projectRoot, ".zelari", "mcp.json"));
30814
31684
  for (const p3 of paths) {
30815
- if (!existsSync25(p3)) continue;
31685
+ if (!existsSync27(p3)) continue;
30816
31686
  try {
30817
- const parsed = JSON.parse(readFileSync22(p3, "utf8"));
31687
+ const parsed = JSON.parse(readFileSync23(p3, "utf8"));
30818
31688
  for (const [name, cfg] of Object.entries(parsed.mcpServers ?? {})) {
30819
31689
  if (!cfg || typeof cfg.command !== "string" || cfg.command.length === 0) continue;
30820
31690
  merged[name] = cfg;
@@ -31046,13 +31916,13 @@ __export(agentsMd_exports, {
31046
31916
  serializeAgentsMd: () => serializeAgentsMd,
31047
31917
  updateAgentsMd: () => updateAgentsMd
31048
31918
  });
31049
- import { existsSync as existsSync26, readFileSync as readFileSync23, writeFileSync as writeFileSync15 } from "node:fs";
31050
- import { createHash as createHash4 } from "node:crypto";
31051
- import { join as join24 } from "node:path";
31919
+ import { existsSync as existsSync28, readFileSync as readFileSync24, writeFileSync as writeFileSync16 } from "node:fs";
31920
+ import { createHash as createHash5 } from "node:crypto";
31921
+ import { join as join26 } from "node:path";
31052
31922
  import { readFile as readFile3 } from "node:fs/promises";
31053
31923
  async function readPackageJson2(projectRoot) {
31054
- const path38 = join24(projectRoot, "package.json");
31055
- if (!existsSync26(path38)) return null;
31924
+ const path38 = join26(projectRoot, "package.json");
31925
+ if (!existsSync28(path38)) return null;
31056
31926
  try {
31057
31927
  return JSON.parse(await readFile3(path38, "utf8"));
31058
31928
  } catch {
@@ -31076,8 +31946,8 @@ async function genTechStack(ctx) {
31076
31946
  ].join("\n");
31077
31947
  }
31078
31948
  async function genDecisions(ctx) {
31079
- const decisionsDir = join24(ctx.rootDir, "decisions");
31080
- if (!existsSync26(decisionsDir)) return "_No ADRs yet._";
31949
+ const decisionsDir = join26(ctx.rootDir, "decisions");
31950
+ if (!existsSync28(decisionsDir)) return "_No ADRs yet._";
31081
31951
  const files = ctx.storage.listMarkdown(decisionsDir).sort();
31082
31952
  const accepted = [];
31083
31953
  const proposed = [];
@@ -31102,9 +31972,9 @@ async function genDecisions(ctx) {
31102
31972
  }
31103
31973
  async function genConventions(ctx) {
31104
31974
  const lines = [];
31105
- const claudeMd = join24(ctx.projectRoot, "CLAUDE.MD");
31106
- if (existsSync26(claudeMd)) {
31107
- const content = readFileSync23(claudeMd, "utf8");
31975
+ const claudeMd = join26(ctx.projectRoot, "CLAUDE.MD");
31976
+ if (existsSync28(claudeMd)) {
31977
+ const content = readFileSync24(claudeMd, "utf8");
31108
31978
  const match = content.match(/## Architecture rules[\s\S]+?(?=\n## |\n*$)/);
31109
31979
  if (match) {
31110
31980
  lines.push('<!-- Extracted from CLAUDE.MD "Architecture rules" -->');
@@ -31136,9 +32006,9 @@ async function genBuild(ctx) {
31136
32006
  ].join("\n");
31137
32007
  }
31138
32008
  async function genOpenQuestions(ctx) {
31139
- const path38 = join24(ctx.rootDir, "risks.md");
31140
- if (!existsSync26(path38)) return "_No open questions._";
31141
- const content = readFileSync23(path38, "utf8");
32009
+ const path38 = join26(ctx.rootDir, "risks.md");
32010
+ if (!existsSync28(path38)) return "_No open questions._";
32011
+ const content = readFileSync24(path38, "utf8");
31142
32012
  const lines = content.split("\n");
31143
32013
  const questions = [];
31144
32014
  let currentTitle = "";
@@ -31212,9 +32082,9 @@ function titleCase(id) {
31212
32082
  return id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
31213
32083
  }
31214
32084
  async function updateAgentsMd(ctx, projectRoot) {
31215
- const agentsPath = join24(projectRoot, "AGENTS.MD");
31216
- if (existsSync26(agentsPath)) {
31217
- const content = readFileSync23(agentsPath, "utf8");
32085
+ const agentsPath = join26(projectRoot, "AGENTS.MD");
32086
+ if (existsSync28(agentsPath)) {
32087
+ const content = readFileSync24(agentsPath, "utf8");
31218
32088
  const hasAnyMarker = AUTO_SECTIONS.some((id) => content.includes(MARKER_OPEN(id)));
31219
32089
  if (!hasAnyMarker) {
31220
32090
  return {
@@ -31229,8 +32099,8 @@ async function updateAgentsMd(ctx, projectRoot) {
31229
32099
  newSections.set(id, await GENERATORS[id](ctx));
31230
32100
  }
31231
32101
  let manualContent = "";
31232
- if (existsSync26(agentsPath)) {
31233
- const { manualBlocks } = parseAgentsMd(readFileSync23(agentsPath, "utf8"));
32102
+ if (existsSync28(agentsPath)) {
32103
+ const { manualBlocks } = parseAgentsMd(readFileSync24(agentsPath, "utf8"));
31234
32104
  manualContent = manualBlocks.after;
31235
32105
  } else {
31236
32106
  const projectName2 = projectName(projectRoot);
@@ -31246,7 +32116,7 @@ async function updateAgentsMd(ctx, projectRoot) {
31246
32116
  ""
31247
32117
  ].join("\n");
31248
32118
  }
31249
- const oldContent = existsSync26(agentsPath) ? readFileSync23(agentsPath, "utf8") : "";
32119
+ const oldContent = existsSync28(agentsPath) ? readFileSync24(agentsPath, "utf8") : "";
31250
32120
  const { sections: oldSections } = parseAgentsMd(oldContent);
31251
32121
  const changedSections = [];
31252
32122
  for (const id of AUTO_SECTIONS) {
@@ -31258,11 +32128,11 @@ async function updateAgentsMd(ctx, projectRoot) {
31258
32128
  return { changed: false, sections: [] };
31259
32129
  }
31260
32130
  const newContent = serializeAgentsMd(manualContent, newSections);
31261
- writeFileSync15(agentsPath, newContent, "utf8");
32131
+ writeFileSync16(agentsPath, newContent, "utf8");
31262
32132
  return { changed: true, sections: changedSections };
31263
32133
  }
31264
32134
  function hash2(s) {
31265
- return createHash4("sha256").update(s).digest("hex").slice(0, 16);
32135
+ return createHash5("sha256").update(s).digest("hex").slice(0, 16);
31266
32136
  }
31267
32137
  var AUTO_SECTIONS, MARKER_OPEN, MARKER_CLOSE, GENERATORS;
31268
32138
  var init_agentsMd = __esm({
@@ -31384,8 +32254,8 @@ var init_completeDesign = __esm({
31384
32254
 
31385
32255
  // src/cli/workspace/projectSmoke.ts
31386
32256
  import { spawn as spawn8 } from "node:child_process";
31387
- import { existsSync as existsSync27, readFileSync as readFileSync24 } from "node:fs";
31388
- import { join as join25 } from "node:path";
32257
+ import { existsSync as existsSync29, readFileSync as readFileSync25 } from "node:fs";
32258
+ import { join as join27 } from "node:path";
31389
32259
  function pickSmokeScript(scripts) {
31390
32260
  if (!scripts) return null;
31391
32261
  for (const name of SMOKE_SCRIPT_PRIORITY) {
@@ -31397,13 +32267,13 @@ async function runProjectSmoke(projectRoot, timeoutMs = DEFAULT_TIMEOUT_MS) {
31397
32267
  if (process.env["ZELARI_SMOKE"] === "0") {
31398
32268
  return { ran: false, reason: "ZELARI_SMOKE=0 (disabled)" };
31399
32269
  }
31400
- const pkgPath = join25(projectRoot, "package.json");
31401
- if (!existsSync27(pkgPath)) {
32270
+ const pkgPath = join27(projectRoot, "package.json");
32271
+ if (!existsSync29(pkgPath)) {
31402
32272
  return { ran: false, reason: "no package.json (skipped)" };
31403
32273
  }
31404
32274
  let scripts = {};
31405
32275
  try {
31406
- const pkg = JSON.parse(readFileSync24(pkgPath, "utf8"));
32276
+ const pkg = JSON.parse(readFileSync25(pkgPath, "utf8"));
31407
32277
  scripts = pkg.scripts ?? {};
31408
32278
  } catch {
31409
32279
  return { ran: false, reason: "package.json unreadable (skipped)" };
@@ -31491,8 +32361,8 @@ __export(postCouncilHook_exports, {
31491
32361
  runPostCouncilHook: () => runPostCouncilHook
31492
32362
  });
31493
32363
  import { spawn as spawn9 } from "node:child_process";
31494
- import { existsSync as existsSync28, readFileSync as readFileSync25 } from "node:fs";
31495
- import { join as join26 } from "node:path";
32364
+ import { existsSync as existsSync30, readFileSync as readFileSync26 } from "node:fs";
32365
+ import { join as join28 } from "node:path";
31496
32366
  async function runCompleteDesignPostProcessor(ctx, options) {
31497
32367
  if (options?.runMode === "implementation") {
31498
32368
  return {
@@ -31503,9 +32373,9 @@ async function runCompleteDesignPostProcessor(ctx, options) {
31503
32373
  if (process.env["ZELARI_COMPLETE_DESIGN"] === "0") {
31504
32374
  return { ran: false, reason: "ZELARI_COMPLETE_DESIGN=0 (disabled)" };
31505
32375
  }
31506
- const planJsonPath2 = join26(ctx.rootDir, "plan.json");
31507
- const scriptPath = join26(ctx.projectRoot, "complete-design.mjs");
31508
- if (!existsSync28(planJsonPath2)) {
32376
+ const planJsonPath2 = join28(ctx.rootDir, "plan.json");
32377
+ const scriptPath = join28(ctx.projectRoot, "complete-design.mjs");
32378
+ if (!existsSync30(planJsonPath2)) {
31509
32379
  return {
31510
32380
  ran: false,
31511
32381
  reason: ".zelari/plan.json missing (not design-phase)"
@@ -31513,7 +32383,7 @@ async function runCompleteDesignPostProcessor(ctx, options) {
31513
32383
  }
31514
32384
  let phaseCount = 0;
31515
32385
  try {
31516
- const parsed = JSON.parse(readFileSync25(planJsonPath2, "utf8"));
32386
+ const parsed = JSON.parse(readFileSync26(planJsonPath2, "utf8"));
31517
32387
  phaseCount = Array.isArray(parsed.phases) ? parsed.phases.length : 0;
31518
32388
  } catch {
31519
32389
  return { ran: false, reason: ".zelari/plan.json corrupt" };
@@ -31521,7 +32391,7 @@ async function runCompleteDesignPostProcessor(ctx, options) {
31521
32391
  if (phaseCount === 0) {
31522
32392
  return { ran: false, reason: ".zelari/plan.json has no phases" };
31523
32393
  }
31524
- if (!existsSync28(scriptPath)) {
32394
+ if (!existsSync30(scriptPath)) {
31525
32395
  try {
31526
32396
  const builtin = await runBuiltinCompleteDesign(ctx);
31527
32397
  return {
@@ -31744,10 +32614,10 @@ __export(councilFeedback_exports, {
31744
32614
  });
31745
32615
  import {
31746
32616
  promises as fs15,
31747
- existsSync as existsSync29,
31748
- readFileSync as readFileSync26,
31749
- writeFileSync as writeFileSync16,
31750
- mkdirSync as mkdirSync11
32617
+ existsSync as existsSync31,
32618
+ readFileSync as readFileSync27,
32619
+ writeFileSync as writeFileSync17,
32620
+ mkdirSync as mkdirSync12
31751
32621
  } from "node:fs";
31752
32622
  import path26 from "node:path";
31753
32623
  import os8 from "node:os";
@@ -31853,9 +32723,9 @@ var init_councilFeedback = __esm({
31853
32723
  }
31854
32724
  // --- persistence ---------------------------------------------------------
31855
32725
  load() {
31856
- if (!existsSync29(this.file)) return;
32726
+ if (!existsSync31(this.file)) return;
31857
32727
  try {
31858
- const raw = readFileSync26(this.file, "utf-8");
32728
+ const raw = readFileSync27(this.file, "utf-8");
31859
32729
  const parsed = JSON.parse(raw);
31860
32730
  if (parsed && Array.isArray(parsed.entries)) {
31861
32731
  this.entries = parsed.entries.filter(
@@ -31866,8 +32736,8 @@ var init_councilFeedback = __esm({
31866
32736
  }
31867
32737
  }
31868
32738
  save() {
31869
- mkdirSync11(path26.dirname(this.file), { recursive: true });
31870
- writeFileSync16(
32739
+ mkdirSync12(path26.dirname(this.file), { recursive: true });
32740
+ writeFileSync17(
31871
32741
  this.file,
31872
32742
  JSON.stringify({ entries: this.entries }, null, 2),
31873
32743
  { encoding: "utf-8", mode: 384 }
@@ -31933,7 +32803,7 @@ var init_buildPolicy = __esm({
31933
32803
  import { execFile as execFile2 } from "node:child_process";
31934
32804
  import { promisify } from "node:util";
31935
32805
  import { mkdtempSync, rmSync } from "node:fs";
31936
- import { tmpdir } from "node:os";
32806
+ import { tmpdir as tmpdir2 } from "node:os";
31937
32807
  import path27 from "node:path";
31938
32808
  import { randomUUID as randomUUID3 } from "node:crypto";
31939
32809
  async function git(cwd, args, env) {
@@ -31954,7 +32824,7 @@ async function isGitRepo(cwd) {
31954
32824
  return await gitSafe(cwd, ["rev-parse", "--is-inside-work-tree"]) === "true";
31955
32825
  }
31956
32826
  async function withTempIndex(fn) {
31957
- const dir = mkdtempSync(path27.join(tmpdir(), "zelari-ckpt-"));
32827
+ const dir = mkdtempSync(path27.join(tmpdir2(), "zelari-ckpt-"));
31958
32828
  const indexFile = path27.join(dir, "index");
31959
32829
  try {
31960
32830
  return await fn(indexFile);
@@ -32828,7 +33698,7 @@ __export(prereqChecks_exports, {
32828
33698
  runPrereqChecks: () => runPrereqChecks
32829
33699
  });
32830
33700
  import { execSync, spawnSync as spawnSync2 } from "node:child_process";
32831
- import { existsSync as existsSync30 } from "node:fs";
33701
+ import { existsSync as existsSync32 } from "node:fs";
32832
33702
  import { dirname as dirname7 } from "node:path";
32833
33703
  function isWslBashPath2(p3) {
32834
33704
  if (!p3 || typeof p3 !== "string") return false;
@@ -32952,7 +33822,7 @@ function agentProbeEnv() {
32952
33822
  }
32953
33823
  function existsSyncSafe2(p3) {
32954
33824
  try {
32955
- return existsSync30(p3);
33825
+ return existsSync32(p3);
32956
33826
  } catch {
32957
33827
  return false;
32958
33828
  }
@@ -33199,7 +34069,7 @@ var init_prereqChecks = __esm({
33199
34069
  });
33200
34070
 
33201
34071
  // src/cli/plugins/prefs.ts
33202
- import { existsSync as existsSync31, readFileSync as readFileSync27, writeFileSync as writeFileSync17, mkdirSync as mkdirSync12 } from "node:fs";
34072
+ import { existsSync as existsSync33, readFileSync as readFileSync28, writeFileSync as writeFileSync18, mkdirSync as mkdirSync13 } from "node:fs";
33203
34073
  import path31 from "node:path";
33204
34074
  import os9 from "node:os";
33205
34075
  function getPluginPrefsPath() {
@@ -33208,8 +34078,8 @@ function getPluginPrefsPath() {
33208
34078
  function getPluginPrefs() {
33209
34079
  const file2 = getPluginPrefsPath();
33210
34080
  try {
33211
- if (!existsSync31(file2)) return { ...DEFAULTS2, dontAskAgain: {} };
33212
- const raw = readFileSync27(file2, "utf-8");
34081
+ if (!existsSync33(file2)) return { ...DEFAULTS2, dontAskAgain: {} };
34082
+ const raw = readFileSync28(file2, "utf-8");
33213
34083
  const parsed = JSON.parse(raw);
33214
34084
  if (parsed && typeof parsed === "object" && parsed.dontAskAgain && typeof parsed.dontAskAgain === "object") {
33215
34085
  const clean = {};
@@ -33224,8 +34094,8 @@ function getPluginPrefs() {
33224
34094
  }
33225
34095
  function writePluginPrefs(prefs) {
33226
34096
  const file2 = getPluginPrefsPath();
33227
- mkdirSync12(path31.dirname(file2), { recursive: true });
33228
- writeFileSync17(file2, JSON.stringify(prefs, null, 2), {
34097
+ mkdirSync13(path31.dirname(file2), { recursive: true });
34098
+ writeFileSync18(file2, JSON.stringify(prefs, null, 2), {
33229
34099
  encoding: "utf-8",
33230
34100
  mode: 384
33231
34101
  });
@@ -33260,7 +34130,7 @@ __export(registry_exports, {
33260
34130
  findPlugin: () => findPlugin,
33261
34131
  isBinaryOnPath: () => isBinaryOnPath
33262
34132
  });
33263
- import { existsSync as existsSync32 } from "node:fs";
34133
+ import { existsSync as existsSync34 } from "node:fs";
33264
34134
  import path32 from "node:path";
33265
34135
  function detectLocalBin(bin) {
33266
34136
  return (cwd) => {
@@ -33277,7 +34147,7 @@ function isBinaryOnPath(bin, opts = {}) {
33277
34147
  return false;
33278
34148
  }
33279
34149
  const platform = opts.platform ?? process.platform;
33280
- const exists = opts.exists ?? existsSync32;
34150
+ const exists = opts.exists ?? existsSync34;
33281
34151
  const pathEnv = opts.pathEnv ?? process.env.PATH ?? "";
33282
34152
  const pathMod = platform === "win32" ? path32.win32 : path32.posix;
33283
34153
  const sep = platform === "win32" ? ";" : ":";
@@ -33404,19 +34274,6 @@ var init_registry2 = __esm({
33404
34274
  detect: detectPathBin(binForLspLanguage("python")),
33405
34275
  featureGate: "ZELARI_LSP",
33406
34276
  description: "Powers go_to_definition / find_references / hover_type / rename_symbol for Python."
33407
- },
33408
- {
33409
- // fff — high-performance codebase search MCP (fffind / ffgrep).
33410
- // Installed as a global CLI; wire it in ~/.zelari-code/mcp.json (see
33411
- // postInstallHint). Kill-switch: ZELARI_FFF=0.
33412
- id: "fff",
33413
- label: "fff (fast codebase search MCP)",
33414
- npmPackage: "fff-mcp",
33415
- installScope: "global",
33416
- detect: detectPathBin("fff-mcp"),
33417
- postInstallHint: 'Add to ~/.zelari-code/mcp.json: {"mcpServers":{"fff":{"command":"fff-mcp","args":[]}}} then restart. Prefer mcp_fff_* tools for search.',
33418
- featureGate: "ZELARI_FFF",
33419
- description: "Accelerates codebase search via fff MCP (fffind, ffgrep, fff-multi-grep) \u2014 faster and more token-efficient than plain grep."
33420
34277
  }
33421
34278
  ];
33422
34279
  }
@@ -33428,7 +34285,7 @@ __export(doctor_exports, {
33428
34285
  runDoctor: () => runDoctor
33429
34286
  });
33430
34287
  import { execSync as execSync2 } from "node:child_process";
33431
- import { existsSync as existsSync37, readFileSync as readFileSync31, readlinkSync, statSync as statSync6 } from "node:fs";
34288
+ import { existsSync as existsSync38, readFileSync as readFileSync31, readlinkSync, statSync as statSync6 } from "node:fs";
33432
34289
  import { createRequire as createRequire3 } from "node:module";
33433
34290
  import { fileURLToPath as fileURLToPath2 } from "node:url";
33434
34291
  import path37 from "node:path";
@@ -33436,7 +34293,7 @@ function findPackageRoot(start) {
33436
34293
  let dir = start;
33437
34294
  for (let i = 0; i < 6; i += 1) {
33438
34295
  const candidate = path37.join(dir, "package.json");
33439
- if (existsSync37(candidate)) {
34296
+ if (existsSync38(candidate)) {
33440
34297
  try {
33441
34298
  const pkg = JSON.parse(readFileSync31(candidate, "utf8"));
33442
34299
  if (pkg.name === "zelari-code") return dir;
@@ -33478,7 +34335,7 @@ function checkShim(pkgName) {
33478
34335
  const isWin = process.platform === "win32";
33479
34336
  const shimName = isWin ? "zelari-code.cmd" : "zelari-code";
33480
34337
  const shimPath = path37.join(prefix, shimName);
33481
- if (!existsSync37(shimPath)) {
34338
+ if (!existsSync38(shimPath)) {
33482
34339
  return FAIL(
33483
34340
  `shim not found at ${shimPath}
33484
34341
  fix: npm install -g ${pkgName}@latest --force`
@@ -33546,7 +34403,7 @@ function checkNode(pkg) {
33546
34403
  }
33547
34404
  function checkBundle() {
33548
34405
  const bundle = path37.join(packageRoot, "dist", "cli", "main.bundled.js");
33549
- if (!existsSync37(bundle)) {
34406
+ if (!existsSync38(bundle)) {
33550
34407
  return FAIL(
33551
34408
  `dist/cli/main.bundled.js missing at ${bundle}
33552
34409
  fix: npm run build:cli (then reinstall or run via tsx)`
@@ -34443,7 +35300,8 @@ function StatusBar({
34443
35300
  cachedTokens = 0,
34444
35301
  cacheHitRate = 0,
34445
35302
  contextUsed = 0,
34446
- contextLimit = 0
35303
+ contextLimit = 0,
35304
+ todoSummary = null
34447
35305
  }) {
34448
35306
  const ctxLabel = contextLimit > 0 ? `${formatTokens(contextUsed)}/${formatTokens(contextLimit)}` : contextUsed > 0 ? formatTokens(contextUsed) : null;
34449
35307
  return /* @__PURE__ */ React6.createElement(Box5, { paddingX: 1, width: "100%", justifyContent: "space-between", gap: 2 }, /* @__PURE__ */ React6.createElement(Box5, { flexShrink: 2 }, /* @__PURE__ */ React6.createElement(Text6, { wrap: "truncate" }, /* @__PURE__ */ React6.createElement(Text6, { color: sessionActive ? "green" : "gray" }, sessionActive ? "\u25CF" : "\u25CB"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " "), /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: phase2 === "plan" ? "yellow" : "green" }, phase2 === "plan" ? "\u25C7 plan" : "\u25C6 build"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React6.createElement(
@@ -34453,7 +35311,7 @@ function StatusBar({
34453
35311
  color: mode === "council" ? "magenta" : mode === "zelari" ? "green" : "cyan"
34454
35312
  },
34455
35313
  mode === "council" ? "council" : mode === "zelari" ? "zelari" : "agent"
34456
- ), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, provider), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React6.createElement(Text6, null, model), cwd ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React6.createElement(Text6, { color: "blue" }, cwd)) : null)), /* @__PURE__ */ React6.createElement(Box5, { flexShrink: 1 }, /* @__PURE__ */ React6.createElement(Text6, { wrap: "truncate" }, busy && elapsedMs !== null ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Spinner, { color: "yellow" }), /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, " ", formatDuration(elapsedMs)), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 ")) : lastMs !== null ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "last ", formatDuration(lastMs)), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 ")) : null, queueCount > 0 ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { color: "magenta" }, "queue ", queueCount), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 ")) : null, ctxLabel ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan" }, ctxLabel), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 ")) : null, costUsd > 0 ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { color: "green" }, formatCost(costUsd)), cachedTokens > 0 ? /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (", formatTokens(cachedTokens), " cached)") : null, cacheHitRate > 0 ? /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " ", Math.round(cacheHitRate * 100), "% hit") : null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 ")) : null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "session ", sessionId))));
35314
+ ), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, provider), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React6.createElement(Text6, null, model), cwd ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React6.createElement(Text6, { color: "blue" }, cwd)) : null)), /* @__PURE__ */ React6.createElement(Box5, { flexShrink: 1 }, /* @__PURE__ */ React6.createElement(Text6, { wrap: "truncate" }, busy && elapsedMs !== null ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Spinner, { color: "yellow" }), /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, " ", formatDuration(elapsedMs)), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 ")) : lastMs !== null ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "last ", formatDuration(lastMs)), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 ")) : null, queueCount > 0 ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { color: "magenta" }, "queue ", queueCount), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 ")) : null, todoSummary ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, todoSummary), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 ")) : null, ctxLabel ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan" }, ctxLabel), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 ")) : null, costUsd > 0 ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { color: "green" }, formatCost(costUsd)), cachedTokens > 0 ? /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (", formatTokens(cachedTokens), " cached)") : null, cacheHitRate > 0 ? /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " ", Math.round(cacheHitRate * 100), "% hit") : null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 ")) : null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "session ", sessionId))));
34457
35315
  }
34458
35316
 
34459
35317
  // src/cli/components/SelectList.tsx
@@ -34887,6 +35745,7 @@ function useExecutionTimer(busy, tickMs = 1e3) {
34887
35745
 
34888
35746
  // src/cli/app.tsx
34889
35747
  init_paths();
35748
+ init_sessionTodos();
34890
35749
 
34891
35750
  // packages/core/dist/agents/skills/builtin/debugging.js
34892
35751
  init_skills();
@@ -37598,6 +38457,84 @@ async function resolveFailoverStream(options) {
37598
38457
  init_shellResolver();
37599
38458
  init_keyStore();
37600
38459
  init_toolRegistry();
38460
+
38461
+ // src/cli/hooks/permissionPicker.ts
38462
+ init_toolPermissions();
38463
+ function createPermissionAskHandler(opts) {
38464
+ const { setPicker: setPicker2, appendSystem: appendSystem2 } = opts;
38465
+ return (req) => new Promise((resolve) => {
38466
+ const cats = req.categories;
38467
+ const catLabel = cats.length === 1 ? cats[0] : cats.join("+") || "action";
38468
+ const title = `Allow tool "${req.toolName}"?`;
38469
+ const detail = req.reason + (cats.length ? ` [${cats.join(", ")}]` : "");
38470
+ appendSystem2?.(
38471
+ `[permission] ${title}
38472
+ ${detail}
38473
+ \u2192 Allow once \xB7 Always (tool) \xB7 Always (${catLabel}) \xB7 Deny`,
38474
+ Date.now()
38475
+ );
38476
+ let settled = false;
38477
+ const finish = (ok, note) => {
38478
+ if (settled) return;
38479
+ settled = true;
38480
+ setPicker2(null);
38481
+ if (note) appendSystem2?.(note, Date.now());
38482
+ resolve(ok);
38483
+ };
38484
+ const items = [
38485
+ { value: "allow", label: "Allow once" },
38486
+ {
38487
+ value: "always-tool",
38488
+ label: `Allow always this session \xB7 tool ${req.toolName}`
38489
+ }
38490
+ ];
38491
+ if (cats.length > 0) {
38492
+ items.push({
38493
+ value: "always-cat",
38494
+ label: `Allow always this session \xB7 ${catLabel}`
38495
+ });
38496
+ }
38497
+ items.push({ value: "deny", label: "Deny" });
38498
+ setPicker2({
38499
+ kind: "clarification",
38500
+ title,
38501
+ items,
38502
+ onAnswer: (value) => {
38503
+ const v = value.trim().toLowerCase();
38504
+ if (v === "deny" || v.startsWith("deny")) {
38505
+ finish(false);
38506
+ return;
38507
+ }
38508
+ if (v === "always-tool" || v.includes("always") && v.includes("tool")) {
38509
+ grantSessionTool(req.toolName);
38510
+ finish(
38511
+ true,
38512
+ `[permission] Granted "${req.toolName}" for this session (tool).`
38513
+ );
38514
+ return;
38515
+ }
38516
+ if (v === "always-cat" || v.includes("always") && !v.includes("tool")) {
38517
+ for (const c of cats) grantSessionCategory(c);
38518
+ grantSessionTool(req.toolName);
38519
+ finish(
38520
+ true,
38521
+ `[permission] Granted ${catLabel} (+ ${req.toolName}) for this session.`
38522
+ );
38523
+ return;
38524
+ }
38525
+ if (v === "allow" || v.startsWith("allow") || v === "yes" || v === "y" || v === "1") {
38526
+ finish(true);
38527
+ return;
38528
+ }
38529
+ finish(false);
38530
+ },
38531
+ onCancel: () => finish(false)
38532
+ });
38533
+ });
38534
+ }
38535
+
38536
+ // src/cli/hooks/useChatTurn.ts
38537
+ init_toolPermissions();
37601
38538
  init_skills2();
37602
38539
  init_fileStateStore();
37603
38540
  init_dist();
@@ -37806,39 +38743,69 @@ function resolveContextLimit() {
37806
38743
  max: 2e6
37807
38744
  });
37808
38745
  }
37809
- function applyBudgetPolicy(history2, phase2, opts) {
38746
+ function phaseKnobs(phase2) {
38747
+ return {
38748
+ historyTurns: phase2 === "plan" ? envNumber(process.env.ZELARI_HISTORY_TURNS, { default: 8, min: 0 }) : envNumber(process.env.ZELARI_HISTORY_TURNS, { default: 6, min: 0 }),
38749
+ maxToolLoopIterations: phase2 === "plan" ? envNumber(process.env.ZELARI_MAX_TOOL_LOOP_ITERATIONS, { default: 60, min: 1 }) : envNumber(process.env.ZELARI_MAX_TOOL_LOOP_ITERATIONS, {
38750
+ default: 120,
38751
+ min: 1
38752
+ })
38753
+ };
38754
+ }
38755
+ function occupancyOf(hist, sessionExtra, contextLimit) {
38756
+ const estimated = estimateHistoryTokens(hist);
38757
+ const occupancy = Math.min(1, (estimated + sessionExtra) / contextLimit);
38758
+ return { estimated, occupancy };
38759
+ }
38760
+ async function applyBudgetPolicyAsync(history2, phase2, opts) {
37810
38761
  const contextLimit = resolveContextLimit();
38762
+ const sessionExtra = opts?.sessionTokens ?? 0;
37811
38763
  const warnings = [];
37812
- let historyTurns = phase2 === "plan" ? envNumber(process.env.ZELARI_HISTORY_TURNS, { default: 8, min: 0 }) : envNumber(process.env.ZELARI_HISTORY_TURNS, { default: 6, min: 0 });
37813
- let maxToolLoopIterations = phase2 === "plan" ? envNumber(process.env.ZELARI_MAX_TOOL_LOOP_ITERATIONS, { default: 60, min: 1 }) : envNumber(process.env.ZELARI_MAX_TOOL_LOOP_ITERATIONS, { default: 120, min: 1 });
38764
+ let { historyTurns, maxToolLoopIterations } = phaseKnobs(phase2);
37814
38765
  let hist = history2;
37815
- let estimated = estimateHistoryTokens(hist);
37816
- const sessionExtra = opts?.sessionTokens ?? 0;
37817
- let occupancy = Math.min(1, (estimated + sessionExtra) / contextLimit);
38766
+ let { estimated, occupancy } = occupancyOf(hist, sessionExtra, contextLimit);
38767
+ let compactSummary = "";
38768
+ let messagesRemoved = 0;
37818
38769
  if (occupancy >= 0.7 && occupancy < 0.85) {
37819
38770
  warnings.push(
37820
38771
  `[budget] context ~${Math.round(occupancy * 100)}% full (${estimated + sessionExtra}/${contextLimit} tok est.) \u2014 consider /compact or shorter replies.`
37821
38772
  );
37822
38773
  }
38774
+ const fold = (r, label, forcedTurns) => {
38775
+ hist = r.messages;
38776
+ if (r.compacted) {
38777
+ messagesRemoved += r.messagesRemoved;
38778
+ if (r.summary) compactSummary = r.summary;
38779
+ }
38780
+ ({ estimated, occupancy } = occupancyOf(hist, sessionExtra, contextLimit));
38781
+ warnings.push(
38782
+ `[budget] ${label} at ${forcedTurns === 2 ? "95%" : "85%"} \u2014 kept ~${forcedTurns} turns (${estimated} tok history est.` + (r.messagesRemoved ? `, removed ${r.messagesRemoved} msgs` : "") + `).`
38783
+ );
38784
+ };
37823
38785
  if (occupancy >= 0.85) {
37824
38786
  const forcedTurns = Math.max(1, Math.floor(historyTurns / 2));
37825
38787
  historyTurns = forcedTurns;
37826
- hist = compactHistory(hist, { maxMessages: forcedTurns * 4 });
37827
- estimated = estimateHistoryTokens(hist);
37828
- occupancy = Math.min(1, (estimated + sessionExtra) / contextLimit);
37829
- warnings.push(
37830
- `[budget] auto-compact at 85% \u2014 kept ~${forcedTurns} turns (${estimated} tok history est.).`
38788
+ maxToolLoopIterations = Math.min(
38789
+ maxToolLoopIterations,
38790
+ phase2 === "plan" ? 24 : 40
37831
38791
  );
37832
- maxToolLoopIterations = Math.min(maxToolLoopIterations, phase2 === "plan" ? 24 : 40);
38792
+ const r = await compactHistoryAsync(hist, {
38793
+ maxMessages: forcedTurns * 4,
38794
+ signal: opts?.signal
38795
+ });
38796
+ const label = r.summary.includes("\xB7 llm") ? "llm-compact" : "auto-compact";
38797
+ fold(r, label, forcedTurns);
37833
38798
  }
37834
38799
  if (occupancy >= 0.95) {
37835
- hist = compactHistory(hist, { maxMessages: 8 });
37836
- estimated = estimateHistoryTokens(hist);
37837
- occupancy = Math.min(1, (estimated + sessionExtra) / contextLimit);
38800
+ const hard = await compactHistoryAsync(hist, {
38801
+ maxMessages: 8,
38802
+ signal: opts?.signal
38803
+ });
38804
+ fold(hard, hard.summary.includes("\xB7 llm") ? "llm-compact" : "auto-compact", 2);
37838
38805
  historyTurns = 2;
37839
38806
  maxToolLoopIterations = Math.min(maxToolLoopIterations, 16);
37840
38807
  warnings.push(
37841
- `[budget] HARD context pressure (\u226595%) \u2014 history cut to last ~2 turns. Prefer /clear or a new session if quality drops.`
38808
+ `[budget] HARD context pressure (\u226595%) \u2014 prefer /clear or a new session if quality drops.`
37842
38809
  );
37843
38810
  }
37844
38811
  return {
@@ -37848,7 +38815,9 @@ function applyBudgetPolicy(history2, phase2, opts) {
37848
38815
  historyTurns,
37849
38816
  estimatedHistoryTokens: estimated,
37850
38817
  contextLimit,
37851
- occupancy
38818
+ occupancy,
38819
+ compactSummary: compactSummary || void 0,
38820
+ messagesRemoved: messagesRemoved || void 0
37852
38821
  };
37853
38822
  }
37854
38823
 
@@ -37865,7 +38834,7 @@ function useChatTurn(params) {
37865
38834
  setSessionStats,
37866
38835
  setLive,
37867
38836
  liveRef,
37868
- setPicker
38837
+ setPicker: setPicker2
37869
38838
  } = params;
37870
38839
  const harnessRef = useRef4(null);
37871
38840
  const [queueCount, setQueueCount] = useState6(0);
@@ -37881,7 +38850,7 @@ function useChatTurn(params) {
37881
38850
  let turnSucceeded = false;
37882
38851
  try {
37883
38852
  compactInPlace();
37884
- const budget = applyBudgetPolicy(getHistory(), getPhase());
38853
+ const budget = await applyBudgetPolicyAsync(getHistory(), getPhase());
37885
38854
  setHistory(budget.history);
37886
38855
  for (const w of budget.warnings) {
37887
38856
  appendSystem(setMessages, w, Date.now());
@@ -37901,7 +38870,7 @@ function useChatTurn(params) {
37901
38870
  }
37902
38871
  setBusy(true);
37903
38872
  const workPhase = getPhase();
37904
- const onAskUser = setPicker ? (req) => new Promise((resolve) => {
38873
+ const onAskUser = setPicker2 ? (req) => new Promise((resolve) => {
37905
38874
  const choices = req.choices ?? [];
37906
38875
  if (choices.length < 2) {
37907
38876
  resolve(null);
@@ -37924,10 +38893,10 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il turno continua dopo
37924
38893
  const finish = (value) => {
37925
38894
  if (settled) return;
37926
38895
  settled = true;
37927
- setPicker(null);
38896
+ setPicker2(null);
37928
38897
  resolve(value);
37929
38898
  };
37930
- setPicker({
38899
+ setPicker2({
37931
38900
  kind: "clarification",
37932
38901
  title: req.question,
37933
38902
  items: choices.map((c) => ({ value: c, label: c })),
@@ -37935,9 +38904,15 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il turno continua dopo
37935
38904
  onCancel: () => finish(null)
37936
38905
  });
37937
38906
  }) : void 0;
38907
+ const onPermissionAsk = setPicker2 ? createPermissionAskHandler({
38908
+ setPicker: setPicker2,
38909
+ appendSystem: (msg, at) => appendSystem(setMessages, msg, at ?? Date.now())
38910
+ }) : void 0;
37938
38911
  const { registry: toolRegistry } = createBuiltinToolRegistry({
37939
38912
  planMode: workPhase === "plan",
37940
- onAskUser
38913
+ onAskUser,
38914
+ onPermissionAsk,
38915
+ permissionPolicy: defaultPermissionPolicy()
37941
38916
  });
37942
38917
  const baseProviderStream = openaiCompatibleProvider(envConfig);
37943
38918
  const failoverResolution = await resolveFailoverStream({
@@ -38334,8 +39309,8 @@ ${choiceLines}` + (clar.context ? `
38334
39309
  _(${clar.context})_` : "") + "\n\u2192 scegli dalla lista oppure digita la risposta e invia.",
38335
39310
  Date.now()
38336
39311
  );
38337
- if (setPicker) {
38338
- setPicker({
39312
+ if (setPicker2) {
39313
+ setPicker2({
38339
39314
  kind: "clarification",
38340
39315
  title: clar.question,
38341
39316
  items: clar.choices.map((c) => ({ value: c, label: c })),
@@ -38406,7 +39381,7 @@ _(${clar.context})_` : "") + "\n\u2192 scegli dalla lista oppure digita la rispo
38406
39381
  setQueueCount,
38407
39382
  setLive,
38408
39383
  liveRef,
38409
- setPicker
39384
+ setPicker: setPicker2
38410
39385
  });
38411
39386
  },
38412
39387
  [
@@ -38419,7 +39394,7 @@ _(${clar.context})_` : "") + "\n\u2192 scegli dalla lista oppure digita la rispo
38419
39394
  setQueueCount,
38420
39395
  setLive,
38421
39396
  liveRef,
38422
- setPicker
39397
+ setPicker2
38423
39398
  ]
38424
39399
  );
38425
39400
  const pendingZelariRef = useRef4(null);
@@ -38473,7 +39448,7 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
38473
39448
  setBusy,
38474
39449
  setLive,
38475
39450
  liveRef,
38476
- setPicker
39451
+ setPicker: setPicker2
38477
39452
  } = deps;
38478
39453
  const useLiveModel = !!(setLive && liveRef);
38479
39454
  const envConfig = await providerFromEnv();
@@ -38488,7 +39463,7 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
38488
39463
  }
38489
39464
  setBusy(true);
38490
39465
  compactInPlace();
38491
- const councilBudget = applyBudgetPolicy(getHistory(), getPhase());
39466
+ const councilBudget = await applyBudgetPolicyAsync(getHistory(), getPhase());
38492
39467
  setHistory(councilBudget.history);
38493
39468
  for (const w of councilBudget.warnings) {
38494
39469
  appendSystem(setMessages, w, Date.now());
@@ -38508,7 +39483,7 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
38508
39483
  const { composeProjectContext: composeProjectContext2 } = await Promise.resolve().then(() => (init_composeContext(), composeContext_exports));
38509
39484
  const { FeedbackStore: FeedbackStore2 } = await Promise.resolve().then(() => (init_councilFeedback(), councilFeedback_exports));
38510
39485
  const workPhase = getPhase();
38511
- const onAskUserCouncil = setPicker ? (req) => new Promise((resolve) => {
39486
+ const onAskUserCouncil = setPicker2 ? (req) => new Promise((resolve) => {
38512
39487
  const choices = req.choices ?? [];
38513
39488
  if (choices.length < 2) {
38514
39489
  resolve(null);
@@ -38531,10 +39506,10 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il membro riprende dop
38531
39506
  const finish = (value) => {
38532
39507
  if (settled) return;
38533
39508
  settled = true;
38534
- setPicker(null);
39509
+ setPicker2(null);
38535
39510
  resolve(value);
38536
39511
  };
38537
- setPicker({
39512
+ setPicker2({
38538
39513
  kind: "clarification",
38539
39514
  title: req.question,
38540
39515
  items: choices.map((c) => ({ value: c, label: c })),
@@ -38557,9 +39532,15 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il membro riprende dop
38557
39532
  );
38558
39533
  }
38559
39534
  }
39535
+ const onPermissionAskCouncil = setPicker2 ? createPermissionAskHandler({
39536
+ setPicker: setPicker2,
39537
+ appendSystem: (msg, at) => appendSystem(setMessages, msg, at ?? Date.now())
39538
+ }) : void 0;
38560
39539
  const { registry: councilToolRegistry } = createBuiltinToolRegistry({
38561
39540
  planMode: workPhase === "plan" || softGatedToDesign,
38562
- onAskUser: onAskUserCouncil
39541
+ onAskUser: onAskUserCouncil,
39542
+ onPermissionAsk: onPermissionAskCouncil,
39543
+ permissionPolicy: defaultPermissionPolicy()
38563
39544
  });
38564
39545
  const workspaceCtx = createWorkspaceContext2();
38565
39546
  const workspaceReg = createWorkspaceToolRegistry2(workspaceCtx);
@@ -38655,7 +39636,7 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il membro riprende dop
38655
39636
  appendSystem(setMessages, message, Date.now());
38656
39637
  },
38657
39638
  // v1.8.0: pause council when a member asks a structured question.
38658
- onClarification: setPicker ? (req) => new Promise((resolve) => {
39639
+ onClarification: setPicker2 ? (req) => new Promise((resolve) => {
38659
39640
  const choices = req.choices ?? [];
38660
39641
  if (choices.length < 2) {
38661
39642
  resolve(null);
@@ -38678,10 +39659,10 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il membro riprende dop
38678
39659
  const finish = (value) => {
38679
39660
  if (settled) return;
38680
39661
  settled = true;
38681
- setPicker(null);
39662
+ setPicker2(null);
38682
39663
  resolve(value);
38683
39664
  };
38684
- setPicker({
39665
+ setPicker2({
38685
39666
  kind: "clarification",
38686
39667
  title: req.question,
38687
39668
  items: choices.map((c) => ({ value: c, label: c })),
@@ -39127,8 +40108,14 @@ async function runZelariMissionInTui(userMessage, deps, emit) {
39127
40108
  );
39128
40109
  return { completionOk: false, ran: false };
39129
40110
  }
40111
+ const onPermissionAskZelari = setPicker ? createPermissionAskHandler({
40112
+ setPicker,
40113
+ appendSystem: (msg, at) => appendSystem(setMessages2, msg, at ?? Date.now())
40114
+ }) : void 0;
39130
40115
  const { registry: toolRegistry } = createBuiltinToolRegistry({
39131
- planMode: false
40116
+ planMode: false,
40117
+ onPermissionAsk: onPermissionAskZelari,
40118
+ permissionPolicy: defaultPermissionPolicy()
39132
40119
  });
39133
40120
  try {
39134
40121
  const { registerMcpTools: registerMcpTools2 } = await Promise.resolve().then(() => (init_mcpManager(), mcpManager_exports));
@@ -40544,7 +41531,7 @@ async function handlePromoteMember(ctx, memberId) {
40544
41531
  }
40545
41532
 
40546
41533
  // src/cli/branchManager.ts
40547
- import { promises as fs19, existsSync as existsSync33, readFileSync as readFileSync28, writeFileSync as writeFileSync18, mkdirSync as mkdirSync13, statSync as statSync4, rmSync as rmSync2 } from "node:fs";
41534
+ import { promises as fs19, existsSync as existsSync35, readFileSync as readFileSync29, writeFileSync as writeFileSync19, mkdirSync as mkdirSync14, statSync as statSync4, rmSync as rmSync2 } from "node:fs";
40548
41535
  import path34 from "node:path";
40549
41536
  import os11 from "node:os";
40550
41537
  var META_FILENAME = "meta.json";
@@ -40566,11 +41553,11 @@ function sessionsPathFor(name, baseDir) {
40566
41553
  }
40567
41554
  function readBranchMeta(name, baseDir) {
40568
41555
  const metaPath = metaPathFor(name, baseDir);
40569
- if (!existsSync33(metaPath)) {
41556
+ if (!existsSync35(metaPath)) {
40570
41557
  throw new BranchNotFoundError(`Branch "${name}" not found`);
40571
41558
  }
40572
41559
  try {
40573
- const raw = readFileSync28(metaPath, "utf-8");
41560
+ const raw = readFileSync29(metaPath, "utf-8");
40574
41561
  const parsed = JSON.parse(raw);
40575
41562
  if (!parsed || typeof parsed !== "object" || typeof parsed.name !== "string" || typeof parsed.createdAt !== "number" || typeof parsed.fromSessionId !== "string") {
40576
41563
  throw new BranchCorruptError(`Branch "${name}" meta.json is malformed`);
@@ -40587,8 +41574,8 @@ function readBranchMeta(name, baseDir) {
40587
41574
  }
40588
41575
  function writeBranchMeta(name, baseDir, meta3) {
40589
41576
  const metaPath = metaPathFor(name, baseDir);
40590
- mkdirSync13(path34.dirname(metaPath), { recursive: true });
40591
- writeFileSync18(metaPath, JSON.stringify(meta3, null, 2), "utf-8");
41577
+ mkdirSync14(path34.dirname(metaPath), { recursive: true });
41578
+ writeFileSync19(metaPath, JSON.stringify(meta3, null, 2), "utf-8");
40592
41579
  }
40593
41580
  async function countSessions(name, baseDir) {
40594
41581
  const sessionsPath = sessionsPathFor(name, baseDir);
@@ -40626,7 +41613,7 @@ var SessionNotFoundError = class extends Error {
40626
41613
  };
40627
41614
  function branchExists(name, baseDir = getBranchesBaseDir()) {
40628
41615
  const bp = branchPathFor(name, baseDir);
40629
- return existsSync33(bp) && existsSync33(metaPathFor(name, baseDir));
41616
+ return existsSync35(bp) && existsSync35(metaPathFor(name, baseDir));
40630
41617
  }
40631
41618
  async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(), sessionsBaseDir = getSessionsBaseDir()) {
40632
41619
  if (!name || name.trim().length === 0) {
@@ -40639,12 +41626,12 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
40639
41626
  throw new BranchAlreadyExistsError(name);
40640
41627
  }
40641
41628
  const sourcePath = path34.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
40642
- if (!existsSync33(sourcePath)) {
41629
+ if (!existsSync35(sourcePath)) {
40643
41630
  throw new SessionNotFoundError(`Source session "${fromSessionId}" not found at ${sourcePath}`);
40644
41631
  }
40645
41632
  const branchPath = branchPathFor(name, baseDir);
40646
41633
  const branchSessionsPath = sessionsPathFor(name, baseDir);
40647
- mkdirSync13(branchSessionsPath, { recursive: true });
41634
+ mkdirSync14(branchSessionsPath, { recursive: true });
40648
41635
  const destPath = path34.join(branchSessionsPath, `${fromSessionId}.jsonl`);
40649
41636
  await fs19.copyFile(sourcePath, destPath);
40650
41637
  const meta3 = {
@@ -40672,7 +41659,7 @@ async function listBranches(baseDir = getBranchesBaseDir()) {
40672
41659
  const results = [];
40673
41660
  for (const entry of entries) {
40674
41661
  const metaPath = metaPathFor(entry, baseDir);
40675
- if (!existsSync33(metaPath)) continue;
41662
+ if (!existsSync35(metaPath)) continue;
40676
41663
  try {
40677
41664
  const meta3 = readBranchMeta(entry, baseDir);
40678
41665
  const sessionCount = await countSessions(entry, baseDir);
@@ -41208,7 +42195,7 @@ import path36 from "node:path";
41208
42195
  import os12 from "node:os";
41209
42196
 
41210
42197
  // src/cli/skillHistory.ts
41211
- import { promises as fs21, existsSync as existsSync34, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync3, mkdirSync as mkdirSync14 } from "node:fs";
42198
+ import { promises as fs21, existsSync as existsSync36, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync3, mkdirSync as mkdirSync15 } from "node:fs";
41212
42199
  var SKILL_HISTORY_ROTATE_BYTES = 10 * 1024 * 1024;
41213
42200
  async function readSkillHistory(file2) {
41214
42201
  let raw = "";
@@ -41960,7 +42947,7 @@ function App() {
41960
42947
  const [clearEpoch, setClearEpoch] = useState8(0);
41961
42948
  const [mode, setMode] = useState8("agent");
41962
42949
  const [phase2, setPhaseUi] = useState8("build");
41963
- const [picker, setPicker] = useState8(null);
42950
+ const [picker, setPicker2] = useState8(null);
41964
42951
  const activeProviderSpec = getActiveProvider();
41965
42952
  const activeModel = providerConfig.modelByProvider[activeProviderSpec.id];
41966
42953
  const session = useSession();
@@ -42001,7 +42988,7 @@ function App() {
42001
42988
  // v1.6.0: lets dispatchPrompt open a SelectList when the agent poses a
42002
42989
  // ---QUESTION--- clarifying block, so the user picks from the offered
42003
42990
  // choices instead of typing (and the answer binds via rolling history).
42004
- setPicker
42991
+ setPicker: setPicker2
42005
42992
  });
42006
42993
  const onNewSession = useCallback5((id) => {
42007
42994
  void session.writerRef.current?.close();
@@ -42035,7 +43022,7 @@ function App() {
42035
43022
  dispatchZelariPrompt: chatTurn.dispatchZelariPrompt,
42036
43023
  mode,
42037
43024
  setMode,
42038
- openPicker: setPicker,
43025
+ openPicker: setPicker2,
42039
43026
  onNewSession,
42040
43027
  onExit,
42041
43028
  onClear,
@@ -42044,7 +43031,7 @@ function App() {
42044
43031
  });
42045
43032
  const onPickerSelect = useCallback5((value) => {
42046
43033
  if (!picker) return;
42047
- setPicker(null);
43034
+ setPicker2(null);
42048
43035
  if (picker.kind === "clarification") {
42049
43036
  picker.onAnswer?.(value);
42050
43037
  return;
@@ -42056,7 +43043,7 @@ function App() {
42056
43043
  if (picker?.kind === "clarification") {
42057
43044
  picker.onCancel?.();
42058
43045
  }
42059
- setPicker(null);
43046
+ setPicker2(null);
42060
43047
  }, [picker]);
42061
43048
  const banner = useMemo(() => {
42062
43049
  return {
@@ -42118,7 +43105,8 @@ function App() {
42118
43105
  cachedTokens: sessionStats.cachedTokens,
42119
43106
  cacheHitRate: sessionStats.cacheHitRate || 0,
42120
43107
  contextUsed: sessionStats.contextTokens || 0,
42121
- contextLimit: Number(process.env.ZELARI_CONTEXT_LIMIT) || 2e5
43108
+ contextLimit: Number(process.env.ZELARI_CONTEXT_LIMIT) || 2e5,
43109
+ todoSummary: formatTodoStatusSummary()
42122
43110
  }
42123
43111
  )), sidebarOpen && /* @__PURE__ */ React10.createElement(Sidebar, { version: VERSION, changes: gitChanges, rows: size.rows })));
42124
43112
  }
@@ -42434,9 +43422,9 @@ function ContinueKey({ onContinue }) {
42434
43422
  init_providerConfig();
42435
43423
 
42436
43424
  // src/cli/wizard/firstRun.ts
42437
- import { existsSync as existsSync35 } from "node:fs";
43425
+ import { existsSync as existsSync37 } from "node:fs";
42438
43426
  function shouldRunWizard(input) {
42439
- const exists = input.exists ?? existsSync35;
43427
+ const exists = input.exists ?? existsSync37;
42440
43428
  if (input.hasResetConfigFlag) {
42441
43429
  return { shouldRun: true, reason: "--reset-config flag forced wizard" };
42442
43430
  }
@@ -42721,7 +43709,7 @@ init_keyStore();
42721
43709
  init_providerConfig();
42722
43710
  init_openai_compatible();
42723
43711
  init_phase();
42724
- import { readFileSync as readFileSync29 } from "node:fs";
43712
+ import { readFileSync as readFileSync30 } from "node:fs";
42725
43713
  function parseHeadlessFlags(argv) {
42726
43714
  if (!argv.includes("--headless")) {
42727
43715
  return { options: null };
@@ -42789,7 +43777,7 @@ function parseHeadlessFlags(argv) {
42789
43777
  let raw = null;
42790
43778
  if (arg === "--history-file") {
42791
43779
  try {
42792
- raw = readFileSync29(next, "utf-8");
43780
+ raw = readFileSync30(next, "utf-8");
42793
43781
  } catch {
42794
43782
  raw = null;
42795
43783
  }
@@ -43019,7 +44007,15 @@ async function registerHeadlessMcp(toolRegistry, opts) {
43019
44007
  async function runHeadlessSingle(opts, provider, model, providerStream) {
43020
44008
  const sessionId = crypto.randomUUID();
43021
44009
  const { registry: toolRegistry } = createBuiltinToolRegistry({
43022
- planMode: planModeFromOpts(opts)
44010
+ planMode: planModeFromOpts(opts),
44011
+ permissionPolicy: {
44012
+ read: "allow",
44013
+ write: "allow",
44014
+ execute: "allow",
44015
+ network: "allow",
44016
+ ui: "allow",
44017
+ auto: true
44018
+ }
43023
44019
  });
43024
44020
  await registerHeadlessMcp(toolRegistry, opts);
43025
44021
  const tools = toolRegistry.toOpenAITools().map((t) => ({
@@ -43324,7 +44320,17 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
43324
44320
  return pass.exitCode;
43325
44321
  }
43326
44322
  async function buildCouncilToolRegistry(planMode, opts) {
43327
- const { registry: toolRegistry } = createBuiltinToolRegistry({ planMode });
44323
+ const { registry: toolRegistry } = createBuiltinToolRegistry({
44324
+ planMode,
44325
+ permissionPolicy: {
44326
+ read: "allow",
44327
+ write: "allow",
44328
+ execute: "allow",
44329
+ network: "allow",
44330
+ ui: "allow",
44331
+ auto: true
44332
+ }
44333
+ });
43328
44334
  const { createWorkspaceContext: createWorkspaceContext2, createWorkspaceStubs: createWorkspaceStubs2 } = await Promise.resolve().then(() => (init_stubs(), stubs_exports));
43329
44335
  const { createWorkspaceToolRegistry: createWorkspaceToolRegistry2 } = await Promise.resolve().then(() => (init_toolRegistry2(), toolRegistry_exports2));
43330
44336
  const { setWorkspaceStubs: setWorkspaceStubs2 } = await Promise.resolve().then(() => (init_skills2(), skills_exports));
@@ -43646,7 +44652,15 @@ ${ragContext}` : slicePrompt;
43646
44652
  const { loadDurableContext: loadDurableContext2 } = await Promise.resolve().then(() => (init_loadDurableContext(), loadDurableContext_exports));
43647
44653
  const { detectDegradedRun: detectDegradedRun2 } = await Promise.resolve().then(() => (init_council(), council_exports));
43648
44654
  const { registry: agentRegistry } = createBuiltinToolRegistry2({
43649
- planMode: false
44655
+ planMode: false,
44656
+ permissionPolicy: {
44657
+ read: "allow",
44658
+ write: "allow",
44659
+ execute: "allow",
44660
+ network: "allow",
44661
+ ui: "allow",
44662
+ auto: true
44663
+ }
43650
44664
  });
43651
44665
  await registerHeadlessMcp(agentRegistry, opts);
43652
44666
  const durableState = await loadDurableContext2(projectRoot);
@@ -44074,118 +45088,8 @@ async function runPluginsInstall(argv) {
44074
45088
  return 0;
44075
45089
  }
44076
45090
 
44077
- // src/cli/skillsMd.ts
44078
- init_skills2();
44079
- import { existsSync as existsSync36, readdirSync as readdirSync5, readFileSync as readFileSync30 } from "node:fs";
44080
- import { join as join29 } from "node:path";
44081
- import { homedir as homedir8 } from "node:os";
44082
- var CODING_CATEGORIES = /* @__PURE__ */ new Set([
44083
- "plan",
44084
- "refactor",
44085
- "debug",
44086
- "review",
44087
- "test",
44088
- "docs",
44089
- "ops",
44090
- "git",
44091
- "db",
44092
- "maint"
44093
- ]);
44094
- function skillMdSearchDirs(projectRoot = process.cwd()) {
44095
- return [
44096
- join29(projectRoot, ".zelari", "skills"),
44097
- join29(projectRoot, ".claude", "skills"),
44098
- join29(projectRoot, ".opencode", "skills"),
44099
- join29(homedir8(), ".zelari-code", "skills")
44100
- ];
44101
- }
44102
- function parseSkillMd(content, sourcePath) {
44103
- const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(content);
44104
- if (!fmMatch) return null;
44105
- const [, fmRaw = "", body = ""] = fmMatch;
44106
- const fields = {};
44107
- for (const line of fmRaw.split(/\r?\n/)) {
44108
- const kv = /^([A-Za-z_][\w-]*)\s*:\s*(.*)$/.exec(line);
44109
- if (!kv) continue;
44110
- const key = (kv[1] ?? "").toLowerCase();
44111
- let value = (kv[2] ?? "").trim();
44112
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
44113
- value = value.slice(1, -1);
44114
- }
44115
- fields[key] = value;
44116
- }
44117
- const name = (fields["name"] ?? "").trim().toLowerCase();
44118
- const description = (fields["description"] ?? "").trim();
44119
- if (!name || !description) return null;
44120
- if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(name)) return null;
44121
- const rawTools = fields["tools"] ?? fields["requiredtools"] ?? fields["required_tools"] ?? "";
44122
- const requiredTools = rawTools.replace(/^\[|\]$/g, "").split(",").map((t) => t.trim().replace(/^["']|["']$/g, "")).filter((t) => t.length > 0);
44123
- const rawCategory = (fields["category"] ?? "").trim().toLowerCase();
44124
- const category = CODING_CATEGORIES.has(rawCategory) ? rawCategory : "maint";
44125
- const rawCost = (fields["cost"] ?? fields["estimatedcost"] ?? "").trim().toLowerCase();
44126
- const estimatedCost = rawCost === "low" || rawCost === "high" ? rawCost : "medium";
44127
- const trimmedBody = body.trim();
44128
- if (!trimmedBody) return null;
44129
- return { name, description, category, requiredTools, estimatedCost, body: trimmedBody, sourcePath };
44130
- }
44131
- function toCodingSkillDefinition(parsed) {
44132
- return {
44133
- id: parsed.name,
44134
- name: parsed.name,
44135
- description: parsed.description,
44136
- version: "1.0.0",
44137
- category: parsed.category,
44138
- systemPromptFragment: parsed.body,
44139
- requiredTools: parsed.requiredTools,
44140
- enabledByDefault: true,
44141
- builtin: false,
44142
- requires: [],
44143
- examples: [],
44144
- triggers: [],
44145
- antiPatterns: [],
44146
- requiredRoles: [],
44147
- estimatedCost: parsed.estimatedCost,
44148
- outputSchema: "string",
44149
- relatedSkills: [],
44150
- tags: ["user", "skill-md"]
44151
- };
44152
- }
44153
- function loadSkillMdSkills(projectRoot = process.cwd(), options = {}) {
44154
- const summary = { loaded: [], skipped: [] };
44155
- const seen = new Set(options.existingIds ?? []);
44156
- for (const dir of skillMdSearchDirs(projectRoot)) {
44157
- if (!existsSync36(dir)) continue;
44158
- let entries;
44159
- try {
44160
- entries = readdirSync5(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
44161
- } catch {
44162
- continue;
44163
- }
44164
- for (const entry of entries) {
44165
- const skillPath = join29(dir, entry, "SKILL.md");
44166
- if (!existsSync36(skillPath)) continue;
44167
- try {
44168
- const parsed = parseSkillMd(readFileSync30(skillPath, "utf8"), skillPath);
44169
- if (!parsed) {
44170
- summary.skipped.push({ path: skillPath, reason: "missing/invalid frontmatter (name, description) or empty body" });
44171
- continue;
44172
- }
44173
- if (seen.has(parsed.name)) {
44174
- summary.skipped.push({ path: skillPath, reason: `name "${parsed.name}" already registered (earlier dir or builtin wins)` });
44175
- continue;
44176
- }
44177
- registerCodingSkill(toCodingSkillDefinition(parsed));
44178
- seen.add(parsed.name);
44179
- summary.loaded.push(parsed.name);
44180
- } catch (err) {
44181
- summary.skipped.push({ path: skillPath, reason: err instanceof Error ? err.message : String(err) });
44182
- }
44183
- }
44184
- }
44185
- return summary;
44186
- }
44187
-
44188
45091
  // src/cli/main.ts
45092
+ init_skillsMd();
44189
45093
  init_skills2();
44190
45094
  init_updater();
44191
45095
  init_mcpConfigIo();
@@ -44204,8 +45108,8 @@ function runPreflight() {
44204
45108
  mode: "preflight"
44205
45109
  });
44206
45110
  for (const w of warnings) {
44207
- const oneLine = w.message.replace(/\s*\n\s*/g, " ").replace(/\s+/g, " ").trim();
44208
- const short = oneLine.length > 120 ? `${oneLine.slice(0, 117)}\u2026` : oneLine;
45111
+ const oneLine2 = w.message.replace(/\s*\n\s*/g, " ").replace(/\s+/g, " ").trim();
45112
+ const short = oneLine2.length > 120 ? `${oneLine2.slice(0, 117)}\u2026` : oneLine2;
44209
45113
  console.error(`\x1B[33m[zelari-code] \u26A0 ${w.tool}: ${short}\x1B[0m`);
44210
45114
  }
44211
45115
  if (hasCriticalFail) {