@forwardimpact/pathway 0.16.0 → 0.17.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.
@@ -22,13 +22,6 @@ import {
22
22
  * @property {string} content - Working style content (markdown)
23
23
  */
24
24
 
25
- /**
26
- * @typedef {Object} ChecklistEntry
27
- * @property {{id: string, name: string}} skill - Skill info
28
- * @property {{id: string, name: string, emojiIcon: string}} capability - Capability info
29
- * @property {string[]} items - Checklist items
30
- */
31
-
32
25
  /**
33
26
  * Prepare agent profile data for template rendering
34
27
  * Normalizes string values by trimming trailing newlines for consistent template output.
@@ -49,8 +42,6 @@ import {
49
42
  * @param {Array<{name: string, dirname: string, useWhen: string}>} params.bodyData.skillIndex - Skill index entries
50
43
  * @param {string} params.bodyData.roleContext - Role context text
51
44
  * @param {WorkingStyleEntry[]} params.bodyData.workingStyles - Working style entries
52
- * @param {ChecklistEntry[]} [params.bodyData.readChecklist] - Read-Then-Do Checklist entries
53
- * @param {ChecklistEntry[]} [params.bodyData.confirmChecklist] - Do-Then-Confirm Checklist entries
54
45
  * @param {string[]} params.bodyData.constraints - List of constraints
55
46
  * @param {Array<{id: string, name: string, description: string}>} [params.bodyData.agentIndex] - List of all available agents
56
47
  * @param {boolean} [params.bodyData.hasAgentIndex] - Whether agent index is available
@@ -81,20 +72,6 @@ function prepareAgentProfileData({ frontmatter, bodyData }) {
81
72
  content: "required",
82
73
  });
83
74
 
84
- // Process readChecklist: trim items in each entry
85
- const readChecklist = (bodyData.readChecklist || []).map((entry) => ({
86
- skill: entry.skill,
87
- capability: entry.capability,
88
- items: (entry.items || []).map((item) => trimRequired(item)),
89
- }));
90
-
91
- // Process confirmChecklist: trim items in each entry
92
- const confirmChecklist = (bodyData.confirmChecklist || []).map((entry) => ({
93
- skill: entry.skill,
94
- capability: entry.capability,
95
- items: (entry.items || []).map((item) => trimRequired(item)),
96
- }));
97
-
98
75
  return {
99
76
  // Frontmatter - flatten description for single-line front matter
100
77
  name: frontmatter.name,
@@ -115,10 +92,6 @@ function prepareAgentProfileData({ frontmatter, bodyData }) {
115
92
  roleContext: trimValue(bodyData.roleContext),
116
93
  workingStyles,
117
94
  hasWorkingStyles: workingStyles.length > 0,
118
- readChecklist,
119
- hasReadChecklist: readChecklist.length > 0,
120
- confirmChecklist,
121
- hasConfirmChecklist: confirmChecklist.length > 0,
122
95
  constraints,
123
96
  hasConstraints: constraints.length > 0,
124
97
  agentIndex,
@@ -143,8 +116,6 @@ function prepareAgentProfileData({ frontmatter, bodyData }) {
143
116
  * @param {Array<{name: string, dirname: string, useWhen: string}>} profile.bodyData.skillIndex - Skill index entries
144
117
  * @param {string} profile.bodyData.roleContext - Role context text
145
118
  * @param {WorkingStyleEntry[]} profile.bodyData.workingStyles - Working style entries
146
- * @param {ChecklistEntry[]} [profile.bodyData.readChecklist] - Read-Then-Do Checklist entries (optional)
147
- * @param {ChecklistEntry[]} [profile.bodyData.confirmChecklist] - Do-Then-Confirm Checklist entries (optional)
148
119
  * @param {string[]} profile.bodyData.constraints - List of constraints
149
120
  * @param {string} template - Mustache template string
150
121
  * @returns {string} Complete .agent.md file content
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Agent Skill Formatter
3
3
  *
4
- * Formats agent skill data into SKILL.md file content
5
- * following the Agent Skills Standard specification.
4
+ * Formats agent skill data into SKILL.md, scripts/install.sh, and
5
+ * references/REFERENCE.md file content following the Agent Skills Standard
6
+ * specification with progressive disclosure.
6
7
  *
7
8
  * Uses Mustache templates for flexible output formatting.
8
9
  * Templates are loaded from data/ directory with fallback to templates/ directory.
@@ -13,6 +14,13 @@ import Mustache from "mustache";
13
14
  import { trimValue, splitLines, trimFields } from "../shared.js";
14
15
  import { flattenToLine } from "../template-preprocess.js";
15
16
 
17
+ /**
18
+ * Lowercase the first character of a string
19
+ * @param {string} s
20
+ * @returns {string}
21
+ */
22
+ const lcFirst = (s) => (s ? s[0].toLowerCase() + s.slice(1) : s);
23
+
16
24
  /**
17
25
  * Prepare agent skill data for template rendering
18
26
  * Normalizes string values by trimming trailing newlines for consistent template output.
@@ -23,7 +31,9 @@ import { flattenToLine } from "../template-preprocess.js";
23
31
  * @param {string} [params.frontmatter.useWhen] - When to use this skill
24
32
  * @param {string} params.title - Human-readable skill title for heading
25
33
  * @param {Array} params.stages - Array of stage objects with stageName, focus, readChecklist, confirmChecklist
26
- * @param {string} params.reference - Reference content (markdown)
34
+ * @param {string} params.instructions - Workflow guidance content (markdown)
35
+ * @param {string} params.installScript - Shell commands for install script
36
+ * @param {string} params.implementationReference - Reference content (markdown)
27
37
  * @param {Array} [params.toolReferences] - Array of tool reference objects
28
38
  * @returns {Object} Data object ready for Mustache template
29
39
  */
@@ -31,7 +41,9 @@ function prepareAgentSkillData({
31
41
  frontmatter,
32
42
  title,
33
43
  stages,
34
- reference,
44
+ instructions,
45
+ installScript,
46
+ implementationReference,
35
47
  toolReferences,
36
48
  }) {
37
49
  // Process stages - trim focus and array values
@@ -43,12 +55,14 @@ function prepareAgentSkillData({
43
55
 
44
56
  // Flatten multi-line strings to single line for front matter compatibility
45
57
  const description = flattenToLine(frontmatter.description);
46
- const useWhen = flattenToLine(frontmatter.useWhen);
58
+ const useWhen = lcFirst(flattenToLine(frontmatter.useWhen));
47
59
 
48
60
  // Keep line arrays for body rendering
49
61
  const descriptionLines = splitLines(frontmatter.description);
50
62
 
51
- const trimmedReference = trimValue(reference) || "";
63
+ const trimmedInstructions = trimValue(instructions) || "";
64
+ const trimmedInstallScript = trimValue(installScript) || "";
65
+ const trimmedReference = trimValue(implementationReference) || "";
52
66
  const tools = toolReferences || [];
53
67
 
54
68
  return {
@@ -63,7 +77,11 @@ function prepareAgentSkillData({
63
77
  title,
64
78
  stages: processedStages,
65
79
  hasStages: processedStages.length > 0,
66
- reference: trimmedReference,
80
+ instructions: trimmedInstructions,
81
+ hasInstructions: !!trimmedInstructions,
82
+ installScript: trimmedInstallScript,
83
+ hasInstallScript: !!trimmedInstallScript,
84
+ implementationReference: trimmedReference,
67
85
  hasReference: !!trimmedReference,
68
86
  toolReferences: tools,
69
87
  hasToolReferences: tools.length > 0,
@@ -72,27 +90,67 @@ function prepareAgentSkillData({
72
90
 
73
91
  /**
74
92
  * Format agent skill as SKILL.md file content using Mustache template
75
- * @param {Object} skill - Skill with frontmatter, title, stages, reference
93
+ * @param {Object} skill - Skill with frontmatter, title, stages, instructions, installScript, implementationReference
76
94
  * @param {Object} skill.frontmatter - YAML frontmatter data
77
95
  * @param {string} skill.frontmatter.name - Skill name (required)
78
96
  * @param {string} skill.frontmatter.description - Skill description (required)
79
97
  * @param {string} skill.title - Human-readable skill title for heading
80
98
  * @param {Array} skill.stages - Array of stage objects with stageName, focus, readChecklist, confirmChecklist
81
- * @param {string} skill.reference - Reference content (markdown)
99
+ * @param {string} skill.instructions - Workflow guidance (markdown)
100
+ * @param {string} skill.installScript - Shell commands for install script
101
+ * @param {string} skill.implementationReference - Reference content (markdown)
82
102
  * @param {Array} [skill.toolReferences] - Array of tool reference objects
83
103
  * @param {string} template - Mustache template string
84
104
  * @returns {string} Complete SKILL.md file content
85
105
  */
86
106
  export function formatAgentSkill(
87
- { frontmatter, title, stages, reference, toolReferences },
107
+ {
108
+ frontmatter,
109
+ title,
110
+ stages,
111
+ instructions,
112
+ installScript,
113
+ implementationReference,
114
+ toolReferences,
115
+ },
88
116
  template,
89
117
  ) {
90
118
  const data = prepareAgentSkillData({
91
119
  frontmatter,
92
120
  title,
93
121
  stages,
94
- reference,
122
+ instructions,
123
+ installScript,
124
+ implementationReference,
95
125
  toolReferences,
96
126
  });
97
127
  return Mustache.render(template, data);
98
128
  }
129
+
130
+ /**
131
+ * Format install script file content using Mustache template
132
+ * @param {Object} skill - Skill data with installScript and frontmatter
133
+ * @param {string} template - Mustache template string for install script
134
+ * @returns {string} Complete install.sh file content
135
+ */
136
+ export function formatInstallScript(skill, template) {
137
+ const data = {
138
+ name: skill.frontmatter.name,
139
+ installScript: trimValue(skill.installScript) || "",
140
+ };
141
+ return Mustache.render(template, data);
142
+ }
143
+
144
+ /**
145
+ * Format reference file content using Mustache template
146
+ * @param {Object} skill - Skill data with implementationReference and title
147
+ * @param {string} template - Mustache template string for reference
148
+ * @returns {string} Complete REFERENCE.md file content
149
+ */
150
+ export function formatReference(skill, template) {
151
+ const data = {
152
+ title: skill.title,
153
+ implementationReference: trimValue(skill.implementationReference) || "",
154
+ };
155
+ return Mustache.render(template, data);
156
+ }
@@ -233,16 +233,18 @@ export function createJobDescriptionSection({
233
233
  template,
234
234
  );
235
235
 
236
- return createDetailSection({
237
- title: "Job Description",
238
- content: createCodeDisplay({
236
+ return section(
237
+ { className: "section section-detail" },
238
+ h2({ className: "section-title" }, "Job Description"),
239
+ createCodeDisplay({
239
240
  content: markdown,
241
+ filename: "job-description.md",
240
242
  description:
241
243
  "Copy this markdown-formatted job description for use in job postings, documentation, or sharing.",
242
244
  toHtml: markdownToHtml,
243
245
  minHeight: 450,
244
246
  }),
245
- });
247
+ );
246
248
  }
247
249
 
248
250
  /**
@@ -18,7 +18,7 @@ import {
18
18
  } from "../../lib/render.js";
19
19
  import { createBackLink } from "../../components/nav.js";
20
20
  import { createLevelCell } from "../../components/detail.js";
21
- import { createCodeDisplay } from "../../components/code-display.js";
21
+ import { createSkillFileViewer } from "../../components/skill-file-viewer.js";
22
22
  import { createToolIcon } from "../../lib/card-mappers.js";
23
23
  import { SKILL_LEVEL_ORDER } from "@forwardimpact/schema/levels";
24
24
  import { prepareSkillDetail } from "./shared.js";
@@ -34,6 +34,7 @@ import { createJsonLdScript, skillToJsonLd } from "../json-ld.js";
34
34
  * @param {Array} context.capabilities - Capability entities
35
35
  * @param {boolean} [context.showBackLink=true] - Whether to show back navigation link
36
36
  * @param {boolean} [context.showToolsAndPatterns=true] - Whether to show required tools and implementation patterns
37
+ * @param {string} [context.agentSkillContent] - Pre-generated SKILL.md content for agent file viewer
37
38
  * @returns {HTMLElement}
38
39
  */
39
40
  export function skillToDOM(
@@ -45,6 +46,7 @@ export function skillToDOM(
45
46
  capabilities,
46
47
  showBackLink = true,
47
48
  showToolsAndPatterns = true,
49
+ agentSkillContent,
48
50
  } = {},
49
51
  ) {
50
52
  const view = prepareSkillDetail(skill, {
@@ -222,18 +224,54 @@ export function skillToDOM(
222
224
  )
223
225
  : null,
224
226
 
225
- // Implementation Reference
226
- showToolsAndPatterns && view.implementationReference
227
+ // Agent Skill Files
228
+ showToolsAndPatterns &&
229
+ (agentSkillContent || view.implementationReference || view.installScript)
227
230
  ? div(
228
231
  { className: "detail-section" },
229
- heading2({ className: "section-title" }, "Implementation Patterns"),
230
- createCodeDisplay({
231
- content: view.implementationReference,
232
- description:
233
- "Project-specific implementation guidance for this skill.",
234
- minHeight: 450,
232
+ heading2({ className: "section-title" }, "Agent Skill Files"),
233
+ createSkillFileViewer({
234
+ files: buildSkillFiles(view, agentSkillContent),
235
+ maxHeight: 450,
235
236
  }),
236
237
  )
237
238
  : null,
238
239
  );
239
240
  }
241
+
242
+ /**
243
+ * Build file descriptors for the skill file viewer
244
+ * @param {import('./shared.js').SkillDetailView} view
245
+ * @param {string} [agentSkillContent] - Pre-generated SKILL.md content
246
+ * @returns {import('../../components/skill-file-viewer.js').SkillFile[]}
247
+ */
248
+ function buildSkillFiles(view, agentSkillContent) {
249
+ /** @type {import('../../components/skill-file-viewer.js').SkillFile[]} */
250
+ const files = [];
251
+
252
+ if (agentSkillContent) {
253
+ files.push({
254
+ filename: "SKILL.md",
255
+ content: agentSkillContent,
256
+ language: "markdown",
257
+ });
258
+ }
259
+
260
+ if (view.installScript) {
261
+ files.push({
262
+ filename: "scripts/install.sh",
263
+ content: view.installScript,
264
+ language: "bash",
265
+ });
266
+ }
267
+
268
+ if (view.implementationReference) {
269
+ files.push({
270
+ filename: "references/REFERENCE.md",
271
+ content: view.implementationReference,
272
+ language: "markdown",
273
+ });
274
+ }
275
+
276
+ return files;
277
+ }
@@ -74,6 +74,7 @@ export function prepareSkillsList(
74
74
  * @property {Array<{id: string, name: string, modifier: number}>} relatedTracks
75
75
  * @property {Array<{id: string, name: string}>} relatedDrivers
76
76
  * @property {Array<{name: string, url?: string, description: string, useWhen: string}>} toolReferences
77
+ * @property {string|null} installScript
77
78
  * @property {string|null} implementationReference
78
79
  */
79
80
 
@@ -130,6 +131,7 @@ export function prepareSkillDetail(
130
131
  toolReferences: (skill.toolReferences || [])
131
132
  .slice()
132
133
  .sort((a, b) => a.name.localeCompare(b.name)),
134
+ installScript: skill.installScript || null,
133
135
  implementationReference: skill.implementationReference || null,
134
136
  };
135
137
  }
@@ -65,6 +65,24 @@ export async function loadSkillTemplate(dataDir) {
65
65
  return loadTemplate("skill.template.md", dataDir);
66
66
  }
67
67
 
68
+ /**
69
+ * Load skill install script template
70
+ * @param {string} dataDir - Path to data directory
71
+ * @returns {Promise<string>} Install script template content
72
+ */
73
+ export async function loadSkillInstallTemplate(dataDir) {
74
+ return loadTemplate("skill-install.template.sh", dataDir);
75
+ }
76
+
77
+ /**
78
+ * Load skill reference template
79
+ * @param {string} dataDir - Path to data directory
80
+ * @returns {Promise<string>} Reference template content
81
+ */
82
+ export async function loadSkillReferenceTemplate(dataDir) {
83
+ return loadTemplate("skill-reference.template.md", dataDir);
84
+ }
85
+
68
86
  /**
69
87
  * Load job description template
70
88
  * @param {string} dataDir - Path to data directory
@@ -76,6 +76,8 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
76
76
  isHumanOnly,
77
77
  human,
78
78
  agent,
79
+ instructions,
80
+ installScript,
79
81
  implementationReference,
80
82
  toolReferences,
81
83
  } = skill;
@@ -88,6 +90,9 @@ async function loadSkillsFromCapabilities(capabilitiesDir) {
88
90
  // Include isHumanOnly flag for agent filtering (defaults to false)
89
91
  ...(isHumanOnly && { isHumanOnly }),
90
92
  ...(agent && { agent }),
93
+ // Include agent skill content fields
94
+ ...(instructions && { instructions }),
95
+ ...(installScript && { installScript }),
91
96
  // Include implementation reference and tool references (shared by human and agent)
92
97
  ...(implementationReference && { implementationReference }),
93
98
  ...(toolReferences && { toolReferences }),
@@ -37,8 +37,13 @@ import {
37
37
  import { createReactive } from "../lib/reactive.js";
38
38
  import { getStageEmoji } from "../formatters/stage/shared.js";
39
39
  import { formatAgentProfile } from "../formatters/agent/profile.js";
40
- import { formatAgentSkill } from "../formatters/agent/skill.js";
40
+ import {
41
+ formatAgentSkill,
42
+ formatInstallScript,
43
+ formatReference,
44
+ } from "../formatters/agent/skill.js";
41
45
  import { createCodeDisplay } from "../components/code-display.js";
46
+ import { createSkillFileViewer } from "../components/skill-file-viewer.js";
42
47
  import { createToolkitTable } from "../formatters/toolkit/dom.js";
43
48
  import { createDetailSection } from "../components/detail.js";
44
49
 
@@ -65,17 +70,21 @@ async function getAgentData(dataDir = "./data") {
65
70
 
66
71
  /**
67
72
  * Load templates with caching
68
- * @returns {Promise<{agent: string, skill: string}>}
73
+ * @returns {Promise<{agent: string, skill: string, install: string, reference: string}>}
69
74
  */
70
75
  async function getTemplates() {
71
76
  if (!templateCache) {
72
- const [agentRes, skillRes] = await Promise.all([
77
+ const [agentRes, skillRes, installRes, referenceRes] = await Promise.all([
73
78
  fetch("./templates/agent.template.md"),
74
79
  fetch("./templates/skill.template.md"),
80
+ fetch("./templates/skill-install.template.sh"),
81
+ fetch("./templates/skill-reference.template.md"),
75
82
  ]);
76
83
  templateCache = {
77
84
  agent: await agentRes.text(),
78
85
  skill: await skillRes.text(),
86
+ install: await installRes.text(),
87
+ reference: await referenceRes.text(),
79
88
  };
80
89
  }
81
90
  return templateCache;
@@ -271,7 +280,6 @@ export async function renderAgentBuilder() {
271
280
  skills: data.skills,
272
281
  behaviours: data.behaviours,
273
282
  agentBehaviours: agentData.behaviours,
274
- capabilities: data.capabilities,
275
283
  vscodeSettings: agentData.vscodeSettings,
276
284
  devcontainer: agentData.devcontainer,
277
285
  templates,
@@ -449,7 +457,6 @@ function createAllStagesPreview(context) {
449
457
  skills,
450
458
  behaviours,
451
459
  agentBehaviours,
452
- capabilities,
453
460
  vscodeSettings,
454
461
  devcontainer,
455
462
  templates,
@@ -468,7 +475,6 @@ function createAllStagesPreview(context) {
468
475
  agentBehaviours,
469
476
  agentDiscipline,
470
477
  agentTrack,
471
- capabilities,
472
478
  stages,
473
479
  });
474
480
 
@@ -482,7 +488,6 @@ function createAllStagesPreview(context) {
482
488
  agentBehaviours,
483
489
  agentDiscipline,
484
490
  agentTrack,
485
- capabilities,
486
491
  stages,
487
492
  agentIndex,
488
493
  });
@@ -545,9 +550,7 @@ function createAllStagesPreview(context) {
545
550
  skillFiles.length > 0
546
551
  ? div(
547
552
  { className: "skill-cards-grid" },
548
- ...skillFiles.map((skill) =>
549
- createSkillCard(skill, templates.skill),
550
- ),
553
+ ...skillFiles.map((skill) => createSkillCard(skill, templates)),
551
554
  )
552
555
  : p(
553
556
  { className: "text-muted" },
@@ -581,7 +584,6 @@ function createSingleStagePreview(context, stage) {
581
584
  skills,
582
585
  behaviours,
583
586
  agentBehaviours,
584
- capabilities,
585
587
  vscodeSettings,
586
588
  devcontainer,
587
589
  stages,
@@ -600,7 +602,6 @@ function createSingleStagePreview(context, stage) {
600
602
  agentBehaviours,
601
603
  agentDiscipline,
602
604
  agentTrack,
603
- capabilities,
604
605
  stages,
605
606
  });
606
607
 
@@ -614,7 +615,6 @@ function createSingleStagePreview(context, stage) {
614
615
  agentBehaviours,
615
616
  agentDiscipline,
616
617
  agentTrack,
617
- capabilities,
618
618
  stages,
619
619
  agentIndex,
620
620
  });
@@ -667,9 +667,7 @@ function createSingleStagePreview(context, stage) {
667
667
  skillFiles.length > 0
668
668
  ? div(
669
669
  { className: "skill-cards-grid" },
670
- ...skillFiles.map((skill) =>
671
- createSkillCard(skill, templates.skill),
672
- ),
670
+ ...skillFiles.map((skill) => createSkillCard(skill, templates)),
673
671
  )
674
672
  : p(
675
673
  { className: "text-muted" },
@@ -716,6 +714,7 @@ function createAgentCard(stage, profile, stages, agentTemplate, _derived) {
716
714
  content,
717
715
  filename: profile.filename,
718
716
  maxHeight: 400,
717
+ open: true,
719
718
  }),
720
719
  ),
721
720
  );
@@ -724,28 +723,58 @@ function createAgentCard(stage, profile, stages, agentTemplate, _derived) {
724
723
  }
725
724
 
726
725
  /**
727
- * Create a skill card
726
+ * Create a skill card with tabbed file viewer
728
727
  * @param {Object} skill - Skill with frontmatter and body
729
- * @param {string} skillTemplate - Mustache template for skill
728
+ * @param {{skill: string, install: string, reference: string}} templates - Mustache templates
730
729
  * @returns {HTMLElement}
731
730
  */
732
- function createSkillCard(skill, skillTemplate) {
733
- const content = formatAgentSkill(skill, skillTemplate);
731
+ function createSkillCard(skill, templates) {
732
+ const content = formatAgentSkill(skill, templates.skill);
734
733
  const filename = `${skill.dirname}/SKILL.md`;
735
734
 
735
+ // Build files array for the tabbed viewer
736
+ /** @type {import('../components/skill-file-viewer.js').SkillFile[]} */
737
+ const files = [
738
+ {
739
+ filename,
740
+ content,
741
+ language: "markdown",
742
+ },
743
+ ];
744
+
745
+ if (skill.installScript) {
746
+ files.push({
747
+ filename: `${skill.dirname}/scripts/install.sh`,
748
+ content: formatInstallScript(skill, templates.install),
749
+ language: "bash",
750
+ });
751
+ }
752
+
753
+ if (skill.implementationReference) {
754
+ files.push({
755
+ filename: `${skill.dirname}/references/REFERENCE.md`,
756
+ content: formatReference(skill, templates.reference),
757
+ language: "markdown",
758
+ });
759
+ }
760
+
761
+ // Count total files for badge
762
+ const fileCount = files.length;
763
+ const headerChildren = [
764
+ span({ className: "skill-card-name" }, skill.frontmatter.name),
765
+ ];
766
+ if (fileCount > 1) {
767
+ headerChildren.push(
768
+ span({ className: "skill-card-badge" }, `${fileCount} files`),
769
+ );
770
+ }
771
+
736
772
  return div(
737
773
  { className: "skill-card" },
738
- div(
739
- { className: "skill-card-header" },
740
- span({ className: "skill-card-name" }, skill.frontmatter.name),
741
- ),
774
+ div({ className: "skill-card-header" }, ...headerChildren),
742
775
  div(
743
776
  { className: "skill-card-preview" },
744
- createCodeDisplay({
745
- content,
746
- filename,
747
- maxHeight: 300,
748
- }),
777
+ createSkillFileViewer({ files, maxHeight: 300 }),
749
778
  ),
750
779
  );
751
780
  }
@@ -787,10 +816,27 @@ function createDownloadAllButton(
787
816
  zip.file(`.github/agents/${profile.filename}`, content);
788
817
  }
789
818
 
790
- // Add skills
819
+ // Add skills (SKILL.md + optional install script + optional reference)
791
820
  for (const skill of skillFiles) {
792
821
  const content = formatAgentSkill(skill, templates.skill);
793
822
  zip.file(`.claude/skills/${skill.dirname}/SKILL.md`, content);
823
+
824
+ if (skill.installScript) {
825
+ const installContent = formatInstallScript(skill, templates.install);
826
+ zip.file(
827
+ `.claude/skills/${skill.dirname}/scripts/install.sh`,
828
+ installContent,
829
+ { unixPermissions: "755" },
830
+ );
831
+ }
832
+
833
+ if (skill.implementationReference) {
834
+ const refContent = formatReference(skill, templates.reference);
835
+ zip.file(
836
+ `.claude/skills/${skill.dirname}/references/REFERENCE.md`,
837
+ refContent,
838
+ );
839
+ }
794
840
  }
795
841
 
796
842
  // Add VS Code settings
@@ -870,10 +916,27 @@ function createDownloadSingleButton(
870
916
  const content = formatAgentProfile(profile, templates.agent);
871
917
  zip.file(`.github/agents/${profile.filename}`, content);
872
918
 
873
- // Add skills
919
+ // Add skills (SKILL.md + optional install script + optional reference)
874
920
  for (const skill of skillFiles) {
875
921
  const skillContent = formatAgentSkill(skill, templates.skill);
876
922
  zip.file(`.claude/skills/${skill.dirname}/SKILL.md`, skillContent);
923
+
924
+ if (skill.installScript) {
925
+ const installContent = formatInstallScript(skill, templates.install);
926
+ zip.file(
927
+ `.claude/skills/${skill.dirname}/scripts/install.sh`,
928
+ installContent,
929
+ { unixPermissions: "755" },
930
+ );
931
+ }
932
+
933
+ if (skill.implementationReference) {
934
+ const refContent = formatReference(skill, templates.reference);
935
+ zip.file(
936
+ `.claude/skills/${skill.dirname}/references/REFERENCE.md`,
937
+ refContent,
938
+ );
939
+ }
877
940
  }
878
941
 
879
942
  // Add VS Code settings
@@ -14,6 +14,23 @@ import {
14
14
  getCapabilityEmoji,
15
15
  getConceptEmoji,
16
16
  } from "@forwardimpact/schema/levels";
17
+ import { generateSkillMarkdown } from "@forwardimpact/model";
18
+ import { formatAgentSkill } from "../formatters/agent/skill.js";
19
+
20
+ /** @type {string|null} Cached skill template */
21
+ let skillTemplateCache = null;
22
+
23
+ /**
24
+ * Load skill Mustache template with caching
25
+ * @returns {Promise<string>}
26
+ */
27
+ async function getSkillTemplate() {
28
+ if (!skillTemplateCache) {
29
+ const res = await fetch("./templates/skill.template.md");
30
+ skillTemplateCache = await res.text();
31
+ }
32
+ return skillTemplateCache;
33
+ }
17
34
 
18
35
  /**
19
36
  * Render skills list page
@@ -69,7 +86,7 @@ export function renderSkillsList() {
69
86
  * Render skill detail page
70
87
  * @param {Object} params - Route params
71
88
  */
72
- export function renderSkillDetail(params) {
89
+ export async function renderSkillDetail(params) {
73
90
  const { data } = getState();
74
91
  const skill = data.skills.find((s) => s.id === params.id);
75
92
 
@@ -83,6 +100,14 @@ export function renderSkillDetail(params) {
83
100
  return;
84
101
  }
85
102
 
103
+ // Generate SKILL.md content if skill has an agent section
104
+ let agentSkillContent;
105
+ if (skill.agent) {
106
+ const template = await getSkillTemplate();
107
+ const skillData = generateSkillMarkdown(skill, data.stages);
108
+ agentSkillContent = formatAgentSkill(skillData, template);
109
+ }
110
+
86
111
  // Use DOM formatter - it handles transformation internally
87
112
  render(
88
113
  skillToDOM(skill, {
@@ -90,6 +115,7 @@ export function renderSkillDetail(params) {
90
115
  tracks: data.tracks,
91
116
  drivers: data.drivers,
92
117
  capabilities: data.capabilities,
118
+ agentSkillContent,
93
119
  }),
94
120
  );
95
121
  }