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.
Files changed (37) hide show
  1. package/dist/api/routes/squads.d.ts.map +1 -1
  2. package/dist/api/routes/squads.js +1 -0
  3. package/dist/api/routes/squads.js.map +1 -1
  4. package/dist/copilot/orchestrator.js +2 -2
  5. package/dist/copilot/orchestrator.js.map +1 -1
  6. package/dist/copilot/tools.d.ts +14 -0
  7. package/dist/copilot/tools.d.ts.map +1 -1
  8. package/dist/copilot/tools.js +235 -10
  9. package/dist/copilot/tools.js.map +1 -1
  10. package/dist/squad/hiring.d.ts +43 -19
  11. package/dist/squad/hiring.d.ts.map +1 -1
  12. package/dist/squad/hiring.js +521 -133
  13. package/dist/squad/hiring.js.map +1 -1
  14. package/dist/squad/index.d.ts +1 -1
  15. package/dist/squad/index.d.ts.map +1 -1
  16. package/dist/squad/index.js +1 -1
  17. package/dist/squad/index.js.map +1 -1
  18. package/dist/squad/manager.d.ts +8 -0
  19. package/dist/squad/manager.d.ts.map +1 -1
  20. package/dist/squad/manager.js +80 -0
  21. package/dist/squad/manager.js.map +1 -1
  22. package/dist/squad/name-generator.d.ts.map +1 -1
  23. package/dist/squad/name-generator.js +14 -8
  24. package/dist/squad/name-generator.js.map +1 -1
  25. package/dist/squad/roles/templates.d.ts +3 -1
  26. package/dist/squad/roles/templates.d.ts.map +1 -1
  27. package/dist/squad/roles/templates.js +14 -10
  28. package/dist/squad/roles/templates.js.map +1 -1
  29. package/node_modules/@io/shared/package.json +1 -1
  30. package/package.json +1 -1
  31. package/public/assets/index-5vJhtAQU.css +1 -0
  32. package/public/assets/index-nNmZHtrp.js +441 -0
  33. package/public/assets/index-nNmZHtrp.js.map +1 -0
  34. package/public/index.html +3 -3
  35. package/public/assets/index-D3cGfBsj.css +0 -1
  36. package/public/assets/index-dINUWXx2.js +0 -336
  37. package/public/assets/index-dINUWXx2.js.map +0 -1
@@ -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, TEAM_LEAD_SKILL } from './roles/templates.js';
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
- * Analyze a project directory to determine what kind of squad it needs.
40
+ * Sample key files from the project to build a codebase summary for the LLM.
13
41
  */
14
- export function analyzeProject(projectPath) {
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
- // Language detection
23
- if (files.includes('package.json')) {
24
- languages.push('TypeScript/JavaScript');
25
- const pkg = safeReadJson(join(projectPath, 'package.json'));
26
- if (pkg) {
27
- const deps = (pkg.dependencies ?? {});
28
- const devDeps = (pkg.devDependencies ?? {});
29
- const allDeps = { ...deps, ...devDeps };
30
- if (allDeps.react || allDeps['react-dom'])
31
- frameworks.push('React');
32
- if (allDeps.vue)
33
- frameworks.push('Vue');
34
- if (allDeps.svelte)
35
- frameworks.push('Svelte');
36
- if (allDeps.next)
37
- frameworks.push('Next.js');
38
- if (allDeps.express || allDeps.fastify || allDeps.koa)
39
- frameworks.push('Node.js Backend');
40
- if (allDeps.vitest || allDeps.jest || allDeps.mocha)
41
- hasTests = true;
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
- * Generate a specialist SKILL.md from a role name and detected context.
175
+ * Propose a squad: clone, analyze, generate names, store proposal for review.
82
176
  */
83
- function generateSpecialistSkill(role, analysis) {
84
- const langContext = analysis.languages.join(', ');
85
- const frameworkContext = analysis.frameworks.length > 0 ? `\nFrameworks: ${analysis.frameworks.join(', ')}` : '';
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: ${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
- # ${titleCase(role)}
377
+ # Tester
97
378
 
98
379
  ## Identity
99
- You are a ${titleCase(role)} specializing in ${langContext}.${frameworkContext}
380
+ You are the Tester responsible for functional testing, integration testing, and CI/CD pipeline health.
100
381
 
101
382
  ## Responsibilities
102
- - Implement features and fix bugs assigned by the Team Lead
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
- - Respond to code review feedback promptly
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 Team Lead
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 team lead review
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}${frameworkContext}
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))