@forwardimpact/pathway 0.2.0 → 0.4.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 (62) hide show
  1. package/app/commands/agent.js +20 -20
  2. package/app/commands/index.js +4 -3
  3. package/app/commands/job.js +9 -4
  4. package/app/commands/skill.js +56 -2
  5. package/app/commands/tool.js +112 -0
  6. package/app/components/builder.js +6 -3
  7. package/app/components/checklist.js +6 -4
  8. package/app/components/markdown-textarea.js +132 -0
  9. package/app/css/components/forms.css +45 -0
  10. package/app/css/components/layout.css +12 -0
  11. package/app/css/components/surfaces.css +22 -0
  12. package/app/css/pages/detail.css +50 -0
  13. package/app/css/pages/job-builder.css +0 -42
  14. package/app/formatters/agent/profile.js +61 -120
  15. package/app/formatters/agent/skill.js +48 -60
  16. package/app/formatters/grade/dom.js +2 -4
  17. package/app/formatters/job/description.js +74 -82
  18. package/app/formatters/job/dom.js +45 -179
  19. package/app/formatters/job/markdown.js +17 -13
  20. package/app/formatters/shared.js +65 -2
  21. package/app/formatters/skill/dom.js +57 -2
  22. package/app/formatters/skill/markdown.js +18 -0
  23. package/app/formatters/skill/shared.js +12 -4
  24. package/app/formatters/stage/microdata.js +1 -1
  25. package/app/formatters/stage/shared.js +1 -1
  26. package/app/formatters/tool/shared.js +72 -0
  27. package/app/handout-main.js +7 -7
  28. package/app/handout.html +7 -0
  29. package/app/index.html +10 -3
  30. package/app/lib/card-mappers.js +64 -17
  31. package/app/lib/form-controls.js +64 -1
  32. package/app/lib/render.js +12 -1
  33. package/app/lib/template-loader.js +9 -0
  34. package/app/lib/yaml-loader.js +12 -1
  35. package/app/main.js +4 -0
  36. package/app/model/agent.js +26 -18
  37. package/app/model/derivation.js +3 -3
  38. package/app/model/levels.js +2 -0
  39. package/app/model/loader.js +12 -1
  40. package/app/model/validation.js +74 -8
  41. package/app/pages/agent-builder.js +8 -5
  42. package/app/pages/job.js +28 -4
  43. package/app/pages/landing.js +34 -14
  44. package/app/pages/progress.js +6 -5
  45. package/app/pages/self-assessment.js +10 -8
  46. package/app/pages/skill.js +5 -17
  47. package/app/pages/stage.js +10 -6
  48. package/app/pages/tool.js +50 -0
  49. package/app/slides/index.js +25 -25
  50. package/app/slides.html +7 -0
  51. package/bin/pathway.js +41 -27
  52. package/examples/capabilities/business.yaml +17 -17
  53. package/examples/capabilities/delivery.yaml +51 -36
  54. package/examples/capabilities/reliability.yaml +127 -114
  55. package/examples/capabilities/scale.yaml +38 -36
  56. package/examples/disciplines/engineering_management.yaml +1 -1
  57. package/examples/framework.yaml +12 -0
  58. package/examples/grades.yaml +18 -19
  59. package/examples/self-assessments.yaml +1 -1
  60. package/package.json +1 -1
  61. package/templates/job.template.md +47 -0
  62. package/templates/skill.template.md +31 -12
@@ -135,25 +135,25 @@ function renderIndex(data) {
135
135
  `${getConceptEmoji(framework, "job")} ${framework.entityDefinitions.job.title}`,
136
136
  ),
137
137
  " - ",
138
- `${data.disciplines.length} disciplines, ${data.tracks.length} tracks, ${data.grades.length} grades`,
138
+ `${data.disciplines.length} disciplines, ${data.grades.length} grades, ${data.tracks.length} tracks`,
139
139
  ),
