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 +182 -0
- package/agents/coordinator.md +19 -0
- package/agents/delegate.md +16 -0
- package/package.json +48 -0
- package/src/agents/agents.ts +186 -0
- package/src/agents/frontmatter.ts +121 -0
- package/src/extension/index.ts +236 -0
- package/src/extension/schemas.ts +148 -0
- package/src/runs/execution.ts +675 -0
- package/src/shared/routing.ts +46 -0
- package/src/shared/types.ts +62 -0
- package/src/tui/render.ts +153 -0
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
|
+
}
|