pi-crew 0.1.30 → 0.1.32

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 (159) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +80 -27
  3. package/agents/analyst.md +11 -11
  4. package/agents/critic.md +11 -11
  5. package/agents/executor.md +11 -11
  6. package/agents/explorer.md +11 -11
  7. package/agents/planner.md +11 -11
  8. package/agents/reviewer.md +11 -11
  9. package/agents/security-reviewer.md +11 -11
  10. package/agents/test-engineer.md +11 -11
  11. package/agents/verifier.md +11 -11
  12. package/agents/writer.md +11 -11
  13. package/docs/architecture.md +173 -164
  14. package/docs/refactor-tasks-phase3.md +394 -394
  15. package/docs/refactor-tasks-phase4.md +564 -564
  16. package/docs/refactor-tasks-phase5.md +402 -402
  17. package/docs/refactor-tasks-phase6.md +662 -662
  18. package/docs/research-extension-examples.md +297 -0
  19. package/docs/research-extension-system.md +324 -0
  20. package/docs/research-optimization-plan.md +548 -0
  21. package/docs/research-pi-coding-agent.md +357 -0
  22. package/docs/resource-formats.md +4 -3
  23. package/docs/runtime-flow.md +148 -148
  24. package/docs/source-runtime-refactor-map.md +83 -83
  25. package/docs/usage.md +3 -3
  26. package/index.ts +6 -6
  27. package/package.json +1 -1
  28. package/schema.json +53 -1
  29. package/skills/git-master/SKILL.md +24 -19
  30. package/skills/read-only-explorer/SKILL.md +26 -21
  31. package/skills/safe-bash/SKILL.md +21 -16
  32. package/skills/task-packet/SKILL.md +28 -23
  33. package/skills/verify-evidence/SKILL.md +27 -22
  34. package/src/agents/agent-serializer.ts +34 -34
  35. package/src/agents/discover-agents.ts +102 -102
  36. package/src/config/config.ts +14 -1
  37. package/src/config/defaults.ts +3 -2
  38. package/src/extension/cross-extension-rpc.ts +82 -82
  39. package/src/extension/import-index.ts +4 -3
  40. package/src/extension/management.ts +2 -2
  41. package/src/extension/project-init.ts +9 -7
  42. package/src/extension/register.ts +63 -0
  43. package/src/extension/registration/artifact-cleanup.ts +15 -14
  44. package/src/extension/registration/commands.ts +208 -208
  45. package/src/extension/registration/compaction-guard.ts +125 -0
  46. package/src/extension/registration/subagent-tools.ts +27 -8
  47. package/src/extension/registration/team-tool.ts +61 -44
  48. package/src/extension/result-watcher.ts +98 -98
  49. package/src/extension/run-import.ts +4 -4
  50. package/src/extension/run-index.ts +14 -14
  51. package/src/extension/run-maintenance.ts +24 -24
  52. package/src/extension/team-tool/api.ts +3 -0
  53. package/src/extension/team-tool/cancel.ts +31 -31
  54. package/src/extension/team-tool/doctor.ts +179 -178
  55. package/src/extension/team-tool/inspect.ts +41 -41
  56. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  57. package/src/extension/team-tool/plan.ts +19 -19
  58. package/src/extension/team-tool/status.ts +73 -73
  59. package/src/prompt/prompt-runtime.ts +68 -68
  60. package/src/runtime/agent-control.ts +64 -64
  61. package/src/runtime/agent-memory.ts +72 -72
  62. package/src/runtime/agent-observability.ts +113 -113
  63. package/src/runtime/async-marker.ts +26 -26
  64. package/src/runtime/background-runner.ts +53 -53
  65. package/src/runtime/crew-agent-runtime.ts +58 -58
  66. package/src/runtime/direct-run.ts +35 -35
  67. package/src/runtime/foreground-control.ts +82 -82
  68. package/src/runtime/green-contract.ts +46 -46
  69. package/src/runtime/group-join.ts +88 -88
  70. package/src/runtime/live-agent-control.ts +78 -78
  71. package/src/runtime/live-agent-manager.ts +85 -85
  72. package/src/runtime/live-control-realtime.ts +36 -36
  73. package/src/runtime/live-session-runtime.ts +299 -299
  74. package/src/runtime/manifest-cache.ts +212 -214
  75. package/src/runtime/model-fallback.ts +261 -261
  76. package/src/runtime/parallel-research.ts +44 -44
  77. package/src/runtime/parallel-utils.ts +99 -99
  78. package/src/runtime/pi-json-output.ts +111 -111
  79. package/src/runtime/pi-spawn.ts +96 -96
  80. package/src/runtime/policy-engine.ts +78 -78
  81. package/src/runtime/post-exit-stdio-guard.ts +86 -86
  82. package/src/runtime/process-status.ts +56 -56
  83. package/src/runtime/progress-event-coalescer.ts +43 -43
  84. package/src/runtime/recovery-recipes.ts +74 -74
  85. package/src/runtime/role-permission.ts +39 -39
  86. package/src/runtime/session-usage.ts +79 -79
  87. package/src/runtime/sidechain-output.ts +28 -28
  88. package/src/runtime/subagent-manager.ts +29 -2
  89. package/src/runtime/task-display.ts +38 -38
  90. package/src/runtime/task-output-context.ts +106 -106
  91. package/src/runtime/task-packet.ts +84 -84
  92. package/src/runtime/task-runner/live-executor.ts +98 -98
  93. package/src/runtime/task-runner/progress.ts +111 -111
  94. package/src/runtime/task-runner/prompt-builder.ts +72 -72
  95. package/src/runtime/task-runner/result-utils.ts +14 -14
  96. package/src/runtime/task-runner/state-helpers.ts +22 -22
  97. package/src/runtime/worker-heartbeat.ts +21 -21
  98. package/src/runtime/worker-startup.ts +57 -57
  99. package/src/schema/config-schema.ts +12 -0
  100. package/src/schema/team-tool-schema.ts +100 -100
  101. package/src/state/artifact-store.ts +108 -108
  102. package/src/state/contracts.ts +105 -105
  103. package/src/state/jsonl-writer.ts +77 -77
  104. package/src/state/state-store.ts +8 -9
  105. package/src/state/task-claims.ts +42 -42
  106. package/src/state/types.ts +180 -180
  107. package/src/state/usage.ts +29 -29
  108. package/src/subagents/async-entry.ts +1 -1
  109. package/src/subagents/index.ts +3 -3
  110. package/src/subagents/live/control.ts +1 -1
  111. package/src/subagents/live/manager.ts +1 -1
  112. package/src/subagents/live/realtime.ts +1 -1
  113. package/src/subagents/live/session-runtime.ts +1 -1
  114. package/src/subagents/manager.ts +1 -1
  115. package/src/subagents/spawn.ts +1 -1
  116. package/src/teams/discover-teams.ts +2 -2
  117. package/src/teams/team-serializer.ts +36 -36
  118. package/src/types/diff.d.ts +18 -0
  119. package/src/ui/crew-footer.ts +101 -101
  120. package/src/ui/crew-select-list.ts +111 -111
  121. package/src/ui/crew-widget.ts +285 -285
  122. package/src/ui/dynamic-border.ts +25 -25
  123. package/src/ui/layout-primitives.ts +106 -106
  124. package/src/ui/loaders.ts +158 -158
  125. package/src/ui/mascot.ts +441 -441
  126. package/src/ui/powerbar-publisher.ts +94 -94
  127. package/src/ui/render-diff.ts +119 -119
  128. package/src/ui/run-dashboard.ts +372 -372
  129. package/src/ui/status-colors.ts +54 -54
  130. package/src/ui/syntax-highlight.ts +116 -116
  131. package/src/ui/transcript-viewer.ts +302 -302
  132. package/src/utils/completion-dedupe.ts +63 -63
  133. package/src/utils/file-coalescer.ts +84 -84
  134. package/src/utils/frontmatter.ts +36 -36
  135. package/src/utils/fs-watch.ts +31 -31
  136. package/src/utils/git.ts +262 -262
  137. package/src/utils/ids.ts +12 -12
  138. package/src/utils/names.ts +26 -26
  139. package/src/utils/paths.ts +34 -7
  140. package/src/utils/sleep.ts +32 -32
  141. package/src/utils/timings.ts +31 -31
  142. package/src/utils/visual.ts +159 -159
  143. package/src/workflows/discover-workflows.ts +2 -2
  144. package/src/workflows/validate-workflow.ts +40 -40
  145. package/src/worktree/branch-freshness.ts +45 -45
  146. package/src/worktree/cleanup.ts +71 -69
  147. package/src/worktree/worktree-manager.ts +3 -1
  148. package/teams/default.team.md +12 -12
  149. package/teams/fast-fix.team.md +11 -11
  150. package/teams/implementation.team.md +18 -18
  151. package/teams/parallel-research.team.md +14 -14
  152. package/teams/research.team.md +11 -11
  153. package/teams/review.team.md +12 -12
  154. package/workflows/default.workflow.md +29 -29
  155. package/workflows/fast-fix.workflow.md +22 -22
  156. package/workflows/implementation.workflow.md +38 -38
  157. package/workflows/parallel-research.workflow.md +46 -46
  158. package/workflows/research.workflow.md +22 -22
  159. package/workflows/review.workflow.md +30 -30
