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.
- package/README.md +27 -1
- package/dist/cli/app.js +50 -18
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/components/Sidebar.js +86 -39
- package/dist/cli/components/Sidebar.js.map +1 -1
- package/dist/cli/components/SplashScreen.js +204 -0
- package/dist/cli/components/SplashScreen.js.map +1 -0
- 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 +38 -32
- 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 +2560 -1231
- package/dist/cli/main.bundled.js.map +4 -4
- package/dist/cli/main.js +9 -2
- package/dist/cli/main.js.map +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/wizard/index.js +1 -1
- package/dist/cli/wizard/index.js.map +1 -1
- package/dist/cli/workspace/completeDesign.js +128 -0
- package/dist/cli/workspace/completeDesign.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 +157 -20
- package/dist/cli/workspace/postCouncilHook.js.map +1 -1
- package/dist/cli/workspace/stubs.js +406 -225
- package/dist/cli/workspace/stubs.js.map +1 -1
- package/package.json +71 -71
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* workspace/stubs.ts — CLI workspace tool stubs.
|
|
3
3
|
*
|
|
4
|
-
* Implements the
|
|
5
|
-
* `updateTask`, `addIdea`, `createMilestone`,
|
|
6
|
-
* `searchDocuments`, `linkDocuments`,
|
|
7
|
-
* filesystem-backed stubs that persist to
|
|
4
|
+
* Implements the 10 council workspace tools (`createPlan`, `createPhase`,
|
|
5
|
+
* `createTask`, `updateTask`, `addIdea`, `createMilestone`,
|
|
6
|
+
* `createDocument`, `searchDocuments`, `linkDocuments`,
|
|
7
|
+
* `getDocumentBacklinks`) as filesystem-backed stubs that persist to
|
|
8
|
+
* `.zelari/` at the project root.
|
|
8
9
|
*
|
|
9
10
|
* Replaces the AnathemaBrain Electron-only ctx (e.g. `ctx.addIdea(...)`)
|
|
10
11
|
* with a `WorkspaceContext` that writes Markdown + YAML frontmatter files.
|
|
11
12
|
*
|
|
12
13
|
* @see docs/plans/2026-07-01-council-workspace-cli-stubs.md
|
|
13
14
|
*/
|
|
14
|
-
import { existsSync, readdirSync, writeFileSync, readFileSync, mkdirSync, renameSync } from
|
|
15
|
-
import { join, basename, dirname, relative } from
|
|
16
|
-
import { workspaceMutex } from
|
|
17
|
-
import { resolveWorkspaceRoot, workspaceFile, workspaceArtifact, projectName as deriveProjectName, } from
|
|
18
|
-
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";
|
|
19
20
|
// ── Context factory ───────────────────────────────────────────────────
|
|
20
21
|
/**
|
|
21
22
|
* Build a WorkspaceContext rooted at the project's `.zelari/` directory.
|
|
@@ -31,7 +32,7 @@ export function createWorkspaceContext(projectRoot = process.cwd()) {
|
|
|
31
32
|
}
|
|
32
33
|
/** Path of the machine-readable plan store (source of truth since v0.7.3). */
|
|
33
34
|
function planJsonPath(ctx) {
|
|
34
|
-
return join(ctx.rootDir,
|
|
35
|
+
return join(ctx.rootDir, "plan.json");
|
|
35
36
|
}
|
|
36
37
|
function readPlan(ctx) {
|
|
37
38
|
// v0.7.3: the plan's source of truth is plan.json. The previous
|
|
@@ -44,7 +45,7 @@ function readPlan(ctx) {
|
|
|
44
45
|
const jsonPath = planJsonPath(ctx);
|
|
45
46
|
if (existsSync(jsonPath)) {
|
|
46
47
|
try {
|
|
47
|
-
const parsed = JSON.parse(readFileSync(jsonPath,
|
|
48
|
+
const parsed = JSON.parse(readFileSync(jsonPath, "utf8"));
|
|
48
49
|
return {
|
|
49
50
|
phases: Array.isArray(parsed.phases) ? parsed.phases : [],
|
|
50
51
|
tasks: Array.isArray(parsed.tasks) ? parsed.tasks : [],
|
|
@@ -57,7 +58,7 @@ function readPlan(ctx) {
|
|
|
57
58
|
}
|
|
58
59
|
// Legacy migration: one best-effort parse of the old plan.md frontmatter
|
|
59
60
|
// (pre-v0.7.3 files). The next writePlan persists it as plan.json.
|
|
60
|
-
const path = workspaceFile(ctx.rootDir,
|
|
61
|
+
const path = workspaceFile(ctx.rootDir, "plan");
|
|
61
62
|
const doc = ctx.storage.readIfExists(path);
|
|
62
63
|
if (!doc)
|
|
63
64
|
return { phases: [], tasks: [], milestones: [] };
|
|
@@ -77,13 +78,13 @@ function readPlan(ctx) {
|
|
|
77
78
|
function writePlan(ctx, summary) {
|
|
78
79
|
const jsonPath = planJsonPath(ctx);
|
|
79
80
|
mkdirSync(dirname(jsonPath), { recursive: true });
|
|
80
|
-
const tmp = jsonPath +
|
|
81
|
-
writeFileSync(tmp, JSON.stringify(summary, null, 2),
|
|
81
|
+
const tmp = jsonPath + ".tmp-" + process.pid;
|
|
82
|
+
writeFileSync(tmp, JSON.stringify(summary, null, 2), "utf8");
|
|
82
83
|
renameSync(tmp, jsonPath);
|
|
83
|
-
const mdPath = workspaceFile(ctx.rootDir,
|
|
84
|
+
const mdPath = workspaceFile(ctx.rootDir, "plan");
|
|
84
85
|
const summaryMeta = {
|
|
85
|
-
kind:
|
|
86
|
-
id:
|
|
86
|
+
kind: "plan-summary",
|
|
87
|
+
id: "_plan",
|
|
87
88
|
updatedAt: new Date().toISOString(),
|
|
88
89
|
};
|
|
89
90
|
ctx.storage.write(mdPath, summaryMeta, renderPlanBody(summary));
|
|
@@ -92,77 +93,195 @@ function writePlan(ctx, summary) {
|
|
|
92
93
|
function renderPlanBody(summary) {
|
|
93
94
|
const lines = [];
|
|
94
95
|
lines.push(`# Execution Plan`);
|
|
95
|
-
lines.push(
|
|
96
|
+
lines.push("");
|
|
96
97
|
lines.push(`> Auto-generated by Zelari Code council. Do not edit by hand.`);
|
|
97
|
-
lines.push(
|
|
98
|
+
lines.push("");
|
|
98
99
|
// Phases
|
|
99
100
|
lines.push(`## Phases`);
|
|
100
|
-
lines.push(
|
|
101
|
+
lines.push("");
|
|
101
102
|
if (summary.phases.length === 0) {
|
|
102
103
|
lines.push(`_(none yet)_`);
|
|
103
104
|
}
|
|
104
105
|
else {
|
|
105
106
|
for (const phase of [...summary.phases].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))) {
|
|
106
|
-
lines.push(`### ${phase.order ??
|
|
107
|
-
lines.push(
|
|
107
|
+
lines.push(`### ${phase.order ?? "?"}. ${phase.id} \`${phase.color ?? ""}\``);
|
|
108
|
+
lines.push("");
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
|
-
lines.push(
|
|
111
|
+
lines.push("");
|
|
111
112
|
// Tasks grouped by phase
|
|
112
113
|
lines.push(`## Tasks`);
|
|
113
|
-
lines.push(
|
|
114
|
+
lines.push("");
|
|
114
115
|
if (summary.tasks.length === 0) {
|
|
115
116
|
lines.push(`_(none yet)_`);
|
|
116
117
|
}
|
|
117
118
|
else {
|
|
118
119
|
const byPhase = new Map();
|
|
119
120
|
for (const task of summary.tasks) {
|
|
120
|
-
const pid = task.phaseId ??
|
|
121
|
+
const pid = task.phaseId ?? "_unassigned";
|
|
121
122
|
if (!byPhase.has(pid))
|
|
122
123
|
byPhase.set(pid, []);
|
|
123
124
|
byPhase.get(pid).push(task);
|
|
124
125
|
}
|
|
125
126
|
for (const [phaseId, tasks] of byPhase) {
|
|
126
127
|
lines.push(`### Phase: \`${phaseId}\``);
|
|
127
|
-
lines.push(
|
|
128
|
+
lines.push("");
|
|
128
129
|
for (const t of tasks) {
|
|
129
|
-
const status = t.status ??
|
|
130
|
-
const pri = t.priority ??
|
|
130
|
+
const status = t.status ?? "pending";
|
|
131
|
+
const pri = t.priority ?? "medium";
|
|
131
132
|
lines.push(`- [ ] **${t.id}** _(${status}, ${pri})_`);
|
|
132
133
|
}
|
|
133
|
-
lines.push(
|
|
134
|
+
lines.push("");
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
|
-
lines.push(
|
|
137
|
+
lines.push("");
|
|
137
138
|
// Milestones
|
|
138
139
|
lines.push(`## Milestones`);
|
|
139
|
-
lines.push(
|
|
140
|
+
lines.push("");
|
|
140
141
|
if (summary.milestones.length === 0) {
|
|
141
142
|
lines.push(`_(none yet)_`);
|
|
142
143
|
}
|
|
143
144
|
else {
|
|
144
145
|
for (const m of summary.milestones) {
|
|
145
|
-
lines.push(`- **${m.id}** → ${m.targetVersion ??
|
|
146
|
+
lines.push(`- **${m.id}** → ${m.targetVersion ?? "?"}`);
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
|
-
lines.push(
|
|
149
|
-
return lines.join(
|
|
149
|
+
lines.push("");
|
|
150
|
+
return lines.join("\n");
|
|
150
151
|
}
|
|
151
152
|
// ── ADR (decisions/) helpers ──────────────────────────────────────────
|
|
152
153
|
function nextAdrId(ctx) {
|
|
153
|
-
const decisionsDir = join(ctx.rootDir,
|
|
154
|
+
const decisionsDir = join(ctx.rootDir, "decisions");
|
|
154
155
|
if (!existsSync(decisionsDir))
|
|
155
|
-
return
|
|
156
|
+
return "001";
|
|
156
157
|
const existing = readdirSync(decisionsDir)
|
|
157
|
-
.filter((f) => f.endsWith(
|
|
158
|
+
.filter((f) => f.endsWith(".md"))
|
|
158
159
|
.map((f) => f.match(/^(\d+)-/))
|
|
159
160
|
.filter((m) => !!m)
|
|
160
161
|
.map((m) => parseInt(m[1], 10));
|
|
161
162
|
const max = existing.length === 0 ? 0 : Math.max(...existing);
|
|
162
|
-
return String(max + 1).padStart(3,
|
|
163
|
+
return String(max + 1).padStart(3, "0");
|
|
163
164
|
}
|
|
164
165
|
function slugify(s) {
|
|
165
|
-
return s
|
|
166
|
+
return s
|
|
167
|
+
.toLowerCase()
|
|
168
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
169
|
+
.replace(/^-|-$/g, "")
|
|
170
|
+
.slice(0, 60);
|
|
171
|
+
}
|
|
172
|
+
/** Add a phase record. Returns the phase id and whether it was newly created. */
|
|
173
|
+
function addPhaseRecord(summary, input) {
|
|
174
|
+
const id = slugify(input.name) || `phase-${input.order}`;
|
|
175
|
+
if (summary.phases.some((p) => p.id === id)) {
|
|
176
|
+
return { id, created: false };
|
|
177
|
+
}
|
|
178
|
+
summary.phases.push({
|
|
179
|
+
kind: "phase",
|
|
180
|
+
id,
|
|
181
|
+
name: input.name,
|
|
182
|
+
description: input.description,
|
|
183
|
+
order: input.order,
|
|
184
|
+
color: input.color,
|
|
185
|
+
});
|
|
186
|
+
return { id, created: true };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Add a task record + its per-task file. With `dedupe: true` a task whose
|
|
190
|
+
* title already exists in the same phase is treated as the same task and
|
|
191
|
+
* NOT re-added (protects the createPlan batch path from duplicating tasks
|
|
192
|
+
* that a partial itemized run already persisted). The itemized createTask
|
|
193
|
+
* stub keeps `dedupe: false` for backward compatibility.
|
|
194
|
+
*/
|
|
195
|
+
function addTaskRecord(ctx, summary, phaseId, t, options) {
|
|
196
|
+
const slug = slugify(t.title);
|
|
197
|
+
if (options.dedupe) {
|
|
198
|
+
const existing = summary.tasks.find((k) => k.phaseId === phaseId &&
|
|
199
|
+
k.id.replace(/-\d+$/, "") === `${phaseId}-${slug}`);
|
|
200
|
+
if (existing)
|
|
201
|
+
return { id: existing.id, created: false };
|
|
202
|
+
}
|
|
203
|
+
const id = `${phaseId}-${slug}-${summary.tasks.filter((k) => k.phaseId === phaseId).length + 1}`;
|
|
204
|
+
summary.tasks.push({
|
|
205
|
+
kind: "task",
|
|
206
|
+
id,
|
|
207
|
+
// v0.7.3: keep the human-readable title in plan.json so
|
|
208
|
+
// buildPlanSummary can render it in the system prompt.
|
|
209
|
+
name: t.title,
|
|
210
|
+
phaseId,
|
|
211
|
+
status: "pending",
|
|
212
|
+
priority: t.priority,
|
|
213
|
+
});
|
|
214
|
+
const taskPath = join(ctx.rootDir, "plan-tasks", `${id}.md`);
|
|
215
|
+
const meta = {
|
|
216
|
+
kind: "task",
|
|
217
|
+
id,
|
|
218
|
+
phaseId,
|
|
219
|
+
status: "pending",
|
|
220
|
+
priority: t.priority,
|
|
221
|
+
tags: t.fileRefs,
|
|
222
|
+
};
|
|
223
|
+
const body = [
|
|
224
|
+
`# ${t.title}`,
|
|
225
|
+
"",
|
|
226
|
+
t.description,
|
|
227
|
+
"",
|
|
228
|
+
`## File references`,
|
|
229
|
+
...t.fileRefs.map((f) => `- \`${f}\``),
|
|
230
|
+
"",
|
|
231
|
+
`## Acceptance criteria`,
|
|
232
|
+
...t.acceptance.map((a) => `- ${a}`),
|
|
233
|
+
"",
|
|
234
|
+
`## QA scenario`,
|
|
235
|
+
"",
|
|
236
|
+
t.qaScenario || "_(not specified)_",
|
|
237
|
+
"",
|
|
238
|
+
].join("\n");
|
|
239
|
+
ctx.storage.write(taskPath, meta, body);
|
|
240
|
+
return { id, created: true };
|
|
241
|
+
}
|
|
242
|
+
/** Add a milestone record + its file. Returns id and whether newly created. */
|
|
243
|
+
function addMilestoneRecord(ctx, summary, input) {
|
|
244
|
+
const id = `m-${slugify(input.title)}`;
|
|
245
|
+
if (summary.milestones.some((m) => m.id === id)) {
|
|
246
|
+
return { id, created: false };
|
|
247
|
+
}
|
|
248
|
+
const version = input.targetVersion ?? input.dueDate ?? "TBD";
|
|
249
|
+
summary.milestones.push({
|
|
250
|
+
kind: "milestone",
|
|
251
|
+
id,
|
|
252
|
+
name: input.title,
|
|
253
|
+
description: input.description,
|
|
254
|
+
dueDate: input.dueDate,
|
|
255
|
+
targetVersion: version,
|
|
256
|
+
});
|
|
257
|
+
const path = join(ctx.rootDir, "milestones", `${id}.md`);
|
|
258
|
+
const meta = {
|
|
259
|
+
kind: "milestone",
|
|
260
|
+
id,
|
|
261
|
+
name: input.title,
|
|
262
|
+
description: input.description,
|
|
263
|
+
dueDate: input.dueDate,
|
|
264
|
+
targetVersion: version,
|
|
265
|
+
};
|
|
266
|
+
const body = [
|
|
267
|
+
`# Milestone: ${input.title}`,
|
|
268
|
+
"",
|
|
269
|
+
input.description,
|
|
270
|
+
"",
|
|
271
|
+
`Target version: ${version}`,
|
|
272
|
+
"",
|
|
273
|
+
].join("\n");
|
|
274
|
+
ctx.storage.write(path, meta, body);
|
|
275
|
+
return { id, created: true };
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Read-only view of the current plan (phases + tasks + milestones).
|
|
279
|
+
* Exported for the built-in complete-design fallback (completeDesign.ts)
|
|
280
|
+
* so it can inspect per-phase task coverage without duplicating the
|
|
281
|
+
* plan.json / legacy plan.md read logic.
|
|
282
|
+
*/
|
|
283
|
+
export function readPlanSummary(ctx) {
|
|
284
|
+
return readPlan(ctx);
|
|
166
285
|
}
|
|
167
286
|
// ── Stub factory ─────────────────────────────────────────────────────
|
|
168
287
|
/**
|
|
@@ -173,6 +292,7 @@ function slugify(s) {
|
|
|
173
292
|
*/
|
|
174
293
|
export function createWorkspaceStubs(ctx) {
|
|
175
294
|
return [
|
|
295
|
+
createPlanStub(ctx),
|
|
176
296
|
createPhaseStub(ctx),
|
|
177
297
|
createTaskStub(ctx),
|
|
178
298
|
updateTaskStub(ctx),
|
|
@@ -185,32 +305,106 @@ export function createWorkspaceStubs(ctx) {
|
|
|
185
305
|
];
|
|
186
306
|
}
|
|
187
307
|
// ── Individual stubs ──────────────────────────────────────────────────
|
|
188
|
-
|
|
308
|
+
/**
|
|
309
|
+
* v0.7.8 — Batch plan creation. One tool call persists the WHOLE plan:
|
|
310
|
+
* every phase, the tasks nested under each phase, and the milestone.
|
|
311
|
+
*
|
|
312
|
+
* Rationale: the itemized contract (4×createPhase + 12×createTask +
|
|
313
|
+
* 1×createMilestone = 17 sequential tool calls) exceeds what mid-tier
|
|
314
|
+
* council models reliably emit in one turn (composer-2.5 persisted ≤7 of
|
|
315
|
+
* 17 in the 2026-07-03 live tests, see HANDOFF.md). A single structured
|
|
316
|
+
* call collapses the whole contract into one emission, which the same
|
|
317
|
+
* models satisfy reliably (proven by the 1-call retry paths of Minosse
|
|
318
|
+
* and Lucifero).
|
|
319
|
+
*/
|
|
320
|
+
function createPlanStub(ctx) {
|
|
189
321
|
return {
|
|
190
|
-
name:
|
|
191
|
-
description:
|
|
192
|
-
|
|
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",
|
|
193
326
|
parameters: [],
|
|
194
327
|
execute: async (args) => {
|
|
195
328
|
return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
329
|
+
const rawPhases = Array.isArray(args["phases"])
|
|
330
|
+
? args["phases"]
|
|
331
|
+
: [];
|
|
332
|
+
if (rawPhases.length === 0) {
|
|
333
|
+
return 'createPlan requires a non-empty "phases" array (each phase: { name, description, order, color, tasks: [{ title, description, fileRefs, acceptance, qaScenario, priority }] }).';
|
|
334
|
+
}
|
|
201
335
|
const summary = readPlan(ctx);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
336
|
+
let phasesCreated = 0;
|
|
337
|
+
let tasksCreated = 0;
|
|
338
|
+
const phaseIds = [];
|
|
339
|
+
rawPhases.forEach((rawPhase, idx) => {
|
|
340
|
+
const { id: phaseId, created } = addPhaseRecord(summary, {
|
|
341
|
+
name: rawPhase["name"] ?? `Phase ${idx + 1}`,
|
|
342
|
+
description: rawPhase["description"] ?? "",
|
|
343
|
+
order: rawPhase["order"] ?? idx + 1,
|
|
344
|
+
color: rawPhase["color"] ?? "#3b82f6",
|
|
345
|
+
});
|
|
346
|
+
if (created)
|
|
347
|
+
phasesCreated += 1;
|
|
348
|
+
phaseIds.push(phaseId);
|
|
349
|
+
const rawTasks = Array.isArray(rawPhase["tasks"])
|
|
350
|
+
? rawPhase["tasks"]
|
|
351
|
+
: [];
|
|
352
|
+
for (const rawTask of rawTasks) {
|
|
353
|
+
const { created: taskCreated } = addTaskRecord(ctx, summary, phaseId, {
|
|
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"),
|
|
365
|
+
}, { dedupe: true });
|
|
366
|
+
if (taskCreated)
|
|
367
|
+
tasksCreated += 1;
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
let milestonesCreated = 0;
|
|
371
|
+
const rawMilestone = args["milestone"];
|
|
372
|
+
if (rawMilestone && typeof rawMilestone === "object") {
|
|
373
|
+
const { created } = addMilestoneRecord(ctx, summary, {
|
|
374
|
+
title: rawMilestone["title"] ?? "v0.1.0 design-complete",
|
|
375
|
+
description: rawMilestone["description"] ?? "",
|
|
376
|
+
dueDate: rawMilestone["dueDate"],
|
|
377
|
+
targetVersion: rawMilestone["targetVersion"],
|
|
378
|
+
});
|
|
379
|
+
if (created)
|
|
380
|
+
milestonesCreated += 1;
|
|
205
381
|
}
|
|
206
|
-
summary
|
|
207
|
-
|
|
208
|
-
|
|
382
|
+
writePlan(ctx, summary);
|
|
383
|
+
return `Plan created: ${phasesCreated} phases, ${tasksCreated} tasks, ${milestonesCreated} milestone(s). Phase ids: ${phaseIds.join(", ")}.`;
|
|
384
|
+
});
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function createPhaseStub(ctx) {
|
|
389
|
+
return {
|
|
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",
|
|
393
|
+
parameters: [],
|
|
394
|
+
execute: async (args) => {
|
|
395
|
+
return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
|
|
396
|
+
const name = args["name"] ?? "New Phase";
|
|
397
|
+
const order = args["order"] ?? 0;
|
|
398
|
+
const summary = readPlan(ctx);
|
|
399
|
+
const { id, created } = addPhaseRecord(summary, {
|
|
209
400
|
name,
|
|
210
|
-
description,
|
|
401
|
+
description: args["description"] ?? "",
|
|
211
402
|
order,
|
|
212
|
-
color,
|
|
403
|
+
color: args["color"] ?? "#3b82f6",
|
|
213
404
|
});
|
|
405
|
+
if (!created) {
|
|
406
|
+
return `Phase "${id}" already exists.`;
|
|
407
|
+
}
|
|
214
408
|
writePlan(ctx, summary);
|
|
215
409
|
return `Phase "${name}" created (id: ${id}, order: ${order}).`;
|
|
216
410
|
});
|
|
@@ -219,64 +413,32 @@ function createPhaseStub(ctx) {
|
|
|
219
413
|
}
|
|
220
414
|
function createTaskStub(ctx) {
|
|
221
415
|
return {
|
|
222
|
-
name:
|
|
223
|
-
description:
|
|
224
|
-
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",
|
|
225
419
|
parameters: [],
|
|
226
420
|
execute: async (args) => {
|
|
227
421
|
return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
|
|
228
|
-
const phaseId = args[
|
|
229
|
-
const title = args[
|
|
230
|
-
const description = args['description'] ?? '';
|
|
231
|
-
const fileRefs = args['fileRefs'] ?? args['files'] ?? [];
|
|
232
|
-
const acceptance = args['acceptance'] ?? [];
|
|
233
|
-
const qaScenario = args['qaScenario'] ?? args['qa'] ?? '';
|
|
234
|
-
const priority = args['priority'] ?? 'medium';
|
|
422
|
+
const phaseId = args["phaseId"] ?? args["phase"];
|
|
423
|
+
const title = args["title"] ?? "New Task";
|
|
235
424
|
if (!phaseId)
|
|
236
|
-
return
|
|
425
|
+
return "Task requires a phaseId.";
|
|
237
426
|
const summary = readPlan(ctx);
|
|
238
427
|
if (!summary.phases.some((p) => p.id === phaseId)) {
|
|
239
428
|
return `Phase "${phaseId}" not found. Create it first via /createPhase.`;
|
|
240
429
|
}
|
|
241
|
-
const id =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
});
|
|
430
|
+
const { id } = addTaskRecord(ctx, summary, phaseId, {
|
|
431
|
+
title,
|
|
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"),
|
|
440
|
+
}, { dedupe: false });
|
|
252
441
|
writePlan(ctx, summary);
|
|
253
|
-
// Also write the task body to a per-task file so it doesn't get lost
|
|
254
|
-
const taskPath = join(ctx.rootDir, 'plan-tasks', `${id}.md`);
|
|
255
|
-
const meta = {
|
|
256
|
-
kind: 'task',
|
|
257
|
-
id,
|
|
258
|
-
phaseId,
|
|
259
|
-
status: 'pending',
|
|
260
|
-
priority: priority,
|
|
261
|
-
tags: fileRefs,
|
|
262
|
-
};
|
|
263
|
-
const body = [
|
|
264
|
-
`# ${title}`,
|
|
265
|
-
'',
|
|
266
|
-
description,
|
|
267
|
-
'',
|
|
268
|
-
`## File references`,
|
|
269
|
-
...fileRefs.map((f) => `- \`${f}\``),
|
|
270
|
-
'',
|
|
271
|
-
`## Acceptance criteria`,
|
|
272
|
-
...acceptance.map((a) => `- ${a}`),
|
|
273
|
-
'',
|
|
274
|
-
`## QA scenario`,
|
|
275
|
-
'',
|
|
276
|
-
qaScenario || '_(not specified)_',
|
|
277
|
-
'',
|
|
278
|
-
].join('\n');
|
|
279
|
-
ctx.storage.write(taskPath, meta, body);
|
|
280
442
|
return `Task "${title}" created (id: ${id}) under phase "${phaseId}".`;
|
|
281
443
|
});
|
|
282
444
|
},
|
|
@@ -284,28 +446,44 @@ function createTaskStub(ctx) {
|
|
|
284
446
|
}
|
|
285
447
|
function updateTaskStub(ctx) {
|
|
286
448
|
return {
|
|
287
|
-
name:
|
|
288
|
-
description:
|
|
289
|
-
category:
|
|
449
|
+
name: "updateTask",
|
|
450
|
+
description: "Update a task's status (pending|in_progress|done|blocked).",
|
|
451
|
+
category: "project",
|
|
290
452
|
parameters: [],
|
|
291
453
|
execute: async (args) => {
|
|
292
454
|
return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
|
|
293
|
-
const taskId = args[
|
|
294
|
-
const rawStatus = args[
|
|
455
|
+
const taskId = args["taskId"];
|
|
456
|
+
const rawStatus = args["status"];
|
|
295
457
|
if (!taskId || !rawStatus)
|
|
296
|
-
return
|
|
297
|
-
const valid = [
|
|
458
|
+
return "updateTask requires taskId and status.";
|
|
459
|
+
const valid = ["pending", "in_progress", "done", "blocked"];
|
|
298
460
|
// v0.7.3: models routinely use kanban vocabulary ("todo", "completed")
|
|
299
461
|
// — map the common synonyms instead of erroring on each attempt.
|
|
300
462
|
const aliases = {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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",
|
|
305
481
|
};
|
|
306
|
-
const status = valid.includes(rawStatus)
|
|
482
|
+
const status = valid.includes(rawStatus)
|
|
483
|
+
? rawStatus
|
|
484
|
+
: aliases[rawStatus.toLowerCase()];
|
|
307
485
|
if (!status)
|
|
308
|
-
return `Invalid status: ${rawStatus}. Must be one of: ${valid.join(
|
|
486
|
+
return `Invalid status: ${rawStatus}. Must be one of: ${valid.join(", ")}.`;
|
|
309
487
|
const summary = readPlan(ctx);
|
|
310
488
|
const task = summary.tasks.find((t) => t.id === taskId);
|
|
311
489
|
if (!task)
|
|
@@ -319,45 +497,45 @@ function updateTaskStub(ctx) {
|
|
|
319
497
|
}
|
|
320
498
|
function addIdeaStub(ctx) {
|
|
321
499
|
return {
|
|
322
|
-
name:
|
|
323
|
-
description:
|
|
324
|
-
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",
|
|
325
503
|
parameters: [],
|
|
326
504
|
execute: async (args) => {
|
|
327
505
|
return workspaceMutex.run(`${ctx.rootDir}:adr`, () => {
|
|
328
|
-
const title = args[
|
|
329
|
-
const context = args[
|
|
330
|
-
const decision = args[
|
|
331
|
-
const consequences = args[
|
|
332
|
-
const tags = args[
|
|
333
|
-
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";
|
|
334
512
|
const id = `${nextAdrId(ctx)}-${slugify(title)}`;
|
|
335
|
-
const path = workspaceArtifact(ctx.rootDir,
|
|
513
|
+
const path = workspaceArtifact(ctx.rootDir, "decisions", id);
|
|
336
514
|
const meta = {
|
|
337
|
-
kind:
|
|
338
|
-
status:
|
|
515
|
+
kind: "adr",
|
|
516
|
+
status: "proposed",
|
|
339
517
|
id,
|
|
340
518
|
date: new Date().toISOString().slice(0, 10),
|
|
341
519
|
tags,
|
|
342
520
|
};
|
|
343
521
|
const body = [
|
|
344
522
|
`# ADR ${id}: ${title}`,
|
|
345
|
-
|
|
523
|
+
"",
|
|
346
524
|
`> Category: ${category} · Status: **proposed** · ${meta.date}`,
|
|
347
|
-
|
|
525
|
+
"",
|
|
348
526
|
`## Context`,
|
|
349
|
-
|
|
527
|
+
"",
|
|
350
528
|
context,
|
|
351
|
-
|
|
529
|
+
"",
|
|
352
530
|
`## Decision`,
|
|
353
|
-
|
|
531
|
+
"",
|
|
354
532
|
decision,
|
|
355
|
-
|
|
533
|
+
"",
|
|
356
534
|
`## Consequences`,
|
|
357
|
-
|
|
535
|
+
"",
|
|
358
536
|
...consequences.map((c) => `- ${c}`),
|
|
359
|
-
|
|
360
|
-
].join(
|
|
537
|
+
"",
|
|
538
|
+
].join("\n");
|
|
361
539
|
ctx.storage.write(path, meta, body);
|
|
362
540
|
return `ADR ${id} created: "${title}". Status: proposed. Promote to accepted via /update ADR or manual edit.`;
|
|
363
541
|
});
|
|
@@ -366,48 +544,24 @@ function addIdeaStub(ctx) {
|
|
|
366
544
|
}
|
|
367
545
|
function createMilestoneStub(ctx) {
|
|
368
546
|
return {
|
|
369
|
-
name:
|
|
370
|
-
description:
|
|
371
|
-
category:
|
|
547
|
+
name: "createMilestone",
|
|
548
|
+
description: "Create a project milestone (a code version that ships when specific acceptance criteria are met).",
|
|
549
|
+
category: "project",
|
|
372
550
|
parameters: [],
|
|
373
551
|
execute: async (args) => {
|
|
374
552
|
return workspaceMutex.run(`${ctx.rootDir}:plan`, () => {
|
|
375
|
-
const title = args[
|
|
376
|
-
const description = args['description'] ?? '';
|
|
377
|
-
const dueDate = args['dueDate'];
|
|
378
|
-
const id = `m-${slugify(title)}`;
|
|
553
|
+
const title = args["title"] ?? "New Milestone";
|
|
379
554
|
const summary = readPlan(ctx);
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
id,
|
|
386
|
-
name: title,
|
|
387
|
-
description,
|
|
388
|
-
dueDate,
|
|
389
|
-
targetVersion: version,
|
|
555
|
+
const { id, created } = addMilestoneRecord(ctx, summary, {
|
|
556
|
+
title,
|
|
557
|
+
description: args["description"] ?? "",
|
|
558
|
+
dueDate: args["dueDate"],
|
|
559
|
+
targetVersion: args["targetVersion"],
|
|
390
560
|
});
|
|
561
|
+
if (!created)
|
|
562
|
+
return `Milestone "${id}" already exists.`;
|
|
391
563
|
writePlan(ctx, summary);
|
|
392
|
-
|
|
393
|
-
const path = join(ctx.rootDir, 'milestones', `${id}.md`);
|
|
394
|
-
const meta = {
|
|
395
|
-
kind: 'milestone',
|
|
396
|
-
id,
|
|
397
|
-
name: title,
|
|
398
|
-
description,
|
|
399
|
-
dueDate,
|
|
400
|
-
targetVersion: version,
|
|
401
|
-
};
|
|
402
|
-
const body = [
|
|
403
|
-
`# Milestone: ${title}`,
|
|
404
|
-
'',
|
|
405
|
-
description,
|
|
406
|
-
'',
|
|
407
|
-
`Target version: ${version}`,
|
|
408
|
-
'',
|
|
409
|
-
].join('\n');
|
|
410
|
-
ctx.storage.write(path, meta, body);
|
|
564
|
+
const version = summary.milestones.find((m) => m.id === id)?.targetVersion ?? "TBD";
|
|
411
565
|
return `Milestone "${title}" created (id: ${id}, version: ${version}).`;
|
|
412
566
|
});
|
|
413
567
|
},
|
|
@@ -415,25 +569,41 @@ function createMilestoneStub(ctx) {
|
|
|
415
569
|
}
|
|
416
570
|
function createDocumentStub(ctx) {
|
|
417
571
|
return {
|
|
418
|
-
name:
|
|
419
|
-
description:
|
|
420
|
-
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",
|
|
421
575
|
parameters: [],
|
|
422
576
|
execute: async (args) => {
|
|
423
577
|
return workspaceMutex.run(`${ctx.rootDir}:doc`, () => {
|
|
424
|
-
const title = args[
|
|
425
|
-
const content = args[
|
|
426
|
-
const tags = args[
|
|
427
|
-
|
|
428
|
-
|
|
578
|
+
const title = args["title"] ?? "New Document";
|
|
579
|
+
const content = args["content"] ?? "";
|
|
580
|
+
const tags = args["tags"] ?? [];
|
|
581
|
+
// v0.7.8: models routinely pass the FILENAME as title ("risks.md",
|
|
582
|
+
// "synthesis.md"), which slugified to "risks-md"/"synthesis-md" and
|
|
583
|
+
// produced duplicated artifacts (docs/risks-md.md next to risks.md,
|
|
584
|
+
// HANDOFF 2026-07-03 §5.1). Strip the extension before slugifying.
|
|
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);
|
|
429
599
|
const meta = {
|
|
430
|
-
kind:
|
|
431
|
-
id,
|
|
600
|
+
kind: "doc",
|
|
601
|
+
id: slug,
|
|
432
602
|
date: new Date().toISOString().slice(0, 10),
|
|
433
603
|
tags,
|
|
434
604
|
};
|
|
435
605
|
ctx.storage.write(path, meta, content);
|
|
436
|
-
return `Document "${title}" created at docs/${
|
|
606
|
+
return `Document "${title}" created at docs/${slug}.md.`;
|
|
437
607
|
});
|
|
438
608
|
},
|
|
439
609
|
};
|
|
@@ -449,20 +619,20 @@ const SEARCH_NUDGE_THRESHOLD = 5;
|
|
|
449
619
|
function searchDocumentsStub(ctx) {
|
|
450
620
|
let callCount = 0;
|
|
451
621
|
return {
|
|
452
|
-
name:
|
|
453
|
-
description:
|
|
454
|
-
|
|
455
|
-
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",
|
|
456
626
|
parameters: [],
|
|
457
627
|
execute: async (args) => {
|
|
458
628
|
callCount++;
|
|
459
629
|
const nudge = callCount > SEARCH_NUDGE_THRESHOLD
|
|
460
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`
|
|
461
|
-
:
|
|
462
|
-
const rawQuery = (args[
|
|
463
|
-
const limit = args[
|
|
631
|
+
: "";
|
|
632
|
+
const rawQuery = (args["query"] ?? "").trim();
|
|
633
|
+
const limit = args["limit"] ?? 5;
|
|
464
634
|
if (!rawQuery)
|
|
465
|
-
return
|
|
635
|
+
return "searchDocuments requires a query.";
|
|
466
636
|
// v0.7.3: models routinely write boolean-ish queries ("plan OR phase OR
|
|
467
637
|
// milestone"). The previous implementation substring-matched the WHOLE
|
|
468
638
|
// query string, so every OR query — and any multi-word query that didn't
|
|
@@ -477,21 +647,21 @@ function searchDocumentsStub(ctx) {
|
|
|
477
647
|
const words = rawQuery
|
|
478
648
|
.toLowerCase()
|
|
479
649
|
.split(/[^a-z0-9_-]+/)
|
|
480
|
-
.filter((w) => w.length >= 3 && w !==
|
|
650
|
+
.filter((w) => w.length >= 3 && w !== "or" && w !== "and" && w !== "the");
|
|
481
651
|
const files = [
|
|
482
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
483
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
484
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
485
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
486
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
487
|
-
workspaceFile(ctx.rootDir,
|
|
488
|
-
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"),
|
|
489
659
|
];
|
|
490
660
|
const results = [];
|
|
491
661
|
for (const file of files) {
|
|
492
662
|
if (!existsSync(file))
|
|
493
663
|
continue;
|
|
494
|
-
const raw = readFileSync(file,
|
|
664
|
+
const raw = readFileSync(file, "utf8");
|
|
495
665
|
const content = raw.toLowerCase();
|
|
496
666
|
let idx = -1;
|
|
497
667
|
let matchLen = 0;
|
|
@@ -517,34 +687,42 @@ function searchDocumentsStub(ctx) {
|
|
|
517
687
|
continue;
|
|
518
688
|
const start = Math.max(0, idx - 40);
|
|
519
689
|
const end = Math.min(raw.length, idx + matchLen + 40);
|
|
520
|
-
const snippet =
|
|
521
|
-
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
|
+
});
|
|
522
695
|
if (results.length >= limit)
|
|
523
696
|
break;
|
|
524
697
|
}
|
|
525
698
|
if (results.length === 0)
|
|
526
699
|
return `${nudge}No matches for "${rawQuery}" in workspace.`;
|
|
527
|
-
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"));
|
|
528
701
|
},
|
|
529
702
|
};
|
|
530
703
|
}
|
|
531
704
|
function linkDocumentsStub(ctx) {
|
|
532
705
|
return {
|
|
533
|
-
name:
|
|
534
|
-
description:
|
|
535
|
-
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",
|
|
536
709
|
parameters: [],
|
|
537
710
|
execute: async (args) => {
|
|
538
711
|
return workspaceMutex.run(`${ctx.rootDir}:link`, () => {
|
|
539
|
-
|
|
540
|
-
|
|
712
|
+
// v0.7.8: the provider-facing JSON schema (toolSchemas.ts) advertises
|
|
713
|
+
// sourceId/targetPathOrTitle — accept those aliases too, otherwise
|
|
714
|
+
// every schema-conformant call fails with "requires fromId and toId".
|
|
715
|
+
const fromId = args["fromId"] ?? args["sourceId"];
|
|
716
|
+
const toId = args["toId"] ??
|
|
717
|
+
args["targetId"] ??
|
|
718
|
+
args["targetPathOrTitle"];
|
|
541
719
|
if (!fromId || !toId)
|
|
542
|
-
return
|
|
720
|
+
return "linkDocuments requires fromId and toId.";
|
|
543
721
|
// Find the source file by id
|
|
544
722
|
const allFiles = [
|
|
545
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
546
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
547
|
-
...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")),
|
|
548
726
|
];
|
|
549
727
|
const source = allFiles.find((f) => {
|
|
550
728
|
try {
|
|
@@ -570,18 +748,19 @@ function linkDocumentsStub(ctx) {
|
|
|
570
748
|
}
|
|
571
749
|
function getDocumentBacklinksStub(ctx) {
|
|
572
750
|
return {
|
|
573
|
-
name:
|
|
574
|
-
description:
|
|
575
|
-
category:
|
|
751
|
+
name: "getDocumentBacklinks",
|
|
752
|
+
description: "List all workspace artifacts that link to the given artifact via their `related:` frontmatter.",
|
|
753
|
+
category: "analysis",
|
|
576
754
|
parameters: [],
|
|
577
755
|
execute: async (args) => {
|
|
578
|
-
|
|
756
|
+
// v0.7.8: the provider-facing JSON schema advertises `id` — accept it.
|
|
757
|
+
const targetId = args["targetId"] ?? args["id"];
|
|
579
758
|
if (!targetId)
|
|
580
|
-
return
|
|
759
|
+
return "getDocumentBacklinks requires targetId.";
|
|
581
760
|
const allFiles = [
|
|
582
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
583
|
-
...ctx.storage.listMarkdown(join(ctx.rootDir,
|
|
584
|
-
...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")),
|
|
585
764
|
];
|
|
586
765
|
const backlinks = [];
|
|
587
766
|
for (const file of allFiles) {
|
|
@@ -593,11 +772,13 @@ function getDocumentBacklinksStub(ctx) {
|
|
|
593
772
|
backlinks.push(doc.meta.id);
|
|
594
773
|
}
|
|
595
774
|
}
|
|
596
|
-
catch {
|
|
775
|
+
catch {
|
|
776
|
+
/* skip */
|
|
777
|
+
}
|
|
597
778
|
}
|
|
598
779
|
if (backlinks.length === 0)
|
|
599
780
|
return `No backlinks to "${targetId}".`;
|
|
600
|
-
return backlinks.map((id) => `- ${id}`).join(
|
|
781
|
+
return backlinks.map((id) => `- ${id}`).join("\n");
|
|
601
782
|
},
|
|
602
783
|
};
|
|
603
784
|
}
|