heyio 3.0.14 → 3.1.1

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 (74) hide show
  1. package/dist/api/middleware/auth.d.ts.map +1 -1
  2. package/dist/api/middleware/auth.js +1 -0
  3. package/dist/api/middleware/auth.js.map +1 -1
  4. package/dist/api/routes/conversations.d.ts.map +1 -1
  5. package/dist/api/routes/conversations.js +7 -51
  6. package/dist/api/routes/conversations.js.map +1 -1
  7. package/dist/api/routes/health.d.ts.map +1 -1
  8. package/dist/api/routes/health.js +9 -0
  9. package/dist/api/routes/health.js.map +1 -1
  10. package/dist/api/routes/skills.d.ts.map +1 -1
  11. package/dist/api/routes/skills.js +49 -7
  12. package/dist/api/routes/skills.js.map +1 -1
  13. package/dist/api/routes/squads.d.ts.map +1 -1
  14. package/dist/api/routes/squads.js +27 -19
  15. package/dist/api/routes/squads.js.map +1 -1
  16. package/dist/copilot/orchestrator.d.ts.map +1 -1
  17. package/dist/copilot/orchestrator.js +15 -15
  18. package/dist/copilot/orchestrator.js.map +1 -1
  19. package/dist/copilot/tools.d.ts +4 -0
  20. package/dist/copilot/tools.d.ts.map +1 -1
  21. package/dist/copilot/tools.js +65 -9
  22. package/dist/copilot/tools.js.map +1 -1
  23. package/dist/skills/discover.d.ts +22 -0
  24. package/dist/skills/discover.d.ts.map +1 -0
  25. package/dist/skills/discover.js +194 -0
  26. package/dist/skills/discover.js.map +1 -0
  27. package/dist/skills/index.d.ts +3 -1
  28. package/dist/skills/index.d.ts.map +1 -1
  29. package/dist/skills/index.js +2 -1
  30. package/dist/skills/index.js.map +1 -1
  31. package/dist/skills/store.d.ts +4 -0
  32. package/dist/skills/store.d.ts.map +1 -1
  33. package/dist/skills/store.js +6 -0
  34. package/dist/skills/store.js.map +1 -1
  35. package/dist/squad/execution/meeting.js +6 -6
  36. package/dist/squad/execution/meeting.js.map +1 -1
  37. package/dist/squad/execution/tasks.js +3 -3
  38. package/dist/squad/execution/tasks.js.map +1 -1
  39. package/dist/squad/hiring.d.ts +29 -19
  40. package/dist/squad/hiring.d.ts.map +1 -1
  41. package/dist/squad/hiring.js +491 -152
  42. package/dist/squad/hiring.js.map +1 -1
  43. package/dist/squad/index.d.ts +1 -1
  44. package/dist/squad/index.d.ts.map +1 -1
  45. package/dist/squad/index.js +1 -1
  46. package/dist/squad/index.js.map +1 -1
  47. package/dist/squad/manager.js +1 -1
  48. package/dist/squad/manager.js.map +1 -1
  49. package/dist/squad/name-generator.d.ts.map +1 -1
  50. package/dist/squad/name-generator.js +14 -8
  51. package/dist/squad/name-generator.js.map +1 -1
  52. package/dist/squad/roles/templates.d.ts +3 -1
  53. package/dist/squad/roles/templates.d.ts.map +1 -1
  54. package/dist/squad/roles/templates.js +14 -10
  55. package/dist/squad/roles/templates.js.map +1 -1
  56. package/dist/store/conversations.d.ts +25 -0
  57. package/dist/store/conversations.d.ts.map +1 -0
  58. package/dist/store/conversations.js +76 -0
  59. package/dist/store/conversations.js.map +1 -0
  60. package/dist/wiki/index.d.ts +1 -1
  61. package/dist/wiki/index.d.ts.map +1 -1
  62. package/dist/wiki/store.d.ts +6 -2
  63. package/dist/wiki/store.d.ts.map +1 -1
  64. package/dist/wiki/store.js +29 -10
  65. package/dist/wiki/store.js.map +1 -1
  66. package/node_modules/@io/shared/package.json +1 -1
  67. package/package.json +1 -1
  68. package/public/assets/index-V-sj6VMY.js +480 -0
  69. package/public/assets/index-V-sj6VMY.js.map +1 -0
  70. package/public/assets/index-kxj8i2cu.css +1 -0
  71. package/public/index.html +9 -3
  72. package/public/assets/index-0a-a5X2R.js +0 -336
  73. package/public/assets/index-0a-a5X2R.js.map +0 -1
  74. package/public/assets/index-D3cGfBsj.css +0 -1
