@agent-relay/wrapper 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/dist/__fixtures__/claude-outputs.d.ts +49 -0
- package/dist/__fixtures__/claude-outputs.d.ts.map +1 -0
- package/dist/__fixtures__/claude-outputs.js +443 -0
- package/dist/__fixtures__/claude-outputs.js.map +1 -0
- package/dist/__fixtures__/codex-outputs.d.ts +9 -0
- package/dist/__fixtures__/codex-outputs.d.ts.map +1 -0
- package/dist/__fixtures__/codex-outputs.js +94 -0
- package/dist/__fixtures__/codex-outputs.js.map +1 -0
- package/dist/__fixtures__/gemini-outputs.d.ts +19 -0
- package/dist/__fixtures__/gemini-outputs.d.ts.map +1 -0
- package/dist/__fixtures__/gemini-outputs.js +144 -0
- package/dist/__fixtures__/gemini-outputs.js.map +1 -0
- package/dist/__fixtures__/index.d.ts +68 -0
- package/dist/__fixtures__/index.d.ts.map +1 -0
- package/dist/__fixtures__/index.js +44 -0
- package/dist/__fixtures__/index.js.map +1 -0
- package/dist/auth-detection.d.ts +49 -0
- package/dist/auth-detection.d.ts.map +1 -0
- package/dist/auth-detection.js +199 -0
- package/dist/auth-detection.js.map +1 -0
- package/dist/base-wrapper.d.ts +225 -0
- package/dist/base-wrapper.d.ts.map +1 -0
- package/dist/base-wrapper.js +572 -0
- package/dist/base-wrapper.js.map +1 -0
- package/dist/client.d.ts +254 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +801 -0
- package/dist/client.js.map +1 -0
- package/dist/id-generator.d.ts +35 -0
- package/dist/id-generator.d.ts.map +1 -0
- package/dist/id-generator.js +60 -0
- package/dist/id-generator.js.map +1 -0
- package/dist/idle-detector.d.ts +110 -0
- package/dist/idle-detector.d.ts.map +1 -0
- package/dist/idle-detector.js +304 -0
- package/dist/idle-detector.js.map +1 -0
- package/dist/inbox.d.ts +37 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +73 -0
- package/dist/inbox.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +236 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +1238 -0
- package/dist/parser.js.map +1 -0
- package/dist/prompt-composer.d.ts +67 -0
- package/dist/prompt-composer.d.ts.map +1 -0
- package/dist/prompt-composer.js +168 -0
- package/dist/prompt-composer.js.map +1 -0
- package/dist/relay-pty-orchestrator.d.ts +407 -0
- package/dist/relay-pty-orchestrator.d.ts.map +1 -0
- package/dist/relay-pty-orchestrator.js +1885 -0
- package/dist/relay-pty-orchestrator.js.map +1 -0
- package/dist/shared.d.ts +201 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +341 -0
- package/dist/shared.js.map +1 -0
- package/dist/stuck-detector.d.ts +161 -0
- package/dist/stuck-detector.d.ts.map +1 -0
- package/dist/stuck-detector.js +402 -0
- package/dist/stuck-detector.js.map +1 -0
- package/dist/tmux-resolver.d.ts +55 -0
- package/dist/tmux-resolver.d.ts.map +1 -0
- package/dist/tmux-resolver.js +175 -0
- package/dist/tmux-resolver.js.map +1 -0
- package/dist/tmux-wrapper.d.ts +345 -0
- package/dist/tmux-wrapper.d.ts.map +1 -0
- package/dist/tmux-wrapper.js +1747 -0
- package/dist/tmux-wrapper.js.map +1 -0
- package/dist/trajectory-integration.d.ts +292 -0
- package/dist/trajectory-integration.d.ts.map +1 -0
- package/dist/trajectory-integration.js +979 -0
- package/dist/trajectory-integration.js.map +1 -0
- package/dist/wrapper-types.d.ts +41 -0
- package/dist/wrapper-types.d.ts.map +1 -0
- package/dist/wrapper-types.js +7 -0
- package/dist/wrapper-types.js.map +1 -0
- package/package.json +63 -0
- package/src/__fixtures__/claude-outputs.ts +471 -0
- package/src/__fixtures__/codex-outputs.ts +99 -0
- package/src/__fixtures__/gemini-outputs.ts +151 -0
- package/src/__fixtures__/index.ts +47 -0
- package/src/auth-detection.ts +244 -0
- package/src/base-wrapper.test.ts +540 -0
- package/src/base-wrapper.ts +741 -0
- package/src/client.test.ts +262 -0
- package/src/client.ts +984 -0
- package/src/id-generator.test.ts +71 -0
- package/src/id-generator.ts +69 -0
- package/src/idle-detector.test.ts +390 -0
- package/src/idle-detector.ts +370 -0
- package/src/inbox.test.ts +233 -0
- package/src/inbox.ts +89 -0
- package/src/index.ts +170 -0
- package/src/parser.regression.test.ts +251 -0
- package/src/parser.test.ts +1359 -0
- package/src/parser.ts +1477 -0
- package/src/prompt-composer.test.ts +219 -0
- package/src/prompt-composer.ts +231 -0
- package/src/relay-pty-orchestrator.test.ts +1027 -0
- package/src/relay-pty-orchestrator.ts +2270 -0
- package/src/shared.test.ts +221 -0
- package/src/shared.ts +454 -0
- package/src/stuck-detector.test.ts +303 -0
- package/src/stuck-detector.ts +511 -0
- package/src/tmux-resolver.test.ts +104 -0
- package/src/tmux-resolver.ts +207 -0
- package/src/tmux-wrapper.test.ts +316 -0
- package/src/tmux-wrapper.ts +2010 -0
- package/src/trajectory-detection.test.ts +151 -0
- package/src/trajectory-integration.ts +1261 -0
- package/src/wrapper-types.ts +45 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
composeForAgent,
|
|
4
|
+
getAvailableRoles,
|
|
5
|
+
parseRoleFromProfile,
|
|
6
|
+
clearPromptCache,
|
|
7
|
+
type AgentProfile,
|
|
8
|
+
} from './prompt-composer.js';
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
|
|
11
|
+
// Mock fs module
|
|
12
|
+
vi.mock('node:fs/promises');
|
|
13
|
+
|
|
14
|
+
describe('prompt-composer', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
clearPromptCache();
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('parseRoleFromProfile', () => {
|
|
21
|
+
it('should parse role from frontmatter', () => {
|
|
22
|
+
const content = `---
|
|
23
|
+
name: Lead
|
|
24
|
+
role: planner
|
|
25
|
+
canSpawnChildren: true
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
Lead agent description.
|
|
29
|
+
`;
|
|
30
|
+
expect(parseRoleFromProfile(content)).toBe('planner');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should return undefined for missing role', () => {
|
|
34
|
+
const content = `---
|
|
35
|
+
name: Worker
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
Worker description.
|
|
39
|
+
`;
|
|
40
|
+
expect(parseRoleFromProfile(content)).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return undefined for invalid role', () => {
|
|
44
|
+
const content = `---
|
|
45
|
+
name: Agent
|
|
46
|
+
role: invalid
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
Description.
|
|
50
|
+
`;
|
|
51
|
+
expect(parseRoleFromProfile(content)).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should handle case-insensitive roles', () => {
|
|
55
|
+
const content = `---
|
|
56
|
+
role: WORKER
|
|
57
|
+
---
|
|
58
|
+
`;
|
|
59
|
+
expect(parseRoleFromProfile(content)).toBe('worker');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should return undefined for no frontmatter', () => {
|
|
63
|
+
const content = 'Just plain markdown without frontmatter.';
|
|
64
|
+
expect(parseRoleFromProfile(content)).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('composeForAgent', () => {
|
|
69
|
+
const mockProjectRoot = '/test/project';
|
|
70
|
+
|
|
71
|
+
it('should compose prompt with role-specific content', async () => {
|
|
72
|
+
const mockRolePrompt = '# Planner Strategy\n\nPlanner instructions...';
|
|
73
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce(mockRolePrompt);
|
|
74
|
+
|
|
75
|
+
const profile: AgentProfile = {
|
|
76
|
+
name: 'Lead',
|
|
77
|
+
role: 'planner',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const result = await composeForAgent(profile, mockProjectRoot);
|
|
81
|
+
|
|
82
|
+
expect(result.content).toContain('Planner Strategy');
|
|
83
|
+
expect(result.rolePrompt).toBe(mockRolePrompt);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should include parent context', async () => {
|
|
87
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('# Worker Focus\n');
|
|
88
|
+
|
|
89
|
+
const profile: AgentProfile = {
|
|
90
|
+
name: 'SubWorker',
|
|
91
|
+
role: 'worker',
|
|
92
|
+
parentAgent: 'Lead',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const result = await composeForAgent(profile, mockProjectRoot);
|
|
96
|
+
|
|
97
|
+
expect(result.content).toContain('working under **Lead**');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should include task description', async () => {
|
|
101
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('# Worker Focus\n');
|
|
102
|
+
|
|
103
|
+
const profile: AgentProfile = {
|
|
104
|
+
name: 'Worker',
|
|
105
|
+
role: 'worker',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const result = await composeForAgent(profile, mockProjectRoot, {
|
|
109
|
+
taskDescription: 'Implement user authentication',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(result.content).toContain('Implement user authentication');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should include team members', async () => {
|
|
116
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('# Planner Strategy\n');
|
|
117
|
+
|
|
118
|
+
const profile: AgentProfile = {
|
|
119
|
+
name: 'Lead',
|
|
120
|
+
role: 'planner',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const result = await composeForAgent(profile, mockProjectRoot, {
|
|
124
|
+
teamMembers: ['Backend', 'Frontend', 'Database'],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(result.content).toContain('Backend');
|
|
128
|
+
expect(result.content).toContain('Frontend');
|
|
129
|
+
expect(result.content).toContain('Database');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should include custom prompt', async () => {
|
|
133
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('# Worker Focus\n');
|
|
134
|
+
|
|
135
|
+
const profile: AgentProfile = {
|
|
136
|
+
name: 'SpecialWorker',
|
|
137
|
+
role: 'worker',
|
|
138
|
+
customPrompt: 'Always use TypeScript strict mode.',
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const result = await composeForAgent(profile, mockProjectRoot);
|
|
142
|
+
|
|
143
|
+
expect(result.content).toContain('TypeScript strict mode');
|
|
144
|
+
expect(result.customAdditions).toBe('Always use TypeScript strict mode.');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should handle missing role prompt file', async () => {
|
|
148
|
+
vi.mocked(fs.readFile).mockRejectedValueOnce({ code: 'ENOENT' });
|
|
149
|
+
|
|
150
|
+
const profile: AgentProfile = {
|
|
151
|
+
name: 'Worker',
|
|
152
|
+
role: 'worker',
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const result = await composeForAgent(profile, mockProjectRoot);
|
|
156
|
+
|
|
157
|
+
// Should not throw, just return without role prompt
|
|
158
|
+
expect(result.rolePrompt).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should cache prompt files', async () => {
|
|
162
|
+
const mockPrompt = '# Cached Content\n';
|
|
163
|
+
vi.mocked(fs.readFile).mockResolvedValue(mockPrompt);
|
|
164
|
+
|
|
165
|
+
const profile: AgentProfile = {
|
|
166
|
+
name: 'Worker',
|
|
167
|
+
role: 'worker',
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// First call
|
|
171
|
+
await composeForAgent(profile, mockProjectRoot);
|
|
172
|
+
// Second call should use cache
|
|
173
|
+
await composeForAgent(profile, mockProjectRoot);
|
|
174
|
+
|
|
175
|
+
// readFile should only be called once due to caching
|
|
176
|
+
expect(fs.readFile).toHaveBeenCalledTimes(1);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('getAvailableRoles', () => {
|
|
181
|
+
const mockProjectRoot = '/test/project';
|
|
182
|
+
|
|
183
|
+
it('should return available roles based on files', async () => {
|
|
184
|
+
vi.mocked(fs.readdir).mockResolvedValueOnce([
|
|
185
|
+
'planner-strategy.md',
|
|
186
|
+
'worker-focus.md',
|
|
187
|
+
'reviewer-criteria.md',
|
|
188
|
+
] as any);
|
|
189
|
+
|
|
190
|
+
const roles = await getAvailableRoles(mockProjectRoot);
|
|
191
|
+
|
|
192
|
+
expect(roles).toContain('planner');
|
|
193
|
+
expect(roles).toContain('lead');
|
|
194
|
+
expect(roles).toContain('worker');
|
|
195
|
+
expect(roles).toContain('reviewer');
|
|
196
|
+
expect(roles).toContain('shadow');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should return partial list if some files missing', async () => {
|
|
200
|
+
vi.mocked(fs.readdir).mockResolvedValueOnce([
|
|
201
|
+
'worker-focus.md',
|
|
202
|
+
] as any);
|
|
203
|
+
|
|
204
|
+
const roles = await getAvailableRoles(mockProjectRoot);
|
|
205
|
+
|
|
206
|
+
expect(roles).toContain('worker');
|
|
207
|
+
expect(roles).not.toContain('planner');
|
|
208
|
+
expect(roles).not.toContain('reviewer');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should return empty array if directory missing', async () => {
|
|
212
|
+
vi.mocked(fs.readdir).mockRejectedValueOnce({ code: 'ENOENT' });
|
|
213
|
+
|
|
214
|
+
const roles = await getAvailableRoles(mockProjectRoot);
|
|
215
|
+
|
|
216
|
+
expect(roles).toEqual([]);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Composer
|
|
3
|
+
*
|
|
4
|
+
* Dynamically composes role-specific prompts for agents based on their profile.
|
|
5
|
+
* Loads prompts from .claude/agents/roles/ and injects them into agent context.
|
|
6
|
+
*
|
|
7
|
+
* Part of agent-relay-512: Role-specific prompts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Agent role types that have specific prompts
|
|
15
|
+
*/
|
|
16
|
+
export type AgentRole = 'planner' | 'worker' | 'reviewer' | 'lead' | 'shadow';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Agent profile with role information
|
|
20
|
+
*/
|
|
21
|
+
export interface AgentProfile {
|
|
22
|
+
/** Agent name */
|
|
23
|
+
name: string;
|
|
24
|
+
/** Agent role */
|
|
25
|
+
role?: AgentRole;
|
|
26
|
+
/** Custom prompt overrides */
|
|
27
|
+
customPrompt?: string;
|
|
28
|
+
/** Whether this is a sub-planner */
|
|
29
|
+
isSubPlanner?: boolean;
|
|
30
|
+
/** Parent agent name (for hierarchical context) */
|
|
31
|
+
parentAgent?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Composed prompt result
|
|
36
|
+
*/
|
|
37
|
+
export interface ComposedPrompt {
|
|
38
|
+
/** The full composed prompt */
|
|
39
|
+
content: string;
|
|
40
|
+
/** Role prompt that was used (if any) */
|
|
41
|
+
rolePrompt?: string;
|
|
42
|
+
/** Custom additions */
|
|
43
|
+
customAdditions?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Prompt cache to avoid repeated file reads
|
|
48
|
+
*/
|
|
49
|
+
const promptCache: Map<string, string> = new Map();
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Clear the prompt cache (useful for testing or hot-reload)
|
|
53
|
+
*/
|
|
54
|
+
export function clearPromptCache(): void {
|
|
55
|
+
promptCache.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Map role to prompt file name
|
|
60
|
+
*/
|
|
61
|
+
function getRolePromptFile(role: AgentRole): string {
|
|
62
|
+
switch (role) {
|
|
63
|
+
case 'planner':
|
|
64
|
+
case 'lead':
|
|
65
|
+
return 'planner-strategy.md';
|
|
66
|
+
case 'worker':
|
|
67
|
+
return 'worker-focus.md';
|
|
68
|
+
case 'reviewer':
|
|
69
|
+
case 'shadow':
|
|
70
|
+
return 'reviewer-criteria.md';
|
|
71
|
+
default:
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Load a prompt file from the prompts directory
|
|
78
|
+
*/
|
|
79
|
+
async function loadPromptFile(
|
|
80
|
+
projectRoot: string,
|
|
81
|
+
filename: string
|
|
82
|
+
): Promise<string | undefined> {
|
|
83
|
+
// Check cache first
|
|
84
|
+
const cacheKey = `${projectRoot}:${filename}`;
|
|
85
|
+
if (promptCache.has(cacheKey)) {
|
|
86
|
+
return promptCache.get(cacheKey);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const promptPath = path.join(projectRoot, '.claude', 'agents', 'roles', filename);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const content = await fs.readFile(promptPath, 'utf-8');
|
|
93
|
+
promptCache.set(cacheKey, content);
|
|
94
|
+
return content;
|
|
95
|
+
} catch (err: any) {
|
|
96
|
+
if (err.code !== 'ENOENT') {
|
|
97
|
+
console.warn(`[prompt-composer] Failed to load ${filename}:`, err.message);
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Compose a prompt for an agent based on their profile
|
|
105
|
+
*
|
|
106
|
+
* @param profile - Agent profile with role information
|
|
107
|
+
* @param projectRoot - Project root directory for finding prompt files
|
|
108
|
+
* @param context - Optional additional context to include
|
|
109
|
+
* @returns Composed prompt with role-specific instructions
|
|
110
|
+
*/
|
|
111
|
+
export async function composeForAgent(
|
|
112
|
+
profile: AgentProfile,
|
|
113
|
+
projectRoot: string,
|
|
114
|
+
context?: {
|
|
115
|
+
taskDescription?: string;
|
|
116
|
+
parentContext?: string;
|
|
117
|
+
teamMembers?: string[];
|
|
118
|
+
}
|
|
119
|
+
): Promise<ComposedPrompt> {
|
|
120
|
+
const parts: string[] = [];
|
|
121
|
+
let rolePrompt: string | undefined;
|
|
122
|
+
|
|
123
|
+
// Load role-specific prompt if role is defined
|
|
124
|
+
if (profile.role) {
|
|
125
|
+
const promptFile = getRolePromptFile(profile.role);
|
|
126
|
+
if (promptFile) {
|
|
127
|
+
rolePrompt = await loadPromptFile(projectRoot, promptFile);
|
|
128
|
+
if (rolePrompt) {
|
|
129
|
+
parts.push(rolePrompt);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add hierarchical context for sub-planners or workers
|
|
135
|
+
if (profile.parentAgent) {
|
|
136
|
+
parts.push(`
|
|
137
|
+
## Team Context
|
|
138
|
+
|
|
139
|
+
You are working under **${profile.parentAgent}**.
|
|
140
|
+
Report your progress and blockers to them.
|
|
141
|
+
`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Add task context if provided
|
|
145
|
+
if (context?.taskDescription) {
|
|
146
|
+
parts.push(`
|
|
147
|
+
## Your Current Task
|
|
148
|
+
|
|
149
|
+
${context.taskDescription}
|
|
150
|
+
`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Add team awareness if provided
|
|
154
|
+
if (context?.teamMembers && context.teamMembers.length > 0) {
|
|
155
|
+
parts.push(`
|
|
156
|
+
## Team Members
|
|
157
|
+
|
|
158
|
+
Other agents you can communicate with:
|
|
159
|
+
${context.teamMembers.map(m => `- ${m}`).join('\n')}
|
|
160
|
+
`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Add custom prompt if provided
|
|
164
|
+
if (profile.customPrompt) {
|
|
165
|
+
parts.push(`
|
|
166
|
+
## Additional Instructions
|
|
167
|
+
|
|
168
|
+
${profile.customPrompt}
|
|
169
|
+
`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
content: parts.join('\n\n---\n\n'),
|
|
174
|
+
rolePrompt,
|
|
175
|
+
customAdditions: profile.customPrompt,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get available role prompts in the project
|
|
181
|
+
*/
|
|
182
|
+
export async function getAvailableRoles(projectRoot: string): Promise<AgentRole[]> {
|
|
183
|
+
const rolesDir = path.join(projectRoot, '.claude', 'agents', 'roles');
|
|
184
|
+
const available: AgentRole[] = [];
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const files = await fs.readdir(rolesDir);
|
|
188
|
+
|
|
189
|
+
if (files.includes('planner-strategy.md')) {
|
|
190
|
+
available.push('planner', 'lead');
|
|
191
|
+
}
|
|
192
|
+
if (files.includes('worker-focus.md')) {
|
|
193
|
+
available.push('worker');
|
|
194
|
+
}
|
|
195
|
+
if (files.includes('reviewer-criteria.md')) {
|
|
196
|
+
available.push('reviewer', 'shadow');
|
|
197
|
+
}
|
|
198
|
+
} catch (err: any) {
|
|
199
|
+
if (err.code !== 'ENOENT') {
|
|
200
|
+
console.warn('[prompt-composer] Failed to list roles:', err.message);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return available;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Parse role from agent profile frontmatter
|
|
209
|
+
*
|
|
210
|
+
* @param profileContent - Raw agent profile markdown content
|
|
211
|
+
* @returns Parsed role or undefined
|
|
212
|
+
*/
|
|
213
|
+
export function parseRoleFromProfile(profileContent: string): AgentRole | undefined {
|
|
214
|
+
// Look for role: in frontmatter
|
|
215
|
+
const frontmatterMatch = profileContent.match(/^---\n([\s\S]*?)\n---/);
|
|
216
|
+
if (!frontmatterMatch) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const frontmatter = frontmatterMatch[1];
|
|
221
|
+
const roleMatch = frontmatter.match(/^role:\s*(\w+)/m);
|
|
222
|
+
|
|
223
|
+
if (roleMatch) {
|
|
224
|
+
const role = roleMatch[1].toLowerCase();
|
|
225
|
+
if (['planner', 'worker', 'reviewer', 'lead', 'shadow'].includes(role)) {
|
|
226
|
+
return role as AgentRole;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|