zelari-code 0.7.8 → 0.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -1
- package/bin/zelari-code.js +1 -1
- package/dist/agents/councilApi.js +7 -7
- package/dist/agents/councilApi.js.map +1 -1
- package/dist/agents/councilDirectives.js +3 -3
- package/dist/agents/promoteMember.js +2 -2
- package/dist/agents/roles.js +50 -32
- package/dist/agents/roles.js.map +1 -1
- package/dist/agents/skills/builtin/debugging.js +4 -4
- package/dist/agents/skills/builtin/debugging.js.map +1 -1
- package/dist/agents/skills/builtin/docs.js +23 -13
- package/dist/agents/skills/builtin/docs.js.map +1 -1
- package/dist/agents/skills/builtin/git-ops.js +3 -3
- package/dist/agents/skills/builtin/git-ops.js.map +1 -1
- package/dist/agents/skills/builtin/planning.js +4 -4
- package/dist/agents/skills/builtin/planning.js.map +1 -1
- package/dist/agents/skills/builtin/refactoring.js +3 -3
- package/dist/agents/skills/builtin/refactoring.js.map +1 -1
- package/dist/agents/skills/builtin/review.js +9 -9
- package/dist/agents/skills/builtin/review.js.map +1 -1
- package/dist/agents/skills/builtin/testing.js +3 -3
- package/dist/agents/skills/builtin/testing.js.map +1 -1
- package/dist/agents/tools.js +2 -9
- package/dist/agents/tools.js.map +1 -1
- package/dist/cli/app.js +48 -17
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/components/CollapsibleToolOutput.js +8 -26
- package/dist/cli/components/CollapsibleToolOutput.js.map +1 -1
- package/dist/cli/components/Header.js +6 -18
- package/dist/cli/components/Header.js.map +1 -1
- package/dist/cli/components/Sidebar.js +86 -37
- package/dist/cli/components/Sidebar.js.map +1 -1
- package/dist/cli/components/SplashScreen.js +117 -71
- package/dist/cli/components/SplashScreen.js.map +1 -1
- package/dist/cli/components/StatusBar.js +22 -18
- package/dist/cli/components/StatusBar.js.map +1 -1
- package/dist/cli/councilConfig.js +38 -0
- package/dist/cli/councilConfig.js.map +1 -0
- package/dist/cli/councilDispatcher.js +35 -49
- package/dist/cli/councilDispatcher.js.map +1 -1
- package/dist/cli/hooks/useChatTurn.js +80 -61
- package/dist/cli/hooks/useChatTurn.js.map +1 -1
- package/dist/cli/hooks/useExecutionTimer.js +25 -0
- package/dist/cli/hooks/useExecutionTimer.js.map +1 -0
- package/dist/cli/hooks/useGitChanges.js +142 -0
- package/dist/cli/hooks/useGitChanges.js.map +1 -0
- package/dist/cli/hooks/useSlashDispatch.js +9 -2
- package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
- package/dist/cli/main.bundled.js +1668 -1151
- package/dist/cli/main.bundled.js.map +4 -4
- package/dist/cli/main.js +1 -1
- package/dist/cli/mcp/mcpClient.js +14 -6
- package/dist/cli/mcp/mcpClient.js.map +1 -1
- package/dist/cli/updater.js +8 -5
- package/dist/cli/updater.js.map +1 -1
- package/dist/cli/utils/cmdline.js +26 -0
- package/dist/cli/utils/cmdline.js.map +1 -0
- package/dist/cli/utils/paths.js +20 -0
- package/dist/cli/utils/paths.js.map +1 -0
- package/dist/cli/workspace/planDetect.js +17 -0
- package/dist/cli/workspace/planDetect.js.map +1 -0
- package/dist/cli/workspace/postCouncilHook.js +39 -28
- package/dist/cli/workspace/postCouncilHook.js.map +1 -1
- package/dist/cli/workspace/stubs.js +241 -196
- package/dist/cli/workspace/stubs.js.map +1 -1
- package/dist/main/core/AgentHarness.js +36 -7
- package/dist/main/core/AgentHarness.js.map +1 -1
- package/dist/main/core/tools/builtin/listFiles.js +43 -5
- package/dist/main/core/tools/builtin/listFiles.js.map +1 -1
- package/dist/main/core/tools/builtin/search.js +28 -120
- package/dist/main/core/tools/builtin/search.js.map +1 -1
- package/package.json +71 -71
- package/dist/cli/oauthCallbackServer.js +0 -207
- package/dist/cli/oauthCallbackServer.js.map +0 -1
- package/dist/main/core/tools/builtin/_walk.js +0 -129
- package/dist/main/core/tools/builtin/_walk.js.map +0 -1
- package/dist/main/core/tools/builtin/diff.js +0 -477
- 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
|
|
16
|
-
import { join, basename, dirname, relative } from
|
|
17
|
-
import { workspaceMutex } from
|
|
18
|
-
import { resolveWorkspaceRoot, workspaceFile, workspaceArtifact, projectName as deriveProjectName, } from
|
|
19
|
-
import { Storage } from
|
|
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,
|
|
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,
|
|
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,
|
|
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 +
|
|
82
|
-
writeFileSync(tmp, JSON.stringify(summary, null, 2),
|
|
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,
|
|
84
|
+
const mdPath = workspaceFile(ctx.rootDir, "plan");
|
|
85
85
|
const summaryMeta = {
|
|
86
|
-
kind:
|
|
87
|
-
id:
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
131
|
-
const pri = t.priority ??
|
|
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(
|
|
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,
|
|
154
|
+
const decisionsDir = join(ctx.rootDir, "decisions");
|
|
155
155
|
if (!existsSync(decisionsDir))
|
|
156
|
-
return
|
|
156
|
+
return "001";
|
|
157
157
|
const existing = readdirSync(decisionsDir)
|
|
158
|
-
.filter((f) => f.endsWith(
|
|
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,
|
|
163
|
+
return String(max + 1).padStart(3, "0");
|
|
164
164
|
}
|
|
165
165
|
function slugify(s) {
|
|
166
|
-
return s
|
|
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:
|
|
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 &&
|
|
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:
|
|
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:
|
|
211
|
+
status: "pending",
|
|
207
212
|
priority: t.priority,
|
|
208
213
|
});
|
|
209
|
-
const taskPath = join(ctx.rootDir,
|
|
214
|
+
const taskPath = join(ctx.rootDir, "plan-tasks", `${id}.md`);
|
|
210
215
|
const meta = {
|
|
211
|
-
kind:
|
|
216
|
+
kind: "task",
|
|
212
217
|
id,
|
|
213
218
|
phaseId,
|
|
214
|
-
status:
|
|
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 ||
|
|
232
|
-
|
|
233
|
-
].join(
|
|
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 ??
|
|
248
|
+
const version = input.targetVersion ?? input.dueDate ?? "TBD";
|
|
244
249
|
summary.milestones.push({
|
|
245
|
-
kind:
|
|
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,
|
|
257
|
+
const path = join(ctx.rootDir, "milestones", `${id}.md`);
|
|
253
258
|
const meta = {
|
|
254
|
-
kind:
|
|
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(
|
|
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:
|
|
318
|
-
description:
|
|
319
|
-
|
|
320
|
-
category:
|
|
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[
|
|
325
|
-
? args[
|
|
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[
|
|
337
|
-
description: rawPhase[
|
|
338
|
-
order: rawPhase[
|
|
339
|
-
color: rawPhase[
|
|
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[
|
|
345
|
-
? rawPhase[
|
|
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[
|
|
350
|
-
description: rawTask[
|
|
351
|
-
fileRefs: rawTask[
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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[
|
|
362
|
-
if (rawMilestone && typeof rawMilestone ===
|
|
371
|
+
const rawMilestone = args["milestone"];
|
|
372
|
+
if (rawMilestone && typeof rawMilestone === "object") {
|
|
363
373
|
const { created } = addMilestoneRecord(ctx, summary, {
|
|
364
|
-
title: rawMilestone[
|
|
365
|
-
description: rawMilestone[
|
|
366
|
-
dueDate: rawMilestone[
|
|
367
|
-
targetVersion: rawMilestone[
|
|
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:
|
|
381
|
-
description:
|
|
382
|
-
category:
|
|
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[
|
|
387
|
-
const order = args[
|
|
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[
|
|
401
|
+
description: args["description"] ?? "",
|
|
392
402
|
order,
|
|
393
|
-
color: args[
|
|
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:
|
|
407
|
-
description:
|
|
408
|
-
category:
|
|
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[
|
|
413
|
-
const title = args[
|
|
422
|
+
const phaseId = args["phaseId"] ?? args["phase"];
|
|
423
|
+
const title = args["title"] ?? "New Task";
|
|
414
424
|
if (!phaseId)
|
|
415
|
-
return
|
|
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[
|
|
423
|
-
fileRefs: args[
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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:
|
|
437
|
-
description:
|
|
438
|
-
category:
|
|
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[
|
|
443
|
-
const rawStatus = args[
|
|
455
|
+
const taskId = args["taskId"];
|
|
456
|
+
const rawStatus = args["status"];
|
|
444
457
|
if (!taskId || !rawStatus)
|
|
445
|
-
return
|
|
446
|
-
const valid = [
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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)
|
|
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:
|
|
472
|
-
description:
|
|
473
|
-
category:
|
|
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[
|
|
478
|
-
const context = args[
|
|
479
|
-
const decision = args[
|
|
480
|
-
const consequences = args[
|
|
481
|
-
const tags = args[
|
|
482
|
-
const category = args[
|
|
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,
|
|
513
|
+
const path = workspaceArtifact(ctx.rootDir, "decisions", id);
|
|
485
514
|
const meta = {
|
|
486
|
-
kind:
|
|
487
|
-
status:
|
|
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(
|
|
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:
|
|
519
|
-
description:
|
|
520
|
-
category:
|
|
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[
|
|
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[
|
|
529
|
-
dueDate: args[
|
|
530
|
-
targetVersion: args[
|
|
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 ??
|
|
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:
|
|
544
|
-
description:
|
|
545
|
-
category:
|
|
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[
|
|
550
|
-
const content = args[
|
|
551
|
-
const tags = args[
|
|
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
|
|
558
|
-
|
|
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:
|
|
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/${
|
|
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:
|
|
583
|
-
description:
|
|
584
|
-
|
|
585
|
-
category:
|
|
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[
|
|
593
|
-
const limit = args[
|
|
631
|
+
: "";
|
|
632
|
+
const rawQuery = (args["query"] ?? "").trim();
|
|
633
|
+
const limit = args["limit"] ?? 5;
|
|
594
634
|
if (!rawQuery)
|
|
595
|
-
return
|
|
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 !==
|
|
650
|
+
.filter((w) => w.length >= 3 && w !== "or" && w !== "and" && w !== "the");
|
|
611
651
|
const files = [
|
|
612
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
613
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
614
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
615
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
616
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
617
|
-
workspaceFile(ctx.rootDir,
|
|
618
|
-
workspaceFile(ctx.rootDir,
|
|
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,
|
|
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 =
|
|
651
|
-
results.push({
|
|
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(
|
|
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:
|
|
664
|
-
description:
|
|
665
|
-
category:
|
|
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[
|
|
673
|
-
const toId = args[
|
|
674
|
-
|
|
675
|
-
|
|
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
|
|
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,
|
|
681
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
682
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
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:
|
|
709
|
-
description:
|
|
710
|
-
category:
|
|
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[
|
|
757
|
+
const targetId = args["targetId"] ?? args["id"];
|
|
715
758
|
if (!targetId)
|
|
716
|
-
return
|
|
759
|
+
return "getDocumentBacklinks requires targetId.";
|
|
717
760
|
const allFiles = [
|
|
718
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
719
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
720
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
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 {
|
|
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(
|
|
781
|
+
return backlinks.map((id) => `- ${id}`).join("\n");
|
|
737
782
|
},
|
|
738
783
|
};
|
|
739
784
|
}
|