zelari-code 1.7.1 → 1.8.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 (36) hide show
  1. package/dist/cli/app.js +18 -4
  2. package/dist/cli/app.js.map +1 -1
  3. package/dist/cli/browser/driver.js +58 -10
  4. package/dist/cli/browser/driver.js.map +1 -1
  5. package/dist/cli/browser/tools.js +4 -1
  6. package/dist/cli/browser/tools.js.map +1 -1
  7. package/dist/cli/budget/tokenBudget.js +92 -0
  8. package/dist/cli/budget/tokenBudget.js.map +1 -0
  9. package/dist/cli/components/PluginGate.js +24 -1
  10. package/dist/cli/components/PluginGate.js.map +1 -1
  11. package/dist/cli/components/Sidebar.js +7 -31
  12. package/dist/cli/components/Sidebar.js.map +1 -1
  13. package/dist/cli/components/StatusBar.js +18 -3
  14. package/dist/cli/components/StatusBar.js.map +1 -1
  15. package/dist/cli/councilDispatcher.js +3 -0
  16. package/dist/cli/councilDispatcher.js.map +1 -1
  17. package/dist/cli/hooks/conversationContext.js +136 -0
  18. package/dist/cli/hooks/conversationContext.js.map +1 -0
  19. package/dist/cli/hooks/useChatTurn.js +156 -56
  20. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  21. package/dist/cli/hooks/useSlashDispatch.js +55 -0
  22. package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
  23. package/dist/cli/main.bundled.js +1043 -393
  24. package/dist/cli/main.bundled.js.map +4 -4
  25. package/dist/cli/phase.js +52 -0
  26. package/dist/cli/phase.js.map +1 -0
  27. package/dist/cli/phaseState.js +12 -0
  28. package/dist/cli/phaseState.js.map +1 -0
  29. package/dist/cli/plugins/registry.js +90 -37
  30. package/dist/cli/plugins/registry.js.map +1 -1
  31. package/dist/cli/slashCommands.js +22 -0
  32. package/dist/cli/slashCommands.js.map +1 -1
  33. package/dist/cli/slashHandlers/provider.js.map +1 -1
  34. package/dist/cli/toolRegistry.js +3 -2
  35. package/dist/cli/toolRegistry.js.map +1 -1
  36. package/package.json +2 -2
