zelari-code 0.7.8 → 0.7.10

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 (89) hide show
  1. package/README.md +95 -44
  2. package/bin/zelari-code.js +1 -1
  3. package/dist/agents/councilApi.js +7 -7
  4. package/dist/agents/councilApi.js.map +1 -1
  5. package/dist/agents/councilDirectives.js +3 -3
  6. package/dist/agents/promoteMember.js +2 -2
  7. package/dist/agents/roles.js +50 -32
  8. package/dist/agents/roles.js.map +1 -1
  9. package/dist/agents/skills/builtin/debugging.js +4 -4
  10. package/dist/agents/skills/builtin/debugging.js.map +1 -1
  11. package/dist/agents/skills/builtin/docs.js +23 -13
  12. package/dist/agents/skills/builtin/docs.js.map +1 -1
  13. package/dist/agents/skills/builtin/git-ops.js +3 -3
  14. package/dist/agents/skills/builtin/git-ops.js.map +1 -1
  15. package/dist/agents/skills/builtin/planning.js +4 -4
  16. package/dist/agents/skills/builtin/planning.js.map +1 -1
  17. package/dist/agents/skills/builtin/refactoring.js +3 -3
  18. package/dist/agents/skills/builtin/refactoring.js.map +1 -1
  19. package/dist/agents/skills/builtin/review.js +9 -9
  20. package/dist/agents/skills/builtin/review.js.map +1 -1
  21. package/dist/agents/skills/builtin/testing.js +3 -3
  22. package/dist/agents/skills/builtin/testing.js.map +1 -1
  23. package/dist/agents/tools.js +2 -9
  24. package/dist/agents/tools.js.map +1 -1
  25. package/dist/cli/app.js +79 -18
  26. package/dist/cli/app.js.map +1 -1
  27. package/dist/cli/components/CollapsibleToolOutput.js +8 -26
  28. package/dist/cli/components/CollapsibleToolOutput.js.map +1 -1
  29. package/dist/cli/components/Header.js +6 -18
  30. package/dist/cli/components/Header.js.map +1 -1
  31. package/dist/cli/components/LiveRegion.js +7 -3
  32. package/dist/cli/components/LiveRegion.js.map +1 -1
  33. package/dist/cli/components/SelectList.js +67 -0
  34. package/dist/cli/components/SelectList.js.map +1 -0
  35. package/dist/cli/components/Sidebar.js +86 -37
  36. package/dist/cli/components/Sidebar.js.map +1 -1
  37. package/dist/cli/components/Spinner.js +51 -0
  38. package/dist/cli/components/Spinner.js.map +1 -0
  39. package/dist/cli/components/SplashScreen.js +117 -71
  40. package/dist/cli/components/SplashScreen.js.map +1 -1
  41. package/dist/cli/components/StatusBar.js +44 -32
  42. package/dist/cli/components/StatusBar.js.map +1 -1
  43. package/dist/cli/councilConfig.js +38 -0
  44. package/dist/cli/councilConfig.js.map +1 -0
  45. package/dist/cli/councilDispatcher.js +35 -49
  46. package/dist/cli/councilDispatcher.js.map +1 -1
  47. package/dist/cli/hooks/useChatTurn.js +80 -61
  48. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  49. package/dist/cli/hooks/useExecutionTimer.js +25 -0
  50. package/dist/cli/hooks/useExecutionTimer.js.map +1 -0
  51. package/dist/cli/hooks/useGitChanges.js +142 -0
  52. package/dist/cli/hooks/useGitChanges.js.map +1 -0
  53. package/dist/cli/hooks/useSlashDispatch.js +20 -3
  54. package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
  55. package/dist/cli/main.bundled.js +19776 -19052
  56. package/dist/cli/main.bundled.js.map +4 -4
  57. package/dist/cli/main.js +1 -1
  58. package/dist/cli/main.js.map +1 -1
  59. package/dist/cli/mcp/mcpClient.js +14 -6
  60. package/dist/cli/mcp/mcpClient.js.map +1 -1
  61. package/dist/cli/slashCommands.js +21 -4
  62. package/dist/cli/slashCommands.js.map +1 -1
  63. package/dist/cli/slashHandlers/provider.js +101 -4
  64. package/dist/cli/slashHandlers/provider.js.map +1 -1
  65. package/dist/cli/updater.js +8 -5
  66. package/dist/cli/updater.js.map +1 -1
  67. package/dist/cli/utils/cmdline.js +26 -0
  68. package/dist/cli/utils/cmdline.js.map +1 -0
  69. package/dist/cli/utils/paths.js +20 -0
  70. package/dist/cli/utils/paths.js.map +1 -0
  71. package/dist/cli/workspace/planDetect.js +17 -0
  72. package/dist/cli/workspace/planDetect.js.map +1 -0
  73. package/dist/cli/workspace/postCouncilHook.js +39 -28
  74. package/dist/cli/workspace/postCouncilHook.js.map +1 -1
  75. package/dist/cli/workspace/stubs.js +241 -196
  76. package/dist/cli/workspace/stubs.js.map +1 -1
  77. package/dist/main/core/AgentHarness.js +36 -7
  78. package/dist/main/core/AgentHarness.js.map +1 -1
  79. package/dist/main/core/tools/builtin/listFiles.js +43 -5
  80. package/dist/main/core/tools/builtin/listFiles.js.map +1 -1
  81. package/dist/main/core/tools/builtin/search.js +28 -120
  82. package/dist/main/core/tools/builtin/search.js.map +1 -1
  83. package/package.json +71 -71
  84. package/dist/cli/oauthCallbackServer.js +0 -207
  85. package/dist/cli/oauthCallbackServer.js.map +0 -1
  86. package/dist/main/core/tools/builtin/_walk.js +0 -129
  87. package/dist/main/core/tools/builtin/_walk.js.map +0 -1
  88. package/dist/main/core/tools/builtin/diff.js +0 -477
  89. package/dist/main/core/tools/builtin/diff.js.map +0 -1
