@tianhai/pi-workflow-kit 0.5.1 → 0.6.0
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 +44 -494
- package/docs/developer-usage-guide.md +41 -401
- package/docs/oversight-model.md +13 -34
- package/docs/workflow-phases.md +32 -46
- package/extensions/workflow-guard.ts +67 -0
- package/package.json +3 -7
- package/skills/brainstorming/SKILL.md +16 -59
- package/skills/executing-tasks/SKILL.md +26 -227
- package/skills/finalizing/SKILL.md +33 -0
- package/skills/writing-plans/SKILL.md +23 -132
- package/ROADMAP.md +0 -16
- package/agents/code-reviewer.md +0 -18
- package/agents/config.ts +0 -5
- package/agents/implementer.md +0 -26
- package/agents/spec-reviewer.md +0 -13
- package/agents/worker.md +0 -17
- package/docs/plans/completed/2026-04-09-cleanup-legacy-state-and-enforce-think-phases-design.md +0 -56
- package/docs/plans/completed/2026-04-09-cleanup-legacy-state-and-enforce-think-phases-implementation.md +0 -196
- package/docs/plans/completed/2026-04-09-workflow-next-autocomplete-design.md +0 -185
- package/docs/plans/completed/2026-04-09-workflow-next-autocomplete-implementation.md +0 -334
- package/docs/plans/completed/2026-04-09-workflow-next-handoff-state-design.md +0 -251
- package/docs/plans/completed/2026-04-09-workflow-next-handoff-state-implementation.md +0 -253
- package/extensions/constants.ts +0 -15
- package/extensions/lib/logging.ts +0 -138
- package/extensions/plan-tracker.ts +0 -502
- package/extensions/subagent/agents.ts +0 -144
- package/extensions/subagent/concurrency.ts +0 -52
- package/extensions/subagent/env.ts +0 -47
- package/extensions/subagent/index.ts +0 -1181
- package/extensions/subagent/lifecycle.ts +0 -25
- package/extensions/subagent/timeout.ts +0 -13
- package/extensions/workflow-monitor/debug-monitor.ts +0 -98
- package/extensions/workflow-monitor/git.ts +0 -31
- package/extensions/workflow-monitor/heuristics.ts +0 -58
- package/extensions/workflow-monitor/investigation.ts +0 -52
- package/extensions/workflow-monitor/reference-tool.ts +0 -42
- package/extensions/workflow-monitor/skip-confirmation.ts +0 -19
- package/extensions/workflow-monitor/tdd-monitor.ts +0 -137
- package/extensions/workflow-monitor/test-runner.ts +0 -37
- package/extensions/workflow-monitor/verification-monitor.ts +0 -61
- package/extensions/workflow-monitor/warnings.ts +0 -81
- package/extensions/workflow-monitor/workflow-handler.ts +0 -358
- package/extensions/workflow-monitor/workflow-next-completions.ts +0 -68
- package/extensions/workflow-monitor/workflow-next-state.ts +0 -112
- package/extensions/workflow-monitor/workflow-tracker.ts +0 -253
- package/extensions/workflow-monitor/workflow-transitions.ts +0 -55
- package/extensions/workflow-monitor.ts +0 -872
- package/skills/dispatching-parallel-agents/SKILL.md +0 -194
- package/skills/receiving-code-review/SKILL.md +0 -196
- package/skills/systematic-debugging/SKILL.md +0 -170
- package/skills/systematic-debugging/condition-based-waiting-example.ts +0 -158
- package/skills/systematic-debugging/condition-based-waiting.md +0 -115
- package/skills/systematic-debugging/defense-in-depth.md +0 -122
- package/skills/systematic-debugging/find-polluter.sh +0 -63
- package/skills/systematic-debugging/reference/rationalizations.md +0 -61
- package/skills/systematic-debugging/root-cause-tracing.md +0 -169
- package/skills/test-driven-development/SKILL.md +0 -266
- package/skills/test-driven-development/reference/examples.md +0 -101
- package/skills/test-driven-development/reference/rationalizations.md +0 -67
- package/skills/test-driven-development/reference/when-stuck.md +0 -33
- package/skills/test-driven-development/testing-anti-patterns.md +0 -299
- package/skills/using-git-worktrees/SKILL.md +0 -231
|
@@ -1,502 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Task Tracker Extension
|
|
3
|
-
*
|
|
4
|
-
* A native pi tool for tracking plan progress with per-task phase and attempt tracking.
|
|
5
|
-
* State is stored in tool result details for proper branching support.
|
|
6
|
-
* Shows a persistent TUI widget above the editor.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { StringEnum } from "@mariozechner/pi-ai";
|
|
10
|
-
import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
11
|
-
import { Text } from "@mariozechner/pi-tui";
|
|
12
|
-
import { type Static, Type } from "@sinclair/typebox";
|
|
13
|
-
import { PLAN_TRACKER_CLEARED_TYPE, PLAN_TRACKER_TOOL_NAME } from "./constants.js";
|
|
14
|
-
|
|
15
|
-
export type TaskStatus = "pending" | "in_progress" | "complete" | "blocked";
|
|
16
|
-
export type TaskPhase =
|
|
17
|
-
| "pending"
|
|
18
|
-
| "define"
|
|
19
|
-
| "approve"
|
|
20
|
-
| "execute"
|
|
21
|
-
| "verify"
|
|
22
|
-
| "review"
|
|
23
|
-
| "fix"
|
|
24
|
-
| "complete"
|
|
25
|
-
| "blocked";
|
|
26
|
-
export type TaskType = "code" | "non-code";
|
|
27
|
-
|
|
28
|
-
export interface PlanTrackerTask {
|
|
29
|
-
name: string;
|
|
30
|
-
status: TaskStatus;
|
|
31
|
-
phase: TaskPhase;
|
|
32
|
-
type: TaskType;
|
|
33
|
-
executeAttempts: number;
|
|
34
|
-
fixAttempts: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface PlanTrackerTaskInit {
|
|
38
|
-
name: string;
|
|
39
|
-
type?: TaskType;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface PlanTrackerDetails {
|
|
43
|
-
action: "init" | "update" | "status" | "clear";
|
|
44
|
-
tasks: PlanTrackerTask[];
|
|
45
|
-
error?: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const TASK_PHASES: readonly string[] = [
|
|
49
|
-
"pending",
|
|
50
|
-
"define",
|
|
51
|
-
"approve",
|
|
52
|
-
"execute",
|
|
53
|
-
"verify",
|
|
54
|
-
"review",
|
|
55
|
-
"fix",
|
|
56
|
-
"complete",
|
|
57
|
-
"blocked",
|
|
58
|
-
] as const;
|
|
59
|
-
|
|
60
|
-
const TASK_STATUSES: readonly string[] = ["pending", "in_progress", "complete", "blocked"] as const;
|
|
61
|
-
|
|
62
|
-
const PlanTrackerParams = Type.Object({
|
|
63
|
-
action: StringEnum(["init", "update", "status", "clear"] as const, {
|
|
64
|
-
description: "Action to perform",
|
|
65
|
-
}),
|
|
66
|
-
tasks: Type.Optional(
|
|
67
|
-
Type.Array(
|
|
68
|
-
Type.Union([
|
|
69
|
-
Type.String(),
|
|
70
|
-
Type.Object({
|
|
71
|
-
name: Type.String({ description: "Task name" }),
|
|
72
|
-
type: Type.Optional(
|
|
73
|
-
StringEnum(["code", "non-code"] as const, {
|
|
74
|
-
description: "Task type",
|
|
75
|
-
}),
|
|
76
|
-
),
|
|
77
|
-
}),
|
|
78
|
-
]),
|
|
79
|
-
{
|
|
80
|
-
description: "Task names or typed task objects (for init)",
|
|
81
|
-
},
|
|
82
|
-
),
|
|
83
|
-
),
|
|
84
|
-
index: Type.Optional(
|
|
85
|
-
Type.Integer({
|
|
86
|
-
minimum: 0,
|
|
87
|
-
description: "Task index, 0-based (for update)",
|
|
88
|
-
}),
|
|
89
|
-
),
|
|
90
|
-
status: Type.Optional(
|
|
91
|
-
StringEnum(TASK_STATUSES as unknown as readonly [string, ...string[]], {
|
|
92
|
-
description: "New status (for update)",
|
|
93
|
-
}),
|
|
94
|
-
),
|
|
95
|
-
phase: Type.Optional(
|
|
96
|
-
StringEnum(TASK_PHASES as unknown as readonly [string, ...string[]], {
|
|
97
|
-
description: "New phase (for update)",
|
|
98
|
-
}),
|
|
99
|
-
),
|
|
100
|
-
type: Type.Optional(
|
|
101
|
-
StringEnum(["code", "non-code"] as const, {
|
|
102
|
-
description: "Task type (for update)",
|
|
103
|
-
}),
|
|
104
|
-
),
|
|
105
|
-
attempts: Type.Optional(
|
|
106
|
-
Type.Integer({
|
|
107
|
-
minimum: 0,
|
|
108
|
-
description: "Set attempt count for executeAttempts or fixAttempts depending on current phase (for update)",
|
|
109
|
-
}),
|
|
110
|
-
),
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
export type PlanTrackerInput = Static<typeof PlanTrackerParams>;
|
|
114
|
-
|
|
115
|
-
function createDefaultTask(input: string | PlanTrackerTaskInit): PlanTrackerTask {
|
|
116
|
-
const task = typeof input === "string" ? { name: input } : input;
|
|
117
|
-
return {
|
|
118
|
-
name: task.name,
|
|
119
|
-
status: "pending",
|
|
120
|
-
phase: "pending",
|
|
121
|
-
type: task.type ?? "code",
|
|
122
|
-
executeAttempts: 0,
|
|
123
|
-
fixAttempts: 0,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function phaseIcon(status: TaskStatus, phase: TaskPhase): string {
|
|
128
|
-
if (status === "complete" || phase === "complete") return "✓";
|
|
129
|
-
if (status === "blocked" || phase === "blocked") return "⛔";
|
|
130
|
-
if (phase === "define") return "📝";
|
|
131
|
-
if (phase === "approve") return "👀";
|
|
132
|
-
if (phase === "execute") return "⚙";
|
|
133
|
-
if (phase === "verify") return "🔎";
|
|
134
|
-
if (phase === "review") return "🔍";
|
|
135
|
-
if (phase === "fix") return "🔧";
|
|
136
|
-
if (status === "in_progress") return "→";
|
|
137
|
-
return "○";
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function formatWidget(tasks: PlanTrackerTask[], theme: Theme): string {
|
|
141
|
-
if (tasks.length === 0) return "";
|
|
142
|
-
|
|
143
|
-
const complete = tasks.filter((t) => t.status === "complete").length;
|
|
144
|
-
const blocked = tasks.filter((t) => t.status === "blocked").length;
|
|
145
|
-
const icons = tasks
|
|
146
|
-
.map((t) => {
|
|
147
|
-
switch (t.status) {
|
|
148
|
-
case "complete":
|
|
149
|
-
return theme.fg("success", "✓");
|
|
150
|
-
case "blocked":
|
|
151
|
-
return theme.fg("error", "⛔");
|
|
152
|
-
case "in_progress":
|
|
153
|
-
return theme.fg("warning", "→");
|
|
154
|
-
default:
|
|
155
|
-
return theme.fg("dim", "○");
|
|
156
|
-
}
|
|
157
|
-
})
|
|
158
|
-
.join("");
|
|
159
|
-
|
|
160
|
-
const summary = theme.fg("muted", `(${complete}/${tasks.length})`);
|
|
161
|
-
const blockedNote = blocked > 0 ? ` ${theme.fg("error", `${blocked} blocked`)}` : "";
|
|
162
|
-
|
|
163
|
-
// Show current task with phase
|
|
164
|
-
const current = tasks.find((t) => t.status === "in_progress") ?? tasks.find((t) => t.status === "pending");
|
|
165
|
-
const currentType = current?.type === "non-code" ? ` ${theme.fg("warning", "📋")}` : "";
|
|
166
|
-
const currentInfo =
|
|
167
|
-
current && current.status === "in_progress"
|
|
168
|
-
? ` ${theme.fg("muted", current.name)}${currentType} — ${theme.fg("dim", current.phase)}${current.phase === "fix" || current.phase === "execute" ? ` (${current.phase === "fix" ? current.fixAttempts : current.executeAttempts}/3)` : ""}`
|
|
169
|
-
: current
|
|
170
|
-
? ` ${theme.fg("muted", current.name)}${currentType}`
|
|
171
|
-
: "";
|
|
172
|
-
|
|
173
|
-
return `${theme.fg("muted", "Tasks:")} ${icons} ${summary}${blockedNote}${currentInfo}`;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function formatStatus(tasks: PlanTrackerTask[]): string {
|
|
177
|
-
if (tasks.length === 0) return "No plan active.";
|
|
178
|
-
|
|
179
|
-
const complete = tasks.filter((t) => t.status === "complete").length;
|
|
180
|
-
const inProgress = tasks.filter((t) => t.status === "in_progress").length;
|
|
181
|
-
const pending = tasks.filter((t) => t.status === "pending").length;
|
|
182
|
-
const blocked = tasks.filter((t) => t.status === "blocked").length;
|
|
183
|
-
|
|
184
|
-
const lines: string[] = [];
|
|
185
|
-
lines.push(
|
|
186
|
-
`Plan: ${complete}/${tasks.length} complete (${inProgress} in progress, ${pending} pending${blocked > 0 ? `, ${blocked} blocked` : ""})`,
|
|
187
|
-
);
|
|
188
|
-
lines.push("");
|
|
189
|
-
for (let i = 0; i < tasks.length; i++) {
|
|
190
|
-
const t = tasks[i];
|
|
191
|
-
const icon = phaseIcon(t.status, t.phase);
|
|
192
|
-
const phaseStr = t.status === "in_progress" ? ` [${t.phase}]` : "";
|
|
193
|
-
const attemptsStr =
|
|
194
|
-
t.status === "in_progress" && (t.phase === "execute" || t.phase === "fix")
|
|
195
|
-
? ` (${t.phase === "fix" ? t.fixAttempts : t.executeAttempts}/3)`
|
|
196
|
-
: "";
|
|
197
|
-
const typeStr = t.type === "non-code" ? " 📋" : "";
|
|
198
|
-
lines.push(` ${icon} [${i}] ${t.name}${typeStr}${phaseStr}${attemptsStr}`);
|
|
199
|
-
}
|
|
200
|
-
return lines.join("\n");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export default function (pi: ExtensionAPI) {
|
|
204
|
-
let tasks: PlanTrackerTask[] = [];
|
|
205
|
-
|
|
206
|
-
const reconstructState = (ctx: ExtensionContext) => {
|
|
207
|
-
tasks = [];
|
|
208
|
-
const entries = ctx.sessionManager.getBranch();
|
|
209
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
210
|
-
const entry = entries[i];
|
|
211
|
-
// Check for explicit clear signal (written by /workflow-reset)
|
|
212
|
-
// biome-ignore lint/suspicious/noExplicitAny: pi SDK session entry type
|
|
213
|
-
if (entry.type === "custom" && (entry as any).customType === PLAN_TRACKER_CLEARED_TYPE) {
|
|
214
|
-
tasks = [];
|
|
215
|
-
break;
|
|
216
|
-
}
|
|
217
|
-
if (entry.type !== "message") continue;
|
|
218
|
-
const msg = entry.message;
|
|
219
|
-
if (msg.role !== "toolResult" || msg.toolName !== PLAN_TRACKER_TOOL_NAME) continue;
|
|
220
|
-
const details = msg.details as PlanTrackerDetails | undefined;
|
|
221
|
-
if (details && !details.error) {
|
|
222
|
-
tasks = details.tasks;
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
const updateWidget = (ctx: ExtensionContext) => {
|
|
229
|
-
if (!ctx.hasUI) return;
|
|
230
|
-
if (tasks.length === 0) {
|
|
231
|
-
ctx.ui.setWidget(PLAN_TRACKER_TOOL_NAME, undefined);
|
|
232
|
-
} else {
|
|
233
|
-
ctx.ui.setWidget(PLAN_TRACKER_TOOL_NAME, (_tui, theme) => {
|
|
234
|
-
return new Text(formatWidget(tasks, theme), 0, 0);
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
// Reconstruct state + widget on session events
|
|
240
|
-
// session_start covers startup, reload, new, resume, fork (pi v0.65.0+)
|
|
241
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
242
|
-
reconstructState(ctx);
|
|
243
|
-
updateWidget(ctx);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// session_tree for /tree navigation where a different session branch is loaded
|
|
247
|
-
pi.on("session_tree", async (_event, ctx) => {
|
|
248
|
-
reconstructState(ctx);
|
|
249
|
-
updateWidget(ctx);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
pi.registerTool({
|
|
253
|
-
name: PLAN_TRACKER_TOOL_NAME,
|
|
254
|
-
label: "Task Tracker",
|
|
255
|
-
description:
|
|
256
|
-
"Track implementation plan progress with per-task phase and attempt tracking. Actions: init (set task list), update (change task status/phase/type/attempts), status (show current state), clear (remove plan).",
|
|
257
|
-
parameters: PlanTrackerParams,
|
|
258
|
-
|
|
259
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
260
|
-
switch (params.action) {
|
|
261
|
-
case "init": {
|
|
262
|
-
if (!params.tasks || params.tasks.length === 0) {
|
|
263
|
-
return {
|
|
264
|
-
content: [{ type: "text", text: "Error: tasks array required for init" }],
|
|
265
|
-
details: {
|
|
266
|
-
action: "init",
|
|
267
|
-
tasks: [...tasks],
|
|
268
|
-
error: "tasks required",
|
|
269
|
-
} as PlanTrackerDetails,
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
tasks = params.tasks.map((task) => createDefaultTask(task));
|
|
273
|
-
updateWidget(ctx);
|
|
274
|
-
return {
|
|
275
|
-
content: [
|
|
276
|
-
{
|
|
277
|
-
type: "text",
|
|
278
|
-
text: `Plan initialized with ${tasks.length} tasks.\n${formatStatus(tasks)}`,
|
|
279
|
-
},
|
|
280
|
-
],
|
|
281
|
-
details: { action: "init", tasks: [...tasks] } as PlanTrackerDetails,
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
case "update": {
|
|
286
|
-
if (params.index === undefined) {
|
|
287
|
-
return {
|
|
288
|
-
content: [{ type: "text", text: "Error: index required for update" }],
|
|
289
|
-
details: {
|
|
290
|
-
action: "update",
|
|
291
|
-
tasks: [...tasks],
|
|
292
|
-
error: "index required",
|
|
293
|
-
} as PlanTrackerDetails,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
if (tasks.length === 0) {
|
|
297
|
-
return {
|
|
298
|
-
content: [{ type: "text", text: "Error: no plan active. Use init first." }],
|
|
299
|
-
details: {
|
|
300
|
-
action: "update",
|
|
301
|
-
tasks: [],
|
|
302
|
-
error: "no plan active",
|
|
303
|
-
} as PlanTrackerDetails,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
if (params.index < 0 || params.index >= tasks.length) {
|
|
307
|
-
return {
|
|
308
|
-
content: [
|
|
309
|
-
{
|
|
310
|
-
type: "text",
|
|
311
|
-
text: `Error: index ${params.index} out of range (0-${tasks.length - 1})`,
|
|
312
|
-
},
|
|
313
|
-
],
|
|
314
|
-
details: {
|
|
315
|
-
action: "update",
|
|
316
|
-
tasks: [...tasks],
|
|
317
|
-
error: `index ${params.index} out of range`,
|
|
318
|
-
} as PlanTrackerDetails,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const task = tasks[params.index];
|
|
323
|
-
const updates: string[] = [];
|
|
324
|
-
|
|
325
|
-
// Compute target status and phase from explicit params first,
|
|
326
|
-
// then auto-sync only the fields that weren't explicitly set.
|
|
327
|
-
// This prevents one param from silently overriding the other.
|
|
328
|
-
const explicitStatus: TaskStatus | undefined = params.status;
|
|
329
|
-
const explicitPhase: TaskPhase | undefined = params.phase;
|
|
330
|
-
|
|
331
|
-
// Apply explicit status
|
|
332
|
-
if (explicitStatus) {
|
|
333
|
-
task.status = explicitStatus;
|
|
334
|
-
updates.push(`status → ${explicitStatus}`);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Apply explicit phase
|
|
338
|
-
if (explicitPhase) {
|
|
339
|
-
task.phase = explicitPhase;
|
|
340
|
-
updates.push(`phase → ${explicitPhase}`);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Auto-sync: derive phase from status (only if phase wasn't explicitly set)
|
|
344
|
-
if (explicitStatus && !explicitPhase) {
|
|
345
|
-
if (explicitStatus === "complete" || explicitStatus === "blocked") {
|
|
346
|
-
task.phase = explicitStatus;
|
|
347
|
-
} else if (explicitStatus === "in_progress" && task.phase === "pending") {
|
|
348
|
-
task.phase = "define";
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Auto-sync: derive status from phase (only if status wasn't explicitly set)
|
|
353
|
-
if (explicitPhase && !explicitStatus) {
|
|
354
|
-
if (explicitPhase === "complete" || explicitPhase === "blocked") {
|
|
355
|
-
task.status = explicitPhase;
|
|
356
|
-
} else {
|
|
357
|
-
task.status = "in_progress";
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Update type
|
|
362
|
-
if (params.type) {
|
|
363
|
-
task.type = params.type;
|
|
364
|
-
updates.push(`type → ${params.type}`);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Update attempts
|
|
368
|
-
if (params.attempts !== undefined) {
|
|
369
|
-
if (task.phase === "fix") {
|
|
370
|
-
task.fixAttempts = params.attempts;
|
|
371
|
-
updates.push(`fixAttempts → ${params.attempts}`);
|
|
372
|
-
} else if (task.phase === "execute") {
|
|
373
|
-
task.executeAttempts = params.attempts;
|
|
374
|
-
updates.push(`executeAttempts → ${params.attempts}`);
|
|
375
|
-
} else {
|
|
376
|
-
// Default to execute attempts if phase is ambiguous
|
|
377
|
-
task.executeAttempts = params.attempts;
|
|
378
|
-
updates.push(`executeAttempts → ${params.attempts}`);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
updateWidget(ctx);
|
|
383
|
-
|
|
384
|
-
const updateSummary = updates.length > 0 ? ` (${updates.join(", ")})` : "";
|
|
385
|
-
return {
|
|
386
|
-
content: [
|
|
387
|
-
{
|
|
388
|
-
type: "text",
|
|
389
|
-
text: `Task ${params.index} "${task.name}"${updateSummary}\n${formatStatus(tasks)}`,
|
|
390
|
-
},
|
|
391
|
-
],
|
|
392
|
-
details: { action: "update", tasks: [...tasks] } as PlanTrackerDetails,
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
case "status": {
|
|
397
|
-
return {
|
|
398
|
-
content: [{ type: "text", text: formatStatus(tasks) }],
|
|
399
|
-
details: { action: "status", tasks: [...tasks] } as PlanTrackerDetails,
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
case "clear": {
|
|
404
|
-
const count = tasks.length;
|
|
405
|
-
tasks = [];
|
|
406
|
-
updateWidget(ctx);
|
|
407
|
-
return {
|
|
408
|
-
content: [
|
|
409
|
-
{
|
|
410
|
-
type: "text",
|
|
411
|
-
text: count > 0 ? `Plan cleared (${count} tasks removed).` : "No plan was active.",
|
|
412
|
-
},
|
|
413
|
-
],
|
|
414
|
-
details: { action: "clear", tasks: [] } as PlanTrackerDetails,
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
default:
|
|
419
|
-
return {
|
|
420
|
-
content: [{ type: "text", text: `Unknown action: ${params.action}` }],
|
|
421
|
-
details: {
|
|
422
|
-
action: "status",
|
|
423
|
-
tasks: [...tasks],
|
|
424
|
-
error: `unknown action`,
|
|
425
|
-
} as PlanTrackerDetails,
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
},
|
|
429
|
-
|
|
430
|
-
renderCall(args, theme) {
|
|
431
|
-
let text = theme.fg("toolTitle", theme.bold("plan_tracker "));
|
|
432
|
-
text += theme.fg("muted", args.action);
|
|
433
|
-
if (args.action === "update" && args.index !== undefined) {
|
|
434
|
-
text += ` ${theme.fg("accent", `[${args.index}]`)}`;
|
|
435
|
-
const parts: string[] = [];
|
|
436
|
-
if (args.status) parts.push(args.status);
|
|
437
|
-
if (args.phase) parts.push(args.phase);
|
|
438
|
-
if (args.type) parts.push(args.type);
|
|
439
|
-
if (args.attempts !== undefined) parts.push(`attempt ${args.attempts}`);
|
|
440
|
-
if (parts.length > 0) text += ` → ${theme.fg("dim", parts.join(", "))}`;
|
|
441
|
-
}
|
|
442
|
-
if (args.action === "init" && args.tasks) {
|
|
443
|
-
text += ` ${theme.fg("dim", `(${args.tasks.length} tasks)`)}`;
|
|
444
|
-
}
|
|
445
|
-
return new Text(text, 0, 0);
|
|
446
|
-
},
|
|
447
|
-
|
|
448
|
-
renderResult(result, _options, theme) {
|
|
449
|
-
const details = result.details as PlanTrackerDetails | undefined;
|
|
450
|
-
if (!details) {
|
|
451
|
-
const text = result.content[0];
|
|
452
|
-
return new Text(text?.type === "text" ? text.text : "", 0, 0);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (details.error) {
|
|
456
|
-
return new Text(theme.fg("error", `Error: ${details.error}`), 0, 0);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const taskList = details.tasks;
|
|
460
|
-
switch (details.action) {
|
|
461
|
-
case "init":
|
|
462
|
-
return new Text(
|
|
463
|
-
theme.fg("success", "✓ ") + theme.fg("muted", `Plan initialized with ${taskList.length} tasks`),
|
|
464
|
-
0,
|
|
465
|
-
0,
|
|
466
|
-
);
|
|
467
|
-
case "update": {
|
|
468
|
-
const complete = taskList.filter((t) => t.status === "complete").length;
|
|
469
|
-
return new Text(
|
|
470
|
-
theme.fg("success", "✓ ") + theme.fg("muted", `Updated (${complete}/${taskList.length} complete)`),
|
|
471
|
-
0,
|
|
472
|
-
0,
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
case "status": {
|
|
476
|
-
if (taskList.length === 0) {
|
|
477
|
-
return new Text(theme.fg("dim", "No plan active"), 0, 0);
|
|
478
|
-
}
|
|
479
|
-
const complete = taskList.filter((t) => t.status === "complete").length;
|
|
480
|
-
let text = theme.fg("muted", `${complete}/${taskList.length} complete`);
|
|
481
|
-
for (const t of taskList) {
|
|
482
|
-
const icon =
|
|
483
|
-
t.status === "complete"
|
|
484
|
-
? theme.fg("success", "✓")
|
|
485
|
-
: t.status === "blocked"
|
|
486
|
-
? theme.fg("error", "⛔")
|
|
487
|
-
: t.status === "in_progress"
|
|
488
|
-
? theme.fg("warning", "→")
|
|
489
|
-
: theme.fg("dim", "○");
|
|
490
|
-
const phaseStr = t.status === "in_progress" ? ` [${t.phase}]` : "";
|
|
491
|
-
text += `\n${icon} ${theme.fg("muted", t.name)}${phaseStr}`;
|
|
492
|
-
}
|
|
493
|
-
return new Text(text, 0, 0);
|
|
494
|
-
}
|
|
495
|
-
case "clear":
|
|
496
|
-
return new Text(theme.fg("success", "✓ ") + theme.fg("muted", "Plan cleared"), 0, 0);
|
|
497
|
-
default:
|
|
498
|
-
return new Text(theme.fg("dim", "Done"), 0, 0);
|
|
499
|
-
}
|
|
500
|
-
},
|
|
501
|
-
});
|
|
502
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent discovery and configuration
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import * as fs from "node:fs";
|
|
6
|
-
import * as os from "node:os";
|
|
7
|
-
import * as path from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
10
|
-
import { log } from "../lib/logging.js";
|
|
11
|
-
|
|
12
|
-
export type AgentScope = "user" | "project" | "both";
|
|
13
|
-
|
|
14
|
-
export interface AgentConfig {
|
|
15
|
-
name: string;
|
|
16
|
-
description: string;
|
|
17
|
-
tools?: string[];
|
|
18
|
-
extensions?: string[];
|
|
19
|
-
model?: string;
|
|
20
|
-
systemPrompt: string;
|
|
21
|
-
source: "user" | "project" | "bundled";
|
|
22
|
-
filePath: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface AgentDiscoveryResult {
|
|
26
|
-
agents: AgentConfig[];
|
|
27
|
-
projectAgentsDir: string | null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function loadAgentsFromDir(dir: string, source: "user" | "project" | "bundled"): AgentConfig[] {
|
|
31
|
-
const agents: AgentConfig[] = [];
|
|
32
|
-
|
|
33
|
-
if (!fs.existsSync(dir)) {
|
|
34
|
-
return agents;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
let entries: fs.Dirent[];
|
|
38
|
-
try {
|
|
39
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
40
|
-
} catch (err) {
|
|
41
|
-
log.warn(`Failed to read agents directory: ${dir} — ${err instanceof Error ? err.message : err}`);
|
|
42
|
-
return agents;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
for (const entry of entries) {
|
|
46
|
-
if (!entry.name.endsWith(".md")) continue;
|
|
47
|
-
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
48
|
-
|
|
49
|
-
const filePath = path.join(dir, entry.name);
|
|
50
|
-
let content: string;
|
|
51
|
-
try {
|
|
52
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
53
|
-
} catch (err) {
|
|
54
|
-
log.warn(`Failed to read agent file: ${filePath} — ${err instanceof Error ? err.message : err}`);
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
|
59
|
-
|
|
60
|
-
if (!frontmatter.name || !frontmatter.description) {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const tools = frontmatter.tools
|
|
65
|
-
?.split(",")
|
|
66
|
-
.map((t: string) => t.trim())
|
|
67
|
-
.filter(Boolean);
|
|
68
|
-
const extensions = frontmatter.extensions
|
|
69
|
-
?.split(",")
|
|
70
|
-
.map((t: string) => t.trim())
|
|
71
|
-
.filter(Boolean);
|
|
72
|
-
|
|
73
|
-
agents.push({
|
|
74
|
-
name: frontmatter.name,
|
|
75
|
-
description: frontmatter.description,
|
|
76
|
-
tools: tools && tools.length > 0 ? tools : undefined,
|
|
77
|
-
extensions: extensions && extensions.length > 0 ? extensions : undefined,
|
|
78
|
-
model: frontmatter.model,
|
|
79
|
-
systemPrompt: body,
|
|
80
|
-
source,
|
|
81
|
-
filePath,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return agents;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function isDirectory(p: string): boolean {
|
|
89
|
-
try {
|
|
90
|
-
return fs.statSync(p).isDirectory();
|
|
91
|
-
} catch (err) {
|
|
92
|
-
log.debug(`stat failed for ${p}: ${err instanceof Error ? err.message : err}`);
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
98
|
-
let currentDir = cwd;
|
|
99
|
-
while (true) {
|
|
100
|
-
const candidate = path.join(currentDir, ".pi", "agents");
|
|
101
|
-
if (isDirectory(candidate)) return candidate;
|
|
102
|
-
|
|
103
|
-
const parentDir = path.dirname(currentDir);
|
|
104
|
-
if (parentDir === currentDir) return null;
|
|
105
|
-
currentDir = parentDir;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
110
|
-
const userDir = path.join(os.homedir(), ".pi", "agent", "agents");
|
|
111
|
-
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
112
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
113
|
-
const packageRoot = path.resolve(path.dirname(thisFile), "..", "..");
|
|
114
|
-
const bundledAgentsDir = path.join(packageRoot, "agents");
|
|
115
|
-
|
|
116
|
-
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
117
|
-
const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
118
|
-
const bundledAgents = scope === "user" ? [] : loadAgentsFromDir(bundledAgentsDir, "bundled");
|
|
119
|
-
|
|
120
|
-
const agentMap = new Map<string, AgentConfig>();
|
|
121
|
-
|
|
122
|
-
if (scope === "both") {
|
|
123
|
-
for (const agent of bundledAgents) agentMap.set(agent.name, agent);
|
|
124
|
-
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
125
|
-
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
126
|
-
} else if (scope === "user") {
|
|
127
|
-
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
128
|
-
} else {
|
|
129
|
-
for (const agent of bundledAgents) agentMap.set(agent.name, agent);
|
|
130
|
-
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {
|
|
137
|
-
if (agents.length === 0) return { text: "none", remaining: 0 };
|
|
138
|
-
const listed = agents.slice(0, maxItems);
|
|
139
|
-
const remaining = agents.length - listed.length;
|
|
140
|
-
return {
|
|
141
|
-
text: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join("; "),
|
|
142
|
-
remaining,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_SUBAGENT_CONCURRENCY = 6;
|
|
2
|
-
|
|
3
|
-
export function getSubagentConcurrency(): number {
|
|
4
|
-
const envVal = process.env.PI_SUBAGENT_CONCURRENCY;
|
|
5
|
-
if (envVal) {
|
|
6
|
-
const parsed = Number.parseInt(envVal, 10);
|
|
7
|
-
if (Number.isFinite(parsed)) return Math.max(1, parsed);
|
|
8
|
-
}
|
|
9
|
-
return DEFAULT_SUBAGENT_CONCURRENCY;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export class Semaphore {
|
|
13
|
-
private _active = 0;
|
|
14
|
-
private _queue: Array<() => void> = [];
|
|
15
|
-
|
|
16
|
-
constructor(private _limit: number) {}
|
|
17
|
-
|
|
18
|
-
get limit(): number {
|
|
19
|
-
return this._limit;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
get active(): number {
|
|
23
|
-
return this._active;
|
|
24
|
-
}
|
|
25
|
-
get waiting(): number {
|
|
26
|
-
return this._queue.length;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async acquire(): Promise<() => void> {
|
|
30
|
-
if (this._active < this._limit) {
|
|
31
|
-
this._active++;
|
|
32
|
-
return this._createRelease();
|
|
33
|
-
}
|
|
34
|
-
return new Promise<() => void>((resolve) => {
|
|
35
|
-
this._queue.push(() => {
|
|
36
|
-
this._active++;
|
|
37
|
-
resolve(this._createRelease());
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
private _createRelease(): () => void {
|
|
43
|
-
let released = false;
|
|
44
|
-
return () => {
|
|
45
|
-
if (released) return;
|
|
46
|
-
released = true;
|
|
47
|
-
this._active--;
|
|
48
|
-
const next = this._queue.shift();
|
|
49
|
-
if (next) next();
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
}
|