zelari-code 0.7.5 → 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 (44) hide show
  1. package/README.md +27 -1
  2. package/dist/cli/app.js +50 -18
  3. package/dist/cli/app.js.map +1 -1
  4. package/dist/cli/components/Sidebar.js +86 -39
  5. package/dist/cli/components/Sidebar.js.map +1 -1
  6. package/dist/cli/components/SplashScreen.js +204 -0
  7. package/dist/cli/components/SplashScreen.js.map +1 -0
  8. package/dist/cli/components/StatusBar.js +22 -18
  9. package/dist/cli/components/StatusBar.js.map +1 -1
  10. package/dist/cli/councilConfig.js +38 -0
  11. package/dist/cli/councilConfig.js.map +1 -0
  12. package/dist/cli/councilDispatcher.js +38 -32
  13. package/dist/cli/councilDispatcher.js.map +1 -1
  14. package/dist/cli/hooks/useChatTurn.js +80 -61
  15. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  16. package/dist/cli/hooks/useExecutionTimer.js +25 -0
  17. package/dist/cli/hooks/useExecutionTimer.js.map +1 -0
  18. package/dist/cli/hooks/useGitChanges.js +142 -0
  19. package/dist/cli/hooks/useGitChanges.js.map +1 -0
  20. package/dist/cli/hooks/useSlashDispatch.js +9 -2
  21. package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
  22. package/dist/cli/main.bundled.js +2560 -1231
  23. package/dist/cli/main.bundled.js.map +4 -4
  24. package/dist/cli/main.js +9 -2
  25. package/dist/cli/main.js.map +1 -1
  26. package/dist/cli/mcp/mcpClient.js +14 -6
  27. package/dist/cli/mcp/mcpClient.js.map +1 -1
  28. package/dist/cli/updater.js +8 -5
  29. package/dist/cli/updater.js.map +1 -1
  30. package/dist/cli/utils/cmdline.js +26 -0
  31. package/dist/cli/utils/cmdline.js.map +1 -0
  32. package/dist/cli/utils/paths.js +20 -0
  33. package/dist/cli/utils/paths.js.map +1 -0
  34. package/dist/cli/wizard/index.js +1 -1
  35. package/dist/cli/wizard/index.js.map +1 -1
  36. package/dist/cli/workspace/completeDesign.js +128 -0
  37. package/dist/cli/workspace/completeDesign.js.map +1 -0
  38. package/dist/cli/workspace/planDetect.js +17 -0
  39. package/dist/cli/workspace/planDetect.js.map +1 -0
  40. package/dist/cli/workspace/postCouncilHook.js +157 -20
  41. package/dist/cli/workspace/postCouncilHook.js.map +1 -1
  42. package/dist/cli/workspace/stubs.js +406 -225
  43. package/dist/cli/workspace/stubs.js.map +1 -1
  44. package/package.json +71 -71
@@ -141,12 +141,13 @@ var init_skills = __esm({
141
141
  color: "#10b981",
142
142
  enabledByDefault: true,
143
143
  builtin: true,
144
- requiredTools: ["createTask", "createPhase", "createMilestone", "updateTask"],
144
+ requiredTools: ["createPlan", "createTask", "createPhase", "createMilestone", "updateTask"],
145
145
  systemPromptFragment: `You plan projects hierarchically.
146
146
  - Decompose work into phases, then tasks within phases.
147
147
  - For each task include: title, description, priority (low|medium|high|critical), and optional tags/subtasks.
148
148
  - Define milestones to mark key deliverables.
149
- - When relevant, reference file paths and acceptance criteria.`
149
+ - When relevant, reference file paths and acceptance criteria.
150
+ - Prefer ONE createPlan call (phases with nested tasks + milestone) over many itemized createPhase/createTask calls.`
150
151
  },
151
152
  {
152
153
  id: "idea-synthesizer",
@@ -15864,7 +15865,7 @@ function resolveBashWindows() {
15864
15865
  return p3;
15865
15866
  }
15866
15867
  try {
15867
- const result = spawnSync("where", ["bash"], { shell: true, encoding: "utf-8" });
15868
+ const result = spawnSync("where", ["bash"], { encoding: "utf-8", windowsHide: true });
15868
15869
  if (result.status === 0 && result.stdout) {
15869
15870
  const first = result.stdout.split(/\r?\n/).find((l) => l.trim().length > 0);
15870
15871
  if (first && existsSyncSafe(first))
@@ -17043,6 +17044,61 @@ var init_toolSchemas = __esm({
17043
17044
  "use strict";
17044
17045
  init_tools();
17045
17046
  PARAM_SCHEMAS = {
17047
+ createPlan: {
17048
+ type: "object",
17049
+ properties: {
17050
+ phases: {
17051
+ type: "array",
17052
+ description: "Ordered list of plan phases, each with its tasks nested (aim for 3 tasks per phase)",
17053
+ items: {
17054
+ type: "object",
17055
+ properties: {
17056
+ name: { type: "string", description: "Phase name" },
17057
+ description: { type: "string", description: "What this phase delivers and its exit criterion" },
17058
+ order: { type: "number", description: "Position of the phase in the plan (1-based)" },
17059
+ color: { type: "string", description: 'Hex color, e.g. "#3b82f6"' },
17060
+ tasks: {
17061
+ type: "array",
17062
+ description: "Concrete tasks for this phase",
17063
+ items: {
17064
+ type: "object",
17065
+ properties: {
17066
+ title: { type: "string", description: "Concise verb-led task title" },
17067
+ description: { type: "string", description: "2-3 sentences of context" },
17068
+ fileRefs: {
17069
+ type: "array",
17070
+ items: { type: "string" },
17071
+ description: 'File paths with line ranges, e.g. "src/App.tsx:L10-L40"'
17072
+ },
17073
+ acceptance: {
17074
+ type: "array",
17075
+ items: { type: "string" },
17076
+ description: "Concrete, testable acceptance criteria"
17077
+ },
17078
+ qaScenario: { type: "string", description: "Step-by-step manual QA scenario" },
17079
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] }
17080
+ },
17081
+ required: ["title"]
17082
+ }
17083
+ }
17084
+ },
17085
+ required: ["name"]
17086
+ }
17087
+ },
17088
+ milestone: {
17089
+ type: "object",
17090
+ description: "The milestone this plan ships (e.g. the design-complete milestone)",
17091
+ properties: {
17092
+ title: { type: "string", description: "Milestone title" },
17093
+ description: { type: "string", description: "Milestone description" },
17094
+ targetVersion: { type: "string", description: 'Target version, e.g. "v0.1.0"' },
17095
+ dueDate: { type: "string", description: "Due date (ISO string)" }
17096
+ },
17097
+ required: ["title"]
17098
+ }
17099
+ },
17100
+ required: ["phases"]
17101
+ },
17046
17102
  createTask: {
17047
17103
  type: "object",
17048
17104
  properties: {
@@ -17051,6 +17107,17 @@ var init_toolSchemas = __esm({
17051
17107
  priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
17052
17108
  phaseId: { type: "string", description: "ID of the phase this task belongs to" },
17053
17109
  parentId: { type: "string", description: "ID of the parent task, if nested" },
17110
+ fileRefs: {
17111
+ type: "array",
17112
+ items: { type: "string" },
17113
+ description: 'File paths with line ranges where the work lands, e.g. "src/App.tsx:L10-L40"'
17114
+ },
17115
+ acceptance: {
17116
+ type: "array",
17117
+ items: { type: "string" },
17118
+ description: "Concrete, testable acceptance criteria"
17119
+ },
17120
+ qaScenario: { type: "string", description: "Step-by-step manual QA scenario" },
17054
17121
  tags: { type: "array", items: { type: "string" } },
17055
17122
  subtasks: { type: "array", items: { type: "string" }, description: "Subtask titles" }
17056
17123
  },
@@ -17180,6 +17247,7 @@ var init_toolSchemas = __esm({
17180
17247
  properties: {
17181
17248
  title: { type: "string", description: "Milestone title" },
17182
17249
  description: { type: "string", description: "Milestone description" },
17250
+ targetVersion: { type: "string", description: 'Target version this milestone ships, e.g. "v0.1.0"' },
17183
17251
  dueDate: { type: "string", description: "Due date (ISO string)" }
17184
17252
  },
17185
17253
  required: ["title"]
@@ -17543,7 +17611,7 @@ async function refreshGrokToken(options) {
17543
17611
  return parseTokenResponseBody(obj, accessToken);
17544
17612
  }
17545
17613
  async function openBrowser(url2) {
17546
- const { spawn: spawn4 } = await import("node:child_process");
17614
+ const { spawn: spawn5 } = await import("node:child_process");
17547
17615
  const cmd = (() => {
17548
17616
  switch (process.platform) {
17549
17617
  case "darwin":
@@ -17556,7 +17624,7 @@ async function openBrowser(url2) {
17556
17624
  })();
17557
17625
  return new Promise((resolve, reject) => {
17558
17626
  try {
17559
- const child = spawn4(cmd.bin, cmd.args, {
17627
+ const child = spawn5(cmd.bin, cmd.args, {
17560
17628
  stdio: "ignore",
17561
17629
  detached: true
17562
17630
  });
@@ -18605,12 +18673,12 @@ import {
18605
18673
  realpathSync
18606
18674
  } from "node:fs";
18607
18675
  import { join, basename } from "node:path";
18608
- import { homedir } from "node:os";
18676
+ import { homedir as homedir2 } from "node:os";
18609
18677
  import { createHash } from "node:crypto";
18610
18678
  function resolveWorkspaceRoot(projectRoot = process.cwd()) {
18611
18679
  const candidates = [
18612
18680
  join(projectRoot, ".zelari"),
18613
- join(homedir(), ".zelari-code", "workspace", hashProject(projectRoot))
18681
+ join(homedir2(), ".zelari-code", "workspace", hashProject(projectRoot))
18614
18682
  ];
18615
18683
  for (const candidate of candidates) {
18616
18684
  if (isWritableDir(projectRoot) || candidate !== candidates[0]) {
@@ -19202,9 +19270,17 @@ __export(stubs_exports, {
19202
19270
  createWorkspaceContext: () => createWorkspaceContext,
19203
19271
  createWorkspaceStubs: () => createWorkspaceStubs,
19204
19272
  deriveProjectName: () => projectName,
19273
+ readPlanSummary: () => readPlanSummary,
19205
19274
  resolveWorkspaceRoot: () => resolveWorkspaceRoot
19206
19275
  });
19207
- 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";
19208
19284
  import { join as join4, basename as basename2, dirname as dirname2, relative as relative2 } from "node:path";
19209
19285
  function createWorkspaceContext(projectRoot = process.cwd()) {
19210
19286
  const rootDir = resolveWorkspaceRoot(projectRoot);
@@ -19221,7 +19297,9 @@ function readPlan(ctx) {
19221
19297
  const jsonPath = planJsonPath(ctx);
19222
19298
  if (existsSync9(jsonPath)) {
19223
19299
  try {
19224
- const parsed = JSON.parse(readFileSync6(jsonPath, "utf8"));
19300
+ const parsed = JSON.parse(
19301
+ readFileSync6(jsonPath, "utf8")
19302
+ );
19225
19303
  return {
19226
19304
  phases: Array.isArray(parsed.phases) ? parsed.phases : [],
19227
19305
  tasks: Array.isArray(parsed.tasks) ? parsed.tasks : [],
@@ -19265,8 +19343,12 @@ function renderPlanBody(summary) {
19265
19343
  if (summary.phases.length === 0) {
19266
19344
  lines.push(`_(none yet)_`);
19267
19345
  } else {
19268
- for (const phase of [...summary.phases].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))) {
19269
- 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
+ );
19270
19352
  lines.push("");
19271
19353
  }
19272
19354
  }
@@ -19316,8 +19398,108 @@ function nextAdrId(ctx) {
19316
19398
  function slugify2(s) {
19317
19399
  return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
19318
19400
  }
19401
+ function addPhaseRecord(summary, input) {
19402
+ const id = slugify2(input.name) || `phase-${input.order}`;
19403
+ if (summary.phases.some((p3) => p3.id === id)) {
19404
+ return { id, created: false };
19405
+ }
19406
+ summary.phases.push({
19407
+ kind: "phase",
19408
+ id,
19409
+ name: input.name,
19410
+ description: input.description,
19411
+ order: input.order,
19412
+ color: input.color
19413
+ });
19414
+ return { id, created: true };
19415
+ }
19416
+ function addTaskRecord(ctx, summary, phaseId, t, options) {
19417
+ const slug = slugify2(t.title);
19418
+ if (options.dedupe) {
19419
+ const existing = summary.tasks.find(
19420
+ (k) => k.phaseId === phaseId && k.id.replace(/-\d+$/, "") === `${phaseId}-${slug}`
19421
+ );
19422
+ if (existing) return { id: existing.id, created: false };
19423
+ }
19424
+ const id = `${phaseId}-${slug}-${summary.tasks.filter((k) => k.phaseId === phaseId).length + 1}`;
19425
+ summary.tasks.push({
19426
+ kind: "task",
19427
+ id,
19428
+ // v0.7.3: keep the human-readable title in plan.json so
19429
+ // buildPlanSummary can render it in the system prompt.
19430
+ name: t.title,
19431
+ phaseId,
19432
+ status: "pending",
19433
+ priority: t.priority
19434
+ });
19435
+ const taskPath = join4(ctx.rootDir, "plan-tasks", `${id}.md`);
19436
+ const meta3 = {
19437
+ kind: "task",
19438
+ id,
19439
+ phaseId,
19440
+ status: "pending",
19441
+ priority: t.priority,
19442
+ tags: t.fileRefs
19443
+ };
19444
+ const body = [
19445
+ `# ${t.title}`,
19446
+ "",
19447
+ t.description,
19448
+ "",
19449
+ `## File references`,
19450
+ ...t.fileRefs.map((f) => `- \`${f}\``),
19451
+ "",
19452
+ `## Acceptance criteria`,
19453
+ ...t.acceptance.map((a) => `- ${a}`),
19454
+ "",
19455
+ `## QA scenario`,
19456
+ "",
19457
+ t.qaScenario || "_(not specified)_",
19458
+ ""
19459
+ ].join("\n");
19460
+ ctx.storage.write(taskPath, meta3, body);
19461
+ return { id, created: true };
19462
+ }
19463
+ function addMilestoneRecord(ctx, summary, input) {
19464
+ const id = `m-${slugify2(input.title)}`;
19465
+ if (summary.milestones.some((m) => m.id === id)) {
19466
+ return { id, created: false };
19467
+ }
19468
+ const version2 = input.targetVersion ?? input.dueDate ?? "TBD";
19469
+ summary.milestones.push({
19470
+ kind: "milestone",
19471
+ id,
19472
+ name: input.title,
19473
+ description: input.description,
19474
+ dueDate: input.dueDate,
19475
+ targetVersion: version2
19476
+ });
19477
+ const path22 = join4(ctx.rootDir, "milestones", `${id}.md`);
19478
+ const meta3 = {
19479
+ kind: "milestone",
19480
+ id,
19481
+ name: input.title,
19482
+ description: input.description,
19483
+ dueDate: input.dueDate,
19484
+ targetVersion: version2
19485
+ };
19486
+ const body = [
19487
+ `# Milestone: ${input.title}`,
19488
+ "",
19489
+ input.description,
19490
+ "",
19491
+ `Target version: ${version2}`,
19492
+ ""
19493
+ ].join("\n");
19494
+ ctx.storage.write(path22, meta3, body);
19495
+ return { id, created: true };
19496
+ }
19497
+ function readPlanSummary(ctx) {
19498
+ return readPlan(ctx);
19499
+ }
19319
19500
  function createWorkspaceStubs(ctx) {
19320
19501
  return [
19502
+ createPlanStub(ctx),
19321
19503
  createPhaseStub(ctx),
19322
19504
  createTaskStub(ctx),
19323
19505
  updateTaskStub(ctx),
@@ -19329,6 +19511,67 @@ function createWorkspaceStubs(ctx) {
19329
19511
  getDocumentBacklinksStub(ctx)
19330
19512
  ];
19331
19513
  }
19514
+ function createPlanStub(ctx) {
19515
+ return {
19516
+ name: "createPlan",
19517
+ description: "Create the FULL project plan in one call: all phases, the tasks nested under each phase, and the target milestone. Preferred over itemized createPhase/createTask/createMilestone calls \u2014 one createPlan call persists everything atomically.",
19518
+ category: "project",
19519
+ parameters: [],
19520
+ execute: async (args) => {
19521
+ return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
19522
+ const rawPhases = Array.isArray(args["phases"]) ? args["phases"] : [];
19523
+ if (rawPhases.length === 0) {
19524
+ return 'createPlan requires a non-empty "phases" array (each phase: { name, description, order, color, tasks: [{ title, description, fileRefs, acceptance, qaScenario, priority }] }).';
19525
+ }
19526
+ const summary = readPlan(ctx);
19527
+ let phasesCreated = 0;
19528
+ let tasksCreated = 0;
19529
+ const phaseIds = [];
19530
+ rawPhases.forEach((rawPhase, idx) => {
19531
+ const { id: phaseId, created } = addPhaseRecord(summary, {
19532
+ name: rawPhase["name"] ?? `Phase ${idx + 1}`,
19533
+ description: rawPhase["description"] ?? "",
19534
+ order: rawPhase["order"] ?? idx + 1,
19535
+ color: rawPhase["color"] ?? "#3b82f6"
19536
+ });
19537
+ if (created) phasesCreated += 1;
19538
+ phaseIds.push(phaseId);
19539
+ const rawTasks = Array.isArray(rawPhase["tasks"]) ? rawPhase["tasks"] : [];
19540
+ for (const rawTask of rawTasks) {
19541
+ const { created: taskCreated } = addTaskRecord(
19542
+ ctx,
19543
+ summary,
19544
+ phaseId,
19545
+ {
19546
+ title: rawTask["title"] ?? "New Task",
19547
+ description: rawTask["description"] ?? "",
19548
+ fileRefs: rawTask["fileRefs"] ?? rawTask["files"] ?? [],
19549
+ acceptance: rawTask["acceptance"] ?? [],
19550
+ qaScenario: rawTask["qaScenario"] ?? rawTask["qa"] ?? "",
19551
+ priority: rawTask["priority"] ?? "medium"
19552
+ },
19553
+ { dedupe: true }
19554
+ );
19555
+ if (taskCreated) tasksCreated += 1;
19556
+ }
19557
+ });
19558
+ let milestonesCreated = 0;
19559
+ const rawMilestone = args["milestone"];
19560
+ if (rawMilestone && typeof rawMilestone === "object") {
19561
+ const { created } = addMilestoneRecord(ctx, summary, {
19562
+ title: rawMilestone["title"] ?? "v0.1.0 design-complete",
19563
+ description: rawMilestone["description"] ?? "",
19564
+ dueDate: rawMilestone["dueDate"],
19565
+ targetVersion: rawMilestone["targetVersion"]
19566
+ });
19567
+ if (created) milestonesCreated += 1;
19568
+ }
19569
+ writePlan(ctx, summary);
19570
+ return `Plan created: ${phasesCreated} phases, ${tasksCreated} tasks, ${milestonesCreated} milestone(s). Phase ids: ${phaseIds.join(", ")}.`;
19571
+ });
19572
+ }
19573
+ };
19574
+ }
19332
19575
  function createPhaseStub(ctx) {
19333
19576
  return {
19334
19577
  name: "createPhase",
@@ -19338,22 +19581,17 @@ function createPhaseStub(ctx) {
19338
19581
  execute: async (args) => {
19339
19582
  return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
19340
19583
  const name = args["name"] ?? "New Phase";
19341
- const description = args["description"] ?? "";
19342
19584
  const order = args["order"] ?? 0;
19343
- const color = args["color"] ?? "#3b82f6";
19344
- const id = slugify2(name) || `phase-${order}`;
19345
19585
  const summary = readPlan(ctx);
19346
- if (summary.phases.some((p3) => p3.id === id)) {
19347
- return `Phase "${id}" already exists.`;
19348
- }
19349
- summary.phases.push({
19350
- kind: "phase",
19351
- id,
19586
+ const { id, created } = addPhaseRecord(summary, {
19352
19587
  name,
19353
- description,
19588
+ description: args["description"] ?? "",
19354
19589
  order,
19355
- color
19590
+ color: args["color"] ?? "#3b82f6"
19356
19591
  });
19592
+ if (!created) {
19593
+ return `Phase "${id}" already exists.`;
19594
+ }
19357
19595
  writePlan(ctx, summary);
19358
19596
  return `Phase "${name}" created (id: ${id}, order: ${order}).`;
19359
19597
  });
@@ -19370,54 +19608,26 @@ function createTaskStub(ctx) {
19370
19608
  return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
19371
19609
  const phaseId = args["phaseId"] ?? args["phase"];
19372
19610
  const title = args["title"] ?? "New Task";
19373
- const description = args["description"] ?? "";
19374
- const fileRefs = args["fileRefs"] ?? args["files"] ?? [];
19375
- const acceptance = args["acceptance"] ?? [];
19376
- const qaScenario = args["qaScenario"] ?? args["qa"] ?? "";
19377
- const priority = args["priority"] ?? "medium";
19378
19611
  if (!phaseId) return "Task requires a phaseId.";
19379
19612
  const summary = readPlan(ctx);
19380
19613
  if (!summary.phases.some((p3) => p3.id === phaseId)) {
19381
19614
  return `Phase "${phaseId}" not found. Create it first via /createPhase.`;
19382
19615
  }
19383
- const id = `${phaseId}-${slugify2(title)}-${summary.tasks.filter((t) => t.phaseId === phaseId).length + 1}`;
19384
- summary.tasks.push({
19385
- kind: "task",
19386
- id,
19387
- // v0.7.3: keep the human-readable title in plan.json so
19388
- // buildPlanSummary can render it in the system prompt.
19389
- name: title,
19616
+ const { id } = addTaskRecord(
19617
+ ctx,
19618
+ summary,
19390
19619
  phaseId,
19391
- status: "pending",
19392
- priority
19393
- });
19620
+ {
19621
+ title,
19622
+ description: args["description"] ?? "",
19623
+ fileRefs: args["fileRefs"] ?? args["files"] ?? [],
19624
+ acceptance: args["acceptance"] ?? [],
19625
+ qaScenario: args["qaScenario"] ?? args["qa"] ?? "",
19626
+ priority: args["priority"] ?? "medium"
19627
+ },
19628
+ { dedupe: false }
19629
+ );
19394
19630
  writePlan(ctx, summary);
19395
- const taskPath = join4(ctx.rootDir, "plan-tasks", `${id}.md`);
19396
- const meta3 = {
19397
- kind: "task",
19398
- id,
19399
- phaseId,
19400
- status: "pending",
19401
- priority,
19402
- tags: fileRefs
19403
- };
19404
- const body = [
19405
- `# ${title}`,
19406
- "",
19407
- description,
19408
- "",
19409
- `## File references`,
19410
- ...fileRefs.map((f) => `- \`${f}\``),
19411
- "",
19412
- `## Acceptance criteria`,
19413
- ...acceptance.map((a) => `- ${a}`),
19414
- "",
19415
- `## QA scenario`,
19416
- "",
19417
- qaScenario || "_(not specified)_",
19418
- ""
19419
- ].join("\n");
19420
- ctx.storage.write(taskPath, meta3, body);
19421
19631
  return `Task "${title}" created (id: ${id}) under phase "${phaseId}".`;
19422
19632
  });
19423
19633
  }
@@ -19433,30 +19643,32 @@ function updateTaskStub(ctx) {
19433
19643
  return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
19434
19644
  const taskId = args["taskId"];
19435
19645
  const rawStatus = args["status"];
19436
- if (!taskId || !rawStatus) return "updateTask requires taskId and status.";
19646
+ if (!taskId || !rawStatus)
19647
+ return "updateTask requires taskId and status.";
19437
19648
  const valid = ["pending", "in_progress", "done", "blocked"];
19438
19649
  const aliases = {
19439
- "todo": "pending",
19650
+ todo: "pending",
19440
19651
  "to-do": "pending",
19441
- "open": "pending",
19442
- "backlog": "pending",
19652
+ open: "pending",
19653
+ backlog: "pending",
19443
19654
  "in-progress": "in_progress",
19444
19655
  "in progress": "in_progress",
19445
- "doing": "in_progress",
19446
- "started": "in_progress",
19447
- "active": "in_progress",
19448
- "complete": "done",
19449
- "completed": "done",
19450
- "finished": "done",
19451
- "closed": "done",
19452
- "resolved": "done",
19453
- "stuck": "blocked",
19454
- "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",
19455
19666
  "on-hold": "blocked",
19456
19667
  "on hold": "blocked"
19457
19668
  };
19458
19669
  const status = valid.includes(rawStatus) ? rawStatus : aliases[rawStatus.toLowerCase()];
19459
- 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(", ")}.`;
19460
19672
  const summary = readPlan(ctx);
19461
19673
  const task = summary.tasks.find((t) => t.id === taskId);
19462
19674
  if (!task) return `Task "${taskId}" not found.`;
@@ -19523,39 +19735,16 @@ function createMilestoneStub(ctx) {
19523
19735
  execute: async (args) => {
19524
19736
  return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
19525
19737
  const title = args["title"] ?? "New Milestone";
19526
- const description = args["description"] ?? "";
19527
- const dueDate = args["dueDate"];
19528
- const id = `m-${slugify2(title)}`;
19529
19738
  const summary = readPlan(ctx);
19530
- if (summary.milestones.some((m) => m.id === id)) return `Milestone "${id}" already exists.`;
19531
- const version2 = args["targetVersion"] ?? dueDate ?? "TBD";
19532
- summary.milestones.push({
19533
- kind: "milestone",
19534
- id,
19535
- name: title,
19536
- description,
19537
- dueDate,
19538
- targetVersion: version2
19739
+ const { id, created } = addMilestoneRecord(ctx, summary, {
19740
+ title,
19741
+ description: args["description"] ?? "",
19742
+ dueDate: args["dueDate"],
19743
+ targetVersion: args["targetVersion"]
19539
19744
  });
19745
+ if (!created) return `Milestone "${id}" already exists.`;
19540
19746
  writePlan(ctx, summary);
19541
- const path22 = join4(ctx.rootDir, "milestones", `${id}.md`);
19542
- const meta3 = {
19543
- kind: "milestone",
19544
- id,
19545
- name: title,
19546
- description,
19547
- dueDate,
19548
- targetVersion: version2
19549
- };
19550
- const body = [
19551
- `# Milestone: ${title}`,
19552
- "",
19553
- description,
19554
- "",
19555
- `Target version: ${version2}`,
19556
- ""
19557
- ].join("\n");
19558
- ctx.storage.write(path22, meta3, body);
19747
+ const version2 = summary.milestones.find((m) => m.id === id)?.targetVersion ?? "TBD";
19559
19748
  return `Milestone "${title}" created (id: ${id}, version: ${version2}).`;
19560
19749
  });
