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,52 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { projectPiRoot, userPiRoot } from "../utils/paths.ts";
4
+
5
+ export interface ImportedRunIndexEntry {
6
+ runId: string;
7
+ scope: "project" | "user";
8
+ bundlePath: string;
9
+ summaryPath: string;
10
+ importedAt?: string;
11
+ status?: string;
12
+ team?: string;
13
+ workflow?: string;
14
+ goal?: string;
15
+ }
16
+
17
+ function readEntry(root: string, scope: "project" | "user", runId: string): ImportedRunIndexEntry | undefined {
18
+ const bundlePath = path.join(root, runId, "run-export.json");
19
+ const summaryPath = path.join(root, runId, "README.md");
20
+ if (!fs.existsSync(bundlePath)) return undefined;
21
+ try {
22
+ const raw = JSON.parse(fs.readFileSync(bundlePath, "utf-8")) as Record<string, unknown>;
23
+ const manifest = raw.manifest && typeof raw.manifest === "object" && !Array.isArray(raw.manifest) ? raw.manifest as Record<string, unknown> : {};
24
+ return {
25
+ runId,
26
+ scope,
27
+ bundlePath,
28
+ summaryPath,
29
+ importedAt: typeof raw.importedAt === "string" ? raw.importedAt : undefined,
30
+ status: typeof manifest.status === "string" ? manifest.status : undefined,
31
+ team: typeof manifest.team === "string" ? manifest.team : undefined,
32
+ workflow: typeof manifest.workflow === "string" ? manifest.workflow : undefined,
33
+ goal: typeof manifest.goal === "string" ? manifest.goal : undefined,
34
+ };
35
+ } catch {
36
+ return { runId, scope, bundlePath, summaryPath };
37
+ }
38
+ }
39
+
40
+ function collect(root: string, scope: "project" | "user"): ImportedRunIndexEntry[] {
41
+ if (!fs.existsSync(root)) return [];
42
+ return fs.readdirSync(root)
43
+ .map((entry) => readEntry(root, scope, entry))
44
+ .filter((entry): entry is ImportedRunIndexEntry => entry !== undefined);
45
+ }
46
+
47
+ export function listImportedRuns(cwd: string): ImportedRunIndexEntry[] {
48
+ const projectRoot = path.join(projectPiRoot(cwd), "teams", "imports");
49
+ const userRoot = path.join(userPiRoot(), "extensions", "pi-crew", "imports");
50
+ return [...collect(userRoot, "user"), ...collect(projectRoot, "project")]
51
+ .sort((a, b) => (b.importedAt ?? "").localeCompare(a.importedAt ?? ""));
52
+ }
@@ -0,0 +1,335 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { AgentConfig, ResourceSource, RoutingMetadata } from "../agents/agent-config.ts";
4
+ import { serializeAgent } from "../agents/agent-serializer.ts";
5
+ import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
6
+ import type { TeamToolDetails } from "./team-tool.ts";
7
+ import { toolResult, type PiTeamsToolResult } from "./tool-result.ts";
8
+ import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
9
+ import type { TeamConfig, TeamRole } from "../teams/team-config.ts";
10
+ import { serializeTeam } from "../teams/team-serializer.ts";
11
+ import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
12
+ import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
13
+ import { serializeWorkflow } from "../workflows/workflow-serializer.ts";
14
+ import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
15
+ import { projectPiRoot, userPiRoot } from "../utils/paths.ts";
16
+ import { hasOwn, parseConfigObject, requireString, sanitizeName } from "../utils/names.ts";
17
+
18
+ interface ManagementContext {
19
+ cwd: string;
20
+ }
21
+
22
+ type MutableSource = "user" | "project";
23
+
24
+ type MutableResource = AgentConfig | TeamConfig | WorkflowConfig;
25
+
26
+ function result(text: string, status: TeamToolDetails["status"] = "ok", isError = false): PiTeamsToolResult {
27
+ return toolResult(text, { action: "management", status }, isError);
28
+ }
29
+
30
+ function scopeDir(ctx: ManagementContext, resource: "agent" | "team" | "workflow", scope: MutableSource): string {
31
+ const base = scope === "user" ? userPiRoot() : projectPiRoot(ctx.cwd);
32
+ if (resource === "agent") return path.join(base, "agents");
33
+ if (resource === "team") return path.join(base, "teams");
34
+ return path.join(base, "workflows");
35
+ }
36
+
37
+ function extensionFor(resource: "agent" | "team" | "workflow"): string {
38
+ if (resource === "agent") return ".md";
39
+ if (resource === "team") return ".team.md";
40
+ return ".workflow.md";
41
+ }
42
+
43
+ function backupFile(filePath: string): string {
44
+ const backupPath = `${filePath}.bak-${new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14)}`;
45
+ fs.copyFileSync(filePath, backupPath);
46
+ return backupPath;
47
+ }
48
+
49
+ function targetPath(ctx: ManagementContext, resource: "agent" | "team" | "workflow", scope: MutableSource, name: string): string {
50
+ return path.join(scopeDir(ctx, resource, scope), `${name}${extensionFor(resource)}`);
51
+ }
52
+
53
+ function parseStringArray(value: unknown): string[] | undefined {
54
+ if (typeof value === "string") return value.split(",").map((entry) => entry.trim()).filter(Boolean);
55
+ if (Array.isArray(value)) return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim());
56
+ return undefined;
57
+ }
58
+
59
+ function parseRouting(value: Record<string, unknown>, fallback?: RoutingMetadata): RoutingMetadata | undefined {
60
+ const routing = {
61
+ triggers: hasOwn(value, "triggers") ? parseStringArray(value.triggers) : fallback?.triggers,
62
+ useWhen: hasOwn(value, "useWhen") ? parseStringArray(value.useWhen) : fallback?.useWhen,
63
+ avoidWhen: hasOwn(value, "avoidWhen") ? parseStringArray(value.avoidWhen) : fallback?.avoidWhen,
64
+ cost: value.cost === "free" || value.cost === "cheap" || value.cost === "expensive" ? value.cost : fallback?.cost,
65
+ category: hasOwn(value, "category") ? (typeof value.category === "string" && value.category.trim() ? value.category.trim() : undefined) : fallback?.category,
66
+ };
67
+ return routing.triggers || routing.useWhen || routing.avoidWhen || routing.cost || routing.category ? routing : undefined;
68
+ }
69
+
70
+ function parseRoles(value: unknown): { roles?: TeamRole[]; error?: string } {
71
+ if (!Array.isArray(value) || value.length === 0) return { error: "config.roles must be a non-empty array." };
72
+ const roles: TeamRole[] = [];
73
+ for (let i = 0; i < value.length; i++) {
74
+ const item = value[i];
75
+ if (!item || typeof item !== "object" || Array.isArray(item)) return { error: `config.roles[${i}] must be an object.` };
76
+ const obj = item as Record<string, unknown>;
77
+ const name = requireString(obj.name, `config.roles[${i}].name`);
78
+ if (name.error) return { error: name.error };
79
+ const agent = requireString(obj.agent, `config.roles[${i}].agent`);
80
+ if (agent.error) return { error: agent.error };
81
+ roles.push({
82
+ name: sanitizeName(name.value!),
83
+ agent: sanitizeName(agent.value!),
84
+ description: typeof obj.description === "string" ? obj.description.trim() : undefined,
85
+ model: typeof obj.model === "string" ? obj.model.trim() : undefined,
86
+ maxConcurrency: typeof obj.maxConcurrency === "number" && Number.isInteger(obj.maxConcurrency) ? obj.maxConcurrency : undefined,
87
+ });
88
+ }
89
+ return { roles };
90
+ }
91
+
92
+ function parseSteps(value: unknown): { steps?: WorkflowStep[]; error?: string } {
93
+ if (!Array.isArray(value) || value.length === 0) return { error: "config.steps must be a non-empty array." };
94
+ const steps: WorkflowStep[] = [];
95
+ for (let i = 0; i < value.length; i++) {
96
+ const item = value[i];
97
+ if (!item || typeof item !== "object" || Array.isArray(item)) return { error: `config.steps[${i}] must be an object.` };
98
+ const obj = item as Record<string, unknown>;
99
+ const id = requireString(obj.id, `config.steps[${i}].id`);
100
+ if (id.error) return { error: id.error };
101
+ const role = requireString(obj.role, `config.steps[${i}].role`);
102
+ if (role.error) return { error: role.error };
103
+ steps.push({
104
+ id: sanitizeName(id.value!),
105
+ role: sanitizeName(role.value!),
106
+ task: typeof obj.task === "string" ? obj.task : "{goal}",
107
+ dependsOn: parseStringArray(obj.dependsOn),
108
+ parallelGroup: typeof obj.parallelGroup === "string" ? obj.parallelGroup.trim() : undefined,
109
+ output: obj.output === false ? false : typeof obj.output === "string" ? obj.output.trim() : undefined,
110
+ reads: obj.reads === false ? false : parseStringArray(obj.reads),
111
+ model: typeof obj.model === "string" ? obj.model.trim() : undefined,
112
+ skills: obj.skills === false ? false : parseStringArray(obj.skills),
113
+ progress: typeof obj.progress === "boolean" ? obj.progress : undefined,
114
+ worktree: typeof obj.worktree === "boolean" ? obj.worktree : undefined,
115
+ verify: typeof obj.verify === "boolean" ? obj.verify : undefined,
116
+ });
117
+ }
118
+ return { steps };
119
+ }
120
+
121
+ function findResource(ctx: ManagementContext, resource: "agent" | "team" | "workflow", name: string, scope?: string): MutableResource[] {
122
+ const normalized = sanitizeName(name);
123
+ const sourceMatches = (item: { name: string; source: ResourceSource }) => (scope === "user" || scope === "project" ? item.source === scope : item.source !== "builtin") && item.name === normalized;
124
+ if (resource === "agent") return allAgents(discoverAgents(ctx.cwd)).filter(sourceMatches);
125
+ if (resource === "team") return allTeams(discoverTeams(ctx.cwd)).filter(sourceMatches);
126
+ return allWorkflows(discoverWorkflows(ctx.cwd)).filter(sourceMatches);
127
+ }
128
+
129
+ function findReferences(ctx: ManagementContext, resource: "agent" | "team" | "workflow", name: string): string[] {
130
+ const refs: string[] = [];
131
+ if (resource === "agent") {
132
+ for (const team of allTeams(discoverTeams(ctx.cwd))) {
133
+ for (const role of team.roles) {
134
+ if (role.agent === name) refs.push(`team '${team.name}' role '${role.name}'`);
135
+ }
136
+ }
137
+ }
138
+ if (resource === "workflow") {
139
+ for (const team of allTeams(discoverTeams(ctx.cwd))) {
140
+ if (team.defaultWorkflow === name) refs.push(`team '${team.name}' defaultWorkflow`);
141
+ }
142
+ }
143
+ return refs;
144
+ }
145
+
146
+ function updateReferencesForRename(ctx: ManagementContext, resource: "agent" | "team" | "workflow", oldName: string, newName: string, scope: MutableSource, dryRun: boolean): string[] {
147
+ if (oldName === newName) return [];
148
+ if (resource !== "agent" && resource !== "workflow") return [];
149
+ const changed: string[] = [];
150
+ for (const team of allTeams(discoverTeams(ctx.cwd)).filter((candidate) => candidate.source === scope)) {
151
+ let updated = false;
152
+ let nextTeam = team;
153
+ if (resource === "agent") {
154
+ const roles = team.roles.map((role) => role.agent === oldName ? { ...role, agent: newName } : role);
155
+ updated = roles.some((role, index) => role.agent !== team.roles[index]!.agent);
156
+ nextTeam = { ...team, roles };
157
+ }
158
+ if (resource === "workflow" && team.defaultWorkflow === oldName) {
159
+ updated = true;
160
+ nextTeam = { ...team, defaultWorkflow: newName };
161
+ }
162
+ if (!updated) continue;
163
+ changed.push(team.filePath);
164
+ if (!dryRun) {
165
+ backupFile(team.filePath);
166
+ fs.writeFileSync(team.filePath, serializeTeam(nextTeam), "utf-8");
167
+ }
168
+ }
169
+ return changed;
170
+ }
171
+
172
+ function resolveMutable(ctx: ManagementContext, params: TeamToolParamsValue): { resource?: MutableResource; error?: PiTeamsToolResult } {
173
+ if (!params.resource) return { error: result("resource is required for update/delete.", "error", true) };
174
+ const name = params.resource === "agent" ? params.agent : params.resource === "team" ? params.team : params.workflow;
175
+ if (!name) return { error: result(`${params.resource} name is required.`, "error", true) };
176
+ const matches = findResource(ctx, params.resource, name, params.scope);
177
+ if (matches.length === 0) return { error: result(`${params.resource} '${name}' not found in mutable user/project scopes.`, "error", true) };
178
+ if (matches.length > 1) return { error: result(`${params.resource} '${name}' exists in multiple scopes. Specify scope: 'user' or 'project'.`, "error", true) };
179
+ return { resource: matches[0] };
180
+ }
181
+
182
+ export function handleCreate(params: TeamToolParamsValue, ctx: ManagementContext): PiTeamsToolResult {
183
+ if (!params.resource) return result("resource is required for create.", "error", true);
184
+ const parsed = parseConfigObject(params.config);
185
+ if (parsed.error) return result(parsed.error, "error", true);
186
+ const cfg = parsed.value!;
187
+ const nameValue = requireString(cfg.name, "config.name");
188
+ if (nameValue.error) return result(nameValue.error, "error", true);
189
+ const descriptionValue = requireString(cfg.description, "config.description");
190
+ if (descriptionValue.error) return result(descriptionValue.error, "error", true);
191
+ const name = sanitizeName(nameValue.value!);
192
+ if (!name) return result("config.name is invalid after sanitization.", "error", true);
193
+ const scope = cfg.scope === "project" ? "project" : "user";
194
+ const filePath = targetPath(ctx, params.resource, scope, name);
195
+ if (fs.existsSync(filePath)) return result(`File already exists: ${filePath}`, "error", true);
196
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
197
+
198
+ let content: string;
199
+ if (params.resource === "agent") {
200
+ const agent: AgentConfig = {
201
+ name,
202
+ description: descriptionValue.value!,
203
+ source: scope,
204
+ filePath,
205
+ systemPrompt: typeof cfg.systemPrompt === "string" ? cfg.systemPrompt : "",
206
+ model: typeof cfg.model === "string" ? cfg.model : undefined,
207
+ fallbackModels: parseStringArray(cfg.fallbackModels),
208
+ thinking: typeof cfg.thinking === "string" ? cfg.thinking : undefined,
209
+ tools: parseStringArray(cfg.tools),
210
+ extensions: hasOwn(cfg, "extensions") ? parseStringArray(cfg.extensions) ?? [] : undefined,
211
+ skills: parseStringArray(cfg.skills),
212
+ systemPromptMode: cfg.systemPromptMode === "append" ? "append" : "replace",
213
+ inheritProjectContext: cfg.inheritProjectContext === true,
214
+ inheritSkills: cfg.inheritSkills === true,
215
+ routing: parseRouting(cfg),
216
+ };
217
+ content = serializeAgent(agent);
218
+ } else if (params.resource === "team") {
219
+ const parsedRoles = parseRoles(cfg.roles);
220
+ if (parsedRoles.error) return result(parsedRoles.error, "error", true);
221
+ content = serializeTeam({
222
+ name,
223
+ description: descriptionValue.value!,
224
+ source: scope,
225
+ filePath,
226
+ roles: parsedRoles.roles!,
227
+ defaultWorkflow: typeof cfg.defaultWorkflow === "string" ? sanitizeName(cfg.defaultWorkflow) : undefined,
228
+ workspaceMode: cfg.workspaceMode === "worktree" ? "worktree" : "single",
229
+ maxConcurrency: typeof cfg.maxConcurrency === "number" && Number.isInteger(cfg.maxConcurrency) ? cfg.maxConcurrency : undefined,
230
+ routing: parseRouting(cfg),
231
+ });
232
+ } else {
233
+ const parsedSteps = parseSteps(cfg.steps);
234
+ if (parsedSteps.error) return result(parsedSteps.error, "error", true);
235
+ content = serializeWorkflow({ name, description: descriptionValue.value!, source: scope, filePath, steps: parsedSteps.steps! });
236
+ }
237
+
238
+ if (params.dryRun) return result(`[dry-run] Would create ${params.resource} '${name}' at ${filePath}:\n\n${content}`);
239
+ fs.writeFileSync(filePath, content, "utf-8");
240
+ return result(`Created ${params.resource} '${name}' at ${filePath}.`);
241
+ }
242
+
243
+ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext): PiTeamsToolResult {
244
+ const resolved = resolveMutable(ctx, params);
245
+ if (resolved.error) return resolved.error;
246
+ const parsed = parseConfigObject(params.config);
247
+ if (parsed.error) return result(parsed.error, "error", true);
248
+ const cfg = parsed.value!;
249
+ const current = resolved.resource!;
250
+ const nextName = hasOwn(cfg, "name") ? sanitizeName(String(cfg.name ?? "")) : current.name;
251
+ if (!nextName) return result("config.name is invalid after sanitization.", "error", true);
252
+ const source = current.source === "project" ? "project" : "user";
253
+ const nextPath = targetPath(ctx, params.resource!, source, nextName);
254
+ if (nextPath !== current.filePath && fs.existsSync(nextPath)) return result(`Target file already exists: ${nextPath}`, "error", true);
255
+
256
+ let content: string;
257
+ if (params.resource === "agent") {
258
+ const agent = current as AgentConfig;
259
+ content = serializeAgent({
260
+ ...agent,
261
+ name: nextName,
262
+ filePath: nextPath,
263
+ description: typeof cfg.description === "string" && cfg.description.trim() ? cfg.description.trim() : agent.description,
264
+ systemPrompt: typeof cfg.systemPrompt === "string" ? cfg.systemPrompt : agent.systemPrompt,
265
+ model: hasOwn(cfg, "model") ? (typeof cfg.model === "string" && cfg.model.trim() ? cfg.model.trim() : undefined) : agent.model,
266
+ fallbackModels: hasOwn(cfg, "fallbackModels") ? parseStringArray(cfg.fallbackModels) : agent.fallbackModels,
267
+ thinking: hasOwn(cfg, "thinking") ? (typeof cfg.thinking === "string" && cfg.thinking.trim() ? cfg.thinking.trim() : undefined) : agent.thinking,
268
+ tools: hasOwn(cfg, "tools") ? parseStringArray(cfg.tools) : agent.tools,
269
+ extensions: hasOwn(cfg, "extensions") ? parseStringArray(cfg.extensions) ?? [] : agent.extensions,
270
+ skills: hasOwn(cfg, "skills") ? parseStringArray(cfg.skills) : agent.skills,
271
+ systemPromptMode: cfg.systemPromptMode === "append" ? "append" : cfg.systemPromptMode === "replace" ? "replace" : agent.systemPromptMode,
272
+ inheritProjectContext: typeof cfg.inheritProjectContext === "boolean" ? cfg.inheritProjectContext : agent.inheritProjectContext,
273
+ inheritSkills: typeof cfg.inheritSkills === "boolean" ? cfg.inheritSkills : agent.inheritSkills,
274
+ routing: parseRouting(cfg, agent.routing),
275
+ });
276
+ } else if (params.resource === "team") {
277
+ const team = current as TeamConfig;
278
+ let roles = team.roles;
279
+ if (hasOwn(cfg, "roles")) {
280
+ const parsedRoles = parseRoles(cfg.roles);
281
+ if (parsedRoles.error) return result(parsedRoles.error, "error", true);
282
+ roles = parsedRoles.roles!;
283
+ }
284
+ content = serializeTeam({
285
+ ...team,
286
+ name: nextName,
287
+ filePath: nextPath,
288
+ description: typeof cfg.description === "string" && cfg.description.trim() ? cfg.description.trim() : team.description,
289
+ roles,
290
+ defaultWorkflow: hasOwn(cfg, "defaultWorkflow") ? (typeof cfg.defaultWorkflow === "string" ? sanitizeName(cfg.defaultWorkflow) : undefined) : team.defaultWorkflow,
291
+ workspaceMode: cfg.workspaceMode === "worktree" ? "worktree" : cfg.workspaceMode === "single" ? "single" : team.workspaceMode,
292
+ maxConcurrency: typeof cfg.maxConcurrency === "number" && Number.isInteger(cfg.maxConcurrency) ? cfg.maxConcurrency : team.maxConcurrency,
293
+ routing: parseRouting(cfg, team.routing),
294
+ });
295
+ } else {
296
+ const workflow = current as WorkflowConfig;
297
+ let steps = workflow.steps;
298
+ if (hasOwn(cfg, "steps")) {
299
+ const parsedSteps = parseSteps(cfg.steps);
300
+ if (parsedSteps.error) return result(parsedSteps.error, "error", true);
301
+ steps = parsedSteps.steps!;
302
+ }
303
+ content = serializeWorkflow({
304
+ ...workflow,
305
+ name: nextName,
306
+ filePath: nextPath,
307
+ description: typeof cfg.description === "string" && cfg.description.trim() ? cfg.description.trim() : workflow.description,
308
+ steps,
309
+ });
310
+ }
311
+
312
+ const referenceUpdates = params.updateReferences ? updateReferencesForRename(ctx, params.resource!, current.name, nextName, source, true) : [];
313
+ if (params.dryRun) {
314
+ return result([`[dry-run] Would update ${params.resource} at ${current.filePath}:`, "", content, ...(referenceUpdates.length ? ["", "Would update references in:", ...referenceUpdates.map((filePath) => `- ${filePath}`)] : [])].join("\n"));
315
+ }
316
+ const backupPath = backupFile(current.filePath);
317
+ if (nextPath !== current.filePath) fs.renameSync(current.filePath, nextPath);
318
+ fs.writeFileSync(nextPath, content, "utf-8");
319
+ const updatedRefs = params.updateReferences ? updateReferencesForRename(ctx, params.resource!, current.name, nextName, source, false) : [];
320
+ return result([`Updated ${params.resource} at ${nextPath}. Backup: ${backupPath}.`, ...(updatedRefs.length ? ["Updated references:", ...updatedRefs.map((filePath) => `- ${filePath}`)] : [])].join("\n"));
321
+ }
322
+
323
+ export function handleDelete(params: TeamToolParamsValue, ctx: ManagementContext): PiTeamsToolResult {
324
+ if (!params.confirm) return result("delete requires confirm: true.", "error", true);
325
+ const resolved = resolveMutable(ctx, params);
326
+ if (resolved.error) return resolved.error;
327
+ const refs = findReferences(ctx, params.resource!, resolved.resource!.name);
328
+ if (refs.length > 0 && !params.force) {
329
+ return result(`${params.resource} '${resolved.resource!.name}' is still referenced. Use force: true to delete anyway.\n${refs.map((ref) => `- ${ref}`).join("\n")}`, "error", true);
330
+ }
331
+ if (params.dryRun) return result(`[dry-run] Would delete ${params.resource} at ${resolved.resource!.filePath}.${refs.length ? `\nReferences:\n${refs.map((ref) => `- ${ref}`).join("\n")}` : ""}`);
332
+ const backupPath = backupFile(resolved.resource!.filePath);
333
+ fs.unlinkSync(resolved.resource!.filePath);
334
+ return result(`Deleted ${params.resource} at ${resolved.resource!.filePath}. Backup: ${backupPath}.`);
335
+ }
@@ -0,0 +1,74 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { packageRoot } from "../utils/paths.ts";
4
+
5
+ export interface ProjectInitOptions {
6
+ copyBuiltins?: boolean;
7
+ overwrite?: boolean;
8
+ }
9
+
10
+ export interface ProjectInitResult {
11
+ createdDirs: string[];
12
+ copiedFiles: string[];
13
+ skippedFiles: string[];
14
+ gitignorePath: string;
15
+ gitignoreUpdated: boolean;
16
+ }
17
+
18
+ function ensureDir(dir: string, createdDirs: string[]): void {
19
+ if (!fs.existsSync(dir)) {
20
+ fs.mkdirSync(dir, { recursive: true });
21
+ createdDirs.push(dir);
22
+ } else {
23
+ fs.mkdirSync(dir, { recursive: true });
24
+ }
25
+ }
26
+
27
+ function copyBuiltinDir(kind: "agents" | "teams" | "workflows", targetDir: string, overwrite: boolean, copiedFiles: string[], skippedFiles: string[]): void {
28
+ const sourceDir = path.join(packageRoot(), kind);
29
+ if (!fs.existsSync(sourceDir)) return;
30
+ for (const entry of fs.readdirSync(sourceDir)) {
31
+ const source = path.join(sourceDir, entry);
32
+ const target = path.join(targetDir, entry);
33
+ if (!fs.statSync(source).isFile()) continue;
34
+ if (fs.existsSync(target) && !overwrite) {
35
+ skippedFiles.push(target);
36
+ continue;
37
+ }
38
+ fs.copyFileSync(source, target);
39
+ copiedFiles.push(target);
40
+ }
41
+ }
42
+
43
+ export function initializeProject(cwd: string, options: ProjectInitOptions = {}): ProjectInitResult {
44
+ const createdDirs: string[] = [];
45
+ const copiedFiles: string[] = [];
46
+ const skippedFiles: string[] = [];
47
+ const piRoot = path.join(cwd, ".pi");
48
+ const agentsDir = path.join(piRoot, "agents");
49
+ const teamsDir = path.join(piRoot, "teams");
50
+ const workflowsDir = path.join(piRoot, "workflows");
51
+ ensureDir(agentsDir, createdDirs);
52
+ ensureDir(teamsDir, createdDirs);
53
+ ensureDir(workflowsDir, createdDirs);
54
+ ensureDir(path.join(piRoot, "teams", "imports"), createdDirs);
55
+
56
+ if (options.copyBuiltins) {
57
+ copyBuiltinDir("agents", agentsDir, options.overwrite === true, copiedFiles, skippedFiles);
58
+ copyBuiltinDir("teams", teamsDir, options.overwrite === true, copiedFiles, skippedFiles);
59
+ copyBuiltinDir("workflows", workflowsDir, options.overwrite === true, copiedFiles, skippedFiles);
60
+ }
61
+
62
+ const gitignorePath = path.join(cwd, ".gitignore");
63
+ const desired = [".pi/teams/state/", ".pi/teams/artifacts/", ".pi/teams/worktrees/", ".pi/teams/imports/"];
64
+ const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf-8") : "";
65
+ const missing = desired.filter((entry) => !existing.split(/\r?\n/).includes(entry));
66
+ let gitignoreUpdated = false;
67
+ if (missing.length > 0) {
68
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
69
+ fs.writeFileSync(gitignorePath, `${existing}${prefix}\n# pi-crew runtime state\n${missing.join("\n")}\n`, "utf-8");
70
+ gitignoreUpdated = true;
71
+ }
72
+
73
+ return { createdDirs, copiedFiles, skippedFiles, gitignorePath, gitignoreUpdated };
74
+ }