@forwardimpact/pathway 0.1.0 → 0.2.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 (131) hide show
  1. package/app/commands/agent.js +109 -21
  2. package/app/commands/command-factory.js +3 -3
  3. package/app/commands/interview.js +14 -7
  4. package/app/commands/job.js +43 -29
  5. package/app/commands/progress.js +14 -7
  6. package/app/commands/serve.js +5 -0
  7. package/app/commands/stage.js +0 -10
  8. package/app/commands/track.js +5 -8
  9. package/app/components/builder.js +111 -27
  10. package/app/css/components/surfaces.css +16 -0
  11. package/app/formatters/agent/profile.js +113 -87
  12. package/app/formatters/agent/skill.js +64 -31
  13. package/app/formatters/behaviour/dom.js +3 -0
  14. package/app/formatters/behaviour/microdata.js +106 -0
  15. package/app/formatters/discipline/dom.js +28 -1
  16. package/app/formatters/discipline/microdata.js +117 -0
  17. package/app/formatters/discipline/shared.js +49 -8
  18. package/app/formatters/driver/dom.js +3 -0
  19. package/app/formatters/driver/microdata.js +91 -0
  20. package/app/formatters/grade/dom.js +3 -0
  21. package/app/formatters/grade/microdata.js +151 -0
  22. package/app/formatters/index.js +32 -1
  23. package/app/formatters/interview/shared.js +13 -8
  24. package/app/formatters/job/description.js +5 -3
  25. package/app/formatters/json-ld.js +242 -0
  26. package/app/formatters/microdata-shared.js +184 -0
  27. package/app/formatters/progress/shared.js +14 -11
  28. package/app/formatters/skill/dom.js +3 -0
  29. package/app/formatters/skill/microdata.js +151 -0
  30. package/app/formatters/stage/dom.js +3 -18
  31. package/app/formatters/stage/microdata.js +110 -0
  32. package/app/formatters/stage/shared.js +0 -27
  33. package/app/formatters/track/dom.js +5 -30
  34. package/app/formatters/track/markdown.js +2 -25
  35. package/app/formatters/track/microdata.js +111 -0
  36. package/app/formatters/track/shared.js +6 -58
  37. package/app/handout-main.js +26 -12
  38. package/app/index.html +11 -0
  39. package/app/lib/card-mappers.js +17 -12
  40. package/app/lib/job-cache.js +12 -9
  41. package/app/lib/template-loader.js +66 -0
  42. package/app/lib/yaml-loader.js +25 -8
  43. package/app/main.js +8 -4
  44. package/app/model/agent.js +158 -130
  45. package/app/model/checklist.js +57 -91
  46. package/app/model/derivation.js +135 -68
  47. package/app/model/index-generator.js +1 -7
  48. package/app/model/job.js +19 -13
  49. package/app/model/levels.js +20 -12
  50. package/app/model/loader.js +41 -17
  51. package/app/model/matching.js +33 -3
  52. package/app/model/profile.js +38 -45
  53. package/app/model/schema-validation.js +438 -0
  54. package/app/model/validation.js +747 -68
  55. package/app/pages/agent-builder.js +119 -25
  56. package/app/pages/assessment-results.js +10 -4
  57. package/app/pages/discipline.js +36 -6
  58. package/app/pages/driver.js +9 -47
  59. package/app/pages/interview-builder.js +3 -1
  60. package/app/pages/interview.js +15 -4
  61. package/app/pages/job-builder.js +4 -1
  62. package/app/pages/job.js +15 -4
  63. package/app/pages/landing.js +10 -10
  64. package/app/pages/progress-builder.js +3 -1
  65. package/app/pages/progress.js +72 -21
  66. package/app/pages/stage.js +3 -126
  67. package/app/slide-main.js +45 -17
  68. package/app/slides/index.js +3 -1
  69. package/app/slides/overview.js +40 -4
  70. package/app/slides/progress.js +4 -2
  71. package/bin/pathway.js +18 -64
  72. package/examples/agents/.claude/skills/architecture-design/SKILL.md +58 -16
  73. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +59 -18
  74. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +58 -17
  75. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +64 -18
  76. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +59 -15
  77. package/examples/agents/.claude/skills/sre-practices/SKILL.md +64 -18
  78. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +58 -17
  79. package/examples/agents/.github/agents/se-platform-code.agent.md +39 -88
  80. package/examples/agents/.github/agents/se-platform-plan.agent.md +41 -88
  81. package/examples/agents/.github/agents/se-platform-review.agent.md +38 -15
  82. package/examples/agents/.vscode/settings.json +1 -1
  83. package/examples/behaviours/outcome_ownership.yaml +1 -2
  84. package/examples/behaviours/polymathic_knowledge.yaml +1 -2
  85. package/examples/behaviours/precise_communication.yaml +1 -2
  86. package/examples/behaviours/relentless_curiosity.yaml +1 -2
  87. package/examples/behaviours/systems_thinking.yaml +1 -2
  88. package/examples/capabilities/business.yaml +80 -142
  89. package/examples/capabilities/delivery.yaml +155 -219
  90. package/examples/capabilities/people.yaml +2 -34
  91. package/examples/capabilities/reliability.yaml +161 -80
  92. package/examples/capabilities/scale.yaml +234 -252
  93. package/examples/copilot-setup-steps.yaml +25 -0
  94. package/examples/devcontainer.yaml +21 -0
  95. package/examples/disciplines/_index.yaml +1 -0
  96. package/examples/disciplines/data_engineering.yaml +14 -12
  97. package/examples/disciplines/engineering_management.yaml +63 -0
  98. package/examples/disciplines/software_engineering.yaml +14 -12
  99. package/examples/drivers.yaml +1 -4
  100. package/examples/framework.yaml +1 -2
  101. package/examples/grades.yaml +1 -3
  102. package/examples/questions/behaviours/outcome_ownership.yaml +1 -2
  103. package/examples/questions/behaviours/polymathic_knowledge.yaml +1 -2
  104. package/examples/questions/behaviours/precise_communication.yaml +1 -2
  105. package/examples/questions/behaviours/relentless_curiosity.yaml +1 -2
  106. package/examples/questions/behaviours/systems_thinking.yaml +1 -2
  107. package/examples/questions/skills/architecture_design.yaml +1 -2
  108. package/examples/questions/skills/cloud_platforms.yaml +1 -2
  109. package/examples/questions/skills/code_quality.yaml +1 -2
  110. package/examples/questions/skills/data_modeling.yaml +1 -2
  111. package/examples/questions/skills/devops.yaml +1 -2
  112. package/examples/questions/skills/full_stack_development.yaml +1 -2
  113. package/examples/questions/skills/sre_practices.yaml +1 -2
  114. package/examples/questions/skills/stakeholder_management.yaml +1 -2
  115. package/examples/questions/skills/team_collaboration.yaml +1 -2
  116. package/examples/questions/skills/technical_writing.yaml +1 -2
  117. package/examples/self-assessments.yaml +1 -3
  118. package/examples/stages.yaml +101 -46
  119. package/examples/tracks/_index.yaml +0 -1
  120. package/examples/tracks/platform.yaml +8 -13
  121. package/examples/tracks/sre.yaml +8 -18
  122. package/examples/vscode-settings.yaml +2 -7
  123. package/package.json +9 -3
  124. package/templates/agent.template.md +65 -0
  125. package/templates/skill.template.md +28 -0
  126. package/examples/agents/.claude/skills/data-modeling/SKILL.md +0 -99
  127. package/examples/agents/.claude/skills/developer-experience/SKILL.md +0 -99
  128. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +0 -100
  129. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +0 -102
  130. package/examples/agents/.claude/skills/technical-writing/SKILL.md +0 -129
  131. package/examples/tracks/manager.yaml +0 -53