@@ -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, 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));
42
107
  }
43
108
  }
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 };
109
+ return ciFiles;
79
110
  }
80
111
  /**
81
- * Generate a specialist SKILL.md from a role name and detected context.
112
+ * Use the LLM to analyze a codebase and recommend specialist roles.
82
113
  */
83
- function generateSpecialistSkill(role, analysis) {
84
- const langContext = analysis.languages.join(', ');
85
- const frameworkContext = analysis.frameworks.length > 0 ? `\nFrameworks: ${analysis.frameworks.join(', ')}` : '';
86
- return `---
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
- # ${titleCase(role)}
119
+ ## README (truncated)
120
+ ${summary.readme || '(no README found)'}
97
121
 
98
- ## Identity
99
- You are a ${titleCase(role)} specializing in ${langContext}.${frameworkContext}
122
+ ## Package Manifests
123
+ ${Object.entries(summary.manifests)
124
+ .map(([f, content]) => `### ${f}\n\`\`\`\n${content}\n\`\`\``)
125
+ .join('\n\n')}
100
126
 
101
- ## Responsibilities
102
- - Implement features and fix bugs assigned by the Team Lead
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
- ## Boundaries
108
- - Only work on tasks assigned to you by the Team Lead
109
- - Do NOT modify files outside your area of expertise unless directed
110
- - Do NOT merge PRs — submit work for team lead review
111
- - Always run the test suite before reporting task completion
130
+ ## Directory Structure
131
+ \`\`\`
132
+ ${summary.directoryTree}
133
+ \`\`\`
112
134
 
113
- ## Project Context
114
- Languages: ${langContext}${frameworkContext}
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
- * 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
175
+ * Propose a squad: clone, analyze, generate names, store proposal for review.
123
176
  */
124
- export async function hireSquad(params) {
177
+ export async function proposeSquad(params) {
125
178
  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 },
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
- for (const specialist of analysis.suggestedSpecialists) {
139
- skillFiles.push({
140
- role: specialist,
141
- content: generateSpecialistSkill(specialist, analysis),
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
- // 3. Generate character names from universe via LLM
146
- const allRoles = skillFiles.map((f) => f.role);
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
- log.info({ squadName, universe: generated.universe }, 'Universe names generated');
149
- // 4. Create squad
150
- const squad = await createSquad({
151
- name: squadName,
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
- repoUrl: params.repoUrl,
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
- // 5. Create wiki folder for this squad
286
+ // 3. Create wiki
157
287
  ensureSquadWiki(squadName);
158
- // 6. Write files and add members
288
+ // 4. Write skill files and add members
159
289
  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 };
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
- // Generate skill content for the role
180
- const analysis = analyzeProject(params.projectPath);
181
- const skillContent = generateRoleSkill(params.role, analysis);
182
- // Write skill file to disk
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 = titleCase(params.role);
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: params.role === 'qa-tester',
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
- * Generate a skill file for a given role, using project analysis if available.
211
- */
212
- function generateRoleSkill(role, analysis) {
213
- // Check for built-in roles
214
- if (role === 'team-lead')
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
- return generateSpecialistSkill(role, analysis);
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
- // Helpers
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))