heyio 3.0.14 → 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/tools.d.ts +4 -0
- package/dist/copilot/tools.d.ts.map +1 -1
- package/dist/copilot/tools.js +65 -9
- package/dist/copilot/tools.js.map +1 -1
- package/dist/squad/hiring.d.ts +29 -19
- package/dist/squad/hiring.d.ts.map +1 -1
- package/dist/squad/hiring.js +491 -152
- 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/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-0a-a5X2R.js +0 -336
- package/public/assets/index-0a-a5X2R.js.map +0 -1
- package/public/assets/index-D3cGfBsj.css +0 -1
package/dist/squad/hiring.js
CHANGED
|
@@ -1,192 +1,333 @@
|
|
|
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));
|
|
42
107
|
}
|
|
43
108
|
}
|
|
44
|
-
|
|
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 };
|
|
109
|
+
return ciFiles;
|
|
79
110
|
}
|
|
80
111
|
/**
|
|
81
|
-
*
|
|
112
|
+
* Use the LLM to analyze a codebase and recommend specialist roles.
|
|
82
113
|
*/
|
|
83
|
-
function
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
role: ${role}
|
|
88
|
-
tools:
|
|
89
|
-
- read_file
|
|
90
|
-
- edit_file
|
|
91
|
-
- run_command
|
|
92
|
-
- search_code
|
|
93
|
-
veto: false
|
|
94
|
-
---
|
|
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:
|
|
95
118
|
|
|
96
|
-
|
|
119
|
+
## README (truncated)
|
|
120
|
+
${summary.readme || '(no README found)'}
|
|
97
121
|
|
|
98
|
-
##
|
|
99
|
-
|
|
122
|
+
## Package Manifests
|
|
123
|
+
${Object.entries(summary.manifests)
|
|
124
|
+
.map(([f, content]) => `### ${f}\n\`\`\`\n${content}\n\`\`\``)
|
|
125
|
+
.join('\n\n')}
|
|
100
126
|
|
|
101
|
-
##
|
|
102
|
-
|
|
103
|
-
- Write clean, well-tested code following project conventions
|
|
104
|
-
- Run tests before submitting work for review
|
|
105
|
-
- Respond to code review feedback promptly
|
|
127
|
+
## Config Files Present
|
|
128
|
+
${summary.configFiles.join(', ') || '(none detected)'}
|
|
106
129
|
|
|
107
|
-
##
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
- Always run the test suite before reporting task completion
|
|
130
|
+
## Directory Structure
|
|
131
|
+
\`\`\`
|
|
132
|
+
${summary.directoryTree}
|
|
133
|
+
\`\`\`
|
|
112
134
|
|
|
113
|
-
##
|
|
114
|
-
|
|
115
|
-
|
|
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();
|
|
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);
|
|
171
|
+
}
|
|
116
172
|
}
|
|
173
|
+
// ─── Propose / Confirm Flow ────────────────────────────────────────────────
|
|
117
174
|
/**
|
|
118
|
-
*
|
|
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
|
|
175
|
+
* Propose a squad: clone, analyze, generate names, store proposal for review.
|
|
123
176
|
*/
|
|
124
|
-
export async function
|
|
177
|
+
export async function proposeSquad(params) {
|
|
125
178
|
const log = logger();
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
},
|
|
137
198
|
];
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
role:
|
|
141
|
-
|
|
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,
|
|
142
212
|
veto: false,
|
|
143
213
|
});
|
|
144
214
|
}
|
|
145
|
-
|
|
146
|
-
|
|
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);
|
|
147
235
|
const generated = await generateSquadNames(allRoles, params.universe);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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 ?? '',
|
|
152
249
|
projectPath: params.projectPath,
|
|
153
|
-
|
|
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,
|
|
154
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,
|
|
155
285
|
});
|
|
156
|
-
//
|
|
286
|
+
// 3. Create wiki
|
|
157
287
|
ensureSquadWiki(squadName);
|
|
158
|
-
//
|
|
288
|
+
// 4. Write skill files and add members
|
|
159
289
|
const memberRoles = [];
|
|
160
|
-
for (const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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 };
|
|
172
308
|
}
|
|
309
|
+
// ─── Legacy API (kept for addMemberToExistingSquad) ─────────────────────────
|
|
173
310
|
/**
|
|
174
311
|
* Add a new member to an existing squad.
|
|
175
312
|
* Generates a skill file and optionally themes the name to the squad's universe.
|
|
176
313
|
*/
|
|
177
314
|
export async function addMemberToExistingSquad(params) {
|
|
178
315
|
const log = logger();
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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);
|
|
183
324
|
const skillsDir = join(homedir(), '.io', 'squads', params.squadName);
|
|
184
325
|
mkdirSync(skillsDir, { recursive: true });
|
|
185
326
|
const filePath = join(skillsDir, `${params.role}.skill.md`);
|
|
186
327
|
writeFileSync(filePath, skillContent, 'utf-8');
|
|
187
328
|
const skill = parseSkillContent(skillContent, filePath);
|
|
188
329
|
// Generate themed name if universe is set
|
|
189
|
-
let displayName =
|
|
330
|
+
let displayName = member.title;
|
|
190
331
|
let persona;
|
|
191
332
|
if (params.universe) {
|
|
192
333
|
const generated = await generateSquadNames([params.role], params.universe);
|
|
@@ -201,25 +342,113 @@ export async function addMemberToExistingSquad(params) {
|
|
|
201
342
|
skill,
|
|
202
343
|
displayName,
|
|
203
344
|
persona,
|
|
204
|
-
isVetoMember:
|
|
345
|
+
isVetoMember: member.veto,
|
|
205
346
|
});
|
|
206
347
|
log.info({ squadId: params.squadId, role: params.role, displayName }, 'Member added to squad');
|
|
207
348
|
return { displayName, role: params.role };
|
|
208
349
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (role === '
|
|
215
|
-
return TEAM_LEAD_SKILL;
|
|
216
|
-
if (role === 'scribe')
|
|
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')
|
|
217
356
|
return SCRIBE_SKILL;
|
|
218
|
-
if (role === 'qa-tester')
|
|
357
|
+
if (member.role === 'qa-tester' || member.role === 'qa-engineer')
|
|
219
358
|
return QA_TESTER_SKILL;
|
|
220
|
-
|
|
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() {
|
|
367
|
+
return `---
|
|
368
|
+
role: tester
|
|
369
|
+
tools:
|
|
370
|
+
- read_file
|
|
371
|
+
- edit_file
|
|
372
|
+
- run_command
|
|
373
|
+
- search_code
|
|
374
|
+
veto: false
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
# Tester
|
|
378
|
+
|
|
379
|
+
## Identity
|
|
380
|
+
You are the Tester — responsible for functional testing, integration testing, and CI/CD pipeline health.
|
|
381
|
+
|
|
382
|
+
## Responsibilities
|
|
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
|
+
`;
|
|
221
401
|
}
|
|
222
|
-
|
|
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
|
|
436
|
+
- Write clean, well-tested code following project conventions
|
|
437
|
+
- Provide expert-level guidance on your specialty area during team discussions
|
|
438
|
+
- Run tests before submitting work for review
|
|
439
|
+
- Mentor other team members on best practices in your domain
|
|
440
|
+
|
|
441
|
+
## Boundaries
|
|
442
|
+
- Only work on tasks assigned to you by the Technical PM
|
|
443
|
+
- Do NOT modify files outside your area of expertise unless directed
|
|
444
|
+
- Do NOT merge PRs — submit work for Technical PM review
|
|
445
|
+
- Always run the test suite before reporting task completion
|
|
446
|
+
|
|
447
|
+
## Project Context
|
|
448
|
+
${langContext ? `Languages: ${langContext}` : ''}
|
|
449
|
+
`;
|
|
450
|
+
}
|
|
451
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
223
452
|
function safeReadDir(dir) {
|
|
224
453
|
try {
|
|
225
454
|
if (!existsSync(dir))
|
|
@@ -230,6 +459,116 @@ function safeReadDir(dir) {
|
|
|
230
459
|
return [];
|
|
231
460
|
}
|
|
232
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
|
+
}
|
|
233
572
|
function safeReadJson(path) {
|
|
234
573
|
try {
|
|
235
574
|
if (!existsSync(path))
|