heyio 3.0.13 → 3.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/api/routes/squads.d.ts.map +1 -1
- package/dist/api/routes/squads.js +1 -0
- package/dist/api/routes/squads.js.map +1 -1
- package/dist/copilot/orchestrator.js +2 -2
- package/dist/copilot/orchestrator.js.map +1 -1
- package/dist/copilot/tools.d.ts +14 -0
- package/dist/copilot/tools.d.ts.map +1 -1
- package/dist/copilot/tools.js +235 -10
- package/dist/copilot/tools.js.map +1 -1
- package/dist/squad/hiring.d.ts +43 -19
- package/dist/squad/hiring.d.ts.map +1 -1
- package/dist/squad/hiring.js +521 -133
- package/dist/squad/hiring.js.map +1 -1
- package/dist/squad/index.d.ts +1 -1
- package/dist/squad/index.d.ts.map +1 -1
- package/dist/squad/index.js +1 -1
- package/dist/squad/index.js.map +1 -1
- package/dist/squad/manager.d.ts +8 -0
- package/dist/squad/manager.d.ts.map +1 -1
- package/dist/squad/manager.js +80 -0
- package/dist/squad/manager.js.map +1 -1
- package/dist/squad/name-generator.d.ts.map +1 -1
- package/dist/squad/name-generator.js +14 -8
- package/dist/squad/name-generator.js.map +1 -1
- package/dist/squad/roles/templates.d.ts +3 -1
- package/dist/squad/roles/templates.d.ts.map +1 -1
- package/dist/squad/roles/templates.js +14 -10
- package/dist/squad/roles/templates.js.map +1 -1
- package/node_modules/@io/shared/package.json +1 -1
- package/package.json +1 -1
- package/public/assets/index-5vJhtAQU.css +1 -0
- package/public/assets/index-nNmZHtrp.js +441 -0
- package/public/assets/index-nNmZHtrp.js.map +1 -0
- package/public/index.html +3 -3
- package/public/assets/index-D3cGfBsj.css +0 -1
- package/public/assets/index-dINUWXx2.js +0 -336
- package/public/assets/index-dINUWXx2.js.map +0 -1
package/dist/squad/hiring.js
CHANGED
|
@@ -1,90 +1,371 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
|
+
import { getClient } from '../copilot/client.js';
|
|
4
5
|
import { createChildLogger } from '../logging/logger.js';
|
|
5
6
|
import { ensureSquadWiki } from '../wiki/index.js';
|
|
6
7
|
import { addMember, createSquad } from './manager.js';
|
|
7
8
|
import { generateSquadNames } from './name-generator.js';
|
|
8
|
-
import { QA_TESTER_SKILL, SCRIBE_SKILL,
|
|
9
|
+
import { QA_TESTER_SKILL, SCRIBE_SKILL, TECHNICAL_PM_SKILL } from './roles/templates.js';
|
|
9
10
|
import { parseSkillContent } from './skill-parser.js';
|
|
10
11
|
const logger = () => createChildLogger('hiring');
|
|
12
|
+
const proposals = new Map();
|
|
13
|
+
export function getProposal(id) {
|
|
14
|
+
return proposals.get(id);
|
|
15
|
+
}
|
|
16
|
+
export function deleteProposal(id) {
|
|
17
|
+
proposals.delete(id);
|
|
18
|
+
}
|
|
19
|
+
// ─── LLM Codebase Analyzer ─────────────────────────────────────────────────
|
|
20
|
+
const ANALYZER_PROMPT = `You are a senior engineering hiring manager. Given a codebase summary, recommend the specialist roles needed for a high-performing engineering squad.
|
|
21
|
+
|
|
22
|
+
Rules:
|
|
23
|
+
- Roles must be SENIOR or PRINCIPAL level — no junior, mid-level, or generic titles
|
|
24
|
+
- Roles must be SPECIFIC to the technology stack (e.g., "Senior React/Vite Engineer", not "Frontend Developer")
|
|
25
|
+
- Recommend 2-5 specialist roles depending on project complexity
|
|
26
|
+
- Consider: languages, frameworks, architecture patterns, testing needs, deployment complexity
|
|
27
|
+
- Include a justification for each role explaining WHY the project needs that specific specialist
|
|
28
|
+
- Decide whether QA and Testing should be separate roles based on project size/complexity. For small projects, one QA/Test Engineer suffices. For large projects with multiple test layers (unit, integration, e2e, performance), recommend separate QA Engineer and Tester.
|
|
29
|
+
- Return ONLY valid JSON, no markdown fencing
|
|
30
|
+
|
|
31
|
+
Respond with this exact JSON structure:
|
|
32
|
+
{
|
|
33
|
+
"specialists": [
|
|
34
|
+
{ "role": "<kebab-case-role-id>", "title": "<Senior/Principal Level Title>", "justification": "<why this project needs this role>" }
|
|
35
|
+
],
|
|
36
|
+
"separateQaAndTester": true/false,
|
|
37
|
+
"qaJustification": "<why QA and Tester should or should not be separate>"
|
|
38
|
+
}`;
|
|
11
39
|
/**
|
|
12
|
-
*
|
|
40
|
+
* Sample key files from the project to build a codebase summary for the LLM.
|
|
13
41
|
*/
|
|
14
|
-
|
|
42
|
+
function sampleCodebase(projectPath) {
|
|
15
43
|
const name = basename(projectPath);
|
|
16
|
-
const languages = [];
|
|
17
|
-
const frameworks = [];
|
|
18
|
-
let hasTests = false;
|
|
19
|
-
let hasCi = false;
|
|
20
|
-
// Check for common project indicators
|
|
21
44
|
const files = safeReadDir(projectPath);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
const readmeFile = files.find((f) => f.toLowerCase().startsWith('readme'));
|
|
46
|
+
const readme = readmeFile ? safeReadFile(join(projectPath, readmeFile), 4000) : '';
|
|
47
|
+
const manifests = collectManifests(projectPath, files);
|
|
48
|
+
const configFiles = detectConfigFiles(files);
|
|
49
|
+
const directoryTree = buildDirectoryTree(projectPath, 2, 60);
|
|
50
|
+
const ciFiles = collectCiFiles(projectPath);
|
|
51
|
+
return { name, readme, manifests, configFiles, directoryTree, ciFiles };
|
|
52
|
+
}
|
|
53
|
+
const MANIFEST_FILES = [
|
|
54
|
+
'package.json',
|
|
55
|
+
'Cargo.toml',
|
|
56
|
+
'go.mod',
|
|
57
|
+
'pyproject.toml',
|
|
58
|
+
'requirements.txt',
|
|
59
|
+
'Gemfile',
|
|
60
|
+
'pom.xml',
|
|
61
|
+
'build.gradle',
|
|
62
|
+
];
|
|
63
|
+
function collectManifests(projectPath, files) {
|
|
64
|
+
const manifests = {};
|
|
65
|
+
for (const mf of MANIFEST_FILES) {
|
|
66
|
+
if (files.includes(mf)) {
|
|
67
|
+
manifests[mf] = safeReadFile(join(projectPath, mf), 2000);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Check for workspace/monorepo manifests
|
|
71
|
+
for (const sub of ['packages', 'apps', 'libs']) {
|
|
72
|
+
const subPath = join(projectPath, sub);
|
|
73
|
+
if (!existsSync(subPath))
|
|
74
|
+
continue;
|
|
75
|
+
const subContents = safeReadDir(subPath);
|
|
76
|
+
for (const pkg of subContents.slice(0, 8)) {
|
|
77
|
+
const pkgManifest = join(subPath, pkg, 'package.json');
|
|
78
|
+
if (existsSync(pkgManifest)) {
|
|
79
|
+
manifests[`${sub}/${pkg}/package.json`] = safeReadFile(pkgManifest, 1000);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return manifests;
|
|
84
|
+
}
|
|
85
|
+
const CONFIG_PATTERNS = [
|
|
86
|
+
'tsconfig.json',
|
|
87
|
+
'vite.config',
|
|
88
|
+
'webpack.config',
|
|
89
|
+
'next.config',
|
|
90
|
+
'tailwind.config',
|
|
91
|
+
'docker-compose',
|
|
92
|
+
'Dockerfile',
|
|
93
|
+
'.env.example',
|
|
94
|
+
'biome.json',
|
|
95
|
+
'eslint',
|
|
96
|
+
];
|
|
97
|
+
function detectConfigFiles(files) {
|
|
98
|
+
return files.filter((f) => CONFIG_PATTERNS.some((p) => f.toLowerCase().includes(p.toLowerCase())));
|
|
99
|
+
}
|
|
100
|
+
function collectCiFiles(projectPath) {
|
|
101
|
+
const ciFiles = [];
|
|
102
|
+
const ghWorkflows = join(projectPath, '.github', 'workflows');
|
|
103
|
+
if (existsSync(ghWorkflows)) {
|
|
104
|
+
const workflows = safeReadDir(ghWorkflows);
|
|
105
|
+
for (const wf of workflows.slice(0, 3)) {
|
|
106
|
+
ciFiles.push(safeReadFile(join(ghWorkflows, wf), 1000));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return ciFiles;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Use the LLM to analyze a codebase and recommend specialist roles.
|
|
113
|
+
*/
|
|
114
|
+
async function analyzeCodebase(projectPath) {
|
|
115
|
+
const log = logger();
|
|
116
|
+
const summary = sampleCodebase(projectPath);
|
|
117
|
+
const userMessage = `Here is a summary of the "${summary.name}" project:
|
|
118
|
+
|
|
119
|
+
## README (truncated)
|
|
120
|
+
${summary.readme || '(no README found)'}
|
|
121
|
+
|
|
122
|
+
## Package Manifests
|
|
123
|
+
${Object.entries(summary.manifests)
|
|
124
|
+
.map(([f, content]) => `### ${f}\n\`\`\`\n${content}\n\`\`\``)
|
|
125
|
+
.join('\n\n')}
|
|
126
|
+
|
|
127
|
+
## Config Files Present
|
|
128
|
+
${summary.configFiles.join(', ') || '(none detected)'}
|
|
129
|
+
|
|
130
|
+
## Directory Structure
|
|
131
|
+
\`\`\`
|
|
132
|
+
${summary.directoryTree}
|
|
133
|
+
\`\`\`
|
|
134
|
+
|
|
135
|
+
## CI/CD Workflows
|
|
136
|
+
${summary.ciFiles.length > 0 ? summary.ciFiles.map((c) => `\`\`\`yaml\n${c}\n\`\`\``).join('\n') : '(none found)'}
|
|
137
|
+
|
|
138
|
+
Based on this codebase, what senior/principal-level specialist roles does this project need?`;
|
|
139
|
+
try {
|
|
140
|
+
const client = await getClient();
|
|
141
|
+
const session = await client.createSession({
|
|
142
|
+
systemMessage: { mode: 'replace', content: ANALYZER_PROMPT },
|
|
143
|
+
});
|
|
144
|
+
let accumulated = '';
|
|
145
|
+
const unsubDelta = session.on('assistant.message_delta', (event) => {
|
|
146
|
+
accumulated += event.data.deltaContent;
|
|
147
|
+
});
|
|
148
|
+
try {
|
|
149
|
+
await session.sendAndWait({ prompt: userMessage }, 90_000);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
unsubDelta();
|
|
42
153
|
}
|
|
154
|
+
const parsed = extractJson(accumulated);
|
|
155
|
+
if (!parsed || !Array.isArray(parsed.specialists)) {
|
|
156
|
+
throw new Error('Invalid LLM response for codebase analysis');
|
|
157
|
+
}
|
|
158
|
+
log.info({
|
|
159
|
+
project: summary.name,
|
|
160
|
+
specialists: parsed.specialists.map((s) => s.title),
|
|
161
|
+
separateQa: parsed.separateQaAndTester,
|
|
162
|
+
}, 'Codebase analyzed');
|
|
163
|
+
return {
|
|
164
|
+
specialists: parsed.specialists,
|
|
165
|
+
separateQaAndTester: !!parsed.separateQaAndTester,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
log.error({ err }, 'LLM codebase analysis failed, using fallback heuristics');
|
|
170
|
+
return fallbackAnalysis(projectPath);
|
|
43
171
|
}
|
|
44
|
-
if (files.includes('Cargo.toml'))
|
|
45
|
-
languages.push('Rust');
|
|
46
|
-
if (files.includes('go.mod'))
|
|
47
|
-
languages.push('Go');
|
|
48
|
-
if (files.includes('requirements.txt') || files.includes('pyproject.toml'))
|
|
49
|
-
languages.push('Python');
|
|
50
|
-
if (files.includes('Gemfile'))
|
|
51
|
-
languages.push('Ruby');
|
|
52
|
-
if (files.some((f) => f.endsWith('.csproj') || f.endsWith('.sln')))
|
|
53
|
-
languages.push('C#/.NET');
|
|
54
|
-
// CI detection
|
|
55
|
-
if (files.includes('.github')) {
|
|
56
|
-
const ghDir = safeReadDir(join(projectPath, '.github'));
|
|
57
|
-
if (ghDir.includes('workflows'))
|
|
58
|
-
hasCi = true;
|
|
59
|
-
}
|
|
60
|
-
// Suggest specialists based on detected tech
|
|
61
|
-
const suggestedSpecialists = [];
|
|
62
|
-
if (frameworks.includes('React') || frameworks.includes('Vue') || frameworks.includes('Svelte'))
|
|
63
|
-
suggestedSpecialists.push('frontend-developer');
|
|
64
|
-
if (frameworks.includes('Node.js Backend'))
|
|
65
|
-
suggestedSpecialists.push('backend-developer');
|
|
66
|
-
if (languages.includes('Rust'))
|
|
67
|
-
suggestedSpecialists.push('rust-developer');
|
|
68
|
-
if (languages.includes('Go'))
|
|
69
|
-
suggestedSpecialists.push('go-developer');
|
|
70
|
-
if (languages.includes('Python'))
|
|
71
|
-
suggestedSpecialists.push('python-developer');
|
|
72
|
-
if (languages.includes('C#/.NET'))
|
|
73
|
-
suggestedSpecialists.push('dotnet-developer');
|
|
74
|
-
// If no specialists detected, add a generic one
|
|
75
|
-
if (suggestedSpecialists.length === 0 && languages.length > 0) {
|
|
76
|
-
suggestedSpecialists.push('developer');
|
|
77
|
-
}
|
|
78
|
-
return { name, languages, frameworks, hasTests, hasCi, suggestedSpecialists };
|
|
79
172
|
}
|
|
173
|
+
// ─── Propose / Confirm Flow ────────────────────────────────────────────────
|
|
80
174
|
/**
|
|
81
|
-
*
|
|
175
|
+
* Propose a squad: clone, analyze, generate names, store proposal for review.
|
|
82
176
|
*/
|
|
83
|
-
function
|
|
84
|
-
const
|
|
85
|
-
const
|
|
177
|
+
export async function proposeSquad(params) {
|
|
178
|
+
const log = logger();
|
|
179
|
+
const projectName = params.name ?? basename(params.projectPath);
|
|
180
|
+
// 1. Analyze with LLM
|
|
181
|
+
const analysis = await analyzeCodebase(params.projectPath);
|
|
182
|
+
// 2. Build member list — core roles + specialists
|
|
183
|
+
const members = [
|
|
184
|
+
{
|
|
185
|
+
role: 'technical-pm',
|
|
186
|
+
title: 'Technical PM',
|
|
187
|
+
justification: 'Coordinates the team, makes architectural decisions, reviews all work',
|
|
188
|
+
isCore: true,
|
|
189
|
+
veto: true,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
role: 'scribe',
|
|
193
|
+
title: 'Scribe',
|
|
194
|
+
justification: 'Records decisions, maintains documentation, writes PR descriptions',
|
|
195
|
+
isCore: true,
|
|
196
|
+
veto: false,
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
if (analysis.separateQaAndTester) {
|
|
200
|
+
members.push({
|
|
201
|
+
role: 'qa-engineer',
|
|
202
|
+
title: 'QA Engineer',
|
|
203
|
+
justification: 'Quality gate — reviews code for edge cases, security issues, and test coverage',
|
|
204
|
+
isCore: true,
|
|
205
|
+
veto: true,
|
|
206
|
+
});
|
|
207
|
+
members.push({
|
|
208
|
+
role: 'tester',
|
|
209
|
+
title: 'Tester',
|
|
210
|
+
justification: 'Functional/integration testing, CI/CD pipeline verification, test automation',
|
|
211
|
+
isCore: true,
|
|
212
|
+
veto: false,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
members.push({
|
|
217
|
+
role: 'qa-tester',
|
|
218
|
+
title: 'QA/Test Engineer',
|
|
219
|
+
justification: 'Quality gate — writes tests, reviews for edge cases, blocks bad merges',
|
|
220
|
+
isCore: true,
|
|
221
|
+
veto: true,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
for (const spec of analysis.specialists) {
|
|
225
|
+
members.push({
|
|
226
|
+
role: spec.role,
|
|
227
|
+
title: spec.title,
|
|
228
|
+
justification: spec.justification,
|
|
229
|
+
isCore: false,
|
|
230
|
+
veto: false,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
// 3. Generate character names
|
|
234
|
+
const allRoles = members.map((m) => m.title);
|
|
235
|
+
const generated = await generateSquadNames(allRoles, params.universe);
|
|
236
|
+
// Assign names to members
|
|
237
|
+
for (const member of members) {
|
|
238
|
+
const assignment = generated.assignments.find((a) => a.role.toLowerCase() === member.title.toLowerCase());
|
|
239
|
+
if (assignment) {
|
|
240
|
+
member.displayName = assignment.displayName;
|
|
241
|
+
member.persona = assignment.persona;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// 4. Store proposal
|
|
245
|
+
const proposalId = crypto.randomUUID();
|
|
246
|
+
const proposal = {
|
|
247
|
+
id: proposalId,
|
|
248
|
+
repoUrl: params.repoUrl ?? '',
|
|
249
|
+
projectPath: params.projectPath,
|
|
250
|
+
projectName,
|
|
251
|
+
universe: generated.universe,
|
|
252
|
+
members,
|
|
253
|
+
createdAt: Date.now(),
|
|
254
|
+
};
|
|
255
|
+
proposals.set(proposalId, proposal);
|
|
256
|
+
log.info({
|
|
257
|
+
proposalId,
|
|
258
|
+
projectName,
|
|
259
|
+
universe: generated.universe,
|
|
260
|
+
memberCount: members.length,
|
|
261
|
+
}, 'Squad proposal created');
|
|
262
|
+
return proposal;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Confirm a squad proposal and create the actual squad.
|
|
266
|
+
*/
|
|
267
|
+
export async function confirmSquad(params) {
|
|
268
|
+
const log = logger();
|
|
269
|
+
const proposal = proposals.get(params.proposalId);
|
|
270
|
+
if (!proposal) {
|
|
271
|
+
throw new Error(`Proposal '${params.proposalId}' not found or has expired.`);
|
|
272
|
+
}
|
|
273
|
+
const squadName = params.name ?? proposal.projectName;
|
|
274
|
+
const removedSet = new Set(params.removedRoles?.map((r) => r.toLowerCase()) ?? []);
|
|
275
|
+
const finalMembers = proposal.members.filter((m) => !removedSet.has(m.role.toLowerCase()));
|
|
276
|
+
// 1. Create skill files directory
|
|
277
|
+
const skillsDir = join(homedir(), '.io', 'squads', squadName);
|
|
278
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
279
|
+
// 2. Create the squad
|
|
280
|
+
const squad = await createSquad({
|
|
281
|
+
name: squadName,
|
|
282
|
+
projectPath: proposal.projectPath,
|
|
283
|
+
repoUrl: proposal.repoUrl || undefined,
|
|
284
|
+
universe: proposal.universe,
|
|
285
|
+
});
|
|
286
|
+
// 3. Create wiki
|
|
287
|
+
ensureSquadWiki(squadName);
|
|
288
|
+
// 4. Write skill files and add members
|
|
289
|
+
const memberRoles = [];
|
|
290
|
+
for (const member of finalMembers) {
|
|
291
|
+
const skillContent = generateSkillForMember(member, proposal.projectPath);
|
|
292
|
+
const filePath = join(skillsDir, `${member.role}.skill.md`);
|
|
293
|
+
writeFileSync(filePath, skillContent, 'utf-8');
|
|
294
|
+
const skill = parseSkillContent(skillContent, filePath);
|
|
295
|
+
await addMember({
|
|
296
|
+
squadId: squad.id,
|
|
297
|
+
skill,
|
|
298
|
+
displayName: member.displayName ?? member.title,
|
|
299
|
+
persona: member.persona,
|
|
300
|
+
isVetoMember: member.veto,
|
|
301
|
+
});
|
|
302
|
+
memberRoles.push(`${member.displayName ?? member.title} (${member.title})`);
|
|
303
|
+
}
|
|
304
|
+
// 5. Clean up proposal
|
|
305
|
+
proposals.delete(params.proposalId);
|
|
306
|
+
log.info({ squadId: squad.id, members: memberRoles, universe: proposal.universe }, 'Squad confirmed and created');
|
|
307
|
+
return { squadId: squad.id, members: memberRoles, universe: proposal.universe };
|
|
308
|
+
}
|
|
309
|
+
// ─── Legacy API (kept for addMemberToExistingSquad) ─────────────────────────
|
|
310
|
+
/**
|
|
311
|
+
* Add a new member to an existing squad.
|
|
312
|
+
* Generates a skill file and optionally themes the name to the squad's universe.
|
|
313
|
+
*/
|
|
314
|
+
export async function addMemberToExistingSquad(params) {
|
|
315
|
+
const log = logger();
|
|
316
|
+
const member = {
|
|
317
|
+
role: params.role,
|
|
318
|
+
title: titleCase(params.role),
|
|
319
|
+
justification: '',
|
|
320
|
+
isCore: false,
|
|
321
|
+
veto: params.role.includes('qa'),
|
|
322
|
+
};
|
|
323
|
+
const skillContent = generateSkillForMember(member, params.projectPath);
|
|
324
|
+
const skillsDir = join(homedir(), '.io', 'squads', params.squadName);
|
|
325
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
326
|
+
const filePath = join(skillsDir, `${params.role}.skill.md`);
|
|
327
|
+
writeFileSync(filePath, skillContent, 'utf-8');
|
|
328
|
+
const skill = parseSkillContent(skillContent, filePath);
|
|
329
|
+
// Generate themed name if universe is set
|
|
330
|
+
let displayName = member.title;
|
|
331
|
+
let persona;
|
|
332
|
+
if (params.universe) {
|
|
333
|
+
const generated = await generateSquadNames([params.role], params.universe);
|
|
334
|
+
const assignment = generated.assignments[0];
|
|
335
|
+
if (assignment) {
|
|
336
|
+
displayName = assignment.displayName;
|
|
337
|
+
persona = assignment.persona;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
await addMember({
|
|
341
|
+
squadId: params.squadId,
|
|
342
|
+
skill,
|
|
343
|
+
displayName,
|
|
344
|
+
persona,
|
|
345
|
+
isVetoMember: member.veto,
|
|
346
|
+
});
|
|
347
|
+
log.info({ squadId: params.squadId, role: params.role, displayName }, 'Member added to squad');
|
|
348
|
+
return { displayName, role: params.role };
|
|
349
|
+
}
|
|
350
|
+
// ─── Skill Generation ───────────────────────────────────────────────────────
|
|
351
|
+
function generateSkillForMember(member, projectPath) {
|
|
352
|
+
// Core roles use built-in templates
|
|
353
|
+
if (member.role === 'technical-pm')
|
|
354
|
+
return TECHNICAL_PM_SKILL;
|
|
355
|
+
if (member.role === 'scribe')
|
|
356
|
+
return SCRIBE_SKILL;
|
|
357
|
+
if (member.role === 'qa-tester' || member.role === 'qa-engineer')
|
|
358
|
+
return QA_TESTER_SKILL;
|
|
359
|
+
// Tester role (separate from QA)
|
|
360
|
+
if (member.role === 'tester') {
|
|
361
|
+
return generateTesterSkill();
|
|
362
|
+
}
|
|
363
|
+
// Specialist roles get generated skills
|
|
364
|
+
return generateSpecialistSkill(member, projectPath);
|
|
365
|
+
}
|
|
366
|
+
function generateTesterSkill() {
|
|
86
367
|
return `---
|
|
87
|
-
role:
|
|
368
|
+
role: tester
|
|
88
369
|
tools:
|
|
89
370
|
- read_file
|
|
90
371
|
- edit_file
|
|
@@ -93,84 +374,81 @@ tools:
|
|
|
93
374
|
veto: false
|
|
94
375
|
---
|
|
95
376
|
|
|
96
|
-
#
|
|
377
|
+
# Tester
|
|
97
378
|
|
|
98
379
|
## Identity
|
|
99
|
-
You are
|
|
380
|
+
You are the Tester — responsible for functional testing, integration testing, and CI/CD pipeline health.
|
|
100
381
|
|
|
101
382
|
## Responsibilities
|
|
102
|
-
-
|
|
383
|
+
- Write and maintain integration and end-to-end tests
|
|
384
|
+
- Verify feature implementations against acceptance criteria
|
|
385
|
+
- Run test suites and report failures with clear reproduction steps
|
|
386
|
+
- Monitor CI/CD pipeline health and investigate flaky tests
|
|
387
|
+
- Set up test infrastructure (fixtures, mocks, test databases)
|
|
388
|
+
|
|
389
|
+
## Boundaries
|
|
390
|
+
- You focus on test code and test infrastructure
|
|
391
|
+
- You do NOT write production features
|
|
392
|
+
- You work alongside the QA Engineer but focus on automation over manual review
|
|
393
|
+
- You ensure CI stays green before any merge
|
|
394
|
+
|
|
395
|
+
## Standards
|
|
396
|
+
- All new features must have integration tests
|
|
397
|
+
- Flaky tests must be identified and either fixed or quarantined
|
|
398
|
+
- Test runs must be reproducible and fast
|
|
399
|
+
- CI/CD pipeline failures are your top priority
|
|
400
|
+
`;
|
|
401
|
+
}
|
|
402
|
+
function generateSpecialistSkill(member, projectPath) {
|
|
403
|
+
const summary = sampleCodebase(projectPath);
|
|
404
|
+
const langContext = Object.keys(summary.manifests)
|
|
405
|
+
.map((f) => {
|
|
406
|
+
if (f.includes('package.json'))
|
|
407
|
+
return 'TypeScript/JavaScript';
|
|
408
|
+
if (f.includes('Cargo.toml'))
|
|
409
|
+
return 'Rust';
|
|
410
|
+
if (f.includes('go.mod'))
|
|
411
|
+
return 'Go';
|
|
412
|
+
if (f.includes('pyproject.toml') || f.includes('requirements.txt'))
|
|
413
|
+
return 'Python';
|
|
414
|
+
return null;
|
|
415
|
+
})
|
|
416
|
+
.filter(Boolean)
|
|
417
|
+
.join(', ');
|
|
418
|
+
return `---
|
|
419
|
+
role: ${member.role}
|
|
420
|
+
tools:
|
|
421
|
+
- read_file
|
|
422
|
+
- edit_file
|
|
423
|
+
- run_command
|
|
424
|
+
- search_code
|
|
425
|
+
veto: false
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
# ${member.title}
|
|
429
|
+
|
|
430
|
+
## Identity
|
|
431
|
+
You are a ${member.title} — a senior/principal-level specialist.
|
|
432
|
+
${member.justification ? `Hired because: ${member.justification}` : ''}
|
|
433
|
+
|
|
434
|
+
## Responsibilities
|
|
435
|
+
- Implement features and fix bugs within your area of expertise
|
|
103
436
|
- Write clean, well-tested code following project conventions
|
|
437
|
+
- Provide expert-level guidance on your specialty area during team discussions
|
|
104
438
|
- Run tests before submitting work for review
|
|
105
|
-
-
|
|
439
|
+
- Mentor other team members on best practices in your domain
|
|
106
440
|
|
|
107
441
|
## Boundaries
|
|
108
|
-
- Only work on tasks assigned to you by the
|
|
442
|
+
- Only work on tasks assigned to you by the Technical PM
|
|
109
443
|
- Do NOT modify files outside your area of expertise unless directed
|
|
110
|
-
- Do NOT merge PRs — submit work for
|
|
444
|
+
- Do NOT merge PRs — submit work for Technical PM review
|
|
111
445
|
- Always run the test suite before reporting task completion
|
|
112
446
|
|
|
113
447
|
## Project Context
|
|
114
|
-
Languages: ${langContext}
|
|
448
|
+
${langContext ? `Languages: ${langContext}` : ''}
|
|
115
449
|
`;
|
|
116
450
|
}
|
|
117
|
-
|
|
118
|
-
* Execute the full squad hiring flow:
|
|
119
|
-
* 1. Analyze the project
|
|
120
|
-
* 2. Create the squad in DB
|
|
121
|
-
* 3. Write SKILL.md files to disk
|
|
122
|
-
* 4. Add all members
|
|
123
|
-
*/
|
|
124
|
-
export async function hireSquad(params) {
|
|
125
|
-
const log = logger();
|
|
126
|
-
// 1. Analyze
|
|
127
|
-
const analysis = analyzeProject(params.projectPath);
|
|
128
|
-
const squadName = params.name ?? analysis.name;
|
|
129
|
-
log.info({ squadName, analysis }, 'Project analyzed');
|
|
130
|
-
// 2. Build skill files
|
|
131
|
-
const skillsDir = join(homedir(), '.io', 'squads', squadName);
|
|
132
|
-
mkdirSync(skillsDir, { recursive: true });
|
|
133
|
-
const skillFiles = [
|
|
134
|
-
{ role: 'team-lead', content: TEAM_LEAD_SKILL, veto: true },
|
|
135
|
-
{ role: 'scribe', content: SCRIBE_SKILL, veto: false },
|
|
136
|
-
{ role: 'qa-tester', content: QA_TESTER_SKILL, veto: true },
|
|
137
|
-
];
|
|
138
|
-
for (const specialist of analysis.suggestedSpecialists) {
|
|
139
|
-
skillFiles.push({
|
|
140
|
-
role: specialist,
|
|
141
|
-
content: generateSpecialistSkill(specialist, analysis),
|
|
142
|
-
veto: false,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
// 3. Generate character names from universe via LLM
|
|
146
|
-
const allRoles = skillFiles.map((f) => f.role);
|
|
147
|
-
const generated = await generateSquadNames(allRoles, params.universe);
|
|
148
|
-
log.info({ squadName, universe: generated.universe }, 'Universe names generated');
|
|
149
|
-
// 4. Create squad
|
|
150
|
-
const squad = await createSquad({
|
|
151
|
-
name: squadName,
|
|
152
|
-
projectPath: params.projectPath,
|
|
153
|
-
repoUrl: params.repoUrl,
|
|
154
|
-
universe: generated.universe,
|
|
155
|
-
});
|
|
156
|
-
// 5. Create wiki folder for this squad
|
|
157
|
-
ensureSquadWiki(squadName);
|
|
158
|
-
// 6. Write files and add members
|
|
159
|
-
const memberRoles = [];
|
|
160
|
-
for (const { role, content, veto } of skillFiles) {
|
|
161
|
-
const filePath = join(skillsDir, `${role}.skill.md`);
|
|
162
|
-
writeFileSync(filePath, content, 'utf-8');
|
|
163
|
-
const skill = parseSkillContent(content, filePath);
|
|
164
|
-
const assignment = generated.assignments.find((a) => a.role === role);
|
|
165
|
-
const displayName = assignment?.displayName ?? role;
|
|
166
|
-
const persona = assignment?.persona;
|
|
167
|
-
await addMember({ squadId: squad.id, skill, displayName, persona, isVetoMember: veto });
|
|
168
|
-
memberRoles.push(`${displayName} (${role})`);
|
|
169
|
-
}
|
|
170
|
-
log.info({ squadId: squad.id, members: memberRoles, universe: generated.universe }, 'Squad hired successfully');
|
|
171
|
-
return { squadId: squad.id, analysis, members: memberRoles, universe: generated.universe };
|
|
172
|
-
}
|
|
173
|
-
// Helpers
|
|
451
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
174
452
|
function safeReadDir(dir) {
|
|
175
453
|
try {
|
|
176
454
|
if (!existsSync(dir))
|
|
@@ -181,6 +459,116 @@ function safeReadDir(dir) {
|
|
|
181
459
|
return [];
|
|
182
460
|
}
|
|
183
461
|
}
|
|
462
|
+
function safeReadFile(path, maxLength) {
|
|
463
|
+
try {
|
|
464
|
+
if (!existsSync(path))
|
|
465
|
+
return '';
|
|
466
|
+
const content = readFileSync(path, 'utf-8');
|
|
467
|
+
return content.length > maxLength ? `${content.slice(0, maxLength)}\n...(truncated)` : content;
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return '';
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function buildDirectoryTree(rootPath, maxDepth, maxEntries) {
|
|
474
|
+
const lines = [];
|
|
475
|
+
function walk(dir, prefix, depth) {
|
|
476
|
+
if (depth > maxDepth || lines.length >= maxEntries)
|
|
477
|
+
return;
|
|
478
|
+
const entries = safeReadDir(dir).filter((e) => !e.startsWith('.') && e !== 'node_modules' && e !== 'dist' && e !== 'target');
|
|
479
|
+
for (const entry of entries.slice(0, 20)) {
|
|
480
|
+
if (lines.length >= maxEntries) {
|
|
481
|
+
lines.push(`${prefix}... (truncated)`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
lines.push(`${prefix}${entry}`);
|
|
485
|
+
const fullPath = join(dir, entry);
|
|
486
|
+
try {
|
|
487
|
+
const stat = statSync(fullPath);
|
|
488
|
+
if (stat.isDirectory()) {
|
|
489
|
+
walk(fullPath, `${prefix} `, depth + 1);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
// skip
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
walk(rootPath, '', 0);
|
|
498
|
+
return lines.join('\n');
|
|
499
|
+
}
|
|
500
|
+
function extractJson(text) {
|
|
501
|
+
try {
|
|
502
|
+
return JSON.parse(text.trim());
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
const match = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
506
|
+
if (match) {
|
|
507
|
+
try {
|
|
508
|
+
return JSON.parse(match[1].trim());
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const braceMatch = text.match(/\{[\s\S]*\}/);
|
|
515
|
+
if (braceMatch) {
|
|
516
|
+
try {
|
|
517
|
+
return JSON.parse(braceMatch[0]);
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function fallbackAnalysis(projectPath) {
|
|
527
|
+
const files = safeReadDir(projectPath);
|
|
528
|
+
const specialists = [];
|
|
529
|
+
if (files.includes('package.json')) {
|
|
530
|
+
const pkg = safeReadJson(join(projectPath, 'package.json'));
|
|
531
|
+
if (pkg) {
|
|
532
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
533
|
+
if (deps.react) {
|
|
534
|
+
specialists.push({
|
|
535
|
+
role: 'senior-react-engineer',
|
|
536
|
+
title: 'Senior React Engineer',
|
|
537
|
+
justification: 'Project uses React for frontend',
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
if (deps.express || deps.fastify || deps.koa) {
|
|
541
|
+
specialists.push({
|
|
542
|
+
role: 'senior-node-backend-engineer',
|
|
543
|
+
title: 'Senior Node.js Backend Engineer',
|
|
544
|
+
justification: 'Project has Node.js backend',
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (files.includes('Cargo.toml')) {
|
|
550
|
+
specialists.push({
|
|
551
|
+
role: 'senior-rust-engineer',
|
|
552
|
+
title: 'Senior Rust Engineer',
|
|
553
|
+
justification: 'Project uses Rust',
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
if (files.includes('go.mod')) {
|
|
557
|
+
specialists.push({
|
|
558
|
+
role: 'senior-go-engineer',
|
|
559
|
+
title: 'Senior Go Engineer',
|
|
560
|
+
justification: 'Project uses Go',
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
if (specialists.length === 0) {
|
|
564
|
+
specialists.push({
|
|
565
|
+
role: 'senior-software-engineer',
|
|
566
|
+
title: 'Senior Software Engineer',
|
|
567
|
+
justification: 'General development work',
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
return { specialists, separateQaAndTester: false };
|
|
571
|
+
}
|
|
184
572
|
function safeReadJson(path) {
|
|
185
573
|
try {
|
|
186
574
|
if (!existsSync(path))
|