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
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "pi-crew",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
+ "author": "baphuongna",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/baphuongna/pi-crew.git"
10
+ },
11
+ "homepage": "https://github.com/baphuongna/pi-crew#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/baphuongna/pi-crew/issues"
14
+ },
15
+ "type": "module",
16
+ "bin": {
17
+ "pi-crew": "install.mjs"
18
+ },
19
+ "keywords": [
20
+ "pi-package",
21
+ "pi",
22
+ "pi-coding-agent",
23
+ "teams",
24
+ "agents",
25
+ "multi-agent",
26
+ "orchestration"
27
+ ],
28
+ "files": [
29
+ "*.ts",
30
+ "*.mjs",
31
+ "src/**/*.ts",
32
+ "agents/",
33
+ "teams/",
34
+ "workflows/",
35
+ "skills/**/*",
36
+ "README.md",
37
+ "AGENTS.md",
38
+ "docs/",
39
+ "tsconfig.json",
40
+ "schema.json",
41
+ "CHANGELOG.md",
42
+ "LICENSE",
43
+ "NOTICE.md"
44
+ ],
45
+ "scripts": {
46
+ "check": "npm run ci",
47
+ "ci": "npm run typecheck && npm test && npm pack --dry-run",
48
+ "typecheck": "tsc --noEmit && node --experimental-strip-types -e \"await import('./index.ts'); console.log('strip-types import ok')\"",
49
+ "test": "npm run test:unit",
50
+ "test:unit": "node --experimental-strip-types --test test/unit/*.test.ts",
51
+ "smoke:pi": "pi install ."
52
+ },
53
+ "exports": {
54
+ "./schema.json": "./schema.json"
55
+ },
56
+ "pi": {
57
+ "extensions": [
58
+ "./index.ts"
59
+ ],
60
+ "skills": [
61
+ "./skills"
62
+ ]
63
+ },
64
+ "peerDependencies": {
65
+ "@mariozechner/pi-agent-core": "*",
66
+ "@mariozechner/pi-ai": "*",
67
+ "@mariozechner/pi-coding-agent": "*",
68
+ "@mariozechner/pi-tui": "*"
69
+ },
70
+ "dependencies": {
71
+ "typebox": "^1.1.24"
72
+ },
73
+ "devDependencies": {
74
+ "@mariozechner/pi-agent-core": "^0.65.0",
75
+ "@mariozechner/pi-ai": "^0.65.0",
76
+ "@mariozechner/pi-coding-agent": "^0.65.0",
77
+ "typescript": "^5.9.3"
78
+ }
79
+ }
package/schema.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://example.invalid/pi-crew.schema.json",
4
+ "title": "pi-crew config",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "asyncByDefault": {
9
+ "type": "boolean",
10
+ "description": "Run team workflows in detached async mode by default when the tool call omits async."
11
+ },
12
+ "executeWorkers": {
13
+ "type": "boolean",
14
+ "description": "Allow real child Pi workers without setting PI_TEAMS_EXECUTE_WORKERS=1."
15
+ },
16
+ "notifierIntervalMs": {
17
+ "type": "number",
18
+ "minimum": 1000,
19
+ "description": "Polling interval for async completion notifications."
20
+ },
21
+ "requireCleanWorktreeLeader": {
22
+ "type": "boolean",
23
+ "description": "Require a clean leader git repository before provisioning worktrees."
24
+ },
25
+ "autonomous": {
26
+ "type": "object",
27
+ "additionalProperties": false,
28
+ "description": "Autonomous team routing policy injected into the agent system prompt.",
29
+ "properties": {
30
+ "profile": { "type": "string", "enum": ["manual", "suggested", "assisted", "aggressive"] },
31
+ "enabled": { "type": "boolean" },
32
+ "injectPolicy": { "type": "boolean" },
33
+ "preferAsyncForLongTasks": { "type": "boolean" },
34
+ "allowWorktreeSuggestion": { "type": "boolean" },
35
+ "magicKeywords": {
36
+ "type": "object",
37
+ "additionalProperties": {
38
+ "type": "array",
39
+ "items": { "type": "string" }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
File without changes
@@ -0,0 +1,27 @@
1
+ export type ResourceSource = "builtin" | "user" | "project";
2
+
3
+ export interface RoutingMetadata {
4
+ triggers?: string[];
5
+ useWhen?: string[];
6
+ avoidWhen?: string[];
7
+ cost?: "free" | "cheap" | "expensive";
8
+ category?: string;
9
+ }
10
+
11
+ export interface AgentConfig {
12
+ name: string;
13
+ description: string;
14
+ source: ResourceSource;
15
+ filePath: string;
16
+ systemPrompt: string;
17
+ model?: string;
18
+ fallbackModels?: string[];
19
+ thinking?: string;
20
+ tools?: string[];
21
+ extensions?: string[];
22
+ skills?: string[];
23
+ systemPromptMode?: "replace" | "append";
24
+ inheritProjectContext?: boolean;
25
+ inheritSkills?: boolean;
26
+ routing?: RoutingMetadata;
27
+ }
@@ -0,0 +1,34 @@
1
+ import type { AgentConfig } from "./agent-config.ts";
2
+
3
+ function line(key: string, value: string | boolean | string[] | undefined): string | undefined {
4
+ if (value === undefined) return undefined;
5
+ if (Array.isArray(value)) return `${key}: ${value.join(", ")}`;
6
+ return `${key}: ${String(value)}`;
7
+ }
8
+
9
+ export function serializeAgent(agent: AgentConfig): string {
10
+ const lines = [
11
+ "---",
12
+ `name: ${agent.name}`,
13
+ `description: ${agent.description}`,
14
+ line("model", agent.model),
15
+ line("fallbackModels", agent.fallbackModels),
16
+ line("thinking", agent.thinking),
17
+ line("tools", agent.tools),
18
+ agent.extensions !== undefined ? line("extensions", agent.extensions) ?? "extensions:" : undefined,
19
+ line("skills", agent.skills),
20
+ line("systemPromptMode", agent.systemPromptMode),
21
+ line("inheritProjectContext", agent.inheritProjectContext),
22
+ line("inheritSkills", agent.inheritSkills),
23
+ line("triggers", agent.routing?.triggers),
24
+ line("useWhen", agent.routing?.useWhen),
25
+ line("avoidWhen", agent.routing?.avoidWhen),
26
+ line("cost", agent.routing?.cost),
27
+ line("category", agent.routing?.category),
28
+ "---",
29
+ "",
30
+ agent.systemPrompt.trim(),
31
+ "",
32
+ ].filter((entry): entry is string => entry !== undefined);
33
+ return lines.join("\n");
34
+ }
@@ -0,0 +1,73 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { AgentConfig, ResourceSource } from "./agent-config.ts";
4
+ import { parseCsv, parseFrontmatter } from "../utils/frontmatter.ts";
5
+ import { packageRoot, projectPiRoot, userPiRoot } from "../utils/paths.ts";
6
+
7
+ export interface AgentDiscoveryResult {
8
+ builtin: AgentConfig[];
9
+ user: AgentConfig[];
10
+ project: AgentConfig[];
11
+ }
12
+
13
+ function parseCost(value: string | undefined): "free" | "cheap" | "expensive" | undefined {
14
+ return value === "free" || value === "cheap" || value === "expensive" ? value : undefined;
15
+ }
16
+
17
+ function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig | undefined {
18
+ try {
19
+ const content = fs.readFileSync(filePath, "utf-8");
20
+ const { frontmatter, body } = parseFrontmatter(content);
21
+ const name = frontmatter.name?.trim() || path.basename(filePath, path.extname(filePath));
22
+ const description = frontmatter.description?.trim() || "No description provided.";
23
+ const triggers = parseCsv(frontmatter.triggers ?? frontmatter.trigger);
24
+ const useWhen = parseCsv(frontmatter.useWhen);
25
+ const avoidWhen = parseCsv(frontmatter.avoidWhen);
26
+ const cost = parseCost(frontmatter.cost);
27
+ const category = frontmatter.category?.trim() || undefined;
28
+ return {
29
+ name,
30
+ description,
31
+ source,
32
+ filePath,
33
+ systemPrompt: body.trim(),
34
+ model: frontmatter.model || undefined,
35
+ fallbackModels: parseCsv(frontmatter.fallbackModels),
36
+ thinking: frontmatter.thinking || undefined,
37
+ tools: parseCsv(frontmatter.tools),
38
+ extensions: frontmatter.extensions === "" ? [] : parseCsv(frontmatter.extensions),
39
+ skills: parseCsv(frontmatter.skills ?? frontmatter.skill),
40
+ systemPromptMode: frontmatter.systemPromptMode === "append" ? "append" : "replace",
41
+ inheritProjectContext: frontmatter.inheritProjectContext === "true",
42
+ inheritSkills: frontmatter.inheritSkills === "true",
43
+ routing: triggers || useWhen || avoidWhen || cost || category ? { triggers, useWhen, avoidWhen, cost, category } : undefined,
44
+ };
45
+ } catch {
46
+ return undefined;
47
+ }
48
+ }
49
+
50
+ function readAgentDir(dir: string, source: ResourceSource): AgentConfig[] {
51
+ if (!fs.existsSync(dir)) return [];
52
+ return fs.readdirSync(dir)
53
+ .filter((entry) => entry.endsWith(".md") && !entry.endsWith(".team.md") && !entry.endsWith(".workflow.md"))
54
+ .map((entry) => parseAgentFile(path.join(dir, entry), source))
55
+ .filter((agent): agent is AgentConfig => agent !== undefined)
56
+ .sort((a, b) => a.name.localeCompare(b.name));
57
+ }
58
+
59
+ export function discoverAgents(cwd: string): AgentDiscoveryResult {
60
+ return {
61
+ builtin: readAgentDir(path.join(packageRoot(), "agents"), "builtin"),
62
+ user: readAgentDir(path.join(userPiRoot(), "agents"), "user"),
63
+ project: readAgentDir(path.join(projectPiRoot(cwd), "agents"), "project"),
64
+ };
65
+ }
66
+
67
+ export function allAgents(discovery: AgentDiscoveryResult): AgentConfig[] {
68
+ const byName = new Map<string, AgentConfig>();
69
+ for (const agent of [...discovery.builtin, ...discovery.user, ...discovery.project]) {
70
+ byName.set(agent.name, agent);
71
+ }
72
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
73
+ }
@@ -0,0 +1,193 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+
5
+ export type PiTeamsAutonomyProfile = "manual" | "suggested" | "assisted" | "aggressive";
6
+
7
+ export interface PiTeamsAutonomousConfig {
8
+ profile?: PiTeamsAutonomyProfile;
9
+ enabled?: boolean;
10
+ injectPolicy?: boolean;
11
+ preferAsyncForLongTasks?: boolean;
12
+ allowWorktreeSuggestion?: boolean;
13
+ magicKeywords?: Record<string, string[]>;
14
+ }
15
+
16
+ export interface PiTeamsConfig {
17
+ asyncByDefault?: boolean;
18
+ executeWorkers?: boolean;
19
+ notifierIntervalMs?: number;
20
+ requireCleanWorktreeLeader?: boolean;
21
+ autonomous?: PiTeamsAutonomousConfig;
22
+ }
23
+
24
+ export interface LoadedPiTeamsConfig {
25
+ config: PiTeamsConfig;
26
+ path: string;
27
+ paths: string[];
28
+ error?: string;
29
+ }
30
+
31
+ export interface SavedPiTeamsConfig {
32
+ config: PiTeamsConfig;
33
+ path: string;
34
+ }
35
+
36
+ export interface UpdateConfigOptions {
37
+ cwd?: string;
38
+ scope?: "user" | "project";
39
+ unsetPaths?: string[];
40
+ }
41
+
42
+ export function configPath(): string {
43
+ const home = process.env.PI_TEAMS_HOME?.trim() || os.homedir();
44
+ return path.join(home, ".pi", "agent", "extensions", "pi-crew", "config.json");
45
+ }
46
+
47
+ export function projectConfigPath(cwd: string): string {
48
+ return path.join(cwd, ".pi", "teams", "config.json");
49
+ }
50
+
51
+ function withoutUndefined<T extends Record<string, unknown>>(value: T): Partial<T> {
52
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as Partial<T>;
53
+ }
54
+
55
+ function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfig {
56
+ const merged: PiTeamsConfig = { ...base, ...withoutUndefined(override as Record<string, unknown>) };
57
+ if (base.autonomous || override.autonomous) {
58
+ merged.autonomous = {
59
+ ...(base.autonomous ?? {}),
60
+ ...withoutUndefined((override.autonomous ?? {}) as Record<string, unknown>),
61
+ };
62
+ }
63
+ return merged;
64
+ }
65
+
66
+ function parseAutonomyProfile(value: unknown): PiTeamsAutonomyProfile | undefined {
67
+ return value === "manual" || value === "suggested" || value === "assisted" || value === "aggressive" ? value : undefined;
68
+ }
69
+
70
+ export function effectiveAutonomousConfig(config: PiTeamsAutonomousConfig | undefined): Required<Pick<PiTeamsAutonomousConfig, "profile" | "enabled" | "injectPolicy" | "preferAsyncForLongTasks" | "allowWorktreeSuggestion">> & Pick<PiTeamsAutonomousConfig, "magicKeywords"> {
71
+ const profile = config?.enabled === false ? "manual" : (config?.profile ?? "suggested");
72
+ const profileDefaults: Record<PiTeamsAutonomyProfile, { enabled: boolean; injectPolicy: boolean; preferAsyncForLongTasks: boolean; allowWorktreeSuggestion: boolean }> = {
73
+ manual: { enabled: false, injectPolicy: false, preferAsyncForLongTasks: false, allowWorktreeSuggestion: false },
74
+ suggested: { enabled: true, injectPolicy: true, preferAsyncForLongTasks: false, allowWorktreeSuggestion: true },
75
+ assisted: { enabled: true, injectPolicy: true, preferAsyncForLongTasks: true, allowWorktreeSuggestion: true },
76
+ aggressive: { enabled: true, injectPolicy: true, preferAsyncForLongTasks: true, allowWorktreeSuggestion: true },
77
+ };
78
+ const defaults = profileDefaults[profile];
79
+ return {
80
+ profile,
81
+ enabled: config?.enabled ?? defaults.enabled,
82
+ injectPolicy: config?.injectPolicy ?? defaults.injectPolicy,
83
+ preferAsyncForLongTasks: config?.preferAsyncForLongTasks ?? defaults.preferAsyncForLongTasks,
84
+ allowWorktreeSuggestion: config?.allowWorktreeSuggestion ?? defaults.allowWorktreeSuggestion,
85
+ magicKeywords: config?.magicKeywords,
86
+ };
87
+ }
88
+
89
+ function parseStringArrayRecord(value: unknown): Record<string, string[]> | undefined {
90
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
91
+ const result: Record<string, string[]> = {};
92
+ for (const [key, rawValues] of Object.entries(value)) {
93
+ if (!Array.isArray(rawValues)) continue;
94
+ const values = rawValues.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim());
95
+ if (values.length > 0) result[key] = values;
96
+ }
97
+ return Object.keys(result).length > 0 ? result : undefined;
98
+ }
99
+
100
+ function parseAutonomousConfig(value: unknown): PiTeamsAutonomousConfig | undefined {
101
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
102
+ const obj = value as Record<string, unknown>;
103
+ return {
104
+ profile: parseAutonomyProfile(obj.profile),
105
+ enabled: typeof obj.enabled === "boolean" ? obj.enabled : undefined,
106
+ injectPolicy: typeof obj.injectPolicy === "boolean" ? obj.injectPolicy : undefined,
107
+ preferAsyncForLongTasks: typeof obj.preferAsyncForLongTasks === "boolean" ? obj.preferAsyncForLongTasks : undefined,
108
+ allowWorktreeSuggestion: typeof obj.allowWorktreeSuggestion === "boolean" ? obj.allowWorktreeSuggestion : undefined,
109
+ magicKeywords: parseStringArrayRecord(obj.magicKeywords),
110
+ };
111
+ }
112
+
113
+ function parseConfig(raw: unknown): PiTeamsConfig {
114
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
115
+ const obj = raw as Record<string, unknown>;
116
+ return {
117
+ asyncByDefault: typeof obj.asyncByDefault === "boolean" ? obj.asyncByDefault : undefined,
118
+ executeWorkers: typeof obj.executeWorkers === "boolean" ? obj.executeWorkers : undefined,
119
+ notifierIntervalMs: typeof obj.notifierIntervalMs === "number" && Number.isFinite(obj.notifierIntervalMs) && obj.notifierIntervalMs >= 1000 ? obj.notifierIntervalMs : undefined,
120
+ requireCleanWorktreeLeader: typeof obj.requireCleanWorktreeLeader === "boolean" ? obj.requireCleanWorktreeLeader : undefined,
121
+ autonomous: parseAutonomousConfig(obj.autonomous),
122
+ };
123
+ }
124
+
125
+ function unsetPath(record: Record<string, unknown>, dottedPath: string): void {
126
+ const parts = dottedPath.split(".").filter(Boolean);
127
+ if (parts.length === 0) return;
128
+ let target: Record<string, unknown> = record;
129
+ for (const part of parts.slice(0, -1)) {
130
+ const current = target[part];
131
+ if (!current || typeof current !== "object" || Array.isArray(current)) return;
132
+ target = current as Record<string, unknown>;
133
+ }
134
+ delete target[parts[parts.length - 1]!];
135
+ }
136
+
137
+ function readConfigRecord(filePath: string): Record<string, unknown> {
138
+ if (!fs.existsSync(filePath)) return {};
139
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
140
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
141
+ return raw as Record<string, unknown>;
142
+ }
143
+
144
+ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
145
+ const filePath = configPath();
146
+ const paths = cwd ? [filePath, projectConfigPath(cwd)] : [filePath];
147
+ try {
148
+ let config = parseConfig(readConfigRecord(filePath));
149
+ if (cwd) config = mergeConfig(config, parseConfig(readConfigRecord(projectConfigPath(cwd))));
150
+ return { path: filePath, paths, config };
151
+ } catch (error) {
152
+ const message = error instanceof Error ? error.message : String(error);
153
+ return { path: filePath, paths, config: {}, error: message };
154
+ }
155
+ }
156
+
157
+ export function updateConfig(patch: PiTeamsConfig, options: UpdateConfigOptions = {}): SavedPiTeamsConfig {
158
+ const filePath = options.scope === "project" && options.cwd ? projectConfigPath(options.cwd) : configPath();
159
+ let current: Record<string, unknown>;
160
+ try {
161
+ current = readConfigRecord(filePath);
162
+ } catch (error) {
163
+ const message = error instanceof Error ? error.message : String(error);
164
+ throw new Error(`Could not update pi-crew config: ${message}`);
165
+ }
166
+ let merged = mergeConfig(parseConfig(current), patch);
167
+ if (options.unsetPaths?.length) {
168
+ const raw = JSON.parse(JSON.stringify(merged)) as Record<string, unknown>;
169
+ for (const unset of options.unsetPaths) unsetPath(raw, unset);
170
+ merged = parseConfig(raw);
171
+ }
172
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
173
+ fs.writeFileSync(filePath, `${JSON.stringify(merged, null, 2)}\n`, "utf-8");
174
+ return { path: filePath, config: merged };
175
+ }
176
+
177
+ export function updateAutonomousConfig(patch: PiTeamsAutonomousConfig): SavedPiTeamsConfig {
178
+ const filePath = configPath();
179
+ let current: Record<string, unknown>;
180
+ try {
181
+ current = readConfigRecord(filePath);
182
+ } catch (error) {
183
+ const message = error instanceof Error ? error.message : String(error);
184
+ throw new Error(`Could not update pi-crew config: ${message}`);
185
+ }
186
+ const currentAutonomous = current.autonomous && typeof current.autonomous === "object" && !Array.isArray(current.autonomous)
187
+ ? current.autonomous as Record<string, unknown>
188
+ : {};
189
+ current.autonomous = { ...currentAutonomous, ...patch };
190
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
191
+ fs.writeFileSync(filePath, `${JSON.stringify(current, null, 2)}\n`, "utf-8");
192
+ return { path: filePath, config: parseConfig(current) };
193
+ }
@@ -0,0 +1,36 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import { listRuns } from "./run-index.ts";
3
+
4
+ export interface AsyncNotifierState {
5
+ seenFinishedRunIds: Set<string>;
6
+ interval?: ReturnType<typeof setInterval>;
7
+ }
8
+
9
+ function isFinished(status: string): boolean {
10
+ return status === "completed" || status === "failed" || status === "cancelled" || status === "blocked";
11
+ }
12
+
13
+ export function startAsyncRunNotifier(ctx: ExtensionContext, state: AsyncNotifierState, intervalMs = 5000): void {
14
+ if (state.interval) clearInterval(state.interval);
15
+ for (const run of listRuns(ctx.cwd)) {
16
+ if (isFinished(run.status)) state.seenFinishedRunIds.add(run.runId);
17
+ }
18
+ state.interval = setInterval(() => {
19
+ try {
20
+ for (const run of listRuns(ctx.cwd).slice(0, 20)) {
21
+ if (!isFinished(run.status) || state.seenFinishedRunIds.has(run.runId)) continue;
22
+ state.seenFinishedRunIds.add(run.runId);
23
+ const level = run.status === "completed" ? "info" : run.status === "cancelled" ? "warning" : "error";
24
+ ctx.ui.notify(`pi-crew run ${run.status}: ${run.runId} (${run.team}/${run.workflow ?? "none"})`, level);
25
+ }
26
+ } catch (error) {
27
+ const message = error instanceof Error ? error.message : String(error);
28
+ console.error(`[pi-crew] async notifier error: ${message}`);
29
+ }
30
+ }, intervalMs);
31
+ }
32
+
33
+ export function stopAsyncRunNotifier(state: AsyncNotifierState): void {
34
+ if (state.interval) clearInterval(state.interval);
35
+ state.interval = undefined;
36
+ }
@@ -0,0 +1,122 @@
1
+ import type { BeforeAgentStartEvent, ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { effectiveAutonomousConfig, loadConfig, type PiTeamsAutonomousConfig } from "../config/config.ts";
3
+ import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
4
+ import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
5
+ import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
6
+
7
+ const DEFAULT_MAGIC_KEYWORDS: Record<string, string[]> = {
8
+ implementation: ["autoteam", "team:", "implementation-team"],
9
+ review: ["review-team", "security review", "code review"],
10
+ fastFix: ["fast-fix", "quick fix"],
11
+ research: ["research-team", "deep research"],
12
+ };
13
+
14
+ function mergeMagicKeywords(configured: Record<string, string[]> | undefined): Record<string, string[]> {
15
+ return { ...DEFAULT_MAGIC_KEYWORDS, ...(configured ?? {}) };
16
+ }
17
+
18
+ export function detectTeamIntent(prompt: string, config: PiTeamsAutonomousConfig = {}): string[] {
19
+ const lower = prompt.toLowerCase();
20
+ const matches: string[] = [];
21
+ for (const [intent, keywords] of Object.entries(mergeMagicKeywords(config.magicKeywords))) {
22
+ if (keywords.some((keyword) => lower.includes(keyword.toLowerCase()))) matches.push(intent);
23
+ }
24
+ return matches;
25
+ }
26
+
27
+ export function buildAutonomousPolicy(prompt: string, config: PiTeamsAutonomousConfig = {}): string {
28
+ const effective = effectiveAutonomousConfig(config);
29
+ const intents = detectTeamIntent(prompt, config);
30
+ const asyncGuidance = effective.preferAsyncForLongTasks
31
+ ? "For long-running team runs, prefer async: true unless the user needs immediate foreground progress."
32
+ : "Use async: true only when the task is clearly long-running or the user asks for background execution.";
33
+ const worktreeGuidance = effective.allowWorktreeSuggestion === false
34
+ ? "Do not suggest worktree mode unless the user explicitly asks for it."
35
+ : "Consider workspaceMode: 'worktree' for parallel or risky code-changing work in clean git repositories.";
36
+ return [
37
+ "# pi-crew Autonomous Delegation Policy",
38
+ "",
39
+ `Autonomy profile: ${effective.profile}.`,
40
+ "You have access to the `team` tool for coordinated multi-agent work. Use it proactively when the task benefits from specialized roles, planning, review, verification, durable artifacts, async execution, or worktree isolation.",
41
+ "",
42
+ "Use `team` automatically when:",
43
+ "- The task spans multiple files, subsystems, or unclear code areas.",
44
+ "- The task requires planning before implementation.",
45
+ "- The task asks for implementation plus tests, review, verification, migration, architecture, security review, or debugging.",
46
+ "- The task would benefit from explorer/planner/executor/reviewer/verifier roles.",
47
+ "",
48
+ "Do not use `team` when:",
49
+ "- The user asks a simple factual question or tiny single-file edit.",
50
+ "- The user explicitly asks you to work directly without delegation.",
51
+ "- The action is destructive (`delete`, `forget`, `prune`, forced cleanup) and the user has not explicitly confirmed it.",
52
+ "",
53
+ "Recommended mappings:",
54
+ "- Complex feature/refactor/migration -> action='run', team='implementation'.",
55
+ "- Small bug fix -> action='run', team='fast-fix'.",
56
+ "- Code/security review -> action='run', team='review'.",
57
+ "- Research or documentation synthesis -> action='run', team='research'.",
58
+ "- Unsure which team/workflow to use -> call the `team` tool with action='recommend' and the user's goal, then follow the suggested plan/run call if appropriate.",
59
+ "- After delegating exploration/research/review, do not duplicate the same search manually. Continue only with non-overlapping work.",
60
+ "- Before claiming delegated work is complete, inspect the run with action='status' or action='summary'.",
61
+ "- Unsure or risky work -> action='plan' first, then run the selected team.",
62
+ "",
63
+ asyncGuidance,
64
+ worktreeGuidance,
65
+ intents.length > 0 ? `Detected pi-crew routing keywords/intents in the user prompt: ${intents.join(", ")}. Consider the matching team workflow if appropriate.` : "No explicit pi-crew magic keyword was detected; decide based on task complexity and risk.",
66
+ ].join("\n");
67
+ }
68
+
69
+ function sourcePriority(source: string): number {
70
+ if (source === "project") return 0;
71
+ if (source === "user") return 1;
72
+ return 2;
73
+ }
74
+
75
+ function capLines(lines: string[], maxChars: number): string[] {
76
+ const kept: string[] = [];
77
+ let used = 0;
78
+ for (const line of lines) {
79
+ const next = used + line.length + 1;
80
+ if (next > maxChars) {
81
+ kept.push("- ...resource guidance truncated to stay within prompt budget");
82
+ break;
83
+ }
84
+ kept.push(line);
85
+ used = next;
86
+ }
87
+ return kept;
88
+ }
89
+
90
+ export function buildResourceRoutingGuidance(cwd: string, maxChars = 5000): string {
91
+ const teams = allTeams(discoverTeams(cwd)).sort((a, b) => sourcePriority(a.source) - sourcePriority(b.source)).slice(0, 12);
92
+ const workflows = allWorkflows(discoverWorkflows(cwd)).sort((a, b) => sourcePriority(a.source) - sourcePriority(b.source)).slice(0, 12);
93
+ const agents = allAgents(discoverAgents(cwd)).sort((a, b) => sourcePriority(a.source) - sourcePriority(b.source)).slice(0, 16);
94
+ const lines = [
95
+ "# pi-crew Available Resources",
96
+ "Use project-scoped resources over user/builtin resources when names overlap.",
97
+ "Teams:",
98
+ ...(teams.length ? teams.map((team) => `- ${team.name} (${team.source}): ${team.description}; defaultWorkflow=${team.defaultWorkflow ?? "default"}; roles=${team.roles.map((role) => `${role.name}->${role.agent}`).join(", ") || "none"}${team.routing?.triggers?.length ? `; triggers=${team.routing.triggers.join(",")}` : ""}${team.routing?.useWhen?.length ? `; useWhen=${team.routing.useWhen.join(";")}` : ""}`) : ["- (none)"]),
99
+ "Workflows:",
100
+ ...(workflows.length ? workflows.map((workflow) => `- ${workflow.name} (${workflow.source}): ${workflow.description}; steps=${workflow.steps.map((step) => `${step.id}:${step.role}`).join(", ") || "none"}`) : ["- (none)"]),
101
+ "Agents:",
102
+ ...(agents.length ? agents.map((agent) => `- ${agent.name} (${agent.source}): ${agent.description}${agent.routing?.triggers?.length ? `; triggers=${agent.routing.triggers.join(",")}` : ""}${agent.routing?.useWhen?.length ? `; useWhen=${agent.routing.useWhen.join(";")}` : ""}${agent.routing?.avoidWhen?.length ? `; avoidWhen=${agent.routing.avoidWhen.join(";")}` : ""}${agent.routing?.cost ? `; cost=${agent.routing.cost}` : ""}${agent.routing?.category ? `; category=${agent.routing.category}` : ""}`) : ["- (none)"]),
103
+ ];
104
+ return capLines(lines, maxChars).join("\n");
105
+ }
106
+
107
+ export function appendAutonomousPolicy(systemPrompt: string, userPrompt: string, config: PiTeamsAutonomousConfig = {}, cwd?: string): string {
108
+ const resourceGuidance = cwd ? `\n\n${buildResourceRoutingGuidance(cwd)}` : "";
109
+ return `${systemPrompt}\n\n${buildAutonomousPolicy(userPrompt, config)}${resourceGuidance}`;
110
+ }
111
+
112
+ export function registerAutonomousPolicy(pi: ExtensionAPI): void {
113
+ pi.on("before_agent_start", (event: BeforeAgentStartEvent) => {
114
+ const options = (event as BeforeAgentStartEvent & { systemPromptOptions?: { cwd?: unknown } }).systemPromptOptions ?? {};
115
+ const cwd = typeof options.cwd === "string" ? options.cwd : undefined;
116
+ const loaded = loadConfig(cwd);
117
+ const autonomous = effectiveAutonomousConfig(loaded.config.autonomous);
118
+ if (!autonomous.enabled) return undefined;
119
+ if (!autonomous.injectPolicy) return undefined;
120
+ return { systemPrompt: appendAutonomousPolicy(event.systemPrompt, event.prompt, autonomous, cwd) };
121
+ });
122
+ }
@@ -0,0 +1,43 @@
1
+ export function piTeamsHelp(): string {
2
+ return [
3
+ "pi-crew commands:",
4
+ "",
5
+ "Core:",
6
+ "- Agent can use the `team` tool autonomously; slash commands are manual controls.",
7
+ "- Tool action `recommend` suggests the best team/workflow for a goal.",
8
+ "- /teams — list teams, workflows, agents, recent runs",
9
+ "- /team-run [--team=name] [--workflow=name] [--async] [--worktree] <goal>",
10
+ "- /team-status <runId>",
11
+ "- /team-summary <runId>",
12
+ "- /team-resume <runId>",
13
+ "- /team-cancel <runId>",
14
+ "",
15
+ "Inspection:",
16
+ "- /team-events <runId>",
17
+ "- /team-artifacts <runId>",
18
+ "- /team-worktrees <runId>",
19
+ "- /team-api <runId> <operation> [taskId=<taskId>] [body=<message>]",
20
+ "- /team-dashboard",
21
+ "- /team-manager",
22
+ "",
23
+ "Maintenance:",
24
+ "- /team-cleanup <runId> [--force]",
25
+ "- /team-forget <runId> --confirm [--force]",
26
+ "- /team-prune --keep=20 --confirm",
27
+ "",
28
+ "Portability:",
29
+ "- /team-export <runId>",
30
+ "- /team-import <path-to-run-export.json> [--user]",
31
+ "- /team-imports",
32
+ "",
33
+ "Diagnostics:",
34
+ "- /team-doctor",
35
+ "- /team-init [--copy-builtins] [--overwrite]",
36
+ "- /team-config [key=value] [--unset=key.path] [--project]",
37
+ "- /team-autonomy [status|on|off|manual|suggested|assisted|aggressive] [--prefer-async] [--no-worktree-suggest]",
38
+ "- /team-validate",
39
+ "- /team-help",
40
+ "",
41
+ "Real child workers are disabled by default. Enable with PI_TEAMS_EXECUTE_WORKERS=1 or config executeWorkers=true.",
42
+ ].join("\n");
43
+ }