@@ -1,102 +1,102 @@
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 { loadConfig } from "../config/config.ts";
5
- import { parseCsv, parseFrontmatter } from "../utils/frontmatter.ts";
6
- import { packageRoot, projectPiRoot, userPiRoot } from "../utils/paths.ts";
7
-
8
- export interface AgentDiscoveryResult {
9
- builtin: AgentConfig[];
10
- user: AgentConfig[];
11
- project: AgentConfig[];
12
- }
13
-
14
- function parseCost(value: string | undefined): "free" | "cheap" | "expensive" | undefined {
15
- return value === "free" || value === "cheap" || value === "expensive" ? value : undefined;
16
- }
17
-
18
- function parseMemory(value: string | undefined): "user" | "project" | "local" | undefined {
19
- return value === "user" || value === "project" || value === "local" ? value : undefined;
20
- }
21
-
22
- function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig | undefined {
23
- try {
24
- const content = fs.readFileSync(filePath, "utf-8");
25
- const { frontmatter, body } = parseFrontmatter(content);
26
- const name = frontmatter.name?.trim() || path.basename(filePath, path.extname(filePath));
27
- const description = frontmatter.description?.trim() || "No description provided.";
28
- const triggers = parseCsv(frontmatter.triggers ?? frontmatter.trigger);
29
- const useWhen = parseCsv(frontmatter.useWhen);
30
- const avoidWhen = parseCsv(frontmatter.avoidWhen);
31
- const cost = parseCost(frontmatter.cost);
32
- const category = frontmatter.category?.trim() || undefined;
33
- return {
34
- name,
35
- description,
36
- source,
37
- filePath,
38
- systemPrompt: body.trim(),
39
- model: frontmatter.model || undefined,
40
- fallbackModels: parseCsv(frontmatter.fallbackModels),
41
- thinking: frontmatter.thinking || undefined,
42
- tools: parseCsv(frontmatter.tools),
43
- extensions: frontmatter.extensions === "" ? [] : parseCsv(frontmatter.extensions),
44
- skills: parseCsv(frontmatter.skills ?? frontmatter.skill),
45
- systemPromptMode: frontmatter.systemPromptMode === "append" ? "append" : "replace",
46
- inheritProjectContext: frontmatter.inheritProjectContext === "true",
47
- inheritSkills: frontmatter.inheritSkills === "true",
48
- memory: parseMemory(frontmatter.memory),
49
- disabled: frontmatter.disabled === "true" || frontmatter.enabled === "false",
50
- routing: triggers || useWhen || avoidWhen || cost || category ? { triggers, useWhen, avoidWhen, cost, category } : undefined,
51
- };
52
- } catch {
53
- return undefined;
54
- }
55
- }
56
-
57
- function readAgentDir(dir: string, source: ResourceSource): AgentConfig[] {
58
- if (!fs.existsSync(dir)) return [];
59
- return fs.readdirSync(dir)
60
- .filter((entry) => entry.endsWith(".md") && !entry.endsWith(".team.md") && !entry.endsWith(".workflow.md"))
61
- .map((entry) => parseAgentFile(path.join(dir, entry), source))
62
- .filter((agent): agent is AgentConfig => agent !== undefined)
63
- .sort((a, b) => a.name.localeCompare(b.name));
64
- }
65
-
66
- function applyAgentOverrides(agents: AgentConfig[], cwd: string): AgentConfig[] {
67
- const loaded = loadConfig(cwd);
68
- const config = loaded.config.agents;
69
- const overrides = config?.overrides ?? {};
70
- return agents
71
- .filter((agent) => !(config?.disableBuiltins && agent.source === "builtin"))
72
- .map((agent) => {
73
- const overrideEntry = Object.entries(overrides).find(([name]) => name.toLowerCase() === agent.name.toLowerCase());
74
- if (!overrideEntry) return agent;
75
- const [, override] = overrideEntry;
76
- return {
77
- ...agent,
78
- disabled: override.disabled ?? agent.disabled,
79
- model: override.model === false ? undefined : override.model ?? agent.model,
80
- fallbackModels: override.fallbackModels === false ? undefined : override.fallbackModels ?? agent.fallbackModels,
81
- thinking: override.thinking === false ? undefined : override.thinking ?? agent.thinking,
82
- tools: override.tools === false ? undefined : override.tools ?? agent.tools,
83
- override: { source: "config", path: loaded.path },
84
- };
85
- });
86
- }
87
-
88
- export function discoverAgents(cwd: string): AgentDiscoveryResult {
89
- return {
90
- builtin: applyAgentOverrides(readAgentDir(path.join(packageRoot(), "agents"), "builtin"), cwd),
91
- user: applyAgentOverrides(readAgentDir(path.join(userPiRoot(), "agents"), "user"), cwd),
92
- project: applyAgentOverrides(readAgentDir(path.join(projectPiRoot(cwd), "agents"), "project"), cwd),
93
- };
94
- }
95
-
96
- export function allAgents(discovery: AgentDiscoveryResult): AgentConfig[] {
97
- const byName = new Map<string, AgentConfig>();
98
- for (const agent of [...discovery.builtin, ...discovery.user, ...discovery.project]) {
99
- byName.set(agent.name.toLowerCase(), agent);
100
- }
101
- return [...byName.values()].filter((agent) => !agent.disabled).sort((a, b) => a.name.localeCompare(b.name));
102
- }
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 { loadConfig } from "../config/config.ts";
5
+ import { parseCsv, parseFrontmatter } from "../utils/frontmatter.ts";
6
+ import { packageRoot, projectCrewRoot, userPiRoot } from "../utils/paths.ts";
7
+
8
+ export interface AgentDiscoveryResult {
9
+ builtin: AgentConfig[];
10
+ user: AgentConfig[];
11
+ project: AgentConfig[];
12
+ }
13
+
14
+ function parseCost(value: string | undefined): "free" | "cheap" | "expensive" | undefined {
15
+ return value === "free" || value === "cheap" || value === "expensive" ? value : undefined;
16
+ }
17
+
18
+ function parseMemory(value: string | undefined): "user" | "project" | "local" | undefined {
19
+ return value === "user" || value === "project" || value === "local" ? value : undefined;
20
+ }
21
+
22
+ function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig | undefined {
23
+ try {
24
+ const content = fs.readFileSync(filePath, "utf-8");
25
+ const { frontmatter, body } = parseFrontmatter(content);
26
+ const name = frontmatter.name?.trim() || path.basename(filePath, path.extname(filePath));
27
+ const description = frontmatter.description?.trim() || "No description provided.";
28
+ const triggers = parseCsv(frontmatter.triggers ?? frontmatter.trigger);
29
+ const useWhen = parseCsv(frontmatter.useWhen);
30
+ const avoidWhen = parseCsv(frontmatter.avoidWhen);
31
+ const cost = parseCost(frontmatter.cost);
32
+ const category = frontmatter.category?.trim() || undefined;
33
+ return {
34
+ name,
35
+ description,
36
+ source,
37
+ filePath,
38
+ systemPrompt: body.trim(),
39
+ model: frontmatter.model || undefined,
40
+ fallbackModels: parseCsv(frontmatter.fallbackModels),
41
+ thinking: frontmatter.thinking || undefined,
42
+ tools: parseCsv(frontmatter.tools),
43
+ extensions: frontmatter.extensions === "" ? [] : parseCsv(frontmatter.extensions),
44
+ skills: parseCsv(frontmatter.skills ?? frontmatter.skill),
45
+ systemPromptMode: frontmatter.systemPromptMode === "append" ? "append" : "replace",
46
+ inheritProjectContext: frontmatter.inheritProjectContext === "true",
47
+ inheritSkills: frontmatter.inheritSkills === "true",
48
+ memory: parseMemory(frontmatter.memory),
49
+ disabled: frontmatter.disabled === "true" || frontmatter.enabled === "false",
50
+ routing: triggers || useWhen || avoidWhen || cost || category ? { triggers, useWhen, avoidWhen, cost, category } : undefined,
51
+ };
52
+ } catch {
53
+ return undefined;
54
+ }
55
+ }
56
+
57
+ function readAgentDir(dir: string, source: ResourceSource): AgentConfig[] {
58
+ if (!fs.existsSync(dir)) return [];
59
+ return fs.readdirSync(dir)
60
+ .filter((entry) => entry.endsWith(".md") && !entry.endsWith(".team.md") && !entry.endsWith(".workflow.md"))
61
+ .map((entry) => parseAgentFile(path.join(dir, entry), source))
62
+ .filter((agent): agent is AgentConfig => agent !== undefined)
63
+ .sort((a, b) => a.name.localeCompare(b.name));
64
+ }
65
+
66
+ function applyAgentOverrides(agents: AgentConfig[], cwd: string): AgentConfig[] {
67
+ const loaded = loadConfig(cwd);
68
+ const config = loaded.config.agents;
69
+ const overrides = config?.overrides ?? {};
70
+ return agents
71
+ .filter((agent) => !(config?.disableBuiltins && agent.source === "builtin"))
72
+ .map((agent) => {
73
+ const overrideEntry = Object.entries(overrides).find(([name]) => name.toLowerCase() === agent.name.toLowerCase());
74
+ if (!overrideEntry) return agent;
75
+ const [, override] = overrideEntry;
76
+ return {
77
+ ...agent,
78
+ disabled: override.disabled ?? agent.disabled,
79
+ model: override.model === false ? undefined : override.model ?? agent.model,
80
+ fallbackModels: override.fallbackModels === false ? undefined : override.fallbackModels ?? agent.fallbackModels,
81
+ thinking: override.thinking === false ? undefined : override.thinking ?? agent.thinking,
82
+ tools: override.tools === false ? undefined : override.tools ?? agent.tools,
83
+ override: { source: "config", path: loaded.path },
84
+ };
85
+ });
86
+ }
87
+
88
+ export function discoverAgents(cwd: string): AgentDiscoveryResult {
89
+ return {
90
+ builtin: applyAgentOverrides(readAgentDir(path.join(packageRoot(), "agents"), "builtin"), cwd),
91
+ user: applyAgentOverrides(readAgentDir(path.join(userPiRoot(), "agents"), "user"), cwd),
92
+ project: applyAgentOverrides(readAgentDir(path.join(projectCrewRoot(cwd), "agents"), "project"), cwd),
93
+ };
94
+ }
95
+
96
+ export function allAgents(discovery: AgentDiscoveryResult): AgentConfig[] {
97
+ const byName = new Map<string, AgentConfig>();
98
+ for (const agent of [...discovery.builtin, ...discovery.user, ...discovery.project]) {
99
+ byName.set(agent.name.toLowerCase(), agent);
100
+ }
101
+ return [...byName.values()].filter((agent) => !agent.disabled).sort((a, b) => a.name.localeCompare(b.name));
102
+ }
@@ -4,6 +4,7 @@ import * as fs from "node:fs";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { PiTeamsAutonomyProfileSchema, PiTeamsConfigSchema } from "../schema/config-schema.ts";
7
+ import { projectCrewRoot } from "../utils/paths.ts";
7
8
 