@@ -12,6 +12,8 @@ import {
12
12
  button,
13
13
  label,
14
14
  section,
15
+ select,
16
+ option,
15
17
  } from "../lib/render.js";
16
18
  import { getState } from "../lib/state.js";
17
19
  import { createBadge } from "./card.js";
@@ -89,9 +91,70 @@ export function createBuilder({
89
91
  buttonText,
90
92
  );
91
93
 
94
+ // Track select element - created once, options updated when discipline changes
95
+ const trackSelectEl = select(
96
+ { className: "form-select", id: "track-select" },
97
+ option({ value: "" }, "Generalist"),
98
+ );
99
+ // Initially disabled until discipline is selected
100
+ trackSelectEl.disabled = true;
101
+
102
+ /**
103
+ * Check if a discipline allows trackless (generalist) jobs
104
+ * @param {Object|null} disciplineObj
105
+ * @returns {boolean}
106
+ */
107
+ function allowsTrackless(disciplineObj) {
108
+ if (!disciplineObj) return false;
109
+ const validTracks = disciplineObj.validTracks ?? [];
110
+ // Empty array = trackless only (legacy), or null in array = trackless allowed
111
+ return validTracks.length === 0 || validTracks.includes(null);
112
+ }
113
+
114
+ /**
115
+ * Get available tracks for a discipline (excludes null entries)
116
+ * @param {Object|null} disciplineObj
117
+ * @returns {Array}
118
+ */
119
+ function getAvailableTracks(disciplineObj) {
120
+ if (!disciplineObj) return [];
121
+ const validTracks = disciplineObj.validTracks ?? [];
122
+ if (validTracks.length === 0) return [];
123
+ // Filter to actual track IDs (exclude null which means "trackless")
124
+ const trackIds = validTracks.filter((t) => t !== null);
125
+ return data.tracks.filter((t) => trackIds.includes(t.id));
126
+ }
127
+
128
+ /**
129
+ * Update track select options based on selected discipline
130
+ * @param {string} disciplineId
131
+ */
132
+ function updateTrackOptions(disciplineId) {
133
+ const disciplineObj = data.disciplines.find((d) => d.id === disciplineId);
134
+ const availableTracks = getAvailableTracks(disciplineObj);
135
+ const canBeTrackless = allowsTrackless(disciplineObj);
136
+
137
+ // Clear existing options
138
+ trackSelectEl.innerHTML = "";
139
+
140
+ // Add generalist option if trackless is allowed
141
+ if (canBeTrackless) {
142
+ trackSelectEl.appendChild(option({ value: "" }, "Generalist"));
143
+ }
144
+
145
+ // Add available track options
146
+ availableTracks.forEach((t) => {
147
+ trackSelectEl.appendChild(option({ value: t.id }, t.name));
148
+ });
149
+
150
+ // Disable if no options (neither trackless nor tracks)
151
+ trackSelectEl.disabled = !canBeTrackless && availableTracks.length === 0;
152
+ }
153
+
92
154
  // Subscribe to selection changes - all updates happen here
93
155
  selection.subscribe(({ discipline, track, grade }) => {
94
- if (!discipline || !track || !grade) {
156
+ // Track is now optional - only discipline and grade are required
157
+ if (!discipline || !grade) {
95
158
  previewContainer.innerHTML = "";
96
159
  previewContainer.appendChild(
97
160
  p({ className: "text-muted" }, emptyPreviewText),
@@ -101,10 +164,10 @@ export function createBuilder({
101
164
  }
102
165
 
103
166
  const disciplineObj = data.disciplines.find((d) => d.id === discipline);
104
- const trackObj = data.tracks.find((t) => t.id === track);
167
+ const trackObj = track ? data.tracks.find((t) => t.id === track) : null;
105
168
  const gradeObj = data.grades.find((g) => g.id === grade);
106
169
 
107
- if (!disciplineObj || !trackObj || !gradeObj) {
170
+ if (!disciplineObj || !gradeObj) {
108
171
  previewContainer.innerHTML = "";
109
172
  previewContainer.appendChild(
110
173
  p({ className: "text-muted" }, "Invalid selection. Please try again."),
@@ -146,7 +209,7 @@ export function createBuilder({
146
209
  h2({}, formTitle),
147
210
  div(
148
211
  { className: "auto-grid-sm gap-lg" },
149
- // Discipline selector
212
+ // Discipline selector (first)
150
213
  div(
151
214
  { className: "form-group" },
152
215
  label({ className: "form-label" }, labels.discipline || "Discipline"),
@@ -156,27 +219,19 @@ export function createBuilder({
156
219
  initialValue: selection.get().discipline,
157
220
  placeholder: "Select a discipline...",
158
221
  onChange: (value) => {
159
- selection.update((prev) => ({ ...prev, discipline: value }));
222
+ // Update track options when discipline changes
223
+ updateTrackOptions(value);
224
+ // Reset track selection when discipline changes
225
+ selection.update((prev) => ({
226
+ ...prev,
227
+ discipline: value,
228
+ track: "",
229
+ }));
160
230
  },
161
231
  getDisplayName: (d) => d.specialization || d.name,
162
232
  }),
163
233
  ),
164
- // Track selector
165
- div(
166
- { className: "form-group" },
167
- label({ className: "form-label" }, labels.track || "Track"),
168
- createSelectWithValue({
169
- id: "track-select",
170
- items: data.tracks,
171
- initialValue: selection.get().track,
172
- placeholder: "Select a track...",
173
- onChange: (value) => {
174
- selection.update((prev) => ({ ...prev, track: value }));
175
- },
176
- getDisplayName: (t) => t.name,
177
- }),
178
- ),
179
- // Grade selector
234
+ // Grade selector (second)
180
235
  div(
181
236
  { className: "form-group" },
182
237
  label({ className: "form-label" }, labels.grade || "Grade"),
@@ -191,6 +246,31 @@ export function createBuilder({
191
246
  getDisplayName: (g) => g.id,
192
247
  }),
193
248
  ),
249
+ // Track selector (third, optional)
250
+ div(
251
+ { className: "form-group" },
252
+ label(
253
+ { className: "form-label" },
254
+ labels.track || "Track (optional)",
255
+ ),
256
+ (() => {
257
+ // Wire up track select change handler
258
+ trackSelectEl.addEventListener("change", (e) => {
259
+ selection.update((prev) => ({ ...prev, track: e.target.value }));
260
+ });
261
+ // Initialize track options if discipline is pre-selected
262
+ const initialDiscipline = selection.get().discipline;
263
+ if (initialDiscipline) {
264
+ updateTrackOptions(initialDiscipline);
265
+ // Set initial track value if provided
266
+ const initialTrack = selection.get().track;
267
+ if (initialTrack) {
268
+ trackSelectEl.value = initialTrack;
269
+ }
270
+ }
271
+ return trackSelectEl;
272
+ })(),
273
+ ),
194
274
  ),
195
275
  previewContainer,
196
276
  div({ className: "page-actions" }, actionButton),
@@ -289,6 +369,15 @@ export function createProgressPreview(preview, selection) {
289
369
 
290
370
  const { discipline, grade, track } = selection;
291
371
 
372
+ // Build badges array - track is optional
373
+ const badges = [
374
+ createBadge(discipline.specialization, "discipline"),
375
+ createBadge(grade.id, "grade"),
376
+ ];
377
+ if (track) {
378
+ badges.push(createBadge(track.name, "track"));
379
+ }
380
+
292
381
  return div(
293
382
  { className: "job-preview-content" },
294
383
  div(
@@ -296,12 +385,7 @@ export function createProgressPreview(preview, selection) {
296
385
  div({ className: "preview-label" }, "Current Role"),
297
386
  div({ className: "preview-title" }, preview.title),
298
387
  ),
299
- div(
300
- { className: "preview-badges" },
301
- createBadge(discipline.specialization, "discipline"),
302
- createBadge(grade.id, "grade"),
303
- createBadge(track.name, "track"),
304
- ),
388
+ div({ className: "preview-badges" }, ...badges),
305
389
  div(
306
390
  { className: "preview-section", style: "margin-top: 1rem" },
307
391
  div({ className: "preview-label" }, "Progression Paths Available"),
@@ -240,4 +240,20 @@
240
240
  #app-footer p {
241
241
  margin: 0 0 var(--space-sm) 0;
242
242
  }
243
+
244
+ .footer-links {
245
+ display: flex;
246
+ gap: var(--space-md);
247
+ justify-content: center;
248
+ }
249
+
250
+ .footer-links a {
251
+ color: var(--color-text-muted);
252
+ text-decoration: none;
253
+ }
254
+
255
+ .footer-links a:hover {
256
+ color: var(--color-primary);
257
+ text-decoration: underline;
258
+ }
243
259
  }
@@ -3,116 +3,62 @@
3
3
  *
4
4
  * Formats agent profile data into .agent.md file content
5
5
  * following the GitHub Copilot Custom Agents specification.
6
+ *
7
+ * Uses Mustache templates for flexible output formatting.
8
+ * Templates are loaded from data/ directory with fallback to templates/ directory.
6
9
  */
7
10
 
8
- /**
9
- * Format YAML frontmatter value
10
- * @param {any} value - Value to format
11
- * @returns {string} Formatted value
12
- */
13
- function formatYamlValue(value) {
14
- if (Array.isArray(value)) {
15
- return JSON.stringify(value);
16
- }
17
- if (typeof value === "boolean") {
18
- return String(value);
19
- }
20
- if (typeof value === "string") {
21
- // Quote strings that contain special characters or newlines
22
- if (value.includes("\n") || value.includes(":") || value.includes("#")) {
23
- return `"${value.replace(/"/g, '\\"')}"`;
24
- }
25
- return value;
26
- }
27
- return String(value);
28
- }
29
-
30
- /**
31
- * Format handoffs array as YAML
32
- * @param {Array} handoffs - Array of handoff objects
33
- * @returns {string[]} YAML lines for handoffs
34
- */
35
- function formatHandoffs(handoffs) {
36
- const lines = ["handoffs:"];
37
- for (const handoff of handoffs) {
38
- lines.push(` - label: ${formatYamlValue(handoff.label)}`);
39
- if (handoff.agent) {
40
- lines.push(` agent: ${formatYamlValue(handoff.agent)}`);
41
- }
42
-
43
- // Format prompt as single-line string, replacing newlines with spaces
44
- const singleLinePrompt = handoff.prompt
45
- .replace(/\n\n+/g, " ")
46
- .replace(/\n/g, " ")
47
- .replace(/\s+/g, " ")
48
- .trim();
49
- lines.push(` prompt: ${formatYamlValue(singleLinePrompt)}`);
50
-
51
- if (handoff.send !== undefined) {
52
- lines.push(` send: ${formatYamlValue(handoff.send)}`);
53
- }
54
- }
55
- return lines;
56
- }
11
+ import Mustache from "mustache";
57
12
 
58
13
  /**
59
- * Format agent profile as .agent.md file content
60
- * @param {Object} profile - Profile with frontmatter and body
14
+ * Format agent profile as .agent.md file content using Mustache template
15
+ * @param {Object} profile - Profile with frontmatter and bodyData
61
16
  * @param {Object} profile.frontmatter - YAML frontmatter data
62
17
  * @param {string} profile.frontmatter.name - Agent name
63
18
  * @param {string} profile.frontmatter.description - Agent description
64
19
  * @param {string[]} profile.frontmatter.tools - Available tools
65
20
  * @param {boolean} profile.frontmatter.infer - Whether to auto-select
66
21
  * @param {Array} [profile.frontmatter.handoffs] - Handoff definitions
67
- * @param {string} profile.body - Markdown body content
22
+ * @param {Object} profile.bodyData - Structured body data
23
+ * @param {string} profile.bodyData.title - Agent title (e.g. "Software Engineering - Platform - Plan Agent")
24
+ * @param {string} profile.bodyData.stageDescription - Stage description text
25
+ * @param {string} profile.bodyData.identity - Core identity text
26
+ * @param {string} [profile.bodyData.priority] - Priority/philosophy statement (optional)
27
+ * @param {string[]} profile.bodyData.capabilities - List of capability names
28
+ * @param {Array<{index: number, text: string}>} profile.bodyData.beforeMakingChanges - Numbered steps
29
+ * @param {string} [profile.bodyData.delegation] - Delegation guidance (optional)
30
+ * @param {string} profile.bodyData.operationalContext - Operational context text
31
+ * @param {string} profile.bodyData.workingStyle - Working style markdown section
32
+ * @param {string} [profile.bodyData.beforeHandoff] - Before handoff checklist markdown (optional)
33
+ * @param {string[]} profile.bodyData.constraints - List of constraints
34
+ * @param {string} template - Mustache template string
68
35
  * @returns {string} Complete .agent.md file content
69
36
  */
70
- export function formatAgentProfile({ frontmatter, body }) {
71
- const lines = ["---"];
72
-
73
- // Name (optional but recommended)
74
- if (frontmatter.name) {
75
- lines.push(`name: ${formatYamlValue(frontmatter.name)}`);
76
- }
77
-
78
- // Description (required)
79
- lines.push(`description: ${formatYamlValue(frontmatter.description)}`);
80
-
81
- // Tools (optional, defaults to all)
82
- if (frontmatter.tools && frontmatter.tools.length > 0) {
83
- lines.push(`tools: ${formatYamlValue(frontmatter.tools)}`);
84
- }
85
-
86
- // Infer (optional)
87
- if (frontmatter.infer !== undefined) {
88
- lines.push(`infer: ${formatYamlValue(frontmatter.infer)}`);
89
- }
90
-
91
- // Handoffs (optional)
92
- if (frontmatter.handoffs && frontmatter.handoffs.length > 0) {
93
- lines.push(...formatHandoffs(frontmatter.handoffs));
94
- }
95
-
96
- lines.push("---");
97
- lines.push("");
98
- lines.push(body);
99
-
100
- return lines.join("\n");
37
+ export function formatAgentProfile({ frontmatter, bodyData }, template) {
38
+ const data = {
39
+ // Frontmatter
40
+ name: frontmatter.name,
41
+ description: frontmatter.description,
42
+ infer: frontmatter.infer,
43
+ handoffs: frontmatter.handoffs || [],
44
+ // Body data
45
+ ...bodyData,
46
+ };
47
+ return Mustache.render(template, data);
101
48
  }
102
49
 
103
50
  /**
104
51
  * Format agent profile for CLI output (markdown)
105
- * @param {Object} profile - Profile with frontmatter and body
52
+ * @param {Object} profile - Profile with frontmatter and bodyData
106
53
  * @returns {string} Markdown formatted for CLI display
107
54
  */
108
- export function formatAgentProfileForCli({ frontmatter, body }) {
55
+ export function formatAgentProfileForCli({ frontmatter, bodyData }) {
109
56
  const lines = [];
110
57
 
111
58
  lines.push(`# Agent Profile: ${frontmatter.name}`);
112
59
  lines.push("");
113
60
  lines.push(`**Description:** ${frontmatter.description}`);
114
61
  lines.push("");
115
- lines.push(`**Tools:** ${frontmatter.tools.join(", ")}`);
116
62
  lines.push(`**Infer:** ${frontmatter.infer}`);
117
63
 
118
64
  if (frontmatter.handoffs && frontmatter.handoffs.length > 0) {
@@ -127,7 +73,87 @@ export function formatAgentProfileForCli({ frontmatter, body }) {
127
73
  lines.push("");
128
74
  lines.push("---");
129
75
  lines.push("");
130
- lines.push(body);
76
+
77
+ // Render structured body data
78
+ lines.push(`# ${bodyData.title}`);
79
+ lines.push("");
80
+ lines.push(bodyData.stageDescription);
81
+ lines.push("");
82
+
83
+ lines.push("## Core Identity");
84
+ lines.push("");
85
+ lines.push(bodyData.identity);
86
+ lines.push("");
87
+
88
+ if (bodyData.priority) {
89
+ lines.push(bodyData.priority);
90
+ lines.push("");
91
+ }
92
+
93
+ if (bodyData.capabilities && bodyData.capabilities.length > 0) {
94
+ lines.push("Your primary capabilities:");
95
+ for (const cap of bodyData.capabilities) {
96
+ lines.push(`- ${cap}`);
97
+ }
98
+ lines.push("");
99
+ }
100
+
101
+ if (bodyData.beforeMakingChanges && bodyData.beforeMakingChanges.length > 0) {
102
+ lines.push("Before making changes:");
103
+ for (const step of bodyData.beforeMakingChanges) {
104
+ lines.push(`${step.index}. ${step.text}`);
105
+ }
106
+ lines.push("");
107
+ }
108
+
109
+ if (bodyData.delegation) {
110
+ lines.push("## Delegation");
111
+ lines.push("");
112
+ lines.push(bodyData.delegation);
113
+ lines.push("");
114
+ }
115
+
116
+ lines.push("## Operational Context");
117
+ lines.push("");
118
+ lines.push(bodyData.operationalContext);
119
+ lines.push("");
120
+
121
+ lines.push(bodyData.workingStyle);
122
+
123
+ if (bodyData.beforeHandoff) {
124
+ lines.push("## Before Handoff");
125
+ lines.push("");
126
+ lines.push(
127
+ "Before offering a handoff, verify and summarize completion of these items:",
128
+ );
129
+ lines.push("");
130
+ lines.push(bodyData.beforeHandoff);
131
+ lines.push("");
132
+ lines.push(
133
+ "When verified, summarize what was accomplished then offer the handoff.",
134
+ );
135
+ lines.push("If items are incomplete, explain what remains.");
136
+ lines.push("");
137
+ }
138
+
139
+ lines.push("## Return Format");
140
+ lines.push("");
141
+ lines.push("When completing work (for handoff or as a subagent), provide:");
142
+ lines.push("");
143
+ lines.push("1. **Work completed**: What was accomplished");
144
+ lines.push(
145
+ "2. **Checklist status**: Items verified from Before Handoff section",
146
+ );
147
+ lines.push("3. **Recommendation**: Ready for next stage, or needs more work");
148
+ lines.push("");
149
+
150
+ if (bodyData.constraints && bodyData.constraints.length > 0) {
151
+ lines.push("## Constraints");
152
+ lines.push("");
153
+ for (const constraint of bodyData.constraints) {
154
+ lines.push(`- ${constraint}`);
155
+ }
156
+ }
131
157
 
132
158
  return lines.join("\n");
133
159
  }
@@ -3,56 +3,89 @@
3
3
  *
4
4
  * Formats agent skill data into SKILL.md file content
5
5
  * following the Agent Skills Standard specification.
6
+ *
7
+ * Uses Mustache templates for flexible output formatting.
8
+ * Templates are loaded from data/ directory with fallback to templates/ directory.
6
9
  */
7
10
 
11
+ import Mustache from "mustache";
12
+
8
13
  /**
9
- * Format agent skill as SKILL.md file content
10
- * @param {Object} skill - Skill with frontmatter and body
14
+ * Format agent skill as SKILL.md file content using Mustache template
15
+ * @param {Object} skill - Skill with frontmatter, title, stages, reference
11
16
  * @param {Object} skill.frontmatter - YAML frontmatter data
12
17
  * @param {string} skill.frontmatter.name - Skill name (required)
13
18
  * @param {string} skill.frontmatter.description - Skill description (required)
14
- * @param {string} skill.body - Markdown body content
19
+ * @param {string} skill.title - Human-readable skill title for heading
20
+ * @param {Array} skill.stages - Array of stage objects with stageName, focus, activities, ready
21
+ * @param {string} skill.reference - Reference content (markdown)
22
+ * @param {string} template - Mustache template string
15
23
  * @returns {string} Complete SKILL.md file content
16
24
  */
17
- export function formatAgentSkill({ frontmatter, body }) {
18
- const lines = ["---"];
19
-
20
- // Name (required)
21
- lines.push(`name: ${frontmatter.name}`);
22
-
23
- // Description (required) - handle multiline
24
- const description = frontmatter.description.trim();
25
- if (description.includes("\n")) {
26
- lines.push("description: |");
27
- for (const line of description.split("\n")) {
28
- lines.push(` ${line}`);
29
- }
30
- } else {
31
- lines.push(`description: ${description}`);
32
- }
33
-
34
- lines.push("---");
35
- lines.push("");
36
- lines.push(body);
37
-
38
- return lines.join("\n");
25
+ export function formatAgentSkill(
26
+ { frontmatter, title, stages, reference },
27
+ template,
28
+ ) {
29
+ const data = {
30
+ name: frontmatter.name,
31
+ descriptionLines: frontmatter.description.trim().split("\n"),
32
+ title,
33
+ stages,
34
+ reference: reference ? reference.trim() : "",
35
+ };
36
+ return Mustache.render(template, data);
39
37
  }
40
38
 
41
39
  /**
42
40
  * Format agent skill for CLI output (markdown)
43
- * @param {Object} skill - Skill with frontmatter and body
41
+ * @param {Object} skill - Skill with frontmatter, title, stages, reference
44
42
  * @returns {string} Markdown formatted for CLI display
45
43
  */
46
- export function formatAgentSkillForCli({ frontmatter, body }) {
44
+ export function formatAgentSkillForCli({
45
+ frontmatter,
46
+ title,
47
+ stages,
48
+ reference,
49
+ }) {
47
50
  const lines = [];
48
51
 
49
- lines.push(`# Skill: ${frontmatter.name}`);
52
+ lines.push(`# ${title}`);
50
53
  lines.push("");
51
- lines.push(`**Description:** ${frontmatter.description.trim()}`);
54
+ lines.push(`**Name:** ${frontmatter.name}`);
52
55
  lines.push("");
53
- lines.push("---");
56
+ lines.push(`**Description:** ${frontmatter.description.trim()}`);
54
57
  lines.push("");
55
- lines.push(body);
58
+
59
+ if (stages && stages.length > 0) {
60
+ lines.push("## Stage Guidance");
61
+ lines.push("");
62
+ for (const stage of stages) {
63
+ lines.push(`### ${stage.stageName} Stage`);
64
+ lines.push("");
65
+ lines.push(`**Focus:** ${stage.focus.trim()}`);
66
+ lines.push("");
67
+ if (stage.activities && stage.activities.length > 0) {
68
+ lines.push("**Activities:**");
69
+ for (const item of stage.activities) {
70
+ lines.push(`- ${item}`);
71
+ }
72
+ lines.push("");
73
+ }
74
+ if (stage.ready && stage.ready.length > 0) {
75
+ lines.push(`**Ready for ${stage.nextStageName} when:**`);
76
+ for (const item of stage.ready) {
77
+ lines.push(`- [ ] ${item}`);
78
+ }
79
+ lines.push("");
80
+ }
81
+ }
82
+ }
83
+
84
+ if (reference) {
85
+ lines.push("## Reference");
86
+ lines.push("");
87
+ lines.push(reference.trim());
88
+ }
56
89
 
57
90
  return lines.join("\n");
58
91
  }
@@ -23,6 +23,7 @@ import {
23
23
  getConceptEmoji,
24
24
  } from "../../model/levels.js";
25
25
  import { prepareBehaviourDetail } from "./shared.js";
26
+ import { createJsonLdScript, behaviourToJsonLd } from "../json-ld.js";
26
27
 
27
28
  /**
28
29
  * Format behaviour detail as DOM elements
@@ -41,6 +42,8 @@ export function behaviourToDOM(
41
42
  const emoji = getConceptEmoji(framework, "behaviour");
42
43
  return div(
43
44
  { className: "detail-page behaviour-detail" },
45
+ // JSON-LD structured data
46
+ createJsonLdScript(behaviourToJsonLd(behaviour)),
44
47
  // Header
45
48
  div(
46
49
  { className: "page-header" },