@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.
Files changed (115) hide show
  1. package/dist/__fixtures__/claude-outputs.d.ts +49 -0
  2. package/dist/__fixtures__/claude-outputs.d.ts.map +1 -0
  3. package/dist/__fixtures__/claude-outputs.js +443 -0
  4. package/dist/__fixtures__/claude-outputs.js.map +1 -0
  5. package/dist/__fixtures__/codex-outputs.d.ts +9 -0
  6. package/dist/__fixtures__/codex-outputs.d.ts.map +1 -0
  7. package/dist/__fixtures__/codex-outputs.js +94 -0
  8. package/dist/__fixtures__/codex-outputs.js.map +1 -0
  9. package/dist/__fixtures__/gemini-outputs.d.ts +19 -0
  10. package/dist/__fixtures__/gemini-outputs.d.ts.map +1 -0
  11. package/dist/__fixtures__/gemini-outputs.js +144 -0
  12. package/dist/__fixtures__/gemini-outputs.js.map +1 -0
  13. package/dist/__fixtures__/index.d.ts +68 -0
  14. package/dist/__fixtures__/index.d.ts.map +1 -0
  15. package/dist/__fixtures__/index.js +44 -0
  16. package/dist/__fixtures__/index.js.map +1 -0
  17. package/dist/auth-detection.d.ts +49 -0
  18. package/dist/auth-detection.d.ts.map +1 -0
  19. package/dist/auth-detection.js +199 -0
  20. package/dist/auth-detection.js.map +1 -0
  21. package/dist/base-wrapper.d.ts +225 -0
  22. package/dist/base-wrapper.d.ts.map +1 -0
  23. package/dist/base-wrapper.js +572 -0
  24. package/dist/base-wrapper.js.map +1 -0
  25. package/dist/client.d.ts +254 -0
  26. package/dist/client.d.ts.map +1 -0
  27. package/dist/client.js +801 -0
  28. package/dist/client.js.map +1 -0
  29. package/dist/id-generator.d.ts +35 -0
  30. package/dist/id-generator.d.ts.map +1 -0
  31. package/dist/id-generator.js +60 -0
  32. package/dist/id-generator.js.map +1 -0
  33. package/dist/idle-detector.d.ts +110 -0
  34. package/dist/idle-detector.d.ts.map +1 -0
  35. package/dist/idle-detector.js +304 -0
  36. package/dist/idle-detector.js.map +1 -0
  37. package/dist/inbox.d.ts +37 -0
  38. package/dist/inbox.d.ts.map +1 -0
  39. package/dist/inbox.js +73 -0
  40. package/dist/inbox.js.map +1 -0
  41. package/dist/index.d.ts +37 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +47 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/parser.d.ts +236 -0
  46. package/dist/parser.d.ts.map +1 -0
  47. package/dist/parser.js +1238 -0
  48. package/dist/parser.js.map +1 -0
  49. package/dist/prompt-composer.d.ts +67 -0
  50. package/dist/prompt-composer.d.ts.map +1 -0
  51. package/dist/prompt-composer.js +168 -0
  52. package/dist/prompt-composer.js.map +1 -0
  53. package/dist/relay-pty-orchestrator.d.ts +407 -0
  54. package/dist/relay-pty-orchestrator.d.ts.map +1 -0
  55. package/dist/relay-pty-orchestrator.js +1885 -0
  56. package/dist/relay-pty-orchestrator.js.map +1 -0
  57. package/dist/shared.d.ts +201 -0
  58. package/dist/shared.d.ts.map +1 -0
  59. package/dist/shared.js +341 -0
  60. package/dist/shared.js.map +1 -0
  61. package/dist/stuck-detector.d.ts +161 -0
  62. package/dist/stuck-detector.d.ts.map +1 -0
  63. package/dist/stuck-detector.js +402 -0
  64. package/dist/stuck-detector.js.map +1 -0
  65. package/dist/tmux-resolver.d.ts +55 -0
  66. package/dist/tmux-resolver.d.ts.map +1 -0
  67. package/dist/tmux-resolver.js +175 -0
  68. package/dist/tmux-resolver.js.map +1 -0
  69. package/dist/tmux-wrapper.d.ts +345 -0
  70. package/dist/tmux-wrapper.d.ts.map +1 -0
  71. package/dist/tmux-wrapper.js +1747 -0
  72. package/dist/tmux-wrapper.js.map +1 -0
  73. package/dist/trajectory-integration.d.ts +292 -0
  74. package/dist/trajectory-integration.d.ts.map +1 -0
  75. package/dist/trajectory-integration.js +979 -0
  76. package/dist/trajectory-integration.js.map +1 -0
  77. package/dist/wrapper-types.d.ts +41 -0
  78. package/dist/wrapper-types.d.ts.map +1 -0
  79. package/dist/wrapper-types.js +7 -0
  80. package/dist/wrapper-types.js.map +1 -0
  81. package/package.json +63 -0
  82. package/src/__fixtures__/claude-outputs.ts +471 -0
  83. package/src/__fixtures__/codex-outputs.ts +99 -0
  84. package/src/__fixtures__/gemini-outputs.ts +151 -0
  85. package/src/__fixtures__/index.ts +47 -0
  86. package/src/auth-detection.ts +244 -0
  87. package/src/base-wrapper.test.ts +540 -0
  88. package/src/base-wrapper.ts +741 -0
  89. package/src/client.test.ts +262 -0
  90. package/src/client.ts +984 -0
  91. package/src/id-generator.test.ts +71 -0
  92. package/src/id-generator.ts +69 -0
  93. package/src/idle-detector.test.ts +390 -0
  94. package/src/idle-detector.ts +370 -0
  95. package/src/inbox.test.ts +233 -0
  96. package/src/inbox.ts +89 -0
  97. package/src/index.ts +170 -0
  98. package/src/parser.regression.test.ts +251 -0
  99. package/src/parser.test.ts +1359 -0
  100. package/src/parser.ts +1477 -0
  101. package/src/prompt-composer.test.ts +219 -0
  102. package/src/prompt-composer.ts +231 -0
  103. package/src/relay-pty-orchestrator.test.ts +1027 -0
  104. package/src/relay-pty-orchestrator.ts +2270 -0
  105. package/src/shared.test.ts +221 -0
  106. package/src/shared.ts +454 -0
  107. package/src/stuck-detector.test.ts +303 -0
  108. package/src/stuck-detector.ts +511 -0
  109. package/src/tmux-resolver.test.ts +104 -0
  110. package/src/tmux-resolver.ts +207 -0
  111. package/src/tmux-wrapper.test.ts +316 -0
  112. package/src/tmux-wrapper.ts +2010 -0
  113. package/src/trajectory-detection.test.ts +151 -0
  114. package/src/trajectory-integration.ts +1261 -0
  115. 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
+ }