@forwardimpact/pathway 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.
Files changed (227) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +104 -0
  3. package/app/commands/agent.js +430 -0
  4. package/app/commands/behaviour.js +61 -0
  5. package/app/commands/command-factory.js +211 -0
  6. package/app/commands/discipline.js +58 -0
  7. package/app/commands/driver.js +94 -0
  8. package/app/commands/grade.js +60 -0
  9. package/app/commands/index.js +20 -0
  10. package/app/commands/init.js +67 -0
  11. package/app/commands/interview.js +68 -0
  12. package/app/commands/job.js +157 -0
  13. package/app/commands/progress.js +77 -0
  14. package/app/commands/questions.js +179 -0
  15. package/app/commands/serve.js +143 -0
  16. package/app/commands/site.js +121 -0
  17. package/app/commands/skill.js +76 -0
  18. package/app/commands/stage.js +129 -0
  19. package/app/commands/track.js +70 -0
  20. package/app/components/action-buttons.js +66 -0
  21. package/app/components/behaviour-profile.js +53 -0
  22. package/app/components/builder.js +341 -0
  23. package/app/components/card.js +98 -0
  24. package/app/components/checklist.js +145 -0
  25. package/app/components/comparison-radar.js +237 -0
  26. package/app/components/detail.js +230 -0
  27. package/app/components/error-page.js +72 -0
  28. package/app/components/grid.js +109 -0
  29. package/app/components/list.js +120 -0
  30. package/app/components/modifier-table.js +142 -0
  31. package/app/components/nav.js +64 -0
  32. package/app/components/progression-table.js +320 -0
  33. package/app/components/radar-chart.js +102 -0
  34. package/app/components/skill-matrix.js +97 -0
  35. package/app/css/base.css +56 -0
  36. package/app/css/bundles/app.css +40 -0
  37. package/app/css/bundles/handout.css +43 -0
  38. package/app/css/bundles/slides.css +40 -0
  39. package/app/css/components/badges.css +215 -0
  40. package/app/css/components/buttons.css +101 -0
  41. package/app/css/components/forms.css +105 -0
  42. package/app/css/components/layout.css +209 -0
  43. package/app/css/components/nav.css +166 -0
  44. package/app/css/components/progress.css +166 -0
  45. package/app/css/components/states.css +82 -0
  46. package/app/css/components/surfaces.css +243 -0
  47. package/app/css/components/tables.css +362 -0
  48. package/app/css/components/typography.css +122 -0
  49. package/app/css/components/utilities.css +41 -0
  50. package/app/css/pages/agent-builder.css +391 -0
  51. package/app/css/pages/assessment-results.css +453 -0
  52. package/app/css/pages/detail.css +59 -0
  53. package/app/css/pages/interview-builder.css +148 -0
  54. package/app/css/pages/job-builder.css +134 -0
  55. package/app/css/pages/landing.css +92 -0
  56. package/app/css/pages/lifecycle.css +118 -0
  57. package/app/css/pages/progress-builder.css +274 -0
  58. package/app/css/pages/self-assessment.css +502 -0
  59. package/app/css/reset.css +50 -0
  60. package/app/css/tokens.css +153 -0
  61. package/app/css/views/handout.css +30 -0
  62. package/app/css/views/print.css +608 -0
  63. package/app/css/views/slide-animations.css +113 -0
  64. package/app/css/views/slide-base.css +330 -0
  65. package/app/css/views/slide-sections.css +597 -0
  66. package/app/css/views/slide-tables.css +275 -0
  67. package/app/formatters/agent/dom.js +540 -0
  68. package/app/formatters/agent/profile.js +133 -0
  69. package/app/formatters/agent/skill.js +58 -0
  70. package/app/formatters/behaviour/dom.js +91 -0
  71. package/app/formatters/behaviour/markdown.js +54 -0
  72. package/app/formatters/behaviour/shared.js +64 -0
  73. package/app/formatters/discipline/dom.js +187 -0
  74. package/app/formatters/discipline/markdown.js +87 -0
  75. package/app/formatters/discipline/shared.js +131 -0
  76. package/app/formatters/driver/dom.js +103 -0
  77. package/app/formatters/driver/shared.js +92 -0
  78. package/app/formatters/grade/dom.js +208 -0
  79. package/app/formatters/grade/markdown.js +94 -0
  80. package/app/formatters/grade/shared.js +86 -0
  81. package/app/formatters/index.js +50 -0
  82. package/app/formatters/interview/dom.js +97 -0
  83. package/app/formatters/interview/markdown.js +66 -0
  84. package/app/formatters/interview/shared.js +332 -0
  85. package/app/formatters/job/description.js +176 -0
  86. package/app/formatters/job/dom.js +411 -0
  87. package/app/formatters/job/markdown.js +102 -0
  88. package/app/formatters/progress/dom.js +135 -0
  89. package/app/formatters/progress/markdown.js +86 -0
  90. package/app/formatters/progress/shared.js +339 -0
  91. package/app/formatters/questions/json.js +43 -0
  92. package/app/formatters/questions/markdown.js +303 -0
  93. package/app/formatters/questions/shared.js +274 -0
  94. package/app/formatters/questions/yaml.js +76 -0
  95. package/app/formatters/shared.js +71 -0
  96. package/app/formatters/skill/dom.js +168 -0
  97. package/app/formatters/skill/markdown.js +109 -0
  98. package/app/formatters/skill/shared.js +125 -0
  99. package/app/formatters/stage/dom.js +135 -0
  100. package/app/formatters/stage/index.js +12 -0
  101. package/app/formatters/stage/shared.js +111 -0
  102. package/app/formatters/track/dom.js +128 -0
  103. package/app/formatters/track/markdown.js +105 -0
  104. package/app/formatters/track/shared.js +181 -0
  105. package/app/handout-main.js +421 -0
  106. package/app/handout.html +21 -0
  107. package/app/index.html +59 -0
  108. package/app/lib/card-mappers.js +173 -0
  109. package/app/lib/cli-output.js +270 -0
  110. package/app/lib/error-boundary.js +70 -0
  111. package/app/lib/errors.js +49 -0
  112. package/app/lib/form-controls.js +47 -0
  113. package/app/lib/job-cache.js +86 -0
  114. package/app/lib/markdown.js +114 -0
  115. package/app/lib/radar.js +866 -0
  116. package/app/lib/reactive.js +77 -0
  117. package/app/lib/render.js +212 -0
  118. package/app/lib/router-core.js +160 -0
  119. package/app/lib/router-pages.js +16 -0
  120. package/app/lib/router-slides.js +202 -0
  121. package/app/lib/state.js +148 -0
  122. package/app/lib/utils.js +14 -0
  123. package/app/lib/yaml-loader.js +327 -0
  124. package/app/main.js +213 -0
  125. package/app/model/agent.js +702 -0
  126. package/app/model/checklist.js +137 -0
  127. package/app/model/derivation.js +699 -0
  128. package/app/model/index-generator.js +71 -0
  129. package/app/model/interview.js +539 -0
  130. package/app/model/job.js +222 -0
  131. package/app/model/levels.js +591 -0
  132. package/app/model/loader.js +564 -0
  133. package/app/model/matching.js +858 -0
  134. package/app/model/modifiers.js +158 -0
  135. package/app/model/profile.js +266 -0
  136. package/app/model/progression.js +507 -0
  137. package/app/model/validation.js +1385 -0
  138. package/app/pages/agent-builder.js +823 -0
  139. package/app/pages/assessment-results.js +507 -0
  140. package/app/pages/behaviour.js +70 -0
  141. package/app/pages/discipline.js +71 -0
  142. package/app/pages/driver.js +106 -0
  143. package/app/pages/grade.js +117 -0
  144. package/app/pages/interview-builder.js +50 -0
  145. package/app/pages/interview.js +304 -0
  146. package/app/pages/job-builder.js +50 -0
  147. package/app/pages/job.js +58 -0
  148. package/app/pages/landing.js +305 -0
  149. package/app/pages/progress-builder.js +58 -0
  150. package/app/pages/progress.js +495 -0
  151. package/app/pages/self-assessment.js +729 -0
  152. package/app/pages/skill.js +113 -0
  153. package/app/pages/stage.js +231 -0
  154. package/app/pages/track.js +69 -0
  155. package/app/slide-main.js +360 -0
  156. package/app/slides/behaviour.js +38 -0
  157. package/app/slides/chapter.js +82 -0
  158. package/app/slides/discipline.js +40 -0
  159. package/app/slides/driver.js +39 -0
  160. package/app/slides/grade.js +32 -0
  161. package/app/slides/index.js +198 -0
  162. package/app/slides/interview.js +58 -0
  163. package/app/slides/job.js +55 -0
  164. package/app/slides/overview.js +126 -0
  165. package/app/slides/progress.js +83 -0
  166. package/app/slides/skill.js +40 -0
  167. package/app/slides/track.js +39 -0
  168. package/app/slides.html +56 -0
  169. package/app/types.js +147 -0
  170. package/bin/pathway.js +489 -0
  171. package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
  172. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
  173. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
  174. package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
  175. package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
  176. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
  177. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
  178. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
  179. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
  180. package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
  181. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
  182. package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
  183. package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
  184. package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
  185. package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
  186. package/examples/agents/.vscode/settings.json +8 -0
  187. package/examples/behaviours/_index.yaml +8 -0
  188. package/examples/behaviours/outcome_ownership.yaml +44 -0
  189. package/examples/behaviours/polymathic_knowledge.yaml +42 -0
  190. package/examples/behaviours/precise_communication.yaml +40 -0
  191. package/examples/behaviours/relentless_curiosity.yaml +38 -0
  192. package/examples/behaviours/systems_thinking.yaml +41 -0
  193. package/examples/capabilities/_index.yaml +8 -0
  194. package/examples/capabilities/business.yaml +251 -0
  195. package/examples/capabilities/delivery.yaml +352 -0
  196. package/examples/capabilities/people.yaml +100 -0
  197. package/examples/capabilities/reliability.yaml +318 -0
  198. package/examples/capabilities/scale.yaml +394 -0
  199. package/examples/disciplines/_index.yaml +5 -0
  200. package/examples/disciplines/data_engineering.yaml +76 -0
  201. package/examples/disciplines/software_engineering.yaml +76 -0
  202. package/examples/drivers.yaml +205 -0
  203. package/examples/framework.yaml +58 -0
  204. package/examples/grades.yaml +118 -0
  205. package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
  206. package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
  207. package/examples/questions/behaviours/precise_communication.yaml +55 -0
  208. package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
  209. package/examples/questions/behaviours/systems_thinking.yaml +53 -0
  210. package/examples/questions/skills/architecture_design.yaml +54 -0
  211. package/examples/questions/skills/cloud_platforms.yaml +48 -0
  212. package/examples/questions/skills/code_quality.yaml +49 -0
  213. package/examples/questions/skills/data_modeling.yaml +46 -0
  214. package/examples/questions/skills/devops.yaml +47 -0
  215. package/examples/questions/skills/full_stack_development.yaml +48 -0
  216. package/examples/questions/skills/sre_practices.yaml +44 -0
  217. package/examples/questions/skills/stakeholder_management.yaml +49 -0
  218. package/examples/questions/skills/team_collaboration.yaml +43 -0
  219. package/examples/questions/skills/technical_writing.yaml +43 -0
  220. package/examples/self-assessments.yaml +66 -0
  221. package/examples/stages.yaml +76 -0
  222. package/examples/tracks/_index.yaml +6 -0
  223. package/examples/tracks/manager.yaml +53 -0
  224. package/examples/tracks/platform.yaml +54 -0
  225. package/examples/tracks/sre.yaml +58 -0
  226. package/examples/vscode-settings.yaml +22 -0
  227. package/package.json +68 -0
