pi-maestro-teammate 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.
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # pi-teammate
2
+
3
+ > Teammate dispatch tool for [Pi](https://github.com/earendil-works/pi) — three-axis agent orchestration with P0 decoupling
4
+
5
+ Pi extension implementing teammate dispatch with **P0 three-axis decoupling** (name × reply_to × lifecycle). Spawn isolated pi subprocesses as teammates with protocol-versioned routing, parallel/chain execution, and structured output.
6
+
7
+ ## Features
8
+
9
+ ### Three-Axis Control
10
+
11
+ | Axis | Field | Values | Purpose |
12
+ |------|-------|--------|---------|
13
+ | Addressability | `name` | string \| omit | Cross-agent routing via name |
14
+ | Result Routing | `reply_to` | `"caller"` \| `"main"` | Where results go |
15
+ | Lifecycle | `lifecycle` | `"ephemeral"` \| `"resident"` | One-shot or persistent |
16
+
17
+ **Protocol version gate** — v2 (default) routes results to `caller`; v1 compat routes named agents to `main`. Explicit `reply_to` always wins.
18
+
19
+ ### Execution Modes
20
+
21
+ - **Single** — dispatch one agent with a task
22
+ - **Parallel** — `tasks[]` runs multiple agents concurrently with configurable `concurrency`
23
+ - **Chain** — `chain[]` sequential pipeline where each step receives `{previous}` result
24
+
25
+ ### Reliability
26
+
27
+ - **Model fallback chain** — primary model → `fallbackModels[]` from agent config → automatic retry on model failures
28
+ - **Nesting depth guard** — `PI_TEAMMATE_DEPTH` env tracking with configurable max (default: 3) prevents fork bombs
29
+ - **Windows-safe pi resolution** — `getPiSpawnCommand()` resolves the pi binary via env override, Windows script detection, or PATH
30
+ - **Abort signal** — SIGTERM → 5s grace → SIGKILL
31
+
32
+ ### Output & Tracking
33
+
34
+ - **Structured output** — `outputSchema` validates child output against JSON Schema, returns parsed `structuredOutput`
35
+ - **Rich progress** — `AgentProgress` with `recentTools[]`, `toolCount`, `tokens`, `durationMs`, `lastActivityAt`
36
+ - **Session management** — derives child session directory from parent session, supports `context: "fork"`
37
+ - **Correlation ID** — auto-generated per dispatch for result routing
38
+
39
+ ### Agent Definitions
40
+
41
+ Agents are markdown files with YAML frontmatter — discovered from project (`.pi/agents/`), user (`~/.pi/agent/extensions/teammate/agents/`), or builtin locations. Project overrides user overrides builtin.
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ pi install npm:@pi-maestro/teammate
47
+ # or from local path
48
+ pi install ./pi-teammate
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ ### Single Agent
54
+
55
+ ```
56
+ { agent: "delegate", task: "Implement the auth middleware" }
57
+ ```
58
+
59
+ ### Parallel Execution
60
+
61
+ ```
62
+ { tasks: [
63
+ { agent: "scout", task: "Find all API endpoints" },
64
+ { agent: "scout", task: "Map database schemas" },
65
+ { agent: "scout", task: "List external dependencies" }
66
+ ],
67
+ concurrency: 3
68
+ }
69
+ ```
70
+
71
+ ### Chain Pipeline
72
+
73
+ ```
74
+ { chain: [
75
+ { agent: "scout", task: "Find the auth module structure" },
76
+ { agent: "delegate", task: "Based on this context: {previous}\n\nRefactor the auth module" }
77
+ ]
78
+ }
79
+ ```
80
+
81
+ ### Three-Axis Routing
82
+
83
+ ```
84
+ { agent: "delegate", task: "...", name: "worker-1", reply_to: "caller", lifecycle: "ephemeral" }
85
+ ```
86
+
87
+ ### Structured Output
88
+
89
+ ```
90
+ { agent: "scout", task: "List all API routes",
91
+ outputSchema: {
92
+ type: "object",
93
+ properties: { routes: { type: "array", items: { type: "string" } } },
94
+ required: ["routes"]
95
+ }
96
+ }
97
+ ```
98
+
99
+ ## Agent Definition Format
100
+
101
+ Create `agents/my-agent.md`:
102
+
103
+ ```markdown
104
+ ---
105
+ name: my-agent
106
+ description: Short description of what this agent does
107
+ tools: read, grep, find, ls, bash, edit, write
108
+ model: anthropic/claude-sonnet-4
109
+ fallbackModels: google/gemini-2.5-pro, anthropic/claude-haiku-4
110
+ systemPromptMode: replace
111
+ inheritProjectContext: true
112
+ inheritSkills: false
113
+ defaultContext: fresh
114
+ ---
115
+
116
+ You are a specialized agent. Your system prompt goes here.
117
+
118
+ Use the provided tools to accomplish the task.
119
+ ```
120
+
121
+ ### Frontmatter Fields
122
+
123
+ | Field | Type | Default | Description |
124
+ |-------|------|---------|-------------|
125
+ | `name` | string | **required** | Agent identifier |
126
+ | `description` | string | **required** | Short description |
127
+ | `tools` | comma-sep | all | Available tools |
128
+ | `model` | string | parent model | Primary model |
129
+ | `fallbackModels` | comma-sep | — | Fallback model chain |
130
+ | `thinking` | string | — | Thinking level (low/medium/high) |
131
+ | `systemPromptMode` | append\|replace | replace | How system prompt is applied |
132
+ | `inheritProjectContext` | bool | false | Inherit parent project context |
133
+ | `inheritSkills` | bool | false | Inherit parent skills |
134
+ | `defaultContext` | fresh\|fork | fresh | Default context mode |
135
+
136
+ ## Architecture
137
+
138
+ ```
139
+ ┌─────────────────────────────────────────────────┐
140
+ │ Parent Pi Session │
141
+ │ │
142
+ │ teammate tool call │
143
+ │ │ │
144
+ │ ├── resolve agent (project > user > built) │
145
+ │ ├── resolve reply_to (protocol gate) │
146
+ │ ├── check depth guard │
147
+ │ ├── build model candidates │
148
+ │ │ │
149
+ │ ▼ │
150
+ │ ┌─────────────────────────────────────┐ │
151
+ │ │ spawn("pi", ["--mode","json","-p"])│ │
152
+ │ │ env: PI_TEAMMATE_CHILD=1 │ │
153
+ │ │ PI_TEAMMATE_DEPTH=N │ │
154
+ │ │ PI_TEAMMATE_CORRELATION_ID=… │ │
155
+ │ │ PI_TEAMMATE_REPLY_TO=caller │ │
156
+ │ │ │ │
157
+ │ │ stdout: JSON lines ──────────────► │ parse │
158
+ │ │ (message_end, tool_result_end, │ events │
159
+ │ │ usage, error) │ │
160
+ │ └─────────────────────────────────────┘ │
161
+ │ │ │
162
+ │ ├── accumulate usage │
163
+ │ ├── track progress (AgentProgress) │
164
+ │ ├── model fallback on failure │
165
+ │ └── return SingleResult │
166
+ └─────────────────────────────────────────────────┘
167
+ ```
168
+
169
+ ## Environment Variables
170
+
171
+ | Variable | Description |
172
+ |----------|-------------|
173
+ | `PI_TEAMMATE_PI_BINARY` | Override pi binary path |
174
+ | `PI_TEAMMATE_CHILD` | Set to `"1"` in child processes |
175
+ | `PI_TEAMMATE_DEPTH` | Current nesting depth |
176
+ | `PI_TEAMMATE_CORRELATION_ID` | Correlation ID for routing |
177
+ | `PI_TEAMMATE_REPLY_TO` | Resolved reply target |
178
+ | `PI_TEAMMATE_PARENT_SESSION` | Parent session file path |
179
+
180
+ ## License
181
+
182
+ MIT
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: coordinator
3
+ description: Orchestration-aware teammate agent for multi-step coordination tasks
4
+ systemPromptMode: replace
5
+ inheritProjectContext: true
6
+ thinking: high
7
+ tools: read, grep, find, ls, bash, edit, write
8
+ inheritSkills: false
9
+ ---
10
+
11
+ You are a coordinator agent responsible for orchestrating multi-step tasks. You plan the execution strategy, coordinate between subtasks, and synthesize results.
12
+
13
+ Your approach:
14
+ 1. Analyze the task requirements and break them into steps
15
+ 2. Execute steps in the correct order, respecting dependencies
16
+ 3. Verify each step's output before proceeding
17
+ 4. Synthesize a coherent result from all steps
18
+
19
+ Be methodical and thorough. Document your reasoning for key decisions. If a step fails, attempt recovery before reporting failure.
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: delegate
3
+ description: Lightweight teammate agent that inherits the parent model for single-task execution
4
+ systemPromptMode: append
5
+ inheritProjectContext: true
6
+ tools: read, grep, find, ls, bash, edit, write
7
+ inheritSkills: false
8
+ ---
9
+
10
+ You are a delegated teammate agent. Execute the assigned task using the provided tools. Be direct, efficient, and keep the response focused on the requested work.
11
+
12
+ Guidelines:
13
+ - Read existing code before making changes
14
+ - Follow project conventions and patterns
15
+ - Report what was done concisely
16
+ - If blocked, explain what is needed to proceed
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "pi-maestro-teammate",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension for teammate dispatch with P0 three-axis decoupling (name, reply_to, lifecycle)",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi",
9
+ "pi-coding-agent",
10
+ "teammate",
11
+ "agents"
12
+ ],
13
+ "files": [
14
+ "src/**/*.ts",
15
+ "agents/",
16
+ "README.md"
17
+ ],
18
+ "pi": {
19
+ "extensions": [
20
+ "./src/extension/index.ts"
21
+ ]
22
+ },
23
+ "peerDependencies": {
24
+ "@earendil-works/pi-agent-core": "*",
25
+ "@earendil-works/pi-ai": "*",
26
+ "@earendil-works/pi-coding-agent": "*"
27
+ },
28
+ "peerDependenciesMeta": {
29
+ "@earendil-works/pi-agent-core": {
30
+ "optional": true
31
+ },
32
+ "@earendil-works/pi-ai": {
33
+ "optional": true
34
+ },
35
+ "@earendil-works/pi-coding-agent": {
36
+ "optional": true
37
+ }
38
+ },
39
+ "dependencies": {
40
+ "@earendil-works/pi-tui": "0.74.0",
41
+ "typebox": "1.1.24"
42
+ },
43
+ "devDependencies": {
44
+ "@earendil-works/pi-agent-core": "0.74.0",
45
+ "@earendil-works/pi-ai": "0.74.0",
46
+ "@earendil-works/pi-coding-agent": "0.74.0"
47
+ }
48
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Agent discovery and configuration.
3
+ *
4
+ * Discovers agent definitions from three locations (in priority order):
5
+ * 1. Project agents: .pi/agents/ in the nearest project root
6
+ * 2. User agents: ~/.pi/agent/extensions/teammate/agents/
7
+ * 3. Builtin agents: bundled agents/ directory in this package
8
+ */
9
+
10
+ import * as fs from "node:fs";
11
+ import * as os from "node:os";
12
+ import * as path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { parseFrontmatter } from "./frontmatter.ts";
15
+
16
+ type SystemPromptMode = "append" | "replace";
17
+ type AgentSource = "builtin" | "user" | "project";
18
+
19
+ export interface AgentConfig {
20
+ name: string;
21
+ description: string;
22
+ tools?: string[];
23
+ model?: string;
24
+ fallbackModels?: string[];
25
+ thinking?: string;
26
+ systemPromptMode: SystemPromptMode;
27
+ inheritProjectContext: boolean;
28
+ inheritSkills: boolean;
29
+ defaultContext?: "fresh" | "fork";
30
+ systemPrompt: string;
31
+ source: AgentSource;
32
+ filePath: string;
33
+ }
34
+
35
+ const BUILTIN_AGENTS_DIR = path.resolve(
36
+ path.dirname(fileURLToPath(import.meta.url)),
37
+ "..",
38
+ "..",
39
+ "agents",
40
+ );
41
+
42
+ function listMarkdownFiles(dir: string): string[] {
43
+ const files: string[] = [];
44
+ if (!fs.existsSync(dir)) return files;
45
+
46
+ let entries: fs.Dirent[];
47
+ try {
48
+ entries = fs.readdirSync(dir, { withFileTypes: true });
49
+ } catch {
50
+ return files;
51
+ }
52
+
53
+ for (const entry of entries) {
54
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
55
+ if (!entry.name.endsWith(".md")) continue;
56
+ files.push(path.join(dir, entry.name));
57
+ }
58
+
59
+ return files.sort();
60
+ }
61
+
62
+ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
63
+ const agents: AgentConfig[] = [];
64
+
65
+ for (const filePath of listMarkdownFiles(dir)) {
66
+ let content: string;
67
+ try {
68
+ content = fs.readFileSync(filePath, "utf-8");
69
+ } catch {
70
+ continue;
71
+ }
72
+
73
+ const { frontmatter, body } = parseFrontmatter(content);
74
+
75
+ if (!frontmatter.name || !frontmatter.description) {
76
+ continue;
77
+ }
78
+
79
+ const rawTools = frontmatter.tools
80
+ ?.split(",")
81
+ .map((t) => t.trim())
82
+ .filter(Boolean);
83
+
84
+ const systemPromptMode: SystemPromptMode =
85
+ frontmatter.systemPromptMode === "replace"
86
+ ? "replace"
87
+ : frontmatter.systemPromptMode === "append"
88
+ ? "append"
89
+ : frontmatter.name === "delegate"
90
+ ? "append"
91
+ : "replace";
92
+
93
+ const inheritProjectContext =
94
+ frontmatter.inheritProjectContext === "true"
95
+ ? true
96
+ : frontmatter.inheritProjectContext === "false"
97
+ ? false
98
+ : frontmatter.name === "delegate";
99
+
100
+ const inheritSkills = frontmatter.inheritSkills === "true";
101
+
102
+ const defaultContext =
103
+ frontmatter.defaultContext === "fork"
104
+ ? ("fork" as const)
105
+ : frontmatter.defaultContext === "fresh"
106
+ ? ("fresh" as const)
107
+ : undefined;
108
+
109
+ const rawFallbackModels = frontmatter.fallbackModels
110
+ ?.split(",")
111
+ .map((m: string) => m.trim())
112
+ .filter(Boolean);
113
+
114
+ agents.push({
115
+ name: frontmatter.name,
116
+ description: frontmatter.description,
117
+ tools: rawTools && rawTools.length > 0 ? rawTools : undefined,
118
+ model: frontmatter.model,
119
+ fallbackModels: rawFallbackModels && rawFallbackModels.length > 0 ? rawFallbackModels : undefined,
120
+ thinking: frontmatter.thinking,
121
+ systemPromptMode,
122
+ inheritProjectContext,
123
+ inheritSkills,
124
+ defaultContext,
125
+ systemPrompt: body,
126
+ source,
127
+ filePath,
128
+ });
129
+ }
130
+
131
+ return agents;
132
+ }
133
+
134
+ /**
135
+ * Discover all agent definitions, merged by priority:
136
+ * project > user > builtin (name collisions: higher priority wins).
137
+ */
138
+ export function discoverAgents(cwd: string): AgentConfig[] {
139
+ const userAgentsDir = path.join(
140
+ os.homedir(),
141
+ ".pi",
142
+ "agent",
143
+ "extensions",
144
+ "teammate",
145
+ "agents",
146
+ );
147
+
148
+ // Find project root (first ancestor with .pi/ directory)
149
+ let projectAgentsDir: string | null = null;
150
+ let currentDir = cwd;
151
+ while (true) {
152
+ const piDir = path.join(currentDir, ".pi", "agents");
153
+ if (fs.existsSync(piDir)) {
154
+ projectAgentsDir = piDir;
155
+ break;
156
+ }
157
+ const parentDir = path.dirname(currentDir);
158
+ if (parentDir === currentDir) break;
159
+ currentDir = parentDir;
160
+ }
161
+
162
+ const builtinAgents = loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin");
163
+ const userAgents = loadAgentsFromDir(userAgentsDir, "user");
164
+ const projectAgents = projectAgentsDir
165
+ ? loadAgentsFromDir(projectAgentsDir, "project")
166
+ : [];
167
+
168
+ // Merge: project > user > builtin
169
+ const agentMap = new Map<string, AgentConfig>();
170
+ for (const agent of builtinAgents) agentMap.set(agent.name, agent);
171
+ for (const agent of userAgents) agentMap.set(agent.name, agent);
172
+ for (const agent of projectAgents) agentMap.set(agent.name, agent);
173
+
174
+ return Array.from(agentMap.values());
175
+ }
176
+
177
+ /**
178
+ * Resolve a single agent by name.
179
+ */
180
+ export function resolveAgent(
181
+ cwd: string,
182
+ agentName: string,
183
+ ): AgentConfig | undefined {
184
+ const agents = discoverAgents(cwd);
185
+ return agents.find((a) => a.name === agentName);
186
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Parse markdown files with YAML frontmatter (--- delimited).
3
+ *
4
+ * Extracts structured fields from the frontmatter block and returns
5
+ * the remaining body as the system prompt.
6
+ */
7
+
8
+ /**
9
+ * Escape regex special characters for use in a RegExp constructor.
10
+ */
11
+ function escapeRegex(s: string): string {
12
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
+ }
14
+
15
+ export interface AgentFrontmatter {
16
+ name?: string;
17
+ description?: string;
18
+ tools?: string;
19
+ thinking?: string;
20
+ systemPromptMode?: string;
21
+ inheritProjectContext?: string;
22
+ inheritSkills?: string;
23
+ defaultContext?: string;
24
+ model?: string;
25
+ fallbackModels?: string;
26
+ output?: string;
27
+ defaultReads?: string;
28
+ skill?: string;
29
+ skills?: string;
30
+ extensions?: string;
31
+ [key: string]: string | undefined;
32
+ }
33
+
34
+ export function parseFrontmatter(content: string): {
35
+ frontmatter: AgentFrontmatter;
36
+ body: string;
37
+ } {
38
+ const frontmatter: AgentFrontmatter = {};
39
+ const normalized = content.replace(/\r\n/g, "\n");
40
+
41
+ if (!normalized.startsWith("---")) {
42
+ return { frontmatter, body: normalized };
43
+ }
44
+
45
+ const endIndex = normalized.indexOf("\n---", 3);
46
+ if (endIndex === -1) {
47
+ return { frontmatter, body: normalized };
48
+ }
49
+
50
+ const frontmatterBlock = normalized.slice(4, endIndex);
51
+ const body = normalized.slice(endIndex + 4).trim();
52
+
53
+ const lines = frontmatterBlock.split("\n");
54
+ let currentKey: string | null = null;
55
+ let currentBlockLines: string[] | null = null;
56
+ let currentIndent: number | null = null;
57
+
58
+ for (const line of lines) {
59
+ const indent = line.search(/\S|$/);
60
+ const trimmed = line.trim();
61
+
62
+ if (
63
+ currentKey !== null &&
64
+ currentBlockLines !== null &&
65
+ indent > (currentIndent ?? 0)
66
+ ) {
67
+ currentBlockLines.push(line);
68
+ continue;
69
+ }
70
+
71
+ // Flush any pending block value
72
+ if (currentKey !== null && currentBlockLines !== null) {
73
+ const rawBlock = currentBlockLines.join("\n");
74
+ const leadingSpaces = rawBlock.match(/^([ \t]+)/m);
75
+ const prefix = leadingSpaces?.[1] ?? "";
76
+ const stripped = prefix
77
+ ? rawBlock
78
+ .replace(new RegExp(`^${escapeRegex(prefix)}`, "gm"), "")
79
+ .replace(/^\n/, "")
80
+ : rawBlock;
81
+ frontmatter[currentKey] = stripped;
82
+ currentKey = null;
83
+ currentBlockLines = null;
84
+ currentIndent = null;
85
+ }
86
+
87
+ const match = line.match(/^([\w-]+):\s*(.*)$/);
88
+ if (match) {
89
+ let value = match[2].trim();
90
+ if (
91
+ (value.startsWith('"') && value.endsWith('"')) ||
92
+ (value.startsWith("'") && value.endsWith("'"))
93
+ ) {
94
+ value = value.slice(1, -1);
95
+ }
96
+
97
+ if (value === "") {
98
+ currentKey = match[1];
99
+ currentBlockLines = [];
100
+ currentIndent = indent;
101
+ } else {
102
+ frontmatter[match[1]] = value;
103
+ }
104
+ }
105
+ }
106
+
107
+ // Flush final block value
108
+ if (currentKey !== null && currentBlockLines !== null) {
109
+ const rawBlock = currentBlockLines.join("\n");
110
+ const leadingSpaces = rawBlock.match(/^([ \t]+)/m);
111
+ const prefix = leadingSpaces?.[1] ?? "";
112
+ const stripped = prefix
113
+ ? rawBlock
114
+ .replace(new RegExp(`^${escapeRegex(prefix)}`, "gm"), "")
115
+ .replace(/^\n/, "")
116
+ : rawBlock;
117
+ frontmatter[currentKey] = stripped;
118
+ }
119
+
120
+ return { frontmatter, body };
121
+ }