pi-crew 0.1.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 (95) hide show
  1. package/AGENTS.md +32 -0
  2. package/CHANGELOG.md +6 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE.md +15 -0
  5. package/README.md +703 -0
  6. package/agents/analyst.md +11 -0
  7. package/agents/critic.md +11 -0
  8. package/agents/executor.md +11 -0
  9. package/agents/explorer.md +11 -0
  10. package/agents/planner.md +11 -0
  11. package/agents/reviewer.md +11 -0
  12. package/agents/security-reviewer.md +11 -0
  13. package/agents/test-engineer.md +11 -0
  14. package/agents/verifier.md +11 -0
  15. package/agents/writer.md +11 -0
  16. package/docs/architecture.md +92 -0
  17. package/docs/live-mailbox-runtime.md +36 -0
  18. package/docs/publishing.md +65 -0
  19. package/docs/resource-formats.md +131 -0
  20. package/docs/usage.md +203 -0
  21. package/index.ts +6 -0
  22. package/install.mjs +19 -0
  23. package/package.json +79 -0
  24. package/schema.json +45 -0
  25. package/skills/.gitkeep +0 -0
  26. package/src/agents/agent-config.ts +27 -0
  27. package/src/agents/agent-serializer.ts +34 -0
  28. package/src/agents/discover-agents.ts +73 -0
  29. package/src/config/config.ts +193 -0
  30. package/src/extension/async-notifier.ts +36 -0
  31. package/src/extension/autonomous-policy.ts +122 -0
  32. package/src/extension/help.ts +43 -0
  33. package/src/extension/import-index.ts +52 -0
  34. package/src/extension/management.ts +335 -0
  35. package/src/extension/project-init.ts +74 -0
  36. package/src/extension/register.ts +349 -0
  37. package/src/extension/run-bundle-schema.ts +85 -0
  38. package/src/extension/run-export.ts +59 -0
  39. package/src/extension/run-import.ts +46 -0
  40. package/src/extension/run-index.ts +28 -0
  41. package/src/extension/run-maintenance.ts +24 -0
  42. package/src/extension/session-summary.ts +8 -0
  43. package/src/extension/team-manager-command.ts +86 -0
  44. package/src/extension/team-recommendation.ts +174 -0
  45. package/src/extension/team-tool.ts +783 -0
  46. package/src/extension/tool-result.ts +16 -0
  47. package/src/extension/validate-resources.ts +77 -0
  48. package/src/prompt/prompt-runtime.ts +58 -0
  49. package/src/runtime/async-runner.ts +26 -0
  50. package/src/runtime/background-runner.ts +43 -0
  51. package/src/runtime/child-pi.ts +75 -0
  52. package/src/runtime/model-fallback.ts +101 -0
  53. package/src/runtime/pi-args.ts +81 -0
  54. package/src/runtime/pi-json-output.ts +110 -0
  55. package/src/runtime/pi-spawn.ts +96 -0
  56. package/src/runtime/process-status.ts +25 -0
  57. package/src/runtime/task-runner.ts +164 -0
  58. package/src/runtime/team-runner.ts +135 -0
  59. package/src/runtime/worker-heartbeat.ts +21 -0
  60. package/src/schema/team-tool-schema.ts +100 -0
  61. package/src/state/artifact-store.ts +36 -0
  62. package/src/state/atomic-write.ts +18 -0
  63. package/src/state/contracts.ts +88 -0
  64. package/src/state/event-log.ts +27 -0
  65. package/src/state/locks.ts +40 -0
  66. package/src/state/mailbox.ts +188 -0
  67. package/src/state/state-store.ts +119 -0
  68. package/src/state/task-claims.ts +42 -0
  69. package/src/state/types.ts +88 -0
  70. package/src/state/usage.ts +29 -0
  71. package/src/teams/discover-teams.ts +84 -0
  72. package/src/teams/team-config.ts +22 -0
  73. package/src/teams/team-serializer.ts +36 -0
  74. package/src/ui/run-dashboard.ts +138 -0
  75. package/src/utils/frontmatter.ts +36 -0
  76. package/src/utils/ids.ts +12 -0
  77. package/src/utils/names.ts +26 -0
  78. package/src/utils/paths.ts +15 -0
  79. package/src/workflows/discover-workflows.ts +101 -0
  80. package/src/workflows/validate-workflow.ts +40 -0
  81. package/src/workflows/workflow-config.ts +24 -0
  82. package/src/workflows/workflow-serializer.ts +31 -0
  83. package/src/worktree/cleanup.ts +69 -0
  84. package/src/worktree/worktree-manager.ts +60 -0
  85. package/teams/default.team.md +12 -0
  86. package/teams/fast-fix.team.md +11 -0
  87. package/teams/implementation.team.md +15 -0
  88. package/teams/research.team.md +11 -0
  89. package/teams/review.team.md +12 -0
  90. package/tsconfig.json +19 -0
  91. package/workflows/default.workflow.md +29 -0
  92. package/workflows/fast-fix.workflow.md +22 -0
  93. package/workflows/implementation.workflow.md +47 -0
  94. package/workflows/research.workflow.md +22 -0
  95. package/workflows/review.workflow.md +30 -0
