@agent-relay/spawner 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/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +552 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +193 -0
- package/dist/types.js.map +1 -0
- package/package.json +47 -0
- package/src/index.ts +8 -0
- package/src/types.test.ts +385 -0
- package/src/types.ts +228 -0
package/dist/types.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawner Types
|
|
3
|
+
*
|
|
4
|
+
* Zod schemas for agent spawning and lifecycle management types.
|
|
5
|
+
* These types are used across the spawner, daemon, and dashboard.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Enums and Basic Types
|
|
10
|
+
// =============================================================================
|
|
11
|
+
/**
|
|
12
|
+
* When shadow agents should activate
|
|
13
|
+
*/
|
|
14
|
+
export const SpeakOnTriggerSchema = z.enum([
|
|
15
|
+
'SESSION_END',
|
|
16
|
+
'CODE_WRITTEN',
|
|
17
|
+
'REVIEW_REQUEST',
|
|
18
|
+
'EXPLICIT_ASK',
|
|
19
|
+
'ALL_MESSAGES',
|
|
20
|
+
]);
|
|
21
|
+
/**
|
|
22
|
+
* Shadow role preset names
|
|
23
|
+
*/
|
|
24
|
+
export const ShadowRolePresetSchema = z.enum(['reviewer', 'auditor', 'active']);
|
|
25
|
+
/**
|
|
26
|
+
* Shadow execution mode
|
|
27
|
+
*/
|
|
28
|
+
export const ShadowModeSchema = z.enum(['subagent', 'process']);
|
|
29
|
+
/**
|
|
30
|
+
* Policy source types
|
|
31
|
+
*/
|
|
32
|
+
export const PolicySourceSchema = z.enum(['repo', 'local', 'workspace', 'default']);
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// Policy Types
|
|
35
|
+
// =============================================================================
|
|
36
|
+
/**
|
|
37
|
+
* Policy decision result
|
|
38
|
+
*/
|
|
39
|
+
export const PolicyDecisionSchema = z.object({
|
|
40
|
+
allowed: z.boolean(),
|
|
41
|
+
reason: z.string(),
|
|
42
|
+
policySource: PolicySourceSchema,
|
|
43
|
+
});
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Spawn Request/Result Types
|
|
46
|
+
// =============================================================================
|
|
47
|
+
/**
|
|
48
|
+
* Request to spawn a new agent
|
|
49
|
+
*/
|
|
50
|
+
export const SpawnRequestSchema = z.object({
|
|
51
|
+
/** Worker agent name (must be unique) */
|
|
52
|
+
name: z.string(),
|
|
53
|
+
/** CLI tool (e.g., 'claude', 'claude:opus', 'codex', 'gemini') */
|
|
54
|
+
cli: z.string(),
|
|
55
|
+
/** Initial task to inject after spawn */
|
|
56
|
+
task: z.string(),
|
|
57
|
+
/** Optional team name for organization */
|
|
58
|
+
team: z.string().optional(),
|
|
59
|
+
/** Working directory (defaults to project root) */
|
|
60
|
+
cwd: z.string().optional(),
|
|
61
|
+
/** Name of requesting agent (for policy enforcement) */
|
|
62
|
+
spawnerName: z.string().optional(),
|
|
63
|
+
/** Interactive mode - disables auto-accept of permission prompts */
|
|
64
|
+
interactive: z.boolean().optional(),
|
|
65
|
+
/** Shadow execution mode (subagent = no extra process) */
|
|
66
|
+
shadowMode: ShadowModeSchema.optional(),
|
|
67
|
+
/** Primary agent to shadow (if this agent is a shadow) */
|
|
68
|
+
shadowOf: z.string().optional(),
|
|
69
|
+
/** Shadow agent profile to use (for subagent mode) */
|
|
70
|
+
shadowAgent: z.string().optional(),
|
|
71
|
+
/** When to trigger the shadow (for subagent mode) */
|
|
72
|
+
shadowTriggers: z.array(SpeakOnTriggerSchema).optional(),
|
|
73
|
+
/** When the shadow should speak (default: ['EXPLICIT_ASK']) */
|
|
74
|
+
shadowSpeakOn: z.array(SpeakOnTriggerSchema).optional(),
|
|
75
|
+
/** User ID for per-user credential storage in shared workspaces */
|
|
76
|
+
userId: z.string().optional(),
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* Result of a spawn operation
|
|
80
|
+
*/
|
|
81
|
+
export const SpawnResultSchema = z.object({
|
|
82
|
+
success: z.boolean(),
|
|
83
|
+
name: z.string(),
|
|
84
|
+
/** PID of the spawned process (for pty-based workers) */
|
|
85
|
+
pid: z.number().optional(),
|
|
86
|
+
error: z.string().optional(),
|
|
87
|
+
/** Policy decision details if spawn was blocked by policy */
|
|
88
|
+
policyDecision: PolicyDecisionSchema.optional(),
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* Information about an active worker
|
|
92
|
+
*/
|
|
93
|
+
export const WorkerInfoSchema = z.object({
|
|
94
|
+
name: z.string(),
|
|
95
|
+
cli: z.string(),
|
|
96
|
+
task: z.string(),
|
|
97
|
+
/** Optional team name this agent belongs to */
|
|
98
|
+
team: z.string().optional(),
|
|
99
|
+
spawnedAt: z.number(),
|
|
100
|
+
/** PID of the pty process */
|
|
101
|
+
pid: z.number().optional(),
|
|
102
|
+
});
|
|
103
|
+
// =============================================================================
|
|
104
|
+
// Shadow Agent Types
|
|
105
|
+
// =============================================================================
|
|
106
|
+
/**
|
|
107
|
+
* Primary agent configuration for spawnWithShadow
|
|
108
|
+
*/
|
|
109
|
+
export const PrimaryAgentConfigSchema = z.object({
|
|
110
|
+
/** Agent name */
|
|
111
|
+
name: z.string(),
|
|
112
|
+
/** CLI command (default: 'claude') */
|
|
113
|
+
command: z.string().optional(),
|
|
114
|
+
/** Initial task to send to the agent */
|
|
115
|
+
task: z.string().optional(),
|
|
116
|
+
/** Team name to organize under */
|
|
117
|
+
team: z.string().optional(),
|
|
118
|
+
});
|
|
119
|
+
/**
|
|
120
|
+
* Shadow agent configuration for spawnWithShadow
|
|
121
|
+
*/
|
|
122
|
+
export const ShadowAgentConfigSchema = z.object({
|
|
123
|
+
/** Shadow agent name */
|
|
124
|
+
name: z.string(),
|
|
125
|
+
/** CLI command (default: same as primary) */
|
|
126
|
+
command: z.string().optional(),
|
|
127
|
+
/** Role preset (reviewer, auditor, active) or custom prompt */
|
|
128
|
+
role: z.string().optional(),
|
|
129
|
+
/** Custom speakOn triggers (overrides role preset) */
|
|
130
|
+
speakOn: z.array(SpeakOnTriggerSchema).optional(),
|
|
131
|
+
/** Custom prompt for the shadow agent */
|
|
132
|
+
prompt: z.string().optional(),
|
|
133
|
+
});
|
|
134
|
+
/**
|
|
135
|
+
* Request for spawning a primary agent with its shadow
|
|
136
|
+
*/
|
|
137
|
+
export const SpawnWithShadowRequestSchema = z.object({
|
|
138
|
+
/** Primary agent configuration */
|
|
139
|
+
primary: PrimaryAgentConfigSchema,
|
|
140
|
+
/** Shadow agent configuration */
|
|
141
|
+
shadow: ShadowAgentConfigSchema,
|
|
142
|
+
});
|
|
143
|
+
/**
|
|
144
|
+
* Result from spawnWithShadow
|
|
145
|
+
*/
|
|
146
|
+
export const SpawnWithShadowResultSchema = z.object({
|
|
147
|
+
success: z.boolean(),
|
|
148
|
+
/** Primary agent spawn result */
|
|
149
|
+
primary: SpawnResultSchema.optional(),
|
|
150
|
+
/** Shadow agent spawn result */
|
|
151
|
+
shadow: SpawnResultSchema.optional(),
|
|
152
|
+
/** Error message if overall operation failed */
|
|
153
|
+
error: z.string().optional(),
|
|
154
|
+
});
|
|
155
|
+
// =============================================================================
|
|
156
|
+
// Bridge/Multi-Project Types
|
|
157
|
+
// =============================================================================
|
|
158
|
+
/**
|
|
159
|
+
* Project configuration for multi-project orchestration
|
|
160
|
+
*/
|
|
161
|
+
export const ProjectConfigSchema = z.object({
|
|
162
|
+
/** Absolute path to project root */
|
|
163
|
+
path: z.string(),
|
|
164
|
+
/** Project identifier (derived from path hash) */
|
|
165
|
+
id: z.string(),
|
|
166
|
+
/** Socket path for this project's daemon */
|
|
167
|
+
socketPath: z.string(),
|
|
168
|
+
/** Lead agent name (auto-generated from dirname if not specified) */
|
|
169
|
+
leadName: z.string(),
|
|
170
|
+
/** CLI tool to use (default: claude) */
|
|
171
|
+
cli: z.string(),
|
|
172
|
+
});
|
|
173
|
+
/**
|
|
174
|
+
* Bridge configuration for multi-project coordination
|
|
175
|
+
*/
|
|
176
|
+
export const BridgeConfigSchema = z.object({
|
|
177
|
+
/** Projects to bridge */
|
|
178
|
+
projects: z.array(ProjectConfigSchema),
|
|
179
|
+
/** CLI override for all projects */
|
|
180
|
+
cliOverride: z.string().optional(),
|
|
181
|
+
});
|
|
182
|
+
/**
|
|
183
|
+
* Lead agent information
|
|
184
|
+
*/
|
|
185
|
+
export const LeadInfoSchema = z.object({
|
|
186
|
+
/** Lead agent name */
|
|
187
|
+
name: z.string(),
|
|
188
|
+
/** Project this lead manages */
|
|
189
|
+
projectId: z.string(),
|
|
190
|
+
/** Whether lead is currently connected */
|
|
191
|
+
connected: z.boolean(),
|
|
192
|
+
});
|
|
193
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-relay/spawner",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent spawning types and utilities for Agent Relay",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./types": {
|
|
14
|
+
"types": "./dist/types.d.ts",
|
|
15
|
+
"import": "./dist/types.js",
|
|
16
|
+
"default": "./dist/types.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"clean": "rm -rf dist"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"zod": "^3.23.8"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^5.9.3",
|
|
30
|
+
"vitest": "^3.0.0"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src"
|
|
35
|
+
],
|
|
36
|
+
"keywords": [
|
|
37
|
+
"agent-relay",
|
|
38
|
+
"spawner",
|
|
39
|
+
"agent-lifecycle"
|
|
40
|
+
],
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/AgentWorkforce/relay.git",
|
|
44
|
+
"directory": "packages/spawner"
|
|
45
|
+
},
|
|
46
|
+
"license": "MIT"
|
|
47
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawner Types Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD approach - tests written first to define expected behavior.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
SpeakOnTriggerSchema,
|
|
10
|
+
ShadowRolePresetSchema,
|
|
11
|
+
PolicyDecisionSchema,
|
|
12
|
+
SpawnRequestSchema,
|
|
13
|
+
SpawnResultSchema,
|
|
14
|
+
WorkerInfoSchema,
|
|
15
|
+
PrimaryAgentConfigSchema,
|
|
16
|
+
ShadowAgentConfigSchema,
|
|
17
|
+
SpawnWithShadowRequestSchema,
|
|
18
|
+
SpawnWithShadowResultSchema,
|
|
19
|
+
ProjectConfigSchema,
|
|
20
|
+
BridgeConfigSchema,
|
|
21
|
+
LeadInfoSchema,
|
|
22
|
+
} from './types.js';
|
|
23
|
+
|
|
24
|
+
describe('Spawner Types', () => {
|
|
25
|
+
describe('SpeakOnTriggerSchema', () => {
|
|
26
|
+
it('should validate all trigger types', () => {
|
|
27
|
+
expect(SpeakOnTriggerSchema.parse('SESSION_END')).toBe('SESSION_END');
|
|
28
|
+
expect(SpeakOnTriggerSchema.parse('CODE_WRITTEN')).toBe('CODE_WRITTEN');
|
|
29
|
+
expect(SpeakOnTriggerSchema.parse('REVIEW_REQUEST')).toBe('REVIEW_REQUEST');
|
|
30
|
+
expect(SpeakOnTriggerSchema.parse('EXPLICIT_ASK')).toBe('EXPLICIT_ASK');
|
|
31
|
+
expect(SpeakOnTriggerSchema.parse('ALL_MESSAGES')).toBe('ALL_MESSAGES');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should reject invalid triggers', () => {
|
|
35
|
+
expect(() => SpeakOnTriggerSchema.parse('INVALID')).toThrow();
|
|
36
|
+
expect(() => SpeakOnTriggerSchema.parse('session_end')).toThrow(); // lowercase
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('ShadowRolePresetSchema', () => {
|
|
41
|
+
it('should validate role presets', () => {
|
|
42
|
+
expect(ShadowRolePresetSchema.parse('reviewer')).toBe('reviewer');
|
|
43
|
+
expect(ShadowRolePresetSchema.parse('auditor')).toBe('auditor');
|
|
44
|
+
expect(ShadowRolePresetSchema.parse('active')).toBe('active');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should reject invalid roles', () => {
|
|
48
|
+
expect(() => ShadowRolePresetSchema.parse('observer')).toThrow();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('PolicyDecisionSchema', () => {
|
|
53
|
+
it('should validate allowed decision', () => {
|
|
54
|
+
const decision = {
|
|
55
|
+
allowed: true,
|
|
56
|
+
reason: 'Policy permits spawn',
|
|
57
|
+
policySource: 'repo',
|
|
58
|
+
};
|
|
59
|
+
const result = PolicyDecisionSchema.parse(decision);
|
|
60
|
+
expect(result.allowed).toBe(true);
|
|
61
|
+
expect(result.policySource).toBe('repo');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should validate denied decision', () => {
|
|
65
|
+
const decision = {
|
|
66
|
+
allowed: false,
|
|
67
|
+
reason: 'Agent limit exceeded',
|
|
68
|
+
policySource: 'workspace',
|
|
69
|
+
};
|
|
70
|
+
const result = PolicyDecisionSchema.parse(decision);
|
|
71
|
+
expect(result.allowed).toBe(false);
|
|
72
|
+
expect(result.reason).toBe('Agent limit exceeded');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should validate all policy sources', () => {
|
|
76
|
+
const sources = ['repo', 'local', 'workspace', 'default'];
|
|
77
|
+
for (const source of sources) {
|
|
78
|
+
const decision = { allowed: true, reason: 'test', policySource: source };
|
|
79
|
+
const result = PolicyDecisionSchema.parse(decision);
|
|
80
|
+
expect(result.policySource).toBe(source);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('SpawnRequestSchema', () => {
|
|
86
|
+
it('should validate minimal spawn request', () => {
|
|
87
|
+
const request = {
|
|
88
|
+
name: 'Worker1',
|
|
89
|
+
cli: 'claude',
|
|
90
|
+
task: 'Implement feature X',
|
|
91
|
+
};
|
|
92
|
+
const result = SpawnRequestSchema.parse(request);
|
|
93
|
+
expect(result.name).toBe('Worker1');
|
|
94
|
+
expect(result.cli).toBe('claude');
|
|
95
|
+
expect(result.task).toBe('Implement feature X');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should validate full spawn request', () => {
|
|
99
|
+
const request = {
|
|
100
|
+
name: 'ShadowAgent',
|
|
101
|
+
cli: 'claude:opus',
|
|
102
|
+
task: 'Review code changes',
|
|
103
|
+
team: 'backend',
|
|
104
|
+
cwd: '/workspace/project',
|
|
105
|
+
spawnerName: 'Lead',
|
|
106
|
+
interactive: false,
|
|
107
|
+
shadowMode: 'process',
|
|
108
|
+
shadowOf: 'Primary',
|
|
109
|
+
shadowAgent: 'reviewer',
|
|
110
|
+
shadowTriggers: ['CODE_WRITTEN', 'REVIEW_REQUEST'],
|
|
111
|
+
shadowSpeakOn: ['EXPLICIT_ASK'],
|
|
112
|
+
userId: 'user-123',
|
|
113
|
+
};
|
|
114
|
+
const result = SpawnRequestSchema.parse(request);
|
|
115
|
+
expect(result.shadowMode).toBe('process');
|
|
116
|
+
expect(result.shadowOf).toBe('Primary');
|
|
117
|
+
expect(result.shadowTriggers).toHaveLength(2);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should validate shadow modes', () => {
|
|
121
|
+
const subagent = { name: 'A', cli: 'claude', task: 't', shadowMode: 'subagent' };
|
|
122
|
+
const process = { name: 'B', cli: 'claude', task: 't', shadowMode: 'process' };
|
|
123
|
+
|
|
124
|
+
expect(SpawnRequestSchema.parse(subagent).shadowMode).toBe('subagent');
|
|
125
|
+
expect(SpawnRequestSchema.parse(process).shadowMode).toBe('process');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should reject invalid shadow mode', () => {
|
|
129
|
+
const request = { name: 'A', cli: 'claude', task: 't', shadowMode: 'invalid' };
|
|
130
|
+
expect(() => SpawnRequestSchema.parse(request)).toThrow();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should allow various CLI formats', () => {
|
|
134
|
+
const clis = ['claude', 'claude:opus', 'codex', 'gemini', 'cursor', 'agent'];
|
|
135
|
+
for (const cli of clis) {
|
|
136
|
+
const request = { name: 'Agent', cli, task: 'task' };
|
|
137
|
+
expect(SpawnRequestSchema.parse(request).cli).toBe(cli);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('SpawnResultSchema', () => {
|
|
143
|
+
it('should validate success result', () => {
|
|
144
|
+
const result = {
|
|
145
|
+
success: true,
|
|
146
|
+
name: 'Worker1',
|
|
147
|
+
pid: 12345,
|
|
148
|
+
};
|
|
149
|
+
const parsed = SpawnResultSchema.parse(result);
|
|
150
|
+
expect(parsed.success).toBe(true);
|
|
151
|
+
expect(parsed.pid).toBe(12345);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should validate failure result', () => {
|
|
155
|
+
const result = {
|
|
156
|
+
success: false,
|
|
157
|
+
name: 'Worker1',
|
|
158
|
+
error: 'Agent already exists',
|
|
159
|
+
};
|
|
160
|
+
const parsed = SpawnResultSchema.parse(result);
|
|
161
|
+
expect(parsed.success).toBe(false);
|
|
162
|
+
expect(parsed.error).toBe('Agent already exists');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should validate policy blocked result', () => {
|
|
166
|
+
const result = {
|
|
167
|
+
success: false,
|
|
168
|
+
name: 'Worker1',
|
|
169
|
+
error: 'Policy denied',
|
|
170
|
+
policyDecision: {
|
|
171
|
+
allowed: false,
|
|
172
|
+
reason: 'Spawner not authorized',
|
|
173
|
+
policySource: 'repo',
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
const parsed = SpawnResultSchema.parse(result);
|
|
177
|
+
expect(parsed.policyDecision?.allowed).toBe(false);
|
|
178
|
+
expect(parsed.policyDecision?.policySource).toBe('repo');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('WorkerInfoSchema', () => {
|
|
183
|
+
it('should validate complete worker info', () => {
|
|
184
|
+
const info = {
|
|
185
|
+
name: 'Worker1',
|
|
186
|
+
cli: 'claude',
|
|
187
|
+
task: 'Build API endpoints',
|
|
188
|
+
team: 'backend',
|
|
189
|
+
spawnedAt: 1705920600000,
|
|
190
|
+
pid: 12345,
|
|
191
|
+
};
|
|
192
|
+
const result = WorkerInfoSchema.parse(info);
|
|
193
|
+
expect(result.name).toBe('Worker1');
|
|
194
|
+
expect(result.team).toBe('backend');
|
|
195
|
+
expect(result.pid).toBe(12345);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should allow worker without optional fields', () => {
|
|
199
|
+
const info = {
|
|
200
|
+
name: 'Worker2',
|
|
201
|
+
cli: 'codex',
|
|
202
|
+
task: 'Fix bug',
|
|
203
|
+
spawnedAt: Date.now(),
|
|
204
|
+
};
|
|
205
|
+
const result = WorkerInfoSchema.parse(info);
|
|
206
|
+
expect(result.team).toBeUndefined();
|
|
207
|
+
expect(result.pid).toBeUndefined();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('PrimaryAgentConfigSchema', () => {
|
|
212
|
+
it('should validate minimal config', () => {
|
|
213
|
+
const config = { name: 'Lead' };
|
|
214
|
+
const result = PrimaryAgentConfigSchema.parse(config);
|
|
215
|
+
expect(result.name).toBe('Lead');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should validate full config', () => {
|
|
219
|
+
const config = {
|
|
220
|
+
name: 'Lead',
|
|
221
|
+
command: 'claude:opus',
|
|
222
|
+
task: 'Coordinate team',
|
|
223
|
+
team: 'core',
|
|
224
|
+
};
|
|
225
|
+
const result = PrimaryAgentConfigSchema.parse(config);
|
|
226
|
+
expect(result.command).toBe('claude:opus');
|
|
227
|
+
expect(result.team).toBe('core');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('ShadowAgentConfigSchema', () => {
|
|
232
|
+
it('should validate minimal shadow config', () => {
|
|
233
|
+
const config = { name: 'ShadowReviewer' };
|
|
234
|
+
const result = ShadowAgentConfigSchema.parse(config);
|
|
235
|
+
expect(result.name).toBe('ShadowReviewer');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should validate full shadow config', () => {
|
|
239
|
+
const config = {
|
|
240
|
+
name: 'Auditor',
|
|
241
|
+
command: 'codex',
|
|
242
|
+
role: 'auditor',
|
|
243
|
+
speakOn: ['SESSION_END', 'EXPLICIT_ASK'],
|
|
244
|
+
prompt: 'Review all code changes for security issues',
|
|
245
|
+
};
|
|
246
|
+
const result = ShadowAgentConfigSchema.parse(config);
|
|
247
|
+
expect(result.role).toBe('auditor');
|
|
248
|
+
expect(result.speakOn).toHaveLength(2);
|
|
249
|
+
expect(result.prompt).toContain('security');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should allow custom role string', () => {
|
|
253
|
+
const config = { name: 'Custom', role: 'custom-observer' };
|
|
254
|
+
const result = ShadowAgentConfigSchema.parse(config);
|
|
255
|
+
expect(result.role).toBe('custom-observer');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('SpawnWithShadowRequestSchema', () => {
|
|
260
|
+
it('should validate spawn with shadow request', () => {
|
|
261
|
+
const request = {
|
|
262
|
+
primary: {
|
|
263
|
+
name: 'Lead',
|
|
264
|
+
command: 'claude',
|
|
265
|
+
task: 'Implement feature',
|
|
266
|
+
team: 'core',
|
|
267
|
+
},
|
|
268
|
+
shadow: {
|
|
269
|
+
name: 'Reviewer',
|
|
270
|
+
role: 'reviewer',
|
|
271
|
+
speakOn: ['CODE_WRITTEN'],
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
const result = SpawnWithShadowRequestSchema.parse(request);
|
|
275
|
+
expect(result.primary.name).toBe('Lead');
|
|
276
|
+
expect(result.shadow.name).toBe('Reviewer');
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('SpawnWithShadowResultSchema', () => {
|
|
281
|
+
it('should validate full success result', () => {
|
|
282
|
+
const result = {
|
|
283
|
+
success: true,
|
|
284
|
+
primary: { success: true, name: 'Lead', pid: 1000 },
|
|
285
|
+
shadow: { success: true, name: 'Reviewer', pid: 1001 },
|
|
286
|
+
};
|
|
287
|
+
const parsed = SpawnWithShadowResultSchema.parse(result);
|
|
288
|
+
expect(parsed.primary?.pid).toBe(1000);
|
|
289
|
+
expect(parsed.shadow?.pid).toBe(1001);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should validate partial success (shadow failed)', () => {
|
|
293
|
+
const result = {
|
|
294
|
+
success: true,
|
|
295
|
+
primary: { success: true, name: 'Lead', pid: 1000 },
|
|
296
|
+
shadow: { success: false, name: 'Reviewer', error: 'No authenticated CLI' },
|
|
297
|
+
error: 'Shadow spawn failed',
|
|
298
|
+
};
|
|
299
|
+
const parsed = SpawnWithShadowResultSchema.parse(result);
|
|
300
|
+
expect(parsed.success).toBe(true); // Overall success because primary worked
|
|
301
|
+
expect(parsed.shadow?.success).toBe(false);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should validate failure result', () => {
|
|
305
|
+
const result = {
|
|
306
|
+
success: false,
|
|
307
|
+
error: 'Primary agent spawn failed',
|
|
308
|
+
};
|
|
309
|
+
const parsed = SpawnWithShadowResultSchema.parse(result);
|
|
310
|
+
expect(parsed.success).toBe(false);
|
|
311
|
+
expect(parsed.primary).toBeUndefined();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe('ProjectConfigSchema', () => {
|
|
316
|
+
it('should validate project config', () => {
|
|
317
|
+
const config = {
|
|
318
|
+
path: '/workspace/myproject',
|
|
319
|
+
id: 'proj-abc123',
|
|
320
|
+
socketPath: '/tmp/relay-myproject.sock',
|
|
321
|
+
leadName: 'ProjectLead',
|
|
322
|
+
cli: 'claude',
|
|
323
|
+
};
|
|
324
|
+
const result = ProjectConfigSchema.parse(config);
|
|
325
|
+
expect(result.path).toBe('/workspace/myproject');
|
|
326
|
+
expect(result.leadName).toBe('ProjectLead');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('BridgeConfigSchema', () => {
|
|
331
|
+
it('should validate bridge config', () => {
|
|
332
|
+
const config = {
|
|
333
|
+
projects: [
|
|
334
|
+
{
|
|
335
|
+
path: '/workspace/project1',
|
|
336
|
+
id: 'proj-1',
|
|
337
|
+
socketPath: '/tmp/relay-1.sock',
|
|
338
|
+
leadName: 'Lead1',
|
|
339
|
+
cli: 'claude',
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
path: '/workspace/project2',
|
|
343
|
+
id: 'proj-2',
|
|
344
|
+
socketPath: '/tmp/relay-2.sock',
|
|
345
|
+
leadName: 'Lead2',
|
|
346
|
+
cli: 'codex',
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
cliOverride: 'claude:opus',
|
|
350
|
+
};
|
|
351
|
+
const result = BridgeConfigSchema.parse(config);
|
|
352
|
+
expect(result.projects).toHaveLength(2);
|
|
353
|
+
expect(result.cliOverride).toBe('claude:opus');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should allow bridge config without override', () => {
|
|
357
|
+
const config = { projects: [] };
|
|
358
|
+
const result = BridgeConfigSchema.parse(config);
|
|
359
|
+
expect(result.cliOverride).toBeUndefined();
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe('LeadInfoSchema', () => {
|
|
364
|
+
it('should validate lead info', () => {
|
|
365
|
+
const info = {
|
|
366
|
+
name: 'ProjectLead',
|
|
367
|
+
projectId: 'proj-123',
|
|
368
|
+
connected: true,
|
|
369
|
+
};
|
|
370
|
+
const result = LeadInfoSchema.parse(info);
|
|
371
|
+
expect(result.name).toBe('ProjectLead');
|
|
372
|
+
expect(result.connected).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should validate disconnected lead', () => {
|
|
376
|
+
const info = {
|
|
377
|
+
name: 'OldLead',
|
|
378
|
+
projectId: 'proj-456',
|
|
379
|
+
connected: false,
|
|
380
|
+
};
|
|
381
|
+
const result = LeadInfoSchema.parse(info);
|
|
382
|
+
expect(result.connected).toBe(false);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
});
|