zelari-code 0.7.8 → 0.7.9

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 (78) hide show
  1. package/README.md +27 -1
  2. package/bin/zelari-code.js +1 -1
  3. package/dist/agents/councilApi.js +7 -7
  4. package/dist/agents/councilApi.js.map +1 -1
  5. package/dist/agents/councilDirectives.js +3 -3
  6. package/dist/agents/promoteMember.js +2 -2
  7. package/dist/agents/roles.js +50 -32
  8. package/dist/agents/roles.js.map +1 -1
  9. package/dist/agents/skills/builtin/debugging.js +4 -4
  10. package/dist/agents/skills/builtin/debugging.js.map +1 -1
  11. package/dist/agents/skills/builtin/docs.js +23 -13
  12. package/dist/agents/skills/builtin/docs.js.map +1 -1
  13. package/dist/agents/skills/builtin/git-ops.js +3 -3
  14. package/dist/agents/skills/builtin/git-ops.js.map +1 -1
  15. package/dist/agents/skills/builtin/planning.js +4 -4
  16. package/dist/agents/skills/builtin/planning.js.map +1 -1
  17. package/dist/agents/skills/builtin/refactoring.js +3 -3
  18. package/dist/agents/skills/builtin/refactoring.js.map +1 -1
  19. package/dist/agents/skills/builtin/review.js +9 -9
  20. package/dist/agents/skills/builtin/review.js.map +1 -1
  21. package/dist/agents/skills/builtin/testing.js +3 -3
  22. package/dist/agents/skills/builtin/testing.js.map +1 -1
  23. package/dist/agents/tools.js +2 -9
  24. package/dist/agents/tools.js.map +1 -1
  25. package/dist/cli/app.js +48 -17
  26. package/dist/cli/app.js.map +1 -1
  27. package/dist/cli/components/CollapsibleToolOutput.js +8 -26
  28. package/dist/cli/components/CollapsibleToolOutput.js.map +1 -1
  29. package/dist/cli/components/Header.js +6 -18
  30. package/dist/cli/components/Header.js.map +1 -1
  31. package/dist/cli/components/Sidebar.js +86 -37
  32. package/dist/cli/components/Sidebar.js.map +1 -1
  33. package/dist/cli/components/SplashScreen.js +117 -71
  34. package/dist/cli/components/SplashScreen.js.map +1 -1
  35. package/dist/cli/components/StatusBar.js +22 -18
  36. package/dist/cli/components/StatusBar.js.map +1 -1
  37. package/dist/cli/councilConfig.js +38 -0
  38. package/dist/cli/councilConfig.js.map +1 -0
  39. package/dist/cli/councilDispatcher.js +35 -49
  40. package/dist/cli/councilDispatcher.js.map +1 -1
  41. package/dist/cli/hooks/useChatTurn.js +80 -61
  42. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  43. package/dist/cli/hooks/useExecutionTimer.js +25 -0
  44. package/dist/cli/hooks/useExecutionTimer.js.map +1 -0
  45. package/dist/cli/hooks/useGitChanges.js +142 -0
  46. package/dist/cli/hooks/useGitChanges.js.map +1 -0
  47. package/dist/cli/hooks/useSlashDispatch.js +9 -2
  48. package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
  49. package/dist/cli/main.bundled.js +1668 -1151
  50. package/dist/cli/main.bundled.js.map +4 -4
  51. package/dist/cli/main.js +1 -1
  52. package/dist/cli/mcp/mcpClient.js +14 -6
  53. package/dist/cli/mcp/mcpClient.js.map +1 -1
  54. package/dist/cli/updater.js +8 -5
  55. package/dist/cli/updater.js.map +1 -1
  56. package/dist/cli/utils/cmdline.js +26 -0
  57. package/dist/cli/utils/cmdline.js.map +1 -0
  58. package/dist/cli/utils/paths.js +20 -0
  59. package/dist/cli/utils/paths.js.map +1 -0
  60. package/dist/cli/workspace/planDetect.js +17 -0
  61. package/dist/cli/workspace/planDetect.js.map +1 -0
  62. package/dist/cli/workspace/postCouncilHook.js +39 -28
  63. package/dist/cli/workspace/postCouncilHook.js.map +1 -1
  64. package/dist/cli/workspace/stubs.js +241 -196
  65. package/dist/cli/workspace/stubs.js.map +1 -1
  66. package/dist/main/core/AgentHarness.js +36 -7
  67. package/dist/main/core/AgentHarness.js.map +1 -1
  68. package/dist/main/core/tools/builtin/listFiles.js +43 -5
  69. package/dist/main/core/tools/builtin/listFiles.js.map +1 -1
  70. package/dist/main/core/tools/builtin/search.js +28 -120
  71. package/dist/main/core/tools/builtin/search.js.map +1 -1
  72. package/package.json +71 -71
  73. package/dist/cli/oauthCallbackServer.js +0 -207
  74. package/dist/cli/oauthCallbackServer.js.map +0 -1
  75. package/dist/main/core/tools/builtin/_walk.js +0 -129
  76. package/dist/main/core/tools/builtin/_walk.js.map +0 -1
  77. package/dist/main/core/tools/builtin/diff.js +0 -477
  78. package/dist/main/core/tools/builtin/diff.js.map +0 -1
@@ -15865,7 +15865,7 @@ function resolveBashWindows() {
15865
15865
  return p3;
15866
15866
  }
