zelari-code 1.7.0 → 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 (38) 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 +1176 -430
  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/dist/cli/utils/prereqChecks.js +96 -26
  37. package/dist/cli/utils/prereqChecks.js.map +1 -1
  38. 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
  }
@@ -16576,6 +16576,28 @@ var init_filesystem = __esm({
16576
16576
  // packages/core/dist/core/tools/builtin/shellResolver.js
16577
16577
  import { existsSync as existsSync4 } from "node:fs";
16578
16578
  import { spawnSync } from "node:child_process";
16579
+ function isWslBashPath(p3) {
16580
+ if (!p3 || typeof p3 !== "string")
16581
+ return false;
16582
+ const n = p3.replace(/\//g, "\\").toLowerCase();
16583
+ if (n.includes("\\windows\\system32\\bash.exe"))
16584
+ return true;
16585
+ if (n.includes("\\windows\\syswow64\\bash.exe"))
16586
+ return true;
16587
+ if (n.includes("\\windowsapps\\bash.exe"))
16588
+ return true;
16589
+ return false;
16590
+ }
16591
+ function acceptBashPath(p3) {
16592
+ if (!p3 || p3.trim().length === 0)
16593
+ return null;
16594
+ const trimmed = p3.trim();
16595
+ if (isWslBashPath(trimmed))
16596
+ return null;
16597
+ if (!existsSyncSafe(trimmed))
16598
+ return null;
16599
+ return trimmed;
16600
+ }
16579
16601
  function resolveShell(forceReResolve = false) {
16580
16602
  if (memoized && !forceReResolve)
16581
16603
  return memoized;
@@ -16596,24 +16618,25 @@ function resolveShell(forceReResolve = false) {
16596
16618
  return memoized;
16597
16619
  }
16598
16620
  function resolveBashWindows() {
16599
- const envShell = process.env.ZELARI_SHELL;
16600
- if (envShell && envShell.trim().length > 0 && existsSyncSafe(envShell)) {
16601
- return envShell;
16602
- }
16603
- const sessionShell = process.env.SHELL;
16604
- if (sessionShell && sessionShell.trim().length > 0 && existsSyncSafe(sessionShell)) {
16605
- return sessionShell;
16606
- }
16621
+ const fromEnv = acceptBashPath(process.env.ZELARI_SHELL);
16622
+ if (fromEnv)
16623
+ return fromEnv;
16624
+ const fromSession = acceptBashPath(process.env.SHELL);
16625
+ if (fromSession)
16626
+ return fromSession;
16607
16627
  for (const p3 of STANDARD_BASH_PATHS) {
16608
- if (existsSyncSafe(p3))
16609
- return p3;
16628
+ const accepted = acceptBashPath(p3);
16629
+ if (accepted)
16630
+ return accepted;
16610
16631
  }
16611
16632
  try {
16612
16633
  const result = spawnSync("where", ["bash"], { encoding: "utf-8", windowsHide: true });
16613
16634
  if (result.status === 0 && result.stdout) {
16614
- const first = result.stdout.split(/\r?\n/).find((l) => l.trim().length > 0);
16615
- if (first && existsSyncSafe(first))
16616
- return first.trim();
16635
+ for (const line of result.stdout.split(/\r?\n/)) {
16636
+ const accepted = acceptBashPath(line);
16637
+ if (accepted)
16638
+ return accepted;
16639
+ }
16617
16640
  }
16618
16641
  } catch {
16619
16642
  }
@@ -16643,6 +16666,24 @@ var init_shellResolver = __esm({
16643
16666
 
16644
16667
  // packages/core/dist/core/tools/builtin/shell.js
16645
16668
  import { spawn } from "node:child_process";
16669
+ import { dirname } from "node:path";
16670
+ function withNodeDirOnPath(base) {
16671
+ const env = { ...base };
16672
+ try {
16673
+ const nodeDir = dirname(process.execPath);
16674
+ if (!nodeDir)
16675
+ return env;
16676
+ const sep = process.platform === "win32" ? ";" : ":";
16677
+ const current = env.PATH ?? env.Path ?? "";
16678
+ const parts = current.split(sep).filter((p3) => p3.length > 0);
16679
+ const has = parts.some((p3) => p3.toLowerCase() === nodeDir.toLowerCase());
16680
+ if (!has) {
16681
+ env.PATH = `${nodeDir}${sep}${current}`;
16682
+ }
16683
+ } catch {
16684
+ }
16685
+ return env;
16686
+ }
16646
16687
  var BashArgsSchema, INTERACTIVE_CANCEL_RE, INTERACTIVE_HINT, bashTool;
16647
16688
  var init_shell = __esm({
16648
16689
  "packages/core/dist/core/tools/builtin/shell.js"() {
@@ -16669,7 +16710,10 @@ var init_shell = __esm({
16669
16710
  const cwd = args.cwd ?? ctx.cwd;
16670
16711
  const resolved = resolveShell();
16671
16712
  let child;
16672
- const baseEnv = { ...process.env, CI: process.env.CI ?? "1" };
16713
+ const baseEnv = withNodeDirOnPath({
16714
+ ...process.env,
16715
+ CI: process.env.CI ?? "1"
16716
+ });
16673
16717
  const env = resolved.isBash ? { ...baseEnv, MSYSTEM: process.env.MSYSTEM ?? "MINGW64" } : baseEnv;
16674
16718
  if (resolved.isBash) {
16675
16719
  child = spawn(resolved.shell, ["-c", args.command], {
@@ -17709,11 +17753,11 @@ var init_tools = __esm({
17709
17753
  if (!ctx.addDocument)
17710
17754
  return "Knowledge vault tool not available.";
17711
17755
  const title = args["title"] || "New Document";
17712
- 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, "-")}`;
17713
17757
  const content = args["content"] || "";
17714
17758
  const tags = args["tags"] || [];
17715
17759
  ctx.addDocument({
17716
- path: path34,
17760
+ path: path36,
17717
17761
  title,
17718
17762
  content,
17719
17763
  format: "markdown",
@@ -17722,7 +17766,7 @@ var init_tools = __esm({
17722
17766
  workspaceId: ctx.workspaceId
17723
17767
  });
17724
17768
  ctx.addActivity("vault", "created document", title);
17725
- return `Document "${title}" created at "${path34}".`;
17769
+ return `Document "${title}" created at "${path36}".`;
17726
17770
  }
17727
17771
  }
17728
17772
  ];
@@ -18993,6 +19037,145 @@ var init_AgentHarness = __esm({
18993
19037
  ...this.config.memberName ? { memberName: this.config.memberName } : {}
18994
19038
  };
18995
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
+ }
18996
19179
  /**
18997
19180
  * Cancel the in-flight run. Events drain until end of stream.
18998
19181
  *
@@ -19210,6 +19393,7 @@ var init_AgentHarness = __esm({
19210
19393
  let turnText = "";
19211
19394
  const turnToolCalls = [];
19212
19395
  const turnToolResults = [];
19396
+ const pendingNativeTools = [];
19213
19397
  for await (const delta of stream) {
19214
19398
  if (this.cancelled) {
19215
19399
  const cancelEvent = createBrainEvent("error", this.sessionId, {
@@ -19244,73 +19428,52 @@ var init_AgentHarness = __esm({
19244
19428
  this.emit(toolStartEvent);
19245
19429
  yield toolStartEvent;
19246
19430
  const skipped = typeof maxToolCalls === "number" && toolCallsThisTurn > maxToolCalls;
19247
- 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 {
19248
19440
  const callKey = hashToolCall(delta.toolName, delta.args);
19249
19441
  const cached2 = this.toolCallCache.get(callKey);
19250
19442
  if (cached2 !== void 0) {
19251
- const dupResult = `[duplicate call \u2014 result repeated; do not call this tool again with the same arguments]
19252
- ${cached2}`;
19253
- const endEvent = createBrainEvent("tool_execution_end", this.sessionId, {
19254
- toolCallId: delta.toolCallId,
19255
- result: dupResult,
19256
- isError: false,
19257
- durationMs: 0
19258
- });
19259
- this.emit(endEvent);
19260
- yield endEvent;
19261
- turnToolResults.push({
19443
+ pendingNativeTools.push({
19262
19444
  toolCallId: delta.toolCallId,
19263
- 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}`
19264
19450
  });
19265
19451
  } else {
19266
- const startMs = Date.now();
19267
- const result = await this.config.toolRegistry.invoke(delta.toolName, delta.args, {
19268
- cwd: this.config.cwd,
19269
- sessionId: this.sessionId,
19270
- signal: this.activeController?.signal
19271
- });
19272
- let resultStr = "";
19273
- if (result.ok) {
19274
- if (typeof result.value === "string") {
19275
- resultStr = result.value;
19276
- } else if (typeof result.value === "object" && result.value !== null) {
19277
- resultStr = JSON.stringify(result.value, null, 2);
19278
- } else {
19279
- resultStr = String(result.value);
19280
- }
19281
- } else {
19282
- resultStr = result.error;
19283
- }
19284
- const endEvent = createBrainEvent("tool_execution_end", this.sessionId, {
19452
+ pendingNativeTools.push({
19285
19453
  toolCallId: delta.toolCallId,
19286
- result: resultStr,
19287
- isError: !result.ok,
19288
- durationMs: Date.now() - startMs
19289
- });
19290
- this.emit(endEvent);
19291
- yield endEvent;
19292
- turnToolResults.push({
19293
- toolCallId: delta.toolCallId,
19294
- content: resultStr
19454
+ toolName: delta.toolName,
19455
+ args: delta.args,
19456
+ skipped: false
19295
19457
  });
19296
- this.toolCallCache.set(callKey, resultStr);
19297
19458
  }
19298
- } else if (skipped) {
19299
- const endEvent = createBrainEvent("tool_execution_end", this.sessionId, {
19300
- toolCallId: delta.toolCallId,
19301
- result: `[skipped] maxToolCallsPerTurn reached (limit=${maxToolCalls})`,
19302
- isError: true,
19303
- durationMs: 0
19304
- });
19305
- this.emit(endEvent);
19306
- yield endEvent;
19307
- turnToolResults.push({
19308
- toolCallId: delta.toolCallId,
19309
- content: `[skipped] maxToolCallsPerTurn reached (limit=${maxToolCalls})`
19310
- });
19311
19459
  }
19312
19460
  } else if (delta.kind === "finish") {
19313
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
+ }
19314
19477
  const textTools = parseTextToolCalls(turnText);
19315
19478
  const nativeWriteRan = turnToolCalls.some((t) => t.name === "write_file" || t.name === "edit_file");
19316
19479
  const textWriteTools = textTools.filter((t) => t.name === "write_file" || t.name === "edit_file");
@@ -21661,12 +21824,44 @@ var init_tools4 = __esm({
21661
21824
  });
21662
21825
 
21663
21826
  // src/cli/browser/driver.ts
21664
- 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) {
21665
21859
  const consoleErrors = [];
21666
21860
  const pageErrors = [];
21667
21861
  const failedRequests = [];
21668
21862
  const base = { ok: false, consoleErrors, pageErrors, failedRequests };
21669
- const pw = await loader();
21863
+ const resolve = loader ?? (() => loadPlaywright(options.cwd ?? process.cwd()));
21864
+ const pw = await resolve();
21670
21865
  if (!pw) {
21671
21866
  return {
21672
21867
  ...base,
@@ -21742,25 +21937,14 @@ async function runBrowserCheck(options, loader = defaultPlaywrightLoader) {
21742
21937
  }
21743
21938
  }
21744
21939
  }
21745
- var defaultPlaywrightLoader;
21746
21940
  var init_driver = __esm({
21747
21941
  "src/cli/browser/driver.ts"() {
21748
21942
  "use strict";
21749
- defaultPlaywrightLoader = async () => {
21750
- try {
21751
- const pkg = "playwright";
21752
- const mod = await import(pkg);
21753
- if (mod && mod.chromium && typeof mod.chromium.launch === "function") return mod;
21754
- return null;
21755
- } catch {
21756
- return null;
21757
- }
21758
- };
21759
21943
  }
21760
21944
  });
21761
21945
 
21762
21946
  // src/cli/browser/tools.ts
21763
- import path21 from "node:path";
21947
+ import path22 from "node:path";
21764
21948
  import os7 from "node:os";
21765
21949
  function createBrowserTool(deps = {}) {
21766
21950
  return {
@@ -21775,13 +21959,14 @@ function createBrowserTool(deps = {}) {
21775
21959
  waitForSelector: external_exports.string().optional().describe("Assert this CSS selector is present after actions."),
21776
21960
  screenshot: external_exports.boolean().optional().describe("Save a screenshot (default true).")
21777
21961
  }),
21778
- execute: async (args) => {
21962
+ execute: async (args, ctx) => {
21779
21963
  const a = args;
21780
21964
  const dir = deps.screenshotDir ?? os7.tmpdir();
21781
- 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`);
21782
21966
  const result = await runBrowserCheck(
21783
21967
  {
21784
21968
  url: a.url,
21969
+ cwd: ctx.cwd,
21785
21970
  ...a.actions ? { actions: a.actions } : {},
21786
21971
  ...a.waitForSelector ? { waitForSelector: a.waitForSelector } : {},
21787
21972
  ...screenshotPath ? { screenshotPath } : {}
@@ -21846,7 +22031,7 @@ function createBuiltinToolRegistry(options = {}) {
21846
22031
  const safeFetchUrl = wrapWithAudit(fetchUrlTool, audit, sessionId);
21847
22032
  const safeWebSearch = wrapWithAudit(webSearchTool, audit, sessionId);
21848
22033
  const registry3 = new ToolRegistry();
21849
- const readOnly = options.readOnly === true;
22034
+ const readOnly = options.readOnly === true || options.planMode === true;
21850
22035
  registry3.register(safeReadFile);
21851
22036
  registry3.register(safeGrepContent);
21852
22037
  registry3.register(safeListFiles);
@@ -23003,11 +23188,11 @@ var init_synthesisAudit = __esm({
23003
23188
  import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
23004
23189
  import { join as join2 } from "node:path";
23005
23190
  function loadNfrSpec(zelariRoot) {
23006
- const path34 = join2(zelariRoot, "nfr-spec.json");
23007
- if (!existsSync10(path34))
23191
+ const path36 = join2(zelariRoot, "nfr-spec.json");
23192
+ if (!existsSync10(path36))
23008
23193
  return null;
23009
23194
  try {
23010
- const raw = JSON.parse(readFileSync8(path34, "utf8"));
23195
+ const raw = JSON.parse(readFileSync8(path36, "utf8"));
23011
23196
  if (raw.version !== 1 || !Array.isArray(raw.targets))
23012
23197
  return null;
23013
23198
  return raw;
@@ -23991,6 +24176,19 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
23991
24176
  role: "system",
23992
24177
  content: `(Agent ${agent.name} asked: ${clarification.question})`
23993
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
+ }
23994
24192
  }
23995
24193
  }
23996
24194
  if (oracle && !completedIds.has(oracle.id)) {
@@ -25219,9 +25417,9 @@ var init_types2 = __esm({
25219
25417
  import { readFileSync as readFileSync13 } from "node:fs";
25220
25418
  import { join as join8 } from "node:path";
25221
25419
  function readLessonsDeduped(zelariRoot) {
25222
- const path34 = join8(zelariRoot, LESSONS_FILE);
25420
+ const path36 = join8(zelariRoot, LESSONS_FILE);
25223
25421
  try {
25224
- const raw = readFileSync13(path34, "utf8");
25422
+ const raw = readFileSync13(path36, "utf8");
25225
25423
  const byId = /* @__PURE__ */ new Map();
25226
25424
  for (const line of raw.split(/\r?\n/)) {
25227
25425
  if (!line.trim())
@@ -25322,8 +25520,8 @@ function keywordsFrom(check2, signature) {
25322
25520
  return [.../* @__PURE__ */ new Set([...fromId, ...words])].slice(0, 12);
25323
25521
  }
25324
25522
  function writeLesson(zelariRoot, lesson) {
25325
- const path34 = join9(zelariRoot, LESSONS_FILE);
25326
- appendFileSync2(path34, `${JSON.stringify(lesson)}
25523
+ const path36 = join9(zelariRoot, LESSONS_FILE);
25524
+ appendFileSync2(path36, `${JSON.stringify(lesson)}
25327
25525
  `, "utf8");
25328
25526
  }
25329
25527
  function findSimilar(lessons, signature) {
@@ -25857,6 +26055,273 @@ var init_council = __esm({
25857
26055
  }
25858
26056
  });
25859
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
+
25860
26325
  // src/cli/workspace/paths.ts
25861
26326
  import {
25862
26327
  mkdirSync as mkdirSync6,
@@ -26038,12 +26503,12 @@ function buildPlanSummary(projectRoot = process.cwd(), options) {
26038
26503
  scopedOpen = [...inScope, ...neutral];
26039
26504
  }
26040
26505
  }
26041
- for (const phase of [...phases].sort(
26506
+ for (const phase2 of [...phases].sort(
26042
26507
  (a, b) => (a.order ?? 0) - (b.order ?? 0)
26043
26508
  )) {
26044
- const phaseTasks = scopedOpen.filter((t) => t.phaseId === phase.id);
26045
- parts.push("", `## ${phase.order ?? "?"}. ${phase.name ?? phase.id}`);
26046
- 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);
26047
26512
  for (const t of phaseTasks.slice(0, PLAN_SUMMARY_MAX_TASKS)) {
26048
26513
  parts.push(formatTaskLine(t));
26049
26514
  }
@@ -26210,7 +26675,7 @@ import {
26210
26675
  readdirSync as readdirSync2,
26211
26676
  renameSync as renameSync2
26212
26677
  } from "node:fs";
26213
- import { dirname, join as join13 } from "node:path";
26678
+ import { dirname as dirname2, join as join13 } from "node:path";
26214
26679
  function parseFrontmatter(md) {
26215
26680
  const m = FRONTMATTER_RE.exec(md);
26216
26681
  if (!m) return { meta: {}, body: md };
@@ -26464,28 +26929,28 @@ var init_storage = __esm({
26464
26929
  VALID_SCALARS = /^(true|false|null|~)$/i;
26465
26930
  Storage = class {
26466
26931
  /** Read a Markdown file with frontmatter. Throws if not found. */
26467
- read(path34) {
26468
- if (!existsSync15(path34)) {
26469
- throw new Error(`File not found: ${path34}`);
26932
+ read(path36) {
26933
+ if (!existsSync15(path36)) {
26934
+ throw new Error(`File not found: ${path36}`);
26470
26935
  }
26471
- const md = readFileSync15(path34, "utf8");
26936
+ const md = readFileSync15(path36, "utf8");
26472
26937
  return parseFrontmatter(md);
26473
26938
  }
26474
26939
  /** Read a Markdown file; returns null if not found. */
26475
- readIfExists(path34) {
26476
- if (!existsSync15(path34)) return null;
26477
- return this.read(path34);
26940
+ readIfExists(path36) {
26941
+ if (!existsSync15(path36)) return null;
26942
+ return this.read(path36);
26478
26943
  }
26479
26944
  /**
26480
26945
  * Write a Markdown file atomically (tmp + rename). Creates parent dirs.
26481
26946
  * The meta object is serialized as YAML frontmatter; body as Markdown.
26482
26947
  */
26483
- write(path34, meta3, body) {
26484
- mkdirSync7(dirname(path34), { recursive: true });
26485
- const tmp = path34 + ".tmp-" + process.pid;
26948
+ write(path36, meta3, body) {
26949
+ mkdirSync7(dirname2(path36), { recursive: true });
26950
+ const tmp = path36 + ".tmp-" + process.pid;
26486
26951
  const md = serializeFrontmatter(meta3, body);
26487
26952
  writeFileSync11(tmp, md, "utf8");
26488
- renameSync2(tmp, path34);
26953
+ renameSync2(tmp, path36);
26489
26954
  }
26490
26955
  /** List all .md files in a directory (non-recursive). */
26491
26956
  listMarkdown(dir) {
@@ -26536,7 +27001,7 @@ import {
26536
27001
  mkdirSync as mkdirSync8,
26537
27002
  renameSync as renameSync3
26538
27003
  } from "node:fs";
26539
- import { join as join14, basename as basename2, dirname as dirname2, relative as relative2 } from "node:path";
27004
+ import { join as join14, basename as basename2, dirname as dirname3, relative as relative2 } from "node:path";
26540
27005
  function createWorkspaceContext(projectRoot = process.cwd()) {
26541
27006
  const rootDir = resolveWorkspaceRoot(projectRoot);
26542
27007
  return {
@@ -26563,8 +27028,8 @@ function readPlan(ctx) {
26563
27028
  } catch {
26564
27029
  }
26565
27030
  }
26566
- const path34 = workspaceFile(ctx.rootDir, "plan");
26567
- const doc = ctx.storage.readIfExists(path34);
27031
+ const path36 = workspaceFile(ctx.rootDir, "plan");
27032
+ const doc = ctx.storage.readIfExists(path36);
26568
27033
  if (!doc) return { phases: [], tasks: [], milestones: [] };
26569
27034
  const meta3 = doc.meta;
26570
27035
  return {
@@ -26575,7 +27040,7 @@ function readPlan(ctx) {
26575
27040
  }
26576
27041
  function writePlan(ctx, summary) {
26577
27042
  const jsonPath = planJsonPath(ctx);
26578
- mkdirSync8(dirname2(jsonPath), { recursive: true });
27043
+ mkdirSync8(dirname3(jsonPath), { recursive: true });
26579
27044
  const tmp = jsonPath + ".tmp-" + process.pid;
26580
27045
  writeFileSync12(tmp, JSON.stringify(summary, null, 2), "utf8");
26581
27046
  renameSync3(tmp, jsonPath);
@@ -26598,11 +27063,11 @@ function renderPlanBody(summary) {
26598
27063
  if (summary.phases.length === 0) {
26599
27064
  lines.push(`_(none yet)_`);
26600
27065
  } else {
26601
- for (const phase of [...summary.phases].sort(
27066
+ for (const phase2 of [...summary.phases].sort(
26602
27067
  (a, b) => (a.order ?? 0) - (b.order ?? 0)
26603
27068
  )) {
26604
27069
  lines.push(
26605
- `### ${phase.order ?? "?"}. ${phase.id} \`${phase.color ?? ""}\``
27070
+ `### ${phase2.order ?? "?"}. ${phase2.id} \`${phase2.color ?? ""}\``
26606
27071
  );
26607
27072
  lines.push("");
26608
27073
  }
@@ -26729,7 +27194,7 @@ function addMilestoneRecord(ctx, summary, input) {
26729
27194
  dueDate: input.dueDate,
26730
27195
  targetVersion: version2
26731
27196
  });
26732
- const path34 = join14(ctx.rootDir, "milestones", `${id}.md`);
27197
+ const path36 = join14(ctx.rootDir, "milestones", `${id}.md`);
26733
27198
  const meta3 = {
26734
27199
  kind: "milestone",
26735
27200
  id,
@@ -26746,7 +27211,7 @@ function addMilestoneRecord(ctx, summary, input) {
26746
27211
  `Target version: ${version2}`,
26747
27212
  ""
26748
27213
  ].join("\n");
26749
- ctx.storage.write(path34, meta3, body);
27214
+ ctx.storage.write(path36, meta3, body);
26750
27215
  return { id, created: true };
26751
27216
  }
26752
27217
  function readPlanSummary(ctx) {
@@ -26950,7 +27415,7 @@ function addIdeaStub(ctx) {
26950
27415
  const tags = args["tags"] ?? [];
26951
27416
  const category = args["category"] ?? "General";
26952
27417
  const id = `${nextAdrId(ctx)}-${slugify3(title)}`;
26953
- const path34 = workspaceArtifact(ctx.rootDir, "decisions", id);
27418
+ const path36 = workspaceArtifact(ctx.rootDir, "decisions", id);
26954
27419
  const meta3 = {
26955
27420
  kind: "adr",
26956
27421
  status: "proposed",
@@ -26976,7 +27441,7 @@ function addIdeaStub(ctx) {
26976
27441
  ...consequences.map((c) => `- ${c}`),
26977
27442
  ""
26978
27443
  ].join("\n");
26979
- ctx.storage.write(path34, meta3, body);
27444
+ ctx.storage.write(path36, meta3, body);
26980
27445
  return `ADR ${id} created: "${title}". Status: proposed. Promote to accepted via /update ADR or manual edit.`;
26981
27446
  });
26982
27447
  }
@@ -27058,14 +27523,14 @@ function createDocumentStub(ctx) {
27058
27523
  ctx.storage.write(risksPath, riskMeta, content);
27059
27524
  return `Document "${title}" created at risks.md (workspace root).`;
27060
27525
  }
27061
- const path34 = workspaceArtifact(ctx.rootDir, "docs", slug);
27526
+ const path36 = workspaceArtifact(ctx.rootDir, "docs", slug);
27062
27527
  const meta3 = {
27063
27528
  kind: "doc",
27064
27529
  id: slug,
27065
27530
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
27066
27531
  tags
27067
27532
  };
27068
- ctx.storage.write(path34, meta3, content);
27533
+ ctx.storage.write(path36, meta3, content);
27069
27534
  return `Document "${title}" created at docs/${slug}.md.`;
27070
27535
  });
27071
27536
  }
@@ -27281,18 +27746,18 @@ __export(updater_exports, {
27281
27746
  performUpdate: () => performUpdate,
27282
27747
  resolveBundledNpmCli: () => resolveBundledNpmCli
27283
27748
  });
27284
- import { createRequire } from "node:module";
27749
+ import { createRequire as createRequire2 } from "node:module";
27285
27750
  import { spawn as spawn4 } from "node:child_process";
27286
27751
  import { existsSync as existsSync17 } from "node:fs";
27287
- import path22 from "node:path";
27752
+ import path23 from "node:path";
27288
27753
  import { fileURLToPath } from "node:url";
27289
27754
  function resolveBundledNpmCli(execPath = process.execPath) {
27290
- const dir = path22.dirname(execPath);
27755
+ const dir = path23.dirname(execPath);
27291
27756
  const candidates = [
27292
27757
  // Windows: C:\...\node.exe → C:\...\node_modules\npm\bin\npm-cli.js
27293
- path22.join(dir, "node_modules", "npm", "bin", "npm-cli.js"),
27758
+ path23.join(dir, "node_modules", "npm", "bin", "npm-cli.js"),
27294
27759
  // POSIX: <prefix>/bin/node → <prefix>/lib/node_modules/npm/bin/npm-cli.js
27295
- path22.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
27760
+ path23.join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")
27296
27761
  ];
27297
27762
  for (const candidate of candidates) {
27298
27763
  try {
@@ -27309,7 +27774,7 @@ function looksLikeBrokenShim(exitCode, output) {
27309
27774
  }
27310
27775
  function getCurrentVersion() {
27311
27776
  try {
27312
- const pkgPath = path22.resolve(__dirname2, "..", "..", "package.json");
27777
+ const pkgPath = path23.resolve(__dirname2, "..", "..", "package.json");
27313
27778
  const pkg = require2(pkgPath);
27314
27779
  return pkg.version;
27315
27780
  } catch {
@@ -27420,8 +27885,8 @@ var init_updater = __esm({
27420
27885
  "src/cli/updater.ts"() {
27421
27886
  "use strict";
27422
27887
  init_cmdline();
27423
- require2 = createRequire(import.meta.url);
27424
- __dirname2 = path22.dirname(fileURLToPath(import.meta.url));
27888
+ require2 = createRequire2(import.meta.url);
27889
+ __dirname2 = path23.dirname(fileURLToPath(import.meta.url));
27425
27890
  REGISTRY_URL = "https://registry.npmjs.org/zelari-code/latest";
27426
27891
  }
27427
27892
  });
@@ -27826,6 +28291,9 @@ async function* dispatchCouncil(userMessage, options) {
27826
28291
  if (options.onCouncilStatus) {
27827
28292
  callbacks.onCouncilStatus = options.onCouncilStatus;
27828
28293
  }
28294
+ if (options.onClarification) {
28295
+ callbacks.onClarification = options.onClarification;
28296
+ }
27829
28297
  yield* runCouncilPure(userMessage, config2, callbacks);
27830
28298
  }
27831
28299
  var CouncilDispatchError;
@@ -27859,10 +28327,10 @@ import { createHash as createHash3 } from "node:crypto";
27859
28327
  import { join as join17 } from "node:path";
27860
28328
  import { readFile as readFile2 } from "node:fs/promises";
27861
28329
  async function readPackageJson2(projectRoot) {
27862
- const path34 = join17(projectRoot, "package.json");
27863
- if (!existsSync20(path34)) return null;
28330
+ const path36 = join17(projectRoot, "package.json");
28331
+ if (!existsSync20(path36)) return null;
27864
28332
  try {
27865
- return JSON.parse(await readFile2(path34, "utf8"));
28333
+ return JSON.parse(await readFile2(path36, "utf8"));
27866
28334
  } catch {
27867
28335
  return null;
27868
28336
  }
@@ -27944,9 +28412,9 @@ async function genBuild(ctx) {
27944
28412
  ].join("\n");
27945
28413
  }
27946
28414
  async function genOpenQuestions(ctx) {
27947
- const path34 = join17(ctx.rootDir, "risks.md");
27948
- if (!existsSync20(path34)) return "_No open questions._";
27949
- 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");
27950
28418
  const lines = content.split("\n");
27951
28419
  const questions = [];
27952
28420
  let currentTitle = "";
@@ -28097,9 +28565,9 @@ var init_agentsMd = __esm({
28097
28565
  });
28098
28566
 
28099
28567
  // src/cli/workspace/completeDesign.ts
28100
- function curatedTasksForPhase(phase) {
28101
- const label = (phase.name ?? phase.id).trim();
28102
- 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()}` : "";
28103
28571
  return [
28104
28572
  {
28105
28573
  title: `Specify ${label} deliverables`,
@@ -28146,15 +28614,15 @@ async function runBuiltinCompleteDesign(ctx) {
28146
28614
  const createMilestone = stubs.find((s) => s.name === "createMilestone");
28147
28615
  const stubCtx = ctx;
28148
28616
  let tasksAdded = 0;
28149
- for (const phase of summary.phases) {
28150
- 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);
28151
28619
  if (existingTasks.length >= MIN_TASKS_PER_PHASE) continue;
28152
28620
  const existingTitles = new Set(existingTasks.map((t) => t.name ?? ""));
28153
- 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);
28154
28622
  for (const t of templates) {
28155
28623
  await createTask.execute(
28156
28624
  {
28157
- phaseId: phase.id,
28625
+ phaseId: phase2.id,
28158
28626
  title: t.title,
28159
28627
  description: t.description,
28160
28628
  fileRefs: t.fileRefs,
@@ -28513,8 +28981,8 @@ async function runPostCouncilHook(ctx, options) {
28513
28981
  sources: scope.sources
28514
28982
  } : void 0
28515
28983
  });
28516
- const path34 = writeCouncilCompletion(ctx.rootDir, completion);
28517
- completionHook = { ran: true, path: path34, completion };
28984
+ const path36 = writeCouncilCompletion(ctx.rootDir, completion);
28985
+ completionHook = { ran: true, path: path36, completion };
28518
28986
  } catch (err) {
28519
28987
  completionHook = {
28520
28988
  ran: true,
@@ -28583,7 +29051,7 @@ import {
28583
29051
  writeFileSync as writeFileSync14,
28584
29052
  mkdirSync as mkdirSync9
28585
29053
  } from "node:fs";
28586
- import path23 from "node:path";
29054
+ import path24 from "node:path";
28587
29055
  import os8 from "node:os";
28588
29056
  var FeedbackStore;
28589
29057
  var init_councilFeedback = __esm({
@@ -28594,7 +29062,7 @@ var init_councilFeedback = __esm({
28594
29062
  now;
28595
29063
  entries = [];
28596
29064
  constructor(options = {}) {
28597
- 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"));
28598
29066
  this.now = options.now ?? Date.now;
28599
29067
  this.load();
28600
29068
  }
@@ -28700,7 +29168,7 @@ var init_councilFeedback = __esm({
28700
29168
  }
28701
29169
  }
28702
29170
  save() {
28703
- mkdirSync9(path23.dirname(this.file), { recursive: true });
29171
+ mkdirSync9(path24.dirname(this.file), { recursive: true });
28704
29172
  writeFileSync14(
28705
29173
  this.file,
28706
29174
  JSON.stringify({ entries: this.entries }, null, 2),
@@ -28735,7 +29203,7 @@ __export(fileBackend_exports, {
28735
29203
  });
28736
29204
  import { randomUUID as randomUUID2 } from "node:crypto";
28737
29205
  import { promises as fs14 } from "node:fs";
28738
- import * as path24 from "node:path";
29206
+ import * as path25 from "node:path";
28739
29207
  function tokenize(text) {
28740
29208
  return text.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((t) => t.length >= 3);
28741
29209
  }
@@ -28773,8 +29241,8 @@ var init_fileBackend = __esm({
28773
29241
  logPath = "";
28774
29242
  memoryDir = "";
28775
29243
  async init(projectRoot) {
28776
- this.memoryDir = path24.join(projectRoot, ".zelari", "memory");
28777
- 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");
28778
29246
  await fs14.mkdir(this.memoryDir, { recursive: true });
28779
29247
  }
28780
29248
  async add(content, metadata = {}, graph) {
@@ -28849,7 +29317,7 @@ import { execFile as execFile2 } from "node:child_process";
28849
29317
  import { promisify } from "node:util";
28850
29318
  import { mkdtempSync, rmSync } from "node:fs";
28851
29319
  import { tmpdir } from "node:os";
28852
- import path25 from "node:path";
29320
+ import path26 from "node:path";
28853
29321
  import { randomUUID as randomUUID3 } from "node:crypto";
28854
29322
  async function git(cwd, args, env) {
28855
29323
  const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], {
@@ -28869,8 +29337,8 @@ async function isGitRepo(cwd) {
28869
29337
  return await gitSafe(cwd, ["rev-parse", "--is-inside-work-tree"]) === "true";
28870
29338
  }
28871
29339
  async function withTempIndex(fn) {
28872
- const dir = mkdtempSync(path25.join(tmpdir(), "zelari-ckpt-"));
28873
- const indexFile = path25.join(dir, "index");
29340
+ const dir = mkdtempSync(path26.join(tmpdir(), "zelari-ckpt-"));
29341
+ const indexFile = path26.join(dir, "index");
28874
29342
  try {
28875
29343
  return await fn(indexFile);
28876
29344
  } finally {
@@ -28961,7 +29429,7 @@ async function restoreCheckpoint(cwd, id) {
28961
29429
  const deleted = [];
28962
29430
  for (const rel2 of added) {
28963
29431
  try {
28964
- rmSync(path25.join(cwd, rel2), { force: true });
29432
+ rmSync(path26.join(cwd, rel2), { force: true });
28965
29433
  deleted.push(rel2);
28966
29434
  } catch {
28967
29435
  }
@@ -28991,7 +29459,7 @@ __export(zelariMission_exports, {
28991
29459
  });
28992
29460
  import { randomUUID as randomUUID4 } from "node:crypto";
28993
29461
  import { promises as fs15 } from "node:fs";
28994
- import * as path26 from "node:path";
29462
+ import * as path27 from "node:path";
28995
29463
  function resolveMaxIterations(env = process.env) {
28996
29464
  const raw = env.ZELARI_MISSION_MAX_ITER;
28997
29465
  const n = raw ? Number.parseInt(raw, 10) : DEFAULT_MAX_ITER;
@@ -29007,10 +29475,10 @@ function isMissionAutoStart(env = process.env) {
29007
29475
  return env.ZELARI_MISSION_AUTO === "1";
29008
29476
  }
29009
29477
  async function writeMissionState(projectRoot, state2) {
29010
- const dir = path26.join(projectRoot, ".zelari");
29478
+ const dir = path27.join(projectRoot, ".zelari");
29011
29479
  await fs15.mkdir(dir, { recursive: true });
29012
29480
  await fs15.writeFile(
29013
- path26.join(dir, "mission-state.json"),
29481
+ path27.join(dir, "mission-state.json"),
29014
29482
  JSON.stringify(state2, null, 2) + "\n",
29015
29483
  "utf8"
29016
29484
  );
@@ -29154,29 +29622,47 @@ __export(prereqChecks_exports, {
29154
29622
  checkAgentGit: () => checkAgentGit,
29155
29623
  checkAgentNode: () => checkAgentNode,
29156
29624
  checkMainNode: () => checkMainNode,
29625
+ isWslBashPath: () => isWslBashPath2,
29157
29626
  runPrereqChecks: () => runPrereqChecks
29158
29627
  });
29159
29628
  import { execSync, spawnSync as spawnSync2 } from "node:child_process";
29160
29629
  import { existsSync as existsSync25 } from "node:fs";
29630
+ import { dirname as dirname4 } from "node:path";
29631
+ function isWslBashPath2(p3) {
29632
+ if (!p3 || typeof p3 !== "string") return false;
29633
+ const n = p3.replace(/\//g, "\\").toLowerCase();
29634
+ if (n.includes("\\windows\\system32\\bash.exe")) return true;
29635
+ if (n.includes("\\windows\\syswow64\\bash.exe")) return true;
29636
+ if (n.includes("\\windowsapps\\bash.exe")) return true;
29637
+ return false;
29638
+ }
29639
+ function acceptBashPath2(p3) {
29640
+ if (!p3 || p3.trim().length === 0) return null;
29641
+ const trimmed = p3.trim();
29642
+ if (isWslBashPath2(trimmed)) return null;
29643
+ if (!existsSyncSafe2(trimmed)) return null;
29644
+ return trimmed;
29645
+ }
29161
29646
  function resolveAgentShellSync() {
29162
29647
  if (process.platform !== "win32") {
29163
29648
  return { bashPath: null, isBash: true, via: "/bin/sh" };
29164
29649
  }
29165
- const envShell = process.env.ZELARI_SHELL;
29166
- if (envShell && envShell.trim().length > 0 && existsSyncSafe2(envShell)) {
29167
- return { bashPath: envShell, isBash: true, via: `bash (${envShell})` };
29650
+ const fromEnv = acceptBashPath2(process.env.ZELARI_SHELL);
29651
+ if (fromEnv) {
29652
+ return { bashPath: fromEnv, isBash: true, via: `bash (${fromEnv})` };
29168
29653
  }
29169
- const sessionShell = process.env.SHELL;
29170
- if (sessionShell && sessionShell.trim().length > 0 && existsSyncSafe2(sessionShell)) {
29654
+ const fromSession = acceptBashPath2(process.env.SHELL);
29655
+ if (fromSession) {
29171
29656
  return {
29172
- bashPath: sessionShell,
29657
+ bashPath: fromSession,
29173
29658
  isBash: true,
29174
- via: `bash (${sessionShell})`
29659
+ via: `bash (${fromSession})`
29175
29660
  };
29176
29661
  }
29177
29662
  for (const p3 of STANDARD_BASH_PATHS2) {
29178
- if (existsSyncSafe2(p3)) {
29179
- return { bashPath: p3, isBash: true, via: `bash (${p3})` };
29663
+ const accepted = acceptBashPath2(p3);
29664
+ if (accepted) {
29665
+ return { bashPath: accepted, isBash: true, via: `bash (${accepted})` };
29180
29666
  }
29181
29667
  }
29182
29668
  try {
@@ -29185,16 +29671,39 @@ function resolveAgentShellSync() {
29185
29671
  windowsHide: true
29186
29672
  });
29187
29673
  if (result.status === 0 && result.stdout) {
29188
- const first = result.stdout.split(/\r?\n/).find((l) => l.trim().length > 0);
29189
- if (first && existsSyncSafe2(first)) {
29190
- const trimmed = first.trim();
29191
- return { bashPath: trimmed, isBash: true, via: `bash (${trimmed})` };
29674
+ for (const line of result.stdout.split(/\r?\n/)) {
29675
+ const accepted = acceptBashPath2(line);
29676
+ if (accepted) {
29677
+ return {
29678
+ bashPath: accepted,
29679
+ isBash: true,
29680
+ via: `bash (${accepted})`
29681
+ };
29682
+ }
29192
29683
  }
29193
29684
  }
29194
29685
  } catch {
29195
29686
  }
29196
29687
  return { bashPath: null, isBash: false, via: "cmd.exe" };
29197
29688
  }
29689
+ function agentProbeEnv() {
29690
+ const env = { ...process.env };
29691
+ try {
29692
+ const nodeDir = dirname4(process.execPath);
29693
+ if (!nodeDir) return env;
29694
+ const sep = process.platform === "win32" ? ";" : ":";
29695
+ const current = env.PATH ?? env.Path ?? "";
29696
+ const parts = current.split(sep).filter((p3) => p3.length > 0);
29697
+ const has = parts.some(
29698
+ (p3) => p3.toLowerCase() === nodeDir.toLowerCase()
29699
+ );
29700
+ if (!has) {
29701
+ env.PATH = `${nodeDir}${sep}${current}`;
29702
+ }
29703
+ } catch {
29704
+ }
29705
+ return env;
29706
+ }
29198
29707
  function existsSyncSafe2(p3) {
29199
29708
  try {
29200
29709
  return existsSync25(p3);
@@ -29205,12 +29714,14 @@ function existsSyncSafe2(p3) {
29205
29714
  function probeTool(tool) {
29206
29715
  const shell = resolveAgentShellSync();
29207
29716
  let stdout = "";
29717
+ const env = agentProbeEnv();
29208
29718
  if (shell.bashPath) {
29209
29719
  try {
29210
29720
  const r = spawnSync2(shell.bashPath, ["-c", `${tool} --version`], {
29211
29721
  encoding: "utf8",
29212
29722
  stdio: ["ignore", "pipe", "ignore"],
29213
- windowsHide: true
29723
+ windowsHide: true,
29724
+ env
29214
29725
  });
29215
29726
  if (r.status === 0) stdout = (r.stdout || "").trim();
29216
29727
  } catch {
@@ -29219,7 +29730,8 @@ function probeTool(tool) {
29219
29730
  try {
29220
29731
  stdout = execSync(`${tool} --version`, {
29221
29732
  encoding: "utf8",
29222
- stdio: ["ignore", "pipe", "ignore"]
29733
+ stdio: ["ignore", "pipe", "ignore"],
29734
+ env
29223
29735
  }).trim();
29224
29736
  } catch {
29225
29737
  }
@@ -29227,7 +29739,8 @@ function probeTool(tool) {
29227
29739
  try {
29228
29740
  stdout = execSync(`${tool} --version`, {
29229
29741
  encoding: "utf8",
29230
- stdio: ["ignore", "pipe", "ignore"]
29742
+ stdio: ["ignore", "pipe", "ignore"],
29743
+ env
29231
29744
  }).trim();
29232
29745
  } catch {
29233
29746
  }
@@ -29242,14 +29755,21 @@ function probeTool(tool) {
29242
29755
  function nodeMissingHint() {
29243
29756
  const shell = resolveAgentShellSync();
29244
29757
  if (process.platform === "win32") {
29758
+ if (!shell.isBash) {
29759
+ return `node is not reachable from the agent's shell (${shell.via}).
29760
+ Install Node >= ${MIN_NODE_MAJOR} (https://nodejs.org) and ensure it is
29761
+ on PATH, then open a NEW terminal. Optional: install Git for
29762
+ Windows so the agent can use real bash (https://git-scm.com/download/win).`;
29763
+ }
29245
29764
  return `node is not reachable from the agent's shell (${shell.via}).
29246
29765
  This usually means Node was installed for "current user" only,
29247
- while Git Bash inherits the SYSTEM Path. Fix (pick one):
29766
+ while Git Bash sees a different Path. Fix (pick one):
29248
29767
  - Reinstall Node (https://nodejs.org) and choose
29249
29768
  "Add to PATH for all users", OR
29250
- - Add C:\\Program Files\\nodejs\\ to the SYSTEM Path
29251
- (System Properties \u2192 Environment Variables \u2192 Path), OR
29252
- - Set ZELARI_SHELL to a bash that already sees node.`;
29769
+ - Add your nodejs folder to the User or System Path, OR
29770
+ - Set ZELARI_SHELL to a bash that already sees node.
29771
+ Note: WSL's C:\\Windows\\System32\\bash.exe is NOT a valid agent
29772
+ shell \u2014 install Git for Windows instead.`;
29253
29773
  }
29254
29774
  return `node is not on the agent's shell PATH.
29255
29775
  Install Node >= ${MIN_NODE_MAJOR} (https://nodejs.org) or, if you use
@@ -29409,10 +29929,10 @@ var init_prereqChecks = __esm({
29409
29929
 
29410
29930
  // src/cli/plugins/prefs.ts
29411
29931
  import { existsSync as existsSync26, readFileSync as readFileSync23, writeFileSync as writeFileSync15, mkdirSync as mkdirSync10 } from "node:fs";
29412
- import path28 from "node:path";
29932
+ import path29 from "node:path";
29413
29933
  import os9 from "node:os";
29414
29934
  function getPluginPrefsPath() {
29415
- 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");
29416
29936
  }
29417
29937
  function getPluginPrefs() {
29418
29938
  const file2 = getPluginPrefsPath();
@@ -29433,7 +29953,7 @@ function getPluginPrefs() {
29433
29953
  }
29434
29954
  function writePluginPrefs(prefs) {
29435
29955
  const file2 = getPluginPrefsPath();
29436
- mkdirSync10(path28.dirname(file2), { recursive: true });
29956
+ mkdirSync10(path29.dirname(file2), { recursive: true });
29437
29957
  writeFileSync15(file2, JSON.stringify(prefs, null, 2), {
29438
29958
  encoding: "utf-8",
29439
29959
  mode: 384
@@ -29466,9 +29986,11 @@ var registry_exports = {};
29466
29986
  __export(registry_exports, {
29467
29987
  PLUGINS: () => PLUGINS,
29468
29988
  detectMissingPlugins: () => detectMissingPlugins,
29469
- findPlugin: () => findPlugin
29989
+ findPlugin: () => findPlugin,
29990
+ isBinaryOnPath: () => isBinaryOnPath
29470
29991
  });
29471
- import { spawnSync as spawnSync3 } from "node:child_process";
29992
+ import { existsSync as existsSync27 } from "node:fs";
29993
+ import path30 from "node:path";
29472
29994
  function detectLocalBin(bin) {
29473
29995
  return (cwd) => {
29474
29996
  try {
@@ -29479,27 +30001,48 @@ function detectLocalBin(bin) {
29479
30001
  }
29480
30002
  };
29481
30003
  }
29482
- function detectGlobalBin(bin) {
29483
- 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) => {
29484
30036
  try {
29485
- const args = ["--version"];
29486
- const res = process.platform === "win32" ? spawnSync3(buildCmdLine(bin, args), {
29487
- stdio: ["ignore", "pipe", "ignore"],
29488
- shell: true,
29489
- timeout: 4e3
29490
- }) : spawnSync3(bin, args, {
29491
- stdio: ["ignore", "pipe", "ignore"],
29492
- timeout: 4e3
29493
- });
29494
- 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);
29495
30038
  } catch {
29496
- return Promise.resolve(false);
29497
30039
  }
30040
+ return Promise.resolve(isBinaryOnPath(bin));
29498
30041
  };
29499
30042
  }
29500
- async function detectPlaywright() {
30043
+ async function detectPlaywright(cwd) {
29501
30044
  try {
29502
- const mod = await defaultPlaywrightLoader();
30045
+ const mod = await loadPlaywright(cwd);
29503
30046
  return mod !== null;
29504
30047
  } catch {
29505
30048
  return false;
@@ -29537,7 +30080,6 @@ var PLUGINS;
29537
30080
  var init_registry2 = __esm({
29538
30081
  "src/cli/plugins/registry.ts"() {
29539
30082
  "use strict";
29540
- init_cmdline();
29541
30083
  init_engine();
29542
30084
  init_driver();
29543
30085
  init_engine();
@@ -29567,7 +30109,7 @@ var init_registry2 = __esm({
29567
30109
  label: "Playwright (browser_check tool)",
29568
30110
  npmPackage: "playwright",
29569
30111
  installScope: "dev",
29570
- detect: () => detectPlaywright(),
30112
+ detect: detectPlaywright,
29571
30113
  postInstallHint: "Then fetch the browser binary: `npx playwright install chromium`",
29572
30114
  featureGate: "ZELARI_BROWSER",
29573
30115
  description: "Powers the browser_check tool (URL probing, click/fill/wait, screenshots)."
@@ -29577,7 +30119,7 @@ var init_registry2 = __esm({
29577
30119
  label: "typescript-language-server (LSP for TS/JS)",
29578
30120
  npmPackage: "typescript-language-server",
29579
30121
  installScope: "global",
29580
- detect: detectGlobalBin(binForLspLanguage("typescript")),
30122
+ detect: detectPathBin(binForLspLanguage("typescript")),
29581
30123
  featureGate: "ZELARI_LSP",
29582
30124
  description: "Powers go_to_definition / find_references / hover_type / rename_symbol for TS/JS."
29583
30125
  },
@@ -29586,9 +30128,24 @@ var init_registry2 = __esm({
29586
30128
  label: "pyright (LSP for Python)",
29587
30129
  npmPackage: "pyright",
29588
30130
  installScope: "global",
29589
- 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")),
29590
30134
  featureGate: "ZELARI_LSP",
29591
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."
29592
30149
  }
29593
30150
  ];
29594
30151
  }
@@ -29600,26 +30157,26 @@ __export(doctor_exports, {
29600
30157
  runDoctor: () => runDoctor
29601
30158
  });
29602
30159
  import { execSync as execSync2 } from "node:child_process";
29603
- import { existsSync as existsSync31, readFileSync as readFileSync26, readlinkSync, statSync as statSync6 } from "node:fs";
29604
- 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";
29605
30162
  import { fileURLToPath as fileURLToPath2 } from "node:url";
29606
- import path33 from "node:path";
30163
+ import path35 from "node:path";
29607
30164
  function findPackageRoot(start) {
29608
30165
  let dir = start;
29609
30166
  for (let i = 0; i < 6; i += 1) {
29610
- const candidate = path33.join(dir, "package.json");
29611
- if (existsSync31(candidate)) {
30167
+ const candidate = path35.join(dir, "package.json");
30168
+ if (existsSync32(candidate)) {
29612
30169
  try {
29613
30170
  const pkg = JSON.parse(readFileSync26(candidate, "utf8"));
29614
30171
  if (pkg.name === "zelari-code") return dir;
29615
30172
  } catch {
29616
30173
  }
29617
30174
  }
29618
- const parent = path33.dirname(dir);
30175
+ const parent = path35.dirname(dir);
29619
30176
  if (parent === dir) break;
29620
30177
  dir = parent;
29621
30178
  }
29622
- return path33.resolve(__dirname3, "..", "..", "..");
30179
+ return path35.resolve(__dirname3, "..", "..", "..");
29623
30180
  }
29624
30181
  function tryExec(cmd) {
29625
30182
  try {
@@ -29633,7 +30190,7 @@ function tryExec(cmd) {
29633
30190
  }
29634
30191
  function readPackageJson3() {
29635
30192
  try {
29636
- const pkgPath = path33.join(packageRoot, "package.json");
30193
+ const pkgPath = path35.join(packageRoot, "package.json");
29637
30194
  return JSON.parse(readFileSync26(pkgPath, "utf8"));
29638
30195
  } catch {
29639
30196
  return null;
@@ -29649,8 +30206,8 @@ function checkShim(pkgName) {
29649
30206
  }
29650
30207
  const isWin = process.platform === "win32";
29651
30208
  const shimName = isWin ? "zelari-code.cmd" : "zelari-code";
29652
- const shimPath = path33.join(prefix, shimName);
29653
- if (!existsSync31(shimPath)) {
30209
+ const shimPath = path35.join(prefix, shimName);
30210
+ if (!existsSync32(shimPath)) {
29654
30211
  return FAIL(
29655
30212
  `shim not found at ${shimPath}
29656
30213
  fix: npm install -g ${pkgName}@latest --force`
@@ -29677,8 +30234,8 @@ function checkShim(pkgName) {
29677
30234
  fix: npm install -g ${pkgName}@latest --force`
29678
30235
  );
29679
30236
  }
29680
- const resolved = path33.resolve(path33.dirname(shimPath), target);
29681
- const expected = path33.join(
30237
+ const resolved = path35.resolve(path35.dirname(shimPath), target);
30238
+ const expected = path35.join(
29682
30239
  prefix,
29683
30240
  "node_modules",
29684
30241
  pkgName,
@@ -29717,8 +30274,8 @@ function checkNode(pkg) {
29717
30274
  return OK(`node ${raw}`);
29718
30275
  }
29719
30276
  function checkBundle() {
29720
- const bundle = path33.join(packageRoot, "dist", "cli", "main.bundled.js");
29721
- if (!existsSync31(bundle)) {
30277
+ const bundle = path35.join(packageRoot, "dist", "cli", "main.bundled.js");
30278
+ if (!existsSync32(bundle)) {
29722
30279
  return FAIL(
29723
30280
  `dist/cli/main.bundled.js missing at ${bundle}
29724
30281
  fix: npm run build:cli (then reinstall or run via tsx)`
@@ -29738,7 +30295,7 @@ function checkRuntimeDeps() {
29738
30295
  const missing = [];
29739
30296
  for (const dep of required2) {
29740
30297
  try {
29741
- const localReq = createRequire2(path33.join(packageRoot, "package.json"));
30298
+ const localReq = createRequire3(path35.join(packageRoot, "package.json"));
29742
30299
  localReq.resolve(dep);
29743
30300
  } catch {
29744
30301
  missing.push(dep);
@@ -29858,8 +30415,8 @@ var init_doctor = __esm({
29858
30415
  "src/cli/utils/doctor.ts"() {
29859
30416
  "use strict";
29860
30417
  init_prereqChecks();
29861
- require3 = createRequire2(import.meta.url);
29862
- __dirname3 = path33.dirname(fileURLToPath2(import.meta.url));
30418
+ require3 = createRequire3(import.meta.url);
30419
+ __dirname3 = path35.dirname(fileURLToPath2(import.meta.url));
29863
30420
  packageRoot = findPackageRoot(__dirname3);
29864
30421
  OK = (message) => ({
29865
30422
  ok: true,
@@ -29884,11 +30441,11 @@ var fixPath_exports = {};
29884
30441
  __export(fixPath_exports, {
29885
30442
  repairWindowsUserPath: () => repairWindowsUserPath
29886
30443
  });
29887
- import { spawnSync as spawnSync4 } from "node:child_process";
30444
+ import { spawnSync as spawnSync3 } from "node:child_process";
29888
30445
  function getGlobalPrefix2() {
29889
30446
  return (process.env.npm_config_prefix || process.env.NPM_CONFIG_PREFIX || "").trim() || (() => {
29890
30447
  try {
29891
- return spawnSync4("npm", ["prefix", "-g"], {
30448
+ return spawnSync3("npm", ["prefix", "-g"], {
29892
30449
  encoding: "utf8",
29893
30450
  stdio: ["ignore", "pipe", "ignore"]
29894
30451
  }).stdout?.trim() ?? "";
@@ -29899,7 +30456,7 @@ function getGlobalPrefix2() {
29899
30456
  }
29900
30457
  function powershell(script) {
29901
30458
  try {
29902
- const res = spawnSync4(
30459
+ const res = spawnSync3(
29903
30460
  "powershell.exe",
29904
30461
  ["-NoProfile", "-NonInteractive", "-Command", script],
29905
30462
  { encoding: "utf8" }
@@ -30468,20 +31025,25 @@ function StatusBar({
30468
31025
  queueCount = 0,
30469
31026
  busy = false,
30470
31027
  mode = "agent",
31028
+ phase: phase2 = "build",
30471
31029
  cwd,
30472
31030
  elapsedMs = null,
30473
31031
  lastMs = null,
30474
31032
  costUsd = 0,
30475
- cachedTokens = 0
31033
+ cachedTokens = 0,
31034
+ contextUsed = 0,
31035
+ contextLimit = 0,
31036
+ brandVersion
30476
31037
  }) {
30477
- 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(
30478
31040
  Text6,
30479
31041
  {
30480
31042
  bold: true,
30481
31043
  color: mode === "council" ? "magenta" : mode === "zelari" ? "green" : "cyan"
30482
31044
  },
30483
- mode === "council" ? "\u26EC council" : mode === "zelari" ? "\u26A1 zelari" : "\u23F5 agent"
30484
- ), /* @__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))));
30485
31047
  }
30486
31048
 
30487
31049
  // src/cli/components/SelectList.tsx
@@ -30529,15 +31091,6 @@ function SelectList({
30529
31091
  // src/cli/components/Sidebar.tsx
30530
31092
  import React8 from "react";
30531
31093
  import { Box as Box7, Text as Text8 } from "ink";
30532
- var EMBLEM_BRAILLE = `\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28F4\u28FF\u28F7\u28C6\u2840
30533
- \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28F6\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E6\u2840
30534
- \u2800\u2800\u2800\u2800\u2800\u2800\u28F0\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E6
30535
- \u2800\u2800\u2800\u2800\u28A0\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28F7\u2844
30536
- \u2800\u2800\u2800\u2800\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2819\u283B\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2806
30537
- \u2800\u2800\u2800\u2800\u28C8\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2800\u28F7\u28EC\u285B\u28BF\u28FF\u28DF\u28C1
30538
- \u2800\u2880\u28F4\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2800\u287F\u28BF\u287F\u2883\u28C8\u28FB\u28FF\u28FF\u28F6
30539
- \u2800\u28FC\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E0\u28F4\u28C6\u283B\u280E\u28BF\u28FF\u28FF\u28FF\u28FF\u28E7
30540
- \u28F0\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2847`;
30541
31094
  var SIDEBAR_WIDTH = 28;
30542
31095
  var SIDEBAR_MIN_COLUMNS = 96;
30543
31096
  var EMBLEM_MIN_ROWS = 26;
@@ -30551,7 +31104,7 @@ function truncatePath(p3, max) {
30551
31104
  return `\u2026${p3.slice(-(max - 1))}`;
30552
31105
  }
30553
31106
  function Sidebar({ version: version2, changes, rows }) {
30554
- const showEmblem = rows >= EMBLEM_MIN_ROWS;
31107
+ void version2;
30555
31108
  const maxFiles = rows >= EMBLEM_MIN_ROWS + 8 ? MAX_FILES_TALL : MAX_FILES_SHORT;
30556
31109
  const visible = changes.files.slice(0, maxFiles);
30557
31110
  const hidden = changes.files.length - visible.length;
@@ -30566,9 +31119,7 @@ function Sidebar({ version: version2, changes, rows }) {
30566
31119
  paddingX: 1,
30567
31120
  flexShrink: 0
30568
31121
  },
30569
- showEmblem && /* @__PURE__ */ React8.createElement(Box7, { justifyContent: "center" }, /* @__PURE__ */ React8.createElement(Text8, { color: "cyan" }, EMBLEM_BRAILLE)),
30570
- /* @__PURE__ */ React8.createElement(Box7, { justifyContent: "center" }, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "white" }, "ZELARI CODE")),
30571
- /* @__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")),
30572
31123
  /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, "\u2500".repeat(innerWidth)),
30573
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"))
30574
31125
  );
@@ -33553,80 +34104,8 @@ function finalizeStreamingAssistant(setMessages) {
33553
34104
  });
33554
34105
  }
33555
34106
 
33556
- // src/cli/utils/envNumber.ts
33557
- function envNumber(raw, opts) {
33558
- const { default: def, min, max } = opts;
33559
- if (raw === void 0 || raw === null) return def;
33560
- const trimmed = raw.trim();
33561
- if (trimmed.length === 0) return def;
33562
- if (trimmed.toLowerCase() === "undefined" || trimmed.toLowerCase() === "null") return def;
33563
- const parsed = Number.parseInt(trimmed, 10);
33564
- if (!Number.isFinite(parsed)) return def;
33565
- const absStr = `${Math.abs(parsed)}`;
33566
- const body = trimmed.replace(/^[-+]/, "").replace(/^0+(?=\d)/, "");
33567
- if (body !== absStr) return def;
33568
- const firstChar = trimmed[0];
33569
- const expectedSign = parsed < 0 ? "-" : /[0-9+]/.test(firstChar ?? "");
33570
- const actualSign = parsed < 0 ? firstChar === "-" : firstChar !== "-";
33571
- if (!expectedSign || !actualSign) return def;
33572
- let clamped = parsed;
33573
- if (min !== void 0 && clamped < min) clamped = min;
33574
- if (max !== void 0 && clamped > max) clamped = max;
33575
- return clamped;
33576
- }
33577
-
33578
- // src/cli/hooks/historyCompaction.ts
33579
- var COMPACT_MARKER = "[history] Earlier turns were compacted to stay within the context budget.";
33580
- function resolveMaxMessages(opts) {
33581
- const envTurns = envNumber(process.env.ZELARI_HISTORY_TURNS, { default: 6, min: 0 });
33582
- const turns = opts?.maxMessages ? Math.ceil(opts.maxMessages / 4) : envTurns;
33583
- if (turns <= 0) return 0;
33584
- return turns * 4;
33585
- }
33586
- function findValidCutIndex(messages, naiveCut) {
33587
- let cut = naiveCut;
33588
- while (cut < messages.length) {
33589
- const kept = messages.slice(cut);
33590
- const declared = /* @__PURE__ */ new Set();
33591
- for (const m of kept) {
33592
- if (m.role === "assistant" && m.toolCalls) {
33593
- for (const tc of m.toolCalls) declared.add(tc.id);
33594
- }
33595
- }
33596
- let moved = false;
33597
- for (let k = 0; k < kept.length; k++) {
33598
- const m = kept[k];
33599
- if (m.role === "tool" && m.toolCallId && !declared.has(m.toolCallId)) {
33600
- for (let j = cut - 1; j >= 0; j--) {
33601
- const prev2 = messages[j];
33602
- if (prev2.role === "assistant" && prev2.toolCalls && prev2.toolCalls.some((tc) => tc.id === m.toolCallId)) {
33603
- cut = j;
33604
- moved = true;
33605
- break;
33606
- }
33607
- }
33608
- break;
33609
- }
33610
- }
33611
- if (!moved) break;
33612
- }
33613
- return cut;
33614
- }
33615
- function compactHistory(messages, opts) {
33616
- const maxMessages = resolveMaxMessages(opts);
33617
- if (maxMessages === 0) return [];
33618
- if (messages.length <= maxMessages * 2) return messages;
33619
- const naiveCut = messages.length - maxMessages;
33620
- const cut = findValidCutIndex(messages, naiveCut);
33621
- const dropped = cut;
33622
- if (dropped === 0) return messages;
33623
- const kept = messages.slice(cut);
33624
- const summary = {
33625
- role: "system",
33626
- content: `${COMPACT_MARKER} ${dropped} earlier message(s) dropped.`
33627
- };
33628
- return [summary, ...kept];
33629
- }
34107
+ // src/cli/hooks/useChatTurn.ts
34108
+ init_conversationContext();
33630
34109
 
33631
34110
  // src/cli/hooks/chatStats.ts
33632
34111
  function computeSessionStatsDelta(realUsage, userText, assistantContent, model, prev2) {
@@ -33641,6 +34120,83 @@ function computeSessionStatsDelta(realUsage, userText, assistantContent, model,
33641
34120
  };
33642
34121
  }
33643
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
+
33644
34200
  // src/cli/hooks/useChatTurn.ts
33645
34201
  function useChatTurn(params) {
33646
34202
  const {
@@ -33658,8 +34214,10 @@ function useChatTurn(params) {
33658
34214
  } = params;
33659
34215
  const harnessRef = useRef4(null);
33660
34216
  const [queueCount, setQueueCount] = useState6(0);
33661
- const historyRef = useRef4([]);
33662
34217
  const useLiveModel = !!(setLive && liveRef);
34218
+ const clearConversationHistory = useCallback2(() => {
34219
+ clearHistory();
34220
+ }, []);
33663
34221
  const dispatchPrompt = useCallback2(
33664
34222
  async (userText, opts) => {
33665
34223
  let envConfig;
@@ -33667,8 +34225,15 @@ function useChatTurn(params) {
33667
34225
  let historySeedLen = 0;
33668
34226
  let turnSucceeded = false;
33669
34227
  try {
33670
- historyRef.current = compactHistory(historyRef.current);
33671
- 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;
33672
34237
  envConfig = await providerFromEnv();
33673
34238
  if (!envConfig) {
33674
34239
  const active = resolveActiveProvider();
@@ -33680,7 +34245,10 @@ function useChatTurn(params) {
33680
34245
  return;
33681
34246
  }
33682
34247
  setBusy(true);
33683
- const { registry: toolRegistry } = createBuiltinToolRegistry();
34248
+ const workPhase = getPhase();
34249
+ const { registry: toolRegistry } = createBuiltinToolRegistry({
34250
+ planMode: workPhase === "plan"
34251
+ });
33684
34252
  const baseProviderStream = openaiCompatibleProvider(envConfig);
33685
34253
  const failoverResolution = await resolveFailoverStream({
33686
34254
  failoverEnabled: process.env.ANATHEMA_FAILOVER !== "0",
@@ -33760,12 +34328,26 @@ function useChatTurn(params) {
33760
34328
  const isWindows = process.platform === "win32";
33761
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.`;
33762
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") : "";
33763
34344
  const shellContextBlock = [
33764
34345
  "# Platform & Shell",
33765
34346
  `platform: ${process.platform}`,
33766
34347
  `shell: ${resolvedShell.via}`,
33767
34348
  shellGuidance,
33768
34349
  nonInteractiveGuidance,
34350
+ planPhaseBlock,
33769
34351
  "",
33770
34352
  "# Working Directory",
33771
34353
  `You are running in: ${cwd}`,
@@ -33819,20 +34401,17 @@ function useChatTurn(params) {
33819
34401
  default: 25,
33820
34402
  min: 1
33821
34403
  });
33822
- const maxToolLoopIterations = envNumber(process.env.ZELARI_MAX_TOOL_LOOP_ITERATIONS, {
33823
- default: 90,
33824
- min: 1
33825
- });
34404
+ const maxToolLoopIterations = budget.maxToolLoopIterations;
33826
34405
  const harness2 = new AgentHarness({
33827
34406
  model: envConfig.model,
33828
34407
  provider: "openai-compatible",
33829
34408
  messages: [
33830
34409
  { role: "system", content: systemPrompt },
33831
- // v1.6.0: seed prior turns so the model sees its own last
33832
- // question when the user answers briefly. historyRef.current
33833
- // is compacted above (possibly empty if ZELARI_HISTORY_TURNS=0).
33834
- ...historyRef.current,
33835
- { 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 }
33836
34415
  ],
33837
34416
  tools: toolRegistry.toOpenAITools().map((t) => ({
33838
34417
  name: t.function.name,
@@ -33973,17 +34552,19 @@ function useChatTurn(params) {
33973
34552
  const all = h.getMessages();
33974
34553
  const seedLen = 1 + historySeedLen + 1;
33975
34554
  if (all.length > seedLen) {
33976
- historyRef.current = historyRef.current.concat(
33977
- all.slice(seedLen)
33978
- );
34555
+ appendMessages(all.slice(seedLen));
33979
34556
  }
33980
34557
  }
33981
34558
  } catch {
33982
34559
  }
33983
- if (turnSucceeded && setPicker && assistantContent) {
34560
+ if (turnSucceeded && assistantContent) {
33984
34561
  try {
33985
34562
  const clar = parseClarificationRequest(assistantContent);
33986
34563
  if (clar && clar.choices && clar.choices.length >= 2) {
34564
+ setLastClarification({
34565
+ question: clar.question,
34566
+ choices: clar.choices
34567
+ });
33987
34568
  const cleaned = cleanAgentContent(assistantContent);
33988
34569
  if (cleaned !== assistantContent) {
33989
34570
  setMessages((prev2) => {
@@ -33997,14 +34578,18 @@ function useChatTurn(params) {
33997
34578
  return next;
33998
34579
  });
33999
34580
  }
34000
- setPicker({
34001
- kind: "clarification",
34002
- title: clar.question,
34003
- items: clar.choices.map((c) => ({ value: c, label: c })),
34004
- onAnswer: (value) => {
34005
- void dispatchPrompt(value);
34006
- }
34007
- });
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);
34008
34593
  }
34009
34594
  } catch {
34010
34595
  }
@@ -34056,7 +34641,8 @@ function useChatTurn(params) {
34056
34641
  setBusy,
34057
34642
  setQueueCount,
34058
34643
  setLive,
34059
- liveRef
34644
+ liveRef,
34645
+ setPicker
34060
34646
  });
34061
34647
  },
34062
34648
  [
@@ -34068,7 +34654,8 @@ function useChatTurn(params) {
34068
34654
  setBusy,
34069
34655
  setQueueCount,
34070
34656
  setLive,
34071
- liveRef
34657
+ liveRef,
34658
+ setPicker
34072
34659
  ]
34073
34660
  );
34074
34661
  const pendingZelariRef = useRef4(null);
@@ -34108,7 +34695,8 @@ function useChatTurn(params) {
34108
34695
  dispatchZelariPrompt,
34109
34696
  harnessRef,
34110
34697
  queueCount,
34111
- setQueueCount
34698
+ setQueueCount,
34699
+ clearConversationHistory
34112
34700
  };
34113
34701
  }
34114
34702
  async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
@@ -34120,7 +34708,8 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
34120
34708
  flushStreaming,
34121
34709
  setBusy,
34122
34710
  setLive,
34123
- liveRef
34711
+ liveRef,
34712
+ setPicker
34124
34713
  } = deps;
34125
34714
  const useLiveModel = !!(setLive && liveRef);
34126
34715
  const envConfig = await providerFromEnv();
@@ -34134,6 +34723,19 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
34134
34723
  return { completionOk: false, ran: false };
34135
34724
  }
34136
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
+ );
34137
34739
  const { dispatchCouncil: dispatchCouncil2 } = await Promise.resolve().then(() => (init_councilDispatcher(), councilDispatcher_exports));
34138
34740
  const { createWorkspaceContext: createWorkspaceContext2, createWorkspaceStubs: createWorkspaceStubs2 } = await Promise.resolve().then(() => (init_stubs(), stubs_exports));
34139
34741
  const { createWorkspaceToolRegistry: createWorkspaceToolRegistry2 } = await Promise.resolve().then(() => (init_toolRegistry2(), toolRegistry_exports2));
@@ -34142,13 +34744,18 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
34142
34744
  const { buildWorkspaceSummary: buildWorkspaceSummary2, buildPlanSummary: buildPlanSummary2 } = await Promise.resolve().then(() => (init_workspaceSummary(), workspaceSummary_exports));
34143
34745
  const { buildLessonsSummary: buildLessonsSummary2 } = await Promise.resolve().then(() => (init_buildLessonsSummary(), buildLessonsSummary_exports));
34144
34746
  const { FeedbackStore: FeedbackStore2 } = await Promise.resolve().then(() => (init_councilFeedback(), councilFeedback_exports));
34145
- const { registry: councilToolRegistry } = createBuiltinToolRegistry();
34747
+ const workPhase = getPhase();
34748
+ const { registry: councilToolRegistry } = createBuiltinToolRegistry({
34749
+ planMode: workPhase === "plan"
34750
+ });
34146
34751
  const workspaceCtx = createWorkspaceContext2();
34147
34752
  const workspaceReg = createWorkspaceToolRegistry2(workspaceCtx);
34148
34753
  for (const name of workspaceReg.list()) {
34149
34754
  const td = workspaceReg.get(name);
34150
- if (td) councilToolRegistry.register(td);
34755
+ if (!td) continue;
34756
+ councilToolRegistry.register(td);
34151
34757
  }
34758
+ const phaseRunMode = overrides.runMode ?? (workPhase === "plan" ? "design-phase" : "implementation");
34152
34759
  try {
34153
34760
  const { registerMcpTools: registerMcpTools2 } = await Promise.resolve().then(() => (init_mcpManager(), mcpManager_exports));
34154
34761
  const mcp = await registerMcpTools2(councilToolRegistry);
@@ -34177,7 +34784,7 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
34177
34784
  let sliceDegraded = false;
34178
34785
  const PROVIDER_ERROR_ABORT_THRESHOLD = 2;
34179
34786
  try {
34180
- for await (const event of dispatchCouncil2(text, {
34787
+ for await (const event of dispatchCouncil2(effectiveText, {
34181
34788
  apiKey: envConfig.apiKey,
34182
34789
  model: envConfig.model,
34183
34790
  provider: "openai-compatible",
@@ -34191,20 +34798,49 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
34191
34798
  // on and projected their identity onto the task.
34192
34799
  // v0.7.3: append the existing plan (if any) so a follow-up /council
34193
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).
34194
34803
  workspaceContext: [
34195
34804
  buildWorkspaceSummary2(process.cwd()),
34196
- buildPlanSummary2(process.cwd(), { userMessage: text }),
34197
- buildLessonsSummary2(process.cwd(), text)
34805
+ buildPlanSummary2(process.cwd(), { userMessage: effectiveText }),
34806
+ buildLessonsSummary2(process.cwd(), effectiveText),
34807
+ formatHistoryForCouncil(4)
34198
34808
  ].filter(Boolean).join("\n\n"),
34199
34809
  maxToolCallsPerTurn: councilMaxToolCalls,
34200
34810
  // v1.0: Zelari-mode per-slice overrides (memory RAG, forced run mode,
34201
34811
  // raised chairman budget). No-ops for a normal /council run.
34202
34812
  ...overrides.ragContext ? { ragContext: overrides.ragContext } : {},
34203
- ...overrides.runMode ? { runMode: overrides.runMode } : {},
34813
+ runMode: phaseRunMode,
34204
34814
  ...overrides.maxToolCallsChairman ? { maxToolCallsChairman: overrides.maxToolCallsChairman } : {},
34205
34815
  onCouncilStatus: (message) => {
34206
34816
  appendSystem(setMessages, message, Date.now());
34207
- }
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
34208
34844
  })) {
34209
34845
  if (councilAborted) {
34210
34846
  if (writerRef.current) await writerRef.current.append(event);
@@ -34355,6 +34991,18 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
34355
34991
  flushStreaming();
34356
34992
  if (useLiveModel) finalizeStreaming(setMessages, setLive);
34357
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
+ }
34358
35006
  const hookShouldRun = membersCompleted > 0 || chairmanProducedOutput;
34359
35007
  sliceRan = hookShouldRun;
34360
35008
  if (hookShouldRun) {
@@ -34377,7 +35025,7 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
34377
35025
  }
34378
35026
  const hook = await runPostCouncilHook2(workspaceCtx, {
34379
35027
  runMode: councilRunMode,
34380
- userMessage: text,
35028
+ userMessage: effectiveText,
34381
35029
  synthesisText: chairmanSynthesisText || void 0,
34382
35030
  degradedRun: degraded.degraded,
34383
35031
  degradedReasons: degraded.reasons
@@ -35027,6 +35675,27 @@ ${formatSkillList(availableSkills)}`
35027
35675
  }
35028
35676
  return { handled: true, kind: "mode_set" };
35029
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
+ }
35030
35699
  case "index": {
35031
35700
  return args[0] === "status" ? { handled: true, kind: "index_status" } : { handled: true, kind: "index_build" };
35032
35701
  }
@@ -35114,7 +35783,7 @@ ${formatSkillList(availableSkills)}`
35114
35783
  // src/cli/gitOps.ts
35115
35784
  import { execFile as execFile3 } from "node:child_process";
35116
35785
  import { promisify as promisify2 } from "node:util";
35117
- import path27 from "node:path";
35786
+ import path28 from "node:path";
35118
35787
  var execFileAsync2 = promisify2(execFile3);
35119
35788
  async function git2(cwd, args) {
35120
35789
  try {
@@ -35160,7 +35829,7 @@ async function undoWorkingChanges(opts = {}) {
35160
35829
  };
35161
35830
  }
35162
35831
  function defaultProjectRoot() {
35163
- return path27.resolve(__dirname, "..", "..", "..");
35832
+ return path28.resolve(__dirname, "..", "..", "..");
35164
35833
  }
35165
35834
 
35166
35835
  // src/cli/slashHandlers/git.ts
@@ -35568,15 +36237,15 @@ ${result.output.split("\n").slice(-8).join("\n")}` : "";
35568
36237
 
35569
36238
  // src/cli/slashHandlers/promoteMember.ts
35570
36239
  import { promises as fs16 } from "node:fs";
35571
- import path29 from "node:path";
36240
+ import path31 from "node:path";
35572
36241
  import os10 from "node:os";
35573
36242
  async function handlePromoteMember(ctx, memberId) {
35574
36243
  try {
35575
36244
  const { promoteMember: promoteMember2 } = await Promise.resolve().then(() => (init_council(), council_exports));
35576
36245
  const { skill, markdown } = promoteMember2(memberId);
35577
- 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");
35578
36247
  await fs16.mkdir(skillDir, { recursive: true });
35579
- const filePath = path29.join(skillDir, `${skill.id}.md`);
36248
+ const filePath = path31.join(skillDir, `${skill.id}.md`);
35580
36249
  await fs16.writeFile(filePath, markdown, "utf8");
35581
36250
  appendSystem(
35582
36251
  ctx.setMessages,
@@ -35593,29 +36262,29 @@ async function handlePromoteMember(ctx, memberId) {
35593
36262
  }
35594
36263
 
35595
36264
  // src/cli/branchManager.ts
35596
- 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";
35597
- 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";
35598
36267
  import os11 from "node:os";
35599
36268
  var META_FILENAME = "meta.json";
35600
36269
  var SESSIONS_SUBDIR = "sessions";
35601
36270
  function getBranchesBaseDir() {
35602
- 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");
35603
36272
  }
35604
36273
  function getSessionsBaseDir() {
35605
- 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");
35606
36275
  }
35607
36276
  function branchPathFor(name, baseDir) {
35608
- return path30.join(baseDir, name);
36277
+ return path32.join(baseDir, name);
35609
36278
  }
35610
36279
  function metaPathFor(name, baseDir) {
35611
- return path30.join(baseDir, name, META_FILENAME);
36280
+ return path32.join(baseDir, name, META_FILENAME);
35612
36281
  }
35613
36282
  function sessionsPathFor(name, baseDir) {
35614
- return path30.join(baseDir, name, SESSIONS_SUBDIR);
36283
+ return path32.join(baseDir, name, SESSIONS_SUBDIR);
35615
36284
  }
35616
36285
  function readBranchMeta(name, baseDir) {
35617
36286
  const metaPath = metaPathFor(name, baseDir);
35618
- if (!existsSync27(metaPath)) {
36287
+ if (!existsSync28(metaPath)) {
35619
36288
  throw new BranchNotFoundError(`Branch "${name}" not found`);
35620
36289
  }
35621
36290
  try {
@@ -35636,7 +36305,7 @@ function readBranchMeta(name, baseDir) {
35636
36305
  }
35637
36306
  function writeBranchMeta(name, baseDir, meta3) {
35638
36307
  const metaPath = metaPathFor(name, baseDir);
35639
- mkdirSync11(path30.dirname(metaPath), { recursive: true });
36308
+ mkdirSync11(path32.dirname(metaPath), { recursive: true });
35640
36309
  writeFileSync16(metaPath, JSON.stringify(meta3, null, 2), "utf-8");
35641
36310
  }
35642
36311
  async function countSessions(name, baseDir) {
@@ -35675,7 +36344,7 @@ var SessionNotFoundError = class extends Error {
35675
36344
  };
35676
36345
  function branchExists(name, baseDir = getBranchesBaseDir()) {
35677
36346
  const bp = branchPathFor(name, baseDir);
35678
- return existsSync27(bp) && existsSync27(metaPathFor(name, baseDir));
36347
+ return existsSync28(bp) && existsSync28(metaPathFor(name, baseDir));
35679
36348
  }
35680
36349
  async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(), sessionsBaseDir = getSessionsBaseDir()) {
35681
36350
  if (!name || name.trim().length === 0) {
@@ -35687,14 +36356,14 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
35687
36356
  if (branchExists(name, baseDir)) {
35688
36357
  throw new BranchAlreadyExistsError(name);
35689
36358
  }
35690
- const sourcePath = path30.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
35691
- if (!existsSync27(sourcePath)) {
36359
+ const sourcePath = path32.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
36360
+ if (!existsSync28(sourcePath)) {
35692
36361
  throw new SessionNotFoundError(`Source session "${fromSessionId}" not found at ${sourcePath}`);
35693
36362
  }
35694
36363
  const branchPath = branchPathFor(name, baseDir);
35695
36364
  const branchSessionsPath = sessionsPathFor(name, baseDir);
35696
36365
  mkdirSync11(branchSessionsPath, { recursive: true });
35697
- const destPath = path30.join(branchSessionsPath, `${fromSessionId}.jsonl`);
36366
+ const destPath = path32.join(branchSessionsPath, `${fromSessionId}.jsonl`);
35698
36367
  await fs17.copyFile(sourcePath, destPath);
35699
36368
  const meta3 = {
35700
36369
  name,
@@ -35721,7 +36390,7 @@ async function listBranches(baseDir = getBranchesBaseDir()) {
35721
36390
  const results = [];
35722
36391
  for (const entry of entries) {
35723
36392
  const metaPath = metaPathFor(entry, baseDir);
35724
- if (!existsSync27(metaPath)) continue;
36393
+ if (!existsSync28(metaPath)) continue;
35725
36394
  try {
35726
36395
  const meta3 = readBranchMeta(entry, baseDir);
35727
36396
  const sessionCount = await countSessions(entry, baseDir);
@@ -35795,14 +36464,14 @@ async function handleBranchCheckout(ctx, branchName) {
35795
36464
 
35796
36465
  // src/cli/slashHandlers/workspace.ts
35797
36466
  import { promises as fs18 } from "node:fs";
35798
- import path31 from "node:path";
36467
+ import path33 from "node:path";
35799
36468
  async function handleWorkspaceShow(ctx, what) {
35800
36469
  try {
35801
- const zelari = path31.join(process.cwd(), ".zelari");
36470
+ const zelari = path33.join(process.cwd(), ".zelari");
35802
36471
  let content;
35803
36472
  switch (what) {
35804
36473
  case "plan": {
35805
- const planPath = path31.join(zelari, "plan.md");
36474
+ const planPath = path33.join(zelari, "plan.md");
35806
36475
  try {
35807
36476
  content = await fs18.readFile(planPath, "utf-8");
35808
36477
  } catch {
@@ -35811,7 +36480,7 @@ async function handleWorkspaceShow(ctx, what) {
35811
36480
  break;
35812
36481
  }
35813
36482
  case "decisions": {
35814
- const decisionsDir = path31.join(zelari, "decisions");
36483
+ const decisionsDir = path33.join(zelari, "decisions");
35815
36484
  try {
35816
36485
  const files = (await fs18.readdir(decisionsDir)).filter((f) => f.endsWith(".md")).sort();
35817
36486
  if (files.length === 0) {
@@ -35821,7 +36490,7 @@ async function handleWorkspaceShow(ctx, what) {
35821
36490
  `];
35822
36491
  const { parseFrontmatter: parseFrontmatter2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
35823
36492
  for (const f of files) {
35824
- const raw = await fs18.readFile(path31.join(decisionsDir, f), "utf-8");
36493
+ const raw = await fs18.readFile(path33.join(decisionsDir, f), "utf-8");
35825
36494
  const { meta: meta3, body } = parseFrontmatter2(raw);
35826
36495
  const title = meta3.title ?? body.split("\n")[0]?.replace(/^#\s*/, "").trim() ?? f;
35827
36496
  lines.push(`- **${f.replace(/\.md$/, "")}** [${meta3.status ?? "unknown"}] ${title}`);
@@ -35834,7 +36503,7 @@ async function handleWorkspaceShow(ctx, what) {
35834
36503
  break;
35835
36504
  }
35836
36505
  case "risks": {
35837
- const risksPath = path31.join(zelari, "risks.md");
36506
+ const risksPath = path33.join(zelari, "risks.md");
35838
36507
  try {
35839
36508
  content = await fs18.readFile(risksPath, "utf-8");
35840
36509
  } catch {
@@ -35843,7 +36512,7 @@ async function handleWorkspaceShow(ctx, what) {
35843
36512
  break;
35844
36513
  }
35845
36514
  case "agents": {
35846
- const agentsPath = path31.join(process.cwd(), "AGENTS.MD");
36515
+ const agentsPath = path33.join(process.cwd(), "AGENTS.MD");
35847
36516
  try {
35848
36517
  content = await fs18.readFile(agentsPath, "utf-8");
35849
36518
  } catch {
@@ -35852,7 +36521,7 @@ async function handleWorkspaceShow(ctx, what) {
35852
36521
  break;
35853
36522
  }
35854
36523
  case "docs": {
35855
- const docsDir = path31.join(zelari, "docs");
36524
+ const docsDir = path33.join(zelari, "docs");
35856
36525
  try {
35857
36526
  const files = (await fs18.readdir(docsDir)).filter((f) => f.endsWith(".md")).sort();
35858
36527
  content = files.length ? `# Docs (${files.length})
@@ -35894,7 +36563,7 @@ async function handleWorkspaceReset(ctx, force) {
35894
36563
  return;
35895
36564
  }
35896
36565
  try {
35897
- const target = path31.join(process.cwd(), ".zelari");
36566
+ const target = path33.join(process.cwd(), ".zelari");
35898
36567
  await fs18.rm(target, { recursive: true, force: true });
35899
36568
  appendSystem(ctx.setMessages, "[workspace] .zelari/ removed");
35900
36569
  } catch (err) {
@@ -36253,11 +36922,11 @@ function handleModelsRefresh(ctx) {
36253
36922
  }
36254
36923
 
36255
36924
  // src/cli/slashHandlers/skills.ts
36256
- import path32 from "node:path";
36925
+ import path34 from "node:path";
36257
36926
  import os12 from "node:os";
36258
36927
 
36259
36928
  // src/cli/skillHistory.ts
36260
- 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";
36261
36930
  var SKILL_HISTORY_ROTATE_BYTES = 10 * 1024 * 1024;
36262
36931
  async function readSkillHistory(file2) {
36263
36932
  let raw = "";
@@ -36355,7 +37024,7 @@ async function applySteerInterrupt(options) {
36355
37024
 
36356
37025
  // src/cli/slashHandlers/skills.ts
36357
37026
  async function handleSkillStats(ctx, skillId) {
36358
- 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");
36359
37028
  try {
36360
37029
  const records = await readSkillHistory(historyFile);
36361
37030
  const stats = getSkillStats(records, skillId);
@@ -36371,7 +37040,7 @@ async function handleSkillCompare(ctx, ids, fallbackMessage) {
36371
37040
  appendSystem(ctx.setMessages, fallbackMessage ?? "[skill-compare] missing args");
36372
37041
  return;
36373
37042
  }
36374
- 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");
36375
37044
  try {
36376
37045
  const formatted = await compareSkillsFromFile(ids[0], ids[1], historyFile);
36377
37046
  appendSystem(ctx.setMessages, formatted);
@@ -36497,6 +37166,11 @@ function useSlashDispatch(params) {
36497
37166
  setSessionId(id);
36498
37167
  setMessages([]);
36499
37168
  setSessionActive(false);
37169
+ try {
37170
+ const { clearHistory: clearHistory2 } = await Promise.resolve().then(() => (init_conversationContext(), conversationContext_exports));
37171
+ clearHistory2();
37172
+ } catch {
37173
+ }
36500
37174
  onNewSession?.(id);
36501
37175
  }
36502
37176
  setInput("");
@@ -36729,6 +37403,40 @@ function useSlashDispatch(params) {
36729
37403
  setInput("");
36730
37404
  return;
36731
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
+ }
36732
37440
  if (result.kind === "promote_member" && result.promoteMemberId) {
36733
37441
  await handlePromoteMember(baseCtx, result.promoteMemberId);
36734
37442
  setInput("");
@@ -36760,6 +37468,11 @@ function useSlashDispatch(params) {
36760
37468
  }
36761
37469
  if (result.kind === "clear") {
36762
37470
  handleClearChat(setMessages, setSessionActive);
37471
+ try {
37472
+ const { clearHistory: clearHistory2 } = await Promise.resolve().then(() => (init_conversationContext(), conversationContext_exports));
37473
+ clearHistory2();
37474
+ } catch {
37475
+ }
36763
37476
  params.onClear?.();
36764
37477
  setInput("");
36765
37478
  return;
@@ -36912,6 +37625,7 @@ function App() {
36912
37625
  const [sessionStats, setSessionStats] = useState8({ totalTokens: 0, totalCostUsd: 0, cachedTokens: 0 });
36913
37626
  const [clearEpoch, setClearEpoch] = useState8(0);
36914
37627
  const [mode, setMode] = useState8("agent");
37628
+ const [phase2, setPhaseUi] = useState8("build");
36915
37629
  const [picker, setPicker] = useState8(null);
36916
37630
  const activeProviderSpec = getActiveProvider();
36917
37631
  const activeModel = providerConfig.modelByProvider[activeProviderSpec.id];
@@ -36990,7 +37704,8 @@ function App() {
36990
37704
  openPicker: setPicker,
36991
37705
  onNewSession,
36992
37706
  onExit,
36993
- onClear
37707
+ onClear,
37708
+ onPhaseChange: setPhaseUi
36994
37709
  });
36995
37710
  const onPickerSelect = useCallback5((value) => {
36996
37711
  if (!picker) return;
@@ -37002,15 +37717,23 @@ function App() {
37002
37717
  const cmd = `${picker.commandPrefix} ${value}`;
37003
37718
  void handleSubmit(cmd);
37004
37719
  }, [picker, handleSubmit]);
37005
- const onPickerCancel = useCallback5(() => setPicker(null), []);
37720
+ const onPickerCancel = useCallback5(() => {
37721
+ if (picker?.kind === "clarification") {
37722
+ picker.onCancel?.();
37723
+ }
37724
+ setPicker(null);
37725
+ }, [picker]);
37006
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);
37007
37730
  return {
37008
37731
  id: "banner-once",
37009
37732
  role: "system",
37010
37733
  ts: 0,
37011
- content: `zelari-code v${VERSION} \u2014 ${activeProviderSpec.id}/${activeModel}
37734
+ content: `${left}${" ".repeat(pad)}${right}
37012
37735
  cwd: ${cwd}
37013
- /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)`
37014
37737
  };
37015
37738
  }, [activeProviderSpec.id, activeModel, cwd]);
37016
37739
  const staticKey = `${session.sessionId || "pre-bootstrap"}-${clearEpoch}`;
@@ -37043,11 +37766,15 @@ cwd: ${cwd}
37043
37766
  queueCount: chatTurn.queueCount,
37044
37767
  busy,
37045
37768
  mode,
37769
+ phase: phase2,
37046
37770
  cwd,
37047
37771
  elapsedMs: timer.elapsedMs,
37048
37772
  lastMs: timer.lastMs,
37049
37773
  costUsd: sessionStats.totalCostUsd,
37050
- cachedTokens: sessionStats.cachedTokens
37774
+ cachedTokens: sessionStats.cachedTokens,
37775
+ contextUsed: sessionStats.totalTokens,
37776
+ contextLimit: Number(process.env.ZELARI_CONTEXT_LIMIT) || 2e5,
37777
+ brandVersion: VERSION
37051
37778
  }
37052
37779
  )), showSidebar && /* @__PURE__ */ React9.createElement(Sidebar, { version: VERSION, changes: gitChanges, rows: size.rows })));
37053
37780
  }
@@ -37233,28 +37960,28 @@ var CHOICE_LATER = "__later__";
37233
37960
  var CHOICE_NEVER = "__never__";
37234
37961
  function PluginGate({ cwd, children }) {
37235
37962
  const { isRawModeSupported } = useStdin4();
37236
- const [phase, setPhase] = useState10("detecting");
37963
+ const [phase2, setPhase2] = useState10("detecting");
37237
37964
  const [queue, setQueue] = useState10([]);
37238
37965
  const [current, setCurrent] = useState10(null);
37239
37966
  const [result, setResult] = useState10(null);
37240
37967
  const skipGate = process.env.ZELARI_NO_PLUGIN_PROMPT === "1" || isRawModeSupported !== true;
37241
37968
  useEffect9(() => {
37242
37969
  if (skipGate) {
37243
- setPhase("done");
37970
+ setPhase2("done");
37244
37971
  return;
37245
37972
  }
37246
37973
  let cancelled = false;
37247
37974
  void detectMissingPlugins(cwd).then((missing) => {
37248
37975
  if (cancelled) return;
37249
37976
  if (missing.length === 0) {
37250
- setPhase("done");
37977
+ setPhase2("done");
37251
37978
  return;
37252
37979
  }
37253
37980
  setQueue(missing);
37254
37981
  setCurrent(missing[0] ?? null);
37255
- setPhase(missing[0] ? "prompting" : "done");
37982
+ setPhase2(missing[0] ? "prompting" : "done");
37256
37983
  }).catch(() => {
37257
- if (!cancelled) setPhase("done");
37984
+ if (!cancelled) setPhase2("done");
37258
37985
  });
37259
37986
  return () => {
37260
37987
  cancelled = true;
@@ -37264,12 +37991,12 @@ function PluginGate({ cwd, children }) {
37264
37991
  setQueue((q) => {
37265
37992
  const rest = q.slice(1);
37266
37993
  if (rest.length === 0) {
37267
- setPhase("done");
37994
+ setPhase2("done");
37268
37995
  setCurrent(null);
37269
37996
  return rest;
37270
37997
  }
37271
37998
  setCurrent(rest[0] ?? null);
37272
- setPhase(rest[0] ? "prompting" : "done");
37999
+ setPhase2(rest[0] ? "prompting" : "done");
37273
38000
  setResult(null);
37274
38001
  return rest;
37275
38002
  });
@@ -37283,31 +38010,49 @@ function PluginGate({ cwd, children }) {
37283
38010
  markDontAskAgain(current.id);
37284
38011
  advance();
37285
38012
  } else if (value === CHOICE_INSTALL) {
37286
- setPhase("installing");
37287
- 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
+ }
37288
38033
  setResult(r);
37289
- setPhase("result");
38034
+ setPhase2("result");
37290
38035
  }).catch(() => {
37291
38036
  setResult({ ok: false, output: "", exitCode: null, error: "unexpected error" });
37292
- setPhase("result");
38037
+ setPhase2("result");
37293
38038
  });
37294
38039
  }
37295
38040
  },
37296
38041
  [current, cwd, advance]
37297
38042
  );
37298
- if (phase === "done") {
38043
+ if (phase2 === "done") {
37299
38044
  return /* @__PURE__ */ React11.createElement(React11.Fragment, null, children);
37300
38045
  }
37301
- if (phase === "detecting") {
38046
+ if (phase2 === "detecting") {
37302
38047
  return /* @__PURE__ */ React11.createElement(Box10, { paddingX: 1 }, /* @__PURE__ */ React11.createElement(Text11, { dimColor: true }, "Checking for optional tool plugins\u2026"));
37303
38048
  }
37304
38049
  if (!current) {
37305
38050
  return /* @__PURE__ */ React11.createElement(React11.Fragment, null, children);
37306
38051
  }
37307
- if (phase === "installing") {
38052
+ if (phase2 === "installing") {
37308
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));
37309
38054
  }
37310
- if (phase === "result") {
38055
+ if (phase2 === "result") {
37311
38056
  const ok = result?.ok === true;
37312
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 }));
37313
38058
  }
@@ -37345,9 +38090,9 @@ function ContinueKey({ onContinue }) {
37345
38090
  init_providerConfig();
37346
38091
 
37347
38092
  // src/cli/wizard/firstRun.ts
37348
- import { existsSync as existsSync29 } from "node:fs";
38093
+ import { existsSync as existsSync30 } from "node:fs";
37349
38094
  function shouldRunWizard(input) {
37350
- const exists = input.exists ?? existsSync29;
38095
+ const exists = input.exists ?? existsSync30;
37351
38096
  if (input.hasResetConfigFlag) {
37352
38097
  return { shouldRun: true, reason: "--reset-config flag forced wizard" };
37353
38098
  }
@@ -37705,6 +38450,7 @@ function emitEvent(event) {
37705
38450
  init_harness();
37706
38451
  init_toolRegistry();
37707
38452
  init_skills2();
38453
+ init_envNumber();
37708
38454
  async function runHeadless(opts) {
37709
38455
  const { provider, model } = resolveHeadlessProvider(opts);
37710
38456
  const key = await resolveHeadlessKey(provider);
@@ -37893,7 +38639,7 @@ async function runHeadlessCouncil(opts, provider, model, providerStream) {
37893
38639
 
37894
38640
  // src/cli/skillsMd.ts
37895
38641
  init_skills2();
37896
- 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";
37897
38643
  import { join as join23 } from "node:path";
37898
38644
  import { homedir as homedir6 } from "node:os";
37899
38645
  var CODING_CATEGORIES = /* @__PURE__ */ new Set([
@@ -37971,7 +38717,7 @@ function loadSkillMdSkills(projectRoot = process.cwd(), options = {}) {
37971
38717
  const summary = { loaded: [], skipped: [] };
37972
38718
  const seen = new Set(options.existingIds ?? []);
37973
38719
  for (const dir of skillMdSearchDirs(projectRoot)) {
37974
- if (!existsSync30(dir)) continue;
38720
+ if (!existsSync31(dir)) continue;
37975
38721
  let entries;
37976
38722
  try {
37977
38723
  entries = readdirSync4(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
@@ -37980,7 +38726,7 @@ function loadSkillMdSkills(projectRoot = process.cwd(), options = {}) {
37980
38726
  }
37981
38727
  for (const entry of entries) {
37982
38728
  const skillPath = join23(dir, entry, "SKILL.md");
37983
- if (!existsSync30(skillPath)) continue;
38729
+ if (!existsSync31(skillPath)) continue;
37984
38730
  try {
37985
38731
  const parsed = parseSkillMd(readFileSync25(skillPath, "utf8"), skillPath);
37986
38732
  if (!parsed) {