8
9
  export type PiTeamsAutonomyProfile = "manual" | "suggested" | "assisted" | "aggressive";
9
10
 
@@ -80,6 +81,16 @@ export interface CrewAgentsConfig {
80
81
  overrides?: Record<string, AgentOverrideConfig>;
81
82
  }
82
83
 
84
+ export interface CrewToolsConfig {
85
+ enableClaudeStyleAliases?: boolean;
86
+ enableSteer?: boolean;
87
+ terminateOnForeground?: boolean;
88
+ }
89
+
90
+ export interface CrewTelemetryConfig {
91
+ enabled?: boolean;
92
+ }
93
+
83
94
  export interface PiTeamsConfig {
84
95
  asyncByDefault?: boolean;
85
96
  executeWorkers?: boolean;
@@ -91,6 +102,8 @@ export interface PiTeamsConfig {
91
102
  control?: CrewControlConfig;
92
103
  worktree?: CrewWorktreeConfig;
93
104
  agents?: CrewAgentsConfig;
105
+ tools?: CrewToolsConfig;
106
+ telemetry?: CrewTelemetryConfig;
94
107
  ui?: CrewUiConfig;
95
108
  }
96
109
 
@@ -124,7 +137,7 @@ export function configPath(): string {
124
137
  }
125
138
 
126
139
  export function projectConfigPath(cwd: string): string {
127
- return path.join(cwd, ".pi", "teams", "config.json");
140
+ return path.join(projectCrewRoot(cwd), "config.json");
128
141
  }
129
142
 
130
143
  function withoutUndefined<T extends Record<string, unknown>>(value: T): Partial<T> {
@@ -38,10 +38,11 @@ export const DEFAULT_ARTIFACT_CLEANUP = {
38
38
 
39
39
  export const DEFAULT_PATHS = {
40
40
  state: {
41
- projectBase: "teams",
42
- userBase: "runs",
43
41
  runsSubdir: "state/runs",
44
42
  artifactsSubdir: "artifacts",
43
+ subagentsSubdir: "state/subagents",
44
+ importsSubdir: "imports",
45
+ worktreesSubdir: "worktrees",
45
46
  manifestFile: "manifest.json",
46
47
  tasksFile: "tasks.json",
47
48
  eventsFile: "events.jsonl",
@@ -1,82 +1,82 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
3
- import { handleTeamTool } from "./team-tool.ts";
4
- import { parseLiveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
5
-
6
- export interface EventBusLike {
7
- on(event: string, handler: (data: unknown) => void): (() => void) | void;
8
- emit(event: string, data: unknown): void;
9
- }
10
-
11
- export type RpcReply<T = unknown> = { success: true; data?: T } | { success: false; error: string };
12
- export const PI_CREW_RPC_VERSION = 1;
13
-
14
- export interface PiCrewRpcHandle {
15
- unsubscribe(): void;
16
- }
17
-
18
- function requestId(raw: unknown): string | undefined {
19
- return raw && typeof raw === "object" && !Array.isArray(raw) && typeof (raw as { requestId?: unknown }).requestId === "string" ? (raw as { requestId: string }).requestId : undefined;
20
- }
21
-
22
- function reply(events: EventBusLike, channel: string, id: string | undefined, payload: RpcReply): void {
23
- if (!id) return;
24
- events.emit(`${channel}:reply:${id}`, payload);
25
- }
26
-
27
- function textOf(result: Awaited<ReturnType<typeof handleTeamTool>>): string {
28
- return result.content?.map((item) => item.type === "text" ? item.text : "").join("\n") ?? "";
29
- }
30
-
31
- function on(events: EventBusLike, channel: string, handler: (raw: unknown) => void): () => void {
32
- const unsub = events.on(channel, handler);
33
- return typeof unsub === "function" ? unsub : () => {};
34
- }
35
-
36
- export function registerPiCrewRpc(events: EventBusLike | undefined, getCtx: () => ExtensionContext | undefined): PiCrewRpcHandle | undefined {
37
- if (!events) return undefined;
38
- const unsubs = [
39
- on(events, "pi-crew:rpc:ping", (raw) => reply(events, "pi-crew:rpc:ping", requestId(raw), { success: true, data: { version: PI_CREW_RPC_VERSION } })),
40
- on(events, "pi-crew:rpc:run", async (raw) => {
41
- const id = requestId(raw);
42
- try {
43
- const ctx = getCtx();
44
- if (!ctx) throw new Error("No active pi-crew session context.");
45
- const params: TeamToolParamsValue = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...(raw as object), action: "run" } as TeamToolParamsValue : { action: "run" };
46
- const result = await handleTeamTool(params, ctx);
47
- reply(events, "pi-crew:rpc:run", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: result.details });
48
- } catch (error) {
49
- reply(events, "pi-crew:rpc:run", id, { success: false, error: error instanceof Error ? error.message : String(error) });
50
- }
51
- }),
52
- on(events, "pi-crew:rpc:status", async (raw) => {
53
- const id = requestId(raw);
54
- try {
55
- const ctx = getCtx();
56
- if (!ctx) throw new Error("No active pi-crew session context.");
57
- const runId = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as { runId?: string }).runId : undefined;
58
- const result = await handleTeamTool({ action: "status", runId }, ctx);
59
- reply(events, "pi-crew:rpc:status", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
60
- } catch (error) {
61
- reply(events, "pi-crew:rpc:status", id, { success: false, error: error instanceof Error ? error.message : String(error) });
62
- }
63
- }),
64
- on(events, "pi-crew:live-control", (raw) => {
65
- const request = parseLiveControlRealtimeMessage(raw);
66
- if (request) publishLiveControlRealtime(request);
67
- }),
68
- on(events, "pi-crew:rpc:live-control", async (raw) => {
69
- const id = requestId(raw);
70
- try {
71
- const ctx = getCtx();
72
- if (!ctx) throw new Error("No active pi-crew session context.");
73
- const obj = raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
74
- const result = await handleTeamTool({ action: "api", runId: typeof obj.runId === "string" ? obj.runId : undefined, config: { operation: typeof obj.operation === "string" ? obj.operation : "steer-agent", agentId: obj.agentId, message: obj.message, prompt: obj.prompt } }, ctx);
75
- reply(events, "pi-crew:rpc:live-control", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
76
- } catch (error) {
77
- reply(events, "pi-crew:rpc:live-control", id, { success: false, error: error instanceof Error ? error.message : String(error) });
78
- }
79
- }),
80
- ];
81
- return { unsubscribe: () => unsubs.forEach((unsub) => unsub()) };
82
- }
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
3
+ import { handleTeamTool } from "./team-tool.ts";
4
+ import { parseLiveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
5
+
6
+ export interface EventBusLike {
7
+ on(event: string, handler: (data: unknown) => void): (() => void) | void;
8
+ emit(event: string, data: unknown): void;
9
+ }
10
+
11
+ export type RpcReply<T = unknown> = { success: true; data?: T } | { success: false; error: string };
12
+ export const PI_CREW_RPC_VERSION = 1;
13
+
14
+ export interface PiCrewRpcHandle {
15
+ unsubscribe(): void;
16
+ }
17
+
18
+ function requestId(raw: unknown): string | undefined {
19
+ return raw && typeof raw === "object" && !Array.isArray(raw) && typeof (raw as { requestId?: unknown }).requestId === "string" ? (raw as { requestId: string }).requestId : undefined;
20
+ }
21
+
22
+ function reply(events: EventBusLike, channel: string, id: string | undefined, payload: RpcReply): void {
23
+ if (!id) return;
24
+ events.emit(`${channel}:reply:${id}`, payload);
25
+ }
26
+
27
+ function textOf(result: Awaited<ReturnType<typeof handleTeamTool>>): string {
28
+ return result.content?.map((item) => item.type === "text" ? item.text : "").join("\n") ?? "";
29
+ }
30
+
31
+ function on(events: EventBusLike, channel: string, handler: (raw: unknown) => void): () => void {
32
+ const unsub = events.on(channel, handler);
33
+ return typeof unsub === "function" ? unsub : () => {};
34
+ }
35
+
36
+ export function registerPiCrewRpc(events: EventBusLike | undefined, getCtx: () => ExtensionContext | undefined): PiCrewRpcHandle | undefined {
37
+ if (!events) return undefined;
38
+ const unsubs = [
39
+ on(events, "pi-crew:rpc:ping", (raw) => reply(events, "pi-crew:rpc:ping", requestId(raw), { success: true, data: { version: PI_CREW_RPC_VERSION } })),
40
+ on(events, "pi-crew:rpc:run", async (raw) => {
41
+ const id = requestId(raw);
42
+ try {
43
+ const ctx = getCtx();
44
+ if (!ctx) throw new Error("No active pi-crew session context.");
45
+ const params: TeamToolParamsValue = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...(raw as object), action: "run" } as TeamToolParamsValue : { action: "run" };
46
+ const result = await handleTeamTool(params, ctx);
47
+ reply(events, "pi-crew:rpc:run", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: result.details });
48
+ } catch (error) {
49
+ reply(events, "pi-crew:rpc:run", id, { success: false, error: error instanceof Error ? error.message : String(error) });
50
+ }
51
+ }),
52
+ on(events, "pi-crew:rpc:status", async (raw) => {
53
+ const id = requestId(raw);
54
+ try {
55
+ const ctx = getCtx();
56
+ if (!ctx) throw new Error("No active pi-crew session context.");
57
+ const runId = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as { runId?: string }).runId : undefined;
58
+ const result = await handleTeamTool({ action: "status", runId }, ctx);
59
+ reply(events, "pi-crew:rpc:status", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
60
+ } catch (error) {
61
+ reply(events, "pi-crew:rpc:status", id, { success: false, error: error instanceof Error ? error.message : String(error) });
62
+ }
63
+ }),
64
+ on(events, "pi-crew:live-control", (raw) => {
65
+ const request = parseLiveControlRealtimeMessage(raw);
66
+ if (request) publishLiveControlRealtime(request);
67
+ }),
68
+ on(events, "pi-crew:rpc:live-control", async (raw) => {
69
+ const id = requestId(raw);
70
+ try {
71
+ const ctx = getCtx();
72
+ if (!ctx) throw new Error("No active pi-crew session context.");
73
+ const obj = raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
74
+ const result = await handleTeamTool({ action: "api", runId: typeof obj.runId === "string" ? obj.runId : undefined, config: { operation: typeof obj.operation === "string" ? obj.operation : "steer-agent", agentId: obj.agentId, message: obj.message, prompt: obj.prompt } }, ctx);
75
+ reply(events, "pi-crew:rpc:live-control", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
76
+ } catch (error) {
77
+ reply(events, "pi-crew:rpc:live-control", id, { success: false, error: error instanceof Error ? error.message : String(error) });
78
+ }
79
+ }),
80
+ ];
81
+ return { unsubscribe: () => unsubs.forEach((unsub) => unsub()) };
82
+ }
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { projectPiRoot, userPiRoot } from "../utils/paths.ts";
3
+ import { projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
4
+ import { DEFAULT_PATHS } from "../config/defaults.ts";
4
5
 
5
6
  export interface ImportedRunIndexEntry {
6
7
  runId: string;
@@ -45,8 +46,8 @@ function collect(root: string, scope: "project" | "user"): ImportedRunIndexEntry
45
46
  }
46
47
 
47
48
  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");
49
+ const projectRoot = path.join(projectCrewRoot(cwd), DEFAULT_PATHS.state.importsSubdir);
50
+ const userRoot = path.join(userCrewRoot(), DEFAULT_PATHS.state.importsSubdir);
50
51
  return [...collect(userRoot, "user"), ...collect(projectRoot, "project")]
51
52
  .sort((a, b) => (b.importedAt ?? "").localeCompare(a.importedAt ?? ""));
52
53
  }
@@ -12,7 +12,7 @@ import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
12
12
  import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
13
13
  import { serializeWorkflow } from "../workflows/workflow-serializer.ts";
14
14
  import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
15
- import { projectPiRoot, userPiRoot } from "../utils/paths.ts";
15
+ import { projectCrewRoot, userPiRoot } from "../utils/paths.ts";
16
16
  import { hasOwn, parseConfigObject, requireString, sanitizeName } from "../utils/names.ts";
17
17
 
18
18
  interface ManagementContext {
@@ -28,7 +28,7 @@ function result(text: string, status: TeamToolDetails["status"] = "ok", isError
28
28
  }
29
29
 
30
30
  function scopeDir(ctx: ManagementContext, resource: "agent" | "team" | "workflow", scope: MutableSource): string {
31
- const base = scope === "user" ? userPiRoot() : projectPiRoot(ctx.cwd);
31
+ const base = scope === "user" ? userPiRoot() : projectCrewRoot(ctx.cwd);
32
32
  if (resource === "agent") return path.join(base, "agents");
33
33
  if (resource === "team") return path.join(base, "teams");
34
34
  return path.join(base, "workflows");
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { packageRoot } from "../utils/paths.ts";
3
+ import { packageRoot, projectCrewRoot } from "../utils/paths.ts";
4
4
 
5
5
  export interface ProjectInitOptions {
6
6
  copyBuiltins?: boolean;
@@ -44,14 +44,16 @@ export function initializeProject(cwd: string, options: ProjectInitOptions = {})
44
44
  const createdDirs: string[] = [];
45
45
  const copiedFiles: string[] = [];
46
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");
47
+ const crewRoot = projectCrewRoot(cwd);
48
+ const usingLegacyPi = path.basename(crewRoot) === "teams" && path.basename(path.dirname(crewRoot)) === ".pi";
49
+ const ignorePrefix = usingLegacyPi ? ".pi/teams" : ".crew";
50
+ const agentsDir = path.join(crewRoot, "agents");
51
+ const teamsDir = path.join(crewRoot, "teams");
52
+ const workflowsDir = path.join(crewRoot, "workflows");
51
53
  ensureDir(agentsDir, createdDirs);
52
54
  ensureDir(teamsDir, createdDirs);
53
55
  ensureDir(workflowsDir, createdDirs);
54
- ensureDir(path.join(piRoot, "teams", "imports"), createdDirs);
56
+ ensureDir(path.join(crewRoot, "imports"), createdDirs);
55
57
 
56
58
  if (options.copyBuiltins) {
57
59
  copyBuiltinDir("agents", agentsDir, options.overwrite === true, copiedFiles, skippedFiles);
@@ -60,7 +62,7 @@ export function initializeProject(cwd: string, options: ProjectInitOptions = {})
60
62
  }
61
63
 
62
64
  const gitignorePath = path.join(cwd, ".gitignore");
63
- const desired = [".pi/teams/state/", ".pi/teams/artifacts/", ".pi/teams/worktrees/", ".pi/teams/imports/"];
65
+ const desired = [`${ignorePrefix}/state/`, `${ignorePrefix}/artifacts/`, `${ignorePrefix}/worktrees/`, `${ignorePrefix}/imports/`];
64
66
  const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf-8") : "";
65
67
  const missing = desired.filter((entry) => !existing.split(/\r?\n/).includes(entry));
66
68
  let gitignoreUpdated = false;
@@ -19,6 +19,7 @@ import { registerTeamCommands } from "./registration/commands.ts";
19
19
  import { registerSubagentTools } from "./registration/subagent-tools.ts";
20
20
  import { runArtifactCleanup } from "./registration/artifact-cleanup.ts";
21
21
  import { registerTeamTool } from "./registration/team-tool.ts";
22
+ import { registerCompactionGuard } from "./registration/compaction-guard.ts";
22
23
 
23
24
  export { __test__subagentSpawnParams };
24
25
 
@@ -47,10 +48,24 @@ export function registerPiTeams(pi: ExtensionAPI): void {
47
48
  manifestCache = createManifestCache(cwd);
48
49
  return manifestCache;
49
50
  };
51
+ const telemetryEnabled = (): boolean => loadConfig(currentCtx?.cwd ?? process.cwd()).config.telemetry?.enabled !== false;
50
52
  const widgetState: CrewWidgetState = { frame: 0 };
51
53
  const subagentManager = new SubagentManager(
52
54
  4,
53
55
  (record) => {
56
+ // Phase 1.3 + 1.6: Emit public crew.subagent.completed event with telemetry.
57
+ // Users can opt out with config.telemetry.enabled=false.
58
+ if (telemetryEnabled()) {
59
+ pi.events?.emit?.("crew.subagent.completed", {
60
+ id: record.id,
61
+ runId: record.runId,
62
+ type: record.type,
63
+ status: record.status,
64
+ turnCount: record.turnCount,
65
+ terminated: record.terminated ?? false,
66
+ durationMs: record.durationMs,
67
+ });
68
+ }
54
69
  if (!record.background || record.resultConsumed) return;
55
70
  if (record.status === "completed" || record.status === "failed" || record.status === "cancelled" || record.status === "blocked" || record.status === "error") {
56
71
  sendFollowUp(pi, [`pi-crew subagent ${record.id} ${record.status}.`, record.runId ? `Run: ${record.runId}` : undefined, `Use get_subagent_result with agent_id=${record.id} for output.`].filter((line): line is string => Boolean(line)).join("\n"));
@@ -108,6 +123,10 @@ export function registerPiTeams(pi: ExtensionAPI): void {
108
123
  const startForegroundRun = (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string): void => {
109
124
  const controller = new AbortController();
110
125
  foregroundControllers.add(controller);
126
+ if (ctx.hasUI) {
127
+ (ctx.ui as { setWorkingIndicator?: (options?: { frames?: string[]; intervalMs?: number }) => void }).setWorkingIndicator?.({ frames: ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"], intervalMs: 80 });
128
+ ctx.ui.setWorkingMessage(runId ? `pi-crew foreground run ${runId}...` : "pi-crew foreground run...");
129
+ }
111
130
  setImmediate(() => {
112
131
  void runner(controller.signal)
113
132
  .catch((error) => {
@@ -124,11 +143,37 @@ export function registerPiTeams(pi: ExtensionAPI): void {
124
143
  })
125
144
  .finally(() => {
126
145
  foregroundControllers.delete(controller);
146
+ if (ctx.hasUI) {
147
+ (ctx.ui as { setWorkingIndicator?: (options?: { frames?: string[]; intervalMs?: number }) => void }).setWorkingIndicator?.();
148
+ ctx.ui.setWorkingMessage();
149
+ }
127
150
  if (runId) {
128
151
  const loaded = loadRunManifestById(ctx.cwd, runId);
129
152
  const status = loaded?.manifest.status ?? "finished";
130
153
  const level = status === "failed" || status === "blocked" ? "error" : status === "cancelled" ? "warning" : "info";
131
154
  ctx.ui.notify(`pi-crew run ${runId} ${status}. Use /team-summary ${runId} or /team-status ${runId}.`, level as "info" | "warning" | "error");
155
+ // Phase 2.3: Persist run completion reference into the Pi session.
156
+ pi.appendEntry("crew:run-completed", {
157
+ runId,
158
+ team: loaded?.manifest.team,
159
+ workflow: loaded?.manifest.workflow,
160
+ goal: loaded?.manifest.goal,
161
+ status,
162
+ taskCount: loaded?.tasks.length,
163
+ timestamp: Date.now(),
164
+ });
165
+ // Phase 1.3: Emit public crew.run.* events
166
+ const eventType = status === "completed" ? "crew.run.completed" : status === "failed" || status === "blocked" ? "crew.run.failed" : status === "cancelled" ? "crew.run.cancelled" : undefined;
167
+ if (eventType) {
168
+ pi.events?.emit?.(eventType, {
169
+ runId,
170
+ team: loaded?.manifest.team,
171
+ workflow: loaded?.manifest.workflow,
172
+ status,
173
+ taskCount: loaded?.tasks.length,
174
+ goal: loaded?.manifest.goal,
175
+ });
176
+ }
132
177
  }
133
178
  if (currentCtx) {
134
179
  const config = loadConfig(currentCtx.cwd).config.ui;
@@ -189,6 +234,24 @@ export function registerPiTeams(pi: ExtensionAPI): void {
189
234
  pi.on("session_before_switch", () => stopSessionBoundSubagents());
190
235
  pi.on("session_shutdown", () => cleanupRuntime());
191
236
 
237
+ registerCompactionGuard(pi, { foregroundControllers });
238
+
239
+ // Phase 1.4: Permission gate for destructive team actions.
240
+ // AGENTS.md requires confirm=true for management deletes.
241
+ pi.on("tool_call", async (event, ctx) => {
242
+ if (event.toolName !== "team") return;
243
+ const input = (event as { input?: Record<string, unknown> }).input;
244
+ if (!input) return;
245
+ const action = typeof input.action === "string" ? input.action : undefined;
246
+ const destructiveActions = new Set(["delete", "forget", "prune", "cleanup"]);
247
+ if (!action || !destructiveActions.has(action)) return;
248
+ if (input.confirm === true || input.force === true) return;
249
+ return {
250
+ block: true,
251
+ reason: `Destructive action '${action}' requires confirm=true (or force=true to bypass reference checks).`,
252
+ };
253
+ });
254
+
192
255
  registerTeamTool(pi, { foregroundControllers, startForegroundRun, openLiveSidebar, getManifestCache, widgetState });
193
256
  registerSubagentTools(pi, subagentManager);
194
257
  time("register.tools");
@@ -1,14 +1,15 @@
1
- import * as path from "node:path";
2
- import { DEFAULT_ARTIFACT_CLEANUP } from "../../config/defaults.ts";
3
- import { CLEANUP_MARKER_FILE, cleanupOldArtifacts } from "../../state/artifact-store.ts";
4
- import { logInternalError } from "../../utils/internal-error.ts";
5
- import { projectPiRoot, userPiRoot } from "../../utils/paths.ts";
6
-
7
- export function runArtifactCleanup(cwd: string): void {
8
- try {
9
- cleanupOldArtifacts(path.join(userPiRoot(), "extensions", "pi-crew", "artifacts"), { maxAgeDays: DEFAULT_ARTIFACT_CLEANUP.maxAgeDays, markerFile: CLEANUP_MARKER_FILE });
10
- cleanupOldArtifacts(path.join(projectPiRoot(cwd), "artifacts"), { maxAgeDays: DEFAULT_ARTIFACT_CLEANUP.maxAgeDays, markerFile: CLEANUP_MARKER_FILE });
11
- } catch (error) {
12
- logInternalError("register.artifact-cleanup", error, `cwd=${cwd}`);
13
- }
14
- }
1
+ import * as path from "node:path";
2
+ import { DEFAULT_ARTIFACT_CLEANUP } from "../../config/defaults.ts";
3
+ import { CLEANUP_MARKER_FILE, cleanupOldArtifacts } from "../../state/artifact-store.ts";
4
+ import { logInternalError } from "../../utils/internal-error.ts";
5
+ import { projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
6
+ import { DEFAULT_PATHS } from "../../config/defaults.ts";
7
+
8
+ export function runArtifactCleanup(cwd: string): void {
9
+ try {
10
+ cleanupOldArtifacts(path.join(userCrewRoot(), DEFAULT_PATHS.state.artifactsSubdir), { maxAgeDays: DEFAULT_ARTIFACT_CLEANUP.maxAgeDays, markerFile: CLEANUP_MARKER_FILE });
11
+ cleanupOldArtifacts(path.join(projectCrewRoot(cwd), DEFAULT_PATHS.state.artifactsSubdir), { maxAgeDays: DEFAULT_ARTIFACT_CLEANUP.maxAgeDays, markerFile: CLEANUP_MARKER_FILE });
12
+ } catch (error) {
13
+ logInternalError("register.artifact-cleanup", error, `cwd=${cwd}`);
14
+ }
15
+ }