140
140
  li(
141
141
  {},
142
142
  a(
143
- { href: "#/skill" },
144
- `${getConceptEmoji(framework, "skill")} ${framework.entityDefinitions.skill.title}`,
143
+ { href: "#/behaviour" },
144
+ `${getConceptEmoji(framework, "behaviour")} ${framework.entityDefinitions.behaviour.title}`,
145
145
  ),
146
146
  " - ",
147
- `${data.skills.length} skill definitions`,
147
+ `${data.behaviours.length} behaviour definitions`,
148
148
  ),
149
149
  li(
150
150
  {},
151
151
  a(
152
- { href: "#/behaviour" },
153
- `${getConceptEmoji(framework, "behaviour")} ${framework.entityDefinitions.behaviour.title}`,
152
+ { href: "#/skill" },
153
+ `${getConceptEmoji(framework, "skill")} ${framework.entityDefinitions.skill.title}`,
154
154
  ),
155
155
  " - ",
156
- `${data.behaviours.length} behaviour definitions`,
156
+ `${data.skills.length} skill definitions`,
157
157
  ),
158
158
  li(
159
159
  {},
package/app/handout.html CHANGED
@@ -5,6 +5,13 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Engineering Pathway - Handout View</title>
7
7
  <link rel="stylesheet" href="css/bundles/handout.css" />
8
+ <script type="importmap">
9
+ {
10
+ "imports": {
11
+ "mustache": "https://esm.sh/mustache@4.2.0"
12
+ }
13
+ }
14
+ </script>
8
15
  </head>
9
16
  <body class="slide-view handout-view">
10
17
  <header
package/app/index.html CHANGED
@@ -5,6 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Engineering Pathway</title>
7
7
  <link rel="stylesheet" href="css/bundles/app.css" />
8
+ <link
9
+ rel="stylesheet"
10
+ href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css"
11
+ />
12
+ <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
13
+ <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-markdown.min.js"></script>
8
14
  <script type="importmap">
9
15
  {
10
16
  "imports": {
@@ -31,12 +37,13 @@
31
37
  </button>
32
38
  <ul class="nav-links" id="nav-links">
33
39
  <li><a href="#/discipline">Disciplines</a></li>
34
- <li><a href="#/track">Tracks</a></li>
35
40
  <li><a href="#/grade">Grades</a></li>
36
- <li><a href="#/skill">Skills</a></li>
41
+ <li><a href="#/track">Tracks</a></li>
37
42
  <li><a href="#/behaviour">Behaviours</a></li>
38
- <li><a href="#/stage">Stages</a></li>
43
+ <li><a href="#/skill">Skills</a></li>
39
44
  <li><a href="#/driver">Drivers</a></li>
45
+ <li><a href="#/stage">Stages</a></li>
46
+ <li><a href="#/tool">Tools</a></li>
40
47
  <li><a href="#/job-builder" class="nav-cta">Build a Job</a></li>
41
48
  <li><a href="#/agent-builder" class="nav-cta">Build an Agent</a></li>
42
49
  <li><a href="#/interview-prep" class="nav-cta">Interview Prep</a></li>
@@ -9,6 +9,23 @@ import { createBadge } from "../components/card.js";
9
9
  import { formatLevel } from "./render.js";
10
10
  import { getCapabilityEmoji } from "../model/levels.js";
11
11
 
12
+ /**
13
+ * Create an external link element styled as a badge
14
+ * @param {string} text - Link text
15
+ * @param {string} url - External URL
16
+ * @returns {HTMLElement}
17
+ */
18
+ function createExternalLink(text, url) {
19
+ const link = document.createElement("a");
20
+ link.href = url;
21
+ link.target = "_blank";
22
+ link.rel = "noopener noreferrer";
23
+ link.className = "badge badge-primary";
24
+ link.textContent = text;
25
+ link.addEventListener("click", (e) => e.stopPropagation()); // Don't trigger card click
26
+ return link;
27
+ }
28
+
12
29
  /**
13
30
  * Map discipline to card config
14
31
  * @param {Object} discipline
@@ -155,24 +172,54 @@ export function jobToCardConfig(job) {
155
172
  }
156
173
 
157
174
  /**
158
- * Format capability for display
159
- * @param {string} capability
175
+ * Map tool to card config
176
+ * @param {Object} tool - Aggregated tool with usages
177
+ * @param {Array} capabilities - Capability entities for emoji lookup
178
+ * @returns {Object}
179
+ */
180
+ export function toolToCardConfig(tool, capabilities) {
181
+ // Create skills list as card content
182
+ const skillsList = createSkillsList(tool.usages, capabilities);
183
+
184
+ return {
185
+ title: tool.name,
186
+ description: tool.description,
187
+ // Docs link in header badges (upper right)
188
+ badges: tool.url ? [createExternalLink("Docs →", tool.url)] : [],
189
+ content: skillsList,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Create an unordered list of skill links with capability emoji
195
+ * @param {Array} usages - Tool usage objects with skillId, skillName, capabilityId
196
+ * @param {Array} capabilities - Capability entities
197
+ * @returns {HTMLElement}
198
+ */
199
+ function createSkillsList(usages, capabilities) {
200
+ const ul = document.createElement("ul");
201
+ ul.className = "tool-skills-list";
202
+
203
+ for (const usage of usages) {
204
+ const emoji = getCapabilityEmoji(capabilities, usage.capabilityId);
205
+ const li = document.createElement("li");
206
+ const link = document.createElement("a");
207
+ link.href = `#/skill/${usage.skillId}`;
208
+ link.textContent = `${emoji} ${usage.skillName}`;
209
+ li.appendChild(link);
210
+ ul.appendChild(li);
211
+ }
212
+
213
+ return ul;
214
+ }
215
+
216
+ /**
217
+ * Format capability for badge display (short, tag-like)
218
+ * @param {string} capabilityId
160
219
  * @param {Array} capabilities
161
220
  * @returns {string}
162
221
  */
163
- function formatCapability(capability, capabilities) {
164
- const capabilityLabels = {
165
- delivery: "Delivery",
166
- scale: "Scale",
167
- reliability: "Reliability",
168
- data: "Data",
169
- ai: "AI",
170
- process: "Process",
171
- business: "Business",
172
- people: "People",
173
- documentation: "Documentation",
174
- };
175
- const label = capabilityLabels[capability] || formatLevel(capability);
176
- const emoji = getCapabilityEmoji(capabilities, capability);
177
- return `${emoji} ${label}`;
222
+ function formatCapability(capabilityId, capabilities) {
223
+ const emoji = getCapabilityEmoji(capabilities, capabilityId);
224
+ return `${emoji} ${capabilityId.toUpperCase()}`;
178
225
  }
@@ -2,7 +2,7 @@
2
2
  * Reusable form control components
3
3
  */
4
4
 
5
- import { select, option } from "./render.js";
5
+ import { select, option, optgroup } from "./render.js";
6
6
 
7
7
  /**
8
8
  * Create a select element with initial value and change handler
@@ -45,3 +45,66 @@ export function createSelectWithValue({
45
45
 
46
46
  return selectEl;
47
47
  }
48
+
49
+ /**
50
+ * Create a discipline select with optgroups for Professional and Management
51
+ * @param {Object} options - Configuration options
52
+ * @param {string} options.id - Element ID
53
+ * @param {Array} options.disciplines - Array of discipline objects
54
+ * @param {string} options.initialValue - Initial selected value
55
+ * @param {string} options.placeholder - Placeholder text for empty option
56
+ * @param {Function} options.onChange - Callback when selection changes
57
+ * @param {Function} [options.getDisplayName] - Optional function to get display name from item
58
+ * @returns {HTMLElement}
59
+ */
60
+ export function createDisciplineSelect({
61
+ id,
62
+ disciplines,
63
+ initialValue,
64
+ placeholder,
65
+ onChange,
66
+ getDisplayName,
67
+ }) {
68
+ const displayFn = getDisplayName || ((d) => d.specialization || d.name);
69
+
70
+ // Separate disciplines by type
71
+ const professional = disciplines.filter((d) => d.isProfessional);
72
+ const management = disciplines.filter((d) => d.isManagement);
73
+
74
+ // Sort each group alphabetically by display name
75
+ professional.sort((a, b) => displayFn(a).localeCompare(displayFn(b)));
76
+ management.sort((a, b) => displayFn(a).localeCompare(displayFn(b)));
77
+
78
+ // Build options for a group
79
+ const buildOptions = (items) =>
80
+ items.map((item) => {
81
+ const opt = option({ value: item.id }, displayFn(item));
82
+ if (item.id === initialValue) {
83
+ opt.selected = true;
84
+ }
85
+ return opt;
86
+ });
87
+
88
+ // Build optgroups - Professional first, then Management
89
+ const groups = [];
90
+ if (professional.length > 0) {
91
+ groups.push(
92
+ optgroup({ label: "Professional" }, ...buildOptions(professional)),
93
+ );
94
+ }
95
+ if (management.length > 0) {
96
+ groups.push(optgroup({ label: "Management" }, ...buildOptions(management)));
97
+ }
98
+
99
+ const selectEl = select(
100
+ { className: "form-select", id },
101
+ option({ value: "" }, placeholder),
102
+ ...groups,
103
+ );
104
+
105
+ selectEl.addEventListener("change", (e) => {
106
+ onChange(e.target.value);
107
+ });
108
+
109
+ return selectEl;
110
+ }
package/app/lib/render.js CHANGED
@@ -111,6 +111,10 @@ export const th = (attrs, ...children) =>
111
111
  createElement("th", attrs, ...children);
112
112
  export const td = (attrs, ...children) =>
113
113
  createElement("td", attrs, ...children);
114
+ export const pre = (attrs, ...children) =>
115
+ createElement("pre", attrs, ...children);
116
+ export const code = (attrs, ...children) =>
117
+ createElement("code", attrs, ...children);
114
118
  export const button = (attrs, ...children) =>
115
119
  createElement("button", attrs, ...children);
116
120
  export const input = (attrs) => createElement("input", attrs);
@@ -118,6 +122,8 @@ export const select = (attrs, ...children) =>
118
122
  createElement("select", attrs, ...children);
119
123
  export const option = (attrs, ...children) =>
120
124
  createElement("option", attrs, ...children);
125
+ export const optgroup = (attrs, ...children) =>
126
+ createElement("optgroup", attrs, ...children);
121
127
  export const label = (attrs, ...children) =>
122
128
  createElement("label", attrs, ...children);
123
129
  export const form = (attrs, ...children) =>
@@ -185,12 +191,17 @@ export function showError(message) {
185
191
 
186
192
  /**
187
193
  * Format a skill level or behaviour maturity for display
194
+ * Handles both snake_case and camelCase
188
195
  * @param {string} value - The level/maturity value
189
196
  * @returns {string}
190
197
  */
191
198
  export function formatLevel(value) {
192
199
  if (!value) return "";
193
- return value.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
200
+ // Insert space before uppercase letters (for camelCase), then handle snake_case
201
+ return value
202
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
203
+ .replace(/_/g, " ")
204
+ .replace(/\b\w/g, (c) => c.toUpperCase());
194
205
  }
195
206
 
196
207
  /**
@@ -64,3 +64,12 @@ export async function loadAgentTemplate(dataDir) {
64
64
  export async function loadSkillTemplate(dataDir) {
65
65
  return loadTemplate("skill.template.md", dataDir);
66
66
  }
67
+
68
+ /**
69
+ * Load job description template
70
+ * @param {string} dataDir - Path to data directory
71
+ * @returns {Promise<string>} Job template content
72
+ */
73
+ export async function loadJobTemplate(dataDir) {
74
+ return loadTemplate("job.template.md", dataDir);
75
+ }
@@ -70,7 +70,15 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
70
70
 
71
71
  if (capability.skills && Array.isArray(capability.skills)) {
72
72
  for (const skill of capability.skills) {
73
- const { id, name, isHumanOnly, human, agent } = skill;
73
+ const {
74
+ id,
75
+ name,
76
+ isHumanOnly,
77
+ human,
78
+ agent,
79
+ implementationReference,
80
+ toolReferences,
81
+ } = skill;
74
82
  allSkills.push({
75
83
  id,
76
84
  name,
@@ -80,6 +88,9 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
80
88
  // Include isHumanOnly flag for agent filtering (defaults to false)
81
89
  ...(isHumanOnly && { isHumanOnly }),
82
90
  ...(agent && { agent }),
91
+ // Include implementation reference and tool references (shared by human and agent)
92
+ ...(implementationReference && { implementationReference }),
93
+ ...(toolReferences && { toolReferences }),
83
94
  });
84
95
  }
85
96
  }
package/app/main.js CHANGED
@@ -26,6 +26,7 @@ import { renderTracksList, renderTrackDetail } from "./pages/track.js";
26
26
  import { renderGradesList, renderGradeDetail } from "./pages/grade.js";
27
27
  import { renderDriversList, renderDriverDetail } from "./pages/driver.js";
28
28
  import { renderStagesList, renderStageDetail } from "./pages/stage.js";
29
+ import { renderToolsList } from "./pages/tool.js";
29
30
  import { renderJobBuilder } from "./pages/job-builder.js";
30
31
  import { renderJobDetail } from "./pages/job.js";
31
32
  import { renderInterviewPrep } from "./pages/interview-builder.js";
@@ -99,6 +100,9 @@ function setupRoutes() {
99
100
  router.on("/stage", renderStagesList);
100
101
  router.on("/stage/:id", renderStageDetail);
101
102
 
103
+ // Tool
104
+ router.on("/tool", renderToolsList);
105
+
102
106
  // Job builder
103
107
  router.on("/job-builder", renderJobBuilder);
104
108
  router.on("/job/:discipline/:grade/:track", renderJobDetail);
@@ -225,21 +225,13 @@ function buildWorkingStyleFromBehaviours(
225
225
  return sections.join("\n");
226
226
  }
227
227
 
228
- /**
229
- * Stage ID to display name and next stage mapping
230
- */
231
- const STAGE_INFO = {
232
- plan: { name: "Plan", nextStage: "Code" },
233
- code: { name: "Code", nextStage: "Review" },
234
- review: { name: "Review", nextStage: "Complete" },
235
- };
236
-
237
228
  /**
238
229
  * Generate SKILL.md content from skill data
239
230
  * @param {Object} skillData - Skill with agent section containing stages
231
+ * @param {Array} stages - All stage entities
240
232
  * @returns {Object} Skill with frontmatter, title, stages array, reference, dirname
241
233
  */
242
- export function generateSkillMd(skillData) {
234
+ export function generateSkillMd(skillData, stages) {
243
235
  const { agent, name } = skillData;
244
236
 
245
237
  if (!agent) {
@@ -250,17 +242,31 @@ export function generateSkillMd(skillData) {
250
242
  throw new Error(`Skill ${skillData.id} agent section missing stages`);
251
243
  }
252
244
 
245
+ // Build stage lookup map
246
+ const stageMap = new Map(stages.map((s) => [s.id, s]));
247
+
253
248
  // Transform stages object to array for template rendering
254
249
  const stagesArray = Object.entries(agent.stages).map(
255
250
  ([stageId, stageData]) => {
256
- const info = STAGE_INFO[stageId] || {
257
- name: stageId,
258
- nextStage: "Next",
259
- };
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
+
260
266
  return {
261
267
  stageId,
262
- stageName: info.name,
263
- nextStageName: info.nextStage,
268
+ stageName,
269
+ nextStageName,
264
270
  focus: stageData.focus,
265
271
  activities: stageData.activities || [],
266
272
  ready: stageData.ready || [],
@@ -277,11 +283,13 @@ export function generateSkillMd(skillData) {
277
283
  return {
278
284
  frontmatter: {
279
285
  name: agent.name,
280
- description: agent.description.trim(),
286
+ description: agent.description,
287
+ useWhen: agent.useWhen || "",
281
288
  },
282
289
  title: name,
283
290
  stages: stagesArray,
284
- reference: agent.reference ? agent.reference.trim() : "",
291
+ reference: skillData.implementationReference || "",
292
+ toolReferences: skillData.toolReferences || [],
285
293
  dirname: agent.name,
286
294
  };
287
295
  }
@@ -376,17 +376,17 @@ export function isValidJobCombination({
376
376
  * @returns {string} Generated job title
377
377
  */
378
378
  export function generateJobTitle(discipline, grade, track = null) {
379
- const { roleTitle, specialization, isManagement } = discipline;
379
+ const { roleTitle, isManagement } = discipline;
380
380
  const { professionalTitle, managementTitle } = grade;
381
381
 
382
382
  // Management discipline (no track needed)
383
383
  if (isManagement && !track) {
384
- return `${managementTitle}, ${specialization}`;
384
+ return `${managementTitle}, ${roleTitle}`;
385
385
  }
386
386
 
387
387
  // Management discipline with track
388
388
  if (isManagement && track) {
389
- return `${managementTitle}, ${track.name}`;
389
+ return `${managementTitle}, ${roleTitle} – ${track.name}`;
390
390
  }
391
391
 
392
392
  // IC discipline with track
@@ -90,6 +90,7 @@ export const Capability = {
90
90
  RELIABILITY: "reliability",
91
91
  DATA: "data",
92
92
  AI: "ai",
93
+ ML: "ml",
93
94
  PROCESS: "process",
94
95
  BUSINESS: "business",
95
96
  PEOPLE: "people",
@@ -111,6 +112,7 @@ export const CAPABILITY_ORDER = [
111
112
  Capability.DELIVERY,
112
113
  Capability.DATA,
113
114
  Capability.AI,
115
+ Capability.ML,
114
116
  Capability.SCALE,
115
117
  Capability.RELIABILITY,
116
118
  Capability.PEOPLE,
@@ -84,7 +84,15 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
84
84
 
85
85
  if (capability.skills && Array.isArray(capability.skills)) {
86
86
  for (const skill of capability.skills) {
87
- const { id, name, isHumanOnly, human, agent } = skill;
87
+ const {
88
+ id,
89
+ name,
90
+ isHumanOnly,
91
+ human,
92
+ agent,
93
+ implementationReference,
94
+ toolReferences,
95
+ } = skill;
88
96
  allSkills.push({
89
97
  id,
90
98
  name,
@@ -95,6 +103,9 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
95
103
  ...(isHumanOnly && { isHumanOnly }),
96
104
  // Preserve agent section for agent generation
97
105
  ...(agent && { agent }),
106
+ // Include implementation reference and tool references (shared by human and agent)
107
+ ...(implementationReference && { implementationReference }),
108
+ ...(toolReferences && { toolReferences }),
98
109
  });
99
110
  }
100
111
  }
@@ -241,17 +241,13 @@ function validateSkill(skill, index, requiredStageIds = []) {
241
241
  }
242
242
  }
243
243
 
244
- // reference is optional but should be a string if present
245
- if (
246
- skill.agent.reference !== undefined &&
247
- typeof skill.agent.reference !== "string"
248
- ) {
244
+ // Error if old 'reference' field is still present (moved to skill.implementationReference)
245
+ if (skill.agent.reference !== undefined) {
249
246
  errors.push(
250
247
  createError(
251
- "INVALID_VALUE",
252
- "Skill agent reference must be a string",
248
+ "INVALID_FIELD",
249
+ "Skill agent 'reference' field is not supported. Use skill.implementationReference instead.",
253
250
  `${agentPath}.reference`,
254
- skill.agent.reference,
255
251
  ),
256
252
  );
257
253
  }
@@ -295,6 +291,76 @@ function validateSkill(skill, index, requiredStageIds = []) {
295
291
  }
296
292
  }
297
293
 
294
+ // Validate implementationReference if present (optional string)
295
+ if (
296
+ skill.implementationReference !== undefined &&
297
+ typeof skill.implementationReference !== "string"
298
+ ) {
299
+ errors.push(
300
+ createError(
301
+ "INVALID_VALUE",
302
+ "Skill implementationReference must be a string",
303
+ `${path}.implementationReference`,
304
+ skill.implementationReference,
305
+ ),
306
+ );
307
+ }
308
+
309
+ // Validate toolReferences array if present
310
+ if (skill.toolReferences !== undefined) {
311
+ if (!Array.isArray(skill.toolReferences)) {
312
+ errors.push(
313
+ createError(
314
+ "INVALID_VALUE",
315
+ "Skill toolReferences must be an array",
316
+ `${path}.toolReferences`,
317
+ skill.toolReferences,
318
+ ),
319
+ );
320
+ } else {
321
+ skill.toolReferences.forEach((tool, i) => {
322
+ const toolPath = `${path}.toolReferences[${i}]`;
323
+ if (!tool.name) {
324
+ errors.push(
325
+ createError(
326
+ "MISSING_REQUIRED",
327
+ "Tool reference missing name",
328
+ `${toolPath}.name`,
329
+ ),
330
+ );
331
+ }
332
+ if (!tool.description) {
333
+ errors.push(
334
+ createError(
335
+ "MISSING_REQUIRED",
336
+ "Tool reference missing description",
337
+ `${toolPath}.description`,
338
+ ),
339
+ );
340
+ }
341
+ if (!tool.useWhen) {
342
+ errors.push(
343
+ createError(
344
+ "MISSING_REQUIRED",
345
+ "Tool reference missing useWhen",
346
+ `${toolPath}.useWhen`,
347
+ ),
348
+ );
349
+ }
350
+ if (tool.url !== undefined && typeof tool.url !== "string") {
351
+ errors.push(
352
+ createError(
353
+ "INVALID_VALUE",
354
+ "Tool reference url must be a string",
355
+ `${toolPath}.url`,
356
+ tool.url,
357
+ ),
358
+ );
359
+ }
360
+ });
361
+ }
362
+ }
363
+
298
364
  return { errors, warnings };
299
365
  }
300
366
 
@@ -26,7 +26,10 @@ import {
26
26
  deriveAgentSkills,
27
27
  deriveReferenceGrade,
28
28
  } from "../model/agent.js";
29
- import { createSelectWithValue } from "../lib/form-controls.js";
29
+ import {
30
+ createSelectWithValue,
31
+ createDisciplineSelect,
32
+ } from "../lib/form-controls.js";
30
33
  import { createReactive } from "../lib/reactive.js";
31
34
  import { getStageEmoji } from "../formatters/stage/shared.js";
32
35
  import { formatAgentProfile } from "../formatters/agent/profile.js";
@@ -242,9 +245,9 @@ export async function renderAgentBuilder() {
242
245
  { className: "form-group" },
243
246
  label({ className: "form-label" }, "Discipline"),
244
247
  availableDisciplines.length > 0
245
- ? createSelectWithValue({
248
+ ? createDisciplineSelect({
246
249
  id: "agent-discipline-select",
247
- items: availableDisciplines,
250
+ disciplines: availableDisciplines,
248
251
  initialValue: selection.get().discipline,
249
252
  placeholder: "Select a discipline...",
250
253
  onChange: (value) => {
@@ -404,7 +407,7 @@ function createAllStagesPreview(context) {
404
407
  const skillFiles = derivedSkills
405
408
  .map((derived) => skills.find((s) => s.id === derived.skillId))
406
409
  .filter((skill) => skill?.agent)
407
- .map((skill) => generateSkillMd(skill));
410
+ .map((skill) => generateSkillMd(skill, stages));
408
411
 
409
412
  return div(
410
413
  { className: "agent-deployment" },
@@ -519,7 +522,7 @@ function createSingleStagePreview(context, stage) {
519
522
  const skillFiles = derivedSkills
520
523
  .map((d) => skills.find((s) => s.id === d.skillId))
521
524
  .filter((skill) => skill?.agent)
522
- .map((skill) => generateSkillMd(skill));
525
+ .map((skill) => generateSkillMd(skill, stages));
523
526
 
524
527
  return div(
525
528
  { className: "agent-deployment" },