@docyrus/docyrus 0.0.40 → 0.0.41

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docyrus/docyrus",
3
- "version": "0.0.40",
3
+ "version": "0.0.41",
4
4
  "private": false,
5
5
  "description": "Docyrus API CLI",
6
6
  "main": "./main.js",
@@ -28,6 +28,7 @@ const PLAN_ANCHOR_TYPE = "plan-anchor";
28
28
  const PLAN_CONFIG_FILE = ".pi/plan-policy.json";
29
29
  const PLAN_WIDGET_KEY = "plan-mode";
30
30
  const PLAN_BRANCH_LABEL = "plan";
31
+ const ARCHITECT_PLAN_ARTIFACT_FILE_NAME = "PLAN.md";
31
32
  const ALLOWED_TODO_ACTIONS = new Set(["list", "list-all", "get"]);
32
33
  const ALL_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh"] satisfies ThinkingLevel[]);
33
34
 
@@ -96,6 +97,12 @@ export interface IReadPlanPolicyResult {
96
97
  error?: string;
97
98
  }
98
99
 
100
+ interface IPlanningCliEnvironment {
101
+ executable: string;
102
+ entryPath: string;
103
+ scope: "local" | "global";
104
+ }
105
+
99
106
  export type IParseResult<T> = { ok: true; value: T } | { ok: false; error: string };
100
107
 
101
108
  let currentPlanState: IPlanSessionState | undefined;
@@ -120,6 +127,41 @@ function expandUserPath(inputPath: string): string {
120
127
  return inputPath;
121
128
  }
122
129
 
130
+ function readPlanningCliEnvironment(env: NodeJS.ProcessEnv = process.env): IPlanningCliEnvironment {
131
+ const executable = env.DOCYRUS_CLI_EXECUTABLE?.trim();
132
+ const entryPath = env.DOCYRUS_CLI_ENTRY?.trim();
133
+ const scope = env.DOCYRUS_CLI_SCOPE?.trim() as "local" | "global" | undefined;
134
+ if (!executable || !entryPath || (scope !== "local" && scope !== "global")) {
135
+ throw new Error("Missing Docyrus CLI runtime env. Expected DOCYRUS_CLI_EXECUTABLE, DOCYRUS_CLI_ENTRY, and DOCYRUS_CLI_SCOPE.");
136
+ }
137
+
138
+ return {
139
+ executable,
140
+ entryPath,
141
+ scope,
142
+ };
143
+ }
144
+
145
+ async function runProjectPlanCliJson<TValue>(
146
+ pi: ExtensionAPI,
147
+ ctx: ExtensionContext,
148
+ args: string[],
149
+ ): Promise<TValue> {
150
+ const environment = readPlanningCliEnvironment();
151
+ const scopedArgs = environment.scope === "global" ? ["-g", ...args] : args;
152
+ const result = await pi.exec(environment.executable, [environment.entryPath, ...scopedArgs, "--json"], {
153
+ cwd: ctx.cwd,
154
+ });
155
+ const stdout = result.stdout?.toString().trim() || "";
156
+ const stderr = result.stderr?.toString().trim() || "";
157
+ const output = stdout || stderr;
158
+ if (result.code !== 0 || !output) {
159
+ throw new Error(output || `Command exited with code ${result.code ?? "unknown"}.`);
160
+ }
161
+
162
+ return JSON.parse(output) as TValue;
163
+ }
164
+
123
165
  function resolveAgentRootPath(): string {
124
166
  const agentDir = process.env.PI_CODING_AGENT_DIR?.trim();
125
167
  return agentDir && agentDir.length > 0 ? expandUserPath(agentDir) : path.join(os.homedir(), ".pi", "agent");
@@ -635,6 +677,28 @@ async function writePlanArtifactFromEvent(event: AgentEndEvent, ctx: ExtensionCo
635
677
  }
636
678
  }
637
679
 
