@forwardimpact/pathway 0.3.0 → 0.5.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 (90) hide show
  1. package/app/commands/agent.js +1 -1
  2. package/app/commands/behaviour.js +1 -1
  3. package/app/commands/command-factory.js +2 -2
  4. package/app/commands/discipline.js +1 -1
  5. package/app/commands/driver.js +1 -1
  6. package/app/commands/grade.js +1 -1
  7. package/app/commands/index.js +4 -3
  8. package/app/commands/serve.js +2 -2
  9. package/app/commands/site.js +22 -2
  10. package/app/commands/skill.js +57 -3
  11. package/app/commands/stage.js +1 -1
  12. package/app/commands/tool.js +112 -0
  13. package/app/commands/track.js +1 -1
  14. package/app/components/card.js +11 -1
  15. package/app/components/checklist.js +6 -4
  16. package/app/components/code-display.js +153 -0
  17. package/app/components/markdown-textarea.js +153 -0
  18. package/app/css/bundles/app.css +14 -0
  19. package/app/css/components/badges.css +15 -8
  20. package/app/css/components/forms.css +55 -0
  21. package/app/css/components/layout.css +12 -0
  22. package/app/css/components/surfaces.css +71 -3
  23. package/app/css/components/typography.css +1 -2
  24. package/app/css/pages/agent-builder.css +11 -102
  25. package/app/css/pages/detail.css +60 -0
  26. package/app/css/pages/job-builder.css +0 -42
  27. package/app/css/tokens.css +3 -0
  28. package/app/formatters/agent/dom.js +26 -71
  29. package/app/formatters/agent/profile.js +67 -10
  30. package/app/formatters/agent/skill.js +48 -6
  31. package/app/formatters/grade/dom.js +6 -6
  32. package/app/formatters/job/description.js +21 -16
  33. package/app/formatters/job/dom.js +9 -70
  34. package/app/formatters/json-ld.js +1 -1
  35. package/app/formatters/shared.js +58 -0
  36. package/app/formatters/skill/dom.js +70 -3
  37. package/app/formatters/skill/markdown.js +18 -0
  38. package/app/formatters/skill/shared.js +14 -4
  39. package/app/formatters/stage/microdata.js +2 -2
  40. package/app/formatters/stage/shared.js +3 -3
  41. package/app/formatters/tool/shared.js +78 -0
  42. package/app/handout-main.js +19 -18
  43. package/app/index.html +16 -3
  44. package/app/lib/card-mappers.js +91 -17
  45. package/app/lib/render.js +4 -0
  46. package/app/lib/yaml-loader.js +12 -1
  47. package/app/main.js +4 -0
  48. package/app/model/agent.js +47 -23
  49. package/app/model/checklist.js +2 -2
  50. package/app/model/derivation.js +5 -5
  51. package/app/model/levels.js +4 -2
  52. package/app/model/loader.js +12 -1
  53. package/app/model/validation.js +77 -11
  54. package/app/pages/agent-builder.js +121 -77
  55. package/app/pages/landing.js +35 -15
  56. package/app/pages/self-assessment.js +7 -5
  57. package/app/pages/skill.js +5 -17
  58. package/app/pages/stage.js +12 -8
  59. package/app/pages/tool.js +50 -0
  60. package/app/slide-main.js +1 -1
  61. package/app/slides/chapter.js +8 -8
  62. package/app/slides/index.js +26 -26
  63. package/app/slides/overview.js +8 -8
  64. package/app/slides/skill.js +1 -0
  65. package/bin/pathway.js +31 -16
  66. package/examples/capabilities/business.yaml +18 -18
  67. package/examples/capabilities/delivery.yaml +54 -37
  68. package/examples/capabilities/people.yaml +1 -1
  69. package/examples/capabilities/reliability.yaml +130 -115
  70. package/examples/capabilities/scale.yaml +39 -37
  71. package/examples/disciplines/engineering_management.yaml +1 -1
  72. package/examples/framework.yaml +21 -9
  73. package/examples/grades.yaml +5 -7
  74. package/examples/self-assessments.yaml +1 -1
  75. package/examples/stages.yaml +18 -10
  76. package/package.json +2 -1
  77. package/templates/agent.template.md +47 -17
  78. package/templates/job.template.md +8 -8
  79. package/templates/skill.template.md +33 -11
  80. package/examples/agents/.claude/skills/architecture-design/SKILL.md +0 -130
  81. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +0 -131
  82. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +0 -108
  83. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +0 -142
  84. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +0 -134
  85. package/examples/agents/.claude/skills/sre-practices/SKILL.md +0 -163
  86. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +0 -164
  87. package/examples/agents/.github/agents/se-platform-code.agent.md +0 -132
  88. package/examples/agents/.github/agents/se-platform-plan.agent.md +0 -131
  89. package/examples/agents/.github/agents/se-platform-review.agent.md +0 -136
  90. package/examples/agents/.vscode/settings.json +0 -8