@@ -0,0 +1,702 @@
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 "./levels.js";
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 (those requiring human presence/experience)
108
+ * Excludes broad skills unless they're in a capability with positive track modifier
109
+ * @param {Object} params - Parameters
110
+ * @param {Object} params.discipline - Human discipline definition
111
+ * @param {Object} params.track - Human track definition
112
+ * @param {Object} params.grade - Reference grade for derivation
113
+ * @param {Array} params.skills - All available skills
114
+ * @returns {Array} Skills sorted by derived level (highest first)
115
+ */
116
+ export function deriveAgentSkills({ discipline, track, grade, skills }) {
117
+ // Use shared derivation
118
+ const skillMatrix = deriveSkillMatrix({
119
+ discipline,
120
+ grade,
121
+ track,
122
+ skills,
123
+ });
124
+
125
+ // Apply agent-specific filtering and sorting
126
+ const filtered = filterSkillsForAgent(skillMatrix, track);
127
+ return sortByLevelDescending(filtered);
128
+ }
129
+
130
+ /**
131
+ * Derive agent behaviours using the unified profile system
132
+ * Returns behaviours sorted by maturity (highest first) for the given discipline × track
133
+ * @param {Object} params - Parameters
134
+ * @param {Object} params.discipline - Human discipline definition
135
+ * @param {Object} params.track - Human track definition
136
+ * @param {Object} params.grade - Reference grade for derivation
137
+ * @param {Array} params.behaviours - All available behaviours
138
+ * @returns {Array} Behaviours sorted by derived maturity (highest first)
139
+ */
140
+ export function deriveAgentBehaviours({
141
+ discipline,
142
+ track,
143
+ grade,
144
+ behaviours,
145
+ }) {
146
+ const profile = deriveBehaviourProfile({
147
+ discipline,
148
+ grade,
149
+ track,
150
+ behaviours,
151
+ });
152
+
153
+ return sortByMaturityDescending(profile);
154
+ }
155
+
156
+ /**
157
+ * Substitute template variables in text
158
+ * @param {string} text - Text with {roleTitle}, {specialization} placeholders
159
+ * @param {Object} discipline - Discipline with roleTitle, specialization properties
160
+ * @returns {string} Text with substituted values
161
+ */
162
+ function substituteTemplateVars(text, discipline) {
163
+ return text
164
+ .replace(/\{roleTitle\}/g, discipline.roleTitle)
165
+ .replace(/\{specialization\}/g, discipline.specialization);
166
+ }
167
+
168
+ /**
169
+ * Find an agent behaviour by id
170
+ * @param {Array} agentBehaviours - Array of agent behaviour definitions
171
+ * @param {string} id - Behaviour id to find
172
+ * @returns {Object|undefined} Agent behaviour or undefined
173
+ */
174
+ function findAgentBehaviour(agentBehaviours, id) {
175
+ return agentBehaviours.find((b) => b.id === id);
176
+ }
177
+
178
+ /**
179
+ * Build working style section from emphasized behaviours
180
+ * Includes workflow patterns when available
181
+ * @param {Array} derivedBehaviours - Behaviours sorted by maturity (highest first)
182
+ * @param {Array} agentBehaviours - Agent behaviour definitions with principles
183
+ * @param {number} topN - Number of top behaviours to include
184
+ * @returns {string} Working style markdown section
185
+ */
186
+ function buildWorkingStyleFromBehaviours(
187
+ derivedBehaviours,
188
+ agentBehaviours,
189
+ topN = 3,
190
+ ) {
191
+ const sections = [];
192
+ sections.push("## Working Style");
193
+ sections.push("");
194
+
195
+ // Get top N behaviours by maturity
196
+ const topBehaviours = derivedBehaviours.slice(0, topN);
197
+
198
+ for (const derived of topBehaviours) {
199
+ const agentBehaviour = findAgentBehaviour(
200
+ agentBehaviours,
201
+ derived.behaviourId,
202
+ );
203
+ // Skip if no agent behaviour data or no content to display
204
+ if (!agentBehaviour) continue;
205
+ if (!agentBehaviour.workingStyle && !agentBehaviour.principles) continue;
206
+
207
+ // Use title as section header
208
+ const title = agentBehaviour.title || derived.behaviourName;
209
+ sections.push(`### ${title}`);
210
+ sections.push("");
211
+
212
+ // Include workingStyle if available (structured guidance)
213
+ if (agentBehaviour.workingStyle) {
214
+ sections.push(agentBehaviour.workingStyle.trim());
215
+ sections.push("");
216
+ } else if (agentBehaviour.principles) {
217
+ // Fall back to principles
218
+ const principles = agentBehaviour.principles.trim();
219
+ sections.push(principles);
220
+ sections.push("");
221
+ }
222
+ }
223
+
224
+ return sections.join("\n");
225
+ }
226
+
227
+ /**
228
+ * Generate SKILL.md content from skill data
229
+ * @param {Object} skillData - Skill with agent section
230
+ * @returns {Object} Skill with frontmatter, body, and dirname
231
+ */
232
+ export function generateSkillMd(skillData) {
233
+ const { agent } = skillData;
234
+
235
+ if (!agent) {
236
+ throw new Error(`Skill ${skillData.id} has no agent section`);
237
+ }
238
+
239
+ return {
240
+ frontmatter: {
241
+ name: agent.name,
242
+ description: agent.description.trim(),
243
+ },
244
+ body: agent.body.trim(),
245
+ dirname: agent.name,
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Validate agent profile against spec constraints
251
+ * @param {Object} profile - Generated profile
252
+ * @returns {Array<string>} Array of error messages (empty if valid)
253
+ */
254
+ export function validateAgentProfile(profile) {
255
+ const errors = [];
256
+
257
+ // Required: description
258
+ if (!profile.frontmatter.description) {
259
+ errors.push("Missing required field: description");
260
+ }
261
+
262
+ // Name format (if provided)
263
+ if (profile.frontmatter.name) {
264
+ if (!/^[a-zA-Z0-9._-]+$/.test(profile.frontmatter.name)) {
265
+ errors.push("Name contains invalid characters");
266
+ }
267
+ }
268
+
269
+ // Body length limit (30,000 chars)
270
+ if (profile.body.length > 30000) {
271
+ errors.push(`Body exceeds 30,000 character limit (${profile.body.length})`);
272
+ }
273
+
274
+ // Tools format
275
+ if (profile.frontmatter.tools && !Array.isArray(profile.frontmatter.tools)) {
276
+ errors.push("Tools must be an array");
277
+ }
278
+
279
+ return errors;
280
+ }
281
+
282
+ /**
283
+ * Validate agent skill against spec constraints
284
+ * @param {Object} skill - Generated skill
285
+ * @returns {Array<string>} Array of error messages (empty if valid)
286
+ */
287
+ export function validateAgentSkill(skill) {
288
+ const errors = [];
289
+
290
+ // Required: name
291
+ if (!skill.frontmatter.name) {
292
+ errors.push("Missing required field: name");
293
+ } else {
294
+ const name = skill.frontmatter.name;
295
+
296
+ // Name format: lowercase, hyphens, 1-64 chars
297
+ if (!/^[a-z0-9-]+$/.test(name)) {
298
+ errors.push("Name must be lowercase alphanumeric with hyphens");
299
+ }
300
+ if (name.length > 64) {
301
+ errors.push("Name exceeds 64 character limit");
302
+ }
303
+ if (name.startsWith("-") || name.endsWith("-")) {
304
+ errors.push("Name cannot start or end with hyphen");
305
+ }
306
+ if (name.includes("--")) {
307
+ errors.push("Name cannot contain consecutive hyphens");
308
+ }
309
+ }
310
+
311
+ // Required: description
312
+ if (!skill.frontmatter.description) {
313
+ errors.push("Missing required field: description");
314
+ } else if (skill.frontmatter.description.length > 1024) {
315
+ errors.push("Description exceeds 1024 character limit");
316
+ }
317
+
318
+ return errors;
319
+ }
320
+
321
+ // =============================================================================
322
+ // Stage-Based Agent Generation
323
+ // =============================================================================
324
+
325
+ /**
326
+ * Derive tools for a stage-based agent
327
+ * Stages define the authoritative tool set for each lifecycle phase
328
+ * @param {Object} params - Parameters
329
+ * @param {Object} params.stage - Stage definition from stages.yaml
330
+ * @returns {string[]} Array of tool names
331
+ */
332
+ export function deriveAgentTools({ stage }) {
333
+ return stage.availableTools || [];
334
+ }
335
+
336
+ /**
337
+ * Derive handoff buttons for a stage-based agent
338
+ * Generates handoff button definitions from stage.handoffs with rich prompts
339
+ * that include summary instructions and target stage entry criteria
340
+ * @param {Object} params - Parameters
341
+ * @param {Object} params.stage - Stage definition
342
+ * @param {Object} params.discipline - Human discipline definition (for naming)
343
+ * @param {Object} params.track - Human track definition (for naming)
344
+ * @param {Array} params.stages - All stages (to look up target stage entry criteria)
345
+ * @returns {Array<{label: string, agent: string, prompt: string, send: boolean}>} Handoff definitions
346
+ */
347
+ export function deriveHandoffs({ stage, discipline, track, stages }) {
348
+ if (!stage.handoffs || stage.handoffs.length === 0) {
349
+ return [];
350
+ }
351
+
352
+ // Build base name for target agents
353
+ const baseName = `${toKebabCase(discipline.id)}-${toKebabCase(track.id)}`;
354
+
355
+ return stage.handoffs.map((handoff) => {
356
+ // Find the target stage to get its entry criteria
357
+ const targetStage = stages.find((s) => s.id === handoff.targetStage);
358
+ const entryCriteria = targetStage?.entryCriteria || [];
359
+
360
+ // Build rich prompt - formatted for single-line display
361
+ const promptParts = [handoff.prompt];
362
+
363
+ // Add summary instruction
364
+ promptParts.push(
365
+ `Summarize what was completed in the ${stage.name} stage.`,
366
+ );
367
+
368
+ // Add entry criteria from target stage with inline numbered list
369
+ if (entryCriteria.length > 0) {
370
+ const formattedCriteria = entryCriteria
371
+ .map((item, index) => `(${index + 1}) ${item}`)
372
+ .join(", ");
373
+ promptParts.push(
374
+ `Before starting, the ${targetStage.name} stage requires: ${formattedCriteria}.`,
375
+ );
376
+ promptParts.push(
377
+ `If critical items are missing, hand back to ${stage.name}.`,
378
+ );
379
+ }
380
+
381
+ return {
382
+ label: handoff.label,
383
+ agent: `${baseName}-${handoff.targetStage}`,
384
+ prompt: promptParts.join(" "),
385
+ send: true,
386
+ };
387
+ });
388
+ }
389
+
390
+ /**
391
+ * Get the handoff type for a stage (used for checklist derivation)
392
+ * @param {string} stageId - Stage ID (plan, code, review)
393
+ * @returns {string|null} Handoff type or null
394
+ */
395
+ function getHandoffForStage(stageId) {
396
+ const handoffMap = {
397
+ plan: "plan_to_code",
398
+ code: "code_to_review",
399
+ review: null, // Review stage doesn't need a checklist
400
+ };
401
+ return handoffMap[stageId] || null;
402
+ }
403
+
404
+ /**
405
+ * Build the profile body for a stage-based agent
406
+ * @param {Object} params - Parameters
407
+ * @param {Object} params.stage - Stage definition
408
+ * @param {Object} params.humanDiscipline - Human discipline definition
409
+ * @param {Object} params.humanTrack - Human track definition
410
+ * @param {Object} params.agentDiscipline - Agent discipline definition
411
+ * @param {Object} params.agentTrack - Agent track definition
412
+ * @param {Array} params.derivedSkills - Skills sorted by level
413
+ * @param {Array} params.derivedBehaviours - Behaviours sorted by maturity
414
+ * @param {Array} params.agentBehaviours - Agent behaviour definitions
415
+ * @param {string} params.checklistMarkdown - Pre-formatted checklist markdown
416
+ * @returns {string} Profile body markdown
417
+ */
418
+ function buildStageProfileBody({
419
+ stage,
420
+ humanDiscipline,
421
+ humanTrack,
422
+ agentDiscipline,
423
+ agentTrack,
424
+ derivedSkills,
425
+ derivedBehaviours,
426
+ agentBehaviours,
427
+ checklistMarkdown,
428
+ }) {
429
+ const name = `${humanDiscipline.specialization || humanDiscipline.name} - ${humanTrack.name}`;
430
+ const stageName = stage.name.charAt(0).toUpperCase() + stage.name.slice(1);
431
+ const sections = [];
432
+
433
+ // Title with stage indicator
434
+ sections.push(`# ${name} - ${stageName} Agent`);
435
+ sections.push("");
436
+
437
+ // Stage description
438
+ sections.push(stage.description);
439
+ sections.push("");
440
+
441
+ // Core Identity
442
+ sections.push("## Core Identity");
443
+ sections.push("");
444
+
445
+ // Use track coreInstructions if available, with template substitution
446
+ const rawInstructions =
447
+ agentTrack.coreInstructions || agentDiscipline.coreInstructions;
448
+ const coreInstructions = substituteTemplateVars(
449
+ rawInstructions,
450
+ humanDiscipline,
451
+ );
452
+ sections.push(coreInstructions.trim());
453
+ sections.push("");
454
+
455
+ // Primary capabilities from derived skills
456
+ const topSkills = derivedSkills.slice(0, 6);
457
+ if (topSkills.length > 0) {
458
+ sections.push("Your primary capabilities:");
459
+ for (const skill of topSkills) {
460
+ sections.push(`- ${skill.skillName}`);
461
+ }
462
+ sections.push("");
463
+ }
464
+
465
+ // Operational Context - use track's roleContext (shared with human job descriptions)
466
+ sections.push("## Operational Context");
467
+ sections.push("");
468
+ sections.push(humanTrack.roleContext.trim());
469
+ sections.push("");
470
+
471
+ // Working Style from derived behaviours
472
+ const workingStyle = buildWorkingStyleFromBehaviours(
473
+ derivedBehaviours,
474
+ agentBehaviours,
475
+ 3,
476
+ );
477
+ sections.push(workingStyle);
478
+
479
+ // Before Handoff (if provided)
480
+ if (checklistMarkdown) {
481
+ sections.push("## Before Handoff");
482
+ sections.push("");
483
+ sections.push(
484
+ "Before offering a handoff, verify and summarize completion of these items:",
485
+ );
486
+ sections.push("");
487
+ sections.push(checklistMarkdown);
488
+ sections.push("");
489
+ sections.push(
490
+ "When verified, summarize what was accomplished then offer the handoff.",
491
+ );
492
+ sections.push("If items are incomplete, explain what remains.");
493
+ sections.push("");
494
+ }
495
+
496
+ // Return Format section
497
+ sections.push("## Return Format");
498
+ sections.push("");
499
+ sections.push(
500
+ "When completing work (for handoff or as a subagent), provide:",
501
+ );
502
+ sections.push("");
503
+ sections.push("1. **Work completed**: What was accomplished");
504
+ sections.push(
505
+ "2. **Checklist status**: Items verified from Before Handoff section",
506
+ );
507
+ sections.push(
508
+ "3. **Recommendation**: Ready for next stage, or needs more work",
509
+ );
510
+ sections.push("");
511
+
512
+ // Constraints (stage + discipline + track)
513
+ const allConstraints = [
514
+ ...(stage.constraints || []),
515
+ ...(agentDiscipline.constraints || []),
516
+ ...(agentTrack.constraints || []),
517
+ ];
518
+ if (allConstraints.length > 0) {
519
+ sections.push("## Constraints");
520
+ sections.push("");
521
+ for (const constraint of allConstraints) {
522
+ sections.push(`- ${constraint}`);
523
+ }
524
+ sections.push("");
525
+ }
526
+
527
+ return sections.join("\n");
528
+ }
529
+
530
+ /**
531
+ * Derive a stage-specific agent profile
532
+ * Combines discipline, track, and stage to produce a complete agent definition
533
+ * @param {Object} params - Parameters
534
+ * @param {Object} params.discipline - Human discipline definition
535
+ * @param {Object} params.track - Human track definition
536
+ * @param {Object} params.stage - Stage definition from stages.yaml
537
+ * @param {Object} params.grade - Reference grade for skill derivation
538
+ * @param {Array} params.skills - All available skills
539
+ * @param {Array} params.behaviours - All available behaviours
540
+ * @param {Array} params.agentBehaviours - Agent behaviour definitions
541
+ * @param {Object} params.agentDiscipline - Agent discipline definition
542
+ * @param {Object} params.agentTrack - Agent track definition
543
+ * @param {Array} params.capabilities - Capabilities with checklists
544
+ * @param {Array} params.stages - All stages (for handoff entry criteria)
545
+ * @returns {Object} Agent definition with skills, behaviours, tools, handoffs, constraints, checklist
546
+ */
547
+ export function deriveStageAgent({
548
+ discipline,
549
+ track,
550
+ stage,
551
+ grade,
552
+ skills,
553
+ behaviours,
554
+ agentBehaviours,
555
+ agentDiscipline,
556
+ agentTrack,
557
+ capabilities,
558
+ stages,
559
+ }) {
560
+ // Derive skills and behaviours
561
+ const derivedSkills = deriveAgentSkills({
562
+ discipline,
563
+ track,
564
+ grade,
565
+ skills,
566
+ });
567
+
568
+ const derivedBehaviours = deriveAgentBehaviours({
569
+ discipline,
570
+ track,
571
+ grade,
572
+ behaviours,
573
+ });
574
+
575
+ // Derive tools from stage
576
+ const tools = deriveAgentTools({ stage });
577
+
578
+ // Derive handoffs from stage
579
+ const handoffs = deriveHandoffs({
580
+ stage,
581
+ discipline,
582
+ track,
583
+ stages,
584
+ });
585
+
586
+ // Derive checklist if applicable
587
+ const handoffType = getHandoffForStage(stage.id);
588
+ let checklist = [];
589
+ if (handoffType && capabilities) {
590
+ checklist = deriveChecklist({
591
+ handoff: handoffType,
592
+ skillMatrix: derivedSkills,
593
+ capabilities,
594
+ });
595
+ }
596
+
597
+ return {
598
+ stage,
599
+ discipline,
600
+ track,
601
+ derivedSkills,
602
+ derivedBehaviours,
603
+ tools,
604
+ handoffs,
605
+ constraints: [
606
+ ...(stage.constraints || []),
607
+ ...(agentDiscipline.constraints || []),
608
+ ...(agentTrack.constraints || []),
609
+ ],
610
+ checklist,
611
+ agentDiscipline,
612
+ agentTrack,
613
+ agentBehaviours,
614
+ };
615
+ }
616
+
617
+ /**
618
+ * Generate a stage-specific agent profile (.agent.md)
619
+ * Produces the complete profile with frontmatter, body, and filename
620
+ * @param {Object} params - Parameters
621
+ * @param {Object} params.discipline - Human discipline definition
622
+ * @param {Object} params.track - Human track definition
623
+ * @param {Object} params.stage - Stage definition
624
+ * @param {Object} params.grade - Reference grade
625
+ * @param {Array} params.skills - All skills
626
+ * @param {Array} params.behaviours - All behaviours
627
+ * @param {Array} params.agentBehaviours - Agent behaviour definitions
628
+ * @param {Object} params.agentDiscipline - Agent discipline definition
629
+ * @param {Object} params.agentTrack - Agent track definition
630
+ * @param {Array} params.capabilities - Capabilities with checklists
631
+ * @param {Array} params.stages - All stages (for handoff entry criteria)
632
+ * @returns {Object} Profile with frontmatter, body, and filename
633
+ */
634
+ export function generateStageAgentProfile({
635
+ discipline,
636
+ track,
637
+ stage,
638
+ grade,
639
+ skills,
640
+ behaviours,
641
+ agentBehaviours,
642
+ agentDiscipline,
643
+ agentTrack,
644
+ capabilities,
645
+ stages,
646
+ }) {
647
+ // Derive the complete agent
648
+ const agent = deriveStageAgent({
649
+ discipline,
650
+ track,
651
+ stage,
652
+ grade,
653
+ skills,
654
+ behaviours,
655
+ agentBehaviours,
656
+ agentDiscipline,
657
+ agentTrack,
658
+ capabilities,
659
+ stages,
660
+ });
661
+
662
+ // Build names
663
+ const fullName = `${toKebabCase(discipline.id)}-${toKebabCase(track.id)}-${stage.id}`;
664
+ const abbrev = getDisciplineAbbreviation(discipline.id);
665
+ const filename = `${abbrev}-${toKebabCase(track.id)}-${stage.id}.agent.md`;
666
+
667
+ // Build description
668
+ const disciplineDesc = discipline.description.trim().split("\n")[0];
669
+ const stageDesc = stage.description.split(" - ")[0]; // Just the short part
670
+ const description = `${stageDesc} agent for ${discipline.specialization || discipline.name} on ${track.name} track. ${disciplineDesc}`;
671
+
672
+ // Format checklist as markdown
673
+ const checklistMarkdown = formatChecklistMarkdown(agent.checklist);
674
+
675
+ // Build profile body
676
+ const body = buildStageProfileBody({
677
+ stage,
678
+ humanDiscipline: discipline,
679
+ humanTrack: track,
680
+ agentDiscipline,
681
+ agentTrack,
682
+ derivedSkills: agent.derivedSkills,
683
+ derivedBehaviours: agent.derivedBehaviours,
684
+ agentBehaviours,
685
+ checklistMarkdown,
686
+ });
687
+
688
+ // Build frontmatter
689
+ const frontmatter = {
690
+ name: fullName,
691
+ description,
692
+ tools: agent.tools,
693
+ infer: true,
694
+ ...(agent.handoffs.length > 0 && { handoffs: agent.handoffs }),
695
+ };
696
+
697
+ return {
698
+ frontmatter,
699
+ body,
700
+ filename,
701
+ };
702
+ }