@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.
Files changed (62) hide show
  1. package/README.md +44 -494
  2. package/docs/developer-usage-guide.md +41 -401
  3. package/docs/oversight-model.md +13 -34
  4. package/docs/workflow-phases.md +32 -46
  5. package/extensions/workflow-guard.ts +67 -0
  6. package/package.json +3 -7
  7. package/skills/brainstorming/SKILL.md +16 -59
  8. package/skills/executing-tasks/SKILL.md +26 -227
  9. package/skills/finalizing/SKILL.md +33 -0
  10. package/skills/writing-plans/SKILL.md +23 -132
  11. package/ROADMAP.md +0 -16
  12. package/agents/code-reviewer.md +0 -18
  13. package/agents/config.ts +0 -5
  14. package/agents/implementer.md +0 -26
  15. package/agents/spec-reviewer.md +0 -13
  16. package/agents/worker.md +0 -17
  17. package/docs/plans/completed/2026-04-09-cleanup-legacy-state-and-enforce-think-phases-design.md +0 -56
  18. package/docs/plans/completed/2026-04-09-cleanup-legacy-state-and-enforce-think-phases-implementation.md +0 -196
  19. package/docs/plans/completed/2026-04-09-workflow-next-autocomplete-design.md +0 -185
  20. package/docs/plans/completed/2026-04-09-workflow-next-autocomplete-implementation.md +0 -334
  21. package/docs/plans/completed/2026-04-09-workflow-next-handoff-state-design.md +0 -251
  22. package/docs/plans/completed/2026-04-09-workflow-next-handoff-state-implementation.md +0 -253
  23. package/extensions/constants.ts +0 -15
  24. package/extensions/lib/logging.ts +0 -138
  25. package/extensions/plan-tracker.ts +0 -502
  26. package/extensions/subagent/agents.ts +0 -144
  27. package/extensions/subagent/concurrency.ts +0 -52
  28. package/extensions/subagent/env.ts +0 -47
  29. package/extensions/subagent/index.ts +0 -1181
  30. package/extensions/subagent/lifecycle.ts +0 -25
  31. package/extensions/subagent/timeout.ts +0 -13
  32. package/extensions/workflow-monitor/debug-monitor.ts +0 -98
  33. package/extensions/workflow-monitor/git.ts +0 -31
  34. package/extensions/workflow-monitor/heuristics.ts +0 -58
  35. package/extensions/workflow-monitor/investigation.ts +0 -52
  36. package/extensions/workflow-monitor/reference-tool.ts +0 -42
  37. package/extensions/workflow-monitor/skip-confirmation.ts +0 -19
  38. package/extensions/workflow-monitor/tdd-monitor.ts +0 -137
  39. package/extensions/workflow-monitor/test-runner.ts +0 -37
  40. package/extensions/workflow-monitor/verification-monitor.ts +0 -61
  41. package/extensions/workflow-monitor/warnings.ts +0 -81
  42. package/extensions/workflow-monitor/workflow-handler.ts +0 -358
  43. package/extensions/workflow-monitor/workflow-next-completions.ts +0 -68
  44. package/extensions/workflow-monitor/workflow-next-state.ts +0 -112
  45. package/extensions/workflow-monitor/workflow-tracker.ts +0 -253
  46. package/extensions/workflow-monitor/workflow-transitions.ts +0 -55
  47. package/extensions/workflow-monitor.ts +0 -872
  48. package/skills/dispatching-parallel-agents/SKILL.md +0 -194
  49. package/skills/receiving-code-review/SKILL.md +0 -196
  50. package/skills/systematic-debugging/SKILL.md +0 -170
  51. package/skills/systematic-debugging/condition-based-waiting-example.ts +0 -158
  52. package/skills/systematic-debugging/condition-based-waiting.md +0 -115
  53. package/skills/systematic-debugging/defense-in-depth.md +0 -122
  54. package/skills/systematic-debugging/find-polluter.sh +0 -63
  55. package/skills/systematic-debugging/reference/rationalizations.md +0 -61
  56. package/skills/systematic-debugging/root-cause-tracing.md +0 -169
  57. package/skills/test-driven-development/SKILL.md +0 -266
  58. package/skills/test-driven-development/reference/examples.md +0 -101
  59. package/skills/test-driven-development/reference/rationalizations.md +0 -67
  60. package/skills/test-driven-development/reference/when-stuck.md +0 -33
  61. package/skills/test-driven-development/testing-anti-patterns.md +0 -299
  62. 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
- }