@@ -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
  }
@@ -312,8 +320,11 @@ function estimateBodyDataLength(bodyData) {
312
320
  }
313
321
 
314
322
  // Array fields
315
- if (bodyData.capabilities) {
316
- length += bodyData.capabilities.join(", ").length;
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
+ }
317
328
  }
318
329
  if (bodyData.beforeMakingChanges) {
319
330
  for (const item of bodyData.beforeMakingChanges) {
@@ -483,6 +494,7 @@ function getChecklistStage(stageId) {
483
494
  * @param {Array} params.derivedSkills - Skills sorted by level
484
495
  * @param {Array} params.derivedBehaviours - Behaviours sorted by maturity
485
496
  * @param {Array} params.agentBehaviours - Agent behaviour definitions
497
+ * @param {Array} params.skills - All skill definitions (for agent section lookup)
486
498
  * @param {string} params.checklistMarkdown - Pre-formatted checklist markdown
487
499
  * @returns {Object} Structured profile body data
488
500
  */
@@ -495,6 +507,7 @@ function buildStageProfileBodyData({
495
507
  derivedSkills,
496
508
  derivedBehaviours,
497
509
  agentBehaviours,
510
+ skills,
498
511
  checklistMarkdown,
499
512
  }) {
500
513
  const name = `${humanDiscipline.specialization || humanDiscipline.name} - ${humanTrack.name}`;
@@ -524,8 +537,18 @@ function buildStageProfileBodyData({
524
537
  ? substituteTemplateVars(rawDelegation, humanDiscipline)
525
538
  : null;
526
539
 
527
- // Primary capabilities from derived skills
528
- const capabilities = derivedSkills.slice(0, 6).map((s) => s.skillName);
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);
529
552
 
530
553
  // Operational Context - use track's roleContext (shared with human job descriptions)
531
554
  const operationalContext = humanTrack.roleContext.trim();
@@ -549,7 +572,7 @@ function buildStageProfileBodyData({
549
572
  stageDescription: stage.description,
550
573
  identity: identity.trim(),
551
574
  priority: priority ? priority.trim() : null,
552
- capabilities,
575
+ skillIndex,
553
576
  beforeMakingChanges,
554
577
  delegation: delegation ? delegation.trim() : null,
555
578
  operationalContext,
@@ -711,6 +734,7 @@ export function generateStageAgentProfile({
711
734
  derivedSkills: agent.derivedSkills,
712
735
  derivedBehaviours: agent.derivedBehaviours,
713
736
  agentBehaviours,
737
+ skills,
714
738
  checklistMarkdown,
715
739
  });
716
740
 
@@ -72,7 +72,7 @@ export function deriveChecklist({
72
72
  capability: {
73
73
  id: capability.id,
74
74
  name: capability.name,
75
- emoji: capability.emoji,
75
+ emojiIcon: capability.emojiIcon,
76
76
  },
77
77
  items: stageData.ready,
78
78
  });
@@ -94,7 +94,7 @@ export function formatChecklistMarkdown(checklist) {
94
94
  }
95
95
 
96
96
  const sections = checklist.map(({ skill, capability, items }) => {
97
- const header = `**${capability.emoji} ${skill.name}**`;
97
+ const header = `**${capability.emojiIcon} ${skill.name}**`;
98
98
  const itemList = items.map((item) => `- [ ] ${item}`).join("\n");
99
99
  return `${header}\n\n${itemList}`;
100
100
  });
@@ -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
@@ -437,7 +437,7 @@ function generateJobId(discipline, grade, track = null) {
437
437
  * @param {import('./levels.js').SkillMatrixEntry[]} params.skillMatrix - Derived skill matrix for the job
438
438
  * @param {Object[]} params.capabilities - Capability definitions with responsibilities
439
439
  * @param {import('./levels.js').Discipline} params.discipline - The discipline (determines which responsibilities to use)
440
- * @returns {Array<{capability: string, capabilityName: string, emoji: string, responsibility: string, level: string}>}
440
+ * @returns {Array<{capability: string, capabilityName: string, emojiIcon: string, responsibility: string, level: string}>}
441
441
  */
442
442
  export function deriveResponsibilities({
443
443
  skillMatrix,
@@ -484,7 +484,7 @@ export function deriveResponsibilities({
484
484
  responsibilities.push({
485
485
  capability: capabilityId,
486
486
  capabilityName: capability.name,
487
- emoji: capability.emoji || "💡",
487
+ emojiIcon: capability.emojiIcon || "💡",
488
488
  displayOrder: capability.displayOrder ?? 999,
489
489
  responsibility: responsibilityText,
490
490
  level,
@@ -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,
@@ -218,7 +220,7 @@ export function getCapabilityOrder(capabilities) {
218
220
  */
219
221
  export function getCapabilityEmoji(capabilities, capabilityId) {
220
222
  const capability = getCapabilityById(capabilities, capabilityId);
221
- return capability?.emoji || "💡";
223
+ return capability?.emojiIcon || "💡";
222
224
  }
223
225
 
224
226
  /**
@@ -595,5 +597,5 @@ export function behaviourMaturityMeetsRequirement(actual, required) {
595
597
  * @returns {string} The emoji for the concept or default "💡"
596
598
  */
597
599
  export function getConceptEmoji(framework, concept) {
598
- return framework?.entityDefinitions?.[concept]?.emoji || "💡";
600
+ return framework?.entityDefinitions?.[concept]?.emojiIcon || "💡";
599
601
  }
@@ -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
 
@@ -1173,12 +1239,12 @@ function validateCapability(capability, index) {
1173
1239
  ),
1174
1240
  );
1175
1241
  }
1176
- if (!capability.emoji) {
1242
+ if (!capability.emojiIcon) {
1177
1243
  warnings.push(
1178
1244
  createWarning(
1179
1245
  "MISSING_OPTIONAL",
1180
- "Capability missing emoji",
1181
- `${path}.emoji`,
1246
+ "Capability missing emojiIcon",
1247
+ `${path}.emojiIcon`,
1182
1248
  ),
1183
1249
  );
1184
1250
  }
@@ -16,6 +16,8 @@ import {
16
16
  span,
17
17
  label,
18
18
  section,
19
+ select,
20
+ option,
19
21
  } from "../lib/render.js";
20
22
  import { getState } from "../lib/state.js";
21
23
  import { loadAgentDataBrowser } from "../lib/yaml-loader.js";
@@ -34,6 +36,7 @@ import { createReactive } from "../lib/reactive.js";
34
36
  import { getStageEmoji } from "../formatters/stage/shared.js";
35
37
  import { formatAgentProfile } from "../formatters/agent/profile.js";
36
38
  import { formatAgentSkill } from "../formatters/agent/skill.js";
39
+ import { createCodeDisplay } from "../components/code-display.js";
37
40
 
38
41
  /** All stages option value */
39
42
  const ALL_STAGES_VALUE = "all";
@@ -101,9 +104,71 @@ export async function renderAgentBuilder() {
101
104
  const availableDisciplines = data.disciplines.filter((d) =>
102
105
  agentDisciplineIds.has(d.id),
103
106
  );
104
- const availableTracks = data.tracks.filter((t) => agentTrackIds.has(t.id));
107
+ // All tracks with agent definitions (will be filtered per-discipline)
108
+ const allAgentTracks = data.tracks.filter((t) => agentTrackIds.has(t.id));
105
109
  const stages = data.stages || [];
106
110
 
111
+ /**
112
+ * Get tracks valid for a discipline that also have agent definitions
113
+ * @param {string} disciplineId - Discipline ID
114
+ * @returns {Array} - Valid tracks for the discipline
115
+ */
116
+ function getValidTracksForDiscipline(disciplineId) {
117
+ const discipline = data.disciplines.find((d) => d.id === disciplineId);
118
+ if (!discipline) return [];
119
+
120
+ const validTracks = discipline.validTracks ?? [];
121
+ // Filter to track IDs only (exclude null which means trackless)
122
+ const validTrackIds = validTracks.filter((t) => t !== null);
123
+
124
+ // Intersection: valid for discipline AND has agent definition
125
+ return allAgentTracks.filter((t) => validTrackIds.includes(t.id));
126
+ }
127
+
128
+ // Track select element - created once, options updated when discipline changes
129
+ const trackSelectEl = select(
130
+ { className: "form-select", id: "agent-track-select" },
131
+ option({ value: "", disabled: true, selected: true }, "Select a track..."),
132
+ );
133
+ trackSelectEl.disabled = true;
134
+
135
+ /**
136
+ * Update track select options based on selected discipline
137
+ * @param {string} disciplineId - Discipline ID
138
+ */
139
+ function updateTrackOptions(disciplineId) {
140
+ const validTracks = getValidTracksForDiscipline(disciplineId);
141
+
142
+ // Clear existing options
143
+ trackSelectEl.innerHTML = "";
144
+
145
+ if (validTracks.length === 0) {
146
+ trackSelectEl.appendChild(
147
+ option(
148
+ { value: "", disabled: true, selected: true },
149
+ "No tracks available for this discipline",
150
+ ),
151
+ );
152
+ trackSelectEl.disabled = true;
153
+ return;
154
+ }
155
+
156
+ // Add placeholder
157
+ trackSelectEl.appendChild(
158
+ option(
159
+ { value: "", disabled: true, selected: true },
160
+ "Select a track...",
161
+ ),
162
+ );
163
+
164
+ // Add available track options
165
+ validTracks.forEach((t) => {
166
+ trackSelectEl.appendChild(option({ value: t.id }, t.name));
167
+ });
168
+
169
+ trackSelectEl.disabled = false;
170
+ }
171
+
107
172
  // Build stage options with "All Stages" first
108
173
  const stageOptions = [
109
174
  { id: ALL_STAGES_VALUE, name: "All Stages" },
@@ -134,7 +199,7 @@ export async function renderAgentBuilder() {
134
199
  // Preview container - will be updated reactively
135
200
  const previewContainer = div(
136
201
  { className: "agent-preview" },
137
- createEmptyState(availableDisciplines.length, availableTracks.length),
202
+ createEmptyState(availableDisciplines.length, allAgentTracks.length),
138
203
  );
139
204
 
140
205
  /**
@@ -156,7 +221,7 @@ export async function renderAgentBuilder() {
156
221
 
157
222
  if (!discipline) {
158
223
  previewContainer.appendChild(
159
- createEmptyState(availableDisciplines.length, availableTracks.length),
224
+ createEmptyState(availableDisciplines.length, allAgentTracks.length),
160
225
  );
161
226
  return;
162
227
  }
@@ -251,7 +316,14 @@ export async function renderAgentBuilder() {
251
316
  initialValue: selection.get().discipline,
252
317
  placeholder: "Select a discipline...",
253
318
  onChange: (value) => {
254
- selection.update((prev) => ({ ...prev, discipline: value }));
319
+ // Update track options when discipline changes
320
+ updateTrackOptions(value);
321
+ // Reset track selection when discipline changes
322
+ selection.update((prev) => ({
323
+ ...prev,
324
+ discipline: value,
325
+ track: "",
326
+ }));
255
327
  },
256
328
  getDisplayName: (d) => d.specialization || d.name,
257
329
  })
@@ -260,25 +332,32 @@ export async function renderAgentBuilder() {
260
332
  "No disciplines have agent definitions.",
261
333
  ),
262
334
  ),
263
- // Track selector
335
+ // Track selector (dynamically filtered by discipline)
264
336
  div(
265
337
  { className: "form-group" },
266
338
  label({ className: "form-label" }, "Track"),
267
- availableTracks.length > 0
268
- ? createSelectWithValue({
269
- id: "agent-track-select",
270
- items: availableTracks,
271
- initialValue: selection.get().track,
272
- placeholder: "Select a track...",
273
- onChange: (value) => {
274
- selection.update((prev) => ({ ...prev, track: value }));
275
- },
276
- getDisplayName: (t) => t.name,
277
- })
278
- : p(
279
- { className: "text-muted" },
280
- "No tracks have agent definitions.",
281
- ),
339
+ (() => {
340
+ // Wire up track select change handler
341
+ trackSelectEl.addEventListener("change", (e) => {
342
+ selection.update((prev) => ({ ...prev, track: e.target.value }));
343
+ });
344
+ // Initialize track options if discipline is pre-selected
345
+ const initialDiscipline = selection.get().discipline;
346
+ if (initialDiscipline) {
347
+ updateTrackOptions(initialDiscipline);
348
+ // Set initial track value if provided and valid
349
+ const initialTrack = selection.get().track;
350
+ const validTracks =
351
+ getValidTracksForDiscipline(initialDiscipline);
352
+ if (
353
+ initialTrack &&
354
+ validTracks.some((t) => t.id === initialTrack)
355
+ ) {
356
+ trackSelectEl.value = initialTrack;
357
+ }
358
+ }
359
+ return trackSelectEl;
360
+ })(),
282
361
  ),
283
362
  // Stage selector (dropdown with All Stages option)
284
363
  div(
@@ -407,7 +486,7 @@ function createAllStagesPreview(context) {
407
486
  const skillFiles = derivedSkills
408
487
  .map((derived) => skills.find((s) => s.id === derived.skillId))
409
488
  .filter((skill) => skill?.agent)
410
- .map((skill) => generateSkillMd(skill));
489
+ .map((skill) => generateSkillMd(skill, stages));
411
490
 
412
491
  return div(
413
492
  { className: "agent-deployment" },
@@ -522,7 +601,7 @@ function createSingleStagePreview(context, stage) {
522
601
  const skillFiles = derivedSkills
523
602
  .map((d) => skills.find((s) => s.id === d.skillId))
524
603
  .filter((skill) => skill?.agent)
525
- .map((skill) => generateSkillMd(skill));
604
+ .map((skill) => generateSkillMd(skill, stages));
526
605
 
527
606
  return div(
528
607
  { className: "agent-deployment" },
@@ -590,10 +669,15 @@ function createAgentCard(stage, profile, stages, agentTemplate, _derived) {
590
669
  span({ className: "agent-card-emoji" }, stageEmoji),
591
670
  h3({}, `${stage.name} Agent`),
592
671
  ),
593
- createCopyButton(content),
594
672
  ),
595
- p({ className: "agent-card-filename" }, profile.filename),
596
- div({ className: "agent-card-preview" }, createCodePreview(content)),
673
+ div(
674
+ { className: "agent-card-preview" },
675
+ createCodeDisplay({
676
+ content,
677
+ filename: profile.filename,
678
+ maxHeight: 400,
679
+ }),
680
+ ),
597
681
  );
598
682
 
599
683
  return card;
@@ -614,57 +698,18 @@ function createSkillCard(skill, skillTemplate) {
614
698
  div(
615
699
  { className: "skill-card-header" },
616
700
  span({ className: "skill-card-name" }, skill.frontmatter.name),
617
- createCopyButton(content),
618
701
  ),
619
- p({ className: "skill-card-filename" }, filename),
620
- div({ className: "skill-card-preview" }, createCodePreview(content)),
702
+ div(
703
+ { className: "skill-card-preview" },
704
+ createCodeDisplay({
705
+ content,
706
+ filename,
707
+ maxHeight: 300,
708
+ }),
709
+ ),
621
710
  );
622
711
  }
623
712
 
624
- /**
625
- * Create a code preview element
626
- * @param {string} content - Code content
627
- * @returns {HTMLElement}
628
- */
629
- function createCodePreview(content) {
630
- const pre = document.createElement("pre");
631
- pre.className = "code-block code-preview";
632
-
633
- const code = document.createElement("code");
634
- code.textContent = content;
635
-
636
- pre.appendChild(code);
637
- return pre;
638
- }
639
-
640
- /**
641
- * Create a copy button
642
- * @param {string} content - Content to copy
643
- * @returns {HTMLElement}
644
- */
645
- function createCopyButton(content) {
646
- const btn = document.createElement("button");
647
- btn.className = "btn btn-sm copy-btn";
648
- btn.textContent = "📋 Copy";
649
-
650
- btn.addEventListener("click", async () => {
651
- try {
652
- await navigator.clipboard.writeText(content);
653
- btn.textContent = "✓ Copied";
654
- setTimeout(() => {
655
- btn.textContent = "📋 Copy";
656
- }, 2000);
657
- } catch {
658
- btn.textContent = "Failed";
659
- setTimeout(() => {
660
- btn.textContent = "📋 Copy";
661
- }, 2000);
662
- }
663
- });
664
-
665
- return btn;
666
- }
667
-
668
713
  /**
669
714
  * Create download all button for all stages
670
715
  * @param {Array} stageAgents - Array of {stage, derived, profile}
@@ -860,11 +905,10 @@ function createCliHint(disciplineId, trackId, stageId) {
860
905
  { className: "agent-section cli-hint" },
861
906
  h2({}, "CLI Alternative"),
862
907
  p({}, "Generate this agent from the command line:"),
863
- div(
864
- { className: "cli-command" },
865
- createCodePreview(command),
866
- createCopyButton(command),
867
- ),
908
+ createCodeDisplay({
909
+ content: command,
910
+ language: "bash",
911
+ }),
868
912
  );
869
913
 
870
914
  return container;