elsabro 6.0.0 → 7.0.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.
@@ -0,0 +1,268 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CallbackProtocol — Agent Teams decision logic + team composition.
5
+ *
6
+ * Encodes the rules for when to use Agent Teams vs regular subagents,
7
+ * and how to compose teams (minimum 5 members, context-aware padding).
8
+ * Does NOT manage team lifecycle — Claude Code handles that automatically.
9
+ */
10
+
11
+ // ---------- Support agent roster for padding ----------
12
+
13
+ const SUPPORT_AGENTS = [
14
+ { id: 'verifier', agent: 'elsabro-verifier', role: 'Quality verification' },
15
+ { id: 'qa', agent: 'elsabro-qa', role: 'Test writing & QA' },
16
+ { id: 'analyst', agent: 'elsabro-analyst', role: 'Requirements analysis' },
17
+ { id: 'debugger', agent: 'elsabro-debugger', role: 'Bug investigation' },
18
+ { id: 'orchestrator', agent: 'elsabro-orchestrator', role: 'Team coordination' },
19
+ { id: 'planner', agent: 'elsabro-planner', role: 'Implementation planning' }
20
+ ];
21
+
22
+ // Context-specific padding priority (index into SUPPORT_AGENTS by id)
23
+ const PADDING_ORDER = {
24
+ implementation: ['verifier', 'analyst', 'debugger', 'qa', 'orchestrator'],
25
+ review: ['verifier', 'qa', 'analyst', 'debugger', 'orchestrator'],
26
+ fix: ['verifier', 'qa', 'analyst', 'orchestrator', 'planner'],
27
+ default: ['verifier', 'qa', 'analyst', 'debugger', 'orchestrator']
28
+ };
29
+
30
+ class CallbackProtocol {
31
+ /**
32
+ * @param {object} [options]
33
+ * @param {string} [options.teamNamePrefix='elsabro'] – prefix for generated team names
34
+ * @param {number} [options.minTeammates=5] – minimum team members
35
+ * @param {object[]} [options.supportAgents] – override support agent roster
36
+ */
37
+ constructor(options = {}) {
38
+ this.teamNamePrefix = options.teamNamePrefix || 'elsabro';
39
+ this.minTeammates = options.minTeammates || 5;
40
+ this.supportAgents = options.supportAgents || SUPPORT_AGENTS;
41
+
42
+ this._activeTeam = null;
43
+ this._teamMembers = [];
44
+ this._log = [];
45
+ }
46
+
47
+ /**
48
+ * Determine if branches require an Agent Team.
49
+ * Rules: 2+ branches AND not all-haiku.
50
+ *
51
+ * @param {object[]} branches – array of branch definitions
52
+ * @returns {boolean}
53
+ */
54
+ requiresTeam(branches) {
55
+ if (!Array.isArray(branches) || branches.length < 2) return false;
56
+
57
+ const allHaiku = branches.every(b =>
58
+ b.config && b.config.model === 'haiku'
59
+ );
60
+
61
+ return !allHaiku;
62
+ }
63
+
64
+ /**
65
+ * Generate a team name from nodeId and optional task slug.
66
+ * Convention: {prefix}-{slug}, max 38 chars.
67
+ *
68
+ * @param {string} nodeId
69
+ * @param {string} [taskSlug]
70
+ * @returns {string}
71
+ */
72
+ generateTeamName(nodeId, taskSlug) {
73
+ const slug = taskSlug
74
+ ? `${nodeId}-${taskSlug}`
75
+ .toLowerCase()
76
+ .replace(/[^a-z0-9-]/g, '-')
77
+ .replace(/-+/g, '-')
78
+ .replace(/^-|-$/g, '')
79
+ : nodeId.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
80
+
81
+ const name = `${this.teamNamePrefix}-${slug}`;
82
+ return name.slice(0, 38);
83
+ }
84
+
85
+ /**
86
+ * Compose a team from branch agents + padding to minimum.
87
+ *
88
+ * @param {object[]} branches – branch definitions with id, agent, config, description
89
+ * @param {string} [nodeContext='default'] – one of: implementation, review, fix, default
90
+ * @returns {{ name: string, agent: string, role: string, model: string }[]}
91
+ */
92
+ composeTeam(branches, nodeContext = 'default') {
93
+ // 1. Start with branch agents
94
+ const members = branches.map((b, i) => ({
95
+ name: `${b.id}-${i + 1}`,
96
+ agent: b.agent,
97
+ role: b.description || `Branch: ${b.id}`,
98
+ model: (b.config && b.config.model) || 'opus'
99
+ }));
100
+
101
+ // 2. Determine padding agents based on context
102
+ const existingAgents = new Set(members.map(m => m.agent));
103
+ const paddingPool = this._getPaddingPool(nodeContext, existingAgents);
104
+
105
+ // 3. Pad to minimum
106
+ let idx = members.length;
107
+ while (members.length < this.minTeammates && paddingPool.length > 0) {
108
+ const support = paddingPool.shift();
109
+ idx++;
110
+ members.push({
111
+ name: `${support.id}-${idx}`,
112
+ agent: support.agent,
113
+ role: support.role,
114
+ model: 'opus'
115
+ });
116
+ }
117
+
118
+ return members;
119
+ }
120
+
121
+ /**
122
+ * Build instruction for a parallel node.
123
+ *
124
+ * @param {string} nodeId
125
+ * @param {object[]} branches
126
+ * @param {object} [context={}] – { nodeContext, taskSlug }
127
+ * @returns {object}
128
+ */
129
+ buildParallelInstruction(nodeId, branches, context = {}) {
130
+ const useAgentTeams = this.requiresTeam(branches);
131
+
132
+ const instruction = {
133
+ type: 'parallel',
134
+ nodeId,
135
+ useAgentTeams,
136
+ branches: branches.map(b => ({
137
+ id: b.id,
138
+ agent: b.agent,
139
+ inputs: b.inputs || {}
140
+ }))
141
+ };
142
+
143
+ if (useAgentTeams) {
144
+ const members = this.composeTeam(branches, context.nodeContext || 'default');
145
+ const teamName = this.generateTeamName(nodeId, context.taskSlug);
146
+
147
+ instruction.team = {
148
+ name: teamName,
149
+ description: `Agent team for ${nodeId}`,
150
+ members
151
+ };
152
+
153
+ this._activeTeam = teamName;
154
+ this._teamMembers = members;
155
+ this._log.push({
156
+ action: 'team_parallel',
157
+ nodeId,
158
+ teamName,
159
+ memberCount: members.length
160
+ });
161
+ } else {
162
+ this._log.push({
163
+ action: 'subagent_parallel',
164
+ nodeId,
165
+ branchCount: branches.length
166
+ });
167
+ }
168
+
169
+ return instruction;
170
+ }
171
+
172
+ /**
173
+ * Build instruction for an agent node.
174
+ */
175
+ buildAgentInstruction(nodeId, agent, config, inputs) {
176
+ this._log.push({ action: 'agent', nodeId, agent });
177
+ return {
178
+ type: 'agent',
179
+ nodeId,
180
+ agent,
181
+ model: (config && config.model) || 'opus',
182
+ inputs: inputs || {}
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Build instruction for an interrupt node.
188
+ */
189
+ buildInterruptInstruction(nodeId, display, routes, reason) {
190
+ this._log.push({ action: 'interrupt', nodeId });
191
+ return {
192
+ type: 'interrupt',
193
+ nodeId,
194
+ display,
195
+ routes: routes || {},
196
+ reason: reason || ''
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Build instruction for a bash command.
202
+ */
203
+ buildBashInstruction(command) {
204
+ return { type: 'bash', command };
205
+ }
206
+
207
+ /**
208
+ * Build instruction for reading files.
209
+ */
210
+ buildReadFilesInstruction(files) {
211
+ return { type: 'read_files', files };
212
+ }
213
+
214
+ /**
215
+ * Build instruction for a sequence node.
216
+ */
217
+ buildSequenceInstruction(nodeId, steps) {
218
+ this._log.push({ action: 'sequence', nodeId, stepCount: (steps || []).length });
219
+ return {
220
+ type: 'sequence',
221
+ nodeId,
222
+ steps: steps || []
223
+ };
224
+ }
225
+
226
+ /** @returns {boolean} */
227
+ hasActiveTeam() {
228
+ return this._activeTeam !== null;
229
+ }
230
+
231
+ /** @returns {string|null} */
232
+ getActiveTeam() {
233
+ return this._activeTeam;
234
+ }
235
+
236
+ /** @returns {object[]} */
237
+ getTeamMembers() {
238
+ return [...this._teamMembers];
239
+ }
240
+
241
+ /** @returns {object[]} */
242
+ getLog() {
243
+ return [...this._log];
244
+ }
245
+
246
+ /** Clear active team state (call after team completes). */
247
+ clearActiveTeam() {
248
+ this._activeTeam = null;
249
+ this._teamMembers = [];
250
+ }
251
+
252
+ // ---------- Internal ----------
253
+
254
+ /**
255
+ * Get ordered list of support agents for padding, excluding those already in the team.
256
+ * @private
257
+ */
258
+ _getPaddingPool(nodeContext, existingAgents) {
259
+ const order = PADDING_ORDER[nodeContext] || PADDING_ORDER.default;
260
+ const supportMap = new Map(this.supportAgents.map(s => [s.id, s]));
261
+
262
+ return order
263
+ .map(id => supportMap.get(id))
264
+ .filter(s => s && !existingAgents.has(s.agent));
265
+ }
266
+ }
267
+
268
+ module.exports = { CallbackProtocol, SUPPORT_AGENTS, PADDING_ORDER };