@@ -12,11 +12,11 @@
12
12
  *
13
13
  * @see docs/plans/2026-07-01-council-workspace-cli-stubs.md
14
14
  */
15
- import { existsSync, readdirSync, writeFileSync, readFileSync, mkdirSync, renameSync } from 'node:fs';
16
- import { join, basename, dirname, relative } from 'node:path';
17
- import { workspaceMutex } from './storage.js';
18
- import { resolveWorkspaceRoot, workspaceFile, workspaceArtifact, projectName as deriveProjectName, } from './paths.js';
19
- import { Storage } from './storage.js';
15
+ import { existsSync, readdirSync, writeFileSync, readFileSync, mkdirSync, renameSync, } from "node:fs";
16
+ import { join, basename, dirname, relative } from "node:path";
17
+ import { workspaceMutex } from "./storage.js";
18
+ import { resolveWorkspaceRoot, workspaceFile, workspaceArtifact, projectName as deriveProjectName, } from "./paths.js";
19
+ import { Storage } from "./storage.js";
20
20
  // ── Context factory ───────────────────────────────────────────────────
21
21
  /**
22
22
  * Build a WorkspaceContext rooted at the project's `.zelari/` directory.
@@ -32,7 +32,7 @@ export function createWorkspaceContext(projectRoot = process.cwd()) {
32
32
  }
33
33
  /** Path of the machine-readable plan store (source of truth since v0.7.3). */
34
34
  function planJsonPath(ctx) {
35
- return join(ctx.rootDir, 'plan.json');
35
+ return join(ctx.rootDir, "plan.json");
36
36
  }