@@ -1535,10 +1535,10 @@ function mergeDefs(...defs) {
1535
1535
  function cloneDef(schema) {
1536
1536
  return mergeDefs(schema._zod.def);
1537
1537
  }
1538
- function getElementAtPath(obj, path34) {
1539
- if (!path34)
1538
+ function getElementAtPath(obj, path36) {
1539
+ if (!path36)
1540
1540
  return obj;
1541
- return path34.reduce((acc, key) => acc?.[key], obj);
1541
+ return path36.reduce((acc, key) => acc?.[key], obj);
1542
1542
  }
1543
1543
  function promiseAllObject(promisesObj) {
1544
1544
  const keys = Object.keys(promisesObj);
@@ -1866,11 +1866,11 @@ function explicitlyAborted(x, startIndex = 0) {
1866
1866
  }
1867
1867
  return false;
1868
1868
  }
1869
- function prefixIssues(path34, issues) {
1869
+ function prefixIssues(path36, issues) {
1870
1870
  return issues.map((iss) => {
1871
1871
  var _a3;
1872
1872
  (_a3 = iss).path ?? (_a3.path = []);
1873
- iss.path.unshift(path34);
1873
+ iss.path.unshift(path36);
1874
1874
  return iss;
1875
1875
  });
1876
1876
  }
@@ -2088,16 +2088,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
2088
2088
  }
2089
2089
  function formatError(error51, mapper = (issue2) => issue2.message) {
2090
2090
  const fieldErrors = { _errors: [] };
2091
- const processError = (error52, path34 = []) => {
2091
+ const processError = (error52, path36 = []) => {
2092
2092
  for (const issue2 of error52.issues) {
2093
2093
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2094
- issue2.errors.map((issues) => processError({ issues }, [...path34, ...issue2.path]));
2094
+ issue2.errors.map((issues) => processError({ issues }, [...path36, ...issue2.path]));
2095
2095
  } else if (issue2.code === "invalid_key") {
2096
- processError({ issues: issue2.issues }, [...path34, ...issue2.path]);
2096
+ processError({ issues: issue2.issues }, [...path36, ...issue2.path]);
2097
2097
  } else if (issue2.code === "invalid_element") {
2098
- processError({ issues: issue2.issues }, [...path34, ...issue2.path]);
2098
+ processError({ issues: issue2.issues }, [...path36, ...issue2.path]);
2099
2099
  } else {
2100
- const fullpath = [...path34, ...issue2.path];
2100
+ const fullpath = [...path36, ...issue2.path];
2101
2101
  if (fullpath.length === 0) {
2102
2102
  fieldErrors._errors.push(mapper(issue2));
2103
2103
  } else {
@@ -2124,17 +2124,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
2124
2124
  }
2125
2125
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
2126
2126
  const result = { errors: [] };
2127
- const processError = (error52, path34 = []) => {
2127
+ const processError = (error52, path36 = []) => {
2128
2128
  var _a3, _b;
2129
2129
  for (const issue2 of error52.issues) {
2130
2130
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2131
- issue2.errors.map((issues) => processError({ issues }, [...path34, ...issue2.path]));
2131
+ issue2.errors.map((issues) => processError({ issues }, [...path36, ...issue2.path]));
2132
2132
  } else if (issue2.code === "invalid_key") {
2133
- processError({ issues: issue2.issues }, [...path34, ...issue2.path]);
2133
+ processError({ issues: issue2.issues }, [...path36, ...issue2.path]);
2134
2134
  } else if (issue2.code === "invalid_element") {
2135
- processError({ issues: issue2.issues }, [...path34, ...issue2.path]);
2135
+ processError({ issues: issue2.issues }, [...path36, ...issue2.path]);
2136
2136
  } else {
2137
- const fullpath = [...path34, ...issue2.path];
2137
+ const fullpath = [...path36, ...issue2.path];
2138
2138
  if (fullpath.length === 0) {
2139
2139
  result.errors.push(mapper(issue2));
2140
2140
  continue;
@@ -2166,8 +2166,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
2166
2166
  }
2167
2167
  function toDotPath(_path) {
2168
2168
  const segs = [];
2169
- const path34 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2170
- for (const seg of path34) {
2169
+ const path36 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2170
+ for (const seg of path36) {
2171
2171
  if (typeof seg === "number")
2172
2172
  segs.push(`[${seg}]`);
2173
2173
  else if (typeof seg === "symbol")
@@ -15670,13 +15670,13 @@ function resolveRef(ref, ctx) {
15670
15670
  if (!ref.startsWith("#")) {
15671
15671
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
15672
15672
  }
15673
- const path34 = ref.slice(1).split("/").filter(Boolean);
15674
- if (path34.length === 0) {
15673
+ const path36 = ref.slice(1).split("/").filter(Boolean);
15674
+ if (path36.length === 0) {
15675
15675
  return ctx.rootSchema;
15676
15676
  }
15677
15677
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
15678
- if (path34[0] === defsKey) {
15679
- const key = path34[1];
15678
+ if (path36[0] === defsKey) {
15679
+ const key = path36[1];
15680
15680
  if (!key || !ctx.defs[key]) {
15681
15681
  throw new Error(`Reference not found: ${ref}`);
15682
15682
  }
@@ -17753,11 +17753,11 @@ var init_tools = __esm({
17753
17753
  if (!ctx.addDocument)
17754
17754
  return "Knowledge vault tool not available.";
17755
17755
  const title = args["title"] || "New Document";
17756
- const path34 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
17756
+ const path36 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
17757
17757
  const content = args["content"] || "";
17758
17758
  const tags = args["tags"] || [];
17759
17759
  ctx.addDocument({
17760
- path: path34,
17760
+ path: path36,
17761
17761
  title,
17762
17762
  content,
17763
17763
  format: "markdown",
@@ -17766,7 +17766,7 @@ var init_tools = __esm({
17766
17766
  workspaceId: ctx.workspaceId
17767
17767
  });
17768
17768
  ctx.addActivity("vault", "created document", title);
17769
- return `Document "${title}" created at "${path34}".`;
17769
+ return `Document "${title}" created at "${path36}".`;
17770
17770
  }
17771
17771
  }
17772
17772
  ];
@@ -19037,6 +19037,145 @@ var init_AgentHarness = __esm({
19037
19037
  ...this.config.memberName ? { memberName: this.config.memberName } : {}
19038
19038
  };
19039
19039
  }
19040
+ /**
19041
+ * v1.8.0: true when a tool may run concurrently with other parallel-safe
19042
+ * tools. Write/execute tools stay serial (preserve file/order safety).
19043
+ * `task` is parallel-safe (read-only sub-agents). Opt out: ZELARI_PARALLEL_TOOLS=0.
19044
+ */
19045
+ isParallelSafeTool(toolName) {
19046
+ if (process.env.ZELARI_PARALLEL_TOOLS === "0")
19047
+ return false;
19048
+ if (toolName === "task")
19049
+ return true;
19050
+ const def = this.config.toolRegistry?.get(toolName);
19051
+ if (!def) {
19052
+ if (toolName.startsWith("mcp_")) {
19053
+ const lower = toolName.toLowerCase();
19054
+ if (lower.includes("write") || lower.includes("edit") || lower.includes("delete")) {
19055
+ return false;
19056
+ }
19057
+ return true;
19058
+ }
19059
+ return false;
19060
+ }
19061
+ const perms = def.permissions ?? [];
19062
+ if (perms.includes("write") || perms.includes("execute"))
19063
+ return false;
19064
+ return true;
19065
+ }
19066
+ /**
19067
+ * Execute buffered native tool calls: consecutive parallel-safe tools run
19068
+ * via Promise.all (chunked by ZELARI_MAX_PARALLEL_TOOLS, default 6); write/
19069
+ * execute tools run one-at-a-time in order.
19070
+ */
19071
+ async executePendingTools(pending, maxToolCalls) {
19072
+ const out = new Array(pending.length);
19073
+ const maxParallel = Math.max(1, Number.parseInt(process.env.ZELARI_MAX_PARALLEL_TOOLS ?? "6", 10) || 6);
19074
+ const inflight = /* @__PURE__ */ new Map();
19075
+ const invokeOne = async (p3) => {
19076
+ if (p3.cached !== void 0) {
19077
+ return {
19078
+ content: p3.cached,
19079
+ isError: p3.skipped || p3.cached.startsWith("[skipped]"),
19080
+ durationMs: 0
19081
+ };
19082
+ }
19083
+ if (p3.skipped || !this.config.toolRegistry) {
19084
+ return {
19085
+ content: `[skipped] maxToolCallsPerTurn reached (limit=${maxToolCalls})`,
19086
+ isError: true,
19087
+ durationMs: 0
19088
+ };
19089
+ }
19090
+ const callKey = hashToolCall(p3.toolName, p3.args);
19091
+ const fromRunCache = this.toolCallCache.get(callKey);
19092
+ if (fromRunCache !== void 0) {
19093
+ return {
19094
+ content: `[duplicate call \u2014 result repeated; do not call this tool again with the same arguments]
19095
+ ${fromRunCache}`,
19096
+ isError: false,
19097
+ durationMs: 0
19098
+ };
19099
+ }
19100
+ const existing = inflight.get(callKey);
19101
+ if (existing) {
19102
+ const shared2 = await existing;
19103
+ return {
19104
+ content: `[duplicate call \u2014 result repeated; do not call this tool again with the same arguments]
19105
+ ${shared2.content}`,
19106
+ isError: false,
19107
+ durationMs: 0
19108
+ };
19109
+ }
19110
+ const startMs = Date.now();
19111
+ const prom = (async () => {
19112
+ const result = await this.config.toolRegistry.invoke(p3.toolName, p3.args, {
19113
+ cwd: this.config.cwd,
19114
+ sessionId: this.sessionId,
19115
+ signal: this.activeController?.signal
19116
+ });
19117
+ let resultStr = "";
19118
+ if (result.ok) {
19119
+ if (typeof result.value === "string")
19120
+ resultStr = result.value;
19121
+ else if (typeof result.value === "object" && result.value !== null) {
19122
+ resultStr = JSON.stringify(result.value, null, 2);
19123
+ } else
19124
+ resultStr = String(result.value);
19125
+ } else {
19126
+ resultStr = result.error;
19127
+ }
19128
+ return {
19129
+ content: resultStr,
19130
+ isError: !result.ok,
19131
+ durationMs: Date.now() - startMs
19132
+ };
19133
+ })();
19134
+ inflight.set(callKey, prom);
19135
+ const r = await prom;
19136
+ if (!r.isError)
19137
+ this.toolCallCache.set(callKey, r.content);
19138
+ return { ...r, cacheKey: callKey };
19139
+ };
19140
+ const toOut = (p3, r) => {
19141
+ const endEvent = createBrainEvent("tool_execution_end", this.sessionId, {
19142
+ toolCallId: p3.toolCallId,
19143
+ result: r.content,
19144
+ isError: r.isError,
19145
+ durationMs: r.durationMs
19146
+ });
19147
+ return {
19148
+ toolCallId: p3.toolCallId,
19149
+ content: r.content,
19150
+ isError: r.isError,
19151
+ endEvent
19152
+ // Cache already written in invokeOne; no need to re-set.
19153
+ };
19154
+ };
19155
+ let i = 0;
19156
+ while (i < pending.length) {
19157
+ const p3 = pending[i];
19158
+ if (p3.skipped || p3.cached !== void 0 || !this.isParallelSafeTool(p3.toolName)) {
19159
+ out[i] = toOut(p3, await invokeOne(p3));
19160
+ i += 1;
19161
+ continue;
19162
+ }
19163
+ let j = i;
19164
+ while (j < pending.length && !pending[j].skipped && pending[j].cached === void 0 && this.isParallelSafeTool(pending[j].toolName)) {
19165
+ j += 1;
19166
+ }
19167
+ for (let off = i; off < j; off += maxParallel) {
19168
+ const end = Math.min(off + maxParallel, j);
19169
+ const slice = pending.slice(off, end);
19170
+ const results = await Promise.all(slice.map((item) => invokeOne(item)));
19171
+ for (let k = 0; k < results.length; k++) {
19172
+ out[off + k] = toOut(slice[k], results[k]);
19173
+ }
19174
+ }
19175
+ i = j;
19176
+ }
19177
+ return out;
19178
+ }
19040
19179
  /**
19041
19180
  * Cancel the in-flight run. Events drain until end of stream.
19042
19181
  *
@@ -19254,6 +19393,7 @@ var init_AgentHarness = __esm({
19254
19393
  let turnText = "";
19255
19394
  const turnToolCalls = [];
19256
19395
  const turnToolResults = [];
19396
+ const pendingNativeTools = [];
19257
19397
  for await (const delta of stream) {
19258
19398
  if (this.cancelled) {
19259
19399
  const cancelEvent = createBrainEvent("error", this.sessionId, {
@@ -19288,73 +19428,52 @@ var init_AgentHarness = __esm({
19288
19428
  this.emit(toolStartEvent);
19289
19429
  yield toolStartEvent;
19290
19430
  const skipped = typeof maxToolCalls === "number" && toolCallsThisTurn > maxToolCalls;
19291
- if (this.config.toolRegistry && !skipped) {
19431
+ if (!this.config.toolRegistry) {
19432
+ } else if (skipped) {
19433
+ pendingNativeTools.push({
19434
+ toolCallId: delta.toolCallId,
19435
+ toolName: delta.toolName,
19436
+ args: delta.args,
19437
+ skipped: true
19438
+ });
19439
+ } else {
19292
19440
  const callKey = hashToolCall(delta.toolName, delta.args);
19293
19441
  const cached2 = this.toolCallCache.get(callKey);
19294
19442
  if (cached2 !== void 0) {
19295
- const dupResult = `[duplicate call \u2014 result repeated; do not call this tool again with the same arguments]
19296
- ${cached2}`;
19297
- const endEvent = createBrainEvent("tool_execution_end", this.sessionId, {
19298
- toolCallId: delta.toolCallId,
19299
- result: dupResult,
19300
- isError: false,
19301
- durationMs: 0
19302
- });
19303
- this.emit(endEvent);
19304
- yield endEvent;
19305
- turnToolResults.push({
19443
+ pendingNativeTools.push({
19306
19444
  toolCallId: delta.toolCallId,
19307
- content: dupResult
19445
+ toolName: delta.toolName,
19446
+ args: delta.args,
19447
+ skipped: false,
19448
+ cached: `[duplicate call \u2014 result repeated; do not call this tool again with the same arguments]
19449
+ ${cached2}`
19308
19450
  });
19309
19451
  } else {
19310
- const startMs = Date.now();
19311
- const result = await this.config.toolRegistry.invoke(delta.toolName, delta.args, {
19312
- cwd: this.config.cwd,
19313
- sessionId: this.sessionId,
19314
- signal: this.activeController?.signal
19315
- });
19316
- let resultStr = "";
19317
- if (result.ok) {
19318
- if (typeof result.value === "string") {
19319
- resultStr = result.value;
19320
- } else if (typeof result.value === "object" && result.value !== null) {
19321
- resultStr = JSON.stringify(result.value, null, 2);
19322
- } else {
19323
- resultStr = String(result.value);
19324
- }
19325
- } else {
19326
- resultStr = result.error;
19327
- }
19328
- const endEvent = createBrainEvent("tool_execution_end", this.sessionId, {
19452
+ pendingNativeTools.push({
19329
19453
  toolCallId: delta.toolCallId,
19330
- result: resultStr,
19331
- isError: !result.ok,
19332
- durationMs: Date.now() - startMs
19454
+ toolName: delta.toolName,
19455
+ args: delta.args,
19456
+ skipped: false
19333
19457
  });
19334
- this.emit(endEvent);
19335
- yield endEvent;
19336
- turnToolResults.push({
19337
- toolCallId: delta.toolCallId,
19338
- content: resultStr
19339
- });
19340
- this.toolCallCache.set(callKey, resultStr);
19341
19458
  }
19342
- } else if (skipped) {
19343
- const endEvent = createBrainEvent("tool_execution_end", this.sessionId, {
19344
- toolCallId: delta.toolCallId,
19345
- result: `[skipped] maxToolCallsPerTurn reached (limit=${maxToolCalls})`,
19346
- isError: true,
19347
- durationMs: 0
19348
- });
19349
- this.emit(endEvent);
19350
- yield endEvent;
19351
- turnToolResults.push({
19352
- toolCallId: delta.toolCallId,
19353
- content: `[skipped] maxToolCallsPerTurn reached (limit=${maxToolCalls})`
19354
- });
19355
19459
  }
19356
19460
  } else if (delta.kind === "finish") {
19357
19461
  finishRef.value = delta.reason;
19462
+ if (pendingNativeTools.length > 0) {
19463
+ const executed = await this.executePendingTools(pendingNativeTools, maxToolCalls);
19464
+ for (const item of executed) {
19465
+ this.emit(item.endEvent);
19466
+ yield item.endEvent;
19467
+ turnToolResults.push({
19468
+ toolCallId: item.toolCallId,
19469
+ content: item.content
19470
+ });
19471
+ if (item.cacheKey && item.content && !item.isError) {
19472
+ this.toolCallCache.set(item.cacheKey, item.content);
19473
+ }
19474
+ }
19475
+ pendingNativeTools.length = 0;
19476
+ }
19358
19477
  const textTools = parseTextToolCalls(turnText);
19359
19478
  const nativeWriteRan = turnToolCalls.some((t) => t.name === "write_file" || t.name === "edit_file");
19360
19479
  const textWriteTools = textTools.filter((t) => t.name === "write_file" || t.name === "edit_file");
@@ -21705,12 +21824,44 @@ var init_tools4 = __esm({
21705
21824
  });
21706
21825
 
21707
21826
  // src/cli/browser/driver.ts
21708
- async function runBrowserCheck(options, loader = defaultPlaywrightLoader) {
21827
+ import { createRequire } from "node:module";
21828
+ import path21 from "node:path";
21829
+ import { pathToFileURL } from "node:url";
21830
+ function asPlaywright(mod) {
21831
+ if (!mod || typeof mod !== "object") return null;
21832
+ const m = mod;
21833
+ if (m.chromium && typeof m.chromium.launch === "function") return m;
21834
+ const d = m.default;
21835
+ if (d && d.chromium && typeof d.chromium.launch === "function") return d;
21836
+ return null;
21837
+ }
21838
+ async function loadPlaywright(cwd) {
21839
+ const base = cwd && cwd.length > 0 ? path21.resolve(cwd) : void 0;
21840
+ if (base) {
21841
+ try {
21842
+ const req = createRequire(path21.join(base, "package.json"));
21843
+ const resolved = req.resolve("playwright");
21844
+ const mod = await import(pathToFileURL(resolved).href);
21845
+ const pw = asPlaywright(mod);
21846
+ if (pw) return pw;
21847
+ } catch {
21848
+ }
21849
+ }
21850
+ try {
21851
+ const pkg = "playwright";
21852
+ const mod = await import(pkg);
21853
+ return asPlaywright(mod);
21854
+ } catch {
21855
+ return null;
21856
+ }
21857
+ }
21858
+ async function runBrowserCheck(options, loader) {
21709
21859
  const consoleErrors = [];
21710
21860
  const pageErrors = [];
21711
21861
  const failedRequests = [];
21712
21862
  const base = { ok: false, consoleErrors, pageErrors, failedRequests };
21713
- const pw = await loader();
21863
+ const resolve = loader ?? (() => loadPlaywright(options.cwd ?? process.cwd()));
21864
+ const pw = await resolve();
21714
21865
  if (!pw) {
21715
21866
  return {
21716
21867
  ...base,
@@ -21786,25 +21937,14 @@ async function runBrowserCheck(options, loader = defaultPlaywrightLoader) {
21786
21937
  }
21787
21938
  }
21788
21939
  }
21789
- var defaultPlaywrightLoader;
21790
21940
  var init_driver = __esm({
21791
21941
  "src/cli/browser/driver.ts"() {
21792
21942
  "use strict";
21793
- defaultPlaywrightLoader = async () => {
21794
- try {
21795
- const pkg = "playwright";
21796
- const mod = await import(pkg);
21797
- if (mod && mod.chromium && typeof mod.chromium.launch === "function") return mod;
21798
- return null;
21799
- } catch {
21800
- return null;
21801
- }
21802
- };
21803
21943
  }
21804
21944
  });
21805
21945
 
21806
21946
  // src/cli/browser/tools.ts
21807
- import path21 from "node:path";
21947
+ import path22 from "node:path";
21808
21948
  import os7 from "node:os";
21809
21949
  function createBrowserTool(deps = {}) {
21810
21950
  return {
@@ -21819,13 +21959,14 @@ function createBrowserTool(deps = {}) {
21819
21959
  waitForSelector: external_exports.string().optional().describe("Assert this CSS selector is present after actions."),
21820
21960
  screenshot: external_exports.boolean().optional().describe("Save a screenshot (default true).")
21821
21961
  }),
21822
- execute: async (args) => {
21962
+ execute: async (args, ctx) => {
21823
21963
  const a = args;
21824
21964
  const dir = deps.screenshotDir ?? os7.tmpdir();
21825
- const screenshotPath = a.screenshot === false ? void 0 : path21.join(dir, `zelari-browser-${Date.now()}.png`);
21965
+ const screenshotPath = a.screenshot === false ? void 0 : path22.join(dir, `zelari-browser-${Date.now()}.png`);
21826
21966
  const result = await runBrowserCheck(
21827
21967
  {
21828
21968
  url: a.url,
21969
+ cwd: ctx.cwd,
21829
21970
  ...a.actions ? { actions: a.actions } : {},
21830
21971
  ...a.waitForSelector ? { waitForSelector: a.waitForSelector } : {},
21831
21972
  ...screenshotPath ? { screenshotPath } : {}
@@ -21890,7 +22031,7 @@ function createBuiltinToolRegistry(options = {}) {
21890
22031
  const safeFetchUrl = wrapWithAudit(fetchUrlTool, audit, sessionId);
21891
22032
  const safeWebSearch = wrapWithAudit(webSearchTool, audit, sessionId);
21892
22033
  const registry3 = new ToolRegistry();
21893
- const readOnly = options.readOnly === true;
22034
+ const readOnly = options.readOnly === true || options.planMode === true;
21894
22035
  registry3.register(safeReadFile);
21895
22036
  registry3.register(safeGrepContent);
21896
22037
  registry3.register(safeListFiles);
@@ -23047,11 +23188,11 @@ var init_synthesisAudit = __esm({
23047
23188
  import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
23048
23189
  import { join as join2 } from "node:path";
23049
23190
  function loadNfrSpec(zelariRoot) {
23050
- const path34 = join2(zelariRoot, "nfr-spec.json");
23051
- if (!existsSync10(path34))
23191
+ const path36 = join2(zelariRoot, "nfr-spec.json");
23192
+ if (!existsSync10(path36))
23052
23193
  return null;
23053
23194
  try {
23054
- const raw = JSON.parse(readFileSync8(path34, "utf8"));
23195
+ const raw = JSON.parse(readFileSync8(path36, "utf8"));
23055
23196
  if (raw.version !== 1 || !Array.isArray(raw.targets))
23056
23197
  return null;
23057
23198
  return raw;
@@ -24035,6 +24176,19 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
24035
24176
  role: "system",
24036
24177
  content: `(Agent ${agent.name} asked: ${clarification.question})`
24037
24178
  });
24179
+ if (callbacks.onClarification && clarification.choices && clarification.choices.length >= 2) {
24180
+ try {
24181
+ const answer = await callbacks.onClarification(clarification);
24182
+ if (answer && answer.trim()) {
24183
+ agentOutputs.push({
24184
+ name: "User",
24185
+ role: "user",
24186
+ content: `Answer to clarifying question "${clarification.question}": ${answer.trim()}`
24187
+ });
24188
+ }
24189
+ } catch {
24190
+ }
24191
+ }
24038
24192
  }
24039
24193
  }
24040
24194
  if (oracle && !completedIds.has(oracle.id)) {
@@ -25263,9 +25417,9 @@ var init_types2 = __esm({
25263
25417
  import { readFileSync as readFileSync13 } from "node:fs";
25264
25418
  import { join as join8 } from "node:path";
25265
25419
  function readLessonsDeduped(zelariRoot) {
25266
- const path34 = join8(zelariRoot, LESSONS_FILE);
25420
+ const path36 = join8(zelariRoot, LESSONS_FILE);
25267
25421
  try {
25268
- const raw = readFileSync13(path34, "utf8");
25422
+ const raw = readFileSync13(path36, "utf8");
25269
25423
  const byId = /* @__PURE__ */ new Map();
25270
25424
  for (const line of raw.split(/\r?\n/)) {
25271
25425
  if (!line.trim())
@@ -25366,8 +25520,8 @@ function keywordsFrom(check2, signature) {
25366
25520
  return [.../* @__PURE__ */ new Set([...fromId, ...words])].slice(0, 12);
25367
25521
  }
25368
25522
  function writeLesson(zelariRoot, lesson) {
25369
- const path34 = join9(zelariRoot, LESSONS_FILE);
25370
- appendFileSync2(path34, `${JSON.stringify(lesson)}
25523
+ const path36 = join9(zelariRoot, LESSONS_FILE);
25524
+ appendFileSync2(path36, `${JSON.stringify(lesson)}
25371
25525
  `, "utf8");
25372
25526
  }
25373
25527
  function findSimilar(lessons, signature) {
@@ -25901,6 +26055,273 @@ var init_council = __esm({
25901
26055
  }
25902
26056
  });
25903
26057
 
26058
+ // src/cli/utils/envNumber.ts
26059
+ function envNumber(raw, opts) {
26060
+ const { default: def, min, max } = opts;
26061
+ if (raw === void 0 || raw === null) return def;
26062
+ const trimmed = raw.trim();
26063
+ if (trimmed.length === 0) return def;
26064
+ if (trimmed.toLowerCase() === "undefined" || trimmed.toLowerCase() === "null") return def;
26065
+ const parsed = Number.parseInt(trimmed, 10);
26066
+ if (!Number.isFinite(parsed)) return def;
26067
+ const absStr = `${Math.abs(parsed)}`;
26068
+ const body = trimmed.replace(/^[-+]/, "").replace(/^0+(?=\d)/, "");
26069
+ if (body !== absStr) return def;
26070
+ const firstChar = trimmed[0];
26071
+ const expectedSign = parsed < 0 ? "-" : /[0-9+]/.test(firstChar ?? "");
26072
+ const actualSign = parsed < 0 ? firstChar === "-" : firstChar !== "-";
26073
+ if (!expectedSign || !actualSign) return def;
26074
+ let clamped = parsed;
26075
+ if (min !== void 0 && clamped < min) clamped = min;
26076
+ if (max !== void 0 && clamped > max) clamped = max;
26077
+ return clamped;
26078
+ }
26079
+ var init_envNumber = __esm({
26080
+ "src/cli/utils/envNumber.ts"() {
26081
+ "use strict";
26082
+ }
26083
+ });
26084
+
26085
+ // src/cli/hooks/historyCompaction.ts
26086
+ function resolveMaxMessages(opts) {
26087
+ const envTurns = envNumber(process.env.ZELARI_HISTORY_TURNS, { default: 6, min: 0 });
26088
+ const turns = opts?.maxMessages ? Math.ceil(opts.maxMessages / 4) : envTurns;
26089
+ if (turns <= 0) return 0;
26090
+ return turns * 4;
26091
+ }
26092
+ function findValidCutIndex(messages, naiveCut) {
26093
+ let cut = naiveCut;
26094
+ while (cut < messages.length) {
26095
+ const kept = messages.slice(cut);
26096
+ const declared = /* @__PURE__ */ new Set();
26097
+ for (const m of kept) {
26098
+ if (m.role === "assistant" && m.toolCalls) {
26099
+ for (const tc of m.toolCalls) declared.add(tc.id);
26100
+ }
26101
+ }
26102
+ let moved = false;
26103
+ for (let k = 0; k < kept.length; k++) {
26104
+ const m = kept[k];
26105
+ if (m.role === "tool" && m.toolCallId && !declared.has(m.toolCallId)) {
26106
+ for (let j = cut - 1; j >= 0; j--) {
26107
+ const prev2 = messages[j];
26108
+ if (prev2.role === "assistant" && prev2.toolCalls && prev2.toolCalls.some((tc) => tc.id === m.toolCallId)) {
26109
+ cut = j;
26110
+ moved = true;
26111
+ break;
26112
+ }
26113
+ }
26114
+ break;
26115
+ }
26116
+ }
26117
+ if (!moved) break;
26118
+ }
26119
+ return cut;
26120
+ }
26121
+ function compactHistory(messages, opts) {
26122
+ const maxMessages = resolveMaxMessages(opts);
26123
+ if (maxMessages === 0) return [];
26124
+ if (messages.length <= maxMessages * 2) return messages;
26125
+ const naiveCut = messages.length - maxMessages;
26126
+ const cut = findValidCutIndex(messages, naiveCut);
26127
+ const dropped = cut;
26128
+ if (dropped === 0) return messages;
26129
+ const kept = messages.slice(cut);
26130
+ const summary = {
26131
+ role: "system",
26132
+ content: `${COMPACT_MARKER} ${dropped} earlier message(s) dropped.`
26133
+ };
26134
+ return [summary, ...kept];
26135
+ }
26136
+ var COMPACT_MARKER;
26137
+ var init_historyCompaction = __esm({
26138
+ "src/cli/hooks/historyCompaction.ts"() {
26139
+ "use strict";
26140
+ init_envNumber();
26141
+ COMPACT_MARKER = "[history] Earlier turns were compacted to stay within the context budget.";
26142
+ }
26143
+ });
26144
+
26145
+ // src/cli/hooks/conversationContext.ts
26146
+ var conversationContext_exports = {};
26147
+ __export(conversationContext_exports, {
26148
+ _resetConversationContextForTests: () => _resetConversationContextForTests,
26149
+ appendMessages: () => appendMessages,
26150
+ clearHistory: () => clearHistory,
26151
+ compactInPlace: () => compactInPlace,
26152
+ formatHistoryForCouncil: () => formatHistoryForCouncil,
26153
+ getHistory: () => getHistory,
26154
+ getLastClarification: () => getLastClarification,
26155
+ hydrateHistory: () => hydrateHistory,
26156
+ maybeAnchorShortAnswer: () => maybeAnchorShortAnswer,
26157
+ serializeHistory: () => serializeHistory,
26158
+ setHistory: () => setHistory,
26159
+ setLastClarification: () => setLastClarification
26160
+ });
26161
+ function getHistory() {
26162
+ return history;
26163
+ }
26164
+ function setHistory(messages) {
26165
+ history = [...messages];
26166
+ }
26167
+ function compactInPlace() {
26168
+ history = compactHistory(history);
26169
+ }
26170
+ function appendMessages(msgs) {
26171
+ if (msgs.length === 0) return;
26172
+ history = history.concat(msgs);
26173
+ }
26174
+ function clearHistory() {
26175
+ history = [];
26176
+ lastClarification = null;
26177
+ }
26178
+ function serializeHistory() {
26179
+ return [...history];
26180
+ }
26181
+ function hydrateHistory(messages) {
26182
+ history = [...messages];
26183
+ }
26184
+ function getLastClarification() {
26185
+ return lastClarification;
26186
+ }
26187
+ function setLastClarification(c) {
26188
+ lastClarification = c ? { question: c.question, choices: c.choices, at: Date.now() } : null;
26189
+ }
26190
+ function maybeAnchorShortAnswer(userText) {
26191
+ const clar = lastClarification;
26192
+ if (!clar) return null;
26193
+ const trimmed = userText.trim();
26194
+ if (!trimmed) return null;
26195
+ if (trimmed.length > 80 || trimmed.includes("\n")) return null;
26196
+ const lower = trimmed.toLowerCase();
26197
+ const choices = clar.choices;
26198
+ const matched = choices.find((c) => c.toLowerCase() === lower) ?? choices.find((c) => c.toLowerCase().startsWith(lower)) ?? choices.find((c) => lower.startsWith(c.toLowerCase().slice(0, Math.min(4, c.length))));
26199
+ let choiceLabel = matched ?? null;
26200
+ if (!choiceLabel && /^\d{1,2}$/.test(trimmed)) {
26201
+ const idx = Number.parseInt(trimmed, 10) - 1;
26202
+ if (idx >= 0 && idx < choices.length) choiceLabel = choices[idx] ?? null;
26203
+ }
26204
+ if (!choiceLabel && trimmed.length > 24) return null;
26205
+ const picked = choiceLabel ?? trimmed;
26206
+ return `The user is answering your previous clarifying question.
26207
+ Question: ${clar.question}
26208
+ Choices were: ${choices.join(" | ")}
26209
+ User's answer: ${picked}
26210
+ Proceed using this answer; do not re-ask the same question unless the answer is still ambiguous.`;
26211
+ }
26212
+ function formatHistoryForCouncil(maxTurns = 4) {
26213
+ if (history.length === 0) return "";
26214
+ const lines = [];
26215
+ let turns = 0;
26216
+ const chunk = [];
26217
+ for (let i = history.length - 1; i >= 0 && turns < maxTurns; i--) {
26218
+ const m = history[i];
26219
+ if (m.role === "user") {
26220
+ chunk.push(`User: ${truncate(m.content, 400)}`);
26221
+ turns += 1;
26222
+ } else if (m.role === "assistant" && m.content.trim()) {
26223
+ chunk.push(`Assistant: ${truncate(m.content, 600)}`);
26224
+ }
26225
+ }
26226
+ if (chunk.length === 0) return "";
26227
+ lines.push("## Prior conversation (rolling context)");
26228
+ lines.push(...chunk.reverse());
26229
+ return lines.join("\n");
26230
+ }
26231
+ function truncate(s, max) {
26232
+ const t = s.replace(/\s+/g, " ").trim();
26233
+ if (t.length <= max) return t;
26234
+ return `${t.slice(0, max - 1)}\u2026`;
26235
+ }
26236
+ function _resetConversationContextForTests() {
26237
+ history = [];
26238
+ lastClarification = null;
26239
+ }
26240
+ var history, lastClarification;
26241
+ var init_conversationContext = __esm({
26242
+ "src/cli/hooks/conversationContext.ts"() {
26243
+ "use strict";
26244
+ init_historyCompaction();
26245
+ history = [];
26246
+ lastClarification = null;
26247
+ }
26248
+ });
26249
+
26250
+ // src/cli/phaseState.ts
26251
+ var phaseState_exports = {};
26252
+ __export(phaseState_exports, {
26253
+ _resetPhaseForTests: () => _resetPhaseForTests,
26254
+ getPhase: () => getPhase,
26255
+ setPhase: () => setPhase
26256
+ });
26257
+ function getPhase() {
26258
+ return phase;
26259
+ }
26260
+ function setPhase(next) {
26261
+ phase = next;
26262
+ }
26263
+ function _resetPhaseForTests() {
26264
+ phase = "build";
26265
+ }
26266
+ var phase;
26267
+ var init_phaseState = __esm({
26268
+ "src/cli/phaseState.ts"() {
26269
+ "use strict";
26270
+ phase = "build";
26271
+ }
26272
+ });
26273
+
26274
+ // src/cli/phase.ts
26275
+ var phase_exports = {};
26276
+ __export(phase_exports, {
26277
+ PHASES: () => PHASES,
26278
+ PLAN_ALLOWED_WRITE_TOOLS: () => PLAN_ALLOWED_WRITE_TOOLS,
26279
+ PLAN_BLOCKED_TOOLS: () => PLAN_BLOCKED_TOOLS,
26280
+ describePhase: () => describePhase,
26281
+ nextPhase: () => nextPhase,
26282
+ parsePhase: () => parsePhase
26283
+ });
26284
+ function parsePhase(input) {
26285
+ const v = input.trim().toLowerCase();
26286
+ return PHASES.includes(v) ? v : null;
26287
+ }
26288
+ function nextPhase(current) {
26289
+ return current === "plan" ? "build" : "plan";
26290
+ }
26291
+ function describePhase(phase2) {
26292
+ switch (phase2) {
26293
+ case "plan":
26294
+ return "plan \u2014 explore & design only (no project writes; plan files allowed)";
26295
+ default:
26296
+ return "build \u2014 implement with full tools";
26297
+ }
26298
+ }
26299
+ var PHASES, PLAN_ALLOWED_WRITE_TOOLS, PLAN_BLOCKED_TOOLS;
26300
+ var init_phase = __esm({
26301
+ "src/cli/phase.ts"() {
26302
+ "use strict";
26303
+ PHASES = ["plan", "build"];
26304
+ PLAN_ALLOWED_WRITE_TOOLS = /* @__PURE__ */ new Set([
26305
+ // Workspace plan/docs — intentional plan-mode outputs
26306
+ "createPlan",
26307
+ "createTask",
26308
+ "updateTask",
26309
+ "createMilestone",
26310
+ "createDocument",
26311
+ "createDecision",
26312
+ "linkDocuments"
26313
+ // Soft writes that only touch .zelari / plan paths are still gated in
26314
+ // toolRegistry by path when needed; write_file/edit_file stay DENIED.
26315
+ ]);
26316
+ PLAN_BLOCKED_TOOLS = /* @__PURE__ */ new Set([
26317
+ "write_file",
26318
+ "edit_file",
26319
+ "apply_diff",
26320
+ "bash"
26321
+ ]);
26322
+ }
26323
+ });
26324
+
25904
26325
  // src/cli/workspace/paths.ts
25905
26326
  import {
25906
26327
  mkdirSync as mkdirSync6,
@@ -26082,12 +26503,12 @@ function buildPlanSummary(projectRoot = process.cwd(), options) {
26082
26503
  scopedOpen = [...inScope, ...neutral];
26083
26504
  }
26084
26505
  }
26085
- for (const phase of [...phases].sort(
26506
+ for (const phase2 of [...phases].sort(
26086
26507
  (a, b) => (a.order ?? 0) - (b.order ?? 0)
26087
26508
  )) {
26088
- const phaseTasks = scopedOpen.filter((t) => t.phaseId === phase.id);
26089
- parts.push("", `## ${phase.order ?? "?"}. ${phase.name ?? phase.id}`);
26090
- if (phase.description) parts.push(phase.description);
26509
+ const phaseTasks = scopedOpen.filter((t) => t.phaseId === phase2.id);
26510
+ parts.push("", `## ${phase2.order ?? "?"}. ${phase2.name ?? phase2.id}`);
26511
+ if (phase2.description) parts.push(phase2.description);
26091
26512
  for (const t of phaseTasks.slice(0, PLAN_SUMMARY_MAX_TASKS)) {
26092
26513
  parts.push(formatTaskLine(t));
26093
26514
  }
@@ -26508,28 +26929,28 @@ var init_storage = __esm({
26508
26929
  VALID_SCALARS = /^(true|false|null|~)$/i;
26509
26930
  Storage = class {
26510
26931
  /** Read a Markdown file with frontmatter. Throws if not found. */
26511
- read(path34) {
26512
- if (!existsSync15(path34)) {
26513
- throw new Error(`File not found: ${path34}`);
26932
+ read(path36) {
26933
+ if (!existsSync15(path36)) {
26934
+ throw new Error(`File not found: ${path36}`);
26514
26935
  }
26515
- const md = readFileSync15(path34, "utf8");
26936
+ const md = readFileSync15(path36, "utf8");
26516
26937
  return parseFrontmatter(md);
26517
26938
  }
26518
26939
  /** Read a Markdown file; returns null if not found. */
26519
- readIfExists(path34) {
26520
- if (!existsSync15(path34)) return null;
26521
- return this.read(path34);
26940
+ readIfExists(path36) {
26941
+ if (!existsSync15(path36)) return null;
26942
+ return this.read(path36);
26522
26943
  }
26523
26944
  /**
26524
26945
  * Write a Markdown file atomically (tmp + rename). Creates parent dirs.
26525
26946
  * The meta object is serialized as YAML frontmatter; body as Markdown.
26526
26947
  */
26527
- write(path34, meta3, body) {
26528
- mkdirSync7(dirname2(path34), { recursive: true });
26529
- const tmp = path34 + ".tmp-" + process.pid;
26948
+ write(path36, meta3, body) {
26949
+ mkdirSync7(dirname2(path36), { recursive: true });
26950
+ const tmp = path36 + ".tmp-" + process.pid;
26530
26951
  const md = serializeFrontmatter(meta3, body);
26531
26952
  writeFileSync11(tmp, md, "utf8");
26532
- renameSync2(tmp, path34);
26953
+ renameSync2(tmp, path36);
26533
26954
  }
26534
26955
  /** List all .md files in a directory (non-recursive). */
26535
26956
  listMarkdown(dir) {
@@ -26607,8 +27028,8 @@ function readPlan(ctx) {
26607
27028
  } catch {
26608
27029
  }
26609
27030
  }
26610
- const path34 = workspaceFile(ctx.rootDir, "plan");
26611
- const doc = ctx.storage.readIfExists(path34);
27031
+ const path36 = workspaceFile(ctx.rootDir, "plan");
27032
+ const doc = ctx.storage.readIfExists(path36);
26612
27033
  if (!doc) return { phases: [], tasks: [], milestones: [] };
26613
27034
  const meta3 = doc.meta;
26614
27035
  return {
@@ -26642,11 +27063,11 @@ function renderPlanBody(summary) {
26642
27063
  if (summary.phases.length === 0) {
26643
27064
  lines.push(`_(none yet)_`);
26644
27065
  } else {
26645
- for (const phase of [...summary.phases].sort(
27066
+ for (const phase2 of [...summary.phases].sort(
26646
27067
  (a, b) => (a.order ?? 0) - (b.order ?? 0)
26647
27068
  )) {
26648
27069
  lines.push(
26649
- `### ${phase.order ?? "?"}. ${phase.id} \`${phase.color ?? ""}\``
27070
+ `### ${phase2.order ?? "?"}. ${phase2.id} \`${phase2.color ?? ""}\``
26650
27071
  );
26651
27072
  lines.push("");
26652
27073
  }
@@ -26773,7 +27194,7 @@ function addMilestoneRecord(ctx, summary, input) {
26773
27194
  dueDate: input.dueDate,
26774
27195
  targetVersion: version2
26775
27196
  });
26776
- const path34 = join14(ctx.rootDir, "milestones", `${id}.md`);
27197
+ const path36 = join14(ctx.rootDir, "milestones", `${id}.md`);
26777
27198
  const meta3 = {
26778
27199
  kind: "milestone",
26779
27200
  id,
@@ -26790,7 +27211,7 @@ function addMilestoneRecord(ctx, summary, input) {
26790
27211
  `Target version: ${version2}`,
26791
27212
  ""
26792
27213
  ].join("\n");
26793
- ctx.storage.write(path34, meta3, body);
27214
+ ctx.storage.write(path36, meta3, body);
26794
27215
  return { id, created: true };
26795
27216
  }
26796
27217
  function readPlanSummary(ctx) {
@@ -26994,7 +27415,7 @@ function addIdeaStub(ctx) {
26994
27415
  const tags = args["tags"] ?? [];
26995
27416
  const category = args["category"] ?? "General";
26996
27417
  const id = `${nextAdrId(ctx)}-${slugify3(title)}`;
26997
- const path34 = workspaceArtifact(ctx.rootDir, "decisions", id);
27418
+ const path36 = workspaceArtifact(ctx.rootDir, "decisions", id);
26998
27419
  const meta3 = {
26999
27420
  kind: "adr",
27000
27421
  status: "proposed",
@@ -27020,7 +27441,7 @@ function addIdeaStub(ctx) {
27020
27441
  ...consequences.map((c) => `- ${c}`),
27021
27442
  ""
27022
27443
  ].join("\n");
27023
- ctx.storage.write(path34, meta3, body);
27444
+ ctx.storage.write(path36, meta3, body);
27024
27445
  return `ADR ${id} created: "${title}". Status: proposed. Promote to accepted via /update ADR or manual edit.`;
27025
27446
  });
27026
27447
  }
@@ -27102,14 +27523,14 @@ function createDocumentStub(ctx) {
27102
27523
  ctx.storage.write(risksPath, riskMeta, content);
27103
27524
  return `Document "${title}" created at risks.md (workspace root).`;
27104
27525
  }
27105
- const path34 = workspaceArtifact(ctx.rootDir, "docs", slug);
27526
+ const path36 = workspaceArtifact(ctx.rootDir, "docs", slug);
27106
27527
  const meta3 = {
27107
27528
  kind: "doc",
27108
27529
  id: slug,
27109
27530
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
27110
27531
  tags
27111
27532
  };
27112
- ctx.storage.write(path34, meta3, content);
27533
+ ctx.storage.write(path36, meta3, content);
27113
27534
  return `Document "${title}" created at docs/${slug}.md.`;
27114
27535
  });
27115
27536
  }
@@ -27325,18 +27746,18 @@ __export(updater_exports, {
27325
27746
  performUpdate: () => performUpdate,
27326
27747
  resolveBundledNpmCli: () => resolveBundledNpmCli
27327
27748
  });
27328
- import { createRequire } from "node:module";
27749
+ import { createRequire as createRequire2 } from "node:module";
27329
27750
  import { spawn as spawn4 } from "node:child_process";
27330
27751
  import { existsSync as existsSync17 } from "node:fs";
27331
- import path22 from "node:path";
27752
+ import path23 from "node:path";
27332
27753
  import { fileURLToPath } from "node:url";
27333
27754
  function resolveBundledNpmCli(execPath = process.execPath) {
27334
- const dir = path22.dirname(execPath);
27755
+ const dir = path23.dirname(execPath);
27335
27756
  const candidates = [
27336
27757
  // Windows: C:\...\node.exe → C:\...\node_modules\npm\bin\npm-cli.js
27337
- path22.join(dir, "node_modules", "npm", "bin", "npm-cli.js"),
27758
+ path23.join(dir, "node_modules", "npm", "bin", "npm-cli.js"),
27338
27759
  // POSIX: <prefix>/bin/node → <prefix>/lib/node_modules/npm/bin/npm-cli.js
27339
- path22.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
27760
+ path23.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
27340
27761
  ];
27341
27762
  for (const candidate of candidates) {
27342
27763
  try {
@@ -27353,7 +27774,7 @@ function looksLikeBrokenShim(exitCode, output) {
27353
27774
  }
27354
27775
  function getCurrentVersion() {
27355
27776
  try {
27356
- const pkgPath = path22.resolve(__dirname2, "..", "..", "package.json");
27777
+ const pkgPath = path23.resolve(__dirname2, "..", "..", "package.json");
27357
27778
  const pkg = require2(pkgPath);
27358
27779
  return pkg.version;
27359
27780
  } catch {
@@ -27464,8 +27885,8 @@ var init_updater = __esm({
27464
27885
  "src/cli/updater.ts"() {
27465
27886
  "use strict";
27466
27887
  init_cmdline();
27467
- require2 = createRequire(import.meta.url);
27468
- __dirname2 = path22.dirname(fileURLToPath(import.meta.url));
27888
+ require2 = createRequire2(import.meta.url);
27889
+ __dirname2 = path23.dirname(fileURLToPath(import.meta.url));
27469
27890
  REGISTRY_URL = "https://registry.npmjs.org/zelari-code/latest";
27470
27891
  }
27471
27892
  });
@@ -27870,6 +28291,9 @@ async function* dispatchCouncil(userMessage, options) {
27870
28291
  if (options.onCouncilStatus) {
27871
28292
  callbacks.onCouncilStatus = options.onCouncilStatus;
27872
28293
  }
28294
+ if (options.onClarification) {
28295
+ callbacks.onClarification = options.onClarification;
28296
+ }
27873
28297
  yield* runCouncilPure(userMessage, config2, callbacks);
27874
28298
  }
27875
28299
  var CouncilDispatchError;
@@ -27903,10 +28327,10 @@ import { createHash as createHash3 } from "node:crypto";
27903
28327
  import { join as join17 } from "node:path";
27904
28328
  import { readFile as readFile2 } from "node:fs/promises";
27905
28329
  async function readPackageJson2(projectRoot) {
27906
- const path34 = join17(projectRoot, "package.json");
27907
- if (!existsSync20(path34)) return null;
28330
+ const path36 = join17(projectRoot, "package.json");
28331
+ if (!existsSync20(path36)) return null;
27908
28332
  try {
27909
- return JSON.parse(await readFile2(path34, "utf8"));
28333
+ return JSON.parse(await readFile2(path36, "utf8"));
27910
28334
  } catch {
27911
28335
  return null;
27912
28336
  }
@@ -27988,9 +28412,9 @@ async function genBuild(ctx) {
27988
28412
  ].join("\n");
27989
28413
  }
27990
28414
  async function genOpenQuestions(ctx) {
27991
- const path34 = join17(ctx.rootDir, "risks.md");
27992
- if (!existsSync20(path34)) return "_No open questions._";
27993
- const content = readFileSync19(path34, "utf8");
28415
+ const path36 = join17(ctx.rootDir, "risks.md");
28416
+ if (!existsSync20(path36)) return "_No open questions._";
28417
+ const content = readFileSync19(path36, "utf8");
27994
28418
  const lines = content.split("\n");
27995
28419
  const questions = [];
27996
28420
  let currentTitle = "";
@@ -28141,9 +28565,9 @@ var init_agentsMd = __esm({
28141
28565
  });
28142
28566
 
28143
28567
  // src/cli/workspace/completeDesign.ts
28144
- function curatedTasksForPhase(phase) {
28145
- const label = (phase.name ?? phase.id).trim();
28146
- const scope = phase.description?.trim() ? ` Scope: ${phase.description.trim()}` : "";
28568
+ function curatedTasksForPhase(phase2) {
28569
+ const label = (phase2.name ?? phase2.id).trim();
28570
+ const scope = phase2.description?.trim() ? ` Scope: ${phase2.description.trim()}` : "";
28147
28571
  return [
28148
28572
  {
28149
28573
  title: `Specify ${label} deliverables`,
@@ -28190,15 +28614,15 @@ async function runBuiltinCompleteDesign(ctx) {
28190
28614
  const createMilestone = stubs.find((s) => s.name === "createMilestone");
28191
28615
  const stubCtx = ctx;
28192
28616
  let tasksAdded = 0;
28193
- for (const phase of summary.phases) {
28194
- const existingTasks = summary.tasks.filter((t) => t.phaseId === phase.id);
28617
+ for (const phase2 of summary.phases) {
28618
+ const existingTasks = summary.tasks.filter((t) => t.phaseId === phase2.id);
28195
28619
  if (existingTasks.length >= MIN_TASKS_PER_PHASE) continue;
28196
28620
  const existingTitles = new Set(existingTasks.map((t) => t.name ?? ""));
28197
- const templates = curatedTasksForPhase(phase).filter((t) => !existingTitles.has(t.title)).slice(0, MIN_TASKS_PER_PHASE - existingTasks.length);
28621
+ const templates = curatedTasksForPhase(phase2).filter((t) => !existingTitles.has(t.title)).slice(0, MIN_TASKS_PER_PHASE - existingTasks.length);
28198
28622
  for (const t of templates) {
28199
28623
  await createTask.execute(
28200
28624
  {
28201
- phaseId: phase.id,
28625
+ phaseId: phase2.id,
28202
28626
  title: t.title,
28203
28627
  description: t.description,
28204
28628
  fileRefs: t.fileRefs,
@@ -28557,8 +28981,8 @@ async function runPostCouncilHook(ctx, options) {
28557
28981
  sources: scope.sources
28558
28982
  } : void 0
28559
28983
  });
28560
- const path34 = writeCouncilCompletion(ctx.rootDir, completion);
28561
- completionHook = { ran: true, path: path34, completion };
28984
+ const path36 = writeCouncilCompletion(ctx.rootDir, completion);
28985
+ completionHook = { ran: true, path: path36, completion };
28562
28986
  } catch (err) {
28563
28987
  completionHook = {
28564
28988
  ran: true,
@@ -28627,7 +29051,7 @@ import {
28627
29051
  writeFileSync as writeFileSync14,
28628
29052
  mkdirSync as mkdirSync9
28629
29053
  } from "node:fs";
28630
- import path23 from "node:path";
29054
+ import path24 from "node:path";
28631
29055
  import os8 from "node:os";
28632
29056
  var FeedbackStore;
28633
29057
  var init_councilFeedback = __esm({
@@ -28638,7 +29062,7 @@ var init_councilFeedback = __esm({
28638
29062
  now;
28639
29063
  entries = [];
28640
29064
  constructor(options = {}) {
28641
- this.file = options.file ?? (process.env.ANATHEMA_COUNCIL_FEEDBACK_FILE ?? path23.join(os8.homedir(), ".tmp", "zelari-code", "council-feedback.json"));
29065
+ this.file = options.file ?? (process.env.ANATHEMA_COUNCIL_FEEDBACK_FILE ?? path24.join(os8.homedir(), ".tmp", "zelari-code", "council-feedback.json"));
28642
29066
  this.now = options.now ?? Date.now;
28643
29067
  this.load();
28644
29068
  }
@@ -28744,7 +29168,7 @@ var init_councilFeedback = __esm({
28744
29168
  }
28745
29169
  }
28746
29170
  save() {
28747
- mkdirSync9(path23.dirname(this.file), { recursive: true });
29171
+ mkdirSync9(path24.dirname(this.file), { recursive: true });
28748
29172
  writeFileSync14(
28749
29173
  this.file,
28750
29174
  JSON.stringify({ entries: this.entries }, null, 2),
@@ -28779,7 +29203,7 @@ __export(fileBackend_exports, {
28779
29203
  });
28780
29204
  import { randomUUID as randomUUID2 } from "node:crypto";
28781
29205
  import { promises as fs14 } from "node:fs";
28782
- import * as path24 from "node:path";
29206
+ import * as path25 from "node:path";
28783
29207
  function tokenize(text) {
28784
29208
  return text.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((t) => t.length >= 3);
28785
29209
  }
@@ -28817,8 +29241,8 @@ var init_fileBackend = __esm({
28817
29241
  logPath = "";
28818
29242
  memoryDir = "";
28819
29243
  async init(projectRoot) {
28820
- this.memoryDir = path24.join(projectRoot, ".zelari", "memory");
28821
- this.logPath = path24.join(this.memoryDir, "log.jsonl");
29244
+ this.memoryDir = path25.join(projectRoot, ".zelari", "memory");
29245
+ this.logPath = path25.join(this.memoryDir, "log.jsonl");
28822
29246
  await fs14.mkdir(this.memoryDir, { recursive: true });
28823
29247
  }
28824
29248
  async add(content, metadata = {}, graph) {
@@ -28893,7 +29317,7 @@ import { execFile as execFile2 } from "node:child_process";
28893
29317
  import { promisify } from "node:util";
28894
29318
  import { mkdtempSync, rmSync } from "node:fs";
28895
29319
  import { tmpdir } from "node:os";
28896
- import path25 from "node:path";
29320
+ import path26 from "node:path";
28897
29321
  import { randomUUID as randomUUID3 } from "node:crypto";
28898
29322
  async function git(cwd, args, env) {
28899
29323
  const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], {
@@ -28913,8 +29337,8 @@ async function isGitRepo(cwd) {
28913
29337
  return await gitSafe(cwd, ["rev-parse", "--is-inside-work-tree"]) === "true";
28914
29338
  }
28915
29339
  async function withTempIndex(fn) {
28916
- const dir = mkdtempSync(path25.join(tmpdir(), "zelari-ckpt-"));
28917
- const indexFile = path25.join(dir, "index");
29340
+ const dir = mkdtempSync(path26.join(tmpdir(), "zelari-ckpt-"));
29341
+ const indexFile = path26.join(dir, "index");
28918
29342
  try {
28919
29343
  return await fn(indexFile);
28920
29344
  } finally {
@@ -29005,7 +29429,7 @@ async function restoreCheckpoint(cwd, id) {
29005
29429
  const deleted = [];
29006
29430
  for (const rel2 of added) {
29007
29431
  try {
29008
- rmSync(path25.join(cwd, rel2), { force: true });
29432
+ rmSync(path26.join(cwd, rel2), { force: true });
29009
29433
  deleted.push(rel2);
29010
29434
  } catch {
29011
29435
  }
@@ -29035,7 +29459,7 @@ __export(zelariMission_exports, {
29035
29459
  });
29036
29460
  import { randomUUID as randomUUID4 } from "node:crypto";
29037
29461
  import { promises as fs15 } from "node:fs";
29038
- import * as path26 from "node:path";
29462
+ import * as path27 from "node:path";
29039
29463
  function resolveMaxIterations(env = process.env) {
29040
29464
  const raw = env.ZELARI_MISSION_MAX_ITER;
29041
29465
  const n = raw ? Number.parseInt(raw, 10) : DEFAULT_MAX_ITER;
@@ -29051,10 +29475,10 @@ function isMissionAutoStart(env = process.env) {
29051
29475
  return env.ZELARI_MISSION_AUTO === "1";
29052
29476
  }
29053
29477
  async function writeMissionState(projectRoot, state2) {
29054
- const dir = path26.join(projectRoot, ".zelari");
29478
+ const dir = path27.join(projectRoot, ".zelari");
29055
29479
  await fs15.mkdir(dir, { recursive: true });
29056
29480
  await fs15.writeFile(
29057
- path26.join(dir, "mission-state.json"),
29481
+ path27.join(dir, "mission-state.json"),
29058
29482
  JSON.stringify(state2, null, 2) + "\n",
29059
29483
  "utf8"
29060
29484
  );
@@ -29505,10 +29929,10 @@ var init_prereqChecks = __esm({
29505
29929
 
29506
29930
  // src/cli/plugins/prefs.ts
29507
29931
  import { existsSync as existsSync26, readFileSync as readFileSync23, writeFileSync as writeFileSync15, mkdirSync as mkdirSync10 } from "node:fs";
29508
- import path28 from "node:path";
29932
+ import path29 from "node:path";
29509
29933
  import os9 from "node:os";
29510
29934
  function getPluginPrefsPath() {
29511
- return process.env.ZELARI_PLUGINS_PREFS_FILE ?? path28.join(os9.homedir(), ".tmp", "zelari-code", "plugins.json");
29935
+ return process.env.ZELARI_PLUGINS_PREFS_FILE ?? path29.join(os9.homedir(), ".tmp", "zelari-code", "plugins.json");
29512
29936
  }
29513
29937
  function getPluginPrefs() {
29514
29938
  const file2 = getPluginPrefsPath();
@@ -29529,7 +29953,7 @@ function getPluginPrefs() {
29529
29953
  }
29530
29954
  function writePluginPrefs(prefs) {
29531
29955
  const file2 = getPluginPrefsPath();
29532
- mkdirSync10(path28.dirname(file2), { recursive: true });
29956
+ mkdirSync10(path29.dirname(file2), { recursive: true });
29533
29957
  writeFileSync15(file2, JSON.stringify(prefs, null, 2), {
29534
29958
  encoding: "utf-8",
29535
29959
  mode: 384
@@ -29562,9 +29986,11 @@ var registry_exports = {};
29562
29986
  __export(registry_exports, {
29563
29987
  PLUGINS: () => PLUGINS,
29564
29988
  detectMissingPlugins: () => detectMissingPlugins,
29565
- findPlugin: () => findPlugin
29989
+ findPlugin: () => findPlugin,
29990
+ isBinaryOnPath: () => isBinaryOnPath
29566
29991
  });
29567
- import { spawnSync as spawnSync3 } from "node:child_process";
29992
+ import { existsSync as existsSync27 } from "node:fs";
29993
+ import path30 from "node:path";
29568
29994
  function detectLocalBin(bin) {
29569
29995
  return (cwd) => {
29570
29996
  try {
@@ -29575,27 +30001,48 @@ function detectLocalBin(bin) {
29575
30001
  }
29576
30002
  };
29577
30003
  }
29578
- function detectGlobalBin(bin) {
29579
- return () => {
30004
+ function isBinaryOnPath(bin, opts = {}) {
30005
+ if (!bin || bin.includes("/") || bin.includes("\\") || bin.includes("..")) {
30006
+ return false;
30007
+ }
30008
+ const platform = opts.platform ?? process.platform;
30009
+ const exists = opts.exists ?? existsSync27;
30010
+ const pathEnv = opts.pathEnv ?? process.env.PATH ?? "";
30011
+ const pathMod = platform === "win32" ? path30.win32 : path30.posix;
30012
+ const sep = platform === "win32" ? ";" : ":";
30013
+ const dirs = pathEnv.split(sep).filter((d) => d.length > 0);
30014
+ const candidates = [bin];
30015
+ if (platform === "win32") {
30016
+ const pathExt = opts.pathExt ?? process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM";
30017
+ for (const ext of pathExt.split(";")) {
30018
+ if (!ext) continue;
30019
+ candidates.push(bin + ext);
30020
+ const lower = ext.toLowerCase();
30021
+ if (lower !== ext) candidates.push(bin + lower);
30022
+ }
30023
+ }
30024
+ for (const dir of dirs) {
30025
+ for (const name of candidates) {
30026
+ try {
30027
+ if (exists(pathMod.join(dir, name))) return true;
30028
+ } catch {
30029
+ }
30030
+ }
30031
+ }
30032
+ return false;
30033
+ }
30034
+ function detectPathBin(bin) {
30035
+ return (cwd) => {
29580
30036
  try {
29581
- const args = ["--version"];
29582
- const res = process.platform === "win32" ? spawnSync3(buildCmdLine(bin, args), {
29583
- stdio: ["ignore", "pipe", "ignore"],
29584
- shell: true,
29585
- timeout: 4e3
29586
- }) : spawnSync3(bin, args, {
29587
- stdio: ["ignore", "pipe", "ignore"],
29588
- timeout: 4e3
29589
- });
29590
- return Promise.resolve(res.status === 0 || res.stdout != null && res.stdout.toString().trim().length > 0 && res.error === void 0);
30037
+ if (resolveBin(bin, cwd) !== bin) return Promise.resolve(true);
29591
30038
  } catch {
29592
- return Promise.resolve(false);
29593
30039
  }
30040
+ return Promise.resolve(isBinaryOnPath(bin));
29594
30041
  };
29595
30042
  }
29596
- async function detectPlaywright() {
30043
+ async function detectPlaywright(cwd) {
29597
30044
  try {
29598
- const mod = await defaultPlaywrightLoader();
30045
+ const mod = await loadPlaywright(cwd);
29599
30046
  return mod !== null;
29600
30047
  } catch {
29601
30048
  return false;
@@ -29633,7 +30080,6 @@ var PLUGINS;
29633
30080
  var init_registry2 = __esm({
29634
30081
  "src/cli/plugins/registry.ts"() {
29635
30082
  "use strict";
29636
- init_cmdline();
29637
30083
  init_engine();
29638
30084
  init_driver();
29639
30085
  init_engine();
@@ -29663,7 +30109,7 @@ var init_registry2 = __esm({
29663
30109
  label: "Playwright (browser_check tool)",
29664
30110
  npmPackage: "playwright",
29665
30111
  installScope: "dev",
29666
- detect: () => detectPlaywright(),
30112
+ detect: detectPlaywright,
29667
30113
  postInstallHint: "Then fetch the browser binary: `npx playwright install chromium`",
29668
30114
  featureGate: "ZELARI_BROWSER",
29669
30115
  description: "Powers the browser_check tool (URL probing, click/fill/wait, screenshots)."
@@ -29673,7 +30119,7 @@ var init_registry2 = __esm({
29673
30119
  label: "typescript-language-server (LSP for TS/JS)",
29674
30120
  npmPackage: "typescript-language-server",
29675
30121
  installScope: "global",
29676
- detect: detectGlobalBin(binForLspLanguage("typescript")),
30122
+ detect: detectPathBin(binForLspLanguage("typescript")),
29677
30123
  featureGate: "ZELARI_LSP",
29678
30124
  description: "Powers go_to_definition / find_references / hover_type / rename_symbol for TS/JS."
29679
30125
  },
@@ -29682,9 +30128,24 @@ var init_registry2 = __esm({
29682
30128
  label: "pyright (LSP for Python)",
29683
30129
  npmPackage: "pyright",
29684
30130
  installScope: "global",
29685
- detect: detectGlobalBin(binForLspLanguage("python")),
30131
+ // Detect the langserver binary runtime spawns (pyright-langserver), not
30132
+ // the `pyright` CLI — and never via --version (langserver rejects it).
30133
+ detect: detectPathBin(binForLspLanguage("python")),
29686
30134
  featureGate: "ZELARI_LSP",
29687
30135
  description: "Powers go_to_definition / find_references / hover_type / rename_symbol for Python."
30136
+ },
30137
+ {
30138
+ // fff — high-performance codebase search MCP (fffind / ffgrep).
30139
+ // Installed as a global CLI; wire it in ~/.zelari-code/mcp.json (see
30140
+ // postInstallHint). Kill-switch: ZELARI_FFF=0.
30141
+ id: "fff",
30142
+ label: "fff (fast codebase search MCP)",
30143
+ npmPackage: "fff-mcp",
30144
+ installScope: "global",
30145
+ detect: detectPathBin("fff-mcp"),
30146
+ postInstallHint: 'Add to ~/.zelari-code/mcp.json: {"mcpServers":{"fff":{"command":"fff-mcp","args":[]}}} then restart. Prefer mcp_fff_* tools for search.',
30147
+ featureGate: "ZELARI_FFF",
30148
+ description: "Accelerates codebase search via fff MCP (fffind, ffgrep, fff-multi-grep) \u2014 faster and more token-efficient than plain grep."
29688
30149
  }
29689
30150
  ];
29690
30151
  }
@@ -29696,26 +30157,26 @@ __export(doctor_exports, {
29696
30157
  runDoctor: () => runDoctor
29697
30158
  });
29698
30159
  import { execSync as execSync2 } from "node:child_process";
29699
- import { existsSync as existsSync31, readFileSync as readFileSync26, readlinkSync, statSync as statSync6 } from "node:fs";
29700
- import { createRequire as createRequire2 } from "node:module";
30160
+ import { existsSync as existsSync32, readFileSync as readFileSync26, readlinkSync, statSync as statSync6 } from "node:fs";
30161
+ import { createRequire as createRequire3 } from "node:module";
29701
30162
  import { fileURLToPath as fileURLToPath2 } from "node:url";
29702
- import path33 from "node:path";
30163
+ import path35 from "node:path";
29703
30164
  function findPackageRoot(start) {
29704
30165
  let dir = start;
29705
30166
  for (let i = 0; i < 6; i += 1) {
29706
- const candidate = path33.join(dir, "package.json");
29707
- if (existsSync31(candidate)) {
30167
+ const candidate = path35.join(dir, "package.json");
30168
+ if (existsSync32(candidate)) {
29708
30169
  try {
29709
30170
  const pkg = JSON.parse(readFileSync26(candidate, "utf8"));
29710
30171
  if (pkg.name === "zelari-code") return dir;
29711
30172
  } catch {
29712
30173
  }
29713
30174
  }
29714
- const parent = path33.dirname(dir);
30175
+ const parent = path35.dirname(dir);
29715
30176
  if (parent === dir) break;
29716
30177
  dir = parent;
29717
30178
  }
29718
- return path33.resolve(__dirname3, "..", "..", "..");
30179
+ return path35.resolve(__dirname3, "..", "..", "..");
29719
30180
  }
29720
30181
  function tryExec(cmd) {
29721
30182
  try {
@@ -29729,7 +30190,7 @@ function tryExec(cmd) {
29729
30190
  }
29730
30191
  function readPackageJson3() {
29731
30192
  try {
29732
- const pkgPath = path33.join(packageRoot, "package.json");
30193
+ const pkgPath = path35.join(packageRoot, "package.json");
29733
30194
  return JSON.parse(readFileSync26(pkgPath, "utf8"));
29734
30195
  } catch {
29735
30196
  return null;
@@ -29745,8 +30206,8 @@ function checkShim(pkgName) {
29745
30206
  }
29746
30207
  const isWin = process.platform === "win32";
29747
30208
  const shimName = isWin ? "zelari-code.cmd" : "zelari-code";
29748
- const shimPath = path33.join(prefix, shimName);
29749
- if (!existsSync31(shimPath)) {
30209
+ const shimPath = path35.join(prefix, shimName);
30210
+ if (!existsSync32(shimPath)) {
29750
30211
  return FAIL(
29751
30212
  `shim not found at ${shimPath}
29752
30213
  fix: npm install -g ${pkgName}@latest --force`
@@ -29773,8 +30234,8 @@ function checkShim(pkgName) {
29773
30234
  fix: npm install -g ${pkgName}@latest --force`
29774
30235
  );
29775
30236
  }
29776
- const resolved = path33.resolve(path33.dirname(shimPath), target);
29777
- const expected = path33.join(
30237
+ const resolved = path35.resolve(path35.dirname(shimPath), target);
30238
+ const expected = path35.join(
29778
30239
  prefix,
29779
30240
  "node_modules",
29780
30241
  pkgName,
@@ -29813,8 +30274,8 @@ function checkNode(pkg) {
29813
30274
  return OK(`node ${raw}`);
29814
30275
  }
29815
30276
  function checkBundle() {
29816
- const bundle = path33.join(packageRoot, "dist", "cli", "main.bundled.js");
29817
- if (!existsSync31(bundle)) {
30277
+ const bundle = path35.join(packageRoot, "dist", "cli", "main.bundled.js");
30278
+ if (!existsSync32(bundle)) {
29818
30279
  return FAIL(
29819
30280
  `dist/cli/main.bundled.js missing at ${bundle}
29820
30281
  fix: npm run build:cli (then reinstall or run via tsx)`
@@ -29834,7 +30295,7 @@ function checkRuntimeDeps() {
29834
30295
  const missing = [];
29835
30296
  for (const dep of required2) {
29836
30297
  try {
29837
- const localReq = createRequire2(path33.join(packageRoot, "package.json"));
30298
+ const localReq = createRequire3(path35.join(packageRoot, "package.json"));
29838
30299
  localReq.resolve(dep);
29839
30300
  } catch {
29840
30301
  missing.push(dep);
@@ -29954,8 +30415,8 @@ var init_doctor = __esm({
29954
30415
  "src/cli/utils/doctor.ts"() {
29955
30416
  "use strict";
29956
30417
  init_prereqChecks();
29957
- require3 = createRequire2(import.meta.url);
29958
- __dirname3 = path33.dirname(fileURLToPath2(import.meta.url));
30418
+ require3 = createRequire3(import.meta.url);
30419
+ __dirname3 = path35.dirname(fileURLToPath2(import.meta.url));
29959
30420
  packageRoot = findPackageRoot(__dirname3);
29960
30421
  OK = (message) => ({
29961
30422
  ok: true,
@@ -29980,11 +30441,11 @@ var fixPath_exports = {};
29980
30441
  __export(fixPath_exports, {
29981
30442
  repairWindowsUserPath: () => repairWindowsUserPath
29982
30443
  });
29983
- import { spawnSync as spawnSync4 } from "node:child_process";
30444
+ import { spawnSync as spawnSync3 } from "node:child_process";
29984
30445
  function getGlobalPrefix2() {
29985
30446
  return (process.env.npm_config_prefix || process.env.NPM_CONFIG_PREFIX || "").trim() || (() => {
29986
30447
  try {
29987
- return spawnSync4("npm", ["prefix", "-g"], {
30448
+ return spawnSync3("npm", ["prefix", "-g"], {
29988
30449
  encoding: "utf8",
29989
30450
  stdio: ["ignore", "pipe", "ignore"]
29990
30451
  }).stdout?.trim() ?? "";
@@ -29995,7 +30456,7 @@ function getGlobalPrefix2() {
29995
30456
  }
29996
30457
  function powershell(script) {
29997
30458
  try {
29998
- const res = spawnSync4(
30459
+ const res = spawnSync3(
29999
30460
  "powershell.exe",
30000
30461
  ["-NoProfile", "-NonInteractive", "-Command", script],
30001
30462
  { encoding: "utf8" }
@@ -30564,20 +31025,25 @@ function StatusBar({
30564
31025
  queueCount = 0,
30565
31026
  busy = false,
30566
31027
  mode = "agent",
31028
+ phase: phase2 = "build",
30567
31029
  cwd,
30568
31030
  elapsedMs = null,
30569
31031
  lastMs = null,
30570
31032
  costUsd = 0,
30571
- cachedTokens = 0
31033
+ cachedTokens = 0,
31034
+ contextUsed = 0,
31035
+ contextLimit = 0,
31036
+ brandVersion
30572
31037
  }) {
30573
- 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(
31038
+ const ctxLabel = contextLimit > 0 ? `${formatTokens(contextUsed)}/${formatTokens(contextLimit)}` : contextUsed > 0 ? formatTokens(contextUsed) : null;
31039
+ 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(
30574
31040
  Text6,
30575
31041
  {
30576
31042
  bold: true,
30577
31043
  color: mode === "council" ? "magenta" : mode === "zelari" ? "green" : "cyan"
30578
31044
  },
30579
- mode === "council" ? "\u26EC council" : mode === "zelari" ? "\u26A1 zelari" : "\u23F5 agent"
30580
- ), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (shift+tab)"), /* @__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, 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, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 ")) : null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "session ", sessionId))));
31045
+ mode === "council" ? "council" : mode === "zelari" ? "zelari" : "agent"
31046
+ ), /* @__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, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 ")) : null, brandVersion ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "white" }, "ZELARI"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " v", brandVersion), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " \xB7 ")) : null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "session ", sessionId))));
30581
31047
  }
30582
31048
 
30583
31049
  // src/cli/components/SelectList.tsx
@@ -30625,15 +31091,6 @@ function SelectList({
30625
31091
  // src/cli/components/Sidebar.tsx
30626
31092
  import React8 from "react";
30627
31093
  import { Box as Box7, Text as Text8 } from "ink";
30628
- var EMBLEM_BRAILLE = `\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28F4\u28FF\u28F7\u28C6\u2840
30629
- \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28F6\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E6\u2840
30630
- \u2800\u2800\u2800\u2800\u2800\u2800\u28F0\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E6
30631
- \u2800\u2800\u2800\u2800\u28A0\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28F7\u2844
30632
- \u2800\u2800\u2800\u2800\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2819\u283B\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2806
30633
- \u2800\u2800\u2800\u2800\u28C8\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2800\u28F7\u28EC\u285B\u28BF\u28FF\u28DF\u28C1
30634
- \u2800\u2880\u28F4\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2800\u287F\u28BF\u287F\u2883\u28C8\u28FB\u28FF\u28FF\u28F6
30635
- \u2800\u28FC\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E0\u28F4\u28C6\u283B\u280E\u28BF\u28FF\u28FF\u28FF\u28FF\u28E7
30636
- \u28F0\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2847`;
30637
31094
  var SIDEBAR_WIDTH = 28;
30638
31095
  var SIDEBAR_MIN_COLUMNS = 96;
30639
31096
  var EMBLEM_MIN_ROWS = 26;
@@ -30647,7 +31104,7 @@ function truncatePath(p3, max) {
30647
31104
  return `\u2026${p3.slice(-(max - 1))}`;
30648
31105
  }
30649
31106
  function Sidebar({ version: version2, changes, rows }) {
30650
- const showEmblem = rows >= EMBLEM_MIN_ROWS;
31107
+ void version2;
30651
31108
  const maxFiles = rows >= EMBLEM_MIN_ROWS + 8 ? MAX_FILES_TALL : MAX_FILES_SHORT;
30652
31109
  const visible = changes.files.slice(0, maxFiles);
30653
31110
  const hidden = changes.files.length - visible.length;
@@ -30662,9 +31119,7 @@ function Sidebar({ version: version2, changes, rows }) {
30662
31119
  paddingX: 1,
30663
31120
  flexShrink: 0
30664
31121
  },
30665
- showEmblem && /* @__PURE__ */ React8.createElement(Box7, { justifyContent: "center" }, /* @__PURE__ */ React8.createElement(Text8, { color: "cyan" }, EMBLEM_BRAILLE)),
30666
- /* @__PURE__ */ React8.createElement(Box7, { justifyContent: "center" }, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "white" }, "ZELARI CODE")),
30667
- /* @__PURE__ */ React8.createElement(Box7, { justifyContent: "center" }, /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "v", version2, changes.branch ? ` \xB7 ${truncatePath(changes.branch, 12)}` : "")),
31122
+ /* @__PURE__ */ React8.createElement(Box7, { justifyContent: "center" }, /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, changes.branch ? truncatePath(changes.branch, 18) : "git")),
30668
31123
  /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "\u2500".repeat(innerWidth)),
30669
31124
  !changes.isRepo ? /* @__PURE__ */ React8.createElement(Text8, { dimColor: true, italic: true }, "not a git repo") : changes.files.length === 0 ? /* @__PURE__ */ React8.createElement(Text8, { dimColor: true, italic: true }, "no changes") : /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "changes (", changes.files.length, ")"), visible.map((f) => /* @__PURE__ */ React8.createElement(FileRow, { key: f.path, file: f, pathWidth: innerWidth - 10 })), hidden > 0 && /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, " +", hidden, " more\u2026"))
30670
31125
  );
@@ -33649,80 +34104,8 @@ function finalizeStreamingAssistant(setMessages) {
33649
34104
  });
33650
34105
  }
33651
34106
 
33652
- // src/cli/utils/envNumber.ts
33653
- function envNumber(raw, opts) {
33654
- const { default: def, min, max } = opts;
33655
- if (raw === void 0 || raw === null) return def;
33656
- const trimmed = raw.trim();
33657
- if (trimmed.length === 0) return def;
33658
- if (trimmed.toLowerCase() === "undefined" || trimmed.toLowerCase() === "null") return def;
33659
- const parsed = Number.parseInt(trimmed, 10);
33660
- if (!Number.isFinite(parsed)) return def;
33661
- const absStr = `${Math.abs(parsed)}`;
33662
- const body = trimmed.replace(/^[-+]/, "").replace(/^0+(?=\d)/, "");
33663
- if (body !== absStr) return def;
33664
- const firstChar = trimmed[0];
33665
- const expectedSign = parsed < 0 ? "-" : /[0-9+]/.test(firstChar ?? "");
33666
- const actualSign = parsed < 0 ? firstChar === "-" : firstChar !== "-";
33667
- if (!expectedSign || !actualSign) return def;
33668
- let clamped = parsed;
33669
- if (min !== void 0 && clamped < min) clamped = min;
33670
- if (max !== void 0 && clamped > max) clamped = max;
33671
- return clamped;
33672
- }
33673
-
33674
- // src/cli/hooks/historyCompaction.ts
33675
- var COMPACT_MARKER = "[history] Earlier turns were compacted to stay within the context budget.";
33676
- function resolveMaxMessages(opts) {
33677
- const envTurns = envNumber(process.env.ZELARI_HISTORY_TURNS, { default: 6, min: 0 });
33678
- const turns = opts?.maxMessages ? Math.ceil(opts.maxMessages / 4) : envTurns;
33679
- if (turns <= 0) return 0;
33680
- return turns * 4;
33681
- }
33682
- function findValidCutIndex(messages, naiveCut) {
33683
- let cut = naiveCut;
33684
- while (cut < messages.length) {
33685
- const kept = messages.slice(cut);
33686
- const declared = /* @__PURE__ */ new Set();
33687
- for (const m of kept) {
33688
- if (m.role === "assistant" && m.toolCalls) {
33689
- for (const tc of m.toolCalls) declared.add(tc.id);
33690
- }
33691
- }
33692
- let moved = false;
33693
- for (let k = 0; k < kept.length; k++) {
33694
- const m = kept[k];
33695
- if (m.role === "tool" && m.toolCallId && !declared.has(m.toolCallId)) {
33696
- for (let j = cut - 1; j >= 0; j--) {
33697
- const prev2 = messages[j];
33698
- if (prev2.role === "assistant" && prev2.toolCalls && prev2.toolCalls.some((tc) => tc.id === m.toolCallId)) {
33699
- cut = j;
33700
- moved = true;
33701
- break;
33702
- }
33703
- }
33704
- break;
33705
- }
33706
- }
33707
- if (!moved) break;
33708
- }
33709
- return cut;
33710
- }
33711
- function compactHistory(messages, opts) {
33712
- const maxMessages = resolveMaxMessages(opts);
33713
- if (maxMessages === 0) return [];
33714
- if (messages.length <= maxMessages * 2) return messages;
33715
- const naiveCut = messages.length - maxMessages;
33716
- const cut = findValidCutIndex(messages, naiveCut);
33717
- const dropped = cut;
33718
- if (dropped === 0) return messages;
33719
- const kept = messages.slice(cut);
33720
- const summary = {
33721
- role: "system",
33722
- content: `${COMPACT_MARKER} ${dropped} earlier message(s) dropped.`
33723
- };
33724
- return [summary, ...kept];
33725
- }
34107
+ // src/cli/hooks/useChatTurn.ts
34108
+ init_conversationContext();
33726
34109
 
33727
34110
  // src/cli/hooks/chatStats.ts
33728
34111
  function computeSessionStatsDelta(realUsage, userText, assistantContent, model, prev2) {
@@ -33737,6 +34120,83 @@ function computeSessionStatsDelta(realUsage, userText, assistantContent, model,
33737
34120
  };
33738
34121
  }
33739
34122
 
34123
+ // src/cli/hooks/useChatTurn.ts
34124
+ init_envNumber();
34125
+ init_phaseState();
34126
+ init_phase();
34127
+
34128
+ // src/cli/budget/tokenBudget.ts
34129
+ init_envNumber();
34130
+ init_historyCompaction();
34131
+ function estimateTokens(text) {
34132
+ if (!text) return 0;
34133
+ return Math.max(1, Math.ceil(text.length / 4));
34134
+ }
34135
+ function estimateHistoryTokens(messages) {
34136
+ let n = 0;
34137
+ for (const m of messages) {
34138
+ n += estimateTokens(m.content);
34139
+ if (m.toolCalls) {
34140
+ for (const tc of m.toolCalls) {
34141
+ n += estimateTokens(tc.name) + estimateTokens(JSON.stringify(tc.args ?? {}));
34142
+ }
34143
+ }
34144
+ }
34145
+ return n;
34146
+ }
34147
+ function resolveContextLimit() {
34148
+ return envNumber(process.env.ZELARI_CONTEXT_LIMIT, {
34149
+ default: 2e5,
34150
+ min: 4e3,
34151
+ max: 2e6
34152
+ });
34153
+ }
34154
+ function applyBudgetPolicy(history2, phase2, opts) {
34155
+ const contextLimit = resolveContextLimit();
34156
+ const warnings = [];
34157
+ let historyTurns = phase2 === "plan" ? envNumber(process.env.ZELARI_HISTORY_TURNS, { default: 8, min: 0 }) : envNumber(process.env.ZELARI_HISTORY_TURNS, { default: 6, min: 0 });
34158
+ let maxToolLoopIterations = phase2 === "plan" ? envNumber(process.env.ZELARI_MAX_TOOL_LOOP_ITERATIONS, { default: 40, min: 1 }) : envNumber(process.env.ZELARI_MAX_TOOL_LOOP_ITERATIONS, { default: 90, min: 1 });
34159
+ let hist = history2;
34160
+ let estimated = estimateHistoryTokens(hist);
34161
+ const sessionExtra = opts?.sessionTokens ?? 0;
34162
+ let occupancy = Math.min(1, (estimated + sessionExtra) / contextLimit);
34163
+ if (occupancy >= 0.7 && occupancy < 0.85) {
34164
+ warnings.push(
34165
+ `[budget] context ~${Math.round(occupancy * 100)}% full (${estimated + sessionExtra}/${contextLimit} tok est.) \u2014 consider /compact or shorter replies.`
34166
+ );
34167
+ }
34168
+ if (occupancy >= 0.85) {
34169
+ const forcedTurns = Math.max(1, Math.floor(historyTurns / 2));
34170
+ historyTurns = forcedTurns;
34171
+ hist = compactHistory(hist, { maxMessages: forcedTurns * 4 });
34172
+ estimated = estimateHistoryTokens(hist);
34173
+ occupancy = Math.min(1, (estimated + sessionExtra) / contextLimit);
34174
+ warnings.push(
34175
+ `[budget] auto-compact at 85% \u2014 kept ~${forcedTurns} turns (${estimated} tok history est.).`
34176
+ );
34177
+ maxToolLoopIterations = Math.min(maxToolLoopIterations, phase2 === "plan" ? 24 : 40);
34178
+ }
34179
+ if (occupancy >= 0.95) {
34180
+ hist = compactHistory(hist, { maxMessages: 8 });
34181
+ estimated = estimateHistoryTokens(hist);
34182
+ occupancy = Math.min(1, (estimated + sessionExtra) / contextLimit);
34183
+ historyTurns = 2;
34184
+ maxToolLoopIterations = Math.min(maxToolLoopIterations, 16);
34185
+ warnings.push(
34186
+ `[budget] HARD context pressure (\u226595%) \u2014 history cut to last ~2 turns. Prefer /clear or a new session if quality drops.`
34187
+ );
34188
+ }
34189
+ return {
34190
+ history: hist,
34191
+ warnings,
34192
+ maxToolLoopIterations,
34193
+ historyTurns,
34194
+ estimatedHistoryTokens: estimated,
34195
+ contextLimit,
34196
+ occupancy
34197
+ };
34198
+ }
34199
+
33740
34200
  // src/cli/hooks/useChatTurn.ts
33741
34201
  function useChatTurn(params) {
33742
34202
  const {
@@ -33754,8 +34214,10 @@ function useChatTurn(params) {
33754
34214
  } = params;
33755
34215
  const harnessRef = useRef4(null);
33756
34216
  const [queueCount, setQueueCount] = useState6(0);
33757
- const historyRef = useRef4([]);
33758
34217
  const useLiveModel = !!(setLive && liveRef);
34218
+ const clearConversationHistory = useCallback2(() => {
34219
+ clearHistory();
34220
+ }, []);
33759
34221
  const dispatchPrompt = useCallback2(
33760
34222
  async (userText, opts) => {
33761
34223
  let envConfig;
@@ -33763,8 +34225,15 @@ function useChatTurn(params) {
33763
34225
  let historySeedLen = 0;
33764
34226
  let turnSucceeded = false;
33765
34227
  try {
33766
- historyRef.current = compactHistory(historyRef.current);
33767
- historySeedLen = historyRef.current.length;
34228
+ compactInPlace();
34229
+ const budget = applyBudgetPolicy(getHistory(), getPhase());
34230
+ setHistory(budget.history);
34231
+ for (const w of budget.warnings) {
34232
+ appendSystem(setMessages, w, Date.now());
34233
+ }
34234
+ historySeedLen = getHistory().length;
34235
+ const anchored = maybeAnchorShortAnswer(userText);
34236
+ const effectiveUserText = anchored ?? userText;
33768
34237
  envConfig = await providerFromEnv();
33769
34238
  if (!envConfig) {
33770
34239
  const active = resolveActiveProvider();
@@ -33776,7 +34245,10 @@ function useChatTurn(params) {
33776
34245
  return;
33777
34246
  }
33778
34247
  setBusy(true);
33779
- const { registry: toolRegistry } = createBuiltinToolRegistry();
34248
+ const workPhase = getPhase();
34249
+ const { registry: toolRegistry } = createBuiltinToolRegistry({
34250
+ planMode: workPhase === "plan"
34251
+ });
33780
34252
  const baseProviderStream = openaiCompatibleProvider(envConfig);
33781
34253
  const failoverResolution = await resolveFailoverStream({
33782
34254
  failoverEnabled: process.env.ANATHEMA_FAILOVER !== "0",
@@ -33856,12 +34328,26 @@ function useChatTurn(params) {
33856
34328
  const isWindows = process.platform === "win32";
33857
34329
  const shellGuidance = resolvedShell.isBash ? `The bash tool runs commands via Git Bash / MSYS2 (${resolvedShell.shell}). Write POSIX commands: ls, grep, $VAR, &&, /c/Users/... all work.` : isWindows ? `The bash tool runs commands via cmd.exe (Git Bash not found). Write Windows-native commands: use dir (not ls), %VAR% (not $VAR), avoid POSIX-only syntax.` : `The bash tool runs commands via /bin/sh.`;
33858
34330
  const nonInteractiveGuidance = "The shell is NON-INTERACTIVE (stdin closed): commands that prompt for input fail immediately. Always pass non-interactive flags (--yes, -y, --template, --force). If a scaffolder still insists on prompting (e.g. `npm create vite` in a non-empty directory), do NOT retry it \u2014 scaffold into a fresh empty subdirectory and move the files, or write package.json/configs/sources yourself with write_file, then run `npm install`.";
34331
+ const planPhaseBlock = workPhase === "plan" ? [
34332
+ "",
34333
+ "# Work Phase: PLAN",
34334
+ "You are in PLAN mode. Explore and design only.",
34335
+ "- Do NOT implement production code or run destructive shell commands.",
34336
+ "- write_file / edit_file / bash / apply_diff are unavailable.",
34337
+ "- Produce a clear plan, ask clarifying questions (---QUESTION---), use workspace plan tools when relevant.",
34338
+ "- When the plan is ready, tell the user to run /build to implement."
34339
+ ].join("\n") : workPhase === "build" && planSummary ? [
34340
+ "",
34341
+ "# Work Phase: BUILD",
34342
+ "Implement the approved plan. Prefer acting over describing. Update plan task statuses as you go."
34343
+ ].join("\n") : "";
33859
34344
  const shellContextBlock = [
33860
34345
  "# Platform & Shell",
33861
34346
  `platform: ${process.platform}`,
33862
34347
  `shell: ${resolvedShell.via}`,
33863
34348
  shellGuidance,
33864
34349
  nonInteractiveGuidance,
34350
+ planPhaseBlock,
33865
34351
  "",
33866
34352
  "# Working Directory",
33867
34353
  `You are running in: ${cwd}`,
@@ -33915,20 +34401,17 @@ function useChatTurn(params) {
33915
34401
  default: 25,
33916
34402
  min: 1
33917
34403
  });
33918
- const maxToolLoopIterations = envNumber(process.env.ZELARI_MAX_TOOL_LOOP_ITERATIONS, {
33919
- default: 90,
33920
- min: 1
33921
- });
34404
+ const maxToolLoopIterations = budget.maxToolLoopIterations;
33922
34405
  const harness2 = new AgentHarness({
33923
34406
  model: envConfig.model,
33924
34407
  provider: "openai-compatible",
33925
34408
  messages: [
33926
34409
  { role: "system", content: systemPrompt },
33927
- // v1.6.0: seed prior turns so the model sees its own last
33928
- // question when the user answers briefly. historyRef.current
33929
- // is compacted above (possibly empty if ZELARI_HISTORY_TURNS=0).
33930
- ...historyRef.current,
33931
- { role: "user", content: userText }
34410
+ // v1.8.0: shared rolling history (agent/council/zelari) so short
34411
+ // answers bind to prior ---QUESTION--- blocks. Possibly empty
34412
+ // when ZELARI_HISTORY_TURNS=0.
34413
+ ...getHistory(),
34414
+ { role: "user", content: effectiveUserText }
33932
34415
  ],
33933
34416
  tools: toolRegistry.toOpenAITools().map((t) => ({
33934
34417
  name: t.function.name,
@@ -34069,17 +34552,19 @@ function useChatTurn(params) {
34069
34552
  const all = h.getMessages();
34070
34553
  const seedLen = 1 + historySeedLen + 1;
34071
34554
  if (all.length > seedLen) {
34072
- historyRef.current = historyRef.current.concat(
34073
- all.slice(seedLen)
34074
- );
34555
+ appendMessages(all.slice(seedLen));
34075
34556
  }
34076
34557
  }
34077
34558
  } catch {
34078
34559
  }
34079
- if (turnSucceeded && setPicker && assistantContent) {
34560
+ if (turnSucceeded && assistantContent) {
34080
34561
  try {
34081
34562
  const clar = parseClarificationRequest(assistantContent);
34082
34563
  if (clar && clar.choices && clar.choices.length >= 2) {
34564
+ setLastClarification({
34565
+ question: clar.question,
34566
+ choices: clar.choices
34567
+ });
34083
34568
  const cleaned = cleanAgentContent(assistantContent);
34084
34569
  if (cleaned !== assistantContent) {
34085
34570
  setMessages((prev2) => {
@@ -34093,14 +34578,18 @@ function useChatTurn(params) {
34093
34578
  return next;
34094
34579
  });
34095
34580
  }
34096
- setPicker({
34097
- kind: "clarification",
34098
- title: clar.question,
34099
- items: clar.choices.map((c) => ({ value: c, label: c })),
34100
- onAnswer: (value) => {
34101
- void dispatchPrompt(value);
34102
- }
34103
- });
34581
+ if (setPicker) {
34582
+ setPicker({
34583
+ kind: "clarification",
34584
+ title: clar.question,
34585
+ items: clar.choices.map((c) => ({ value: c, label: c })),
34586
+ onAnswer: (value) => {
34587
+ void dispatchPrompt(value);
34588
+ }
34589
+ });
34590
+ }
34591
+ } else {
34592
+ setLastClarification(null);
34104
34593
  }
34105
34594
  } catch {
34106
34595
  }
@@ -34152,7 +34641,8 @@ function useChatTurn(params) {
34152
34641
  setBusy,
34153
34642
  setQueueCount,
34154
34643
  setLive,
34155
- liveRef
34644
+ liveRef,
34645
+ setPicker
34156
34646
  });
34157
34647
  },
34158
34648
  [
@@ -34164,7 +34654,8 @@ function useChatTurn(params) {
34164
34654
  setBusy,
34165
34655
  setQueueCount,
34166
34656
  setLive,
34167
- liveRef
34657
+ liveRef,
34658
+ setPicker
34168
34659
  ]
34169
34660
  );
34170
34661
  const pendingZelariRef = useRef4(null);
@@ -34204,7 +34695,8 @@ function useChatTurn(params) {
34204
34695
  dispatchZelariPrompt,
34205
34696
  harnessRef,
34206
34697
  queueCount,
34207
- setQueueCount
34698
+ setQueueCount,
34699
+ clearConversationHistory
34208
34700
  };
34209
34701
  }
34210
34702
  async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
@@ -34216,7 +34708,8 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
34216
34708
  flushStreaming,
34217
34709
  setBusy,
34218
34710
  setLive,
34219
- liveRef
34711
+ liveRef,
34712
+ setPicker
34220
34713
  } = deps;
34221
34714
  const useLiveModel = !!(setLive && liveRef);
34222
34715
  const envConfig = await providerFromEnv();
@@ -34230,6 +34723,19 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
34230
34723
  return { completionOk: false, ran: false };
34231
34724
  }
34232
34725
  setBusy(true);
34726
+ compactInPlace();
34727
+ const councilBudget = applyBudgetPolicy(getHistory(), getPhase());
34728
+ setHistory(councilBudget.history);
34729
+ for (const w of councilBudget.warnings) {
34730
+ appendSystem(setMessages, w, Date.now());
34731
+ }
34732
+ const anchored = maybeAnchorShortAnswer(text);
34733
+ const effectiveText = anchored ?? text;
34734
+ appendSystem(
34735
+ setMessages,
34736
+ `[phase] ${describePhase(getPhase())}`,
34737
+ Date.now()
34738
+ );
34233
34739
  const { dispatchCouncil: dispatchCouncil2 } = await Promise.resolve().then(() => (init_councilDispatcher(), councilDispatcher_exports));
34234
34740
  const { createWorkspaceContext: createWorkspaceContext2, createWorkspaceStubs: createWorkspaceStubs2 } = await Promise.resolve().then(() => (init_stubs(), stubs_exports));
34235
34741
  const { createWorkspaceToolRegistry: createWorkspaceToolRegistry2 } = await Promise.resolve().then(() => (init_toolRegistry2(), toolRegistry_exports2));
@@ -34238,13 +34744,18 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
34238
34744
  const { buildWorkspaceSummary: buildWorkspaceSummary2, buildPlanSummary: buildPlanSummary2 } = await Promise.resolve().then(() => (init_workspaceSummary(), workspaceSummary_exports));
34239
34745
  const { buildLessonsSummary: buildLessonsSummary2 } = await Promise.resolve().then(() => (init_buildLessonsSummary(), buildLessonsSummary_exports));
34240
34746
  const { FeedbackStore: FeedbackStore2 } = await Promise.resolve().then(() => (init_councilFeedback(), councilFeedback_exports));
34241
- const { registry: councilToolRegistry } = createBuiltinToolRegistry();
34747
+ const workPhase = getPhase();
34748
+ const { registry: councilToolRegistry } = createBuiltinToolRegistry({
34749
+ planMode: workPhase === "plan"
34750
+ });
34242
34751
  const workspaceCtx = createWorkspaceContext2();
34243
34752
  const workspaceReg = createWorkspaceToolRegistry2(workspaceCtx);
34244
34753
  for (const name of workspaceReg.list()) {
34245
34754
  const td = workspaceReg.get(name);
34246
- if (td) councilToolRegistry.register(td);
34755
+ if (!td) continue;
34756
+ councilToolRegistry.register(td);
34247
34757
  }
34758
+ const phaseRunMode = overrides.runMode ?? (workPhase === "plan" ? "design-phase" : "implementation");
34248
34759
  try {
34249
34760
  const { registerMcpTools: registerMcpTools2 } = await Promise.resolve().then(() => (init_mcpManager(), mcpManager_exports));
34250
34761
  const mcp = await registerMcpTools2(councilToolRegistry);
@@ -34273,7 +34784,7 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
34273
34784
  let sliceDegraded = false;
34274
34785
  const PROVIDER_ERROR_ABORT_THRESHOLD = 2;
34275
34786
  try {
34276
- for await (const event of dispatchCouncil2(text, {
34787
+ for await (const event of dispatchCouncil2(effectiveText, {
34277
34788
  apiKey: envConfig.apiKey,
34278
34789
  model: envConfig.model,
34279
34790
  provider: "openai-compatible",
@@ -34287,20 +34798,49 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
34287
34798
  // on and projected their identity onto the task.
34288
34799
  // v0.7.3: append the existing plan (if any) so a follow-up /council
34289
34800
  // continues it instead of re-planning from scratch.
34801
+ // v1.8.0: rolling conversation context so short answers bind across
34802
+ // council turns (same history store as the single-agent path).
34290
34803
  workspaceContext: [
34291
34804
  buildWorkspaceSummary2(process.cwd()),
34292
- buildPlanSummary2(process.cwd(), { userMessage: text }),
34293
- buildLessonsSummary2(process.cwd(), text)
34805
+ buildPlanSummary2(process.cwd(), { userMessage: effectiveText }),
34806
+ buildLessonsSummary2(process.cwd(), effectiveText),
34807
+ formatHistoryForCouncil(4)
34294
34808
  ].filter(Boolean).join("\n\n"),
34295
34809
  maxToolCallsPerTurn: councilMaxToolCalls,
34296
34810
  // v1.0: Zelari-mode per-slice overrides (memory RAG, forced run mode,
34297
34811
  // raised chairman budget). No-ops for a normal /council run.
34298
34812
  ...overrides.ragContext ? { ragContext: overrides.ragContext } : {},
34299
- ...overrides.runMode ? { runMode: overrides.runMode } : {},
34813
+ runMode: phaseRunMode,
34300
34814
  ...overrides.maxToolCallsChairman ? { maxToolCallsChairman: overrides.maxToolCallsChairman } : {},
34301
34815
  onCouncilStatus: (message) => {
34302
34816
  appendSystem(setMessages, message, Date.now());
34303
- }
34817
+ },
34818
+ // v1.8.0: pause council when a member asks a structured question.
34819
+ onClarification: setPicker ? (req) => new Promise((resolve) => {
34820
+ const choices = req.choices ?? [];
34821
+ if (choices.length < 2) {
34822
+ resolve(null);
34823
+ return;
34824
+ }
34825
+ setLastClarification({
34826
+ question: req.question,
34827
+ choices
34828
+ });
34829
+ let settled = false;
34830
+ const finish = (value) => {
34831
+ if (settled) return;
34832
+ settled = true;
34833
+ setPicker(null);
34834
+ resolve(value);
34835
+ };
34836
+ setPicker({
34837
+ kind: "clarification",
34838
+ title: req.question,
34839
+ items: choices.map((c) => ({ value: c, label: c })),
34840
+ onAnswer: (value) => finish(value),
34841
+ onCancel: () => finish(null)
34842
+ });
34843
+ }) : void 0
34304
34844
  })) {
34305
34845
  if (councilAborted) {
34306
34846
  if (writerRef.current) await writerRef.current.append(event);
@@ -34451,6 +34991,18 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
34451
34991
  flushStreaming();
34452
34992
  if (useLiveModel) finalizeStreaming(setMessages, setLive);
34453
34993
  else finalizeStreamingAssistant(setMessages);
34994
+ if (membersCompleted > 0 || chairmanProducedOutput) {
34995
+ try {
34996
+ appendMessages([
34997
+ { role: "user", content: effectiveText },
34998
+ {
34999
+ role: "assistant",
35000
+ content: chairmanSynthesisText.trim() || "[council completed without chairman synthesis text]"
35001
+ }
35002
+ ]);
35003
+ } catch {
35004
+ }
35005
+ }
34454
35006
  const hookShouldRun = membersCompleted > 0 || chairmanProducedOutput;
34455
35007
  sliceRan = hookShouldRun;
34456
35008
  if (hookShouldRun) {
@@ -34473,7 +35025,7 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
34473
35025
  }
34474
35026
  const hook = await runPostCouncilHook2(workspaceCtx, {
34475
35027
  runMode: councilRunMode,
34476
- userMessage: text,
35028
+ userMessage: effectiveText,
34477
35029
  synthesisText: chairmanSynthesisText || void 0,
34478
35030
  degradedRun: degraded.degraded,
34479
35031
  degradedReasons: degraded.reasons
@@ -35123,6 +35675,27 @@ ${formatSkillList(availableSkills)}`
35123
35675
  }
35124
35676
  return { handled: true, kind: "mode_set" };
35125
35677
  }
35678
+ case "plan": {
35679
+ const goal = args.join(" ").trim();
35680
+ return {
35681
+ handled: true,
35682
+ kind: "phase_set",
35683
+ phaseTarget: "plan",
35684
+ ...goal ? { phaseGoal: goal } : {}
35685
+ };
35686
+ }
35687
+ case "build": {
35688
+ const goal = args.join(" ").trim();
35689
+ return {
35690
+ handled: true,
35691
+ kind: "phase_set",
35692
+ phaseTarget: "build",
35693
+ ...goal ? { phaseGoal: goal } : {}
35694
+ };
35695
+ }
35696
+ case "view-plan": {
35697
+ return { handled: true, kind: "view_plan" };
35698
+ }
35126
35699
  case "index": {
35127
35700
  return args[0] === "status" ? { handled: true, kind: "index_status" } : { handled: true, kind: "index_build" };
35128
35701
  }
@@ -35210,7 +35783,7 @@ ${formatSkillList(availableSkills)}`
35210
35783
  // src/cli/gitOps.ts
35211
35784
  import { execFile as execFile3 } from "node:child_process";
35212
35785
  import { promisify as promisify2 } from "node:util";
35213
- import path27 from "node:path";
35786
+ import path28 from "node:path";
35214
35787
  var execFileAsync2 = promisify2(execFile3);
35215
35788
  async function git2(cwd, args) {
35216
35789
  try {
@@ -35256,7 +35829,7 @@ async function undoWorkingChanges(opts = {}) {
35256
35829
  };
35257
35830
  }
35258
35831
  function defaultProjectRoot() {
35259
- return path27.resolve(__dirname, "..", "..", "..");
35832
+ return path28.resolve(__dirname, "..", "..", "..");
35260
35833
  }
35261
35834
 
35262
35835
  // src/cli/slashHandlers/git.ts
@@ -35664,15 +36237,15 @@ ${result.output.split("\n").slice(-8).join("\n")}` : "";
35664
36237
 
35665
36238
  // src/cli/slashHandlers/promoteMember.ts
35666
36239
  import { promises as fs16 } from "node:fs";
35667
- import path29 from "node:path";
36240
+ import path31 from "node:path";
35668
36241
  import os10 from "node:os";
35669
36242
  async function handlePromoteMember(ctx, memberId) {
35670
36243
  try {
35671
36244
  const { promoteMember: promoteMember2 } = await Promise.resolve().then(() => (init_council(), council_exports));
35672
36245
  const { skill, markdown } = promoteMember2(memberId);
35673
- const skillDir = process.env.ANATHEMA_SKILL_DIR ?? path29.join(os10.homedir(), ".tmp", "zelari-code", "skills");
36246
+ const skillDir = process.env.ANATHEMA_SKILL_DIR ?? path31.join(os10.homedir(), ".tmp", "zelari-code", "skills");
35674
36247
  await fs16.mkdir(skillDir, { recursive: true });
35675
- const filePath = path29.join(skillDir, `${skill.id}.md`);
36248
+ const filePath = path31.join(skillDir, `${skill.id}.md`);
35676
36249
  await fs16.writeFile(filePath, markdown, "utf8");
35677
36250
  appendSystem(
35678
36251
  ctx.setMessages,
@@ -35689,29 +36262,29 @@ async function handlePromoteMember(ctx, memberId) {
35689
36262
  }
35690
36263
 
35691
36264
  // src/cli/branchManager.ts
35692
- import { promises as fs17, existsSync as existsSync27, readFileSync as readFileSync24, writeFileSync as writeFileSync16, mkdirSync as mkdirSync11, statSync as statSync4, rmSync as rmSync2 } from "node:fs";
35693
- import path30 from "node:path";
36265
+ import { promises as fs17, existsSync as existsSync28, readFileSync as readFileSync24, writeFileSync as writeFileSync16, mkdirSync as mkdirSync11, statSync as statSync4, rmSync as rmSync2 } from "node:fs";
36266
+ import path32 from "node:path";
35694
36267
  import os11 from "node:os";
35695
36268
  var META_FILENAME = "meta.json";
35696
36269
  var SESSIONS_SUBDIR = "sessions";
35697
36270
  function getBranchesBaseDir() {
35698
- return process.env.ANATHEMA_BRANCHES_DIR ?? path30.join(os11.homedir(), ".tmp", "zelari-code", "branches");
36271
+ return process.env.ANATHEMA_BRANCHES_DIR ?? path32.join(os11.homedir(), ".tmp", "zelari-code", "branches");
35699
36272
  }
35700
36273
  function getSessionsBaseDir() {
35701
- return process.env.ANATHEMA_SESSIONS_DIR ?? path30.join(os11.homedir(), ".tmp", "zelari-code", "sessions");
36274
+ return process.env.ANATHEMA_SESSIONS_DIR ?? path32.join(os11.homedir(), ".tmp", "zelari-code", "sessions");
35702
36275
  }
35703
36276
  function branchPathFor(name, baseDir) {
35704
- return path30.join(baseDir, name);
36277
+ return path32.join(baseDir, name);
35705
36278
  }
35706
36279
  function metaPathFor(name, baseDir) {
35707
- return path30.join(baseDir, name, META_FILENAME);
36280
+ return path32.join(baseDir, name, META_FILENAME);
35708
36281
  }
35709
36282
  function sessionsPathFor(name, baseDir) {
35710
- return path30.join(baseDir, name, SESSIONS_SUBDIR);
36283
+ return path32.join(baseDir, name, SESSIONS_SUBDIR);
35711
36284
  }
35712
36285
  function readBranchMeta(name, baseDir) {
35713
36286
  const metaPath = metaPathFor(name, baseDir);
35714
- if (!existsSync27(metaPath)) {
36287
+ if (!existsSync28(metaPath)) {
35715
36288
  throw new BranchNotFoundError(`Branch "${name}" not found`);
35716
36289
  }
35717
36290
  try {
@@ -35732,7 +36305,7 @@ function readBranchMeta(name, baseDir) {
35732
36305
  }
35733
36306
  function writeBranchMeta(name, baseDir, meta3) {
35734
36307
  const metaPath = metaPathFor(name, baseDir);
35735
- mkdirSync11(path30.dirname(metaPath), { recursive: true });
36308
+ mkdirSync11(path32.dirname(metaPath), { recursive: true });
35736
36309
  writeFileSync16(metaPath, JSON.stringify(meta3, null, 2), "utf-8");
35737
36310
  }
35738
36311
  async function countSessions(name, baseDir) {
@@ -35771,7 +36344,7 @@ var SessionNotFoundError = class extends Error {
35771
36344
  };
35772
36345
  function branchExists(name, baseDir = getBranchesBaseDir()) {
35773
36346
  const bp = branchPathFor(name, baseDir);
35774
- return existsSync27(bp) && existsSync27(metaPathFor(name, baseDir));
36347
+ return existsSync28(bp) && existsSync28(metaPathFor(name, baseDir));
35775
36348
  }
35776
36349
  async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(), sessionsBaseDir = getSessionsBaseDir()) {
35777
36350
  if (!name || name.trim().length === 0) {
@@ -35783,14 +36356,14 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
35783
36356
  if (branchExists(name, baseDir)) {
35784
36357
  throw new BranchAlreadyExistsError(name);
35785
36358
  }
35786
- const sourcePath = path30.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
35787
- if (!existsSync27(sourcePath)) {
36359
+ const sourcePath = path32.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
36360
+ if (!existsSync28(sourcePath)) {
35788
36361
  throw new SessionNotFoundError(`Source session "${fromSessionId}" not found at ${sourcePath}`);
35789
36362
  }
35790
36363
  const branchPath = branchPathFor(name, baseDir);
35791
36364
  const branchSessionsPath = sessionsPathFor(name, baseDir);
35792
36365
  mkdirSync11(branchSessionsPath, { recursive: true });
35793
- const destPath = path30.join(branchSessionsPath, `${fromSessionId}.jsonl`);
36366
+ const destPath = path32.join(branchSessionsPath, `${fromSessionId}.jsonl`);
35794
36367
  await fs17.copyFile(sourcePath, destPath);
35795
36368
  const meta3 = {
35796
36369
  name,
@@ -35817,7 +36390,7 @@ async function listBranches(baseDir = getBranchesBaseDir()) {
35817
36390
  const results = [];
35818
36391
  for (const entry of entries) {
35819
36392
  const metaPath = metaPathFor(entry, baseDir);
35820
- if (!existsSync27(metaPath)) continue;
36393
+ if (!existsSync28(metaPath)) continue;
35821
36394
  try {
35822
36395
  const meta3 = readBranchMeta(entry, baseDir);
35823
36396
  const sessionCount = await countSessions(entry, baseDir);
@@ -35891,14 +36464,14 @@ async function handleBranchCheckout(ctx, branchName) {
35891
36464
 
35892
36465
  // src/cli/slashHandlers/workspace.ts
35893
36466
  import { promises as fs18 } from "node:fs";
35894
- import path31 from "node:path";
36467
+ import path33 from "node:path";
35895
36468
  async function handleWorkspaceShow(ctx, what) {
35896
36469
  try {
35897
- const zelari = path31.join(process.cwd(), ".zelari");
36470
+ const zelari = path33.join(process.cwd(), ".zelari");
35898
36471
  let content;
35899
36472
  switch (what) {
35900
36473
  case "plan": {
35901
- const planPath = path31.join(zelari, "plan.md");
36474
+ const planPath = path33.join(zelari, "plan.md");
35902
36475
  try {
35903
36476
  content = await fs18.readFile(planPath, "utf-8");
35904
36477
  } catch {
@@ -35907,7 +36480,7 @@ async function handleWorkspaceShow(ctx, what) {
35907
36480
  break;
35908
36481
  }
35909
36482
  case "decisions": {
35910
- const decisionsDir = path31.join(zelari, "decisions");
36483
+ const decisionsDir = path33.join(zelari, "decisions");
35911
36484
  try {
35912
36485
  const files = (await fs18.readdir(decisionsDir)).filter((f) => f.endsWith(".md")).sort();
35913
36486
  if (files.length === 0) {
@@ -35917,7 +36490,7 @@ async function handleWorkspaceShow(ctx, what) {
35917
36490
  `];
35918
36491
  const { parseFrontmatter: parseFrontmatter2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
35919
36492
  for (const f of files) {
35920
- const raw = await fs18.readFile(path31.join(decisionsDir, f), "utf-8");
36493
+ const raw = await fs18.readFile(path33.join(decisionsDir, f), "utf-8");
35921
36494
  const { meta: meta3, body } = parseFrontmatter2(raw);
35922
36495
  const title = meta3.title ?? body.split("\n")[0]?.replace(/^#\s*/, "").trim() ?? f;
35923
36496
  lines.push(`- **${f.replace(/\.md$/, "")}** [${meta3.status ?? "unknown"}] ${title}`);
@@ -35930,7 +36503,7 @@ async function handleWorkspaceShow(ctx, what) {
35930
36503
  break;
35931
36504
  }
35932
36505
  case "risks": {
35933
- const risksPath = path31.join(zelari, "risks.md");
36506
+ const risksPath = path33.join(zelari, "risks.md");
35934
36507
  try {
35935
36508
  content = await fs18.readFile(risksPath, "utf-8");
35936
36509
  } catch {
@@ -35939,7 +36512,7 @@ async function handleWorkspaceShow(ctx, what) {
35939
36512
  break;
35940
36513
  }
35941
36514
  case "agents": {
35942
- const agentsPath = path31.join(process.cwd(), "AGENTS.MD");
36515
+ const agentsPath = path33.join(process.cwd(), "AGENTS.MD");
35943
36516
  try {
35944
36517
  content = await fs18.readFile(agentsPath, "utf-8");
35945
36518
  } catch {
@@ -35948,7 +36521,7 @@ async function handleWorkspaceShow(ctx, what) {
35948
36521
  break;
35949
36522
  }
35950
36523
  case "docs": {
35951
- const docsDir = path31.join(zelari, "docs");
36524
+ const docsDir = path33.join(zelari, "docs");
35952
36525
  try {
35953
36526
  const files = (await fs18.readdir(docsDir)).filter((f) => f.endsWith(".md")).sort();
35954
36527
  content = files.length ? `# Docs (${files.length})
@@ -35990,7 +36563,7 @@ async function handleWorkspaceReset(ctx, force) {
35990
36563
  return;
35991
36564
  }
35992
36565
  try {
35993
- const target = path31.join(process.cwd(), ".zelari");
36566
+ const target = path33.join(process.cwd(), ".zelari");
35994
36567
  await fs18.rm(target, { recursive: true, force: true });
35995
36568
  appendSystem(ctx.setMessages, "[workspace] .zelari/ removed");
35996
36569
  } catch (err) {
@@ -36349,11 +36922,11 @@ function handleModelsRefresh(ctx) {
36349
36922
  }
36350
36923
 
36351
36924
  // src/cli/slashHandlers/skills.ts
36352
- import path32 from "node:path";
36925
+ import path34 from "node:path";
36353
36926
  import os12 from "node:os";
36354
36927
 
36355
36928
  // src/cli/skillHistory.ts
36356
- import { promises as fs19, existsSync as existsSync28, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync3, mkdirSync as mkdirSync12 } from "node:fs";
36929
+ import { promises as fs19, existsSync as existsSync29, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync3, mkdirSync as mkdirSync12 } from "node:fs";
36357
36930
  var SKILL_HISTORY_ROTATE_BYTES = 10 * 1024 * 1024;
36358
36931
  async function readSkillHistory(file2) {
36359
36932
  let raw = "";
@@ -36451,7 +37024,7 @@ async function applySteerInterrupt(options) {
36451
37024
 
36452
37025
  // src/cli/slashHandlers/skills.ts
36453
37026
  async function handleSkillStats(ctx, skillId) {
36454
- const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path32.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
37027
+ const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path34.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
36455
37028
  try {
36456
37029
  const records = await readSkillHistory(historyFile);
36457
37030
  const stats = getSkillStats(records, skillId);
@@ -36467,7 +37040,7 @@ async function handleSkillCompare(ctx, ids, fallbackMessage) {
36467
37040
  appendSystem(ctx.setMessages, fallbackMessage ?? "[skill-compare] missing args");
36468
37041
  return;
36469
37042
  }
36470
- const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path32.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
37043
+ const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path34.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
36471
37044
  try {
36472
37045
  const formatted = await compareSkillsFromFile(ids[0], ids[1], historyFile);
36473
37046
  appendSystem(ctx.setMessages, formatted);
@@ -36593,6 +37166,11 @@ function useSlashDispatch(params) {
36593
37166
  setSessionId(id);
36594
37167
  setMessages([]);
36595
37168
  setSessionActive(false);
37169
+ try {
37170
+ const { clearHistory: clearHistory2 } = await Promise.resolve().then(() => (init_conversationContext(), conversationContext_exports));
37171
+ clearHistory2();
37172
+ } catch {
37173
+ }
36596
37174
  onNewSession?.(id);
36597
37175
  }
36598
37176
  setInput("");
@@ -36825,6 +37403,40 @@ function useSlashDispatch(params) {
36825
37403
  setInput("");
36826
37404
  return;
36827
37405
  }
37406
+ if (result.kind === "phase_set" && result.phaseTarget) {
37407
+ const { setPhase: setPhase2 } = await Promise.resolve().then(() => (init_phaseState(), phaseState_exports));
37408
+ const { describePhase: describePhase2 } = await Promise.resolve().then(() => (init_phase(), phase_exports));
37409
+ setPhase2(result.phaseTarget);
37410
+ params.onPhaseChange?.(result.phaseTarget);
37411
+ appendSystem(setMessages, `[phase] ${describePhase2(result.phaseTarget)}`);
37412
+ setInput("");
37413
+ if (result.phaseGoal) {
37414
+ appendUser(setMessages, result.phaseGoal);
37415
+ setSessionActive(true);
37416
+ if (mode === "zelari") {
37417
+ await dispatchZelariPrompt(result.phaseGoal);
37418
+ } else if (mode === "council") {
37419
+ await dispatchCouncilPrompt(result.phaseGoal);
37420
+ } else {
37421
+ await dispatchPrompt(result.phaseGoal);
37422
+ }
37423
+ }
37424
+ return;
37425
+ }
37426
+ if (result.kind === "view_plan") {
37427
+ try {
37428
+ const { buildPlanSummary: buildPlanSummary2 } = await Promise.resolve().then(() => (init_workspaceSummary(), workspaceSummary_exports));
37429
+ const summary = buildPlanSummary2(process.cwd(), { userMessage: "" });
37430
+ appendSystem(
37431
+ setMessages,
37432
+ summary?.trim() ? summary : "[plan] No plan found. Run /plan <goal> or /workspace show plan after a design council."
37433
+ );
37434
+ } catch {
37435
+ appendSystem(setMessages, "[plan] Unable to read plan (workspace summary failed).");
37436
+ }
37437
+ setInput("");
37438
+ return;
37439
+ }
36828
37440
  if (result.kind === "promote_member" && result.promoteMemberId) {
36829
37441
  await handlePromoteMember(baseCtx, result.promoteMemberId);
36830
37442
  setInput("");
@@ -36856,6 +37468,11 @@ function useSlashDispatch(params) {
36856
37468
  }
36857
37469
  if (result.kind === "clear") {
36858
37470
  handleClearChat(setMessages, setSessionActive);
37471
+ try {
37472
+ const { clearHistory: clearHistory2 } = await Promise.resolve().then(() => (init_conversationContext(), conversationContext_exports));
37473
+ clearHistory2();
37474
+ } catch {
37475
+ }
36859
37476
  params.onClear?.();
36860
37477
  setInput("");
36861
37478
  return;
@@ -37008,6 +37625,7 @@ function App() {
37008
37625
  const [sessionStats, setSessionStats] = useState8({ totalTokens: 0, totalCostUsd: 0, cachedTokens: 0 });
37009
37626
  const [clearEpoch, setClearEpoch] = useState8(0);
37010
37627
  const [mode, setMode] = useState8("agent");
37628
+ const [phase2, setPhaseUi] = useState8("build");
37011
37629
  const [picker, setPicker] = useState8(null);
37012
37630
  const activeProviderSpec = getActiveProvider();
37013
37631
  const activeModel = providerConfig.modelByProvider[activeProviderSpec.id];
@@ -37086,7 +37704,8 @@ function App() {
37086
37704
  openPicker: setPicker,
37087
37705
  onNewSession,
37088
37706
  onExit,
37089
- onClear
37707
+ onClear,
37708
+ onPhaseChange: setPhaseUi
37090
37709
  });
37091
37710
  const onPickerSelect = useCallback5((value) => {
37092
37711
  if (!picker) return;
@@ -37098,15 +37717,23 @@ function App() {
37098
37717
  const cmd = `${picker.commandPrefix} ${value}`;
37099
37718
  void handleSubmit(cmd);
37100
37719
  }, [picker, handleSubmit]);
37101
- const onPickerCancel = useCallback5(() => setPicker(null), []);
37720
+ const onPickerCancel = useCallback5(() => {
37721
+ if (picker?.kind === "clarification") {
37722
+ picker.onCancel?.();
37723
+ }
37724
+ setPicker(null);
37725
+ }, [picker]);
37102
37726
  const banner = useMemo(() => {
37727
+ const left = `zelari-code \xB7 ${activeProviderSpec.id}/${activeModel}`;
37728
+ const right = `ZELARI CODE v${VERSION}`;
37729
+ const pad = Math.max(2, 72 - left.length - right.length);
37103
37730
  return {
37104
37731
  id: "banner-once",
37105
37732
  role: "system",
37106
37733
  ts: 0,
37107
- content: `zelari-code v${VERSION} \u2014 ${activeProviderSpec.id}/${activeModel}
37734
+ content: `${left}${" ".repeat(pad)}${right}
37108
37735
  cwd: ${cwd}
37109
- /help for commands \xB7 /skill <name> \xB7 shift+tab toggles agent/council`
37736
+ /help \xB7 /plan \xB7 /build \xB7 /view-plan \xB7 shift+tab mode (agent/council/zelari)`
37110
37737
  };
37111
37738
  }, [activeProviderSpec.id, activeModel, cwd]);
37112
37739
  const staticKey = `${session.sessionId || "pre-bootstrap"}-${clearEpoch}`;
@@ -37139,11 +37766,15 @@ cwd: ${cwd}
37139
37766
  queueCount: chatTurn.queueCount,
37140
37767
  busy,
37141
37768
  mode,
37769
+ phase: phase2,
37142
37770
  cwd,
37143
37771
  elapsedMs: timer.elapsedMs,
37144
37772
  lastMs: timer.lastMs,
37145
37773
  costUsd: sessionStats.totalCostUsd,
37146
- cachedTokens: sessionStats.cachedTokens
37774
+ cachedTokens: sessionStats.cachedTokens,
37775
+ contextUsed: sessionStats.totalTokens,
37776
+ contextLimit: Number(process.env.ZELARI_CONTEXT_LIMIT) || 2e5,
37777
+ brandVersion: VERSION
37147
37778
  }
37148
37779
  )), showSidebar && /* @__PURE__ */ React9.createElement(Sidebar, { version: VERSION, changes: gitChanges, rows: size.rows })));
37149
37780
  }
@@ -37329,28 +37960,28 @@ var CHOICE_LATER = "__later__";
37329
37960
  var CHOICE_NEVER = "__never__";
37330
37961
  function PluginGate({ cwd, children }) {
37331
37962
  const { isRawModeSupported } = useStdin4();
37332
- const [phase, setPhase] = useState10("detecting");
37963
+ const [phase2, setPhase2] = useState10("detecting");
37333
37964
  const [queue, setQueue] = useState10([]);
37334
37965
  const [current, setCurrent] = useState10(null);
37335
37966
  const [result, setResult] = useState10(null);
37336
37967
  const skipGate = process.env.ZELARI_NO_PLUGIN_PROMPT === "1" || isRawModeSupported !== true;
37337
37968
  useEffect9(() => {
37338
37969
  if (skipGate) {
37339
- setPhase("done");
37970
+ setPhase2("done");
37340
37971
  return;
37341
37972
  }
37342
37973
  let cancelled = false;
37343
37974
  void detectMissingPlugins(cwd).then((missing) => {
37344
37975
  if (cancelled) return;
37345
37976
  if (missing.length === 0) {
37346
- setPhase("done");
37977
+ setPhase2("done");
37347
37978
  return;
37348
37979
  }
37349
37980
  setQueue(missing);
37350
37981
  setCurrent(missing[0] ?? null);
37351
- setPhase(missing[0] ? "prompting" : "done");
37982
+ setPhase2(missing[0] ? "prompting" : "done");
37352
37983
  }).catch(() => {
37353
- if (!cancelled) setPhase("done");
37984
+ if (!cancelled) setPhase2("done");
37354
37985
  });
37355
37986
  return () => {
37356
37987
  cancelled = true;
@@ -37360,12 +37991,12 @@ function PluginGate({ cwd, children }) {
37360
37991
  setQueue((q) => {
37361
37992
  const rest = q.slice(1);
37362
37993
  if (rest.length === 0) {
37363
- setPhase("done");
37994
+ setPhase2("done");
37364
37995
  setCurrent(null);
37365
37996
  return rest;
37366
37997
  }
37367
37998
  setCurrent(rest[0] ?? null);
37368
- setPhase(rest[0] ? "prompting" : "done");
37999
+ setPhase2(rest[0] ? "prompting" : "done");
37369
38000
  setResult(null);
37370
38001
  return rest;
37371
38002
  });
@@ -37379,31 +38010,49 @@ function PluginGate({ cwd, children }) {
37379
38010
  markDontAskAgain(current.id);
37380
38011
  advance();
37381
38012
  } else if (value === CHOICE_INSTALL) {
37382
- setPhase("installing");
37383
- void installPlugin(current, cwd).then((r) => {
38013
+ setPhase2("installing");
38014
+ void installPlugin(current, cwd).then(async (r) => {
38015
+ if (r.ok) {
38016
+ let present = false;
38017
+ try {
38018
+ present = await current.detect(cwd);
38019
+ } catch {
38020
+ present = false;
38021
+ }
38022
+ if (!present) {
38023
+ setResult({
38024
+ ok: false,
38025
+ output: r.output,
38026
+ exitCode: r.exitCode,
38027
+ error: `npm reported success but ${current.label} is still not detectable from this workspace. Try \`/plugins install ${current.id}\` or see the post-install hint.`
38028
+ });
38029
+ setPhase2("result");
38030
+ return;
38031
+ }
38032
+ }
37384
38033
  setResult(r);
37385
- setPhase("result");
38034
+ setPhase2("result");
37386
38035
  }).catch(() => {
37387
38036
  setResult({ ok: false, output: "", exitCode: null, error: "unexpected error" });
37388
- setPhase("result");
38037
+ setPhase2("result");
37389
38038
  });
37390
38039
  }
37391
38040
  },
37392
38041
  [current, cwd, advance]
37393
38042
  );
37394
- if (phase === "done") {
38043
+ if (phase2 === "done") {
37395
38044
  return /* @__PURE__ */ React11.createElement(React11.Fragment, null, children);
37396
38045
  }
37397
- if (phase === "detecting") {
38046
+ if (phase2 === "detecting") {
37398
38047
  return /* @__PURE__ */ React11.createElement(Box10, { paddingX: 1 }, /* @__PURE__ */ React11.createElement(Text11, { dimColor: true }, "Checking for optional tool plugins\u2026"));
37399
38048
  }
37400
38049
  if (!current) {
37401
38050
  return /* @__PURE__ */ React11.createElement(React11.Fragment, null, children);
37402
38051
  }
37403
- if (phase === "installing") {
38052
+ if (phase2 === "installing") {
37404
38053
  return /* @__PURE__ */ React11.createElement(Box10, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React11.createElement(Text11, { color: "cyan" }, "\u23F3 Installing ", current.label, "\u2026"), /* @__PURE__ */ React11.createElement(Text11, { dimColor: true }, "npm install ", current.installScope === "global" ? "-g" : "-D", " ", current.npmPackage));
37405
38054
  }
37406
- if (phase === "result") {
38055
+ if (phase2 === "result") {
37407
38056
  const ok = result?.ok === true;
37408
38057
  return /* @__PURE__ */ React11.createElement(Box10, { flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React11.createElement(Text11, { color: ok ? "green" : "red" }, ok ? "\u2713" : "\u2717", " ", ok ? "Installed" : "Install failed", ": ", current.label), !ok && result?.error ? /* @__PURE__ */ React11.createElement(Text11, { dimColor: true }, result.error) : null, ok && current.postInstallHint ? /* @__PURE__ */ React11.createElement(Text11, { color: "yellow" }, " \u2192 ", current.postInstallHint) : null, /* @__PURE__ */ React11.createElement(Text11, { dimColor: true }, "Press any key to continue\u2026"), /* @__PURE__ */ React11.createElement(ContinueKey, { onContinue: advance }));
37409
38058
  }
@@ -37441,9 +38090,9 @@ function ContinueKey({ onContinue }) {
37441
38090
  init_providerConfig();
37442
38091
 
37443
38092
  // src/cli/wizard/firstRun.ts
37444
- import { existsSync as existsSync29 } from "node:fs";
38093
+ import { existsSync as existsSync30 } from "node:fs";
37445
38094
  function shouldRunWizard(input) {
37446
- const exists = input.exists ?? existsSync29;
38095
+ const exists = input.exists ?? existsSync30;
37447
38096
  if (input.hasResetConfigFlag) {
37448
38097
  return { shouldRun: true, reason: "--reset-config flag forced wizard" };
37449
38098
  }
@@ -37801,6 +38450,7 @@ function emitEvent(event) {
37801
38450
  init_harness();
37802
38451
  init_toolRegistry();
37803
38452
  init_skills2();
38453
+ init_envNumber();
37804
38454
  async function runHeadless(opts) {
37805
38455
  const { provider, model } = resolveHeadlessProvider(opts);
37806
38456
  const key = await resolveHeadlessKey(provider);
@@ -37989,7 +38639,7 @@ async function runHeadlessCouncil(opts, provider, model, providerStream) {
37989
38639
 
37990
38640
  // src/cli/skillsMd.ts
37991
38641
  init_skills2();
37992
- import { existsSync as existsSync30, readdirSync as readdirSync4, readFileSync as readFileSync25 } from "node:fs";
38642
+ import { existsSync as existsSync31, readdirSync as readdirSync4, readFileSync as readFileSync25 } from "node:fs";
37993
38643
  import { join as join23 } from "node:path";
37994
38644
  import { homedir as homedir6 } from "node:os";
37995
38645
  var CODING_CATEGORIES = /* @__PURE__ */ new Set([
@@ -38067,7 +38717,7 @@ function loadSkillMdSkills(projectRoot = process.cwd(), options = {}) {
38067
38717
  const summary = { loaded: [], skipped: [] };
38068
38718
  const seen = new Set(options.existingIds ?? []);
38069
38719
  for (const dir of skillMdSearchDirs(projectRoot)) {
38070
- if (!existsSync30(dir)) continue;
38720
+ if (!existsSync31(dir)) continue;
38071
38721
  let entries;
38072
38722
  try {
38073
38723
  entries = readdirSync4(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
@@ -38076,7 +38726,7 @@ function loadSkillMdSkills(projectRoot = process.cwd(), options = {}) {
38076
38726
  }
38077
38727
  for (const entry of entries) {
38078
38728
  const skillPath = join23(dir, entry, "SKILL.md");
38079
- if (!existsSync30(skillPath)) continue;
38729
+ if (!existsSync31(skillPath)) continue;
38080
38730
  try {
38081
38731
  const parsed = parseSkillMd(readFileSync25(skillPath, "utf8"), skillPath);
38082
38732
  if (!parsed) {