680
+ async function writeArchitectArtifactFromEvent(event: AgentEndEvent, ctx: ExtensionContext): Promise<void> {
681
+ const state = getPlanState(ctx);
682
+ if (!state?.active || !state.artifactPath || state.mode !== "architect") {
683
+ return;
684
+ }
685
+
686
+ const text = extractLastAssistantText(event.messages ?? []);
687
+ if (!text || parseAskUserRequestFromText(text)) {
688
+ return;
689
+ }
690
+
691
+ try {
692
+ await fs.mkdir(state.artifactPath, { recursive: true });
693
+ await fs.writeFile(path.join(state.artifactPath, ARCHITECT_PLAN_ARTIFACT_FILE_NAME), `${text.trim()}\n`, "utf8");
694
+ } catch (error) {
695
+ if (ctx.hasUI) {
696
+ const message = error instanceof Error ? error.message : String(error);
697
+ ctx.ui.notify(`Failed to write architect plan artifact: ${message}`, "error");
698
+ }
699
+ }
700
+ }
701
+
638
702
  export function shouldBlockTodoAction(action: string | undefined): boolean {
639
703
  return !!action && !ALLOWED_TODO_ACTIONS.has(action);
640
704
  }
@@ -973,6 +1037,36 @@ export async function endPlanningWorkflow(pi: ExtensionAPI, ctx: ExtensionComman
973
1037
  setPlanWidget(ctx, undefined);
974
1038
  await restoreSourceModel(pi, ctx, state);
975
1039
 
1040
+ if (state.artifactPath) {
1041
+ try {
1042
+ const syncResult = state.mode === "architect"
1043
+ ? await runProjectPlanCliJson<{ updatedTaskIds?: string[] }>(pi, ctx, [
1044
+ "project-plan",
1045
+ "upsert-from-architect",
1046
+ "--artifactDir",
1047
+ state.artifactPath,
1048
+ ...(state.task ? ["--brief", state.task] : []),
1049
+ ])
1050
+ : await runProjectPlanCliJson<{ updatedTaskIds?: string[] }>(pi, ctx, [
1051
+ "project-plan",
1052
+ "upsert-from-plan",
1053
+ "--artifactPath",
1054
+ state.artifactPath,
1055
+ ...(state.task ? ["--task", state.task] : []),
1056
+ ]);
1057
+
1058
+ if (ctx.hasUI) {
1059
+ const updatedCount = Array.isArray(syncResult.updatedTaskIds) ? syncResult.updatedTaskIds.length : 0;
1060
+ ctx.ui.notify(`Project plan synced from ${state.mode} artifact (${updatedCount} task update${updatedCount === 1 ? "" : "s"}).`, "info");
1061
+ }
1062
+ } catch (error) {
1063
+ if (ctx.hasUI) {
1064
+ const message = error instanceof Error ? error.message : String(error);
1065
+ ctx.ui.notify(`Project plan sync failed after ${state.mode}: ${message}`, "warning");
1066
+ }
1067
+ }
1068
+ }
1069
+
976
1070
  if (ctx.hasUI) {
977
1071
  const artifactSuffix = state.artifactPath ? ` Artifact: ${state.artifactPath}` : "";
978
1072
  ctx.ui.notify(`Planning session ended.${artifactSuffix}`, "info");
@@ -1176,6 +1270,7 @@ export default function planExtension(pi: ExtensionAPI) {
1176
1270
  }
1177
1271
 
1178
1272
  await writePlanArtifactFromEvent(event, ctx);
1273
+ await writeArchitectArtifactFromEvent(event, ctx);
1179
1274
  });
1180
1275
 