@@ -0,0 +1,86 @@
1
+ import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
+ import { listRuns } from "./run-index.ts";
3
+ import { handleTeamTool } from "./team-tool.ts";
4
+ import { isToolError, textFromToolResult } from "./tool-result.ts";
5
+
6
+ async function notifyResult(ctx: ExtensionCommandContext, result: Awaited<ReturnType<typeof handleTeamTool>>): Promise<void> {
7
+ const text = textFromToolResult(result);
8
+ ctx.ui.notify(text.length > 1000 ? `${text.slice(0, 997)}...` : text, isToolError(result) ? "error" : "info");
9
+ }
10
+
11
+ export async function handleTeamManagerCommand(_args: string, ctx: ExtensionCommandContext): Promise<void> {
12
+ const action = await ctx.ui.select("pi-crew", [
13
+ "List teams/workflows/agents/runs",
14
+ "Run team",
15
+ "Show run status",
16
+ "Cleanup run worktrees",
17
+ "Create routed resource",
18
+ "Update routed resource",
19
+ "Doctor",
20
+ ]);
21
+ if (!action) return;
22
+
23
+ if (action.startsWith("List")) {
24
+ await notifyResult(ctx, await handleTeamTool({ action: "list" }, ctx));
25
+ return;
26
+ }
27
+
28
+ if (action === "Doctor") {
29
+ await notifyResult(ctx, await handleTeamTool({ action: "doctor" }, ctx));
30
+ return;
31
+ }
32
+
33
+ if (action === "Create routed resource" || action === "Update routed resource") {
34
+ const isUpdate = action === "Update routed resource";
35
+ const resource = await ctx.ui.select("Resource type", ["agent", "team"]);
36
+ if (resource !== "agent" && resource !== "team") return;
37
+ const name = await ctx.ui.input("Name", resource === "agent" ? "custom-agent" : "custom-team");
38
+ if (!name) return;
39
+ const description = await ctx.ui.input("Description", "When to use this resource");
40
+ if (!description) return;
41
+ const triggers = await ctx.ui.input("Triggers (comma-separated)", "");
42
+ const useWhen = await ctx.ui.input("Use when (comma-separated)", "");
43
+ const avoidWhen = await ctx.ui.input("Avoid when (comma-separated)", "");
44
+ const cost = await ctx.ui.select("Cost", ["cheap", "free", "expensive"]);
45
+ const category = await ctx.ui.input("Category", "custom");
46
+ const baseConfig = { name, description, scope: "project", triggers, useWhen, avoidWhen, cost, category };
47
+ if (resource === "agent") {
48
+ const systemPrompt = isUpdate ? undefined : `You are ${name}.`;
49
+ await notifyResult(ctx, await handleTeamTool({ action: isUpdate ? "update" : "create", resource, agent: name, config: { ...baseConfig, systemPrompt } }, ctx));
50
+ return;
51
+ }
52
+ const agent = await ctx.ui.input("Role agent", "executor");
53
+ await notifyResult(ctx, await handleTeamTool({ action: isUpdate ? "update" : "create", resource, team: name, config: { ...baseConfig, roles: [{ name: "executor", agent: agent || "executor" }] } }, ctx));
54
+ return;
55
+ }
56
+
57
+ if (action === "Run team") {
58
+ const team = await ctx.ui.input("Team name", "default");
59
+ if (team === undefined) return;
60
+ const goal = await ctx.ui.input("Goal", "Describe the team objective");
61
+ if (!goal) return;
62
+ const asyncRun = await ctx.ui.confirm("Async run?", "Run in detached background mode?");
63
+ const worktree = await ctx.ui.confirm("Worktree mode?", "Use git worktrees for task workspaces? Requires a clean repo by default.");
64
+ await notifyResult(ctx, await handleTeamTool({ action: "run", team: team || "default", goal, async: asyncRun, workspaceMode: worktree ? "worktree" : "single" }, ctx));
65
+ return;
66
+ }
67
+
68
+ const runs = listRuns(ctx.cwd).slice(0, 20);
69
+ if (runs.length === 0) {
70
+ ctx.ui.notify("No pi-crew runs found.", "info");
71
+ return;
72
+ }
73
+ const selected = await ctx.ui.select("Select run", runs.map((run) => `${run.runId} [${run.status}] ${run.team}/${run.workflow ?? "none"}`));
74
+ if (!selected) return;
75
+ const runId = selected.split(" ")[0];
76
+ if (!runId) return;
77
+
78
+ if (action === "Show run status") {
79
+ await notifyResult(ctx, await handleTeamTool({ action: "status", runId }, ctx));
80
+ return;
81
+ }
82
+ if (action === "Cleanup run worktrees") {
83
+ const force = await ctx.ui.confirm("Force cleanup?", "Force may remove dirty worktrees. Choose false to preserve dirty worktrees and capture cleanup diffs.");
84
+ await notifyResult(ctx, await handleTeamTool({ action: "cleanup", runId, force }, ctx));
85
+ }
86
+ }
@@ -0,0 +1,174 @@
1
+ import { detectTeamIntent } from "./autonomous-policy.ts";
2
+ import type { AgentConfig } from "../agents/agent-config.ts";
3
+ import type { TeamConfig } from "../teams/team-config.ts";
4
+ import type { PiTeamsAutonomousConfig } from "../config/config.ts";
5
+
6
+ export type DecompositionStrategy = "numbered" | "bulleted" | "conjunction" | "atomic";
7
+
8
+ export interface RecommendedSubtask {
9
+ subject: string;
10
+ description: string;
11
+ role: string;
12
+ }
13
+
14
+ export interface TeamRecommendation {
15
+ team: "default" | "implementation" | "review" | "fast-fix" | "research";
16
+ workflow: "default" | "implementation" | "review" | "fast-fix" | "research";
17
+ action: "plan" | "run";
18
+ async: boolean;
19
+ workspaceMode: "single" | "worktree";
20
+ confidence: "low" | "medium" | "high";
21
+ decomposition: { strategy: DecompositionStrategy; subtasks: RecommendedSubtask[]; fanout: number };
22
+ reasons: string[];
23
+ }
24
+
25
+ const REVIEW_TERMS = ["review", "audit", "security", "vulnerability", "diff", "pr", "pull request"];
26
+ const RESEARCH_TERMS = ["research", "investigate", "compare", "analyze", "document", "docs", "explain", "architecture"];
27
+ const FAST_FIX_TERMS = ["quick fix", "fast-fix", "small bug", "typo", "one-line", "minor", "lint"];
28
+ const IMPLEMENTATION_TERMS = ["implement", "refactor", "migrate", "feature", "tests", "test", "integration", "upgrade", "build", "create", "add"];
29
+ const RISKY_TERMS = ["migration", "refactor", "large", "multiple", "parallel", "concurrent", "risky", "critical"];
30
+ const NUMBERED_LINE_RE = /^\s*\d+[.)]\s+(.+)$/;
31
+ const BULLETED_LINE_RE = /^\s*[-*•]\s+(.+)$/;
32
+ const CONJUNCTION_SPLIT_RE = /\s+(?:and|,\s*and|,)\s+/i;
33
+ const FILE_REF_RE = /\b\S+\.\w{1,8}\b/g;
34
+ const CODE_SYMBOL_RE = /`[^`]+`/g;
35
+
36
+ function includesAny(text: string, terms: string[]): string[] {
37
+ return terms.filter((term) => text.includes(term));
38
+ }
39
+
40
+ function wordCount(text: string): number {
41
+ return text.trim().split(/\s+/).filter(Boolean).length;
42
+ }
43
+
44
+ function recommendRole(text: string): string {
45
+ const lower = text.toLowerCase();
46
+ if (includesAny(lower, ["test", "spec", "coverage", "verify"]).length > 0) return "test-engineer";
47
+ if (includesAny(lower, ["security", "vulnerability", "auth", "owasp"]).length > 0) return "security-reviewer";
48
+ if (includesAny(lower, ["review", "audit", "diff"]).length > 0) return "reviewer";
49
+ if (includesAny(lower, ["doc", "readme", "guide", "write"]).length > 0) return "writer";
50
+ if (includesAny(lower, ["research", "investigate", "explore", "find", "trace"]).length > 0) return "explorer";
51
+ if (includesAny(lower, ["plan", "design", "architecture"]).length > 0) return "planner";
52
+ return "executor";
53
+ }
54
+
55
+ function makeSubtask(text: string): RecommendedSubtask {
56
+ const subject = text.trim().slice(0, 80) || "Task";
57
+ return { subject, description: text.trim(), role: recommendRole(text) };
58
+ }
59
+
60
+ export function decomposeGoal(goal: string): { strategy: DecompositionStrategy; subtasks: RecommendedSubtask[]; fanout: number } {
61
+ const lines = goal.split("\n").map((line) => line.trim()).filter(Boolean);
62
+ const fileRefs = goal.match(FILE_REF_RE)?.length ?? 0;
63
+ const codeSymbols = goal.match(CODE_SYMBOL_RE)?.length ?? 0;
64
+ const hasParallelKeyword = /\b(?:parallel|concurrently|simultaneously|independently)\b/i.test(goal);
65
+ if (fileRefs >= 3 || codeSymbols >= 3 || hasParallelKeyword) {
66
+ const subtask = makeSubtask(goal);
67
+ return { strategy: "atomic", subtasks: [subtask], fanout: 1 };
68
+ }
69
+ if (lines.length >= 2 && lines.every((line) => NUMBERED_LINE_RE.test(line))) {
70
+ const subtasks = lines.map((line) => makeSubtask(line.match(NUMBERED_LINE_RE)?.[1] ?? line));
71
+ return { strategy: "numbered", subtasks, fanout: subtasks.length };
72
+ }
73
+ if (lines.length >= 2 && lines.every((line) => BULLETED_LINE_RE.test(line))) {
74
+ const subtasks = lines.map((line) => makeSubtask(line.match(BULLETED_LINE_RE)?.[1] ?? line));
75
+ return { strategy: "bulleted", subtasks, fanout: subtasks.length };
76
+ }
77
+ if (lines.length === 1) {
78
+ const parts = lines[0].split(CONJUNCTION_SPLIT_RE).map((part) => part.trim()).filter(Boolean);
79
+ if (parts.length >= 2) {
80
+ const subtasks = parts.map((part) => makeSubtask(part));
81
+ return { strategy: "conjunction", subtasks, fanout: subtasks.length };
82
+ }
83
+ }
84
+ const subtask = makeSubtask(goal);
85
+ return { strategy: "atomic", subtasks: [subtask], fanout: 1 };
86
+ }
87
+
88
+ function metadataMatches(goal: string, values: string[] | undefined): string[] {
89
+ const lower = goal.toLowerCase();
90
+ return (values ?? []).filter((value) => lower.includes(value.toLowerCase()));
91
+ }
92
+
93
+ export function recommendTeam(goal: string, config: PiTeamsAutonomousConfig = {}, resources?: { teams?: TeamConfig[]; agents?: AgentConfig[] }): TeamRecommendation {
94
+ const normalized = goal.toLowerCase();
95
+ const intents = detectTeamIntent(goal, config);
96
+ const reasons: string[] = [];
97
+ let team: TeamRecommendation["team"] = "default";
98
+ let workflow: TeamRecommendation["workflow"] = "default";
99
+ let action: TeamRecommendation["action"] = "run";
100
+ let confidence: TeamRecommendation["confidence"] = "medium";
101
+
102
+ if (intents.length > 0) reasons.push(`Matched explicit intent keyword(s): ${intents.join(", ")}.`);
103
+
104
+ const metadataTeamMatches = (resources?.teams ?? [])
105
+ .map((candidate) => ({ team: candidate, matches: [...metadataMatches(goal, candidate.routing?.triggers), ...metadataMatches(goal, candidate.routing?.useWhen)] }))
106
+ .filter((candidate) => candidate.matches.length > 0)
107
+ .sort((a, b) => b.matches.length - a.matches.length);
108
+
109
+ const reviewMatches = includesAny(normalized, REVIEW_TERMS);
110
+ const researchMatches = includesAny(normalized, RESEARCH_TERMS);
111
+ const fastFixMatches = includesAny(normalized, FAST_FIX_TERMS);
112
+ const implementationMatches = includesAny(normalized, IMPLEMENTATION_TERMS);
113
+ const riskyMatches = includesAny(normalized, RISKY_TERMS);
114
+
115
+ if (metadataTeamMatches[0]) {
116
+ team = metadataTeamMatches[0].team.name as TeamRecommendation["team"];
117
+ workflow = (metadataTeamMatches[0].team.defaultWorkflow ?? metadataTeamMatches[0].team.name) as TeamRecommendation["workflow"];
118
+ confidence = "high";
119
+ reasons.push(`Matched team routing metadata for '${metadataTeamMatches[0].team.name}': ${metadataTeamMatches[0].matches.join(", ")}.`);
120
+ } else if (intents.includes("review") || reviewMatches.length >= 2 || normalized.includes("security review")) {
121
+ team = "review";
122
+ workflow = "review";
123
+ confidence = "high";
124
+ reasons.push(`Review/audit terms detected: ${reviewMatches.join(", ") || "explicit review intent"}.`);
125
+ } else if (intents.includes("research") || (researchMatches.length > 0 && implementationMatches.length === 0)) {
126
+ team = "research";
127
+ workflow = "research";
128
+ confidence = researchMatches.length >= 2 ? "high" : "medium";
129
+ reasons.push(`Research/analysis terms detected: ${researchMatches.join(", ")}.`);
130
+ } else if (intents.includes("fastFix") || fastFixMatches.length > 0) {
131
+ team = "fast-fix";
132
+ workflow = "fast-fix";
133
+ confidence = "high";
134
+ reasons.push(`Small fix terms detected: ${fastFixMatches.join(", ") || "fast-fix intent"}.`);
135
+ } else if (intents.includes("implementation") || implementationMatches.length > 0) {
136
+ team = "implementation";
137
+ workflow = "implementation";
138
+ confidence = implementationMatches.length >= 2 || riskyMatches.length > 0 ? "high" : "medium";
139
+ reasons.push(`Implementation terms detected: ${implementationMatches.join(", ") || "implementation intent"}.`);
140
+ } else {
141
+ action = "plan";
142
+ confidence = wordCount(goal) < 8 ? "low" : "medium";
143
+ reasons.push("No strong team-specific intent detected; start with planning/default discovery.");
144
+ }
145
+
146
+ const decomposition = decomposeGoal(goal);
147
+ if (decomposition.strategy !== "atomic") reasons.push(`Goal decomposes into ${decomposition.subtasks.length} subtasks using ${decomposition.strategy} parsing.`);
148
+ const async = config.preferAsyncForLongTasks === true && (wordCount(goal) > 24 || riskyMatches.length > 0 || implementationMatches.length >= 2 || decomposition.fanout >= 3);
149
+ const workspaceMode = config.allowWorktreeSuggestion === false ? "single" : (riskyMatches.length > 0 && team === "implementation" ? "worktree" : "single");
150
+ if (async) reasons.push("Task appears long/risky and config prefers async for long tasks.");
151
+ if (workspaceMode === "worktree") reasons.push(`Risk/isolation terms detected: ${riskyMatches.join(", ")}.`);
152
+
153
+ return { team, workflow, action, async, workspaceMode, confidence, decomposition, reasons };
154
+ }
155
+
156
+ export function formatRecommendation(goal: string, recommendation: TeamRecommendation): string {
157
+ return [
158
+ "pi-crew recommendation:",
159
+ `Goal: ${goal}`,
160
+ `Action: ${recommendation.action}`,
161
+ `Team: ${recommendation.team}`,
162
+ `Workflow: ${recommendation.workflow}`,
163
+ `Async: ${recommendation.async}`,
164
+ `Workspace mode: ${recommendation.workspaceMode}`,
165
+ `Confidence: ${recommendation.confidence}`,
166
+ `Decomposition: ${recommendation.decomposition.strategy} (${recommendation.decomposition.fanout} lane${recommendation.decomposition.fanout === 1 ? "" : "s"})`,
167
+ "Subtasks:",
168
+ ...recommendation.decomposition.subtasks.map((task, index) => `- ${index + 1}. [${task.role}] ${task.subject}`),
169
+ "Reasons:",
170
+ ...recommendation.reasons.map((reason) => `- ${reason}`),
171
+ "Suggested tool call:",
172
+ JSON.stringify({ action: recommendation.action, team: recommendation.team, workflow: recommendation.workflow, goal, async: recommendation.async, workspaceMode: recommendation.workspaceMode }, null, 2),
173
+ ].join("\n");
174
+ }