@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/README.md +5 -0
- package/main.js +1886 -477
- package/main.js.map +4 -4
- package/package.json +1 -1
- package/resources/pi-agent/extensions/plan.ts +95 -0
- package/resources/pi-agent/extensions/tasks.ts +497 -0
- package/resources/pi-agent/extensions/todos.ts +102 -2
- package/server-loader.js +24187 -3349
- package/server-loader.js.map +4 -4
package/package.json
CHANGED
|
@@ -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
|
+
}
|