1181
1276
  pi.on("session_start", async(_event, ctx) => {
@@ -0,0 +1,497 @@
1
+ import { execFile } from "node:child_process";
2
+ import { StringEnum } from "@mariozechner/pi-ai";
3
+ import { Type } from "@sinclair/typebox";
4
+ import type {
5
+ ExtensionAPI,
6
+ ExtensionCommandContext,
7
+ } from "@mariozechner/pi-coding-agent";
8
+ import type { Theme } from "@mariozechner/pi-tui";
9
+ import { Text } from "@mariozechner/pi-tui";
10
+
11
+ type ITasksAction =
12
+ | "show"
13
+ | "get"
14
+ | "create-feature"
15
+ | "create-task"
16
+ | "set-status"
17
+ | "create-linked-todo";
18
+
19
+ interface ICliEnvironment {
20
+ executable: string;
21
+ entryPath: string;
22
+ scope: "local" | "global";
23
+ }
24
+
25
+ interface ICommandResult {
26
+ code: number | null;
27
+ stdout: string;
28
+ stderr: string;
29
+ }
30
+
31
+ interface IProjectTaskSummary {
32
+ id: string;
33
+ title: string;
34
+ summary: string;
35
+ type: string;
36
+ assignee: string;
37
+ status: string;
38
+ acceptanceCriteria: string[];
39
+ featureId: string;
40
+ sectionId: string;
41
+ linkedTodoCount: number;
42
+ }
43
+
44
+ interface IProjectFeatureSummary {
45
+ id: string;
46
+ title: string;
47
+ slug: string;
48
+ summary: string;
49
+ sectionId: string;
50
+ status: string;
51
+ taskCount: number;
52
+ tasks: IProjectTaskSummary[];
53
+ }
54
+
55
+ interface IProjectSectionSummary {
56
+ sectionId: string;
57
+ heading: string;
58
+ filePath: string;
59
+ status: string;
60
+ featureCount: number;
61
+ taskCount: number;
62
+ features: IProjectFeatureSummary[];
63
+ }
64
+
65
+ interface IProjectPlanShowPayload {
66
+ hierarchy: {
67
+ sections: IProjectSectionSummary[];
68
+ };
69
+ }
70
+
71
+ const TaskParams = Type.Object({
72
+ action: StringEnum([
73
+ "show",
74
+ "get",
75
+ "create-feature",
76
+ "create-task",
77
+ "set-status",
78
+ "create-linked-todo",
79
+ ] as const),
80
+ taskId: Type.Optional(Type.String({ description: "Canonical task id" })),
81
+ featureId: Type.Optional(Type.String({ description: "Canonical feature id" })),
82
+ sectionId: Type.Optional(Type.String({ description: "Knowledge section id" })),
83
+ title: Type.Optional(Type.String({ description: "Feature or task title" })),
84
+ summary: Type.Optional(Type.String({ description: "Feature or task summary" })),
85
+ slug: Type.Optional(Type.String({ description: "Feature slug" })),
86
+ type: Type.Optional(Type.String({ description: "Task type" })),
87
+ assignee: Type.Optional(Type.String({ description: "Task assignee" })),
88
+ status: Type.Optional(Type.String({ description: "Task status" })),
89
+ acceptanceCriteria: Type.Optional(Type.Array(Type.String({ description: "Acceptance criterion" }))),
90
+ });
91
+
92
+ function readCliEnvironment(env: NodeJS.ProcessEnv = process.env): ICliEnvironment {
93
+ const executable = env.DOCYRUS_CLI_EXECUTABLE?.trim();
94
+ const entryPath = env.DOCYRUS_CLI_ENTRY?.trim();
95
+ const scope = env.DOCYRUS_CLI_SCOPE?.trim() as "local" | "global" | undefined;
96
+ if (!executable || !entryPath || (scope !== "local" && scope !== "global")) {
97
+ throw new Error("Missing Docyrus CLI runtime env. Expected DOCYRUS_CLI_EXECUTABLE, DOCYRUS_CLI_ENTRY, and DOCYRUS_CLI_SCOPE.");
98
+ }
99
+
100
+ return {
101
+ executable,
102
+ entryPath,
103
+ scope,
104
+ };
105
+ }
106
+
107
+ function buildCommandArgs(environment: ICliEnvironment, args: string[], useJson = false): string[] {
108
+ const scopedArgs = environment.scope === "global" ? ["-g", ...args] : args;
109
+ return [environment.entryPath, ...scopedArgs, ...(useJson ? ["--json"] : [])];
110
+ }
111
+
112
+ function summarizeFailure(result: ICommandResult): string {
113
+ const stderr = result.stderr.trim();
114
+ if (stderr) {
115
+ return stderr;
116
+ }
117
+
118
+ const stdout = result.stdout.trim();
119
+ if (stdout) {
120
+ return stdout.split(/\r?\n/gu).at(-1) || stdout;
121
+ }
122
+
123
+ if (typeof result.code === "number") {
124
+ return `Command exited with code ${result.code}.`;
125
+ }
126
+
127
+ return "Command failed.";
128
+ }
129
+
130
+ function runCli(environment: ICliEnvironment, args: string[], cwd: string, useJson = false): Promise<ICommandResult> {
131
+ return new Promise((resolve, reject) => {
132
+ execFile(environment.executable, buildCommandArgs(environment, args, useJson), {
133
+ cwd,
134
+ encoding: "utf8",
135
+ env: {
136
+ ...process.env,
137
+ },
138
+ maxBuffer: 20 * 1024 * 1024,
139
+ }, (error, stdout, stderr) => {
140
+ if (error && typeof (error as NodeJS.ErrnoException).code === "string" && (error as NodeJS.ErrnoException).code === "ENOENT") {
141
+ reject(error);
142
+ return;
143
+ }
144
+
145
+ resolve({
146
+ code: typeof (error as { code?: number } | null)?.code === "number" ? (error as { code?: number }).code || null : null,
147
+ stdout,
148
+ stderr,
149
+ });
150
+ });
151
+ });
152
+ }
153
+
154
+ async function runCliJson<TValue>(environment: ICliEnvironment, args: string[], cwd: string): Promise<TValue> {
155
+ const result = await runCli(environment, args, cwd, true);
156
+ const output = result.stdout.trim() || result.stderr.trim();
157
+ if (!output) {
158
+ throw new Error("Docyrus CLI produced no JSON output.");
159
+ }
160
+ if (result.code !== 0) {
161
+ throw new Error(summarizeFailure(result));
162
+ }
163
+ return JSON.parse(output) as TValue;
164
+ }
165
+
166
+ function formatTaskSummary(task: IProjectTaskSummary): string {
167
+ return `${task.id} · ${task.title} · ${task.status} · ${task.assignee} · ${task.type}${task.linkedTodoCount > 0 ? ` · ${task.linkedTodoCount} linked todo(s)` : ""}`;
168
+ }
169
+
170
+ function formatHierarchySummary(payload: IProjectPlanShowPayload): string {
171
+ const populatedSections = payload.hierarchy.sections.filter((section) => section.features.length > 0);
172
+ if (populatedSections.length === 0) {
173
+ return "No project-plan features or tasks yet.";
174
+ }
175
+
176
+ const lines: string[] = [];
177
+ for (const section of populatedSections) {
178
+ lines.push(`${section.heading} (${section.status})`);
179
+ for (const feature of section.features) {
180
+ lines.push(` - ${feature.title} (${feature.status})`);
181
+ for (const task of feature.tasks) {
182
+ lines.push(` * ${formatTaskSummary(task)}`);
183
+ }
184
+ }
185
+ }
186
+
187
+ return lines.join("\n");
188
+ }
189
+
190
+ async function selectFromOptions(
191
+ ctx: ExtensionCommandContext,
192
+ title: string,
193
+ options: Array<{ label: string; value: string }>,
194
+ ): Promise<string | undefined> {
195
+ const selected = await ctx.ui.select(title, options.map((option) => option.label));
196
+ if (selected === undefined) {
197
+ return undefined;
198
+ }
199
+
200
+ return options.find((option) => option.label === selected)?.value;
201
+ }
202
+
203
+ async function tasksCommandHandler(pi: ExtensionAPI, ctx: ExtensionCommandContext, rawArgs: string): Promise<void> {
204
+ const environment = readCliEnvironment();
205
+ const payload = await runCliJson<IProjectPlanShowPayload>(environment, ["project-plan", "show"], ctx.cwd);
206
+ const summary = formatHierarchySummary(payload);
207
+
208
+ if (!ctx.hasUI) {
209
+ process.stdout.write(summary.endsWith("\n") ? summary : `${summary}\n`);
210
+ return;
211
+ }
212
+
213
+ const query = rawArgs.trim().toLowerCase();
214
+ const sections = payload.hierarchy.sections.filter((section) => section.features.length > 0)
215
+ .filter((section) => !query || section.heading.toLowerCase().includes(query) || section.sectionId.toLowerCase().includes(query));
216
+ if (sections.length === 0) {
217
+ ctx.ui.notify("No project-plan sections with tasks were found.", "info");
218
+ return;
219
+ }
220
+
221
+ const selectedSectionId = await selectFromOptions(ctx, "Project Sections", sections.map((section) => ({
222
+ label: `${section.heading} (${section.status})`,
223
+ value: section.sectionId,
224
+ })));
225
+ if (!selectedSectionId) {
226
+ return;
227
+ }
228
+
229
+ const section = sections.find((item) => item.sectionId === selectedSectionId);
230
+ if (!section) {
231
+ return;
232
+ }
233
+
234
+ const selectedFeatureId = await selectFromOptions(ctx, "Features", section.features.map((feature) => ({
235
+ label: `${feature.title} (${feature.status})`,
236
+ value: feature.id,
237
+ })));
238
+ if (!selectedFeatureId) {
239
+ return;
240
+ }
241
+
242
+ const feature = section.features.find((item) => item.id === selectedFeatureId);
243
+ if (!feature) {
244
+ return;
245
+ }
246
+
247
+ const selectedTaskId = await selectFromOptions(ctx, "Tasks", feature.tasks.map((task) => ({
248
+ label: formatTaskSummary(task),
249
+ value: task.id,
250
+ })));
251
+ if (!selectedTaskId) {
252
+ return;
253
+ }
254
+
255
+ const task = feature.tasks.find((item) => item.id === selectedTaskId);
256
+ if (!task) {
257
+ return;
258
+ }
259
+
260
+ const actionOptions = [
261
+ { label: "View", value: "view" },
262
+ { label: "Mark in progress", value: "in_progress" },
263
+ { label: "Mark blocked", value: "blocked" },
264
+ { label: "Mark done", value: "done" },
265
+ ...(task.assignee === "agent" ? [{ label: "Create linked todo", value: "linked-todo" }] : []),
266
+ ...(task.assignee === "agent" ? [{ label: "Work on task", value: "work" }] : []),
267
+ ];
268
+ const selectedAction = await selectFromOptions(ctx, "Task Action", actionOptions);
269
+ if (!selectedAction) {
270
+ return;
271
+ }
272
+
273
+ if (selectedAction === "view") {
274
+ ctx.ui.notify([
275
+ `${task.title}`,
276
+ `status: ${task.status}`,
277
+ `assignee: ${task.assignee}`,
278
+ `type: ${task.type}`,
279
+ ...(task.summary ? [`summary: ${task.summary}`] : []),
280
+ `linkedTodos: ${task.linkedTodoCount}`,
281
+ ].join("\n"), "info");
282
+ return;
283
+ }
284
+
285
+ if (selectedAction === "linked-todo") {
286
+ const linkedTodo = await runCliJson<{ id: string }>(environment, [
287
+ "project-plan",
288
+ "create-linked-todo",
289
+ "--taskId",
290
+ task.id,
291
+ ], ctx.cwd);
292
+ ctx.ui.notify(`Created linked todo ${linkedTodo.id} for ${task.id}`, "info");
293
+ return;
294
+ }
295
+
296
+ if (selectedAction === "work") {
297
+ if (task.linkedTodoCount === 0) {
298
+ try {
299
+ await runCliJson(environment, [
300
+ "project-plan",
301
+ "create-linked-todo",
302
+ "--taskId",
303
+ task.id,
304
+ ], ctx.cwd);
305
+ }
306
+ catch (error) {
307
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
308
+ }
309
+ }
310
+ ctx.ui.setEditorText(`work on task ${task.id} "${task.title}"`);
311
+ return;
312
+ }
313
+
314
+ await runCliJson(environment, [
315
+ "project-plan",
316
+ "set-task-status",
317
+ "--taskId",
318
+ task.id,
319
+ "--status",
320
+ selectedAction,
321
+ ], ctx.cwd);
322
+ ctx.ui.notify(`Updated ${task.id} to ${selectedAction}`, "info");
323
+ }
324
+
325
+ export default function tasksExtension(pi: ExtensionAPI) {
326
+ pi.registerTool({
327
+ name: "project_task",
328
+ label: "Project Task",
329
+ description: "Manage the canonical repo-tracked project-plan graph and linked local subtasks.",
330
+ parameters: TaskParams,
331
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
332
+ const environment = readCliEnvironment();
333
+ const action = params.action as ITasksAction;
334
+
335
+ switch (action) {
336
+ case "show": {
337
+ const payload = await runCliJson<IProjectPlanShowPayload>(environment, ["project-plan", "show"], ctx.cwd);
338
+ const text = formatHierarchySummary(payload);
339
+ return {
340
+ content: [{ type: "text", text }],
341
+ details: payload,
342
+ };
343
+ }
344
+
345
+ case "get": {
346
+ if (!params.taskId) {
347
+ return {
348
+ content: [{ type: "text", text: "taskId required" }],
349
+ isError: true,
350
+ };
351
+ }
352
+ const payload = await runCliJson<Record<string, unknown>>(environment, [
353
+ "project-plan",
354
+ "get-task",
355
+ "--taskId",
356
+ String(params.taskId),
357
+ ], ctx.cwd);
358
+ return {
359
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
360
+ details: payload,
361
+ };
362
+ }
363
+
364
+ case "create-feature": {
365
+ if (!params.sectionId || !params.title) {
366
+ return {
367
+ content: [{ type: "text", text: "sectionId and title required" }],
368
+ isError: true,
369
+ };
370
+ }
371
+ const args = [
372
+ "project-plan",
373
+ "upsert-feature",
374
+ "--sectionId",
375
+ String(params.sectionId),
376
+ "--title",
377
+ String(params.title),
378
+ ];
379
+ if (params.slug) {
380
+ args.push("--slug", String(params.slug));
381
+ }
382
+ if (params.summary) {
383
+ args.push("--summary", String(params.summary));
384
+ }
385
+ const payload = await runCliJson<Record<string, unknown>>(environment, args, ctx.cwd);
386
+ return {
387
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
388
+ details: payload,
389
+ };
390
+ }
391
+
392
+ case "create-task": {
393
+ if (!params.featureId || !params.title || !params.type || !params.assignee) {
394
+ return {
395
+ content: [{ type: "text", text: "featureId, title, type, and assignee required" }],
396
+ isError: true,
397
+ };
398
+ }
399
+ const args = [
400
+ "project-plan",
401
+ "upsert-task",
402
+ "--featureId",
403
+ String(params.featureId),
404
+ "--title",
405
+ String(params.title),
406
+ "--type",
407
+ String(params.type),
408
+ "--assignee",
409
+ String(params.assignee),
410
+ ];
411
+ if (params.sectionId) {
412
+ args.push("--sectionId", String(params.sectionId));
413
+ }
414
+ if (params.summary) {
415
+ args.push("--summary", String(params.summary));
416
+ }
417
+ if (params.status) {
418
+ args.push("--status", String(params.status));
419
+ }
420
+ if (Array.isArray(params.acceptanceCriteria) && params.acceptanceCriteria.length > 0) {
421
+ args.push("--acceptanceCriteria", JSON.stringify(params.acceptanceCriteria));
422
+ }
423
+ const payload = await runCliJson<Record<string, unknown>>(environment, args, ctx.cwd);
424
+ return {
425
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
426
+ details: payload,
427
+ };
428
+ }
429
+
430
+ case "set-status": {
431
+ if (!params.taskId || !params.status) {
432
+ return {
433
+ content: [{ type: "text", text: "taskId and status required" }],
434
+ isError: true,
435
+ };
436
+ }
437
+ const payload = await runCliJson<Record<string, unknown>>(environment, [
438
+ "project-plan",
439
+ "set-task-status",
440
+ "--taskId",
441
+ String(params.taskId),
442
+ "--status",
443
+ String(params.status),
444
+ ], ctx.cwd);
445
+ return {
446
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
447
+ details: payload,
448
+ };
449
+ }
450
+
451
+ case "create-linked-todo": {
452
+ if (!params.taskId) {
453
+ return {
454
+ content: [{ type: "text", text: "taskId required" }],
455
+ isError: true,
456
+ };
457
+ }
458
+ const args = [
459
+ "project-plan",
460
+ "create-linked-todo",
461
+ "--taskId",
462
+ String(params.taskId),
463
+ ];
464
+ if (params.title) {
465
+ args.push("--title", String(params.title));
466
+ }
467
+ if (params.summary) {
468
+ args.push("--body", String(params.summary));
469
+ }
470
+ const payload = await runCliJson<Record<string, unknown>>(environment, args, ctx.cwd);
471
+ return {
472
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
473
+ details: payload,
474
+ };
475
+ }
476
+ }
477
+ },
478
+ renderCall(args, theme: Theme) {
479
+ return new Text(
480
+ `${theme.fg("toolTitle", theme.bold("project task "))}${theme.fg("muted", String(args.action || "show"))}`,
481
+ 0,
482
+ 0,
483
+ );
484
+ },
485
+ renderResult(result, _options, _theme) {
486
+ const text = result.content[0];
487
+ return new Text(text?.type === "text" ? text.text : "", 0, 0);
488
+ },
489
+ });
490
+
491
+ pi.registerCommand("tasks", {
492
+ description: "Browse project-plan sections, features, and tasks",
493
+ handler: async(args, ctx) => {
494
+ await tasksCommandHandler(pi, ctx, args);
495
+ },
496
+ });
497
+ }