@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 +754 -0
- package/lib/checklist.js +103 -0
- package/lib/derivation.js +766 -0
- package/lib/index.js +121 -0
- package/lib/interview.js +539 -0
- package/lib/job-cache.js +89 -0
- package/lib/job.js +228 -0
- package/lib/matching.js +891 -0
- package/lib/modifiers.js +158 -0
- package/lib/profile.js +262 -0
- package/lib/progression.js +510 -0
- package/package.json +35 -0
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
|
+
}
|