19561
19750
  }
@@ -19572,16 +19761,28 @@ function createDocumentStub(ctx) {
19572
19761
  const title = args["title"] ?? "New Document";
19573
19762
  const content = args["content"] ?? "";
19574
19763
  const tags = args["tags"] ?? [];
19575
- const id = slugify2(title) || `doc-${Date.now()}`;
19576
- const path22 = workspaceArtifact(ctx.rootDir, "docs", id);
19764
+ const normalizedTitle = title.replace(/\.(md|markdown)\s*$/i, "");
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);
19577
19778
  const meta3 = {
19578
19779
  kind: "doc",
19579
- id,
19780
+ id: slug,
19580
19781
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
19581
19782
  tags
19582
19783
  };
19583
19784
  ctx.storage.write(path22, meta3, content);
19584
- return `Document "${title}" created at docs/${id}.md.`;
19785
+ return `Document "${title}" created at docs/${slug}.md.`;
19585
19786
  });
19586
19787
  }
19587
19788
  };
@@ -19602,7 +19803,9 @@ function searchDocumentsStub(ctx) {
19602
19803
  const limit = args["limit"] ?? 5;
19603
19804
  if (!rawQuery) return "searchDocuments requires a query.";
19604
19805
  const phrases = rawQuery.toLowerCase().split(/\s+or\s+/i).map((p3) => p3.trim()).filter((p3) => p3.length > 0);
19605
- 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
+ );
19606
19809
  const files = [
19607
19810
  ...ctx.storage.listMarkdown(join4(ctx.rootDir, "decisions")),
19608
19811
  ...ctx.storage.listMarkdown(join4(ctx.rootDir, "docs")),
@@ -19641,10 +19844,14 @@ function searchDocumentsStub(ctx) {
19641
19844
  const start = Math.max(0, idx - 40);
19642
19845
  const end = Math.min(raw.length, idx + matchLen + 40);
19643
19846
  const snippet = "\u2026" + raw.slice(start, end).replace(/\n/g, " ").trim() + "\u2026";
19644
- results.push({ path: relative2(ctx.rootDir, file2) || basename2(file2), snippet });
19847
+ results.push({
19848
+ path: relative2(ctx.rootDir, file2) || basename2(file2),
19849
+ snippet
19850
+ });
19645
19851
  if (results.length >= limit) break;
19646
19852
  }
19647
- 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.`;
19648
19855
  return nudge + results.map((r) => `[${r.path}]
19649
19856
  ${r.snippet}`).join("\n\n");
19650
19857
  }
@@ -19658,8 +19865,8 @@ function linkDocumentsStub(ctx) {
19658
19865
  parameters: [],
19659
19866
  execute: async (args) => {
19660
19867
  return workspaceMutex.run(`${ctx.rootDir}:link`, () => {
19661
- const fromId = args["fromId"];
19662
- const toId = args["toId"];
19868
+ const fromId = args["fromId"] ?? args["sourceId"];
19869
+ const toId = args["toId"] ?? args["targetId"] ?? args["targetPathOrTitle"];
19663
19870
  if (!fromId || !toId) return "linkDocuments requires fromId and toId.";
19664
19871
  const allFiles = [
19665
19872
  ...ctx.storage.listMarkdown(join4(ctx.rootDir, "decisions")),
@@ -19692,7 +19899,7 @@ function getDocumentBacklinksStub(ctx) {
19692
19899
  category: "analysis",
19693
19900
  parameters: [],
19694
19901
  execute: async (args) => {
19695
- const targetId = args["targetId"];
19902
+ const targetId = args["targetId"] ?? args["id"];
19696
19903
  if (!targetId) return "getDocumentBacklinks requires targetId.";
19697
19904
  const allFiles = [
19698
19905
  ...ctx.storage.listMarkdown(join4(ctx.rootDir, "decisions")),
@@ -19702,7 +19909,9 @@ function getDocumentBacklinksStub(ctx) {
19702
19909
  const backlinks = [];
19703
19910
  for (const file2 of allFiles) {
19704
19911
  try {
19705
- const doc = ctx.storage.read(file2);
19912
+ const doc = ctx.storage.read(
19913
+ file2
19914
+ );
19706
19915
  if (doc.meta.id === targetId) continue;
19707
19916
  if ((doc.meta.related ?? []).includes(targetId)) {
19708
19917
  backlinks.push(doc.meta.id);
@@ -19776,12 +19985,28 @@ var init_toolRegistry = __esm({
19776
19985
  }
19777
19986
  });
19778
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
+
19779
20003
  // src/cli/mcp/mcpClient.ts
19780
20004
  import { spawn as spawn2 } from "node:child_process";
19781
20005
  var DEFAULT_REQUEST_TIMEOUT_MS, INIT_TIMEOUT_MS, MCP_PROTOCOL_VERSION, McpClient;
19782
20006
  var init_mcpClient = __esm({
19783
20007
  "src/cli/mcp/mcpClient.ts"() {
19784
20008
  "use strict";
20009
+ init_cmdline();
19785
20010
  DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
19786
20011
  INIT_TIMEOUT_MS = 15e3;
19787
20012
  MCP_PROTOCOL_VERSION = "2025-03-26";
@@ -19798,14 +20023,15 @@ var init_mcpClient = __esm({
19798
20023
  /** Spawn the server process and run the MCP initialize handshake. */
19799
20024
  async start() {
19800
20025
  if (this.child) return;
19801
- const child = spawn2(this.config.command, this.config.args ?? [], {
20026
+ const spawnOpts = {
19802
20027
  stdio: ["pipe", "pipe", "pipe"],
19803
20028
  env: { ...process.env, ...this.config.env ?? {} },
19804
- // On Windows `npx`/`uvx` resolve to .cmd shims which plain spawn
19805
- // cannot execute; shell:true lets cmd.exe resolve them.
19806
- shell: process.platform === "win32",
19807
20029
  windowsHide: true
19808
- });
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);
19809
20035
  this.child = child;
19810
20036
  child.stdout.setEncoding("utf8");
19811
20037
  child.stdout.on("data", (chunk) => this.onStdout(chunk));
@@ -19820,7 +20046,7 @@ var init_mcpClient = __esm({
19820
20046
  {
19821
20047
  protocolVersion: MCP_PROTOCOL_VERSION,
19822
20048
  capabilities: {},
19823
- clientInfo: { name: "zelari-code", version: "0.7.5" }
20049
+ clientInfo: { name: "zelari-code", version: "0.7.9" }
19824
20050
  },
19825
20051
  INIT_TIMEOUT_MS
19826
20052
  );
@@ -19922,11 +20148,11 @@ __export(mcpManager_exports, {
19922
20148
  });
19923
20149
  import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
19924
20150
  import { join as join5 } from "node:path";
19925
- import { homedir as homedir2 } from "node:os";
20151
+ import { homedir as homedir3 } from "node:os";
19926
20152
  function readMcpConfig(projectRoot = process.cwd()) {
19927
20153
  const merged = {};
19928
20154
  const paths = [
19929
- join5(homedir2(), ".zelari-code", "mcp.json"),
20155
+ join5(homedir3(), ".zelari-code", "mcp.json"),
19930
20156
  join5(projectRoot, ".zelari", "mcp.json")
19931
20157
  // later = higher precedence
19932
20158
  ];
@@ -20135,7 +20361,40 @@ Example format:
20135
20361
  - Make dependencies explicit (task B depends on task A).
20136
20362
  - If the stack/platform is unknown, ask ONCE (see clarification protocol) rather than guessing.
20137
20363
 
20138
- Keep plans hierarchical and practical. Stay under 250 words.${CLARIFICATION_PROTOCOL8}`,
20364
+ Keep plans hierarchical and practical. Stay under 250 words.${CLARIFICATION_PROTOCOL8}
20365
+
20366
+ ## Design-phase mandatory plan artifact
20367
+ In design-phase mode (TASK mentions design/architecture/spec, no existing codebase to edit), you MUST persist the plan through workspace tools. The plan exists ONLY if it was persisted via a tool call \u2014 prose does not count, and the post-run check will flag your run if the plan is missing.
20368
+
20369
+ PREFERRED \u2014 emit ONE single \`createPlan\` call containing the whole plan: 4 phases, 12 concrete tasks (3 per phase), and 1 milestone, all in the same call:
20370
+
20371
+ \`\`\`
20372
+ createPlan({
20373
+ phases: [
20374
+ {
20375
+ name: "Foundation & Technical Blueprint",
20376
+ description: "Lock the stack and the non-negotiable quality budget. Exit: baseline builds green.",
20377
+ order: 1,
20378
+ color: "#3b82f6",
20379
+ tasks: [
20380
+ { title: "Lock stack baseline", description: "2-3 sentences of context", fileRefs: ["src/main.tsx:L1-L40"], acceptance: ["App boots with strict TS", "CI build green"], qaScenario: "Run npm run build and confirm zero errors", priority: "high" },
20381
+ { title: "Define NFR budget", description: "...", fileRefs: ["docs/nfr.md"], acceptance: ["LCP/WCAG/Lighthouse targets documented"], qaScenario: "...", priority: "high" },
20382
+ { title: "Document design-phase exit criteria", description: "...", fileRefs: ["docs/exit-criteria.md"], acceptance: ["Each phase has a testable exit row"], qaScenario: "...", priority: "medium" }
20383
+ ]
20384
+ },
20385
+ { name: "...", order: 2, tasks: [ /* 3 tasks */ ] },
20386
+ { name: "...", order: 3, tasks: [ /* 3 tasks */ ] },
20387
+ { name: "...", order: 4, tasks: [ /* 3 tasks */ ] }
20388
+ ],
20389
+ milestone: { title: "v0.1.0 design-complete", description: "All design artifacts exist and the green-light checklist passes.", targetVersion: "v0.1.0" }
20390
+ })
20391
+ \`\`\`
20392
+
20393
+ Every task MUST include fileRefs, acceptance, and qaScenario \u2014 the QA scenario proves the task is verifiable.
20394
+
20395
+ FALLBACK \u2014 only if you cannot batch, use the itemized tools: \`createPhase\` once per phase; each createPhase response returns the new phase id \u2014 use that exact id as \`phaseId\` in the 3 \`createTask\` calls for that phase; finish with one \`createMilestone\` ({ title, description, targetVersion }). If you forget an id, call \`searchDocuments\` (limit 1) to look it up, then continue.
20396
+
20397
+ Do NOT output tasks as prose. The system will refuse any task that does not pass a valid phaseId.`,
20139
20398
  // v0.7.2: read/search the codebase to ground the plan in reality.
20140
20399
  tools: ["list_files", "read_file", "grep_content"],
20141
20400
  skills: ["project-planner", "vault-manager"]
@@ -20166,10 +20425,21 @@ Keep plans hierarchical and practical. Stay under 250 words.${CLARIFICATION_PROT
20166
20425
  ## Themes
20167
20426
  ## Top picks (with feasibility/novelty + de-risk note)
20168
20427
 
20169
- Stay under 200 words.${CLARIFICATION_PROTOCOL8}`,
20428
+ Stay under 200 words.${CLARIFICATION_PROTOCOL8}
20429
+
20430
+ ## Design-phase artifact (mandatory when running council in design-phase mode)
20431
+ If the council is in design-phase mode (TASK mentions design/architecture/spec, no existing codebase to edit), you MUST also persist your ideation output as workspace documents via the \`createDocument\` tool. Emit AT LEAST 3 separate \`createDocument\` calls, each tagged with one of these categories:
20432
+
20433
+ - \`customer-journey-map\` \u2014 2-3 personas (give them names, demographics, goals), journey table (stage \u2192 action \u2192 touchpoint \u2192 emotion), pain points per stage. This is a customer journey deliverable.
20434
+ - \`information-architecture\` \u2014 site map tree (root \u2192 section \u2192 page), navigation model (primary nav, breadcrumbs, footer), URL patterns. This is an information architecture deliverable.
20435
+ - \`design-tokens\` \u2014 color palette (semantic + hex), typography scale, spacing scale, motion principles. This is a design tokens deliverable.
20436
+
20437
+ You may optionally add a 4th design system doc if the project warrants it.
20438
+
20439
+ Pass the tool call as: \`createDocument({ title: "<category>", content: "<markdown body>" })\`. Do NOT summarize these into prose \u2014 they are the deliverable.`,
20170
20440
  // v0.7.2: explore the codebase to ground ideas in what exists.
20171
20441
  tools: ["list_files", "read_file"],
20172
- skills: ["idea-synthesizer", "mind-mapper", "document-writer"]
20442
+ skills: ["document-writer", "mind-mapper"]
20173
20443
  },
20174
20444
  {
20175
20445
  id: "pluton",
@@ -20194,10 +20464,17 @@ Stay under 200 words.${CLARIFICATION_PROTOCOL8}`,
20194
20464
  - Keep the graph comprehensible: prune redundant or duplicate nodes.
20195
20465
 
20196
20466
  ## Output format
20197
- 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.`,
20198
20475
  // v0.7.2: map the actual code/module structure instead of an abstract mind-map.
20199
20476
  tools: ["list_files", "read_file", "grep_content"],
20200
- skills: ["mind-mapper", "research-analyst"]
20477
+ skills: ["document-writer", "research-analyst"]
20201
20478
  },
20202
20479
  {
20203
20480
  id: "minos",
@@ -20226,9 +20503,21 @@ Describe the proposed structure (root \u2192 branches \u2192 leaves) in text and
20226
20503
  ## Contradictions
20227
20504
  ## Top improvements
20228
20505
 
20229
- Stay under 200 words. (You do not create workspace artifacts, so you do not emit a tools block \u2014 but you MAY ask the user a clarifying question if a core ambiguity blocks judgment.)${CLARIFICATION_PROTOCOL8}`,
20506
+ Stay under 200 words.${CLARIFICATION_PROTOCOL8}
20507
+
20508
+ ## Design-phase risks artifact (mandatory when running council in design-phase mode)
20509
+ When the council is in design-phase mode (TASK mentions design/architecture/spec, no existing codebase to edit), you ALSO write a \`risks\` document via the \`createDocument\` tool. This is your one allowed artifact emission. Pass the tool call as:
20510
+
20511
+ \`\`\`
20512
+ createDocument({
20513
+ title: "risks",
20514
+ content: \`# Risks\\n\\n## 1. <Risk title>\\n- Impact: <high|medium|low>\\n- Likelihood: <high|medium|low>\\n- Mitigation: <one-line mitigation>\\n\\n## 2. ...\`
20515
+ })
20516
+ \`\`\`
20517
+
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.`,
20230
20519
  tools: [],
20231
- skills: ["research-analyst"]
20520
+ skills: ["document-writer", "research-analyst"]
20232
20521
  },
20233
20522
  {
20234
20523
  id: "lucifer",
@@ -20251,7 +20540,19 @@ Stay under 200 words. (You do not create workspace artifacts, so you do not emit
20251
20540
  - Apply Minosse's highest-value improvements; drop descoped items.
20252
20541
  - After making changes, verify they work (compile, run tests, etc.) when feasible.
20253
20542
 
20254
- Use the tools available to you (read_file, write_file, edit_file, bash, list_files) directly as tool calls \u2014 the harness handles execution.${CLARIFICATION_PROTOCOL8}`,
20543
+ Use the tools available to you (read_file, write_file, edit_file, bash, list_files) directly as tool calls \u2014 the harness handles execution.${CLARIFICATION_PROTOCOL8}
20544
+
20545
+ ## Design-phase synthesis artifact (mandatory when running council in design-phase mode)
20546
+ When the council is in design-phase mode (TASK mentions design/architecture/spec, no existing codebase to edit), your final deliverable is a \`synthesis\` document via the \`createDocument\` tool (NOT \`write_file\` and NOT \`list_files\`):
20547
+
20548
+ \`\`\`
20549
+ createDocument({
20550
+ title: "synthesis",
20551
+ content: \`# Council Synthesis\\n\\n## Executive summary\\n<2-3 paragraphs>\\n\\n## Stack and key decisions\\n- <ADR-by-ADR summary>\\n\\n## Phases\\n<1-2 line summary per phase>\\n\\n## Top risks\\n<top 3 risks from Minosse>\\n\\n## Green-light checklist\\n- [ ] All ADRs accepted\\n- [ ] Risks have mitigations\\n- [ ] Tasks have acceptance criteria\`
20552
+ })
20553
+ \`\`\`
20554
+
20555
+ DO NOT call \`list_files\` \u2014 it is NOT a workspace tool. Use \`searchDocuments\` if you need to look something up (limit 2-3 searches, then act on the results). Your deliverable is the synthesis document; emit it via \`createDocument\`, not via prose.`,
20255
20556
  // v0.7.2: implementation tools — the synthesizer writes/edits files and runs commands.
20256
20557
  tools: ["read_file", "write_file", "edit_file", "bash", "list_files"],
20257
20558
  skills: ["vault-manager", "project-planner", "idea-synthesizer"]
@@ -20609,6 +20910,57 @@ var init_systemPromptBuilder = __esm({
20609
20910
  }
20610
20911
  });
20611
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
+
20612
20964
  // packages/core/dist/agents/councilApi.js
20613
20965
  function parseClarificationRequest(text) {
20614
20966
  const start = text.indexOf(QUESTION_MARKER);
@@ -20641,7 +20993,7 @@ function parseThinking(text) {
20641
20993
  function cleanAgentContent(text) {
20642
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();
20643
20995
  }
20644
- function buildAgentMessages(agent, userMessage, ragContext, workspaceContext, priorOutputs, aiConfig, executableTools) {
20996
+ function buildAgentMessages(agent, userMessage, ragContext, workspaceContext, priorOutputs, aiConfig, executableTools, runMode = "implementation") {
20645
20997
  const allToolNames = computeAgentTools(agent, aiConfig);
20646
20998
  const toolNames = executableTools ? allToolNames.filter((n) => executableTools.has(n)) : allToolNames;
20647
20999
  const enhancedSystemPrompt = buildSystemPrompt(agent, {
@@ -20653,6 +21005,7 @@ function buildAgentMessages(agent, userMessage, ragContext, workspaceContext, pr
20653
21005
  });
20654
21006
  const messages = [
20655
21007
  { role: "system", content: enhancedSystemPrompt },
21008
+ { role: "system", content: councilModeBanner(runMode) },
20656
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." }
20657
21010
  ];
20658
21011
  if (ragContext) {
@@ -20683,6 +21036,13 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
20683
21036
  ...config2.existingOutputs ?? []
20684
21037
  ];
20685
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
+ });
20686
21046
  yield {
20687
21047
  type: "agent_start",
20688
21048
  id: crypto.randomUUID(),
@@ -20707,8 +21067,14 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
20707
21067
  };
20708
21068
  callbacks.onMemberCost?.(cost);
20709
21069
  };
20710
- const executableNames = config2.tools ? new Set(config2.tools.list()) : null;
20711
- const filterExecutable = (names) => executableNames ? names.filter((n) => executableNames.has(n)) : names;
21070
+ const executorToolNames = config2.tools ? config2.tools.list() : [];
21071
+ const executableNames = config2.tools ? new Set(executorToolNames) : null;
21072
+ const filterExecutable = (names) => {
21073
+ if (!executableNames)
21074
+ return names;
21075
+ const merged = Array.from(/* @__PURE__ */ new Set([...names, ...executorToolNames]));
21076
+ return merged.filter((n) => executableNames.has(n));
21077
+ };
20712
21078
  const allSpecialists = agents.filter((a) => a.id !== "lucifer" && a.id !== "minos");
20713
21079
  const specialists = config2.feedbackStore ? config2.feedbackStore.ranked(allSpecialists) : allSpecialists;
20714
21080
  const oracle = agents.find((a) => a.id === "minos");
@@ -20730,7 +21096,7 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
20730
21096
  model: effectiveModel,
20731
21097
  provider: effectiveProvider,
20732
21098
  sessionId,
20733
- 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),
20734
21100
  tools: agentTools,
20735
21101
  eventBus: config2.eventBus,
20736
21102
  toolRegistry: config2.tools,
@@ -20751,12 +21117,14 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
20751
21117
  let toolCalls = 0;
20752
21118
  let usage = null;
20753
21119
  let errored = false;
21120
+ const emittedToolNames = [];
20754
21121
  const memberStart = Date.now();
20755
21122
  try {
20756
21123
  for await (const event of harness.run()) {
20757
21124
  yield event;
20758
21125
  if (event.type === "tool_execution_start") {
20759
21126
  toolCalls += 1;
21127
+ emittedToolNames.push(event.toolName);
20760
21128
  }
20761
21129
  if (event.type === "message_end" && event.usage) {
20762
21130
  usage = event.usage;
@@ -20774,6 +21142,27 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
20774
21142
  fullText = `Error: ${err instanceof Error ? err.message : "Unknown"}`;
20775
21143
  errored = true;
20776
21144
  }
21145
+ if (isDesignPhase && !errored && !NON_RETRY_AGENTS.has(agent.id)) {
21146
+ const specialistCheck = enforceDesignPhaseToolEmissions(agent.id, emittedToolNames);
21147
+ yield* applyRetryIfMissing({
21148
+ agent,
21149
+ check: specialistCheck,
21150
+ requirements: DESIGN_PHASE_REQUIREMENTS[agent.id],
21151
+ emittedToolNames,
21152
+ executableNames,
21153
+ sessionId,
21154
+ userMessage,
21155
+ agentOutputs,
21156
+ config: config2,
21157
+ effectiveProvider,
21158
+ effectiveModel,
21159
+ onToolCall: () => {
21160
+ toolCalls += 1;
21161
+ }
21162
+ });
21163
+ } else if (isDesignPhase) {
21164
+ enforceDesignPhaseToolEmissions(agent.id, emittedToolNames);
21165
+ }
20777
21166
  const memberDuration = Date.now() - memberStart;
20778
21167
  emitMemberCost({
20779
21168
  memberId: agent.id,
@@ -20819,7 +21208,7 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
20819
21208
  });
20820
21209
  }
20821
21210
  }
20822
- if (config2.debateMode && oracle && !completedIds.has(oracle.id)) {
21211
+ if (oracle && !completedIds.has(oracle.id)) {
20823
21212
  callbacks.onAgentStart?.(oracle);
20824
21213
  const override = config2.agentModels?.[oracle.id];
20825
21214
  const effectiveProvider = override?.providerId ?? config2.provider ?? "minimax";
@@ -20833,8 +21222,19 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
20833
21222
  model: effectiveModel,
20834
21223
  provider: effectiveProvider,
20835
21224
  sessionId,
20836
- messages: buildAgentMessages(oracle, `Review these proposals for: "${userMessage}"`, "", "", anonymized, config2.aiConfig, executableNames),
20837
- 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
+ })(),
20838
21238
  eventBus: config2.eventBus,
20839
21239
  toolRegistry: config2.tools,
20840
21240
  // Task G.2 — same per-turn limit applies to oracle.
@@ -20852,12 +21252,14 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
20852
21252
  let toolCalls = 0;
20853
21253
  let usage = null;
20854
21254
  let errored = false;
21255
+ const emittedToolNames = [];
20855
21256
  const memberStart = Date.now();
20856
21257
  try {
20857
21258
  for await (const event of harness.run()) {
20858
21259
  yield event;
20859
21260
  if (event.type === "tool_execution_start") {
20860
21261
  toolCalls += 1;
21262
+ emittedToolNames.push(event.toolName);
20861
21263
  }
20862
21264
  if (event.type === "message_end" && event.usage) {
20863
21265
  usage = event.usage;
@@ -20875,6 +21277,27 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
20875
21277
  fullText = `Review error: ${err instanceof Error ? err.message : "Unknown"}`;
20876
21278
  errored = true;
20877
21279
  }
21280
+ if (isDesignPhase && !errored) {
21281
+ const oracleCheck = enforceDesignPhaseToolEmissions(oracle.id, emittedToolNames);
21282
+ yield* applyRetryIfMissing({
21283
+ agent: oracle,
21284
+ check: oracleCheck,
21285
+ requirements: DESIGN_PHASE_REQUIREMENTS[oracle.id],
21286
+ emittedToolNames,
21287
+ executableNames,
21288
+ sessionId,
21289
+ userMessage: `Review these proposals for: "${userMessage}"`,
21290
+ agentOutputs,
21291
+ config: config2,
21292
+ effectiveProvider,
21293
+ effectiveModel,
21294
+ onToolCall: () => {
21295
+ toolCalls += 1;
21296
+ }
21297
+ });
21298
+ } else if (isDesignPhase) {
21299
+ enforceDesignPhaseToolEmissions(oracle.id, emittedToolNames);
21300
+ }
20878
21301
  const memberDuration = Date.now() - memberStart;
20879
21302
  emitMemberCost({
20880
21303
  memberId: oracle.id,
@@ -20929,7 +21352,7 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
20929
21352
  model: effectiveModel,
20930
21353
  provider: effectiveProvider,
20931
21354
  sessionId,
20932
- 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),
20933
21356
  tools: chairmanTools,
20934
21357
  eventBus: config2.eventBus,
20935
21358
  toolRegistry: config2.tools,
@@ -20948,12 +21371,14 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
20948
21371
  let usage = null;
20949
21372
  let errored = false;
20950
21373
  let lastErrorMessage = "";
21374
+ const emittedToolNames = [];
20951
21375
  const memberStart = Date.now();
20952
21376
  try {
20953
21377
  for await (const event of chairmanHarness.run()) {
20954
21378
  yield event;
20955
21379
  if (event.type === "tool_execution_start") {
20956
21380
  toolCalls += 1;
21381
+ emittedToolNames.push(event.toolName);
20957
21382
  }
20958
21383
  if (event.type === "message_end" && event.usage) {
20959
21384
  usage = event.usage;
@@ -20975,6 +21400,27 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
20975
21400
  errored = true;
20976
21401
  lastErrorMessage = err instanceof Error ? err.message : String(err);
20977
21402
  }
21403
+ if (isDesignPhase && !errored) {
21404
+ const chairmanCheck = enforceDesignPhaseToolEmissions(chairman.id, emittedToolNames);
21405
+ yield* applyRetryIfMissing({
21406
+ agent: chairman,
21407
+ check: chairmanCheck,
21408
+ requirements: DESIGN_PHASE_REQUIREMENTS[chairman.id],
21409
+ emittedToolNames,
21410
+ executableNames,
21411
+ sessionId,
21412
+ userMessage,
21413
+ agentOutputs,
21414
+ config: config2,
21415
+ effectiveProvider,
21416
+ effectiveModel,
21417
+ onToolCall: () => {
21418
+ toolCalls += 1;
21419
+ }
21420
+ });
21421
+ } else if (isDesignPhase) {
21422
+ enforceDesignPhaseToolEmissions(chairman.id, emittedToolNames);
21423
+ }
20978
21424
  const memberDuration = Date.now() - memberStart;
20979
21425
  const finalSynthesis = errored && fullText.length === 0 ? `[Chairman synthesis failed: ${lastErrorMessage || "unknown error"}]` : fullText;
20980
21426
  callbacks.onSynthesisDone?.(finalSynthesis, void 0, void 0);
@@ -21008,7 +21454,143 @@ async function* runCouncilPure(userMessage, config2, callbacks = {}) {
21008
21454
  durationMs: 0
21009
21455
  };
21010
21456
  }
21011
- var QUESTION_MARKER, QUESTION_END_MARKER;
21457
+ function checkMemberToolEmissions(_memberId, emittedToolNames, requirements) {
21458
+ if (requirements.length === 0) {
21459
+ return { ok: true, missing: [] };
21460
+ }
21461
+ const counts = /* @__PURE__ */ new Map();
21462
+ for (const name of emittedToolNames) {
21463
+ counts.set(name, (counts.get(name) ?? 0) + 1);
21464
+ }
21465
+ const missing = [];
21466
+ for (const req of requirements) {
21467
+ const got = counts.get(req.name) ?? 0;
21468
+ if (got < req.min) {
21469
+ missing.push(`${req.name} (got ${got}, need >= ${req.min})`);
21470
+ }
21471
+ }
21472
+ return { ok: missing.length === 0, missing };
21473
+ }
21474
+ function checkMemberToolEmissionSets(memberId, emittedToolNames, sets) {
21475
+ if (sets.length === 0) {
21476
+ return { ok: true, missing: [] };
21477
+ }
21478
+ const results = sets.map((set2) => checkMemberToolEmissions(memberId, emittedToolNames, set2));
21479
+ if (results.some((r) => r.ok)) {
21480
+ return { ok: true, missing: [] };
21481
+ }
21482
+ return results[0];
21483
+ }
21484
+ function enforceDesignPhaseToolEmissions(memberId, emittedToolNames) {
21485
+ const sets = DESIGN_PHASE_REQUIREMENT_SETS[memberId];
21486
+ if (!sets || sets.length === 0) {
21487
+ return { ok: true, missing: [] };
21488
+ }
21489
+ const result = checkMemberToolEmissionSets(memberId, emittedToolNames, sets);
21490
+ if (!result.ok) {
21491
+ console.warn(`[council] member "${memberId}" did not emit required tools: ${result.missing.join(", ")}. The downstream .zelari/ deliverable may be incomplete. (A forced retry turn scoped to the missing tools follows; the deterministic complete-design fallback covers any remaining gap.)`);
21492
+ }
21493
+ return result;
21494
+ }
21495
+ function shouldRetryMember(missingToolNames, attemptsSoFar) {
21496
+ if (missingToolNames.length === 0)
21497
+ return false;
21498
+ if (attemptsSoFar >= MAX_RETRY_PER_MEMBER)
21499
+ return false;
21500
+ return true;
21501
+ }
21502
+ function buildRetryPrompt(missingToolNames) {
21503
+ const names = missingToolNames.join(", ");
21504
+ return `You did not emit the required workspace tools: ${names}. Call ${names} NOW with concrete arguments. No prose. No search.`;
21505
+ }
21506
+ async function* runRetryTurnForMember(args) {
21507
+ const executableMissing = args.executableTools ? args.missingToolNames.filter((n) => args.executableTools.has(n)) : args.missingToolNames;
21508
+ if (executableMissing.length === 0) {
21509
+ return [];
21510
+ }
21511
+ const retryToolNames = executableMissing;
21512
+ const retryToolSpecs = getProviderTools(retryToolNames).map((t) => ({
21513
+ name: t.function.name,
21514
+ description: t.function.description,
21515
+ parameters: t.function.parameters
21516
+ }));
21517
+ const baseMessages = buildAgentMessages(args.agent, args.userMessage, args.ragContext, args.workspaceContext, args.priorOutputs, args.aiConfig, args.executableTools, args.runMode ?? "implementation");
21518
+ const retryMessages = [
21519
+ ...baseMessages,
21520
+ { role: "user", content: buildRetryPrompt(executableMissing) }
21521
+ ];
21522
+ const maxToolCalls = args.minPerTool !== void 0 ? Object.entries(args.minPerTool).filter(([name]) => executableMissing.includes(name)).reduce((sum, [, min]) => sum + min, 0) : retryToolNames.length;
21523
+ const retryHarness = new AgentHarness({
21524
+ model: args.effectiveModel,
21525
+ provider: args.effectiveProvider,
21526
+ sessionId: args.sessionId,
21527
+ messages: retryMessages,
21528
+ tools: retryToolSpecs,
21529
+ eventBus: args.eventBus,
21530
+ toolRegistry: args.toolRegistry,
21531
+ // Budget the retry so the model can satisfy every requirement in
21532
+ // ONE tool_calls turn. For createTask min:12 this needs 12 calls.
21533
+ maxToolCallsPerTurn: maxToolCalls,
21534
+ memberId: args.agent.id,
21535
+ memberName: args.agent.name,
21536
+ providerStream: (params) => args.providerStream(params)
21537
+ });
21538
+ const retryEmitted = [];
21539
+ for await (const event of retryHarness.run()) {
21540
+ if (event.type === "tool_execution_start") {
21541
+ retryEmitted.push(event.toolName);
21542
+ }
21543
+ yield event;
21544
+ }
21545
+ return retryEmitted;
21546
+ }
21547
+ async function* applyRetryIfMissing(args) {
21548
+ if (args.check.ok)
21549
+ return;
21550
+ const missingToolNames = args.check.missing.map((m) => m.split(" ")[0]);
21551
+ if (!shouldRetryMember(missingToolNames, 0))
21552
+ return;
21553
+ console.warn(`[council] ${args.agent.id} retrying missing tools: ${missingToolNames.join(", ")}`);
21554
+ const minPerTool = {};
21555
+ if (args.requirements) {
21556
+ for (const req of args.requirements) {
21557
+ if (missingToolNames.includes(req.name)) {
21558
+ minPerTool[req.name] = req.min;
21559
+ }
21560
+ }
21561
+ }
21562
+ try {
21563
+ const retryGenerator = runRetryTurnForMember({
21564
+ agent: args.agent,
21565
+ missingToolNames,
21566
+ minPerTool,
21567
+ executableTools: args.executableNames,
21568
+ userMessage: args.userMessage,
21569
+ ragContext: args.config.ragContext,
21570
+ workspaceContext: args.config.workspaceContext,
21571
+ priorOutputs: args.agentOutputs,
21572
+ aiConfig: args.config.aiConfig,
21573
+ sessionId: args.sessionId,
21574
+ effectiveModel: args.effectiveModel,
21575
+ effectiveProvider: args.effectiveProvider,
21576
+ eventBus: args.config.eventBus,
21577
+ toolRegistry: args.config.tools,
21578
+ providerStream: args.config.providerStream,
21579
+ runMode: args.config.runMode
21580
+ });
21581
+ for await (const event of retryGenerator) {
21582
+ if (event.type === "tool_execution_start") {
21583
+ args.onToolCall();
21584
+ args.emittedToolNames.push(event.toolName);
21585
+ }
21586
+ yield event;
21587
+ }
21588
+ } catch (retryErr) {
21589
+ console.error(`[council] ${args.agent.id} retry failed:`, retryErr);
21590
+ }
21591
+ enforceDesignPhaseToolEmissions(args.agent.id, args.emittedToolNames);
21592
+ }
21593
+ var NON_RETRY_AGENTS, QUESTION_MARKER, QUESTION_END_MARKER, DESIGN_PHASE_REQUIREMENT_SETS, DESIGN_PHASE_REQUIREMENTS, MAX_RETRY_PER_MEMBER;
21012
21594
  var init_councilApi = __esm({
21013
21595
  "packages/core/dist/agents/councilApi.js"() {
21014
21596
  "use strict";
@@ -21018,8 +21600,35 @@ var init_councilApi = __esm({
21018
21600
  init_tools();
21019
21601
  init_events();
21020
21602
  init_AgentHarness();
21603
+ init_modeBanners();
21604
+ init_runMode();
21605
+ NON_RETRY_AGENTS = /* @__PURE__ */ new Set([]);
21021
21606
  QUESTION_MARKER = "---QUESTION---";
21022
21607
  QUESTION_END_MARKER = "---END---";
21608
+ DESIGN_PHASE_REQUIREMENT_SETS = {
21609
+ nettun: [
21610
+ [{ name: "createPlan", min: 1 }],
21611
+ [
21612
+ { name: "createPhase", min: 3 },
21613
+ { name: "createTask", min: 6 },
21614
+ { name: "createMilestone", min: 1 }
21615
+ ]
21616
+ ],
21617
+ geryon: [
21618
+ [{ name: "createDocument", min: 3 }]
21619
+ ],
21620
+ pluton: [
21621
+ [{ name: "createDocument", min: 1 }]
21622
+ ],
21623
+ minos: [
21624
+ [{ name: "createDocument", min: 1 }]
21625
+ ],
21626
+ lucifer: [
21627
+ [{ name: "createDocument", min: 1 }]
21628
+ ]
21629
+ };
21630
+ DESIGN_PHASE_REQUIREMENTS = Object.fromEntries(Object.entries(DESIGN_PHASE_REQUIREMENT_SETS).map(([id, sets]) => [id, sets[0]]));
21631
+ MAX_RETRY_PER_MEMBER = 1;
21023
21632
  }
21024
21633
  });
21025
21634
 
@@ -21186,16 +21795,29 @@ var council_exports = {};
21186
21795
  __export(council_exports, {
21187
21796
  AGENT_ROLES: () => AGENT_ROLES,
21188
21797
  COLLABORATION_DIRECTIVE: () => COLLABORATION_DIRECTIVE,
21798
+ DESIGN_PHASE_MODE_BANNER: () => DESIGN_PHASE_MODE_BANNER,
21799
+ DESIGN_PHASE_REQUIREMENTS: () => DESIGN_PHASE_REQUIREMENTS,
21800
+ DESIGN_PHASE_REQUIREMENT_SETS: () => DESIGN_PHASE_REQUIREMENT_SETS,
21801
+ IMPLEMENTATION_MODE_BANNER: () => IMPLEMENTATION_MODE_BANNER,
21802
+ MAX_RETRY_PER_MEMBER: () => MAX_RETRY_PER_MEMBER,
21803
+ NON_RETRY_AGENTS: () => NON_RETRY_AGENTS,
21189
21804
  OUTPUT_QUALITY_DIRECTIVE: () => OUTPUT_QUALITY_DIRECTIVE,
21190
21805
  PROMPT_MODULES: () => PROMPT_MODULES,
21191
21806
  STRUCTURED_REASONING_DIRECTIVE: () => STRUCTURED_REASONING_DIRECTIVE,
21192
21807
  TOOL_USE_PROTOCOL_DIRECTIVE: () => TOOL_USE_PROTOCOL_DIRECTIVE,
21193
21808
  UnknownMemberError: () => UnknownMemberError,
21809
+ applyRetryIfMissing: () => applyRetryIfMissing,
21810
+ buildRetryPrompt: () => buildRetryPrompt,
21194
21811
  buildSkillDefinition: () => buildSkillDefinition,
21195
21812
  buildSystemPrompt: () => buildSystemPrompt,
21813
+ checkMemberToolEmissionSets: () => checkMemberToolEmissionSets,
21814
+ checkMemberToolEmissions: () => checkMemberToolEmissions,
21196
21815
  cleanAgentContent: () => cleanAgentContent,
21197
21816
  computeAgentSkills: () => computeAgentSkills,
21198
21817
  computeAgentTools: () => computeAgentTools,
21818
+ councilModeBanner: () => councilModeBanner,
21819
+ councilTierFromSize: () => councilTierFromSize,
21820
+ enforceDesignPhaseToolEmissions: () => enforceDesignPhaseToolEmissions,
21199
21821
  getAgent: () => getAgent,
21200
21822
  getBasePromptModules: () => getBasePromptModules,
21201
21823
  getCouncilAgents: () => getCouncilAgents,
@@ -21206,7 +21828,10 @@ __export(council_exports, {
21206
21828
  parseThinking: () => parseThinking,
21207
21829
  promoteMember: () => promoteMember,
21208
21830
  renderSkillMarkdown: () => renderSkillMarkdown,
21831
+ resolveCouncilRunMode: () => resolveCouncilRunMode,
21209
21832
  runCouncilPure: () => runCouncilPure,
21833
+ runRetryTurnForMember: () => runRetryTurnForMember,
21834
+ shouldRetryMember: () => shouldRetryMember,
21210
21835
  slugify: () => slugify3,
21211
21836
  stripClarificationProtocol: () => stripClarificationProtocol,
21212
21837
  swapMembers: () => swapMembers
@@ -21215,6 +21840,8 @@ var init_council = __esm({
21215
21840
  "packages/core/dist/council/index.js"() {
21216
21841
  "use strict";
21217
21842
  init_councilApi();
21843
+ init_runMode();
21844
+ init_modeBanners();
21218
21845
  init_roles();
21219
21846
  init_promoteMember();
21220
21847
  init_councilDirectives();
@@ -21223,6 +21850,64 @@ var init_council = __esm({
21223
21850
  }
21224
21851
  });
21225
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
+
21226
21911
  // src/cli/councilDispatcher.ts
21227
21912
  var councilDispatcher_exports = {};
21228
21913
  __export(councilDispatcher_exports, {
@@ -21236,26 +21921,44 @@ async function* dispatchCouncil(userMessage, options) {
21236
21921
  );
21237
21922
  }
21238
21923
  if (!userMessage || userMessage.trim().length === 0) {
21239
- 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
+ );
21240
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
+ });
21241
21937
  const config2 = {
21242
21938
  apiKey: options.apiKey,
21243
21939
  provider: options.provider ?? "openai-compatible",
21244
21940
  model: options.model,
21245
- councilSize: options.councilSize ?? 3,
21941
+ councilSize,
21246
21942
  debateMode: options.debateMode ?? false,
21943
+ runMode,
21247
21944
  ragContext: options.ragContext ?? "",
21248
21945
  workspaceContext: options.workspaceContext ?? "",
21249
21946
  providerStream: options.providerStream,
21250
21947
  eventBus: options.eventBus,
21251
21948
  sessionId: options.sessionId,
21252
- // Default to the workspace tool registry (CLI standalone has no
21253
- // AnathemaBrain Electron ctx). Pass `tools: undefined` via options
21254
- // to skip workspace wiring (e.g. in tests).
21255
- tools: options.tools ?? (options.disableWorkspaceTools ? void 0 : createWorkspaceToolRegistry(createWorkspaceContext())),
21949
+ tools: options.tools ?? (options.disableWorkspaceTools ? void 0 : createWorkspaceToolRegistry(createWorkspaceContext(projectRoot))),
21256
21950
  maxToolCallsPerTurn: options.maxToolCallsPerTurn,
21257
21951
  feedbackStore: options.feedbackStore
21258
21952
  };
21953
+ if (!options.disableWorkspaceTools) {
21954
+ const { setWorkspaceStubs: setWorkspaceStubs2 } = await Promise.resolve().then(() => (init_skills2(), skills_exports));
21955
+ const {
21956
+ createWorkspaceContext: buildCtx,
21957
+ createWorkspaceStubs: buildStubs
21958
+ } = await Promise.resolve().then(() => (init_stubs(), stubs_exports));
21959
+ const wsCtx = buildCtx(projectRoot);
21960
+ setWorkspaceStubs2(buildStubs(wsCtx));
21961
+ }
21259
21962
  yield* runCouncilPure(userMessage, config2, {});
21260
21963
  }
21261
21964
  var CouncilDispatchError;
@@ -21263,6 +21966,8 @@ var init_councilDispatcher = __esm({
21263
21966
  "src/cli/councilDispatcher.ts"() {
21264
21967
  "use strict";
21265
21968
  init_council();
21969
+ init_councilConfig();
21970
+ init_planDetect();
21266
21971
  init_stubs();
21267
21972
  init_toolRegistry();
21268
21973
  CouncilDispatchError = class extends Error {
@@ -21282,13 +21987,13 @@ __export(agentsMd_exports, {
21282
21987
  serializeAgentsMd: () => serializeAgentsMd,
21283
21988
  updateAgentsMd: () => updateAgentsMd
21284
21989
  });
21285
- 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";
21286
21991
  import { createHash as createHash2 } from "node:crypto";
21287
- import { join as join6 } from "node:path";
21992
+ import { join as join7 } from "node:path";
21288
21993
  import { readFile } from "node:fs/promises";
21289
21994
  async function readPackageJson2(projectRoot) {
21290
- const path22 = join6(projectRoot, "package.json");
21291
- if (!existsSync11(path22)) return null;
21995
+ const path22 = join7(projectRoot, "package.json");
21996
+ if (!existsSync12(path22)) return null;
21292
21997
  try {
21293
21998
  return JSON.parse(await readFile(path22, "utf8"));
21294
21999
  } catch {
@@ -21312,8 +22017,8 @@ async function genTechStack(ctx) {
21312
22017
  ].join("\n");
21313
22018
  }
21314
22019
  async function genDecisions(ctx) {
21315
- const decisionsDir = join6(ctx.rootDir, "decisions");
21316
- if (!existsSync11(decisionsDir)) return "_No ADRs yet._";
22020
+ const decisionsDir = join7(ctx.rootDir, "decisions");
22021
+ if (!existsSync12(decisionsDir)) return "_No ADRs yet._";
21317
22022
  const files = ctx.storage.listMarkdown(decisionsDir).sort();
21318
22023
  const accepted = [];
21319
22024
  const proposed = [];
@@ -21338,9 +22043,9 @@ async function genDecisions(ctx) {
21338
22043
  }
21339
22044
  async function genConventions(ctx) {
21340
22045
  const lines = [];
21341
- const claudeMd = join6(ctx.projectRoot, "CLAUDE.MD");
21342
- if (existsSync11(claudeMd)) {
21343
- const content = readFileSync8(claudeMd, "utf8");
22046
+ const claudeMd = join7(ctx.projectRoot, "CLAUDE.MD");
22047
+ if (existsSync12(claudeMd)) {
22048
+ const content = readFileSync9(claudeMd, "utf8");
21344
22049
  const match = content.match(/## Architecture rules[\s\S]+?(?=\n## |\n*$)/);
21345
22050
  if (match) {
21346
22051
  lines.push('<!-- Extracted from CLAUDE.MD "Architecture rules" -->');
@@ -21372,9 +22077,9 @@ async function genBuild(ctx) {
21372
22077
  ].join("\n");
21373
22078
  }
21374
22079
  async function genOpenQuestions(ctx) {
21375
- const path22 = join6(ctx.rootDir, "risks.md");
21376
- if (!existsSync11(path22)) return "_No open questions._";
21377
- 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");
21378
22083
  const lines = content.split("\n");
21379
22084
  const questions = [];
21380
22085
  let currentTitle = "";
@@ -21448,9 +22153,9 @@ function titleCase(id) {
21448
22153
  return id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
21449
22154
  }
21450
22155
  async function updateAgentsMd(ctx, projectRoot) {
21451
- const agentsPath = join6(projectRoot, "AGENTS.MD");
21452
- if (existsSync11(agentsPath)) {
21453
- const content = readFileSync8(agentsPath, "utf8");
22156
+ const agentsPath = join7(projectRoot, "AGENTS.MD");
22157
+ if (existsSync12(agentsPath)) {
22158
+ const content = readFileSync9(agentsPath, "utf8");
21454
22159
  const hasAnyMarker = AUTO_SECTIONS.some((id) => content.includes(MARKER_OPEN(id)));
21455
22160
  if (!hasAnyMarker) {
21456
22161
  return {
@@ -21465,8 +22170,8 @@ async function updateAgentsMd(ctx, projectRoot) {
21465
22170
  newSections.set(id, await GENERATORS[id](ctx));
21466
22171
  }
21467
22172
  let manualContent = "";
21468
- if (existsSync11(agentsPath)) {
21469
- const { manualBlocks } = parseAgentsMd(readFileSync8(agentsPath, "utf8"));
22173
+ if (existsSync12(agentsPath)) {
22174
+ const { manualBlocks } = parseAgentsMd(readFileSync9(agentsPath, "utf8"));
21470
22175
  manualContent = manualBlocks.after;
21471
22176
  } else {
21472
22177
  const projectName2 = projectName(projectRoot);
@@ -21482,7 +22187,7 @@ async function updateAgentsMd(ctx, projectRoot) {
21482
22187
  ""
21483
22188
  ].join("\n");
21484
22189
  }
21485
- const oldContent = existsSync11(agentsPath) ? readFileSync8(agentsPath, "utf8") : "";
22190
+ const oldContent = existsSync12(agentsPath) ? readFileSync9(agentsPath, "utf8") : "";
21486
22191
  const { sections: oldSections } = parseAgentsMd(oldContent);
21487
22192
  const changedSections = [];
21488
22193
  for (const id of AUTO_SECTIONS) {
@@ -21524,36 +22229,229 @@ var init_agentsMd = __esm({
21524
22229
  }
21525
22230
  });
21526
22231
 
22232
+ // src/cli/workspace/completeDesign.ts
22233
+ function curatedTasksForPhase(phase) {
22234
+ const label = (phase.name ?? phase.id).trim();
22235
+ const scope = phase.description?.trim() ? ` Scope: ${phase.description.trim()}` : "";
22236
+ return [
22237
+ {
22238
+ title: `Specify ${label} deliverables`,
22239
+ description: `Turn the council documents for "${label}" into a concrete specification: enumerate deliverables, interfaces, constraints, and open decisions, grounding each item in the ADRs and design docs already present in .zelari/.${scope}`,
22240
+ fileRefs: [".zelari/docs/", ".zelari/decisions/"],
22241
+ acceptance: [
22242
+ `A written spec for "${label}" exists and references at least one ADR or design doc`,
22243
+ "Every deliverable in the spec has an owner artifact (file, doc, or component) named"
22244
+ ],
22245
+ qaScenario: `Open the spec for "${label}" and confirm each deliverable maps to a concrete artifact and no open decision is left unresolved.`,
22246
+ priority: "high"
22247
+ },
22248
+ {
22249
+ title: `Implement ${label} deliverables`,
22250
+ description: `Produce the artifacts the "${label}" phase promises, following the spec task of this phase and the decisions recorded in .zelari/decisions/.${scope}`,
22251
+ fileRefs: ["src/"],
22252
+ acceptance: [
22253
+ `Every deliverable listed in the "${label}" spec exists and builds/renders without errors`,
22254
+ "No implementation contradicts an accepted ADR"
22255
+ ],
22256
+ qaScenario: `Walk the "${label}" spec top to bottom and check each deliverable off against the produced artifact.`,
22257
+ priority: "high"
22258
+ },
22259
+ {
22260
+ title: `Verify ${label} exit criteria`,
22261
+ description: `Run the QA pass for "${label}": execute the QA scenarios of the phase tasks, check the phase row in the synthesis green-light checklist, and record any gaps as follow-up tasks.${scope}`,
22262
+ fileRefs: [".zelari/docs/synthesis.md"],
22263
+ acceptance: [
22264
+ `The "${label}" row of the synthesis green-light checklist passes`,
22265
+ "All QA scenarios of this phase have been executed with recorded outcomes"
22266
+ ],
22267
+ qaScenario: `Open .zelari/docs/synthesis.md, locate the "${label}" checklist row, and confirm every criterion is checked with evidence.`,
22268
+ priority: "medium"
22269
+ }
22270
+ ];
22271
+ }
22272
+ async function runBuiltinCompleteDesign(ctx) {
22273
+ const summary = readPlanSummary(ctx);
22274
+ if (summary.phases.length === 0) {
22275
+ return { ran: false, tasksAdded: 0, milestonesAdded: 0, reason: "plan has no phases" };
22276
+ }
22277
+ const stubs = createWorkspaceStubs(ctx);
22278
+ const createTask = stubs.find((s) => s.name === "createTask");
22279
+ const createMilestone = stubs.find((s) => s.name === "createMilestone");
22280
+ const stubCtx = ctx;
22281
+ let tasksAdded = 0;
22282
+ for (const phase of summary.phases) {
22283
+ const existingTasks = summary.tasks.filter((t) => t.phaseId === phase.id);
22284
+ if (existingTasks.length >= MIN_TASKS_PER_PHASE) continue;
22285
+ const existingTitles = new Set(existingTasks.map((t) => t.name ?? ""));
22286
+ const templates = curatedTasksForPhase(phase).filter((t) => !existingTitles.has(t.title)).slice(0, MIN_TASKS_PER_PHASE - existingTasks.length);
22287
+ for (const t of templates) {
22288
+ await createTask.execute(
22289
+ {
22290
+ phaseId: phase.id,
22291
+ title: t.title,
22292
+ description: t.description,
22293
+ fileRefs: t.fileRefs,
22294
+ acceptance: t.acceptance,
22295
+ qaScenario: t.qaScenario,
22296
+ priority: t.priority
22297
+ },
22298
+ stubCtx
22299
+ );
22300
+ tasksAdded += 1;
22301
+ }
22302
+ }
22303
+ let milestonesAdded = 0;
22304
+ if (summary.milestones.length === 0) {
22305
+ await createMilestone.execute(
22306
+ {
22307
+ title: "v0.1.0 design-complete",
22308
+ description: "All design-phase artifacts (phases, tasks, ADRs, design docs, risks, synthesis) exist and the green-light checklist passes.",
22309
+ targetVersion: "v0.1.0"
22310
+ },
22311
+ stubCtx
22312
+ );
22313
+ milestonesAdded = 1;
22314
+ }
22315
+ return { ran: true, tasksAdded, milestonesAdded };
22316
+ }
22317
+ var MIN_TASKS_PER_PHASE;
22318
+ var init_completeDesign = __esm({
22319
+ "src/cli/workspace/completeDesign.ts"() {
22320
+ "use strict";
22321
+ init_stubs();
22322
+ MIN_TASKS_PER_PHASE = 3;
22323
+ }
22324
+ });
22325
+
21527
22326
  // src/cli/workspace/postCouncilHook.ts
21528
22327
  var postCouncilHook_exports = {};
21529
22328
  __export(postCouncilHook_exports, {
22329
+ runCompleteDesignPostProcessor: () => runCompleteDesignPostProcessor,
21530
22330
  runPostCouncilHook: () => runPostCouncilHook
21531
22331
  });
21532
- async function runPostCouncilHook(ctx) {
21533
- if (process.env["ZELARI_AGENTS_MD"] === "0") {
21534
- return { ran: false, changed: false, sections: [], reason: "ZELARI_AGENTS_MD=0 (disabled)" };
21535
- }
21536
- try {
21537
- const result = await updateAgentsMd(ctx, ctx.projectRoot);
22332
+ import { spawn as spawn3 } from "node:child_process";
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") {
21538
22337
  return {
21539
- ran: true,
21540
- changed: result.changed,
21541
- sections: result.sections,
21542
- ...result.reason ? { reason: result.reason } : {}
22338
+ ran: false,
22339
+ reason: "implementation mode (complete-design skipped)"
21543
22340
  };
21544
- } catch (err) {
22341
+ }
22342
+ if (process.env["ZELARI_COMPLETE_DESIGN"] === "0") {
22343
+ return { ran: false, reason: "ZELARI_COMPLETE_DESIGN=0 (disabled)" };
22344
+ }
22345
+ const planJsonPath2 = join8(ctx.rootDir, "plan.json");
22346
+ const scriptPath = join8(ctx.projectRoot, "complete-design.mjs");
22347
+ if (!existsSync13(planJsonPath2)) {
21545
22348
  return {
22349
+ ran: false,
22350
+ reason: ".zelari/plan.json missing (not design-phase)"
22351
+ };
22352
+ }
22353
+ let phaseCount = 0;
22354
+ try {
22355
+ const parsed = JSON.parse(readFileSync10(planJsonPath2, "utf8"));
22356
+ phaseCount = Array.isArray(parsed.phases) ? parsed.phases.length : 0;
22357
+ } catch {
22358
+ return { ran: false, reason: ".zelari/plan.json corrupt" };
22359
+ }
22360
+ if (phaseCount === 0) {
22361
+ return { ran: false, reason: ".zelari/plan.json has no phases" };
22362
+ }
22363
+ if (!existsSync13(scriptPath)) {
22364
+ try {
22365
+ const builtin = await runBuiltinCompleteDesign(ctx);
22366
+ return {
22367
+ ran: builtin.ran,
22368
+ ...builtin.ran ? { exitCode: 0 } : {},
22369
+ output: `builtin complete-design: +${builtin.tasksAdded} tasks, +${builtin.milestonesAdded} milestones`,
22370
+ ...builtin.reason ? { reason: builtin.reason } : {}
22371
+ };
22372
+ } catch (err) {
22373
+ return {
22374
+ ran: true,
22375
+ exitCode: -1,
22376
+ reason: `builtin complete-design error: ${err instanceof Error ? err.message : String(err)}`
22377
+ };
22378
+ }
22379
+ }
22380
+ return await new Promise((resolveRun) => {
22381
+ const child = spawn3(process.execPath, [scriptPath], {
22382
+ cwd: ctx.projectRoot,
22383
+ stdio: ["ignore", "pipe", "pipe"],
22384
+ env: process.env
22385
+ });
22386
+ let stdout = "";
22387
+ let stderr = "";
22388
+ child.stdout?.on("data", (chunk) => {
22389
+ stdout += chunk.toString("utf8");
22390
+ });
22391
+ child.stderr?.on("data", (chunk) => {
22392
+ stderr += chunk.toString("utf8");
22393
+ });
22394
+ child.on("error", (err) => {
22395
+ resolveRun({
22396
+ ran: true,
22397
+ exitCode: -1,
22398
+ output: stderr,
22399
+ reason: `spawn error: ${err.message}`
22400
+ });
22401
+ });
22402
+ child.on("close", (code) => {
22403
+ const exitCode = code ?? -1;
22404
+ const ok = exitCode === 0;
22405
+ resolveRun({
22406
+ ran: true,
22407
+ exitCode,
22408
+ output: stdout + stderr,
22409
+ ...ok ? {} : { reason: `complete-design exited with code ${exitCode}` }
22410
+ });
22411
+ });
22412
+ });
22413
+ }
22414
+ async function runPostCouncilHook(ctx, options) {
22415
+ let agentsMdResult;
22416
+ if (process.env["ZELARI_AGENTS_MD"] === "0") {
22417
+ agentsMdResult = {
21546
22418
  ran: false,
21547
22419
  changed: false,
21548
22420
  sections: [],
21549
- reason: `error: ${err instanceof Error ? err.message : String(err)}`
22421
+ reason: "ZELARI_AGENTS_MD=0 (disabled)"
21550
22422
  };
22423
+ } else {
22424
+ try {
22425
+ const result = await updateAgentsMd(ctx, ctx.projectRoot);
22426
+ agentsMdResult = {
22427
+ ran: true,
22428
+ changed: result.changed,
22429
+ sections: result.sections,
22430
+ ...result.reason ? { reason: result.reason } : {}
22431
+ };
22432
+ } catch (err) {
22433
+ agentsMdResult = {
22434
+ ran: false,
22435
+ changed: false,
22436
+ sections: [],
22437
+ reason: `error: ${err instanceof Error ? err.message : String(err)}`
22438
+ };
22439
+ }
21551
22440
  }
22441
+ const completeDesign = await runCompleteDesignPostProcessor(ctx, options);
22442
+ return {
22443
+ ran: agentsMdResult.ran || completeDesign.ran,
22444
+ changed: agentsMdResult.changed,
22445
+ sections: agentsMdResult.sections,
22446
+ ...agentsMdResult.reason ? { reason: agentsMdResult.reason } : {},
22447
+ completeDesign
22448
+ };
21552
22449
  }
21553
22450
  var init_postCouncilHook = __esm({
21554
22451
  "src/cli/workspace/postCouncilHook.ts"() {
21555
22452
  "use strict";
21556
22453
  init_agentsMd();
22454
+ init_completeDesign();
21557
22455
  }
21558
22456
  });
21559
22457
 
@@ -21564,8 +22462,8 @@ __export(councilFeedback_exports, {
21564
22462
  });
21565
22463
  import {
21566
22464
  promises as fs11,
21567
- existsSync as existsSync12,
21568
- readFileSync as readFileSync9,
22465
+ existsSync as existsSync14,
22466
+ readFileSync as readFileSync11,
21569
22467
  writeFileSync as writeFileSync8,
21570
22468
  mkdirSync as mkdirSync8
21571
22469
  } from "node:fs";
@@ -21673,9 +22571,9 @@ var init_councilFeedback = __esm({
21673
22571
  }
21674
22572
  // --- persistence ---------------------------------------------------------
21675
22573
  load() {
21676
- if (!existsSync12(this.file)) return;
22574
+ if (!existsSync14(this.file)) return;
21677
22575
  try {
21678
- const raw = readFileSync9(this.file, "utf-8");
22576
+ const raw = readFileSync11(this.file, "utf-8");
21679
22577
  const parsed = JSON.parse(raw);
21680
22578
  if (parsed && Array.isArray(parsed.entries)) {
21681
22579
  this.entries = parsed.entries.filter(
@@ -21721,7 +22619,7 @@ __export(updater_exports, {
21721
22619
  performUpdate: () => performUpdate
21722
22620
  });
21723
22621
  import { createRequire } from "node:module";
21724
- import { spawn as spawn3 } from "node:child_process";
22622
+ import { spawn as spawn4 } from "node:child_process";
21725
22623
  import path16 from "node:path";
21726
22624
  import { fileURLToPath } from "node:url";
21727
22625
  function getCurrentVersion() {
@@ -21786,15 +22684,13 @@ async function checkForUpdate(fetcher = fetch, registryUrl) {
21786
22684
  updateAvailable: cmp < 0
21787
22685
  };
21788
22686
  }
21789
- async function performUpdate(packageName = "zelari-code", executor = spawn3) {
22687
+ async function performUpdate(packageName = "zelari-code", executor = spawn4) {
21790
22688
  return new Promise((resolve) => {
21791
22689
  const args = ["install", "-g", `${packageName}@latest`];
21792
22690
  let stdout = "";
21793
22691
  let stderr = "";
21794
- const child = executor("npm", args, {
21795
- stdio: ["ignore", "pipe", "pipe"],
21796
- shell: process.platform === "win32"
21797
- });
22692
+ const stdio = ["ignore", "pipe", "pipe"];
22693
+ const child = process.platform === "win32" ? executor(buildCmdLine("npm", args), { stdio, shell: true }) : executor("npm", args, { stdio });
21798
22694
  child.stdout?.on("data", (chunk) => {
21799
22695
  stdout += chunk.toString();
21800
22696
  });
@@ -21824,6 +22720,7 @@ var require2, __dirname2, REGISTRY_URL;
21824
22720
  var init_updater = __esm({
21825
22721
  "src/cli/updater.ts"() {
21826
22722
  "use strict";
22723
+ init_cmdline();
21827
22724
  require2 = createRequire(import.meta.url);
21828
22725
  __dirname2 = path16.dirname(fileURLToPath(import.meta.url));
21829
22726
  REGISTRY_URL = "https://registry.npmjs.org/zelari-code/latest";
@@ -21831,12 +22728,12 @@ var init_updater = __esm({
21831
22728
  });
21832
22729
 
21833
22730
  // src/cli/main.ts
21834
- import React9 from "react";
22731
+ import React11 from "react";
21835
22732
  import { render } from "ink";
21836
22733
 
21837
22734
  // src/cli/app.tsx
21838
- import React6, { useState as useState4, useMemo, useCallback as useCallback5 } from "react";
21839
- 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";
21840
22737
 
21841
22738
  // src/cli/components/InputBar.tsx
21842
22739
  import React from "react";
@@ -22106,68 +23003,22 @@ function StreamingTail(m) {
22106
23003
  import React5 from "react";
22107
23004
  import { Box as Box5, Text as Text5 } from "ink";
22108
23005
 
22109
- // src/cli/modelPricing.ts
22110
- var PRICES_PER_MILLION = {
22111
- // xAI Grok
22112
- "grok-4": { input: 3, output: 15 },
22113
- "grok-4-fast": { input: 0.2, output: 0.5 },
22114
- "grok-3": { input: 3, output: 15 },
22115
- "grok-3-mini": { input: 0.3, output: 0.5 },
22116
- "grok-2-vision": { input: 2, output: 10 },
22117
- // GLM / Z.AI
22118
- "glm-4.6": { input: 0.6, output: 2.2 },
22119
- "glm-4.5": { input: 0.5, output: 2 },
22120
- "glm-4.5-air": { input: 0.1, output: 0.6 },
22121
- "glm-z1": { input: 0.5, output: 2 },
22122
- // MiniMax
22123
- "MiniMax-M2.5": { input: 0.2, output: 1.1 },
22124
- "MiniMax-M2": { input: 0.2, output: 1.1 },
22125
- "MiniMax-M2-her": { input: 0.3, output: 1.2 },
22126
- // OpenAI (for openai-compatible fallback)
22127
- "gpt-4o": { input: 2.5, output: 10 },
22128
- "gpt-4o-mini": { input: 0.15, output: 0.6 },
22129
- "gpt-4-turbo": { input: 10, output: 30 },
22130
- "o1-preview": { input: 15, output: 60 },
22131
- "o1-mini": { input: 3, output: 12 }
22132
- };
22133
- var DEFAULT_RATE = { input: 1, output: 3 };
22134
- function envOverride(model) {
22135
- const key = `ANATHEMA_PRICE_${model.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
22136
- const raw = process.env[key] ?? process.env.ANATHEMA_PRICE_DEFAULT;
22137
- return parseRateOverride(raw);
22138
- }
22139
- function parseRateOverride(raw) {
22140
- if (!raw) return null;
22141
- const [inStr, outStr] = raw.split("/");
22142
- const input = Number.parseFloat(inStr);
22143
- const output = outStr !== void 0 ? Number.parseFloat(outStr) : input;
22144
- if (!Number.isFinite(input) || !Number.isFinite(output)) return null;
22145
- return { input, output };
22146
- }
22147
- function getModelRate(model) {
22148
- if (!model) return DEFAULT_RATE;
22149
- return envOverride(model) ?? PRICES_PER_MILLION[model] ?? DEFAULT_RATE;
22150
- }
22151
- function calculateCost(model, promptTokens, completionTokens) {
22152
- if (!Number.isFinite(promptTokens) || promptTokens < 0) return 0;
22153
- if (!Number.isFinite(completionTokens) || completionTokens < 0) return 0;
22154
- const rate = getModelRate(model);
22155
- const inputCost = promptTokens / 1e6 * rate.input;
22156
- const outputCost = completionTokens / 1e6 * rate.output;
22157
- return Number((inputCost + outputCost).toFixed(6));
22158
- }
22159
- function formatCost(usd) {
22160
- if (!Number.isFinite(usd) || usd < 0) return "$0.0000";
22161
- if (usd === 0) return "$0.0000";
22162
- if (usd < 1e-4) return "<$0.0001";
22163
- return `$${usd.toFixed(4)}`;
22164
- }
22165
- function formatTokens(tokens) {
22166
- if (!Number.isFinite(tokens) || tokens < 0) return "0";
22167
- if (tokens < 1e3) return `${tokens}`;
22168
- if (tokens < 1e6) return `${(tokens / 1e3).toFixed(1)}k`;
22169
- if (tokens < 1e9) return `${(tokens / 1e6).toFixed(2)}M`;
22170
- 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}`;
22171
23022
  }
22172
23023
 
22173
23024
  // src/cli/components/StatusBar.tsx
@@ -22176,622 +23027,369 @@ function StatusBar({
22176
23027
  provider,
22177
23028
  sessionId,
22178
23029
  sessionActive,
22179
- totalTokens,
22180
- totalCostUsd,
22181
23030
  queueCount = 0,
22182
- busy = false
23031
+ busy = false,
23032
+ mode = "agent",
23033
+ cwd,
23034
+ elapsedMs = null,
23035
+ lastMs = null
22183
23036
  }) {
22184
- const hasCost = typeof totalCostUsd === "number" && totalCostUsd > 0;
22185
- const costStr = hasCost ? formatCost(totalCostUsd) : "$0.0000";
22186
- const tokStr = formatTokens(totalTokens ?? 0);
22187
- 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
+ );
22188
23086
  }
22189
-
22190
- // src/cli/slashCommands.ts
22191
- function parseSlashCommand(text) {
22192
- const trimmed = text.trim();
22193
- if (!trimmed.startsWith("/")) return null;
22194
- const parts = trimmed.slice(1).split(/\s+/);
22195
- const command = parts[0];
22196
- const args = parts.slice(1);
22197
- 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"))));
22198
23090
  }
22199
- function expandSkillTemplate(skill, options = {}) {
22200
- const userInput = options.input?.trim() ?? "";
22201
- const prompt = userInput ? `${skill.systemPromptFragment}
22202
23091
 
22203
- ## User input
22204
- ${userInput}` : `${skill.systemPromptFragment}
23092
+ // src/cli/app.tsx
23093
+ init_skills2();
22205
23094
 
22206
- ## User input
22207
- (Please provide the task description or input for this skill.)`;
22208
- return {
22209
- skillId: skill.id,
22210
- prompt,
22211
- requiredRoles: skill.requiredRoles,
22212
- requiredTools: skill.requiredTools,
22213
- estimatedCost: skill.estimatedCost
22214
- };
22215
- }
22216
- function formatSkillList(skills) {
22217
- if (skills.length === 0) return "(no skills available)";
22218
- const byCategory = /* @__PURE__ */ new Map();
22219
- for (const skill of skills) {
22220
- const list = byCategory.get(skill.category) ?? [];
22221
- list.push(skill);
22222
- 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
+ });
22223
23113
  }
22224
- const lines = ["Available skills:"];
22225
- for (const [category, list] of [...byCategory.entries()].sort()) {
22226
- lines.push(`
22227
- [${category}]`);
22228
- for (const skill of list) {
22229
- const costLabel = skill.estimatedCost;
22230
- 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;
22231
23142
  }
22232
23143
  }
22233
- 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));
22234
23151
  }
22235
- function handleSlashCommand(text, availableSkills) {
22236
- const parsed = parseSlashCommand(text);
22237
- if (!parsed) {
22238
- 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 {
22239
23167
  }
22240
- const { command, args } = parsed;
22241
- switch (command) {
22242
- case "help":
22243
- return {
22244
- handled: true,
22245
- kind: "help",
22246
- message: `Available commands:
22247
- /login <provider> \u2014 authenticate with provider (grok, minimax, glm, custom)
22248
- /model <name> \u2014 switch the active model
22249
- /model refresh \u2014 re-discover models for the active provider (v3-U)
22250
- /models \u2014 list discovered models for the active provider (v3-U)
22251
- /models refresh \u2014 re-discover models for the active provider (v3-U)
22252
- /provider <name> \u2014 switch the active provider
22253
- /provider custom <baseUrl> \u2014 point the active provider at a self-hosted endpoint (Ollama, LM Studio, vLLM, ...)
22254
- /provider custom clear \u2014 clear the custom endpoint override
22255
- /skill <name> [input] \u2014 invoke a skill (autocomplete with /skill <TAB>)
22256
- /skill-stats [name] \u2014 show invocation stats (success rate, avg duration, total tokens)
22257
- /council <input> \u2014 invoke the multi-agent council on input
22258
- /council-feedback <memberId> <1-5> [note] \u2014 rate a council member for future ranking (Task I.2)
22259
- /promote-member <memberId> \u2014 promote a council member to a standalone skill (v3-K)
22260
- /update [--yes|-y] \u2014 check for zelari-code updates; --yes performs the update (v3-N)
22261
- /steer <text> \u2014 enqueue a follow-up prompt on the active run (Task 18.2)
22262
- /steer --interrupt <text> \u2014 cancel current run + enqueue <text> for next dispatch (Task C.3.2)
22263
- /compact \u2014 compact the session transcript
22264
- /clear \u2014 clear the visible transcript (session is preserved)
22265
- /sessions \u2014 list past sessions
22266
- /resume <id> \u2014 load a past session
22267
- /branch <name> \u2014 snapshot the current session into a new branch
22268
- /branches \u2014 list branches
22269
- /checkout <name> \u2014 switch the active branch
22270
- /new \u2014 start a fresh session
22271
- /diff [--staged] \u2014 show uncommitted changes (or staged with --staged)
22272
- /undo [--yes] \u2014 revert working-tree changes (destructive! requires --yes)
22273
- /help \u2014 show this help
22274
- /exit \u2014 exit the CLI
22275
-
22276
- ${formatSkillList(availableSkills)}`
22277
- };
22278
- case "exit":
22279
- return { handled: true, kind: "exit", message: "Goodbye." };
22280
- case "clear":
22281
- return { handled: true, kind: "clear", message: "Transcript cleared." };
22282
- case "login": {
22283
- const provider = args[0];
22284
- if (!provider) {
22285
- return { handled: true, kind: "login", message: "Usage: /login <provider> [key] (or /login <provider> for OAuth if supported)" };
22286
- }
22287
- const key = args.slice(1).join(" ").trim();
22288
- if (!key) {
22289
- if (provider === "grok") {
22290
- return { handled: true, kind: "login_oauth", provider };
22291
- }
22292
- return {
22293
- handled: true,
22294
- kind: "login",
22295
- provider,
22296
- message: `[login] no key supplied \u2014 set ${provider.toUpperCase()}_API_KEY env or pass the key: /login ${provider} <key>`
22297
- };
22298
- }
22299
- return { handled: true, kind: "login", provider, loginKey: key };
22300
- }
22301
- case "model": {
22302
- const model = args[0];
22303
- if (!model) {
22304
- return {
22305
- handled: true,
22306
- kind: "model_show",
22307
- message: "Usage: /model <name> to change, /model to show current"
22308
- };
22309
- }
22310
- if (model === "refresh") {
22311
- return { handled: true, kind: "model_refresh" };
22312
- }
22313
- return { handled: true, kind: "model_set", model };
22314
- }
22315
- case "models": {
22316
- const sub = args[0];
22317
- if (sub === "refresh") {
22318
- return { handled: true, kind: "models_refresh" };
22319
- }
22320
- return { handled: true, kind: "models_list" };
22321
- }
22322
- case "provider": {
22323
- const subcommand = args[0];
22324
- if (!subcommand) {
22325
- return {
22326
- handled: true,
22327
- kind: "provider_list",
22328
- 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"
22329
- };
22330
- }
22331
- if (subcommand === "custom") {
22332
- const target = args[1];
22333
- if (!target || target === "show") {
22334
- return {
22335
- handled: true,
22336
- kind: "provider_custom",
22337
- 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"
22338
- };
22339
- }
22340
- if (target === "clear") {
22341
- return {
22342
- handled: true,
22343
- kind: "provider_custom",
22344
- customClear: true
22345
- };
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);
22346
23199
  }
22347
- const url2 = args.slice(1).join(" ").trim();
22348
- return {
22349
- handled: true,
22350
- kind: "provider_custom",
22351
- customEndpoint: url2
22352
- };
22353
- }
22354
- const providerId = subcommand;
22355
- const sub = args[1];
22356
- if (sub === "refresh") {
22357
- return { handled: true, kind: "provider_refresh", provider: providerId };
23200
+ } catch {
22358
23201
  }
22359
- if (sub === "status") {
22360
- return { handled: true, kind: "provider_status", provider: providerId };
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."
22361
23327
  }
22362
- return { handled: true, kind: "provider_set", provider: subcommand };
22363
23328
  }
22364
- case "branch": {
22365
- const name = args[0];
22366
- if (!name) {
22367
- return {
22368
- handled: true,
22369
- kind: "branch_create",
22370
- message: "Usage: /branch <name> \u2014 snapshots the current session into a new branch"
22371
- };
22372
- }
22373
- return { handled: true, kind: "branch_create", branchName: name };
22374
- }
22375
- case "branches":
22376
- return { handled: true, kind: "branch_list" };
22377
- case "checkout": {
22378
- const name = args[0];
22379
- if (!name) {
22380
- return {
22381
- handled: true,
22382
- kind: "branch_checkout",
22383
- message: "Usage: /checkout <name> \u2014 switch the active branch"
22384
- };
22385
- }
22386
- return { handled: true, kind: "branch_checkout", branchName: name };
22387
- }
22388
- case "compact": {
22389
- let compactThreshold;
22390
- let compactKeepRecent;
22391
- for (let i = 0; i < args.length; i++) {
22392
- const a = args[i];
22393
- if (a === "--threshold" || a === "-t") {
22394
- const v = Number.parseInt(args[++i] ?? "", 10);
22395
- if (Number.isFinite(v) && v > 0) compactThreshold = v;
22396
- } else if (a === "--keep" || a === "-k") {
22397
- const v = Number.parseInt(args[++i] ?? "", 10);
22398
- if (Number.isFinite(v) && v > 0) compactKeepRecent = v;
22399
- }
22400
- }
22401
- return {
22402
- handled: true,
22403
- kind: "compact",
22404
- message: "Compacting session...",
22405
- compactThreshold,
22406
- compactKeepRecent
22407
- };
22408
- }
22409
- case "sessions":
22410
- return { handled: true, kind: "session" };
22411
- case "resume": {
22412
- const targetId = args[0];
22413
- if (!targetId) {
22414
- return { handled: true, kind: "resume", message: "Usage: /resume <id>" };
22415
- }
22416
- return {
22417
- handled: true,
22418
- kind: "resume",
22419
- targetSessionId: targetId,
22420
- message: `[resume] session ${targetId} ready \u2014 restart zelari-code to load`
22421
- };
22422
- }
22423
- case "new":
22424
- return {
22425
- handled: true,
22426
- kind: "new",
22427
- message: "[new] session reset \u2014 next prompt starts fresh"
22428
- };
22429
- case "council": {
22430
- const input = args.join(" ").trim();
22431
- if (!input) {
22432
- return {
22433
- handled: true,
22434
- kind: "council",
22435
- message: "Usage: /council <input> \u2014 invokes the multi-agent council on the input"
22436
- };
22437
- }
22438
- return { handled: true, kind: "council", councilInput: input };
22439
- }
22440
- case "council-feedback": {
22441
- const memberId = args[0];
22442
- const scoreStr = args[1];
22443
- if (!memberId || !scoreStr) {
22444
- return {
22445
- handled: true,
22446
- kind: "council_feedback",
22447
- message: "Usage: /council-feedback <memberId> <1-5> [note...] \u2014 rate a council member (e.g. /council-feedback sisyphus 4 great framing)"
22448
- };
22449
- }
22450
- const score = Number.parseInt(scoreStr, 10);
22451
- if (!Number.isInteger(score) || score < 1 || score > 5) {
22452
- return {
22453
- handled: true,
22454
- kind: "council_feedback",
22455
- message: `Invalid score "${scoreStr}" \u2014 must be an integer in [1, 5].`
22456
- };
22457
- }
22458
- const note = args.slice(2).join(" ").trim() || void 0;
22459
- return {
22460
- handled: true,
22461
- kind: "council_feedback",
22462
- feedbackMemberId: memberId,
22463
- feedbackScore: score,
22464
- ...note ? { feedbackNote: note } : {}
22465
- };
22466
- }
22467
- case "skill": {
22468
- const skillId = args[0];
22469
- if (!skillId) {
22470
- return {
22471
- handled: true,
22472
- kind: "skill",
22473
- skillError: "Usage: /skill <name> [input]",
22474
- message: formatSkillList(availableSkills)
22475
- };
22476
- }
22477
- const skill = availableSkills.find((s) => s.id === skillId);
22478
- if (!skill) {
22479
- return {
22480
- handled: true,
22481
- kind: "skill",
22482
- skillError: `Unknown skill: "${skillId}". Use /skill <TAB> for autocomplete.`,
22483
- message: formatSkillList(availableSkills)
22484
- };
22485
- }
22486
- const input = args.slice(1).join(" ");
22487
- const expandedSkill = expandSkillTemplate(skill, { input });
22488
- return { handled: true, kind: "skill", expandedSkill };
22489
- }
22490
- case "skill_stats": {
22491
- const skillId = args[0];
22492
- return {
22493
- handled: true,
22494
- kind: "skill_stats",
22495
- skillStatsSkillId: skillId,
22496
- message: skillId ? `Computing stats for skill "${skillId}"\u2026` : "Computing stats for all skills\u2026"
22497
- };
22498
- }
22499
- case "skill-compare": {
22500
- const id1 = args[0];
22501
- const id2 = args[1];
22502
- if (!id1 || !id2) {
22503
- return {
22504
- handled: true,
22505
- kind: "skill-compare",
22506
- message: "\u26A0 /skill-compare requires exactly 2 skill IDs (e.g. `/skill-compare debug refactor`)."
22507
- };
22508
- }
22509
- return {
22510
- handled: true,
22511
- kind: "skill-compare",
22512
- compareIds: [id1, id2],
22513
- message: `Comparing skills "${id1}" vs "${id2}"\u2026`
22514
- };
22515
- }
22516
- case "steer": {
22517
- let interrupt = false;
22518
- const filtered = [];
22519
- for (const a of args) {
22520
- if (a === "--interrupt" || a === "-i") {
22521
- interrupt = true;
22522
- } else {
22523
- filtered.push(a);
22524
- }
22525
- }
22526
- const text2 = filtered.join(" ").trim();
22527
- if (!text2) {
22528
- return {
22529
- handled: true,
22530
- kind: interrupt ? "steer_interrupt" : "steer",
22531
- message: "Usage: /steer [--interrupt|-i] <text> \u2014 enqueue (and optionally cancel) a follow-up prompt"
22532
- };
22533
- }
22534
- return {
22535
- handled: true,
22536
- kind: interrupt ? "steer_interrupt" : "steer",
22537
- steerText: text2
22538
- };
22539
- }
22540
- case "diff": {
22541
- const staged = args.includes("--staged") || args.includes("--cached");
22542
- return { handled: true, kind: "diff", diffStaged: staged ? true : void 0 };
22543
- }
22544
- case "promote-member": {
22545
- const memberId = args[0];
22546
- if (!memberId) {
22547
- return {
22548
- handled: true,
22549
- kind: "promote_member_error",
22550
- promoteMemberError: "Usage: /promote-member <memberId> \u2014 e.g. /promote-member geryon"
22551
- };
22552
- }
22553
- return { handled: true, kind: "promote_member", promoteMemberId: memberId };
22554
- }
22555
- case "update": {
22556
- let force = false;
22557
- for (const a of args) {
22558
- if (a === "--yes" || a === "-y") {
22559
- force = true;
22560
- }
22561
- }
22562
- if (args.length > 0 && !force) {
22563
- return {
22564
- handled: true,
22565
- kind: "update_usage",
22566
- 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)"
22567
- };
22568
- }
22569
- return {
22570
- handled: true,
22571
- kind: force ? "update_perform" : "update_check",
22572
- updateForce: force
22573
- };
22574
- }
22575
- case "undo": {
22576
- const confirmed = args.includes("--yes") || args.includes("-y");
22577
- if (confirmed) {
22578
- return { handled: true, kind: "undo_confirm", undoConfirmed: true };
22579
- }
22580
- return {
22581
- handled: true,
22582
- kind: "undo",
22583
- message: "\u26A0 /undo is DESTRUCTIVE \u2014 it reverts all unstaged modifications and unstages everything.\nUse `/undo --yes` (or `/undo -y`) to confirm."
22584
- };
22585
- }
22586
- case "workspace": {
22587
- const sub = args[0];
22588
- if (!sub || sub === "--help" || sub === "help") {
22589
- return {
22590
- handled: true,
22591
- kind: "workspace",
22592
- 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."
22593
- };
22594
- }
22595
- if (sub === "show") {
22596
- const what = args[1];
22597
- if (!what) {
22598
- return {
22599
- handled: true,
22600
- kind: "workspace_show",
22601
- workspaceWhat: "",
22602
- message: "Usage: /workspace show <plan|decisions|risks|agents|docs>"
22603
- };
22604
- }
22605
- if (!["plan", "decisions", "risks", "agents", "docs"].includes(what)) {
22606
- return {
22607
- handled: true,
22608
- kind: "workspace_show",
22609
- workspaceWhat: "",
22610
- message: `Unknown artifact "${what}". Available: plan | decisions | risks | agents | docs`
22611
- };
22612
- }
22613
- return {
22614
- handled: true,
22615
- kind: "workspace_show",
22616
- workspaceWhat: what
22617
- };
22618
- }
22619
- if (sub === "sync") {
22620
- return { handled: true, kind: "workspace_sync" };
22621
- }
22622
- if (sub === "reset") {
22623
- const force = args.includes("--yes") || args.includes("-y");
22624
- if (force) {
22625
- return { handled: true, kind: "workspace_reset", workspaceForce: true };
22626
- }
22627
- return {
22628
- handled: true,
22629
- kind: "workspace_reset",
22630
- message: "\u26A0 /workspace reset is DESTRUCTIVE \u2014 deletes the entire `.zelari/` directory.\nUse `/workspace reset --yes` to confirm."
22631
- };
22632
- }
22633
- return {
22634
- handled: true,
22635
- kind: "workspace",
22636
- message: `Unknown /workspace subcommand: "${sub}". Type /workspace for usage.`
22637
- };
22638
- }
22639
- default:
22640
- return {
22641
- handled: false,
22642
- kind: "unknown",
22643
- message: `Unknown command: /${command}. Type /help for available commands.`
22644
- };
22645
- }
22646
- }
22647
-
22648
- // src/cli/app.tsx
22649
- init_skills2();
22650
-
22651
- // packages/core/dist/agents/skills/builtin/debugging.js
22652
- init_skills();
22653
- var CLARIFICATION_PROTOCOL = `
22654
-
22655
- WHEN TO ASK THE USER (clarification):
22656
- 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:
22657
-
22658
- ---QUESTION---
22659
- { "question": "One focused question", "choices": ["Option A", "Option B", "Option C"], "context": "Why this matters" }
22660
- ---END---
22661
-
22662
- Rules for clarifications:
22663
- - Ask AT MOST ONE question per turn, and only when genuinely blocked.
22664
- - Prefer a small set of concrete "choices" (2-4). The user can still type a wave-in response.
22665
- - Do NOT ask for information that could be reasonably assumed or already in shared context.
22666
- - If you can proceed with a sound documented assumption, DO SO instead of asking.`;
22667
- var reproduceBug = {
22668
- id: "reproduce-bug",
22669
- version: "1.0.0",
22670
- name: "Reproduce Bug",
22671
- description: "Convert a bug report into a MINIMAL failing test. The test must fail without any code changes, then pass after the fix.",
22672
- category: "debug",
22673
- requiredRoles: ["minos", "pluton"],
22674
- requiredTools: ["read_file", "write_file"],
22675
- estimatedCost: "medium",
22676
- enabledByDefault: true,
22677
- builtin: true,
22678
- triggers: [
22679
- "Bug report without a minimal reproduction",
22680
- "Bug that only happens in production (not in tests)",
22681
- 'Bug that happens "sometimes" (intermittent)',
22682
- "Bug reported via screenshot or video, not repro steps",
22683
- "A complex multi-agent state race condition that is hard to verify without a test"
22684
- ],
22685
- antiPatterns: [
22686
- "Bug is in production AND has minimal repro steps already \u2014 skip to fix",
22687
- "Bug is in test code itself \u2014 fix the test, not the production code",
22688
- "Bug requires special hardware/network state \u2014 use a mock instead"
22689
- ],
22690
- requires: [],
22691
- relatedSkills: ["debug-with-rag", "root-cause-five-whys"],
22692
- tags: ["debug", "reproduction", "test", "minimal-example"],
22693
- examples: [
22694
- {
22695
- input: 'Bug report: "Council sometimes shows duplicate messages at the start of a session"',
22696
- output: {
22697
- minimalRepro: `// tests/regression/duplicate-messages.test.ts
22698
- import { runCouncilPure } from '../agents/councilApi.js';
22699
-
22700
- describe('council duplicate message bug', () => {
22701
- it('does not emit duplicate user message when session resumes', async () => {
22702
- const mockProvider = async function* () {
22703
- yield { kind: 'text', delta: 'first' };
22704
- yield { kind: 'finish', reason: 'stop' };
22705
- };
22706
- const harness = new AgentHarness({
22707
- model: 'test', provider: 'test',
22708
- messages: [
22709
- { role: 'user', content: 'hi' },
22710
- { role: 'user', content: 'hi' }, // duplicate from session resume
22711
- ],
22712
- tools: [],
22713
- providerStream: mockProvider,
22714
- });
22715
- const events = [];
22716
- for await (const e of harness.run()) events.push(e);
22717
- const userMessages = events.filter(e => e.type === 'message_delta').length;
22718
- expect(userMessages).toBeGreaterThan(0); // sanity
22719
- // The actual bug: duplicate is rendered twice
22720
- // After fix, expect exactly 1 user message rendered
22721
- });
22722
- });`,
22723
- reproSteps: [
22724
- "1. Set up session with duplicate user message in transcript",
22725
- "2. Run runCouncilPure",
22726
- "3. Observe duplicate message in output"
22727
- ],
22728
- whyMinimal: "Removed all agents except the first specialist (was: sisyphus+prometheus+hephaestus+oracle+chairman). Removed RAG context, workspace context. Kept only the duplication pattern."
22729
- }
22730
- }
22731
- ],
22732
- outputSchema: "{ minimalRepro: string; reproSteps: string[]; whyMinimal: string }",
22733
- systemPromptFragment: `You are converting a bug report into a minimal failing test.
22734
-
22735
- ## Methodology
22736
- 1. **Read the bug report** carefully: what's the exact symptom? When does it happen?
22737
- 2. **Strip to essentials**: remove every agent, tool, env var, user state that isn't strictly required.
22738
- 3. **Identify the trigger**: what ONE input causes the bug?
22739
- 4. **Write the failing test**: it must FAIL on the current (buggy) code.
22740
- 5. **Verify it fails**: the test must fail without any code changes.
22741
- 6. **After the fix lands**, the test must PASS.
22742
-
22743
- ## Output format (JSON-typed)
22744
- - minimalRepro: string (the test code, runnable as-is)
22745
- - reproSteps: string[] (3-7 numbered steps to reproduce manually)
22746
- - whyMinimal: string (what you stripped + why)
22747
-
22748
- ## Minimal-repro principles
22749
- - **One assertion per test** \u2014 easier to debug when the test fails
22750
- - **Deterministic** \u2014 no flaky timing, no random data
22751
- - **No external dependencies** \u2014 use mocks for network/DB
22752
- - **Fast** \u2014 under 100ms if possible
22753
- - **Independent** \u2014 doesn't depend on other tests' state
22754
-
22755
- Stay under 300 words.${CLARIFICATION_PROTOCOL}`
22756
- };
22757
- var debugWithRag = {
22758
- id: "debug-with-rag",
22759
- version: "1.0.0",
22760
- name: "Debug With RAG",
22761
- description: "Search the knowledge base for similar past bugs, read the stack trace, propose the most likely root cause and a concrete fix.",
22762
- category: "debug",
22763
- requiredRoles: ["minos", "pluton"],
22764
- requiredTools: ["searchRAG", "grep_content", "read_file"],
22765
- estimatedCost: "medium",
22766
- enabledByDefault: true,
22767
- builtin: true,
22768
- triggers: [
22769
- "A bug report with a stack trace or error message",
22770
- "A regression: feature worked before, now broken",
22771
- "A flaky test that fails intermittently",
22772
- "Performance regression: response time > 5s where it used to be < 500ms",
22773
- "A memory leak causing out-of-memory crashes"
22774
- ],
22775
- antiPatterns: [
22776
- "Bug is obvious from the error message (e.g. TypeError: cannot read property of undefined \u2014 fix directly)",
22777
- "No stack trace or repro steps \u2014 first ask the user for a minimal repro",
22778
- "A test is failing because the test itself is wrong \u2014 debug the test, not the code"
22779
- ],
22780
- requires: ["reproduce-bug"],
22781
- relatedSkills: ["root-cause-five-whys", "reproduce-bug"],
22782
- tags: ["debug", "rag", "stack-trace", "root-cause", "regression"],
22783
- examples: [
22784
- {
22785
- input: 'TypeError: Cannot read properties of undefined (reading "map") at Council.tsx:847',
22786
- output: {
22787
- 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.',
22788
- 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.",
22789
- 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.",
22790
- proposedFix: `function renderMessages() {
22791
- const messages = session.messages ?? [];
22792
- return messages.map(m => <Message key={m.id} {...m} />);
22793
- }`,
22794
- 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."
22795
23393
  }
22796
23394
  }
22797
23395
  ],
@@ -24698,7 +25296,7 @@ function defaultBaseDir() {
24698
25296
  }
24699
25297
 
24700
25298
  // src/cli/hooks/useSession.ts
24701
- import { useState, useRef, useCallback, useEffect } from "react";
25299
+ import { useState as useState3, useRef as useRef3, useCallback, useEffect as useEffect3 } from "react";
24702
25300
 
24703
25301
  // src/cli/sessionManager.ts
24704
25302
  import { promises as fs8, existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, unlinkSync, statSync } from "node:fs";
@@ -24978,20 +25576,20 @@ function completeTool(setFinalized, setLive, toolCallId, isError, durationMs, re
24978
25576
 
24979
25577
  // src/cli/hooks/useSession.ts
24980
25578
  function useSession() {
24981
- const [sessionId, setSessionId] = useState("");
24982
- const [messages, setMessages] = useState([]);
24983
- const [live, setLive] = useState(EMPTY_LIVE);
24984
- const [sessionActive, setSessionActive] = useState(false);
24985
- const writerRef = useRef(null);
24986
- const liveRef = useRef(EMPTY_LIVE);
24987
- 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(() => {
24988
25586
  liveRef.current = live;
24989
25587
  }, [live]);
24990
25588
  const resetTranscript = useCallback(() => {
24991
25589
  setMessages([]);
24992
25590
  setLive(EMPTY_LIVE);
24993
25591
  }, []);
24994
- useEffect(() => {
25592
+ useEffect3(() => {
24995
25593
  let cancelled = false;
24996
25594
  (async () => {
24997
25595
  await ensureSessionDir();
@@ -25085,7 +25683,7 @@ ${lines.join("\n")}`;
25085
25683
  }
25086
25684
 
25087
25685
  // src/cli/hooks/useChatTurn.ts
25088
- 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";
25089
25687
 
25090
25688
  // src/cli/metrics.ts
25091
25689
  import { promises as fs9, existsSync as existsSync5, statSync as statSync2, renameSync, appendFileSync, mkdirSync as mkdirSync4 } from "node:fs";
@@ -25866,6 +26464,57 @@ function finalizeStreamingAssistant(setMessages) {
25866
26464
  });
25867
26465
  }
25868
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));
26516
+ }
26517
+
25869
26518
  // src/cli/hooks/chatStats.ts
25870
26519
  function computeSessionStatsDelta(realUsage, userText, assistantContent, model, prev2) {
25871
26520
  const promptTokens = realUsage ? realUsage.promptTokens : Math.ceil(userText.length / 4);
@@ -25891,8 +26540,8 @@ function useChatTurn(params) {
25891
26540
  setLive,
25892
26541
  liveRef
25893
26542
  } = params;
25894
- const harnessRef = useRef2(null);
25895
- const [queueCount, setQueueCount] = useState2(0);
26543
+ const harnessRef = useRef4(null);
26544
+ const [queueCount, setQueueCount] = useState4(0);
25896
26545
  const useLiveModel = !!(setLive && liveRef);
25897
26546
  const dispatchPrompt = useCallback2(
25898
26547
  async (userText, opts) => {
@@ -25939,7 +26588,11 @@ function useChatTurn(params) {
25939
26588
  let workspaceSummary = null;
25940
26589
  let zelariReadHint = "";
25941
26590
  try {
25942
- 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));
25943
26596
  planSummary = buildPlanSummary2(cwd);
25944
26597
  workspaceSummary = buildWorkspaceSummary2(cwd);
25945
26598
  zelariReadHint = buildZelariReadHint2(cwd);
@@ -25963,7 +26616,9 @@ function useChatTurn(params) {
25963
26616
  if (wantedWorkspaceTools.size > 0) {
25964
26617
  const { createWorkspaceContext: createWorkspaceContext2 } = await Promise.resolve().then(() => (init_stubs(), stubs_exports));
25965
26618
  const { createWorkspaceToolRegistry: createWorkspaceToolRegistry2 } = await Promise.resolve().then(() => (init_toolRegistry(), toolRegistry_exports));
25966
- const wsRegistry = createWorkspaceToolRegistry2(createWorkspaceContext2(cwd));
26619
+ const wsRegistry = createWorkspaceToolRegistry2(
26620
+ createWorkspaceContext2(cwd)
26621
+ );
25967
26622
  for (const name of wantedWorkspaceTools) {
25968
26623
  const td = wsRegistry.get(name);
25969
26624
  if (td) toolRegistry.register(td);
@@ -26097,10 +26752,15 @@ function useChatTurn(params) {
26097
26752
  ...event.memberName ? { memberName: event.memberName } : {}
26098
26753
  });
26099
26754
  } else {
26100
- appendOrExtendStreamingAssistant(commitStreaming, streamContent, Date.now(), {
26101
- ...event.memberId ? { memberId: event.memberId } : {},
26102
- ...event.memberName ? { memberName: event.memberName } : {}
26103
- });
26755
+ appendOrExtendStreamingAssistant(
26756
+ commitStreaming,
26757
+ streamContent,
26758
+ Date.now(),
26759
+ {
26760
+ ...event.memberId ? { memberId: event.memberId } : {},
26761
+ ...event.memberName ? { memberName: event.memberName } : {}
26762
+ }
26763
+ );
26104
26764
  }
26105
26765
  } else if (event.type === "error") {
26106
26766
  flushStreaming();
@@ -26110,17 +26770,42 @@ function useChatTurn(params) {
26110
26770
  flushStreaming();
26111
26771
  if (useLiveModel) {
26112
26772
  finalizeStreaming(setMessages, setLive);
26113
- 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
+ );
26114
26780
  } else {
26115
26781
  finalizeStreamingAssistant(setMessages);
26116
- 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
+ );
26117
26789
  }
26118
26790
  streamContent = "";
26119
26791
  } else if (event.type === "tool_execution_end") {
26120
26792
  if (useLiveModel) {
26121
- 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
+ );
26122
26801
  } else {
26123
- 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
+ );
26124
26809
  }
26125
26810
  }
26126
26811
  }
@@ -26170,278 +26855,746 @@ function useChatTurn(params) {
26170
26855
  sessionId,
26171
26856
  writerRef,
26172
26857
  setMessages,
26173
- commitStreaming,
26174
- flushStreaming,
26175
- setBusy,
26176
- setQueueCount,
26177
- setLive,
26178
- liveRef
26179
- });
26180
- },
26181
- [
26182
- sessionId,
26183
- writerRef,
26184
- setMessages,
26185
- commitStreaming,
26186
- flushStreaming,
26187
- setBusy,
26188
- setQueueCount,
26189
- setLive,
26190
- liveRef
26191
- ]
26192
- );
26858
+ commitStreaming,
26859
+ flushStreaming,
26860
+ setBusy,
26861
+ setQueueCount,
26862
+ setLive,
26863
+ liveRef
26864
+ });
26865
+ },
26866
+ [
26867
+ sessionId,
26868
+ writerRef,
26869
+ setMessages,
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.)`;
26193
27153
  return {
26194
- dispatchPrompt,
26195
- dispatchCouncilPrompt,
26196
- harnessRef,
26197
- queueCount,
26198
- setQueueCount
27154
+ skillId: skill.id,
27155
+ prompt,
27156
+ requiredRoles: skill.requiredRoles,
27157
+ requiredTools: skill.requiredTools,
27158
+ estimatedCost: skill.estimatedCost
26199
27159
  };
26200
27160
  }
26201
- async function dispatchCouncilPromptImpl(text, deps) {
26202
- const {
26203
- sessionId,
26204
- writerRef,
26205
- setMessages,
26206
- commitStreaming,
26207
- flushStreaming,
26208
- setBusy,
26209
- setLive,
26210
- liveRef
26211
- } = deps;
26212
- const useLiveModel = !!(setLive && liveRef);
26213
- const envConfig = await providerFromEnv();
26214
- if (!envConfig) {
26215
- const active = resolveActiveProvider();
26216
- const spec = PROVIDERS.find((p3) => p3.id === active);
26217
- appendSystem(
26218
- setMessages,
26219
- `No API key for the active provider "${active}". Set ${spec?.envVar ?? "the provider API key env var"} or run /login ${active} before invoking /council.`
26220
- );
26221
- 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);
26222
27168
  }
26223
- setBusy(true);
26224
- const { dispatchCouncil: dispatchCouncil2 } = await Promise.resolve().then(() => (init_councilDispatcher(), councilDispatcher_exports));
26225
- const { createWorkspaceContext: createWorkspaceContext2, createWorkspaceStubs: createWorkspaceStubs2 } = await Promise.resolve().then(() => (init_stubs(), stubs_exports));
26226
- const { createWorkspaceToolRegistry: createWorkspaceToolRegistry2 } = await Promise.resolve().then(() => (init_toolRegistry(), toolRegistry_exports));
26227
- const { setWorkspaceStubs: setWorkspaceStubs2 } = await Promise.resolve().then(() => (init_skills2(), skills_exports));
26228
- const { runPostCouncilHook: runPostCouncilHook2 } = await Promise.resolve().then(() => (init_postCouncilHook(), postCouncilHook_exports));
26229
- const { buildWorkspaceSummary: buildWorkspaceSummary2, buildPlanSummary: buildPlanSummary2 } = await Promise.resolve().then(() => (init_workspaceSummary(), workspaceSummary_exports));
26230
- const { FeedbackStore: FeedbackStore2 } = await Promise.resolve().then(() => (init_councilFeedback(), councilFeedback_exports));
26231
- const { registry: councilToolRegistry } = createBuiltinToolRegistry();
26232
- const workspaceCtx = createWorkspaceContext2();
26233
- const workspaceReg = createWorkspaceToolRegistry2(workspaceCtx);
26234
- for (const name of workspaceReg.list()) {
26235
- const td = workspaceReg.get(name);
26236
- 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
+ }
26237
27177
  }
26238
- try {
26239
- const { registerMcpTools: registerMcpTools2 } = await Promise.resolve().then(() => (init_mcpManager(), mcpManager_exports));
26240
- const mcp = await registerMcpTools2(councilToolRegistry);
26241
- for (const w of mcp.warnings) appendSystem(setMessages, w);
26242
- } 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" };
26243
27184
  }
26244
- setWorkspaceStubs2(createWorkspaceStubs2(workspaceCtx));
26245
- const councilFeedbackStore = new FeedbackStore2();
26246
- let streamContent = "";
26247
- let streamMemberId = null;
26248
- const councilMaxToolCalls = (() => {
26249
- const raw = process.env.ZELARI_MAX_TOOL_CALLS;
26250
- const n = raw ? Number.parseInt(raw, 10) : 15;
26251
- return Number.isFinite(n) && n > 0 ? n : 15;
26252
- })();
26253
- let membersCompleted = 0;
26254
- let chairmanProducedOutput = false;
26255
- let consecutiveProviderErrors = 0;
26256
- let lastErrorMessage = "";
26257
- let councilAborted = false;
26258
- const PROVIDER_ERROR_ABORT_THRESHOLD = 2;
26259
- try {
26260
- for await (const event of dispatchCouncil2(text, {
26261
- apiKey: envConfig.apiKey,
26262
- model: envConfig.model,
26263
- provider: "openai-compatible",
26264
- providerStream: openaiCompatibleProvider(envConfig),
26265
- sessionId,
26266
- tools: councilToolRegistry,
26267
- feedbackStore: councilFeedbackStore,
26268
- // v0.7.2 (B2): give the council the same project awareness the
26269
- // single-prompt path has — cwd, tech stack, file layout, build scripts.
26270
- // Without this, members had no idea which project they were operating
26271
- // on and projected their identity onto the task.
26272
- // v0.7.3: append the existing plan (if any) so a follow-up /council
26273
- // continues it instead of re-planning from scratch.
26274
- workspaceContext: [
26275
- buildWorkspaceSummary2(process.cwd()),
26276
- buildPlanSummary2(process.cwd())
26277
- ].filter(Boolean).join("\n\n"),
26278
- maxToolCallsPerTurn: councilMaxToolCalls
26279
- })) {
26280
- if (councilAborted) {
26281
- if (writerRef.current) await writerRef.current.append(event);
26282
- 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)" };
26283
27231
  }
26284
- if (writerRef.current) {
26285
- await writerRef.current.append(event);
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" };
27264
+ }
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
+ };
26286
27275
  }
26287
- if (event.type === "message_delta") {
26288
- if (useLiveModel) {
26289
- const memberId = event.memberId ?? null;
26290
- if (memberId !== streamMemberId) {
26291
- flushStreaming();
26292
- finalizeStreaming(setMessages, setLive);
26293
- streamContent = "";
26294
- streamMemberId = memberId;
26295
- }
26296
- streamContent += event.delta;
26297
- setStreaming(commitStreaming, streamContent, event.ts, {
26298
- ...event.memberId ? { memberId: event.memberId } : {},
26299
- ...event.memberName ? { memberName: event.memberName } : {}
26300
- });
26301
- } else {
26302
- commitStreaming((prev2) => {
26303
- const last = prev2[prev2.length - 1];
26304
- if (last && last.role === "assistant" && last.id.startsWith("streaming-") && (last.memberId ?? null) === (event.memberId ?? null)) {
26305
- return [
26306
- ...prev2.slice(0, -1),
26307
- { ...last, content: last.content + event.delta }
26308
- ];
26309
- }
26310
- return [
26311
- ...prev2,
26312
- {
26313
- id: `streaming-${crypto.randomUUID()}`,
26314
- role: "assistant",
26315
- content: event.delta,
26316
- ts: event.ts,
26317
- ...event.memberId ? { memberId: event.memberId } : {},
26318
- ...event.memberName ? { memberName: event.memberName } : {}
26319
- }
26320
- ];
26321
- });
26322
- }
26323
- } else if (event.type === "message_end") {
26324
- flushStreaming();
26325
- if (useLiveModel) finalizeStreaming(setMessages, setLive);
26326
- else finalizeStreamingAssistant(setMessages);
26327
- streamContent = "";
26328
- streamMemberId = null;
26329
- membersCompleted++;
26330
- if (event.memberId === "lucifer" || event.memberName === "Lucifero") {
26331
- chairmanProducedOutput = true;
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
+ };
26332
27284
  }
26333
- } else if (event.type === "tool_execution_start") {
26334
- flushStreaming();
26335
- if (useLiveModel) {
26336
- finalizeStreaming(setMessages, setLive);
26337
- startTool(
26338
- setLive,
26339
- event.toolName,
26340
- event.toolCallId,
26341
- event.args,
26342
- event.ts
26343
- );
26344
- } else {
26345
- finalizeStreamingAssistant(setMessages);
26346
- appendToolStart(
26347
- setMessages,
26348
- event.toolName,
26349
- event.toolCallId,
26350
- event.args,
26351
- event.ts
26352
- );
27285
+ if (target === "clear") {
27286
+ return {
27287
+ handled: true,
27288
+ kind: "provider_custom",
27289
+ customClear: true
27290
+ };
26353
27291
  }
26354
- streamContent = "";
26355
- } else if (event.type === "tool_execution_end") {
26356
- if (useLiveModel) {
26357
- completeTool(
26358
- setMessages,
26359
- setLive,
26360
- event.toolCallId,
26361
- event.isError,
26362
- event.durationMs,
26363
- event.result
26364
- );
26365
- } else {
26366
- updateToolMessageEnd(
26367
- setMessages,
26368
- event.toolCallId,
26369
- event.isError,
26370
- event.durationMs,
26371
- event.result
26372
- );
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;
26373
27344
  }
26374
- } else if (event.type === "error") {
26375
- flushStreaming();
26376
- const memberTag = event.memberName ? ` \xB7 ${event.memberName}` : "";
26377
- appendSystem(
26378
- setMessages,
26379
- `[error${memberTag}] ${event.message}`,
26380
- event.ts
26381
- );
26382
- if (event.message === lastErrorMessage) {
26383
- consecutiveProviderErrors++;
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;
26384
27467
  } else {
26385
- consecutiveProviderErrors = 1;
26386
- lastErrorMessage = event.message;
27468
+ filtered.push(a);
26387
27469
  }
26388
- if (consecutiveProviderErrors >= PROVIDER_ERROR_ABORT_THRESHOLD) {
26389
- councilAborted = true;
26390
- appendSystem(
26391
- setMessages,
26392
- `[council aborted: repeated provider error \u2014 ${consecutiveProviderErrors}\xD7 "${event.message.slice(0, 80)}"]`,
26393
- Date.now()
26394
- );
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;
26395
27505
  }
26396
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
+ };
26397
27519
  }
26398
- } catch (err) {
26399
- flushStreaming();
26400
- appendSystem(
26401
- setMessages,
26402
- `[council error] ${err instanceof Error ? err.message : String(err)}`,
26403
- Date.now()
26404
- );
26405
- } finally {
26406
- flushStreaming();
26407
- if (useLiveModel) finalizeStreaming(setMessages, setLive);
26408
- else finalizeStreamingAssistant(setMessages);
26409
- const hookShouldRun = membersCompleted > 0 || chairmanProducedOutput;
26410
- if (hookShouldRun) {
26411
- try {
26412
- const hook = await runPostCouncilHook2(workspaceCtx);
26413
- if (hook.ran && hook.changed) {
26414
- appendSystem(
26415
- setMessages,
26416
- `[agents.md] updated: ${hook.sections.length} section(s) changed (${hook.sections.join(", ")})`,
26417
- Date.now()
26418
- );
26419
- } else if (hook.ran && hook.reason) {
26420
- if (!hook.reason.includes("disabled")) {
26421
- appendSystem(setMessages, `[agents.md] ${hook.reason}`, Date.now());
26422
- }
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
+ };
26423
27549
  }
26424
- } catch {
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
+ };
27557
+ }
27558
+ return {
27559
+ handled: true,
27560
+ kind: "workspace_show",
27561
+ workspaceWhat: what
27562
+ };
26425
27563
  }
26426
- } else if (!councilAborted) {
26427
- appendSystem(
26428
- setMessages,
26429
- "[agents.md] skipped \u2014 council produced no output",
26430
- Date.now()
26431
- );
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 };
27571
+ }
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
+ };
27577
+ }
27578
+ return {
27579
+ handled: true,
27580
+ kind: "workspace",
27581
+ message: `Unknown /workspace subcommand: "${sub}". Type /workspace for usage.`
27582
+ };
26432
27583
  }
26433
- setBusy(false);
27584
+ default:
27585
+ return {
27586
+ handled: false,
27587
+ kind: "unknown",
27588
+ message: `Unknown command: /${command}. Type /help for available commands.`
27589
+ };
26434
27590
  }
26435
27591
  }
26436
27592
 
26437
- // src/cli/hooks/useSlashDispatch.ts
26438
- import { useCallback as useCallback3 } from "react";
26439
-
26440
27593
  // src/cli/gitOps.ts
26441
- import { execFile } from "node:child_process";
27594
+ import { execFile as execFile2 } from "node:child_process";
26442
27595
  import { promisify } from "node:util";
26443
27596
  import path15 from "node:path";
26444
- var execFileAsync = promisify(execFile);
27597
+ var execFileAsync = promisify(execFile2);
26445
27598
  async function git(cwd, args) {
26446
27599
  try {
26447
27600
  const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], {
@@ -26651,7 +27804,7 @@ async function handlePromoteMember(ctx, memberId) {
26651
27804
  }
26652
27805
 
26653
27806
  // src/cli/branchManager.ts
26654
- import { promises as fs13, existsSync as existsSync13, readFileSync as readFileSync10, 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";
26655
27808
  import path18 from "node:path";
26656
27809
  import os9 from "node:os";
26657
27810
  var META_FILENAME = "meta.json";
@@ -26673,11 +27826,11 @@ function sessionsPathFor(name, baseDir) {
26673
27826
  }
26674
27827
  function readBranchMeta(name, baseDir) {
26675
27828
  const metaPath = metaPathFor(name, baseDir);
26676
- if (!existsSync13(metaPath)) {
27829
+ if (!existsSync15(metaPath)) {
26677
27830
  throw new BranchNotFoundError(`Branch "${name}" not found`);
26678
27831
  }
26679
27832
  try {
26680
- const raw = readFileSync10(metaPath, "utf-8");
27833
+ const raw = readFileSync12(metaPath, "utf-8");
26681
27834
  const parsed = JSON.parse(raw);
26682
27835
  if (!parsed || typeof parsed !== "object" || typeof parsed.name !== "string" || typeof parsed.createdAt !== "number" || typeof parsed.fromSessionId !== "string") {
26683
27836
  throw new BranchCorruptError(`Branch "${name}" meta.json is malformed`);
@@ -26733,7 +27886,7 @@ var SessionNotFoundError = class extends Error {
26733
27886
  };
26734
27887
  function branchExists(name, baseDir = getBranchesBaseDir()) {
26735
27888
  const bp = branchPathFor(name, baseDir);
26736
- return existsSync13(bp) && existsSync13(metaPathFor(name, baseDir));
27889
+ return existsSync15(bp) && existsSync15(metaPathFor(name, baseDir));
26737
27890
  }
26738
27891
  async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(), sessionsBaseDir = getSessionsBaseDir()) {
26739
27892
  if (!name || name.trim().length === 0) {
@@ -26746,7 +27899,7 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
26746
27899
  throw new BranchAlreadyExistsError(name);
26747
27900
  }
26748
27901
  const sourcePath = path18.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
26749
- if (!existsSync13(sourcePath)) {
27902
+ if (!existsSync15(sourcePath)) {
26750
27903
  throw new SessionNotFoundError(`Source session "${fromSessionId}" not found at ${sourcePath}`);
26751
27904
  }
26752
27905
  const branchPath = branchPathFor(name, baseDir);
@@ -26779,7 +27932,7 @@ async function listBranches(baseDir = getBranchesBaseDir()) {
26779
27932
  const results = [];
26780
27933
  for (const entry of entries) {
26781
27934
  const metaPath = metaPathFor(entry, baseDir);
26782
- if (!existsSync13(metaPath)) continue;
27935
+ if (!existsSync15(metaPath)) continue;
26783
27936
  try {
26784
27937
  const meta3 = readBranchMeta(entry, baseDir);
26785
27938
  const sessionCount = await countSessions(entry, baseDir);
@@ -27017,8 +28170,8 @@ async function validateApiKey(providerId, apiKey, options = {}) {
27017
28170
  init_providerConfig();
27018
28171
 
27019
28172
  // src/cli/modelDiscovery.ts
27020
- import { promises as fs15, existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
27021
- 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";
27022
28175
  import path20 from "node:path";
27023
28176
  var PROVIDER_BASE_URLS = {
27024
28177
  "grok": "https://api.x.ai/v1",
@@ -27027,15 +28180,15 @@ var PROVIDER_BASE_URLS = {
27027
28180
  "openai-compatible": "https://api.openai.com/v1"
27028
28181
  };
27029
28182
  function defaultModelsFilePath() {
27030
- 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");
27031
28184
  }
27032
28185
  function getModelsFilePath() {
27033
28186
  return defaultModelsFilePath();
27034
28187
  }
27035
28188
  function loadModelsRegistry(file2 = getModelsFilePath()) {
27036
- if (!existsSync14(file2)) return {};
28189
+ if (!existsSync16(file2)) return {};
27037
28190
  try {
27038
- const raw = readFileSync11(file2, "utf-8");
28191
+ const raw = readFileSync13(file2, "utf-8");
27039
28192
  const parsed = JSON.parse(raw);
27040
28193
  if (!parsed || typeof parsed !== "object") return {};
27041
28194
  return parsed;
@@ -27159,24 +28312,6 @@ var ModelDiscoveryError = class extends Error {
27159
28312
  }
27160
28313
  };
27161
28314
 
27162
- // src/cli/utils/duration.ts
27163
- function formatDuration(ms) {
27164
- const abs = Math.abs(ms);
27165
- const sign = ms < 0 ? " ago" : "";
27166
- if (abs < 1e3) return `${abs}ms${sign}`;
27167
- const s = Math.floor(abs / 1e3);
27168
- if (s < 60) return `${s}s${sign}`;
27169
- const m = Math.floor(s / 60);
27170
- const rs = s % 60;
27171
- if (m < 60) return rs === 0 ? `${m}m${sign}` : `${m}m ${rs}s${sign}`;
27172
- const h = Math.floor(m / 60);
27173
- const rm = m % 60;
27174
- if (h < 24) return rm === 0 ? `${h}h${sign}` : `${h}h ${rm}m${sign}`;
27175
- const d = Math.floor(h / 24);
27176
- const rh = h % 24;
27177
- return rh === 0 ? `${d}d${sign}` : `${d}d ${rh}h${sign}`;
27178
- }
27179
-
27180
28315
  // src/cli/slashHandlers/provider.ts
27181
28316
  var UNKNOWN_PROVIDER_MSG = (id) => `[provider] unknown: ${id}. Available: openai-compatible, minimax, glm, grok, custom`;
27182
28317
  function handleProviderList(ctx) {
@@ -27395,7 +28530,7 @@ import path21 from "node:path";
27395
28530
  import os10 from "node:os";
27396
28531
 
27397
28532
  // src/cli/skillHistory.ts
27398
- import { promises as fs16, existsSync as existsSync15, 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";
27399
28534
  var SKILL_HISTORY_ROTATE_BYTES = 10 * 1024 * 1024;
27400
28535
  async function readSkillHistory(file2) {
27401
28536
  let raw = "";
@@ -27582,7 +28717,8 @@ function useSlashDispatch(params) {
27582
28717
  harnessRef,
27583
28718
  setQueueCount,
27584
28719
  dispatchPrompt,
27585
- dispatchCouncilPrompt
28720
+ dispatchCouncilPrompt,
28721
+ mode = "agent"
27586
28722
  } = params;
27587
28723
  return useCallback3(async (value) => {
27588
28724
  if (!value.trim()) return;
@@ -27604,6 +28740,11 @@ function useSlashDispatch(params) {
27604
28740
  if (!result.handled) {
27605
28741
  appendUser(setMessages, value);
27606
28742
  setSessionActive(true);
28743
+ if (mode === "council") {
28744
+ setInput("");
28745
+ await dispatchCouncilPrompt(value);
28746
+ return;
28747
+ }
27607
28748
  await dispatchPrompt(value);
27608
28749
  setInput("");
27609
28750
  return;
@@ -27845,15 +28986,16 @@ function useSlashDispatch(params) {
27845
28986
  setQueueCount,
27846
28987
  dispatchPrompt,
27847
28988
  dispatchCouncilPrompt,
28989
+ mode,
27848
28990
  params
27849
28991
  ]);
27850
28992
  }
27851
28993
 
27852
28994
  // src/cli/hooks/useBatchedMessages.ts
27853
- 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";
27854
28996
  function useBatchedMessages(state2, setState, cadenceMs = 16) {
27855
- const pendingRef = useRef3(null);
27856
- const timerRef = useRef3(null);
28997
+ const pendingRef = useRef5(null);
28998
+ const timerRef = useRef5(null);
27857
28999
  const flush = useCallback4(() => {
27858
29000
  if (timerRef.current !== null) {
27859
29001
  clearTimeout(timerRef.current);
@@ -27891,7 +29033,7 @@ function useBatchedMessages(state2, setState, cadenceMs = 16) {
27891
29033
  },
27892
29034
  [cadenceMs, setState]
27893
29035
  );
27894
- useEffect2(() => {
29036
+ useEffect4(() => {
27895
29037
  return () => {
27896
29038
  if (timerRef.current !== null) {
27897
29039
  clearTimeout(timerRef.current);
@@ -27903,16 +29045,16 @@ function useBatchedMessages(state2, setState, cadenceMs = 16) {
27903
29045
  }
27904
29046
 
27905
29047
  // src/cli/hooks/useTerminalSize.ts
27906
- import { useEffect as useEffect3, useState as useState3 } from "react";
29048
+ import { useEffect as useEffect5, useState as useState5 } from "react";
27907
29049
  import { useStdout as useStdout2 } from "ink";
27908
29050
  function useTerminalSize(options = {}) {
27909
29051
  const { defaults = { columns: 80, rows: 24 }, coalesceMs = 16 } = options;
27910
29052
  const { stdout } = useStdout2();
27911
- const [size, setSize] = useState3({
29053
+ const [size, setSize] = useState5({
27912
29054
  columns: stdout?.columns ?? defaults.columns,
27913
29055
  rows: stdout?.rows ?? defaults.rows
27914
29056
  });
27915
- useEffect3(() => {
29057
+ useEffect5(() => {
27916
29058
  if (!stdout) return;
27917
29059
  setSize({
27918
29060
  columns: stdout.columns ?? defaults.columns,
@@ -27954,15 +29096,28 @@ var providerDefaults = {
27954
29096
  "glm": "glm-4.5"
27955
29097
  };
27956
29098
  function App() {
27957
- const [input, setInput] = useState4("");
27958
- const [busy, setBusy] = useState4(false);
27959
- const [providerConfig, setProviderConfig] = useState4(() => getProviderConfig());
27960
- const [sessionStats, setSessionStats] = useState4({ totalTokens: 0, totalCostUsd: 0 });
27961
- 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");
27962
29105
  const activeProviderSpec = getActiveProvider();
27963
29106
  const activeModel = providerConfig.modelByProvider[activeProviderSpec.id];
27964
29107
  const session = useSession();
27965
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
+ );
27966
29121
  const { commit: commitLive, flush: flushLive } = useBatchedMessages(
27967
29122
  session.live,
27968
29123
  session.setLive
@@ -28008,55 +29163,227 @@ function App() {
28008
29163
  setQueueCount: chatTurn.setQueueCount,
28009
29164
  dispatchPrompt: chatTurn.dispatchPrompt,
28010
29165
  dispatchCouncilPrompt: chatTurn.dispatchCouncilPrompt,
29166
+ mode,
28011
29167
  onNewSession,
28012
29168
  onExit,
28013
29169
  onClear
28014
29170
  });
28015
- const skills = useMemo(() => listCodingSkills(), []);
28016
29171
  const banner = useMemo(() => {
28017
- const skillList = formatSkillList(skills);
28018
29172
  return {
28019
29173
  id: "banner-once",
28020
29174
  role: "system",
28021
29175
  ts: 0,
28022
- content: `zelari-code v0.7.3 \u2014 ${activeProviderSpec.id}/${activeModel}
28023
- ${skills.length} skills available. Type /help for the list, or /skill <name>.
28024
- \u2500\u2500 skills \u2500\u2500
28025
- ${skillList}`
29176
+ content: `zelari-code v${VERSION} \u2014 ${activeProviderSpec.id}/${activeModel}
29177
+ cwd: ${cwd}
29178
+ /help for commands \xB7 /skill <name> \xB7 shift+tab toggles agent/council`
28026
29179
  };
28027
- }, [skills, activeProviderSpec.id, activeModel]);
29180
+ }, [activeProviderSpec.id, activeModel, cwd]);
28028
29181
  const staticKey = `${session.sessionId || "pre-bootstrap"}-${clearEpoch}`;
28029
- const staticItems = [banner, ...session.messages];
28030
- 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(
28031
29193
  StatusBar,
28032
29194
  {
28033
29195
  model: activeModel,
28034
29196
  provider: activeProviderSpec.id,
28035
29197
  sessionId: session.sessionId ? session.sessionId.slice(0, 8) : "...",
28036
29198
  sessionActive: session.sessionActive,
28037
- totalTokens: sessionStats.totalTokens,
28038
- totalCostUsd: sessionStats.totalCostUsd,
28039
29199
  queueCount: chatTurn.queueCount,
28040
- busy
28041
- }
28042
- ), /* @__PURE__ */ React6.createElement(
28043
- InputBar,
29200
+ busy,
29201
+ mode,
29202
+ cwd,
29203
+ elapsedMs: timer.elapsedMs,
29204
+ lastMs: timer.lastMs
29205
+ }
29206
+ )), showSidebar && /* @__PURE__ */ React7.createElement(Sidebar, { version: VERSION, changes: gitChanges, rows: size.rows })));
29207
+ }
29208
+
29209
+ // src/cli/components/SplashScreen.tsx
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";
29212
+ var SPLASH_DURATION_MS = 2e3;
29213
+ var EMBLEM_LARGE = ` =%@*-
29214
+ =%@@@@@#:
29215
+ *%%%%%%@@%-
29216
+ :#%%%%%%%%%@@*
29217
+ :*%%%%%%%%%%%%%@%*.
29218
+ =%%%%%%%%%%%%%%%%@@%.
29219
+ =%%%%%%%%%%%%%%%%%%%@%-
29220
+ *%%%%%%%%%%%%%%%%%%%%%@@=
29221
+ .#%%%%%%%%@@@@@@@@%%%%%%@@@+
29222
+ :#%%%%%%@@@@@@@@@@@@@@@%%%%@@+
29223
+ -%%%%%@@@@@@@@@@@@@@@@@@@@@%%@@#.
29224
+ -%%%%%@@@@@@@@@@@@@@@@@@@@@@@%%@@%:
29225
+ =%%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@%@@%.
29226
+ +%%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%@@%:
29227
+ =%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-
29228
+ -%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%:
29229
+ :%%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%
29230
+ #%%%%%@@@@@@@@@@@@@@@%--#%@@@@@@@@@@@@@%%@@@=
29231
+ #@@%%%%@@@@@@@@@@@@@@%: -*@@@@@@@@@@@%%@@@@=
29232
+ #@@%%%%@@@@@@@@@@@@@%: .. -#@@@@@@@%%%%@@@=
29233
+ +@@@%%%@@@@@@@@@@@@%: :#= =%@@@@%%%@@@%-
29234
+ =@@@@%%%@@@@@@@@@@%: -%%*: +%@@%%@@@#
29235
+ +%@@@@%%@@@@@@@@@%: -%@@%*=.:+#@@@@#:
29236
+ .+*@@@@@@@@@%%%@@@@@@%: -%@@@@@#- +@@@@@@*=
29237
+ =*%%%%@@@@@@@@@@@%%@@@@%: -#@@@@@@@*: :*@@@@@@%+=
29238
+ -*%%%%%%%%%%@@@@@@@@@@@@@@@: -%@@@@@@@@#- -#@@@@@@@@*
29239
+ #%%%%%%%%%%%%%@@@@@@@@@@@@@: -#@@@@@%+-::. *@@@@@@@@=
29240
+ =%%%%%%%%%%%%%%%@@@@@@@@@@@@: :*+--=%#. :+++++*@@@@@@@@%
29241
+ #@%%%%%%%%%%%%%@@@@@@@@@@@@%: .. - -## *@@@@@@@@@@@@@@=
29242
+ :@@%%%%%%%%%%%%@@@@@@@@@@@@@%: .**= *%= :%@@@@@@@@@@@@@%
29243
+ #@@@%%%%%%%%%%%@@@@@@@@@@@@@@- =#@@%- :**: =%@@@@@@@@@@@@@+
29244
+ =@@@@@%%%@@%%%%@@@@@@@@@@@@@@@@@@@@@@%- :: .-#@@@@@@@@@@@@@%.
29245
+ :@@@@@@%%@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@%==*%%@@@@@@@@@@@@@@@@#
29246
+ *@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:
29247
+ :@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+
29248
+ =##########*######**##**#*********##**#*#######################*`;
29249
+ var EMBLEM_SMALL = ` =%%*:
29250
+ :#%%@@@+
29251
+ =%%%%%%%@#.
29252
+ .*%%%%%%%%%@%-
29253
+ =%%%%%%%%%%%%%@*.
29254
+ +%%%%%%@@@@@%%%%@%:
29255
+ *%%%%@@@@@@@@@@@%%@%:
29256
+ *%%%@@@@@@@@@@@@@@@%@%-
29257
+ :#%%@@@@@@@@@@@@@@@@@@@@@+
29258
+ :%%%@@@@@@@@@@@@@@@@@@@@@@@+
29259
+ #%%@@@@@@@@@@@@@@@@@@@@@@@@@-
29260
+ +%%@@@@@@@@@@@@#@@@@@@@@@@@@@%:
29261
+ %%%%@@@@@@@@@@%::*@@@@@@@@@%@@=
29262
+ -%@%%@@@@@@@@@%:.::*@@@@@%%@@*.
29263
+ :%@@%%@@@@@@@%.:%#::*@@%@@@-
29264
+ :#@@@%@@@@@@%.:%@%+.-#@@@=
29265
+ :+#@@@@@@@%@@@@%.:%@@@%=.+@@@%*-
29266
+ .*#%%%%%%@@@@@@@@@@.:%@@@@@*:.+%@@@@#-
29267
+ +%%%%%%%%%%@@@@@@@@::##*%* :----%@@@@%.
29268
+ .@%%%%%%%%%@@@@@@@@@:.-..-#=:%@@@@@@@@@+
29269
+ *@@%%%%%%%%@@@@@@@@@: =#+ +#.-@@@@@@@@@@.
29270
+ -@@@%%%%%%%@@@@@@@@@@#%@@@- -.:*@@@@@@@@@*
29271
+ %@@@@%@@%@@@@@@@@@@@@@@@@@%**%@@@@@@@@@@@@:
29272
+ =@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*`;
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;
29304
+ function measure(art) {
29305
+ const lines = art.split("\n");
29306
+ return {
29307
+ art,
29308
+ width: Math.max(...lines.map((l) => l.length)),
29309
+ height: lines.length
29310
+ };
29311
+ }
29312
+ var VARIANTS = [
29313
+ measure(EMBLEM_LARGE),
29314
+ measure(EMBLEM_SMALL),
29315
+ measure(EMBLEM_TINY),
29316
+ measure(EMBLEM_MICRO)
29317
+ ];
29318
+ function pickSplashArt(columns, rows) {
29319
+ for (const v of VARIANTS) {
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 };
29323
+ }
29324
+ return null;
29325
+ }
29326
+ function shouldShowSplash(opts) {
29327
+ if (!opts.isTTY) return false;
29328
+ if (opts.env["ZELARI_NO_SPLASH"] === "1") return false;
29329
+ return pickSplashArt(opts.columns, opts.rows) !== null;
29330
+ }
29331
+ function Splash({ onDone, version: version2 }) {
29332
+ const { isRawModeSupported } = useStdin2();
29333
+ const columns = process.stdout.columns ?? 80;
29334
+ const rows = process.stdout.rows ?? 24;
29335
+ const picked = pickSplashArt(columns, rows);
29336
+ useEffect6(() => {
29337
+ const t = setTimeout(onDone, SPLASH_DURATION_MS);
29338
+ return () => clearTimeout(t);
29339
+ }, [onDone]);
29340
+ useInput2(
29341
+ () => {
29342
+ onDone();
29343
+ },
29344
+ { isActive: isRawModeSupported === true }
29345
+ );
29346
+ if (!picked) return null;
29347
+ return /* @__PURE__ */ React8.createElement(
29348
+ Box8,
28044
29349
  {
28045
- value: input,
28046
- onChange: setInput,
28047
- onSubmit: handleSubmit,
28048
- disabled: busy
28049
- }
28050
- )));
29350
+ flexDirection: "column",
29351
+ alignItems: "center",
29352
+ justifyContent: "center",
29353
+ width: columns,
29354
+ height: rows - 1
29355
+ },
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")
29360
+ );
29361
+ }
29362
+ function SplashGate({
29363
+ children,
29364
+ version: version2
29365
+ }) {
29366
+ const [show, setShow] = useState7(
29367
+ () => shouldShowSplash({
29368
+ isTTY: process.stdout.isTTY === true,
29369
+ env: process.env,
29370
+ columns: process.stdout.columns ?? 80,
29371
+ rows: process.stdout.rows ?? 24
29372
+ })
29373
+ );
29374
+ if (show) {
29375
+ return /* @__PURE__ */ React8.createElement(Splash, { onDone: () => setShow(false), version: version2 });
29376
+ }
29377
+ return /* @__PURE__ */ React8.createElement(React8.Fragment, null, children);
28051
29378
  }
28052
29379
 
28053
29380
  // src/cli/main.ts
28054
29381
  init_providerConfig();
28055
29382
 
28056
29383
  // src/cli/wizard/firstRun.ts
28057
- import { existsSync as existsSync16 } from "node:fs";
29384
+ import { existsSync as existsSync18 } from "node:fs";
28058
29385
  function shouldRunWizard(input) {
28059
- const exists = input.exists ?? existsSync16;
29386
+ const exists = input.exists ?? existsSync18;
28060
29387
  if (input.hasResetConfigFlag) {
28061
29388
  return { shouldRun: true, reason: "--reset-config flag forced wizard" };
28062
29389
  }
@@ -28091,15 +29418,15 @@ function parseWizardFlags(argv) {
28091
29418
  }
28092
29419
 
28093
29420
  // src/cli/wizard/runWizard.tsx
28094
- import React8, { useEffect as useEffect4, useState as useState5 } from "react";
28095
- import { Box as Box8, Text as Text8 } from "ink";
28096
- import { useInput } 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";
28097
29424
  init_keyStore();
28098
29425
  init_providerConfig();
28099
29426
 
28100
29427
  // src/cli/wizard/index.tsx
28101
- import React7 from "react";
28102
- import { Box as Box7, Text as Text7 } from "ink";
29428
+ import React9 from "react";
29429
+ import { Box as Box9, Text as Text9 } from "ink";
28103
29430
 
28104
29431
  // src/cli/wizard/useWizardState.ts
28105
29432
  init_providerConfig();
@@ -28214,18 +29541,17 @@ function createWizardState(opts) {
28214
29541
  var API_KEY_OPTIONS = ["env", "keystore", "skip"];
28215
29542
 
28216
29543
  // src/cli/wizard/index.tsx
28217
- var VERSION = "0.7.2";
28218
29544
  function Frame({ children }) {
28219
- return /* @__PURE__ */ React7.createElement(Box7, { 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);
28220
29546
  }
28221
29547
  function Step(props) {
28222
- return /* @__PURE__ */ React7.createElement(Box7, null, /* @__PURE__ */ React7.createElement(Text7, { 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, " "));
28223
29549
  }
28224
29550
  function renderProviderList(providers, cursor) {
28225
- return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column" }, providers.map((p3, i) => {
29551
+ return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column" }, providers.map((p3, i) => {
28226
29552
  const arrow = i === cursor ? "\u279C " : " ";
28227
29553
  const color = i === cursor ? "cyan" : void 0;
28228
- return /* @__PURE__ */ React7.createElement(Text7, { key: p3.id, color }, arrow, p3.displayName, " ", /* @__PURE__ */ React7.createElement(Text7, { 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, ")"));
28229
29555
  }));
28230
29556
  }
28231
29557
  function renderApiKeyOptions(cursor) {
@@ -28234,37 +29560,37 @@ function renderApiKeyOptions(cursor) {
28234
29560
  keystore: "Save to local keyStore (encrypted)",
28235
29561
  skip: "Skip for now (chat will fail until key is set)"
28236
29562
  };
28237
- return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column" }, API_KEY_OPTIONS.map((choice, i) => {
29563
+ return /* @__PURE__ */ React9.createElement(Box9, { flexDirection: "column" }, API_KEY_OPTIONS.map((choice, i) => {
28238
29564
  const arrow = i === cursor ? "\u279C " : " ";
28239
29565
  const color = i === cursor ? "cyan" : void 0;
28240
- return /* @__PURE__ */ React7.createElement(Text7, { key: choice, color }, arrow, labels[choice]);
29566
+ return /* @__PURE__ */ React9.createElement(Text9, { key: choice, color }, arrow, labels[choice]);
28241
29567
  }));
28242
29568
  }
28243
29569
  function Wizard({ state: state2, providers }) {
28244
29570
  const s = state2.state;
28245
29571
  if (s.committed) {
28246
- return /* @__PURE__ */ React7.createElement(Frame, null, /* @__PURE__ */ React7.createElement(Text7, { color: "green" }, "\u2713 Setup complete!"), /* @__PURE__ */ React7.createElement(Text7, null, "Provider: ", /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, s.providerId), " | ", "Model: ", /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, s.model), " | ", "API key: ", /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, s.apiKeyChoice ?? "n/a")), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { 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)")));
28247
29573
  }
28248
- return /* @__PURE__ */ React7.createElement(Frame, null, /* @__PURE__ */ React7.createElement(Text7, { color: "cyan", bold: true }, "zelari-code v", VERSION, " \u2014 first-time setup"), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1, marginBottom: 1, flexDirection: "row" }, /* @__PURE__ */ React7.createElement(Step, { index: 1, total: 5, name: "welcome", active: s.step === "welcome" }), /* @__PURE__ */ React7.createElement(Step, { index: 2, total: 5, name: "provider", active: s.step === "provider" }), /* @__PURE__ */ React7.createElement(Step, { index: 3, total: 5, name: "model", active: s.step === "model" }), /* @__PURE__ */ React7.createElement(Step, { index: 4, total: 5, name: "apikey", active: s.step === "apikey" }), /* @__PURE__ */ React7.createElement(Step, { index: 5, total: 5, name: "confirm", active: s.step === "confirm" })), s.step === "welcome" && /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement(Text7, null, "Welcome! Let's get you coding in under two minutes."), /* @__PURE__ */ React7.createElement(Text7, { color: "gray" }, "We'll pick a provider, default model, and how to handle your API key."), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, null, "Press "), /* @__PURE__ */ React7.createElement(Text7, { color: "cyan", inverse: true }, " Enter "), /* @__PURE__ */ React7.createElement(Text7, null, " to continue, or "), /* @__PURE__ */ React7.createElement(Text7, { color: "red", inverse: true }, " Q "), /* @__PURE__ */ React7.createElement(Text7, null, " to quit (re-run with "), /* @__PURE__ */ React7.createElement(Text7, { color: "gray" }, "--no-wizard"), /* @__PURE__ */ React7.createElement(Text7, null, " to skip later)."))), s.step === "provider" && /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement(Text7, null, "Choose your LLM provider:"), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, renderProviderList(providers, s.providerCursor)), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { color: "gray" }, "\u2191/\u2193 to move, Enter to confirm"))), s.step === "model" && /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement(Text7, null, "Model for ", /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, s.providerId), ":"), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1, borderStyle: "single", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React7.createElement(Text7, null, s.model ?? "(empty)")), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, null, "Press ", /* @__PURE__ */ React7.createElement(Text7, { color: "cyan", inverse: true }, " Enter "), "to accept, or type a new name. ", /* @__PURE__ */ React7.createElement(Text7, { color: "gray" }, "(default kept on empty input)")))), s.step === "apikey" && /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement(Text7, null, "How should we handle the API key?"), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, renderApiKeyOptions(s.apiKeyCursor)), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { color: "gray" }, "\u2191/\u2193 to move, Enter to confirm"))), s.step === "confirm" && /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement(Text7, null, "Confirm your setup:"), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Text7, null, "Provider: ", /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, s.providerId)), /* @__PURE__ */ React7.createElement(Text7, null, "Model: ", /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, s.model)), /* @__PURE__ */ React7.createElement(Text7, null, "API key: ", /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, s.apiKeyChoice ?? "(unset)"))), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, null, "Press ", /* @__PURE__ */ React7.createElement(Text7, { color: "green", inverse: true }, " Enter "), "to save and launch, or ", /* @__PURE__ */ React7.createElement(Text7, { 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."))));
28249
29575
  }
28250
29576
 
28251
29577
  // src/cli/wizard/runWizard.tsx
28252
29578
  function RunWizard(_props) {
28253
- const [wiz] = useState5(
29579
+ const [wiz] = useState8(
28254
29580
  () => createWizardState({
28255
29581
  providers: PROVIDERS,
28256
29582
  defaultModelFor: (id) => getModelForProvider(id)
28257
29583
  })
28258
29584
  );
28259
- const [, force] = useState5(0);
28260
- useEffect4(() => {
29585
+ const [, force] = useState8(0);
29586
+ useEffect7(() => {
28261
29587
  if (typeof wiz.subscribe === "function") {
28262
29588
  const sub = wiz.subscribe(() => force((n) => n + 1));
28263
29589
  return () => sub();
28264
29590
  }
28265
29591
  return void 0;
28266
29592
  }, [wiz]);
28267
- useInput((input, key) => {
29593
+ useInput3((input, key) => {
28268
29594
  if (wiz.state.committed) return;
28269
29595
  const s = wiz.state;
28270
29596
  if (key.escape) {
@@ -28321,20 +29647,20 @@ function RunWizard(_props) {
28321
29647
  }
28322
29648
  });
28323
29649
  if (wiz.state.committed) {
28324
- return /* @__PURE__ */ React8.createElement(PostCommitBridge, null);
29650
+ return /* @__PURE__ */ React10.createElement(PostCommitBridge, null);
28325
29651
  }
28326
- return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", paddingX: 2, paddingY: 1 }, /* @__PURE__ */ React8.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 }));
28327
29653
  }
28328
29654
  function PostCommitBridge() {
28329
- const [showApp, setShowApp] = useState5(false);
28330
- useEffect4(() => {
29655
+ const [showApp, setShowApp] = useState8(false);
29656
+ useEffect7(() => {
28331
29657
  const t = setTimeout(() => setShowApp(true), 1200);
28332
29658
  return () => clearTimeout(t);
28333
29659
  }, []);
28334
29660
  if (showApp) {
28335
- return /* @__PURE__ */ React8.createElement(App, null);
29661
+ return /* @__PURE__ */ React10.createElement(App, null);
28336
29662
  }
28337
- return /* @__PURE__ */ React8.createElement(Box8, { flexDirection: "column", paddingX: 2, paddingY: 1 }, /* @__PURE__ */ React8.createElement(Box8, { borderStyle: "round", borderColor: "green", paddingX: 2, paddingY: 1, flexDirection: "column" }, /* @__PURE__ */ React8.createElement(Text8, { color: "green", bold: true }, "\u2713 Setup complete! Launching zelari-code\u2026"), /* @__PURE__ */ React8.createElement(Text8, { 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.")));
28338
29664
  }
28339
29665
 
28340
29666
  // src/cli/headless.ts
@@ -28546,9 +29872,9 @@ async function runHeadlessCouncil(opts, provider, model, providerStream) {
28546
29872
 
28547
29873
  // src/cli/skillsMd.ts
28548
29874
  init_skills2();
28549
- import { existsSync as existsSync17, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "node:fs";
28550
- import { join as join7 } from "node:path";
28551
- 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";
28552
29878
  var CODING_CATEGORIES = /* @__PURE__ */ new Set([
28553
29879
  "plan",
28554
29880
  "refactor",
@@ -28563,10 +29889,10 @@ var CODING_CATEGORIES = /* @__PURE__ */ new Set([
28563
29889
  ]);
28564
29890
  function skillMdSearchDirs(projectRoot = process.cwd()) {
28565
29891
  return [
28566
- join7(projectRoot, ".zelari", "skills"),
28567
- join7(projectRoot, ".claude", "skills"),
28568
- join7(projectRoot, ".opencode", "skills"),
28569
- join7(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")
28570
29896
  ];
28571
29897
  }
28572
29898
  function parseSkillMd(content, sourcePath) {
@@ -28624,7 +29950,7 @@ function loadSkillMdSkills(projectRoot = process.cwd(), options = {}) {
28624
29950
  const summary = { loaded: [], skipped: [] };
28625
29951
  const seen = new Set(options.existingIds ?? []);
28626
29952
  for (const dir of skillMdSearchDirs(projectRoot)) {
28627
- if (!existsSync17(dir)) continue;
29953
+ if (!existsSync19(dir)) continue;
28628
29954
  let entries;
28629
29955
  try {
28630
29956
  entries = readdirSync4(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
@@ -28632,10 +29958,10 @@ function loadSkillMdSkills(projectRoot = process.cwd(), options = {}) {
28632
29958
  continue;
28633
29959
  }
28634
29960
  for (const entry of entries) {
28635
- const skillPath = join7(dir, entry, "SKILL.md");
28636
- if (!existsSync17(skillPath)) continue;
29961
+ const skillPath = join9(dir, entry, "SKILL.md");
29962
+ if (!existsSync19(skillPath)) continue;
28637
29963
  try {
28638
- const parsed = parseSkillMd(readFileSync12(skillPath, "utf8"), skillPath);
29964
+ const parsed = parseSkillMd(readFileSync14(skillPath, "utf8"), skillPath);
28639
29965
  if (!parsed) {
28640
29966
  summary.skipped.push({ path: skillPath, reason: "missing/invalid frontmatter (name, description) or empty body" });
28641
29967
  continue;
@@ -28657,7 +29983,7 @@ function loadSkillMdSkills(projectRoot = process.cwd(), options = {}) {
28657
29983
 
28658
29984
  // src/cli/main.ts
28659
29985
  init_skills2();
28660
- var VERSION2 = "0.7.5";
29986
+ var VERSION = "0.7.9";
28661
29987
  async function backgroundUpdateCheck() {
28662
29988
  if (process.env.ANATHEMA_DEV === "1") return;
28663
29989
  await new Promise((resolve) => setTimeout(resolve, 3e3));
@@ -28687,7 +30013,7 @@ async function shutdown() {
28687
30013
  function pickRootComponent() {
28688
30014
  const argv = process.argv.slice(2);
28689
30015
  if (argv.includes("--version") || argv.includes("-v")) {
28690
- console.log(`zelari-code v${VERSION2}`);
30016
+ console.log(`zelari-code v${VERSION}`);
28691
30017
  process.exit(0);
28692
30018
  }
28693
30019
  if (argv.includes("--help") || argv.includes("-h")) {
@@ -28713,9 +30039,12 @@ function pickRootComponent() {
28713
30039
  });
28714
30040
  if (decision.shouldRun) {
28715
30041
  console.error(`[zelari-code] starting wizard: ${decision.reason}`);
28716
- return { kind: "wizard", element: React9.createElement(RunWizard) };
30042
+ return { kind: "wizard", element: React11.createElement(RunWizard) };
28717
30043
  }
28718
- return { kind: "app", element: React9.createElement(App) };
30044
+ return {
30045
+ kind: "app",
30046
+ element: React11.createElement(SplashGate, { version: VERSION }, React11.createElement(App))
30047
+ };
28719
30048
  }
28720
30049
  function loadUserSkills() {
28721
30050
  try {
@@ -28758,6 +30087,6 @@ function main() {
28758
30087
  }
28759
30088
  main();
28760
30089
  export {
28761
- VERSION2 as VERSION
30090
+ VERSION
28762
30091
  };
28763
30092
  //# sourceMappingURL=main.bundled.js.map