37
37
  function readPlan(ctx) {
38
38
  // v0.7.3: the plan's source of truth is plan.json. The previous
@@ -45,7 +45,7 @@ function readPlan(ctx) {
45
45
  const jsonPath = planJsonPath(ctx);
46
46
  if (existsSync(jsonPath)) {
47
47
  try {
48
- const parsed = JSON.parse(readFileSync(jsonPath, 'utf8'));
48
+ const parsed = JSON.parse(readFileSync(jsonPath, "utf8"));
49
49
  return {
50
50
  phases: Array.isArray(parsed.phases) ? parsed.phases : [],
51
51
  tasks: Array.isArray(parsed.tasks) ? parsed.tasks : [],
@@ -58,7 +58,7 @@ function readPlan(ctx) {
58
58
  }
59
59
  // Legacy migration: one best-effort parse of the old plan.md frontmatter
60
60
  // (pre-v0.7.3 files). The next writePlan persists it as plan.json.
61
- const path = workspaceFile(ctx.rootDir, 'plan');
61
+ const path = workspaceFile(ctx.rootDir, "plan");
62
62
  const doc = ctx.storage.readIfExists(path);
63
63
  if (!doc)
64
64
  return { phases: [], tasks: [], milestones: [] };
@@ -78,13 +78,13 @@ function readPlan(ctx) {
78
78
  function writePlan(ctx, summary) {
79
79
  const jsonPath = planJsonPath(ctx);
80
80
  mkdirSync(dirname(jsonPath), { recursive: true });
81
- const tmp = jsonPath + '.tmp-' + process.pid;
82
- writeFileSync(tmp, JSON.stringify(summary, null, 2), 'utf8');
81
+ const tmp = jsonPath + ".tmp-" + process.pid;
82
+ writeFileSync(tmp, JSON.stringify(summary, null, 2), "utf8");
83
83
  renameSync(tmp, jsonPath);
84
- const mdPath = workspaceFile(ctx.rootDir, 'plan');
84
+ const mdPath = workspaceFile(ctx.rootDir, "plan");
85
85
  const summaryMeta = {
86
- kind: 'plan-summary',
87
- id: '_plan',
86
+ kind: "plan-summary",
87
+ id: "_plan",
88
88
  updatedAt: new Date().toISOString(),
89
89
  };
90
90
  ctx.storage.write(mdPath, summaryMeta, renderPlanBody(summary));
@@ -93,77 +93,81 @@ function writePlan(ctx, summary) {
93
93
  function renderPlanBody(summary) {
94
94
  const lines = [];
95
95
  lines.push(`# Execution Plan`);
96
- lines.push('');
96
+ lines.push("");
97
97
  lines.push(`> Auto-generated by Zelari Code council. Do not edit by hand.`);
98
- lines.push('');
98
+ lines.push("");
99
99
  // Phases
100
100
  lines.push(`## Phases`);
101
- lines.push('');
101
+ lines.push("");
102
102
  if (summary.phases.length === 0) {
103
103
  lines.push(`_(none yet)_`);
104
104
  }
105
105
  else {
106
106
  for (const phase of [...summary.phases].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))) {
107
- lines.push(`### ${phase.order ?? '?'}. ${phase.id} \`${phase.color ?? ''}\``);
108
- lines.push('');
107
+ lines.push(`### ${phase.order ?? "?"}. ${phase.id} \`${phase.color ?? ""}\``);
108
+ lines.push("");
109
109
  }
110
110
  }
111
- lines.push('');
111
+ lines.push("");
112
112
  // Tasks grouped by phase
113
113
  lines.push(`## Tasks`);
114
- lines.push('');
114
+ lines.push("");
115
115
  if (summary.tasks.length === 0) {
116
116
  lines.push(`_(none yet)_`);
117
117
  }
118
118
  else {
119
119
  const byPhase = new Map();
120
120
  for (const task of summary.tasks) {
121
- const pid = task.phaseId ?? '_unassigned';
121
+ const pid = task.phaseId ?? "_unassigned";
122
122
  if (!byPhase.has(pid))
123
123
  byPhase.set(pid, []);
124
124
  byPhase.get(pid).push(task);
125
125
  }
126
126
  for (const [phaseId, tasks] of byPhase) {
127
127
  lines.push(`### Phase: \`${phaseId}\``);
128
- lines.push('');
128
+ lines.push("");
129
129
  for (const t of tasks) {
130
- const status = t.status ?? 'pending';
131
- const pri = t.priority ?? 'medium';
130
+ const status = t.status ?? "pending";
131
+ const pri = t.priority ?? "medium";
132
132
  lines.push(`- [ ] **${t.id}** _(${status}, ${pri})_`);
133
133
  }
134
- lines.push('');
134
+ lines.push("");
135
135
  }
136
136
  }
137
- lines.push('');
137
+ lines.push("");
138
138
  // Milestones
139
139
  lines.push(`## Milestones`);
140
- lines.push('');
140
+ lines.push("");
141
141
  if (summary.milestones.length === 0) {
142
142
  lines.push(`_(none yet)_`);
143
143
  }
144
144
  else {
145
145
  for (const m of summary.milestones) {
146
- lines.push(`- **${m.id}** → ${m.targetVersion ?? '?'}`);
146
+ lines.push(`- **${m.id}** → ${m.targetVersion ?? "?"}`);
147
147
  }
148
148
  }
149
- lines.push('');
150
- return lines.join('\n');
149
+ lines.push("");
150
+ return lines.join("\n");
151
151
  }
152
152
  // ── ADR (decisions/) helpers ──────────────────────────────────────────
153
153
  function nextAdrId(ctx) {
154
- const decisionsDir = join(ctx.rootDir, 'decisions');
154
+ const decisionsDir = join(ctx.rootDir, "decisions");
155
155
  if (!existsSync(decisionsDir))
156
- return '001';
156
+ return "001";
157
157
  const existing = readdirSync(decisionsDir)
158
- .filter((f) => f.endsWith('.md'))
158
+ .filter((f) => f.endsWith(".md"))
159
159
  .map((f) => f.match(/^(\d+)-/))
160
160
  .filter((m) => !!m)
161
161
  .map((m) => parseInt(m[1], 10));
162
162
  const max = existing.length === 0 ? 0 : Math.max(...existing);
163
- return String(max + 1).padStart(3, '0');
163
+ return String(max + 1).padStart(3, "0");
164
164
  }
165
165
  function slugify(s) {
166
- return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60);
166
+ return s
167
+ .toLowerCase()
168
+ .replace(/[^a-z0-9]+/g, "-")
169
+ .replace(/^-|-$/g, "")
170
+ .slice(0, 60);
167
171
  }
168
172
  /** Add a phase record. Returns the phase id and whether it was newly created. */
169
173
  function addPhaseRecord(summary, input) {
@@ -172,7 +176,7 @@ function addPhaseRecord(summary, input) {
172
176
  return { id, created: false };
173
177
  }
174
178
  summary.phases.push({
175
- kind: 'phase',
179
+ kind: "phase",
176
180
  id,
177
181
  name: input.name,
178
182
  description: input.description,
@@ -191,46 +195,47 @@ function addPhaseRecord(summary, input) {
191
195
  function addTaskRecord(ctx, summary, phaseId, t, options) {
192
196
  const slug = slugify(t.title);
193
197
  if (options.dedupe) {
194
- const existing = summary.tasks.find((k) => k.phaseId === phaseId && k.id.replace(/-\d+$/, '') === `${phaseId}-${slug}`);
198
+ const existing = summary.tasks.find((k) => k.phaseId === phaseId &&
199
+ k.id.replace(/-\d+$/, "") === `${phaseId}-${slug}`);
195
200
  if (existing)
196
201
  return { id: existing.id, created: false };
197
202
  }
198
203
  const id = `${phaseId}-${slug}-${summary.tasks.filter((k) => k.phaseId === phaseId).length + 1}`;
199
204
  summary.tasks.push({
200
- kind: 'task',
205
+ kind: "task",
201
206
  id,
202
207
  // v0.7.3: keep the human-readable title in plan.json so
203
208
  // buildPlanSummary can render it in the system prompt.
204
209
  name: t.title,
205
210
  phaseId,
206
- status: 'pending',
211
+ status: "pending",
207
212
  priority: t.priority,
208
213
  });
209
- const taskPath = join(ctx.rootDir, 'plan-tasks', `${id}.md`);
214
+ const taskPath = join(ctx.rootDir, "plan-tasks", `${id}.md`);
210
215
  const meta = {
211
- kind: 'task',
216
+ kind: "task",
212
217
  id,
213
218
  phaseId,
214
- status: 'pending',
219
+ status: "pending",
215
220
  priority: t.priority,
216
221
  tags: t.fileRefs,
217
222
  };
218
223
  const body = [
219
224
  `# ${t.title}`,
220
- '',
225
+ "",
221
226
  t.description,
222
- '',
227
+ "",
223
228
  `## File references`,
224
229
  ...t.fileRefs.map((f) => `- \`${f}\``),
225
- '',
230
+ "",
226
231
  `## Acceptance criteria`,
227
232
  ...t.acceptance.map((a) => `- ${a}`),
228
- '',
233
+ "",
229
234
  `## QA scenario`,
230
- '',
231
- t.qaScenario || '_(not specified)_',
232
- '',
233
- ].join('\n');
235
+ "",
236
+ t.qaScenario || "_(not specified)_",
237
+ "",
238
+ ].join("\n");
234
239
  ctx.storage.write(taskPath, meta, body);
235
240
  return { id, created: true };
236
241
  }
@@ -240,18 +245,18 @@ function addMilestoneRecord(ctx, summary, input) {
240
245
  if (summary.milestones.some((m) => m.id === id)) {
241
246
  return { id, created: false };
242
247
  }
243
- const version = input.targetVersion ?? input.dueDate ?? 'TBD';
248
+ const version = input.targetVersion ?? input.dueDate ?? "TBD";
244
249
  summary.milestones.push({
245
- kind: 'milestone',
250
+ kind: "milestone",
246
251
  id,
247
252
  name: input.title,
248
253
  description: input.description,
249
254
  dueDate: input.dueDate,
250
255
  targetVersion: version,
251
256
  });
252
- const path = join(ctx.rootDir, 'milestones', `${id}.md`);
257
+ const path = join(ctx.rootDir, "milestones", `${id}.md`);
253
258
  const meta = {
254
- kind: 'milestone',
259
+ kind: "milestone",
255
260
  id,
256
261
  name: input.title,
257
262
  description: input.description,
@@ -260,12 +265,12 @@ function addMilestoneRecord(ctx, summary, input) {
260
265
  };
261
266
  const body = [
262
267
  `# Milestone: ${input.title}`,
263
- '',
268
+ "",
264
269
  input.description,
265
- '',
270
+ "",
266
271
  `Target version: ${version}`,
267
- '',
268
- ].join('\n');
272
+ "",
273
+ ].join("\n");
269
274
  ctx.storage.write(path, meta, body);
270
275
  return { id, created: true };
271
276
  }
@@ -314,15 +319,15 @@ export function createWorkspaceStubs(ctx) {
314
319
  */
315
320
  function createPlanStub(ctx) {
316
321
  return {
317
- name: 'createPlan',
318
- description: 'Create the FULL project plan in one call: all phases, the tasks nested under each phase, and the target milestone. ' +
319
- 'Preferred over itemized createPhase/createTask/createMilestone calls — one createPlan call persists everything atomically.',
320
- category: 'project',
322
+ name: "createPlan",
323
+ description: "Create the FULL project plan in one call: all phases, the tasks nested under each phase, and the target milestone. " +
324
+ "Preferred over itemized createPhase/createTask/createMilestone calls — one createPlan call persists everything atomically.",
325
+ category: "project",
321
326
  parameters: [],
322
327
  execute: async (args) => {
323
328
  return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
324
- const rawPhases = Array.isArray(args['phases'])
325
- ? args['phases']
329
+ const rawPhases = Array.isArray(args["phases"])
330
+ ? args["phases"]
326
331
  : [];
327
332
  if (rawPhases.length === 0) {
328
333
  return 'createPlan requires a non-empty "phases" array (each phase: { name, description, order, color, tasks: [{ title, description, fileRefs, acceptance, qaScenario, priority }] }).';
@@ -333,64 +338,69 @@ function createPlanStub(ctx) {
333
338
  const phaseIds = [];
334
339
  rawPhases.forEach((rawPhase, idx) => {
335
340
  const { id: phaseId, created } = addPhaseRecord(summary, {
336
- name: rawPhase['name'] ?? `Phase ${idx + 1}`,
337
- description: rawPhase['description'] ?? '',
338
- order: rawPhase['order'] ?? idx + 1,
339
- color: rawPhase['color'] ?? '#3b82f6',
341
+ name: rawPhase["name"] ?? `Phase ${idx + 1}`,
342
+ description: rawPhase["description"] ?? "",
343
+ order: rawPhase["order"] ?? idx + 1,
344
+ color: rawPhase["color"] ?? "#3b82f6",
340
345
  });
341
346
  if (created)
342
347
  phasesCreated += 1;
343
348
  phaseIds.push(phaseId);
344
- const rawTasks = Array.isArray(rawPhase['tasks'])
345
- ? rawPhase['tasks']
349
+ const rawTasks = Array.isArray(rawPhase["tasks"])
350
+ ? rawPhase["tasks"]
346
351
  : [];
347
352
  for (const rawTask of rawTasks) {
348
353
  const { created: taskCreated } = addTaskRecord(ctx, summary, phaseId, {
349
- title: rawTask['title'] ?? 'New Task',
350
- description: rawTask['description'] ?? '',
351
- fileRefs: rawTask['fileRefs'] ?? rawTask['files'] ?? [],
352
- acceptance: rawTask['acceptance'] ?? [],
353
- qaScenario: rawTask['qaScenario'] ?? rawTask['qa'] ?? '',
354
- priority: (rawTask['priority'] ?? 'medium'),
354
+ title: rawTask["title"] ?? "New Task",
355
+ description: rawTask["description"] ?? "",
356
+ fileRefs: rawTask["fileRefs"] ??
357
+ rawTask["files"] ??
358
+ [],
359
+ acceptance: rawTask["acceptance"] ?? [],
360
+ qaScenario: rawTask["qaScenario"] ??
361
+ rawTask["qa"] ??
362
+ "",
363
+ priority: (rawTask["priority"] ??
364
+ "medium"),
355
365
  }, { dedupe: true });
356
366
  if (taskCreated)
357
367
  tasksCreated += 1;
358
368
  }
359
369
  });
360
370
  let milestonesCreated = 0;
361
- const rawMilestone = args['milestone'];
362
- if (rawMilestone && typeof rawMilestone === 'object') {
371
+ const rawMilestone = args["milestone"];
372
+ if (rawMilestone && typeof rawMilestone === "object") {
363
373
  const { created } = addMilestoneRecord(ctx, summary, {
364
- title: rawMilestone['title'] ?? 'v0.1.0 design-complete',
365
- description: rawMilestone['description'] ?? '',
366
- dueDate: rawMilestone['dueDate'],
367
- targetVersion: rawMilestone['targetVersion'],
374
+ title: rawMilestone["title"] ?? "v0.1.0 design-complete",
375
+ description: rawMilestone["description"] ?? "",
376
+ dueDate: rawMilestone["dueDate"],
377
+ targetVersion: rawMilestone["targetVersion"],
368
378
  });
369
379
  if (created)
370
380
  milestonesCreated += 1;
371
381
  }
372
382
  writePlan(ctx, summary);
373
- return `Plan created: ${phasesCreated} phases, ${tasksCreated} tasks, ${milestonesCreated} milestone(s). Phase ids: ${phaseIds.join(', ')}.`;
383
+ return `Plan created: ${phasesCreated} phases, ${tasksCreated} tasks, ${milestonesCreated} milestone(s). Phase ids: ${phaseIds.join(", ")}.`;
374
384
  });
375
385
  },
376
386
  };
377
387
  }
378
388
  function createPhaseStub(ctx) {
379
389
  return {
380
- name: 'createPhase',
381
- description: 'Create a new execution phase in the project plan. Each phase is a milestone toward shipping a feature.',
382
- category: 'project',
390
+ name: "createPhase",
391
+ description: "Create a new execution phase in the project plan. Each phase is a milestone toward shipping a feature.",
392
+ category: "project",
383
393
  parameters: [],
384
394
  execute: async (args) => {
385
395
  return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
386
- const name = args['name'] ?? 'New Phase';
387
- const order = args['order'] ?? 0;
396
+ const name = args["name"] ?? "New Phase";
397
+ const order = args["order"] ?? 0;
388
398
  const summary = readPlan(ctx);
389
399
  const { id, created } = addPhaseRecord(summary, {
390
400
  name,
391
- description: args['description'] ?? '',
401
+ description: args["description"] ?? "",
392
402
  order,
393
- color: args['color'] ?? '#3b82f6',
403
+ color: args["color"] ?? "#3b82f6",
394
404
  });
395
405
  if (!created) {
396
406
  return `Phase "${id}" already exists.`;
@@ -403,27 +413,30 @@ function createPhaseStub(ctx) {
403
413
  }
404
414
  function createTaskStub(ctx) {
405
415
  return {
406
- name: 'createTask',
407
- description: 'Create a task within an existing phase. Tasks have file references, acceptance criteria, and a QA scenario.',
408
- category: 'project',
416
+ name: "createTask",
417
+ description: "Create a task within an existing phase. Tasks have file references, acceptance criteria, and a QA scenario.",
418
+ category: "project",
409
419
  parameters: [],
410
420
  execute: async (args) => {
411
421
  return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
412
- const phaseId = args['phaseId'] ?? args['phase'];
413
- const title = args['title'] ?? 'New Task';
422
+ const phaseId = args["phaseId"] ?? args["phase"];
423
+ const title = args["title"] ?? "New Task";
414
424
  if (!phaseId)
415
- return 'Task requires a phaseId.';
425
+ return "Task requires a phaseId.";
416
426
  const summary = readPlan(ctx);
417
427
  if (!summary.phases.some((p) => p.id === phaseId)) {
418
428
  return `Phase "${phaseId}" not found. Create it first via /createPhase.`;
419
429
  }
420
430
  const { id } = addTaskRecord(ctx, summary, phaseId, {
421
431
  title,
422
- description: args['description'] ?? '',
423
- fileRefs: args['fileRefs'] ?? args['files'] ?? [],
424
- acceptance: args['acceptance'] ?? [],
425
- qaScenario: args['qaScenario'] ?? args['qa'] ?? '',
426
- priority: (args['priority'] ?? 'medium'),
432
+ description: args["description"] ?? "",
433
+ fileRefs: args["fileRefs"] ??
434
+ args["files"] ??
435
+ [],
436
+ acceptance: args["acceptance"] ?? [],
437
+ qaScenario: args["qaScenario"] ?? args["qa"] ?? "",
438
+ priority: (args["priority"] ??
439
+ "medium"),
427
440
  }, { dedupe: false });
428
441
  writePlan(ctx, summary);
429
442
  return `Task "${title}" created (id: ${id}) under phase "${phaseId}".`;
@@ -433,28 +446,44 @@ function createTaskStub(ctx) {
433
446
  }
434
447
  function updateTaskStub(ctx) {
435
448
  return {
436
- name: 'updateTask',
437
- description: 'Update a task\'s status (pending|in_progress|done|blocked).',
438
- category: 'project',
449
+ name: "updateTask",
450
+ description: "Update a task's status (pending|in_progress|done|blocked).",
451
+ category: "project",
439
452
  parameters: [],
440
453
  execute: async (args) => {
441
454
  return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
442
- const taskId = args['taskId'];
443
- const rawStatus = args['status'];
455
+ const taskId = args["taskId"];
456
+ const rawStatus = args["status"];
444
457
  if (!taskId || !rawStatus)
445
- return 'updateTask requires taskId and status.';
446
- const valid = ['pending', 'in_progress', 'done', 'blocked'];
458
+ return "updateTask requires taskId and status.";
459
+ const valid = ["pending", "in_progress", "done", "blocked"];
447
460
  // v0.7.3: models routinely use kanban vocabulary ("todo", "completed")
448
461
  // — map the common synonyms instead of erroring on each attempt.
449
462
  const aliases = {
450
- 'todo': 'pending', 'to-do': 'pending', 'open': 'pending', 'backlog': 'pending',
451
- 'in-progress': 'in_progress', 'in progress': 'in_progress', 'doing': 'in_progress', 'started': 'in_progress', 'active': 'in_progress',
452
- 'complete': 'done', 'completed': 'done', 'finished': 'done', 'closed': 'done', 'resolved': 'done',
453
- 'stuck': 'blocked', 'waiting': 'blocked', 'on-hold': 'blocked', 'on hold': 'blocked',
463
+ todo: "pending",
464
+ "to-do": "pending",
465
+ open: "pending",
466
+ backlog: "pending",
467
+ "in-progress": "in_progress",
468
+ "in progress": "in_progress",
469
+ doing: "in_progress",
470
+ started: "in_progress",
471
+ active: "in_progress",
472
+ complete: "done",
473
+ completed: "done",
474
+ finished: "done",
475
+ closed: "done",
476
+ resolved: "done",
477
+ stuck: "blocked",
478
+ waiting: "blocked",
479
+ "on-hold": "blocked",
480
+ "on hold": "blocked",
454
481
  };
455
- const status = valid.includes(rawStatus) ? rawStatus : aliases[rawStatus.toLowerCase()];
482
+ const status = valid.includes(rawStatus)
483
+ ? rawStatus
484
+ : aliases[rawStatus.toLowerCase()];
456
485
  if (!status)
457
- return `Invalid status: ${rawStatus}. Must be one of: ${valid.join(', ')}.`;
486
+ return `Invalid status: ${rawStatus}. Must be one of: ${valid.join(", ")}.`;
458
487
  const summary = readPlan(ctx);
459
488
  const task = summary.tasks.find((t) => t.id === taskId);
460
489
  if (!task)
@@ -468,45 +497,45 @@ function updateTaskStub(ctx) {
468
497
  }
469
498
  function addIdeaStub(ctx) {
470
499
  return {
471
- name: 'addIdea',
472
- description: 'Add an Architecture Decision Record (ADR) to the project. ADRs document the context, decision, and consequences of significant choices.',
473
- category: 'project',
500
+ name: "addIdea",
501
+ description: "Add an Architecture Decision Record (ADR) to the project. ADRs document the context, decision, and consequences of significant choices.",
502
+ category: "project",
474
503
  parameters: [],
475
504
  execute: async (args) => {
476
505
  return workspaceMutex.run(`${ctx.rootDir}:adr`, () => {
477
- const title = args['title'] ?? 'New Decision';
478
- const context = args['context'] ?? args['content'] ?? '';
479
- const decision = args['decision'] ?? title;
480
- const consequences = args['consequences'] ?? [];
481
- const tags = args['tags'] ?? [];
482
- const category = args['category'] ?? 'General';
506
+ const title = args["title"] ?? "New Decision";
507
+ const context = args["context"] ?? args["content"] ?? "";
508
+ const decision = args["decision"] ?? title;
509
+ const consequences = args["consequences"] ?? [];
510
+ const tags = args["tags"] ?? [];
511
+ const category = args["category"] ?? "General";
483
512
  const id = `${nextAdrId(ctx)}-${slugify(title)}`;
484
- const path = workspaceArtifact(ctx.rootDir, 'decisions', id);
513
+ const path = workspaceArtifact(ctx.rootDir, "decisions", id);
485
514
  const meta = {
486
- kind: 'adr',
487
- status: 'proposed',
515
+ kind: "adr",
516
+ status: "proposed",
488
517
  id,
489
518
  date: new Date().toISOString().slice(0, 10),
490
519
  tags,
491
520
  };
492
521
  const body = [
493
522
  `# ADR ${id}: ${title}`,
494
- '',
523
+ "",
495
524
  `> Category: ${category} · Status: **proposed** · ${meta.date}`,
496
- '',
525
+ "",
497
526
  `## Context`,
498
- '',
527
+ "",
499
528
  context,
500
- '',
529
+ "",
501
530
  `## Decision`,
502
- '',
531
+ "",
503
532
  decision,
504
- '',
533
+ "",
505
534
  `## Consequences`,
506
- '',
535
+ "",
507
536
  ...consequences.map((c) => `- ${c}`),
508
- '',
509
- ].join('\n');
537
+ "",
538
+ ].join("\n");
510
539
  ctx.storage.write(path, meta, body);
511
540
  return `ADR ${id} created: "${title}". Status: proposed. Promote to accepted via /update ADR or manual edit.`;
512
541
  });
@@ -515,24 +544,24 @@ function addIdeaStub(ctx) {
515
544
  }
516
545
  function createMilestoneStub(ctx) {
517
546
  return {
518
- name: 'createMilestone',
519
- description: 'Create a project milestone (a code version that ships when specific acceptance criteria are met).',
520
- category: 'project',
547
+ name: "createMilestone",
548
+ description: "Create a project milestone (a code version that ships when specific acceptance criteria are met).",
549
+ category: "project",
521
550
  parameters: [],
522
551
  execute: async (args) => {
523
552
  return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
524
- const title = args['title'] ?? 'New Milestone';
553
+ const title = args["title"] ?? "New Milestone";
525
554
  const summary = readPlan(ctx);
526
555
  const { id, created } = addMilestoneRecord(ctx, summary, {
527
556
  title,
528
- description: args['description'] ?? '',
529
- dueDate: args['dueDate'],
530
- targetVersion: args['targetVersion'],
557
+ description: args["description"] ?? "",
558
+ dueDate: args["dueDate"],
559
+ targetVersion: args["targetVersion"],
531
560
  });
532
561
  if (!created)
533
562
  return `Milestone "${id}" already exists.`;
534
563
  writePlan(ctx, summary);
535
- const version = summary.milestones.find((m) => m.id === id)?.targetVersion ?? 'TBD';
564
+ const version = summary.milestones.find((m) => m.id === id)?.targetVersion ?? "TBD";
536
565
  return `Milestone "${title}" created (id: ${id}, version: ${version}).`;
537
566
  });
538
567
  },
@@ -540,30 +569,41 @@ function createMilestoneStub(ctx) {
540
569
  }
541
570
  function createDocumentStub(ctx) {
542
571
  return {
543
- name: 'createDocument',
544
- description: 'Create a Markdown document in the project docs (README/CHANGELOG/spec draft produced by the council).',
545
- category: 'vault',
572
+ name: "createDocument",
573
+ description: "Create a Markdown document in the project docs (README/CHANGELOG/spec draft produced by the council).",
574
+ category: "vault",
546
575
  parameters: [],
547
576
  execute: async (args) => {
548
577
  return workspaceMutex.run(`${ctx.rootDir}:doc`, () => {
549
- const title = args['title'] ?? 'New Document';
550
- const content = args['content'] ?? '';
551
- const tags = args['tags'] ?? [];
578
+ const title = args["title"] ?? "New Document";
579
+ const content = args["content"] ?? "";
580
+ const tags = args["tags"] ?? [];
552
581
  // v0.7.8: models routinely pass the FILENAME as title ("risks.md",
553
582
  // "synthesis.md"), which slugified to "risks-md"/"synthesis-md" and
554
583
  // produced duplicated artifacts (docs/risks-md.md next to risks.md,
555
584
  // HANDOFF 2026-07-03 §5.1). Strip the extension before slugifying.
556
- const normalizedTitle = title.replace(/\.(md|markdown)\s*$/i, '');
557
- const id = slugify(normalizedTitle) || `doc-${Date.now()}`;
558
- const path = workspaceArtifact(ctx.rootDir, 'docs', id);
585
+ const normalizedTitle = title.replace(/\.(md|markdown)\s*$/i, "");
586
+ const slug = slugify(normalizedTitle) || `doc-${Date.now()}`;
587
+ if (slug === "risks") {
588
+ const risksPath = workspaceFile(ctx.rootDir, "risks");
589
+ const riskMeta = {
590
+ kind: "risk",
591
+ id: "risks",
592
+ severity: "medium",
593
+ date: new Date().toISOString().slice(0, 10),
594
+ };
595
+ ctx.storage.write(risksPath, riskMeta, content);
596
+ return `Document "${title}" created at risks.md (workspace root).`;
597
+ }
598
+ const path = workspaceArtifact(ctx.rootDir, "docs", slug);
559
599
  const meta = {
560
- kind: 'doc',
561
- id,
600
+ kind: "doc",
601
+ id: slug,
562
602
  date: new Date().toISOString().slice(0, 10),
563
603
  tags,
564
604
  };
565
605
  ctx.storage.write(path, meta, content);
566
- return `Document "${title}" created at docs/${id}.md.`;
606
+ return `Document "${title}" created at docs/${slug}.md.`;
567
607
  });
568
608
  },
569
609
  };
@@ -579,20 +619,20 @@ const SEARCH_NUDGE_THRESHOLD = 5;
579
619
  function searchDocumentsStub(ctx) {
580
620
  let callCount = 0;
581
621
  return {
582
- name: 'searchDocuments',
583
- description: 'Search all workspace documents (`.zelari/**`) for a keyword. Returns matching files with snippets. ' +
584
- 'Search AT MOST 2-3 times, then act on the results: read the matched files with read_file instead of rephrasing the same search.',
585
- category: 'analysis',
622
+ name: "searchDocuments",
623
+ description: "Search all workspace documents (`.zelari/**`) for a keyword. Returns matching files with snippets. " +
624
+ "Search AT MOST 2-3 times, then act on the results: read the matched files with read_file instead of rephrasing the same search.",
625
+ category: "analysis",
586
626
  parameters: [],
587
627
  execute: async (args) => {
588
628
  callCount++;
589
629
  const nudge = callCount > SEARCH_NUDGE_THRESHOLD
590
630
  ? `[note] This is searchDocuments call #${callCount} in this run — STOP searching. Read the files you already found with read_file and act on them.\n\n`
591
- : '';
592
- const rawQuery = (args['query'] ?? '').trim();
593
- const limit = args['limit'] ?? 5;
631
+ : "";
632
+ const rawQuery = (args["query"] ?? "").trim();
633
+ const limit = args["limit"] ?? 5;
594
634
  if (!rawQuery)
595
- return 'searchDocuments requires a query.';
635
+ return "searchDocuments requires a query.";
596
636
  // v0.7.3: models routinely write boolean-ish queries ("plan OR phase OR
597
637
  // milestone"). The previous implementation substring-matched the WHOLE
598
638
  // query string, so every OR query — and any multi-word query that didn't
@@ -607,21 +647,21 @@ function searchDocumentsStub(ctx) {
607
647
  const words = rawQuery
608
648
  .toLowerCase()
609
649
  .split(/[^a-z0-9_-]+/)
610
- .filter((w) => w.length >= 3 && w !== 'or' && w !== 'and' && w !== 'the');
650
+ .filter((w) => w.length >= 3 && w !== "or" && w !== "and" && w !== "the");
611
651
  const files = [
612
- ...ctx.storage.listMarkdown(join(ctx.rootDir, 'decisions')),
613
- ...ctx.storage.listMarkdown(join(ctx.rootDir, 'docs')),
614
- ...ctx.storage.listMarkdown(join(ctx.rootDir, 'reviews')),
615
- ...ctx.storage.listMarkdown(join(ctx.rootDir, 'plan-tasks')),
616
- ...ctx.storage.listMarkdown(join(ctx.rootDir, 'milestones')),
617
- workspaceFile(ctx.rootDir, 'plan'),
618
- workspaceFile(ctx.rootDir, 'risks'),
652
+ ...ctx.storage.listMarkdown(join(ctx.rootDir, "decisions")),
653
+ ...ctx.storage.listMarkdown(join(ctx.rootDir, "docs")),
654
+ ...ctx.storage.listMarkdown(join(ctx.rootDir, "reviews")),
655
+ ...ctx.storage.listMarkdown(join(ctx.rootDir, "plan-tasks")),
656
+ ...ctx.storage.listMarkdown(join(ctx.rootDir, "milestones")),
657
+ workspaceFile(ctx.rootDir, "plan"),
658
+ workspaceFile(ctx.rootDir, "risks"),
619
659
  ];
620
660
  const results = [];
621
661
  for (const file of files) {
622
662
  if (!existsSync(file))
623
663
  continue;
624
- const raw = readFileSync(file, 'utf8');
664
+ const raw = readFileSync(file, "utf8");
625
665
  const content = raw.toLowerCase();
626
666
  let idx = -1;
627
667
  let matchLen = 0;
@@ -647,39 +687,42 @@ function searchDocumentsStub(ctx) {
647
687
  continue;
648
688
  const start = Math.max(0, idx - 40);
649
689
  const end = Math.min(raw.length, idx + matchLen + 40);
650
- const snippet = '' + raw.slice(start, end).replace(/\n/g, ' ').trim() + '';
651
- results.push({ path: relative(ctx.rootDir, file) || basename(file), snippet });
690
+ const snippet = "" + raw.slice(start, end).replace(/\n/g, " ").trim() + "";
691
+ results.push({
692
+ path: relative(ctx.rootDir, file) || basename(file),
693
+ snippet,
694
+ });
652
695
  if (results.length >= limit)
653
696
  break;
654
697
  }
655
698
  if (results.length === 0)
656
699
  return `${nudge}No matches for "${rawQuery}" in workspace.`;
657
- return nudge + results.map((r) => `[${r.path}]\n${r.snippet}`).join('\n\n');
700
+ return (nudge + results.map((r) => `[${r.path}]\n${r.snippet}`).join("\n\n"));
658
701
  },
659
702
  };
660
703
  }
661
704
  function linkDocumentsStub(ctx) {
662
705
  return {
663
- name: 'linkDocuments',
664
- description: 'Link two workspace artifacts. Adds the `related:` field to the source frontmatter so backreferences can be traced.',
665
- category: 'vault',
706
+ name: "linkDocuments",
707
+ description: "Link two workspace artifacts. Adds the `related:` field to the source frontmatter so backreferences can be traced.",
708
+ category: "vault",
666
709
  parameters: [],
667
710
  execute: async (args) => {
668
711
  return workspaceMutex.run(`${ctx.rootDir}:link`, () => {
669
712
  // v0.7.8: the provider-facing JSON schema (toolSchemas.ts) advertises
670
713
  // sourceId/targetPathOrTitle — accept those aliases too, otherwise
671
714
  // every schema-conformant call fails with "requires fromId and toId".
672
- const fromId = args['fromId'] ?? args['sourceId'];
673
- const toId = args['toId']
674
- ?? args['targetId']
675
- ?? args['targetPathOrTitle'];
715
+ const fromId = args["fromId"] ?? args["sourceId"];
716
+ const toId = args["toId"] ??
717
+ args["targetId"] ??
718
+ args["targetPathOrTitle"];
676
719
  if (!fromId || !toId)
677
- return 'linkDocuments requires fromId and toId.';
720
+ return "linkDocuments requires fromId and toId.";
678
721
  // Find the source file by id
679
722
  const allFiles = [
680
- ...ctx.storage.listMarkdown(join(ctx.rootDir, 'decisions')),
681
- ...ctx.storage.listMarkdown(join(ctx.rootDir, 'docs')),
682
- ...ctx.storage.listMarkdown(join(ctx.rootDir, 'reviews')),
723
+ ...ctx.storage.listMarkdown(join(ctx.rootDir, "decisions")),
724
+ ...ctx.storage.listMarkdown(join(ctx.rootDir, "docs")),
725
+ ...ctx.storage.listMarkdown(join(ctx.rootDir, "reviews")),
683
726
  ];
684
727
  const source = allFiles.find((f) => {
685
728
  try {
@@ -705,19 +748,19 @@ function linkDocumentsStub(ctx) {
705
748
  }
706
749
  function getDocumentBacklinksStub(ctx) {
707
750
  return {
708
- name: 'getDocumentBacklinks',
709
- description: 'List all workspace artifacts that link to the given artifact via their `related:` frontmatter.',
710
- category: 'analysis',
751
+ name: "getDocumentBacklinks",
752
+ description: "List all workspace artifacts that link to the given artifact via their `related:` frontmatter.",
753
+ category: "analysis",
711
754
  parameters: [],
712
755
  execute: async (args) => {
713
756
  // v0.7.8: the provider-facing JSON schema advertises `id` — accept it.
714
- const targetId = args['targetId'] ?? args['id'];
757
+ const targetId = args["targetId"] ?? args["id"];
715
758
  if (!targetId)
716
- return 'getDocumentBacklinks requires targetId.';
759
+ return "getDocumentBacklinks requires targetId.";
717
760
  const allFiles = [
718
- ...ctx.storage.listMarkdown(join(ctx.rootDir, 'decisions')),
719
- ...ctx.storage.listMarkdown(join(ctx.rootDir, 'docs')),
720
- ...ctx.storage.listMarkdown(join(ctx.rootDir, 'reviews')),
761
+ ...ctx.storage.listMarkdown(join(ctx.rootDir, "decisions")),
762
+ ...ctx.storage.listMarkdown(join(ctx.rootDir, "docs")),
763
+ ...ctx.storage.listMarkdown(join(ctx.rootDir, "reviews")),
721
764
  ];
722
765
  const backlinks = [];
723
766
  for (const file of allFiles) {
@@ -729,11 +772,13 @@ function getDocumentBacklinksStub(ctx) {
729
772
  backlinks.push(doc.meta.id);
730
773
  }
731
774
  }
732
- catch { /* skip */ }
775
+ catch {
776
+ /* skip */
777
+ }
733
778
  }
734
779
  if (backlinks.length === 0)
735
780
  return `No backlinks to "${targetId}".`;
736
- return backlinks.map((id) => `- ${id}`).join('\n');
781
+ return backlinks.map((id) => `- ${id}`).join("\n");
737
782
  },
738
783
  };
739
784
  }