@forwardimpact/model 0.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/lib/agent.js ADDED
@@ -0,0 +1,754 @@
1
+ /**
2
+ * Agent Generation Model
3
+ *
4
+ * Pure functions for generating AI coding agent configurations
5
+ * from Engineering Pathway data. Outputs follow GitHub Copilot specifications:
6
+ * - Agent Profiles (.agent.md files)
7
+ * - Agent Skills (SKILL.md files)
8
+ *
9
+ * Agent profiles are derived using the SAME modifier logic as human job profiles.
10
+ * Emphasized behaviours and skills (those with positive modifiers) drive agent
11
+ * identity, creating distinct profiles for each discipline × track combination.
12
+ *
13
+ * Stage-based agents (plan, code, review) use lifecycle stages for tool sets,
14
+ * handoffs, and constraints. See concept/lifecycle.md for details.
15
+ *
16
+ * NOTE: This module uses prepareAgentProfile() from profile.js for unified
17
+ * skill/behaviour derivation. The deriveAgentSkills() and deriveAgentBehaviours()
18
+ * functions are thin wrappers for backward compatibility.
19
+ */
20
+
21
+ import { deriveSkillMatrix, deriveBehaviourProfile } from "./derivation.js";
22
+ import { deriveChecklist, formatChecklistMarkdown } from "./checklist.js";
23
+ import {
24
+ filterSkillsForAgent,
25
+ sortByLevelDescending,
26
+ sortByMaturityDescending,
27
+ } from "./profile.js";
28
+ import { SkillLevel } from "@forwardimpact/schema/levels";
29
+
30
+ /**
31
+ * Derive the reference grade for agent generation.
32
+ *
33
+ * The reference grade determines the skill and behaviour expectations for agents.
34
+ * We select the first grade where primary skills reach "practitioner" level,
35
+ * as this represents substantive senior-level expertise suitable for AI agents.
36
+ *
37
+ * Fallback logic:
38
+ * 1. First grade with practitioner-level primary skills
39
+ * 2. First grade with working-level primary skills (if no practitioner found)
40
+ * 3. Middle grade by level (if neither found)
41
+ *
42
+ * @param {Array<Object>} grades - Array of grade definitions, each with baseSkillLevels.primary
43
+ * @returns {Object} The reference grade
44
+ * @throws {Error} If no grades are provided
45
+ */
46
+ export function deriveReferenceGrade(grades) {
47
+ if (!grades || grades.length === 0) {
48
+ throw new Error("No grades configured");
49
+ }
50
+
51
+ // Sort by level to ensure consistent ordering
52
+ const sorted = [...grades].sort((a, b) => a.ordinalRank - b.ordinalRank);
53
+
54
+ // First: find the first grade with practitioner-level primary skills
55
+ const practitionerGrade = sorted.find(
56
+ (g) => g.baseSkillLevels?.primary === SkillLevel.PRACTITIONER,
57
+ );
58
+ if (practitionerGrade) {
59
+ return practitionerGrade;
60
+ }
61
+
62
+ // Fallback: find the first grade with working-level primary skills
63
+ const workingGrade = sorted.find(
64
+ (g) => g.baseSkillLevels?.primary === SkillLevel.WORKING,
65
+ );
66
+ if (workingGrade) {
67
+ return workingGrade;
68
+ }
69
+
70
+ // Final fallback: use the middle grade
71
+ const middleIndex = Math.floor(sorted.length / 2);
72
+ return sorted[middleIndex];
73
+ }
74
+
75
+ /**
76
+ * Discipline ID to abbreviation mapping for file naming
77
+ * Falls back to first letters of discipline name if not specified
78
+ * @type {Object.<string, string>}
79
+ */
80
+ const DISCIPLINE_ABBREVIATIONS = {
81
+ software_engineering: "se",
82
+ data_engineering: "de",
83
+ };
84
+
85
+ /**
86
+ * Get abbreviation for a discipline ID
87
+ * Falls back to first two letters if no mapping exists
88
+ * @param {string} disciplineId - Discipline identifier
89
+ * @returns {string} Short form abbreviation
90
+ */
91
+ export function getDisciplineAbbreviation(disciplineId) {
92
+ return DISCIPLINE_ABBREVIATIONS[disciplineId] || disciplineId.slice(0, 2);
93
+ }
94
+
95
+ /**
96
+ * Convert snake_case id to kebab-case for agent naming
97
+ * @param {string} id - Snake case identifier
98
+ * @returns {string} Kebab case identifier
99
+ */
100
+ export function toKebabCase(id) {
101
+ return id.replace(/_/g, "-");
102
+ }
103
+
104
+ /**
105
+ * Derive agent skills using the unified profile system
106
+ * Returns skills sorted by level (highest first) for the given discipline × track
107
+ * Excludes human-only skills and keeps only skills at the highest derived level.
108
+ * This approach respects track modifiers—a broad skill boosted to the same level
109
+ * as primary skills will be included.
110
+ * @param {Object} params - Parameters
111
+ * @param {Object} params.discipline - Human discipline definition
112
+ * @param {Object} params.track - Human track definition
113
+ * @param {Object} params.grade - Reference grade for derivation
114
+ * @param {Array} params.skills - All available skills
115
+ * @returns {Array} Skills sorted by derived level (highest first)
116
+ */
117
+ export function deriveAgentSkills({ discipline, track, grade, skills }) {
118
+ // Use shared derivation
119
+ const skillMatrix = deriveSkillMatrix({
120
+ discipline,
121
+ grade,
122
+ track,
123
+ skills,
124
+ });
125
+
126
+ // Apply agent-specific filtering and sorting
127
+ const filtered = filterSkillsForAgent(skillMatrix);
128
+ return sortByLevelDescending(filtered);
129
+ }
130
+
131
+ /**
132
+ * Derive agent behaviours using the unified profile system
133
+ * Returns behaviours sorted by maturity (highest first) for the given discipline × track
134
+ * @param {Object} params - Parameters
135
+ * @param {Object} params.discipline - Human discipline definition
136
+ * @param {Object} params.track - Human track definition
137
+ * @param {Object} params.grade - Reference grade for derivation
138
+ * @param {Array} params.behaviours - All available behaviours
139
+ * @returns {Array} Behaviours sorted by derived maturity (highest first)
140
+ */
141
+ export function deriveAgentBehaviours({
142
+ discipline,
143
+ track,
144
+ grade,
145
+ behaviours,
146
+ }) {
147
+ const profile = deriveBehaviourProfile({
148
+ discipline,
149
+ grade,
150
+ track,
151
+ behaviours,
152
+ });
153
+
154
+ return sortByMaturityDescending(profile);
155
+ }
156
+
157
+ /**
158
+ * Substitute template variables in text
159
+ * @param {string} text - Text with {roleTitle}, {specialization} placeholders
160
+ * @param {Object} discipline - Discipline with roleTitle, specialization properties
161
+ * @returns {string} Text with substituted values
162
+ */
163
+ function substituteTemplateVars(text, discipline) {
164
+ return text
165
+ .replace(/\{roleTitle\}/g, discipline.roleTitle)
166
+ .replace(/\{specialization\}/g, discipline.specialization);
167
+ }
168
+
169
+ /**
170
+ * Find an agent behaviour by id
171
+ * @param {Array} agentBehaviours - Array of agent behaviour definitions
172
+ * @param {string} id - Behaviour id to find
173
+ * @returns {Object|undefined} Agent behaviour or undefined
174
+ */
175
+ function findAgentBehaviour(agentBehaviours, id) {
176
+ return agentBehaviours.find((b) => b.id === id);
177
+ }
178
+
179
+ /**
180
+ * Build working style section from emphasized behaviours
181
+ * Includes workflow patterns when available
182
+ * @param {Array} derivedBehaviours - Behaviours sorted by maturity (highest first)
183
+ * @param {Array} agentBehaviours - Agent behaviour definitions with principles
184
+ * @param {number} topN - Number of top behaviours to include
185
+ * @returns {string} Working style markdown section
186
+ */
187
+ function buildWorkingStyleFromBehaviours(
188
+ derivedBehaviours,
189
+ agentBehaviours,
190
+ topN = 3,
191
+ ) {
192
+ const sections = [];
193
+ sections.push("## Working Style");
194
+ sections.push("");
195
+
196
+ // Get top N behaviours by maturity
197
+ const topBehaviours = derivedBehaviours.slice(0, topN);
198
+
199
+ for (const derived of topBehaviours) {
200
+ const agentBehaviour = findAgentBehaviour(
201
+ agentBehaviours,
202
+ derived.behaviourId,
203
+ );
204
+ // Skip if no agent behaviour data or no content to display
205
+ if (!agentBehaviour) continue;
206
+ if (!agentBehaviour.workingStyle && !agentBehaviour.principles) continue;
207
+
208
+ // Use title as section header
209
+ const title = agentBehaviour.title || derived.behaviourName;
210
+ sections.push(`### ${title}`);
211
+ sections.push("");
212
+
213
+ // Include workingStyle if available (structured guidance)
214
+ if (agentBehaviour.workingStyle) {
215
+ sections.push(agentBehaviour.workingStyle.trim());
216
+ sections.push("");
217
+ } else if (agentBehaviour.principles) {
218
+ // Fall back to principles
219
+ const principles = agentBehaviour.principles.trim();
220
+ sections.push(principles);
221
+ sections.push("");
222
+ }
223
+ }
224
+
225
+ return sections.join("\n");
226
+ }
227
+
228
+ /**
229
+ * Generate SKILL.md content from skill data
230
+ * @param {Object} skillData - Skill with agent section containing stages
231
+ * @param {Array} stages - All stage entities
232
+ * @returns {Object} Skill with frontmatter, title, stages array, reference, dirname
233
+ */
234
+ export function generateSkillMd(skillData, stages) {
235
+ const { agent, name } = skillData;
236
+
237
+ if (!agent) {
238
+ throw new Error(`Skill ${skillData.id} has no agent section`);
239
+ }
240
+
241
+ if (!agent.stages) {
242
+ throw new Error(`Skill ${skillData.id} agent section missing stages`);
243
+ }
244
+
245
+ // Build stage lookup map
246
+ const stageMap = new Map(stages.map((s) => [s.id, s]));
247
+
248
+ // Transform stages object to array for template rendering
249
+ const stagesArray = Object.entries(agent.stages).map(
250
+ ([stageId, stageData]) => {
251
+ const stageEntity = stageMap.get(stageId);
252
+ const stageName = stageEntity?.name || stageId;
253
+
254
+ // Find next stage from handoffs
255
+ let nextStageName = "Complete";
256
+ if (stageEntity?.handoffs) {
257
+ const nextHandoff = stageEntity.handoffs.find(
258
+ (h) => h.targetStage !== stageId,
259
+ );
260
+ if (nextHandoff) {
261
+ const nextStage = stageMap.get(nextHandoff.targetStage);
262
+ nextStageName = nextStage?.name || nextHandoff.targetStage;
263
+ }
264
+ }
265
+
266
+ return {
267
+ stageId,
268
+ stageName,
269
+ nextStageName,
270
+ focus: stageData.focus,
271
+ activities: stageData.activities || [],
272
+ ready: stageData.ready || [],
273
+ };
274
+ },
275
+ );
276
+
277
+ // Sort stages in order: plan, code, review
278
+ const stageOrder = ["plan", "code", "review"];
279
+ stagesArray.sort(
280
+ (a, b) => stageOrder.indexOf(a.stageId) - stageOrder.indexOf(b.stageId),
281
+ );
282
+
283
+ return {
284
+ frontmatter: {
285
+ name: agent.name,
286
+ description: agent.description,
287
+ useWhen: agent.useWhen || "",
288
+ },
289
+ title: name,
290
+ stages: stagesArray,
291
+ reference: skillData.implementationReference || "",
292
+ toolReferences: skillData.toolReferences || [],
293
+ dirname: agent.name,
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Estimate total character length of bodyData fields
299
+ * @param {Object} bodyData - Structured profile body data
300
+ * @returns {number} Estimated character count
301
+ */
302
+ function estimateBodyDataLength(bodyData) {
303
+ let length = 0;
304
+
305
+ // String fields
306
+ const stringFields = [
307
+ "title",
308
+ "stageDescription",
309
+ "identity",
310
+ "priority",
311
+ "delegation",
312
+ "operationalContext",
313
+ "workingStyle",
314
+ "beforeHandoff",
315
+ ];
316
+ for (const field of stringFields) {
317
+ if (bodyData[field]) {
318
+ length += bodyData[field].length;
319
+ }
320
+ }
321
+
322
+ // Array fields
323
+ if (bodyData.skillIndex) {
324
+ for (const skill of bodyData.skillIndex) {
325
+ length +=
326
+ skill.name.length + skill.dirname.length + skill.useWhen.length + 50;
327
+ }
328
+ }
329
+ if (bodyData.beforeMakingChanges) {
330
+ for (const item of bodyData.beforeMakingChanges) {
331
+ length += item.text.length + 5; // +5 for "1. " prefix
332
+ }
333
+ }
334
+ if (bodyData.constraints) {
335
+ for (const c of bodyData.constraints) {
336
+ length += c.length + 2; // +2 for "- " prefix
337
+ }
338
+ }
339
+
340
+ return length;
341
+ }
342
+
343
+ /**
344
+ * Validate agent profile against spec constraints
345
+ * @param {Object} profile - Generated profile
346
+ * @returns {Array<string>} Array of error messages (empty if valid)
347
+ */
348
+ export function validateAgentProfile(profile) {
349
+ const errors = [];
350
+
351
+ // Required: description
352
+ if (!profile.frontmatter.description) {
353
+ errors.push("Missing required field: description");
354
+ }
355
+
356
+ // Name format (if provided)
357
+ if (profile.frontmatter.name) {
358
+ if (!/^[a-zA-Z0-9._-]+$/.test(profile.frontmatter.name)) {
359
+ errors.push("Name contains invalid characters");
360
+ }
361
+ }
362
+
363
+ // Body length limit (30,000 chars) - estimate from bodyData fields
364
+ const bodyLength = estimateBodyDataLength(profile.bodyData);
365
+ if (bodyLength > 30000) {
366
+ errors.push(`Body exceeds 30,000 character limit (${bodyLength})`);
367
+ }
368
+
369
+ // Tools format
370
+ if (profile.frontmatter.tools && !Array.isArray(profile.frontmatter.tools)) {
371
+ errors.push("Tools must be an array");
372
+ }
373
+
374
+ return errors;
375
+ }
376
+
377
+ /**
378
+ * Validate agent skill against spec constraints
379
+ * @param {Object} skill - Generated skill
380
+ * @returns {Array<string>} Array of error messages (empty if valid)
381
+ */
382
+ export function validateAgentSkill(skill) {
383
+ const errors = [];
384
+
385
+ // Required: name
386
+ if (!skill.frontmatter.name) {
387
+ errors.push("Missing required field: name");
388
+ } else {
389
+ const name = skill.frontmatter.name;
390
+
391
+ // Name format: lowercase, hyphens, 1-64 chars
392
+ if (!/^[a-z0-9-]+$/.test(name)) {
393
+ errors.push("Name must be lowercase alphanumeric with hyphens");
394
+ }
395
+ if (name.length > 64) {
396
+ errors.push("Name exceeds 64 character limit");
397
+ }
398
+ if (name.startsWith("-") || name.endsWith("-")) {
399
+ errors.push("Name cannot start or end with hyphen");
400
+ }
401
+ if (name.includes("--")) {
402
+ errors.push("Name cannot contain consecutive hyphens");
403
+ }
404
+ }
405
+
406
+ // Required: description
407
+ if (!skill.frontmatter.description) {
408
+ errors.push("Missing required field: description");
409
+ } else if (skill.frontmatter.description.length > 1024) {
410
+ errors.push("Description exceeds 1024 character limit");
411
+ }
412
+
413
+ return errors;
414
+ }
415
+
416
+ // =============================================================================
417
+ // Stage-Based Agent Generation
418
+ // =============================================================================
419
+
420
+ /**
421
+ * Derive handoff buttons for a stage-based agent
422
+ * Generates handoff button definitions from stage.handoffs with rich prompts
423
+ * that include summary instructions and target stage entry criteria
424
+ * @param {Object} params - Parameters
425
+ * @param {Object} params.stage - Stage definition
426
+ * @param {Object} params.discipline - Human discipline definition (for naming)
427
+ * @param {Object} params.track - Human track definition (for naming)
428
+ * @param {Array} params.stages - All stages (to look up target stage entry criteria)
429
+ * @returns {Array<{label: string, agent: string, prompt: string, send: boolean}>} Handoff definitions
430
+ */
431
+ export function deriveHandoffs({ stage, discipline, track, stages }) {
432
+ if (!stage.handoffs || stage.handoffs.length === 0) {
433
+ return [];
434
+ }
435
+
436
+ // Build base name for target agents (matches filename without .agent.md)
437
+ const abbrev = getDisciplineAbbreviation(discipline.id);
438
+ const baseName = `${abbrev}-${toKebabCase(track.id)}`;
439
+
440
+ return stage.handoffs.map((handoff) => {
441
+ // Find the target stage to get its entry criteria
442
+ const targetStage = stages.find((s) => s.id === handoff.targetStage);
443
+ const entryCriteria = targetStage?.entryCriteria || [];
444
+
445
+ // Build rich prompt - formatted for single-line display
446
+ const promptParts = [handoff.prompt];
447
+
448
+ // Add summary instruction
449
+ promptParts.push(
450
+ `Summarize what was completed in the ${stage.name} stage.`,
451
+ );
452
+
453
+ // Add entry criteria from target stage with inline numbered list
454
+ if (entryCriteria.length > 0) {
455
+ const formattedCriteria = entryCriteria
456
+ .map((item, index) => `(${index + 1}) ${item}`)
457
+ .join(", ");
458
+ promptParts.push(
459
+ `Before starting, the ${targetStage.name} stage requires: ${formattedCriteria}.`,
460
+ );
461
+ promptParts.push(
462
+ `If critical items are missing, hand back to ${stage.name}.`,
463
+ );
464
+ }
465
+
466
+ return {
467
+ label: handoff.label,
468
+ agent: `${baseName}-${handoff.targetStage}`,
469
+ prompt: promptParts.join(" "),
470
+ send: true,
471
+ };
472
+ });
473
+ }
474
+
475
+ /**
476
+ * Get the handoff type for a stage (used for checklist derivation)
477
+ * @param {string} stageId - Stage ID (plan, code, review)
478
+ * @returns {string|null} Stage ID for checklist or null
479
+ */
480
+ function getChecklistStage(stageId) {
481
+ // Plan and code stages have checklists, review doesn't
482
+ return stageId === "review" ? null : stageId;
483
+ }
484
+
485
+ /**
486
+ * Build the profile body data for a stage-based agent
487
+ * Returns structured data for template rendering
488
+ * @param {Object} params - Parameters
489
+ * @param {Object} params.stage - Stage definition
490
+ * @param {Object} params.humanDiscipline - Human discipline definition
491
+ * @param {Object} params.humanTrack - Human track definition
492
+ * @param {Object} params.agentDiscipline - Agent discipline definition
493
+ * @param {Object} params.agentTrack - Agent track definition
494
+ * @param {Array} params.derivedSkills - Skills sorted by level
495
+ * @param {Array} params.derivedBehaviours - Behaviours sorted by maturity
496
+ * @param {Array} params.agentBehaviours - Agent behaviour definitions
497
+ * @param {Array} params.skills - All skill definitions (for agent section lookup)
498
+ * @param {string} params.checklistMarkdown - Pre-formatted checklist markdown
499
+ * @returns {Object} Structured profile body data
500
+ */
501
+ function buildStageProfileBodyData({
502
+ stage,
503
+ humanDiscipline,
504
+ humanTrack,
505
+ agentDiscipline,
506
+ agentTrack,
507
+ derivedSkills,
508
+ derivedBehaviours,
509
+ agentBehaviours,
510
+ skills,
511
+ checklistMarkdown,
512
+ }) {
513
+ const name = `${humanDiscipline.specialization || humanDiscipline.name} - ${humanTrack.name}`;
514
+ const stageName = stage.name.charAt(0).toUpperCase() + stage.name.slice(1);
515
+
516
+ // Build identity - prefer track, fall back to discipline
517
+ const rawIdentity = agentTrack.identity || agentDiscipline.identity;
518
+ const identity = substituteTemplateVars(rawIdentity, humanDiscipline);
519
+
520
+ // Build priority - prefer track, fall back to discipline (optional)
521
+ const rawPriority = agentTrack.priority || agentDiscipline.priority;
522
+ const priority = rawPriority
523
+ ? substituteTemplateVars(rawPriority, humanDiscipline)
524
+ : null;
525
+
526
+ // Build beforeMakingChanges list - prefer track, fall back to discipline
527
+ const rawSteps =
528
+ agentTrack.beforeMakingChanges || agentDiscipline.beforeMakingChanges || [];
529
+ const beforeMakingChanges = rawSteps.map((text, i) => ({
530
+ index: i + 1,
531
+ text: substituteTemplateVars(text, humanDiscipline),
532
+ }));
533
+
534
+ // Delegation (from discipline only, optional)
535
+ const rawDelegation = agentDiscipline.delegation;
536
+ const delegation = rawDelegation
537
+ ? substituteTemplateVars(rawDelegation, humanDiscipline)
538
+ : null;
539
+
540
+ // Build skill index from derived skills with agent sections
541
+ const skillIndex = derivedSkills
542
+ .map((derived) => {
543
+ const skill = skills.find((s) => s.id === derived.skillId);
544
+ if (!skill?.agent) return null;
545
+ return {
546
+ name: derived.skillName,
547
+ dirname: skill.agent.name,
548
+ useWhen: skill.agent.useWhen?.trim() || "",
549
+ };
550
+ })
551
+ .filter(Boolean);
552
+
553
+ // Operational Context - use track's roleContext (shared with human job descriptions)
554
+ const operationalContext = humanTrack.roleContext.trim();
555
+
556
+ // Working Style from derived behaviours (still markdown for now)
557
+ const workingStyle = buildWorkingStyleFromBehaviours(
558
+ derivedBehaviours,
559
+ agentBehaviours,
560
+ 3,
561
+ );
562
+
563
+ // Constraints (stage + discipline + track)
564
+ const constraints = [
565
+ ...(stage.constraints || []),
566
+ ...(agentDiscipline.constraints || []),
567
+ ...(agentTrack.constraints || []),
568
+ ];
569
+
570
+ return {
571
+ title: `${name} - ${stageName} Agent`,
572
+ stageDescription: stage.description,
573
+ identity: identity.trim(),
574
+ priority: priority ? priority.trim() : null,
575
+ skillIndex,
576
+ beforeMakingChanges,
577
+ delegation: delegation ? delegation.trim() : null,
578
+ operationalContext,
579
+ workingStyle,
580
+ beforeHandoff: checklistMarkdown || null,
581
+ constraints,
582
+ };
583
+ }
584
+
585
+ /**
586
+ * Derive a stage-specific agent profile
587
+ * Combines discipline, track, and stage to produce a complete agent definition
588
+ * @param {Object} params - Parameters
589
+ * @param {Object} params.discipline - Human discipline definition
590
+ * @param {Object} params.track - Human track definition
591
+ * @param {Object} params.stage - Stage definition from stages.yaml
592
+ * @param {Object} params.grade - Reference grade for skill derivation
593
+ * @param {Array} params.skills - All available skills
594
+ * @param {Array} params.behaviours - All available behaviours
595
+ * @param {Array} params.agentBehaviours - Agent behaviour definitions
596
+ * @param {Object} params.agentDiscipline - Agent discipline definition
597
+ * @param {Object} params.agentTrack - Agent track definition
598
+ * @param {Array} params.capabilities - Capabilities for checklist grouping
599
+ * @param {Array} params.stages - All stages (for handoff entry criteria)
600
+ * @returns {Object} Agent definition with skills, behaviours, tools, handoffs, constraints, checklist
601
+ */
602
+ export function deriveStageAgent({
603
+ discipline,
604
+ track,
605
+ stage,
606
+ grade,
607
+ skills,
608
+ behaviours,
609
+ agentBehaviours,
610
+ agentDiscipline,
611
+ agentTrack,
612
+ capabilities,
613
+ stages,
614
+ }) {
615
+ // Derive skills and behaviours
616
+ const derivedSkills = deriveAgentSkills({
617
+ discipline,
618
+ track,
619
+ grade,
620
+ skills,
621
+ });
622
+
623
+ const derivedBehaviours = deriveAgentBehaviours({
624
+ discipline,
625
+ track,
626
+ grade,
627
+ behaviours,
628
+ });
629
+
630
+ // Derive handoffs from stage
631
+ const handoffs = deriveHandoffs({
632
+ stage,
633
+ discipline,
634
+ track,
635
+ stages,
636
+ });
637
+
638
+ // Derive checklist if applicable
639
+ const checklistStage = getChecklistStage(stage.id);
640
+ let checklist = [];
641
+ if (checklistStage && capabilities) {
642
+ checklist = deriveChecklist({
643
+ stageId: checklistStage,
644
+ skillMatrix: derivedSkills,
645
+ skills,
646
+ capabilities,
647
+ });
648
+ }
649
+
650
+ return {
651
+ stage,
652
+ discipline,
653
+ track,
654
+ derivedSkills,
655
+ derivedBehaviours,
656
+ handoffs,
657
+ constraints: [
658
+ ...(stage.constraints || []),
659
+ ...(agentDiscipline.constraints || []),
660
+ ...(agentTrack.constraints || []),
661
+ ],
662
+ checklist,
663
+ agentDiscipline,
664
+ agentTrack,
665
+ agentBehaviours,
666
+ };
667
+ }
668
+
669
+ /**
670
+ * Generate a stage-specific agent profile (.agent.md)
671
+ * Produces the complete profile with frontmatter, bodyData, and filename
672
+ * @param {Object} params - Parameters
673
+ * @param {Object} params.discipline - Human discipline definition
674
+ * @param {Object} params.track - Human track definition
675
+ * @param {Object} params.stage - Stage definition
676
+ * @param {Object} params.grade - Reference grade
677
+ * @param {Array} params.skills - All skills
678
+ * @param {Array} params.behaviours - All behaviours
679
+ * @param {Array} params.agentBehaviours - Agent behaviour definitions
680
+ * @param {Object} params.agentDiscipline - Agent discipline definition
681
+ * @param {Object} params.agentTrack - Agent track definition
682
+ * @param {Array} params.capabilities - Capabilities with checklists
683
+ * @param {Array} params.stages - All stages (for handoff entry criteria)
684
+ * @returns {Object} Profile with frontmatter, bodyData, and filename
685
+ */
686
+ export function generateStageAgentProfile({
687
+ discipline,
688
+ track,
689
+ stage,
690
+ grade,
691
+ skills,
692
+ behaviours,
693
+ agentBehaviours,
694
+ agentDiscipline,
695
+ agentTrack,
696
+ capabilities,
697
+ stages,
698
+ }) {
699
+ // Derive the complete agent
700
+ const agent = deriveStageAgent({
701
+ discipline,
702
+ track,
703
+ stage,
704
+ grade,
705
+ skills,
706
+ behaviours,
707
+ agentBehaviours,
708
+ agentDiscipline,
709
+ agentTrack,
710
+ capabilities,
711
+ stages,
712
+ });
713
+
714
+ // Build names (abbreviated form used consistently for filename, name, and handoffs)
715
+ const abbrev = getDisciplineAbbreviation(discipline.id);
716
+ const fullName = `${abbrev}-${toKebabCase(track.id)}-${stage.id}`;
717
+ const filename = `${fullName}.agent.md`;
718
+
719
+ // Build description
720
+ const disciplineDesc = discipline.description.trim().split("\n")[0];
721
+ const stageDesc = stage.description.split(" - ")[0]; // Just the short part
722
+ const description = `${stageDesc} agent for ${discipline.specialization || discipline.name} on ${track.name} track. ${disciplineDesc}`;
723
+
724
+ // Format checklist as markdown
725
+ const checklistMarkdown = formatChecklistMarkdown(agent.checklist);
726
+
727
+ // Build structured profile body data
728
+ const bodyData = buildStageProfileBodyData({
729
+ stage,
730
+ humanDiscipline: discipline,
731
+ humanTrack: track,
732
+ agentDiscipline,
733
+ agentTrack,
734
+ derivedSkills: agent.derivedSkills,
735
+ derivedBehaviours: agent.derivedBehaviours,
736
+ agentBehaviours,
737
+ skills,
738
+ checklistMarkdown,
739
+ });
740
+
741
+ // Build frontmatter
742
+ const frontmatter = {
743
+ name: fullName,
744
+ description,
745
+ infer: true,
746
+ ...(agent.handoffs.length > 0 && { handoffs: agent.handoffs }),
747
+ };
748
+
749
+ return {
750
+ frontmatter,
751
+ bodyData,
752
+ filename,
753
+ };
754
+ }