15867
15867
  try {
15868
- const result = spawnSync("where", ["bash"], { shell: true, encoding: "utf-8" });
15868
+ const result = spawnSync("where", ["bash"], { encoding: "utf-8", windowsHide: true });
15869
15869
  if (result.status === 0 && result.stdout) {
15870
15870
  const first = result.stdout.split(/\r?\n/).find((l) => l.trim().length > 0);
15871
15871
  if (first && existsSyncSafe(first))
@@ -18673,12 +18673,12 @@ import {
18673
18673
  realpathSync
18674
18674
  } from "node:fs";
18675
18675
  import { join, basename } from "node:path";
18676
- import { homedir } from "node:os";
18676
+ import { homedir as homedir2 } from "node:os";
18677
18677
  import { createHash } from "node:crypto";
18678
18678
  function resolveWorkspaceRoot(projectRoot = process.cwd()) {
18679
18679
  const candidates = [
18680
18680
  join(projectRoot, ".zelari"),
18681
- join(homedir(), ".zelari-code", "workspace", hashProject(projectRoot))
18681
+ join(homedir2(), ".zelari-code", "workspace", hashProject(projectRoot))
18682
18682
  ];
18683
18683
  for (const candidate of candidates) {
18684
18684
  if (isWritableDir(projectRoot) || candidate !== candidates[0]) {
@@ -19273,7 +19273,14 @@ __export(stubs_exports, {
19273
19273
  readPlanSummary: () => readPlanSummary,
19274
19274
  resolveWorkspaceRoot: () => resolveWorkspaceRoot
19275
19275
  });
19276
- import { existsSync as existsSync9, readdirSync as readdirSync3, writeFileSync as writeFileSync6, readFileSync as readFileSync6, mkdirSync as mkdirSync7, renameSync as renameSync3 } from "node:fs";
19276
+ import {
19277
+ existsSync as existsSync9,
19278
+ readdirSync as readdirSync3,
19279
+ writeFileSync as writeFileSync6,
19280
+ readFileSync as readFileSync6,
19281
+ mkdirSync as mkdirSync7,
19282
+ renameSync as renameSync3
19283
+ } from "node:fs";
19277
19284
  import { join as join4, basename as basename2, dirname as dirname2, relative as relative2 } from "node:path";
19278
19285
  function createWorkspaceContext(projectRoot = process.cwd()) {
19279
19286
  const rootDir = resolveWorkspaceRoot(projectRoot);
@@ -19290,7 +19297,9 @@ function readPlan(ctx) {
19290
19297
  const jsonPath = planJsonPath(ctx);
19291
19298
  if (existsSync9(jsonPath)) {
19292
19299
  try {
19293
- const parsed = JSON.parse(readFileSync6(jsonPath, "utf8"));
19300
+ const parsed = JSON.parse(
19301
+ readFileSync6(jsonPath, "utf8")
19302
+ );
19294
19303
  return {
19295
19304
  phases: Array.isArray(parsed.phases) ? parsed.phases : [],
19296
19305
  tasks: Array.isArray(parsed.tasks) ? parsed.tasks : [],
@@ -19334,8 +19343,12 @@ function renderPlanBody(summary) {
19334
19343
  if (summary.phases.length === 0) {
19335
19344
  lines.push(`_(none yet)_`);
19336
19345
  } else {
19337
- for (const phase of [...summary.phases].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))) {
19338
- lines.push(`### ${phase.order ?? "?"}. ${phase.id} \`${phase.color ?? ""}\``);
19346
+ for (const phase of [...summary.phases].sort(
19347
+ (a, b) => (a.order ?? 0) - (b.order ?? 0)
19348
+ )) {
19349
+ lines.push(
19350
+ `### ${phase.order ?? "?"}. ${phase.id} \`${phase.color ?? ""}\``
19351
+ );
19339
19352
  lines.push("");
19340
19353
  }
19341
19354
  }
@@ -19630,30 +19643,32 @@ function updateTaskStub(ctx) {
19630
19643
  return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
19631
19644
  const taskId = args["taskId"];
19632
19645
  const rawStatus = args["status"];
19633
- if (!taskId || !rawStatus) return "updateTask requires taskId and status.";
19646
+ if (!taskId || !rawStatus)
19647
+ return "updateTask requires taskId and status.";
19634
19648
  const valid = ["pending", "in_progress", "done", "blocked"];
19635
19649
  const aliases = {
19636
- "todo": "pending",
19650
+ todo: "pending",
19637
19651
  "to-do": "pending",
19638
- "open": "pending",
19639
- "backlog": "pending",
19652
+ open: "pending",
19653
+ backlog: "pending",
19640
19654
  "in-progress": "in_progress",
19641
19655
  "in progress": "in_progress",
19642
- "doing": "in_progress",
19643
- "started": "in_progress",
19644
- "active": "in_progress",
19645
- "complete": "done",
19646
- "completed": "done",
19647
- "finished": "done",
19648
- "closed": "done",
19649
- "resolved": "done",
19650
- "stuck": "blocked",
19651
- "waiting": "blocked",
19656
+ doing: "in_progress",
19657
+ started: "in_progress",
19658
+ active: "in_progress",
19659
+ complete: "done",
19660
+ completed: "done",
19661
+ finished: "done",
19662
+ closed: "done",
19663
+ resolved: "done",
19664
+ stuck: "blocked",
19665
+ waiting: "blocked",
19652
19666
  "on-hold": "blocked",
19653
19667
  "on hold": "blocked"
19654
19668
  };
19655
19669
  const status = valid.includes(rawStatus) ? rawStatus : aliases[rawStatus.toLowerCase()];
19656
- if (!status) return `Invalid status: ${rawStatus}. Must be one of: ${valid.join(", ")}.`;
19670
+ if (!status)
19671
+ return `Invalid status: ${rawStatus}. Must be one of: ${valid.join(", ")}.`;
19657
19672
  const summary = readPlan(ctx);
19658
19673
  const task = summary.tasks.find((t) => t.id === taskId);
19659
19674
  if (!task) return `Task "${taskId}" not found.`;
@@ -19747,16 +19762,27 @@ function createDocumentStub(ctx) {
19747
19762
  const content = args["content"] ?? "";
19748
19763
  const tags = args["tags"] ?? [];
19749
19764
  const normalizedTitle = title.replace(/\.(md|markdown)\s*$/i, "");
19750
- const id = slugify2(normalizedTitle) || `doc-${Date.now()}`;
19751
- const path22 = workspaceArtifact(ctx.rootDir, "docs", id);
19765
+ const slug = slugify2(normalizedTitle) || `doc-${Date.now()}`;
19766
+ if (slug === "risks") {
19767
+ const risksPath = workspaceFile(ctx.rootDir, "risks");
19768
+ const riskMeta = {
19769
+ kind: "risk",
19770
+ id: "risks",
19771
+ severity: "medium",
19772
+ date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
19773
+ };
19774
+ ctx.storage.write(risksPath, riskMeta, content);
19775
+ return `Document "${title}" created at risks.md (workspace root).`;
19776
+ }
19777
+ const path22 = workspaceArtifact(ctx.rootDir, "docs", slug);
19752
19778
  const meta3 = {
19753
19779
  kind: "doc",
19754
- id,
19780
+ id: slug,
19755
19781
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
19756
19782
  tags
19757
19783
  };
19758
19784
  ctx.storage.write(path22, meta3, content);
19759
- return `Document "${title}" created at docs/${id}.md.`;
19785
+ return `Document "${title}" created at docs/${slug}.md.`;
19760
19786
  });
19761
19787
  }
19762
19788
  };
@@ -19777,7 +19803,9 @@ function searchDocumentsStub(ctx) {
19777
19803
  const limit = args["limit"] ?? 5;
19778
19804
  if (!rawQuery) return "searchDocuments requires a query.";
19779
19805
  const phrases = rawQuery.toLowerCase().split(/\s+or\s+/i).map((p3) => p3.trim()).filter((p3) => p3.length > 0);
19780
- const words = rawQuery.toLowerCase().split(/[^a-z0-9_-]+/).filter((w) => w.length >= 3 && w !== "or" && w !== "and" && w !== "the");
19806
+ const words = rawQuery.toLowerCase().split(/[^a-z0-9_-]+/).filter(
19807
+ (w) => w.length >= 3 && w !== "or" && w !== "and" && w !== "the"
19808
+ );
19781
19809
  const files = [
19782
19810
  ...ctx.storage.listMarkdown(join4(ctx.rootDir, "decisions")),
19783
19811
  ...ctx.storage.listMarkdown(join4(ctx.rootDir, "docs")),
@@ -19816,10 +19844,14 @@ function searchDocumentsStub(ctx) {
19816
19844
  const start = Math.max(0, idx - 40);
19817
19845
  const end = Math.min(raw.length, idx + matchLen + 40);
19818
19846
  const snippet = "\u2026" + raw.slice(start, end).replace(/\n/g, " ").trim() + "\u2026";
19819
- results.push({ path: relative2(ctx.rootDir, file2) || basename2(file2), snippet });
19847
+ results.push({
19848
+ path: relative2(ctx.rootDir, file2) || basename2(file2),
19849
+ snippet
19850
+ });
19820
19851
  if (results.length >= limit) break;
19821
19852
  }
19822
- if (results.length === 0) return `${nudge}No matches for "${rawQuery}" in workspace.`;
19853
+ if (results.length === 0)
19854
+ return `${nudge}No matches for "${rawQuery}" in workspace.`;
19823
19855
  return nudge + results.map((r) => `[${r.path}]
19824
19856
  ${r.snippet}`).join("\n\n");
19825
19857
  }
@@ -19877,7 +19909,9 @@ function getDocumentBacklinksStub(ctx) {
19877
19909
  const backlinks = [];
19878
19910
  for (const file2 of allFiles) {
19879
19911
  try {
19880
- const doc = ctx.storage.read(file2);
19912
+ const doc = ctx.storage.read(
19913
+ file2
19914
+ );
19881
19915
  if (doc.meta.id === targetId) continue;
19882
19916
  if ((doc.meta.related ?? []).includes(targetId)) {
19883
19917
  backlinks.push(doc.meta.id);
@@ -19951,12 +19985,28 @@ var init_toolRegistry = __esm({
19951
19985
  }
19952
19986
  });
19953
19987
 
19988
+ // src/cli/utils/cmdline.ts
19989
+ function quoteCmdArg(arg) {
19990
+ if (arg === "") return '""';
19991
+ if (!/[\s"^&|<>()%!]/.test(arg)) return arg;
19992
+ return `"${arg.replace(/"/g, '""')}"`;
19993
+ }
19994
+ function buildCmdLine(command, args) {
19995
+ return [command, ...args].map(quoteCmdArg).join(" ");
19996
+ }
19997
+ var init_cmdline = __esm({
19998
+ "src/cli/utils/cmdline.ts"() {
19999
+ "use strict";
20000
+ }
20001
+ });
20002
+
19954
20003
  // src/cli/mcp/mcpClient.ts
19955
20004
  import { spawn as spawn2 } from "node:child_process";
19956
20005
  var DEFAULT_REQUEST_TIMEOUT_MS, INIT_TIMEOUT_MS, MCP_PROTOCOL_VERSION, McpClient;
19957
20006
  var init_mcpClient = __esm({
19958
20007
  "src/cli/mcp/mcpClient.ts"() {
19959
20008
  "use strict";
20009
+ init_cmdline();
19960
20010
  DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
19961
20011
  INIT_TIMEOUT_MS = 15e3;
19962
20012
  MCP_PROTOCOL_VERSION = "2025-03-26";
@@ -19973,14 +20023,15 @@ var init_mcpClient = __esm({
19973
20023
  /** Spawn the server process and run the MCP initialize handshake. */
19974
20024
  async start() {
19975
20025
  if (this.child) return;
19976
- const child = spawn2(this.config.command, this.config.args ?? [], {
20026
+ const spawnOpts = {
19977
20027
  stdio: ["pipe", "pipe", "pipe"],
19978
20028
  env: { ...process.env, ...this.config.env ?? {} },
19979
- // On Windows `npx`/`uvx` resolve to .cmd shims which plain spawn
19980
- // cannot execute; shell:true lets cmd.exe resolve them.
19981
- shell: process.platform === "win32",
19982
20029
  windowsHide: true
19983
- });
20030
+ };
20031
+ const child = process.platform === "win32" ? spawn2(buildCmdLine(this.config.command, this.config.args ?? []), {
20032
+ ...spawnOpts,
20033
+ shell: true
20034
+ }) : spawn2(this.config.command, this.config.args ?? [], spawnOpts);
19984
20035
  this.child = child;
19985
20036
  child.stdout.setEncoding("utf8");
19986
20037
  child.stdout.on("data", (chunk) => this.onStdout(chunk));
@@ -19995,7 +20046,7 @@ var init_mcpClient = __esm({
19995
20046
  {
19996
20047
  protocolVersion: MCP_PROTOCOL_VERSION,
19997
20048
  capabilities: {},
19998
- clientInfo: { name: "zelari-code", version: "0.7.8" }
20049
+ clientInfo: { name: "zelari-code", version: "0.7.9" }
19999
20050
  },
20000
20051
  INIT_TIMEOUT_MS
20001
20052
  );
@@ -20097,11 +20148,11 @@ __export(mcpManager_exports, {
20097
20148
  });
20098
20149
  import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
20099
20150
  import { join as join5 } from "node:path";
20100
- import { homedir as homedir2 } from "node:os";
20151
+ import { homedir as homedir3 } from "node:os";
20101
20152
  function readMcpConfig(projectRoot = process.cwd()) {
20102
20153
  const merged = {};
20103
20154
  const paths = [
20104
- join5(homedir2(), ".zelari-code", "mcp.json"),
20155
+ join5(homedir3(), ".zelari-code", "mcp.json"),
20105
20156
  join5(projectRoot, ".zelari", "mcp.json")
20106
20157
  // later = higher precedence
20107
20158
  ];
@@ -20388,7 +20439,7 @@ You may optionally add a 4th design system doc if the project warrants it.
20388
20439
  Pass the tool call as: \`createDocument({ title: "<category>", content: "<markdown body>" })\`. Do NOT summarize these into prose \u2014 they are the deliverable.`,
20389
20440
  // v0.7.2: explore the codebase to ground ideas in what exists.
20390
20441
  tools: ["list_files", "read_file"],
20391
- skills: ["idea-synthesizer", "mind-mapper", "document-writer"]
20442
+ skills: ["document-writer", "mind-mapper"]
20392
20443
  },
20393
20444
  {
20394
20445
  id: "pluton",
@@ -20413,10 +20464,17 @@ Pass the tool call as: \`createDocument({ title: "<category>", content: "<markdo
20413
20464
  - Keep the graph comprehensible: prune redundant or duplicate nodes.
20414
20465
 
20415
20466
  ## Output format
20416
- Describe the proposed structure (root \u2192 branches \u2192 leaves) in text and, when building via tool, emit a buildMindMap payload. Stay under 200 words.${CLARIFICATION_PROTOCOL8}`,
20467
+ Describe the proposed structure (root \u2192 branches \u2192 leaves) in text. Stay under 200 words.${CLARIFICATION_PROTOCOL8}
20468
+
20469
+ ## Design-phase artifact (mandatory when running council in design-phase mode)
20470
+ Persist the knowledge map as ONE \`createDocument\` call:
20471
+
20472
+ \`createDocument({ title: "knowledge-map", content: "<markdown: root concept, branches, leaf nodes, and cross-links>" })\`
20473
+
20474
+ Do NOT rely on buildMindMap \u2014 it is not available in the CLI workspace.`,
20417
20475
  // v0.7.2: map the actual code/module structure instead of an abstract mind-map.
20418
20476
  tools: ["list_files", "read_file", "grep_content"],
20419
- skills: ["mind-mapper", "research-analyst"]
20477
+ skills: ["document-writer", "research-analyst"]
20420
20478
  },
20421
20479
  {
20422
20480
  id: "minos",
@@ -20457,9 +20515,9 @@ createDocument({
20457
20515
  })
20458
20516
  \`\`\`
20459
20517
 
20460
- Include AT LEAST 5 risks, each scored on Impact and Likelihood, with a one-line mitigation. Cover: technical (e.g. stack risk), product (e.g. scope creep), accessibility, performance, security. Do NOT emit other workspace artifacts \u2014 your role is to evaluate, not to build.`,
20518
+ Include AT LEAST 5 risks, each scored on Impact and Likelihood, with a one-line mitigation. Cover: technical (e.g. stack risk), product (e.g. scope creep), accessibility, performance, security. The artifact is persisted at \`.zelari/risks.md\` (workspace root), NOT under docs/. Do NOT emit other workspace artifacts \u2014 your role is to evaluate, not to build.`,
20461
20519
  tools: [],
20462
- skills: ["research-analyst"]
20520
+ skills: ["document-writer", "research-analyst"]
20463
20521
  },
20464
20522
  {
20465
20523
  id: "lucifer",
@@ -20852,6 +20910,57 @@ var init_systemPromptBuilder = __esm({
20852
20910
  }
20853
20911
  });
20854
20912
 
20913
+ // packages/core/dist/council/modeBanners.js
20914
+ function councilModeBanner(runMode) {
20915
+ return runMode === "design-phase" ? DESIGN_PHASE_MODE_BANNER : IMPLEMENTATION_MODE_BANNER;
20916
+ }
20917
+ var DESIGN_PHASE_MODE_BANNER, IMPLEMENTATION_MODE_BANNER;
20918
+ var init_modeBanners = __esm({
20919
+ "packages/core/dist/council/modeBanners.js"() {
20920
+ "use strict";
20921
+ DESIGN_PHASE_MODE_BANNER = `COUNCIL RUN MODE: design-phase.
20922
+ Workspace tool emissions described in your role prompt are MANDATORY in this run \u2014 prose alone does not count as a deliverable. Persist artifacts via the workspace tools listed in your AVAILABLE TOOLS section.`;
20923
+ IMPLEMENTATION_MODE_BANNER = `COUNCIL RUN MODE: implementation.
20924
+ Prefer write_file, edit_file, and bash for code changes. Design-phase mandatory workspace blocks in your role prompt are INACTIVE in this run \u2014 only call workspace tools when they add durable project value.`;
20925
+ }
20926
+ });
20927
+
20928
+ // packages/core/dist/council/runMode.js
20929
+ function resolveCouncilRunMode(input) {
20930
+ if (input.forceMode) {
20931
+ return input.forceMode;
20932
+ }
20933
+ const envMode = input.env?.ZELARI_COUNCIL_MODE?.toLowerCase();
20934
+ if (envMode === "design" || envMode === "design-phase") {
20935
+ return "design-phase";
20936
+ }
20937
+ if (envMode === "implementation" || envMode === "impl") {
20938
+ return "implementation";
20939
+ }
20940
+ const msg = input.userMessage;
20941
+ const hasDesign = DESIGN_KEYWORDS.test(msg);
20942
+ const hasImpl = IMPLEMENTATION_KEYWORDS.test(msg);
20943
+ if (hasDesign && !hasImpl) {
20944
+ return "design-phase";
20945
+ }
20946
+ if (PLAN_CONTINUE.test(msg) && input.hasExistingPlan) {
20947
+ return "design-phase";
20948
+ }
20949
+ return "implementation";
20950
+ }
20951
+ function councilTierFromSize(councilSize) {
20952
+ return councilSize >= 6 ? "full" : "lite";
20953
+ }
20954
+ var DESIGN_KEYWORDS, IMPLEMENTATION_KEYWORDS, PLAN_CONTINUE;
20955
+ var init_runMode = __esm({
20956
+ "packages/core/dist/council/runMode.js"() {
20957
+ "use strict";
20958
+ DESIGN_KEYWORDS = /\b(design|architect|architecture|spec|blueprint|mockup|wireframe|greenfield|from scratch)\b/i;
20959
+ IMPLEMENTATION_KEYWORDS = /\b(fix|refactor|bug|implement|patch|migrate|add tests|debug|repair|hotfix)\b/i;
20960
+ PLAN_CONTINUE = /\b(continue|extend|update|refine)\b[\s\S]{0,40}\b(plan|phase|milestone)\b/i;
20961
+ }
20962
+ });
20963
+
20855
20964
  // packages/core/dist/agents/councilApi.js
20856
20965
  function parseClarificationRequest(text) {
20857
20966
  const start = text.indexOf(QUESTION_MARKER);
@@ -20884,7 +20993,7 @@ function parseThinking(text) {
20884
20993
  function cleanAgentContent(text) {
20885
20994
  return text.replace(/<think>[\s\S]*?<\/think>/g, "").replace(/<minimax:tool_call>[\s\S]*?<\/minimax:tool_call>/g, "").replace(/---QUESTION---[\s\S]*?---END---/g, "").trim();
20886
20995
  }
20887
- function buildAgentMessages(agent, userMessage, ragContext, workspaceContext, priorOutputs, aiConfig, executableTools) {
20996
+ function buildAgentMessages(agent, userMessage, ragContext, workspaceContext, priorOutputs, aiConfig, executableTools, runMode = "implementation") {
20888
20997
  const allToolNames = computeAgentTools(agent, aiConfig);
20889
20998
  const toolNames = executableTools ? allToolNames.filter((n) => executableTools.has(n)) : allToolNames;
20890
20999
  const enhancedSystemPrompt = buildSystemPrompt(agent, {
@@ -20896,6 +21005,7 @@ function buildAgentMessages(agent, userMessage, ragContext, workspaceContext, pr
20896
21005
  });
20897
21006
  const messages = [
20898
21007
  { role: "system", content: enhancedSystemPrompt },
21008
+ { role: "system", content: councilModeBanner(runMode) },
20899
21009
  { role: "system", content: "IMPORTANT: Before making any tool calls or expensive operations, check if the information already exists in the shared context from previous agents. Avoid redundant work." }
20900
21010
  ];
20901
21011
  if (ragContext) {
@@ -20926,6 +21036,13 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
20926
21036
  ...config2.existingOutputs ?? []
20927
21037
  ];
20928
21038
  const sessionId = config2.sessionId ?? crypto.randomUUID();
21039
+ const runMode = config2.runMode ?? "implementation";
21040
+ const isDesignPhase = runMode === "design-phase";
21041
+ yield createBrainEvent("council_mode", sessionId, {
21042
+ tier: councilTierFromSize(config2.councilSize),
21043
+ councilSize: config2.councilSize,
21044
+ runMode
21045
+ });
20929
21046
  yield {
20930
21047
  type: "agent_start",
20931
21048
  id: crypto.randomUUID(),
@@ -20979,7 +21096,7 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
20979
21096
  model: effectiveModel,
20980
21097
  provider: effectiveProvider,
20981
21098
  sessionId,
20982
- messages: buildAgentMessages(agent, userMessage, config2.ragContext, config2.workspaceContext, agentOutputs, config2.aiConfig, executableNames),
21099
+ messages: buildAgentMessages(agent, userMessage, config2.ragContext, config2.workspaceContext, agentOutputs, config2.aiConfig, executableNames, runMode),
20983
21100
  tools: agentTools,
20984
21101
  eventBus: config2.eventBus,
20985
21102
  toolRegistry: config2.tools,
@@ -21025,7 +21142,7 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
21025
21142
  fullText = `Error: ${err instanceof Error ? err.message : "Unknown"}`;
21026
21143
  errored = true;
21027
21144
  }
21028
- if (!errored && !NON_RETRY_AGENTS.has(agent.id)) {
21145
+ if (isDesignPhase && !errored && !NON_RETRY_AGENTS.has(agent.id)) {
21029
21146
  const specialistCheck = enforceDesignPhaseToolEmissions(agent.id, emittedToolNames);
21030
21147
  yield* applyRetryIfMissing({
21031
21148
  agent,
@@ -21043,7 +21160,7 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
21043
21160
  toolCalls += 1;
21044
21161
  }
21045
21162
  });
21046
- } else {
21163
+ } else if (isDesignPhase) {
21047
21164
  enforceDesignPhaseToolEmissions(agent.id, emittedToolNames);
21048
21165
  }
21049
21166
  const memberDuration = Date.now() - memberStart;
@@ -21105,8 +21222,19 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
21105
21222
  model: effectiveModel,
21106
21223
  provider: effectiveProvider,
21107
21224
  sessionId,
21108
- messages: buildAgentMessages(oracle, `Review these proposals for: "${userMessage}"`, "", "", anonymized, config2.aiConfig, executableNames),
21109
- tools: [],
21225
+ messages: buildAgentMessages(oracle, `Review these proposals for: "${userMessage}"`, "", "", anonymized, config2.aiConfig, executableNames, runMode),
21226
+ tools: (() => {
21227
+ const oracleToolNames = filterExecutable(Array.from(/* @__PURE__ */ new Set([
21228
+ "createDocument",
21229
+ "searchDocuments",
21230
+ ...computeAgentTools(oracle, config2.aiConfig)
21231
+ ])));
21232
+ return oracleToolNames.length > 0 ? getProviderTools(oracleToolNames).map((tool) => ({
21233
+ name: tool.function.name,
21234
+ description: tool.function.description,
21235
+ parameters: tool.function.parameters
21236
+ })) : [];
21237
+ })(),
21110
21238
  eventBus: config2.eventBus,
21111
21239
  toolRegistry: config2.tools,
21112
21240
  // Task G.2 — same per-turn limit applies to oracle.
@@ -21149,7 +21277,7 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
21149
21277
  fullText = `Review error: ${err instanceof Error ? err.message : "Unknown"}`;
21150
21278
  errored = true;
21151
21279
  }
21152
- if (!errored) {
21280
+ if (isDesignPhase && !errored) {
21153
21281
  const oracleCheck = enforceDesignPhaseToolEmissions(oracle.id, emittedToolNames);
21154
21282
  yield* applyRetryIfMissing({
21155
21283
  agent: oracle,
@@ -21167,7 +21295,7 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
21167
21295
  toolCalls += 1;
21168
21296
  }
21169
21297
  });
21170
- } else {
21298
+ } else if (isDesignPhase) {
21171
21299
  enforceDesignPhaseToolEmissions(oracle.id, emittedToolNames);
21172
21300
  }
21173
21301
  const memberDuration = Date.now() - memberStart;
@@ -21224,7 +21352,7 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
21224
21352
  model: effectiveModel,
21225
21353
  provider: effectiveProvider,
21226
21354
  sessionId,
21227
- messages: buildAgentMessages(chairman, userMessage, config2.ragContext, config2.workspaceContext, agentOutputs, config2.aiConfig, executableNames),
21355
+ messages: buildAgentMessages(chairman, userMessage, config2.ragContext, config2.workspaceContext, agentOutputs, config2.aiConfig, executableNames, runMode),
21228
21356
  tools: chairmanTools,
21229
21357
  eventBus: config2.eventBus,
21230
21358
  toolRegistry: config2.tools,
@@ -21272,7 +21400,7 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
21272
21400
  errored = true;
21273
21401
  lastErrorMessage = err instanceof Error ? err.message : String(err);
21274
21402
  }
21275
- if (!errored) {
21403
+ if (isDesignPhase && !errored) {
21276
21404
  const chairmanCheck = enforceDesignPhaseToolEmissions(chairman.id, emittedToolNames);
21277
21405
  yield* applyRetryIfMissing({
21278
21406
  agent: chairman,
@@ -21290,7 +21418,7 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
21290
21418
  toolCalls += 1;
21291
21419
  }
21292
21420
  });
21293
- } else {
21421
+ } else if (isDesignPhase) {
21294
21422
  enforceDesignPhaseToolEmissions(chairman.id, emittedToolNames);
21295
21423
  }
21296
21424
  const memberDuration = Date.now() - memberStart;
@@ -21386,7 +21514,7 @@ async function* runRetryTurnForMember(args) {
21386
21514
  description: t.function.description,
21387
21515
  parameters: t.function.parameters
21388
21516
  }));
21389
- const baseMessages = buildAgentMessages(args.agent, args.userMessage, args.ragContext, args.workspaceContext, args.priorOutputs, args.aiConfig, args.executableTools);
21517
+ const baseMessages = buildAgentMessages(args.agent, args.userMessage, args.ragContext, args.workspaceContext, args.priorOutputs, args.aiConfig, args.executableTools, args.runMode ?? "implementation");
21390
21518
  const retryMessages = [
21391
21519
  ...baseMessages,
21392
21520
  { role: "user", content: buildRetryPrompt(executableMissing) }
@@ -21447,7 +21575,8 @@ async function* applyRetryIfMissing(args) {
21447
21575
  effectiveProvider: args.effectiveProvider,
21448
21576
  eventBus: args.config.eventBus,
21449
21577
  toolRegistry: args.config.tools,
21450
- providerStream: args.config.providerStream
21578
+ providerStream: args.config.providerStream,
21579
+ runMode: args.config.runMode
21451
21580
  });
21452
21581
  for await (const event of retryGenerator) {
21453
21582
  if (event.type === "tool_execution_start") {
@@ -21471,6 +21600,8 @@ var init_councilApi = __esm({
21471
21600
  init_tools();
21472
21601
  init_events();
21473
21602
  init_AgentHarness();
21603
+ init_modeBanners();
21604
+ init_runMode();
21474
21605
  NON_RETRY_AGENTS = /* @__PURE__ */ new Set([]);
21475
21606
  QUESTION_MARKER = "---QUESTION---";
21476
21607
  QUESTION_END_MARKER = "---END---";
@@ -21486,6 +21617,9 @@ var init_councilApi = __esm({
21486
21617
  geryon: [
21487
21618
  [{ name: "createDocument", min: 3 }]
21488
21619
  ],
21620
+ pluton: [
21621
+ [{ name: "createDocument", min: 1 }]
21622
+ ],
21489
21623
  minos: [
21490
21624
  [{ name: "createDocument", min: 1 }]
21491
21625
  ],
@@ -21661,8 +21795,10 @@ var council_exports = {};
21661
21795
  __export(council_exports, {
21662
21796
  AGENT_ROLES: () => AGENT_ROLES,
21663
21797
  COLLABORATION_DIRECTIVE: () => COLLABORATION_DIRECTIVE,
21798
+ DESIGN_PHASE_MODE_BANNER: () => DESIGN_PHASE_MODE_BANNER,
21664
21799
  DESIGN_PHASE_REQUIREMENTS: () => DESIGN_PHASE_REQUIREMENTS,
21665
21800
  DESIGN_PHASE_REQUIREMENT_SETS: () => DESIGN_PHASE_REQUIREMENT_SETS,
21801
+ IMPLEMENTATION_MODE_BANNER: () => IMPLEMENTATION_MODE_BANNER,
21666
21802
  MAX_RETRY_PER_MEMBER: () => MAX_RETRY_PER_MEMBER,
21667
21803
  NON_RETRY_AGENTS: () => NON_RETRY_AGENTS,
21668
21804
  OUTPUT_QUALITY_DIRECTIVE: () => OUTPUT_QUALITY_DIRECTIVE,
@@ -21679,6 +21815,8 @@ __export(council_exports, {
21679
21815
  cleanAgentContent: () => cleanAgentContent,
21680
21816
  computeAgentSkills: () => computeAgentSkills,
21681
21817
  computeAgentTools: () => computeAgentTools,
21818
+ councilModeBanner: () => councilModeBanner,
21819
+ councilTierFromSize: () => councilTierFromSize,
21682
21820
  enforceDesignPhaseToolEmissions: () => enforceDesignPhaseToolEmissions,
21683
21821
  getAgent: () => getAgent,
21684
21822
  getBasePromptModules: () => getBasePromptModules,
@@ -21690,6 +21828,7 @@ __export(council_exports, {
21690
21828
  parseThinking: () => parseThinking,
21691
21829
  promoteMember: () => promoteMember,
21692
21830
  renderSkillMarkdown: () => renderSkillMarkdown,
21831
+ resolveCouncilRunMode: () => resolveCouncilRunMode,
21693
21832
  runCouncilPure: () => runCouncilPure,
21694
21833
  runRetryTurnForMember: () => runRetryTurnForMember,
21695
21834
  shouldRetryMember: () => shouldRetryMember,
@@ -21701,6 +21840,8 @@ var init_council = __esm({
21701
21840
  "packages/core/dist/council/index.js"() {
21702
21841
  "use strict";
21703
21842
  init_councilApi();
21843
+ init_runMode();
21844
+ init_modeBanners();
21704
21845
  init_roles();
21705
21846
  init_promoteMember();
21706
21847
  init_councilDirectives();
@@ -21709,6 +21850,64 @@ var init_council = __esm({
21709
21850
  }
21710
21851
  });
21711
21852
 
21853
+ // src/cli/councilConfig.ts
21854
+ function resolveCouncilTier(opts) {
21855
+ const env = opts?.env ?? process.env;
21856
+ if (opts?.explicitSize !== void 0) {
21857
+ const size = clampCouncilSize(opts.explicitSize);
21858
+ return { tier: size >= COUNCIL_TIER_SIZES.full ? "full" : "lite", councilSize: size };
21859
+ }
21860
+ const envSize = env["ZELARI_COUNCIL_SIZE"];
21861
+ if (envSize) {
21862
+ const parsed = Number.parseInt(envSize, 10);
21863
+ if (Number.isFinite(parsed) && parsed > 0) {
21864
+ const size = clampCouncilSize(parsed);
21865
+ return { tier: size >= COUNCIL_TIER_SIZES.full ? "full" : "lite", councilSize: size };
21866
+ }
21867
+ }
21868
+ const tierEnv = env["ZELARI_COUNCIL_TIER"]?.toLowerCase();
21869
+ if (tierEnv === "lite") {
21870
+ return { tier: "lite", councilSize: COUNCIL_TIER_SIZES.lite };
21871
+ }
21872
+ if (tierEnv === "full") {
21873
+ return { tier: "full", councilSize: COUNCIL_TIER_SIZES.full };
21874
+ }
21875
+ return { tier: "full", councilSize: COUNCIL_TIER_SIZES.full };
21876
+ }
21877
+ function clampCouncilSize(n) {
21878
+ return Math.min(COUNCIL_TIER_SIZES.full, Math.max(1, Math.floor(n)));
21879
+ }
21880
+ var COUNCIL_TIER_SIZES;
21881
+ var init_councilConfig = __esm({
21882
+ "src/cli/councilConfig.ts"() {
21883
+ "use strict";
21884
+ COUNCIL_TIER_SIZES = {
21885
+ lite: 3,
21886
+ full: 6
21887
+ };
21888
+ }
21889
+ });
21890
+
21891
+ // src/cli/workspace/planDetect.ts
21892
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
21893
+ import { join as join6 } from "node:path";
21894
+ function hasWorkspacePlan(projectRoot = process.cwd()) {
21895
+ const planPath = join6(resolveWorkspaceRoot(projectRoot), "plan.json");
21896
+ if (!existsSync11(planPath)) return false;
21897
+ try {
21898
+ const parsed = JSON.parse(readFileSync8(planPath, "utf8"));
21899
+ return Array.isArray(parsed.phases) && parsed.phases.length > 0;
21900
+ } catch {
21901
+ return false;
21902
+ }
21903
+ }
21904
+ var init_planDetect = __esm({
21905
+ "src/cli/workspace/planDetect.ts"() {
21906
+ "use strict";
21907
+ init_paths();
21908
+ }
21909
+ });
21910
+
21712
21911
  // src/cli/councilDispatcher.ts
21713
21912
  var councilDispatcher_exports = {};
21714
21913
  __export(councilDispatcher_exports, {
@@ -21722,30 +21921,42 @@ async function* dispatchCouncil(userMessage, options) {
21722
21921
  );
21723
21922
  }
21724
21923
  if (!userMessage || userMessage.trim().length === 0) {
21725
- throw new CouncilDispatchError("Empty userMessage \u2014 /council requires a non-empty input.");
21924
+ throw new CouncilDispatchError(
21925
+ "Empty userMessage \u2014 /council requires a non-empty input."
21926
+ );
21726
21927
  }
21928
+ const projectRoot = options.workspaceRoot ?? process.cwd();
21929
+ const { councilSize } = resolveCouncilTier({
21930
+ explicitSize: options.councilSize
21931
+ });
21932
+ const runMode = options.runMode ?? resolveCouncilRunMode({
21933
+ userMessage,
21934
+ hasExistingPlan: hasWorkspacePlan(projectRoot),
21935
+ env: process.env
21936
+ });
21727
21937
  const config2 = {
21728
21938
  apiKey: options.apiKey,
21729
21939
  provider: options.provider ?? "openai-compatible",
21730
21940
  model: options.model,
21731
- councilSize: options.councilSize ?? 3,
21941
+ councilSize,
21732
21942
  debateMode: options.debateMode ?? false,
21943
+ runMode,
21733
21944
  ragContext: options.ragContext ?? "",
21734
21945
  workspaceContext: options.workspaceContext ?? "",
21735
21946
  providerStream: options.providerStream,
21736
21947
  eventBus: options.eventBus,
21737
21948
  sessionId: options.sessionId,
21738
- // Default to the workspace tool registry (CLI standalone has no
21739
- // AnathemaBrain Electron ctx). Pass `tools: undefined` via options
21740
- // to skip workspace wiring (e.g. in tests).
21741
- tools: options.tools ?? (options.disableWorkspaceTools ? void 0 : createWorkspaceToolRegistry(createWorkspaceContext())),
21949
+ tools: options.tools ?? (options.disableWorkspaceTools ? void 0 : createWorkspaceToolRegistry(createWorkspaceContext(projectRoot))),
21742
21950
  maxToolCallsPerTurn: options.maxToolCallsPerTurn,
21743
21951
  feedbackStore: options.feedbackStore
21744
21952
  };
21745
21953
  if (!options.disableWorkspaceTools) {
21746
21954
  const { setWorkspaceStubs: setWorkspaceStubs2 } = await Promise.resolve().then(() => (init_skills2(), skills_exports));
21747
- const { createWorkspaceContext: buildCtx, createWorkspaceStubs: buildStubs } = await Promise.resolve().then(() => (init_stubs(), stubs_exports));
21748
- const wsCtx = options.workspaceRoot ? buildCtx(options.workspaceRoot) : buildCtx();
21955
+ const {
21956
+ createWorkspaceContext: buildCtx,
21957
+ createWorkspaceStubs: buildStubs
21958
+ } = await Promise.resolve().then(() => (init_stubs(), stubs_exports));
21959
+ const wsCtx = buildCtx(projectRoot);
21749
21960
  setWorkspaceStubs2(buildStubs(wsCtx));
21750
21961
  }
21751
21962
  yield* runCouncilPure(userMessage, config2, {});
@@ -21755,6 +21966,8 @@ var init_councilDispatcher = __esm({
21755
21966
  "src/cli/councilDispatcher.ts"() {
21756
21967
  "use strict";
21757
21968
  init_council();
21969
+ init_councilConfig();
21970
+ init_planDetect();
21758
21971
  init_stubs();
21759
21972
  init_toolRegistry();
21760
21973
  CouncilDispatchError = class extends Error {
@@ -21774,13 +21987,13 @@ __export(agentsMd_exports, {
21774
21987
  serializeAgentsMd: () => serializeAgentsMd,
21775
21988
  updateAgentsMd: () => updateAgentsMd
21776
21989
  });
21777
- import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "node:fs";
21990
+ import { existsSync as existsSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "node:fs";
21778
21991
  import { createHash as createHash2 } from "node:crypto";
21779
- import { join as join6 } from "node:path";
21992
+ import { join as join7 } from "node:path";
21780
21993
  import { readFile } from "node:fs/promises";
21781
21994
  async function readPackageJson2(projectRoot) {
21782
- const path22 = join6(projectRoot, "package.json");
21783
- if (!existsSync11(path22)) return null;
21995
+ const path22 = join7(projectRoot, "package.json");
21996
+ if (!existsSync12(path22)) return null;
21784
21997
  try {
21785
21998
  return JSON.parse(await readFile(path22, "utf8"));
21786
21999
  } catch {
@@ -21804,8 +22017,8 @@ async function genTechStack(ctx) {
21804
22017
  ].join("\n");
21805
22018
  }
21806
22019
  async function genDecisions(ctx) {
21807
- const decisionsDir = join6(ctx.rootDir, "decisions");
21808
- if (!existsSync11(decisionsDir)) return "_No ADRs yet._";
22020
+ const decisionsDir = join7(ctx.rootDir, "decisions");
22021
+ if (!existsSync12(decisionsDir)) return "_No ADRs yet._";
21809
22022
  const files = ctx.storage.listMarkdown(decisionsDir).sort();
21810
22023
  const accepted = [];
21811
22024
  const proposed = [];
@@ -21830,9 +22043,9 @@ async function genDecisions(ctx) {
21830
22043
  }
21831
22044
  async function genConventions(ctx) {
21832
22045
  const lines = [];
21833
- const claudeMd = join6(ctx.projectRoot, "CLAUDE.MD");
21834
- if (existsSync11(claudeMd)) {
21835
- const content = readFileSync8(claudeMd, "utf8");
22046
+ const claudeMd = join7(ctx.projectRoot, "CLAUDE.MD");
22047
+ if (existsSync12(claudeMd)) {
22048
+ const content = readFileSync9(claudeMd, "utf8");
21836
22049
  const match = content.match(/## Architecture rules[\s\S]+?(?=\n## |\n*$)/);
21837
22050
  if (match) {
21838
22051
  lines.push('<!-- Extracted from CLAUDE.MD "Architecture rules" -->');
@@ -21864,9 +22077,9 @@ async function genBuild(ctx) {
21864
22077
  ].join("\n");
21865
22078
  }
21866
22079
  async function genOpenQuestions(ctx) {
21867
- const path22 = join6(ctx.rootDir, "risks.md");
21868
- if (!existsSync11(path22)) return "_No open questions._";
21869
- const content = readFileSync8(path22, "utf8");
22080
+ const path22 = join7(ctx.rootDir, "risks.md");
22081
+ if (!existsSync12(path22)) return "_No open questions._";
22082
+ const content = readFileSync9(path22, "utf8");
21870
22083
  const lines = content.split("\n");
21871
22084
  const questions = [];
21872
22085
  let currentTitle = "";
@@ -21940,9 +22153,9 @@ function titleCase(id) {
21940
22153
  return id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
21941
22154
  }
21942
22155
  async function updateAgentsMd(ctx, projectRoot) {
21943
- const agentsPath = join6(projectRoot, "AGENTS.MD");
21944
- if (existsSync11(agentsPath)) {
21945
- const content = readFileSync8(agentsPath, "utf8");
22156
+ const agentsPath = join7(projectRoot, "AGENTS.MD");
22157
+ if (existsSync12(agentsPath)) {
22158
+ const content = readFileSync9(agentsPath, "utf8");
21946
22159
  const hasAnyMarker = AUTO_SECTIONS.some((id) => content.includes(MARKER_OPEN(id)));
21947
22160
  if (!hasAnyMarker) {
21948
22161
  return {
@@ -21957,8 +22170,8 @@ async function updateAgentsMd(ctx, projectRoot) {
21957
22170
  newSections.set(id, await GENERATORS[id](ctx));
21958
22171
  }
21959
22172
  let manualContent = "";
21960
- if (existsSync11(agentsPath)) {
21961
- const { manualBlocks } = parseAgentsMd(readFileSync8(agentsPath, "utf8"));
22173
+ if (existsSync12(agentsPath)) {
22174
+ const { manualBlocks } = parseAgentsMd(readFileSync9(agentsPath, "utf8"));
21962
22175
  manualContent = manualBlocks.after;
21963
22176
  } else {
21964
22177
  const projectName2 = projectName(projectRoot);
@@ -21974,7 +22187,7 @@ async function updateAgentsMd(ctx, projectRoot) {
21974
22187
  ""
21975
22188
  ].join("\n");
21976
22189
  }
21977
- const oldContent = existsSync11(agentsPath) ? readFileSync8(agentsPath, "utf8") : "";
22190
+ const oldContent = existsSync12(agentsPath) ? readFileSync9(agentsPath, "utf8") : "";
21978
22191
  const { sections: oldSections } = parseAgentsMd(oldContent);
21979
22192
  const changedSections = [];
21980
22193
  for (const id of AUTO_SECTIONS) {
@@ -22117,20 +22330,29 @@ __export(postCouncilHook_exports, {
22117
22330
  runPostCouncilHook: () => runPostCouncilHook
22118
22331
  });
22119
22332
  import { spawn as spawn3 } from "node:child_process";
22120
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
22121
- import { join as join7 } from "node:path";
22122
- async function runCompleteDesignPostProcessor(ctx) {
22333
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "node:fs";
22334
+ import { join as join8 } from "node:path";
22335
+ async function runCompleteDesignPostProcessor(ctx, options) {
22336
+ if (options?.runMode === "implementation") {
22337
+ return {
22338
+ ran: false,
22339
+ reason: "implementation mode (complete-design skipped)"
22340
+ };
22341
+ }
22123
22342
  if (process.env["ZELARI_COMPLETE_DESIGN"] === "0") {
22124
22343
  return { ran: false, reason: "ZELARI_COMPLETE_DESIGN=0 (disabled)" };
22125
22344
  }
22126
- const planJsonPath2 = join7(ctx.rootDir, "plan.json");
22127
- const scriptPath = join7(ctx.projectRoot, "complete-design.mjs");
22128
- if (!existsSync12(planJsonPath2)) {
22129
- return { ran: false, reason: ".zelari/plan.json missing (not design-phase)" };
22345
+ const planJsonPath2 = join8(ctx.rootDir, "plan.json");
22346
+ const scriptPath = join8(ctx.projectRoot, "complete-design.mjs");
22347
+ if (!existsSync13(planJsonPath2)) {
22348
+ return {
22349
+ ran: false,
22350
+ reason: ".zelari/plan.json missing (not design-phase)"
22351
+ };
22130
22352
  }
22131
22353
  let phaseCount = 0;
22132
22354
  try {
22133
- const parsed = JSON.parse(readFileSync9(planJsonPath2, "utf8"));
22355
+ const parsed = JSON.parse(readFileSync10(planJsonPath2, "utf8"));
22134
22356
  phaseCount = Array.isArray(parsed.phases) ? parsed.phases.length : 0;
22135
22357
  } catch {
22136
22358
  return { ran: false, reason: ".zelari/plan.json corrupt" };
@@ -22138,7 +22360,7 @@ async function runCompleteDesignPostProcessor(ctx) {
22138
22360
  if (phaseCount === 0) {
22139
22361
  return { ran: false, reason: ".zelari/plan.json has no phases" };
22140
22362
  }
22141
- if (!existsSync12(scriptPath)) {
22363
+ if (!existsSync13(scriptPath)) {
22142
22364
  try {
22143
22365
  const builtin = await runBuiltinCompleteDesign(ctx);
22144
22366
  return {
@@ -22189,7 +22411,7 @@ async function runCompleteDesignPostProcessor(ctx) {
22189
22411
  });
22190
22412
  });
22191
22413
  }
22192
- async function runPostCouncilHook(ctx) {
22414
+ async function runPostCouncilHook(ctx, options) {
22193
22415
  let agentsMdResult;
22194
22416
  if (process.env["ZELARI_AGENTS_MD"] === "0") {
22195
22417
  agentsMdResult = {
@@ -22216,7 +22438,7 @@ async function runPostCouncilHook(ctx) {
22216
22438
  };
22217
22439
  }
22218
22440
  }
22219
- const completeDesign = await runCompleteDesignPostProcessor(ctx);
22441
+ const completeDesign = await runCompleteDesignPostProcessor(ctx, options);
22220
22442
  return {
22221
22443
  ran: agentsMdResult.ran || completeDesign.ran,
22222
22444
  changed: agentsMdResult.changed,
@@ -22240,8 +22462,8 @@ __export(councilFeedback_exports, {
22240
22462
  });
22241
22463
  import {
22242
22464
  promises as fs11,
22243
- existsSync as existsSync13,
22244
- readFileSync as readFileSync10,
22465
+ existsSync as existsSync14,
22466
+ readFileSync as readFileSync11,
22245
22467
  writeFileSync as writeFileSync8,
22246
22468
  mkdirSync as mkdirSync8
22247
22469
  } from "node:fs";
@@ -22349,9 +22571,9 @@ var init_councilFeedback = __esm({
22349
22571
  }
22350
22572
  // --- persistence ---------------------------------------------------------
22351
22573
  load() {
22352
- if (!existsSync13(this.file)) return;
22574
+ if (!existsSync14(this.file)) return;
22353
22575
  try {
22354
- const raw = readFileSync10(this.file, "utf-8");
22576
+ const raw = readFileSync11(this.file, "utf-8");
22355
22577
  const parsed = JSON.parse(raw);
22356
22578
  if (parsed && Array.isArray(parsed.entries)) {
22357
22579
  this.entries = parsed.entries.filter(
@@ -22467,10 +22689,8 @@ async function performUpdate(packageName = "zelari-code", executor = spawn4) {
22467
22689
  const args = ["install", "-g", `${packageName}@latest`];
22468
22690
  let stdout = "";
22469
22691
  let stderr = "";
22470
- const child = executor("npm", args, {
22471
- stdio: ["ignore", "pipe", "pipe"],
22472
- shell: process.platform === "win32"
22473
- });
22692
+ const stdio = ["ignore", "pipe", "pipe"];
22693
+ const child = process.platform === "win32" ? executor(buildCmdLine("npm", args), { stdio, shell: true }) : executor("npm", args, { stdio });
22474
22694
  child.stdout?.on("data", (chunk) => {
22475
22695
  stdout += chunk.toString();
22476
22696
  });
@@ -22500,6 +22720,7 @@ var require2, __dirname2, REGISTRY_URL;
22500
22720
  var init_updater = __esm({
22501
22721
  "src/cli/updater.ts"() {
22502
22722
  "use strict";
22723
+ init_cmdline();
22503
22724
  require2 = createRequire(import.meta.url);
22504
22725
  __dirname2 = path16.dirname(fileURLToPath(import.meta.url));
22505
22726
  REGISTRY_URL = "https://registry.npmjs.org/zelari-code/latest";
@@ -22507,12 +22728,12 @@ var init_updater = __esm({
22507
22728
  });
22508
22729
 
22509
22730
  // src/cli/main.ts
22510
- import React10 from "react";
22731
+ import React11 from "react";
22511
22732
  import { render } from "ink";
22512
22733
 
22513
22734
  // src/cli/app.tsx
22514
- import React6, { useState as useState4, useMemo, useCallback as useCallback5 } from "react";
22515
- import { Box as Box6, Static } from "ink";
22735
+ import React7, { useState as useState6, useMemo, useCallback as useCallback5 } from "react";
22736
+ import { Box as Box7, Static, useInput, useStdin } from "ink";
22516
22737
 
22517
22738
  // src/cli/components/InputBar.tsx
22518
22739
  import React from "react";
@@ -22782,68 +23003,22 @@ function StreamingTail(m) {
22782
23003
  import React5 from "react";
22783
23004
  import { Box as Box5, Text as Text5 } from "ink";
22784
23005
 
22785
- // src/cli/modelPricing.ts
22786
- var PRICES_PER_MILLION = {
22787
- // xAI Grok
22788
- "grok-4": { input: 3, output: 15 },
22789
- "grok-4-fast": { input: 0.2, output: 0.5 },
22790
- "grok-3": { input: 3, output: 15 },
22791
- "grok-3-mini": { input: 0.3, output: 0.5 },
22792
- "grok-2-vision": { input: 2, output: 10 },
22793
- // GLM / Z.AI
22794
- "glm-4.6": { input: 0.6, output: 2.2 },
22795
- "glm-4.5": { input: 0.5, output: 2 },
22796
- "glm-4.5-air": { input: 0.1, output: 0.6 },
22797
- "glm-z1": { input: 0.5, output: 2 },
22798
- // MiniMax
22799
- "MiniMax-M2.5": { input: 0.2, output: 1.1 },
22800
- "MiniMax-M2": { input: 0.2, output: 1.1 },
22801
- "MiniMax-M2-her": { input: 0.3, output: 1.2 },
22802
- // OpenAI (for openai-compatible fallback)
22803
- "gpt-4o": { input: 2.5, output: 10 },
22804
- "gpt-4o-mini": { input: 0.15, output: 0.6 },
22805
- "gpt-4-turbo": { input: 10, output: 30 },
22806
- "o1-preview": { input: 15, output: 60 },
22807
- "o1-mini": { input: 3, output: 12 }
22808
- };
22809
- var DEFAULT_RATE = { input: 1, output: 3 };
22810
- function envOverride(model) {
22811
- const key = `ANATHEMA_PRICE_${model.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
22812
- const raw = process.env[key] ?? process.env.ANATHEMA_PRICE_DEFAULT;
22813
- return parseRateOverride(raw);
22814
- }
22815
- function parseRateOverride(raw) {
22816
- if (!raw) return null;
22817
- const [inStr, outStr] = raw.split("/");
22818
- const input = Number.parseFloat(inStr);
22819
- const output = outStr !== void 0 ? Number.parseFloat(outStr) : input;
22820
- if (!Number.isFinite(input) || !Number.isFinite(output)) return null;
22821
- return { input, output };
22822
- }
22823
- function getModelRate(model) {
22824
- if (!model) return DEFAULT_RATE;
22825
- return envOverride(model) ?? PRICES_PER_MILLION[model] ?? DEFAULT_RATE;
22826
- }
22827
- function calculateCost(model, promptTokens, completionTokens) {
22828
- if (!Number.isFinite(promptTokens) || promptTokens < 0) return 0;
22829
- if (!Number.isFinite(completionTokens) || completionTokens < 0) return 0;
22830
- const rate = getModelRate(model);
22831
- const inputCost = promptTokens / 1e6 * rate.input;
22832
- const outputCost = completionTokens / 1e6 * rate.output;
22833
- return Number((inputCost + outputCost).toFixed(6));
22834
- }
22835
- function formatCost(usd) {
22836
- if (!Number.isFinite(usd) || usd < 0) return "$0.0000";
22837
- if (usd === 0) return "$0.0000";
22838
- if (usd < 1e-4) return "<$0.0001";
22839
- return `$${usd.toFixed(4)}`;
22840
- }
22841
- function formatTokens(tokens) {
22842
- if (!Number.isFinite(tokens) || tokens < 0) return "0";
22843
- if (tokens < 1e3) return `${tokens}`;
22844
- if (tokens < 1e6) return `${(tokens / 1e3).toFixed(1)}k`;
22845
- if (tokens < 1e9) return `${(tokens / 1e6).toFixed(2)}M`;
22846
- return `${(tokens / 1e9).toFixed(2)}B`;
23006
+ // src/cli/utils/duration.ts
23007
+ function formatDuration(ms) {
23008
+ const abs = Math.abs(ms);
23009
+ const sign = ms < 0 ? " ago" : "";
23010
+ if (abs < 1e3) return `${abs}ms${sign}`;
23011
+ const s = Math.floor(abs / 1e3);
23012
+ if (s < 60) return `${s}s${sign}`;
23013
+ const m = Math.floor(s / 60);
23014
+ const rs = s % 60;
23015
+ if (m < 60) return rs === 0 ? `${m}m${sign}` : `${m}m ${rs}s${sign}`;
23016
+ const h = Math.floor(m / 60);
23017
+ const rm = m % 60;
23018
+ if (h < 24) return rm === 0 ? `${h}h${sign}` : `${h}h ${rm}m${sign}`;
23019
+ const d = Math.floor(h / 24);
23020
+ const rh = h % 24;
23021
+ return rh === 0 ? `${d}d${sign}` : `${d}d ${rh}h${sign}`;
22847
23022
  }
22848
23023
 
22849
23024
  // src/cli/components/StatusBar.tsx
@@ -22852,622 +23027,369 @@ function StatusBar({
22852
23027
  provider,
22853
23028
  sessionId,
22854
23029
  sessionActive,
22855
- totalTokens,
22856
- totalCostUsd,
22857
23030
  queueCount = 0,
22858
- busy = false
23031
+ busy = false,
23032
+ mode = "agent",
23033
+ cwd,
23034
+ elapsedMs = null,
23035
+ lastMs = null
22859
23036
  }) {
22860
- const hasCost = typeof totalCostUsd === "number" && totalCostUsd > 0;
22861
- const costStr = hasCost ? formatCost(totalCostUsd) : "$0.0000";
22862
- const tokStr = formatTokens(totalTokens ?? 0);
22863
- return /* @__PURE__ */ React5.createElement(Box5, { paddingX: 1 }, /* @__PURE__ */ React5.createElement(Text5, { color: sessionActive ? "green" : "gray" }, sessionActive ? "\u25CF" : "\u25CB"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " "), /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "cyan" }, provider), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React5.createElement(Text5, null, model), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "session ", sessionId), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React5.createElement(Text5, { color: "gray" }, tokStr, " tok"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React5.createElement(Text5, { color: hasCost ? "yellow" : "gray" }, costStr), queueCount > 0 ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, "queue ", queueCount)) : null, busy ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, "working")) : null);
23037
+ return /* @__PURE__ */ React5.createElement(Box5, { paddingX: 1 }, /* @__PURE__ */ React5.createElement(Text5, { color: sessionActive ? "green" : "gray" }, sessionActive ? "\u25CF" : "\u25CB"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " "), /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: mode === "council" ? "magenta" : "cyan" }, mode === "council" ? "\u26EC council" : "\u23F5 agent"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " (shift+tab)"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React5.createElement(Text5, { bold: true, color: "cyan" }, provider), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React5.createElement(Text5, null, model), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "session ", sessionId), cwd ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React5.createElement(Text5, { color: "blue" }, cwd)) : null, busy && elapsedMs !== null ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, "\u23F1 ", formatDuration(elapsedMs))) : lastMs !== null ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "last ", formatDuration(lastMs))) : null, queueCount > 0 ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 "), /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, "queue ", queueCount)) : null);
23038
+ }
23039
+
23040
+ // src/cli/components/Sidebar.tsx
23041
+ import React6 from "react";
23042
+ import { Box as Box6, Text as Text6 } from "ink";
23043
+ var EMBLEM_BRAILLE = `\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28F4\u28FF\u28F7\u28C6\u2840
23044
+ \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28F6\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E6\u2840
23045
+ \u2800\u2800\u2800\u2800\u2800\u2800\u28F0\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E6
23046
+ \u2800\u2800\u2800\u2800\u28A0\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28F7\u2844
23047
+ \u2800\u2800\u2800\u2800\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2819\u283B\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2806
23048
+ \u2800\u2800\u2800\u2800\u28C8\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2800\u28F7\u28EC\u285B\u28BF\u28FF\u28DF\u28C1
23049
+ \u2800\u2880\u28F4\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2800\u287F\u28BF\u287F\u2883\u28C8\u28FB\u28FF\u28FF\u28F6
23050
+ \u2800\u28FC\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28E0\u28F4\u28C6\u283B\u280E\u28BF\u28FF\u28FF\u28FF\u28FF\u28E7
23051
+ \u28F0\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u28FF\u2847`;
23052
+ var SIDEBAR_WIDTH = 28;
23053
+ var SIDEBAR_MIN_COLUMNS = 96;
23054
+ var EMBLEM_MIN_ROWS = 26;
23055
+ var MAX_FILES_SHORT = 6;
23056
+ var MAX_FILES_TALL = 10;
23057
+ function shouldShowSidebar(columns, rows) {
23058
+ return columns >= SIDEBAR_MIN_COLUMNS && rows >= 16;
23059
+ }
23060
+ function truncatePath(p3, max) {
23061
+ if (p3.length <= max) return p3;
23062
+ return `\u2026${p3.slice(-(max - 1))}`;
23063
+ }
23064
+ function Sidebar({ version: version2, changes, rows }) {
23065
+ const showEmblem = rows >= EMBLEM_MIN_ROWS;
23066
+ const maxFiles = rows >= EMBLEM_MIN_ROWS + 8 ? MAX_FILES_TALL : MAX_FILES_SHORT;
23067
+ const visible = changes.files.slice(0, maxFiles);
23068
+ const hidden = changes.files.length - visible.length;
23069
+ const innerWidth = SIDEBAR_WIDTH - 4;
23070
+ return /* @__PURE__ */ React6.createElement(
23071
+ Box6,
23072
+ {
23073
+ flexDirection: "column",
23074
+ width: SIDEBAR_WIDTH,
23075
+ borderStyle: "single",
23076
+ borderColor: "gray",
23077
+ paddingX: 1,
23078
+ flexShrink: 0
23079
+ },
23080
+ showEmblem && /* @__PURE__ */ React6.createElement(Box6, { justifyContent: "center" }, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan" }, EMBLEM_BRAILLE)),
23081
+ /* @__PURE__ */ React6.createElement(Box6, { justifyContent: "center" }, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "white" }, "ZELARI CODE")),
23082
+ /* @__PURE__ */ React6.createElement(Box6, { justifyContent: "center" }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "v", version2, changes.branch ? ` \xB7 ${truncatePath(changes.branch, 12)}` : "")),
23083
+ /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "\u2500".repeat(innerWidth)),
23084
+ !changes.isRepo ? /* @__PURE__ */ React6.createElement(Text6, { dimColor: true, italic: true }, "not a git repo") : changes.files.length === 0 ? /* @__PURE__ */ React6.createElement(Text6, { dimColor: true, italic: true }, "no changes") : /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "changes (", changes.files.length, ")"), visible.map((f) => /* @__PURE__ */ React6.createElement(FileRow, { key: f.path, file: f, pathWidth: innerWidth - 10 })), hidden > 0 && /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " +", hidden, " more\u2026"))
23085
+ );
22864
23086
  }
22865
-
22866
- // src/cli/slashCommands.ts
22867
- function parseSlashCommand(text) {
22868
- const trimmed = text.trim();
22869
- if (!trimmed.startsWith("/")) return null;
22870
- const parts = trimmed.slice(1).split(/\s+/);
22871
- const command = parts[0];
22872
- const args = parts.slice(1);
22873
- return { command, args };
23087
+ function FileRow({ file: file2, pathWidth }) {
23088
+ const name = truncatePath(file2.path, Math.max(6, pathWidth));
23089
+ return /* @__PURE__ */ React6.createElement(Box6, null, /* @__PURE__ */ React6.createElement(Text6, { wrap: "truncate" }, /* @__PURE__ */ React6.createElement(Text6, { color: file2.untracked ? "yellow" : "white" }, name), file2.untracked ? /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, " new") : /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, { color: "green" }, " +", file2.added ?? "\xB7"), /* @__PURE__ */ React6.createElement(Text6, { color: "red" }, " -", file2.removed ?? "\xB7"))));
22874
23090
  }
22875
- function expandSkillTemplate(skill, options = {}) {
22876
- const userInput = options.input?.trim() ?? "";
22877
- const prompt = userInput ? `${skill.systemPromptFragment}
22878
23091
 
22879
- ## User input
22880
- ${userInput}` : `${skill.systemPromptFragment}
23092
+ // src/cli/app.tsx
23093
+ init_skills2();
22881
23094
 
22882
- ## User input
22883
- (Please provide the task description or input for this skill.)`;
22884
- return {
22885
- skillId: skill.id,
22886
- prompt,
22887
- requiredRoles: skill.requiredRoles,
22888
- requiredTools: skill.requiredTools,
22889
- estimatedCost: skill.estimatedCost
22890
- };
22891
- }
22892
- function formatSkillList(skills) {
22893
- if (skills.length === 0) return "(no skills available)";
22894
- const byCategory = /* @__PURE__ */ new Map();
22895
- for (const skill of skills) {
22896
- const list = byCategory.get(skill.category) ?? [];
22897
- list.push(skill);
22898
- byCategory.set(skill.category, list);
23095
+ // src/cli/hooks/useGitChanges.ts
23096
+ import { useEffect, useRef, useState } from "react";
23097
+ import { execFile } from "node:child_process";
23098
+ var EMPTY_GIT_CHANGES = { isRepo: false, branch: null, files: [] };
23099
+ function parseNumstat(out) {
23100
+ const files = [];
23101
+ for (const line of out.split("\n")) {
23102
+ if (!line.trim()) continue;
23103
+ const parts = line.split(" ");
23104
+ if (parts.length < 3) continue;
23105
+ const [a, r] = parts;
23106
+ const rawPath = parts.slice(2).join(" ");
23107
+ files.push({
23108
+ path: normalizeRenamePath(rawPath),
23109
+ added: a === "-" ? null : Number.parseInt(a, 10),
23110
+ removed: r === "-" ? null : Number.parseInt(r, 10),
23111
+ untracked: false
23112
+ });
22899
23113
  }
22900
- const lines = ["Available skills:"];
22901
- for (const [category, list] of [...byCategory.entries()].sort()) {
22902
- lines.push(`
22903
- [${category}]`);
22904
- for (const skill of list) {
22905
- const costLabel = skill.estimatedCost;
22906
- lines.push(` /skill ${skill.id} \u2014 ${skill.name} (${costLabel} cost)`);
23114
+ return files;
23115
+ }
23116
+ function normalizeRenamePath(p3) {
23117
+ const braced = p3.match(/^(.*)\{.* => (.*)\}(.*)$/);
23118
+ if (braced) return `${braced[1]}${braced[2]}${braced[3]}`.replace(/\/\//g, "/");
23119
+ const arrow = p3.match(/^.* => (.*)$/);
23120
+ if (arrow) return arrow[1];
23121
+ return p3;
23122
+ }
23123
+ function parseUntracked(out) {
23124
+ const paths = [];
23125
+ for (const line of out.split("\n")) {
23126
+ if (!line.startsWith("?? ")) continue;
23127
+ let p3 = line.slice(3);
23128
+ if (p3.startsWith('"') && p3.endsWith('"')) p3 = p3.slice(1, -1);
23129
+ paths.push(p3);
23130
+ }
23131
+ return paths;
23132
+ }
23133
+ function mergeChanges(unstaged, staged, untrackedPaths) {
23134
+ const byPath = /* @__PURE__ */ new Map();
23135
+ for (const f of [...unstaged, ...staged]) {
23136
+ const prev2 = byPath.get(f.path);
23137
+ if (!prev2) {
23138
+ byPath.set(f.path, { ...f });
23139
+ } else {
23140
+ prev2.added = prev2.added === null || f.added === null ? null : prev2.added + f.added;
23141
+ prev2.removed = prev2.removed === null || f.removed === null ? null : prev2.removed + f.removed;
22907
23142
  }
22908
23143
  }
22909
- return lines.join("\n");
23144
+ for (const p3 of untrackedPaths) {
23145
+ if (!byPath.has(p3)) {
23146
+ byPath.set(p3, { path: p3, added: null, removed: null, untracked: true });
23147
+ }
23148
+ }
23149
+ const churn = (f) => f.untracked ? -1 : (f.added ?? 0) + (f.removed ?? 0);
23150
+ return [...byPath.values()].sort((a, b) => churn(b) - churn(a));
22910
23151
  }
22911
- function handleSlashCommand(text, availableSkills) {
22912
- const parsed = parseSlashCommand(text);
22913
- if (!parsed) {
22914
- return { handled: false, kind: "unknown" };
23152
+ function runGit(args, cwd) {
23153
+ return new Promise((resolve, reject) => {
23154
+ execFile(
23155
+ "git",
23156
+ args,
23157
+ { cwd, timeout: 5e3, windowsHide: true, maxBuffer: 4 * 1024 * 1024 },
23158
+ (err, stdout) => err ? reject(err) : resolve(stdout)
23159
+ );
23160
+ });
23161
+ }
23162
+ async function snapshotGitChanges(cwd) {
23163
+ let branch = null;
23164
+ try {
23165
+ branch = (await runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd)).trim() || null;
23166
+ } catch {
22915
23167
  }
22916
- const { command, args } = parsed;
22917
- switch (command) {
22918
- case "help":
22919
- return {
22920
- handled: true,
22921
- kind: "help",
22922
- message: `Available commands:
22923
- /login <provider> \u2014 authenticate with provider (grok, minimax, glm, custom)
22924
- /model <name> \u2014 switch the active model
22925
- /model refresh \u2014 re-discover models for the active provider (v3-U)
22926
- /models \u2014 list discovered models for the active provider (v3-U)
22927
- /models refresh \u2014 re-discover models for the active provider (v3-U)
22928
- /provider <name> \u2014 switch the active provider
22929
- /provider custom <baseUrl> \u2014 point the active provider at a self-hosted endpoint (Ollama, LM Studio, vLLM, ...)
22930
- /provider custom clear \u2014 clear the custom endpoint override
22931
- /skill <name> [input] \u2014 invoke a skill (autocomplete with /skill <TAB>)
22932
- /skill-stats [name] \u2014 show invocation stats (success rate, avg duration, total tokens)
22933
- /council <input> \u2014 invoke the multi-agent council on input
22934
- /council-feedback <memberId> <1-5> [note] \u2014 rate a council member for future ranking (Task I.2)
22935
- /promote-member <memberId> \u2014 promote a council member to a standalone skill (v3-K)
22936
- /update [--yes|-y] \u2014 check for zelari-code updates; --yes performs the update (v3-N)
22937
- /steer <text> \u2014 enqueue a follow-up prompt on the active run (Task 18.2)
22938
- /steer --interrupt <text> \u2014 cancel current run + enqueue <text> for next dispatch (Task C.3.2)
22939
- /compact \u2014 compact the session transcript
22940
- /clear \u2014 clear the visible transcript (session is preserved)
22941
- /sessions \u2014 list past sessions
22942
- /resume <id> \u2014 load a past session
22943
- /branch <name> \u2014 snapshot the current session into a new branch
22944
- /branches \u2014 list branches
22945
- /checkout <name> \u2014 switch the active branch
22946
- /new \u2014 start a fresh session
22947
- /diff [--staged] \u2014 show uncommitted changes (or staged with --staged)
22948
- /undo [--yes] \u2014 revert working-tree changes (destructive! requires --yes)
22949
- /help \u2014 show this help
22950
- /exit \u2014 exit the CLI
22951
-
22952
- ${formatSkillList(availableSkills)}`
22953
- };
22954
- case "exit":
22955
- return { handled: true, kind: "exit", message: "Goodbye." };
22956
- case "clear":
22957
- return { handled: true, kind: "clear", message: "Transcript cleared." };
22958
- case "login": {
22959
- const provider = args[0];
22960
- if (!provider) {
22961
- return { handled: true, kind: "login", message: "Usage: /login <provider> [key] (or /login <provider> for OAuth if supported)" };
22962
- }
22963
- const key = args.slice(1).join(" ").trim();
22964
- if (!key) {
22965
- if (provider === "grok") {
22966
- return { handled: true, kind: "login_oauth", provider };
23168
+ let status;
23169
+ try {
23170
+ status = await runGit(["status", "--porcelain=v1"], cwd);
23171
+ } catch {
23172
+ return EMPTY_GIT_CHANGES;
23173
+ }
23174
+ const [unstaged, staged] = await Promise.all([
23175
+ runGit(["diff", "--numstat"], cwd).catch(() => ""),
23176
+ runGit(["diff", "--numstat", "--cached"], cwd).catch(() => "")
23177
+ ]);
23178
+ return {
23179
+ isRepo: true,
23180
+ branch,
23181
+ files: mergeChanges(parseNumstat(unstaged), parseNumstat(staged), parseUntracked(status))
23182
+ };
23183
+ }
23184
+ function useGitChanges(opts = {}) {
23185
+ const { pollMs = 4e3, cwd = process.cwd() } = opts;
23186
+ const [changes, setChanges] = useState(EMPTY_GIT_CHANGES);
23187
+ const lastJson = useRef("");
23188
+ useEffect(() => {
23189
+ let cancelled = false;
23190
+ let timer = null;
23191
+ const tick = async () => {
23192
+ try {
23193
+ const snap = await snapshotGitChanges(cwd);
23194
+ if (cancelled) return;
23195
+ const json2 = JSON.stringify(snap);
23196
+ if (json2 !== lastJson.current) {
23197
+ lastJson.current = json2;
23198
+ setChanges(snap);
22967
23199
  }
22968
- return {
22969
- handled: true,
22970
- kind: "login",
22971
- provider,
22972
- message: `[login] no key supplied \u2014 set ${provider.toUpperCase()}_API_KEY env or pass the key: /login ${provider} <key>`
22973
- };
22974
- }
22975
- return { handled: true, kind: "login", provider, loginKey: key };
22976
- }
22977
- case "model": {
22978
- const model = args[0];
22979
- if (!model) {
22980
- return {
22981
- handled: true,
22982
- kind: "model_show",
22983
- message: "Usage: /model <name> to change, /model to show current"
22984
- };
22985
- }
22986
- if (model === "refresh") {
22987
- return { handled: true, kind: "model_refresh" };
23200
+ } catch {
22988
23201
  }
22989
- return { handled: true, kind: "model_set", model };
22990
- }
22991
- case "models": {
22992
- const sub = args[0];
22993
- if (sub === "refresh") {
22994
- return { handled: true, kind: "models_refresh" };
23202
+ if (!cancelled) timer = setTimeout(tick, pollMs);
23203
+ };
23204
+ void tick();
23205
+ return () => {
23206
+ cancelled = true;
23207
+ if (timer !== null) clearTimeout(timer);
23208
+ };
23209
+ }, [pollMs, cwd]);
23210
+ return changes;
23211
+ }
23212
+
23213
+ // src/cli/hooks/useExecutionTimer.ts
23214
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
23215
+ function useExecutionTimer(busy, tickMs = 1e3) {
23216
+ const [elapsedMs, setElapsedMs] = useState2(null);
23217
+ const [lastMs, setLastMs] = useState2(null);
23218
+ const startRef = useRef2(null);
23219
+ useEffect2(() => {
23220
+ if (busy) {
23221
+ startRef.current = Date.now();
23222
+ setElapsedMs(0);
23223
+ const t = setInterval(() => {
23224
+ if (startRef.current !== null) setElapsedMs(Date.now() - startRef.current);
23225
+ }, tickMs);
23226
+ return () => clearInterval(t);
23227
+ }
23228
+ if (startRef.current !== null) {
23229
+ setLastMs(Date.now() - startRef.current);
23230
+ startRef.current = null;
23231
+ }
23232
+ setElapsedMs(null);
23233
+ return void 0;
23234
+ }, [busy, tickMs]);
23235
+ return { elapsedMs, lastMs };
23236
+ }
23237
+
23238
+ // src/cli/utils/paths.ts
23239
+ import { homedir } from "node:os";
23240
+ function shortenCwd(p3, maxLen = 40, home = homedir()) {
23241
+ let out = p3;
23242
+ if (home && (out === home || out.startsWith(`${home}\\`) || out.startsWith(`${home}/`))) {
23243
+ out = `~${out.slice(home.length)}`;
23244
+ }
23245
+ if (out.length <= maxLen) return out;
23246
+ return `\u2026${out.slice(-(maxLen - 1))}`;
23247
+ }
23248
+
23249
+ // packages/core/dist/agents/skills/builtin/debugging.js
23250
+ init_skills();
23251
+ var CLARIFICATION_PROTOCOL = `
23252
+
23253
+ WHEN TO ASK THE USER (clarification):
23254
+ If a single missing fact would materially change your output (target platform, scope, a binary design choice with significant trade-offs, a constraint you cannot safely assume), pause and ask the user by appending EXACTLY this block at the end of your message:
23255
+
23256
+ ---QUESTION---
23257
+ { "question": "One focused question", "choices": ["Option A", "Option B", "Option C"], "context": "Why this matters" }
23258
+ ---END---
23259
+
23260
+ Rules for clarifications:
23261
+ - Ask AT MOST ONE question per turn, and only when genuinely blocked.
23262
+ - Prefer a small set of concrete "choices" (2-4). The user can still type a wave-in response.
23263
+ - Do NOT ask for information that could be reasonably assumed or already in shared context.
23264
+ - If you can proceed with a sound documented assumption, DO SO instead of asking.`;
23265
+ var reproduceBug = {
23266
+ id: "reproduce-bug",
23267
+ version: "1.0.0",
23268
+ name: "Reproduce Bug",
23269
+ description: "Convert a bug report into a MINIMAL failing test. The test must fail without any code changes, then pass after the fix.",
23270
+ category: "debug",
23271
+ requiredRoles: ["minos", "pluton"],
23272
+ requiredTools: ["read_file", "write_file"],
23273
+ estimatedCost: "medium",
23274
+ enabledByDefault: true,
23275
+ builtin: true,
23276
+ triggers: [
23277
+ "Bug report without a minimal reproduction",
23278
+ "Bug that only happens in production (not in tests)",
23279
+ 'Bug that happens "sometimes" (intermittent)',
23280
+ "Bug reported via screenshot or video, not repro steps",
23281
+ "A complex multi-agent state race condition that is hard to verify without a test"
23282
+ ],
23283
+ antiPatterns: [
23284
+ "Bug is in production AND has minimal repro steps already \u2014 skip to fix",
23285
+ "Bug is in test code itself \u2014 fix the test, not the production code",
23286
+ "Bug requires special hardware/network state \u2014 use a mock instead"
23287
+ ],
23288
+ requires: [],
23289
+ relatedSkills: ["debug-with-rag", "root-cause-five-whys"],
23290
+ tags: ["debug", "reproduction", "test", "minimal-example"],
23291
+ examples: [
23292
+ {
23293
+ input: 'Bug report: "Council sometimes shows duplicate messages at the start of a session"',
23294
+ output: {
23295
+ minimalRepro: `// tests/regression/duplicate-messages.test.ts
23296
+ import { runCouncilPure } from '../agents/councilApi.js';
23297
+
23298
+ describe('council duplicate message bug', () => {
23299
+ it('does not emit duplicate user message when session resumes', async () => {
23300
+ const mockProvider = async function* () {
23301
+ yield { kind: 'text', delta: 'first' };
23302
+ yield { kind: 'finish', reason: 'stop' };
23303
+ };
23304
+ const harness = new AgentHarness({
23305
+ model: 'test', provider: 'test',
23306
+ messages: [
23307
+ { role: 'user', content: 'hi' },
23308
+ { role: 'user', content: 'hi' }, // duplicate from session resume
23309
+ ],
23310
+ tools: [],
23311
+ providerStream: mockProvider,
23312
+ });
23313
+ const events = [];
23314
+ for await (const e of harness.run()) events.push(e);
23315
+ const userMessages = events.filter(e => e.type === 'message_delta').length;
23316
+ expect(userMessages).toBeGreaterThan(0); // sanity
23317
+ // The actual bug: duplicate is rendered twice
23318
+ // After fix, expect exactly 1 user message rendered
23319
+ });
23320
+ });`,
23321
+ reproSteps: [
23322
+ "1. Set up session with duplicate user message in transcript",
23323
+ "2. Run runCouncilPure",
23324
+ "3. Observe duplicate message in output"
23325
+ ],
23326
+ whyMinimal: "Removed all agents except the first specialist (was: sisyphus+prometheus+hephaestus+oracle+chairman). Removed RAG context, workspace context. Kept only the duplication pattern."
22995
23327
  }
22996
- return { handled: true, kind: "models_list" };
22997
23328
  }
22998
- case "provider": {
22999
- const subcommand = args[0];
23000
- if (!subcommand) {
23001
- return {
23002
- handled: true,
23003
- kind: "provider_list",
23004
- message: "Usage: /provider <name> \u2014 switch active provider\n /provider custom <baseUrl> \u2014 set custom base URL (Ollama, LM Studio, vLLM, ...)\n /provider custom clear \u2014 clear the custom override\n /provider <name> refresh \u2014 force token refresh (v3-F)\n /provider <name> status \u2014 show key source, expiry, refresh impl (v3-F)\nAvailable: openai-compatible, minimax, glm, grok, custom"
23005
- };
23006
- }
23007
- if (subcommand === "custom") {
23008
- const target = args[1];
23009
- if (!target || target === "show") {
23010
- return {
23011
- handled: true,
23012
- kind: "provider_custom",
23013
- message: "Usage: /provider custom <baseUrl> \u2014 set custom base URL for the active provider\n /provider custom clear \u2014 clear the custom override for the active provider"
23014
- };
23015
- }
23016
- if (target === "clear") {
23017
- return {
23018
- handled: true,
23019
- kind: "provider_custom",
23020
- customClear: true
23021
- };
23022
- }
23023
- const url2 = args.slice(1).join(" ").trim();
23024
- return {
23025
- handled: true,
23026
- kind: "provider_custom",
23027
- customEndpoint: url2
23028
- };
23029
- }
23030
- const providerId = subcommand;
23031
- const sub = args[1];
23032
- if (sub === "refresh") {
23033
- return { handled: true, kind: "provider_refresh", provider: providerId };
23034
- }
23035
- if (sub === "status") {
23036
- return { handled: true, kind: "provider_status", provider: providerId };
23037
- }
23038
- return { handled: true, kind: "provider_set", provider: subcommand };
23039
- }
23040
- case "branch": {
23041
- const name = args[0];
23042
- if (!name) {
23043
- return {
23044
- handled: true,
23045
- kind: "branch_create",
23046
- message: "Usage: /branch <name> \u2014 snapshots the current session into a new branch"
23047
- };
23048
- }
23049
- return { handled: true, kind: "branch_create", branchName: name };
23050
- }
23051
- case "branches":
23052
- return { handled: true, kind: "branch_list" };
23053
- case "checkout": {
23054
- const name = args[0];
23055
- if (!name) {
23056
- return {
23057
- handled: true,
23058
- kind: "branch_checkout",
23059
- message: "Usage: /checkout <name> \u2014 switch the active branch"
23060
- };
23061
- }
23062
- return { handled: true, kind: "branch_checkout", branchName: name };
23063
- }
23064
- case "compact": {
23065
- let compactThreshold;
23066
- let compactKeepRecent;
23067
- for (let i = 0; i < args.length; i++) {
23068
- const a = args[i];
23069
- if (a === "--threshold" || a === "-t") {
23070
- const v = Number.parseInt(args[++i] ?? "", 10);
23071
- if (Number.isFinite(v) && v > 0) compactThreshold = v;
23072
- } else if (a === "--keep" || a === "-k") {
23073
- const v = Number.parseInt(args[++i] ?? "", 10);
23074
- if (Number.isFinite(v) && v > 0) compactKeepRecent = v;
23075
- }
23076
- }
23077
- return {
23078
- handled: true,
23079
- kind: "compact",
23080
- message: "Compacting session...",
23081
- compactThreshold,
23082
- compactKeepRecent
23083
- };
23084
- }
23085
- case "sessions":
23086
- return { handled: true, kind: "session" };
23087
- case "resume": {
23088
- const targetId = args[0];
23089
- if (!targetId) {
23090
- return { handled: true, kind: "resume", message: "Usage: /resume <id>" };
23091
- }
23092
- return {
23093
- handled: true,
23094
- kind: "resume",
23095
- targetSessionId: targetId,
23096
- message: `[resume] session ${targetId} ready \u2014 restart zelari-code to load`
23097
- };
23098
- }
23099
- case "new":
23100
- return {
23101
- handled: true,
23102
- kind: "new",
23103
- message: "[new] session reset \u2014 next prompt starts fresh"
23104
- };
23105
- case "council": {
23106
- const input = args.join(" ").trim();
23107
- if (!input) {
23108
- return {
23109
- handled: true,
23110
- kind: "council",
23111
- message: "Usage: /council <input> \u2014 invokes the multi-agent council on the input"
23112
- };
23113
- }
23114
- return { handled: true, kind: "council", councilInput: input };
23115
- }
23116
- case "council-feedback": {
23117
- const memberId = args[0];
23118
- const scoreStr = args[1];
23119
- if (!memberId || !scoreStr) {
23120
- return {
23121
- handled: true,
23122
- kind: "council_feedback",
23123
- message: "Usage: /council-feedback <memberId> <1-5> [note...] \u2014 rate a council member (e.g. /council-feedback sisyphus 4 great framing)"
23124
- };
23125
- }
23126
- const score = Number.parseInt(scoreStr, 10);
23127
- if (!Number.isInteger(score) || score < 1 || score > 5) {
23128
- return {
23129
- handled: true,
23130
- kind: "council_feedback",
23131
- message: `Invalid score "${scoreStr}" \u2014 must be an integer in [1, 5].`
23132
- };
23133
- }
23134
- const note = args.slice(2).join(" ").trim() || void 0;
23135
- return {
23136
- handled: true,
23137
- kind: "council_feedback",
23138
- feedbackMemberId: memberId,
23139
- feedbackScore: score,
23140
- ...note ? { feedbackNote: note } : {}
23141
- };
23142
- }
23143
- case "skill": {
23144
- const skillId = args[0];
23145
- if (!skillId) {
23146
- return {
23147
- handled: true,
23148
- kind: "skill",
23149
- skillError: "Usage: /skill <name> [input]",
23150
- message: formatSkillList(availableSkills)
23151
- };
23152
- }
23153
- const skill = availableSkills.find((s) => s.id === skillId);
23154
- if (!skill) {
23155
- return {
23156
- handled: true,
23157
- kind: "skill",
23158
- skillError: `Unknown skill: "${skillId}". Use /skill <TAB> for autocomplete.`,
23159
- message: formatSkillList(availableSkills)
23160
- };
23161
- }
23162
- const input = args.slice(1).join(" ");
23163
- const expandedSkill = expandSkillTemplate(skill, { input });
23164
- return { handled: true, kind: "skill", expandedSkill };
23165
- }
23166
- case "skill_stats": {
23167
- const skillId = args[0];
23168
- return {
23169
- handled: true,
23170
- kind: "skill_stats",
23171
- skillStatsSkillId: skillId,
23172
- message: skillId ? `Computing stats for skill "${skillId}"\u2026` : "Computing stats for all skills\u2026"
23173
- };
23174
- }
23175
- case "skill-compare": {
23176
- const id1 = args[0];
23177
- const id2 = args[1];
23178
- if (!id1 || !id2) {
23179
- return {
23180
- handled: true,
23181
- kind: "skill-compare",
23182
- message: "\u26A0 /skill-compare requires exactly 2 skill IDs (e.g. `/skill-compare debug refactor`)."
23183
- };
23184
- }
23185
- return {
23186
- handled: true,
23187
- kind: "skill-compare",
23188
- compareIds: [id1, id2],
23189
- message: `Comparing skills "${id1}" vs "${id2}"\u2026`
23190
- };
23191
- }
23192
- case "steer": {
23193
- let interrupt = false;
23194
- const filtered = [];
23195
- for (const a of args) {
23196
- if (a === "--interrupt" || a === "-i") {
23197
- interrupt = true;
23198
- } else {
23199
- filtered.push(a);
23200
- }
23201
- }
23202
- const text2 = filtered.join(" ").trim();
23203
- if (!text2) {
23204
- return {
23205
- handled: true,
23206
- kind: interrupt ? "steer_interrupt" : "steer",
23207
- message: "Usage: /steer [--interrupt|-i] <text> \u2014 enqueue (and optionally cancel) a follow-up prompt"
23208
- };
23209
- }
23210
- return {
23211
- handled: true,
23212
- kind: interrupt ? "steer_interrupt" : "steer",
23213
- steerText: text2
23214
- };
23215
- }
23216
- case "diff": {
23217
- const staged = args.includes("--staged") || args.includes("--cached");
23218
- return { handled: true, kind: "diff", diffStaged: staged ? true : void 0 };
23219
- }
23220
- case "promote-member": {
23221
- const memberId = args[0];
23222
- if (!memberId) {
23223
- return {
23224
- handled: true,
23225
- kind: "promote_member_error",
23226
- promoteMemberError: "Usage: /promote-member <memberId> \u2014 e.g. /promote-member geryon"
23227
- };
23228
- }
23229
- return { handled: true, kind: "promote_member", promoteMemberId: memberId };
23230
- }
23231
- case "update": {
23232
- let force = false;
23233
- for (const a of args) {
23234
- if (a === "--yes" || a === "-y") {
23235
- force = true;
23236
- }
23237
- }
23238
- if (args.length > 0 && !force) {
23239
- return {
23240
- handled: true,
23241
- kind: "update_usage",
23242
- message: "Usage: /update \u2014 check for the latest zelari-code version\n /update --yes (or -y) \u2014 install the latest version (will ask you to restart manually)"
23243
- };
23244
- }
23245
- return {
23246
- handled: true,
23247
- kind: force ? "update_perform" : "update_check",
23248
- updateForce: force
23249
- };
23250
- }
23251
- case "undo": {
23252
- const confirmed = args.includes("--yes") || args.includes("-y");
23253
- if (confirmed) {
23254
- return { handled: true, kind: "undo_confirm", undoConfirmed: true };
23255
- }
23256
- return {
23257
- handled: true,
23258
- kind: "undo",
23259
- message: "\u26A0 /undo is DESTRUCTIVE \u2014 it reverts all unstaged modifications and unstages everything.\nUse `/undo --yes` (or `/undo -y`) to confirm."
23260
- };
23261
- }
23262
- case "workspace": {
23263
- const sub = args[0];
23264
- if (!sub || sub === "--help" || sub === "help") {
23265
- return {
23266
- handled: true,
23267
- kind: "workspace",
23268
- message: "Workspace commands (`.zelari/` + `AGENTS.MD`):\n /workspace \u2014 list artifacts (plan, decisions, risks, reviews, docs)\n /workspace show plan \u2014 render plan.md\n /workspace show decisions \u2014 list ADRs (Architecture Decision Records)\n /workspace show risks \u2014 render risks.md\n /workspace show agents \u2014 render AGENTS.MD\n /workspace show docs \u2014 list docs drafts\n /workspace sync \u2014 re-run AGENTS.MD auto-curation now\n /workspace reset \u2014 delete .zelari/ (destructive \u2014 confirmation required)\n\nSee `docs/plans/2026-07-01-council-workspace-cli-stubs.md` for schema."
23269
- };
23270
- }
23271
- if (sub === "show") {
23272
- const what = args[1];
23273
- if (!what) {
23274
- return {
23275
- handled: true,
23276
- kind: "workspace_show",
23277
- workspaceWhat: "",
23278
- message: "Usage: /workspace show <plan|decisions|risks|agents|docs>"
23279
- };
23280
- }
23281
- if (!["plan", "decisions", "risks", "agents", "docs"].includes(what)) {
23282
- return {
23283
- handled: true,
23284
- kind: "workspace_show",
23285
- workspaceWhat: "",
23286
- message: `Unknown artifact "${what}". Available: plan | decisions | risks | agents | docs`
23287
- };
23288
- }
23289
- return {
23290
- handled: true,
23291
- kind: "workspace_show",
23292
- workspaceWhat: what
23293
- };
23294
- }
23295
- if (sub === "sync") {
23296
- return { handled: true, kind: "workspace_sync" };
23297
- }
23298
- if (sub === "reset") {
23299
- const force = args.includes("--yes") || args.includes("-y");
23300
- if (force) {
23301
- return { handled: true, kind: "workspace_reset", workspaceForce: true };
23302
- }
23303
- return {
23304
- handled: true,
23305
- kind: "workspace_reset",
23306
- message: "\u26A0 /workspace reset is DESTRUCTIVE \u2014 deletes the entire `.zelari/` directory.\nUse `/workspace reset --yes` to confirm."
23307
- };
23308
- }
23309
- return {
23310
- handled: true,
23311
- kind: "workspace",
23312
- message: `Unknown /workspace subcommand: "${sub}". Type /workspace for usage.`
23313
- };
23314
- }
23315
- default:
23316
- return {
23317
- handled: false,
23318
- kind: "unknown",
23319
- message: `Unknown command: /${command}. Type /help for available commands.`
23320
- };
23321
- }
23322
- }
23323
-
23324
- // src/cli/app.tsx
23325
- init_skills2();
23326
-
23327
- // packages/core/dist/agents/skills/builtin/debugging.js
23328
- init_skills();
23329
- var CLARIFICATION_PROTOCOL = `
23330
-
23331
- WHEN TO ASK THE USER (clarification):
23332
- If a single missing fact would materially change your output (target platform, scope, a binary design choice with significant trade-offs, a constraint you cannot safely assume), pause and ask the user by appending EXACTLY this block at the end of your message:
23333
-
23334
- ---QUESTION---
23335
- { "question": "One focused question", "choices": ["Option A", "Option B", "Option C"], "context": "Why this matters" }
23336
- ---END---
23337
-
23338
- Rules for clarifications:
23339
- - Ask AT MOST ONE question per turn, and only when genuinely blocked.
23340
- - Prefer a small set of concrete "choices" (2-4). The user can still type a wave-in response.
23341
- - Do NOT ask for information that could be reasonably assumed or already in shared context.
23342
- - If you can proceed with a sound documented assumption, DO SO instead of asking.`;
23343
- var reproduceBug = {
23344
- id: "reproduce-bug",
23345
- version: "1.0.0",
23346
- name: "Reproduce Bug",
23347
- description: "Convert a bug report into a MINIMAL failing test. The test must fail without any code changes, then pass after the fix.",
23348
- category: "debug",
23349
- requiredRoles: ["minos", "pluton"],
23350
- requiredTools: ["read_file", "write_file"],
23351
- estimatedCost: "medium",
23352
- enabledByDefault: true,
23353
- builtin: true,
23354
- triggers: [
23355
- "Bug report without a minimal reproduction",
23356
- "Bug that only happens in production (not in tests)",
23357
- 'Bug that happens "sometimes" (intermittent)',
23358
- "Bug reported via screenshot or video, not repro steps",
23359
- "A complex multi-agent state race condition that is hard to verify without a test"
23360
- ],
23361
- antiPatterns: [
23362
- "Bug is in production AND has minimal repro steps already \u2014 skip to fix",
23363
- "Bug is in test code itself \u2014 fix the test, not the production code",
23364
- "Bug requires special hardware/network state \u2014 use a mock instead"
23365
- ],
23366
- requires: [],
23367
- relatedSkills: ["debug-with-rag", "root-cause-five-whys"],
23368
- tags: ["debug", "reproduction", "test", "minimal-example"],
23369
- examples: [
23370
- {
23371
- input: 'Bug report: "Council sometimes shows duplicate messages at the start of a session"',
23372
- output: {
23373
- minimalRepro: `// tests/regression/duplicate-messages.test.ts
23374
- import { runCouncilPure } from '../agents/councilApi.js';
23375
-
23376
- describe('council duplicate message bug', () => {
23377
- it('does not emit duplicate user message when session resumes', async () => {
23378
- const mockProvider = async function* () {
23379
- yield { kind: 'text', delta: 'first' };
23380
- yield { kind: 'finish', reason: 'stop' };
23381
- };
23382
- const harness = new AgentHarness({
23383
- model: 'test', provider: 'test',
23384
- messages: [
23385
- { role: 'user', content: 'hi' },
23386
- { role: 'user', content: 'hi' }, // duplicate from session resume
23387
- ],
23388
- tools: [],
23389
- providerStream: mockProvider,
23390
- });
23391
- const events = [];
23392
- for await (const e of harness.run()) events.push(e);
23393
- const userMessages = events.filter(e => e.type === 'message_delta').length;
23394
- expect(userMessages).toBeGreaterThan(0); // sanity
23395
- // The actual bug: duplicate is rendered twice
23396
- // After fix, expect exactly 1 user message rendered
23397
- });
23398
- });`,
23399
- reproSteps: [
23400
- "1. Set up session with duplicate user message in transcript",
23401
- "2. Run runCouncilPure",
23402
- "3. Observe duplicate message in output"
23403
- ],
23404
- whyMinimal: "Removed all agents except the first specialist (was: sisyphus+prometheus+hephaestus+oracle+chairman). Removed RAG context, workspace context. Kept only the duplication pattern."
23405
- }
23406
- }
23407
- ],
23408
- outputSchema: "{ minimalRepro: string; reproSteps: string[]; whyMinimal: string }",
23409
- systemPromptFragment: `You are converting a bug report into a minimal failing test.
23410
-
23411
- ## Methodology
23412
- 1. **Read the bug report** carefully: what's the exact symptom? When does it happen?
23413
- 2. **Strip to essentials**: remove every agent, tool, env var, user state that isn't strictly required.
23414
- 3. **Identify the trigger**: what ONE input causes the bug?
23415
- 4. **Write the failing test**: it must FAIL on the current (buggy) code.
23416
- 5. **Verify it fails**: the test must fail without any code changes.
23417
- 6. **After the fix lands**, the test must PASS.
23418
-
23419
- ## Output format (JSON-typed)
23420
- - minimalRepro: string (the test code, runnable as-is)
23421
- - reproSteps: string[] (3-7 numbered steps to reproduce manually)
23422
- - whyMinimal: string (what you stripped + why)
23423
-
23424
- ## Minimal-repro principles
23425
- - **One assertion per test** \u2014 easier to debug when the test fails
23426
- - **Deterministic** \u2014 no flaky timing, no random data
23427
- - **No external dependencies** \u2014 use mocks for network/DB
23428
- - **Fast** \u2014 under 100ms if possible
23429
- - **Independent** \u2014 doesn't depend on other tests' state
23430
-
23431
- Stay under 300 words.${CLARIFICATION_PROTOCOL}`
23432
- };
23433
- var debugWithRag = {
23434
- id: "debug-with-rag",
23435
- version: "1.0.0",
23436
- name: "Debug With RAG",
23437
- description: "Search the knowledge base for similar past bugs, read the stack trace, propose the most likely root cause and a concrete fix.",
23438
- category: "debug",
23439
- requiredRoles: ["minos", "pluton"],
23440
- requiredTools: ["searchRAG", "grep_content", "read_file"],
23441
- estimatedCost: "medium",
23442
- enabledByDefault: true,
23443
- builtin: true,
23444
- triggers: [
23445
- "A bug report with a stack trace or error message",
23446
- "A regression: feature worked before, now broken",
23447
- "A flaky test that fails intermittently",
23448
- "Performance regression: response time > 5s where it used to be < 500ms",
23449
- "A memory leak causing out-of-memory crashes"
23450
- ],
23451
- antiPatterns: [
23452
- "Bug is obvious from the error message (e.g. TypeError: cannot read property of undefined \u2014 fix directly)",
23453
- "No stack trace or repro steps \u2014 first ask the user for a minimal repro",
23454
- "A test is failing because the test itself is wrong \u2014 debug the test, not the code"
23455
- ],
23456
- requires: ["reproduce-bug"],
23457
- relatedSkills: ["root-cause-five-whys", "reproduce-bug"],
23458
- tags: ["debug", "rag", "stack-trace", "root-cause", "regression"],
23459
- examples: [
23460
- {
23461
- input: 'TypeError: Cannot read properties of undefined (reading "map") at Council.tsx:847',
23462
- output: {
23463
- ragSearch: 'searchRAG(query="Council.tsx undefined map error") returned 2 prior incidents: similar bug fixed in commit a3f9d by adding optional chaining; another similar issue in Council.tsx:612.',
23464
- stackTraceAnalysis: "Line 847 is inside `renderMessages()`. The most likely cause: `messages` is undefined when `runCouncilPure()` returns early on the first iteration before any agent has produced output.",
23465
- rootCause: "race condition: messages array is set after the first agent completes, but renderMessages() is called during the loading state when messages is still undefined.",
23466
- proposedFix: `function renderMessages() {
23467
- const messages = session.messages ?? [];
23468
- return messages.map(m => <Message key={m.id} {...m} />);
23469
- }`,
23470
- verification: "Add a unit test that calls renderMessages() with messages=undefined and expects [] (not crash). Manual: trigger the bug scenario, confirm no crash."
23329
+ ],
23330
+ outputSchema: "{ minimalRepro: string; reproSteps: string[]; whyMinimal: string }",
23331
+ systemPromptFragment: `You are converting a bug report into a minimal failing test.
23332
+
23333
+ ## Methodology
23334
+ 1. **Read the bug report** carefully: what's the exact symptom? When does it happen?
23335
+ 2. **Strip to essentials**: remove every agent, tool, env var, user state that isn't strictly required.
23336
+ 3. **Identify the trigger**: what ONE input causes the bug?
23337
+ 4. **Write the failing test**: it must FAIL on the current (buggy) code.
23338
+ 5. **Verify it fails**: the test must fail without any code changes.
23339
+ 6. **After the fix lands**, the test must PASS.
23340
+
23341
+ ## Output format (JSON-typed)
23342
+ - minimalRepro: string (the test code, runnable as-is)
23343
+ - reproSteps: string[] (3-7 numbered steps to reproduce manually)
23344
+ - whyMinimal: string (what you stripped + why)
23345
+
23346
+ ## Minimal-repro principles
23347
+ - **One assertion per test** \u2014 easier to debug when the test fails
23348
+ - **Deterministic** \u2014 no flaky timing, no random data
23349
+ - **No external dependencies** \u2014 use mocks for network/DB
23350
+ - **Fast** \u2014 under 100ms if possible
23351
+ - **Independent** \u2014 doesn't depend on other tests' state
23352
+
23353
+ Stay under 300 words.${CLARIFICATION_PROTOCOL}`
23354
+ };
23355
+ var debugWithRag = {
23356
+ id: "debug-with-rag",
23357
+ version: "1.0.0",
23358
+ name: "Debug With RAG",
23359
+ description: "Search the knowledge base for similar past bugs, read the stack trace, propose the most likely root cause and a concrete fix.",
23360
+ category: "debug",
23361
+ requiredRoles: ["minos", "pluton"],
23362
+ requiredTools: ["searchRAG", "grep_content", "read_file"],
23363
+ estimatedCost: "medium",
23364
+ enabledByDefault: true,
23365
+ builtin: true,
23366
+ triggers: [
23367
+ "A bug report with a stack trace or error message",
23368
+ "A regression: feature worked before, now broken",
23369
+ "A flaky test that fails intermittently",
23370
+ "Performance regression: response time > 5s where it used to be < 500ms",
23371
+ "A memory leak causing out-of-memory crashes"
23372
+ ],
23373
+ antiPatterns: [
23374
+ "Bug is obvious from the error message (e.g. TypeError: cannot read property of undefined \u2014 fix directly)",
23375
+ "No stack trace or repro steps \u2014 first ask the user for a minimal repro",
23376
+ "A test is failing because the test itself is wrong \u2014 debug the test, not the code"
23377
+ ],
23378
+ requires: ["reproduce-bug"],
23379
+ relatedSkills: ["root-cause-five-whys", "reproduce-bug"],
23380
+ tags: ["debug", "rag", "stack-trace", "root-cause", "regression"],
23381
+ examples: [
23382
+ {
23383
+ input: 'TypeError: Cannot read properties of undefined (reading "map") at Council.tsx:847',
23384
+ output: {
23385
+ ragSearch: 'searchRAG(query="Council.tsx undefined map error") returned 2 prior incidents: similar bug fixed in commit a3f9d by adding optional chaining; another similar issue in Council.tsx:612.',
23386
+ stackTraceAnalysis: "Line 847 is inside `renderMessages()`. The most likely cause: `messages` is undefined when `runCouncilPure()` returns early on the first iteration before any agent has produced output.",
23387
+ rootCause: "race condition: messages array is set after the first agent completes, but renderMessages() is called during the loading state when messages is still undefined.",
23388
+ proposedFix: `function renderMessages() {
23389
+ const messages = session.messages ?? [];
23390
+ return messages.map(m => <Message key={m.id} {...m} />);
23391
+ }`,
23392
+ verification: "Add a unit test that calls renderMessages() with messages=undefined and expects [] (not crash). Manual: trigger the bug scenario, confirm no crash."
23471
23393
  }
23472
23394
  }
23473
23395
  ],
@@ -25374,7 +25296,7 @@ function defaultBaseDir() {
25374
25296
  }
25375
25297
 
25376
25298
  // src/cli/hooks/useSession.ts
25377
- import { useState, useRef, useCallback, useEffect } from "react";
25299
+ import { useState as useState3, useRef as useRef3, useCallback, useEffect as useEffect3 } from "react";
25378
25300
 
25379
25301
  // src/cli/sessionManager.ts
25380
25302
  import { promises as fs8, existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync, statSync } from "node:fs";
@@ -25654,20 +25576,20 @@ function completeTool(setFinalized, setLive, toolCallId, isError, durationMs, re
25654
25576
 
25655
25577
  // src/cli/hooks/useSession.ts
25656
25578
  function useSession() {
25657
- const [sessionId, setSessionId] = useState("");
25658
- const [messages, setMessages] = useState([]);
25659
- const [live, setLive] = useState(EMPTY_LIVE);
25660
- const [sessionActive, setSessionActive] = useState(false);
25661
- const writerRef = useRef(null);
25662
- const liveRef = useRef(EMPTY_LIVE);
25663
- useEffect(() => {
25579
+ const [sessionId, setSessionId] = useState3("");
25580
+ const [messages, setMessages] = useState3([]);
25581
+ const [live, setLive] = useState3(EMPTY_LIVE);
25582
+ const [sessionActive, setSessionActive] = useState3(false);
25583
+ const writerRef = useRef3(null);
25584
+ const liveRef = useRef3(EMPTY_LIVE);
25585
+ useEffect3(() => {
25664
25586
  liveRef.current = live;
25665
25587
  }, [live]);
25666
25588
  const resetTranscript = useCallback(() => {
25667
25589
  setMessages([]);
25668
25590
  setLive(EMPTY_LIVE);
25669
25591
  }, []);
25670
- useEffect(() => {
25592
+ useEffect3(() => {
25671
25593
  let cancelled = false;
25672
25594
  (async () => {
25673
25595
  await ensureSessionDir();
@@ -25761,7 +25683,7 @@ ${lines.join("\n")}`;
25761
25683
  }
25762
25684
 
25763
25685
  // src/cli/hooks/useChatTurn.ts
25764
- import { useState as useState2, useRef as useRef2, useCallback as useCallback2 } from "react";
25686
+ import { useState as useState4, useRef as useRef4, useCallback as useCallback2 } from "react";
25765
25687
 
25766
25688
  // src/cli/metrics.ts
25767
25689
  import { promises as fs9, existsSync as existsSync5, statSync as statSync2, renameSync, appendFileSync, mkdirSync as mkdirSync4 } from "node:fs";
@@ -26532,14 +26454,65 @@ function updateToolMessageEnd(setMessages, toolCallId, isError, durationMs, resu
26532
26454
  return prev2;
26533
26455
  });
26534
26456
  }
26535
- function finalizeStreamingAssistant(setMessages) {
26536
- setMessages((prev2) => {
26537
- const last = prev2[prev2.length - 1];
26538
- if (last && last.role === "assistant" && last.id.startsWith("streaming-")) {
26539
- return [...prev2.slice(0, -1), { ...last, id: last.id.slice("streaming-".length) }];
26540
- }
26541
- return prev2;
26542
- });
26457
+ function finalizeStreamingAssistant(setMessages) {
26458
+ setMessages((prev2) => {
26459
+ const last = prev2[prev2.length - 1];
26460
+ if (last && last.role === "assistant" && last.id.startsWith("streaming-")) {
26461
+ return [...prev2.slice(0, -1), { ...last, id: last.id.slice("streaming-".length) }];
26462
+ }
26463
+ return prev2;
26464
+ });
26465
+ }
26466
+
26467
+ // src/cli/modelPricing.ts
26468
+ var PRICES_PER_MILLION = {
26469
+ // xAI Grok
26470
+ "grok-4": { input: 3, output: 15 },
26471
+ "grok-4-fast": { input: 0.2, output: 0.5 },
26472
+ "grok-3": { input: 3, output: 15 },
26473
+ "grok-3-mini": { input: 0.3, output: 0.5 },
26474
+ "grok-2-vision": { input: 2, output: 10 },
26475
+ // GLM / Z.AI
26476
+ "glm-4.6": { input: 0.6, output: 2.2 },
26477
+ "glm-4.5": { input: 0.5, output: 2 },
26478
+ "glm-4.5-air": { input: 0.1, output: 0.6 },
26479
+ "glm-z1": { input: 0.5, output: 2 },
26480
+ // MiniMax
26481
+ "MiniMax-M2.5": { input: 0.2, output: 1.1 },
26482
+ "MiniMax-M2": { input: 0.2, output: 1.1 },
26483
+ "MiniMax-M2-her": { input: 0.3, output: 1.2 },
26484
+ // OpenAI (for openai-compatible fallback)
26485
+ "gpt-4o": { input: 2.5, output: 10 },
26486
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
26487
+ "gpt-4-turbo": { input: 10, output: 30 },
26488
+ "o1-preview": { input: 15, output: 60 },
26489
+ "o1-mini": { input: 3, output: 12 }
26490
+ };
26491
+ var DEFAULT_RATE = { input: 1, output: 3 };
26492
+ function envOverride(model) {
26493
+ const key = `ANATHEMA_PRICE_${model.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
26494
+ const raw = process.env[key] ?? process.env.ANATHEMA_PRICE_DEFAULT;
26495
+ return parseRateOverride(raw);
26496
+ }
26497
+ function parseRateOverride(raw) {
26498
+ if (!raw) return null;
26499
+ const [inStr, outStr] = raw.split("/");
26500
+ const input = Number.parseFloat(inStr);
26501
+ const output = outStr !== void 0 ? Number.parseFloat(outStr) : input;
26502
+ if (!Number.isFinite(input) || !Number.isFinite(output)) return null;
26503
+ return { input, output };
26504
+ }
26505
+ function getModelRate(model) {
26506
+ if (!model) return DEFAULT_RATE;
26507
+ return envOverride(model) ?? PRICES_PER_MILLION[model] ?? DEFAULT_RATE;
26508
+ }
26509
+ function calculateCost(model, promptTokens, completionTokens) {
26510
+ if (!Number.isFinite(promptTokens) || promptTokens < 0) return 0;
26511
+ if (!Number.isFinite(completionTokens) || completionTokens < 0) return 0;
26512
+ const rate = getModelRate(model);
26513
+ const inputCost = promptTokens / 1e6 * rate.input;
26514
+ const outputCost = completionTokens / 1e6 * rate.output;
26515
+ return Number((inputCost + outputCost).toFixed(6));
26543
26516
  }
26544
26517
 
26545
26518
  // src/cli/hooks/chatStats.ts
@@ -26567,8 +26540,8 @@ function useChatTurn(params) {
26567
26540
  setLive,
26568
26541
  liveRef
26569
26542
  } = params;
26570
- const harnessRef = useRef2(null);
26571
- const [queueCount, setQueueCount] = useState2(0);
26543
+ const harnessRef = useRef4(null);
26544
+ const [queueCount, setQueueCount] = useState4(0);
26572
26545
  const useLiveModel = !!(setLive && liveRef);
26573
26546
  const dispatchPrompt = useCallback2(
26574
26547
  async (userText, opts) => {
@@ -26615,7 +26588,11 @@ function useChatTurn(params) {
26615
26588
  let workspaceSummary = null;
26616
26589
  let zelariReadHint = "";
26617
26590
  try {
26618
- const { buildPlanSummary: buildPlanSummary2, buildWorkspaceSummary: buildWorkspaceSummary2, buildZelariReadHint: buildZelariReadHint2 } = await Promise.resolve().then(() => (init_workspaceSummary(), workspaceSummary_exports));
26591
+ const {
26592
+ buildPlanSummary: buildPlanSummary2,
26593
+ buildWorkspaceSummary: buildWorkspaceSummary2,
26594
+ buildZelariReadHint: buildZelariReadHint2
26595
+ } = await Promise.resolve().then(() => (init_workspaceSummary(), workspaceSummary_exports));
26619
26596
  planSummary = buildPlanSummary2(cwd);
26620
26597
  workspaceSummary = buildWorkspaceSummary2(cwd);
26621
26598
  zelariReadHint = buildZelariReadHint2(cwd);
@@ -26639,7 +26616,9 @@ function useChatTurn(params) {
26639
26616
  if (wantedWorkspaceTools.size > 0) {
26640
26617
  const { createWorkspaceContext: createWorkspaceContext2 } = await Promise.resolve().then(() => (init_stubs(), stubs_exports));
26641
26618
  const { createWorkspaceToolRegistry: createWorkspaceToolRegistry2 } = await Promise.resolve().then(() => (init_toolRegistry(), toolRegistry_exports));
26642
- const wsRegistry = createWorkspaceToolRegistry2(createWorkspaceContext2(cwd));
26619
+ const wsRegistry = createWorkspaceToolRegistry2(
26620
+ createWorkspaceContext2(cwd)
26621
+ );
26643
26622
  for (const name of wantedWorkspaceTools) {
26644
26623
  const td = wsRegistry.get(name);
26645
26624
  if (td) toolRegistry.register(td);
@@ -26773,10 +26752,15 @@ function useChatTurn(params) {
26773
26752
  ...event.memberName ? { memberName: event.memberName } : {}
26774
26753
  });
26775
26754
  } else {
26776
- appendOrExtendStreamingAssistant(commitStreaming, streamContent, Date.now(), {
26777
- ...event.memberId ? { memberId: event.memberId } : {},
26778
- ...event.memberName ? { memberName: event.memberName } : {}
26779
- });
26755
+ appendOrExtendStreamingAssistant(
26756
+ commitStreaming,
26757
+ streamContent,
26758
+ Date.now(),
26759
+ {
26760
+ ...event.memberId ? { memberId: event.memberId } : {},
26761
+ ...event.memberName ? { memberName: event.memberName } : {}
26762
+ }
26763
+ );
26780
26764
  }
26781
26765
  } else if (event.type === "error") {
26782
26766
  flushStreaming();
@@ -26786,17 +26770,42 @@ function useChatTurn(params) {
26786
26770
  flushStreaming();
26787
26771
  if (useLiveModel) {
26788
26772
  finalizeStreaming(setMessages, setLive);
26789
- startTool(setLive, event.toolName, event.toolCallId, event.args, event.ts);
26773
+ startTool(
26774
+ setLive,
26775
+ event.toolName,
26776
+ event.toolCallId,
26777
+ event.args,
26778
+ event.ts
26779
+ );
26790
26780
  } else {
26791
26781
  finalizeStreamingAssistant(setMessages);
26792
- appendToolStart(setMessages, event.toolName, event.toolCallId, event.args, event.ts);
26782
+ appendToolStart(
26783
+ setMessages,
26784
+ event.toolName,
26785
+ event.toolCallId,
26786
+ event.args,
26787
+ event.ts
26788
+ );
26793
26789
  }
26794
26790
  streamContent = "";
26795
26791
  } else if (event.type === "tool_execution_end") {
26796
26792
  if (useLiveModel) {
26797
- completeTool(setMessages, setLive, event.toolCallId, event.isError, event.durationMs, event.result);
26793
+ completeTool(
26794
+ setMessages,
26795
+ setLive,
26796
+ event.toolCallId,
26797
+ event.isError,
26798
+ event.durationMs,
26799
+ event.result
26800
+ );
26798
26801
  } else {
26799
- updateToolMessageEnd(setMessages, event.toolCallId, event.isError, event.durationMs, event.result);
26802
+ updateToolMessageEnd(
26803
+ setMessages,
26804
+ event.toolCallId,
26805
+ event.isError,
26806
+ event.durationMs,
26807
+ event.result
26808
+ );
26800
26809
  }
26801
26810
  }
26802
26811
  }
@@ -26858,266 +26867,734 @@ function useChatTurn(params) {
26858
26867
  sessionId,
26859
26868
  writerRef,
26860
26869
  setMessages,
26861
- commitStreaming,
26862
- flushStreaming,
26863
- setBusy,
26864
- setQueueCount,
26865
- setLive,
26866
- liveRef
26867
- ]
26868
- );
26870
+ commitStreaming,
26871
+ flushStreaming,
26872
+ setBusy,
26873
+ setQueueCount,
26874
+ setLive,
26875
+ liveRef
26876
+ ]
26877
+ );
26878
+ return {
26879
+ dispatchPrompt,
26880
+ dispatchCouncilPrompt,
26881
+ harnessRef,
26882
+ queueCount,
26883
+ setQueueCount
26884
+ };
26885
+ }
26886
+ async function dispatchCouncilPromptImpl(text, deps) {
26887
+ const {
26888
+ sessionId,
26889
+ writerRef,
26890
+ setMessages,
26891
+ commitStreaming,
26892
+ flushStreaming,
26893
+ setBusy,
26894
+ setLive,
26895
+ liveRef
26896
+ } = deps;
26897
+ const useLiveModel = !!(setLive && liveRef);
26898
+ const envConfig = await providerFromEnv();
26899
+ if (!envConfig) {
26900
+ const active = resolveActiveProvider();
26901
+ const spec = PROVIDERS.find((p3) => p3.id === active);
26902
+ appendSystem(
26903
+ setMessages,
26904
+ `No API key for the active provider "${active}". Set ${spec?.envVar ?? "the provider API key env var"} or run /login ${active} before invoking /council.`
26905
+ );
26906
+ return;
26907
+ }
26908
+ setBusy(true);
26909
+ const { dispatchCouncil: dispatchCouncil2 } = await Promise.resolve().then(() => (init_councilDispatcher(), councilDispatcher_exports));
26910
+ const { createWorkspaceContext: createWorkspaceContext2, createWorkspaceStubs: createWorkspaceStubs2 } = await Promise.resolve().then(() => (init_stubs(), stubs_exports));
26911
+ const { createWorkspaceToolRegistry: createWorkspaceToolRegistry2 } = await Promise.resolve().then(() => (init_toolRegistry(), toolRegistry_exports));
26912
+ const { setWorkspaceStubs: setWorkspaceStubs2 } = await Promise.resolve().then(() => (init_skills2(), skills_exports));
26913
+ const { runPostCouncilHook: runPostCouncilHook2 } = await Promise.resolve().then(() => (init_postCouncilHook(), postCouncilHook_exports));
26914
+ const { buildWorkspaceSummary: buildWorkspaceSummary2, buildPlanSummary: buildPlanSummary2 } = await Promise.resolve().then(() => (init_workspaceSummary(), workspaceSummary_exports));
26915
+ const { FeedbackStore: FeedbackStore2 } = await Promise.resolve().then(() => (init_councilFeedback(), councilFeedback_exports));
26916
+ const { registry: councilToolRegistry } = createBuiltinToolRegistry();
26917
+ const workspaceCtx = createWorkspaceContext2();
26918
+ const workspaceReg = createWorkspaceToolRegistry2(workspaceCtx);
26919
+ for (const name of workspaceReg.list()) {
26920
+ const td = workspaceReg.get(name);
26921
+ if (td) councilToolRegistry.register(td);
26922
+ }
26923
+ try {
26924
+ const { registerMcpTools: registerMcpTools2 } = await Promise.resolve().then(() => (init_mcpManager(), mcpManager_exports));
26925
+ const mcp = await registerMcpTools2(councilToolRegistry);
26926
+ for (const w of mcp.warnings) appendSystem(setMessages, w);
26927
+ } catch {
26928
+ }
26929
+ setWorkspaceStubs2(createWorkspaceStubs2(workspaceCtx));
26930
+ const councilFeedbackStore = new FeedbackStore2();
26931
+ let streamContent = "";
26932
+ let streamMemberId = null;
26933
+ const councilMaxToolCalls = (() => {
26934
+ const raw = process.env.ZELARI_MAX_TOOL_CALLS;
26935
+ const n = raw ? Number.parseInt(raw, 10) : 15;
26936
+ return Number.isFinite(n) && n > 0 ? n : 15;
26937
+ })();
26938
+ let membersCompleted = 0;
26939
+ let chairmanProducedOutput = false;
26940
+ let consecutiveProviderErrors = 0;
26941
+ let lastErrorMessage = "";
26942
+ let councilAborted = false;
26943
+ let councilRunMode = "implementation";
26944
+ const PROVIDER_ERROR_ABORT_THRESHOLD = 2;
26945
+ try {
26946
+ for await (const event of dispatchCouncil2(text, {
26947
+ apiKey: envConfig.apiKey,
26948
+ model: envConfig.model,
26949
+ provider: "openai-compatible",
26950
+ providerStream: openaiCompatibleProvider(envConfig),
26951
+ sessionId,
26952
+ tools: councilToolRegistry,
26953
+ feedbackStore: councilFeedbackStore,
26954
+ // v0.7.2 (B2): give the council the same project awareness the
26955
+ // single-prompt path has — cwd, tech stack, file layout, build scripts.
26956
+ // Without this, members had no idea which project they were operating
26957
+ // on and projected their identity onto the task.
26958
+ // v0.7.3: append the existing plan (if any) so a follow-up /council
26959
+ // continues it instead of re-planning from scratch.
26960
+ workspaceContext: [
26961
+ buildWorkspaceSummary2(process.cwd()),
26962
+ buildPlanSummary2(process.cwd())
26963
+ ].filter(Boolean).join("\n\n"),
26964
+ maxToolCallsPerTurn: councilMaxToolCalls
26965
+ })) {
26966
+ if (councilAborted) {
26967
+ if (writerRef.current) await writerRef.current.append(event);
26968
+ continue;
26969
+ }
26970
+ if (writerRef.current) {
26971
+ await writerRef.current.append(event);
26972
+ }
26973
+ if (event.type === "council_mode") {
26974
+ councilRunMode = event.runMode;
26975
+ appendSystem(
26976
+ setMessages,
26977
+ `[council] ${event.tier} \xB7 ${event.runMode} \xB7 ${event.councilSize} members`,
26978
+ event.ts
26979
+ );
26980
+ } else if (event.type === "message_delta") {
26981
+ if (useLiveModel) {
26982
+ const memberId = event.memberId ?? null;
26983
+ if (memberId !== streamMemberId) {
26984
+ flushStreaming();
26985
+ finalizeStreaming(setMessages, setLive);
26986
+ streamContent = "";
26987
+ streamMemberId = memberId;
26988
+ }
26989
+ streamContent += event.delta;
26990
+ setStreaming(commitStreaming, streamContent, event.ts, {
26991
+ ...event.memberId ? { memberId: event.memberId } : {},
26992
+ ...event.memberName ? { memberName: event.memberName } : {}
26993
+ });
26994
+ } else {
26995
+ commitStreaming((prev2) => {
26996
+ const last = prev2[prev2.length - 1];
26997
+ if (last && last.role === "assistant" && last.id.startsWith("streaming-") && (last.memberId ?? null) === (event.memberId ?? null)) {
26998
+ return [
26999
+ ...prev2.slice(0, -1),
27000
+ { ...last, content: last.content + event.delta }
27001
+ ];
27002
+ }
27003
+ return [
27004
+ ...prev2,
27005
+ {
27006
+ id: `streaming-${crypto.randomUUID()}`,
27007
+ role: "assistant",
27008
+ content: event.delta,
27009
+ ts: event.ts,
27010
+ ...event.memberId ? { memberId: event.memberId } : {},
27011
+ ...event.memberName ? { memberName: event.memberName } : {}
27012
+ }
27013
+ ];
27014
+ });
27015
+ }
27016
+ } else if (event.type === "message_end") {
27017
+ flushStreaming();
27018
+ if (useLiveModel) finalizeStreaming(setMessages, setLive);
27019
+ else finalizeStreamingAssistant(setMessages);
27020
+ streamContent = "";
27021
+ streamMemberId = null;
27022
+ membersCompleted++;
27023
+ if (event.memberId === "lucifer" || event.memberName === "Lucifero") {
27024
+ chairmanProducedOutput = true;
27025
+ }
27026
+ } else if (event.type === "tool_execution_start") {
27027
+ flushStreaming();
27028
+ if (useLiveModel) {
27029
+ finalizeStreaming(setMessages, setLive);
27030
+ startTool(
27031
+ setLive,
27032
+ event.toolName,
27033
+ event.toolCallId,
27034
+ event.args,
27035
+ event.ts
27036
+ );
27037
+ } else {
27038
+ finalizeStreamingAssistant(setMessages);
27039
+ appendToolStart(
27040
+ setMessages,
27041
+ event.toolName,
27042
+ event.toolCallId,
27043
+ event.args,
27044
+ event.ts
27045
+ );
27046
+ }
27047
+ streamContent = "";
27048
+ } else if (event.type === "tool_execution_end") {
27049
+ if (useLiveModel) {
27050
+ completeTool(
27051
+ setMessages,
27052
+ setLive,
27053
+ event.toolCallId,
27054
+ event.isError,
27055
+ event.durationMs,
27056
+ event.result
27057
+ );
27058
+ } else {
27059
+ updateToolMessageEnd(
27060
+ setMessages,
27061
+ event.toolCallId,
27062
+ event.isError,
27063
+ event.durationMs,
27064
+ event.result
27065
+ );
27066
+ }
27067
+ } else if (event.type === "error") {
27068
+ flushStreaming();
27069
+ const memberTag = event.memberName ? ` \xB7 ${event.memberName}` : "";
27070
+ appendSystem(
27071
+ setMessages,
27072
+ `[error${memberTag}] ${event.message}`,
27073
+ event.ts
27074
+ );
27075
+ if (event.message === lastErrorMessage) {
27076
+ consecutiveProviderErrors++;
27077
+ } else {
27078
+ consecutiveProviderErrors = 1;
27079
+ lastErrorMessage = event.message;
27080
+ }
27081
+ if (consecutiveProviderErrors >= PROVIDER_ERROR_ABORT_THRESHOLD) {
27082
+ councilAborted = true;
27083
+ appendSystem(
27084
+ setMessages,
27085
+ `[council aborted: repeated provider error \u2014 ${consecutiveProviderErrors}\xD7 "${event.message.slice(0, 80)}"]`,
27086
+ Date.now()
27087
+ );
27088
+ }
27089
+ }
27090
+ }
27091
+ } catch (err) {
27092
+ flushStreaming();
27093
+ appendSystem(
27094
+ setMessages,
27095
+ `[council error] ${err instanceof Error ? err.message : String(err)}`,
27096
+ Date.now()
27097
+ );
27098
+ } finally {
27099
+ flushStreaming();
27100
+ if (useLiveModel) finalizeStreaming(setMessages, setLive);
27101
+ else finalizeStreamingAssistant(setMessages);
27102
+ const hookShouldRun = membersCompleted > 0 || chairmanProducedOutput;
27103
+ if (hookShouldRun) {
27104
+ try {
27105
+ const hook = await runPostCouncilHook2(workspaceCtx, {
27106
+ runMode: councilRunMode
27107
+ });
27108
+ if (hook.ran && hook.changed) {
27109
+ appendSystem(
27110
+ setMessages,
27111
+ `[agents.md] updated: ${hook.sections.length} section(s) changed (${hook.sections.join(", ")})`,
27112
+ Date.now()
27113
+ );
27114
+ } else if (hook.ran && hook.reason) {
27115
+ if (!hook.reason.includes("disabled")) {
27116
+ appendSystem(setMessages, `[agents.md] ${hook.reason}`, Date.now());
27117
+ }
27118
+ }
27119
+ } catch {
27120
+ }
27121
+ } else if (!councilAborted) {
27122
+ appendSystem(
27123
+ setMessages,
27124
+ "[agents.md] skipped \u2014 council produced no output",
27125
+ Date.now()
27126
+ );
27127
+ }
27128
+ setBusy(false);
27129
+ }
27130
+ }
27131
+
27132
+ // src/cli/hooks/useSlashDispatch.ts
27133
+ import { useCallback as useCallback3 } from "react";
27134
+
27135
+ // src/cli/slashCommands.ts
27136
+ function parseSlashCommand(text) {
27137
+ const trimmed = text.trim();
27138
+ if (!trimmed.startsWith("/")) return null;
27139
+ const parts = trimmed.slice(1).split(/\s+/);
27140
+ const command = parts[0];
27141
+ const args = parts.slice(1);
27142
+ return { command, args };
27143
+ }
27144
+ function expandSkillTemplate(skill, options = {}) {
27145
+ const userInput = options.input?.trim() ?? "";
27146
+ const prompt = userInput ? `${skill.systemPromptFragment}
27147
+
27148
+ ## User input
27149
+ ${userInput}` : `${skill.systemPromptFragment}
27150
+
27151
+ ## User input
27152
+ (Please provide the task description or input for this skill.)`;
26869
27153
  return {
26870
- dispatchPrompt,
26871
- dispatchCouncilPrompt,
26872
- harnessRef,
26873
- queueCount,
26874
- setQueueCount
27154
+ skillId: skill.id,
27155
+ prompt,
27156
+ requiredRoles: skill.requiredRoles,
27157
+ requiredTools: skill.requiredTools,
27158
+ estimatedCost: skill.estimatedCost
26875
27159
  };
26876
27160
  }
26877
- async function dispatchCouncilPromptImpl(text, deps) {
26878
- const {
26879
- sessionId,
26880
- writerRef,
26881
- setMessages,
26882
- commitStreaming,
26883
- flushStreaming,
26884
- setBusy,
26885
- setLive,
26886
- liveRef
26887
- } = deps;
26888
- const useLiveModel = !!(setLive && liveRef);
26889
- const envConfig = await providerFromEnv();
26890
- if (!envConfig) {
26891
- const active = resolveActiveProvider();
26892
- const spec = PROVIDERS.find((p3) => p3.id === active);
26893
- appendSystem(
26894
- setMessages,
26895
- `No API key for the active provider "${active}". Set ${spec?.envVar ?? "the provider API key env var"} or run /login ${active} before invoking /council.`
26896
- );
26897
- return;
27161
+ function formatSkillList(skills) {
27162
+ if (skills.length === 0) return "(no skills available)";
27163
+ const byCategory = /* @__PURE__ */ new Map();
27164
+ for (const skill of skills) {
27165
+ const list = byCategory.get(skill.category) ?? [];
27166
+ list.push(skill);
27167
+ byCategory.set(skill.category, list);
26898
27168
  }
26899
- setBusy(true);
26900
- const { dispatchCouncil: dispatchCouncil2 } = await Promise.resolve().then(() => (init_councilDispatcher(), councilDispatcher_exports));
26901
- const { createWorkspaceContext: createWorkspaceContext2, createWorkspaceStubs: createWorkspaceStubs2 } = await Promise.resolve().then(() => (init_stubs(), stubs_exports));
26902
- const { createWorkspaceToolRegistry: createWorkspaceToolRegistry2 } = await Promise.resolve().then(() => (init_toolRegistry(), toolRegistry_exports));
26903
- const { setWorkspaceStubs: setWorkspaceStubs2 } = await Promise.resolve().then(() => (init_skills2(), skills_exports));
26904
- const { runPostCouncilHook: runPostCouncilHook2 } = await Promise.resolve().then(() => (init_postCouncilHook(), postCouncilHook_exports));
26905
- const { buildWorkspaceSummary: buildWorkspaceSummary2, buildPlanSummary: buildPlanSummary2 } = await Promise.resolve().then(() => (init_workspaceSummary(), workspaceSummary_exports));
26906
- const { FeedbackStore: FeedbackStore2 } = await Promise.resolve().then(() => (init_councilFeedback(), councilFeedback_exports));
26907
- const { registry: councilToolRegistry } = createBuiltinToolRegistry();
26908
- const workspaceCtx = createWorkspaceContext2();
26909
- const workspaceReg = createWorkspaceToolRegistry2(workspaceCtx);
26910
- for (const name of workspaceReg.list()) {
26911
- const td = workspaceReg.get(name);
26912
- if (td) councilToolRegistry.register(td);
27169
+ const lines = ["Available skills:"];
27170
+ for (const [category, list] of [...byCategory.entries()].sort()) {
27171
+ lines.push(`
27172
+ [${category}]`);
27173
+ for (const skill of list) {
27174
+ const costLabel = skill.estimatedCost;
27175
+ lines.push(` /skill ${skill.id} \u2014 ${skill.name} (${costLabel} cost)`);
27176
+ }
26913
27177
  }
26914
- try {
26915
- const { registerMcpTools: registerMcpTools2 } = await Promise.resolve().then(() => (init_mcpManager(), mcpManager_exports));
26916
- const mcp = await registerMcpTools2(councilToolRegistry);
26917
- for (const w of mcp.warnings) appendSystem(setMessages, w);
26918
- } catch {
27178
+ return lines.join("\n");
27179
+ }
27180
+ function handleSlashCommand(text, availableSkills) {
27181
+ const parsed = parseSlashCommand(text);
27182
+ if (!parsed) {
27183
+ return { handled: false, kind: "unknown" };
26919
27184
  }
26920
- setWorkspaceStubs2(createWorkspaceStubs2(workspaceCtx));
26921
- const councilFeedbackStore = new FeedbackStore2();
26922
- let streamContent = "";
26923
- let streamMemberId = null;
26924
- const councilMaxToolCalls = (() => {
26925
- const raw = process.env.ZELARI_MAX_TOOL_CALLS;
26926
- const n = raw ? Number.parseInt(raw, 10) : 15;
26927
- return Number.isFinite(n) && n > 0 ? n : 15;
26928
- })();
26929
- let membersCompleted = 0;
26930
- let chairmanProducedOutput = false;
26931
- let consecutiveProviderErrors = 0;
26932
- let lastErrorMessage = "";
26933
- let councilAborted = false;
26934
- const PROVIDER_ERROR_ABORT_THRESHOLD = 2;
26935
- try {
26936
- for await (const event of dispatchCouncil2(text, {
26937
- apiKey: envConfig.apiKey,
26938
- model: envConfig.model,
26939
- provider: "openai-compatible",
26940
- providerStream: openaiCompatibleProvider(envConfig),
26941
- sessionId,
26942
- tools: councilToolRegistry,
26943
- feedbackStore: councilFeedbackStore,
26944
- // v0.7.2 (B2): give the council the same project awareness the
26945
- // single-prompt path has — cwd, tech stack, file layout, build scripts.
26946
- // Without this, members had no idea which project they were operating
26947
- // on and projected their identity onto the task.
26948
- // v0.7.3: append the existing plan (if any) so a follow-up /council
26949
- // continues it instead of re-planning from scratch.
26950
- workspaceContext: [
26951
- buildWorkspaceSummary2(process.cwd()),
26952
- buildPlanSummary2(process.cwd())
26953
- ].filter(Boolean).join("\n\n"),
26954
- maxToolCallsPerTurn: councilMaxToolCalls
26955
- })) {
26956
- if (councilAborted) {
26957
- if (writerRef.current) await writerRef.current.append(event);
26958
- continue;
27185
+ const { command, args } = parsed;
27186
+ switch (command) {
27187
+ case "help":
27188
+ return {
27189
+ handled: true,
27190
+ kind: "help",
27191
+ message: `Available commands:
27192
+ /login <provider> \u2014 authenticate with provider (grok, minimax, glm, custom)
27193
+ /model <name> \u2014 switch the active model
27194
+ /model refresh \u2014 re-discover models for the active provider (v3-U)
27195
+ /models \u2014 list discovered models for the active provider (v3-U)
27196
+ /models refresh \u2014 re-discover models for the active provider (v3-U)
27197
+ /provider <name> \u2014 switch the active provider
27198
+ /provider custom <baseUrl> \u2014 point the active provider at a self-hosted endpoint (Ollama, LM Studio, vLLM, ...)
27199
+ /provider custom clear \u2014 clear the custom endpoint override
27200
+ /skill <name> [input] \u2014 invoke a skill (autocomplete with /skill <TAB>)
27201
+ /skill-stats [name] \u2014 show invocation stats (success rate, avg duration, total tokens)
27202
+ /council <input> \u2014 invoke the multi-agent council on input
27203
+ /council-feedback <memberId> <1-5> [note] \u2014 rate a council member for future ranking (Task I.2)
27204
+ /promote-member <memberId> \u2014 promote a council member to a standalone skill (v3-K)
27205
+ /update [--yes|-y] \u2014 check for zelari-code updates; --yes performs the update (v3-N)
27206
+ /steer <text> \u2014 enqueue a follow-up prompt on the active run (Task 18.2)
27207
+ /steer --interrupt <text> \u2014 cancel current run + enqueue <text> for next dispatch (Task C.3.2)
27208
+ /compact \u2014 compact the session transcript
27209
+ /clear \u2014 clear the visible transcript (session is preserved)
27210
+ /sessions \u2014 list past sessions
27211
+ /resume <id> \u2014 load a past session
27212
+ /branch <name> \u2014 snapshot the current session into a new branch
27213
+ /branches \u2014 list branches
27214
+ /checkout <name> \u2014 switch the active branch
27215
+ /new \u2014 start a fresh session
27216
+ /diff [--staged] \u2014 show uncommitted changes (or staged with --staged)
27217
+ /undo [--yes] \u2014 revert working-tree changes (destructive! requires --yes)
27218
+ /help \u2014 show this help
27219
+ /exit \u2014 exit the CLI
27220
+
27221
+ ${formatSkillList(availableSkills)}`
27222
+ };
27223
+ case "exit":
27224
+ return { handled: true, kind: "exit", message: "Goodbye." };
27225
+ case "clear":
27226
+ return { handled: true, kind: "clear", message: "Transcript cleared." };
27227
+ case "login": {
27228
+ const provider = args[0];
27229
+ if (!provider) {
27230
+ return { handled: true, kind: "login", message: "Usage: /login <provider> [key] (or /login <provider> for OAuth if supported)" };
27231
+ }
27232
+ const key = args.slice(1).join(" ").trim();
27233
+ if (!key) {
27234
+ if (provider === "grok") {
27235
+ return { handled: true, kind: "login_oauth", provider };
27236
+ }
27237
+ return {
27238
+ handled: true,
27239
+ kind: "login",
27240
+ provider,
27241
+ message: `[login] no key supplied \u2014 set ${provider.toUpperCase()}_API_KEY env or pass the key: /login ${provider} <key>`
27242
+ };
27243
+ }
27244
+ return { handled: true, kind: "login", provider, loginKey: key };
27245
+ }
27246
+ case "model": {
27247
+ const model = args[0];
27248
+ if (!model) {
27249
+ return {
27250
+ handled: true,
27251
+ kind: "model_show",
27252
+ message: "Usage: /model <name> to change, /model to show current"
27253
+ };
27254
+ }
27255
+ if (model === "refresh") {
27256
+ return { handled: true, kind: "model_refresh" };
27257
+ }
27258
+ return { handled: true, kind: "model_set", model };
27259
+ }
27260
+ case "models": {
27261
+ const sub = args[0];
27262
+ if (sub === "refresh") {
27263
+ return { handled: true, kind: "models_refresh" };
26959
27264
  }
26960
- if (writerRef.current) {
26961
- await writerRef.current.append(event);
27265
+ return { handled: true, kind: "models_list" };
27266
+ }
27267
+ case "provider": {
27268
+ const subcommand = args[0];
27269
+ if (!subcommand) {
27270
+ return {
27271
+ handled: true,
27272
+ kind: "provider_list",
27273
+ message: "Usage: /provider <name> \u2014 switch active provider\n /provider custom <baseUrl> \u2014 set custom base URL (Ollama, LM Studio, vLLM, ...)\n /provider custom clear \u2014 clear the custom override\n /provider <name> refresh \u2014 force token refresh (v3-F)\n /provider <name> status \u2014 show key source, expiry, refresh impl (v3-F)\nAvailable: openai-compatible, minimax, glm, grok, custom"
27274
+ };
26962
27275
  }
26963
- if (event.type === "message_delta") {
26964
- if (useLiveModel) {
26965
- const memberId = event.memberId ?? null;
26966
- if (memberId !== streamMemberId) {
26967
- flushStreaming();
26968
- finalizeStreaming(setMessages, setLive);
26969
- streamContent = "";
26970
- streamMemberId = memberId;
26971
- }
26972
- streamContent += event.delta;
26973
- setStreaming(commitStreaming, streamContent, event.ts, {
26974
- ...event.memberId ? { memberId: event.memberId } : {},
26975
- ...event.memberName ? { memberName: event.memberName } : {}
26976
- });
26977
- } else {
26978
- commitStreaming((prev2) => {
26979
- const last = prev2[prev2.length - 1];
26980
- if (last && last.role === "assistant" && last.id.startsWith("streaming-") && (last.memberId ?? null) === (event.memberId ?? null)) {
26981
- return [
26982
- ...prev2.slice(0, -1),
26983
- { ...last, content: last.content + event.delta }
26984
- ];
26985
- }
26986
- return [
26987
- ...prev2,
26988
- {
26989
- id: `streaming-${crypto.randomUUID()}`,
26990
- role: "assistant",
26991
- content: event.delta,
26992
- ts: event.ts,
26993
- ...event.memberId ? { memberId: event.memberId } : {},
26994
- ...event.memberName ? { memberName: event.memberName } : {}
26995
- }
26996
- ];
26997
- });
27276
+ if (subcommand === "custom") {
27277
+ const target = args[1];
27278
+ if (!target || target === "show") {
27279
+ return {
27280
+ handled: true,
27281
+ kind: "provider_custom",
27282
+ message: "Usage: /provider custom <baseUrl> \u2014 set custom base URL for the active provider\n /provider custom clear \u2014 clear the custom override for the active provider"
27283
+ };
26998
27284
  }
26999
- } else if (event.type === "message_end") {
27000
- flushStreaming();
27001
- if (useLiveModel) finalizeStreaming(setMessages, setLive);
27002
- else finalizeStreamingAssistant(setMessages);
27003
- streamContent = "";
27004
- streamMemberId = null;
27005
- membersCompleted++;
27006
- if (event.memberId === "lucifer" || event.memberName === "Lucifero") {
27007
- chairmanProducedOutput = true;
27285
+ if (target === "clear") {
27286
+ return {
27287
+ handled: true,
27288
+ kind: "provider_custom",
27289
+ customClear: true
27290
+ };
27008
27291
  }
27009
- } else if (event.type === "tool_execution_start") {
27010
- flushStreaming();
27011
- if (useLiveModel) {
27012
- finalizeStreaming(setMessages, setLive);
27013
- startTool(
27014
- setLive,
27015
- event.toolName,
27016
- event.toolCallId,
27017
- event.args,
27018
- event.ts
27019
- );
27292
+ const url2 = args.slice(1).join(" ").trim();
27293
+ return {
27294
+ handled: true,
27295
+ kind: "provider_custom",
27296
+ customEndpoint: url2
27297
+ };
27298
+ }
27299
+ const providerId = subcommand;
27300
+ const sub = args[1];
27301
+ if (sub === "refresh") {
27302
+ return { handled: true, kind: "provider_refresh", provider: providerId };
27303
+ }
27304
+ if (sub === "status") {
27305
+ return { handled: true, kind: "provider_status", provider: providerId };
27306
+ }
27307
+ return { handled: true, kind: "provider_set", provider: subcommand };
27308
+ }
27309
+ case "branch": {
27310
+ const name = args[0];
27311
+ if (!name) {
27312
+ return {
27313
+ handled: true,
27314
+ kind: "branch_create",
27315
+ message: "Usage: /branch <name> \u2014 snapshots the current session into a new branch"
27316
+ };
27317
+ }
27318
+ return { handled: true, kind: "branch_create", branchName: name };
27319
+ }
27320
+ case "branches":
27321
+ return { handled: true, kind: "branch_list" };
27322
+ case "checkout": {
27323
+ const name = args[0];
27324
+ if (!name) {
27325
+ return {
27326
+ handled: true,
27327
+ kind: "branch_checkout",
27328
+ message: "Usage: /checkout <name> \u2014 switch the active branch"
27329
+ };
27330
+ }
27331
+ return { handled: true, kind: "branch_checkout", branchName: name };
27332
+ }
27333
+ case "compact": {
27334
+ let compactThreshold;
27335
+ let compactKeepRecent;
27336
+ for (let i = 0; i < args.length; i++) {
27337
+ const a = args[i];
27338
+ if (a === "--threshold" || a === "-t") {
27339
+ const v = Number.parseInt(args[++i] ?? "", 10);
27340
+ if (Number.isFinite(v) && v > 0) compactThreshold = v;
27341
+ } else if (a === "--keep" || a === "-k") {
27342
+ const v = Number.parseInt(args[++i] ?? "", 10);
27343
+ if (Number.isFinite(v) && v > 0) compactKeepRecent = v;
27344
+ }
27345
+ }
27346
+ return {
27347
+ handled: true,
27348
+ kind: "compact",
27349
+ message: "Compacting session...",
27350
+ compactThreshold,
27351
+ compactKeepRecent
27352
+ };
27353
+ }
27354
+ case "sessions":
27355
+ return { handled: true, kind: "session" };
27356
+ case "resume": {
27357
+ const targetId = args[0];
27358
+ if (!targetId) {
27359
+ return { handled: true, kind: "resume", message: "Usage: /resume <id>" };
27360
+ }
27361
+ return {
27362
+ handled: true,
27363
+ kind: "resume",
27364
+ targetSessionId: targetId,
27365
+ message: `[resume] session ${targetId} ready \u2014 restart zelari-code to load`
27366
+ };
27367
+ }
27368
+ case "new":
27369
+ return {
27370
+ handled: true,
27371
+ kind: "new",
27372
+ message: "[new] session reset \u2014 next prompt starts fresh"
27373
+ };
27374
+ case "council": {
27375
+ const input = args.join(" ").trim();
27376
+ if (!input) {
27377
+ return {
27378
+ handled: true,
27379
+ kind: "council",
27380
+ message: "Usage: /council <input> \u2014 invokes the multi-agent council on the input"
27381
+ };
27382
+ }
27383
+ return { handled: true, kind: "council", councilInput: input };
27384
+ }
27385
+ case "council-feedback": {
27386
+ const memberId = args[0];
27387
+ const scoreStr = args[1];
27388
+ if (!memberId || !scoreStr) {
27389
+ return {
27390
+ handled: true,
27391
+ kind: "council_feedback",
27392
+ message: "Usage: /council-feedback <memberId> <1-5> [note...] \u2014 rate a council member (e.g. /council-feedback sisyphus 4 great framing)"
27393
+ };
27394
+ }
27395
+ const score = Number.parseInt(scoreStr, 10);
27396
+ if (!Number.isInteger(score) || score < 1 || score > 5) {
27397
+ return {
27398
+ handled: true,
27399
+ kind: "council_feedback",
27400
+ message: `Invalid score "${scoreStr}" \u2014 must be an integer in [1, 5].`
27401
+ };
27402
+ }
27403
+ const note = args.slice(2).join(" ").trim() || void 0;
27404
+ return {
27405
+ handled: true,
27406
+ kind: "council_feedback",
27407
+ feedbackMemberId: memberId,
27408
+ feedbackScore: score,
27409
+ ...note ? { feedbackNote: note } : {}
27410
+ };
27411
+ }
27412
+ case "skill": {
27413
+ const skillId = args[0];
27414
+ if (!skillId) {
27415
+ return {
27416
+ handled: true,
27417
+ kind: "skill",
27418
+ skillError: "Usage: /skill <name> [input]",
27419
+ message: formatSkillList(availableSkills)
27420
+ };
27421
+ }
27422
+ const skill = availableSkills.find((s) => s.id === skillId);
27423
+ if (!skill) {
27424
+ return {
27425
+ handled: true,
27426
+ kind: "skill",
27427
+ skillError: `Unknown skill: "${skillId}". Use /skill <TAB> for autocomplete.`,
27428
+ message: formatSkillList(availableSkills)
27429
+ };
27430
+ }
27431
+ const input = args.slice(1).join(" ");
27432
+ const expandedSkill = expandSkillTemplate(skill, { input });
27433
+ return { handled: true, kind: "skill", expandedSkill };
27434
+ }
27435
+ case "skill_stats": {
27436
+ const skillId = args[0];
27437
+ return {
27438
+ handled: true,
27439
+ kind: "skill_stats",
27440
+ skillStatsSkillId: skillId,
27441
+ message: skillId ? `Computing stats for skill "${skillId}"\u2026` : "Computing stats for all skills\u2026"
27442
+ };
27443
+ }
27444
+ case "skill-compare": {
27445
+ const id1 = args[0];
27446
+ const id2 = args[1];
27447
+ if (!id1 || !id2) {
27448
+ return {
27449
+ handled: true,
27450
+ kind: "skill-compare",
27451
+ message: "\u26A0 /skill-compare requires exactly 2 skill IDs (e.g. `/skill-compare debug refactor`)."
27452
+ };
27453
+ }
27454
+ return {
27455
+ handled: true,
27456
+ kind: "skill-compare",
27457
+ compareIds: [id1, id2],
27458
+ message: `Comparing skills "${id1}" vs "${id2}"\u2026`
27459
+ };
27460
+ }
27461
+ case "steer": {
27462
+ let interrupt = false;
27463
+ const filtered = [];
27464
+ for (const a of args) {
27465
+ if (a === "--interrupt" || a === "-i") {
27466
+ interrupt = true;
27020
27467
  } else {
27021
- finalizeStreamingAssistant(setMessages);
27022
- appendToolStart(
27023
- setMessages,
27024
- event.toolName,
27025
- event.toolCallId,
27026
- event.args,
27027
- event.ts
27028
- );
27468
+ filtered.push(a);
27029
27469
  }
27030
- streamContent = "";
27031
- } else if (event.type === "tool_execution_end") {
27032
- if (useLiveModel) {
27033
- completeTool(
27034
- setMessages,
27035
- setLive,
27036
- event.toolCallId,
27037
- event.isError,
27038
- event.durationMs,
27039
- event.result
27040
- );
27041
- } else {
27042
- updateToolMessageEnd(
27043
- setMessages,
27044
- event.toolCallId,
27045
- event.isError,
27046
- event.durationMs,
27047
- event.result
27048
- );
27470
+ }
27471
+ const text2 = filtered.join(" ").trim();
27472
+ if (!text2) {
27473
+ return {
27474
+ handled: true,
27475
+ kind: interrupt ? "steer_interrupt" : "steer",
27476
+ message: "Usage: /steer [--interrupt|-i] <text> \u2014 enqueue (and optionally cancel) a follow-up prompt"
27477
+ };
27478
+ }
27479
+ return {
27480
+ handled: true,
27481
+ kind: interrupt ? "steer_interrupt" : "steer",
27482
+ steerText: text2
27483
+ };
27484
+ }
27485
+ case "diff": {
27486
+ const staged = args.includes("--staged") || args.includes("--cached");
27487
+ return { handled: true, kind: "diff", diffStaged: staged ? true : void 0 };
27488
+ }
27489
+ case "promote-member": {
27490
+ const memberId = args[0];
27491
+ if (!memberId) {
27492
+ return {
27493
+ handled: true,
27494
+ kind: "promote_member_error",
27495
+ promoteMemberError: "Usage: /promote-member <memberId> \u2014 e.g. /promote-member geryon"
27496
+ };
27497
+ }
27498
+ return { handled: true, kind: "promote_member", promoteMemberId: memberId };
27499
+ }
27500
+ case "update": {
27501
+ let force = false;
27502
+ for (const a of args) {
27503
+ if (a === "--yes" || a === "-y") {
27504
+ force = true;
27049
27505
  }
27050
- } else if (event.type === "error") {
27051
- flushStreaming();
27052
- const memberTag = event.memberName ? ` \xB7 ${event.memberName}` : "";
27053
- appendSystem(
27054
- setMessages,
27055
- `[error${memberTag}] ${event.message}`,
27056
- event.ts
27057
- );
27058
- if (event.message === lastErrorMessage) {
27059
- consecutiveProviderErrors++;
27060
- } else {
27061
- consecutiveProviderErrors = 1;
27062
- lastErrorMessage = event.message;
27506
+ }
27507
+ if (args.length > 0 && !force) {
27508
+ return {
27509
+ handled: true,
27510
+ kind: "update_usage",
27511
+ message: "Usage: /update \u2014 check for the latest zelari-code version\n /update --yes (or -y) \u2014 install the latest version (will ask you to restart manually)"
27512
+ };
27513
+ }
27514
+ return {
27515
+ handled: true,
27516
+ kind: force ? "update_perform" : "update_check",
27517
+ updateForce: force
27518
+ };
27519
+ }
27520
+ case "undo": {
27521
+ const confirmed = args.includes("--yes") || args.includes("-y");
27522
+ if (confirmed) {
27523
+ return { handled: true, kind: "undo_confirm", undoConfirmed: true };
27524
+ }
27525
+ return {
27526
+ handled: true,
27527
+ kind: "undo",
27528
+ message: "\u26A0 /undo is DESTRUCTIVE \u2014 it reverts all unstaged modifications and unstages everything.\nUse `/undo --yes` (or `/undo -y`) to confirm."
27529
+ };
27530
+ }
27531
+ case "workspace": {
27532
+ const sub = args[0];
27533
+ if (!sub || sub === "--help" || sub === "help") {
27534
+ return {
27535
+ handled: true,
27536
+ kind: "workspace",
27537
+ message: "Workspace commands (`.zelari/` + `AGENTS.MD`):\n /workspace \u2014 list artifacts (plan, decisions, risks, reviews, docs)\n /workspace show plan \u2014 render plan.md\n /workspace show decisions \u2014 list ADRs (Architecture Decision Records)\n /workspace show risks \u2014 render risks.md\n /workspace show agents \u2014 render AGENTS.MD\n /workspace show docs \u2014 list docs drafts\n /workspace sync \u2014 re-run AGENTS.MD auto-curation now\n /workspace reset \u2014 delete .zelari/ (destructive \u2014 confirmation required)\n\nSee `docs/plans/2026-07-01-council-workspace-cli-stubs.md` for schema."
27538
+ };
27539
+ }
27540
+ if (sub === "show") {
27541
+ const what = args[1];
27542
+ if (!what) {
27543
+ return {
27544
+ handled: true,
27545
+ kind: "workspace_show",
27546
+ workspaceWhat: "",
27547
+ message: "Usage: /workspace show <plan|decisions|risks|agents|docs>"
27548
+ };
27063
27549
  }
27064
- if (consecutiveProviderErrors >= PROVIDER_ERROR_ABORT_THRESHOLD) {
27065
- councilAborted = true;
27066
- appendSystem(
27067
- setMessages,
27068
- `[council aborted: repeated provider error \u2014 ${consecutiveProviderErrors}\xD7 "${event.message.slice(0, 80)}"]`,
27069
- Date.now()
27070
- );
27550
+ if (!["plan", "decisions", "risks", "agents", "docs"].includes(what)) {
27551
+ return {
27552
+ handled: true,
27553
+ kind: "workspace_show",
27554
+ workspaceWhat: "",
27555
+ message: `Unknown artifact "${what}". Available: plan | decisions | risks | agents | docs`
27556
+ };
27071
27557
  }
27558
+ return {
27559
+ handled: true,
27560
+ kind: "workspace_show",
27561
+ workspaceWhat: what
27562
+ };
27072
27563
  }
27073
- }
27074
- } catch (err) {
27075
- flushStreaming();
27076
- appendSystem(
27077
- setMessages,
27078
- `[council error] ${err instanceof Error ? err.message : String(err)}`,
27079
- Date.now()
27080
- );
27081
- } finally {
27082
- flushStreaming();
27083
- if (useLiveModel) finalizeStreaming(setMessages, setLive);
27084
- else finalizeStreamingAssistant(setMessages);
27085
- const hookShouldRun = membersCompleted > 0 || chairmanProducedOutput;
27086
- if (hookShouldRun) {
27087
- try {
27088
- const hook = await runPostCouncilHook2(workspaceCtx);
27089
- if (hook.ran && hook.changed) {
27090
- appendSystem(
27091
- setMessages,
27092
- `[agents.md] updated: ${hook.sections.length} section(s) changed (${hook.sections.join(", ")})`,
27093
- Date.now()
27094
- );
27095
- } else if (hook.ran && hook.reason) {
27096
- if (!hook.reason.includes("disabled")) {
27097
- appendSystem(setMessages, `[agents.md] ${hook.reason}`, Date.now());
27098
- }
27564
+ if (sub === "sync") {
27565
+ return { handled: true, kind: "workspace_sync" };
27566
+ }
27567
+ if (sub === "reset") {
27568
+ const force = args.includes("--yes") || args.includes("-y");
27569
+ if (force) {
27570
+ return { handled: true, kind: "workspace_reset", workspaceForce: true };
27099
27571
  }
27100
- } catch {
27572
+ return {
27573
+ handled: true,
27574
+ kind: "workspace_reset",
27575
+ message: "\u26A0 /workspace reset is DESTRUCTIVE \u2014 deletes the entire `.zelari/` directory.\nUse `/workspace reset --yes` to confirm."
27576
+ };
27101
27577
  }
27102
- } else if (!councilAborted) {
27103
- appendSystem(
27104
- setMessages,
27105
- "[agents.md] skipped \u2014 council produced no output",
27106
- Date.now()
27107
- );
27578
+ return {
27579
+ handled: true,
27580
+ kind: "workspace",
27581
+ message: `Unknown /workspace subcommand: "${sub}". Type /workspace for usage.`
27582
+ };
27108
27583
  }
27109
- setBusy(false);
27584
+ default:
27585
+ return {
27586
+ handled: false,
27587
+ kind: "unknown",
27588
+ message: `Unknown command: /${command}. Type /help for available commands.`
27589
+ };
27110
27590
  }
27111
27591
  }
27112
27592
 
27113
- // src/cli/hooks/useSlashDispatch.ts
27114
- import { useCallback as useCallback3 } from "react";
27115
-
27116
27593
  // src/cli/gitOps.ts
27117
- import { execFile } from "node:child_process";
27594
+ import { execFile as execFile2 } from "node:child_process";
27118
27595
  import { promisify } from "node:util";
27119
27596
  import path15 from "node:path";
27120
- var execFileAsync = promisify(execFile);
27597
+ var execFileAsync = promisify(execFile2);
27121
27598
  async function git(cwd, args) {
27122
27599
  try {
27123
27600
  const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], {
@@ -27327,7 +27804,7 @@ async function handlePromoteMember(ctx, memberId) {
27327
27804
  }
27328
27805
 
27329
27806
  // src/cli/branchManager.ts
27330
- import { promises as fs13, existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, statSync as statSync4, rmSync } from "node:fs";
27807
+ import { promises as fs13, existsSync as existsSync15, readFileSync as readFileSync12, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, statSync as statSync4, rmSync } from "node:fs";
27331
27808
  import path18 from "node:path";
27332
27809
  import os9 from "node:os";
27333
27810
  var META_FILENAME = "meta.json";
@@ -27349,11 +27826,11 @@ function sessionsPathFor(name, baseDir) {
27349
27826
  }
27350
27827
  function readBranchMeta(name, baseDir) {
27351
27828
  const metaPath = metaPathFor(name, baseDir);
27352
- if (!existsSync14(metaPath)) {
27829
+ if (!existsSync15(metaPath)) {
27353
27830
  throw new BranchNotFoundError(`Branch "${name}" not found`);
27354
27831
  }
27355
27832
  try {
27356
- const raw = readFileSync11(metaPath, "utf-8");
27833
+ const raw = readFileSync12(metaPath, "utf-8");
27357
27834
  const parsed = JSON.parse(raw);
27358
27835
  if (!parsed || typeof parsed !== "object" || typeof parsed.name !== "string" || typeof parsed.createdAt !== "number" || typeof parsed.fromSessionId !== "string") {
27359
27836
  throw new BranchCorruptError(`Branch "${name}" meta.json is malformed`);
@@ -27409,7 +27886,7 @@ var SessionNotFoundError = class extends Error {
27409
27886
  };
27410
27887
  function branchExists(name, baseDir = getBranchesBaseDir()) {
27411
27888
  const bp = branchPathFor(name, baseDir);
27412
- return existsSync14(bp) && existsSync14(metaPathFor(name, baseDir));
27889
+ return existsSync15(bp) && existsSync15(metaPathFor(name, baseDir));
27413
27890
  }
27414
27891
  async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(), sessionsBaseDir = getSessionsBaseDir()) {
27415
27892
  if (!name || name.trim().length === 0) {
@@ -27422,7 +27899,7 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
27422
27899
  throw new BranchAlreadyExistsError(name);
27423
27900
  }
27424
27901
  const sourcePath = path18.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
27425
- if (!existsSync14(sourcePath)) {
27902
+ if (!existsSync15(sourcePath)) {
27426
27903
  throw new SessionNotFoundError(`Source session "${fromSessionId}" not found at ${sourcePath}`);
27427
27904
  }
27428
27905
  const branchPath = branchPathFor(name, baseDir);
@@ -27455,7 +27932,7 @@ async function listBranches(baseDir = getBranchesBaseDir()) {
27455
27932
  const results = [];
27456
27933
  for (const entry of entries) {
27457
27934
  const metaPath = metaPathFor(entry, baseDir);
27458
- if (!existsSync14(metaPath)) continue;
27935
+ if (!existsSync15(metaPath)) continue;
27459
27936
  try {
27460
27937
  const meta3 = readBranchMeta(entry, baseDir);
27461
27938
  const sessionCount = await countSessions(entry, baseDir);
@@ -27693,8 +28170,8 @@ async function validateApiKey(providerId, apiKey, options = {}) {
27693
28170
  init_providerConfig();
27694
28171
 
27695
28172
  // src/cli/modelDiscovery.ts
27696
- import { promises as fs15, existsSync as existsSync15, readFileSync as readFileSync12 } from "node:fs";
27697
- import { homedir as homedir3 } from "node:os";
28173
+ import { promises as fs15, existsSync as existsSync16, readFileSync as readFileSync13 } from "node:fs";
28174
+ import { homedir as homedir4 } from "node:os";
27698
28175
  import path20 from "node:path";
27699
28176
  var PROVIDER_BASE_URLS = {
27700
28177
  "grok": "https://api.x.ai/v1",
@@ -27703,15 +28180,15 @@ var PROVIDER_BASE_URLS = {
27703
28180
  "openai-compatible": "https://api.openai.com/v1"
27704
28181
  };
27705
28182
  function defaultModelsFilePath() {
27706
- return process.env.ANATHEMA_MODELS_FILE ?? path20.join(homedir3(), ".tmp", "zelari-code", "models.json");
28183
+ return process.env.ANATHEMA_MODELS_FILE ?? path20.join(homedir4(), ".tmp", "zelari-code", "models.json");
27707
28184
  }
27708
28185
  function getModelsFilePath() {
27709
28186
  return defaultModelsFilePath();
27710
28187
  }
27711
28188
  function loadModelsRegistry(file2 = getModelsFilePath()) {
27712
- if (!existsSync15(file2)) return {};
28189
+ if (!existsSync16(file2)) return {};
27713
28190
  try {
27714
- const raw = readFileSync12(file2, "utf-8");
28191
+ const raw = readFileSync13(file2, "utf-8");
27715
28192
  const parsed = JSON.parse(raw);
27716
28193
  if (!parsed || typeof parsed !== "object") return {};
27717
28194
  return parsed;
@@ -27835,24 +28312,6 @@ var ModelDiscoveryError = class extends Error {
27835
28312
  }
27836
28313
  };
27837
28314
 
27838
- // src/cli/utils/duration.ts
27839
- function formatDuration(ms) {
27840
- const abs = Math.abs(ms);
27841
- const sign = ms < 0 ? " ago" : "";
27842
- if (abs < 1e3) return `${abs}ms${sign}`;
27843
- const s = Math.floor(abs / 1e3);
27844
- if (s < 60) return `${s}s${sign}`;
27845
- const m = Math.floor(s / 60);
27846
- const rs = s % 60;
27847
- if (m < 60) return rs === 0 ? `${m}m${sign}` : `${m}m ${rs}s${sign}`;
27848
- const h = Math.floor(m / 60);
27849
- const rm = m % 60;
27850
- if (h < 24) return rm === 0 ? `${h}h${sign}` : `${h}h ${rm}m${sign}`;
27851
- const d = Math.floor(h / 24);
27852
- const rh = h % 24;
27853
- return rh === 0 ? `${d}d${sign}` : `${d}d ${rh}h${sign}`;
27854
- }
27855
-
27856
28315
  // src/cli/slashHandlers/provider.ts
27857
28316
  var UNKNOWN_PROVIDER_MSG = (id) => `[provider] unknown: ${id}. Available: openai-compatible, minimax, glm, grok, custom`;
27858
28317
  function handleProviderList(ctx) {
@@ -28071,7 +28530,7 @@ import path21 from "node:path";
28071
28530
  import os10 from "node:os";
28072
28531
 
28073
28532
  // src/cli/skillHistory.ts
28074
- import { promises as fs16, existsSync as existsSync16, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync2, mkdirSync as mkdirSync11 } from "node:fs";
28533
+ import { promises as fs16, existsSync as existsSync17, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync2, mkdirSync as mkdirSync11 } from "node:fs";
28075
28534
  var SKILL_HISTORY_ROTATE_BYTES = 10 * 1024 * 1024;
28076
28535
  async function readSkillHistory(file2) {
28077
28536
  let raw = "";
@@ -28258,7 +28717,8 @@ function useSlashDispatch(params) {
28258
28717
  harnessRef,
28259
28718
  setQueueCount,
28260
28719
  dispatchPrompt,
28261
- dispatchCouncilPrompt
28720
+ dispatchCouncilPrompt,
28721
+ mode = "agent"
28262
28722
  } = params;
28263
28723
  return useCallback3(async (value) => {
28264
28724
  if (!value.trim()) return;
@@ -28280,6 +28740,11 @@ function useSlashDispatch(params) {
28280
28740
  if (!result.handled) {
28281
28741
  appendUser(setMessages, value);
28282
28742
  setSessionActive(true);
28743
+ if (mode === "council") {
28744
+ setInput("");
28745
+ await dispatchCouncilPrompt(value);
28746
+ return;
28747
+ }
28283
28748
  await dispatchPrompt(value);
28284
28749
  setInput("");
28285
28750
  return;
@@ -28521,15 +28986,16 @@ function useSlashDispatch(params) {
28521
28986
  setQueueCount,
28522
28987
  dispatchPrompt,
28523
28988
  dispatchCouncilPrompt,
28989
+ mode,
28524
28990
  params
28525
28991
  ]);
28526
28992
  }
28527
28993
 
28528
28994
  // src/cli/hooks/useBatchedMessages.ts
28529
- import { useCallback as useCallback4, useEffect as useEffect2, useRef as useRef3 } from "react";
28995
+ import { useCallback as useCallback4, useEffect as useEffect4, useRef as useRef5 } from "react";
28530
28996
  function useBatchedMessages(state2, setState, cadenceMs = 16) {
28531
- const pendingRef = useRef3(null);
28532
- const timerRef = useRef3(null);
28997
+ const pendingRef = useRef5(null);
28998
+ const timerRef = useRef5(null);
28533
28999
  const flush = useCallback4(() => {
28534
29000
  if (timerRef.current !== null) {
28535
29001
  clearTimeout(timerRef.current);
@@ -28567,7 +29033,7 @@ function useBatchedMessages(state2, setState, cadenceMs = 16) {
28567
29033
  },
28568
29034
  [cadenceMs, setState]
28569
29035
  );
28570
- useEffect2(() => {
29036
+ useEffect4(() => {
28571
29037
  return () => {
28572
29038
  if (timerRef.current !== null) {
28573
29039
  clearTimeout(timerRef.current);
@@ -28579,16 +29045,16 @@ function useBatchedMessages(state2, setState, cadenceMs = 16) {
28579
29045
  }
28580
29046
 
28581
29047
  // src/cli/hooks/useTerminalSize.ts
28582
- import { useEffect as useEffect3, useState as useState3 } from "react";
29048
+ import { useEffect as useEffect5, useState as useState5 } from "react";
28583
29049
  import { useStdout as useStdout2 } from "ink";
28584
29050
  function useTerminalSize(options = {}) {
28585
29051
  const { defaults = { columns: 80, rows: 24 }, coalesceMs = 16 } = options;
28586
29052
  const { stdout } = useStdout2();
28587
- const [size, setSize] = useState3({
29053
+ const [size, setSize] = useState5({
28588
29054
  columns: stdout?.columns ?? defaults.columns,
28589
29055
  rows: stdout?.rows ?? defaults.rows
28590
29056
  });
28591
- useEffect3(() => {
29057
+ useEffect5(() => {
28592
29058
  if (!stdout) return;
28593
29059
  setSize({
28594
29060
  columns: stdout.columns ?? defaults.columns,
@@ -28630,15 +29096,28 @@ var providerDefaults = {
28630
29096
  "glm": "glm-4.5"
28631
29097
  };
28632
29098
  function App() {
28633
- const [input, setInput] = useState4("");
28634
- const [busy, setBusy] = useState4(false);
28635
- const [providerConfig, setProviderConfig] = useState4(() => getProviderConfig());
28636
- const [sessionStats, setSessionStats] = useState4({ totalTokens: 0, totalCostUsd: 0 });
28637
- const [clearEpoch, setClearEpoch] = useState4(0);
29099
+ const [input, setInput] = useState6("");
29100
+ const [busy, setBusy] = useState6(false);
29101
+ const [providerConfig, setProviderConfig] = useState6(() => getProviderConfig());
29102
+ const [sessionStats, setSessionStats] = useState6({ totalTokens: 0, totalCostUsd: 0 });
29103
+ const [clearEpoch, setClearEpoch] = useState6(0);
29104
+ const [mode, setMode] = useState6("agent");
28638
29105
  const activeProviderSpec = getActiveProvider();
28639
29106
  const activeModel = providerConfig.modelByProvider[activeProviderSpec.id];
28640
29107
  const session = useSession();
28641
29108
  const size = useTerminalSize();
29109
+ const gitChanges = useGitChanges();
29110
+ const cwd = useMemo(() => shortenCwd(process.cwd(), 32), []);
29111
+ const timer = useExecutionTimer(busy);
29112
+ const { isRawModeSupported } = useStdin();
29113
+ useInput(
29114
+ (_input, key) => {
29115
+ if (key.tab && key.shift) {
29116
+ setMode((m) => m === "agent" ? "council" : "agent");
29117
+ }
29118
+ },
29119
+ { isActive: isRawModeSupported === true }
29120
+ );
28642
29121
  const { commit: commitLive, flush: flushLive } = useBatchedMessages(
28643
29122
  session.live,
28644
29123
  session.setLive
@@ -28684,51 +29163,52 @@ function App() {
28684
29163
  setQueueCount: chatTurn.setQueueCount,
28685
29164
  dispatchPrompt: chatTurn.dispatchPrompt,
28686
29165
  dispatchCouncilPrompt: chatTurn.dispatchCouncilPrompt,
29166
+ mode,
28687
29167
  onNewSession,
28688
29168
  onExit,
28689
29169
  onClear
28690
29170
  });
28691
- const skills = useMemo(() => listCodingSkills(), []);
28692
29171
  const banner = useMemo(() => {
28693
- const skillList = formatSkillList(skills);
28694
29172
  return {
28695
29173
  id: "banner-once",
28696
29174
  role: "system",
28697
29175
  ts: 0,
28698
29176
  content: `zelari-code v${VERSION} \u2014 ${activeProviderSpec.id}/${activeModel}
28699
- ${skills.length} skills available. Type /help for the list, or /skill <name>.
28700
- \u2500\u2500 skills \u2500\u2500
28701
- ${skillList}`
29177
+ cwd: ${cwd}
29178
+ /help for commands \xB7 /skill <name> \xB7 shift+tab toggles agent/council`
28702
29179
  };
28703
- }, [skills, activeProviderSpec.id, activeModel]);
29180
+ }, [activeProviderSpec.id, activeModel, cwd]);
28704
29181
  const staticKey = `${session.sessionId || "pre-bootstrap"}-${clearEpoch}`;
28705
- const staticItems = [banner, ...session.messages];
28706
- return /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Static, { key: staticKey, items: staticItems }, (item) => renderMessage(item)), /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", borderTop: true, paddingX: 1 }, /* @__PURE__ */ React6.createElement(LiveRegion, { live: session.live, busy }), /* @__PURE__ */ React6.createElement(
29182
+ const staticItems = session.sessionId ? [banner, ...session.messages] : [];
29183
+ const showSidebar = shouldShowSidebar(size.columns, size.rows);
29184
+ return /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement(Static, { key: staticKey, items: staticItems }, (item) => renderMessage(item)), /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "row" }, /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", flexGrow: 1, paddingX: 1 }, /* @__PURE__ */ React7.createElement(LiveRegion, { live: session.live, busy }), /* @__PURE__ */ React7.createElement(
29185
+ InputBar,
29186
+ {
29187
+ value: input,
29188
+ onChange: setInput,
29189
+ onSubmit: handleSubmit,
29190
+ disabled: busy
29191
+ }
29192
+ ), /* @__PURE__ */ React7.createElement(
28707
29193
  StatusBar,
28708
29194
  {
28709
29195
  model: activeModel,
28710
29196
  provider: activeProviderSpec.id,
28711
29197
  sessionId: session.sessionId ? session.sessionId.slice(0, 8) : "...",
28712
29198
  sessionActive: session.sessionActive,
28713
- totalTokens: sessionStats.totalTokens,
28714
- totalCostUsd: sessionStats.totalCostUsd,
28715
29199
  queueCount: chatTurn.queueCount,
28716
- busy
28717
- }
28718
- ), /* @__PURE__ */ React6.createElement(
28719
- InputBar,
28720
- {
28721
- value: input,
28722
- onChange: setInput,
28723
- onSubmit: handleSubmit,
28724
- disabled: busy
29200
+ busy,
29201
+ mode,
29202
+ cwd,
29203
+ elapsedMs: timer.elapsedMs,
29204
+ lastMs: timer.lastMs
28725
29205
  }
28726
- )));
29206
+ )), showSidebar && /* @__PURE__ */ React7.createElement(Sidebar, { version: VERSION, changes: gitChanges, rows: size.rows })));
28727
29207
  }
28728
29208
 
28729
29209
  // src/cli/components/SplashScreen.tsx
28730
- import React7, { useEffect as useEffect4, useState as useState5 } from "react";
28731
- import { Box as Box7, Text as Text7, useInput, useStdin } from "ink";
29210
+ import React8, { useEffect as useEffect6, useState as useState7 } from "react";
29211
+ import { Box as Box8, Text as Text8, useInput as useInput2, useStdin as useStdin2 } from "ink";
28732
29212
  var SPLASH_DURATION_MS = 2e3;
28733
29213
  var EMBLEM_LARGE = ` =%@*-
28734
29214
  =%@@@@@#:
@@ -28790,7 +29270,37 @@ var EMBLEM_SMALL = ` =%%*:
28790
29270
  -@@@%%%%%%%@@@@@@@@@@#%@@@- -.:*@@@@@@@@@*
28791
29271
  %@@@@%@@%@@@@@@@@@@@@@@@@@%**%@@@@@@@@@@@@:
28792
29272
  =@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*`;
28793
- var FOOTER_ROWS = 5;
29273
+ var EMBLEM_TINY = ` -%%+
29274
+ .*%%%@#:
29275
+ =%%%%%%%@*
29276
+ +%%%%%%%%%%#.
29277
+ +%%%@@@@@@@%@#.
29278
+ .*%@@@@@@@@@@@@@%-
29279
+ .#%@@@@@@@@@@@@@@@@-
29280
+ *%@@@@@@@@@@@@@@@@@%.
29281
+ :@%%@@@@@@%:+%@@@@@%@=
29282
+ =@%%@@@@@%.=-+%@%%@*
29283
+ .=@@%@@@@%.*@*-+@@*.
29284
+ -*%@@@@@@@@%.*@@@+:#@@#=.
29285
+ *%%%%%%@@@@@@.*#%#=-:+@@@%
29286
+ :@%%%%%%@@@@@@.:=.*:*@@@@@@=
29287
+ *@%%@%%@@@@@@@*%@*:-:%@@@@@%.
29288
+ :@@@%@@@@@@@@@@@@@@#%@@@@@@@@=
29289
+ *%%%%%%%%%%%%%%%%%%%%%%%%%%%%#`;
29290
+ var EMBLEM_MICRO = ` -#%=
29291
+ +%%%%#.
29292
+ :#%%%%%%%-
29293
+ :%%@@@@@@@@=
29294
+ -%@@@@@@@@@@@+
29295
+ :%@@@@@@%@@@@@@=
29296
+ -%%@@@@@.+%@@%@+
29297
+ -@@@@@@:%++%@=
29298
+ =*%@@@@@@:@@%=*@#+.
29299
+ =%%%%%@@@@:+++-*%@@*
29300
+ .%%%%%@@@@@+%+=:%@@@@:
29301
+ *@@@@@@@@@@@@@%@@@@@@*`;
29302
+ var FOOTER_ROWS_FULL = 5;
29303
+ var FOOTER_ROWS_COMPACT = 2;
28794
29304
  function measure(art) {
28795
29305
  const lines = art.split("\n");
28796
29306
  return {
@@ -28799,10 +29309,17 @@ function measure(art) {
28799
29309
  height: lines.length
28800
29310
  };
28801
29311
  }
28802
- var VARIANTS = [measure(EMBLEM_LARGE), measure(EMBLEM_SMALL)];
29312
+ var VARIANTS = [
29313
+ measure(EMBLEM_LARGE),
29314
+ measure(EMBLEM_SMALL),
29315
+ measure(EMBLEM_TINY),
29316
+ measure(EMBLEM_MICRO)
29317
+ ];
28803
29318
  function pickSplashArt(columns, rows) {
28804
29319
  for (const v of VARIANTS) {
28805
- if (v.width <= columns - 2 && v.height + FOOTER_ROWS <= rows) return v;
29320
+ if (v.width > columns - 2) continue;
29321
+ if (v.height + FOOTER_ROWS_FULL <= rows) return { ...v, compact: false };
29322
+ if (v.height + FOOTER_ROWS_COMPACT <= rows) return { ...v, compact: true };
28806
29323
  }
28807
29324
  return null;
28808
29325
  }
@@ -28812,23 +29329,23 @@ function shouldShowSplash(opts) {
28812
29329
  return pickSplashArt(opts.columns, opts.rows) !== null;
28813
29330
  }
28814
29331
  function Splash({ onDone, version: version2 }) {
28815
- const { isRawModeSupported } = useStdin();
29332
+ const { isRawModeSupported } = useStdin2();
28816
29333
  const columns = process.stdout.columns ?? 80;
28817
29334
  const rows = process.stdout.rows ?? 24;
28818
29335
  const picked = pickSplashArt(columns, rows);
28819
- useEffect4(() => {
29336
+ useEffect6(() => {
28820
29337
  const t = setTimeout(onDone, SPLASH_DURATION_MS);
28821
29338
  return () => clearTimeout(t);
28822
29339
  }, [onDone]);
28823
- useInput(
29340
+ useInput2(
28824
29341
  () => {
28825
29342
  onDone();
28826
29343
  },
28827
29344
  { isActive: isRawModeSupported === true }
28828
29345
  );
28829
29346
  if (!picked) return null;
28830
- return /* @__PURE__ */ React7.createElement(
28831
- Box7,
29347
+ return /* @__PURE__ */ React8.createElement(
29348
+ Box8,
28832
29349
  {
28833
29350
  flexDirection: "column",
28834
29351
  alignItems: "center",
@@ -28836,17 +29353,17 @@ function Splash({ onDone, version: version2 }) {
28836
29353
  width: columns,
28837
29354
  height: rows - 1
28838
29355
  },
28839
- /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, picked.art),
28840
- /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "white" }, "Z E L A R I C O D E")),
28841
- /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, `${version2 ? `v${version2} \u2014 ` : ""}N-THEM Studio`),
28842
- /* @__PURE__ */ React7.createElement(Text7, { dimColor: true, italic: true }, "press any key to skip")
29356
+ /* @__PURE__ */ React8.createElement(Text8, { color: "cyan" }, picked.art),
29357
+ /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, { bold: true, color: "white" }, "Z E L A R I C O D E")),
29358
+ !picked.compact && /* @__PURE__ */ React8.createElement(Text8, { dimColor: true }, `${version2 ? `v${version2} \u2014 ` : ""}N-THEM Studio`),
29359
+ !picked.compact && /* @__PURE__ */ React8.createElement(Text8, { dimColor: true, italic: true }, "press any key to skip")
28843
29360
  );
28844
29361
  }
28845
29362
  function SplashGate({
28846
29363
  children,
28847
29364
  version: version2
28848
29365
  }) {
28849
- const [show, setShow] = useState5(
29366
+ const [show, setShow] = useState7(
28850
29367
  () => shouldShowSplash({
28851
29368
  isTTY: process.stdout.isTTY === true,
28852
29369
  env: process.env,
@@ -28855,18 +29372,18 @@ function SplashGate({
28855
29372
  })
28856
29373
  );
28857
29374
  if (show) {
28858
- return /* @__PURE__ */ React7.createElement(Splash, { onDone: () => setShow(false), version: version2 });
29375
+ return /* @__PURE__ */ React8.createElement(Splash, { onDone: () => setShow(false), version: version2 });
28859
29376
  }
28860
- return /* @__PURE__ */ React7.createElement(React7.Fragment, null, children);
29377
+ return /* @__PURE__ */ React8.createElement(React8.Fragment, null, children);
28861
29378
  }
28862
29379
 
28863
29380
  // src/cli/main.ts
28864
29381
  init_providerConfig();
28865
29382
 
28866
29383
  // src/cli/wizard/firstRun.ts
28867
- import { existsSync as existsSync17 } from "node:fs";
29384
+ import { existsSync as existsSync18 } from "node:fs";
28868
29385
  function shouldRunWizard(input) {
28869
- const exists = input.exists ?? existsSync17;
29386
+ const exists = input.exists ?? existsSync18;
28870
29387
  if (input.hasResetConfigFlag) {
28871
29388
  return { shouldRun: true, reason: "--reset-config flag forced wizard" };
28872
29389
  }
@@ -28901,15 +29418,15 @@ function parseWizardFlags(argv) {
28901
29418
  }
28902
29419
 
28903
29420
  // src/cli/wizard/runWizard.tsx
28904
- import React9, { useEffect as useEffect5, useState as useState6 } from "react";
28905
- import { Box as Box9, Text as Text9 } from "ink";
28906
- import { useInput as useInput2 } from "ink";
29421
+ import React10, { useEffect as useEffect7, useState as useState8 } from "react";
29422
+ import { Box as Box10, Text as Text10 } from "ink";
29423
+ import { useInput as useInput3 } from "ink";
28907
29424
  init_keyStore();
28908
29425
  init_providerConfig();
28909
29426
 
28910
29427
  // src/cli/wizard/index.tsx
28911
- import React8 from "react";
28912
- import { Box as Box8, Text as Text8 } from "ink";
29428
+ import React9 from "react";
29429
+ import { Box as Box9, Text as Text9 } from "ink";
28913
29430
 
28914
29431
  // src/cli/wizard/useWizardState.ts
28915
29432
  init_providerConfig();
@@ -29025,16 +29542,16 @@ var API_KEY_OPTIONS = ["env", "keystore", "skip"];
29025
29542
 
29026
29543
  // src/cli/wizard/index.tsx
29027
29544
  function Frame({ children }) {
29028
- return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 1 }, children);
29545
+ return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 1 }, children);
29029
29546
  }
29030
29547
  function Step(props) {
29031
- return /* @__PURE__ */ React8.createElement(Box8, null, /* @__PURE__ */ React8.createElement(Text8, { color: props.active ? "cyan" : "gray", inverse: props.active }, " ", props.index, "/", props.total, " ", props.name, " "));
29548
+ return /* @__PURE__ */ React9.createElement(Box9, null, /* @__PURE__ */ React9.createElement(Text9, { color: props.active ? "cyan" : "gray", inverse: props.active }, " ", props.index, "/", props.total, " ", props.name, " "));
29032
29549
  }
29033
29550
  function renderProviderList(providers, cursor) {
29034
- return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column" }, providers.map((p3, i) => {
29551
+ return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column" }, providers.map((p3, i) => {
29035
29552
  const arrow = i === cursor ? "\u279C " : " ";
29036
29553
  const color = i === cursor ? "cyan" : void 0;
29037
- return /* @__PURE__ */ React8.createElement(Text8, { key: p3.id, color }, arrow, p3.displayName, " ", /* @__PURE__ */ React8.createElement(Text8, { color: "gray" }, "(", p3.id, ", uses env ", p3.envVar, ")"));
29554
+ return /* @__PURE__ */ React9.createElement(Text9, { key: p3.id, color }, arrow, p3.displayName, " ", /* @__PURE__ */ React9.createElement(Text9, { color: "gray" }, "(", p3.id, ", uses env ", p3.envVar, ")"));
29038
29555
  }));
29039
29556
  }
29040
29557
  function renderApiKeyOptions(cursor) {
@@ -29043,37 +29560,37 @@ function renderApiKeyOptions(cursor) {
29043
29560
  keystore: "Save to local keyStore (encrypted)",
29044
29561
  skip: "Skip for now (chat will fail until key is set)"
29045
29562
  };
29046
- return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column" }, API_KEY_OPTIONS.map((choice, i) => {
29563
+ return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column" }, API_KEY_OPTIONS.map((choice, i) => {
29047
29564
  const arrow = i === cursor ? "\u279C " : " ";
29048
29565
  const color = i === cursor ? "cyan" : void 0;
29049
- return /* @__PURE__ */ React8.createElement(Text8, { key: choice, color }, arrow, labels[choice]);
29566
+ return /* @__PURE__ */ React9.createElement(Text9, { key: choice, color }, arrow, labels[choice]);
29050
29567
  }));
29051
29568
  }
29052
29569
  function Wizard({ state: state2, providers }) {
29053
29570
  const s = state2.state;
29054
29571
  if (s.committed) {
29055
- return /* @__PURE__ */ React8.createElement(Frame, null, /* @__PURE__ */ React8.createElement(Text8, { color: "green" }, "\u2713 Setup complete!"), /* @__PURE__ */ React8.createElement(Text8, null, "Provider: ", /* @__PURE__ */ React8.createElement(Text8, { color: "cyan" }, s.providerId), " | ", "Model: ", /* @__PURE__ */ React8.createElement(Text8, { color: "cyan" }, s.model), " | ", "API key: ", /* @__PURE__ */ React8.createElement(Text8, { color: "cyan" }, s.apiKeyChoice ?? "n/a")), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, { color: "gray" }, "Launching zelari-code\u2026 (press any key)")));
29572
+ return /* @__PURE__ */ React9.createElement(Frame, null, /* @__PURE__ */ React9.createElement(Text9, { color: "green" }, "\u2713 Setup complete!"), /* @__PURE__ */ React9.createElement(Text9, null, "Provider: ", /* @__PURE__ */ React9.createElement(Text9, { color: "cyan" }, s.providerId), " | ", "Model: ", /* @__PURE__ */ React9.createElement(Text9, { color: "cyan" }, s.model), " | ", "API key: ", /* @__PURE__ */ React9.createElement(Text9, { color: "cyan" }, s.apiKeyChoice ?? "n/a")), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, { color: "gray" }, "Launching zelari-code\u2026 (press any key)")));
29056
29573
  }
29057
- return /* @__PURE__ */ React8.createElement(Frame, null, /* @__PURE__ */ React8.createElement(Text8, { color: "cyan", bold: true }, "zelari-code v", VERSION, " \u2014 first-time setup"), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1, marginBottom: 1, flexDirection: "row" }, /* @__PURE__ */ React8.createElement(Step, { index: 1, total: 5, name: "welcome", active: s.step === "welcome" }), /* @__PURE__ */ React8.createElement(Step, { index: 2, total: 5, name: "provider", active: s.step === "provider" }), /* @__PURE__ */ React8.createElement(Step, { index: 3, total: 5, name: "model", active: s.step === "model" }), /* @__PURE__ */ React8.createElement(Step, { index: 4, total: 5, name: "apikey", active: s.step === "apikey" }), /* @__PURE__ */ React8.createElement(Step, { index: 5, total: 5, name: "confirm", active: s.step === "confirm" })), s.step === "welcome" && /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Text8, null, "Welcome! Let's get you coding in under two minutes."), /* @__PURE__ */ React8.createElement(Text8, { color: "gray" }, "We'll pick a provider, default model, and how to handle your API key."), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, null, "Press "), /* @__PURE__ */ React8.createElement(Text8, { color: "cyan", inverse: true }, " Enter "), /* @__PURE__ */ React8.createElement(Text8, null, " to continue, or "), /* @__PURE__ */ React8.createElement(Text8, { color: "red", inverse: true }, " Q "), /* @__PURE__ */ React8.createElement(Text8, null, " to quit (re-run with "), /* @__PURE__ */ React8.createElement(Text8, { color: "gray" }, "--no-wizard"), /* @__PURE__ */ React8.createElement(Text8, null, " to skip later)."))), s.step === "provider" && /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Text8, null, "Choose your LLM provider:"), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, renderProviderList(providers, s.providerCursor)), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, { color: "gray" }, "\u2191/\u2193 to move, Enter to confirm"))), s.step === "model" && /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Text8, null, "Model for ", /* @__PURE__ */ React8.createElement(Text8, { color: "cyan" }, s.providerId), ":"), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1, borderStyle: "single", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React8.createElement(Text8, null, s.model ?? "(empty)")), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, null, "Press ", /* @__PURE__ */ React8.createElement(Text8, { color: "cyan", inverse: true }, " Enter "), "to accept, or type a new name. ", /* @__PURE__ */ React8.createElement(Text8, { color: "gray" }, "(default kept on empty input)")))), s.step === "apikey" && /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Text8, null, "How should we handle the API key?"), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, renderApiKeyOptions(s.apiKeyCursor)), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, { color: "gray" }, "\u2191/\u2193 to move, Enter to confirm"))), s.step === "confirm" && /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Text8, null, "Confirm your setup:"), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React8.createElement(Text8, null, "Provider: ", /* @__PURE__ */ React8.createElement(Text8, { color: "cyan" }, s.providerId)), /* @__PURE__ */ React8.createElement(Text8, null, "Model: ", /* @__PURE__ */ React8.createElement(Text8, { color: "cyan" }, s.model)), /* @__PURE__ */ React8.createElement(Text8, null, "API key: ", /* @__PURE__ */ React8.createElement(Text8, { color: "cyan" }, s.apiKeyChoice ?? "(unset)"))), /* @__PURE__ */ React8.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text8, null, "Press ", /* @__PURE__ */ React8.createElement(Text8, { color: "green", inverse: true }, " Enter "), "to save and launch, or ", /* @__PURE__ */ React8.createElement(Text8, { color: "yellow", inverse: true }, " B "), "to go back."))));
29574
+ return /* @__PURE__ */ React9.createElement(Frame, null, /* @__PURE__ */ React9.createElement(Text9, { color: "cyan", bold: true }, "zelari-code v", VERSION, " \u2014 first-time setup"), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1, marginBottom: 1, flexDirection: "row" }, /* @__PURE__ */ React9.createElement(Step, { index: 1, total: 5, name: "welcome", active: s.step === "welcome" }), /* @__PURE__ */ React9.createElement(Step, { index: 2, total: 5, name: "provider", active: s.step === "provider" }), /* @__PURE__ */ React9.createElement(Step, { index: 3, total: 5, name: "model", active: s.step === "model" }), /* @__PURE__ */ React9.createElement(Step, { index: 4, total: 5, name: "apikey", active: s.step === "apikey" }), /* @__PURE__ */ React9.createElement(Step, { index: 5, total: 5, name: "confirm", active: s.step === "confirm" })), s.step === "welcome" && /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement(Text9, null, "Welcome! Let's get you coding in under two minutes."), /* @__PURE__ */ React9.createElement(Text9, { color: "gray" }, "We'll pick a provider, default model, and how to handle your API key."), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, null, "Press "), /* @__PURE__ */ React9.createElement(Text9, { color: "cyan", inverse: true }, " Enter "), /* @__PURE__ */ React9.createElement(Text9, null, " to continue, or "), /* @__PURE__ */ React9.createElement(Text9, { color: "red", inverse: true }, " Q "), /* @__PURE__ */ React9.createElement(Text9, null, " to quit (re-run with "), /* @__PURE__ */ React9.createElement(Text9, { color: "gray" }, "--no-wizard"), /* @__PURE__ */ React9.createElement(Text9, null, " to skip later)."))), s.step === "provider" && /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement(Text9, null, "Choose your LLM provider:"), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, renderProviderList(providers, s.providerCursor)), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, { color: "gray" }, "\u2191/\u2193 to move, Enter to confirm"))), s.step === "model" && /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement(Text9, null, "Model for ", /* @__PURE__ */ React9.createElement(Text9, { color: "cyan" }, s.providerId), ":"), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1, borderStyle: "single", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React9.createElement(Text9, null, s.model ?? "(empty)")), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, null, "Press ", /* @__PURE__ */ React9.createElement(Text9, { color: "cyan", inverse: true }, " Enter "), "to accept, or type a new name. ", /* @__PURE__ */ React9.createElement(Text9, { color: "gray" }, "(default kept on empty input)")))), s.step === "apikey" && /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement(Text9, null, "How should we handle the API key?"), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, renderApiKeyOptions(s.apiKeyCursor)), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, { color: "gray" }, "\u2191/\u2193 to move, Enter to confirm"))), s.step === "confirm" && /* @__PURE__ */ React9.createElement(React9.Fragment, null, /* @__PURE__ */ React9.createElement(Text9, null, "Confirm your setup:"), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React9.createElement(Text9, null, "Provider: ", /* @__PURE__ */ React9.createElement(Text9, { color: "cyan" }, s.providerId)), /* @__PURE__ */ React9.createElement(Text9, null, "Model: ", /* @__PURE__ */ React9.createElement(Text9, { color: "cyan" }, s.model)), /* @__PURE__ */ React9.createElement(Text9, null, "API key: ", /* @__PURE__ */ React9.createElement(Text9, { color: "cyan" }, s.apiKeyChoice ?? "(unset)"))), /* @__PURE__ */ React9.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text9, null, "Press ", /* @__PURE__ */ React9.createElement(Text9, { color: "green", inverse: true }, " Enter "), "to save and launch, or ", /* @__PURE__ */ React9.createElement(Text9, { color: "yellow", inverse: true }, " B "), "to go back."))));
29058
29575
  }
29059
29576
 
29060
29577
  // src/cli/wizard/runWizard.tsx
29061
29578
  function RunWizard(_props) {
29062
- const [wiz] = useState6(
29579
+ const [wiz] = useState8(
29063
29580
  () => createWizardState({
29064
29581
  providers: PROVIDERS,
29065
29582
  defaultModelFor: (id) => getModelForProvider(id)
29066
29583
  })
29067
29584
  );
29068
- const [, force] = useState6(0);
29069
- useEffect5(() => {
29585
+ const [, force] = useState8(0);
29586
+ useEffect7(() => {
29070
29587
  if (typeof wiz.subscribe === "function") {
29071
29588
  const sub = wiz.subscribe(() => force((n) => n + 1));
29072
29589
  return () => sub();
29073
29590
  }
29074
29591
  return void 0;
29075
29592
  }, [wiz]);
29076
- useInput2((input, key) => {
29593
+ useInput3((input, key) => {
29077
29594
  if (wiz.state.committed) return;
29078
29595
  const s = wiz.state;
29079
29596
  if (key.escape) {
@@ -29130,20 +29647,20 @@ function RunWizard(_props) {
29130
29647
  }
29131
29648
  });
29132
29649
  if (wiz.state.committed) {
29133
- return /* @__PURE__ */ React9.createElement(PostCommitBridge, null);
29650
+ return /* @__PURE__ */ React10.createElement(PostCommitBridge, null);
29134
29651
  }
29135
- return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column", paddingX: 2, paddingY: 1 }, /* @__PURE__ */ React9.createElement(Wizard, { state: wiz, providers: PROVIDERS }));
29652
+ return /* @__PURE__ */ React10.createElement(Box10, { flexDirection: "column", paddingX: 2, paddingY: 1 }, /* @__PURE__ */ React10.createElement(Wizard, { state: wiz, providers: PROVIDERS }));
29136
29653
  }
29137
29654
  function PostCommitBridge() {
29138
- const [showApp, setShowApp] = useState6(false);
29139
- useEffect5(() => {
29655
+ const [showApp, setShowApp] = useState8(false);
29656
+ useEffect7(() => {
29140
29657
  const t = setTimeout(() => setShowApp(true), 1200);
29141
29658
  return () => clearTimeout(t);
29142
29659
  }, []);
29143
29660
  if (showApp) {
29144
- return /* @__PURE__ */ React9.createElement(App, null);
29661
+ return /* @__PURE__ */ React10.createElement(App, null);
29145
29662
  }
29146
- return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column", paddingX: 2, paddingY: 1 }, /* @__PURE__ */ React9.createElement(Box9, { borderStyle: "round", borderColor: "green", paddingX: 2, paddingY: 1, flexDirection: "column" }, /* @__PURE__ */ React9.createElement(Text9, { color: "green", bold: true }, "\u2713 Setup complete! Launching zelari-code\u2026"), /* @__PURE__ */ React9.createElement(Text9, { color: "gray" }, "Press Ctrl+C any time to exit.")));
29663
+ return /* @__PURE__ */ React10.createElement(Box10, { flexDirection: "column", paddingX: 2, paddingY: 1 }, /* @__PURE__ */ React10.createElement(Box10, { borderStyle: "round", borderColor: "green", paddingX: 2, paddingY: 1, flexDirection: "column" }, /* @__PURE__ */ React10.createElement(Text10, { color: "green", bold: true }, "\u2713 Setup complete! Launching zelari-code\u2026"), /* @__PURE__ */ React10.createElement(Text10, { color: "gray" }, "Press Ctrl+C any time to exit.")));
29147
29664
  }
29148
29665
 
29149
29666
  // src/cli/headless.ts
@@ -29355,9 +29872,9 @@ async function runHeadlessCouncil(opts, provider, model, providerStream) {
29355
29872
 
29356
29873
  // src/cli/skillsMd.ts
29357
29874
  init_skills2();
29358
- import { existsSync as existsSync18, readdirSync as readdirSync4, readFileSync as readFileSync13 } from "node:fs";
29359
- import { join as join8 } from "node:path";
29360
- import { homedir as homedir4 } from "node:os";
29875
+ import { existsSync as existsSync19, readdirSync as readdirSync4, readFileSync as readFileSync14 } from "node:fs";
29876
+ import { join as join9 } from "node:path";
29877
+ import { homedir as homedir5 } from "node:os";
29361
29878
  var CODING_CATEGORIES = /* @__PURE__ */ new Set([
29362
29879
  "plan",
29363
29880
  "refactor",
@@ -29372,10 +29889,10 @@ var CODING_CATEGORIES = /* @__PURE__ */ new Set([
29372
29889
  ]);
29373
29890
  function skillMdSearchDirs(projectRoot = process.cwd()) {
29374
29891
  return [
29375
- join8(projectRoot, ".zelari", "skills"),
29376
- join8(projectRoot, ".claude", "skills"),
29377
- join8(projectRoot, ".opencode", "skills"),
29378
- join8(homedir4(), ".zelari-code", "skills")
29892
+ join9(projectRoot, ".zelari", "skills"),
29893
+ join9(projectRoot, ".claude", "skills"),
29894
+ join9(projectRoot, ".opencode", "skills"),
29895
+ join9(homedir5(), ".zelari-code", "skills")
29379
29896
  ];
29380
29897
  }
29381
29898
  function parseSkillMd(content, sourcePath) {
@@ -29433,7 +29950,7 @@ function loadSkillMdSkills(projectRoot = process.cwd(), options = {}) {
29433
29950
  const summary = { loaded: [], skipped: [] };
29434
29951
  const seen = new Set(options.existingIds ?? []);
29435
29952
  for (const dir of skillMdSearchDirs(projectRoot)) {
29436
- if (!existsSync18(dir)) continue;
29953
+ if (!existsSync19(dir)) continue;
29437
29954
  let entries;
29438
29955
  try {
29439
29956
  entries = readdirSync4(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
@@ -29441,10 +29958,10 @@ function loadSkillMdSkills(projectRoot = process.cwd(), options = {}) {
29441
29958
  continue;
29442
29959
  }
29443
29960
  for (const entry of entries) {
29444
- const skillPath = join8(dir, entry, "SKILL.md");
29445
- if (!existsSync18(skillPath)) continue;
29961
+ const skillPath = join9(dir, entry, "SKILL.md");
29962
+ if (!existsSync19(skillPath)) continue;
29446
29963
  try {
29447
- const parsed = parseSkillMd(readFileSync13(skillPath, "utf8"), skillPath);
29964
+ const parsed = parseSkillMd(readFileSync14(skillPath, "utf8"), skillPath);
29448
29965
  if (!parsed) {
29449
29966
  summary.skipped.push({ path: skillPath, reason: "missing/invalid frontmatter (name, description) or empty body" });
29450
29967
  continue;
@@ -29466,7 +29983,7 @@ function loadSkillMdSkills(projectRoot = process.cwd(), options = {}) {
29466
29983
 
29467
29984
  // src/cli/main.ts
29468
29985
  init_skills2();
29469
- var VERSION = "0.7.8";
29986
+ var VERSION = "0.7.9";
29470
29987
  async function backgroundUpdateCheck() {
29471
29988
  if (process.env.ANATHEMA_DEV === "1") return;
29472
29989
  await new Promise((resolve) => setTimeout(resolve, 3e3));
@@ -29522,11 +30039,11 @@ function pickRootComponent() {
29522
30039
  });
29523
30040
  if (decision.shouldRun) {
29524
30041
  console.error(`[zelari-code] starting wizard: ${decision.reason}`);
29525
- return { kind: "wizard", element: React10.createElement(RunWizard) };
30042
+ return { kind: "wizard", element: React11.createElement(RunWizard) };
29526
30043
  }
29527
30044
  return {
29528
30045
  kind: "app",
29529
- element: React10.createElement(SplashGate, { version: VERSION }, React10.createElement(App))
30046
+ element: React11.createElement(SplashGate, { version: VERSION }, React11.createElement(App))
29530
30047
  };
29531
30048
  }
29532
30049
  function loadUserSkills() {